Skip to content

Commit

Permalink
Read system configuration from flash partition
Browse files Browse the repository at this point in the history
  • Loading branch information
zargony committed Sep 28, 2024
1 parent f143653 commit e1bd72c
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 1 deletion.
3 changes: 3 additions & 0 deletions firmware/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/target

config*.json
!config-example.json
29 changes: 29 additions & 0 deletions firmware/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions firmware/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ embedded-graphics = "0.8"
embedded-hal-async = "1.0"
embedded-io-async = "0.6"
embedded-nal-async = "0.7"
embedded-storage = "0.3"
esp-alloc = "0.4"
esp-backtrace = { version = "0.14", features = ["esp32c3", "custom-halt", "panic-handler", "exception-handler", "println"] }
esp-hal = { version = "0.20", features = ["esp32c3", "async"] }
esp-hal-embassy = { version = "0.3", features = ["esp32c3"] }
esp-partition-table = "0.1"
esp-println = { version = "0.11", features = ["esp32c3", "log"] }
esp-storage = { version = "0.3", features = ["esp32c3"] }
esp-wifi = { version = "0.9", default-features = false, features = ["esp32c3", "async", "embassy-net", "phy-enable-usb", "wifi"] }
log = { version = "0.4", features = ["release_max_level_info"] }
pn532 = "0.4"
Expand Down
18 changes: 17 additions & 1 deletion firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,30 @@ Use Rust's build tool `cargo` to build the firmware:
cargo build --release
```

## Flash to Device
## Flash Firmware to Device

To flash the firmware to a device, connect the device via its USB-C serial port and use `espflash`:

```sh
cargo espflash flash --release
```

## Flash Configuration to Device

Configuration is stored in a separate flash partition (`nvs`) and is therefore unaffected by firmware updates. As there is currently no way to change the configuration at runtime, it needs to be flashed to the device manually (once).

Create a custom configuration, e.g. `config.json`. See `config-example.json` for available settings. Keep it as small as possible, either by removing all comments and whitespace manually or by using the `jq` tool:

```sh
jq -c < config.json > config.min.json
```

Store the minimized configuration to the device's `nvs` partition at 0x9000 using `espflash`:

```sh
espflash write-bin 0x9000 config.min.json
```

## Contributions

If you implement changes or features that can be useful for everyone, please fork this repository and open a pull request. Make sure to also update documentation and code comments accordingly and add a high level description of your changes to the changelog. Also make sure that all CI jobs are passing and ideally try flashing and using the firmware image artifact to verify its behaviour.
Expand Down
19 changes: 19 additions & 0 deletions firmware/config-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Note that you must remove all comments in your own configuration file since
// JSON officially doesn't allow comments and parsing will fail with comments.
{
// SSID and WPA2 password to use for connecting to Wifi network. Wifi must
// provide an IPv4 address via DHCP and internet access to required services.
"wifi-ssid": "My Wifi",
"wifi-password": "12345",

// Credentials for connecting to the Vereinsflieger API. See Vereinsflieger
// REST Documentation for details. Note that password needs to be given as
// its hex MD5 hash instead of plain text.
"vf-username": "[email protected]",
"vf-password-md5": "00000000000000000000000000000000",
"vf-appkey": "00000000000000000000000000000000",
"vf-cid": 0,

// Vereinsflieger article id to use for purchases
"vf-article-id": 0
}
115 changes: 115 additions & 0 deletions firmware/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use alloc::string::String;
use core::fmt;
use core::ops::Deref;
use embedded_storage::ReadStorage;
use esp_partition_table::{DataPartitionType, PartitionTable, PartitionType};
use esp_storage::FlashStorage;
use log::{debug, info, warn};
use serde::Deserialize;

/// String with sensitive content (debug and display output redacted)
#[derive(Default, Deserialize)]
pub struct SensitiveString(String);

impl fmt::Debug for SensitiveString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
self.0.fmt(f)
} else {
"<redacted>".fmt(f)
}
}
}

impl fmt::Display for SensitiveString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
self.0.fmt(f)
} else {
"<redacted>".fmt(f)
}
}
}

impl Deref for SensitiveString {
type Target = String;

fn deref(&self) -> &Self::Target {
&self.0
}
}

/// System configuration
///
/// System configuration is stored in the `nvs` flash partition, so it stays unaffected by firmware
/// updates via USB or OTA. Currently, configuration is read-only at runtime, i.e. it needs to be
/// flashed manually once per device. To make this easier, it is expected to be stored in JSON
/// format at the first sector (4 kb) of the `nvs` flash data partition (this is incompatible with
/// the format that IDF nvs functions expect in this flash partition). See README.md for details
/// on how to flash a configuration.
///
/// If there is no valid JSON or no valid `nvs` data partition, a default configuration is provided
/// (which isn't very useful, but at least doesn't prevent the device from starting).
#[derive(Debug, Default, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Config {
/// Wifi SSID to connect to
pub wifi_ssid: String,
/// Wifi password
pub wifi_password: SensitiveString,
}

impl Config {
/// Read configuration from nvs flash partition
pub fn read() -> Self {
let mut storage = FlashStorage::new();

// Read partition table (at 0x8000 by default)
let table = PartitionTable::default();
debug!("Config: Reading partition table at 0x{:x}", table.addr);

// Look up nvs data partition (at 0x9000 by default)
let nvs_offset = if let Some(offset) = table
.iter_storage(&mut storage, false)
.flatten()
.find(|partition| partition.type_ == PartitionType::Data(DataPartitionType::Nvs))
.map(|partition| partition.offset)
{
debug!("Config: Found nvs data partition at offset 0x{:x}", offset);
offset
} else {
warn!("Config: Unable to find nvs data partition");
return Self::default();
};

// Read first sector (4 kb) of nvs partition
let mut bytes = [0; FlashStorage::SECTOR_SIZE as usize];
if let Err(_err) = storage.read(nvs_offset, &mut bytes) {
warn!("Config: Unable to read nvs partition");
return Self::default();
}
// Find first non-ascii character and trim to the end. This removes trailing 0xff bytes
// (unused flash bytes), which would otherwise lead to 'trailing characters' serde error
// nightly: let (json, _rest) = bytes.split_once(|b| !b.is_ascii());
let json = bytes
.split(|b| !b.is_ascii())
.next()
.unwrap_or(bytes.as_ref());

// Parse JSON config
let config = match serde_json::from_slice::<Self>(json) {
Ok(config) => config,
Err(err) => {
warn!(
"Config: Unable to parse configuration in nvs partition: {}",
err
);
return Self::default();
}
};

debug!("Config: System configuration: {:?}", config);
info!("Config: Configuration loaded from nvs partition");
config
}
}
6 changes: 6 additions & 0 deletions firmware/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#![no_main]

mod buzzer;
mod config;
mod display;
mod error;
mod http;
Expand Down Expand Up @@ -115,6 +116,9 @@ async fn main(spawner: Spawner) {
esp_println::logger::init_logger_from_env();
info!("Touch 'n Drink {VERSION_STR} ({GIT_SHA_STR})");

// Read system configuration
let config = config::Config::read();

// Initialize I2C controller
let i2c = I2C::new_with_timeout_async(
peripherals.I2C0,
Expand Down Expand Up @@ -168,6 +172,8 @@ async fn main(spawner: Spawner) {
&clocks,
peripherals.WIFI,
spawner,
&config.wifi_ssid,
&config.wifi_password,
)
.await
{
Expand Down
9 changes: 9 additions & 0 deletions firmware/src/wifi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,16 @@ pub struct Wifi {

impl Wifi {
/// Create and initialize Wifi interface
#[allow(clippy::too_many_arguments)]
pub async fn new(
timer: impl EspWifiTimerSource,
mut rng: Rng,
radio_clocks: peripherals::RADIO_CLK,
clocks: &Clocks<'_>,
wifi: peripherals::WIFI,
spawner: Spawner,
ssid: &str,
password: &str,
) -> Result<Self, InitializationError> {
debug!("Wifi: Initializing controller...");

Expand All @@ -155,6 +158,12 @@ impl Wifi {
debug!("Wifi: Static configuration: {:?}", esp_wifi::CONFIG);
let init = esp_wifi::initialize(EspWifiInitFor::Wifi, timer, rng, radio_clocks, clocks)?;
let client_config = WifiClientConfiguration {
ssid: ssid
.try_into()
.map_err(|()| InitializationError::General(0))?,
password: password
.try_into()
.map_err(|()| InitializationError::General(0))?,
..Default::default()
};
let (device, mut controller) = wifi::new_with_config(&init, wifi, client_config)?;
Expand Down

0 comments on commit e1bd72c

Please sign in to comment.