Rotary encoder menu on Pi Pico without display

Hi All ,

I did an internet search for’rotary encoder menu for Pi Pico with 12 neopixels connected and not using a display.

The result that came back with an AI example was very similar to the code below

from machine import Pin
import utime
import os
#led = machine.Pin("LED", machine.Pin.OUT)
led_onboard = machine.Pin("LED", machine.Pin.OUT)
#led=ledPin
ledPin = Pin(17, mode = Pin.OUT, value = 0)
# === Configuration ===
encoder_clk = Pin(14, Pin.IN, Pin.PULL_UP)
encoder_dt = Pin(15, Pin.IN, Pin.PULL_UP)
encoder_btn = Pin(13, Pin.IN, Pin.PULL_UP)
#output_pin = machine.Pin("LED", machine.Pin.OUT)    #led = Pin(25, Pin.OUT)  # On-board LED
output_pin = Pin(0, Pin.OUT)  # Shared output pin for all scripts

# === Script list ===
scripts = ['script 1.py', 'script 2.py', 'script 3.py']
selected_index = 0
last_clk = encoder_clk.value()
debounce_time = 0.5   # 0

# === Flash LED to indicate change ===
def flash_led(times=2, delay=300):
    for _ in range(times):
        ledPin.on()
        utime.sleep_ms(delay)
        ledPin.off()
        utime.sleep_ms(delay)

# === Load and execute selected script ===
def run_script(filename):
    try:
        with open(filename) as f:
            exec(f.read(), {'output_pin': output_pin})
    except Exception as e:
        print(f"Error running {filename}: {e}")
        flash_led(times=3, delay=200)

# === Main loop ===
print("Rotary menu started. Turn encoder to select, press to run.")

while True:
    clk_val = encoder_clk.value()
    dt_val = encoder_dt.value()

    # Detect rotation
    if clk_val != last_clk and clk_val == 0:
        if dt_val == 1:
            selected_index = (selected_index + 1) % len(scripts)
        else:
            selected_index = (selected_index - 1) % len(scripts)

        print(f"Selected: {scripts[selected_index]}")
        flash_led()

    last_clk = clk_val

    # Button press detection with debounce
    if encoder_btn.value() == 0 and (utime.ticks_ms() - debounce_time) > 300:
        debounce_time = utime.ticks_ms()
        print(f"Running: {scripts[selected_index]}")
        flash_led(times=4)
        run_script(scripts[selected_index])

    utime.sleep_ms(10)

For the most part it works and when I turn the rotary encoder I get the following result in the shell

“Rotary menu started. Turn encoder to select, press to run.”
Selected: script 2.py
Selected: script 3.py
Selected: script 1.py
Running: script 1.py

However once I have selected script 1 , 2 , or 3 and it runs a script , I cannot run any of the other scripts without restarting the menu program, and then i can select a different script and run that one

eg

Rotary menu started. Turn encoder to select, press to run.
Selected: script 2.py
Running: script 2.py

eg

Rotary menu started. Turn encoder to select, press to run.
Selected: script 2.py
Selected: script 3.py
Running: script 3.py

Each script does run when it is selected.

So my problem is , why can’t I select the other scripts after I have selected the initial script and run them without restarting the menu.

Any assistance greatly appreciated

cheers

Nick

2 Likes

Hi Nick,

Once you are in the new script you’ll either need a way to return to the “main script”

Or you could use classes or functions to handle all of the differences.

1 Like

Oh I think I’m following.
Are you saying that, when a script is running, your encoder no longer functions?
Sometimes we talk about “non-blocking code”, which is where multiple scripts can run parallel: at the same time. Is that what you’re aiming for?

You’re updating debounce_time inside the button detection section, but the condition that checks the time (utime.ticks_ms() - debounce_time > 300) might not be working as expected after a script is executed. Once the script runs, the time stored in debounce_time becomes stale, which means subsequent presses won’t be detected until a full cycle passes.

Hi Liam

Thank you ,I am working on it , no success yet

Hi there

Yes I think that is what I’m aiming for. However it’s a bit of a challenge

Hi ,

I have been working on this almost non stop today.

I have copied and modified the code and have been working through the errors. I am presently stuck at line 90

if dt_val == 1: This was not a problem in the original code but is now

Traceback (most recent call last):
File “”, line 90
SyntaxError: invalid syntax

I presume there will be more errors but I hav’nt found them yet. Please see the new

