Compare commits
68 commits
v1.9.9-pre
...
main
Author | SHA1 | Date | |
---|---|---|---|
7ffdfd1fc3 | |||
9d12bce452 | |||
b549ceebab | |||
b28026820c | |||
ab3cd28f83 | |||
cec26d8cfb | |||
186d8fc71a | |||
05edfe6b84 | |||
c4c99eff1d | |||
76c33534be | |||
9bd1243129 | |||
667f51d375 | |||
9a87b4f820 | |||
37c7412df1 | |||
03d6278ba9 | |||
7508459414 | |||
b28f39667d | |||
d0018b7953 | |||
b5121ce7f4 | |||
8b2ef6513f | |||
12bc89ede6 | |||
ae9091c95e | |||
21f3a176ab | |||
ade01e4a91 | |||
edfc0373db | |||
f4f5e4bd3e | |||
8c8aba919d | |||
b8c2a8c114 | |||
4f2534b5a0 | |||
4905a89b33 | |||
a908490bb0 | |||
ed82c3444e | |||
88144693a8 | |||
0b53d347dc | |||
9e19f669c5 | |||
2fbae49297 | |||
e914f2fd10 | |||
f41e763743 | |||
23fe7acbb0 | |||
813f26500c | |||
8bc7c8e17c | |||
47e711f111 | |||
93aeff17cb | |||
7e664848dd | |||
cc2f493401 | |||
7ceaf73037 | |||
8b26a8d6bb | |||
9c87f12a7d | |||
97b5cfc370 | |||
9272aa20ad | |||
4445f874ea | |||
5a0265f6df | |||
ea3a88eb4a | |||
ad8a7b253f | |||
ec8bbdbd41 | |||
5359d679cb | |||
fbe4214918 | |||
a1a80276c9 | |||
51aec2ce00 | |||
46e23d1da1 | |||
296e1d0ccf | |||
11503615de | |||
59994c75c1 | |||
786701794e | |||
8a48645b53 | |||
1552a10518 | |||
0205b9be37 | |||
34a735deec |
42 changed files with 3381 additions and 848 deletions
0
.gitmodules
vendored
0
.gitmodules
vendored
32
.taplo.toml
Normal file
32
.taplo.toml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[formatting]
|
||||||
|
indent_string = "\t"
|
||||||
|
reorder_keys = false
|
||||||
|
reorder_arrays = false
|
||||||
|
reorder_inline_tables = true
|
||||||
|
|
||||||
|
[[rule]]
|
||||||
|
include = ["**/Cargo.toml"]
|
||||||
|
|
||||||
|
[rule.schema]
|
||||||
|
path = "https://json.schemastore.org/cargo.json"
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
# schema definition for .cargo/config.toml is not yet available
|
||||||
|
# see https://github.com/rust-lang/cargo/issues/12883
|
||||||
|
|
||||||
|
[[rule]]
|
||||||
|
include = ["**/Cargo.toml"]
|
||||||
|
keys = [
|
||||||
|
"build-dependencies",
|
||||||
|
"dependencies",
|
||||||
|
"dev-dependencies",
|
||||||
|
"target.*.build-dependencies",
|
||||||
|
"target.*.dependencies",
|
||||||
|
"target.*.dev-dependencies",
|
||||||
|
"workspace.dependencies",
|
||||||
|
"workspace.lints.clippy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[rule.formatting]
|
||||||
|
reorder_keys = true
|
||||||
|
reorder_arrays = true
|
44
Cargo.lock
generated
44
Cargo.lock
generated
|
@ -185,9 +185,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -239,19 +239,23 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charge-controller-supervisor"
|
name = "charge-controller-supervisor"
|
||||||
version = "1.9.9-pre-4"
|
version = "1.9.9-pre-30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.7.0",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"eyre",
|
"eyre",
|
||||||
"futures",
|
"futures",
|
||||||
|
"include_dir",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"notify-debouncer-mini",
|
||||||
"prometheus",
|
"prometheus",
|
||||||
"rocket",
|
"rocket",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-modbus",
|
"tokio-modbus",
|
||||||
"tokio-serial",
|
"tokio-serial",
|
||||||
|
@ -379,7 +383,7 @@ version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
|
checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.7.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"proc-macro2-diagnostics",
|
"proc-macro2-diagnostics",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1167,7 +1171,7 @@ version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.7.0",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
@ -1358,7 +1362,7 @@ version = "7.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.7.0",
|
||||||
"filetime",
|
"filetime",
|
||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
"inotify",
|
"inotify",
|
||||||
|
@ -1589,7 +1593,7 @@ dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
@ -1608,7 +1612,7 @@ dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
|
@ -1673,7 +1677,7 @@ version = "0.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1886,7 +1890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.7.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
@ -1909,7 +1913,7 @@ version = "0.38.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.7.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
|
@ -2051,7 +2055,7 @@ version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "779e2977f0cc2ff39708fef48f96f3768ac8ddd8c6caaaab82e83bd240ef99b2"
|
checksum = "779e2977f0cc2ff39708fef48f96f3768ac8ddd8c6caaaab82e83bd240ef99b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.7.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
|
@ -2201,7 +2205,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tesla-charge-controller"
|
name = "tesla-charge-controller"
|
||||||
version = "1.9.9-pre-4"
|
version = "1.9.9-pre-30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
@ -2220,7 +2224,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serialport",
|
"serialport",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-modbus",
|
"tokio-modbus",
|
||||||
"tokio-serial",
|
"tokio-serial",
|
||||||
|
@ -2237,11 +2241,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.9"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.9",
|
"thiserror-impl 2.0.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2257,9 +2261,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.9"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2374,7 +2378,7 @@ dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
29
Cargo.toml
29
Cargo.toml
|
@ -4,15 +4,32 @@ default-members = ["charge-controller-supervisor"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.9.9-pre-4"
|
version = "1.9.9-pre-30"
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
pedantic = "warn"
|
pedantic = "warn"
|
||||||
module-name-repetitions = { level = "allow", priority = 1 }
|
|
||||||
struct-excessive-bools = { level = "allow", priority = 1 }
|
branches_sharing_code = "warn"
|
||||||
too-many-lines = { level = "allow", priority = 1 }
|
derive_partial_eq_without_eq = "warn"
|
||||||
default-trait-access = { level = "allow", priority = 1 }
|
equatable_if_let = "warn"
|
||||||
cast-precision-loss = { level = "allow", priority = 1 }
|
fallible_impl_from = "warn"
|
||||||
|
large_stack_frames = "warn"
|
||||||
|
missing_const_for_fn = "warn"
|
||||||
|
needless_collect = "warn"
|
||||||
|
needless_pass_by_ref_mut = "warn"
|
||||||
|
or_fun_call = "warn"
|
||||||
|
redundant_clone = "warn"
|
||||||
|
significant_drop_in_scrutinee = "warn"
|
||||||
|
significant_drop_tightening = "warn"
|
||||||
|
too_long_first_doc_paragraph = "warn"
|
||||||
|
trait_duplication_in_bounds = "warn"
|
||||||
|
|
||||||
cast-possible-truncation = { level = "allow", priority = 1 }
|
cast-possible-truncation = { level = "allow", priority = 1 }
|
||||||
|
cast-precision-loss = { level = "allow", priority = 1 }
|
||||||
|
default-trait-access = { level = "allow", priority = 1 }
|
||||||
missing-errors-doc = { level = "allow", priority = 1 }
|
missing-errors-doc = { level = "allow", priority = 1 }
|
||||||
missing-panics-doc = { level = "allow", priority = 1 }
|
missing-panics-doc = { level = "allow", priority = 1 }
|
||||||
|
module-name-repetitions = { level = "allow", priority = 1 }
|
||||||
|
similar-names = { level = "allow", priority = 1 }
|
||||||
|
struct-excessive-bools = { level = "allow", priority = 1 }
|
||||||
|
too-many-lines = { level = "allow", priority = 1 }
|
||||||
|
|
Binary file not shown.
|
@ -12,20 +12,24 @@ systemd-units = { enable = false }
|
||||||
depends = ""
|
depends = ""
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bitflags = { version = "2.7.0", features = ["serde"] }
|
||||||
|
chrono = "0.4.39"
|
||||||
clap = { version = "4.5.23", features = ["derive"] }
|
clap = { version = "4.5.23", features = ["derive"] }
|
||||||
|
env_logger = "0.11.6"
|
||||||
|
eyre = "0.6.12"
|
||||||
|
futures = "0.3.31"
|
||||||
|
include_dir = "0.7.4"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
log = "0.4.22"
|
||||||
|
notify-debouncer-mini = { version = "0.5.0", default-features = false }
|
||||||
|
prometheus = "0.13.4"
|
||||||
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
serde = { version = "1.0.216", features = ["derive"] }
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
serde_json = "1.0.134"
|
serde_json = "1.0.134"
|
||||||
|
thiserror = "2.0.11"
|
||||||
tokio = { version = "1.42.0", features = ["full"] }
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
|
||||||
chrono = "0.4.39"
|
|
||||||
prometheus = "0.13.4"
|
|
||||||
env_logger = "0.11.6"
|
|
||||||
log = "0.4.22"
|
|
||||||
futures = "0.3.31"
|
|
||||||
tokio-modbus = "0.16.1"
|
tokio-modbus = "0.16.1"
|
||||||
tokio-serial = "5.4.4"
|
tokio-serial = "5.4.4"
|
||||||
lazy_static = "1.5.0"
|
|
||||||
eyre = "0.6.12"
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -1,23 +1,208 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
static CONFIG_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
|
||||||
|
pub(super) static CONFIG: std::sync::OnceLock<tokio::sync::RwLock<Config>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
pub async fn access_config<'a>() -> tokio::sync::RwLockReadGuard<'a, Config> {
|
||||||
|
CONFIG.get().unwrap().read().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ConfigWatcher {
|
||||||
|
_debouncer: notify_debouncer_mini::Debouncer<notify_debouncer_mini::notify::RecommendedWatcher>,
|
||||||
|
_handle: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_config(path: impl AsRef<std::path::Path>) -> Option<ConfigWatcher> {
|
||||||
|
let _ = CONFIG_PATH.get_or_init(|| path.as_ref().to_path_buf());
|
||||||
|
log::trace!("loading config...");
|
||||||
|
let config = Config::load(&path).unwrap();
|
||||||
|
if let Err(e) = config.save() {
|
||||||
|
log::warn!("couldn't save updated config: {e:?}");
|
||||||
|
}
|
||||||
|
log::trace!("watching config for changes...");
|
||||||
|
let config_watcher = ConfigWatcher::new(&path);
|
||||||
|
CONFIG.set(tokio::sync::RwLock::new(config)).unwrap();
|
||||||
|
|
||||||
|
config_watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigWatcher {
|
||||||
|
pub fn new(path: impl AsRef<std::path::Path>) -> Option<Self> {
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
let mut debouncer =
|
||||||
|
notify_debouncer_mini::new_debouncer(std::time::Duration::from_secs(1), move |v| {
|
||||||
|
tx.send(v).unwrap();
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
debouncer
|
||||||
|
.watcher()
|
||||||
|
.watch(
|
||||||
|
path.as_ref(),
|
||||||
|
notify_debouncer_mini::notify::RecursiveMode::NonRecursive,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let config_path = std::path::PathBuf::from(path.as_ref());
|
||||||
|
|
||||||
|
let handle = tokio::task::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
Some(Ok(_event)) => {
|
||||||
|
let config = Config::load(&config_path).unwrap();
|
||||||
|
|
||||||
|
if let Err(e) = overwrite_config(config).await {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(e)) => log::error!("Error {e:?} from watcher"),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
_debouncer: debouncer,
|
||||||
|
_handle: handle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn overwrite_config(config: Config) -> eyre::Result<()> {
|
||||||
|
let mut h = CONFIG
|
||||||
|
.get()
|
||||||
|
.ok_or_else(|| eyre::eyre!("could not get config"))?
|
||||||
|
.write()
|
||||||
|
.await;
|
||||||
|
if h.charge_controllers != config.charge_controllers
|
||||||
|
|| h.primary_charge_controller != config.primary_charge_controller
|
||||||
|
{
|
||||||
|
log::warn!("charge controller configuration changed on disk; please restart");
|
||||||
|
}
|
||||||
|
*h = config;
|
||||||
|
drop(h);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConfigHandle<'a> {
|
||||||
|
handle: tokio::sync::RwLockWriteGuard<'a, Config>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> core::ops::Deref for ConfigHandle<'a> {
|
||||||
|
type Target = tokio::sync::RwLockWriteGuard<'a, Config>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for ConfigHandle<'_> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ConfigHandle<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
log::error!("error saving config on drop of handle: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_to_config<'a>() -> ConfigHandle<'a> {
|
||||||
|
ConfigHandle {
|
||||||
|
handle: CONFIG.get().unwrap().write().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
#[serde(tag = "version")]
|
||||||
|
pub enum ConfigStorage {
|
||||||
|
#[serde(rename = "1")]
|
||||||
|
V1(outdated::ConfigV1),
|
||||||
|
#[serde(rename = "2")]
|
||||||
|
V2(Config),
|
||||||
|
}
|
||||||
|
|
||||||
|
mod outdated;
|
||||||
|
|
||||||
|
impl Default for ConfigStorage {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from_latest(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigStorage {
|
||||||
|
pub const fn from_latest(config: Config) -> Self {
|
||||||
|
Self::V2(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_latest(self) -> Config {
|
||||||
|
match self {
|
||||||
|
ConfigStorage::V1(v1) => v1.into(),
|
||||||
|
ConfigStorage::V2(config) => config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(path: impl AsRef<std::path::Path>) -> eyre::Result<Self> {
|
||||||
|
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_to(&self, path: impl AsRef<std::path::Path>) -> eyre::Result<()> {
|
||||||
|
Ok(serde_json::ser::to_writer_pretty(
|
||||||
|
std::io::BufWriter::new(std::fs::File::create(path)?),
|
||||||
|
self,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> eyre::Result<()> {
|
||||||
|
self.save_to(CONFIG_PATH.get().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub primary_charge_controller: String,
|
pub primary_charge_controller: String,
|
||||||
|
pub enable_secondary_control: bool,
|
||||||
pub charge_controllers: Vec<ChargeControllerConfig>,
|
pub charge_controllers: Vec<ChargeControllerConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
impl Config {
|
||||||
pub struct ChargeControllerConfig {
|
fn load(path: impl AsRef<std::path::Path>) -> eyre::Result<Self> {
|
||||||
pub name: String,
|
let storage = ConfigStorage::load(path)?;
|
||||||
pub serial_port: String,
|
Ok(storage.into_latest())
|
||||||
pub baud_rate: u32,
|
}
|
||||||
pub watch_interval_seconds: u64,
|
|
||||||
pub variant: ChargeControllerVariant,
|
fn save(&self) -> eyre::Result<()> {
|
||||||
|
let as_storage = ConfigStorage::from_latest(self.clone());
|
||||||
|
as_storage.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ChargeControllerConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub watch_interval_seconds: u64,
|
||||||
|
pub variant: ChargeControllerVariant,
|
||||||
|
#[serde(default)]
|
||||||
|
pub follow_primary: bool,
|
||||||
|
pub transport: Transport,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum ChargeControllerVariant {
|
pub enum ChargeControllerVariant {
|
||||||
Tristar,
|
Tristar,
|
||||||
Pl { timeout_milliseconds: u64 },
|
Pl { timeout_milliseconds: u64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Transport {
|
||||||
|
Serial { port: String, baud_rate: u32 },
|
||||||
|
Tcp { ip: std::net::IpAddr, port: u16 },
|
||||||
|
}
|
||||||
|
|
72
charge-controller-supervisor/src/config/outdated.rs
Normal file
72
charge-controller-supervisor/src/config/outdated.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
pub use v1::ConfigV1;
|
||||||
|
|
||||||
|
mod v1 {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ConfigV1 {
|
||||||
|
primary_charge_controller: String,
|
||||||
|
enable_secondary_control: bool,
|
||||||
|
charge_controllers: Vec<ChargeControllerConfigV1>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
struct ChargeControllerConfigV1 {
|
||||||
|
name: String,
|
||||||
|
serial_port: String,
|
||||||
|
baud_rate: u32,
|
||||||
|
watch_interval_seconds: u64,
|
||||||
|
variant: ChargeControllerVariantV1,
|
||||||
|
#[serde(default)]
|
||||||
|
follow_primary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
enum ChargeControllerVariantV1 {
|
||||||
|
Tristar,
|
||||||
|
Pl { timeout_milliseconds: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChargeControllerConfigV1> for crate::config::ChargeControllerConfig {
|
||||||
|
fn from(value: ChargeControllerConfigV1) -> Self {
|
||||||
|
Self {
|
||||||
|
name: value.name,
|
||||||
|
transport: crate::config::Transport::Serial {
|
||||||
|
port: value.serial_port,
|
||||||
|
baud_rate: value.baud_rate,
|
||||||
|
},
|
||||||
|
watch_interval_seconds: value.watch_interval_seconds,
|
||||||
|
variant: value.variant.into(),
|
||||||
|
follow_primary: value.follow_primary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChargeControllerVariantV1> for crate::config::ChargeControllerVariant {
|
||||||
|
fn from(value: ChargeControllerVariantV1) -> Self {
|
||||||
|
match value {
|
||||||
|
ChargeControllerVariantV1::Tristar => Self::Tristar,
|
||||||
|
ChargeControllerVariantV1::Pl {
|
||||||
|
timeout_milliseconds,
|
||||||
|
} => Self::Pl {
|
||||||
|
timeout_milliseconds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ConfigV1> for crate::config::Config {
|
||||||
|
fn from(value: ConfigV1) -> Self {
|
||||||
|
Self {
|
||||||
|
primary_charge_controller: value.primary_charge_controller,
|
||||||
|
enable_secondary_control: value.enable_secondary_control,
|
||||||
|
charge_controllers: value
|
||||||
|
.charge_controllers
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,17 @@
|
||||||
|
use crate::storage::ControllerData;
|
||||||
|
|
||||||
|
mod pl;
|
||||||
|
mod tristar;
|
||||||
|
|
||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
name: String,
|
name: String,
|
||||||
interval: std::time::Duration,
|
interval: std::time::Duration,
|
||||||
inner: ControllerInner,
|
inner: ControllerInner,
|
||||||
data: std::sync::Arc<tokio::sync::RwLock<CommonData>>,
|
data: std::sync::Arc<ControllerData>,
|
||||||
|
follow_voltage: bool,
|
||||||
|
voltage_rx: tokio::sync::watch::Receiver<VoltageCommand>,
|
||||||
|
voltage_tx: Option<tokio::sync::watch::Sender<VoltageCommand>>,
|
||||||
|
settings_last_read: Option<std::time::Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, serde::Serialize, Clone)]
|
#[derive(Default, serde::Serialize, Clone)]
|
||||||
|
@ -12,70 +21,201 @@ pub struct CommonData {
|
||||||
pub battery_temp: f64,
|
pub battery_temp: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(tag = "model")]
|
||||||
|
pub enum ControllerState {
|
||||||
|
Pl(pl::PlState),
|
||||||
|
Tristar(tristar::TristarState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControllerState {
|
||||||
|
pub fn common(&self) -> CommonData {
|
||||||
|
match self {
|
||||||
|
Self::Pl(pl_state) => crate::controller::CommonData {
|
||||||
|
battery_voltage: pl_state.battery_voltage,
|
||||||
|
target_voltage: pl_state.target_voltage,
|
||||||
|
battery_temp: pl_state.battery_temp,
|
||||||
|
},
|
||||||
|
Self::Tristar(tristar_state) => crate::controller::CommonData {
|
||||||
|
battery_voltage: tristar_state.battery_voltage,
|
||||||
|
target_voltage: tristar_state.target_voltage,
|
||||||
|
battery_temp: f64::from(tristar_state.battery_temp),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(tag = "model")]
|
||||||
|
pub enum ControllerSettings {
|
||||||
|
Pl(pl::PlSettings),
|
||||||
|
Tristar(tristar::TristarSettings),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum VoltageCommand {
|
||||||
|
None,
|
||||||
|
Set(f64),
|
||||||
|
}
|
||||||
|
|
||||||
impl Controller {
|
impl Controller {
|
||||||
pub fn new(config: crate::config::ChargeControllerConfig) -> eyre::Result<Self> {
|
pub async fn new(
|
||||||
|
config: crate::config::ChargeControllerConfig,
|
||||||
|
voltage_rx: tokio::sync::watch::Receiver<VoltageCommand>,
|
||||||
|
) -> eyre::Result<Self> {
|
||||||
let inner = match config.variant {
|
let inner = match config.variant {
|
||||||
crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar(
|
crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar(
|
||||||
crate::tristar::Tristar::new(&config.serial_port, &config.name, config.baud_rate)?,
|
tristar::Tristar::new(&config.name, &config.transport).await?,
|
||||||
),
|
),
|
||||||
crate::config::ChargeControllerVariant::Pl {
|
crate::config::ChargeControllerVariant::Pl {
|
||||||
timeout_milliseconds,
|
timeout_milliseconds,
|
||||||
} => ControllerInner::Pl(crate::pl::Pli::new(
|
} => match &config.transport {
|
||||||
&config.serial_port,
|
crate::config::Transport::Serial { port, baud_rate } => ControllerInner::Pl(
|
||||||
&config.name,
|
pl::Pli::new(port, &config.name, *baud_rate, timeout_milliseconds)?,
|
||||||
config.baud_rate,
|
),
|
||||||
timeout_milliseconds,
|
crate::config::Transport::Tcp { ip: _, port: _ } => {
|
||||||
)?),
|
return Err(eyre::eyre!("pl doesn't support tcp"))
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let data = CommonData::default();
|
let data = std::sync::Arc::new(ControllerData::new());
|
||||||
|
|
||||||
let data = std::sync::Arc::new(tokio::sync::RwLock::new(data));
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
interval: std::time::Duration::from_secs(config.watch_interval_seconds),
|
interval: std::time::Duration::from_secs(config.watch_interval_seconds),
|
||||||
inner,
|
inner,
|
||||||
data,
|
data,
|
||||||
|
voltage_rx,
|
||||||
|
voltage_tx: None,
|
||||||
|
settings_last_read: None,
|
||||||
|
follow_voltage: config.follow_primary,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_data_ptr(&self) -> std::sync::Arc<tokio::sync::RwLock<CommonData>> {
|
pub fn get_data_ptr(&self) -> std::sync::Arc<ControllerData> {
|
||||||
self.data.clone()
|
self.data.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(&mut self) -> eyre::Result<()> {
|
pub async fn refresh(&mut self) -> eyre::Result<()> {
|
||||||
let data = self.inner.refresh().await?;
|
let data = self.inner.refresh().await?;
|
||||||
*self.data.write().await = data;
|
|
||||||
|
if let Some(tx) = self.voltage_tx.as_mut() {
|
||||||
|
if crate::config::access_config()
|
||||||
|
.await
|
||||||
|
.enable_secondary_control
|
||||||
|
{
|
||||||
|
let target = data.common().target_voltage;
|
||||||
|
log::debug!(
|
||||||
|
"tristar {}: primary: sending target voltage {}",
|
||||||
|
self.name,
|
||||||
|
target
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.send(VoltageCommand::Set(target))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.data.write_state().await = Some(data);
|
||||||
|
if self.needs_new_settings() {
|
||||||
|
match self.inner.get_settings().await {
|
||||||
|
Ok(s) => {
|
||||||
|
*self.data.write_settings().await = Some(s);
|
||||||
|
self.settings_last_read = Some(std::time::Instant::now());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("couldn't read config from {}: {e:?}", self.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timeout_interval(&self) -> std::time::Duration {
|
pub const fn timeout_interval(&self) -> std::time::Duration {
|
||||||
self.interval
|
self.interval
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_tx_to_secondary(&mut self, tx: tokio::sync::watch::Sender<VoltageCommand>) {
|
||||||
|
assert!(
|
||||||
|
!self.follow_voltage,
|
||||||
|
"trying to set {} as primary when it is also a secondary!",
|
||||||
|
self.name
|
||||||
|
);
|
||||||
|
|
||||||
|
self.voltage_tx = Some(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rx(&mut self) -> &mut tokio::sync::watch::Receiver<VoltageCommand> {
|
||||||
|
&mut self.voltage_rx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_command(&mut self, command: VoltageCommand) -> eyre::Result<()> {
|
||||||
|
match command {
|
||||||
|
VoltageCommand::Set(target_voltage) => {
|
||||||
|
if self.follow_voltage {
|
||||||
|
self.inner.set_target_voltage(target_voltage).await
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VoltageCommand::None => {
|
||||||
|
// todo: disable voltage control
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_new_settings(&self) -> bool {
|
||||||
|
self.settings_last_read.is_none_or(|t| {
|
||||||
|
std::time::Instant::now().duration_since(t) >= std::time::Duration::from_secs(60 * 60)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::large_enum_variant)]
|
||||||
pub enum ControllerInner {
|
pub enum ControllerInner {
|
||||||
Pl(crate::pl::Pli),
|
Pl(pl::Pli),
|
||||||
Tristar(crate::tristar::Tristar),
|
Tristar(tristar::Tristar),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControllerInner {
|
impl ControllerInner {
|
||||||
pub async fn refresh(&mut self) -> eyre::Result<CommonData> {
|
pub async fn refresh(&mut self) -> eyre::Result<ControllerState> {
|
||||||
match self {
|
match self {
|
||||||
ControllerInner::Pl(pli) => {
|
ControllerInner::Pl(pli) => {
|
||||||
let pl_data = pli.refresh().await?;
|
let pl_data = pli.refresh().await?;
|
||||||
Ok(pl_data)
|
|
||||||
|
Ok(ControllerState::Pl(pl_data))
|
||||||
}
|
}
|
||||||
ControllerInner::Tristar(tristar) => {
|
ControllerInner::Tristar(tristar) => {
|
||||||
let tristar_data = tristar.refresh().await?;
|
let tristar_data = tristar.refresh().await?;
|
||||||
Ok(tristar_data)
|
|
||||||
|
Ok(ControllerState::Tristar(tristar_data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_settings(&mut self) -> eyre::Result<ControllerSettings> {
|
||||||
|
match self {
|
||||||
|
ControllerInner::Pl(pl) => {
|
||||||
|
let settings = pl.read_settings().await?;
|
||||||
|
Ok(ControllerSettings::Pl(settings))
|
||||||
|
}
|
||||||
|
ControllerInner::Tristar(tristar) => {
|
||||||
|
let settings = tristar.read_settings().await?;
|
||||||
|
Ok(ControllerSettings::Tristar(settings))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_target_voltage(&mut self, target_voltage: f64) -> eyre::Result<()> {
|
||||||
|
match self {
|
||||||
|
ControllerInner::Pl(_) => todo!(),
|
||||||
|
ControllerInner::Tristar(tristar) => tristar.set_target_voltage(target_voltage).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
671
charge-controller-supervisor/src/controller/pl.rs
Normal file
671
charge-controller-supervisor/src/controller/pl.rs
Normal file
|
@ -0,0 +1,671 @@
|
||||||
|
use chrono::Timelike;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio_serial::{SerialPort, SerialPortBuilderExt};
|
||||||
|
|
||||||
|
use crate::gauges::{
|
||||||
|
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, INPUT_CURRENT, PL_DUTY_CYCLE, PL_LOAD_CURRENT,
|
||||||
|
TARGET_VOLTAGE,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Pli {
|
||||||
|
friendly_name: String,
|
||||||
|
port: tokio_serial::SerialStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
||||||
|
pub struct PlState {
|
||||||
|
pub battery_voltage: f64,
|
||||||
|
pub target_voltage: f64,
|
||||||
|
pub duty_cycle: f64,
|
||||||
|
pub internal_charge_current: f64,
|
||||||
|
pub internal_load_current: f64,
|
||||||
|
pub battery_temp: f64,
|
||||||
|
pub regulator_state: RegulatorState,
|
||||||
|
// pub internal_charge_ah_accumulator: u16,
|
||||||
|
// pub external_charge_ah_accumulator: u16,
|
||||||
|
// pub internal_load_ah_accumulator: u16,
|
||||||
|
// pub external_load_ah_accumulator: u16,
|
||||||
|
// pub internal_charge_ah: u16,
|
||||||
|
// pub external_charge_ah: u16,
|
||||||
|
// pub internal_load_ah: u16,
|
||||||
|
// pub external_load_ah: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum RegulatorState {
|
||||||
|
Boost,
|
||||||
|
Equalise,
|
||||||
|
Absorption,
|
||||||
|
Float,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for RegulatorState {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
match value & 0b11 {
|
||||||
|
0b00 => Self::Boost,
|
||||||
|
0b01 => Self::Equalise,
|
||||||
|
0b10 => Self::Absorption,
|
||||||
|
0b11 => Self::Float,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RegulatorState> for u8 {
|
||||||
|
fn from(value: RegulatorState) -> Self {
|
||||||
|
match value {
|
||||||
|
RegulatorState::Boost => 0b00,
|
||||||
|
RegulatorState::Equalise => 0b01,
|
||||||
|
RegulatorState::Absorption => 0b10,
|
||||||
|
RegulatorState::Float => 0b11,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RegulatorState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Absorption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_regulator_gauges(state: RegulatorState, label: &str) {
|
||||||
|
let boost = CHARGE_STATE.with_label_values(&[label, "boost"]);
|
||||||
|
let equalise = CHARGE_STATE.with_label_values(&[label, "equalise"]);
|
||||||
|
let absorption = CHARGE_STATE.with_label_values(&[label, "absorption"]);
|
||||||
|
let float = CHARGE_STATE.with_label_values(&[label, "float"]);
|
||||||
|
|
||||||
|
match state {
|
||||||
|
RegulatorState::Boost => {
|
||||||
|
boost.set(1);
|
||||||
|
equalise.set(0);
|
||||||
|
absorption.set(0);
|
||||||
|
float.set(0);
|
||||||
|
}
|
||||||
|
RegulatorState::Equalise => {
|
||||||
|
boost.set(0);
|
||||||
|
equalise.set(1);
|
||||||
|
absorption.set(0);
|
||||||
|
float.set(0);
|
||||||
|
}
|
||||||
|
RegulatorState::Absorption => {
|
||||||
|
boost.set(0);
|
||||||
|
equalise.set(0);
|
||||||
|
absorption.set(1);
|
||||||
|
float.set(0);
|
||||||
|
}
|
||||||
|
RegulatorState::Float => {
|
||||||
|
boost.set(0);
|
||||||
|
equalise.set(0);
|
||||||
|
absorption.set(0);
|
||||||
|
float.set(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PlSettings {
|
||||||
|
load_disconnect_voltage: f64,
|
||||||
|
load_reconnect_voltage: f64,
|
||||||
|
delay_before_disconnect_minutes: u8,
|
||||||
|
days_between_boost: u8,
|
||||||
|
absorption_time: u8,
|
||||||
|
hysteresis: u8,
|
||||||
|
boost_return_voltage: f64,
|
||||||
|
charge_current_limit: u8,
|
||||||
|
battery_2_regulation_voltage: f64,
|
||||||
|
days_between_equalization: u8,
|
||||||
|
equalization_length: u8,
|
||||||
|
absorption_voltage: f64,
|
||||||
|
equalization_voltage: f64,
|
||||||
|
float_voltage: f64,
|
||||||
|
boost_voltage: f64,
|
||||||
|
program: Program,
|
||||||
|
system_voltage: SystemVoltage,
|
||||||
|
battery_capacity_ah: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum Program {
|
||||||
|
Prog0,
|
||||||
|
Prog1,
|
||||||
|
Prog2,
|
||||||
|
Prog3,
|
||||||
|
Prog4,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum SystemVoltage {
|
||||||
|
V12,
|
||||||
|
V24,
|
||||||
|
V32,
|
||||||
|
V36,
|
||||||
|
V48,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code, reason = "writing settings is not yet implemented")]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum PliRequest {
|
||||||
|
ReadRam(u8),
|
||||||
|
ReadEeprom(u8),
|
||||||
|
SyncTime,
|
||||||
|
SetRegulatorState(RegulatorState),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pli {
|
||||||
|
pub fn new(
|
||||||
|
serial_port: &str,
|
||||||
|
friendly_name: &str,
|
||||||
|
baud_rate: u32,
|
||||||
|
timeout: u64,
|
||||||
|
) -> Result<Self, tokio_serial::Error> {
|
||||||
|
let port = tokio_serial::new(serial_port, baud_rate)
|
||||||
|
.timeout(std::time::Duration::from_millis(timeout))
|
||||||
|
.open_native_async()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
friendly_name: friendly_name.to_owned(),
|
||||||
|
port,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh(&mut self) -> eyre::Result<PlState> {
|
||||||
|
let new_state = self.read_state().await?;
|
||||||
|
BATTERY_VOLTAGE
|
||||||
|
.with_label_values(&[&self.friendly_name])
|
||||||
|
.set(new_state.battery_voltage);
|
||||||
|
TARGET_VOLTAGE
|
||||||
|
.with_label_values(&[&self.friendly_name])
|
||||||
|
.set(new_state.target_voltage);
|
||||||
|
PL_DUTY_CYCLE
|
||||||
|
.with_label_values(&[&self.friendly_name])
|
||||||
|
.set(new_state.duty_cycle);
|
||||||
|
INPUT_CURRENT
|
||||||
|
.with_label_values(&[&self.friendly_name])
|
||||||
|
.set(new_state.internal_charge_current);
|
||||||
|
PL_LOAD_CURRENT
|
||||||
|
.with_label_values(&[&self.friendly_name])
|
||||||
|
.set(new_state.internal_load_current);
|
||||||
|
BATTERY_TEMP
|
||||||
|
.with_label_values(&[&self.friendly_name])
|
||||||
|
.set(new_state.battery_temp);
|
||||||
|
|
||||||
|
set_regulator_gauges(new_state.regulator_state, &self.friendly_name);
|
||||||
|
|
||||||
|
Ok(new_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code, reason = "writing settings is not yet implemented")]
|
||||||
|
pub async fn process_request(&mut self, message: PliRequest) -> eyre::Result<()> {
|
||||||
|
match message {
|
||||||
|
PliRequest::ReadRam(address) => {
|
||||||
|
let data = self.read_ram_with_retires(address).await?;
|
||||||
|
log::warn!("Read RAM at {address}: data {data}");
|
||||||
|
}
|
||||||
|
PliRequest::ReadEeprom(address) => {
|
||||||
|
let data = self.read_eeprom(address).await?;
|
||||||
|
log::warn!("Read EEPROM at {address}: data {data}");
|
||||||
|
}
|
||||||
|
PliRequest::SyncTime => {
|
||||||
|
let now = chrono::Local::now();
|
||||||
|
let timestamp = (((now.hour() * 10) + (now.minute() / 6)) & 0xFF) as u8;
|
||||||
|
let min = (now.minute() % 6) as u8;
|
||||||
|
let sec = (now.second() / 2).min(29) as u8;
|
||||||
|
self.write_ram(PlRamAddress::Hour, timestamp).await?;
|
||||||
|
self.write_ram(PlRamAddress::Min, min).await?;
|
||||||
|
self.write_ram(PlRamAddress::Sec, sec).await?;
|
||||||
|
log::warn!(
|
||||||
|
"Set time: {now} corresponds to {timestamp} + minutes {min} + seconds {sec}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PliRequest::SetRegulatorState(state) => {
|
||||||
|
log::warn!("Setting regulator state to {state:?}");
|
||||||
|
self.write_ram(PlRamAddress::Rstate, state.into()).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_settings(&mut self) -> eyre::Result<PlSettings> {
|
||||||
|
let load_disconnect_voltage =
|
||||||
|
f64::from(self.read_eeprom(PlEepromAddress::LOff).await?) * (4. / 10.);
|
||||||
|
let load_reconnect_voltage =
|
||||||
|
f64::from(self.read_eeprom(PlEepromAddress::LOn).await?) * (4. / 10.);
|
||||||
|
let delay_before_disconnect_minutes = self.read_eeprom(PlEepromAddress::LDel).await?;
|
||||||
|
let days_between_boost = self.read_eeprom(PlEepromAddress::BstFreq).await?;
|
||||||
|
let absorption_time = self.read_eeprom(PlEepromAddress::ATim).await?;
|
||||||
|
let hysteresis = self.read_eeprom(PlEepromAddress::Hyst).await?;
|
||||||
|
let boost_return_voltage =
|
||||||
|
f64::from(self.read_eeprom(PlEepromAddress::BRet).await?) * (4. / 10.);
|
||||||
|
let charge_current_limit = self.read_eeprom(PlEepromAddress::CurLim).await?;
|
||||||
|
let battery_2_regulation_voltage =
|
||||||
|
f64::from(self.read_eeprom(PlEepromAddress::Bat2).await?) * (4. / 10.);
|
||||||
|
let days_between_equalization = self.read_eeprom(PlEepromAddress::EqFreq).await?;
|
||||||
|
let equalization_length = self.read_eeprom(PlEepromAddress::ETim).await?;
|
||||||
|
let absorption_voltage =
|
||||||
|
f64::from(self.read_eeprom(PlEepromAddress::AbsV).await?) * (4. / 10.);
|
||||||
|
let equalization_voltage =
|
||||||
|
f64::from(self.read_eeprom(PlEepromAddress::EMax).await?) * (4. / 10.);
|
||||||
|
let float_voltage = f64::from(self.read_eeprom(PlEepromAddress::FltV).await?) * (4. / 10.);
|
||||||
|
let boost_voltage = f64::from(self.read_eeprom(PlEepromAddress::BMax).await?) * (4. / 10.);
|
||||||
|
let volt = self.read_eeprom(PlEepromAddress::Volt).await?;
|
||||||
|
let program = match volt >> 4 {
|
||||||
|
0 => Program::Prog0,
|
||||||
|
1 => Program::Prog1,
|
||||||
|
2 => Program::Prog2,
|
||||||
|
3 => Program::Prog3,
|
||||||
|
4 => Program::Prog4,
|
||||||
|
_ => Program::Unknown,
|
||||||
|
};
|
||||||
|
let system_voltage = match volt & 0xF {
|
||||||
|
0 => SystemVoltage::V12,
|
||||||
|
1 => SystemVoltage::V24,
|
||||||
|
2 => SystemVoltage::V32,
|
||||||
|
3 => SystemVoltage::V36,
|
||||||
|
4 => SystemVoltage::V48,
|
||||||
|
_ => SystemVoltage::Unknown,
|
||||||
|
};
|
||||||
|
let battery_cap = usize::from(self.read_eeprom(PlEepromAddress::BCap).await?);
|
||||||
|
|
||||||
|
let battery_capacity_ah = if battery_cap > 50 {
|
||||||
|
(50 * 20) + ((battery_cap - 50) * 100)
|
||||||
|
} else {
|
||||||
|
battery_cap * 20
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PlSettings {
|
||||||
|
load_disconnect_voltage,
|
||||||
|
load_reconnect_voltage,
|
||||||
|
delay_before_disconnect_minutes,
|
||||||
|
days_between_boost,
|
||||||
|
absorption_time,
|
||||||
|
hysteresis,
|
||||||
|
boost_return_voltage,
|
||||||
|
charge_current_limit,
|
||||||
|
battery_2_regulation_voltage,
|
||||||
|
days_between_equalization,
|
||||||
|
equalization_length,
|
||||||
|
absorption_voltage,
|
||||||
|
equalization_voltage,
|
||||||
|
float_voltage,
|
||||||
|
boost_voltage,
|
||||||
|
program,
|
||||||
|
system_voltage,
|
||||||
|
battery_capacity_ah,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_state(&mut self) -> eyre::Result<PlState> {
|
||||||
|
// let int_charge_acc_low = self.read_ram(PlRamAddress::Ciacc1).await?;
|
||||||
|
// let int_charge_acc = self.read_ram(PlRamAddress::Ciacc2).await?;
|
||||||
|
// let int_charge_acc_high = self.read_ram(PlRamAddress::Ciacc3).await?;
|
||||||
|
// let mut internal_charge_ah_accumulator = u16::from(int_charge_acc_high) << 9;
|
||||||
|
// internal_charge_ah_accumulator |= u16::from(int_charge_acc) << 1;
|
||||||
|
// internal_charge_ah_accumulator |= u16::from(int_charge_acc_low & 0b1);
|
||||||
|
// let int_charge_low = self.read_ram(PlRamAddress::Ciahl).await?;
|
||||||
|
// let int_charge_high = self.read_ram(PlRamAddress::Ciahh).await?;
|
||||||
|
// let internal_charge_ah = u16::from_le_bytes([int_charge_low, int_charge_high]);
|
||||||
|
|
||||||
|
// let int_load_acc_low = self.read_ram(PlRamAddress::Liacc1).await?;
|
||||||
|
// let int_load_acc = self.read_ram(PlRamAddress::Liacc2).await?;
|
||||||
|
// let int_load_acc_high = self.read_ram(PlRamAddress::Liacc3).await?;
|
||||||
|
// let mut internal_load_ah_accumulator = u16::from(int_load_acc_high) << 9;
|
||||||
|
// internal_load_ah_accumulator |= u16::from(int_load_acc) << 1;
|
||||||
|
// internal_load_ah_accumulator |= u16::from(int_load_acc_low & 0b1);
|
||||||
|
// let int_load_low = self.read_ram(PlRamAddress::Liahl).await?;
|
||||||
|
// let int_load_high = self.read_ram(PlRamAddress::Liahh).await?;
|
||||||
|
// let internal_load_ah = u16::from_le_bytes([int_load_low, int_load_high]);
|
||||||
|
|
||||||
|
// let ext_charge_acc_low = self.read_ram(PlRamAddress::Ceacc1).await?;
|
||||||
|
// let ext_charge_acc = self.read_ram(PlRamAddress::Ceacc2).await?;
|
||||||
|
// let ext_charge_acc_high = self.read_ram(PlRamAddress::Ceacc3).await?;
|
||||||
|
// let mut external_charge_ah_accumulator = u16::from(ext_charge_acc_high) << 9;
|
||||||
|
// external_charge_ah_accumulator |= u16::from(ext_charge_acc) << 1;
|
||||||
|
// external_charge_ah_accumulator |= u16::from(ext_charge_acc_low & 0b1);
|
||||||
|
// let ext_charge_low = self.read_ram(PlRamAddress::Ceahl).await?;
|
||||||
|
// let ext_charge_high = self.read_ram(PlRamAddress::Ceahh).await?;
|
||||||
|
// let external_charge_ah = u16::from_le_bytes([ext_charge_low, ext_charge_high]);
|
||||||
|
|
||||||
|
// let ext_load_acc_low = self.read_ram(PlRamAddress::Leacc1).await?;
|
||||||
|
// let ext_load_acc = self.read_ram(PlRamAddress::Leacc2).await?;
|
||||||
|
// let ext_load_acc_high = self.read_ram(PlRamAddress::Leacc3).await?;
|
||||||
|
// let mut external_load_ah_accumulator = u16::from(ext_load_acc_high) << 9;
|
||||||
|
// external_load_ah_accumulator |= u16::from(ext_load_acc) << 1;
|
||||||
|
// external_load_ah_accumulator |= u16::from(ext_load_acc_low & 0b1);
|
||||||
|
// let ext_load_low = self.read_ram(PlRamAddress::Leahl).await?;
|
||||||
|
// let ext_load_high = self.read_ram(PlRamAddress::Leahh).await?;
|
||||||
|
// let external_load_ah = u16::from_le_bytes([ext_load_low, ext_load_high]);
|
||||||
|
|
||||||
|
Ok(PlState {
|
||||||
|
battery_voltage: f64::from(self.read_ram_with_retires(PlRamAddress::Batv).await?)
|
||||||
|
* (4. / 10.),
|
||||||
|
target_voltage: f64::from(self.read_ram_with_retires(PlRamAddress::Vreg).await?)
|
||||||
|
* (4. / 10.),
|
||||||
|
duty_cycle: f64::from(self.read_ram_with_retires(PlRamAddress::Dutycyc).await?) / 255.,
|
||||||
|
internal_charge_current: f64::from(
|
||||||
|
self.read_ram_with_retires(PlRamAddress::Cint).await?,
|
||||||
|
) * (4. / 10.),
|
||||||
|
internal_load_current: f64::from(self.read_ram_with_retires(PlRamAddress::Lint).await?)
|
||||||
|
* (2. / 10.),
|
||||||
|
battery_temp: f64::from(self.read_ram_with_retires(PlRamAddress::Battemp).await?),
|
||||||
|
regulator_state: self
|
||||||
|
.read_ram_with_retires(PlRamAddress::Rstate)
|
||||||
|
.await?
|
||||||
|
.into(),
|
||||||
|
// internal_charge_ah_accumulator,
|
||||||
|
// external_charge_ah_accumulator,
|
||||||
|
// internal_load_ah_accumulator,
|
||||||
|
// external_load_ah_accumulator,
|
||||||
|
// internal_charge_ah,
|
||||||
|
// external_charge_ah,
|
||||||
|
// internal_load_ah,
|
||||||
|
// external_load_ah,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_command(&mut self, req: [u8; 4]) -> eyre::Result<()> {
|
||||||
|
self.flush()?;
|
||||||
|
self.port.write_all(&req).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) -> eyre::Result<()> {
|
||||||
|
self.port.clear(tokio_serial::ClearBuffer::All)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive<const LENGTH: usize>(&mut self) -> eyre::Result<[u8; LENGTH]> {
|
||||||
|
let mut buf = [0; LENGTH];
|
||||||
|
self.port.read_exact(&mut buf).await?;
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_ram_with_retires<T>(&mut self, address: T) -> eyre::Result<u8>
|
||||||
|
where
|
||||||
|
T: Into<u8>,
|
||||||
|
{
|
||||||
|
const READ_TRIES: usize = 3;
|
||||||
|
let address: u8 = address.into();
|
||||||
|
|
||||||
|
let mut last_err = None;
|
||||||
|
for _ in 0..READ_TRIES {
|
||||||
|
match self.read_ram_single(address).await {
|
||||||
|
Ok(v) => return Ok(v),
|
||||||
|
Err(e) => last_err = Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_err.unwrap_or_else(|| {
|
||||||
|
eyre::eyre!(
|
||||||
|
"no error was stored in read_ram_with_retries: this should be unreachable??"
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_ram_single<T>(&mut self, address: T) -> eyre::Result<u8>
|
||||||
|
where
|
||||||
|
T: Into<u8>,
|
||||||
|
{
|
||||||
|
self.send_command(command(20, address.into(), 0)).await?;
|
||||||
|
let response = self.get_response().await?;
|
||||||
|
match response {
|
||||||
|
Ok(()) => Ok(self.receive::<1>().await?[0]),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_ram<T>(&mut self, address: T, data: u8) -> eyre::Result<()>
|
||||||
|
where
|
||||||
|
T: Into<u8>,
|
||||||
|
{
|
||||||
|
self.send_command(command(152, address.into(), data))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_eeprom<T>(&mut self, address: T) -> eyre::Result<u8>
|
||||||
|
where
|
||||||
|
T: Into<u8>,
|
||||||
|
{
|
||||||
|
self.send_command(command(72, address.into(), 0)).await?;
|
||||||
|
|
||||||
|
let response = self.get_response().await?;
|
||||||
|
match response {
|
||||||
|
Ok(()) => Ok(self.receive::<1>().await?[0]),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_response(&mut self) -> eyre::Result<Result<(), PlError>> {
|
||||||
|
let res = self.receive::<1>().await?[0];
|
||||||
|
Ok(PlError::parse(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
enum PlError {
|
||||||
|
#[error("No comms or corrupt comms")]
|
||||||
|
NoComms,
|
||||||
|
#[error("Loopback response code")]
|
||||||
|
Loopback,
|
||||||
|
#[error("Timeout error")]
|
||||||
|
Timeout,
|
||||||
|
#[error("Checksum error in PLI receive data.")]
|
||||||
|
Checksum,
|
||||||
|
#[error("Command received by PLI is not recognised.")]
|
||||||
|
CommandNotRecognised,
|
||||||
|
#[error("Unused - or could be returning PL40 version!")]
|
||||||
|
Unused1,
|
||||||
|
#[error("Processor did not receive a reply to request.")]
|
||||||
|
NoReply,
|
||||||
|
#[error("Error in reply from PL.")]
|
||||||
|
ErrorFromPl,
|
||||||
|
#[error("<not used> #2")]
|
||||||
|
Unused2,
|
||||||
|
#[error("<not used> #3")]
|
||||||
|
Unused3,
|
||||||
|
#[error("Unknown: {0:#X?}")]
|
||||||
|
Unknown(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlError {
|
||||||
|
const fn parse(value: u8) -> Result<(), Self> {
|
||||||
|
match value {
|
||||||
|
200 => Ok(()),
|
||||||
|
5 => Err(Self::NoComms),
|
||||||
|
128 => Err(Self::Loopback),
|
||||||
|
129 => Err(Self::Timeout),
|
||||||
|
130 => Err(Self::Checksum),
|
||||||
|
131 => Err(Self::CommandNotRecognised),
|
||||||
|
132 => Err(Self::Unused1),
|
||||||
|
133 => Err(Self::NoReply),
|
||||||
|
134 => Err(Self::ErrorFromPl),
|
||||||
|
135 => Err(Self::Unused2),
|
||||||
|
136 => Err(Self::Unused3),
|
||||||
|
v => Err(Self::Unknown(v)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum PlRamAddress {
|
||||||
|
Dutycyc,
|
||||||
|
Sec,
|
||||||
|
Min,
|
||||||
|
Hour,
|
||||||
|
Batv,
|
||||||
|
Battemp,
|
||||||
|
Rstate,
|
||||||
|
Vreg,
|
||||||
|
Cint,
|
||||||
|
Lint,
|
||||||
|
Ciacc1,
|
||||||
|
Ciacc2,
|
||||||
|
Ciacc3,
|
||||||
|
Ciahl,
|
||||||
|
Ciahh,
|
||||||
|
Ceacc1,
|
||||||
|
Ceacc2,
|
||||||
|
Ceacc3,
|
||||||
|
Ceahl,
|
||||||
|
Ceahh,
|
||||||
|
Liacc1,
|
||||||
|
Liacc2,
|
||||||
|
Liacc3,
|
||||||
|
Liahl,
|
||||||
|
Liahh,
|
||||||
|
Leacc1,
|
||||||
|
Leacc2,
|
||||||
|
Leacc3,
|
||||||
|
Leahl,
|
||||||
|
Leahh,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlRamAddress> for u8 {
|
||||||
|
fn from(value: PlRamAddress) -> Self {
|
||||||
|
match value {
|
||||||
|
PlRamAddress::Dutycyc => 39,
|
||||||
|
PlRamAddress::Sec => 46,
|
||||||
|
PlRamAddress::Min => 47,
|
||||||
|
PlRamAddress::Hour => 48,
|
||||||
|
PlRamAddress::Batv => 50,
|
||||||
|
PlRamAddress::Battemp => 52,
|
||||||
|
PlRamAddress::Rstate => 101,
|
||||||
|
PlRamAddress::Vreg => 105,
|
||||||
|
PlRamAddress::Cint => 213,
|
||||||
|
PlRamAddress::Lint => 217,
|
||||||
|
PlRamAddress::Ciacc1 => 0xB9,
|
||||||
|
PlRamAddress::Ciacc2 => 0xBA,
|
||||||
|
PlRamAddress::Ciacc3 => 0xBB,
|
||||||
|
PlRamAddress::Ciahl => 0xBC,
|
||||||
|
PlRamAddress::Ciahh => 0xBD,
|
||||||
|
PlRamAddress::Ceacc1 => 0xBE,
|
||||||
|
PlRamAddress::Ceacc2 => 0xBF,
|
||||||
|
PlRamAddress::Ceacc3 => 0xC0,
|
||||||
|
PlRamAddress::Ceahl => 0xC1,
|
||||||
|
PlRamAddress::Ceahh => 0xC2,
|
||||||
|
PlRamAddress::Liacc1 => 0xC3,
|
||||||
|
PlRamAddress::Liacc2 => 0xC4,
|
||||||
|
PlRamAddress::Liacc3 => 0xC5,
|
||||||
|
PlRamAddress::Liahl => 0xC6,
|
||||||
|
PlRamAddress::Liahh => 0xC7,
|
||||||
|
PlRamAddress::Leacc1 => 0xC8,
|
||||||
|
PlRamAddress::Leacc2 => 0xC9,
|
||||||
|
PlRamAddress::Leacc3 => 0xCA,
|
||||||
|
PlRamAddress::Leahl => 0xCB,
|
||||||
|
PlRamAddress::Leahh => 0xCC,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum PlEepromAddress {
|
||||||
|
// calibration
|
||||||
|
BCals,
|
||||||
|
BCal12,
|
||||||
|
BCal24,
|
||||||
|
BCal48,
|
||||||
|
ChargeOffset,
|
||||||
|
ChargeGain,
|
||||||
|
LoadOffset,
|
||||||
|
LoadGain,
|
||||||
|
BatTmpOffset,
|
||||||
|
BatTmpGain,
|
||||||
|
SolarOffset,
|
||||||
|
SolarGain,
|
||||||
|
BatsenOffset,
|
||||||
|
BatsenGain,
|
||||||
|
|
||||||
|
// settings
|
||||||
|
GOn,
|
||||||
|
GOff,
|
||||||
|
GDel,
|
||||||
|
GExF,
|
||||||
|
GRun,
|
||||||
|
LOff,
|
||||||
|
LOn,
|
||||||
|
LDel,
|
||||||
|
ASet,
|
||||||
|
BstFreq,
|
||||||
|
ATim,
|
||||||
|
Hyst,
|
||||||
|
BRet,
|
||||||
|
CurLim,
|
||||||
|
Bat2,
|
||||||
|
ESet1,
|
||||||
|
ESet2,
|
||||||
|
ESet3,
|
||||||
|
EqFreq,
|
||||||
|
ETim,
|
||||||
|
AbsV,
|
||||||
|
EMax,
|
||||||
|
FltV,
|
||||||
|
BMax,
|
||||||
|
LGSet,
|
||||||
|
PwmE,
|
||||||
|
SStop,
|
||||||
|
EtMod,
|
||||||
|
GMod,
|
||||||
|
Volt,
|
||||||
|
BCap,
|
||||||
|
HistoryDataPtr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlEepromAddress> for u8 {
|
||||||
|
fn from(value: PlEepromAddress) -> Self {
|
||||||
|
match value {
|
||||||
|
PlEepromAddress::BCals => 0x00,
|
||||||
|
PlEepromAddress::BCal12 => 0x01,
|
||||||
|
PlEepromAddress::BCal24 => 0x02,
|
||||||
|
PlEepromAddress::BCal48 => 0x03,
|
||||||
|
PlEepromAddress::ChargeOffset => 0x04,
|
||||||
|
PlEepromAddress::ChargeGain => 0x05,
|
||||||
|
PlEepromAddress::LoadOffset => 0x06,
|
||||||
|
PlEepromAddress::LoadGain => 0x07,
|
||||||
|
PlEepromAddress::BatTmpOffset => 0x08,
|
||||||
|
PlEepromAddress::BatTmpGain => 0x09,
|
||||||
|
PlEepromAddress::SolarOffset => 0x0A,
|
||||||
|
PlEepromAddress::SolarGain => 0x0B,
|
||||||
|
PlEepromAddress::BatsenOffset => 0x0C,
|
||||||
|
PlEepromAddress::BatsenGain => 0x0D,
|
||||||
|
PlEepromAddress::GOn => 0x0E,
|
||||||
|
PlEepromAddress::GOff => 0x0F,
|
||||||
|
PlEepromAddress::GDel => 0x10,
|
||||||
|
PlEepromAddress::GExF => 0x11,
|
||||||
|
PlEepromAddress::GRun => 0x12,
|
||||||
|
PlEepromAddress::LOff => 0x13,
|
||||||
|
PlEepromAddress::LOn => 0x14,
|
||||||
|
PlEepromAddress::LDel => 0x15,
|
||||||
|
PlEepromAddress::ASet => 0x16,
|
||||||
|
PlEepromAddress::BstFreq => 0x17,
|
||||||
|
PlEepromAddress::ATim => 0x18,
|
||||||
|
PlEepromAddress::Hyst => 0x19,
|
||||||
|
PlEepromAddress::BRet => 0x1A,
|
||||||
|
PlEepromAddress::CurLim => 0x1B,
|
||||||
|
PlEepromAddress::Bat2 => 0x1C,
|
||||||
|
PlEepromAddress::ESet1 => 0x1D,
|
||||||
|
PlEepromAddress::ESet2 => 0x1E,
|
||||||
|
PlEepromAddress::ESet3 => 0x1F,
|
||||||
|
PlEepromAddress::EqFreq => 0x20,
|
||||||
|
PlEepromAddress::ETim => 0x21,
|
||||||
|
PlEepromAddress::AbsV => 0x22,
|
||||||
|
PlEepromAddress::EMax => 0x23,
|
||||||
|
PlEepromAddress::FltV => 0x24,
|
||||||
|
PlEepromAddress::BMax => 0x25,
|
||||||
|
PlEepromAddress::LGSet => 0x26,
|
||||||
|
PlEepromAddress::PwmE => 0x27,
|
||||||
|
PlEepromAddress::SStop => 0x28,
|
||||||
|
PlEepromAddress::EtMod => 0x29,
|
||||||
|
PlEepromAddress::GMod => 0x2A,
|
||||||
|
PlEepromAddress::Volt => 0x2B,
|
||||||
|
PlEepromAddress::BCap => 0x2c,
|
||||||
|
PlEepromAddress::HistoryDataPtr => 0x2d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn command(operation: u8, address: u8, data: u8) -> [u8; 4] {
|
||||||
|
[operation, address, data, !operation]
|
||||||
|
}
|
1001
charge-controller-supervisor/src/controller/tristar.rs
Normal file
1001
charge-controller-supervisor/src/controller/tristar.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,192 @@
|
||||||
|
pub struct ModbusTimeout {
|
||||||
|
context: Option<tokio_modbus::client::Context>,
|
||||||
|
transport_settings: crate::config::Transport,
|
||||||
|
counters: Counters,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODBUS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||||
|
|
||||||
|
async fn connect(
|
||||||
|
transport_settings: &crate::config::Transport,
|
||||||
|
) -> eyre::Result<tokio_modbus::client::Context> {
|
||||||
|
let slave = tokio_modbus::Slave(super::DEVICE_ID);
|
||||||
|
|
||||||
|
let modbus = match transport_settings {
|
||||||
|
crate::config::Transport::Serial { port, baud_rate } => {
|
||||||
|
let modbus_serial =
|
||||||
|
tokio_serial::SerialStream::open(&tokio_serial::new(port, *baud_rate))?;
|
||||||
|
tokio_modbus::client::rtu::attach_slave(modbus_serial, slave)
|
||||||
|
}
|
||||||
|
crate::config::Transport::Tcp { ip, port } => {
|
||||||
|
let modbus_tcp = tokio::net::TcpStream::connect((*ip, *port)).await?;
|
||||||
|
tokio_modbus::client::tcp::attach_slave(modbus_tcp, slave)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(modbus)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModbusDeviceResult<T> = Result<T, tokio_modbus::ExceptionCode>;
|
||||||
|
type ModbusResult<T> = Result<ModbusDeviceResult<T>, tokio_modbus::Error>;
|
||||||
|
|
||||||
|
type ContextFuture<'a, R> = dyn std::future::Future<Output = ModbusResult<R>> + Send + 'a;
|
||||||
|
type ContextFn<R, D> =
|
||||||
|
fn(&mut tokio_modbus::client::Context, D) -> std::pin::Pin<Box<ContextFuture<'_, R>>>;
|
||||||
|
|
||||||
|
trait TryInsert {
|
||||||
|
type T;
|
||||||
|
async fn get_or_try_insert_with<F, R, Fut>(&mut self, f: F) -> Result<&mut Self::T, R>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = Result<Self::T, R>>,
|
||||||
|
F: FnOnce() -> Fut;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryInsert for Option<T> {
|
||||||
|
type T = T;
|
||||||
|
|
||||||
|
async fn get_or_try_insert_with<F, R, Fut>(&mut self, f: F) -> Result<&mut Self::T, R>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = Result<Self::T, R>>,
|
||||||
|
F: FnOnce() -> Fut,
|
||||||
|
{
|
||||||
|
if self.is_none() {
|
||||||
|
let got = f().await?;
|
||||||
|
*self = Some(got);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a `None` variant for `self` would have been replaced by a `Some` variant
|
||||||
|
// in the code above, or the ? would have caused an early return
|
||||||
|
Ok(self.as_mut().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct Counters {
|
||||||
|
gateway: usize,
|
||||||
|
timeout: usize,
|
||||||
|
protocol: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_ERRORS: usize = 2;
|
||||||
|
|
||||||
|
impl Counters {
|
||||||
|
fn reset(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn any_above_max(&self) -> bool {
|
||||||
|
self.gateway > MAX_ERRORS || self.timeout > MAX_ERRORS || self.protocol > MAX_ERRORS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NUM_TRIES: usize = 3;
|
||||||
|
|
||||||
|
impl ModbusTimeout {
|
||||||
|
pub async fn new(transport_settings: crate::config::Transport) -> eyre::Result<Self> {
|
||||||
|
let context = Some(connect(&transport_settings).await?);
|
||||||
|
Ok(Self {
|
||||||
|
context,
|
||||||
|
transport_settings,
|
||||||
|
counters: Counters::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn with_context<R, D: Copy>(
|
||||||
|
&mut self,
|
||||||
|
f: ContextFn<R, D>,
|
||||||
|
data: D,
|
||||||
|
) -> eyre::Result<Result<R, tokio_modbus::ExceptionCode>> {
|
||||||
|
let mut last_err = None;
|
||||||
|
for _ in 0..NUM_TRIES {
|
||||||
|
if let Ok(context) = self
|
||||||
|
.context
|
||||||
|
.get_or_try_insert_with(async || connect(&self.transport_settings).await)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let res = tokio::time::timeout(MODBUS_TIMEOUT, f(context, data)).await;
|
||||||
|
match res {
|
||||||
|
Ok(Ok(Err(e)))
|
||||||
|
if e == tokio_modbus::ExceptionCode::GatewayTargetDevice
|
||||||
|
|| e == tokio_modbus::ExceptionCode::GatewayPathUnavailable =>
|
||||||
|
{
|
||||||
|
log::warn!("gateway error: {e:?}");
|
||||||
|
last_err = Some(e.into());
|
||||||
|
self.counters.gateway += 1;
|
||||||
|
}
|
||||||
|
Ok(Ok(v)) => {
|
||||||
|
self.counters.reset();
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
Ok(Err(tokio_modbus::Error::Protocol(e))) => {
|
||||||
|
// protocol error
|
||||||
|
log::warn!("protocol error: {e:?}");
|
||||||
|
last_err = Some(e.into());
|
||||||
|
self.counters.protocol += 1;
|
||||||
|
}
|
||||||
|
Ok(Err(tokio_modbus::Error::Transport(e))) => {
|
||||||
|
// transport error
|
||||||
|
log::warn!("reconnecting due to transport error: {e:?}");
|
||||||
|
last_err = Some(e.into());
|
||||||
|
self.context = None;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// timeout
|
||||||
|
last_err = Some(eyre::eyre!("timeout"));
|
||||||
|
self.counters.timeout += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.counters.any_above_max() {
|
||||||
|
self.context = None;
|
||||||
|
log::warn!(
|
||||||
|
"reconnecting due to multiple errors without a successful operation: {:?}",
|
||||||
|
self.counters
|
||||||
|
);
|
||||||
|
self.counters.reset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// failed to reconnect
|
||||||
|
return Err(eyre::eyre!("failed to reconnect to controller"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_err.unwrap_or_else(|| eyre::eyre!("unknown last error????")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_single_register(
|
||||||
|
&mut self,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
word: u16,
|
||||||
|
) -> eyre::Result<ModbusDeviceResult<()>> {
|
||||||
|
async fn write(
|
||||||
|
context: &mut tokio_modbus::client::Context,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
word: u16,
|
||||||
|
) -> ModbusResult<()> {
|
||||||
|
use tokio_modbus::client::Writer;
|
||||||
|
context.write_single_register(addr, word).await
|
||||||
|
}
|
||||||
|
|
||||||
|
let fut: ContextFn<(), _> = |context, (addr, word)| Box::pin(write(context, addr, word));
|
||||||
|
let r = self.with_context(fut, (addr, word)).await?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_holding_registers(
|
||||||
|
&mut self,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
cnt: tokio_modbus::Quantity,
|
||||||
|
) -> eyre::Result<ModbusDeviceResult<Vec<u16>>> {
|
||||||
|
async fn read(
|
||||||
|
context: &mut tokio_modbus::client::Context,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
cnt: tokio_modbus::Quantity,
|
||||||
|
) -> ModbusResult<Vec<u16>> {
|
||||||
|
use tokio_modbus::client::Reader;
|
||||||
|
context.read_holding_registers(addr, cnt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
let fut: ContextFn<_, _> = |context, (addr, cnt)| Box::pin(read(context, addr, cnt));
|
||||||
|
let res = self.with_context(fut, (addr, cnt)).await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,8 @@ lazy_static! {
|
||||||
&[PL_LABEL]
|
&[PL_LABEL]
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
pub static ref HEATSINK_TEMP: GaugeVec =
|
||||||
|
register_gauge_vec!("heatsink_temp", "Heatsink temperature", &[TRISTAR_LABEL]).unwrap();
|
||||||
pub static ref TRISTAR_INPUT_VOLTAGE: GaugeVec =
|
pub static ref TRISTAR_INPUT_VOLTAGE: GaugeVec =
|
||||||
register_gauge_vec!("tristar_input_voltage", "Input voltage", &[TRISTAR_LABEL]).unwrap();
|
register_gauge_vec!("tristar_input_voltage", "Input voltage", &[TRISTAR_LABEL]).unwrap();
|
||||||
pub static ref TRISTAR_CHARGE_CURRENT: GaugeVec =
|
pub static ref TRISTAR_CHARGE_CURRENT: GaugeVec =
|
||||||
|
@ -67,4 +69,16 @@ lazy_static! {
|
||||||
&[TRISTAR_LABEL]
|
&[TRISTAR_LABEL]
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
pub static ref TRISTAR_TOTAL_AH_CHARGE_DAILY: GaugeVec = register_gauge_vec!(
|
||||||
|
"tristar_total_ah_charge_daily",
|
||||||
|
"Total charge daily (Ah)",
|
||||||
|
&[TRISTAR_LABEL]
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
pub static ref TRISTAR_TOTAL_WH_CHARGE_DAILY: GaugeVec = register_gauge_vec!(
|
||||||
|
"tristar_total_wh_charge_daily",
|
||||||
|
"Total charge daily (Wh)",
|
||||||
|
&[TRISTAR_LABEL]
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![feature(let_chains)]
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -23,8 +25,8 @@ enum Commands {
|
||||||
|
|
||||||
mod controller;
|
mod controller;
|
||||||
mod gauges;
|
mod gauges;
|
||||||
mod pl;
|
|
||||||
mod tristar;
|
mod storage;
|
||||||
|
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
|
@ -57,7 +59,32 @@ async fn run() -> eyre::Result<()> {
|
||||||
match args.command {
|
match args.command {
|
||||||
Commands::Watch => watch(args).await,
|
Commands::Watch => watch(args).await,
|
||||||
Commands::GenerateConfig => {
|
Commands::GenerateConfig => {
|
||||||
let config = config::Config::default();
|
let mut config = config::Config::default();
|
||||||
|
config
|
||||||
|
.charge_controllers
|
||||||
|
.push(config::ChargeControllerConfig {
|
||||||
|
name: String::from("tcp"),
|
||||||
|
transport: config::Transport::Tcp {
|
||||||
|
ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 102)),
|
||||||
|
port: 420,
|
||||||
|
},
|
||||||
|
watch_interval_seconds: 0,
|
||||||
|
variant: config::ChargeControllerVariant::Tristar,
|
||||||
|
follow_primary: false,
|
||||||
|
});
|
||||||
|
config
|
||||||
|
.charge_controllers
|
||||||
|
.push(config::ChargeControllerConfig {
|
||||||
|
name: String::from("serial"),
|
||||||
|
transport: config::Transport::Serial {
|
||||||
|
port: "/dev/someport".to_string(),
|
||||||
|
baud_rate: 69,
|
||||||
|
},
|
||||||
|
watch_interval_seconds: 0,
|
||||||
|
variant: config::ChargeControllerVariant::Tristar,
|
||||||
|
follow_primary: false,
|
||||||
|
});
|
||||||
|
let config = config::ConfigStorage::from_latest(config);
|
||||||
let json = serde_json::to_string_pretty(&config)?;
|
let json = serde_json::to_string_pretty(&config)?;
|
||||||
println!("{json}");
|
println!("{json}");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -66,31 +93,70 @@ async fn run() -> eyre::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn watch(args: Args) -> eyre::Result<()> {
|
async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
let config: config::Config = serde_json::from_reader(std::fs::File::open(args.config)?)?;
|
let _w = config::init_config(&args.config);
|
||||||
|
let (storage, follow_voltage_tx, mut controller_tasks) = {
|
||||||
|
let config = config::access_config().await;
|
||||||
|
if config
|
||||||
|
.charge_controllers
|
||||||
|
.iter()
|
||||||
|
.any(|cc| cc.follow_primary && cc.name == config.primary_charge_controller)
|
||||||
|
{
|
||||||
|
return Err(eyre::eyre!(
|
||||||
|
"primary charge controller is set to follow primary!"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let mut controllers = futures::stream::FuturesUnordered::new();
|
let mut controllers = Vec::new();
|
||||||
let mut map = std::collections::HashMap::new();
|
let mut map = std::collections::HashMap::new();
|
||||||
|
|
||||||
for config in config.charge_controllers {
|
let (voltage_tx, voltage_rx) =
|
||||||
|
tokio::sync::watch::channel(controller::VoltageCommand::None);
|
||||||
|
|
||||||
|
for config in &config.charge_controllers {
|
||||||
let n = config.name.clone();
|
let n = config.name.clone();
|
||||||
match controller::Controller::new(config) {
|
match controller::Controller::new(config.clone(), voltage_rx.clone()).await {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
map.insert(n, v.get_data_ptr());
|
map.insert(n, v.get_data_ptr());
|
||||||
controllers.push(run_loop(v));
|
controllers.push(v);
|
||||||
}
|
}
|
||||||
Err(e) => log::error!("couldn't connect to {}: {e:?}", n),
|
Err(e) => log::error!("couldn't connect to {}: {e:?}", n),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let server = web::rocket(web::ServerState::new(map));
|
if let Some(primary) = controllers
|
||||||
|
.iter_mut()
|
||||||
|
.find(|c| c.name() == config.primary_charge_controller)
|
||||||
|
{
|
||||||
|
primary.set_tx_to_secondary(voltage_tx.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(config);
|
||||||
|
|
||||||
|
let controller_tasks = futures::stream::FuturesUnordered::new();
|
||||||
|
for controller in controllers {
|
||||||
|
controller_tasks.push(run_loop(controller));
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
storage::AllControllers::new(map),
|
||||||
|
voltage_tx,
|
||||||
|
controller_tasks,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = web::rocket(web::ServerState::new(
|
||||||
|
&config::access_config().await.primary_charge_controller,
|
||||||
|
storage,
|
||||||
|
follow_voltage_tx,
|
||||||
|
));
|
||||||
let server_task = tokio::task::spawn(server.launch());
|
let server_task = tokio::task::spawn(server.launch());
|
||||||
|
log::warn!("...started!");
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
v = controllers.next() => {
|
v = controller_tasks.next() => {
|
||||||
match v {
|
match v {
|
||||||
Some(Err(e)) => {
|
Some(Err(e)) => {
|
||||||
log::error!("{e:?}");
|
log::error!("{e:?}");
|
||||||
std::process::exit(1);
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("no controller tasks left???");
|
log::error!("no controller tasks left???");
|
||||||
|
@ -98,7 +164,12 @@ async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
v = server_task => {
|
v = server_task => {
|
||||||
log::error!("server exited: {v:#?}");
|
if let Err(e)=v {
|
||||||
|
log::error!("server exited: {e:#?}");
|
||||||
|
std::process::exit(1);
|
||||||
|
} else {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,10 +178,26 @@ async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
|
|
||||||
async fn run_loop(mut controller: controller::Controller) -> eyre::Result<()> {
|
async fn run_loop(mut controller: controller::Controller) -> eyre::Result<()> {
|
||||||
let mut timeout = tokio::time::interval(controller.timeout_interval());
|
let mut timeout = tokio::time::interval(controller.timeout_interval());
|
||||||
|
timeout.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
timeout.tick().await;
|
let rx = controller.get_rx();
|
||||||
if let Err(e) = controller.refresh().await {
|
tokio::select! {
|
||||||
log::warn!("error reading controller {}: {e:?}", controller.name());
|
_ = timeout.tick() => {
|
||||||
|
do_refresh(&mut controller).await;
|
||||||
|
}
|
||||||
|
Ok(()) = rx.changed() => {
|
||||||
|
let command = *rx.borrow();
|
||||||
|
if let Err(e) = controller.process_command(command).await {
|
||||||
|
log::error!("controller {} failed to process command: {e}", controller.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn do_refresh(controller: &mut controller::Controller) {
|
||||||
|
if let Err(e) = controller.refresh().await {
|
||||||
|
log::warn!("error reading controller {}: {e:?}", controller.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,283 +0,0 @@
|
||||||
use chrono::Timelike;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio_serial::{SerialPort, SerialPortBuilderExt};
|
|
||||||
|
|
||||||
use crate::gauges::{
|
|
||||||
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, INPUT_CURRENT, PL_DUTY_CYCLE, PL_LOAD_CURRENT,
|
|
||||||
TARGET_VOLTAGE,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Pli {
|
|
||||||
friendly_name: String,
|
|
||||||
port: tokio_serial::SerialStream,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
|
||||||
pub struct PlState {
|
|
||||||
pub battery_voltage: f64,
|
|
||||||
pub target_voltage: f64,
|
|
||||||
pub duty_cycle: f64,
|
|
||||||
pub internal_charge_current: f64,
|
|
||||||
pub internal_load_current: f64,
|
|
||||||
pub battery_temp: f64,
|
|
||||||
pub regulator_state: RegulatorState,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub enum RegulatorState {
|
|
||||||
Boost,
|
|
||||||
Equalise,
|
|
||||||
Absorption,
|
|
||||||
Float,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u8> for RegulatorState {
|
|
||||||
fn from(value: u8) -> Self {
|
|
||||||
match value & 0b11 {
|
|
||||||
0b00 => Self::Boost,
|
|
||||||
0b01 => Self::Equalise,
|
|
||||||
0b10 => Self::Absorption,
|
|
||||||
0b11 => Self::Float,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RegulatorState> for u8 {
|
|
||||||
fn from(value: RegulatorState) -> Self {
|
|
||||||
match value {
|
|
||||||
RegulatorState::Boost => 0b00,
|
|
||||||
RegulatorState::Equalise => 0b01,
|
|
||||||
RegulatorState::Absorption => 0b10,
|
|
||||||
RegulatorState::Float => 0b11,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RegulatorState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Absorption
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_regulator_gauges(state: RegulatorState, label: &str) {
|
|
||||||
let boost = CHARGE_STATE.with_label_values(&[label, "boost"]);
|
|
||||||
let equalise = CHARGE_STATE.with_label_values(&[label, "equalise"]);
|
|
||||||
let absorption = CHARGE_STATE.with_label_values(&[label, "absorption"]);
|
|
||||||
let float = CHARGE_STATE.with_label_values(&[label, "float"]);
|
|
||||||
|
|
||||||
match state {
|
|
||||||
RegulatorState::Boost => {
|
|
||||||
boost.set(1);
|
|
||||||
equalise.set(0);
|
|
||||||
absorption.set(0);
|
|
||||||
float.set(0);
|
|
||||||
}
|
|
||||||
RegulatorState::Equalise => {
|
|
||||||
boost.set(0);
|
|
||||||
equalise.set(1);
|
|
||||||
absorption.set(0);
|
|
||||||
float.set(0);
|
|
||||||
}
|
|
||||||
RegulatorState::Absorption => {
|
|
||||||
boost.set(0);
|
|
||||||
equalise.set(0);
|
|
||||||
absorption.set(1);
|
|
||||||
float.set(0);
|
|
||||||
}
|
|
||||||
RegulatorState::Float => {
|
|
||||||
boost.set(0);
|
|
||||||
equalise.set(0);
|
|
||||||
absorption.set(0);
|
|
||||||
float.set(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[expect(dead_code, reason = "writing settings is not yet implemented")]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum PliRequest {
|
|
||||||
ReadRam(u8),
|
|
||||||
ReadEeprom(u8),
|
|
||||||
SyncTime,
|
|
||||||
SetRegulatorState(RegulatorState),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Pli {
|
|
||||||
pub fn new(
|
|
||||||
serial_port: &str,
|
|
||||||
friendly_name: &str,
|
|
||||||
baud_rate: u32,
|
|
||||||
timeout: u64,
|
|
||||||
) -> Result<Self, tokio_serial::Error> {
|
|
||||||
let port = tokio_serial::new(serial_port, baud_rate)
|
|
||||||
.timeout(std::time::Duration::from_millis(timeout))
|
|
||||||
.open_native_async()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
friendly_name: friendly_name.to_owned(),
|
|
||||||
port,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh(&mut self) -> eyre::Result<crate::controller::CommonData> {
|
|
||||||
let new_state = self.read_state().await?;
|
|
||||||
BATTERY_VOLTAGE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.battery_voltage);
|
|
||||||
TARGET_VOLTAGE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.target_voltage);
|
|
||||||
PL_DUTY_CYCLE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.duty_cycle);
|
|
||||||
INPUT_CURRENT
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.internal_charge_current);
|
|
||||||
PL_LOAD_CURRENT
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.internal_load_current);
|
|
||||||
BATTERY_TEMP
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.battery_temp);
|
|
||||||
|
|
||||||
set_regulator_gauges(new_state.regulator_state, &self.friendly_name);
|
|
||||||
|
|
||||||
Ok(crate::controller::CommonData {
|
|
||||||
battery_voltage: new_state.battery_voltage,
|
|
||||||
target_voltage: new_state.target_voltage,
|
|
||||||
battery_temp: new_state.battery_temp,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[expect(dead_code, reason = "writing settings is not yet implemented")]
|
|
||||||
pub async fn process_request(&mut self, message: PliRequest) -> eyre::Result<()> {
|
|
||||||
match message {
|
|
||||||
PliRequest::ReadRam(address) => {
|
|
||||||
let data = self.read_ram(address).await?;
|
|
||||||
log::warn!("Read RAM at {address}: data {data}");
|
|
||||||
}
|
|
||||||
PliRequest::ReadEeprom(address) => {
|
|
||||||
let data = self.read_eeprom(address).await?;
|
|
||||||
log::warn!("Read EEPROM at {address}: data {data}");
|
|
||||||
}
|
|
||||||
PliRequest::SyncTime => {
|
|
||||||
let now = chrono::Local::now();
|
|
||||||
let timestamp = (((now.hour() * 10) + (now.minute() / 6)) & 0xFF) as u8;
|
|
||||||
let min = (now.minute() % 6) as u8;
|
|
||||||
let sec = (now.second() / 2).min(29) as u8;
|
|
||||||
self.write_ram(PlRamAddress::Hour, timestamp).await?;
|
|
||||||
self.write_ram(PlRamAddress::Min, min).await?;
|
|
||||||
self.write_ram(PlRamAddress::Sec, sec).await?;
|
|
||||||
log::warn!(
|
|
||||||
"Set time: {now} corresponds to {timestamp} + minutes {min} + seconds {sec}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
PliRequest::SetRegulatorState(state) => {
|
|
||||||
log::warn!("Setting regulator state to {state:?}");
|
|
||||||
self.write_ram(PlRamAddress::Rstate, state.into()).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_state(&mut self) -> eyre::Result<PlState> {
|
|
||||||
Ok(PlState {
|
|
||||||
battery_voltage: f64::from(self.read_ram(PlRamAddress::Batv).await?) * (4. / 10.),
|
|
||||||
target_voltage: f64::from(self.read_ram(PlRamAddress::Vreg).await?) * (4. / 10.),
|
|
||||||
duty_cycle: f64::from(self.read_ram(PlRamAddress::Dutycyc).await?) / 255.,
|
|
||||||
internal_charge_current: f64::from(self.read_ram(PlRamAddress::Cint).await?)
|
|
||||||
* (4. / 10.),
|
|
||||||
internal_load_current: f64::from(self.read_ram(PlRamAddress::Lint).await?) * (4. / 10.),
|
|
||||||
battery_temp: f64::from(self.read_ram(PlRamAddress::Battemp).await?),
|
|
||||||
regulator_state: self.read_ram(PlRamAddress::Rstate).await?.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_command(&mut self, req: [u8; 4]) -> eyre::Result<()> {
|
|
||||||
self.flush()?;
|
|
||||||
self.port.write_all(&req).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> eyre::Result<()> {
|
|
||||||
self.port.clear(tokio_serial::ClearBuffer::All)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive<const LENGTH: usize>(&mut self) -> eyre::Result<[u8; LENGTH]> {
|
|
||||||
let mut buf = [0; LENGTH];
|
|
||||||
self.port.read_exact(&mut buf).await?;
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_ram<T>(&mut self, address: T) -> eyre::Result<u8>
|
|
||||||
where
|
|
||||||
T: Into<u8>,
|
|
||||||
{
|
|
||||||
self.send_command(command(20, address.into(), 0)).await?;
|
|
||||||
let buf = self.receive::<1>().await?;
|
|
||||||
if buf[0] == 200 {
|
|
||||||
Ok(self.receive::<1>().await?[0])
|
|
||||||
} else {
|
|
||||||
Err(eyre::eyre!("read error: result is {}", buf[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_ram<T>(&mut self, address: T, data: u8) -> eyre::Result<()>
|
|
||||||
where
|
|
||||||
T: Into<u8>,
|
|
||||||
{
|
|
||||||
self.send_command(command(152, address.into(), data))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_eeprom<T>(&mut self, address: T) -> eyre::Result<u8>
|
|
||||||
where
|
|
||||||
T: Into<u8>,
|
|
||||||
{
|
|
||||||
self.send_command(command(72, address.into(), 0)).await?;
|
|
||||||
let buf = self.receive::<1>().await?;
|
|
||||||
if buf[0] == 200 {
|
|
||||||
Ok(self.receive::<1>().await?[0])
|
|
||||||
} else {
|
|
||||||
Err(eyre::eyre!("read error: result is {}", buf[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PlRamAddress {
|
|
||||||
Dutycyc,
|
|
||||||
Sec,
|
|
||||||
Min,
|
|
||||||
Hour,
|
|
||||||
Batv,
|
|
||||||
Battemp,
|
|
||||||
Rstate,
|
|
||||||
Vreg,
|
|
||||||
Cint,
|
|
||||||
Lint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PlRamAddress> for u8 {
|
|
||||||
fn from(value: PlRamAddress) -> Self {
|
|
||||||
match value {
|
|
||||||
PlRamAddress::Dutycyc => 39,
|
|
||||||
PlRamAddress::Sec => 46,
|
|
||||||
PlRamAddress::Min => 47,
|
|
||||||
PlRamAddress::Hour => 48,
|
|
||||||
PlRamAddress::Batv => 50,
|
|
||||||
PlRamAddress::Battemp => 52,
|
|
||||||
PlRamAddress::Rstate => 101,
|
|
||||||
PlRamAddress::Vreg => 105,
|
|
||||||
PlRamAddress::Cint => 213,
|
|
||||||
PlRamAddress::Lint => 217,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command(operation: u8, address: u8, data: u8) -> [u8; 4] {
|
|
||||||
[operation, address, data, !operation]
|
|
||||||
}
|
|
61
charge-controller-supervisor/src/storage.rs
Normal file
61
charge-controller-supervisor/src/storage.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
pub struct AllControllers {
|
||||||
|
map: std::collections::HashMap<String, std::sync::Arc<ControllerData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AllControllers {
|
||||||
|
pub const fn new(
|
||||||
|
map: std::collections::HashMap<String, std::sync::Arc<ControllerData>>,
|
||||||
|
) -> Self {
|
||||||
|
Self { map }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn controller_names(&self) -> impl Iterator<Item = &String> {
|
||||||
|
self.map.keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, name: &str) -> Option<&std::sync::Arc<ControllerData>> {
|
||||||
|
self.map.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all_data(&self) -> impl Iterator<Item = (&String, &std::sync::Arc<ControllerData>)> {
|
||||||
|
self.map.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ControllerData {
|
||||||
|
state: tokio::sync::RwLock<Option<crate::controller::ControllerState>>,
|
||||||
|
settings: tokio::sync::RwLock<Option<crate::controller::ControllerSettings>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControllerData {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: tokio::sync::RwLock::const_new(None),
|
||||||
|
settings: tokio::sync::RwLock::const_new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_state(
|
||||||
|
&self,
|
||||||
|
) -> tokio::sync::RwLockWriteGuard<Option<crate::controller::ControllerState>> {
|
||||||
|
self.state.write().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_state(
|
||||||
|
&self,
|
||||||
|
) -> tokio::sync::RwLockReadGuard<Option<crate::controller::ControllerState>> {
|
||||||
|
self.state.read().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_settings(
|
||||||
|
&self,
|
||||||
|
) -> tokio::sync::RwLockWriteGuard<Option<crate::controller::ControllerSettings>> {
|
||||||
|
self.settings.write().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_settings(
|
||||||
|
&self,
|
||||||
|
) -> tokio::sync::RwLockReadGuard<Option<crate::controller::ControllerSettings>> {
|
||||||
|
self.settings.read().await
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,326 +0,0 @@
|
||||||
use prometheus::core::{AtomicI64, GenericGauge};
|
|
||||||
use tokio_modbus::client::Reader;
|
|
||||||
|
|
||||||
use crate::gauges::{
|
|
||||||
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, INPUT_CURRENT, TARGET_VOLTAGE,
|
|
||||||
TRISTAR_CHARGE_CURRENT, TRISTAR_INPUT_VOLTAGE, TRISTAR_MAX_ARRAY_POWER,
|
|
||||||
TRISTAR_MAX_ARRAY_VOLTAGE, TRISTAR_OPEN_CIRCUIT_VOLTAGE, TRISTAR_POWER_IN, TRISTAR_POWER_OUT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEVICE_ID: u8 = 0x01;
|
|
||||||
const RAM_DATA_SIZE: u16 = 0x005B;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Scaling {
|
|
||||||
pub v_scale: f64,
|
|
||||||
pub i_scale: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scaling {
|
|
||||||
fn from(data: &[u16]) -> Self {
|
|
||||||
Self::from_internal(data[0], data[1], data[2], data[3])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_internal(v_pu_hi: u16, v_pu_lo: u16, i_pu_hi: u16, i_pu_lo: u16) -> Self {
|
|
||||||
Self {
|
|
||||||
v_scale: f64::from(v_pu_hi) + (f64::from(v_pu_lo) / f64::powf(2., 16.)),
|
|
||||||
i_scale: f64::from(i_pu_hi) + (f64::from(i_pu_lo) / f64::powf(2., 16.)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_voltage(&self, data: u16) -> f64 {
|
|
||||||
f64::from(data) * self.v_scale * f64::powf(2., -15.)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current(&self, data: u16) -> f64 {
|
|
||||||
f64::from(data) * self.i_scale * f64::powf(2., -15.)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_power(&self, data: u16) -> f64 {
|
|
||||||
f64::from(data) * self.v_scale * self.i_scale * f64::powf(2., -17.)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Tristar {
|
|
||||||
friendly_name: String,
|
|
||||||
modbus: tokio_modbus::client::Context,
|
|
||||||
charge_state_gauges: ChargeStateGauges,
|
|
||||||
consecutive_errors: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy)]
|
|
||||||
pub struct TristarState {
|
|
||||||
battery_voltage: f64,
|
|
||||||
target_voltage: f64,
|
|
||||||
input_current: f64,
|
|
||||||
battery_temp: u16,
|
|
||||||
charge_state: ChargeState,
|
|
||||||
tristar_input_voltage: f64,
|
|
||||||
tristar_charge_current: f64,
|
|
||||||
tristar_power_out: f64,
|
|
||||||
tristar_power_in: f64,
|
|
||||||
tristar_max_array_power: f64,
|
|
||||||
tristar_max_array_voltage: f64,
|
|
||||||
tristar_open_circuit_voltage: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TristarState {
|
|
||||||
fn from_ram(ram: &[u16]) -> Self {
|
|
||||||
let scaling = Scaling::from(ram);
|
|
||||||
Self {
|
|
||||||
battery_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVbFMed]),
|
|
||||||
target_voltage: scaling.get_voltage(ram[TristarRamAddress::VbRef]),
|
|
||||||
input_current: scaling.get_current(ram[TristarRamAddress::AdcIaFShadow]),
|
|
||||||
battery_temp: ram[TristarRamAddress::Tbatt],
|
|
||||||
charge_state: ChargeState::from(ram[TristarRamAddress::ChargeState]),
|
|
||||||
tristar_input_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVaF]),
|
|
||||||
tristar_charge_current: scaling.get_current(ram[TristarRamAddress::AdcIbFShadow]),
|
|
||||||
tristar_power_out: scaling.get_power(ram[TristarRamAddress::PowerOutShadow]),
|
|
||||||
tristar_power_in: scaling.get_power(ram[TristarRamAddress::PowerInShadow]),
|
|
||||||
tristar_max_array_power: scaling.get_power(ram[TristarRamAddress::SweepPinMax]),
|
|
||||||
tristar_max_array_voltage: scaling.get_voltage(ram[TristarRamAddress::SweepVmp]),
|
|
||||||
tristar_open_circuit_voltage: scaling.get_voltage(ram[TristarRamAddress::SweepVoc]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum ChargeState {
|
|
||||||
Start,
|
|
||||||
NightCheck,
|
|
||||||
Disconnect,
|
|
||||||
Night,
|
|
||||||
Fault,
|
|
||||||
Mppt,
|
|
||||||
Absorption,
|
|
||||||
Float,
|
|
||||||
Equalize,
|
|
||||||
Slave,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ChargeState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u16> for ChargeState {
|
|
||||||
fn from(value: u16) -> Self {
|
|
||||||
match value {
|
|
||||||
0 => Self::Start,
|
|
||||||
1 => Self::NightCheck,
|
|
||||||
2 => Self::Disconnect,
|
|
||||||
3 => Self::Night,
|
|
||||||
4 => Self::Fault,
|
|
||||||
5 => Self::Mppt,
|
|
||||||
6 => Self::Absorption,
|
|
||||||
7 => Self::Float,
|
|
||||||
8 => Self::Equalize,
|
|
||||||
9 => Self::Slave,
|
|
||||||
_ => {
|
|
||||||
log::warn!("Unknown chargestate value: {value}");
|
|
||||||
Self::Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChargeStateGauges {
|
|
||||||
start: GenericGauge<AtomicI64>,
|
|
||||||
night_check: GenericGauge<AtomicI64>,
|
|
||||||
disconnect: GenericGauge<AtomicI64>,
|
|
||||||
night: GenericGauge<AtomicI64>,
|
|
||||||
fault: GenericGauge<AtomicI64>,
|
|
||||||
mppt: GenericGauge<AtomicI64>,
|
|
||||||
absorption: GenericGauge<AtomicI64>,
|
|
||||||
float: GenericGauge<AtomicI64>,
|
|
||||||
equalize: GenericGauge<AtomicI64>,
|
|
||||||
slave: GenericGauge<AtomicI64>,
|
|
||||||
unknown: GenericGauge<AtomicI64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChargeStateGauges {
|
|
||||||
fn new(label: &str) -> Self {
|
|
||||||
let start = CHARGE_STATE.with_label_values(&[label, "start"]);
|
|
||||||
let night_check = CHARGE_STATE.with_label_values(&[label, "night_check"]);
|
|
||||||
let disconnect = CHARGE_STATE.with_label_values(&[label, "disconnect"]);
|
|
||||||
let night = CHARGE_STATE.with_label_values(&[label, "night"]);
|
|
||||||
let fault = CHARGE_STATE.with_label_values(&[label, "fault"]);
|
|
||||||
let mppt = CHARGE_STATE.with_label_values(&[label, "mppt"]);
|
|
||||||
let absorption = CHARGE_STATE.with_label_values(&[label, "absorption"]);
|
|
||||||
let float = CHARGE_STATE.with_label_values(&[label, "float"]);
|
|
||||||
let equalize = CHARGE_STATE.with_label_values(&[label, "equalize"]);
|
|
||||||
let slave = CHARGE_STATE.with_label_values(&[label, "slave"]);
|
|
||||||
let unknown = CHARGE_STATE.with_label_values(&[label, "unknown"]);
|
|
||||||
Self {
|
|
||||||
start,
|
|
||||||
night_check,
|
|
||||||
disconnect,
|
|
||||||
night,
|
|
||||||
fault,
|
|
||||||
mppt,
|
|
||||||
absorption,
|
|
||||||
float,
|
|
||||||
equalize,
|
|
||||||
slave,
|
|
||||||
unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn zero_all(&mut self) {
|
|
||||||
self.start.set(0);
|
|
||||||
self.night_check.set(0);
|
|
||||||
self.disconnect.set(0);
|
|
||||||
self.night.set(0);
|
|
||||||
self.fault.set(0);
|
|
||||||
self.mppt.set(0);
|
|
||||||
self.absorption.set(0);
|
|
||||||
self.float.set(0);
|
|
||||||
self.equalize.set(0);
|
|
||||||
self.slave.set(0);
|
|
||||||
self.unknown.set(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set(&mut self, state: ChargeState) {
|
|
||||||
match state {
|
|
||||||
ChargeState::Start => {
|
|
||||||
self.zero_all();
|
|
||||||
self.start.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::NightCheck => {
|
|
||||||
self.zero_all();
|
|
||||||
self.night_check.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Disconnect => {
|
|
||||||
self.zero_all();
|
|
||||||
self.disconnect.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Night => {
|
|
||||||
self.zero_all();
|
|
||||||
self.night.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Fault => {
|
|
||||||
self.zero_all();
|
|
||||||
self.fault.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Mppt => {
|
|
||||||
self.zero_all();
|
|
||||||
self.mppt.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Absorption => {
|
|
||||||
self.zero_all();
|
|
||||||
self.absorption.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Float => {
|
|
||||||
self.zero_all();
|
|
||||||
self.float.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Equalize => {
|
|
||||||
self.zero_all();
|
|
||||||
self.equalize.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Slave => {
|
|
||||||
self.zero_all();
|
|
||||||
self.slave.set(1);
|
|
||||||
}
|
|
||||||
ChargeState::Unknown => {
|
|
||||||
self.zero_all();
|
|
||||||
self.unknown.set(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tristar {
|
|
||||||
pub fn new(serial_port: &str, friendly_name: &str, baud_rate: u32) -> eyre::Result<Self> {
|
|
||||||
let modbus_serial =
|
|
||||||
tokio_serial::SerialStream::open(&tokio_serial::new(serial_port, baud_rate))?;
|
|
||||||
let slave = tokio_modbus::Slave(DEVICE_ID);
|
|
||||||
let modbus = tokio_modbus::client::rtu::attach_slave(modbus_serial, slave);
|
|
||||||
let charge_state_gauges = ChargeStateGauges::new(friendly_name);
|
|
||||||
Ok(Self {
|
|
||||||
friendly_name: friendly_name.to_owned(),
|
|
||||||
modbus,
|
|
||||||
charge_state_gauges,
|
|
||||||
consecutive_errors: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh(&mut self) -> eyre::Result<crate::controller::CommonData> {
|
|
||||||
let new_state = self.get_data().await?;
|
|
||||||
self.consecutive_errors = 0;
|
|
||||||
BATTERY_VOLTAGE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.battery_voltage);
|
|
||||||
TARGET_VOLTAGE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.target_voltage);
|
|
||||||
INPUT_CURRENT
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.input_current);
|
|
||||||
BATTERY_TEMP
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.battery_temp.into());
|
|
||||||
TRISTAR_INPUT_VOLTAGE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.tristar_input_voltage);
|
|
||||||
TRISTAR_CHARGE_CURRENT
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.tristar_charge_current);
|
|
||||||
TRISTAR_POWER_OUT
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.tristar_power_out);
|
|
||||||
TRISTAR_POWER_IN
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.tristar_power_in);
|
|
||||||
TRISTAR_MAX_ARRAY_POWER
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.tristar_max_array_power);
|
|
||||||
TRISTAR_MAX_ARRAY_VOLTAGE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.tristar_max_array_voltage);
|
|
||||||
TRISTAR_OPEN_CIRCUIT_VOLTAGE
|
|
||||||
.with_label_values(&[&self.friendly_name])
|
|
||||||
.set(new_state.tristar_open_circuit_voltage);
|
|
||||||
|
|
||||||
self.charge_state_gauges.set(new_state.charge_state);
|
|
||||||
|
|
||||||
Ok(crate::controller::CommonData {
|
|
||||||
battery_voltage: new_state.battery_voltage,
|
|
||||||
target_voltage: new_state.target_voltage,
|
|
||||||
battery_temp: f64::from(new_state.battery_temp),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_data(&mut self) -> eyre::Result<TristarState> {
|
|
||||||
let data = self
|
|
||||||
.modbus
|
|
||||||
.read_holding_registers(0x0000, RAM_DATA_SIZE + 1)
|
|
||||||
.await??;
|
|
||||||
Ok(TristarState::from_ram(&data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TristarRamAddress {
|
|
||||||
AdcVbFMed = 0x0018,
|
|
||||||
AdcVaF = 0x001B,
|
|
||||||
AdcIbFShadow = 0x001C,
|
|
||||||
AdcIaFShadow = 0x001D,
|
|
||||||
Tbatt = 0x0025,
|
|
||||||
ChargeState = 0x0032,
|
|
||||||
VbRef = 0x0033,
|
|
||||||
PowerOutShadow = 0x003A,
|
|
||||||
PowerInShadow = 0x003B,
|
|
||||||
SweepPinMax = 0x003C,
|
|
||||||
SweepVmp = 0x003D,
|
|
||||||
SweepVoc = 0x003E,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Index<TristarRamAddress> for [u16] {
|
|
||||||
type Output = u16;
|
|
||||||
|
|
||||||
fn index(&self, index: TristarRamAddress) -> &Self::Output {
|
|
||||||
&self[index as usize]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +1,83 @@
|
||||||
use rocket::{get, routes, serde::json::Json, State};
|
use rocket::{get, post, routes, serde::json::Json, State};
|
||||||
|
|
||||||
|
use crate::storage::AllControllers;
|
||||||
|
|
||||||
|
mod static_handler;
|
||||||
|
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
map: std::collections::HashMap<
|
primary_name: String,
|
||||||
String,
|
data: AllControllers,
|
||||||
std::sync::Arc<tokio::sync::RwLock<crate::controller::CommonData>>,
|
tx_to_controllers: tokio::sync::watch::Sender<crate::controller::VoltageCommand>,
|
||||||
>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
map: std::collections::HashMap<
|
primary_name: &impl ToString,
|
||||||
String,
|
data: AllControllers,
|
||||||
std::sync::Arc<tokio::sync::RwLock<crate::controller::CommonData>>,
|
tx_to_controllers: tokio::sync::watch::Sender<crate::controller::VoltageCommand>,
|
||||||
>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { map }
|
let primary_name = primary_name.to_string();
|
||||||
|
Self {
|
||||||
|
primary_name,
|
||||||
|
data,
|
||||||
|
tx_to_controllers,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
pub fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
||||||
|
// serve the html from disk if running in a debug build
|
||||||
|
// this allows editing the webpage without having to rebuild the executable
|
||||||
|
// but in release builds, bundle the entire webapp folder into the exe
|
||||||
|
let fileserver: Vec<rocket::Route> = if cfg!(debug_assertions) {
|
||||||
|
rocket::fs::FileServer::from(format!(
|
||||||
|
"{}/webapp",
|
||||||
|
std::env::var("CARGO_MANIFEST_DIR").unwrap()
|
||||||
|
))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
static_handler::UiStatic {}.into()
|
||||||
|
};
|
||||||
rocket::build()
|
rocket::build()
|
||||||
|
.attach(Cors)
|
||||||
.manage(state)
|
.manage(state)
|
||||||
.mount("/", routes![metrics, interfaces, all_interfaces, interface])
|
.mount("/", fileserver)
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
routes![
|
||||||
|
metrics,
|
||||||
|
interfaces,
|
||||||
|
all_interfaces,
|
||||||
|
all_interfaces_full,
|
||||||
|
all_interfaces_settings,
|
||||||
|
primary_interface,
|
||||||
|
interface,
|
||||||
|
interface_full,
|
||||||
|
interface_settings,
|
||||||
|
get_control,
|
||||||
|
enable_control,
|
||||||
|
disable_control
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/interfaces")]
|
#[get("/interfaces")]
|
||||||
fn interfaces(state: &State<ServerState>) -> Json<Vec<String>> {
|
fn interfaces(state: &State<ServerState>) -> Json<Vec<String>> {
|
||||||
Json(state.map.keys().cloned().collect())
|
Json(state.data.controller_names().cloned().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/interfaces/primary")]
|
||||||
|
async fn primary_interface(
|
||||||
|
state: &State<ServerState>,
|
||||||
|
) -> Result<Json<crate::controller::CommonData>, ServerError> {
|
||||||
|
let s = state
|
||||||
|
.data
|
||||||
|
.get(&state.primary_name)
|
||||||
|
.ok_or(ServerError::InvalidPrimaryName)?
|
||||||
|
.read_state()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(Json(s.as_ref().ok_or(ServerError::NoData)?.common()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/interfaces/data")]
|
#[get("/interfaces/data")]
|
||||||
|
@ -35,8 +86,43 @@ async fn all_interfaces(
|
||||||
) -> Json<Vec<(String, crate::controller::CommonData)>> {
|
) -> Json<Vec<(String, crate::controller::CommonData)>> {
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
|
|
||||||
for (k, v) in &state.map {
|
for (k, v) in state.data.all_data() {
|
||||||
data.push((k.clone(), v.read().await.clone()));
|
let v = v.read_state().await;
|
||||||
|
if let Some(v) = v.as_ref() {
|
||||||
|
data.push((k.clone(), v.common().clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/interfaces/data/full")]
|
||||||
|
async fn all_interfaces_full(
|
||||||
|
state: &State<ServerState>,
|
||||||
|
) -> Json<Vec<(String, crate::controller::ControllerState)>> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
|
||||||
|
for (k, v) in state.data.all_data() {
|
||||||
|
let v = v.read_state().await;
|
||||||
|
if let Some(v) = v.as_ref() {
|
||||||
|
data.push((k.clone(), v.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/interfaces/settings")]
|
||||||
|
async fn all_interfaces_settings(
|
||||||
|
state: &State<ServerState>,
|
||||||
|
) -> Json<Vec<(String, crate::controller::ControllerSettings)>> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
|
||||||
|
for (k, v) in state.data.all_data() {
|
||||||
|
let v = v.read_settings().await;
|
||||||
|
if let Some(v) = v.as_ref() {
|
||||||
|
data.push((k.clone(), v.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Json(data)
|
Json(data)
|
||||||
|
@ -48,13 +134,40 @@ async fn interface(
|
||||||
state: &State<ServerState>,
|
state: &State<ServerState>,
|
||||||
) -> Result<Json<crate::controller::CommonData>, ServerError> {
|
) -> Result<Json<crate::controller::CommonData>, ServerError> {
|
||||||
let data = state
|
let data = state
|
||||||
.map
|
.data
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or(ServerError::NotFound)?
|
.ok_or(ServerError::NotFound)?
|
||||||
.read()
|
.read_state()
|
||||||
.await
|
.await;
|
||||||
.clone();
|
Ok(Json(data.as_ref().ok_or(ServerError::NoData)?.common()))
|
||||||
Ok(Json(data))
|
}
|
||||||
|
|
||||||
|
#[get("/interface/<name>/full")]
|
||||||
|
async fn interface_full(
|
||||||
|
name: &str,
|
||||||
|
state: &State<ServerState>,
|
||||||
|
) -> Result<Json<crate::controller::ControllerState>, ServerError> {
|
||||||
|
let data = state
|
||||||
|
.data
|
||||||
|
.get(name)
|
||||||
|
.ok_or(ServerError::NotFound)?
|
||||||
|
.read_state()
|
||||||
|
.await;
|
||||||
|
Ok(Json(data.as_ref().ok_or(ServerError::NoData)?.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/interface/<name>/settings")]
|
||||||
|
async fn interface_settings(
|
||||||
|
name: &str,
|
||||||
|
state: &State<ServerState>,
|
||||||
|
) -> Result<Json<crate::controller::ControllerSettings>, ServerError> {
|
||||||
|
let data = state
|
||||||
|
.data
|
||||||
|
.get(name)
|
||||||
|
.ok_or(ServerError::NotFound)?
|
||||||
|
.read_settings()
|
||||||
|
.await;
|
||||||
|
Ok(Json(data.as_ref().ok_or(ServerError::NoData)?.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/metrics")]
|
#[get("/metrics")]
|
||||||
|
@ -65,9 +178,45 @@ fn metrics() -> Result<String, ServerError> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ControlState {
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/control")]
|
||||||
|
async fn get_control() -> Json<ControlState> {
|
||||||
|
let enabled = crate::config::access_config()
|
||||||
|
.await
|
||||||
|
.enable_secondary_control;
|
||||||
|
Json(ControlState { enabled })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/control/enable")]
|
||||||
|
async fn enable_control() {
|
||||||
|
log::warn!("enabling control");
|
||||||
|
crate::config::write_to_config()
|
||||||
|
.await
|
||||||
|
.enable_secondary_control = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/control/disable")]
|
||||||
|
async fn disable_control(state: &State<ServerState>) -> Result<(), ServerError> {
|
||||||
|
log::warn!("disabling control");
|
||||||
|
crate::config::write_to_config()
|
||||||
|
.await
|
||||||
|
.enable_secondary_control = false;
|
||||||
|
state
|
||||||
|
.tx_to_controllers
|
||||||
|
.send(crate::controller::VoltageCommand::None)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
enum ServerError {
|
enum ServerError {
|
||||||
Prometheus,
|
Prometheus,
|
||||||
NotFound,
|
NotFound,
|
||||||
|
InvalidPrimaryName,
|
||||||
|
NoData,
|
||||||
|
ControllerTx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<prometheus::Error> for ServerError {
|
impl From<prometheus::Error> for ServerError {
|
||||||
|
@ -76,11 +225,55 @@ impl From<prometheus::Error> for ServerError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> From<tokio::sync::watch::error::SendError<T>> for ServerError {
|
||||||
|
fn from(_: tokio::sync::watch::error::SendError<T>) -> Self {
|
||||||
|
Self::ControllerTx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> rocket::response::Responder<'a, 'a> for ServerError {
|
impl<'a> rocket::response::Responder<'a, 'a> for ServerError {
|
||||||
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
|
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
|
||||||
Err(match self {
|
Err(match self {
|
||||||
Self::NotFound => rocket::http::Status::NotFound,
|
Self::NotFound => rocket::http::Status::NotFound,
|
||||||
ServerError::Prometheus => rocket::http::Status::InternalServerError,
|
Self::InvalidPrimaryName => rocket::http::Status::ServiceUnavailable,
|
||||||
|
Self::ControllerTx | Self::NoData | Self::Prometheus => {
|
||||||
|
rocket::http::Status::InternalServerError
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Cors;
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl rocket::fairing::Fairing for Cors {
|
||||||
|
fn info(&self) -> rocket::fairing::Info {
|
||||||
|
rocket::fairing::Info {
|
||||||
|
name: "Add CORS headers to responses",
|
||||||
|
kind: rocket::fairing::Kind::Response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_response<'r>(
|
||||||
|
&self,
|
||||||
|
_request: &'r rocket::Request<'_>,
|
||||||
|
response: &mut rocket::Response<'r>,
|
||||||
|
) {
|
||||||
|
response.set_header(rocket::http::Header::new(
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"*",
|
||||||
|
));
|
||||||
|
response.set_header(rocket::http::Header::new(
|
||||||
|
"Access-Control-Allow-Methods",
|
||||||
|
"POST, GET, PATCH, OPTIONS",
|
||||||
|
));
|
||||||
|
response.set_header(rocket::http::Header::new(
|
||||||
|
"Access-Control-Allow-Headers",
|
||||||
|
"*",
|
||||||
|
));
|
||||||
|
response.set_header(rocket::http::Header::new(
|
||||||
|
"Access-Control-Allow-Credentials",
|
||||||
|
"true",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
83
charge-controller-supervisor/src/web/static_handler.rs
Normal file
83
charge-controller-supervisor/src/web/static_handler.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use include_dir::{include_dir, Dir};
|
||||||
|
use rocket::{
|
||||||
|
http::{ContentType, Status},
|
||||||
|
outcome::IntoOutcome,
|
||||||
|
response::Responder,
|
||||||
|
route::{Handler, Outcome},
|
||||||
|
Data, Request,
|
||||||
|
};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
static UI_DIR_FILES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/webapp");
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(super) struct UiStatic {}
|
||||||
|
|
||||||
|
impl From<UiStatic> for Vec<rocket::Route> {
|
||||||
|
fn from(server: UiStatic) -> Self {
|
||||||
|
vec![rocket::Route::ranked(
|
||||||
|
None,
|
||||||
|
rocket::http::Method::Get,
|
||||||
|
"/<path..>",
|
||||||
|
server,
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl Handler for UiStatic {
|
||||||
|
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
|
||||||
|
use rocket::http::uri::fmt::Path;
|
||||||
|
|
||||||
|
let path = req
|
||||||
|
.segments::<rocket::http::uri::Segments<'_, Path>>(0..)
|
||||||
|
.ok()
|
||||||
|
.and_then(|segments| segments.to_path_buf(true).ok());
|
||||||
|
|
||||||
|
match path {
|
||||||
|
Some(p) => {
|
||||||
|
if p.as_os_str() == "" {
|
||||||
|
let index = UI_DIR_FILES.get_file("index.html").map(|v| RawHtml {
|
||||||
|
data: v.contents().to_vec(),
|
||||||
|
name: PathBuf::from("index.html"),
|
||||||
|
});
|
||||||
|
|
||||||
|
index.respond_to(req).or_forward((data, Status::NotFound))
|
||||||
|
} else {
|
||||||
|
let plus_index = p.join("index.html");
|
||||||
|
let file = UI_DIR_FILES
|
||||||
|
.get_file(&p)
|
||||||
|
.map(|v| RawHtml {
|
||||||
|
data: v.contents().to_vec(),
|
||||||
|
name: p,
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
UI_DIR_FILES.get_file(&plus_index).map(|v| RawHtml {
|
||||||
|
data: v.contents().to_vec(),
|
||||||
|
name: plus_index,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
file.respond_to(req).or_forward((data, Status::NotFound))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Outcome::forward(data, Status::NotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawHtml {
|
||||||
|
data: Vec<u8>,
|
||||||
|
name: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> Responder<'r, 'static> for RawHtml {
|
||||||
|
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'static> {
|
||||||
|
let mut response = self.data.respond_to(request)?;
|
||||||
|
if let Some(ext) = self.name.extension()
|
||||||
|
&& let Some(ct) = ContentType::from_extension(&ext.to_string_lossy())
|
||||||
|
{
|
||||||
|
response.set_header(ct);
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
26
charge-controller-supervisor/webapp/index.html
Normal file
26
charge-controller-supervisor/webapp/index.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Charge Control Control</title>
|
||||||
|
<link id="favicon" rel="icon"
|
||||||
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤯</text></svg>">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload="init_main()">
|
||||||
|
<div class="container">
|
||||||
|
<h3>Follow primary controller:</h3>
|
||||||
|
<div class="selector" id="control-selector">
|
||||||
|
<input id="control-enabled" type="radio" name="control" onclick="enable_automatic_control()">
|
||||||
|
<label for="control-enabled">enabled</label>
|
||||||
|
<input id="control-disabled" type="radio" name="control" onclick="disable_automatic_control()">
|
||||||
|
<label for="control-disabled">disabled</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
80
charge-controller-supervisor/webapp/script.js
Normal file
80
charge-controller-supervisor/webapp/script.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
const api_url =
|
||||||
|
window.location.protocol +
|
||||||
|
"//" +
|
||||||
|
window.location.hostname +
|
||||||
|
":" +
|
||||||
|
window.location.port;
|
||||||
|
|
||||||
|
const delay = (time) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, time));
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.prototype.disable = function () {
|
||||||
|
var that = this;
|
||||||
|
for (var i = 0, len = that.length; i < len; i++) {
|
||||||
|
that[i].disabled = true;
|
||||||
|
}
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.prototype.enable = function () {
|
||||||
|
var that = this;
|
||||||
|
for (var i = 0, len = that.length; i < len; i++) {
|
||||||
|
that[i].disabled = false;
|
||||||
|
}
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
function init_main() {
|
||||||
|
refresh_buttons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function disable_automatic_control() {
|
||||||
|
if (is_automatic_control) {
|
||||||
|
document.getElementById("control-disabled").checked = true;
|
||||||
|
document.body.classList.add("loading");
|
||||||
|
|
||||||
|
fetch(api_url + "/control/disable", { method: "POST" }).then(
|
||||||
|
async (response) => {
|
||||||
|
let delayres = await delay(100);
|
||||||
|
refresh_buttons();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enable_automatic_control() {
|
||||||
|
if (!is_automatic_control) {
|
||||||
|
document.getElementById("control-enabled").checked = true;
|
||||||
|
document.body.classList.add("loading");
|
||||||
|
fetch(api_url + "/control/enable", { method: "POST" }).then(
|
||||||
|
async (response) => {
|
||||||
|
let delayres = await delay(100);
|
||||||
|
refresh_buttons();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_control_buttons(data) {
|
||||||
|
document.body.classList.remove("loading");
|
||||||
|
console.log("got data");
|
||||||
|
console.log(data);
|
||||||
|
is_automatic_control = data.enabled;
|
||||||
|
if (is_automatic_control) {
|
||||||
|
document.getElementById("control-enabled").checked = true;
|
||||||
|
} else {
|
||||||
|
document.getElementById("control-disabled").checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh_buttons() {
|
||||||
|
console.log("get data");
|
||||||
|
console.log(api_url);
|
||||||
|
fetch(api_url + "/control").then(async (response) => {
|
||||||
|
console.log("got response");
|
||||||
|
json = await response.json();
|
||||||
|
console.log(json);
|
||||||
|
update_control_buttons(json);
|
||||||
|
});
|
||||||
|
}
|
101
charge-controller-supervisor/webapp/style.css
Normal file
101
charge-controller-supervisor/webapp/style.css
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
|
||||||
|
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
background-color: #333;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0.5rem 2rem 2rem 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #faf9fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outlink {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.outlink {
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgb(52, 52, 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.outlink:hover {
|
||||||
|
color: rgb(110, 100, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.loading * {
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled,
|
||||||
|
.disabled * {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: gray;
|
||||||
|
color: #333;
|
||||||
|
width: max-content;
|
||||||
|
border: 0.2rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 0.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.2s 0s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked + label {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked:disabled + label {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:disabled + label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width > 600px) {
|
||||||
|
.container {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #191919;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background-color: #444;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.outlink {
|
||||||
|
text-decoration: none;
|
||||||
|
/* color: rgb(152, 152, 242); */
|
||||||
|
color: rgb(125, 125, 250);
|
||||||
|
/* color: rgb(94, 94, 252); */
|
||||||
|
}
|
||||||
|
|
||||||
|
a.outlink:hover {
|
||||||
|
color: rgb(130, 120, 255);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
targeting pl's target voltage
|
|
||||||
if load current > 0 or duty cycle < ~0.8 then there is power to spare
|
|
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly-2025-01-16"
|
||||||
|
targets = ["aarch64-unknown-linux-musl"]
|
|
@ -1,12 +0,0 @@
|
||||||
cabin_overheat_protection:
|
|
||||||
FanOnly
|
|
||||||
Off
|
|
||||||
On
|
|
||||||
climate_keeper_mode:
|
|
||||||
off
|
|
||||||
hvac_auto_request:
|
|
||||||
Override
|
|
||||||
On
|
|
||||||
conn_charge_cable:
|
|
||||||
<invalid>
|
|
||||||
IEC
|
|
|
@ -12,29 +12,30 @@ systemd-units = { enable = false }
|
||||||
depends = "charge-controller-supervisor"
|
depends = "charge-controller-supervisor"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = "0.4.39"
|
||||||
clap = { version = "4.5.23", features = ["derive"] }
|
clap = { version = "4.5.23", features = ["derive"] }
|
||||||
|
env_logger = "0.11.6"
|
||||||
|
eyre = "0.6.12"
|
||||||
|
if_chain = "1.0.2"
|
||||||
|
include_dir = "0.7.4"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
log = "0.4.22"
|
||||||
|
notify-debouncer-mini = { version = "0.5.0", default-features = false }
|
||||||
|
prometheus = "0.13.4"
|
||||||
|
rand = "0.8.5"
|
||||||
|
reqwest = { version = "0.12.9", default-features = false, features = [
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
ron = "0.8.1"
|
ron = "0.8.1"
|
||||||
serde = { version = "1.0.216", features = ["derive"] }
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
serde_json = "1.0.134"
|
serde_json = "1.0.134"
|
||||||
tokio = { version = "1.42.0", features = ["full"] }
|
|
||||||
thiserror = "2.0.9"
|
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
|
||||||
include_dir = "0.7.4"
|
|
||||||
chrono = "0.4.39"
|
|
||||||
prometheus = "0.13.4"
|
|
||||||
env_logger = "0.11.6"
|
|
||||||
log = "0.4.22"
|
|
||||||
serialport = "4.6.1"
|
serialport = "4.6.1"
|
||||||
|
thiserror = "2.0.9"
|
||||||
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
tokio-modbus = "0.16.1"
|
tokio-modbus = "0.16.1"
|
||||||
tokio-serial = "5.4.4"
|
tokio-serial = "5.4.4"
|
||||||
if_chain = "1.0.2"
|
|
||||||
notify-debouncer-mini = { version = "0.5.0", default-features = false }
|
|
||||||
lazy_static = "1.5.0"
|
|
||||||
rand = "0.8.5"
|
|
||||||
reqwest = { version = "0.12.9", default-features = false, features = [
|
|
||||||
"rustls-tls",
|
|
||||||
] }
|
|
||||||
eyre = "0.6.12"
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -83,6 +83,7 @@ impl Vehicle {
|
||||||
.timeout(std::time::Duration::from_secs(5))
|
.timeout(std::time::Duration::from_secs(5))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let response: ApiResponseOuter = data.json().await?;
|
let response: ApiResponseOuter = data.json().await?;
|
||||||
let response = response.response;
|
let response = response.response;
|
||||||
|
|
||||||
|
@ -94,6 +95,7 @@ impl Vehicle {
|
||||||
Ok(state.charge_state)
|
Ok(state.charge_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code, reason = "active charge control not yet implemented")]
|
||||||
pub async fn set_charging_amps(&self, charging_amps: i64) -> eyre::Result<()> {
|
pub async fn set_charging_amps(&self, charging_amps: i64) -> eyre::Result<()> {
|
||||||
self.client
|
self.client
|
||||||
.post(format!(
|
.post(format!(
|
||||||
|
|
|
@ -42,34 +42,36 @@ impl Car {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn vehicle(&self) -> &http::Vehicle {
|
pub const fn vehicle(&self) -> &http::Vehicle {
|
||||||
&self.vehicle
|
&self.vehicle
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(&self) -> &tokio::sync::RwLock<CarState> {
|
pub const fn state(&self) -> &tokio::sync::RwLock<CarState> {
|
||||||
&self.state
|
&self.state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CarState {
|
impl CarState {
|
||||||
pub fn charge_state(&self) -> Option<&ChargeState> {
|
pub async fn charge_state(&self) -> Option<&ChargeState> {
|
||||||
if self.is_outdated() {
|
if self.is_outdated().await {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
self.charge_state.as_ref()
|
self.charge_state.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_charging(&self) -> bool {
|
pub async fn is_charging(&self) -> bool {
|
||||||
self.charge_state().is_some_and(ChargeState::is_charging)
|
self.charge_state()
|
||||||
|
.await
|
||||||
|
.is_some_and(ChargeState::is_charging)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_age(&self) -> std::time::Duration {
|
pub fn data_age(&self) -> std::time::Duration {
|
||||||
std::time::Instant::now().duration_since(self.data_received)
|
std::time::Instant::now().duration_since(self.data_received)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_outdated(&self) -> bool {
|
async fn is_outdated(&self) -> bool {
|
||||||
self.data_age() > crate::CONFIG.read().car_state_interval
|
self.data_age() > crate::config::access_config().await.car_state_interval
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +153,7 @@ impl ChargeState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum ChargingState {
|
pub enum ChargingState {
|
||||||
Charging,
|
Charging,
|
||||||
Stopped,
|
Stopped,
|
||||||
|
|
|
@ -1,5 +1,114 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
static CONFIG_PATH: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
|
||||||
|
pub(super) static CONFIG: std::sync::OnceLock<tokio::sync::RwLock<Config>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
pub async fn access_config<'a>() -> tokio::sync::RwLockReadGuard<'a, Config> {
|
||||||
|
CONFIG.get().unwrap().read().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ConfigWatcher {
|
||||||
|
_debouncer: notify_debouncer_mini::Debouncer<notify_debouncer_mini::notify::RecommendedWatcher>,
|
||||||
|
_handle: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigWatcher {
|
||||||
|
pub fn new(path: impl AsRef<std::path::Path>) -> Option<Self> {
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
let mut debouncer =
|
||||||
|
notify_debouncer_mini::new_debouncer(std::time::Duration::from_secs(1), move |v| {
|
||||||
|
tx.send(v).unwrap();
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
debouncer
|
||||||
|
.watcher()
|
||||||
|
.watch(
|
||||||
|
path.as_ref(),
|
||||||
|
notify_debouncer_mini::notify::RecursiveMode::NonRecursive,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let config_path = std::path::PathBuf::from(path.as_ref());
|
||||||
|
|
||||||
|
let handle = tokio::task::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
Some(Ok(_event)) => {
|
||||||
|
let mut config = Config::load(&config_path);
|
||||||
|
config.validate();
|
||||||
|
|
||||||
|
if let Err(e) = overwrite_config(config).await {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(e)) => log::error!("Error {e:?} from watcher"),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
_debouncer: debouncer,
|
||||||
|
_handle: handle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn overwrite_config(config: Config) -> eyre::Result<()> {
|
||||||
|
*CONFIG
|
||||||
|
.get()
|
||||||
|
.ok_or_else(|| eyre::eyre!("could not get config"))?
|
||||||
|
.write()
|
||||||
|
.await = config;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_config(path: impl AsRef<std::path::Path>) -> Option<ConfigWatcher> {
|
||||||
|
log::trace!("loading config...");
|
||||||
|
let config = Config::load_and_save_defaults(&path);
|
||||||
|
log::trace!("watching config for changes...");
|
||||||
|
let config_watcher = ConfigWatcher::new(&path);
|
||||||
|
let _ = CONFIG_PATH.get_or_init(|| path.as_ref().to_path_buf());
|
||||||
|
CONFIG.set(tokio::sync::RwLock::new(config)).unwrap();
|
||||||
|
|
||||||
|
config_watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConfigHandle<'a> {
|
||||||
|
handle: tokio::sync::RwLockWriteGuard<'a, Config>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> core::ops::Deref for ConfigHandle<'a> {
|
||||||
|
type Target = tokio::sync::RwLockWriteGuard<'a, Config>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for ConfigHandle<'_> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ConfigHandle<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = self.save() {
|
||||||
|
log::error!("error saving config on drop of handle: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_to_config<'a>() -> ConfigHandle<'a> {
|
||||||
|
ConfigHandle {
|
||||||
|
handle: CONFIG.get().unwrap().write().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -34,20 +143,6 @@ impl Default for Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl common::config::ConfigTrait for Config {
|
|
||||||
fn load_and_save_defaults(path: impl AsRef<std::path::Path>) -> Self {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save(&self) -> eyre::Result<()> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid(&self) -> bool {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct PidControls {
|
pub struct PidControls {
|
||||||
|
@ -67,3 +162,41 @@ impl Default for PidControls {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn load(path: impl AsRef<std::path::Path>) -> Self {
|
||||||
|
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_to(&self, path: impl AsRef<std::path::Path>) -> eyre::Result<()> {
|
||||||
|
Ok(serde_json::ser::to_writer_pretty(
|
||||||
|
std::io::BufWriter::new(std::fs::File::create(path)?),
|
||||||
|
self,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> eyre::Result<()> {
|
||||||
|
self.save_to(CONFIG_PATH.get().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_and_save_defaults(path: impl AsRef<std::path::Path>) -> Self {
|
||||||
|
let mut config = Self::load(&path);
|
||||||
|
config.validate();
|
||||||
|
let result = config.save_to(path);
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("Failed to save config: {e:#?}",);
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&mut self) {
|
||||||
|
self.shutoff_voltage = self.shutoff_voltage.clamp(40.0, 60.0);
|
||||||
|
self.max_rate = self.max_rate.clamp(0, 30);
|
||||||
|
self.min_rate = self.min_rate.clamp(0, self.max_rate);
|
||||||
|
// self.duty_cycle_too_high = self.duty_cycle_too_high.clamp(0.0, 1.0);
|
||||||
|
// self.duty_cycle_too_low = self.duty_cycle_too_low.clamp(0.0, 1.0);
|
||||||
|
self.pid_controls.proportional_gain = self.pid_controls.proportional_gain.clamp(0.0, 50.0);
|
||||||
|
self.pid_controls.derivative_gain = self.pid_controls.derivative_gain.clamp(0.0, 50.0);
|
||||||
|
self.pid_controls.load_divisor = self.pid_controls.load_divisor.clamp(1.0, 50.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub struct VehicleController {
|
||||||
control_state: ChargeRateControllerState,
|
control_state: ChargeRateControllerState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code, reason = "not all states are currently in use")]
|
||||||
pub enum ChargeRateControllerState {
|
pub enum ChargeRateControllerState {
|
||||||
Inactive,
|
Inactive,
|
||||||
Charging { rate_amps: i64 },
|
Charging { rate_amps: i64 },
|
||||||
|
@ -14,7 +15,7 @@ pub enum InterfaceRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VehicleController {
|
impl VehicleController {
|
||||||
pub fn new(
|
pub const fn new(
|
||||||
car: std::sync::Arc<crate::api::Car>,
|
car: std::sync::Arc<crate::api::Car>,
|
||||||
requests: tokio::sync::mpsc::UnboundedReceiver<InterfaceRequest>,
|
requests: tokio::sync::mpsc::UnboundedReceiver<InterfaceRequest>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -25,14 +26,35 @@ impl VehicleController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> ! {
|
||||||
|
let (mut car_state_tick, mut pid_loop_tick) = {
|
||||||
|
let config = crate::config::access_config().await;
|
||||||
|
let car_state_tick = tokio::time::interval(config.car_state_interval);
|
||||||
|
let pid_loop_tick = tokio::time::interval(std::time::Duration::from_secs(
|
||||||
|
config.pid_controls.loop_time_seconds,
|
||||||
|
));
|
||||||
|
(car_state_tick, pid_loop_tick)
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = car_state_tick.tick() => { self.car.update().await; },
|
||||||
|
_ = pid_loop_tick.tick() => { self.run_cycle().await; }
|
||||||
|
Some(req) = self.requests.recv() => { self.process_requests(req).await; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_cycle(&mut self) {
|
pub async fn run_cycle(&mut self) {
|
||||||
let age = self.car.state().read().await.data_age();
|
let age = self.car.state().read().await.data_age();
|
||||||
if age >= crate::CONFIG.read().car_state_interval {
|
if age >= crate::config::access_config().await.car_state_interval {
|
||||||
self.car.update().await;
|
self.car.update().await;
|
||||||
}
|
}
|
||||||
match self.control_state {
|
match self.control_state {
|
||||||
ChargeRateControllerState::Inactive => {
|
ChargeRateControllerState::Inactive => {
|
||||||
if let Some(state) = self.car.state().read().await.charge_state() {
|
let car_state = self.car.state().read().await;
|
||||||
|
let state = car_state.charge_state().await;
|
||||||
|
|
||||||
|
if let Some(state) = state {
|
||||||
if state.is_charging() {
|
if state.is_charging() {
|
||||||
self.control_state = ChargeRateControllerState::Charging {
|
self.control_state = ChargeRateControllerState::Charging {
|
||||||
rate_amps: state.charge_amps,
|
rate_amps: state.charge_amps,
|
||||||
|
@ -40,19 +62,19 @@ impl VehicleController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChargeRateControllerState::Charging { rate_amps } => todo!(),
|
ChargeRateControllerState::Charging { rate_amps: _ } => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn process_requests(&mut self) -> eyre::Result<()> {
|
#[expect(
|
||||||
while let Some(req) = self.requests.recv().await {
|
clippy::needless_pass_by_ref_mut,
|
||||||
match req {
|
reason = "this will eventually need to mutate self"
|
||||||
InterfaceRequest::FlashLights => {
|
)]
|
||||||
self.car.vehicle().flash_lights().await?;
|
pub async fn process_requests(&mut self, req: InterfaceRequest) {
|
||||||
|
if let Err(e) = match req {
|
||||||
|
InterfaceRequest::FlashLights => self.car.vehicle().flash_lights().await,
|
||||||
|
} {
|
||||||
|
log::error!("failed to execute request: {e:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::significant_drop_tightening)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
@ -7,16 +9,13 @@ mod config;
|
||||||
mod control;
|
mod control;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
static CONFIG: common::config::ConfigSingleton<config::Config> =
|
|
||||||
common::config::ConfigSingleton::new();
|
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug, Clone)]
|
#[derive(clap::Parser, Debug, Clone)]
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
#[clap(long, default_value = "/etc/tesla-charge-controller")]
|
#[clap(long, default_value = "/etc/tesla-charge-controller/config.json")]
|
||||||
config_dir: PathBuf,
|
config: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand, Debug, Clone)]
|
#[derive(clap::Subcommand, Debug, Clone)]
|
||||||
|
@ -29,26 +28,55 @@ enum Commands {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
common::init_log();
|
env_logger::builder()
|
||||||
|
.format_module_path(false)
|
||||||
|
.format_timestamp(
|
||||||
|
if std::env::var("LOG_TIMESTAMP").is_ok_and(|v| v == "false") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(env_logger::TimestampPrecision::Seconds)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
if let Err(e) = run().await {
|
let args = Args::parse();
|
||||||
|
match args.command {
|
||||||
|
Commands::Watch => {
|
||||||
|
if let Err(e) = run(args).await {
|
||||||
log::error!("{e:?}");
|
log::error!("{e:?}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Commands::GenerateConfig => {
|
||||||
|
let config = config::Config::default();
|
||||||
|
match serde_json::to_string_pretty(&config) {
|
||||||
|
Ok(config_json) => {
|
||||||
|
println!("{config_json}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error serializing config: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run() -> eyre::Result<()> {
|
async fn run(args: Args) -> eyre::Result<()> {
|
||||||
let args = Args::parse();
|
let _config_watcher = config::init_config(args.config);
|
||||||
CONFIG.load(args.config_dir.join("tesla.json"))?;
|
|
||||||
|
|
||||||
let car = api::Car::new(&CONFIG.read().car_vin);
|
let car = {
|
||||||
|
let config = config::access_config().await;
|
||||||
|
api::Car::new(&config.car_vin)
|
||||||
|
};
|
||||||
let car = std::sync::Arc::new(car);
|
let car = std::sync::Arc::new(car);
|
||||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let mut vehicle_controller = control::VehicleController::new(car.clone(), rx);
|
let mut vehicle_controller = control::VehicleController::new(car.clone(), rx);
|
||||||
let server = server::launch_server(server::ServerState::new(car, tx));
|
let server = server::launch_server(server::ServerState::new(car, tx));
|
||||||
server.await;
|
|
||||||
|
|
||||||
vehicle_controller.run_cycle().await;
|
tokio::select! {
|
||||||
|
() = server => {}
|
||||||
|
() = vehicle_controller.run() => {}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,11 @@ use rocket::{
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::{api::Car, control::InterfaceRequest};
|
use crate::{
|
||||||
|
api::Car,
|
||||||
|
config::{access_config, write_to_config},
|
||||||
|
control::InterfaceRequest,
|
||||||
|
};
|
||||||
|
|
||||||
use self::static_handler::UiStatic;
|
use self::static_handler::UiStatic;
|
||||||
|
|
||||||
|
@ -23,7 +27,7 @@ pub struct ServerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
pub fn new(car: Arc<Car>, api_requests: UnboundedSender<InterfaceRequest>) -> Self {
|
pub const fn new(car: Arc<Car>, api_requests: UnboundedSender<InterfaceRequest>) -> Self {
|
||||||
Self { car, api_requests }
|
Self { car, api_requests }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,6 +87,7 @@ async fn car_state(
|
||||||
.read()
|
.read()
|
||||||
.await
|
.await
|
||||||
.charge_state()
|
.charge_state()
|
||||||
|
.await
|
||||||
.ok_or(ServerError::NoData)?
|
.ok_or(ServerError::NoData)?
|
||||||
.clone(),
|
.clone(),
|
||||||
))
|
))
|
||||||
|
@ -98,8 +103,8 @@ struct ControlState {
|
||||||
|
|
||||||
#[get("/control-state")]
|
#[get("/control-state")]
|
||||||
async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
|
async fn control_state(state: &State<ServerState>) -> Result<Json<ControlState>, ServerError> {
|
||||||
let is_charging = state.car.state().read().await.is_charging();
|
let is_charging = state.car.state().read().await.is_charging().await;
|
||||||
let config = crate::CONFIG.read();
|
let config = access_config().await;
|
||||||
let control_enable = config.control_enable;
|
let control_enable = config.control_enable;
|
||||||
let max_rate = config.max_rate;
|
let max_rate = config.max_rate;
|
||||||
let min_rate = config.min_rate;
|
let min_rate = config.min_rate;
|
||||||
|
@ -119,29 +124,29 @@ fn flash(state: &State<ServerState>, remote_addr: std::net::IpAddr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/disable-control")]
|
#[post("/disable-control")]
|
||||||
fn disable_control(remote_addr: std::net::IpAddr) {
|
async fn disable_control(remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("disabling control: {remote_addr:?}");
|
log::warn!("disabling control: {remote_addr:?}");
|
||||||
crate::CONFIG.write().control_enable = false;
|
write_to_config().await.control_enable = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/enable-control")]
|
#[post("/enable-control")]
|
||||||
fn enable_control(remote_addr: std::net::IpAddr) {
|
async fn enable_control(remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("enabling control: {remote_addr:?}");
|
log::warn!("enabling control: {remote_addr:?}");
|
||||||
crate::CONFIG.write().control_enable = true;
|
write_to_config().await.control_enable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/shutoff/voltage/<voltage>")]
|
#[post("/shutoff/voltage/<voltage>")]
|
||||||
fn set_shutoff(voltage: f64, remote_addr: std::net::IpAddr) {
|
async fn set_shutoff(voltage: f64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting shutoff voltage: {remote_addr:?}");
|
log::warn!("setting shutoff voltage: {remote_addr:?}");
|
||||||
let voltage = voltage.clamp(40., 60.);
|
let voltage = voltage.clamp(40., 60.);
|
||||||
crate::CONFIG.write().shutoff_voltage = voltage;
|
write_to_config().await.shutoff_voltage = voltage;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/shutoff/time/<time>")]
|
#[post("/shutoff/time/<time>")]
|
||||||
fn set_shutoff_time(time: u64, remote_addr: std::net::IpAddr) {
|
async fn set_shutoff_time(time: u64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting shutoff time: {remote_addr:?}");
|
log::warn!("setting shutoff time: {remote_addr:?}");
|
||||||
let time = time.clamp(5, 120);
|
let time = time.clamp(5, 120);
|
||||||
crate::CONFIG.write().shutoff_voltage_time_seconds = time;
|
write_to_config().await.shutoff_voltage_time_seconds = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
@ -151,8 +156,8 @@ struct ShutoffStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/shutoff/status")]
|
#[get("/shutoff/status")]
|
||||||
fn shutoff_status() -> Json<ShutoffStatus> {
|
async fn shutoff_status() -> Json<ShutoffStatus> {
|
||||||
let config = crate::CONFIG.read();
|
let config = access_config().await;
|
||||||
Json(ShutoffStatus {
|
Json(ShutoffStatus {
|
||||||
voltage: config.shutoff_voltage,
|
voltage: config.shutoff_voltage,
|
||||||
time: config.shutoff_voltage_time_seconds,
|
time: config.shutoff_voltage_time_seconds,
|
||||||
|
@ -160,46 +165,48 @@ fn shutoff_status() -> Json<ShutoffStatus> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/set-max/<limit>")]
|
#[post("/set-max/<limit>")]
|
||||||
fn set_max(limit: i64, remote_addr: std::net::IpAddr) {
|
async fn set_max(limit: i64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting max: {remote_addr:?}");
|
log::warn!("setting max: {remote_addr:?}");
|
||||||
let limit = limit.clamp(crate::CONFIG.read().min_rate, 32);
|
let mut config = write_to_config().await;
|
||||||
crate::CONFIG.write().max_rate = limit;
|
let limit = limit.clamp(config.min_rate, 32);
|
||||||
|
config.max_rate = limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/set-min/<limit>")]
|
#[post("/set-min/<limit>")]
|
||||||
fn set_min(limit: i64, remote_addr: std::net::IpAddr) {
|
async fn set_min(limit: i64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting min: {remote_addr:?}");
|
log::warn!("setting min: {remote_addr:?}");
|
||||||
let limit = limit.clamp(0, crate::CONFIG.read().max_rate);
|
let mut config = write_to_config().await;
|
||||||
crate::CONFIG.write().min_rate = limit;
|
let limit = limit.clamp(0, config.max_rate);
|
||||||
|
config.min_rate = limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/pid-settings/proportional/<gain>")]
|
#[post("/pid-settings/proportional/<gain>")]
|
||||||
fn set_proportional_gain(gain: f64, remote_addr: std::net::IpAddr) {
|
async fn set_proportional_gain(gain: f64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting proportional gain: {remote_addr:?}");
|
log::warn!("setting proportional gain: {remote_addr:?}");
|
||||||
crate::CONFIG.write().pid_controls.proportional_gain = gain;
|
write_to_config().await.pid_controls.proportional_gain = gain;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/pid-settings/derivative/<gain>")]
|
#[post("/pid-settings/derivative/<gain>")]
|
||||||
fn set_derivative_gain(gain: f64, remote_addr: std::net::IpAddr) {
|
async fn set_derivative_gain(gain: f64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting derivative gain: {remote_addr:?}");
|
log::warn!("setting derivative gain: {remote_addr:?}");
|
||||||
crate::CONFIG.write().pid_controls.derivative_gain = gain;
|
write_to_config().await.pid_controls.derivative_gain = gain;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/pid-settings/loop_time_seconds/<time>")]
|
#[post("/pid-settings/loop_time_seconds/<time>")]
|
||||||
fn set_pid_loop_length(time: u64, remote_addr: std::net::IpAddr) {
|
async fn set_pid_loop_length(time: u64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting pid loop interval: {remote_addr:?}");
|
log::warn!("setting pid loop interval: {remote_addr:?}");
|
||||||
crate::CONFIG.write().pid_controls.loop_time_seconds = time;
|
write_to_config().await.pid_controls.loop_time_seconds = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/pid-settings/load_divisor/<divisor>")]
|
#[post("/pid-settings/load_divisor/<divisor>")]
|
||||||
fn set_load_divisor(divisor: f64, remote_addr: std::net::IpAddr) {
|
async fn set_load_divisor(divisor: f64, remote_addr: std::net::IpAddr) {
|
||||||
log::warn!("setting load divisor interval: {remote_addr:?}");
|
log::warn!("setting load divisor interval: {remote_addr:?}");
|
||||||
crate::CONFIG.write().pid_controls.load_divisor = divisor;
|
write_to_config().await.pid_controls.load_divisor = divisor;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/pid-settings/status")]
|
#[get("/pid-settings/status")]
|
||||||
fn pid_settings() -> Json<crate::config::PidControls> {
|
async fn pid_settings() -> Json<crate::config::PidControls> {
|
||||||
Json(crate::CONFIG.read().pid_controls)
|
Json(access_config().await.pid_controls)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/metrics")]
|
#[get("/metrics")]
|
||||||
|
|
|
@ -52,10 +52,12 @@ impl Handler for UiStatic {
|
||||||
data: v.contents().to_vec(),
|
data: v.contents().to_vec(),
|
||||||
name: p,
|
name: p,
|
||||||
})
|
})
|
||||||
.or(UI_DIR_FILES.get_file(&plus_index).map(|v| RawHtml {
|
.or_else(|| {
|
||||||
|
UI_DIR_FILES.get_file(&plus_index).map(|v| RawHtml {
|
||||||
data: v.contents().to_vec(),
|
data: v.contents().to_vec(),
|
||||||
name: plus_index,
|
name: plus_index,
|
||||||
}));
|
})
|
||||||
|
});
|
||||||
file.respond_to(req).or_forward((data, Status::NotFound))
|
file.respond_to(req).or_forward((data, Status::NotFound))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
watch.sh
7
watch.sh
|
@ -1,7 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
(
|
|
||||||
trap 'kill 0' SIGINT
|
|
||||||
cargo watch -w "src" -x check -s 'touch .trigger' &
|
|
||||||
RUST_LOG=error,warn,info cargo watch --no-vcs-ignores -w .trigger -x "run -- --config-dir test-config watch"
|
|
||||||
)
|
|
Loading…
Add table
Reference in a new issue