track regulator + config changes refresh interval

This commit is contained in:
Alex Janka 2024-01-11 10:28:01 +11:00
parent 162d32756c
commit dceefffd45
7 changed files with 187 additions and 47 deletions

2
Cargo.lock generated
View file

@ -2262,7 +2262,7 @@ dependencies = [
[[package]] [[package]]
name = "tesla-charge-controller" name = "tesla-charge-controller"
version = "0.1.14" version = "0.1.15"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel", "async-channel",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tesla-charge-controller" name = "tesla-charge-controller"
version = "0.1.14" version = "0.1.15"
edition = "2021" edition = "2021"
license = "MITNFA" license = "MITNFA"
description = "Controls Tesla charge rate based on solar charge data" description = "Controls Tesla charge rate based on solar charge data"

View file

@ -142,7 +142,10 @@ impl TeslaInterface {
Ok(new_state) => { Ok(new_state) => {
self.metrics.tesla_online.set(1.); self.metrics.tesla_online.set(1.);
self.last_refresh = Instant::now(); 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 { if let Some(new_charge_state) = new_state.charge_state {
self.metrics self.metrics

View file

@ -1,12 +1,11 @@
use std::time::Duration;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::Coords; use crate::types::Coords;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
pub watch_interval: Duration, pub tesla_watch_interval: u64,
pub pl_watch_interval: u64,
pub coords: Coords, pub coords: Coords,
pub serial_port: String, pub serial_port: String,
pub baud_rate: u32, pub baud_rate: u32,
@ -15,7 +14,8 @@ pub struct Config {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
watch_interval: Duration::from_secs(60), tesla_watch_interval: 120,
pl_watch_interval: 30,
coords: Coords { coords: Coords {
latitude: 0., latitude: 0.,
longitude: 0., longitude: 0.,

View file

@ -61,16 +61,45 @@ async fn main() {
// and to the pli thread // and to the pli thread
let (pli_requests, pli_receiver) = async_channel::unbounded(); 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 { let server_handle = server::launch_server(server::ServerState {
config: config.clone(), config: config.clone(),
state: interface.state.clone(), car_state: interface.state.clone(),
pl_state,
api_requests, api_requests,
pli_requests, pli_requests,
}); });
// spawn the api loop // spawn the api loop
tokio::task::spawn(async move { 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 { loop {
// await either the next interval OR a message from the other thread // await either the next interval OR a message from the other thread
tokio::select! { 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; server_handle.await;
} }
Err(e) => error!("{}", e.error_string()), Err(e) => error!("{}", e.error_string()),

View file

@ -1,16 +1,116 @@
use std::{io::Write, time::Duration}; use std::{
io::Write,
sync::{Arc, RwLock},
time::Duration,
};
use anyhow::Context; use anyhow::Context;
use metrics::{describe_gauge, gauge, Gauge}; use metrics::{describe_gauge, gauge, Gauge};
use serde::{Deserialize, Serialize};
use serialport::SerialPort; use serialport::SerialPort;
use termcolor::WriteColor; use termcolor::WriteColor;
pub struct Pli { pub struct Pli {
pub state: Arc<RwLock<PlState>>,
port: Box<dyn SerialPort>, port: Box<dyn SerialPort>,
voltage_gauge: Gauge, voltage_gauge: Gauge,
duty_cycle: Gauge, duty_cycle: Gauge,
internal_charge_current: Gauge, internal_charge_current: Gauge,
internal_load_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<u8> 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)] #[derive(Debug, Clone, Copy)]
@ -34,28 +134,27 @@ impl Pli {
let internal_load_current = gauge!("pl_internal_load_current"); let internal_load_current = gauge!("pl_internal_load_current");
Ok(Self { Ok(Self {
state: Arc::new(RwLock::new(Default::default())),
port, port,
voltage_gauge, voltage_gauge,
duty_cycle, duty_cycle,
internal_charge_current, internal_charge_current,
internal_load_current, internal_load_current,
regulator_gauges: RegulatorGauges::new(),
}) })
} }
pub fn refresh(&mut self) { pub fn refresh(&mut self) {
if let Ok(batv) = self.read_ram(PlRamAddress::Batv) { if let Ok(new_state) = self.read_state() {
self.voltage_gauge.set((batv as f64) * (4. / 10.)); self.voltage_gauge.set(new_state.battery_voltage);
} self.duty_cycle.set(new_state.duty_cycle);
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) {
self.internal_charge_current self.internal_charge_current
.set((internal_charge_current as f64) * (4. / 10.)); .set(new_state.internal_charge_current);
}
if let Ok(internal_load_current) = self.read_ram(PlRamAddress::Lint) {
self.internal_load_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<PlState> {
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]) { fn send_command(&mut self, req: [u8; 4]) {
self.port self.port
.write_all(&req) .write_all(&req)
@ -108,6 +217,7 @@ impl Pli {
enum PlRamAddress { enum PlRamAddress {
Dutycyc, Dutycyc,
Batv, Batv,
Rstate,
Cint, Cint,
Lint, Lint,
} }
@ -117,6 +227,7 @@ impl From<PlRamAddress> for u8 {
match value { match value {
PlRamAddress::Dutycyc => 39, PlRamAddress::Dutycyc => 39,
PlRamAddress::Batv => 50, PlRamAddress::Batv => 50,
PlRamAddress::Rstate => 101,
PlRamAddress::Cint => 213, PlRamAddress::Cint => 213,
PlRamAddress::Lint => 217, PlRamAddress::Lint => 217,
} }

View file

@ -11,7 +11,7 @@ use rocket::{
use crate::{ use crate::{
api_interface::InterfaceRequest, api_interface::InterfaceRequest,
config::Config, config::Config,
pl_interface::PliRequest, pl_interface::{PlState, PliRequest},
types::{CarState, ChargeState, ClimateState}, types::{CarState, ChargeState, ClimateState},
}; };
@ -21,7 +21,8 @@ mod static_handler;
pub struct ServerState { pub struct ServerState {
pub config: Config, pub config: Config,
pub state: Arc<RwLock<CarState>>, pub car_state: Arc<RwLock<CarState>>,
pub pl_state: Option<Arc<RwLock<PlState>>>,
pub api_requests: Sender<InterfaceRequest>, pub api_requests: Sender<InterfaceRequest>,
pub pli_requests: Sender<PliRequest>, pub pli_requests: Sender<PliRequest>,
} }
@ -49,24 +50,32 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
.mount("/", fileserver) .mount("/", fileserver)
.mount( .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")] #[get("/home")]
async fn home(state: &State<ServerState>) -> Option<Json<bool>> { async fn home(state: &State<ServerState>) -> Option<Json<bool>> {
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))) Some(Json(location_data.coords.overlaps(&state.config.coords)))
} }
#[get("/charge-state")] #[get("/charge-state")]
async fn charge_state(state: &State<ServerState>) -> Option<Json<ChargeState>> { async fn charge_state(state: &State<ServerState>) -> Option<Json<ChargeState>> {
Some(Json(state.state.read().ok()?.charge_state?)) Some(Json(state.car_state.read().ok()?.charge_state?))
} }
#[get("/climate-state")] #[get("/climate-state")]
async fn climate_state(state: &State<ServerState>) -> Option<Json<ClimateState>> { async fn climate_state(state: &State<ServerState>) -> Option<Json<ClimateState>> {
Some(Json(state.state.read().ok()?.climate_state?)) Some(Json(state.car_state.read().ok()?.climate_state?))
} }
#[post("/flash")] #[post("/flash")]
@ -87,6 +96,14 @@ async fn read_ram(address: u8, state: &State<ServerState>) -> String {
format!("reading at ram address {address}") format!("reading at ram address {address}")
} }
#[get("/regulator-state")]
async fn regulator_state(state: &State<ServerState>) -> Option<Json<PlState>> {
state
.pl_state
.as_ref()
.and_then(|v| Some(Json(*(v.read().ok()?))))
}
pub struct Cors; pub struct Cors;
#[rocket::async_trait] #[rocket::async_trait]