first commit
This commit is contained in:
23
crates/zcl/Cargo.toml
Normal file
23
crates/zcl/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "zcl"
|
||||
version = "0.1.0"
|
||||
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
categories.workspace = true
|
||||
keywords.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.5.0"
|
||||
hex = "0.4.3"
|
||||
hue = { version = "0.1.0", path = "../hue" }
|
||||
packed_struct = "0.10.1"
|
||||
thiserror = "2.0.11"
|
||||
351
crates/zcl/src/attr.rs
Normal file
351
crates/zcl/src/attr.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
use std::io::Read;
|
||||
use std::{fmt::Debug, io::Cursor};
|
||||
|
||||
use byteorder::{LE, ReadBytesExt};
|
||||
use packed_struct::prelude::*;
|
||||
|
||||
use crate::error::{ZclError, ZclResult};
|
||||
|
||||
#[derive(PrimitiveEnum_u8, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ZclProfileCommand {
|
||||
ReadAttribute = 0x00,
|
||||
ReadAttributeRsp = 0x01,
|
||||
WriteAttribute = 0x02,
|
||||
WriteAttributeRsp = 0x03,
|
||||
}
|
||||
|
||||
#[derive(PrimitiveEnum_u8, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ZclCommand {
|
||||
ReadAttrib = 0x00,
|
||||
ReadAttribResp = 0x01,
|
||||
WriteAttrib = 0x02,
|
||||
WriteAttribUndiv = 0x03,
|
||||
WriteAttribResp = 0x04,
|
||||
WriteAttribNoResp = 0x05,
|
||||
ConfigReport = 0x06,
|
||||
ConfigReportResp = 0x07,
|
||||
ReadReportCfg = 0x08,
|
||||
ReadReportCfgResp = 0x09,
|
||||
ReportAttrib = 0x0a,
|
||||
DefaultResp = 0x0b,
|
||||
DiscAttrib = 0x0c,
|
||||
DiscAttribResp = 0x0d,
|
||||
ReadAttribStruct = 0x0e,
|
||||
WriteAttribStruct = 0x0f,
|
||||
WriteAttribStructResp = 0x10,
|
||||
DiscoverCommandsReceived = 0x11,
|
||||
DiscoverCommandsReceivedRes = 0x12,
|
||||
DiscoverCommandsGenerated = 0x13,
|
||||
DiscoverCommandsGeneratedRes = 0x14,
|
||||
DiscoverAttrExt = 0x15,
|
||||
DiscoverAttrExtRes = 0x16,
|
||||
}
|
||||
|
||||
#[derive(PrimitiveEnum_u8, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ZclDataType {
|
||||
/** Null data type */
|
||||
Null = 0x00,
|
||||
|
||||
/** 8-bit value data type */
|
||||
Zcl8bit = 0x08,
|
||||
|
||||
/** 16-bit value data type */
|
||||
Zcl16bit = 0x09,
|
||||
|
||||
/** 32-bit value data type */
|
||||
Zcl32bit = 0x0b,
|
||||
|
||||
/** Boolean data type */
|
||||
ZclBool = 0x10,
|
||||
|
||||
/** 8-bit bitmap data type */
|
||||
Zcl8bitmap = 0x18,
|
||||
|
||||
/** 16-bit bitmap data type */
|
||||
Zcl16bitmap = 0x19,
|
||||
|
||||
/** 32-bit bitmap data type */
|
||||
Zcl32bitmap = 0x1b,
|
||||
|
||||
/** 40-bit bitmap data type */
|
||||
Zcl40bitmap = 0x1c,
|
||||
|
||||
/** 48-bit bitmap data type */
|
||||
Zcl48bitmap = 0x1d,
|
||||
|
||||
/** 56-bit bitmap data type */
|
||||
Zcl56bitmap = 0x1e,
|
||||
|
||||
/** 64-bit bitmap data type */
|
||||
Zcl64bitmap = 0x1f,
|
||||
|
||||
/** Unsigned 8-bit value data type */
|
||||
ZclU8 = 0x20,
|
||||
|
||||
/** Unsigned 16-bit value data type */
|
||||
ZclU16 = 0x21,
|
||||
|
||||
/** Unsigned 32-bit value data type */
|
||||
ZclU32 = 0x23,
|
||||
|
||||
/** Unsigned 16-bit value data type */
|
||||
ZclI16 = 0x29,
|
||||
|
||||
/** Unsigned 8-bit value data type */
|
||||
ZclE8 = 0x30,
|
||||
|
||||
/** Byte array data type */
|
||||
ZclBytearray = 0x41,
|
||||
|
||||
/** Charactery string (array) data type */
|
||||
ZclCharstring = 0x42,
|
||||
|
||||
/** IEEE address (U64) type */
|
||||
ZclIeeeaddr = 0xf0,
|
||||
|
||||
/** 128-bit security key */
|
||||
ZclSecurityKey = 0xf1,
|
||||
|
||||
/** Invalid data type */
|
||||
ZclInvalid = 0xff,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZclReadAttr {
|
||||
pub attr: Vec<u16>,
|
||||
}
|
||||
|
||||
impl ZclReadAttr {
|
||||
pub fn parse(data: &[u8]) -> ZclResult<Self> {
|
||||
if data.len() % 2 != 0 {
|
||||
return Err(ZclError::PackedStructError(PackingError::InvalidValue));
|
||||
}
|
||||
|
||||
let mut attr = vec![];
|
||||
|
||||
data.chunks(2)
|
||||
.for_each(|v| attr.push(u16::from_le_bytes([v[0], v[1]])));
|
||||
|
||||
Ok(Self { attr })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ZclAttrValue {
|
||||
Null,
|
||||
X8(i8),
|
||||
X16(i16),
|
||||
X32(i32),
|
||||
Bool(bool),
|
||||
B8(u8),
|
||||
B16(u16),
|
||||
B32(u32),
|
||||
B40(u64),
|
||||
B48(u64),
|
||||
B56(u64),
|
||||
B64(u64),
|
||||
U8(u8),
|
||||
U16(u16),
|
||||
U32(u32),
|
||||
I16(i16),
|
||||
E8(u8),
|
||||
Bytes(Vec<u8>),
|
||||
String(String),
|
||||
IeeeAddr(Vec<u8>),
|
||||
SecurityKey([u8; 16]),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl Debug for ZclAttrValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Null => write!(f, "Null"),
|
||||
Self::X8(val) => write!(f, "x8:{val}"),
|
||||
Self::X16(val) => write!(f, "x16:{val}"),
|
||||
Self::X32(val) => write!(f, "x32:{val}"),
|
||||
Self::Bool(val) => write!(f, "bool:{val}"),
|
||||
Self::B8(val) => write!(f, "b8:{val:02X}"),
|
||||
Self::B16(val) => write!(f, "b16:{val:04X}"),
|
||||
Self::B32(val) => write!(f, "b32:{val:08X}"),
|
||||
Self::B40(val) => write!(f, "b40:{val:010X}"),
|
||||
Self::B48(val) => write!(f, "b48:{val:012X}"),
|
||||
Self::B56(val) => write!(f, "b56:{val:014X}"),
|
||||
Self::B64(val) => write!(f, "b64:{val:016X}"),
|
||||
Self::U8(val) => write!(f, "u8:{val:02X}"),
|
||||
Self::U16(val) => write!(f, "u16:{val:04X}"),
|
||||
Self::U32(val) => write!(f, "u32:{val:08X}"),
|
||||
Self::I16(val) => write!(f, "i16:{val:04X}"),
|
||||
Self::E8(val) => write!(f, "e8:{val:02X}"),
|
||||
Self::Bytes(val) => write!(f, "hex:{}", hex::encode(val)),
|
||||
Self::String(val) => write!(f, "str:{val}"),
|
||||
Self::IeeeAddr(val) => write!(f, "ieeeaddr {}", hex::encode(val)),
|
||||
Self::SecurityKey(val) => write!(f, "seckey {}", hex::encode(val)),
|
||||
Self::Unsupported => write!(f, "Unsupported"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for ZclAttr {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:04x}:{:?}", self.key, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZclAttr {
|
||||
pub key: u16,
|
||||
pub value: ZclAttrValue,
|
||||
}
|
||||
|
||||
impl ZclAttr {
|
||||
fn from_reader(rdr: &mut impl Read, check_status: bool) -> ZclResult<Self> {
|
||||
let key = rdr.read_u16::<LE>()?;
|
||||
|
||||
if check_status {
|
||||
let status = rdr.read_u8()?;
|
||||
if status != 0 {
|
||||
return Ok(Self {
|
||||
key,
|
||||
value: ZclAttrValue::Unsupported,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let zdt = rdr.read_u8()?;
|
||||
let dtype = ZclDataType::from_primitive(zdt).ok_or(ZclError::UnsupportedAttrType(zdt))?;
|
||||
|
||||
let value = match dtype {
|
||||
ZclDataType::Null => ZclAttrValue::Null,
|
||||
ZclDataType::Zcl8bit => ZclAttrValue::X8(rdr.read_i8()?),
|
||||
ZclDataType::Zcl16bit => ZclAttrValue::X16(rdr.read_i16::<LE>()?),
|
||||
ZclDataType::Zcl32bit => ZclAttrValue::X32(rdr.read_i32::<LE>()?),
|
||||
ZclDataType::ZclBool => ZclAttrValue::Bool(rdr.read_u8()? != 0),
|
||||
ZclDataType::Zcl8bitmap => ZclAttrValue::B8(rdr.read_u8()?),
|
||||
ZclDataType::Zcl16bitmap => ZclAttrValue::B16(rdr.read_u16::<LE>()?),
|
||||
ZclDataType::Zcl32bitmap => ZclAttrValue::B32(rdr.read_u32::<LE>()?),
|
||||
ZclDataType::Zcl40bitmap => todo!(),
|
||||
ZclDataType::Zcl48bitmap => todo!(),
|
||||
ZclDataType::Zcl56bitmap => todo!(),
|
||||
ZclDataType::Zcl64bitmap => ZclAttrValue::B64(rdr.read_u64::<LE>()?),
|
||||
ZclDataType::ZclU8 => ZclAttrValue::U8(rdr.read_u8()?),
|
||||
ZclDataType::ZclU16 => ZclAttrValue::U16(rdr.read_u16::<LE>()?),
|
||||
ZclDataType::ZclU32 => ZclAttrValue::U32(rdr.read_u32::<LE>()?),
|
||||
ZclDataType::ZclI16 => ZclAttrValue::I16(rdr.read_i16::<LE>()?),
|
||||
ZclDataType::ZclE8 => ZclAttrValue::E8(rdr.read_u8()?),
|
||||
ZclDataType::ZclBytearray => {
|
||||
let len = rdr.read_u8()?;
|
||||
let mut buf = vec![0; len as usize];
|
||||
rdr.read_exact(&mut buf)?;
|
||||
ZclAttrValue::Bytes(buf)
|
||||
}
|
||||
ZclDataType::ZclCharstring => {
|
||||
let len = rdr.read_u8()?;
|
||||
let mut buf = vec![0; len as usize];
|
||||
rdr.read_exact(&mut buf)?;
|
||||
ZclAttrValue::String(String::from_utf8(buf)?)
|
||||
}
|
||||
ZclDataType::ZclIeeeaddr => todo!(),
|
||||
ZclDataType::ZclSecurityKey => {
|
||||
let mut buf = [0; 16];
|
||||
rdr.read_exact(&mut buf)?;
|
||||
ZclAttrValue::SecurityKey(buf)
|
||||
}
|
||||
ZclDataType::ZclInvalid => todo!(),
|
||||
};
|
||||
|
||||
Ok(Self { key, value })
|
||||
}
|
||||
|
||||
pub fn readattr_from_reader(rdr: &mut impl Read) -> ZclResult<Self> {
|
||||
Self::from_reader(rdr, true)
|
||||
}
|
||||
|
||||
pub fn writeattr_from_reader(rdr: &mut impl Read) -> ZclResult<Self> {
|
||||
Self::from_reader(rdr, false)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZclReadAttrResp {
|
||||
pub attr: Vec<ZclAttr>,
|
||||
}
|
||||
|
||||
impl ZclReadAttrResp {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn parse(data: &[u8]) -> ZclResult<Self> {
|
||||
let mut attr = vec![];
|
||||
|
||||
let mut cur = Cursor::new(data);
|
||||
while (cur.position() as usize) < data.len() {
|
||||
attr.push(ZclAttr::readattr_from_reader(&mut cur)?);
|
||||
}
|
||||
|
||||
Ok(Self { attr })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZclWriteAttr {
|
||||
pub attr: Vec<ZclAttr>,
|
||||
}
|
||||
|
||||
impl ZclWriteAttr {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn parse(data: &[u8]) -> ZclResult<Self> {
|
||||
let mut attr = vec![];
|
||||
|
||||
let mut cur = Cursor::new(data);
|
||||
while (cur.position() as usize) < data.len() {
|
||||
attr.push(ZclAttr::writeattr_from_reader(&mut cur)?);
|
||||
}
|
||||
|
||||
Ok(Self { attr })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZclReportAttr {
|
||||
pub attr: Vec<ZclAttr>,
|
||||
}
|
||||
|
||||
impl ZclReportAttr {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn parse(data: &[u8]) -> ZclResult<Self> {
|
||||
let mut attr = vec![];
|
||||
|
||||
let mut cur = Cursor::new(data);
|
||||
while (cur.position() as usize) < data.len() {
|
||||
attr.push(ZclAttr::writeattr_from_reader(&mut cur)?);
|
||||
}
|
||||
|
||||
Ok(Self { attr })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZclDefaultResp {
|
||||
pub cmd: u8,
|
||||
pub stat: u8,
|
||||
}
|
||||
|
||||
impl ZclDefaultResp {
|
||||
pub const fn parse(data: &[u8]) -> ZclResult<Self> {
|
||||
Ok(Self {
|
||||
cmd: data[0],
|
||||
stat: data[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ZclWriteAttrResp {
|
||||
pub attr: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ZclWriteAttrResp {
|
||||
pub fn parse(data: &[u8]) -> ZclResult<Self> {
|
||||
Ok(Self {
|
||||
attr: data.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
35
crates/zcl/src/cluster/colorctrl.rs
Normal file
35
crates/zcl/src/cluster/colorctrl.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::frame::{ZclFrame, ZclFrameDirection};
|
||||
|
||||
#[must_use]
|
||||
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
|
||||
if frame.manufacturer_specific() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if frame.flags.direction != ZclFrameDirection::ClientToServer {
|
||||
return None;
|
||||
}
|
||||
|
||||
match frame.cmd {
|
||||
0x00 => Some("MoveToHue".to_string()),
|
||||
0x01 => Some("MoveHue".to_string()),
|
||||
0x02 => Some("StepHue".to_string()),
|
||||
0x03 => Some("MoveToSaturation".to_string()),
|
||||
0x04 => Some("MoveSaturation".to_string()),
|
||||
0x05 => Some("StepSaturation".to_string()),
|
||||
0x06 => Some("MoveToHueAndSaturation".to_string()),
|
||||
0x07 => Some("MoveToColor".to_string()),
|
||||
0x08 => Some("MoveColor".to_string()),
|
||||
0x09 => Some("StepColor".to_string()),
|
||||
0x0a => Some("MoveToColorTemp".to_string()),
|
||||
0x40 => Some("EnhancedMoveToHue".to_string()),
|
||||
0x41 => Some("EnhancedMoveHue".to_string()),
|
||||
0x42 => Some("EnhancedStepHue".to_string()),
|
||||
0x43 => Some("EnhancedMoveToHueAndSaturation".to_string()),
|
||||
0x44 => Some("ColorLoopSet".to_string()),
|
||||
0x47 => Some("StopMoveStep".to_string()),
|
||||
0x4b => Some("MoveColorTemp".to_string()),
|
||||
0x4c => Some("StepColorTemp".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
20
crates/zcl/src/cluster/commissioning.rs
Normal file
20
crates/zcl/src/cluster/commissioning.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::error::ZclResult;
|
||||
use crate::frame::ZclFrame;
|
||||
use hue::zigbee::HueEntFrame;
|
||||
|
||||
pub fn describe(frame: &ZclFrame, data: &[u8]) -> ZclResult<Option<String>> {
|
||||
if !frame.cluster_specific() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match frame.cmd {
|
||||
0x00 => Ok(Some("ScanRequest".to_string())),
|
||||
0x02 => {
|
||||
let (data, csum) = data.split_at(data.len() - 4);
|
||||
let csum = u32::from_be_bytes([csum[0], csum[1], csum[2], csum[3]]);
|
||||
let hes = HueEntFrame::parse(data)?;
|
||||
Ok(Some(format!("{hes:x?} [PROXY, {csum:08x}]")))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
13
crates/zcl/src/cluster/effects.rs
Normal file
13
crates/zcl/src/cluster/effects.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use crate::frame::{ZclFrame, ZclFrameDirection};
|
||||
|
||||
#[must_use]
|
||||
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
|
||||
if frame.flags.direction == ZclFrameDirection::ClientToServer {
|
||||
match frame.cmd {
|
||||
0x40 => Some("Trigger".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
18
crates/zcl/src/cluster/groups.rs
Normal file
18
crates/zcl/src/cluster/groups.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use crate::frame::{ZclFrame, ZclFrameDirection};
|
||||
|
||||
#[must_use]
|
||||
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
|
||||
if frame.flags.direction == ZclFrameDirection::ClientToServer {
|
||||
match frame.cmd {
|
||||
0x00 => Some("Add".to_string()),
|
||||
0x02 => Some("GetMembership".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
match frame.cmd {
|
||||
0x00 => Some("AddResp".to_string()),
|
||||
0x02 => Some("GetMembershipResp".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
26
crates/zcl/src/cluster/hue_fc01.rs
Normal file
26
crates/zcl/src/cluster/hue_fc01.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use packed_struct::PackedStructSlice;
|
||||
|
||||
use crate::error::ZclResult;
|
||||
use crate::frame::ZclFrame;
|
||||
use hue::zigbee::{HueEntFrame, HueEntSegmentConfig, HueEntSegmentLayout, HueEntStop};
|
||||
|
||||
pub fn describe(frame: &ZclFrame, data: &[u8]) -> ZclResult<Option<String>> {
|
||||
if !frame.cluster_specific() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match frame.cmd {
|
||||
1 => Ok(Some(format!("{:x?}", HueEntFrame::parse(data)?))),
|
||||
3 => Ok(Some(format!("{:x?}", HueEntStop::unpack_from_slice(data)?))),
|
||||
4 => {
|
||||
let res = if frame.c2s() && data.len() == 1 {
|
||||
"HueEntSegmentLayoutReq".to_string()
|
||||
} else {
|
||||
format!("{:x?}", HueEntSegmentLayout::parse(data)?)
|
||||
};
|
||||
Ok(Some(res))
|
||||
}
|
||||
7 => Ok(Some(format!("{:x?}", HueEntSegmentConfig::parse(data)?))),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
18
crates/zcl/src/cluster/hue_fc03.rs
Normal file
18
crates/zcl/src/cluster/hue_fc03.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use hue::zigbee::Flags;
|
||||
|
||||
use crate::error::ZclResult;
|
||||
use crate::frame::ZclFrame;
|
||||
|
||||
pub fn describe(frame: &ZclFrame, data: &[u8]) -> ZclResult<Option<String>> {
|
||||
if !frame.cluster_specific() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match frame.cmd {
|
||||
0x00 => {
|
||||
let zflags = Flags::from_bits(u16::from(data[0]) | (u16::from(data[1]) << 8)).unwrap();
|
||||
Ok(Some(format!("{:?} {}", zflags, hex::encode(&data[2..]))))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
24
crates/zcl/src/cluster/levelctrl.rs
Normal file
24
crates/zcl/src/cluster/levelctrl.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::frame::{ZclFrame, ZclFrameDirection};
|
||||
|
||||
#[must_use]
|
||||
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
|
||||
if frame.manufacturer_specific() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if frame.flags.direction != ZclFrameDirection::ClientToServer {
|
||||
return None;
|
||||
}
|
||||
|
||||
match frame.cmd {
|
||||
0x00 => Some("MoveToLevel".to_string()),
|
||||
0x01 => Some("Move".to_string()),
|
||||
0x02 => Some("Step".to_string()),
|
||||
0x03 => Some("Stop".to_string()),
|
||||
0x04 => Some("MoveToLevelWithOnOff".to_string()),
|
||||
0x05 => Some("MoveWithOnOff".to_string()),
|
||||
0x06 => Some("StepWithOnOff".to_string()),
|
||||
0x07 => Some("StopWithOnOff".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
10
crates/zcl/src/cluster/mod.rs
Normal file
10
crates/zcl/src/cluster/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub mod colorctrl;
|
||||
pub mod commissioning;
|
||||
pub mod effects;
|
||||
pub mod groups;
|
||||
pub mod hue_fc01;
|
||||
pub mod hue_fc03;
|
||||
pub mod levelctrl;
|
||||
pub mod onoff;
|
||||
pub mod scenes;
|
||||
pub mod standard;
|
||||
19
crates/zcl/src/cluster/onoff.rs
Normal file
19
crates/zcl/src/cluster/onoff.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::frame::{ZclFrame, ZclFrameDirection};
|
||||
|
||||
#[must_use]
|
||||
pub fn describe(frame: &ZclFrame, _data: &[u8]) -> Option<String> {
|
||||
if frame.manufacturer_specific() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if frame.flags.direction != ZclFrameDirection::ClientToServer {
|
||||
return None;
|
||||
}
|
||||
|
||||
match frame.cmd {
|
||||
0x00 => Some("Off".to_string()),
|
||||
0x01 => Some("On".to_string()),
|
||||
0x40 => Some("OffWithEffect".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
39
crates/zcl/src/cluster/scenes.rs
Normal file
39
crates/zcl/src/cluster/scenes.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
#![allow(clippy::collapsible_else_if)]
|
||||
|
||||
use hue::zigbee::Flags;
|
||||
|
||||
use crate::frame::{ZclFrame, ZclFrameDirection};
|
||||
|
||||
#[must_use]
|
||||
pub fn describe(frame: &ZclFrame, data: &[u8]) -> Option<String> {
|
||||
if frame.manufacturer_specific() {
|
||||
if frame.flags.direction == ZclFrameDirection::ClientToServer {
|
||||
match frame.cmd {
|
||||
0x02 => Some(format!(
|
||||
"SetComposite {:?}",
|
||||
Flags::from_bits(u16::from(data[3]) | (u16::from(data[4]) << 8)).unwrap()
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
match frame.cmd {
|
||||
0x02 => Some("SetCompositeOk".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if frame.flags.direction == ZclFrameDirection::ClientToServer {
|
||||
match frame.cmd {
|
||||
0x02 => Some("Remove".to_string()),
|
||||
0x05 => Some("Recall".to_string()),
|
||||
0x06 => Some("GetMembership".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
match frame.cmd {
|
||||
0x06 => Some("GetMembershipResp".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
crates/zcl/src/cluster/standard.rs
Normal file
41
crates/zcl/src/cluster/standard.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use packed_struct::PrimitiveEnum;
|
||||
|
||||
use crate::attr::{
|
||||
ZclCommand, ZclReadAttr, ZclReadAttrResp, ZclReportAttr, ZclWriteAttr, ZclWriteAttrResp,
|
||||
};
|
||||
use crate::error::ZclResult;
|
||||
use crate::frame::ZclFrame;
|
||||
|
||||
pub fn describe(frame: &ZclFrame, data: &[u8]) -> ZclResult<Option<String>> {
|
||||
let cmd = ZclCommand::from_primitive(frame.cmd);
|
||||
let desc = match cmd {
|
||||
Some(ZclCommand::ReadAttrib) => {
|
||||
let req = ZclReadAttr::parse(data)?;
|
||||
Some(format!("Attr rd -> {:04x?}", req.attr))
|
||||
}
|
||||
Some(ZclCommand::ReadAttribResp) => {
|
||||
let req = ZclReadAttrResp::parse(data)?;
|
||||
Some(format!("Attr rd <- {:?}", req.attr))
|
||||
}
|
||||
Some(ZclCommand::WriteAttrib) => {
|
||||
let req = ZclWriteAttr::parse(data)?;
|
||||
Some(format!("Attr wr -> {:?}", req.attr))
|
||||
}
|
||||
Some(ZclCommand::WriteAttribResp) => {
|
||||
let req = ZclWriteAttrResp::parse(data)?;
|
||||
Some(format!("Attr wr <- {:02x?}", req.attr))
|
||||
}
|
||||
Some(ZclCommand::ReportAttrib) => {
|
||||
let req = ZclReportAttr::parse(data)?;
|
||||
Some(format!("Attr rp <- {:02x?}", req.attr))
|
||||
}
|
||||
Some(ZclCommand::DefaultResp) => {
|
||||
/* let req = ZclDefaultResp::parse(data)?; */
|
||||
/* format!("Attr dr <- {:02x} {:02x}", req.cmd, req.stat) */
|
||||
return Ok(Some(String::new()));
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(desc)
|
||||
}
|
||||
22
crates/zcl/src/error.rs
Normal file
22
crates/zcl/src/error.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ZclError {
|
||||
/* mapped errors */
|
||||
#[error(transparent)]
|
||||
FromUtf8Error(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[error(transparent)]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
HueError(#[from] hue::error::HueError),
|
||||
|
||||
#[error("Attribute type 0x{0:02x} not supported")]
|
||||
UnsupportedAttrType(u8),
|
||||
|
||||
#[error(transparent)]
|
||||
PackedStructError(#[from] packed_struct::PackingError),
|
||||
}
|
||||
|
||||
pub type ZclResult<T> = Result<T, ZclError>;
|
||||
100
crates/zcl/src/frame.rs
Normal file
100
crates/zcl/src/frame.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::fmt::Debug;
|
||||
use std::io::Read;
|
||||
|
||||
use byteorder::{BigEndian as BE, ReadBytesExt};
|
||||
use packed_struct::prelude::*;
|
||||
|
||||
use crate::error::ZclResult;
|
||||
|
||||
#[derive(PrimitiveEnum_u8, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ZclFrameType {
|
||||
ProfileWide = 0x00,
|
||||
ClusterSpecific = 0x01,
|
||||
}
|
||||
|
||||
#[derive(PrimitiveEnum_u8, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ZclFrameDirection {
|
||||
ClientToServer = 0x00,
|
||||
ServerToClient = 0x01,
|
||||
}
|
||||
|
||||
#[derive(PackedStruct, Clone, Copy)]
|
||||
#[packed_struct(size_bytes = "1", bit_numbering = "lsb0")]
|
||||
pub struct ZclFrameFlags {
|
||||
#[packed_field(bits = "0..2", ty = "enum")]
|
||||
pub frame_type: ZclFrameType,
|
||||
|
||||
#[packed_field(bits = "2")]
|
||||
pub manufacturer_specific: bool,
|
||||
|
||||
#[packed_field(bits = "3", ty = "enum")]
|
||||
pub direction: ZclFrameDirection,
|
||||
|
||||
#[packed_field(bits = "4")]
|
||||
pub disable_default_response: bool,
|
||||
}
|
||||
|
||||
impl Debug for ZclFrameFlags {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let ft = match self.frame_type {
|
||||
ZclFrameType::ProfileWide => "PW",
|
||||
ZclFrameType::ClusterSpecific => "CS",
|
||||
};
|
||||
let dir = match self.direction {
|
||||
ZclFrameDirection::ClientToServer => "C2S",
|
||||
ZclFrameDirection::ServerToClient => "S2C",
|
||||
};
|
||||
write!(f, "[ ")?;
|
||||
write!(f, "ft:{ft}, ")?;
|
||||
write!(f, "ms:{}, ", u8::from(self.manufacturer_specific))?;
|
||||
write!(f, "dir:{dir}, ")?;
|
||||
write!(f, "ddr:{}", u8::from(self.disable_default_response))?;
|
||||
write!(f, " ]")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ZclFrame {
|
||||
pub flags: ZclFrameFlags,
|
||||
pub mfcode: Option<u16>,
|
||||
pub seqnr: u8,
|
||||
pub cmd: u8,
|
||||
}
|
||||
|
||||
impl ZclFrame {
|
||||
pub fn parse(data: &mut impl Read) -> ZclResult<Self> {
|
||||
let flags = ZclFrameFlags::unpack(&[data.read_u8()?])?;
|
||||
|
||||
let mfcode = if flags.manufacturer_specific {
|
||||
Some(data.read_u16::<BE>()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let seqnr = data.read_u8()?;
|
||||
let cmd = data.read_u8()?;
|
||||
|
||||
Ok(Self {
|
||||
flags,
|
||||
mfcode,
|
||||
seqnr,
|
||||
cmd,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn c2s(&self) -> bool {
|
||||
self.flags.direction == ZclFrameDirection::ClientToServer
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn cluster_specific(&self) -> bool {
|
||||
self.flags.frame_type == ZclFrameType::ClusterSpecific
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn manufacturer_specific(&self) -> bool {
|
||||
self.flags.manufacturer_specific
|
||||
}
|
||||
}
|
||||
4
crates/zcl/src/lib.rs
Normal file
4
crates/zcl/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod attr;
|
||||
pub mod cluster;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
Reference in New Issue
Block a user