supervisor: web
This commit is contained in:
parent
2c7aa8641c
commit
70c9188f55
6 changed files with 176 additions and 112 deletions
charge-controller-supervisor/src
|
@ -8,34 +8,16 @@ pub struct Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
pub enum ChargeControllerConfig {
|
pub struct ChargeControllerConfig {
|
||||||
Pl {
|
pub name: String,
|
||||||
serial_port: String,
|
pub serial_port: String,
|
||||||
baud_rate: u32,
|
pub baud_rate: u32,
|
||||||
timeout_milliseconds: u64,
|
pub watch_interval_seconds: u64,
|
||||||
watch_interval_seconds: u64,
|
pub variant: ChargeControllerVariant,
|
||||||
},
|
|
||||||
Tristar {
|
|
||||||
serial_port: String,
|
|
||||||
baud_rate: u32,
|
|
||||||
watch_interval_seconds: u64,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChargeControllerConfig {
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
pub fn interval(&self) -> u64 {
|
pub enum ChargeControllerVariant {
|
||||||
match self {
|
Tristar,
|
||||||
ChargeControllerConfig::Pl {
|
Pl { timeout_milliseconds: u64 },
|
||||||
serial_port: _,
|
|
||||||
baud_rate: _,
|
|
||||||
timeout_milliseconds: _,
|
|
||||||
watch_interval_seconds,
|
|
||||||
}
|
|
||||||
| ChargeControllerConfig::Tristar {
|
|
||||||
serial_port: _,
|
|
||||||
baud_rate: _,
|
|
||||||
watch_interval_seconds,
|
|
||||||
} => *watch_interval_seconds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
80
charge-controller-supervisor/src/controller.rs
Normal file
80
charge-controller-supervisor/src/controller.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
pub struct Controller {
|
||||||
|
name: String,
|
||||||
|
interval: std::time::Duration,
|
||||||
|
inner: ControllerInner,
|
||||||
|
data: std::sync::Arc<tokio::sync::RwLock<CommonData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, serde::Serialize, Clone)]
|
||||||
|
pub struct CommonData {
|
||||||
|
pub battery_voltage: f64,
|
||||||
|
pub target_voltage: f64,
|
||||||
|
pub battery_temp: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Controller {
|
||||||
|
pub fn new(config: crate::config::ChargeControllerConfig) -> eyre::Result<Self> {
|
||||||
|
let inner = match config.variant {
|
||||||
|
crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar(
|
||||||
|
crate::tristar::Tristar::new(config.serial_port, config.baud_rate)?,
|
||||||
|
),
|
||||||
|
crate::config::ChargeControllerVariant::Pl {
|
||||||
|
timeout_milliseconds,
|
||||||
|
} => ControllerInner::Pl(crate::pl::Pli::new(
|
||||||
|
config.serial_port,
|
||||||
|
config.baud_rate,
|
||||||
|
timeout_milliseconds,
|
||||||
|
)?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = CommonData::default();
|
||||||
|
|
||||||
|
let data = std::sync::Arc::new(tokio::sync::RwLock::new(data));
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
name: config.name,
|
||||||
|
interval: std::time::Duration::from_secs(config.watch_interval_seconds),
|
||||||
|
inner,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_data_ptr(&self) -> std::sync::Arc<tokio::sync::RwLock<CommonData>> {
|
||||||
|
self.data.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh(&mut self) -> eyre::Result<()> {
|
||||||
|
let data = self.inner.refresh().await?;
|
||||||
|
*self.data.write().await = data;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timeout_interval(&self) -> std::time::Duration {
|
||||||
|
self.interval
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ControllerInner {
|
||||||
|
Pl(crate::pl::Pli),
|
||||||
|
Tristar(crate::tristar::Tristar),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControllerInner {
|
||||||
|
pub async fn refresh(&mut self) -> eyre::Result<CommonData> {
|
||||||
|
match self {
|
||||||
|
ControllerInner::Pl(pli) => {
|
||||||
|
let pl_data = pli.refresh()?;
|
||||||
|
Ok(pl_data)
|
||||||
|
}
|
||||||
|
ControllerInner::Tristar(tristar) => {
|
||||||
|
let tristar_data = tristar.refresh().await?;
|
||||||
|
Ok(tristar_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ enum Commands {
|
||||||
GenerateConfig,
|
GenerateConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod controller;
|
||||||
mod gauges;
|
mod gauges;
|
||||||
mod pl;
|
mod pl;
|
||||||
mod tristar;
|
mod tristar;
|
||||||
|
@ -50,56 +51,6 @@ async fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Controller {
|
|
||||||
Pl(pl::Pli),
|
|
||||||
Tristar(tristar::Tristar),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Controller {
|
|
||||||
async fn refresh(&mut self) -> eyre::Result<()> {
|
|
||||||
match self {
|
|
||||||
Controller::Pl(pli) => {
|
|
||||||
pli.refresh()?;
|
|
||||||
}
|
|
||||||
Controller::Tristar(tristar) => {
|
|
||||||
tristar.refresh().await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Controller::Pl(pli) => pli.name().to_owned(),
|
|
||||||
Controller::Tristar(tristar) => tristar.name().to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl config::ChargeControllerConfig {
|
|
||||||
fn connect(&self) -> eyre::Result<Controller> {
|
|
||||||
match self {
|
|
||||||
config::ChargeControllerConfig::Pl {
|
|
||||||
serial_port,
|
|
||||||
baud_rate,
|
|
||||||
timeout_milliseconds,
|
|
||||||
watch_interval_seconds: _,
|
|
||||||
} => {
|
|
||||||
let pl = pl::Pli::new(serial_port.to_owned(), *baud_rate, *timeout_milliseconds)?;
|
|
||||||
Ok(Controller::Pl(pl))
|
|
||||||
}
|
|
||||||
config::ChargeControllerConfig::Tristar {
|
|
||||||
serial_port,
|
|
||||||
baud_rate,
|
|
||||||
watch_interval_seconds: _,
|
|
||||||
} => {
|
|
||||||
let tristar = tristar::Tristar::new(serial_port.to_owned(), *baud_rate)?;
|
|
||||||
Ok(Controller::Tristar(tristar))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run() -> eyre::Result<()> {
|
async fn run() -> eyre::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
@ -118,15 +69,20 @@ async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
let config: config::Config = serde_json::from_reader(std::fs::File::open(args.config)?)?;
|
let config: config::Config = serde_json::from_reader(std::fs::File::open(args.config)?)?;
|
||||||
|
|
||||||
let mut controllers = futures::stream::FuturesUnordered::new();
|
let mut controllers = futures::stream::FuturesUnordered::new();
|
||||||
|
let mut map = std::collections::HashMap::new();
|
||||||
|
|
||||||
for controller in config.charge_controllers {
|
for config in config.charge_controllers {
|
||||||
match controller.connect() {
|
let n = config.name.clone();
|
||||||
Ok(v) => controllers.push(run_loop(v, controller.interval())),
|
match controller::Controller::new(config) {
|
||||||
Err(e) => log::error!("couldn't connect to {controller:?}: {e:?}"),
|
Ok(v) => {
|
||||||
|
map.insert(n, v.get_data_ptr());
|
||||||
|
controllers.push(run_loop(v));
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("couldn't connect to {}: {e:?}", n),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let server = web::rocket();
|
let server = web::rocket(web::ServerState::new(map));
|
||||||
let server_task = tokio::task::spawn(server.launch());
|
let server_task = tokio::task::spawn(server.launch());
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
@ -149,8 +105,8 @@ async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
std::process::exit(1)
|
std::process::exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_loop(mut controller: Controller, timeout_interval: u64) -> eyre::Result<()> {
|
async fn run_loop(mut controller: controller::Controller) -> eyre::Result<()> {
|
||||||
let mut timeout = tokio::time::interval(std::time::Duration::from_secs(timeout_interval));
|
let mut timeout = tokio::time::interval(controller.timeout_interval());
|
||||||
loop {
|
loop {
|
||||||
timeout.tick().await;
|
timeout.tick().await;
|
||||||
if let Err(e) = controller.refresh().await {
|
if let Err(e) = controller.refresh().await {
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
use std::{
|
use std::{io::Write, time::Duration};
|
||||||
io::Write,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use chrono::Timelike;
|
use chrono::Timelike;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -14,7 +10,7 @@ use crate::gauges::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Pli {
|
pub struct Pli {
|
||||||
pub state: Arc<RwLock<PlState>>,
|
// pub state: Arc<RwLock<PlState>>,
|
||||||
port_name: String,
|
port_name: String,
|
||||||
port: Box<dyn SerialPort>,
|
port: Box<dyn SerialPort>,
|
||||||
}
|
}
|
||||||
|
@ -121,13 +117,13 @@ impl Pli {
|
||||||
.open()?;
|
.open()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
state: Arc::new(RwLock::new(Default::default())),
|
// state: Arc::new(RwLock::new(Default::default())),
|
||||||
port_name: serial_port,
|
port_name: serial_port,
|
||||||
port,
|
port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh(&mut self) -> eyre::Result<()> {
|
pub fn refresh(&mut self) -> eyre::Result<crate::controller::CommonData> {
|
||||||
let new_state = self.read_state()?;
|
let new_state = self.read_state()?;
|
||||||
BATTERY_VOLTAGE
|
BATTERY_VOLTAGE
|
||||||
.with_label_values(&[&self.port_name])
|
.with_label_values(&[&self.port_name])
|
||||||
|
@ -150,9 +146,11 @@ impl Pli {
|
||||||
|
|
||||||
set_regulator_gauges(new_state.regulator_state, &self.port_name);
|
set_regulator_gauges(new_state.regulator_state, &self.port_name);
|
||||||
|
|
||||||
*self.state.write().expect("PLI state handler panicked!!") = new_state;
|
Ok(crate::controller::CommonData {
|
||||||
|
battery_voltage: new_state.battery_voltage,
|
||||||
Ok(())
|
target_voltage: new_state.target_voltage,
|
||||||
|
battery_temp: new_state.battery_temp,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(dead_code, reason = "writing settings is not yet implemented")]
|
#[expect(dead_code, reason = "writing settings is not yet implemented")]
|
||||||
|
@ -254,10 +252,6 @@ impl Pli {
|
||||||
Err(eyre::eyre!("read error: result is {}", buf[0]))
|
Err(eyre::eyre!("read error: result is {}", buf[0]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.port_name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PlRamAddress {
|
enum PlRamAddress {
|
||||||
|
|
|
@ -42,7 +42,6 @@ impl Scaling {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Tristar {
|
pub struct Tristar {
|
||||||
state: TristarState,
|
|
||||||
port_name: String,
|
port_name: String,
|
||||||
modbus: tokio_modbus::client::Context,
|
modbus: tokio_modbus::client::Context,
|
||||||
charge_state_gauges: ChargeStateGauges,
|
charge_state_gauges: ChargeStateGauges,
|
||||||
|
@ -241,7 +240,6 @@ impl Tristar {
|
||||||
let modbus = tokio_modbus::client::rtu::attach_slave(modbus_serial, slave);
|
let modbus = tokio_modbus::client::rtu::attach_slave(modbus_serial, slave);
|
||||||
let charge_state_gauges = ChargeStateGauges::new(&serial_port);
|
let charge_state_gauges = ChargeStateGauges::new(&serial_port);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
state: Default::default(),
|
|
||||||
port_name: serial_port,
|
port_name: serial_port,
|
||||||
modbus,
|
modbus,
|
||||||
charge_state_gauges,
|
charge_state_gauges,
|
||||||
|
@ -249,7 +247,7 @@ impl Tristar {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(&mut self) -> eyre::Result<()> {
|
pub async fn refresh(&mut self) -> eyre::Result<crate::controller::CommonData> {
|
||||||
let new_state = self.get_data().await?;
|
let new_state = self.get_data().await?;
|
||||||
self.consecutive_errors = 0;
|
self.consecutive_errors = 0;
|
||||||
BATTERY_VOLTAGE
|
BATTERY_VOLTAGE
|
||||||
|
@ -287,13 +285,12 @@ impl Tristar {
|
||||||
.set(new_state.tristar_open_circuit_voltage);
|
.set(new_state.tristar_open_circuit_voltage);
|
||||||
|
|
||||||
self.charge_state_gauges.set(new_state.charge_state);
|
self.charge_state_gauges.set(new_state.charge_state);
|
||||||
self.state = new_state;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(crate::controller::CommonData {
|
||||||
}
|
battery_voltage: new_state.battery_voltage,
|
||||||
|
target_voltage: new_state.target_voltage,
|
||||||
pub fn name(&self) -> &str {
|
battery_temp: f64::from(new_state.battery_temp),
|
||||||
&self.port_name
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_data(&mut self) -> eyre::Result<TristarState> {
|
async fn get_data(&mut self) -> eyre::Result<TristarState> {
|
||||||
|
|
|
@ -1,7 +1,60 @@
|
||||||
use rocket::{get, routes};
|
use rocket::{get, routes, serde::json::Json, State};
|
||||||
|
|
||||||
pub fn rocket() -> rocket::Rocket<rocket::Build> {
|
pub struct ServerState {
|
||||||
rocket::build().mount("/", routes![metrics,])
|
map: std::collections::HashMap<
|
||||||
|
String,
|
||||||
|
std::sync::Arc<tokio::sync::RwLock<crate::controller::CommonData>>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
pub fn new(
|
||||||
|
map: std::collections::HashMap<
|
||||||
|
String,
|
||||||
|
std::sync::Arc<tokio::sync::RwLock<crate::controller::CommonData>>,
|
||||||
|
>,
|
||||||
|
) -> Self {
|
||||||
|
Self { map }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
||||||
|
rocket::build()
|
||||||
|
.manage(state)
|
||||||
|
.mount("/", routes![metrics, interfaces, all_interfaces, interface])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/interfaces")]
|
||||||
|
fn interfaces(state: &State<ServerState>) -> Json<Vec<String>> {
|
||||||
|
Json(state.map.keys().cloned().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/interfaces/data")]
|
||||||
|
async fn all_interfaces(
|
||||||
|
state: &State<ServerState>,
|
||||||
|
) -> Json<Vec<(String, crate::controller::CommonData)>> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
|
||||||
|
for (k, v) in &state.map {
|
||||||
|
data.push((k.clone(), v.read().await.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/interface/<name>")]
|
||||||
|
async fn interface(
|
||||||
|
name: &str,
|
||||||
|
state: &State<ServerState>,
|
||||||
|
) -> Result<Json<crate::controller::CommonData>, ServerError> {
|
||||||
|
let data = state
|
||||||
|
.map
|
||||||
|
.get(name)
|
||||||
|
.ok_or(ServerError::NotFound)?
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.clone();
|
||||||
|
Ok(Json(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/metrics")]
|
#[get("/metrics")]
|
||||||
|
@ -14,6 +67,7 @@ fn metrics() -> Result<String, ServerError> {
|
||||||
|
|
||||||
enum ServerError {
|
enum ServerError {
|
||||||
Prometheus,
|
Prometheus,
|
||||||
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<prometheus::Error> for ServerError {
|
impl From<prometheus::Error> for ServerError {
|
||||||
|
@ -25,6 +79,7 @@ impl From<prometheus::Error> for ServerError {
|
||||||
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,
|
||||||
ServerError::Prometheus => rocket::http::Status::InternalServerError,
|
ServerError::Prometheus => rocket::http::Status::InternalServerError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue