config validation and reloading

This commit is contained in:
Alex Janka 2024-01-23 12:14:04 +11:00
parent b0b8cac319
commit 2ae9403893
5 changed files with 219 additions and 25 deletions

120
Cargo.lock generated
View file

@ -485,6 +485,15 @@ dependencies = [
"libc",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.17"
@ -676,6 +685,18 @@ dependencies = [
"version_check",
]
[[package]]
name = "filetime"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"windows-sys 0.52.0",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -706,6 +727,15 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@ -1071,6 +1101,26 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "io-kit-sys"
version = "0.4.0"
@ -1129,6 +1179,26 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "kqueue"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "lazy_static"
version = "0.2.11"
@ -1382,6 +1452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"log 0.4.20",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
@ -1444,6 +1515,35 @@ dependencies = [
"memchr 1.0.2",
]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.4.1",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log 0.4.20",
"mio",
"walkdir",
"windows-sys 0.48.0",
]
[[package]]
name = "notify-debouncer-mini"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
dependencies = [
"log 0.4.20",
"notify",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -2140,6 +2240,15 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.22"
@ -2535,6 +2644,7 @@ dependencies = [
"log 0.4.20",
"metrics",
"metrics-prometheus",
"notify-debouncer-mini",
"prometheus",
"rocket",
"ron",
@ -3004,6 +3114,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"

View file

@ -31,3 +31,4 @@ log = "0.4"
serialport = "4.3"
libmodbus-rs = "0.8.3"
if_chain = "1.0.2"
notify-debouncer-mini = { version = "0.4.1", default-features = false }

View file

@ -1,24 +1,83 @@
use std::{
ops::DerefMut,
path::PathBuf,
path::{Path, PathBuf},
sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
time::Duration,
};
use notify_debouncer_mini::{new_debouncer, notify::RecommendedWatcher, Debouncer};
use serde::{Deserialize, Serialize};
use tokio::task::JoinHandle;
use crate::{
errors::{ConfigError, PrintErrors},
types::Coords,
};
pub(super) static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
static CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
static CONFIG_PATH: OnceLock<PathBuf> = OnceLock::new();
pub(super) static CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
pub fn access_config<'a>() -> RwLockReadGuard<'a, Config> {
CONFIG
.get_or_init(|| RwLock::new(Config::load_and_save_defaults()))
.read()
.unwrap()
CONFIG.get().unwrap().read().unwrap()
}
pub(super) struct ConfigWatcher {
_debouncer: Debouncer<RecommendedWatcher>,
_handle: JoinHandle<()>,
}
impl ConfigWatcher {
pub fn new(path: &Path) -> Option<Self> {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut debouncer = new_debouncer(Duration::from_secs(1), move |v| {
tx.send(v)
.some_or_print_with("Failed to send event to queue");
})
.ok()?;
debouncer
.watcher()
.watch(
path,
notify_debouncer_mini::notify::RecursiveMode::NonRecursive,
)
.ok()?;
let config_path = PathBuf::from(path);
let handle = tokio::task::spawn(async move {
log::warn!("from task");
loop {
match rx.recv().await {
Some(Ok(_event)) => {
log::warn!("Reloading config");
let mut config = Config::load(&config_path);
config.validate();
if let Some(mut c) = CONFIG.get().and_then(|v| v.write().ok()) {
*c = config;
} else {
log::error!("Reloading config: got notified, but failed to lock");
}
}
Some(Err(e)) => log::error!("Error {e:?} from watcher"),
None => {}
}
}
});
Some(Self {
_debouncer: debouncer,
_handle: handle,
})
}
}
pub fn init_config(path: PathBuf) -> (Config, Option<ConfigWatcher>) {
let config = Config::load_and_save_defaults(&path);
let config_watcher = ConfigWatcher::new(&path);
let _ = CONFIG_PATH.get_or_init(|| path);
(config, config_watcher)
}
pub struct ConfigHandle<'a> {
@ -48,14 +107,11 @@ impl<'a> Drop for ConfigHandle<'a> {
pub fn write_to_config<'a>() -> ConfigHandle<'a> {
ConfigHandle {
handle: CONFIG
.get_or_init(|| RwLock::new(Config::load_and_save_defaults()))
.write()
.unwrap(),
handle: CONFIG.get().unwrap().write().unwrap(),
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)]
pub struct Config {
pub tesla_update_interval_seconds: u64,
@ -74,7 +130,7 @@ pub struct Config {
pub pid_controls: PidControls,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
pub struct PidControls {
pub proportional_gain: f64,
pub derivative_gain: f64,
@ -91,7 +147,7 @@ impl Default for PidControls {
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum ChargeControllerConfig {
Pl {
serial_port: String,
@ -131,23 +187,38 @@ impl Default for Config {
}
impl Config {
fn load() -> Self {
serde_json::from_str(&std::fs::read_to_string(CONFIG_PATH.get().unwrap()).unwrap()).unwrap()
fn load(path: &Path) -> Self {
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
}
fn save(&self) -> Result<(), ConfigError> {
fn save_to(&self, path: &Path) -> Result<(), ConfigError> {
Ok(serde_json::ser::to_writer_pretty(
std::io::BufWriter::new(std::fs::File::create(CONFIG_PATH.get().unwrap())?),
std::io::BufWriter::new(std::fs::File::create(path)?),
self,
)?)
}
fn load_and_save_defaults() -> Self {
let config = Self::load();
let result = config.save();
fn save(&self) -> Result<(), ConfigError> {
self.save_to(CONFIG_PATH.get().unwrap())
}
fn load_and_save_defaults(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(1, 30);
self.min_rate = self.min_rate.clamp(1, 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);
}
}

View file

@ -4,9 +4,9 @@ extern crate rocket;
use api_interface::TeslaInterface;
use charge_controllers::{pl::Pli, tristar::Tristar};
use clap::{Parser, Subcommand};
use config::{access_config, ChargeControllerConfig, CONFIG_PATH};
use config::{access_config, ChargeControllerConfig};
use errors::PrintErrors;
use std::path::PathBuf;
use std::{path::PathBuf, sync::RwLock};
use tesla_charge_rate::TeslaChargeRateController;
use teslatte::vehicles::ChargingState;
@ -52,7 +52,9 @@ async fn main() {
let args = Args::parse();
let auth_path = args.config_dir.join("auth");
let _ = CONFIG_PATH.set(args.config_dir.join("config.json"));
let config_path = args.config_dir.join("config.json");
let (config, _config_watcher) = config::init_config(config_path);
config::CONFIG.get_or_init(|| RwLock::new(config));
let _recorder = metrics_prometheus::install();
match args.command {

View file

@ -144,7 +144,7 @@ impl TryFrom<teslatte::vehicles::DriveState> for FromDriveState {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct Coords {
pub latitude: f64,
pub longitude: f64,