Skip to main content

std_rs/records/
timestamp.rs

1use epics_base_rs::error::{CaError, CaResult};
2use epics_base_rs::server::record::{FieldDesc, ProcessOutcome, Record};
3use epics_base_rs::types::{DbFieldType, EpicsValue};
4
5use chrono::Local;
6
7/// EPICS epoch: 1990-01-01 00:00:00 UTC
8const EPICS_EPOCH_OFFSET: i64 = 631152000;
9
10/// Timestamp format strings indexed by TST field value.
11const TIMESTAMP_FORMATS: &[&str] = &[
12    "%y/%m/%d %H:%M:%S", // 0
13    "%m/%d/%y %H:%M:%S", // 1
14    "%b %d %H:%M:%S %y", // 2
15    "%b %d %H:%M:%S",    // 3
16    "%H:%M:%S",          // 4
17    "%H:%M",             // 5
18    "%d/%m/%y %H:%M:%S", // 6
19    "%d %b %H:%M:%S %y", // 7
20    "%d-%b-%Y %H:%M:%S", // 8: VMS format
21];
22
23/// Timestamp record — generates formatted timestamp strings.
24///
25/// Ported from EPICS std module `timestampRecord.c`.
26pub struct TimestampRecord {
27    /// Current formatted timestamp string (VAL).
28    pub val: String,
29    /// Previous value for change detection (OVAL).
30    pub oval: String,
31    /// Seconds past EPICS epoch (RVAL).
32    pub rval: i32,
33    /// Timestamp format selector 0–10 (TST).
34    pub tst: i16,
35}
36
37impl Default for TimestampRecord {
38    fn default() -> Self {
39        Self {
40            val: String::new(),
41            oval: String::new(),
42            rval: 0,
43            tst: 0,
44        }
45    }
46}
47
48static FIELDS: &[FieldDesc] = &[
49    FieldDesc {
50        name: "VAL",
51        dbf_type: DbFieldType::String,
52        read_only: false,
53    },
54    FieldDesc {
55        name: "OVAL",
56        dbf_type: DbFieldType::String,
57        read_only: true,
58    },
59    FieldDesc {
60        name: "RVAL",
61        dbf_type: DbFieldType::Long,
62        read_only: false,
63    },
64    FieldDesc {
65        name: "TST",
66        dbf_type: DbFieldType::Short,
67        read_only: false,
68    },
69];
70
71impl TimestampRecord {
72    fn format_timestamp(&self) -> (String, i32) {
73        let now = Local::now();
74        let unix_secs = now.timestamp();
75        let sec_past_epoch = (unix_secs - EPICS_EPOCH_OFFSET) as i32;
76
77        if sec_past_epoch <= 0 {
78            return ("-NULL-".to_string(), 0);
79        }
80
81        let tst = self.tst.clamp(0, 10) as usize;
82
83        let formatted = if tst <= 8 {
84            now.format(TIMESTAMP_FORMATS[tst]).to_string()
85        } else {
86            // Formats 9 and 10 include milliseconds
87            let ms = now.timestamp_subsec_millis();
88            let base = if tst == 9 {
89                now.format("%b %d %Y %H:%M:%S").to_string()
90            } else {
91                // tst == 10
92                now.format("%m/%d/%y %H:%M:%S").to_string()
93            };
94            format!("{}.{:03}", base, ms)
95        };
96
97        (formatted, sec_past_epoch)
98    }
99}
100
101impl Record for TimestampRecord {
102    fn record_type(&self) -> &'static str {
103        "timestamp"
104    }
105
106    fn process(&mut self) -> CaResult<ProcessOutcome> {
107        let (formatted, sec_past_epoch) = self.format_timestamp();
108        self.oval = std::mem::replace(&mut self.val, formatted);
109        self.rval = sec_past_epoch;
110        Ok(ProcessOutcome::complete())
111    }
112
113    fn get_field(&self, name: &str) -> Option<EpicsValue> {
114        match name {
115            "VAL" => Some(EpicsValue::String(self.val.clone())),
116            "OVAL" => Some(EpicsValue::String(self.oval.clone())),
117            "RVAL" => Some(EpicsValue::Long(self.rval)),
118            "TST" => Some(EpicsValue::Short(self.tst)),
119            _ => None,
120        }
121    }
122
123    fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
124        match name {
125            "VAL" => match value {
126                EpicsValue::String(v) => {
127                    self.val = v;
128                    Ok(())
129                }
130                _ => Err(CaError::TypeMismatch(name.into())),
131            },
132            "RVAL" => match value {
133                EpicsValue::Long(v) => {
134                    self.rval = v;
135                    Ok(())
136                }
137                _ => Err(CaError::TypeMismatch(name.into())),
138            },
139            "TST" => match value {
140                EpicsValue::Short(v) => {
141                    self.tst = v;
142                    Ok(())
143                }
144                _ => Err(CaError::TypeMismatch(name.into())),
145            },
146            "OVAL" => Err(CaError::ReadOnlyField(name.into())),
147            _ => Err(CaError::FieldNotFound(name.into())),
148        }
149    }
150
151    fn field_list(&self) -> &'static [FieldDesc] {
152        FIELDS
153    }
154
155    fn clears_udf(&self) -> bool {
156        true
157    }
158}