use actix_web::{web, App, HttpResponse, HttpServer, Result}; use actix_files as actix_fs; use std::sync::{Arc}; use tera::Context; use std::fs; use serde_yaml; use std::collections::HashMap; use serde::{Serialize, Deserialize}; mod config; mod modbus; mod app_state; mod mqtt; pub mod modbus_types; use crate::config::{AppConfig, ModbusValueMaps}; use crate::modbus_types::{ModbusInputRegisterConfig, ModbusHoldingRegisterConfig, ModbusCoilsConfig}; use crate::app_state::AppState; async fn index(data: web::Data) -> Result { let config = data.config.lock().unwrap(); let value_maps = data.value_maps.lock().unwrap(); let mut context = Context::new(); 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"); context.insert("value_map", &value_maps.modbus_input_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)) } async fn table_page(data: web::Data, path: web::Path) -> Result { let table_id = path.into_inner(); let config = data.config.lock().unwrap(); let value_maps = data.value_maps.lock().unwrap(); let mut context = Context::new(); match table_id.as_str() { "modbus_input_register" => { 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", &table_id); context.insert("active_page", &table_id); context.insert("value_map", &value_maps.modbus_input_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_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) -> Result { let config = data.config.lock().unwrap(); let mut context = Context::new(); 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") .insert_header(("Cache-Control", "no-store, must-revalidate")) .body(html)) } #[derive(Serialize, Deserialize, Clone)] #[serde(untagged)] enum SaveTableRows { InputRegister(Vec>), HoldingRegister(Vec>), Coils(Vec>), } #[derive(Serialize, Deserialize)] struct SaveTableRequest { table_id: String, rows: SaveTableRows, } async fn save_table( data: web::Data, req: web::Json, ) -> Result { let mut config = data.config.lock().unwrap(); let conf_path = "paramod.yaml"; match req.table_id.as_str() { "modbus_input_register" => { if let SaveTableRows::InputRegister(rows) = req.rows.clone() { config.modbus_input_register = Some(rows); } else { return Ok(HttpResponse::BadRequest().body("Falscher Typ für input_register")); } } "modbus_holding_register" => { if let SaveTableRows::HoldingRegister(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")), } let yaml_str = match serde_yaml::to_string(&*config) { Ok(s) => s, Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Serialisierungsfehler: {}", e))), }; if let Err(e) = fs::write(conf_path, yaml_str) { return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Schreiben: {}", e))); } Ok(HttpResponse::Ok().body("success")) } async fn save_settings( data: web::Data, settings: web::Json, ) -> Result { let mut config = data.config.lock().unwrap(); let mut new_config = settings.into_inner(); // Tabellenwerte erhalten, falls sie im Request null sind if new_config.modbus_coils.is_none() { new_config.modbus_coils = config.modbus_coils.clone(); } if new_config.modbus_input_register.is_none() { new_config.modbus_input_register = config.modbus_input_register.clone(); } if new_config.modbus_holding_register.is_none() { new_config.modbus_holding_register = config.modbus_holding_register.clone(); } // Value-Maps neu initialisieren let mut value_maps = data.value_maps.lock().unwrap(); *value_maps = ModbusValueMaps::from_config(&new_config); let conf_path = "paramod.yaml"; let yaml_str = match serde_yaml::to_string(&new_config) { Ok(s) => s, Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Serialisierungsfehler: {}", e))), }; if let Err(e) = fs::write(conf_path, yaml_str) { return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Schreiben: {}", e))); } // Reload config from file let conf_str = match std::fs::read_to_string(conf_path) { Ok(s) => s, Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Lesen: {}", e))), }; let updated_config: AppConfig = match serde_yaml::from_str(&conf_str) { Ok(cfg) => cfg, Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Deserialisierungsfehler: {}", e))), }; // darkmode-Konvertierung: String zu bool // (YAML kann bool als string interpretieren) *config = updated_config; Ok(HttpResponse::Ok().body("success")) } async fn get_config(data: web::Data) -> HttpResponse { let config = data.config.lock().unwrap(); HttpResponse::Ok().json(&*config) } #[actix_web::main] async fn main() -> std::io::Result<()> { let config_path = "paramod.yaml"; let app_state = web::Data::new(AppState::load_from_conf(config_path)); // Starte Modbus-Polling-Thread { let config = app_state.config.lock().unwrap().clone(); let value_maps = Arc::clone(&app_state.value_maps); modbus::start_modbus_polling_thread( &config.modbus, &config.modbus_input_register, &config.modbus_holding_register, &config.modbus_coils, value_maps, std::time::Duration::from_secs(2), // Poll-Intervall ); } // Starte MQTT-Thread { let value_maps = Arc::clone(&app_state.value_maps); mqtt::start_mqtt_thread(Arc::clone(&app_state.config), value_maps); } println!("Server läuft auf http://0.0.0.0:8080"); HttpServer::new(move || { App::new() .app_data(app_state.clone()) .route("/", web::get().to(index)) .route("/table/{id}", web::get().to(table_page)) .route("/settings", web::get().to(settings_page)) .route("/api/save", web::post().to(save_table)) .route("/api/save-settings", web::post().to(save_settings)) .route("/api/config", web::get().to(get_config)) .service(actix_fs::Files::new("/static", "./static")) }) .bind("0.0.0.0:8080")? .run() .await }