use std::{fmt::Display, pin::Pin, time::Duration};
use btleplug::{
api::{
BDAddr, Central as _, Characteristic, Manager as _, Peripheral, ScanFilter,
ValueNotification, WriteType,
},
platform::Manager,
};
use futures::{stream::StreamExt, Stream};
use tracing::{debug, error, trace, warn};
use uuid::{uuid, Uuid};
use super::{Exchange, Transport};
use crate::{
info::{ConnInfo, LedgerInfo, Model},
Error,
};
pub struct BleTransport {
manager: Manager,
peripherals: Vec<(LedgerInfo, btleplug::platform::Peripheral)>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BleInfo {
name: String,
addr: BDAddr,
}
impl Display for BleInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
pub struct BleDevice {
pub info: BleInfo,
mtu: u8,
p: btleplug::platform::Peripheral,
c_write: Characteristic,
c_read: Characteristic,
}
#[derive(Clone, PartialEq, Debug)]
struct BleSpec {
pub model: Model,
pub service_uuid: Uuid,
pub notify_uuid: Uuid,
pub write_uuid: Uuid,
pub write_cmd_uuid: Uuid,
}
const BLE_SPECS: &[BleSpec] = &[
BleSpec {
model: Model::NanoX,
service_uuid: uuid!("13d63400-2c97-0004-0000-4c6564676572"),
notify_uuid: uuid!("13d63400-2c97-0004-0001-4c6564676572"),
write_uuid: uuid!("13d63400-2c97-0004-0002-4c6564676572"),
write_cmd_uuid: uuid!("13d63400-2c97-0004-0003-4c6564676572"),
},
BleSpec {
model: Model::Stax,
service_uuid: uuid!("13d63400-2c97-6004-0000-4c6564676572"),
notify_uuid: uuid!("13d63400-2c97-6004-0001-4c6564676572"),
write_uuid: uuid!("13d63400-2c97-6004-0002-4c6564676572"),
write_cmd_uuid: uuid!("13d63400-2c97-6004-0003-4c6564676572"),
},
];
impl BleTransport {
pub async fn new() -> Result<Self, Error> {
let manager = Manager::new().await?;
Ok(Self {
manager,
peripherals: vec![],
})
}
async fn scan_internal(
&self,
duration: Duration,
) -> Result<Vec<(LedgerInfo, btleplug::platform::Peripheral)>, Error> {
let mut matched = vec![];
let adapters = self.manager.adapters().await?;
let f = ScanFilter { services: vec![] };
for adapter in adapters.iter() {
let info = adapter.adapter_info().await?;
debug!("Scan with adapter {info}");
adapter.start_scan(f.clone()).await?;
tokio::time::sleep(duration).await;
let mut peripherals = adapter.peripherals().await?;
if peripherals.is_empty() {
debug!("No peripherals found on adaptor {info}");
continue;
}
for p in peripherals.drain(..) {
let (properties, _connected) = (p.properties().await?, p.is_connected().await?);
let properties = match properties {
Some(v) => v,
None => {
debug!("Failed to fetch properties for peripheral: {p:?}");
continue;
}
};
let name = match &properties.local_name {
Some(v) => v,
None => continue,
};
debug!("Peripheral: {p:?} props: {properties:?}");
let model = if name.contains("Nano X") {
Model::NanoX
} else if name.contains("Stax") {
Model::Stax
} else {
continue;
};
matched.push((
LedgerInfo {
model: model.clone(),
conn: BleInfo {
name: name.clone(),
addr: properties.address,
}
.into(),
},
p,
));
}
}
Ok(matched)
}
}
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
impl Transport for BleTransport {
type Filters = ();
type Info = BleInfo;
type Device = BleDevice;
async fn list(&mut self, _filters: Self::Filters) -> Result<Vec<LedgerInfo>, Error> {
let devices = self.scan_internal(Duration::from_millis(1000)).await?;
let info: Vec<_> = devices.iter().map(|d| d.0.clone()).collect();
self.peripherals = devices;
Ok(info)
}
async fn connect(&mut self, info: Self::Info) -> Result<Self::Device, Error> {
let (d, p) = match self
.peripherals
.iter()
.find(|(d, _p)| d.conn == info.clone().into())
{
Some(v) => v,
None => {
warn!("No device found matching: {info:?}");
return Err(Error::NoDevices);
}
};
let i = match &d.conn {
ConnInfo::Ble(i) => i,
_ => unreachable!(),
};
let name = &i.name;
let properties = p.properties().await?;
let specs = match BLE_SPECS.iter().find(|s| s.model == d.model) {
Some(v) => v,
None => {
warn!("No specs for model: {:?}", d.model);
return Err(Error::Unknown);
}
};
if !p.is_connected().await? {
if let Err(e) = p.connect().await {
warn!("Failed to connect to {name}: {e:?}");
return Err(Error::Unknown);
}
if !p.is_connected().await? {
warn!("Not connected to {name}");
return Err(Error::Unknown);
}
}
debug!("peripheral {name}: {p:?} properties: {properties:?}");
p.discover_services().await?;
let characteristics = p.characteristics();
trace!("Characteristics: {characteristics:?}");
let c_write = characteristics.iter().find(|c| c.uuid == specs.write_uuid);
let c_read = characteristics.iter().find(|c| c.uuid == specs.notify_uuid);
let (c_write, c_read) = match (c_write, c_read) {
(Some(w), Some(r)) => (w, r),
_ => {
error!("Failed to match read and write characteristics for {name}");
return Err(Error::Unknown);
}
};
let mut d = BleDevice {
info: info.clone(),
mtu: 23,
p: p.clone(),
c_write: c_write.clone(),
c_read: c_read.clone(),
};
match d.fetch_mtu().await {
Ok(mtu) => d.mtu = mtu,
Err(e) => {
warn!("Failed to fetch MTU: {:?}", e);
}
}
debug!("using MTU: {}", d.mtu);
Ok(d)
}
}
const BLE_HEADER_LEN: usize = 3;
impl BleDevice {
async fn write_command(&mut self, cmd: u8, payload: &[u8]) -> Result<(), Error> {
let mut data = Vec::with_capacity(payload.len() + 2);
data.extend_from_slice(&(payload.len() as u16).to_be_bytes()); data.extend_from_slice(payload); debug!("TX cmd: 0x{cmd:02x} payload: {data:02x?}");
for (i, c) in data.chunks(self.mtu as usize - BLE_HEADER_LEN).enumerate() {
let mut buff = Vec::with_capacity(self.mtu as usize);
let cmd = match i == 0 {
true => cmd,
false => 0x03,
};
buff.push(cmd); buff.extend_from_slice(&(i as u16).to_be_bytes()); buff.extend_from_slice(c);
debug!("Write chunk {i}: {:02x?}", buff);
self.p
.write(&self.c_write, &buff, WriteType::WithResponse)
.await?;
}
Ok(())
}
async fn read_data(
&mut self,
mut notifications: Pin<Box<dyn Stream<Item = ValueNotification> + Send>>,
) -> Result<Vec<u8>, Error> {
let v = match notifications.next().await {
Some(v) => v.value,
None => {
return Err(Error::Closed);
}
};
debug!("RX: {:02x?}", v);
if v.len() < 5 {
error!("response too short");
return Err(Error::UnexpectedResponse);
} else if v[0] != 0x05 {
error!("unexpected response type: {:?}", v[0]);
return Err(Error::UnexpectedResponse);
}
let len = v[4] as usize;
if len == 0 {
return Err(Error::EmptyResponse);
}
trace!("Expecting response length: {}", len);
let mut buff = Vec::with_capacity(len);
buff.extend_from_slice(&v[5..]);
while buff.len() < len {
let v = match notifications.next().await {
Some(v) => v.value,
None => {
error!("Failed to fetch next chunk from peripheral");
self.p.unsubscribe(&self.c_read).await?;
return Err(Error::Closed);
}
};
debug!("RX: {v:02x?}");
buff.extend_from_slice(&v[5..]);
}
Ok(buff)
}
async fn fetch_mtu(&mut self) -> Result<u8, Error> {
self.p.subscribe(&self.c_read).await?;
let mut n = self.p.notifications().await?;
self.write_command(0x08, &[]).await?;
let mtu = match n.next().await {
Some(r) if r.value[0] == 0x08 && r.value.len() == 6 => {
debug!("RX: {:02x?}", r);
r.value[5]
}
Some(r) => {
warn!("Unexpected MTU response: {r:02x?}");
return Err(Error::Unknown);
}
None => {
warn!("Failed to request MTU");
return Err(Error::Unknown);
}
};
self.p.unsubscribe(&self.c_read).await?;
Ok(mtu)
}
pub(crate) async fn is_connected(&self) -> Result<bool, Error> {
let c = self.p.is_connected().await?;
Ok(c)
}
}
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
impl Exchange for BleDevice {
async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
self.p.subscribe(&self.c_read).await?;
let notifications = self.p.notifications().await?;
if let Err(e) = self.write_command(0x05, command).await {
self.p.unsubscribe(&self.c_read).await?;
return Err(e);
}
debug!("Await response");
let buff = match tokio::time::timeout(timeout, self.read_data(notifications)).await {
Ok(Ok(v)) => v,
Ok(Err(e)) => {
self.p.unsubscribe(&self.c_read).await?;
return Err(e);
}
Err(e) => {
self.p.unsubscribe(&self.c_read).await?;
return Err(e.into());
}
};
Ok(buff)
}
}