supervisor: web

This commit is contained in:
Alex Janka 2024-12-28 20:39:07 +11:00
parent 2c7aa8641c
commit 70c9188f55
6 changed files with 176 additions and 112 deletions

View file

@ -8,34 +8,16 @@ pub struct Config {
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum ChargeControllerConfig {
Pl {
serial_port: String,
baud_rate: u32,
timeout_milliseconds: u64,
watch_interval_seconds: u64,
},
Tristar {
serial_port: String,
baud_rate: u32,
watch_interval_seconds: u64,
},
pub struct ChargeControllerConfig {
pub name: String,
pub serial_port: String,
pub baud_rate: u32,
pub watch_interval_seconds: u64,
pub variant: ChargeControllerVariant,
}
impl ChargeControllerConfig {
pub fn interval(&self) -> u64 {
match self {
ChargeControllerConfig::Pl {
serial_port: _,
baud_rate: _,
timeout_milliseconds: _,
watch_interval_seconds,
}
| ChargeControllerConfig::Tristar {
serial_port: _,
baud_rate: _,
watch_interval_seconds,
} => *watch_interval_seconds,
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum ChargeControllerVariant {
Tristar,
Pl { timeout_milliseconds: u64 },
}

View 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)
}
}
}
}

View file

@ -21,6 +21,7 @@ enum Commands {
GenerateConfig,
}
mod controller;
mod gauges;
mod pl;
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<()> {
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 mut controllers = futures::stream::FuturesUnordered::new();
let mut map = std::collections::HashMap::new();
for controller in config.charge_controllers {
match controller.connect() {
Ok(v) => controllers.push(run_loop(v, controller.interval())),
Err(e) => log::error!("couldn't connect to {controller:?}: {e:?}"),
for config in config.charge_controllers {
let n = config.name.clone();
match controller::Controller::new(config) {
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());
tokio::select! {
@ -149,8 +105,8 @@ async fn watch(args: Args) -> eyre::Result<()> {
std::process::exit(1)
}
async fn run_loop(mut controller: Controller, timeout_interval: u64) -> eyre::Result<()> {
let mut timeout = tokio::time::interval(std::time::Duration::from_secs(timeout_interval));
async fn run_loop(mut controller: controller::Controller) -> eyre::Result<()> {
let mut timeout = tokio::time::interval(controller.timeout_interval());
loop {
timeout.tick().await;
if let Err(e) = controller.refresh().await {

View file

@ -1,8 +1,4 @@
use std::{
io::Write,
sync::{Arc, RwLock},
time::Duration,
};
use std::{io::Write, time::Duration};
use chrono::Timelike;
use serde::{Deserialize, Serialize};
@ -14,7 +10,7 @@ use crate::gauges::{
};
pub struct Pli {
pub state: Arc<RwLock<PlState>>,
// pub state: Arc<RwLock<PlState>>,
port_name: String,
port: Box<dyn SerialPort>,
}
@ -121,13 +117,13 @@ impl Pli {
.open()?;
Ok(Self {
state: Arc::new(RwLock::new(Default::default())),
// state: Arc::new(RwLock::new(Default::default())),
port_name: serial_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()?;
BATTERY_VOLTAGE
.with_label_values(&[&self.port_name])
@ -150,9 +146,11 @@ impl Pli {
set_regulator_gauges(new_state.regulator_state, &self.port_name);
*self.state.write().expect("PLI state handler panicked!!") = new_state;
Ok(())
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")]
@ -254,10 +252,6 @@ impl Pli {
Err(eyre::eyre!("read error: result is {}", buf[0]))
}
}
pub fn name(&self) -> &str {
&self.port_name
}
}
enum PlRamAddress {

View file

@ -42,7 +42,6 @@ impl Scaling {
}
pub struct Tristar {
state: TristarState,
port_name: String,
modbus: tokio_modbus::client::Context,
charge_state_gauges: ChargeStateGauges,
@ -241,7 +240,6 @@ impl Tristar {
let modbus = tokio_modbus::client::rtu::attach_slave(modbus_serial, slave);
let charge_state_gauges = ChargeStateGauges::new(&serial_port);
Ok(Self {
state: Default::default(),
port_name: serial_port,
modbus,
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?;
self.consecutive_errors = 0;
BATTERY_VOLTAGE
@ -287,13 +285,12 @@ impl Tristar {
.set(new_state.tristar_open_circuit_voltage);
self.charge_state_gauges.set(new_state.charge_state);
self.state = new_state;
Ok(())
}
pub fn name(&self) -> &str {
&self.port_name
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> {

View file

@ -1,7 +1,60 @@
use rocket::{get, routes};
use rocket::{get, routes, serde::json::Json, State};
pub fn rocket() -> rocket::Rocket<rocket::Build> {
rocket::build().mount("/", routes![metrics,])
pub struct ServerState {
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")]
@ -14,6 +67,7 @@ fn metrics() -> Result<String, ServerError> {
enum ServerError {
Prometheus,
NotFound,
}
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 {
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
Err(match self {
Self::NotFound => rocket::http::Status::NotFound,
ServerError::Prometheus => rocket::http::Status::InternalServerError,
})
}