implement basic pid controller
This commit is contained in:
parent
52c918cf1f
commit
63d85d3385
8 changed files with 229 additions and 10 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
/kicad/*-backups/
|
/kicad/*-backups/
|
||||||
/kicad/*.lck
|
/kicad/*.lck
|
||||||
|
/firmware/cfg.toml
|
||||||
|
|
@ -3,7 +3,7 @@ target = "xtensa-esp32s2-espidf"
|
||||||
|
|
||||||
[target.xtensa-esp32s2-espidf]
|
[target.xtensa-esp32s2-espidf]
|
||||||
linker = "ldproxy"
|
linker = "ldproxy"
|
||||||
runner = "espflash flash --monitor --before no-reset -a no-reset --no-stub"
|
runner = "espflash flash --monitor --before no-reset -a no-reset --no-stub -T part_table.csv"
|
||||||
rustflags = [ "--cfg", "espidf_time64"]
|
rustflags = [ "--cfg", "espidf_time64"]
|
||||||
|
|
||||||
[unstable]
|
[unstable]
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-
|
||||||
anyhow = "1.0.97"
|
anyhow = "1.0.97"
|
||||||
embedded-graphics = "0.8.1"
|
embedded-graphics = "0.8.1"
|
||||||
ssd1306 = "0.9.0"
|
ssd1306 = "0.9.0"
|
||||||
|
toml-cfg = "0.2.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
embuild = "0.33"
|
embuild = "0.33"
|
||||||
|
toml-cfg = "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,15 @@
|
||||||
|
#[toml_cfg::toml_config]
|
||||||
|
pub struct Config {
|
||||||
|
#[default("")]
|
||||||
|
wifi_ssid: &'static str,
|
||||||
|
#[default("")]
|
||||||
|
wifi_psk: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
if !std::path::Path::new("cfg.toml").exists() {
|
||||||
|
panic!("You need to create a `cfg.toml` file with your Wi-Fi credentials! Use `cfg.toml.template` as a template.");
|
||||||
|
}
|
||||||
|
|
||||||
embuild::espidf::sysenv::output();
|
embuild::espidf::sysenv::output();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
firmware/cfg.toml.template
Normal file
3
firmware/cfg.toml.template
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[http-server]
|
||||||
|
wifi_ssid = "beans32"
|
||||||
|
wifi_psk = "12345678"
|
||||||
5
firmware/part_table.csv
Normal file
5
firmware/part_table.csv
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# ESP-IDF Partition Table
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x6000,
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
|
factory, app, factory, 0x10000, 2M,
|
||||||
|
|
|
@ -1,21 +1,61 @@
|
||||||
mod hid;
|
mod hid;
|
||||||
|
mod wifi;
|
||||||
|
|
||||||
use std::{thread, time::{Duration, Instant}};
|
use std::{sync::{atomic::{AtomicBool, AtomicPtr, Ordering}, Arc, Mutex, RwLock}, thread, time::{Duration, Instant}};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
mono_font::{ascii::FONT_6X10, MonoTextStyleBuilder},
|
mono_font::{ascii::FONT_6X10, MonoTextStyleBuilder},
|
||||||
pixelcolor::BinaryColor,
|
pixelcolor::BinaryColor,
|
||||||
};
|
};
|
||||||
use esp_idf_svc::hal::{
|
use esp_idf_svc::{eventloop::EspSystemEventLoop, hal::{
|
||||||
adc::{attenuation::DB_11, oneshot::{config::AdcChannelConfig, AdcChannelDriver, AdcDriver}},
|
adc::{attenuation::DB_11, oneshot::{config::AdcChannelConfig, AdcChannelDriver, AdcDriver}},
|
||||||
gpio::{InterruptType, PinDriver},
|
gpio::{InterruptType, PinDriver},
|
||||||
i2c::{I2cConfig, I2cDriver},
|
i2c::{I2cConfig, I2cDriver},
|
||||||
prelude::Peripherals,
|
prelude::Peripherals,
|
||||||
units::*
|
units::*
|
||||||
};
|
}, http::{server::{Configuration, EspHttpServer}, Method}, io::{EspIOError, Write}};
|
||||||
use hid::{Button, GPIOIndicatedButton, Indicator};
|
use hid::{Button, GPIOIndicatedButton, Indicator};
|
||||||
use ssd1306::{mode::DisplayConfig, prelude::DisplayRotation, size::DisplaySize128x32, I2CDisplayInterface, Ssd1306};
|
use ssd1306::{mode::DisplayConfig, prelude::DisplayRotation, size::DisplaySize128x32, I2CDisplayInterface, Ssd1306};
|
||||||
|
|
||||||
|
#[toml_cfg::toml_config]
|
||||||
|
pub struct Config {
|
||||||
|
#[default("")]
|
||||||
|
wifi_ssid: &'static str,
|
||||||
|
#[default("")]
|
||||||
|
wifi_psk: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PIDController {
|
||||||
|
p_gain: f32,
|
||||||
|
i_gain: f32,
|
||||||
|
d_gain: f32,
|
||||||
|
prev_err: f32,
|
||||||
|
sum_err: f32,
|
||||||
|
target: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PIDController {
|
||||||
|
pub fn new(p_gain: f32, i_gain: f32, d_gain: f32, target: f32) -> Self {
|
||||||
|
return Self {
|
||||||
|
p_gain,
|
||||||
|
i_gain,
|
||||||
|
d_gain,
|
||||||
|
prev_err: 0_f32,
|
||||||
|
sum_err: 0_f32,
|
||||||
|
target,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_control(&mut self, measured: f32) -> f32 {
|
||||||
|
let ctrl_err = self.target - measured;
|
||||||
|
let delta_err = ctrl_err - self.prev_err;
|
||||||
|
let control = self.p_gain * ctrl_err + self.i_gain * self.sum_err + self.d_gain * delta_err;
|
||||||
|
self.sum_err += ctrl_err;
|
||||||
|
self.prev_err = ctrl_err;
|
||||||
|
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// It is necessary to call this function once. Otherwise some patches to the runtime
|
// It is necessary to call this function once. Otherwise some patches to the runtime
|
||||||
|
|
@ -63,6 +103,22 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
log::info!("Hello, world!");
|
log::info!("Hello, world!");
|
||||||
|
|
||||||
|
let sysloop = EspSystemEventLoop::take()?;
|
||||||
|
let app_config = CONFIG;
|
||||||
|
|
||||||
|
// Connect to the Wi-Fi network
|
||||||
|
let _wifi = wifi::wifi(
|
||||||
|
app_config.wifi_ssid,
|
||||||
|
app_config.wifi_psk,
|
||||||
|
peripherals.modem,
|
||||||
|
sysloop,
|
||||||
|
)?;
|
||||||
|
let mut server = EspHttpServer::new(&Configuration::default())?;
|
||||||
|
|
||||||
|
const PID_TARGET: f32 = 70_f32;
|
||||||
|
|
||||||
|
let mut pid = PIDController::new(0.5, 0.01, 0.3, PID_TARGET);
|
||||||
|
|
||||||
const R1: f32 = 100000_f32;
|
const R1: f32 = 100000_f32;
|
||||||
const R0: f32 = 100000_f32;
|
const R0: f32 = 100000_f32;
|
||||||
const V1: f32 = 5_f32;
|
const V1: f32 = 5_f32;
|
||||||
|
|
@ -70,13 +126,41 @@ fn main() -> Result<()> {
|
||||||
const B: f32 = 4285_f32;
|
const B: f32 = 4285_f32;
|
||||||
|
|
||||||
let mut samples_sum = 0_f32;
|
let mut samples_sum = 0_f32;
|
||||||
let mut temperature = 0_f32;
|
let temperature = Arc::new(Mutex::new(0_f32));
|
||||||
|
let c_temperature = Arc::clone(&temperature);
|
||||||
|
|
||||||
|
let pid_control = Arc::new(Mutex::new(0_f32));
|
||||||
|
let c_pid_control = Arc::clone(&pid_control);
|
||||||
|
|
||||||
|
let heater_state = Arc::new(AtomicBool::new(heater_drv_pin.is_set_high()));
|
||||||
|
let c_heater_state = Arc::clone(&heater_state);
|
||||||
|
|
||||||
let mut i = 0_u32;
|
let mut i = 0_u32;
|
||||||
|
|
||||||
|
let headers = &[("Content-Type", "text/plain")];
|
||||||
|
|
||||||
|
server.fn_handler(
|
||||||
|
"/metrics",
|
||||||
|
Method::Get,
|
||||||
|
move |request| -> core::result::Result<(), EspIOError> {
|
||||||
|
let mut response = request.into_response(200, Option::None, headers)?;
|
||||||
|
let (pidv, tempv) = {
|
||||||
|
(*pid_control.lock().unwrap(), *temperature.lock().unwrap())
|
||||||
|
};
|
||||||
|
response.write(format!("pid_controller_action {}\n", pidv).as_bytes())?;
|
||||||
|
response.write(format!("pid_controller_target {}\n", PID_TARGET).as_bytes())?;
|
||||||
|
response.write(format!("boiler_temperature {}\n", tempv).as_bytes())?;
|
||||||
|
response.write(format!("boiler_heater_state {}\n", heater_state.load(Ordering::Relaxed) as i32 as f32).as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
let mut blink_time = Instant::now();
|
let mut blink_time = Instant::now();
|
||||||
let mut measure_time = Instant::now();
|
let mut measure_time = Instant::now();
|
||||||
let mut input_time = Option::from(Instant::now());
|
let mut input_time = Option::from(Instant::now());
|
||||||
|
|
||||||
|
let mut is_meltdown_state = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// when we receive button event, button interrupt will be automatically disabled until we enable it manually
|
// when we receive button event, button interrupt will be automatically disabled until we enable it manually
|
||||||
// so we use this feature to "debounce" our button by enabling interrupt only after some time passes
|
// so we use this feature to "debounce" our button by enabling interrupt only after some time passes
|
||||||
|
|
@ -88,8 +172,12 @@ fn main() -> Result<()> {
|
||||||
heater_drv_pin.set_low()?;
|
heater_drv_pin.set_low()?;
|
||||||
btn1.indicator.set_state(false)?;
|
btn1.indicator.set_state(false)?;
|
||||||
} else {
|
} else {
|
||||||
heater_drv_pin.set_high()?;
|
if !is_meltdown_state {
|
||||||
btn1.indicator.set_state(true)?;
|
heater_drv_pin.set_high()?;
|
||||||
|
btn1.indicator.set_state(true)?;
|
||||||
|
} else {
|
||||||
|
println!("DON'T CLICK WE'RE ABOUT TO EXPLODE!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input_time = Option::from(Instant::now());
|
input_time = Option::from(Instant::now());
|
||||||
|
|
@ -111,8 +199,37 @@ fn main() -> Result<()> {
|
||||||
samples_sum += temp;
|
samples_sum += temp;
|
||||||
i += 1;
|
i += 1;
|
||||||
} else {
|
} else {
|
||||||
temperature = samples_sum / 64_f32;
|
let tempv = samples_sum / 64_f32;
|
||||||
println!("Temperature value: {}", temperature);
|
let pidv = pid.get_control(tempv);
|
||||||
|
{
|
||||||
|
let mut t = c_temperature.lock().unwrap();
|
||||||
|
let mut p = c_pid_control.lock().unwrap();
|
||||||
|
*t = tempv;
|
||||||
|
*p = pidv;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Temperature value: {}", tempv);
|
||||||
|
println!("PID Control Value: {}", pidv);
|
||||||
|
|
||||||
|
if !is_meltdown_state {
|
||||||
|
if pidv > 12_f32 && !heater_drv_pin.is_set_high() {
|
||||||
|
println!("[PID] Set heater to high");
|
||||||
|
heater_drv_pin.set_high()?;
|
||||||
|
} else if pidv < 10_f32 && !heater_drv_pin.is_set_low(){
|
||||||
|
println!("[PID] Set heater to low");
|
||||||
|
heater_drv_pin.set_low()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tempv > 85_f32 {
|
||||||
|
println!("CRITICAL TEMPERATURE SHUTTING HEATER DOWN");
|
||||||
|
println!("CRITICAL TEMPERATURE SHUTTING HEATER DOWN");
|
||||||
|
println!("CRITICAL TEMPERATURE SHUTTING HEATER DOWN");
|
||||||
|
println!("CRITICAL TEMPERATURE SHUTTING HEATER DOWN");
|
||||||
|
heater_drv_pin.set_low()?;
|
||||||
|
is_meltdown_state = true;
|
||||||
|
}
|
||||||
|
|
||||||
samples_sum = 0_f32;
|
samples_sum = 0_f32;
|
||||||
i = 0;
|
i = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +259,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
blink_time = Instant::now();
|
blink_time = Instant::now();
|
||||||
}
|
}
|
||||||
|
c_heater_state.store(heater_drv_pin.is_set_high(), Ordering::Relaxed);
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
thread::sleep(Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
78
firmware/src/wifi.rs
Normal file
78
firmware/src/wifi.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use esp_idf_svc::{
|
||||||
|
eventloop::EspSystemEventLoop,
|
||||||
|
hal::peripheral,
|
||||||
|
wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi},
|
||||||
|
};
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
pub fn wifi(
|
||||||
|
ssid: &str,
|
||||||
|
pass: &str,
|
||||||
|
modem: impl peripheral::Peripheral<P = esp_idf_svc::hal::modem::Modem> + 'static,
|
||||||
|
sysloop: EspSystemEventLoop,
|
||||||
|
) -> Result<Box<EspWifi<'static>>> {
|
||||||
|
let mut auth_method = AuthMethod::WPA2Personal;
|
||||||
|
if ssid.is_empty() {
|
||||||
|
bail!("Missing WiFi name")
|
||||||
|
}
|
||||||
|
if pass.is_empty() {
|
||||||
|
auth_method = AuthMethod::None;
|
||||||
|
info!("Wifi password is empty");
|
||||||
|
}
|
||||||
|
let mut esp_wifi = EspWifi::new(modem, sysloop.clone(), None)?;
|
||||||
|
|
||||||
|
let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sysloop)?;
|
||||||
|
|
||||||
|
wifi.set_configuration(&Configuration::Client(ClientConfiguration::default()))?;
|
||||||
|
|
||||||
|
info!("Starting wifi...");
|
||||||
|
|
||||||
|
wifi.start()?;
|
||||||
|
|
||||||
|
info!("Scanning...");
|
||||||
|
|
||||||
|
let ap_infos = wifi.scan()?;
|
||||||
|
|
||||||
|
let ours = ap_infos.into_iter().find(|a| a.ssid == ssid);
|
||||||
|
|
||||||
|
let channel = if let Some(ours) = ours {
|
||||||
|
info!(
|
||||||
|
"Found configured access point {} on channel {}",
|
||||||
|
ssid, ours.channel
|
||||||
|
);
|
||||||
|
Some(ours.channel)
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"Configured access point {} not found during scanning, will go with unknown channel",
|
||||||
|
ssid
|
||||||
|
);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
wifi.set_configuration(&Configuration::Client(ClientConfiguration {
|
||||||
|
ssid: ssid
|
||||||
|
.try_into()
|
||||||
|
.expect("Could not parse the given SSID into WiFi config"),
|
||||||
|
password: pass
|
||||||
|
.try_into()
|
||||||
|
.expect("Could not parse the given password into WiFi config"),
|
||||||
|
channel,
|
||||||
|
auth_method,
|
||||||
|
..Default::default()
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
info!("Connecting wifi...");
|
||||||
|
|
||||||
|
wifi.connect()?;
|
||||||
|
|
||||||
|
info!("Waiting for DHCP lease...");
|
||||||
|
|
||||||
|
wifi.wait_netif_up()?;
|
||||||
|
|
||||||
|
let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
|
||||||
|
|
||||||
|
info!("Wifi DHCP info: {:?}", ip_info);
|
||||||
|
|
||||||
|
Ok(Box::new(esp_wifi))
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue