From 3668d9b80dbbc869c8326060c9bbda1b7fb1e36a Mon Sep 17 00:00:00 2001 From: Steins7 Date: Fri, 7 Jan 2022 07:45:37 +0000 Subject: [PATCH] Resolve "Design new GUI" --- Cargo.toml | 8 +- docs/Graphs.ods | Bin 30414 -> 44391 bytes docs/encoder_accel.py | 15 +++ src/config.rs | 290 ++++++++++++++++++++++++++++++++++++++++-- src/lcd_gui.rs | 207 ++++++++++++++++++++++++------ src/main.rs | 96 +++++++++----- src/state.rs | 31 +++-- src/utils.rs | 19 ++- 8 files changed, 566 insertions(+), 100 deletions(-) create mode 100644 docs/encoder_accel.py diff --git a/Cargo.toml b/Cargo.toml index 14ffaf2..00450cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,15 @@ version = "0.1.0" [dependencies] embedded-hal = "0.2.6" nb = "0.1.2" -cortex-m = "0.6.0" -cortex-m-rt = "0.6.10" -cortex-m-semihosting = "0.3.3" +cortex-m = "0.7.4" +cortex-m-rt = "0.7.1" +cortex-m-semihosting = "0.3.7" panic-halt = "0.2.0" hd44780-driver = "0.3.0" libm = "0.2.1" [dependencies.stm32f1xx-hal] -version = "0.7.0" +version = "0.8.0" features = ["stm32f103", "rt", "medium"] # this lets you use `cargo fix`! diff --git a/docs/Graphs.ods b/docs/Graphs.ods index f1eb706f00cfd7d8e497a93180408405f1ae033e..a2f8294fa58f955a5e376042b99d3ac924cd92f4 100644 GIT binary patch delta 37341 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 diff --git a/docs/encoder_accel.py b/docs/encoder_accel.py new file mode 100644 index 0000000..180dc1d --- /dev/null +++ b/docs/encoder_accel.py @@ -0,0 +1,15 @@ +import matplotlib.pyplot as plt +import numpy as np +import seaborn as sns + +sns.set_theme(style="whitegrid") + +a = 0.85 +b = 0.2 + +x = np.arange(-5, 6) +y = np.copysign(a*abs(x) * 10**(b*abs(x)) * 0.1, x) + +sns.lineplot(x=x, y=y, palette="tab10", linewidth=2.5) +plt.show() + diff --git a/src/config.rs b/src/config.rs index a433624..36c9866 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,23 +1,40 @@ +use core::fmt; + +use crate::lcd_gui::LCDScreen; + +//---Config struct---------------------------------------------------------------------------------- pub enum ConfigError { LoadError, } +#[derive(Copy, Clone)] pub enum FanOutput { P1, P2, } -pub enum FanState { - ON, - OFF, +#[derive(Copy, Clone)] +pub struct CalData { + pub offset: f32, + pub raw_value: Option, } +impl CalData { + fn new() -> CalData { + CalData { + offset: 0.0, + raw_value: None, + } + } +} + +#[derive(Copy, Clone)] pub struct SystemConfig { pub max_temp_diff: f32, pub min_temp_diff: f32, - pub ext_offset: f32, - pub p1_offset: f32, - pub p2_offset: f32, + pub ext_offset: CalData, + pub p1_offset: CalData, + pub p2_offset: CalData, pub fan_output: FanOutput, } @@ -27,18 +44,273 @@ impl SystemConfig { SystemConfig { max_temp_diff: 8.0, min_temp_diff: 4.0, - ext_offset: 0.0, - p1_offset: 0.0, - p2_offset: 0.0, + ext_offset: CalData::new(), + p1_offset: CalData::new(), + p2_offset: CalData::new(), fan_output: FanOutput::P1, } } + #[allow(dead_code)] pub fn load() -> Result { unimplemented!(); } + #[allow(dead_code)] pub fn store() -> SystemConfig { unimplemented!(); } } + +//---Menu struct------------------------------------------------------------------------------------ +const MENU_ENTRY_NB: usize = 6; + +pub enum MenuState { + Scroll, + Entry, +} + +pub struct Menu { + entries: [Entry; MENU_ENTRY_NB], + state: MenuState, + should_refresh: bool, + entry_index: usize, +} + +impl Menu { + + pub fn new(config: &SystemConfig) -> Menu { + + Menu { + entries: [ + Entry::new("fan out", EntryValue::FanOut (FanOutput::P1), config), + Entry::new("max diff", EntryValue::MaxTempDiff (0.0), config), + Entry::new("min diff", EntryValue::MinTempDiff (0.0), config), + Entry::new("ext cal", EntryValue::ExtOffset (CalData::new()), config), + Entry::new("p1 cal", EntryValue::P1Offset (CalData::new()), config), + Entry::new("p2 cal", EntryValue::P2Offset (CalData::new()), config), + ], + state: MenuState::Scroll, + should_refresh: true, + entry_index: 0, + } + } + + pub fn button_update(&mut self, config: &mut SystemConfig) -> bool { + + self.should_refresh = true; + + // update state machine + match self.state { + MenuState::Scroll => { + if self.entry_index == 0 { return true } + // read desired value from config + self.state = MenuState::Entry; + self.entries[self.entry_index - 1].select(config); + }, + MenuState::Entry => { + // write modified value to config + self.state = MenuState::Scroll; + self.entries[self.entry_index - 1].deselect(config); + } + } + + false + } + + pub fn movement_update(&mut self, movement: i32) { + + if movement != 0 { + match self.state { + MenuState::Scroll => { + + // convert movement to entry index + let mut index = self.entry_index as i32 + movement; + if index > MENU_ENTRY_NB as i32 { + index = MENU_ENTRY_NB as i32; + } else if index < 0 { + index = 0; + } + self.entry_index = index as usize; + }, + MenuState::Entry => { + + // update entry + self.entries[self.entry_index - 1].update(-movement); + //note : "-" sign is just to revert encoder direction, feels better when using + //the interface + }, + } + + self.should_refresh = true; + } + } + + pub fn reset(&mut self) { + + match self.state { + MenuState::Entry => { + // if an entry is selected, disguard all modifications + self.entries[self.entry_index - 1].disgard(); + self.state = MenuState::Scroll; + }, + _ => (), + } + + self.entry_index = 0; + } + + pub fn display(&mut self, screen: &mut L) { + + if self.should_refresh { + + // first line + screen.clear(); + screen.set_cursor_pos(0); + let _ = write!(screen, "\u{007e}"); + if self.entry_index == 0 { + let _ = write!(screen, "retour"); + } else { + self.entries[self.entry_index - 1].display(screen); + } + + // second line (if any) + if self.entry_index < MENU_ENTRY_NB { + screen.set_cursor_pos(0x40); + let _ = write!(screen, " "); + self.entries[self.entry_index].display(screen); + } + + self.should_refresh = false; + } + } +} + +pub enum EntryValue { + MaxTempDiff (f32), + MinTempDiff (f32), + ExtOffset (CalData), + P1Offset (CalData), + P2Offset (CalData), + FanOut (FanOutput), +} + +impl EntryValue { + + fn modify(&mut self, n: i32) { + use libm::{powf, copysignf, fabsf}; + + use crate::utils::TEMP_RANGE; + + match self { + Self::MaxTempDiff (val) | Self::MinTempDiff (val) => { + *val += copysignf(fabsf(n as f32) * 0.85 * powf(10.0, 0.15 * fabsf(n as f32)) + * 0.1, n as f32); + // avoid display issues by limiting value range + if *val > 99.9 { *val = 99.9; } + else if *val < -99.9 { *val = -99.9; } + }, + Self::ExtOffset (data) | Self::P1Offset (data) | Self::P2Offset (data) => { + data.offset += copysignf(fabsf(n as f32) * 0.85 + * powf(10.0, 0.15 * fabsf(n as f32)) * 0.1, n as f32); + // avoid limiting issues like before, taking probe bounds into account + if data.offset + TEMP_RANGE.end > 99.9 { + data.offset = 99.9 - TEMP_RANGE.end; + } + else if data.offset + TEMP_RANGE.start < -99.9 { + data.offset = -99.9 - TEMP_RANGE.start; + } + }, + Self::FanOut (output) => { + if n%2 != 0 { + match output { + FanOutput::P1 => *output = FanOutput::P2, + FanOutput::P2 => *output = FanOutput::P1, + }}}} + } + + fn load(&mut self, config: &SystemConfig) { + match self { + Self::MaxTempDiff (val) => *val = config.max_temp_diff, + Self::MinTempDiff (val) => *val = config.min_temp_diff, + Self::ExtOffset (data) => *data = config.ext_offset, + Self::P1Offset (data) => *data = config.p1_offset, + Self::P2Offset (data) => *data = config.p2_offset, + Self::FanOut (val) => *val = config.fan_output, + }; + } + + fn store(&mut self, config: &mut SystemConfig) { + match self { + Self::MaxTempDiff (val) => config.max_temp_diff = *val, + Self::MinTempDiff (val) => config.min_temp_diff = *val, + Self::ExtOffset (data) => config.ext_offset = *data, + Self::P1Offset (data) => config.p1_offset = *data, + Self::P2Offset (data) => config.p2_offset = *data, + Self::FanOut (val) => config.fan_output = *val, + }; + } +} + +impl fmt::Display for EntryValue { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + use crate::lcd_gui::Temp; + + match self { + Self::MaxTempDiff (val) | Self::MinTempDiff (val) => + formatter.write_fmt(format_args!("{:.1}", val)), + Self::ExtOffset (data) | Self::P1Offset (data) | Self::P2Offset (data) => { + let temp: Temp = data.raw_value.map(|val| val + data.offset).into(); + formatter.write_fmt(format_args!("{}", temp)) + }, + Self::FanOut (output) => { + match output { + FanOutput::P1 => formatter.write_fmt(format_args!("1")), + FanOutput::P2 => formatter.write_fmt(format_args!("2")), + }}} + } +} + + +pub struct Entry { + text: &'static str, + value: EntryValue, + is_selected: bool, +} + +impl Entry { + + pub fn new(text: &'static str, mut value: EntryValue, config: &SystemConfig) -> Entry { + + value.load(config); + Entry { + text, + value, + is_selected: false, + } + } + + pub fn display(&self, buffer: &mut B) { + let _ = match self.is_selected { + true => write!(buffer, "{}:{}", self.text, self.value), + false => write!(buffer, "{} {}", self.text, self.value), + }; + } + + pub fn select(&mut self, config: &SystemConfig) { + self.is_selected = true; + self.value.load(config); + } + pub fn deselect(&mut self, config: &mut SystemConfig) { + self.is_selected = false; + self.value.store(config); + } + + pub fn disgard(&mut self) { + self.is_selected = false; + } + + pub fn update(&mut self, n: i32) { + self.value.modify(n); + } +} diff --git a/src/lcd_gui.rs b/src/lcd_gui.rs index 355def2..d0e7a52 100644 --- a/src/lcd_gui.rs +++ b/src/lcd_gui.rs @@ -17,8 +17,8 @@ use hd44780_driver::{ }; use crate::{ - config::SystemConfig, state::SystemState, + config::{Menu, SystemConfig, FanOutput}, }; //---Temp Enum-------------------------------------------------------------------------------------- @@ -33,16 +33,53 @@ pub enum Temp { impl fmt::Display for Temp { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { match self { - Temp::Valid (deg) => formatter.write_fmt(format_args!("{:.2}", deg)), + Temp::Valid (deg) => formatter.write_fmt(format_args!("{:.1}", deg)), Temp::Invalid => formatter.write_str("inval"), } } } +impl core::convert::From> for Temp { + fn from(val: Option) -> Self { + match val { + Some(deg) => Temp::Valid(deg), + None => Temp::Invalid, + } + } +} + +impl core::convert::From for Temp { + fn from(val: f32) -> Self { + Temp::Valid(val) + } +} + +impl core::cmp::PartialEq for Temp { + fn eq(&self, other: &Self) -> bool { + match self { + Self::Valid (val) => match other { + Self::Invalid => false, + Self::Valid (other_val) => val == other_val, + }, + Self::Invalid => match other { + Self::Invalid => true, + _ => false, + }, + } + } +} + //---LCDGui Struct---------------------------------------------------------------------------------- + +pub enum GUIState { + Off, + Idle, + Menu, +} + /// Manages the lcd screen and inputs (encoder + button) and display relevant information. Can also /// be used to configure the system. The update() function should be called frequently to refresh -/// the GUI (every 0.2s is enough) +/// the GUI (every 0.2s is enough). pub struct LCDGui where L: LCDScreen, @@ -53,9 +90,13 @@ where qei: Q, button: &'static AtomicBool, backlight: Option, - count: u16, + count: i32, + state: GUIState, + menu: Menu, ext_deg: Temp, p1_deg: Temp, + p2_deg: Temp, + should_refresh: bool, } impl LCDGui @@ -65,19 +106,26 @@ where B: OutputPin, { - pub fn new(lcd: L, qei: Q, button: &'static AtomicBool, backlight: Option) + /// Create and configure a new GUI. By default the screen will display the idle screen. + pub fn new(lcd: L, qei: Q, button: &'static AtomicBool, backlight: Option, + config: &SystemConfig) -> LCDGui { + use hd44780_driver::{Cursor, CursorBlink, Display}; - let count = qei.count(); + let count = qei.count() as i32; let mut gui = LCDGui { lcd, qei, button, backlight, count, + state: GUIState::Idle, + menu: Menu::new(config), ext_deg: Temp::Invalid, p1_deg: Temp::Invalid, + p2_deg: Temp::Invalid, + should_refresh: true, }; if let Some(bl) = &mut gui.backlight { let _ = bl.set_high(); }; @@ -87,52 +135,135 @@ where gui.lcd.set_display_mode( DisplayMode { display: Display::On, - cursor_visibility: Cursor::Visible, - cursor_blink: CursorBlink::On, - } - ); - let _ = gui.lcd.write_str("Hello world!"); + cursor_visibility: Cursor::Invisible, + cursor_blink: CursorBlink::Off, + }); gui } - pub fn update(&mut self, state: &mut SystemState) { + /// Perform the necessary operations after an action on the button or the encoder. This + /// function should be called frequently (every 0.2s is enough) to keep track of the state of + /// the encoder. For added reactivness, it can also be called on a button press. The function + /// can modifify the system's config through the system state and will take care of updating + /// the latter. Return true when something has been updated during the function call, usefull + /// for implementing powersaving features. + pub fn update(&mut self, state: &mut SystemState) -> bool { - self.ext_deg = match state.ext_temp() { - Some(deg) => Temp::Valid(deg), - None => Temp::Invalid, - }; + let mut input_update = false; - self.p1_deg = match state.p1_temp() { - Some(deg) => Temp::Valid(deg), - None => Temp::Invalid, - }; - - //TODO deduplicate button detection + // manage button press if self.button.swap(false, Ordering::AcqRel) { - self.lcd.write_str("paf").unwrap(); + input_update = true; + + // update state machine + match self.state { + GUIState::Off => { + self.state = GUIState::Idle; + self.lcd.clear(); + if let Some(bl) = &mut self.backlight { let _ = bl.set_high(); }; + self.should_refresh = true; + }, + GUIState::Idle => { + self.state = GUIState::Menu; + self.menu.display(&mut self.lcd); + }, + GUIState::Menu => { + + //TODO improve that + let mut config: SystemConfig = *state.config(); + if self.menu.button_update(&mut config) { + self.state = GUIState::Idle; + self.update_temps(state); + } else { + state.update_config(config); + self.menu.display(&mut self.lcd); + } + }}} + + // manage encoder movement + let count = self.qei.count() as i32 / 2; + let diff1 = count - self.count; + let diff2 = 511 - i32::abs(diff1); + let diff = if i32::abs(diff1) < diff2 { + diff1 + } else { + diff2 * -i32::signum(diff1) + }; + + if diff != 0 { + input_update = true; + self.count = count; } - if self.count != self.qei.count() { - self.count = self.qei.count(); - self.lcd.set_cursor_pos(0x40); - self.lcd.write_str(" ").unwrap(); - self.lcd.set_cursor_pos(0x40); - write!(self.lcd, "{}", self.qei.count()/2).unwrap(); - } + // display relevant screen + match self.state { + GUIState::Off => {}, + GUIState::Idle => { + if self.should_refresh { + display_idle_screen(&mut self.lcd, &self.ext_deg, &self.p1_deg, &self.p2_deg, + &state.config().fan_output); + self.should_refresh = false; + } + }, + GUIState::Menu => { + if diff != 0 { + self.menu.movement_update(diff); + self.menu.display(&mut self.lcd); + } + }} - self.lcd.set_cursor_pos(0); - self.lcd.write_str(" ").unwrap(); - self.lcd.set_cursor_pos(0); - let _ = write!(self.lcd, "{}\u{00df}C", self.ext_deg); + input_update + } - self.lcd.set_cursor_pos(0x40); - self.lcd.write_str(" ").unwrap(); - self.lcd.set_cursor_pos(0x40); - let _ = write!(self.lcd, "{}\u{00df}C", self.p1_deg); + /// Update the temperatures to display on the idle screen. Should be called whenever the + /// temperatures are modified. + pub fn update_temps(&mut self, state: &SystemState) { + + self.ext_deg = state.ext_temp().into(); + self.p1_deg = state.p1_temp().into(); + self.p2_deg = state.p2_temp().into(); + + self.should_refresh = true; + } + + pub fn sleep(&mut self) { + + self.state = GUIState::Off; + + // reset menu in case the user was in-menu + self.menu.reset(); + + // manage lcd + self.lcd.clear(); + //TODO set display off ? + if let Some(bl) = &mut self.backlight { let _ = bl.set_low(); }; } } +fn display_idle_screen(screen: &mut L, ext_deg: &Temp, p1_deg: &Temp, p2_deg: &Temp, + fan_output: &FanOutput) { + + // display temperatures + screen.clear(); + screen.set_cursor_pos(0); + let _ = write!(screen, "Ext:{}", ext_deg); + screen.set_cursor_pos(0x41); + let _ = write!(screen, "1:{}", p1_deg); + screen.set_cursor_pos(0x49); + let _ = write!(screen, "2:{}", p2_deg); + + // display probe selection + match fan_output { + FanOutput::P1 => screen.set_cursor_pos(0x40), + FanOutput::P2 => screen.set_cursor_pos(0x48), + } + let _ = write!(screen, "*"); +} + +//---LCDScreen trait-------------------------------------------------------------------------------- +/// A wrapper trait to complement the write trait by adding the extra functions provided by a lcd +/// screen, such as cursor position. pub trait LCDScreen: core::fmt::Write { fn reset(&mut self); diff --git a/src/main.rs b/src/main.rs index b788158..a1c8ad9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,6 @@ use cortex_m::interrupt::Mutex; use cortex_m_rt::entry; use embedded_hal::digital::{ - v2::OutputPin, v1_compat::OldOutputPin, }; @@ -24,7 +23,7 @@ use stm32f1xx_hal::{ prelude::*, timer::{Timer, CountDownTimer, Event}, qei::{QeiOptions, SlaveMode}, - rtc::Rtc, + rtc::{Rtc, RtcClkLsi}, }; mod lcd_gui; @@ -45,9 +44,10 @@ use state::SystemState; /* system config */ const GUI_TICK_SEC: f32 = 0.2; +const AWAKE_TIMEOUT_SEC: f32 = 20.0; const HEARTBEAT_SEC: f32 = 1.0; -const TEMPS_TICK_SEC: u32 = 2; +const TEMPS_TICK_SEC: u32 = 30; //-------------------------------------------------------------------------------------------------- /* interrupt variables */ @@ -64,10 +64,17 @@ static BUTTON_PIN: Mutex>>>> static BUTTON_FLAG: AtomicBool = AtomicBool::new(false); // temps interrupt -static RTC: Mutex>> = Mutex::new(RefCell::new(None)); +static RTC: Mutex>>> = Mutex::new(RefCell::new(None)); static G_EXTI: Mutex>> = Mutex::new(RefCell::new(None)); static TEMP_FLAG: AtomicBool = AtomicBool::new(false); +//-------------------------------------------------------------------------------------------------- +/* power management */ +enum PowerState { + Awake, + Sleeping, +} + //-------------------------------------------------------------------------------------------------- /* interrupt service routines */ #[interrupt] @@ -126,16 +133,16 @@ fn main() -> ! { let mut dp = pac::Peripherals::take().unwrap(); // clocks config - let mut rcc = dp.RCC.constrain(); + let rcc = dp.RCC.constrain(); let mut flash = dp.FLASH.constrain(); let clocks = rcc.cfgr .use_hse(8.mhz()) .freeze(&mut flash.acr); // GPIOs - let mut gpioa = dp.GPIOA.split(&mut rcc.apb2); - let mut gpiob = dp.GPIOB.split(&mut rcc.apb2); - let mut gpioc = dp.GPIOC.split(&mut rcc.apb2); + let mut gpioa = dp.GPIOA.split(); + let mut gpiob = dp.GPIOB.split(); + let mut gpioc = dp.GPIOC.split(); // setup LED let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); @@ -146,7 +153,7 @@ fn main() -> ! { }); // Configure the timer 2 as tick timer - let mut timer = Timer::tim2(dp.TIM2, &clocks, &mut rcc.apb1) + let mut timer = Timer::tim2(dp.TIM2, &clocks) .start_count_down(((1.0/GUI_TICK_SEC) as u32).hz()); timer.listen(Event::Update); @@ -159,8 +166,8 @@ fn main() -> ! { dp.EXTI.imr.write(|w| w.mr17().set_bit()); // setup RTC - let mut backup_domain = rcc.bkp.constrain(dp.BKP, &mut rcc.apb1, &mut dp.PWR); - let mut rtc = Rtc::rtc(dp.RTC, &mut backup_domain); + let mut backup_domain = rcc.bkp.constrain(dp.BKP, &mut dp.PWR); + let mut rtc = Rtc::::rtc(dp.RTC, &mut backup_domain); rtc.set_alarm(rtc.current_time() + TEMPS_TICK_SEC); rtc.listen_alarm(); @@ -168,7 +175,11 @@ fn main() -> ! { RTC.borrow(cs).borrow_mut().replace(rtc) }); - let mut afio = dp.AFIO.constrain(&mut rcc.apb2); + let mut afio = dp.AFIO.constrain(); + + // initialize system state + let config = SystemConfig::new(); + let mut state = SystemState::new(config, gpiob.pb12.into_push_pull_output(&mut gpiob.crh)); // Setup display let mut gui = { @@ -193,7 +204,7 @@ fn main() -> ! { // Force write mode let mut rw_pin = gpioa.pa9.into_push_pull_output(&mut gpioa.crh); - rw_pin.set_low().unwrap(); + rw_pin.set_low(); let lcd = HD44780::new_8bit( OldOutputPin::new(rs_pin), @@ -212,7 +223,7 @@ fn main() -> ! { // setup button (interrupt) let mut but_pin = gpiob.pb8.into_pull_up_input(&mut gpiob.crh); but_pin.make_interrupt_source(&mut afio); - but_pin.trigger_on_edge(&dp.EXTI, Edge::RISING); + but_pin.trigger_on_edge(&dp.EXTI, Edge::Rising); but_pin.enable_interrupt(&dp.EXTI); cortex_m::interrupt::free(|cs| { @@ -223,14 +234,14 @@ fn main() -> ! { let backlight_pin = gpiob.pb9.into_push_pull_output(&mut gpiob.crh); // setup encoder - let enc = Timer::tim4(dp.TIM4, &clocks, &mut rcc.apb1) + let enc = Timer::tim4(dp.TIM4, &clocks) .qei((gpiob.pb6, gpiob.pb7), &mut afio.mapr, QeiOptions { - slave_mode:SlaveMode::EncoderMode2, - auto_reload_value: 200 + slave_mode:SlaveMode::EncoderMode1, + auto_reload_value: 1022, }); - LCDGui::new(lcd, enc, &BUTTON_FLAG, Some(backlight_pin)) + LCDGui::new(lcd, enc, &BUTTON_FLAG, Some(backlight_pin), state.config()) }; let exti = dp.EXTI; @@ -254,42 +265,65 @@ fn main() -> ! { // configure stop mode (not working yet, probably due to fake chip) //dp.PWR.cr.write(|w| w.pdds().stop_mode()); //dp.PWR.cr.write(|w| w.lpds().set_bit()); - cp.SCB.set_sleepdeep(); // setup adc - let mut adc = Adc::adc1(dp.ADC1, &mut rcc.apb2, clocks); + let mut adc = Adc::adc1(dp.ADC1, clocks); adc.set_sample_time(SampleTime::T_71); let adc = RefCell::new(adc); let ext1 = gpioa.pa4.into_analog(&mut gpioa.crl); let ext2 = gpioa.pa5.into_analog(&mut gpioa.crl); - let p1_1 = gpioa.pa2.into_analog(&mut gpioa.crl); - let p1_2 = gpioa.pa3.into_analog(&mut gpioa.crl); + let p2_1 = gpioa.pa2.into_analog(&mut gpioa.crl); + let p2_2 = gpioa.pa3.into_analog(&mut gpioa.crl); + let p1_1 = gpioa.pa0.into_analog(&mut gpioa.crl); + let p1_2 = gpioa.pa1.into_analog(&mut gpioa.crl); let mut ext_probe = TemperatureProbe::new(&adc, ext1, ext2).unwrap(); let mut p1_probe = TemperatureProbe::new(&adc, p1_1, p1_2).unwrap(); - - // initialize system state - let config = SystemConfig::new(); - let mut state = SystemState::new(config, gpiob.pb12.into_push_pull_output(&mut gpiob.crh)); + let mut p2_probe = TemperatureProbe::new(&adc, p2_1, p2_2).unwrap(); + let mut power_state = PowerState::Awake; + let mut timeout = 0; + /* run */ let _ = TEMP_FLAG.swap(true, Ordering::Release); loop { - //compute temps if TEMP_FLAG.swap(false, Ordering::AcqRel) { + + // compute temps let ext_temp = ext_probe.read().ok(); let p1_temp = p1_probe.read().ok(); + let p2_temp = p2_probe.read().ok(); - state.update(ext_temp, p1_temp, None); - } + state.update(ext_temp, p1_temp, p2_temp); + gui.update_temps(&mut state); - gui.update(&mut state); + } else { + + match gui.update(&mut state) { + true => timeout = 0, + false => timeout += 1, + } + + // manage power state + match power_state { + PowerState::Awake => { // normal gui update + if timeout as f32 * GUI_TICK_SEC >= AWAKE_TIMEOUT_SEC { + // go to sleep + power_state = PowerState::Sleeping; + gui.sleep(); + cp.SCB.set_sleepdeep(); + } + }, + PowerState::Sleeping => { // button was pressed during sleep + // wake up + power_state = PowerState::Awake; + cp.SCB.clear_sleepdeep(); + }}} // put device in sleep mode until next interrupt (button or timer) cortex_m::asm::wfi(); } } - diff --git a/src/state.rs b/src/state.rs index b892465..9186545 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,8 +7,8 @@ use crate::config::{ }; pub enum FanState { - ON, - OFF, + On, + Off, } pub struct Fan { @@ -23,19 +23,19 @@ impl Fan { // erroring here means the pin is wrongly configured, there is nothing else to do... pin.set_low().map_err(|_| "fan pin configuration").unwrap(); Fan { - state: FanState::OFF, + state: FanState::Off, pin, } } pub fn on(&mut self) { self.pin.set_high().map_err(|_| "fan pin configuration").unwrap(); - self.state = FanState::ON; + self.state = FanState::On; } pub fn off(&mut self) { self.pin.set_low().map_err(|_| "fan pin configuration").unwrap(); - self.state = FanState::OFF; + self.state = FanState::Off; } pub fn state(&self) -> &FanState { &self.state } @@ -70,9 +70,9 @@ impl SystemState { pub fn update_config(&mut self, config: SystemConfig) { // remove offsets - let ext_temp = self.ext_temp.map(|temp| temp - self.config.ext_offset); - let p1_temp = self.p1_temp.map(|temp| temp - self.config.p1_offset); - let p2_temp = self.p2_temp.map(|temp| temp - self.config.p2_offset); + let ext_temp = self.ext_temp.map(|temp| temp - self.config.ext_offset.offset); + let p1_temp = self.p1_temp.map(|temp| temp - self.config.p1_offset.offset); + let p2_temp = self.p2_temp.map(|temp| temp - self.config.p2_offset.offset); // replace config self.config = config; @@ -87,9 +87,14 @@ impl SystemState { pub fn update(&mut self, ext_temp: Option, p1_temp: Option, p2_temp: Option) { // apply offsets - self.ext_temp = ext_temp.map(|temp| temp + self.config.ext_offset); - self.p1_temp = p1_temp.map(|temp| temp + self.config.p1_offset); - self.p2_temp = p2_temp.map(|temp| temp + self.config.p2_offset); + self.ext_temp = ext_temp.map(|temp| temp + self.config.ext_offset.offset); + self.p1_temp = p1_temp.map(|temp| temp + self.config.p1_offset.offset); + self.p2_temp = p2_temp.map(|temp| temp + self.config.p2_offset.offset); + + // update cal values + self.config.ext_offset.raw_value = ext_temp; + self.config.p1_offset.raw_value = p1_temp; + self.config.p2_offset.raw_value = p2_temp; // select right probe and check if data is available let p = match match self.config.fan_output { @@ -112,12 +117,12 @@ impl SystemState { // compute fan state match self.fan.state() { - FanState::ON => { + FanState::On => { if (p - ext) < self.config.min_temp_diff { self.fan.off(); } }, - FanState::OFF => { + FanState::Off => { if (p - ext) > self.config.max_temp_diff { self.fan.on(); } diff --git a/src/utils.rs b/src/utils.rs index 1c396f5..0a8d5a3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,7 @@ use core::{ marker::PhantomData, cell::RefCell, + ops::Range, }; use embedded_hal::adc::*; @@ -9,16 +10,21 @@ use libm::*; //-------------------------------------------------------------------------------------------------- /* 2nd order linearisation factor for the temperature probe */ -const A: f32 = -0.00058; -const B: f32 = 0.0677; +const A: f32 = -0.000574; +const B: f32 = 0.0676; const C: f32 = -0.07; +//-------------------------------------------------------------------------------------------------- +/* valid temperature range */ +pub const TEMP_RANGE: Range = -10.0..45.0; + //-------------------------------------------------------------------------------------------------- /* error management */ #[derive(Debug)] pub enum ProbeError { ReadError, Initializing, + OutOfRange, } //-------------------------------------------------------------------------------------------------- @@ -76,9 +82,9 @@ where // compute first temp approximation to speed up stabilization let mut temp = 0.0; for _ in 0..10 { - temp += read_temp(&adc, &mut pos_pin, &mut neg_pin)?; + temp += read_temp(adc, &mut pos_pin, &mut neg_pin)?; } - temp = temp/10.0; + temp /= 10.0; Ok(TemperatureProbe { adc, @@ -97,7 +103,10 @@ where match self.stabilized { true => { self.filtered_temp += (temp - self.filtered_temp)/FILTER_TIME_CONSTANT; - Ok(self.filtered_temp) + match TEMP_RANGE.contains(&self.filtered_temp) { + true => Ok(self.filtered_temp), + false => Err(ProbeError::OutOfRange), + } }, false => { // filter is not yet stabilized let old_temp = self.filtered_temp;