fan_monitor/src/lcd_gui.rs
Steins7 3fc3dbaae8 Added config error screen
+ added error management
+ added config error when failing to load
2022-01-25 14:09:16 +01:00

329 lines
9.5 KiB
Rust

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<Option<f32>> for Temp {
fn from(val: Option<f32>) -> Self {
match val {
Some(deg) => Temp::Valid(deg),
None => Temp::Invalid,
}
}
}
impl core::convert::From<f32> for Temp {
fn from(val: f32) -> Self {
Temp::Valid(val)
}
}
impl core::cmp::PartialEq<Temp> 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<L, Q, B>
where
L: LCDScreen,
Q: Qei<Count = u16>,
B: OutputPin,
{
lcd: L,
qei: Q,
button: &'static AtomicBool,
backlight: Option<B>,
count: i32,
state: GUIState,
menu: Menu,
ext_deg: Temp,
p1_deg: Temp,
p2_deg: Temp,
should_refresh: bool,
blink_counter: i32,
}
impl<L, Q, B> LCDGui<L, Q, B>
where
L: LCDScreen,
Q: Qei<Count = u16>,
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<B>,
config: &SystemConfig)
-> LCDGui<L, Q, B> {
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<O: OutputPin>(&mut self, state: &mut SystemState<O>) -> 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<O: OutputPin>(&mut self, state: &SystemState<O>) {
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<L: LCDScreen>(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<D, DB> LCDScreen for HD44780<D, DB>
where
D: DelayUs<u16> + DelayMs<u8>,
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);
}
}