From c3d92462edf926039b1338f39c22903e30adc725 Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Mon, 7 Sep 2026 13:41:48 +0200 Subject: [PATCH 1/4] denm: Show warning symbol --- Readme.md | 9 +- .../Stationary_Emergency_Vehicle-140x105.svg | 38 ++++ .../Stationary_Emergency_Vehicle-70px.tga | Bin 0 -> 3728 bytes graphics/Stationary_Vehicle-140x70.svg | 15 ++ graphics/Stationary_Vehicle-80px.tga | Bin 0 -> 3218 bytes graphics/Work_Zone-140x95.svg | 17 ++ graphics/Work_Zone-80px.tga | Bin 0 -> 4338 bytes src/applogic/denm.rs | 9 +- src/main.rs | 21 ++- src/screen.rs | 173 +++++++++++++++++- 10 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 graphics/Stationary_Emergency_Vehicle-140x105.svg create mode 100644 graphics/Stationary_Emergency_Vehicle-70px.tga create mode 100644 graphics/Stationary_Vehicle-140x70.svg create mode 100644 graphics/Stationary_Vehicle-80px.tga create mode 100644 graphics/Work_Zone-140x95.svg create mode 100644 graphics/Work_Zone-80px.tga diff --git a/Readme.md b/Readme.md index 97d547d..4e48650 100644 --- a/Readme.md +++ b/Readme.md @@ -145,7 +145,10 @@ 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 -``` +magick Sinnbild_Radfahrer_StVO_1992-200px-white.png -resize x25 -background black -alpha background -flatten -alpha off -colorspace Gray -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 -colorspace Gray -verbose Sinnbild_Radfahrer_StVO_1992-25px-black.tga +magick Stationary_Emergency_Vehicle-140x105.svg -resize 70 -background black -alpha background -flatten -alpha off -colorspace Gray -verbose Stationary_Emergency_Vehicle-70px.tga +magick Stationary_Vehicle-140x70.svg -resize 80 -background black -alpha background -flatten -alpha off -colorspace Gray -verbose Stationary_Vehicle-80px.tga +magick Work_Zone-140x95.svg -resize 80 -background black -alpha background -flatten -alpha off -colorspace Gray -verbose Work_Zone-80px.tga +``` diff --git a/graphics/Stationary_Emergency_Vehicle-140x105.svg b/graphics/Stationary_Emergency_Vehicle-140x105.svg new file mode 100644 index 0000000..f5b5aa0 --- /dev/null +++ b/graphics/Stationary_Emergency_Vehicle-140x105.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphics/Stationary_Emergency_Vehicle-70px.tga b/graphics/Stationary_Emergency_Vehicle-70px.tga new file mode 100644 index 0000000000000000000000000000000000000000..3076adb5ccaab339e011367c56d45af8aaca2655 GIT binary patch literal 3728 zcmZQzU}k^;HwIG%4u$`tV918Rmp8A$;K!F&uV4T8@(K*z{v^pi-`2)Dfr0y;S@w>e z`)AmLd9DRV{}FG_uU;($MKDm`&?&2+xus1G%u`e_-%h+K2Mm>zAwX+mm%Os})^-I5 zS6RXD1<}T|$;d;2>V^&(IrS~AFabG@oy3~*cYQ?_6x3bVQBhTYc{^O7>L?MWyxKKy z^2CXgCd0ub5F1QDg(gj$G-um$d`5j*PEK z9L9VpS5i_|QY7paMP+3r#q9UkjoF~7q^zjn786GRMA@k-Dl4hX#cIm?NCjn;&}~mX zeI@`t-d*mlsI1`m6q_-dG?f$+aQPNZ!zC|8?2h@IprB}Z9!)h)?haigtjTznj*?=_ zU!2;|B);V+D5)($6a4@GTe^ax%{2^RoB}6I6cs|=pi3XrQ&MUp+A074|E^b5(pZgV z%FlcSMceCWf_S+%926BI-@%mp1(#o^j1`sINpQ;l|Nkc`D`{>3neyw`|0i`(ZF^t* ztWr?4y8#j+9zXF^P>B2Z|_@`@T>nI=j~-NYLJQMp1xQLiY{NJ&B7!dXdK zSxG@bNzroK?VBWlTPJ)Jl@-C+R|!@&D(TtUkP7r+1%Oh%B9y5FDW6Fpp*ocn4K4>O zD=4CrA_R?tX@=-hQc$p1^#AdK1Zx$D0GL!UF(Uv>RFKS3(hqDq^XLEn|39uo!zv18 zWre7d7cbxi7tfU`!;Mig+Wrv|WB>n@Dkv+114KdIub`U$gjS|d!$(R6Gd6A@5p0}qq=Xu}%E}s~19eb( zL5^uO&R8YRb=Ic=@7`Orj}P@RJ=Sd!c|N} zxTO;=h=;LC6;{~E=OS#q8vu + + + + + + + + + + + + + + diff --git a/graphics/Stationary_Vehicle-80px.tga b/graphics/Stationary_Vehicle-80px.tga new file mode 100644 index 0000000000000000000000000000000000000000..475ef757c52191545ebec6f16c1919e3de25a4cd GIT binary patch literal 3218 zcmZQzU}k`U00s>P4u$_X!LM(4!S_Enbz+lvu(BY)A0G&*+xi-tMyz5#S9_@_D&Pl- zYGDVlYQ_}&GhJIznJ`eW-ixUho5)d9C1phoZM;BBMM+t~^8q&P=wjb86qMDHw_Lb_ z2V6cm(OOYidE!5G%~<#c43*RxKVp$U6*}gmq~vlBRSX;NTcM(2%o}VnXkwdnl~g97 ziDBWMFj3Urh(#Pj=xe5eqWeP(VKjlC<%$ZignbLrb=XizWjcs~51%tt(%Ono0YdI~ zt&*bWV+0?a1&XXcHHwPypV4J-@UJ^4DbE4PfpmgsWc*`&_u^Z>E?X;VZbKHv$No1- zRnhy|k6TMS*L}xOx7$cw#V#>RSuy1+h8!+|=YERn*-3V)^7{KQlr+jKD=R80DQoth zKXaByaIRKaSxHe*Sy{0P(>w)8mQ~Tho6%4uH6ePG70WQp+oKOP5UPX}NG!k-xK`~Uy>-c}vBGWGn)6LEvdCF*dM`ptWuV=?Xj|NlQXSU^=N zsZ@XZ{|`6#|2jz#>KVnr7uZev|9@48hmJC+;M210CR8HaudvY4($-Q_Rt$QH!@N7m z-hK{BjTg?IJ$H|Ab@Sua*)wNO#wg)5?`NNPM1YUpEF89B758eC1q6@Ir9y{G*C z|Nl|CkDrr0@xTa2{QjKal@6}`)(3bSDB{l=D6%-rJ09-8792<)s(oBEl$Dhg--~5L5%zVYl^R@jIyC z1>^~kk=V?;o8~q77fk1abRRoag63_;W8SX`UMY8B#{K`l&d=MBBJ)m0_(38AV&?lw z9~X5({@n(5U&t$nHaPjd%B$)FocI6#|I?8^-WQMsu&__*fdWriSy?^{y>46Q8!%zt zUIf^=FxflqCKfZ0g}&uO%u_N&u3n#PTD@jPim!iY2oebP^Yim-UxOE{UfXL5N--+x zam(QfSMQC}P*qd2aKi*H`l@R9fw~6RJT(m@W%{AYii(O#YU&t3RgoN^d40}QYJ%ke DLlD3G literal 0 HcmV?d00001 diff --git a/graphics/Work_Zone-140x95.svg b/graphics/Work_Zone-140x95.svg new file mode 100644 index 0000000..bbbe6fb --- /dev/null +++ b/graphics/Work_Zone-140x95.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/graphics/Work_Zone-80px.tga b/graphics/Work_Zone-80px.tga new file mode 100644 index 0000000000000000000000000000000000000000..a397e217730bffcd9ed066fe9835bad4158b5baf GIT binary patch literal 4338 zcmZQzU}k`U00uJ#4u$`?!SB}(-~7QXO{&a=3NIJ0`dg%G!=-+cm4bqTf};C*Tnb1N zziuzD3;5Nx_)ageif*u3{}6(li!HG{nejZvq7eG>|Nr9#Fy|>NE4sae+ei{)hmMl6vXajB|NsA#gG>g6 z9w>SYj*(;xT=5DGka?xQ{{O#f4GKu8dCE#Xa3v%$ZaK;;DJ#dH`tyG>%s8;G6_P%Y zWDZR6zg^*)3QCHm%_n@I-c@vW1I3Wh6_^qd>9=cwR1_7JEi}RIQdZL1Tmz~-R9BE_ z4nq0!8IH2vGXWU^O2|r9U60R# zJ?!@pBtstlKEXyA8X=0xUQ?YFm6a9E&X8voNY}UesBj}Ch>1#y>KY*5E6xVVkcYo# zE7(T*>Om6?#9)Q=FW3zGdt>Utd)Oq9#eU|?E2`Ru`>8^M5$rze7syJW>?a*oN-EAX z-$1!Is9%Nh%E}7%r)D}U!A(>$y^N{)P$)=&qH4-DOgSWx-zD1FvHm1QEGn53*Px z4$@$G4mL0C|Nr+Z{Z$l|lBPme4xm17t}E;V;M;|NleGONDUmw3sR=DX2xQNW?Hs zSxKQ7TOPmZ3R0>N{R(0Ns(JtaoGCO=R8js$gZMc6j;z(hd~A2=o42$_F)>U}m7u zN{Tie*P-DAG4H+yI5L8tLb(6`|9=H)A;I1E_fm}oIDw;>gWxDC+O{Ee%zjpa6e}4Z z^)z0)fZUe?@yGj#){3C;Kp23*QdDwW{vGC;ouKxRvSK|v7ySSK+8t~j*cWe0R0x;` zGG0-;3u@r|2#{|TeDA}||NsC08!xzdznXAF1jrO5tf&L6t{<9#8UtDzkc|8P))!=6 zGRS=g4MCxSq!AZW(eXCeh_?X>%F2qBD9zjV!65UJ{{H{}BOBy;TxOt%EA{>V|L4o+ zPb-X-l>IM#{fq?OmVoRi`~3Oa5!4a@#Q-#pg5Uk`9lpN4KDtWE7Jj}c0GvLo{e1oG riS#X~qE&bGca)Qtmse0!RFFpn3W|zgK{ Option { let mgmt = &etsi.denm.management; if mgmt.termination.is_some() { - return; + return None; } if let (Some(sit), Some(loc)) = (&etsi.denm.situation, &etsi.denm.location) { @@ -38,7 +39,11 @@ pub fn handle_denm(etsi: &denm_pdu_description::DENM) { "DENN: {station_id}/{seq_num} CC {cc}/{scc} at {lat_deg:.3}°N, {lon_deg:.3}°E until {end_time:?}" ); } + + Some(sit.event_type.cc_and_scc.clone()) } else { info!("DENM: One of situation or location container not available"); + + None } } diff --git a/src/main.rs b/src/main.rs index 57a8963..3b02775 100644 --- a/src/main.rs +++ b/src/main.rs @@ -390,7 +390,16 @@ async fn main(spawner: Spawner) -> ! { } // update GNSS HB on screen - let _ = screen.update_gnss(pvt); + #[cfg(feature = "screen")] + { + let _ = screen.update_gnss(pvt); + + // revert to waiting for DENMs, if SPAT is not enabled + #[cfg(all(not(feature = "spat"), feature = "denm"))] + let _ = screen + .update(screen::ScreenState::DenmWaiting()) + .inspect_err(log_screen_error); + } } ublox::proto27::PacketRef::AckNak(nak) => { error!("UBX: {nak:?}"); @@ -580,7 +589,15 @@ async fn main(spawner: Spawner) -> ! { transport: _, ref etsi, } => { - applogic::denm::handle_denm(etsi); + match applogic::denm::handle_denm(etsi) { + #[cfg(feature = "screen")] + Some(cc) => { + let _ = screen + .update(screen::ScreenState::DenmWarning(cc)) + .inspect_err(log_screen_error); + } + _ => {} + } // Send data to BLE thread #[cfg(feature = "ble")] diff --git a/src/screen.rs b/src/screen.rs index 32bcfc7..22d0645 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -1,11 +1,16 @@ //! Screen Output +#[cfg(feature = "denm")] +use c_its_parser::standards::cdd_2_2_1; use embedded_graphics::prelude::*; use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text}; #[cfg(feature = "spat")] use crate::applogic; +#[cfg(feature = "denm")] +const DENM_MIN_DISPLAY_TIME: esp_hal::time::Duration = esp_hal::time::Duration::from_secs(3); + pub struct DisplaySize { width: u16, height: u16, @@ -24,10 +29,20 @@ where prev_state: ScreenState, hb_state: bool, gnss_hb_state: bool, + #[cfg(feature = "denm")] + denm_display_start_time: esp_hal::time::Instant, // assets #[cfg(feature = "spat")] - bike_image: tinytga::Tga<'static, pixelcolor::Rgb565>, + bike_image: tinytga::Tga<'static, pixelcolor::Rgb565>, // 45x25px + // #[cfg(feature = "denm")] + // warn_icon: tinytga::Tga<'static, pixelcolor::Rgb565>, // 200x153 px + #[cfg(feature = "denm")] + sv_insert: tinytga::Tga<'static, pixelcolor::Rgb565>, // 80px wide (original 140x70) + #[cfg(feature = "denm")] + ev_insert: tinytga::Tga<'static, pixelcolor::Rgb565>, // 60px wide (original 140x105) + #[cfg(feature = "denm")] + rww_insert: tinytga::Tga<'static, pixelcolor::Rgb565>, // 80px wide (original 140x95) } #[derive(Debug, Default, PartialEq, Eq)] @@ -43,6 +58,10 @@ pub enum ScreenState { GlosaFound(applogic::GlosaFound), #[cfg(feature = "spat")] GlosaLocked(applogic::GlosaSignalInfo), + #[cfg(all(not(feature = "spat"), feature = "denm"))] + DenmWaiting(), + #[cfg(feature = "denm")] + DenmWarning(cdd_2_2_1::etsi_its_cdd::CauseCodeChoice), } #[cfg(feature = "spat")] @@ -101,9 +120,23 @@ where 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"); + let bike_image = tinytga::Tga::from_slice(include_bytes!( + "../graphics/Sinnbild_Radfahrer_StVO_1992-25px-white.tga" + )) + .expect("Failed to parse TGA image"); + + #[cfg(feature = "denm")] + let sv_insert = + tinytga::Tga::from_slice(include_bytes!("../graphics/Stationary_Vehicle-80px.tga")) + .expect("Failed to parse TGA image"); + #[cfg(feature = "denm")] + let ev_insert = tinytga::Tga::from_slice(include_bytes!( + "../graphics/Stationary_Emergency_Vehicle-70px.tga" + )) + .expect("Failed to parse TGA image"); + #[cfg(feature = "denm")] + let rww_insert = tinytga::Tga::from_slice(include_bytes!("../graphics/Work_Zone-80px.tga")) + .expect("Failed to parse TGA image"); Self { target, @@ -111,15 +144,53 @@ where prev_state: ScreenState::Uninitialized, hb_state: false, gnss_hb_state: false, + #[cfg(feature = "denm")] + denm_display_start_time: esp_hal::time::Instant::EPOCH, #[cfg(feature = "spat")] bike_image, + // #[cfg(feature = "denm")] + // warn_icon, + #[cfg(feature = "denm")] + sv_insert, + #[cfg(feature = "denm")] + ev_insert, + #[cfg(feature = "denm")] + rww_insert, } } pub fn update(&mut self, state: ScreenState) -> Result<(), D::Error> { + #[cfg(feature = "denm")] + let now = esp_hal::time::Instant::now(); + + #[cfg(feature = "denm")] + // when a DENM was shown before, make sure it's shown for a minimum time + if let ScreenState::DenmWarning(_) = self.prev_state { + use core::ops::Sub; + + // update DENM display, if there are updates + if let ScreenState::DenmWarning(cause_code_choice) = &state { + // TODO: find a way to immediately turn off the DENM display when the DENM is over, but keep it for a certain time initially + self.denm_display_start_time = now; + + if self.prev_state != state { + self.draw_denm_warning(cause_code_choice)?; + self.prev_state = state; + + return Ok(()); + } + } + + // otherwise early return when timer isn't expired yet + if now.sub(self.denm_display_start_time) < DENM_MIN_DISPLAY_TIME { + return Ok(()); + } + } + if self.prev_state != state { match &state { ScreenState::Uninitialized | ScreenState::Initial => self.draw_slash_screen()?, + #[cfg(feature = "spat")] ScreenState::GlosaSearching(_glosa_searching) => self.draw_glosa_searching()?, #[cfg(feature = "spat")] @@ -136,10 +207,18 @@ where self.draw_glosa(glosa_data, prev_glosa_data)?; } ScreenState::Error(msg) => self.draw_message(msg)?, + #[cfg(all(not(feature = "spat"), feature = "denm"))] + ScreenState::DenmWaiting() => self.draw_denm_waiting()?, + #[cfg(feature = "denm")] + ScreenState::DenmWarning(cause_code_choice) => { + self.denm_display_start_time = now; + self.draw_denm_warning(cause_code_choice)?; + } } } // Update indicator/ heartbeat + #[cfg(feature = "spat")] { let update_ind_dia = 10; let circle_x_pos = @@ -477,6 +556,92 @@ where Ok(()) } + #[cfg(all(not(feature = "spat"), feature = "denm"))] + fn draw_denm_waiting(&mut self) -> Result<(), D::Error> { + // clear whole display and draw text + self.clear()?; + + text::Text::with_alignment( + "Waiting\nfor DENM...", + geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top), + Self::DEFAULT_TEXT_STYLE, + text::Alignment::Center, + ) + .draw(&mut self.target)?; + + Ok(()) + } + + #[cfg(feature = "denm")] + pub fn draw_denm_warning( + &mut self, + cc: &cdd_2_2_1::etsi_its_cdd::CauseCodeChoice, + ) -> Result<(), D::Error> { + // clear whole display and draw image + self.clear()?; + + let centerline = i32::from(self.size.width) / 2; + let sign_top = self.size.offset_top + 40; + + // build triangle shape + { + let point1 = geometry::Point::new(centerline, sign_top); + let point2 = geometry::Point::new(centerline - 100, sign_top + 150); + let point3 = geometry::Point::new(centerline + 100, sign_top + 150); + let mut line_style = + primitives::PrimitiveStyle::with_stroke(pixelcolor::Rgb565::CSS_RED, 10); + line_style.fill_color = Some(pixelcolor::Rgb565::WHITE); + + primitives::Triangle::new(point1, point2, point3) + .into_styled(line_style) + .draw(&mut self.target)?; + } + + // add icon inside the triangle + match cc { + cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::slowVehicle26(_) + | cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::stationaryVehicle94(_) => { + embedded_graphics::image::Image::new( + &self.sv_insert, + geometry::Point::new(centerline - (80 / 2), sign_top + 90), + ) + .draw(&mut self.target)?; + } + + cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::rescueAndRecoveryWorkInProgress15(_) + | cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::emergencyVehicleApproaching95(_) => { + embedded_graphics::image::Image::new( + &self.ev_insert, + geometry::Point::new(centerline - (70 / 2), sign_top + 85), + ) + .draw(&mut self.target)?; + } + + cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::roadworks3(_) => { + embedded_graphics::image::Image::new( + &self.rww_insert, + geometry::Point::new(centerline - (80 / 2), sign_top + 85), + ) + .draw(&mut self.target)?; + } + _ => {} + } + + let (cc, scc) = cc.to_u8_tuple(); + text::Text::with_alignment( + &alloc::format!("DENM CC {cc}/{scc}"), + geometry::Point::new( + i32::from(self.size.width) / 2, + sign_top + 150 + (10 / 2) + 30, + ), + Self::MID_TEXT_STYLE, + text::Alignment::Center, + ) + .draw(&mut self.target)?; + + Ok(()) + } + #[allow(unused)] fn draw_slash_screen(&mut self) -> Result<(), D::Error> { self.clear_all()?; From cd84754465e52ed2e77b343ecfd4d019efc76b2e Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Wed, 30 Sep 2026 09:31:47 +0200 Subject: [PATCH 2/4] build: Update default configurations --- Readme.md | 3 ++- feat-permut.sh | 15 ++++++++------- run-standalone-ble.sh | 6 ++++++ run-standalone-cam.sh | 2 +- ...andalone-no_cam.sh => run-standalone-simple.sh | 0 5 files changed, 17 insertions(+), 9 deletions(-) create mode 100755 run-standalone-ble.sh rename run-standalone-no_cam.sh => run-standalone-simple.sh (100%) diff --git a/Readme.md b/Readme.md index 97d547d..968afd2 100644 --- a/Readme.md +++ b/Readme.md @@ -98,7 +98,8 @@ Additional features are available through feature flags: Officially supported feature combinations are: - `esp,spat,gnss_ubx,screen` (default features, "standalone") -- `esp,spat,gnss_ubx,screen,cam_tx` ("standalone with CAM") +- `esp,spat,gnss_ubx,screen,ble` ("standalone with BLE") +- `esp,spat,gnss_ubx,screen,cam_tx,ble` ("standalone with CAM") - `esp,spat,gnss_ubx,screen,cam_tx,cam,denm` - `esp,uart,cam,denm` ("smartphone companion with UART") - `esp,ble,cam,denm,cam_tx` ("smartphone companion with BLE") diff --git a/feat-permut.sh b/feat-permut.sh index c8d5646..9cebe55 100755 --- a/feat-permut.sh +++ b/feat-permut.sh @@ -7,15 +7,16 @@ extended=${2:-false} features_base=( - uart,cam,denm - ble,cam,denm,cam_tx - uart,ble,cam,denm - spat,gnss_ubx,screen - spat,gnss_ubx,screen,cam_tx + spat,gnss_ubx,screen # "standalone" simple + spat,gnss_ubx,screen,ble # "standalone" with BLE + spat,gnss_ubx,screen,ble,cam_tx # "standalone" with CAM + spat,gnss_ubx,screen,cam_tx,cam,denm + uart,cam,denm # "smartphone companion with UART" + ble,cam,denm,cam_tx # "smartphone companion with BLE" ) features_more=( - spat,gnss_ubx,screen,ble - spat,gnss_ubx,screen,ble,cam_tx + uart,ble,cam,denm + spat,gnss_ubx,screen,cam_tx spat,gnss_ubx,screen,cam,denm spat,gnss_ubx,screen,cam_tx,cam,denm gnss_ubx,screen,cam_tx,cam,denm diff --git a/run-standalone-ble.sh b/run-standalone-ble.sh new file mode 100755 index 0000000..6957199 --- /dev/null +++ b/run-standalone-ble.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +## Builds common config for standalone device +## -> enable SPAT/ GLOSA with GNSS and a screen + BLE + +cargo run --release -F ble diff --git a/run-standalone-cam.sh b/run-standalone-cam.sh index 9bfafbe..0bc50c2 100755 --- a/run-standalone-cam.sh +++ b/run-standalone-cam.sh @@ -3,4 +3,4 @@ ## Builds common config for standalone device with automatic CAM transmission ## -> enable SPAT/ GLOSA with GNSS and a screen + CAM transmission -cargo run --release -F cam_tx +cargo run --release -F cam_tx,ble diff --git a/run-standalone-no_cam.sh b/run-standalone-simple.sh similarity index 100% rename from run-standalone-no_cam.sh rename to run-standalone-simple.sh From dc54b55230fa2851b9caa2013c08a53b24152c26 Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Tue, 29 Sep 2026 16:38:52 +0200 Subject: [PATCH 3/4] ble: Add device information service --- build.rs | 22 +++++++++++++++ src/ble.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 13 ++++++++- src/util.rs | 38 +++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index f376e0e..00deee1 100644 --- a/build.rs +++ b/build.rs @@ -9,6 +9,7 @@ fn main() -> std::io::Result<()> { println!("cargo:rustc-link-arg=-Tlinkall.x"); } + // compile protobuf definition to rust code let descriptor_path = std::path::PathBuf::from(env::var("OUT_DIR").unwrap()).join("proto_descriptor.bin"); @@ -17,6 +18,27 @@ fn main() -> std::io::Result<()> { .compile_well_known_types() .compile_protos(&["docs/c_its-messages.proto"], &["docs/"])?; + // get git revision + println!("cargo:rustc-rerun-if-changed=.git/HEAD"); + + // length: + // - tag name: 6-8 chars (v0.1.0 to v1.23.42) + // - offset+hash: 0-12 chars (up to 99 commits from tag + 8 char hash) + // - SUM: 6-20 + let git_output = std::process::Command::new("git") + .args(["describe", "--tags", "--match", "v*", "--always"]) + .output() + .unwrap(); + assert!( + git_output.stderr.is_empty(), + "{}", + String::from_utf8(git_output.stderr).unwrap() + ); + println!( + "cargo:rustc-env=GIT_REVISION={}", + String::from_utf8(git_output.stdout).unwrap() + ); + Ok(()) } diff --git a/src/ble.rs b/src/ble.rs index f72e972..8ff24c4 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -39,9 +39,73 @@ pub enum BleSendable { SpatemRaw(io::msg::RawMsgRx), } +pub const DIS_SERIAL_MAX_LEN: usize = 17; +pub const DIS_FW_REVISION_MAX_LEN: usize = 20; // git-describe +pub const DIS_SW_REVISION_MAX_LEN: usize = 66; // max length of get_feature_list, joined with comma + +pub struct DeviceInfo { + /// WiFi MAC + pub serial_number: [u8; DIS_SERIAL_MAX_LEN], + /// git tag/ revision + pub fw_revision: [u8; DIS_FW_REVISION_MAX_LEN], + /// used for feature flags + pub sw_revision: [u8; DIS_SW_REVISION_MAX_LEN], +} + +impl DeviceInfo { + pub fn new(serial_number: &str, fw_revision: &str, sw_revision: &str) -> Self { + Self { + serial_number: string_as_padded_buf(serial_number, DIS_SERIAL_MAX_LEN) + .try_into() + .unwrap(), + fw_revision: string_as_padded_buf(fw_revision, DIS_FW_REVISION_MAX_LEN) + .try_into() + .unwrap(), + sw_revision: string_as_padded_buf(sw_revision, DIS_SW_REVISION_MAX_LEN) + .try_into() + .unwrap(), + } + } +} + +pub fn string_as_padded_buf(value: &str, length: usize) -> alloc::vec::Vec { + let buf = value.as_bytes(); + let buf_len = buf.len(); + + // cut buffer at maximum length or pad with whitespace + if buf_len > length { + buf[..length].to_vec() + } else { + let padding_len = length - buf_len; + let padding = alloc::vec![b' '; padding_len]; + + [buf, &padding].concat() + } +} + #[gatt_server] struct Server { c_its_events: CITSEvents, + device_info_service: DeviceInfoService, +} + +#[gatt_service(uuid = service::DEVICE_INFORMATION)] +struct DeviceInfoService { + /// Manufacturer name + #[characteristic(uuid = characteristic::MANUFACTURER_NAME_STRING, read, value = "jtbx")] + mfg_name: &'static str, + /// Model number + #[characteristic(uuid = characteristic::MODEL_NUMBER_STRING, read, value = "ESP32-C5 C-ITS Companion")] + model_number: &'static str, + // Serial Number String + #[characteristic(uuid = characteristic::SERIAL_NUMBER_STRING, read, value = [0u8; DIS_SERIAL_MAX_LEN])] + serial_number: [u8; DIS_SERIAL_MAX_LEN], + // Firmware Revision String + #[characteristic(uuid = characteristic::FIRMWARE_REVISION_STRING, read, value = [0u8; DIS_FW_REVISION_MAX_LEN])] + fw_revision: [u8; DIS_FW_REVISION_MAX_LEN], + // Software Revision String + #[characteristic(uuid = characteristic::SOFTWARE_REVISION_STRING, read, value = [0u8; DIS_SW_REVISION_MAX_LEN])] + sw_revision: [u8; DIS_SW_REVISION_MAX_LEN], } #[gatt_service(uuid = "c0b70000-d4f4-4000-ada8-f99a02ee315c")] @@ -111,6 +175,7 @@ pub async fn run( >, name: alloc::string::String, short_name: alloc::string::String, + device_info: DeviceInfo, ) { let mut resources: HostResources = HostResources::new(); @@ -132,6 +197,21 @@ pub async fn run( })) .unwrap(); + // set "dynamic" device info data + let _ = server + .device_info_service + .serial_number + .set(&server, &device_info.serial_number); + let _ = server + .device_info_service + .fw_revision + .set(&server, &device_info.fw_revision); + let _ = server + .device_info_service + .sw_revision + .set(&server, &device_info.sw_revision); + + // advertise and run update logic let _ = embassy_futures::join::join(ble_task(runner), async { loop { match advertise(&mut peripheral, &server, &short_name).await { diff --git a/src/main.rs b/src/main.rs index 57a8963..d5956b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -228,7 +228,18 @@ async fn main(spawner: Spawner) -> ! { serial[2] ); let short_name = alloc::format!("V2X {:02x}{:02x}", serial[1], serial[2]); // max 8 chars! - spawner.spawn(ble::run(controller, name, short_name).expect("Failed to spawn BLE task")); + + let fw_revision = core::env!("GIT_REVISION"); + let sw_revision = util::get_feature_list().join(","); + spawner.spawn( + ble::run( + controller, + name, + short_name, + ble::DeviceInfo::new(&mac.to_string(), fw_revision, &sw_revision), + ) + .expect("Failed to spawn BLE task"), + ); } // Setup GNSS: diff --git a/src/util.rs b/src/util.rs index 4b0dd89..e92a760 100644 --- a/src/util.rs +++ b/src/util.rs @@ -31,3 +31,41 @@ pub(crate) fn get_tx_feature_list() -> Vec<&'static str> { features } + +#[allow(unused)] +pub(crate) fn get_feature_list() -> Vec<&'static str> { + let mut features = alloc::vec![]; + + if cfg!(feature = "gnss_ubx") { + features.push("gnss_ubx"); + } + if cfg!(feature = "gnss_l67k") { + features.push("gnss_l67k"); + } + if cfg!(feature = "cam_tx") { + features.push("cam_tx"); + } + if cfg!(feature = "cam") { + features.push("cam"); + } + if cfg!(feature = "denm") { + features.push("denm"); + } + if cfg!(feature = "spat") { + features.push("spat"); + } + if cfg!(feature = "spat_debug") { + features.push("spat_debug"); + } + if cfg!(feature = "screen") { + features.push("screen"); + } + if cfg!(feature = "uart") { + features.push("uart"); + } + if cfg!(feature = "ble") { + features.push("ble"); + } + + features +} From d1dc07b7e9a934bf18fadf22a34dfee37e71779f Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Mon, 7 Sep 2026 13:41:48 +0200 Subject: [PATCH 4/4] denm: Show warning symbol --- Readme.md | 9 +- .../Stationary_Emergency_Vehicle-140x105.svg | 38 ++++ .../Stationary_Emergency_Vehicle-70px.tga | Bin 0 -> 3728 bytes graphics/Stationary_Vehicle-140x70.svg | 15 ++ graphics/Stationary_Vehicle-80px.tga | Bin 0 -> 3218 bytes graphics/Work_Zone-140x95.svg | 17 ++ graphics/Work_Zone-80px.tga | Bin 0 -> 4338 bytes src/applogic/denm.rs | 9 +- src/main.rs | 21 ++- src/screen.rs | 173 +++++++++++++++++- 10 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 graphics/Stationary_Emergency_Vehicle-140x105.svg create mode 100644 graphics/Stationary_Emergency_Vehicle-70px.tga create mode 100644 graphics/Stationary_Vehicle-140x70.svg create mode 100644 graphics/Stationary_Vehicle-80px.tga create mode 100644 graphics/Work_Zone-140x95.svg create mode 100644 graphics/Work_Zone-80px.tga diff --git a/Readme.md b/Readme.md index 968afd2..ae71ef3 100644 --- a/Readme.md +++ b/Readme.md @@ -146,7 +146,10 @@ 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 -``` +magick Sinnbild_Radfahrer_StVO_1992-200px-white.png -resize x25 -background black -alpha background -flatten -alpha off -colorspace Gray -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 -colorspace Gray -verbose Sinnbild_Radfahrer_StVO_1992-25px-black.tga +magick Stationary_Emergency_Vehicle-140x105.svg -resize 70 -background black -alpha background -flatten -alpha off -colorspace Gray -verbose Stationary_Emergency_Vehicle-70px.tga +magick Stationary_Vehicle-140x70.svg -resize 80 -background black -alpha background -flatten -alpha off -colorspace Gray -verbose Stationary_Vehicle-80px.tga +magick Work_Zone-140x95.svg -resize 80 -background black -alpha background -flatten -alpha off -colorspace Gray -verbose Work_Zone-80px.tga +``` diff --git a/graphics/Stationary_Emergency_Vehicle-140x105.svg b/graphics/Stationary_Emergency_Vehicle-140x105.svg new file mode 100644 index 0000000..f5b5aa0 --- /dev/null +++ b/graphics/Stationary_Emergency_Vehicle-140x105.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphics/Stationary_Emergency_Vehicle-70px.tga b/graphics/Stationary_Emergency_Vehicle-70px.tga new file mode 100644 index 0000000000000000000000000000000000000000..3076adb5ccaab339e011367c56d45af8aaca2655 GIT binary patch literal 3728 zcmZQzU}k^;HwIG%4u$`tV918Rmp8A$;K!F&uV4T8@(K*z{v^pi-`2)Dfr0y;S@w>e z`)AmLd9DRV{}FG_uU;($MKDm`&?&2+xus1G%u`e_-%h+K2Mm>zAwX+mm%Os})^-I5 zS6RXD1<}T|$;d;2>V^&(IrS~AFabG@oy3~*cYQ?_6x3bVQBhTYc{^O7>L?MWyxKKy z^2CXgCd0ub5F1QDg(gj$G-um$d`5j*PEK z9L9VpS5i_|QY7paMP+3r#q9UkjoF~7q^zjn786GRMA@k-Dl4hX#cIm?NCjn;&}~mX zeI@`t-d*mlsI1`m6q_-dG?f$+aQPNZ!zC|8?2h@IprB}Z9!)h)?haigtjTznj*?=_ zU!2;|B);V+D5)($6a4@GTe^ax%{2^RoB}6I6cs|=pi3XrQ&MUp+A074|E^b5(pZgV z%FlcSMceCWf_S+%926BI-@%mp1(#o^j1`sINpQ;l|Nkc`D`{>3neyw`|0i`(ZF^t* ztWr?4y8#j+9zXF^P>B2Z|_@`@T>nI=j~-NYLJQMp1xQLiY{NJ&B7!dXdK zSxG@bNzroK?VBWlTPJ)Jl@-C+R|!@&D(TtUkP7r+1%Oh%B9y5FDW6Fpp*ocn4K4>O zD=4CrA_R?tX@=-hQc$p1^#AdK1Zx$D0GL!UF(Uv>RFKS3(hqDq^XLEn|39uo!zv18 zWre7d7cbxi7tfU`!;Mig+Wrv|WB>n@Dkv+114KdIub`U$gjS|d!$(R6Gd6A@5p0}qq=Xu}%E}s~19eb( zL5^uO&R8YRb=Ic=@7`Orj}P@RJ=Sd!c|N} zxTO;=h=;LC6;{~E=OS#q8vu + + + + + + + + + + + + + + diff --git a/graphics/Stationary_Vehicle-80px.tga b/graphics/Stationary_Vehicle-80px.tga new file mode 100644 index 0000000000000000000000000000000000000000..475ef757c52191545ebec6f16c1919e3de25a4cd GIT binary patch literal 3218 zcmZQzU}k`U00s>P4u$_X!LM(4!S_Enbz+lvu(BY)A0G&*+xi-tMyz5#S9_@_D&Pl- zYGDVlYQ_}&GhJIznJ`eW-ixUho5)d9C1phoZM;BBMM+t~^8q&P=wjb86qMDHw_Lb_ z2V6cm(OOYidE!5G%~<#c43*RxKVp$U6*}gmq~vlBRSX;NTcM(2%o}VnXkwdnl~g97 ziDBWMFj3Urh(#Pj=xe5eqWeP(VKjlC<%$ZignbLrb=XizWjcs~51%tt(%Ono0YdI~ zt&*bWV+0?a1&XXcHHwPypV4J-@UJ^4DbE4PfpmgsWc*`&_u^Z>E?X;VZbKHv$No1- zRnhy|k6TMS*L}xOx7$cw#V#>RSuy1+h8!+|=YERn*-3V)^7{KQlr+jKD=R80DQoth zKXaByaIRKaSxHe*Sy{0P(>w)8mQ~Tho6%4uH6ePG70WQp+oKOP5UPX}NG!k-xK`~Uy>-c}vBGWGn)6LEvdCF*dM`ptWuV=?Xj|NlQXSU^=N zsZ@XZ{|`6#|2jz#>KVnr7uZev|9@48hmJC+;M210CR8HaudvY4($-Q_Rt$QH!@N7m z-hK{BjTg?IJ$H|Ab@Sua*)wNO#wg)5?`NNPM1YUpEF89B758eC1q6@Ir9y{G*C z|Nl|CkDrr0@xTa2{QjKal@6}`)(3bSDB{l=D6%-rJ09-8792<)s(oBEl$Dhg--~5L5%zVYl^R@jIyC z1>^~kk=V?;o8~q77fk1abRRoag63_;W8SX`UMY8B#{K`l&d=MBBJ)m0_(38AV&?lw z9~X5({@n(5U&t$nHaPjd%B$)FocI6#|I?8^-WQMsu&__*fdWriSy?^{y>46Q8!%zt zUIf^=FxflqCKfZ0g}&uO%u_N&u3n#PTD@jPim!iY2oebP^Yim-UxOE{UfXL5N--+x zam(QfSMQC}P*qd2aKi*H`l@R9fw~6RJT(m@W%{AYii(O#YU&t3RgoN^d40}QYJ%ke DLlD3G literal 0 HcmV?d00001 diff --git a/graphics/Work_Zone-140x95.svg b/graphics/Work_Zone-140x95.svg new file mode 100644 index 0000000..bbbe6fb --- /dev/null +++ b/graphics/Work_Zone-140x95.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/graphics/Work_Zone-80px.tga b/graphics/Work_Zone-80px.tga new file mode 100644 index 0000000000000000000000000000000000000000..a397e217730bffcd9ed066fe9835bad4158b5baf GIT binary patch literal 4338 zcmZQzU}k`U00uJ#4u$`?!SB}(-~7QXO{&a=3NIJ0`dg%G!=-+cm4bqTf};C*Tnb1N zziuzD3;5Nx_)ageif*u3{}6(li!HG{nejZvq7eG>|Nr9#Fy|>NE4sae+ei{)hmMl6vXajB|NsA#gG>g6 z9w>SYj*(;xT=5DGka?xQ{{O#f4GKu8dCE#Xa3v%$ZaK;;DJ#dH`tyG>%s8;G6_P%Y zWDZR6zg^*)3QCHm%_n@I-c@vW1I3Wh6_^qd>9=cwR1_7JEi}RIQdZL1Tmz~-R9BE_ z4nq0!8IH2vGXWU^O2|r9U60R# zJ?!@pBtstlKEXyA8X=0xUQ?YFm6a9E&X8voNY}UesBj}Ch>1#y>KY*5E6xVVkcYo# zE7(T*>Om6?#9)Q=FW3zGdt>Utd)Oq9#eU|?E2`Ru`>8^M5$rze7syJW>?a*oN-EAX z-$1!Is9%Nh%E}7%r)D}U!A(>$y^N{)P$)=&qH4-DOgSWx-zD1FvHm1QEGn53*Px z4$@$G4mL0C|Nr+Z{Z$l|lBPme4xm17t}E;V;M;|NleGONDUmw3sR=DX2xQNW?Hs zSxKQ7TOPmZ3R0>N{R(0Ns(JtaoGCO=R8js$gZMc6j;z(hd~A2=o42$_F)>U}m7u zN{Tie*P-DAG4H+yI5L8tLb(6`|9=H)A;I1E_fm}oIDw;>gWxDC+O{Ee%zjpa6e}4Z z^)z0)fZUe?@yGj#){3C;Kp23*QdDwW{vGC;ouKxRvSK|v7ySSK+8t~j*cWe0R0x;` zGG0-;3u@r|2#{|TeDA}||NsC08!xzdznXAF1jrO5tf&L6t{<9#8UtDzkc|8P))!=6 zGRS=g4MCxSq!AZW(eXCeh_?X>%F2qBD9zjV!65UJ{{H{}BOBy;TxOt%EA{>V|L4o+ zPb-X-l>IM#{fq?OmVoRi`~3Oa5!4a@#Q-#pg5Uk`9lpN4KDtWE7Jj}c0GvLo{e1oG riS#X~qE&bGca)Qtmse0!RFFpn3W|zgK{ Option { let mgmt = &etsi.denm.management; if mgmt.termination.is_some() { - return; + return None; } if let (Some(sit), Some(loc)) = (&etsi.denm.situation, &etsi.denm.location) { @@ -38,7 +39,11 @@ pub fn handle_denm(etsi: &denm_pdu_description::DENM) { "DENN: {station_id}/{seq_num} CC {cc}/{scc} at {lat_deg:.3}°N, {lon_deg:.3}°E until {end_time:?}" ); } + + Some(sit.event_type.cc_and_scc.clone()) } else { info!("DENM: One of situation or location container not available"); + + None } } diff --git a/src/main.rs b/src/main.rs index d5956b7..8860627 100644 --- a/src/main.rs +++ b/src/main.rs @@ -401,7 +401,16 @@ async fn main(spawner: Spawner) -> ! { } // update GNSS HB on screen - let _ = screen.update_gnss(pvt); + #[cfg(feature = "screen")] + { + let _ = screen.update_gnss(pvt); + + // revert to waiting for DENMs, if SPAT is not enabled + #[cfg(all(not(feature = "spat"), feature = "denm"))] + let _ = screen + .update(screen::ScreenState::DenmWaiting()) + .inspect_err(log_screen_error); + } } ublox::proto27::PacketRef::AckNak(nak) => { error!("UBX: {nak:?}"); @@ -591,7 +600,15 @@ async fn main(spawner: Spawner) -> ! { transport: _, ref etsi, } => { - applogic::denm::handle_denm(etsi); + match applogic::denm::handle_denm(etsi) { + #[cfg(feature = "screen")] + Some(cc) => { + let _ = screen + .update(screen::ScreenState::DenmWarning(cc)) + .inspect_err(log_screen_error); + } + _ => {} + } // Send data to BLE thread #[cfg(feature = "ble")] diff --git a/src/screen.rs b/src/screen.rs index 32bcfc7..22d0645 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -1,11 +1,16 @@ //! Screen Output +#[cfg(feature = "denm")] +use c_its_parser::standards::cdd_2_2_1; use embedded_graphics::prelude::*; use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text}; #[cfg(feature = "spat")] use crate::applogic; +#[cfg(feature = "denm")] +const DENM_MIN_DISPLAY_TIME: esp_hal::time::Duration = esp_hal::time::Duration::from_secs(3); + pub struct DisplaySize { width: u16, height: u16, @@ -24,10 +29,20 @@ where prev_state: ScreenState, hb_state: bool, gnss_hb_state: bool, + #[cfg(feature = "denm")] + denm_display_start_time: esp_hal::time::Instant, // assets #[cfg(feature = "spat")] - bike_image: tinytga::Tga<'static, pixelcolor::Rgb565>, + bike_image: tinytga::Tga<'static, pixelcolor::Rgb565>, // 45x25px + // #[cfg(feature = "denm")] + // warn_icon: tinytga::Tga<'static, pixelcolor::Rgb565>, // 200x153 px + #[cfg(feature = "denm")] + sv_insert: tinytga::Tga<'static, pixelcolor::Rgb565>, // 80px wide (original 140x70) + #[cfg(feature = "denm")] + ev_insert: tinytga::Tga<'static, pixelcolor::Rgb565>, // 60px wide (original 140x105) + #[cfg(feature = "denm")] + rww_insert: tinytga::Tga<'static, pixelcolor::Rgb565>, // 80px wide (original 140x95) } #[derive(Debug, Default, PartialEq, Eq)] @@ -43,6 +58,10 @@ pub enum ScreenState { GlosaFound(applogic::GlosaFound), #[cfg(feature = "spat")] GlosaLocked(applogic::GlosaSignalInfo), + #[cfg(all(not(feature = "spat"), feature = "denm"))] + DenmWaiting(), + #[cfg(feature = "denm")] + DenmWarning(cdd_2_2_1::etsi_its_cdd::CauseCodeChoice), } #[cfg(feature = "spat")] @@ -101,9 +120,23 @@ where 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"); + let bike_image = tinytga::Tga::from_slice(include_bytes!( + "../graphics/Sinnbild_Radfahrer_StVO_1992-25px-white.tga" + )) + .expect("Failed to parse TGA image"); + + #[cfg(feature = "denm")] + let sv_insert = + tinytga::Tga::from_slice(include_bytes!("../graphics/Stationary_Vehicle-80px.tga")) + .expect("Failed to parse TGA image"); + #[cfg(feature = "denm")] + let ev_insert = tinytga::Tga::from_slice(include_bytes!( + "../graphics/Stationary_Emergency_Vehicle-70px.tga" + )) + .expect("Failed to parse TGA image"); + #[cfg(feature = "denm")] + let rww_insert = tinytga::Tga::from_slice(include_bytes!("../graphics/Work_Zone-80px.tga")) + .expect("Failed to parse TGA image"); Self { target, @@ -111,15 +144,53 @@ where prev_state: ScreenState::Uninitialized, hb_state: false, gnss_hb_state: false, + #[cfg(feature = "denm")] + denm_display_start_time: esp_hal::time::Instant::EPOCH, #[cfg(feature = "spat")] bike_image, + // #[cfg(feature = "denm")] + // warn_icon, + #[cfg(feature = "denm")] + sv_insert, + #[cfg(feature = "denm")] + ev_insert, + #[cfg(feature = "denm")] + rww_insert, } } pub fn update(&mut self, state: ScreenState) -> Result<(), D::Error> { + #[cfg(feature = "denm")] + let now = esp_hal::time::Instant::now(); + + #[cfg(feature = "denm")] + // when a DENM was shown before, make sure it's shown for a minimum time + if let ScreenState::DenmWarning(_) = self.prev_state { + use core::ops::Sub; + + // update DENM display, if there are updates + if let ScreenState::DenmWarning(cause_code_choice) = &state { + // TODO: find a way to immediately turn off the DENM display when the DENM is over, but keep it for a certain time initially + self.denm_display_start_time = now; + + if self.prev_state != state { + self.draw_denm_warning(cause_code_choice)?; + self.prev_state = state; + + return Ok(()); + } + } + + // otherwise early return when timer isn't expired yet + if now.sub(self.denm_display_start_time) < DENM_MIN_DISPLAY_TIME { + return Ok(()); + } + } + if self.prev_state != state { match &state { ScreenState::Uninitialized | ScreenState::Initial => self.draw_slash_screen()?, + #[cfg(feature = "spat")] ScreenState::GlosaSearching(_glosa_searching) => self.draw_glosa_searching()?, #[cfg(feature = "spat")] @@ -136,10 +207,18 @@ where self.draw_glosa(glosa_data, prev_glosa_data)?; } ScreenState::Error(msg) => self.draw_message(msg)?, + #[cfg(all(not(feature = "spat"), feature = "denm"))] + ScreenState::DenmWaiting() => self.draw_denm_waiting()?, + #[cfg(feature = "denm")] + ScreenState::DenmWarning(cause_code_choice) => { + self.denm_display_start_time = now; + self.draw_denm_warning(cause_code_choice)?; + } } } // Update indicator/ heartbeat + #[cfg(feature = "spat")] { let update_ind_dia = 10; let circle_x_pos = @@ -477,6 +556,92 @@ where Ok(()) } + #[cfg(all(not(feature = "spat"), feature = "denm"))] + fn draw_denm_waiting(&mut self) -> Result<(), D::Error> { + // clear whole display and draw text + self.clear()?; + + text::Text::with_alignment( + "Waiting\nfor DENM...", + geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top), + Self::DEFAULT_TEXT_STYLE, + text::Alignment::Center, + ) + .draw(&mut self.target)?; + + Ok(()) + } + + #[cfg(feature = "denm")] + pub fn draw_denm_warning( + &mut self, + cc: &cdd_2_2_1::etsi_its_cdd::CauseCodeChoice, + ) -> Result<(), D::Error> { + // clear whole display and draw image + self.clear()?; + + let centerline = i32::from(self.size.width) / 2; + let sign_top = self.size.offset_top + 40; + + // build triangle shape + { + let point1 = geometry::Point::new(centerline, sign_top); + let point2 = geometry::Point::new(centerline - 100, sign_top + 150); + let point3 = geometry::Point::new(centerline + 100, sign_top + 150); + let mut line_style = + primitives::PrimitiveStyle::with_stroke(pixelcolor::Rgb565::CSS_RED, 10); + line_style.fill_color = Some(pixelcolor::Rgb565::WHITE); + + primitives::Triangle::new(point1, point2, point3) + .into_styled(line_style) + .draw(&mut self.target)?; + } + + // add icon inside the triangle + match cc { + cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::slowVehicle26(_) + | cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::stationaryVehicle94(_) => { + embedded_graphics::image::Image::new( + &self.sv_insert, + geometry::Point::new(centerline - (80 / 2), sign_top + 90), + ) + .draw(&mut self.target)?; + } + + cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::rescueAndRecoveryWorkInProgress15(_) + | cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::emergencyVehicleApproaching95(_) => { + embedded_graphics::image::Image::new( + &self.ev_insert, + geometry::Point::new(centerline - (70 / 2), sign_top + 85), + ) + .draw(&mut self.target)?; + } + + cdd_2_2_1::etsi_its_cdd::CauseCodeChoice::roadworks3(_) => { + embedded_graphics::image::Image::new( + &self.rww_insert, + geometry::Point::new(centerline - (80 / 2), sign_top + 85), + ) + .draw(&mut self.target)?; + } + _ => {} + } + + let (cc, scc) = cc.to_u8_tuple(); + text::Text::with_alignment( + &alloc::format!("DENM CC {cc}/{scc}"), + geometry::Point::new( + i32::from(self.size.width) / 2, + sign_top + 150 + (10 / 2) + 30, + ), + Self::MID_TEXT_STYLE, + text::Alignment::Center, + ) + .draw(&mut self.target)?; + + Ok(()) + } + #[allow(unused)] fn draw_slash_screen(&mut self) -> Result<(), D::Error> { self.clear_all()?;