use core::{ sync::atomic::{AtomicBool, Ordering}, fmt, }; use embedded_hal::{ //digital::v1_compat::OldOutputPin, digital::v2::OutputPin, Qei, blocking::delay::{DelayUs, DelayMs}, }; use hd44780_driver::{ HD44780, display_mode::DisplayMode, bus::DataBus, }; use crate::{ state::SystemState, config::{Menu, SystemConfig, FanOutput}, }; //-------------------------------------------------------------------------------------------------- /* GUI config */ pub const GUI_TICK_SEC: f32 = 0.2; const GUI_ERROR_SEC: f32 = 0.8; //---Temp Enum-------------------------------------------------------------------------------------- /// A simple enum to handle displaying temps on the GUI. Temperatures can be valid or not. An /// invalid temperature is displayed as "inval°C" whereas a valid temperature is displayed as /// XX.XX°C pub enum Temp { Valid (f32), Invalid, } impl fmt::Display for Temp { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { match self { 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, Error (&'static str), } /// 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). pub struct LCDGui where L: LCDScreen, Q: Qei, B: OutputPin, { lcd: L, qei: Q, button: &'static AtomicBool, backlight: Option, count: i32, state: GUIState, menu: Menu, ext_deg: Temp, p1_deg: Temp, p2_deg: Temp, should_refresh: bool, blink_counter: i32, } impl LCDGui where L: LCDScreen, Q: Qei, B: OutputPin, { /// 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() 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, blink_counter: 0, }; if let Some(bl) = &mut gui.backlight { let _ = bl.set_high(); }; gui.lcd.reset(); gui.lcd.clear(); gui.lcd.set_display_mode( DisplayMode { display: Display::On, cursor_visibility: Cursor::Invisible, cursor_blink: CursorBlink::Off, }); gui } /// 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 { let mut input_update = false; // manage button press if self.button.swap(false, Ordering::AcqRel) { 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); } }, GUIState::Error (_) => { self.state = GUIState::Idle; self.lcd.clear(); self.should_refresh = true; }}} // 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; } // 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); } }, GUIState::Error (err)=> { self.blink_counter += 1; if self.blink_counter as f32 * GUI_TICK_SEC >= 2.0*GUI_ERROR_SEC { let _ = write!(self.lcd, "Erreur: "); self.lcd.set_cursor_pos(0x40); let _ = write!(self.lcd, "{}", err); self.blink_counter = 0; } else if self.blink_counter as f32 * GUI_TICK_SEC >= GUI_ERROR_SEC { self.lcd.clear(); } }} input_update } /// 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(); }; } pub fn display_error(&mut self, error: &'static str) { self.state = GUIState::Error (error); self.blink_counter = 0; } } 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); fn clear(&mut self); fn set_display_mode(&mut self, mode: DisplayMode); fn set_cursor_pos(&mut self, positions: u8); } impl LCDScreen for HD44780 where D: DelayUs + DelayMs, DB: DataBus, { fn reset(&mut self) { self.reset() } fn clear(&mut self) { self.clear() } fn set_display_mode(&mut self, mode: DisplayMode) { self.set_display_mode(mode) } fn set_cursor_pos(&mut self, position: u8) { self.set_cursor_pos(position); } }