#!/usr/bin/python3 # # Run strings of 24VDC lighting, via GPIO, with PWM. # # standard libraries, requires install import logging # micropython libraries import machine import uasyncio # our local libraries import homeutil import devices APP = homeutil.BaseServer() LOGGER = logging.getLogger(__name__) # Tempting to set this high, but the Rimikon lights cannot handle it. # The 1kHz frequency seems to work fine across all duty levels. PWM_FREQ = 1000 DUTY_LEVELS = ( # CIELUV 0, 483, 1059, 1958, 3261, 5041, 7373, 10332, 13993, 18430, 23718, 29932, 37146, 45434, 54872, 65535, ) LEVEL_50 = 10332 # Approximately 50% perceived brightness. class Light: def __init__(self, pin): self.pin = machine.Pin(pin, machine.Pin.OUT) self.pwm = machine.PWM(self.pin, freq=PWM_FREQ) def use_opcode(self, opcode): LOGGER.debug(f'pin="{self.pin}" PWM="{self.pwm}" opcode={opcode}') if opcode == 't': self.toggle() elif opcode == '1': self.turn_on() elif opcode == '0': self.turn_off() elif opcode.startswith('p'): # percentage brightness (0-100) pct = int(opcode[1:]) self.set_percent(pct) else: return 'Unknown opcode.', 400 def toggle(self): duty = self.pwm.duty_u16() if duty < LEVEL_50: # Light is (mostly-) off. Turn it on. self.turn_on() else: # Light is (somewhat-) on. Turn it off. self.turn_off() def turn_on(self): self.pwm.duty_u16(65535) def turn_off(self): self.pwm.duty_u16(0) def set_percent(self, pct): assert 0 <= pct <= 100 duty = percent_to_duty(pct) #print('DUTY:', duty) self.pwm.duty_u16(duty) async def test_cycling(light): def duty_gen(): "For testing: cycle through the duty cycles." while True: for d in DUTY_LEVELS: yield d duties = duty_gen() while True: duty = next(duties) #print('DUTY:', duty) light.pwm.duty_u16(duty) await uasyncio.sleep(0.2) @APP.route('/l//') async def light(request, lid, opcode): return APP.lights[lid].use_opcode(opcode) @APP.route('/v/1') async def vars_v1(request): "Return all variable values as JSON, v1." def v1_value(light): "Return dict to describe state of LIGHT." duty = light.pwm.duty_u16() if duty == 0: state = 'off' elif duty == 65535: state = 'on' else: state = 'dimmed' ### include a duty-to-percent value return { 'duty': duty, 'state': state, } v1 = dict((li, v1_value(lv)) for li, lv in APP.lights.items() if li not in {'n/c', 'unused', 'GND',}) return v1 # will be formatted as a JSON response def percent_to_duty(pct): "Convert percentage luminance to a duty cycle value." # Fast-case this, as it would throw off the interpolation step. if pct == 100: return 65535 # Determine the index *just below* the requested percentage. # This is a simple map of [0..100] into [0..15]. idx = pct * 15 // 100 # While the overall curve is logarithmic, we will use a simple # linear interpolation between the individual points. This will # produce a value that is close enough. duty1 = DUTY_LEVELS[idx] duty2 = DUTY_LEVELS[idx + 1] diff = duty2 - duty1 p_idx = (idx * 100) // 15 return duty1 + (diff * (pct - p_idx) * 15) // 100 async def run(): # Our lights are defined in devices:LIGHTS_24 APP.lights = dict((l.id, Light(l.pin)) for l in devices.LIGHTS_24) #uasyncio.create_task(test_cycling(APP.lights[13])) # Create the web server, and wait for it to start. ### always use default port 80? await APP.begin() # Run until something tells us to close/stop the server. await APP.server.wait_closed() def main(): uasyncio.run(run()) if __name__ == '__main__': main()