from machine import Pinimport utimeimport osfrom rotary_irq_rp2 import RotaryIRQ#led = machine.Pin(“LED”, machine.Pin.OUT)
rotary =RotaryIRQ(14,15)btn = Pin(13, Pin.IN)led_onboard = machine.Pin(“LED”, machine.Pin.OUT)
last_press_time = 0press_count = 0double_press_window_ms = 500 # Time in milliseconds for double press detection
#led=ledPinledPin1 = Pin(17, mode = Pin.OUT, value = 0)ledPin2 = Pin(18, mode = Pin.OUT, value = 0)ledPin3 = Pin(19, mode = Pin.OUT, value = 0)ledPin = Pin(0, mode = Pin.OUT, value = 0)  #17
=== Configuration ===
encoder_clk = Pin(14, Pin.IN, Pin.PULL_UP)encoder_dt = Pin(15, Pin.IN, Pin.PULL_UP)encoder_btn = Pin(13, Pin.IN, Pin.PULL_UP)#output_pin = machine.Pin(“LED”, machine.Pin.OUT)    #led = Pin(25, Pin.OUT)  # On-board LEDoutput_pin = Pin(0, Pin.OUT)  # Shared output pin for all scripts
=== Script list ===
scripts = [‘script 1.py’, ‘script 2.py’, ‘script 3.py’]selected_index = 0last_clk = encoder_clk.value()debounce_time = 0.5   # 0
=== Flash LED to indicate change ===
def flash_led(times=2, delay=300):  #redfor _ in range(times):ledPin1.on()utime.sleep_ms(delay)ledPin1.off()utime.sleep_ms(delay)
def flash_led2(times=2, delay=300):  #bluefor _ in range(times):ledPin2.on()utime.sleep_ms(delay)ledPin2.off()utime.sleep_ms(delay)
=== Load and execute selected script ===
def run_script(filename):try:with open(filename) as f:exec(f.read(), {‘output_pin’: output_pin})except Exception as e:print(f"Error running {filename}: {e}")flash_led2(times=3, delay=200)
=== Main loop ===
print(“Rotary menu started. Turn encoder to select, press to run.”)#new_val = rotary.val()#current_val = new_valcurrent_val = 0  # track the last known value of the encoder
while True:
if button_pin.value() == 0:  # Button pressed (assuming pull-up)
    current_time = time.ticks_ms()

if current_time - last_press_time < double_press_window_ms:
        press_count += 1
if press_count == 2:
   print("Double button press detected! Resetting menu...")
   machine.soft_reset() # Perform soft reset
else:
   press_count = 1 # Start new sequence
    
   last_press_time = current_time
    
    # Simple debounce (adjust delay as needed)
   time.sleep_ms(50)  

#---------

clk_val = encoder_clk.value()
dt_val = encoder_dt.value()

# Detect rotation
if clk_val != last_clk and clk_val == 0:
if dt_val == 1:
   selected_index = (selected_index + 1) % len(scripts)
else:
   selected_index = (selected_index - 1) % len(scripts)

    print(f"Selected: {scripts[selected_index]}")
    flash_led2()

last_clk = clk_val

# Button press detection with debounce
if encoder_btn.value() == 0 and (utime.ticks_ms() - debounce_time) > 100:
    debounce_time = utime.ticks_ms()
    print(f"Running: {scripts[selected_index]}")
    flash_led(times=4)
    run_script(scripts[selected_index])
    
    new_val = rotary.val()
if current_value != new_val:
    print('Encoder value:' ,new_val)
    
    rotary.reset()
    utime.sleep_ms(10)

This code seems to have some weird formatting……….. I don’t know whats going on.

Anyway I still can’t get this workink the way I want

Cheers




from machine import Pin
import utime
import os
from rotary_irq_rp2 import RotaryIRQ
#led = machine.Pin("LED", machine.Pin.OUT)

rotary =RotaryIRQ(14,15)
btn = Pin(13, Pin.IN)
led_onboard = machine.Pin("LED", machine.Pin.OUT)


last_press_time = 0
press_count = 0
double_press_window_ms = 500 # Time in milliseconds for double press detection


#led=ledPin
ledPin1 = Pin(17, mode = Pin.OUT, value = 0)
ledPin2 = Pin(18, mode = Pin.OUT, value = 0)
ledPin3 = Pin(19, mode = Pin.OUT, value = 0)
ledPin = Pin(0, mode = Pin.OUT, value = 0)  #17
# === Configuration ===
encoder_clk = Pin(14, Pin.IN, Pin.PULL_UP)
encoder_dt = Pin(15, Pin.IN, Pin.PULL_UP)
encoder_btn = Pin(13, Pin.IN, Pin.PULL_UP)
#output_pin = machine.Pin("LED", machine.Pin.OUT)    #led = Pin(25, Pin.OUT)  # On-board LED
output_pin = Pin(0, Pin.OUT)  # Shared output pin for all scripts

# === Script list ===
scripts = ['script 1.py', 'script 2.py', 'script 3.py']
selected_index = 0
last_clk = encoder_clk.value()
debounce_time = 0.5   # 0

# === Flash LED to indicate change ===
def flash_led(times=2, delay=300):  #red
    for _ in range(times):
        ledPin1.on()
        utime.sleep_ms(delay)
        ledPin1.off()
        utime.sleep_ms(delay)
        
def flash_led2(times=2, delay=300):  #blue
    for _ in range(times):
        ledPin2.on()
        utime.sleep_ms(delay)
        ledPin2.off()
        utime.sleep_ms(delay)        

# === Load and execute selected script ===
def run_script(filename):
    try:
        with open(filename) as f:
            exec(f.read(), {'output_pin': output_pin})
    except Exception as e:
        print(f"Error running {filename}: {e}")
        flash_led2(times=3, delay=200)

# === Main loop ===
print("Rotary menu started. Turn encoder to select, press to run.")
#new_val = rotary.val()
#current_val = new_val
current_val = 0  # track the last known value of the encoder

while True:
    
    if button_pin.value() == 0:  # Button pressed (assuming pull-up)
        current_time = time.ticks_ms()

    if current_time - last_press_time < double_press_window_ms:
            press_count += 1
    if press_count == 2:
       print("Double button press detected! Resetting menu...")
       machine.soft_reset() # Perform soft reset
    else:
       press_count = 1 # Start new sequence
        
       last_press_time = current_time
        
        # Simple debounce (adjust delay as needed)
       time.sleep_ms(50)  
    
    #---------
    
    clk_val = encoder_clk.value()
    dt_val = encoder_dt.value()

    # Detect rotation
    if clk_val != last_clk and clk_val == 0:
    if dt_val == 1:
       selected_index = (selected_index + 1) % len(scripts)
    else:
       selected_index = (selected_index - 1) % len(scripts)

        print(f"Selected: {scripts[selected_index]}")
        flash_led2()

    last_clk = clk_val

    # Button press detection with debounce
    if encoder_btn.value() == 0 and (utime.ticks_ms() - debounce_time) > 100:
        debounce_time = utime.ticks_ms()
        print(f"Running: {scripts[selected_index]}")
        flash_led(times=4)
        run_script(scripts[selected_index])
        
        new_val = rotary.val()
    if current_value != new_val:
        print('Encoder value:' ,new_val)
        
        rotary.reset()
        utime.sleep_ms(10)



Fixed it

1 Like

Hi Nick,

Do you have all of your functionality working now? If you want to call other scripts would it be possible to send those through as well?

1 Like

When I said ‘fixed it’ what I meant was that I’d worked out why it didn’t upload cleanly and I solved that problem. But no, I have not managed to make the menu system work properly. I still have to restart the menu to be able to reinitialise it completely to choose a different script in the menu. I’ve tried many different options which I haven’t published because it takes up so much space and I can’t make sense of it really. The code I have just uploaded has a number of changes that I hoped would work but that was not the case

1 Like

Hi Nick,

Ni worries about taking up a lot of space, can you please post the filenames along with the code, and one script.

See what we can whip up

Shall do

1 Like

Hi Nick,

Another thought, you could put machine.reset() at the end of every script to automatically reset

hi Liam

Thanks for your reply.

The file name for the very first script in this discussion I entitled rotary menu from A I internet.py .

The second script that I uploaded (twice) is titled ‘rotary menu modified A I.py’

Here is the code for “script 1.py”

#works with the off time different to on time

import machine
import neopixel
import time

# Configuration for the NeoPixel strip
PIN = 0  # GPIO pin connected to the NeoPixel data input
NUM_PIXELS = 12

# Create a NeoPixel object
np = neopixel.NeoPixel(machine.Pin(PIN), NUM_PIXELS)

# Define colors
RED = (255, 0, 0)
GREEN = (0, 255, 0)
ORANGE = (200, 30,  0)
BLUE = (0, 0, 255)
PINK = (200, 0, 200)
OFF = (0, 0, 0)

# Define delays
ON_DELAY = 2.2  # Time in seconds for the pixels to be on
OFF_DELAY = 0.2  # Time in seconds for the pixels to be off (different from ON_DELAY)

def set_all_pixels(color):
    """Sets all pixels on the strip to a given color."""
    for i in range(NUM_PIXELS):
        np[i] = color
    np.write()

while True:
    
    # Turn all pixels off
    set_all_pixels(OFF)
    time.sleep(OFF_DELAY)
    
    
    # Turn all pixels red
    set_all_pixels(RED)
    time.sleep(ON_DELAY)

    # Turn all pixels off
    set_all_pixels(OFF)
    time.sleep(OFF_DELAY)

    # Turn all pixels green
    set_all_pixels(GREEN)
    time.sleep(ON_DELAY)
    
    # Turn all pixels ORANGE
    set_all_pixels(ORANGE)
    time.sleep(ON_DELAY)

    # Turn all pixels off
    set_all_pixels(OFF)
    time.sleep(OFF_DELAY)

    # Turn all pixels blue
    set_all_pixels(BLUE)
    time.sleep(ON_DELAY)

    # Turn all pixels off
    set_all_pixels(OFF)
    time.sleep(OFF_DELAY)
    
     # Turn all pixels blue
    set_all_pixels(PINK)
    time.sleep(ON_DELAY)

    # Turn all pixels off
    set_all_pixels(OFF)
    time.sleep(OFF_DELAY)

Here is the code for “script 2.py

import neopixel
from machine import Pin
import time

ws_pin = 0
led_num = 12
BRIGHTNESS = 0.3  # Adjust the brightness (0.0 - 1.0)

neoRing = neopixel.NeoPixel(Pin(ws_pin), led_num)

def set_brightness(color):
    r, g, b = color
    r = int(r * BRIGHTNESS)
    g = int(g * BRIGHTNESS)
    b = int(b * BRIGHTNESS)
    return (r, g, b)

def loop():
    # Display red
    color = (255, 0, 0)  # Red color
    color = set_brightness(color)
    neoRing.fill(color)
    neoRing.write()
    time.sleep(2)
    
    # Display orange
    color = (255, 50, 0)  # orange color
    color = set_brightness(color)
    neoRing.fill(color)
    neoRing.write()
    time.sleep(2)
    
     # Display yellow
    color = (255, 150, 0)  # orange color
    color = set_brightness(color)
    neoRing.fill(color)
    neoRing.write()
    time.sleep(2)

    # Display green
    color = (0, 255, 0)  # Green color
    color = set_brightness(color)
    neoRing.fill(color)
    neoRing.write()
    time.sleep(2)
    
    # Display light blue
    color = (0, 255,150)  # light blue color
    color = set_brightness(color)
    neoRing.fill(color)
    neoRing.write()
    time.sleep(2)
    
    # Display pink
    color = (250, 0,250)  # pink color
    color = set_brightness(color)
    neoRing.fill(color)
    neoRing.write()
    time.sleep(2)

    # Display blue
    color = (0, 0, 255)  # Blue color
    color = set_brightness(color)
    neoRing.fill(color)
    neoRing.write()
    time.sleep(2)

while True:
    loop()


And “script 3.py

import machine
from machine import Pin
import utime
import array, time
from machine import Pin
import rp2

# Configure the number of WS2812 LEDs.
NUM_LEDS = 12
PIN_NUM = 0
brightness = 0.9

led_onboard = machine.Pin("LED", machine.Pin.OUT)
ledPin = Pin(15, mode = Pin.OUT, value = 0) # Onboard led on GPIO 15
ledPin1 = Pin(17, mode = Pin.OUT, value = 0) # Onboard led on GPIO 15

@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
    T1 = 2
    T2 = 5
    T3 = 3
    wrap_target()
    label("bitloop")
    out(x, 1)               .side(0)    [T3 - 1]
    jmp(not_x, "do_zero")   .side(1)    [T1 - 1]
    jmp("bitloop")          .side(1)    [T2 - 1]
    label("do_zero")
    nop()                   .side(0)    [T2 - 1]
    wrap()


# Create the StateMachine with the ws2812 program, outputting on pin
sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(PIN_NUM))

# Start the StateMachine, it will wait for data on its FIFO.
sm.active(1)

# Display a pattern on the LEDs via an array of LED RGB values.
ar = array.array("I", [0 for _ in range(NUM_LEDS)])

##########################################################################
def pixels_show():
    dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)])
    for i,c in enumerate(ar):
        r = int(((c >> 8) & 0xFF) * brightness)
        g = int(((c >> 16) & 0xFF) * brightness)
        b = int((c & 0xFF) * brightness)
        dimmer_ar[i] = (g<<16) + (r<<8) + b
    sm.put(dimmer_ar, 8)
    time.sleep_ms(10)

def pixels_set(i, color):
    ar[i] = (color[1]<<16) + (color[0]<<8) + color[2]

def pixels_fill(color):
    for i in range(len(ar)):
        pixels_set(i, color)

def color_chase(color, wait):
    for i in range(NUM_LEDS):
        pixels_set(i, color)
        time.sleep(wait)
        pixels_show()
    time.sleep(0.2)
 
def wheel(pos):
    # Input a value 0 to 255 to get a color value.
    # The colours are a transition r - g - b - back to r.
    if pos < 0 or pos > 255:
        return (0, 0, 0)
    if pos < 85:
        return (255 - pos * 3, pos * 3, 0)
    if pos < 170:
        pos -= 85
        return (0, 255 - pos * 3, pos * 3)
    pos -= 170
    return (pos * 3, 0, 255 - pos * 3)
 
 
def rainbow_cycle(wait):
    for j in range(255):
        for i in range(NUM_LEDS):
            rc_index = (i * 256 // NUM_LEDS) + j
            pixels_set(i, wheel(rc_index & 255))
        pixels_show()
        time.sleep(wait)

BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)

print("fills")
for color in COLORS:       
    pixels_fill(color)
    pixels_show()
    time.sleep(0.2)

print("chases")
for color in COLORS:       
    color_chase(color, 0.01)

print("rainbow")
rainbow_cycle(0)


while True:
    led_onboard.value(1)
    #print('on')
    utime.sleep_ms(50)
    
    led_onboard.value(0)
    #print('off')
    utime.sleep_ms(200)
    ledPin.toggle()
    utime.sleep_ms(100)
    utime.sleep_ms(200)
    ledPin1.toggle()
    utime.sleep_ms(100)
    
    utime.sleep_ms(200)
    ledPin.toggle()
    ledPin1.toggle()
    utime.sleep_ms(100)
    utime.sleep_ms(200)
    ledPin1.toggle()
    utime.sleep_ms(100)

These scripts 1,2,3….py do not have “machine reset at the end but I will try that now.

Thank you again.

1 Like

I basically want each script to run until the rotary encoder interupts the script so I’m not sure if machine.reset () will prevent this

1 Like

I am using a Pico2 W

1 Like

Hi Nick,

If you are using an LLM to code, I would ask it

Please turn these scripts into functions that can be called.
Using a global flag or similar please have a break function that will call either machine.reset() or execute the original script

The issue you are running into is when you run the “exec” function, micropython stops running your original script and starts running the new one. If you have no way of getting back to the original you have to cut power or reset your project another way

3 Likes

Thanks for the suggestion Liam .I was pretty busy yesterday. I’ll spend some time researchig your suggestion and see if I I can implement it

2 Likes

Hey @Nicholas193967,

Looks like the community has already helped you make some great progress.

The core issue seems to stem from using exec() to run your scripts, this blocks your rotary encoder menu from continuing because exec() runs the new script in the current context and takes over execution. That’s why you’re unable to return to the menu after a script starts.

Refactoring each script into a function (or class with a step() method), as suggested by @Liam120347, is a solid approach. It gives you much better control and allows you to switch between scripts and the menu without restarting the whole system. You can also manage this flow more cleanly by using a global flag or state variable to track what’s running.

For some inspiration, you might want to take a look at this project:
https://github.com/miketeachman/micropython-rotary
It has some good examples of how to handle rotary input in a MicroPython context, possibly helpful as you refine your menu system.

2 Likes

I can highly recommend Peter Hinch’s drivers for rotary encoders (and everything else !!). Look here:

That won’t address the issue of control being lost when you run exec.

My project uses an encoder to navigate a muti-level menu hierarchy, driven by a dictionary, and invoking functions selected. Also providing a way to return to the top-level menu. I use asyncio a lot. I do NOT exec standalone scripts… but the menu approach might be useful in your project? Let me know if you’d like more details… I can post some code.

Cheers,

T.

3 Likes