diff --git a/Cargo.lock b/Cargo.lock index 0e05ccc2f..83f173eb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[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.6.7" @@ -425,6 +431,12 @@ dependencies = [ "paste", ] +[[package]] +name = "menu" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03d7f798bfe97329ad6df937951142eec93886b37d87010502dd25e8cc75fd5" + [[package]] name = "miniconf" version = "0.9.0" @@ -631,6 +643,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 = "proc-macro-error" version = "1.0.4" @@ -904,6 +927,7 @@ dependencies = [ "cortex-m-rt", "cortex-m-rtic", "embedded-hal", + "embedded-storage", "enum-iterator", "fugit", "heapless", @@ -911,12 +935,14 @@ dependencies = [ "lm75", "log", "mcp230xx", + "menu", "miniconf", "minimq", "mono-clock", "mutex-trait", "num_enum 0.7.1", "paste", + "postcard", "rand_core", "rand_xorshift", "rtt-logger", diff --git a/Cargo.toml b/Cargo.toml index e1f95e4da..9e9531bbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ default-target = "thumbv7em-none-eabihf" members = ["ad9959"] [dependencies] +embedded-storage = "0.3" +menu = "0.3" cortex-m = { version = "0.7.7", features = ["inline-asm", "critical-section-single-core"] } cortex-m-rt = { version = "0.7", features = ["device"] } log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } @@ -67,6 +69,7 @@ usbd-serial = "0.1.1" miniconf = "0.9.0" smoltcp-nal = { version = "0.4.1", features = ["shared-stack"]} bbqueue = "0.5" +postcard = "1" [dependencies.stm32h7xx-hal] version = "0.15.1" diff --git a/book/src/setup.md b/book/src/setup.md index e00d26bac..1b280ee23 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -40,10 +40,8 @@ Stabilizer requires an MQTT broker that supports MQTTv5. The MQTT broker is used to distribute and exchange elemetry data and to view/change application settings. The broker must be reachable by both the host-side applications used to interact with the application on Stabilizer and by the application running on Stabilizer. -Determine the IPv4 address of the broker as seen from the network Stabilizer is -connected to. The broker IP address must be stable. It will be used later -during firmware build. -The broker must be reachable on port 1883 on that IP address. +The broker must be reachable on port 1883 on that IP address - it may either be an IP address or a +fully qualified domain name. Firewalls between Stabilizer and the broker may need to be configured to allow connections from Stabilizer to that port and IP address. @@ -83,23 +81,21 @@ docker run -p 1883:1883 --name mosquitto -v ${pwd}/mosquitto.conf:/mosquitto/con git clone https://github.com/quartiq/stabilizer cd stabilizer ``` -5. Build firmware specifying the MQTT broker IP. Replace `10.34.16.1` by the - stable and reachable broker IPv4 address determined above. +5. Build firmware ```bash # Bash - BROKER="10.34.16.1" cargo build --release + cargo build --release # Powershell - # Note: This sets the broker for all future builds as well. - $env:BROKER='10.34.16.1'; cargo build --release + cargo build --release ``` 6. Extract the application binary (substitute `dual-iir` below with the desired application name) ```bash # Bash - BROKER="10.34.16.1" cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin + cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin # Powershell - $env:BROKER='10.34.16.1'; cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin + cargo objcopy --release --bin dual-iir -- -O binary dual-iir.bin ``` ## Flashing @@ -163,16 +159,29 @@ described [above](#st-link-virtual-mass-storage). 2. Build and run firmware on the device ```bash # Bash - BROKER="10.34.16.1" cargo run --release --bin dual-iir + cargo run --release --bin dual-iir # Powershell - $Env:BROKER='10.34.16.1'; cargo run --release --bin dual-iir + cargo run --release --bin dual-iir ``` When using debug (non `--release`) mode, decrease the sampling frequency significantly. The added error checking code and missing optimizations may lead to the application missing timer deadlines and panicing. +## Set the MQTT broker + +The MQTT broker can be configured via the USB port on Stabilizer's front. Connect a USB cable and +open up the serial port in a serial terminal of your choice. `pyserial` provides a simple, +easy-to-use terminal emulator: +```sh +python -m serial +``` + +Once you have opened the port, you can use the provided menu to update the MQTT broker address. The +address can be an IP address or a domain name. Once the broker has been updated, power cycle +stabilizer to have the new broker address take effect. + ## Verify MQTT connection Once your MQTT broker and Stabilizer are both running, verify that the application @@ -190,7 +199,8 @@ Broker. ![MQTT Explorer Configuration](assets/mqtt-explorer.png) -> **Note:** In MQTT explorer, use the same broker address that you used when building the firmware. +> **Note:** In MQTT explorer, use the same broker address that you set in the Stabilizer serial +> terminal. In addition to the `alive` status, telemetry messages are published at regular intervals when Stabilizer has connected to the broker. Once you observe incoming telemetry, diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index 5c27e50d4..2479985a2 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -208,7 +208,7 @@ mod app { let clock = SystemTimer::new(|| monotonics::now().ticks() as u32); // Configure the microcontroller - let (stabilizer, _pounder) = hardware::setup::setup( + let (mut stabilizer, _pounder) = hardware::setup::setup( c.core, c.device, clock, @@ -216,13 +216,14 @@ mod app { SAMPLE_TICKS, ); + let flash = stabilizer.usb_serial.flash(); let mut network = NetworkUsers::new( stabilizer.net.stack, stabilizer.net.phy, clock, env!("CARGO_BIN_NAME"), - stabilizer.net.mac_address, - option_env!("BROKER").unwrap_or("mqtt"), + &flash.settings.broker, + &flash.settings.id, ); let generator = network.configure_streaming(StreamFormat::AdcDacData); diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index e89731171..e0accc39e 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -256,13 +256,14 @@ mod app { SAMPLE_TICKS, ); + let flash = stabilizer.usb_serial.flash(); let mut network = NetworkUsers::new( stabilizer.net.stack, stabilizer.net.phy, clock, env!("CARGO_BIN_NAME"), - stabilizer.net.mac_address, - option_env!("BROKER").unwrap_or("mqtt"), + &flash.settings.broker, + &flash.settings.id, ); let generator = network.configure_streaming(StreamFormat::AdcDacData); diff --git a/src/hardware/flash.rs b/src/hardware/flash.rs new file mode 100644 index 000000000..60c73d991 --- /dev/null +++ b/src/hardware/flash.rs @@ -0,0 +1,65 @@ +use core::fmt::Write; +use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; +use stm32h7xx_hal::flash::{LockedFlashBank, UnlockedFlashBank}; + +#[derive(Clone, serde::Serialize, serde::Deserialize, miniconf::Tree)] +pub struct Settings { + pub broker: heapless::String<255>, + pub id: heapless::String<23>, + #[serde(skip)] + #[tree(skip)] + pub mac: smoltcp_nal::smoltcp::wire::EthernetAddress, +} + +impl Settings { + fn new(mac: smoltcp_nal::smoltcp::wire::EthernetAddress) -> Self { + let mut id = heapless::String::new(); + write!(&mut id, "{mac}").unwrap(); + + Self { + broker: "mqtt".into(), + id, + mac, + } + } + + pub fn reset(&mut self) { + *self = Self::new(self.mac) + } +} + +pub struct FlashSettings { + flash: LockedFlashBank, + pub settings: Settings, +} + +impl FlashSettings { + pub fn new( + mut flash: LockedFlashBank, + mac: smoltcp_nal::smoltcp::wire::EthernetAddress, + ) -> Self { + let mut buffer = [0u8; 512]; + flash.read(0, &mut buffer[..]).unwrap(); + + let settings = match postcard::from_bytes(&buffer) { + Ok(settings) => settings, + Err(_) => { + log::warn!( + "Failed to load settings from flash. Using defaults" + ); + Settings::new(mac) + } + }; + + Self { flash, settings } + } + + pub fn save(&mut self) { + let mut bank = self.flash.unlocked(); + + let mut data = [0; 512]; + let serialized = postcard::to_slice(&self.settings, &mut data).unwrap(); + bank.erase(0, UnlockedFlashBank::ERASE_SIZE as u32).unwrap(); + bank.write(0, serialized).unwrap(); + } +} diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs index 3f58bb8e5..a75cd1f39 100644 --- a/src/hardware/mod.rs +++ b/src/hardware/mod.rs @@ -9,6 +9,7 @@ pub mod cpu_temp_sensor; pub mod dac; pub mod delay; pub mod design_parameters; +pub mod flash; pub mod input_stamper; pub mod pounder; pub mod serial_terminal; diff --git a/src/hardware/serial_terminal.rs b/src/hardware/serial_terminal.rs index 649ad2836..3f5ec6dcb 100644 --- a/src/hardware/serial_terminal.rs +++ b/src/hardware/serial_terminal.rs @@ -1,5 +1,73 @@ use super::UsbBus; +use crate::hardware::flash::FlashSettings; +use crate::hardware::flash::Settings; use core::fmt::Write; +use miniconf::{JsonCoreSlash, TreeKey}; + +struct Context { + output: OutputBuffer, + flash: FlashSettings, +} + +const ROOT_MENU: menu::Menu = menu::Menu { + label: "root", + items: &[ + &menu::Item { + command: "reboot", + help: Some("Reboot the device to force new settings to take effect."), + item_type: menu::ItemType::Callback { + function: handle_reboot, + parameters: &[] + }, + }, + &menu::Item { + command: "factory-reset", + help: Some("Reset the device settings to default values."), + item_type: menu::ItemType::Callback { + function: handle_reset, + parameters: &[] + }, + }, + &menu::Item { + command: "list", + help: Some("List all available settings and their current values."), + item_type: menu::ItemType::Callback { + function: handle_list, + parameters: &[], + }, + }, + &menu::Item { + command: "get", + help: Some("Read a setting_from the device."), + item_type: menu::ItemType::Callback { + function: handle_get, + parameters: &[menu::Parameter::Mandatory { + parameter_name: "item", + help: Some("The name of the setting to read."), + }] + }, + }, + &menu::Item { + command: "set", + help: Some("Update a a setting in the device."), + item_type: menu::ItemType::Callback { + function: handle_set, + parameters: &[ + menu::Parameter::Mandatory { + parameter_name: "item", + help: Some("The name of the setting to write."), + }, + menu::Parameter::Mandatory { + parameter_name: "value", + help: Some("Specifies the value to be written. Values must be JSON-encoded"), + }, + ] + }, + }, + ], + entry: None, + exit: None, +}; static OUTPUT_BUFFER: bbqueue::BBBuffer<512> = bbqueue::BBBuffer::new(); @@ -25,28 +93,146 @@ impl Write for OutputBuffer { } } +impl core::fmt::Write for Context { + /// Write data to the serial terminal. + /// + /// # Note + /// The terminal uses an internal buffer. Overflows of the output buffer are silently ignored. + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.output.write_str(s) + } +} + +fn handle_list( + _menu: &menu::Menu, + _item: &menu::Item, + _args: &[&str], + context: &mut Context, +) { + writeln!(context, "Available properties:").unwrap(); + + let mut defaults = context.flash.settings.clone(); + defaults.reset(); + + let mut buf = [0; 256]; + let mut default_buf = [0; 256]; + for path in Settings::iter_paths::>("/") { + let path = path.unwrap(); + let current_value = { + let len = context.flash.settings.get_json(&path, &mut buf).unwrap(); + core::str::from_utf8(&buf[..len]).unwrap() + }; + let default_value = { + let len = defaults.get_json(&path, &mut default_buf).unwrap(); + core::str::from_utf8(&default_buf[..len]).unwrap() + }; + writeln!( + context, + "{path}: {current_value} [default: {default_value}]" + ) + .unwrap(); + } +} + +fn handle_reboot( + _menu: &menu::Menu, + _item: &menu::Item, + _args: &[&str], + _context: &mut Context, +) { + cortex_m::peripheral::SCB::sys_reset(); +} + +fn handle_reset( + _menu: &menu::Menu, + _item: &menu::Item, + _args: &[&str], + context: &mut Context, +) { + context.flash.settings.reset(); + context.flash.save(); +} + +fn handle_get( + _menu: &menu::Menu, + item: &menu::Item, + args: &[&str], + context: &mut Context, +) { + let mut buf = [0u8; 256]; + let key = menu::argument_finder(item, args, "item").unwrap().unwrap(); + let len = match context.flash.settings.get_json(key, &mut buf) { + Err(e) => { + writeln!(context, "Failed to read {key}: {e}").unwrap(); + return; + } + Ok(len) => len, + }; + + let stringified = core::str::from_utf8(&buf[..len]).unwrap(); + writeln!(context, "{key}: {stringified}").unwrap(); +} + +fn handle_set( + _menu: &menu::Menu, + item: &menu::Item, + args: &[&str], + context: &mut Context, +) { + let key = menu::argument_finder(item, args, "item").unwrap().unwrap(); + let value = menu::argument_finder(item, args, "value").unwrap().unwrap(); + + // Now, write the new value into memory. + // TODO: Validate it first? + match context.flash.settings.set_json(key, value.as_bytes()) { + Ok(_) => { + context.flash.save(); + writeln!( + context, + "Settings in memory may differ from currently operating settings. \ + Reset device to apply settings." + ) + .unwrap(); + } + Err(e) => { + writeln!(context, "Failed to update {key}: {e:?}").unwrap(); + } + } +} + pub struct SerialTerminal { usb_device: usb_device::device::UsbDevice<'static, UsbBus>, usb_serial: usbd_serial::SerialPort<'static, UsbBus>, + menu: menu::Runner<'static, Context>, output: bbqueue::Consumer<'static, 512>, - buffer: OutputBuffer, } impl SerialTerminal { pub fn new( usb_device: usb_device::device::UsbDevice<'static, UsbBus>, usb_serial: usbd_serial::SerialPort<'static, UsbBus>, + flash: FlashSettings, ) -> Self { let (producer, consumer) = OUTPUT_BUFFER.try_split().unwrap(); + let input_buffer = + cortex_m::singleton!(: [u8; 256] = [0; 256]).unwrap(); + let context = Context { + output: OutputBuffer { producer }, + flash, + }; Self { - buffer: OutputBuffer { producer }, + menu: menu::Runner::new(&ROOT_MENU, input_buffer, context), usb_device, usb_serial, output: consumer, } } + pub fn flash(&mut self) -> &mut FlashSettings { + &mut self.menu.context.flash + } + fn flush(&mut self) { let read = match self.output.read() { Ok(grant) => grant, @@ -79,17 +265,20 @@ impl SerialTerminal { match self.usb_serial.read(&mut buffer) { Ok(count) => { for &value in &buffer[..count] { - writeln!(self.buffer, "echo: {}", value as char).unwrap(); + self.menu.input_byte(value); } } Err(usbd_serial::UsbError::WouldBlock) => {} Err(_) => { - // Clear the output buffer if USB is not connected. - while let Ok(grant) = self.output.read() { - let len = grant.buf().len(); - grant.release(len); - } + self.menu.prompt(true); + self.output + .read() + .map(|grant| { + let len = grant.buf().len(); + grant.release(len); + }) + .ok(); } } } diff --git a/src/hardware/setup.rs b/src/hardware/setup.rs index 0873aca43..2e825c724 100644 --- a/src/hardware/setup.rs +++ b/src/hardware/setup.rs @@ -14,7 +14,7 @@ use smoltcp_nal::smoltcp; use super::{ adc, afe, cpu_temp_sensor::CpuTempSensor, dac, delay, design_parameters, - eeprom, input_stamper::InputStamper, pounder, + eeprom, flash::FlashSettings, input_stamper::InputStamper, pounder, pounder::dds_output::DdsOutput, serial_terminal::SerialTerminal, shared_adc::SharedAdc, timers, DigitalInput0, DigitalInput1, EemDigitalInput0, EemDigitalInput1, EemDigitalOutput0, EemDigitalOutput1, @@ -1070,6 +1070,11 @@ pub fn setup( (usb_device, serial) }; + let (_, flash_bank2) = device.FLASH.split(); + + let settings = + FlashSettings::new(flash_bank2.unwrap(), network_devices.mac_address); + let stabilizer = StabilizerDevices { systick, afes, @@ -1084,7 +1089,7 @@ pub fn setup( timestamp_timer, digital_inputs, eem_gpio, - usb_serial: SerialTerminal::new(usb_device, usb_serial), + usb_serial: SerialTerminal::new(usb_device, usb_serial, settings), }; // info!("Version {} {}", build_info::PKG_VERSION, build_info::GIT_VERSION.unwrap()); diff --git a/src/net/mod.rs b/src/net/mod.rs index 7e3b7b66b..884bd912a 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -84,7 +84,6 @@ where /// * `phy` - The ethernet PHY connecting the network. /// * `clock` - A `SystemTimer` implementing `Clock`. /// * `app` - The name of the application. - /// * `mac` - The MAC address of the network. /// * `broker` - The domain name of the MQTT broker to use. /// /// # Returns @@ -94,8 +93,8 @@ where phy: EthernetPhy, clock: SystemTimer, app: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, broker: &str, + id: &str, ) -> Self { let stack_manager = cortex_m::singleton!(: NetworkManager = NetworkManager::new(stack)) @@ -104,7 +103,7 @@ where let processor = NetworkProcessor::new(stack_manager.acquire_stack(), phy); - let prefix = get_device_prefix(app, mac); + let prefix = get_device_prefix(app, id); let store = cortex_m::singleton!(: MqttStorage = MqttStorage::default()) @@ -124,7 +123,7 @@ where named_broker, &mut store.settings, ) - .client_id(&get_client_id(app, "settings", mac)) + .client_id(&get_client_id(id, "settings")) .unwrap(), ) .unwrap(); @@ -141,7 +140,7 @@ where // The telemetry client doesn't receive any messages except MQTT control packets. // As such, we don't need much of the buffer for RX. .rx_buffer(minimq::config::BufferConfig::Maximum(100)) - .client_id(&get_client_id(app, "tlm", mac)) + .client_id(&get_client_id(id, "tlm")) .unwrap(), ); @@ -218,19 +217,14 @@ where /// Get an MQTT client ID for a client. /// /// # Args -/// * `app` - The name of the application -/// * `client` - The unique tag of the client -/// * `mac` - The MAC address of the device. +/// * `id` - The base client ID +/// * `mode` - The operating mode of this client. (i.e. tlm, settings) /// /// # Returns /// A client ID that may be used for MQTT client identification. -fn get_client_id( - app: &str, - client: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, -) -> String<64> { +fn get_client_id(id: &str, mode: &str) -> String<64> { let mut identifier = String::new(); - write!(&mut identifier, "{app}-{mac}-{client}").unwrap(); + write!(&mut identifier, "{id}-{mode}").unwrap(); identifier } @@ -238,18 +232,15 @@ fn get_client_id( /// /// # Args /// * `app` - The name of the application that is executing. -/// * `mac` - The ethernet MAC address of the device. +/// * `id` - The MQTT ID of the device. /// /// # Returns /// The MQTT prefix used for this device. -pub fn get_device_prefix( - app: &str, - mac: smoltcp_nal::smoltcp::wire::EthernetAddress, -) -> String<128> { +pub fn get_device_prefix(app: &str, id: &str) -> String<128> { // Note(unwrap): The mac address + binary name must be short enough to fit into this string. If // they are defined too long, this will panic and the device will fail to boot. let mut prefix: String<128> = String::new(); - write!(&mut prefix, "dt/sinara/{app}/{mac}").unwrap(); + write!(&mut prefix, "dt/sinara/{app}/{id}").unwrap(); prefix }