Skip to main content

hsm_firmware/
animation.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//! LED animation task.
17//!
18//! Reads the current [`hsm_firmware_logic::TokenState`] off
19//! [`crate::channels::TOKEN_STATE`] and drives the green (GP16) and yellow
20//! (GP17) LEDs at the cadence dictated by the state's
21//! [`hsm_firmware_logic::LedPattern`].
22//!
23//! The task runs an infinite loop with two interleaved concerns:
24//!
25//! 1. **Apply the current pattern.** Each pattern has a tick interval and
26//!    a per-tick action (toggle green, toggle yellow, alternate, etc).
27//!    The task sleeps for the tick interval, then performs the action.
28//! 2. **React to state changes promptly.** Sleeping for a 500 ms tick
29//!    would make a transition into `Error` invisible for half a second.
30//!    To avoid that, the task races the tick timer against
31//!    [`crate::channels::TOKEN_STATE`] using `embassy_futures::select`.
32//!    Whichever fires first wins.
33//!
34//! # Testability
35//!
36//! The actual driving logic lives in [`apply_pattern`], which is generic
37//! over the [`Led`] trait. The embassy task wrapper
38//! [`animation_task`] is a thin shell that supplies real `Output<'static>`
39//! values; host-side tests instantiate `apply_pattern` directly with
40//! mock LEDs to assert on transition counts.
41
42use defmt::info;
43use embassy_futures::select::{select, Either};
44use embassy_time::{Duration, Timer};
45
46use hsm_firmware_logic::{Led, LedPattern};
47
48use crate::channels::TOKEN_STATE;
49use crate::hal_rp2040::Rp2040Led;
50
51/// Tick interval for the slow pulse pattern (green, 1 Hz).
52const SLOW_PULSE_INTERVAL: Duration = Duration::from_millis(500);
53
54/// Tick interval for the fast blink pattern (5 Hz).
55const FAST_BLINK_INTERVAL: Duration = Duration::from_millis(100);
56
57/// Tick interval for the yellow blink pattern (2 Hz).
58const YELLOW_BLINK_INTERVAL: Duration = Duration::from_millis(250);
59
60/// Tick interval for the alternate-both pattern (5 Hz).
61const ALTERNATE_INTERVAL: Duration = Duration::from_millis(100);
62
63/// Drive the LEDs based on the current operating state. Spawn once at boot.
64///
65/// Both LEDs are owned by this task for the lifetime of the firmware.
66#[embassy_executor::task]
67pub(crate) async fn animation_task
68(
69    mut led_green: Rp2040Led,
70    mut led_yellow: Rp2040Led,
71) -> !
72{
73    // Wait for the first state publication. The state task always emits
74    // the initial state at startup, so this returns promptly.
75    let mut state = TOKEN_STATE.wait().await;
76    info!("animation task started, initial state {:?}", state);
77
78    // Tick phase: toggles every iteration to drive blink/pulse patterns.
79    let mut tick_high = false;
80
81    loop
82    {
83        // Apply the current pattern via the generic helper.
84        let interval = apply_pattern
85        (
86            state.led_pattern(),
87            tick_high,
88            &mut led_green,
89            &mut led_yellow,
90        );
91        tick_high = !tick_high;
92
93        // Race the tick timer against a state change.
94        match select(Timer::after(interval), TOKEN_STATE.wait()).await
95        {
96            Either::First(()) =>
97            {
98                // Tick elapsed, continue with the same pattern.
99            }
100            Either::Second(new_state) =>
101            {
102                // State changed: reset the tick phase and switch pattern
103                // immediately.
104                info!("animation: state {:?} -> {:?}", state, new_state);
105                state = new_state;
106                tick_high = false;
107            }
108        }
109    }
110}
111
112/// Apply one tick of `pattern` to the LEDs and return the duration to wait
113/// before the next tick.
114///
115/// `tick_high` alternates `false`/`true` each iteration. For static
116/// patterns (solid, all-off) the value is ignored.
117///
118/// Generic over [`Led`] so the firmware task and host-side unit tests
119/// can share the same code path.
120pub(crate) fn apply_pattern<G, Y>
121(
122    pattern: LedPattern,
123    tick_high: bool,
124    led_green: &mut G,
125    led_yellow: &mut Y,
126) -> Duration
127where
128    G: Led,
129    Y: Led,
130{
131    match pattern
132    {
133        LedPattern::AllOff =>
134        {
135            led_green.off();
136            led_yellow.off();
137            // No animation: long wait, the select will wake us on the
138            // next state change.
139            Duration::from_secs(60)
140        }
141        LedPattern::GreenSolid =>
142        {
143            led_green.on();
144            led_yellow.off();
145            Duration::from_secs(60)
146        }
147        LedPattern::GreenSlowPulse =>
148        {
149            led_yellow.off();
150            led_green.set(tick_high);
151            SLOW_PULSE_INTERVAL
152        }
153        LedPattern::GreenFastBlink =>
154        {
155            led_yellow.off();
156            led_green.set(tick_high);
157            FAST_BLINK_INTERVAL
158        }
159        LedPattern::YellowSolid =>
160        {
161            led_green.off();
162            led_yellow.on();
163            Duration::from_secs(60)
164        }
165        LedPattern::YellowBlink =>
166        {
167            led_green.off();
168            led_yellow.set(tick_high);
169            YELLOW_BLINK_INTERVAL
170        }
171        LedPattern::AlternateBoth =>
172        {
173            led_green.set(tick_high);
174            led_yellow.set(!tick_high);
175            ALTERNATE_INTERVAL
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests
182{
183    use super::*;
184
185    /// Minimal mock LED that just tracks state and transitions.
186    /// Mirrors the one in `hsm_firmware_logic::io::test_mocks` but lives
187    /// here so this crate can test `apply_pattern` without exposing a
188    /// `pub(crate)` mock across crates.
189    #[derive(Debug, Default)]
190    struct MockLed
191    {
192        state:       bool,
193        transitions: u32,
194    }
195
196    impl Led for MockLed
197    {
198        fn on(&mut self)
199        {
200            if !self.state
201            {
202                self.transitions += 1;
203            }
204            self.state = true;
205        }
206
207        fn off(&mut self)
208        {
209            if self.state
210            {
211                self.transitions += 1;
212            }
213            self.state = false;
214        }
215    }
216
217    #[test]
218    fn green_solid_lights_green_and_kills_yellow()
219    {
220        let mut green = MockLed::default();
221        let mut yellow = MockLed::default();
222        green.off();
223        yellow.on();
224        let _ = apply_pattern(LedPattern::GreenSolid, false, &mut green, &mut yellow);
225        assert!(green.state);
226        assert!(!yellow.state);
227    }
228
229    #[test]
230    fn alternate_both_swaps_each_tick()
231    {
232        let mut green = MockLed::default();
233        let mut yellow = MockLed::default();
234        let _ = apply_pattern(LedPattern::AlternateBoth, true, &mut green, &mut yellow);
235        assert!(green.state);
236        assert!(!yellow.state);
237        let _ = apply_pattern(LedPattern::AlternateBoth, false, &mut green, &mut yellow);
238        assert!(!green.state);
239        assert!(yellow.state);
240    }
241
242    #[test]
243    fn all_off_clears_both()
244    {
245        let mut green = MockLed::default();
246        let mut yellow = MockLed::default();
247        green.on();
248        yellow.on();
249        let _ = apply_pattern(LedPattern::AllOff, true, &mut green, &mut yellow);
250        assert!(!green.state);
251        assert!(!yellow.state);
252    }
253
254    #[test]
255    fn green_slow_pulse_returns_500ms_interval()
256    {
257        let mut green = MockLed::default();
258        let mut yellow = MockLed::default();
259        let d = apply_pattern(LedPattern::GreenSlowPulse, true, &mut green, &mut yellow);
260        assert_eq!(d, SLOW_PULSE_INTERVAL);
261    }
262}