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}