Files
chimemon/src/sources/uccm.rs
2026-02-04 03:13:39 -08:00

636 lines
22 KiB
Rust

use std::io::{BufRead, Cursor};
use std::str;
use std::sync::Arc;
use async_trait::async_trait;
use bitflags::bitflags;
use byteorder::{BigEndian, ReadBytesExt};
use bytes::{Buf, BytesMut};
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use figment::value::Map;
use futures::future::join;
use influxdb2::models::DataPoint;
use influxdb2::models::data_point::DataPointBuilder;
use itertools::Itertools;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
use tokio::sync::Mutex;
use tokio::time::sleep;
use tokio::{join, select};
use tokio_serial::{SerialPort, SerialStream};
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use crate::{
ChimemonMessage, ChimemonSource, ChimemonSourceChannel, SourceMetric, SourceReportDetails,
TimeReport, UCCMConfig,
};
pub const GPS_EPOCH: i64 = 315964800; // Doesn't seem possible to have a const DateTime object
pub type UccmEndian = BigEndian;
pub enum UCCMMonitorParseState {
Idle,
ReadStatus,
ReadLoopDiag,
ReadTOD,
}
pub struct UCCMMonitor {
pub name: String,
config: UCCMConfig,
rx: ReadHalf<SerialStream>,
tx: WriteHalf<SerialStream>,
pub info: Option<UCCMInfo>,
}
#[derive(Debug)]
pub struct UCCMTODReport {
pub time: DateTime<Utc>, // TAI timestamp
pub leaps: i8,
pub flags: UCCMFlags,
}
impl SourceReportDetails for UCCMTODReport {
fn is_healthy(&self) -> bool {
self.flags.contains(UCCMFlags::OSC_LOCK)
&& self.flags.contains(UCCMFlags::HAVE_GPS_TIME)
&& !self.flags.contains(UCCMFlags::INIT_UNLOCK)
&& !self.flags.contains(UCCMFlags::INIT_NO_SATS)
&& !self.flags.contains(UCCMFlags::POWER_FAIL)
&& !self.flags.contains(UCCMFlags::NO_GPS_SYNC)
&& !self.flags.contains(UCCMFlags::NO_GPS_SYNC2)
&& !self.flags.contains(UCCMFlags::NO_ANT)
&& !self.flags.contains(UCCMFlags::GPS_LOS)
}
fn to_metrics(&self) -> Vec<SourceMetric> {
let no_tags = Arc::new(vec![]);
vec![
SourceMetric::new_int("leaps", self.leaps as i64, no_tags.clone()),
SourceMetric::new_bool(
"osc_lock",
self.flags.contains(UCCMFlags::OSC_LOCK),
no_tags.clone(),
),
SourceMetric::new_bool(
"leap_flag",
self.flags.contains(UCCMFlags::LEAP_FLAG),
no_tags.clone(),
),
SourceMetric::new_bool(
"init_unlock",
self.flags.contains(UCCMFlags::INIT_UNLOCK),
no_tags.clone(),
),
SourceMetric::new_bool(
"init_no_sats",
self.flags.contains(UCCMFlags::INIT_NO_SATS),
no_tags.clone(),
),
SourceMetric::new_bool(
"have_gps_time",
self.flags.contains(UCCMFlags::HAVE_GPS_TIME),
no_tags.clone(),
),
SourceMetric::new_bool(
"power_fail",
self.flags.contains(UCCMFlags::POWER_FAIL),
no_tags.clone(),
),
SourceMetric::new_bool(
"no_gps_sync",
self.flags.contains(UCCMFlags::NO_GPS_SYNC),
no_tags.clone(),
),
SourceMetric::new_bool(
"no_gps_sync2",
self.flags.contains(UCCMFlags::NO_GPS_SYNC2),
no_tags.clone(),
),
SourceMetric::new_bool(
"ant_fault",
self.flags.contains(UCCMFlags::NO_ANT),
no_tags.clone(),
),
SourceMetric::new_bool(
"gps_los",
self.flags.contains(UCCMFlags::GPS_LOS),
no_tags.clone(),
),
]
}
}
impl UCCMTODReport {
pub fn as_builder(&self, measurement: &String, tags: &Map<String, String>) -> DataPointBuilder {
let mut builder =
DataPoint::builder(measurement).timestamp(self.time.timestamp_nanos_opt().unwrap());
builder = builder.field("leaps", self.leaps as i64);
builder = builder.field("osc_lock", self.flags.contains(UCCMFlags::OSC_LOCK));
builder = builder.field("leap_flag", self.flags.contains(UCCMFlags::LEAP_FLAG));
builder = builder.field("init_unlock", self.flags.contains(UCCMFlags::INIT_UNLOCK));
builder = builder.field("init_no_sats", self.flags.contains(UCCMFlags::INIT_NO_SATS));
builder = builder.field(
"have_gps_time",
self.flags.contains(UCCMFlags::HAVE_GPS_TIME),
);
builder = builder.field("power_fail", self.flags.contains(UCCMFlags::POWER_FAIL));
builder = builder.field("no_gps_sync", self.flags.contains(UCCMFlags::NO_GPS_SYNC));
builder = builder.field("no_gps_sync2", self.flags.contains(UCCMFlags::NO_GPS_SYNC2));
builder = builder.field("ant_fault", self.flags.contains(UCCMFlags::NO_ANT));
builder = builder.field("gps_los", self.flags.contains(UCCMFlags::GPS_LOS));
builder = tags
.iter()
.fold(builder, |builder, (k, v)| builder.tag(k, v));
builder
}
}
#[derive(Debug)]
pub struct UCCMLoopDiagReport {
pub ocxo: f32,
}
impl UCCMLoopDiagReport {
pub fn as_builder(&self, measurement: &String, tags: &Map<String, String>) -> DataPointBuilder {
let mut builder = DataPoint::builder(measurement);
builder = builder.field("ocxo_offset", self.ocxo as f64);
builder = tags
.iter()
.fold(builder, |builder, (k, v)| builder.tag(k, v));
builder
}
}
#[derive(Debug)]
pub struct UCCMGpsSvTracking {
pub sv: u8,
pub cno: u8,
}
impl UCCMGpsSvTracking {
fn as_builder(&self, measurement: &String, tags: &Map<String, String>) -> DataPointBuilder {
let mut builder = DataPoint::builder(measurement)
.field("sv_cno", self.cno as i64)
.tag("sv_id", self.sv.to_string());
builder = tags
.iter()
.fold(builder, |builder, (k, v)| builder.tag(k, v));
builder
}
}
#[derive(Debug)]
pub struct UCCMGPSSatsReport {
pub tracked_svs: Vec<UCCMGpsSvTracking>,
}
impl UCCMGPSSatsReport {
pub fn build(&self, measurement: &String, tags: &Map<String, String>) -> Vec<DataPoint> {
self.tracked_svs
.iter()
.map(|sv| sv.as_builder(measurement, tags))
.map(|b| b.build().unwrap())
.collect()
}
}
bitflags! {
#[derive(Debug)]
pub struct UCCMFlags: u32 {
const OSC_LOCK = (1<<29);
const LEAP_FLAG = (1<<25);
const INIT_UNLOCK = (1<<24);
const INIT_NO_SATS = (1<<19);
const HAVE_GPS_TIME = (1<<18);
const POWER_FAIL = (1<<17);
const MODEL_SYMMETRICOM = (1<<15);
const MODEL_TRIMBLE = (1<<14);
const NO_GPS_SYNC = (1<<11);
const NO_GPS_SYNC2 = (1<<9);
const MODEL_SYMMETRICOM2 = (1<<7);
const MODEL_TRIMBLE2 = (1<<6);
const NO_ANT = (1<<5);
const GPS_LOS = (1<<4);
}
}
/* FLAGS:
MSB 31 x
30 ALWAYS 1
29 oscillator locked
28..26 x
25 leap flag
24 initial unlock
23..20 x
19 initialized no sats (trimble)
18 have gps time flag
17 power fail
16 x
15..14 model/protocol ( 01 = trimble 10 = symmetricom )
13..12 x
11 no gps sync
10 always 1
9 no gps sync
8 always 1
7..6 model/protocol ( 01 = trimble 10 = symmetricom )
5 no ant
4 gps los
LSB 3..0 x
*/
#[derive(Debug)]
pub struct GPSSVInfo {
pub tracked: bool,
pub in_view: bool,
pub prn: u8,
pub elevation: usize,
pub azimuth: usize,
pub cn0: usize,
}
#[derive(Debug)]
pub struct UCCMStatusReport {
pub tfom: u8,
pub ffom: u8,
pub gps_phase: f32,
pub gps_pps_valid: bool,
pub gps_svs: [GPSSVInfo; 32],
pub gps_time: NaiveDateTime,
pub ant_voltage: f32,
pub ant_current: f32,
pub temp: f32,
pub efc_dac: u32,
pub freq_error: f32,
}
impl SourceReportDetails for UCCMStatusReport {
fn is_healthy(&self) -> bool {
self.gps_pps_valid
}
fn to_metrics(&self) -> Vec<SourceMetric> {
let no_tags = Arc::new(vec![]);
vec![
SourceMetric::new_int("tfom", self.tfom as i64, no_tags.clone()),
SourceMetric::new_int("ffom", self.ffom as i64, no_tags.clone()),
SourceMetric::new_float("gps_phase", self.gps_phase as f64, no_tags.clone()),
// TODO: sv info
// TOOD: timestamp
SourceMetric::new_float("ant_voltage", self.ant_voltage as f64, no_tags.clone()),
SourceMetric::new_float("ant_current", self.ant_current as f64, no_tags.clone()),
SourceMetric::new_float("temp", self.temp as f64, no_tags.clone()),
SourceMetric::new_int("efc_dac", self.efc_dac as i64, no_tags.clone()),
SourceMetric::new_float("freq_error", self.freq_error as f64, no_tags.clone()),
]
}
}
pub struct UCCMInfo {
pub vendor: String,
pub model: String,
pub serial: String,
pub version: String,
}
impl TryFrom<&[u8]> for UCCMTODReport {
type Error = String;
fn try_from(strbuf: &[u8]) -> Result<Self, Self::Error> {
debug!("TOD buffer: `{:#?}`", String::from_utf8(strbuf.to_vec()));
let resp: Vec<u8> = strbuf
.split(|c| *c == b' ')
.map(|x| u8::from_str_radix(str::from_utf8(x).unwrap_or(""), 16).unwrap_or(0))
.collect();
let mut rdr = Cursor::new(resp);
// Sync flag
let c = rdr.read_u16::<UccmEndian>().unwrap();
if c != 0xc500 {
return Err(format!("Missing start delimter (got: `{}`)", c));
}
// Consume 25 unknown bytes
rdr.advance(25);
let time = rdr.read_u32::<UccmEndian>().unwrap();
rdr.advance(1); // consume padding
let leaps = rdr.read_i8().unwrap();
let flags = rdr.read_u32::<UccmEndian>().unwrap();
debug!("Flags: {}", flags);
rdr.advance(6); // Consume padding and checksum, don't check it
let c = rdr.read_u8().unwrap();
if c != 0xca {
return Err(format!("Missing end delimiter (got: `{}`)", c));
}
debug!("TOD time: {} leaps: {} flags: {}", time, leaps, flags);
Ok(UCCMTODReport {
time: DateTime::from_timestamp(GPS_EPOCH + time as i64, 0).unwrap(),
leaps,
flags: UCCMFlags::from_bits_truncate(flags),
})
}
}
impl TryFrom<&str> for UCCMLoopDiagReport {
type Error = std::io::Error;
fn try_from(strbuf: &str) -> Result<Self, Self::Error> {
debug!("LoopDiag buffer: `{:#?}`", strbuf);
if &strbuf[0..4] != "OCXO" {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid response (expected `OCXO`)",
))
} else {
let mut cursor = Cursor::new(strbuf);
let mut lines = BufRead::lines(&mut cursor);
let ocxo_line = lines.next().ok_or(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"No lines!",
))??;
let ocxo_val = ocxo_line
.split(':')
.nth(1)
.ok_or(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"no colon!",
))?
.trim();
let ocxo_val_f = ocxo_val
.parse::<f32>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(UCCMLoopDiagReport { ocxo: ocxo_val_f })
}
}
}
impl TryFrom<&str> for UCCMGPSSatsReport {
type Error = std::io::Error;
fn try_from(strbuf: &str) -> Result<Self, Self::Error> {
let resp = strbuf.split_once('\n').unwrap().0.trim();
let (nsats, cnos) = resp.split_once(' ').ok_or(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid response (expected `NSATS CNOS`)",
))?;
let nsats = nsats.parse::<u8>().map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid number of sats ({e})"),
)
})?;
let tracked_svs = cnos
.split(',')
.chunks(2)
.into_iter()
.map(|chunk| chunk.collect_tuple().unwrap())
.inspect(|chunk| debug!(" chunk: {:?}", chunk))
.map(|(sv_s, cno_s)| UCCMGpsSvTracking {
sv: sv_s.parse().unwrap(),
cno: cno_s.parse().unwrap(),
})
.collect_vec();
assert_eq!(nsats as usize, tracked_svs.len());
Ok(UCCMGPSSatsReport { tracked_svs })
}
}
impl UCCMMonitor {
pub async fn send_cmd(&mut self, cmd: &[u8]) -> Result<String, std::io::Error> {
debug!("cmd: `{:?}`", String::from_utf8_lossy(cmd));
self.tx.write_all(cmd).await.unwrap();
self.tx.write_u8(b'\n').await.unwrap();
let mut reader = BufReader::new(&mut self.rx);
let mut resp = String::new();
while !resp.contains("UCCM>") {
debug!("'{}' doesn't contain UCCM>", resp);
let mut buf = Vec::new();
reader.read_until(b'>', &mut buf).await.unwrap();
resp.push_str(&String::from_utf8_lossy(&buf));
}
// Remove the command we sent from the response
resp.replace_range(0..resp.find('\n').unwrap_or(0) + 1, "");
resp = resp.replace("\r", "");
debug!("cmd response: `{:?}`", resp);
Ok(resp)
}
pub async fn get_info(&mut self) -> Result<(), std::io::Error> {
self.send_cmd(b"SCPI").await.unwrap_or_default();
self.send_cmd(b"TOD DI").await.unwrap_or_default();
sleep(tokio::time::Duration::from_secs(1)).await;
let resp = self.send_cmd(b"*IDN?").await.unwrap_or_default();
let info: Vec<&str> = resp.lines().next().unwrap().split(',').collect();
debug!("Response length: {}", info.len());
if info.len() != 4 {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Unexpected *IDN? response",
));
}
self.info = Some(UCCMInfo {
vendor: info[0].into(),
model: info[1].into(),
serial: info[2].into(),
version: info[3].into(),
});
info!(
"Found {} {} s/n: {} version: {}",
self.info.as_ref().unwrap().vendor,
self.info.as_ref().unwrap().model,
self.info.as_ref().unwrap().serial,
self.info.as_ref().unwrap().version
);
Ok(())
}
async fn rx_loop(
mut self,
chan: ChimemonSourceChannel,
state: Arc<Mutex<UCCMMonitorParseState>>,
) {
let mut rdbuf = BytesMut::with_capacity(1024);
let mut last_loop_diag: Option<UCCMLoopDiagReport> = None;
let mut last_gps_sats: Option<UCCMGPSSatsReport> = None;
let mut last_sent_report = Utc::now() - self.config.status_interval;
loop {
match tokio::io::AsyncReadExt::read_buf(&mut self.rx, &mut rdbuf).await {
Ok(n) => {
if n == 0 {
continue;
}
}
Err(_) => continue,
}
match *state.lock().await {
UCCMMonitorParseState::Idle => {
while !rdbuf.starts_with(b"c5 00") && rdbuf.remaining() > 0 {
rdbuf.advance(1);
}
if rdbuf.len() < (44 * 2 + 43) {
// TOD frame is 44 bytes, plus 43 spaces
continue;
};
let frame = rdbuf.split_to(44 * 2 + 43);
match UCCMTODReport::try_from(&frame[..]) {
Ok(tod) => {
let sysnow = Utc::now();
let offset = tod.time - Duration::seconds(tod.leaps as i64) - sysnow;
debug!(
"System time: {:#?} GPS time: {:#?} Leaps: {:#?}",
sysnow, tod.time, tod.leaps
);
debug!("TOD offset: {}ms", offset.num_milliseconds());
info!("{:#?}", tod);
let valid = tod.leaps > 0
&& tod
.flags
.contains(UCCMFlags::OSC_LOCK | UCCMFlags::HAVE_GPS_TIME);
chan.send(ChimemonMessage::TimeReport(TimeReport {
system_time: sysnow,
offset,
leaps: tod.leaps as isize,
leap_flag: tod.flags.contains(UCCMFlags::LEAP_FLAG),
valid,
}))
.expect("Unable to send to channel");
if sysnow - last_sent_report
>= Duration::from_std(self.config.status_interval).unwrap()
{
// let mut points = vec![
// tod.as_builder(&self.config.measurement, &self.config.tags)
// .build()
// .unwrap(),
// ];
// if let Some(loop_diag) = &last_loop_diag {
// points.push(
// loop_diag
// .as_builder(
// &self.config.measurement,
// &self.config.influxdb.tags,
// )
// .build()
// .unwrap(),
// )
// }
// if let Some(gps_sats) = &last_gps_sats {
// points.extend(gps_sats.build(
// &self.config.sources.uccm.measurement,
// &self.config.influxdb.tags,
// ));
// }
// chan.send(ChimemonMessage::DataPoints(points))
// .expect("Unable to send to channel");
last_sent_report = sysnow;
}
}
Err(e) => {
warn!("Unable to parse TOD frame: {}", e);
rdbuf.clear();
}
}
let loop_diag_resp = self.send_cmd(b"DIAG:LOOP?").await.unwrap();
let gps_sats_resp = self.send_cmd(b"GPSSAT").await.unwrap();
let loop_report = UCCMLoopDiagReport::try_from(loop_diag_resp.as_str());
let gps_report = UCCMGPSSatsReport::try_from(gps_sats_resp.as_str());
if let Ok(loop_report) = loop_report {
last_loop_diag = Some(loop_report)
} else {
warn!(
"Unable to parse loop diag report `{}`: {}",
loop_diag_resp,
loop_report.unwrap_err()
);
}
if let Ok(gps_report) = gps_report {
last_gps_sats = Some(gps_report)
} else {
warn!(
"Unable to parse GPS sats report `{}`: {}",
gps_sats_resp,
gps_report.unwrap_err()
);
}
}
UCCMMonitorParseState::ReadLoopDiag => todo!(),
UCCMMonitorParseState::ReadStatus => todo!(),
UCCMMonitorParseState::ReadTOD => todo!(),
}
}
}
}
#[async_trait]
impl ChimemonSource for UCCMMonitor {
type Config = UCCMConfig;
fn new(name: &str, config: Self::Config) -> 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);
UCCMMonitor {
name: name.to_owned(),
config,
rx,
tx,
info: None,
}
}
async fn run(mut self, chan: ChimemonSourceChannel, cancel: CancellationToken) {
info!("UCCM task starting");
if self.get_info().await.is_err() {
warn!("Error starting UCCM");
return;
}
self.send_cmd(b"SCPI").await.unwrap();
self.send_cmd(b"SYSTem:PRESet").await.unwrap();
self.send_cmd(b"OUTPut:TP:SELection PP1S").await.unwrap();
self.send_cmd(b"TESTMODE EN").await.unwrap();
self.send_cmd(b"TOD EN").await.unwrap();
let state = Arc::new(Mutex::<UCCMMonitorParseState>::new(
UCCMMonitorParseState::Idle,
));
let rx_handle = tokio::spawn(self.rx_loop(chan.clone(), state.clone()));
// let tx_handle = tokio::spawn(async move {
// let mut interval = interval(self.config.status_interval);
// loop {
// interval.tick().await;
// let wfut = self.tx.write_all(b"SYST:STAT?\n");
// *state.lock().await = UCCMMonitorParseState::ReadStatus;
// wfut.await;
// }
// });
//
select! {
_ = cancel.cancelled() => { return },
_ = rx_handle => { return }
};
join!(rx_handle).0.unwrap();
}
}