use crate::error::Result;
use async_trait::async_trait;
use tokio::sync::mpsc;
pub(crate) const HAP_INSTANCE_ID_DESC: &str = "dc46f0fe-81d2-4616-b5d9-6abdd796939a";
pub(crate) const HAP_SERVICE_ID_CHAR: &str = "e604e95d-a759-4817-87d3-aa005083a0d1";
pub(crate) fn u16_le(v: &[u8]) -> Option<u16> {
match v {
[lo, hi, ..] => Some(u16::from_le_bytes([*lo, *hi])),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GattCharacteristic {
pub uuid: String,
pub iid: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GattService {
pub uuid: String,
pub iid: u16,
pub characteristics: Vec<GattCharacteristic>,
}
#[async_trait]
pub trait GattConnection: Send + Sync {
async fn write(&self, char_uuid: &str, value: &[u8]) -> Result<()>;
async fn read(&self, char_uuid: &str) -> Result<Vec<u8>>;
async fn subscribe(&self, char_uuid: &str) -> Result<mpsc::Receiver<Vec<u8>>>;
async fn instance_id(&self, char_uuid: &str) -> Result<u16>;
async fn enumerate(&self) -> Result<Vec<GattService>>;
async fn max_write(&self) -> usize {
DEFAULT_FRAGMENT_SIZE
}
async fn generation(&self) -> u64 {
0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawAdvert {
pub manufacturer_data: Vec<u8>,
}
#[async_trait]
pub trait AdvertSource: Send + Sync {
async fn watch_adverts(&self) -> Result<mpsc::Receiver<RawAdvert>> {
let (_tx, rx) = mpsc::channel(1);
Ok(rx)
}
}
pub(crate) const DEFAULT_FRAGMENT_SIZE: usize = 180;
#[cfg(test)]
pub(crate) struct MockGatt {
values: std::sync::Mutex<std::collections::HashMap<String, Vec<u8>>>,
queued:
std::sync::Mutex<std::collections::HashMap<String, std::collections::VecDeque<Vec<u8>>>>,
services: std::sync::Mutex<Vec<GattService>>,
senders: std::sync::Mutex<std::collections::HashMap<String, mpsc::Sender<Vec<u8>>>>,
generation: std::sync::atomic::AtomicU64,
advert_tx: mpsc::Sender<RawAdvert>,
advert_rx: std::sync::Mutex<Option<mpsc::Receiver<RawAdvert>>>,
}
#[cfg(test)]
impl Default for MockGatt {
fn default() -> Self {
let (advert_tx, advert_rx) = mpsc::channel(16);
Self {
values: std::sync::Mutex::new(std::collections::HashMap::new()),
queued: std::sync::Mutex::new(std::collections::HashMap::new()),
services: std::sync::Mutex::new(Vec::new()),
senders: std::sync::Mutex::new(std::collections::HashMap::new()),
generation: std::sync::atomic::AtomicU64::new(0),
advert_tx,
advert_rx: std::sync::Mutex::new(Some(advert_rx)),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] impl MockGatt {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn with_services(self, services: Vec<GattService>) -> Self {
*self.services.lock().unwrap() = services;
self
}
#[allow(dead_code)] pub(crate) fn queue_read(&self, char_uuid: &str, value: Vec<u8>) {
self.queued
.lock()
.unwrap()
.entry(char_uuid.to_string())
.or_default()
.push_back(value);
}
#[allow(dead_code)] pub(crate) fn notifier(&self, char_uuid: &str) -> Option<mpsc::Sender<Vec<u8>>> {
self.senders.lock().unwrap().get(char_uuid).cloned()
}
#[allow(dead_code)] pub(crate) fn bump_generation(&self) {
self.generation
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
#[allow(dead_code)] pub(crate) fn advert_sender(&self) -> mpsc::Sender<RawAdvert> {
self.advert_tx.clone()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] #[async_trait]
impl AdvertSource for MockGatt {
async fn watch_adverts(&self) -> Result<mpsc::Receiver<RawAdvert>> {
self.advert_rx.lock().unwrap().take().map_or_else(
|| {
let (_tx, rx) = mpsc::channel(1);
Ok(rx)
},
Ok,
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] #[async_trait]
impl GattConnection for MockGatt {
async fn instance_id(&self, char_uuid: &str) -> Result<u16> {
self.services
.lock()
.unwrap()
.iter()
.flat_map(|s| &s.characteristics)
.find(|c| c.uuid.eq_ignore_ascii_case(char_uuid))
.map(|c| c.iid)
.ok_or(crate::error::BleError::CharacteristicNotFound { aid: 0, iid: 0 })
}
async fn write(&self, char_uuid: &str, value: &[u8]) -> Result<()> {
self.values
.lock()
.unwrap()
.insert(char_uuid.to_string(), value.to_vec());
Ok(())
}
async fn read(&self, char_uuid: &str) -> Result<Vec<u8>> {
if let Some(q) = self.queued.lock().unwrap().get_mut(char_uuid) {
if let Some(v) = q.pop_front() {
return Ok(v);
}
}
Ok(self
.values
.lock()
.unwrap()
.get(char_uuid)
.cloned()
.unwrap_or_default())
}
async fn subscribe(&self, char_uuid: &str) -> Result<mpsc::Receiver<Vec<u8>>> {
let (tx, rx) = mpsc::channel(8);
self.senders
.lock()
.unwrap()
.insert(char_uuid.to_string(), tx);
Ok(rx)
}
async fn enumerate(&self) -> Result<Vec<GattService>> {
Ok(self.services.lock().unwrap().clone())
}
async fn generation(&self) -> u64 {
self.generation.load(std::sync::atomic::Ordering::SeqCst)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn mock_echoes_written_value_on_read() {
let gatt = MockGatt::new();
gatt.write("char-a", &[1, 2, 3]).await.unwrap();
assert_eq!(gatt.read("char-a").await.unwrap(), vec![1, 2, 3]);
}
#[tokio::test]
#[allow(clippy::unwrap_used)]
async fn mock_enumerate_returns_seeded_db() {
let svc = GattService {
uuid: "svc".into(),
iid: 1,
characteristics: vec![GattCharacteristic {
uuid: "c".into(),
iid: 2,
}],
};
let gatt = MockGatt::new().with_services(vec![svc.clone()]);
assert_eq!(gatt.enumerate().await.unwrap(), vec![svc]);
}
}