use core::pin::Pin;
use ::btleplug::api::{
Characteristic as BtleplugCharacteristic, Peripheral, ValueNotification, WriteType,
};
use futures_util::{Stream, StreamExt};
use thiserror::Error;
use uuid::Uuid;
use crate::protocol::{Command, WriteKind};
#[cfg(doc)]
use crate::transport;
#[cfg(feature = "tokio")]
use crate::transport::TokioTransport;
use crate::transport::gatt::Characteristic;
use crate::transport::{AsyncTransport, Notification, NotificationSource};
type NotificationStream = Pin<Box<dyn Stream<Item = ValueNotification> + Send>>;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Backend(#[from] ::btleplug::Error),
#[error("required Chessnut Move characteristic is missing: {0:?}")]
MissingCharacteristic(Characteristic),
#[error("the btleplug notification stream ended")]
NotificationStreamEnded,
#[error("notification has {actual} bytes but the provided buffer holds {capacity}")]
NotificationTooLong {
actual: usize,
capacity: usize,
},
#[error("received a notification from unexpected characteristic {0}")]
UnexpectedNotificationCharacteristic(Uuid),
}
#[cfg_attr(
feature = "tokio",
doc = "See [`TokioTransport`](transport::tokio::TokioTransport) for the actor-compatible transport contract."
)]
pub struct BtleplugTransport<P> {
peripheral: P,
command_write: BtleplugCharacteristic,
position_notification: BtleplugCharacteristic,
command_response: BtleplugCharacteristic,
notifications: NotificationStream,
}
impl<P: Peripheral> BtleplugTransport<P> {
pub async fn new(peripheral: P) -> Result<Self, Error> {
debug_event!("discovering Chessnut Move GATT services");
peripheral.discover_services().await?;
let characteristics = peripheral.characteristics();
trace_event!(
characteristic_count = characteristics.len(),
"discovered peripheral characteristics"
);
let find = |characteristic: Characteristic| {
let uuid = Uuid::from_u128(characteristic.uuid_u128());
characteristics
.iter()
.find(|candidate| candidate.uuid == uuid)
.cloned()
.ok_or_else(|| {
warn_event!(
characteristic = ?characteristic,
"required Chessnut Move characteristic is missing"
);
Error::MissingCharacteristic(characteristic)
})
};
let command_write = find(Characteristic::CommandWrite)?;
let position_notification = find(Characteristic::PositionNotification)?;
let command_response = find(Characteristic::CommandResponse)?;
let notifications = peripheral.notifications().await?;
debug_event!("Chessnut Move GATT transport is ready");
Ok(Self {
peripheral,
command_write,
position_notification,
command_response,
notifications,
})
}
pub const fn peripheral(&self) -> &P {
&self.peripheral
}
pub fn peripheral_mut(&mut self) -> &mut P {
&mut self.peripheral
}
pub fn into_peripheral(self) -> P {
self.peripheral
}
fn notification_characteristic(&self, source: NotificationSource) -> &BtleplugCharacteristic {
match source {
NotificationSource::Position => &self.position_notification,
NotificationSource::CommandResponse => &self.command_response,
}
}
async fn subscribe_source(&mut self, source: NotificationSource) -> Result<(), Error> {
trace_event!(source = ?source, "subscribing to BLE notifications");
self
.peripheral
.subscribe(self.notification_characteristic(source))
.await?;
Ok(())
}
async fn unsubscribe_source(&mut self, source: NotificationSource) -> Result<(), Error> {
trace_event!(source = ?source, "unsubscribing from BLE notifications");
self
.peripheral
.unsubscribe(self.notification_characteristic(source))
.await?;
Ok(())
}
async fn write(&mut self, command: &Command) -> Result<(), Error> {
trace_event!(
command_len = command.bytes().len(),
write_kind = ?command.write_kind(),
"writing command to BLE characteristic"
);
let write_type = match command.write_kind() {
WriteKind::WithResponse => WriteType::WithResponse,
WriteKind::WithoutResponse => WriteType::WithoutResponse,
};
self
.peripheral
.write(&self.command_write, command.bytes(), write_type)
.await?;
Ok(())
}
async fn receive<'a>(&'a mut self, buffer: &'a mut [u8]) -> Result<Notification<'a>, Error> {
let notification = self
.notifications
.next()
.await
.ok_or(Error::NotificationStreamEnded)?;
let source =
if notification.uuid == Uuid::from_u128(Characteristic::PositionNotification.uuid_u128()) {
NotificationSource::Position
} else if notification.uuid == Uuid::from_u128(Characteristic::CommandResponse.uuid_u128()) {
NotificationSource::CommandResponse
} else {
warn_event!(
characteristic_uuid = %notification.uuid,
"received notification from an unexpected characteristic"
);
return Err(Error::UnexpectedNotificationCharacteristic(
notification.uuid,
));
};
let actual = notification.value.len();
if actual > buffer.len() {
warn_event!(
notification_len = actual,
buffer_capacity = buffer.len(),
"notification exceeds receive buffer"
);
return Err(Error::NotificationTooLong {
actual,
capacity: buffer.len(),
});
}
buffer[..actual].copy_from_slice(¬ification.value);
trace_event!(
source = ?source,
notification_len = actual,
"received BLE notification"
);
Ok(Notification::new(source, &buffer[..actual]))
}
}
impl<P: Peripheral> AsyncTransport for BtleplugTransport<P> {
type Error = Error;
async fn subscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
self.subscribe_source(source).await
}
async fn unsubscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
self.unsubscribe_source(source).await
}
async fn write_command(&mut self, command: &Command) -> Result<(), Self::Error> {
self.write(command).await
}
async fn next_notification<'a>(
&'a mut self,
buffer: &'a mut [u8],
) -> Result<Notification<'a>, Self::Error> {
self.receive(buffer).await
}
}
#[cfg(feature = "tokio")]
impl<P: Peripheral + 'static> TokioTransport for BtleplugTransport<P> {
type Error = Error;
async fn subscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
self.subscribe_source(source).await
}
async fn unsubscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
self.unsubscribe_source(source).await
}
async fn write_command(&mut self, command: &Command) -> Result<(), Self::Error> {
self.write(command).await
}
async fn next_notification<'a>(
&'a mut self,
buffer: &'a mut [u8],
) -> Result<Notification<'a>, Self::Error> {
self.receive(buffer).await
}
}