1use crate::error::Result;
5use async_trait::async_trait;
6use tokio::sync::mpsc;
7
8pub(crate) const HAP_INSTANCE_ID_DESC: &str = "dc46f0fe-81d2-4616-b5d9-6abdd796939a";
12
13pub(crate) const HAP_SERVICE_ID_CHAR: &str = "e604e95d-a759-4817-87d3-aa005083a0d1";
16
17pub(crate) fn u16_le(v: &[u8]) -> Option<u16> {
19 match v {
20 [lo, hi, ..] => Some(u16::from_le_bytes([*lo, *hi])),
21 _ => None,
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct GattCharacteristic {
28 pub uuid: String,
30 pub iid: u16,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct GattService {
37 pub uuid: String,
39 pub iid: u16,
41 pub characteristics: Vec<GattCharacteristic>,
43}
44
45#[async_trait]
48pub trait GattConnection: Send + Sync {
49 async fn write(&self, char_uuid: &str, value: &[u8]) -> Result<()>;
51 async fn read(&self, char_uuid: &str) -> Result<Vec<u8>>;
53 async fn subscribe(&self, char_uuid: &str) -> Result<mpsc::Receiver<Vec<u8>>>;
56 async fn instance_id(&self, char_uuid: &str) -> Result<u16>;
60 async fn enumerate(&self) -> Result<Vec<GattService>>;
62 async fn max_write(&self) -> usize {
66 DEFAULT_FRAGMENT_SIZE
67 }
68 async fn generation(&self) -> u64 {
74 0
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct RawAdvert {
82 pub manufacturer_data: Vec<u8>,
84}
85
86#[async_trait]
91pub trait AdvertSource: Send + Sync {
92 async fn watch_adverts(&self) -> Result<mpsc::Receiver<RawAdvert>> {
97 let (_tx, rx) = mpsc::channel(1);
98 Ok(rx)
99 }
100}
101
102pub(crate) const DEFAULT_FRAGMENT_SIZE: usize = 180;
105
106#[cfg(test)]
112pub(crate) struct MockGatt {
113 values: std::sync::Mutex<std::collections::HashMap<String, Vec<u8>>>,
114 queued:
115 std::sync::Mutex<std::collections::HashMap<String, std::collections::VecDeque<Vec<u8>>>>,
116 services: std::sync::Mutex<Vec<GattService>>,
117 senders: std::sync::Mutex<std::collections::HashMap<String, mpsc::Sender<Vec<u8>>>>,
118 generation: std::sync::atomic::AtomicU64,
119 advert_tx: mpsc::Sender<RawAdvert>,
120 advert_rx: std::sync::Mutex<Option<mpsc::Receiver<RawAdvert>>>,
121 blocked:
123 std::sync::Mutex<std::collections::HashMap<String, std::sync::Arc<tokio::sync::Notify>>>,
124}
125
126#[cfg(test)]
127impl Default for MockGatt {
128 fn default() -> Self {
129 let (advert_tx, advert_rx) = mpsc::channel(16);
130 Self {
131 values: std::sync::Mutex::new(std::collections::HashMap::new()),
132 queued: std::sync::Mutex::new(std::collections::HashMap::new()),
133 services: std::sync::Mutex::new(Vec::new()),
134 senders: std::sync::Mutex::new(std::collections::HashMap::new()),
135 generation: std::sync::atomic::AtomicU64::new(0),
136 advert_tx,
137 advert_rx: std::sync::Mutex::new(Some(advert_rx)),
138 blocked: std::sync::Mutex::new(std::collections::HashMap::new()),
139 }
140 }
141}
142
143#[cfg(test)]
144#[allow(clippy::unwrap_used)] impl MockGatt {
146 pub(crate) fn new() -> Self {
147 Self::default()
148 }
149
150 pub(crate) fn with_services(self, services: Vec<GattService>) -> Self {
151 *self.services.lock().unwrap() = services;
152 self
153 }
154
155 #[allow(dead_code)] pub(crate) fn queue_read(&self, char_uuid: &str, value: Vec<u8>) {
159 self.queued
160 .lock()
161 .unwrap()
162 .entry(char_uuid.to_string())
163 .or_default()
164 .push_back(value);
165 }
166
167 #[allow(dead_code)] pub(crate) fn notifier(&self, char_uuid: &str) -> Option<mpsc::Sender<Vec<u8>>> {
170 self.senders.lock().unwrap().get(char_uuid).cloned()
171 }
172
173 #[allow(dead_code)] pub(crate) fn bump_generation(&self) {
177 self.generation
178 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
179 }
180
181 #[allow(dead_code)] pub(crate) fn advert_sender(&self) -> mpsc::Sender<RawAdvert> {
184 self.advert_tx.clone()
185 }
186
187 #[allow(dead_code)] pub(crate) fn block_next_read(&self, char_uuid: &str) -> std::sync::Arc<tokio::sync::Notify> {
192 let gate = std::sync::Arc::new(tokio::sync::Notify::new());
193 self.blocked
194 .lock()
195 .unwrap()
196 .insert(char_uuid.to_string(), gate.clone());
197 gate
198 }
199}
200
201#[cfg(test)]
202#[allow(clippy::unwrap_used)] #[async_trait]
204impl AdvertSource for MockGatt {
205 async fn watch_adverts(&self) -> Result<mpsc::Receiver<RawAdvert>> {
206 self.advert_rx.lock().unwrap().take().map_or_else(
208 || {
209 let (_tx, rx) = mpsc::channel(1);
210 Ok(rx)
211 },
212 Ok,
213 )
214 }
215}
216
217#[cfg(test)]
218#[allow(clippy::unwrap_used)] #[async_trait]
220impl GattConnection for MockGatt {
221 async fn instance_id(&self, char_uuid: &str) -> Result<u16> {
222 self.services
223 .lock()
224 .unwrap()
225 .iter()
226 .flat_map(|s| &s.characteristics)
227 .find(|c| c.uuid.eq_ignore_ascii_case(char_uuid))
228 .map(|c| c.iid)
229 .ok_or(crate::error::BleError::CharacteristicNotFound { aid: 0, iid: 0 })
230 }
231
232 async fn write(&self, char_uuid: &str, value: &[u8]) -> Result<()> {
233 self.values
234 .lock()
235 .unwrap()
236 .insert(char_uuid.to_string(), value.to_vec());
237 Ok(())
238 }
239
240 async fn read(&self, char_uuid: &str) -> Result<Vec<u8>> {
241 let gate = self.blocked.lock().unwrap().remove(char_uuid);
242 if let Some(gate) = gate {
243 gate.notified().await;
244 }
245 if let Some(q) = self.queued.lock().unwrap().get_mut(char_uuid) {
246 if let Some(v) = q.pop_front() {
247 return Ok(v);
248 }
249 }
250 Ok(self
251 .values
252 .lock()
253 .unwrap()
254 .get(char_uuid)
255 .cloned()
256 .unwrap_or_default())
257 }
258
259 async fn subscribe(&self, char_uuid: &str) -> Result<mpsc::Receiver<Vec<u8>>> {
260 let (tx, rx) = mpsc::channel(8);
261 self.senders
262 .lock()
263 .unwrap()
264 .insert(char_uuid.to_string(), tx);
265 Ok(rx)
266 }
267
268 async fn enumerate(&self) -> Result<Vec<GattService>> {
269 Ok(self.services.lock().unwrap().clone())
270 }
271
272 async fn generation(&self) -> u64 {
273 self.generation.load(std::sync::atomic::Ordering::SeqCst)
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[tokio::test]
282 #[allow(clippy::unwrap_used)]
283 async fn mock_echoes_written_value_on_read() {
284 let gatt = MockGatt::new();
285 gatt.write("char-a", &[1, 2, 3]).await.unwrap();
286 assert_eq!(gatt.read("char-a").await.unwrap(), vec![1, 2, 3]);
287 }
288
289 #[tokio::test]
290 #[allow(clippy::unwrap_used)]
291 async fn mock_enumerate_returns_seeded_db() {
292 let svc = GattService {
293 uuid: "svc".into(),
294 iid: 1,
295 characteristics: vec![GattCharacteristic {
296 uuid: "c".into(),
297 iid: 2,
298 }],
299 };
300 let gatt = MockGatt::new().with_services(vec![svc.clone()]);
301 assert_eq!(gatt.enumerate().await.unwrap(), vec![svc]);
302 }
303}