UI Mode System Architecture in rpi_pico_wav_player: State Machine Design in src/UIMode.h
The UI mode system in rpi_pico_wav_player implements a statically-allocated state machine where each screen inherits from the abstract UIMode class, transitions occur by returning new mode pointers from update(), and all seven concrete modes are managed through a singleton registry in src/UIMode.h.
The rpi_pico_wav_player project uses a compact, resource-efficient UI mode system architecture to manage multiple screens on the Raspberry Pi Pico. Defined primarily in src/UIMode.h and implemented in src/UIMode.cpp, this architecture employs an abstract base class pattern with static allocation to minimize heap fragmentation while supporting complex navigation flows like file browsing and configuration menus.
Core Components of the UI Mode Architecture
The UIMode Abstract Base Class
At the heart of the system is the abstract class UIMode defined in src/UIMode.h. This class establishes the contract that every UI screen must fulfill through three pure virtual methods:
update(): Processes button events and timer-based logic, returning aUIMode*pointer to determine the next active state.entry(): Called when transitioning into the mode, receiving a pointer to the previous mode for context-aware initialization.draw(): Renders the screen content to the LCD via the sharedLcdCanvassingleton.
The base class also maintains common state including btn_act and btn_unit for button events, idle_count for power management timeouts, and a static pointer to UIVars for shared application data.
Static Mode Registry and ui_mode_ary
Rather than using dynamic allocation, the architecture pre-allocates all mode instances in a static array. The UIMode class declares ui_mode_ary, a std::array containing singleton instances of every concrete mode:
// From src/UIMode.h
static std::array<UIMode*, 7> ui_mode_ary;
The UIMode::initialize() function in src/UIMode.cpp populates this array by instantiating each concrete class:
void UIMode::initialize(UIVars* vars) {
ui_mode_ary[InitialMode] = new UIInitialMode(vars);
ui_mode_ary[ChargeMode] = new UIChargeMode(vars);
ui_mode_ary[OpeningMode] = new UIOpeningMode(vars);
ui_mode_ary[FileViewMode] = new UIFileViewMode(vars);
ui_mode_ary[PlayMode] = new UIPlayMode(vars);
ui_mode_ary[ConfigMode] = new UIConfigMode(vars);
ui_mode_ary[PowerOffMode] = new UIPowerOffMode(vars);
}
Access to modes occurs through UIMode::getUIMode(ui_mode_enm_t mode), which returns the pointer stored in the array.
Shared State with UIVars
All modes share mutable state through the UIVars structure, passed to each constructor and stored as a static member in the base class. This structure contains:
- File system navigation data (
dir_stackfor directory hierarchy traversal) - Playback state (current file index, play mode settings)
- UI configuration (cursor positions, display preferences)
- System flags (battery status, USB connection state)
The dir_stack, implemented as a static std::stack<std::string>, enables the file browser modes to remember navigation history when drilling into subdirectories.
State Machine Implementation and Mode Transitions
The Main Loop State Machine
The UI mode system architecture operates as a cooperative state machine driven by the main loop in src/arduino_main.cpp. The loop maintains a pointer to the current mode and repeatedly executes the update-draw cycle:
static UIMode* cur = UIMode::getUIMode(UIInitialMode);
while (true) {
cur = cur->update(); // State transition happens here
cur->draw(); // Render the new state
}
This design decouples the main application logic from individual screen implementations, allowing each mode to encapsulate its own behavior while the framework handles the lifecycle.
Transition Logic in update() Methods
State transitions occur when a mode's update() method returns a pointer to a different mode rather than this. The base class provides the getUIMode() accessor to retrieve mode instances by their enum identifier:
// Example transition from any mode to the file browser
UIMode* SomeMode::update() {
ui_get_btn_evt(btn_act, btn_unit); // Check for input
if (btn_act == Enter && btn_unit == Center) {
// Transition to file view mode
return UIMode::getUIMode(FileViewMode);
}
// Remain in current mode
return this;
}
When a transition occurs, the new mode's entry() method receives a pointer to the previous mode, enabling context-aware initialization. For example, UIFileViewMode uses this to determine whether it arrived from the opening screen or from playback.
Concrete UI Mode Implementations
The architecture defines seven concrete mode classes, each handling a distinct screen:
- UIInitialMode: Displays the splash screen and determines whether to proceed to charging or opening based on USB power detection.
- UIChargeMode: Shows battery charging status and implements idle timeout logic for automatic power-off.
- UIOpeningMode: Mounts the SD card, restores the navigation stack from flash memory, and displays the opening logo.
- UIFileViewMode: Implements the file browser with directory listing, navigation via
dir_stack, and file selection for playback. - UIPlayMode: Manages audio decoding, displays track information via
readTag(), and handles playback controls (play, pause, skip). - UIConfigMode: Presents hierarchical configuration menus for volume, UI layout, and playback preferences.
- UIPowerOffMode: Saves the current navigation state to flash via
storeToFlash(), performs clean shutdown, and manages reboot sequences.
Each class overrides the three virtual methods from UIMode, with UIFileViewMode and UIPlayMode containing the most complex logic due to their interaction with the file system and audio decoder.
Key Source Files and Build Integration
| File | Role in UI Architecture |
|---|---|
src/UIMode.h |
Declares the abstract UIMode class, UIVars struct, mode enum, and interfaces for all concrete modes. |
src/UIMode.cpp |
Implements base class functionality including initialize(), getUIMode(), and all concrete mode methods. |
src/arduino_main.cpp |
Contains the main state machine loop that drives update() and draw() calls. |
src/ui_control.h / src/ui_control.cpp |
Provides ui_get_btn_evt() for button event processing used by all modes. |
src/LcdCanvas.h / src/LcdCanvas.cpp |
Singleton LCD abstraction used by draw() methods across all modes. |
src/file_menu_FatFs.h |
File system interface used by UIFileViewMode for directory navigation. |
Summary
- The UI mode system architecture in
rpi_pico_wav_playerimplements a statically-allocated state machine centered on the abstractUIModeclass defined insrc/UIMode.h. - Seven concrete modes (Initial, Charge, Opening, FileView, Play, Config, PowerOff) inherit from
UIModeand implementupdate(),entry(), anddraw()methods. - State transitions occur when
update()returns a pointer to a different mode retrieved viaUIMode::getUIMode(), while the main loop inarduino_main.cppdrives the state machine. - Shared resources including
UIVars,LcdCanvas, anddir_stackenable consistent navigation, display rendering, and file system browsing across all modes without dynamic memory allocation during runtime.
Frequently Asked Questions
How does the UI mode system handle state transitions between screens?
State transitions are handled through return values in the update() method. When a mode wants to change screens, it returns a pointer to the target mode obtained via UIMode::getUIMode(ModeEnum). If no transition is needed, the method returns this. The main loop in src/arduino_main.cpp assigns this return value to the current mode pointer, effectively switching the active state machine context.
What is the purpose of the UIVars structure in the UI architecture?
UIVars serves as the global mutable state container shared across all UI modes. Defined in src/UIMode.h and passed to each mode during initialization, it stores critical runtime data including the directory navigation stack (dir_stack), current file cursor positions, playback state, battery status, and configuration flags. This design eliminates the need for modes to maintain separate state copies while ensuring consistent data access during screen transitions.
How does the file browser mode manage directory navigation history?
The UIFileViewMode class utilizes a static std::stack<std::string> called dir_stack to track directory traversal history. When users navigate into subdirectories, the current path is pushed onto the stack. When returning to parent directories or transitioning between related modes, the stack provides the navigation context needed to restore the file browser's position. This mechanism is part of the shared UIVars structure accessible to all modes.
Why does the architecture use a static array instead of dynamic allocation for modes?
The UIMode architecture uses a static std::array named ui_mode_ary to store singleton instances of all concrete modes, populated once during UIMode::initialize(). This design choice eliminates heap fragmentation risks on the resource-constrained Raspberry Pi Pico microcontroller, ensures deterministic memory usage, and provides O(1) access to any mode via getUIMode(). The approach aligns with embedded systems best practices where dynamic memory allocation during runtime is minimized to prevent memory leaks and fragmentation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →