Skip to main content

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}