212 lines
5.7 KiB
Rust
212 lines
5.7 KiB
Rust
#![allow(clippy::cast_possible_truncation, clippy::match_same_arms)]
|
|
|
|
use std::fmt::Debug;
|
|
use std::io::{Cursor, stdin};
|
|
|
|
use clap::Parser;
|
|
use serde::Deserialize;
|
|
|
|
use bifrost::error::ApiResult;
|
|
use zcl::cluster;
|
|
use zcl::error::ZclResult;
|
|
use zcl::frame::{ZclFrame, ZclFrameDirection, ZclFrameType};
|
|
|
|
#[macro_use]
|
|
extern crate log;
|
|
|
|
pub fn f64_str<'de, D>(deserializer: D) -> Result<f64, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
use std::str::FromStr;
|
|
let s = String::deserialize(deserializer)?;
|
|
f64::from_str(&s).map_err(serde::de::Error::custom)
|
|
}
|
|
|
|
pub fn u16_hex<'de, D>(deserializer: D) -> Result<u16, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
Ok(u16::from_be(
|
|
u16::from_str_radix(&s, 16).map_err(serde::de::Error::custom)?,
|
|
))
|
|
}
|
|
|
|
pub fn u16_hex_opt<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let opt = Option::<String>::deserialize(deserializer)?;
|
|
if let Some(s) = opt {
|
|
Ok(Some(u16::from_be(
|
|
u16::from_str_radix(&s, 16).map_err(serde::de::Error::custom)?,
|
|
)))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub fn vec_hex_opt<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let opt = Option::<String>::deserialize(deserializer)?;
|
|
if let Some(s) = opt {
|
|
Ok(hex::decode(s).map_err(serde::de::Error::custom)?)
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct Record {
|
|
/* pub src_mac: String, */
|
|
/* pub cmd: Option<String>, */
|
|
#[serde(deserialize_with = "f64_str")]
|
|
pub time: f64,
|
|
|
|
pub index: u64,
|
|
|
|
#[serde(deserialize_with = "u16_hex")]
|
|
pub src: u16,
|
|
|
|
#[serde(deserialize_with = "u16_hex")]
|
|
pub dst: u16,
|
|
|
|
#[serde(deserialize_with = "u16_hex")]
|
|
pub cluster: u16,
|
|
|
|
#[serde(deserialize_with = "vec_hex_opt")]
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
fn parse(rec: &Record, no_index: bool) -> ZclResult<()> {
|
|
if rec.data.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut cur = Cursor::new(&rec.data);
|
|
let frame = ZclFrame::parse(&mut cur)?;
|
|
|
|
let data = &rec.data[cur.position() as usize..];
|
|
|
|
let src = hex::encode(rec.src.to_be_bytes());
|
|
let dst = hex::encode(rec.dst.to_be_bytes());
|
|
let flags = frame.flags;
|
|
let cmd = frame.cmd;
|
|
let cls = rec.cluster;
|
|
let index = if no_index { 0 } else { rec.index };
|
|
|
|
let describe = |cat: &str, desc: ZclResult<Option<String>>| {
|
|
let dir = if flags.direction == ZclFrameDirection::ClientToServer {
|
|
" :>"
|
|
} else {
|
|
"<: "
|
|
};
|
|
|
|
match desc {
|
|
Ok(Some(desc)) => {
|
|
if desc.is_empty() {
|
|
return;
|
|
}
|
|
|
|
info!(
|
|
"[{index:6}] [{src} -> {dst}] {flags:?} [{cls:04x}] {cmd:02x} {dir} {cat}{desc} {}",
|
|
hex::encode(data)
|
|
);
|
|
}
|
|
Ok(None) => {
|
|
warn!(
|
|
"[{index:6}] [{src} -> {dst}] {flags:?} [{cls:04x}] {cmd:02x} {dir} {cat}Unknown {}",
|
|
hex::encode(data)
|
|
);
|
|
}
|
|
Err(err) => {
|
|
error!(
|
|
"[{index:6}] [{src} -> {dst}] {flags:?} [{cls:04x}] {cmd:02x} {dir} FAILED {}: {err}",
|
|
hex::encode(data)
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
if frame.flags.frame_type == ZclFrameType::ProfileWide {
|
|
describe("", cluster::standard::describe(&frame, data));
|
|
return Ok(());
|
|
}
|
|
|
|
match rec.cluster {
|
|
0x0003 => describe("Effect:", Ok(cluster::effects::describe(&frame, data))),
|
|
0x0004 => describe("Group:", Ok(cluster::groups::describe(&frame, data))),
|
|
0x0005 => describe("Scene:", Ok(cluster::scenes::describe(&frame, data))),
|
|
0x0006 => describe("OnOff:", Ok(cluster::onoff::describe(&frame, data))),
|
|
0x0008 => describe("LevelCtrl:", Ok(cluster::levelctrl::describe(&frame, data))),
|
|
|
|
0x0019 => {
|
|
// suppress OTA messages
|
|
}
|
|
|
|
0x0021 => {
|
|
// suppress ZGP (Zigbee Green Power) messages
|
|
}
|
|
|
|
0x0300 => describe("ColorCtrl:", Ok(cluster::colorctrl::describe(&frame, data))),
|
|
|
|
0x0406 => {
|
|
// suppress occypancy sensing
|
|
}
|
|
|
|
0x1000 => describe(
|
|
"Commissioning:",
|
|
cluster::commissioning::describe(&frame, data),
|
|
),
|
|
|
|
0xFC01 => describe("HueEnt:", cluster::hue_fc01::describe(&frame, data)),
|
|
0xFC03 => describe("HueCmp:", cluster::hue_fc03::describe(&frame, data)),
|
|
|
|
_ => describe("UNKNOWN:", Ok(None)),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, long_about = None)]
|
|
#[command(about("Parses hue zigbee frames (as hex-encoded lines on stdin)"))]
|
|
struct Args {
|
|
/// Ignore packet number (easier diffing, since all packets are numbered 0)
|
|
#[arg(short, name = "no-index", default_value_t = false)]
|
|
no_index: bool,
|
|
}
|
|
|
|
fn main() -> ApiResult<()> {
|
|
pretty_env_logger::formatted_builder()
|
|
.filter_level(log::LevelFilter::Debug)
|
|
.parse_default_env()
|
|
.init();
|
|
|
|
let args = Args::parse();
|
|
|
|
for line in stdin().lines() {
|
|
let line = line?;
|
|
|
|
match serde_json::from_str::<Record>(line.trim()) {
|
|
Ok(data) => {
|
|
if let Err(err) = parse(&data, args.no_index) {
|
|
error!("Failed parse: {err}");
|
|
eprintln!(" {line:<40}");
|
|
eprintln!(" {data:?}");
|
|
}
|
|
}
|
|
|
|
Err(err) => {
|
|
error!("Failed to parse json: {err}");
|
|
eprintln!(" {line:<40}");
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|