From 9e421b91b3944070141edd646edabefeba149b6a Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Sun, 14 Aug 2022 18:05:48 +1000 Subject: [PATCH] Sloppily implement scale/offset --- Cargo.lock | 28 +++++++++ Cargo.toml | 1 + src/main.rs | 90 ++++++----------------------- src/modbus/config.rs | 31 +++++++--- src/modbus/mod.rs | 131 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27bf7d3..255dac6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "async-trait" version = "0.1.56" @@ -504,6 +510,7 @@ dependencies = [ "humantime-serde", "itertools", "rumqttc", + "rust_decimal", "serde", "serde_json", "serialport", @@ -547,6 +554,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.1" @@ -710,6 +726,18 @@ dependencies = [ "url", ] +[[package]] +name = "rust_decimal" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" +dependencies = [ + "arrayvec", + "num-traits", + "serde", + "serde_json", +] + [[package]] name = "rustls" version = "0.20.6" diff --git a/Cargo.toml b/Cargo.toml index 4a3b24b..e37bc63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ clap = { version = "3.2.12", features = ["derive", "env"] } humantime-serde = "1.1.1" itertools = "0.10.3" rumqttc = { version = "0.13.0", features = ["url"], git = "https://github.com/bytebeamio/rumqtt" } +rust_decimal = { version = "1.26.1", features = ["serde-arbitrary-precision", "serde-float", "serde_json", "maths"] } serde = { version = "1.0.139", features = ["serde_derive"] } serde_json = "1.0.82" serialport = { version = "4.2.0", features = ["serde"] } diff --git a/src/main.rs b/src/main.rs index 3d0b177..8dfc58d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -375,92 +375,38 @@ async fn watch_registers( r.address.checked_sub(address_offset.unsigned_abs() as u16) }; if let Some(address) = address { + let size = r.parse.value_type.size(); debug!( - "Polling {:?} {} {}", - read_type, + name = r.name.as_ref().unwrap_or(&"".to_string()), address, - &r.name.as_ref().unwrap_or(&"".to_string()) + size, + register_type = ?read_type, + value_type = r.parse.value_type.type_name(), + "Polling register", ); let (tx, rx) = oneshot::channel(); modbus - .send(ModbusCommand::Read( - read_type, - address, - r.parse.value_type.size(), - tx, - )) + .send(ModbusCommand::Read(read_type, address, size, tx)) .await .unwrap(); - let values = rx.await.unwrap().unwrap(); + let words = rx.await.unwrap().unwrap(); - let swapped_values = if r.parse.swap_bytes.0 { - values.iter().map(|v| v.swap_bytes()).collect() - } else { - values.clone() - }; + let swapped_words = r.apply_swaps(&words); - let swapped_values = if r.parse.swap_words.0 { - swapped_values - .chunks_exact(2) - .flat_map(|chunk| vec![chunk[1], chunk[0]]) - .collect() - } else { - swapped_values - }; + let value = r.from_words(&swapped_words); - let bytes: Vec = swapped_values - .iter() - .flat_map(|v| v.to_ne_bytes()) - .collect(); + debug!( + name = r.name.as_ref().unwrap_or(&"".to_string()), + address, + %value, + raw = ?words, + "Received value", + ); - use crate::modbus::config::RegisterValueType as T; - use crate::modbus::config::{RegisterArray, RegisterNumeric as N, RegisterString}; - - let value = match r.parse.value_type { - T::Numeric { - ref of, - adjust: ref _adjust, - } => match of { - N::U8 => json!(bytes[1]), // or is it 0? - N::U16 => json!(swapped_values[0]), - N::U32 => { - json!(bytes.try_into().map(|bytes| u32::from_le_bytes(bytes)).ok()) - } - N::U64 => { - json!(bytes.try_into().map(|bytes| u64::from_le_bytes(bytes)).ok()) - } - N::I8 => json!(vec![bytes[1]] - .try_into() - .map(|bytes| i8::from_le_bytes(bytes))), - N::I16 => { - json!(bytes.try_into().map(|bytes| i16::from_le_bytes(bytes)).ok()) - } - N::I32 => { - json!(bytes.try_into().map(|bytes| i32::from_le_bytes(bytes)).ok()) - } - N::I64 => { - json!(bytes.try_into().map(|bytes| i64::from_le_bytes(bytes)).ok()) - } - N::F32 => { - json!(bytes.try_into().map(|bytes| f32::from_le_bytes(bytes)).ok()) - } - N::F64 => { - json!(bytes.try_into().map(|bytes| f64::from_le_bytes(bytes)).ok()) - } - }, - T::String(RegisterString { .. }) => { - json!(String::from_utf16_lossy(&swapped_values)) - } - T::Array(RegisterArray { .. }) => todo!(), - }; - - let payload = serde_json::to_vec( - &json!({ "raw": values, "swapped": swapped_values, "value": value }), - ) - .unwrap(); + let payload = serde_json::to_vec(&json!({ "value": value, "raw": words })).unwrap(); dispatcher .send(DispatchCommand::Publish { diff --git a/src/modbus/config.rs b/src/modbus/config.rs index 6769aaa..9210fdc 100644 --- a/src/modbus/config.rs +++ b/src/modbus/config.rs @@ -54,20 +54,17 @@ fn default_modbus_parity() -> tokio_serial::Parity { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "lowercase", default)] pub struct RegisterNumericAdjustment { - #[serde(default)] - scale: i8, // powers of 10 (0 = no adjustment, 1 = x10, -1 = /10) - - #[serde(default)] - offset: i8, + pub scale: i8, // powers of 10 (0 = no adjustment, 1 = x10, -1 = /10) + pub offset: i8, // precision: Option, } impl Default for RegisterNumericAdjustment { fn default() -> Self { Self { - scale: 1, + scale: 0, offset: 0, } } @@ -107,6 +104,10 @@ impl RegisterNumeric { U64 | I64 | F64 => 4, } } + + fn type_name(&self) -> String { + format!("{:?}", *self).to_lowercase() + } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -152,6 +153,16 @@ pub enum RegisterValueType { String(RegisterString), } +impl RegisterValueType { + pub fn type_name(&self) -> String { + match *self { + RegisterValueType::Numeric { ref of, .. } => of.type_name(), + RegisterValueType::Array(_) => "array".to_owned(), + RegisterValueType::String(_) => "string".to_owned(), + } + } +} + impl Default for RegisterValueType { fn default() -> Self { RegisterValueType::Numeric { @@ -175,6 +186,7 @@ impl RegisterValueType { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] pub struct Swap(pub bool); impl Default for Swap { @@ -404,7 +416,10 @@ fn parse_empty_register_parser_defaults() { swap_words: Swap(false), value_type: RegisterValueType::Numeric { of: RegisterNumeric::U16, - .. + adjust: RegisterNumericAdjustment { + scale: 0, + offset: 0, + } } } )); diff --git a/src/modbus/mod.rs b/src/modbus/mod.rs index 5a39435..a73b5db 100644 --- a/src/modbus/mod.rs +++ b/src/modbus/mod.rs @@ -1,5 +1,10 @@ +use rust_decimal::{prelude::FromPrimitive, Decimal}; use serde::Serialize; +use crate::modbus::config::RegisterNumericAdjustment; + +use self::config::{Register, RegisterValueType}; + pub mod config; #[derive(Serialize)] @@ -19,3 +24,129 @@ pub struct ConnectStatus { pub type UnitId = tokio_modbus::prelude::SlaveId; pub type Unit = tokio_modbus::prelude::Slave; + +impl RegisterValueType { + pub fn from_words(&self, words: &[u16]) -> serde_json::Value { + use self::config::RegisterValueType as T; + use self::config::{RegisterArray, RegisterNumeric as N, RegisterString}; + use serde_json::json; + + let bytes: Vec = words.iter().flat_map(|v| v.to_ne_bytes()).collect(); + + match *self { + T::Numeric { ref of, ref adjust } => { + use rust_decimal::MathematicalOps; + let scale: Decimal = Decimal::TEN.powi(adjust.scale.into()).normalize(); + let offset = Decimal::from(adjust.offset); + match of { + N::U8 => json!(scale * Decimal::from(bytes[1]) + offset), // or is it 0? + N::U16 => json!(scale * Decimal::from(words[0]) + offset), + N::U32 => { + json!(bytes + .try_into() + .map(|bytes| scale * Decimal::from(u32::from_le_bytes(bytes)) + offset) + .ok()) + } + N::U64 => { + json!(bytes + .try_into() + .map(|bytes| scale * Decimal::from(u64::from_le_bytes(bytes)) + offset) + .ok()) + } + N::I8 => { + json!(vec![bytes[1]] + .try_into() + .map(|bytes| scale * Decimal::from(i8::from_le_bytes(bytes)) + offset) + .ok()) + } + N::I16 => { + json!(bytes + .try_into() + .map(|bytes| scale * Decimal::from(i16::from_le_bytes(bytes)) + offset) + .ok()) + } + N::I32 => { + json!(bytes + .try_into() + .map(|bytes| scale * Decimal::from(i32::from_le_bytes(bytes)) + offset) + .ok()) + } + N::I64 => { + json!(bytes + .try_into() + .map(|bytes| scale * Decimal::from(i64::from_le_bytes(bytes)) + offset) + .ok()) + } + N::F32 => { + json!(bytes + .try_into() + .map(|bytes| scale + * Decimal::from_f32(f32::from_le_bytes(bytes)).unwrap() + + offset) + .ok()) + } + N::F64 => { + json!(bytes + .try_into() + .map(|bytes| scale + * Decimal::from_f64(f64::from_le_bytes(bytes)).unwrap() + + offset) + .ok()) + } + } + } + T::String(RegisterString { .. }) => { + json!(String::from_utf16_lossy(&words)) + } + T::Array(RegisterArray { .. }) => todo!(), + } + } +} + +impl Register { + pub fn from_words(&self, words: &[u16]) -> serde_json::Value { + self.parse.value_type.from_words(words) + } + + pub fn apply_swaps(&self, words: &[u16]) -> Vec { + let words: Vec = if self.parse.swap_bytes.0 { + words.iter().map(|v| v.swap_bytes()).collect() + } else { + words.into() + }; + + if self.parse.swap_words.0 { + words + .chunks_exact(2) + .flat_map(|chunk| vec![chunk[1], chunk[0]]) + .collect() + } else { + words + } + } +} + +#[test] +fn test_parse_1() { + use self::config::{RegisterParse, Swap}; + use serde_json::json; + + let reg = Register { + address: 42, + name: None, + interval: Default::default(), + parse: RegisterParse { + swap_bytes: Swap(false), + swap_words: Swap(false), + value_type: RegisterValueType::Numeric { + of: config::RegisterNumeric::I32, + adjust: RegisterNumericAdjustment { + scale: 0, + offset: 0, + }, + }, + }, + }; + + assert_eq!(reg.from_words(&vec![843, 0]), json!(843)); +}