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