Open meijer3 opened 1 year ago
This would indeed be nice.
can I assign this one to you @meijer3 ?
Yes for a first setup, but iam not an expert. :)
@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%
};
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-correctionExtra 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.