{"id":13802326,"url":"https://github.com/sgall17a/encodermenu","last_synced_at":"2025-10-25T19:30:49.859Z","repository":{"id":63732844,"uuid":"350277372","full_name":"sgall17a/encodermenu","owner":"sgall17a","description":"Simple GUI menu for micropython using a rotary encoder and basic display.","archived":false,"fork":false,"pushed_at":"2021-04-08T11:18:47.000Z","size":91,"stargazers_count":108,"open_issues_count":1,"forks_count":18,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-10-01T05:11:31.729Z","etag":null,"topics":["asyncio","encoder","menu","micropython","oled","rotary","wizard"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sgall17a.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-03-22T09:10:15.000Z","updated_at":"2024-07-13T06:36:40.000Z","dependencies_parsed_at":"2022-11-24T21:11:31.983Z","dependency_job_id":null,"html_url":"https://github.com/sgall17a/encodermenu","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgall17a%2Fencodermenu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgall17a%2Fencodermenu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgall17a%2Fencodermenu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sgall17a%2Fencodermenu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sgall17a","download_url":"https://codeload.github.com/sgall17a/encodermenu/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219865145,"owners_count":16555931,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","encoder","menu","micropython","oled","rotary","wizard"],"created_at":"2024-08-04T00:01:42.110Z","updated_at":"2025-10-25T19:30:49.520Z","avatar_url":"https://github.com/sgall17a.png","language":"Python","readme":"# Micropython encoder based menu\n\n\nThis is a simple menu system written in micropython.  It uses a switch,  a rotary encoder and an OLED display.  \nIt was developed  on a Raspberry Pi Pico but also runs on an ESP32 and ESP8266.\n\nThe prototype used a little 128 * 64 pixel SSD1306 based OLED.  \nIt could an be  adapted to other displays using micropython's framebuffer or even  to a very basic like a one-line display like a liquid crystal display. \nThe rotary encoder I used has a switch on the shaft, which is used as the click button. \nThe Rotaryirq library for an ESP32 worked perfectly on Raspi Pico and the display used the library SSD1306I2c.py\n\nWhen the Pico is  started the root menu is shown and menuitems are actioned when the switch is clicked.  \nAny number of subitems can be shown.  \nPossible menu actions include running a Python funciton, entering an integer by twiddling the encoder and entering a string.\n\nSince some functions can be slow or can block, the menu runs within an asyncio loop.\n\n## Libraries\nFor rotary encoder.\nhttps://github.com/miketeachman/micropython-rotary\n\nFor OLED display:\nhttps://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py\n\nAny display driver that depends on the module framebuf should work out of the box.\nIt would be simple to adapt the display function for a single or two line dislay like an LCD.\n\nI have written my library with a minimal  \"hardware abstration layer\" in case you want to use other libraries.\n\n#### WARNING - STILL BEING DEVELOPED.  Some of the information below may not immediately match the actual library code.\n\n## Defining menus and Submenus\n\nMenus are defined as a list of menu-items. Submenus are really the same as menus.\nEach menu item is defined as a pair of values:\n\n1. A Caption  (string)\n2. An action function (a python function with NO parameters)\n\nExample of a main menu and two submenus called trees and patterns.\n\n\n  ```python\ntrees     = wrap_menu( [('gum',wizard),('ti-tree',info),('red-gum count',get_integer),('willow',treesize),('Back!',back)])\n\npatterns  = wrap_menu( [('Chaser',yellow),('Red',red),('Blue',blue),('Rainbow',rainbow),('Back!',back)])\n\nmain_menu = wrap_menu( [('Patterns',patterns),('trees',trees),('Brightness',brightness)])\n\n  ```\n\n\nHere we have defined three menus, 'trees', 'patterns' and 'main_menu'.  \n\nNote that the menu caption needs quotes (because it is a string), but the function does not because it is just the name of a function.\n\nTo transform our list into a function we pass it to another function called 'wrap_menu'.  \nNote that  our wrapped menu becomes a function,  so it can be the function that is called from a menuitem.  In this way we get sub-menus.\n\nMenu functions must be defined before they are used.  The root menu should be the last to be defined.\n(Later versions may address this by allowing an action function to be defined as a string which is turned into a function on a second pass).\n\n### Functions to get information\n\nThere are three predefined functions to get information:\n\n1. get_integer\n2. get_selection\n3. wizard\n\n#### get_integer\n\nThis allows us to set a number by twiddling the shaft of the encoder.  The number is entered by clicking the switch.  \nThe result is stored in a global dictionary called data.  The key is set by a field parameter.\n\n``` python\nsethours   = get_integer(field = 'hour', low_v=1,high_v=24,increment=1,caption='Hours',field='hour')\n```\n\nLater the value can be retrieved from the global dictionary data  as shown in the example below:\n\n``` python\nif data['hour'] = 10:\n\tpass\n\t#Then do something\n\n```\n\nThe value from the encoder ranges from low to high.  It goes up or down by one each time we turn the encoder by one click. \n\nSometimes the desired value is a relatively high number, say 0-100 for percentages.\n\nDoing 100 clicks can be tedious if we do not really need that degree of precision so there is an option for the encoder value to be multiplied by Increment when it is  displayed.  \nIn this way,  the display will have  increment of 10 for instance,  we can go from 0 to 100 with 10 clicks.  \nNote that the stored value is still the raw value. \nFor instance, with an increment of 10 the display may show 50 but the stored value will be 5.\n\n#### get_selection\n\nThe selection function lets us get a string value from a list of values. The list is similar to a menu's list but  also has a field parameter so it can be retrieved from the global menu_data dictionary.\n\n```python \ncolour1 = selection('colour1',['RED',('Green','GREEN'),('Blue','BLUE'),('Yellow','YELLOW'),('WHITE','white')])\n```\n\nThe name displayed is the first value in the tuple and the value returned is the second element of the tuple.  \nThere is an option to just provide a string (say \"RED\"). In this case the string value is exanded to a tuple ('RED','RED') behind the scenes.\n  \nAs we turn the shaft the name of the various colours are scrolled, in the same way as a menu.  \nWhen we click the shaft  value string is stored in the global dictionary and we return to the parent menu.  \n\n**Default values for selection and get_integer**\n\nA selection or get_integer is initialised to the value already in the dictionary, \nif a value exists for that field, otherwise the initial value is zero or an empty .  \nThis way we can get a default value by storing values in the dictionary before starting the program and we can revisit the selection to change values.\n\n#### Wizard and get_integer\n\nIn small microprocessor systems we often want to enter a series of numbers.  \nFor instance we may want to set   hours, minutes and seconds for a clock or day, month year to set the date.  \n\nThe wizard calls a series of functions in sequence, usually get_integer.  A  wizard  is defined similarly to a menu.\n\n``` python\ntimewizard = wizard([(\"Hours\",sethours),(\"Minutes\",setminutes),(\"Seconds\",setseconds)])\n```\n\nIn this example, the wizard will gather hours, minutes and seconds in that order,   then return.\nThe wizard list looks like the menu list but in fact the caption part is ignored ( because a caption has to be provided to get_integer.)\n\n ### The info function.\n\n The info function just displays a screen of text which will be shown when you click its menuitem. \n Any click or scroll with clear the display (back to the parent menu).   \n You can provide the text as a simple string but you can also provide a function that returns a string.  This would allow you, for instance, to display the current time. \n\nExamples of these alternatives are show below:\n\n```python\nshowtime = info('I dont have a clock')\nshowtime = info(my_gettime_function)  \n# don't use brackets on the function name\n```\n\n## How to get data out of the system.\n\nAn integer or a string  is returned by the functions get_integer and selection.  Both of these functions  have a parameter called field.  \nThe fields is used as the key to store the value in a global dictionary menu_data.\n\nSince the data dictionary is global it can accessed by other functions.\n\n### Hardware abstraction functions\n\nThese functions are provided  as a form of hardware abstraction in case you want to use different libraries from  SSD1306_i2c and  rotary-irq library (which I think is probably unlikely)\n\n**set_encoder(minimum_value, maximum_value,increment)**\n\n*setencoder* sets the rotary encoder so that value ranges between the maximum and minimum value inclusive. It increments on clockwise clicks and decrements on anti-clockwise clicks. It wraps over to minimum after click maximum and vice versa.  This is the standard behaviour of the library rotary_irq.\n\nCalls  *rotary_irq.setencoder()*\n\nSetencoder is provider as a function as a form of hardware abstraction in case you use a different rotary encoder library.\n\n**get_value()**\n\nReturns  the encoder value as an integer.\n\nSame as rotary_irq.encoder.value()\n\n***display(text)***\n\nDisplay lines of text on the display device.  Up to 4 lines of text are allowed.\n\n\n\n### Utility functions\n\n***back()***\n\nClicking goes forward in a menu and scrolling goes up and down but we need a way to go back.  \nThis is achieved by calling the back function as a menu action. (see menu examples).\n\nIf we had a way to provide more events than simple clicking and scrolling we could use one of these events to go back.  \nPossible sources for such an event would be another button or using long push or double click on a single button.  \nWhile easy to implement, this has not been done since the current system seems quite intuitive.\n\n***make_task(coroutine)***\n\nThis simple turns a co-routine into a task and stores the task in a global variable called task.  Its main use is to hide the global.\n\n***Stop()***\n\nThis cancels the global task above.  This way we can make and stop tasks without worrying about global declarations.  For instance if we have a rolling rainbow display on neopixels this should be run as a task, otherwise it will block the menu.  We can make the task by passing our function or more precisely our co-routine to *make_task()*.  If we want to run a different pattern we would stop the current pattern by calling *stop().*\n\n\n\n ##  Writing action functions.\n\nThere are several considerations in writing action functions.\n\n\n\n**Simplest**\n\nThe simplest action function is just a normal python function with no parameters.\n\n\n\n  ```python\n  def  showblueneopixels():\n    \tmake_strip_blue()\n  ```\n\nThis will work  as a menu function.  The menu will be unchanged, which is usually fine, especially if your function is small and quick. \n\n**An action function with parameters.**\n\n``` python\ndef showpixels(colour):\n  make_strip(color)\n \n#There are two ways to convert this into a function with no parameters\ndef showpixels(color = 'blue'):\n  make_strip(color)\n# calling showpixels() with no parameters will make the strip blue\n\ndef wrap_show_pixels():\n  color = 'blue'\n  def show_pixels(color):\n    \tmake_strip(color)\n  return show_pixels\n\n# calling wrap_show_pixels() with no parameters will make the strip blue\n  \n```\n\n**Writing a co-routine or multiasking.**\n\nA long running action on your microprocessor could block the menu system.  \nTo avoid this the menu system supports co-operative multitasking using uasyncio.\n\nThere are several tutorials about multitasking with asyncio. Peter Hinchs tutorial is a particularly good one but gets moderately advanced. https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md \n\n\n\n**HOW TO TURN A FUNCTION INTO A CO-ROUTINE AND THEN A TASK **\nI will assume that our  program is long running because it  has a loop. This is  usually the case.\n\n1. Import uasyncio as asyncio to make the async functions available.\n\n1. put async before the def part of the function.\n2. Put \"await asyncio.sleep(0)\" somewhere in the loop so it is called frequently.  This makes our function play nicely with others, including the main menu.\n3. Turn the co-routine into a task.  This also starts it running, so we dont have to await it. Make_task(co-routine ) is provided as a utility.\n4. cancel the task when we want our loop to stop. Stop() is provided as a utility.\n\nExample:\n\n``` python\nimport uasyncio as aysncio\nfrom encodermenu import make_task, stop\n\n# Define this function in the body of the program or (better) locally in an imported action module.\n# This is the function that actually does the work.\n\nasync def worker_loopy_function(): #This makes it a co-routine\n    do_something()\n    await asyncio.sleep(0)  #This makes it play nicely with others (including our menu)\n    \n#In our action function\ndef my_action_function():\n  stop() # stop any previously running tasks \n  make_task(worker_loopy_function) # this will make a task and run it\n  #note there are no brackets on our worker_loopy_function\n\n```\n\n# How it works\n\n* The system runs a bit like a normal event driven GUI.\n* There is a event loop that polls the switch and the encoder. \n* If the switch is pressed or the value of the encoder changes then an on_click or on_scroll event is called.\n* Events are handled by an object, which can be a Menu, GetInteger, Selection, Info, Wizard and so on.\n* The object currently handling  events is a global variable called current.\n* We change menus and entry screens etc by changing the current object.\n* The convenience function back() pops the parent object off the stack and makes it current\n* The main loop runs as an asyncio task so it does not block.\n  Any function called within the menu system is also running within the asyncio loop so it can be a task or routine.\n* A convenience function  make_task  stores the task in a global variable called task.\n* A  convenience function called stop can cancels  the running task.\n  (Note: This simple system will only handle one task. You can run more tasks, but you will have to manange them yourself).\n\n## Going back up the menu.\nYou may have noticed that we can go down our menu tree but we cant go back.\nTo get around this a back() function has been provided which is called like any other menu-item action.\nAn alternative would be to provide a back button for a separate event.  Another alternative would be to get some extra events off our single button (like long-press or double click)  \n\nThe menu uses  uasyncio which  has  a good primitives libary from Peter Hinch that allows us to easily program for long presses or double clicks. I may have a look at this later but I think the current system works pretty well.\n\n### Writing a menu (menus are functions and actions are functions)\n\nWe note that a menuitem is defined as tuple composed of a string menu item caption and an action that is performed if the menu is clicked, like so ('Caption1', action1).  A menu is a list of menu-items.  (Note - we could use either lists or tuples  or some other iterative - it does not really matter-but lists and tuples look nice).  \n\nAfter we have provided a list of menu-items we have to wrap the list up into a function and we do this by using the wrap function.  This means that both actions and submenus are handled the same way in our system.  A submenu is simply an action that installs a new menu.\n\n","funding_links":[],"categories":["Libraries"],"sub_categories":["IO"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsgall17a%2Fencodermenu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsgall17a%2Fencodermenu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsgall17a%2Fencodermenu/lists"}