diff options
author | Yves Fischer <yvesf+git@xapek.org> | 2021-12-31 23:57:29 +0100 |
---|---|---|
committer | Yves Fischer <yvesf+git@xapek.org> | 2021-12-31 23:57:29 +0100 |
commit | d0c7100b706d70dfff342def2bac864693fe8544 (patch) | |
tree | 333268ca977222e2f5a41eff68607b36721f7694 /ebus-rust/src | |
parent | caae83f445935c06cd6aef36f283a4688675278a (diff) | |
parent | 810721ceac0249e747cdf36f3edec22d829d4371 (diff) | |
download | ebus-d0c7100b706d70dfff342def2bac864693fe8544.tar.gz ebus-d0c7100b706d70dfff342def2bac864693fe8544.zip |
Diffstat (limited to 'ebus-rust/src')
-rw-r--r-- | ebus-rust/src/build.rs | 0 | ||||
-rw-r--r-- | ebus-rust/src/layer2/crc.rs | 23 | ||||
-rw-r--r-- | ebus-rust/src/layer2/mod.rs | 263 | ||||
-rw-r--r-- | ebus-rust/src/layer7/mod.rs | 129 | ||||
-rw-r--r-- | ebus-rust/src/layer7/types.rs | 213 | ||||
-rw-r--r-- | ebus-rust/src/main.rs | 403 | ||||
-rw-r--r-- | ebus-rust/src/model/mod.rs | 126 |
7 files changed, 1157 insertions, 0 deletions
diff --git a/ebus-rust/src/build.rs b/ebus-rust/src/build.rs new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ebus-rust/src/build.rs diff --git a/ebus-rust/src/layer2/crc.rs b/ebus-rust/src/layer2/crc.rs new file mode 100644 index 0000000..6fb782d --- /dev/null +++ b/ebus-rust/src/layer2/crc.rs @@ -0,0 +1,23 @@ +const CRC: [u8; 256] = [ + 0x00, 0x9b, 0xad, 0x36, 0xc1, 0x5a, 0x6c, 0xf7, 0x19, 0x82, 0xb4, 0x2f, 0xd8, 0x43, 0x75, 0xee, + 0x32, 0xa9, 0x9f, 0x04, 0xf3, 0x68, 0x5e, 0xc5, 0x2b, 0xb0, 0x86, 0x1d, 0xea, 0x71, 0x47, 0xdc, + 0x64, 0xff, 0xc9, 0x52, 0xa5, 0x3e, 0x08, 0x93, 0x7d, 0xe6, 0xd0, 0x4b, 0xbc, 0x27, 0x11, 0x8a, + 0x56, 0xcd, 0xfb, 0x60, 0x97, 0x0c, 0x3a, 0xa1, 0x4f, 0xd4, 0xe2, 0x79, 0x8e, 0x15, 0x23, 0xb8, + 0xc8, 0x53, 0x65, 0xfe, 0x09, 0x92, 0xa4, 0x3f, 0xd1, 0x4a, 0x7c, 0xe7, 0x10, 0x8b, 0xbd, 0x26, + 0xfa, 0x61, 0x57, 0xcc, 0x3b, 0xa0, 0x96, 0x0d, 0xe3, 0x78, 0x4e, 0xd5, 0x22, 0xb9, 0x8f, 0x14, + 0xac, 0x37, 0x01, 0x9a, 0x6d, 0xf6, 0xc0, 0x5b, 0xb5, 0x2e, 0x18, 0x83, 0x74, 0xef, 0xd9, 0x42, + 0x9e, 0x05, 0x33, 0xa8, 0x5f, 0xc4, 0xf2, 0x69, 0x87, 0x1c, 0x2a, 0xb1, 0x46, 0xdd, 0xeb, 0x70, + 0x0b, 0x90, 0xa6, 0x3d, 0xca, 0x51, 0x67, 0xfc, 0x12, 0x89, 0xbf, 0x24, 0xd3, 0x48, 0x7e, 0xe5, + 0x39, 0xa2, 0x94, 0x0f, 0xf8, 0x63, 0x55, 0xce, 0x20, 0xbb, 0x8d, 0x16, 0xe1, 0x7a, 0x4c, 0xd7, + 0x6f, 0xf4, 0xc2, 0x59, 0xae, 0x35, 0x03, 0x98, 0x76, 0xed, 0xdb, 0x40, 0xb7, 0x2c, 0x1a, 0x81, + 0x5d, 0xc6, 0xf0, 0x6b, 0x9c, 0x07, 0x31, 0xaa, 0x44, 0xdf, 0xe9, 0x72, 0x85, 0x1e, 0x28, 0xb3, + 0xc3, 0x58, 0x6e, 0xf5, 0x02, 0x99, 0xaf, 0x34, 0xda, 0x41, 0x77, 0xec, 0x1b, 0x80, 0xb6, 0x2d, + 0xf1, 0x6a, 0x5c, 0xc7, 0x30, 0xab, 0x9d, 0x06, 0xe8, 0x73, 0x45, 0xde, 0x29, 0xb2, 0x84, 0x1f, + 0xa7, 0x3c, 0x0a, 0x91, 0x66, 0xfd, 0xcb, 0x50, 0xbe, 0x25, 0x13, 0x88, 0x7f, 0xe4, 0xd2, 0x49, + 0x95, 0x0e, 0x38, 0xa3, 0x54, 0xcf, 0xf9, 0x62, 0x8c, 0x17, 0x21, 0xba, 0x4d, 0xd6, 0xe0, 0x7b, +]; + +#[inline] +pub fn crc(data: &[u8]) -> u8 { + data.iter().fold(0, |acc, x| CRC[acc as usize] ^ x) +} diff --git a/ebus-rust/src/layer2/mod.rs b/ebus-rust/src/layer2/mod.rs new file mode 100644 index 0000000..ef07aad --- /dev/null +++ b/ebus-rust/src/layer2/mod.rs @@ -0,0 +1,263 @@ +use std::fmt; + +mod crc; + +const EBUS_SYN: u8 = 0xaa; +const EBUS_ESCAPE: u8 = 0xa9; +const EBUS_ACKOK: u8 = 0x00; + +#[derive(Default)] +pub struct Packet { + pub source: u8, + pub destination: u8, + pub primary: u8, + pub secondary: u8, + payload_length: u8, + payload: Vec<u8>, + pub crc: u8, + payload_slave_length: u8, + payload_slave: Vec<u8>, + crc_slave: u8, +} + +impl Packet { + // payload returns the un-escaped payload + pub fn payload_decoded(&self) -> Vec<u8> { + let mut v = Vec::new(); + let mut i = 0; + + while i < self.payload.len() { + let c = self.payload[i]; + if c == EBUS_ESCAPE && i + 1 < self.payload.len() { + i += 1; + let c = self.payload[i]; + if c == 0x0 { + v.push(EBUS_ESCAPE); + } else if c == 0x1 { + v.push(EBUS_SYN); + } else { + v.push(c); + } + } else { + v.push(c); + } + i += 1; + } + v + } + + pub fn calc_crc(&self) -> u8 { + let x = &[ + self.source, + self.destination, + self.primary, + self.secondary, + self.payload_length, + ]; + crc::crc(&[&x[..], self.payload.as_slice()].concat()) + } +} + +impl fmt::Debug for Packet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.payload_slave_length > 0 { + f.debug_struct("Packet") + .field("source", &format!("{:#02x}", &self.source)) + .field("destination", &format!("{:#02x}", &self.destination)) + .field("primary", &format!("{:#02x}", &self.primary)) + .field("secondary", &format!("{:#02x}", &self.secondary)) + .field("payload", &format!("{:02x?}", &self.payload)) + .field("payload_length", &self.payload_length) + .field("crc", &format!("{:#02x}", &self.crc)) + .field( + "payload_slave_length", + &format!("{:#02x}", &self.payload_slave_length), + ) + .field("payload_slave", &format!("{:02x?}", &self.payload_slave)) + .field("crc_slave", &format!("{:#02x}", &self.crc_slave)) + .finish() + } else { + f.debug_struct("Packet") + .field("source", &format!("{:#02x}", &self.source)) + .field("destination", &format!("{:#02x}", &self.destination)) + .field("primary", &format!("{:#02x}", &self.primary)) + .field("secondary", &format!("{:#02x}", &self.secondary)) + .field("payload", &format!("{:02x?}", &self.payload)) + .field("payload_length", &self.payload_length) + .field("crc", &format!("{:#02x}", &self.crc)) + .finish() + } + } +} + +fn read_header(data: &[u8]) -> nom::IResult<&[u8], Packet> { + use nom::number::streaming::u8; + use nom::sequence::tuple; + let (input, (source, destination, primary, secondary, payload_length)) = + tuple((u8, u8, u8, u8, u8))(data)?; + Ok(( + input, + Packet { + source, + destination, + primary, + secondary, + payload_length, + ..Default::default() + }, + )) +} + +fn read_packet(data: &[u8]) -> nom::IResult<&[u8], Packet> { + use nom::bytes::streaming::tag; + use nom::combinator::opt; + use nom::multi::count; + use nom::number::streaming::u8; + use nom::sequence::tuple; + + let (input, mut packet) = read_header(data)?; + + let (input2, (payload, crc, _, payload_slave_length)) = tuple(( + count(u8, packet.payload_length as usize), // payload + u8, // crc + opt(tag([EBUS_ACKOK])), // ACK but non-ack is tolerated + u8, // SYN or payload slave length + ))(input)?; + packet.payload = payload; + packet.crc = crc; + + if payload_slave_length != EBUS_SYN { + packet.payload_slave_length = payload_slave_length; + let (input3, (payload_slave, crc_slave)) = + tuple((count(u8, payload_slave_length as usize), u8))(input2)?; + packet.payload_slave = payload_slave; + packet.crc_slave = crc_slave; + Ok((input3, packet)) + } else { + Ok((input2, packet)) + } +} + +pub fn parse(data: &[u8]) -> Result<Packet, nom::Err<nom::error::Error<&[u8]>>> { + use nom::bytes::streaming::take_while; + use nom::sequence::preceded; + fn take_syn(data: &[u8]) -> nom::IResult<&[u8], &[u8]> { + take_while(|chr| chr == EBUS_SYN)(data) + } + + let mut parser = preceded(take_syn, read_packet); + let result = parser(data); + result.map(|r| r.1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn teststructure_mastermaster() { + let data: Vec<u8> = vec![ + 170, // Syn + 170, // Syn + 003, // Source + 241, // Destination + 008, // primaryCommand + 000, // secondaryCommand + 008, // payloadLength + 128, // p1 + 040, // p2 + 230, // p3 + 002, // p4 + 000, // p5 + 002, // p6 + 000, // p7 + 010, // p8 + 128, // CRC + 000, // ACK + 170, // SYN + 170, + ]; + + let res = parse(&data).unwrap(); + assert_eq!(res.source, 0003); + assert_eq!(res.destination, 241); + assert_eq!(res.primary, 008); + assert_eq!(res.secondary, 000); + assert_eq!(res.payload_length, 0008); + assert_eq!(res.crc, 128); + assert_eq!(res.payload, vec![128, 040, 230, 002, 000, 002, 000, 010]); + } + + #[test] + fn testcrc() { + let packets = [ + &[ + 0x10, 0x03, 0x08, 0x00, 0x08, 0x00, 0x05, 0x00, 0x13, 0x80, 0x40, 0x00, 0x0a, 0x71, + 0x00, 0xaa, + ][..], + &[ + 0x03, 0xf1, 0x08, 0x00, 0x08, 0x00, 0x14, 0x00, 0x13, 0x80, 0x00, 0x00, 0x0f, 0xc7, + 0x00, 0xaa, + ][..], + &[ + 0x10, 0x03, 0x05, 0x07, 0x09, 0x00, 0x01, 0x50, 0x00, 0x01, 0x00, 0xff, 0x14, 0xff, + 0xa6, 0x00, 0xaa, + ][..], + &[ + 0xf1, 0xfe, 0x05, 0x03, 0x08, 0x01, 0x00, 0x40, 0xff, 0x30, 0xff, 0x00, 0x13, 0xd8, + 0x00, 0xaa, + ][..], + &[ + 0x03, 0xfe, 0x05, 0x03, 0x08, 0x01, 0x00, 0x00, 0x00, 0x30, 0x17, 0x33, 0x13, 0x82, + 0x00, 0xaa, + ][..], + &[ + // 00000d60: aaaa 1003 0507 09bb 044b 0300 80ff 54ff .........K....T. + // 00000d70: 0400 aaaa aaaa aaaa f1fe 0503 0801 0110 ................ + 0x10, 0x03, 0x05, 0x07, 0x09, 0xbb, 0x04, 0x4b, 0x03, 0x00, 0x80, 0xff, 0x54, 0xff, + 0x04, 0x00, 0xaa, + ][..], + ]; + for d in &packets { + let p = parse(d).unwrap(); + assert_eq!(p.crc, p.calc_crc(), "{:?}", p); + } + } + + #[test] + fn test_escape() { + let data: Vec<u8> = vec![ + 170, // Syn + 170, // Syn + 3, // Source + 241, // Destination + 8, // primaryCommand + 0, // secondaryCommand + 8, // payloadLength + 128, // p1 + 40, // p2 + EBUS_ESCAPE, // p3 + 1, // p4 + EBUS_ESCAPE, // p5 + 0, // p6 + 0, // p7 + 10, // p8 + 128, // CRC + 0, // ACK + 170, // SYN + 170, + ]; + + let res = parse(&data).unwrap(); + assert_eq!(res.source, 0003); + assert_eq!(res.destination, 241); + assert_eq!(res.primary, 008); + assert_eq!(res.secondary, 000); + assert_eq!(res.payload_length, 0008); + assert_eq!(res.crc, 128); + assert_eq!( + res.payload_decoded(), + vec![128, 040, EBUS_SYN, EBUS_ESCAPE, 000, 010] + ); + } +} diff --git a/ebus-rust/src/layer7/mod.rs b/ebus-rust/src/layer7/mod.rs new file mode 100644 index 0000000..7f22478 --- /dev/null +++ b/ebus-rust/src/layer7/mod.rs @@ -0,0 +1,129 @@ +pub mod types; +use crate::layer2; +use crate::model; + +pub struct DecodedField { + pub v: Option<types::Value>, + pub name: String, + pub field: model::PacketField, +} + +pub fn decode_fields(p: &layer2::Packet, fields: &[model::PacketField]) -> Vec<DecodedField> { + fields + .iter() + .map(|f| { + let r = |decoder: &dyn Fn(&[u8]) -> Option<types::Value>, + offset: &u8, + name: &String| DecodedField { + v: decoder( + p.payload_decoded() + .get((*offset as usize)..) + .unwrap_or(&[0; 0]), + ), + name: name.clone(), + field: f.clone(), + }; + match f { + model::PacketField::Byte { offset, name } => r(&types::byte, offset, name), + model::PacketField::Data1b { offset, name } => r(&types::data1b, offset, name), + model::PacketField::Data1c { offset, name } => r(&types::data1c, offset, name), + model::PacketField::Data2b { offset, name } => r(&types::data2b, offset, name), + model::PacketField::Data2c { offset, name } => r(&types::data2c, offset, name), + model::PacketField::ByteEnum { + offset, + name, + options, + } => r(&(move |data| types::byteenum(data, options)), offset, name), + model::PacketField::Bcd { offset, name } => r(&types::bcd, offset, name), + model::PacketField::Bit { offset, name } => r(&types::bit, offset, name), + model::PacketField::Word { offset, name } => r(&types::word, offset, name), + model::PacketField::String { + offset, + name, + length, + } => r( + &(move |data| types::string(data, *length as usize)), + offset, + name, + ), + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + static EBUS_XML: &[u8] = include_bytes!("../../../ebus-xml/ebus.xml"); + + fn tostring(values: Vec<DecodedField>) -> String { + values + .iter() + .map(|df| { + df.name.clone() + + "=" + + &df + .v + .as_ref() + .map(|v2| v2.convert()) + .unwrap_or(String::from("<None>")) + }) + .collect::<Vec<String>>() + .join(",") + } + + #[test] + fn test1() { + let conf = model::read_config(EBUS_XML).unwrap(); + let data = [ + 0xf1, 0xfe, 0x5, 0x3, 0x08, 0x01, 0x01, 0x10, 0xff, 0x4e, 0xff, 0x35, 0x0b, 0x92, 0x00, + 0xaa, + ]; + + let l2p = layer2::parse(&data[..]).unwrap(); + let pack = conf.packets.get(l2p.primary, l2p.secondary).unwrap(); + let values = decode_fields(&l2p, pack.fields.fields.as_ref().unwrap()); + assert_eq!("stellgradKesselleistung=<None>,kesselTemperatur=39,ruecklaufTemperatur=<None>,boilerTemperatur=53,aussenTemperatur=11", tostring(values)); + } + + #[test] + fn test() { + let conf = model::read_config(EBUS_XML).unwrap(); + + let data: Vec<u8> = vec![ + 170, // Syn + 170, // Syn + 003, // Source + 241, // Destination + 008, // primaryCommand + 000, // secondaryCommand + 008, // payloadLength + 128, // p1 + 040, // p2 + 230, // p3 + 002, // p4 + 000, // p5 + 002, // p6 + 000, // p7 + 010, // p8 + 128, // CRC + 000, // ACK + 170, // SYN + 170, + ]; + + let res = layer2::parse(&data).unwrap(); + assert_eq!( + res.payload_decoded(), + vec![128, 040, 230, 002, 000, 002, 000, 010] + ); + + let pack = conf.packets.get(res.primary, res.secondary).unwrap(); + let values = decode_fields(&res, pack.fields.fields.as_ref().unwrap()); + assert_eq!( + "TK_soll=40.5,TA_ist=2.8984375,L_zwang=0,Status=false,TB_soll=10", + tostring(values) + ); + } +} diff --git a/ebus-rust/src/layer7/types.rs b/ebus-rust/src/layer7/types.rs new file mode 100644 index 0000000..c119600 --- /dev/null +++ b/ebus-rust/src/layer7/types.rs @@ -0,0 +1,213 @@ +use crate::model; +use std::convert::TryFrom; +use std::ops::Neg; + +#[derive(PartialEq, Debug)] +pub enum Value { + Bool(bool), + I8(i8), + U8(u8), + U16(u16), + F32(f32), + String(String), +} + +impl Value { + pub fn convert(&self) -> String { + match self { + Value::Bool(v) => format!("{}", v), + Value::I8(v) => format!("{}", v), + Value::U8(v) => format!("{}", v), + Value::U16(v) => format!("{}", v), + Value::F32(v) => format!("{}", v), + Value::String(v) => v.clone(), + } + } +} + +#[inline] +fn low_nibble(v: u8) -> u8 { + 0x0f & v +} + +#[inline] +fn high_nibble(v: u8) -> u8 { + v >> 4 +} + +pub fn bcd(data: &[u8]) -> Option<Value> { + data.get(0).and_then(|value| { + if *value == 0xff { + None // Ersatzwert + } else { + Some(Value::U8(((*value & 0xf0) >> 4) * 10 + (*value & 0x0f))) + } + }) +} + +pub fn word(data: &[u8]) -> Option<Value> { + data.get(0..2) + .and_then(|data| <[u8; 2]>::try_from(data).ok()) + .and_then(|[low, high]| { + if low == 0xff && high == 0xff { + None // Ersatzwert + } else { + Some(Value::U16(low as u16 | (high as u16) << 8)) + } + }) +} + +pub fn byteenum(data: &[u8], options: &model::ByteEnumList) -> Option<Value> { + data.get(0).and_then(|v| options.get(*v)).map(Value::String) +} + +pub fn data2c(data: &[u8]) -> Option<Value> { + data.get(0..2) + .and_then(|data| <[u8; 2]>::try_from(data).ok()) + .and_then(|[low, high]| { + if high == 0x80 && low == 0x00 { + None + } else if high & 0x80 == 0x80 { + Some(Value::F32( + ((((!high as u16) * 16) + (high_nibble(!low) as u16)) as f32 + + ((low_nibble(!low) + 1) as f32 / 16.0)) + .neg(), + )) + } else { + Some(Value::F32( + ((high as u16) * 16 + high_nibble(low) as u16) as f32 + + low_nibble(low) as f32 / 16.0, + )) + } + }) +} + +pub fn data2b(data: &[u8]) -> Option<Value> { + data.get(0..2) + .and_then(|data| <[u8; 2]>::try_from(data).ok()) + .and_then(|[low, high]| { + if high == 0x80 && low == 0x00 { + None + } else if high & 0x80 == 0x80 { + Some(Value::F32( + ((!high as f32) + ((!low as f32 + 1.0) / 256.0_f32)).neg(), + )) + } else { + Some(Value::F32(high as f32 + (low as f32 / 256.0))) + } + }) +} + +pub fn bit(data: &[u8]) -> Option<Value> { + data.get(0).map(|v| *v == 1).map(Value::Bool) +} + +pub fn byte(data: &[u8]) -> Option<Value> { + data.get(0).and_then(|value| { + if *value == 0xff { + None + } else { + Some(Value::U8(*value)) + } + }) +} + +pub fn data1b(data: &[u8]) -> Option<Value> { + data.get(0).and_then(|value| { + if *value == 0x80 { + None + } else if *value >> 7 == 1 { + Some(Value::I8(((1 + !(*value)) as i8).neg())) + } else { + Some(Value::I8(*value as i8)) + } + }) +} + +pub fn data1c(data: &[u8]) -> Option<Value> { + data.get(0).and_then(|value| { + if *value == 0xff { + None + } else { + Some(Value::F32(*value as f32 / 2.0)) + } + }) +} + +pub fn string(data: &[u8], length: usize) -> Option<Value> { + data.get(0..length) + .map(|data| { + data.iter() + .map(|p| *p as char) + .filter(|p| { + *p >= 'a' && *p <= 'z' || *p >= 'A' && *p <= 'Z' || *p >= ' ' && *p <= '9' + }) + .map(String::from) + .collect::<Vec<String>>() + .join("") + }) + .map(Value::String) +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_f32_eq_approx { + ($x:expr, $y:expr, $Δ:expr) => { + match $y { + Some(Value::F32(v)) => { + if !($x - v < $Δ && v - $x < $Δ) { + panic!("Left({}) != Right({}) Δ={}", $x, v, $Δ); + } + } + _ => panic!("Left({}) != Right(None)", $x), + } + }; + } + + #[test] + fn test_decoders() { + // Bei allen 16-Bit Typen (2 Byte), wird das Low-Byte immer zuerst übertragen. + assert_eq!(None, bcd(&[0xff])); + assert_eq!(Some(Value::U8(0)), bcd(&[0x00])); + assert_eq!(Some(Value::U8(1)), bcd(&[0x01])); + assert_eq!(Some(Value::U8(2)), bcd(&[0x02])); + assert_eq!(Some(Value::U8(10)), bcd(&[0x10])); + assert_eq!(Some(Value::U8(11)), bcd(&[0x11])); + assert_eq!(Some(Value::U8(99)), bcd(&[0x99])); + + assert_eq!(Some(Value::I8(0)), data1b(&[0x00])); + assert_eq!(Some(Value::I8(1)), data1b(&[0x01])); + assert_eq!(Some(Value::I8(127)), data1b(&[0x7f])); + assert_eq!(Some(Value::I8(-127)), data1b(&[0x81])); + assert_eq!(None, data1b(&[0x80])); + + assert_eq!(Some(Value::F32(0.0)), data1c(&[0])); + assert_eq!(Some(Value::F32(50.0)), data1c(&[0x64])); + assert_eq!(Some(Value::F32(100.0)), data1c(&[0xc8])); + + assert_eq!(Some(Value::F32(0.0)), data2b(&[0x00, 0x00])); + assert_eq!(Some(Value::F32(1.0 / 256.0)), data2b(&[0x01, 0x00,])); + assert_eq!(Some(Value::F32(-1.0 / 256.0)), data2b(&[0xff, 0xff])); + assert_eq!(Some(Value::F32(-1.0)), data2b(&[0x00, 0xff])); + assert_eq!(None, data2b(&[0x00, 0x80])); + assert_f32_eq_approx!(-127.996, data2b(&[0x01, 0x80]), 0.001); + assert_f32_eq_approx!(127.99609, data2b(&[0xff, 0x7f]), 0.001); + assert_f32_eq_approx!(52.7, data2b(&[0xb3, 0x34]), 0.1); + + assert_f32_eq_approx!(0.0, data2c(&[0x00, 0x00]), f32::EPSILON); + assert_f32_eq_approx!((1.0 / 16.0), data2c(&[0x01, 0x00]), f32::EPSILON); + assert_f32_eq_approx!((-1.0 / 16.0), data2c(&[0xff, 0xff]), f32::EPSILON); + assert_f32_eq_approx!(-1.0, data2c(&[0xf0, 0xff]), f32::EPSILON); + assert_eq!(None, data2c(&[0x00, 0x80])); + assert_f32_eq_approx!(-2047.9, data2c(&[0x01, 0x80]), 0.1); + assert_f32_eq_approx!(2047.9, data2c(&[0xff, 0x7f]), 0.1); + + assert_eq!(None, word(&[0xff, 0xff])); + assert_eq!(Some(Value::U16(65279)), word(&[0xff, 0xfe])); + assert_eq!(Some(Value::U16(256)), word(&[0x00, 0x01])); + assert_eq!(Some(Value::U16(1)), word(&[0x01, 0x00])); + assert_eq!(Some(Value::U16(0)), word(&[0x00, 0x00])); + } +} diff --git a/ebus-rust/src/main.rs b/ebus-rust/src/main.rs new file mode 100644 index 0000000..9046ce6 --- /dev/null +++ b/ebus-rust/src/main.rs @@ -0,0 +1,403 @@ +extern crate bytes; +extern crate env_logger; +extern crate log; +extern crate nom; +extern crate quick_xml; +extern crate serde; +extern crate structopt; + +mod layer2; +mod layer7; +mod model; + +use bytes::{BufMut, BytesMut}; +use std::fs::File; +use std::io; +use std::io::Read; +use std::path::Path; +use std::thread; +use std::time; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +struct Opts { + #[structopt( + short, + long, + default_value = "", + help = "if not set internal copy will be used" + )] + config: String, + #[structopt(skip)] + ebus_xml: Vec<u8>, + #[structopt(short, long, help = "Use the adapter.ebusd.eu enhanced protocol")] + enhanced: bool, + #[structopt(subcommand)] + subcmd: Command, +} + +#[derive(Debug, StructOpt)] +enum Command { + Dump(Dump), + DumpL2(DumpL2), + ParseL2(ParseL2), + ParseL7(ParseL7), + Influxdb(Influxdb), +} + +impl Command { + fn run(&self, opts: &Opts) { + match self { + Command::Dump(..) => dump(opts), + Command::DumpL2(..) => dump_l2(opts), + Command::ParseL2(..) => parse_l2(opts, &|p| println!("{:?}", p)), + Command::ParseL7(..) => parse_l7(opts, &|_, _, _, _| ()), + Command::Influxdb(cfg) => influxdb(opts, cfg), + } + } +} + +#[derive(Debug, StructOpt)] +#[structopt(about = "Dump the configuration from XML to stdout")] +struct Dump {} + +#[derive(Debug, StructOpt)] +#[structopt(about = "Dump raw data")] +struct DumpL2 {} + +#[derive(Debug, StructOpt)] +#[structopt(about = "Parse and dump L2 from stdin")] +struct ParseL2 {} + +#[derive(Debug, StructOpt)] +#[structopt(about = "Parse and dump L7 from stdin")] +struct ParseL7 {} + +#[derive(Debug, StructOpt)] +#[structopt(about = "Parse from stdin and write to influxdb")] +struct Influxdb { + #[structopt(short, long, default_value = "http://localhost:8086")] + url: String, + #[structopt(short, long, default_value = "ebus")] + db: String, + #[structopt(short, long, default_value = "ebus")] + measurement: String, +} + +fn main() { + env_logger::init(); + let mut opts: Opts = Opts::from_args(); + + log::info!("Enhanced protocol is status: {}", opts.enhanced); + + if opts.config.is_empty() { + opts.ebus_xml = Vec::from(*include_bytes!("../../ebus-xml/ebus.xml")); + } else { + let path = Path::new(&opts.config); + let mut file = File::open(&path) + .map_err(|e| format!("could not open file: {}", e)) + .unwrap(); + let mut xml = String::new(); + file.read_to_string(&mut xml) + .map_err(|e| format!("could not read: {}", e)) + .unwrap(); + opts.ebus_xml = Vec::from(xml.as_bytes()); + } + + opts.subcmd.run(&opts); +} + +fn dump(opts: &Opts) { + let conf = model::read_config(&opts.ebus_xml).unwrap(); + for dev in conf.devices.devices { + println!("device: address={} name={}", dev.address, dev.name); + for d in dev.descriptions { + println!(" {}: {}", d.lang, d.text) + } + } + for p in conf.packets.packets { + println!( + "packet: primary={:02} secondary={:02} name={}", + p.primary, p.secondary, p.name + ); + for d in p.descriptions { + println!(" {}: {}", d.lang, d.text); + } + for f in p.fields.fields.unwrap() { + match f { + model::PacketField::Byte { offset, name } => { + println!(" [{:02}] Byte: {}", offset, name); + } + model::PacketField::Data1b { offset, name } => { + println!(" [{:02}] Data1b: {}", offset, name) + } + model::PacketField::Data1c { offset, name } => { + println!(" [{:02}] Data1c: {}", offset, name) + } + model::PacketField::Data2b { offset, name } => { + println!(" [{:02}] Data2b: {}", offset, name) + } + model::PacketField::Data2c { offset, name } => { + println!(" [{:02}] Data1c: {}", offset, name) + } + model::PacketField::ByteEnum { + offset, + name, + options, + } => { + println!(" [{:02}] ByteEnum: {}", offset, name); + for o in options.0 { + println!(" [{:02}] {}", o.value, o.name); + } + } + model::PacketField::Bcd { offset, name } => { + println!(" [{:02}] Bcd: {}", offset, name) + } + model::PacketField::Bit { offset, name } => { + println!(" [{:02}] Bit: {}", offset, name) + } + model::PacketField::Word { offset, name } => { + println!(" [{:02}] Word: {}", offset, name) + } + model::PacketField::String { + offset, + name, + length, + } => { + println!(" [{:02}] String: {} / {}", offset, name, length) + } + } + } + } +} + +fn dump_l2(opts: &Opts) { + let mut buf = BytesMut::with_capacity(1024); + let mut other: u8 = 0; + let mut sync = false; + + for b in io::stdin().bytes() { + let mut b = b.unwrap(); + if opts.enhanced && b >= 0x80 { + if other == 0 { + if b & 0xc0 == 0xc0 { + other = b; + } + continue; + } else { + // byte-1 byte-2 + // 76543210 76543210 + // 11ccccdd 10dddddd + // c: command (0x01 = receive) + // d: data (here b) + b = (other & 0x03) << 6 | b & 0x3f; + other = 0; + } + } + if !sync { + if b == 0xaa { + sync = true; + } + continue; + } + buf.put_u8(b); + + if b == 0xaa { + if buf.len() > 1 { + println!( + "{:#02x?}", + buf.iter() + .map(|v| format!("0x{:02x}", v)) + .collect::<Vec<String>>() + .join(", ") + ); + } + buf.clear() + } + } +} + +fn parse_l2(opts: &Opts, cb: &dyn Fn(layer2::Packet)) { + let mut buf = BytesMut::with_capacity(1024); + let mut other: u8 = 0; + let mut sync = false; + for b in io::stdin().bytes() { + let mut b = b.unwrap(); + if opts.enhanced && b >= 0x80 { + if other == 0 { + if b & 0xc0 == 0xc0 { + other = b; + } + continue; + } else { + // byte-1 byte-2 + // 76543210 76543210 + // 11ccccdd 10dddddd + // c: command (0x01 = receive) + // d: data (here b) + b = (other & 0x03) << 6 | b & 0x3f; + other = 0; + } + } + if !sync { + if b == 0xaa { + sync = true; + } + continue; + } + if b == 0xaa { + if buf.len() > 1 { + buf.put_u8(0xaa); + let l2p = layer2::parse(&buf); + match l2p.ok() { + Some(p) => { + if p.calc_crc() == p.crc { + log::debug!( + "{:#02x?} from {:#02x?}", + p, + buf.iter() + .map(|v| format!("0x{:02x}", v)) + .collect::<Vec<String>>() + .join(", ") + ); + } else { + log::info!( + "CRC-Fail: [{}] should be crc={:#02x}", + buf.iter() + .map(|v| format!("0x{:02x}", v)) + .collect::<Vec<String>>() + .join(", "), + p.calc_crc() + ); + } + cb(p); + } + None => { + if buf.len() > 2 { + log::info!( + "Discard: [{}]", + buf.iter() + .map(|v| format!("0x{:02x}", v)) + .collect::<Vec<String>>() + .join(", ") + ); + } + } + } + buf.clear(); + } + continue; + } + buf.put_u8(b); + } +} + +fn parse_l7(opts: &Opts, cb: &dyn Fn(String, String, String, &Vec<layer7::DecodedField>)) { + let conf = model::read_config(&opts.ebus_xml).unwrap(); + + parse_l2(opts, &|p| { + let pack = conf.packets.get(p.primary, p.secondary); + if pack.is_none() { + log::info!("No definition: {:?}", p); + return; + } + let pack = pack.unwrap(); + let values = pack + .fields + .fields + .as_ref() + .map(|fields| layer7::decode_fields(&p, fields)); + + let source = conf + .devices + .devices + .iter() + .find(|d| d.address == p.source) + .map(|d| d.name.clone()) + .unwrap_or_else(|| format!("{:#02x}", p.source)); + let destination = conf + .devices + .devices + .iter() + .find(|d| d.address == p.destination) + .map(|d| d.name.clone()) + .unwrap_or_else(|| format!("{:#02x}", p.destination)); + + log::info!( + "{} -> {}: {} {}", + source, + destination, + pack.name, + values + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|field| format!( + "{}={}", + field.name.clone(), + field + .v + .as_ref() + .map(|v2| v2.convert()) + .unwrap_or_else(|| String::from("<>")) + )) + .collect::<Vec<String>>() + .join(", ") + ); + if let Some(values) = values { + cb(source, destination, pack.name.clone(), &values); + } + }) +} + +impl From<&layer7::types::Value> for rinfluxdb::line_protocol::FieldValue { + fn from(v: &layer7::types::Value) -> Self { + match v { + layer7::types::Value::Bool(v) => rinfluxdb::line_protocol::FieldValue::from(*v), + layer7::types::Value::I8(v) => rinfluxdb::line_protocol::FieldValue::from(*v as i64), + layer7::types::Value::U8(v) => rinfluxdb::line_protocol::FieldValue::from(*v as u64), + layer7::types::Value::U16(v) => rinfluxdb::line_protocol::FieldValue::from(*v as u64), + layer7::types::Value::F32(v) => rinfluxdb::line_protocol::FieldValue::from(*v as f64), + layer7::types::Value::String(v) => { + rinfluxdb::line_protocol::FieldValue::from(v.clone()) + } + } + } +} + +fn influxdb(opts: &Opts, cfg: &Influxdb) { + use rinfluxdb::line_protocol; + use rinfluxdb::line_protocol::blocking::Client; + use rinfluxdb::line_protocol::LineBuilder; + use url::Url; + + let url = Url::parse(&cfg.url).unwrap(); + let client = Client::new(url, Some(("", ""))).unwrap(); + + parse_l7(opts, &|source, destination, name, values| { + let lines = values + .iter() + .filter(|df| df.v.is_some()) // filter ersatzwert values + .map(|df| { + let l = LineBuilder::new(cfg.measurement.clone()) + .insert_tag("source", source.clone()) + .insert_tag("destination", destination.clone()) + .insert_field( + format!("{}.{}", name.clone(), df.name.clone()), + df.v.as_ref().unwrap(), + ) + .build(); + log::debug!("{:?}", l); + l + }) + .collect::<Vec<line_protocol::Line>>(); + + match client.send(&cfg.db, &lines) { + Ok(_) => (), + Err(err) => { + log::error!("Sleep 1s after error sending values to influxdb: {}", err); + thread::sleep(time::Duration::from_millis(1000)); + } + }; + }); +} diff --git a/ebus-rust/src/model/mod.rs b/ebus-rust/src/model/mod.rs new file mode 100644 index 0000000..fd738fa --- /dev/null +++ b/ebus-rust/src/model/mod.rs @@ -0,0 +1,126 @@ +use serde::Deserialize; + +use quick_xml::de::{from_reader}; + +pub fn read_config(xml: &[u8]) -> Result<Ebus, String> { + let ebus_configuration = from_reader(xml).map_err(|e| format!("failed to read xml: {}", e))?; + Ok(ebus_configuration) +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Description { + pub lang: String, + #[serde(rename = "$value")] + pub text: String, +} + +#[derive(Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum DeviceType { + Master, + Slave, + Broadcast, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Device { + pub address: u8, + #[serde(rename = "type", default)] + pub device_type: Option<DeviceType>, + pub name: String, + #[serde(rename = "description", default)] + pub descriptions: Vec<Description>, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Devices { + #[serde(rename = "device", default)] + pub devices: Vec<Device>, +} + +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct ByteEnumOption { + pub value: u8, + pub name: String, +} + +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct ByteEnumList(pub Vec<ByteEnumOption>); + +impl ByteEnumList { + pub fn get(&self, value: u8) -> Option<String> { + self.0 + .iter() + .find(|v| v.value == value) + .map(|v| v.name.clone()) + } +} + +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub enum PacketField { + #[serde(rename = "byte")] + Byte { offset: u8, name: String }, + #[serde(rename = "data1b")] + Data1b { offset: u8, name: String }, + #[serde(rename = "data1c")] + Data1c { offset: u8, name: String }, + #[serde(rename = "data2b")] + Data2b { offset: u8, name: String }, + #[serde(rename = "data2c")] + Data2c { offset: u8, name: String }, + #[serde(rename = "byteEnum")] + ByteEnum { + offset: u8, + name: String, + #[serde(rename = "option")] + options: ByteEnumList, + }, + #[serde(rename = "bcd")] + Bcd { offset: u8, name: String }, + #[serde(rename = "bit")] + Bit { offset: u8, name: String }, + #[serde(rename = "word")] + Word { offset: u8, name: String }, + #[serde(rename = "string")] + String { + offset: u8, + name: String, + length: u8, + }, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct PacketFields { + #[serde(rename = "$value")] + pub fields: Option<Vec<PacketField>>, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Packet { + pub primary: u8, + pub secondary: u8, + pub name: String, + #[serde(rename = "description", default)] + pub descriptions: Vec<Description>, + pub fields: PacketFields, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Packets { + #[serde(rename = "packet", default)] + pub packets: Vec<Packet>, +} + +impl Packets { + pub fn get(&self, primary: u8, secondary: u8) -> Option<&Packet> { + self.packets + .iter() + .find(|p| p.primary == primary && p.secondary == secondary) + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Ebus { + pub devices: Devices, + pub packets: Packets, +} |