tokio_sunspec/
lib.rs

1pub mod error;
2pub mod model;
3pub mod models;
4pub mod point;
5pub mod types;
6pub mod utils;
7
8use error::Error;
9use model::Model;
10use point::{Point, PointType};
11use std::{collections::HashMap, net::SocketAddr};
12use tokio_modbus::{client::Context, prelude::*};
13use tokio_serial::SerialStream;
14use types::Address;
15
16pub struct Client {
17    /// Slave Id of device
18    pub slave_id: u8,
19
20    /// Address where the sunspec models start
21    pub start_address: Address,
22
23    /// Contains all discovered models. Key = Model id, Value = Start address
24    pub models: HashMap<u16, Address>,
25
26    /// Modbus client
27    modbus_client: Context,
28}
29
30#[cfg(feature = "tcp")]
31pub async fn connect_tcp(
32    socket_addr: SocketAddr,
33    slave_id: u8,
34    start_address: Address,
35) -> Result<Client, Error> {
36    let modbus_client = tcp::connect_slave(socket_addr, Slave(slave_id))
37        .await
38        .map_err(Error::Io)?;
39
40    return connect(modbus_client, slave_id, start_address).await;
41}
42
43#[cfg(feature = "rtu")]
44pub async fn connect_rtu(
45    device_path: &str,
46    baud_rate: u32,
47    slave_id: u8,
48    start_address: Address,
49) -> Result<Client, Error> {
50    let builder = tokio_serial::new(device_path, baud_rate);
51    let serial = SerialStream::open(&builder).unwrap();
52    let modbus_client = rtu::connect_slave(serial, Slave(slave_id))
53        .await
54        .map_err(Error::Io)?;
55
56    return connect(modbus_client, slave_id, start_address).await;
57}
58
59pub async fn connect(
60    client: Context,
61    slave_id: u8,
62    start_address: Address,
63) -> Result<Client, Error> {
64    let mut client = Client {
65        slave_id,
66        start_address,
67        models: HashMap::new(),
68        modbus_client: client,
69    };
70
71    client.model_discovery().await?;
72    return Ok(client);
73}
74
75impl Client {
76    /// Discover the supported models of the connected device.
77    async fn model_discovery(&mut self) -> Result<(), Error> {
78        let mut base_addr = self.start_address;
79
80        // Check for Sunspec identifier
81        let res = self
82            .read_holding_registers(base_addr, 2)
83            .await
84            .expect("SunS identifier");
85
86        if res != vec![0x5375, 0x6e53] {
87            return Err(Error::Client());
88        }
89        base_addr += 2;
90
91        // Scan supported models
92        loop {
93            let res = self.read_holding_registers(base_addr, 2).await?;
94            let model_id = res[0];
95            let model_length = res[1];
96
97            if model_id == 0xFFFF || model_length == 0xFFFF {
98                return Ok(()); // Last model reached. We are done parsing.
99            }
100            self.models.insert(model_id, base_addr + 2);
101
102            base_addr += 2; // increase by two register which we were reading earlier
103            base_addr += model_length; // increase by length of model to get to next model
104        }
105    }
106
107    /// Easy access to modbus `read_holding_registers`.
108    async fn read_holding_registers(&mut self, addr: Address, cnt: u16) -> Result<Vec<u16>, Error> {
109        return self
110            .modbus_client
111            .read_holding_registers(addr, cnt)
112            .await
113            .map_err(Error::Io);
114    }
115
116    /// Easy access to modbus `write_multiple_registers`.
117    async fn write_holding_registers(
118        &mut self,
119        addr: Address,
120        data: Vec<u16>,
121    ) -> Result<(), Error> {
122        return self
123            .modbus_client
124            .write_multiple_registers(addr, &data)
125            .await
126            .map_err(Error::Io);
127    }
128}
129
130impl Client {
131    /// Read the data for the given point
132    pub async fn read_point<T: Model, K: PointType<K>>(
133        &mut self,
134        point: Point<T, K>,
135    ) -> Result<K, Error> {
136        if let Some(model_addr) = self.models.get(&T::ID) {
137            let address = *model_addr + point.offset;
138            return self
139                .read_holding_registers(address, point.length)
140                .await
141                .and_then(|res| Ok(K::decode(res)));
142        }
143
144        return Err(Error::UnsupportedModel(T::ID));
145    }
146
147    /// Write data to the given point
148    pub async fn write_point<T: Model, K: PointType<K>>(
149        &mut self,
150        point: Point<T, K>,
151        data: K,
152    ) -> Result<(), Error> {
153        if !point.write_access {
154            return Err(Error::WriteNotSupported());
155        }
156
157        if let Some(model_addr) = self.models.get(&T::ID) {
158            let address = *model_addr + point.offset;
159            let buff = K::encode(data);
160            return self.write_holding_registers(address, buff).await;
161        }
162
163        return Err(Error::UnsupportedModel(T::ID));
164    }
165}