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}