Compare commits
14 commits
v1.9.9-pre
...
main
Author | SHA1 | Date | |
---|---|---|---|
7ffdfd1fc3 | |||
9d12bce452 | |||
b549ceebab | |||
b28026820c | |||
ab3cd28f83 | |||
cec26d8cfb | |||
186d8fc71a | |||
05edfe6b84 | |||
c4c99eff1d | |||
76c33534be | |||
9bd1243129 | |||
667f51d375 | |||
9a87b4f820 | |||
37c7412df1 |
16 changed files with 297 additions and 166 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -239,7 +239,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charge-controller-supervisor"
|
name = "charge-controller-supervisor"
|
||||||
version = "1.9.9-pre-26"
|
version = "1.9.9-pre-30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.7.0",
|
"bitflags 2.7.0",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -2205,7 +2205,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tesla-charge-controller"
|
name = "tesla-charge-controller"
|
||||||
version = "1.9.9-pre-26"
|
version = "1.9.9-pre-30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
|
@ -4,7 +4,7 @@ default-members = ["charge-controller-supervisor"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.9.9-pre-26"
|
version = "1.9.9-pre-30"
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
pedantic = "warn"
|
pedantic = "warn"
|
||||||
|
|
|
@ -8,8 +8,9 @@ pub struct Controller {
|
||||||
interval: std::time::Duration,
|
interval: std::time::Duration,
|
||||||
inner: ControllerInner,
|
inner: ControllerInner,
|
||||||
data: std::sync::Arc<ControllerData>,
|
data: std::sync::Arc<ControllerData>,
|
||||||
voltage_rx: Option<tokio::sync::mpsc::UnboundedReceiver<VoltageCommand>>,
|
follow_voltage: bool,
|
||||||
voltage_tx: Option<MultiTx>,
|
voltage_rx: tokio::sync::watch::Receiver<VoltageCommand>,
|
||||||
|
voltage_tx: Option<tokio::sync::watch::Sender<VoltageCommand>>,
|
||||||
settings_last_read: Option<std::time::Instant>,
|
settings_last_read: Option<std::time::Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,16 +54,15 @@ pub enum ControllerSettings {
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum VoltageCommand {
|
pub enum VoltageCommand {
|
||||||
|
None,
|
||||||
Set(f64),
|
Set(f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Controller {
|
impl Controller {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
config: crate::config::ChargeControllerConfig,
|
config: crate::config::ChargeControllerConfig,
|
||||||
) -> eyre::Result<(
|
voltage_rx: tokio::sync::watch::Receiver<VoltageCommand>,
|
||||||
Self,
|
) -> 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(
|
||||||
tristar::Tristar::new(&config.name, &config.transport).await?,
|
tristar::Tristar::new(&config.name, &config.transport).await?,
|
||||||
|
@ -81,15 +81,7 @@ impl Controller {
|
||||||
|
|
||||||
let data = std::sync::Arc::new(ControllerData::new());
|
let data = std::sync::Arc::new(ControllerData::new());
|
||||||
|
|
||||||
let (voltage_tx, voltage_rx) = if config.follow_primary {
|
Ok(Self {
|
||||||
let (a, b) = tokio::sync::mpsc::unbounded_channel();
|
|
||||||
(Some(a), Some(b))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
Self {
|
|
||||||
name: config.name,
|
name: config.name,
|
||||||
interval: std::time::Duration::from_secs(config.watch_interval_seconds),
|
interval: std::time::Duration::from_secs(config.watch_interval_seconds),
|
||||||
inner,
|
inner,
|
||||||
|
@ -97,9 +89,8 @@ impl Controller {
|
||||||
voltage_rx,
|
voltage_rx,
|
||||||
voltage_tx: None,
|
voltage_tx: None,
|
||||||
settings_last_read: None,
|
settings_last_read: None,
|
||||||
},
|
follow_voltage: config.follow_primary,
|
||||||
voltage_tx,
|
})
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_data_ptr(&self) -> std::sync::Arc<ControllerData> {
|
pub fn get_data_ptr(&self) -> std::sync::Arc<ControllerData> {
|
||||||
|
@ -121,7 +112,7 @@ impl Controller {
|
||||||
target
|
target
|
||||||
);
|
);
|
||||||
|
|
||||||
tx.send_to_all(VoltageCommand::Set(target));
|
tx.send(VoltageCommand::Set(target))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,12 +137,12 @@ impl Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
pub fn name(&self) -> &str {
|
||||||
&self.name
|
self.name.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_tx_to_secondary(&mut self, tx: MultiTx) {
|
pub fn set_tx_to_secondary(&mut self, tx: tokio::sync::watch::Sender<VoltageCommand>) {
|
||||||
assert!(
|
assert!(
|
||||||
self.voltage_rx.is_none(),
|
!self.follow_voltage,
|
||||||
"trying to set {} as primary when it is also a secondary!",
|
"trying to set {} as primary when it is also a secondary!",
|
||||||
self.name
|
self.name
|
||||||
);
|
);
|
||||||
|
@ -159,14 +150,22 @@ impl Controller {
|
||||||
self.voltage_tx = Some(tx);
|
self.voltage_tx = Some(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rx(&mut self) -> Option<&mut tokio::sync::mpsc::UnboundedReceiver<VoltageCommand>> {
|
pub fn get_rx(&mut self) -> &mut tokio::sync::watch::Receiver<VoltageCommand> {
|
||||||
self.voltage_rx.as_mut()
|
&mut self.voltage_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn process_command(&mut self, command: VoltageCommand) -> eyre::Result<()> {
|
pub async fn process_command(&mut self, command: VoltageCommand) -> eyre::Result<()> {
|
||||||
match command {
|
match command {
|
||||||
VoltageCommand::Set(target_voltage) => {
|
VoltageCommand::Set(target_voltage) => {
|
||||||
|
if self.follow_voltage {
|
||||||
self.inner.set_target_voltage(target_voltage).await
|
self.inner.set_target_voltage(target_voltage).await
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VoltageCommand::None => {
|
||||||
|
// todo: disable voltage control
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,19 +177,7 @@ impl Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[expect(clippy::large_enum_variant)]
|
||||||
pub struct MultiTx(pub Vec<tokio::sync::mpsc::UnboundedSender<VoltageCommand>>);
|
|
||||||
|
|
||||||
impl MultiTx {
|
|
||||||
pub fn send_to_all(&self, command: VoltageCommand) {
|
|
||||||
for sender in &self.0 {
|
|
||||||
if let Err(e) = sender.send(command) {
|
|
||||||
log::error!("failed to send command {command:?}: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ControllerInner {
|
pub enum ControllerInner {
|
||||||
Pl(pl::Pli),
|
Pl(pl::Pli),
|
||||||
Tristar(tristar::Tristar),
|
Tristar(tristar::Tristar),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
use modbus_wrapper::ModbusTimeout;
|
||||||
use prometheus::core::{AtomicI64, GenericGauge};
|
use prometheus::core::{AtomicI64, GenericGauge};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio_modbus::client::{Reader, Writer};
|
|
||||||
|
|
||||||
use crate::gauges::{
|
use crate::gauges::{
|
||||||
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, HEATSINK_TEMP, INPUT_CURRENT, TARGET_VOLTAGE,
|
BATTERY_TEMP, BATTERY_VOLTAGE, CHARGE_STATE, HEATSINK_TEMP, INPUT_CURRENT, TARGET_VOLTAGE,
|
||||||
|
@ -54,57 +54,10 @@ pub struct Tristar {
|
||||||
charge_state_gauges: ChargeStateGauges,
|
charge_state_gauges: ChargeStateGauges,
|
||||||
consecutive_errors: usize,
|
consecutive_errors: usize,
|
||||||
scaling: Scaling,
|
scaling: Scaling,
|
||||||
transport_settings: crate::config::Transport,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ModbusTimeout {
|
mod modbus_wrapper;
|
||||||
context: tokio_modbus::client::Context,
|
|
||||||
reconnect_required: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODBUS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
|
||||||
|
|
||||||
impl ModbusTimeout {
|
|
||||||
const fn new(context: tokio_modbus::client::Context) -> Self {
|
|
||||||
Self {
|
|
||||||
context,
|
|
||||||
reconnect_required: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn write_single_register(
|
|
||||||
&mut self,
|
|
||||||
addr: tokio_modbus::Address,
|
|
||||||
word: u16,
|
|
||||||
) -> eyre::Result<()> {
|
|
||||||
let r = tokio::time::timeout(
|
|
||||||
MODBUS_TIMEOUT,
|
|
||||||
self.context.write_single_register(addr, word),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if let Err(tokio_modbus::Error::Transport(_)) = &r {
|
|
||||||
self.reconnect_required = true;
|
|
||||||
}
|
|
||||||
r??;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_holding_registers(
|
|
||||||
&mut self,
|
|
||||||
addr: tokio_modbus::Address,
|
|
||||||
cnt: tokio_modbus::Quantity,
|
|
||||||
) -> eyre::Result<Vec<u16>> {
|
|
||||||
let r = tokio::time::timeout(
|
|
||||||
MODBUS_TIMEOUT,
|
|
||||||
self.context.read_holding_registers(addr, cnt),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if let Err(tokio_modbus::Error::Transport(_)) = &r {
|
|
||||||
self.reconnect_required = true;
|
|
||||||
}
|
|
||||||
Ok(r??)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TristarSettings {
|
pub struct TristarSettings {
|
||||||
network: Option<NetworkSettings>,
|
network: Option<NetworkSettings>,
|
||||||
|
@ -780,32 +733,14 @@ impl ChargeStateGauges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect_modbus(transport: &crate::config::Transport) -> eyre::Result<ModbusTimeout> {
|
|
||||||
let slave = tokio_modbus::Slave(DEVICE_ID);
|
|
||||||
|
|
||||||
let modbus = match transport {
|
|
||||||
crate::config::Transport::Serial { port, baud_rate } => {
|
|
||||||
let modbus_serial =
|
|
||||||
tokio_serial::SerialStream::open(&tokio_serial::new(port, *baud_rate))?;
|
|
||||||
tokio_modbus::client::rtu::attach_slave(modbus_serial, slave)
|
|
||||||
}
|
|
||||||
crate::config::Transport::Tcp { ip, port } => {
|
|
||||||
let modbus_tcp = tokio::net::TcpStream::connect((*ip, *port)).await?;
|
|
||||||
tokio_modbus::client::tcp::attach_slave(modbus_tcp, slave)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ModbusTimeout::new(modbus))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tristar {
|
impl Tristar {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
friendly_name: &str,
|
friendly_name: &str,
|
||||||
transport: &crate::config::Transport,
|
transport: &crate::config::Transport,
|
||||||
) -> eyre::Result<Self> {
|
) -> eyre::Result<Self> {
|
||||||
let mut modbus = connect_modbus(transport).await?;
|
let mut modbus = ModbusTimeout::new(transport.clone()).await?;
|
||||||
let scaling = {
|
let scaling = {
|
||||||
let data = modbus.read_holding_registers(0x0000, 4).await?;
|
let data = modbus.read_holding_registers(0x0000, 4).await??;
|
||||||
Scaling::from(&data)
|
Scaling::from(&data)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -817,14 +752,10 @@ impl Tristar {
|
||||||
charge_state_gauges,
|
charge_state_gauges,
|
||||||
consecutive_errors: 0,
|
consecutive_errors: 0,
|
||||||
scaling,
|
scaling,
|
||||||
transport_settings: transport.clone(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh(&mut self) -> eyre::Result<TristarState> {
|
pub async fn refresh(&mut self) -> eyre::Result<TristarState> {
|
||||||
if self.modbus.reconnect_required {
|
|
||||||
self.modbus = connect_modbus(&self.transport_settings).await?;
|
|
||||||
}
|
|
||||||
let new_state = self.get_data().await?;
|
let new_state = self.get_data().await?;
|
||||||
|
|
||||||
self.scaling = new_state.scaling;
|
self.scaling = new_state.scaling;
|
||||||
|
@ -881,7 +812,7 @@ impl Tristar {
|
||||||
let network = if let Ok(network_data) = self
|
let network = if let Ok(network_data) = self
|
||||||
.modbus
|
.modbus
|
||||||
.read_holding_registers(NETWORK_DATA_ADDR_START, NETWORK_DATA_LENGTH)
|
.read_holding_registers(NETWORK_DATA_ADDR_START, NETWORK_DATA_LENGTH)
|
||||||
.await
|
.await?
|
||||||
{
|
{
|
||||||
Some(NetworkSettings::from_buf(&network_data)?)
|
Some(NetworkSettings::from_buf(&network_data)?)
|
||||||
} else {
|
} else {
|
||||||
|
@ -891,12 +822,12 @@ impl Tristar {
|
||||||
let charge_data_1 = self
|
let charge_data_1 = self
|
||||||
.modbus
|
.modbus
|
||||||
.read_holding_registers(CHARGE_DATA_ADDR_START, 0x22)
|
.read_holding_registers(CHARGE_DATA_ADDR_START, 0x22)
|
||||||
.await?;
|
.await??;
|
||||||
|
|
||||||
let charge_data_2 = self
|
let charge_data_2 = self
|
||||||
.modbus
|
.modbus
|
||||||
.read_holding_registers(CHARGE_DATA_ADDR_START + 0x80, 0x4e)
|
.read_holding_registers(CHARGE_DATA_ADDR_START + 0x80, 0x4e)
|
||||||
.await?;
|
.await??;
|
||||||
|
|
||||||
let mut charge_data = vec![0; 0xCE];
|
let mut charge_data = vec![0; 0xCE];
|
||||||
charge_data[..0x22].copy_from_slice(&charge_data_1);
|
charge_data[..0x22].copy_from_slice(&charge_data_1);
|
||||||
|
@ -911,7 +842,7 @@ impl Tristar {
|
||||||
let scaled_voltage: u16 = self.scale_voltage(target_voltage);
|
let scaled_voltage: u16 = self.scale_voltage(target_voltage);
|
||||||
self.modbus
|
self.modbus
|
||||||
.write_single_register(TristarRamAddress::VbRefSlave as u16, scaled_voltage)
|
.write_single_register(TristarRamAddress::VbRefSlave as u16, scaled_voltage)
|
||||||
.await?;
|
.await??;
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"tristar {} being set to voltage {target_voltage} (scaled: {scaled_voltage:#X?})",
|
"tristar {} being set to voltage {target_voltage} (scaled: {scaled_voltage:#X?})",
|
||||||
|
@ -929,7 +860,7 @@ impl Tristar {
|
||||||
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??;
|
||||||
Ok(TristarState::from_ram(&data))
|
Ok(TristarState::from_ram(&data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
pub struct ModbusTimeout {
|
||||||
|
context: Option<tokio_modbus::client::Context>,
|
||||||
|
transport_settings: crate::config::Transport,
|
||||||
|
counters: Counters,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODBUS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||||
|
|
||||||
|
async fn connect(
|
||||||
|
transport_settings: &crate::config::Transport,
|
||||||
|
) -> eyre::Result<tokio_modbus::client::Context> {
|
||||||
|
let slave = tokio_modbus::Slave(super::DEVICE_ID);
|
||||||
|
|
||||||
|
let modbus = match transport_settings {
|
||||||
|
crate::config::Transport::Serial { port, baud_rate } => {
|
||||||
|
let modbus_serial =
|
||||||
|
tokio_serial::SerialStream::open(&tokio_serial::new(port, *baud_rate))?;
|
||||||
|
tokio_modbus::client::rtu::attach_slave(modbus_serial, slave)
|
||||||
|
}
|
||||||
|
crate::config::Transport::Tcp { ip, port } => {
|
||||||
|
let modbus_tcp = tokio::net::TcpStream::connect((*ip, *port)).await?;
|
||||||
|
tokio_modbus::client::tcp::attach_slave(modbus_tcp, slave)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(modbus)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModbusDeviceResult<T> = Result<T, tokio_modbus::ExceptionCode>;
|
||||||
|
type ModbusResult<T> = Result<ModbusDeviceResult<T>, tokio_modbus::Error>;
|
||||||
|
|
||||||
|
type ContextFuture<'a, R> = dyn std::future::Future<Output = ModbusResult<R>> + Send + 'a;
|
||||||
|
type ContextFn<R, D> =
|
||||||
|
fn(&mut tokio_modbus::client::Context, D) -> std::pin::Pin<Box<ContextFuture<'_, R>>>;
|
||||||
|
|
||||||
|
trait TryInsert {
|
||||||
|
type T;
|
||||||
|
async fn get_or_try_insert_with<F, R, Fut>(&mut self, f: F) -> Result<&mut Self::T, R>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = Result<Self::T, R>>,
|
||||||
|
F: FnOnce() -> Fut;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TryInsert for Option<T> {
|
||||||
|
type T = T;
|
||||||
|
|
||||||
|
async fn get_or_try_insert_with<F, R, Fut>(&mut self, f: F) -> Result<&mut Self::T, R>
|
||||||
|
where
|
||||||
|
Fut: std::future::Future<Output = Result<Self::T, R>>,
|
||||||
|
F: FnOnce() -> Fut,
|
||||||
|
{
|
||||||
|
if self.is_none() {
|
||||||
|
let got = f().await?;
|
||||||
|
*self = Some(got);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a `None` variant for `self` would have been replaced by a `Some` variant
|
||||||
|
// in the code above, or the ? would have caused an early return
|
||||||
|
Ok(self.as_mut().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct Counters {
|
||||||
|
gateway: usize,
|
||||||
|
timeout: usize,
|
||||||
|
protocol: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_ERRORS: usize = 2;
|
||||||
|
|
||||||
|
impl Counters {
|
||||||
|
fn reset(&mut self) {
|
||||||
|
*self = Self::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn any_above_max(&self) -> bool {
|
||||||
|
self.gateway > MAX_ERRORS || self.timeout > MAX_ERRORS || self.protocol > MAX_ERRORS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NUM_TRIES: usize = 3;
|
||||||
|
|
||||||
|
impl ModbusTimeout {
|
||||||
|
pub async fn new(transport_settings: crate::config::Transport) -> eyre::Result<Self> {
|
||||||
|
let context = Some(connect(&transport_settings).await?);
|
||||||
|
Ok(Self {
|
||||||
|
context,
|
||||||
|
transport_settings,
|
||||||
|
counters: Counters::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn with_context<R, D: Copy>(
|
||||||
|
&mut self,
|
||||||
|
f: ContextFn<R, D>,
|
||||||
|
data: D,
|
||||||
|
) -> eyre::Result<Result<R, tokio_modbus::ExceptionCode>> {
|
||||||
|
let mut last_err = None;
|
||||||
|
for _ in 0..NUM_TRIES {
|
||||||
|
if let Ok(context) = self
|
||||||
|
.context
|
||||||
|
.get_or_try_insert_with(async || connect(&self.transport_settings).await)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let res = tokio::time::timeout(MODBUS_TIMEOUT, f(context, data)).await;
|
||||||
|
match res {
|
||||||
|
Ok(Ok(Err(e)))
|
||||||
|
if e == tokio_modbus::ExceptionCode::GatewayTargetDevice
|
||||||
|
|| e == tokio_modbus::ExceptionCode::GatewayPathUnavailable =>
|
||||||
|
{
|
||||||
|
log::warn!("gateway error: {e:?}");
|
||||||
|
last_err = Some(e.into());
|
||||||
|
self.counters.gateway += 1;
|
||||||
|
}
|
||||||
|
Ok(Ok(v)) => {
|
||||||
|
self.counters.reset();
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
Ok(Err(tokio_modbus::Error::Protocol(e))) => {
|
||||||
|
// protocol error
|
||||||
|
log::warn!("protocol error: {e:?}");
|
||||||
|
last_err = Some(e.into());
|
||||||
|
self.counters.protocol += 1;
|
||||||
|
}
|
||||||
|
Ok(Err(tokio_modbus::Error::Transport(e))) => {
|
||||||
|
// transport error
|
||||||
|
log::warn!("reconnecting due to transport error: {e:?}");
|
||||||
|
last_err = Some(e.into());
|
||||||
|
self.context = None;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// timeout
|
||||||
|
last_err = Some(eyre::eyre!("timeout"));
|
||||||
|
self.counters.timeout += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.counters.any_above_max() {
|
||||||
|
self.context = None;
|
||||||
|
log::warn!(
|
||||||
|
"reconnecting due to multiple errors without a successful operation: {:?}",
|
||||||
|
self.counters
|
||||||
|
);
|
||||||
|
self.counters.reset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// failed to reconnect
|
||||||
|
return Err(eyre::eyre!("failed to reconnect to controller"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_err.unwrap_or_else(|| eyre::eyre!("unknown last error????")))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_single_register(
|
||||||
|
&mut self,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
word: u16,
|
||||||
|
) -> eyre::Result<ModbusDeviceResult<()>> {
|
||||||
|
async fn write(
|
||||||
|
context: &mut tokio_modbus::client::Context,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
word: u16,
|
||||||
|
) -> ModbusResult<()> {
|
||||||
|
use tokio_modbus::client::Writer;
|
||||||
|
context.write_single_register(addr, word).await
|
||||||
|
}
|
||||||
|
|
||||||
|
let fut: ContextFn<(), _> = |context, (addr, word)| Box::pin(write(context, addr, word));
|
||||||
|
let r = self.with_context(fut, (addr, word)).await?;
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_holding_registers(
|
||||||
|
&mut self,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
cnt: tokio_modbus::Quantity,
|
||||||
|
) -> eyre::Result<ModbusDeviceResult<Vec<u16>>> {
|
||||||
|
async fn read(
|
||||||
|
context: &mut tokio_modbus::client::Context,
|
||||||
|
addr: tokio_modbus::Address,
|
||||||
|
cnt: tokio_modbus::Quantity,
|
||||||
|
) -> ModbusResult<Vec<u16>> {
|
||||||
|
use tokio_modbus::client::Reader;
|
||||||
|
context.read_holding_registers(addr, cnt).await
|
||||||
|
}
|
||||||
|
|
||||||
|
let fut: ContextFn<_, _> = |context, (addr, cnt)| Box::pin(read(context, addr, cnt));
|
||||||
|
let res = self.with_context(fut, (addr, cnt)).await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,29 +108,26 @@ async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
|
|
||||||
let mut controllers = Vec::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();
|
|
||||||
|
let (voltage_tx, voltage_rx) =
|
||||||
|
tokio::sync::watch::channel(controller::VoltageCommand::None);
|
||||||
|
|
||||||
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.clone()).await {
|
match controller::Controller::new(config.clone(), voltage_rx.clone()).await {
|
||||||
Ok((v, voltage_tx)) => {
|
Ok(v) => {
|
||||||
map.insert(n, v.get_data_ptr());
|
map.insert(n, v.get_data_ptr());
|
||||||
controllers.push(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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let follow_voltage_tx = controller::MultiTx(follow_voltage_tx);
|
|
||||||
|
|
||||||
if let Some(primary) = controllers
|
if let Some(primary) = controllers
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|c| c.name() == config.primary_charge_controller)
|
.find(|c| c.name() == config.primary_charge_controller)
|
||||||
{
|
{
|
||||||
primary.set_tx_to_secondary(follow_voltage_tx.clone());
|
primary.set_tx_to_secondary(voltage_tx.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(config);
|
drop(config);
|
||||||
|
@ -142,7 +139,7 @@ async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
|
|
||||||
(
|
(
|
||||||
storage::AllControllers::new(map),
|
storage::AllControllers::new(map),
|
||||||
follow_voltage_tx,
|
voltage_tx,
|
||||||
controller_tasks,
|
controller_tasks,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -153,6 +150,7 @@ async fn watch(args: Args) -> eyre::Result<()> {
|
||||||
follow_voltage_tx,
|
follow_voltage_tx,
|
||||||
));
|
));
|
||||||
let server_task = tokio::task::spawn(server.launch());
|
let server_task = tokio::task::spawn(server.launch());
|
||||||
|
log::warn!("...started!");
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
v = controller_tasks.next() => {
|
v = controller_tasks.next() => {
|
||||||
|
@ -180,23 +178,21 @@ 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());
|
||||||
|
timeout.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(rx) = controller.get_rx() {
|
let rx = controller.get_rx();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = timeout.tick() => {
|
_ = timeout.tick() => {
|
||||||
do_refresh(&mut controller).await;
|
do_refresh(&mut controller).await;
|
||||||
}
|
}
|
||||||
Some(command) = rx.recv() => {
|
Ok(()) = rx.changed() => {
|
||||||
|
let command = *rx.borrow();
|
||||||
if let Err(e) = controller.process_command(command).await {
|
if let Err(e) = controller.process_command(command).await {
|
||||||
log::error!("controller {} failed to process command: {e}", controller.name());
|
log::error!("controller {} failed to process command: {e}", controller.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
timeout.tick().await;
|
|
||||||
do_refresh(&mut controller).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,14 @@ mod static_handler;
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
primary_name: String,
|
primary_name: String,
|
||||||
data: AllControllers,
|
data: AllControllers,
|
||||||
tx_to_controllers: crate::controller::MultiTx,
|
tx_to_controllers: tokio::sync::watch::Sender<crate::controller::VoltageCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
primary_name: &impl ToString,
|
primary_name: &impl ToString,
|
||||||
data: AllControllers,
|
data: AllControllers,
|
||||||
tx_to_controllers: crate::controller::MultiTx,
|
tx_to_controllers: tokio::sync::watch::Sender<crate::controller::VoltageCommand>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let primary_name = primary_name.to_string();
|
let primary_name = primary_name.to_string();
|
||||||
Self {
|
Self {
|
||||||
|
@ -200,20 +200,23 @@ async fn enable_control() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/control/disable")]
|
#[post("/control/disable")]
|
||||||
async fn disable_control(state: &State<ServerState>) {
|
async fn disable_control(state: &State<ServerState>) -> Result<(), ServerError> {
|
||||||
log::warn!("disabling control");
|
log::warn!("disabling control");
|
||||||
crate::config::write_to_config()
|
crate::config::write_to_config()
|
||||||
.await
|
.await
|
||||||
.enable_secondary_control = false;
|
.enable_secondary_control = false;
|
||||||
state
|
state
|
||||||
.tx_to_controllers
|
.tx_to_controllers
|
||||||
.send_to_all(crate::controller::VoltageCommand::Set(-1.0));
|
.send(crate::controller::VoltageCommand::None)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ServerError {
|
enum ServerError {
|
||||||
Prometheus,
|
Prometheus,
|
||||||
NotFound,
|
NotFound,
|
||||||
InvalidPrimaryName,
|
InvalidPrimaryName,
|
||||||
NoData,
|
NoData,
|
||||||
|
ControllerTx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<prometheus::Error> for ServerError {
|
impl From<prometheus::Error> for ServerError {
|
||||||
|
@ -222,12 +225,20 @@ impl From<prometheus::Error> for ServerError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> From<tokio::sync::watch::error::SendError<T>> for ServerError {
|
||||||
|
fn from(_: tokio::sync::watch::error::SendError<T>) -> Self {
|
||||||
|
Self::ControllerTx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> rocket::response::Responder<'a, 'a> for ServerError {
|
impl<'a> rocket::response::Responder<'a, 'a> for ServerError {
|
||||||
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
|
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
|
||||||
Err(match self {
|
Err(match self {
|
||||||
Self::NotFound => rocket::http::Status::NotFound,
|
Self::NotFound => rocket::http::Status::NotFound,
|
||||||
Self::InvalidPrimaryName => rocket::http::Status::ServiceUnavailable,
|
Self::InvalidPrimaryName => rocket::http::Status::ServiceUnavailable,
|
||||||
Self::NoData | Self::Prometheus => rocket::http::Status::InternalServerError,
|
Self::ControllerTx | Self::NoData | Self::Prometheus => {
|
||||||
|
rocket::http::Status::InternalServerError
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "nightly-2025-01-16"
|
||||||
|
targets = ["aarch64-unknown-linux-musl"]
|
||||||
|
|
|
@ -95,6 +95,7 @@ impl Vehicle {
|
||||||
Ok(state.charge_state)
|
Ok(state.charge_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code, reason = "active charge control not yet implemented")]
|
||||||
pub async fn set_charging_amps(&self, charging_amps: i64) -> eyre::Result<()> {
|
pub async fn set_charging_amps(&self, charging_amps: i64) -> eyre::Result<()> {
|
||||||
self.client
|
self.client
|
||||||
.post(format!(
|
.post(format!(
|
||||||
|
|
|
@ -42,11 +42,11 @@ impl Car {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn vehicle(&self) -> &http::Vehicle {
|
pub const fn vehicle(&self) -> &http::Vehicle {
|
||||||
&self.vehicle
|
&self.vehicle
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(&self) -> &tokio::sync::RwLock<CarState> {
|
pub const fn state(&self) -> &tokio::sync::RwLock<CarState> {
|
||||||
&self.state
|
&self.state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,7 @@ impl ChargeState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum ChargingState {
|
pub enum ChargingState {
|
||||||
Charging,
|
Charging,
|
||||||
Stopped,
|
Stopped,
|
||||||
|
|
|
@ -60,7 +60,7 @@ impl ConfigWatcher {
|
||||||
async fn overwrite_config(config: Config) -> eyre::Result<()> {
|
async fn overwrite_config(config: Config) -> eyre::Result<()> {
|
||||||
*CONFIG
|
*CONFIG
|
||||||
.get()
|
.get()
|
||||||
.ok_or(eyre::eyre!("could not get config"))?
|
.ok_or_else(|| eyre::eyre!("could not get config"))?
|
||||||
.write()
|
.write()
|
||||||
.await = config;
|
.await = config;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub struct VehicleController {
|
||||||
control_state: ChargeRateControllerState,
|
control_state: ChargeRateControllerState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code, reason = "not all states are currently in use")]
|
||||||
pub enum ChargeRateControllerState {
|
pub enum ChargeRateControllerState {
|
||||||
Inactive,
|
Inactive,
|
||||||
Charging { rate_amps: i64 },
|
Charging { rate_amps: i64 },
|
||||||
|
@ -14,7 +15,7 @@ pub enum InterfaceRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VehicleController {
|
impl VehicleController {
|
||||||
pub fn new(
|
pub const fn new(
|
||||||
car: std::sync::Arc<crate::api::Car>,
|
car: std::sync::Arc<crate::api::Car>,
|
||||||
requests: tokio::sync::mpsc::UnboundedReceiver<InterfaceRequest>,
|
requests: tokio::sync::mpsc::UnboundedReceiver<InterfaceRequest>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -50,7 +51,10 @@ impl VehicleController {
|
||||||
}
|
}
|
||||||
match self.control_state {
|
match self.control_state {
|
||||||
ChargeRateControllerState::Inactive => {
|
ChargeRateControllerState::Inactive => {
|
||||||
if let Some(state) = self.car.state().read().await.charge_state().await {
|
let car_state = self.car.state().read().await;
|
||||||
|
let state = car_state.charge_state().await;
|
||||||
|
|
||||||
|
if let Some(state) = state {
|
||||||
if state.is_charging() {
|
if state.is_charging() {
|
||||||
self.control_state = ChargeRateControllerState::Charging {
|
self.control_state = ChargeRateControllerState::Charging {
|
||||||
rate_amps: state.charge_amps,
|
rate_amps: state.charge_amps,
|
||||||
|
@ -58,10 +62,14 @@ impl VehicleController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChargeRateControllerState::Charging { rate_amps } => todo!(),
|
ChargeRateControllerState::Charging { rate_amps: _ } => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(
|
||||||
|
clippy::needless_pass_by_ref_mut,
|
||||||
|
reason = "this will eventually need to mutate self"
|
||||||
|
)]
|
||||||
pub async fn process_requests(&mut self, req: InterfaceRequest) {
|
pub async fn process_requests(&mut self, req: InterfaceRequest) {
|
||||||
if let Err(e) = match req {
|
if let Err(e) = match req {
|
||||||
InterfaceRequest::FlashLights => self.car.vehicle().flash_lights().await,
|
InterfaceRequest::FlashLights => self.car.vehicle().flash_lights().await,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#![allow(clippy::significant_drop_tightening)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
|
@ -27,7 +27,7 @@ pub struct ServerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
pub fn new(car: Arc<Car>, api_requests: UnboundedSender<InterfaceRequest>) -> Self {
|
pub const fn new(car: Arc<Car>, api_requests: UnboundedSender<InterfaceRequest>) -> Self {
|
||||||
Self { car, api_requests }
|
Self { car, api_requests }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,10 +52,12 @@ impl Handler for UiStatic {
|
||||||
data: v.contents().to_vec(),
|
data: v.contents().to_vec(),
|
||||||
name: p,
|
name: p,
|
||||||
})
|
})
|
||||||
.or(UI_DIR_FILES.get_file(&plus_index).map(|v| RawHtml {
|
.or_else(|| {
|
||||||
|
UI_DIR_FILES.get_file(&plus_index).map(|v| RawHtml {
|
||||||
data: v.contents().to_vec(),
|
data: v.contents().to_vec(),
|
||||||
name: plus_index,
|
name: plus_index,
|
||||||
}));
|
})
|
||||||
|
});
|
||||||
file.respond_to(req).or_forward((data, Status::NotFound))
|
file.respond_to(req).or_forward((data, Status::NotFound))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue