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}