274 lines
11 KiB
Rust
274 lines
11 KiB
Rust
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<AppState>) -> Result<HttpResponse> {
|
|
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<AppState>, path: web::Path<String>) -> Result<HttpResponse> {
|
|
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<AppState>) -> Result<HttpResponse> {
|
|
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<HashMap<String, ModbusInputRegisterConfig>>),
|
|
HoldingRegister(Vec<HashMap<String, ModbusHoldingRegisterConfig>>),
|
|
Coils(Vec<HashMap<String, ModbusCoilsConfig>>),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct SaveTableRequest {
|
|
table_id: String,
|
|
rows: SaveTableRows,
|
|
}
|
|
|
|
async fn save_table(
|
|
data: web::Data<AppState>,
|
|
req: web::Json<SaveTableRequest>,
|
|
) -> Result<HttpResponse> {
|
|
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<AppState>,
|
|
settings: web::Json<AppConfig>,
|
|
) -> Result<HttpResponse> {
|
|
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<AppState>) -> 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
|
|
}
|