#![doc = "# Session APIs"]
#![cfg_attr(
feature = "async",
doc = "- [`Board`][transport::Board] is the default runtime-neutral async session."
)]
#![cfg_attr(
feature = "blocking",
doc = "- [`BlockingBoard`][transport::BlockingBoard] provides the synchronous session."
)]
#![cfg_attr(
feature = "tokio",
doc = "- The [`tokio`][transport::tokio] module provides a cloneable actor handle and event streams."
)]
use thiserror::Error;
#[cfg(doc)]
use crate::protocol;
use crate::protocol::{
BoardEvent, DecodePositionNotificationError, DecodeResponseNotificationError,
decode_position_notification, decode_response_notification,
};
#[cfg(feature = "async")]
mod asynchronous;
#[cfg(feature = "blocking")]
mod blocking;
#[cfg(feature = "btleplug")]
pub mod btleplug;
pub mod gatt;
#[cfg(feature = "tokio")]
pub mod tokio;
#[cfg(feature = "async")]
pub use asynchronous::{AsyncBoard, AsyncTransport};
#[cfg(feature = "blocking")]
pub use blocking::{BlockingBoard, BlockingTransport};
#[cfg(feature = "tokio")]
pub use tokio::TokioTransport;
const REALTIME_UPDATES_ACKNOWLEDGEMENT: u8 = 0x23;
#[cfg(feature = "async")]
pub type Board<T> = AsyncBoard<T>;
pub const MAX_NOTIFICATION_LEN: usize = 3 + 34 * 4;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NotificationSource {
Position,
CommandResponse,
}
impl NotificationSource {
pub const fn characteristic(self) -> gatt::Characteristic {
match self {
Self::Position => gatt::Characteristic::PositionNotification,
Self::CommandResponse => gatt::Characteristic::CommandResponse,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Notification<'a> {
source: NotificationSource,
bytes: &'a [u8],
}
impl<'a> Notification<'a> {
pub const fn new(source: NotificationSource, bytes: &'a [u8]) -> Self {
Self { source, bytes }
}
pub const fn source(&self) -> NotificationSource {
self.source
}
pub const fn bytes(&self) -> &'a [u8] {
self.bytes
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum DecodeNotificationError {
#[error(transparent)]
Position(#[from] DecodePositionNotificationError),
#[error(transparent)]
CommandResponse(#[from] DecodeResponseNotificationError),
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DecodedNotification {
Event(BoardEvent),
RealtimeUpdatesAcknowledged,
}
#[derive(Debug, Error)]
pub enum BoardError<E> {
#[error("transport error: {0}")]
Transport(E),
#[error(transparent)]
Decode(#[from] DecodeNotificationError),
}
pub fn decode_notification(
notification: Notification<'_>,
) -> Result<DecodedNotification, DecodeNotificationError> {
let _span = trace_span!(
"decode_notification",
source = ?notification.source,
notification_len = notification.bytes.len()
);
let decoded = match notification.source {
NotificationSource::Position => decode_position_notification(notification.bytes)
.map(BoardEvent::PositionChanged)
.map(DecodedNotification::Event)
.map_err(Into::into),
NotificationSource::CommandResponse
if notification.bytes.first().copied() == Some(REALTIME_UPDATES_ACKNOWLEDGEMENT) =>
{
Ok(DecodedNotification::RealtimeUpdatesAcknowledged)
}
NotificationSource::CommandResponse => decode_response_notification(notification.bytes)
.map(DecodedNotification::Event)
.map_err(Into::into),
};
match &decoded {
Ok(DecodedNotification::RealtimeUpdatesAcknowledged) => {
debug_event!("received realtime-update acknowledgement");
}
Ok(DecodedNotification::Event(BoardEvent::PositionChanged(_))) => {
trace_event!(event = "position_changed", "decoded board event");
}
Ok(DecodedNotification::Event(BoardEvent::BatteryStatus(_))) => {
trace_event!(event = "battery_status", "decoded board event");
}
Ok(DecodedNotification::Event(BoardEvent::PieceStatus(_))) => {
trace_event!(event = "piece_status", "decoded board event");
}
Err(_error) => {
trace_event!(error = ?_error, "notification decoding failed");
}
}
decoded
}
#[cfg(any(feature = "async", feature = "blocking"))]
pub(crate) struct BoardState<T> {
pub(crate) transport: T,
pub(crate) notification_buffer: [u8; MAX_NOTIFICATION_LEN],
}
#[cfg(any(feature = "async", feature = "blocking"))]
impl<T> BoardState<T> {
pub(crate) const fn new(transport: T) -> Self {
Self {
transport,
notification_buffer: [0; MAX_NOTIFICATION_LEN],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::BatteryStatus;
#[test]
fn decodes_notifications_according_to_their_source() {
let response = [0x41, 0x03, 0x0c, 0x01, 74];
let event = decode_notification(Notification::new(
NotificationSource::CommandResponse,
&response,
));
assert_eq!(
event,
Ok(DecodedNotification::Event(BoardEvent::BatteryStatus(
BatteryStatus {
charging: true,
percentage: 74,
},
)))
);
}
#[test]
fn recognizes_realtime_update_acknowledgement() {
let notification = Notification::new(NotificationSource::CommandResponse, &[0x23, 0x01, 0x00]);
assert_eq!(
decode_notification(notification),
Ok(DecodedNotification::RealtimeUpdatesAcknowledged)
);
}
}