eculocate

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 93dc2055cf4791bb3b9f561a0e369faf947d1dcb
parent 996abe6efb4e97ba0f9f26d51fad54466110a1bd
Author: Daniel Barlow <dan@telent.net>
Date:   Sun, 16 Nov 2025 21:28:19 +0000

move BLE into its own file

Diffstat:
Msrc/main.rs | 207++-----------------------------------------------------------------------------
Msrc/wibble.rs | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 203 insertions(+), 204 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -16,18 +16,13 @@ use esp_hal::{ gpio::{Level, Output, OutputConfig}, clock::CpuClock, uart::{Uart,DataBits}, -// main, }; use embassy_executor::Spawner; -use embassy_futures::join::join; -use embassy_futures::select::select; use embassy_time::Timer; -use trouble_host::prelude::*; +use embassy_futures::join::join; -// use esp_hal::time::{Duration, Instant}; -use esp_radio::ble::controller::BleConnector; -use log::{warn,info}; +use log::{info}; extern crate alloc; @@ -61,13 +56,11 @@ async fn main(_s : Spawner) { let mut led = Output::new(peripherals.GPIO8, Level::High, output_config); led.set_high(); - info!("thing {}", wibble::the_answer()); - info!("let's go!"); led.set_low(); let _ = join( - setup_ble(peripherals.BT), + wibble::run_ble(peripherals.BT), run_ecu(peripherals.UART0, peripherals.GPIO21.reborrow(), peripherals.GPIO20) @@ -113,197 +106,3 @@ async fn run_ecu(periph_uart : esp_hal::peripherals::UART0<'_>, Timer::after_secs(1).await; } } - - -/// Max number of connections -const CONNECTIONS_MAX: usize = 1; - -/// Max number of L2CAP channels. -const L2CAP_CHANNELS_MAX: usize = 2; // Signal + att - -// GATT Server definition -#[gatt_server] -struct Server { - battery_service: BatteryService, -} - -/// Battery service -#[gatt_service(uuid = service::BATTERY)] -struct BatteryService { - /// Battery Level - #[descriptor(uuid = descriptors::VALID_RANGE, read, value = [0, 100])] - #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "hello", read, value = "Battery Level")] - #[characteristic(uuid = characteristic::BATTERY_LEVEL, read, notify, value = 10)] - level: u8, - #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", write, read, notify)] - status: bool, -} - -async fn setup_ble(peripheral : esp_hal::peripherals::BT<'_>) { - let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); - let connector = BleConnector::new(&radio_init, peripheral, Default::default()).expect("connector"); - let controller: ExternalController<_, 20> = ExternalController::new(connector); - - // Using a fixed "random" address can be useful for testing. In real scenarios, one would - // use e.g. the MAC 6 byte array as the address (how to get that varies by the platform). - let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]); - info!("Our address = {:?}", address); - - let mut resources: HostResources<DefaultPacketPool, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = HostResources::new(); - let stack = trouble_host::new(controller, &mut resources).set_random_address(address); - let Host { - mut peripheral, runner, .. - } = stack.build(); - - info!("Starting advertising and GATT service"); - let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig { - name: "TrouBLE", - appearance: &appearance::power_device::GENERIC_POWER_DEVICE, - })) - .unwrap(); - - let _ = join( - ble_task(runner), - async { - loop { - match advertise("Trouble Example", &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); - let b = custom_task(&server, &conn, &stack); - // run until any task ends (usually because the connection has been closed), - // then return to advertising state. - select(a, b).await; - } - Err(e) => { - // #[cfg(feature = "defmt")] - // let e = defmt::Debug2Format(&e); - panic!("[adv] error: {:?}", e); - } - } - } - }) - .await; -} - -/// This is a background task that is required to run forever alongside any other BLE tasks. -/// -/// ## Alternative -/// -/// If you didn't require this to be generic for your application, you could statically spawn this with i.e. -/// -/// ```rust,ignore -/// -/// #[embassy_executor::task] -/// async fn ble_task(mut runner: Runner<'static, SoftdeviceController<'static>>) { -/// runner.run().await; -/// } -/// -/// spawner.must_spawn(ble_task(runner)); -/// ``` -async fn ble_task<C: Controller, P: PacketPool>(mut runner: Runner<'_, C, P>) { - loop { - if let Err(e) = runner.run().await { - // #[cfg(feature = "defmt")] - // let e = defmt::Debug2Format(&e); - panic!("[ble_task] error: {:?}", e); - } - } -} - -/// Stream Events until the connection closes. -/// -/// This function will handle the GATT events and process them. -/// This is how we interact with read and write requests. -async fn gatt_events_task<P: PacketPool>(server: &Server<'_>, conn: &GattConnection<'_, '_, P>) -> Result<(), Error> { - let level = server.battery_service.level; - let reason = loop { - match conn.next().await { - GattConnectionEvent::Disconnected { reason } => break reason, - GattConnectionEvent::Gatt { event } => { - match &event { - GattEvent::Read(event) => { - if event.handle() == level.handle { - let value = server.get(&level); - info!("[gatt] Read Event to Level Characteristic: {:?}", value); - } - } - GattEvent::Write(event) => { - if event.handle() == level.handle { - info!("[gatt] Write Event to Level Characteristic: {:?}", event.data()); - } - } - _ => {} - }; - // This step is also performed at drop(), but writing it explicitly is necessary - // in order to ensure reply is sent. - match event.accept() { - Ok(reply) => reply.send().await, - Err(e) => warn!("[gatt] error sending response: {:?}", e), - }; - } - _ => {} // ignore other Gatt Connection Events - } - }; - info!("[gatt] disconnected: {:?}", reason); - Ok(()) -} - -/// 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<GattConnection<'values, 'server, DefaultPacketPool>, BleHostError<C::Error>> { - let mut advertiser_data = [0; 31]; - let len = AdStructure::encode_slice( - &[ - AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), - AdStructure::ServiceUuids16(&[[0x0f, 0x18]]), - AdStructure::CompleteLocalName(name.as_bytes()), - ], - &mut advertiser_data[..], - )?; - let advertiser = peripheral - .advertise( - &Default::default(), - Advertisement::ConnectableScannableUndirected { - adv_data: &advertiser_data[..len], - scan_data: &[], - }, - ) - .await?; - info!("[adv] advertising"); - let conn = advertiser.accept().await?.with_attribute_server(server)?; - info!("[adv] connection established"); - Ok(conn) -} - -/// Example task to use the BLE notifier interface. -/// This task will notify the connected central of a counter value every 2 seconds. -/// It will also read the RSSI value every 2 seconds. -/// and will stop when the connection is closed by the central or an error occurs. -async fn custom_task<C: Controller, P: PacketPool>( - server: &Server<'_>, - conn: &GattConnection<'_, '_, P>, - stack: &Stack<'_, C, P>, -) { - let mut tick: u8 = 0; - let level = server.battery_service.level; - loop { - tick = tick.wrapping_add(1); - info!("[custom_task] notifying connection of tick {}", tick); - if level.notify(conn, &tick).await.is_err() { - info!("[custom_task] error notifying connection"); - break; - }; - // read RSSI (Received Signal Strength Indicator) of the connection. - if let Ok(rssi) = conn.raw().rssi(stack).await { - info!("[custom_task] RSSI: {:?}", rssi); - } else { - info!("[custom_task] error getting RSSI"); - break; - }; - Timer::after_secs(2).await; - } -} diff --git a/src/wibble.rs b/src/wibble.rs @@ -1,2 +1,202 @@ +use embassy_time::Timer; +use trouble_host::prelude::*; +use esp_radio::ble::controller::BleConnector; +use log::{warn,info}; +use embassy_futures::join::join; +use embassy_futures::select::select; pub fn the_answer() -> i32 { 42 } + + +/// Max number of connections +const CONNECTIONS_MAX: usize = 1; + +/// Max number of L2CAP channels. +const L2CAP_CHANNELS_MAX: usize = 2; // Signal + att + +// GATT Server definition +#[gatt_server] +struct Server { + battery_service: BatteryService, +} + +/// Battery service +#[gatt_service(uuid = service::BATTERY)] +struct BatteryService { + /// Battery Level + #[descriptor(uuid = descriptors::VALID_RANGE, read, value = [0, 100])] + #[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "hello", read, value = "Battery Level")] + #[characteristic(uuid = characteristic::BATTERY_LEVEL, read, notify, value = 10)] + level: u8, + #[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", write, read, notify)] + status: bool, +} + +pub async fn run_ble(peripheral : esp_hal::peripherals::BT<'_>) { + let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); + let connector = BleConnector::new(&radio_init, peripheral, Default::default()).expect("connector"); + let controller: ExternalController<_, 20> = ExternalController::new(connector); + + // Using a fixed "random" address can be useful for testing. In real scenarios, one would + // use e.g. the MAC 6 byte array as the address (how to get that varies by the platform). + let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]); + info!("Our address = {:?}", address); + + let mut resources: HostResources<DefaultPacketPool, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = HostResources::new(); + let stack = trouble_host::new(controller, &mut resources).set_random_address(address); + let Host { + mut peripheral, runner, .. + } = stack.build(); + + info!("Starting advertising and GATT service"); + let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig { + name: "TrouBLE", + appearance: &appearance::power_device::GENERIC_POWER_DEVICE, + })) + .unwrap(); + + let _ = join( + ble_task(runner), + async { + loop { + match advertise("Trouble Example", &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); + let b = custom_task(&server, &conn, &stack); + // run until any task ends (usually because the connection has been closed), + // then return to advertising state. + select(a, b).await; + } + Err(e) => { + // #[cfg(feature = "defmt")] + // let e = defmt::Debug2Format(&e); + panic!("[adv] error: {:?}", e); + } + } + } + }) + .await; +} + +/// This is a background task that is required to run forever alongside any other BLE tasks. +/// +/// ## Alternative +/// +/// If you didn't require this to be generic for your application, you could statically spawn this with i.e. +/// +/// ```rust,ignore +/// +/// #[embassy_executor::task] +/// async fn ble_task(mut runner: Runner<'static, SoftdeviceController<'static>>) { +/// runner.run().await; +/// } +/// +/// spawner.must_spawn(ble_task(runner)); +/// ``` +async fn ble_task<C: Controller, P: PacketPool>(mut runner: Runner<'_, C, P>) { + loop { + if let Err(e) = runner.run().await { + // #[cfg(feature = "defmt")] + // let e = defmt::Debug2Format(&e); + panic!("[ble_task] error: {:?}", e); + } + } +} + +/// Stream Events until the connection closes. +/// +/// This function will handle the GATT events and process them. +/// This is how we interact with read and write requests. +async fn gatt_events_task<P: PacketPool>(server: &Server<'_>, conn: &GattConnection<'_, '_, P>) -> Result<(), Error> { + let level = server.battery_service.level; + let reason = loop { + match conn.next().await { + GattConnectionEvent::Disconnected { reason } => break reason, + GattConnectionEvent::Gatt { event } => { + match &event { + GattEvent::Read(event) => { + if event.handle() == level.handle { + let value = server.get(&level); + info!("[gatt] Read Event to Level Characteristic: {:?}", value); + } + } + GattEvent::Write(event) => { + if event.handle() == level.handle { + info!("[gatt] Write Event to Level Characteristic: {:?}", event.data()); + } + } + _ => {} + }; + // This step is also performed at drop(), but writing it explicitly is necessary + // in order to ensure reply is sent. + match event.accept() { + Ok(reply) => reply.send().await, + Err(e) => warn!("[gatt] error sending response: {:?}", e), + }; + } + _ => {} // ignore other Gatt Connection Events + } + }; + info!("[gatt] disconnected: {:?}", reason); + Ok(()) +} + +/// 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<GattConnection<'values, 'server, DefaultPacketPool>, BleHostError<C::Error>> { + let mut advertiser_data = [0; 31]; + let len = AdStructure::encode_slice( + &[ + AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), + AdStructure::ServiceUuids16(&[[0x0f, 0x18]]), + AdStructure::CompleteLocalName(name.as_bytes()), + ], + &mut advertiser_data[..], + )?; + let advertiser = peripheral + .advertise( + &Default::default(), + Advertisement::ConnectableScannableUndirected { + adv_data: &advertiser_data[..len], + scan_data: &[], + }, + ) + .await?; + info!("[adv] advertising"); + let conn = advertiser.accept().await?.with_attribute_server(server)?; + info!("[adv] connection established"); + Ok(conn) +} + +/// Example task to use the BLE notifier interface. +/// This task will notify the connected central of a counter value every 2 seconds. +/// It will also read the RSSI value every 2 seconds. +/// and will stop when the connection is closed by the central or an error occurs. +async fn custom_task<C: Controller, P: PacketPool>( + server: &Server<'_>, + conn: &GattConnection<'_, '_, P>, + stack: &Stack<'_, C, P>, +) { + let mut tick: u8 = 0; + let level = server.battery_service.level; + loop { + tick = tick.wrapping_add(1); + info!("[custom_task] notifying connection of tick {}", tick); + if level.notify(conn, &tick).await.is_err() { + info!("[custom_task] error notifying connection"); + break; + }; + // read RSSI (Received Signal Strength Indicator) of the connection. + if let Ok(rssi) = conn.raw().rssi(stack).await { + info!("[custom_task] RSSI: {:?}", rssi); + } else { + info!("[custom_task] error getting RSSI"); + break; + }; + Timer::after_secs(2).await; + } +}