Skip to main content

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}