funktionierender Settings-Dialog
This commit is contained in:
1 parent
bb2bb8d493
commit
e74689c70f
10 files changed
+889
-197
No files matched your search
+177
-133
@@ -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()
|
||||
|
||||
@@ -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 _ =
|
||||
Reference in new issue
Block a user