1use embassy_time::Timer;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum LcdTextError {
10 I2cWrite { address: u8 },
12 RowOutOfBounds { row: usize },
14}
15
16#[derive(Clone, Copy, Debug)]
18#[doc(hidden)]
20pub struct LcdTextFrame<const MAX_CHARS: usize> {
21 pub width: usize,
23 pub height: usize,
25 pub cells: [u8; MAX_CHARS],
27}
28
29impl<const MAX_CHARS: usize> LcdTextFrame<MAX_CHARS> {
30 #[must_use]
32 pub const fn new_blank(width: usize, height: usize) -> Self {
33 assert!(
34 width * height <= MAX_CHARS,
35 "frame geometry exceeds capacity"
36 );
37 Self {
38 width,
39 height,
40 cells: [b' '; MAX_CHARS],
41 }
42 }
43
44 #[must_use]
46 pub fn from_rows<const W: usize, const H: usize>(rows: [[u8; W]; H]) -> Self {
47 let mut lcd_text_frame = Self::new_blank(W, H);
48 let mut row_index = 0;
49 while row_index < H {
50 let mut column_index = 0;
51 while column_index < W {
52 let flat_index = row_index * W + column_index;
53 lcd_text_frame.cells[flat_index] = rows[row_index][column_index];
54 column_index += 1;
55 }
56 row_index += 1;
57 }
58 lcd_text_frame
59 }
60
61 #[must_use]
63 pub fn cell(&self, row: usize, col: usize) -> u8 {
64 self.cells[row * self.width + col]
65 }
66}
67
68#[must_use]
77#[doc(hidden)]
79pub fn render_lcd_text_frame<const W: usize, const H: usize, const MAX_CHARS: usize>(
80 text: &str,
81) -> LcdTextFrame<MAX_CHARS> {
82 let mut rows = [[b' '; W]; H];
83
84 for (row_index, line) in text.split('\n').enumerate() {
85 if row_index >= H {
86 break;
87 }
88
89 for (column_index, ch) in line.chars().enumerate() {
90 if column_index >= W {
91 break;
92 }
93 rows[row_index][column_index] = if ch.is_ascii() { ch as u8 } else { b'?' };
94 }
95 }
96
97 LcdTextFrame::<MAX_CHARS>::from_rows(rows)
98}
99
100pub trait LcdText<const W: usize, const H: usize> {
131 const WIDTH: usize = W;
133 const HEIGHT: usize = H;
135 const ADDRESS: u8;
137
138 fn write_text(&self, text: impl AsRef<str>);
141}
142
143pub trait LcdTextWrite {
145 fn write(&mut self, address: u8, data: u8) -> Result<(), LcdTextError>;
147}
148
149const LCD_BACKLIGHT: u8 = 0x08;
151const LCD_ENABLE: u8 = 0x04;
152const LCD_RS: u8 = 0x01;
153
154#[doc(hidden)]
157pub struct LcdTextDriver {
158 address: u8,
159}
160
161impl LcdTextDriver {
162 #[must_use]
164 pub const fn new(address: u8) -> Self {
165 Self { address }
166 }
167
168 pub fn set_address(&mut self, address: u8) {
170 self.address = address;
171 }
172
173 pub async fn init(
175 &mut self,
176 lcd_text_write: &mut impl LcdTextWrite,
177 ) -> Result<(), LcdTextError> {
178 Timer::after_millis(50).await;
179
180 self.write_nibble(lcd_text_write, 0x03, false).await?;
181 Timer::after_millis(5).await;
182 self.write_nibble(lcd_text_write, 0x03, false).await?;
183 Timer::after_micros(150).await;
184 self.write_nibble(lcd_text_write, 0x03, false).await?;
185 self.write_nibble(lcd_text_write, 0x02, false).await?;
186
187 self.write_byte(lcd_text_write, 0x28, false).await?;
189 self.write_byte(lcd_text_write, 0x0C, false).await?;
191 self.write_byte(lcd_text_write, 0x01, false).await?;
193 Timer::after_millis(2).await;
194 self.write_byte(lcd_text_write, 0x06, false).await?;
196 Ok(())
197 }
198
199 pub async fn write_frame<const MAX_CHARS: usize>(
201 &mut self,
202 lcd_text_write: &mut impl LcdTextWrite,
203 lcd_text_frame: &LcdTextFrame<MAX_CHARS>,
204 ) -> Result<(), LcdTextError> {
205 self.clear(lcd_text_write).await?;
206
207 for row_index in 0..lcd_text_frame.height {
208 self.set_cursor(lcd_text_write, row_index, 0).await?;
209 for column_index in 0..lcd_text_frame.width {
210 self.write_byte(
211 lcd_text_write,
212 lcd_text_frame.cell(row_index, column_index),
213 true,
214 )
215 .await?;
216 }
217 }
218
219 Ok(())
220 }
221
222 #[expect(clippy::arithmetic_side_effects, reason = "Bit operations")]
223 async fn write_nibble(
224 &mut self,
225 lcd_text_write: &mut impl LcdTextWrite,
226 nibble: u8,
227 rs: bool,
228 ) -> Result<(), LcdTextError> {
229 let rs_bit = if rs { LCD_RS } else { 0 };
230 let data = (nibble << 4) | LCD_BACKLIGHT | rs_bit;
231
232 lcd_text_write.write(self.address, data | LCD_ENABLE)?;
233 Timer::after_micros(1).await;
234 lcd_text_write.write(self.address, data)?;
235 Timer::after_micros(50).await;
236 Ok(())
237 }
238
239 async fn write_byte(
240 &mut self,
241 lcd_text_write: &mut impl LcdTextWrite,
242 byte: u8,
243 rs: bool,
244 ) -> Result<(), LcdTextError> {
245 self.write_nibble(lcd_text_write, (byte >> 4) & 0x0F, rs)
246 .await?;
247 self.write_nibble(lcd_text_write, byte & 0x0F, rs).await?;
248 Ok(())
249 }
250
251 async fn clear(&mut self, lcd_text_write: &mut impl LcdTextWrite) -> Result<(), LcdTextError> {
252 self.write_byte(lcd_text_write, 0x01, false).await?;
253 Timer::after_millis(2).await;
254 Ok(())
255 }
256
257 #[expect(
258 clippy::arithmetic_side_effects,
259 reason = "Row/column values are small"
260 )]
261 async fn set_cursor(
262 &mut self,
263 lcd_text_write: &mut impl LcdTextWrite,
264 row: usize,
265 col: u8,
266 ) -> Result<(), LcdTextError> {
267 let address = match row {
268 0 => col,
269 1 => 0x40_u8 + col,
270 2 => 0x14_u8 + col,
271 3 => 0x54_u8 + col,
272 _ => return Err(LcdTextError::RowOutOfBounds { row }),
273 };
274 self.write_byte(lcd_text_write, 0x80 | address, false)
275 .await?;
276 Ok(())
277 }
278}