diff --git a/examples/apps/src/ble_l2cap_peripheral_ios.rs b/examples/apps/src/ble_l2cap_peripheral_ios.rs new file mode 100644 index 00000000..b55d440b --- /dev/null +++ b/examples/apps/src/ble_l2cap_peripheral_ios.rs @@ -0,0 +1,150 @@ +use embassy_futures::join::join; +use embassy_futures::select::select; +use trouble_host::prelude::AdStructure; +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 + +/// L2CAP PSM to be published to the client +/// Only values in range 0x0040 - 0x00FF are accepted by iOS/macOS clients. +const L2CAP_PSM: u16 = 0x00C0; + +/// GATT Server +#[gatt_server] +struct Server { + psm_service: PsmService, +} + +/// PSM Service - publishes the PSM value for L2CAP client connection +/// The PSM Service UUID is specific to the services iOS/macOS App is scanning for +/// Here, we use value from https://github.com/paulw11/L2CapDemo (L2CapDemo/L2CapDemo/Constants.swift) +/// The PSM Characteristic UUID is predefined https://developer.apple.com/documentation/corebluetooth/cbuuidl2cappsmcharacteristicstring +#[gatt_service(uuid = "12E61727-B41A-436F-B64D-4777B35F2294")] +struct PsmService { + #[characteristic(uuid = "ABDD3056-28FA-441D-A470-55A75A52553A", read, indicate, value = L2CAP_PSM)] + psm: u16, +} + +pub async fn run(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 = 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]; + AdStructure::encode_slice( + &[AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED)], + &mut adv_data[..], + ) + .expect("Failed to encode adv_data"); + + // Advertise the PSM service the iOS/macOS client is scanning for + let mut scan_data = [0; 31]; + AdStructure::encode_slice( + &[ + AdStructure::CompleteLocalName(b"Trouble"), + AdStructure::ServiceUuids128(&[0x12E61727_B41A_436F_B64D_4777B35F2294_u128.to_le_bytes()]), + ], + &mut scan_data[..], + ) + .expect("Failed to encode scan_data"); + + let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig { + name: "Trouble L2CAP Server", + appearance: &appearance::sensor::GENERIC_SENSOR, + })) + .expect("Failed to create GATT server"); + + let _ = join(runner.run(), async { + loop { + info!("Advertising, waiting for connection..."); + let advertiser = peripheral + .advertise( + &Default::default(), + Advertisement::ConnectableScannableUndirected { + adv_data: &adv_data[..], + scan_data: &scan_data[..], + }, + ) + .await + .expect("Failed to advertise"); + + let conn = advertiser + .accept() + .await + .expect("Advertising accept failed") + .with_attribute_server(&server) + .expect("Failed to set GATT server"); + + info!("Connection established"); + + let gatt_fut = gatt_task(&conn); + let l2cap_fut = l2cap_task::<_, _, L2CAP_MTU, L2CAP_PSM>(&conn, &stack); + select(gatt_fut, l2cap_fut).await; + } + }) + .await; +} + +/// Task to handle Gatt events +async fn gatt_task(conn: &GattConnection<'_, '_, P>) { + let reason = loop { + match conn.next().await { + GattConnectionEvent::Disconnected { reason } => break reason, + GattConnectionEvent::Gatt { event } => { + match event.accept() { + Ok(reply) => reply.send().await, + Err(e) => warn!("[gatt] error sending response: {:?}", e), + }; + } + _ => {} // Ignore other GATT events + } + }; + info!("[gatt] disconnected: {:?}", reason); +} + +/// Task to handle L2CAP data streaming +async fn l2cap_task<'a, C: Controller, P: PacketPool, const MTU: usize, const PSM: u16>( + conn: &GattConnection<'_, '_, P>, + stack: &'a Stack<'a, C, P>, +) { + let mut channel = match L2capChannel::accept(&stack, conn.raw(), &[PSM], &Default::default()).await { + Ok(chan) => chan, + Err(e) => { + warn!("[l2cap] channel accept error: {:?}", e); + return; + } + }; + info!("[l2cap] channel accepted"); + + let mut buf = [0; MTU]; + loop { + match channel.receive(&stack, &mut buf).await { + Ok(len) => { + let rx_data = &buf[..len]; + info!("[l2cap] received: {:02x?}", rx_data); + // Echo received data + if let Err(e) = channel.send(&stack, rx_data).await { + warn!("[l2cap] error sending data: {:?}", e); + } + } + Err(e) => { + warn!("[l2cap] error receiving data: {:?}", e); + } + } + } +} diff --git a/examples/apps/src/lib.rs b/examples/apps/src/lib.rs index c8187037..2981e3fb 100644 --- a/examples/apps/src/lib.rs +++ b/examples/apps/src/lib.rs @@ -13,6 +13,7 @@ pub mod ble_bas_peripheral_sec; pub mod ble_beacon; pub mod ble_l2cap_central; pub mod ble_l2cap_peripheral; +pub mod ble_l2cap_peripheral_ios; pub mod ble_scanner; pub mod high_throughput_ble_l2cap_central; pub mod high_throughput_ble_l2cap_peripheral; diff --git a/examples/serial-hci/src/bin/ble_l2cap_peripheral_ios.rs b/examples/serial-hci/src/bin/ble_l2cap_peripheral_ios.rs new file mode 100644 index 00000000..e2b74fb3 --- /dev/null +++ b/examples/serial-hci/src/bin/ble_l2cap_peripheral_ios.rs @@ -0,0 +1,54 @@ +// Use with any serial HCI +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use log::*; +use tokio::time::Duration; +use tokio_serial::{DataBits, Parity, SerialStream, StopBits}; +use trouble_example_apps::ble_l2cap_peripheral_ios; +use trouble_host::prelude::{ExternalController, SerialTransport}; + +#[tokio::main] +async fn main() { + env_logger::builder() + .filter_level(log::LevelFilter::Trace) + .format_timestamp_nanos() + .init(); + + let baudrate = 1000000; + + if std::env::args().len() != 2 { + println!("Provide the serial port as the one and only command line argument."); + return; + } + + let args: Vec = std::env::args().collect(); + + let mut port = SerialStream::open( + &tokio_serial::new(args[1].as_str(), 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, + _ => {} + } + } + info!("Ready!"); + + 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); + let controller: ExternalController<_, 10> = ExternalController::new(driver); + + ble_l2cap_peripheral_ios::run::<_, 251>(controller).await; +}