Resolve "Design new GUI"
This commit is contained in:
parent
891e3d03cd
commit
3668d9b80d
@ -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`!
|
||||
|
||||
BIN
docs/Graphs.ods
BIN
docs/Graphs.ods
Binary file not shown.
15
docs/encoder_accel.py
Normal file
15
docs/encoder_accel.py
Normal file
@ -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()
|
||||
|
||||
290
src/config.rs
290
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<f32>,
|
||||
}
|
||||
|
||||
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<ConfigError, SystemConfig> {
|
||||
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<L: LCDScreen>(&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<B: core::fmt::Write>(&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);
|
||||
}
|
||||
}
|
||||
|
||||
207
src/lcd_gui.rs
207
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<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,
|
||||
}
|
||||
|
||||
/// 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<L, Q, B>
|
||||
where
|
||||
L: LCDScreen,
|
||||
@ -53,9 +90,13 @@ where
|
||||
qei: Q,
|
||||
button: &'static AtomicBool,
|
||||
backlight: Option<B>,
|
||||
count: u16,
|
||||
count: i32,
|
||||
state: GUIState,
|
||||
menu: Menu,
|
||||
ext_deg: Temp,
|
||||
p1_deg: Temp,
|
||||
p2_deg: Temp,
|
||||
should_refresh: bool,
|
||||
}
|
||||
|
||||
impl<L, Q, B> LCDGui<L, Q, B>
|
||||
@ -65,19 +106,26 @@ where
|
||||
B: OutputPin,
|
||||
{
|
||||
|
||||
pub fn new(lcd: L, qei: Q, button: &'static AtomicBool, backlight: Option<B>)
|
||||
/// 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();
|
||||
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<O: OutputPin>(&mut self, state: &mut SystemState<O>) {
|
||||
/// 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 {
|
||||
|
||||
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<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(); };
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
96
src/main.rs
96
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<RefCell<Option<gpiob::PB8<Input<PullUp>>>>>
|
||||
static BUTTON_FLAG: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// temps interrupt
|
||||
static RTC: Mutex<RefCell<Option<Rtc>>> = Mutex::new(RefCell::new(None));
|
||||
static RTC: Mutex<RefCell<Option<Rtc::<RtcClkLsi>>>> = Mutex::new(RefCell::new(None));
|
||||
static G_EXTI: Mutex<RefCell<Option<EXTI>>> = 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::<RtcClkLsi>::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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
31
src/state.rs
31
src/state.rs
@ -7,8 +7,8 @@ use crate::config::{
|
||||
};
|
||||
|
||||
pub enum FanState {
|
||||
ON,
|
||||
OFF,
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
pub struct Fan<O: OutputPin> {
|
||||
@ -23,19 +23,19 @@ impl<O: OutputPin> Fan<O> {
|
||||
// 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<O: OutputPin> SystemState<O> {
|
||||
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<O: OutputPin> SystemState<O> {
|
||||
pub fn update(&mut self, ext_temp: Option<f32>, p1_temp: Option<f32>, p2_temp: Option<f32>) {
|
||||
|
||||
// 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<O: OutputPin> SystemState<O> {
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
19
src/utils.rs
19
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<f32> = -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user