From a921ebd4342310d6f0ba8b624f0521755c7bcf00 Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Tue, 18 Aug 2026 17:11:31 +0200 Subject: [PATCH 1/3] WIP BLE: Add "service change attribute" TODO: doesn't work yet See https://github.com/embassy-rs/trouble/issues/180 and maybe https://github.com/embassy-rs/trouble/issues/515 --- Cargo.toml | 2 +- Readme.md | 4 +--- src/ble.rs | 28 +++++++++++++++++++++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0136b8e..01fa77b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:p # enable I/O via UART uart = ["_uart", "_gnss"] # enable I/O via BLE -ble = ["_gnss", "denm"] +ble = ["_gnss"] # internal feature, that any UART I/O is enabled _uart = [] diff --git a/Readme.md b/Readme.md index 5fb34e6..16453c6 100644 --- a/Readme.md +++ b/Readme.md @@ -57,9 +57,7 @@ Enable UART I/O protocol according to [docs/uart-protocol.md](./docs/uart-protoc ### BLE Enable BLE I/O protocol according to [docs/ble-protocol.md](./docs/ble-protocol.md). -Will enable `denm` feature, but also enabling `cam` is recommended for full functionality. - -Note: Raw message reception is currently not implemented! +Enabling `denm` feature or `cam` feature is recommended, otherwise no data will be received. ## Usage diff --git a/src/ble.rs b/src/ble.rs index 64b175d..ff5f22d 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -41,9 +41,16 @@ pub enum BleSendable { #[gatt_server] struct Server { + generic: Generic, c_its_events: CITSEvents, } +#[gatt_service(uuid = "1801")] +pub(crate) struct Generic { + #[characteristic(uuid = characteristic::SERVICE_CHANGED, indicate, read, value = 0x0100FFFF)] + service_changed: u32, +} + #[gatt_service(uuid = "c0b70000-d4f4-4000-ada8-f99a02ee315c")] struct CITSEvents { #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "Position", read, value = "PositionState proto msg", type = &'static str)] @@ -136,13 +143,20 @@ pub async fn run( ); let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig { name: "ESP32 C-ITS", - appearance: &appearance::UNKNOWN, + appearance: &appearance::UNKNOWN, // TODO MESH_DEVICE?? outdoor_sports_activity::LOCATION_AND_NAVIGATION_DISPLAY })) .unwrap(); + let handle_start = 0x0100_u16; // little-endian + let handle_end = 0xFFFF_u16; + let concatenated: u32 = ((handle_start as u32) << 16) | handle_end as u32; + info!("Concatenated: {concatenated:?}"); + let test = server.set(&server.generic.service_changed, &concatenated); + info!("We were able to set service changed? {test:?}"); + let _ = embassy_futures::join::join(ble_task(runner), async { loop { - match advertise("ESP32 C-ITS", &mut peripheral, &server).await { + match advertise(&mut peripheral, &server).await { Ok(conn) => { // set up tasks when the connection is established to a central, so they don't run when no one is connected. let a = gatt_events_task(&server, &conn); @@ -157,7 +171,7 @@ pub async fn run( }); } Err(e) => { - panic!("BLE ADV: fatal error {e:?}"); + panic!("BLE ADV: fatal error: {e:?}"); } } } @@ -182,15 +196,19 @@ pub fn update(payload: BleSendable) { /// Create an advertiser to use to connect to a BLE Central, and wait for it to connect. async fn advertise<'values, 'server, C: Controller>( - name: &'values str, peripheral: &mut Peripheral<'values, C, DefaultPacketPool>, server: &'server Server<'values>, ) -> Result, BleHostError> { let mut advertiser_data = [0; 35]; + let service_uuid = Uuid::from(0xc0b70000d4f44000ada8f99a02ee315cu128) + .as_raw() + .try_into() + .unwrap(); let len = AdStructure::encode_slice( &[ AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), - AdStructure::CompleteLocalName(name.as_bytes()), + AdStructure::ServiceUuids128(&[service_uuid]), + AdStructure::ServiceUuids16(&[service::GATT.to_le_bytes()]), ], &mut advertiser_data[..], )?; From 729f18ee40c96fb75c718ccf8a0e9b8b1cd65138 Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Tue, 18 Aug 2026 17:11:31 +0200 Subject: [PATCH 2/3] BLE: Add "service change attribute", advertise service --- Cargo.toml | 2 +- Readme.md | 4 +-- src/ble.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0136b8e..01fa77b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ screen = ["dep:mipidsi", "dep:embedded-graphics", "dep:embedded-hal-bus", "dep:p # enable I/O via UART uart = ["_uart", "_gnss"] # enable I/O via BLE -ble = ["_gnss", "denm"] +ble = ["_gnss"] # internal feature, that any UART I/O is enabled _uart = [] diff --git a/Readme.md b/Readme.md index 5fb34e6..16453c6 100644 --- a/Readme.md +++ b/Readme.md @@ -57,9 +57,7 @@ Enable UART I/O protocol according to [docs/uart-protocol.md](./docs/uart-protoc ### BLE Enable BLE I/O protocol according to [docs/ble-protocol.md](./docs/ble-protocol.md). -Will enable `denm` feature, but also enabling `cam` is recommended for full functionality. - -Note: Raw message reception is currently not implemented! +Enabling `denm` feature or `cam` feature is recommended, otherwise no data will be received. ## Usage diff --git a/src/ble.rs b/src/ble.rs index 64b175d..c480d46 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -39,11 +39,69 @@ pub enum BleSendable { SpatemRaw(io::msg::RawMsgRx), } -#[gatt_server] -struct Server { +// GAP + 4 GATT attributes + our service(s) +const _ATTRIBUTE_TABLE_SIZE: usize = GAP_SERVICE_ATTRIBUTE_COUNT + 4 + CITSEvents::ATTRIBUTE_COUNT; + +pub struct Server<'values> { + server: AttributeServer< + 'values, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + DefaultPacketPool, + _ATTRIBUTE_TABLE_SIZE, + 1, + CONNECTIONS_MAX, + >, c_its_events: CITSEvents, } +impl<'values> Server<'values> { + // we implement this ourselves to add the "SERVICE_CHANGED" GATT characteristic, + // otherwise client devices won't update the service's characteristics when they change. + fn new_with_config(config: PeripheralConfig<'values>) -> Result { + let mut table: AttributeTable< + '_, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + _ATTRIBUTE_TABLE_SIZE, + > = AttributeTable::new(); + + // manual implementation of: gap.build(&mut table)?; + if !config.name.is_ascii() || config.name.len() > 22 { + return Err("Device name is too long. Max length is 22 bytes"); + } + + let mut gap_builder = table.add_service(Service::new(service::GAP)); + gap_builder.add_characteristic_ro(characteristic::DEVICE_NAME, config.name.as_bytes()); + gap_builder.add_characteristic_ro(characteristic::APPEARANCE, config.appearance); + gap_builder.build(); + + // add custom GATT service + let mut gatt_builder = table.add_service(Service::new(service::GATT)); + gatt_builder.add_characteristic_ro(characteristic::SERVICE_CHANGED, &[1, 0, 255, 255]); // [0x0001, 0xFFFF] in little-endian + gatt_builder.build(); + + let c_its_events = CITSEvents::new(&mut table); + + Ok(Self { + server: AttributeServer::new(table), + c_its_events, + }) + } +} + +impl<'values> core::ops::Deref for Server<'values> { + type Target = AttributeServer< + 'values, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + DefaultPacketPool, + _ATTRIBUTE_TABLE_SIZE, + 1, + CONNECTIONS_MAX, + >; + fn deref(&self) -> &Self::Target { + &self.server + } +} + #[gatt_service(uuid = "c0b70000-d4f4-4000-ada8-f99a02ee315c")] struct CITSEvents { #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "Position", read, value = "PositionState proto msg", type = &'static str)] @@ -56,7 +114,7 @@ struct CITSEvents { raw_notify: [u8; io::msg::RawNotify::PROTO_SIZE], #[cfg(feature = "cam")] - #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "CAM", read, value = "RawMsgRx proto msg", type = &'static str)] + // #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "CAM", read, value = "RawMsgRx proto msg", type = &'static str)] #[characteristic(uuid = "c0b70001-d4f4-4003-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])] cam_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE], #[cfg(feature = "cam")] @@ -65,7 +123,7 @@ struct CITSEvents { cam_event: [u8; io::msg::CamEvent::PROTO_SIZE], #[cfg(feature = "denm")] - #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "DENM", read, value = "RawMsgRx proto msg", type = &'static str)] + // #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "DENM", read, value = "RawMsgRx proto msg", type = &'static str)] #[characteristic(uuid = "c0b70001-d4f4-4005-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])] denm_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE], #[cfg(feature = "denm")] @@ -74,11 +132,11 @@ struct CITSEvents { denm_event: [u8; io::msg::DenmEvent::PROTO_SIZE], #[cfg(feature = "spat")] - #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "MAPEM", read, value = "RawMsgRx proto msg", type = &'static str)] + // #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "MAPEM", read, value = "RawMsgRx proto msg", type = &'static str)] #[characteristic(uuid = "c0b70001-d4f4-4007-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])] mapem_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE], #[cfg(feature = "spat")] - #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "SPATEM", read, value = "RawMsgRx proto msg", type = &'static str)] + // #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "SPATEM", read, value = "RawMsgRx proto msg", type = &'static str)] #[characteristic(uuid = "c0b70001-d4f4-4008-ada8-f99a02ee315c", read, value = [0u8; io::msg::RawMsgRx::PROTO_SIZE])] spatem_raw: [u8; io::msg::RawMsgRx::PROTO_SIZE], } @@ -142,8 +200,10 @@ pub async fn run( let _ = embassy_futures::join::join(ble_task(runner), async { loop { - match advertise("ESP32 C-ITS", &mut peripheral, &server).await { + match advertise(&mut peripheral, &server).await { Ok(conn) => { + info!("BLE: New client connected"); + // set up tasks when the connection is established to a central, so they don't run when no one is connected. let a = gatt_events_task(&server, &conn); let b = publishing_task(&server, &conn); @@ -157,7 +217,7 @@ pub async fn run( }); } Err(e) => { - panic!("BLE ADV: fatal error {e:?}"); + panic!("BLE ADV: fatal error: {e:?}"); } } } @@ -182,15 +242,18 @@ pub fn update(payload: BleSendable) { /// Create an advertiser to use to connect to a BLE Central, and wait for it to connect. async fn advertise<'values, 'server, C: Controller>( - name: &'values str, peripheral: &mut Peripheral<'values, C, DefaultPacketPool>, server: &'server Server<'values>, ) -> Result, BleHostError> { let mut advertiser_data = [0; 35]; + let service_uuid = Uuid::from(0xc0b7_0000_d4f4_4000_ada8_f99a_02ee_315c_u128) + .as_raw() + .try_into() + .unwrap(); let len = AdStructure::encode_slice( &[ AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), - AdStructure::CompleteLocalName(name.as_bytes()), + AdStructure::ServiceUuids128(&[service_uuid]), ], &mut advertiser_data[..], )?; From 6eff349894d81c0e06ceeac7c714d2644d1f08e5 Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Wed, 19 Aug 2026 14:37:49 +0200 Subject: [PATCH 3/3] BLE: Use unique name --- src/ble.rs | 9 +++++---- src/main.rs | 9 ++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ble.rs b/src/ble.rs index c480d46..b5a11fc 100644 --- a/src/ble.rs +++ b/src/ble.rs @@ -167,6 +167,7 @@ pub async fn run( esp_radio::ble::controller::BleConnector<'static>, CONTROLLER_SLOTS, >, + name: alloc::string::String, ) { let mut resources: HostResources = HostResources::new(); @@ -192,10 +193,10 @@ pub async fn run( "Starting advertising and GATT service for {}", features.join(",") ); - let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig { - name: "ESP32 C-ITS", - appearance: &appearance::UNKNOWN, - })) + let server = Server::new_with_config(PeripheralConfig { + name: &name, + appearance: &appearance::UNKNOWN, // maybe appearance::outdoor_sports_activity::LOCATION_AND_NAVIGATION_DISPLAY ? + }) .unwrap(); let _ = embassy_futures::join::join(ble_task(runner), async { diff --git a/src/main.rs b/src/main.rs index d69a9ba..9ed637c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -219,7 +219,14 @@ async fn main(spawner: Spawner) -> ! { #[cfg(feature = "ble")] { let controller = ble::make_controller(peripherals.BT); - spawner.spawn(ble::run(controller).expect("Failed to spawn BLE task")); + let serial = &mac.as_bytes()[4..7]; + let name = alloc::format!( + "ESP32 C-ITS {:02x}{:02x}{:02x}", + serial[0], + serial[1], + serial[2] + ); + spawner.spawn(ble::run(controller, name).expect("Failed to spawn BLE task")); } // Setup GNSS: