summaryrefslogtreecommitdiff
path: root/ebus-rust/src
diff options
context:
space:
mode:
authorYves Fischer <yvesf+git@xapek.org>2021-12-31 23:57:29 +0100
committerYves Fischer <yvesf+git@xapek.org>2021-12-31 23:57:29 +0100
commitd0c7100b706d70dfff342def2bac864693fe8544 (patch)
tree333268ca977222e2f5a41eff68607b36721f7694 /ebus-rust/src
parentcaae83f445935c06cd6aef36f283a4688675278a (diff)
parent810721ceac0249e747cdf36f3edec22d829d4371 (diff)
downloadebus-d0c7100b706d70dfff342def2bac864693fe8544.tar.gz
ebus-d0c7100b706d70dfff342def2bac864693fe8544.zip
Merge branch 'ebus-rust'HEADmaster
Diffstat (limited to 'ebus-rust/src')
-rw-r--r--ebus-rust/src/build.rs0
-rw-r--r--ebus-rust/src/layer2/crc.rs23
-rw-r--r--ebus-rust/src/layer2/mod.rs263
-rw-r--r--ebus-rust/src/layer7/mod.rs129
-rw-r--r--ebus-rust/src/layer7/types.rs213
-rw-r--r--ebus-rust/src/main.rs403
-rw-r--r--ebus-rust/src/model/mod.rs126
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,
+}