sunspec 0.9.0

SunSpec 1.1 compliant library with tokio support
Documentation
# SunSpec Rust Implementation

[![Latest Version](https://img.shields.io/crates/v/sunspec.svg)](https://crates.io/crates/sunspec)
[![CI](https://img.shields.io/github/actions/workflow/status/bikeshedder/sunspec/rust.yml?logo=github&label=CI)](https://github.com/bikeshedder/sunspec/actions?query=workflow%3ARust)
![Unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg "Unsafe forbidden")
[![Rust 1.76+](https://img.shields.io/badge/rustc-1.76+-lightgray.svg "Rust 1.76+")](https://blog.rust-lang.org/2024/02/08/Rust-1.76.0/)

This [Rust](https://www.rust-lang.org) crate contains code for accessing [SunSpec](https://en.wikipedia.org/wiki/SunSpec) compliant devices
in a safe and convenient way.

## Highlights

- [x] Pure Rust library
- [x] No unsafe code
- [x] Panic free
- [x] All communication is abstracted via traits making it runtime agnostic
- [x] Supports Modbus TCP and RTU (via [tokio-modbus]https://crates.io/crates/tokio-modbus).
- [x] `tokio-modbus` is optional. Custom transports can implement the client trait directly.
- [x] Implements "Device Information Model Discovery" as
  defined in the SunSpec specification.
- [x] Compile-time model selection via Cargo features
- [x] Fully typed models generated from the JSON files contained in the
  [SunSpec models repository]https://github.com/sunspec/models/
- [x] Fully typed enums
- [x] Fully typed bitfields
- [x] Fully documented. Even the generated models.
- [x] Reading of complete models in a single request.
- [x] Supports nested and repeating groups.
- [x] Unknown or unsupported models are reported during discovery.

## Features

| Feature        | Description                   | Extra dependencies        | Default |
| -------------- | ----------------------------- | ------------------------- | ------- |
| `tokio`        | Enable tokio-based timeouts   | `tokio`, `tokio/time`     | yes     |
| `tokio-modbus` | Enable `tokio-modbus` support | `tokio-modbus`, `tokio`   | yes     |
| `serde`        | Enable `serde` support        | `serde`, `bitflags/serde` | yes     |
| `all-models`   | Enable all generated models   | `model1`, `model2`, ...   | yes     |
| `model<X>`     | Enable generated model `X`    | _none_                    | yes     |

If you only need a small subset of models, disable default features and opt in
to the features and specific models you need:

```toml
[dependencies]
sunspec = { version = "...", default-features = false, features = ["tokio-modbus", "model1", "model103"] }
```

## Examples

The `examples` directory in the code repository contains the unabridged code.

- `examples/readme`: minimal end-to-end example used in this README
- `examples/model103`: reading a common inverter model from a device
- `examples/model712`: reading a model with nested and repeating groups

### Example code for accessing data from a three phase inverter using the model 103

```rust,ignore
use std::{error::Error, net::SocketAddr, time::Duration};

use clap::Parser;
use itertools::Itertools;
use sunspec::{
    client::{AsyncClient, Config},
    models::{model1::Model1, model103::Model103},
};
use tokio::time::sleep;
use tokio_modbus::client::tcp::connect;

#[derive(Parser)]
struct Args {
    addr: SocketAddr,
    device_id: u8,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let args = Args::parse();

    let client = AsyncClient::new(connect(args.addr).await?, Config::default());
    let device = client.device(args.device_id).await?;

    let m1: Model1 = device.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: {}",
        device
            .models
            .supported_model_ids()
            .iter()
            .map(|id| id.to_string())
            .join(", ")
    );

    loop {
        let m103: Model103 = device.read_model().await?;
        let w = m103.w as f32 * 10f32.powf(m103.w_sf.into());
        let wh = m103.wh as f32 * 10f32.powf(m103.wh_sf.into());
        println!("{:12.3} kWh {:9.3} kW", wh / 1000.0, w / 1000.0,);
        sleep(Duration::from_secs(1)).await;
    }
}
```

## FAQ

How does this crate differ from crates like `tokio-sunspec`, `sunspec-models`, `sunspec_rs`?

- This crate generates all code using Rust code via the official
  [SunSpec models repository]https://github.com/sunspec/models/
  with a code generator that was written in Rust, too.

- All generated models are plain Rust structs. A single Modbus call
  can return the complete data for a model rather than having to fetch
  points individually.

- All public types are documented. Even the generated models.

- Full support for nested and repeating groups.

How do I reduce compile times or binary size?

- Disable default features and enable only the features you need.
- This is is useful if you interact only with a small number of models.

Do I have to use `tokio-modbus`?

- No. `tokio-modbus` is just the bundled transport adapter.
- You can provide your own transport by implementing the async client trait and
  constructing an `AsyncClient` with it.
- The separate `tokio` feature only controls tokio-based timeout handling.

What happens if a device exposes models this crate does not know?

- Discovery still succeeds.
- Known models are stored in `device.models`.
- Unknown model ids, addresses, and lengths are returned in `device.unknown_models`.

Can I scan for all slave IDs on a bus?

- Yes. `AsyncClient::devices()` probes slave ids `0..=255` and returns every
  device that responds with a valid SunSpec header.
- If you already know the slave id, use `AsyncClient::device(slave_id)` instead.

How do discovery addresses and timeouts work?

- The default discovery addresses are `[40000, 0, 50000]`.
- That order avoids unnecessary timeouts on devices that do not behave well at
  address `0`.
- You can override discovery addresses and read/write timeouts via `Config`.

How are large models handled?

- Models larger than the Modbus single-request limit are read in chunks and then
  decoded as one typed model value.
- Nested groups and repeating groups are handled by the generated model code.

## License

Licensed under either of

- Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0)>
- MIT license ([LICENSE-MIT]LICENSE-MIT or <http://opensource.org/licenses/MIT)>

at your option.