MichielVanwelsenaere / HomeAutomation.CoDeSys3

Home Automation system build in CoDeSys 3 with MQTT communication to any third party Home Automation software
MIT License
109 stars 36 forks source link

Idea: Non-linear dimming #133

Open meijer3 opened 1 year ago

meijer3 commented 1 year ago

I see the dimmer FB uses LIN_TRAFO: Util.LIN_TRAFO Most other libraries uses a non-linear like: quadratic cubic or quadruple. I suppose non-linear looks more natural dimming to the human eye. https://github.com/Breina/ha-artnet-led#output-correction image

Extra note: Personally it is hard to time full brightness with a pushbutton right now. A small delay at the top of the curve would help. I also know some devides (like my headlight) blinks shortly when it is at max.

MichielVanwelsenaere commented 1 year ago

This would indeed be nice.

MichielVanwelsenaere commented 1 year ago

can I assign this one to you @meijer3 ?

meijer3 commented 1 year ago

Yes for a first setup, but iam not an expert. :)

ntphx commented 6 months ago

@meijer3 @MichielVanwelsenaere

This python script will generate natural dimmings curves. I'm not the original author so I can't take credit for it.

#!/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# -*- coding: utf-8 -*-

# Y(x) = (1 - C) + C*x**a
# X(y) = (1 - (1-y)/C)**(1/a)
#
# xc = ( yc / (a + yc - a*yc ))^(1/a)
# yc = a*xc^a / (1 - (1-a)*xc^a )
# C = yc/a - yc + 1 = 1/(1+(a-1)*xc^a)

