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);
87 let (sec_past_epoch, nsec) = epics_time_parts(ts);
88
89 let phas = self.phas;
93
94 let formatted = if sec_past_epoch == 0 && nsec == 0 {
95 "<undefined>".to_string()
100 } else {
101 let local: DateTime<Local> = DateTime::<Local>::from(ts);
104 if phas != 0 {
105 local.format("%m/%d/%y %H:%M:%S").to_string()
106 } else {
107 local.format("%b %d, %Y %H:%M:%S").to_string()
108 }
109 };
110
111 record.put_field("VAL", EpicsValue::String(formatted.into()))?;
112 Ok(DeviceReadOutcome::computed())
113 }
114
115 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
116 Ok(())
117 }
118}
119
120pub struct SecPastEpochDeviceSupport {
127 phas: i16,
131 tse: i16,
137 time: SystemTime,
138}
139
140impl Default for SecPastEpochDeviceSupport {
141 fn default() -> Self {
142 Self {
143 phas: 0,
144 tse: 0,
145 time: SystemTime::UNIX_EPOCH,
146 }
147 }
148}
149
150impl SecPastEpochDeviceSupport {
151 pub fn new() -> Self {
152 Self::default()
153 }
154}
155
156impl DeviceSupport for SecPastEpochDeviceSupport {
157 fn dtyp(&self) -> &str {
158 "Sec Past Epoch"
159 }
160
161 fn set_process_context(&mut self, ctx: &ProcessContext) {
162 self.phas = ctx.phas;
163 self.tse = ctx.tse;
164 self.time = ctx.time;
165 }
166
167 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
168 let ts = get_time_stamp(self.tse, self.time);
174 let (sec_past_epoch, nsec) = epics_time_parts(ts);
175
176 let phas = self.phas;
180
181 let val = if phas != 0 {
182 sec_past_epoch as f64 + (nsec as f64 / 1e9)
183 } else {
184 sec_past_epoch as f64
185 };
186
187 record.put_field("VAL", EpicsValue::Double(val))?;
188 Ok(DeviceReadOutcome::computed())
189 }
190
191 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
192 Ok(())
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use epics_base_rs::server::record::ProcessContext;
200 use epics_base_rs::server::records::stringin::StringinRecord;
201 use std::time::Duration;
202
203 fn ctx_with_phas(phas: i16) -> ProcessContext {
204 ProcessContext {
205 udf: false,
206 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
207 nsev: epics_base_rs::server::record::AlarmSeverity::NoAlarm,
208 phas,
209 tse: 0,
210 time: SystemTime::UNIX_EPOCH,
211 tsel: String::new(),
212 dtyp: String::new(),
213 }
214 }
215
216 fn ctx_device_time(phas: i16, time: SystemTime) -> ProcessContext {
220 ProcessContext {
221 udf: false,
222 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
223 nsev: epics_base_rs::server::record::AlarmSeverity::NoAlarm,
224 phas,
225 tse: -2,
226 time,
227 tsel: String::new(),
228 dtyp: String::new(),
229 }
230 }
231
232 #[test]
238 fn time_of_day_phas_zero_uses_long_format() {
239 let mut dev = TimeOfDayStringDeviceSupport::new();
240 let mut rec = StringinRecord::new("");
241 dev.set_process_context(&ctx_with_phas(0));
242 dev.read(&mut rec).unwrap();
243 let val = match rec.get_field("VAL") {
244 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
245 other => panic!("expected String VAL, got {other:?}"),
246 };
247 assert!(val.contains(','), "PHAS=0 long format has a comma: {val}");
249 assert!(
250 !val.contains('/'),
251 "PHAS=0 long format has no slashes: {val}"
252 );
253 }
254
255 #[test]
259 fn time_of_day_phas_nonzero_uses_slash_format() {
260 let mut dev = TimeOfDayStringDeviceSupport::new();
261 let mut rec = StringinRecord::new("");
262 dev.set_process_context(&ctx_with_phas(1));
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_eq!(
270 val.matches('/').count(),
271 2,
272 "PHAS!=0 slash format has two slashes: {val}"
273 );
274 assert!(
275 !val.contains(','),
276 "PHAS!=0 slash format has no comma: {val}"
277 );
278 }
279
280 #[test]
284 fn sec_past_epoch_phas_zero_is_whole_seconds() {
285 let mut dev = SecPastEpochDeviceSupport::new();
286 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
287 dev.set_process_context(&ctx_with_phas(0));
288 dev.read(&mut rec).unwrap();
289 let val = match rec.get_field("VAL") {
290 Some(EpicsValue::Double(v)) => v,
291 other => panic!("expected Double VAL, got {other:?}"),
292 };
293 assert_eq!(
294 val.fract(),
295 0.0,
296 "PHAS=0 must yield whole seconds, got {val}"
297 );
298 }
299
300 #[test]
306 fn time_of_day_epoch_zero_is_undefined() {
307 let mut dev = TimeOfDayStringDeviceSupport::new();
308 let mut rec = StringinRecord::new("");
309 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
310 dev.read(&mut rec).unwrap();
311 let val = match rec.get_field("VAL") {
312 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
313 other => panic!("expected String VAL, got {other:?}"),
314 };
315 assert_eq!(val, "<undefined>", "epoch stamp must format as sentinel");
316 }
317
318 #[test]
322 fn time_of_day_epics_epoch_is_undefined() {
323 let mut dev = TimeOfDayStringDeviceSupport::new();
324 let mut rec = StringinRecord::new("");
325 let epics_epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET);
326 dev.set_process_context(&ctx_device_time(0, epics_epoch));
327 dev.read(&mut rec).unwrap();
328 let val = match rec.get_field("VAL") {
329 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
330 other => panic!("expected String VAL, got {other:?}"),
331 };
332 assert_eq!(val, "<undefined>", "EPICS-epoch stamp is also undefined");
333 }
334
335 #[test]
340 fn time_of_day_resolves_device_time_not_wall_clock() {
341 let mut dev = TimeOfDayStringDeviceSupport::new();
342 let mut rec = StringinRecord::new("");
343 let device_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
344 dev.set_process_context(&ctx_device_time(0, device_time));
345 dev.read(&mut rec).unwrap();
346 let val = match rec.get_field("VAL") {
347 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
348 other => panic!("expected String VAL, got {other:?}"),
349 };
350 assert!(val.contains("2001"), "must format the device time: {val}");
354 assert_ne!(val, "<undefined>");
355 }
356
357 #[test]
361 fn sec_past_epoch_epoch_zero_is_zero() {
362 let mut dev = SecPastEpochDeviceSupport::new();
363 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
364 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
365 dev.read(&mut rec).unwrap();
366 let val = match rec.get_field("VAL") {
367 Some(EpicsValue::Double(v)) => v,
368 other => panic!("expected Double VAL, got {other:?}"),
369 };
370 assert_eq!(val, 0.0, "epoch stamp yields secPastEpoch 0");
371 }
372
373 #[test]
378 fn sec_past_epoch_resolves_device_time_with_fraction() {
379 let device_time = SystemTime::UNIX_EPOCH
380 + Duration::from_secs(1_000_000_000)
381 + Duration::from_nanos(500_000_000);
382 let expected_secs = (1_000_000_000u64 - EPICS_EPOCH_OFFSET) as f64;
383
384 let mut dev = SecPastEpochDeviceSupport::new();
386 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
387 dev.set_process_context(&ctx_device_time(0, device_time));
388 dev.read(&mut rec).unwrap();
389 match rec.get_field("VAL") {
390 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs),
391 other => panic!("expected Double VAL, got {other:?}"),
392 };
393
394 let mut dev = SecPastEpochDeviceSupport::new();
396 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
397 dev.set_process_context(&ctx_device_time(1, device_time));
398 dev.read(&mut rec).unwrap();
399 match rec.get_field("VAL") {
400 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs + 0.5),
401 other => panic!("expected Double VAL, got {other:?}"),
402 };
403 }
404}