Modbus-Table-Konfigurationen ausgelagert

This commit is contained in:
Eric Neuber 2026-02-13 21:16:25 +01:00
parent 84eb4b2365
commit b9bc0e52d3
7 changed files with 157 additions and 89 deletions

View File

@ -6,6 +6,9 @@ Zusätzlich soll noch die Möglichkeit gegeben werden, über MQTT Werte zu setze
## Technische Details ## Technische Details
### Konfiguration ### Konfiguration
Die Konfiguration soll sowohl über Webservice als auch über eine Konfigurationsdatei möglich sein. Änderungen an der Weboberfläche sollen direkten Einfluss haben und in der Konfigurationsdatei persistiert werden. Die Konfigurationsdatei soll beim Start genutzt werden. Die Konfiguration soll sowohl über Webservice als auch über eine Konfigurationsdatei möglich sein. Änderungen an der Weboberfläche sollen direkten Einfluss haben und in der Konfigurationsdatei persistiert werden. Die Konfigurationsdatei soll beim Start genutzt werden. Die Konfiguration soll im yaml-Format vorliegen.
An der Weboberfläche soll es drei Tabellen geben, um die Modbus-Einstellungen für coils, input_register und holding_register vorzunehmen. Weiterhin soll es ein Einstellungsmenü geben, um den MQTT-Broker, den Modbus-Server, den InfluxDB-Server und allgemeine Einstellungen vorzunehmen. An der Weboberfläche soll es drei Tabellen geben, um die Modbus-Einstellungen für coils, input_register und holding_register vorzunehmen. Weiterhin soll es ein Einstellungsmenü geben, um den MQTT-Broker, den Modbus-Server, den InfluxDB-Server und allgemeine Einstellungen vorzunehmen.
In den Tabellen soll es jeweils eine Spalte mit dem aktuellen Wert geben. Dieser wird aus den Modbus-Werten auf Rust-Seite ausgelesen und wird je nach Type aus einer bzw. zwei Adressen berechnet und mit dem Faktor multipliziert. Das trifft für input_register und holding_register zu. Bei coils handelt es sich um 1-Bit Datenpunkte und damit Boolean.

32
src/app_state.rs Normal file
View File

@ -0,0 +1,32 @@
use std::sync::{Mutex, Arc};
use tera::Tera;
use crate::config::{AppConfig, ModbusValueMaps};
pub struct AppState {
pub config: Mutex<AppConfig>,
pub value_maps: Arc<Mutex<ModbusValueMaps>>,
pub templates: Tera,
}
impl AppState {
pub fn load_from_conf(conf_path: &str) -> Self {
let conf_str = std::fs::read_to_string(conf_path).expect("Config-Datei konnte nicht gelesen werden");
let config: AppConfig = serde_yaml::from_str(&conf_str).expect("Config-Deserialisierung fehlgeschlagen");
let value_maps = Arc::new(Mutex::new(ModbusValueMaps::from_config(&config)));
let tera = match Tera::new("templates/**/*") {
Ok(t) => t,
Err(e) => {
println!("Template parsing error: {}", e);
std::process::exit(1);
}
};
AppState {
config: Mutex::new(config),
value_maps,
templates: tera,
}
}
}

View File

