Skip to main content

device_envoy_core/
lcd_text.rs

1//! A device abstraction for shared HD44780 character LCD protocol/state helpers.
2//!
3//! See `device_envoy_rp::lcd_text` for constructors and usage examples.
4
5use embassy_time::Timer;
6
7/// A packed text frame for an HD44780 display.
8#[derive(Clone, Copy, Debug)]
9// Public for cross-crate platform plumbing; hidden from end-user docs.
10#[doc(hidden)]
11pub struct LcdTextFrame<const MAX_CHARS: usize> {
12    /// Frame width in characters.
13    pub width: usize,
14    /// Frame height in characters.
15    pub height: usize,
16    /// Packed row-major cell bytes.
17    pub cells: [u8; MAX_CHARS],
18}
19
20impl<const MAX_CHARS: usize> LcdTextFrame<MAX_CHARS> {
21    /// Create a blank frame with spaces.
22    #[must_use]
23    pub const fn new_blank(width: usize, height: usize) -> Self {
24        assert!(
25            width * height <= MAX_CHARS,
26            "frame geometry exceeds capacity"
27        );
28        Self {
29            width,
30            height,
31            cells: [b' '; MAX_CHARS],
32        }
33    }
34
35    /// Build a packed frame from a fixed `W x H` buffer.
36    #[must_use]
37    pub fn from_rows<const W: usize, const H: usize>(rows: [[u8; W]; H]) -> Self {
38        let mut lcd_text_frame = Self::new_blank(W, H);
39        let mut row_index = 0;
40        while row_index < H {
41            let mut column_index = 0;
42            while column_index < W {
43                let flat_index = row_index * W + column_index;
44                lcd_text_frame.cells[flat_index] = rows[row_index][column_index];
45                column_index += 1;
46            }
47            row_index += 1;
48        }
49        lcd_text_frame
50    }
51
52    /// Returns the byte at `(row, col)` in this frame.
53    #[must_use]
54    pub fn cell(&self, row: usize, col: usize) -> u8 {
55        self.cells[row * self.width + col]
56    }
57}
58
59/// Render text into a fixed-size LCD frame using `W x H` geometry.
60///
61/// Behavior:
62/// - `\n` starts a new row.
63/// - Characters past `W` on a row are ignored.
64/// - Rows past `H` are ignored.
65/// - Non-ASCII Unicode characters are replaced with `?`.
66/// - Missing characters are padded with spaces.
67#[must_use]
68// Public for cross-crate platform plumbing; hidden from end-user docs.
69#[doc(hidden)]
70pub fn render_lcd_text_frame<const W: usize, const H: usize, const MAX_CHARS: usize>(
71    text: &str,
72) -> LcdTextFrame<MAX_CHARS> {
73    let mut rows = [[b' '; W]; H];
74
75    for (row_index, line) in text.split('\n').enumerate() {
76        if row_index >= H {
77            break;
78        }
79
80        for (column_index, ch) in line.chars().enumerate() {
81            if column_index >= W {
82                break;
83            }
84            rows[row_index][column_index] = if ch.is_ascii() { ch as u8 } else { b'?' };
85        }
86    }
87
88    LcdTextFrame::<MAX_CHARS>::from_rows(rows)
89}
90
91/// Platform-agnostic LCD text device contract.
92///
93/// Platform crates implement this trait for their generated LCD text types so
94/// shared logic can write text without knowing the hardware backend.
95///
96/// Design intent:
97///
98/// - This trait is intended for static dispatch on embedded targets.
99/// - Dimensions are const generics so geometry remains compile-time.
100/// - `write_text` accepts any string-like input via `AsRef<str>`.
101///
102/// # Example: Write Text
103///
104/// This example writes text through a generic trait-bound helper.
105///
106/// ```rust,no_run
107/// use device_envoy_core::lcd_text::LcdText;
108///
109/// fn write_message<const W: usize, const H: usize>(lcd_text: &impl LcdText<W, H>) {
110///     lcd_text.write_text("Hello from\ndevice-envoy!");
111/// }
112///
113/// # struct LcdTextSimple;
114/// # impl LcdText<16, 2> for LcdTextSimple {
115/// #     const ADDRESS: u8 = 0x27;
116/// #     fn write_text(&self, _text: impl AsRef<str>) {}
117/// # }
118/// # let lcd_text_simple = LcdTextSimple;
119/// # write_message(&lcd_text_simple);
120/// ```
121pub trait LcdText<const W: usize, const H: usize> {
122    /// Display width in characters.
123    const WIDTH: usize = W;
124    /// Display height in characters.
125    const HEIGHT: usize = H;
126    /// LCD I2C address.
127    const ADDRESS: u8;
128
129    /// Write text to the display.
130    /// See the [LcdText trait documentation](Self) for usage examples.
131    fn write_text(&self, text: impl AsRef<str>);
132}
133
134/// Character LCD write adapter for platform crates.
135pub trait LcdTextWrite {
136    /// Write one byte to the configured LCD I2C expander.
137    fn write(&mut self, address: u8, data: u8) -> crate::Result<()>;
138}
139
140// PCF8574 pin mapping: P0=RS, P1=RW, P2=E, P3=Backlight, P4-P7=Data.
141const LCD_BACKLIGHT: u8 = 0x08;
142const LCD_ENABLE: u8 = 0x04;
143const LCD_RS: u8 = 0x01;
144
145/// HD44780 protocol driver over a byte-oriented I2C expander transport.
146// Public for cross-crate platform plumbing; hidden from end-user docs.
147#[doc(hidden)]
148pub struct LcdTextDriver {
149    address: u8,
150}
151
152impl LcdTextDriver {
153    /// Creates a driver for a specific PCF8574 backpack address.
154    #[must_use]
155    pub const fn new(address: u8) -> Self {
156        Self { address }
157    }
158
159    /// Set the active LCD I2C address for subsequent writes.
160    pub fn set_address(&mut self, address: u8) {
161        self.address = address;
162    }
163
164    /// Initialize the LCD in 4-bit mode and clear it.
165    pub async fn init(&mut self, lcd_text_write: &mut impl LcdTextWrite) -> crate::Result<()> {
166        Timer::after_millis(50).await;
167
168        self.write_nibble(lcd_text_write, 0x03, false).await?;
169        Timer::after_millis(5).await;
170        self.write_nibble(lcd_text_write, 0x03, false).await?;
171        Timer::after_micros(150).await;
172        self.write_nibble(lcd_text_write, 0x03, false).await?;
173        self.write_nibble(lcd_text_write, 0x02, false).await?;
174
175        // Function set: 4-bit, 2 lines, 5x8 font.
176        self.write_byte(lcd_text_write, 0x28, false).await?;
177        // Display control: display on, cursor off, blink off.
178        self.write_byte(lcd_text_write, 0x0C, false).await?;
179        // Clear display.
180        self.write_byte(lcd_text_write, 0x01, false).await?;
181        Timer::after_millis(2).await;
182        // Entry mode: increment cursor, no shift.
183        self.write_byte(lcd_text_write, 0x06, false).await?;
184        Ok(())
185    }
186
187    /// Write one full frame to the LCD.
188    pub async fn write_frame<const MAX_CHARS: usize>(
189        &mut self,
190        lcd_text_write: &mut impl LcdTextWrite,
191        lcd_text_frame: &LcdTextFrame<MAX_CHARS>,
192    ) -> crate::Result<()> {
193        self.clear(lcd_text_write).await?;
194
195        for row_index in 0..lcd_text_frame.height {
196            self.set_cursor(lcd_text_write, row_index, 0).await?;
197            for column_index in 0..lcd_text_frame.width {
198                self.write_byte(
199                    lcd_text_write,
200                    lcd_text_frame.cell(row_index, column_index),
201                    true,
202                )
203                .await?;
204            }
205        }
206
207        Ok(())
208    }
209
210    async fn write_nibble(
211        &mut self,
212        lcd_text_write: &mut impl LcdTextWrite,
213        nibble: u8,
214        rs: bool,
215    ) -> crate::Result<()> {
216        let rs_bit = if rs { LCD_RS } else { 0 };
217        let data = (nibble << 4) | LCD_BACKLIGHT | rs_bit;
218
219        lcd_text_write.write(self.address, data | LCD_ENABLE)?;
220        Timer::after_micros(1).await;
221        lcd_text_write.write(self.address, data)?;
222        Timer::after_micros(50).await;
223        Ok(())
224    }
225
226    async fn write_byte(
227        &mut self,
228        lcd_text_write: &mut impl LcdTextWrite,
229        byte: u8,
230        rs: bool,
231    ) -> crate::Result<()> {
232        self.write_nibble(lcd_text_write, (byte >> 4) & 0x0F, rs)
233            .await?;
234        self.write_nibble(lcd_text_write, byte & 0x0F, rs).await?;
235        Ok(())
236    }
237
238    async fn clear(&mut self, lcd_text_write: &mut impl LcdTextWrite) -> crate::Result<()> {
239        self.write_byte(lcd_text_write, 0x01, false).await?;
240        Timer::after_millis(2).await;
241        Ok(())
242    }
243
244    #[expect(
245        clippy::arithmetic_side_effects,
246        reason = "Row/column values are small"
247    )]
248    async fn set_cursor(
249        &mut self,
250        lcd_text_write: &mut impl LcdTextWrite,
251        row: usize,
252        col: u8,
253    ) -> crate::Result<()> {
254        let address = match row {
255            0 => col,
256            1 => 0x40_u8 + col,
257            2 => 0x14_u8 + col,
258            3 => 0x54_u8 + col,
259            _ => return Err(crate::Error::LcdRowOutOfBounds { row }),
260        };
261        self.write_byte(lcd_text_write, 0x80 | address, false)
262            .await?;
263        Ok(())
264    }
265}