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]]
name = "tesla-charge-controller"
version = "0.1.14"
version = "0.1.15"
dependencies = [
"anyhow",
"async-channel",

View file

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

View file

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

View file

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

View file

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

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 metrics::{describe_gauge, gauge, Gauge};
use serde::{Deserialize, Serialize};
use serialport::SerialPort;
use termcolor::WriteColor;
pub struct Pli {
pub state: Arc<RwLock<PlState>>,
port: Box<dyn SerialPort>,
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<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)]
@ -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<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]) {
self.port
.write_all(&req)
@ -108,6 +217,7 @@ impl Pli {
enum PlRamAddress {
Dutycyc,
Batv,
Rstate,
Cint,
Lint,
}
@ -117,6 +227,7 @@ impl From<PlRamAddress> for u8 {
match value {
PlRamAddress::Dutycyc => 39,
PlRamAddress::Batv => 50,
PlRamAddress::Rstate => 101,
PlRamAddress::Cint => 213,
PlRamAddress::Lint => 217,
}

View file

@ -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<RwLock<CarState>>,
pub car_state: Arc<RwLock<CarState>>,
pub pl_state: Option<Arc<RwLock<PlState>>>,
pub api_requests: Sender<InterfaceRequest>,
pub pli_requests: Sender<PliRequest>,
}
@ -49,24 +50,32 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
.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<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)))
}
#[get("/charge-state")]
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")]
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")]
@ -87,6 +96,14 @@ async fn read_ram(address: u8, state: &State<ServerState>) -> String {
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;
#[rocket::async_trait]