implement basic pid controller

This commit is contained in:
soffee 2025-03-08 22:52:15 +03:00
parent 52c918cf1f
commit 63d85d3385
8 changed files with 229 additions and 10 deletions

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
/kicad/*-backups/
/kicad/*.lck
/kicad/*.lck
/firmware/cfg.toml

View file

@ -3,7 +3,7 @@ target = "xtensa-esp32s2-espidf"
[target.xtensa-esp32s2-espidf]
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"]
[unstable]

View file

@ -28,6 +28,8 @@ esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-
anyhow = "1.0.97"
embedded-graphics = "0.8.1"
ssd1306 = "0.9.0"
toml-cfg = "0.2.0"
[build-dependencies]
embuild = "0.33"
toml-cfg = "0.2.0"

View file

@ -1,3 +1,15 @@
#[toml_cfg::toml_config]
pub struct Config {
#[default("")]
wifi_ssid: &'static str,
#[default("")]
wifi_psk: &'static str,
}
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();
}

View file

@ -0,0 +1,3 @@
[http-server]
wifi_ssid = "beans32"
wifi_psk = "12345678"

5
firmware/part_table.csv Normal file
View 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 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 factory, app, factory, 0x10000, 2M,

View file

@ -1,21 +1,61 @@
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 embedded_graphics::{
mono_font::{ascii::FONT_6X10, MonoTextStyleBuilder},
pixelcolor::BinaryColor,
};
use esp_idf_svc::hal::{
use esp_idf_svc::{eventloop::EspSystemEventLoop, hal::{
adc::{attenuation::DB_11, oneshot::{config::AdcChannelConfig, AdcChannelDriver, AdcDriver}},
gpio::{InterruptType, PinDriver},
i2c::{I2cConfig, I2cDriver},
prelude::Peripherals,
units::*
};
}, http::{server::{Configuration, EspHttpServer}, Method}, io::{EspIOError, Write}};
use hid::{Button, GPIOIndicatedButton, Indicator};
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<()> {
// 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!");
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 R0: f32 = 100000_f32;
const V1: f32 = 5_f32;
@ -70,12 +126,40 @@ fn main() -> Result<()> {
const B: f32 = 4285_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 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 measure_time = Instant::now();
let mut input_time = Option::from(Instant::now());
let mut is_meltdown_state = false;
loop {
// when we receive button event, button interrupt will be automatically disabled until we enable it manually
@ -88,8 +172,12 @@ fn main() -> Result<()> {
heater_drv_pin.set_low()?;
btn1.indicator.set_state(false)?;
} else {
heater_drv_pin.set_high()?;
btn1.indicator.set_state(true)?;
if !is_meltdown_state {
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());
@ -111,8 +199,37 @@ fn main() -> Result<()> {
samples_sum += temp;
i += 1;
} else {
temperature = samples_sum / 64_f32;
println!("Temperature value: {}", temperature);
let tempv = samples_sum / 64_f32;
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;
i = 0;
}
@ -142,6 +259,7 @@ fn main() -> Result<()> {
blink_time = Instant::now();
}
c_heater_state.store(heater_drv_pin.is_set_high(), Ordering::Relaxed);
thread::sleep(Duration::from_millis(10));
}

78
firmware/src/wifi.rs Normal file
View 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))
}