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}
122
123#[cfg(test)]
124impl Default for MockGatt {
125 fn default() -> Self {
126 let (advert_tx, advert_rx) = mpsc::channel(16);
127 Self {
128 values: std::sync::Mutex::new(std::collections::HashMap::new()),
129 queued: std::sync::Mutex::new(std::collections::HashMap::new()),
130 services: std::sync::Mutex::new(Vec::new()),
131 senders: std::sync::Mutex::new(std::collections::HashMap::new()),
132 generation: std::sync::atomic::AtomicU64::new(0),
133 advert_tx,
134 advert_rx: std::sync::Mutex::new(Some(advert_rx)),
135 }
136 }
137}
138
139#[cfg(test)]
140#[allow(clippy::unwrap_used)] impl MockGatt {
142 pub(crate) fn new() -> Self {
143 Self::default()
144 }
145
146 pub(crate) fn with_services(self, services: Vec<GattService>) -> Self {
147 *self.services.lock().unwrap() = services;
148 self
149 }
150
151 #[allow(dead_code)] pub(crate) fn queue_read(&self, char_uuid: &str, value: Vec<u8>) {
155 self.queued
156 .lock()
157 .unwrap()
158 .entry(char_uuid.to_string())
159 .or_default()
160 .push_back(value);
161 }
162
163 #[allow(dead_code)] pub(crate) fn notifier(&self, char_uuid: &str) -> Option<mpsc::Sender<Vec<u8>>> {
166 self.senders.lock().unwrap().get(char_uuid).cloned()
167 }
168
169 #[allow(dead_code)] pub(crate) fn bump_generation(&self) {
173 self.generation
174 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
175 }
176
177 #[allow(dead_code)] pub(crate) fn advert_sender(&self) -> mpsc::Sender<RawAdvert> {
180 self.advert_tx.clone()
181 }
182}
183
184#[cfg(test)]
185#[allow(clippy::unwrap_used)] #[async_trait]
187impl AdvertSource for MockGatt {
188 async fn watch_adverts(&self) -> Result<mpsc::Receiver<RawAdvert>> {
189 self.advert_rx.lock().unwrap().take().map_or_else(
191 || {
192 let (_tx, rx) = mpsc::channel(1);
193 Ok(rx)
194 },
195 Ok,
196 )
197 }
198}
199
200#[cfg(test)]
201#[allow(clippy::unwrap_used)] #[async_trait]
203impl GattConnection for MockGatt {
204 async fn instance_id(&self, char_uuid: &str) -> Result<u16> {
205 self.services
206 .lock()
207 .unwrap()
208 .iter()
209 .flat_map(|s| &s.characteristics)
210 .find(|c| c.uuid.eq_ignore_ascii_case(char_uuid))
211 .map(|c| c.iid)
212 .ok_or(crate::error::BleError::CharacteristicNotFound { aid: 0, iid: 0 })
213 }
214
215 async fn write(&self, char_uuid: &str, value: &[u8]) -> Result<()> {
216 self.values
217 .lock()
218 .unwrap()
219 .insert(char_uuid.to_string(), value.to_vec());
220 Ok(())
221 }
222
223 async fn read(&self, char_uuid: &str) -> Result<Vec<u8>> {
224 if let Some(q) = self.queued.lock().unwrap().get_mut(char_uuid) {
225 if let Some(v) = q.pop_front() {
226 return Ok(v);
227 }
228 }
229 Ok(self
230 .values
231 .lock()
232 .unwrap()
233 .get(char_uuid)
234 .cloned()
235 .unwrap_or_default())
236 }
237
238 async fn subscribe(&self, char_uuid: &str) -> Result<mpsc::Receiver<Vec<u8>>> {
239 let (tx, rx) = mpsc::channel(8);
240 self.senders
241 .lock()
242 .unwrap()
243 .insert(char_uuid.to_string(), tx);
244 Ok(rx)
245 }
246
247 async fn enumerate(&self) -> Result<Vec<GattService>> {
248 Ok(self.services.lock().unwrap().clone())
249 }
250
251 async fn generation(&self) -> u64 {
252 self.generation.load(std::sync::atomic::Ordering::SeqCst)
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[tokio::test]
261 #[allow(clippy::unwrap_used)]
262 async fn mock_echoes_written_value_on_read() {
263 let gatt = MockGatt::new();
264 gatt.write("char-a", &[1, 2, 3]).await.unwrap();
265 assert_eq!(gatt.read("char-a").await.unwrap(), vec![1, 2, 3]);
266 }
267
268 #[tokio::test]
269 #[allow(clippy::unwrap_used)]
270 async fn mock_enumerate_returns_seeded_db() {
271 let svc = GattService {
272 uuid: "svc".into(),
273 iid: 1,
274 characteristics: vec![GattCharacteristic {
275 uuid: "c".into(),
276 iid: 2,
277 }],
278 };
279 let gatt = MockGatt::new().with_services(vec![svc.clone()]);
280 assert_eq!(gatt.enumerate().await.unwrap(), vec![svc]);
281 }
282}