paramod-rust/src/modbus.rs
Eric Neuber 3ed80f4b68
Some checks failed
Build Docker Image (Podman) / build (push) Failing after 15m52s
Schreibe MQTT.Set Werte, wenn das Leitsystem aktiv ist
2026-03-13 22:56:06 +01:00

228 lines
11 KiB
Rust

use crate::config::{ModbusValueMaps, ModbusConfig};
use crate::modbus_types::{ModbusInputRegisterConfig, ModbusHoldingRegisterConfig, ModbusCoilsConfig};
use crate::config::AppConfig;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
use std::net::SocketAddr;
pub fn write_value_by_name(config: &AppConfig, name: &str, payload: &str) -> Result<(), String> {
let addr: SocketAddr = format!("{}:{}", config.modbus.host, config.modbus.port)
.parse()
.map_err(|e| format!("Ungültige Modbus-Adresse: {}", e))?;
if let Some(ref coils) = config.modbus_coils {
for map in coils {
if let Some(coil) = map.get(name) {
if !coil.write.unwrap_or(false) {
return Err(format!("Schreiben für '{}' nicht erlaubt", name));
}
let bool_value = parse_bool_payload(payload)?;
let rt = tokio::runtime::Runtime::new()
.map_err(|e| format!("Tokio Runtime Fehler: {}", e))?;
rt.block_on(async {
let mut client = tcp::connect_slave(addr, 1u8.into())
.await
.map_err(|e| format!("Modbus Verbindung fehlgeschlagen: {}", e))?;
client
.write_single_coil(coil.addr, bool_value)
.await
.map_err(|e| format!("Coil schreiben fehlgeschlagen: {}", e))
})?;
return Ok(());
}
}
}
if let Some(ref holding_registers) = config.modbus_holding_register {
for map in holding_registers {
if let Some(reg) = map.get(name) {
if !reg.write.unwrap_or(false) {
return Err(format!("Schreiben für '{}' nicht erlaubt", name));
}
let numeric_value = payload
.trim()
.parse::<f64>()
.map_err(|e| format!("Ungültiger Zahlenwert '{}': {}", payload, e))?;
let factor = reg.factor.unwrap_or(1.0);
let raw = if factor == 0.0 {
numeric_value
} else {
numeric_value / factor
};
let rt = tokio::runtime::Runtime::new()
.map_err(|e| format!("Tokio Runtime Fehler: {}", e))?;
return rt.block_on(async {
let mut client = tcp::connect_slave(addr, 1u8.into())
.await
.map_err(|e| format!("Modbus Verbindung fehlgeschlagen: {}", e))?;
match reg.r#type.as_deref().unwrap_or("UINT16") {
"INT16" => {
let int_value = raw.round();
if int_value < i16::MIN as f64 || int_value > i16::MAX as f64 {
return Err(format!("Wert außerhalb INT16-Bereich für '{}': {}", name, numeric_value));
}
let register = int_value as i16 as u16;
client
.write_multiple_registers(reg.addr, &[register])
.await
.map_err(|e| format!("Holding Register schreiben fehlgeschlagen: {}", e))?;
}
"UINT32" => {
let int_value = raw.round();
if int_value < 0.0 || int_value > u32::MAX as f64 {
return Err(format!("Wert außerhalb UINT32-Bereich für '{}': {}", name, numeric_value));
}
let val = int_value as u32;
let high = ((val >> 16) & 0xFFFF) as u16;
let low = (val & 0xFFFF) as u16;
client
.write_multiple_registers(reg.addr, &[high, low])
.await
.map_err(|e| format!("Holding Register (UINT32) schreiben fehlgeschlagen: {}", e))?;
}
_ => {
let int_value = raw.round();
if int_value < 0.0 || int_value > u16::MAX as f64 {
return Err(format!("Wert außerhalb UINT16-Bereich für '{}': {}", name, numeric_value));
}
client
.write_multiple_registers(reg.addr, &[int_value as u16])
.await
.map_err(|e| format!("Holding Register schreiben fehlgeschlagen: {}", e))?;
}
}
Ok(())
});
}
}
}
Err(format!("Kein schreibbares Modbus-Mapping für '{}' gefunden", name))
}
fn parse_bool_payload(payload: &str) -> Result<bool, String> {
match payload.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "on" => Ok(true),
"0" | "false" | "off" => Ok(false),
_ => Err(format!("Ungültiger bool-Wert '{}', erwartet 0/1 oder true/false", payload)),
}
}
/// Startet einen Thread, der zyklisch Modbus-Register abfragt und ModbusValueMaps aktualisiert
pub fn start_modbus_polling_thread(
modbus_config: &ModbusConfig,
input_registers: &Option<Vec<HashMap<String, ModbusInputRegisterConfig>>>,
holding_registers: &Option<Vec<HashMap<String, ModbusHoldingRegisterConfig>>>,
coils: &Option<Vec<HashMap<String, ModbusCoilsConfig>>>,
value_maps: Arc<Mutex<ModbusValueMaps>>,
poll_interval: Duration,
) {
let host = modbus_config.host.clone();
let port = modbus_config.port;
let input_registers = input_registers.clone();
let holding_registers = holding_registers.clone();
let coils = coils.clone();
thread::spawn(move || {
let addr: SocketAddr = format!("{}:{}", host, port).parse().unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
loop {
// Verbindungsaufbau
let connect_result = rt.block_on(async {
tcp::connect_slave(addr, 1u8.into()).await
});
match connect_result {
Ok(mut client) => {
// Werte abfragen
// Input Register
if let Some(ref vec) = input_registers {
for map in vec {
for (key, reg) in map {
let addr = reg.addr;
let typ = reg.r#type.as_deref().unwrap_or("");
let factor = reg.factor.unwrap_or(1.0);
let value: Option<f64> = rt.block_on(async {
match typ {
"INT16" => client.read_input_registers(addr, 1).await.ok().map(|v| {
let raw = v[0] as i16;
if raw == -32768 || raw == 32767 { None } else { Some(((raw as f64 * factor) * 10.0).round() / 10.0) }
}).flatten(),
"UINT16" => client.read_input_registers(addr, 1).await.ok().map(|v| {
if v[0] == 0xFFFF { None } else { Some(((v[0] as f64 * factor) * 10.0).round() / 10.0) }
}).flatten(),
"UINT32" => client.read_input_registers(addr, 2).await.ok().map(|v| {
let raw = (v[0] as u32) << 16 | (v[1] as u32);
if raw == 0xFFFFFFFF { None } else { Some(((raw as f64 * factor) * 10.0).round() / 10.0) }
}).flatten(),
_ => Some(0.0),
}
});
if let Ok(mut maps) = value_maps.lock() {
maps.modbus_input_register_values.insert(key.clone(), value);
}
}
}
}
// Holding Register
if let Some(ref vec) = holding_registers {
for map in vec {
for (key, reg) in map {
let addr = reg.addr;
let typ = reg.r#type.as_deref().unwrap_or("");
let factor = reg.factor.unwrap_or(1.0);
let value: Option<f64> = rt.block_on(async {
match typ {
"INT16" => client.read_holding_registers(addr, 1).await.ok().map(|v| {
let raw = v[0] as i16;
if raw == -32768 || raw == 32767 { None } else { Some(((raw as f64 * factor) * 10.0).round() / 10.0) }
}).flatten(),
"UINT16" => client.read_holding_registers(addr, 1).await.ok().map(|v| {
if v[0] == 0xFFFF { None } else { Some(((v[0] as f64 * factor) * 10.0).round() / 10.0) }
}).flatten(),
"UINT32" => client.read_holding_registers(addr, 2).await.ok().map(|v| {
let raw = (v[0] as u32) << 16 | (v[1] as u32);
if raw == 0xFFFFFFFF { None } else { Some(((raw as f64 * factor) * 10.0).round() / 10.0) }
}).flatten(),
_ => Some(0.0),
}
});
if let Ok(mut maps) = value_maps.lock() {
maps.modbus_holding_register_values.insert(key.clone(), value);
}
}
}
}
// Coils
if let Some(ref vec) = coils {
for map in vec {
for (key, reg) in map {
let addr = reg.addr;
let value = rt.block_on(async {
client.read_coils(addr, 1).await.ok().map(|v| if v[0] { 1.0 } else { 0.0 })
}).unwrap_or(0.0);
if let Ok(mut maps) = value_maps.lock() {
maps.modbus_coils_values.insert(key.clone(), Some(value));
}
}
}
}
},
Err(_) => {
eprintln!("Modbus: Verbindung fehlgeschlagen, retry in {:?}", poll_interval);
thread::sleep(poll_interval);
continue;
}
}
thread::sleep(poll_interval);
}
});
}