329 lines
9.5 KiB
Rust
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);
|
|
}
|
|
}
|
|
|