#![allow(clippy::bad_bit_mask)]
use core::cell::Cell;
use core::fmt::{self, Debug};
use strum::FromRepr;
use crate::attribute_enum;
use crate::dm::Metadata;
use crate::error::{Error, ErrorCode};
use crate::im::encoding::{AttrPath, AttrStatus, IMStatusCode};
use crate::tlv::{AsNullable, FromTLV, Nullable, TLVBuilder, TLVBuilderParent, TLVElement, TLVTag};
use crate::utils::maybe::Maybe;
use super::{Access, AttrId, Cluster, ClusterId, EndptId, Quality};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Attribute {
pub id: AttrId,
pub access: Access,
pub quality: Quality,
}
impl Attribute {
pub const fn new(id: AttrId, access: Access, quality: Quality) -> Self {
Self {
id,
access,
quality,
}
}
pub const fn is_system(&self) -> bool {
Self::is_system_attr(self.id)
}
pub const fn is_system_attr(attr_id: AttrId) -> bool {
attr_id >= (GlobalElements::GeneratedCmdList as AttrId) && attr_id <= u16::MAX as AttrId
}
}
impl core::fmt::Display for Attribute {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, FromRepr)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u32)]
pub enum GlobalElements {
InteractionModelRevision = 0xFF,
FabricIndex = 0xFE,
GeneratedCmdList = 0xFFF8,
AcceptedCmdList = 0xFFF9,
EventList = 0xFFFA,
AttributeList = 0xFFFB,
FeatureMap = 0xFFFC,
ClusterRevision = 0xFFFD,
}
attribute_enum!(GlobalElements);
pub const GENERATED_COMMAND_LIST: Attribute = Attribute::new(
GlobalElements::GeneratedCmdList as _,
Access::RV,
Quality::A,
);
pub const ACCEPTED_COMMAND_LIST: Attribute =
Attribute::new(GlobalElements::AcceptedCmdList as _, Access::RV, Quality::A);
pub const EVENT_LIST: Attribute =
Attribute::new(GlobalElements::EventList as _, Access::RV, Quality::A);
pub const ATTRIBUTE_LIST: Attribute =
Attribute::new(GlobalElements::AttributeList as _, Access::RV, Quality::A);
pub const FEATURE_MAP: Attribute =
Attribute::new(GlobalElements::FeatureMap as _, Access::RV, Quality::NONE);
pub const CLUSTER_REVISION: Attribute = Attribute::new(
GlobalElements::ClusterRevision as _,
Access::RV,
Quality::NONE,
);
#[allow(unused_macros)]
#[macro_export]
macro_rules! attributes {
() => {
&[
$crate::dm::GENERATED_COMMAND_LIST,
$crate::dm::ACCEPTED_COMMAND_LIST,
$crate::dm::EVENT_LIST,
$crate::dm::ATTRIBUTE_LIST,
$crate::dm::FEATURE_MAP,
$crate::dm::CLUSTER_REVISION,
]
};
($attr0:expr $(, $attr:expr)* $(,)?) => {
&[
$attr0,
$($attr,)*
$crate::dm::GENERATED_COMMAND_LIST,
$crate::dm::ACCEPTED_COMMAND_LIST,
$crate::dm::EVENT_LIST,
$crate::dm::ATTRIBUTE_LIST,
$crate::dm::FEATURE_MAP,
$crate::dm::CLUSTER_REVISION,
]
}
}
#[allow(unused_macros)]
#[macro_export]
macro_rules! attribute_enum {
($en:ty) => {
impl core::convert::TryFrom<$crate::dm::AttrId> for $en {
type Error = $crate::error::Error;
fn try_from(id: $crate::dm::AttrId) -> Result<Self, Self::Error> {
<$en>::from_repr(id)
.ok_or_else(|| $crate::error::ErrorCode::AttributeNotFound.into())
}
}
};
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ArrayAttributeRead<T, E> {
ReadAll(T),
ReadOne(u16, E),
ReadNone(T),
}
impl<T, E> ArrayAttributeRead<T, E> {
pub fn new<P>(
index: Option<Maybe<u16, AsNullable>>,
parent: P,
tag: &TLVTag,
) -> Result<Self, Error>
where
P: TLVBuilderParent,
T: TLVBuilder<P>,
E: TLVBuilder<P>,
{
match index.map(Nullable::into_option) {
Some(Some(index)) => Ok(Self::ReadOne(index, E::new(parent, tag)?)),
Some(None) => Ok(Self::ReadNone(T::new(parent, tag)?)),
None => Ok(Self::ReadAll(T::new(parent, tag)?)),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ArrayAttributeWrite<T, E> {
Replace(T),
Add(E),
Update(u16, E),
Remove(u16),
}
impl<T, E> ArrayAttributeWrite<T, E> {
pub fn new<'a>(
index: Option<Maybe<u16, AsNullable>>,
data: &TLVElement<'a>,
) -> Result<Self, Error>
where
T: FromTLV<'a>,
E: FromTLV<'a>,
{
match index.map(Nullable::into_option) {
Some(Some(_index)) => {
Err(ErrorCode::InvalidAction.into())
}
Some(None) => {
Ok(Self::Add(FromTLV::from_tlv(data)?))
}
None => {
Ok(Self::Replace(FromTLV::from_tlv(data)?))
}
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct AttrDetails {
pub endpoint_id: EndptId,
pub cluster_id: ClusterId,
pub attr_id: AttrId,
pub list_index: Option<Nullable<u16>>,
pub list_chunked: bool,
pub fab_idx: u8,
pub fab_filter: bool,
pub dataver: Option<u32>,
pub wildcard: bool,
pub array: bool,
pub cluster_status: Cell<u8>,
}
impl AttrDetails {
pub const fn is_system(&self) -> bool {
Attribute::is_system_attr(self.attr_id)
}
pub fn reply_path(&self) -> AttrPath {
AttrPath {
node: None,
endpoint: Some(self.endpoint_id),
cluster: Some(self.cluster_id),
attr: Some(self.attr_id),
list_index: if self.list_chunked {
match self.list_index.as_ref().map(|li| li.as_opt_ref()) {
Some(Some(_)) => Some(Nullable::none()),
Some(None) | None => None,
}
} else {
self.list_index.clone()
},
tag_compression: None,
}
}
pub fn cluster<M, F, R>(&self, metadata: M, f: F) -> Result<R, Error>
where
M: Metadata,
F: FnOnce(&Cluster) -> Result<R, Error>,
{
metadata.access(|node| {
let cluster = node
.endpoint(self.endpoint_id)
.and_then(|endpoint| endpoint.cluster(self.cluster_id))
.ok_or_else(|| {
error!("Cluster not found");
Error::new(ErrorCode::ClusterNotFound)
})?;
f(cluster)
})
}
pub fn set_cluster_status(&self, cluster_status: u8) {
self.cluster_status.set(cluster_status);
}
pub fn cluster_status(&self) -> Option<u16> {
match self.cluster_status.get() {
0 => None,
n => Some(n as u16),
}
}
pub fn status(&self, status: IMStatusCode) -> Option<AttrStatus> {
if self.should_report(status) {
Some(AttrStatus::new(
self.reply_path(),
status,
self.cluster_status(),
))
} else {
None
}
}
pub fn check_dataver(&self, dataver: u32) -> Result<(), Error> {
if let Some(req_dataver) = self.dataver {
if req_dataver != dataver {
Err(ErrorCode::DataVersionMismatch)?;
}
}
Ok(())
}
const fn should_report(&self, status: IMStatusCode) -> bool {
!self.wildcard
|| !matches!(
status,
IMStatusCode::UnsupportedEndpoint
| IMStatusCode::UnsupportedCluster
| IMStatusCode::UnsupportedAttribute
| IMStatusCode::UnsupportedCommand
| IMStatusCode::UnsupportedAccess
| IMStatusCode::UnsupportedRead
| IMStatusCode::UnsupportedWrite
| IMStatusCode::DataVersionMismatch
)
}
}
#[cfg(test)]
#[allow(clippy::bool_assert_comparison)]
mod tests {
use super::Access;
use crate::dm::Privilege;
#[test]
fn test_read() {
let c = Access::READ;
assert_eq!(c.is_ok(Access::READ, Privilege::VIEW), false);
let c = Access::WRITE | Access::NEED_VIEW;
assert_eq!(c.is_ok(Access::READ, Privilege::VIEW), false);
let c = Access::RV;
assert_eq!(c.is_ok(Access::READ, Privilege::VIEW), true);
assert_eq!(c.is_ok(Access::READ, Privilege::ADMIN), true);
let c = Access::READ | Access::NEED_ADMIN;
assert_eq!(c.is_ok(Access::READ, Privilege::VIEW), false);
assert_eq!(c.is_ok(Access::READ, Privilege::OPERATE), false);
assert_eq!(c.is_ok(Access::READ, Privilege::MANAGE), false);
assert_eq!(c.is_ok(Access::READ, Privilege::ADMIN), true);
let c = Access::READ | Access::NEED_OPERATE;
assert_eq!(c.is_ok(Access::READ, Privilege::VIEW), false);
assert_eq!(c.is_ok(Access::READ, Privilege::OPERATE), true);
assert_eq!(c.is_ok(Access::READ, Privilege::MANAGE), true);
assert_eq!(c.is_ok(Access::READ, Privilege::ADMIN), true);
}
#[test]
fn test_write() {
let c = Access::WRITE;
assert_eq!(c.is_ok(Access::WRITE, Privilege::VIEW), false);
let c = Access::READ | Access::NEED_MANAGE;
assert_eq!(c.is_ok(Access::WRITE, Privilege::MANAGE), false);
let c = Access::RWVA;
assert_eq!(c.is_ok(Access::WRITE, Privilege::VIEW), false);
assert_eq!(c.is_ok(Access::WRITE, Privilege::ADMIN), true);
let c = Access::RWVA;
assert_eq!(c.is_ok(Access::WRITE, Privilege::VIEW), false);
assert_eq!(c.is_ok(Access::WRITE, Privilege::OPERATE), false);
assert_eq!(c.is_ok(Access::WRITE, Privilege::MANAGE), false);
assert_eq!(c.is_ok(Access::WRITE, Privilege::ADMIN), true);
assert_eq!(c.is_ok(Access::READ, Privilege::VIEW), true);
assert_eq!(c.is_ok(Access::READ, Privilege::OPERATE), true);
assert_eq!(c.is_ok(Access::READ, Privilege::MANAGE), true);
assert_eq!(c.is_ok(Access::READ, Privilege::ADMIN), true);
let c = Access::WRITE | Access::NEED_OPERATE;
assert_eq!(c.is_ok(Access::WRITE, Privilege::VIEW), false);
assert_eq!(c.is_ok(Access::WRITE, Privilege::OPERATE), true);
assert_eq!(c.is_ok(Access::WRITE, Privilege::MANAGE), true);
assert_eq!(c.is_ok(Access::WRITE, Privilege::ADMIN), true);
}
}