From dceefffd45e0efcf94988614094ea73b4b6db8ac Mon Sep 17 00:00:00 2001 From: Alex Janka Date: Thu, 11 Jan 2024 10:28:01 +1100 Subject: [PATCH] track regulator + config changes refresh interval --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/api_interface.rs | 5 +- src/config.rs | 8 +-- src/main.rs | 53 ++++++++++------- src/pl_interface.rs | 135 +++++++++++++++++++++++++++++++++++++++---- src/server/mod.rs | 29 ++++++++-- 7 files changed, 187 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0735435..1786b67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2262,7 +2262,7 @@ dependencies = [ [[package]] name = "tesla-charge-controller" -version = "0.1.14" +version = "0.1.15" dependencies = [ "anyhow", "async-channel", diff --git a/Cargo.toml b/Cargo.toml index eb5ebc8..094a894 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tesla-charge-controller" -version = "0.1.14" +version = "0.1.15" edition = "2021" license = "MITNFA" description = "Controls Tesla charge rate based on solar charge data" diff --git a/src/api_interface.rs b/src/api_interface.rs index cf95982..ce301a3 100644 --- a/src/api_interface.rs +++ b/src/api_interface.rs @@ -142,7 +142,10 @@ impl TeslaInterface { Ok(new_state) => { self.metrics.tesla_online.set(1.); self.last_refresh = Instant::now(); - let mut state = self.state.write().expect("State handler panicked!!"); + let mut state = self + .state + .write() + .expect("Tesla API state handler panicked!!"); if let Some(new_charge_state) = new_state.charge_state { self.metrics diff --git a/src/config.rs b/src/config.rs index d716c9c..1cdfce2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,11 @@ -use std::time::Duration; - use serde::{Deserialize, Serialize}; use crate::types::Coords; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { - pub watch_interval: Duration, + pub tesla_watch_interval: u64, + pub pl_watch_interval: u64, pub coords: Coords, pub serial_port: String, pub baud_rate: u32, @@ -15,7 +14,8 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - watch_interval: Duration::from_secs(60), + tesla_watch_interval: 120, + pl_watch_interval: 30, coords: Coords { latitude: 0., longitude: 0., diff --git a/src/main.rs b/src/main.rs index b5e51ed..bb7d7ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,16 +61,45 @@ async fn main() { // and to the pli thread let (pli_requests, pli_receiver) = async_channel::unbounded(); + // try to spawn the pli loop + let pl_state = match Pli::new(config.serial_port.clone(), config.baud_rate) { + Ok(mut pli) => { + let pl_state = pli.state.clone(); + tokio::task::spawn(async move { + let mut interval = tokio::time::interval( + std::time::Duration::from_secs(config.pl_watch_interval), + ); + loop { + tokio::select! { + _ = interval.tick() => pli.refresh(), + message = pli_receiver.recv() => match message { + Ok(message) => pli.process_request(message), + Err(e) => error!("Error on receive channel: {e:#?}") + } + } + } + }); + Some(pl_state) + } + Err(e) => { + log::error!("Error connecting to serial device for PLI: {e:?}"); + None + } + }; + let server_handle = server::launch_server(server::ServerState { config: config.clone(), - state: interface.state.clone(), + car_state: interface.state.clone(), + pl_state, api_requests, pli_requests, }); // spawn the api loop tokio::task::spawn(async move { - let mut interval = tokio::time::interval(std::time::Duration::from_secs(120)); + let mut interval = tokio::time::interval(std::time::Duration::from_secs( + config.tesla_watch_interval, + )); loop { // await either the next interval OR a message from the other thread tokio::select! { @@ -83,26 +112,6 @@ async fn main() { } }); - // try to spawn the pli loop - match Pli::new(config.serial_port, config.baud_rate) { - Ok(mut pli) => { - tokio::task::spawn(async move { - let mut interval = - tokio::time::interval(std::time::Duration::from_secs(30)); - loop { - tokio::select! { - _ = interval.tick() => pli.refresh(), - message = pli_receiver.recv() => match message { - Ok(message) => pli.process_request(message), - Err(e) => error!("Error on receive channel: {e:#?}") - } - } - } - }); - } - Err(e) => log::error!("Error connecting to serial device for PLI: {e:?}"), - } - server_handle.await; } Err(e) => error!("{}", e.error_string()), diff --git a/src/pl_interface.rs b/src/pl_interface.rs index c58fc4e..4bc54c4 100644 --- a/src/pl_interface.rs +++ b/src/pl_interface.rs @@ -1,16 +1,116 @@ -use std::{io::Write, time::Duration}; +use std::{ + io::Write, + sync::{Arc, RwLock}, + time::Duration, +}; use anyhow::Context; use metrics::{describe_gauge, gauge, Gauge}; +use serde::{Deserialize, Serialize}; use serialport::SerialPort; use termcolor::WriteColor; pub struct Pli { + pub state: Arc>, port: Box, voltage_gauge: Gauge, duty_cycle: Gauge, internal_charge_current: Gauge, internal_load_current: Gauge, + regulator_gauges: RegulatorGauges, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct PlState { + pub battery_voltage: f64, + pub duty_cycle: f64, + pub internal_charge_current: f64, + pub internal_load_current: f64, + pub regulator_state: RegulatorState, +} + +impl Default for PlState { + fn default() -> Self { + Self { + battery_voltage: Default::default(), + duty_cycle: Default::default(), + internal_charge_current: Default::default(), + internal_load_current: Default::default(), + regulator_state: RegulatorState::Absorption, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum RegulatorState { + Boost, + Equalise, + Absorption, + Float, +} + +impl From for RegulatorState { + fn from(value: u8) -> Self { + match value & 0b11 { + 0b00 => Self::Boost, + 0b01 => Self::Equalise, + 0b10 => Self::Absorption, + 0b11 => Self::Float, + _ => unreachable!(), + } + } +} + +struct RegulatorGauges { + boost: Gauge, + equalise: Gauge, + absorption: Gauge, + float: Gauge, +} + +impl RegulatorGauges { + fn new() -> Self { + describe_gauge!("pl_regulator", "Regulator state"); + let boost = gauge!("pl_regulator", "state" => "boost"); + let equalise = gauge!("pl_regulator", "state" => "equalise"); + let absorption = gauge!("pl_regulator", "state" => "absorption"); + let float = gauge!("pl_regulator", "state" => "float"); + Self { + boost, + equalise, + absorption, + float, + } + } + + fn set(&mut self, state: &RegulatorState) { + match state { + RegulatorState::Boost => { + self.boost.set(1.); + self.equalise.set(0.); + self.absorption.set(0.); + self.float.set(0.); + } + RegulatorState::Equalise => { + self.boost.set(0.); + self.equalise.set(1.); + self.absorption.set(0.); + self.float.set(0.); + } + RegulatorState::Absorption => { + self.boost.set(0.); + self.equalise.set(0.); + self.absorption.set(1.); + self.float.set(0.); + } + RegulatorState::Float => { + self.boost.set(0.); + self.equalise.set(0.); + self.absorption.set(0.); + self.float.set(1.); + } + } + } } #[derive(Debug, Clone, Copy)] @@ -34,28 +134,27 @@ impl Pli { let internal_load_current = gauge!("pl_internal_load_current"); Ok(Self { + state: Arc::new(RwLock::new(Default::default())), port, voltage_gauge, duty_cycle, internal_charge_current, internal_load_current, + regulator_gauges: RegulatorGauges::new(), }) } pub fn refresh(&mut self) { - if let Ok(batv) = self.read_ram(PlRamAddress::Batv) { - self.voltage_gauge.set((batv as f64) * (4. / 10.)); - } - if let Ok(duty_cycle) = self.read_ram(PlRamAddress::Dutycyc) { - self.duty_cycle.set((duty_cycle as f64) / 255.); - } - if let Ok(internal_charge_current) = self.read_ram(PlRamAddress::Cint) { + if let Ok(new_state) = self.read_state() { + self.voltage_gauge.set(new_state.battery_voltage); + self.duty_cycle.set(new_state.duty_cycle); self.internal_charge_current - .set((internal_charge_current as f64) * (4. / 10.)); - } - if let Ok(internal_load_current) = self.read_ram(PlRamAddress::Lint) { + .set(new_state.internal_charge_current); self.internal_load_current - .set((internal_load_current as f64) * (4. / 10.)); + .set(new_state.internal_load_current); + self.regulator_gauges.set(&new_state.regulator_state); + + *self.state.write().expect("PLI state handler panicked!!") = new_state; } } @@ -79,6 +178,16 @@ impl Pli { } } + fn read_state(&mut self) -> anyhow::Result { + Ok(PlState { + battery_voltage: (self.read_ram(PlRamAddress::Batv)? as f64) * (4. / 10.), + duty_cycle: (self.read_ram(PlRamAddress::Dutycyc)? as f64) / 255., + internal_charge_current: (self.read_ram(PlRamAddress::Cint)? as f64) * (4. / 10.), + internal_load_current: (self.read_ram(PlRamAddress::Lint)? as f64) * (4. / 10.), + regulator_state: self.read_ram(PlRamAddress::Rstate)?.into(), + }) + } + fn send_command(&mut self, req: [u8; 4]) { self.port .write_all(&req) @@ -108,6 +217,7 @@ impl Pli { enum PlRamAddress { Dutycyc, Batv, + Rstate, Cint, Lint, } @@ -117,6 +227,7 @@ impl From for u8 { match value { PlRamAddress::Dutycyc => 39, PlRamAddress::Batv => 50, + PlRamAddress::Rstate => 101, PlRamAddress::Cint => 213, PlRamAddress::Lint => 217, } diff --git a/src/server/mod.rs b/src/server/mod.rs index 8499854..935a098 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -11,7 +11,7 @@ use rocket::{ use crate::{ api_interface::InterfaceRequest, config::Config, - pl_interface::PliRequest, + pl_interface::{PlState, PliRequest}, types::{CarState, ChargeState, ClimateState}, }; @@ -21,7 +21,8 @@ mod static_handler; pub struct ServerState { pub config: Config, - pub state: Arc>, + pub car_state: Arc>, + pub pl_state: Option>>, pub api_requests: Sender, pub pli_requests: Sender, } @@ -49,24 +50,32 @@ fn rocket(state: ServerState) -> rocket::Rocket { .mount("/", fileserver) .mount( "/", - routes![home, charge_state, flash, climate_state, metrics, read_ram], + routes![ + home, + charge_state, + flash, + climate_state, + metrics, + read_ram, + regulator_state + ], ) } #[get("/home")] async fn home(state: &State) -> Option> { - let location_data = &state.state.read().ok()?.location_data?; + let location_data = &state.car_state.read().ok()?.location_data?; Some(Json(location_data.coords.overlaps(&state.config.coords))) } #[get("/charge-state")] async fn charge_state(state: &State) -> Option> { - Some(Json(state.state.read().ok()?.charge_state?)) + Some(Json(state.car_state.read().ok()?.charge_state?)) } #[get("/climate-state")] async fn climate_state(state: &State) -> Option> { - Some(Json(state.state.read().ok()?.climate_state?)) + Some(Json(state.car_state.read().ok()?.climate_state?)) } #[post("/flash")] @@ -87,6 +96,14 @@ async fn read_ram(address: u8, state: &State) -> String { format!("reading at ram address {address}") } +#[get("/regulator-state")] +async fn regulator_state(state: &State) -> Option> { + state + .pl_state + .as_ref() + .and_then(|v| Some(Json(*(v.read().ok()?)))) +} + pub struct Cors; #[rocket::async_trait]