use epics_base_rs::error::{CaError, CaResult};
use epics_base_rs::server::record::{
EPICS_TIME_EVENT_DEVICE_TIME, FieldDesc, ProcessContext, ProcessOutcome, Record,
};
use epics_base_rs::types::{DbFieldType, EpicsValue};
use chrono::{Local, TimeZone};
const EPICS_EPOCH_OFFSET: i64 = 631152000;
const VAL_VISIBLE_MAX: usize = 39;
const TIMESTAMP_FORMATS: &[&str] = &[
"%y/%m/%d %H:%M:%S", "%m/%d/%y %H:%M:%S", "%b %d %H:%M:%S %y", "%b %d %H:%M:%S", "%H:%M:%S", "%H:%M", "%d/%m/%y %H:%M:%S", "%d %b %H:%M:%S %y", "%d-%b-%Y %H:%M:%S", ];
pub struct TimestampRecord {
pub val: String,
pub oval: String,
pub rval: i32,
pub tst: i16,
tse: i16,
}
impl Default for TimestampRecord {
fn default() -> Self {
Self {
val: String::new(),
oval: String::new(),
rval: 0,
tst: 0,
tse: 0,
}
}
}
static FIELDS: &[FieldDesc] = &[
FieldDesc {
name: "VAL",
dbf_type: DbFieldType::String,
read_only: false,
},
FieldDesc {
name: "OVAL",
dbf_type: DbFieldType::String,
read_only: true,
},
FieldDesc {
name: "RVAL",
dbf_type: DbFieldType::Long,
read_only: false,
},
FieldDesc {
name: "TST",
dbf_type: DbFieldType::Short,
read_only: false,
},
];
impl TimestampRecord {
fn format_timestamp(&self) -> (String, i32) {
let now = if self.tse == EPICS_TIME_EVENT_DEVICE_TIME {
let secs = Local::now().timestamp();
Local
.timestamp_opt(secs, 0)
.single()
.unwrap_or_else(Local::now)
} else {
Local::now()
};
let unix_secs = now.timestamp();
let sec_past_epoch = (unix_secs - EPICS_EPOCH_OFFSET) as i32;
if sec_past_epoch == 0 {
return ("-NULL-".to_string(), sec_past_epoch);
}
let tst = self.tst;
let formatted = match tst {
0..=8 => now.format(TIMESTAMP_FORMATS[tst as usize]).to_string(),
9 | 10 => {
let ms = now.timestamp_subsec_millis();
let base = if tst == 9 {
now.format("%b %d %Y %H:%M:%S").to_string()
} else {
now.format("%m/%d/%y %H:%M:%S").to_string()
};
format!("{base}.{ms:03}")
}
_ => now.format(TIMESTAMP_FORMATS[0]).to_string(),
};
(truncate_to(formatted, VAL_VISIBLE_MAX), sec_past_epoch)
}
}
fn truncate_to(mut s: String, max: usize) -> String {
if s.len() > max {
let mut cut = max;
while cut > 0 && !s.is_char_boundary(cut) {
cut -= 1;
}
s.truncate(cut);
}
s
}
impl Record for TimestampRecord {
fn record_type(&self) -> &'static str {
"timestamp"
}
fn process(&mut self) -> CaResult<ProcessOutcome> {
let (formatted, sec_past_epoch) = self.format_timestamp();
self.oval = std::mem::replace(&mut self.val, formatted);
self.rval = sec_past_epoch;
Ok(ProcessOutcome::complete())
}
fn get_field(&self, name: &str) -> Option<EpicsValue> {
match name {
"VAL" => Some(EpicsValue::String(self.val.clone())),
"OVAL" => Some(EpicsValue::String(self.oval.clone())),
"RVAL" => Some(EpicsValue::Long(self.rval)),
"TST" => Some(EpicsValue::Short(self.tst)),
_ => None,
}
}
fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
match name {
"VAL" => match value {
EpicsValue::String(v) => {
self.val = truncate_to(v, VAL_VISIBLE_MAX);
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"RVAL" => match value {
EpicsValue::Long(v) => {
self.rval = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"TST" => match value {
EpicsValue::Short(v) => {
self.tst = v;
Ok(())
}
_ => Err(CaError::TypeMismatch(name.into())),
},
"OVAL" => Err(CaError::ReadOnlyField(name.into())),
_ => Err(CaError::FieldNotFound(name.into())),
}
}
fn field_list(&self) -> &'static [FieldDesc] {
FIELDS
}
fn set_process_context(&mut self, ctx: &ProcessContext) {
self.tse = ctx.tse;
}
fn clears_udf(&self) -> bool {
true
}
}