Skip to main content

device_envoy/
led4.rs

1//! A device abstraction for a 4-digit, 7-segment LED display for text with optional animation and blinking.
2//!
3//! See [`Led4`] for the primary text/blinking example and [`Led4::animate_text`] for the animation example.
4//!
5//! This module provides device abstraction for controlling common-cathode
6//! 4-digit 7-segment LED displays. Supports displaying text and numbers with
7//! optional blinking.
8
9use core::borrow::Borrow;
10
11use embassy_executor::Spawner;
12use embassy_futures::select::{Either, select};
13use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
14use embassy_time::{Duration, Timer};
15use heapless::Vec;
16
17use crate::{Error, Result};
18
19#[cfg(feature = "display-trace")]
20use defmt::info;
21
22// ============================================================================
23// Led4Simple Submodule (internal helper)
24// ============================================================================
25
26pub(crate) mod led4_simple;
27use self::led4_simple::{Led4Simple, Led4SimpleStatic};
28
29// ============================================================================
30// OutputArray Submodule
31// ============================================================================
32
33mod output_array;
34pub use output_array::OutputArray;
35
36// ============================================================================
37// Constants
38// ============================================================================
39
40/// The number of cells (digits) in the display.
41pub(crate) const CELL_COUNT_U8: u8 = 4;
42pub(crate) const CELL_COUNT: usize = CELL_COUNT_U8 as usize;
43
44/// The number of segments per digit in the display.
45pub(crate) const SEGMENT_COUNT: usize = 8;
46
47/// Sleep duration between multiplexing updates.
48pub(crate) const MULTIPLEX_SLEEP: Duration = Duration::from_millis(3);
49
50/// Delay for the "off" state during blinking.
51const BLINK_OFF_DELAY: Duration = Duration::from_millis(50);
52
53/// Delay for the "on" state during blinking.
54const BLINK_ON_DELAY: Duration = Duration::from_millis(150);
55
56// This is not configurable for now because that would require use of an extra macro.
57const ANIMATION_MAX_FRAMES: usize = 16;
58
59// ============================================================================
60// BlinkState Enum
61// ============================================================================
62
63/// Blinking behavior for 4-digit LED displays.
64///
65/// Used with [`Led4::write_text()`] to control whether the display blinks.
66/// See the [`Led4`] documentation for usage examples.
67#[derive(Debug, Clone, Copy, defmt::Format, Default)]
68pub enum BlinkState {
69    /// Display is always on (solid, no blinking).
70    #[default]
71    Solid,
72    /// Display blinks; currently shows on.
73    BlinkingAndOn,
74    /// Display blinks; currently shows off.
75    BlinkingButOff,
76}
77
78#[derive(Clone)]
79pub(crate) enum Led4Command {
80    Text {
81        blink_state: BlinkState,
82        text: [char; CELL_COUNT],
83    },
84    Animation(Vec<AnimationFrame, ANIMATION_MAX_FRAMES>),
85}
86
87/// Frame of animated text for [`Led4::animate_text`]. See that method's example for usage.
88#[derive(Clone, Copy, Debug)]
89pub struct AnimationFrame {
90    /// Text to display (4 characters for a 4-digit display).
91    pub text: [char; CELL_COUNT],
92    /// Duration to display this frame.
93    pub duration: Duration,
94}
95
96impl AnimationFrame {
97    /// Creates a new animation frame with text and duration.
98    #[must_use]
99    pub const fn new(text: [char; CELL_COUNT], duration: Duration) -> Self {
100        Self { text, duration }
101    }
102}
103
104// ============================================================================
105// Led4 Virtual Device
106// ============================================================================
107
108/// A device abstraction for a 4-digit, 7-segment LED display with blinking support.
109///
110/// # Hardware Requirements
111///
112/// This abstraction is designed for common-cathode 7-segment displays where:
113/// - Cell pins control which digit is active (LOW = on, HIGH = off)
114/// - Segment pins control which segments light up (HIGH = on, LOW = off)
115///
116/// # Example
117///
118/// ```rust,no_run
119/// # #![no_std]
120/// # #![no_main]
121/// use device_envoy::{Error, led4::{BlinkState, Led4, Led4Static, OutputArray}};
122/// # #[panic_handler]
123/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
124///
125/// async fn example(p: embassy_rp::Peripherals, spawner: embassy_executor::Spawner) -> Result<(), Error> {
126///     // Set up cell pins (control which digit is active)
127///     let cells = OutputArray::new([
128///         embassy_rp::gpio::Output::new(p.PIN_1, embassy_rp::gpio::Level::High),
129///         embassy_rp::gpio::Output::new(p.PIN_2, embassy_rp::gpio::Level::High),
130///         embassy_rp::gpio::Output::new(p.PIN_3, embassy_rp::gpio::Level::High),
131///         embassy_rp::gpio::Output::new(p.PIN_4, embassy_rp::gpio::Level::High),
132///     ]);
133///
134///     // Set up segment pins (control which segments light up)
135///     let segments = OutputArray::new([
136///         embassy_rp::gpio::Output::new(p.PIN_5, embassy_rp::gpio::Level::Low),  // Segment A
137///         embassy_rp::gpio::Output::new(p.PIN_6, embassy_rp::gpio::Level::Low),  // Segment B
138///         embassy_rp::gpio::Output::new(p.PIN_7, embassy_rp::gpio::Level::Low),  // Segment C
139///         embassy_rp::gpio::Output::new(p.PIN_8, embassy_rp::gpio::Level::Low),  // Segment D
140///         embassy_rp::gpio::Output::new(p.PIN_9, embassy_rp::gpio::Level::Low),  // Segment E
141///         embassy_rp::gpio::Output::new(p.PIN_10, embassy_rp::gpio::Level::Low), // Segment F
142///         embassy_rp::gpio::Output::new(p.PIN_11, embassy_rp::gpio::Level::Low), // Segment G
143///         embassy_rp::gpio::Output::new(p.PIN_12, embassy_rp::gpio::Level::Low), // Decimal point
144///     ]);
145///
146///     // Create the display
147///     static LED4_STATIC: Led4Static = Led4::new_static();
148///     let display = Led4::new(&LED4_STATIC, cells, segments, spawner)?;
149///
150///     // Display "1234" (solid)
151///     display.write_text(['1', '2', '3', '4'], BlinkState::Solid);
152///     
153///     // Display "rUSt" blinking
154///     display.write_text(['r', 'U', 'S', 't'], BlinkState::BlinkingAndOn);
155///     
156///     Ok(())
157/// }
158/// ```
159///
160/// Beyond simple text, the driver can loop animations via [`Led4::animate_text`].
161/// The struct owns the background task and signal wiring; create it once with
162/// [`Led4::new`] and use the returned handle for all display updates.
163pub struct Led4<'a>(&'a Led4OuterStatic);
164
165/// Signal for sending display commands to the [`Led4`] device.
166pub(crate) type Led4OuterStatic = Signal<CriticalSectionRawMutex, Led4Command>;
167
168/// Static for the [`Led4`] device.
169pub struct Led4Static {
170    outer: Led4OuterStatic,
171    display: Led4SimpleStatic,
172}
173
174impl Led4Static {
175    /// Creates static resources for the 4-digit LED display device.
176    pub(crate) const fn new() -> Self {
177        Self {
178            outer: Signal::new(),
179            display: Led4Simple::new_static(),
180        }
181    }
182
183    fn split(&self) -> (&Led4OuterStatic, &Led4SimpleStatic) {
184        (&self.outer, &self.display)
185    }
186}
187
188impl Led4<'_> {
189    /// Creates the display device and spawns its background task; see [`Led4`] docs.
190    #[must_use = "Must be used to manage the spawned task"]
191    pub fn new(
192        led4_static: &'static Led4Static,
193        cell_pins: OutputArray<'static, CELL_COUNT>,
194        segment_pins: OutputArray<'static, SEGMENT_COUNT>,
195        spawner: Spawner,
196    ) -> Result<Self> {
197        let (outer_static, display_static) = led4_static.split();
198        let display = Led4Simple::new(display_static, cell_pins, segment_pins, spawner)?;
199        let token = device_loop(outer_static, display);
200        spawner.spawn(token).map_err(Error::TaskSpawn)?;
201        Ok(Self(outer_static))
202    }
203
204    /// Creates static channel resources for [`Led4::new`]; see [`Led4`] docs.
205    #[must_use]
206    pub const fn new_static() -> Led4Static {
207        Led4Static::new()
208    }
209
210    /// Sends text to the display with optional blinking.
211    ///
212    /// See the main [`Led4`] example for end-to-end usage.
213    pub fn write_text(&self, text: [char; CELL_COUNT], blink_state: BlinkState) {
214        #[cfg(feature = "display-trace")]
215        info!("blink_state: {:?}, text: {:?}", blink_state, text);
216        self.0.signal(Led4Command::Text { blink_state, text });
217    }
218
219    /// Plays a looped text animation using the provided frames.
220    ///
221    /// # Example
222    ///
223    /// ```rust,no_run
224    /// # #![no_std]
225    /// # #![no_main]
226    /// # use panic_probe as _;
227    /// # use embassy_rp::gpio::{Level, Output};
228    /// # use embassy_executor::Spawner;
229    /// use device_envoy::{Result, led4::{AnimationFrame, Led4, Led4Static, OutputArray}};
230    /// use embassy_time::Duration;
231    /// async fn demo(p: embassy_rp::Peripherals, spawner: Spawner) -> Result<()> {
232    ///     let cells = OutputArray::new([
233    ///         Output::new(p.PIN_1, Level::High),
234    ///         Output::new(p.PIN_2, Level::High),
235    ///         Output::new(p.PIN_3, Level::High),
236    ///         Output::new(p.PIN_4, Level::High),
237    ///     ]);
238    ///     let segments = OutputArray::new([
239    ///         Output::new(p.PIN_5, Level::Low),
240    ///         Output::new(p.PIN_6, Level::Low),
241    ///         Output::new(p.PIN_7, Level::Low),
242    ///         Output::new(p.PIN_8, Level::Low),
243    ///         Output::new(p.PIN_9, Level::Low),
244    ///         Output::new(p.PIN_10, Level::Low),
245    ///         Output::new(p.PIN_11, Level::Low),
246    ///         Output::new(p.PIN_12, Level::Low),
247    ///     ]);
248    ///     static LED4_STATIC: Led4Static = Led4::new_static();
249    ///     let display = Led4::new(&LED4_STATIC, cells, segments, spawner)?;
250    ///     const FRAME_DURATION: Duration = Duration::from_millis(120);
251    ///     let animation = [
252    ///         AnimationFrame::new(['-', '-', '-', '-'], FRAME_DURATION),
253    ///         AnimationFrame::new([' ', ' ', ' ', ' '], FRAME_DURATION),
254    ///         AnimationFrame::new(['1', '2', '3', '4'], FRAME_DURATION),
255    ///     ];
256    ///     display.animate_text(animation);
257    ///     Ok(())
258    /// }
259    /// ```
260    /// See the example below for how to build animations.
261    pub fn animate_text<I>(&self, animation: I)
262    where
263        I: IntoIterator,
264        I::Item: Borrow<AnimationFrame>,
265    {
266        let mut frames: Vec<AnimationFrame, ANIMATION_MAX_FRAMES> = Vec::new();
267        for animation_frame in animation {
268            let animation_frame = *animation_frame.borrow();
269            frames
270                .push(animation_frame)
271                .expect("animate sequence fits within ANIMATION_MAX_FRAMES");
272        }
273        self.0.signal(Led4Command::Animation(frames));
274    }
275}
276
277#[embassy_executor::task]
278async fn device_loop(outer_static: &'static Led4OuterStatic, display: Led4Simple<'static>) -> ! {
279    let mut command = Led4Command::Text {
280        blink_state: BlinkState::default(),
281        text: [' '; CELL_COUNT],
282    };
283
284    loop {
285        command = match command {
286            Led4Command::Text { blink_state, text } => {
287                run_text_loop(blink_state, text, outer_static, &display).await
288            }
289            Led4Command::Animation(animation) => {
290                run_animation_loop(animation, outer_static, &display).await
291            }
292        };
293    }
294}
295
296async fn run_text_loop(
297    mut blink_state: BlinkState,
298    text: [char; CELL_COUNT],
299    outer_static: &'static Led4OuterStatic,
300    display: &Led4Simple<'_>,
301) -> Led4Command {
302    loop {
303        match blink_state {
304            BlinkState::Solid => {
305                display.write_text(text);
306                return outer_static.wait().await;
307            }
308            BlinkState::BlinkingAndOn => {
309                display.write_text(text);
310                match select(outer_static.wait(), Timer::after(BLINK_ON_DELAY)).await {
311                    Either::First(command) => return command,
312                    Either::Second(()) => blink_state = BlinkState::BlinkingButOff,
313                }
314            }
315            BlinkState::BlinkingButOff => {
316                display.write_text([' '; CELL_COUNT]);
317                match select(outer_static.wait(), Timer::after(BLINK_OFF_DELAY)).await {
318                    Either::First(command) => return command,
319                    Either::Second(()) => blink_state = BlinkState::BlinkingAndOn,
320                }
321            }
322        }
323    }
324}
325
326async fn run_animation_loop(
327    animation: Vec<AnimationFrame, ANIMATION_MAX_FRAMES>,
328    outer_static: &'static Led4OuterStatic,
329    display: &Led4Simple<'_>,
330) -> Led4Command {
331    if animation.is_empty() {
332        return outer_static.wait().await;
333    }
334
335    let frames = animation;
336    let len = frames.len();
337    let mut index = 0;
338
339    loop {
340        let frame = frames[index];
341        display.write_text(frame.text);
342        match select(outer_static.wait(), Timer::after(frame.duration)).await {
343            Either::First(command) => return command,
344            Either::Second(()) => {
345                index = (index + 1) % len;
346            }
347        }
348    }
349}
350
351/// Creates a circular outline animation that chases around the edges of the display.
352///
353/// Returns an animation with 8 frames showing a segment moving clockwise or
354/// counter-clockwise around the perimeter of the 4-digit display.
355///
356/// # Arguments
357///
358/// * `clockwise` - If `true`, animates clockwise; if `false`, counter-clockwise
359///
360/// # Example
361///
362/// ```rust,no_run
363/// # #![no_std]
364/// # #![no_main]
365/// use device_envoy::led4::{Led4, circular_outline_animation};
366/// # #[panic_handler]
367/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
368///
369/// async fn example(led4: &Led4<'_>) {
370///     // Animate clockwise
371///     led4.animate_text(circular_outline_animation(true));
372///
373///     // Animate counter-clockwise
374///     led4.animate_text(circular_outline_animation(false));
375/// }
376/// ```
377#[must_use]
378pub fn circular_outline_animation(clockwise: bool) -> Vec<AnimationFrame, ANIMATION_MAX_FRAMES> {
379    const FRAME_DURATION: Duration = Duration::from_millis(120);
380    const CLOCKWISE: [[char; 4]; 8] = [
381        ['\'', '\'', '\'', '\''],
382        ['\'', '\'', '\'', '"'],
383        [' ', ' ', ' ', '>'],
384        [' ', ' ', ' ', ')'],
385        ['_', '_', '_', '_'],
386        ['*', '_', '_', '_'],
387        ['<', ' ', ' ', ' '],
388        ['(', '\'', '\'', '\''],
389    ];
390    const COUNTER: [[char; 4]; 8] = [
391        ['(', '\'', '\'', '\''],
392        ['<', ' ', ' ', ' '],
393        ['*', '_', '_', '_'],
394        ['_', '_', '_', '_'],
395        [' ', ' ', ' ', ')'],
396        [' ', ' ', ' ', '>'],
397        ['\'', '\'', '\'', '"'],
398        ['\'', '\'', '\'', '\''],
399    ];
400
401    let mut animation = Vec::new();
402    let frames = if clockwise { &CLOCKWISE } else { &COUNTER };
403    for text in frames {
404        animation
405            .push(AnimationFrame::new(*text, FRAME_DURATION))
406            .expect("animation exceeds frame capacity");
407    }
408    animation
409}