From 7f24bf5a9186490b130ae3af4bc05a131994ca4c Mon Sep 17 00:00:00 2001 From: Keenan Tims Date: Sun, 1 Feb 2026 20:06:25 -0800 Subject: [PATCH] prs10: intermediate work --- src/prs10.rs | 487 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 src/prs10.rs diff --git a/src/prs10.rs b/src/prs10.rs new file mode 100644 index 0000000..d3789df --- /dev/null +++ b/src/prs10.rs @@ -0,0 +1,487 @@ +use std::sync::Arc; + +use crate::{ + ChimemonMessage, ChimemonSource, ChimemonSourceChannel, Prs10Config, SourceMetric, + SourceReport, SourceReportDetails, SourceStatus, +}; +use async_trait::async_trait; +use bit_struct::u4; +use bitflags::bitflags; +use itertools::Itertools; +use serde::Deserialize; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf}; +use tokio::select; +use tokio::sync::OnceCell; +use tokio::time::{Interval, interval, timeout}; +use tokio_serial; +use tokio_serial::{SerialPort, SerialStream}; +use tracing::{debug, error, info, warn}; + +#[derive(Debug)] +pub struct Prs10Info { + pub model: String, + pub version: String, + pub serial: String, +} + +impl TryFrom<&[u8]> for Prs10Info { + type Error = Box; + fn try_from(value: &[u8]) -> Result { + let parts = value.splitn(3, |c| *c == b'_'); + let (model, version, serial) = parts + .collect_tuple() + .ok_or("Not enough parts in ID response")?; + Ok(Self { + model: str::from_utf8(model)?.to_string(), + version: str::from_utf8(version)?.to_string(), + serial: str::from_utf8(serial)?.to_string(), + }) + } +} + +bitflags! { + pub struct Prs10PowerLampFlags: u8 { + const ELEC_VOLTAGE_LOW = (1<<0); + const ELEC_VOLTAGE_HIGH = (1<<1); + const HEAT_VOLTAGE_LOW = (1<<2); + const HEAT_VOLTAGE_HIGH = (1<<3); + const LAMP_LIGHT_LOW = (1<<4); + const LAMP_LIGHT_HIGH = (1<<5); + const GATE_VOLTAGE_LOW = (1<<6); + const GATE_VOLTAGE_HIGH = (1<<7); + } +} + +impl Prs10PowerLampFlags { + pub fn get_metrics(&self, no_tags: Vec) -> Vec { + // Define the mapping statically + const FLAG_LABELS: [(&Prs10PowerLampFlags, &str); 8] = [ + (&Prs10PowerLampFlags::ELEC_VOLTAGE_LOW, "elec_voltage_low"), + (&Prs10PowerLampFlags::ELEC_VOLTAGE_HIGH, "elec_voltage_high"), + (&Prs10PowerLampFlags::HEAT_VOLTAGE_LOW, "heat_voltage_low"), + (&Prs10PowerLampFlags::HEAT_VOLTAGE_HIGH, "heat_voltage_high"), + (&Prs10PowerLampFlags::LAMP_LIGHT_LOW, "lamp_light_low"), + (&Prs10PowerLampFlags::LAMP_LIGHT_HIGH, "lamp_light_high"), + (&Prs10PowerLampFlags::GATE_VOLTAGE_LOW, "gate_voltage_low"), + (&Prs10PowerLampFlags::GATE_VOLTAGE_HIGH, "gate_voltage_high"), + ]; + + let no_tags = Arc::new(vec![]); + + // Generate metrics based on flag availability + FLAG_LABELS + .iter() + .map(|(flag, label)| { + // We track whether each flag is set (true) or not (false) + SourceMetric::new_bool(*label, self.contains(**flag), no_tags.clone()) + }) + .collect() + } +} + +bitflags! { + pub struct Prs10RfFlags: u8 { + const PLL_UNLOCK = (1<<0); + const XTAL_VAR_LOW = (1<<1); + const XTAL_VAR_HIGH = (1<<2); + const VCO_CTRL_LOW = (1<<3); + const VCO_CTRL_HIGH = (1<<4); + const AGC_CTRL_LOW = (1<<5); + const AGC_CTRL_HIGH = (1<<6); + const PLL_BAD_PARAM = (1<<7); + } +} + +bitflags! { + pub struct Prs10TempFlags: u8 { + const LAMP_TEMP_LOW = (1<<0); + const LAMP_TEMP_HIGH = (1<<1); + const XTAL_TEMP_LOW = (1<<2); + const XTAL_TEMP_HIGH = (1<<3); + const CELL_TEMP_LOW = (1<<4); + const CELL_TEMP_HIGH = (1<<5); + const CASE_TEMP_LOW = (1<<6); + const CASE_TEMP_HIGH = (1<<7); + } +} + +bitflags! { + pub struct Prs10FllFlags: u8 { + const FLL_OFF = (1<<0); + const FLL_DISABLED = (1<<1); + const EFC_HIGH = (1<<2); + const EFC_LOW = (1<<3); + const CAL_VOLTAGE_HIGH = (1<<4); + const CAL_VOLTAGE_LOW = (1<<5); + } +} + +bitflags! { + pub struct Prs10PpsFlags: u8 { + const PLL_DISABLED = (1<<0); + const PPS_WARMUP = (1<<1); + const PLL_ACTIVE = (1<<2); + const PPS_BAD = (1<<3); + const PPS_INTERVAL_LONG = (1<<4); + const PLL_RESTART = (1<<5); + const PLL_SATURATED = (1<<6); + const PPS_MISSING = (1<<7); + } +} + +bitflags! { + pub struct Prs10SystemFlags: u8 { + const LAMP_RESTART = (1<<0); + const WDT_RESET = (1<<1); + const BAD_INT_VECTOR = (1<<2); + const EEPROM_WRITE_FAIL = (1<<3); + const EEPROM_CORRUPT = (1<<4); + const BAD_COMMAND = (1<<5); + const BAD_COMMAND_PARAM = (1<<6); + const SYSTEM_RESET = (1<<7); + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct Prs10Status { + pub volt_lamp_flags: Prs10PowerLampFlags, + pub rf_flags: Prs10RfFlags, + pub temp_flags: Prs10TempFlags, + pub fll_flags: Prs10FllFlags, + pub pps_flags: Prs10PpsFlags, + pub system_flags: Prs10SystemFlags, +} + +impl Default for Prs10Status { + fn default() -> Self { + Self { + volt_lamp_flags: Prs10PowerLampFlags::empty(), + rf_flags: Prs10RfFlags::empty(), + temp_flags: Prs10TempFlags::empty(), + fll_flags: Prs10FllFlags::empty(), + pps_flags: Prs10PpsFlags::empty(), + system_flags: Prs10SystemFlags::empty(), + } + } +} + +impl SourceReportDetails for Prs10Status { + fn is_healthy(&self) -> bool { + const HEALTHY_PPS: Prs10PpsFlags = Prs10PpsFlags { bits: 4 }; + self.volt_lamp_flags.is_empty() + && self.rf_flags.is_empty() + && self.temp_flags.is_empty() + && self.fll_flags.is_empty() + && self.pps_flags == HEALTHY_PPS + } + + fn to_metrics(&self) -> Vec { + let no_tags = Arc::new(vec![]); + vec![ + SourceMetric::new_int( + "volt_lamp_flags", + self.volt_lamp_flags.bits() as i64, + no_tags.clone(), + ), + SourceMetric::new_int("rf_flags", self.rf_flags.bits() as i64, no_tags.clone()), + SourceMetric::new_int("temp_flags", self.temp_flags.bits() as i64, no_tags.clone()), + SourceMetric::new_int("fll_flags", self.fll_flags.bits() as i64, no_tags.clone()), + SourceMetric::new_int("pps_flags", self.pps_flags.bits() as i64, no_tags.clone()), + // system flags are kind of useless because we can't guarantee they get upstreamed and will only appear once since they are 'event flags' + ] + } +} + +impl TryFrom<&[u8]> for Prs10Status { + type Error = Box; + fn try_from(value: &[u8]) -> Result { + let (volt_lamp_flags, rf_flags, temp_flags, fll_flags, pps_flags, system_flags) = value + .splitn(6, |c| *c == b',') + .map(|s| str::from_utf8(s).unwrap().parse::()) + .collect_tuple() + .ok_or("Not enough parts in ST reply")?; + Ok(Self { + volt_lamp_flags: Prs10PowerLampFlags::from_bits(volt_lamp_flags?) + .ok_or("Invalid bits set ({volt_lamp_flags}) for power/lamp flags")?, + rf_flags: Prs10RfFlags::from_bits(rf_flags?) + .ok_or("Invalid bits set ({rf_flags}) for RF flags")?, + temp_flags: Prs10TempFlags::from_bits(temp_flags?) + .ok_or("Invalid bits set ({temp_flags}) for temp flags")?, + fll_flags: Prs10FllFlags::from_bits(fll_flags?) + .ok_or("Invalid bits set ({fll_flags}) for FLL flags")?, + pps_flags: Prs10PpsFlags::from_bits(pps_flags?) + .ok_or("Invalid bits set ({pps_flags}) for PPS flags")?, + system_flags: Prs10SystemFlags::from_bits(system_flags?) + .ok_or("Invalid bits set ({system_flags}) for system flags")?, + }) + } +} + +pub struct Prs10Stats { + pub ocxo_efc: u32, + pub error_signal_volts: f64, + pub detect_signal_volts: f64, + pub freq_offset_ppt: u16, + pub mag_efc: u16, + pub heat_volts: f64, + pub elec_volts: f64, + pub lamp_fet_drain_volts: f64, + pub lamp_fet_gate_volts: f64, + pub ocxo_heat_volts: f64, + pub cell_heat_volts: f64, + pub lamp_heat_volts: f64, + pub rb_photo: f64, + pub rb_photo_iv: f64, + pub case_temp: f64, + pub ocxo_therm: f64, + pub cell_therm: f64, + pub lamp_therm: f64, + pub ext_cal_volts: f64, + pub analog_gnd_volts: f64, +} + +impl SourceReportDetails for Prs10Stats { + fn is_healthy(&self) -> bool { + true + } + fn to_metrics(&self) -> Vec { + let no_tags = Arc::new(vec![]); + vec![ + // Integer Metrics + SourceMetric::new_int("ocxo_efc", self.ocxo_efc as i64, no_tags.clone()), + // Float Metrics + SourceMetric::new_float( + "error_signal_volts", + self.error_signal_volts, + no_tags.clone(), + ), + SourceMetric::new_float( + "detect_signal_volts", + self.detect_signal_volts, + no_tags.clone(), + ), + SourceMetric::new_float("heat_volts", self.heat_volts, no_tags.clone()), + SourceMetric::new_float("elec_volts", self.elec_volts, no_tags.clone()), + SourceMetric::new_float( + "lamp_fet_drain_volts", + self.lamp_fet_drain_volts, + no_tags.clone(), + ), + SourceMetric::new_float( + "lamp_fet_gate_volts", + self.lamp_fet_gate_volts, + no_tags.clone(), + ), + SourceMetric::new_float("ocxo_heat_volts", self.ocxo_heat_volts, no_tags.clone()), + SourceMetric::new_float("cell_heat_volts", self.cell_heat_volts, no_tags.clone()), + SourceMetric::new_float("lamp_heat_volts", self.lamp_heat_volts, no_tags.clone()), + SourceMetric::new_float("rb_photo", self.rb_photo, no_tags.clone()), + SourceMetric::new_float("rb_photo_iv", self.rb_photo_iv, no_tags.clone()), + SourceMetric::new_float("case_temp", self.case_temp, no_tags.clone()), + SourceMetric::new_float("ocxo_therm", self.ocxo_therm, no_tags.clone()), + SourceMetric::new_float("cell_therm", self.cell_therm, no_tags.clone()), + SourceMetric::new_float("lamp_therm", self.lamp_therm, no_tags.clone()), + SourceMetric::new_float("ext_cal_volts", self.ext_cal_volts, no_tags.clone()), + SourceMetric::new_float("analog_gnd_volts", self.analog_gnd_volts, no_tags.clone()), + // U16 Metrics (optional, but can be treated as integers) + SourceMetric::new_int( + "freq_offset_ppt", + self.freq_offset_ppt as i64, + no_tags.clone(), + ), + SourceMetric::new_int("mag_efc", self.mag_efc as i64, no_tags.clone()), + ] + } +} + +pub struct Prs10Monitor { + rx: ReadHalf, + tx: WriteHalf, + info: OnceCell, + config: Prs10Config, +} + +impl Prs10Monitor { + pub fn new(config: Prs10Config) -> Self { + let builder = tokio_serial::new(&config.port, config.baud) + .timeout(config.timeout) + .data_bits(tokio_serial::DataBits::Eight) + .parity(tokio_serial::Parity::None) + .stop_bits(tokio_serial::StopBits::One) + .flow_control(tokio_serial::FlowControl::None); + let mut port = SerialStream::open(&builder).expect("Must be able to open serial port"); + port.set_exclusive(true).expect("Can't lock serial port"); + info!( + "Opened serial port {}@{}", + port.name().unwrap(), + port.baud_rate().unwrap() + ); + let (rx, tx) = tokio::io::split(port); + + Self { + rx, + tx, + config, + info: OnceCell::new(), + } + } + + pub fn info(&self) -> &Prs10Info { + self.info.get().expect("info() used before run()") + } + + pub async fn cmd_response(&mut self, cmd: &[u8]) -> Result, std::io::Error> { + debug!("cmd: `{:?}`", String::from_utf8_lossy(cmd)); + self.tx.write_all(cmd).await.unwrap(); + self.tx.write_u8(b'\r').await.unwrap(); + let mut reader = BufReader::new(&mut self.rx); + let mut buf = Vec::new(); + let read = timeout(self.config.timeout, reader.read_until(b'\r', &mut buf)).await??; + buf.truncate(buf.len() - 1); // strip "\r" + debug!("cmd response: ({read}) `{buf:?}`"); + Ok(buf) + } + + async fn set_info(&mut self) -> Result<(), Box> { + let id = self.get_id().await?; + self.info.set(id); + debug!("Set info to {:?}", self.info); + Ok(()) + } + + pub async fn get_status(&mut self) -> Result> { + debug!("Getting status"); + let resp = self.cmd_response(b"ST?").await?; + let status = resp.as_slice().try_into(); + debug!("Got: {status:?}"); + status + } + + pub async fn get_id(&mut self) -> Result> { + debug!("Getting identity"); + let resp = self.cmd_response(b"ID?").await?; + let id = resp.as_slice().try_into(); + debug!("Got: {id:?}"); + id + } + + pub async fn get_analog(&mut self, id: u4) -> Result> { + debug!("Getting analog value {id}"); + let mut cmd = b"AD".to_vec(); + cmd.extend_from_slice(id.to_string().as_bytes()); + cmd.push(b'?'); + let resp = self.cmd_response(&cmd).await?; + let value = str::from_utf8(&resp)?.parse::()?; + + Ok(value) + } + + pub async fn get_ocxo_efc(&mut self) -> Result> { + debug!("Getting u16,u16 -> u32 for OCXO EFC value"); + let resp = self.cmd_response(b"FC?").await?; + let values = resp + .splitn(2, |c| *c == b',') + .map(|s| str::from_utf8(s).unwrap().parse::()) + .collect_tuple() + .ok_or("Not enough values in response to FC?")?; + if let (Ok(high), Ok(low)) = values { + Ok((high as u32) << 8 | low as u32) + } else { + Err("Unparseable numbers in response to FC?".into()) + } + } + + pub async fn get_float(&mut self, cmd: &[u8]) -> Result> { + debug!("Getting float value for command {cmd:?}"); + let resp = self.cmd_response(cmd).await?; + let val = str::from_utf8(&resp)?.parse::()?; + Ok(val) + } + + async fn status_poll(&mut self) -> Option { + debug!("polling status"); + let status = self.get_status().await; + if let Ok(status) = status { + Some(ChimemonMessage::SourceReport(SourceReport { + name: "prs10".into(), + status: if status.is_healthy() { + SourceStatus::Healthy + } else { + SourceStatus::Unknown + }, + details: Arc::new(status), + })) + } else { + None + } + } + + async fn stats_poll(&mut self) -> Option { + debug!("polling stats"); + + let ocxo_efc = self.get_ocxo_efc().await; + + Some(ChimemonMessage::SourceReport(SourceReport { + name: "prs10".into(), + status: SourceStatus::Unknown, + details: Arc::new(Prs10Stats {}), + })) + } +} + +#[async_trait] +impl ChimemonSource for Prs10Monitor { + async fn run(mut self, chan: ChimemonSourceChannel) { + info!("PRS10 task starting"); + if let Err(e) = self.set_info().await { + warn!("Error starting PRS10: {e:?}"); + return; + } + info!( + "Connected to PRS10 model: {} version: {} serial: {}", + self.info().model, + self.info().version, + self.info().serial + ); + + let mut status_timer = interval(self.config.status_interval); + let mut pps_timer = interval(self.config.stats_interval); + + loop { + let msg = select! { + _ = status_timer.tick() => { + self.status_poll().await + }, + _ = pps_timer.tick() => { + self.stats_poll().await + } + }; + if let Some(msg) = msg { + chan.send(msg).expect("Unable to send to channel"); + } + } + } +} + +mod tests { + use crate::prs10::{Prs10Info, Prs10PowerLampFlags, Prs10PpsFlags, Prs10Status}; + #[test] + fn test_info_parse() -> Result<(), Box> { + const INFO_VECTOR: &[u8] = b"PRS10_3.15_SN_12345"; + let info: Prs10Info = INFO_VECTOR.try_into()?; + assert_eq!(info.model, "PRS10"); + assert_eq!(info.version, "3.15"); + assert_eq!(info.serial, "SN_12345"); + Ok(()) + } + #[test] + fn test_status_parse() -> Result<(), Box> { + //TODO: Add vectors for some more complicated state + const STATUS_VECTOR1: &[u8] = b"0,0,0,0,4,0"; + let status: Prs10Status = STATUS_VECTOR1.try_into()?; + let mut expect = Prs10Status::default(); + expect.pps_flags.set(Prs10PpsFlags::PLL_ACTIVE, true); + assert_eq!(status, expect); + Ok(()) + } +}