From 62e6522bf3ccb1ae0b457c92f5ddfe20b589e1e2 Mon Sep 17 00:00:00 2001
From: Stephen D <webmaster@scd31.com>
Date: Sat, 3 Feb 2024 13:11:12 -0400
Subject: [PATCH] allow setting frequency

---
 Cargo.lock    | 112 ++++++++++++++++++++++++++--------
 Cargo.toml    |   6 +-
 rustfmt.toml  |   1 +
 src/config.rs | 163 +++++++++++++++++++++-----------------------------
 src/radio.rs  |   3 +-
 src/shell.rs  | 111 ++++++++++++++++++++--------------
 6 files changed, 224 insertions(+), 172 deletions(-)
 create mode 100644 rustfmt.toml

diff --git a/Cargo.lock b/Cargo.lock
index 49e0998..b6d03ce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7,6 +7,9 @@ name = "arrayvec"
 version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "atomic-polyfill"
@@ -46,9 +49,9 @@ checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
 
 [[package]]
 name = "bitflags"
-version = "2.4.1"
+version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
 
 [[package]]
 name = "bitvec"
@@ -84,8 +87,10 @@ dependencies = [
  "nmea",
  "num-traits",
  "panic-reset",
+ "postcard",
  "rand",
  "rf4463",
+ "serde",
  "stm32f4xx-hal",
  "systick-monotonic",
  "usb-device",
@@ -101,13 +106,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.31"
+version = "0.4.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
+checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
 dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "cobs"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
+
 [[package]]
 name = "cortex-m"
 version = "0.7.7"
@@ -138,7 +149,7 @@ checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -166,7 +177,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rtic-syntax",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -207,9 +218,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
 [[package]]
 name = "deranged"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
 dependencies = [
  "powerfmt",
 ]
@@ -307,7 +318,7 @@ dependencies = [
 [[package]]
 name = "ham-cats"
 version = "0.2.0"
-source = "git+https://gitlab.scd31.com/cats/ham-cats#3f423d2bd2aff5321a2d3e8c66740c936e1f5cdf"
+source = "git+https://gitlab.scd31.com/cats/ham-cats#846af8f55b2398da94da31737287b57caf419505"
 dependencies = [
  "arrayvec",
  "bitvec",
@@ -343,6 +354,7 @@ dependencies = [
  "atomic-polyfill",
  "hash32",
  "rustc_version 0.4.0",
+ "serde",
  "spin",
  "stable_deref_trait",
 ]
@@ -366,8 +378,7 @@ dependencies = [
 [[package]]
 name = "labrador-ldpc"
 version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23c19ea22fc166b77441be6ea377e5aa20121490cdec0af18a14356d390f45b5"
+source = "git+https://github.com/adamgreig/labrador-ldpc#a6b1b1feb65ec3eae5bb1797f2e4798d4fcee92e"
 
 [[package]]
 name = "libm"
@@ -387,9 +398,9 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.6.4"
+version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
 
 [[package]]
 name = "minimal-lexical"
@@ -436,6 +447,12 @@ dependencies = [
  "minimal-lexical",
 ]
 
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
 [[package]]
 name = "num-traits"
 version = "0.2.17"
@@ -461,6 +478,17 @@ version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
 
+[[package]]
+name = "postcard"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8"
+dependencies = [
+ "cobs",
+ "heapless",
+ "serde",
+]
+
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -476,7 +504,7 @@ dependencies = [
  "proc-macro-error-attr",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
  "version_check",
 ]
 
@@ -493,18 +521,18 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.70"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.33"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 dependencies = [
  "proc-macro2",
 ]
@@ -533,7 +561,7 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 [[package]]
 name = "rf4463"
 version = "0.1.0"
-source = "git+https://gitlab.scd31.com/stephen/rf4463-lib#79c8def87540f8ab2663bfa3c9fb13db344ef84e"
+source = "git+https://gitlab.scd31.com/stephen/rf4463-lib#e6ef9a2fd2c64bff5ea7c1716dcc21943add0c2d"
 dependencies = [
  "embedded-hal 0.2.7",
 ]
@@ -559,7 +587,7 @@ dependencies = [
  "indexmap",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -577,7 +605,7 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
 dependencies = [
- "semver 1.0.20",
+ "semver 1.0.21",
 ]
 
 [[package]]
@@ -597,9 +625,9 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.20"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
+checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
 
 [[package]]
 name = "semver-parser"
@@ -607,6 +635,26 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
+[[package]]
+name = "serde"
+version = "1.0.196"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.196"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
 [[package]]
 name = "snafu"
 version = "0.7.5"
@@ -626,7 +674,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -693,6 +741,17 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "syn"
+version = "2.0.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
 [[package]]
 name = "synopsys-usb-otg"
 version = "0.3.2"
@@ -724,11 +783,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
 [[package]]
 name = "time"
-version = "0.3.30"
+version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
+checksum = "00b24b79b7a07f10209f19e683ca1e289d80b1e76ffa8c2b779718566a083679"
 dependencies = [
  "deranged",
+ "num-conv",
  "powerfmt",
  "time-core",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 1b71233..6412f1d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,11 +26,13 @@ ham-cats = { git = "https://gitlab.scd31.com/cats/ham-cats" }
 half = { version = "2.3.1", default-features = false }
 # TODO can get rid of some of these features
 nmea = { version = "0.6.0", default-features = false, features = ["VTG", "GGA", "RMC", "GNS", "GLL"] }
-arrayvec = { version = "0.7.4", default-features = false }
+arrayvec = { version = "0.7.4", default-features = false, features = ["serde"] }
 ushell = "0.3.6"
 usbd-serial = "0.1.1"
 usb-device = "0.2.9"
 crc = "3.0.1"
 embedded-storage = "0.2.0"
 num-traits = { version = "0.2.17", default-features = false, features = ["libm"] }
-rand = { version = "0.8", default_features = false, features = ["small_rng"] }
+rand = { version = "0.8", default-features = false, features = ["small_rng"] }
+postcard = "1.0.8"
+serde = { version = "1.0.196", default-features = false, features = ["derive"] }
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..3a26366
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1 @@
+edition = "2021"
diff --git a/src/config.rs b/src/config.rs
index 4d0f42c..d84cd28 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -3,15 +3,18 @@ use core::fmt::Display;
 use arrayvec::ArrayString;
 use crc::{Crc, CRC_32_ISCSI};
 use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
+use serde::{Deserialize, Serialize};
 use stm32f4xx_hal::flash::{FlashExt, LockedFlash};
 
 use crate::geo::distance_km;
 
-const CONFIG_LEN: usize = 844;
+const DEFAULT_FREQUENCY: u32 = 430_500_000;
+const CONFIG_LEN: usize = 2048;
 const CASTAGNOLI: Crc<u32> = Crc::<u32>::new(&CRC_32_ISCSI);
 
-#[derive(Clone)]
+#[derive(Clone, Serialize, Deserialize)]
 pub struct Config {
+    pub frequency: Frequency,
     pub callsign: ArrayString<252>,
     pub icon: u16,
     pub ssid: u8,
@@ -59,88 +62,32 @@ impl Config {
         // version
         // allows us to update the config structure in the future
         // while maintaining backwards compatibility
-        buf[0] = 0;
-
-        buf[1..3].copy_from_slice(&self.icon.to_le_bytes());
-        buf[3] = self.ssid;
-        buf[4] = self.max_hops;
-        buf[5..13].copy_from_slice(&self.transmit_period_seconds.to_le_bytes());
-        match self.gps {
-            GpsSetting::Disabled => {
-                buf[13] = 0;
-            }
-
-            GpsSetting::Fixed(lat, lon) => {
-                buf[13] = 1;
-                buf[14..22].copy_from_slice(&lat.to_le_bytes());
-                buf[22..30].copy_from_slice(&lon.to_le_bytes());
-            }
-
-            GpsSetting::Receiver => {
-                buf[13] = 2;
-            }
-        }
-
-        let enable_tx: u8 = self.enable_transmit.into();
-        let enable_digi: u8 = self.enable_digipeating.into();
-        buf[30] = enable_tx | (enable_digi << 1);
-
-        buf[31] = self.callsign.len().try_into().unwrap();
-        buf[32..(32 + self.callsign.len())].copy_from_slice(self.callsign.as_bytes());
-        buf[284..286].copy_from_slice(&u16::try_from(self.comment.len()).unwrap().to_le_bytes());
-        buf[286..(286 + self.comment.len())].copy_from_slice(self.comment.as_bytes());
-
-        self.black_hole
-            .encode((&mut buf[798..823]).try_into().unwrap());
-
-        match self.antenna_height {
-            Maybe::Some(height) => {
-                buf[823] = 1;
-                buf[824] = height;
-            }
-            Maybe::None => buf[823] = 0,
-        }
-
-        match self.antenna_gain {
-            Maybe::Some(gain) => {
-                buf[825] = 1;
-                buf[826..830].copy_from_slice(&gain.to_le_bytes());
-            }
-            Maybe::None => buf[825] = 0,
-        }
+        buf[0] = 1;
 
-        match self.tx_power {
-            Maybe::Some(power) => {
-                buf[830] = 1;
-                buf[831..835].copy_from_slice(&power.to_le_bytes());
-            }
-            Maybe::None => {
-                buf[830] = 0;
-            }
-        }
-
-        match self.min_voltage {
-            Maybe::Some(v) => {
-                buf[835] = 1;
-                buf[836..840].copy_from_slice(&v.to_le_bytes());
-            }
-            Maybe::None => {
-                buf[835] = 0;
-            }
-        }
+        postcard::to_slice(self, &mut buf[1..]).unwrap();
 
-        // Checksum
-        let checksum = CASTAGNOLI.checksum(&buf[0..840]);
-        buf[840..844].copy_from_slice(&checksum.to_le_bytes());
+        let checksum = CASTAGNOLI.checksum(&buf[0..2044]);
+        buf[2044..2048].copy_from_slice(&checksum.to_le_bytes());
     }
 
     fn deserialize(buf: &[u8; CONFIG_LEN]) -> Option<Self> {
         match buf[0] {
             0 => Self::deserialize_v0(buf),
+            1 => Self::deserialize_v1(buf),
             _ => None,
         }
     }
 
+    fn deserialize_v1(buf: &[u8; CONFIG_LEN]) -> Option<Self> {
+        let expected_checksum = CASTAGNOLI.checksum(&buf[0..2044]);
+        let actual_checksum = u32::from_le_bytes(buf[2044..2048].try_into().unwrap());
+        if expected_checksum != actual_checksum {
+            return None;
+        }
+
+        postcard::from_bytes(&buf[1..2044]).ok()
+    }
+
     fn deserialize_v0(buf: &[u8; CONFIG_LEN]) -> Option<Self> {
         let expected_checksum = CASTAGNOLI.checksum(&buf[0..840]);
         let actual_checksum = u32::from_le_bytes(buf[840..844].try_into().unwrap());
@@ -205,6 +152,7 @@ impl Config {
         };
 
         Some(Config {
+            frequency: Frequency::default(),
             callsign,
             icon,
             ssid,
@@ -227,6 +175,7 @@ impl Config {
 impl Default for Config {
     fn default() -> Self {
         Config {
+            frequency: Frequency::default(),
             callsign: "N0CALL".try_into().unwrap(),
             icon: 0,
             ssid: 0,
@@ -246,7 +195,7 @@ impl Default for Config {
     }
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Serialize, Deserialize)]
 pub enum GpsSetting {
     Disabled,
     Fixed(f64, f64),
@@ -263,7 +212,7 @@ impl Display for GpsSetting {
     }
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Serialize, Deserialize)]
 pub enum MaybeBlackHole {
     Some(BlackHoleSetting),
     None,
@@ -281,19 +230,6 @@ impl MaybeBlackHole {
         }
     }
 
-    fn encode(&self, buf: &mut [u8; 25]) {
-        match self {
-            Self::None => {
-                buf[0] = 0;
-            }
-
-            Self::Some(bhs) => {
-                buf[0] = 1;
-                bhs.encode((&mut buf[1..]).try_into().unwrap());
-            }
-        }
-    }
-
     fn decode(buf: &[u8; 25]) -> Option<Self> {
         match buf[0] {
             0 => Some(Self::None),
@@ -314,7 +250,7 @@ impl Display for MaybeBlackHole {
     }
 }
 
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Serialize, Deserialize)]
 pub struct BlackHoleSetting {
     pub latitude: f64,
     pub longitude: f64,
@@ -322,12 +258,6 @@ pub struct BlackHoleSetting {
 }
 
 impl BlackHoleSetting {
-    fn encode(&self, buf: &mut [u8; 24]) {
-        buf[0..8].copy_from_slice(&self.latitude.to_le_bytes());
-        buf[8..16].copy_from_slice(&self.longitude.to_le_bytes());
-        buf[16..24].copy_from_slice(&self.radius_meters.to_le_bytes());
-    }
-
     fn decode(buf: &[u8; 24]) -> Self {
         let latitude = f64::from_le_bytes(buf[0..8].try_into().unwrap());
         let longitude = f64::from_le_bytes(buf[8..16].try_into().unwrap());
@@ -352,7 +282,7 @@ impl Display for BlackHoleSetting {
 }
 
 // Like Option, but local, so we can implement Display on it
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Serialize, Deserialize)]
 pub enum Maybe<T> {
     Some(T),
     None,
@@ -378,3 +308,44 @@ impl<T> From<Maybe<T>> for Option<T> {
         }
     }
 }
+
+#[derive(Copy, Clone, Serialize, Deserialize)]
+pub struct Frequency(u32);
+
+impl Frequency {
+    pub fn new(f: u32) -> Option<Self> {
+        if Self::validate_freq(f) {
+            Some(Self(f))
+        } else {
+            None
+        }
+    }
+
+    pub fn new_mhz(f: f64) -> Option<Self> {
+        let hz = u32::try_from((f * 1_000_000.0) as u64).ok()?;
+
+        Self::new(hz)
+    }
+
+    pub fn hz(&self) -> u32 {
+        self.0
+    }
+
+    fn validate_freq(f: u32) -> bool {
+        (420_000_000..=450_000_000).contains(&f)
+    }
+}
+
+impl Default for Frequency {
+    fn default() -> Self {
+        Self(DEFAULT_FREQUENCY)
+    }
+}
+
+impl Display for Frequency {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        let freq_mhz = self.0 as f64 / 1_000_000.0;
+
+        write!(f, "{freq_mhz:.3} MHz")
+    }
+}
diff --git a/src/radio.rs b/src/radio.rs
index eebb054..328d733 100644
--- a/src/radio.rs
+++ b/src/radio.rs
@@ -40,8 +40,7 @@ impl<'a> RadioManager<'a> {
     ) -> Option<Self> {
         let mut radio = Rf4463::new(spi, sdn, cs, delay, &mut RADIO_CONFIG_CATS.clone()).ok()?;
 
-        // sets us up for the default CATS frequency, 430.500 MHz
-        radio.set_channel(20);
+        radio.set_frequency(config.frequency.hz()).ok()?;
 
         let enable_digipeating = config.enable_digipeating;
         let seed = rand_seed_from_str(&config.callsign) ^ u64::from(config.ssid);
diff --git a/src/shell.rs b/src/shell.rs
index ff0e0a9..4bf8851 100644
--- a/src/shell.rs
+++ b/src/shell.rs
@@ -10,7 +10,7 @@ use usbd_serial::SerialPort;
 use ushell::{autocomplete::StaticAutocomplete, history::LRUHistory, Input, ShellError, UShell};
 
 use crate::{
-    config::{BlackHoleSetting, Config, GpsSetting, Maybe, MaybeBlackHole},
+    config::{BlackHoleSetting, Config, Frequency, GpsSetting, Maybe, MaybeBlackHole},
     voltage::VoltMeter,
 };
 
@@ -19,8 +19,8 @@ type ShellType =
 
 const SAVE_TEXT: &str =
     "Saved your settings. Remove USB and press the reset button when you're ready.\r\n\
-	 The board won't transmit when USB is attached, even if enable_transmit is true!\r\n\
-	 ";
+     The board won't transmit when USB is attached, even if enable_transmit is true!\r\n\
+     ";
 
 const HELP_TEXT: &str = concat!(
     "\r\n\
@@ -28,14 +28,14 @@ const HELP_TEXT: &str = concat!(
      Firmware version v",
     env!("CARGO_PKG_VERSION"),
     "\r\n\
-	 Available commands:\r\n\
-	 \r\n\
-	 help                       Display this text\r\n\r\n\
-	 get                        Show the current configuration\r\n\r\n\
-	 set [property] [value]     Update the current configuration\r\n                           For a list of properties, see the `get` command\r\n\r\n\
-	 save                       Save the current settings to flash memory for persistence\r\n\r\n\
-	 volts                      Show the current input voltage. Only reads from the screw terminals - will show ~0V if only connected to USB\r\n\
-	 "
+     Available commands:\r\n\
+     \r\n\
+     help                       Display this text\r\n\r\n\
+     get                        Show the current configuration\r\n\r\n\
+     set [property] [value]     Update the current configuration\r\n                           For a list of properties, see the `get` command\r\n\r\n\
+     save                       Save the current settings to flash memory for persistence\r\n\r\n\
+     volts                      Show the current input voltage. Only reads from the screw terminals - will show ~0V if only connected to USB\r\n\
+     "
 );
 
 pub struct Shell {
@@ -62,6 +62,7 @@ impl Shell {
                     match cmd {
                         "get" => {
                             let Config {
+                                frequency,
                                 callsign,
                                 icon,
                                 ssid,
@@ -88,27 +89,28 @@ impl Shell {
                             write!(
                                 ushell,
                                 "\r\n\
-							 callsign           {callsign}\r\n\
-							 ssid               {ssid}\r\n\
-							 icon               {icon}\r\n\
-							 max_hops           {max_hops}\r\n\
-							 comment            {comment}\r\n\
-							 transmit_period    {transmit_period_seconds} (seconds)\r\n\
-							 gps                {gps}\r\n\
-							 black_hole         {black_hole}\r\n\
-							 min_voltage        {min_voltage:.2}\r\n\
-							 enable_transmit    {enable_transmit}\r\n\
-							 enable_digipeating {enable_digipeating}\r\n\
-							 \r\n\
-							 The following settings do not change the behaviour of the transceiver.\r\n\
-							 Instead, they only change the contents of the NodeInfo whisker.\r\n\
-							 Info configuration:\r\n\
-							 antenna_height     {antenna_height:.1} (meters)\r\n\
-							 antenna_gain       {antenna_gain:.1} (dBi)\r\n\
-							 tx_power           {tx_power} (dBm)\r\n\
-							 \r\n\
-							 To change a setting, type `set <property> <value>`\r\n\
-							 For example, `set transmit_period 300`\r\n"
+                             frequency          {frequency}\r\n\
+                             callsign           {callsign}\r\n\
+                             ssid               {ssid}\r\n\
+                             icon               {icon}\r\n\
+                             max_hops           {max_hops}\r\n\
+                             comment            {comment}\r\n\
+                             transmit_period    {transmit_period_seconds} (seconds)\r\n\
+                             gps                {gps}\r\n\
+                             black_hole         {black_hole}\r\n\
+                             min_voltage        {min_voltage:.2}\r\n\
+                             enable_transmit    {enable_transmit}\r\n\
+                             enable_digipeating {enable_digipeating}\r\n\
+                             \r\n\
+                             The following settings do not change the behaviour of the transceiver.\r\n\
+                             Instead, they only change the contents of the NodeInfo whisker.\r\n\
+                             Info configuration:\r\n\
+                             antenna_height     {antenna_height:.1} (meters)\r\n\
+                             antenna_gain       {antenna_gain:.1} (dBi)\r\n\
+                             tx_power           {tx_power} (dBm)\r\n\
+                             \r\n\
+                             To change a setting, type `set <property> <value>`\r\n\
+                             For example, `set transmit_period 300`\r\n"
                             )
                             .ok();
                         }
@@ -116,6 +118,13 @@ impl Shell {
                             let (property, value) = args.split_once(' ').unwrap_or((args, ""));
 
                             let res = match property {
+                                "frequency" => match parse_frequency(value) {
+                                    Ok(v) => {
+                                        self.config.frequency = v;
+                                        Ok(())
+                                    }
+                                    Err(e) => Err(e),
+                                },
                                 "callsign" => {
                                     if value.trim().is_empty() {
                                         Err("Blank callsign is invalid")
@@ -231,9 +240,9 @@ impl Shell {
                             };
 
                             match res {
-								Ok(()) => write!(ushell, "\r\nOK. Don't forget to save your settings when you're done with `save`!\r\n").ok(),
-								Err(e) => write!(ushell, "\r\nError: {}\r\n", e).ok()
-							};
+                                Ok(()) => write!(ushell, "\r\nOK. Don't forget to save your settings when you're done with `save`!\r\n").ok(),
+                                Err(e) => write!(ushell, "\r\nError: {}\r\n", e).ok()
+                            };
                         }
                         "save" => {
                             write!(ushell, "\r\n").ok();
@@ -261,16 +270,20 @@ impl Shell {
                         }
                         // Used for QA only
                         "quicktest" => {
+                            let comment = ArrayString::from(
+                                "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. \
+                                 Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \
+                                 Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
+                                 Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, \
+                                 venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibu").unwrap();
+
                             self.config = Config {
+                                frequency: Frequency::new(430_450_000).unwrap(),
                                 callsign: args.try_into().unwrap(),
                                 icon: 0,
                                 ssid: 255,
                                 max_hops: 0,
-                                comment: ArrayString::from("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. \
-                                                            Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \
-                                                            Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
-                                                            Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, \
-                                                            venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibu").unwrap(),
+                                comment,
                                 transmit_period_seconds: 3,
                                 gps: GpsSetting::Receiver,
                                 enable_transmit: true,
@@ -279,7 +292,7 @@ impl Shell {
                                 antenna_height: Maybe::None,
                                 antenna_gain: Maybe::None,
                                 tx_power: Maybe::Some(30.0),
-								min_voltage: Maybe::Some(9.0),
+                                min_voltage: Maybe::Some(9.0),
                             };
                             self.config.save_to_flash(flash);
 
@@ -302,6 +315,12 @@ impl Shell {
     }
 }
 
+fn parse_frequency(val: &str) -> Result<Frequency, &'static str> {
+    const ERR: &str = "Invalid value. Expected a frequency between 420 and 450 MHz.";
+
+    val.parse().ok().and_then(Frequency::new_mhz).ok_or(ERR)
+}
+
 fn parse_arrstring<const N: usize>(val: &str) -> Result<ArrayString<N>, &'static str> {
     match val.try_into() {
         Ok(x) => Ok(x),
@@ -353,9 +372,9 @@ fn parse_maybe_f32(val: &str) -> Result<Maybe<f32>, &'static str> {
 
 fn parse_gps(val: &str) -> Result<GpsSetting, &'static str> {
     const GENERIC_ERR: &str = "Invalid value. Expected one of the following options:\r\n\
-							   The literal `disabled` (set gps disable) - Do not set GPS data in transmitted packets\r\n\
-							   The literal `receiver` (set gps receiver) - Use a connected GPS module\r\n\
-							   A lat/lon pair (set gps 12.34 56.78) - Use the specified fixed coordinates";
+                               The literal `disabled` (set gps disable) - Do not set GPS data in transmitted packets\r\n\
+                               The literal `receiver` (set gps receiver) - Use a connected GPS module\r\n\
+                               A lat/lon pair (set gps 12.34 56.78) - Use the specified fixed coordinates";
 
     match val {
         "disabled" => Ok(GpsSetting::Disabled),
@@ -372,8 +391,8 @@ fn parse_gps(val: &str) -> Result<GpsSetting, &'static str> {
 
 fn parse_black_hole(val: &str) -> Result<MaybeBlackHole, &'static str> {
     const ERR: &str = "Invalid value. Expected `lat lon radius`, where radius is in meters.\r\n\
-					   For example, `set black_hole 12.34 56.78 1000` to exclude 1000m around (12.34, 56.78).\r\n\
-					   Alternatively, clear this setting with `set black_hole none`.";
+                       For example, `set black_hole 12.34 56.78 1000` to exclude 1000m around (12.34, 56.78).\r\n\
+                       Alternatively, clear this setting with `set black_hole none`.";
 
     if val.trim().eq_ignore_ascii_case("none") {
         return Ok(MaybeBlackHole::None);
-- 
GitLab