use std::time::{SystemTime, UNIX_EPOCH};
use epics_base_rs::error::CaResult;
use epics_base_rs::server::device_support::{DeviceReadOutcome, DeviceSupport};
use epics_base_rs::server::record::{ProcessContext, Record};
use epics_base_rs::types::EpicsValue;
use chrono::Local;
const EPICS_EPOCH_OFFSET: u64 = 631152000;
#[derive(Default)]
pub struct TimeOfDayStringDeviceSupport {
phas: i16,
}
impl TimeOfDayStringDeviceSupport {
pub fn new() -> Self {
Self::default()
}
}
impl DeviceSupport for TimeOfDayStringDeviceSupport {
fn dtyp(&self) -> &str {
"Time of Day"
}
fn set_process_context(&mut self, ctx: &ProcessContext) {
self.phas = ctx.phas;
}
fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
let now = Local::now();
let phas = self.phas;
let formatted = if phas != 0 {
now.format("%m/%d/%y %H:%M:%S").to_string()
} else {
now.format("%b %d, %Y %H:%M:%S").to_string()
};
record.put_field("VAL", EpicsValue::String(formatted))?;
Ok(DeviceReadOutcome::computed())
}
fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
Ok(())
}
}
#[derive(Default)]
pub struct SecPastEpochDeviceSupport {
phas: i16,
}
impl SecPastEpochDeviceSupport {
pub fn new() -> Self {
Self::default()
}
}
impl DeviceSupport for SecPastEpochDeviceSupport {
fn dtyp(&self) -> &str {
"Sec Past Epoch"
}
fn set_process_context(&mut self, ctx: &ProcessContext) {
self.phas = ctx.phas;
}
fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let sec_past_epoch = now.as_secs().saturating_sub(EPICS_EPOCH_OFFSET);
let phas = self.phas;
let val = if phas != 0 {
sec_past_epoch as f64 + (now.subsec_nanos() as f64 / 1e9)
} else {
sec_past_epoch as f64
};
record.put_field("VAL", EpicsValue::Double(val))?;
Ok(DeviceReadOutcome::computed())
}
fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use epics_base_rs::server::record::ProcessContext;
use epics_base_rs::server::records::stringin::StringinRecord;
fn ctx_with_phas(phas: i16) -> ProcessContext {
ProcessContext {
udf: false,
udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
phas,
tse: 0,
tsel: String::new(),
dtyp: String::new(),
}
}
#[test]
fn time_of_day_phas_zero_uses_long_format() {
let mut dev = TimeOfDayStringDeviceSupport::new();
let mut rec = StringinRecord::new("");
dev.set_process_context(&ctx_with_phas(0));
dev.read(&mut rec).unwrap();
let val = match rec.get_field("VAL") {
Some(EpicsValue::String(s)) => s,
other => panic!("expected String VAL, got {other:?}"),
};
assert!(val.contains(','), "PHAS=0 long format has a comma: {val}");
assert!(
!val.contains('/'),
"PHAS=0 long format has no slashes: {val}"
);
}
#[test]
fn time_of_day_phas_nonzero_uses_slash_format() {
let mut dev = TimeOfDayStringDeviceSupport::new();
let mut rec = StringinRecord::new("");
dev.set_process_context(&ctx_with_phas(1));
dev.read(&mut rec).unwrap();
let val = match rec.get_field("VAL") {
Some(EpicsValue::String(s)) => s,
other => panic!("expected String VAL, got {other:?}"),
};
assert_eq!(
val.matches('/').count(),
2,
"PHAS!=0 slash format has two slashes: {val}"
);
assert!(
!val.contains(','),
"PHAS!=0 slash format has no comma: {val}"
);
}
#[test]
fn sec_past_epoch_phas_zero_is_whole_seconds() {
let mut dev = SecPastEpochDeviceSupport::new();
let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
dev.set_process_context(&ctx_with_phas(0));
dev.read(&mut rec).unwrap();
let val = match rec.get_field("VAL") {
Some(EpicsValue::Double(v)) => v,
other => panic!("expected Double VAL, got {other:?}"),
};
assert_eq!(
val.fract(),
0.0,
"PHAS=0 must yield whole seconds, got {val}"
);
}
}