Compare commits

...

14 commits

Author SHA1 Message Date
7ffdfd1fc3 ccs: log message when server spawned 2025-01-18 12:00:57 +11:00
9d12bce452 tcc: fix clippy lints 2025-01-18 11:58:22 +11:00
b549ceebab update toolchain 2025-01-18 11:52:32 +11:00
b28026820c rename .gitea -> .forgejo 2025-01-18 11:18:11 +11:00
ab3cd28f83 v1.9.9-pre-30: revert always send voltages
All checks were successful
Build and release .deb / Release (push) Successful in 54s
2025-01-16 11:30:22 +11:00
cec26d8cfb v1.9.9-pre-29: always send but don't follow target
All checks were successful
Build and release .deb / Release (push) Successful in 56s
for debugging
2025-01-16 11:21:36 +11:00
186d8fc71a ccs: use tokio::sync::watch for tx to controllers 2025-01-16 11:15:31 +11:00
05edfe6b84 release v1.9.9-pre-28
All checks were successful
Build and release .deb / Release (push) Successful in 56s
2025-01-15 12:11:34 +11:00
c4c99eff1d ccs: set refresh missedtickbehavior 2025-01-15 12:09:57 +11:00
76c33534be ccs: modbus wrapper: consolidate max errors 2025-01-15 12:06:23 +11:00
9bd1243129 ccs: tristar modbus wrapper: actually retry 2025-01-15 12:05:41 +11:00
667f51d375 ccs: reinstate ControllerInner large_enum_variant 2025-01-15 12:04:36 +11:00
9a87b4f820 release v1.9.9-pre-27
All checks were successful
Build and release .deb / Release (push) Successful in 59s
2025-01-14 19:47:25 +11:00
37c7412df1 ccs: tristar: wrap modbus 2025-01-14 19:46:37 +11:00
16 changed files with 297 additions and 166 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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,25 +81,16 @@ 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(); name: config.name,
(Some(a), Some(b)) interval: std::time::Duration::from_secs(config.watch_interval_seconds),
} else { inner,
(None, None) data,
}; voltage_rx,
voltage_tx: None,
Ok(( settings_last_read: None,
Self { follow_voltage: config.follow_primary,
name: config.name, })
interval: std::time::Duration::from_secs(config.watch_interval_seconds),
inner,
data,
voltage_rx,
voltage_tx: None,
settings_last_read: None,
},
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) => {
self.inner.set_target_voltage(target_voltage).await if self.follow_voltage {
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),

View file

@ -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))
} }
} }

View file

@ -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)
}
}

View file

@ -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,22 +178,20 @@ 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() => {
if let Err(e) = controller.process_command(command).await { let command = *rx.borrow();
log::error!("controller {} failed to process command: {e}", controller.name()); 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;
} }
} }
} }

View file

@ -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
}
}) })
} }
} }

View file

@ -1,2 +1,3 @@
[toolchain] [toolchain]
channel = "nightly" channel = "nightly-2025-01-16"
targets = ["aarch64-unknown-linux-musl"]

View file

@ -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!(

View file

@ -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,

View file

@ -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(())

View file

@ -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,

View file

@ -1,3 +1,5 @@
#![allow(clippy::significant_drop_tightening)]
use std::path::PathBuf; use std::path::PathBuf;
use clap::Parser; use clap::Parser;

View file

@ -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 }
} }
} }

View file

@ -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(|| {
data: v.contents().to_vec(), UI_DIR_FILES.get_file(&plus_index).map(|v| RawHtml {
name: plus_index, data: v.contents().to_vec(),
})); name: plus_index,
})
});
file.respond_to(req).or_forward((data, Status::NotFound)) file.respond_to(req).or_forward((data, Status::NotFound))
} }
} }