std_rs/device_support/
time_of_day.rs1use 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::record::{ProcessContext, Record};
6use epics_base_rs::types::EpicsValue;
7
8use chrono::Local;
9
10const EPICS_EPOCH_OFFSET: u64 = 631152000;
12
13#[derive(Default)]
22pub struct TimeOfDayStringDeviceSupport {
23 phas: i16,
30}
31
32impl TimeOfDayStringDeviceSupport {
33 pub fn new() -> Self {
34 Self::default()
35 }
36}
37
38impl DeviceSupport for TimeOfDayStringDeviceSupport {
39 fn dtyp(&self) -> &str {
40 "Time of Day"
41 }
42
43 fn set_process_context(&mut self, ctx: &ProcessContext) {
44 self.phas = ctx.phas;
45 }
46
47 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
48 let now = Local::now();
49
50 let phas = self.phas;
55
56 let formatted = if phas != 0 {
57 now.format("%m/%d/%y %H:%M:%S").to_string()
58 } else {
59 now.format("%b %d, %Y %H:%M:%S").to_string()
60 };
61
62 record.put_field("VAL", EpicsValue::String(formatted))?;
63 Ok(DeviceReadOutcome::computed())
64 }
65
66 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
67 Ok(())
68 }
69}
70
71#[derive(Default)]
78pub struct SecPastEpochDeviceSupport {
79 phas: i16,
83}
84
85impl SecPastEpochDeviceSupport {
86 pub fn new() -> Self {
87 Self::default()
88 }
89}
90
91impl DeviceSupport for SecPastEpochDeviceSupport {
92 fn dtyp(&self) -> &str {
93 "Sec Past Epoch"
94 }
95
96 fn set_process_context(&mut self, ctx: &ProcessContext) {
97 self.phas = ctx.phas;
98 }
99
100 fn read(&mut self, record: &mut dyn Record) -> CaResult<DeviceReadOutcome> {
101 let now = SystemTime::now()
102 .duration_since(UNIX_EPOCH)
103 .unwrap_or_default();
104
105 let sec_past_epoch = now.as_secs().saturating_sub(EPICS_EPOCH_OFFSET);
106
107 let phas = self.phas;
111
112 let val = if phas != 0 {
113 sec_past_epoch as f64 + (now.subsec_nanos() as f64 / 1e9)
114 } else {
115 sec_past_epoch as f64
116 };
117
118 record.put_field("VAL", EpicsValue::Double(val))?;
119 Ok(DeviceReadOutcome::computed())
120 }
121
122 fn write(&mut self, _record: &mut dyn Record) -> CaResult<()> {
123 Ok(())
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use epics_base_rs::server::record::ProcessContext;
131 use epics_base_rs::server::records::stringin::StringinRecord;
132
133 fn ctx_with_phas(phas: i16) -> ProcessContext {
134 ProcessContext {
135 udf: false,
136 udfs: epics_base_rs::server::record::AlarmSeverity::Invalid,
137 phas,
138 tse: 0,
139 tsel: String::new(),
140 dtyp: String::new(),
141 }
142 }
143
144 #[test]
150 fn time_of_day_phas_zero_uses_long_format() {
151 let mut dev = TimeOfDayStringDeviceSupport::new();
152 let mut rec = StringinRecord::new("");
153 dev.set_process_context(&ctx_with_phas(0));
154 dev.read(&mut rec).unwrap();
155 let val = match rec.get_field("VAL") {
156 Some(EpicsValue::String(s)) => s,
157 other => panic!("expected String VAL, got {other:?}"),
158 };
159 assert!(val.contains(','), "PHAS=0 long format has a comma: {val}");
161 assert!(
162 !val.contains('/'),
163 "PHAS=0 long format has no slashes: {val}"
164 );
165 }
166
167 #[test]
171 fn time_of_day_phas_nonzero_uses_slash_format() {
172 let mut dev = TimeOfDayStringDeviceSupport::new();
173 let mut rec = StringinRecord::new("");
174 dev.set_process_context(&ctx_with_phas(1));
175 dev.read(&mut rec).unwrap();
176 let val = match rec.get_field("VAL") {
177 Some(EpicsValue::String(s)) => s,
178 other => panic!("expected String VAL, got {other:?}"),
179 };
180 assert_eq!(
182 val.matches('/').count(),
183 2,
184 "PHAS!=0 slash format has two slashes: {val}"
185 );
186 assert!(
187 !val.contains(','),
188 "PHAS!=0 slash format has no comma: {val}"
189 );
190 }
191
192 #[test]
196 fn sec_past_epoch_phas_zero_is_whole_seconds() {
197 let mut dev = SecPastEpochDeviceSupport::new();
198 let mut rec = epics_base_rs::server::records::ai::AiRecord::new(0.0);
199 dev.set_process_context(&ctx_with_phas(0));
200 dev.read(&mut rec).unwrap();
201 let val = match rec.get_field("VAL") {
202 Some(EpicsValue::Double(v)) => v,
203 other => panic!("expected Double VAL, got {other:?}"),
204 };
205 assert_eq!(
206 val.fract(),
207 0.0,
208 "PHAS=0 must yield whole seconds, got {val}"
209 );
210 }
211}