1use crate::broadcast_state::BleBroadcastState;
5use crate::db;
6use crate::error::{BleError, Result};
7use crate::gatt::{GattConnection, GattService};
8use crate::pairing;
9use crate::pdu::{self, OpCode};
10use crate::session::BleSession;
11use hap_crypto::{AccessoryPairing, ControllerKeypair};
12use hap_model::format::{CharFormat, CharValue};
13use hap_model::tree::Accessory;
14use hap_model::{CharacteristicType, ServiceType};
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::sync::Mutex;
18use tokio_stream::StreamExt as _;
19
20fn gsn_is_newer(new: u16, last: u16) -> bool {
24 let diff = new.wrapping_sub(last);
25 diff != 0 && diff < 0x8000
26}
27
28fn parse_device_id(s: &str) -> Option<[u8; 6]> {
34 let mut out = [0u8; 6];
35 let mut parts = s.split(':');
36 for slot in &mut out {
37 let p = parts.next()?;
38 if p.len() != 2 {
39 return None;
40 }
41 *slot = u8::from_str_radix(p, 16).ok()?;
42 }
43 parts.next().is_none().then_some(out)
44}
45
46const MAX_REVIVE_RETRIES: u32 = 3;
49
50const ENABLE_BROADCAST_BODY: [u8; 7] = [0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01];
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct CharacteristicEvent {
57 pub aid: u64,
59 pub iid: u64,
61 pub value: CharValue,
63}
64
65struct Secure {
68 session: BleSession,
69 tid: u8,
70 generation: u64,
74}
75
76struct Reviver {
79 keypair: ControllerKeypair,
80 pairing: AccessoryPairing,
81 verify_char: String,
82 verify_iid: u16,
83 frag_size: usize,
84}
85
86pub(crate) struct SecureContext {
91 pub session: BleSession,
93 pub session_generation: u64,
95 pub keypair: ControllerKeypair,
97 pub pairing: AccessoryPairing,
99 pub verify_char: String,
101 pub verify_iid: u16,
102 pub pairings_char: String,
104 pub pairings_iid: u16,
105 pub broadcast_key: hap_crypto::BroadcastKey,
107 pub initial_gsn: u16,
109}
110
111async fn revive_if_stale(
115 gatt: &dyn GattConnection,
116 s: &mut Secure,
117 reviver: &Reviver,
118) -> Result<()> {
119 if gatt.generation().await <= s.generation {
120 return Ok(());
121 }
122 let (session, _bkey) = pairing::pair_verify(
123 gatt,
124 &reviver.verify_char,
125 reviver.verify_iid,
126 &reviver.keypair,
127 &reviver.pairing,
128 reviver.frag_size,
129 )
130 .await?;
131 s.session = session;
132 s.tid = 0;
133 s.generation = gatt.generation().await;
136 Ok(())
137}
138
139mod pairings_tlv {
141 pub(super) const STATE: u8 = 0x06;
142 pub(super) const METHOD: u8 = 0x00;
143 pub(super) const IDENTIFIER: u8 = 0x01;
144 pub(super) const ERROR: u8 = 0x07;
145 pub(super) const STATE_M1: u8 = 0x01;
146 pub(super) const STATE_M2: u8 = 0x02;
147 pub(super) const METHOD_REMOVE: u8 = 0x04;
148}
149
150fn encode_remove_pairing(controller_id: &str) -> Vec<u8> {
153 let mut out = Vec::new();
154 let mut w = hap_tlv8::Tlv8Writer::new(&mut out);
155 w.push_u8(pairings_tlv::STATE, pairings_tlv::STATE_M1);
156 w.push_u8(pairings_tlv::METHOD, pairings_tlv::METHOD_REMOVE);
157 w.push(pairings_tlv::IDENTIFIER, controller_id.as_bytes());
158 out
159}
160
161fn expect_remove_m2(tlv: &[u8]) -> Result<()> {
164 let map = hap_tlv8::Tlv8Map::parse(tlv)?;
165 if let Some(err) = map.get(pairings_tlv::ERROR) {
166 return Err(BleError::PairingRejected(err.first().copied().unwrap_or(1)));
167 }
168 match map
169 .get(pairings_tlv::STATE)
170 .and_then(|s| s.first().copied())
171 {
172 Some(pairings_tlv::STATE_M2) => Ok(()),
173 _ => Err(BleError::MalformedPdu("remove-pairing reply not state M2")),
174 }
175}
176
177async fn dedup_should_emit(emitted: &Mutex<HashMap<u64, u16>>, iid: u64, gsn: u16) -> bool {
186 let mut e = emitted.lock().await;
187 let prev = e.get(&iid).copied();
188 if prev == Some(gsn) {
189 return false;
190 }
191 if prev.is_none_or(|p| gsn_is_newer(gsn, p)) {
192 e.insert(iid, gsn);
193 }
194 true
195}
196
197async fn read_char_raw(
202 gatt: &dyn GattConnection,
203 secure: &Mutex<Secure>,
204 reviver: &Reviver,
205 uuid: &str,
206 iid: u64,
207 frag_size: usize,
208) -> Result<Vec<u8>> {
209 let iid16 = u16::try_from(iid).map_err(|_| BleError::CharacteristicNotFound { aid: 0, iid })?;
210 let mut s = secure.lock().await;
211 let mut attempts = 0;
212 loop {
213 revive_if_stale(gatt, &mut s, reviver).await?;
214 s.tid = s.tid.wrapping_add(1);
215 let tid = s.tid;
216 match pdu::request_secure(
217 gatt,
218 &mut s.session,
219 uuid,
220 OpCode::CharacteristicRead,
221 tid,
222 iid16,
223 &[],
224 frag_size,
225 )
226 .await
227 {
228 Ok(resp) => return pdu::value_param(&resp.body),
229 Err(e) => {
233 attempts += 1;
234 if attempts < MAX_REVIVE_RETRIES && gatt.generation().await > s.generation {
235 continue;
236 }
237 return Err(e);
238 }
239 }
240 }
241}
242
243async fn write_char_raw(
250 gatt: &dyn GattConnection,
251 secure: &Mutex<Secure>,
252 reviver: &Reviver,
253 uuid: &str,
254 iid: u64,
255 value_bytes: &[u8],
256 frag_size: usize,
257) -> Result<()> {
258 let iid16 = u16::try_from(iid).map_err(|_| BleError::CharacteristicNotFound { aid: 0, iid })?;
259 let body = pdu::encode_write_body(value_bytes);
260 let mut s = secure.lock().await;
261 let mut attempts = 0;
262 loop {
263 revive_if_stale(gatt, &mut s, reviver).await?;
264 s.tid = s.tid.wrapping_add(1);
265 let tid = s.tid;
266 match pdu::request_secure(
267 gatt,
268 &mut s.session,
269 uuid,
270 OpCode::CharacteristicWrite,
271 tid,
272 iid16,
273 &body,
274 frag_size,
275 )
276 .await
277 {
278 Ok(resp) if resp.status != 0 => return Err(BleError::RequestRejected(resp.status)),
279 Ok(_) => return Ok(()),
280 Err(e) => {
281 attempts += 1;
282 if attempts < MAX_REVIVE_RETRIES && gatt.generation().await > s.generation {
283 continue;
284 }
285 return Err(e);
286 }
287 }
288 }
289}
290
291pub struct BleAccessory {
295 gatt: Arc<dyn GattConnection>,
296 secure: Arc<Mutex<Secure>>,
297 reviver: Arc<Reviver>,
298 pairings: (String, u16),
300 frag_size: usize,
301 accessories: Vec<Accessory>,
302 chars: HashMap<(u64, u64), (String, CharFormat)>,
304 events_tx: tokio::sync::broadcast::Sender<CharacteristicEvent>,
305 tasks: Vec<tokio::task::JoinHandle<()>>,
307 last_gsn: Arc<Mutex<u16>>,
309 emitted: Arc<Mutex<HashMap<u64, u16>>>,
312 broadcast_key: hap_crypto::BroadcastKey,
314 advert_source: Option<Arc<dyn crate::gatt::AdvertSource>>,
318}
319
320impl Drop for BleAccessory {
321 fn drop(&mut self) {
322 for task in &self.tasks {
323 task.abort();
324 }
325 }
326}
327
328impl BleAccessory {
329 pub(crate) fn new(
336 gatt: Arc<dyn GattConnection>,
337 ctx: SecureContext,
338 frag_size: usize,
339 gatt_services: &[GattService],
340 accessories: Vec<Accessory>,
341 ) -> Self {
342 let (events_tx, _) = tokio::sync::broadcast::channel(64);
343 let mut uuid_by_iid: HashMap<u64, String> = HashMap::new();
347 for gs in gatt_services {
348 for gc in &gs.characteristics {
349 uuid_by_iid.insert(u64::from(gc.iid), gc.uuid.clone());
350 }
351 }
352 let mut chars = HashMap::new();
353 for acc in &accessories {
354 for svc in &acc.services {
355 for ch in &svc.characteristics {
356 if let Some(uuid) = uuid_by_iid.get(&ch.iid) {
357 chars.insert((acc.aid, ch.iid), (uuid.clone(), ch.format));
358 }
359 }
360 }
361 }
362 Self {
363 gatt,
364 secure: Arc::new(Mutex::new(Secure {
365 session: ctx.session,
366 tid: 0,
367 generation: ctx.session_generation,
368 })),
369 reviver: Arc::new(Reviver {
370 keypair: ctx.keypair,
371 pairing: ctx.pairing,
372 verify_char: ctx.verify_char,
373 verify_iid: ctx.verify_iid,
374 frag_size,
375 }),
376 pairings: (ctx.pairings_char, ctx.pairings_iid),
377 frag_size,
378 accessories,
379 chars,
380 events_tx,
381 tasks: Vec::new(),
382 last_gsn: Arc::new(Mutex::new(ctx.initial_gsn)),
383 emitted: Arc::new(Mutex::new(HashMap::new())),
384 broadcast_key: ctx.broadcast_key,
385 advert_source: None,
386 }
387 }
388
389 pub fn accessories(&self) -> &[Accessory] {
391 &self.accessories
392 }
393
394 pub async fn broadcast_state(&self) -> BleBroadcastState {
397 BleBroadcastState {
398 key: self.broadcast_key.clone(),
399 gsn: *self.last_gsn.lock().await,
400 }
401 }
402
403 #[allow(clippy::needless_pass_by_value)]
411 pub fn find(&self, svc: ServiceType, chr: CharacteristicType) -> Result<(u64, u64)> {
412 for acc in &self.accessories {
413 for service in &acc.services {
414 if service.service_type == svc {
415 for ch in &service.characteristics {
416 if ch.char_type == chr {
417 return Ok((acc.aid, ch.iid));
418 }
419 }
420 }
421 }
422 }
423 Err(BleError::CharacteristicNotFound { aid: 0, iid: 0 })
424 }
425
426 pub async fn read(&mut self, aid: u64, iid: u64) -> Result<CharValue> {
431 let (uuid, format) = self
432 .chars
433 .get(&(aid, iid))
434 .cloned()
435 .ok_or(BleError::CharacteristicNotFound { aid, iid })?;
436 let raw = read_char_raw(
437 self.gatt.as_ref(),
438 &self.secure,
439 &self.reviver,
440 &uuid,
441 iid,
442 self.frag_size,
443 )
444 .await?;
445 db::decode_value(format, &raw)
446 }
447
448 pub async fn remove_pairing(&mut self, controller_id: &str) -> Result<()> {
469 let (uuid, iid) = self.pairings.clone();
470 let removing_self = controller_id == self.reviver.keypair.id;
471 let tlv = encode_remove_pairing(controller_id);
472 let body = pdu::encode_write_body(&tlv);
473 let mut s = self.secure.lock().await;
474 revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
475 s.tid = s.tid.wrapping_add(1);
476 let tid = s.tid;
477 let result = pdu::request_secure(
478 self.gatt.as_ref(),
479 &mut s.session,
480 &uuid,
481 OpCode::CharacteristicWrite,
482 tid,
483 iid,
484 &body,
485 self.frag_size,
486 )
487 .await;
488 match result {
489 Ok(resp) if resp.status != 0 => Err(BleError::PairingRejected(resp.status)),
490 Ok(resp) => expect_remove_m2(&pdu::value_param(&resp.body)?),
491 Err(BleError::Disconnected | BleError::Crypto(_)) if removing_self => Ok(()),
495 Err(e) => Err(e),
496 }
497 }
498
499 pub async fn write(&mut self, aid: u64, iid: u64, value: CharValue) -> Result<()> {
508 let (uuid, format) = self
509 .chars
510 .get(&(aid, iid))
511 .cloned()
512 .ok_or(BleError::CharacteristicNotFound { aid, iid })?;
513 let bytes = db::encode_value(format, &value)?;
514 write_char_raw(
515 self.gatt.as_ref(),
516 &self.secure,
517 &self.reviver,
518 &uuid,
519 iid,
520 &bytes,
521 self.frag_size,
522 )
523 .await
524 }
525
526 #[must_use]
528 pub fn pairing_id(&self) -> &str {
529 &self.reviver.pairing.pairing_id
530 }
531
532 pub async fn enable_broadcasts(&mut self, iids: &[u64]) -> Result<()> {
543 let mut s = self.secure.lock().await;
544 for &iid in iids {
545 let Some((uuid, _)) = self.chars.get(&(1, iid)).cloned() else {
546 tracing::debug!(
547 iid,
548 "enable_broadcasts: characteristic not on this accessory — skipped"
549 );
550 continue;
551 };
552 let Ok(iid16) = u16::try_from(iid) else {
553 tracing::debug!(iid, "enable_broadcasts: iid exceeds u16 — skipped");
554 continue;
555 };
556 revive_if_stale(self.gatt.as_ref(), &mut s, &self.reviver).await?;
557 s.tid = s.tid.wrapping_add(1);
558 let tid = s.tid;
559 match pdu::request_secure(
565 self.gatt.as_ref(),
566 &mut s.session,
567 &uuid,
568 OpCode::CharacteristicConfig,
569 tid,
570 iid16,
571 &ENABLE_BROADCAST_BODY,
572 self.frag_size,
573 )
574 .await
575 {
576 Ok(r) if r.status == 0 => {
577 tracing::debug!(iid, "enable_broadcasts: accepted by accessory");
578 }
579 Ok(r) => {
580 tracing::debug!(
581 iid,
582 status = r.status,
583 "enable_broadcasts: rejected by accessory (non-zero HAP status)"
584 );
585 }
586 Err(e) => {
587 tracing::debug!(iid, error = %e, "enable_broadcasts: write failed");
588 }
589 }
590 }
591 Ok(())
592 }
593
594 pub async fn disconnect(&self) {
597 self.gatt.disconnect().await;
598 }
599
600 pub async fn subscribe(&mut self, aid: u64, iid: u64) -> Result<()> {
613 let (uuid, format) = self
614 .chars
615 .get(&(aid, iid))
616 .cloned()
617 .ok_or(BleError::CharacteristicNotFound { aid, iid })?;
618 let mut rx = self.gatt.subscribe(&uuid).await?;
619 let tx = self.events_tx.clone();
620 let gatt = self.gatt.clone();
621 let secure = self.secure.clone();
622 let reviver = self.reviver.clone();
623 let frag_size = self.frag_size;
624 let task = tokio::spawn(async move {
625 while rx.recv().await.is_some() {
627 if let Ok(raw) =
628 read_char_raw(gatt.as_ref(), &secure, &reviver, &uuid, iid, frag_size).await
629 {
630 if let Ok(value) = db::decode_value(format, &raw) {
631 let _ = tx.send(CharacteristicEvent { aid, iid, value });
632 }
633 }
634 }
635 });
636 self.tasks.push(task);
637 Ok(())
638 }
639
640 #[allow(clippy::too_many_lines)]
656 pub async fn watch_sleepy_events_with_source(
657 &mut self,
658 advert_source: Arc<dyn crate::gatt::AdvertSource>,
659 device_id: [u8; 6],
660 poll_iids: Vec<(u64, u64)>,
661 ) -> Result<()> {
662 let mut targets = Vec::new();
665 for (aid, iid) in poll_iids {
666 if let Some((uuid, format)) = self.chars.get(&(aid, iid)).cloned() {
667 targets.push((aid, iid, uuid, format));
668 }
669 }
670 let formats: std::collections::HashMap<u64, CharFormat> = self
672 .chars
673 .iter()
674 .map(|((_, iid), (_, f))| (*iid, *f))
675 .collect();
676 let broadcast_key = self.broadcast_key.clone();
677
678 let mut adverts = advert_source.watch_adverts().await?;
679
680 let (poll_tx, mut poll_rx) = tokio::sync::watch::channel(0u16);
685 if !targets.is_empty() {
686 let gatt = self.gatt.clone();
687 let secure = self.secure.clone();
688 let reviver = self.reviver.clone();
689 let frag = self.frag_size;
690 let poll_events = self.events_tx.clone();
691 let poll_emitted = self.emitted.clone();
692 let poll_task = tokio::spawn(async move {
693 while poll_rx.changed().await.is_ok() {
694 let gsn = *poll_rx.borrow_and_update();
695 tracing::debug!(
696 gsn,
697 targets = targets.len(),
698 "catch-up poll firing — reconnecting to read"
699 );
700 for (aid, iid, uuid, format) in &targets {
701 match read_char_raw(gatt.as_ref(), &secure, &reviver, uuid, *iid, frag)
702 .await
703 {
704 Ok(raw_val) => {
705 if let Ok(value) = db::decode_value(*format, &raw_val) {
706 if dedup_should_emit(&poll_emitted, *iid, gsn).await {
707 tracing::debug!(
708 aid = *aid,
709 iid = *iid,
710 gsn,
711 "catch-up poll emitting event"
712 );
713 let _ = poll_events.send(CharacteristicEvent {
714 aid: *aid,
715 iid: *iid,
716 value,
717 });
718 } else {
719 tracing::debug!(iid = *iid, gsn, "catch-up poll read ok but (iid,gsn) already emitted — deduped");
720 }
721 }
722 }
723 Err(e) => {
724 tracing::debug!(iid = *iid, error = %e, "catch-up poll read failed");
725 }
726 }
727 }
728 }
729 });
730 self.tasks.push(poll_task);
731 }
732
733 let tx = self.events_tx.clone();
734 let last_gsn = self.last_gsn.clone();
735 let emitted = self.emitted.clone();
736 let advert_task = tokio::spawn(async move {
737 tracing::debug!(target = ?device_id, "sleepy advert watch armed");
738 while let Some(raw) = adverts.recv().await {
739 tracing::trace!(
740 len = raw.manufacturer_data.len(),
741 first = ?raw.manufacturer_data.first(),
742 "advert frame reached the sleepy loop"
743 );
744 match crate::advert::HapAdvert::parse(&raw.manufacturer_data) {
745 Some(crate::advert::HapAdvert::Regular {
746 device_id: d, gsn, ..
747 }) => {
748 if d != device_id {
749 tracing::trace!(saw = ?d, target = ?device_id, "0x06 advert: device-id mismatch, ignoring");
750 continue;
751 }
752 {
753 let mut lg = last_gsn.lock().await;
754 if !gsn_is_newer(gsn, *lg) {
755 tracing::debug!(
756 gsn,
757 last_gsn = *lg,
758 "0x06 advert for our device: gsn NOT newer — poll suppressed"
759 );
760 continue;
761 }
762 tracing::debug!(
763 gsn,
764 prev = *lg,
765 "0x06 advert for our device: gsn bump — triggering poll"
766 );
767 *lg = gsn;
768 }
769 let _ = poll_tx.send(gsn);
772 }
773 Some(crate::advert::HapAdvert::EncryptedNotification {
774 advertising_id,
775 payload,
776 }) => {
777 if advertising_id != device_id {
778 tracing::trace!(saw = ?advertising_id, target = ?device_id, "0x11 broadcast: advertising-id mismatch, ignoring");
779 continue;
780 }
781 tracing::debug!(
782 ?advertising_id,
783 "0x11 encrypted broadcast for our device — attempting decrypt"
784 );
785 let start = *last_gsn.lock().await;
786 let candidates = std::iter::once(start.wrapping_add(1))
789 .chain(std::iter::once(start))
790 .chain((2..=100u16).map(|d| start.wrapping_add(d)));
791 for gsn in candidates {
792 let Ok(pt) = broadcast_key.open(gsn, &payload, &advertising_id) else {
793 continue;
794 };
795 if pt.len() < 12 {
796 continue;
797 }
798 if u16::from_le_bytes([pt[0], pt[1]]) != gsn {
799 continue;
800 }
801 if !gsn_is_newer(gsn, start) {
803 break;
804 }
805 let iid = u64::from(u16::from_le_bytes([pt[2], pt[3]]));
806 {
810 let mut lg = last_gsn.lock().await;
811 *lg = gsn;
812 }
813 let Some(format) = formats.get(&iid).copied() else {
814 break;
815 };
816 if let Ok(value) = db::decode_value(format, &pt[4..12]) {
817 if dedup_should_emit(&emitted, iid, gsn).await {
818 let _ = tx.send(CharacteristicEvent { aid: 1, iid, value });
819 }
820 }
821 break;
822 }
823 }
824 _ => {}
825 }
826 }
827 });
828 self.tasks.push(advert_task);
829 Ok(())
830 }
831
832 pub fn set_advert_source(&mut self, src: Arc<dyn crate::gatt::AdvertSource>) {
835 self.advert_source = Some(src);
836 }
837
838 pub async fn watch_sleepy_events(&mut self, poll_iids: Vec<(u64, u64)>) -> Result<()> {
848 let src = self.advert_source.clone().ok_or(BleError::NoAdvertSource)?;
849 let device_id =
850 parse_device_id(self.reviver.pairing.pairing_id.as_str()).ok_or_else(|| {
851 BleError::Backend("malformed pairing id; cannot derive device id".into())
852 })?;
853 self.watch_sleepy_events_with_source(src, device_id, poll_iids)
854 .await
855 }
856
857 pub fn events(&self) -> impl tokio_stream::Stream<Item = CharacteristicEvent> {
860 tokio_stream::wrappers::BroadcastStream::new(self.events_tx.subscribe())
861 .filter_map(std::result::Result::ok)
862 }
863}
864
865#[cfg(test)]
866mod tests {
867 use super::*;
868 use crate::test_support::ble_accessory_with_db;
869
870 #[tokio::test]
871 #[allow(clippy::unwrap_used)]
872 async fn find_locates_characteristic() {
873 let (h, _g) = ble_accessory_with_db().await;
874 let (aid, iid) = h
875 .find(ServiceType::LightBulb, CharacteristicType::On)
876 .unwrap();
877 assert_eq!((aid, iid), (1, 11));
878 }
879
880 #[tokio::test]
881 #[allow(clippy::unwrap_used)]
882 async fn find_missing_errors() {
883 let (h, _g) = ble_accessory_with_db().await;
884 let err = h
885 .find(ServiceType::LightBulb, CharacteristicType::Brightness)
886 .unwrap_err();
887 assert!(matches!(err, BleError::CharacteristicNotFound { .. }));
888 }
889
890 #[test]
891 fn encode_remove_pairing_matches_hap_layout() {
892 let tlv = encode_remove_pairing("c2");
894 assert_eq!(
895 tlv,
896 vec![0x06, 0x01, 0x01, 0x00, 0x01, 0x04, 0x01, 0x02, b'c', b'2']
897 );
898 }
899
900 #[test]
901 fn expect_remove_m2_accepts_m2_and_rejects_error() {
902 assert!(expect_remove_m2(&[0x06, 0x01, 0x02]).is_ok());
903 assert!(matches!(
905 expect_remove_m2(&[0x07, 0x01, 0x02]),
906 Err(BleError::PairingRejected(2))
907 ));
908 assert!(matches!(
910 expect_remove_m2(&[0x06, 0x01, 0x01]),
911 Err(BleError::MalformedPdu(_))
912 ));
913 }
914
915 #[tokio::test]
916 #[allow(clippy::unwrap_used)]
917 async fn remove_pairing_writes_request_and_accepts_m2() {
918 let (mut h, gatt) = ble_accessory_with_db().await;
919
920 let m2 = vec![0x06, 0x01, 0x02];
923 let vbody = crate::pdu::encode_value_param(&m2);
924 let mut plain = vec![0x02, 0x01, 0x00];
925 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
926 plain.extend_from_slice(&vbody);
927 let sealed =
928 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
929 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", sealed);
930
931 h.remove_pairing("AE:EC:86:C0:BF:D7").await.unwrap();
932 }
933
934 #[tokio::test]
935 #[allow(clippy::unwrap_used)]
936 async fn remove_own_pairing_tolerates_session_teardown() {
937 let (mut h, gatt) = ble_accessory_with_db().await;
939 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
943 h.remove_pairing("test-controller").await.unwrap();
944 }
945
946 #[tokio::test]
947 #[allow(clippy::unwrap_used)]
948 async fn remove_other_pairing_propagates_teardown_error() {
949 let (mut h, gatt) = ble_accessory_with_db().await;
952 gatt.queue_read("00000050-0000-1000-8000-0026bb765291", vec![0u8; 24]);
953 let err = h.remove_pairing("some-other-controller").await.unwrap_err();
954 assert!(matches!(err, BleError::Crypto(_)));
955 }
956
957 #[tokio::test]
958 #[allow(clippy::unwrap_used)]
959 async fn subscribe_then_event_decodes_value() {
960 use tokio_stream::StreamExt as _;
961 let (mut h, gatt) = ble_accessory_with_db().await;
962
963 let mut plain = vec![0x02, 0x01, 0x00];
967 let vbody = crate::pdu::encode_value_param(&[0x01]); plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
969 plain.extend_from_slice(&vbody);
970 let sealed =
971 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
972 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
973
974 h.subscribe(1, 11).await.unwrap();
975 let mut events = h.events();
976
977 gatt.notifier("00000025-0000-1000-8000-0026bb765291")
979 .unwrap()
980 .send(Vec::new())
981 .await
982 .unwrap();
983
984 let ev = events.next().await.unwrap();
985 assert_eq!(ev.iid, 11);
986 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
987 }
988
989 #[tokio::test]
990 #[allow(clippy::unwrap_used)]
991 async fn gsn_bump_triggers_disconnected_event_read() {
992 use tokio_stream::StreamExt as _;
993 let (mut h, gatt) = ble_accessory_with_db().await;
994
995 let mut plain = vec![0x02, 0x01, 0x00];
998 let vbody = crate::pdu::encode_value_param(&[0x01]);
999 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1000 plain.extend_from_slice(&vbody);
1001 let sealed =
1002 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1003 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1004
1005 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1006 h.watch_sleepy_events_with_source(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
1007 .await
1008 .unwrap();
1009 let mut events = h.events();
1010
1011 gatt.advert_sender()
1013 .send(crate::gatt::RawAdvert {
1014 manufacturer_data: vec![
1015 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
1016 ],
1017 })
1018 .await
1019 .unwrap();
1020
1021 let ev = events.next().await.unwrap();
1022 assert_eq!(ev.iid, 11);
1023 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
1024 }
1025
1026 #[tokio::test]
1027 #[allow(clippy::unwrap_used)]
1028 async fn encrypted_broadcast_0x11_decrypts_and_emits_event() {
1029 use tokio_stream::StreamExt as _;
1030 let (mut h, gatt) = ble_accessory_with_db().await;
1032
1033 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1036 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1037 let mut pt = Vec::new();
1038 pt.extend_from_slice(&1u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed = key.seal(1, &pt, &aid_bytes);
1042
1043 let mut mfg = vec![0x11u8, 0x00];
1045 mfg.extend_from_slice(&aid_bytes);
1046 mfg.extend_from_slice(&sealed);
1047
1048 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1049 h.watch_sleepy_events_with_source(advert_source, aid_bytes, vec![])
1051 .await
1052 .unwrap();
1053 let mut events = h.events();
1054
1055 gatt.advert_sender()
1056 .send(crate::gatt::RawAdvert {
1057 manufacturer_data: mfg,
1058 })
1059 .await
1060 .unwrap();
1061
1062 let ev = events.next().await.unwrap();
1063 assert_eq!(ev.aid, 1);
1064 assert_eq!(ev.iid, 11);
1065 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
1066 }
1067
1068 #[test]
1069 fn gsn_is_newer_handles_wraparound() {
1070 assert!(gsn_is_newer(6, 5));
1071 assert!(!gsn_is_newer(5, 5));
1072 assert!(!gsn_is_newer(4, 5));
1073 assert!(gsn_is_newer(1, 65535)); assert!(!gsn_is_newer(65535, 1)); }
1076
1077 #[tokio::test]
1078 #[allow(clippy::unwrap_used)]
1079 async fn same_change_via_poll_and_broadcast_emits_once() {
1080 use tokio_stream::StreamExt as _;
1081 let (mut h, gatt) = ble_accessory_with_db().await;
1082
1083 let mut plain = vec![0x02, 0x01, 0x00];
1086 let vbody = crate::pdu::encode_value_param(&[0x01]);
1087 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1088 plain.extend_from_slice(&vbody);
1089 let sealed =
1090 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1091 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1092
1093 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1094 h.watch_sleepy_events_with_source(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
1095 .await
1096 .unwrap();
1097 let mut events = h.events();
1098
1099 gatt.advert_sender()
1101 .send(crate::gatt::RawAdvert {
1102 manufacturer_data: vec![
1103 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
1104 ],
1105 })
1106 .await
1107 .unwrap();
1108
1109 let ev = events.next().await.unwrap();
1111 assert_eq!(ev.iid, 11);
1112 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
1113
1114 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1116 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1117 let mut pt = Vec::new();
1118 pt.extend_from_slice(&9u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed_bc = key.seal(9, &pt, &aid_bytes);
1122
1123 let mut mfg = vec![0x11u8, 0x00];
1124 mfg.extend_from_slice(&aid_bytes);
1125 mfg.extend_from_slice(&sealed_bc);
1126
1127 gatt.advert_sender()
1128 .send(crate::gatt::RawAdvert {
1129 manufacturer_data: mfg,
1130 })
1131 .await
1132 .unwrap();
1133
1134 let timeout_result =
1136 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1137 assert!(
1138 timeout_result.is_err(),
1139 "expected dedup to suppress the duplicate 0x11 broadcast event, but got one"
1140 );
1141 }
1142
1143 #[tokio::test]
1148 #[allow(clippy::unwrap_used)]
1149 async fn broadcast_delivered_while_poll_read_blocked() {
1150 use tokio_stream::StreamExt as _;
1151 let (mut h, gatt) = ble_accessory_with_db().await;
1152
1153 let release = gatt.block_next_read("00000025-0000-1000-8000-0026bb765291");
1156 let mut plain = vec![0x02, 0x01, 0x00];
1157 let vbody = crate::pdu::encode_value_param(&[0x01]);
1158 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1159 plain.extend_from_slice(&vbody);
1160 let sealed =
1161 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1162 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1163
1164 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1165 h.watch_sleepy_events_with_source(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
1166 .await
1167 .unwrap();
1168 let mut events = h.events();
1169
1170 gatt.advert_sender()
1172 .send(crate::gatt::RawAdvert {
1173 manufacturer_data: vec![
1174 0x06, 0x21, 0x01, 1, 2, 3, 4, 5, 6, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
1175 ],
1176 })
1177 .await
1178 .unwrap();
1179
1180 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1183 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1184 let mut pt = Vec::new();
1185 pt.extend_from_slice(&10u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x00, 0, 0, 0, 0, 0, 0, 0]); let sealed_bc = key.seal(10, &pt, &aid_bytes);
1189 let mut mfg = vec![0x11u8, 0x00];
1190 mfg.extend_from_slice(&aid_bytes);
1191 mfg.extend_from_slice(&sealed_bc);
1192 gatt.advert_sender()
1193 .send(crate::gatt::RawAdvert {
1194 manufacturer_data: mfg,
1195 })
1196 .await
1197 .unwrap();
1198
1199 let ev = tokio::time::timeout(std::time::Duration::from_secs(2), events.next())
1203 .await
1204 .unwrap()
1205 .unwrap();
1206 assert_eq!(ev.iid, 11);
1207 assert_eq!(ev.value, hap_model::format::CharValue::Bool(false));
1208
1209 release.notify_one();
1211 let ev2 = tokio::time::timeout(std::time::Duration::from_secs(2), events.next())
1212 .await
1213 .unwrap()
1214 .unwrap();
1215 assert_eq!(ev2.iid, 11);
1216 assert_eq!(ev2.value, hap_model::format::CharValue::Bool(true));
1217 }
1218
1219 #[tokio::test]
1224 #[allow(clippy::unwrap_used)]
1225 async fn foreign_device_advert_ignored() {
1226 use tokio_stream::StreamExt as _;
1227 let (mut h, gatt) = ble_accessory_with_db().await;
1228
1229 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1230 h.watch_sleepy_events_with_source(advert_source, [1, 2, 3, 4, 5, 6], vec![(1, 11)])
1232 .await
1233 .unwrap();
1234 let mut events = h.events();
1235
1236 gatt.advert_sender()
1238 .send(crate::gatt::RawAdvert {
1239 manufacturer_data: vec![
1240 0x06, 0x21, 0x01, 9, 9, 9, 9, 9, 9, 0x01, 0x00, 0x09, 0x00, 0x01, 0x00,
1241 ],
1242 })
1243 .await
1244 .unwrap();
1245
1246 let timeout_result =
1247 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1248 assert!(
1249 timeout_result.is_err(),
1250 "foreign device advert must not emit an event, but one was received"
1251 );
1252 }
1253
1254 #[tokio::test]
1257 #[allow(clippy::unwrap_used)]
1258 async fn stale_gsn_broadcast_ignored() {
1259 use tokio_stream::StreamExt as _;
1260 let (mut h, gatt) = ble_accessory_with_db().await;
1261
1262 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1263 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1264
1265 let mut pt = Vec::new();
1268 pt.extend_from_slice(&5u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed = key.seal(5, &pt, &aid_bytes);
1272
1273 let mut mfg = vec![0x11u8, 0x00];
1274 mfg.extend_from_slice(&aid_bytes);
1275 mfg.extend_from_slice(&sealed);
1276
1277 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1278 h.watch_sleepy_events_with_source(advert_source, aid_bytes, vec![])
1279 .await
1280 .unwrap();
1281 let mut events = h.events();
1282
1283 gatt.advert_sender()
1285 .send(crate::gatt::RawAdvert {
1286 manufacturer_data: mfg.clone(),
1287 })
1288 .await
1289 .unwrap();
1290
1291 let ev = events.next().await.unwrap();
1292 assert_eq!(ev.iid, 11);
1293 assert_eq!(ev.value, hap_model::format::CharValue::Bool(true));
1294
1295 gatt.advert_sender()
1297 .send(crate::gatt::RawAdvert {
1298 manufacturer_data: mfg,
1299 })
1300 .await
1301 .unwrap();
1302
1303 let timeout_result =
1304 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1305 assert!(
1306 timeout_result.is_err(),
1307 "duplicate GSN 5 broadcast must not emit a second event"
1308 );
1309 }
1310
1311 #[tokio::test]
1314 #[allow(clippy::unwrap_used)]
1315 async fn wrong_broadcast_key_ignored() {
1316 use tokio_stream::StreamExt as _;
1317 let (mut h, gatt) = ble_accessory_with_db().await;
1319
1320 let wrong_key = hap_crypto::BroadcastKey::from_bytes([0xFF; 32]);
1322 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1323
1324 let mut pt = Vec::new();
1325 pt.extend_from_slice(&1u16.to_le_bytes());
1326 pt.extend_from_slice(&11u16.to_le_bytes());
1327 pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]);
1328 let sealed = wrong_key.seal(1, &pt, &aid_bytes);
1329
1330 let mut mfg = vec![0x11u8, 0x00];
1331 mfg.extend_from_slice(&aid_bytes);
1332 mfg.extend_from_slice(&sealed);
1333
1334 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1335 h.watch_sleepy_events_with_source(advert_source, aid_bytes, vec![])
1336 .await
1337 .unwrap();
1338 let mut events = h.events();
1339
1340 gatt.advert_sender()
1341 .send(crate::gatt::RawAdvert {
1342 manufacturer_data: mfg,
1343 })
1344 .await
1345 .unwrap();
1346
1347 let timeout_result =
1348 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1349 assert!(
1350 timeout_result.is_err(),
1351 "wrong-key broadcast must not emit any event (all candidate opens fail)"
1352 );
1353 }
1354
1355 #[tokio::test]
1359 #[allow(clippy::unwrap_used)]
1360 async fn malformed_0x11_advert_ignored() {
1361 use tokio_stream::StreamExt as _;
1362 let (mut h, gatt) = ble_accessory_with_db().await;
1363
1364 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1365 h.watch_sleepy_events_with_source(advert_source, [1, 2, 3, 4, 5, 6], vec![])
1366 .await
1367 .unwrap();
1368 let mut events = h.events();
1369
1370 let manufacturer_data = vec![0x11, 0x00, 1, 2, 3, 4, 5, 6, 0xAA, 0xBB];
1372 gatt.advert_sender()
1373 .send(crate::gatt::RawAdvert { manufacturer_data })
1374 .await
1375 .unwrap();
1376
1377 let timeout_result =
1378 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1379 assert!(
1380 timeout_result.is_err(),
1381 "malformed (too-short payload) 0x11 advert must not emit any event"
1382 );
1383 }
1384
1385 #[tokio::test]
1389 #[allow(clippy::unwrap_used)]
1390 async fn broadcast_value_self_inconsistent_gsn_ignored() {
1391 use tokio_stream::StreamExt as _;
1392 let (mut h, gatt) = ble_accessory_with_db().await;
1393
1394 let key = hap_crypto::BroadcastKey::from_bytes([0u8; 32]);
1395 let aid_bytes: [u8; 6] = [1, 2, 3, 4, 5, 6];
1396
1397 let mut pt = Vec::new();
1401 pt.extend_from_slice(&3u16.to_le_bytes()); pt.extend_from_slice(&11u16.to_le_bytes()); pt.extend_from_slice(&[0x01, 0, 0, 0, 0, 0, 0, 0]); let sealed = key.seal(7, &pt, &aid_bytes); let mut mfg = vec![0x11u8, 0x00];
1407 mfg.extend_from_slice(&aid_bytes);
1408 mfg.extend_from_slice(&sealed);
1409
1410 let advert_source: std::sync::Arc<dyn crate::gatt::AdvertSource> = gatt.clone();
1411 h.watch_sleepy_events_with_source(advert_source, aid_bytes, vec![])
1412 .await
1413 .unwrap();
1414 let mut events = h.events();
1415
1416 gatt.advert_sender()
1417 .send(crate::gatt::RawAdvert {
1418 manufacturer_data: mfg,
1419 })
1420 .await
1421 .unwrap();
1422
1423 let timeout_result =
1424 tokio::time::timeout(std::time::Duration::from_millis(200), events.next()).await;
1425 assert!(
1426 timeout_result.is_err(),
1427 "self-inconsistent GSN (embedded 3 != nonce 7) must not emit any event"
1428 );
1429 }
1430
1431 #[tokio::test]
1432 #[allow(clippy::unwrap_used)]
1433 async fn read_after_reconnect_re_verifies_before_using_session() {
1434 let (mut h, gatt) = ble_accessory_with_db().await;
1435
1436 let mut plain = vec![0x02, 0x01, 0x00];
1439 let vbody = crate::pdu::encode_value_param(&[0x01]);
1440 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1441 plain.extend_from_slice(&vbody);
1442 let sealed =
1443 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1444 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1445
1446 gatt.bump_generation();
1451 let err = h.read(1, 11).await.unwrap_err();
1452 assert!(
1453 !matches!(err, BleError::CharacteristicNotFound { .. }),
1454 "expected a verify/transport error from the re-verify attempt, got {err:?}"
1455 );
1456 }
1457
1458 #[tokio::test]
1459 async fn dedup_emits_once_per_gsn_and_never_downgrades() {
1460 let emitted = Mutex::new(HashMap::new());
1461 assert!(dedup_should_emit(&emitted, 11, 9).await);
1463 assert!(!dedup_should_emit(&emitted, 11, 9).await);
1465 assert!(dedup_should_emit(&emitted, 11, 10).await);
1467 assert!(dedup_should_emit(&emitted, 11, 9).await);
1470 assert!(!dedup_should_emit(&emitted, 11, 10).await);
1472 assert!(dedup_should_emit(&emitted, 12, 65535).await);
1474 assert!(dedup_should_emit(&emitted, 12, 1).await);
1475 assert!(!dedup_should_emit(&emitted, 12, 1).await);
1476 }
1477
1478 #[tokio::test]
1479 #[allow(clippy::unwrap_used)]
1480 async fn write_sends_secure_pdu_and_accepts_success() {
1481 let (mut h, gatt) = ble_accessory_with_db().await;
1482 let plain = vec![0x02, 0x01, 0x00];
1484 let sealed =
1485 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1486 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1487 h.write(1, 11, hap_model::format::CharValue::Bool(true))
1488 .await
1489 .unwrap();
1490 }
1491
1492 #[tokio::test]
1493 #[allow(clippy::unwrap_used)]
1494 async fn write_surfaces_nonzero_pdu_status() {
1495 let (mut h, gatt) = ble_accessory_with_db().await;
1496 let plain = vec![0x02, 0x01, 0x06]; let sealed =
1498 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1499 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1500 let err = h
1501 .write(1, 11, hap_model::format::CharValue::Bool(true))
1502 .await
1503 .unwrap_err();
1504 assert!(matches!(err, BleError::RequestRejected(6)));
1505 }
1506
1507 #[tokio::test]
1508 #[allow(clippy::unwrap_used)]
1509 async fn pairing_id_exposes_the_stored_pairing() {
1510 let (h, _g) = ble_accessory_with_db().await;
1511 assert_eq!(h.pairing_id(), "AE:EC:86:C0:BF:D7");
1512 }
1513
1514 #[tokio::test]
1515 #[allow(clippy::unwrap_used)]
1516 async fn disconnect_is_callable_on_the_accessory() {
1517 let (h, _g) = ble_accessory_with_db().await;
1518 h.disconnect().await; }
1520
1521 #[tokio::test]
1522 #[allow(clippy::unwrap_used)]
1523 async fn self_sourcing_watch_errors_without_source() {
1524 let (mut h, _g) = ble_accessory_with_db().await;
1525 let err = h.watch_sleepy_events(vec![(1, 11)]).await.unwrap_err();
1527 assert!(matches!(err, BleError::NoAdvertSource));
1528 }
1529
1530 #[tokio::test]
1531 #[allow(clippy::unwrap_used)]
1532 async fn self_sourcing_watch_emits_via_set_source() {
1533 use tokio_stream::StreamExt as _;
1534 let (mut h, gatt) = ble_accessory_with_db().await;
1535 h.set_advert_source(gatt.clone() as std::sync::Arc<dyn crate::gatt::AdvertSource>);
1537 let mut plain = vec![0x02, 0x01, 0x00];
1539 let vbody = crate::pdu::encode_value_param(&[0x01]);
1540 plain.extend_from_slice(&u16::try_from(vbody.len()).unwrap().to_le_bytes());
1541 plain.extend_from_slice(&vbody);
1542 let sealed =
1543 hap_crypto::aead::chacha20poly1305_seal(&[0u8; 32], &[0u8; 12], &[], &plain).unwrap();
1544 gatt.queue_read("00000025-0000-1000-8000-0026bb765291", sealed);
1545 h.watch_sleepy_events(vec![(1, 11)]).await.unwrap();
1546 let mut events = h.events();
1547 gatt.advert_sender()
1549 .send(crate::gatt::RawAdvert {
1550 manufacturer_data: vec![
1551 0x06, 0x21, 0x01, 0xAE, 0xEC, 0x86, 0xC0, 0xBF, 0xD7, 0x01, 0x00, 0x09, 0x00,
1552 0x01, 0x00,
1553 ],
1554 })
1555 .await
1556 .unwrap();
1557 let ev = events.next().await.unwrap();
1558 assert_eq!((ev.aid, ev.iid), (1, 11));
1559 }
1560}