Compare commits

...

14 Commits

Author SHA1 Message Date
4b7e5723ca Fixed small issue in an old call to time.sleep() which has been replaced by sleep() since a long time 2024-11-15 22:28:35 +01:00
50e9b5211b Finished last todos and cleaned up README.md (removed the Roadmap Section) 2024-11-13 23:03:37 +01:00
d5b208f3bd Changed section titles in README.md for consistency 2024-11-13 23:02:12 +01:00
8a17a02ac2 Restructured and added starter instructions 2024-11-13 23:00:29 +01:00
5c26facc0f Updated gallery table in README.md for better readability 2024-11-13 21:40:55 +00:00
091f61f5e7 [README.md] Fixed the broken showcase table 2024-11-13 21:33:27 +00:00
c7b86280a1 Added a first attempt for documentation 2024-11-13 21:59:07 +01:00
fdef354be9 Updated the error message changed previously 2024-11-13 20:33:24 +01:00
a45cd6e90d Made error messages clearer 2024-11-13 18:30:23 +01:00
4550749c8e Added 'production ready but there's a todo' message to the README.md 2024-11-12 22:17:06 +01:00
64ce12f78b Added quitting a menu functionality (actually quitting the run()'s infinite internal loop) 2024-11-12 22:15:15 +01:00
974229f4b2 Added a small check for empty menu item lists (which make no sense) 2024-11-12 21:52:05 +01:00
10562d6410 Fixed small bug of lcdMenu not resuming to the menu after execution of a item in case of no return (of the callback) 2024-11-12 21:28:11 +01:00
81fb57e2bf Finally fully implemented the vertical scrolling; also, added more comments to the program for better understandability 2024-11-12 21:26:34 +01:00
14 changed files with 205 additions and 60 deletions

106
README.md
View File

@@ -4,19 +4,107 @@ A micropython library, which supports vertical and horizontal scrolling through
This project is the completely rewritten successor of my old (and now archived) [ProgramChooser](/BlueFox/ProgramChooser) library. This project is the completely rewritten successor of my old (and now archived) [ProgramChooser](/BlueFox/ProgramChooser) library.
It is now production-ready (tested on 2x16 displays only at the moment.)
## Roadmap
- [x] forward, backward and select button # Installation
- [ ] support for horizontal and vertical scrolling
- [x] support for both 2x16 and 4x20 LCDs To "install" the library on your Pico, clone the repository first using `git clone`. Open [Thonny](https://thonny.org). There, open a file from "This computer" (thonny asks for the location after clicking on "Open"), and select the `__init__.py` from this repository. To get this file to your Pico's storage, choose "Save as" and choose "Raspberry Pi Pico" as the location.
- [x] a reliable order of the menu items
- [ ] good documentation for all of this (maybe through examples) Now, create a new directory called lcdMenu in the root of the Pico's file system (you can do that in the "magic" /lib folder too, makes no difference), and after that save the file there (with `__init__.py` being the file name).
- [x] show an exit screen when a specific exit code is returned by a callback function
- [ ] make the menu itself exitable (to enable stuff like submenus, etc.) Now you can use the library using `import lcdMenu` in any of your scripts!
- [x] make the project a valid python package
# Usage
For basic usage, you just have to create an LCD object, use it to create a lcdMenu along with some other arguments. On this newly created object, run the `setup()` method with your menu item list. Then, start the menu by calling the (blocking) `run()` method on it.
To stop a running instance of lcdMenu (started by `run()`), call the method `stop()` on it!
For further usage, see the comments above the respective method definition in question in the [__init__.py](__init__.py) file. These try to describe the behaviour of each parameter pretty precise. Also, have a look into the examples which can be found in the [examples](examples/) folder.
## Gallery
Here are some of examples of how a lcdMenu will look, showcasing the amount of options you have with lcdMenu. Currently, as the library is only tested with 2x16 displays, these are the only ones showing up below - but on 4x20, it should look the same except it's bigger!
<table>
<tr>
<th>Scroll direction</th>
<th>Cycling</th>
<th>Title shown</th>
<th>Initial selection</th>
<th>Result</th>
</tr>
<tr>
<td>horizontal</td>
<td>yes</td>
<td>yes</td>
<td>first</td>
<td><img src="images/2x16-title-horizontal.jpg" alt="With title, middle item -> forward and backward, horizontal scrolling"></td>
</tr>
<tr>
<td>vertical</td>
<td>yes</td>
<td>no</td>
<td>first</td>
<td><img src="images/2x16-title-vertical-up-down-cycling.jpg" alt="No title, first item & cycling on -> up & down, vertical scrolling"/></td>
</tr>
<tr>
<td>vertical</td>
<td>no</td>
<td>no</td>
<td>first</td>
<td><img src="images/2x16-no-title-vertical-only-down.jpg" alt="No title, first item &amp; no cycling -&gt; only down, vertical scrolling"></td>
</tr>
<tr>
<td>vertical</td>
<td>no</td>
<td>no</td>
<td>middle</td>
<td><img src="images/2x16-no-title-vertical-up-down.jpg" alt="No title, middle item -&gt; up &amp; down, vertical scrolling"></td>
</tr>
<tr>
<td>vertical</td>
<td>no</td>
<td>no</td>
<td>last</td>
<td><img src="images/2x16-no-title-vertical-only-up.jpg" alt="No title, last item &amp; no cycling -&gt; only up, vertical scrolling"></td>
</tr>
<tr>
<td>vertical</td>
<td>no</td>
<td>yes</td>
<td>no options (first and last)</td>
<td><img src="images/2x16-title-vertical-no-options.jpg" alt="With title, only one option, vertical scrolling"></td>
</tr>
<tr>
<td>vertical</td>
<td>no</td>
<td>yes</td>
<td>first</td>
<td><img src="images/2x16-title-vertical-only-down.jpg" alt="With title, first item & no cycling -> only down, vertical scrolling"></td>
</tr>
<tr>
<td>vertical</td>
<td>no</td>
<td>yes</td>
<td>middle</td>
<td><img src="images/2x16-title-vertical-up-down.jpg" alt="With title, middle item -> up & down, vertical scrolling"></td>
</tr>
<tr>
<td>vertical</td>
<td>no</td>
<td>yes</td>
<td>last</td>
<td><img src="images/2x16-title-vertical-only-up.jpg" alt="With title, last item & no cycling -> only up, vertical scrolling"></td>
</tr>
</table>
## License ## License
This project is licensed under the GPL-v3-or-later. A copy can be found [here](LICENSE). This project is licensed under the GPL-v3-or-later. A copy can be found [here](LICENSE).

View File

@@ -23,9 +23,8 @@ class lcdMenu:
# cycle: bool - if true, start again with the first menu entry after the last one (and show the last before the first) # cycle: bool - if true, start again with the first menu entry after the last one (and show the last before the first)
# hide_menu_name: bool - OPTIONAL - if true, hide the menu's name (won't work in combination with a vertical scrolling direction) # hide_menu_name: bool - OPTIONAL - if true, hide the menu's name (won't work in combination with a vertical scrolling direction)
# name: str - OPTIONAL - a string with the name of the menu (can be hidden under certain circumstances) # name: str - OPTIONAL - a string with the name of the menu (can be hidden under certain circumstances)
# default_selection: int - OPTIONAL - the index of the item selected by default (starting with 0) - DON'T USE NEGATIVE INDEXES
# debounce_time: float - OPTIONAL - the debounce time used by the library to debounce button presses # debounce_time: float - OPTIONAL - the debounce time used by the library to debounce button presses
def __init__(self, lcd, buttons: dict, menu_items: list, scroll_direction: bool, cycle: bool, hide_menu_name: bool = False, name: str = "CHOOSE", default_selection: int = 0, debounce_time: float = 0.15): def __init__(self, lcd, buttons: dict, scroll_direction: bool, cycle: bool, hide_menu_name: bool = False, name: str = "CHOOSE", debounce_time: float = 0.15):
# save the argument variables # save the argument variables
self.lcd = lcd self.lcd = lcd
if "prev_btn" in buttons.keys(): if "prev_btn" in buttons.keys():
@@ -34,14 +33,15 @@ class lcdMenu:
self.prev_btn = None self.prev_btn = None
self.next_btn = buttons["next_btn"] self.next_btn = buttons["next_btn"]
self.ok_btn = buttons["ok_btn"] self.ok_btn = buttons["ok_btn"]
self.menu_items = menu_items
self.scroll_direction = scroll_direction self.scroll_direction = scroll_direction
self.cycle = cycle self.cycle = cycle
self.hide_menu_name = hide_menu_name self.hide_menu_name = hide_menu_name
self.name = name self.name = name
self.current_selection = default_selection
self.debounce_time = debounce_time self.debounce_time = debounce_time
self.current_selection = 0 # set a standard value (can/most of the time will be changed directly after __init__ by a call to self.setup())
self.menu_items = [] # set a standard empty (can/most of the time will be changed directly after __init__ by a call to self.setup())
# variables that are very unlikely the user want's to set them (but can be set via <lcdMenuObject>.<attribute_name>, of course) # variables that are very unlikely the user want's to set them (but can be set via <lcdMenuObject>.<attribute_name>, of course)
self.start_execution_msg = "Selected..." # the string displayed when an menu item is selected self.start_execution_msg = "Selected..." # the string displayed when an menu item is selected
self.end_execution_msg = "Closing..." # the string displayed when the callback function of a selected menu item returns self.end_execution_wait = 1 # the time (in seconds) to wait after the callback function of a selected menu item returns self.end_execution_msg = "Closing..." # the string displayed when the callback function of a selected menu item returns self.end_execution_wait = 1 # the time (in seconds) to wait after the callback function of a selected menu item returns
@@ -50,11 +50,21 @@ class lcdMenu:
self.fill_char = '-' # the character used to fill up space (used only on 4x20 displays); MUST BE 1 character long self.fill_char = '-' # the character used to fill up space (used only on 4x20 displays); MUST BE 1 character long
# menu_items: list - a list (-> maintains order!) containing tuples with the following format: (<ENTRY_NAME>,<CALLBACK FUNCTION>)
# start_selection: int - OPTIONAL - the index of the item selected by default (starting with 0) - DON'T USE NEGATIVE INDEXES
def setup(self, menu_items: list, start_selection: int = 0):
self.menu_items = menu_items
self.current_selection = start_selection
def show_selection(self): def show_selection(self):
# a check: # some checks:
# if you scrolling vertically, I found no elegant way to hide the name (there just need's to be something up there!) # 1. if you scrolling vertically, I found no elegant way to hide the name (there just need's to be something up there!)
if self.scroll_direction and self.hide_menu_name: if self.scroll_direction and self.hide_menu_name:
raise TypeError("Hiding the menu name whilst having the scroll direction set to horizontal!") raise TypeError("Hiding the menu name whilst having the scroll direction set to horizontal is unsupported!")
# 2. if there are no menu items to display...
if len(self.menu_items) == 0:
raise TypeError("Can't show empty menus! Maybe you forgot calling self.setup() after initializing me?")
# get some often used values into local variables # get some often used values into local variables
selection_name = self.menu_items[self.current_selection][0] selection_name = self.menu_items[self.current_selection][0]
@@ -63,10 +73,10 @@ class lcdMenu:
# fill the custom character fields in the displays memory # fill the custom character fields in the displays memory
self.lcd.custom_char(0, bytearray([0x04,0x0A,0x11,0x00,0x00,0x00,0x00,0x00])) # arrow up self.lcd.custom_char(0, bytearray([0x04,0x0A,0x11,0x00,0x00,0x00,0x00,0x00])) # arrow up
self.lcd.custom_char(1, bytearray([0x00,0x00,0x00,0x00,0x00,0x11,0x0A,0x04])) # arrow down self.lcd.custom_char(1, bytearray([0x00,0x00,0x00,0x00,0x00,0x11,0x0A,0x04])) # arrow down
self.lcd.custom_char(2, bytearray([0x04,0x0A,0x11,0x00,0x00,0x11,0x0A,0x04])) # arrow up and down (variant 5 from above) self.lcd.custom_char(2, bytearray([0x04,0x0A,0x11,0x00,0x00,0x11,0x0A,0x04])) # arrow up and down
self.lcd.custom_char(3, bytearray([0x11,0x0A,0x04,0x00,0x00,0x04,0x0A,0x11])) # no options (variant 3 from above) self.lcd.custom_char(3, bytearray([0x11,0x0A,0x04,0x00,0x00,0x04,0x0A,0x11])) # no options
self.lcd.custom_char(4, bytearray([0x08,0x08,0x08,0x0F,0x08,0x08,0x08,0x08])) # line with a fork (to show the current selection - h scrolling) self.lcd.custom_char(4, bytearray([0x08,0x08,0x08,0x0F,0x08,0x08,0x08,0x08])) # line with a fork (to show the current selection - v scrolling)
self.lcd.custom_char(5, bytearray([0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08])) # line without a fork (to show unselected items - h scrolling) self.lcd.custom_char(5, bytearray([0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08])) # line without a fork (to show unselected items - v scrolling)
self.lcd.custom_char(6, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x00])) # three dots in a row self.lcd.custom_char(6, bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x00])) # three dots in a row
# now show it off! # now show it off!
@@ -97,31 +107,28 @@ class lcdMenu:
# maybe the following could be done with a crazy math formula - but I want to keep it simpler! # maybe the following could be done with a crazy math formula - but I want to keep it simpler!
# as there aren't enough menu items after the current selection to fill the display, we have to... # as there aren't enough menu items after the current selection to fill the display, we have to...
# ... calculate where to place the current selection, how many items there are before it and how many after it # ... calculate where to place the current selection, how many items there are before it and how many after it
""" menu_items_cut = self.menu_items[::-1][:lines_for_display][::-1] # cut the menu_items list to the relevant last n ones maintaining order (n = number of lines for display)
if lines_for_display > len(self.menu_items): # if it is because of an overall small number of menu items (less than we can display) current_pos_in_cut = -len(self.menu_items) + self.current_selection + lines_for_display # calculate the current index of the selection in the new cut
for i in range(len(self.menu_items)): # draw all the lines
if i < self.current_selection: for i in range(lines_for_display):
self.lcd.putstr(chr(5)+" "+self.menu_items[i][0][0:lw-4]) if i == current_pos_in_cut: # if drawing the currently selected item
self.lcd.putstr(" "*(lw-4-len(self.menu_items[i][0][0:lw-4]))) self.lcd.putstr(f"{chr(4)} {menu_items_cut[i][0][0:lw-4]}")
if len(self.menu_items) <= 1: self.lcd.putstr(" " * ((lw-4)-len(menu_items_cut[i][0][0:lw-4]))) # fit the line
self.lcd.putstr(" " + chr(3)) # no options icon (as there's only one menu item!)
elif self.current_selection == 0 and not self.cycle:
self.lcd.putstr(" "+chr(1))
elif self.current_selection == (len(self.menu_items)-1) and not self.cycle:
self.lcd.putstr(" "+chr(0))
else: else:
self.lcd.putstr(" "+chr(2)) self.lcd.putstr(f"{chr(5)} {menu_items_cut[i][0][0:lw-4]}")
elif i == self.current_selection: self.lcd.putstr(" " * ((lw-4)-len(menu_items_cut[i][0][0:lw-4]))) # fit the line
self.lcd.putstr(chr(5)+" "+self.menu_items[i][0][0:lw-4]) # now the arrow
self.lcd.putstr(" "*(lw-4-len(self.menu_items[i][0][0:lw-4]))) if i == 0: # if the first element is drawn, think about printing or not printing the up arrow
elif i > self.current_selection: if self.current_selection == 0 and not self.cycle: # first item selected and no cycling
self.lcd.putstr(" ") # leave the line with spaces
else:
self.lcd.putstr(" " + chr(0))
elif i == lines_for_display-1: # if the last element is drawn, print a down arrow?
if self.current_selection == (len(self.menu_items)-1) and not self.cycle: # first item selected and no cycling
self.lcd.putstr(" ") # leave the line with spaces
else:
self.lcd.putstr(" " + chr(1)) # arrow down
print("Im here!")
else: # if it is because we reached the end of a (possibly) long menu list -> we have enough items to fill all the available lines
print("Else here!")
"""
print("ToDo!")
else: # there are enough items to fill the display after the current selection else: # there are enough items to fill the display after the current selection
# the first line # the first line
self.lcd.putstr(f"{chr(4)} {selection_name[0:lw-4]}") self.lcd.putstr(f"{chr(4)} {selection_name[0:lw-4]}")
@@ -129,12 +136,12 @@ class lcdMenu:
if len(self.menu_items) <= 1: if len(self.menu_items) <= 1:
self.lcd.putstr(" " + chr(3)) # no options icon (as there's only one menu item!) self.lcd.putstr(" " + chr(3)) # no options icon (as there's only one menu item!)
elif lines_for_display == 1: # if there is exactly one line to display the menu entries elif lines_for_display == 1: # if there is exactly one line to display the menu entries...
if self.current_selection == 0 and not self.cycle: if self.current_selection == 0 and not self.cycle: # ...and the first element is selected and cycling is disabled so you can't go back
self.lcd.putstr(" "+chr(1)) self.lcd.putstr(" "+chr(1))
elif self.current_selection == (len(self.menu_items)-1) and not self.cycle: elif self.current_selection == (len(self.menu_items)-1) and not self.cycle: # ...as before but with the last element -> you can't go further
self.lcd.putstr(" "+chr(0)) self.lcd.putstr(" "+chr(0))
else: else: # ...or anything else (cycling or in the middle -> you can go in both directions
self.lcd.putstr(" "+chr(2)) self.lcd.putstr(" "+chr(2))
elif self.current_selection == 0 and not self.cycle: # first item selected and no cycling elif self.current_selection == 0 and not self.cycle: # first item selected and no cycling
self.lcd.putstr(" ") # leave the line with spaces self.lcd.putstr(" ") # leave the line with spaces
@@ -190,8 +197,8 @@ class lcdMenu:
return_value = selection[1]() return_value = selection[1]()
# show a exit when there's no specific return value # show a exit when there's no specific return value
if not return_value: # if the return value is None / nothing was returned if not return_value: # if the return value is None / nothing was returned -> show a closing message
while self.ok_btn.value() == 1: time.sleep(self.debounce_time) # wait till ok_btn release (e.g. if the "program" is a simple send action) while self.ok_btn.value() == 1: sleep(self.debounce_time) # wait till ok_btn release (e.g. if the "program" is a simple send action)
self.lcd.move_to(0,0) self.lcd.move_to(0,0)
if self.lcd.num_lines == 4: if self.lcd.num_lines == 4:
self.lcd.putstr(f"{self.fill_char*lw}{' '*lw*2}{self.fill_char*lw}") # fill the first and last line with 'fill_char's self.lcd.putstr(f"{self.fill_char*lw}{' '*lw*2}{self.fill_char*lw}") # fill the first and last line with 'fill_char's
@@ -199,6 +206,8 @@ class lcdMenu:
self.lcd.putstr(f"[{selection[0][0:lw].center(lw-2)}]{self.end_execution_msg[0:lw].center(lw)}") self.lcd.putstr(f"[{selection[0][0:lw].center(lw-2)}]{self.end_execution_msg[0:lw].center(lw)}")
sleep(self.end_execution_wait) sleep(self.end_execution_wait)
self.show_selection() self.show_selection()
else: # -> show no message and quit directly back into the lcdMenu
self.show_selection()
# listen for button presses (this method should be called in an endless loop, see method run) # listen for button presses (this method should be called in an endless loop, see method run)
@@ -216,11 +225,17 @@ class lcdMenu:
while self.ok_btn.value() == 1: sleep(self.debounce_time) # wait till release while self.ok_btn.value() == 1: sleep(self.debounce_time) # wait till release
self.execute_selection() self.execute_selection()
def stop(self): # here to act as a callback for a menu entry (if the user wants to ofc!)
self.running = False
return True
def run(self): def run(self):
# show the selection first # show the selection first
self.show_selection() self.show_selection()
self.running = True
# then listen on button presses in a loop... # then listen on button presses in a loop...
while True: while self.running:
self.loop() self.loop()
return True # to prevent a "Closing menu ..." in submenu-situations

