From 3f0c0eeea83cec1329c8723fd5671f422a1deb8d Mon Sep 17 00:00:00 2001 From: "Michael P. Jung" Date: Wed, 11 Sep 2024 12:58:40 +0200 Subject: [PATCH] WiP groups --- Cargo.lock | 35 ++++++++- Cargo.toml | 1 + examples/model712/Cargo.toml | 14 ++++ examples/model712/README.md | 26 +++++++ examples/model712/src/main.rs | 68 +++++++++++++++++ src/model.rs | 2 + sunspec-gen/src/gen.rs | 135 ++++++++++++++++++++++++++++++++-- sunspec-gen/src/json.rs | 2 +- tests/test_model712.rs | 57 ++++++++++++++ 9 files changed, 331 insertions(+), 9 deletions(-) create mode 100644 examples/model712/Cargo.toml create mode 100644 examples/model712/README.md create mode 100644 examples/model712/src/main.rs create mode 100644 tests/test_model712.rs diff --git a/Cargo.lock b/Cargo.lock index 89a1543..44c72fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,7 +445,7 @@ dependencies = [ "strum", "thiserror", "tokio", - "tokio-modbus", + "tokio-modbus 0.15.0", ] [[package]] @@ -456,7 +456,18 @@ dependencies = [ "itertools", "sunspec", "tokio", - "tokio-modbus", + "tokio-modbus 0.15.0", +] + +[[package]] +name = "sunspec-example-model712" +version = "0.1.0" +dependencies = [ + "clap", + "itertools", + "sunspec", + "tokio", + "tokio-modbus 0.14.0", ] [[package]] @@ -467,7 +478,7 @@ dependencies = [ "itertools", "sunspec", "tokio", - "tokio-modbus", + "tokio-modbus 0.15.0", ] [[package]] @@ -544,6 +555,24 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-modbus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033b1b9843d693c3543e6b9c656a566ea45d2564e72ad5447e83233b9e2f3fe1" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "futures-core", + "futures-util", + "log", + "smallvec", + "thiserror", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-modbus" version = "0.15.0" diff --git a/Cargo.toml b/Cargo.toml index 807c89f..67ae35d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,5 @@ members = [ "sunspec-gen", "examples/readme", "examples/model103", + "examples/model712", ] diff --git a/examples/model712/Cargo.toml b/examples/model712/Cargo.toml new file mode 100644 index 0000000..f2f972b --- /dev/null +++ b/examples/model712/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "sunspec-example-model712" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros", "time"] } +tokio-modbus = { version = "0.15", features = ["tcp"] } +sunspec = { path = "../../" } +clap = { version = "4.4.7", features = ["derive"] } +itertools = "0.13.0" diff --git a/examples/model712/README.md b/examples/model712/README.md new file mode 100644 index 0000000..9418a87 --- /dev/null +++ b/examples/model712/README.md @@ -0,0 +1,26 @@ +# Model 103 example + +This code connects to a device that supports sunspec via modbus TCP and +outputs the contents of model 1 and then proceeds reading model 103 +(three phase inverter) outputting the value "W / W_SF" and "WH / WH_SF" +every second. + +Usage example: + +``` +$ cargo run 192.168.178.38:1502 1 +``` + +Example output: + +``` +Manufacturer: SolarEdge +Model: SE25K-RW00IBNM4 +Version: 0004.0018.0518 +Serial Number: -redacted- +Supported models: 1, 103 + 157.185 kWh 2.292 kW + 157.185 kWh 2.269 kW + 157.186 kWh 2.270 kW +... +``` diff --git a/examples/model712/src/main.rs b/examples/model712/src/main.rs new file mode 100644 index 0000000..5c7714b --- /dev/null +++ b/examples/model712/src/main.rs @@ -0,0 +1,68 @@ +use std::{error::Error, net::SocketAddr, time::Duration}; + +use clap::Parser; +use itertools::Itertools; +use sunspec::{ + client::{AsyncClient, Config}, models::{model1::Model1, model712::Model712}, DEFAULT_DISCOVERY_ADDRESSES +}; +use tokio_modbus::{client::tcp::connect_slave, Slave}; + +#[derive(Parser)] +struct Args { + addr: SocketAddr, + device_id: u8, + #[arg( + long, + short='d', + help = "Discovery addresses", + name = "ADDRESS", + default_values_t = DEFAULT_DISCOVERY_ADDRESSES + )] + discovery_addresses: Vec, + #[arg( + long, + short = 't', + help = "Read timeout in seconds", + name = "SECONDS", + default_value_t = 1.0 + )] + read_timeout: f32, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + let mut client = AsyncClient::new( + connect_slave(args.addr, Slave(args.device_id)).await?, + Config { + discovery_addresses: args.discovery_addresses, + read_timeout: (args.read_timeout != 0.0) + .then(|| Duration::from_secs_f32(args.read_timeout)), + ..Default::default() + }, + ) + .await?; + + let m1: Model1 = client.read_model().await?; + + println!("Manufacturer: {}", m1.mn); + println!("Model: {}", m1.md); + println!("Version: {}", m1.vr.as_deref().unwrap_or("(unspecified)")); + println!("Serial Number: {}", m1.sn); + + println!( + "Supported models: {}", + client + .models + .supported_model_ids() + .iter() + .map(|id| id.to_string()) + .join(", ") + ); + + let m712: Model712 = client.read_model().await?; + println!("{:?}", m712); + + Ok(()) +} diff --git a/src/model.rs b/src/model.rs index b873ec1..cb2887d 100644 --- a/src/model.rs +++ b/src/model.rs @@ -7,6 +7,8 @@ use crate::{DecodeError, Models}; pub trait Model: Sized { /// Model ID const ID: u16; + /// Model length + const LEN: u16; /// Parse model points from a given u16 slice fn from_data(data: &[u16]) -> Result; /// Get model address from discovered models struct diff --git a/sunspec-gen/src/gen.rs b/sunspec-gen/src/gen.rs index 7224789..25a3106 100644 --- a/sunspec-gen/src/gen.rs +++ b/sunspec-gen/src/gen.rs @@ -2,7 +2,7 @@ use proc_macro2::{Literal, TokenStream}; use quote::{format_ident, quote, ToTokens}; use thiserror::Error; -use crate::json::{Model, Point, PointAccess, PointMandatory, PointType}; +use crate::json::{Group, GroupCount, Model, Point, PointAccess, PointMandatory, PointType}; /// This fixes sunspec identifiers which contains things like /// `SoC` and `SoH` which causes heck to transform them to `so_c` @@ -130,7 +130,7 @@ pub fn gen_model_struct(model: &Model) -> Result { } } let points = &model.group.points[2..]; - let model_fields = points + let point_fields = points .iter() .filter(|point| !point.is_padding()) .map(|point| { @@ -144,13 +144,33 @@ pub fn gen_model_struct(model: &Model) -> Result { } }); + let groups = model + .group + .groups + .iter() + // XXX It's quite unfortunate but groups with a count of 0 + // aren't defined properly in the sunspec JSON files. + .filter(|group| group.count != GroupCount::Integer(0)) + .collect::>(); + + let group_fields = groups.iter().map(|group| { + let field_name = format_ident!("{}", snake_case(&group.name)); + let group_type = format_ident!("{}", upper_camel_case(&group.name)); + let group_doc = doc_to_ts(&group.doc.to_doc_string()); + quote! { + #group_doc + pub #field_name: Vec<#group_type>, + } + }); + // FIXME do not add empty model docs let model_struct = quote! { #model_doc #[derive(Debug)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct #model_name { - #(#model_fields)* + #(#point_fields)* + #(#group_fields)* } }; @@ -191,16 +211,35 @@ pub fn gen_model_struct(model: &Model) -> Result { } }); + let from_data_groups_default = groups.iter().map(|group| { + let field_name = format_ident!("{}", snake_case(&group.name)); + quote! { + #field_name: Vec::new(), + } + }); + + let from_data_groups = groups.iter().map(|group| { + let group_type = format_ident!("{}", upper_camel_case(&group.name)); + quote! { + let data = #group_type::load(data, &mut model)?; + } + }); + let model_id = Literal::u16_unsuffixed(model.id); + let model_len = Literal::u16_unsuffixed(points.iter().map(|point| point.size).sum()); let allow_unused = points.is_empty().then(|| quote! { #[allow(unused)] }); let m_name = format_ident!("m{}", model.id); let trait_impl = quote! { impl crate::Model for #model_name { const ID: u16 = #model_id; + const LEN: u16 = #model_len; fn from_data(#allow_unused data: &[u16]) -> Result { - Ok(Self { + let model = Self { #(#from_data_fields)* - }) + #(#from_data_groups_default)* + }; + //#( #from_data_groups )* + Ok(model) } fn addr(models: &crate::Models) -> crate::ModelAddr { models.#m_name @@ -222,6 +261,10 @@ pub fn gen_model_struct(model: &Model) -> Result { } } + for group in groups { + extra.extend(gen_group(model, group, "")); + } + Ok(quote! { #![doc = #module_doc] #model_struct @@ -231,6 +274,88 @@ pub fn gen_model_struct(model: &Model) -> Result { }) } +fn gen_group(model: &Model, group: &Group, prefix: &str) -> TokenStream { + let mut extra = TokenStream::new(); + // group structure + let identifier = format_ident!( + "{}{}", + upper_camel_case(&prefix), + upper_camel_case(&group.name) + ); + let prefix = format!("{}{}", prefix, group.name); + let points = group + .points + .iter() + .filter(|point| !point.is_padding()) + .map(|point| { + let point_name = format_ident!("{}", snake_case(&point.name)); + let point_type = rust_type(point, &group.name); + let point_doc = doc_to_ts(&point.doc.to_doc_string()); + quote! { + #point_doc + pub #point_name: #point_type, + } + }); + let group_doc = doc_to_ts(&group.doc.to_doc_string()); + let group_struct = quote! { + #group_doc + #[derive(Debug)] + #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] + pub struct #identifier { + #(#points)* + } + }; + + let model_type = format_ident!("Model{}", model.id); + /* + let GroupCount::String(count_field) = &group.count else { + todo!( + "Non string counts are unsupported: {} {} {:?}", + model.group.name, + group.name, + group.count + ); + }; + let count_field = format_ident!("{}", count_field.to_snake_case()); + */ + let group_impl = quote! { + impl #identifier { + fn load<'a>(data: &'a [u16], _model: &mut #model_type) -> Result<&'a [u16], crate::DecodeError> { + //let length = model.#count_field; + // FIXME implement actual loading + Ok(data) + } + } + }; + + // Generate enums and bitfields + for point in &group.points { + match point.r#type { + PointType::Enum16 | PointType::Enum32 if !point.symbols.is_empty() => { + extra.extend(gen_enum(point, &prefix)); + } + PointType::Bitfield16 | PointType::Bitfield32 | PointType::Bitfield64 => { + extra.extend(gen_bitfield(point, &prefix)); + } + _ => {} + } + } + + for group in &group.groups { + extra.extend(gen_group( + model, + group, + &format!("{}{}", &prefix, group.name), + )); + } + + quote! { + #group_struct + #group_impl + #extra + } +} + fn gen_enum(point: &Point, prefix: &str) -> TokenStream { let size = point.r#type.size().unwrap(); let repr = format_ident!("u{}", size * 16); diff --git a/sunspec-gen/src/json.rs b/sunspec-gen/src/json.rs index 2a06d75..a09bb29 100644 --- a/sunspec-gen/src/json.rs +++ b/sunspec-gen/src/json.rs @@ -64,7 +64,7 @@ pub enum GroupType { Sync, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(untagged)] pub enum GroupCount { Integer(u32), diff --git a/tests/test_model712.rs b/tests/test_model712.rs new file mode 100644 index 0000000..cb33be6 --- /dev/null +++ b/tests/test_model712.rs @@ -0,0 +1,57 @@ +use sunspec::{ + models::model712::{self, Model712}, + Model, +}; + +#[test] +fn test_model712() { + #[rustfmt::skip] + let data = &[ + 0, // Ena + 0, // adpt_crv_req + 1, // adpt_crv_rslt + 6, // n_pt + 2, // n_crv + 65535, 65535, // rvrt_tms + 65535, 65535, // rvrt_rem + 65535, // rvrt_crv + 65534, // w_sf + 65534, // dept_ref_sf + 6, 0, 65535, 1, // crv[0] + 0, 0, // crv[0].pt[0] + 5000, 0, // crv[0].pt[1] + 6000, 0, // crv[0].pt[2] + 8000, 0, // crv[0].pt[3] + 9000, 0, // crv[0].pt[4] + 10000, 0, // crv[0].pt[5] + 0, 0, 65535, 0, // crv[1] + 0, 0, // crv[1].pt[0] + 0, 0, // crv[1].pt[1] + 0, 0, // crv[1].pt[2] + 0, 0, // crv[1].pt[3] + 0, 0, // crv[1].pt[4] + 0, 0, // crv[1].pt[5] + ]; + let m = Model712::from_data(data).unwrap(); + assert_eq!(m.ena, model712::Ena::Disabled); + assert_eq!(m.adpt_crv_req, 0); + assert_eq!(m.adpt_crv_rslt, model712::AdptCrvRslt::Completed); + assert_eq!(m.n_pt, 6); + assert_eq!(m.n_crv, 2); + assert_eq!(m.rvrt_tms, None); + assert_eq!(m.rvrt_rem, None); + assert_eq!(m.rvrt_crv, None); + assert_eq!(m.w_sf, -2); + assert_eq!(m.dept_ref_sf, -2); + assert_eq!(m.crv.len(), 2); + assert_eq!(m.crv[0].act_pt, 6); + assert_eq!(m.crv[0].dept_ref, model712::CrvDeptRef::WMaxPct); + assert_eq!(m.crv[0].pri, None); + assert_eq!(m.crv[0].read_only, model712::CrvReadOnly::R); + // FIXME check the points, too + assert_eq!(m.crv[1].act_pt, 0); + assert_eq!(m.crv[1].dept_ref, model712::CrvDeptRef::WMaxPct); + assert_eq!(m.crv[1].pri, None); + assert_eq!(m.crv[1].read_only, model712::CrvReadOnly::Rw); + todo!(); +}