diff --git a/charge-controller-supervisor/src/config.rs b/charge-controller-supervisor/src/config.rs index e46812b..ad202bd 100644 --- a/charge-controller-supervisor/src/config.rs +++ b/charge-controller-supervisor/src/config.rs @@ -14,6 +14,8 @@ pub struct ChargeControllerConfig { pub baud_rate: u32, pub watch_interval_seconds: u64, pub variant: ChargeControllerVariant, + #[serde(default)] + pub follow_primary: bool, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] diff --git a/charge-controller-supervisor/src/controller.rs b/charge-controller-supervisor/src/controller.rs index 6932d71..557cf0c 100644 --- a/charge-controller-supervisor/src/controller.rs +++ b/charge-controller-supervisor/src/controller.rs @@ -3,6 +3,8 @@ pub struct Controller { interval: std::time::Duration, inner: ControllerInner, data: std::sync::Arc>, + voltage_rx: Option>, + voltage_tx: Option, } #[derive(Default, serde::Serialize, Clone)] @@ -12,8 +14,18 @@ pub struct CommonData { pub battery_temp: f64, } +#[derive(Clone, Copy, Debug)] +pub enum VoltageCommand { + Set(f64), +} + impl Controller { - pub fn new(config: crate::config::ChargeControllerConfig) -> eyre::Result { + pub fn new( + config: crate::config::ChargeControllerConfig, + ) -> eyre::Result<( + Self, + Option>, + )> { let inner = match config.variant { crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar( crate::tristar::Tristar::new(&config.serial_port, &config.name, config.baud_rate)?, @@ -32,12 +44,24 @@ impl Controller { 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, - }) + let (voltage_tx, voltage_rx) = if config.follow_primary { + let (a, b) = tokio::sync::mpsc::unbounded_channel(); + (Some(a), Some(b)) + } else { + (None, None) + }; + + Ok(( + Self { + name: config.name, + interval: std::time::Duration::from_secs(config.watch_interval_seconds), + inner, + data, + voltage_rx, + voltage_tx: None, + }, + voltage_tx, + )) } pub fn get_data_ptr(&self) -> std::sync::Arc> { @@ -46,6 +70,18 @@ impl Controller { pub async fn refresh(&mut self) -> eyre::Result<()> { let data = self.inner.refresh().await?; + + if let Some(tx) = self.voltage_tx.as_mut() { + if *crate::IS_PRINTING_TARGETS { + log::error!( + "tristar {}: primary: sending target voltage {}", + self.name, + data.target_voltage + ); + } + tx.send_to_all(VoltageCommand::Set(data.target_voltage)); + } + *self.data.write().await = data; Ok(()) @@ -58,6 +94,44 @@ impl Controller { pub fn name(&self) -> &str { &self.name } + + pub fn set_tx_to_secondary( + &mut self, + tx: Vec>, + ) { + if self.voltage_rx.is_some() { + panic!( + "trying to set {} as primary when it is also a secondary!", + self.name + ); + } + + self.voltage_tx = Some(MultiTx(tx)) + } + + pub fn get_rx(&mut self) -> Option<&mut tokio::sync::mpsc::UnboundedReceiver> { + self.voltage_rx.as_mut() + } + + pub async fn process_command(&mut self, command: VoltageCommand) -> eyre::Result<()> { + match command { + VoltageCommand::Set(target_voltage) => { + self.inner.set_target_voltage(target_voltage).await + } + } + } +} + +struct MultiTx(Vec>); + +impl MultiTx { + fn send_to_all(&mut self, command: VoltageCommand) { + for sender in self.0.iter_mut() { + if let Err(e) = sender.send(command) { + log::error!("failed to send command {command:?}: {e:?}"); + } + } + } } pub enum ControllerInner { @@ -78,4 +152,11 @@ impl ControllerInner { } } } + + 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, + } + } } diff --git a/charge-controller-supervisor/src/main.rs b/charge-controller-supervisor/src/main.rs index c0a1588..53c75c9 100644 --- a/charge-controller-supervisor/src/main.rs +++ b/charge-controller-supervisor/src/main.rs @@ -65,28 +65,57 @@ async fn run() -> eyre::Result<()> { } } +static IS_PRINTING_TARGETS: std::sync::LazyLock = + std::sync::LazyLock::new(|| std::env::var("SECONDARIES_PRINT_TARGET").is_ok()); + async fn watch(args: Args) -> eyre::Result<()> { let config: config::Config = serde_json::from_reader(std::fs::File::open(args.config)?)?; + 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 = futures::stream::FuturesUnordered::new(); + 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) { - Ok(v) => { + Ok((v, voltage_tx)) => { map.insert(n, v.get_data_ptr()); - controllers.push(run_loop(v)); + 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), } } + if let Some(primary) = controllers + .iter_mut() + .find(|c| c.name() == config.primary_charge_controller) + { + primary.set_tx_to_secondary(follow_voltage_tx); + } + + let mut controller_tasks = futures::stream::FuturesUnordered::new(); + for controller in controllers { + controller_tasks.push(run_loop(controller)); + } + let server = web::rocket(web::ServerState::new(config.primary_charge_controller, map)); let server_task = tokio::task::spawn(server.launch()); tokio::select! { - v = controllers.next() => { + v = controller_tasks.next() => { match v { Some(Err(e)) => { log::error!("{e:?}"); @@ -110,10 +139,28 @@ async fn watch(args: Args) -> eyre::Result<()> { 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 { - log::warn!("error reading controller {}: {e:?}", controller.name()); + 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()); + } +} diff --git a/charge-controller-supervisor/src/tristar.rs b/charge-controller-supervisor/src/tristar.rs index 7f76a89..44cffd8 100644 --- a/charge-controller-supervisor/src/tristar.rs +++ b/charge-controller-supervisor/src/tristar.rs @@ -40,6 +40,10 @@ impl Scaling { fn get_power(&self, data: u16) -> f64 { f64::from(data) * self.v_scale * self.i_scale * f64::powf(2., -17.) } + + fn from_voltage(&self, voltage: f64) -> u16 { + (voltage / (self.v_scale * f64::powf(2., -15.))) as u16 + } } pub struct Tristar { @@ -47,10 +51,12 @@ pub struct Tristar { modbus: tokio_modbus::client::Context, charge_state_gauges: ChargeStateGauges, consecutive_errors: usize, + scaling: Option, } #[derive(Default, Debug, Clone, Copy)] pub struct TristarState { + scaling: Option, battery_voltage: f64, target_voltage: f64, input_current: f64, @@ -79,6 +85,7 @@ impl TristarState { fn from_ram(ram: &[u16]) -> Self { let scaling = Scaling::from(ram); Self { + scaling: Some(scaling), battery_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVbFMed]), target_voltage: scaling.get_voltage(ram[TristarRamAddress::VbRef]), input_current: scaling.get_current(ram[TristarRamAddress::AdcIaFShadow]), @@ -258,11 +265,14 @@ impl Tristar { modbus, charge_state_gauges, consecutive_errors: 0, + scaling: None, }) } pub async fn refresh(&mut self) -> eyre::Result { let new_state = self.get_data().await?; + + self.scaling = new_state.scaling; self.consecutive_errors = 0; BATTERY_VOLTAGE .with_label_values(&[&self.friendly_name]) @@ -316,11 +326,39 @@ impl Tristar { }) } + pub async fn set_target_voltage(&mut self, target_voltage: f64) -> eyre::Result<()> { + let scaled_voltage: u16 = self.scale_voltage(target_voltage)?; + // self.modbus + // .write_single_register(TristarRamAddress::VbRefSlave as u16, scaled_voltage) + // .await??; + if *crate::IS_PRINTING_TARGETS { + log::error!( + "tristar {} being set to voltage {target_voltage} (scaled: {scaled_voltage:#X?})", + self.friendly_name + ); + } + Ok(()) + } + + fn scale_voltage(&self, voltage: f64) -> eyre::Result { + let Some(scaling) = &self.scaling else { + return Err(eyre::eyre!("no scaling data present")); + }; + Ok(scaling.from_voltage(voltage)) + } + async fn get_data(&mut self) -> eyre::Result { let data = self .modbus .read_holding_registers(0x0000, RAM_DATA_SIZE + 1) .await??; + if *crate::IS_PRINTING_TARGETS { + log::error!( + "tristar {}: target voltage raw {:#X?}", + self.friendly_name, + data[TristarRamAddress::VbRef as usize] + ) + } Ok(TristarState::from_ram(&data)) } }