36
examples/quittable.py Normal file
View File

@@ -0,0 +1,36 @@
from lcdMenu import lcdMenu
from machine import Pin, I2C
from PCF8574T import I2C_LCD
prev_btn = Pin(13, Pin.IN, Pin.PULL_DOWN) # input of the first btn
next_btn = Pin(14, Pin.IN, Pin.PULL_DOWN) # input of the second btn
ok_btn = Pin(15, Pin.IN, Pin.PULL_DOWN) # input of switch
LCD = I2C_LCD(I2C(0, sda=Pin(8), scl=Pin(9), freq=400000),0x27,2, 16)
def first_callback():
print("first_callback() called!")
def second_cb():
print("second_cb() called")
def third_cb():
print("third_cb() called")
return True
button_mappings = {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn}
submenu = lcdMenu(LCD, button_mappings, scroll_direction=True, cycle=False, hide_menu_name=False, name="Submenu!")
# the submenu.stop callback is a special callback specifically designed for use cases where the lcdMenu ist started with run()
# submenu.stop breaks an infinite loop inside the run() method, essentially quitting the run() method and giving back flow to the caller context
# so we can utilize this for us to quit a submenu (or a "main" menu if you want to, of course!)
submenuItems = [("first item", first_callback),
("second item", second_cb),
("third item", third_cb),
("back", submenu.stop)]
submenu.setup(submenuItems)
mainmenu = lcdMenu(LCD, button_mappings, scroll_direction=False, cycle=True, hide_menu_name=False, name="Main menu!")
mainmenu.setup([("Sub menu",submenu.run)])
mainmenu.run()

View File

@@ -20,6 +20,8 @@ def third_cb():
menuItems = [("first item", first_callback), menuItems = [("first item", first_callback),
("second item", second_cb), ("second item", second_cb),
("third item", third_cb)] ("third item", third_cb)]
menu = lcdMenu(LCD, {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn}, menuItems, scroll_direction=False, cycle=False, hide_menu_name=True, name="Fullscreen!") menu = lcdMenu(LCD, {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn}, scroll_direction=False, cycle=False, hide_menu_name=True, name="Fullscreen!")
menu.setup(menuItems)
menu.run() menu.run()

View File

@@ -22,7 +22,11 @@ menuItems = [("first item", first_callback),
("third item", third_cb)] ("third item", third_cb)]
button_mappings = {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn} button_mappings = {"prev_btn":prev_btn, "next_btn": next_btn, "ok_btn": ok_btn}
submenu = lcdMenu(LCD, button_mappings, menuItems, scroll_direction=True, cycle=False, hide_menu_name=False, name="Submenu!") submenu = lcdMenu(LCD, button_mappings, scroll_direction=True, cycle=False, hide_menu_name=False, name="Submenu!")
submenu.setup(menuItems)
mainmenu = lcdMenu(LCD, button_mappings, scroll_direction=False, cycle=True, hide_menu_name=False, name="Main menu!")
mainmenu.setup([("Sub menu",submenu.run)])
mainmenu = lcdMenu(LCD, button_mappings, [("Sub menu",submenu.run)], scroll_direction=False, cycle=True, hide_menu_name=False, name="Main menu!")
mainmenu.run() mainmenu.run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB