Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: Non-linear dimming #133

Open
meijer3 opened this issue Jan 16, 2023 · 4 comments
Open

Idea: Non-linear dimming #133

meijer3 opened this issue Jan 16, 2023 · 4 comments
Assignees
Labels
enhancement New feature or request

Comments

@meijer3
Copy link
Contributor

meijer3 commented Jan 16, 2023

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 MichielVanwelsenaere added the enhancement New feature or request label Jan 17, 2023
@MichielVanwelsenaere
Copy link
Owner

This would indeed be nice.

@MichielVanwelsenaere
Copy link
Owner

can I assign this one to you @meijer3 ?

@meijer3
Copy link
Contributor Author

meijer3 commented Jan 24, 2023

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

@ntphx
Copy link
Contributor

ntphx commented Feb 25, 2024

@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%
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants