Pi Pico+ rotary encoder+ssd1306 menu control

Hi All
I am attempting to put together a raspberry pi pico rotary encoder menu system which will eventually control a stepper motor to vary the angles and time durations that it operates for. My final aim in all this is to have a rotating platform for a GoPro to do rotating time lapses. I am not quite clever enough to do all this on my lonesome and I have found a YouTube video by Kevin McAleer and the associated code that he has produced. The problem that I am having is that even though the display works and shows the menu the rotary encoder will not change the highlighted line in the code and the push button on the encoder will not work either. I have seen Jacob’s explanation ( thank you Jacob) and it works fine.
I have changed the connections on the Pico for the encoder from 16,17&18 to 14,15,& 12 but it’s made no difference.
I have also changed the encoder to one which has 3 pull-up resistors and therefore commented out the pull up part of the code but it’s made no difference.
This is the link to Kevin’s code

I couldn’t acquire the link to the video.

Here is his code

> # Rotary Menu
# Kevin McAleer
# May 2021

from os import listdir
from time import sleep
from machine import I2C, Pin
from ssd1306 import SSD1306_I2C

# I2C variables
ID = 0
SDA = Pin(0)
SCL = Pin(1)
i2c = I2C(id=ID, scl=SCL, sda=SDA)

# Screen Variables
WIDTH = 128
HEIGHT = 64

line = 1 
highlight = 1
shift = 0
list_length = 0
TOTAL_LINES = 6

# create the display
oled = SSD1306_I2C(width=WIDTH, height=HEIGHT, i2c=i2c)
oled.init_display()

# Setup the Rotary Encoder
button_pin = Pin(16, Pin.IN, Pin.PULL_UP)
direction_pin = Pin(17, Pin.IN, Pin.PULL_UP)
step_pin  = Pin(18, Pin.IN, Pin.PULL_UP)

# for tracking the direction and button state
previous_value = True
button_down = False

def get_files():
    """ Get a list of Python files in the root folder of the Pico """

    files = listdir()
    menu = []
    for file in files:
        if file.endswith(".py"):
            menu.append(file)

    return menu


def show_menu(menu):
    """ Shows the menu on the screen"""

    # bring in the global variables
    global line, highlight, shift, list_length

    # menu variables
    item = 1
    line = 1
    line_height = 10

    # clear the display
    oled.fill_rect(0,0,WIDTH,HEIGHT,0)

    # Shift the list of files so that it shows on the display
    list_length = len(menu)
    short_list = menu[shift:shift+TOTAL_LINES]

    for item in short_list:
        if highlight == line:
            oled.fill_rect(0,(line-1)*line_height, WIDTH,line_height,1)
            oled.text(">",0, (line-1)*line_height,0)
            oled.text(item, 10, (line-1)*line_height,0)
            oled.show()
        else:
            oled.text(item, 10, (line-1)*line_height,1)
            oled.show()
        line += 1
    oled.show()


def launch(filename):
    """ Launch the Python script <filename> """
    global file_list
    # clear the screen
    oled.fill_rect(0,0,WIDTH,HEIGHT,0)
    oled.text("Launching", 1, 10)
    oled.text(filename,1, 20)
    oled.show()
    sleep(3)
    exec(open(filename).read())
    show_menu(file_list)


# Get the list of Python files and display the menu
file_list = get_files()
show_menu(file_list)

# Repeat forever
while True:
    if previous_value != step_pin.value():
        if step_pin.value() is False:

            # Turned Left
            if direction_pin.value() is False:
                if highlight > 1:
                    highlight -= 1
                else:
                    if shift > 0:
                        shift -= 1

            # Turned Right
            else:
                if highlight < TOTAL_LINES:
                    highlight += 1
                else:
                    if shift+TOTAL_LINES < list_length:
                        shift += 1

            show_menu(file_list)
        previous_value = step_pin.value()

    # Check for button pressed
    if button_pin.value() is False and not button_down:
        button_down = True

        print("Launching", file_list[highlight-1+shift])

        # execute script
        launch(file_list[(highlight-1) + shift])

        print("Returned from launch")

    # Decbounce button
    if button_pin.value() is True and button_down:
        button_down = False

>

Any assistance in solving what I missing is greatly appreciated
Cheers
Nick

1 Like

You should break the problem down into a series of simpler sketches that test each portion of the task. If you can’t get past one of the steps show the code for that sketch and describe the error.

A starting point would be a simple sketch that just responds to the button press by displaying a message.

A second sketch would include code only for the rotary encoder, and simply displays the result of reading the encoder movement - the number of steps and the direction.

You could then combine them and confirm you can reliably get encoder step and direction, and button press. This is pretty much the main loop from the example you are following.

The next step would be to connect the stepper drivers and write a sketch to confirm that you can control the motor speed and direction using hard-coded values in the sketch.

Then you can merge the sketch that simply reports encoder movement and button presses into the sketch that controls the motors and you have the completed task. The bit about highlighting a list of files doesn’t seem applicable to what you want to accomplish.

Note that you have changed the encoder connection to 14,15 & 12 but your code still thinks it is connected at 16, 17 &18 - that will need to be fixed before step 2 above.

5 Likes

I looked at the code on the github link. Despite all the comments on YouTube of it being great, it is not. Kevin produces some good flashy presentations but often the important substance you need is missing. Your post is good evidence of this.

For what you want to do, it’s not necessary to use Kevin’s code, it doesn’t do much to be honest.
Observations :-

  • There is no sleep statement. The while True: loop does not need to run as fast as it possibly can.
  • Because there is no sleep statement, he had to incorporate debounce, complicating things. I rarely need debounce in my Pico programs, the Pico GPIO’s are pretty good.
  • Why use a rotary encoder, two buttons would be better, menu up menu down or just one that cycles through the list of files.
  • No parsing of the file list. It should only list the Test.py files. Two of them actually do nothing. The one that does only displays for a second, not really long enough.

I’d forget about this code and focus on getting your rotating platform to rotate first.
If you still want to get this to work, follow what @Jeff105671 said.

Regards
Jim

PS Github link

3 Likes

Hey @Nicholas193967,

I second what the others have said. It is probably a good idea to reduce the complexity of your code. I have changed Kevin’s code to remove everything but the encoder value and output that to the console which should be useful for you in testing this.

I also changed the debounce implementation as @James46717 suggested so that it uses a sleep delay instead.

from os import listdir
from time import sleep
from machine import Pin

# Setup the Rotary Encoder
button_pin = Pin(16, Pin.IN, Pin.PULL_UP)
direction_pin = Pin(17, Pin.IN, Pin.PULL_UP)
step_pin = Pin(18, Pin.IN, Pin.PULL_UP)

# for tracking the direction and button state
previous_value = True
button_down = False

# Delay of 50 ms for debouncing button input
DEBOUNCE_DELAY = 0.05  

encoder_value = 0

# Repeat forever
while True:

    if previous_value != step_pin.value():
        if step_pin.value() == False:

            # Turned Left
            if direction_pin.value() == False:
                encoder_value -= 1
                print("Encoder value: ", encoder_value)
                print("Encoder rotating: LEFT")

            # Turned Right
            else:
                encoder_value += 1
                print("Encoder value: ", encoder_value)
                print("Encoder rotating: RIGHT")

        previous_value = step_pin.value()

    # Check for button pressed
    if not button_pin.value():
        if not button_down:
            # Debounce delay
            sleep(DEBOUNCE_DELAY) 
            
            # Check button state again after delay
            if not button_pin.value():  
                button_down = True
                print("Button pressed")
                
    elif button_down:
        button_down = False

2 Likes

Thank you all for your replies. I will work on them and see what I can come up with.
Samuel , thank you for your test code ,it works perfectly , I’m going to try and interface some menu code with that, and possibly also try a button menu
Cheers
Nick

2 Likes

Another appoarch

H All
I have been working on this continuously since I was last online.
Jeff , I determined that I couldn’t get anything on the screen without having most of the code in place.
Samuel, I have run your code and it woks well however I went back to the original code and discovered that it had a number of places which said (is False) or similar and I determined that "is " should be replaced by(==) is equal to. Now the menu is on the screen and it scrolls, but it takes 2 clicks of the rotary encoder to move one line .
Please see my modified code below

> 
# Rotary Menu
# Kevin McAleer
# May 2021

from os import listdir
from time import sleep
from machine import I2C, Pin
from ssd1306 import SSD1306_I2C
import utime
# I2C variables
ID = 0
SDA = Pin(0)
SCL = Pin(1)
i2c = I2C(id=ID, scl=SCL, sda=SDA)
#led_onboard=machine.Pin("LED", machine.Pin.OUT)  #led


# Screen Variables
WIDTH = 128
HEIGHT = 64

line = 1 
highlight = 1
shift = 0
list_length = 0
TOTAL_LINES = 7

# create the display
oled = SSD1306_I2C(width=WIDTH, height=HEIGHT, i2c=i2c)
oled.init_display()

# Setup the Rotary Encoder
button_pin = Pin(16, Pin.IN, Pin.PULL_UP)     #12
direction_pin = Pin(17, Pin.IN, Pin.PULL_UP)   #14
step_pin  = Pin(18, Pin.IN, Pin.PULL_UP)      #15

# for tracking the direction and button state
previous_value = True
button_down = False
# Delay of 50 ms for debouncing button input
DEBOUNCE_DELAY = 0.05  

def get_files():
    """ Get a list of Python files in the root folder of the Pico """

    files = listdir()
    menu = []
    for file in files:
        if file.endswith(".py"):
            menu.append(file)

    return menu


def show_menu(menu):
    """ Shows the menu on the screen"""

    # bring in the global variables
    global line, highlight, shift, list_length

    # menu variables
    item = 1
    line = 1
    line_height = 10

    # clear the display
    oled.fill_rect(0,0,WIDTH,HEIGHT,0)   # start point 0,0 ;w, h;  0 colour

    # Shift the list of files so that it shows on the display
    list_length = len(menu)
    short_list = menu[shift:shift+TOTAL_LINES]

    for item in short_list:
        if highlight == line:
            oled.fill_rect(0,(line-1)*line_height, WIDTH,line_height,1)
            oled.text(">",0, (line-1)*line_height,0)
            oled.text(item, 10, (line-1)*line_height,0)
            oled.show()
        else:
            oled.text(item, 10, (line-1)*line_height,1)
            oled.show()
        line += 1
    oled.show()


def launch(filename):
    """ Launch the Python script <filename> """
    global file_list
    #clear the screen
    oled.fill_rect(0,0,WIDTH,HEIGHT,0)
    oled.text("Launching", 1, 10)
    oled.text(filename,1, 20)
    oled.show()
    sleep(3)
    exec(open(filename).read())
    show_menu(file_list)


# Get the list of Python files and display the menu
file_list = get_files()
show_menu(file_list)

# Repeat forever
while True:
#      led_onboard.value(1)
#      #print('on')
#      utime.sleep_ms(50)
#     
#      led_onboard.value(0)
#      #print('off')
#      utime.sleep_ms(1000) 
    
    
    
    if previous_value != step_pin.value():
        if step_pin.value() == False:   #is

            # Turned Left
            if direction_pin.value() == False:   #is
                if highlight > 1:
                    highlight -= 1
                else:
                    if shift > 0:
                        shift -= 1

            # Turned Right
            else:
                if highlight < TOTAL_LINES:
                    highlight += 1
                else:
                    if shift+TOTAL_LINES < list_length:
                        shift += 1

            show_menu(file_list)
        previous_value = step_pin.value()

 # Check for button pressed
if button_pin.value() == False and not button_down:    #is
        button_down = True
        sleep(DEBOUNCE_DELAY) 
        print("Launching", file_list[highlight-1+shift])

        # execute script
        launch(file_list[(highlight-1) + shift])

        print("Returned from launch")
# Debounce delay
        sleep(DEBOUNCE_DELAY) 
# Decbounce button
if button_pin.value() == True and button_down:   #is
        button_down = False

>

Elliot , I have tried to run the code you provided but it gives me this error,
ImportError: no module named ‘rotary.rotary_irq’ , .Icannot find a link to this module although I can find “rotary_irq_rp2.py” but this doesn’t work in this instance
so I am still at it , just slow progress . Thank you for your assistance.
cheers
Nick

Did you start with the first suggestion: “… a simple sketch that just responds to the button press by displaying a message.”?

If you got that far then the subsequent sketches would show you the results of adding the additional code and confirming that it was working as expected. If you didn’t get to that first step then I would not recommend trying to move further on in the process - it will make debugging very difficult if you can’t display messages to screen.

2 Likes

Hi Jeff
Yes I have put together a code that displays to the shell and at the same time puts a message on the ssd1306 and responds to left or right turn of the rotary enc and also to the button press.
see code below
One thing that I would also like to do is to count the number of times the switch is pressed and display that( I think it requires strings but I am not familiar with the process) and then comes
creating executable menu’s
cheers
Nick


from os import listdir
from time import sleep
from machine import I2C, Pin, SoftI2C
import utime
from time import sleep
import ssd1306
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))   #22  21

oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)
oled.init_display()

# Setup the Rotary Encoder
button_pin = Pin(16, Pin.IN, Pin.PULL_UP)     #12
direction_pin = Pin(17, Pin.IN, Pin.PULL_UP)   #14
step_pin  = Pin(18, Pin.IN, Pin.PULL_UP)      #15

# for tracking the direction and button state
previous_value = True
button_down = False

# Delay of 50 ms for debouncing button input
DEBOUNCE_DELAY = 0.05  

encoder_value = 0

# Repeat forever
while True:
    oled.fill_rect(0,0,oled_width,oled_height,0)   # start point 0,0 ;w, h;  0 colour
    if previous_value != step_pin.value():
        if step_pin.value() == False:

            # Turned Left
            if direction_pin.value() == False:
                encoder_value -= 1
                # oled.text("Encoder value: ",1,30 ) #encoder_value,)
                # oled.text("Encoder rotating: LEFT")
                print("Encoder value: ", encoder_value)
                print("Encoder rotating: LEFT")
                oled.text("Launching", 1, 10)
                oled.text("rotating left",1, 20)
                oled.text("Hi There",1, 30)
                oled.show()
                #oled.fill()
            # Turned Right
            else:
                encoder_value += 1
                ##encoder_string = str(encoder value)
                #screen1_row1 = ("Encoder value", encoder_value)
                print("Encoder value: ", encoder_value)
                print("Encoder rotating: RIGHT")
                oled.text("Encoder value: ", 1, 10)    # encoder_value)
                oled.text("Enc rot: RIGHT",1, 20)
                ##oled.text("Enccoder_str",1, 30)
                oled.show()
                #oled.fill()

        previous_value = step_pin.value()

    # Check for button pressed
    if not button_pin.value():
        if not button_down:
            # Debounce delay
            sleep(DEBOUNCE_DELAY) 
            
            # Check button state again after delay
            if not button_pin.value():  
                button_down = True
                print("Button Pressed")
                oled.text("Button pressed",1,20)
                oled.show()
    elif button_down:
        button_down = False

Hey @Nicholas193967,

I’m assuming the code you pasted is confirmed working? That’s a great start!

To display the number of times the button was pressed, you should simply be able to increment a counter. To write it to the OLED display you should just be able to use the str() function.

Hope this helps!

Zach
Yes code confirmed working
Thanks
Nick

1 Like

Hi Nick,
Sorry for the delay, only recently got my account back (not CE’s problem).
I can confirm the uMenu library works.

RE: Error - no module named ‘rotary.rotary_irq
The library module is located here:
GitHub - miketeachman/micropython-rotary: MicroPython module to read a rotary encoder. ← MICROPYTHON

Additonal Background Info that might help:

and
arduino/libraries/Rotary at master · buxtronix/arduino · GitHub <— NOTE ARDUINO

RE: Counting Buttons when using uMenu
I used the Piico Dev Buttons (SKU: CE08500 ) on I2C pins

Just for counting buttons, not encoder.

print("| ! WARNING MUST HAVE 2 Button Modules!")
print("| ")


#LIBRARIES
print("|-- LIBRARIES ---")
from machine import Pin
from PiicoDev_Switch import PiicoDev_Switch # Switch may be used for other types of PiicoDev Switch devices
from PiicoDev_Unified import sleep_ms
print("|- All Libraries Imported")
print("| ")

#VARIBLES
print("|--- VARIABLES ---")
count_button_0 = 0
count_button_1 = 0
print(" |- All Variables loaded")
print("| ")

#MODULE CONNECTION & INITIALISATION
print("|--- MODULE CONNECTION ---")
button_0   = PiicoDev_Switch(id=[0,0,0,0])   # Initialise the 1st module
print(" | - Button 0 White Cap Connected")
button_1 = PiicoDev_Switch(id=[1,0,0,0])   # Initialise the 2nd module
print(" | - Button 1 Blue Cap Connected")
print("| ")


#MAIN -THREAD
print("|---  MAIN  ---")
print("| Tests for: Was the button 0 or 1 pressed ?")
print("| If button 0 advances button 0's count")
print("| If button 1 advances button 1's count")
print("| Holding down (on) the button will do nothing")
print("| Will work until button 0 is pressed 10 times")
print("| ")
print("| Current button 0 count: ", count_button_0)
print("| Current button 1 count: ", count_button_1)

while count_button_0 < 10:
    if button_0.was_pressed: 
        count_button_0 +=   button_0.press_count
        print("| Current button 0 count: ", count_button_0)
    elif button_1.was_pressed:
        count_button_1 +=   button_1.press_count
        print("| Current button 1 count: ", count_button_1)
    else:
        print("| No buttons pressed")
    sleep_ms(1000)
    
# END
print("| ")
print("| Button 0 pressed 10 times program will exit ")
print("| ")
print("-----  END OF PROGRAM  -----")

By Interrupts

print("| ! WARNING MUST HAVE 2 Button Modules!")
print("| ")


#LIBRARIES
print("|-- LIBRARIES ---")
from machine import Pin
from PiicoDev_Switch import PiicoDev_Switch # Switch may be used for other types of PiicoDev Switch devices
from PiicoDev_Unified import sleep_ms
print("|- All Libraries Imported")
print("| ")

#VARIBLES
print("|--- VARIABLES ---")
count_button_0 = 0
count_button_1 = 0
print(" |- All Variables loaded")
print("| ")

#MODULE CONNECTION & INITIALISATION
print("|--- MODULE CONNECTION ---")
button_0   = PiicoDev_Switch(id=[0,0,0,0])   # Initialise the 1st module
print(" | - Button 0 White Cap Connected")
button_1 = PiicoDev_Switch(id=[1,0,0,0])   # Initialise the 2nd module
print(" | - Button 1 Blue Cap Connected")
print("| ")


def irq_Button0(p):
    global count_button_0
    
    count_button_0 +=   button_0.press_count
    print("| Current button 0 count: ", count_button_0)
    Pin(6, Pin.IN, Pin.PULL_UP)
    
def irq_Button1(p):
    global count_button_1
    
    count_button_1 +=   button_1.press_count
    print("| Current button 1 count: ", count_button_1)
    Pin(9, Pin.IN, Pin.PULL_UP)
    
# Set up an interrupt pin (you can choose any available GPIO pin)
# I2C SDA pin 6
interrupt_button0 = Pin(6, Pin.IN, Pin.PULL_UP)
interrupt_button0.irq(trigger=Pin.IRQ_FALLING, handler=irq_Button0)
interrupt_button1 = Pin(9, Pin.IN, Pin.PULL_UP)
interrupt_button1.irq(trigger=Pin.IRQ_FALLING, handler=irq_Button1)

#MAIN -
print("|---  MAIN  ---")
print("| Tests for: Was the button 0 or 1 pressed ?")
print("| If button 0 advances button 0's count")
print("| If button 1 advances button 1's count")
print("| Holding down (on) the button will do nothing")
print("| Will work until button 0 is pressed 10 times")
print("| ")
print("| Current button 0 count: ", count_button_0)
print("| Current button 1 count: ", count_button_1)

while count_button_0 < 10:
    if button_0.was_pressed: 
        Pin(6, Pin.IN, Pin.PULL_DOWN) # causing the interrupt on Pin 6
    elif button_1.was_pressed:
        Pin(9, Pin.IN, Pin.PULL_DOWN) # causing the interrupt on Pin 9
    else:
        print("| No buttons pressed")
    sleep_ms(1000)
    
# END
print("| ")
print("| Button 0 pressed 10 times program will exit ")
print("| ")
print("-----  END OF PROGRAM  -----")

In above code you can change the prints to display for showing on display module.
Using Piico Dev Display (SKU: CE07911)

# Library Modules - in addition to above
from PiicoDev_SSD1306 import *					# For Display, note line height default is 10

from machine import Timer                                                  # For sleep in milliseconds sleep_ms()

# Setup Display - in addition to above
display = create_PiicoDev_SSD1306()

# Def function to clear display (buffer)
def disp_clear ():
    display.fill(0)
    display.show()
    sleep_ms(2000)

# can display by direct command
#  text only in between '  '
display.text('*<Text to Display*>', X spacing, Y spacing (line height), 1)  
display.show()
disp_clear()

# Showing varibale value only no '  ' 
display.text(*<variable name>*,X spacing  , Y spacing, 1)  
display.show()
disp_clear()

# show Text and varible value using str()
display.text('Text to display ' + str(variable_name),  X spacing,  Y spacing,1)
display.show()
disp_clear()


# Multi line Text
display.text('*Text on line 1*',5, 0, 1)                                    # 5 pixels in from the Left hand side
display.text('*Text on line 2*', 5, 10, 1)                                # On line 2 (Height 10)
display.text('*Text on line 3*', 5, 20, 1)                                # On line 3 (Height 20)
                                                                                               # At default height can get 5 lines
display.show()										# Show on display
sleep_ms(2000)										# pause time to view in ms

Hope this helps

1 Like