From 52f1b65e300e8635f8a8a35b4b7fdfbf9a7876c8 Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Sat, 1 Aug 2026 21:31:04 +0200 Subject: [PATCH] WIP: Adapt to new screen TODO: Make better use of the bigger screen TODO: Show main loop HB different from GNSS updates? --- Readme.md | 2 +- src/main.rs | 6 +- src/screen.rs | 162 ++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 134 insertions(+), 36 deletions(-) diff --git a/Readme.md b/Readme.md index 2ae3aab..03f74f9 100644 --- a/Readme.md +++ b/Readme.md @@ -8,7 +8,7 @@ The code is currently tailored to: - U-Blox M10Q GNSS (connected on GPIO12 (RX), GPIO11 (TX), and GPIO25 (reset)) or Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset)) only available with `gnss_ubx` or `gnss_l67k` feature (which is included in `spat` feature) -- Some ST7789 172*320px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), TODO GPIO0 (BL)) +- Some ST7789 240*280px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), GPIO0 (BL)) only available with `screen` feature (which is included in `spat` feature) ## Features diff --git a/src/main.rs b/src/main.rs index 67e181c..1ebfb06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,8 +127,8 @@ async fn main(spawner: Spawner) -> ! { let mut spi_buffer = [0_u8; 512]; #[cfg(feature = "screen")] let mut screen = { - // 172*320px in landscape orientation - let (display, width, height) = screen::make_small_display( + // 240*280px in portrait orientation + let (display, size) = screen::make_large_display( peripherals.GPIO8, peripherals.GPIO10, peripherals.GPIO7, @@ -140,7 +140,7 @@ async fn main(spawner: Spawner) -> ! { ) .expect("Fatal error building screen"); - let mut screen = screen::Handler::new(display, width, height); + let mut screen = screen::Handler::new(display, size); let _ = screen .update(screen::ScreenState::Initial) diff --git a/src/screen.rs b/src/screen.rs index b2ddf90..a0968f7 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -5,13 +5,19 @@ use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text}; use crate::applogic; +pub struct DisplaySize { + width: u16, + height: u16, + offset_top: i32, + corner_radius: i32, +} + pub struct Handler where D: DrawTarget, { target: D, - width: u16, - height: u16, + size: DisplaySize, // internal state prev_state: ScreenState, @@ -82,11 +88,10 @@ where const SMALL_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> = mono_font::MonoTextStyle::new(&profont::PROFONT_14_POINT, Self::COLOR_FG); - pub fn new(target: D, width: u16, height: u16) -> Self { + pub fn new(target: D, display_size: DisplaySize) -> Self { Self { target, - width, - height, + size: display_size, prev_state: ScreenState::Uninitialized, hb_state: false, } @@ -115,18 +120,19 @@ where // Update indicator/ heartbeat { let update_ind_dia = 10; - let circle_x_pos = i32::from(self.width) - update_ind_dia - 10; + let circle_x_pos = + i32::from(self.size.width) - update_ind_dia - self.size.corner_radius; text::Text::with_alignment( "HB", - geometry::Point::new(circle_x_pos - 5, 10), + geometry::Point::new(circle_x_pos - 5, 10 + self.size.offset_top), Self::SMALL_TEXT_STYLE, text::Alignment::Right, ) .draw(&mut self.target)?; primitives::Circle::new( - geometry::Point::new(circle_x_pos, 2), + geometry::Point::new(circle_x_pos, 2 + self.size.offset_top), update_ind_dia.cast_unsigned(), ) .into_styled(if self.hb_state { @@ -149,7 +155,7 @@ where { primitives::Rectangle::new( geometry::Point::new(0, 0), - geometry::Size::new(self.width.into(), self.height.into()), + geometry::Size::new(self.size.width.into(), self.size.height.into()), ) .into_styled(Self::BK_STYLE) .draw(&mut self.target)?; @@ -171,7 +177,7 @@ where text::Text::with_alignment( msg, - geometry::Point::new(i32::from(self.width) / 2, 40), + geometry::Point::new(i32::from(self.size.width) / 2, 40 + self.size.offset_top), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -186,8 +192,8 @@ where self.clear()?; text::Text::with_alignment( - "No upcoming SPAT", - geometry::Point::new(i32::from(self.width) / 2, 130), + "Searching...", + geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -209,7 +215,7 @@ where text::Text::with_alignment( &text, - geometry::Point::new(i32::from(self.width) / 2, 130), + geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -235,23 +241,28 @@ where // only clear maneuver and timing area otherwise primitives::Rectangle::new( - geometry::Point::new(0, 105), - geometry::Size::new(self.width.into(), 67), + geometry::Point::new(0, 105 + self.size.offset_top), + geometry::Size::new(self.size.width.into(), 67), ) .into_styled(Self::BK_STYLE) .draw(&mut self.target)?; } + // do nothing more when no signals + if num_signals == 0 { + return Ok(()); + } + // draw intersection ID text::Text::new( &alloc::format!("#{}", data.intersection_id), - geometry::Point::new(15, 10), + geometry::Point::new(self.size.corner_radius + 5, 10 + self.size.offset_top), Self::SMALL_TEXT_STYLE, ) .draw(&mut self.target)?; #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - let sig_width = i32::from(self.width) / (num_signals as i32); + let sig_width = i32::from(self.size.width) / (num_signals as i32); for (idx, sig) in data.signal_groups.iter().enumerate() { #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] @@ -269,7 +280,7 @@ where applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE, }; primitives::Circle::new( - geometry::Point::new(offset + circle_margin, 15), + geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top), circle_dia.cast_unsigned(), ) .into_styled(style) @@ -280,7 +291,7 @@ where // draw maneuver text::Text::with_alignment( &maneuver_to_str(sig.maneuver), - geometry::Point::new(centerline, 130), + geometry::Point::new(centerline, 130 + self.size.offset_top), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -290,7 +301,7 @@ where if let Some(end_sec) = sig.end_sec { text::Text::with_alignment( &alloc::format!("{end_sec}s"), - geometry::Point::new(centerline, 160), + geometry::Point::new(centerline, 160 + self.size.offset_top), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -307,7 +318,7 @@ where self.clear()?; - let sig_width = i32::from(self.width) / 3; + let sig_width = i32::from(self.size.width) / 3; let circle_dia = 50; let circle_margin = (sig_width - circle_dia) / 2; @@ -319,7 +330,7 @@ where let offset = (idx as i32) * sig_width; primitives::Circle::new( - geometry::Point::new(offset + circle_margin, 15), + geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top), circle_dia.cast_unsigned(), ) .into_styled(*style) @@ -327,10 +338,10 @@ where } // draw welcome text - let centerline = i32::from(self.width) / 2; + let centerline = i32::from(self.size.width) / 2; text::Text::with_alignment( - "C-ITS Signal Phase Display\nWaiting for GNSS fix...", - geometry::Point::new(centerline, 130), + "C-ITS Signal Phase\nWaiting for GNSS...", + geometry::Point::new(centerline, 130 + self.size.offset_top), text_style, text::Alignment::Center, ) @@ -378,8 +389,7 @@ pub fn make_small_display( mipidsi::models::ST7789, esp_hal::gpio::Output<'_>, >, - u16, - u16, + DisplaySize, ), alloc::string::String, > @@ -396,10 +406,98 @@ where esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static, Spi: esp_hal::spi::master::Instance + 'static, { - const DISPLAY_WIDTH: u16 = 320; - const DISPLAY_HEIGHT: u16 = 172; const DISPLAY_HIDDEN_X: u16 = 34; // somehow this display has 34 px of invisible space on the left (in native orientation) + let size = DisplaySize { + width: 320, + height: 172, + offset_top: 0, + corner_radius: 10, + }; + + let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi) + .map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?; + + let output_config = esp_hal::gpio::OutputConfig::default(); + let dc_output = esp_hal::gpio::Output::new(dc, esp_hal::gpio::Level::Low, output_config); + let rst_output = esp_hal::gpio::Output::new(res, esp_hal::gpio::Level::High, output_config); + let _ = esp_hal::gpio::Output::new(bl, esp_hal::gpio::Level::High, output_config); // enable backlight + + let mut display_delay = esp_hal::delay::Delay::new(); + + let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs) + .map_err(|err| alloc::format!("Failed to initialize SPI device: {err}"))?; + + // Note: height and width are swapped b/c of 90° display rotation + let display = mipidsi::Builder::new( + mipidsi::models::ST7789, + mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer), + ) + .display_size(size.height + DISPLAY_HIDDEN_X, size.width) + .reset_pin(rst_output) + .invert_colors(mipidsi::options::ColorInversion::Inverted) + .orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90)) + .init(&mut display_delay) + .map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?; + + Ok((display, size)) +} + +/// Creates an [`mipidsi::Display`], width, height tuple for an ST7789 240*280px screen in portrait orientation +/// +/// # Errors +/// Human-readable fatal errors +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +pub fn make_large_display( + scl: IoScl, + sda: IoSda, + res: IoRes, + dc: IoDc, + cs: IoCs, + bl: IoBl, + spi: Spi, + spi_buffer: &mut [u8], +) -> Result< + ( + mipidsi::Display< + mipidsi::interface::SpiInterface< + '_, + embedded_hal_bus::spi::ExclusiveDevice< + esp_hal::spi::master::Spi<'_, esp_hal::Blocking>, + esp_hal::gpio::Output<'_>, + embedded_hal_bus::spi::NoDelay, + >, + esp_hal::gpio::Output<'_>, + >, + mipidsi::models::ST7789, + esp_hal::gpio::Output<'_>, + >, + DisplaySize, + ), + alloc::string::String, +> +where + IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>, + IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>, + IoRes: + esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static, + IoDc: + esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static, + IoCs: + esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static, + IoBl: + esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static, + Spi: esp_hal::spi::master::Instance + 'static, +{ + const DISPLAY_HIDDEN_Y: u16 = 20; // somehow this display has 20 px of invisible space on the top (in native orientation) + + let size = DisplaySize { + width: 240, + height: 280 + DISPLAY_HIDDEN_Y, + offset_top: DISPLAY_HIDDEN_Y.into(), + corner_radius: 30, + }; + let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi) .map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?; @@ -417,14 +515,14 @@ where mipidsi::models::ST7789, mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer), ) - .display_size(DISPLAY_HEIGHT + DISPLAY_HIDDEN_X, DISPLAY_WIDTH) + .display_size(size.width, size.height + DISPLAY_HIDDEN_Y) .reset_pin(rst_output) .invert_colors(mipidsi::options::ColorInversion::Inverted) - .orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90)) + .orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg0)) .init(&mut display_delay) .map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?; - Ok((display, DISPLAY_WIDTH, DISPLAY_HEIGHT)) + Ok((display, size)) } /// Builds the SPI interface for the ST7789 display