#!/usr/bin/env python from __future__ import print_function import math interactive = False # supported shapes: ninth, seventh, fifth, cube, square, log #ramp_shape = 'cube' max_pwm = 255 max_pwms = [] dyn_pwm = False def main(args): """Calculates PWM levels for visually-linear steps. """ cli_answers = [] global max_pwm, max_pwms, dyn_pwm pwm_arg = str(max_pwm) i = 0 while i < len(args): a = args[i] if a in ('--pwm',): i += 1 pwm_arg = args[i] else: #print('unrecognized option: "%s"' % (a,)) cli_answers.append(a) i += 1 # Get parameters from the user questions_main = [ (str, 'ramp_shape', 'cube', 'Ramp shape? [cube, square, fifth, seventh, ninth, log, N.NN]'), (int, 'num_channels', 1, 'How many power channels?'), (int, 'num_levels', 4, 'How many total levels do you want?'), ] questions_per_channel = [ (str, 'type', '7135', 'Type of channel - 7135 or FET:'), (int, 'pwm_min', 6, 'Lowest visible PWM level:'), (float, 'lm_min', 0.25, 'How bright is the lowest level, in lumens?'), #(int, 'pwm_max', max_pwm, 'Highest PWM level:'), (float, 'lm_max', 1000, 'How bright is the highest level, in lumens?'), ] def ask(questions, ans): for typ, name, default, text in questions: value = get_value(text, default, cli_answers) if not value: value = default else: value = typ(value) setattr(ans, name, value) answers = Empty() ask(questions_main, answers) if pwm_arg: if pwm_arg.startswith('dyn:'): dyn_pwm = True parts = pwm_arg.split(':') dpwm_steps = int(parts[1]) dpwn_max = int(parts[2]) dpwn_min = int(parts[3]) max_pwms = [dpwn_min] * answers.num_levels for i in range(dpwm_steps): span = dpwn_max - dpwn_min x = dpwn_min + (span * (float(dpwm_steps - i) / dpwm_steps)) max_pwms[i] = int(x) max_pwm = dpwn_min else: val = int(pwm_arg) max_pwm = val max_pwms = [val] * answers.num_levels global ramp_shape ramp_shape = answers.ramp_shape channels = [] if not answers: print('Describe the channels in order of lowest to highest power.') for chan_num in range(answers.num_channels): if not answers: print('===== Channel %s =====' % (chan_num+1)) chan = Empty() chan.pwm_max = max_pwm ask(questions_per_channel, chan) chan.type = chan.type.upper() if chan.type not in ('7135', 'FET'): raise ValueError('Invalid channel type: %s' % (chan.type,)) channels.append(chan) # calculate total output of all previous channels for i, channel in enumerate(channels): channel.prev_lm = 0.0 for j in range(i): if channels[j].type == '7135': channel.prev_lm += channels[j].lm_max # figure out the desired PWM values multi_pwm(answers, channels) if interactive: # Wait on exit, in case user invoked us by clicking an icon print('Press Enter to exit:') input_text() class Empty: pass def multi_pwm(answers, channels): lm_min = channels[0].lm_min # figure out the highest mode lm_max = max([(c.lm_max+c.prev_lm) for c in channels]) if channels[-1].type == 'FET': if channels[-1].lm_max > channels[-1].prev_lm: # assume the highest output is with only the FET enabled lm_max = channels[-1].lm_max else: # this would be a stupid driver design raise ValueError("FET channel isn't the most powerful?") visual_min = invpower(lm_min) visual_max = invpower(lm_max) step_size = (visual_max - visual_min) / (answers.num_levels-1) # Determine ideal lumen levels goals = [] goal_vis = visual_min for i in range(answers.num_levels): goal_lm = power(goal_vis) goals.append((goal_vis, goal_lm)) goal_vis += step_size # Calculate each channel's output for each level for cnum, channel in enumerate(channels): channel.modes = [] for i in range(answers.num_levels): goal_vis, goal_lm = goals[i] # This channel already is maxed out if goal_lm >= (channel.lm_max + channel.prev_lm): # This shouldn't happen, the FET is assumed to be the highest channel if channel.type == 'FET': # this would be a stupid driver design raise ValueError("FET channel isn't the most powerful?") # Handle FET turbo specially if (i == (answers.num_levels - 1)) \ and (cnum < (len(channels)-1)) \ and (channels[-1].type == 'FET'): channel.modes.append(0.0) # Normal non-turbo mode or non-FET turbo else: channel.modes.append(max_pwms[i]) # This channel's active ramp-up range #elif goal_lm > (channel.prev_lm + channel.lm_min): elif goal_lm > channel.prev_lm: # assume 7135 channels all add together if channel.type == '7135': diff = channel.lm_max - channel.lm_min # assume FET channel gets higher output on its own elif channel.type == 'FET': diff = channel.lm_max - channel.prev_lm - channel.lm_min needed = goal_lm - channel.prev_lm - channel.lm_min ceil = max_pwms[i] ratio = needed / diff * (ceil-channel.pwm_min) # if there's wiggle room, adjust ceiling to reduce error #if dyn_pwm: # this_step = max(1, math.floor(ratio)) # next_step = this_step + 0.5 # limit = float(this_step) / next_step * ceil # limit = max(limit, max_pwm) #while (ceil > limit) and ((ratio - math.floor(ratio)) > 0.1): # ceil -= 1 # ratio = needed / diff * (ceil-channel.pwm_min) # max_pwms[i] = ceil if dyn_pwm and (ceil > max_pwm): this_step = max(1, math.floor(ratio)) next_step = this_step + 1 fpart = ratio - math.floor(ratio) correction = (next_step - fpart) / next_step ceil = int(ceil * correction) ratio = needed / diff * (ceil-channel.pwm_min) max_pwms[i] = ceil # save the result pwm = max(0, ratio + channel.pwm_min) channel.modes.append(pwm) # This channel isn't active yet, output too low else: channel.modes.append(0) # Show individual levels in detail for i in range(answers.num_levels): goal_vis, goal_lm = goals[i] pwms = [] for channel in channels: pwms.append('%.2f/%i' % (channel.modes[i], max_pwms[i])) print('%i: visually %.2f (%.2f lm): %s' % (i+1, goal_vis, goal_lm, ', '.join(pwms))) # Show values we can paste into source code for cnum, channel in enumerate(channels): print('PWM%s values: %s' % (cnum+1, ','.join([str(int(round(i))) for i in channel.modes]))) # Show PFM values (PWM TOP) if dyn_pwm: print('PWM_TOP: %s' % (','.join(str(x) for x in max_pwms))) # Show highest level for each channel before next channel starts for cnum, channel in enumerate(channels[:-1]): prev = 0 i = 1 while (i < answers.num_levels) \ and (channels[cnum+1].modes[i] == 0): #and (channel.modes[i] >= channel.modes[i-1]) \ i += 1 print('Ch%i max: %i (%.2f/%s)' % (cnum, i, channel.modes[i-1], max_pwms[i])) def get_value(text, default, args): """Get input from the user, or from the command line args.""" if args: result = args[0] del args[0] else: global interactive interactive = True print(text + ' (%s) ' % (default), end='') result = input_text() result = result.strip() return result shapes = dict( ninth = (lambda x: x**9, lambda x: math.pow(x, 1/9.0)), seventh= (lambda x: x**7, lambda x: math.pow(x, 1/7.0)), fifth = (lambda x: x**5, lambda x: math.pow(x, 1/5.0)), cube = (lambda x: x**3, lambda x: math.pow(x, 1/3.0)), square = (lambda x: x**2, lambda x: math.pow(x, 1/2.0)), log = (lambda x: math.e**x, lambda x: math.log(x, math.e)), # makes no difference; all logs have the same curve #log_2 = (lambda x: 2.0**x, lambda x: math.log(x, 2.0)), ) def power(x): try: factor = float(ramp_shape) return math.pow(x, factor) except ValueError: return shapes[ramp_shape][0](x) def invpower(x): try: factor = float(ramp_shape) return math.pow(x, 1.0 / factor) except ValueError: return shapes[ramp_shape][1](x) def input_text(): try: value = raw_input() # python2 except NameError: value = input() # python3 return value if __name__ == "__main__": import sys main(sys.argv[1:])