commit 31c9b93f9cadc40d1f9154abd366ef7e0a30fff4
parent 09d7488c6e425980985c529c8ecfa32e007dfb96
Author: Daniel Barlow <dan@telent.net>
Date: Sun, 16 Nov 2025 10:36:57 +0000
add ble peripheral
Diffstat:
| M | Cargo.lock | | | 157 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | Cargo.toml | | | 17 | +++++++++++++++-- |
| M | src/bin/main.rs | | | 266 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- |
3 files changed, 410 insertions(+), 30 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -117,6 +117,9 @@ name = "btuuid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0acfef8a77a02866e04f7e2ad3f4c7b32d575696c49c4bbad742b4aecb8e4a3"
+dependencies = [
+ "uuid",
+]
[[package]]
name = "bumpalo"
@@ -149,6 +152,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa"
[[package]]
+name = "convert_case"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -267,8 +279,13 @@ name = "ecu-logical"
version = "0.1.0"
dependencies = [
"bleps",
- "critical-section",
+ "bt-hci",
+ "embassy-executor",
+ "embassy-futures",
+ "embassy-sync 0.7.2",
+ "embassy-time",
"embedded-io 0.7.1",
+ "embedded-io-async 0.7.0",
"esp-alloc",
"esp-backtrace",
"esp-bootloader-esp-idf",
@@ -277,6 +294,8 @@ dependencies = [
"esp-radio",
"esp-rtos",
"log",
+ "static_cell",
+ "trouble-host",
]
[[package]]
@@ -297,6 +316,36 @@ dependencies = [
]
[[package]]
+name = "embassy-executor"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06070468370195e0e86f241c8e5004356d696590a678d47d6676795b2e439c6b"
+dependencies = [
+ "critical-section",
+ "document-features",
+ "embassy-executor-macros",
+ "embassy-executor-timer-queue",
+]
+
+[[package]]
+name = "embassy-executor-macros"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfdddc3a04226828316bf31393b6903ee162238576b1584ee2669af215d55472"
+dependencies = [
+ "darling 0.20.11",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "embassy-executor-timer-queue"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c"
+
+[[package]]
name = "embassy-futures"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -340,6 +389,41 @@ dependencies = [
]
[[package]]
+name = "embassy-time"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65"
+dependencies = [
+ "cfg-if",
+ "critical-section",
+ "document-features",
+ "embassy-time-driver",
+ "embedded-hal 0.2.7",
+ "embedded-hal 1.0.0",
+ "embedded-hal-async",
+ "futures-core",
+]
+
+[[package]]
+name = "embassy-time-driver"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0a244c7dc22c8d0289379c8d8830cae06bb93d8f990194d0de5efb3b5ae7ba6"
+dependencies = [
+ "document-features",
+]
+
+[[package]]
+name = "embassy-time-queue-utils"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454"
+dependencies = [
+ "embassy-executor-timer-queue",
+ "heapless 0.8.0",
+]
+
+[[package]]
name = "embedded-can"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -679,7 +763,10 @@ dependencies = [
"allocator-api2",
"cfg-if",
"document-features",
+ "embassy-executor",
"embassy-sync 0.7.2",
+ "embassy-time-driver",
+ "embassy-time-queue-utils",
"esp-alloc",
"esp-config",
"esp-hal",
@@ -1367,6 +1454,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
+name = "static_cell"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1468,6 +1564,39 @@ dependencies = [
]
[[package]]
+name = "trouble-host"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b5d5992d07744e464b294ccf2dd6d96285bd61c267c4e0307d1f7057a8cd7f"
+dependencies = [
+ "bt-hci",
+ "embassy-futures",
+ "embassy-sync 0.7.2",
+ "embassy-time",
+ "embedded-io 0.6.1",
+ "futures",
+ "heapless 0.9.2",
+ "rand_core 0.6.4",
+ "static_cell",
+ "trouble-host-macros",
+ "zerocopy",
+]
+
+[[package]]
+name = "trouble-host-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcb85bec3a8393c22ca1a7c25c82c2d33689ab412f3487c492fd01a033ede7c2"
+dependencies = [
+ "convert_case",
+ "darling 0.20.11",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+ "uuid",
+]
+
+[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1486,6 +1615,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1633,3 +1768,23 @@ dependencies = [
"quote",
"syn 2.0.110",
]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
diff --git a/Cargo.toml b/Cargo.toml
@@ -16,6 +16,7 @@ esp-rtos = { version = "0.2.0", features = [
"esp-radio",
"esp32c3",
"log-04",
+ "embassy",
] }
esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32c3", "log-04"] }
@@ -25,9 +26,12 @@ bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", rev = "a5
"async",
"macros",
] }
-critical-section = "1.2.0"
-embedded-io = "0.7.1"
esp-alloc = "0.9.0"
+bt-hci = { version = "0.6.0" }
+embassy-executor = { version = "0.9.1", features = [] }
+embassy-time = { version = "0.5.0", features = [] }
+embedded-io = { version = "0.7.1", features = [] }
+embedded-io-async = { version = "0.7.0", features = [] }
esp-backtrace = { version = "0.18.1", features = [
"esp32c3",
"panic-handler",
@@ -43,6 +47,15 @@ esp-radio = { version = "0.17.0", features = [
] }
+embassy-futures = "0.1.1"
+embassy-sync = { version = "0.7" }
+
+static_cell = "2"
+trouble-host = { version = "0.5.0", features = [
+ "peripheral", "gatt", "default-packet-pool-mtu-255",
+ "derive", "scan",
+] }
+
[profile.dev]
# Rust debug is too slow.
# For debug builds always builds with some optimization
diff --git a/src/bin/main.rs b/src/bin/main.rs
@@ -7,20 +7,26 @@
)]
use esp_backtrace as _;
+use esp_println as _;
+
use esp_hal::{
delay::Delay,
timer::timg::TimerGroup,
gpio::{Level, Output, OutputConfig},
clock::CpuClock,
uart::{Uart,DataBits},
- main,
+// main,
};
-// use esp_hal::clock::CpuClock;
-// use esp_hal::main;
+
+use embassy_executor::Spawner;
+use embassy_futures::join::join;
+use embassy_futures::select::select;
+use embassy_time::Timer;
+use trouble_host::prelude::*;
+
// use esp_hal::time::{Duration, Instant};
-// use esp_hal::timer::timg::TimerGroup;
use esp_radio::ble::controller::BleConnector;
-use log::info;
+use log::{warn,info};
extern crate alloc;
@@ -28,10 +34,9 @@ extern crate alloc;
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();
-#[main]
-fn main() -> ! {
- // generator version: 1.0.1
-
+// #[main]
+#[esp_rtos::main]
+async fn main(_s : Spawner) {
esp_println::logger::init_logger_from_env();
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
@@ -51,34 +56,43 @@ fn main() -> ! {
let sw_interrupt =
esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
- let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller");
- let _connector = BleConnector::new(&radio_init, peripherals.BT, Default::default());
let mut led = Output::new(peripherals.GPIO8, Level::High, output_config);
led.set_high();
- let delay = Delay::new();
-
- let tx = peripherals.GPIO21.reborrow();
-
- let mut output = Output::new(tx, Level::High, OutputConfig::default());
info!("let's go!");
led.set_low();
+ let _ = join(
+ setup_ble(peripherals.BT),
+ run_ecu(peripherals.UART0,
+ peripherals.GPIO21.reborrow(),
+ peripherals.GPIO20)
+ ).await;
+}
+
+async fn run_ecu(periph_uart : esp_hal::peripherals::UART0<'_>,
+ mut tx_peripheral : esp_hal::peripherals::GPIO21<'_>,
+ rx_peripheral : esp_hal::peripherals::GPIO20<'_>) {
+ let tx = tx_peripheral.reborrow();
+ let delay = Delay::new();
+
+ let mut output = Output::new(tx, Level::High, OutputConfig::default());
+
output.set_low();
delay.delay_millis(70);
output.set_high();
delay.delay_millis(120);
let mut uart = Uart::new(
- peripherals.UART0,
+ periph_uart,
esp_hal::uart::Config::default()
.with_baudrate(10_400)
.with_data_bits(DataBits::_8)
)
.expect("now I got a reason")
- .with_rx(peripherals.GPIO20)
- .with_tx(peripherals.GPIO21.reborrow());
+ .with_rx(rx_peripheral)
+ .with_tx(tx_peripheral.reborrow());
uart.write(b"\xfe\x04\xff\xff").expect("write failed");
let _ = uart.flush();
@@ -86,15 +100,213 @@ fn main() -> ! {
uart.write(b"\x72\x05\x00\xF0\x99").expect("write 2 failed");
let _ = uart.flush();
- loop {
- info!("waiting");
- let mut buf = [0u8; 80];
- let bytes = uart.read(&mut buf[..]).expect("read failed");
- if bytes>0 { led.toggle(); }
+ // loop {
+ // info!("waiting");
+ // let mut buf = [0u8; 80];
+ // let bytes = uart.read(&mut buf[..]).expect("read failed");
+ // // if bytes>0 { led.toggle(); }
+
+ // info!("got {}", bytes);
+ // Timer::after_secs(1).await;
+ // }
+}
+
+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);
+ run_ble_stack(controller).await;
+}
- info!("got {bytes}");
- delay.delay_millis(50);
+/// 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)
+}
- // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0/examples/src/bin
+/// 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;
+ }
}