use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{CaError, CaResult};
use crate::server::device_support::{DeviceReadOutcome, DeviceSupport};
use crate::server::recgbl::get_time_stamp;
use crate::server::record::{ProcessContext, Record};
use crate::types::EpicsValue;
use chrono::{DateTime, Local};
const EPICS_EPOCH_OFFSET_SECS: u64 = 631_152_000;
const NSEC_FRAC_DIGITS: u32 = 9;
const STRINGIN_VAL_BYTES: usize = 40;
const STRFTIME_PREFIX_BUF: usize = 256;
fn epics_time_parts(ts: SystemTime) -> (u64, u32) {
let dur = ts.duration_since(UNIX_EPOCH).unwrap_or_default();
(
dur.as_secs().saturating_sub(EPICS_EPOCH_OFFSET_SECS),
dur.subsec_nanos(),
)
}
fn frac_format_find(fmt: &str) -> (&str, Option<u32>, Option<&str>) {
let bytes = fmt.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 1 < bytes.len() && bytes[i + 1] == b'%' {
i += 2;
continue;
} else if i + 1 < bytes.len() && bytes[i + 1] == b'f' {
return (&fmt[..i], Some(u32::MAX), Some(&fmt[i + 2..]));
} else {
let mut j = i + 1;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j > i + 1 && j < bytes.len() && bytes[j] == b'f' {
if let Ok(w) = fmt[i + 1..j].parse::<u32>() {
if w > 0 {
return (&fmt[..i], Some(w), Some(&fmt[j + 1..]));
}
}
}
}
}
i += 1;
}
(fmt, None, None)
}
fn format_strftime_prefix(prefix: &str, dt: &DateTime<Local>) -> String {
use chrono::format::{Item, StrftimeItems};
let items: Vec<Item> = StrftimeItems::new(prefix).collect();
if items.iter().any(|it| matches!(it, Item::Error)) {
return prefix.to_string();
}
dt.format_with_items(items.into_iter()).to_string()
}
fn push_truncated(out: &mut String, s: &str, buf_len_left: usize) -> usize {
let n = s.len().min(buf_len_left.saturating_sub(1));
out.push_str(&s[..n]);
n
}
fn epics_time_to_strftime(format: &str, ts: SystemTime) -> String {
let (sec_past_epoch, nsec) = epics_time_parts(ts);
let mut out = String::new();
let mut buf_len_left = STRINGIN_VAL_BYTES;
if sec_past_epoch == 0 && nsec == 0 {
push_truncated(&mut out, "<undefined>", buf_len_left);
return out;
}
let dt: DateTime<Local> = ts.into();
let mut rest = format;
while !rest.is_empty() && buf_len_left > 1 {
let (raw_prefix, mut frac, after) = frac_format_find(rest);
let prefix;
let stop;
if raw_prefix.len() >= STRFTIME_PREFIX_BUF {
prefix = "<invalid format>";
frac = None;
stop = true;
} else {
prefix = raw_prefix;
stop = false;
}
if prefix.is_empty() && frac.is_none() {
break;
}
if !prefix.is_empty() {
let formatted = format_strftime_prefix(prefix, &dt);
if formatted.len() < buf_len_left {
out.push_str(&formatted);
buf_len_left -= formatted.len();
}
}
if let Some(w) = frac {
if buf_len_left > 1 {
let width = w.min(NSEC_FRAC_DIGITS) as usize;
if width < buf_len_left {
let div = 10u64.pow(NSEC_FRAC_DIGITS - width as u32);
let mut f = nsec as u64 + div / 2;
if f >= 1_000_000_000 {
f = 1_000_000_000 - 1;
}
f /= div;
let digits = format!("{f:0width$}");
buf_len_left -= push_truncated(&mut out, &digits, buf_len_left);
} else {
push_truncated(&mut out, "************", buf_len_left);
break;
}
}
}
if stop {
break;
}
rest = after.unwrap_or("");
}
out
}
pub struct SoftTimestampDeviceSupport {
format: String,
tse: i16,
time: SystemTime,
}
impl SoftTimestampDeviceSupport {
pub fn new(inp: &str) -> Self {
let trimmed = inp.trim();
let format = trimmed.strip_prefix('@').unwrap_or(trimmed).to_string();
Self {
format,
tse: 0,
time: SystemTime::UNIX_EPOCH,
}
}
}
impl DeviceSupport for SoftTimestampDeviceSupport {
fn dtyp(&self) -> &str {
"Soft Timestamp"
}
fn set_process_context(&mut self, ctx: &ProcessContext) {
self.tse = ctx.tse;
self.time = ctx.time;
}
fn init(&mut self, record: &mut dyn Record) -> CaResult<()> {
let rt = record.record_type();
if !matches!(rt, "ai" | "stringin") {
return Err(CaError::InvalidValue(format!(
"DTYP='Soft Timestamp': unsupported record type '{rt}' (use ai or stringin)"
)));
}
Ok(())
}
fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
let ts = get_time_stamp(self.tse, self.time);
match record.record_type() {
"ai" => {
let (sec, nsec) = epics_time_parts(ts);
let val = sec as f64 + nsec as f64 * 1e-9;
record.put_field("VAL", EpicsValue::Double(val))?;
Ok(DeviceReadOutcome::computed())
}
"stringin" => {
let s = epics_time_to_strftime(&self.format, ts);
record.put_field("VAL", EpicsValue::String(s.into()))?;
Ok(DeviceReadOutcome::computed())
}
other => Err(CaError::InvalidValue(format!(
"DTYP='Soft Timestamp': unsupported record type '{other}'"
))),
}
}
fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
Err(CaError::InvalidValue(
"Soft Timestamp device support is read-only (ai / stringin)".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::record::{AlarmSeverity, ProcessContext};
use crate::server::records::ai::AiRecord;
use crate::server::records::stringin::StringinRecord;
use std::time::Duration;
fn ctx_device_time(time: SystemTime) -> ProcessContext {
ProcessContext {
udf: false,
udfs: AlarmSeverity::Invalid,
nsev: AlarmSeverity::NoAlarm,
phas: 0,
tse: -2,
time,
tsel: String::new(),
dtyp: String::new(),
}
}
fn fixed_device_time() -> SystemTime {
SystemTime::UNIX_EPOCH
+ Duration::from_secs(1_000_000_000)
+ Duration::from_nanos(250_000_000)
}
#[test]
fn ai_writes_seconds_past_epoch_with_fraction() {
let mut dev = SoftTimestampDeviceSupport::new("@n");
let mut rec = AiRecord::new(0.0);
dev.set_process_context(&ctx_device_time(fixed_device_time()));
dev.read(&mut rec).unwrap();
let expected = (1_000_000_000u64 - EPICS_EPOCH_OFFSET_SECS) as f64 + 0.25;
match rec.get_field("VAL") {
Some(EpicsValue::Double(v)) => assert!(
(v - expected).abs() < 1e-6,
"ai VAL must be secPastEpoch+frac, got {v} expected {expected}"
),
other => panic!("expected Double VAL, got {other:?}"),
}
}
#[test]
fn stringin_formats_device_time() {
let mut dev = SoftTimestampDeviceSupport::new("@%Y-%m-%d");
let mut rec = StringinRecord::new("");
dev.set_process_context(&ctx_device_time(fixed_device_time()));
dev.read(&mut rec).unwrap();
match rec.get_field("VAL") {
Some(EpicsValue::String(s)) => {
let s = s.as_str_lossy();
assert!(s.starts_with("2001-"), "expected 2001 date, got {s}");
}
other => panic!("expected String VAL, got {other:?}"),
}
}
#[test]
fn stringin_epoch_is_undefined() {
let mut dev = SoftTimestampDeviceSupport::new("@%Y-%m-%d %H:%M:%S");
let mut rec = StringinRecord::new("");
dev.set_process_context(&ctx_device_time(SystemTime::UNIX_EPOCH));
dev.read(&mut rec).unwrap();
match rec.get_field("VAL") {
Some(EpicsValue::String(s)) => {
assert_eq!(s.as_str_lossy(), "<undefined>");
}
other => panic!("expected String VAL, got {other:?}"),
}
}
#[test]
fn init_rejects_unsupported_record_type() {
use crate::server::records::bo::BoRecord;
let mut dev = SoftTimestampDeviceSupport::new("@n");
let mut rec = BoRecord::default();
assert!(dev.init(&mut rec).is_err());
}
#[test]
fn new_strips_at_prefix() {
let dev = SoftTimestampDeviceSupport::new("@%Y-%m-%d");
assert_eq!(dev.format, "%Y-%m-%d");
let dev = SoftTimestampDeviceSupport::new(" %H:%M ");
assert_eq!(dev.format, "%H:%M");
}
#[test]
fn frac_format_find_splits_fractional_token() {
let (p, w, r) = frac_format_find("%S.%03f");
assert_eq!(p, "%S.");
assert_eq!(w, Some(3));
assert_eq!(r, Some(""));
let (p, w, _) = frac_format_find("%S.%f");
assert_eq!(p, "%S.");
assert_eq!(w, Some(u32::MAX));
let (p, w, r) = frac_format_find("%Y-%m-%d %H:%M:%S");
assert_eq!(p, "%Y-%m-%d %H:%M:%S");
assert_eq!(w, None);
assert_eq!(r, None);
let (p, w, _) = frac_format_find("100%%%2f");
assert_eq!(p, "100%%");
assert_eq!(w, Some(2));
}
#[test]
fn strftime_fractional_seconds_round() {
let t = SystemTime::UNIX_EPOCH
+ Duration::from_secs(1_000_000_000)
+ Duration::from_nanos(250_000_000);
assert!(epics_time_to_strftime("%S.%03f", t).ends_with(".250"));
assert!(epics_time_to_strftime("%S.%9f", t).ends_with(".250000000"));
assert!(epics_time_to_strftime("%S.%1f", t).ends_with(".3"));
}
#[test]
fn stringin_long_format_truncates_without_alarm() {
let fmt = "@%Y-%m-%d %H:%M:%S.%3f trailing zzzzzzzzzzzzzzzzzzzzzzzzz";
let mut dev = SoftTimestampDeviceSupport::new(fmt);
let mut rec = StringinRecord::new("");
dev.set_process_context(&ctx_device_time(fixed_device_time()));
let outcome = dev.read(&mut rec).expect("no alarm on a too-long format");
assert!(outcome.did_compute, "stringin read returns computed (C: 2)");
match rec.get_field("VAL") {
Some(EpicsValue::String(s)) => {
let s = s.as_str_lossy();
assert!(s.starts_with("2001-"), "date segment kept, got {s:?}");
assert!(s.ends_with(".250"), "3-digit fraction kept, got {s:?}");
assert!(
!s.contains('z'),
"overflowing tail omitted whole, got {s:?}"
);
assert!(s.len() <= 39, "VAL bounded to ≤39 bytes, got {}", s.len());
}
other => panic!("expected String VAL, got {other:?}"),
}
}
#[test]
fn strftime_fractional_field_overflow_emits_stars() {
let t = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
let prefix = "x".repeat(35);
let out = epics_time_to_strftime(&format!("{prefix}%9f"), t);
assert_eq!(out, format!("{prefix}****"));
assert_eq!(out.len(), 39);
}
#[test]
fn strftime_prefix_too_long_emits_invalid_format() {
let t = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
let out = epics_time_to_strftime(&format!("{}%f", "a".repeat(256)), t);
assert_eq!(out, "<invalid format>");
}
#[test]
fn strftime_invalid_format_omitted_whole_when_no_room() {
let t = SystemTime::UNIX_EPOCH
+ Duration::from_secs(1_000_000_000)
+ Duration::from_nanos(250_000_000);
let fmt = format!("{}%1f{}%f", "x".repeat(35), "a".repeat(256));
let out = epics_time_to_strftime(&fmt, t);
assert_eq!(out, format!("{}3", "x".repeat(35)));
assert!(
!out.contains('<'),
"no truncated sentinel fragment, got {out:?}"
);
}
}