Input Handling
Button and encoder input
Input Callback
Use the on_input callback in the app descriptor:
void my_input(os_app_t* app, uint8_t btn, KeyStateEnum state) {
if(btn == BTN1 && state == KEY_STATE_PRESSED) {
// BTN1 pressed
}
}BTN4 and BTN5 HOLD are the firmware's on a tapp that DECLARES a type and never reach on_input there; their presses do (and those two PRESS cells are the tapp's to label, see ui_hints_set()). By role:
- utility (the default, declare nothing): both holds and every hint cell belong to the tapp, and it exits with os_app_exit(). Opened from the utils list (BTN1 hold on setup). In WINDOW mode BTN4/BTN5 HOLD close the window instead - the menu underneath cannot take input while a window is up; presses stay with the tapp. See os_app_set_window().
- standalone: a hold reveals the tapp's hints or, if it installed none, opens the main menu straight away - a band of only the firmware's two icons would be pointless. Hints up, BTN4 hold opens the main menu over the running tapp (audio keeps going, tick() pauses, on_pause/on_resume bracket it); BTN5 hold puts away hints the FIRMWARE revealed, and opens the menu too on a band the tapp keeps up with ui_hints_show(true). A short BTN4/BTN5 press on a cell left blank dismisses system-revealed hints and is swallowed. The user leaves through the main-app switcher (BTN1 hold on the main menu's tape ENTRY - the gesture is on the entry, not a row inside it - which lists tapedecks first while a tapp runs: picking it returns to the decks, picking another app swaps the running one).
- source: BTN4 hold = back to the menu, BTN5 hold = out to the main screen; both background the tapp. While the tapp's own ui_menu_t is shown it is modal and takes those holds itself (back / close). os_app_exit() remains for a programmatic exit.
Button IDs
| id | Common use |
|---|---|
| BTN1 | Select |
| BTN2 | - |
| BTN3 | - |
| BTN4 | system: quick hints / main menu (HOLD) on a standalone; press goes to the tapp |
| BTN5 | system: close hints / main menu (HOLD) on a standalone; press goes to the tapp |
| BTN_ENCODER | the encoder turned - NOT a button |
There is no encoder push button. BTN_ENCODER fires when the encoder is turned, and its state is always KEY_STATE_RELEASED - never branch on it. Either ignore the event and poll os_controls_encoder_get_delta() from tick(), or treat it as "something moved" and read the delta in response. hint_press(btn) / hint_hold(btn) name a button's cells.
Get encoder rotation delta since last call
Returns — Signed delta (positive = clockwise, negative = counter-clockwise)
int32_t diff = os_controls_encoder_get_delta();
if (diff != 0) {
model->value += diff;
}Take (or give back) the BTN5 hold-to-power-off gesture
| bool | state | true = BTN5 hold powers the device off (the default), false = the app owns it |
A hold on BTN5 switches the device off after ~3.2s, from anywhere - and from ~1s the hint cell in BTN5's column turns into a filling power glyph. That is the right default, but it collides with an app that maps BTN5 holds to something of its own: hold "right" in a game for three seconds and the device powers down mid-play.
Pass false to take the button, true to give it back. Take it only if the app actually uses BTN5 holds - while the app holds it, the user cannot switch the device off without leaving the app first, so leaving must remain possible.
Restored automatically when the image closes, INCLUDING when init() returns false (which never runs deinit()) and when the tapp faults. The app should still give it back explicitly; the sweep is a safety net, not the contract.
static bool app_init(os_app_t* app) {
os_controls_enable_shutdown(false); // BTN4/BTN5 holds are our D-pad
return true;
}
static void app_deinit(os_app_t* app) {
os_controls_enable_shutdown(true);
}