Compare commits
2 commits
475dffe6b2
...
63d85d3385
| Author | SHA1 | Date | |
|---|---|---|---|
| 63d85d3385 | |||
| 52c918cf1f |
8 changed files with 239 additions and 26 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
/kicad/*-backups/
|
||||
/kicad/*.lck
|
||||
/kicad/*.lck
|
||||
/firmware/cfg.toml
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
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 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,43 +126,69 @@ 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
|
||||
// so we use this feature to "debounce" our button by enabling interrupt only after some time passes
|
||||
let not = btn1.button.wait_event(esp_idf_svc::hal::delay::NON_BLOCK);
|
||||
match not {
|
||||
Some(n) => {
|
||||
println!("Button 1 pressed!");
|
||||
if not.is_some() {
|
||||
println!("Button 1 pressed!");
|
||||
|
||||
if heater_drv_pin.is_set_high() {
|
||||
heater_drv_pin.set_low()?;
|
||||
btn1.indicator.set_state(false)?;
|
||||
} else {
|
||||
if heater_drv_pin.is_set_high() {
|
||||
heater_drv_pin.set_low()?;
|
||||
btn1.indicator.set_state(false)?;
|
||||
} else {
|
||||
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());
|
||||
},
|
||||
None => (),
|
||||
input_time = Option::from(Instant::now());
|
||||
}
|
||||
|
||||
// enable button interrupt again after 300ms from last click
|
||||
match input_time {
|
||||
Some(time) => {
|
||||
if time.elapsed().as_millis() >= 300 {
|
||||
btn1.button.enable_interrupt()?;
|
||||
input_time.take();
|
||||
}
|
||||
},
|
||||
None => (),
|
||||
if let Some(time) = input_time {
|
||||
if time.elapsed().as_millis() >= 300 {
|
||||
btn1.button.enable_interrupt()?;
|
||||
input_time.take();
|
||||
}
|
||||
}
|
||||
|
||||
if measure_time.elapsed().as_millis() >= 10 {
|
||||
|
|
@ -117,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;
|
||||
}
|
||||
|
|
@ -148,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
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