0
0
Fork 0

Compare commits

...

1 commit

Author SHA1 Message Date
52f1b65e30 WIP: Adapt to new screen
TODO: Make better use of the bigger screen
TODO: Show main loop HB different from GNSS updates?
2026-08-02 22:40:25 +02:00
3 changed files with 133 additions and 35 deletions

View file

@ -8,7 +8,7 @@ The code is currently tailored to:
- U-Blox M10Q GNSS (connected on GPIO12 (RX), GPIO11 (TX), and GPIO25 (reset)) - 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)) 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) 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) only available with `screen` feature (which is included in `spat` feature)
## Features ## Features

View file

@ -127,8 +127,8 @@ async fn main(spawner: Spawner) -> ! {
let mut spi_buffer = [0_u8; 512]; let mut spi_buffer = [0_u8; 512];
#[cfg(feature = "screen")] #[cfg(feature = "screen")]
let mut screen = { let mut screen = {
// 172*320px in landscape orientation // 240*280px in portrait orientation
let (display, width, height) = screen::make_small_display( let (display, size) = screen::make_large_display(
peripherals.GPIO8, peripherals.GPIO8,
peripherals.GPIO10, peripherals.GPIO10,
peripherals.GPIO7, peripherals.GPIO7,
@ -140,7 +140,7 @@ async fn main(spawner: Spawner) -> ! {
) )
.expect("Fatal error building screen"); .expect("Fatal error building screen");
let mut screen = screen::Handler::new(display, width, height); let mut screen = screen::Handler::new(display, size);
let _ = screen let _ = screen
.update(screen::ScreenState::Initial) .update(screen::ScreenState::Initial)

View file

@ -5,13 +5,19 @@ use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text};
use crate::applogic; use crate::applogic;
pub struct DisplaySize {
width: u16,
height: u16,
offset_top: i32,
corner_radius: i32,
}
pub struct Handler<D> pub struct Handler<D>
where where
D: DrawTarget<Color = pixelcolor::Rgb565>, D: DrawTarget<Color = pixelcolor::Rgb565>,
{ {
target: D, target: D,
width: u16, size: DisplaySize,
height: u16,
// internal state // internal state
prev_state: ScreenState, prev_state: ScreenState,
@ -82,11 +88,10 @@ where
const SMALL_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> = const SMALL_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
mono_font::MonoTextStyle::new(&profont::PROFONT_14_POINT, Self::COLOR_FG); 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 { Self {
target, target,
width, size: display_size,
height,
prev_state: ScreenState::Uninitialized, prev_state: ScreenState::Uninitialized,
hb_state: false, hb_state: false,
} }
@ -115,18 +120,19 @@ where
// Update indicator/ heartbeat // Update indicator/ heartbeat
{ {
let update_ind_dia = 10; 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( text::Text::with_alignment(
"HB", "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, Self::SMALL_TEXT_STYLE,
text::Alignment::Right, text::Alignment::Right,
) )
.draw(&mut self.target)?; .draw(&mut self.target)?;
primitives::Circle::new( 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(), update_ind_dia.cast_unsigned(),
) )
.into_styled(if self.hb_state { .into_styled(if self.hb_state {
@ -149,7 +155,7 @@ where
{ {
primitives::Rectangle::new( primitives::Rectangle::new(
geometry::Point::new(0, 0), 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) .into_styled(Self::BK_STYLE)
.draw(&mut self.target)?; .draw(&mut self.target)?;
@ -171,7 +177,7 @@ where
text::Text::with_alignment( text::Text::with_alignment(
msg, 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, Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center, text::Alignment::Center,
) )
@ -186,8 +192,8 @@ where
self.clear()?; self.clear()?;
text::Text::with_alignment( text::Text::with_alignment(
"No upcoming SPAT", "Searching...",
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, Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center, text::Alignment::Center,
) )
@ -209,7 +215,7 @@ where
text::Text::with_alignment( text::Text::with_alignment(
&text, &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, Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center, text::Alignment::Center,
) )
@ -235,23 +241,28 @@ where
// only clear maneuver and timing area otherwise // only clear maneuver and timing area otherwise
primitives::Rectangle::new( primitives::Rectangle::new(
geometry::Point::new(0, 105), geometry::Point::new(0, 105 + self.size.offset_top),
geometry::Size::new(self.width.into(), 67), geometry::Size::new(self.size.width.into(), 67),
) )
.into_styled(Self::BK_STYLE) .into_styled(Self::BK_STYLE)
.draw(&mut self.target)?; .draw(&mut self.target)?;
} }
// do nothing more when no signals
if num_signals == 0 {
return Ok(());
}
// draw intersection ID // draw intersection ID
text::Text::new( text::Text::new(
&alloc::format!("#{}", data.intersection_id), &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, Self::SMALL_TEXT_STYLE,
) )
.draw(&mut self.target)?; .draw(&mut self.target)?;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[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() { for (idx, sig) in data.signal_groups.iter().enumerate() {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
@ -269,7 +280,7 @@ where
applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE, applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE,
}; };
primitives::Circle::new( 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(), circle_dia.cast_unsigned(),
) )
.into_styled(style) .into_styled(style)
@ -280,7 +291,7 @@ where
// draw maneuver // draw maneuver
text::Text::with_alignment( text::Text::with_alignment(
&maneuver_to_str(sig.maneuver), &maneuver_to_str(sig.maneuver),
geometry::Point::new(centerline, 130), geometry::Point::new(centerline, 130 + self.size.offset_top),
Self::DEFAULT_TEXT_STYLE, Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center, text::Alignment::Center,
) )
@ -290,7 +301,7 @@ where
if let Some(end_sec) = sig.end_sec { if let Some(end_sec) = sig.end_sec {
text::Text::with_alignment( text::Text::with_alignment(
&alloc::format!("{end_sec}s"), &alloc::format!("{end_sec}s"),
geometry::Point::new(centerline, 160), geometry::Point::new(centerline, 160 + self.size.offset_top),
Self::DEFAULT_TEXT_STYLE, Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center, text::Alignment::Center,
) )
@ -307,7 +318,7 @@ where
self.clear()?; 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_dia = 50;
let circle_margin = (sig_width - circle_dia) / 2; let circle_margin = (sig_width - circle_dia) / 2;
@ -319,7 +330,7 @@ where
let offset = (idx as i32) * sig_width; let offset = (idx as i32) * sig_width;
primitives::Circle::new( 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(), circle_dia.cast_unsigned(),
) )
.into_styled(*style) .into_styled(*style)
@ -327,10 +338,10 @@ where
} }
// draw welcome text // draw welcome text
let centerline = i32::from(self.width) / 2; let centerline = i32::from(self.size.width) / 2;
text::Text::with_alignment( text::Text::with_alignment(
"C-ITS Signal Phase Display\nWaiting for GNSS fix...", "C-ITS Signal Phase\nWaiting for GNSS...",
geometry::Point::new(centerline, 130), geometry::Point::new(centerline, 130 + self.size.offset_top),
text_style, text_style,
text::Alignment::Center, text::Alignment::Center,
) )
@ -378,8 +389,7 @@ pub fn make_small_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
mipidsi::models::ST7789, mipidsi::models::ST7789,
esp_hal::gpio::Output<'_>, esp_hal::gpio::Output<'_>,
>, >,
u16, DisplaySize,
u16,
), ),
alloc::string::String, alloc::string::String,
> >
@ -396,10 +406,98 @@ where
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static, esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
Spi: esp_hal::spi::master::Instance + '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) 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<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
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) let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi)
.map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?; .map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?;
@ -417,14 +515,14 @@ where
mipidsi::models::ST7789, mipidsi::models::ST7789,
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer), 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) .reset_pin(rst_output)
.invert_colors(mipidsi::options::ColorInversion::Inverted) .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) .init(&mut display_delay)
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?; .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 /// Builds the SPI interface for the ST7789 display