diff --git a/Readme.md b/Readme.md index 5fb34e6..c2ecbab 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 240*280px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), GPIO0 (BL)) +- Some ST7789 172*320px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), TODO GPIO0 (BL)) only available with `screen` feature (which is included in `spat` feature) ## Features diff --git a/src/applogic/mod.rs b/src/applogic/mod.rs index 55672a7..7e7ebc4 100644 --- a/src/applogic/mod.rs +++ b/src/applogic/mod.rs @@ -106,51 +106,7 @@ pub struct GlosaSignalInfo { pub struct SignalGroupData { pub phase: SignalPhase, pub maneuver: map::Maneuvers, - pub timing: Option, -} - -#[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, - pub max_end_sec: Option, -} - -impl TimingData { - pub fn new(value: &map::SignalGroup, now: chrono::DateTime) -> Option { - 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, - time_point: chrono::DateTime, - ) -> 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 - } + pub end_sec: Option, } #[cfg(feature = "spat")] @@ -401,17 +357,33 @@ impl State { let sig_grps = approach_sig_grps .iter() .filter_map(|sig| { - if let Some(maneuver) = sig.maneuvers() { + if let Some(manv) = sig.maneuvers() { + use core::ops::Sub; + + use num_traits::Float; + let phase = sig .phase() .map(core::convert::Into::into) .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 { phase, - maneuver, - timing, + maneuver: manv, + end_sec, }) } else { None diff --git a/src/applogic/v2x/map.rs b/src/applogic/v2x/map.rs index d7e2195..bac8a69 100644 --- a/src/applogic/v2x/map.rs +++ b/src/applogic/v2x/map.rs @@ -173,12 +173,6 @@ impl Intersection { { sig_grp.likely_time = Some(likely.to_datetime_from_moy(moy, year)); } - if let Some(max) = &timing.max_end_time - && !max.is_out_of_range() - && !max.is_unknown() - { - sig_grp.max_end_time = Some(max.to_datetime_from_moy(moy, year)); - } } } } else { @@ -229,7 +223,6 @@ impl Intersection { target_lanes, phase: None, min_end_time: None, - max_end_time: None, likely_time: None, } }) @@ -417,7 +410,7 @@ pub struct SignalGroup { /// current signal phase phase: Option, min_end_time: Option>, - max_end_time: Option>, + // max_end_time: Option>, likely_time: Option>, } @@ -450,9 +443,6 @@ impl SignalGroup { pub fn likely_time(&self) -> Option> { self.likely_time } - pub fn max_end_time(&self) -> Option> { - self.max_end_time - } pub fn phase_to_str(&self) -> Option { self.phase.map(|v| { diff --git a/src/main.rs b/src/main.rs index 13be5d6..02e4855 100644 --- a/src/main.rs +++ b/src/main.rs @@ -154,8 +154,8 @@ async fn main(spawner: Spawner) -> ! { let mut spi_buffer = [0_u8; 512]; #[cfg(feature = "screen")] let mut screen = { - // 240*280px in portrait orientation - let (display, size) = screen::make_large_display( + // 172*320px in landscape orientation + let (display, width, height) = screen::make_small_display( peripherals.GPIO8, peripherals.GPIO10, peripherals.GPIO7, @@ -167,7 +167,7 @@ async fn main(spawner: Spawner) -> ! { ) .expect("Fatal error building screen"); - let mut screen = screen::Handler::new(display, size); + let mut screen = screen::Handler::new(display, width, height); let _ = screen .update(screen::ScreenState::Initial) @@ -381,15 +381,6 @@ async fn main(spawner: Spawner) -> ! { } 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:?}"); @@ -469,9 +460,8 @@ async fn main(spawner: Spawner) -> ! { if let applogic::GlosaOutput::Locked(data) = &glosa_data { for sig in &data.signal_groups { let duration_str = sig - .timing - .as_ref() - .map(|v| alloc::format!("for {} s", v.min_end_sec)) + .end_sec + .map(|v| alloc::format!("for {v} s")) .unwrap_or_default(); println!("GLOSA: {} {} {duration_str}", sig.maneuver, sig.phase); } diff --git a/src/screen.rs b/src/screen.rs index 791d086..b2ddf90 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -5,24 +5,17 @@ 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, - size: DisplaySize, + width: u16, + height: u16, // internal state prev_state: ScreenState, hb_state: bool, - gnss_hb_state: bool, } #[derive(Debug, Default, PartialEq, Eq)] @@ -86,20 +79,16 @@ where const DEFAULT_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> = 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> = mono_font::MonoTextStyle::new(&profont::PROFONT_14_POINT, Self::COLOR_FG); - const BIG_HEIGHT_THLD: u16 = 172; - - pub fn new(target: D, display_size: DisplaySize) -> Self { + pub fn new(target: D, width: u16, height: u16) -> Self { Self { target, - size: display_size, + width, + height, prev_state: ScreenState::Uninitialized, hb_state: false, - gnss_hb_state: false, } } @@ -126,19 +115,18 @@ where // Update indicator/ heartbeat { let update_ind_dia = 10; - let circle_x_pos = - i32::from(self.size.width) - update_ind_dia - self.size.corner_radius; + let circle_x_pos = i32::from(self.width) - update_ind_dia - 10; text::Text::with_alignment( "HB", - geometry::Point::new(circle_x_pos - 5, 10 + self.size.offset_top), + geometry::Point::new(circle_x_pos - 5, 10), Self::SMALL_TEXT_STYLE, text::Alignment::Right, ) .draw(&mut self.target)?; primitives::Circle::new( - geometry::Point::new(circle_x_pos, 2 + self.size.offset_top), + geometry::Point::new(circle_x_pos, 2), update_ind_dia.cast_unsigned(), ) .into_styled(if self.hb_state { @@ -155,67 +143,13 @@ where 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> where D: DrawTarget, { primitives::Rectangle::new( geometry::Point::new(0, 0), - geometry::Size::new(self.size.width.into(), self.size.height.into()), + geometry::Size::new(self.width.into(), self.height.into()), ) .into_styled(Self::BK_STYLE) .draw(&mut self.target)?; @@ -223,10 +157,6 @@ where Ok(()) } - fn is_big_height(&self) -> bool { - self.size.height > Self::BIG_HEIGHT_THLD - } - #[allow(unused)] pub fn test_img(&mut self) -> Result<(), D::Error> { mipidsi::TestImage::new().draw(&mut self.target)?; @@ -241,7 +171,7 @@ where text::Text::with_alignment( msg, - geometry::Point::new(i32::from(self.size.width) / 2, 40 + self.size.offset_top), + geometry::Point::new(i32::from(self.width) / 2, 40), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -256,8 +186,8 @@ where self.clear()?; text::Text::with_alignment( - "Searching...", - geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top), + "No upcoming SPAT", + geometry::Point::new(i32::from(self.width) / 2, 130), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -279,7 +209,7 @@ where text::Text::with_alignment( &text, - geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top), + geometry::Point::new(i32::from(self.width) / 2, 130), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) @@ -288,9 +218,6 @@ where 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)] fn draw_glosa( &mut self, @@ -299,8 +226,6 @@ where ) -> Result<(), D::Error> { let num_signals = data.signal_groups.len(); - let is_big_height = self.is_big_height(); - if let Some(prev) = prev_data && prev.signal_groups.len() != num_signals { @@ -308,30 +233,25 @@ where self.clear(); } else { // only clear maneuver and timing area otherwise - let sig_total_height = 67 + if is_big_height { 20 } else { 0 }; + primitives::Rectangle::new( - geometry::Point::new(0, 105 + self.size.offset_top), - geometry::Size::new(self.size.width.into(), sig_total_height), + geometry::Point::new(0, 105), + geometry::Size::new(self.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(self.size.corner_radius + 5, 10 + self.size.offset_top), + geometry::Point::new(15, 10), Self::SMALL_TEXT_STYLE, ) .draw(&mut self.target)?; #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - let sig_width = i32::from(self.size.width) / (num_signals as i32); + let sig_width = i32::from(self.width) / (num_signals as i32); for (idx, sig) in data.signal_groups.iter().enumerate() { #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] @@ -349,7 +269,7 @@ where applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE, }; primitives::Circle::new( - geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top), + geometry::Point::new(offset + circle_margin, 15), circle_dia.cast_unsigned(), ) .into_styled(style) @@ -360,50 +280,24 @@ where // draw maneuver text::Text::with_alignment( &maneuver_to_str(sig.maneuver), - geometry::Point::new(centerline, 130 + self.size.offset_top), + geometry::Point::new(centerline, 130), Self::DEFAULT_TEXT_STYLE, text::Alignment::Center, ) .draw(&mut self.target)?; - // draw timing indicator(s) - if let Some(timing) = &sig.timing { - if let (Some(likely), Some(max)) = (timing.likely_end_sec, timing.max_end_sec) { - text::Text::with_alignment( - &alloc::format!("{likely}s"), - geometry::Point::new(centerline, 160 + self.size.offset_top), - Self::DEFAULT_TEXT_STYLE, - text::Alignment::Center, - ) - .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)?; - } + // draw timing indicator + if let Some(end_sec) = sig.end_sec { + text::Text::with_alignment( + &alloc::format!("{end_sec}s"), + geometry::Point::new(centerline, 160), + Self::DEFAULT_TEXT_STYLE, + text::Alignment::Center, + ) + .draw(&mut self.target)?; } } - // if is_big_height { - // // TODO: Add ETA - // } - Ok(()) } @@ -413,7 +307,7 @@ where self.clear()?; - let sig_width = i32::from(self.size.width) / 3; + let sig_width = i32::from(self.width) / 3; let circle_dia = 50; let circle_margin = (sig_width - circle_dia) / 2; @@ -425,7 +319,7 @@ where let offset = (idx as i32) * sig_width; primitives::Circle::new( - geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top), + geometry::Point::new(offset + circle_margin, 15), circle_dia.cast_unsigned(), ) .into_styled(*style) @@ -433,10 +327,10 @@ where } // draw welcome text - let centerline = i32::from(self.size.width) / 2; + let centerline = i32::from(self.width) / 2; text::Text::with_alignment( - "C-ITS Signal Phase\nWaiting for GNSS...", - geometry::Point::new(centerline, 130 + self.size.offset_top), + "C-ITS Signal Phase Display\nWaiting for GNSS fix...", + geometry::Point::new(centerline, 130), text_style, text::Alignment::Center, ) @@ -459,7 +353,7 @@ fn maneuver_to_str(value: applogic::v2x::map::Maneuvers) -> alloc::string::Strin /// /// # Errors /// Human-readable fatal errors -#[allow(unused, clippy::too_many_arguments, clippy::type_complexity)] +#[allow(clippy::too_many_arguments, clippy::type_complexity)] pub fn make_small_display( scl: IoScl, sda: IoSda, @@ -484,7 +378,8 @@ pub fn make_small_display( mipidsi::models::ST7789, esp_hal::gpio::Output<'_>, >, - DisplaySize, + u16, + u16, ), alloc::string::String, > @@ -501,15 +396,10 @@ 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}"))?; @@ -523,101 +413,18 @@ where 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) + .display_size(DISPLAY_HEIGHT + DISPLAY_HIDDEN_X, DISPLAY_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( - 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}"))?; - - 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}"))?; - - let display = mipidsi::Builder::new( - mipidsi::models::ST7789, - mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer), - ) - .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::Deg0)) - .init(&mut display_delay) - .map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?; - - Ok((display, size)) + Ok((display, DISPLAY_WIDTH, DISPLAY_HEIGHT)) } /// Builds the SPI interface for the ST7789 display