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