Created
May 5, 2025 18:07
-
-
Save bagbag/18699b49e5c1c8427eb9fdaa9aa288de to your computer and use it in GitHub Desktop.
mipidsi async usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use core::{ | |
| fmt::{self, Write}, | |
| str::FromStr, | |
| }; | |
| use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice; | |
| use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel}; | |
| use embassy_time::{Delay, Duration, Instant}; | |
| use embedded_graphics::{ | |
| Drawable, | |
| mono_font::{MonoFont, MonoTextStyle, iso_8859_1::FONT_10X20}, | |
| pixelcolor::Rgb565, | |
| prelude::{Point, RgbColor, Size}, | |
| primitives::Rectangle, | |
| text::{Baseline, Text}, | |
| }; | |
| use embedded_graphics_framebuf::FrameBuf; | |
| use esp_hal::{Async, gpio::Output, spi::master::SpiDmaBus}; | |
| use heapless::String; | |
| use mipidsi::{ | |
| Builder, Display, | |
| interface::SpiInterface, | |
| models::ST7789, | |
| options::{ColorInversion, Orientation}, | |
| }; | |
| use crate::trace::TaskStats; | |
| pub enum DisplayCommand { | |
| RenderPendingTaskStats(TaskStats), | |
| RenderRunningTaskStats(TaskStats), | |
| RenderInstant(Instant), | |
| RenderTemperature(i8), | |
| RenderEvSteps(f32), | |
| RenderWiFiState(String<32>), | |
| RenderNetworkStatus(String<32>), | |
| RenderWiFiSignal(Option<i8>), | |
| } | |
| pub static DISPLAY_COMMAND_CHANNEL: Channel<CriticalSectionRawMutex, DisplayCommand, 10> = Channel::new(); | |
| pub static FONT: &MonoFont<'_> = &FONT_10X20; | |
| pub static PADDING: i32 = 0; | |
| pub static WHITE: Rgb565 = Rgb565::WHITE; | |
| pub static BLACK: Rgb565 = Rgb565::BLACK; | |
| const FONT_WIDTH: i32 = FONT.character_size.width as i32; | |
| const FONT_HEIGHT: i32 = FONT.character_size.height as i32; | |
| const CHAR_WIDTH: u32 = 10; | |
| const CHAR_HEIGHT: u32 = 20; | |
| const DISPLAY_WIDTH: i32 = 320; | |
| const DISPLAY_HEIGHT: i32 = 240; | |
| type Driver<'a, SPI, M> = Display<SpiInterface<'a, SPI, Output<'a>>, M, Output<'a>>; | |
| pub struct HeishamodDisplay<'a, SPI> | |
| where | |
| SPI: embedded_hal_async::spi::SpiDevice, | |
| { | |
| driver: Display<SpiInterface<'a, SPI, Output<'a>>, ST7789, Output<'a>>, | |
| background_color: Rgb565, | |
| } | |
| impl<'a, SPI> HeishamodDisplay<'a, SPI> | |
| where | |
| SPI: embedded_hal_async::spi::SpiDevice, | |
| { | |
| pub async fn init(spi_device: SPI, dc: Output<'a>, rst: Output<'a>, buffer: &'a mut [u8], background_color: Rgb565) -> Self | |
| where | |
| SPI: embedded_hal_async::spi::SpiDevice, | |
| { | |
| let interface = SpiInterface::new(spi_device, dc, buffer); | |
| let driver = Builder::new(ST7789, interface) | |
| .reset_pin(rst) | |
| .invert_colors(ColorInversion::Inverted) | |
| .orientation(Orientation::new().rotate(mipidsi::options::Rotation::Deg90)) | |
| .init(&mut Delay) | |
| .await | |
| .unwrap(); | |
| return HeishamodDisplay { driver, background_color }; | |
| } | |
| } | |
| fn format_duration(duration: Duration) -> (u32, String<2>) { | |
| let mut value = duration.as_micros(); | |
| let mut unit = String::from_str("us").unwrap(); | |
| if value >= 10_000 { | |
| value /= 1_000; | |
| unit.clear(); | |
| unit.push_str("ms").unwrap(); | |
| } | |
| (value as u32, unit) | |
| } | |
| #[embassy_executor::task] | |
| pub async fn display_task( | |
| mut display: HeishamodDisplay<'static, SpiDevice<'static, CriticalSectionRawMutex, SpiDmaBus<'static, Async>, Output<'static>>>, | |
| ) { | |
| let pixels = (0..(DISPLAY_WIDTH * DISPLAY_HEIGHT)).map(|_| Rgb565::BLACK); | |
| display | |
| .driver | |
| .set_pixels(0, 0, (DISPLAY_WIDTH - 1) as u16, (DISPLAY_HEIGHT - 1) as u16, pixels) | |
| .await | |
| .unwrap(); | |
| let mut instant_text = TextRenderItem::<12>::new(0, 0, Alignment::TopLeft, FONT, WHITE, display.background_color); | |
| let mut temperature_text = TextRenderItem::<6>::new(0, 0, Alignment::TopRight, FONT, WHITE, display.background_color); | |
| let mut wifi_state_text = TextRenderItem::<14>::new(5, 2, Alignment::BottomLeft, FONT, WHITE, display.background_color); | |
| let mut wifi_signal_text = TextRenderItem::<10>::new(0, 1, Alignment::BottomLeft, FONT, WHITE, display.background_color); | |
| let mut network_status_text = TextRenderItem::<20>::new(0, 0, Alignment::BottomLeft, FONT, WHITE, display.background_color); | |
| let mut ev_steps_text = TextRenderItem::<10>::new(4, 4, Alignment::TopLeft, FONT, WHITE, display.background_color); | |
| let mut task_pending_avg = TextRenderItem::<8>::new(0, 2, Alignment::TopRight, FONT, WHITE, display.background_color); | |
| let mut task_pending_max = TextRenderItem::<8>::new(0, 3, Alignment::TopRight, FONT, WHITE, display.background_color); | |
| let mut task_running_avg = TextRenderItem::<8>::new(0, 4, Alignment::TopRight, FONT, WHITE, display.background_color); | |
| let mut task_running_max = TextRenderItem::<8>::new(0, 5, Alignment::TopRight, FONT, WHITE, display.background_color); | |
| TextRenderItem::<4>::new(0, 4, Alignment::TopLeft, FONT, WHITE, display.background_color) | |
| .update("EV: ", &mut display.driver) | |
| .await | |
| .unwrap(); | |
| TextRenderItem::<7>::new(0, 2, Alignment::BottomLeft, FONT, WHITE, display.background_color) | |
| .update("Net:", &mut display.driver) | |
| .await | |
| .unwrap(); | |
| loop { | |
| let command = DISPLAY_COMMAND_CHANNEL.receive().await; | |
| match command { | |
| DisplayCommand::RenderInstant(instant) => { | |
| format_time(&mut instant_text, instant); | |
| instant_text.render(&mut display.driver).await; | |
| } | |
| DisplayCommand::RenderPendingTaskStats(stats) => { | |
| let (avg, avg_unit) = format_duration(stats.into_avg()); | |
| let (max, max_unit) = format_duration(stats.max); | |
| fmt::write(&mut task_pending_avg, format_args!("P:{:#4}{}", avg, avg_unit)).unwrap(); | |
| fmt::write(&mut task_pending_max, format_args!("P:{:#4}{}", max, max_unit)).unwrap(); | |
| task_pending_avg.render(&mut display.driver).await; | |
| task_pending_max.render(&mut display.driver).await; | |
| } | |
| DisplayCommand::RenderRunningTaskStats(stats) => { | |
| let (avg, avg_unit) = format_duration(stats.into_avg()); | |
| let (max, max_unit) = format_duration(stats.max); | |
| fmt::write(&mut task_running_avg, format_args!("R:{:#4}{}", avg, avg_unit)).unwrap(); | |
| fmt::write(&mut task_running_max, format_args!("R:{:#4}{}", max, max_unit)).unwrap(); | |
| task_running_avg.render(&mut display.driver).await; | |
| task_running_max.render(&mut display.driver).await; | |
| } | |
| DisplayCommand::RenderTemperature(temperature) => { | |
| fmt::write(&mut temperature_text, format_args!("{:>4} C", temperature)).unwrap(); | |
| temperature_text.render(&mut display.driver).await; | |
| } | |
| DisplayCommand::RenderEvSteps(steps) => { | |
| fmt::write(&mut ev_steps_text, format_args!("{:}", steps)).unwrap(); | |
| ev_steps_text.render(&mut display.driver).await; | |
| } | |
| DisplayCommand::RenderWiFiState(state) => { | |
| wifi_state_text.update(state.as_str(), &mut display.driver).await.unwrap(); | |
| } | |
| DisplayCommand::RenderNetworkStatus(status) => { | |
| network_status_text.update(status.as_str(), &mut display.driver).await.unwrap(); | |
| } | |
| DisplayCommand::RenderWiFiSignal(signal) => { | |
| if let Some(signal) = signal { | |
| fmt::write(&mut wifi_signal_text, format_args!("{} dBm", signal)).unwrap(); | |
| } else { | |
| wifi_signal_text.set("-").unwrap(); | |
| } | |
| wifi_signal_text.render(&mut display.driver).await; | |
| } | |
| } | |
| } | |
| } | |
| pub struct TextRenderItem<'a, const N: usize> { | |
| last_text: String<N>, | |
| next_text: String<N>, | |
| position: Point, | |
| style: MonoTextStyle<'a, Rgb565>, | |
| background_color: Rgb565, | |
| } | |
| impl<'a, const N: usize> TextRenderItem<'a, N> { | |
| pub fn new(x: i32, y: i32, alignment: Alignment, font: &'a MonoFont<'a>, color: Rgb565, background_color: Rgb565) -> Self { | |
| TextRenderItem { | |
| last_text: String::new(), | |
| next_text: String::new(), | |
| position: get_text_point_align(x, y, N as i32, &alignment), | |
| style: MonoTextStyle::new(font, color), | |
| background_color, | |
| } | |
| } | |
| #[inline] | |
| pub fn set(&mut self, text: &str) -> Result<(), ()> { | |
| self.next_text.clear(); | |
| self.next_text.push_str(text) | |
| } | |
| #[inline] | |
| pub async fn update<SPI>(&mut self, text: &str, target: &mut Driver<'_, SPI, ST7789>) -> Result<(), ()> | |
| where | |
| SPI: embedded_hal_async::spi::SpiDevice, | |
| { | |
| self.set(text)?; | |
| self.render(target).await; | |
| Ok(()) | |
| } | |
| pub async fn render<SPI>(&mut self, target: &mut Driver<'_, SPI, ST7789>) | |
| where | |
| SPI: embedded_hal_async::spi::SpiDevice, | |
| { | |
| const BUF_SIZE: usize = (CHAR_WIDTH * CHAR_HEIGHT) as usize; | |
| const POINT_ZERO: Point = Point::zero(); | |
| const AREA_SIZE: Size = Size::new(CHAR_WIDTH, CHAR_HEIGHT); | |
| let mut last_char_iter = self.last_text.chars(); | |
| let mut next_char_iter = self.next_text.chars(); | |
| let mut char_str_buf = [0u8; 4]; | |
| let mut char_index = 0; | |
| loop { | |
| let last_char = last_char_iter.next(); | |
| let next_char = next_char_iter.next(); | |
| if last_char.is_none() && next_char.is_none() { | |
| break; | |
| } | |
| if last_char != next_char { | |
| let mut data: [Rgb565; BUF_SIZE] = [self.background_color; BUF_SIZE]; | |
| let mut framebuffer = FrameBuf::new(&mut data, CHAR_WIDTH as usize, CHAR_HEIGHT as usize); | |
| if let Some(c) = next_char { | |
| let char_str = c.encode_utf8(&mut char_str_buf); | |
| Text::with_baseline(char_str, POINT_ZERO, self.style, Baseline::Top) | |
| .draw(&mut framebuffer) | |
| .ok(); | |
| } | |
| let area = Rectangle::new(Point::new(self.position.x + (char_index * CHAR_WIDTH) as i32, self.position.y), AREA_SIZE); | |
| let sx = area.top_left.x as u16; | |
| let sy = area.top_left.y as u16; | |
| let ex = (area.top_left.x as u32 + area.size.width - 1) as u16; | |
| let ey = (area.top_left.y as u32 + area.size.height - 1) as u16; | |
| target.set_pixels(sx, sy, ex, ey, data).await.unwrap(); | |
| } | |
| char_index += 1; | |
| } | |
| self.last_text = self.next_text.clone(); | |
| self.next_text.clear(); | |
| } | |
| } | |
| impl<'a, const N: usize> fmt::Write for TextRenderItem<'a, N> { | |
| fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { | |
| self.next_text.push_str(s).map_err(|_| fmt::Error) | |
| } | |
| fn write_char(&mut self, c: char) -> Result<(), fmt::Error> { | |
| self.next_text.push(c).map_err(|_| fmt::Error) | |
| } | |
| } | |
| pub enum Alignment { | |
| TopLeft, | |
| TopRight, | |
| BottomLeft, | |
| BottomRight, | |
| } | |
| fn get_text_point_align(x: i32, y: i32, length: i32, alignment: &Alignment) -> Point { | |
| match alignment { | |
| Alignment::TopLeft => Point::new(PADDING + x * FONT_WIDTH, PADDING + y * (FONT_HEIGHT + PADDING)), | |
| Alignment::TopRight => Point::new( | |
| DISPLAY_WIDTH - PADDING - ((x + length) * FONT_WIDTH), | |
| PADDING + y * (FONT_HEIGHT + PADDING), | |
| ), | |
| Alignment::BottomLeft => Point::new(PADDING + x * FONT_WIDTH, DISPLAY_HEIGHT - PADDING - ((y + 1) * (FONT_HEIGHT + PADDING))), | |
| Alignment::BottomRight => Point::new( | |
| DISPLAY_WIDTH - PADDING - ((x + length) * FONT_WIDTH), | |
| DISPLAY_HEIGHT - PADDING - ((y + 1) * (FONT_HEIGHT + PADDING)), | |
| ), | |
| } | |
| } | |
| fn format_time(str: &mut dyn Write, instant: Instant) { | |
| let total_seconds = instant.as_secs(); | |
| let hours = total_seconds / 3600; | |
| let minutes = (total_seconds % 3600) / 60; | |
| let seconds = total_seconds % 60; | |
| let milliseconds = instant.as_millis() % 1_000; | |
| // let microseconds = instant.as_micros() % 1_000_000; | |
| fmt::write(str, format_args!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, milliseconds)).unwrap(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment