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