Repository: quartiq/stabilizer (language: rust)

Issue:
support persistent crash and firmware/hardware info
`service` platform command like on booster
potentially check and synchronize how the mqtt `/alive` topic can be improved with this info as well.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e61cb6979..5b9aafb6f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,7 +53,7 @@ jobs:
     strategy:
       matrix:
         # keep MSRV in sync in ci.yaml and Cargo.toml
-        toolchain: [stable, '1.65.0']
+        toolchain: [stable, '1.66.0']
         features: ['']
         continue-on-error: [false]
         include:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1549acf6c..b226864c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,9 +10,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Added
 * Serial terminal is available on USB for settings configurations
 * Reboot to DFU support added via the serial terminal for remote bootloading
+* The `meta` topic now contains metadata about the compiler, firmware, and hardware similar to
+Booster
+* Panic information is now persisted after reboot and available via telemetry and the USB serial
+console.
 
 ### Changed
 * Broker is no longer configured at compile time, but is maintained in device memory
+* MSRV bumped to v1.66
 
 ## [0.9.0](https://github.com/quartiq/stabilizer/compare/v0.8.1...v0.9.0)
 
diff --git a/Cargo.lock b/Cargo.lock
index 1e17444b2..ce2bf5c66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -102,6 +102,15 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
 
+[[package]]
+name = "built"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53"
+dependencies = [
+ "git2",
+]
+
 [[package]]
 name = "bytemuck"
 version = "1.14.0"
@@ -120,6 +129,16 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "jobserver",
+ "libc",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -283,6 +302,15 @@ dependencies = [
  "syn 2.0.38",
 ]
 
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
 [[package]]
 name = "fugit"
 version = "0.3.7"
@@ -326,6 +354,19 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "git2"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd"
+dependencies = [
+ "bitflags 2.4.1",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "url",
+]
+
 [[package]]
 name = "hash32"
 version = "0.2.1"
@@ -374,6 +415,16 @@ dependencies = [
  "stable_deref_trait",
 ]
 
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
 [[package]]
 name = "idsp"
 version = "0.13.0"
@@ -401,12 +452,51 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
 
+[[package]]
+name = "jobserver"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
+
+[[package]]
+name = "libgit2-sys"
+version = "0.16.1+1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "pkg-config",
+]
+
 [[package]]
 name = "libm"
 version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
+[[package]]
+name = "libz-sys"
+version = "1.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "lm75"
 version = "0.2.0"
@@ -676,12 +766,33 @@ dependencies = [
  "syn 2.0.38",
 ]
 
+[[package]]
+name = "panic-persist"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32bb382689ecd2c4d2d4df9fd56700ba8d43b7b31cca11018cc0e6f8aef39fd5"
+dependencies = [
+ "cortex-m 0.7.7",
+]
+
 [[package]]
 name = "paste"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
 
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
 [[package]]
 name = "portable-atomic"
 version = "1.5.1"
@@ -987,6 +1098,8 @@ version = "0.9.0"
 dependencies = [
  "ad9959",
  "bbqueue",
+ "bit_field",
+ "built",
  "cortex-m 0.7.7",
  "cortex-m-rt",
  "cortex-m-rtic",
@@ -1005,6 +1118,7 @@ dependencies = [
  "mono-clock",
  "mutex-trait",
  "num_enum 0.7.1",
+ "panic-persist",
  "paste",
  "postcard",
  "rand_core",
@@ -1107,6 +1221,21 @@ dependencies = [
  "rtic-monotonic",
 ]
 
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
 [[package]]
 name = "typenum"
 version = "1.17.0"
@@ -1119,12 +1248,38 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69"
 
+[[package]]
+name = "unicode-bidi"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
 [[package]]
 name = "usb-device"
 version = "0.3.1"
@@ -1139,7 +1294,7 @@ dependencies = [
 [[package]]
 name = "usbd-serial"
 version = "0.2.0"
-source = "git+https://github.com/rust-embedded-community/usbd-serial#6611dc092822c60b4482e28aab2fe084c1756410"
+source = "git+https://github.com/rust-embedded-community/usbd-serial?rev=096742c1c480f6f63c1a936a3c23ede7993c624d#096742c1c480f6f63c1a936a3c23ede7993c624d"
 dependencies = [
  "embedded-hal",
  "embedded-io",
@@ -1159,6 +1314,12 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
diff --git a/Cargo.toml b/Cargo.toml
index 679e75e92..4e1db96cf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,8 @@ readme = "README.md"
 documentation = "https://docs.rs/stabilizer/"
 edition = "2021"
 # keep MSRV in sync in ci.yaml and Cargo.toml
-rust-version = "1.65"
+rust-version = "1.66"
+build = "build.rs"
 exclude = [
 	".gitignore",
 	"doc/",
@@ -34,6 +35,7 @@ default-target = "thumbv7em-none-eabihf"
 members = ["ad9959", "serial-settings"]
 
 [dependencies]
+panic-persist = { version = "0.3", features = ["utf8", "custom-panic-handler"] }
 sequential-storage = "0.6"
 embedded-io = "0.6"
 embedded-storage = "0.3"
@@ -71,6 +73,10 @@ miniconf = "0.9.0"
 smoltcp-nal = { version = "0.4.1", features = ["shared-stack"]}
 bbqueue = "0.5"
 postcard = "1"
+bit_field = "0.10.2"
+
+[build-dependencies]
+built = { version = "0.7", features = ["git2"], default-features = false }
 
 [dependencies.stm32h7xx-hal]
 version = "0.15.1"
@@ -80,6 +86,7 @@ features = ["stm32h743v", "rt", "ethernet", "xspi", "usb_hs"]
 
 [patch.crates-io.usbd-serial]
 git = "https://github.com/rust-embedded-community/usbd-serial"
+rev = "096742c1c480f6f63c1a936a3c23ede7993c624d"
 
 [features]
 nightly = [ ]
diff --git a/build.rs b/build.rs
index e71f6a660..f57ef7a69 100644
--- a/build.rs
+++ b/build.rs
@@ -1,3 +1,5 @@
 fn main() {
+    built::write_built_file()
+        .expect("Failed to acquire build-time information");
     println!("cargo:rerun-if-changed=memory.x");
 }
diff --git a/memory.x b/memory.x
index ff76fc918..dfa174802 100644
--- a/memory.x
+++ b/memory.x
@@ -10,10 +10,16 @@ MEMORY
   RAM_B  (rwx) : ORIGIN = 0x38800000, LENGTH = 4K
   FLASH  (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
   FLASH1 (rx)  : ORIGIN = 0x08100000, LENGTH = 1024K
-  BOOTFLAG_RAM: ORIGIN = 0x2001FC00, LENGTH = 1K
+  PERSISTENT_RAM: ORIGIN = 0x2001FC00, LENGTH = 1K
 }
 
-_bootflag = ORIGIN(BOOTFLAG_RAM);
+/*
+ * Persistent memory has a u32 bootflag at the beginning and then the remainder is used for
+ * persisting panic information between boots.
+ */
+_bootflag = ORIGIN(PERSISTENT_RAM);
+_panic_dump_start = ORIGIN(PERSISTENT_RAM) + 4;
+_panic_dump_end = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM) - 4;
 
 SECTIONS {
   .axisram (NOLOAD) : ALIGN(8) {
diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs
index 92a973f82..70e2861fe 100644
--- a/src/bin/dual-iir.rs
+++ b/src/bin/dual-iir.rs
@@ -225,6 +225,7 @@ mod app {
             env!("CARGO_BIN_NAME"),
             &settings.broker,
             &settings.id,
+            stabilizer.metadata,
         );
 
         let generator = network.configure_streaming(StreamFormat::AdcDacData);
diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs
index 7959c5af9..c4237c8be 100644
--- a/src/bin/lockin.rs
+++ b/src/bin/lockin.rs
@@ -265,6 +265,7 @@ mod app {
             env!("CARGO_BIN_NAME"),
             &settings.broker,
             &settings.id,
+            stabilizer.metadata,
         );
 
         let generator = network.configure_streaming(StreamFormat::AdcDacData);
diff --git a/src/hardware/metadata.rs b/src/hardware/metadata.rs
new file mode 100644
index 000000000..84cd632a9
--- /dev/null
+++ b/src/hardware/metadata.rs
@@ -0,0 +1,41 @@
+use crate::hardware::HardwareVersion;
+use serde::Serialize;
+
+mod build_info {
+    include!(concat!(env!("OUT_DIR"), "/built.rs"));
+}
+
+#[derive(Serialize)]
+pub struct ApplicationMetadata {
+    pub firmware_version: &'static str,
+    pub rust_version: &'static str,
+    pub profile: &'static str,
+    pub git_dirty: bool,
+    pub features: &'static str,
+    pub panic_info: &'static str,
+    pub hardware_version: HardwareVersion,
+}
+
+impl ApplicationMetadata {
+    /// Construct the global metadata.
+    ///
+    /// # Note
+    /// This may only be called once.
+    ///
+    /// # Args
+    /// * `hardware_version` - The hardware version detected.
+    ///
+    /// # Returns
+    /// A reference to the global metadata.
+    pub fn new(version: HardwareVersion) -> &'static ApplicationMetadata {
+        cortex_m::singleton!(: ApplicationMetadata = ApplicationMetadata {
+            firmware_version: build_info::GIT_VERSION.unwrap_or("Unspecified"),
+            rust_version: build_info::RUSTC_VERSION,
+            profile: build_info::PROFILE,
+            git_dirty: build_info::GIT_DIRTY.unwrap_or(false),
+            features: build_info::FEATURES_STR,
+            hardware_version: version,
+            panic_info: panic_persist::get_panic_message_utf8().unwrap_or("None"),
+        }).unwrap()
+    }
+}
diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs
index db95a64dd..477192466 100644
--- a/src/hardware/mod.rs
+++ b/src/hardware/mod.rs
@@ -12,6 +12,7 @@ pub mod design_parameters;
 mod eeprom;
 pub mod flash;
 pub mod input_stamper;
+pub mod metadata;
 pub mod platform;
 pub mod pounder;
 pub mod setup;
@@ -86,6 +87,53 @@ pub type I2c1Proxy =
 pub type SerialTerminal =
     serial_settings::Runner<'static, crate::settings::SerialSettingsPlatform>;
 
+pub enum HardwareVersion {
+    Rev1_0,
+    Rev1_1,
+    Rev1_2,
+    Rev1_3,
+    Unknown(u8),
+}
+
+impl From<u8> for HardwareVersion {
+    fn from(bitfield: u8) -> Self {
+        match bitfield {
+            0b000 => HardwareVersion::Rev1_0,
+            0b001 => HardwareVersion::Rev1_1,
+            0b010 => HardwareVersion::Rev1_2,
+            0b011 => HardwareVersion::Rev1_3,
+            other => HardwareVersion::Unknown(other),
+        }
+    }
+}
+
+impl core::fmt::Display for HardwareVersion {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            HardwareVersion::Rev1_0 => write!(f, "v1.0"),
+            HardwareVersion::Rev1_1 => write!(f, "v1.1"),
+            HardwareVersion::Rev1_2 => write!(f, "v1.2"),
+            HardwareVersion::Rev1_3 => write!(f, "v1.3"),
+            HardwareVersion::Unknown(other) => {
+                write!(f, "Unknown ({:#b})", other)
+            }
+        }
+    }
+}
+
+impl serde::Serialize for HardwareVersion {
+    fn serialize<S: serde::Serializer>(
+        &self,
+        serializer: S,
+    ) -> Result<S::Ok, S::Error> {
+        use core::fmt::Write;
+
+        let mut version_string: heapless::String<32> = heapless::String::new();
+        write!(&mut version_string, "{}", self).unwrap();
+        serializer.serialize_str(&version_string)
+    }
+}
+
 #[inline(never)]
 #[panic_handler]
 fn panic(info: &core::panic::PanicInfo) -> ! {
@@ -115,6 +163,8 @@ fn panic(info: &core::panic::PanicInfo) -> ! {
         writeln!(channel, "{info}").ok();
     }
 
+    panic_persist::report_panic_info(info);
+
     // Abort
     asm::udf();
     // Halt
diff --git a/src/hardware/setup.rs b/src/hardware/setup.rs
index f493bad8f..2733edaaa 100644
--- a/src/hardware/setup.rs
+++ b/src/hardware/setup.rs
@@ -1,6 +1,7 @@
 //! Stabilizer hardware configuration
 //!
 //! This file contains all of the hardware-specific configuration of Stabilizer.
+use bit_field::BitField;
 use core::sync::atomic::{self, AtomicBool, Ordering};
 use core::{fmt::Write, ptr, slice};
 use stm32h7xx_hal::{
@@ -14,11 +15,12 @@ use smoltcp_nal::smoltcp;
 
 use super::{
     adc, afe, cpu_temp_sensor::CpuTempSensor, dac, delay, design_parameters,
-    eeprom, input_stamper::InputStamper, platform, pounder,
-    pounder::dds_output::DdsOutput, shared_adc::SharedAdc, timers,
-    DigitalInput0, DigitalInput1, EemDigitalInput0, EemDigitalInput1,
-    EemDigitalOutput0, EemDigitalOutput1, EthernetPhy, NetworkStack,
-    SerialTerminal, SystemTimer, Systick, UsbBus, UsbDevice, AFE0, AFE1,
+    eeprom, input_stamper::InputStamper, metadata::ApplicationMetadata,
+    platform, pounder, pounder::dds_output::DdsOutput, shared_adc::SharedAdc,
+    timers, DigitalInput0, DigitalInput1, EemDigitalInput0, EemDigitalInput1,
+    EemDigitalOutput0, EemDigitalOutput1, EthernetPhy, HardwareVersion,
+    NetworkStack, SerialTerminal, SystemTimer, Systick, UsbBus, UsbDevice,
+    AFE0, AFE1,
 };
 
 const NUM_TCP_SOCKETS: usize = 4;
@@ -119,6 +121,7 @@ pub struct StabilizerDevices {
     pub eem_gpio: EemGpioDevices,
     pub usb_serial: SerialTerminal,
     pub usb: UsbDevice,
+    pub metadata: &'static ApplicationMetadata,
 }
 
 /// The available Pounder-specific hardware interfaces.
@@ -596,6 +599,25 @@ pub fn setup(
         )
     };
 
+    let metadata = {
+        // Read the hardware version pins.
+        let hardware_version = {
+            let hwrev0 = gpiog.pg0.into_pull_down_input();
+            let hwrev1 = gpiog.pg1.into_pull_down_input();
+            let hwrev2 = gpiog.pg2.into_pull_down_input();
+            let hwrev3 = gpiog.pg3.into_pull_down_input();
+
+            HardwareVersion::from(
+                *0u8.set_bit(0, hwrev0.is_high())
+                    .set_bit(1, hwrev1.is_high())
+                    .set_bit(2, hwrev2.is_high())
+                    .set_bit(3, hwrev3.is_high()),
+            )
+        };
+
+        ApplicationMetadata::new(hardware_version)
+    };
+
     let mac_addr = smoltcp::wire::EthernetAddress(eeprom::read_eui48(
         &mut eeprom_i2c,
         &mut delay,
@@ -1098,6 +1120,7 @@ pub fn setup(
                 ),
                 storage,
                 settings,
+                metadata,
             },
             input_buffer,
             serialize_buffer,
@@ -1121,6 +1144,7 @@ pub fn setup(
         eem_gpio,
         usb: usb_device,
         usb_serial,
+        metadata,
     };
 
     // info!("Version {} {}", build_info::PKG_VERSION, build_info::GIT_VERSION.unwrap());
diff --git a/src/net/mod.rs b/src/net/mod.rs
index 884bd912a..4248049dd 100644
--- a/src/net/mod.rs
+++ b/src/net/mod.rs
@@ -13,7 +13,10 @@ pub mod data_stream;
 pub mod network_processor;
 pub mod telemetry;
 
-use crate::hardware::{EthernetPhy, NetworkManager, NetworkStack, SystemTimer};
+use crate::hardware::{
+    metadata::ApplicationMetadata, EthernetPhy, NetworkManager, NetworkStack,
+    SystemTimer,
+};
 use data_stream::{DataStream, FrameGenerator};
 use network_processor::NetworkProcessor;
 use telemetry::TelemetryClient;
@@ -85,6 +88,8 @@ where
     /// * `clock` - A `SystemTimer` implementing `Clock`.
     /// * `app` - The name of the application.
     /// * `broker` - The domain name of the MQTT broker to use.
+    /// * `id` - The MQTT client ID base to use.
+    /// * `metadata` - The application metadata
     ///
     /// # Returns
     /// A new struct of network users.
@@ -95,6 +100,7 @@ where
         app: &str,
         broker: &str,
         id: &str,
+        metadata: &'static ApplicationMetadata,
     ) -> Self {
         let stack_manager =
             cortex_m::singleton!(: NetworkManager = NetworkManager::new(stack))
@@ -144,7 +150,7 @@ where
                 .unwrap(),
         );
 
-        let telemetry = TelemetryClient::new(mqtt, &prefix);
+        let telemetry = TelemetryClient::new(mqtt, &prefix, metadata);
 
         let (generator, stream) =
             data_stream::setup_streaming(stack_manager.acquire_stack());
diff --git a/src/net/telemetry.rs b/src/net/telemetry.rs
index e5efd28ef..6cbbe768a 100644
--- a/src/net/telemetry.rs
+++ b/src/net/telemetry.rs
@@ -10,12 +10,17 @@
 //! sampling frequency. Instead, the raw codes are stored and the telemetry is generated as
 //! required immediately before transmission. This ensures that any slower computation required
 //! for unit conversion can be off-loaded to lower priority tasks.
+use crate::hardware::metadata::ApplicationMetadata;
 use heapless::{String, Vec};
+use minimq::{DeferredPublication, Publication};
 use serde::Serialize;
 
 use super::NetworkReference;
 use crate::hardware::{adc::AdcCode, afe::Gain, dac::DacCode, SystemTimer};
 
+/// Default metadata message if formatting errors occur.
+const DEFAULT_METADATA: &str = "{\"message\":\"Truncated: See USB terminal\"}";
+
 /// The telemetry client for reporting telemetry data over MQTT.
 pub struct TelemetryClient<T: Serialize> {
     mqtt: minimq::Minimq<
@@ -24,8 +29,10 @@ pub struct TelemetryClient<T: Serialize> {
         SystemTimer,
         minimq::broker::NamedBroker<NetworkReference>,
     >,
-    telemetry_topic: String<128>,
+    prefix: String<128>,
+    meta_published: bool,
     _telemetry: core::marker::PhantomData<T>,
+    metadata: &'static ApplicationMetadata,
 }
 
 /// The telemetry buffer is used for storing sample values during execution.
@@ -114,14 +121,14 @@ impl<T: Serialize> TelemetryClient<T> {
             minimq::broker::NamedBroker<NetworkReference>,
         >,
         prefix: &str,
+        metadata: &'static ApplicationMetadata,
     ) -> Self {
-        let mut telemetry_topic: String<128> = String::from(prefix);
-        telemetry_topic.push_str("/telemetry").unwrap();
-
         Self {
             mqtt,
-            telemetry_topic,
+            meta_published: false,
+            prefix: String::from(prefix),
             _telemetry: core::marker::PhantomData,
+            metadata,
         }
     }
 
@@ -134,13 +141,17 @@ impl<T: Serialize> TelemetryClient<T> {
     /// # Args
     /// * `telemetry` - The telemetry to report
     pub fn publish(&mut self, telemetry: &T) {
+        let mut topic = self.prefix.clone();
+        topic.push_str("/telemetry").unwrap();
+
         let telemetry: Vec<u8, 512> =
             serde_json_core::to_vec(telemetry).unwrap();
+
         self.mqtt
             .client()
             .publish(
                 minimq::Publication::<&[u8]>::new(&telemetry)
-                    .topic(&self.telemetry_topic)
+                    .topic(&topic)
                     .finish()
                     .unwrap(),
             )
@@ -165,5 +176,50 @@ impl<T: Serialize> TelemetryClient<T> {
             Err(error) => log::info!("Unexpected error: {:?}", error),
             _ => {}
         }
+
+        if !self.mqtt.client().is_connected() {
+            self.meta_published = false;
+            return;
+        }
+
+        // Publish application metadata
+        if !self.meta_published
+            && self.mqtt.client().can_publish(minimq::QoS::AtMostOnce)
+        {
+            let Self {
+                ref mut mqtt,
+                metadata,
+                ..
+            } = self;
+
+            let mut topic = self.prefix.clone();
+            topic.push_str("/meta").unwrap();
+
+            if mqtt
+                .client()
+                .publish(
+                    DeferredPublication::new(|buf| {
+                        serde_json_core::to_slice(&metadata, buf)
+                    })
+                    .topic(&topic)
+                    .finish()
+                    .unwrap(),
+                )
+                .is_err()
+            {
+                // Note(unwrap): We can guarantee that this message will be sent because we checked
+                // for ability to publish above.
+                mqtt.client()
+                    .publish(
+                        Publication::new(DEFAULT_METADATA.as_bytes())
+                            .topic(&topic)
+                            .finish()
+                            .unwrap(),
+                    )
+                    .unwrap();
+            }
+
+            self.meta_published = true;
+        }
     }
 }
diff --git a/src/settings.rs b/src/settings.rs
index b6e479602..42fd86934 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -22,8 +22,7 @@
 //!    settings values
 //! 3. Unknown/unneeded settings values in flash can be actively ignored, facilitating simple flash
 //!    storage sharing.
-use crate::hardware::flash::Flash;
-use crate::hardware::platform;
+use crate::hardware::{flash::Flash, metadata::ApplicationMetadata, platform};
 use core::fmt::Write;
 use miniconf::{TreeDeserialize, TreeKey, TreeSerialize};
 use postcard::ser_flavors::Flavor;
@@ -133,6 +132,9 @@ pub struct SerialSettingsPlatform {
     pub settings: Settings,
     /// The storage mechanism used to persist settings to between boots.
     pub storage: Flash,
+
+    /// Metadata associated with the application
+    pub metadata: &'static ApplicationMetadata,
 }
 
 impl serial_settings::Platform for SerialSettingsPlatform {
@@ -200,10 +202,44 @@ impl serial_settings::Platform for SerialSettingsPlatform {
         match cmd {
             "reboot" => cortex_m::peripheral::SCB::sys_reset(),
             "dfu" => platform::start_dfu_reboot(),
+            "service" => {
+                writeln!(
+                    &mut self.interface,
+                    "{:<20}: {} [{}]",
+                    "Version",
+                    self.metadata.firmware_version,
+                    self.metadata.profile,
+                )
+                .unwrap();
+                writeln!(
+                    &mut self.interface,
+                    "{:<20}: {}",
+                    "Hardware Revision", self.metadata.hardware_version
+                )
+                .unwrap();
+                writeln!(
+                    &mut self.interface,
+                    "{:<20}: {}",
+                    "Rustc Version", self.metadata.rust_version
+                )
+                .unwrap();
+                writeln!(
+                    &mut self.interface,
+                    "{:<20}: {}",
+                    "Features", self.metadata.features
+                )
+                .unwrap();
+                writeln!(
+                    &mut self.interface,
+                    "{:<20}: {}",
+                    "Panic Info", self.metadata.panic_info
+                )
+                .unwrap();
+            }
             _ => {
                 writeln!(
                     self.interface_mut(),
-                    "Invalid platform command: `{cmd}` not in [`dfu`, `reboot`]"
+                    "Invalid platform command: `{cmd}` not in [`dfu`, `reboot`, `service`]"
                 )
                 .ok();
             }


Test patch: