Skip to content

Instantly share code, notes, and snippets.

@bagbag
Created May 5, 2025 18:07
Show Gist options
  • Select an option

  • Save bagbag/18699b49e5c1c8427eb9fdaa9aa288de to your computer and use it in GitHub Desktop.

Select an option

Save bagbag/18699b49e5c1c8427eb9fdaa9aa288de to your computer and use it in GitHub Desktop.
mipidsi async usage
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