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, tx: WriteHalf, pub info: Option, } #[derive(Debug)] pub struct UCCMTODReport { pub time: DateTime, // 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 { 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) -> 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) -> 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) -> 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, } impl UCCMGPSSatsReport { pub fn build(&self, measurement: &String, tags: &Map) -> Vec { 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 { 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 { debug!("TOD buffer: `{:#?}`", String::from_utf8(strbuf.to_vec())); let resp: Vec = 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::().unwrap(); if c != 0xc500 { return Err(format!("Missing start delimter (got: `{}`)", c)); } // Consume 25 unknown bytes rdr.advance(25); let time = rdr.read_u32::().unwrap(); rdr.advance(1); // consume padding let leaps = rdr.read_i8().unwrap(); let flags = rdr.read_u32::().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 { 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::() .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 { 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::().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 { 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>, ) { let mut rdbuf = BytesMut::with_capacity(1024); let mut last_loop_diag: Option = None; let mut last_gps_sats: Option = 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::::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(); } }