use embassy_time::Timer;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LcdTextError {
I2cWrite { address: u8 },
RowOutOfBounds { row: usize },
}
#[derive(Clone, Copy, Debug)]
#[doc(hidden)]
pub struct LcdTextFrame<const MAX_CHARS: usize> {
pub width: usize,
pub height: usize,
pub cells: [u8; MAX_CHARS],
}
impl<const MAX_CHARS: usize> LcdTextFrame<MAX_CHARS> {
#[must_use]
pub const fn new_blank(width: usize, height: usize) -> Self {
assert!(
width * height <= MAX_CHARS,
"frame geometry exceeds capacity"
);
Self {
width,
height,
cells: [b' '; MAX_CHARS],
}
}
#[must_use]
pub fn from_rows<const W: usize, const H: usize>(rows: [[u8; W]; H]) -> Self {
let mut lcd_text_frame = Self::new_blank(W, H);
let mut row_index = 0;
while row_index < H {
let mut column_index = 0;
while column_index < W {
let flat_index = row_index * W + column_index;
lcd_text_frame.cells[flat_index] = rows[row_index][column_index];
column_index += 1;
}
row_index += 1;
}
lcd_text_frame
}
#[must_use]
pub fn cell(&self, row: usize, col: usize) -> u8 {
self.cells[row * self.width + col]
}
}
#[must_use]
#[doc(hidden)]
pub fn render_lcd_text_frame<const W: usize, const H: usize, const MAX_CHARS: usize>(
text: &str,
) -> LcdTextFrame<MAX_CHARS> {
let mut rows = [[b' '; W]; H];
for (row_index, line) in text.split('\n').enumerate() {
if row_index >= H {
break;
}
for (column_index, ch) in line.chars().enumerate() {
if column_index >= W {
break;
}
rows[row_index][column_index] = if ch.is_ascii() { ch as u8 } else { b'?' };
}
}
LcdTextFrame::<MAX_CHARS>::from_rows(rows)
}
pub trait LcdText<const W: usize, const H: usize> {
const WIDTH: usize = W;
const HEIGHT: usize = H;
const ADDRESS: u8;
fn write_text(&self, text: impl AsRef<str>);
}
pub trait LcdTextWrite {
fn write(&mut self, address: u8, data: u8) -> Result<(), LcdTextError>;
}
const LCD_BACKLIGHT: u8 = 0x08;
const LCD_ENABLE: u8 = 0x04;
const LCD_RS: u8 = 0x01;
#[doc(hidden)]
pub struct LcdTextDriver {
address: u8,
}
impl LcdTextDriver {
#[must_use]
pub const fn new(address: u8) -> Self {
Self { address }
}
pub fn set_address(&mut self, address: u8) {
self.address = address;
}
pub async fn init(
&mut self,
lcd_text_write: &mut impl LcdTextWrite,
) -> Result<(), LcdTextError> {
Timer::after_millis(50).await;
self.write_nibble(lcd_text_write, 0x03, false).await?;
Timer::after_millis(5).await;
self.write_nibble(lcd_text_write, 0x03, false).await?;
Timer::after_micros(150).await;
self.write_nibble(lcd_text_write, 0x03, false).await?;
self.write_nibble(lcd_text_write, 0x02, false).await?;
self.write_byte(lcd_text_write, 0x28, false).await?;
self.write_byte(lcd_text_write, 0x0C, false).await?;
self.write_byte(lcd_text_write, 0x01, false).await?;
Timer::after_millis(2).await;
self.write_byte(lcd_text_write, 0x06, false).await?;
Ok(())
}
pub async fn write_frame<const MAX_CHARS: usize>(
&mut self,
lcd_text_write: &mut impl LcdTextWrite,
lcd_text_frame: &LcdTextFrame<MAX_CHARS>,
) -> Result<(), LcdTextError> {
self.clear(lcd_text_write).await?;
for row_index in 0..lcd_text_frame.height {
self.set_cursor(lcd_text_write, row_index, 0).await?;
for column_index in 0..lcd_text_frame.width {
self.write_byte(
lcd_text_write,
lcd_text_frame.cell(row_index, column_index),
true,
)
.await?;
}
}
Ok(())
}
#[expect(clippy::arithmetic_side_effects, reason = "Bit operations")]
async fn write_nibble(
&mut self,
lcd_text_write: &mut impl LcdTextWrite,
nibble: u8,
rs: bool,
) -> Result<(), LcdTextError> {
let rs_bit = if rs { LCD_RS } else { 0 };
let data = (nibble << 4) | LCD_BACKLIGHT | rs_bit;
lcd_text_write.write(self.address, data | LCD_ENABLE)?;
Timer::after_micros(1).await;
lcd_text_write.write(self.address, data)?;
Timer::after_micros(50).await;
Ok(())
}
async fn write_byte(
&mut self,
lcd_text_write: &mut impl LcdTextWrite,
byte: u8,
rs: bool,
) -> Result<(), LcdTextError> {
self.write_nibble(lcd_text_write, (byte >> 4) & 0x0F, rs)
.await?;
self.write_nibble(lcd_text_write, byte & 0x0F, rs).await?;
Ok(())
}
async fn clear(&mut self, lcd_text_write: &mut impl LcdTextWrite) -> Result<(), LcdTextError> {
self.write_byte(lcd_text_write, 0x01, false).await?;
Timer::after_millis(2).await;
Ok(())
}
#[expect(
clippy::arithmetic_side_effects,
reason = "Row/column values are small"
)]
async fn set_cursor(
&mut self,
lcd_text_write: &mut impl LcdTextWrite,
row: usize,
col: u8,
) -> Result<(), LcdTextError> {
let address = match row {
0 => col,
1 => 0x40_u8 + col,
2 => 0x14_u8 + col,
3 => 0x54_u8 + col,
_ => return Err(LcdTextError::RowOutOfBounds { row }),
};
self.write_byte(lcd_text_write, 0x80 | address, false)
.await?;
Ok(())
}
}