1use core::str::FromStr;
21
22use crate::dm::{Cluster, Dataver, InvokeContext, ReadContext, WriteContext};
23use crate::error::{Error, ErrorCode};
24use crate::fabric::MAX_FABRICS;
25use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist, BASIC_INFO_KEY};
26use crate::tlv::{
27 FromTLV, Nullable, NullableBuilder, TLVBuilderParent, TLVElement, ToTLV, Utf8StrBuilder,
28};
29use crate::transport::exchange::Exchange;
30use crate::transport::session::MAX_SESSIONS;
31use crate::utils::bitflags::bitflags;
32use crate::utils::init::{init, Init};
33use crate::{except, with};
34
35pub use crate::dm::clusters::decl::basic_information::*;
36pub use crate::dm::clusters::decl::general_commissioning::RegulatoryLocationTypeEnum;
37pub use crate::dm::clusters::decl::globals::{
38 AreaTypeTag, LocationDescriptorStruct, LocationDescriptorStructBuilder,
39};
40
41pub const DEFAULT_MATTER_SPEC_VERSION: u32 = 0x01060000;
45
46pub const DEFAULT_DATA_MODEL_REVISION: u16 = 21;
50
51pub const DEFAULT_MAX_PATHS_PER_INVOKE: u16 = 5;
56
57bitflags! {
58 #[repr(transparent)]
59 #[derive(Default)]
60 #[cfg_attr(not(feature = "defmt"), derive(Debug, Copy, Clone, Eq, PartialEq, Hash))]
61 pub struct PairingHintFlags: u32 {
62 const POWER_CYCLE = 0x0000_0001;
67 const DEV_MANUFACTURER_URL = 0x0000_0002;
74 const ADMINISTRATOR = 0x0000_0004;
78 const SETTINGS_MENU = 0x0000_0008;
81 const CUSTOM_INSTRUCTION = 0x0000_0010;
85 const DEVICE_MANUAL = 0x0000_0020;
90 const PRESS_RESET_BUTTON = 0x0000_0040;
92 const PRESS_RESET_BUTTON_WITH_POWER = 0x0000_0080;
94 const PRESS_RESET_BUTTON_FOR_N_SECONDS = 0x0000_0100;
97 const PRESS_RESET_BUTTON_UNTIL_LIGHT_BLINKS = 0x0000_0200;
100 const PRESS_RESET_BUTTON_FOR_N_SECONDS_WITH_POWER = 0x0000_0400;
103 const PRESS_RESET_BUTTON_UNTIL_LIGHT_BLINKS_WITH_POWER = 0x0000_0800;
107 const PRESS_RESET_BUTTON_N_TIMES = 0x0000_1000;
110 const PRESS_SETUP_BUTTON = 0x0000_2000;
112 const PRESS_SETUP_BUTTON_WITH_POWER = 0x0000_4000;
114 const PRESS_SETUP_BUTTON_FOR_N_SECONDS = 0x0000_8000;
117 const PRESS_SETUP_BUTTON_UNTIL_LIGHT_BLINKS = 0x0001_0000;
120 const PRESS_SETUP_BUTTON_FOR_N_SECONDS_WITH_POWER = 0x0002_0000;
123 const PRESS_SETUP_BUTTON_UNTIL_LIGHT_BLINKS_WITH_POWER = 0x0004_0000;
127 const PRESS_SETUP_BUTTON_N_TIMES = 0x0008_0000;
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
137#[cfg_attr(feature = "defmt", derive(defmt::Format))]
138pub struct DeviceLocationConfig<'a> {
139 pub location_name: &'a str,
141 pub floor_number: Option<i16>,
143 pub area_type: Option<AreaTypeTag>,
145}
146
147#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
155#[cfg_attr(feature = "defmt", derive(defmt::Format))]
156pub struct BasicInfoConfig<'a> {
157 pub vendor_name: &'a str,
159 pub vid: u16,
161 pub product_name: &'a str,
163 pub pid: u16,
165 pub hw_ver: u16,
167 pub hw_ver_str: &'a str,
169 pub sw_ver: u32,
171 pub sw_ver_str: &'a str,
173 pub manufacturing_date: &'a str,
175 pub part_number: &'a str,
177 pub product_url: &'a str,
179 pub product_label: &'a str,
181 pub serial_no: &'a str,
183 pub unique_id: &'a str,
185 pub location: Option<&'a str>,
188 pub device_location: Option<DeviceLocationConfig<'a>>,
191 pub capability_minima: CapabilityMinima,
193 pub product_appearance: ProductAppearance,
195 pub specification_version: u32,
197 pub data_model_revision: u16,
199 pub max_paths_per_invoke: u16,
201 pub device_name: &'a str,
205 pub device_type: Option<u16>,
209 pub pairing_hint: PairingHintFlags,
213 pub pairing_instruction: &'a str,
217 pub sai: Option<u32>,
225 pub sii: Option<u32>,
233 pub tcp_supported: bool,
240}
241
242impl BasicInfoConfig<'_> {
243 pub const fn new() -> Self {
244 Self {
245 vid: 0,
246 pid: 0,
247 hw_ver: 0,
248 hw_ver_str: "",
249 sw_ver: 0,
250 sw_ver_str: "",
251 serial_no: "",
252 product_name: "",
253 vendor_name: "",
254 manufacturing_date: "",
255 part_number: "",
256 product_url: "",
257 product_label: "",
258 unique_id: "",
259 location: None,
260 device_location: None,
261 capability_minima: CapabilityMinima::new(),
262 product_appearance: ProductAppearance::new(),
263 specification_version: DEFAULT_MATTER_SPEC_VERSION,
264 data_model_revision: DEFAULT_DATA_MODEL_REVISION,
265 max_paths_per_invoke: DEFAULT_MAX_PATHS_PER_INVOKE,
266 device_name: "",
267 device_type: None,
268 pairing_hint: PairingHintFlags::empty(),
269 pairing_instruction: "",
270 sai: None,
271 sii: None,
272 tcp_supported: false,
273 }
274 }
275}
276
277impl Default for BasicInfoConfig<'_> {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
285#[cfg_attr(feature = "defmt", derive(defmt::Format))]
286pub struct CapabilityMinima {
287 pub case_sessions_per_fabric: u16,
289 pub subscriptions_per_fabric: u16,
291 pub simultaneous_invocations_supported: u16,
294 pub simultaneous_writes_supported: u16,
296 pub read_paths_supported: u16,
299 pub subscribe_paths_supported: u16,
302}
303
304const SUBSCRIPTIONS_PER_FABRIC: u16 = 3;
309
310const SIMULTANEOUS_INVOCATIONS_SUPPORTED: u16 = 1;
312const SIMULTANEOUS_WRITES_SUPPORTED: u16 = 1;
314const READ_PATHS_SUPPORTED: u16 = 9;
316const SUBSCRIBE_PATHS_SUPPORTED: u16 = 3;
318
319impl CapabilityMinima {
320 pub const fn new() -> Self {
324 Self {
325 case_sessions_per_fabric: (MAX_SESSIONS / MAX_FABRICS) as _,
326 subscriptions_per_fabric: SUBSCRIPTIONS_PER_FABRIC,
327 simultaneous_invocations_supported: SIMULTANEOUS_INVOCATIONS_SUPPORTED,
328 simultaneous_writes_supported: SIMULTANEOUS_WRITES_SUPPORTED,
329 read_paths_supported: READ_PATHS_SUPPORTED,
330 subscribe_paths_supported: SUBSCRIBE_PATHS_SUPPORTED,
331 }
332 }
333}
334
335impl Default for CapabilityMinima {
336 fn default() -> Self {
337 Self::new()
338 }
339}
340
341#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
343#[cfg_attr(feature = "defmt", derive(defmt::Format))]
344pub struct ProductAppearance {
345 pub finish: ProductFinishEnum,
347 pub color: Option<ColorEnum>,
349}
350
351impl ProductAppearance {
352 pub const fn new() -> Self {
355 Self {
356 finish: ProductFinishEnum::Other,
357 color: None,
358 }
359 }
360}
361
362impl Default for ProductAppearance {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[derive(Debug, Clone, Eq, PartialEq, Hash, ToTLV, FromTLV)]
371#[cfg_attr(feature = "defmt", derive(defmt::Format))]
372pub struct DeviceLocation {
373 pub location_name: heapless::String<128>,
375 pub floor_number: Option<i16>,
377 pub area_type: Option<AreaTypeTag>,
380}
381
382impl DeviceLocation {
383 pub const fn new() -> Self {
386 Self {
387 location_name: heapless::String::new(),
388 floor_number: None,
389 area_type: None,
390 }
391 }
392}
393
394impl Default for DeviceLocation {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400#[derive(Debug, Clone, Eq, PartialEq, Hash, ToTLV, FromTLV)]
402#[cfg_attr(feature = "defmt", derive(defmt::Format))]
403pub struct BasicInfoSettings {
404 pub node_label: heapless::String<32>, pub location: Option<heapless::String<2>>, pub location_type: Option<RegulatoryLocationTypeEnum>,
409 pub local_config_disabled: bool,
410 pub configuration_version: u32,
417 pub device_location: Option<Nullable<DeviceLocation>>,
420 pub recovery_identifier: Option<u64>,
428}
429
430impl BasicInfoSettings {
431 pub const fn new() -> Self {
433 Self {
434 node_label: heapless::String::new(),
435 location: None,
436 location_type: None,
437 local_config_disabled: false,
438 configuration_version: 1,
441 device_location: None,
442 recovery_identifier: None,
443 }
444 }
445
446 pub fn init() -> impl Init<Self> {
448 init!(Self {
449 node_label: heapless::String::new(),
450 location: None,
451 location_type: None,
452 local_config_disabled: false,
453 configuration_version: 1,
454 device_location: None,
455 recovery_identifier: None,
456 })
457 }
458
459 pub fn reset(&mut self) {
464 self.node_label.clear();
465 self.location = None;
466 self.location_type = None;
467 self.local_config_disabled = false;
468 self.configuration_version = 1;
469 self.device_location = None;
470 self.recovery_identifier = None;
471 }
472
473 pub fn bump_configuration_version(&mut self) -> u32 {
484 self.configuration_version = self.configuration_version.saturating_add(1);
485 self.configuration_version
486 }
487
488 pub fn set_location(&mut self, location: &str) {
490 self.location = Some(unwrap!(heapless::String::<2>::from_str(location)));
491 }
492
493 pub fn reset_persist<S: KvBlobStore>(
499 &mut self,
500 mut store: S,
501 buf: &mut [u8],
502 ) -> Result<(), Error> {
503 self.reset();
504
505 store.remove(BASIC_INFO_KEY, buf)?;
506
507 info!("Removed basic info settings from storage");
508
509 Ok(())
510 }
511
512 pub fn load(&mut self, data: &[u8]) -> Result<(), Error> {
514 let info = Self::from_tlv(&TLVElement::new(data))?;
515
516 self.node_label = info.node_label;
517 self.location = info.location;
518 self.location_type = info.location_type;
519 self.local_config_disabled = info.local_config_disabled;
520 self.configuration_version = info.configuration_version;
521 self.device_location = info.device_location;
522 self.recovery_identifier = info.recovery_identifier;
523
524 Ok(())
525 }
526
527 #[inline(never)]
537 pub fn store_persist<S: KvBlobStoreAccess>(
538 &self,
539 persist: &mut Persist<S>,
540 ) -> Result<(), Error> {
541 persist.store_tlv(BASIC_INFO_KEY, self)
542 }
543
544 pub fn load_persist<S: KvBlobStore>(
550 &mut self,
551 mut store: S,
552 buf: &mut [u8],
553 ) -> Result<(), Error> {
554 self.reset();
555
556 if let Some(data) = store.load(BASIC_INFO_KEY, buf)? {
557 self.load(data)?;
558
559 info!("Loaded basic info settings from storage");
560 }
561
562 Ok(())
563 }
564}
565
566impl Default for BasicInfoSettings {
567 fn default() -> Self {
568 Self::new()
569 }
570}
571
572#[derive(Clone, Debug)]
574#[cfg_attr(feature = "defmt", derive(defmt::Format))]
575pub struct BasicInfoHandler(Dataver);
576
577impl BasicInfoHandler {
578 pub fn new(dataver: Dataver) -> Self {
580 Self(dataver)
581 }
582
583 pub const fn adapt(self) -> HandlerAdaptor<Self> {
585 HandlerAdaptor(self)
586 }
587
588 fn config<'a>(exchange: &'a Exchange) -> &'a BasicInfoConfig<'a> {
589 exchange.matter().dev_det()
590 }
591
592 fn with_settings<F, R>(exchange: &Exchange, f: F) -> Result<R, Error>
593 where
594 F: FnOnce(&mut BasicInfoSettings) -> Result<R, Error>,
595 {
596 exchange.with_state(|state| f(&mut state.basic_info_settings))
597 }
598}
599
600pub const CLUSTER_DEVICE_LOCATION: Cluster<'static> = FULL_CLUSTER
612 .with_attrs(except!(AttributeId::Reachable))
613 .with_cmds(with!());
614
615impl ClusterHandler for BasicInfoHandler {
616 const CLUSTER: Cluster<'static> = FULL_CLUSTER
617 .with_attrs(except!(
626 AttributeId::Reachable | AttributeId::DeviceLocation
627 ))
628 .with_cmds(with!());
629
630 fn dataver(&self) -> u32 {
631 self.0.get()
632 }
633
634 fn dataver_changed(&self) {
635 self.0.changed();
636 }
637
638 fn data_model_revision(&self, ctx: impl ReadContext) -> Result<u16, Error> {
639 Ok(Self::config(ctx.exchange()).data_model_revision)
640 }
641
642 fn vendor_id(&self, ctx: impl ReadContext) -> Result<u16, Error> {
643 Ok(Self::config(ctx.exchange()).vid)
644 }
645
646 fn vendor_name<P: TLVBuilderParent>(
647 &self,
648 ctx: impl ReadContext,
649 out: Utf8StrBuilder<P>,
650 ) -> Result<P, Error> {
651 out.set(Self::config(ctx.exchange()).vendor_name)
652 }
653
654 fn product_id(&self, ctx: impl ReadContext) -> Result<u16, Error> {
655 Ok(Self::config(ctx.exchange()).pid)
656 }
657
658 fn product_name<P: TLVBuilderParent>(
659 &self,
660 ctx: impl ReadContext,
661 out: Utf8StrBuilder<P>,
662 ) -> Result<P, Error> {
663 out.set(Self::config(ctx.exchange()).product_name)
664 }
665
666 fn hardware_version(&self, ctx: impl ReadContext) -> Result<u16, Error> {
667 Ok(Self::config(ctx.exchange()).hw_ver)
668 }
669
670 fn hardware_version_string<P: TLVBuilderParent>(
671 &self,
672 ctx: impl ReadContext,
673 out: Utf8StrBuilder<P>,
674 ) -> Result<P, Error> {
675 out.set(Self::config(ctx.exchange()).hw_ver_str)
676 }
677
678 fn software_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
679 Ok(Self::config(ctx.exchange()).sw_ver)
680 }
681
682 fn software_version_string<P: TLVBuilderParent>(
683 &self,
684 ctx: impl ReadContext,
685 out: Utf8StrBuilder<P>,
686 ) -> Result<P, Error> {
687 out.set(Self::config(ctx.exchange()).sw_ver_str)
688 }
689
690 fn node_label<P: TLVBuilderParent>(
691 &self,
692 ctx: impl ReadContext,
693 out: Utf8StrBuilder<P>,
694 ) -> Result<P, Error> {
695 Self::with_settings(ctx.exchange(), |settings| {
696 out.set(settings.node_label.as_str())
697 })
698 }
699
700 fn set_node_label(&self, ctx: impl WriteContext, label: &str) -> Result<(), Error> {
701 if label.len() > 32 {
702 return Err(ErrorCode::ConstraintError.into());
703 }
704
705 let mut persist = Persist::new(ctx.kv());
706
707 Self::with_settings(ctx.exchange(), |settings| {
708 settings.node_label.clear();
709 settings
710 .node_label
711 .push_str(label)
712 .map_err(|_| ErrorCode::ConstraintError)?;
713
714 settings.store_persist(&mut persist)
715 })?;
716
717 persist.run()
718 }
719
720 fn location<P: TLVBuilderParent>(
721 &self,
722 ctx: impl ReadContext,
723 out: Utf8StrBuilder<P>,
724 ) -> Result<P, Error> {
725 let config = Self::config(ctx.exchange());
729
730 Self::with_settings(ctx.exchange(), |settings| {
731 out.set(
732 settings
733 .location
734 .as_deref()
735 .or(config.location)
736 .unwrap_or("XX"),
737 )
738 })
739 }
740
741 fn set_location(&self, ctx: impl WriteContext, location: &str) -> Result<(), Error> {
742 if location.len() != 2 {
743 return Err(ErrorCode::ConstraintError.into());
744 }
745
746 let mut persist = Persist::new(ctx.kv());
747
748 Self::with_settings(ctx.exchange(), |settings| {
749 settings.set_location(location);
750
751 settings.store_persist(&mut persist)
752 })?;
753
754 persist.run()
755 }
756
757 fn capability_minima<P: TLVBuilderParent>(
758 &self,
759 ctx: impl ReadContext,
760 builder: CapabilityMinimaStructBuilder<P>,
761 ) -> Result<P, Error> {
762 let cm = Self::config(ctx.exchange()).capability_minima;
763
764 builder
765 .case_sessions_per_fabric(cm.case_sessions_per_fabric)?
766 .subscriptions_per_fabric(cm.subscriptions_per_fabric)?
767 .simultaneous_invocations_supported(Some(cm.simultaneous_invocations_supported))?
768 .simultaneous_writes_supported(Some(cm.simultaneous_writes_supported))?
769 .read_paths_supported(Some(cm.read_paths_supported))?
770 .subscribe_paths_supported(Some(cm.subscribe_paths_supported))?
771 .end()
772 }
773
774 fn specification_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
775 Ok(Self::config(ctx.exchange()).specification_version)
776 }
777
778 fn max_paths_per_invoke(&self, ctx: impl ReadContext) -> Result<u16, Error> {
779 Ok(Self::config(ctx.exchange()).max_paths_per_invoke)
780 }
781
782 fn configuration_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
783 Self::with_settings(
786 ctx.exchange(),
787 |settings| Ok(settings.configuration_version),
788 )
789 }
790
791 #[inline(never)]
794 fn device_location<P: TLVBuilderParent>(
795 &self,
796 ctx: impl ReadContext,
797 builder: NullableBuilder<P, LocationDescriptorStructBuilder<P>>,
798 ) -> Result<P, Error> {
799 let config = Self::config(ctx.exchange());
800
801 Self::with_settings(ctx.exchange(), |settings| {
802 let (location_name, floor_number, area_type) = match settings.device_location.as_ref() {
806 Some(location) => match location.as_opt_ref() {
807 Some(location) => (
808 location.location_name.as_str(),
809 location.floor_number,
810 location.area_type,
811 ),
812 None => return builder.null(),
813 },
814 None => match config.device_location {
815 Some(location) => (
816 location.location_name,
817 location.floor_number,
818 location.area_type,
819 ),
820 None => return builder.null(),
821 },
822 };
823
824 builder
825 .non_null()?
826 .location_name(location_name)?
827 .floor_number(Nullable::new(floor_number))?
828 .area_type(Nullable::new(area_type))?
829 .end()
830 })
831 }
832
833 #[inline(never)]
836 fn set_device_location(
837 &self,
838 ctx: impl WriteContext,
839 value: Nullable<LocationDescriptorStruct<'_>>,
840 ) -> Result<(), Error> {
841 let mut persist = Persist::new(ctx.kv());
842
843 Self::with_settings(ctx.exchange(), |settings| {
844 if let Some(value) = value.as_opt_ref() {
845 let location_name = value.location_name()?;
848 if location_name.len() > 128 {
849 return Err(ErrorCode::ConstraintError.into());
850 }
851
852 let floor_number = value.floor_number()?.into_option();
853 let area_type = value.area_type()?.into_option();
854
855 let location = settings.device_location.get_or_insert_with(Nullable::none);
860 if location.is_none() {
861 *location = Nullable::some(DeviceLocation::new());
862 }
863
864 let location = unwrap!(location.as_opt_mut());
865
866 location.location_name.clear();
867 unwrap!(location.location_name.push_str(location_name));
868 location.floor_number = floor_number;
869 location.area_type = area_type;
870 } else {
871 settings.device_location = Some(Nullable::none());
872 }
873
874 settings.store_persist(&mut persist)
875 })?;
876
877 persist.run()
878 }
879
880 fn handle_mfg_specific_ping(&self, _ctx: impl InvokeContext) -> Result<(), Error> {
881 Err(ErrorCode::CommandNotFound.into())
882 }
883
884 fn manufacturing_date<P: TLVBuilderParent>(
885 &self,
886 ctx: impl ReadContext,
887 builder: Utf8StrBuilder<P>,
888 ) -> Result<P, Error> {
889 builder.set(Self::config(ctx.exchange()).manufacturing_date)
890 }
891
892 fn part_number<P: TLVBuilderParent>(
893 &self,
894 ctx: impl ReadContext,
895 builder: Utf8StrBuilder<P>,
896 ) -> Result<P, Error> {
897 builder.set(Self::config(ctx.exchange()).part_number)
898 }
899
900 fn product_url<P: TLVBuilderParent>(
901 &self,
902 ctx: impl ReadContext,
903 builder: Utf8StrBuilder<P>,
904 ) -> Result<P, Error> {
905 builder.set(Self::config(ctx.exchange()).product_url)
906 }
907
908 fn product_label<P: TLVBuilderParent>(
909 &self,
910 ctx: impl ReadContext,
911 builder: Utf8StrBuilder<P>,
912 ) -> Result<P, Error> {
913 builder.set(Self::config(ctx.exchange()).product_label)
914 }
915
916 fn serial_number<P: TLVBuilderParent>(
917 &self,
918 ctx: impl ReadContext,
919 builder: Utf8StrBuilder<P>,
920 ) -> Result<P, Error> {
921 builder.set(Self::config(ctx.exchange()).serial_no)
922 }
923
924 fn local_config_disabled(&self, ctx: impl ReadContext) -> Result<bool, Error> {
925 Self::with_settings(
926 ctx.exchange(),
927 |settings| Ok(settings.local_config_disabled),
928 )
929 }
930
931 fn set_local_config_disabled(&self, ctx: impl WriteContext, value: bool) -> Result<(), Error> {
932 let mut persist = Persist::new(ctx.kv());
933
934 Self::with_settings(ctx.exchange(), |settings| {
935 settings.local_config_disabled = value;
936
937 settings.store_persist(&mut persist)
938 })?;
939
940 persist.run()
941 }
942
943 fn unique_id<P: TLVBuilderParent>(
944 &self,
945 ctx: impl ReadContext,
946 builder: Utf8StrBuilder<P>,
947 ) -> Result<P, Error> {
948 builder.set(Self::config(ctx.exchange()).unique_id)
949 }
950
951 fn product_appearance<P: TLVBuilderParent>(
952 &self,
953 ctx: impl ReadContext,
954 builder: ProductAppearanceStructBuilder<P>,
955 ) -> Result<P, Error> {
956 let appearance = Self::config(ctx.exchange()).product_appearance;
957
958 builder
959 .finish(appearance.finish)?
960 .primary_color(Nullable::new(appearance.color))?
961 .end()
962 }
963}
964
965#[cfg(test)]
966mod tests {
967 use super::{BasicInfoSettings, DeviceLocation};
968
969 use crate::tlv::{Nullable, TLVElement, TLVTag, ToTLV};
970 use crate::utils::storage::WriteBuf;
971
972 fn round_trip(settings: &BasicInfoSettings) -> BasicInfoSettings {
974 let mut buf = [0; 512];
975 let mut wb = WriteBuf::new(&mut buf);
976 settings.to_tlv(&TLVTag::Anonymous, &mut wb).unwrap();
977
978 let mut loaded = BasicInfoSettings::new();
979 loaded.load(wb.as_slice()).unwrap();
980
981 loaded
982 }
983
984 #[test]
989 fn explicit_xx_location_is_kept() {
990 let mut settings = BasicInfoSettings::new();
991 assert!(settings.location.is_none());
992
993 settings.set_location("XX");
994 assert_eq!(settings.location.as_deref(), Some("XX"));
995 assert_eq!(round_trip(&settings).location.as_deref(), Some("XX"));
996 }
997
998 #[test]
1002 fn device_location_states_survive_persistence() {
1003 let mut settings = BasicInfoSettings::new();
1004 assert!(round_trip(&settings).device_location.is_none());
1005
1006 settings.device_location = Some(Nullable::none());
1007 let loaded = round_trip(&settings);
1008 assert!(matches!(&loaded.device_location, Some(l) if l.is_none()));
1009
1010 settings.device_location = Some(Nullable::some(DeviceLocation {
1011 location_name: "Basement".try_into().unwrap(),
1012 floor_number: Some(-1),
1013 area_type: None,
1014 }));
1015 let loaded = round_trip(&settings);
1016 let location = loaded.device_location.unwrap().into_option().unwrap();
1017 assert_eq!(location.location_name, "Basement");
1018 assert_eq!(location.floor_number, Some(-1));
1019 assert_eq!(location.area_type, None);
1020 }
1021
1022 #[test]
1025 fn missing_device_location_field_loads_as_unset() {
1026 let settings = BasicInfoSettings::new();
1030 let loaded = round_trip(&settings);
1031 assert!(loaded.device_location.is_none());
1032
1033 let mut settings = BasicInfoSettings::new();
1036 settings.set_location("US");
1037 settings.location_type = Some(super::RegulatoryLocationTypeEnum::Indoor);
1038 settings.device_location = Some(Nullable::none());
1039 settings.reset();
1040 assert!(settings.location.is_none());
1041 assert!(settings.location_type.is_none());
1042 assert!(settings.device_location.is_none());
1043 }
1044
1045 #[test]
1050 fn recovery_identifier_survives_persistence_and_resets() {
1051 let mut settings = BasicInfoSettings::new();
1053 assert!(settings.recovery_identifier.is_none());
1054 assert!(round_trip(&settings).recovery_identifier.is_none());
1055
1056 settings.recovery_identifier = Some(0x1122_3344_5566_7788);
1058 assert_eq!(
1059 round_trip(&settings).recovery_identifier,
1060 Some(0x1122_3344_5566_7788)
1061 );
1062
1063 settings.reset();
1065 assert!(settings.recovery_identifier.is_none());
1066 }
1067
1068 #[allow(unused)]
1070 fn _t(_: TLVElement) {}
1071}