Skip to main content

epics_base_rs/server/record/
common_fields.rs

1use std::time::SystemTime;
2
3use super::alarm::{AlarmSeverity, AnalogAlarmConfig};
4use super::scan::{ScanType, SimModeScan};
5use crate::types::PvString;
6
7/// Common fields shared by all records.
8#[derive(Clone, Debug)]
9pub struct CommonFields {
10    // Alarm state (current/result)
11    pub sevr: AlarmSeverity,
12    pub stat: u16,
13    /// Alarm message string (current). Mirrors epics-base PR #568:
14    /// records may attach a human-readable explanation alongside
15    /// `stat`/`sevr`. Empty means "no message". Transferred from
16    /// `namsg` by `rec_gbl_reset_alarms`.
17    pub amsg: String,
18    // New alarm state (pending, transferred by rec_gbl_reset_alarms)
19    pub nsev: AlarmSeverity,
20    pub nsta: u16,
21    /// Pending alarm message — set during process(), transferred to
22    /// `amsg` by `rec_gbl_reset_alarms` (epics-base PR #566).
23    pub namsg: String,
24    // Alarm acknowledgement
25    pub acks: AlarmSeverity,
26    pub ackt: bool,
27    /// `UDF` (`DBF_UCHAR` in `dbCommon.dbd`) — "value undefined". C models it
28    /// as one `epicsUInt8` that is BOTH the predicate (`if (prec->udf)`) and
29    /// the served byte: `caput UDF 255` stores 255 and `caget` renders it
30    /// signed `DBR_CHAR` (-1). Modeled as the raw `u8` so the byte round-trips
31    /// on records that do not re-derive it; every predicate reader tests
32    /// `!= 0`, and the record-facing [`crate::server::record::ProcessContext::udf`] boundary keeps the
33    /// `bool` view.
34    pub udf: u8,
35    /// `UDFS` (`DBF_MENU menu(menuAlarmSevr)`) — the severity raised for a UDF
36    /// record. Holds the **raw stored ordinal** (see [`AnalogAlarmConfig`]): C
37    /// stores `(epicsEnum16)` verbatim on a numeric put, so an out-of-range
38    /// `UDFS` round-trips (`caput REC.UDFS 4` → `4`); the alarm meaning is
39    /// `AlarmSeverity::from_u16(udfs as u16)`.
40    pub udfs: i16,
41    // Scan
42    pub scan: ScanType,
43    // SSCN's dbd default is the out-of-range sentinel 65535 ("use SCAN"),
44    // unrepresentable in `ScanType`; see [`SimModeScan`].
45    pub sscn: SimModeScan,
46    /// `OLDSIMM` (`DBF_MENU menu(menuSimm)`, `special(SPC_NOMOD)`) — the
47    /// PREVIOUS simulation mode, the latch C's `recGblSaveSimm`
48    /// (`recGbl.c:421-425`) writes and `recGblCheckSimm` (`recGbl.c:427-437`)
49    /// compares against to detect a SIMM transition and swap SCAN with SSCN.
50    ///
51    /// C declares it per record (in each of the 21 dbd files that carry SSCN);
52    /// this port keeps it next to `sscn` in the common fields, for the same
53    /// reason `sscn` lives here — it is framework state, written by exactly one
54    /// owner (`PvDatabase::rec_gbl_save_simm`) and read by exactly one
55    /// (`rec_gbl_check_simm`). Read-only to clients (SPC_NOMOD).
56    pub oldsimm: i16,
57    /// `PINI` is `DBF_MENU`/`menu(menuPini)` — a six-choice lifecycle
58    /// selector, not a flag (see [`crate::server::record::PiniMode`] for the choice semantics). Holds
59    /// the **raw stored ordinal** as `i16`: C stores `(epicsEnum16)` verbatim on
60    /// a numeric put, so `caput REC.PINI 6` keeps `6` and `caput REC.PINI -1`
61    /// keeps `65535`. `doRecordPini` compares against the exact valid indices,
62    /// so any out-of-range ordinal simply matches no lifecycle pass — read a
63    /// choice with `PiniMode::from_u16(pini as u16)`.
64    pub pini: i16,
65    /// `TPRO` (`DBF_UCHAR` in `dbCommon.dbd`) — trace-processing flag. C
66    /// stores the raw put byte and serves it as SIGNED `DBR_CHAR`
67    /// (`caput TPRO 255` → `caget` = -1). Modeled as the raw `u8`, like
68    /// [`Self::bkpt`], so the byte round-trips; consumers test `!= 0`.
69    pub tpro: u8,
70    pub bkpt: u8,
71    // Links (raw strings)
72    pub flnk: String,
73    pub inp: String,
74    pub out: String,
75    // Device
76    pub dtyp: String,
77    // Timestamp
78    pub time: SystemTime,
79    pub tse: i16,
80    pub tsel: String,
81    /// Time-tag — C `dbCommon.dbd.pod` `field(UTAG,DBF_UINT64)`. A
82    /// 64-bit user/hardware tag set alongside `time` by
83    /// `recGblGetTimeStampSimm` via `dbGetTimeStampTag`. Zero when no
84    /// time-tag source is configured.
85    pub utag: u64,
86    // Analog alarm config (Some for analog record types)
87    pub analog_alarm: Option<AnalogAlarmConfig>,
88    /// Access security group — C `dbCommon.ASG`, a `DBF_STRING` with NO
89    /// `initial()` in `dbCommon.dbd`, so a record that does not name a group
90    /// holds the EMPTY string and `caget -t REC.ASG` on any C IOC prints an
91    /// empty line. It is `asAddMember` (asLibRoutines.c:893-928) that resolves
92    /// an empty or unknown name to the DEFAULT group — the FIELD never says
93    /// "DEFAULT" unless the `.db` put it there. Ask [`Self::access_group`] for
94    /// the group to evaluate against; read this only to serve the field.
95    pub asg: String,
96    /// Access security level. C `dbCommon.ASL`
97    /// (0 or 1, default 0). Compared against `RULE(N, …)` levels
98    /// in [`crate::server::access_security::AccessSecurityConfig::check_access_method`] —
99    /// a rule with `RULE(M, …)` only applies when `ASL ≤ M`. The
100    /// earlier code hard-coded ASL=0 at every ACF call site
101    /// (CA tcp.rs, PVA native_source GET/PUT/MONITOR), so every
102    /// `RULE(N>0, WRITE)` was always considered "applicable" and
103    /// the per-record ASL gate was silently inert.
104    pub asl: u8,
105    /// Description — C `dbCommon` `field(DESC,DBF_STRING) size(41)`. A
106    /// genuine DBF_STRING data field served verbatim to clients, so it is
107    /// a byte-preserving [`PvString`] (a non-UTF-8 DESC put must round-trip
108    /// unchanged, matching EPICS fixed-size char-array string semantics).
109    pub desc: PvString,
110    // Phase/priority/event
111    pub phas: i16,
112    /// Event name for `SCAN="Event"` records. C `dbCommon.dbd.pod`:
113    /// `field(EVNT,DBF_STRING) { size(40) }` — since EPICS 7 this is
114    /// an event *name* (resolved by `eventNameToHandle`), not a
115    /// numeric subscript. A numeric string ("5") still works for
116    /// backward compatibility. Empty means "no event".
117    pub evnt: String,
118    pub prio: i16,
119    // Disable support
120    pub disv: i16,
121    pub disa: i16,
122    pub sdis: String,
123    /// `DISS` (`DBF_MENU menu(menuAlarmSevr)`) — the severity a disabled record
124    /// takes. Holds the **raw stored ordinal** (see [`AnalogAlarmConfig`]).
125    pub diss: i16,
126    // Alarm hysteresis (analog records)
127    pub hyst: f64,
128    // Lock count (re-entrance counter)
129    pub lcnt: i16,
130    // DISP — disable putfield from CA. `DBF_UCHAR` in `dbCommon.dbd`: C
131    // stores the raw put byte and serves it SIGNED as `DBR_CHAR`
132    // (`caput DISP 255` → `caget` = -1). Raw `u8` like [`Self::bkpt`];
133    // consumers test `!= 0`.
134    pub disp: u8,
135    // Process control
136    pub putf: bool,
137    // RPRO — reprocess flag. `DBF_UCHAR`, raw-byte readback like DISP/TPRO.
138    pub rpro: u8,
139    // PROC — force-processing field. `DBF_UCHAR` in `dbCommon.dbd`
140    // (`field(PROC,DBF_UCHAR){ pp(TRUE) }`): C's `dbPut` stores the raw put
141    // byte in `prec->proc` and never resets it (retained across processing),
142    // serving it back SIGNED as `DBR_CHAR` (`caput PROC 255` → `caget` = -1).
143    // Raw `u8` like [`Self::disp`]/[`Self::rpro`]; the `pp(TRUE)` reprocess is
144    // orthogonal and driven by the put-path force-process intercept.
145    pub proc_field: u8,
146    // Fallback monitor/archive last-sent values for records without MLST/ALST fields
147    pub mlst: Option<f64>,
148    pub alst: Option<f64>,
149}
150
151impl CommonFields {
152    /// **The single owner of "which access group does this record belong to"** —
153    /// C `asAddMember(&prec->asp, prec->asg)`, whose `asAddMemberPvt`
154    /// (asLibRoutines.c:893-928) resolves an empty or unknown group name to the
155    /// always-present `DEFAULT` group.
156    ///
157    /// Every access-security call site asks this, never [`Self::asg`] directly:
158    /// the FIELD is what the `.db` wrote (empty by default, and that is what the
159    /// wire must show), the GROUP is what the ACF is evaluated against. Three
160    /// call sites used to spell the empty→DEFAULT rule out for themselves and a
161    /// fourth (the CA server) relied on the config lookup missing — which is a
162    /// different rule (C's unknown-NAME reassignment) that happened to have the
163    /// same effect.
164    pub fn access_group(&self) -> &str {
165        if self.asg.is_empty() {
166            "DEFAULT"
167        } else {
168            &self.asg
169        }
170    }
171
172    /// Build a [`ProcessContext`](super::record_trait::ProcessContext)
173    /// snapshot of the framework-owned state a record's `process()` or
174    /// device support's `read()` needs to observe during the cycle.
175    pub fn process_context(&self) -> super::record_trait::ProcessContext {
176        super::record_trait::ProcessContext {
177            udf: self.udf != 0,
178            udfs: AlarmSeverity::from_u16(self.udfs as u16),
179            nsev: self.nsev,
180            phas: self.phas,
181            tse: self.tse,
182            time: self.time,
183            tsel: self.tsel.clone(),
184            dtyp: self.dtyp.clone(),
185            callback_priority: self.callback_priority(),
186        }
187    }
188
189    /// The callback band this record's `PRIO` selects — C
190    /// `callbackSetPriority(prec->prio, ...)` (`seqRecord.c:146`).
191    pub fn callback_priority(&self) -> crate::runtime::task::CallbackPriority {
192        crate::runtime::task::CallbackPriority::from_record_prio(self.prio)
193    }
194}
195
196impl Default for CommonFields {
197    fn default() -> Self {
198        Self {
199            sevr: AlarmSeverity::NoAlarm,
200            // C dbd `field(STAT,DBF_MENU){ menu(menuAlarmStat) initial("UDF") }`
201            // (`dbCommon.dbd.pod:296-301`): a record is born UNDEFINED, not
202            // NO_ALARM. SEVR has no `initial()` — it starts NO_ALARM and is
203            // raised to UDFS by the init prologue
204            // (`RecordInstance::run_init_passes`, C `iocInit.c:521-523`), which
205            // keys off exactly this STAT value.
206            stat: crate::server::recgbl::alarm_status::UDF_ALARM,
207            amsg: String::new(),
208            nsev: AlarmSeverity::NoAlarm,
209            nsta: 0,
210            namsg: String::new(),
211            acks: AlarmSeverity::NoAlarm,
212            ackt: true,
213            udf: 1,
214            udfs: AlarmSeverity::Invalid as i16,
215            scan: ScanType::Passive,
216            // C dbd `field(SSCN,DBF_MENU){ menu(menuScan) initial("65535") }`:
217            // the default is the out-of-range "use SCAN" sentinel, not Passive.
218            sscn: SimModeScan::default(),
219            // C dbd `field(OLDSIMM,DBF_MENU){ menu(menuSimm) }` — no
220            // `initial()`, so it starts at index 0 (`menuSimmNO`).
221            oldsimm: 0,
222            pini: 0,
223            tpro: 0,
224            bkpt: 0,
225            flnk: String::new(),
226            inp: String::new(),
227            out: String::new(),
228            dtyp: String::new(),
229            // An `epicsTimeStamp {0,0}` — C's never-processed `dbCommon.time`
230            // — is the EPICS epoch, not the Unix epoch. Seeding this with
231            // `UNIX_EPOCH` made a never-processed record publish
232            // `timeStamp.secondsPastEpoch = 0` on PVA where pvxs publishes
233            // 631152000 (`iocsource.cpp:240` adds POSIX_TIME_AT_EPICS_EPOCH
234            // to the record's raw EPICS seconds). See
235            // [`crate::runtime::general_time::epics_epoch`].
236            time: crate::runtime::general_time::epics_epoch(),
237            tse: 0,
238            tsel: String::new(),
239            utag: 0,
240            analog_alarm: None,
241            asg: String::new(),
242            asl: 0,
243            desc: PvString::new(),
244            phas: 0,
245            evnt: String::new(),
246            prio: 0,
247            disv: 1,
248            disa: 0,
249            sdis: String::new(),
250            diss: 0,
251            hyst: 0.0,
252            lcnt: 0,
253            disp: 0,
254            putf: false,
255            rpro: 0,
256            proc_field: 0,
257            mlst: None,
258            alst: None,
259        }
260    }
261}