first pass at pid loop!!!
All checks were successful
Build .deb on release / Build-Deb (push) Successful in 2m9s

This commit is contained in:
Alex Janka 2024-01-22 12:40:29 +11:00
parent 84c999a662
commit dd7711eafd
7 changed files with 168 additions and 25 deletions

2
Cargo.lock generated
View file

@ -2573,7 +2573,7 @@ dependencies = [
[[package]]
name = "tesla-charge-controller"
version = "1.0.15"
version = "1.0.16"
dependencies = [
"async-channel",
"chrono",

View file

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

View file

@ -58,7 +58,6 @@ pub fn write_to_config<'a>() -> ConfigHandle<'a> {
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(default)]
pub struct Config {
pub charge_rate_update_interval_seconds: u64,
pub tesla_update_interval_seconds: u64,
pub tesla_update_interval_while_charging_seconds: u64,
pub pl_watch_interval_seconds: u64,
@ -73,6 +72,24 @@ pub struct Config {
pub duty_cycle_too_high: f64,
pub duty_cycle_too_low: f64,
pub additional_charge_controllers: Vec<ChargeControllerConfig>,
pub pid_controls: PidControls,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
pub struct PidControls {
pub proportional_gain: f64,
pub derivative_gain: f64,
pub loop_time_seconds: u64,
}
impl Default for PidControls {
fn default() -> Self {
Self {
proportional_gain: 1.,
derivative_gain: 1.,
loop_time_seconds: 30,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -93,24 +110,24 @@ pub enum ChargeControllerConfig {
impl Default for Config {
fn default() -> Self {
Self {
charge_rate_update_interval_seconds: 30,
tesla_update_interval_seconds: 120,
tesla_update_interval_while_charging_seconds: 5,
pl_watch_interval_seconds: 30,
pl_timeout_milliseconds: 250,
pl_watch_interval_seconds: 5,
pl_timeout_milliseconds: 400,
coords: Coords {
latitude: 0.,
longitude: 0.,
},
serial_port: String::from("/dev/ttyUSB0"),
baud_rate: 9600,
shutoff_voltage: 52.,
shutoff_voltage: 50.,
shutoff_voltage_time_seconds: 15,
min_rate: 5,
max_rate: 10,
duty_cycle_too_high: 0.9,
duty_cycle_too_low: 0.7,
duty_cycle_too_high: 0.8,
duty_cycle_too_low: 0.1,
additional_charge_controllers: Vec::new(),
pid_controls: Default::default(),
}
}
}

View file

@ -177,7 +177,7 @@ async fn main() {
));
let mut charge_rate_update_interval =
tokio::time::interval(std::time::Duration::from_secs(
access_config().charge_rate_update_interval_seconds,
access_config().pid_controls.loop_time_seconds,
));
let mut was_connected = false;

View file

@ -64,6 +64,10 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
enable_control,
set_max,
set_min,
set_proportional_gain,
set_derivative_gain,
set_pid_loop_length,
pid_settings,
metrics,
sync_time,
read_ram,
@ -154,6 +158,29 @@ async fn set_min(limit: i64) -> String {
format!("set lower limit to {limit}")
}
#[post("/pid-settings/proportional/<gain>")]
async fn set_proportional_gain(gain: f64) -> String {
write_to_config().pid_controls.proportional_gain = gain;
format!("set proportional gain to {gain}")
}
#[post("/pid-settings/derivative/<gain>")]
async fn set_derivative_gain(gain: f64) -> String {
write_to_config().pid_controls.derivative_gain = gain;
format!("set derivative gain to {gain}")
}
#[post("/pid-settings/loop_time_seconds/<time>")]
async fn set_pid_loop_length(time: u64) -> String {
write_to_config().pid_controls.loop_time_seconds = time;
format!("set pid loop length to {time} seconds")
}
#[get("/pid-settings/status")]
async fn pid_settings() -> Json<crate::config::PidControls> {
Json(access_config().pid_controls)
}
#[get("/metrics")]
fn metrics() -> Result<String, ServerError> {
Ok(

View file

@ -15,6 +15,7 @@ pub struct TeslaChargeRateController {
pub car_state: Arc<RwLock<CarState>>,
pub pl_state: Option<Arc<RwLock<PlState>>>,
pub tcrc_state: Arc<RwLock<TcrcState>>,
pid: PidLoop,
voltage_low: u64,
control_enable_gauge: Gauge,
}
@ -44,19 +45,21 @@ impl TeslaChargeRateController {
car_state,
pl_state,
tcrc_state: Default::default(),
pid: Default::default(),
voltage_low: 0,
control_enable_gauge: gauge!("tcrc_control_enable"),
}
}
pub fn control_charge_rate(&mut self) -> Option<InterfaceRequest> {
let delta_time = access_config().pid_controls.loop_time_seconds;
if_chain! {
if let Some(pl_state) = self.pl_state.as_ref().and_then(|v| v.read().ok());
if let Some(charge_state) = self.car_state.read().ok().and_then(|v| v.charge_state);
then {
if pl_state.battery_voltage < access_config().shutoff_voltage {
self.voltage_low += 1;
if (self.voltage_low * access_config().charge_rate_update_interval_seconds)
if (self.voltage_low * delta_time)
>= access_config().shutoff_voltage_time_seconds
{
return Some(InterfaceRequest::StopCharge);
@ -64,7 +67,7 @@ impl TeslaChargeRateController {
} else {
self.voltage_low = 0;
if self.tcrc_state.read().is_ok_and(|v| v.control_enable) {
return get_control(&pl_state, &charge_state);
return self.pid.step(&pl_state, &charge_state, delta_time as f64).map(InterfaceRequest::SetChargeRate);
}
}
}
@ -92,20 +95,36 @@ impl TeslaChargeRateController {
}
}
fn get_control(pl_state: &PlState, charge_state: &ChargeState) -> Option<InterfaceRequest> {
let config = access_config();
if pl_state.duty_cycle > config.duty_cycle_too_high {
let rate = charge_state.charge_amps - 2;
return valid_rate(rate, charge_state.charge_amps).map(InterfaceRequest::SetChargeRate);
} else if pl_state.duty_cycle < config.duty_cycle_too_low {
let rate = charge_state.charge_amps + 1;
return valid_rate(rate, charge_state.charge_amps).map(InterfaceRequest::SetChargeRate);
}
#[derive(Default)]
struct PidLoop {
previous_error: f64,
}
// if max/min charge rate has changed, then the current charge rate might no longer be valid
// and we should move up/down to keep it in the new bounds
valid_rate(charge_state.charge_amps, charge_state.charge_amps)
.map(InterfaceRequest::SetChargeRate)
impl PidLoop {
fn step(
&mut self,
pl_state: &PlState,
charge_state: &ChargeState,
delta_time: f64,
) -> Option<i64> {
let error = pl_state.target_voltage - pl_state.battery_voltage;
let derivative = (error - self.previous_error) / delta_time;
let config = access_config();
let extra_offsets = if pl_state.internal_load_current > 1. {
1.
} else {
0.
};
let new_target = ((config.pid_controls.proportional_gain * error)
+ (config.pid_controls.derivative_gain * derivative))
+ (charge_state.charge_amps as f64)
+ extra_offsets;
self.previous_error = error;
let new_target_int = new_target.round() as i64;
valid_rate(new_target_int, charge_state.charge_amps)
}
}
fn valid_rate(rate: i64, previous: i64) -> Option<i64> {

View file

@ -86,11 +86,13 @@
refresh_interval = register();
refresh_gains();
refresh();
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
clearInterval(refresh_interval);
} else {
refresh_gains();
refresh();
refresh_interval = register();
}
@ -152,6 +154,48 @@
set_button.disabled = (number_input.value == current_max_rate);
}
function set_proportional() {
var set_button = document.getElementById("set-proportional");
var number_input = document.getElementById("proportional-gain");
if (!isNaN(number_input.value)) {
set_button.disabled = true;
number_input.disabled = true;
fetch(api_url + "/pid-settings/proportional/" + number_input.value, { method: "POST" })
.then(async (response) => {
let delayres = await delay(100);
refresh_gains();
});
}
}
function set_derivative() {
var set_button = document.getElementById("set-derivative");
var number_input = document.getElementById("derivative-gain");
if (!isNaN(number_input.value)) {
set_button.disabled = true;
number_input.disabled = true;
fetch(api_url + "/pid-settings/derivative/" + number_input.value, { method: "POST" })
.then(async (response) => {
let delayres = await delay(100);
refresh_gains();
});
}
}
function set_pid_length() {
var set_button = document.getElementById("set-pid-length");
var number_input = document.getElementById("pid-length");
if (!isNaN(number_input.value)) {
set_button.disabled = true;
number_input.disabled = true;
fetch(api_url + "/pid-settings/loop_time_seconds/" + number_input.value, { method: "POST" })
.then(async (response) => {
let delayres = await delay(100);
refresh_gains();
});
}
}
function disable_automatic_control() {
if (is_automatic_control) {
document.getElementById('control-disabled').checked = true;
@ -218,6 +262,30 @@
}
}
function refresh_gains() {
fetch(api_url + "/pid-settings/status")
.then((response) => response.json())
.then((json) => update_gains(json));
}
function update_gains(data) {
var proportional_set_button = document.getElementById("set-proportional");
var proportional_number_input = document.getElementById("proportional-gain");
proportional_set_button.disabled = false;
proportional_number_input.disabled = false;
proportional_number_input.value = data.proportional_gain;
var derivative_set_button = document.getElementById("set-derivative");
var derivative_number_input = document.getElementById("derivative-gain");
derivative_set_button.disabled = false;
derivative_number_input.disabled = false;
derivative_number_input.value = data.derivative_gain;
var pid_length_button = document.getElementById("set-pid-length");
var pid_length_input = document.getElementById("pid-length");
pid_length_button.disabled = false;
pid_length_input.disabled = false;
pid_length_input.value = data.loop_time_seconds;
}
function refresh_buttons() {
fetch(api_url + "/control-state")
.then((response) => response.json())
@ -230,6 +298,7 @@
favicon.setAttribute("href", "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>" + "⏳" + "</text></svg>");
refresh_buttons();
fetch(api_url + "/car-state")
.then((response) => response.json())
.then((json) => update_state(json));
@ -282,6 +351,17 @@
<input type="number" id="min-rate" max="15" min="3" onchange="change_min()" autocomplete="off" />
<button id="set-minimum" onclick="set_minimum()" disabled>Set minimum</button>
</div>
<div id="pid-control">
<h3>PID:</h3>
<input type="number" id="proportional-gain" step="0.1" autocomplete="off" />
<button id="set-proportional" onclick="set_proportional()">Set proportional gain</button>
<br><br>
<input type="number" id="derivative-gain" step="0.1" autocomplete="off" />
<button id="set-derivative" onclick="set_derivative()">Set derivative gain</button>
<br><br>
<input type="number" id="pid-length" step="1" autocomplete="off" />
<button id="set-pid-length" onclick="set_pid_length()">Set pid length</button>
</div>
<br>
<button onclick="flash()">flash</button>
<div id="info"></div>