umsh_ux_display_tracker/lib.rs
1#![no_std]
2
3//! UX mechanism for display-tracker-class UMSH boards.
4//!
5//! This crate is the user-experience layer for boards whose physical UX
6//! is a small display plus a button — optionally with a D-pad — and no
7//! keyboard: T-Echo, Heltec LoRa32 V3, Wio Tracker L1, T-Beam Supreme.
8//! They differ in panel technology and input richness, but a user who
9//! learns one should already know the others, so the interaction model
10//! lives here rather than in each firmware.
11//!
12//! Like [`umsh_ux_tracker`], this crate provides only **mechanism**:
13//!
14//! - [`menu`] — the on-screen menu: a wrapping item list narrowed
15//! per-board, with confirmation in front of the destructive entries.
16//! - [`attention`] — when to stop assuming the user is looking, and what
17//! that means for an emissive panel (power it off) versus a persistent
18//! one (send the menu home).
19//! - [`gate`] — what a gesture means when the screen is dark, an alert
20//! is running, or the panel is mid-refresh.
21//! - [`screen`] — what a frame looks like: the rows, what they say, and
22//! the battery indicator every frame carries. Behind the `screen`
23//! feature, since two boards in this family have no panel.
24//!
25//! Button gestures themselves come from
26//! [`umsh_ux_tracker::button::ButtonFsm`]; this crate adds only the
27//! shared timing policy, [`button_timings`]. Policy that depends on the
28//! board — which effects the menu items map to, what shutting down
29//! entails — belongs in the firmware.
30//!
31//! Rendering used to belong there too, on the theory that a 128×64 OLED
32//! and a 200×200 e-paper have too little in common to share a layout.
33//! They have more in common than they have pixels: [`screen`] takes a
34//! per-board [`screen::Layout`] and draws the rest once, so the class
35//! agrees on what a status page is rather than on how tall it is.
36//!
37//! See `docs/ux/` for the user-facing rules these modules encode and
38//! `docs/firmware-architecture.md` for the BSP / UX / App / Binary
39//! layering.
40
41pub mod attention;
42pub mod gate;
43pub mod menu;
44#[cfg(feature = "screen")]
45pub mod screen;
46
47use core::time::Duration;
48use umsh_ux_tracker::button::ButtonTimings;
49
50/// The gesture timing shared by every display tracker.
51///
52/// One set of numbers across the class is the point: the same hold
53/// powers off a T-Echo and a Heltec V3, and muscle memory carries.
54///
55/// A click is at most 500 ms; a chord continues while presses are within
56/// 400 ms of each other. Holding for 1 s and releasing is Back; holding
57/// through 4 s powers off without waiting for the release, so the
58/// gesture confirms itself while the user is still committing to it.
59pub fn button_timings() -> ButtonTimings {
60 ButtonTimings {
61 max_click_hold: Duration::from_millis(500),
62 inter_click_gap: Duration::from_millis(400),
63 long_press: Duration::from_secs(1),
64 very_long_press: Some(Duration::from_secs(4)),
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use umsh_ux_tracker::button::{ButtonEdge, ButtonEvent, ButtonFsm};
72
73 /// The class vocabulary, end to end through the real recognizer:
74 /// single = Forward, double = Select, 1 s hold-release = Back,
75 /// 4 s hold = power off.
76 #[test]
77 fn the_shared_timings_recognize_the_class_vocabulary() {
78 let t = button_timings();
79
80 let mut fsm = ButtonFsm::new(t);
81 fsm.on_edge(ButtonEdge::Press, 0);
82 assert_eq!(fsm.on_edge(ButtonEdge::Release, 100), None);
83 assert_eq!(fsm.poll(500), Some(ButtonEvent::Single));
84
85 let mut fsm = ButtonFsm::new(t);
86 fsm.on_edge(ButtonEdge::Press, 0);
87 fsm.on_edge(ButtonEdge::Release, 100);
88 fsm.on_edge(ButtonEdge::Press, 300);
89 assert_eq!(fsm.on_edge(ButtonEdge::Release, 400), None);
90 assert_eq!(fsm.poll(800), Some(ButtonEvent::Double));
91
92 let mut fsm = ButtonFsm::new(t);
93 fsm.on_edge(ButtonEdge::Press, 0);
94 assert_eq!(
95 fsm.on_edge(ButtonEdge::Release, 1_500),
96 Some(ButtonEvent::Long)
97 );
98
99 let mut fsm = ButtonFsm::new(t);
100 fsm.on_edge(ButtonEdge::Press, 0);
101 // Fires while still held, before any release.
102 assert_eq!(fsm.poll(4_000), Some(ButtonEvent::VeryLong));
103 }
104
105 /// A hold released between Back and power-off must not power off.
106 #[test]
107 fn releasing_before_the_power_off_threshold_is_only_back() {
108 let mut fsm = ButtonFsm::new(button_timings());
109 fsm.on_edge(ButtonEdge::Press, 0);
110 assert_eq!(fsm.poll(3_999), None);
111 assert_eq!(
112 fsm.on_edge(ButtonEdge::Release, 3_999),
113 Some(ButtonEvent::Long)
114 );
115 }
116}