prs10: intermediate work
This commit is contained in:
487
src/prs10.rs
Normal file
487
src/prs10.rs
Normal file
@@ -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<dyn std::error::Error>;
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
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<String>) -> Vec<SourceMetric> {
|
||||
// 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<SourceMetric> {
|
||||
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<dyn std::error::Error>;
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
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::<u8>())
|
||||
.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<SourceMetric> {
|
||||
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<SerialStream>,
|
||||
tx: WriteHalf<SerialStream>,
|
||||
info: OnceCell<Prs10Info>,
|
||||
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<Vec<u8>, 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<dyn std::error::Error>> {
|
||||
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<Prs10Status, Box<dyn std::error::Error>> {
|
||||
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<Prs10Info, Box<dyn std::error::Error>> {
|
||||
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<f64, Box<dyn std::error::Error>> {
|
||||
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::<f64>()?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn get_ocxo_efc(&mut self) -> Result<u32, Box<dyn std::error::Error>> {
|
||||
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::<u16>())
|
||||
.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<f64, Box<dyn std::error::Error>> {
|
||||
debug!("Getting float value for command {cmd:?}");
|
||||
let resp = self.cmd_response(cmd).await?;
|
||||
let val = str::from_utf8(&resp)?.parse::<f64>()?;
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
async fn status_poll(&mut self) -> Option<ChimemonMessage> {
|
||||
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<ChimemonMessage> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
//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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user