diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index aaf1f63..1f1be42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ on: pull_request: jobs: - ci: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -24,7 +24,19 @@ jobs: - name: Test run: | cd host - cargo test -- --nocapture + cargo test --lib -- --nocapture - name: Build examples run: for i in nrf-sdc; do pushd examples/$i; cargo fmt --check && cargo clippy && cargo build --release; popd; done; + #integration-tests: + # runs-on: self-hosted + # steps: + # - uses: actions/checkout@v4 + # - name: Build + # env: + # TEST_ADAPTER_ONE: /dev/ttyACM0 + # TEST_ADAPTER_TWO: /dev/ttyACM1 + # RUST_LOG: info + # run: | + # cd host + # cargo test --test '*' -- --nocapture diff --git a/examples/nrf-sdc/src/bin/ble_bas_peripheral.rs b/examples/nrf-sdc/src/bin/ble_bas_peripheral.rs index ded0a3b..c728257 100644 --- a/examples/nrf-sdc/src/bin/ble_bas_peripheral.rs +++ b/examples/nrf-sdc/src/bin/ble_bas_peripheral.rs @@ -2,6 +2,7 @@ #![no_main] #![feature(impl_trait_in_assoc_type)] +use bt_hci::cmd::le::LeSetRandomAddr; use bt_hci::cmd::SyncCmd; use bt_hci::param::BdAddr; use defmt::{error, info, unwrap}; @@ -12,7 +13,6 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_time::{Duration, Timer}; use nrf_sdc::{self as sdc, mpsl, mpsl::MultiprotocolServiceLayer}; use sdc::rng_pool::RngPool; -use sdc::vendor::ZephyrWriteBdAddr; use static_cell::StaticCell; use trouble_host::{ adapter::{Adapter, HostResources}, @@ -40,8 +40,8 @@ async fn mpsl_task(mpsl: &'static MultiprotocolServiceLayer<'static>) -> ! { fn bd_addr() -> BdAddr { unsafe { let ficr = &*pac::FICR::ptr(); - let high = u64::from((ficr.deviceid[1].read().bits() & 0x0000ffff) | 0x0000c000); - let addr = high << 32 | u64::from(ficr.deviceid[0].read().bits()); + let high = u64::from((ficr.deviceaddr[1].read().bits() & 0x0000ffff) | 0x0000c000); + let addr = high << 32 | u64::from(ficr.deviceaddr[0].read().bits()); BdAddr::new(unwrap!(addr.to_le_bytes()[..6].try_into())) } } @@ -108,8 +108,8 @@ async fn main(spawner: Spawner) { let mut sdc_mem = sdc::Mem::<3312>::new(); let sdc = unwrap!(build_sdc(sdc_p, &rng, mpsl, &mut sdc_mem)); - info!("Advertising as {:02x}", bd_addr()); - unwrap!(ZephyrWriteBdAddr::new(bd_addr()).exec(&sdc).await); + info!("Our address = {:02x}", bd_addr()); + unwrap!(LeSetRandomAddr::new(bd_addr()).exec(&sdc).await); Timer::after(Duration::from_millis(200)).await; static HOST_RESOURCES: StaticCell> = @@ -119,11 +119,11 @@ async fn main(spawner: Spawner) { let adapter: Adapter<'_, NoopRawMutex, _, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = Adapter::new(sdc, host_resources); let config = AdvertiseConfig { params: None, - data: &[ + adv_data: &[ AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), AdStructure::ServiceUuids16(&[Uuid::Uuid16([0x0f, 0x18])]), - AdStructure::CompleteLocalName("Trouble"), ], + scan_data: &[AdStructure::CompleteLocalName(b"Trouble")], }; let mut table: AttributeTable<'_, NoopRawMutex, 10> = AttributeTable::new(); diff --git a/examples/nrf-sdc/src/bin/ble_l2cap_central.rs b/examples/nrf-sdc/src/bin/ble_l2cap_central.rs index 03e93a4..53a4536 100644 --- a/examples/nrf-sdc/src/bin/ble_l2cap_central.rs +++ b/examples/nrf-sdc/src/bin/ble_l2cap_central.rs @@ -2,6 +2,7 @@ #![no_main] #![feature(impl_trait_in_assoc_type)] +use bt_hci::cmd::le::LeSetRandomAddr; use bt_hci::cmd::SyncCmd; use bt_hci::param::BdAddr; use defmt::{info, unwrap}; @@ -12,7 +13,6 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_time::{Duration, Timer}; use nrf_sdc::{self as sdc, mpsl, mpsl::MultiprotocolServiceLayer}; use sdc::rng_pool::RngPool; -use sdc::vendor::ZephyrWriteBdAddr; use static_cell::StaticCell; use trouble_host::{ adapter::{Adapter, HostResources}, @@ -41,8 +41,8 @@ async fn mpsl_task(mpsl: &'static MultiprotocolServiceLayer<'static>) -> ! { fn bd_addr() -> BdAddr { unsafe { let ficr = &*pac::FICR::ptr(); - let high = u64::from((ficr.deviceid[1].read().bits() & 0x0000ffff) | 0x0000c000); - let addr = high << 32 | u64::from(ficr.deviceid[0].read().bits()); + let high = u64::from((ficr.deviceaddr[1].read().bits() & 0x0000ffff) | 0x0000c000); + let addr = high << 32 | u64::from(ficr.deviceaddr[0].read().bits()); BdAddr::new(unwrap!(addr.to_le_bytes()[..6].try_into())) } } @@ -117,7 +117,7 @@ async fn main(spawner: Spawner) { let sdc = unwrap!(build_sdc(sdc_p, &rng, mpsl, &mut sdc_mem)); info!("Our address = {:02x}", bd_addr()); - unwrap!(ZephyrWriteBdAddr::new(bd_addr()).exec(&sdc).await); + unwrap!(LeSetRandomAddr::new(bd_addr()).exec(&sdc).await); Timer::after(Duration::from_millis(200)).await; static HOST_RESOURCES: StaticCell> = @@ -126,7 +126,10 @@ async fn main(spawner: Spawner) { let adapter: Adapter<'_, NoopRawMutex, _, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = Adapter::new(sdc, host_resources); - let config = ScanConfig { params: None }; + let config = ScanConfig { + params: None, + filter_accept_list: &[], + }; // NOTE: Modify this to match the address of the peripheral you want to connect to let target: BdAddr = BdAddr::new([0xf5, 0x9f, 0x1a, 0x05, 0xe4, 0xee]); diff --git a/examples/nrf-sdc/src/bin/ble_l2cap_peripheral.rs b/examples/nrf-sdc/src/bin/ble_l2cap_peripheral.rs index dceb7bb..c0a1d09 100644 --- a/examples/nrf-sdc/src/bin/ble_l2cap_peripheral.rs +++ b/examples/nrf-sdc/src/bin/ble_l2cap_peripheral.rs @@ -2,6 +2,7 @@ #![no_main] #![feature(impl_trait_in_assoc_type)] +use bt_hci::cmd::le::LeSetRandomAddr; use bt_hci::cmd::SyncCmd; use bt_hci::param::BdAddr; use defmt::{info, unwrap}; @@ -12,7 +13,6 @@ use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_time::{Duration, Timer}; use nrf_sdc::{self as sdc, mpsl, mpsl::MultiprotocolServiceLayer}; use sdc::rng_pool::RngPool; -use sdc::vendor::ZephyrWriteBdAddr; use static_cell::StaticCell; use trouble_host::{ adapter::{Adapter, HostResources}, @@ -40,8 +40,8 @@ async fn mpsl_task(mpsl: &'static MultiprotocolServiceLayer<'static>) -> ! { fn bd_addr() -> BdAddr { unsafe { let ficr = &*pac::FICR::ptr(); - let high = u64::from((ficr.deviceid[1].read().bits() & 0x0000ffff) | 0x0000c000); - let addr = high << 32 | u64::from(ficr.deviceid[0].read().bits()); + let high = u64::from((ficr.deviceaddr[1].read().bits() & 0x0000ffff) | 0x0000c000); + let addr = high << 32 | u64::from(ficr.deviceaddr[0].read().bits()); BdAddr::new(unwrap!(addr.to_le_bytes()[..6].try_into())) } } @@ -116,7 +116,7 @@ async fn main(spawner: Spawner) { let sdc = unwrap!(build_sdc(sdc_p, &rng, mpsl, &mut sdc_mem)); info!("Our address = {:02x}", bd_addr()); - unwrap!(ZephyrWriteBdAddr::new(bd_addr()).exec(&sdc).await); + unwrap!(LeSetRandomAddr::new(bd_addr()).exec(&sdc).await); Timer::after(Duration::from_millis(200)).await; static HOST_RESOURCES: StaticCell> = @@ -127,10 +127,8 @@ async fn main(spawner: Spawner) { let config = AdvertiseConfig { params: None, - data: &[ - AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), - AdStructure::CompleteLocalName("Trouble"), - ], + adv_data: &[AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED)], + scan_data: &[AdStructure::CompleteLocalName(b"Trouble")], }; let _ = join(adapter.run(), async { diff --git a/examples/rp-pico-w/.cargo/config.toml b/examples/rp-pico-w/.cargo/config.toml deleted file mode 100644 index 3d7d617..0000000 --- a/examples/rp-pico-w/.cargo/config.toml +++ /dev/null @@ -1,8 +0,0 @@ -[target.'cfg(all(target_arch = "arm", target_os = "none"))'] -runner = "probe-rs run --chip RP2040" - -[build] -target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ - -[env] -DEFMT_LOG = "debug" diff --git a/examples/rp-pico-w/Cargo.toml b/examples/rp-pico-w/Cargo.toml deleted file mode 100644 index 6b73bf7..0000000 --- a/examples/rp-pico-w/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "rp-pico-w" -version = "0.1.0" -edition = "2021" - -[dependencies] -bt-hci = { version = "0.1.0", default-features = false, features = ["defmt"] } -trouble-host = { version = "0.1.0", path = "../../host", features = ["defmt"] } - -embassy-embedded-hal = { version = "0.1.0", features = ["defmt"] } -embassy-sync = { version = "0.5.0", features = ["defmt"] } -embassy-executor = { version = "0.5.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "integrated-timers"] } -embassy-time = { version = "0.3.0", features = ["defmt-timestamp-uptime"] } -embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-pac", "time-driver", "critical-section-impl"] } -embassy-futures = { version = "0.1.0" } -cyw43 = { version = "0.1.0", features = ["firmware-logs"] } -cyw43-pio = { version = "0.1.0", features = ["overclock"] } - -defmt = "0.3" -defmt-rtt = "0.4" -fixed = "1.23.1" -fixed-macro = "1.2" - -#cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } -cortex-m = { version = "0.7.6", features = ["inline-asm"] } -cortex-m-rt = "0.7.0" -panic-probe = { version = "0.3", features = ["print-defmt"] } -futures = { version = "0.3.17", default-features = false, features = ["async-await", "cfg-target-has-atomic", "unstable"] } -byte-slice-cast = { version = "1.2.0", default-features = false } -heapless = "0.8" -usbd-hid = "0.7.0" - -embedded-hal-1 = { package = "embedded-hal", version = "1.0" } -embedded-hal-async = "1.0" -embedded-hal-bus = { version = "0.1", features = ["async"] } -embedded-io-async = { version = "0.6.1", features = ["defmt-03"] } -embedded-io = { version = "0.6.1", features = ["defmt-03"] } -embedded-storage = { version = "0.3" } -static_cell = "2" -portable-atomic = { version = "1.5", features = ["critical-section"] } -pio-proc = "0.2" -pio = "0.2.1" -rand = { version = "0.8.5", default-features = false } - -[profile.release] -debug = 2 - -[patch.crates-io] -#bt-hci = { git = "https://github.com/alexmoon/bt-hci.git", branch = "serial-controller" } -embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -embassy-time = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -embassy-rp = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -embassy-futures = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -cyw43 = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -cyw43-pio = { git = "https://github.com/embassy-rs/embassy.git", branch = "pico-bluetooth" } -#embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" } -bt-hci = { path = "../../../bt-hci" } diff --git a/examples/rp-pico-w/build.rs b/examples/rp-pico-w/build.rs deleted file mode 100644 index 3f915f9..0000000 --- a/examples/rp-pico-w/build.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! This build script copies the `memory.x` file from the crate root into -//! a directory where the linker can always find it at build time. -//! For many projects this is optional, as the linker always searches the -//! project root directory -- wherever `Cargo.toml` is. However, if you -//! are using a workspace or have a more complicated build setup, this -//! build script becomes required. Additionally, by requesting that -//! Cargo re-run the build script whenever `memory.x` is changed, -//! updating `memory.x` ensures a rebuild of the application with the -//! new memory settings. - -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; - -fn main() { - // Put `memory.x` in our output directory and ensure it's - // on the linker search path. - let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); - File::create(out.join("memory.x")) - .unwrap() - .write_all(include_bytes!("memory.x")) - .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=-Tlink-rp.x"); - println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); -} diff --git a/examples/rp-pico-w/memory.x b/examples/rp-pico-w/memory.x deleted file mode 100644 index ef19dff..0000000 --- a/examples/rp-pico-w/memory.x +++ /dev/null @@ -1,17 +0,0 @@ -MEMORY { - BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 - FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 - - /* Pick one of the two options for RAM layout */ - - /* OPTION A: Use all RAM banks as one big block */ - /* Reasonable, unless you are doing something */ - /* really particular with DMA or other concurrent */ - /* access that would benefit from striping */ - RAM : ORIGIN = 0x20000000, LENGTH = 264K - - /* OPTION B: Keep the unstriped sections separate */ - /* RAM: ORIGIN = 0x20000000, LENGTH = 256K */ - /* SCRATCH_A: ORIGIN = 0x20040000, LENGTH = 4K */ - /* SCRATCH_B: ORIGIN = 0x20041000, LENGTH = 4K */ -} diff --git a/examples/rp-pico-w/src/main.rs b/examples/rp-pico-w/src/main.rs deleted file mode 100644 index 79c74af..0000000 --- a/examples/rp-pico-w/src/main.rs +++ /dev/null @@ -1,167 +0,0 @@ -#![no_std] -#![no_main] -use bt_hci::driver::{HciController, HciDriver}; -use bt_hci::{ - data, param, Controller, ControllerCmdAsync, ControllerCmdSync, ControllerToHostPacket, FromHciBytes, PacketKind, - ReadHci, WithIndicator, WriteHci, -}; -use core::cell::RefCell; -use core::future::{pending, Future}; -use core::ops::DerefMut; -use cyw43_pio::PioSpi; -use defmt::{assert_eq, todo, *}; -use embassy_executor::{Executor, Spawner}; -use embassy_futures::join::join3; -use embassy_futures::yield_now; -use embassy_rp::bind_interrupts; -use embassy_rp::gpio::{Level, Output}; -use embassy_rp::peripherals::{DMA_CH0, PIO0}; -use embassy_rp::pio::{InterruptHandler, Pio}; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_sync::mutex::Mutex; -use embassy_time::{Duration, Timer}; -use embedded_io_async::Read; -use static_cell::StaticCell; -use trouble_host::adapter::{Adapter, HostResources}; -use trouble_host::advertise::{AdStructure, AdvertiseConfig, BR_EDR_NOT_SUPPORTED, LE_GENERAL_DISCOVERABLE}; -use trouble_host::attribute::{AttributeTable, CharacteristicProp, Service, Uuid}; -use trouble_host::PacketQos; -use {defmt_rtt as _, embassy_time as _, panic_probe as _}; - -bind_interrupts!(struct Irqs { - PIO0_IRQ_0 => InterruptHandler; -}); - -#[embassy_executor::main] -async fn main(spawner: Spawner) { - let p = embassy_rp::init(Default::default()); - let fw = include_bytes!("../../../../embassy/cyw43-firmware/43439A0.bin"); - let clm = include_bytes!("../../../../embassy/cyw43-firmware/43439A0_clm.bin"); - let btfw = include_bytes!("../../../../embassy/cyw43-firmware/43439A0_btfw.bin"); - - // To make flashing faster for development, you may want to flash the firmwares independently - // at hardcoded addresses, instead of baking them into the program with `include_bytes!`: - // probe-rs download 43439A0.bin --format bin --chip RP2040 --base-address 0x10100000 - // probe-rs download 43439A0_clm.bin --format bin --chip RP2040 --base-address 0x10140000 - //let fw = unsafe { core::slice::from_raw_parts(0x10100000 as *const u8, 224190) }; - //let clm = unsafe { core::slice::from_raw_parts(0x10140000 as *const u8, 4752) }; - - let pwr = Output::new(p.PIN_23, Level::Low); - let cs = Output::new(p.PIN_25, Level::High); - let mut pio = Pio::new(p.PIO0, Irqs); - let spi = PioSpi::new(&mut pio.common, pio.sm0, pio.irq0, cs, p.PIN_24, p.PIN_29, p.DMA_CH0); - - static STATE: StaticCell = StaticCell::new(); - let state = STATE.init(cyw43::State::new()); - let (_net_device, mut control, runner) = cyw43::new_with_bluetooth(state, pwr, spi, fw, btfw).await; - - let driver = PicoWController::new(runner); - let controller: HciController<_, 10> = HciController::new(driver); - static HOST_RESOURCES: StaticCell> = StaticCell::new(); - let host_resources = HOST_RESOURCES.init(HostResources::new(PacketQos::None)); - - let adapter: Adapter<'_, NoopRawMutex, _, 2, 4, 1, 1> = Adapter::new(controller, host_resources); - let config = AdvertiseConfig { - params: None, - data: &[ - AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), - AdStructure::ServiceUuids16(&[Uuid::Uuid16([0x0f, 0x18])]), - AdStructure::CompleteLocalName("Trouble PicoW"), - ], - }; - - let mut table: AttributeTable<'_, NoopRawMutex, 10> = AttributeTable::new(); - - // Generic Access Service (mandatory) - let id = b"Trouble PicoW"; - let appearance = [0x80, 0x07]; - let mut bat_level = [0; 1]; - let handle = { - let mut svc = table.add_service(Service::new(0x1800)); - let _ = svc.add_characteristic_ro(0x2a00, id); - let _ = svc.add_characteristic_ro(0x2a01, &appearance[..]); - drop(svc); - - // Generic attribute service (mandatory) - table.add_service(Service::new(0x1801)); - - // Battery service - let mut svc = table.add_service(Service::new(0x180f)); - - svc.add_characteristic( - 0x2a19, - &[CharacteristicProp::Read, CharacteristicProp::Notify], - &mut bat_level, - ) - }; - - let server = adapter.gatt_server(&table); - - info!("Starting advertising and GATT service"); - let _ = join3( - adapter.run(), - async { - loop { - match server.next().await { - Ok(event) => { - info!("Gatt event: {:?}", event); - } - Err(e) => { - error!("Error processing GATT events: {:?}", e); - } - } - } - }, - async { - let conn = adapter.advertise(&config).await.unwrap(); - // Keep connection alive - let mut tick: u8 = 0; - loop { - Timer::after(Duration::from_secs(10)).await; - tick += 1; - server.notify(handle, &conn, &[tick]).await.unwrap(); - } - }, - ) - .await; -} - -struct PicoWController { - runner: Mutex, PioSpi<'static, PIO0, 0, DMA_CH0>>>, -} - -impl PicoWController { - pub fn new(runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>) -> Self { - Self { - runner: Mutex::new(runner), - } - } -} - -#[derive(Debug, defmt::Format)] -pub struct Error; - -impl embedded_io::Error for Error { - fn kind(&self) -> embedded_io::ErrorKind { - embedded_io::ErrorKind::Other - } -} - -impl HciDriver for PicoWController { - type Error = Error; - fn read(&self, rx: &mut [u8]) -> impl Future> { - async { - let mut runner = self.runner.lock().await; - let n = runner.hci_read(rx).await as usize; - Ok(n) - } - } - - fn write(&self, tx: &[u8]) -> impl Future> { - async { - let mut runner = self.runner.lock().await; - runner.hci_write(tx).await; - Ok(()) - } - } -} diff --git a/examples/serial-hci/src/main.rs b/examples/serial-hci/src/main.rs index b111af4..0a70584 100644 --- a/examples/serial-hci/src/main.rs +++ b/examples/serial-hci/src/main.rs @@ -64,11 +64,11 @@ async fn main() { let adapter: Adapter<'_, NoopRawMutex, _, 2, 4, 1, 1> = Adapter::new(controller, host_resources); let config = AdvertiseConfig { params: None, - data: &[ + adv_data: &[ AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), AdStructure::ServiceUuids16(&[Uuid::Uuid16([0x0f, 0x18])]), - AdStructure::CompleteLocalName("Trouble HCI"), ], + scan_data: &[AdStructure::CompleteLocalName(b"Trouble HCI")], }; let mut table: AttributeTable<'_, NoopRawMutex, 10> = AttributeTable::new(); diff --git a/host/Cargo.toml b/host/Cargo.toml index b04154b..23689b2 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -29,6 +29,14 @@ futures-intrusive = { version = "0.5.0", default-features = false } log = { version = "0.4.16", optional = true } defmt = {version = "0.3", optional = true } +[dev-dependencies] +tokio = { version = "1", features = ["full"] } +embedded-io-adapters = { version = "0.6.1", features = ["tokio-1"] } +embedded-io-async = { version = "0.6.1" } +tokio-serial = "5.4" +env_logger = "0.11" +critical-section = { version = "1", features = ["std"] } + [features] defmt = [ "dep:defmt" ] diff --git a/host/src/adapter.rs b/host/src/adapter.rs index ba6dbd2..5ad0427 100644 --- a/host/src/adapter.rs +++ b/host/src/adapter.rs @@ -14,8 +14,8 @@ use crate::types::l2cap::L2capLeSignal; use crate::{AdapterError, Error}; use bt_hci::cmd::controller_baseband::{Reset, SetEventMask}; use bt_hci::cmd::le::{ - LeCreateConn, LeCreateConnParams, LeReadBufferSize, LeSetAdvData, LeSetAdvEnable, LeSetAdvParams, LeSetScanEnable, - LeSetScanParams, + LeAddDeviceToFilterAcceptList, LeClearFilterAcceptList, LeCreateConn, LeCreateConnParams, LeReadBufferSize, + LeSetAdvData, LeSetAdvEnable, LeSetAdvParams, LeSetScanEnable, LeSetScanParams, LeSetScanResponseData, }; use bt_hci::cmd::link_control::{Disconnect, DisconnectParams}; use bt_hci::cmd::{AsyncCmd, SyncCmd}; @@ -26,6 +26,7 @@ use bt_hci::event::le::LeEvent; use bt_hci::event::Event; use bt_hci::param::{BdAddr, ConnHandle, DisconnectReason, EventMask}; use bt_hci::ControllerToHostPacket; +use core::task::Poll; use embassy_futures::select::{select, Either}; use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_sync::channel::Channel; @@ -104,16 +105,33 @@ where /// Performs a BLE scan, return a report for discovering peripherals. /// /// Scan is stopped when a report is received. Call this method repeatedly to continue scanning. - pub async fn scan(&self, config: &ScanConfig) -> Result> + pub async fn scan(&self, config: &ScanConfig<'_>) -> Result> where - T: ControllerCmdSync + ControllerCmdSync, + T: ControllerCmdSync + + ControllerCmdSync + + ControllerCmdSync + + ControllerCmdSync, { + LeClearFilterAcceptList::new().exec(&self.controller).await?; + + if !config.filter_accept_list.is_empty() { + for entry in config.filter_accept_list { + LeAddDeviceToFilterAcceptList::new(entry.0, *entry.1) + .exec(&self.controller) + .await?; + } + } + let params = config.params.unwrap_or(LeSetScanParams::new( - bt_hci::param::LeScanKind::Passive, + bt_hci::param::LeScanKind::Active, bt_hci::param::Duration::from_millis(1_000), bt_hci::param::Duration::from_millis(1_000), - bt_hci::param::AddrKind::PUBLIC, - bt_hci::param::ScanningFilterPolicy::BasicUnfiltered, + bt_hci::param::AddrKind::RANDOM, + if config.filter_accept_list.is_empty() { + bt_hci::param::ScanningFilterPolicy::BasicUnfiltered + } else { + bt_hci::param::ScanningFilterPolicy::BasicFiltered + }, )); params.exec(&self.controller).await?; @@ -130,14 +148,17 @@ where /// in which case a handle for the connection is returned. pub async fn advertise<'m>(&'m self, config: &AdvertiseConfig<'_>) -> Result, AdapterError> where - T: ControllerCmdSync + ControllerCmdSync + ControllerCmdSync, + T: ControllerCmdSync + + ControllerCmdSync + + ControllerCmdSync + + ControllerCmdSync, { let params = &config.params.unwrap_or(LeSetAdvParams::new( bt_hci::param::Duration::from_millis(400), bt_hci::param::Duration::from_millis(400), bt_hci::param::AdvKind::AdvInd, - bt_hci::param::AddrKind::PUBLIC, - bt_hci::param::AddrKind::PUBLIC, + bt_hci::param::AddrKind::RANDOM, + bt_hci::param::AddrKind::RANDOM, BdAddr::default(), bt_hci::param::AdvChannelMap::ALL, bt_hci::param::AdvFilterPolicy::default(), @@ -145,13 +166,28 @@ where params.exec(&self.controller).await?; - let mut data = [0; 31]; - let mut w = WriteCursor::new(&mut data[..]); - for item in config.data.iter() { - item.encode(&mut w)?; + if !config.adv_data.is_empty() { + let mut data = [0; 31]; + let mut w = WriteCursor::new(&mut data[..]); + for item in config.adv_data.iter() { + item.encode(&mut w)?; + } + let len = w.len(); + LeSetAdvData::new(len as u8, data).exec(&self.controller).await?; } - let len = w.len(); - LeSetAdvData::new(len as u8, data).exec(&self.controller).await?; + + if !config.scan_data.is_empty() { + let mut data = [0; 31]; + let mut w = WriteCursor::new(&mut data[..]); + for item in config.scan_data.iter() { + item.encode(&mut w)?; + } + let len = w.len(); + LeSetScanResponseData::new(len as u8, data) + .exec(&self.controller) + .await?; + } + LeSetAdvEnable::new(true).exec(&self.controller).await?; let conn = Connection::accept(self).await; LeSetAdvEnable::new(false).exec(&self.controller).await?; @@ -197,7 +233,9 @@ where } other if other >= L2CAP_CID_DYN_START => match self.channels.dispatch(packet).await { - Ok(_) => {} + Ok(_) => { + info!("L2CAP packet dispatched!"); + } Err(e) => { warn!("Error dispatching l2cap packet to channel: {:?}", e); } @@ -235,6 +273,7 @@ where Ok(ControllerToHostPacket::Event(event)) => match event { Event::Le(event) => match event { LeEvent::LeConnectionComplete(e) => { + warn!("CONNECTION COMPLET!"); if let Err(err) = self.connections.connect( e.handle, ConnectionInfo { @@ -359,11 +398,29 @@ where } pub struct HciController<'d, T: Controller> { - controller: &'d T, - permits: &'d LocalSemaphore, + pub(crate) controller: &'d T, + pub(crate) permits: &'d LocalSemaphore, } impl<'d, T: Controller> HciController<'d, T> { + pub(crate) fn try_send(&self, handle: ConnHandle, pdu: &[u8]) -> Result<(), AdapterError> { + let permit = self + .permits + .try_acquire(1) + .ok_or::>(Error::Busy.into())?; + let acl = AclPacket::new( + handle, + AclPacketBoundary::FirstNonFlushable, + AclBroadcastFlag::PointToPoint, + pdu, + ); + let fut = self.controller.write_acl_data(&acl); + match embassy_futures::poll_once(fut) { + Poll::Ready(result) => result.map_err(AdapterError::Controller), + Poll::Pending => Err(Error::Busy.into()), + } + } + pub(crate) async fn send(&self, handle: ConnHandle, pdu: &[u8]) -> Result<(), AdapterError> { self.permits.acquire(1).await.disarm(); let acl = AclPacket::new( diff --git a/host/src/advertise.rs b/host/src/advertise.rs index f07537a..0ec157f 100644 --- a/host/src/advertise.rs +++ b/host/src/advertise.rs @@ -1,9 +1,14 @@ -use crate::{codec, cursor::WriteCursor, types::uuid::Uuid}; +use crate::{ + codec, + cursor::{ReadCursor, WriteCursor}, + types::uuid::Uuid, +}; use bt_hci::cmd::le::LeSetAdvParams; pub struct AdvertiseConfig<'d> { pub params: Option, - pub data: &'d [AdStructure<'d>], + pub adv_data: &'d [AdStructure<'d>], + pub scan_data: &'d [AdStructure<'d>], } pub const AD_FLAG_LE_LIMITED_DISCOVERABLE: u8 = 0b00000001; @@ -42,10 +47,10 @@ pub enum AdStructure<'a> { /// Sets the full (unabbreviated) device name. /// /// This will be shown to the user when this device is found. - CompleteLocalName(&'a str), + CompleteLocalName(&'a [u8]), /// Sets the shortened device name. - ShortenedLocalName(&'a str), + ShortenedLocalName(&'a [u8]), /// Set manufacturer specific data ManufacturerSpecificData { @@ -82,11 +87,11 @@ impl<'d> AdStructure<'d> { } AdStructure::ShortenedLocalName(name) => { w.append(&[(name.len() + 1) as u8, 0x08])?; - w.append(name.as_bytes())?; + w.append(name)?; } AdStructure::CompleteLocalName(name) => { w.append(&[(name.len() + 1) as u8, 0x09])?; - w.append(name.as_bytes())?; + w.append(name)?; } AdStructure::ServiceData16 { uuid, data } => { w.append(&[(data.len() + 3) as u8, 0x16])?; @@ -108,4 +113,42 @@ impl<'d> AdStructure<'d> { } Ok(()) } + + pub fn decode(data: &[u8]) -> impl Iterator, codec::Error>> { + AdStructureIter { + cursor: ReadCursor::new(data), + } + } +} + +pub struct AdStructureIter<'d> { + cursor: ReadCursor<'d>, +} + +impl<'d> AdStructureIter<'d> { + fn read(&mut self) -> Result, codec::Error> { + let len: u8 = self.cursor.read()?; + let code: u8 = self.cursor.read()?; + let data = self.cursor.slice(len as usize - 1)?; + match code { + 0x01 => Ok(AdStructure::Flags(data[0])), + // 0x02 => unimplemented!(), + // 0x07 => unimplemented!(), + 0x08 => Ok(AdStructure::ShortenedLocalName(data)), + 0x09 => Ok(AdStructure::CompleteLocalName(data)), + // 0x16 => unimplemented!(), + // 0xff => unimplemented!(), + ty => Ok(AdStructure::Unknown { ty, data }), + } + } +} + +impl<'d> Iterator for AdStructureIter<'d> { + type Item = Result, codec::Error>; + fn next(&mut self) -> Option { + if self.cursor.available() == 0 { + return None; + } + Some(self.read()) + } } diff --git a/host/src/channel_manager.rs b/host/src/channel_manager.rs index d5abb2b..119f0ce 100644 --- a/host/src/channel_manager.rs +++ b/host/src/channel_manager.rs @@ -40,7 +40,7 @@ pub struct ChannelManager<'d, M: RawMutex, const CHANNELS: usize, const L2CAP_TX } pub trait DynamicChannelManager<'d> { - fn poll_request_to_send(&self, cid: u16, credits: usize, cx: &mut Context<'_>) -> Poll>; + fn poll_request_to_send(&self, cid: u16, credits: usize, cx: Option<&mut Context<'_>>) -> Poll>; fn confirm_received(&self, cid: u16, credits: usize) -> Result<(ConnHandle, L2capLeSignal), Error>; fn confirm_disconnected(&self, cid: u16) -> Result<(), Error>; } @@ -227,6 +227,8 @@ impl<'d, M: RawMutex, const CHANNELS: usize, const L2CAP_TXQ: usize, const L2CAP }), ); + info!("Responding to create: {:?}", response); + controller.signal(conn, response).await?; Ok((state, self.inbound[idx].receiver().into())) } @@ -302,13 +304,13 @@ impl<'d, M: RawMutex, const CHANNELS: usize, const L2CAP_TXQ: usize, const L2CAP self.inbound[chan].send(Some(Pdu::new(p, len))).await; Ok(()) } else { - warn!("No memory for channel {}", packet.channel); + warn!("No memory for channel {} (id {})", packet.channel, chan); Err(Error::OutOfMemory) } } pub async fn control(&self, conn: ConnHandle, signal: L2capLeSignal) -> Result<(), Error> { - // info!("Inbound signal: {:?}", signal); + info!("Inbound signal: {:?}", signal); match signal.data { L2capLeSignalData::LeCreditConnReq(req) => { self.connect(ConnectingState { @@ -414,7 +416,7 @@ impl<'d, M: RawMutex, const CHANNELS: usize, const L2CAP_TXQ: usize, const L2CAP }) } - fn poll_request_to_send(&self, cid: u16, credits: usize, cx: &mut Context<'_>) -> Poll> { + fn poll_request_to_send(&self, cid: u16, credits: usize, cx: Option<&mut Context<'_>>) -> Poll> { self.state.lock(|state| { let mut state = state.borrow_mut(); for (idx, storage) in state.channels.iter_mut().enumerate() { @@ -424,7 +426,9 @@ impl<'d, M: RawMutex, const CHANNELS: usize, const L2CAP_TXQ: usize, const L2CAP s.peer_credits -= credits as u16; return Poll::Ready(Ok(())); } else { - state.credit_wakers[idx].register(cx.waker()); + if let Some(cx) = cx { + state.credit_wakers[idx].register(cx.waker()); + } return Poll::Pending; } } diff --git a/host/src/connection.rs b/host/src/connection.rs index a6ac069..39294d4 100644 --- a/host/src/connection.rs +++ b/host/src/connection.rs @@ -1,20 +1,22 @@ use bt_hci::{ cmd::{le::LeCreateConnParams, link_control::DisconnectParams}, - param::{AddrKind, BdAddr, ConnHandle, DisconnectReason, Duration}, + param::{AddrKind, BdAddr, ConnHandle, DisconnectReason, Duration, LeConnRole}, }; use embassy_sync::{blocking_mutex::raw::RawMutex, channel::DynamicSender}; use crate::adapter::{Adapter, ControlCommand}; +pub use crate::connection_manager::ConnectionInfo; + #[derive(Clone)] pub struct Connection<'d> { - handle: ConnHandle, + info: ConnectionInfo, control: DynamicSender<'d, ControlCommand>, } impl<'d> Connection<'d> { pub fn handle(&self) -> ConnHandle { - self.handle + self.info.handle } pub async fn accept< @@ -27,9 +29,9 @@ impl<'d> Connection<'d> { >( adapter: &'d Adapter<'_, M, T, CONNS, CHANNELS, L2CAP_TXQ, L2CAP_RXQ>, ) -> Self { - let handle = adapter.connections.accept(None).await; + let info = adapter.connections.accept(None).await; Connection { - handle, + info, control: adapter.control.sender().into(), } } @@ -37,12 +39,20 @@ impl<'d> Connection<'d> { pub async fn disconnect(&mut self) { self.control .send(ControlCommand::Disconnect(DisconnectParams { - handle: self.handle, + handle: self.info.handle, reason: DisconnectReason::RemoteUserTerminatedConn, })) .await; } + pub fn role(&self) -> LeConnRole { + self.info.role + } + + pub fn peer_address(&self) -> BdAddr { + self.info.peer_address + } + pub async fn connect< M: RawMutex, T, @@ -58,21 +68,21 @@ impl<'d> Connection<'d> { let params = LeCreateConnParams { le_scan_interval: Duration::from_micros(1707500), le_scan_window: Duration::from_micros(312500), - use_filter_accept_list: false, - peer_addr_kind: AddrKind::PUBLIC, + use_filter_accept_list: true, + peer_addr_kind: AddrKind::RANDOM, peer_addr, own_addr_kind: AddrKind::PUBLIC, - conn_interval_min: Duration::from_millis(25), - conn_interval_max: Duration::from_millis(50), + conn_interval_min: Duration::from_millis(80), + conn_interval_max: Duration::from_millis(80), max_latency: 0, - supervision_timeout: Duration::from_millis(250), + supervision_timeout: Duration::from_millis(8000), min_ce_length: Duration::from_millis(0), max_ce_length: Duration::from_millis(0), }; adapter.control.send(ControlCommand::Connect(params)).await; - let handle = adapter.connections.accept(Some(params.peer_addr)).await; + let info = adapter.connections.accept(Some(params.peer_addr)).await; Connection { - handle, + info, control: adapter.control.sender().into(), } } diff --git a/host/src/connection_manager.rs b/host/src/connection_manager.rs index 05bbc04..aaf8193 100644 --- a/host/src/connection_manager.rs +++ b/host/src/connection_manager.rs @@ -64,21 +64,21 @@ impl ConnectionManager { }) } - pub fn poll_accept(&self, peer: Option, cx: &mut Context<'_>) -> Poll { + pub fn poll_accept(&self, peer: Option, cx: &mut Context<'_>) -> Poll { self.state.lock(|state| { let mut state = state.borrow_mut(); for storage in state.connections.iter_mut() { if let ConnectionState::Connecting(handle, info) = storage { if let Some(peer) = peer { if info.peer_address == peer { - let handle = *handle; - *storage = ConnectionState::Connected(handle, *info); - return Poll::Ready(handle); + let i = *info; + *storage = ConnectionState::Connected(*handle, *info); + return Poll::Ready(i); } } else { - let handle = *handle; - *storage = ConnectionState::Connected(handle, *info); - return Poll::Ready(handle); + let i = *info; + *storage = ConnectionState::Connected(*handle, *info); + return Poll::Ready(i); } } } @@ -87,9 +87,23 @@ impl ConnectionManager { }) } - pub async fn accept(&self, peer: Option) -> ConnHandle { + pub async fn accept(&self, peer: Option) -> ConnectionInfo { poll_fn(move |cx| self.poll_accept(peer, cx)).await } + + pub fn info(&self, handle: ConnHandle) -> Result { + self.state.lock(|state| { + let mut state = state.borrow_mut(); + for storage in state.connections.iter_mut() { + if let ConnectionState::Connected(h, info) = storage { + if *h == handle { + return Ok(*info); + } + } + } + Err(Error::NotFound) + }) + } } pub enum ConnectionState { diff --git a/host/src/cursor.rs b/host/src/cursor.rs index ded02a2..669e78f 100644 --- a/host/src/cursor.rs +++ b/host/src/cursor.rs @@ -112,7 +112,7 @@ impl<'d> ReadCursor<'d> { Ok(val) } - pub fn slice(&mut self, nbytes: usize) -> Result<&[u8], Error> { + pub fn slice(&mut self, nbytes: usize) -> Result<&'d [u8], Error> { if self.available() < nbytes { Err(Error::InsufficientSpace) } else { diff --git a/host/src/l2cap.rs b/host/src/l2cap.rs index bc9976b..47ff41c 100644 --- a/host/src/l2cap.rs +++ b/host/src/l2cap.rs @@ -1,6 +1,6 @@ use core::future::poll_fn; -use crate::adapter::{Adapter, HciController}; +use crate::adapter::{Adapter, ControlCommand, HciController}; use crate::channel_manager::DynamicChannelManager; use crate::codec; use crate::connection::Connection; @@ -8,11 +8,15 @@ use crate::cursor::{ReadCursor, WriteCursor}; use crate::packet_pool::{AllocId, DynamicPacketPool}; use crate::pdu::Pdu; use crate::{AdapterError, Error}; +use bt_hci::cmd::link_control::DisconnectParams; use bt_hci::controller::Controller; use bt_hci::data::AclPacket; use bt_hci::param::ConnHandle; +use bt_hci::param::DisconnectReason; +use core::task::Poll; use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_sync::channel::DynamicReceiver; +use embassy_sync::channel::DynamicSender; pub(crate) const L2CAP_CID_ATT: u16 = 0x0004; pub(crate) const L2CAP_CID_LE_U_SIGNAL: u16 = 0x0005; @@ -29,6 +33,7 @@ impl<'d> L2capPacket<'d> { pub fn decode(packet: AclPacket<'_>) -> Result<(bt_hci::param::ConnHandle, L2capPacket), codec::Error> { let handle = packet.handle(); let data = packet.data(); + let mut r = ReadCursor::new(data); let length: u16 = r.read()?; let channel: u16 = r.read()?; @@ -55,10 +60,54 @@ pub struct L2capChannel<'a, 'd, T: Controller, const MTU: usize> { pool: &'d dyn DynamicPacketPool<'d>, manager: &'a dyn DynamicChannelManager<'d>, rx: DynamicReceiver<'a, Option>>, + control: DynamicSender<'a, ControlCommand>, tx: HciController<'a, T>, } +impl<'a, 'd, T: Controller, const MTU: usize> Clone for L2capChannel<'a, 'd, T, MTU> { + fn clone(&self) -> Self { + Self { + conn: self.conn, + pool_id: self.pool_id, + cid: self.cid, + peer_cid: self.peer_cid, + mps: self.mps, + pool: self.pool, + manager: self.manager, + rx: self.rx, + tx: HciController { + controller: self.tx.controller, + permits: self.tx.permits, + }, + control: self.control, + } + } +} + impl<'a, 'd, T: Controller, const MTU: usize> L2capChannel<'a, 'd, T, MTU> { + fn encode(&self, data: &[u8], header: Option) -> Result, Error> { + if let Some(mut packet) = self.pool.alloc(self.pool_id) { + let mut w = WriteCursor::new(packet.as_mut()); + if header.is_some() { + w.write(2 + data.len() as u16)?; + } else { + w.write(data.len() as u16)?; + } + w.write(self.peer_cid)?; + + if let Some(len) = header { + w.write(len)?; + } + + w.append(data)?; + let len = w.len(); + let pdu = Pdu::new(packet, len); + Ok(pdu) + } else { + Err(Error::OutOfMemory) + } + } + pub async fn send(&mut self, buf: &[u8]) -> Result<(), AdapterError> { // The number of packets we'll need to send for this payload let n_packets = 1 + (buf.len().saturating_sub(self.mps - 2)).div_ceil(self.mps); @@ -70,42 +119,53 @@ impl<'a, 'd, T: Controller, const MTU: usize> L2capChannel<'a, 'd, T, MTU> { return Err(Error::OutOfMemory.into()); } - poll_fn(|cx| self.manager.poll_request_to_send(self.cid, n_packets, cx)).await?; + poll_fn(|cx| self.manager.poll_request_to_send(self.cid, n_packets, Some(cx))).await?; // Segment using mps - let (first, remaining) = buf.split_at(self.mps - 2); - if let Some(mut packet) = self.pool.alloc(self.pool_id) { - let len = { - let mut w = WriteCursor::new(packet.as_mut()); - w.write(2 + first.len() as u16)?; - w.write(self.peer_cid)?; - let len = buf.len() as u16; - w.write(len)?; - w.append(first)?; - w.len() - }; - let pdu = Pdu::new(packet, len); + let (first, remaining) = buf.split_at(buf.len().min(self.mps - 2)); + + let pdu = self.encode(first, Some(buf.len() as u16))?; + self.tx.send(self.conn, pdu.as_ref()).await?; + + let chunks = remaining.chunks(self.mps); + let num_chunks = chunks.len(); + + for (i, chunk) in chunks.enumerate() { + let pdu = self.encode(chunk, None)?; self.tx.send(self.conn, pdu.as_ref()).await?; - } else { + } + + Ok(()) + } + + pub fn try_send(&mut self, buf: &[u8]) -> Result<(), AdapterError> { + // The number of packets we'll need to send for this payload + let n_packets = 1 + (buf.len().saturating_sub(self.mps - 2)).div_ceil(self.mps); + + // TODO: We could potentially make this more graceful by sending as much as we can, and wait + // for pool to get the available packets back, which would require some poll/async behavior + // support for the pool. + if self.pool.available(self.pool_id) < n_packets { return Err(Error::OutOfMemory.into()); } + match self.manager.poll_request_to_send(self.cid, n_packets, None) { + Poll::Ready(res) => res?, + Poll::Pending => return Err(Error::Busy.into()), + } + + // Segment using mps + let (first, remaining) = buf.split_at(buf.len().min(self.mps - 2)); + + let pdu = self.encode(first, Some(buf.len() as u16))?; + self.tx.try_send(self.conn, pdu.as_ref())?; + let chunks = remaining.chunks(self.mps); let num_chunks = chunks.len(); + for (i, chunk) in chunks.enumerate() { - if let Some(mut packet) = self.pool.alloc(self.pool_id) { - let len = { - let mut w = WriteCursor::new(packet.as_mut()); - w.write(chunk.len() as u16)?; - w.write(self.peer_cid)?; - w.append(chunk)?; - w.len() - }; - let pdu = Pdu::new(packet, len); - self.tx.send(self.conn, pdu.as_ref()).await?; - } else { - return Err(Error::OutOfMemory.into()); - } + let pdu = self.encode(chunk, None)?; + self.tx.try_send(self.conn, pdu.as_ref())?; } Ok(()) @@ -124,15 +184,25 @@ impl<'a, 'd, T: Controller, const MTU: usize> L2capChannel<'a, 'd, T, MTU> { pub async fn receive(&mut self, buf: &mut [u8]) -> Result> { let mut n_received = 1; let packet = self.receive_pdu().await?; + let len = packet.len; + let mut r = ReadCursor::new(packet.as_ref()); let remaining: u16 = r.read()?; - let data = r.remaining(); + info!("Total expected: {}", remaining); + let data = r.remaining(); let to_copy = data.len().min(buf.len()); buf[..to_copy].copy_from_slice(&data[..to_copy]); let mut pos = to_copy; + info!("Received {} bytes so far", pos); let mut remaining = remaining as usize - data.len(); + info!( + "Total size of PDU is {}, read buffer size is {} remaining; {}", + len, + buf.len(), + remaining + ); // We have some k-frames to reassemble while remaining > 0 { let packet = self.receive_pdu().await?; @@ -148,6 +218,7 @@ impl<'a, 'd, T: Controller, const MTU: usize> L2capChannel<'a, 'd, T, MTU> { let (handle, response) = self.manager.confirm_received(self.cid, n_received)?; self.tx.signal(handle, response).await?; + info!("Total reserved {} bytes", pos); Ok(pos) } @@ -178,10 +249,24 @@ impl<'a, 'd, T: Controller, const MTU: usize> L2capChannel<'a, 'd, T, MTU> { pool_id: state.pool_id, manager: &adapter.channels, tx: adapter.hci(), + control: adapter.control.sender().into(), rx, }) } + pub fn disconnect(&self, close_connection: bool) -> Result<(), AdapterError> { + self.manager.confirm_disconnected(self.cid)?; + if close_connection { + self.control + .try_send(ControlCommand::Disconnect(DisconnectParams { + handle: self.conn, + reason: DisconnectReason::RemoteUserTerminatedConn, + })) + .map_err(|_| Error::Busy)?; + } + Ok(()) + } + pub async fn create< M: RawMutex, const CONNS: usize, @@ -210,6 +295,7 @@ where { pool: adapter.pool, manager: &adapter.channels, tx: adapter.hci(), + control: adapter.control.sender().into(), rx, }) } diff --git a/host/src/lib.rs b/host/src/lib.rs index ddb75bd..656163b 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -50,6 +50,8 @@ pub enum Error { OutOfMemory, NotSupported, ChannelClosed, + Busy, + Disconnected, Other, } diff --git a/host/src/scan.rs b/host/src/scan.rs index e88f746..fe5d333 100644 --- a/host/src/scan.rs +++ b/host/src/scan.rs @@ -1,10 +1,15 @@ use core::iter::FusedIterator; -use bt_hci::{cmd::le::LeSetScanParams, param::LeAdvReport, FromHciBytes, FromHciBytesError}; +use bt_hci::{ + cmd::le::LeSetScanParams, + param::{AddrKind, BdAddr, LeAdvReport}, + FromHciBytes, FromHciBytesError, +}; use heapless::Vec; -pub struct ScanConfig { +pub struct ScanConfig<'d> { pub params: Option, + pub filter_accept_list: &'d [(AddrKind, &'d BdAddr)], } pub struct ScanReport { diff --git a/host/tests/l2cap.rs b/host/tests/l2cap.rs new file mode 100644 index 0000000..ea6382d --- /dev/null +++ b/host/tests/l2cap.rs @@ -0,0 +1,201 @@ +// Use with any serial HCI +use bt_hci::controller::ExternalController; +use bt_hci::transport::SerialTransport; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embedded_io_adapters::tokio_1::FromTokio; +use tokio::io::ReadHalf; +use tokio::io::WriteHalf; +use tokio::select; +use tokio::time::Duration; +use tokio_serial::SerialStream; +use tokio_serial::{DataBits, Parity, StopBits}; +use trouble_host::{ + adapter::{Adapter, HostResources}, + advertise::{AdStructure, AdvertiseConfig, BR_EDR_NOT_SUPPORTED, LE_GENERAL_DISCOVERABLE}, + connection::Connection, + l2cap::L2capChannel, + scan::ScanConfig, + PacketQos, +}; + +const CONNECTIONS_MAX: usize = 1; +const L2CAP_CHANNELS_MAX: usize = 3; + +async fn create_controller( + port: &str, +) -> ExternalController< + SerialTransport>, FromTokio>>, + 10, +> { + let baudrate = 1000000; + let mut port = SerialStream::open( + &tokio_serial::new(port, baudrate) + .baud_rate(baudrate) + .data_bits(DataBits::Eight) + .parity(Parity::None) + .stop_bits(StopBits::One), + ) + .unwrap(); + + // Drain input + tokio::time::sleep(Duration::from_secs(1)).await; + loop { + let mut buf = [0; 1]; + match port.try_read(&mut buf[..]) { + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break, + _ => {} + } + } + + let (reader, writer) = tokio::io::split(port); + + let reader = embedded_io_adapters::tokio_1::FromTokio::new(reader); + let writer = embedded_io_adapters::tokio_1::FromTokio::new(writer); + + let driver: SerialTransport = SerialTransport::new(reader, writer); + ExternalController::new(driver) +} + +/// Verify l2cap le connection oriented channels using two HCI adapters attached to the test machine. +#[tokio::test] +async fn l2cap_connection_oriented_channels() { + let _ = env_logger::try_init(); + let peripheral = std::env::var("TEST_ADAPTER_ONE").unwrap(); + let central = std::env::var("TEST_ADAPTER_TWO").unwrap(); + + let local = tokio::task::LocalSet::new(); + + // Spawn peripheral + let peripheral = local.spawn_local(async move { + let controller_peripheral = create_controller(&peripheral).await; + + let mut host_resources: HostResources = + HostResources::new(PacketQos::Guaranteed(4)); + + let adapter: Adapter<'_, NoopRawMutex, _, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = + Adapter::new(controller_peripheral, &mut host_resources); + + let config = AdvertiseConfig { + params: None, + adv_data: &[ + AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), + AdStructure::CompleteLocalName(b"trouble-l2cap-int"), + ], + scan_data: &[], + }; + + select! { + r = adapter.run() => { + r + } + r = async { + loop { + println!("[peripheral] advertising"); + let conn = adapter.advertise(&config).await?; + println!("[peripheral] connected"); + + let mut ch1: L2capChannel<'_, '_, _, PAYLOAD_LEN> = + L2capChannel::accept(&adapter, &conn, 0x2349).await?; + + println!("[peripheral] channel created"); + + // 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(&mut rx).await?; + assert_eq!(len, rx.len()); + assert_eq!(rx, [i; PAYLOAD_LEN]); + } + println!("[peripheral] data received"); + + tokio::time::sleep(Duration::from_secs(1)).await; + for i in 0..10 { + let tx = [i; PAYLOAD_LEN]; + ch1.send(&tx).await?; + } + println!("[peripheral] data sent"); + break; + } + Ok(()) + } => { + r + } + } + }); + + // Spawn central + let central = local.spawn_local(async move { + let controller_central = create_controller(¢ral).await; + let mut host_resources: HostResources = + HostResources::new(PacketQos::Guaranteed(4)); + + let adapter: Adapter<'_, NoopRawMutex, _, CONNECTIONS_MAX, L2CAP_CHANNELS_MAX> = + Adapter::new(controller_central, &mut host_resources); + + let config = ScanConfig { + params: None, + filter_accept_list: &[], + }; + + select! { + r = adapter.run() => { + r + } + r = async { + println!("[central] scanning"); + loop { + let reports = adapter.scan(&config).await?; + let mut found = None; + for report in reports.iter() { + let report = report.unwrap(); + for adv in AdStructure::decode(report.data) { + if let Ok(AdStructure::CompleteLocalName(b"trouble-l2cap-int")) = adv { + found.replace(report.addr); + break; + } + } + } + + if let Some(target) = found { + println!("[central] connecting"); + let conn = Connection::connect(&adapter, target).await; + println!("[central] connected"); + const PAYLOAD_LEN: usize = 27; + let mut ch1: L2capChannel<'_, '_, _, PAYLOAD_LEN> = + L2capChannel::create(&adapter, &conn, 0x2349).await?; + println!("[central] channel created"); + for i in 0..10 { + let tx = [i; PAYLOAD_LEN]; + ch1.send(&tx).await?; + } + println!("[central] data sent"); + let mut rx = [0; PAYLOAD_LEN]; + for i in 0..10 { + let len = ch1.receive(&mut rx).await?; + assert_eq!(len, rx.len()); + assert_eq!(rx, [i; PAYLOAD_LEN]); + } + println!("[central] data received"); + break; + } + } + Ok(()) + } => { + r + } + } + }); + + match tokio::time::timeout(Duration::from_secs(30), local).await { + Ok(_) => { + let _ = central.await.unwrap().unwrap(); + let _ = peripheral.await.unwrap().unwrap(); + println!("Test completed successfully"); + } + Err(e) => { + println!("Test timed out: {:?}", e); + assert!(false); + } + } +}