@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use crate::modbus_types::{ModbusRegisterConfig, ModbusCoilsConfig};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DefaultConfig { pub struct DefaultConfig {
@ -35,15 +36,7 @@ pub struct InfluxConfig {
pub measurement: Option<String>, pub measurement: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] // ModbusRegisterConfig und ModbusCoilsConfig werden jetzt aus modbus_types.rs importiert
pub struct ModbusRegisterConfig {
pub addr: u16,
pub r#type: Option<String>,
pub factor: Option<f64>,
pub mqtt: Option<bool>,
pub influxdb: Option<bool>,
pub comment: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig { pub struct AppConfig {
@ -51,7 +44,7 @@ pub struct AppConfig {
pub mqtt: MqttConfig, pub mqtt: MqttConfig,
pub influxdb: InfluxConfig, pub influxdb: InfluxConfig,
pub modbus: ModbusConfig, pub modbus: ModbusConfig,
pub modbus_coils: Option<Vec<HashMap<String, ModbusRegisterConfig>>>, pub modbus_coils: Option<Vec<HashMap<String, ModbusCoilsConfig>>>,
pub modbus_input_register: Option<Vec<HashMap<String, ModbusRegisterConfig>>>, pub modbus_input_register: Option<Vec<HashMap<String, ModbusRegisterConfig>>>,
pub modbus_holding_register: Option<Vec<HashMap<String, ModbusRegisterConfig>>>, pub modbus_holding_register: Option<Vec<HashMap<String, ModbusRegisterConfig>>>,
} }

View File

@ -1,44 +1,18 @@
use actix_web::{web, App, HttpResponse, HttpServer, Result}; use actix_web::{web, App, HttpResponse, HttpServer, Result};
use actix_files as actix_fs; use actix_files as actix_fs;
use std::sync::{Mutex, Arc}; use std::sync::{Arc};
use tera::{Context, Tera}; use tera::Context;
use std::fs; use std::fs;
use serde_yaml; use serde_yaml;
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
mod config; mod config;
mod modbus; mod modbus;
use crate::config::{AppConfig, ModbusRegisterConfig, ModbusValueMaps}; mod app_state;
pub mod modbus_types;
use crate::config::{AppConfig, ModbusValueMaps};
struct AppState { use crate::modbus_types::{ModbusRegisterConfig, ModbusCoilsConfig};
config: Mutex<AppConfig>, use crate::app_state::AppState;
value_maps: Arc<Mutex<ModbusValueMaps>>,
templates: Tera,
}
impl AppState {
fn load_from_conf(conf_path: &str) -> Self {
let conf_str = std::fs::read_to_string(conf_path).expect("Config-Datei konnte nicht gelesen werden");
let config: AppConfig = serde_yaml::from_str(&conf_str).expect("Config-Deserialisierung fehlgeschlagen");
let value_maps = Arc::new(Mutex::new(ModbusValueMaps::from_config(&config)));
let tera = match Tera::new("templates/**/*") {
Ok(t) => t,
Err(e) => {
println!("Template parsing error: {}", e);
std::process::exit(1);
}
};
AppState {
config: Mutex::new(config),
value_maps,
templates: tera,
}
}
}
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> { async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
let config = data.config.lock().unwrap(); let config = data.config.lock().unwrap();
@ -67,37 +41,63 @@ async fn table_page(data: web::Data<AppState>, path: web::Path<String>) -> Resul
let config = data.config.lock().unwrap(); let config = data.config.lock().unwrap();
let value_maps = data.value_maps.lock().unwrap(); let value_maps = data.value_maps.lock().unwrap();
let mut context = Context::new(); let mut context = Context::new();
let (rows, value_map) = match table_id.as_str() { match table_id.as_str() {
"modbus_input_register" => ( "modbus_input_register" => {
config.modbus_input_register.clone().unwrap_or_default(), let mut rows = config.modbus_input_register.clone().unwrap_or_default();
&value_maps.modbus_input_register_values rows.sort_by(|a, b| {
),
"modbus_holding_register" => (
config.modbus_holding_register.clone().unwrap_or_default(),
&value_maps.modbus_holding_register_values
),
"modbus_coils" => (
config.modbus_coils.clone().unwrap_or_default(),
&value_maps.modbus_coils_values
),
_ => return Ok(HttpResponse::NotFound().body("Table not found")),
};
let mut sorted_rows = rows;
sorted_rows.sort_by(|a, b| {
let addr_a = a.values().next().map(|v| v.addr).unwrap_or(0); let addr_a = a.values().next().map(|v| v.addr).unwrap_or(0);
let addr_b = b.values().next().map(|v| v.addr).unwrap_or(0); let addr_b = b.values().next().map(|v| v.addr).unwrap_or(0);
addr_a.cmp(&addr_b) addr_a.cmp(&addr_b)
}); });
context.insert("rows", &sorted_rows); context.insert("rows", &rows);
context.insert("table_id", &table_id); context.insert("table_id", &table_id);
context.insert("active_page", &table_id); context.insert("active_page", &table_id);
context.insert("value_map", value_map); context.insert("value_map", &value_maps.modbus_input_register_values);
let html = data.templates.render("index.html", &context) let html = data.templates.render("index.html", &context)
.map_err(|e| { .map_err(|e| {
eprintln!("Template error: {}", e); eprintln!("Template error: {}", e);
actix_web::error::ErrorInternalServerError("Template error") actix_web::error::ErrorInternalServerError("Template error")
})?; })?;
Ok(HttpResponse::Ok().content_type("text/html").body(html)) Ok(HttpResponse::Ok().content_type("text/html").body(html))
}
"modbus_holding_register" => {
let mut rows = config.modbus_holding_register.clone().unwrap_or_default();
rows.sort_by(|a, b| {
let addr_a = a.values().next().map(|v| v.addr).unwrap_or(0);
let addr_b = b.values().next().map(|v| v.addr).unwrap_or(0);
addr_a.cmp(&addr_b)
});
context.insert("rows", &rows);
context.insert("table_id", &table_id);
context.insert("active_page", &table_id);
context.insert("value_map", &value_maps.modbus_holding_register_values);
let html = data.templates.render("index.html", &context)
.map_err(|e| {
eprintln!("Template error: {}", e);
actix_web::error::ErrorInternalServerError("Template error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(html))
}
"modbus_coils" => {
let mut rows = config.modbus_coils.clone().unwrap_or_default();
rows.sort_by(|a, b| {
let addr_a = a.values().next().map(|v| v.addr).unwrap_or(0);
let addr_b = b.values().next().map(|v| v.addr).unwrap_or(0);
addr_a.cmp(&addr_b)
});
context.insert("rows", &rows);
context.insert("table_id", &table_id);
context.insert("active_page", &table_id);
context.insert("value_map", &value_maps.modbus_coils_values);
let html = data.templates.render("index.html", &context)
.map_err(|e| {
eprintln!("Template error: {}", e);
actix_web::error::ErrorInternalServerError("Template error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(html))
}
_ => Ok(HttpResponse::NotFound().body("Table not found")),
}
} }
async fn settings_page(data: web::Data<AppState>) -> Result<HttpResponse> { async fn settings_page(data: web::Data<AppState>) -> Result<HttpResponse> {
@ -122,10 +122,17 @@ async fn settings_page(data: web::Data<AppState>) -> Result<HttpResponse> {
#[derive(Serialize, Deserialize, Clone)]
#[serde(untagged)]
enum SaveTableRows {
Register(Vec<HashMap<String, ModbusRegisterConfig>>),
Coils(Vec<HashMap<String, ModbusCoilsConfig>>),
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct SaveTableRequest { struct SaveTableRequest {
table_id: String, table_id: String,
rows: Vec<HashMap<String, ModbusRegisterConfig>>, rows: SaveTableRows,
} }
async fn save_table( async fn save_table(
@ -134,17 +141,29 @@ async fn save_table(
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let mut config = data.config.lock().unwrap(); let mut config = data.config.lock().unwrap();
let conf_path = "paramod.yaml"; let conf_path = "paramod.yaml";
let key = match req.table_id.as_str() { match req.table_id.as_str() {
"modbus_input_register" => "modbus_input_register", "modbus_input_register" => {
"modbus_holding_register" => "modbus_holding_register", if let SaveTableRows::Register(rows) = req.rows.clone() {
"modbus_coils" => "modbus_coils", config.modbus_input_register = Some(rows);
} else {
return Ok(HttpResponse::BadRequest().body("Falscher Typ für input_register"));
}
}
"modbus_holding_register" => {
if let SaveTableRows::Register(rows) = req.rows.clone() {
config.modbus_holding_register = Some(rows);
} else {
return Ok(HttpResponse::BadRequest().body("Falscher Typ für holding_register"));
}
}
"modbus_coils" => {
if let SaveTableRows::Coils(rows) = req.rows.clone() {
config.modbus_coils = Some(rows);
} else {
return Ok(HttpResponse::BadRequest().body("Falscher Typ für coils"));
}
}
_ => return Ok(HttpResponse::BadRequest().body("Invalid table_id")), _ => return Ok(HttpResponse::BadRequest().body("Invalid table_id")),
};
match key {
"modbus_input_register" => config.modbus_input_register = Some(req.rows.clone()),
"modbus_holding_register" => config.modbus_holding_register = Some(req.rows.clone()),
"modbus_coils" => config.modbus_coils = Some(req.rows.clone()),
_ => {}
} }
let yaml_str = match serde_yaml::to_string(&*config) { let yaml_str = match serde_yaml::to_string(&*config) {
Ok(s) => s, Ok(s) => s,

View File

@ -1,4 +1,5 @@
use crate::config::{ModbusValueMaps, ModbusRegisterConfig, ModbusConfig}; use crate::config::{ModbusValueMaps, ModbusConfig};
use crate::modbus_types::{ModbusRegisterConfig, ModbusCoilsConfig};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
@ -9,7 +10,7 @@ pub fn start_modbus_polling_thread(
_modbus_config: &ModbusConfig, _modbus_config: &ModbusConfig,
input_registers: &Option<Vec<HashMap<String, ModbusRegisterConfig>>>, input_registers: &Option<Vec<HashMap<String, ModbusRegisterConfig>>>,
holding_registers: &Option<Vec<HashMap<String, ModbusRegisterConfig>>>, holding_registers: &Option<Vec<HashMap<String, ModbusRegisterConfig>>>,
coils: &Option<Vec<HashMap<String, ModbusRegisterConfig>>>, coils: &Option<Vec<HashMap<String, ModbusCoilsConfig>>>,
value_maps: Arc<Mutex<ModbusValueMaps>>, value_maps: Arc<Mutex<ModbusValueMaps>>,
poll_interval: Duration, poll_interval: Duration,
) { ) {

20
src/modbus_types.rs Normal file
View File

@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ModbusCoilsConfig {
pub addr: u16,
pub write: Option<bool>,
pub mqtt: Option<bool>,
pub influxdb: Option<bool>,
pub comment: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ModbusRegisterConfig {
pub addr: u16,
pub r#type: Option<String>,
pub factor: Option<f64>,
pub mqtt: Option<bool>,
pub influxdb: Option<bool>,
pub comment: Option<String>,
}

View File

@ -6,7 +6,7 @@
<title>Einstellungen</title> <title>Einstellungen</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
<body> <body{% if default.darkmode == true or default.darkmode == "true" %} class="darkmode"{% endif %}>
<header class="header"> <header class="header">
<div class="header-content"> <div class="header-content">
<div class="logo"> <div class="logo">