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 phas,
208 tse: 0,
209 time: SystemTime::UNIX_EPOCH,
210 tsel: String::new(),
211 dtyp: String::new(),
212 }
213 }
214
215 fn ctx_device_time(phas: i16, time: SystemTime) -> ProcessContext {
219 ProcessContext {
220 udf: false,
221 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
222 phas,
223 tse: -2,
224 time,
225 tsel: String::new(),
226 dtyp: String::new(),
227 }
228 }
229
230 #[test]
236 fn time_of_day_phas_zero_uses_long_format() {
237 let mut dev = TimeOfDayStringDeviceSupport::new();
238 let mut rec = StringinRecord::new("");
239 dev.set_process_context(&ctx_with_phas(0));
240 dev.read(&mut rec).unwrap();
241 let val = match rec.get_field("VAL") {
242 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
243 other => panic!("expected String VAL, got {other:?}"),
244 };
245 assert!(val.contains(','), "PHAS=0 long format has a comma: {val}");
247 assert!(
248 !val.contains('/'),
249 "PHAS=0 long format has no slashes: {val}"
250 );
251 }
252
253 #[test]
257 fn time_of_day_phas_nonzero_uses_slash_format() {
258 let mut dev = TimeOfDayStringDeviceSupport::new();
259 let mut rec = StringinRecord::new("");
260 dev.set_process_context(&ctx_with_phas(1));
261 dev.read(&mut rec).unwrap();
262 let val = match rec.get_field("VAL") {
263 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
264 other => panic!("expected String VAL, got {other:?}"),
265 };
266 assert_eq!(
268 val.matches('/').count(),
269 2,
270 "PHAS!=0 slash format has two slashes: {val}"
271 );
272 assert!(
273 !val.contains(','),
274 "PHAS!=0 slash format has no comma: {val}"
275 );
276 }
277
278 #[test]
282 fn sec_past_epoch_phas_zero_is_whole_seconds() {
283 let mut dev = SecPastEpochDeviceSupport::new();
284 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
285 dev.set_process_context(&ctx_with_phas(0));
286 dev.read(&mut rec).unwrap();
287 let val = match rec.get_field("VAL") {
288 Some(EpicsValue::Double(v)) => v,
289 other => panic!("expected Double VAL, got {other:?}"),
290 };
291 assert_eq!(
292 val.fract(),
293 0.0,
294 "PHAS=0 must yield whole seconds, got {val}"
295 );
296 }
297
298 #[test]
304 fn time_of_day_epoch_zero_is_undefined() {
305 let mut dev = TimeOfDayStringDeviceSupport::new();
306 let mut rec = StringinRecord::new("");
307 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
308 dev.read(&mut rec).unwrap();
309 let val = match rec.get_field("VAL") {
310 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
311 other => panic!("expected String VAL, got {other:?}"),
312 };
313 assert_eq!(val, "<undefined>", "epoch stamp must format as sentinel");
314 }
315
316 #[test]
320 fn time_of_day_epics_epoch_is_undefined() {
321 let mut dev = TimeOfDayStringDeviceSupport::new();
322 let mut rec = StringinRecord::new("");
323 let epics_epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_EPOCH_OFFSET);
324 dev.set_process_context(&ctx_device_time(0, epics_epoch));
325 dev.read(&mut rec).unwrap();
326 let val = match rec.get_field("VAL") {
327 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
328 other => panic!("expected String VAL, got {other:?}"),
329 };
330 assert_eq!(val, "<undefined>", "EPICS-epoch stamp is also undefined");
331 }
332
333 #[test]
338 fn time_of_day_resolves_device_time_not_wall_clock() {
339 let mut dev = TimeOfDayStringDeviceSupport::new();
340 let mut rec = StringinRecord::new("");
341 let device_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000);
342 dev.set_process_context(&ctx_device_time(0, device_time));
343 dev.read(&mut rec).unwrap();
344 let val = match rec.get_field("VAL") {
345 Some(EpicsValue::String(s)) => s.as_str_lossy().into_owned(),
346 other => panic!("expected String VAL, got {other:?}"),
347 };
348 assert!(val.contains("2001"), "must format the device time: {val}");
352 assert_ne!(val, "<undefined>");
353 }
354
355 #[test]
359 fn sec_past_epoch_epoch_zero_is_zero() {
360 let mut dev = SecPastEpochDeviceSupport::new();
361 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
362 dev.set_process_context(&ctx_device_time(0, SystemTime::UNIX_EPOCH));
363 dev.read(&mut rec).unwrap();
364 let val = match rec.get_field("VAL") {
365 Some(EpicsValue::Double(v)) => v,
366 other => panic!("expected Double VAL, got {other:?}"),
367 };
368 assert_eq!(val, 0.0, "epoch stamp yields secPastEpoch 0");
369 }
370
371 #[test]
376 fn sec_past_epoch_resolves_device_time_with_fraction() {
377 let device_time = SystemTime::UNIX_EPOCH
378 + Duration::from_secs(1_000_000_000)
379 + Duration::from_nanos(500_000_000);
380 let expected_secs = (1_000_000_000u64 - EPICS_EPOCH_OFFSET) as f64;
381
382 let mut dev = SecPastEpochDeviceSupport::new();
384 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
385 dev.set_process_context(&ctx_device_time(0, device_time));
386 dev.read(&mut rec).unwrap();
387 match rec.get_field("VAL") {
388 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs),
389 other => panic!("expected Double VAL, got {other:?}"),
390 };
391
392 let mut dev = SecPastEpochDeviceSupport::new();
394 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
395 dev.set_process_context(&ctx_device_time(1, device_time));
396 dev.read(&mut rec).unwrap();
397 match rec.get_field("VAL") {
398 Some(EpicsValue::Double(v)) => assert_eq!(v, expected_secs + 0.5),
399 other => panic!("expected Double VAL, got {other:?}"),
400 };
401 }
402}