if __name__ == '__main__':
    import argparse
    from sys import stderr, stdout, exit
    from math import floor, ceil

    parser = argparse.ArgumentParser(description="Psychophysical sublinear fitting (0 < EXP < 1)")
    parser.add_argument('smin', metavar='SMIN', type=float, help='Minimum setting')
    parser.add_argument('smax', metavar='SMAX', type=float, help='Maximum setting')
    parser.add_argument('dmin', metavar='DMIN', type=float, help='Minimum device control')
    parser.add_argument('dmax', metavar='DMAX', type=float, help='Maximum device control')
    parser.add_argument('-v', '--verbose', dest='verbose', action='count', default=0, help='Verbose output')
    parser.add_argument('-e', '--exponent', metavar='EXP',   type=float, dest='exponent', default=1.0/3.0, help='Exponent, default 0.333333')
    parser.add_argument('-l', '--linear',   metavar='F',     type=float, dest='linear',   default=0.08,  help='Linear fraction, default 0.08')
    parser.add_argument('-f', '--forward', dest='forward', action='count', default=0, help='Forward conversion table')
    parser.add_argument('-r', '--reverse', dest='reverse', action='count', default=0, help='Reverse conversion table')
    parser.add_argument('-c', dest='code', action='count', default=0, help='Generate C code')
    parser.add_argument('-d', dest='plot', action='count', default=0, help='Display using gnuplot')
    args = parser.parse_args()

    dmin = args.dmin
    dmax = args.dmax
    if round(dmax) <= round(dmin):
        stderr.write('Invalid input range.\n')
        exit(1)

    smin = args.smin
    smax = args.smax
    if round(smax) <= round(smin):
        stderr.write('Invalid output range.\n')
        exit(1)

    exponent = args.exponent
    if exponent <= 0.0:
        stderr.write('Exponent must be positive (and less than 1).\n')
        exit(1)
    if exponent >= 1.0:
        stderr.write('Exponent must be less than 1 (and positive).\n')
        exit(1)

    slinear = args.linear
    if slinear < 0.0 or slinear >= 1.0:
        stderr.write('Linear output fraction must be between 0 and 1.\n')

    if slinear > 0.0:
        dlinear = ( slinear / (exponent + slinear - exponent*slinear))**(1/exponent)
    else:
        dlinear = 0.0

    C = slinear/exponent - slinear + 1

    def device_to_setting(d):
        if d <= 0:
            return 0
        elif d <= dlinear:
            return d*slinear/dlinear
        elif d < 1:
            return (1 - C) + C*(d**exponent)
        else:
            return 1

    def setting_to_device(s):
        if s <= 0:
            return 0
        elif s <= slinear:
            return s*dlinear/slinear
        elif s < 1:
            return (1 - (1 - s)/C)**(1/exponent)
        else:
            return 1

    if args.verbose > 0:
        stdout.write('exponent = %.6f\n' % exponent)
        stdout.write('smin = %.6f, smax = %.6f, slinear = %.6f\n' % (smin, smax, slinear))
        stdout.write('dmin = %.6f, dmax = %.6f, dlinear = %.6f\n' % (dmin, dmax, dlinear))
    if args.verbose > 1:
        stdout.write('C = %.9f\n' % C)

    ismin = round(smin)
    ismax = round(smax)

    idmin = round(dmin)
    idmax = round(dmax)

    if args.forward > 0 or args.reverse > 0:
        if args.forward == 1:
            stdout.write('         Setting | Device control\n')
            stdout.write(' ----------------+----------------\n')
            for s in range(ismin, ismax+1):
                d = round(dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) ))
                stdout.write(' %11d      %11d\n' % (s, d))
            stdout.write('\n')
        elif args.forward == 2:
            stdout.write('         Setting | Device control\n')
            stdout.write(' ----------------+----------------\n')
            for s in range(ismin, ismax+1):
                d = dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) )
                stdout.write(' %11d      %15.4f\n' % (s, d))
            stdout.write('\n')

        if args.reverse == 1:
            stdout.write('  Device control |        Setting\n')
            stdout.write(' ----------------+----------------\n')
            for d in range(idmin, idmax+1):
                s = round(smin + (smax - smin) * device_to_setting( (d - dmin) / (dmax - dmin) ))
                stdout.write(' %11d      %11d\n' % (d, s))
            stdout.write('\n')
        elif args.reverse == 2:
            stdout.write('  Device control |        Setting\n')
            stdout.write(' ----------------+----------------\n')
            for d in range(idmin, idmax+1):
                s = smin + (smax - smin) * device_to_setting( (d - dmin) / (dmax - dmin) )
                stdout.write(' %11d      %15.4f\n' % (d, s))
            stdout.write('\n')

    if args.code:
        stdout.write('\nconst int device_lookup_table[%d] = {\n   ' % (ismax - ismin + 1))
        for s in range(ismin, ismax+1):
            d = round(dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) ))
            stdout.write(' %d,' % d)
        stdout.write('\n};\n\nstatic inline int device_lookup(int setting) {\n')
        stdout.write('    if (setting < %d)\n' % ismin)
        stdout.write('        return %d;\n' % round(dmin))
        stdout.write('    if (setting <= %d)\n' % ismax)
        if ismin != 0:
            stdout.write('        return device_lookup_table[setting - %d];\n' % ismin)
        else:
            stdout.write('        return device_lookup_table[setting];\n')
        stdout.write('    return %d;\n' % round(dmax))
        stdout.write('}\n\n')

    if args.plot:
        import subprocess

        dataset = []
        dataset += [ '$dataset << END\n' ]
        for s in range(ismin, ismax+1):
            d = round(dmin + (dmax - dmin) * setting_to_device( (s - smin) / (smax - smin) ))
            dataset += [ '%.0f %.0f\n' % (d, s) ]
        dataset += [ 'END\n',
                     'set key bottom right\n',
                     'set ylabel "Setting"\n',
                     'set xlabel "Device Control"\n',
                     'plot $dataset u 1:2 t "Generated" w lines lc rgbcolor "#000000",',
                     '     $dataset u 1:2 notitle w points pt 1 lc rgbcolor "#000000",',
                     '     %.6f+%.6f*((x-%.6f)/%.6f)**%.6f t "x^{%.6f}" w lines lc rgbcolor "#FF0000"\n' % (smin, smax-smin, dmin, dmax-dmin, exponent, exponent),
                     'pause mouse\n' ]
        subprocess.run(['gnuplot'], shell=False, encoding='UTF-8', input=''.join(dataset))

Here's an example where a dimming curve across 256 steps is mapped to 100 increments. The scripts takes arguments which allow you to define the pwm value minimums and maximums between the curve should be calculated in x amount of brightness increase or decrease steps.

const uint32_t settings[101]  = {
    0, 0, 1, 1, 1, 1, 2, 2, 2, 3,                         //0% > 9%
    3, 3, 4, 4, 4, 5, 5, 6, 6, 7,                         //10% > 19%
    8, 8, 9, 10, 10, 11, 12, 13, 14, 15,                  //20% > 29%
    16, 17, 18, 19, 21, 22, 23, 24, 26, 27,               //30% > 39%
    29, 30, 32, 34, 35, 37, 39, 41, 43, 45,               //40% > 49%
    47, 49, 52, 54, 56, 59, 61, 64, 66, 69,               //50% > 59%
    72, 75, 78, 81, 84, 87, 90, 94, 97, 101,              //60% > 69%
    104, 108, 112, 116, 120, 124, 128, 132, 136, 141,     //70% > 79%
    145, 150, 154, 159, 164, 169, 174, 179, 184, 190,     //80% > 89%
    195, 201, 207, 212, 218, 224, 230, 237, 243, 249,     //90% > 99%
    255                                                   //100%
};