first pass at pid loop!!!
All checks were successful
Build .deb on release / Build-Deb (push) Successful in 2m9s
All checks were successful
Build .deb on release / Build-Deb (push) Successful in 2m9s
This commit is contained in:
parent
84c999a662
commit
dd7711eafd
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2573,7 +2573,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tesla-charge-controller"
|
||||
version = "1.0.15"
|
||||
version = "1.0.16"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"chrono",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue