LCD Menu: Link UI Selections to Application State
This installment of How to Program a Raspberry Pi Pico with Rust covers how to make the navigation and selections a user is making on a menu visible and relevant to the application the menu is meant to control.
LCD Menu Control Design
Let’s start by assuming there is a set of files that form the code that controls the LCD menu. If this is the case, user interactions, navigation commands, and any other relevant information related to menu stuff are kept here, in this area of the program.
This is a good thing! But the problem now becomes, “How can the rest of the program use this information?” To address this, let’s address using interfaces to migrate context from one scope into another one. I call this practice Context Traversal. Let’s briefly cover this concept.
Early Housekeeping: Limit Context Traversal as Much As Possible
It’s a priority in this project to keep contexts lightweight and to keep the surface area between them as small as possible.
Context Traversal is anytime the instruction pointer moves from one context to another, but after moving it needs some of the data that was available in the scope it was previously in. This need for context to move from one place to another is a source of obfuscation, spaghetti code, untestable code, and various other maintenance-related problems. But it’s also an immensely powerful tool, that makes programming much more fun for me.
What are the contexts we are concerned with?
In PicoPonics at the time of writing, and in any application of this size and configuration, we will have 3 contexts to be concerned with.
1. The UI Context (Our LCD Menu Code)
2. The Application State Context (a representation of the current state of the system)
3. The Hardware Abstraction Layer (HAL)
It’s not feasible or us to control this system without exchanging information between these contexts. Some programs will not distinguish these contexts at all, and all data will be visible in all contexts at all times.
For a proof of concept that’s fine, but for software that will have any real lifecycle, it’s a pretty bad idea, and that’s why
Deciding which information is needed by the application state coming from the UI layer?
Exactly and only the amount of information it needs to accurately update the physical hardware to be in the desired state. The application state serves as a single source of truth, and It will change its own shape and pass that new shape to the hardware layer. The hardware layer is updated based on the values in the application state.
What selection the user made (the pure selection information)
A way to determine what that selection is (what part of the system does this selection impact)
A way to determine how to implement that selection (how to map the selection into the running system)
User Selections
The menu may have lots of underlying data and information that is used for the purpose of controlling the menu. Most of that isn’t useful outside the context of the menu. Isolating the useful bits and making that available to the rest of the application is the responsibility of the UI context. Handle User Selections!
What actually is that selection?
When deciding to migrate data from the UI context, the intent of that data should also move as well. This can be done through variable names, comments, structures with named properties and methods, etc.
The point is that some data being referenced in a place where it’s literally out-of-context feels very alien, and will be meaningless to the next developer unless they go babysit that variable and see exactly how it’s created. Give the next developer (or your future self) a bit of information about what this migrated data is meant to do.
Implement the migrated data!
Finally, make sure that the meaningfully migrated data can be implemented directly into the place where it’s traveling to. Mutating, massaging, and other operations should happen in the origin context (LCD menu context should create a struct that is meaningful to the application state context).
If the task is to “Update the application state so that pin 12 is active” the data structure from the menu should contain that information, and be plug-and-play directly into the application state. From the ‘global’ scope, we should see 1 function call to get the relevant menu data, then a second function call to load that data into the state. Decoupled, Concerns are Separated, but they use a common interface to exchange application state data.
Descriptive. Maintainable. Manageable.
Example Code
This is some example code because the real code can be found in the repo for PicoPonics. And at the time of writing it’s in a state of disrepair due to some hasty debugging so I wanted to convey the concept as cleanly as possible. The full example code with working unit test(s) can be found here:
Incoming Code Wall!
The app state looks like this:
And the interface between them (technically interface, technically member property of the app_state struct) looks like this:
Contextual Exchange Via Interface
That seems like it could be a cool acronym, CEVI. Anyway, above there are our two noted contexts, LCDMenu (The UI Layer), and AppState. They are independent of one another, and during design, it can be a bit confusing or daunting to implement a strategy to move data between the two contexts.
My method of dealing with this is to boil down the feature I’m currently working on to the bare minimum amount of information that is needed at a specific point in either context. Then I craft an interface class or function to handle that scenario, name it so that it explains to me what’s happening, and then carry on programming it.
The method below from the LCDMenu struct implementation does exactly that.
pub fn create_pin_update(&self) -> Pin {
let mut pu: Pin = Pin::new(self.pin_selection);
pu.active = menu_option_as_bool(&self.option);
pu
}
This Data is Prepped for Departure
The type Pin is derived from variables that are present within the LCDMenu struct. We are taking variables from one context, mapping them onto a structure with obvious names, and that structure can then be used in a different context, and just from the naming and commenting of that structure, the next developer (future you) will understand what the intent behind this code is.
Programmed to Receive
Next, we have the AppState struct, which has a function that is built for handling an incoming Pin struct and maps the values from that struct accordingly, onto itself.
pub fn update_pin_state(&mut self, pin: Pin) {
self.pins[pin.id as usize].update_pin_state(pin.active);
}
While this is an oversimplification, and probably not perfect rust code, the concept is sound. LCDMenu and AppState with this code now have a one-way communication set up where the LCDMenu has the ability to publish a Pin Update, that the App State knows how to handle.
This is exactly what we set out to do, and this code is perfectly adequate (and extendable) to be able to carry out further responsibilities. ((like actually having text do display in the LCD menu)).
Conclusion
This post was focused almost equally on code abstractions as it was the concept of programming an LCD Menu, but the principles were covered pretty loosely:
Determine what data needs to be present for the screen
Create means of manipulating and navigating around using that data
Create a means of migrating selection data from the UI context to the Application State.
Interfaces should be thought of from both sides (all sides if touching many places) when picking names and commenting intent