Skip to content

Instantly share code, notes, and snippets.

@todbot
Last active January 15, 2023 00:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save todbot/ed6f4a03c6e222746722e94fb781e56a to your computer and use it in GitHub Desktop.
Save todbot/ed6f4a03c6e222746722e94fb781e56a to your computer and use it in GitHub Desktop.
Demonstrate one way to do smooth LED/PWM fading if input is rotary encoder
#
# demonstrate one way to do smooth LED/PWM fading if input is rotary encoder
# by having a separate "fader" that runs independent of encoder setting
# (but that smoothly catches up to it)
# 14 Jan 2023 - @todbot / Tod Kurt
#
import time
import board
import rotaryio
import pwmio
pwm = pwmio.PWMOut(board.A3, frequency=20000)
encoder = rotaryio.IncrementalEncoder(board.A1, board.A2)
pwm_pos = 0 # current "position" of pwm brightness value
pwm_dest_pos = 0 # where we want pwm brightness to end up
pwm_fade_by = 10 # how much to change LED brightness each loop/
pwm_encoder_scale = 12*256 # how much brightness each encoder click (most encoders are 12 or 24 ppr)
pwm_min = 0 # smallest allowed pwm value
pwm_max = 65535 # largest allow pwm value
encoder_val_last = encoder.position
while True:
# LED fading handling
pwm_delta = pwm_dest_pos - pwm_pos # get how far pos is from destination (pwm_fade_to)
if pwm_delta > 0:
pwm_pos += pwm_fade_by # fade up
elif pwm_delta < 0:
pwm_pos -= pwm_fade_by # fade down
pwm_pos = min(max(pwm_pos, pwm_min), pwm_max) # constrain
# LED setting
pwm.duty_cycle = pwm_pos
# Encoder handling
encoder_val = encoder.position
if encoder_val != encoder_val_last:
encoder_delta = (encoder_val - encoder_val_last) # how much encoder was moved
encoder_val_last = encoder_val
pwm_dest_pos += encoder_delta * pwm_encoder_scale # scale encoder change to get brightness change
pwm_dest_pos = min(max(pwm_dest_pos, pwm_min), pwm_max) # constrain to 0-65535
# some debugging
print("delta:", encoder_delta, "pwm_pos:", pwm_pos, "pwm_dest_pos:",pwm_dest_pos)
@todbot
Copy link
Author

todbot commented Jan 15, 2023

Could also more "linearize" the brightness curve by applying a gamma function:

def gamma(x,maxval=65535):
    gamma_val = 2.8
    return int(pow(x/maxval, gamma_val) * maxval )

# and then in the main loop:
    # LED setting
    pwm.duty_cycle = gamma(pwm_pos)

@todbot
Copy link
Author

todbot commented Jan 15, 2023

And here's a version using asyncio with the gamma function also:

import asyncio   # install via circup
import time
import board
import rotaryio
import pwmio

pwm = pwmio.PWMOut(board.A3, frequency=20000)
encoder = rotaryio.IncrementalEncoder(board.A1, board.A2, divisor=4)

pwm_dest_pos = 0  # where we want pwm brightness to end up
pwm_fade_by = 20  # how much to change LED brightness each loop/
pwm_encoder_scale = 12*256  # how much brightness each encoder click (most encoders are 12 or 24 ppr)
pwm_min = 0  # smallest allowed pwm value (usually zero)
pwm_max = 65535  # largest allow pwm value (usually 65535)

def gamma(x,maxval=65535):
    gamma_val = 2.8
    return int(pow(x/maxval, gamma_val) * maxval )

# LED fading handling
async def led_fader():
    pwm_pos = pwm_dest_pos  # current "position" of pwm brightness value

    while True:
        pwm_delta = pwm_dest_pos - pwm_pos  # get how far pos is from destination (pwm_fade_to)
        if pwm_delta > 0:
            pwm_pos += pwm_fade_by  # fade up
        elif pwm_delta < 0:
            pwm_pos -= pwm_fade_by  # fade down

        pwm_pos = min(max(pwm_pos, pwm_min), pwm_max) # constrain

        pwm.duty_cycle = gamma(pwm_pos)  # actually set LED brightness w/ gamma curve

        await asyncio.sleep(0) # .0001)  # Let another task run.

async def read_encoder():
    global pwm_dest_pos
    encoder_val_last = encoder.position

    while True:
        # Encoder handling
        encoder_val = encoder.position
        if encoder_val != encoder_val_last:
            encoder_delta = (encoder_val - encoder_val_last)  # how much encoder was moved
            encoder_val_last = encoder_val
            pwm_dest_pos += encoder_delta * pwm_encoder_scale            # scale encoder change to get brightness change
            pwm_dest_pos = min(max(pwm_dest_pos, pwm_min), pwm_max)   # constrain to 0-65535
            print("delta:", encoder_delta, "pwm_dest_pos:",pwm_dest_pos)       # some debugging
        await asyncio.sleep(0.001)  # Let another task run.

# create the tasks that hold our functions
async def main():
    led_fader_task = asyncio.create_task( led_fader() )
    read_encoder_task = asyncio.create_task( read_encoder() )
    await asyncio.gather( led_fader_task, read_encoder_task )

# and start everything running, this function never exits
print("here we go")
asyncio.run(main())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment