Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saving default fan speed to EEPROM #216

Merged
merged 3 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* The W5500 now operates as an external MAC, and smoltcp is used as the network stack.
* DHCP support has been added, `netmask`, `ip-address`, and `gateway` settings have been removed.
Settings are backwards compatible with previous Booster releases.
* Fan speed is now stored in EEPROM and configurable via the serial interface.

## [0.3.0]

Expand Down
5 changes: 3 additions & 2 deletions src/hardware/chassis_fans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ impl ChassisFans {
///
/// # Args
/// * `fans` - The fan controllers to use.
/// * `leds` - The LEDs on Booster's main board.
/// * `duty_cycle` - The default (normalized) duty cycle to use when enabling fans.
///
/// # Returns
/// A new fan controller.
pub fn new(fans: [Max6639<I2cProxy>; 3], leds: MainboardLeds) -> Self {
pub fn new(fans: [Max6639<I2cProxy>; 3], leds: MainboardLeds, default_speed: f32) -> Self {
ChassisFans {
fans,
duty_cycle: DEFAULT_FAN_SPEED,
duty_cycle: default_speed.clamp(0.0, 1.0),
leds,
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/hardware/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ pub fn setup(
max6639::Max6639::new(i2c_bus_manager.acquire_i2c(), max6639::AddressPin::Pullup)
.unwrap();

ChassisFans::new([fan1, fan2, fan3], main_board_leds)
ChassisFans::new([fan1, fan2, fan3], main_board_leds, settings.fan_speed())
};

assert!(fans.self_test(&mut delay));
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ mod app {

let mut settings = RuntimeSettings::default();

// Load the default fan speed
settings.fan_speed = booster.settings.fan_speed();

for idx in Channel::into_enum_iter() {
settings.channel[idx as usize] = booster
.main_bus
Expand Down
85 changes: 52 additions & 33 deletions src/serial_terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ enum Token {
#[token("broker-address")]
BrokerAddress,

#[token("fan")]
FanSpeed,

#[regex(r"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+", |lex| lex.slice().parse())]
IpAddress(Ipv4Addr),

Expand All @@ -65,6 +68,7 @@ pub enum Property {
Mac,
BrokerAddress,
Identifier,
FanSpeed,
}

pub enum Request {
Expand All @@ -75,6 +79,7 @@ pub enum Request {
ServiceInfo,
WriteIpAddress(Property, Ipv4Addr),
WriteIdentifier(String<32>),
WriteFanSpeed(f32),
}

fn get_property_string(prop: Property, settings: &BoosterSettings) -> String<128> {
Expand All @@ -83,6 +88,7 @@ fn get_property_string(prop: Property, settings: &BoosterSettings) -> String<128
Property::Identifier => writeln!(&mut msg, "{}", settings.id()).unwrap(),
Property::Mac => writeln!(&mut msg, "{}", settings.mac()).unwrap(),
Property::BrokerAddress => writeln!(&mut msg, "{}", settings.broker()).unwrap(),
Property::FanSpeed => writeln!(&mut msg, "{:.2} %", settings.fan_speed() * 100.0).unwrap(),
};
msg
}
Expand Down Expand Up @@ -248,6 +254,10 @@ impl SerialTerminal {
}
}

Request::WriteFanSpeed(speed) => {
self.settings.set_fan_speed(speed);
}

Request::Read(prop) => {
let msg = get_property_string(prop, &self.settings);
self.write(msg.as_bytes());
Expand Down Expand Up @@ -386,13 +396,13 @@ impl SerialTerminal {
| Booster Command Help :
+----------------------+
* `reset` - Resets the device
* `dfu` - Resets the device to DFU mode
* `read <PROP>` - Reads the value of <PROP>. <PROP> may be [broker-address, mac, id]
* `write broker-address <IP>` - Writes the value of <IP> to the broker address.
<IP> must be an IP address (e.g. 192.168.1.1)
* `write id <ID>` - Write the MQTT client ID of the device. <ID> must be 23 or less ASCII \
characters.
* `service` - Read the service information. Service infromation clears once read.
* `dfu` - Resets the device to DFU mode
* `read <PROP>` - Reads the value of <PROP>. <PROP> may be [broker-address, mac, id, fan]
* `write [broker-address <IP> | id <ID> | fan <DUTY>]`
- Writes the value of <IP> to the broker address. <IP> must be an IP address (e.g. 192.168.1.1)
- Write the MQTT client ID of the device. <ID> must be 23 or less ASCII characters.
- Write the <DUTY> default fan speed duty cycle, which is specified [0, 1.0].
"
.as_bytes(),
);
Expand Down Expand Up @@ -427,41 +437,50 @@ characters.
Token::Mac => Property::Mac,
Token::BrokerAddress => Property::BrokerAddress,
Token::Identifier => Property::Identifier,
Token::FanSpeed => Property::FanSpeed,
_ => return Err("Invalid property read"),
};

Request::Read(property)
}
Token::Write => {
// Validate that there are two valid token following.
let property_token = lex.next().ok_or("Malformed command")?;

// Check that the property is acceptable for a read.
let property = match property_token {
Token::BrokerAddress => Property::BrokerAddress,
Token::Identifier => Property::Identifier,
_ => return Err("Invalid property write"),
};

let value_token = lex.next().ok_or("Malformed property")?;

match value_token {
Token::IpAddress(addr) => Request::WriteIpAddress(property, addr),
Token::DeviceIdentifier if property == Property::Identifier => {
if lex.slice().len() < 23 {
// The regex on this capture allow us to assume it is valid utf8, since
// we know it is alphanumeric.
let id: String<32> = String::from_str(
core::str::from_utf8(lex.slice().as_bytes()).unwrap(),
)
.unwrap();

Request::WriteIdentifier(id)
// Check that the property is acceptable for a write.
match lex.next().ok_or("Malformed command")? {
Token::BrokerAddress => {
if let Token::IpAddress(addr) = lex.next().ok_or("Malformed address")? {
Request::WriteIpAddress(Property::BrokerAddress, addr)
} else {
return Err("ID too long");
return Err("Invalid property");
}
}
_ => return Err("Invalid write request"),
Token::Identifier => {
if let Token::DeviceIdentifier = lex.next().ok_or("Malformed ID")? {
if lex.slice().len() < 23 {
// The regex on this capture allow us to assume it is valid utf8, since
// we know it is alphanumeric.
let id: String<32> = String::from_str(
core::str::from_utf8(lex.slice().as_bytes()).unwrap(),
)
.unwrap();

Request::WriteIdentifier(id)
} else {
return Err("ID too long");
}
} else {
return Err("Invalid property");
}
}
Token::FanSpeed => {
let fan_speed: f32 = lex
.remainder()
.trim()
.parse()
.map_err(|_| "Invalid float")?;
lex.bump(lex.remainder().len());
Request::WriteFanSpeed(fan_speed)
}
_ => return Err("Invalid property write"),
}
}
_ => return Err("Invalid command"),
Expand All @@ -470,7 +489,7 @@ characters.
// Finally, verify that the lexer was consumed during parsing. Otherwise, the command
// was malformed.
if lex.next().is_some() {
Err("Malformed command\n")
Err("Malformed command - trailing data\n")
} else {
Ok(Some(request))
}
Expand Down
2 changes: 1 addition & 1 deletion src/settings/channel_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl VersionedChannelData {
}

// Validate the version of the settings.
if !EXPECTED_VERSION.is_compatible(&data.version) {
if !EXPECTED_VERSION.is_compatible_with(&data.version) {
return Err(Error::Invalid);
}

Expand Down
83 changes: 51 additions & 32 deletions src/settings/global_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use heapless::String;
use minimq::embedded_nal::Ipv4Addr;
use serde::{Deserialize, Serialize};

use crate::hardware::chassis_fans::DEFAULT_FAN_SPEED;

#[cfg(feature = "phy_w5500")]
use w5500::MacAddress;

Expand All @@ -23,8 +25,8 @@ use core::fmt::Write;
/// `BoosterMainBoardData` layout is updated.
const EXPECTED_VERSION: SemVersion = SemVersion {
major: 1,
minor: 0,
patch: 1,
minor: 1,
patch: 0,
};

fn array_to_addr(addr: &[u8; 4]) -> Ipv4Addr {
Expand All @@ -48,6 +50,7 @@ struct BoosterMainBoardData {
_unused_netmask: [u8; 4],
identifier: [u8; 23],
id_size: usize,
fan_speed: f32,
}

impl BoosterMainBoardData {
Expand Down Expand Up @@ -75,6 +78,7 @@ impl BoosterMainBoardData {
_unused_netmask: [255, 255, 255, 0],
identifier: id,
id_size: name.len(),
fan_speed: DEFAULT_FAN_SPEED,
}
}

Expand All @@ -85,13 +89,26 @@ impl BoosterMainBoardData {
/// * `data` - The data to deserialize from.
///
/// # Returns
/// The configuration if deserialization was successful. Otherwise, returns an error.
pub fn deserialize(data: &[u8; 64]) -> Result<Self, Error> {
let config: BoosterMainBoardData = postcard::from_bytes(data).unwrap();

// Validate the version of the settings.
if !EXPECTED_VERSION.is_compatible(&config.version) {
return Err(Error::Invalid);
/// The configuration if deserialization was successful along with a bool indicating if the
/// configuration was automatically upgraded. Otherwise, returns an error.
pub fn deserialize(data: &[u8; 64]) -> Result<(Self, bool), Error> {
let mut config: BoosterMainBoardData = postcard::from_bytes(data).unwrap();
let mut modified = false;

// Check if the stored EEPROM version is older (or incompatible)
if !EXPECTED_VERSION.is_compatible_with(&config.version) {
// If the stored config is compatible with the new version (e.g. older), we can upgrade
// the config version in a backward compatible manner by adding in new parameters and
// writing it back.
if config.version.is_compatible_with(&EXPECTED_VERSION) {
log::info!("Adding default fan speed setting");
config.fan_speed = DEFAULT_FAN_SPEED;
config.version = EXPECTED_VERSION;
modified = true;
} else {
// The version stored in EEPROM is some future version that we don't understand.
return Err(Error::Invalid);
}
}

// Validate configuration parameters.
Expand All @@ -100,7 +117,7 @@ impl BoosterMainBoardData {
return Err(Error::Invalid);
}

Ok(config)
Ok((config, modified))
}

/// Serialize the booster config into a sinara configuration for storage into EEPROM.
Expand Down Expand Up @@ -132,7 +149,6 @@ impl BoosterMainBoardData {
/// # Returns
/// Ok if the update was successful. Otherwise, returns an error.
pub fn set_id(&mut self, id: &str) -> Result<(), Error> {
// TODO: Verify the ID is valid.
if !identifier_is_valid(id) {
return Err(Error::Invalid);
}
Expand Down Expand Up @@ -167,37 +183,28 @@ impl BoosterSettings {
let mut mac: [u8; 6] = [0; 6];
eeprom.read_eui48(&mut mac).unwrap();

// Load the sinara configuration from EEPROM.
let (board_data, write_back) = Self::load_config(&mut eeprom)
.and_then(|config| BoosterMainBoardData::deserialize(&config.board_data))
.unwrap_or((BoosterMainBoardData::default(&mac), true));

let mut settings = Self {
board_data: BoosterMainBoardData::default(&mac),
board_data,
eui48: mac,
dirty: false,
eeprom,
};

// Load the sinara configuration from EEPROM.
match settings.load_config() {
Ok(config) => match BoosterMainBoardData::deserialize(&config.board_data) {
Ok(data) => settings.board_data = data,

Err(_) => {
settings.board_data = BoosterMainBoardData::default(&settings.eui48);
settings.save();
}
},

// If we failed to load configuration, use a default config.
Err(_) => {
settings.board_data = BoosterMainBoardData::default(&settings.eui48);
settings.save();
}
};
if write_back {
settings.save();
}

settings
}

/// Save the configuration settings to EEPROM for retrieval.
pub fn save(&mut self) {
let mut config = match self.load_config() {
let mut config = match Self::load_config(&mut self.eeprom) {
Err(_) => SinaraConfiguration::default(SinaraBoardId::Mainboard),
Ok(config) => config,
};
Expand All @@ -212,10 +219,10 @@ impl BoosterSettings {
/// # Returns
/// Ok(settings) if the settings loaded successfully. Otherwise, Err(settings), where `settings`
/// are default values.
fn load_config(&mut self) -> Result<SinaraConfiguration, Error> {
fn load_config(eeprom: &mut Eeprom) -> Result<SinaraConfiguration, Error> {
// Read the sinara-config from memory.
let mut sinara_config: [u8; 256] = [0; 256];
self.eeprom.read(0, &mut sinara_config).unwrap();
eeprom.read(0, &mut sinara_config).unwrap();

SinaraConfiguration::try_deserialize(sinara_config)
}
Expand All @@ -237,6 +244,18 @@ impl BoosterSettings {
MacAddress { octets: self.eui48 }
}

/// Get the saved Booster fan speed.
pub fn fan_speed(&self) -> f32 {
self.board_data.fan_speed
}

/// Set the default fan speed of the device.
pub fn set_fan_speed(&mut self, fan_speed: f32) {
self.board_data.fan_speed = fan_speed.clamp(0.0, 1.0);
self.dirty = true;
self.save();
}

/// Get the MQTT broker IP address.
pub fn broker(&self) -> Ipv4Addr {
self.board_data.broker()
Expand Down
2 changes: 1 addition & 1 deletion src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub struct SemVersion {

impl SemVersion {
/// Determine if this version is compatible with `rhs`.
pub fn is_compatible(&self, rhs: &SemVersion) -> bool {
pub fn is_compatible_with(&self, rhs: &SemVersion) -> bool {
(self.major == rhs.major) && (self.minor <= rhs.minor)
}
}