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 baud_rate: u32,
|
||||||
pub watch_interval_seconds: u64,
|
pub watch_interval_seconds: u64,
|
||||||
pub variant: ChargeControllerVariant,
|
pub variant: ChargeControllerVariant,
|
||||||
|
#[serde(default)]
|
||||||
|
pub follow_primary: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
|
|
@ -3,6 +3,8 @@ pub struct Controller {
|
||||||
interval: std::time::Duration,
|
interval: std::time::Duration,
|
||||||
inner: ControllerInner,
|
inner: ControllerInner,
|
||||||
data: std::sync::Arc<tokio::sync::RwLock<CommonData>>,
|
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)]
|
#[derive(Default, serde::Serialize, Clone)]
|
||||||
|
@ -12,8 +14,18 @@ pub struct CommonData {
|
||||||
pub battery_temp: f64,
|
pub battery_temp: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum VoltageCommand {
|
||||||
|
Set(f64),
|
||||||
|
}
|
||||||
|
|
||||||
impl Controller {
|
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 {
|
let inner = match config.variant {
|
||||||
crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar(
|
crate::config::ChargeControllerVariant::Tristar => ControllerInner::Tristar(
|
||||||
crate::tristar::Tristar::new(&config.serial_port, &config.name, config.baud_rate)?,
|
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));
|
let data = std::sync::Arc::new(tokio::sync::RwLock::new(data));
|
||||||
|
|
||||||
Ok(Self {
|
let (voltage_tx, voltage_rx) = if config.follow_primary {
|
||||||
name: config.name,
|
let (a, b) = tokio::sync::mpsc::unbounded_channel();
|
||||||
interval: std::time::Duration::from_secs(config.watch_interval_seconds),
|
(Some(a), Some(b))
|
||||||
inner,
|
} else {
|
||||||
data,
|
(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>> {
|
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<()> {
|
pub async fn refresh(&mut self) -> eyre::Result<()> {
|
||||||
let data = self.inner.refresh().await?;
|
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;
|
*self.data.write().await = data;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -58,6 +94,44 @@ impl Controller {
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
&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 {
|
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<()> {
|
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)?)?;
|
||||||
|
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 map = std::collections::HashMap::new();
|
||||||
|
let mut follow_voltage_tx = Vec::new();
|
||||||
|
|
||||||
for config in config.charge_controllers {
|
for config in config.charge_controllers {
|
||||||
let n = config.name.clone();
|
let n = config.name.clone();
|
||||||
match controller::Controller::new(config) {
|
match controller::Controller::new(config) {
|
||||||
Ok(v) => {
|
Ok((v, voltage_tx)) => {
|
||||||
map.insert(n, v.get_data_ptr());
|
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),
|
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 = web::rocket(web::ServerState::new(config.primary_charge_controller, map));
|
||||||
let server_task = tokio::task::spawn(server.launch());
|
let server_task = tokio::task::spawn(server.launch());
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
v = controllers.next() => {
|
v = controller_tasks.next() => {
|
||||||
match v {
|
match v {
|
||||||
Some(Err(e)) => {
|
Some(Err(e)) => {
|
||||||
log::error!("{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<()> {
|
async fn run_loop(mut controller: controller::Controller) -> eyre::Result<()> {
|
||||||
let mut timeout = tokio::time::interval(controller.timeout_interval());
|
let mut timeout = tokio::time::interval(controller.timeout_interval());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
timeout.tick().await;
|
if let Some(rx) = controller.get_rx() {
|
||||||
if let Err(e) = controller.refresh().await {
|
tokio::select! {
|
||||||
log::warn!("error reading controller {}: {e:?}", controller.name());
|
_ = 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,10 @@ impl Scaling {
|
||||||
fn get_power(&self, data: u16) -> f64 {
|
fn get_power(&self, data: u16) -> f64 {
|
||||||
f64::from(data) * self.v_scale * self.i_scale * f64::powf(2., -17.)
|
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 {
|
pub struct Tristar {
|
||||||
|
@ -47,10 +51,12 @@ pub struct Tristar {
|
||||||
modbus: tokio_modbus::client::Context,
|
modbus: tokio_modbus::client::Context,
|
||||||
charge_state_gauges: ChargeStateGauges,
|
charge_state_gauges: ChargeStateGauges,
|
||||||
consecutive_errors: usize,
|
consecutive_errors: usize,
|
||||||
|
scaling: Option<Scaling>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy)]
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
pub struct TristarState {
|
pub struct TristarState {
|
||||||
|
scaling: Option<Scaling>,
|
||||||
battery_voltage: f64,
|
battery_voltage: f64,
|
||||||
target_voltage: f64,
|
target_voltage: f64,
|
||||||
input_current: f64,
|
input_current: f64,
|
||||||
|
@ -79,6 +85,7 @@ impl TristarState {
|
||||||
fn from_ram(ram: &[u16]) -> Self {
|
fn from_ram(ram: &[u16]) -> Self {
|
||||||
let scaling = Scaling::from(ram);
|
let scaling = Scaling::from(ram);
|
||||||
Self {
|
Self {
|
||||||
|
scaling: Some(scaling),
|
||||||
battery_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVbFMed]),
|
battery_voltage: scaling.get_voltage(ram[TristarRamAddress::AdcVbFMed]),
|
||||||
target_voltage: scaling.get_voltage(ram[TristarRamAddress::VbRef]),
|
target_voltage: scaling.get_voltage(ram[TristarRamAddress::VbRef]),
|
||||||
input_current: scaling.get_current(ram[TristarRamAddress::AdcIaFShadow]),
|
input_current: scaling.get_current(ram[TristarRamAddress::AdcIaFShadow]),
|
||||||
|
@ -258,11 +265,14 @@ impl Tristar {
|
||||||
modbus,
|
modbus,
|
||||||
charge_state_gauges,
|
charge_state_gauges,
|
||||||
consecutive_errors: 0,
|
consecutive_errors: 0,
|
||||||
|
scaling: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(&mut self) -> eyre::Result<crate::controller::CommonData> {
|
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.scaling = new_state.scaling;
|
||||||
self.consecutive_errors = 0;
|
self.consecutive_errors = 0;
|
||||||
BATTERY_VOLTAGE
|
BATTERY_VOLTAGE
|
||||||
.with_label_values(&[&self.friendly_name])
|
.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> {
|
async fn get_data(&mut self) -> eyre::Result<TristarState> {
|
||||||
let data = self
|
let data = self
|
||||||
.modbus
|
.modbus
|
||||||
.read_holding_registers(0x0000, RAM_DATA_SIZE + 1)
|
.read_holding_registers(0x0000, RAM_DATA_SIZE + 1)
|
||||||
.await??;
|
.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))
|
Ok(TristarState::from_ram(&data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue