1use std::time::{SystemTime, UNIX_EPOCH};
2
3use epics_base_rs::error::CaResult;
4use epics_base_rs::server::device_support::{DeviceReadOutcome, DeviceSupport, DeviceUdf};
5use epics_base_rs::server::recgbl::get_time_stamp;
6use epics_base_rs::server::record::{ProcessContext, Record};
7use epics_base_rs::types::EpicsValue;
8
9use chrono::{DateTime, Local};
10
11const EPICS_EPOCH_OFFSET: u64 = 631152000;
13
14fn epics_time_parts(ts: SystemTime) -> (u32, u32) {
25 if ts == UNIX_EPOCH {
30 return (0, 0);
31 }
32 let dur = ts.duration_since(UNIX_EPOCH).unwrap_or_default();
35 (
36 dur.as_secs().wrapping_sub(EPICS_EPOCH_OFFSET) as u32,
37 dur.subsec_nanos(),
38 )
39}
40
41pub struct TimeOfDayStringDeviceSupport {
50 phas: i16,
57 tse: i16,
64 time: SystemTime,
65}
66
67impl Default for TimeOfDayStringDeviceSupport {
68 fn default() -> Self {
69 Self {
70 phas: 0,
71 tse: 0,
72 time: SystemTime::UNIX_EPOCH,
73 }
74 }
75}
76
77impl TimeOfDayStringDeviceSupport {
78 pub fn new() -> Self {
79 Self::default()
80 }
81}
82
83impl DeviceSupport for TimeOfDayStringDeviceSupport {
84 fn dtyp(&self) -> &str {
85 "Time of Day"
86 }
87
88 fn set_process_context(&mut self, ctx: &ProcessContext) {
89 self.phas = ctx.phas;
90 self.tse = ctx.tse;
91 self.time = ctx.time;
92 }
93
94 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
95 let ts = get_time_stamp(self.tse, self.time).unwrap_or(self.time);
101 let (sec_past_epoch, nsec) = epics_time_parts(ts);
102
103 let phas = self.phas;
107
108 let formatted = if sec_past_epoch == 0 && nsec == 0 {
109 "<undefined>".to_string()
114 } else {
115 let local: DateTime<Local> = DateTime::<Local>::from(ts);
118 if phas != 0 {
119 local.format("%m/%d/%y %H:%M:%S").to_string()
120 } else {
121 local.format("%b %d, %Y %H:%M:%S").to_string()
122 }
123 };
124
125 record.put_field("VAL", EpicsValue::String(formatted.into()))?;
126 Ok(DeviceReadOutcome::converted(DeviceUdf::Defined))
129 }
130
131 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
132 Ok(())
133 }
134}
135
136pub struct SecPastEpochDeviceSupport {
143 phas: i16,
147 tse: i16,
153 time: SystemTime,
154}
155
156impl Default for SecPastEpochDeviceSupport {
157 fn default() -> Self {
158 Self {
159 phas: 0,
160 tse: 0,
161 time: SystemTime::UNIX_EPOCH,
162 }
163 }
164}
165
166impl SecPastEpochDeviceSupport {
167 pub fn new() -> Self {
168 Self::default()
169 }
170}
171
172impl DeviceSupport for SecPastEpochDeviceSupport {
173 fn dtyp(&self) -> &str {
174 "Sec Past Epoch"
175 }
176
177 fn set_process_context(&mut self, ctx: &ProcessContext) {
178 self.phas = ctx.phas;
179 self.tse = ctx.tse;
180 self.time = ctx.time;
181 }
182
183 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
184 let ts = get_time_stamp(self.tse, self.time).unwrap_or(self.time);
193 let (sec_past_epoch, nsec) = epics_time_parts(ts);
194
195 let phas = self.phas;
199
200 let val = if phas != 0 {
201 sec_past_epoch as f64 + (nsec as f64 / 1e9)
202 } else {
203 sec_past_epoch as f64
204 };
205
206 record.put_field("VAL", EpicsValue::Double(val))?;
207 Ok(DeviceReadOutcome::computed(DeviceUdf::Defined))
210 }
211
212 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
213 Ok(())
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use epics_base_rs::server::record::ProcessContext;
221 use epics_base_rs::server::records::stringin::StringinRecord;
222 use std::time::Duration;
223
224 fn ctx_with_phas(phas: i16) -> ProcessContext<'static> {
225 ProcessContext {
226 udf: false,
227 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
228 nsev: epics_base_rs::server::record::AlarmSeverity::NoAlarm,
229 phas,
230 tse: 0,
231 time: SystemTime::UNIX_EPOCH,
232 dtyp: "",
233 callback_priority: epics_base_rs::runtime::task::CallbackPriority::Low,
234 }
235 }
236
237 fn ctx_device_time(phas: i16, time: SystemTime) -> ProcessContext<'static> {
241 ProcessContext {
242 udf: false,
243 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
244 nsev: epics_base_rs::server::record::AlarmSeverity::NoAlarm,
245 phas,
246 tse: -2,
247 time,
248 dtyp: "",
249 callback_priority: epics_base_rs::runtime::task::CallbackPriority::Low,
250 }
251 }
252
253 #[test]
259 fn time_of_day_phas_zero_uses_long_format() {
260 let mut dev = TimeOfDayStringDeviceSupport::new();
261 let mut rec = StringinRecord::new("");
262 dev.set_process_context(&ctx_with_phas(0));
263 dev.read(&mut rec).unwrap();
264 let val = match rec.get_field("VAL") {
265 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
266 other => panic!("expected String VAL, got {other:?}"),
267 };
268 assert!(val.contains(','), "PHAS=0 long format has a comma: {val}");
270 assert!(
271 !val.contains('/'),
272 "PHAS=0 long format has no slashes: {val}"
273 );
274 }
275
276 #[test]
280 fn time_of_day_phas_nonzero_uses_slash_format() {
281 let mut dev = TimeOfDayStringDeviceSupport::new();
282 let mut rec = StringinRecord::new("");
283 dev.set_process_context(&ctx_with_phas(1));
284 dev.read(&mut rec).unwrap();
285 let val = match rec.get_field("VAL") {
286 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
287 other => panic!("expected String VAL, got {other:?}"),
288 };
289 assert_eq!(
291 val.matches('/').count(),
292 2,
293 "PHAS!=0 slash format has two slashes: {val}"
294 );
295 assert!(
296 !val.contains(','),
297 "PHAS!=0 slash format has no comma: {val}"
298 );
299 }
300
301 #[test]
305 fn sec_past_epoch_phas_zero_is_whole_seconds() {
306 let mut dev = SecPastEpochDeviceSupport::new();
307 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
308 dev.set_process_context(&ctx_with_phas(0));
309 dev.read(&mut rec).unwrap();
310 let val = match rec.get_field("VAL") {
311 Some(EpicsValue::Double(v)) => v,
312 other => panic!("expected Double VAL, got {other:?}"),
313 };
314 assert_eq!(
315 val.fract(),
316 0.0,
317 "PHAS=0 must yield whole seconds, got {val}"
318 );
319 }
320
321 #[test]
327 fn time_of_day_epoch_zero_is_undefined() {
328 let mut dev = TimeOfDayStringDeviceSupport::new();
329 let mut rec = StringinRecord::new("");
330 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
331 dev.read(&mut rec).unwrap();
332 let val = match rec.get_field("VAL") {
333 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
334 other => panic!("expected String VAL, got {other:?}"),
335 };
336 assert_eq!(val, "<undefined>", "epoch stamp must format as sentinel");
337 }
338
339 #[test]
343 fn time_of_day_epics_epoch_is_undefined() {
344 let mut dev = TimeOfDayStringDeviceSupport::new();
345 let mut rec = StringinRecord::new("");
346 let epics_epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET);
347 dev.set_process_context(&ctx_device_time(0, epics_epoch));
348 dev.read(&mut rec).unwrap();
349 let val = match rec.get_field("VAL") {
350 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
351 other => panic!("expected String VAL, got {other:?}"),
352 };
353 assert_eq!(val, "<undefined>", "EPICS-epoch stamp is also undefined");
354 }
355
356 #[test]
362 fn time_of_day_pre_1990_clock_is_a_date_not_the_undefined_sentinel() {
363 let mut dev = TimeOfDayStringDeviceSupport::new();
364 let mut rec = StringinRecord::new("");
365 let before = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET - 1);
366 dev.set_process_context(&ctx_device_time(0, before));
367 dev.read(&mut rec).unwrap();
368 let val = match rec.get_field("VAL") {
369 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
370 other => panic!("expected String VAL, got {other:?}"),
371 };
372 assert_ne!(val, "<undefined>", "a pre-1990 clock is not C's {{0, 0}}");
373 }
374
375 #[test]
377 fn epics_time_parts_wraps_both_ends_and_keeps_the_unset_sentinel() {
378 assert_eq!(epics_time_parts(SystemTime::UNIX_EPOCH), (0, 0));
379 let at = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET);
380 assert_eq!(epics_time_parts(at).0, 0);
381 let before = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET - 1);
382 assert_eq!(epics_time_parts(before).0, u32::MAX);
383 let last =
384 SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET + u32::MAX as u64);
385 assert_eq!(epics_time_parts(last).0, u32::MAX);
386 let past =
387 SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET + u32::MAX as u64 + 1);
388 assert_eq!(epics_time_parts(past).0, 0);
389 }
390
391 #[test]
396 fn time_of_day_resolves_device_time_not_wall_clock() {
397 let mut dev = TimeOfDayStringDeviceSupport::new();
398 let mut rec = StringinRecord::new("");
399 let device_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
400 dev.set_process_context(&ctx_device_time(0, device_time));
401 dev.read(&mut rec).unwrap();
402 let val = match rec.get_field("VAL") {
403 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
404 other => panic!("expected String VAL, got {other:?}"),
405 };
406 assert!(val.contains("2001"), "must format the device time: {val}");
410 assert_ne!(val, "<undefined>");
411 }
412
413 #[test]
417 fn sec_past_epoch_epoch_zero_is_zero() {
418 let mut dev = SecPastEpochDeviceSupport::new();
419 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
420 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
421 dev.read(&mut rec).unwrap();
422 let val = match rec.get_field("VAL") {
423 Some(EpicsValue::Double(v)) => v,
424 other => panic!("expected Double VAL, got {other:?}"),
425 };
426 assert_eq!(val, 0.0, "epoch stamp yields secPastEpoch 0");
427 }
428
429 #[test]
434 fn sec_past_epoch_resolves_device_time_with_fraction() {
435 let device_time = SystemTime::UNIX_EPOCH
436 + Duration::from_secs(1_000_000_000)
437 + Duration::from_nanos(500_000_000);
438 let expected_secs = (1_000_000_000u64 - EPICS_EPOCH_OFFSET) as f64;
439
440 let mut dev = SecPastEpochDeviceSupport::new();
442 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
443 dev.set_process_context(&ctx_device_time(0, device_time));
444 dev.read(&mut rec).unwrap();
445 match rec.get_field("VAL") {
446 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs),
447 other => panic!("expected Double VAL, got {other:?}"),
448 };
449
450 let mut dev = SecPastEpochDeviceSupport::new();
452 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
453 dev.set_process_context(&ctx_device_time(1, device_time));
454 dev.read(&mut rec).unwrap();
455 match rec.get_field("VAL") {
456 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs + 0.5),
457 other => panic!("expected Double VAL, got {other:?}"),
458 };
459 }
460}