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 {
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 tsel: String::new(),
233 dtyp: String::new(),
234 callback_priority: epics_base_rs::runtime::task::CallbackPriority::Low,
235 }
236 }
237
238 fn ctx_device_time(phas: i16, time: SystemTime) -> ProcessContext {
242 ProcessContext {
243 udf: false,
244 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
245 nsev: epics_base_rs::server::record::AlarmSeverity::NoAlarm,
246 phas,
247 tse: -2,
248 time,
249 tsel: String::new(),
250 dtyp: String::new(),
251 callback_priority: epics_base_rs::runtime::task::CallbackPriority::Low,
252 }
253 }
254
255 #[test]
261 fn time_of_day_phas_zero_uses_long_format() {
262 let mut dev = TimeOfDayStringDeviceSupport::new();
263 let mut rec = StringinRecord::new("");
264 dev.set_process_context(&ctx_with_phas(0));
265 dev.read(&mut rec).unwrap();
266 let val = match rec.get_field("VAL") {
267 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
268 other => panic!("expected String VAL, got {other:?}"),
269 };
270 assert!(val.contains(','), "PHAS=0 long format has a comma: {val}");
272 assert!(
273 !val.contains('/'),
274 "PHAS=0 long format has no slashes: {val}"
275 );
276 }
277
278 #[test]
282 fn time_of_day_phas_nonzero_uses_slash_format() {
283 let mut dev = TimeOfDayStringDeviceSupport::new();
284 let mut rec = StringinRecord::new("");
285 dev.set_process_context(&ctx_with_phas(1));
286 dev.read(&mut rec).unwrap();
287 let val = match rec.get_field("VAL") {
288 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
289 other => panic!("expected String VAL, got {other:?}"),
290 };
291 assert_eq!(
293 val.matches('/').count(),
294 2,
295 "PHAS!=0 slash format has two slashes: {val}"
296 );
297 assert!(
298 !val.contains(','),
299 "PHAS!=0 slash format has no comma: {val}"
300 );
301 }
302
303 #[test]
307 fn sec_past_epoch_phas_zero_is_whole_seconds() {
308 let mut dev = SecPastEpochDeviceSupport::new();
309 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
310 dev.set_process_context(&ctx_with_phas(0));
311 dev.read(&mut rec).unwrap();
312 let val = match rec.get_field("VAL") {
313 Some(EpicsValue::Double(v)) => v,
314 other => panic!("expected Double VAL, got {other:?}"),
315 };
316 assert_eq!(
317 val.fract(),
318 0.0,
319 "PHAS=0 must yield whole seconds, got {val}"
320 );
321 }
322
323 #[test]
329 fn time_of_day_epoch_zero_is_undefined() {
330 let mut dev = TimeOfDayStringDeviceSupport::new();
331 let mut rec = StringinRecord::new("");
332 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
333 dev.read(&mut rec).unwrap();
334 let val = match rec.get_field("VAL") {
335 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
336 other => panic!("expected String VAL, got {other:?}"),
337 };
338 assert_eq!(val, "<undefined>", "epoch stamp must format as sentinel");
339 }
340
341 #[test]
345 fn time_of_day_epics_epoch_is_undefined() {
346 let mut dev = TimeOfDayStringDeviceSupport::new();
347 let mut rec = StringinRecord::new("");
348 let epics_epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET);
349 dev.set_process_context(&ctx_device_time(0, epics_epoch));
350 dev.read(&mut rec).unwrap();
351 let val = match rec.get_field("VAL") {
352 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
353 other => panic!("expected String VAL, got {other:?}"),
354 };
355 assert_eq!(val, "<undefined>", "EPICS-epoch stamp is also undefined");
356 }
357
358 #[test]
364 fn time_of_day_pre_1990_clock_is_a_date_not_the_undefined_sentinel() {
365 let mut dev = TimeOfDayStringDeviceSupport::new();
366 let mut rec = StringinRecord::new("");
367 let before = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET - 1);
368 dev.set_process_context(&ctx_device_time(0, before));
369 dev.read(&mut rec).unwrap();
370 let val = match rec.get_field("VAL") {
371 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
372 other => panic!("expected String VAL, got {other:?}"),
373 };
374 assert_ne!(val, "<undefined>", "a pre-1990 clock is not C's {{0, 0}}");
375 }
376
377 #[test]
379 fn epics_time_parts_wraps_both_ends_and_keeps_the_unset_sentinel() {
380 assert_eq!(epics_time_parts(SystemTime::UNIX_EPOCH), (0, 0));
381 let at = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET);
382 assert_eq!(epics_time_parts(at).0, 0);
383 let before = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET - 1);
384 assert_eq!(epics_time_parts(before).0, u32::MAX);
385 let last =
386 SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET + u32::MAX as u64);
387 assert_eq!(epics_time_parts(last).0, u32::MAX);
388 let past =
389 SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET + u32::MAX as u64 + 1);
390 assert_eq!(epics_time_parts(past).0, 0);
391 }
392
393 #[test]
398 fn time_of_day_resolves_device_time_not_wall_clock() {
399 let mut dev = TimeOfDayStringDeviceSupport::new();
400 let mut rec = StringinRecord::new("");
401 let device_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
402 dev.set_process_context(&ctx_device_time(0, device_time));
403 dev.read(&mut rec).unwrap();
404 let val = match rec.get_field("VAL") {
405 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
406 other => panic!("expected String VAL, got {other:?}"),
407 };
408 assert!(val.contains("2001"), "must format the device time: {val}");
412 assert_ne!(val, "<undefined>");
413 }
414
415 #[test]
419 fn sec_past_epoch_epoch_zero_is_zero() {
420 let mut dev = SecPastEpochDeviceSupport::new();
421 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
422 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
423 dev.read(&mut rec).unwrap();
424 let val = match rec.get_field("VAL") {
425 Some(EpicsValue::Double(v)) => v,
426 other => panic!("expected Double VAL, got {other:?}"),
427 };
428 assert_eq!(val, 0.0, "epoch stamp yields secPastEpoch 0");
429 }
430
431 #[test]
436 fn sec_past_epoch_resolves_device_time_with_fraction() {
437 let device_time = SystemTime::UNIX_EPOCH
438 + Duration::from_secs(1_000_000_000)
439 + Duration::from_nanos(500_000_000);
440 let expected_secs = (1_000_000_000u64 - EPICS_EPOCH_OFFSET) as f64;
441
442 let mut dev = SecPastEpochDeviceSupport::new();
444 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
445 dev.set_process_context(&ctx_device_time(0, device_time));
446 dev.read(&mut rec).unwrap();
447 match rec.get_field("VAL") {
448 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs),
449 other => panic!("expected Double VAL, got {other:?}"),
450 };
451
452 let mut dev = SecPastEpochDeviceSupport::new();
454 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
455 dev.set_process_context(&ctx_device_time(1, device_time));
456 dev.read(&mut rec).unwrap();
457 match rec.get_field("VAL") {
458 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs + 0.5),
459 other => panic!("expected Double VAL, got {other:?}"),
460 };
461 }
462}