diff --git a/Cargo.lock b/Cargo.lock index 4d9cb80..72f63c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1216,6 +1216,7 @@ dependencies = [ "prost", "prost-build", "static_cell", + "tinytga", "trouble-host", "ublox", ] @@ -2529,6 +2530,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinytga" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "477839bd612acb4d0551915eaf6eef8cc1b3a9dd58e18e9c2b746c78614a25d5" +dependencies = [ + "embedded-graphics", + "nom", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6dc5d8e..eaa2c22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ spat = ["c-its-parser/spatem", "c-its-parser/mapem"] # enable some debug printing (without enabling debug logging of the ESP RTOS) spat_debug = [] # enable ST7789 based display -screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:profont"] +screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:profont", "dep:tinytga"] # enable I/O via UART uart = ["_uart", "_gnss"] # enable I/O via BLE @@ -123,6 +123,7 @@ mipidsi = { version = "0.10.0", optional = true } num-traits = { version = "0.2.19", default-features = false, features = ["libm"] } profont = { version = "0.7.0", optional = true } prost = { version = "0.14", default-features = false, features = ["derive"] } +tinytga = { version = "0.5.0", optional = true } ublox = { version = "0.10", optional = true, default-features = false, features = ["alloc", "ubx_proto27"] } [dev-dependencies] diff --git a/Readme.md b/Readme.md index 97dfd2b..2b8ed56 100644 --- a/Readme.md +++ b/Readme.md @@ -119,3 +119,16 @@ The unit tests in this project are meant to be run on the host: ``` cargo test --target host-tuple --bin test --no-default-features -F std,spat ``` + +## Tips and Tricks + +### Create TGA Assets + +Images are loaded as TGA assets, but shall not have an alpha channel. +You can convert them using [ImageMagick](https://imagemagick.org) like this: +```shell +# resize to 25px height, adding a black background to the white symbol and vice versa +magick Sinnbild_Radfahrer_StVO_1992-200px-white.png -resize x25 -background black -alpha background -flatten -alpha off -verbose Sinnbild_Radfahrer_StVO_1992-25px-white.tga +magick Sinnbild_Radfahrer_StVO_1992-200px-black.png -resize x25 -background white -alpha background -flatten -alpha off -verbose Sinnbild_Radfahrer_StVO_1992-25px-black.tga +``` + diff --git a/graphics/Sinnbild_Radfahrer_StVO_1992-25px-black.tga b/graphics/Sinnbild_Radfahrer_StVO_1992-25px-black.tga new file mode 100644 index 0000000..e458e96 Binary files /dev/null and b/graphics/Sinnbild_Radfahrer_StVO_1992-25px-black.tga differ diff --git a/graphics/Sinnbild_Radfahrer_StVO_1992-25px-white.tga b/graphics/Sinnbild_Radfahrer_StVO_1992-25px-white.tga new file mode 100644 index 0000000..e3005bd Binary files /dev/null and b/graphics/Sinnbild_Radfahrer_StVO_1992-25px-white.tga differ diff --git a/src/applogic/mod.rs b/src/applogic/mod.rs index 0ced524..bf078a0 100644 --- a/src/applogic/mod.rs +++ b/src/applogic/mod.rs @@ -106,6 +106,7 @@ pub struct GlosaSignalInfo { pub struct SignalGroupData { pub phase: SignalPhase, pub maneuver: map::Maneuvers, + pub usages: Vec, pub timing: Option, } @@ -383,35 +384,29 @@ impl State { }), ) } else { - // get signal groups which are in our approach + // get signal groups which are in our approach and have a maneuver let mut approach_sig_grps = intersection .signal_groups() .iter() - .filter(|i| { - info.signal_groups - .iter() - .find(|sg_id| **sg_id == i.id()) - .is_some() - }) - .collect::>(); - - // sort signal groups by maneuver - approach_sig_grps.sort_by_key(|v| v.maneuvers()); - - // display signal phases - let sig_grps = approach_sig_grps - .iter() - .filter_map(|sig| { - if let Some(maneuver) = sig.maneuvers() { - let phase = sig + .filter_map(|i| { + if let Some(maneuver) = i.maneuvers() + && info + .signal_groups + .iter() + .find(|sg_id| **sg_id == i.id()) + .is_some() + { + let phase = i .phase() .map(core::convert::Into::into) .unwrap_or_default(); - let timing = TimingData::new(sig, self.time.and_utc()); + let timing = TimingData::new(i, self.time.and_utc()); + let usages = i.source_lane_usages().to_vec(); Some(SignalGroupData { phase, maneuver, + usages, timing, }) } else { @@ -420,11 +415,39 @@ impl State { }) .collect::>(); + // sort signal groups by maneuver + approach_sig_grps.sort_by_key(|v| v.maneuver); + + // // display signal phases + // let sig_grps = approach_sig_grps + // .iter() + // .filter_map(|sig| { + // if let Some(maneuver) = sig.sig_group.maneuvers() { + // let phase = sig + // .sig_group + // .phase() + // .map(core::convert::Into::into) + // .unwrap_or_default(); + // let timing = + // TimingData::new(&sig.sig_group, self.time.and_utc()); + + // Some(SignalGroupData { + // phase, + // maneuver, + // timing, + // usages: sig.lane_types.clone(), + // }) + // } else { + // None + // } + // }) + // .collect::>(); + ( None, GlosaOutput::Locked(GlosaSignalInfo { intersection_id: intersection.id(), - signal_groups: sig_grps, + signal_groups: approach_sig_grps, }), ) // keep state, return signal groups } diff --git a/src/applogic/v2x/map.rs b/src/applogic/v2x/map.rs index d7e2195..1e1055f 100644 --- a/src/applogic/v2x/map.rs +++ b/src/applogic/v2x/map.rs @@ -191,27 +191,35 @@ impl Intersection { fn collect_signal_groups(lanes: &[Lane]) -> Vec { let mut connections = lanes .iter() - .flat_map(|i| i.connections.clone()) - .filter(|i| i.signal_group.is_some()) + .flat_map(ConnectionExt::from_lane) + .filter(|i| i.conn.signal_group.is_some()) .collect::>(); - connections.sort_by_key(|i| i.signal_group.unwrap_or_default()); + connections.sort_by_key(|i| i.conn.signal_group.unwrap_or_default()); - let chunks = connections.chunk_by(|a, b| a.signal_group == b.signal_group); + let chunks = connections.chunk_by(|a, b| a.conn.signal_group == b.conn.signal_group); chunks .map(|chunk| { - let (sig_grp_id, maneuvers, mut target_lanes) = chunk.iter().fold( - (None, Maneuvers::default(), vec![]), - |(sig_grp_id, maneuvers, mut lanes), i| { - let new_maneuvers = i - .maneuvers - .map(|v| core::ops::Add::add(maneuvers, v)) - .unwrap_or_default(); - lanes.push(i.target_lane_id); + let (sig_grp_id, maneuvers, mut target_lanes, mut source_lane_usages) = + chunk.iter().fold( + (None, Maneuvers::default(), vec![], vec![]), + |(sig_grp_id, maneuvers, mut lanes, mut usages), i| { + let new_maneuvers = i + .conn + .maneuvers + .map(|v| core::ops::Add::add(maneuvers, v)) + .unwrap_or_default(); + lanes.push(i.conn.target_lane_id); - (i.signal_group, new_maneuvers, lanes) - }, - ); + let lane_usages = i.source_lane_usages.clone(); + usages.extend_from_slice(&lane_usages); + + usages.sort_unstable(); + usages.dedup(); // TODO: only do this after all lanes were added? + + (i.conn.signal_group, new_maneuvers, lanes, usages) + }, + ); // unwrap is fine since we removed all connections without signal group beforehand let id = sig_grp_id.unwrap(); @@ -226,6 +234,7 @@ impl Intersection { SignalGroup { id, maneuvers, + source_lane_usages, target_lanes, phase: None, min_end_time: None, @@ -402,6 +411,28 @@ impl From<&etsi_its_dsrc::Connection> for Connection { } } +#[derive(Debug, Clone)] +#[allow(unused)] +struct ConnectionExt { + source_lane_id: u8, + source_lane_usages: Vec, + conn: Connection, +} + +impl ConnectionExt { + fn from_lane(value: &Lane) -> Vec { + value + .connections + .iter() + .map(|conn| Self { + source_lane_id: value.id, + source_lane_usages: value.usages.clone(), + conn: *conn, + }) + .collect() + } +} + /// Different view on a lane connection /// /// Technically this data is redundant with the `Connection` data of a `Lane`, @@ -411,7 +442,8 @@ impl From<&etsi_its_dsrc::Connection> for Connection { pub struct SignalGroup { id: u8, maneuvers: Option, - target_lanes: Vec, // mainly for debugging + source_lane_usages: Vec, // for easier signal group evaluation + target_lanes: Vec, // mainly for debugging // SPAT data /// current signal phase @@ -441,6 +473,9 @@ impl SignalGroup { pub fn maneuvers(&self) -> Option { self.maneuvers } + pub fn source_lane_usages(&self) -> &[UsageType] { + &self.source_lane_usages + } pub fn phase(&self) -> Option { self.phase } diff --git a/src/screen.rs b/src/screen.rs index 791d086..5b291bf 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -23,6 +23,10 @@ where prev_state: ScreenState, hb_state: bool, gnss_hb_state: bool, + + // assets + #[cfg(feature = "spat")] + bike_image: tinytga::Tga<'static, pixelcolor::Rgb565>, } #[derive(Debug, Default, PartialEq, Eq)] @@ -94,12 +98,19 @@ where const BIG_HEIGHT_THLD: u16 = 172; pub fn new(target: D, display_size: DisplaySize) -> Self { + #[cfg(feature = "spat")] + let data = include_bytes!("../graphics/Sinnbild_Radfahrer_StVO_1992-25px-white.tga"); + #[cfg(feature = "spat")] + let bike_image = tinytga::Tga::from_slice(data).expect("Failed to parse TGA image"); + Self { target, size: display_size, prev_state: ScreenState::Uninitialized, hb_state: false, gnss_hb_state: false, + #[cfg(feature = "spat")] + bike_image, } } @@ -307,7 +318,7 @@ where // clear everything if number of signals has changed self.clear(); } else { - // only clear maneuver and timing area otherwise + // only clear maneuver and timing area otherwise (lane type will clear itself) let sig_total_height = 67 + if is_big_height { 20 } else { 0 }; primitives::Rectangle::new( geometry::Point::new(0, 105 + self.size.offset_top), @@ -355,7 +366,38 @@ where .into_styled(style) .draw(&mut self.target)?; - // TODO: draw lane type? + // mark bike lanes which are not a vehicle lane, too + let lane_symbol_y_pos = 75; + if sig.usages.contains(&applogic::v2x::map::UsageType::Vehicle) { + // clear area + primitives::Rectangle::new( + geometry::Point::new( + centerline - (45 / 2), + lane_symbol_y_pos + self.size.offset_top, + ), + geometry::Size::new(45, 25), + ) + .into_styled(Self::BK_STYLE) + .draw(&mut self.target)?; + } else if sig.usages.contains(&applogic::v2x::map::UsageType::Bike) { + // bike symbol (45x25px) + embedded_graphics::image::Image::new( + &self.bike_image, + geometry::Point::new( + centerline - (45 / 2) + 1, + lane_symbol_y_pos + self.size.offset_top, + ), + ) + .draw(&mut self.target)?; + } else { + text::Text::with_alignment( + "?", + geometry::Point::new(centerline, lane_symbol_y_pos + 24 + self.size.offset_top), + Self::DEFAULT_TEXT_STYLE, + text::Alignment::Center, + ) + .draw(&mut self.target)?; + }; // draw maneuver text::Text::with_alignment( @@ -434,6 +476,13 @@ where // draw welcome text let centerline = i32::from(self.size.width) / 2; + + embedded_graphics::image::Image::new( + &self.bike_image, + geometry::Point::new(centerline - (45 / 2) + 1, 75 + self.size.offset_top), + ) + .draw(&mut self.target)?; + text::Text::with_alignment( "C-ITS Signal Phase\nWaiting for GNSS...", geometry::Point::new(centerline, 130 + self.size.offset_top),