use core::str::FromStr;
use crate::dm::{Cluster, Dataver, InvokeContext, ReadContext, WriteContext};
use crate::error::{Error, ErrorCode};
use crate::fabric::MAX_FABRICS;
use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist, BASIC_INFO_KEY};
use crate::tlv::{
FromTLV, Nullable, NullableBuilder, TLVBuilderParent, TLVElement, ToTLV, Utf8StrBuilder,
};
use crate::transport::exchange::Exchange;
use crate::transport::session::MAX_SESSIONS;
use crate::utils::bitflags::bitflags;
use crate::utils::init::{init, Init};
use crate::{except, with};
pub use crate::dm::clusters::decl::basic_information::*;
pub use crate::dm::clusters::decl::general_commissioning::RegulatoryLocationTypeEnum;
pub use crate::dm::clusters::decl::globals::{
AreaTypeTag, LocationDescriptorStruct, LocationDescriptorStructBuilder,
};
pub const DEFAULT_MATTER_SPEC_VERSION: u32 = 0x01060000;
pub const DEFAULT_DATA_MODEL_REVISION: u16 = 21;
pub const DEFAULT_MAX_PATHS_PER_INVOKE: u16 = 5;
bitflags! {
#[repr(transparent)]
#[derive(Default)]
#[cfg_attr(not(feature = "defmt"), derive(Debug, Copy, Clone, Eq, PartialEq, Hash))]
pub struct PairingHintFlags: u32 {
const POWER_CYCLE = 0x0000_0001;
const DEV_MANUFACTURER_URL = 0x0000_0002;
const ADMINISTRATOR = 0x0000_0004;
const SETTINGS_MENU = 0x0000_0008;
const CUSTOM_INSTRUCTION = 0x0000_0010;
const DEVICE_MANUAL = 0x0000_0020;
const PRESS_RESET_BUTTON = 0x0000_0040;
const PRESS_RESET_BUTTON_WITH_POWER = 0x0000_0080;
const PRESS_RESET_BUTTON_FOR_N_SECONDS = 0x0000_0100;
const PRESS_RESET_BUTTON_UNTIL_LIGHT_BLINKS = 0x0000_0200;
const PRESS_RESET_BUTTON_FOR_N_SECONDS_WITH_POWER = 0x0000_0400;
const PRESS_RESET_BUTTON_UNTIL_LIGHT_BLINKS_WITH_POWER = 0x0000_0800;
const PRESS_RESET_BUTTON_N_TIMES = 0x0000_1000;
const PRESS_SETUP_BUTTON = 0x0000_2000;
const PRESS_SETUP_BUTTON_WITH_POWER = 0x0000_4000;
const PRESS_SETUP_BUTTON_FOR_N_SECONDS = 0x0000_8000;
const PRESS_SETUP_BUTTON_UNTIL_LIGHT_BLINKS = 0x0001_0000;
const PRESS_SETUP_BUTTON_FOR_N_SECONDS_WITH_POWER = 0x0002_0000;
const PRESS_SETUP_BUTTON_UNTIL_LIGHT_BLINKS_WITH_POWER = 0x0004_0000;
const PRESS_SETUP_BUTTON_N_TIMES = 0x0008_0000;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct DeviceLocationConfig<'a> {
pub location_name: &'a str,
pub floor_number: Option<i16>,
pub area_type: Option<AreaTypeTag>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct BasicInfoConfig<'a> {
pub vendor_name: &'a str,
pub vid: u16,
pub product_name: &'a str,
pub pid: u16,
pub hw_ver: u16,
pub hw_ver_str: &'a str,
pub sw_ver: u32,
pub sw_ver_str: &'a str,
pub manufacturing_date: &'a str,
pub part_number: &'a str,
pub product_url: &'a str,
pub product_label: &'a str,
pub serial_no: &'a str,
pub unique_id: &'a str,
pub location: Option<&'a str>,
pub device_location: Option<DeviceLocationConfig<'a>>,
pub capability_minima: CapabilityMinima,
pub product_appearance: ProductAppearance,
pub specification_version: u32,
pub data_model_revision: u16,
pub max_paths_per_invoke: u16,
pub device_name: &'a str,
pub device_type: Option<u16>,
pub pairing_hint: PairingHintFlags,
pub pairing_instruction: &'a str,
pub sai: Option<u32>,
pub sii: Option<u32>,
pub tcp_supported: bool,
}
impl BasicInfoConfig<'_> {
pub const fn new() -> Self {
Self {
vid: 0,
pid: 0,
hw_ver: 0,
hw_ver_str: "",
sw_ver: 0,
sw_ver_str: "",
serial_no: "",
product_name: "",
vendor_name: "",
manufacturing_date: "",
part_number: "",
product_url: "",
product_label: "",
unique_id: "",
location: None,
device_location: None,
capability_minima: CapabilityMinima::new(),
product_appearance: ProductAppearance::new(),
specification_version: DEFAULT_MATTER_SPEC_VERSION,
data_model_revision: DEFAULT_DATA_MODEL_REVISION,
max_paths_per_invoke: DEFAULT_MAX_PATHS_PER_INVOKE,
device_name: "",
device_type: None,
pairing_hint: PairingHintFlags::empty(),
pairing_instruction: "",
sai: None,
sii: None,
tcp_supported: false,
}
}
}
impl Default for BasicInfoConfig<'_> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct CapabilityMinima {
pub case_sessions_per_fabric: u16,
pub subscriptions_per_fabric: u16,
pub simultaneous_invocations_supported: u16,
pub simultaneous_writes_supported: u16,
pub read_paths_supported: u16,
pub subscribe_paths_supported: u16,
}
const SUBSCRIPTIONS_PER_FABRIC: u16 = 3;
const SIMULTANEOUS_INVOCATIONS_SUPPORTED: u16 = 1;
const SIMULTANEOUS_WRITES_SUPPORTED: u16 = 1;
const READ_PATHS_SUPPORTED: u16 = 9;
const SUBSCRIBE_PATHS_SUPPORTED: u16 = 3;
impl CapabilityMinima {
pub const fn new() -> Self {
Self {
case_sessions_per_fabric: (MAX_SESSIONS / MAX_FABRICS) as _,
subscriptions_per_fabric: SUBSCRIPTIONS_PER_FABRIC,
simultaneous_invocations_supported: SIMULTANEOUS_INVOCATIONS_SUPPORTED,
simultaneous_writes_supported: SIMULTANEOUS_WRITES_SUPPORTED,
read_paths_supported: READ_PATHS_SUPPORTED,
subscribe_paths_supported: SUBSCRIBE_PATHS_SUPPORTED,
}
}
}
impl Default for CapabilityMinima {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct ProductAppearance {
pub finish: ProductFinishEnum,
pub color: Option<ColorEnum>,
}
impl ProductAppearance {
pub const fn new() -> Self {
Self {
finish: ProductFinishEnum::Other,
color: None,
}
}
}
impl Default for ProductAppearance {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, ToTLV, FromTLV)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct DeviceLocation {
pub location_name: heapless::String<128>,
pub floor_number: Option<i16>,
pub area_type: Option<AreaTypeTag>,
}
impl DeviceLocation {
pub const fn new() -> Self {
Self {
location_name: heapless::String::new(),
floor_number: None,
area_type: None,
}
}
}
impl Default for DeviceLocation {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, ToTLV, FromTLV)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct BasicInfoSettings {
pub node_label: heapless::String<32>, pub location: Option<heapless::String<2>>, pub location_type: Option<RegulatoryLocationTypeEnum>,
pub local_config_disabled: bool,
pub configuration_version: u32,
pub device_location: Option<Nullable<DeviceLocation>>,
pub recovery_identifier: Option<u64>,
}
impl BasicInfoSettings {
pub const fn new() -> Self {
Self {
node_label: heapless::String::new(),
location: None,
location_type: None,
local_config_disabled: false,
configuration_version: 1,
device_location: None,
recovery_identifier: None,
}
}
pub fn init() -> impl Init<Self> {
init!(Self {
node_label: heapless::String::new(),
location: None,
location_type: None,
local_config_disabled: false,
configuration_version: 1,
device_location: None,
recovery_identifier: None,
})
}
pub fn reset(&mut self) {
self.node_label.clear();
self.location = None;
self.location_type = None;
self.local_config_disabled = false;
self.configuration_version = 1;
self.device_location = None;
self.recovery_identifier = None;
}
pub fn bump_configuration_version(&mut self) -> u32 {
self.configuration_version = self.configuration_version.saturating_add(1);
self.configuration_version
}
pub fn set_location(&mut self, location: &str) {
self.location = Some(unwrap!(heapless::String::<2>::from_str(location)));
}
pub fn reset_persist<S: KvBlobStore>(
&mut self,
mut store: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
store.remove(BASIC_INFO_KEY, buf)?;
info!("Removed basic info settings from storage");
Ok(())
}
pub fn load(&mut self, data: &[u8]) -> Result<(), Error> {
let info = Self::from_tlv(&TLVElement::new(data))?;
self.node_label = info.node_label;
self.location = info.location;
self.location_type = info.location_type;
self.local_config_disabled = info.local_config_disabled;
self.configuration_version = info.configuration_version;
self.device_location = info.device_location;
self.recovery_identifier = info.recovery_identifier;
Ok(())
}
#[inline(never)]
pub fn store_persist<S: KvBlobStoreAccess>(
&self,
persist: &mut Persist<S>,
) -> Result<(), Error> {
persist.store_tlv(BASIC_INFO_KEY, self)
}
pub fn load_persist<S: KvBlobStore>(
&mut self,
mut store: S,
buf: &mut [u8],
) -> Result<(), Error> {
self.reset();
if let Some(data) = store.load(BASIC_INFO_KEY, buf)? {
self.load(data)?;
info!("Loaded basic info settings from storage");
}
Ok(())
}
}
impl Default for BasicInfoSettings {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct BasicInfoHandler(Dataver);
impl BasicInfoHandler {
pub fn new(dataver: Dataver) -> Self {
Self(dataver)
}
pub const fn adapt(self) -> HandlerAdaptor<Self> {
HandlerAdaptor(self)
}
fn config<'a>(exchange: &'a Exchange) -> &'a BasicInfoConfig<'a> {
exchange.matter().dev_det()
}
fn with_settings<F, R>(exchange: &Exchange, f: F) -> Result<R, Error>
where
F: FnOnce(&mut BasicInfoSettings) -> Result<R, Error>,
{
exchange.with_state(|state| f(&mut state.basic_info_settings))
}
}
pub const CLUSTER_DEVICE_LOCATION: Cluster<'static> = FULL_CLUSTER
.with_attrs(except!(AttributeId::Reachable))
.with_cmds(with!());
impl ClusterHandler for BasicInfoHandler {
const CLUSTER: Cluster<'static> = FULL_CLUSTER
.with_attrs(except!(
AttributeId::Reachable | AttributeId::DeviceLocation
))
.with_cmds(with!());
fn dataver(&self) -> u32 {
self.0.get()
}
fn dataver_changed(&self) {
self.0.changed();
}
fn data_model_revision(&self, ctx: impl ReadContext) -> Result<u16, Error> {
Ok(Self::config(ctx.exchange()).data_model_revision)
}
fn vendor_id(&self, ctx: impl ReadContext) -> Result<u16, Error> {
Ok(Self::config(ctx.exchange()).vid)
}
fn vendor_name<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
out: Utf8StrBuilder<P>,
) -> Result<P, Error> {
out.set(Self::config(ctx.exchange()).vendor_name)
}
fn product_id(&self, ctx: impl ReadContext) -> Result<u16, Error> {
Ok(Self::config(ctx.exchange()).pid)
}
fn product_name<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
out: Utf8StrBuilder<P>,
) -> Result<P, Error> {
out.set(Self::config(ctx.exchange()).product_name)
}
fn hardware_version(&self, ctx: impl ReadContext) -> Result<u16, Error> {
Ok(Self::config(ctx.exchange()).hw_ver)
}
fn hardware_version_string<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
out: Utf8StrBuilder<P>,
) -> Result<P, Error> {
out.set(Self::config(ctx.exchange()).hw_ver_str)
}
fn software_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
Ok(Self::config(ctx.exchange()).sw_ver)
}
fn software_version_string<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
out: Utf8StrBuilder<P>,
) -> Result<P, Error> {
out.set(Self::config(ctx.exchange()).sw_ver_str)
}
fn node_label<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
out: Utf8StrBuilder<P>,
) -> Result<P, Error> {
Self::with_settings(ctx.exchange(), |settings| {
out.set(settings.node_label.as_str())
})
}
fn set_node_label(&self, ctx: impl WriteContext, label: &str) -> Result<(), Error> {
if label.len() > 32 {
return Err(ErrorCode::ConstraintError.into());
}
let mut persist = Persist::new(ctx.kv());
Self::with_settings(ctx.exchange(), |settings| {
settings.node_label.clear();
settings
.node_label
.push_str(label)
.map_err(|_| ErrorCode::ConstraintError)?;
settings.store_persist(&mut persist)
})?;
persist.run()
}
fn location<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
out: Utf8StrBuilder<P>,
) -> Result<P, Error> {
let config = Self::config(ctx.exchange());
Self::with_settings(ctx.exchange(), |settings| {
out.set(
settings
.location
.as_deref()
.or(config.location)
.unwrap_or("XX"),
)
})
}
fn set_location(&self, ctx: impl WriteContext, location: &str) -> Result<(), Error> {
if location.len() != 2 {
return Err(ErrorCode::ConstraintError.into());
}
let mut persist = Persist::new(ctx.kv());
Self::with_settings(ctx.exchange(), |settings| {
settings.set_location(location);
settings.store_persist(&mut persist)
})?;
persist.run()
}
fn capability_minima<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: CapabilityMinimaStructBuilder<P>,
) -> Result<P, Error> {
let cm = Self::config(ctx.exchange()).capability_minima;
builder
.case_sessions_per_fabric(cm.case_sessions_per_fabric)?
.subscriptions_per_fabric(cm.subscriptions_per_fabric)?
.simultaneous_invocations_supported(Some(cm.simultaneous_invocations_supported))?
.simultaneous_writes_supported(Some(cm.simultaneous_writes_supported))?
.read_paths_supported(Some(cm.read_paths_supported))?
.subscribe_paths_supported(Some(cm.subscribe_paths_supported))?
.end()
}
fn specification_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
Ok(Self::config(ctx.exchange()).specification_version)
}
fn max_paths_per_invoke(&self, ctx: impl ReadContext) -> Result<u16, Error> {
Ok(Self::config(ctx.exchange()).max_paths_per_invoke)
}
fn configuration_version(&self, ctx: impl ReadContext) -> Result<u32, Error> {
Self::with_settings(
ctx.exchange(),
|settings| Ok(settings.configuration_version),
)
}
#[inline(never)]
fn device_location<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: NullableBuilder<P, LocationDescriptorStructBuilder<P>>,
) -> Result<P, Error> {
let config = Self::config(ctx.exchange());
Self::with_settings(ctx.exchange(), |settings| {
let (location_name, floor_number, area_type) = match settings.device_location.as_ref() {
Some(location) => match location.as_opt_ref() {
Some(location) => (
location.location_name.as_str(),
location.floor_number,
location.area_type,
),
None => return builder.null(),
},
None => match config.device_location {
Some(location) => (
location.location_name,
location.floor_number,
location.area_type,
),
None => return builder.null(),
},
};
builder
.non_null()?
.location_name(location_name)?
.floor_number(Nullable::new(floor_number))?
.area_type(Nullable::new(area_type))?
.end()
})
}
#[inline(never)]
fn set_device_location(
&self,
ctx: impl WriteContext,
value: Nullable<LocationDescriptorStruct<'_>>,
) -> Result<(), Error> {
let mut persist = Persist::new(ctx.kv());
Self::with_settings(ctx.exchange(), |settings| {
if let Some(value) = value.as_opt_ref() {
let location_name = value.location_name()?;
if location_name.len() > 128 {
return Err(ErrorCode::ConstraintError.into());
}
let floor_number = value.floor_number()?.into_option();
let area_type = value.area_type()?.into_option();
let location = settings.device_location.get_or_insert_with(Nullable::none);
if location.is_none() {
*location = Nullable::some(DeviceLocation::new());
}
let location = unwrap!(location.as_opt_mut());
location.location_name.clear();
unwrap!(location.location_name.push_str(location_name));
location.floor_number = floor_number;
location.area_type = area_type;
} else {
settings.device_location = Some(Nullable::none());
}
settings.store_persist(&mut persist)
})?;
persist.run()
}
fn handle_mfg_specific_ping(&self, _ctx: impl InvokeContext) -> Result<(), Error> {
Err(ErrorCode::CommandNotFound.into())
}
fn manufacturing_date<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: Utf8StrBuilder<P>,
) -> Result<P, Error> {
builder.set(Self::config(ctx.exchange()).manufacturing_date)
}
fn part_number<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: Utf8StrBuilder<P>,
) -> Result<P, Error> {
builder.set(Self::config(ctx.exchange()).part_number)
}
fn product_url<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: Utf8StrBuilder<P>,
) -> Result<P, Error> {
builder.set(Self::config(ctx.exchange()).product_url)
}
fn product_label<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: Utf8StrBuilder<P>,
) -> Result<P, Error> {
builder.set(Self::config(ctx.exchange()).product_label)
}
fn serial_number<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: Utf8StrBuilder<P>,
) -> Result<P, Error> {
builder.set(Self::config(ctx.exchange()).serial_no)
}
fn local_config_disabled(&self, ctx: impl ReadContext) -> Result<bool, Error> {
Self::with_settings(
ctx.exchange(),
|settings| Ok(settings.local_config_disabled),
)
}
fn set_local_config_disabled(&self, ctx: impl WriteContext, value: bool) -> Result<(), Error> {
let mut persist = Persist::new(ctx.kv());
Self::with_settings(ctx.exchange(), |settings| {
settings.local_config_disabled = value;
settings.store_persist(&mut persist)
})?;
persist.run()
}
fn unique_id<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: Utf8StrBuilder<P>,
) -> Result<P, Error> {
builder.set(Self::config(ctx.exchange()).unique_id)
}
fn product_appearance<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: ProductAppearanceStructBuilder<P>,
) -> Result<P, Error> {
let appearance = Self::config(ctx.exchange()).product_appearance;
builder
.finish(appearance.finish)?
.primary_color(Nullable::new(appearance.color))?
.end()
}
}
#[cfg(test)]
mod tests {
use super::{BasicInfoSettings, DeviceLocation};
use crate::tlv::{Nullable, TLVElement, TLVTag, ToTLV};
use crate::utils::storage::WriteBuf;
fn round_trip(settings: &BasicInfoSettings) -> BasicInfoSettings {
let mut buf = [0; 512];
let mut wb = WriteBuf::new(&mut buf);
settings.to_tlv(&TLVTag::Anonymous, &mut wb).unwrap();
let mut loaded = BasicInfoSettings::new();
loaded.load(wb.as_slice()).unwrap();
loaded
}
#[test]
fn explicit_xx_location_is_kept() {
let mut settings = BasicInfoSettings::new();
assert!(settings.location.is_none());
settings.set_location("XX");
assert_eq!(settings.location.as_deref(), Some("XX"));
assert_eq!(round_trip(&settings).location.as_deref(), Some("XX"));
}
#[test]
fn device_location_states_survive_persistence() {
let mut settings = BasicInfoSettings::new();
assert!(round_trip(&settings).device_location.is_none());
settings.device_location = Some(Nullable::none());
let loaded = round_trip(&settings);
assert!(matches!(&loaded.device_location, Some(l) if l.is_none()));
settings.device_location = Some(Nullable::some(DeviceLocation {
location_name: "Basement".try_into().unwrap(),
floor_number: Some(-1),
area_type: None,
}));
let loaded = round_trip(&settings);
let location = loaded.device_location.unwrap().into_option().unwrap();
assert_eq!(location.location_name, "Basement");
assert_eq!(location.floor_number, Some(-1));
assert_eq!(location.area_type, None);
}
#[test]
fn missing_device_location_field_loads_as_unset() {
let settings = BasicInfoSettings::new();
let loaded = round_trip(&settings);
assert!(loaded.device_location.is_none());
let mut settings = BasicInfoSettings::new();
settings.set_location("US");
settings.location_type = Some(super::RegulatoryLocationTypeEnum::Indoor);
settings.device_location = Some(Nullable::none());
settings.reset();
assert!(settings.location.is_none());
assert!(settings.location_type.is_none());
assert!(settings.device_location.is_none());
}
#[test]
fn recovery_identifier_survives_persistence_and_resets() {
let mut settings = BasicInfoSettings::new();
assert!(settings.recovery_identifier.is_none());
assert!(round_trip(&settings).recovery_identifier.is_none());
settings.recovery_identifier = Some(0x1122_3344_5566_7788);
assert_eq!(
round_trip(&settings).recovery_identifier,
Some(0x1122_3344_5566_7788)
);
settings.reset();
assert!(settings.recovery_identifier.is_none());
}
#[allow(unused)]
fn _t(_: TLVElement) {}
}