use core::fmt::{self, Debug};
use crate::acl::{AccessReq, Accessor};
use crate::dm::*;
use crate::error::{Error, ErrorCode};
use crate::im::encoding::{GenericPath, IMStatusCode};
use crate::tlv::{TLVTag, TLVWrite};
pub type WithAttrs = fn(&Attribute, u16, u32) -> bool;
pub type WithCmds = fn(&Command, u16, u32) -> bool;
pub type WithEvents = fn(&Event, u16, u32) -> bool;
#[derive(Debug, Clone)]
pub struct Cluster<'a> {
pub id: ClusterId,
pub revision: u16,
pub feature_map: u32,
pub attributes: &'a [Attribute],
pub commands: &'a [Command],
pub events: &'a [Event],
pub with_attrs: WithAttrs,
pub with_cmds: WithCmds,
pub with_events: WithEvents,
}
impl<'a> Cluster<'a> {
#[allow(clippy::too_many_arguments)]
pub const fn new(
id: ClusterId,
revision: u16,
feature_map: u32,
attributes: &'a [Attribute],
commands: &'a [Command],
events: &'a [Event],
with_attrs: WithAttrs,
with_cmds: WithCmds,
with_events: WithEvents,
) -> Self {
Self {
id,
revision,
feature_map,
attributes,
commands,
events,
with_attrs,
with_cmds,
with_events,
}
}
pub const fn with_revision(self, revision: u16) -> Self {
Self { revision, ..self }
}
pub const fn with_features(self, feature_map: u32) -> Self {
Self {
feature_map,
..self
}
}
pub const fn with_attrs(self, with_attrs: WithAttrs) -> Self {
Self { with_attrs, ..self }
}
pub const fn with_cmds(self, with_cmds: WithCmds) -> Self {
Self { with_cmds, ..self }
}
pub const fn with_events(self, with_events: WithEvents) -> Self {
Self {
with_events,
..self
}
}
pub(crate) fn check_attr_access(
&self,
accessor: &Accessor,
timed: bool,
path: GenericPath,
device_types: &[DeviceType],
write: bool,
attr_id: AttrId,
) -> Result<(), IMStatusCode> {
let mut access_req = AccessReq::new_with_device_types(
accessor,
path,
if write { Access::WRITE } else { Access::READ },
device_types,
);
let target_perms = self
.attributes
.iter()
.find(|attr| attr.id == attr_id)
.map(|attr| attr.access)
.unwrap_or(Access::empty());
if write && !timed && target_perms.contains(Access::TIMED_ONLY) {
Err(IMStatusCode::NeedsTimedInteraction)?;
}
if !target_perms.contains(access_req.operation()) {
Err(if matches!(access_req.operation(), Access::WRITE) {
IMStatusCode::UnsupportedWrite
} else {
IMStatusCode::UnsupportedRead
})?;
}
access_req.set_target_perms(target_perms);
if access_req.allow() {
Ok(())
} else {
Err(IMStatusCode::UnsupportedAccess)
}
}
pub(crate) fn check_cmd_access(
&self,
accessor: &Accessor,
timed: bool,
path: GenericPath,
device_types: &[DeviceType],
cmd_id: CmdId,
) -> Result<(), IMStatusCode> {
let mut access_req =
AccessReq::new_with_device_types(accessor, path, Access::WRITE, device_types);
let target_perms = self
.commands
.iter()
.find(|cmd| cmd.id == cmd_id)
.map(|cmd| cmd.access)
.unwrap_or(Access::empty());
if !timed && target_perms.contains(Access::TIMED_ONLY) {
Err(IMStatusCode::NeedsTimedInteraction)?;
}
if target_perms.contains(Access::FAB_SCOPED) && accessor.fab_idx == 0 {
Err(IMStatusCode::UnsupportedAccess)?;
}
access_req.set_target_perms(target_perms);
if access_req.allow() {
Ok(())
} else {
Err(IMStatusCode::UnsupportedAccess)
}
}
pub(crate) fn check_event_access(
&self,
accessor: &Accessor,
path: GenericPath,
device_types: &[DeviceType],
event_id: EventId,
) -> Result<(), IMStatusCode> {
let mut access_req =
AccessReq::new_with_device_types(accessor, path, Access::READ, device_types);
let target_perms = self
.events
.iter()
.find(|event| event.id == event_id)
.map(|event| event.access)
.unwrap_or(Access::empty());
access_req.set_target_perms(target_perms);
if access_req.allow() {
Ok(())
} else {
Err(IMStatusCode::UnsupportedAccess)
}
}
pub fn attribute(&self, id: AttrId) -> Option<&Attribute> {
self.attributes().find(|attr| attr.id == id)
}
pub fn command(&self, id: CmdId) -> Option<&Command> {
self.commands().find(|cmd| cmd.id == id)
}
pub fn event(&self, id: EventId) -> Option<&Event> {
self.events().find(|event| event.id == id)
}
pub(crate) fn attributes(&self) -> impl Iterator<Item = &Attribute> + '_ {
self.attributes
.iter()
.filter(|attr| (self.with_attrs)(attr, self.revision, self.feature_map))
}
pub(crate) fn commands(&self) -> impl Iterator<Item = &Command> + '_ {
self.commands
.iter()
.filter(|cmd| (self.with_cmds)(cmd, self.revision, self.feature_map))
}
pub(crate) fn events(&self) -> impl Iterator<Item = &Event> + '_ {
self.events
.iter()
.filter(|event| (self.with_events)(event, self.revision, self.feature_map))
}
pub fn read<W: Reply>(&self, attr: &AttrDetails, mut writer: W) -> Result<(), Error> {
match attr.attr_id.try_into()? {
GlobalElements::GeneratedCmdList => {
self.encode_generated_command_ids(
Self::fetch_index(attr),
&W::TAG,
writer.writer(),
)?;
writer.complete()
}
GlobalElements::AcceptedCmdList => {
self.encode_accepted_command_ids(
Self::fetch_index(attr),
&W::TAG,
writer.writer(),
)?;
writer.complete()
}
GlobalElements::EventList => {
self.encode_event_ids(Self::fetch_index(attr), &W::TAG, &mut writer.writer())?;
writer.complete()
}
GlobalElements::AttributeList => {
self.encode_attribute_ids(Self::fetch_index(attr), &W::TAG, &mut writer.writer())?;
writer.complete()
}
GlobalElements::FeatureMap => {
debug!(
"Endpt(0x??)::Cluster(0x{:04x})::Attr::FeatureMap(0xfffc)::Read -> Ok({:08x})",
self.id, self.feature_map
);
writer.set(self.feature_map)
}
GlobalElements::ClusterRevision => {
debug!(
"Endpt(0x??)::Cluster(0x{:04x})::Attr::ClusterRevision(0xfffd)::Read -> Ok({})",
self.id, self.revision
);
writer.set(self.revision)
}
other => {
error!("Attribute {:?} not supported", other);
Err(ErrorCode::AttributeNotFound.into())
}
}
}
fn encode_attribute_ids<W: TLVWrite>(
&self,
index: Option<Option<usize>>,
tag: &TLVTag,
mut tw: W,
) -> Result<(), Error> {
debug!(
"Endpt(0x??)::Cluster(0x{:04x})::Attr::AttributeIDs(0xfffb)::Read{{{:?}}} -> Ok([",
self.id, index
);
if let Some(Some(index)) = index {
let attr = self
.attributes()
.nth(index)
.ok_or(ErrorCode::ConstraintError)?;
tw.u32(tag, attr.id)?;
debug!(" Attr: 0x{:02x},", attr.id);
} else {
tw.start_array(tag)?;
if index.is_none() {
for attr in self.attributes() {
tw.u32(&TLVTag::Anonymous, attr.id)?;
debug!(" Attr: 0x{:02x},", attr.id);
}
}
tw.end_container()?;
}
debug!("])");
Ok(())
}
fn encode_accepted_command_ids<W: TLVWrite>(
&self,
index: Option<Option<usize>>,
tag: &TLVTag,
mut tw: W,
) -> Result<(), Error> {
debug!(
"Endpt(0x??)::Cluster(0x{:04x})::Attr::AcceptedCmdIDs(0xfff9)::Read{{{:?}}} -> Ok([",
self.id, index
);
if let Some(Some(index)) = index {
let cmd = self
.commands()
.nth(index)
.ok_or(ErrorCode::ConstraintError)?;
tw.u32(tag, cmd.id)?;
debug!(" Cmd: 0x{:02x}, ", cmd.id);
} else {
tw.start_array(tag)?;
if index.is_none() {
for cmd in self.commands() {
tw.u32(&TLVTag::Anonymous, cmd.id)?;
debug!(" Cmd: 0x{:02x}, ", cmd.id);
}
}
tw.end_container()?;
}
debug!("])");
Ok(())
}
fn encode_generated_command_ids<W: TLVWrite>(
&self,
index: Option<Option<usize>>,
tag: &TLVTag,
mut tw: W,
) -> Result<(), Error> {
debug!(
"Endpt(0x??)::Cluster(0x{:04x})::Attr::GeneratedCmdIDs(0xfff8)::Read{{{:?}}} -> Ok(",
self.id, index
);
if !matches!(index, Some(Some(_))) {
tw.start_array(tag)?;
}
let mut count = 0;
let mut max_inserted_cmd = None;
while let Some(next_cmd) = self
.commands()
.filter_map(|cmd| cmd.resp_id)
.filter(|cmd| {
max_inserted_cmd
.map(|max_inserted| *cmd > max_inserted)
.unwrap_or(true)
})
.min()
{
if index == Some(Some(count)) {
tw.u32(tag, next_cmd)?;
debug!(" Cmd: 0x{:02x}, ", next_cmd);
} else if index.is_none() {
tw.u32(&TLVTag::Anonymous, next_cmd)?;
debug!(" Cmd: 0x{:02x}, ", next_cmd);
}
max_inserted_cmd = Some(next_cmd);
count += 1;
}
if let Some(Some(index)) = index {
if index >= count {
Err(ErrorCode::ConstraintError)?;
}
}
if !matches!(index, Some(Some(_))) {
tw.end_container()?;
}
debug!("])");
Ok(())
}
fn encode_event_ids<W: TLVWrite>(
&self,
index: Option<Option<usize>>,
tag: &TLVTag,
mut tw: W,
) -> Result<(), Error> {
debug!(
"Endpt(0x??)::Cluster(0x{:04x})::Attr::EventIDs(0xfffa)::Read{{{:?}}} -> Ok([",
self.id, index
);
if let Some(Some(index)) = index {
let event = self.events().nth(index).ok_or(ErrorCode::ConstraintError)?;
tw.u32(tag, event.id)?;
debug!(" Event: 0x{:02x}, ", event.id);
} else {
tw.start_array(tag)?;
if index.is_none() {
for event in self.events() {
tw.u32(&TLVTag::Anonymous, event.id)?;
debug!(" Event: 0x{:02x}, ", event.id);
}
}
tw.end_container()?;
}
debug!("])");
Ok(())
}
fn fetch_index(attr: &AttrDetails) -> Option<Option<usize>> {
attr.list_index
.clone()
.map(|li| li.into_option().map(|index| index as usize))
}
}
impl core::fmt::Display for Cluster<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "id: {}, ", self.id)?;
write!(f, "attrs [")?;
for (index, attr) in self.attributes().enumerate() {
if index > 0 {
write!(f, ", {}", attr)?;
} else {
write!(f, "{}", attr)?;
}
}
write!(f, "], cmds [")?;
for (index, cmd) in self.commands().enumerate() {
if index > 0 {
write!(f, ", {}", cmd)?;
} else {
write!(f, "{}", cmd)?;
}
}
write!(f, "]")
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Cluster<'_> {
fn format(&self, f: defmt::Formatter<'_>) {
defmt::write!(f, "id: {}, ", self.id);
defmt::write!(f, "attrs [");
for (index, attr) in self.attributes().enumerate() {
if index > 0 {
defmt::write!(f, ", ");
}
defmt::write!(f, "{}", attr);
}
defmt::write!(f, "], cmds [");
for (index, cmd) in self.commands().enumerate() {
if index > 0 {
defmt::write!(f, ", ");
}
defmt::write!(f, "{}", cmd);
}
defmt::write!(f, "]")
}
}
#[allow(unused_macros)]
#[macro_export]
macro_rules! clusters {
(sys
$(, sw_diag($($sw_opt:ident),* $(,)?))?
$(, time_sync($($ts_opt:ident),* $(,)?))?
; $($cluster:expr $(,)?)*
) => {
$crate::clusters!(
<$crate::dm::clusters::desc::DescHandler as $crate::dm::clusters::desc::ClusterHandler>::CLUSTER,
<$crate::dm::clusters::acl::AclHandler as $crate::dm::clusters::acl::ClusterHandler>::CLUSTER,
<$crate::dm::clusters::basic_info::BasicInfoHandler as $crate::dm::clusters::basic_info::ClusterHandler>::CLUSTER,
<$crate::dm::clusters::gen_comm::GenCommHandler as $crate::dm::clusters::gen_comm::ClusterHandler>::CLUSTER,
<$crate::dm::clusters::gen_diag::GenDiagHandler as $crate::dm::clusters::gen_diag::ClusterHandler>::CLUSTER,
$crate::dm::clusters::sw_diag::cluster(
$crate::__sw_diag_options!( $($($sw_opt),*)? )
),
$crate::dm::clusters::time_sync::cluster::<{
$crate::__time_sync_options!( $($($ts_opt),*)? ).bits()
}>(),
<$crate::dm::clusters::adm_comm::AdminCommHandler as $crate::dm::clusters::adm_comm::ClusterHandler>::CLUSTER,
<$crate::dm::clusters::noc::NocHandler as $crate::dm::clusters::noc::ClusterHandler>::CLUSTER,
<$crate::dm::clusters::grp_key_mgmt::GrpKeyMgmtHandler as $crate::dm::clusters::grp_key_mgmt::ClusterHandler>::CLUSTER,
$($cluster,)*
)
};
(eth
$(, sw_diag($($sw_opt:ident),* $(,)?))?
$(, time_sync($($ts_opt:ident),* $(,)?))?
; $($cluster:expr $(,)?)*
) => {
$crate::clusters!(
sys
$(, sw_diag($($sw_opt),*))?
$(, time_sync($($ts_opt),*))?
;
$crate::dm::clusters::net_comm::NetworkType::Ethernet.cluster(),
<$crate::dm::clusters::eth_diag::EthDiagHandler as $crate::dm::clusters::eth_diag::ClusterHandler>::CLUSTER,
$($cluster,)*
)
};
(thread
$(, sw_diag($($sw_opt:ident),* $(,)?))?
$(, time_sync($($ts_opt:ident),* $(,)?))?
; $($cluster:expr $(,)?)*
) => {
$crate::clusters!(
sys
$(, sw_diag($($sw_opt),*))?
$(, time_sync($($ts_opt),*))?
;
$crate::dm::clusters::net_comm::NetworkType::Thread.cluster(),
<$crate::dm::clusters::thread_diag::ThreadDiagHandler as $crate::dm::clusters::thread_diag::ClusterHandler>::CLUSTER,
$($cluster,)*
)
};
(wifi
$(, sw_diag($($sw_opt:ident),* $(,)?))?
$(, time_sync($($ts_opt:ident),* $(,)?))?
; $($cluster:expr $(,)?)*
) => {
$crate::clusters!(
sys
$(, sw_diag($($sw_opt),*))?
$(, time_sync($($ts_opt),*))?
;
$crate::dm::clusters::net_comm::NetworkType::Wifi.cluster(),
<$crate::dm::clusters::wifi_diag::WifiDiagHandler as $crate::dm::clusters::wifi_diag::ClusterHandler>::CLUSTER,
$($cluster,)*
)
};
($($cluster:expr $(,)?)*) => {
&[
$($cluster,)*
]
}
}
#[allow(unused_macros)]
#[doc(hidden)]
#[macro_export]
macro_rules! __sw_diag_options {
() => { $crate::dm::clusters::sw_diag::Options::empty() };
($($opt:ident),+ $(,)?) => {
$crate::dm::clusters::sw_diag::Options::empty()
$(.union($crate::__sw_diag_option_bit!($opt)))+
};
}
#[allow(unused_macros)]
#[doc(hidden)]
#[macro_export]
macro_rules! __sw_diag_option_bit {
(heap) => {
$crate::dm::clusters::sw_diag::Options::HEAP
};
(watermarks) => {
$crate::dm::clusters::sw_diag::Options::WATERMARKS
};
(thread) => {
$crate::dm::clusters::sw_diag::Options::THREAD
};
}
#[allow(unused_macros)]
#[doc(hidden)]
#[macro_export]
macro_rules! __time_sync_options {
() => { $crate::dm::clusters::time_sync::Options::empty() };
($($opt:ident),+ $(,)?) => {
$crate::dm::clusters::time_sync::Options::empty()
$(.union($crate::__time_sync_option_bit!($opt)))+
};
}
#[allow(unused_macros)]
#[doc(hidden)]
#[macro_export]
macro_rules! __time_sync_option_bit {
(time_zone) => {
$crate::dm::clusters::time_sync::Options::TIME_ZONE
};
(ntp_client) => {
$crate::dm::clusters::time_sync::Options::NTP_CLIENT
};
(ntp_server) => {
$crate::dm::clusters::time_sync::Options::NTP_SERVER
};
(time_sync_client) => {
$crate::dm::clusters::time_sync::Options::TIME_SYNC_CLIENT
};
}
#[allow(unused_macros)]
#[macro_export]
macro_rules! with {
() => {
|_, _, _| false
};
(all) => {
|_, _, _| true
};
(required) => {
|attr, _, _| !attr.quality.contains($crate::dm::Quality::OPTIONAL)
};
(required; $($id:path $(|)?)*) => {
#[allow(clippy::collapsible_match)]
|attr, _, _| {
if !attr.quality.contains($crate::dm::Quality::OPTIONAL) {
true
} else if let Ok(l) = attr.id.try_into() {
#[allow(unreachable_patterns)]
match l {
$($id => true,)*
_ => false,
}
} else {
false
}
}
};
(system) => {
|attr, _, _| attr.is_system()
};
(system; $($id:path $(|)?)*) => {
#[allow(clippy::collapsible_match)]
|attr, _, _| {
if attr.is_system() {
true
} else if let Ok(l) = attr.id.try_into() {
#[allow(unreachable_patterns)]
match l {
$($id => true,)*
_ => false,
}
} else {
false
}
}
};
($id0:path $(| $id:path $(|)?)*) => {
#[allow(clippy::collapsible_match)]
|leaf, _, _| {
if let Ok(l) = leaf.id.try_into() {
#[allow(unreachable_patterns)]
match l {
$id0 => true,
$($id => true,)*
_ => false,
}
} else {
false
}
}
};
}
#[allow(unused_macros)]
#[macro_export]
macro_rules! except {
() => {
|_, _, _| true
};
(all) => {
|_, _, _| false
};
($id0:path $(| $id:path $(|)?)*) => {
#[allow(clippy::collapsible_match)]
|leaf, _, _| {
if let Ok(l) = leaf.id.try_into() {
#[allow(unreachable_patterns)]
match l {
$id0 => false,
$($id => false,)*
_ => true,
}
} else {
false
}
}
};
}