ccs: follow primary
so far only printing so that we dont cook it
This commit is contained in:
parent
59994c75c1
commit
11503615de
4 changed files with 182 additions and 14 deletions
|
@ -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)]
|
||||
|
|
|
@ -3,6 +3,8 @@ pub struct Controller {
|
|||
interval: std::time::Duration,
|
||||
inner: ControllerInner,
|
||||
data: std::sync::Arc<tokio::sync::RwLock<CommonData>>,
|
||||
voltage_rx: Option<tokio::sync::mpsc::UnboundedReceiver<VoltageCommand>>,
|
||||
voltage_tx: Option<MultiTx>,
|
||||
}
|
||||
|
||||
#[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<Self> {
|
||||
pub fn new(
|
||||
config: crate::config::ChargeControllerConfig,
|
||||
) -> eyre::Result<(
|
||||
Self,
|
||||
Option<tokio::sync::mpsc::UnboundedSender<VoltageCommand>>,
|
||||
)> {
|
||||
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 {
|
||||
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<tokio::sync::RwLock<CommonData>> {
|
||||
|
@ -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<tokio::sync::mpsc::UnboundedSender<VoltageCommand>>,
|
||||
) {
|
||||
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<VoltageCommand>> {
|
||||
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<tokio::sync::mpsc::UnboundedSender<VoltageCommand>>);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,28 +65,57 @@ async fn run() -> eyre::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
static IS_PRINTING_TARGETS: std::sync::LazyLock<bool> =
|
||||
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 {
|
||||
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;
|
||||
if let Err(e) = controller.refresh().await {
|
||||
log::warn!("error reading controller {}: {e:?}", controller.name());
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Scaling>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct TristarState {
|
||||
scaling: Option<Scaling>,
|
||||
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<crate::controller::CommonData> {
|
||||
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<u16> {
|
||||
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<TristarState> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue