Compare commits

..

2 Commits

Author SHA1 Message Date
Eric Neuber
e74689c70f funktionierender Settings-Dialog 2025-12-29 17:58:56 +01:00
Eric Neuber
bb2bb8d493 rust debuggen ermöglicht 2025-12-28 12:17:27 +01:00
12 changed files with 919 additions and 197 deletions

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug main.rs",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/target/debug/table-server",
"args": [],
"cwd": "${workspaceFolder}",
"sourceLanguages": ["rust"],
"preLaunchTask": "cargo build"
}
]
}

15
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "cargo build",
"type": "shell",
"command": "cargo build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$rustc"]
}
]
}

278
Cargo.lock generated
View File

@ -52,7 +52,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"base64", "base64 0.22.1",
"bitflags", "bitflags",
"brotli", "brotli",
"bytes", "bytes",
@ -214,6 +214,18 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@ -238,6 +250,12 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@ -247,12 +265,35 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@ -264,6 +305,9 @@ name = "bitflags"
version = "2.10.0" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@ -377,6 +421,54 @@ dependencies = [
"phf_codegen", "phf_codegen",
] ]
[[package]]
name = "config"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
dependencies = [
"async-trait",
"convert_case",
"json5",
"nom",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml",
"yaml-rust2",
]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.16",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.16.2" version = "0.16.2"
@ -437,6 +529,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@ -504,6 +602,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@ -662,12 +769,31 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hashlink"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@ -861,7 +987,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.16.0",
] ]
[[package]] [[package]]
@ -890,6 +1016,17 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.3.2" version = "0.3.2"
@ -974,6 +1111,12 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@ -996,6 +1139,16 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@ -1017,6 +1170,16 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@ -1049,6 +1212,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -1305,6 +1474,28 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "ron"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
dependencies = [
"base64 0.21.7",
"bitflags",
"serde",
"serde_derive",
]
[[package]]
name = "rust-ini"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@ -1375,6 +1566,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -1512,10 +1712,14 @@ version = "0.1.0"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-web", "actix-web",
"config",
"serde", "serde",
"serde_derive",
"serde_json", "serde_json",
"tera", "tera",
"tokio", "tokio",
"toml",
"toml_edit",
] ]
[[package]] [[package]]
@ -1571,6 +1775,15 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.2"
@ -1622,6 +1835,47 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.41" version = "0.1.41"
@ -2014,6 +2268,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.46.0" version = "0.46.0"
@ -2026,6 +2289,17 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "yaml-rust2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
dependencies = [
"arraydeque",
"encoding_rs",
"hashlink",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.1"

View File

@ -4,9 +4,15 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
toml_edit = "0.22"
actix-web = "4.4.0" actix-web = "4.4.0"
actix-files = "0.6.2" actix-files = "0.6.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tera = "1.19" tera = "1.19"
# Für TOML/INI-Konfiguration
config = "0.14"
toml = "0.8"
serde_derive = "1.0"

11
concept.md Normal file
View File

@ -0,0 +1,11 @@
# Heizungsteuerung einer Paradigma-Anlage über Modbus
Man kann an einer Paradigma Anlage über Modbus-TCP viele Daten abfragen und und setzen. Ziel dieses Projekts ist es, die Daten sowohl als MQTT-Daten bereitzustellen, als auch in eine InfluxDB einzupflegen.
Zusätzlich soll noch die Möglichkeit gegeben werden, über MQTT Werte zu setzen und damit die Steuerung zu übernehmen.
## Technische Details
### 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.
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.

140
old/paramod.conf Normal file
View File

@ -0,0 +1,140 @@
[default]
loglevel = "DEBUG"
[mqtt]
broker = "192.168.178.2"
button_circulation = "zigbee2mqtt/WirelessButton"
password = "97sm3pHNSMZ4M5qUj0x8"
port = 1883
user = "admin"
[influxdb]
bucket = "Paradigma"
location = "Radebeul"
measurement = "ParadigmaModbus"
org = "skaville"
token = "i-sXFQbEkSC1XVzqFEaFwXwzasbsEIciVlK4SaAUOEvk0VjQPkD3fr8d7_3SPeyseTZkqj7ZMZU78b3n2F6_SQ=="
url = "192.168.178.2:8086"
[modbus]
host = "192.168.178.10"
max_coils_addr = 8
max_holding_addr = 61
max_input_addr = 45
port = 502
[[modbus_coils]]
MgtSystem = {addr = 0, comment = "Leitsystem aktiv"}
HK1pres = {addr = 1, comment = "HK1 vorhanden"}
HK2pres = {addr = 2, comment = "HK2 vorhanden"}
HK3pres = {addr = 3, comment = "HK3 vorhanden"}
TWrelease = {addr = 4, comment = "Trinkwassererwärmung freigegeben"}
TWlock = {addr = 5, comment = "Trinkwassererwärmung gesprerrt"}
Zrelease = {addr = 6, comment = "Zirkulation freigegeben"}
Zlock = {addr = 7, comment = "Zirkulation gesperrt"}
SHKpres = {addr = 8, comment = "Schwimmbadheizkrei vorhanden"}
[[modbus_input_register]]
TA = {addr = 0, type = "INT16", factor = 0.1, mqtt = true, influxdb = true}
TV = {addr = 1, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TR = {addr = 2, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TWO = {addr = 3, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TPO = {addr = 4, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TPU = {addr = 5, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TZR = {addr = 6, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV2 = {addr = 7, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TR2 = {addr = 8, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
RI1 = {addr = 9, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
RI2 = {addr = 10, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSA = {addr = 11, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
FATV = {addr = 12, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
FATR = {addr = 13, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TVKH = {addr = 14, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TRKH = {addr = 15, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TPOKH = {addr = 16, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV3 = {addr = 17, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TR3 = {addr = 18, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSB = {addr = 19, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TVSB = {addr = 20, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TRSB = {addr = 21, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TWE = {addr = 22, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TWA = {addr = 23, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TWS = {addr = 24, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TVSI = {addr = 25, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TK = {addr = 26, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV1 = {addr = 27, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV2 = {addr = 28, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV3 = {addr = 29, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV4 = {addr = 30, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSE = {addr = 31, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TW = {addr = 32, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSV = {addr = 33, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TW2 = {addr = 34, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
S = {addr = 35, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TAM = {addr = 36, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TAM2 = {addr = 37, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSA1 = {addr = 38, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TSA2 = {addr = 39, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSP = {addr = 40, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TWW = {addr = 41, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TKW = {addr = 42, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
VKW = {addr = 43, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
VSPm = {addr = 44, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
[[modbus_holding_register]]
nothing = {addr = 0, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrLS = {addr = 1, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
TVsoll = {addr = 2, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TV2soll = {addr = 3, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV3soll = {addr = 4, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
HK1soll = {addr = 5, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
HK2soll = {addr = 6, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
HK3soll = {addr = 7, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
TWWsoll = {addr = 8, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TV1max = {addr = 9, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TV2max = {addr = 10, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV3max = {addr = 11, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
ErrHR = {addr = 12, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
ErrSR = {addr = 13, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
ErrWE1_1 = {addr = 14, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
ErrWE1_2 = {addr = 15, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE1_3 = {addr = 16, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE1_4 = {addr = 17, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE1_5 = {addr = 18, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
KollLei = {addr = 19, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
TagesS = {addr = 20, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
GesS = {addr = 21, type = "UINT32", factor = 0.1, mqtt = false, influxdb = true}
GesWW = {addr = 23, type = "UINT32", factor = 0.1, mqtt = false, influxdb = true}
GesZ = {addr = 25, type = "UINT32", factor = 0.1, mqtt = false, influxdb = true}
HGesK1 = {addr = 27, type = "UINT32", factor = 1, mqtt = false, influxdb = true}
StartK1 = {addr = 29, type = "UINT32", factor = 1, mqtt = false, influxdb = true}
HGesPel = {addr = 31, type = "UINT32", factor = 1, mqtt = false, influxdb = true}
VGesPel = {addr = 33, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
StatWW = {addr = 34, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatZ = {addr = 35, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatHK1 = {addr = 36, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatHK2 = {addr = 37, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
StatHK3 = {addr = 38, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
StatS = {addr = 39, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatSB = {addr = 40, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
StatK1 = {addr = 41, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatPel = {addr = 42, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatKH = {addr = 43, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
TPOsoll = {addr = 44, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
FATVsoll = {addr = 45, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
TSBsollHK = {addr = 46, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSBsollS = {addr = 47, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
BetrHK1 = {addr = 48, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
BetrHK2 = {addr = 49, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
BetrHK3 = {addr = 50, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
BetrSB = {addr = 51, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
GesKKsoll = {addr = 52, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE1 = {addr = 53, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE2 = {addr = 54, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE3 = {addr = 55, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE4 = {addr = 56, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
ErrWE1 = {addr = 57, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE2 = {addr = 58, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE3 = {addr = 59, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE4 = {addr = 60, type = "UINT16", factor = 1, mqtt = false, influxdb = false}

View File

@ -1,109 +1,78 @@
use actix_web::{web, App, HttpResponse, HttpServer, Result}; use actix_web::{web, App, HttpResponse, HttpServer, Result};
use actix_files as fs; use actix_files as actix_fs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs as std_fs;
use std::sync::Mutex; use std::sync::Mutex;
use tera::{Context, Tera}; 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
struct TableRow { pub struct MqttConfig {
bezeichnung: String, pub broker: String,
adresse: String, pub port: u16,
r#type: String, pub user: Option<String>,
faktor: String, pub password: Option<String>,
mqtt: bool, pub button_circulation: Option<String>,
influxdb: bool,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
struct Settings { pub struct InfluxConfig {
mqtt_broker: String, pub bucket: String,
mqtt_port: String, pub org: String,
influxdb_url: String, pub token: String,
influxdb_token: String, pub url: String,
} pub location: Option<String>,
pub measurement: Option<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(),
}
}
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
struct AppConfig { pub struct ModbusRegisterConfig {
table1: Vec<TableRow>, pub addr: u16,
table2: Vec<TableRow>, pub r#type: Option<String>,
table3: Vec<TableRow>, pub factor: Option<f64>,
settings: Settings, pub mqtt: Option<bool>,
pub influxdb: Option<bool>,
pub comment: Option<String>,
} }
impl Default for AppConfig { #[derive(Debug, Serialize, Deserialize, Clone)]
fn default() -> Self { pub struct ModbusConfig {
AppConfig { pub host: String,
table1: vec![ pub port: u16,
TableRow { pub max_coils_addr: Option<u16>,
bezeichnung: "Temp Sensor 1".to_string(), pub max_input_addr: Option<u16>,
adresse: "192.168.1.100".to_string(), pub max_holding_addr: Option<u16>,
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 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 { struct AppState {
config: Mutex<AppConfig>, config: Mutex<AppConfig>,
config_path: String,
templates: Tera, templates: Tera,
} }
impl AppState { impl AppState {
fn load_or_create(config_path: &str) -> Self { fn load_from_conf(conf_path: &str) -> Self {
let config = match std_fs::read_to_string(config_path) { let conf_str = std::fs::read_to_string(conf_path).expect("Config-Datei konnte nicht gelesen werden");
Ok(content) => serde_json::from_str(&content).unwrap_or_default(), let config: AppConfig = toml::from_str(&conf_str).expect("Config-Deserialisierung fehlgeschlagen");
Err(_) => {
let default = AppConfig::default();
let _ = std_fs::write(config_path, serde_json::to_string_pretty(&default).unwrap());
default
}
};
let tera = match Tera::new("templates/**/*") { let tera = match Tera::new("templates/**/*") {
Ok(t) => t, Ok(t) => t,
@ -115,32 +84,29 @@ impl AppState {
AppState { AppState {
config: Mutex::new(config), config: Mutex::new(config),
config_path: config_path.to_string(),
templates: tera, 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> { async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
let config = data.config.lock().unwrap(); let config = data.config.lock().unwrap();
let mut context = Context::new(); let mut context = Context::new();
context.insert("rows", &config.table1); // modbus_input_register als rows, sortiert nach addr
context.insert("table_id", "table1"); let mut rows = config.modbus_input_register.clone().unwrap_or_default();
context.insert("active_page", "table1"); 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) 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))
} }
@ -148,24 +114,26 @@ async fn table_page(data: web::Data<AppState>, path: web::Path<String>) -> Resul
let table_id = path.into_inner(); let table_id = path.into_inner();
let config = data.config.lock().unwrap(); let config = data.config.lock().unwrap();
let rows = match table_id.as_str() { let mut context = Context::new();
"table1" => &config.table1, let mut rows = match table_id.as_str() {
"table2" => &config.table2, "modbus_input_register" => config.modbus_input_register.clone().unwrap_or_default(),
"table3" => &config.table3, "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")), _ => return Ok(HttpResponse::NotFound().body("Table not found")),
}; };
rows.sort_by(|a, b| {
let mut context = Context::new(); let addr_a = a.values().next().map(|v| v.addr).unwrap_or(0);
context.insert("rows", rows); 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("table_id", &table_id);
context.insert("active_page", &table_id); context.insert("active_page", &table_id);
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))
} }
@ -173,22 +141,28 @@ async fn settings_page(data: web::Data<AppState>) -> Result<HttpResponse> {
let config = data.config.lock().unwrap(); let config = data.config.lock().unwrap();
let mut context = Context::new(); 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"); context.insert("active_page", "settings");
let html = data.templates.render("settings.html", &context) let html = data.templates.render("settings.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))
} }
#[derive(Deserialize)]
#[derive(Serialize, Deserialize)]
struct SaveTableRequest { struct SaveTableRequest {
table_id: String, table_id: String,
rows: Vec<TableRow>, rows: Vec<HashMap<String, ModbusRegisterConfig>>,
} }
async fn save_table( async fn save_table(
@ -196,40 +170,110 @@ async fn save_table(
req: web::Json<SaveTableRequest>, req: web::Json<SaveTableRequest>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let mut config = data.config.lock().unwrap(); let mut config = data.config.lock().unwrap();
// 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 req.table_id.as_str() { let key = match req.table_id.as_str() {
"table1" => config.table1 = req.rows.clone(), "modbus_input_register" => "modbus_input_register",
"table2" => config.table2 = req.rows.clone(), "modbus_holding_register" => "modbus_holding_register",
"table3" => config.table3 = req.rows.clone(), "modbus_coils" => "modbus_coils",
_ => return Ok(HttpResponse::BadRequest().json(serde_json::json!({"status": "error", "message": "Invalid table_id"}))), _ => return Ok(HttpResponse::BadRequest().body("Invalid table_id")),
} };
// Serialisiere nur das relevante Feld als TOML
drop(config); let value_toml = match toml::to_string(&req.rows) {
Ok(s) => s,
match data.save() { Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Serialisierungsfehler: {}", e))),
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))), };
Err(_) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({"status": "error"}))), // 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( async fn save_settings(
data: web::Data<AppState>, data: web::Data<AppState>,
settings: web::Json<Settings>, settings: web::Json<AppConfig>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let mut config = data.config.lock().unwrap(); let mut config = data.config.lock().unwrap();
config.settings = settings.into_inner(); *config = settings.into_inner();
drop(config); let conf_path = "old/paramod.conf";
let conf_str = match fs::read_to_string(conf_path) {
match data.save() { Ok(s) => s,
Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({"status": "success"}))), Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Lesen: {}", e))),
Err(_) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({"status": "error"}))), };
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] #[actix_web::main]
async fn main() -> std::io::Result<()> { 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"); 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("/settings", web::get().to(settings_page))
.route("/api/save", web::post().to(save_table)) .route("/api/save", web::post().to(save_table))
.route("/api/save-settings", web::post().to(save_settings)) .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")? .bind("0.0.0.0:8080")?
.run() .run()

100
src/modbus_thread.rs Normal file
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 _ =

View File

@ -1,3 +1,18 @@
// Sortiere die Tabelle nach Adresse (addr) beim Laden der Seite
document.addEventListener('DOMContentLoaded', function() {
const tableBody = document.getElementById('tableBody');
if (tableBody) {
// Extrahiere alle Zeilen und deren addr
const rows = Array.from(tableBody.querySelectorAll('tr'));
rows.sort((a, b) => {
const addrA = parseInt(a.querySelector("input[data-field='addr']")?.value || '0', 10);
const addrB = parseInt(b.querySelector("input[data-field='addr']")?.value || '0', 10);
return addrA - addrB;
});
// Füge sortierte Zeilen wieder ein
rows.forEach(row => tableBody.appendChild(row));
}
});
function addRow() { function addRow() {
const tableBody = document.getElementById('tableBody'); const tableBody = document.getElementById('tableBody');
const rowCount = tableBody.querySelectorAll('tr').length; const rowCount = tableBody.querySelectorAll('tr').length;
@ -47,23 +62,26 @@ function updateRowIndices() {
async function saveTable() { async function saveTable() {
const rows = []; const rows = [];
const tableRows = document.querySelectorAll('#tableBody tr'); const tableRows = document.querySelectorAll('#tableBody tr');
tableRows.forEach((row) => { tableRows.forEach((row) => {
const bezeichnung = row.querySelector("input[data-field='bezeichnung']").value; const key = row.querySelector("input[data-field='bezeichnung']").value.trim();
const adresse = row.querySelector("input[data-field='adresse']").value; if (!key) return; // leere Zeilen überspringen
const type = row.querySelector("input[data-field='type']").value; const addr = parseInt(row.querySelector("input[data-field='addr']")?.value || row.querySelector("input[data-field='adresse']")?.value || '0', 10);
const faktor = row.querySelector("input[data-field='faktor']").value; const rtype = row.querySelector("input[data-field='type']")?.value || null;
const mqtt = row.querySelector("input[data-field='mqtt']").checked; const factor = parseFloat(row.querySelector("input[data-field='factor']")?.value || row.querySelector("input[data-field='faktor']")?.value || '1.0');
const influxdb = row.querySelector("input[data-field='influxdb']").checked; const mqtt = row.querySelector("input[data-field='mqtt']")?.checked || false;
const influxdb = row.querySelector("input[data-field='influxdb']")?.checked || false;
rows.push({ const comment = row.querySelector("input[data-field='comment']")?.value || null;
bezeichnung, const value = {
adresse, addr: addr,
type, rtype: rtype,
faktor, factor: factor,
mqtt, mqtt: mqtt,
influxdb influxdb: influxdb,
}); comment: comment
};
const obj = {};
obj[key] = value;
rows.push(obj);
}); });
try { try {

View File

@ -1,14 +1,45 @@
async function saveSettings() { async function saveSettings() {
const mqtt_broker = document.getElementById('mqtt_broker').value; // MQTT
const mqtt_port = document.getElementById('mqtt_port').value; const mqtt = {
const influxdb_url = document.getElementById('influxdb_url').value; broker: document.getElementById('mqtt_broker').value,
const influxdb_token = document.getElementById('influxdb_token').value; port: parseInt(document.getElementById('mqtt_port').value, 10),
user: document.getElementById('mqtt_user').value || null,
password: document.getElementById('mqtt_password').value || null,
button_circulation: document.getElementById('mqtt_button_circulation').value || null
};
// InfluxDB
const influxdb = {
url: document.getElementById('influxdb_url').value,
token: document.getElementById('influxdb_token').value,
bucket: document.getElementById('influxdb_bucket').value,
org: document.getElementById('influxdb_org').value,
location: document.getElementById('influxdb_location').value || null,
measurement: document.getElementById('influxdb_measurement').value || null
};
// Modbus
const modbus = {
host: document.getElementById('modbus_host').value,
port: parseInt(document.getElementById('modbus_port').value, 10),
max_coils_addr: parseInt(document.getElementById('modbus_max_coils_addr').value, 10) || null,
max_input_addr: parseInt(document.getElementById('modbus_max_input_addr').value, 10) || null,
max_holding_addr: parseInt(document.getElementById('modbus_max_holding_addr').value, 10) || null,
modbus_coils: null,
modbus_input_register: null,
modbus_holding_register: null
};
// Default (für [default])
const defaultConfig = {
loglevel: document.getElementById('loglevel')?.value || null
};
const settings = { const settings = {
mqtt_broker, default: defaultConfig,
mqtt_port, mqtt,
influxdb_url, influxdb,
influxdb_token modbus
}; };
try { try {

View File

@ -17,9 +17,9 @@
<span class="logo-text">Sensor Manager</span> <span class="logo-text">Sensor Manager</span>
</div> </div>
<nav class="nav"> <nav class="nav">
<a href="/" class="nav-link {% if active_page == 'table1' %}active{% endif %}">Tabelle 1</a> <a href="/table/modbus_input_register" class="nav-link {% if active_page == 'modbus_input_register' %}active{% endif %}">Input Register</a>
<a href="/table/table2" class="nav-link {% if active_page == 'table2' %}active{% endif %}">Tabelle 2</a> <a href="/table/modbus_holding_register" class="nav-link {% if active_page == 'modbus_holding_register' %}active{% endif %}">Holding Register</a>
<a href="/table/table3" class="nav-link {% if active_page == 'table3' %}active{% endif %}">Tabelle 3</a> <a href="/table/modbus_coils" class="nav-link {% if active_page == 'modbus_coils' %}active{% endif %}">Coils</a>
<a href="/settings" class="nav-link {% if active_page == 'settings' %}active{% endif %}">⚙️ Einstellungen</a> <a href="/settings" class="nav-link {% if active_page == 'settings' %}active{% endif %}">⚙️ Einstellungen</a>
</nav> </nav>
</div> </div>
@ -42,28 +42,30 @@
</tr> </tr>
</thead> </thead>
<tbody id="tableBody"> <tbody id="tableBody">
{% for row in rows %} {% for entry in rows %}
<tr data-row="{{ loop.index0 }}"> {% for key, row in entry %}
<td><input type='text' class='text-input' data-field='bezeichnung' value='{{ row.bezeichnung }}' /></td> <tr data-row="{{ loop.index0 }}">
<td><input type='text' class='text-input' data-field='adresse' value='{{ row.adresse }}' /></td> <td><input type='text' class='text-input' data-field='key' value='{{ key }}' /></td>
<td><input type='text' class='text-input' data-field='type' value='{{ row.type }}' /></td> <td><input type='number' class='text-input' data-field='addr' value='{{ row.addr }}' /></td>
<td><input type='text' class='text-input' data-field='faktor' value='{{ row.faktor }}' /></td> <td><input type='text' class='text-input' data-field='type' value='{{ row.type | default(value="") }}' /></td>
<td> <td><input type='text' class='text-input' data-field='factor' value='{{ row.factor | default(value="") }}' /></td>
<label class='switch'> <td>
<input type='checkbox' class='bool-input' data-field='mqtt' {% if row.mqtt %}checked{% endif %} /> <label class='switch'>
<span class='slider'></span> <input type='checkbox' class='bool-input' data-field='mqtt' {% if row.mqtt %}checked{% endif %} />
</label> <span class='slider'></span>
</td> </label>
<td> </td>
<label class='switch'> <td>
<input type='checkbox' class='bool-input' data-field='influxdb' {% if row.influxdb %}checked{% endif %} /> <label class='switch'>
<span class='slider'></span> <input type='checkbox' class='bool-input' data-field='influxdb' {% if row.influxdb %}checked{% endif %} />
</label> <span class='slider'></span>
</td> </label>
<td> </td>
<button class="delete-btn" onclick="deleteRow(this)">🗑️</button> <td>
</td> <button class="delete-btn" onclick="deleteRow(this)">🗑️</button>
</tr> </td>
</tr>
{% endfor %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -17,27 +17,53 @@
<span class="logo-text">Sensor Manager</span> <span class="logo-text">Sensor Manager</span>
</div> </div>
<nav class="nav"> <nav class="nav">
<a href="/" class="nav-link {% if active_page == 'table1' %}active{% endif %}">Tabelle 1</a> <a href="/table/modbus_input_register" class="nav-link {% if active_page == 'modbus_input_register' %}active{% endif %}">Input Register</a>
<a href="/table/table2" class="nav-link {% if active_page == 'table2' %}active{% endif %}">Tabelle 2</a> <a href="/table/modbus_holding_register" class="nav-link {% if active_page == 'modbus_holding_register' %}active{% endif %}">Holding Register</a>
<a href="/table/table3" class="nav-link {% if active_page == 'table3' %}active{% endif %}">Tabelle 3</a> <a href="/table/modbus_coils" class="nav-link {% if active_page == 'modbus_coils' %}active{% endif %}">Coils</a>
<a href="/settings" class="nav-link {% if active_page == 'settings' %}active{% endif %}">⚙️ Einstellungen</a> <a href="/settings" class="nav-link {% if active_page == 'settings' %}active{% endif %}">⚙️ Einstellungen</a>
</nav> </nav>
</div> </div>
</header> </header>
<div class="container"> <div class="container">
<h1>⚙️ Einstellungen</h1> <h1>⚙️ Einstellungen</h1>
<div id="message" class="message"></div> <div id="message" class="message"></div>
<div class="settings-section">
<h2>Allgemein</h2>
<div class="form-group">
<label for="loglevel">Loglevel:</label>
<select id="loglevel" class="text-input">
<option value="DEBUG">DEBUG</option>
<option value="INFO">INFO</option>
<option value="WARN">WARN</option>
<option value="ERROR">ERROR</option>
</select>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h2>MQTT Konfiguration</h2> <h2>MQTT Konfiguration</h2>
<div class="form-group"> <div class="form-group">
<label for="mqtt_broker">MQTT Broker:</label> <label for="mqtt_broker">MQTT Broker:</label>
<input type="text" id="mqtt_broker" class="text-input" value="{{ settings.mqtt_broker }}" /> <input type="text" id="mqtt_broker" class="text-input" value="{{ mqtt.broker }}" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="mqtt_port">MQTT Port:</label> <label for="mqtt_port">MQTT Port:</label>
<input type="text" id="mqtt_port" class="text-input" value="{{ settings.mqtt_port }}" /> <input type="text" id="mqtt_port" class="text-input" value="{{ mqtt.port }}" />
</div>
<div class="form-group">
<label for="mqtt_user">MQTT User:</label>
<input type="text" id="mqtt_user" class="text-input" value="{{ mqtt.user | default(value="") }}" />
</div>
<div class="form-group">
<label for="mqtt_password">MQTT Passwort:</label>
<input type="password" id="mqtt_password" class="text-input" value="{{ mqtt.password | default(value="") }}" />
</div>
<div class="form-group">
<label for="mqtt_button_circulation">Button Circulation:</label>
<input type="text" id="mqtt_button_circulation" class="text-input" value="{{ mqtt.button_circulation | default(value="") }}" />
</div> </div>
</div> </div>
@ -45,11 +71,51 @@
<h2>InfluxDB Konfiguration</h2> <h2>InfluxDB Konfiguration</h2>
<div class="form-group"> <div class="form-group">
<label for="influxdb_url">InfluxDB URL:</label> <label for="influxdb_url">InfluxDB URL:</label>
<input type="text" id="influxdb_url" class="text-input" value="{{ settings.influxdb_url }}" /> <input type="text" id="influxdb_url" class="text-input" value="{{ influxdb.url }}" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="influxdb_token">InfluxDB Token:</label> <label for="influxdb_token">InfluxDB Token:</label>
<input type="password" id="influxdb_token" class="text-input" value="{{ settings.influxdb_token }}" /> <input type="password" id="influxdb_token" class="text-input" value="{{ influxdb.token }}" />
</div>
<div class="form-group">
<label for="influxdb_bucket">Bucket:</label>
<input type="text" id="influxdb_bucket" class="text-input" value="{{ influxdb.bucket }}" />
</div>
<div class="form-group">
<label for="influxdb_org">Org:</label>
<input type="text" id="influxdb_org" class="text-input" value="{{ influxdb.org }}" />
</div>
<div class="form-group">
<label for="influxdb_location">Location:</label>
<input type="text" id="influxdb_location" class="text-input" value="{{ influxdb.location | default(value="") }}" />
</div>
<div class="form-group">
<label for="influxdb_measurement">Measurement:</label>
<input type="text" id="influxdb_measurement" class="text-input" value="{{ influxdb.measurement | default(value="") }}" />
</div>
</div>
<div class="settings-section">
<h2>Modbus Konfiguration</h2>
<div class="form-group">
<label for="modbus_host">Host:</label>
<input type="text" id="modbus_host" class="text-input" value="{{ modbus.host }}" />
</div>
<div class="form-group">
<label for="modbus_port">Port:</label>
<input type="text" id="modbus_port" class="text-input" value="{{ modbus.port }}" />
</div>
<div class="form-group">
<label for="modbus_max_coils_addr">Max Coils Addr:</label>
<input type="text" id="modbus_max_coils_addr" class="text-input" value="{{ modbus.max_coils_addr | default(value="") }}" />
</div>
<div class="form-group">
<label for="modbus_max_input_addr">Max Input Addr:</label>
<input type="text" id="modbus_max_input_addr" class="text-input" value="{{ modbus.max_input_addr | default(value="") }}" />
</div>
<div class="form-group">
<label for="modbus_max_holding_addr">Max Holding Addr:</label>
<input type="text" id="modbus_max_holding_addr" class="text-input" value="{{ modbus.max_holding_addr | default(value="") }}" />
</div> </div>
</div> </div>