From 8e1963258f6ad288103d0730210b68b6a5b6f534 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Thu, 14 Mar 2024 15:59:22 +0100 Subject: [PATCH] Add example of using trouble with a serial HCI adapter --- examples/serial-hci/Cargo.toml | 31 +++ examples/serial-hci/src/main.rs | 250 +++++++++++++++++++++++++ examples/serial-hci/src/serial_port.rs | 66 +++++++ 3 files changed, 347 insertions(+) create mode 100644 examples/serial-hci/Cargo.toml create mode 100644 examples/serial-hci/src/main.rs create mode 100644 examples/serial-hci/src/serial_port.rs diff --git a/examples/serial-hci/Cargo.toml b/examples/serial-hci/Cargo.toml new file mode 100644 index 00000000..3f14b3a3 --- /dev/null +++ b/examples/serial-hci/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "serial-hci" +version = "0.1.0" +edition = "2021" + +[dependencies] +serialport = "4.2.0" +env_logger = "0.10.0" +log = "0.4" +crossterm = "0.27.0" +rand_core = { version = "0.6.4", features = ["std"] } +embassy-executor = { version = "0.5.0", features = ["task-arena-size-32768", "arch-std", "executor-thread", "log", "integrated-timers"] } +embedded-io-adapters = { version = "0.6.1", features = ["futures-03"] } +embedded-io-async = { version = "0.6.1" } +embassy-time = { version = "0.3.0", features = ["log", "std", ] } +embassy-sync = { version = "0.5.0", features = ["log"] } +critical-section = { version = "1.1", features = ["std"] } +embassy-futures = { version = "0.1" } +nix = "0.26.2" +async-io = "1.6.0" +static_cell = "2" +futures = { version = "0.3.17" } + +bt-hci = { version = "0.1.0", default-features = false } #features = ["log"] } +trouble-host = { version = "0.1.0", path = "../../host" } #, features = ["log"] } + +[patch.crates-io] +bt-hci = { git = "https://github.com/alexmoon/bt-hci.git", branch = "main" } +embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" } +embassy-time = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" } +embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", branch = "main" } diff --git a/examples/serial-hci/src/main.rs b/examples/serial-hci/src/main.rs new file mode 100644 index 00000000..0ad1edcc --- /dev/null +++ b/examples/serial-hci/src/main.rs @@ -0,0 +1,250 @@ +// Use with any serial HCI +use async_io::Async; +use bt_hci::cmd::AsyncCmd; +use bt_hci::cmd::SyncCmd; +use bt_hci::data; +use bt_hci::param; +use bt_hci::Controller; +use bt_hci::ControllerCmdAsync; +use bt_hci::ControllerCmdSync; +use bt_hci::ControllerToHostPacket; +use bt_hci::ReadHci; +use bt_hci::WithIndicator; +use bt_hci::WriteHci; +use core::future::Future; +use core::ops::DerefMut; +use embassy_executor::Executor; +use embassy_futures::join::join3; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::mutex::Mutex; +use embassy_time as _; +use embassy_time::{Duration, Timer}; +use embedded_io_async::Read; +use log::*; +use nix::sys::termios; +use static_cell::StaticCell; +use trouble_host::{ + adapter::{Adapter, HostResources}, + advertise::{AdStructure, AdvertiseConfig, BR_EDR_NOT_SUPPORTED, LE_GENERAL_DISCOVERABLE}, + attribute::{AttributeTable, Characteristic, CharacteristicProp, Service, Uuid}, + PacketQos, +}; + +mod serial_port; +use self::serial_port::SerialPort; + +static EXECUTOR: StaticCell = StaticCell::new(); + +fn main() { + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .filter_module("async_io", log::LevelFilter::Info) + .format_timestamp_nanos() + .init(); + + let executor = EXECUTOR.init(Executor::new()); + executor.run(|spawner| { + spawner.spawn(run()).unwrap(); + }); +} + +#[embassy_executor::task] +async fn run() { + let baudrate = termios::BaudRate::B115200; + + 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 port = SerialPort::new(args[1].as_str(), baudrate).unwrap(); + let port = Async::new(port).unwrap(); + let mut port = embedded_io_adapters::futures_03::FromFutures::new(port); + + println!("Reset the target"); + let mut buffer = [0u8; 1]; + + loop { + match port.read(&mut buffer).await { + Ok(_len) => { + if buffer[0] == 0xff { + break; + } + } + Err(_) => (), + } + } + + println!("Connected"); + println!("Q to exit, N to notify, X force disconnect"); + + let controller = SerialController::new(port); + 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"), + ], + }; + + let mut table: AttributeTable<'_, NoopRawMutex, 10> = AttributeTable::new(); + + // Generic Access Service (mandatory) + let mut id = [b'T', b'r', b'o', b'u', b'b', b'l', b'e']; + let mut appearance = [0x80, 0x07]; + let mut bat_level = [0; 1]; + let handle = { + let mut svc = table.add_service(Service::new(0x1800)); + let _ = svc.add_characteristic(Characteristic::new(0x2a00, &[CharacteristicProp::Read], &mut id[..])); + let _ = svc.add_characteristic(Characteristic::new( + 0x2a01, + &[CharacteristicProp::Read], + &mut 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(Characteristic::new( + 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; +} + +pub struct SerialController +where + T: embedded_io_async::Read + embedded_io_async::Write, +{ + io: Mutex, +} + +impl SerialController +where + T: embedded_io_async::Read + embedded_io_async::Write, +{ + pub fn new(io: T) -> Self { + Self { io: Mutex::new(io) } + } +} + +impl Controller for SerialController +where + T: embedded_io_async::Read + embedded_io_async::Write, +{ + type Error = T::Error; + fn write_acl_data(&self, packet: &data::AclPacket) -> impl Future> { + async { + let mut io = self.io.lock().await; + WithIndicator::new(packet) + .write_hci_async(io.deref_mut()) + .await + .unwrap(); + Ok(()) + } + } + + fn write_sync_data(&self, packet: &data::SyncPacket) -> impl Future> { + async { + let mut io = self.io.lock().await; + WithIndicator::new(packet) + .write_hci_async(io.deref_mut()) + .await + .unwrap(); + Ok(()) + } + } + + fn write_iso_data(&self, packet: &data::IsoPacket) -> impl Future> { + async { + let mut io = self.io.lock().await; + WithIndicator::new(packet) + .write_hci_async(io.deref_mut()) + .await + .unwrap(); + Ok(()) + } + } + + fn read<'a>(&self, buf: &'a mut [u8]) -> impl Future, Self::Error>> { + async { + let mut io = self.io.lock().await; + let value = ControllerToHostPacket::read_hci_async(io.deref_mut(), buf) + .await + .unwrap(); + Ok(value) + } + } +} + +impl ControllerCmdSync for SerialController +where + T: embedded_io_async::Read + embedded_io_async::Write, + C: SyncCmd, + C::Return: bt_hci::FixedSizeValue, +{ + fn exec(&self, cmd: &C) -> impl Future> { + async { + let mut buf = [0; 512]; + let mut io = self.io.lock().await; + WithIndicator::new(cmd).write_hci_async(io.deref_mut()).await.unwrap(); + let value = C::Return::read_hci_async(io.deref_mut(), &mut buf[..]).await.unwrap(); + Ok(value) + } + } +} + +impl ControllerCmdAsync for SerialController +where + T: embedded_io_async::Read + embedded_io_async::Write, + C: AsyncCmd, +{ + fn exec(&self, cmd: &C) -> impl Future> { + async { + let mut io = self.io.lock().await; + Ok(WithIndicator::new(cmd).write_hci_async(io.deref_mut()).await.unwrap()) + } + } +} diff --git a/examples/serial-hci/src/serial_port.rs b/examples/serial-hci/src/serial_port.rs new file mode 100644 index 00000000..c41abd4d --- /dev/null +++ b/examples/serial-hci/src/serial_port.rs @@ -0,0 +1,66 @@ +use std::io; +use std::os::unix::io::{AsRawFd, RawFd}; + +use nix::errno::Errno; +use nix::fcntl::OFlag; +use nix::sys::termios; + +pub struct SerialPort { + fd: RawFd, +} + +impl SerialPort { + pub fn new(path: &P, baudrate: termios::BaudRate) -> io::Result { + let fd = nix::fcntl::open( + path, + OFlag::O_RDWR | OFlag::O_NOCTTY | OFlag::O_NONBLOCK, + nix::sys::stat::Mode::empty(), + ) + .map_err(to_io_error)?; + + let mut cfg = termios::tcgetattr(fd).map_err(to_io_error)?; + cfg.input_flags = termios::InputFlags::empty(); + cfg.output_flags = termios::OutputFlags::empty(); + cfg.control_flags = termios::ControlFlags::empty(); + cfg.local_flags = termios::LocalFlags::empty(); + termios::cfmakeraw(&mut cfg); + cfg.input_flags |= termios::InputFlags::IGNBRK; + cfg.control_flags |= termios::ControlFlags::CREAD; + //cfg.control_flags |= termios::ControlFlags::CRTSCTS; + termios::cfsetospeed(&mut cfg, baudrate).map_err(to_io_error)?; + termios::cfsetispeed(&mut cfg, baudrate).map_err(to_io_error)?; + termios::cfsetspeed(&mut cfg, baudrate).map_err(to_io_error)?; + // Set VMIN = 1 to block until at least one character is received. + cfg.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1; + termios::tcsetattr(fd, termios::SetArg::TCSANOW, &cfg).map_err(to_io_error)?; + termios::tcflush(fd, termios::FlushArg::TCIOFLUSH).map_err(to_io_error)?; + + Ok(Self { fd }) + } +} + +impl AsRawFd for SerialPort { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl io::Read for SerialPort { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + nix::unistd::read(self.fd, buf).map_err(to_io_error) + } +} + +impl io::Write for SerialPort { + fn write(&mut self, buf: &[u8]) -> io::Result { + nix::unistd::write(self.fd, buf).map_err(to_io_error) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn to_io_error(e: Errno) -> io::Error { + e.into() +}