1use std::time::{SystemTime, UNIX_EPOCH};
2
3use epics_base_rs::error::CaResult;
4use epics_base_rs::server::device_support::{DeviceReadOutcome, DeviceSupport};
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) -> (u64, u32) {
23 let dur = ts.duration_since(UNIX_EPOCH).unwrap_or_default();
24 (
25 dur.as_secs().saturating_sub(EPICS_EPOCH_OFFSET),
26 dur.subsec_nanos(),
27 )
28}
29
30pub struct TimeOfDayStringDeviceSupport {
39 phas: i16,
46 tse: i16,
53 time: SystemTime,
54}
55
56impl Default for TimeOfDayStringDeviceSupport {
57 fn default() -> Self {
58 Self {
59 phas: 0,
60 tse: 0,
61 time: SystemTime::UNIX_EPOCH,
62 }
63 }
64}
65
66impl TimeOfDayStringDeviceSupport {
67 pub fn new() -> Self {
68 Self::default()
69 }
70}
71
72impl DeviceSupport for TimeOfDayStringDeviceSupport {
73 fn dtyp(&self) -> &str {
74 "Time of Day"
75 }
76
77 fn set_process_context(&mut self, ctx: &ProcessContext) {
78 self.phas = ctx.phas;
79 self.tse = ctx.tse;
80 self.time = ctx.time;
81 }
82
83 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
84 let ts = get_time_stamp(self.tse, self.time).unwrap_or(self.time);
90 let (sec_past_epoch, nsec) = epics_time_parts(ts);
91
92 let phas = self.phas;
96
97 let formatted = if sec_past_epoch == 0 && nsec == 0 {
98 "<undefined>".to_string()
103 } else {
104 let local: DateTime<Local> = DateTime::<Local>::from(ts);
107 if phas != 0 {
108 local.format("%m/%d/%y %H:%M:%S").to_string()
109 } else {
110 local.format("%b %d, %Y %H:%M:%S").to_string()
111 }
112 };
113
114 record.put_field("VAL", EpicsValue::String(formatted.into()))?;
115 Ok(DeviceReadOutcome::computed())
116 }
117
118 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
119 Ok(())
120 }
121}
122
123pub struct SecPastEpochDeviceSupport {
130 phas: i16,
134 tse: i16,
140 time: SystemTime,
141}
142
143impl Default for SecPastEpochDeviceSupport {
144 fn default() -> Self {
145 Self {
146 phas: 0,
147 tse: 0,
148 time: SystemTime::UNIX_EPOCH,
149 }
150 }
151}
152
153impl SecPastEpochDeviceSupport {
154 pub fn new() -> Self {
155 Self::default()
156 }
157}
158
159impl DeviceSupport for SecPastEpochDeviceSupport {
160 fn dtyp(&self) -> &str {
161 "Sec Past Epoch"
162 }
163
164 fn set_process_context(&mut self, ctx: &ProcessContext) {
165 self.phas = ctx.phas;
166 self.tse = ctx.tse;
167 self.time = ctx.time;
168 }
169
170 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
171 let ts = get_time_stamp(self.tse, self.time).unwrap_or(self.time);
180 let (sec_past_epoch, nsec) = epics_time_parts(ts);
181
182 let phas = self.phas;
186
187 let val = if phas != 0 {
188 sec_past_epoch as f64 + (nsec as f64 / 1e9)
189 } else {
190 sec_past_epoch as f64
191 };
192
193 record.put_field("VAL", EpicsValue::Double(val))?;
194 Ok(DeviceReadOutcome::computed())
195 }
196
197 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
198 Ok(())
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use epics_base_rs::server::record::ProcessContext;
206 use epics_base_rs::server::records::stringin::StringinRecord;
207 use std::time::Duration;
208
209 fn ctx_with_phas(phas: i16) -> ProcessContext {
210 ProcessContext {
211 udf: false,
212 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
213 nsev: epics_base_rs::server::record::AlarmSeverity::NoAlarm,
214 phas,
215 tse: 0,
216 time: SystemTime::UNIX_EPOCH,
217 tsel: String::new(),
218 dtyp: String::new(),
219 }
220 }
221
222 fn ctx_device_time(phas: i16, time: SystemTime) -> ProcessContext {
226 ProcessContext {
227 udf: false,
228 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
229 nsev: epics_base_rs::server::record::AlarmSeverity::NoAlarm,
230 phas,
231 tse: -2,
232 time,
233 tsel: String::new(),
234 dtyp: String::new(),
235 }
236 }
237
238 #[test]
244 fn time_of_day_phas_zero_uses_long_format() {
245 let mut dev = TimeOfDayStringDeviceSupport::new();
246 let mut rec = StringinRecord::new("");
247 dev.set_process_context(&ctx_with_phas(0));
248 dev.read(&mut rec).unwrap();
249 let val = match rec.get_field("VAL") {
250 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
251 other => panic!("expected String VAL, got {other:?}"),
252 };
253 assert!(val.contains(','), "PHAS=0 long format has a comma: {val}");
255 assert!(
256 !val.contains('/'),
257 "PHAS=0 long format has no slashes: {val}"
258 );
259 }
260
261 #[test]
265 fn time_of_day_phas_nonzero_uses_slash_format() {
266 let mut dev = TimeOfDayStringDeviceSupport::new();
267 let mut rec = StringinRecord::new("");
268 dev.set_process_context(&ctx_with_phas(1));
269 dev.read(&mut rec).unwrap();
270 let val = match rec.get_field("VAL") {
271 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
272 other => panic!("expected String VAL, got {other:?}"),
273 };
274 assert_eq!(
276 val.matches('/').count(),
277 2,
278 "PHAS!=0 slash format has two slashes: {val}"
279 );
280 assert!(
281 !val.contains(','),
282 "PHAS!=0 slash format has no comma: {val}"
283 );
284 }
285
286 #[test]
290 fn sec_past_epoch_phas_zero_is_whole_seconds() {
291 let mut dev = SecPastEpochDeviceSupport::new();
292 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
293 dev.set_process_context(&ctx_with_phas(0));
294 dev.read(&mut rec).unwrap();
295 let val = match rec.get_field("VAL") {
296 Some(EpicsValue::Double(v)) => v,
297 other => panic!("expected Double VAL, got {other:?}"),
298 };
299 assert_eq!(
300 val.fract(),
301 0.0,
302 "PHAS=0 must yield whole seconds, got {val}"
303 );
304 }
305
306 #[test]
312 fn time_of_day_epoch_zero_is_undefined() {
313 let mut dev = TimeOfDayStringDeviceSupport::new();
314 let mut rec = StringinRecord::new("");
315 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
316 dev.read(&mut rec).unwrap();
317 let val = match rec.get_field("VAL") {
318 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
319 other => panic!("expected String VAL, got {other:?}"),
320 };
321 assert_eq!(val, "<undefined>", "epoch stamp must format as sentinel");
322 }
323
324 #[test]
328 fn time_of_day_epics_epoch_is_undefined() {
329 let mut dev = TimeOfDayStringDeviceSupport::new();
330 let mut rec = StringinRecord::new("");
331 let epics_epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET);
332 dev.set_process_context(&ctx_device_time(0, epics_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>", "EPICS-epoch stamp is also undefined");
339 }
340
341 #[test]
346 fn time_of_day_resolves_device_time_not_wall_clock() {
347 let mut dev = TimeOfDayStringDeviceSupport::new();
348 let mut rec = StringinRecord::new("");
349 let device_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
350 dev.set_process_context(&ctx_device_time(0, device_time));
351 dev.read(&mut rec).unwrap();
352 let val = match rec.get_field("VAL") {
353 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
354 other => panic!("expected String VAL, got {other:?}"),
355 };
356 assert!(val.contains("2001"), "must format the device time: {val}");
360 assert_ne!(val, "<undefined>");
361 }
362
363 #[test]
367 fn sec_past_epoch_epoch_zero_is_zero() {
368 let mut dev = SecPastEpochDeviceSupport::new();
369 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
370 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
371 dev.read(&mut rec).unwrap();
372 let val = match rec.get_field("VAL") {
373 Some(EpicsValue::Double(v)) => v,
374 other => panic!("expected Double VAL, got {other:?}"),
375 };
376 assert_eq!(val, 0.0, "epoch stamp yields secPastEpoch 0");
377 }
378
379 #[test]
384 fn sec_past_epoch_resolves_device_time_with_fraction() {
385 let device_time = SystemTime::UNIX_EPOCH
386 + Duration::from_secs(1_000_000_000)
387 + Duration::from_nanos(500_000_000);
388 let expected_secs = (1_000_000_000u64 - EPICS_EPOCH_OFFSET) as f64;
389
390 let mut dev = SecPastEpochDeviceSupport::new();
392 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
393 dev.set_process_context(&ctx_device_time(0, device_time));
394 dev.read(&mut rec).unwrap();
395 match rec.get_field("VAL") {
396 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs),
397 other => panic!("expected Double VAL, got {other:?}"),
398 };
399
400 let mut dev = SecPastEpochDeviceSupport::new();
402 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
403 dev.set_process_context(&ctx_device_time(1, device_time));
404 dev.read(&mut rec).unwrap();
405 match rec.get_field("VAL") {
406 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs + 0.5),
407 other => panic!("expected Double VAL, got {other:?}"),
408 };
409 }
410}