funktionierender Settings-Dialog

This commit is contained in:
Eric Neuber committed 2025-12-29 17:58:56 +01:00
1 parent bb2bb8d493
commit e74689c70f
10 files changed
+889 -197

No files matched your search

+177 -133
View File
@@ -1,109 +1,78 @@
use actix_web::{web, App, HttpResponse, HttpServer, Result};
use actix_files as fs;
use actix_files as actix_fs;
use serde::{Deserialize, Serialize};
use std::fs as std_fs;
use std::sync::Mutex;
use tera::{Context, Tera};
// use config::{Config, File};
use std::fs;
use toml_edit::{Document, value, Item};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct TableRow {
bezeichnung: String,
adresse: String,
r#type: String,
faktor: String,
mqtt: bool,
influxdb: bool,
pub struct MqttConfig {
pub broker: String,
pub port: u16,
pub user: Option<String>,
pub password: Option<String>,
pub button_circulation: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Settings {
mqtt_broker: String,
mqtt_port: String,
influxdb_url: String,
influxdb_token: String,
}
impl Default for Settings {
fn default() -> Self {
Settings {
mqtt_broker: "localhost".to_string(),
mqtt_port: "1883".to_string(),
influxdb_url: "http://localhost:8086".to_string(),
influxdb_token: "".to_string(),
}
}
pub struct InfluxConfig {
pub bucket: String,
pub org: String,
pub token: String,
pub url: String,
pub location: Option<String>,
pub measurement: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AppConfig {
table1: Vec<TableRow>,
table2: Vec<TableRow>,
table3: Vec<TableRow>,
settings: Settings,
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>,
}
impl Default for AppConfig {
fn default() -> Self {
AppConfig {
table1: vec![
TableRow {
bezeichnung: "Temp Sensor 1".to_string(),
adresse: "192.168.1.100".to_string(),
r#type: "Temperatur".to_string(),
faktor: "1.0".to_string(),
mqtt: true,
influxdb: false,
},
TableRow {
bezeichnung: "Temp Sensor 2".to_string(),
adresse: "192.168.1.101".to_string(),
r#type: "Temperatur".to_string(),
faktor: "1.0".to_string(),
mqtt: false,
influxdb: true,
},
],
table2: vec![
TableRow {
bezeichnung: "Humidity Sensor 1".to_string(),
adresse: "192.168.1.200".to_string(),
r#type: "Luftfeuchtigkeit".to_string(),
faktor: "0.5".to_string(),
mqtt: true,
influxdb: true,
},
],
table3: vec![
TableRow {
bezeichnung: "Pressure Sensor 1".to_string(),
adresse: "192.168.1.300".to_string(),
r#type: "Druck".to_string(),
faktor: "2.0".to_string(),
mqtt: true,
influxdb: false,
},
],
settings: Settings::default(),
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ModbusConfig {
pub host: String,
pub port: u16,
pub max_coils_addr: Option<u16>,
pub max_input_addr: Option<u16>,
pub max_holding_addr: Option<u16>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DefaultConfig {
pub loglevel: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig {
pub default: DefaultConfig,
pub mqtt: MqttConfig,
pub influxdb: InfluxConfig,
pub modbus: ModbusConfig,
pub modbus_coils: Option<Vec<HashMap<String, ModbusRegisterConfig>>>,
pub modbus_input_register: Option<Vec<HashMap<String, ModbusRegisterConfig>>>,
pub modbus_holding_register: Option<Vec<HashMap<String, ModbusRegisterConfig>>>,
}
struct AppState {
config: Mutex<AppConfig>,
config_path: String,
templates: Tera,
}
impl AppState {
fn load_or_create(config_path: &str) -> Self {
let config = match std_fs::read_to_string(config_path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(_) => {
let default = AppConfig::default();
let _ = std_fs::write(config_path, serde_json::to_string_pretty(&default).unwrap());
default
}
};
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 = toml::from_str(&conf_str).expect("Config-Deserialisierung fehlgeschlagen");
let tera = match Tera::new("templates/**/*") {
Ok(t) => t,
@@ -115,32 +84,29 @@ impl AppState {
AppState {
config: Mutex::new(config),
config_path: config_path.to_string(),
templates: tera,
}
}
fn save(&self) -> Result<(), std::io::Error> {
let config = self.config.lock().unwrap();
let json = serde_json::to_string_pretty(&*config)?;
std_fs::write(&self.config_path, json)
}
}
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
let config = data.config.lock().unwrap();
let mut context = Context::new();
context.insert("rows", &config.table1);
context.insert("table_id", "table1");
context.insert("active_page", "table1");
// modbus_input_register als rows, sortiert nach addr
let mut rows = config.modbus_input_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", "modbus_input_register");
context.insert("active_page", "modbus_input_register");
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))
}
@@ -148,24 +114,26 @@ async fn table_page(data: web::Data<AppState>, path: web::Path<String>) -> Resul
let table_id = path.into_inner();
let config = data.config.lock().unwrap();
let rows = match table_id.as_str() {
"table1" => &config.table1,
"table2" => &config.table2,
"table3" => &config.table3,
let mut context = Context::new();
let mut rows = match table_id.as_str() {
"modbus_input_register" => config.modbus_input_register.clone().unwrap_or_default(),
"modbus_holding_register" => config.modbus_holding_register.clone().unwrap_or_default(),
"modbus_coils" => config.modbus_coils.clone().unwrap_or_default(),
_ => return Ok(HttpResponse::NotFound().body("Table not found")),
};
let mut context = Context::new();
context.insert("rows", rows);
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);
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))
}
@@ -173,22 +141,28 @@ async fn settings_page(data: web::Data<AppState>) -> Result<HttpResponse> {
let config = data.config.lock().unwrap();
let mut context = Context::new();
context.insert("settings", &config.settings);
context.insert("default", &config.default);
context.insert("mqtt", &config.mqtt);
context.insert("influxdb", &config.influxdb);
context.insert("modbus", &config.modbus);
context.insert("modbus_coils", &config.modbus_coils);
context.insert("modbus_input_register", &config.modbus_input_register);
context.insert("modbus_holding_register", &config.modbus_holding_register);
context.insert("active_page", "settings");
let html = data.templates.render("settings.html", &context)
.map_err(|e| {
eprintln!("Template error: {}", e);
actix_web::error::ErrorInternalServerError("Template error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(html))
}
#[derive(Deserialize)]
#[derive(Serialize, Deserialize)]
struct SaveTableRequest {
table_id: String,
rows: Vec<TableRow>,
rows: Vec<HashMap<String, ModbusRegisterConfig>>,
}
async fn save_table(
@@ -196,40 +170,110 @@ async fn save_table(
req: web::Json<SaveTableRequest>,
) -> Result<HttpResponse> {
let mut config = data.config.lock().unwrap();
match req.table_id.as_str() {
"table1" => config.table1 = req.rows.clone(),
"table2" => config.table2 = req.rows.clone(),
"table3" => config.table3 = req.rows.clone(),
_ => return Ok(HttpResponse::BadRequest().json(serde_json::json!({"status": "error", "message": "Invalid table_id"}))),
}
drop(config);
// Lade bestehende Datei mit toml_edit
let conf_path = "old/paramod.conf";
let conf_str = match fs::read_to_string(conf_path) {
Ok(s) => s,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Lesen: {}", e))),
};
let mut doc = match conf_str.parse::<Document>() {
Ok(d) => d,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("TOML-Parse-Fehler: {}", e))),
};
match data.save() {
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))),
Err(_) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({"status": "error"}))),
let key = match req.table_id.as_str() {
"modbus_input_register" => "modbus_input_register",
"modbus_holding_register" => "modbus_holding_register",
"modbus_coils" => "modbus_coils",
_ => return Ok(HttpResponse::BadRequest().body("Invalid table_id")),
};
// Serialisiere nur das relevante Feld als TOML
let value_toml = match toml::to_string(&req.rows) {
Ok(s) => s,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Serialisierungsfehler: {}", e))),
};
// value_toml ist z.B. "[[...]]", wir brauchen nur den Wert
let value_item = value_toml.parse::<Item>().unwrap_or(Item::None);
doc[key] = value_item;
// Schreibe zurück
if let Err(e) = fs::write(conf_path, doc.to_string()) {
return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Schreiben: {}", e)));
}
Ok(HttpResponse::Ok().body("success"))
}
async fn save_settings(
data: web::Data<AppState>,
settings: web::Json<Settings>,
settings: web::Json<AppConfig>,
) -> Result<HttpResponse> {
let mut config = data.config.lock().unwrap();
config.settings = settings.into_inner();
drop(config);
match data.save() {
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))),
Err(_) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({"status": "error"}))),
*config = settings.into_inner();
let conf_path = "old/paramod.conf";
let conf_str = match fs::read_to_string(conf_path) {
Ok(s) => s,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Lesen: {}", e))),
};
let mut doc = match conf_str.parse::<Document>() {
Ok(d) => d,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("TOML-Parse-Fehler: {}", e))),
};
// mqtt
let mqtt_value = toml::Value::try_from(&config.mqtt).map_err(|e| actix_web::error::ErrorInternalServerError(format!("Serialisierungsfehler: {}", e)))?;
if let toml::Value::Table(table) = mqtt_value {
let mut item = Item::Table(Default::default());
if let Item::Table(ref mut t) = item {
for (k, v) in table {
t[&k] = v.to_string().parse::<Item>().unwrap_or(Item::None);
}
}
doc["mqtt"] = item;
}
// influxdb
let influxdb_value = toml::Value::try_from(&config.influxdb).map_err(|e| actix_web::error::ErrorInternalServerError(format!("Serialisierungsfehler: {}", e)))?;
if let toml::Value::Table(table) = influxdb_value {
let mut item = Item::Table(Default::default());
if let Item::Table(ref mut t) = item {
for (k, v) in table {
t[&k] = v.to_string().parse::<Item>().unwrap_or(Item::None);
}
}
doc["influxdb"] = item;
}
// modbus
let modbus_value = toml::Value::try_from(&config.modbus).map_err(|e| actix_web::error::ErrorInternalServerError(format!("Serialisierungsfehler: {}", e)))?;
if let toml::Value::Table(table) = modbus_value {
let mut item = Item::Table(Default::default());
if let Item::Table(ref mut t) = item {
for (k, v) in table {
t[&k] = v.to_string().parse::<Item>().unwrap_or(Item::None);
}
}
doc["modbus"] = item;
}
// [default] Abschnitt
let default_value = toml::Value::try_from(&config.default).map_err(|e| actix_web::error::ErrorInternalServerError(format!("Serialisierungsfehler: {}", e)))?;
if let toml::Value::Table(table) = default_value {
let mut item = Item::Table(Default::default());
if let Item::Table(ref mut t) = item {
for (k, v) in table {
t[&k] = v.to_string().parse::<Item>().unwrap_or(Item::None);
}
}
doc["default"] = item;
}
// Schreibe zurück
if let Err(e) = fs::write(conf_path, doc.to_string()) {
return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Schreiben: {}", e)));
}
Ok(HttpResponse::Ok().body("success"))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let config_path = "table_config.json";
let app_state = web::Data::new(AppState::load_or_create(config_path));
let config_path = "old/paramod.conf";
let app_state = web::Data::new(AppState::load_from_conf(config_path));
println!("Server läuft auf http://0.0.0.0:8080");
@@ -241,7 +285,7 @@ async fn main() -> std::io::Result<()> {
.route("/settings", web::get().to(settings_page))
.route("/api/save", web::post().to(save_table))
.route("/api/save-settings", web::post().to(save_settings))
.service(fs::Files::new("/static", "./static"))
.service(actix_fs::Files::new("/static", "./static"))
})
.bind("0.0.0.0:8080")?
.run()
+100
View File
@@ -0,0 +1,100 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::Notify;
use tokio::task::JoinHandle;
use tokio_modbus::client::tcp;
use tokio_modbus::prelude::*;
use log::{info, error, debug};
// Platzhalter für Konfiguration
#[derive(Clone)]
pub struct ModbusConfig {
pub modbus_host: String,
pub modbus_port: u16,
pub modbus_max_holding_addr: u16,
pub modbus_max_input_addr: u16,
// Register-Konfigurationen als Map: key -> (addr, type, factor, mqtt, influxdb)
pub modbus_input_register: HashMap<String, RegisterConfig>,
pub modbus_holding_register: HashMap<String, RegisterConfig>,
}
#[derive(Clone)]
pub struct RegisterConfig {
pub addr: u16,
pub reg_type: String, // z.B. "INT16", "UINT16"
pub factor: f64,
pub mqtt: bool,
pub influxdb: bool,
}
const NONSET_INT16: i16 = -32768;
const NONSET_UINT16: u16 = 65535;
pub struct ModbusThread {
config: ModbusConfig,
timeout: Duration,
polling_interval: Duration,
min_write_interval: Duration,
running: Arc<Notify>,
stop_notify: Arc<Notify>,
// Register-Cache
input_register: Arc<Mutex<Vec<u16>>>,
holding_register: Arc<Mutex<Vec<u16>>>,
// Schreib-Queue
write_holding_register: Arc<Mutex<HashMap<u16, u16>>>,
// Async-Task-Handle
handle: Option<JoinHandle<()>>,
}
impl ModbusThread {
pub fn new(config: ModbusConfig, timeout: f64, polling_interval: f64, min_write_interval: f64) -> Self {
Self {
config,
timeout: Duration::from_secs_f64(timeout),
polling_interval: Duration::from_secs_f64(polling_interval),
min_write_interval: Duration::from_secs_f64(min_write_interval),
running: Arc::new(Notify::new()),
stop_notify: Arc::new(Notify::new()),
input_register: Arc::new(Mutex::new(Vec::new())),
holding_register: Arc::new(Mutex::new(Vec::new())),
write_holding_register: Arc::new(Mutex::new(HashMap::new())),
handle: None,
}
}
pub fn start(&mut self) {
let config = self.config.clone();
let timeout = self.timeout;
let polling_interval = self.polling_interval;
let min_write_interval = self.min_write_interval;
let running = self.running.clone();
let stop_notify = self.stop_notify.clone();
let input_register = self.input_register.clone();
let holding_register = self.holding_register.clone();
let write_holding_register = self.write_holding_register.clone();
self.handle = Some(tokio::spawn(async move {
let mut write_countdown = min_write_interval;
let socket_addr = format!("{}:{}", config.modbus_host, config.modbus_port);
loop {
// Stop-Check
if stop_notify.is_notified() {
break;
}
// Connect
let mut ctx = match tcp::connect(socket_addr.clone()).await {
Ok(c) => {
info!("Modbus: Verbindung hergestellt");
c
}
Err(e) => {
error!("Modbus: Verbindungsfehler: {:?}", e);
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
};
// Polling-Loop
loop {
if stop_notify.is_notified() {
let _ =