hsm_firmware_logic/io.rs
1// Copyright (c) 2026 Tuloup Simon
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! GPIO abstractions: LEDs and the touch button.
17//!
18//! The hardware-facing implementations on the RP2040 use `embassy_rp::gpio`
19//! and live in `hsm-firmware/src/hal_rp2040.rs`. They implement the [`Led`]
20//! and [`Button`] traits defined here. The firmware tasks (`animation`,
21//! `touch`) are generic over those traits so they can be exercised against
22//! mocks host-side.
23//!
24//! # Debouncing
25//!
26//! The button on the dongle has an RC low-pass filter in hardware
27//! (see schematic) plus a software debouncer here. The software step
28//! requires the input to remain stable for at least
29//! [`DEBOUNCE_STABLE_SAMPLES`] consecutive samples before the state is
30//! considered settled. Sampling is the caller's responsibility (this module
31//! is just the debouncer state machine), typically driven by a 1 ms timer
32//! task in the firmware.
33
34/// LED control trait.
35///
36/// Three methods: turn on, turn off, set explicitly. The third one has a
37/// default impl in terms of the first two so backends only need to
38/// implement [`Self::on`] and [`Self::off`]. Everything fancier (blink,
39/// pulse, fade) is composed on top by reading the LED pattern out of the
40/// [`crate::state_machine::TokenState`] and calling [`Self::set`] at the
41/// right cadence.
42pub trait Led
43{
44 /// Turn the LED on (drive the GPIO high on active-high wiring).
45 fn on(&mut self);
46
47 /// Turn the LED off.
48 fn off(&mut self);
49
50 /// Convenience: set the LED to a specific level. Default impl
51 /// dispatches to [`Self::on`] or [`Self::off`].
52 fn set(&mut self, on: bool)
53 {
54 if on
55 {
56 self.on();
57 }
58 else
59 {
60 self.off();
61 }
62 }
63}
64
65/// Button input trait.
66///
67/// Active-low on this hardware: the GPIO reads `false` when the button is
68/// pressed and `true` when released, because of the pull-up to 3V3. The
69/// trait abstracts that away and returns a boolean in the user-friendly
70/// direction: `true` means "user is pressing the button right now".
71pub trait Button
72{
73 /// `true` if the button is currently pressed at the GPIO level (raw,
74 /// not yet debounced).
75 fn is_pressed_raw(&self) -> bool;
76}
77
78/// Number of consecutive identical samples required for the debouncer to
79/// accept a new stable state.
80///
81/// At a 1 ms sampling rate this gives a 5 ms minimum settling time, well
82/// above the typical 1 ms of switch bounce we expect from the tactile
83/// switches we use.
84pub const DEBOUNCE_STABLE_SAMPLES: u8 = 5;
85
86/// Software debouncer state.
87///
88/// Holds the last stable level and a running counter that increments while
89/// the raw input matches the candidate new level and resets when it does
90/// not. Once the counter reaches [`DEBOUNCE_STABLE_SAMPLES`] the candidate
91/// becomes the new stable level.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct Debouncer
94{
95 /// Currently-accepted stable level.
96 stable: bool,
97 /// Candidate level being observed.
98 candidate: bool,
99 /// Consecutive samples matching `candidate`.
100 count: u8,
101}
102
103impl Debouncer
104{
105 /// Build a fresh debouncer assuming the initial level is `initial`.
106 #[must_use]
107 pub const fn new(initial: bool) -> Self
108 {
109 Self
110 {
111 stable: initial,
112 candidate: initial,
113 count: 0,
114 }
115 }
116
117 /// Feed one new sample.
118 ///
119 /// Returns `Some(level)` when the stable level just changed as a
120 /// result of this sample, `None` otherwise. The caller can use this
121 /// as a trigger to issue a press/release event.
122 pub fn sample(&mut self, level: bool) -> Option<bool>
123 {
124 if level == self.stable
125 {
126 // Same as currently accepted. Reset any in-progress candidate.
127 self.candidate = self.stable;
128 self.count = 0;
129 return None;
130 }
131
132 if level == self.candidate
133 {
134 // Candidate confirmed for another sample.
135 self.count = self.count.saturating_add(1);
136 if self.count >= DEBOUNCE_STABLE_SAMPLES
137 {
138 self.stable = self.candidate;
139 self.count = 0;
140 return Some(self.stable);
141 }
142 None
143 }
144 else
145 {
146 // New candidate, restart the counter.
147 self.candidate = level;
148 self.count = 1;
149 None
150 }
151 }
152
153 /// Current stable level (without consuming a sample).
154 ///
155 /// Useful for diagnostics: a task can query the current stable state
156 /// without committing to a sampling cycle. The `touch_task` logs
157 /// this at startup so the developer can confirm the resting state of
158 /// the button line via defmt RTT or USB CDC trace.
159 #[must_use]
160 pub const fn stable(&self) -> bool
161 {
162 self.stable
163 }
164}
165
166
167#[cfg(test)]
168mod test_mocks
169{
170 use super::{Button, Led};
171
172 /// Mock LED that records every state change in a tiny log.
173 #[derive(Debug, Clone, PartialEq, Eq)]
174 pub(crate) struct MockLed
175 {
176 pub(crate) state: bool,
177 pub(crate) transitions: u32,
178 }
179
180 impl MockLed
181 {
182 pub(crate) const fn new() -> Self
183 {
184 Self { state: false, transitions: 0 }
185 }
186 }
187
188 impl Led for MockLed
189 {
190 fn on(&mut self)
191 {
192 if !self.state
193 {
194 self.transitions += 1;
195 }
196 self.state = true;
197 }
198
199 fn off(&mut self)
200 {
201 if self.state
202 {
203 self.transitions += 1;
204 }
205 self.state = false;
206 }
207 }
208
209 /// Mock button whose state is set by the test.
210 pub(crate) struct MockButton
211 {
212 pub(crate) pressed: bool,
213 }
214
215 impl MockButton
216 {
217 pub(crate) const fn new(pressed: bool) -> Self
218 {
219 Self { pressed }
220 }
221 }
222
223 impl Button for MockButton
224 {
225 fn is_pressed_raw(&self) -> bool
226 {
227 self.pressed
228 }
229 }
230}
231
232#[cfg(test)]
233mod tests
234{
235 use super::*;
236 use super::test_mocks::{MockButton, MockLed};
237
238 #[test]
239 fn debouncer_holds_initial_state_until_threshold()
240 {
241 let mut d = Debouncer::new(false);
242 // Four samples of the opposite level should not flip yet
243 // (threshold is 5).
244 for _ in 0..4
245 {
246 assert_eq!(d.sample(true), None);
247 }
248 // The fifth flips it.
249 assert_eq!(d.sample(true), Some(true));
250 assert!(d.stable());
251 }
252
253 #[test]
254 fn debouncer_ignores_brief_noise()
255 {
256 let mut d = Debouncer::new(false);
257 // Bounce: a single true, then back to false, repeated.
258 for _ in 0..10
259 {
260 assert_eq!(d.sample(true), None);
261 assert_eq!(d.sample(false), None);
262 }
263 assert!(!d.stable());
264 }
265
266 #[test]
267 fn debouncer_resets_counter_on_revert()
268 {
269 let mut d = Debouncer::new(false);
270 // Three samples of true (almost there).
271 for _ in 0..3
272 {
273 assert_eq!(d.sample(true), None);
274 }
275 // One sample of false: counter reset.
276 assert_eq!(d.sample(false), None);
277 // Now four samples of true are needed again from scratch.
278 for _ in 0..4
279 {
280 assert_eq!(d.sample(true), None);
281 }
282 assert_eq!(d.sample(true), Some(true));
283 }
284
285 #[test]
286 fn debouncer_handles_release_after_press()
287 {
288 let mut d = Debouncer::new(false);
289 // Establish a press.
290 for _ in 0..4
291 {
292 d.sample(true);
293 }
294 assert_eq!(d.sample(true), Some(true));
295
296 // Now release: same threshold for the reverse direction.
297 for _ in 0..4
298 {
299 assert_eq!(d.sample(false), None);
300 }
301 assert_eq!(d.sample(false), Some(false));
302 }
303
304 #[test]
305 fn mock_led_tracks_transitions()
306 {
307 let mut led = MockLed::new();
308 led.on();
309 led.on(); // no-op transition count-wise
310 led.off();
311 assert_eq!(led.transitions, 2);
312 assert!(!led.state);
313 }
314
315 #[test]
316 fn mock_button_returns_configured_state()
317 {
318 let pressed = MockButton::new(true);
319 let released = MockButton::new(false);
320 assert!(pressed.is_pressed_raw());
321 assert!(!released.is_pressed_raw());
322 }
323}