Some checks failed
Build Docker Image (Podman) / build (push) Failing after 15m52s
228 lines
11 KiB
Rust
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);
|
|
}
|
|
});
|
|
}
|