Skip to content

Commit

Permalink
Add example of using trouble with a serial HCI adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
lulf committed Mar 14, 2024
1 parent 173aea6 commit 8e19632
Show file tree
Hide file tree
Showing 3 changed files with 347 additions and 0 deletions.
31 changes: 31 additions & 0 deletions examples/serial-hci/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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" }
250 changes: 250 additions & 0 deletions examples/serial-hci/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<Executor> = 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<String> = 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<HostResources<NoopRawMutex, 4, 32, 27>> = 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<T>
where
T: embedded_io_async::Read + embedded_io_async::Write,
{
io: Mutex<NoopRawMutex, T>,
}

impl<T> SerialController<T>
where
T: embedded_io_async::Read + embedded_io_async::Write,
{
pub fn new(io: T) -> Self {
Self { io: Mutex::new(io) }
}
}

impl<T> Controller for SerialController<T>
where
T: embedded_io_async::Read + embedded_io_async::Write,
{
type Error = T::Error;
fn write_acl_data(&self, packet: &data::AclPacket) -> impl Future<Output = Result<(), Self::Error>> {
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<Output = Result<(), Self::Error>> {
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<Output = Result<(), Self::Error>> {
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<Output = Result<ControllerToHostPacket<'a>, 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<T, C> ControllerCmdSync<C> for SerialController<T>
where
T: embedded_io_async::Read + embedded_io_async::Write,
C: SyncCmd,
C::Return: bt_hci::FixedSizeValue,
{
fn exec(&self, cmd: &C) -> impl Future<Output = Result<C::Return, param::Error>> {
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<T, C> ControllerCmdAsync<C> for SerialController<T>
where
T: embedded_io_async::Read + embedded_io_async::Write,
C: AsyncCmd,
{
fn exec(&self, cmd: &C) -> impl Future<Output = Result<(), param::Error>> {
async {
let mut io = self.io.lock().await;
Ok(WithIndicator::new(cmd).write_hci_async(io.deref_mut()).await.unwrap())
}
}
}
66 changes: 66 additions & 0 deletions examples/serial-hci/src/serial_port.rs
Original file line number Diff line number Diff line change
@@ -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<P: ?Sized + nix::NixPath>(path: &P, baudrate: termios::BaudRate) -> io::Result<Self> {
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<usize> {
nix::unistd::read(self.fd, buf).map_err(to_io_error)
}
}

impl io::Write for SerialPort {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
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()
}

0 comments on commit 8e19632

Please sign in to comment.