Watch
0
0
Fork
You've already forked esp32-c_its-companion
0

Adapt to new screen, add more GNSS and timing info

This commit is contained in:
Jannik Beyerstedt 2026-08-01 21:31:04 +02:00
commit 1a5012dec5
4 changed files with 299 additions and 68 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

@ -106,7 +106,51 @@ pub struct GlosaSignalInfo {
pub struct SignalGroupData { pub struct SignalGroupData {
pub phase: SignalPhase, pub phase: SignalPhase,
pub maneuver: map::Maneuvers, pub maneuver: map::Maneuvers,
pub end_sec: Option<i16>, pub timing: Option<TimingData>,
}
#[cfg(feature = "spat")]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[allow(clippy::struct_field_names)]
pub struct TimingData {
pub min_end_sec: i16,
pub likely_end_sec: Option<i16>,
pub max_end_sec: Option<i16>,
}
impl TimingData {
pub fn new(value: &map::SignalGroup, now: chrono::DateTime<chrono::Utc>) -> Option<Self> {
let min = value
.min_end_time()
.map(|val| Self::time_to_duration_sec(now, val));
let likely_end_sec = value
.likely_time()
.map(|val| Self::time_to_duration_sec(now, val));
let max_end_sec = value
.max_end_time()
.map(|val| Self::time_to_duration_sec(now, val));
min.map(|min_end_sec| Self {
min_end_sec,
likely_end_sec,
max_end_sec,
})
}
fn time_to_duration_sec(
now: chrono::DateTime<chrono::Utc>,
time_point: chrono::DateTime<chrono::Utc>,
) -> i16 {
use core::ops::Sub as _;
use num_traits::Float as _;
let dur = time_point.sub(now).as_seconds_f32();
#[allow(clippy::cast_possible_truncation)]
let duration_sec = dur.round() as i16;
duration_sec
}
} }
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
@ -357,33 +401,17 @@ impl State {
let sig_grps = approach_sig_grps let sig_grps = approach_sig_grps
.iter() .iter()
.filter_map(|sig| { .filter_map(|sig| {
if let Some(manv) = sig.maneuvers() { if let Some(maneuver) = sig.maneuvers() {
use core::ops::Sub;
use num_traits::Float;
let phase = sig let phase = sig
.phase() .phase()
.map(core::convert::Into::into) .map(core::convert::Into::into)
.unwrap_or_default(); .unwrap_or_default();
let timing = TimingData::new(sig, self.time.and_utc());
let end_sec = sig
.likely_time()
.or_else(|| sig.min_end_time())
.map(|time| {
let dur =
time.sub(self.time.and_utc()).as_seconds_f32();
#[allow(clippy::cast_possible_truncation)]
let end_sec = dur.round() as i16;
end_sec
});
Some(SignalGroupData { Some(SignalGroupData {
phase, phase,
maneuver: manv, maneuver,
end_sec, timing,
}) })
} else { } else {
None None

View file

@ -154,8 +154,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,
@ -167,7 +167,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)
@ -381,6 +381,15 @@ async fn main(spawner: Spawner) -> ! {
} }
Err(err) => warn!("{err}"), Err(err) => warn!("{err}"),
} }
// update GNSS HB on screen
let _ = screen.update_gnss(pvt);
}
ublox::proto27::PacketRef::AckNak(nak) => {
error!("UBX: {nak:?}");
}
ublox::proto27::PacketRef::AckAck(ack) => {
info!("UBX: {ack:?}");
} }
_ => { _ => {
println!("UBX: {packet_ref:?}"); println!("UBX: {packet_ref:?}");
@ -460,8 +469,9 @@ async fn main(spawner: Spawner) -> ! {
if let applogic::GlosaOutput::Locked(data) = &glosa_data { if let applogic::GlosaOutput::Locked(data) = &glosa_data {
for sig in &data.signal_groups { for sig in &data.signal_groups {
let duration_str = sig let duration_str = sig
.end_sec .timing
.map(|v| alloc::format!("for {v} s")) .as_ref()
.map(|v| alloc::format!("for {} s", v.min_end_sec))
.unwrap_or_default(); .unwrap_or_default();
println!("GLOSA: {} {} {duration_str}", sig.maneuver, sig.phase); println!("GLOSA: {} {} {duration_str}", sig.maneuver, sig.phase);
} }

View file

@ -5,17 +5,24 @@ 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,
hb_state: bool, hb_state: bool,
gnss_hb_state: bool,
} }
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq)]
@ -79,16 +86,20 @@ where
const DEFAULT_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> = const DEFAULT_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
mono_font::MonoTextStyle::new(&profont::PROFONT_24_POINT, Self::COLOR_FG); mono_font::MonoTextStyle::new(&profont::PROFONT_24_POINT, Self::COLOR_FG);
const MID_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
mono_font::MonoTextStyle::new(&profont::PROFONT_18_POINT, Self::COLOR_FG);
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 { const BIG_HEIGHT_THLD: u16 = 172;
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,
gnss_hb_state: false,
} }
} }
@ -115,18 +126,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 {
@ -143,13 +155,67 @@ where
Ok(()) Ok(())
} }
// on big screns: uses full screen width and height from 250 to 280 pixels
pub fn update_gnss(
&mut self,
pvt: &ublox::nav_pvt::proto27::NavPvtRef,
) -> Result<(), D::Error> {
use alloc::string::ToString as _;
// this only applies to big screens
if !self.is_big_height() {
return Ok(());
}
let num_sat = pvt.num_satellites();
let fix_str = match pvt.fix_type() {
ublox::GnssFixType::NoFix
| ublox::GnssFixType::DeadReckoningOnly
| ublox::GnssFixType::TimeOnlyFix => {
alloc::format!("{num_sat:2} sat")
}
ublox::GnssFixType::Fix2D => "2D fix".to_string(),
ublox::GnssFixType::Fix3D | ublox::GnssFixType::GPSPlusDeadReckoning => {
"3D fix".to_string()
}
_ => " ".to_string(),
};
// clear area and write information
// note: text position is x baseline, not top left corner
primitives::Rectangle::new(
geometry::Point::new(0, 250 + self.size.offset_top),
geometry::Size::new(self.size.width.into(), 20),
)
.into_styled(Self::BK_STYLE)
.draw(&mut self.target)?;
let colon = if self.gnss_hb_state { ":" } else { " " };
let message = alloc::format!(
"{fix_str}{colon} {:2.0}km/h, {:3.0}°",
pvt.ground_speed_2d() * 3.6,
pvt.heading_motion()
);
text::Text::with_alignment(
&message,
geometry::Point::new(self.size.corner_radius / 2, 265 + self.size.offset_top),
Self::SMALL_TEXT_STYLE,
text::Alignment::Left,
)
.draw(&mut self.target)?;
self.gnss_hb_state = !self.gnss_hb_state;
Ok(())
}
fn clear(&mut self) -> Result<(), D::Error> fn clear(&mut self) -> Result<(), D::Error>
where where
D: DrawTarget<Color = pixelcolor::Rgb565>, D: DrawTarget<Color = pixelcolor::Rgb565>,
{ {
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)?;
@ -157,6 +223,10 @@ where
Ok(()) Ok(())
} }
fn is_big_height(&self) -> bool {
self.size.height > Self::BIG_HEIGHT_THLD
}
#[allow(unused)] #[allow(unused)]
pub fn test_img(&mut self) -> Result<(), D::Error> { pub fn test_img(&mut self) -> Result<(), D::Error> {
mipidsi::TestImage::new().draw(&mut self.target)?; mipidsi::TestImage::new().draw(&mut self.target)?;
@ -171,7 +241,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 +256,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 +279,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,
) )
@ -218,6 +288,9 @@ where
Ok(()) Ok(())
} }
// uses full screen width and height
// - from 0 to 105+67 = 172 pixels for small screens
// - from 0 to 105+67+20 = 192 pixels for big screens
#[allow(unused)] #[allow(unused)]
fn draw_glosa( fn draw_glosa(
&mut self, &mut self,
@ -226,6 +299,8 @@ where
) -> Result<(), D::Error> { ) -> Result<(), D::Error> {
let num_signals = data.signal_groups.len(); let num_signals = data.signal_groups.len();
let is_big_height = self.is_big_height();
if let Some(prev) = prev_data if let Some(prev) = prev_data
&& prev.signal_groups.len() != num_signals && prev.signal_groups.len() != num_signals
{ {
@ -233,25 +308,30 @@ where
self.clear(); self.clear();
} else { } else {
// only clear maneuver and timing area otherwise // only clear maneuver and timing area otherwise
let sig_total_height = 67 + if is_big_height { 20 } else { 0 };
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(), sig_total_height),
) )
.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 +349,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,24 +360,50 @@ 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,
) )
.draw(&mut self.target)?; .draw(&mut self.target)?;
// draw timing indicator // draw timing indicator(s)
if let Some(end_sec) = sig.end_sec { if let Some(timing) = &sig.timing {
text::Text::with_alignment( if let (Some(likely), Some(max)) = (timing.likely_end_sec, timing.max_end_sec) {
&alloc::format!("{end_sec}s"), text::Text::with_alignment(
geometry::Point::new(centerline, 160), &alloc::format!("{likely}s"),
Self::DEFAULT_TEXT_STYLE, geometry::Point::new(centerline, 160 + self.size.offset_top),
text::Alignment::Center, Self::DEFAULT_TEXT_STYLE,
) text::Alignment::Center,
.draw(&mut self.target)?; )
.draw(&mut self.target)?;
// only for big screen
if is_big_height {
// TODO: show as diff to likely time?
text::Text::with_alignment(
&alloc::format!("{}-{max}", timing.min_end_sec),
geometry::Point::new(centerline, 185 + self.size.offset_top),
Self::MID_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
}
} else {
text::Text::with_alignment(
&alloc::format!("{}s", timing.min_end_sec),
geometry::Point::new(centerline, 160 + self.size.offset_top),
Self::DEFAULT_TEXT_STYLE,
text::Alignment::Center,
)
.draw(&mut self.target)?;
}
} }
} }
// if is_big_height {
// // TODO: Add ETA
// }
Ok(()) Ok(())
} }
@ -307,7 +413,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 +425,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 +433,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,
) )
@ -353,7 +459,7 @@ fn maneuver_to_str(value: applogic::v2x::map::Maneuvers) -> alloc::string::Strin
/// ///
/// # Errors /// # Errors
/// Human-readable fatal errors /// Human-readable fatal errors
#[allow(clippy::too_many_arguments, clippy::type_complexity)] #[allow(unused, clippy::too_many_arguments, clippy::type_complexity)]
pub fn make_small_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>( pub fn make_small_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
scl: IoScl, scl: IoScl,
sda: IoSda, sda: IoSda,
@ -378,8 +484,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 +501,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(unused, 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 +610,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