///! Stabilizer Telemetry Capabilities ///! ///! # Design ///! Telemetry is reported regularly using an MQTT client. All telemetry is reported in SI units ///! using standard JSON format. ///! ///! In order to report ADC/DAC codes generated during the DSP routines, a telemetry buffer is ///! employed to track the latest codes. Converting these codes to SI units would result in ///! repetitive and unnecessary calculations within the DSP routine, slowing it down and limiting ///! 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 heapless::{consts, String, Vec}; use serde::Serialize; use minimq::QoS; use super::NetworkReference; use crate::hardware::{AfeGain, design_parameters::MQTT_BROKER}; /// The telemetry client for reporting telemetry data over MQTT. pub struct TelemetryClient { mqtt: minimq::MqttClient, telemetry_topic: String, _telemetry: core::marker::PhantomData, } /// The telemetry buffer is used for storing sample values during execution. /// /// # Note /// These values can be converted to SI units immediately before reporting to save processing time. /// This allows for the DSP process to continually update the values without incurring significant /// run-time overhead during conversion to SI units. #[derive(Copy, Clone)] pub struct TelemetryBuffer { /// The latest input sample on ADC0/ADC1. pub latest_samples: [i16; 2], /// The latest output code on DAC0/DAC1. pub latest_outputs: [u16; 2], /// The latest digital input states during processing. pub digital_inputs: [bool; 2], } /// The telemetry structure is data that is ultimately reported as telemetry over MQTT. /// /// # Note /// This structure should be generated on-demand by the buffer when required to minimize conversion /// overhead. #[derive(Serialize)] pub struct Telemetry { input_levels: [f32; 2], output_levels: [f32; 2], digital_inputs: [bool; 2], } impl Default for TelemetryBuffer { fn default() -> Self { Self { latest_samples: [0, 0], latest_outputs: [0, 0], digital_inputs: [false, false], } } } impl TelemetryBuffer { /// Convert the telemetry buffer to finalized, SI-unit telemetry for reporting. /// /// # Args /// * `afe0` - The current AFE configuration for channel 0. /// * `afe1` - The current AFE configuration for channel 1. /// /// # Returns /// The finalized telemetry structure that can be serialized and reported. pub fn finalize(self, afe0: AfeGain, afe1: AfeGain) -> Telemetry { // The input voltage is measured by the ADC across a dynamic scale of +/- 4.096 V with a // dynamic range across signed integers. Additionally, the signal is fully differential, so // the differential voltage is measured at the ADC. Thus, the single-ended signal is // measured at the input is half of the ADC-reported measurement. As a pre-filter, the // input signal has a fixed gain of 1/5 through a static input active filter. Finally, at // the very front-end of the signal, there's an analog input multiplier that is // configurable by the user. let in0_volts = (self.latest_samples[0] as f32 / i16::MAX as f32) * 4.096 / 2.0 * 5.0 / afe0.as_multiplier(); let in1_volts = (self.latest_samples[1] as f32 / i16::MAX as f32) * 4.096 / 2.0 * 5.0 / afe1.as_multiplier(); // The output voltage is generated by the DAC with an output range of +/- 4.096 V. This // signal then passes through a 2.5x gain stage. Note that the DAC operates using unsigned // integers, and u16::MAX / 2 is considered zero voltage output. Thus, the dynamic range of // the output stage is +/- 10.24 V. At a DAC code of zero, there is an output of -10.24 V, // and at a max DAC code, there is an output of 10.24 V. let out0_volts = (10.24 * 2.0) * (self.latest_outputs[0] as f32 / (u16::MAX as f32)) - 10.24; let out1_volts = (10.24 * 2.0) * (self.latest_outputs[1] as f32 / (u16::MAX as f32)) - 10.24; Telemetry { input_levels: [in0_volts, in1_volts], output_levels: [out0_volts, out1_volts], digital_inputs: self.digital_inputs, } } } impl TelemetryClient { /// Construct a new telemetry client. /// /// # Args /// * `stack` - A reference to the (shared) underlying network stack. /// * `client_id` - The MQTT client ID of the telemetry client. /// * `prefix` - The device prefix to use for MQTT telemetry reporting. /// /// # Returns /// A new telemetry client. pub fn new(stack: NetworkReference, client_id: &str, prefix: &str) -> Self { let mqtt = minimq::MqttClient::new(MQTT_BROKER.into(), client_id, stack) .unwrap(); let mut telemetry_topic: String = String::from(prefix); telemetry_topic.push_str("/telemetry").unwrap(); Self { mqtt, telemetry_topic, _telemetry: core::marker::PhantomData::default(), } } /// Publish telemetry over MQTT /// /// # Note /// Telemetry is reported in a "best-effort" fashion. Failure to transmit telemetry will cause /// it to be silently dropped. /// /// # Args /// * `telemetry` - The telemetry to report pub fn publish(&mut self, telemetry: &T) { let telemetry: Vec = serde_json_core::to_vec(telemetry).unwrap(); self.mqtt .publish(&self.telemetry_topic, &telemetry, QoS::AtMostOnce, &[]) .ok(); } /// Update the telemetry client /// /// # Note /// This function is provided to force the underlying MQTT state machine to process incoming /// and outgoing messages. Without this, the client will never connect to the broker. This /// should be called regularly. pub fn update(&mut self) { match self.mqtt.poll(|_client, _topic, _message, _properties| {}) { Err(minimq::Error::Network( smoltcp_nal::NetworkError::NoIpAddress, )) => {} Err(error) => log::info!("Unexpected error: {:?}", error), _ => {} } } }