chessnut_move/transport/
btleplug.rs1use core::pin::Pin;
76
77use ::btleplug::api::{
78 Characteristic as BtleplugCharacteristic, Peripheral, ValueNotification, WriteType,
79};
80use futures_util::{Stream, StreamExt};
81use thiserror::Error;
82use uuid::Uuid;
83
84use crate::protocol::{Command, WriteKind};
85#[cfg(doc)]
86use crate::transport;
87#[cfg(feature = "tokio")]
88use crate::transport::TokioTransport;
89use crate::transport::gatt::Characteristic;
90use crate::transport::{AsyncTransport, Notification, NotificationSource};
91
92type NotificationStream = Pin<Box<dyn Stream<Item = ValueNotification> + Send>>;
93
94#[derive(Debug, Error)]
96pub enum Error {
97 #[error(transparent)]
99 Backend(#[from] ::btleplug::Error),
100
101 #[error("required Chessnut Move characteristic is missing: {0:?}")]
103 MissingCharacteristic(Characteristic),
104
105 #[error("the btleplug notification stream ended")]
107 NotificationStreamEnded,
108
109 #[error("notification has {actual} bytes but the provided buffer holds {capacity}")]
111 NotificationTooLong {
112 actual: usize,
114
115 capacity: usize,
117 },
118
119 #[error("received a notification from unexpected characteristic {0}")]
121 UnexpectedNotificationCharacteristic(Uuid),
122}
123
124#[cfg_attr(
133 feature = "tokio",
134 doc = "See [`TokioTransport`](transport::tokio::TokioTransport) for the actor-compatible transport contract."
135)]
136pub struct BtleplugTransport<P> {
137 peripheral: P,
138 command_write: BtleplugCharacteristic,
139 position_notification: BtleplugCharacteristic,
140 command_response: BtleplugCharacteristic,
141 notifications: NotificationStream,
142}
143
144impl<P: Peripheral> BtleplugTransport<P> {
145 pub async fn new(peripheral: P) -> Result<Self, Error> {
171 debug_event!("discovering Chessnut Move GATT services");
172 peripheral.discover_services().await?;
173
174 let characteristics = peripheral.characteristics();
175 trace_event!(
176 characteristic_count = characteristics.len(),
177 "discovered peripheral characteristics"
178 );
179 let find = |characteristic: Characteristic| {
180 let uuid = Uuid::from_u128(characteristic.uuid_u128());
181 characteristics
182 .iter()
183 .find(|candidate| candidate.uuid == uuid)
184 .cloned()
185 .ok_or_else(|| {
186 warn_event!(
187 characteristic = ?characteristic,
188 "required Chessnut Move characteristic is missing"
189 );
190 Error::MissingCharacteristic(characteristic)
191 })
192 };
193
194 let command_write = find(Characteristic::CommandWrite)?;
195 let position_notification = find(Characteristic::PositionNotification)?;
196 let command_response = find(Characteristic::CommandResponse)?;
197 let notifications = peripheral.notifications().await?;
198 debug_event!("Chessnut Move GATT transport is ready");
199
200 Ok(Self {
201 peripheral,
202 command_write,
203 position_notification,
204 command_response,
205 notifications,
206 })
207 }
208
209 pub const fn peripheral(&self) -> &P {
211 &self.peripheral
212 }
213
214 pub fn peripheral_mut(&mut self) -> &mut P {
219 &mut self.peripheral
220 }
221
222 pub fn into_peripheral(self) -> P {
226 self.peripheral
227 }
228
229 fn notification_characteristic(&self, source: NotificationSource) -> &BtleplugCharacteristic {
231 match source {
232 NotificationSource::Position => &self.position_notification,
233 NotificationSource::CommandResponse => &self.command_response,
234 }
235 }
236
237 async fn subscribe_source(&mut self, source: NotificationSource) -> Result<(), Error> {
239 trace_event!(source = ?source, "subscribing to BLE notifications");
240 self
241 .peripheral
242 .subscribe(self.notification_characteristic(source))
243 .await?;
244 Ok(())
245 }
246
247 async fn unsubscribe_source(&mut self, source: NotificationSource) -> Result<(), Error> {
249 trace_event!(source = ?source, "unsubscribing from BLE notifications");
250 self
251 .peripheral
252 .unsubscribe(self.notification_characteristic(source))
253 .await?;
254 Ok(())
255 }
256
257 async fn write(&mut self, command: &Command) -> Result<(), Error> {
259 trace_event!(
260 command_len = command.bytes().len(),
261 write_kind = ?command.write_kind(),
262 "writing command to BLE characteristic"
263 );
264 let write_type = match command.write_kind() {
265 WriteKind::WithResponse => WriteType::WithResponse,
266 WriteKind::WithoutResponse => WriteType::WithoutResponse,
267 };
268
269 self
270 .peripheral
271 .write(&self.command_write, command.bytes(), write_type)
272 .await?;
273 Ok(())
274 }
275
276 async fn receive<'a>(&'a mut self, buffer: &'a mut [u8]) -> Result<Notification<'a>, Error> {
278 let notification = self
281 .notifications
282 .next()
283 .await
284 .ok_or(Error::NotificationStreamEnded)?;
285
286 let source =
287 if notification.uuid == Uuid::from_u128(Characteristic::PositionNotification.uuid_u128()) {
288 NotificationSource::Position
289 } else if notification.uuid == Uuid::from_u128(Characteristic::CommandResponse.uuid_u128()) {
290 NotificationSource::CommandResponse
291 } else {
292 warn_event!(
293 characteristic_uuid = %notification.uuid,
294 "received notification from an unexpected characteristic"
295 );
296 return Err(Error::UnexpectedNotificationCharacteristic(
297 notification.uuid,
298 ));
299 };
300
301 let actual = notification.value.len();
302 if actual > buffer.len() {
303 warn_event!(
304 notification_len = actual,
305 buffer_capacity = buffer.len(),
306 "notification exceeds receive buffer"
307 );
308 return Err(Error::NotificationTooLong {
309 actual,
310 capacity: buffer.len(),
311 });
312 }
313
314 buffer[..actual].copy_from_slice(¬ification.value);
315 trace_event!(
316 source = ?source,
317 notification_len = actual,
318 "received BLE notification"
319 );
320 Ok(Notification::new(source, &buffer[..actual]))
321 }
322}
323
324impl<P: Peripheral> AsyncTransport for BtleplugTransport<P> {
325 type Error = Error;
326
327 async fn subscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
328 self.subscribe_source(source).await
329 }
330
331 async fn unsubscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
332 self.unsubscribe_source(source).await
333 }
334
335 async fn write_command(&mut self, command: &Command) -> Result<(), Self::Error> {
336 self.write(command).await
337 }
338
339 async fn next_notification<'a>(
340 &'a mut self,
341 buffer: &'a mut [u8],
342 ) -> Result<Notification<'a>, Self::Error> {
343 self.receive(buffer).await
344 }
345}
346
347#[cfg(feature = "tokio")]
348impl<P: Peripheral + 'static> TokioTransport for BtleplugTransport<P> {
349 type Error = Error;
350
351 async fn subscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
352 self.subscribe_source(source).await
353 }
354
355 async fn unsubscribe(&mut self, source: NotificationSource) -> Result<(), Self::Error> {
356 self.unsubscribe_source(source).await
357 }
358
359 async fn write_command(&mut self, command: &Command) -> Result<(), Self::Error> {
360 self.write(command).await
361 }
362
363 async fn next_notification<'a>(
364 &'a mut self,
365 buffer: &'a mut [u8],
366 ) -> Result<Notification<'a>, Self::Error> {
367 self.receive(buffer).await
368 }
369}