Skip to main content

device_envoy_esp/
lcd_text.rs

1//! A device abstraction for HD44780-compatible character LCDs (e.g., 16x2, 20x2, 20x4).
2//!
3//! This page provides the primary documentation and examples for LCD text
4//! devices.
5//!
6//! **After reading the examples below, see also:**
7//!
8//! - [`lcd_text!`](macro@crate::lcd_text) — Macro to generate a single LCD
9//!   text type (includes syntax details).
10//! - [`i2cs!`](macro@crate::i2cs) — Macro to generate multiple LCD text types
11//!   sharing one I2C resource (includes syntax details).
12//! - [`LcdTextGenerated`](lcd_text_generated::LcdTextGenerated) — Sample
13//!   generated LCD text type showing the constructor path.
14//! - [`I2csGenerated`](lcd_text_generated::I2csGenerated) — Sample generated
15//!   I2C group type for multiple LCD text devices.
16//! - [`LcdText`] — Core LCD text trait implemented by generated types.
17//!
18//! # Text Behavior
19//!
20//! `write_text(...)` behavior:
21//!
22//! - `\n` starts a new LCD row.
23//! - Characters past `WIDTH` on a row are "ignored".
24//! - Rows past `HEIGHT` are "ignored".
25//! - Non-ASCII Unicode characters are replaced with `?`.
26//! - Missing characters are padded with spaces.
27//!
28//! # Example: Write Text on One LCD
29//!
30//! In this example, the generated type is `LcdTextSimple`.
31//!
32//! ```rust,no_run
33//! # #![no_std]
34//! # #![no_main]
35//! # use core::convert::Infallible;
36//! # use esp_backtrace as _;
37//! use device_envoy_esp::{Result, init_and_start, lcd_text::{self, LcdText as _}};
38//!
39//! lcd_text! {
40//!     i2c: I2C0,
41//!     sda_pin: GPIO16,
42//!     scl_pin: GPIO17,
43//!     LcdTextSimple {
44//!         width: 16,
45//!         height: 2,
46//!         address: 0x27
47//!     }
48//! }
49//!
50//! # #[esp_rtos::main]
51//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
52//! #     let err = example(spawner).await.unwrap_err();
53//! #     panic!("{err:?}");
54//! # }
55//! async fn example(spawner: embassy_executor::Spawner) -> Result<Infallible> {
56//!     init_and_start!(p);
57//!     let lcd_text_simple = LcdTextSimple::new(p.I2C0, p.GPIO16, p.GPIO17, spawner)?;
58//!
59//!     lcd_text_simple.write_text("Hello from\ndevice-envoy!");
60//!
61//!     core::future::pending().await
62//! }
63//! ```
64//!
65//! # Example: Two LCDs Sharing One I2C Peripheral
66//!
67//! In this example, the generated group type is `I2cs0`.
68//!
69//! ```rust,no_run
70//! # #![no_std]
71//! # #![no_main]
72//! # use core::convert::Infallible;
73//! # use esp_backtrace as _;
74//! use device_envoy_esp::{Result, i2cs, init_and_start, lcd_text::LcdText as _};
75//!
76//! i2cs! {
77//!     i2c: I2C0,
78//!     sda_pin: GPIO16,
79//!     scl_pin: GPIO17,
80//!     I2cs0 {
81//!         LcdText16x2 { width: 16, height: 2, address: 0x27 },
82//!         LcdText20x4 { width: 20, height: 4, address: 0x3F },
83//!     }
84//! }
85//!
86//! # #[esp_rtos::main]
87//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
88//! #     let err = example(spawner).await.unwrap_err();
89//! #     panic!("{err:?}");
90//! # }
91//! async fn example(spawner: embassy_executor::Spawner) -> Result<Infallible> {
92//!     init_and_start!(p);
93//!     let (lcd_text16x2, lcd_text20x4) = I2cs0::new(p.I2C0, p.GPIO16, p.GPIO17, spawner)?;
94//!
95//!     lcd_text16x2.write_text("16x2\nready");
96//!     lcd_text20x4.write_text("20x4\nshared i2c\naddress 0x3F");
97//!
98//!     core::future::pending().await
99//! }
100//! ```
101
102use device_envoy_core::lcd_text::{LcdTextDriver, LcdTextFrame, LcdTextWrite};
103use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
104use embassy_sync::signal::Signal;
105use heapless::Vec;
106
107#[doc(hidden)]
108pub use paste;
109
110pub use device_envoy_core::lcd_text::LcdText;
111
112#[cfg(doc)]
113pub mod lcd_text_generated {
114    use crate::Result;
115
116    /// Sample struct type generated by the [`i2cs!`](macro@crate::i2cs) macro.
117    ///
118    /// This page exists to show constructor and methods in one place.
119    /// For narrative examples, see the [`lcd_text`](mod@crate::lcd_text) module.
120    pub struct LcdTextGenerated;
121
122    /// Sample I2C LCD group type generated by [`i2cs!`](macro@crate::i2cs).
123    pub struct I2csGenerated;
124
125    /// Sample LCD text struct type generated by [`i2cs!`](macro@crate::i2cs).
126    pub struct LcdTextGenerated20x4;
127
128    impl I2csGenerated {
129        /// Construct all generated LCD text devices in this group.
130        /// See the [`lcd_text` module documentation](mod@crate::lcd_text) for usage examples.
131        pub fn new<I2cPeripheral, SdaPin, SclPin>(
132            i2c_peripheral: I2cPeripheral,
133            sda: SdaPin,
134            scl: SclPin,
135            spawner: embassy_executor::Spawner,
136        ) -> Result<(&'static LcdTextGenerated, &'static LcdTextGenerated20x4)> {
137            static INSTANCE_16X2: LcdTextGenerated = LcdTextGenerated;
138            static INSTANCE_20X4: LcdTextGenerated20x4 = LcdTextGenerated20x4;
139            let _ = (i2c_peripheral, sda, scl, spawner);
140            Ok((&INSTANCE_16X2, &INSTANCE_20X4))
141        }
142    }
143
144    impl LcdTextGenerated {
145        /// Display width in characters.
146        pub const WIDTH: usize = 16;
147        /// Display height in characters.
148        pub const HEIGHT: usize = 2;
149        /// LCD I2C address.
150        pub const ADDRESS: u8 = 0x27;
151
152        /// Create this generated LCD text instance.
153        /// See the [`lcd_text` module documentation](mod@crate::lcd_text) for usage examples.
154        pub fn new<I2cPeripheral, SdaPin, SclPin>(
155            i2c_peripheral: I2cPeripheral,
156            sda: SdaPin,
157            scl: SclPin,
158            spawner: embassy_executor::Spawner,
159        ) -> Result<&'static Self> {
160            static INSTANCE: LcdTextGenerated = LcdTextGenerated;
161            let _ = (i2c_peripheral, sda, scl, spawner);
162            Ok(&INSTANCE)
163        }
164    }
165
166    impl crate::lcd_text::LcdText<16, 2> for LcdTextGenerated {
167        const ADDRESS: u8 = 0x27;
168
169        fn write_text(&self, text: impl AsRef<str>) {
170            let _ = text;
171        }
172    }
173
174    impl LcdTextGenerated20x4 {
175        /// Display width in characters.
176        pub const WIDTH: usize = 20;
177        /// Display height in characters.
178        pub const HEIGHT: usize = 4;
179        /// LCD I2C address.
180        pub const ADDRESS: u8 = 0x3F;
181
182        /// Create this generated LCD text instance.
183        /// See the [`lcd_text` module documentation](mod@crate::lcd_text) for usage examples.
184        pub fn new<I2cPeripheral, SdaPin, SclPin>(
185            i2c_peripheral: I2cPeripheral,
186            sda: SdaPin,
187            scl: SclPin,
188            spawner: embassy_executor::Spawner,
189        ) -> Result<&'static Self> {
190            static INSTANCE: LcdTextGenerated20x4 = LcdTextGenerated20x4;
191            let _ = (i2c_peripheral, sda, scl, spawner);
192            Ok(&INSTANCE)
193        }
194    }
195
196    impl crate::lcd_text::LcdText<20, 4> for LcdTextGenerated20x4 {
197        const ADDRESS: u8 = 0x3F;
198
199        fn write_text(&self, text: impl AsRef<str>) {
200            let _ = text;
201        }
202    }
203}
204
205#[doc(hidden)]
206pub type __I2csSignal<T> = Signal<CriticalSectionRawMutex, T>;
207#[doc(hidden)]
208pub use device_envoy_core::lcd_text::LcdText as __LcdText;
209#[doc(hidden)]
210pub use device_envoy_core::lcd_text::LcdTextDriver as __LcdTextDriver;
211#[doc(hidden)]
212pub use device_envoy_core::lcd_text::render_lcd_text_frame as __render_lcd_text_frame;
213#[doc(hidden)]
214pub type __LcdTextFrame<const MAX_CHARS: usize> =
215    device_envoy_core::lcd_text::LcdTextFrame<MAX_CHARS>;
216#[doc(hidden)]
217pub const fn __max_lcd_cells<const N: usize>(widths: [usize; N], heights: [usize; N]) -> usize {
218    let mut max_cells = 0;
219    let mut index = 0;
220    while index < N {
221        let cells = widths[index] * heights[index];
222        if cells > max_cells {
223            max_cells = cells;
224        }
225        index += 1;
226    }
227    max_cells
228}
229#[doc(hidden)]
230pub async fn __select_array<Fut, const N: usize>(futures: [Fut; N]) -> (Fut::Output, usize)
231where
232    Fut: core::future::Future,
233{
234    embassy_futures::select::select_array(futures).await
235}
236
237#[doc(hidden)]
238pub const fn __assert_unique_addresses<const N: usize>(addresses: [u8; N]) {
239    let mut first_index = 0;
240    while first_index < N {
241        let mut second_index = first_index + 1;
242        while second_index < N {
243            if addresses[first_index] == addresses[second_index] {
244                panic!("duplicate lcd_text I2C address in i2cs! group");
245            }
246            second_index += 1;
247        }
248        first_index += 1;
249    }
250}
251
252#[doc(hidden)]
253pub async fn __write_lcd_text_cells<const ADDRESS_COUNT: usize, const MAX_CHARS: usize>(
254    lcd_text_driver: &mut LcdTextDriver,
255    lcd_text_write: &mut impl LcdTextWrite,
256    initialized_addresses: &mut Vec<u8, ADDRESS_COUNT>,
257    address: u8,
258    width: usize,
259    height: usize,
260    cells: &[u8],
261) {
262    let first_use_of_address = !initialized_addresses
263        .iter()
264        .any(|initialized_address| *initialized_address == address);
265
266    lcd_text_driver.set_address(address);
267    if first_use_of_address {
268        if lcd_text_driver.init(lcd_text_write).await.is_err() {
269            return;
270        }
271        let _ = initialized_addresses.push(address);
272    }
273
274    let mut lcd_text_frame = LcdTextFrame::<MAX_CHARS>::new_blank(width, height);
275    let cell_count = core::cmp::min(width * height, cells.len());
276    for cell_index in 0..cell_count {
277        lcd_text_frame.cells[cell_index] = cells[cell_index];
278    }
279
280    let _ = lcd_text_driver
281        .write_frame(lcd_text_write, &lcd_text_frame)
282        .await;
283}
284
285#[doc(hidden)]
286pub struct EspLcdTextWrite {
287    i2c: crate::esp_hal::i2c::master::I2c<'static, crate::esp_hal::Blocking>,
288}
289
290impl EspLcdTextWrite {
291    #[doc(hidden)]
292    pub fn __new(i2c: crate::esp_hal::i2c::master::I2c<'static, crate::esp_hal::Blocking>) -> Self {
293        Self { i2c }
294    }
295}
296
297impl LcdTextWrite for EspLcdTextWrite {
298    fn write(&mut self, address: u8, data: u8) -> device_envoy_core::Result<()> {
299        self.i2c
300            .write(address, &[data])
301            .map_err(|_| device_envoy_core::Error::LcdI2cWrite { address })
302    }
303}
304
305/// Macro to generate multiple LCD text device types that share one I2C
306/// resource (includes syntax details).
307///
308/// For a single LCD type, see [`lcd_text!`](macro@crate::lcd_text).
309///
310/// **Syntax:**
311///
312/// ```text
313/// i2cs! {
314///     i2c: <i2c_ident>,
315///     sda_pin: <sda_pin_ident>,
316///     scl_pin: <scl_pin_ident>,
317///     [<visibility>] <GroupName> {
318///         [<visibility>] <LcdName> {
319///             width: <usize_expr>,
320///             height: <usize_expr>,
321///             address: <u8_expr>
322///         },
323///         // ...more LCD entries...
324///     }
325/// }
326/// ```
327///
328/// **See the [lcd_text module documentation](mod@crate::lcd_text) for usage
329/// examples.**
330#[cfg(not(feature = "host"))]
331#[doc(hidden)]
332#[macro_export]
333macro_rules! i2cs {
334    ($($tt:tt)*) => { $crate::__i2cs_impl! { $($tt)* } };
335}
336
337#[cfg(not(feature = "host"))]
338#[doc(hidden)]
339#[macro_export]
340macro_rules! __i2cs_impl {
341    (
342        i2c: $i2c:ident,
343        sda_pin: $sda_pin:ident,
344        scl_pin: $scl_pin:ident,
345        $group_vis:vis $group_name:ident {
346            $(
347                $lcd_vis:vis $lcd_name:ident {
348                    width: $width:expr,
349                    height: $height:expr,
350                    address: $address:expr
351                }
352            ),+ $(,)?
353        }
354    ) => {
355        $crate::lcd_text::paste::paste! {
356            const _: () = {
357                $crate::lcd_text::__assert_unique_addresses([$($address,)+]);
358            };
359            const [<__ $group_name:upper _MAX_LCD_CELLS>]: usize =
360                $crate::lcd_text::__max_lcd_cells([$($width,)+], [$($height,)+]);
361
362            $(
363                static [<$lcd_name:upper _FRAME_SIGNAL>]:
364                    $crate::lcd_text::__I2csSignal<
365                        $crate::lcd_text::__LcdTextFrame<{ [<__ $group_name:upper _MAX_LCD_CELLS>] }>
366                    > =
367                    $crate::lcd_text::__I2csSignal::new();
368            )+
369
370            $group_vis struct $group_name;
371
372            struct [<__ $group_name Devices>] {
373                $(
374                    [<$lcd_name:snake>]: &'static $lcd_name,
375                )+
376            }
377
378            impl [<__ $group_name Devices>] {
379                fn into_tuple(self) -> ($(&'static $lcd_name,)+) {
380                    (
381                        $(self.[<$lcd_name:snake>],)+
382                    )
383                }
384            }
385
386            impl $group_name {
387                fn __new_devices(
388                    i2c_peripheral: $crate::esp_hal::peripherals::$i2c<'static>,
389                    sda: $crate::esp_hal::peripherals::$sda_pin<'static>,
390                    scl: $crate::esp_hal::peripherals::$scl_pin<'static>,
391                    spawner: embassy_executor::Spawner,
392                ) -> $crate::Result<[<__ $group_name Devices>]> {
393                    let i2c = $crate::esp_hal::i2c::master::I2c::new(
394                        i2c_peripheral,
395                        $crate::esp_hal::i2c::master::Config::default(),
396                    )
397                    .map_err($crate::Error::I2cConfig)?
398                    .with_sda(sda)
399                    .with_scl(scl);
400
401                    let token = [<__i2cs_task_ $group_name:snake>](i2c);
402                    spawner.spawn(token.map_err($crate::Error::TaskSpawn)?);
403
404                    $(
405                        static [<$lcd_name:upper _INSTANCE>]: $lcd_name = $lcd_name;
406                        let [<$lcd_name:snake>] = &[<$lcd_name:upper _INSTANCE>];
407                    )+
408
409                    Ok([<__ $group_name Devices>] {
410                        $(
411                            [<$lcd_name:snake>],
412                        )+
413                    })
414                }
415
416                pub fn new(
417                    i2c_peripheral: $crate::esp_hal::peripherals::$i2c<'static>,
418                    sda: $crate::esp_hal::peripherals::$sda_pin<'static>,
419                    scl: $crate::esp_hal::peripherals::$scl_pin<'static>,
420                    spawner: embassy_executor::Spawner,
421                ) -> $crate::Result<($(&'static $lcd_name,)+)> {
422                    Ok(Self::__new_devices(i2c_peripheral, sda, scl, spawner)?.into_tuple())
423                }
424            }
425
426            $(
427                $lcd_vis struct $lcd_name;
428
429                impl $crate::lcd_text::__LcdText<$width, $height> for $lcd_name {
430                    const ADDRESS: u8 = $address;
431
432                    fn write_text(&self, text: impl AsRef<str>) {
433                        ::core::assert!($width > 0, "lcd_text width must be > 0");
434                        ::core::assert!($height > 0, "lcd_text height must be > 0");
435                        ::core::assert!(
436                            $height <= 4,
437                            "lcd_text height must be <= 4 for HD44780 row map"
438                        );
439                        let lcd_text_frame =
440                            $crate::lcd_text::__render_lcd_text_frame::<
441                                $width,
442                                $height,
443                                { [<__ $group_name:upper _MAX_LCD_CELLS>] }
444                            >(text.as_ref());
445                        [<$lcd_name:upper _FRAME_SIGNAL>].signal(lcd_text_frame);
446                    }
447                }
448
449                impl $lcd_name {
450                    pub const WIDTH: usize = $width;
451                    pub const HEIGHT: usize = $height;
452                    pub const ADDRESS: u8 = $address;
453
454                    pub fn new(
455                        i2c_peripheral: $crate::esp_hal::peripherals::$i2c<'static>,
456                        sda: $crate::esp_hal::peripherals::$sda_pin<'static>,
457                        scl: $crate::esp_hal::peripherals::$scl_pin<'static>,
458                        spawner: embassy_executor::Spawner,
459                    ) -> $crate::Result<&'static Self> {
460                        let [<__ $group_name:snake _devices>] =
461                            $group_name::__new_devices(i2c_peripheral, sda, scl, spawner)?;
462                        Ok([<__ $group_name:snake _devices>].[<$lcd_name:snake>])
463                    }
464
465                }
466            )+
467
468            #[embassy_executor::task]
469            async fn [<__i2cs_task_ $group_name:snake>](
470                i2c: $crate::esp_hal::i2c::master::I2c<'static, $crate::esp_hal::Blocking>,
471            ) -> ! {
472                let mut esp_lcd_text_write = $crate::lcd_text::EspLcdTextWrite::__new(i2c);
473                let mut lcd_text_driver = $crate::lcd_text::__LcdTextDriver::new(0x27);
474                const ADDRESS_COUNT: usize = [$($address,)+].len();
475                let mut initialized_addresses: heapless::Vec<u8, ADDRESS_COUNT> = heapless::Vec::new();
476                let addresses = [$($address,)+];
477                let widths = [$($width,)+];
478                let heights = [$($height,)+];
479
480                loop {
481                    let (lcd_text_frame, ready_index) = $crate::lcd_text::__select_array([
482                        $([<$lcd_name:upper _FRAME_SIGNAL>].wait(),)+
483                    ]).await;
484                    $crate::lcd_text::__write_lcd_text_cells::<
485                        ADDRESS_COUNT,
486                        { [<__ $group_name:upper _MAX_LCD_CELLS>] }
487                    >(
488                        &mut lcd_text_driver,
489                        &mut esp_lcd_text_write,
490                        &mut initialized_addresses,
491                        addresses[ready_index],
492                        widths[ready_index],
493                        heights[ready_index],
494                        &lcd_text_frame.cells,
495                    ).await;
496                }
497            }
498        }
499    };
500}
501
502#[cfg(not(feature = "host"))]
503#[doc(inline)]
504pub use i2cs;
505
506/// Macro to generate a single LCD text device type with a direct constructor.
507///
508/// **Syntax:**
509///
510/// ```text
511/// lcd_text! {
512///     i2c: <i2c_ident>,
513///     sda_pin: <sda_pin_ident>,
514///     scl_pin: <scl_pin_ident>,
515///     [<visibility>] <LcdName> {
516///         width: <usize_expr>,
517///         height: <usize_expr>,
518///         address: <u8_expr>
519///     }
520/// }
521/// ```
522///
523/// For multiple LCD types sharing one I2C peripheral, see
524/// [`i2cs!`](macro@crate::i2cs).
525///
526/// **See the [lcd_text module documentation](mod@crate::lcd_text) for usage
527/// examples.**
528#[cfg(not(feature = "host"))]
529#[doc(hidden)]
530#[macro_export]
531macro_rules! lcd_text {
532    ($($tt:tt)*) => { $crate::__lcd_text_impl! { $($tt)* } };
533}
534
535#[cfg(not(feature = "host"))]
536#[doc(hidden)]
537#[macro_export]
538macro_rules! __lcd_text_impl {
539    (
540        i2c: $i2c:ident,
541        sda_pin: $sda_pin:ident,
542        scl_pin: $scl_pin:ident,
543        $lcd_vis:vis $lcd_name:ident {
544            width: $width:expr,
545            height: $height:expr,
546            address: $address:expr
547        }
548    ) => {
549        $crate::lcd_text::paste::paste! {
550            $crate::i2cs! {
551                i2c: $i2c,
552                sda_pin: $sda_pin,
553                scl_pin: $scl_pin,
554                [<LcdTextGroupFor $lcd_name>] {
555                    $lcd_vis $lcd_name {
556                        width: $width,
557                        height: $height,
558                        address: $address
559                    }
560                }
561            }
562        }
563    };
564}
565
566#[cfg(not(feature = "host"))]
567#[doc(inline)]
568pub use lcd_text;