up
This commit is contained in:
1245
Cargo.lock
generated
1245
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
111
Cargo.toml
111
Cargo.toml
@@ -1,27 +1,30 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["tflite_demo", "lsm6ds3tr_demo"]
|
members = [
|
||||||
|
"tflite_demo",
|
||||||
|
"lsm6ds3tr_demo",
|
||||||
|
"bluetooth_demo",
|
||||||
|
"blinky",
|
||||||
|
"bluetooth_recorder",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.features]
|
[workspace.features]
|
||||||
default = ["ble-l2cap", "ble-gatt-server", "ble-gatt-client", "ble-sec"]
|
nrf52840 = ["embassy-nrf/nrf52840", "nrf-sdc/nrf52840"]
|
||||||
|
|
||||||
ble-l2cap = ["nrf-softdevice/ble-l2cap"]
|
defmt = [
|
||||||
ble-gatt-server = ["nrf-softdevice/ble-gatt-server"]
|
"dep:defmt",
|
||||||
ble-gatt-client = ["nrf-softdevice/ble-gatt-client"]
|
"trouble-host/defmt",
|
||||||
ble-sec = ["nrf-softdevice/ble-sec"]
|
"bt-hci/defmt",
|
||||||
|
"embedded-io/defmt-03",
|
||||||
nrf52840 = [
|
"embedded-hal/defmt-03",
|
||||||
"embassy-nrf/nrf52840",
|
|
||||||
"nrf-softdevice/nrf52840",
|
|
||||||
"nrf-softdevice/s140",
|
|
||||||
"dep:nrf-softdevice-s140",
|
|
||||||
]
|
]
|
||||||
|
security = ["trouble-host/security"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
assign-resources = "0.4.1"
|
|
||||||
embassy-futures = { version = "0.1.0" }
|
embassy-futures = { version = "0.1.0" }
|
||||||
embassy-usb = { version = "0.3.0", features = ["defmt"] }
|
embassy-usb = { version = "0.3.0", features = ["defmt"] }
|
||||||
embassy-executor = { version = "0.7.0", features = [
|
embassy-executor = { version = "0.7.0", features = [
|
||||||
"task-arena-size-32768",
|
# "task-arena-size-32768",
|
||||||
"arch-cortex-m",
|
"arch-cortex-m",
|
||||||
"executor-thread",
|
"executor-thread",
|
||||||
"executor-interrupt",
|
"executor-interrupt",
|
||||||
@@ -41,38 +44,62 @@ embassy-nrf = { version = "0.3.1", features = [
|
|||||||
"reset-pin-as-gpio",
|
"reset-pin-as-gpio",
|
||||||
] }
|
] }
|
||||||
embassy-embedded-hal = "0.3.0"
|
embassy-embedded-hal = "0.3.0"
|
||||||
cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
|
embassy-sync = { version = "0.7.0" }
|
||||||
cortex-m-rt = "0.7.0"
|
|
||||||
defmt = "0.3"
|
futures = { version = "0.3", default-features = false, features = [
|
||||||
defmt-rtt = "0.4"
|
"async-await",
|
||||||
|
] }
|
||||||
|
nrf-sdc = { version = "0.1.0", default-features = false, features = [
|
||||||
|
"defmt",
|
||||||
|
"peripheral",
|
||||||
|
"central",
|
||||||
|
"nrf52",
|
||||||
|
] }
|
||||||
|
nrf-mpsl = { version = "0.1.0", default-features = false, features = [
|
||||||
|
"defmt",
|
||||||
|
"critical-section-impl",
|
||||||
|
"nrf52",
|
||||||
|
] }
|
||||||
|
bt-hci = { version = "0.3", default-features = false, features = ["defmt"] }
|
||||||
|
trouble-host = { version = "0.2.0", features = ["derive", "scan"] }
|
||||||
|
trouble-example-apps = { version = "0.1.0", path = "trouble-example-apps", features = [
|
||||||
|
"defmt",
|
||||||
|
] }
|
||||||
|
|
||||||
|
defmt = "1.0.1"
|
||||||
|
defmt-rtt = "1.0.0"
|
||||||
|
|
||||||
|
# cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
|
||||||
|
cortex-m = { version = "0.7.6" }
|
||||||
|
cortex-m-rt = "0.7.5"
|
||||||
panic-probe = { version = "0.3", features = ["print-defmt"] }
|
panic-probe = { version = "0.3", features = ["print-defmt"] }
|
||||||
nrf52840-hal = "0.16.0"
|
rand = { version = "0.8.5", default-features = false }
|
||||||
usb-device = "0.2.7"
|
static_cell = "2.0.0"
|
||||||
usbd-serial = "0.1.0"
|
rand_core = { version = "0.6" }
|
||||||
microflow = { path = "../microflow-rs" }
|
rand_chacha = { version = "0.3", default-features = false }
|
||||||
# nalgebra = { version = "0.33.2", default-features = false, features = [
|
|
||||||
# "macros",
|
libm = "0.2.15"
|
||||||
# ] }
|
|
||||||
libm = "0.2"
|
|
||||||
panic-halt = "1.0.0"
|
panic-halt = "1.0.0"
|
||||||
heapless = "0.8.0"
|
heapless = "0.8.0"
|
||||||
lsm6ds3tr = { git = "https://avoid.sh/PetActivityMonitor/lsm6ds3tr.git", rev = "b368f6200c3ec9a84114e8ca137eb5fe0aa08e29", features = [
|
lsm6ds3tr = { git = "https://hax.avoid.sh/PetActivityMonitor/lsm6ds3tr.git", rev = "b368f6200c3ec9a84114e8ca137eb5fe0aa08e29", features = [
|
||||||
"defmt",
|
"defmt",
|
||||||
] }
|
] }
|
||||||
embassy-sync = { version = "0.5.0" }
|
assign-resources = "0.4.1"
|
||||||
embedded-storage = "0.3.0"
|
|
||||||
embedded-hal = "1.0.0"
|
|
||||||
embedded-hal-async = { version = "1.0.0" }
|
|
||||||
embedded-alloc = "0.6.0"
|
|
||||||
nrf-softdevice = { version = "0.1.0", features = [
|
|
||||||
"defmt",
|
|
||||||
"ble-peripheral",
|
|
||||||
"ble-central",
|
|
||||||
"critical-section-impl",
|
|
||||||
"nrf52840",
|
|
||||||
"s140",
|
|
||||||
] }
|
|
||||||
nrf-softdevice-s140 = { version = "0.1.1" }
|
|
||||||
fixed = "1.24.0"
|
fixed = "1.24.0"
|
||||||
atomic-pool = "1.0.1"
|
atomic-pool = "1.0.1"
|
||||||
static_cell = "2.0.0"
|
critical-once-cell = "0.2.0"
|
||||||
|
|
||||||
|
microflow = { version = "0.1.3" }
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
nrf-sdc = { git = "https://github.com/alexmoon/nrf-sdc.git", rev = "7be9b853e15ca0404d65c623d1ec5795fd96c204" }
|
||||||
|
nrf-mpsl = { git = "https://github.com/alexmoon/nrf-sdc.git", rev = "7be9b853e15ca0404d65c623d1ec5795fd96c204" }
|
||||||
|
|
||||||
|
embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", rev = "f35aa4005a63e8d478b2b95aaa2bfb316b72dece" }
|
||||||
|
embassy-nrf = { git = "https://github.com/embassy-rs/embassy.git", rev = "f35aa4005a63e8d478b2b95aaa2bfb316b72dece" }
|
||||||
|
embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", rev = "f35aa4005a63e8d478b2b95aaa2bfb316b72dece" }
|
||||||
|
embassy-futures = { git = "https://github.com/embassy-rs/embassy.git", rev = "f35aa4005a63e8d478b2b95aaa2bfb316b72dece" }
|
||||||
|
embassy-time = { git = "https://github.com/embassy-rs/embassy.git", rev = "f35aa4005a63e8d478b2b95aaa2bfb316b72dece" }
|
||||||
|
embassy-time-driver = { git = "https://github.com/embassy-rs/embassy.git", rev = "f35aa4005a63e8d478b2b95aaa2bfb316b72dece" }
|
||||||
|
embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy.git", rev = "f35aa4005a63e8d478b2b95aaa2bfb316b72dece" }
|
||||||
|
|||||||
33
blinky/Cargo.toml
Normal file
33
blinky/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "blinky"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cortex-m.workspace = true
|
||||||
|
cortex-m-rt.workspace = true
|
||||||
|
# nrf52840-hal.workspace = true
|
||||||
|
# microflow.workspace = true
|
||||||
|
libm.workspace = true
|
||||||
|
# nalgebra.workspace = true
|
||||||
|
heapless.workspace = true
|
||||||
|
lsm6ds3tr.workspace = true
|
||||||
|
defmt.workspace = true
|
||||||
|
defmt-rtt.workspace = true
|
||||||
|
# embedded-alloc.workspace = true
|
||||||
|
# embedded-hal.workspace = true
|
||||||
|
# embedded-hal-async.workspace = true
|
||||||
|
embassy-nrf.workspace = true
|
||||||
|
embassy-time.workspace = true
|
||||||
|
embassy-executor.workspace = true
|
||||||
|
embassy-sync.workspace = true
|
||||||
|
embassy-embedded-hal.workspace = true
|
||||||
|
fixed.workspace = true
|
||||||
|
atomic-pool.workspace = true
|
||||||
|
static_cell.workspace = true
|
||||||
|
embassy-usb.workspace = true
|
||||||
|
embassy-futures.workspace = true
|
||||||
|
panic-probe.workspace = true
|
||||||
|
# assign-resources.workspace = true
|
||||||
4
blinky/src/common.rs
Normal file
4
blinky/src/common.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#![macro_use]
|
||||||
|
|
||||||
|
use defmt_rtt as _; // global logger
|
||||||
|
use embassy_nrf as _; // time driver
|
||||||
32
blinky/src/main.rs
Normal file
32
blinky/src/main.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#![no_main]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use defmt::unwrap;
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_nrf::gpio::{Level, Output, OutputDrive};
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::main]
|
||||||
|
async fn main(spawner: Spawner) {
|
||||||
|
let p = embassy_nrf::init(Default::default());
|
||||||
|
let led = Output::new(p.P0_06, Level::Low, OutputDrive::Standard);
|
||||||
|
|
||||||
|
unwrap!(spawner.spawn(blinker(led, Duration::from_millis(300))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn blinker(mut led: Output<'static>, interval: Duration) {
|
||||||
|
loop {
|
||||||
|
led.set_high();
|
||||||
|
Timer::after(interval).await;
|
||||||
|
led.set_low();
|
||||||
|
Timer::after(interval).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
bluetooth_recorder/.DS_Store
vendored
Normal file
BIN
bluetooth_recorder/.DS_Store
vendored
Normal file
Binary file not shown.
43
bluetooth_recorder/Cargo.toml
Normal file
43
bluetooth_recorder/Cargo.toml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[package]
|
||||||
|
name = "bluetooth_recorder"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[features]
|
||||||
|
nrf52840 = ["embassy-nrf/nrf52840", "nrf-sdc/nrf52840"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
embassy-futures.workspace = true
|
||||||
|
embassy-executor.workspace = true
|
||||||
|
embassy-time.workspace = true
|
||||||
|
embassy-nrf.workspace = true
|
||||||
|
embassy-embedded-hal.workspace = true
|
||||||
|
embassy-sync.workspace = true
|
||||||
|
|
||||||
|
futures.workspace = true
|
||||||
|
nrf-sdc.workspace = true
|
||||||
|
nrf-mpsl.workspace = true
|
||||||
|
bt-hci.workspace = true
|
||||||
|
trouble-host.workspace = true
|
||||||
|
trouble-example-apps.workspace = true
|
||||||
|
|
||||||
|
defmt.workspace = true
|
||||||
|
defmt-rtt.workspace = true
|
||||||
|
|
||||||
|
cortex-m.workspace = true
|
||||||
|
cortex-m-rt.workspace = true
|
||||||
|
panic-probe.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
static_cell.workspace = true
|
||||||
|
rand_core.workspace = true
|
||||||
|
rand_chacha.workspace = true
|
||||||
|
|
||||||
|
libm.workspace = true
|
||||||
|
heapless.workspace = true
|
||||||
|
lsm6ds3tr.workspace = true
|
||||||
|
|
||||||
|
fixed.workspace = true
|
||||||
|
atomic-pool.workspace = true
|
||||||
|
# critical-once-cell.workspace = true
|
||||||
489
bluetooth_recorder/src/main.rs
Normal file
489
bluetooth_recorder/src/main.rs
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use core::ops::Deref;
|
||||||
|
|
||||||
|
use core::fmt::Write;
|
||||||
|
use defmt::{info, unwrap, warn};
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_nrf::gpio::{AnyPin, Level, Output, OutputDrive};
|
||||||
|
use embassy_nrf::mode::Async;
|
||||||
|
use embassy_nrf::peripherals::{self, RNG};
|
||||||
|
use embassy_nrf::pwm::SequenceMode;
|
||||||
|
use embassy_nrf::{bind_interrupts, rng, twim, Peri, Peripherals};
|
||||||
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
|
use embassy_sync::mutex::Mutex;
|
||||||
|
use heapless::String;
|
||||||
|
use lsm6ds3tr::interface::I2cInterface;
|
||||||
|
use lsm6ds3tr::{
|
||||||
|
registers::{GyroSampleRate, GyroScale},
|
||||||
|
AccelSampleRate, AccelScale, AccelSettings, GyroSettings, LsmSettings, LSM6DS3TR,
|
||||||
|
};
|
||||||
|
use nrf_sdc::mpsl::MultiprotocolServiceLayer;
|
||||||
|
use nrf_sdc::{self as sdc, mpsl};
|
||||||
|
use static_cell::{ConstStaticCell, StaticCell};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_futures::select::select;
|
||||||
|
use embassy_time::Timer;
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
RNG => rng::InterruptHandler<RNG>;
|
||||||
|
EGU0_SWI0 => nrf_sdc::mpsl::LowPrioInterruptHandler;
|
||||||
|
CLOCK_POWER => nrf_sdc::mpsl::ClockInterruptHandler;
|
||||||
|
RADIO => nrf_sdc::mpsl::HighPrioInterruptHandler;
|
||||||
|
TIMER0 => nrf_sdc::mpsl::HighPrioInterruptHandler;
|
||||||
|
RTC0 => nrf_sdc::mpsl::HighPrioInterruptHandler;
|
||||||
|
});
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn mpsl_task(mpsl: &'static MultiprotocolServiceLayer<'static>) -> ! {
|
||||||
|
mpsl.run().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many outgoing L2CAP buffers per link
|
||||||
|
const L2CAP_TXQ: u8 = 3;
|
||||||
|
|
||||||
|
/// How many incoming L2CAP buffers per link
|
||||||
|
const L2CAP_RXQ: u8 = 3;
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
fake_service: FakeService,
|
||||||
|
led_service: LedService,
|
||||||
|
gyro_service: GyroService,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fakr service
|
||||||
|
#[gatt_service(uuid = "18B10000-E8F2-537E-4F6C-D104768A1210")]
|
||||||
|
struct FakeService {
|
||||||
|
/// Fake Life
|
||||||
|
#[descriptor(uuid = descriptors::VALID_RANGE, read, value = [0, 100])]
|
||||||
|
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "hello", read, value = "Fake Life")]
|
||||||
|
#[characteristic(
|
||||||
|
uuid = "407813df-5dd4-1f87-ec11-cdb001100000",
|
||||||
|
read,
|
||||||
|
notify,
|
||||||
|
value = 10
|
||||||
|
)]
|
||||||
|
level: u8,
|
||||||
|
#[descriptor(uuid = descriptors::MEASUREMENT_DESCRIPTION, name = "boolean", read, value = "Bollean")]
|
||||||
|
#[characteristic(uuid = "408813df-5dd4-1f87-ec11-cdb001100000", write, read, notify)]
|
||||||
|
status: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gatt_service(uuid = "17B10000-E8F2-537E-4F6C-D104768A1214")]
|
||||||
|
struct LedService {
|
||||||
|
#[descriptor(
|
||||||
|
uuid = descriptors::MEASUREMENT_DESCRIPTION,
|
||||||
|
name = "led",
|
||||||
|
read,
|
||||||
|
value = "Led Toggle"
|
||||||
|
)]
|
||||||
|
#[characteristic(
|
||||||
|
uuid = "17B10000-E8F2-537E-4F6C-D104768A1215",
|
||||||
|
write,
|
||||||
|
read,
|
||||||
|
notify,
|
||||||
|
value = false
|
||||||
|
)]
|
||||||
|
on_off: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gatt_service(uuid = "19B10000-E8F2-537E-4F6C-D104768A1214")]
|
||||||
|
struct GyroService {
|
||||||
|
#[descriptor(
|
||||||
|
uuid = descriptors::MEASUREMENT_DESCRIPTION,
|
||||||
|
name = "gyro_toggle",
|
||||||
|
read,
|
||||||
|
value = "Gyro Toggle"
|
||||||
|
)]
|
||||||
|
#[characteristic(
|
||||||
|
uuid = "19B10000-E8F2-537E-4F6C-D104768A1215",
|
||||||
|
write,
|
||||||
|
read,
|
||||||
|
notify,
|
||||||
|
value = false
|
||||||
|
)]
|
||||||
|
on_off: bool, // This characteristic controls whether IMU data is sent
|
||||||
|
#[descriptor(
|
||||||
|
uuid = descriptors::MEASUREMENT_DESCRIPTION,
|
||||||
|
name = "imu_data",
|
||||||
|
read,
|
||||||
|
value = "Combined Gyro and Accel Data"
|
||||||
|
)]
|
||||||
|
#[characteristic(uuid = "19B10000-E8F2-537E-4F6C-D104768A1216", read, notify, value = [0;32])]
|
||||||
|
imu_data: [u8; 32], // Gyro (X, Y, Z), Accel (X, Y, Z) as 6 f32s = 24 bytes + Timestamp (u64) = 8 bytes = 32 bytes
|
||||||
|
}
|
||||||
|
fn build_sdc<'d, const N: usize>(
|
||||||
|
p: nrf_sdc::Peripherals<'d>,
|
||||||
|
rng: &'d mut rng::Rng<RNG, Async>,
|
||||||
|
mpsl: &'d MultiprotocolServiceLayer,
|
||||||
|
mem: &'d mut sdc::Mem<N>,
|
||||||
|
) -> Result<nrf_sdc::SoftdeviceController<'d>, nrf_sdc::Error> {
|
||||||
|
sdc::Builder::new()?
|
||||||
|
.support_adv()?
|
||||||
|
.support_peripheral()?
|
||||||
|
.peripheral_count(1)?
|
||||||
|
.buffer_cfg(
|
||||||
|
DefaultPacketPool::MTU as u16,
|
||||||
|
DefaultPacketPool::MTU as u16,
|
||||||
|
L2CAP_TXQ,
|
||||||
|
L2CAP_RXQ,
|
||||||
|
)?
|
||||||
|
.build(p, rng, mpsl, mem)
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_interrupts!(struct IrqsTest {
|
||||||
|
TWISPI0 => twim::InterruptHandler<peripherals::TWISPI0>;
|
||||||
|
});
|
||||||
|
|
||||||
|
type Imu<'a> = LSM6DS3TR<I2cInterface<twim::Twim<'a, peripherals::TWISPI0>>>;
|
||||||
|
static IMU: StaticCell<Mutex<CriticalSectionRawMutex, Imu<'static>>> = StaticCell::new();
|
||||||
|
|
||||||
|
#[embassy_executor::main]
|
||||||
|
async fn main(spawner: Spawner) {
|
||||||
|
let p = embassy_nrf::init(Default::default());
|
||||||
|
|
||||||
|
let mut pin = Output::new(p.P1_08, Level::High, OutputDrive::HighDrive);
|
||||||
|
pin.set_high();
|
||||||
|
Timer::after_millis(100).await;
|
||||||
|
|
||||||
|
let sda_pin = p.P0_07;
|
||||||
|
let scl_pin = p.P0_27;
|
||||||
|
|
||||||
|
let mut imu_config = twim::Config::default();
|
||||||
|
// This limits the ADXL355 ODR to 200Hz max.
|
||||||
|
imu_config.frequency = twim::Frequency::K400;
|
||||||
|
// Internal pullups for SCL and SDA must be enabled.
|
||||||
|
imu_config.scl_pullup = true;
|
||||||
|
imu_config.sda_pullup = true;
|
||||||
|
|
||||||
|
static RAM_BUFFER: ConstStaticCell<[u8; 16]> = ConstStaticCell::new([0; 16]);
|
||||||
|
|
||||||
|
let i2c = twim::Twim::new(
|
||||||
|
p.TWISPI0,
|
||||||
|
IrqsTest,
|
||||||
|
sda_pin,
|
||||||
|
scl_pin,
|
||||||
|
imu_config,
|
||||||
|
RAM_BUFFER.take(),
|
||||||
|
);
|
||||||
|
let settings = LsmSettings::basic()
|
||||||
|
.with_gyro(
|
||||||
|
GyroSettings::new()
|
||||||
|
.with_sample_rate(GyroSampleRate::_104Hz)
|
||||||
|
.with_scale(GyroScale::_2000DPS),
|
||||||
|
)
|
||||||
|
.with_accel(
|
||||||
|
AccelSettings::new()
|
||||||
|
.with_sample_rate(AccelSampleRate::_104Hz)
|
||||||
|
.with_scale(AccelScale::_4G),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut imu_driver = LSM6DS3TR::new(I2cInterface::new(i2c)).with_settings(settings);
|
||||||
|
imu_driver
|
||||||
|
.init()
|
||||||
|
.expect("LSM6DS3TR-C initialization failure!");
|
||||||
|
|
||||||
|
let imu = IMU.init(Mutex::new(imu_driver));
|
||||||
|
|
||||||
|
let mpsl_p =
|
||||||
|
mpsl::Peripherals::new(p.RTC0, p.TIMER0, p.TEMP, p.PPI_CH19, p.PPI_CH30, p.PPI_CH31);
|
||||||
|
let lfclk_cfg = mpsl::raw::mpsl_clock_lfclk_cfg_t {
|
||||||
|
source: mpsl::raw::MPSL_CLOCK_LF_SRC_RC as u8,
|
||||||
|
rc_ctiv: mpsl::raw::MPSL_RECOMMENDED_RC_CTIV as u8,
|
||||||
|
rc_temp_ctiv: mpsl::raw::MPSL_RECOMMENDED_RC_TEMP_CTIV as u8,
|
||||||
|
accuracy_ppm: mpsl::raw::MPSL_DEFAULT_CLOCK_ACCURACY_PPM as u16,
|
||||||
|
skip_wait_lfclk_started: mpsl::raw::MPSL_DEFAULT_SKIP_WAIT_LFCLK_STARTED != 0,
|
||||||
|
};
|
||||||
|
static MPSL: StaticCell<MultiprotocolServiceLayer> = StaticCell::new();
|
||||||
|
let mpsl = MPSL.init(unwrap!(mpsl::MultiprotocolServiceLayer::new(
|
||||||
|
mpsl_p, Irqs, lfclk_cfg
|
||||||
|
)));
|
||||||
|
spawner.must_spawn(mpsl_task(&*mpsl));
|
||||||
|
|
||||||
|
let sdc_p = sdc::Peripherals::new(
|
||||||
|
p.PPI_CH17, p.PPI_CH18, p.PPI_CH20, p.PPI_CH21, p.PPI_CH22, p.PPI_CH23, p.PPI_CH24,
|
||||||
|
p.PPI_CH25, p.PPI_CH26, p.PPI_CH27, p.PPI_CH28, p.PPI_CH29,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rng = rng::Rng::new(p.RNG, Irqs);
|
||||||
|
|
||||||
|
let mut sdc_mem = sdc::Mem::<4720>::new();
|
||||||
|
let sdc = unwrap!(build_sdc(sdc_p, &mut rng, mpsl, &mut sdc_mem));
|
||||||
|
|
||||||
|
let led_pin = p.P0_06.into();
|
||||||
|
|
||||||
|
// peripheral::run(sdc).await;
|
||||||
|
run(sdc, led_pin, imu).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the BLE stack.
|
||||||
|
pub async fn run<C>(
|
||||||
|
controller: C,
|
||||||
|
led_pin: Peri<'static, AnyPin>,
|
||||||
|
imu: &'static Mutex<CriticalSectionRawMutex, Imu<'static>>,
|
||||||
|
) where
|
||||||
|
C: Controller,
|
||||||
|
{
|
||||||
|
let mut led = Output::new(led_pin, Level::Low, OutputDrive::Standard);
|
||||||
|
|
||||||
|
// 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, &mut led, imu);
|
||||||
|
// run until any task ends (usually because the connection has been closed),
|
||||||
|
// then return to advertising state.
|
||||||
|
select(a, b).await;
|
||||||
|
// a.await;
|
||||||
|
|
||||||
|
led.set_low();
|
||||||
|
}
|
||||||
|
Err(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 {
|
||||||
|
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 fake_level = server.fake_service.level;
|
||||||
|
let led_level = server.led_service.handle;
|
||||||
|
let reason = loop {
|
||||||
|
match conn.next().await {
|
||||||
|
GattConnectionEvent::Disconnected { reason } => break reason,
|
||||||
|
GattConnectionEvent::Gatt { event } => {
|
||||||
|
match &event {
|
||||||
|
GattEvent::Read(event) => {
|
||||||
|
// if event.handle() == fake_level.handle {
|
||||||
|
// let value = server.get(&fake_level);
|
||||||
|
// // info!("[gatt] Read Event to Level Characteristic: {:?}", value);
|
||||||
|
// }
|
||||||
|
if event.handle() == led_level {
|
||||||
|
let value = server.get(&server.led_service.on_off);
|
||||||
|
// info!("[gatt] Read Event to Level Characteristic: {:?}", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GattEvent::Write(event) => {
|
||||||
|
// if event.handle() == fake_level.handle {
|
||||||
|
// // info!(
|
||||||
|
// // "[gatt] Write Event to Level Characteristic: {:?}",
|
||||||
|
// // event.data()
|
||||||
|
// // );
|
||||||
|
// }
|
||||||
|
if event.handle() == led_level {
|
||||||
|
// 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) => panic!("[gatt] error sending response"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {} // 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>,
|
||||||
|
led: &mut Output<'static>,
|
||||||
|
imu: &'static Mutex<CriticalSectionRawMutex, Imu<'static>>,
|
||||||
|
) {
|
||||||
|
let mut tick: u8 = 0;
|
||||||
|
let fake_level = server.fake_service.level;
|
||||||
|
let led_on_off = server.led_service.on_off;
|
||||||
|
let gyro_service_on_off = server.gyro_service.on_off; // Get gyro service toggle
|
||||||
|
let imu_data_char = &server.gyro_service.imu_data; // Combined IMU data characteristic
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Only read and send IMU data if the gyro_service_on_off characteristic is true
|
||||||
|
if gyro_service_on_off.get(server).unwrap_or(false) {
|
||||||
|
let mut imu_guard = imu.lock().await;
|
||||||
|
|
||||||
|
if let (Ok(xyz_a), Ok(xyz_g)) = (imu_guard.read_accel_raw(), imu_guard.read_gyro()) {
|
||||||
|
info!("Accel: x:{} y:{} z:{}", xyz_a.x, xyz_a.y, xyz_a.z);
|
||||||
|
info!("Gyro: x:{} y:{} z:{}", xyz_g.x, xyz_g.y, xyz_g.z);
|
||||||
|
|
||||||
|
// Capture timestamp immediately after reading IMU data for better accuracy
|
||||||
|
let current_time_us = embassy_time::Instant::now().as_micros();
|
||||||
|
|
||||||
|
// Convert i32 gyro values to f32 and then to their byte representation
|
||||||
|
let gx_f32 = xyz_g.x as f32;
|
||||||
|
let gy_f32 = xyz_g.y as f32;
|
||||||
|
let gz_f32 = xyz_g.z as f32;
|
||||||
|
|
||||||
|
// Convert i32 accel values to f32 and then to their byte representation
|
||||||
|
let ax_f32 = xyz_a.x as f32;
|
||||||
|
let ay_f32 = xyz_a.y as f32;
|
||||||
|
let az_f32 = xyz_a.z as f32;
|
||||||
|
|
||||||
|
let gx_bytes = gx_f32.to_le_bytes();
|
||||||
|
let gy_bytes = gy_f32.to_le_bytes();
|
||||||
|
let gz_bytes = gz_f32.to_le_bytes();
|
||||||
|
let ax_bytes = ax_f32.to_le_bytes();
|
||||||
|
let ay_bytes = ay_f32.to_le_bytes();
|
||||||
|
let az_bytes = az_f32.to_le_bytes();
|
||||||
|
|
||||||
|
// Combine gyro, accel, and timestamp byte arrays into a single [u8; 32] array
|
||||||
|
// Order: Gx, Gy, Gz, Ax, Ay, Az (each 4 bytes) then Timestamp (8 bytes)
|
||||||
|
let mut combined_imu_data = [0u8; 24];
|
||||||
|
combined_imu_data[0..4].copy_from_slice(&gx_bytes);
|
||||||
|
combined_imu_data[4..8].copy_from_slice(&gy_bytes);
|
||||||
|
combined_imu_data[8..12].copy_from_slice(&gz_bytes);
|
||||||
|
combined_imu_data[12..16].copy_from_slice(&ax_bytes);
|
||||||
|
combined_imu_data[16..20].copy_from_slice(&ay_bytes);
|
||||||
|
combined_imu_data[20..24].copy_from_slice(&az_bytes);
|
||||||
|
let timestamp_bytes = current_time_us.to_le_bytes();
|
||||||
|
|
||||||
|
let mut final_imu_packet = [0u8; 32];
|
||||||
|
final_imu_packet[0..24].copy_from_slice(&combined_imu_data);
|
||||||
|
final_imu_packet[24..32].copy_from_slice(×tamp_bytes);
|
||||||
|
|
||||||
|
// Update combined IMU characteristic and notify
|
||||||
|
imu_data_char.set(server, &final_imu_packet);
|
||||||
|
if imu_data_char.notify(conn, &final_imu_packet).await.is_err() {
|
||||||
|
info!(
|
||||||
|
"[custom_task] error notifying combined IMU and timestamp characteristic"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
warn!("Could not read IMU data");
|
||||||
|
}
|
||||||
|
} // End of gyro_service_on_off check
|
||||||
|
|
||||||
|
tick = tick.wrapping_add(1);
|
||||||
|
// info!("[custom_task] notifying connection of tick {}", tick);
|
||||||
|
if fake_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;
|
||||||
|
};
|
||||||
|
if led_on_off.get(server).unwrap() == true {
|
||||||
|
led.set_high();
|
||||||
|
}
|
||||||
|
// Timer::after_millis(100).await;
|
||||||
|
if led_on_off.get(server).unwrap() == true {
|
||||||
|
led.set_low();
|
||||||
|
}
|
||||||
|
Timer::after_millis(100).await
|
||||||
|
}
|
||||||
|
}
|
||||||
26
build.rs
26
build.rs
@@ -1,6 +1,22 @@
|
|||||||
fn main() {
|
fn linker_data() -> %'static [u8] {
|
||||||
// Rebuild if memory.x file was rebuild.
|
return include_bytes!("memory.x");
|
||||||
// Linker seems to pick it up automatically.
|
}
|
||||||
// (via the include in link.x which is added with a linker argument!)
|
|
||||||
println!("cargo:rerun-if-changed=memory.x");
|
fn main() {
|
||||||
|
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||||
|
File::create(out.join("memory.x"))
|
||||||
|
.unwrap()
|
||||||
|
.write_all(linker_data())
|
||||||
|
.unwrap();
|
||||||
|
println!("cargo:rustc-link-search={}", out.display());
|
||||||
|
|
||||||
|
// By default, Cargo will re-run a build script whenever
|
||||||
|
// any file in the project changes. By specifying `memory.x`
|
||||||
|
// here, we ensure the build script is only re-run when
|
||||||
|
// `memory.x` is changed.
|
||||||
|
println!("cargo:rerun-if-changed=memory.x");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-arg-bins=--nmagic");
|
||||||
|
println!("cargo:rustc-link-arg-bins=-Tlink.x");
|
||||||
|
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,41 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
COM_PORT=/dev/cu.usbmodem1101
|
# Find the COM port dynamically
|
||||||
|
COM_PORT=$(ls /dev/cu.usbmodem* 2>/dev/null | head -n 1)
|
||||||
|
|
||||||
echo -e "bootloader" > $COM_PORT
|
# Check if a COM port was found
|
||||||
|
if [ -z "$COM_PORT" ]; then
|
||||||
|
echo "Error: No /dev/cu.usbmodem device found."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Using COM Port: $COM_PORT"
|
||||||
|
# Your script logic goes here, using $COM_PORT
|
||||||
|
# For example:
|
||||||
|
# minicom -D $COM_PORT
|
||||||
|
# screen $COM_PORT 9600
|
||||||
|
# echo -e "bootloader" > $COM_PORT
|
||||||
|
|
||||||
sleep 1s
|
# sleep 1s
|
||||||
|
|
||||||
while getopts 'abc:h' opt; do
|
while getopts 'abc:h' opt; do
|
||||||
case "$opt" in
|
case "$opt" in
|
||||||
?|h)
|
?|h)
|
||||||
echo "Usage: $(basename $0) crate_name"
|
echo "Usage: $(basename $0) crate_name"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
shift "$(($OPTIND -1))"
|
shift "$(($OPTIND -1))"
|
||||||
CRATE=$1
|
CRATE=$1
|
||||||
|
|
||||||
|
cargo build -p $CRATE --release
|
||||||
|
arm-none-eabi-objcopy -O ihex target/thumbv7em-none-eabihf/release/$CRATE target/$CRATE.hex
|
||||||
|
adafruit-nrfutil dfu genpkg --dev-type 0x0052 --sd-req 0x0123 --application target/$CRATE.hex target/$CRATE.zip
|
||||||
|
# Use our custom reboot system to boot the controller into serial-only DFU mode.
|
||||||
|
# echo -e "bootloader" > $COM_PORT
|
||||||
|
# Wait for the reboot.
|
||||||
|
# sleep 1s
|
||||||
|
adafruit-nrfutil --verbose dfu serial -pkg target/$CRATE.zip -p $COM_PORT -b 115200 --singlebank
|
||||||
|
fi
|
||||||
|
|
||||||
cargo build -p $CRATE --release
|
|
||||||
arm-none-eabi-objcopy -O ihex target/thumbv7em-none-eabihf/release/$CRATE target/$CRATE.hex
|
|
||||||
adafruit-nrfutil dfu genpkg --dev-type 0x0052 --sd-req 0x0123 --application target/$CRATE.hex target/$CRATE.zip
|
|
||||||
# Use our custom reboot system to boot the controller into serial-only DFU mode.
|
|
||||||
# echo -e "bootloader" > $COM_PORT
|
|
||||||
# Wait for the reboot.
|
|
||||||
# sleep 1s
|
|
||||||
adafruit-nrfutil --verbose dfu serial -pkg target/$CRATE.zip -p $COM_PORT -b 115200 --singlebank
|
|
||||||
8
mac_clear_ble_cache.sh
Executable file
8
mac_clear_ble_cache.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
|
||||||
|
# A script to clear the macOS Bluetooth cache to get the latest BLE service changes.
|
||||||
|
|
||||||
|
sudo pkill bluetoothd
|
||||||
|
sudo rm /Library/Preferences/com.apple.Bluetooth.plist &> /dev/null
|
||||||
|
rm ~/Library/Preferences/ByHost/com.apple.Bluetooth.*.plist &> /dev/null
|
||||||
|
sudo pkill bluetoothd
|
||||||
20
memory-nrf52840.x
Normal file
20
memory-nrf52840.x
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
MEMORY
|
||||||
|
{
|
||||||
|
/* https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsds_s140%2FSDS%2Fs1xx%2Fmem_usage%2Fmem_resource_reqs.html&cp=5_7_4_0_13_0_0 */
|
||||||
|
|
||||||
|
/* Need to leave space for the SoftDevice
|
||||||
|
These values are confirmed working for S140 7.3.0
|
||||||
|
|
||||||
|
They were extracted from the Arduino IDE plugin linked indirectly at https://wiki.seeedstudio.com/XIAO_BLE/
|
||||||
|
*/
|
||||||
|
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000
|
||||||
|
|
||||||
|
/* SRAM required by Softdevice depend on
|
||||||
|
* - Attribute Table Size (Number of Services and Characteristics)
|
||||||
|
* - Vendor UUID count
|
||||||
|
* - Max ATT MTU
|
||||||
|
* - Concurrent connection peripheral + central + secure links
|
||||||
|
* - Event Len, HVN queue, Write CMD queue
|
||||||
|
*/
|
||||||
|
RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000
|
||||||
|
}
|
||||||
33
trouble-example-apps/Cargo.toml
Normal file
33
trouble-example-apps/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "trouble-example-apps"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
trouble-host = { version = "0.2.0", features = ["derive", "scan"] }
|
||||||
|
bt-hci = { version = "0.3.2" }
|
||||||
|
embassy-executor = { version = "0.7.0" }
|
||||||
|
embassy-futures = "0.1.1"
|
||||||
|
embassy-sync = { version = "0.7" }
|
||||||
|
embassy-time = "0.4"
|
||||||
|
embedded-hal = "1.0"
|
||||||
|
static_cell = "2"
|
||||||
|
embedded-io = "0.6"
|
||||||
|
heapless = "0.8"
|
||||||
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
|
defmt = { version = "1.0.1", optional = true }
|
||||||
|
log = { version = "0.4", optional = true }
|
||||||
|
|
||||||
|
|
||||||
|
[features]
|
||||||
|
defmt = [
|
||||||
|
"dep:defmt",
|
||||||
|
"trouble-host/defmt",
|
||||||
|
"bt-hci/defmt",
|
||||||
|
"embedded-io/defmt-03",
|
||||||
|
"embedded-hal/defmt-03",
|
||||||
|
]
|
||||||
|
log = ["dep:log", "trouble-host/log", "bt-hci/log"]
|
||||||
|
security = ["trouble-host/security"]
|
||||||
62
trouble-example-apps/src/ble_advertise.rs
Normal file
62
trouble-example-apps/src/ble_advertise.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
use bt_hci::cmd::le::*;
|
||||||
|
use bt_hci::controller::ControllerCmdSync;
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtAdvData<'t>>
|
||||||
|
+ ControllerCmdSync<LeClearAdvSets>
|
||||||
|
+ ControllerCmdSync<LeSetExtAdvParams>
|
||||||
|
+ ControllerCmdSync<LeSetAdvSetRandomAddr>
|
||||||
|
+ ControllerCmdSync<LeReadNumberOfSupportedAdvSets>
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtAdvEnable<'t>>
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtScanResponseData<'t>>,
|
||||||
|
{
|
||||||
|
let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
info!("Our address = {:?}", address);
|
||||||
|
|
||||||
|
let mut resources: HostResources<DefaultPacketPool, 0, 0> = HostResources::new();
|
||||||
|
let stack = trouble_host::new(controller, &mut resources).set_random_address(address);
|
||||||
|
let Host {
|
||||||
|
mut peripheral,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
let mut adv_data = [0; 31];
|
||||||
|
let len = AdStructure::encode_slice(
|
||||||
|
&[
|
||||||
|
AdStructure::CompleteLocalName(b"Trouble Advert"),
|
||||||
|
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
|
||||||
|
],
|
||||||
|
&mut adv_data[..],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
info!("Starting advertising");
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
loop {
|
||||||
|
let mut params = AdvertisementParameters::default();
|
||||||
|
params.interval_min = Duration::from_millis(100);
|
||||||
|
params.interval_max = Duration::from_millis(100);
|
||||||
|
let _advertiser = peripheral
|
||||||
|
.advertise(
|
||||||
|
¶ms,
|
||||||
|
Advertisement::NonconnectableScannableUndirected {
|
||||||
|
adv_data: &adv_data[..len],
|
||||||
|
scan_data: &[],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
loop {
|
||||||
|
info!("Still running");
|
||||||
|
Timer::after(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
77
trouble-example-apps/src/ble_advertise_multiple.rs
Normal file
77
trouble-example-apps/src/ble_advertise_multiple.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use bt_hci::cmd::le::*;
|
||||||
|
use bt_hci::controller::ControllerCmdSync;
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtAdvData<'t>>
|
||||||
|
+ ControllerCmdSync<LeClearAdvSets>
|
||||||
|
+ ControllerCmdSync<LeSetExtAdvParams>
|
||||||
|
+ ControllerCmdSync<LeSetAdvSetRandomAddr>
|
||||||
|
+ ControllerCmdSync<LeReadNumberOfSupportedAdvSets>
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtAdvEnable<'t>>
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtScanResponseData<'t>>,
|
||||||
|
{
|
||||||
|
let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
info!("Our address = {:?}", address);
|
||||||
|
|
||||||
|
let mut resources: HostResources<DefaultPacketPool, 0, 0, 2> = HostResources::new();
|
||||||
|
let stack = trouble_host::new(controller, &mut resources).set_random_address(address);
|
||||||
|
let Host {
|
||||||
|
mut peripheral,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
let mut adv_data = [0; 31];
|
||||||
|
let len = AdStructure::encode_slice(
|
||||||
|
&[
|
||||||
|
AdStructure::CompleteLocalName(b"Trouble Multiadv"),
|
||||||
|
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
|
||||||
|
],
|
||||||
|
&mut adv_data[..],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut params_1m = AdvertisementParameters::default();
|
||||||
|
params_1m.primary_phy = PhyKind::Le1M;
|
||||||
|
params_1m.secondary_phy = PhyKind::Le1M;
|
||||||
|
params_1m.interval_min = Duration::from_millis(160);
|
||||||
|
params_1m.interval_max = Duration::from_millis(160);
|
||||||
|
|
||||||
|
let mut params_coded = AdvertisementParameters::default();
|
||||||
|
params_coded.primary_phy = PhyKind::LeCoded;
|
||||||
|
params_coded.secondary_phy = PhyKind::LeCoded;
|
||||||
|
params_coded.interval_min = Duration::from_millis(400);
|
||||||
|
params_coded.interval_max = Duration::from_millis(400);
|
||||||
|
let sets = [
|
||||||
|
AdvertisementSet {
|
||||||
|
params: params_1m,
|
||||||
|
data: Advertisement::ExtNonconnectableScannableUndirected {
|
||||||
|
scan_data: &adv_data[..len],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AdvertisementSet {
|
||||||
|
params: params_coded,
|
||||||
|
data: Advertisement::ExtNonconnectableScannableUndirected {
|
||||||
|
scan_data: &adv_data[..len],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut handles = AdvertisementSet::handles(&sets);
|
||||||
|
|
||||||
|
info!("Starting advertising");
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
loop {
|
||||||
|
let _advertiser = peripheral.advertise_ext(&sets, &mut handles).await.unwrap();
|
||||||
|
loop {
|
||||||
|
info!("Still running");
|
||||||
|
Timer::after(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
86
trouble-example-apps/src/ble_bas_central.rs
Normal file
86
trouble-example-apps/src/ble_bas_central.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
/// Max number of connections
|
||||||
|
const CONNECTIONS_MAX: usize = 1;
|
||||||
|
|
||||||
|
/// Max number of L2CAP channels.
|
||||||
|
const L2CAP_CHANNELS_MAX: usize = 3; // Signal + att + CoC
|
||||||
|
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller,
|
||||||
|
{
|
||||||
|
// 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, 0x1b, 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 central,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
// NOTE: Modify this to match the address of the peripheral you want to connect to.
|
||||||
|
// Currently it matches the address used by the peripheral examples
|
||||||
|
let target: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
|
||||||
|
let config = ConnectConfig {
|
||||||
|
connect_params: Default::default(),
|
||||||
|
scan_config: ScanConfig {
|
||||||
|
filter_accept_list: &[(target.kind, &target.addr)],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Scanning for peripheral...");
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
info!("Connecting");
|
||||||
|
|
||||||
|
let conn = central.connect(&config).await.unwrap();
|
||||||
|
info!("Connected, creating gatt client");
|
||||||
|
|
||||||
|
let client = GattClient::<C, DefaultPacketPool, 10>::new(&stack, &conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let _ = join(client.task(), async {
|
||||||
|
info!("Looking for battery service");
|
||||||
|
let services = client.services_by_uuid(&Uuid::new_short(0x180f)).await.unwrap();
|
||||||
|
let service = services.first().unwrap().clone();
|
||||||
|
|
||||||
|
info!("Looking for value handle");
|
||||||
|
let c: Characteristic<u8> = client
|
||||||
|
.characteristic_by_uuid(&service, &Uuid::new_short(0x2a19))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
info!("Subscribing notifications");
|
||||||
|
let mut listener = client.subscribe(&c, false).await.unwrap();
|
||||||
|
|
||||||
|
let _ = join(
|
||||||
|
async {
|
||||||
|
loop {
|
||||||
|
let mut data = [0; 1];
|
||||||
|
client.read_characteristic(&c, &mut data[..]).await.unwrap();
|
||||||
|
info!("Read value: {}", data[0]);
|
||||||
|
Timer::after(Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
loop {
|
||||||
|
let data = listener.next().await;
|
||||||
|
info!("Got notification: {:?} (val: {})", data.as_ref(), data.as_ref()[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
98
trouble-example-apps/src/ble_bas_central_sec.rs
Normal file
98
trouble-example-apps/src/ble_bas_central_sec.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use rand_core::{CryptoRng, RngCore};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
/// Max number of connections
|
||||||
|
const CONNECTIONS_MAX: usize = 1;
|
||||||
|
|
||||||
|
/// Max number of L2CAP channels.
|
||||||
|
const L2CAP_CHANNELS_MAX: usize = 3; // Signal + att + CoC
|
||||||
|
|
||||||
|
pub async fn run<C, RNG>(controller: C, random_generator: &mut RNG)
|
||||||
|
where
|
||||||
|
C: Controller,
|
||||||
|
RNG: RngCore + CryptoRng,
|
||||||
|
{
|
||||||
|
// 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, 0x1b, 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)
|
||||||
|
.set_random_generator_seed(random_generator);
|
||||||
|
|
||||||
|
let Host {
|
||||||
|
mut central,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
// NOTE: Modify this to match the address of the peripheral you want to connect to.
|
||||||
|
// Currently it matches the address used by the peripheral examples
|
||||||
|
let target: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
|
||||||
|
let config = ConnectConfig {
|
||||||
|
connect_params: Default::default(),
|
||||||
|
scan_config: ScanConfig {
|
||||||
|
filter_accept_list: &[(target.kind, &target.addr)],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Scanning for peripheral...");
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
info!("Connecting");
|
||||||
|
|
||||||
|
let conn = central.connect(&config).await.unwrap();
|
||||||
|
info!("Connected, creating gatt client");
|
||||||
|
|
||||||
|
#[cfg(feature = "security")]
|
||||||
|
{
|
||||||
|
if let Err(_error) = central.pairing(&conn).await {
|
||||||
|
error!("Pairing failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = GattClient::<C, DefaultPacketPool, 10>::new(&stack, &conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let _ = join(client.task(), async {
|
||||||
|
info!("Looking for battery service");
|
||||||
|
let services = client.services_by_uuid(&Uuid::new_short(0x180f)).await.unwrap();
|
||||||
|
let service = services.first().unwrap().clone();
|
||||||
|
|
||||||
|
info!("Looking for value handle");
|
||||||
|
let c: Characteristic<u8> = client
|
||||||
|
.characteristic_by_uuid(&service, &Uuid::new_short(0x2a19))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
info!("Subscribing notifications");
|
||||||
|
let mut listener = client.subscribe(&c, false).await.unwrap();
|
||||||
|
|
||||||
|
let _ = join(
|
||||||
|
async {
|
||||||
|
loop {
|
||||||
|
let mut data = [0; 1];
|
||||||
|
client.read_characteristic(&c, &mut data[..]).await.unwrap();
|
||||||
|
info!("Read value: {}", data[0]);
|
||||||
|
Timer::after(Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
loop {
|
||||||
|
let data = listener.next().await;
|
||||||
|
info!("Got notification: {:?} (val: {})", data.as_ref(), data.as_ref()[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
195
trouble-example-apps/src/ble_bas_peripheral.rs
Normal file
195
trouble-example-apps/src/ble_bas_peripheral.rs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_futures::select::select;
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the BLE stack.
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller,
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
228
trouble-example-apps/src/ble_bas_peripheral_sec.rs
Normal file
228
trouble-example-apps/src/ble_bas_peripheral_sec.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_futures::select::select;
|
||||||
|
use embassy_time::Timer;
|
||||||
|
use rand_core::{CryptoRng, RngCore};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the BLE stack.
|
||||||
|
pub async fn run<C, RNG>(controller: C, random_generator: &mut RNG)
|
||||||
|
where
|
||||||
|
C: Controller,
|
||||||
|
RNG: RngCore + CryptoRng,
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
|
.set_random_generator_seed(random_generator);
|
||||||
|
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(
|
||||||
|
server: &Server<'_>,
|
||||||
|
conn: &GattConnection<'_, '_, DefaultPacketPool>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let level = server.battery_service.level;
|
||||||
|
let reason = loop {
|
||||||
|
match conn.next().await {
|
||||||
|
GattConnectionEvent::Disconnected { reason } => break reason,
|
||||||
|
GattConnectionEvent::Gatt { event } => {
|
||||||
|
let result = match &event {
|
||||||
|
GattEvent::Read(event) => {
|
||||||
|
if event.handle() == level.handle {
|
||||||
|
let value = server.get(&level);
|
||||||
|
info!("[gatt] Read Event to Level Characteristic: {:?}", value);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "security")]
|
||||||
|
if conn.raw().encrypted() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(AttErrorCode::INSUFFICIENT_ENCRYPTION)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "security"))]
|
||||||
|
None
|
||||||
|
}
|
||||||
|
GattEvent::Write(event) => {
|
||||||
|
if event.handle() == level.handle {
|
||||||
|
info!(
|
||||||
|
"[gatt] Write Event to Level Characteristic: {:?}",
|
||||||
|
event.data()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "security")]
|
||||||
|
if conn.raw().encrypted() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(AttErrorCode::INSUFFICIENT_ENCRYPTION)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "security"))]
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let reply_result = if let Some(code) = result {
|
||||||
|
event.reject(code)
|
||||||
|
} else {
|
||||||
|
event.accept()
|
||||||
|
};
|
||||||
|
match reply_result {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
trouble-example-apps/src/ble_beacon.rs
Normal file
109
trouble-example-apps/src/ble_beacon.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// BLE beacon example
|
||||||
|
//
|
||||||
|
// A beacon is a device that advertises packets that are constantly being
|
||||||
|
// updated to reflect the current state of the device, but usually does not
|
||||||
|
// accept any conections. This allows broadcasting device information.
|
||||||
|
//
|
||||||
|
|
||||||
|
use bt_hci::cmd::le::*;
|
||||||
|
use bt_hci::controller::ControllerCmdSync;
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
// Use your company ID (register for free with Bluetooth SIG)
|
||||||
|
const COMPANY_ID: u16 = 0xFFFF;
|
||||||
|
|
||||||
|
fn make_adv_payload(start: Instant, update_count: u32) -> [u8; 8] {
|
||||||
|
let mut data = [0u8; 8];
|
||||||
|
let elapsed_ms = Instant::now().duration_since(start).as_millis() as u32;
|
||||||
|
data[0..4].copy_from_slice(&update_count.to_be_bytes());
|
||||||
|
data[4..8].copy_from_slice(&elapsed_ms.to_be_bytes());
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtAdvData<'t>>
|
||||||
|
+ ControllerCmdSync<LeClearAdvSets>
|
||||||
|
+ ControllerCmdSync<LeSetExtAdvParams>
|
||||||
|
+ ControllerCmdSync<LeSetAdvSetRandomAddr>
|
||||||
|
+ ControllerCmdSync<LeReadNumberOfSupportedAdvSets>
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtAdvEnable<'t>>
|
||||||
|
+ for<'t> ControllerCmdSync<LeSetExtScanResponseData<'t>>,
|
||||||
|
{
|
||||||
|
let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
info!("Our address = {:?}", address);
|
||||||
|
|
||||||
|
let mut resources: HostResources<DefaultPacketPool, 0, 0, 27> = HostResources::new();
|
||||||
|
let stack = trouble_host::new(controller, &mut resources).set_random_address(address);
|
||||||
|
let Host {
|
||||||
|
mut peripheral,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
let mut adv_data = [0; 64];
|
||||||
|
let mut update_count = 0u32;
|
||||||
|
let start = Instant::now();
|
||||||
|
let len = AdStructure::encode_slice(
|
||||||
|
&[
|
||||||
|
AdStructure::CompleteLocalName(b"Trouble Beacon"),
|
||||||
|
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
|
||||||
|
AdStructure::ManufacturerSpecificData {
|
||||||
|
company_identifier: COMPANY_ID,
|
||||||
|
payload: &make_adv_payload(start, update_count),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
&mut adv_data[..],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
info!("Starting advertising");
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
loop {
|
||||||
|
let mut params = AdvertisementParameters::default();
|
||||||
|
params.interval_min = Duration::from_millis(25);
|
||||||
|
params.interval_max = Duration::from_millis(150);
|
||||||
|
let _advertiser = peripheral
|
||||||
|
.advertise(
|
||||||
|
¶ms,
|
||||||
|
Advertisement::NonconnectableNonscannableUndirected {
|
||||||
|
adv_data: &adv_data[..len],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
loop {
|
||||||
|
Timer::after(Duration::from_millis(10)).await;
|
||||||
|
update_count = update_count.wrapping_add(1);
|
||||||
|
|
||||||
|
let len = AdStructure::encode_slice(
|
||||||
|
&[
|
||||||
|
AdStructure::CompleteLocalName(b"Trouble Beacon"),
|
||||||
|
AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED),
|
||||||
|
AdStructure::ManufacturerSpecificData {
|
||||||
|
company_identifier: COMPANY_ID,
|
||||||
|
payload: &make_adv_payload(start, update_count),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
&mut adv_data[..],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
peripheral
|
||||||
|
.update_adv_data(Advertisement::NonconnectableNonscannableUndirected {
|
||||||
|
adv_data: &adv_data[..len],
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if update_count % 100 == 0 {
|
||||||
|
info!("Still running: Updated the beacon {} times", update_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
74
trouble-example-apps/src/ble_l2cap_central.rs
Normal file
74
trouble-example-apps/src/ble_l2cap_central.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
use crate::common::PSM_L2CAP_EXAMPLES;
|
||||||
|
|
||||||
|
/// Max number of connections
|
||||||
|
const CONNECTIONS_MAX: usize = 1;
|
||||||
|
|
||||||
|
/// Max number of L2CAP channels.
|
||||||
|
const L2CAP_CHANNELS_MAX: usize = 3; // Signal + att + CoC
|
||||||
|
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller,
|
||||||
|
{
|
||||||
|
// 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, 0x1b, 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 central,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
// NOTE: Modify this to match the address of the peripheral you want to connect to.
|
||||||
|
// Currently, it matches the address used by the peripheral examples
|
||||||
|
let target: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
|
||||||
|
let config = ConnectConfig {
|
||||||
|
connect_params: Default::default(),
|
||||||
|
scan_config: ScanConfig {
|
||||||
|
filter_accept_list: &[(target.kind, &target.addr)],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Scanning for peripheral...");
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
loop {
|
||||||
|
let conn = central.connect(&config).await.unwrap();
|
||||||
|
info!("Connected, creating l2cap channel");
|
||||||
|
const PAYLOAD_LEN: usize = 27;
|
||||||
|
let config = L2capChannelConfig {
|
||||||
|
mtu: Some(PAYLOAD_LEN as u16),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut ch1 = L2capChannel::create(&stack, &conn, PSM_L2CAP_EXAMPLES, &config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
info!("New l2cap channel created, sending some data!");
|
||||||
|
for i in 0..10 {
|
||||||
|
let tx = [i; PAYLOAD_LEN];
|
||||||
|
ch1.send(&stack, &tx).await.unwrap();
|
||||||
|
}
|
||||||
|
info!("Sent data, waiting for them to be sent back");
|
||||||
|
let mut rx = [0; PAYLOAD_LEN];
|
||||||
|
for i in 0..10 {
|
||||||
|
let len = ch1.receive(&stack, &mut rx).await.unwrap();
|
||||||
|
assert_eq!(len, rx.len());
|
||||||
|
assert_eq!(rx, [i; PAYLOAD_LEN]);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Received successfully!");
|
||||||
|
|
||||||
|
Timer::after(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
88
trouble-example-apps/src/ble_l2cap_peripheral.rs
Normal file
88
trouble-example-apps/src/ble_l2cap_peripheral.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
use crate::common::PSM_L2CAP_EXAMPLES;
|
||||||
|
|
||||||
|
/// Max number of connections
|
||||||
|
const CONNECTIONS_MAX: usize = 1;
|
||||||
|
|
||||||
|
/// Max number of L2CAP channels.
|
||||||
|
const L2CAP_CHANNELS_MAX: usize = 3; // Signal + att + CoC
|
||||||
|
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller,
|
||||||
|
{
|
||||||
|
// Hardcoded peripheral address
|
||||||
|
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,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
let mut adv_data = [0; 31];
|
||||||
|
let adv_data_len = AdStructure::encode_slice(
|
||||||
|
&[AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED)],
|
||||||
|
&mut adv_data[..],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut scan_data = [0; 31];
|
||||||
|
let scan_data_len =
|
||||||
|
AdStructure::encode_slice(&[AdStructure::CompleteLocalName(b"Trouble")], &mut scan_data[..]).unwrap();
|
||||||
|
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
loop {
|
||||||
|
info!("Advertising, waiting for connection...");
|
||||||
|
let advertiser = peripheral
|
||||||
|
.advertise(
|
||||||
|
&Default::default(),
|
||||||
|
Advertisement::ConnectableScannableUndirected {
|
||||||
|
adv_data: &adv_data[..adv_data_len],
|
||||||
|
scan_data: &scan_data[..scan_data_len],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let conn = advertiser.accept().await.unwrap();
|
||||||
|
|
||||||
|
info!("Connection established");
|
||||||
|
|
||||||
|
let config = L2capChannelConfig {
|
||||||
|
mtu: Some(PAYLOAD_LEN as u16),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut ch1 = L2capChannel::accept(&stack, &conn, &[PSM_L2CAP_EXAMPLES], &config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
info!("L2CAP channel accepted");
|
||||||
|
|
||||||
|
// Size of payload we're expecting
|
||||||
|
const PAYLOAD_LEN: usize = 27;
|
||||||
|
let mut rx = [0; PAYLOAD_LEN];
|
||||||
|
for i in 0..10 {
|
||||||
|
let len = ch1.receive(&stack, &mut rx).await.unwrap();
|
||||||
|
assert_eq!(len, rx.len());
|
||||||
|
assert_eq!(rx, [i; PAYLOAD_LEN]);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("L2CAP data received, echoing");
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
for i in 0..10 {
|
||||||
|
let tx = [i; PAYLOAD_LEN];
|
||||||
|
ch1.send(&stack, &tx).await.unwrap();
|
||||||
|
}
|
||||||
|
info!("L2CAP data echoed");
|
||||||
|
|
||||||
|
Timer::after(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
66
trouble-example-apps/src/ble_scanner.rs
Normal file
66
trouble-example-apps/src/ble_scanner.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use bt_hci::cmd::le::LeSetScanParams;
|
||||||
|
use bt_hci::controller::ControllerCmdSync;
|
||||||
|
use core::cell::RefCell;
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Timer};
|
||||||
|
use heapless::Deque;
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
/// Max number of connections
|
||||||
|
const CONNECTIONS_MAX: usize = 1;
|
||||||
|
const L2CAP_CHANNELS_MAX: usize = 1;
|
||||||
|
|
||||||
|
pub async fn run<C>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller + ControllerCmdSync<LeSetScanParams>,
|
||||||
|
{
|
||||||
|
// 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, 0x1b, 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 {
|
||||||
|
central, mut runner, ..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
let printer = Printer {
|
||||||
|
seen: RefCell::new(Deque::new()),
|
||||||
|
};
|
||||||
|
let mut scanner = Scanner::new(central);
|
||||||
|
let _ = join(runner.run_with_handler(&printer), async {
|
||||||
|
let mut config = ScanConfig::default();
|
||||||
|
config.active = true;
|
||||||
|
config.phys = PhySet::M1;
|
||||||
|
config.interval = Duration::from_secs(1);
|
||||||
|
config.window = Duration::from_secs(1);
|
||||||
|
let mut _session = scanner.scan(&config).await.unwrap();
|
||||||
|
// Scan forever
|
||||||
|
loop {
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Printer {
|
||||||
|
seen: RefCell<Deque<BdAddr, 128>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventHandler for Printer {
|
||||||
|
fn on_adv_reports(&self, mut it: LeAdvReportsIter<'_>) {
|
||||||
|
let mut seen = self.seen.borrow_mut();
|
||||||
|
while let Some(Ok(report)) = it.next() {
|
||||||
|
if seen.iter().find(|b| b.raw() == report.addr.raw()).is_none() {
|
||||||
|
info!("discovered: {:?}", report.addr);
|
||||||
|
if seen.is_full() {
|
||||||
|
seen.pop_front();
|
||||||
|
}
|
||||||
|
seen.push_back(report.addr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
trouble-example-apps/src/common.rs
Normal file
7
trouble-example-apps/src/common.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// PSM from the dynamic range (0x0080-0x00FF) according to the Bluetooth
|
||||||
|
// Specification for L2CAP channels using LE Credit Based Flow Control mode.
|
||||||
|
// used for the BLE L2CAP examples.
|
||||||
|
//
|
||||||
|
// https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Core-60/out/en/host/logical-link-control-and-adaptation-protocol-specification.html#UUID-1ffdf913-7b8a-c7ba-531e-2a9c6f6da8fb
|
||||||
|
//
|
||||||
|
pub(crate) const PSM_L2CAP_EXAMPLES: u16 = 0x0081;
|
||||||
259
trouble-example-apps/src/fmt.rs
Normal file
259
trouble-example-apps/src/fmt.rs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
#![macro_use]
|
||||||
|
#![allow(unused_macros)]
|
||||||
|
|
||||||
|
use core::fmt::{Debug, Display, LowerHex};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "defmt", feature = "log"))]
|
||||||
|
compile_error!("You may not enable both `defmt` and `log` features.");
|
||||||
|
|
||||||
|
macro_rules! assert {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::assert!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::assert!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! assert_eq {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::assert_eq!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::assert_eq!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! assert_ne {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::assert_ne!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::assert_ne!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! debug_assert {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::debug_assert!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::debug_assert!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! debug_assert_eq {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::debug_assert_eq!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::debug_assert_eq!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! debug_assert_ne {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::debug_assert_ne!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::debug_assert_ne!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! todo {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::todo!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::todo!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
macro_rules! unreachable {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
::core::unreachable!($($x)*)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
macro_rules! unreachable {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
::defmt::unreachable!($($x)*)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! panic {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
{
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
::core::panic!($($x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::panic!($($x)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! trace {
|
||||||
|
($s:literal $(, $x:expr)* $(,)?) => {
|
||||||
|
{
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
::log::trace!($s $(, $x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::trace!($s $(, $x)*);
|
||||||
|
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||||
|
let _ = ($( & $x ),*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! debug {
|
||||||
|
($s:literal $(, $x:expr)* $(,)?) => {
|
||||||
|
{
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
::log::debug!($s $(, $x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::debug!($s $(, $x)*);
|
||||||
|
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||||
|
let _ = ($( & $x ),*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! info {
|
||||||
|
($s:literal $(, $x:expr)* $(,)?) => {
|
||||||
|
{
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
::log::info!($s $(, $x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::info!($s $(, $x)*);
|
||||||
|
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||||
|
let _ = ($( & $x ),*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! warn {
|
||||||
|
($s:literal $(, $x:expr)* $(,)?) => {
|
||||||
|
{
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
::log::warn!($s $(, $x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::warn!($s $(, $x)*);
|
||||||
|
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||||
|
let _ = ($( & $x ),*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! error {
|
||||||
|
($s:literal $(, $x:expr)* $(,)?) => {
|
||||||
|
{
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
::log::error!($s $(, $x)*);
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
::defmt::error!($s $(, $x)*);
|
||||||
|
#[cfg(not(any(feature = "log", feature="defmt")))]
|
||||||
|
let _ = ($( & $x ),*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
macro_rules! unwrap {
|
||||||
|
($($x:tt)*) => {
|
||||||
|
::defmt::unwrap!($($x)*)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "defmt"))]
|
||||||
|
macro_rules! unwrap {
|
||||||
|
($arg:expr) => {
|
||||||
|
match $crate::fmt::Try::into_result($arg) {
|
||||||
|
::core::result::Result::Ok(t) => t,
|
||||||
|
::core::result::Result::Err(e) => {
|
||||||
|
::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($arg:expr, $($msg:expr),+ $(,)? ) => {
|
||||||
|
match $crate::fmt::Try::into_result($arg) {
|
||||||
|
::core::result::Result::Ok(t) => t,
|
||||||
|
::core::result::Result::Err(e) => {
|
||||||
|
::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct NoneError;
|
||||||
|
|
||||||
|
pub trait Try {
|
||||||
|
type Ok;
|
||||||
|
type Error;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn into_result(self) -> Result<Self::Ok, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Try for Option<T> {
|
||||||
|
type Ok = T;
|
||||||
|
type Error = NoneError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_result(self) -> Result<T, NoneError> {
|
||||||
|
self.ok_or(NoneError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Try for Result<T, E> {
|
||||||
|
type Ok = T;
|
||||||
|
type Error = E;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_result(self) -> Self {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) struct Bytes<'a>(pub &'a [u8]);
|
||||||
|
|
||||||
|
impl<'a> Debug for Bytes<'a> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "{:#02x?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Display for Bytes<'a> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "{:#02x?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LowerHex for Bytes<'a> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "{:#02x?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "defmt")]
|
||||||
|
impl<'a> defmt::Format for Bytes<'a> {
|
||||||
|
fn format(&self, fmt: defmt::Formatter) {
|
||||||
|
defmt::write!(fmt, "{:02x}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
128
trouble-example-apps/src/high_throughput_ble_l2cap_central.rs
Normal file
128
trouble-example-apps/src/high_throughput_ble_l2cap_central.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use bt_hci::cmd::le::{LeReadLocalSupportedFeatures, LeSetDataLength, LeSetPhy};
|
||||||
|
use bt_hci::controller::{ControllerCmdAsync, ControllerCmdSync};
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
use crate::common::PSM_L2CAP_EXAMPLES;
|
||||||
|
|
||||||
|
/// Max number of connections
|
||||||
|
const CONNECTIONS_MAX: usize = 1;
|
||||||
|
|
||||||
|
/// Max number of L2CAP channels.
|
||||||
|
const L2CAP_CHANNELS_MAX: usize = 3; // Signal + att + CoC
|
||||||
|
|
||||||
|
pub async fn run<C, P>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller
|
||||||
|
+ ControllerCmdSync<LeSetDataLength>
|
||||||
|
+ ControllerCmdAsync<LeSetPhy>
|
||||||
|
+ ControllerCmdSync<LeReadLocalSupportedFeatures>,
|
||||||
|
P: PacketPool,
|
||||||
|
{
|
||||||
|
// 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, 0x1b, 0x05, 0xe4, 0xff]);
|
||||||
|
info!("Our address = {:?}", address);
|
||||||
|
|
||||||
|
let mut resources: HostResources<P, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = HostResources::new();
|
||||||
|
let stack = trouble_host::new(controller, &mut resources).set_random_address(address);
|
||||||
|
let Host {
|
||||||
|
mut central,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
// NOTE: Modify this to match the address of the peripheral you want to connect to.
|
||||||
|
// Currently, it matches the address used by the peripheral examples
|
||||||
|
let target: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
|
||||||
|
let config = ConnectConfig {
|
||||||
|
connect_params: ConnectParams {
|
||||||
|
min_connection_interval: Duration::from_millis(80),
|
||||||
|
max_connection_interval: Duration::from_millis(80),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
scan_config: ScanConfig {
|
||||||
|
filter_accept_list: &[(target.kind, &target.addr)],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Scanning for peripheral...");
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
loop {
|
||||||
|
// Check that the controller used supports the necessary features for high throughput.
|
||||||
|
let res = stack
|
||||||
|
.command(LeReadLocalSupportedFeatures::new())
|
||||||
|
.await
|
||||||
|
.expect("LeReadLocalSupportedFeatures command failed");
|
||||||
|
assert!(res.supports_le_data_packet_length_extension());
|
||||||
|
assert!(res.supports_le_2m_phy());
|
||||||
|
|
||||||
|
let conn = central.connect(&config).await.expect("Connect failed");
|
||||||
|
info!("Connected, creating l2cap channel");
|
||||||
|
|
||||||
|
// Once connected, request a change in the PDU data length.
|
||||||
|
stack
|
||||||
|
.command(LeSetDataLength::new(conn.handle(), 251, 2120))
|
||||||
|
.await
|
||||||
|
.expect("LeSetDataLength command failed");
|
||||||
|
|
||||||
|
// and request changing the physical link to 2M PHY.
|
||||||
|
// *Note* Change to the PDU data length and PHY can also be initiated by the peripheral.
|
||||||
|
conn.set_phy(&stack, PhyKind::Le2M)
|
||||||
|
.await
|
||||||
|
.expect("set phy command failed");
|
||||||
|
|
||||||
|
const PAYLOAD_LEN: usize = 2510;
|
||||||
|
const L2CAP_MTU: usize = 251;
|
||||||
|
let l2cap_channel_config = L2capChannelConfig {
|
||||||
|
mtu: Some(PAYLOAD_LEN as u16 - 6),
|
||||||
|
mps: Some(L2CAP_MTU as u16 - 4),
|
||||||
|
// Ensure there will be enough credits to send data throughout the entire connection event.
|
||||||
|
flow_policy: CreditFlowPolicy::Every(50),
|
||||||
|
initial_credits: Some(200),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ch1 = L2capChannel::create(&stack, &conn, PSM_L2CAP_EXAMPLES, &l2cap_channel_config)
|
||||||
|
.await
|
||||||
|
.expect("L2capChannel create failed");
|
||||||
|
|
||||||
|
// Wait for the ratios to switch to 2M PHY.
|
||||||
|
// If we do not wait, communication will still occur at 1M for the first 500 ms.
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
info!("New l2cap channel created, sending some data!");
|
||||||
|
|
||||||
|
const NUM_PAYLOADS: u8 = 40;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
for i in 0..NUM_PAYLOADS {
|
||||||
|
let tx = [i; PAYLOAD_LEN];
|
||||||
|
ch1.send(&stack, &tx).await.expect("L2CAP send failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = start.elapsed();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Sent {} bytes at {} kbps, waiting for them to be sent back.",
|
||||||
|
(PAYLOAD_LEN as u64 * NUM_PAYLOADS as u64),
|
||||||
|
((PAYLOAD_LEN as u64 * NUM_PAYLOADS as u64 * 8).div_ceil(duration.as_millis()))
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rx = [0; PAYLOAD_LEN];
|
||||||
|
for i in 0..NUM_PAYLOADS {
|
||||||
|
let len = ch1.receive(&stack, &mut rx).await.expect("L2CAP receive failed");
|
||||||
|
assert_eq!(len, rx.len());
|
||||||
|
assert_eq!(rx, [i; PAYLOAD_LEN]);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Received successfully!");
|
||||||
|
|
||||||
|
Timer::after(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
115
trouble-example-apps/src/high_throughput_ble_l2cap_peripheral.rs
Normal file
115
trouble-example-apps/src/high_throughput_ble_l2cap_peripheral.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use bt_hci::cmd::le::LeReadLocalSupportedFeatures;
|
||||||
|
use bt_hci::controller::ControllerCmdSync;
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_time::{Duration, Instant, Timer};
|
||||||
|
use trouble_host::prelude::*;
|
||||||
|
|
||||||
|
use crate::common::PSM_L2CAP_EXAMPLES;
|
||||||
|
|
||||||
|
/// Max number of connections
|
||||||
|
const CONNECTIONS_MAX: usize = 1;
|
||||||
|
|
||||||
|
/// Max number of L2CAP channels.
|
||||||
|
const L2CAP_CHANNELS_MAX: usize = 3; // Signal + att + CoC
|
||||||
|
|
||||||
|
pub async fn run<C, P>(controller: C)
|
||||||
|
where
|
||||||
|
C: Controller + ControllerCmdSync<LeReadLocalSupportedFeatures>,
|
||||||
|
P: PacketPool,
|
||||||
|
{
|
||||||
|
// Hardcoded peripheral address
|
||||||
|
let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]);
|
||||||
|
info!("Our address = {:?}", address);
|
||||||
|
|
||||||
|
let mut resources: HostResources<P, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = HostResources::new();
|
||||||
|
let stack = trouble_host::new(controller, &mut resources).set_random_address(address);
|
||||||
|
let Host {
|
||||||
|
mut peripheral,
|
||||||
|
mut runner,
|
||||||
|
..
|
||||||
|
} = stack.build();
|
||||||
|
|
||||||
|
let mut adv_data = [0; 31];
|
||||||
|
let adv_data_len = AdStructure::encode_slice(
|
||||||
|
&[AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED)],
|
||||||
|
&mut adv_data[..],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut scan_data = [0; 31];
|
||||||
|
let scan_data_len =
|
||||||
|
AdStructure::encode_slice(&[AdStructure::CompleteLocalName(b"TroubleHT")], &mut scan_data[..]).unwrap();
|
||||||
|
|
||||||
|
let _ = join(runner.run(), async {
|
||||||
|
loop {
|
||||||
|
// Check that the controller used supports the necessary features for high throughput.
|
||||||
|
let res = stack
|
||||||
|
.command(LeReadLocalSupportedFeatures::new())
|
||||||
|
.await
|
||||||
|
.expect("LeReadLocalSupportedFeatures command failed");
|
||||||
|
assert!(res.supports_le_data_packet_length_extension());
|
||||||
|
assert!(res.supports_le_2m_phy());
|
||||||
|
|
||||||
|
info!("Advertising, waiting for connection...");
|
||||||
|
let advertiser = peripheral
|
||||||
|
.advertise(
|
||||||
|
&Default::default(),
|
||||||
|
Advertisement::ConnectableScannableUndirected {
|
||||||
|
adv_data: &adv_data[..adv_data_len],
|
||||||
|
scan_data: &scan_data[..scan_data_len],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Advertising failed");
|
||||||
|
let conn = advertiser.accept().await.expect("Connection failed");
|
||||||
|
|
||||||
|
info!("Connection established");
|
||||||
|
|
||||||
|
const PAYLOAD_LEN: usize = 2510;
|
||||||
|
const L2CAP_MTU: usize = 251;
|
||||||
|
let l2cap_channel_config = L2capChannelConfig {
|
||||||
|
mtu: Some(PAYLOAD_LEN as u16 - 6),
|
||||||
|
mps: Some(L2CAP_MTU as u16 - 4),
|
||||||
|
// Ensure there will be enough credits to send data throughout the entire connection event.
|
||||||
|
flow_policy: CreditFlowPolicy::Every(50),
|
||||||
|
initial_credits: Some(200),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ch1 = L2capChannel::accept(&stack, &conn, &[PSM_L2CAP_EXAMPLES], &l2cap_channel_config)
|
||||||
|
.await
|
||||||
|
.expect("L2capChannel create failed");
|
||||||
|
|
||||||
|
info!("L2CAP channel accepted");
|
||||||
|
|
||||||
|
// Size of payload we're expecting
|
||||||
|
const NUM_PAYLOADS: u8 = 40;
|
||||||
|
let mut rx = [0; PAYLOAD_LEN];
|
||||||
|
for i in 0..NUM_PAYLOADS {
|
||||||
|
let len = ch1.receive(&stack, &mut rx).await.expect("L2CAP receive failed");
|
||||||
|
assert_eq!(len, rx.len());
|
||||||
|
assert_eq!(rx, [i; PAYLOAD_LEN]);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("L2CAP data received, echoing");
|
||||||
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
for i in 0..NUM_PAYLOADS {
|
||||||
|
let tx = [i; PAYLOAD_LEN];
|
||||||
|
ch1.send(&stack, &tx).await.expect("L2CAP send failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = start.elapsed();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"L2CAP data of {} bytes echoed at {} kbps.",
|
||||||
|
(PAYLOAD_LEN as u64 * NUM_PAYLOADS as u64),
|
||||||
|
((PAYLOAD_LEN as u64 * NUM_PAYLOADS as u64 * 8).div_ceil(duration.as_millis()))
|
||||||
|
);
|
||||||
|
|
||||||
|
Timer::after(Duration::from_secs(60)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
18
trouble-example-apps/src/lib.rs
Normal file
18
trouble-example-apps/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#![no_std]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
pub(crate) mod common;
|
||||||
|
pub(crate) mod fmt;
|
||||||
|
|
||||||
|
pub mod ble_advertise;
|
||||||
|
pub mod ble_advertise_multiple;
|
||||||
|
pub mod ble_bas_central;
|
||||||
|
pub mod ble_bas_central_sec;
|
||||||
|
pub mod ble_bas_peripheral;
|
||||||
|
pub mod ble_bas_peripheral_sec;
|
||||||
|
pub mod ble_beacon;
|
||||||
|
pub mod ble_l2cap_central;
|
||||||
|
pub mod ble_l2cap_peripheral;
|
||||||
|
pub mod ble_scanner;
|
||||||
|
pub mod high_throughput_ble_l2cap_central;
|
||||||
|
pub mod high_throughput_ble_l2cap_peripheral;
|
||||||
Reference in New Issue
Block a user