1use core::num::NonZeroU8;
34
35use embassy_time::{Duration, Instant};
36
37use crate::acl::AccessReq;
38use crate::crypto::{CanonAeadKey, Crypto};
39use crate::dm::endpoints::ROOT_ENDPOINT_ID;
40use crate::dm::{
41 Access, ArrayAttributeRead, Cluster, Dataver, HandlerContext, InvokeContext, LifecycleOp,
42 ReadContext,
43};
44use crate::error::{Error, ErrorCode};
45use crate::fabric::MAX_FABRICS;
46use crate::im::encoding::GenericPath;
47use crate::persist::{KvBlobStore, Persist, ICD_REGISTERED_CLIENTS_KEY};
48use crate::sc::checkin::{CheckIn, CheckInCounter};
49use crate::tlv::{FromTLV, TLVBuilderParent, TLVElement, ToTLV};
50use crate::utils::cell::RefCell;
51use crate::utils::init::{init, Init};
52use crate::utils::storage::Vec;
53use crate::utils::sync::blocking::Mutex;
54use crate::utils::sync::Notification;
55use crate::with;
56use crate::Matter;
57
58pub use crate::dm::clusters::decl::icd_management::*;
59
60pub const CLIENTS_PER_FABRIC: usize = 2;
67
68pub const MAX_REGISTERED_CLIENTS: usize = CLIENTS_PER_FABRIC * MAX_FABRICS;
70
71pub const STAY_ACTIVE_MAX_MS: u32 = 30_000;
76
77#[derive(Debug, Clone, FromTLV, ToTLV)]
82#[cfg_attr(feature = "defmt", derive(defmt::Format))]
83pub struct MonitoringRegistration {
84 pub fab_idx: NonZeroU8,
86 pub check_in_node_id: u64,
88 pub monitored_subject: u64,
90 pub client_type: ClientTypeEnum,
92 pub key: CanonAeadKey,
97}
98
99#[derive(Debug, Clone, Copy, Eq, PartialEq)]
102pub enum KeyVerdict {
103 NotFound,
105 Match,
107 Mismatch,
109}
110
111#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
117#[cfg_attr(feature = "defmt", derive(defmt::Format))]
118pub struct IcdModeConfig {
119 pub idle_mode_duration_s: u32,
122 pub active_mode_duration_ms: u32,
124 pub active_mode_threshold_ms: u16,
127 pub user_active_mode_trigger_hint: u32,
130 pub user_active_mode_trigger_instruction: &'static str,
133}
134
135struct IcdState {
139 clients: Vec<MonitoringRegistration, MAX_REGISTERED_CLIENTS>,
141 counter: CheckInCounter,
143 stay_active_until: Option<Instant>,
146}
147
148impl IcdState {
149 fn init(counter: CheckInCounter) -> impl Init<Self> {
150 init!(Self {
151 clients <- Vec::init(),
152 counter: counter,
153 stay_active_until: None,
154 })
155 }
156}
157
158pub struct Icd {
169 state: Mutex<RefCell<IcdState>>,
170 mode: IcdModeConfig,
173 registrations_changed: Notification,
175 active_extended: Notification,
177}
178
179impl Icd {
180 pub const fn new(counter: CheckInCounter, mode: IcdModeConfig) -> Self {
186 Self {
187 state: Mutex::new(RefCell::new(IcdState {
188 clients: Vec::new(),
189 counter,
190 stay_active_until: None,
191 })),
192 mode,
193 registrations_changed: Notification::new(),
194 active_extended: Notification::new(),
195 }
196 }
197
198 pub fn init(counter: CheckInCounter, mode: IcdModeConfig) -> impl Init<Self> {
201 init!(Self {
202 state <- Mutex::init(RefCell::init(IcdState::init(counter))),
203 mode: mode,
204 registrations_changed <- Notification::init(),
205 active_extended <- Notification::init(),
206 })
207 }
208
209 pub fn mode(&self) -> IcdModeConfig {
211 self.mode
212 }
213
214 pub fn registrations_len(&self) -> usize {
218 self.state.lock(|s| s.borrow().clients.len())
219 }
220
221 pub fn registrations_is_empty(&self) -> bool {
223 self.registrations_len() == 0
224 }
225
226 pub fn operating_mode(&self) -> OperatingModeEnum {
235 if self.registrations_is_empty() {
236 OperatingModeEnum::SIT
237 } else {
238 OperatingModeEnum::LIT
239 }
240 }
241
242 pub fn fabric_registrations_len(&self, fab_idx: NonZeroU8) -> usize {
244 self.state.lock(|s| {
245 s.borrow()
246 .clients
247 .iter()
248 .filter(|c| c.fab_idx == fab_idx)
249 .count()
250 })
251 }
252
253 pub fn register(&self, registration: MonitoringRegistration) -> Result<(), Error> {
259 self.state.lock(|s| -> Result<(), Error> {
260 let clients = &mut s.borrow_mut().clients;
261
262 if let Some(existing) = clients.iter_mut().find(|c| {
263 c.fab_idx == registration.fab_idx
264 && c.check_in_node_id == registration.check_in_node_id
265 }) {
266 *existing = registration;
267 } else {
268 if clients
269 .iter()
270 .filter(|c| c.fab_idx == registration.fab_idx)
271 .count()
272 >= CLIENTS_PER_FABRIC
273 {
274 Err(ErrorCode::ResourceExhausted)?;
275 }
276 clients
277 .push(registration)
278 .map_err(|_| ErrorCode::ResourceExhausted)?;
279 }
280
281 Ok(())
282 })?;
283
284 self.registrations_changed.notify();
285
286 Ok(())
287 }
288
289 pub fn unregister(&self, fab_idx: NonZeroU8, check_in_node_id: u64) -> Result<(), Error> {
293 let removed = self.state.lock(|s| {
294 let clients = &mut s.borrow_mut().clients;
295 let before = clients.len();
296 clients.retain(|c| !(c.fab_idx == fab_idx && c.check_in_node_id == check_in_node_id));
297 clients.len() != before
298 });
299
300 if !removed {
301 Err(ErrorCode::NotFound)?;
302 }
303
304 self.registrations_changed.notify();
305
306 Ok(())
307 }
308
309 pub fn verify_key(
316 &self,
317 fab_idx: NonZeroU8,
318 check_in_node_id: u64,
319 key: Option<&[u8]>,
320 ) -> KeyVerdict {
321 self.state.lock(|s| {
322 let state = s.borrow();
323 let Some(entry) = state
324 .clients
325 .iter()
326 .find(|c| c.fab_idx == fab_idx && c.check_in_node_id == check_in_node_id)
327 else {
328 return KeyVerdict::NotFound;
329 };
330
331 match key {
332 Some(key) if key == entry.key.access() => KeyVerdict::Match,
333 _ => KeyVerdict::Mismatch,
334 }
335 })
336 }
337
338 pub fn remove_fabric(&self, fab_idx: NonZeroU8) -> bool {
342 let removed = self.state.lock(|s| {
343 let clients = &mut s.borrow_mut().clients;
344 let before = clients.len();
345 clients.retain(|c| c.fab_idx != fab_idx);
346 clients.len() != before
347 });
348
349 if removed {
350 self.registrations_changed.notify();
351 }
352
353 removed
354 }
355
356 pub fn with_registrations<R>(&self, f: impl FnOnce(&[MonitoringRegistration]) -> R) -> R {
361 self.state.lock(|s| f(&s.borrow().clients))
362 }
363
364 pub async fn wait_registrations_changed(&self) {
366 self.registrations_changed.wait().await;
367 }
368
369 pub fn load_registrations<S: KvBlobStore>(
372 &self,
373 mut kv: S,
374 buf: &mut [u8],
375 ) -> Result<(), Error> {
376 let clients = match kv.load(ICD_REGISTERED_CLIENTS_KEY, buf)? {
377 Some(data) => Vec::from_tlv(&TLVElement::new(data))?,
378 None => Vec::new(),
379 };
380
381 self.state.lock(|s| s.borrow_mut().clients = clients);
382
383 Ok(())
384 }
385
386 pub fn store_registrations<C: HandlerContext>(&self, ctx: &C) -> Result<(), Error> {
388 let mut persist = Persist::new(ctx.kv());
389
390 self.state
391 .lock(|s| persist.store_tlv(ICD_REGISTERED_CLIENTS_KEY, &s.borrow().clients))?;
392
393 persist.run()
394 }
395
396 pub fn active_until(&self) -> Option<Instant> {
408 self.state.lock(|s| s.borrow().stay_active_until)
409 }
410
411 pub async fn wait_active_extended(&self) {
414 self.active_extended.wait().await;
415 }
416
417 fn extend_active(&self, duration_ms: u32) -> u32 {
425 let now = Instant::now();
426 let requested = now.saturating_add(Duration::from_millis(duration_ms as u64));
427
428 let deadline = self.state.lock(|s| {
429 let stay = &mut s.borrow_mut().stay_active_until;
430 let deadline = stay.map_or(requested, |current| current.max(requested));
431 *stay = Some(deadline);
432 deadline
433 });
434
435 self.active_extended.notify();
436
437 deadline.saturating_duration_since(now).as_millis() as u32
439 }
440
441 pub fn next_counter(&self) -> u32 {
445 self.state.lock(|s| s.borrow().counter.next())
446 }
447
448 pub fn advance_counter<S: KvBlobStore>(&self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
454 let to_persist = self.state.lock(|s| s.borrow_mut().counter.advance());
455
456 if let Some(value) = to_persist {
457 kv.store(
458 crate::persist::ICD_CHECK_IN_COUNTER_KEY,
459 &value.to_le_bytes(),
460 buf,
461 )?;
462 }
463
464 Ok(())
465 }
466
467 #[must_use = "a moved boundary must be persisted via persist_counter"]
475 pub fn invalidate_counter(&self, delta: u32) -> bool {
476 self.state
477 .lock(|s| s.borrow_mut().counter.advance_by(delta))
478 .is_some()
479 }
480
481 pub fn persist_counter<S: KvBlobStore>(&self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
483 let value = self.state.lock(|s| s.borrow().counter.persist_value());
484 kv.store(
485 crate::persist::ICD_CHECK_IN_COUNTER_KEY,
486 &value.to_le_bytes(),
487 buf,
488 )
489 }
490
491 pub fn load_counter<S: KvBlobStore>(
494 &self,
495 mut kv: S,
496 epoch: u32,
497 buf: &mut [u8],
498 ) -> Result<(), Error> {
499 let start = match kv.load(crate::persist::ICD_CHECK_IN_COUNTER_KEY, buf)? {
500 Some(data) => u32::from_le_bytes(data.try_into().map_err(|_| ErrorCode::Invalid)?),
501 None => return Ok(()),
503 };
504
505 self.state
506 .lock(|s| s.borrow_mut().counter = CheckInCounter::new(start, epoch));
507
508 Ok(())
509 }
510
511 pub async fn send_one_check_in<C: Crypto>(
523 &self,
524 matter: &Matter<'_>,
525 crypto: C,
526 fab_idx: NonZeroU8,
527 node_id: u64,
528 counter: u32,
529 buf: &mut [u8],
530 ) -> Result<(), Error> {
531 let key = self
534 .state
535 .lock(|s| {
536 s.borrow()
537 .clients
538 .iter()
539 .find(|c| c.fab_idx == fab_idx && c.check_in_node_id == node_id)
540 .map(|c| c.key.clone())
541 })
542 .ok_or(ErrorCode::NotFound)?;
543
544 let app_data = self.mode.active_mode_threshold_ms.to_le_bytes();
545
546 CheckIn::new(key.reference())
547 .send_to(matter, crypto, fab_idx, node_id, counter, &app_data, buf)
548 .await
549 }
550
551 pub async fn send_check_in<C: Crypto, const NS: usize>(
562 &self,
563 matter: &Matter<'_>,
564 crypto: C,
565 subscriptions: &crate::im::subscriptions::Subscriptions<NS>,
566 kv: impl KvBlobStore,
567 buf: &mut [u8],
568 ) -> Result<(), Error> {
569 let mut targets: Vec<(NonZeroU8, u64, CanonAeadKey), MAX_REGISTERED_CLIENTS> = Vec::new();
573
574 let counter = self.state.lock(|s| {
575 let state = s.borrow();
576 for c in &state.clients {
577 if subscriptions.has_subscription_for(c.fab_idx, c.monitored_subject) {
581 continue;
582 }
583 let _ = targets.push((c.fab_idx, c.check_in_node_id, c.key.clone()));
585 }
586 state.counter.next()
587 });
588
589 if targets.is_empty() {
590 return Ok(());
591 }
592
593 let app_data = self.mode.active_mode_threshold_ms.to_le_bytes();
594
595 for (fab_idx, node_id, key) in &targets {
596 let _ = CheckIn::new(key.reference())
598 .send_to(matter, &crypto, *fab_idx, *node_id, counter, &app_data, buf)
599 .await;
600 }
601
602 self.advance_counter(kv, buf)
603 }
604}
605
606pub struct IcdMgmtHandler<'a> {
615 dataver: Dataver,
616 icd: &'a Icd,
617}
618
619impl<'a> IcdMgmtHandler<'a> {
620 pub const fn new(dataver: Dataver, icd: &'a Icd) -> Self {
622 Self { dataver, icd }
623 }
624
625 pub const fn adapt(self) -> HandlerAdaptor<Self> {
627 HandlerAdaptor(self)
628 }
629
630 fn cmd_fabric(ctx: &impl InvokeContext) -> Result<NonZeroU8, Error> {
632 ctx.accessor()?.fab_idx()
633 }
634
635 fn caller_is_admin(ctx: &impl InvokeContext) -> Result<bool, Error> {
640 let accessor = ctx.accessor()?;
641 let cmd = ctx.cmd();
642 let path = GenericPath::new(
643 Some(cmd.endpoint_id),
644 Some(cmd.cluster_id),
645 Some(cmd.cmd_id),
646 );
647
648 let mut req = AccessReq::new(&accessor, path, Access::WRITE, &[]);
649 req.set_target_perms(Access::WRITE | Access::NEED_ADMIN);
650
651 Ok(req.allow())
652 }
653
654 fn sync_icd_mode(&self, ctx: &impl HandlerContext) {
658 ctx.matter().set_icd_mode(Some(self.icd.operating_mode()));
659 }
660}
661
662impl ClusterHandler for IcdMgmtHandler<'_> {
663 const CLUSTER: Cluster<'static> = FULL_CLUSTER
670 .with_features(
671 Feature::CHECK_IN_PROTOCOL_SUPPORT
672 .union(Feature::LONG_IDLE_TIME_SUPPORT)
673 .union(Feature::USER_ACTIVE_MODE_TRIGGER)
674 .union(Feature::DYNAMIC_SIT_LIT_SUPPORT)
675 .bits(),
676 )
677 .with_attrs(with!(required;
678 AttributeId::RegisteredClients
679 | AttributeId::ICDCounter
680 | AttributeId::ClientsSupportedPerFabric
681 | AttributeId::MaximumCheckInBackOff
682 | AttributeId::OperatingMode
683 | AttributeId::UserActiveModeTriggerHint
684 | AttributeId::UserActiveModeTriggerInstruction));
685
686 fn dataver(&self) -> u32 {
687 self.dataver.get()
688 }
689
690 fn dataver_changed(&self) {
691 self.dataver.changed();
692 }
693
694 fn lifecycle(&self, ctx: impl HandlerContext, op: LifecycleOp) -> Result<(), Error> {
695 match op {
696 LifecycleOp::Startup | LifecycleOp::FactoryReset => Ok(()),
700 LifecycleOp::FabricRemoval { fab_idx } => {
701 let mode_before = self.icd.operating_mode();
702
703 if self.icd.remove_fabric(fab_idx) {
704 self.icd.store_registrations(&ctx)?;
705
706 if self.icd.operating_mode() != mode_before {
712 ctx.notify_attr_changed(
713 ROOT_ENDPOINT_ID,
714 Self::CLUSTER.id,
715 AttributeId::OperatingMode as _,
716 );
717 }
718
719 self.sync_icd_mode(&ctx);
720 }
721
722 Ok(())
723 }
724 }
725 }
726
727 fn idle_mode_duration(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
728 Ok(self.icd.mode.idle_mode_duration_s)
729 }
730
731 fn active_mode_duration(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
732 Ok(self.icd.mode.active_mode_duration_ms)
733 }
734
735 fn active_mode_threshold(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
736 Ok(self.icd.mode.active_mode_threshold_ms)
737 }
738
739 fn clients_supported_per_fabric(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
740 Ok(CLIENTS_PER_FABRIC as u16)
741 }
742
743 fn maximum_check_in_back_off(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
746 Ok(self.icd.mode.idle_mode_duration_s)
747 }
748
749 fn operating_mode(&self, _ctx: impl ReadContext) -> Result<OperatingModeEnum, Error> {
750 Ok(self.icd.operating_mode())
751 }
752
753 fn user_active_mode_trigger_hint(
754 &self,
755 _ctx: impl ReadContext,
756 ) -> Result<UserActiveModeTriggerBitmap, Error> {
757 Ok(UserActiveModeTriggerBitmap::from_bits_truncate(
758 self.icd.mode.user_active_mode_trigger_hint,
759 ))
760 }
761
762 fn user_active_mode_trigger_instruction<P: TLVBuilderParent>(
763 &self,
764 _ctx: impl ReadContext,
765 builder: crate::tlv::Utf8StrBuilder<P>,
766 ) -> Result<P, Error> {
767 builder.set(self.icd.mode.user_active_mode_trigger_instruction)
768 }
769
770 fn icd_counter(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
771 Ok(self.icd.next_counter())
772 }
773
774 fn registered_clients<P: TLVBuilderParent>(
775 &self,
776 ctx: impl ReadContext,
777 builder: ArrayAttributeRead<
778 MonitoringRegistrationStructArrayBuilder<P>,
779 MonitoringRegistrationStructBuilder<P>,
780 >,
781 ) -> Result<P, Error> {
782 let attr = ctx.attr();
783 let fab_filter = attr
784 .fab_filter
785 .then(|| NonZeroU8::new(attr.fab_idx).ok_or(ErrorCode::UnsupportedAccess))
786 .transpose()?;
787
788 self.icd.with_registrations(|clients| {
789 let mut iter = clients
790 .iter()
791 .filter(|c| fab_filter.is_none_or(|f| c.fab_idx == f));
792
793 match builder {
794 ArrayAttributeRead::ReadAll(mut array) => {
795 for c in iter {
796 array = array
797 .push()?
798 .check_in_node_id(Some(c.check_in_node_id))?
799 .monitored_subject(Some(c.monitored_subject))?
800 .client_type(Some(c.client_type))?
801 .fabric_index(Some(c.fab_idx.get()))?
802 .end()?;
803 }
804 array.end()
805 }
806 ArrayAttributeRead::ReadOne(index, item) => {
807 let Some(c) = iter.nth(index as usize) else {
808 return Err(ErrorCode::ConstraintError.into());
809 };
810 item.check_in_node_id(Some(c.check_in_node_id))?
811 .monitored_subject(Some(c.monitored_subject))?
812 .client_type(Some(c.client_type))?
813 .fabric_index(Some(c.fab_idx.get()))?
814 .end()
815 }
816 ArrayAttributeRead::ReadNone(array) => array.end(),
817 }
818 })
819 }
820
821 fn handle_register_client<P: TLVBuilderParent>(
822 &self,
823 ctx: impl InvokeContext,
824 request: RegisterClientRequest<'_>,
825 response: RegisterClientResponseBuilder<P>,
826 ) -> Result<P, Error> {
827 let fab_idx = Self::cmd_fabric(&ctx)?;
828 let node_id = request.check_in_node_id()?;
829
830 if !Self::caller_is_admin(&ctx)? {
833 let presented = request.verification_key()?.map(|k| k.0);
834 if self.icd.verify_key(fab_idx, node_id, presented) == KeyVerdict::Mismatch {
835 Err(ErrorCode::Failure)?;
836 }
837 }
838
839 let key = request.key()?;
840
841 self.icd.register(MonitoringRegistration {
842 fab_idx,
843 check_in_node_id: node_id,
844 monitored_subject: request.monitored_subject()?,
845 client_type: request
848 .client_type()
849 .map_err(|_| ErrorCode::ConstraintError)?,
850 key: key.0.try_into().map_err(|_| ErrorCode::ConstraintError)?,
851 })?;
852
853 self.icd.store_registrations(&ctx)?;
854 ctx.notify_own_cluster_changed();
855 self.sync_icd_mode(&ctx);
856
857 response.icd_counter(self.icd.next_counter())?.end()
859 }
860
861 fn handle_unregister_client(
862 &self,
863 ctx: impl InvokeContext,
864 request: UnregisterClientRequest<'_>,
865 ) -> Result<(), Error> {
866 let fab_idx = Self::cmd_fabric(&ctx)?;
867 let node_id = request.check_in_node_id()?;
868
869 if !Self::caller_is_admin(&ctx)? {
872 let presented = request.verification_key()?.map(|k| k.0);
873 match self.icd.verify_key(fab_idx, node_id, presented) {
874 KeyVerdict::NotFound => Err(ErrorCode::NotFound)?,
875 KeyVerdict::Mismatch => Err(ErrorCode::Failure)?,
876 KeyVerdict::Match => {}
877 }
878 }
879
880 self.icd.unregister(fab_idx, node_id)?;
881
882 self.icd.store_registrations(&ctx)?;
883 ctx.notify_own_cluster_changed();
884 self.sync_icd_mode(&ctx);
885
886 Ok(())
887 }
888
889 fn handle_stay_active_request<P: TLVBuilderParent>(
890 &self,
891 _ctx: impl InvokeContext,
892 request: StayActiveRequestRequest<'_>,
893 response: StayActiveResponseBuilder<P>,
894 ) -> Result<P, Error> {
895 let requested = request.stay_active_duration()?.min(STAY_ACTIVE_MAX_MS);
899 let promised = self.icd.extend_active(requested);
900
901 response.promised_active_duration(promised)?.end()
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908
909 fn fab(i: u8) -> NonZeroU8 {
910 NonZeroU8::new(i).unwrap()
911 }
912
913 fn reg(fab: u8, node: u64) -> MonitoringRegistration {
914 MonitoringRegistration {
915 fab_idx: NonZeroU8::new(fab).unwrap(),
916 check_in_node_id: node,
917 monitored_subject: node,
918 client_type: ClientTypeEnum::Permanent,
919 key: CanonAeadKey::new(),
920 }
921 }
922
923 fn icd() -> Icd {
924 Icd::new(CheckInCounter::new(0, 10), mode())
925 }
926
927 fn nodes(icd: &Icd, fab_filter: Option<NonZeroU8>) -> alloc::vec::Vec<u64> {
929 icd.with_registrations(|clients| {
930 clients
931 .iter()
932 .filter(|c| fab_filter.is_none_or(|f| c.fab_idx == f))
933 .map(|c| c.check_in_node_id)
934 .collect()
935 })
936 }
937
938 #[test]
939 fn register_adds_and_updates() {
940 let icd = icd();
941
942 icd.register(reg(1, 100)).unwrap();
943 assert_eq!(icd.registrations_len(), 1);
944 assert_eq!(icd.fabric_registrations_len(fab(1)), 1);
945
946 let mut updated = reg(1, 100);
948 updated.monitored_subject = 999;
949 icd.register(updated).unwrap();
950 assert_eq!(icd.registrations_len(), 1);
951 let subject = icd.with_registrations(|c| c[0].monitored_subject);
952 assert_eq!(subject, 999);
953 }
954
955 #[test]
956 fn per_fabric_limit_is_enforced_independently() {
957 let icd = icd();
958
959 for i in 0..CLIENTS_PER_FABRIC {
961 icd.register(reg(1, 100 + i as u64)).unwrap();
962 }
963 assert_eq!(icd.fabric_registrations_len(fab(1)), CLIENTS_PER_FABRIC);
964
965 assert!(icd
967 .register(reg(1, 100 + CLIENTS_PER_FABRIC as u64))
968 .is_err());
969 assert_eq!(icd.fabric_registrations_len(fab(1)), CLIENTS_PER_FABRIC);
970
971 icd.register(reg(1, 100)).unwrap();
973 assert_eq!(icd.fabric_registrations_len(fab(1)), CLIENTS_PER_FABRIC);
974
975 icd.register(reg(2, 200)).unwrap();
977 assert_eq!(icd.fabric_registrations_len(fab(2)), 1);
978 }
979
980 #[test]
981 fn unregister_and_remove_fabric() {
982 let icd = icd();
983 icd.register(reg(1, 100)).unwrap();
984 icd.register(reg(2, 200)).unwrap();
985
986 assert!(icd.unregister(fab(1), 999).is_err()); icd.unregister(fab(1), 100).unwrap();
988 assert_eq!(icd.registrations_len(), 1);
989
990 assert!(icd.remove_fabric(fab(2)));
992 assert!(icd.registrations_is_empty());
993 assert!(!icd.remove_fabric(fab(2))); }
995
996 #[test]
997 fn operating_mode_follows_the_registration_set() {
998 let icd = icd();
999 assert_eq!(icd.operating_mode(), OperatingModeEnum::SIT);
1000
1001 icd.register(reg(1, 100)).unwrap();
1002 assert_eq!(icd.operating_mode(), OperatingModeEnum::LIT);
1003
1004 icd.register(reg(1, 101)).unwrap();
1005 icd.unregister(fab(1), 100).unwrap();
1006 assert_eq!(icd.operating_mode(), OperatingModeEnum::LIT);
1007
1008 icd.unregister(fab(1), 101).unwrap();
1009 assert_eq!(icd.operating_mode(), OperatingModeEnum::SIT);
1010 }
1011
1012 #[test]
1013 fn verify_key_matches_only_the_stored_key() {
1014 let icd = icd();
1015
1016 let mut r = reg(1, 100);
1017 let stored = [7u8; 16];
1018 r.key.try_load_from_slice(&stored).unwrap();
1019 icd.register(r).unwrap();
1020
1021 assert_eq!(
1023 icd.verify_key(fab(1), 999, Some(&stored)),
1024 KeyVerdict::NotFound
1025 );
1026 assert_eq!(
1028 icd.verify_key(fab(2), 100, Some(&stored)),
1029 KeyVerdict::NotFound
1030 );
1031 assert_eq!(
1033 icd.verify_key(fab(1), 100, Some(&stored)),
1034 KeyVerdict::Match
1035 );
1036 assert_eq!(
1038 icd.verify_key(fab(1), 100, Some(&[0u8; 16])),
1039 KeyVerdict::Mismatch
1040 );
1041 assert_eq!(icd.verify_key(fab(1), 100, None), KeyVerdict::Mismatch);
1042 }
1043
1044 #[test]
1045 fn with_registrations_honors_the_fabric_filter() {
1046 let icd = icd();
1047 icd.register(reg(1, 100)).unwrap();
1048 icd.register(reg(2, 200)).unwrap();
1049
1050 let mut all = nodes(&icd, None);
1051 all.sort_unstable();
1052 assert_eq!(all, [100, 200]);
1053
1054 assert_eq!(nodes(&icd, Some(fab(1))), [100]);
1055 }
1056
1057 #[derive(Default)]
1060 struct MemKv {
1061 value: Option<alloc::vec::Vec<u8>>,
1062 }
1063
1064 impl KvBlobStore for &mut MemKv {
1065 fn load<'a>(&mut self, _key: u16, buf: &'a mut [u8]) -> Result<Option<&'a [u8]>, Error> {
1066 Ok(self.value.as_ref().map(|v| {
1067 buf[..v.len()].copy_from_slice(v);
1068 &buf[..v.len()]
1069 }))
1070 }
1071
1072 fn store(&mut self, _key: u16, data: &[u8], _buf: &mut [u8]) -> Result<(), Error> {
1073 self.value = Some(data.to_vec());
1074 Ok(())
1075 }
1076
1077 fn remove(&mut self, _key: u16, _buf: &mut [u8]) -> Result<(), Error> {
1078 self.value = None;
1079 Ok(())
1080 }
1081 }
1082
1083 fn mode() -> IcdModeConfig {
1084 IcdModeConfig {
1085 idle_mode_duration_s: 60,
1086 active_mode_duration_ms: 300,
1087 active_mode_threshold_ms: 500,
1088 user_active_mode_trigger_hint: 0,
1089 user_active_mode_trigger_instruction: "",
1090 }
1091 }
1092
1093 #[test]
1094 fn stay_active_combines_with_max_and_reports_remaining() {
1095 let icd = Icd::new(CheckInCounter::new(0, 10), mode());
1096
1097 assert!(icd.active_until().is_none());
1099
1100 let promised = icd.extend_active(STAY_ACTIVE_MAX_MS);
1102 assert!(promised <= STAY_ACTIVE_MAX_MS);
1103 assert!(promised > STAY_ACTIVE_MAX_MS - 1_000, "promised {promised}");
1104 let deadline = icd.active_until().expect("deadline now set");
1105
1106 let promised2 = icd.extend_active(1_000);
1109 assert!(
1110 promised2 > 1_000,
1111 "shorter request must not shrink: {promised2}"
1112 );
1113 assert_eq!(icd.active_until(), Some(deadline), "deadline unchanged");
1114
1115 icd.extend_active(2 * STAY_ACTIVE_MAX_MS);
1117 assert!(icd.active_until().unwrap() > deadline);
1118 }
1119
1120 #[test]
1121 fn stay_active_request_clamps_to_the_guaranteed_max() {
1122 let icd = Icd::new(CheckInCounter::new(0, 10), mode());
1125
1126 let requested = STAY_ACTIVE_MAX_MS + 5_000;
1127 let promised = icd.extend_active(requested.min(STAY_ACTIVE_MAX_MS));
1128 assert!(promised <= STAY_ACTIVE_MAX_MS, "must clamp: {promised}");
1129 }
1130
1131 #[test]
1132 fn counter_persists_at_boundary_and_resumes_across_restart() {
1133 const EPOCH: u32 = 10;
1134 let mut kv = MemKv::default();
1135 let mut buf = [0u8; 16];
1136
1137 let icd = Icd::new(CheckInCounter::new(100, EPOCH), mode());
1139
1140 assert_eq!(icd.next_counter(), 101);
1142 for _ in 0..9 {
1143 icd.advance_counter(&mut kv, &mut buf).unwrap();
1144 }
1145 assert_eq!(kv.value, None, "no persist before the boundary");
1146
1147 let last_used = icd.next_counter();
1149 icd.advance_counter(&mut kv, &mut buf).unwrap();
1150 assert_eq!(last_used, 110);
1151 assert!(kv.value.is_some(), "boundary crossing must persist");
1152
1153 let icd2 = Icd::new(CheckInCounter::new(0, EPOCH), mode());
1156 icd2.load_counter(&mut kv, EPOCH, &mut buf).unwrap();
1157 assert!(icd2.next_counter() > last_used);
1158 }
1159}