207 lines
6.2 KiB
Rust
207 lines
6.2 KiB
Rust
#![feature(let_chains)]
|
|
|
|
use clap::Parser;
|
|
use futures::StreamExt;
|
|
use std::path::PathBuf;
|
|
|
|
mod config;
|
|
|
|
#[derive(clap::Parser, Debug, Clone)]
|
|
#[clap(author, version, about, long_about = None)]
|
|
struct Args {
|
|
#[command(subcommand)]
|
|
command: Commands,
|
|
#[clap(long, default_value = "/etc/charge-controller-supervisor/config.json")]
|
|
config: PathBuf,
|
|
}
|
|
|
|
#[derive(clap::Subcommand, Debug, Clone)]
|
|
enum Commands {
|
|
/// Run charge controller server
|
|
Watch,
|
|
/// Print the default config file
|
|
GenerateConfig,
|
|
}
|
|
|
|
mod controller;
|
|
mod gauges;
|
|
|
|
mod storage;
|
|
|
|
mod web;
|
|
|
|
pub const CHARGE_CONTROLLER_LABEL: &str = "charge_controller";
|
|
pub const PL_LABEL: &str = "pl_device";
|
|
pub const TRISTAR_LABEL: &str = "tristar_device";
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
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 {
|
|
log::error!("{e:?}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
async fn run() -> eyre::Result<()> {
|
|
let args = Args::parse();
|
|
|
|
match args.command {
|
|
Commands::Watch => watch(args).await,
|
|
Commands::GenerateConfig => {
|
|
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)?;
|
|
println!("{json}");
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn watch(args: Args) -> eyre::Result<()> {
|
|
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 = Vec::new();
|
|
let mut map = std::collections::HashMap::new();
|
|
let mut follow_voltage_tx = Vec::new();
|
|
|
|
for config in &config.charge_controllers {
|
|
let n = config.name.clone();
|
|
match controller::Controller::new(config.clone()).await {
|
|
Ok((v, voltage_tx)) => {
|
|
map.insert(n, v.get_data_ptr());
|
|
controllers.push(v);
|
|
if let Some(voltage_tx) = voltage_tx {
|
|
follow_voltage_tx.push(voltage_tx);
|
|
}
|
|
}
|
|
Err(e) => log::error!("couldn't connect to {}: {e:?}", n),
|
|
}
|
|
}
|
|
|
|
let follow_voltage_tx = controller::MultiTx(follow_voltage_tx);
|
|
|
|
if let Some(primary) = controllers
|
|
.iter_mut()
|
|
.find(|c| c.name() == config.primary_charge_controller)
|
|
{
|
|
primary.set_tx_to_secondary(follow_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),
|
|
follow_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());
|
|
|
|
tokio::select! {
|
|
v = controller_tasks.next() => {
|
|
match v {
|
|
Some(Err(e)) => {
|
|
log::error!("{e:?}");
|
|
}
|
|
_ => {
|
|
log::error!("no controller tasks left???");
|
|
}
|
|
}
|
|
}
|
|
v = server_task => {
|
|
if let Err(e)=v {
|
|
log::error!("server exited: {e:#?}");
|
|
std::process::exit(1);
|
|
} else {
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::process::exit(1)
|
|
}
|
|
|
|
async fn run_loop(mut controller: controller::Controller) -> eyre::Result<()> {
|
|
let mut timeout = tokio::time::interval(controller.timeout_interval());
|
|
|
|
loop {
|
|
if let Some(rx) = controller.get_rx() {
|
|
tokio::select! {
|
|
_ = timeout.tick() => {
|
|
do_refresh(&mut controller).await;
|
|
}
|
|
Some(command) = rx.recv() => {
|
|
if let Err(e) = controller.process_command(command).await {
|
|
log::error!("controller {} failed to process command: {e}", controller.name());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
timeout.tick().await;
|
|
do_refresh(&mut controller).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn do_refresh(controller: &mut controller::Controller) {
|
|
if let Err(e) = controller.refresh().await {
|
|
log::warn!("error reading controller {}: {e:?}", controller.name());
|
|
}
|
|
}
|