Inline register type to Register struct
parent
bb715f30b5
commit
120f62f0c7
|
@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning].
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
- ...
|
### Deprecated
|
||||||
|
|
||||||
|
- Separate `holding` and `input` sections, in favour of specifying `register_type` field on the register definition to either `"input"` (default) or `"holding"`.
|
||||||
|
|
||||||
## [0.2.0] - 2022-09-09
|
## [0.2.0] - 2022-09-09
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,10 @@ Post to `$MODBUS_MQTT_TOPIC/$CONNECTION_ID/$TYPE/$ADDRESS` where `$TYPE` is one
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
|
"address": 5123, // REQUIRED
|
||||||
|
|
||||||
|
"register_type": "input", // OPTIONAL
|
||||||
|
|
||||||
"name": null, // OPTIONAL - gives the register a name which is used in the register MQTT topics (must be a valid topic component)
|
"name": null, // OPTIONAL - gives the register a name which is used in the register MQTT topics (must be a valid topic component)
|
||||||
|
|
||||||
"interval": "1m", // OPTIONAL - how often to update the registers value to MQTT
|
"interval": "1m", // OPTIONAL - how often to update the registers value to MQTT
|
||||||
|
@ -123,23 +127,12 @@ Post to `$MODBUS_MQTT_TOPIC/$CONNECTION_ID/$TYPE/$ADDRESS` where `$TYPE` is one
|
||||||
// to turn W into kW, you would provide scale=-3
|
// to turn W into kW, you would provide scale=-3
|
||||||
|
|
||||||
"offset": 0, // OPTIONAL - will be added to the final result (AFTER scaling)
|
"offset": 0, // OPTIONAL - will be added to the final result (AFTER scaling)
|
||||||
|
|
||||||
|
|
||||||
// Additionally, "type" can be set to "array":
|
|
||||||
"type": "array",
|
|
||||||
"of": "u16" // The default array element is u16, but you can change it with the `of` field
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Further, the `type` field can additionally be set to `"array"`, in which case, a `count` field must be provided. The array elements default to `"s16"` but can be overriden in the `"of"` field.
|
|
||||||
|
|
||||||
NOTE: this is likely to change such that there is always a `count` field (with default of 1) and if provided to be greater than 1, it will be interpreted to be an array of elements of the `type` specified.
|
|
||||||
|
|
||||||
There is some code to accept `"string"` type (with a required `length` field) but this is experimental and untested.
|
|
||||||
|
|
||||||
##### Register shorthand
|
##### Register shorthand
|
||||||
|
|
||||||
When issuing the `connect` payload, you can optionally include `input` and/or `holding` fields as arrays containing the above register schema, as long as an `address` field is added. When present, these payloads will be replayed to the MQTT server as if the user had specified each register separately, as above.
|
When issuing the `connect` payload, you can optionally include a top-level `registers` array, containing the above register schema. When present, these payloads will be replayed to the MQTT server as if the user had specified each register separately, as above.
|
||||||
|
|
||||||
This is a recommended way to specify connections, but the registers are broken out separately so that they can be dynamically added to too.
|
This is a recommended way to specify connections, but the registers are broken out separately so that they can be dynamically added to too.
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"host": "10.10.10.219",
|
"host": "10.10.10.219",
|
||||||
"unit": 1,
|
"unit": 1,
|
||||||
"proto": "winet-s",
|
"proto": "winet-s",
|
||||||
"input": [
|
"registers": [
|
||||||
{
|
{
|
||||||
"address": 13000,
|
"address": 13000,
|
||||||
"type": "u16",
|
"type": "u16",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"unit": 1,
|
"unit": 1,
|
||||||
"proto": "tcp",
|
"proto": "tcp",
|
||||||
"address_offset": -1,
|
"address_offset": -1,
|
||||||
"input": [
|
"registers": [
|
||||||
{
|
{
|
||||||
"address": 5017,
|
"address": 5017,
|
||||||
"type": "u32",
|
"type": "u32",
|
||||||
|
@ -87,25 +87,27 @@
|
||||||
"address": 5013,
|
"address": 5013,
|
||||||
"name": "mppt2_current"
|
"name": "mppt2_current"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"holding": [
|
|
||||||
{
|
{
|
||||||
|
"register_type": "holding",
|
||||||
"address": 13058,
|
"address": 13058,
|
||||||
"name": "max_soc",
|
"name": "max_soc",
|
||||||
"period": "90s",
|
"period": "90s",
|
||||||
"scale": -1
|
"scale": -1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"register_type": "holding",
|
||||||
"address": 13059,
|
"address": 13059,
|
||||||
"name": "min_soc",
|
"name": "min_soc",
|
||||||
"period": "90s",
|
"period": "90s",
|
||||||
"scale": -1
|
"scale": -1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"register_type": "holding",
|
||||||
"address": 13100,
|
"address": 13100,
|
||||||
"name": "battery_reserve"
|
"name": "battery_reserve"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"register_type": "holding",
|
||||||
"address": 33148,
|
"address": 33148,
|
||||||
"name": "forced_battery_power",
|
"name": "forced_battery_power",
|
||||||
"scale": 1
|
"scale": 1
|
||||||
|
|
|
@ -130,22 +130,12 @@ impl Connection {
|
||||||
select! {
|
select! {
|
||||||
Some(cmd) = self.rx.recv() => { self.process_command(cmd).await; },
|
Some(cmd) = self.rx.recv() => { self.process_command(cmd).await; },
|
||||||
|
|
||||||
Some((reg_type, reg)) = registers_rx.recv() => {
|
Some(register) = registers_rx.recv() => {
|
||||||
debug!(?reg_type, ?reg);
|
debug!(?register);
|
||||||
let scope = format!(
|
let mqtt = self.mqtt.scoped("registers");
|
||||||
"{}/{}",
|
|
||||||
match ®_type {
|
|
||||||
RegisterType::Input => "input",
|
|
||||||
RegisterType::Holding => "holding",
|
|
||||||
},
|
|
||||||
reg.address
|
|
||||||
);
|
|
||||||
let mqtt = self.mqtt.scoped(scope);
|
|
||||||
let modbus = self.handle();
|
let modbus = self.handle();
|
||||||
register::Monitor::new(
|
register::Monitor::new(
|
||||||
reg.register,
|
register,
|
||||||
reg_type,
|
|
||||||
reg.address,
|
|
||||||
mqtt,
|
mqtt,
|
||||||
modbus,
|
modbus,
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::modbus::{connection, register};
|
||||||
use crate::mqtt::{Payload, Scopable};
|
use crate::mqtt::{Payload, Scopable};
|
||||||
use crate::{mqtt, shutdown::Shutdown};
|
use crate::{mqtt, shutdown::Shutdown};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::value::RawValue as RawJSON;
|
use serde_json::value::Value as JSON;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
@ -81,34 +81,45 @@ async fn parse_and_connect(
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn connect(config: Config<'_>, mqtt: mqtt::Handle, shutdown: Shutdown) -> crate::Result<()> {
|
async fn connect(config: Config, mqtt: mqtt::Handle, shutdown: Shutdown) -> crate::Result<()> {
|
||||||
if shutdown.is_shutdown() {
|
if shutdown.is_shutdown() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
let Config {
|
let Config {
|
||||||
connection: settings,
|
connection: settings,
|
||||||
input,
|
input,
|
||||||
holding,
|
holding,
|
||||||
|
registers,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
let _ = connection::run(settings, mqtt.clone(), shutdown).await?;
|
let _ = connection::run(settings, mqtt.clone(), shutdown).await?;
|
||||||
|
|
||||||
// TODO: consider waiting 1 second before sending the registers to MQTT, to ensure that the connection is listening.
|
// TODO: consider waiting 1 second before sending the registers to MQTT, to ensure that the connection is listening.
|
||||||
|
|
||||||
for (reg_type, registers) in [("holding", holding), ("input", input)] {
|
enum Type {
|
||||||
let mqtt = mqtt.scoped(reg_type);
|
Holding,
|
||||||
|
Input,
|
||||||
|
Unchanged,
|
||||||
|
}
|
||||||
|
for (reg_type, registers) in [
|
||||||
|
(Type::Holding, holding),
|
||||||
|
(Type::Input, input),
|
||||||
|
(Type::Unchanged, registers),
|
||||||
|
] {
|
||||||
|
use register::*;
|
||||||
|
let mqtt = mqtt.scoped("registers");
|
||||||
for reg in registers {
|
for reg in registers {
|
||||||
if let Ok(r) =
|
if let Ok(mut reg) = serde_json::from_value::<Register>(reg) {
|
||||||
serde_json::from_slice::<register::AddressedRegister>(reg.get().as_bytes())
|
reg.register_type = match reg_type {
|
||||||
{
|
Type::Holding => RegisterType::Holding,
|
||||||
let json = serde_json::to_vec(&r.register).unwrap(); // unwrap() should be fine because we JUST deserialized it successfully
|
Type::Input => RegisterType::Input,
|
||||||
mqtt.publish(r.address.to_string(), json).await?;
|
Type::Unchanged => reg.register_type,
|
||||||
// if let Some(name) = r.register.name {
|
};
|
||||||
// r.register.name = None;
|
|
||||||
// let json = serde_json::to_vec(&r).unwrap(); // unwrap() should be fine because we JUST deserialized it successfully
|
let json = serde_json::to_vec(®).unwrap(); // unwrap() should be fine because we JUST deserialized it successfully
|
||||||
// mqtt.publish(name, json).await?;
|
mqtt.publish(format!("{}/config", reg.path()), json).await?;
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,15 +130,22 @@ async fn connect(config: Config<'_>, mqtt: mqtt::Handle, shutdown: Shutdown) ->
|
||||||
/// Wrapper around `modbus::connection::Config` that can include some registers inline, which the connector will
|
/// Wrapper around `modbus::connection::Config` that can include some registers inline, which the connector will
|
||||||
/// re-publish to the appropriate topic once the connection is established.
|
/// re-publish to the appropriate topic once the connection is established.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Config<'a> {
|
struct Config {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
connection: connection::Config,
|
connection: connection::Config,
|
||||||
|
|
||||||
// Allow registers to be defined inline, but capture them as raw JSON so that if they have incorrect schema, we can
|
// Allow registers to be defined inline, but capture them as raw JSON so that if they have incorrect schema, we can
|
||||||
// still establish the Modbus connection. Valid registers will be re-emitted as individual register configs to MQTT,
|
// still establish the Modbus connection. Valid registers will be re-emitted as individual register configs to MQTT,
|
||||||
// to be picked up by the connection.
|
// to be picked up by the connection.
|
||||||
#[serde(default, borrow)]
|
#[deprecated]
|
||||||
pub input: Vec<&'a RawJSON>,
|
#[serde(default)]
|
||||||
#[serde(alias = "hold", default, borrow)]
|
input: Vec<JSON>,
|
||||||
pub holding: Vec<&'a RawJSON>,
|
|
||||||
|
#[deprecated]
|
||||||
|
#[serde(alias = "hold", default)]
|
||||||
|
holding: Vec<JSON>,
|
||||||
|
|
||||||
|
#[deprecated]
|
||||||
|
#[serde(default)]
|
||||||
|
registers: Vec<JSON>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use super::Word;
|
use super::Word;
|
||||||
|
use crate::mqtt::{self, Payload, Scopable};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
select,
|
|
||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
time::{interval, MissedTickBehavior},
|
time::{interval, MissedTickBehavior},
|
||||||
};
|
};
|
||||||
|
@ -11,25 +11,15 @@ use tracing::{debug, warn};
|
||||||
pub struct Monitor {
|
pub struct Monitor {
|
||||||
mqtt: mqtt::Handle,
|
mqtt: mqtt::Handle,
|
||||||
modbus: super::Handle,
|
modbus: super::Handle,
|
||||||
address: u16,
|
|
||||||
register: Register,
|
register: Register,
|
||||||
register_type: RegisterType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Monitor {
|
impl Monitor {
|
||||||
pub fn new(
|
pub fn new(register: Register, mqtt: mqtt::Handle, modbus: super::Handle) -> Monitor {
|
||||||
register: Register,
|
|
||||||
register_type: RegisterType,
|
|
||||||
address: u16,
|
|
||||||
mqtt: mqtt::Handle,
|
|
||||||
modbus: super::Handle,
|
|
||||||
) -> Monitor {
|
|
||||||
Monitor {
|
Monitor {
|
||||||
mqtt,
|
mqtt: mqtt.scoped(register.path()),
|
||||||
register_type,
|
|
||||||
register,
|
|
||||||
address,
|
|
||||||
modbus,
|
modbus,
|
||||||
|
register,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,9 +31,9 @@ impl Monitor {
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
if let Ok(words) = self.read().await {
|
if let Ok(words) = self.read().await {
|
||||||
debug!(address=%self.address, "type"=?self.register_type, ?words);
|
debug!(address=%self.register.address, "type"=?self.register.register_type, ?words);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(feature = "raw")]
|
||||||
self.mqtt
|
self.mqtt
|
||||||
.publish("raw", serde_json::to_vec(&words).unwrap())
|
.publish("raw", serde_json::to_vec(&words).unwrap())
|
||||||
.await
|
.await
|
||||||
|
@ -61,54 +51,41 @@ impl Monitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read(&self) -> crate::Result<Vec<Word>> {
|
async fn read(&self) -> crate::Result<Vec<Word>> {
|
||||||
match self.register_type {
|
let Self { ref register, .. } = self;
|
||||||
|
match register.register_type {
|
||||||
RegisterType::Input => {
|
RegisterType::Input => {
|
||||||
self.modbus
|
self.modbus
|
||||||
.read_input_register(self.address, self.register.size())
|
.read_input_register(register.address, register.size())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
RegisterType::Holding => {
|
RegisterType::Holding => {
|
||||||
self.modbus
|
self.modbus
|
||||||
.read_holding_register(self.address, self.register.size())
|
.read_holding_register(register.address, register.size())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn subscribe(
|
pub(crate) async fn subscribe(mqtt: &mqtt::Handle) -> crate::Result<mpsc::Receiver<Register>> {
|
||||||
mqtt: &mqtt::Handle,
|
|
||||||
) -> crate::Result<mpsc::Receiver<(RegisterType, AddressedRegister)>> {
|
|
||||||
let (tx, rx) = mpsc::channel(8);
|
let (tx, rx) = mpsc::channel(8);
|
||||||
let mut input_registers = mqtt.subscribe("input/+").await?;
|
let mut registers = mqtt.subscribe("registers/+/config").await?;
|
||||||
let mut holding_registers = mqtt.subscribe("holding/+").await?;
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
fn to_register(payload: &Payload) -> crate::Result<AddressedRegister> {
|
fn to_register(payload: &Payload) -> crate::Result<Register> {
|
||||||
let Payload { bytes, topic } = payload;
|
Ok(serde_json::from_slice(&payload.bytes)?)
|
||||||
let address = topic
|
|
||||||
.rsplit('/')
|
|
||||||
.next()
|
|
||||||
.expect("subscribed topic guarantees we have a last segment")
|
|
||||||
.parse()?;
|
|
||||||
Ok(AddressedRegister {
|
|
||||||
address,
|
|
||||||
register: serde_json::from_slice(bytes)?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
select! {
|
if let Some(ref payload) = registers.recv().await {
|
||||||
Some(ref payload) = input_registers.recv() => {
|
|
||||||
match to_register(payload) {
|
match to_register(payload) {
|
||||||
Ok(register) => if (tx.send((RegisterType::Input, register)).await).is_err() { break; },
|
Ok(register) => {
|
||||||
Err(error) => warn!(?error, def=?payload.bytes, "ignoring invalid input register definition"),
|
if (tx.send(register).await).is_err() {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Some(ref payload) = holding_registers.recv() => {
|
Err(error) => {
|
||||||
match to_register(payload) {
|
warn!(?error, def=?payload.bytes, "ignoring invalid input register definition")
|
||||||
Ok(register) => if (tx.send((RegisterType::Holding, register)).await).is_err() { break; },
|
|
||||||
Err(error) => warn!(?error, def=?payload.bytes, "ignoring invalid holding register definition"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,8 +95,10 @@ pub(crate) async fn subscribe(
|
||||||
Ok(rx)
|
Ok(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Deserialize, Serialize, PartialEq, Default, Clone, Copy, Debug)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum RegisterType {
|
pub enum RegisterType {
|
||||||
|
#[default]
|
||||||
Input,
|
Input,
|
||||||
Holding,
|
Holding,
|
||||||
}
|
}
|
||||||
|
@ -280,6 +259,11 @@ pub struct Register {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
pub address: u16,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "IsDefault::is_default")]
|
||||||
|
pub register_type: RegisterType,
|
||||||
|
|
||||||
#[serde(flatten, default, skip_serializing_if = "IsDefault::is_default")]
|
#[serde(flatten, default, skip_serializing_if = "IsDefault::is_default")]
|
||||||
pub parse: RegisterParse,
|
pub parse: RegisterParse,
|
||||||
|
|
||||||
|
@ -291,13 +275,6 @@ pub struct Register {
|
||||||
)]
|
)]
|
||||||
pub interval: Duration,
|
pub interval: Duration,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct AddressedRegister {
|
|
||||||
pub address: u16,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub register: Register,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_register_interval() -> Duration {
|
fn default_register_interval() -> Duration {
|
||||||
Duration::from_secs(60)
|
Duration::from_secs(60)
|
||||||
|
@ -501,6 +478,14 @@ impl Register {
|
||||||
self.parse.value_type.size()
|
self.parse.value_type.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> String {
|
||||||
|
if let Some(ref name) = self.name {
|
||||||
|
name.clone()
|
||||||
|
} else {
|
||||||
|
self.address.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_words(&self, words: &[u16]) -> serde_json::Value {
|
pub fn parse_words(&self, words: &[u16]) -> serde_json::Value {
|
||||||
self.parse.value_type.parse_words(&self.apply_swaps(words))
|
self.parse.value_type.parse_words(&self.apply_swaps(words))
|
||||||
}
|
}
|
||||||
|
@ -525,12 +510,13 @@ impl Register {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use crate::mqtt::{self, Payload};
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_1() {
|
fn test_parse_1() {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
let reg = Register {
|
let reg = Register {
|
||||||
|
register_type: RegisterType::Input,
|
||||||
|
address: 42,
|
||||||
name: None,
|
name: None,
|
||||||
interval: Default::default(),
|
interval: Default::default(),
|
||||||
parse: RegisterParse {
|
parse: RegisterParse {
|
||||||
|
|
Loading…
Reference in New Issue