Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for nested and repeating groups #5

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ members = [
"sunspec-gen",
"examples/readme",
"examples/model103",
"examples/model712",
]
14 changes: 14 additions & 0 deletions examples/model712/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
26 changes: 26 additions & 0 deletions examples/model712/README.md
Original file line number Diff line number Diff line change
@@ -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
...
```
68 changes: 68 additions & 0 deletions examples/model712/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<u16>,
#[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<dyn Error>> {
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(())
}
2 changes: 2 additions & 0 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, DecodeError>;
/// Get model address from discovered models struct
Expand Down
135 changes: 130 additions & 5 deletions sunspec-gen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -130,7 +130,7 @@ pub fn gen_model_struct(model: &Model) -> Result<TokenStream, GenModelError> {
}
}
let points = &model.group.points[2..];
let model_fields = points
let point_fields = points
.iter()
.filter(|point| !point.is_padding())
.map(|point| {
Expand All @@ -144,13 +144,33 @@ pub fn gen_model_struct(model: &Model) -> Result<TokenStream, GenModelError> {
}
});

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::<Vec<_>>();

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)*
}
};

Expand Down Expand Up @@ -191,16 +211,35 @@ pub fn gen_model_struct(model: &Model) -> Result<TokenStream, GenModelError> {
}
});

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<Self, crate::DecodeError> {
Ok(Self {
let model = Self {
#(#from_data_fields)*
})
#(#from_data_groups_default)*
};
//#( #from_data_groups )*
Ok(model)
}
fn addr(models: &crate::Models) -> crate::ModelAddr<Self> {
models.#m_name
Expand All @@ -222,6 +261,10 @@ pub fn gen_model_struct(model: &Model) -> Result<TokenStream, GenModelError> {
}
}

for group in groups {
extra.extend(gen_group(model, group, ""));
}

Ok(quote! {
#![doc = #module_doc]
#model_struct
Expand All @@ -231,6 +274,88 @@ pub fn gen_model_struct(model: &Model) -> Result<TokenStream, GenModelError> {
})
}

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);
Expand Down
2 changes: 1 addition & 1 deletion sunspec-gen/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading