hsm_firmware/touch.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//! Touch button sampling task.
17//!
18//! Samples the button GPIO at 1 ms intervals, runs the signal through a
19//! [`Debouncer`], and posts [`Event::TouchPressed`] on every confirmed
20//! press (low -> high transition of the user-facing "pressed" boolean).
21//!
22//! The button is active-low on this hardware (pull-up to 3V3, switch to
23//! ground); the [`Button`] trait abstracts that away, so
24//! [`Button::is_pressed_raw`] returns `true` when the user is pressing.
25//!
26//! # Testability
27//!
28//! The driving logic is implemented as the generic helper
29//! [`sample_once`], which takes any `Button` impl. The embassy task
30//! [`touch_task`] is a thin shell that owns a real `Input<'static>`
31//! and forwards to the helper; tests in this module exercise the
32//! helper directly with a `MockButton`.
33
34use defmt::info;
35use embassy_time::{Duration, Timer};
36
37use hsm_firmware_logic::{Button, Debouncer, Event};
38
39use crate::channels::post_event;
40use crate::hal_rp2040::Rp2040Button;
41
42/// Sampling period for the button GPIO.
43const SAMPLE_INTERVAL: Duration = Duration::from_millis(1);
44
45/// Drive the touch button sampler. Spawn once at boot.
46///
47/// `button` is a wrapped `Input` configured with an internal pull-up. The
48/// pin is owned by this task for the lifetime of the firmware.
49#[embassy_executor::task]
50pub(crate) async fn touch_task(button: Rp2040Button) -> !
51{
52 // Initialise the debouncer to the current raw state so we don't post
53 // a spurious "press" if the user is already holding the button at
54 // boot.
55 let initial = button.is_pressed_raw();
56 let mut debouncer = Debouncer::new(initial);
57 info!
58 (
59 "touch task started, initial stable state = {}",
60 debouncer.stable(),
61 );
62
63 loop
64 {
65 Timer::after(SAMPLE_INTERVAL).await;
66 sample_once(&button, &mut debouncer);
67 }
68}
69
70/// Sample the button once and post a [`Event::TouchPressed`] event if
71/// the debouncer accepts a low->high transition (release -> press).
72///
73/// Generic over [`Button`] so it can be exercised in host-side tests
74/// with a mock button.
75fn sample_once<B: Button>(button: &B, debouncer: &mut Debouncer)
76{
77 let pressed = button.is_pressed_raw();
78
79 if let Some(new_stable) = debouncer.sample(pressed)
80 {
81 if new_stable
82 {
83 info!("touch pressed");
84 post_event(Event::TouchPressed);
85 }
86 else
87 {
88 // Release: no event in the current state machine. Logged
89 // for debug only.
90 info!("touch released");
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests
97{
98 use super::*;
99 use hsm_firmware_logic::DEBOUNCE_STABLE_SAMPLES;
100
101 /// Mock button whose raw state is mutable from the test.
102 struct MockButton
103 {
104 pressed: bool,
105 }
106
107 impl Button for MockButton
108 {
109 fn is_pressed_raw(&self) -> bool
110 {
111 self.pressed
112 }
113 }
114
115 #[test]
116 fn sample_once_does_not_post_on_no_transition()
117 {
118 // Both the button and the debouncer are already at "not
119 // pressed". A sample of the same state should not produce a
120 // transition.
121 let button = MockButton { pressed: false };
122 let mut debouncer = Debouncer::new(false);
123 // No assertion possible on `post_event` here (it writes to a
124 // global channel), so we assert on the debouncer's own state
125 // staying put. If `sample` returned `Some`, the debouncer
126 // would have committed.
127 for _ in 0..(DEBOUNCE_STABLE_SAMPLES + 2)
128 {
129 sample_once(&button, &mut debouncer);
130 }
131 assert!(!debouncer.stable());
132 }
133
134 #[test]
135 fn sample_once_promotes_after_stable_count()
136 {
137 // Simulate the bouncer phase where every sample is `pressed=true`
138 // and the debouncer needs `DEBOUNCE_STABLE_SAMPLES` of them to
139 // commit.
140 let button = MockButton { pressed: true };
141 let mut debouncer = Debouncer::new(false);
142 for _ in 0..(DEBOUNCE_STABLE_SAMPLES - 1)
143 {
144 sample_once(&button, &mut debouncer);
145 assert!(!debouncer.stable());
146 }
147 // The N-th sample commits.
148 sample_once(&button, &mut debouncer);
149 assert!(debouncer.stable());
150 }
151}