epics_base_rs/server/record/record_trait.rs
1use crate::error::CaResult;
2use crate::types::c_parse::Converted;
3use crate::types::{DbFieldType, DbfCode, EpicsValue, PvString, c_parse};
4
5use super::scan::ScanType;
6
7/// Which of a `devXxxSoftRaw` dset's two entry points is delivering a value to
8/// [`Record::raw_soft_input`]. They are not the same function in C, and they do
9/// not agree about `MASK`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum RawSoftEntry {
12 /// `devXxxSoftRaw::init_record` — `recGblInitConstantLink(&prec->inp,
13 /// DBF_x, &prec->rval)` (`devAiSoftRaw.c:41`, `devBiSoftRaw.c:42`,
14 /// `devMbbiSoftRaw.c:42`, `devMbbiDirectSoftRaw.c:42`). A CONSTANT `INP`
15 /// (`field(INP,"12")`) is loaded ONCE, at iocInit, straight into `RVAL`.
16 ///
17 /// `recGblInitConstantLink` is a plain typed store — **no MASK**. The mask
18 /// lives in `read_xxx`, which a constant INP never reaches (a constant link
19 /// delivers nothing at process).
20 InitConstant,
21 /// `devXxxSoftRaw::read_xxx` — the per-cycle `dbGetLink(&prec->inp, ...)`
22 /// followed by the dset's own masking (`devBiSoftRaw.c:56-57` `if
23 /// (prec->mask) prec->rval &= prec->mask;`, `devMbbiSoftRaw.c:78-79`
24 /// unconditionally).
25 Read,
26}
27
28/// The `special(SPC_*)` dispatch code a field declares — C `special.h`.
29///
30/// C hands this to the record's `special(DBADDR *, int after)` on every put, and
31/// `dbAccess.c` acts on three of them itself before the record ever sees the
32/// write: `NoMod` refuses it (`S_db_noMod`), `DbAddr` means the field's type and
33/// element count come from the record's `cvt_dbaddr` rather than the `.dbd`, and
34/// `As` re-evaluates access security.
35///
36/// The discriminants ARE C's `SPC_*` numbers (`special.h:26-39` @R7.0.10),
37/// so `special as i16` is the integer C prints and stores. They are written
38/// out rather than left implicit because the table is not contiguous — C has
39/// no code 4, and the record-specific half starts at 100 — so the obvious
40/// cast on an implicitly-numbered enum would have answered 4 for
41/// [`Self::AlarmAck`] and 7 for [`Self::Mod`]. Carrying the number on the
42/// type is what makes every future cast right by construction.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[repr(i16)]
45pub enum Special {
46 /// No `special()` declared — C leaves `pdbFldDes->special` 0.
47 None = 0,
48 /// `SPC_NOMOD` — the field must not be modified. Mirrored into
49 /// [`FieldDesc::read_only`], which is the bit the put gate reads.
50 NoMod = 1,
51 /// `SPC_DBADDR` — the record's `cvt_dbaddr` supplies the field's type
52 /// and element count. [`FieldDesc::dbf_type`] carries the type C serves at
53 /// the selector field's default; a record whose type is state-dependent
54 /// (`waveform.VAL` on `FTVL`, `mbbo.VAL` on `SDEF`) overrides it at runtime.
55 DbAddr = 2,
56 /// `SPC_SCAN` — a scan-related field; C re-registers the scan.
57 Scan = 3,
58 /// `SPC_ALARMACK` — an alarm acknowledgement. C skips 4.
59 AlarmAck = 5,
60 /// `SPC_AS` — access security; C re-computes the record's ASG.
61 As = 6,
62 /// `SPC_ATTRIBUTE` — a pseudo (attribute) field. Set internally; it
63 /// is the one code `pamapspcType` omits, so no `.dbd` declares it.
64 Attribute = 7,
65 /// `SPC_MOD` — the record's own `special()` runs on the put. C's
66 /// record-specific range starts here.
67 Mod = 100,
68 /// `SPC_RESET` — the `RES` field is being modified.
69 Reset = 101,
70 /// `SPC_LINCONV` — a linear-conversion field changed; C calls the
71 /// device support's `special_linconv`.
72 LinConv = 102,
73 /// `SPC_CALC` — the `CALC` expression changed; C recompiles it.
74 Calc = 103,
75}
76
77/// A field's access-security level — C `.dbd` `asl(ASL0|ASL1)`.
78///
79/// `ASL1` is the `.dbd` default (`dbLexRoutines.c:570`); `asl(ASL0)` lowers a
80/// field to the level an operator may write.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum Asl {
83 Asl0,
84 Asl1,
85}
86
87/// The numeric base `dbGetString` renders an integer field in — C's
88/// `ctType` (`dbBase.h:63`), set by the `.dbd` `base(DECIMAL|HEX)` item
89/// (`dbLexRoutines.c:652-661`).
90///
91/// It selects the renderer inside `dbGetStringNum` (`dbStaticLib.c:2074-2124`),
92/// so it changes what `dbpr`, `dbDumpRecord` and the `.db` writer print and
93/// nothing else: `dbgf` reads through `dbConvert`, which has no base. In
94/// EPICS base the whole `HEX` population is `mbbi`/`mbbo`'s sixteen `*VL`
95/// fields, which C prints as `ZRVL: 0xa` where the port printed `ZRVL: 10`.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Base {
98 /// `CT_DECIMAL` — the `.dbd` default.
99 Decimal,
100 /// `CT_HEX`.
101 Hex,
102}
103
104/// The `.dbd` declaration of a single record field.
105///
106/// Every one of these is **generated** from the vendored EPICS `.dbd` by
107/// `tools/dbd-codegen` — see [`dbd_generated`](super::dbd_generated). They used
108/// to be hand-copied, which is what made a wrong `dbf_type` or a missed
109/// `special(SPC_NOMOD)` a recurring finding rather than an impossible state.
110///
111/// The struct carries the *whole* declaration, not just the three attributes the
112/// runtime consumes today: dropping the rest at the parser is how the port ended
113/// up unable to answer questions like "is this field `pp(TRUE)`?" without
114/// re-reading the `.dbd`.
115#[derive(Debug, Clone)]
116pub struct FieldDesc {
117 /// The field name, upper-case as declared.
118 pub name: &'static str,
119 /// The type the field is **SERVED** as, on every delivery path — see
120 /// [`RecordInstance::project_to_declared_type`](super::RecordInstance::project_to_declared_type),
121 /// which projects the stored value onto it. The one exception is a
122 /// [`Self::runtime_typed`] field, where C's `cvt_dbaddr` overrides.
123 ///
124 /// It is NOT the `DBF_*` token the `.dbd` declared — that is
125 /// [`Self::declared_dbf`], and the two differ for every link and menu
126 /// field. The CA wire type is derived from this one by
127 /// [`DbFieldType::ca_wire_type`], which owns the promotions CA has no type
128 /// for (`ULong`/`Int64`/`UInt64` -> `DBR_DOUBLE`, `UShort` -> `DBR_LONG`,
129 /// `UChar` -> `DBR_CHAR`). Do not pre-promote here: PVA serves the native
130 /// width.
131 pub dbf_type: DbFieldType,
132 /// The `DBF_*` token the `.dbd` declared, carried verbatim from the
133 /// generator's input — C's `pdbFldDes->field_type`, which is what
134 /// `dbDumpField` prints and `dbGetFieldType`/`dbGetFieldTypeString`
135 /// (`dbStaticLib.c:964-977`) answer for a `DBENTRY`.
136 ///
137 /// C's `dba` prints `paddr->field_type` instead, which is this token for
138 /// every field EXCEPT the 83 `special(SPC_DBADDR)` rows, where
139 /// `dbNameToAddr` lets the record's `cvt_dbaddr` overwrite it; on those
140 /// rows [`Self::dbf_type`] carries the overwritten type. 80 of the 83
141 /// declare `DBF_NOACCESS` here, which is precisely why [`Self::no_access`]
142 /// reads this field and not `dbf_type`.
143 ///
144 /// A SECOND fact, not a spelling of [`Self::dbf_type`]. The generator maps
145 /// six C tokens onto two served types (`DBF_ENUM|DBF_MENU|DBF_DEVICE` all
146 /// serve as [`DbFieldType::Enum`], `DBF_INLINK|DBF_OUTLINK|DBF_FWDLINK` all
147 /// as [`DbFieldType::String`]), and that collapse is one-way: no mapping
148 /// out of `dbf_type` can recover the token, because `ai.INP` and
149 /// `dbCommon.FLNK` are the same served variant. Nor can the field NAME —
150 /// `LNK1` is `DBF_OUTLINK` on `sseq` and `DBF_FWDLINK` on `fanout`. So the
151 /// declaration is carried, not derived.
152 pub declared_dbf: DbfCode,
153 /// C's `cvt_dbaddr` re-types this field at name-resolution time from the
154 /// record's own state — `waveform.VAL` from `FTVL`, `aSub.A` from `FTA`,
155 /// `mbbo.VAL` from `SDEF` — so the `.dbd` declaration is a placeholder
156 /// carrying only the selector's default. [`Self::dbf_type`] is therefore
157 /// NOT what such a field is served as; the record's stored variant is this
158 /// port's `cvt_dbaddr` answer, and it wins.
159 ///
160 /// A `special(SPC_DBADDR)` field whose type is nevertheless FIXED
161 /// (`compress.VAL` is always a double array, `histogram.VAL` always
162 /// `epicsUInt32`) is *not* runtime-typed: its row in `cvt_dbaddr.types`
163 /// carries no selector, so the declared type is the true one and it is
164 /// projected like any other field.
165 pub runtime_typed: bool,
166 /// `special(SPC_NOMOD)` — the field is immutable for this record type. The
167 /// static half of the no-modify declaration; see [`Record::field_no_mod`]
168 /// for the half a record decides at runtime.
169 pub read_only: bool,
170 /// The full `special()` code AS RESOLVED, of which [`Self::read_only`] is
171 /// one case: the `.dbd` declaration for most fields, but for a
172 /// `special(SPC_DBADDR)` field the code its `cvt_dbaddr` raises instead —
173 /// `SPC_MOD` on `lsi.VAL`, `SPC_NOMOD` on `lsi.OVAL`
174 /// (`lsiRecord.c:127-134`). That is the code the put gate and the CA
175 /// write-access answer must read, because it is what C's `DBADDR` carries
176 /// after name resolution.
177 ///
178 /// It is NOT what the `.dbd` said — see [`Self::declared_special`], which
179 /// is. The pair splits the same way [`Self::dbf_type`] and
180 /// [`Self::declared_dbf`] do, and for the same reason: one cell holding
181 /// both answers is a cell that is wrong for one of its two readers.
182 pub special: Special,
183 /// The `special()` code the `.dbd` DECLARED — C's `pdbFldDes->special`,
184 /// which `cvt_dbaddr` never touches because it writes the per-resolution
185 /// `DBADDR` and not the shared field descriptor.
186 ///
187 /// Every reader that asks about the DECLARATION rather than about a
188 /// resolved address wants this one. The db loader's field-name suggestion
189 /// is the case in hand: it skips `SPC_NOMOD` and `SPC_DBADDR` candidates
190 /// (`dbLexRoutines.c:1283-1285`), and reading [`Self::special`] there would
191 /// let `lsi.VAL` and `lso.VAL` — declared `SPC_DBADDR`, resolved `SPC_MOD`
192 /// — be proposed where C does not propose them.
193 pub declared_special: Special,
194 /// `pp(TRUE)` — a put to this field processes the record.
195 pub pp: bool,
196 /// `asl(...)` — the access-security level.
197 pub asl: Asl,
198 /// The byte width C's `pdbFldDes->size` carries.
199 ///
200 /// Two sources, one meaning. A `DBF_STRING` field takes it from `size(N)`,
201 /// which C's loader REQUIRES on every such row
202 /// (`dbLexRoutines.c:755-758`). A `DBF_NOACCESS` internal takes it from
203 /// the width of the C struct member [`Self::extra`] declares, which is
204 /// what a C IOC's generated `<rec>RecordSizeOffset` writes into the same
205 /// cell — the `.dbd` never spells it.
206 ///
207 /// Not string-only, and not decoration on the second kind: `dbpr`'s
208 /// `DBF_NOACCESS` arm dispatches on it (`dbTest.c:1235`, `:1241`) and then
209 /// prints exactly this many bytes as hex (`:1249-1262`), so a width of 0
210 /// prints an empty row where C prints the field.
211 ///
212 /// 0 for every other declaration, where C carries `sizeof` the member and
213 /// no reader in this port asks.
214 pub size: u16,
215 /// `extra("...")` — the C struct member a `DBF_NOACCESS` field stands for,
216 /// verbatim, and `None` for a field declaring none.
217 ///
218 /// C's loader requires it on exactly the rows [`Self::size`]'s second
219 /// source covers (`dbLexRoutines.c:759-762`), and it is not documentation:
220 /// `dbpr` reads the declaration TEXT to choose a renderer — a `*` in it
221 /// means print a pointer, a leading `ELLLIST` means print a list header
222 /// (`dbTest.c:1235-1247`). The type name is the only thing that says how
223 /// wide the member is, since no `.dbd` states it.
224 pub extra: Option<&'static str>,
225 /// `menu(...)` choice strings, in index order, for a `DBF_MENU` field. The
226 /// index is the stored value and the strings are what `get_enum_strs` serves,
227 /// so a client sees `"NO CONVERSION"` rather than `0`.
228 pub menu: Option<&'static [&'static str]>,
229 /// `initial("...")` — the value C's dbd loader seeds the field with.
230 pub initial: Option<&'static str>,
231 /// `interest(N)` — the `dbpr` verbosity level at which C prints the field.
232 pub interest: u8,
233 /// `prop(YES)` — the field is a property: a change to it posts a
234 /// `DBE_PROPERTY` event.
235 pub prop: bool,
236 /// `prompt("...")` — the operator-facing label. C prints it in the
237 /// parenthesised half of the db loader's field suggestion
238 /// (`dbLexRoutines.c:1380-1384`), which is the only reason it is carried;
239 /// a field that declares none makes C print the bare `Did you mean` line.
240 pub prompt: Option<&'static str>,
241 /// `promptgroup("...")` — the DCT group the field belongs to, or `None`
242 /// when the `.dbd` declares none. C stores it as a 1-based key into
243 /// `guiGroupList` (`findOrAddGuiGroup`, `dbLexRoutines.c:1176-1189`), so
244 /// zero means absent and this is `Option`, not the group's index. The
245 /// suggestion halves a groupless field's score (`:1288-1289`): a field no
246 /// DCT screen offers is an unlikely thing to have misspelled.
247 pub promptgroup: Option<&'static str>,
248 /// `base(DECIMAL|HEX)` — the base `db_get_string` renders this field in.
249 pub base: Base,
250}
251
252impl FieldDesc {
253 /// A hand-written descriptor carrying only the three attributes the port
254 /// used to model.
255 ///
256 /// **Transitional.** Every record type is migrating to the generated table
257 /// in [`dbd_generated`](super::dbd_generated), which carries the whole `.dbd`
258 /// declaration; this constructor exists only so the not-yet-migrated records
259 /// keep compiling, and it goes away with the last of them. It does NOT know
260 /// the field's `pp`/`asl`/`size`/`menu`/`initial`, so it reports the neutral
261 /// value for each — a record still on this constructor answers "no menu"
262 /// here and resolves its choices through the
263 /// [`Record::menu_field_choices`] fallback instead.
264 pub const fn new(name: &'static str, dbf_type: DbFieldType, read_only: bool) -> Self {
265 Self {
266 name,
267 dbf_type,
268 // A hand-written table names one type and means both facts by it:
269 // it has no `.dbd` behind it to disagree with, so the declaration
270 // is the served type's own code. That also makes `no_access()`
271 // false for every such row, which is right — a hand-written table
272 // never declares a C internal.
273 declared_dbf: dbf_type.dbf_code(),
274 // A plain field: the type it names is the type it is served as.
275 // The `cvt_dbaddr` records are all on the generated table, which
276 // sets this from `cvt_dbaddr.types`.
277 runtime_typed: false,
278 read_only,
279 special: if read_only {
280 Special::NoMod
281 } else {
282 Special::None
283 },
284 // A hand-written row has no `.dbd` behind it to disagree with, so
285 // the declaration is the resolved code.
286 declared_special: if read_only {
287 Special::NoMod
288 } else {
289 Special::None
290 },
291 pp: false,
292 asl: Asl::Asl1,
293 size: 0,
294 extra: None,
295 menu: None,
296 initial: None,
297 interest: 0,
298 prop: false,
299 prompt: None,
300 promptgroup: None,
301 base: Base::Decimal,
302 }
303 }
304
305 /// The field is declared `DBF_NOACCESS` — a C internal with no dbStatic
306 /// representation. C's `dbPutString` switches on exactly this and answers
307 /// `S_dbLib_badField` with "Can't set array field before iocInit()"
308 /// (`dbStaticLib.c:2646-2650`), so a `.db` file cannot assign one.
309 ///
310 /// Derived rather than stored, so the two cannot disagree. It is NOT
311 /// implied by [`Self::special`] — `mbbo.VAL` is `DBF_ENUM` + `SPC_DBADDR`
312 /// and is perfectly settable, while `waveform.VAL` is `DBF_NOACCESS` +
313 /// `SPC_DBADDR` and is not — nor by [`Self::dbf_type`], which for these
314 /// rows carries the type C SERVES rather than the one declared.
315 pub const fn no_access(&self) -> bool {
316 matches!(self.declared_dbf, DbfCode::NoAccess)
317 }
318
319 /// The field resolves as a NAME but has no readable value — C `dbGet`'s
320 /// validity gate, `field_type > DBF_DEVICE` (`dbAccess.c:667-675`
321 /// @R7.0.10).
322 ///
323 /// Not the same question as [`Self::no_access`], and the difference is
324 /// the whole point. `dbEntryToAddr` seeds `paddr->field_type` from the
325 /// DECLARATION and then overwrites it from the record's `cvt_dbaddr` —
326 /// but only for a field whose `special` is `SPC_DBADDR`
327 /// (`dbAccess.c:640-647`). So a `DBF_NOACCESS` row carrying
328 /// `SPC_DBADDR` — `waveform.VAL` — arrives at the gate re-typed to what
329 /// it serves and PASSES, while one that does not — `dbCommon`'s `BKPT`
330 /// and `TIME` — arrives still holding 17 and FAILS. Both facts are
331 /// needed; neither alone answers it.
332 ///
333 /// Derived rather than stored, for the same reason `no_access` is: a
334 /// stored copy is a second answer that can drift from the declaration.
335 pub const fn unreadable(&self) -> bool {
336 self.no_access() && !matches!(self.declared_special, Special::DbAddr)
337 }
338}
339
340/// One `recGblInitConstantLink(&prec->LINK, DBF_x, &prec->TARGET)` call from a
341/// record's C `init_record` — the seed of a CONSTANT input link.
342///
343/// Declared by [`Record::constant_init_links`] and applied by the single owner
344/// `crate::server::database::PvDatabase::rec_gbl_init_constant_links`.
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub struct ConstantInitLink {
347 /// The link field holding the constant (`INPA`, `NVL`, `SELL`, `DOL1`,
348 /// `SUBL`, `DOL`, ...).
349 pub link_field: &'static str,
350 /// The value field the constant is loaded into (`A`, `SELN`, `DO1`,
351 /// `SNAM`, `VAL`, ...).
352 pub target_field: &'static str,
353 /// Whether a successful seed clears UDF — C's
354 /// `if (recGblInitConstantLink(&prec->dol, ...)) prec->udf = FALSE;`
355 /// (`aoRecord.c:112-113`, `longoutRecord.c:113`, `mbboRecord.c:133`,
356 /// `int64outRecord.c:110`, `dfanoutRecord.c:105`). The multi-input seeders
357 /// (calc/sub/sel/aSub/seq/fanout) do NOT clear UDF: they seed A..L, not
358 /// VAL.
359 pub clears_udf: bool,
360 /// Whether the loaded value is stored as its BOOLEAN — C `boRecord.c:146-148`
361 /// loads the constant into a temporary and stores `prec->val = !!ival`, so
362 /// `field(DOL,"5")` leaves a bo at VAL=1, not 5. The only seed whose stored
363 /// value differs from the loaded one.
364 pub normalize_bool: bool,
365}
366
367impl ConstantInitLink {
368 /// A seed that does not touch UDF — the INPA..L / SELL / NVL / DOLn form.
369 pub const fn new(link_field: &'static str, target_field: &'static str) -> Self {
370 Self {
371 link_field,
372 target_field,
373 clears_udf: false,
374 normalize_bool: false,
375 }
376 }
377
378 /// A DOL→VAL seed, which C follows with `prec->udf = FALSE`.
379 pub const fn dol_to_val(link_field: &'static str, target_field: &'static str) -> Self {
380 Self {
381 link_field,
382 target_field,
383 clears_udf: true,
384 normalize_bool: false,
385 }
386 }
387
388 /// bo's DOL→VAL seed: `prec->val = !!ival; prec->udf = FALSE;`
389 /// (`boRecord.c:146-149`).
390 pub const fn dol_to_bool_val(link_field: &'static str, target_field: &'static str) -> Self {
391 Self {
392 link_field,
393 target_field,
394 clears_udf: true,
395 normalize_bool: true,
396 }
397 }
398}
399
400/// The seed table for a record whose C seeds exactly the input links it
401/// fetches — the `for (i = 0; i < N; i++) recGblInitConstantLink(plink++,
402/// DBF_DOUBLE, pvalue++)` loop of calc / calcout / sub / sel / aSub /
403/// scalcout / acalcout / transform, expressed over the record's own
404/// [`Record::multi_input_links`] table.
405pub fn seed_input_links(pairs: &[(&'static str, &'static str)]) -> Vec<ConstantInitLink> {
406 pairs
407 .iter()
408 .map(|(link, value)| ConstantInitLink::new(link, value))
409 .collect()
410}
411
412/// Resolved metadata of an OUT-link TARGET, as C's soft device support
413/// obtains it before choosing its write buffer.
414///
415/// C's two sources, both mirrored by
416/// [`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase):
417/// - `DB_LINK` — `dbNameToAddr` gives `field_type` and `no_elements`
418/// (`devsCalcoutSoft.c:127-131`, `devaCalcoutSoft.c:78-79`); an
419/// unresolvable name leaves the caller's initializers untouched.
420/// - `CA_LINK` — `dbCaGetLinkDBFtype` / `dbCaGetNelements`
421/// (`dbCa.c:662-704`), which both return `-1` on a disconnected link and
422/// likewise leave the initializers untouched.
423///
424/// A record reproduces its C device support's buffer switch on this in
425/// [`Record::multi_output_buffer`].
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub struct OutTarget {
428 /// Target field's DBF type. `None` = unresolved (disconnected CA link,
429 /// or a name this IOC cannot resolve) — C's `field_type` initializer.
430 pub field_type: Option<DbFieldType>,
431 /// Target field's element capacity — C `no_elements` / `dbCaGetNelements`.
432 /// `1` when unresolved, matching C's `n_elements = 1` initializer.
433 pub element_count: i64,
434 /// True when C would classify this link as a `CA_LINK`: an explicit
435 /// `ca://`/`pva://` link, or a DB-style name that is not a record of
436 /// this IOC (`dbInitLink` locality). Device support that splits its
437 /// buffer choice on sync-vs-async (`devsCalcoutSoft.c:76`, gated on
438 /// `plink->type == CA_LINK && pscalcout->wait`) reads this.
439 pub is_ca_link: bool,
440 /// True when the target field is one of the seven DBF classes C's soft
441 /// device support puts as `DBR_STRING` — `DBF_STRING`, `DBF_ENUM`,
442 /// `DBF_MENU`, `DBF_DEVICE`, `DBF_INLINK`, `DBF_OUTLINK`, `DBF_FWDLINK`
443 /// (`devsCalcoutSoft.c:83-85`, `:128-130`).
444 ///
445 /// Carried here rather than re-derived from [`Self::field_type`] because
446 /// [`DbFieldType`] is the DBR *wire* type: it has no `Menu` or `Device`
447 /// variant, so a menu target (`PRIO`, `STAT`, `SEVR`, `DISS`, `ACKT`, …)
448 /// or `DTYP` is indistinguishable from a plain numeric/string field by
449 /// type alone. The classification is made once, at resolution, by the
450 /// side that holds the target's field metadata
451 /// (`RecordInstance::field_puts_as_string`); a record's
452 /// [`Record::multi_output_buffer`] just reads the answer.
453 pub puts_as_string: bool,
454}
455
456impl OutTarget {
457 /// C's initializer state: `field_type = 0` is never *used* as a type by
458 /// the port (a `None` type routes to the device support's `default:`
459 /// arm), and `n_elements = 1`. An unresolved target is not in the string
460 /// class — C's `field_type = 0` matches no `case` and falls to `default:`.
461 pub const UNRESOLVED: Self = Self {
462 field_type: None,
463 element_count: 1,
464 is_ca_link: false,
465 puts_as_string: false,
466 };
467}
468
469/// The `dbrType` a record asks an INPUT link for — the second argument of C
470/// `dbGetLink(plink, dbrType, pbuffer, options, pnRequest)` (`dbLink.c:305`).
471///
472/// The READ twin of [`Record::typed_output_buffer`]'s destination switch. C's
473/// input-side switch is on the SOURCE's DBF class (`dbGetLinkDBFtype(&dol)`,
474/// `sseqRecord.c:640-705`) and each arm asks `dbGetLink` for a DIFFERENT
475/// `dbrType`, so the value a record receives is not the source's native one:
476/// a `DBF_ENUM`/`DBF_MENU` source read with `DBR_STRING` delivers its state
477/// LABEL, and a `DBF_CHAR` array read with `DBF_CHAR` delivers bytes, not a
478/// number. The record declares the request
479/// ([`Record::input_link_read_as`]); the framework, which is the side that
480/// can address the source, performs the conversion.
481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub enum LinkReadAs {
483 /// The source's NATIVE value, coerced (or preserved) by the target field's
484 /// own `put_field_internal`. The framework default: every record whose C
485 /// `dbGetLink` request does not switch on the source class (compress `INP`,
486 /// waveform `INP`, sseq `SELL`, epid, motor, table) reads this way.
487 Native,
488 /// C `dbGetLink(..., DBR_STRING, ...)`. An `ENUM`/`MENU` source delivers its
489 /// state LABEL (`dbConvert.c` `getEnumString` → the record's
490 /// `get_enum_str`), never the index; a link/`DTYP` field delivers its text.
491 String,
492 /// C `dbGetLink(..., DBR_DOUBLE, ...)`.
493 Double,
494 /// C `dbGetLink(..., DBF_CHAR|DBF_UCHAR, buf, 0, &n)` — up to `max_elements`
495 /// bytes of the source's char array, taken as the string they spell
496 /// (`sseqRecord.c:682-686`: `n_elements` clamped to the record's 40-byte
497 /// `s` buffer, then `strcmp`/`atof` read it as a C string).
498 CharArrayAsString { max_elements: usize },
499}
500
501/// How C gates a secondary field named by
502/// [`Record::fields_posted_with_value_mask`] *inside* the guard that decides
503/// whether VAL posts at all.
504///
505/// Both variants share the outer guard (the field posts only on a cycle where
506/// VAL's own monitor mask is live, and carries that same mask); they differ in
507/// whether C re-tests the secondary field's own value once inside it. Folding
508/// the two into one rule is what over- or under-posts the field: gating
509/// `timestamp`'s RVAL on its own change silences it (see [`Self::WithValue`]),
510/// and NOT gating `ai`'s RVAL on its own change posts a raw count that never
511/// moved.
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513pub enum ValuePostGate {
514 /// C re-tests the field's own previous value inside the guard, and posts
515 /// only if it moved: `ai` `RVAL` — `if (prec->oraw != prec->rval) {
516 /// db_post_events(&prec->rval, monitor_mask); prec->oraw = prec->rval; }`
517 /// (aiRecord.c:460-465).
518 OnChange,
519 /// C posts the field whenever the guard fires, with no test of its own
520 /// value: `timestamp` `RVAL` — `if (strncmp(oval, val, ...)) {
521 /// db_post_events(&val[0], mask); db_post_events(&rval, mask); }`
522 /// (timestampRecord.c:158-162). The VAL-string change is the *only* gate,
523 /// so a cycle that re-renders the same seconds count still re-posts RVAL.
524 WithValue,
525 /// [`Self::OnChange`]'s own-value test, but the guard is the record's
526 /// widened one ([`Record::take_secondary_value_mask`]) and the event
527 /// carries C's forced `monitor_mask | DBE_VALUE | DBE_LOG`: ao `RVAL` /
528 /// `RBV` — `if(prec->oraw != prec->rval) { db_post_events(&prec->rval,
529 /// monitor_mask|DBE_VALUE|DBE_LOG); prec->oraw = prec->rval; }`
530 /// (aoRecord.c:539-548), all inside `if(monitor_mask)` (`:536`).
531 ///
532 /// Distinct from [`Self::OnChange`] in BOTH halves, which is why it is a
533 /// third variant and not a flag: ai's RVAL rides VAL's own mask and VAL's
534 /// own guard, while ao's rides a forced mask and a guard `omod` can open
535 /// when VAL's is shut.
536 ///
537 /// The own-value test AND the advance of the record's "old" copy are one
538 /// step, [`Record::take_secondary_value_change`], never split across the
539 /// code that computed the new value — C assigns `oraw` INSIDE the post it
540 /// guards, so a cycle that changes RVAL without posting it must leave
541 /// `oraw` stale for the next cycle to find.
542 OnChangeForced,
543}
544
545/// The event mask ONE per-cycle mark posts with
546/// ([`Record::take_cycle_posted_fields`]).
547///
548/// A record can mark the same field from two different C `db_post_events` call
549/// sites in one cycle, and the two need not agree on the mask. aCalcout does
550/// exactly that with its arrays: `afterCalc` posts the AMASK-flagged ones with a
551/// LITERAL `DBE_VALUE|DBE_LOG` (`aCalcoutRecord.c:296`) while `monitor()` posts
552/// the NEWM-flagged ones with `monitor_mask|DBE_VALUE|DBE_LOG` (`:1034`). An
553/// array in BOTH masks gets BOTH events — the record marks it twice, and the
554/// variant carried with each mark is what keeps them distinguishable.
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum CyclePostMask {
557 /// A literal `DBE_VALUE` — no LOG bit, no alarm bits. C's shape for a
558 /// field the record re-DERIVED from the one it was given: sseq re-renders
559 /// `STRn` after a `DOn` write and posts it with a bare `DBE_VALUE`
560 /// (`sseqRecord.c:679`, `:1115`), while the view actually written carries
561 /// `DBE_VALUE|DBE_LOG`.
562 Value,
563 /// A literal `DBE_VALUE | DBE_LOG` — the alarm-transition bits are NOT
564 /// folded in, because this C call site does not have `monitor_mask` in
565 /// scope (aCalcout `afterCalc`, `aCalcoutRecord.c:296`).
566 ValueLog,
567 /// `monitor_mask | DBE_VALUE | DBE_LOG` — C's usual `monitor()` shape
568 /// (aCalcout `monitor()`, `aCalcoutRecord.c:1034`).
569 MonitorValueLog,
570}
571
572/// The [`ValuePostGate`] a record declared for `field`, or `None` when `field`
573/// is not one of its secondary value-mask fields.
574///
575/// The single lookup for [`Record::fields_posted_with_value_mask`], shared by
576/// every monitor loop (both `process_record_*` paths, the deferred-completion
577/// path, and `RecordInstance::process_local`) so they cannot drift apart on how
578/// a secondary field is gated.
579pub(crate) fn value_gate(
580 value_masked: &'static [(&'static str, ValuePostGate)],
581 field: &str,
582) -> Option<ValuePostGate> {
583 value_masked
584 .iter()
585 .find(|(name, _)| *name == field)
586 .map(|(_, gate)| *gate)
587}
588
589/// The event mask a change-detected AUXILIARY field posts with — the single
590/// owner of that decision, built once per cycle from the record's declarations
591/// and shared by every monitor loop (both `process_record_*` paths, the
592/// deferred-completion path, and `RecordInstance::process_local`), so they
593/// cannot drift apart on what mask a field carries.
594///
595/// C's usual shape for the "post every input/aux field that changed" loop is
596/// `monitor_mask | DBE_VALUE | DBE_LOG` (calcRecord.c:420, subRecord.c:400,
597/// motor `DBE_VAL_LOG`) — that is the default. Three record-declared exceptions
598/// narrow it, and no two are the same narrowing:
599///
600/// * [`Record::value_only_change_fields`] — a literal `DBE_VALUE`
601/// (tableRecord.c:659, scaler `Sn`): alarm bits + `DBE_VALUE`, never `LOG`.
602/// * [`Record::fields_posted_with_monitor_mask`] — `monitor_mask | DBE_VALUE`
603/// (swaitRecord.c:650): VAL's own monitor mask, so `DBE_LOG` rides along
604/// exactly when VAL's ADEL deadband crossed.
605/// * [`Record::fields_posted_without_alarm_bits`] — a literal
606/// `DBE_VALUE | DBE_LOG` (epidRecord.c:376): both value classes, alarm bits
607/// discarded.
608#[derive(Clone, Copy)]
609pub(crate) struct AuxPostMask {
610 value_only: &'static [&'static str],
611 monitor_masked: &'static [&'static str],
612 no_alarm_bits: &'static [&'static str],
613}
614
615impl AuxPostMask {
616 /// Read the record's three declarations once, outside the per-field loop.
617 pub(crate) fn of(record: &dyn Record) -> Self {
618 Self {
619 value_only: record.value_only_change_fields(),
620 monitor_masked: record.fields_posted_with_monitor_mask(),
621 no_alarm_bits: record.fields_posted_without_alarm_bits(),
622 }
623 }
624
625 /// `alarm_bits` is this cycle's `recGblResetAlarms` result; `deadband_mask`
626 /// is VAL's own monitor mask (those alarm bits, plus `DBE_VALUE` when MDEL
627 /// crossed and `DBE_LOG` when ADEL crossed).
628 pub(crate) fn mask_for(
629 &self,
630 field: &str,
631 alarm_bits: crate::server::recgbl::EventMask,
632 deadband_mask: crate::server::recgbl::EventMask,
633 ) -> crate::server::recgbl::EventMask {
634 use crate::server::recgbl::EventMask;
635 if self.value_only.contains(&field) {
636 alarm_bits | EventMask::VALUE
637 } else if self.monitor_masked.contains(&field) {
638 deadband_mask | EventMask::VALUE
639 } else if self.no_alarm_bits.contains(&field) {
640 EventMask::VALUE | EventMask::LOG
641 } else {
642 alarm_bits | EventMask::VALUE | EventMask::LOG
643 }
644 }
645}
646
647/// Outcome of a record's array-style monitor decision, returned by
648/// [`Record::array_monitor_post`] (C waveform/aai/aao `monitor()`,
649/// waveformRecord.c:291-326).
650#[derive(Debug, Clone, Copy)]
651pub struct ArrayMonitorPost {
652 /// Include `DBE_VALUE` on the VAL post this cycle (MPST = Always, or
653 /// MPST = On Change with a changed hash).
654 pub post_value: bool,
655 /// Include `DBE_LOG` on the VAL post this cycle (APST = Always, or
656 /// APST = On Change with a changed hash).
657 pub post_archive: bool,
658 /// The content hash changed this cycle (On Change mode) — the owner
659 /// posts `HASH` with a literal `DBE_VALUE`.
660 pub hash_changed: bool,
661}
662
663/// The record type's RSET metadata slots — which of C's six nullable
664/// `get_*` property functions the record type implements.
665///
666/// This is the port's `rset` property table, transcribed slot by slot
667/// from the `#define get_xxx NULL` lines of each C record's `.c`. It is
668/// what `dbGet` consults to *narrow* the caller's `options` mask
669/// (`dbAccess.c:336-427`), and therefore what decides whether QSRV marks
670/// an NT leaf at all (pvxs `ioc/iocsource.cpp:263-305`). Without it the
671/// port fabricated every leaf it could name and marked it as supplied —
672/// telling the client a made-up `display.precision = 0` on a `longout`,
673/// or `valueAlarm` bands at zero on a `waveform`, were authoritative.
674///
675/// A slot counts as supplied when the C function pointer is non-NULL,
676/// even if the function writes nothing for the field in question — C
677/// leaves the option bit set either way (e.g. `boRecord.c:294-299`,
678/// whose `get_units` writes `"s"` only for `HIGH`, yet `DBR_UNITS`
679/// survives for every `bo` field).
680/// What a record type's C `get_control_double` writes for a field its switch
681/// does not list — the slot's LAST arm.
682///
683/// Independent of [`crate::server::snapshot::PropertySupport::control_double`],
684/// which says whether the slot EXISTS at all (a NULL slot makes
685/// `dbAccess.c:257` fail and clears the option bit, so no leaf is served).
686/// This says what a slot that DOES exist answers when it falls through. The
687/// two genuinely differ: `acalcout` supplies the slot and still writes nothing
688/// for an unlisted field.
689///
690/// Slot-neutral: the two arm SHAPES are the same for `get_control_double` and
691/// `get_graphic_double`, but which one a record type takes is asked per slot —
692/// [`control_default_arm`] and [`graphic_default_arm`] are separate answers.
693/// `aSub` is the type that proves they must be: its `get_control_double` is a
694/// bare `recGblGetControlDouble` (`aSubRecord.c:372-376`) while its
695/// `get_graphic_double` (`:350-368`) has no recGbl call at all.
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697pub enum RsetDefaultArm {
698 /// The slot ends in `recGblGetControlDouble` / `recGblGetGraphicDouble` —
699 /// the field TYPE's numeric range (`recGbl.c:146-171`, table at
700 /// `:372-419`).
701 RecGblRange,
702 /// The slot returns without writing, so the `dbAccess.c:256` / `:216`
703 /// `(0.0, 0.0)` seed stands.
704 Seed,
705}
706
707/// The last arm of `rtype`'s C `get_control_double`.
708///
709/// Audited by reading each ported record type's own C rset. Every record in
710/// EPICS base delegates (`aiRecord.c:267`, `aoRecord.c:341`, `calcRecord.c:235`,
711/// `calcoutRecord.c:506`, `aSubRecord.c:372`, `subRecord.c:272`,
712/// `selRecord.c:203`, `seqRecord.c:342`, `dfanoutRecord.c:197`,
713/// `longinRecord.c:217`, `longoutRecord.c:268`, `int64inRecord.c:212`,
714/// `int64outRecord.c:251`, `boRecord.c:310`, `waveformRecord.c:268`,
715/// `aaiRecord.c:293`, `aaoRecord.c:296`, `subArrayRecord.c:262`,
716/// `compressRecord.c:487`, `histogramRecord.c:458`), as do the downstream
717/// types that supply the slot (`motorRecord.cc:3303`, `epidRecord.c:285`,
718/// `tableRecord.c:806`). The rest NULL it outright and never reach here
719/// (`biRecord.c`, `mbbiRecord.c`, `mbboRecord.c`, `mbbiDirectRecord.c`,
720/// `mbboDirectRecord.c`, `stringinRecord.c`, `stringoutRecord.c`,
721/// `lsiRecord.c`, `lsoRecord.c`, `eventRecord.c`, `fanoutRecord.c`,
722/// `permissiveRecord.c`, `printfRecord.c`, `stateRecord.c`,
723/// `sseqRecord.c:141`, `swaitRecord.c`, `transformRecord.c`, `busyRecord.c`,
724/// `asynRecord.c`, `throttleRecord.c:70`, `scalerRecord.c:157`).
725///
726/// Only the synApps calc pair writes nothing: both end `get_control_double`
727/// with a bare `return(0)` after their listed cases, so C serves the seed
728/// where a delegating record serves the type range. Measured on the
729/// differential oracle as 42 `acalcout`/`scalcout` fields.
730pub fn control_default_arm(rtype: &str) -> RsetDefaultArm {
731 match rtype {
732 // aCalcoutRecord.c:793-822, sCalcoutRecord.c:653-682 — the switch lists
733 // VAL/HIHI/HIGH/LOW/LOLO and the A-L / PA-PL ranges, then falls off
734 // the end into `return(0)` with no recGbl delegation.
735 "acalcout" | "scalcout" => RsetDefaultArm::Seed,
736 _ => RsetDefaultArm::RecGblRange,
737 }
738}
739
740/// The last arm of `rtype`'s C `get_graphic_double` — the twin of
741/// [`control_default_arm`], and NOT the same answer for every type.
742///
743/// Read from each ported type's own rset. `aSubRecord.c:350-368` is the one
744/// that separates the two slots: it tries `get_inlinkNumber` then
745/// `get_outlinkNumber` and, for a field that is neither, falls out of the
746/// function having written nothing — no `default:`, no recGbl call — so the
747/// `dbAccess.c:216` seed stands. Its `get_control_double` (`:372-376`) is a
748/// bare `recGblGetControlDouble` in the same file, which is why one shared bit
749/// could not answer both. Measured: `ASUB.PHAS` serves display 0/0 where
750/// `CALC.PHAS` serves the DBF_SHORT range ±32767.
751///
752/// The synApps calc pair ends the same way — the listed cases return early and
753/// the function ends `return(0)` with no delegation
754/// (`aCalcoutRecord.c:762-791`, `sCalcoutRecord.c:622-651`).
755///
756/// Every other ported type that supplies the slot delegates
757/// (`aiRecord.c:244`, `aoRecord.c:316`, `calcRecord.c:187`,
758/// `calcoutRecord.c:452`, `subRecord.c:222`, `selRecord.c:181`,
759/// `seqRecord.c:322`, `dfanoutRecord.c:181`, `longinRecord.c:190`,
760/// `int64inRecord.c:196`, `int64outRecord.c:235`, `longoutRecord.c:252`,
761/// `waveformRecord.c:251`, `aaiRecord.c:276`, `aaoRecord.c:279`,
762/// `subArrayRecord.c:231`, `compressRecord.c:471`, `histogramRecord.c:442`).
763pub fn graphic_default_arm(rtype: &str) -> RsetDefaultArm {
764 match rtype {
765 "aSub" | "acalcout" | "scalcout" => RsetDefaultArm::Seed,
766 _ => RsetDefaultArm::RecGblRange,
767 }
768}
769
770/// How `rtype`'s C `get_alarm_double` answers the fields it lists explicitly
771/// (see [`alarm_explicit_fields`]).
772///
773/// The severity gate is NOT universal, so this bit cannot be inferred from the
774/// base analog shape — it is read from each ported type's own rset.
775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
776pub enum AlarmValArm {
777 /// `pad->upper_alarm_limit = prec->hhsv ? prec->hihi : epicsNAN` — each
778 /// limit is served only when its severity is enabled. The base analog
779 /// shape (`aiRecord.c:290-301`, and ao/longin/longout/calc/calcout/sel/
780 /// sub/dfanout alike).
781 Gated,
782 /// `pad->upper_alarm_limit = prec->hihi` — the four limits verbatim, with
783 /// no severity test at all, so an unset record serves 0 rather than NaN
784 /// (`int64inRecord.c:235-246`, `int64outRecord.c:279-290`,
785 /// `sCalcoutRecord.c:683-696`, `aCalcoutRecord.c:823-836`,
786 /// `motorRecord.cc:3344-3361`, `epidRecord.c:289-301`).
787 Unconditional,
788}
789
790/// The fields `rtype`'s C `get_alarm_double` lists BEFORE its
791/// `recGblGetAlarmDouble` fall-through — the ones that take
792/// [`alarm_val_arm`]'s answer instead of the four NaN.
793///
794/// Empty means the rset lists nothing: even VAL falls to the default arm.
795/// `seqRecord.c:355-367` routes only its `DOn` fields (through their `DOLn`
796/// link), `aSubRecord.c:378-404` only its `INPn`/`OUTn` links, and
797/// `swaitRecord.c:608-612` is a bare `recGblGetAlarmDouble(paddr,pad)` with no
798/// field test whatsoever. A constant link supplies no alarm limits, so the link
799/// arm lands on the same four NaN — which is why only the listed set needs a
800/// per-type answer and the link fields do not.
801///
802/// `motorRecord.cc:3344-3361` is the one type listing a second field: its case
803/// is `fieldIndex == motorRecordVAL || fieldIndex == motorRecordDVAL`, so the
804/// dial-coordinate readback carries the same limits as VAL.
805pub fn alarm_explicit_fields(rtype: &str) -> &'static [&'static str] {
806 match rtype {
807 "seq" | "aSub" | "swait" => &[],
808 "motor" => &["VAL", "DVAL"],
809 _ => &["VAL"],
810 }
811}
812
813/// See [`AlarmValArm`]. Types whose rset lists nothing
814/// ([`alarm_explicit_fields`] empty) never consult this.
815pub fn alarm_val_arm(rtype: &str) -> AlarmValArm {
816 match rtype {
817 "int64in" | "int64out" | "scalcout" | "acalcout" | "motor" | "epid" => {
818 AlarmValArm::Unconditional
819 }
820 _ => AlarmValArm::Gated,
821 }
822}
823
824/// The record fields `rtype`'s C `get_graphic_double` reads for the fields its
825/// rset lists — the source of the record-level display limits.
826///
827/// `HOPR`/`LOPR` in every ported type but one: `motorRecord.cc:3221-3225`
828/// answers the soft travel limits `HLM`/`LLM` and never reads its own
829/// HOPR/LOPR.
830///
831/// The `"motor"` arm decides nothing today and is kept deliberately.
832/// `MotorRecord::field_metadata_override` (`motor-rs/src/record/mod.rs:201-205`)
833/// answers `Some(..)` for EVERY field, and the override layer runs after
834/// `route_field_metadata` (pinned by
835/// `epics-base-rs/tests/the_override_layer_runs_last.rs`), so every served
836/// motor field takes its window from `motor-rs` and never from this table.
837/// The arm states what C's rset does for the day a motor field reaches the
838/// routing layer without an override.
839pub fn graphic_limit_fields(rtype: &str) -> (&'static str, &'static str) {
840 match rtype {
841 "motor" => ("HLM", "LLM"),
842 _ => ("HOPR", "LOPR"),
843 }
844}
845
846/// Which of the record's own fields `rtype`'s C `get_control_double` answers
847/// with for the fields its rset lists.
848#[derive(Debug, Clone, Copy, PartialEq, Eq)]
849pub enum ControlLimitSource {
850 /// `DRVH`/`DRVL` unconditionally (`aoRecord.c:356-357`).
851 Drive,
852 /// `DRVH`/`DRVL` when `DRVH > DRVL`, else `HOPR`/`LOPR`
853 /// (`longoutRecord.c:282-287`, `int64outRecord.c:265-270`).
854 DriveWhenSet,
855 /// The soft travel limits `HLM`/`LLM` (`motorRecord.cc:3272-3276`).
856 SoftLimits,
857 /// `HOPR`/`LOPR` — every other ported type that supplies the slot.
858 Operator,
859}
860
861/// See [`ControlLimitSource`]. Read from each ported type's own rset; the
862/// three named types are the only ones in base or the ported modules whose
863/// control limits are not the operator range.
864///
865/// The `"motor"` arm is dead for the same reason, and kept for the same
866/// reason, as the `"motor"` arm of [`graphic_limit_fields`].
867pub fn control_limit_source(rtype: &str) -> ControlLimitSource {
868 match rtype {
869 "ao" => ControlLimitSource::Drive,
870 "longout" | "int64out" => ControlLimitSource::DriveWhenSet,
871 "motor" => ControlLimitSource::SoftLimits,
872 _ => ControlLimitSource::Operator,
873 }
874}
875
876pub fn default_property_support(rtype: &str) -> crate::server::snapshot::PropertySupport {
877 use crate::server::snapshot::PropertySupport as P;
878 match rtype {
879 // Every numeric slot, no enum strings.
880 // aiRecord.c:68-87, aoRecord.c:67-86, calcRecord.c:63-82,
881 // calcoutRecord.c:67-86, selRecord.c:58-77, subRecord.c:62-81,
882 // dfanoutRecord.c:66-85, seqRecord.c:56-75.
883 "ai" | "ao" | "calc" | "calcout" | "sel" | "sub" | "dfanout" | "seq" => P::NUMERIC,
884
885 // Integer scalars: `#define get_precision NULL`
886 // (longinRecord.c, longoutRecord.c, int64inRecord.c,
887 // int64outRecord.c). This is the measured `longout` case —
888 // pvxs leaves `display.precision` absent, the port sent 0.
889 "longin" | "longout" | "int64in" | "int64out" => P {
890 precision: false,
891 ..P::NUMERIC
892 },
893
894 // Arrays and compress: `#define get_alarm_double NULL`
895 // (waveformRecord.c, aaiRecord.c, aaoRecord.c,
896 // subArrayRecord.c, compressRecord.c, histogramRecord.c).
897 // This is the measured `waveform` case — pvxs leaves all four
898 // `valueAlarm.*Limit` absent, the port sent four zeros.
899 "waveform" | "aai" | "aao" | "subArray" | "compress" | "histogram" => P {
900 alarm_double: false,
901 ..P::NUMERIC
902 },
903
904 // No property slots at all: every `get_*` is `#define`d NULL.
905 // stringinRecord.c:62-81, stringoutRecord.c:64-83,
906 // lsiRecord.c:287-306, lsoRecord.c:328-347,
907 // eventRecord.c:62-81, permissiveRecord.c:56-75,
908 // stateRecord.c:58-77, printfRecord.c:456-475,
909 // fanoutRecord.c:60-79, timestampRecord (std-rs).
910 // This is the measured `stringout` case — pvxs leaves
911 // `display.units` absent, the port sent "".
912 "stringin" | "stringout" | "lsi" | "lso" | "event" | "permissive" | "state" | "printf"
913 | "fanout" | "timestamp" => P::NONE,
914
915 // Enum records. `biRecord.c:61-80` and `mbbiRecord.c:65-84` /
916 // `mbboRecord.c:64-83` NULL every numeric slot and supply only
917 // `get_enum_strs`. `boRecord.c:54-61` keeps `get_units`,
918 // `get_precision` and `get_control_double` (they serve the
919 // `HIGH` field) but NULLs `get_graphic_double` and
920 // `get_alarm_double`.
921 "bi" | "mbbi" | "mbbo" => P {
922 enum_strs: true,
923 ..P::NONE
924 },
925 "bo" => P {
926 units: true,
927 precision: true,
928 control_double: true,
929 enum_strs: true,
930 ..P::NONE
931 },
932 // busyRecord.c (synApps busy): units/graphic/control/alarm NULL,
933 // get_precision and get_enum_strs present.
934 "busy" => P {
935 precision: true,
936 enum_strs: true,
937 ..P::NONE
938 },
939 // mbbiDirectRecord.c:63-81 / mbboDirectRecord.c:63-81 — only
940 // `get_precision` survives, and C's DBF_FLOAT/DOUBLE gate
941 // (`dbAccess.c:388-395`) drops it again for their DBF_ENUM/LONG
942 // value, so nothing is marked. `Snapshot::precision` applies
943 // that gate.
944 "mbbiDirect" | "mbboDirect" => P {
945 precision: true,
946 ..P::NONE
947 },
948
949 // synApps, transcribed the same way.
950 // sCalcoutRecord.c / aCalcoutRecord.c / epidRecord.c /
951 // motorRecord.cc:259-279 (the rset table: get_units, get_precision,
952 // get_graphic_double, get_control_double and get_alarm_double all
953 // supplied, get_enum_strs NULL) / aSubRecord.c: full numeric set, no
954 // enum strings.
955 "scalcout" | "acalcout" | "motor" | "epid" | "aSub" => P::NUMERIC,
956 // scalerRecord.c:147-158 NULLs every property slot but one:
957 // `#define get_units NULL` (:151), `get_enum_strs` (:154),
958 // `get_graphic_double` (:156), `get_control_double` (:157) and
959 // `get_alarm_double` (:158). Only `get_precision` (:152) survives.
960 //
961 // Grouping scaler with the full-numeric synApps types claimed TEN
962 // leaves QSRV2 never serves: `display.units`, the two `display.limit*`,
963 // the two `control.limit*`, the four `valueAlarm.*Limit` — and
964 // `display.precision`, which pvxs assigns only inside its
965 // `DBR_GR_DOUBLE` branch (`iocsource.cpp:288-291`). That nesting is
966 // also why `precision` stays true here and yet marks nothing: it
967 // records what the rset supplies, exactly as `transform`/`sseq` do.
968 "scaler" => P {
969 precision: true,
970 ..P::NONE
971 },
972 // tableRecord.cc (optics): `#define get_alarm_double NULL`.
973 "table" => P {
974 alarm_double: false,
975 ..P::NUMERIC
976 },
977 // swaitRecord.c: get_units and get_control_double are NULL.
978 "swait" => P {
979 units: false,
980 control_double: false,
981 ..P::NUMERIC
982 },
983 // Only `get_precision` survives; the rset NULLs the other five.
984 // transformRecord.c; sseqRecord.c:124-144 (the rset table itself —
985 // `NULL, /* get_units */ get_precision, /* get_precision */ ...
986 // NULL, /* get_graphic_double */ NULL, /* get_control_double */
987 // NULL /* get_alarm_double */`). `sseq` was previously grouped with
988 // the full-numeric synApps types, which marked six leaves per field
989 // that QSRV2 omits entirely.
990 //
991 // `asyn` is NOT here: asyn-rs owns that record and declares its own
992 // row (asynRecord.c:84-91, the same shape) — a downstream crate
993 // cannot reach this table, which is why `Record::property_support`
994 // is the hook and this is only its default.
995 "transform" | "sseq" => P {
996 precision: true,
997 ..P::NONE
998 },
999 "throttle" => P {
1000 precision: true,
1001 graphic_double: true,
1002 ..P::NONE
1003 },
1004
1005 // A record type whose C rset the port has not transcribed keeps
1006 // the pre-existing "supplies what it populated" behaviour rather
1007 // than silently losing metadata. Add an arm above — with the C
1008 // file and line — when porting a new record type.
1009 _ => P::NUMERIC,
1010 }
1011}
1012
1013/// Per-field metadata deltas returned by
1014/// [`Record::field_metadata_override`].
1015///
1016/// Each `Some` member replaces the corresponding member of the
1017/// snapshot's record-level display/control metadata; `None` members
1018/// keep the record-level value.
1019#[derive(Debug, Clone, Default)]
1020pub struct FieldMetadataOverride {
1021 /// `display.units` — C RSET `get_units`.
1022 pub units: Option<crate::types::PvString>,
1023 /// `display.precision` — C RSET `get_precision`.
1024 pub precision: Option<i16>,
1025 /// `(upper, lower)` display limits — C RSET `get_graphic_double`.
1026 pub disp_limits: Option<(f64, f64)>,
1027 /// `(upper, lower)` control limits — C RSET `get_control_double`.
1028 pub ctrl_limits: Option<(f64, f64)>,
1029 /// `(hihi, high, low, lolo)` — C RSET `get_alarm_double`.
1030 pub alarm_limits: Option<(f64, f64, f64, f64)>,
1031}
1032
1033/// Side-effect actions that a record requests from the processing framework.
1034///
1035/// Records return these from `process()` via `ProcessOutcome::actions`.
1036/// The framework executes them at the appropriate point in the processing
1037/// cycle, keeping records as pure state machines without direct DB access.
1038#[derive(Clone, Debug, PartialEq)]
1039pub enum ProcessAction {
1040 /// Write a value to a DB link. The framework reads `link_field` from the
1041 /// record to get the target PV name, then writes `value` to that PV.
1042 ///
1043 /// Executed after alarm/snapshot, before FLNK.
1044 /// Example: scaler writes CNT to COUT/COUTP links.
1045 WriteDbLink {
1046 link_field: &'static str,
1047 value: EpicsValue,
1048 },
1049
1050 /// Resolve an OUT link's TARGET ([`OutTarget`]) and hand it to the record
1051 /// through [`Record::set_resolved_out_target`], BEFORE `process()` runs.
1052 ///
1053 /// **Pre-process action** — the OUT-link twin of [`Self::ReadDbLink`],
1054 /// and C's `checkLinks`-cached `lnk_field_type`: a record whose fire-time
1055 /// branch depends on the target's DBF class (sseq decides the wire buffer
1056 /// AND whether a `WAITn` put-callback is issued from the one switch,
1057 /// `sseqRecord.c:714-792`) must have the class in hand when it decides, not
1058 /// after the framework's put path has already been entered.
1059 ResolveOutTarget { link_field: &'static str },
1060
1061 /// Read a value from a DB link into a record field. The framework reads
1062 /// `link_field` from the record to get the source PV name, reads that PV,
1063 /// and writes the result into `target_field` via an internal put that
1064 /// bypasses read-only checks.
1065 ///
1066 /// The value delivered is the link target's **native** [`EpicsValue`] — it
1067 /// is NOT coerced to a numeric type on the way in. The record coerces (or
1068 /// preserves) it at its own `put_field`/`put_field_internal` boundary, so a
1069 /// string-class source can reach a string field byte-exact (the `sseq`
1070 /// `DOLn`→`STRn` path, C `sseqRecord.c:643-705`). Records whose
1071 /// `target_field` is numeric simply convert there, exactly as before.
1072 ///
1073 /// **Pre-process action**: executed BEFORE the next process() cycle so
1074 /// the value is immediately available. This matches C EPICS `dbGetLink()`
1075 /// which is synchronous/immediate.
1076 ///
1077 /// Example: throttle reads SINP into VAL when SYNC is triggered.
1078 ReadDbLink {
1079 link_field: &'static str,
1080 target_field: &'static str,
1081 },
1082
1083 /// Schedule a re-process of this record after the given duration.
1084 /// The framework defers it through
1085 /// `database/processing.rs` (`schedule_delayed_reprocess`), which uses
1086 /// `spawn_background` + `sleep_background` — *not* the ambient
1087 /// `tokio::spawn`/`tokio::time::sleep`, which a record-processing thread
1088 /// has no runtime for. The current cycle's OUT/FLNK/notify proceed
1089 /// normally.
1090 ///
1091 /// Equivalent to C EPICS `callbackRequestDelayed()` + `scanOnce()`
1092 /// (`db/callback.c:410` (`callbackRequestDelayed`), `db/dbScan.c:660`
1093 /// (`scanOnce`); epics-base R7.0.10).
1094 ReprocessAfter(std::time::Duration),
1095
1096 /// C `callbackRequestDelayed()` armed with a handler that mutates the
1097 /// record BEFORE it calls `dbProcess` — `boRecord.c::myCallbackFunc`
1098 /// (:105-118) and its `busyRecord.c:107-124` twin, the HIGH one-shot.
1099 ///
1100 /// Distinct from [`Self::ReprocessAfter`], whose timer re-enters
1101 /// `process()` unchanged. Here the fire runs
1102 /// [`Record::delayed_callback_fire`] under the record gate first, and that
1103 /// hook — not `process()` — performs the timer's own mutation. The record
1104 /// therefore keeps no "a timer is pending" flag for `process()` to consume,
1105 /// so a foreign scan, a caput or a FLNK arriving inside the delay window
1106 /// cannot take the one-shot the timer owns.
1107 DelayedCallbackAfter(std::time::Duration),
1108
1109 /// C `scanOnce(precord)` — queue ONE process of this record, now.
1110 ///
1111 /// A record's `special()` emits this when a put changed state the record
1112 /// must act on but the put itself will not process the record. C guards
1113 /// every such call with `if (precord->scan)` — scaler `special()`
1114 /// (scalerRecord.c:655 CNT, :667 CONT), whose comment is exactly the
1115 /// contract: *"Scan record if it's not Passive. (If it's Passive, it'll
1116 /// get scanned automatically, since .cnt is a Process-Passive field.)"*
1117 ///
1118 /// The FRAMEWORK owns that gate, not the record: the framework owns SCAN
1119 /// and owns the `pp(TRUE)` reprocess decision (`dbPutField`,
1120 /// dbAccess.c:1265-1268), and a record's `special()` cannot see either. So
1121 /// a record emits `ScanOnce` unconditionally wherever C calls `scanOnce`,
1122 /// and the executor drops it for a Passive record — where the put's own
1123 /// process already covers it and a second one would double-process.
1124 ///
1125 /// Queued, not inline: C's `scanOnce` hands the record to the scan-once
1126 /// thread, which takes `dbScanLock` — so the process lands after the
1127 /// putting thread leaves `dbPutField`.
1128 ScanOnce,
1129
1130 /// Send a named command to the device support driver.
1131 /// The framework calls `DeviceSupport::handle_command()` with this data.
1132 /// Used by scaler to request reset/arm/write_preset operations
1133 /// without the record holding a direct driver reference.
1134 DeviceCommand {
1135 command: &'static str,
1136 args: Vec<EpicsValue>,
1137 },
1138
1139 /// Write a value to a DB link as a put-*with-completion*, then re-enter
1140 /// THIS record's `process()` when the downstream operation completes.
1141 ///
1142 /// The framework arms a put-notify wait-set (C `dbProcessNotify`),
1143 /// writes `link_field`'s target through it, releases the initiator's
1144 /// own count, and wires the completion to an async re-entry of this
1145 /// record (`mint_async_token` + `reprocess_on_notify`). The record
1146 /// returns [`RecordProcessResult::AsyncPending`] alongside this action
1147 /// and is re-entered once the downstream record (and its FLNK/OUT
1148 /// chain) finishes — the synApps `sseq` `WAITn` "wait for the put
1149 /// callback" dependency (`sseqRecord.c::processNextLink`,
1150 /// `dbCaPutLinkCallback`). Built on the same `new_put_notify` +
1151 /// `reprocess_on_notify` primitive an out-of-band
1152 /// [`crate::server::database::AsyncDbHandle`] caller uses.
1153 ///
1154 /// Executed before FLNK, like [`Self::WriteDbLink`].
1155 WriteDbLinkNotify {
1156 link_field: &'static str,
1157 value: EpicsValue,
1158 },
1159
1160 /// (Re)arm this record's monitor watchdog — C `histogramRecord.c::wdogInit`
1161 /// (:126-152), whose `callbackRequestDelayed(&pcallback->callback,
1162 /// prec->sdel)` starts (or restarts) the periodic
1163 /// [`Record::watchdog_fire`] tick.
1164 ///
1165 /// Emitted from a record's `special()` when the put changed the watchdog's
1166 /// period (histogram SDEL is `special(SPC_RESET)` precisely so it can
1167 /// re-arm, `histogramRecord.c:266-268`). The framework also arms every
1168 /// record's watchdog once at `iocInit`, which is C's other `wdogInit` call
1169 /// site (`init_record` pass 1, `:168`).
1170 ///
1171 /// Arming supersedes any tick already pending for the record, exactly as
1172 /// C's `callbackRequestDelayed` replaces an outstanding delayed callback.
1173 ArmWatchdog,
1174
1175 /// Cancel this record's outstanding async re-entry (C
1176 /// `callbackCancelDelayed`): the framework advances the record's
1177 /// re-entry generation so any pending `ReprocessAfter` timer or
1178 /// `WriteDbLinkNotify` completion re-entry becomes a structural no-op
1179 /// (the `AsyncToken` gate), with no runtime "is-aborted" check on the
1180 /// re-entry path. Used by `sseq` `ABORT` to drop a pending `DLYn`
1181 /// delay or `WAITn` wait; the record resets its own sequence state in
1182 /// the same `process()` cycle that emits this.
1183 CancelReprocess,
1184}
1185
1186/// What the [`ProcessAction::DelayedCallbackAfter`] timer does once the
1187/// record's [`Record::delayed_callback_fire`] handler has run — the three arms
1188/// of C `boRecord.c::myCallbackFunc` (:105-118).
1189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1190pub enum DelayedCallbackOutcome {
1191 /// C's `else` branch (`boRecord.c:115-117`): the handler made its change,
1192 /// so re-enter `process()` now.
1193 Reprocess,
1194 /// C's `if (prec->pact)` branch (`boRecord.c:107-114`): the record is
1195 /// mid-async-cycle, so the handler changed nothing and the timer is re-armed
1196 /// for this long instead.
1197 Rearm(std::time::Duration),
1198 /// C's `if (prec->pact)` branch with its inner test false: the timer expires
1199 /// with nothing left to do, neither mutating nor processing.
1200 Drop,
1201}
1202
1203/// Result of a record's process() call.
1204///
1205/// Determines how the framework handles the current processing cycle.
1206/// Side-effect actions (link writes, delayed reprocess, etc.) are expressed
1207/// separately in `ProcessOutcome::actions`.
1208#[derive(Clone, Debug, PartialEq)]
1209pub enum RecordProcessResult {
1210 /// Processing completed synchronously this cycle.
1211 /// Framework proceeds with alarm/timestamp/snapshot/OUT/FLNK.
1212 Complete,
1213 /// Processing started but not yet complete (PACT stays set).
1214 /// Current cycle skips alarm/timestamp/snapshot/OUT/FLNK.
1215 /// ProcessActions (if any) are still executed.
1216 AsyncPending,
1217 /// Async pending, but notify these intermediate field changes immediately.
1218 /// Used by motor records to flush DMOV=0 before the move completes.
1219 AsyncPendingNotify(Vec<(String, EpicsValue)>),
1220 /// Completed synchronously (PACT cleared, unlike `AsyncPending`), but the
1221 /// record produced no new value to publish this cycle — the framework must
1222 /// skip the value-publication epilogue (UDF clear / timestamp / monitor /
1223 /// FLNK). C parity `compressRecord.c:365` `if (status != 1)`: a compress
1224 /// record still accumulating toward its next compressed sample runs none of
1225 /// `recGblGetTimeStamp` / `monitor` / `recGblFwdLink` on that cycle.
1226 CompleteNoEmit,
1227 /// Ran the value-publication epilogue NOW (UDF clear / timestamp / monitor —
1228 /// VAL and the alarm fields are posted this cycle), but the OUTPUT side (OUT
1229 /// link write / OEVT / forward link) is deferred to a scheduled
1230 /// reprocess, with PACT held across the wait. C parity `swaitRecord.c::process`
1231 /// (lines 425-481): when `schedOutput` arms the ODLY watchdog it sets
1232 /// `async=TRUE`, so `process` still runs `monitor()` (line 475) — posting the
1233 /// value side at the START of the delay — but skips the `if(!async)
1234 /// {recGblFwdLink; pact=FALSE;}` tail; the deferred `execOutput` (watchdog,
1235 /// at delay-END) does the OUT write + OEVT + forward link and posts no
1236 /// monitors. Unlike the calcout/scalcout/acalcout family, whose C `process`
1237 /// `return`s BEFORE `monitor()` (calcoutRecord.c:282, only `dlya` posted), so
1238 /// they defer the value side too and use `AsyncPendingNotify`. The deferral
1239 /// must carry a [`ProcessAction::ReprocessAfter`] — that scheduled reprocess
1240 /// is the continuation that releases the held PACT (same by-construction
1241 /// invariant as the `AsyncPendingNotify` ODLY defer).
1242 CompleteDeferOutput,
1243 /// Completed synchronously (PACT cleared), and the framework runs the ALARM
1244 /// epilogue ONLY: the UDF update, `check_alarms`, `recGblResetAlarms`
1245 /// (committing SEVR/STAT/AMSG and posting those fields with their C masks)
1246 /// and the timestamp. The VALUE side is skipped entirely — no `monitor()`
1247 /// value posts (so the last-posted trackers stay put and the next publishing
1248 /// cycle re-detects the change, exactly as C leaves `LA..LP` un-updated), no
1249 /// OUT / OEVT write, no process actions, no forward link.
1250 ///
1251 /// C parity `transformRecord.c:554-560`: an INVALID input severity with
1252 /// `IVLA == transformIVLA_DO_NOTHING` makes `process()` run
1253 /// `recGblGetTimeStamp` + `checkAlarms` + `recGblResetAlarms`, clear `pact`
1254 /// and `return` — skipping the calc loop, all 16 OUTx `dbPutLink` writes,
1255 /// `monitor()` and `recGblFwdLink()`.
1256 ///
1257 /// Distinct from [`RecordProcessResult::CompleteNoEmit`], which skips the
1258 /// alarm commit and the timestamp too (C `compressRecord.c:365` returns
1259 /// before `checkAlarms`).
1260 CompleteAlarmOnly,
1261}
1262
1263/// Complete outcome of a record's process() call.
1264///
1265/// Contains the processing result (Complete, AsyncPending, etc.) and a list
1266/// of side-effect actions for the framework to execute.
1267#[derive(Clone, Debug)]
1268pub struct ProcessOutcome {
1269 pub result: RecordProcessResult,
1270 pub actions: Vec<ProcessAction>,
1271 /// Set by the framework when device support's read() returned
1272 /// `did_compute: true`. The record's process() can check this to
1273 /// skip its built-in computation (e.g., PID). Replaces the `pid_done`
1274 /// flag pattern.
1275 pub device_did_compute: bool,
1276 /// Field stores this cycle owes the framework, to be applied only AFTER
1277 /// the cycle's queued [`ProcessAction::WriteDbLink`] have executed.
1278 ///
1279 /// C runs a record's `dbPutLink` calls and the completion-flag clear that
1280 /// follows them inside ONE `dbScanLock` — `sseqRecord.c::processCallback`
1281 /// puts `LNKn` (`:714-792`) and `asyncFinish` clears `busy` (`:498-505`)
1282 /// with the lock held throughout, and `scalerRecord.c::process` clears
1283 /// `cnt` (`:370`) and puts `COUT`/`COUTP` (`:457`, `:463`) in one cycle —
1284 /// so `dbGetField`, which takes the same lock, can never observe the flag
1285 /// clear ahead of the writes. The port cannot hold the record's data guard
1286 /// across the writes (a self/cyclic OUT link would dead-lock the
1287 /// non-reentrant guard), so a `process()` that both queues a write and
1288 /// clears its flag exposes a state C cannot produce: `BUSY == 0` with
1289 /// `LNKn` unwritten, `CNT == 0` with `COUT` unwritten.
1290 ///
1291 /// A record therefore does not store such a field in `process()`; it emits
1292 /// it here, and the framework applies it — store plus a `DBE_VALUE` monitor
1293 /// post — immediately after this cycle's link writes and before the
1294 /// cycle's snapshot notification. One owner for one transition.
1295 ///
1296 /// Applied in order, so a cycle that raises and clears the same field
1297 /// (`sseqRecord.c:302-305` `busy = 1` then an invalid `SELN` reaching
1298 /// `asyncFinish`) publishes both transitions exactly as C does.
1299 ///
1300 /// Only the STORE moves. A flag whose value is an input to a later
1301 /// decision in the same cycle keeps its `process()` store: motor's `DMOV`
1302 /// gates `recGblFwdLink` at `motorRecord.cc:1509-1510`, so it is read after
1303 /// it is set and does not belong here.
1304 pub post_write_fields: Vec<(String, EpicsValue)>,
1305}
1306
1307impl ProcessOutcome {
1308 /// Shorthand for a simple Complete with no actions.
1309 pub fn complete() -> Self {
1310 Self {
1311 result: RecordProcessResult::Complete,
1312 actions: Vec::new(),
1313 device_did_compute: false,
1314 post_write_fields: Vec::new(),
1315 }
1316 }
1317
1318 /// Shorthand for Complete with actions.
1319 pub fn complete_with(actions: Vec<ProcessAction>) -> Self {
1320 Self {
1321 result: RecordProcessResult::Complete,
1322 actions,
1323 device_did_compute: false,
1324 post_write_fields: Vec::new(),
1325 }
1326 }
1327
1328 /// Completed synchronously, but no new value was emitted this cycle, so
1329 /// the framework skips the value-publication epilogue (UDF clear /
1330 /// timestamp / monitor / FLNK). See `RecordProcessResult::CompleteNoEmit`.
1331 pub fn complete_no_emit() -> Self {
1332 Self {
1333 result: RecordProcessResult::CompleteNoEmit,
1334 actions: Vec::new(),
1335 device_did_compute: false,
1336 post_write_fields: Vec::new(),
1337 }
1338 }
1339
1340 /// Completed synchronously with the alarm epilogue only — no value posts,
1341 /// no output, no forward link. See `RecordProcessResult::CompleteAlarmOnly`.
1342 pub fn complete_alarm_only() -> Self {
1343 Self {
1344 result: RecordProcessResult::CompleteAlarmOnly,
1345 actions: Vec::new(),
1346 device_did_compute: false,
1347 post_write_fields: Vec::new(),
1348 }
1349 }
1350
1351 /// Shorthand for AsyncPending with no actions.
1352 pub fn async_pending() -> Self {
1353 Self {
1354 result: RecordProcessResult::AsyncPending,
1355 actions: Vec::new(),
1356 device_did_compute: false,
1357 post_write_fields: Vec::new(),
1358 }
1359 }
1360}
1361
1362impl Default for ProcessOutcome {
1363 fn default() -> Self {
1364 Self::complete()
1365 }
1366}
1367
1368/// Result of setting a common field, indicating what scan index updates are needed.
1369#[derive(Clone, Debug, PartialEq, Eq)]
1370pub enum CommonFieldPutResult {
1371 NoChange,
1372 ScanChanged {
1373 old_scan: ScanType,
1374 new_scan: ScanType,
1375 phas: i16,
1376 },
1377 PhasChanged {
1378 scan: ScanType,
1379 old_phas: i16,
1380 new_phas: i16,
1381 },
1382}
1383
1384/// Read-only snapshot of framework-owned `CommonFields` state that a
1385/// record's `process()` or device support's `read()` needs to see
1386/// *during* the processing cycle.
1387///
1388/// The framework owns `RecordInstance.common`; a record `process()`
1389/// receives only `&mut self` (the concrete record) and device support
1390/// `read()` receives only `&mut dyn Record`. Neither can reach
1391/// `CommonFields`. C records, by contrast, see `dbCommon` directly —
1392/// e.g. `epidRecord.c:195` reads `pepid->udf`, `timestampRecord.c:90`
1393/// reads `ptimestamp->tse`, `devTimeOfDay.c:122` reads `psi->phas`.
1394///
1395/// The framework builds a `ProcessContext` from `common` and pushes it
1396/// onto the record (via [`Record::set_process_context`]) and onto the
1397/// device support (via
1398/// [`crate::server::device_support::DeviceSupport::set_process_context`])
1399/// immediately before the respective call. This mirrors the existing
1400/// `set_device_did_compute` framework-set-hook pattern: additive,
1401/// no `process()` / `read()` signature change.
1402#[derive(Clone, Debug, PartialEq)]
1403pub struct ProcessContext {
1404 /// `dbCommon.udf` — value is undefined. C records check this at the
1405 /// top of `process()` (e.g. `epidRecord.c:195`).
1406 pub udf: bool,
1407 /// `dbCommon.udfs` — alarm severity raised for a UDF record.
1408 pub udfs: crate::server::record::AlarmSeverity,
1409 /// `dbCommon.nsev` — the *pending* (new) alarm severity this cycle has
1410 /// accumulated so far, BEFORE the record body runs. C `dbGetLink` folds an
1411 /// `MS`-class input link's severity into `nsev` at fetch time, so a record
1412 /// body that branches on the input severity reads it here — e.g.
1413 /// `transformRecord.c:554` `if ((ptran->nsev >= INVALID_ALARM) && (ptran->ivla
1414 /// == transformIVLA_DO_NOTHING))`. The framework folds every input-link alarm
1415 /// into `common.nsev` before building this snapshot, so `nsev` is the single
1416 /// source of truth; the record never re-derives it from the links.
1417 pub nsev: crate::server::record::AlarmSeverity,
1418 /// `dbCommon.phas` — phase. Used by device support for format
1419 /// selection (`devTimeOfDay.c:122`).
1420 pub phas: i16,
1421 /// `dbCommon.tse` — time-stamp event. `timestampRecord.c:90`
1422 /// branches on `tse == epicsTimeEventDeviceTime`.
1423 pub tse: i16,
1424 /// `dbCommon.time` — the record's current resolved time stamp at the
1425 /// start of this cycle (the previous cycle's stamp, or `UNIX_EPOCH`
1426 /// before the first process). Device support that has to format the
1427 /// record's time during `read()` — the std module's `devTimeOfDay.c`
1428 /// `recGblGetTimeStamp(psi)` call, which runs *before* the framework's
1429 /// per-cycle timestamp application — resolves the stamp with
1430 /// [`crate::server::recgbl::get_time_stamp`]`(tse, time)`. The `time`
1431 /// member is the device-provided value that helper returns verbatim on
1432 /// the `TSE == epicsTimeEventDeviceTime (-2)` branch.
1433 pub time: std::time::SystemTime,
1434 /// `dbCommon.tsel` — time-stamp event link string.
1435 pub tsel: String,
1436 /// `dbCommon.dtyp` — device-support type name. A record's
1437 /// `process()` / pre-process hooks can branch on the DTYP to mirror
1438 /// C device support that lives in a separate DSET (e.g. the epid
1439 /// record's `devEpidSoftCallback` callback DSET drives the TRIG
1440 /// readback link, whereas `devEpidSoft` does not).
1441 pub dtyp: String,
1442 /// The callback band `dbCommon.prio` selects for work this cycle defers —
1443 /// C `seqRecord.c:145-146`, which re-runs
1444 /// `callbackSetPriority(prec->prio, &pcb->callback)` at the top of every
1445 /// `process()` so a `PRIO` written between cycles takes effect on the next
1446 /// one. Already converted: a record stashes this and hands it to
1447 /// [`crate::runtime::task::spawn_background`] rather than re-deriving a
1448 /// band from a raw menu index.
1449 pub callback_priority: crate::runtime::task::CallbackPriority,
1450}
1451
1452/// C `epicsTime.h`: `epicsTimeEventDeviceTime` — the `TSE` sentinel
1453/// meaning "device support provides the time stamp". `timestampRecord.c`
1454/// uses it to take the OS-clock branch instead of `recGblGetTimeStamp`.
1455pub const EPICS_TIME_EVENT_DEVICE_TIME: i16 = -2;
1456
1457/// Snapshot of changes from a process cycle, used for notify outside lock.
1458pub struct ProcessSnapshot {
1459 /// `(field, value, mask)` — every posted field carries its own
1460 /// `DBE_*` posting mask, mirroring C's per-field
1461 /// `db_post_events(prec, &field, mask)`. One process cycle posts
1462 /// different classes per field: a deadband-gated readback narrows
1463 /// to the deadbands that actually crossed (MDEL → `DBE_VALUE`,
1464 /// ADEL → `DBE_LOG`; motorRecord.cc `monitor()` 3476-3507,
1465 /// aiRecord.c `monitor()`), while a change-detected auxiliary
1466 /// field posts `DBE_VALUE | DBE_LOG` (motorRecord.cc 3522-3645
1467 /// `DBE_VAL_LOG`; calcRecord.c:420). A single record-wide mask
1468 /// collapses that granularity — an archive-only deadband crossing
1469 /// would wrongly reach `DBE_VALUE` subscribers whenever any other
1470 /// field changed in the same pass.
1471 pub changed_fields: Vec<(String, EpicsValue, crate::server::recgbl::EventMask)>,
1472}
1473
1474impl ProcessSnapshot {
1475 /// The union of every `DBE_*` class this cycle actually published — the
1476 /// port's answer to "what did `db_post_events` send for this record".
1477 ///
1478 /// A field carrying an empty mask is not posted at all
1479 /// (`RecordInstance::notify_from_snapshot` skips it), so an empty union
1480 /// means the cycle published nothing and no monitor of any class fired.
1481 pub fn published_mask(&self) -> crate::server::recgbl::EventMask {
1482 self.changed_fields
1483 .iter()
1484 .fold(crate::server::recgbl::EventMask::NONE, |acc, (_, _, m)| {
1485 acc | *m
1486 })
1487 }
1488}
1489
1490/// What C's `fetch_values()` does when one of the record's input links fails
1491/// to read, and whether that failure gates the record body.
1492///
1493/// Every C record with an INPA..INPx block has a `fetch_values()` helper, but
1494/// they do not share a failure shape, so the framework cannot pick one rule
1495/// for all of them — each record declares its own via
1496/// [`Record::input_fetch_policy`].
1497///
1498/// The two dimensions C varies are "does the loop stop at the first failure"
1499/// and "does a failure gate the record body", and it uses three of the four
1500/// combinations. Whichever variant a record picks, the framework reduces the
1501/// cycle to ONE outcome — C's `fetch_values()` return status, zero or not —
1502/// and delivers it through a single owner: [`Record::set_fetch_gate_failed`]
1503/// for records that compute in their own `process()`, and
1504/// `RecordInstance::suppress_subroutine_run` for the two whose body is a
1505/// framework-dispatched subroutine (sub/aSub).
1506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1507pub enum InputFetchPolicy {
1508 /// Read every configured link; a failed read neither stops the loop nor
1509 /// gates the record body. C `transformRecord.c::process` (534-547) reads
1510 /// on through a failed `dbGetLink` and computes anyway.
1511 ReadAll,
1512 /// Read every configured link — a failure does NOT stop the loop, so the
1513 /// inputs behind it still refresh — but the body is skipped this cycle.
1514 ///
1515 /// C `calcRecord.c::fetch_values` (427-443) keeps the FIRST failing status
1516 /// while looping to the end (`if (status == 0) status = newStatus;`), and
1517 /// `calcRecord.c::process` (120) runs `calcPerform` only
1518 /// `if (fetch_values(prec) == 0)` — so VAL and UDF freeze, no CALC_ALARM is
1519 /// raised, and everything after the calc (timestamp, alarms, monitors,
1520 /// forward link) still runs. `calcoutRecord.c` (694-709 fetch, 237 gate) is
1521 /// the same shape, and its OOPT decision then runs against the frozen VAL.
1522 ReadAllGateOnFailure,
1523 /// Stop at the FIRST failed link and skip the record body this cycle.
1524 ///
1525 /// C `subRecord.c::fetch_values` (407-418) `return -1`s on the first
1526 /// failing `dbGetLink`, so the inputs behind it are never read and keep
1527 /// their previous values; `subRecord.c::process` (144 fetch, 147 gate)
1528 /// then runs `do_sub` only `if (status == 0)`, freezing VAL/UDF and raising
1529 /// none of the subroutine's alarms. `aSubRecord.c` (277-289 fetch, 216-218
1530 /// process), `sCalcoutRecord.c` (885-887 fetch, 356 gate),
1531 /// `aCalcoutRecord.c` (1068-1071 fetch, 399 gate) and
1532 /// `swaitRecord.c` (686-705 fetch, 408 gate) are the same shape.
1533 AbortOnFirstFailure,
1534 /// Read every configured link and gate the body on the LAST link's status.
1535 ///
1536 /// C `selRecord.c::fetch_values` (434-437) assigns `status` unguarded on
1537 /// every pass of its all-inputs loop and returns it, so what
1538 /// `selRecord.c::process` (114-116) gates `do_sel` on is INPL's status
1539 /// alone — a failed INPA is read, posted, and then ignored. In
1540 /// `Specified` mode the loop is one link (`:421-432`), so the same rule
1541 /// reads as "the selected input's status". Faithful to C, quirk included:
1542 /// see `sel`'s doc for why the quirk is C's and not ours.
1543 ReadAllGateOnLastFailure,
1544}
1545
1546/// **The** field declaration of a record type, and the only way to obtain one.
1547///
1548/// Not implementable: the blanket `impl` below covers every [`Record`], so a
1549/// second `impl FieldDeclaration for MyRecord` is a coherence error. A record
1550/// type therefore cannot *supply* a field list — it can only be *asked* for
1551/// one, and the answer is resolved here:
1552///
1553/// * a record type **base's** vendored `.dbd` set covers is declared by the
1554/// table generated from that `.dbd` ([`crate::server::record::dbd_generated::record_fields`]);
1555/// * any other record type is asked for its own declaration,
1556/// [`Record::declared_fields`] — which for the downstream record types is the
1557/// table generated from the `.dbd` *their* crate vendors, and for a synthetic
1558/// record type (tests) is a hand-written table.
1559///
1560/// The two are mutually exclusive by construction, which is what closes the
1561/// invariant *one declaration per record type*. It used to be closed by luck:
1562/// both tables were live, `field_desc_of` merely happened to consult the
1563/// generated one first, and every consumer that reached for `field_list()`
1564/// directly (`dbpr`, the `dbpf` typo hint, `motor`'s field gate) read the
1565/// hand-written one — which is how `waveform.FTVL` was declared `DBF_SHORT`
1566/// with no menu while `waveformRecord.dbd` said `DBF_MENU`/`menu(menuFtype)`.
1567/// `field`'s offset into a contiguous argument block whose members are the
1568/// single letters `A`..`A+nargs-1` behind `prefix` — C's `get_linkNumber`
1569/// index test (`idx >= indexof(A) && idx < indexof(A) + NARGS`) written as a
1570/// letter test, which is the same set because the `.dbd` declares those
1571/// fields contiguously in letter order (`calcRecord.dbd.pod:792-980`).
1572///
1573/// `prefix` is `""` for the plain arg letters, `"L"` for calc's previous-value
1574/// twins `LA`..`LU`, `"VAL"` for aSub's output slots `VALA`..`VALU`.
1575pub fn arg_letter_offset(field: &str, prefix: &str, nargs: u8) -> Option<u8> {
1576 let rest = field.strip_prefix(prefix)?;
1577 let &[c] = rest.as_bytes() else { return None };
1578 if !c.is_ascii_uppercase() {
1579 return None;
1580 }
1581 let n = c - b'A';
1582 (n < nargs).then_some(n)
1583}
1584
1585/// The `nargs`-wide link field named by [`arg_letter_offset`]'s answer:
1586/// offset 0 behind `"INP"` is `"INPA"`. The one place the port turns a C
1587/// `&prec->inpa + linkNumber` pointer walk into a field name.
1588pub fn arg_link_field(prefix: &str, offset: u8) -> String {
1589 format!("{prefix}{}", (b'A' + offset) as char)
1590}
1591
1592/// C `CALCPERFORM_NARGS` (`postfix.h:29`) and `subRecord.c:89`'s
1593/// `INP_ARG_MAX` — the same 21, which is why calc, calcout and sub share one
1594/// arg-link mapping.
1595pub const CALC_CLASS_NARGS: u8 = 21;
1596
1597/// The calc-class `get_linkNumber` mapping (`calcRecord.c:161-167`,
1598/// `calcoutRecord.c:417-423`, `subRecord.c:198-204`): the arg letters
1599/// `A`..`U` AND their previous-value twins `LA`..`LU` both index
1600/// `&prec->inpa + n`, so both answer their metadata from `INPn`.
1601///
1602/// `sel` names its args the same way and is deliberately NOT routed here: its
1603/// rset lists them explicitly on HOPR/LOPR and calls `dbGetGraphicLimits`
1604/// nowhere.
1605pub fn calc_class_link_backed_metadata_field(field: &str) -> Option<String> {
1606 arg_letter_offset(field, "", CALC_CLASS_NARGS)
1607 .or_else(|| arg_letter_offset(field, "L", CALC_CLASS_NARGS))
1608 .map(|n| arg_link_field("INP", n))
1609}
1610
1611pub trait FieldDeclaration {
1612 /// The record type's field descriptors, in `.dbd` declaration order.
1613 fn field_list(&self) -> &'static [FieldDesc];
1614
1615 /// The record-own `DBF_NOACCESS` internal names the generator dropped
1616 /// from [`FieldDeclaration::field_list`] (`BPTR`, `RPVT`, ...): names
1617 /// C's `dbNameToAddr` resolves — so a SEARCH is answered — but whose
1618 /// channel is refused at creation. Same base-table-then-own resolution
1619 /// as `field_list`.
1620 fn noaccess_names(&self) -> &'static [&'static str];
1621
1622 /// The channel's native (maximum) element count for `field`, when it
1623 /// differs from the count of the field's current value — C's `cvt_dbaddr`
1624 /// `paddr->no_elements` against `get_array_info`'s current valid length, so
1625 /// a client's `ca_element_count` is the capacity even though a GET returns
1626 /// fewer elements.
1627 ///
1628 /// Only a `special(SPC_DBADDR)` field reaches `cvt_dbaddr` in C, and the
1629 /// `.dbd` is what says which fields those are. This reads that from
1630 /// [`FieldDeclaration::field_list`] and only then asks the record for the
1631 /// number ([`Record::dbaddr_capacity`]) — so the population comes from the
1632 /// one declaration every consumer already goes through, and a record cannot
1633 /// advertise a capacity for a field C would never have handed one.
1634 fn field_native_count(&self, field: &str) -> Option<u32>;
1635
1636 /// C `paddr->pfldDes->special == SPC_DBADDR` — the field's STATIC `.dbd`
1637 /// declaration, and the single owner of "is this an ARRAY destination".
1638 ///
1639 /// `dbPut` reads exactly this to pick its array arm
1640 /// (`dbAccess.c:1350`, tag `R7.0.10`:
1641 /// `if (nRequest>1 || paddr->pfldDes->special == SPC_DBADDR)`), and it is
1642 /// `pfldDes` — the shared field descriptor — NOT the per-`DBADDR`
1643 /// `paddr->special` that `cvt_dbaddr` is free to overwrite. The two differ:
1644 /// `lsiRecord.c:127-134` raises `paddr->special` to `SPC_MOD` on VAL and
1645 /// `SPC_NOMOD` on OVAL, and the branch still takes the array arm.
1646 ///
1647 /// That distinction is why this cannot be one lookup here either.
1648 /// [`FieldDesc::special`] carries the `cvt_dbaddr` special for a DBADDR
1649 /// field, not the declaration (`dbd/cvt_dbaddr.types` says so, and derives
1650 /// `read_only` from it) — which preserves `SPC_DBADDR` for every such field
1651 /// except the five long-string ones, whose `cvt_dbaddr` overwrites it. Those
1652 /// five are exactly [`Record::long_string_fields`] (`lsi`/`lso` VAL+OVAL,
1653 /// `printf` VAL), so the second half asks the record for them.
1654 fn field_is_dbaddr(&self, field: &str) -> bool;
1655}
1656
1657impl<R: Record + ?Sized> FieldDeclaration for R {
1658 fn field_list(&self) -> &'static [FieldDesc] {
1659 super::dbd_generated::record_fields(self.record_type())
1660 .unwrap_or_else(|| self.declared_fields())
1661 }
1662
1663 fn noaccess_names(&self) -> &'static [&'static str] {
1664 super::dbd_generated::record_noaccess_fields(self.record_type())
1665 .unwrap_or_else(|| self.declared_noaccess_fields())
1666 }
1667
1668 fn field_native_count(&self, field: &str) -> Option<u32> {
1669 if !self.field_is_dbaddr(field) {
1670 return None;
1671 }
1672 self.dbaddr_capacity(field)
1673 }
1674
1675 fn field_is_dbaddr(&self, field: &str) -> bool {
1676 self.field_list()
1677 .iter()
1678 .any(|d| d.name.eq_ignore_ascii_case(field) && d.special == Special::DbAddr)
1679 || self
1680 .long_string_fields()
1681 .iter()
1682 .any(|f| f.eq_ignore_ascii_case(field))
1683 }
1684}
1685
1686/// Trait that all EPICS record types must implement.
1687pub trait Record: Send + Sync + 'static {
1688 /// Return the record type name (e.g., "ai", "ao", "bi").
1689 fn record_type(&self) -> &'static str;
1690
1691 /// Process the record (scan/compute cycle).
1692 ///
1693 /// Returns a `ProcessOutcome` containing the processing result and any
1694 /// side-effect actions for the framework to execute.
1695 fn process(&mut self) -> CaResult<ProcessOutcome> {
1696 Ok(ProcessOutcome::complete())
1697 }
1698
1699 /// Optional: report whether this record's last `process()` call
1700 /// mutated a metadata-class field (EGU/PREC/HOPR/LOPR/HLM/LLM/
1701 /// alarm limits / DRVH/DRVL / state strings).
1702 ///
1703 /// The framework checks this after every `process()` call and, if
1704 /// true, invalidates the record's metadata cache so the next
1705 /// snapshot rebuilds from the new values.
1706 ///
1707 /// Default: `false` — most records never touch metadata fields
1708 /// during processing. Override only when your record dynamically
1709 /// adjusts limits or unit strings (e.g., a motor that recomputes
1710 /// HLM/LLM after a hardware homing operation).
1711 ///
1712 /// Implementations should reset their internal flag after returning
1713 /// `true` so the next cycle starts clean.
1714 fn took_metadata_change(&mut self) -> bool {
1715 false
1716 }
1717
1718 /// Get a field value by name.
1719 fn get_field(&self, name: &str) -> Option<EpicsValue>;
1720
1721 /// Set a field value by name.
1722 fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()>;
1723
1724 /// The field declaration of a record type **base's** `.dbd` set does not
1725 /// cover — a record type that lives in another crate.
1726 ///
1727 /// C has exactly one declaration per record type: the `.dbd`, read at
1728 /// runtime. The port compiles the vendored `.dbd`s into a generated table,
1729 /// and [`FieldDeclaration::field_list`] — the single resolver every consumer
1730 /// goes through — serves base's
1731 /// [`dbd_generated`](crate::server::record::dbd_generated) table for every
1732 /// record type IT covers and **never falls through to here**. So for a
1733 /// base record type this method is unreachable: it cannot declare one of its
1734 /// fields a second time, whatever it writes here.
1735 ///
1736 /// A record type outside base declares itself here, and the answer is still
1737 /// its `.dbd`: the downstream Tier-3 record types (`motor`, `table`,
1738 /// `scaler`, `epid`, `throttle`, `timestamp`) vendor their upstream `.dbd`
1739 /// into their OWN crate, `tools/dbd-codegen` generates a table into that
1740 /// crate (`tools/dbd-codegen/src/targets.rs`), and this method returns it.
1741 /// Each such crate carries the same ratchet base does
1742 /// (`one_declaration_per_record_type`): the table returned here must BE the
1743 /// generated one, so the `.dbd` stays the single declaration across a crate
1744 /// boundary the generator's output cannot cross on its own.
1745 ///
1746 /// A hand-written table is what is left when a record type has no `.dbd`
1747 /// anywhere — the synthetic record types the tests define. There are no
1748 /// others; a shipped record type has a `.dbd`, and its declaration is that
1749 /// `.dbd`.
1750 ///
1751 /// The declaration is a *spec*: it says what each field's type, menu and
1752 /// `special(SPC_NOMOD)` are. It does NOT say who implements the field. Ask
1753 /// [`Record::implements_field`] for that — see its docs for why the two
1754 /// must not be the same question.
1755 fn declared_fields(&self) -> &'static [FieldDesc] {
1756 &[]
1757 }
1758
1759 /// The analogue of [`Record::declared_fields`] for
1760 /// [`FieldDeclaration::noaccess_names`]: the record-own `DBF_NOACCESS`
1761 /// internal names of a record type no base `.dbd` declares. A downstream
1762 /// crate whose generated table reports dropped internals returns its
1763 /// `record_noaccess_fields` entry here, next to its `declared_fields`.
1764 fn declared_noaccess_fields(&self) -> &'static [&'static str] {
1765 &[]
1766 }
1767
1768 /// Does this record type implement `name` in its own `get_field` /
1769 /// `put_field`, as opposed to leaving it to the framework's dbCommon
1770 /// handling?
1771 ///
1772 /// This used to be answered by `field_list()` membership, which conflated
1773 /// two questions: "what is this field?" and "who owns it?". They give
1774 /// different answers for `INP`/`OUT`: every record type *declares* them in
1775 /// the `.dbd`, but only some drive the link themselves
1776 /// (`multi_output_links` for `acalcout`/`scalcout`, device support for
1777 /// `motorRecord`/`scalerRecord`); for the rest the framework arms
1778 /// `parsed_inp`/`parsed_out` and drives it. While `field_list()` was
1779 /// hand-written and incomplete the conflation was invisible, because the
1780 /// hand-written tables happened to omit exactly the fields the framework
1781 /// owns. A complete, spec-derived `field_list()` makes membership true for
1782 /// every record, so ownership needs its own predicate or the framework
1783 /// would stop arming any link at all.
1784 ///
1785 /// The default answers it truthfully — a record implements the fields its
1786 /// own `get_field` can produce. (Verified equivalent to the old
1787 /// `field_list()` membership on all 1,757 fields of all 40 record types at
1788 /// the time of the split, so the split changed no behaviour.)
1789 fn implements_field(&self, name: &str) -> bool {
1790 self.get_field(name).is_some()
1791 }
1792
1793 /// `SPC_NOMOD` that a record's `cvt_dbaddr` decides **at runtime, from
1794 /// record state** — the dynamic half of the no-modify declaration.
1795 ///
1796 /// [`FieldDesc::read_only`] is the static half: it carries the `.dbd`
1797 /// `special(SPC_NOMOD)` of a field that is immutable for the record type,
1798 /// full stop. But C lets a record's `cvt_dbaddr` *raise* SPC_NOMOD per
1799 /// dbAddr, keyed on the record's own fields, and one record does:
1800 ///
1801 /// ```c
1802 /// /* compressRecord.c:395-407 */
1803 /// static long cvt_dbaddr(DBADDR *paddr) {
1804 /// ...
1805 /// if (prec->balg == bufferingALG_LIFO)
1806 /// paddr->special = SPC_NOMOD;
1807 /// }
1808 /// ```
1809 ///
1810 /// A compress VAL is writable under BALG=FIFO and refused under BALG=LIFO —
1811 /// a per-record-state fact no static `FieldDesc` can express. The one gate
1812 /// that owns field immutability (`field_io::check_no_mod`) consults this
1813 /// hook alongside the static set, so the dynamic refusal reaches EVERY put
1814 /// route exactly as the static one does.
1815 ///
1816 /// (C caches `paddr->special` in the DBADDR at name-resolution time, so a
1817 /// CA channel opened while FIFO keeps writing after a switch to LIFO until
1818 /// it reconnects; `dbpf`, which resolves fresh, is refused immediately. The
1819 /// port evaluates live on every put — the invariant C's own `dbpf` path
1820 /// shows, without the stale-cache hole.)
1821 ///
1822 /// `field` is upper-case. Default: no dynamic NOMOD.
1823 fn field_no_mod(&self, _field: &str) -> bool {
1824 false
1825 }
1826
1827 /// Choice strings for a record-specific `DBF_MENU` field served as
1828 /// `DBR_ENUM`, keyed by field name (uppercase, as declared).
1829 ///
1830 /// EPICS dbStaticLib serves a `DBF_MENU` field as `DBR_ENUM`: the value
1831 /// is the menu index and the field carries its `menu()` choice strings,
1832 /// so `caget`/`pvget` present the labels rather than a bare number
1833 /// (`dbStaticLib.c` `dbGetMenuChoices`; `dbAccess.c` `get_enum_str`).
1834 /// A record returns the label table (in index order) for each field it
1835 /// serves as [`DbFieldType::Enum`] from a `menu()`; the framework
1836 /// attaches it to the field snapshot's `EnumInfo` so the CA/PVA enum
1837 /// encoders present the labels — the same mechanism `bi`/`bo`/`mbbi`/
1838 /// `mbbo` already use for their `VAL` state strings, but per field
1839 /// rather than per record (a record can carry several distinct menus).
1840 ///
1841 /// This is the single owner of "menu field -> choice table": a record
1842 /// declares its menu fields here once, and `get_field` returns the menu
1843 /// index as [`EpicsValue::Enum`]. Default: no record-specific menu
1844 /// fields. The dbCommon menu fields (`SCAN`, etc.) are handled
1845 /// separately by the framework, not here.
1846 ///
1847 /// INVARIANT — answering here is a claim that the field is `DBF_MENU`, so
1848 /// [`FieldDeclaration::field_list`] MUST declare it [`DbFieldType::Enum`]: C's
1849 /// `mapDBFToDBR` serves every `DBF_MENU` as `DBR_ENUM`, and the DECLARED
1850 /// type is what goes on the wire
1851 /// ([`RecordInstance::project_to_declared_type`](super::RecordInstance::project_to_declared_type)).
1852 /// A field with choices but a `Short` declaration is a self-contradictory
1853 /// declaration: it would be served as a bare `DBR_SHORT` index while
1854 /// claiming to have labels for it. `menu_choices_are_served_as_dbr_enum`
1855 /// (`tests/menu_fields_serve_enum_choices.rs`) fails on any record type
1856 /// that breaks it — the storage may be a short, the declaration may not.
1857 fn menu_field_choices(&self, _field: &str) -> Option<&'static [&'static str]> {
1858 None
1859 }
1860
1861 /// Per-field override of the record-level display/control metadata
1862 /// for a GET / monitor snapshot of `field`.
1863 ///
1864 /// C record support serves metadata PER FIELD: the RSET functions
1865 /// `get_units` / `get_precision` / `get_graphic_double` /
1866 /// `get_control_double` / `get_alarm_double` all key on
1867 /// `dbGetFieldIndex(paddr)` and fall back to the `recGbl*` defaults
1868 /// for unlisted fields. The framework's metadata cache is per
1869 /// record (built by `populate_display_info` /
1870 /// `populate_control_info` from the VAL-class fields); a record
1871 /// whose RSET serves different metadata for non-VAL fields
1872 /// overrides this hook to patch the cached values for that field
1873 /// (e.g. the motor record: VELO's display range is VMAX/VBAS, not
1874 /// HLM/LLM — `motorRecord.cc:3247-3250`).
1875 ///
1876 /// Applied on both the GET path (`snapshot_for_field`) and the
1877 /// monitor path (`make_monitor_snapshot`), AFTER the cached
1878 /// record-level metadata — and computed live on each call, so an
1879 /// override derived from non-cached fields can never go stale.
1880 /// `field` is uppercase, as declared in [`FieldDeclaration::field_list`].
1881 /// Default: `None` — record-level metadata serves every field.
1882 fn field_metadata_override(&self, _field: &str) -> Option<FieldMetadataOverride> {
1883 None
1884 }
1885
1886 /// Which of THIS record's own link fields C's rset reads `field`'s
1887 /// `get_units` / `get_precision` / `get_graphic_double` /
1888 /// `get_alarm_double` from — C's `get_linkNumber` (`calcRecord.c:161-167`,
1889 /// `calcoutRecord.c:417-423`, `subRecord.c:198-204`), `get_dol`
1890 /// (`seqRecord.c:279-280`) and aSub's `get_inlinkNumber` /
1891 /// `get_outlinkNumber` pair (`aSubRecord.c:294-304`).
1892 ///
1893 /// Twenty-four call sites across five record types answer four of the six
1894 /// metadata slots from a LINK rather than from the record — the target's
1895 /// EGU, PREC, HOPR/LOPR and alarm ladder, not the source's. Control is
1896 /// deliberately absent: `dbGetControlLimits` has no caller in all of base,
1897 /// and `aSubRecord.c:372-376` is the type specimen calling
1898 /// `recGblGetControlDouble` with no link branch.
1899 ///
1900 /// The record type owns this answer for the same reason it owns
1901 /// [`Self::property_support`]: it is a transcription of the record's own C
1902 /// rset, and a central table keyed on the record-type *string* silently
1903 /// answers "no link" for every type nobody remembered to add. `aSub` was
1904 /// exactly that omission — its eight sites are the largest group of the
1905 /// twenty-four and it alone routes OUT links, a shape a
1906 /// `match rtype { "calc" | "calcout" | "sub" => ... }` cannot express at
1907 /// all.
1908 ///
1909 /// Returns the link field's name as declared (`"INPA"`, `"OUTC"`,
1910 /// `"DOL3"`), uppercase. Default: `None` — the record answers every field
1911 /// from itself.
1912 fn link_backed_metadata_field(&self, _field: &str) -> Option<String> {
1913 None
1914 }
1915
1916 /// Which of C's six nullable `rset` `get_*` property slots THIS record
1917 /// type implements — the record's own `#define get_xxx NULL` lines.
1918 ///
1919 /// `dbGet` consults the rset to *narrow* the caller's options mask
1920 /// (`dbAccess.c:336-427`): a NULL slot clears the option bit, so the leaf
1921 /// never reaches the client. That is what decides whether QSRV marks an NT
1922 /// leaf at all (pvxs `ioc/iocsource.cpp:263-305`). A slot counts as
1923 /// supplied when the C function pointer is non-NULL **even if the function
1924 /// writes nothing for the field in question** — C leaves the option bit
1925 /// set either way (`boRecord.c:294-299` writes units only for `HIGH`, yet
1926 /// `DBR_UNITS` survives for every `bo` field).
1927 ///
1928 /// The record type owns this answer because the record type owns its C
1929 /// rset. A central table keyed on the record-type *string* cannot: a
1930 /// record implemented in a downstream crate (`asyn-rs`'s `asynRecord`,
1931 /// the motor/scaler/optics types) has no way to add a row to it, so it
1932 /// silently inherited a default that marked every leaf it could name —
1933 /// telling clients a fabricated `display.units` of `""` and
1934 /// `valueAlarm` bands of zero were authoritative.
1935 ///
1936 /// The default answers from `default_property_support`, the
1937 /// transcription of the record types epics-base-rs implements itself.
1938 /// Override it in the record's own file, citing the C rset lines.
1939 fn property_support(&self) -> crate::server::snapshot::PropertySupport {
1940 default_property_support(self.record_type())
1941 }
1942
1943 /// Field names this record serves as a *long string*: a `DBF_CHAR`
1944 /// array field that semantically holds a NUL-terminated string.
1945 ///
1946 /// In EPICS such a field is declared `DBF_NOACCESS` (or carries a `$`
1947 /// modifier) and is accessed through a `DBR_CHAR` array view whose
1948 /// `form` is `"String"`; pvxs maps that view to a scalar `pvString`
1949 /// rather than an `int8[]` (`ioc/channel.cpp:58-68`,
1950 /// `ioc/iocsource.cpp:619-643`). QSRV uses this list to serve those
1951 /// fields as scalar-string NTScalar values instead of byte scalars.
1952 ///
1953 /// The record keeps its `CharArray` storage; the QSRV boundary does
1954 /// the `CharArray <-> String` conversion. Default empty — only
1955 /// long-string record types (`lsi`/`lso` VAL/OVAL, `printf` VAL)
1956 /// override this. Names are matched case-insensitively.
1957 fn long_string_fields(&self) -> &'static [&'static str] {
1958 &[]
1959 }
1960
1961 /// Field names declared `pp(TRUE)` in this record type's DBD (empty if
1962 /// none, e.g. `event`/`histogram`, or if the type is unmodeled).
1963 ///
1964 /// Drives the `dbPutField` processing gate: C `dbAccess.c:1263`
1965 /// re-processes a record on a put only when the put field is `PROC` or it
1966 /// is `pp(TRUE)` **and** `SCAN == Passive`. The table is total and
1967 /// fail-safe — an unmodeled type returns `&[]` (and warns once), so its
1968 /// field puts never auto-process (only `PROC` does). The default consults
1969 /// the central DBD-sourced table keyed by [`Record::record_type`]; record
1970 /// types can override.
1971 fn process_passive_fields(&self) -> &'static [&'static str] {
1972 super::process_passive::pp_fields_for(self.record_type())
1973 }
1974
1975 /// Whether a put to `field` should reprocess this Passive record.
1976 ///
1977 /// The default is pure `pp(TRUE)` membership — the put gate's
1978 /// `field in process_passive_fields()` test. A record type overrides this
1979 /// when its C `special()` conditionally returns ERROR to suppress the
1980 /// reprocess for a `pp(TRUE)` field on certain values (e.g. motor STUP:
1981 /// only a `STUP == ON` put runs the status-update process; any other value
1982 /// is clamped to OFF and C returns ERROR so no process runs). Modeling that
1983 /// here keeps the suppression at the same gate as the pp test, with no
1984 /// per-put one-shot state — the post-clamp field value is deterministic.
1985 fn processes_after_put(&self, field: &str) -> bool {
1986 self.process_passive_fields()
1987 .iter()
1988 .any(|f| f.eq_ignore_ascii_case(field))
1989 }
1990
1991 /// The record's `DBF_ENUM` state strings — the C rset slot pair
1992 /// `get_enum_strs` / `put_enum_str`, which in C read the same fields and
1993 /// are therefore ONE table here.
1994 ///
1995 /// It is what a client reads as the `DBR_ENUM` choice list AND the set of
1996 /// names a `DBR_STRING` put to the record's `DBF_ENUM` `VAL` may name
1997 /// (`dbConvert.c::putStringEnum` → [`crate::server::record::resolve_enum_state_string`]). Taking
1998 /// both from one table is the point: a name the record advertises is a name
1999 /// a client may put, by construction — the two cannot drift.
2000 ///
2001 /// The table is already trimmed to C's `no_str`: `bi`/`bo`/`busy` drop
2002 /// `ONAM` when `ZNAM` is set and `ONAM` is empty (`boRecord.c:342-352`);
2003 /// `mbbi`/`mbbo` cut at the last non-empty state (`mbbiRecord.c:262-269`).
2004 ///
2005 /// `None` — the record type leaves both rset slots NULL. That is every
2006 /// record whose `VAL` is not `DBF_ENUM`, and `mbbiDirect`/`mbboDirect`
2007 /// (`mbbiDirectRecord.c:58` `#define put_enum_str NULL`), whose `VAL` is
2008 /// `DBF_LONG`. C then fails a `DBR_STRING` put with `S_db_noRSET`.
2009 fn enum_state_strings(&self) -> Option<Vec<PvString>> {
2010 None
2011 }
2012
2013 /// The record's `get_enum_str` rset slot — how a `DBR_STRING` READ of its
2014 /// `DBF_ENUM` `VAL` renders. A THIRD slot, distinct from the pair above:
2015 /// C's `get_enum_str` (singular) is not `get_enum_strs` (plural), and
2016 /// serving the read from the plural table is what made an undefined `mbbi`
2017 /// state come out as its index.
2018 ///
2019 /// The difference is the trimming. `get_enum_strs` reports `no_str`, so the
2020 /// label list stops at the last non-empty state; `get_enum_str` indexes the
2021 /// state array *untrimmed* (`mbbiRecord.c:246-250`: any `val <= 15` reads
2022 /// `zrst + val * sizeof(zrst)`, empty or not) and only an index past the
2023 /// array reaches the sentinel. Verified on the compiled C `softIoc`: an
2024 /// `mbbi` with `ZRST`/`ONST` set answers `caget -t` with `""` at `VAL=5` and
2025 /// `"Illegal Value"` at `VAL=20`.
2026 ///
2027 /// `None` — the record leaves the rset slot NULL (`#define get_enum_str
2028 /// NULL`), which is every record but `bi`/`bo`/`mbbi`/`mbbo`. A field on
2029 /// such a record renders from its menu instead; see
2030 /// `RecordInstance::enum_string_form_for`,
2031 /// the single owner that picks between the two.
2032 fn enum_string_form(&self) -> Option<crate::server::snapshot::EnumStringForm> {
2033 None
2034 }
2035
2036 /// Validate a put before it is applied. Return Err to reject.
2037 fn validate_put(&self, _field: &str, _value: &EpicsValue) -> CaResult<()> {
2038 Ok(())
2039 }
2040
2041 /// Hook called after a successful put_field.
2042 fn on_put(&mut self, _field: &str) {}
2043
2044 /// Whether a put to `field` names a subroutine that must resolve in the
2045 /// function registry — C `special(SPC_MOD)` → `registryFunctionFind`
2046 /// (`aSubRecord.c::special`, `subRecord.c::special`). C stores the name in
2047 /// `dbPut`, then `special(after)` looks it up and returns `S_db_BadSub`
2048 /// (→ rsrv `ECA_PUTFAIL`) when the name is non-empty and unregistered, so
2049 /// the client's write is REFUSED while the field keeps the value it was
2050 /// given. An empty name names no routine and is accepted.
2051 ///
2052 /// The record's `special()` has no database handle and so cannot reach the
2053 /// registry itself (the registry is the DB's single owner); this hook only
2054 /// says "a put to `field` is a subroutine name that must be resolved". The
2055 /// put owner — which holds the registry — extracts the name from the write,
2056 /// performs the lookup, and applies the `S_db_BadSub` refusal at the point
2057 /// C's `dbPut` returns the after-put `special()` status. Returns `false`
2058 /// for any field/mode C accepts without a lookup (a non-SNAM field, or an
2059 /// aSub in `LFLG=READ` where the name comes from the SUBL link at process
2060 /// time and a SNAM put is not validated).
2061 fn is_subroutine_name_field(&self, _field: &str) -> bool {
2062 false
2063 }
2064
2065 /// Primary field name (default "VAL"). Override for waveform etc.
2066 fn primary_field(&self) -> &'static str {
2067 "VAL"
2068 }
2069
2070 /// Whether a put to `field` directly defines the record — clears UDF
2071 /// exactly like a primary-value-field put. C `dbAccess.c::dbPut`
2072 /// (`:1409-1410`) clears `udf` synchronously only when the put target is
2073 /// `dbIsValueField` (i.e. `field == primary_field()`); this hook is where
2074 /// a record type says its own `special()` ALSO clears UDF for a
2075 /// non-value field, independent of `dbIsValueField`.
2076 ///
2077 /// `mbboDirect` is the case this exists for: `mbboDirectRecord.c::special`
2078 /// (`after==1`, B0..B1F, line 290) sets `prec->udf = FALSE` on a bit-field
2079 /// put — the bit write is a second value source alongside a VAL put and
2080 /// the closed-loop DOL fetch. Default: only the primary field.
2081 fn is_udf_defining_put(&self, field: &str) -> bool {
2082 field == self.primary_field()
2083 }
2084
2085 /// Get the primary value.
2086 fn val(&self) -> Option<EpicsValue> {
2087 self.get_field(self.primary_field())
2088 }
2089
2090 /// Set the primary value.
2091 ///
2092 /// Matches C EPICS `dbPut` behavior: if the value type doesn't match
2093 /// the field type, it is automatically coerced (e.g., Long→Double for
2094 /// ai, Long→Enum for bi/mbbi). This prevents silent failures when
2095 /// asyn device support provides Int32 values to Enum-typed records.
2096 fn set_val(&mut self, value: EpicsValue) -> CaResult<()> {
2097 // Soft-channel INP/DOL delivery into the record's value field is
2098 // internal delivery, so it takes the same single owner every other
2099 // link target takes — `put_field_internal`. It was a parallel path
2100 // (put_field, then a `TypeMismatch`-triggered `convert_to` off the
2101 // *current* value's type), which silently dropped a shape the typed
2102 // arm rejected and `convert_to` could not fix: an array source into a
2103 // scalar VAL stayed an array and never landed. C's link layer asks for
2104 // one element (`dbGetLink(..., nRequest = NULL)`), so a waveform INP
2105 // into an `ai.VAL` delivers `wf[0]`.
2106 let field = self.primary_field();
2107 self.put_field_internal(field, value)
2108 }
2109
2110 /// Whether this record's `INP` is read by DEVICE SUPPORT (a C `DSET`), as
2111 /// opposed to by the record body itself.
2112 ///
2113 /// The init-time load of a CONSTANT `INP` into the record's value field is
2114 /// soft device support's `init_record` (`devAiSoft.c`, `devLonginSoft.c`,
2115 /// … each call `recGblInitConstantLink(&prec->inp, …)`), so it exists only
2116 /// where a DSET exists. `compress` has no device support at all
2117 /// (`compressRecord.c` declares no `dset`; its `process` calls `dbGetLink`
2118 /// on `INP` itself), which is why C leaves a `field(INP,"5")` compress with
2119 /// an EMPTY circular buffer — the constant is loaded nowhere and delivers
2120 /// nothing at process (`dbConstGetValue`). Seeding it anyway put a phantom
2121 /// sample in the buffer before the first scan.
2122 fn input_read_by_device_support(&self) -> bool {
2123 true
2124 }
2125
2126 /// The rest of the soft INPUT device support's `init_record`, once the
2127 /// constant-INP load above has (or has not) landed.
2128 ///
2129 /// Most soft dsets are exactly `recGblInitConstantLink()` and stop there —
2130 /// a link they could not load leaves the record's own init state alone. The
2131 /// ARRAY dsets do not: `devWfSoft.c:39-51` runs `dbLoadLinkArray` on every
2132 /// waveform and sets `prec->nord = 0` when it fails (a real link, or none —
2133 /// `dbLoadLinkArray` has no `loadArray` lset outside a constant), which is
2134 /// why C serves `NORD = 0` on a `record(waveform,"X"){}` even though the
2135 /// record's own `init_record` seeded `nord = (nelm == 1)` a moment earlier.
2136 ///
2137 /// `loaded` is whether a constant INP reached the value field. Defaulted to
2138 /// a no-op: a dset that only seeds does not need this half.
2139 fn soft_input_dset_init(&mut self, loaded: bool) {
2140 let _ = loaded;
2141 }
2142
2143 /// The plain `DTYP="Soft Channel"` INPUT dset body — C's `devXxxSoft.c`
2144 /// (and `devXxxSoftCallback.c`) `read_xxx`, handed the outcome of its own
2145 /// `dbGetLink(&prec->inp, ...)` once per process cycle.
2146 ///
2147 /// `Some(value)` is C's `if (!status)` arm, `None` its failure arm. Most
2148 /// soft dsets store what the link delivered and do nothing on failure,
2149 /// which is this default. `ai`'s two do not: `devAiSoft.c:81-93` and
2150 /// `devAiSoftCallback.c:180-194` blend the reading into the previous `VAL`
2151 /// through `SMOO` and keep their own "a read has completed" state, which
2152 /// they clear on a failed read so the next good one is unsmoothed.
2153 ///
2154 /// That filter belongs here and not in `process()`: `aiRecord.c:440-444`
2155 /// is the copy the RAW dset needs (`devAiSoftRaw::read_ai` returns 0, so
2156 /// `convert()` runs), while both soft dsets return 2 and `convert()` never
2157 /// runs for them at all.
2158 fn soft_input_read(&mut self, value: Option<EpicsValue>) -> CaResult<()> {
2159 match value {
2160 Some(value) => self.set_val(value),
2161 None => Ok(()),
2162 }
2163 }
2164
2165 /// The `DTYP="Raw Soft Channel"` INPUT dset — C's four `devXxxSoftRaw.c`
2166 /// read supports (`devAiSoftRaw`, `devBiSoftRaw`, `devMbbiSoftRaw`,
2167 /// `devMbbiDirectSoftRaw`).
2168 ///
2169 /// The value read from the INP link goes to **`RVAL`**, not `VAL`: the
2170 /// record's own `RVAL → VAL` convert then runs (that is the whole
2171 /// difference from `"Soft Channel"`, whose `read_xxx` returns 2 and writes
2172 /// `VAL` directly).
2173 ///
2174 /// **`Some`/`None` IS the dset table.** A record type that implements this
2175 /// is one for which C ships a SoftRaw input dset; a record type that does
2176 /// not, C has no such dset for, so `DTYP="Raw Soft Channel"` on it is a
2177 /// configuration C rejects at iocInit. There is no separate boolean saying
2178 /// whether the record "accepts" raw input — that boolean existed, defaulted
2179 /// to `false`, had ONE override in the workspace, and silently sent the
2180 /// other three input records' raw values into `VAL`, where their own convert
2181 /// then overwrote them from an unseeded `RVAL=0` (R19-66). A capability
2182 /// answer that can disagree with the implementation is the bug.
2183 fn raw_soft_input(&mut self, entry: RawSoftEntry, value: EpicsValue) -> Option<CaResult<()>> {
2184 let _ = (entry, value);
2185 None
2186 }
2187
2188 /// The `DTYP="Raw Soft Channel"` OUTPUT dset — C's four `devXxxSoftRaw.c`
2189 /// write supports: the value `write_xxx()` puts to the OUT link.
2190 ///
2191 /// `devAoSoftRaw.c` / `devBoSoftRaw.c` put `RVAL` (`dbPutLink(&prec->out,
2192 /// DBR_LONG, &prec->rval, 1)`); `devMbboSoftRaw.c` /
2193 /// `devMbboDirectSoftRaw.c` put `RVAL & MASK` as `DBR_ULONG`. All four
2194 /// write the RAW word — never the engineering `OVAL` that
2195 /// [`Record::output_link_value`] (the `"Soft Channel"` dset) puts.
2196 ///
2197 /// Same rule as [`Record::raw_soft_input`]: `Some`/`None` IS the dset
2198 /// table. Before this hook existed, a `DTYP="Raw Soft Channel"` output
2199 /// record matched neither the soft-OUT arm (which tests for `"Soft
2200 /// Channel"`) nor the device arm (it has no device), so it wrote **nothing
2201 /// at all** to OUT.
2202 fn raw_soft_output_value(&self) -> Option<EpicsValue> {
2203 None
2204 }
2205
2206 /// Apply a raw device value read *back* from an output record's device
2207 /// support (the asyn init seed and driver readback callback), the output
2208 /// counterpart of an input record's raw path, where device support
2209 /// writes `RVAL` and the record's own conversion produces `VAL`
2210 /// (`device_support.rs:43-46`). An output record whose
2211 /// `convert()` is forward (engineering → raw) must invert it here — store
2212 /// the raw value into `RVAL` and compute the engineering `VAL` — because
2213 /// the framework's forward convert would otherwise recompute `RVAL` from
2214 /// the stale `VAL` and discard the readback (C `processAo`/`initAo` set
2215 /// `rval`/`val` directly, devAsynInt32.c:955-957/:973-994).
2216 ///
2217 /// Returns `true` when the record fully produced `VAL` from the raw value
2218 /// (the asyn store path then reports `computed` so the forward convert is
2219 /// skipped). The default returns `false`: records whose own `convert()` is
2220 /// already `raw → eng` (`ai`) or that need no conversion (`longout`,
2221 /// `mbbo`, whose `set_val` re-derives from the raw value) keep the legacy
2222 /// raw → `RVAL` / direct-`VAL` path.
2223 fn apply_raw_readback(&mut self, _raw: i32) -> bool {
2224 false
2225 }
2226
2227 /// Apply a float64 device value read *back* from an output record's asyn
2228 /// device support — the `asynFloat64` analogue of
2229 /// [`Record::apply_raw_readback`]. A float64 output (`ao`) whose device
2230 /// value carries an `ASLO`/`AOFF` linear scaling must seed the engineering
2231 /// `VAL` here (`VAL = value * ASLO + AOFF`), because the asyn store path
2232 /// would otherwise write the raw device value straight into `VAL` and drop
2233 /// the scaling. Sets `VAL` only (a float64 `ao` carries no `RVAL`); the
2234 /// reverse scaling `(OVAL - AOFF) / ASLO` is applied on the device-write
2235 /// side. Mirrors C `initAo`/`processAo` (devAsynFloat64.c:628-630/:647-649).
2236 ///
2237 /// Returns `true` when the record produced `VAL` from the raw value (the
2238 /// asyn store path then reports `computed`, skipping the forward convert).
2239 /// The default returns `false`: records with no float64 readback scaling
2240 /// keep the raw `set_val` path.
2241 fn apply_float64_readback(&mut self, _raw: f64) -> bool {
2242 false
2243 }
2244
2245 /// Hand the record the database's breakpoint-table registry so an `ai`/`ao`
2246 /// with `LINR >= 3` can resolve and cache the table its `LINR` selects.
2247 /// Called once at iocInit, before the first `process`/`convert`. The record
2248 /// resolves the table lazily on the first conversion (and re-resolves when
2249 /// `LINR` changes at runtime), mirroring C `cvtRawToEngBpt`'s
2250 /// `init || *ppbrk == NULL` cache. The default is a no-op: only `ai`/`ao`
2251 /// carry `LINR`.
2252 fn install_breaktable_registry(
2253 &mut self,
2254 _registry: std::sync::Arc<crate::server::cvt_bpt::BreakTableRegistry>,
2255 ) {
2256 }
2257
2258 /// Apply IVOA=2 ("set outputs to IVOV") semantics. C line numbers below
2259 /// resolve at epics-base `R7.0.10`; `busy` is module `busy` at
2260 /// `R1-7-4-6-g2dfe92d`.
2261 ///
2262 /// **The rule for every record that has a raw/staged output word is
2263 /// `VAL = IVOV`, then the record's OWN conversion — never `RVAL = IVOV`
2264 /// or `OVAL = IVOV`.** IVOV is a value in VAL's engineering units, so it
2265 /// has to travel the same VAL->raw path an ordinary cycle uses:
2266 ///
2267 /// - `ao` (`aoRecord.c:207-212`): `val = ivov; value = ivov;
2268 /// convert(prec, value);` — OROC, linearisation and raw rounding all
2269 /// run, so OVAL *and* RVAL are the converted IVOV.
2270 /// - `bo` (`boRecord.c:230-238`), `busy` (`busyRecord.c:235-243`):
2271 /// `val = ivov;` then the inline `/* convert val to rval */` MASK rule.
2272 /// - `mbbo` (`mbboRecord.c:232-236`), `mbboDirect`
2273 /// (`mbboDirectRecord.c:210-214`): `val = ivov; convert(prec);` — the
2274 /// ZRVL..FFVL state-value lookup (mbbo only) and the SHFT shift.
2275 ///
2276 /// Records with no raw word assign directly and there is nothing to
2277 /// convert:
2278 ///
2279 /// - `calcout` (`calcoutRecord.c:646-647`), `scalcout`, `acalcout`:
2280 /// `oval = ivov` — and inside the `doOutput`-gated `execOutput`, so a
2281 /// non-output INVALID cycle must not touch OVAL at all. VAL is the calc
2282 /// result, not the output.
2283 /// - `lso` (`lsoRecord.c:131-138`): `strncpy(val, ivov, sizv-1)` and
2284 /// `len = strlen(val)+1`. OVAL is `monitor()`'s tracker here, not
2285 /// output staging, and seeding it suppresses the write's own post.
2286 /// - `dfanout` (`dfanoutRecord.c:137-139`): `val = ivov;` then
2287 /// `push_values(prec)`.
2288 ///
2289 /// Default uses [`Record::set_val`], which is the `dfanout` shape and is
2290 /// correct for any record whose OUT path reads VAL only.
2291 fn apply_invalid_output_value(&mut self, ivov: EpicsValue) -> CaResult<()> {
2292 self.set_val(ivov)
2293 }
2294
2295 /// Whether this record type supports device write (output records only).
2296 /// `aao` is included here even though it's served by the same
2297 /// concrete struct as `waveform`/`aai`/`subArray` — the
2298 /// WaveformRecord's `can_device_write` override picks the right
2299 /// answer per [`crate::server::records::waveform::ArrayKind`], but this default matters for code that
2300 /// only has the record-type string.
2301 fn can_device_write(&self) -> bool {
2302 matches!(
2303 self.record_type(),
2304 "ao" | "bo"
2305 | "longout"
2306 | "int64out"
2307 | "mbbo"
2308 | "mbboDirect"
2309 | "stringout"
2310 | "lso"
2311 | "printf"
2312 | "aao"
2313 )
2314 }
2315
2316 /// Has this cycle REACHED the record's `recGblFwdLink` line?
2317 ///
2318 /// C `if (!pact && prec->pact) return(0)` — device support took the write
2319 /// asynchronously, so `process()` returns before the tail and the client's
2320 /// `ca_put_callback` is still owed. A record that returns
2321 /// `AsyncPendingNotify` answers `false` until its device round-trip is
2322 /// done. Default: true, the synchronous record that runs to its own tail.
2323 ///
2324 /// One of the two inputs to `complete_put_notify`; see
2325 /// [`Self::should_fire_forward_link`] for the other and for the invariant
2326 /// they share.
2327 fn is_put_complete(&self) -> bool {
2328 true
2329 }
2330
2331 /// Does this cycle TAKE the record's `recGblFwdLink` line, having reached
2332 /// it?
2333 ///
2334 /// # Invariant (CONTRACT)
2335 ///
2336 /// A cycle completes an outstanding put-notify if and only if it runs
2337 /// `recGblFwdLink`. C `recGbl.c:290-303` is why: `if (pdbc->ppn)
2338 /// dbNotifyCompletion(pdbc)` sits INSIDE the function a record type
2339 /// chooses whether to call, so declining the forward link withholds the
2340 /// client's `ca_put_callback` by construction. The two are one decision in
2341 /// C and must stay one here — mca's own source says so where it makes the
2342 /// choice: "Process forward-linked record. Tell EPICS dbPutNotify
2343 /// mechanism that processing is finished." (`mcaRecord.c:820-826`).
2344 ///
2345 /// The single owner of that decision is
2346 /// `database::processing::complete_put_notify`, which reads this method
2347 /// and [`Self::is_put_complete`] together. A record type states its C gate
2348 /// HERE and nowhere else; it must not carry a second, separately-drifting
2349 /// `is_put_complete` override to say the same thing.
2350 ///
2351 /// Six types gate the call, each transcribing one C line:
2352 ///
2353 /// - `busy` (`busyRecord.c:271`): `if ((prec->val == 0) || (prec->oval ==
2354 /// 0)) recGblFwdLink(prec);`
2355 /// - `motor` (`motorRecord.cc:1509`): `if (pmr->dmov != 0)`
2356 /// - `mca` (`mcaRecord.c:824`): `if (!pmca->acqg)`
2357 /// - `scaler` (`scalerRecord.c:475`): `if ((pscal->pcnt==0) && (pscal->us
2358 /// == USER_STATE_IDLE))` inside the `ss == SCALER_STATE_IDLE` arm
2359 /// - `epid` (`epidRecord.c:201`): the UDF-gated `return(0)` above the
2360 /// `:212` call
2361 /// - `throttle` (`throttleRecord.c:308`): the call is commented out in
2362 /// `process()` and fired only from `valuePut`'s real-OUT-write branch
2363 /// (`:582`)
2364 ///
2365 /// Default: true — the standard record, which calls it unconditionally.
2366 fn should_fire_forward_link(&self) -> bool {
2367 true
2368 }
2369
2370 /// C parity: a record whose completion restamps `TIME` AFTER the VAL
2371 /// monitor post and the forward link, not before the value post like
2372 /// every standard record (`aoRecord.c:190` stamps ahead of `writeValue`).
2373 ///
2374 /// Only sseq's `asyncFinish` (`sseqRecord.c`) has this ordering: it posts
2375 /// VAL at `:474`, runs `recGblFwdLink` at `:499`, and only then calls
2376 /// `recGblGetTimeStamp` at `:501`. So the first VAL monitor event carries
2377 /// the record's pre-update timestamp, and `TIME` advances only for the
2378 /// following BUSY post and the next cycle — the VAL timestamp always lags
2379 /// one completion behind. The framework's synchronous `Complete` tail
2380 /// consults this to skip the pre-output restamp and apply it after the
2381 /// forward-link tail instead. Default false: every other record (including
2382 /// the base `seq`, whose `seqRecord.c:224` restamps BEFORE the `:229` VAL
2383 /// post) stamps before the value post.
2384 fn restamps_time_after_completion(&self) -> bool {
2385 false
2386 }
2387
2388 /// Whether this record's OUT link should be written after processing.
2389 /// Defaults to true. Override in calcout / longout to implement OOPT
2390 /// conditional output (epics-base 7.0.8).
2391 fn should_output(&self) -> bool {
2392 true
2393 }
2394
2395 /// The epilogue of C's `conditional_write` — where the record advances
2396 /// the reference the NEXT cycle's [`Self::should_output`] compares
2397 /// against (`longout.pval`, and the `outpvt` first-cycle bit with it).
2398 ///
2399 /// `longoutRecord.c:489-493` puts `prec->pval = prec->val;
2400 /// prec->outpvt = DONT_EXEC_OUTPUT;` OUTSIDE `if (doDevSupWrite)`, so a
2401 /// cycle that OOPT suppressed still advances. That unconditionality is
2402 /// the whole transition mechanism: `Transition_To_Zero` fires on the
2403 /// cycle where `val` reaches 0 and `pval` does not, which can only
2404 /// happen if the earlier nonzero cycle — which wrote nothing — latched.
2405 ///
2406 /// The framework therefore calls this once per cycle that reaches
2407 /// `conditional_write`, not once per write: C skips it only where
2408 /// `writeValue` returns before the switch (SIMM simulation, a failed
2409 /// SIML read) or where IVOA vetoes the call outright. Default: no-op —
2410 /// `calcout`/`sCalcout`/`aCalcout`/`swait` latch inside their own
2411 /// `process()`, as their C does.
2412 fn after_output_decision(&mut self) {}
2413
2414 /// Whether this record uses MDEL/ADEL deadband for monitor posting.
2415 /// Binary records (bi, bo, busy, mbbi, mbbo) return false because
2416 /// C EPICS always posts monitors for these record types regardless
2417 /// of whether the value changed.
2418 fn uses_monitor_deadband(&self) -> bool {
2419 true
2420 }
2421
2422 /// Whether this record's process cycle posts its primary value (`VAL`)
2423 /// as a value monitor (`DBE_VALUE` / `DBE_LOG`).
2424 ///
2425 /// Default `true`: for most records C `monitor()` posts `VAL` whenever the
2426 /// value moved (deadband, change-gate, or always).
2427 ///
2428 /// `false` for the "trigger" records `fanout` and `seq`. Their `VAL` is
2429 /// `field(VAL,DBF_LONG){ pp(TRUE) }` — "Used to trigger" — and their C
2430 /// `process()` posts `VAL` ONLY with the alarm events `recGblResetAlarms`
2431 /// returns: `if (events) db_post_events(prec, &prec->val, events)`
2432 /// (fanoutRecord.c:148-150, seqRecord.c:227-229), never `DBE_VALUE` /
2433 /// `DBE_LOG`. Writing `VAL` fans out the forward links / sequences the
2434 /// `DOn`→`LNKn` writes; the value itself is not a monitored quantity, so a
2435 /// run of `caput VAL` posts no per-put value event (only the initial
2436 /// subscription snapshot fires). The alarm bits still reach `VAL` through
2437 /// the deadband post's `alarm_bits`, so an alarm transition posts `VAL`
2438 /// with `DBE_ALARM` exactly as C's `if (events)` does.
2439 fn process_posts_value_monitor(&self) -> bool {
2440 true
2441 }
2442
2443 /// Per-record VALUE/LOG monitor gate for record types that post a
2444 /// monitor *only when the value actually changed* — and have no
2445 /// MDEL/ADEL deadband to express that.
2446 ///
2447 /// `Some(changed)` makes the framework post the VALUE and LOG
2448 /// monitors iff `changed`; `None` (the default) leaves the decision
2449 /// to the deadband / always-post path.
2450 ///
2451 /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` raise `DBE_VALUE |
2452 /// DBE_LOG` only when `len != olen || memcmp(oval, val, len)`. Those
2453 /// records return [`Self::uses_monitor_deadband`]`== false`, which
2454 /// otherwise routes them to the unconditional always-post path
2455 /// (correct for binary records, wrong for lsi/lso).
2456 ///
2457 /// **THIS HOOK IS C's `monitor()`.** It takes `&mut self` because that is
2458 /// where C both compares and commits: `if (prec->mlst != prec->val) {
2459 /// events |= DBE_VALUE | DBE_LOG; prec->mlst = prec->val; }`
2460 /// (`boRecord.c:395-400`) and its `oval`/`olen` twin
2461 /// (`lsoRecord.c:248-252`). The implementation compares LIVE and commits
2462 /// its own previous-value tracker here — it must NOT capture a flag during
2463 /// `process()`.
2464 ///
2465 /// The distinction is not cosmetic. A record whose value can still change
2466 /// between `process()` returning and the monitors being posted — every
2467 /// record with an `IVOA = Set_output_to_IVOV` arm, since the port hoists
2468 /// that decision into the framework's single IVOA owner where C keeps it
2469 /// inside `process()` — reports a flag captured against the PRE-arm value
2470 /// if it captures early, so the post for the IVOV write lands a cycle late
2471 /// or, if the arm also seeds the tracker, never. Comparing here is the
2472 /// uniform rule that removes the ordering from each record's concern.
2473 ///
2474 /// Called exactly once per completed cycle, from
2475 /// `RecordInstance::value_include_classes`, which
2476 /// the three cycle owners — the sync epilogue, the async completion, and
2477 /// the SIMM tail — reach on mutually exclusive paths.
2478 fn monitor_value_changed(&mut self) -> Option<bool> {
2479 None
2480 }
2481
2482 /// `menuPost` "Always" override for the VALUE / LOG monitor masks.
2483 ///
2484 /// Returns `(post_value_always, post_archive_always)`. The framework
2485 /// ORs these into the change-gated mask from
2486 /// [`Self::monitor_value_changed`], so an *unchanged* process cycle
2487 /// still posts `DBE_VALUE` (resp. `DBE_LOG`) when the record's MPST
2488 /// (resp. APST) menu field is set to `Always`.
2489 ///
2490 /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` compute the VAL post
2491 /// mask from three independent inputs:
2492 ///
2493 /// * the change test `len != olen || memcmp(oval, val, len)` →
2494 /// `DBE_VALUE | DBE_LOG`,
2495 /// * `if (mpst == menuPost_Always) events |= DBE_VALUE;`,
2496 /// * `if (apst == menuPost_Always) events |= DBE_LOG;`.
2497 ///
2498 /// [`Self::monitor_value_changed`] carries the first input; this hook
2499 /// carries the other two. Records without a `menuPost` field keep the
2500 /// default `(false, false)`, which leaves the change gate unchanged.
2501 fn monitor_always_post(&self) -> (bool, bool) {
2502 (false, false)
2503 }
2504
2505 /// The value the MDEL/ADEL deadband is evaluated against.
2506 ///
2507 /// For most records C `monitor()` applies the value deadband to
2508 /// `VAL`, so the default is [`Self::val`]. A record whose monitored
2509 /// quantity is not its primary value must override this: the motor
2510 /// record, for instance, has `VAL` as the setpoint and applies
2511 /// MDEL/ADEL to `RBV` (the readback) — its C `monitor()` deadbands
2512 /// `RBV`, not `VAL`. Such a record returns its readback field here.
2513 ///
2514 /// Default is `val()`, so existing records are unaffected.
2515 fn monitor_deadband_value(&self) -> Option<EpicsValue> {
2516 self.val()
2517 }
2518
2519 /// The FIELD whose VALUE/LOG monitor delivery the MDEL/ADEL
2520 /// deadband gates — the field [`Self::monitor_deadband_value`]
2521 /// reads. A record overriding one must override both consistently.
2522 ///
2523 /// For most records the deadband gates the primary value itself,
2524 /// so the default returns [`Self::primary_field`] and nothing
2525 /// changes. The motor record deadbands RBV: C `monitor()`
2526 /// (motorRecord.cc:3468-3507) throttles the RBV post with
2527 /// MDEL/ADEL, while VAL is posted only when an actual setpoint
2528 /// change marked it (M_VAL). When this returns a non-primary
2529 /// field, the framework's snapshot builders:
2530 ///
2531 /// * deliver THIS field on the deadband triggers (instead of raw
2532 /// change-detection), and
2533 /// * route the primary field through generic change-detection, so
2534 /// an unchanged setpoint is not re-posted on every readback
2535 /// poll.
2536 fn monitor_deadband_field(&self) -> &'static str {
2537 self.primary_field()
2538 }
2539
2540 /// Fields the record's C `monitor()` posts on every cycle whose
2541 /// alarm transition fired, even when their value did not change.
2542 ///
2543 /// C motorRecord.cc `monitor()` (3456-3646) computes
2544 /// `local_mask = monitor_mask | (MARKED(x) ? DBE_VAL_LOG : 0)`
2545 /// for each field in its posting list — when the alarm moved
2546 /// (`monitor_mask != 0`), `local_mask` is non-zero for UNMARKED
2547 /// fields too, so every listed field posts with `DBE_ALARM` and a
2548 /// `DBE_ALARM`-only subscriber observes the alarm moment on any of
2549 /// them. The framework's change-detection loop posts a listed,
2550 /// subscribed, unchanged field with the cycle's alarm bits when
2551 /// this list names it.
2552 ///
2553 /// Default: empty — most C record types post only their value
2554 /// field(s) on an alarm transition (aiRecord.c `monitor()` posts
2555 /// VAL with `monitor_mask` and RVAL only when it changed), which
2556 /// the deadband-field post already covers.
2557 fn alarm_cycle_monitored_fields(&self) -> &'static [&'static str] {
2558 &[]
2559 }
2560
2561 /// Fields the record's C `monitor()` re-posts with `DBE_VAL_LOG` on
2562 /// every cycle that recomputed them, even when the value did not
2563 /// change — the analogue of an unconditional `MARK(field)` in C.
2564 ///
2565 /// Unlike [`Self::alarm_cycle_monitored_fields`] (which posts unchanged
2566 /// fields only on a cycle whose alarm transition fired), these post on
2567 /// any cycle the record names them, with `DBE_VALUE | DBE_LOG` (plus the
2568 /// cycle's alarm bits when one fired). The framework's change-detection
2569 /// loop posts a listed, subscribed, unchanged field with that mask.
2570 ///
2571 /// C motorRecord `process_motor_info` (motorRecord.cc:3764-3767)
2572 /// `MARK`s `M_DIFF`/`M_RDIF` unconditionally on every `CALLBACK_DATA`
2573 /// pass, and `monitor()` (3522-3531) posts them with `monitor_mask |
2574 /// DBE_VAL_LOG`; a `camonitor DIFF` on an axis parked at a constant
2575 /// non-zero following error thus gets an event every poll. The record
2576 /// returns the fields ONLY on the cycles it actually re-marked them (it
2577 /// reads its own per-cycle state), so a pass that did not recompute them
2578 /// does not over-post.
2579 ///
2580 /// Default: empty — most record types post a field only when it
2581 /// changed (or on an alarm transition), which the existing gates cover.
2582 fn force_posted_fields(&self) -> &'static [&'static str] {
2583 &[]
2584 }
2585
2586 /// Fields this cycle's C `monitor()` posted UNCONDITIONALLY, chosen per
2587 /// cycle from record state — the DYNAMIC sibling of
2588 /// [`Self::force_posted_fields`].
2589 ///
2590 /// Some records decide which fields to post from a per-cycle BIT MASK
2591 /// rather than from a fixed list. aCalcout has two of them, and neither
2592 /// consults the value: `afterCalc` posts exactly the AMASK-flagged array
2593 /// fields — the ones the expression STORED into
2594 /// (`aCalcoutRecord.c:294-298`) — and `monitor()` posts exactly the
2595 /// NEWM-flagged ones — the input arrays whose link delivered a CHANGED
2596 /// value (`:1031-1036`). `AA := AA` therefore still posts AA.
2597 ///
2598 /// Neither existing gate can express that. The change-detection loop posts
2599 /// only what moved, so it drops the store-the-same-value case;
2600 /// [`Self::force_posted_fields`] is `&'static`, so it cannot name a set
2601 /// that varies per cycle (twelve arrays, 2^12 combinations) without
2602 /// over-posting every one of them every cycle.
2603 ///
2604 /// Each entry is ONE C `db_post_events` call, with the mask THAT call site
2605 /// uses ([`CyclePostMask`]) — not one entry per field. The two aCalcout call
2606 /// sites disagree on the mask (`afterCalc` posts a literal `DBE_VALUE|
2607 /// DBE_LOG`, `monitor()` posts `monitor_mask|DBE_VALUE|DBE_LOG`), and an
2608 /// array in BOTH masks is posted TWICE by C, once from each. So a field may
2609 /// legitimately appear twice, and the framework emits an event per entry.
2610 ///
2611 /// TAKE semantics: called exactly once per process cycle, and the record
2612 /// clears whatever state it answered from — C's `pcalc->newm = 0` (`:1036`)
2613 /// is part of the same step.
2614 ///
2615 /// Default: empty — and `Vec::new()` does not allocate.
2616 fn take_cycle_posted_fields(&mut self) -> Vec<(&'static str, CyclePostMask)> {
2617 Vec::new()
2618 }
2619
2620 /// Fields this record posts on its FIRST monitor cycle whether or not they
2621 /// changed — C's `|| (prpvt->firstCalcPosted == 0)` term
2622 /// (`transformRecord.c:798`), which `monitor()` disarms unconditionally
2623 /// afterwards (`:807`) so it fires once per IOC lifetime.
2624 ///
2625 /// Deliberately NOT [`Self::take_cycle_posted_fields`], which carries marks
2626 /// the RUNNING cycle made and which iocInit drops wholesale
2627 /// (`seed_record_after_init`, so a seed put's mark cannot become a late
2628 /// event). This flag is initial record state, made by no put, so it has to
2629 /// survive the seed — putting it on the per-cycle channel would let the
2630 /// init drain consume it and the first cycle would post nothing.
2631 ///
2632 /// Same TAKE semantics and same masks: called once per process cycle, and
2633 /// the record clears what it answered from.
2634 ///
2635 /// Default: empty.
2636 fn take_first_monitor_cycle(&mut self) -> Vec<(&'static str, CyclePostMask)> {
2637 Vec::new()
2638 }
2639
2640 /// Fields whose ONLY post path is the record's own per-cycle mark — C never
2641 /// change-detects them, so a value change alone must post nothing.
2642 ///
2643 /// aCalcout's arrays AA..LL are the case. C's `monitor()` compares scalar
2644 /// A..L against their PA..PL previous values (`aCalcoutRecord.c:1024-1029`)
2645 /// and OVAL against POVL (`:1039`), but it keeps NO previous copy of an
2646 /// array and runs no array comparison anywhere: an array posts if and only
2647 /// if the expression stored into it (AMASK, `afterCalc` `:294-298`) or its
2648 /// input link delivered a changed value (NEWM, `:1031-1036`) — both reported
2649 /// by [`Self::take_cycle_posted_fields`].
2650 ///
2651 /// So the change-detection arm must not see these fields at all. It is not
2652 /// merely redundant with the marks: a client `caput` to AA posts the put's
2653 /// value without advancing the subscriber's `last_posted`, and the next
2654 /// process — which stored nothing into AA and fetched nothing into it —
2655 /// then found AA "changed" and emitted a post C has no counterpart for.
2656 ///
2657 /// Default: empty — every other record's auxiliary fields post on change.
2658 fn fields_posted_only_when_marked(&self) -> &'static [&'static str] {
2659 &[]
2660 }
2661
2662 /// Fields the record's C `monitor()` re-posts with `DBE_LOG` ONLY on
2663 /// every cycle it names them, regardless of change — the analogue of
2664 /// an unconditional `db_post_events(field, DBE_LOG)` sweep.
2665 ///
2666 /// Distinct from [`Self::force_posted_fields`], which posts with
2667 /// `DBE_VALUE | DBE_LOG`: these post with `DBE_LOG` alone, so only a
2668 /// `DBE_LOG` (archiver) subscriber receives the event.
2669 ///
2670 /// The sweep is an INDEPENDENT post, NOT an alternative to the
2671 /// change-detected post. C's `db_post_events` calls compose: on the
2672 /// scaler's count-completion cycle `updateCounts()` posts each changed
2673 /// `Sn` with `DBE_VALUE` (scalerRecord.c:582) and then `monitor()` —
2674 /// reached because the done-interrupt set `ss = IDLE` (`:367`,
2675 /// `:510`) — posts the SAME `Sn` again with a literal `DBE_LOG`
2676 /// (`:757-773`). Two events, one field, one cycle. So the framework
2677 /// emits the sweep post in addition to whatever the change detection
2678 /// produced; gating it on "did not change" would silently drop the
2679 /// `DBE_LOG` half on exactly the cycle that carries the final counts.
2680 ///
2681 /// For a field that is ALSO a [`Self::value_only_change_fields`]
2682 /// member (scaler `Sn`) the change post carries `DBE_VALUE` only, so
2683 /// this sweep is the sole source of its `DBE_LOG` events, matching C.
2684 ///
2685 /// The record returns the names ONLY on the cycles whose C `monitor()`
2686 /// runs — the scaler reads its own `ss` state and returns `S1..Snch`
2687 /// while idle, empty while counting (a counting cycle never reaches C
2688 /// `monitor()`).
2689 ///
2690 /// The sweep post carries `DBE_LOG` plus the cycle's ALARM-transition bits
2691 /// (`recGblResetAlarms`). C's scaler computes that mask and then posts with a
2692 /// literal `DBE_LOG`, dropping the alarm bit — CBUG-B19, a deliberate
2693 /// deviation; see the post site in `collect_subscriber_posts`.
2694 ///
2695 /// Default: empty — most record types have no LOG-only sweep.
2696 fn log_swept_fields(&self) -> &'static [&'static str] {
2697 &[]
2698 }
2699
2700 /// Fields whose change-detected monitor post must carry `DBE_VALUE`
2701 /// only — the LOG bit is stripped — instead of the framework default
2702 /// `DBE_VALUE | DBE_LOG`.
2703 ///
2704 /// The generic change-detection post (and the deadband post for a
2705 /// deadband field named here) normally bundles `DBE_LOG` so an
2706 /// archiver subscribed `DBE_LOG` sees every value change. A record
2707 /// whose C `db_post_events` calls pass a literal `DBE_VALUE` for
2708 /// these fields names them here so the framework drops the LOG bit;
2709 /// the cycle's alarm bits are still OR'd in (alarm posting is a
2710 /// separate per-field contract, unaffected by this hook).
2711 ///
2712 /// C `scalerRecord.c` posts CNT/T/VAL/PR1/TP/FREQ and each active
2713 /// channel `S1..Snch` with a literal `DBE_VALUE` on a value change
2714 /// (scalerRecord.c:372,478,582,588 et al.); `DBE_LOG` appears ONLY in
2715 /// the idle `monitor()` sweep ([`Self::log_swept_fields`],
2716 /// scalerRecord.c:771). The two hooks are complementary: a `DBE_LOG`
2717 /// subscriber on `Sn` is served by the idle sweep, never by a
2718 /// counting-cycle value change — matching C.
2719 ///
2720 /// Default: empty — most record types post changes with
2721 /// `DBE_VALUE | DBE_LOG` (C `monitor_mask | DBE_VALUE | DBE_LOG`,
2722 /// calcRecord.c:420, subRecord.c:400).
2723 fn value_only_change_fields(&self) -> &'static [&'static str] {
2724 &[]
2725 }
2726
2727 /// Secondary value fields a record posts with the *primary VAL
2728 /// monitor mask*, from INSIDE the same guard C wraps its VAL post in —
2729 /// never with a forced `DBE_VALUE | DBE_LOG` on every change.
2730 ///
2731 /// Mirrors C records that drive a raw secondary field with the shared
2732 /// `monitor_mask` rather than `monitor_mask | DBE_VALUE | DBE_LOG`. Each
2733 /// entry pairs the field with the gate C applies to it *inside* that
2734 /// guard — see [`ValuePostGate`], which is the whole reason this is a
2735 /// pair and not a bare name: `ai` re-tests the raw value
2736 /// (`if (prec->oraw != prec->rval)`) while `timestamp` does not.
2737 ///
2738 /// Distinct from the default change-detected aux post (which carries
2739 /// `DBE_VALUE | DBE_LOG` unconditionally): ao `RVAL`/`RBV`, mbbo/
2740 /// mbboDirect/mbbiDirect `RVAL`/`RBV`, sel `SELN` and compress `NUSE`
2741 /// are all posted by C with the `DBE_VALUE | DBE_LOG`-forced mask, so
2742 /// they stay on the default path and must NOT be named here.
2743 ///
2744 /// Default: empty.
2745 fn fields_posted_with_value_mask(&self) -> &'static [(&'static str, ValuePostGate)] {
2746 &[]
2747 }
2748
2749 /// Per-cycle widening of the guard [`ValuePostGate::OnChangeForced`]
2750 /// fields sit behind — C `if (prec->omod) monitor_mask |=
2751 /// (DBE_VALUE|DBE_LOG);` (aoRecord.c:535), which opens the secondary block
2752 /// on a cycle where VAL's own monitor mask is empty.
2753 ///
2754 /// ao needs it because `omod` tracks OVAL, not VAL: OROC can walk the
2755 /// output one step per cycle toward a VAL that has not moved since MLST,
2756 /// so C posts OVAL/RVAL on cycles that post no VAL at all.
2757 ///
2758 /// Clear-on-read, because C's is: `omod` can only ever be true on a cycle
2759 /// whose guard therefore fires, and the guard's first act is
2760 /// `prec->omod = FALSE` (`:537`).
2761 ///
2762 /// Default: empty — no widening, so the guard is VAL's own mask.
2763 fn take_secondary_value_mask(&mut self) -> crate::server::recgbl::EventMask {
2764 crate::server::recgbl::EventMask::NONE
2765 }
2766
2767 /// C's whole `if (prec->oraw != prec->rval) { db_post_events(...);
2768 /// prec->oraw = prec->rval; }` (aoRecord.c:541-543) minus the post: TEST
2769 /// the [`ValuePostGate::OnChangeForced`] field against the record's own
2770 /// "old" copy and, when it moved, ADVANCE that copy — one indivisible
2771 /// step, because C's two statements are inseparable and splitting them is
2772 /// what the port got wrong.
2773 ///
2774 /// Called once per cycle per declared field, by the framework, only when
2775 /// the guard is open — and independently of whether anyone is subscribed,
2776 /// since C's `db_post_events` runs whether or not anyone is listening. A
2777 /// record must not advance the copy anywhere else: an eager `oraw = rval`
2778 /// in the compute step makes the next guarded cycle see no change and
2779 /// withhold an event C sends.
2780 ///
2781 /// Default: `false` — nothing declared, nothing to test.
2782 fn take_secondary_value_change(&mut self, field: &str) -> bool {
2783 let _ = field;
2784 false
2785 }
2786
2787 /// Change-detected auxiliary fields this record posts with C's
2788 /// `monitor_mask | DBE_VALUE` — VAL's monitor mask ORed with `DBE_VALUE`,
2789 /// and NOT the framework default `monitor_mask | DBE_VALUE | DBE_LOG`.
2790 ///
2791 /// The difference is the forced `DBE_LOG`. For a field named here the LOG
2792 /// bit is present only when it is already in VAL's monitor mask — i.e. only
2793 /// when VAL's own ADEL deadband crossed this cycle — so a `DBE_LOG`
2794 /// subscriber (an archiver) receives the field exactly on the cycles C
2795 /// sends it, instead of on every change.
2796 ///
2797 /// `swaitRecord.c::monitor` (647-654) is this shape for its A..L inputs:
2798 ///
2799 /// ```c
2800 /// if (*pnew != *pprev)
2801 /// db_post_events(pwait, pnew, monitor_mask | DBE_VALUE);
2802 /// ```
2803 ///
2804 /// while `calcRecord.c:420` — the same loop, one module over — writes
2805 /// `monitor_mask | DBE_VALUE | DBE_LOG`. The two records genuinely differ,
2806 /// so the mask is a per-record property, not a framework-wide rule.
2807 ///
2808 /// Distinct from [`Self::value_only_change_fields`] (a literal `DBE_VALUE`,
2809 /// which drops the ADEL LOG bit as well) and from
2810 /// [`Self::fields_posted_with_value_mask`] (posted from INSIDE C's
2811 /// `if (monitor_mask)` guard, so they do not post at all on a cycle where
2812 /// VAL itself does not). The fields named here post on every change,
2813 /// guard or no guard.
2814 ///
2815 /// Default: empty.
2816 fn fields_posted_with_monitor_mask(&self) -> &'static [&'static str] {
2817 &[]
2818 }
2819
2820 /// Fields whose change post carries a LITERAL `DBE_VALUE | DBE_LOG` — this
2821 /// cycle's alarm bits DISCARDED.
2822 ///
2823 /// C's fourth mask shape, and the only one that *drops* information the
2824 /// record already computed. `epidRecord.c::monitor` builds VAL's mask from
2825 /// `recGblResetAlarms` (`:351`) and posts VAL with it, then REASSIGNS
2826 /// (not `|=`) before the secondaries:
2827 ///
2828 /// ```c
2829 /// monitor_mask = DBE_LOG|DBE_VALUE; /* :376 */
2830 /// if (pepid->ovlp != pepid->oval) db_post_events(pepid, &pepid->oval, monitor_mask);
2831 /// ... /* P, I, D, CT, DT, ERR, CVAL */
2832 /// ```
2833 ///
2834 /// so on an alarm-transition cycle a `DBE_ALARM`-only subscriber to one of
2835 /// those fields is sent NOTHING, while the generic aux mask
2836 /// (`alarm_bits | DBE_VALUE | DBE_LOG`) would send it an event.
2837 ///
2838 /// Distinct from the three narrower shapes: [`Self::value_only_change_fields`]
2839 /// (literal `DBE_VALUE`), [`Self::fields_posted_with_monitor_mask`]
2840 /// (`monitor_mask | DBE_VALUE` — keeps the alarm bits AND VAL's ADEL LOG bit)
2841 /// and [`Self::fields_posted_with_value_mask`] (VAL's mask, posted from inside
2842 /// C's `if (monitor_mask)` guard). A field named here posts on every change,
2843 /// with both value classes and no alarm class, whatever the cycle's alarms did.
2844 ///
2845 /// Resolved for every change-detected field by `AuxPostMask::mask_for`, the single
2846 /// owner of the aux-post mask.
2847 ///
2848 /// Default: empty.
2849 fn fields_posted_without_alarm_bits(&self) -> &'static [&'static str] {
2850 &[]
2851 }
2852
2853 /// The array-style monitor decision (C waveform/aai/aao `monitor()`,
2854 /// waveformRecord.c:291-326). `None` (the default) means the record has
2855 /// no MPST/APST/HASH mechanism and the generic MDEL/ADEL deadband
2856 /// decision applies. `Some(_)` lets the record replace that with its
2857 /// "Always vs On Change" rule: it hashes the array content, compares to
2858 /// the stored `HASH`, updates it, and reports whether `DBE_VALUE` /
2859 /// `DBE_LOG` should be on the VAL post this cycle and whether the hash
2860 /// changed (so the owner posts `HASH` with `DBE_VALUE`). Called by
2861 /// `check_deadband_ext` (the single owner of the VAL-mask decision).
2862 ///
2863 /// The hook is the VAL mask, not the MPST/APST rule specifically: any
2864 /// record whose C `monitor()` decides the mask by its own rule implements
2865 /// it. `histogram` is the other implementor — its rule is the MDEL COUNT
2866 /// deadband (`mcnt > mdel`, histogramRecord.c:287-291), and like waveform's
2867 /// it updates the state it keys on (there, `MCNT = 0`; here, `HASH`).
2868 fn array_monitor_post(&mut self) -> Option<ArrayMonitorPost> {
2869 None
2870 }
2871
2872 /// Fields the record posts itself via an event-driven, individually
2873 /// masked path rather than the generic change-detection loop. The
2874 /// framework excludes these from that loop so they are neither
2875 /// double-posted nor spuriously posted on a cycle the event did not
2876 /// fire. C waveform/aai/aao `monitor()` posts `HASH` this way —
2877 /// `db_post_events(prec, &prec->hash, DBE_VALUE)` only when the content
2878 /// hash changed (waveformRecord.c:317-319), never via VAL's change.
2879 ///
2880 /// The CLOSED set of fields a process cycle of this record may post —
2881 /// the record's C `process()` + `monitor()` `db_post_events` calls,
2882 /// enumerated.
2883 ///
2884 /// `None` (the default) leaves the framework's generic rule in force:
2885 /// every subscribed field that changed since its last post is posted.
2886 /// That rule is right for a record whose C `monitor()` walks its fields
2887 /// and posts whatever moved (calc, sub, ai …). It is WRONG for a record
2888 /// whose C `monitor()` posts a fixed list and leaves every other field it
2889 /// wrote silent — the framework then invents events C never sends:
2890 ///
2891 /// * a field the record WRITES during `process()` but C never posts
2892 /// (scaler's gate→direction copy, `scalerRecord.c:413-414`: `pdir[i] =
2893 /// pgate[i]` with no `db_post_events` — C posts `Dn` only from
2894 /// `special()`).
2895 ///
2896 /// (A field a PUT already posted is NOT in that category: the put's own
2897 /// post advances `last_posted` — see the `RecordInstance::last_posted`
2898 /// contract — so the next process cycle does not change-detect it. This
2899 /// hook must not be used to paper over a framework double post.)
2900 ///
2901 /// `Some(list)` closes it by construction: a field outside the list is
2902 /// never posted by a process cycle — its only monitors come from its own
2903 /// put and from [`Self::monitor_side_effect_fields`]. The list is a
2904 /// whitelist, not a blacklist, so a field added to the record later stays
2905 /// silent unless C posts it.
2906 ///
2907 /// Fields inside the list keep their normal treatment (change detection,
2908 /// [`Self::value_only_change_fields`] mask, deadband, `log_swept_fields`).
2909 fn process_posted_fields(&self) -> Option<&'static [&'static str]> {
2910 None
2911 }
2912
2913 /// Fields the record posts itself via an event-driven, individually
2914 /// masked path rather than the generic change-detection loop. The
2915 /// framework excludes these from that loop so they are neither
2916 /// double-posted nor spuriously posted on a cycle the event did not
2917 /// fire. C waveform/aai/aao `monitor()` posts `HASH` this way —
2918 /// `db_post_events(prec, &prec->hash, DBE_VALUE)` only when the content
2919 /// hash changed (waveformRecord.c:317-319), never via VAL's change.
2920 ///
2921 /// Default: empty.
2922 fn event_posted_fields(&self) -> &'static [&'static str] {
2923 &[]
2924 }
2925
2926 /// Initialize record (pass 0: field defaults; pass 1: dependent init).
2927 fn init_record(&mut self, _pass: u8) -> CaResult<()> {
2928 Ok(())
2929 }
2930
2931 /// Did `init_record` leave the record PERMANENTLY ACTIVE — C's
2932 /// `prec->pact = TRUE` inside `init_record`, which is how a record type
2933 /// disables itself when it cannot possibly process?
2934 ///
2935 /// `subRecord.c:119-123` is the live case: an empty `SNAM` has no
2936 /// subroutine to call, so C prints `"%s.SNAM is empty"`, sets `pact = TRUE`
2937 /// and returns 0. `dbProcess` then takes the PACT-active branch on every
2938 /// scan — the record serves its fields but never runs record support.
2939 /// `caget X.PACT` on a bare `record(sub,"X"){}` reads 1 on a C IOC.
2940 ///
2941 /// This is a STATE PREDICATE over the record's current fields, not a
2942 /// one-time init verdict: C re-asks it on every put to a field in
2943 /// [`Self::pact_park_fields`], through the two passes of `special()`
2944 /// (`subRecord.c:170-194`). Pass 0 releases the park, pass 1 re-takes it if
2945 /// the value just stored still leaves the record unable to run, so
2946 /// `caput X.SNAM mySub` revives a parked record and `caput X.SNAM ""` parks
2947 /// a running one.
2948 ///
2949 /// PACT is a `dbCommon` field with a single owner
2950 /// ([`crate::server::record::RecordInstance::enter_pact`] / [`leave_pact`]), so a record cannot
2951 /// park it itself. It answers here and the owner performs the transition —
2952 /// at the end of the init passes, and either side of a park-field put.
2953 ///
2954 /// [`leave_pact`]: crate::server::record::RecordInstance::leave_pact
2955 fn parks_pact(&self) -> bool {
2956 false
2957 }
2958
2959 /// The fields whose put can change [`Self::parks_pact`]'s answer — C's
2960 /// `special(SPC_MOD)` set for the PACT park (`subRecord.dbd.pod` marks only
2961 /// `SNAM`). Nothing else re-asks the question, so a put to an unrelated
2962 /// field of a parked record cannot disturb the park the way an
2963 /// every-put re-assertion would. Default: none, so the whole mechanism is
2964 /// inert for every record type that does not use PACT as a self-disable.
2965 fn pact_park_fields(&self) -> &'static [&'static str] {
2966 &[]
2967 }
2968
2969 /// Post-init finalisation hook with mutable access to the
2970 /// framework's UDF flag. Called once after both `init_record`
2971 /// passes complete. Default implementation is a no-op.
2972 ///
2973 /// epics-base PR `dabcf89` (mbboDirect): when VAL is undefined
2974 /// at init time but the user populated B0..B1F bits, the bits
2975 /// should be folded into VAL and UDF cleared. The framework
2976 /// owns `common.udf`, so the record cannot mutate it from
2977 /// `init_record` alone — this hook is the controlled point of
2978 /// access.
2979 fn post_init_finalize_undef(&mut self, _udf: &mut bool) -> CaResult<()> {
2980 Ok(())
2981 }
2982
2983 /// Whether this record type's C `init_record` resets the record to a
2984 /// defined, no-alarm state — `prec->udf = 0; recGblResetAlarms(prec)` — so
2985 /// a freshly loaded, never-processed record reads `UDF=0`,
2986 /// `STAT=NO_ALARM`, `SEVR=NO_ALARM` instead of the born `UDF`/`INVALID`/`1`.
2987 ///
2988 /// Almost no record does this: a not-yet-processed record is normally left
2989 /// `UDF` so an `MS` consumer inherits `INVALID` at IOC startup (see
2990 /// `RecordInstance::run_init_passes`). The asyn record is the exception —
2991 /// `asynRecord.c` `init_record` pass 0 does `pasynRec->udf = 0;
2992 /// recGblResetAlarms(pasynRec)` unconditionally, because a device-config
2993 /// record is defined the moment it loads, even against a disconnected port.
2994 /// `UDF` is a common field `init_record` cannot reach, so the record
2995 /// declares the fact here and the init owner performs the reset. Default
2996 /// `false`.
2997 fn init_resets_alarms(&self) -> bool {
2998 false
2999 }
3000
3001 /// C `cvt_dbaddr`'s `paddr->no_elements` for one of this record type's
3002 /// `special(SPC_DBADDR)` fields — the CHANNEL's capacity, which is not the
3003 /// count `get_array_info` serves.
3004 ///
3005 /// Answer the number only. WHICH fields reach `cvt_dbaddr` is not a record's
3006 /// question: it is the `.dbd`'s, and
3007 /// [`FieldDeclaration::field_native_count`] reads it from there before
3008 /// asking. A record that hand-lists its own array fields here is copying a
3009 /// declaration it already has, which is how the two drift apart.
3010 ///
3011 /// Returning `None` (the default, and the answer for a record type with no
3012 /// `SPC_DBADDR` field) means the channel count is the value's own count.
3013 ///
3014 /// - waveform `VAL` → `NELM` (buffer capacity; the value serves `NORD`).
3015 /// - asyn `BOUT` → `OMAX`, `BINP` → `IMAX` (the `SPC_DBADDR` octet buffers;
3016 /// the value serves the transferred byte count `NOWT`/`NORD`).
3017 /// - acalcout `AVAL`/`AA..LL`/`OAV` → `NELM`, or the `NUSE` window under
3018 /// `SIZE=NUSE` (`aCalcoutRecord.c:627-631`).
3019 /// - mca `VAL`/`BG` → `NMAX` (`mcaRecord.c:857`).
3020 fn dbaddr_capacity(&self, _field: &str) -> Option<u32> {
3021 None
3022 }
3023
3024 /// The part of C's `init_record` tail that is NOT tracker seeding: an
3025 /// output record's closing VAL→RVAL conversion, run at the same point
3026 /// [`Self::seed_deadband_tracking`] is and immediately before it, which is
3027 /// the order C writes the two in (`boRecord.c:167-175` converts, then
3028 /// seeds).
3029 ///
3030 /// It cannot live in [`Self::init_record`]: that runs before the
3031 /// `recGblInitConstantLink` table, so a record whose VAL arrives from a
3032 /// constant DOL would convert the pre-seed VAL. C has no such split —
3033 /// `busyRecord.c:151-179` is one function with the constant load at the
3034 /// top and the conversion at the bottom.
3035 ///
3036 /// Every record whose C init tail does non-tracker work implements it here
3037 /// — the conversion and the output-tracking stores C writes around the
3038 /// `mlst`/`alst`/`lalm` lines:
3039 ///
3040 /// - busy `:176-179` — the convert alone; it seeds no tracker at all.
3041 /// - bo `:166-170,174-175` — convert through MASK, then `oraw`/`orbv`.
3042 /// - mbbo `:176-177,181-182` — `convert()`, then `oraw`/`orbv`.
3043 /// - mbboDirect `:142-143,161-162` — `bitsFromVAL`, then `oraw`/`orbv`.
3044 /// - ao `:156,160-161` — `oval`/`pval`, then `oraw`/`orbv`.
3045 ///
3046 /// The split is by MEANING, not by convenience: this hook is the tail's
3047 /// derived state (RVAL and the bit cells from VAL, ORAW/ORBV from RVAL/RBV,
3048 /// ao's OVAL/PVAL), and [`Self::seed_deadband_tracking`] is the tail's
3049 /// deadband trackers and nothing else. Four records used to carry both
3050 /// halves in `seed_deadband_tracking`, which made "the trackers are seeded"
3051 /// and "the record is converted" the same call — so a caller that wanted
3052 /// one got the other, and busy (which needs the convert without the
3053 /// trackers) had to be the exception. Default: empty.
3054 fn init_record_tail(&mut self) {}
3055
3056 /// Seed the monitor/archive/alarm deadband trackers (MLST/ALST/LALM)
3057 /// from the initial value at iocInit, called once by the builder after
3058 /// both `init_record` passes and `post_init_finalize_undef`.
3059 ///
3060 /// Most C value records end `init_record` with
3061 /// `prec->mlst = prec->alst = prec->lalm = prec->val`
3062 /// (`aiRecord.c:129-131`, `longinRecord.c:120-122`), so their first
3063 /// `monitor()` evaluates `DELTA(mlst, val) > mdel` with `mlst == val`
3064 /// and posts nothing for a value that has not moved since init. The
3065 /// default does that for whichever of MLST/ALST/LALM the record serves,
3066 /// so a record given a nonzero initial VAL (constant DOL, `field(VAL,..)`)
3067 /// does not post a spurious first-cycle update.
3068 ///
3069 /// It is NOT universal, which is why it stays overridable: `calcRecord.c`
3070 /// (:90-114), `dfanoutRecord.c` (:96-111), `selRecord.c` (:88-110),
3071 /// `sCalcoutRecord.c` (:203-322), `aCalcoutRecord.c` (:171-281) and
3072 /// `busyRecord.c` (:127-183) end `init_record` without touching the
3073 /// trackers, so those six leave them at 0 and DO post that first update.
3074 /// Each overrides this with an empty body. The output records override it
3075 /// only to name the subset their C tail actually writes (`mbboDirect`
3076 /// seeds MLST and no LALM, `boRecord.c:172-173` seeds MLST and LALM but no
3077 /// ALST); everything else their tail does is
3078 /// [`Self::init_record_tail`]'s.
3079 fn seed_deadband_tracking(&mut self) {
3080 let seed = match self.monitor_deadband_value().and_then(|v| v.to_f64()) {
3081 Some(v) if v.is_finite() => v,
3082 _ => return,
3083 };
3084 for field in ["MLST", "ALST", "LALM"] {
3085 // Coerce to the cell's own type first. `bi`/`mbbi` declare MLST
3086 // DBF_USHORT and reject a Double outright, so seeding them as a
3087 // Double left them at 0 with the error dropped on the floor —
3088 // silently the no-seed behaviour, for two records C does seed.
3089 let Some(current) = self.get_field(field) else {
3090 continue;
3091 };
3092 let coerced = EpicsValue::Double(seed).convert_to(current.db_field_type());
3093 let _ = self.put_field(field, coerced);
3094 }
3095 }
3096
3097 /// Called by the framework immediately after applying this cycle's
3098 /// [`Record::multi_input_links`] fetches, before `process()`.
3099 ///
3100 /// `resolved` lists the `link_field` names (the first element of
3101 /// each `multi_input_links` pair) whose fetch SUCCEEDED this cycle —
3102 /// C's `RTN_SUCCESS(dbGetLink(...))`, i.e. status 0. That includes a
3103 /// CONSTANT link, which returns success having delivered nothing
3104 /// (`dbConstGetValue`, `dbConstLink.c:219-225`) — `epidRecord.c:191`
3105 /// clears UDF on exactly that. A link field absent from the slice
3106 /// either had no link configured or its DB/CA fetch FAILED.
3107 ///
3108 /// This is the framework analogue of C device support inspecting
3109 /// `RTN_SUCCESS(dbGetLink(...))` — e.g. `epidRecord.c:191-193`
3110 /// clears `udf` only when `dbGetLink(&prec->stpl, ...)` returns
3111 /// success. A record's `process()` cannot otherwise observe whether
3112 /// an input link's fetch succeeded, because a failed fetch simply
3113 /// leaves the target field unwritten.
3114 ///
3115 /// Additive, framework-set-hook pattern (same shape as
3116 /// [`Record::set_process_context`]). Default: ignore.
3117 fn set_resolved_input_links(&mut self, _resolved: &[&'static str]) {}
3118
3119 /// Report this cycle's `fetch_values()` outcome: `failed == true` means C's
3120 /// helper would have returned a non-zero status, so the record body — the
3121 /// `calcPerform` / `do_sel` the C `process()` wraps in
3122 /// `if (fetch_values(prec) == 0)` — must NOT run, and VAL/UDF freeze.
3123 ///
3124 /// This is the single delivery point for that outcome, whichever
3125 /// [`InputFetchPolicy`] produced it (a failed link read) and whichever
3126 /// record-specific rule did (sel's `Specified`-mode selected-input read).
3127 /// Records that gate: calc (calcRecord.c:120), calcout (:237), sCalcout
3128 /// (sCalcoutRecord.c:356), aCalcout (aCalcoutRecord.c:399), swait
3129 /// (swaitRecord.c:408 — which additionally raises READ_ALARM/INVALID on the
3130 /// failure) and sel (selRecord.c:114). sub/aSub gate the same outcome, but
3131 /// their body is the framework-dispatched subroutine, so they consume it
3132 /// through `RecordInstance::suppress_subroutine_run` instead.
3133 ///
3134 /// Default: ignore (records with no fetch gate). Same framework-set hook
3135 /// pattern as [`Record::set_resolved_input_links`].
3136 fn set_fetch_gate_failed(&mut self, _failed: bool) {}
3137
3138 /// Whether this record's multi-input fetch ([`Self::multi_input_links`]) is
3139 /// C `dbGetLink` — which raises `setLinkAlarm` (LINK/INVALID, AMSG
3140 /// `field <NAME>`) on every failed read — or a `recDynLink` CA-style get,
3141 /// which raises nothing.
3142 ///
3143 /// `true` for every base record and for sCalcout/aCalcout/transform
3144 /// (`calcRecord.c:439`, `calcoutRecord.c:705`, `subRecord.c:414`,
3145 /// `aSubRecord.c:282`, `selRecord.c:430`, `printfRecord.c:54`,
3146 /// `sCalcoutRecord.c:886`, `aCalcoutRecord.c:1070`,
3147 /// `transformRecord.c:537`). `false` only for swait, whose `fetch_values`
3148 /// (`swaitRecord.c:702`) reads INAA..INPL with `recDynLinkGet` and answers a
3149 /// failure with `recGblSetSevr(READ_ALARM, INVALID_ALARM)` at
3150 /// `swaitRecord.c:413` instead. Both alarms are INVALID and
3151 /// `rec_gbl_set_sevr*` is strict-greater, so raising the wrong one first
3152 /// wins the tie and publishes the wrong STAT.
3153 fn multi_input_fetch_is_db_get_link(&self) -> bool {
3154 true
3155 }
3156
3157 /// Whether a FAILED read of this multi-input link leaves the cycle
3158 /// untouched — no `setLinkAlarm`, no [`Self::input_fetch_policy`] gate,
3159 /// no value stored.
3160 ///
3161 /// One record needs it: acalcout guards its ARRAY half with the link's own
3162 /// connection status (`aCalcoutRecord.c:1078`)
3163 ///
3164 /// ```c
3165 /// if ((*plinkValid==acalcoutINAV_EXT) || (*plinkValid==acalcoutINAV_LOC))
3166 /// ```
3167 ///
3168 /// so an array link C believes cannot deliver is never read at all, and
3169 /// `fetch_values` returns 0 with the calc still running. C moves a link
3170 /// between `EXT_NC` and `EXT` from a `dbCaIsLinkConnected` watchdog
3171 /// (`aCalcoutRecord.c:1155`, `:1187`); this port has no CA client, so it
3172 /// publishes `EXT_NC` for EVERY external link
3173 /// ([`crate::server::records::link_status::classify_link`]) and cannot make
3174 /// that test at select time. Reading and then discarding the failure is the
3175 /// same observable behaviour for every state the port can reach — a link
3176 /// that delivers is C's `EXT`/`LOC`, a link that does not is C's `EXT_NC` —
3177 /// and, unlike gating on the published `IAAV`, it does not stop reading the
3178 /// external array links whose values the resolver really does serve.
3179 ///
3180 /// The SCALAR half is deliberately NOT inert: its loop (`:1068-1071`) has
3181 /// no status test and `return`s at the first failing `dbGetLink`.
3182 fn input_link_failure_is_inert(&self, _link_field: &str) -> bool {
3183 false
3184 }
3185
3186 /// Whether this record's C `process()` performs the SCALAR closed-loop DOL
3187 /// fetch — `dbGetLink(&prec->dol, <dbr>, &prec->val, 0, 0)` guarded by
3188 /// `prec->dol.type != CONSTANT && prec->omsl == menuOmslclosed_loop`
3189 /// (`boRecord.c:191-205`, `busyRecord.c:196-208`, `dfanoutRecord.c:116-122`,
3190 /// and the same block in ao/longout/int64out/mbbo/mbboDirect/stringout/lso).
3191 /// The framework then owns the fetch, its LINK/INVALID failure arm
3192 /// ([`Self::closed_loop_dol_read_failed`]) and the UDF clear.
3193 ///
3194 /// Declaring both `menu(menuOmsl)` and `field(DOL,DBF_INLINK)` is NOT the
3195 /// same question and cannot stand in for this one: `aao` declares both and
3196 /// copies DOL as an ARRAY (`aaoRecord.c::fetchValue`), `motor` declares both
3197 /// and drives DOL through its own C code. Both source DOL record-locally and
3198 /// answer `false` here. `epid` has `menuOmsl` and no DOL at all — it fetches
3199 /// from `STPL`.
3200 ///
3201 /// This was a record-name match inside the process cycle. A list the
3202 /// compiler cannot check, in a file no record author reads, had already
3203 /// been wrong twice — `dfanout` once and `busy` again — so the fact moved
3204 /// to the record that owns it. Default: false.
3205 fn fetches_dol_closed_loop(&self) -> bool {
3206 false
3207 }
3208
3209 /// C's failure arm for THIS cycle's closed-loop DOL read: the framework
3210 /// calls it when `dbGetLink(&prec->dol, ...)` returned a non-zero status
3211 /// with `OMSL = closed_loop` and a non-constant DOL.
3212 ///
3213 /// The LINK/INVALID alarm is not this hook's business — `db_get_link` owns
3214 /// it, exactly as C's `dbGetLink` does. What is left is the per-record body
3215 /// gate, and C writes a different one in each record:
3216 ///
3217 /// * ao — `fetch_value` (aoRecord.c:442) sets `prec->val = prec->pval`
3218 /// BEFORE the read, and `if(!status) convert(prec, value)` (:188) then
3219 /// skips the convert, so VAL falls back to the last actual output and
3220 /// OVAL/PVAL/RVAL freeze (an OROC ramp stops advancing).
3221 /// * longout (:155) / int64out (:146) — same `if (!status) convert(...)`,
3222 /// whose convert is the DRVH/DRVL clamp.
3223 /// * mbbo (:206) / mbboDirect (:186) — `goto CONTINUE` past `udf = FALSE`,
3224 /// `bitsFromVAL`, `convert` and the pre-output timestamp.
3225 /// * bo (:200-204), stringout, lso, dfanout — nothing: bo converts VAL to
3226 /// RVAL whether the read succeeded or not, and the other three have no
3227 /// convert step at all. They keep the default no-op.
3228 ///
3229 /// Additive, framework-set-hook pattern, same shape as
3230 /// [`Self::set_fetch_gate_failed`]. Default: ignore.
3231 fn closed_loop_dol_read_failed(&mut self) {}
3232
3233 /// The INP counterpart of [`Self::closed_loop_dol_read_failed`]: the
3234 /// framework calls it when a soft-channel `read_xxx`'s `dbGetLink(&prec->inp,
3235 /// ...)` returned a non-zero status, so no value was sourced this cycle.
3236 ///
3237 /// The LINK/INVALID alarm is not this hook's business — `db_get_link` owns
3238 /// it, as C's `dbGetLink` does. What is left is what the record makes of a
3239 /// cycle that read nothing, and C writes that per record:
3240 ///
3241 /// * subArray — `read_sa` skips `subset()` entirely on a non-zero status
3242 /// (`devSASoft.c:118-120`), `readValue` returns it, and `process` does
3243 /// `prec->udf = !!status` (`subArrayRecord.c:148`). An UNDEFINED subArray
3244 /// then serves ZERO elements, not the stale slice
3245 /// (`get_array_info`, `:181-184`) — the only array record with that rule.
3246 /// * waveform/aai/aao — nothing: they clear UDF on the line after
3247 /// `readValue` whatever it returned ([`Self::clears_udf_unconditionally`]).
3248 /// * the scalar soft records — nothing here either; C leaves `prec->udf`
3249 /// untouched on a failed read (`if (status == 0) … prec->udf = FALSE`),
3250 /// which is the framework's per-cycle re-derive, not a record hook.
3251 ///
3252 /// Additive, framework-set-hook pattern. Default: ignore.
3253 fn soft_input_read_failed(&mut self) {}
3254
3255 /// Report this cycle's subroutine status — C `process`'s `status` variable
3256 /// for `sub`/`aSub`:
3257 ///
3258 /// ```c
3259 /// status = fetch_values(prec);
3260 /// if (!status) { status = do_sub(prec); prec->val = status; }
3261 /// ...
3262 /// if (!status) /* aSubRecord.c:234-240 */
3263 /// for (i = 0; i < NUM_ARGS; i++)
3264 /// dbPutLink(&(&prec->outa)[i], (&prec->ftva)[i], (&prec->vala)[i],
3265 /// (&prec->neva)[i]);
3266 /// ```
3267 ///
3268 /// so `0` — and only `0` — means the input fetch succeeded AND `do_sub` ran
3269 /// and returned success. It is the gate on aSub's OUT-link pushes.
3270 ///
3271 /// Delivered by `RecordInstance::run_registered_subroutine`, the single
3272 /// owner of the `do_sub` call, on every one of its exit paths (the
3273 /// suppressed cycle, no bound routine, the routine's own return).
3274 ///
3275 /// Default: ignore (records with no subroutine).
3276 fn set_subroutine_status(&mut self, _status: i64) {}
3277
3278 /// Called before/after a field put for side-effect processing.
3279 fn special(&mut self, _field: &str, _after: bool) -> CaResult<()> {
3280 Ok(())
3281 }
3282
3283 /// The period of this record's monitor watchdog, or `None` when it has
3284 /// none — C `histogramRecord.c::wdogInit` (:126-152):
3285 ///
3286 /// ```c
3287 /// static void wdogInit(histogramRecord *prec) {
3288 /// if (prec->sdel > 0) { ... callbackRequestDelayed(&pcallback->callback, prec->sdel); }
3289 /// }
3290 /// ```
3291 ///
3292 /// A watchdog is NOT a process cycle: it posts monitors for a record whose
3293 /// value is changing but whose deadband (histogram MDEL) is holding the
3294 /// posts back, so a slow accumulation still reaches a display. The
3295 /// framework re-reads this on every tick, so clearing SDEL stops the
3296 /// watchdog at the next fire without any separate cancel.
3297 ///
3298 /// histogram is the only base record with one. Default: no watchdog.
3299 fn watchdog_interval(&self) -> Option<std::time::Duration> {
3300 None
3301 }
3302
3303 /// One watchdog tick — C `histogramRecord.c::wdogCallback` (:102-124):
3304 ///
3305 /// ```c
3306 /// if (prec->mcnt > 0) {
3307 /// dbScanLock(prec);
3308 /// recGblGetTimeStamp(prec);
3309 /// db_post_events(prec, &prec->val, DBE_VALUE | DBE_LOG);
3310 /// prec->mcnt = 0;
3311 /// dbScanUnlock(prec);
3312 /// }
3313 /// ```
3314 ///
3315 /// The record performs its own state change (histogram: zero MCNT) and
3316 /// returns the fields whose monitors the framework must post — the
3317 /// `db_post_events` half, which a record cannot do itself. An empty slice
3318 /// means "nothing changed since the last tick": no timestamp, no post. The
3319 /// framework holds the record lock across the call (C `dbScanLock`) and
3320 /// re-arms afterwards from [`Self::watchdog_interval`].
3321 ///
3322 /// Default: nothing to post.
3323 fn watchdog_fire(&mut self) -> &'static [&'static str] {
3324 &[]
3325 }
3326
3327 /// The body of the delayed callback armed by
3328 /// [`ProcessAction::DelayedCallbackAfter`] — C `boRecord.c::myCallbackFunc`
3329 /// (:105-118):
3330 ///
3331 /// ```c
3332 /// if (prec->pact) {
3333 /// if ((prec->val == 1) && (prec->high > 0)) { callbackRequestDelayed(cb, prec->high); }
3334 /// } else {
3335 /// prec->val = 0;
3336 /// dbProcess((struct dbCommon *)prec);
3337 /// }
3338 /// ```
3339 ///
3340 /// Run under the record gate (C `dbScanLock`) when the timer fires, and
3341 /// before any `process()` re-entry. `pact` is the record's PACT at fire
3342 /// time, which the record cannot read for itself.
3343 ///
3344 /// This hook is the ONLY place the one-shot's mutation may happen: keeping
3345 /// it out of `process()` is what stops an unrelated process cycle from
3346 /// consuming it. Default: re-enter `process()` and change nothing, so a
3347 /// record that emits the action without overriding still behaves as a plain
3348 /// [`ProcessAction::ReprocessAfter`].
3349 fn delayed_callback_fire(&mut self, _pact: bool) -> DelayedCallbackOutcome {
3350 DelayedCallbackOutcome::Reprocess
3351 }
3352
3353 /// Whether this record type's support reads SIML through the `recGbl`
3354 /// simulation helpers (`recGblGetSimm`/`recGblInitSimm`, and therefore
3355 /// `recGblSaveSimm`/`recGblCheckSimm`) rather than a bare `dbGetLink`.
3356 ///
3357 /// ONE C fact, two consequences — which is why it is one predicate:
3358 ///
3359 /// - **The SCAN swap.** The helpers take `&prec->sscn` and `&prec->oldsimm`,
3360 /// so only a record that declares those fields can call them, and only
3361 /// such a record swaps SCAN with SSCN on a SIMM transition
3362 /// (`recGblCheckSimm`, `recGbl.c:427-437`).
3363 /// - **The alarm on a failed SIML read.** `recGblGetSimm` reads SIML with
3364 /// `dbTryGetLink` — which does NOT call `setLinkAlarm` — and then raises
3365 /// the alarm itself, by writing `nsta` DIRECTLY:
3366 /// `if (status && !pcommon->nsev) pcommon->nsta = LINK_ALARM;`
3367 /// (`recGbl.c:454`). SEVR is left alone. A record reading SIML with a
3368 /// plain `dbGetLink` (`busyRecord.c:399`, `swaitRecord.c:402`) instead
3369 /// gets `setLinkAlarm` (`dbLink.c:319-323`) →
3370 /// `recGblSetSevrMsg(LINK_ALARM, INVALID_ALARM)`, a full severity raise.
3371 ///
3372 /// The 21 base records that declare SSCN answer `true`; `busy` and `swait`
3373 /// carry SIMM/SIML/SIOL but neither SSCN nor OLDSIMM. Records with no
3374 /// simulation block answer `false` trivially.
3375 ///
3376 /// The default consults [`record_type_has_sscn`](crate::server::recgbl::simm::record_type_has_sscn),
3377 /// which is enumerated from the C dbd files, so no record has to restate it.
3378 fn uses_recgbl_simm_helpers(&self) -> bool {
3379 crate::server::recgbl::simm::record_type_has_sscn(self.record_type())
3380 }
3381
3382 /// The record's `readValue`/`writeValue` ABORTS when the SIML read fails —
3383 /// it returns before performing any I/O, so the cycle does no device write,
3384 /// no SIOL redirect, and raises no SIMM_ALARM.
3385 ///
3386 /// `busy` is the only record that does this (busyRecord.c:399-401):
3387 ///
3388 /// ```c
3389 /// status = dbGetLink(&prec->siml, DBR_USHORT, &prec->simm, 0, 0);
3390 /// if (status)
3391 /// return(status); /* <-- before write_busy AND before dbPutLink */
3392 /// ```
3393 ///
3394 /// The LINK_ALARM that `dbGetLink`'s `setLinkAlarm` already raised is the
3395 /// cycle's only simulation alarm.
3396 ///
3397 /// The other two families do NOT abort, for different reasons:
3398 ///
3399 /// - The 21 [`Self::uses_recgbl_simm_helpers`] records look like they do —
3400 /// `readValue` has `status = recGblGetSimm(...); if (status) return status;`
3401 /// (longinRecord.c:403-405) — but `recGblGetSimm` ends with an
3402 /// unconditional `return 0` (recGbl.c:456), so that branch is DEAD and the
3403 /// record always proceeds to its `switch (prec->simm)`.
3404 /// - `swait` reads SIML with a plain `dbGetLink` and simply never tests the
3405 /// status (swaitRecord.c:402), so it proceeds too.
3406 ///
3407 /// Default: `false`.
3408 fn aborts_on_failed_siml_read(&self) -> bool {
3409 false
3410 }
3411
3412 /// The record joined (`true`) or left (`false`) the `SCAN="I/O Intr"` list.
3413 ///
3414 /// C parity: `dbScan.c::scanAdd` calls the record's device support
3415 /// `get_ioint_info(0, precord, &iopvt)` when SCAN becomes `I/O Intr`, and
3416 /// `scanDelete` calls `get_ioint_info(1, ...)` when it leaves. Device
3417 /// support that registers driver interrupt callbacks does so there —
3418 /// `asynRecord.c:582-597` registers/cancels its per-interface interrupt
3419 /// users in exactly those two calls, and clears its `gotValue` cell on the
3420 /// register.
3421 ///
3422 /// The port's device supports own their subscription through
3423 /// [`crate::server::device_support::DeviceSupport::io_intr_receiver`],
3424 /// which the framework asks for once at `iocInit`. This hook is the
3425 /// *runtime* half: a record whose own state decides what to subscribe to
3426 /// (asynRecord's PORT/IFACE/UI32MASK/REASON) must (re)register when the
3427 /// operator moves SCAN in or out of `I/O Intr` after `iocInit`.
3428 ///
3429 /// Called from the single owner of the SCAN transition
3430 /// (`RecordInstance::put_common_field*`, and the `scanAdd`-failure demotion
3431 /// in `ioc_app`) only when I/O Intr membership actually changes, so it is
3432 /// never invoked twice for the same state. Default: ignore.
3433 fn set_io_intr_scan(&mut self, _active: bool) {}
3434
3435 /// The link writes a C `special()` performs *itself*, inside `dbPut`.
3436 ///
3437 /// `special()` takes the record alone — it has no database handle — so a
3438 /// record whose C `special()` calls `dbPutLink` cannot make that write from
3439 /// `special()`. It queues the write here instead, and the put owner
3440 /// (`field_io`'s `dbPut` paths) drains the queue immediately after
3441 /// `special(field, true)` returns and executes the actions BEFORE the put's
3442 /// `pp(TRUE)` process cycle. That is C's order: `dbPutField` → `dbPut` →
3443 /// `dbPutSpecial(paddr, 1)` — which runs the `dbPutLink` to completion,
3444 /// target processing included — → `dbProcess`.
3445 ///
3446 /// The scaler is the case this exists for: `scalerRecord.c:623-624` puts
3447 /// `CNT` to `COUTP` inside `special()`, so a record wired to `.COUTP` is
3448 /// processed while the scaler is still IDLE, before the count is armed. The
3449 /// port deferred that write to the head of the CNT-triggered process cycle,
3450 /// where the target saw an already-COUNTING scaler.
3451 ///
3452 /// This is neither the record's "should the link fire" state nor part of the
3453 /// process cycle's action list: a `special()` put and a `process()` put to
3454 /// the same link (scaler `COUTP` again, `:463`) are independent writes.
3455 ///
3456 /// The drain is unconditional — it runs even when `special()` returned an
3457 /// error — so a queued action can never survive the put that queued it and
3458 /// fire against a later, unrelated put.
3459 ///
3460 /// Default: none (a `special()` that writes no link).
3461 fn take_special_actions(&mut self) -> Vec<ProcessAction> {
3462 Vec::new()
3463 }
3464
3465 /// A `special()`/`put_field` pass that ended on C's `prec->udf = FALSE`.
3466 ///
3467 /// UDF is a common field, so a record method cannot write it — the same
3468 /// wall [`Self::post_init_finalize_undef`] exists for at init time. This is
3469 /// the runtime half: `histogramRecord.c:354-364`'s `clear_histogram` ends
3470 /// on `prec->udf = FALSE` and is reached from two puts (the `CMD <= 1`
3471 /// SPC_CALC arm `:246-259` and the ULIM/LLIM SPC_RESET arm `:266-273`), so
3472 /// latching inside the clear rather than at each caller is what makes the
3473 /// two agree by construction.
3474 ///
3475 /// Drained unconditionally by the after-put owner, for
3476 /// [`Self::take_special_actions`]'s reason: C performs this assignment
3477 /// inside `special()`, before any status can divert the caller.
3478 ///
3479 /// Default: none (the record's puts never clear UDF).
3480 fn take_udf_clear(&mut self) -> bool {
3481 false
3482 }
3483
3484 /// Other fields whose monitors must be posted because a put to
3485 /// `put_field` changed them as a side effect, without driving a full
3486 /// process cycle.
3487 ///
3488 /// Mirrors the explicit `db_post_events` calls a C `special()` makes:
3489 /// e.g. `compressRecord.c::reset` (invoked on a `SPC_RESET` write to
3490 /// `RES`) posts `NUSE` and `VAL` even though `RES` is not `pp(TRUE)`
3491 /// and so does not process. The framework posts a `VALUE|LOG` monitor
3492 /// for each returned field after the put. Default: none.
3493 fn monitor_side_effect_fields(&self, _put_field: &str) -> &'static [&'static str] {
3494 &[]
3495 }
3496
3497 /// True iff the C `special()` for a put to `put_field` runs the record's
3498 /// own `monitor()` — whose first act is `recGblResetAlarms(prec)`, which
3499 /// commits `nsta`/`nsev` into `stat`/`sevr` and clears the born-UDF alarm.
3500 ///
3501 /// `compress` is the case this exists for: `compressRecord.c::special`
3502 /// (:385-388) calls `reset(prec); monitor(prec);` on any SPC_RESET write
3503 /// (RES/ALG/PBUF/BALG/N), and `compressRecord.c::monitor` (:103) opens with
3504 /// `recGblResetAlarms`. A born-UDF compress record therefore transitions to
3505 /// NO_ALARM the moment one of those fields is put, with no process cycle.
3506 ///
3507 /// The framework already posts the `db_post_events` half of that `monitor()`
3508 /// through [`Record::monitor_side_effect_fields`]; this hook is the
3509 /// `recGblResetAlarms` half. When it returns true, the put owner
3510 /// (`field_io`'s `dbPut` paths) runs `rec_gbl_reset_alarms` and posts the
3511 /// resulting STAT/SEVR/AMSG/ACKS transition. Default: false (a `special()`
3512 /// that does not run the record's `monitor()`).
3513 fn special_commits_alarms(&self, _put_field: &str) -> bool {
3514 false
3515 }
3516
3517 /// True iff the C `special()` for a put to `put_field` runs code that
3518 /// writes `stat`/`sevr` DIRECTLY (not `nsta`/`nsev`) with NO `monitor()` /
3519 /// `recGblResetAlarms` after it — so the write STICKS and a later `caget`
3520 /// observes it, unlike the process path where `recGblResetAlarms` erases it.
3521 ///
3522 /// `histogram` is the case this exists for: a `.SGNL` caput is C's SPC_MOD
3523 /// `special()` → `add_count`, which writes `prec->stat = SOFT_ALARM` on
3524 /// inverted limits (histogramRecord.c:329-334) and returns with no monitor.
3525 /// When true, the put owner (`field_io`) runs
3526 /// [`Record::check_alarms`] after the store — the same direct write the
3527 /// process path makes, but here it persists because no process cycle
3528 /// follows. Default: false (no direct special-path alarm write).
3529 fn special_checks_alarms(&self, _put_field: &str) -> bool {
3530 false
3531 }
3532
3533 /// Downcast to concrete type for device support init injection.
3534 /// Override in record types that need device support to inject state (e.g., MotorRecord).
3535 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3536 None
3537 }
3538
3539 /// Whether processing this record should clear UDF.
3540 /// Override to return false for record types that don't produce a valid value every cycle.
3541 fn clears_udf(&self) -> bool {
3542 true
3543 }
3544
3545 /// Does this record's C `process()` re-derive `udf` after a DEVICE read
3546 /// that wrote VAL directly (C `return 2`)?
3547 ///
3548 /// True where the `else if (status == 2) status = 0;` fold happens BEFORE
3549 /// the UDF assignment, so a `2` arrives at it as a `0` and the record
3550 /// re-derives whatever the dset already wrote: `aiRecord.c:158-161` is the
3551 /// shape, and it is the majority.
3552 ///
3553 /// False for the five records that keep the assignment INSIDE
3554 /// `if (status == 0)` with the fold after it — `biRecord.c:55-60`,
3555 /// `mbbiRecord.c:61-86`, `mbbiDirectRecord.c:58-69` — or that have no fold
3556 /// at all — `longinRecord.c:58`, `int64inRecord.c:58`. There `udf` belongs
3557 /// to the dset, which is exactly where C puts it: `devBiSoft.c::readLocked`
3558 /// writes `prec->udf = FALSE` and returns 2, and `devBiDbState.c:67` does
3559 /// the same. The port's equivalent is
3560 /// [`DeviceUdf`](crate::server::device_support::DeviceUdf), applied before
3561 /// this rule runs.
3562 ///
3563 /// Not consulted for a soft-channel INP read: there the framework IS the
3564 /// dset, so it owns the clear the way `readLocked` does.
3565 fn rederives_udf_on_computed_read(&self) -> bool {
3566 true
3567 }
3568
3569 /// Whether this record's C `process()` clears UDF regardless of the read's
3570 /// status — i.e. even when `readValue` failed.
3571 ///
3572 /// Most records gate the clear on the read: `if (status == 0) prec->udf =
3573 /// FALSE;` inside `readValue`'s SIOL branch (`longinRecord.c:418`), so a
3574 /// failed simulation read leaves the record undefined. The array records do
3575 /// NOT: their `process()` clears UDF itself, unconditionally, on the line
3576 /// after `readValue` returns — `prec->pact = TRUE; prec->udf = FALSE;`
3577 /// (`waveformRecord.c:143-144`, `aaiRecord.c:173-174`) and
3578 /// `if (!pact) { prec->udf = FALSE; ... }` (`aaoRecord.c:164-165`) — whatever
3579 /// status came back, including the `-1` of an illegal SIMM. (waveform's
3580 /// readValue also clears UDF on a good SIOL read, `:353`, but the
3581 /// process-level clear runs after it and dominates.)
3582 ///
3583 /// Consulted by the simulation tail, which otherwise gates the clear on the
3584 /// SIOL fetch status. Default `false` — the status-gated majority.
3585 fn clears_udf_unconditionally(&self) -> bool {
3586 false
3587 }
3588
3589 /// Does this record's C `process()` assign `udf` on a cycle whose INP read
3590 /// FAILED?
3591 ///
3592 /// The scalar input records do not — the assignment sits inside
3593 /// `if (status == 0)`: `aiRecord.c:161`, `biRecord.c:136-140`,
3594 /// `mbbiRecord.c:168-174`, `mbbiDirectRecord.c:155-164`,
3595 /// `longinRecord.c:148`, `int64inRecord.c:144`. A broken link therefore
3596 /// leaves the record at whatever UDF it already had, and that is what makes
3597 /// the `if (prec->udf) recGblSetSevr(prec, UDF_ALARM, ...)` on the next line
3598 /// reachable at all (`mbbiDirectRecord.c:168-169`).
3599 ///
3600 /// The array records and `compress` do — `prec->udf = FALSE` runs after
3601 /// `readValue` whatever it returned (`waveformRecord.c:144`,
3602 /// `aaiRecord.c:174`, `aaoRecord.c:165`); `compressRecord.c:342-366` folds a
3603 /// failed `dbGetLink` into `status = 0` and clears anyway; and subArray's
3604 /// `prec->udf = !!status` (`subArrayRecord.c:148`) IS the status.
3605 ///
3606 /// Default `false` — the status-gated majority. Records whose
3607 /// [`Self::clears_udf`] is false never reach the question: their gate is
3608 /// already "a value was sourced this cycle", which a failed read fails.
3609 fn derives_udf_on_read_failure(&self) -> bool {
3610 false
3611 }
3612
3613 /// Whether this record type's `.dbd` declares an `INP` field at all.
3614 ///
3615 /// The port keeps INP on `CommonFields` for every record, which is right for
3616 /// the input records (`aiRecord.dbd.pod` … all declare it) but wrong for the
3617 /// ones whose C `.dbd` has no INP: C's dbd is the gate there, and
3618 /// `field(INP,...)` on such a record is a load error ("field not found"),
3619 /// leaving the record inert.
3620 ///
3621 /// `histogram` is the case this exists for: `histogramRecord.dbd.pod`
3622 /// declares NO INP — its DBF_INLINK is `SVL` (:212), read into SGNL by
3623 /// `devHistogramSoft.c` — so a histogram driven from INP is a database that
3624 /// no C IOC can load. Default `true`.
3625 ///
3626 /// This is the whole namespace gate, not just the loader's: in C the dbd
3627 /// also decides which `.FIELD` channels exist, so a histogram's INP is not
3628 /// resolvable at all (`dbgf HI.INP` → `PV 'HI.INP' not found`). Both
3629 /// [`RecordInstance::get_common_field`] and
3630 /// [`RecordInstance::put_common_field`] consult this, so the field cannot
3631 /// be readable on one route while refused on the other.
3632 ///
3633 /// [`RecordInstance::get_common_field`]: crate::server::record::RecordInstance::get_common_field
3634 /// [`RecordInstance::put_common_field`]: crate::server::record::RecordInstance::put_common_field
3635 fn declares_inp_link(&self) -> bool {
3636 true
3637 }
3638
3639 /// Process-time INP read when the INP link is CONSTANT (or unset) — the
3640 /// per-record exception to the load-once rule.
3641 ///
3642 /// Nearly every soft input device support skips a constant INP at process:
3643 /// `devWfSoft.c::read_wf` and `devAaiSoft.c::read_aai` open with
3644 /// `if (dbLinkIsConstant(pinp)) return 0;`, and the scalar ones call
3645 /// `dbGetLink`, whose `dbConstGetValue` (`dbConstLink.c:219-225`) writes
3646 /// nothing. The constant reaches such a record ONCE, at init, through
3647 /// `recGblInitConstantLink` / `dbLoadLinkArray`
3648 /// (`PvDatabase::rec_gbl_init_constant_inp`), so a client's caput to VAL is
3649 /// never clobbered by a re-delivered constant.
3650 ///
3651 /// `devSASoft.c::read_sa` (92-123) is the documented exception: it re-runs
3652 /// `dbLoadLinkArray` on a constant INP EVERY process, and on an EMPTY INP
3653 /// (`S_db_badField`) it sets `nRequest = prec->nord` and still subsets — so
3654 /// the record re-slices the client-written VAL by INDX each cycle. C draws
3655 /// that line at the device-support layer (`devSASoft` vs `devAaiSoft`), and
3656 /// so does this hook.
3657 ///
3658 /// Called by the framework on a soft-DTYP cycle whose INP is constant, with
3659 /// `value = Some(constant)` for a non-empty constant and `None` for an
3660 /// empty/unset INP. Returns whether the record consumed the input stage.
3661 /// Default `false` — the load-once rule.
3662 fn read_constant_inp(&mut self, _value: Option<EpicsValue>) -> bool {
3663 false
3664 }
3665
3666 /// Whether this record type raises UDF_ALARM at all.
3667 ///
3668 /// C has no framework-level UDF alarm: every record that reports one does
3669 /// it itself, with the guard at the top of its own `checkAlarms` —
3670 /// `if (prec->udf) { recGblSetSevr(prec, UDF_ALARM, prec->udfs); return; }`
3671 /// (`aiRecord.c:319-323`, `calcRecord.c:300-304`, …). A record whose
3672 /// support has no such guard NEVER reports UDF_ALARM, no matter what its
3673 /// `UDF` field says — `swaitRecord.c` is the case in point: its two only
3674 /// `udf` statements are `udf = FALSE` (`:411`, `:419`); it has no
3675 /// `checkAlarms` and never names UDF_ALARM.
3676 ///
3677 /// The framework raises UDF_ALARM centrally (`rec_gbl_check_udf`), so this
3678 /// hook is where a record type says C does not. Default `true` — the base
3679 /// analog/binary/string records all carry the guard.
3680 fn raises_udf_alarm(&self) -> bool {
3681 true
3682 }
3683
3684 /// Whether this record's C `checkAlarms` tests the UDF byte with
3685 /// `if (prec->udf == TRUE)` (EXACT-ONE) rather than `if (prec->udf)`
3686 /// (truthy). See [`crate::server::recgbl::udf_alarm_active`]: exact-one
3687 /// records (`boRecord.c:371`, `stringoutRecord.c:146`, `biRecord.c:225`,
3688 /// `busyRecord.c:337`) do NOT raise UDF_ALARM for a `udf` byte that is
3689 /// neither 0 nor 1.
3690 ///
3691 /// This only changes behavior for a record whose `udf` byte can actually
3692 /// hold such a value at `checkAlarms` time — one that does NOT re-derive
3693 /// `udf` every cycle ([`Record::clears_udf`] `== false`), reached via a
3694 /// direct `caput .UDF 255` (or `-1`, stored as `255` in the `DBF_UCHAR`
3695 /// field). For the re-deriving records (`clears_udf() == true`, e.g.
3696 /// `bi`/`busy`/the calc family) the byte is always 0/1 here, so exact-one
3697 /// and truthy agree and the flag is left at its default. Default `false`.
3698 fn udf_alarm_on_exact_one(&self) -> bool {
3699 false
3700 }
3701
3702 /// The severity C raises `UDF_ALARM` at, or `None` to use the record's
3703 /// live `UDFS` field.
3704 ///
3705 /// 19 of the 21 `UDF_ALARM` raise sites in `std/rec` pass `prec->udfs`;
3706 /// exactly two pass the literal `INVALID_ALARM` — `lsoRecord.c:117-118`
3707 /// and `mbbiDirectRecord.c:168-169`. Nothing derives that split: the
3708 /// Direct pair disagrees with itself (`mbboDirectRecord.c:191` passes
3709 /// `prec->udfs`), and so does the long-string pair (`lsi` raises no UDF
3710 /// alarm at all). It is a per-record fact, so it belongs on the record and
3711 /// not in a two-name branch inside `rec_gbl_check_udf`.
3712 ///
3713 /// Overriding this makes `UDFS` inert for that record, which is the point:
3714 /// `rec_gbl_set_sevr_msg` is strict-greater, so `UDFS=NO_ALARM` on an `lso`
3715 /// otherwise raised nothing at all where C reports INVALID/UDF.
3716 fn udf_alarm_severity(&self) -> Option<crate::server::record::AlarmSeverity> {
3717 None
3718 }
3719
3720 /// The alarm message C attaches when raising `UDF_ALARM`.
3721 ///
3722 /// Almost every base record raises UDF with plain
3723 /// `recGblSetSevr(prec, UDF_ALARM, prec->udfs)` — and `recGblSetSevr`
3724 /// forwards a NULL message to `recGblSetSevrMsg`, which sets
3725 /// `namsg[0] = '\0'` (`recGbl.c:249-251,258-261`). So the C amsg for a
3726 /// UDF record is EMPTY, and pvxs then serves the `"UDF"` condition
3727 /// string for `alarm.message` (`iocsource.cpp:230-236`). Default `""`
3728 /// models exactly that.
3729 ///
3730 /// The sole exception in base is `mbboDirectRecord.c:191`, which raises
3731 /// `recGblSetSevrMsg(prec, UDF_ALARM, prec->udfs, "UDFS")` — a bespoke
3732 /// literal. That record overrides this to `"UDFS"`.
3733 fn udf_alarm_message(&self) -> &str {
3734 ""
3735 }
3736
3737 /// Whether the record's current `VAL` is undefined (UDF must
3738 /// stay set).
3739 ///
3740 /// C parity: `aiRecord.c:285` / `calcRecord.c::checkAlarms` /
3741 /// `int64inRecord.c:144` clear `UDF` **only** when the computed /
3742 /// read value is valid — `if (status == 0)` and, for floating
3743 /// records, only when `VAL` is not NaN. The framework owns
3744 /// `common.udf`; it calls `clears_udf()` to decide whether this
3745 /// record type clears UDF at all, then this method to decide
3746 /// whether the *value produced this cycle* is actually defined.
3747 ///
3748 /// Default: a floating `VAL` that is NaN (e.g. a calc
3749 /// divide-by-zero, or a soft input whose link read failed and
3750 /// left VAL un-updated) is undefined; everything else is defined.
3751 /// A record whose `val()` yields `None` (no primary value) is
3752 /// also treated as undefined.
3753 fn value_is_undefined(&self) -> bool {
3754 match self.val() {
3755 Some(EpicsValue::Double(v)) => v.is_nan(),
3756 Some(EpicsValue::Float(v)) => v.is_nan(),
3757 Some(_) => false,
3758 None => true,
3759 }
3760 }
3761
3762 /// Per-record alarm hook — evaluate record-type-specific alarms
3763 /// (STATE / COS / analog limit / SOFT) and accumulate them into
3764 /// `nsta`/`nsev` via `recGblSetSevr`.
3765 ///
3766 /// The framework centralises the generic alarm machinery (UDF
3767 /// check, `recGblResetAlarms` transfer, MS/MSI/MSS link-alarm
3768 /// inheritance). The record-type-specific severity logic that C
3769 /// puts in each record's `checkAlarms()` belongs here so a record
3770 /// can raise its own alarms without the framework hardcoding a
3771 /// per-type `match` on `record_type()`.
3772 ///
3773 /// `common` is the record's [`crate::server::record::CommonFields`]; implementations
3774 /// raise alarms with [`crate::server::recgbl::rec_gbl_set_sevr`]
3775 /// / [`crate::server::recgbl::rec_gbl_set_sevr_msg`].
3776 ///
3777 /// Default: no-op — records that have not yet migrated their
3778 /// `checkAlarms` logic here are still covered by the framework's
3779 /// legacy centralised `evaluate_alarms` match.
3780 fn check_alarms(&mut self, _common: &mut crate::server::record::CommonFields) {}
3781
3782 /// Return multi-input link field pairs: (link_field, value_field).
3783 /// Override in calc, calcout, sel, sub to return INPA..INPL → A..L mappings.
3784 fn multi_input_links(&self) -> &[(&'static str, &'static str)] {
3785 &[]
3786 }
3787
3788 /// The `(link_field, value_field)` pairs whose CONSTANT value this record's
3789 /// C `special()` RE-SEEDS on a runtime put to the link field —
3790 /// `recGblInitConstantLink(plink, DBF_DOUBLE, pvalue)` +
3791 /// `db_post_events(prec, pvalue, DBE_VALUE)` + `INAV = CON`
3792 /// (`calcoutRecord.c:373-378`, `sCalcoutRecord.c:513-518`,
3793 /// `aCalcoutRecord.c:533-538`).
3794 ///
3795 /// Without it a constant link is load-once dead state: `caput CO.INPB 7`
3796 /// stores the link text, the link layer then delivers nothing at process
3797 /// time (a constant link is not read), and `B` keeps its `.db`-load value
3798 /// forever.
3799 ///
3800 /// Declaring the pair is all a record does — the put path
3801 /// (`database::field_io::special_after_put`, the one `special(field, true)`
3802 /// owner) runs the load through
3803 /// [`crate::server::record::rec_gbl_init_constant_link`], the same owner the
3804 /// init seed uses, and posts the value field. A record cannot declare the
3805 /// pair and forget to implement the re-seed.
3806 ///
3807 /// Default: EMPTY — and that is the correct answer for every record whose C
3808 /// `special()` does NOT re-seed. `recGblInitConstantLink` appears inside a
3809 /// `special()` body in exactly FOUR record types across base and synApps
3810 /// calc — calcout, sCalcout, aCalcout, transform. Everywhere else (calc,
3811 /// sub, sel, aSub, seq, fanout, dfanout, swait, sseq, ao/bo/longout/…) it is
3812 /// called only from `init_record`, so those records seed once and never
3813 /// again, and they inherit this empty default.
3814 ///
3815 /// The overriding records list only the inputs C actually re-seeds:
3816 /// sCalcout/aCalcout guard with `fieldIndex <= INPL` (their string/array
3817 /// inputs are init-load only) and transform with
3818 /// `fieldIndex < transformRecordOUTA` (its OUT half is not an input).
3819 fn special_reseed_input_links(&self) -> &[(&'static str, &'static str)] {
3820 &[]
3821 }
3822
3823 /// The event mask of the `db_post_events` call in that record's `special()`
3824 /// re-seed arm — the C call sites do not agree:
3825 ///
3826 /// * calcout (`calcoutRecord.c:377`), sCalcout (`sCalcoutRecord.c:516`),
3827 /// aCalcout (`aCalcoutRecord.c:536`)
3828 /// post a literal `DBE_VALUE`.
3829 /// * transform (`transformRecord.c:719`) posts `DBE_VALUE | DBE_LOG`.
3830 ///
3831 /// Default: `DBE_VALUE`, the majority shape. Only consulted for the fields
3832 /// named by [`Self::special_reseed_input_links`].
3833 fn special_reseed_post_mask(&self) -> crate::server::recgbl::EventMask {
3834 crate::server::recgbl::EventMask::VALUE
3835 }
3836
3837 /// Every CONSTANT input link this record seeds ONCE, at `init_record` —
3838 /// the record's own `recGblInitConstantLink` / `dbLoadLinkArray` table,
3839 /// transcribed from its C.
3840 ///
3841 /// This is the OTHER half of the one rule the link layer enforces: a
3842 /// constant link delivers NOTHING at process time (`dbConstGetValue`,
3843 /// `dbConstLink.c:219-225`), so the ONLY way a `field(INPA,"5")` ever
3844 /// reaches `A` is this table, applied by the single init-seed owner
3845 /// `crate::server::database::PvDatabase::rec_gbl_init_constant_links`.
3846 /// A record that fetches an input link but declares no seed for it (swait,
3847 /// whose C uses `recDynLink` and seeds nothing) simply never sees the
3848 /// constant — which is what its C does.
3849 ///
3850 /// Default: none.
3851 fn constant_init_links(&self) -> Vec<ConstantInitLink> {
3852 Vec::new()
3853 }
3854
3855 /// The link field this record loads into its long-string VAL at init through
3856 /// C's `dbLoadLinkLS` — `"DOL"` for `lso` (`lsoRecord.c:82`), `"INP"` for
3857 /// `lsi` (its soft device support, `devLsiSoft.c:24`). `loadLS` is a lset
3858 /// entry of its own, so this is a SEPARATE table from
3859 /// [`Self::constant_init_links`], not a variant of it; the same init-seed
3860 /// owner runs both.
3861 ///
3862 /// Default: none — a record with no long-string VAL has no `loadLS` seed.
3863 fn constant_ls_link(&self) -> Option<&'static str> {
3864 None
3865 }
3866
3867 /// Apply the [`Self::constant_ls_link`] load, and return the resulting LEN.
3868 /// The record clamps the text at its own `SIZV` and runs C's init tail
3869 /// (`if (prec->len) { strcpy(prec->oval, prec->val); prec->olen = prec->len; }`,
3870 /// lsoRecord.c:92-95 / lsiRecord.c:85-88); the owner turns a non-zero LEN
3871 /// into `udf = FALSE`.
3872 fn apply_ls_load(&mut self, _load: crate::server::record::LsLoad) -> u32 {
3873 0
3874 }
3875
3876 /// Whether this record's CONSTANT input links deliver their value on
3877 /// EVERY process cycle instead of only at init.
3878 ///
3879 /// `false` for every record that fetches with a plain `dbGetLink`. `printf`
3880 /// is the one exception in the whole database: its `GET_PRINT` macro
3881 /// (`printfRecord.c:49-52`) tests `dbLinkIsConstant` and re-runs
3882 /// `recGblInitConstantLink` on every `doPrintf`, so a constant INP0..9
3883 /// really is re-read each cycle.
3884 fn constant_inputs_deliver_at_process(&self) -> bool {
3885 false
3886 }
3887
3888 /// The subset of [`Self::multi_input_links`] the framework should
3889 /// actually fetch this cycle, given an optional externally-resolved
3890 /// selector index (sel's NVL→SELN value, or `None` when no NVL link
3891 /// drove it). Default `None` = fetch every input link.
3892 ///
3893 /// C `selRecord.c::fetch_values` (lines 421-432) fetches ONLY `INP[SELN]`
3894 /// in `Specified` mode and all inputs otherwise; sel returns
3895 /// `Some(vec![INP[SELN]])` so the non-selected inputs are never read and
3896 /// raise no monitors or link-alarm SEVR.
3897 fn select_input_links(
3898 &self,
3899 _selector: Option<u16>,
3900 ) -> Option<Vec<(&'static str, &'static str)>> {
3901 None
3902 }
3903
3904 /// A `SIMM != NO` cycle substitutes only this record's INPUT STAGE — the
3905 /// rest of its `process()` still runs.
3906 ///
3907 /// C's SIML/SIMM/SIOL group has three shapes, and this hook names the third:
3908 ///
3909 /// * `readValue` (ai, bi, longin, …): the simulated read replaces the device
3910 /// read, which is the whole of the record's input; the framework performs
3911 /// the SIOL read and completes the cycle itself.
3912 /// * `writeValue` (ao, bo, …): the simulated write replaces the device write
3913 /// at the END of the body, so the body runs and only the output is
3914 /// redirected to SIOL.
3915 /// * swait (`swaitRecord.c:402-422`): the simulated read replaces
3916 /// `fetch_values()` **and** `calcPerform()` and nothing else — VAL comes
3917 /// from SIOL through SVAL, and the OOPT switch, `execOutput`, the monitors
3918 /// and the forward link all still run from the record's own `process()`.
3919 ///
3920 /// A record that returns `true` gets, on a simulated cycle: SIMM resolved
3921 /// from SIML, SIOL read into SVAL, `VAL = SVAL` and `UDF = FALSE` when that
3922 /// read succeeded (C `:417-420` — a failed read changes neither), SIMM_ALARM
3923 /// raised at SIMS *before* the body so it maximizes against whatever the
3924 /// body raises (C `:421`), no input-link fetch, and
3925 /// [`Self::set_simulation_active`] pushed before `process()`.
3926 fn simulation_substitutes_input_stage(&self) -> bool {
3927 false
3928 }
3929
3930 /// Land the scalar a simulated cycle read from SIOL (through SVAL, where
3931 /// the record has one) — C `readValue`'s assignment plus whatever the
3932 /// record's `process()` body then does with it, under the same
3933 /// `status == 0` gate the framework applies before calling this.
3934 ///
3935 /// The base records assign the value straight to VAL — `longinRecord.c:417`
3936 /// `prec->val = prec->sval;` — which is the default (`set_val`).
3937 ///
3938 /// `histogram` does NOT: `histogramRecord.c:385-386` lands it in SGNL
3939 /// (`prec->sgnl = prec->sval;`), and `process()` (`:218-219`,
3940 /// `if (status == 0) add_count(prec);`) bins that signal into the VAL
3941 /// bin-count array. Its VAL is the array, so a `set_val` of the scalar
3942 /// no-ops and the simulated record is frozen. It overrides.
3943 fn land_simulated_value(&mut self, value: EpicsValue) -> CaResult<()> {
3944 self.set_val(value)
3945 }
3946
3947 /// Whether the record's C `switch (prec->simm)` carries a `default:` arm
3948 /// that REFUSES a SIMM value outside its own menu:
3949 ///
3950 /// ```c
3951 /// default:
3952 /// recGblSetSevr(prec, SOFT_ALARM, INVALID_ALARM);
3953 /// status = -1;
3954 /// ```
3955 ///
3956 /// Every record in the framework has it — all 21 base records
3957 /// (`longinRecord.c:436-438` and its twins; `aaiRecord.c:381-384` writes the
3958 /// same arm as an `else if (prec->simm != menuYesNoNO)`) and `busy`
3959 /// (`busyRecord.c:409-413`, the `else` of its YES test).
3960 ///
3961 /// `swait` is the sole exception: `swaitRecord.c:407-422` is a plain
3962 /// `if (pwait->simm == menuYesNoNO) { … } else { /* SIMULATION MODE */ … }`,
3963 /// so every non-NO value — legal or not — simulates. It overrides to
3964 /// `false`.
3965 ///
3966 /// Consumed by [`resolve_sim_mode`](crate::server::recgbl::simm::resolve_sim_mode),
3967 /// the single owner of the SIMM dispatch.
3968 fn rejects_illegal_sim_mode(&self) -> bool {
3969 true
3970 }
3971
3972 /// Whether this record's `readValue` raises `SIMM_ALARM` AFTER its SIOL
3973 /// read instead of before it.
3974 ///
3975 /// `recGblSetSevr` is strict-greater, so the ORDER of the SIMM raise
3976 /// against the LINK_ALARM/INVALID that a failed `dbGetLink` raises inside
3977 /// itself (`dbLink.c:319` `setLinkAlarm`, reached from `:336`) decides an
3978 /// equal-severity tie. With `SIMS = INVALID` and a broken SIOL the record
3979 /// publishes whichever of the two was raised FIRST.
3980 ///
3981 /// The base records raise it first, at the top of the `case menuYesNoYES:`
3982 /// arm — `longinRecord.c:414` then `:416` — so they publish
3983 /// `STAT = SIMM_ALARM`. That is the default, and it is what the generic
3984 /// raise in `check_simulation_mode` performs.
3985 ///
3986 /// `mca` is the other order: `mcaRecord.c:1118` reads SIOL and only then
3987 /// `:1129` runs `recGblSetSevr(pmca, SIMM_ALARM, pmca->sims)`, past the
3988 /// `if/else` on SIMM, so the LINK_ALARM raised inside the read WINS the tie
3989 /// and C publishes `STAT = LINK_ALARM` with `AMSG = "field SIOL"`.
3990 /// (Read against `mca` at `687d563`; that tree records no pin.)
3991 ///
3992 /// `swait` is the same order but does not answer here — it has its own
3993 /// `input_stage` path in `check_simulation_mode`, which already reads
3994 /// before it raises (`swaitRecord.c:416` then `:421`).
3995 ///
3996 /// Declared per record rather than special-cased at the raise site so a
3997 /// record type carries its own C ordering; overriding to `true` is the
3998 /// whole opt-in.
3999 fn raises_simm_after_read(&self) -> bool {
4000 false
4001 }
4002
4003 /// This cycle's simulation state, pushed by the framework before
4004 /// `process()` — the twin of [`Self::set_fetch_gate_failed`], and only for a
4005 /// record that declares [`Self::simulation_substitutes_input_stage`].
4006 ///
4007 /// It is pushed on EVERY cycle of such a record (`false` included), so the
4008 /// flag cannot survive the cycle it belongs to. The record uses it to skip
4009 /// exactly what C's simulation branch skips — for swait, `fetch_values()`
4010 /// (through [`Self::select_input_links`]) and `calcPerform()`.
4011 fn set_simulation_active(&mut self, _active: bool) {}
4012
4013 /// How C's `fetch_values()` for this record type reacts to a link read
4014 /// that fails. Drives the framework's [`Self::multi_input_links`] fetch
4015 /// loop; see [`InputFetchPolicy`]. Default: [`InputFetchPolicy::ReadAll`].
4016 ///
4017 /// It governs the [`Self::multi_input_links`] loop ONLY.
4018 /// [`Self::string_input_links`] is C's *second*, separately-gated fetch
4019 /// loop and never participates in this policy.
4020 fn input_fetch_policy(&self) -> InputFetchPolicy {
4021 InputFetchPolicy::ReadAll
4022 }
4023
4024 /// String-valued input links: `(link_field, value_field)` pairs read as
4025 /// DBR_STRING, C `sCalcoutRecord.c::fetch_values` (890-941) — the SECOND
4026 /// loop of that function, over `INAA`..`INLL` → `AA`..`LL`:
4027 ///
4028 /// ```c
4029 /// for (i=0, plink=&pcalc->inaa, psvalue=pcalc->strs; i<STRING_MAX_FIELDS; ...) {
4030 /// ...
4031 /// if (((field_type==DBR_CHAR) || (field_type==DBR_UCHAR)) && nelm>1) {
4032 /// status = dbGetLink(plink, field_type, tmpstr, 0, &nelm);
4033 /// epicsStrSnPrintEscaped(*psvalue, STRING_SIZE-1, tmpstr, strlen(tmpstr));
4034 /// } else {
4035 /// status = dbGetLink(plink, DBR_STRING, *psvalue, 0, 0);
4036 /// }
4037 /// if (!RTN_SUCCESS(status))
4038 /// epicsSnprintf(*psvalue, STRING_SIZE-1, "%s:fetch(%s) failed", pcalc->name, sFldnames[i]);
4039 /// }
4040 /// return(0);
4041 /// ```
4042 ///
4043 /// Three properties this loop does NOT share with [`Self::multi_input_links`],
4044 /// which is why it is a separate list rather than more entries in that one:
4045 ///
4046 /// 1. **Ungated.** It ends in `return(0)` — a failing string link never
4047 /// makes `fetch_values` non-zero, so it cannot suppress the record body.
4048 /// The record's single [`Self::input_fetch_policy`] describes the numeric
4049 /// loop (`AbortOnFirstFailure` for scalcout) and cannot also describe this
4050 /// one.
4051 /// 2. **A failed read still writes the field** — with the diagnostic text
4052 /// `"<record>:fetch(<FIELD>) failed"`, not with the previous value.
4053 /// 3. **A `DBF_CHAR`/`DBF_UCHAR` array source is read as text**, C-escaped
4054 /// (`epicsStrSnPrintEscaped`), which is how a >40-char string reaches a
4055 /// string calc; every other source type converts as DBR_STRING.
4056 ///
4057 /// The value is delivered through [`Self::put_field_internal`], so the
4058 /// target field's declared `DbFieldType` performs the final coercion.
4059 fn string_input_links(&self) -> &'static [(&'static str, &'static str)] {
4060 &[]
4061 }
4062
4063 /// Input links this record reads at OUTPUT time instead of during the
4064 /// input-fetch phase: `(link_name_field, value_field)` pairs. The framework
4065 /// reads each configured link immediately before the OUT write, and ONLY on
4066 /// a cycle where the output actually fires ([`Self::should_output`] and no
4067 /// IVOA veto), then writes the value into `value_field` via
4068 /// [`Self::put_field`]; a failed read leaves the field alone.
4069 ///
4070 /// C `swaitRecord.c::execOutput` (763-772) does exactly this for `DOL`:
4071 ///
4072 /// ```c
4073 /// if (pwait->dopt) { /* DOPT = "Use DOL" */
4074 /// if (!pwait->dolv) { /* DOL PV connected */
4075 /// oldDold = pwait->dold;
4076 /// recDynLinkGet(&pcbst->caLinkStruct[DOL_INDEX], &(pwait->dold), ...);
4077 /// if (pwait->dold != oldDold)
4078 /// db_post_events(pwait, &pcbst->pwait->dold, DBE_VALUE);
4079 /// }
4080 /// outValue = pwait->dold;
4081 /// }
4082 /// ```
4083 ///
4084 /// The timing is the point: the value written out is the one the link holds
4085 /// at output time (ODLY delay-end included), and a cycle whose output does
4086 /// not fire never refreshes — or posts — the field. Fetching such a link in
4087 /// the normal input phase would do both. Default: none.
4088 fn output_time_input_links(&self) -> &'static [(&'static str, &'static str)] {
4089 &[]
4090 }
4091
4092 /// The value the framework writes to the OUT link. The single owner of
4093 /// "what goes out", shared by the soft-OUT write, the async-completion
4094 /// write and the simulated SIOL redirect.
4095 ///
4096 /// The default is the C staging convention: the record computed the output
4097 /// into `OVAL` during `process()` (`calcout`/`ao`/`bo`/...), falling back to
4098 /// `VAL` for records that have no `OVAL`. Override when the record's C
4099 /// composes the output value at *output* time rather than staging it — e.g.
4100 /// swait, whose `execOutput` (`swaitRecord.c:763-772`) picks between `VAL`
4101 /// and the just-fetched `DOLD` and whose `OVAL` field is C's "Old Value"
4102 /// (the previous VAL, used only by the OOPT test), not an output stage.
4103 fn output_link_value(&self) -> Option<EpicsValue> {
4104 self.get_field("OVAL").or_else(|| self.val())
4105 }
4106
4107 /// Return multi-output link field pairs: (link_field, value_field).
4108 /// Override in transform to return OUTA..OUTP → A..P mappings.
4109 fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
4110 &[]
4111 }
4112
4113 /// The record's C soft device support write-buffer switch for a
4114 /// multi-output pair: given the pair's staged value (the value field
4115 /// named by [`Self::multi_output_links`]) and the RESOLVED TARGET
4116 /// metadata, return the buffer C would actually put.
4117 ///
4118 /// C's soft device supports do not blindly write one field: they read
4119 /// the target's DBF type and element count and pick a buffer from them —
4120 /// `devaCalcoutSoft.c::write_acalcout` (75-87) picks
4121 /// `nelm == 1 ? &scalar : array`, `devsCalcoutSoft.c::write_scalcout`
4122 /// (66-144) routes a string-class target to the computed string, a
4123 /// `CHAR`/`UCHAR` array to the string's bytes, and everything else to
4124 /// the numeric. The framework resolves the target
4125 /// ([`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase))
4126 /// and hands it here; the record reproduces its device support's switch.
4127 ///
4128 /// Default: write the staged value unchanged (no device-support switch).
4129 fn multi_output_buffer(
4130 &self,
4131 link_field: &str,
4132 staged: EpicsValue,
4133 target: &OutTarget,
4134 ) -> EpicsValue {
4135 let _ = (link_field, target);
4136 staged
4137 }
4138
4139 /// The buffer to put on an OUT link the record drives itself, chosen from
4140 /// the RESOLVED TARGET — the no-staged-value sibling of
4141 /// [`Self::multi_output_buffer`].
4142 ///
4143 /// C `sseqRecord.c::processCallback` (708-795) is the case: the value a
4144 /// step forwards is not one field but a *switch on the destination*
4145 /// (`dbGetLinkDBFtype(&lnk)` / `dbGetNelements(&lnk)`), taken at fire
4146 /// time — the string view `s` for a string-class target, the double view
4147 /// `dov` for a numeric one, `s`'s bytes for a `CHAR`/`UCHAR` array, and
4148 /// **no put at all** for a target whose type does not resolve (C's
4149 /// `default: break`). `None` is that no-put: the caller issues no write.
4150 ///
4151 /// The record calls this ITSELF, on the target
4152 /// [`ProcessAction::ResolveOutTarget`] handed it before `process()` — one
4153 /// decision, made before anything is issued, because the same switch
4154 /// decides more than the buffer (sseq: whether a `WAITn` put-callback goes
4155 /// out, and hence whether `WTGn` is raised). See
4156 /// [`Self::set_resolved_out_target`].
4157 ///
4158 /// Default: `None`. Only a record that resolves its own OUT target reaches
4159 /// this, and it must override.
4160 fn typed_output_buffer(&self, link_field: &str, target: &OutTarget) -> Option<EpicsValue> {
4161 let _ = (link_field, target);
4162 None
4163 }
4164
4165 /// Receive the RESOLVED target of an OUT link the record asked to have
4166 /// resolved before this cycle's `process()`
4167 /// ([`ProcessAction::ResolveOutTarget`]).
4168 ///
4169 /// C resolves an OUT link's DBF class OUTSIDE the put — `checkLinks` caches
4170 /// it in the record (`sseqRecord.c:203-240`) — so `processCallback` can make
4171 /// ONE decision from it: which view goes on the wire, AND whether a
4172 /// put-callback is issued (hence whether `waiting` is raised). Its
4173 /// `default:` arm (`:790`) does neither. A record that learns the class only
4174 /// from inside the framework's put path cannot keep those two halves
4175 /// together: it has to raise `waiting` first and find out afterwards that no
4176 /// put was made. This hook is that cached class — the record decides, then
4177 /// acts.
4178 ///
4179 /// Default: no-op. Only a record that emits the action reaches this.
4180 fn set_resolved_out_target(&mut self, link_field: &str, target: OutTarget) {
4181 let _ = (link_field, target);
4182 }
4183
4184 /// The `dbrType` this record's framework-run input read on `link_field`
4185 /// asks the SOURCE for — the READ twin of [`Self::typed_output_buffer`],
4186 /// and C's `dbGetLink` second argument. Consulted by every framework
4187 /// fetch path: the pre-input [`ProcessAction::ReadDbLink`] stage, the
4188 /// single-INP soft fetch, the closed-loop DOL fetch, the multi-input
4189 /// loop and the SIOL simulation read.
4190 ///
4191 /// `source` is the far end of the input link as C's
4192 /// `dbGetLinkDBFtype`/`dbGetNelements` report it (the same lset accessors
4193 /// the OUT side uses — sseq asks them of `dol` at `sseqRecord.c:641` and of
4194 /// `lnk` at `:709`), resolved by the framework
4195 /// ([`PvDatabase::resolve_out_target`](crate::server::database::PvDatabase)).
4196 ///
4197 /// `None` means C's `default: break` — **no read at all**, the arm a source
4198 /// class the record's switch does not name falls to (an unresolvable /
4199 /// constant / disconnected source, and sseq's un-cased `DBF_INT64`). The
4200 /// link is left untouched and no LINK alarm is raised, exactly as C's
4201 /// skipped `dbGetLink` call leaves `status` alone.
4202 ///
4203 /// Default: [`LinkReadAs::Native`] — the source's native value, coerced at
4204 /// the target field's own put boundary.
4205 fn input_link_read_as(&self, link_field: &str, source: &OutTarget) -> Option<LinkReadAs> {
4206 let _ = (link_field, source);
4207 Some(LinkReadAs::Native)
4208 }
4209
4210 /// Return the name of the output event (`OEVT`) to post this cycle, or
4211 /// `None`. The event-subsystem twin of the OUT write: a downstream
4212 /// `SCAN="Event"` / `EVNT="<name>"` record is woken each time the record
4213 /// drives output. Mirrors C `calcout`/`sCalcout`/`aCalcout` `execOutput`,
4214 /// which calls `postEvent(epvt)` / `post_event(oevt)` immediately after
4215 /// `writeValue` in every OUT-driving branch.
4216 ///
4217 /// The override MUST fold in the record's own output-fire decision
4218 /// (`should_output()` for `calcout`; the cached OOPT/calc-fail/ODLY
4219 /// decision for `sCalcout`/`aCalcout`) and return `None` when output did
4220 /// not fire or when `OEVT` is unset. The framework adds the only gate the
4221 /// record cannot see — the IVOA `Don't_drive` veto on an INVALID cycle —
4222 /// so the post fires on exactly the cycles the OUT write does. Numeric
4223 /// `OEVT` (DBF_USHORT) stringifies to match the `EVNT` ingest; a string
4224 /// `OEVT` (DBF_STRING) is the event name verbatim.
4225 fn output_event(&self) -> Option<String> {
4226 None
4227 }
4228
4229 /// Internal field write that bypasses read-only checks.
4230 /// Used by the framework to write values from ReadDbLink actions
4231 /// into fields that are normally read-only (e.g., epid.CVAL).
4232 /// Default implementation delegates to put_field().
4233 ///
4234 /// On the `ReadDbLink` path this is also where a pvalink NTEnum
4235 /// carrier ([`EpicsValue::EnumWithChoices`]) is resolved. The
4236 /// dbrType-blind link resolver produces it for an NTEnum source;
4237 /// pvxs `pvaGetValue` (`pvxs/ioc/pvalink_lset.cpp:330-360`) picks
4238 /// label-vs-index by the TARGET field's dbrType — only a DBR_STRING
4239 /// target gets the `choices[index]` label, every other type takes
4240 /// the numeric index. Route it through [`EpicsValue::convert_to`]
4241 /// (the single value-coercion owner) against the target field's
4242 /// `db_field_type`, so the transient carrier is consumed before any
4243 /// record `put_field` / storage / wire path can see it. The
4244 /// single-INP→VAL apply path reaches the same `convert_to` via
4245 /// `set_val`'s `TypeMismatch` auto-coerce.
4246 fn put_field_internal(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
4247 put_field_internal_default(self, name, value)
4248 }
4249
4250 /// Return pre-process actions (ReadDbLink) that the framework should
4251 /// execute BEFORE calling process(). This is called once per cycle.
4252 /// Default returns empty. Override in records that need link reads
4253 /// to be available during process().
4254 fn pre_process_actions(&mut self) -> Vec<ProcessAction> {
4255 Vec::new()
4256 }
4257
4258 /// Return actions the framework must execute BEFORE the input-link
4259 /// (`multi_input_links`, INP -> value-field) fetch for this cycle.
4260 ///
4261 /// This is strictly earlier than [`Self::pre_process_actions`]: the
4262 /// framework resolves input links *before* it calls
4263 /// `pre_process_actions`, so an action that must affect what an
4264 /// input link reads cannot be expressed there.
4265 ///
4266 /// The motivating case is the epid record's `devEpidSoftCallback`
4267 /// DB-type TRIG link: C `devEpidSoftCallback.c:120-132` writes the
4268 /// readback-trigger link with `dbPutLink` — which synchronously
4269 /// processes the triggered source chain — and only *then*
4270 /// (`devEpidSoftCallback.c:151`) does `dbGetLink(&pepid->inp, ...)`
4271 /// read `CVAL`. The trigger write therefore has to land before the
4272 /// `INP -> CVAL` fetch, in the same process pass.
4273 ///
4274 /// Called once per cycle, while a record write lock is held; the
4275 /// framework executes the returned actions (currently `WriteDbLink`
4276 /// and `ReadDbLink`) and then performs the input-link fetch.
4277 /// Default returns empty.
4278 fn pre_input_link_actions(&mut self) -> Vec<ProcessAction> {
4279 Vec::new()
4280 }
4281
4282 /// Called by the framework immediately before `process()` to push a
4283 /// read-only snapshot of framework-owned [`crate::server::record::CommonFields`] state
4284 /// ([`ProcessContext`]) that the record's `process()` needs to see.
4285 ///
4286 /// The framework owns `RecordInstance.common`; a record `process()`
4287 /// only gets `&mut self`. C records read `dbCommon` directly — e.g.
4288 /// `epidRecord.c:195` checks `pepid->udf` at the top of `process()`,
4289 /// `timestampRecord.c:90` branches on `ptimestamp->tse`. This hook
4290 /// is the controlled equivalent: a record that needs `udf`/`phas`/
4291 /// `tse`/`tsel` during `process()` overrides this to stash the
4292 /// values into its own fields.
4293 ///
4294 /// Additive, framework-set-hook pattern (same shape as
4295 /// [`Record::set_device_did_compute`]). Default: ignore — most
4296 /// records never need common state during `process()`.
4297 fn set_process_context(&mut self, _ctx: &ProcessContext) {}
4298
4299 /// Called by the framework immediately before `process()` to say whether
4300 /// this cycle is the record's OWN scheduled re-entry — the
4301 /// [`ProcessAction::ReprocessAfter`] timer firing, or a put-notify
4302 /// completion — rather than a fresh put / scan / forward-link process.
4303 ///
4304 /// C hands records this distinction for free: `callbackRequestDelayed`
4305 /// dispatches to a callback function of the record's own, never to
4306 /// `process()`. A port that models the timer with `ReprocessAfter` has
4307 /// both events arriving at the same entry point, so a record whose C
4308 /// original keeps the two paths separate — throttle's `delayFuncCallback`
4309 /// -> `valuePut` versus `process()` -> `enterValue`
4310 /// (`throttleRecord.c:518-613`) — cannot route the cycle without knowing
4311 /// which one it is. The framework already knows; this hook is what it
4312 /// tells the record, so the record never has to infer it from a clock
4313 /// reading (which a mid-flight interval change silently falsifies).
4314 ///
4315 /// Records that hold PACT across their delay (calcout / scalcout /
4316 /// acalcout / swait / sseq ODLY) already disambiguate through PACT and do
4317 /// not need this. Additive, framework-set-hook pattern (same shape as
4318 /// [`Record::set_process_context`]). Default: ignore.
4319 fn set_process_continuation(&mut self, _continuation: bool) {}
4320
4321 /// Called by the framework once for every [`ProcessAction::WriteDbLink`]
4322 /// this record emitted, reporting the value it carried and whether the
4323 /// put failed — the port's stand-in for the `dbPutLink` return C reads
4324 /// inline.
4325 ///
4326 /// A record whose C original derives a field from that return needs it:
4327 /// throttle's `STS` is `throttleSTS_SUC` only on success and its `SENT`
4328 /// advances only then (`throttleRecord.c:564-575`). Without this the
4329 /// record can only commit its own intent, which reports Success for a put
4330 /// that never landed. The framework raises the LINK/INVALID alarm either
4331 /// way; this is the record-owned half.
4332 ///
4333 /// Called after the put, while the cycle's monitor snapshot is still
4334 /// ahead, so a field set here is posted by this cycle. Every emitted
4335 /// action reports exactly once, an unresolvable link included (reported
4336 /// as failed). Additive, framework-set-hook pattern (same shape as
4337 /// [`Record::set_process_context`]). Default: ignore — most records
4338 /// derive nothing from the put's result, as their C originals do not.
4339 fn set_out_link_write_status(
4340 &mut self,
4341 _link_field: &'static str,
4342 _value: &EpicsValue,
4343 _failed: bool,
4344 ) {
4345 }
4346
4347 /// Called once by the framework when the record is registered
4348 /// (`add_record`), delivering the record its own canonical name plus a
4349 /// cycle-free [`crate::server::database::AsyncDbHandle`] for driving
4350 /// async-side updates from OUTSIDE a `process()` cycle.
4351 ///
4352 /// The handle wraps a `Weak` reference to the database, so a record
4353 /// that stashes it creates no ownership cycle (the database owns the
4354 /// record; a stored strong handle would leak it). It is the controlled
4355 /// equivalent of C device support capturing `precord` plus the
4356 /// dbCommon scan lock for an out-of-band `db_post_events` /
4357 /// `callbackRequest`: e.g. the asyn TRACE/exception callback posts
4358 /// trace-flag fields immediately from the driver thread, and AQR
4359 /// cancels a queued I/O re-entry — neither happens inside `process()`.
4360 ///
4361 /// The in-band counterpart for a record's *own* process cycle is the
4362 /// completion-driven [`ProcessAction`] family
4363 /// ([`ProcessAction::WriteDbLinkNotify`],
4364 /// [`ProcessAction::CancelReprocess`],
4365 /// [`ProcessAction::ReprocessAfter`]); this hook exists for the
4366 /// out-of-band path that has no `process()` return to ride on.
4367 ///
4368 /// Additive, framework-set-hook pattern (same shape as
4369 /// [`Self::set_process_context`]). Default: ignore — most records do
4370 /// no out-of-band async posting.
4371 fn set_async_context(&mut self, _name: String, _db: crate::server::database::AsyncDbHandle) {}
4372
4373 /// Framework init hook: called once at record load *after* the common
4374 /// link fields (`INP`/`OUT`/`FLNK`/...) have been resolved and the
4375 /// `init_record` passes have run, with the record's resolved
4376 /// [`CommonFields`](crate::server::record::CommonFields).
4377 ///
4378 /// This is the seam for records that classify their links into status
4379 /// diagnostics at init the way C `init_record` does (e.g. calcout's
4380 /// `INAV..INUV`/`OUTV` `menu(calcoutINAV)` checkLinks loop): a record's
4381 /// *common* link strings (`OUT` is a common field, not a record field)
4382 /// are invisible to [`Self::set_async_context`] — which runs at
4383 /// `add_record`, *before* the common fields are applied — and to
4384 /// `init_record`, which carries no `CommonFields`. The record captures
4385 /// whichever common links it needs here so a passive, never-processed
4386 /// record already exposes its link status. Records whose links are all
4387 /// record-owned (e.g. sseq DOLn/LNKn) do not need this hook.
4388 ///
4389 /// Additive, framework-set-hook pattern. Default: ignore.
4390 fn init_links(&mut self, _common: &crate::server::record::CommonFields) {}
4391
4392 /// Called by the framework before process() to indicate whether device
4393 /// support's read() already performed the record's compute step.
4394 /// Override in records that have a built-in compute (e.g., epid PID)
4395 /// to skip it when device support already ran it.
4396 /// Default: ignore.
4397 fn set_device_did_compute(&mut self, _did_compute: bool) {}
4398
4399 /// Whether this record has a raw-to-engineering (`RVAL → VAL`)
4400 /// `convert()` step that must be skipped on a `Soft Channel` input.
4401 ///
4402 /// C `devAiSoft.c:65` `read_ai` (and the other soft-channel input
4403 /// `read_xxx`) always returns 2 ("don't convert"), so `aiRecord.c`'s
4404 /// `if (status==0) convert(prec)` is bypassed for a `Soft Channel`
4405 /// input record. The framework expresses this by calling
4406 /// [`Record::set_device_did_compute`]`(true)` on the record before
4407 /// `process()`.
4408 ///
4409 /// This hook exists so the framework only suppresses `convert()` —
4410 /// NOT a record's entire built-in compute. Records like `epid` also
4411 /// override `set_device_did_compute` but interpret it as "skip the
4412 /// whole compute step" (the PID loop); those records have no
4413 /// `RVAL → VAL` convert and MUST keep the default `false` so a
4414 /// `Soft Channel` `epid` still runs `do_pid()` in `process()`.
4415 ///
4416 /// Default `false`: a record is only opted into the soft-channel
4417 /// convert-skip when it explicitly returns `true`.
4418 fn soft_channel_skips_convert(&self) -> bool {
4419 false
4420 }
4421
4422 /// Whether this output record's forward `VAL → RVAL` `convert()` must be
4423 /// SKIPPED on a process cycle where VAL is still undefined (`UDF != 0`) and
4424 /// no value source ran.
4425 ///
4426 /// C's output records take an early `goto CONTINUE` before `convert()` when
4427 /// the record is undefined and no value was sourced this cycle:
4428 ///
4429 /// ```c
4430 /// /* mbboRecord.c:199-217 (and ao/bo/mbboDirect alike) */
4431 /// if (!pact) {
4432 /// if (!dbLinkIsConstant(&prec->dol) && omsl == closed_loop) {
4433 /// ... prec->val = <DOL>; /* value sourced -> udf cleared */
4434 /// }
4435 /// else if (prec->udf) {
4436 /// recGblSetSevr(prec, UDF_ALARM, prec->udfs);
4437 /// goto CONTINUE; /* skip udf=FALSE AND convert() */
4438 /// }
4439 /// prec->udf = FALSE;
4440 /// convert(prec); /* VAL -> RVAL */
4441 /// }
4442 /// ```
4443 ///
4444 /// So a `caput REC.RVAL 1` on a bare `record(mbbo,"M"){}` (UDF still 1, no
4445 /// VAL put, no closed-loop DOL) leaves RVAL at the client value: `convert`
4446 /// never runs to recompute `RVAL = VAL(=0)`. Verified on the compiled
4447 /// softIoc — bare RVAL put reads back the put value; after a VAL put clears
4448 /// UDF, the next RVAL put IS overwritten by `convert`.
4449 ///
4450 /// The framework consults this before `process()`: an opted-in output record
4451 /// with `UDF != 0` and no value source this cycle is told
4452 /// [`Self::set_device_did_compute`]`(true)` so its `process()` skips the
4453 /// forward convert. A VAL put (UDF cleared in `field_io`) or a closed-loop
4454 /// DOL fetch (UDF cleared at the DOL-apply site) leaves `UDF == 0`, so the
4455 /// convert runs exactly as C's fall-through does.
4456 ///
4457 /// Default `false`. The rest of C's output family (ao/bo/mbboDirect) shares
4458 /// the same `goto CONTINUE`; they are a separate change and stay opted out
4459 /// here.
4460 fn skips_forward_convert_when_undefined(&self) -> bool {
4461 false
4462 }
4463
4464 /// C `mbboRecord.c:210-221` / `mbboDirectRecord.c:190-202` — the same
4465 /// `else if (prec->udf) goto CONTINUE` that skips the forward `convert()`
4466 /// ALSO jumps past the pre-output `recGblGetTimeStampSimm` call. So a soft
4467 /// (synchronous) mbbo/mbboDirect that is still UNDEFINED never stamps TIME
4468 /// on the first-pass output stage: the only other stamp after `CONTINUE:`
4469 /// is guarded by `if (pact)` (mbboRecord.c:256-258), which fires on
4470 /// ASYNCHRONOUS completion re-entry only. A sync UDF record therefore keeps
4471 /// TIME at the EPICS epoch ("never processed") until a VAL put clears UDF.
4472 ///
4473 /// Contrast ao/bo/longout/int64out/stringout: their `if (!pact)` block
4474 /// calls `recGblGetTimeStampSimm` UNCONDITIONALLY (aoRecord.c:192,
4475 /// boRecord.c:215), so they stamp even while undefined and do NOT opt in.
4476 ///
4477 /// The framework consults this at the synchronous output-stage stamp
4478 /// (`processing.rs` `process_record_with_links_inner`, the pre-output
4479 /// `apply_timestamp`): an opted-in record with `UDF != 0` skips that stamp,
4480 /// mirroring C's `goto CONTINUE`. The async-completion stamp
4481 /// (`complete_async_record_inner`) stays unconditional, matching C's
4482 /// `if (pact)` re-stamp on async devices.
4483 ///
4484 /// This is a SEPARATE hook from [`Self::skips_forward_convert_when_undefined`]:
4485 /// mbboDirect's VAL is bit-derived and does NOT opt into the convert-skip,
4486 /// yet it DOES share this timestamp-skip. Do not conflate the two.
4487 ///
4488 /// Default `false`. Only mbbo/mbboDirect carry the `goto CONTINUE`
4489 /// timestamp-skip in C; every other record stays opted out.
4490 fn skips_timestamp_when_undefined(&self) -> bool {
4491 false
4492 }
4493}
4494
4495/// The body of [`Record::put_field_internal`] — the framework's internal write
4496/// path — as a free function, so a record that needs to observe an internal
4497/// write can WRAP it instead of re-implementing it.
4498///
4499/// A record overriding `put_field_internal` and ending in `self.put_field(..)`
4500/// silently drops the coercion below for every field it does not special-case.
4501/// Calling this instead keeps the one owner of that coercion.
4502///
4503/// Input-link / internal delivery coerces the source to the target field's
4504/// stored type before `put_field`, mirroring C `dbGetLink(DBF_<target>)`: the
4505/// link layer converts any numeric source to the requested type, so a record's
4506/// typed `put_field` arm never sees a mismatched type. This covers every
4507/// `ReadDbLink` target by construction (e.g. a `compress` INP from a `DBF_LONG`
4508/// record delivers a `Long`/`LongArray` that must become `Double`/`DoubleArray`
4509/// for the Double-only VAL arm, which otherwise drops it and never advances the
4510/// buffer). An `EnumWithChoices` carrier is always collapsed to a bare index by
4511/// `convert_to`, even when the target is already `Enum`.
4512/// Render a put request in the DESTINATION field's shape — C `dbPut`'s value
4513/// branch (`dbAccess.c:1350-1367`, tag `R7.0.10`) expressed as a value
4514/// transform, and the single owner of both of its directions.
4515///
4516/// C never asks a record to accept a shape. `dbPut` copies `nRequest` elements
4517/// through `dbPutConvertRoutine` into the field, so a ONE-element request over
4518/// a `special(SPC_DBADDR)` destination lands element 0 of a BUFFER (which is
4519/// how pvxs's `putScalar` — `doDbPut(chan, dbr, &value, 1)`,
4520/// `iocsource.cpp:599-601` — writes a one-element channel), and a
4521/// MULTI-element request over a scalar destination is clamped to `no_elements`
4522/// at `:1360` and lands element 0 of a SCALAR. This port hands the record one
4523/// `EpicsValue` whose variant its `put_field` arm matches on, so C's two arms
4524/// have to be a rendering, and one every client put shares: the two PVA
4525/// servers each carried the scalar-to-buffer half themselves, and a third
4526/// caller would have needed its own copy.
4527///
4528/// This is the `dbPut` contract only — [`dbput_coerce_value`] is the entry
4529/// that applies it. Internal delivery has [`link_value_in_field_shape`], and
4530/// the two are separate functions rather than one with a flag because they
4531/// disagree about the same request: one sample into a `compress` VAL is a
4532/// one-element buffer to `dbPut` and a `compress_scalar` sample to a link.
4533///
4534/// An `EnumWithChoices` carrier is left alone in both — it is a transient link
4535/// payload, not a stored value, and `convert_to` collapses it to a bare index.
4536/// `shaped_destination` carries the rest of the shared rule.
4537pub fn put_value_in_field_shape<R: Record + ?Sized>(
4538 record: &R,
4539 field: &str,
4540 value: EpicsValue,
4541) -> EpicsValue {
4542 let Some(dest_is_array) = shaped_destination(record, field, &value) else {
4543 return value;
4544 };
4545 match (dest_is_array, value.is_array()) {
4546 // C's SCALAR arm: the clamp at `:1360` leaves one element.
4547 (false, true) => value.first_element().unwrap_or(value),
4548 // C's ARRAY arm at `nRequest == 1` — except into a long-string field.
4549 // C's `cvt_dbaddr` re-types those five to `DBF_STRING`
4550 // (`lsiRecord.c:127-134`), so their conversion row is
4551 // `putStringString`, a byte copy of the text, and this port carries
4552 // that text as the value itself — `lsi.VAL` as a `String`, `printf.VAL`
4553 // as a `CharArray`. Wrapping a `String` request into a one-element
4554 // `StringArray` hands that row an array and the record a variant no
4555 // `put_field` arm takes.
4556 (true, false) if !is_long_string_field(record, field) => {
4557 one_element_buffer(&value).unwrap_or(value)
4558 }
4559 _ => value,
4560 }
4561}
4562
4563/// `field` is one of the record's long-string fields (`lsi`/`lso` VAL+OVAL,
4564/// `printf` VAL), matched as [`Record::long_string_fields`] specifies.
4565fn is_long_string_field<R: Record + ?Sized>(record: &R, field: &str) -> bool {
4566 record
4567 .long_string_fields()
4568 .iter()
4569 .any(|f| f.eq_ignore_ascii_case(field))
4570}
4571
4572/// The same question for INTERNAL DELIVERY — an input link or device support
4573/// handing a record a value — which has only C's scalar arm.
4574///
4575/// C's link layer asks for exactly ONE element (`dbGetLink(..., nRequest =
4576/// NULL)`), so `dbGet` converts the field at offset 0 and the record sees a
4577/// scalar: a waveform INP into an `ai.VAL` lands `wf[0]` rather than being
4578/// dropped by the typed `put_field` arm it does not match.
4579///
4580/// There is no array arm to render into, and that is the whole reason this is
4581/// a second entry rather than a flag on [`put_value_in_field_shape`]: internal
4582/// delivery is not a `dbPut`. A `compress` INP delivering ONE sample means
4583/// `compress_scalar` — the running `cvb` accumulator — and rendering it as a
4584/// one-element buffer runs `push_array`'s array algorithm instead, which
4585/// writes the N clamp back into the record.
4586pub fn link_value_in_field_shape<R: Record + ?Sized>(
4587 record: &R,
4588 field: &str,
4589 value: EpicsValue,
4590) -> EpicsValue {
4591 match shaped_destination(record, field, &value) {
4592 Some(false) if value.is_array() => value.first_element().unwrap_or(value),
4593 _ => value,
4594 }
4595}
4596
4597/// `Some(dest_is_array)` when `field` has a shape this row may render into,
4598/// `None` when it must be left alone.
4599///
4600/// The destination test is the field's CURRENT VALUE SHAPE, not
4601/// [`FieldDeclaration::field_is_dbaddr`]. `mbbo.VAL` is `special(SPC_DBADDR)`
4602/// and stored as a scalar; wrapping it would put a one-element array in front
4603/// of the menu row that owns that put.
4604///
4605/// The exempt shape is a `CharArray` into a `DBF_STRING` field: that is the
4606/// dbChannel `$` char view of a string field, decoded by `convert_to`.
4607fn shaped_destination<R: Record + ?Sized>(
4608 record: &R,
4609 field: &str,
4610 value: &EpicsValue,
4611) -> Option<bool> {
4612 let target = record
4613 .get_field(field)
4614 .map(|v| v.db_field_type())
4615 .or_else(|| super::record_instance::declared_field_type_of(record, field));
4616 if matches!(value, EpicsValue::CharArray(_)) && target == Some(DbFieldType::String) {
4617 return None;
4618 }
4619 Some(record.get_field(field).is_some_and(|v| v.is_array()))
4620}
4621
4622/// The client `dbPut` entry to the write-side converter: C `dbPut`'s value
4623/// branch in full — the destination-shape arm
4624/// ([`put_value_in_field_shape`]) and then the `dbPutConvertRoutine` row for
4625/// the request that arm produced ([`coerce_put_value`]).
4626///
4627/// Separate from `coerce_put_value` because the two ways a value enters a
4628/// record field have different contracts, and one function that renders the
4629/// shape for both means two things by context. Only a client `dbPut` gets the
4630/// array arm; internal delivery calls `coerce_put_value` with a request whose
4631/// shape [`link_value_in_field_shape`] already settled, and
4632/// `RecordInstance::put_declared_override` calls it for a destination that has
4633/// no shape at all (a shadow cell for a field the record does not serve).
4634///
4635/// It is UNCONDITIONAL. Its caller must not skip it when the request already
4636/// carries the destination's DBF: that skip is what let a scalar reach a
4637/// buffer field unrendered, so that `histogram.VAL` had to grow a scalar arm
4638/// of its own to accept what `dbPut` converts for every other array field.
4639pub fn dbput_coerce_value<R: Record + ?Sized>(
4640 record: &R,
4641 field: &str,
4642 target: DbFieldType,
4643 value: EpicsValue,
4644) -> CaResult<Converted> {
4645 let value = put_value_in_field_shape(record, field, value);
4646 coerce_put_value(record, field, target, value)
4647}
4648
4649/// `value` as the one-element buffer of its OWN element type. The element type
4650/// is not this function's business — [`coerce_put_value`] runs C's
4651/// `dbPutConvertRoutine` row over the request afterwards, array to array.
4652fn one_element_buffer(value: &EpicsValue) -> Option<EpicsValue> {
4653 Some(match value {
4654 EpicsValue::Short(v) => EpicsValue::ShortArray(vec![*v]),
4655 EpicsValue::Float(v) => EpicsValue::FloatArray(vec![*v]),
4656 EpicsValue::Enum(v) => EpicsValue::EnumArray(vec![*v]),
4657 EpicsValue::Double(v) => EpicsValue::DoubleArray(vec![*v]),
4658 EpicsValue::Long(v) => EpicsValue::LongArray(vec![*v]),
4659 EpicsValue::Int64(v) => EpicsValue::Int64Array(vec![*v]),
4660 EpicsValue::UInt64(v) => EpicsValue::UInt64Array(vec![*v]),
4661 EpicsValue::UShort(v) => EpicsValue::UShortArray(vec![*v]),
4662 EpicsValue::ULong(v) => EpicsValue::ULongArray(vec![*v]),
4663 EpicsValue::UChar(v) => EpicsValue::UCharArray(vec![*v]),
4664 EpicsValue::Char(v) => EpicsValue::CharArray(vec![*v]),
4665 EpicsValue::String(v) => EpicsValue::StringArray(vec![v.clone()]),
4666 // Not a stored value; `convert_to` collapses it to a bare index.
4667 EpicsValue::EnumWithChoices { .. } => return None,
4668 // Already a buffer.
4669 _ => return None,
4670 })
4671}
4672
4673pub fn put_field_internal_default<R: Record + ?Sized>(
4674 record: &mut R,
4675 name: &str,
4676 value: EpicsValue,
4677) -> CaResult<()> {
4678 // Coerce to the type the record STORES, not the type it SERVES. The two are
4679 // the same for most fields, but a `menu()` field is declared `DBF_MENU` and
4680 // served as `DBR_ENUM` with its choices (`promote_menu_value`) while the
4681 // record stores the bare choice index as a `Short` — and `put_field`'s arms
4682 // match on what is stored. Coercing to the served type would hand every
4683 // `put_field` an `Enum` its `Short` arm cannot match. This is the inverse of
4684 // `promote_menu_value`, and asking the record what it holds keeps the rule
4685 // uniform instead of special-casing menus here.
4686 //
4687 // The `.dbd` type is the fallback for a field the record cannot currently
4688 // produce a value for (an uninitialised array, a port-internal field).
4689 let target_type = record
4690 .get_field(name)
4691 .map(|v| v.db_field_type())
4692 .or_else(|| crate::server::record::record_instance::declared_field_type_of(record, name));
4693 let value = link_value_in_field_shape(&*record, name, value);
4694 let is_enum_carrier = matches!(value, EpicsValue::EnumWithChoices { .. });
4695 let value = match target_type {
4696 // A String target routes through the converter even on a type match: C's
4697 // `putStringString` truncates to `field_size - 1` (see `coerce_put_value`).
4698 Some(target)
4699 if is_enum_carrier
4700 || ((value.db_field_type() != target || target == DbFieldType::String)
4701 && !value.is_empty_array()) =>
4702 {
4703 match coerce_put_value(record, name, target, value)? {
4704 Converted::Stored(v) => v,
4705 // C's converter returned success without storing: the field
4706 // keeps its old value and the put still succeeds.
4707 Converted::Unchanged => return Ok(()),
4708 }
4709 }
4710 // Carrier with no known target field: collapse to a bare index (the prior
4711 // fallback) rather than letting it reach storage.
4712 None if is_enum_carrier => value.convert_to(DbFieldType::Long),
4713 _ => value,
4714 };
4715 record.put_field(name, value)
4716}
4717
4718/// C's `putString*` row when the destination is an ARRAY — `Some` when this row
4719/// owns the put, `None` when it does not apply and the scalar rows below do.
4720///
4721/// `dbPut` reaches it for every `special(SPC_DBADDR)` field regardless of
4722/// `nRequest` (`dbAccess.c:1350`), so a one-element string put is this row too;
4723/// keeping the two counts on one owner is what stops them drifting apart.
4724///
4725/// The two array destinations whose row is a byte copy rather than a parse — a
4726/// long-string field (`lsi`/`lso`/`printf` VAL, `DBF_STRING` in their
4727/// `cvt_dbaddr`) and an `FTVL=STRING` buffer, whose `NumericField::of` is
4728/// `None` — still belong to this row; it hands them the text unchanged for
4729/// their record to store.
4730fn put_string_array_row<R: Record + ?Sized>(
4731 record: &R,
4732 field: &str,
4733 target: DbFieldType,
4734 value: &EpicsValue,
4735) -> CaResult<Option<Converted>> {
4736 let texts: &[crate::types::PvString] = match value {
4737 EpicsValue::String(s) => std::slice::from_ref(s),
4738 EpicsValue::StringArray(a) => a,
4739 _ => return Ok(None),
4740 };
4741 if !record.get_field(field).is_some_and(|v| v.is_array()) {
4742 return Ok(None);
4743 }
4744 // `putStringString` is a byte copy, and the record stores the text: hand the
4745 // value over unchanged rather than falling through to the SCALAR numeric
4746 // row below, which would parse `printf.VAL`'s `"7"` into the number 7.
4747 if record
4748 .long_string_fields()
4749 .iter()
4750 .any(|f| f.eq_ignore_ascii_case(field))
4751 {
4752 return Ok(Some(Converted::Stored(value.clone())));
4753 }
4754 let Some(numeric) = c_parse::NumericField::of(target) else {
4755 return Ok(Some(Converted::Stored(value.clone())));
4756 };
4757 c_parse::put_string_elements(field, numeric, texts).map(Some)
4758}
4759
4760/// Coerce a written value to a field's stored type — the single owner of C
4761/// `dbConvert.c`'s `dbFastPutConvertRoutine[dbrType][field_type]` table, shared
4762/// by the two paths a value can enter a record's field through: a client
4763/// `dbPut` (`crate::server::database::field_io`) and an internal link /
4764/// device-support delivery ([`put_field_internal_default`]).
4765///
4766/// Every `DBR_STRING` row of C's put table is a converter that can FAIL, and
4767/// none of them is `EpicsValue::convert_to`:
4768///
4769/// * `DBF_MENU` → `putStringMenu` — exact label, else an index below `nChoice`
4770/// ([`crate::server::record::resolve_menu_field_string`]).
4771/// * `DBF_ENUM` → `putStringEnum` — the record's state strings, else an index
4772/// below `no_str` ([`crate::server::record::resolve_enum_state_string`]).
4773/// * `DBF_STRING` → `putStringString` — a byte copy, the one row that cannot
4774/// fail.
4775/// * every numeric width → `putStringChar` … `putStringDouble`, i.e.
4776/// `epicsParse*`, which refuses the put on overflow and on unparseable text
4777/// ([`c_parse::put_string`]).
4778///
4779/// `convert_to` cannot express any of the failures — it is field-blind and
4780/// total, mapping unparseable text to `0` and an out-of-range number to the
4781/// nearest representable one. That is how `caput MY:VALVE Open` became a silent
4782/// no-op that drove `VAL` to state 0, and how `caput REC.PREC 32768` — which the
4783/// compiled softIoc REFUSES — stored 32767.
4784///
4785/// This is the TYPE row alone. The request's shape is settled before it
4786/// arrives, by whichever entry the value came through
4787/// ([`dbput_coerce_value`] for a client `dbPut`,
4788/// [`link_value_in_field_shape`] for internal delivery), so nothing here
4789/// renders a destination shape that one of those two contracts would disagree
4790/// with.
4791///
4792/// An ARRAY destination takes the same rule through
4793/// [`c_parse::put_string_elements`], which runs `putString*` over every element
4794/// of the request. It is run HERE rather than delegated to the record, because
4795/// delegation made the row a convention nothing enforced: `waveform` ran it
4796/// while `compress.VAL` and `histogram.VAL` answered `TypeMismatch` to a put the
4797/// compiled softIoc accepts, and the `StringArray` half of the same row never
4798/// reached this function at all — it fell through to the total `convert_to`,
4799/// which stored `0.0` for text `epicsParseFloat64` refuses.
4800///
4801/// Two array destinations are outside the numeric row, and both are C's own
4802/// distinction rather than a carve-out here: a LONG-STRING field
4803/// ([`Record::long_string_fields`]) is `DBF_STRING` in its `cvt_dbaddr`, so its
4804/// row is `putStringString`, a byte copy; and an `FTVL=STRING` buffer is the
4805/// same copy per element. Those keep the string and their records store it. A
4806/// string reaching a char buffer as its BYTES is the DBR_CHAR row (`caput -S`),
4807/// which arrives as a `CharArray` and needs no conversion.
4808pub fn coerce_put_value<R: Record + ?Sized>(
4809 record: &R,
4810 field: &str,
4811 target: DbFieldType,
4812 value: EpicsValue,
4813) -> CaResult<Converted> {
4814 // The ARRAY row first, so both counts of the SAME C routine have one owner:
4815 // a scalar `String` and a `StringArray` into an array destination are both
4816 // `dbPutConvertRoutine[DBR_STRING][target]` over `nRequest` elements
4817 // (`dbAccess.c:1350` takes that arm for every `special(SPC_DBADDR)` field,
4818 // whatever the count). Menu/DTYP/enum fields are never array destinations,
4819 // so nothing below is shadowed.
4820 if let Some(converted) = put_string_array_row(record, field, target, &value)? {
4821 return Ok(converted);
4822 }
4823 if let EpicsValue::String(s) = &value {
4824 // DTYP (DBF_DEVICE) validates against the record type's FULL device
4825 // menu — static `device()` lines PLUS runtime-contributed device
4826 // support — the same set the read/announce path exposes via
4827 // `RecordInstance::device_choices`. `menu_choices_of`'s DTYP branch
4828 // returns only the static half, so a contributed device-support name
4829 // (asyn's `asynInt32`, scaler-rs's `Asyn Scaler`, ...) would wrongly
4830 // fail this put even though a client can read it in the DTYP choices.
4831 // Resolve DTYP against the merged menu to keep put and read symmetric.
4832 if field.eq_ignore_ascii_case("DTYP") {
4833 let choices = super::merged_device_menu(record.record_type());
4834 if !choices.is_empty() {
4835 return super::resolve_menu_field_string(
4836 field,
4837 &choices,
4838 target,
4839 &s.as_str_lossy(),
4840 )
4841 .map(Converted::Stored);
4842 }
4843 // No device menu declared or contributed for this record type: fall
4844 // through to the generic handling below (unchanged behavior).
4845 } else if let Some(choices) = super::record_instance::menu_choices_of(record, field) {
4846 return super::resolve_menu_field_string(field, choices, target, &s.as_str_lossy())
4847 .map(Converted::Stored);
4848 }
4849 if target == DbFieldType::Enum {
4850 return super::resolve_enum_state_string(
4851 field,
4852 record.enum_state_strings().as_deref(),
4853 s,
4854 )
4855 .map(Converted::Stored);
4856 }
4857 if let Some(numeric) = c_parse::NumericField::of(target) {
4858 return c_parse::put_string(field, numeric, &s.as_str_lossy());
4859 }
4860 if target == DbFieldType::String {
4861 // C `putStringString` (dbConvert.c:916-925): `strncpy(pdst, psrc,
4862 // field_size); pdst[field_size-1] = 0` — the DBF_STRING put
4863 // truncates to `field_size - 1` bytes. The row is NOT a no-op even
4864 // for a String source, so it must run even when source and stored
4865 // type match (the two gates that call this converter skip it on a
4866 // type match; both route a String target here regardless). The CA
4867 // wire already caps a DBR_STRING at `MAX_STRING_SIZE - 1` (39), so
4868 // this only bites a field whose `.dbd` `size(N)` is under 40 —
4869 // dbCommon `ASG` `size(29)` → 28, and the like.
4870 return Ok(Converted::Stored(EpicsValue::String(
4871 cap_string_to_field_size(record, field, s),
4872 )));
4873 }
4874 }
4875 // The float/double rows of C's DBR_STRING put column render through the
4876 // record's `get_precision`, seeded 6: `putFloatString`/`putDoubleString`
4877 // (`dbConvert.c:1558`/`:1600`) for the array path,
4878 // `cvt_f_st`/`cvt_d_st` (`dbFastLinkConv.c:1216`/`:1333`) for the scalar
4879 // one that `dbAccess.c:1391` actually takes. `convert_to` cannot express
4880 // it — it is field-blind by contract, and precision is the record's — so
4881 // the row belongs here, next to the menu and enum rows it also cannot
4882 // express. Rendering through the GET direction's own converter is what
4883 // keeps `dbtgf REC.DESC` and a `caput` of the same number agreeing.
4884 if target == DbFieldType::String
4885 && let Some(rendered) =
4886 crate::types::codec::dbr_string_at_precision(&value, put_string_precision(record))
4887 {
4888 return Ok(Converted::Stored(rendered));
4889 }
4890 Ok(Converted::Stored(value.convert_to(target)))
4891}
4892
4893/// C's `prset->get_precision` answer for a **`DBF_STRING`** destination — the
4894/// precision `putFloatString`/`putDoubleString` render a numeric put with.
4895///
4896/// The seed is 6 and only `get_precision` overwrites it
4897/// (`dbConvert.c:1562-1568`, `dbFastLinkConv.c:1220-1228`). For a STRING field
4898/// the shared tail is a no-op: `recGblGetPrec`'s switch has no `DBF_STRING`
4899/// case (`recGbl.c:141-142`), so whatever the body seeded survives. And no
4900/// `get_precision` in base or in the ported modules names a `DBF_STRING` field
4901/// in its own switch, so the answer is per RECORD rather than per field: it is
4902/// `PREC` for every body that seeds `*precision = prec->prec` ahead of the tail
4903/// — ai, ao, aai, aao, aSub, calc, calcout, compress, dfanout, sel, seq, sub,
4904/// subArray, waveform, sCalcout, aCalcout, epid, mca, motor, scaler, sseq,
4905/// swait, throttle, transform. The two ported types that do not seed:
4906///
4907/// * `histogram` — a switch with no seed and no case a dbCommon field reaches,
4908/// so the caller's 6 arrives at `cvtDoubleToString`
4909/// (`histogramRecord.c:420-438`).
4910/// * `asyn` — `*precision = 0;` before the tail (`asynRecord.c::get_precision`).
4911///
4912/// `bo`, `busy`, `mbbiDirect` and `mbboDirect` are unseeded too and need no arm:
4913/// none of them declares `PREC`, so the fallback is already C's 6. No record
4914/// type declares `PREC` while NULLing `get_precision` (checked across base and
4915/// the ported modules), which is what lets "has a PREC field" stand in for
4916/// "supplies the slot" without a second table to keep in step.
4917///
4918/// A negative PREC is not clamped, for the reason
4919/// [`crate::types::codec`]'s GET side spells out: C hands the `long` to
4920/// `cvtDoubleToString`'s `epicsUInt16` parameter and the conversion
4921/// reinterprets it.
4922fn put_string_precision<R: Record + ?Sized>(record: &R) -> u16 {
4923 match record.record_type() {
4924 "histogram" => 6,
4925 "asyn" => 0,
4926 _ => record
4927 .get_field("PREC")
4928 .and_then(|v| v.as_int_i64())
4929 .map_or(6, |p| p as i16 as u16),
4930 }
4931}
4932
4933/// C `putStringString`'s truncation: a `DBF_STRING` field stores at most
4934/// `field_size - 1` bytes (its `.dbd` `size(N)` less the forced NUL). A field
4935/// with no declared size (`0` — a Tier 3 hand table, or a field with no
4936/// declaration) is left uncapped beyond the wire's own `MAX_STRING_SIZE - 1`.
4937fn cap_string_to_field_size<R: Record + ?Sized>(record: &R, field: &str, s: &PvString) -> PvString {
4938 match super::record_instance::field_desc_of(record, field) {
4939 Some(desc) if desc.size > 0 => {
4940 let cap = (desc.size as usize).saturating_sub(1);
4941 let bytes = s.as_bytes();
4942 if bytes.len() > cap {
4943 PvString::from_bytes(bytes[..cap].to_vec())
4944 } else {
4945 s.clone()
4946 }
4947 }
4948 _ => s.clone(),
4949 }
4950}
4951
4952/// Subroutine function type for `sub`/`aSub` records.
4953///
4954/// The return value is the subroutine's C `long` status
4955/// (`subRecord.c::do_sub` / `aSubRecord.c::do_sub`): `< 0` raises
4956/// `SOFT_ALARM` at the record's `BRSV` severity, and for `aSub` the status
4957/// is published as `VAL` (`aSubRecord.c:224`). Return `Ok(0)` for the
4958/// normal no-alarm path. `Err(..)` is reserved for an infrastructure
4959/// failure inside the closure (e.g. a field write error), which aborts
4960/// processing — it is distinct from a negative status.
4961pub type SubroutineFn = Box<dyn Fn(&mut dyn Record) -> CaResult<i64> + Send + Sync>;
4962
4963#[cfg(test)]
4964mod declaration_numbering_tests {
4965 use super::*;
4966 use crate::server::record::dbd_generated::{DB_COMMON_FIELDS, record_fields};
4967
4968 /// C's `special.h:26-39` @R7.0.10 in full, plus the 0 C leaves in
4969 /// `pdbFldDes->special` for a field with no `special()`.
4970 ///
4971 /// The WHOLE table, not a sample, because the numbering is not
4972 /// contiguous: C has no 4, and the record-specific half jumps to 100. A
4973 /// twelfth variant inserted in the middle must fail on its own row here
4974 /// rather than shift every code after it.
4975 const C_SPECIAL: [(Special, i16, &str); 11] = [
4976 (Special::None, 0, "(none)"),
4977 (Special::NoMod, 1, "SPC_NOMOD"),
4978 (Special::DbAddr, 2, "SPC_DBADDR"),
4979 (Special::Scan, 3, "SPC_SCAN"),
4980 (Special::AlarmAck, 5, "SPC_ALARMACK"),
4981 (Special::As, 6, "SPC_AS"),
4982 (Special::Attribute, 7, "SPC_ATTRIBUTE"),
4983 (Special::Mod, 100, "SPC_MOD"),
4984 (Special::Reset, 101, "SPC_RESET"),
4985 (Special::LinConv, 102, "SPC_LINCONV"),
4986 (Special::Calc, 103, "SPC_CALC"),
4987 ];
4988
4989 /// Exhaustive by construction: a twelfth variant stops the build here, so
4990 /// the distinctness check below turns 11 rows into a proof of coverage.
4991 fn _the_enum_has_no_variant_outside_the_table(s: Special) {
4992 match s {
4993 Special::None
4994 | Special::NoMod
4995 | Special::DbAddr
4996 | Special::Scan
4997 | Special::AlarmAck
4998 | Special::As
4999 | Special::Attribute
5000 | Special::Mod
5001 | Special::Reset
5002 | Special::LinConv
5003 | Special::Calc => (),
5004 }
5005 }
5006
5007 #[test]
5008 fn every_special_carries_cs_spc_number() {
5009 for (spc, n, name) in C_SPECIAL {
5010 assert_eq!(spc as i16, n, "{name}: wrong SPC_ number");
5011 }
5012 for (a, (x, ..)) in C_SPECIAL.iter().enumerate() {
5013 for (y, ..) in C_SPECIAL.iter().skip(a + 1) {
5014 assert_ne!(x, y, "{x:?} listed twice");
5015 }
5016 }
5017 // The gap is the reason the discriminants are written out: an
5018 // implicitly-numbered enum would have answered 4 here and 7 for
5019 // `Mod`, and `special as i16` is the integer C prints.
5020 assert!(!C_SPECIAL.iter().any(|(_, n, _)| *n == 4), "C has no SPC 4");
5021 // `pamapspcType[SPC_NTYPES]` (`special.h:42,51-61`) is the `.dbd`
5022 // parser's table: nine entries, omitting SPC_ATTRIBUTE — which C sets
5023 // internally — and the 0 that means no `special()` at all.
5024 assert_eq!(
5025 C_SPECIAL
5026 .iter()
5027 .filter(|(s, ..)| !matches!(s, Special::None | Special::Attribute))
5028 .count(),
5029 9,
5030 );
5031 }
5032
5033 #[test]
5034 fn the_declared_token_survives_the_generators_collapse() {
5035 // `dbf_to_rust` maps DBF_INLINK|DBF_OUTLINK|DBF_FWDLINK onto one
5036 // served type, so `dbf_type` cannot tell a forward link from an
5037 // output link — and the field NAME cannot either: `LNK1` is
5038 // DBF_OUTLINK on sseq and DBF_FWDLINK on fanout. Only the carried
5039 // declaration separates them, which is what `dba` and `dbDumpField`
5040 // have to print.
5041 let field = |rec: &str, name: &str| {
5042 record_fields(rec)
5043 .unwrap_or_else(|| panic!("no field table for {rec}"))
5044 .iter()
5045 .find(|f| f.name == name)
5046 .unwrap_or_else(|| panic!("{rec}.{name} not in the table"))
5047 };
5048 let sseq_lnk1 = field("sseq", "LNK1");
5049 let fanout_lnk1 = field("fanout", "LNK1");
5050 let ai_inp = field("ai", "INP");
5051 let flnk = DB_COMMON_FIELDS
5052 .iter()
5053 .find(|f| f.name == "FLNK")
5054 .expect("dbCommon.FLNK");
5055
5056 for f in [sseq_lnk1, fanout_lnk1, ai_inp, flnk] {
5057 assert_eq!(f.dbf_type, DbFieldType::String, "{}: served type", f.name);
5058 }
5059 assert_eq!(sseq_lnk1.declared_dbf, DbfCode::Outlink);
5060 assert_eq!(fanout_lnk1.declared_dbf, DbfCode::Fwdlink);
5061 assert_eq!(ai_inp.declared_dbf, DbfCode::Inlink);
5062 assert_eq!(flnk.declared_dbf, DbfCode::Fwdlink);
5063
5064 // The other half of the collapse: DBF_ENUM|DBF_MENU|DBF_DEVICE all
5065 // serve as Enum. `DTYP` is the one DBF_DEVICE field in all of base.
5066 let dtyp = DB_COMMON_FIELDS
5067 .iter()
5068 .find(|f| f.name == "DTYP")
5069 .expect("dbCommon.DTYP");
5070 let scan = DB_COMMON_FIELDS
5071 .iter()
5072 .find(|f| f.name == "SCAN")
5073 .expect("dbCommon.SCAN");
5074 assert_eq!(dtyp.dbf_type, DbFieldType::Enum);
5075 assert_eq!(dtyp.declared_dbf, DbfCode::Device);
5076 assert_eq!(scan.dbf_type, DbFieldType::Enum);
5077 assert_eq!(scan.declared_dbf, DbfCode::Menu);
5078 }
5079
5080 #[test]
5081 fn no_access_reads_the_declaration_not_the_served_type() {
5082 // `waveform.VAL` is declared DBF_NOACCESS and re-typed by
5083 // `cvt_dbaddr`, so its served type says nothing; the declaration is
5084 // what `dbPutString` refuses a `.db` assignment on.
5085 let val = record_fields("waveform")
5086 .expect("waveform")
5087 .iter()
5088 .find(|f| f.name == "VAL")
5089 .expect("waveform.VAL");
5090 assert_eq!(val.declared_dbf, DbfCode::NoAccess);
5091 assert!(val.no_access());
5092 assert_ne!(val.dbf_type, DbFieldType::String, "re-typed by cvt_dbaddr");
5093
5094 // `mbbo.VAL` is the counter-example the doc names: SPC_DBADDR too,
5095 // but declared DBF_ENUM, so it is settable.
5096 let mbbo_val = record_fields("mbbo")
5097 .expect("mbbo")
5098 .iter()
5099 .find(|f| f.name == "VAL")
5100 .expect("mbbo.VAL");
5101 assert!(!mbbo_val.no_access());
5102
5103 // A hand-written descriptor has no `.dbd` behind it, so its
5104 // declaration is its served type's own code and it is never a C
5105 // internal.
5106 let hand = FieldDesc::new("VAL", DbFieldType::Double, false);
5107 assert_eq!(hand.declared_dbf, DbfCode::Double);
5108 assert!(!hand.no_access());
5109 }
5110}
5111
5112#[cfg(test)]
5113mod tests {
5114 use super::*;
5115 use crate::server::records::compress::CompressRecord;
5116
5117 /// Internal delivery is not a `dbPut`, and the two entries have to stay
5118 /// two. A `compress` INP reading a `DBF_LONG` source delivers ONE sample
5119 /// as an `EpicsValue::Long`; C's link layer converts it to the target's
5120 /// type and the record folds it into `compress_scalar`'s running `cvb`.
5121 /// Rendering it as a one-element buffer sends it to `push_array` instead,
5122 /// whose array algorithm writes the N clamp back into the record
5123 /// (`compressRecord.c:171-172`) — a field the scalar algorithm never
5124 /// touches.
5125 #[test]
5126 fn a_one_sample_link_delivery_is_not_a_one_element_buffer() {
5127 let mut rec = CompressRecord::new(4, 0);
5128 rec.put_field("N", EpicsValue::Long(0)).unwrap();
5129 assert_eq!(rec.get_field("N"), Some(EpicsValue::ULong(0)));
5130
5131 // The source's DBF differs from VAL's, so this delivery reaches the
5132 // converter rather than being handed over unchanged.
5133 put_field_internal_default(&mut rec, "VAL", EpicsValue::Long(5)).unwrap();
5134
5135 assert_eq!(
5136 rec.get_field("N"),
5137 Some(EpicsValue::ULong(0)),
5138 "compressRecord.c:273-304 never touches prec->n"
5139 );
5140 }
5141}