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]]
|
[[package]]
|
||||||
name = "tesla-charge-controller"
|
name = "tesla-charge-controller"
|
||||||
version = "1.0.15"
|
version = "1.0.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tesla-charge-controller"
|
name = "tesla-charge-controller"
|
||||||
version = "1.0.15"
|
version = "1.0.16"
|
||||||
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"
|
||||||
|
|
|
@ -58,7 +58,6 @@ pub fn write_to_config<'a>() -> ConfigHandle<'a> {
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub charge_rate_update_interval_seconds: u64,
|
|
||||||
pub tesla_update_interval_seconds: u64,
|
pub tesla_update_interval_seconds: u64,
|
||||||
pub tesla_update_interval_while_charging_seconds: u64,
|
pub tesla_update_interval_while_charging_seconds: u64,
|
||||||
pub pl_watch_interval_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_high: f64,
|
||||||
pub duty_cycle_too_low: f64,
|
pub duty_cycle_too_low: f64,
|
||||||
pub additional_charge_controllers: Vec<ChargeControllerConfig>,
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
@ -93,24 +110,24 @@ pub enum ChargeControllerConfig {
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
charge_rate_update_interval_seconds: 30,
|
|
||||||
tesla_update_interval_seconds: 120,
|
tesla_update_interval_seconds: 120,
|
||||||
tesla_update_interval_while_charging_seconds: 5,
|
tesla_update_interval_while_charging_seconds: 5,
|
||||||
pl_watch_interval_seconds: 30,
|
pl_watch_interval_seconds: 5,
|
||||||
pl_timeout_milliseconds: 250,
|
pl_timeout_milliseconds: 400,
|
||||||
coords: Coords {
|
coords: Coords {
|
||||||
latitude: 0.,
|
latitude: 0.,
|
||||||
longitude: 0.,
|
longitude: 0.,
|
||||||
},
|
},
|
||||||
serial_port: String::from("/dev/ttyUSB0"),
|
serial_port: String::from("/dev/ttyUSB0"),
|
||||||
baud_rate: 9600,
|
baud_rate: 9600,
|
||||||
shutoff_voltage: 52.,
|
shutoff_voltage: 50.,
|
||||||
shutoff_voltage_time_seconds: 15,
|
shutoff_voltage_time_seconds: 15,
|
||||||
min_rate: 5,
|
min_rate: 5,
|
||||||
max_rate: 10,
|
max_rate: 10,
|
||||||
duty_cycle_too_high: 0.9,
|
duty_cycle_too_high: 0.8,
|
||||||
duty_cycle_too_low: 0.7,
|
duty_cycle_too_low: 0.1,
|
||||||
additional_charge_controllers: Vec::new(),
|
additional_charge_controllers: Vec::new(),
|
||||||
|
pid_controls: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ async fn main() {
|
||||||
));
|
));
|
||||||
let mut charge_rate_update_interval =
|
let mut charge_rate_update_interval =
|
||||||
tokio::time::interval(std::time::Duration::from_secs(
|
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;
|
let mut was_connected = false;
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,10 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
||||||
enable_control,
|
enable_control,
|
||||||
set_max,
|
set_max,
|
||||||
set_min,
|
set_min,
|
||||||
|
set_proportional_gain,
|
||||||
|
set_derivative_gain,
|
||||||
|
set_pid_loop_length,
|
||||||
|
pid_settings,
|
||||||
metrics,
|
metrics,
|
||||||
sync_time,
|
sync_time,
|
||||||
read_ram,
|
read_ram,
|
||||||
|
@ -154,6 +158,29 @@ async fn set_min(limit: i64) -> String {
|
||||||
format!("set lower limit to {limit}")
|
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")]
|
#[get("/metrics")]
|
||||||
fn metrics() -> Result<String, ServerError> {
|
fn metrics() -> Result<String, ServerError> {
|
||||||
Ok(
|
Ok(
|
||||||
|
|
|
@ -15,6 +15,7 @@ pub struct TeslaChargeRateController {
|
||||||
pub car_state: Arc<RwLock<CarState>>,
|
pub car_state: Arc<RwLock<CarState>>,
|
||||||
pub pl_state: Option<Arc<RwLock<PlState>>>,
|
pub pl_state: Option<Arc<RwLock<PlState>>>,
|
||||||
pub tcrc_state: Arc<RwLock<TcrcState>>,
|
pub tcrc_state: Arc<RwLock<TcrcState>>,
|
||||||
|
pid: PidLoop,
|
||||||
voltage_low: u64,
|
voltage_low: u64,
|
||||||
control_enable_gauge: Gauge,
|
control_enable_gauge: Gauge,
|
||||||
}
|
}
|
||||||
|
@ -44,19 +45,21 @@ impl TeslaChargeRateController {
|
||||||
car_state,
|
car_state,
|
||||||
pl_state,
|
pl_state,
|
||||||
tcrc_state: Default::default(),
|
tcrc_state: Default::default(),
|
||||||
|
pid: Default::default(),
|
||||||
voltage_low: 0,
|
voltage_low: 0,
|
||||||
control_enable_gauge: gauge!("tcrc_control_enable"),
|
control_enable_gauge: gauge!("tcrc_control_enable"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn control_charge_rate(&mut self) -> Option<InterfaceRequest> {
|
pub fn control_charge_rate(&mut self) -> Option<InterfaceRequest> {
|
||||||
|
let delta_time = access_config().pid_controls.loop_time_seconds;
|
||||||
if_chain! {
|
if_chain! {
|
||||||
if let Some(pl_state) = self.pl_state.as_ref().and_then(|v| v.read().ok());
|
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);
|
if let Some(charge_state) = self.car_state.read().ok().and_then(|v| v.charge_state);
|
||||||
then {
|
then {
|
||||||
if pl_state.battery_voltage < access_config().shutoff_voltage {
|
if pl_state.battery_voltage < access_config().shutoff_voltage {
|
||||||
self.voltage_low += 1;
|
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
|
>= access_config().shutoff_voltage_time_seconds
|
||||||
{
|
{
|
||||||
return Some(InterfaceRequest::StopCharge);
|
return Some(InterfaceRequest::StopCharge);
|
||||||
|
@ -64,7 +67,7 @@ impl TeslaChargeRateController {
|
||||||
} else {
|
} else {
|
||||||
self.voltage_low = 0;
|
self.voltage_low = 0;
|
||||||
if self.tcrc_state.read().is_ok_and(|v| v.control_enable) {
|
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> {
|
#[derive(Default)]
|
||||||
let config = access_config();
|
struct PidLoop {
|
||||||
if pl_state.duty_cycle > config.duty_cycle_too_high {
|
previous_error: f64,
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if max/min charge rate has changed, then the current charge rate might no longer be valid
|
impl PidLoop {
|
||||||
// and we should move up/down to keep it in the new bounds
|
fn step(
|
||||||
valid_rate(charge_state.charge_amps, charge_state.charge_amps)
|
&mut self,
|
||||||
.map(InterfaceRequest::SetChargeRate)
|
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> {
|
fn valid_rate(rate: i64, previous: i64) -> Option<i64> {
|
||||||
|
|
|
@ -86,11 +86,13 @@
|
||||||
|
|
||||||
refresh_interval = register();
|
refresh_interval = register();
|
||||||
|
|
||||||
|
refresh_gains();
|
||||||
refresh();
|
refresh();
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
clearInterval(refresh_interval);
|
clearInterval(refresh_interval);
|
||||||
} else {
|
} else {
|
||||||
|
refresh_gains();
|
||||||
refresh();
|
refresh();
|
||||||
refresh_interval = register();
|
refresh_interval = register();
|
||||||
}
|
}
|
||||||
|
@ -152,6 +154,48 @@
|
||||||
set_button.disabled = (number_input.value == current_max_rate);
|
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() {
|
function disable_automatic_control() {
|
||||||
if (is_automatic_control) {
|
if (is_automatic_control) {
|
||||||
document.getElementById('control-disabled').checked = true;
|
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() {
|
function refresh_buttons() {
|
||||||
fetch(api_url + "/control-state")
|
fetch(api_url + "/control-state")
|
||||||
.then((response) => response.json())
|
.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>");
|
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();
|
refresh_buttons();
|
||||||
|
|
||||||
fetch(api_url + "/car-state")
|
fetch(api_url + "/car-state")
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => update_state(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" />
|
<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>
|
<button id="set-minimum" onclick="set_minimum()" disabled>Set minimum</button>
|
||||||
</div>
|
</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>
|
<br>
|
||||||
<button onclick="flash()">flash</button>
|
<button onclick="flash()">flash</button>
|
||||||
<div id="info"></div>
|
<div id="info"></div>
|
||||||
|
|
Loading…
Reference in a new issue