Skip to main content

device_envoy_esp/
led4.rs

1//! A device abstraction for a 4-digit, 7-segment LED display for text with optional animation and blinking.
2//!
3//! See [`Led4Esp`] for the primary text/blinking example and [`Led4`] for trait methods.
4//!
5//! **Limations**: You can create up to two concurrent `Led4Esp` instances per program; a third is expected to fail at runtime because the `led4` task pool uses `pool_size = 2`. Animation APIs support up to 16 steps per animation (`ANIMATION_MAX_FRAMES`).
6//!
7//! This module provides device abstraction for controlling common-cathode
8//! 4-digit 7-segment LED displays. Supports displaying text and numbers with
9//! optional blinking.
10
11pub use device_envoy_core::led4::{ANIMATION_MAX_FRAMES, AnimationFrame, BlinkState, Led4};
12/// Frame buffer type used by led4 text animations.
13pub type Animation = device_envoy_core::led4::Animation;
14
15/// Creates a circular outline animation that chases around display edges.
16#[must_use]
17pub fn circular_outline_animation(clockwise: bool) -> Animation {
18    device_envoy_core::led4::circular_outline_animation(clockwise)
19}
20
21#[cfg(target_os = "none")]
22const CELL_COUNT: usize = device_envoy_core::led4::CELL_COUNT;
23#[cfg(target_os = "none")]
24const SEGMENT_COUNT: usize = device_envoy_core::led4::SEGMENT_COUNT;
25
26#[cfg(target_os = "none")]
27use core::convert::Infallible;
28
29#[cfg(target_os = "none")]
30use device_envoy_core::led4::{
31    BitMatrixLed4, Led4OutputAdapter, Led4SimpleLoopError, run_command_loop, run_simple_loop,
32    signal_animation, signal_text,
33};
34#[cfg(target_os = "none")]
35use embassy_executor::Spawner;
36#[cfg(target_os = "none")]
37use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
38
39#[cfg(target_os = "none")]
40use crate::{Error, Result};
41
42#[cfg(target_os = "none")]
43mod output_array;
44#[cfg(target_os = "none")]
45pub use output_array::OutputArray;
46
47#[cfg(target_os = "none")]
48struct Led4SimpleStatic(Signal<CriticalSectionRawMutex, BitMatrixLed4>);
49
50#[cfg(target_os = "none")]
51impl Led4SimpleStatic {
52    const fn new() -> Self {
53        Self(Signal::new())
54    }
55
56    fn signal(&self, bit_matrix_led4: BitMatrixLed4) {
57        self.0.signal(bit_matrix_led4);
58    }
59}
60
61#[cfg(target_os = "none")]
62struct Led4Simple<'a>(&'a Led4SimpleStatic);
63
64#[cfg(target_os = "none")]
65impl Led4Simple<'_> {
66    const fn new_static() -> Led4SimpleStatic {
67        Led4SimpleStatic::new()
68    }
69
70    #[must_use = "Must be used to manage the spawned task"]
71    fn new(
72        led4_simple_static: &'static Led4SimpleStatic,
73        cell_pins: OutputArray<'static, CELL_COUNT>,
74        segment_pins: OutputArray<'static, SEGMENT_COUNT>,
75        spawner: Spawner,
76    ) -> Result<Self> {
77        let token = led4_simple_device_loop(cell_pins, segment_pins, led4_simple_static);
78        spawner.spawn(token.map_err(Error::TaskSpawn)?);
79        Ok(Self(led4_simple_static))
80    }
81
82    fn write_text(&self, text: [char; CELL_COUNT]) {
83        self.0.signal(BitMatrixLed4::from_text(&text));
84    }
85}
86
87#[embassy_executor::task(pool_size = 2)]
88#[cfg(target_os = "none")]
89async fn led4_simple_device_loop(
90    cell_pins: OutputArray<'static, CELL_COUNT>,
91    segment_pins: OutputArray<'static, SEGMENT_COUNT>,
92    led4_simple_static: &'static Led4SimpleStatic,
93) -> ! {
94    let error = inner_led4_simple_device_loop(cell_pins, segment_pins, led4_simple_static)
95        .await
96        .unwrap_err();
97    panic!("{error:?}");
98}
99
100#[cfg(target_os = "none")]
101async fn inner_led4_simple_device_loop(
102    cell_pins: OutputArray<'static, CELL_COUNT>,
103    segment_pins: OutputArray<'static, SEGMENT_COUNT>,
104    led4_simple_static: &'static Led4SimpleStatic,
105) -> Result<Infallible> {
106    let mut esp_led4_output = EspLed4Output {
107        cell_pins,
108        segment_pins,
109    };
110    run_simple_loop(&mut esp_led4_output, &led4_simple_static.0)
111        .await
112        .map_err(Error::from)
113}
114
115#[cfg(target_os = "none")]
116struct EspLed4Output {
117    cell_pins: OutputArray<'static, CELL_COUNT>,
118    segment_pins: OutputArray<'static, SEGMENT_COUNT>,
119}
120
121#[cfg(target_os = "none")]
122impl Led4OutputAdapter for EspLed4Output {
123    type Error = Error;
124
125    fn set_segments_from_nonzero_bits(&mut self, bits: core::num::NonZeroU8) {
126        self.segment_pins.set_from_nonzero_bits(bits);
127    }
128
129    fn set_cells_active(&mut self, indexes: &[u8], active: bool) -> Result<(), Self::Error> {
130        let level = if active {
131            esp_hal::gpio::Level::Low
132        } else {
133            esp_hal::gpio::Level::High
134        };
135        self.cell_pins.set_levels_at_indexes(indexes, level)
136    }
137}
138
139#[cfg(target_os = "none")]
140impl From<Led4SimpleLoopError<Error>> for Error {
141    fn from(error: Led4SimpleLoopError<Error>) -> Self {
142        match error {
143            Led4SimpleLoopError::BitsToIndexes(error) => Self::from(error),
144            Led4SimpleLoopError::Output(error) => error,
145        }
146    }
147}
148
149/// A device abstraction for a 4-digit, 7-segment LED display with blinking support.
150///
151/// # Hardware Requirements
152///
153/// This abstraction is designed for common-cathode 7-segment displays where:
154/// - Cell pins control which digit is active (LOW = on, HIGH = off)
155/// - Segment pins control which segments light up (HIGH = on, LOW = off)
156///
157/// # Example
158///
159/// ```rust,no_run
160/// # #![no_std]
161/// # #![no_main]
162/// # use esp_backtrace as _;
163/// use device_envoy_esp::{
164///     Error, Result, init_and_start,
165///     led4::{BlinkState, Led4 as _, Led4Esp, Led4EspStatic, OutputArray, circular_outline_animation},
166/// };
167/// use esp_hal::gpio::{Level, Output, OutputConfig};
168/// use embassy_time::{Duration, Timer};
169///
170/// # #[esp_rtos::main]
171/// # async fn main(spawner: embassy_executor::Spawner) -> ! {
172/// #     match example(spawner).await {
173/// #         Ok(()) => loop {},
174/// #         Err(error) => panic!("{error:?}"),
175/// #     }
176/// # }
177/// async fn example(spawner: embassy_executor::Spawner) -> Result<(), Error> {
178///     init_and_start!(p);
179///
180///     let cells = OutputArray::new([
181///         Output::new(p.GPIO14, Level::High, OutputConfig::default()),
182///         Output::new(p.GPIO13, Level::High, OutputConfig::default()),
183///         Output::new(p.GPIO12, Level::High, OutputConfig::default()),
184///         Output::new(p.GPIO11, Level::High, OutputConfig::default()),
185///     ]);
186///
187///     let segments = OutputArray::new([
188///         Output::new(p.GPIO10, Level::Low, OutputConfig::default()),
189///         Output::new(p.GPIO9, Level::Low, OutputConfig::default()),
190///         Output::new(p.GPIO4, Level::Low, OutputConfig::default()),
191///         Output::new(p.GPIO3, Level::Low, OutputConfig::default()),
192///         Output::new(p.GPIO8, Level::Low, OutputConfig::default()),
193///         Output::new(p.GPIO18, Level::Low, OutputConfig::default()),
194///         Output::new(p.GPIO17, Level::Low, OutputConfig::default()),
195///         Output::new(p.GPIO16, Level::Low, OutputConfig::default()),
196///     ]);
197///
198///     static LED4_STATIC: Led4EspStatic = Led4Esp::new_static();
199///     let display = Led4Esp::new(&LED4_STATIC, cells, segments, spawner)?;
200///
201///     // Blink "1234" for three seconds.
202///     display.write_text(['1', '2', '3', '4'], BlinkState::BlinkingAndOn);
203///     Timer::after(Duration::from_secs(3)).await;
204///
205///     // Run the circular outline animation for three seconds.
206///     display.animate_text(circular_outline_animation(true));
207///     Timer::after(Duration::from_secs(3)).await;
208///
209///     // Show "rUSt" solid forever.
210///     display.write_text(['r', 'U', 'S', 't'], BlinkState::Solid);
211///     core::future::pending().await
212/// }
213/// ```
214///
215/// Beyond simple text, the driver can loop animations via [`Led4::animate_text`].
216/// The struct owns the background task and signal wiring; create it once with
217/// [`Led4Esp::new`] and use the returned handle for all display updates.
218#[cfg(target_os = "none")]
219pub struct Led4Esp<'a>(&'a Led4EspOuterStatic);
220
221#[cfg(target_os = "none")]
222type Led4EspOuterStatic = device_envoy_core::led4::Led4CommandSignal;
223
224/// Static for the [`Led4Esp`] device.
225#[cfg(target_os = "none")]
226pub struct Led4EspStatic {
227    outer: Led4EspOuterStatic,
228    display: Led4SimpleStatic,
229}
230
231#[cfg(target_os = "none")]
232impl Led4EspStatic {
233    const fn new() -> Self {
234        Self {
235            outer: device_envoy_core::led4::Led4CommandSignal::new(),
236            display: Led4Simple::new_static(),
237        }
238    }
239
240    fn split(&self) -> (&Led4EspOuterStatic, &Led4SimpleStatic) {
241        (&self.outer, &self.display)
242    }
243}
244
245#[cfg(target_os = "none")]
246impl Led4Esp<'_> {
247    /// Creates the display device and spawns its background task.
248    #[must_use = "Must be used to manage the spawned task"]
249    pub fn new(
250        led4_static: &'static Led4EspStatic,
251        cell_pins: OutputArray<'static, CELL_COUNT>,
252        segment_pins: OutputArray<'static, SEGMENT_COUNT>,
253        spawner: Spawner,
254    ) -> Result<Self> {
255        let (outer_static, display_static) = led4_static.split();
256        let display = Led4Simple::new(display_static, cell_pins, segment_pins, spawner)?;
257        let token = led4_device_loop(outer_static, display);
258        spawner.spawn(token.map_err(Error::TaskSpawn)?);
259        Ok(Self(outer_static))
260    }
261
262    /// Creates static channel resources for [`Led4Esp::new`].
263    #[must_use]
264    pub const fn new_static() -> Led4EspStatic {
265        Led4EspStatic::new()
266    }
267}
268
269#[cfg(target_os = "none")]
270impl device_envoy_core::led4::Led4 for Led4Esp<'_> {
271    fn write_text(&self, text: [char; CELL_COUNT], blink_state: BlinkState) {
272        signal_text(self.0, text, blink_state);
273    }
274
275    fn animate_text<I>(&self, animation: I)
276    where
277        I: IntoIterator,
278        I::Item: core::borrow::Borrow<AnimationFrame>,
279    {
280        signal_animation(self.0, animation);
281    }
282}
283
284#[embassy_executor::task(pool_size = 2)]
285#[cfg(target_os = "none")]
286async fn led4_device_loop(
287    outer_static: &'static Led4EspOuterStatic,
288    display: Led4Simple<'static>,
289) -> ! {
290    run_command_loop(outer_static, |text| display.write_text(text)).await
291}