Skip to main content

epics_base_rs/server/record/
record_trait.rs

1use crate::error::CaResult;
2use crate::types::{DbFieldType, EpicsValue};
3
4use super::scan::ScanType;
5
6/// Metadata describing a single field in a record.
7#[derive(Debug, Clone)]
8pub struct FieldDesc {
9    pub name: &'static str,
10    pub dbf_type: DbFieldType,
11    pub read_only: bool,
12}
13
14/// Per-field metadata deltas returned by
15/// [`Record::field_metadata_override`].
16///
17/// Each `Some` member replaces the corresponding member of the
18/// snapshot's record-level display/control metadata; `None` members
19/// keep the record-level value.
20#[derive(Debug, Clone, Default)]
21pub struct FieldMetadataOverride {
22    /// `display.units` — C RSET `get_units`.
23    pub units: Option<crate::types::PvString>,
24    /// `display.precision` — C RSET `get_precision`.
25    pub precision: Option<i16>,
26    /// `(upper, lower)` display limits — C RSET `get_graphic_double`.
27    pub disp_limits: Option<(f64, f64)>,
28    /// `(upper, lower)` control limits — C RSET `get_control_double`.
29    pub ctrl_limits: Option<(f64, f64)>,
30    /// `(hihi, high, low, lolo)` — C RSET `get_alarm_double`.
31    pub alarm_limits: Option<(f64, f64, f64, f64)>,
32}
33
34/// Side-effect actions that a record requests from the processing framework.
35///
36/// Records return these from `process()` via `ProcessOutcome::actions`.
37/// The framework executes them at the appropriate point in the processing
38/// cycle, keeping records as pure state machines without direct DB access.
39#[derive(Clone, Debug, PartialEq)]
40pub enum ProcessAction {
41    /// Write a value to a DB link. The framework reads `link_field` from the
42    /// record to get the target PV name, then writes `value` to that PV.
43    ///
44    /// Executed after alarm/snapshot, before FLNK.
45    /// Example: scaler writes CNT to COUT/COUTP links.
46    WriteDbLink {
47        link_field: &'static str,
48        value: EpicsValue,
49    },
50
51    /// Read a value from a DB link into a record field. The framework reads
52    /// `link_field` from the record to get the source PV name, reads that PV,
53    /// and writes the result into `target_field` via an internal put that
54    /// bypasses read-only checks.
55    ///
56    /// The value delivered is the link target's **native** [`EpicsValue`] — it
57    /// is NOT coerced to a numeric type on the way in. The record coerces (or
58    /// preserves) it at its own `put_field`/`put_field_internal` boundary, so a
59    /// string-class source can reach a string field byte-exact (the `sseq`
60    /// `DOLn`→`STRn` path, C `sseqRecord.c:643-705`). Records whose
61    /// `target_field` is numeric simply convert there, exactly as before.
62    ///
63    /// **Pre-process action**: executed BEFORE the next process() cycle so
64    /// the value is immediately available. This matches C EPICS `dbGetLink()`
65    /// which is synchronous/immediate.
66    ///
67    /// Example: throttle reads SINP into VAL when SYNC is triggered.
68    ReadDbLink {
69        link_field: &'static str,
70        target_field: &'static str,
71    },
72
73    /// Schedule a re-process of this record after the given duration.
74    /// The framework spawns `tokio::spawn(sleep(d) + process_record(name))`.
75    /// The current cycle's OUT/FLNK/notify proceed normally.
76    ///
77    /// Equivalent to C EPICS `callbackRequestDelayed()` + `scanOnce()`.
78    ReprocessAfter(std::time::Duration),
79
80    /// Send a named command to the device support driver.
81    /// The framework calls `DeviceSupport::handle_command()` with this data.
82    /// Used by scaler to request reset/arm/write_preset operations
83    /// without the record holding a direct driver reference.
84    DeviceCommand {
85        command: &'static str,
86        args: Vec<EpicsValue>,
87    },
88
89    /// Write a value to a DB link as a put-*with-completion*, then re-enter
90    /// THIS record's `process()` when the downstream operation completes.
91    ///
92    /// The framework arms a put-notify wait-set (C `dbProcessNotify`),
93    /// writes `link_field`'s target through it, releases the initiator's
94    /// own count, and wires the completion to an async re-entry of this
95    /// record (`mint_async_token` + `reprocess_on_notify`). The record
96    /// returns [`RecordProcessResult::AsyncPending`] alongside this action
97    /// and is re-entered once the downstream record (and its FLNK/OUT
98    /// chain) finishes — the synApps `sseq` `WAITn` "wait for the put
99    /// callback" dependency (`sseqRecord.c::processNextLink`,
100    /// `dbCaPutLinkCallback`). Built on the same `new_put_notify` +
101    /// `reprocess_on_notify` primitive an out-of-band
102    /// [`crate::server::database::AsyncDbHandle`] caller uses.
103    ///
104    /// Executed before FLNK, like [`Self::WriteDbLink`].
105    WriteDbLinkNotify {
106        link_field: &'static str,
107        value: EpicsValue,
108    },
109
110    /// Cancel this record's outstanding async re-entry (C
111    /// `callbackCancelDelayed`): the framework advances the record's
112    /// re-entry generation so any pending `ReprocessAfter` timer or
113    /// `WriteDbLinkNotify` completion re-entry becomes a structural no-op
114    /// (the `AsyncToken` gate), with no runtime "is-aborted" check on the
115    /// re-entry path. Used by `sseq` `ABORT` to drop a pending `DLYn`
116    /// delay or `WAITn` wait; the record resets its own sequence state in
117    /// the same `process()` cycle that emits this.
118    CancelReprocess,
119}
120
121/// Result of a record's process() call.
122///
123/// Determines how the framework handles the current processing cycle.
124/// Side-effect actions (link writes, delayed reprocess, etc.) are expressed
125/// separately in `ProcessOutcome::actions`.
126#[derive(Clone, Debug, PartialEq)]
127pub enum RecordProcessResult {
128    /// Processing completed synchronously this cycle.
129    /// Framework proceeds with alarm/timestamp/snapshot/OUT/FLNK.
130    Complete,
131    /// Processing started but not yet complete (PACT stays set).
132    /// Current cycle skips alarm/timestamp/snapshot/OUT/FLNK.
133    /// ProcessActions (if any) are still executed.
134    AsyncPending,
135    /// Async pending, but notify these intermediate field changes immediately.
136    /// Used by motor records to flush DMOV=0 before the move completes.
137    AsyncPendingNotify(Vec<(String, EpicsValue)>),
138}
139
140/// Complete outcome of a record's process() call.
141///
142/// Contains the processing result (Complete, AsyncPending, etc.) and a list
143/// of side-effect actions for the framework to execute.
144#[derive(Clone, Debug)]
145pub struct ProcessOutcome {
146    pub result: RecordProcessResult,
147    pub actions: Vec<ProcessAction>,
148    /// Set by the framework when device support's read() returned
149    /// `did_compute: true`. The record's process() can check this to
150    /// skip its built-in computation (e.g., PID). Replaces the `pid_done`
151    /// flag pattern.
152    pub device_did_compute: bool,
153}
154
155impl ProcessOutcome {
156    /// Shorthand for a simple Complete with no actions.
157    pub fn complete() -> Self {
158        Self {
159            result: RecordProcessResult::Complete,
160            actions: Vec::new(),
161            device_did_compute: false,
162        }
163    }
164
165    /// Shorthand for Complete with actions.
166    pub fn complete_with(actions: Vec<ProcessAction>) -> Self {
167        Self {
168            result: RecordProcessResult::Complete,
169            actions,
170            device_did_compute: false,
171        }
172    }
173
174    /// Shorthand for AsyncPending with no actions.
175    pub fn async_pending() -> Self {
176        Self {
177            result: RecordProcessResult::AsyncPending,
178            actions: Vec::new(),
179            device_did_compute: false,
180        }
181    }
182}
183
184impl Default for ProcessOutcome {
185    fn default() -> Self {
186        Self::complete()
187    }
188}
189
190/// Result of setting a common field, indicating what scan index updates are needed.
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub enum CommonFieldPutResult {
193    NoChange,
194    ScanChanged {
195        old_scan: ScanType,
196        new_scan: ScanType,
197        phas: i16,
198    },
199    PhasChanged {
200        scan: ScanType,
201        old_phas: i16,
202        new_phas: i16,
203    },
204}
205
206/// Read-only snapshot of framework-owned `CommonFields` state that a
207/// record's `process()` or device support's `read()` needs to see
208/// *during* the processing cycle.
209///
210/// The framework owns `RecordInstance.common`; a record `process()`
211/// receives only `&mut self` (the concrete record) and device support
212/// `read()` receives only `&mut dyn Record`. Neither can reach
213/// `CommonFields`. C records, by contrast, see `dbCommon` directly —
214/// e.g. `epidRecord.c:195` reads `pepid->udf`, `timestampRecord.c:90`
215/// reads `ptimestamp->tse`, `devTimeOfDay.c:122` reads `psi->phas`.
216///
217/// The framework builds a `ProcessContext` from `common` and pushes it
218/// onto the record (via [`Record::set_process_context`]) and onto the
219/// device support (via
220/// [`crate::server::device_support::DeviceSupport::set_process_context`])
221/// immediately before the respective call. This mirrors the existing
222/// `set_device_did_compute` framework-set-hook pattern: additive,
223/// no `process()` / `read()` signature change.
224#[derive(Clone, Debug, PartialEq)]
225pub struct ProcessContext {
226    /// `dbCommon.udf` — value is undefined. C records check this at the
227    /// top of `process()` (e.g. `epidRecord.c:195`).
228    pub udf: bool,
229    /// `dbCommon.udfs` — alarm severity raised for a UDF record.
230    pub udfs: crate::server::record::AlarmSeverity,
231    /// `dbCommon.phas` — phase. Used by device support for format
232    /// selection (`devTimeOfDay.c:122`).
233    pub phas: i16,
234    /// `dbCommon.tse` — time-stamp event. `timestampRecord.c:90`
235    /// branches on `tse == epicsTimeEventDeviceTime`.
236    pub tse: i16,
237    /// `dbCommon.tsel` — time-stamp event link string.
238    pub tsel: String,
239    /// `dbCommon.dtyp` — device-support type name. A record's
240    /// `process()` / pre-process hooks can branch on the DTYP to mirror
241    /// C device support that lives in a separate DSET (e.g. the epid
242    /// record's `devEpidSoftCallback` callback DSET drives the TRIG
243    /// readback link, whereas `devEpidSoft` does not).
244    pub dtyp: String,
245}
246
247/// C `epicsTime.h`: `epicsTimeEventDeviceTime` — the `TSE` sentinel
248/// meaning "device support provides the time stamp". `timestampRecord.c`
249/// uses it to take the OS-clock branch instead of `recGblGetTimeStamp`.
250pub const EPICS_TIME_EVENT_DEVICE_TIME: i16 = -2;
251
252/// Snapshot of changes from a process cycle, used for notify outside lock.
253pub struct ProcessSnapshot {
254    /// `(field, value, mask)` — every posted field carries its own
255    /// `DBE_*` posting mask, mirroring C's per-field
256    /// `db_post_events(prec, &field, mask)`. One process cycle posts
257    /// different classes per field: a deadband-gated readback narrows
258    /// to the deadbands that actually crossed (MDEL → `DBE_VALUE`,
259    /// ADEL → `DBE_LOG`; motorRecord.cc `monitor()` 3477-3507,
260    /// aiRecord.c `monitor()`), while a change-detected auxiliary
261    /// field posts `DBE_VALUE | DBE_LOG` (motorRecord.cc 3522-3645
262    /// `DBE_VAL_LOG`; calcRecord.c:420). A single record-wide mask
263    /// collapses that granularity — an archive-only deadband crossing
264    /// would wrongly reach `DBE_VALUE` subscribers whenever any other
265    /// field changed in the same pass.
266    pub changed_fields: Vec<(String, EpicsValue, crate::server::recgbl::EventMask)>,
267}
268
269/// Trait that all EPICS record types must implement.
270pub trait Record: Send + Sync + 'static {
271    /// Return the record type name (e.g., "ai", "ao", "bi").
272    fn record_type(&self) -> &'static str;
273
274    /// Process the record (scan/compute cycle).
275    ///
276    /// Returns a `ProcessOutcome` containing the processing result and any
277    /// side-effect actions for the framework to execute.
278    fn process(&mut self) -> CaResult<ProcessOutcome> {
279        Ok(ProcessOutcome::complete())
280    }
281
282    /// Optional: report whether this record's last `process()` call
283    /// mutated a metadata-class field (EGU/PREC/HOPR/LOPR/HLM/LLM/
284    /// alarm limits / DRVH/DRVL / state strings).
285    ///
286    /// The framework checks this after every `process()` call and, if
287    /// true, invalidates the record's metadata cache so the next
288    /// snapshot rebuilds from the new values.
289    ///
290    /// Default: `false` — most records never touch metadata fields
291    /// during processing. Override only when your record dynamically
292    /// adjusts limits or unit strings (e.g., a motor that recomputes
293    /// HLM/LLM after a hardware homing operation).
294    ///
295    /// Implementations should reset their internal flag after returning
296    /// `true` so the next cycle starts clean.
297    fn took_metadata_change(&mut self) -> bool {
298        false
299    }
300
301    /// Get a field value by name.
302    fn get_field(&self, name: &str) -> Option<EpicsValue>;
303
304    /// Set a field value by name.
305    fn put_field(&mut self, name: &str, value: EpicsValue) -> CaResult<()>;
306
307    /// Return the list of field descriptors.
308    fn field_list(&self) -> &'static [FieldDesc];
309
310    /// Choice strings for a record-specific `DBF_MENU` field served as
311    /// `DBR_ENUM`, keyed by field name (uppercase, as declared).
312    ///
313    /// EPICS dbStaticLib serves a `DBF_MENU` field as `DBR_ENUM`: the value
314    /// is the menu index and the field carries its `menu()` choice strings,
315    /// so `caget`/`pvget` present the labels rather than a bare number
316    /// (`dbStaticLib.c` `dbGetMenuChoices`; `dbAccess.c` `get_enum_str`).
317    /// A record returns the label table (in index order) for each field it
318    /// serves as [`DbFieldType::Enum`] from a `menu()`; the framework
319    /// attaches it to the field snapshot's `EnumInfo` so the CA/PVA enum
320    /// encoders present the labels — the same mechanism `bi`/`bo`/`mbbi`/
321    /// `mbbo` already use for their `VAL` state strings, but per field
322    /// rather than per record (a record can carry several distinct menus).
323    ///
324    /// This is the single owner of "menu field -> choice table": a record
325    /// declares its menu fields here once, and `get_field` returns the menu
326    /// index as [`EpicsValue::Enum`]. Default: no record-specific menu
327    /// fields. The dbCommon menu fields (`SCAN`, etc.) are handled
328    /// separately by the framework, not here.
329    fn menu_field_choices(&self, _field: &str) -> Option<&'static [&'static str]> {
330        None
331    }
332
333    /// Per-field override of the record-level display/control metadata
334    /// for a GET / monitor snapshot of `field`.
335    ///
336    /// C record support serves metadata PER FIELD: the RSET functions
337    /// `get_units` / `get_precision` / `get_graphic_double` /
338    /// `get_control_double` / `get_alarm_double` all key on
339    /// `dbGetFieldIndex(paddr)` and fall back to the `recGbl*` defaults
340    /// for unlisted fields. The framework's metadata cache is per
341    /// record (built by `populate_display_info` /
342    /// `populate_control_info` from the VAL-class fields); a record
343    /// whose RSET serves different metadata for non-VAL fields
344    /// overrides this hook to patch the cached values for that field
345    /// (e.g. the motor record: VELO's display range is VMAX/VBAS, not
346    /// HLM/LLM — `motorRecord.cc:3247-3250`).
347    ///
348    /// Applied on both the GET path (`snapshot_for_field`) and the
349    /// monitor path (`make_monitor_snapshot`), AFTER the cached
350    /// record-level metadata — and computed live on each call, so an
351    /// override derived from non-cached fields can never go stale.
352    /// `field` is uppercase, as declared in [`Record::field_list`].
353    /// Default: `None` — record-level metadata serves every field.
354    fn field_metadata_override(&self, _field: &str) -> Option<FieldMetadataOverride> {
355        None
356    }
357
358    /// Field names this record serves as a *long string*: a `DBF_CHAR`
359    /// array field that semantically holds a NUL-terminated string.
360    ///
361    /// In EPICS such a field is declared `DBF_NOACCESS` (or carries a `$`
362    /// modifier) and is accessed through a `DBR_CHAR` array view whose
363    /// `form` is `"String"`; pvxs maps that view to a scalar `pvString`
364    /// rather than an `int8[]` (`ioc/channel.cpp:58-68`,
365    /// `ioc/iocsource.cpp:619-643`). QSRV uses this list to serve those
366    /// fields as scalar-string NTScalar values instead of byte scalars.
367    ///
368    /// The record keeps its `CharArray` storage; the QSRV boundary does
369    /// the `CharArray <-> String` conversion. Default empty — only
370    /// long-string record types (`lsi`/`lso` VAL/OVAL, `printf` VAL)
371    /// override this. Names are matched case-insensitively.
372    fn long_string_fields(&self) -> &'static [&'static str] {
373        &[]
374    }
375
376    /// Field names declared `pp(TRUE)` in this record type's DBD, or
377    /// `None` if the type's pp-flags have not been modeled.
378    ///
379    /// Drives the `dbPutField` processing gate: C
380    /// `dbAccess.c:1263` re-processes a record on a put only when the put
381    /// field is `PROC` or it is `pp(TRUE)` **and** `SCAN == Passive`. A
382    /// `None` return tells the put path to fall back to the legacy
383    /// "process on every put" behavior, so un-modeled record types keep
384    /// working unchanged. The default consults the central DBD-sourced
385    /// table keyed by [`Record::record_type`]; record types can override.
386    fn process_passive_fields(&self) -> Option<&'static [&'static str]> {
387        super::process_passive::pp_fields_for(self.record_type())
388    }
389
390    /// Validate a put before it is applied. Return Err to reject.
391    fn validate_put(&self, _field: &str, _value: &EpicsValue) -> CaResult<()> {
392        Ok(())
393    }
394
395    /// Hook called after a successful put_field.
396    fn on_put(&mut self, _field: &str) {}
397
398    /// Primary field name (default "VAL"). Override for waveform etc.
399    fn primary_field(&self) -> &'static str {
400        "VAL"
401    }
402
403    /// Get the primary value.
404    fn val(&self) -> Option<EpicsValue> {
405        self.get_field(self.primary_field())
406    }
407
408    /// Set the primary value.
409    ///
410    /// Matches C EPICS `dbPut` behavior: if the value type doesn't match
411    /// the field type, it is automatically coerced (e.g., Long→Double for
412    /// ai, Long→Enum for bi/mbbi). This prevents silent failures when
413    /// asyn device support provides Int32 values to Enum-typed records.
414    fn set_val(&mut self, value: EpicsValue) -> CaResult<()> {
415        let field = self.primary_field();
416        match self.put_field(field, value.clone()) {
417            Ok(()) => Ok(()),
418            Err(crate::error::CaError::TypeMismatch(_)) => {
419                // Auto-coerce: determine target type from current VAL
420                let target_type = self
421                    .get_field(field)
422                    .map(|v| v.db_field_type())
423                    .unwrap_or(DbFieldType::Double);
424                let coerced = value.convert_to(target_type);
425                self.put_field(field, coerced)
426            }
427            Err(e) => Err(e),
428        }
429    }
430
431    /// Whether this record implements the `DTYP="Raw Soft Channel"`
432    /// read path via [`Record::apply_raw_input`]. Records that return
433    /// `true` opt into framework routing of the INP link value through
434    /// `apply_raw_input` (RVAL + MASK) instead of the default
435    /// soft-channel `VAL` direct write.
436    ///
437    /// Default `false` keeps any record that has not been wired for
438    /// raw soft channel on the legacy path (which sets VAL directly).
439    fn accepts_raw_soft_input(&self) -> bool {
440        false
441    }
442
443    /// Apply a value read from a `DTYP="Raw Soft Channel"` INP link.
444    ///
445    /// Mirrors the C `devXxxSoftRaw.c` `read_xxx()` convention: the
446    /// raw value goes to `RVAL` (so the record's `process()` then runs
447    /// the standard `RVAL → VAL` conversion). Records that expose a
448    /// `MASK` field must apply it here, matching epics-base
449    /// `f2fe9d12` (devBiSoftRaw: `prec->rval &= prec->mask`).
450    ///
451    /// Only invoked by the framework when
452    /// [`Record::accepts_raw_soft_input`] returns `true`.
453    fn apply_raw_input(&mut self, value: EpicsValue) -> CaResult<()> {
454        self.set_val(value)
455    }
456
457    /// Apply IVOA=2 ("set outputs to IVOV") semantics: copy the
458    /// IVOV value into whatever output staging field the OUT
459    /// writeback consumes for this record type. Mirrors the
460    /// per-record C `recXxx.c` behaviour:
461    ///
462    /// - `ao`/`lso`: `OVAL = IVOV; VAL = OVAL`
463    /// - `bo`/`busy`/`mbbo`/`mbboDirect`: `RVAL = IVOV; VAL = IVOV`
464    /// - `calcout`/`scalcout`: `OVAL = IVOV` (VAL is calc input, not
465    ///   touched on invalid-output)
466    /// - `dfanout`: `VAL = IVOV` (the broadcast value)
467    ///
468    /// Default uses [`Record::set_val`] for records whose OUT path
469    /// reads VAL only.
470    fn apply_invalid_output_value(&mut self, ivov: EpicsValue) -> CaResult<()> {
471        self.set_val(ivov)
472    }
473
474    /// Whether this record type supports device write (output records only).
475    /// `aao` is included here even though it's served by the same
476    /// concrete struct as `waveform`/`aai`/`subArray` — the
477    /// WaveformRecord's `can_device_write` override picks the right
478    /// answer per [`ArrayKind`], but this default matters for code that
479    /// only has the record-type string.
480    fn can_device_write(&self) -> bool {
481        matches!(
482            self.record_type(),
483            "ao" | "bo"
484                | "longout"
485                | "int64out"
486                | "mbbo"
487                | "mbboDirect"
488                | "stringout"
489                | "lso"
490                | "aao"
491        )
492    }
493
494    /// Whether async processing has completed and put_notify can respond.
495    /// Records that return AsyncPendingNotify should return false while
496    /// async work is in progress, and true when done.
497    /// Default: true (synchronous records are always complete).
498    fn is_put_complete(&self) -> bool {
499        true
500    }
501
502    /// Whether this record should fire its forward link after processing.
503    fn should_fire_forward_link(&self) -> bool {
504        true
505    }
506
507    /// Whether this record's OUT link should be written after processing.
508    /// Defaults to true. Override in calcout / longout to implement OOPT
509    /// conditional output (epics-base 7.0.8).
510    fn should_output(&self) -> bool {
511        true
512    }
513
514    /// Notify the record that the OUT-link / device write completed
515    /// successfully on this cycle. The framework calls this right after
516    /// the actual write so transition-detection state (e.g.
517    /// `longout.pval`) can update for the next cycle's
518    /// [`Self::should_output`] check. Default: no-op.
519    fn on_output_complete(&mut self) {}
520
521    /// Whether this record uses MDEL/ADEL deadband for monitor posting.
522    /// Binary records (bi, bo, busy, mbbi, mbbo) return false because
523    /// C EPICS always posts monitors for these record types regardless
524    /// of whether the value changed.
525    fn uses_monitor_deadband(&self) -> bool {
526        true
527    }
528
529    /// Per-record VALUE/LOG monitor gate for record types that post a
530    /// monitor *only when the value actually changed* — and have no
531    /// MDEL/ADEL deadband to express that.
532    ///
533    /// `Some(changed)` makes the framework post the VALUE and LOG
534    /// monitors iff `changed`; `None` (the default) leaves the decision
535    /// to the deadband / always-post path.
536    ///
537    /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` raise `DBE_VALUE |
538    /// DBE_LOG` only when `len != olen || memcmp(oval, val, len)`. Those
539    /// records return [`Self::uses_monitor_deadband`]`== false`, which
540    /// otherwise routes them to the unconditional always-post path
541    /// (correct for binary records, wrong for lsi/lso). Because the
542    /// framework posts monitors *after* `process()` — by which point the
543    /// record has already committed `oval`/`olen` — the implementation
544    /// captures the comparison result during `process()` and returns the
545    /// captured flag here, not a live re-comparison.
546    fn monitor_value_changed(&self) -> Option<bool> {
547        None
548    }
549
550    /// `menuPost` "Always" override for the VALUE / LOG monitor masks.
551    ///
552    /// Returns `(post_value_always, post_archive_always)`. The framework
553    /// ORs these into the change-gated mask from
554    /// [`Self::monitor_value_changed`], so an *unchanged* process cycle
555    /// still posts `DBE_VALUE` (resp. `DBE_LOG`) when the record's MPST
556    /// (resp. APST) menu field is set to `Always`.
557    ///
558    /// C `lsiRecord.c`/`lsoRecord.c` `monitor()` compute the VAL post
559    /// mask from three independent inputs:
560    ///
561    /// * the change test `len != olen || memcmp(oval, val, len)` →
562    ///   `DBE_VALUE | DBE_LOG`,
563    /// * `if (mpst == menuPost_Always) events |= DBE_VALUE;`,
564    /// * `if (apst == menuPost_Always) events |= DBE_LOG;`.
565    ///
566    /// [`Self::monitor_value_changed`] carries the first input; this hook
567    /// carries the other two. Records without a `menuPost` field keep the
568    /// default `(false, false)`, which leaves the change gate unchanged.
569    fn monitor_always_post(&self) -> (bool, bool) {
570        (false, false)
571    }
572
573    /// The value the MDEL/ADEL deadband is evaluated against.
574    ///
575    /// For most records C `monitor()` applies the value deadband to
576    /// `VAL`, so the default is [`Self::val`]. A record whose monitored
577    /// quantity is not its primary value must override this: the motor
578    /// record, for instance, has `VAL` as the setpoint and applies
579    /// MDEL/ADEL to `RBV` (the readback) — its C `monitor()` deadbands
580    /// `RBV`, not `VAL`. Such a record returns its readback field here.
581    ///
582    /// Default is `val()`, so existing records are unaffected.
583    fn monitor_deadband_value(&self) -> Option<EpicsValue> {
584        self.val()
585    }
586
587    /// The FIELD whose VALUE/LOG monitor delivery the MDEL/ADEL
588    /// deadband gates — the field [`Self::monitor_deadband_value`]
589    /// reads. A record overriding one must override both consistently.
590    ///
591    /// For most records the deadband gates the primary value itself,
592    /// so the default returns [`Self::primary_field`] and nothing
593    /// changes. The motor record deadbands RBV: C `monitor()`
594    /// (motorRecord.cc:3468-3507) throttles the RBV post with
595    /// MDEL/ADEL, while VAL is posted only when an actual setpoint
596    /// change marked it (M_VAL). When this returns a non-primary
597    /// field, the framework's snapshot builders:
598    ///
599    /// * deliver THIS field on the deadband triggers (instead of raw
600    ///   change-detection), and
601    /// * route the primary field through generic change-detection, so
602    ///   an unchanged setpoint is not re-posted on every readback
603    ///   poll.
604    fn monitor_deadband_field(&self) -> &'static str {
605        self.primary_field()
606    }
607
608    /// Fields the record's C `monitor()` posts on every cycle whose
609    /// alarm transition fired, even when their value did not change.
610    ///
611    /// C motorRecord.cc `monitor()` (3513-3645) computes
612    /// `local_mask = monitor_mask | (MARKED(x) ? DBE_VAL_LOG : 0)`
613    /// for each field in its posting list — when the alarm moved
614    /// (`monitor_mask != 0`), `local_mask` is non-zero for UNMARKED
615    /// fields too, so every listed field posts with `DBE_ALARM` and a
616    /// `DBE_ALARM`-only subscriber observes the alarm moment on any of
617    /// them. The framework's change-detection loop posts a listed,
618    /// subscribed, unchanged field with the cycle's alarm bits when
619    /// this list names it.
620    ///
621    /// Default: empty — most C record types post only their value
622    /// field(s) on an alarm transition (aiRecord.c `monitor()` posts
623    /// VAL with `monitor_mask` and RVAL only when it changed), which
624    /// the deadband-field post already covers.
625    fn alarm_cycle_monitored_fields(&self) -> &'static [&'static str] {
626        &[]
627    }
628
629    /// Fields the record's C `monitor()` re-posts with `DBE_VAL_LOG` on
630    /// every cycle that recomputed them, even when the value did not
631    /// change — the analogue of an unconditional `MARK(field)` in C.
632    ///
633    /// Unlike [`Self::alarm_cycle_monitored_fields`] (which posts unchanged
634    /// fields only on a cycle whose alarm transition fired), these post on
635    /// any cycle the record names them, with `DBE_VALUE | DBE_LOG` (plus the
636    /// cycle's alarm bits when one fired). The framework's change-detection
637    /// loop posts a listed, subscribed, unchanged field with that mask.
638    ///
639    /// C motorRecord `process_motor_info` (motorRecord.cc:3764-3767)
640    /// `MARK`s `M_DIFF`/`M_RDIF` unconditionally on every `CALLBACK_DATA`
641    /// pass, and `monitor()` (3522-3531) posts them with `monitor_mask |
642    /// DBE_VAL_LOG`; a `camonitor DIFF` on an axis parked at a constant
643    /// non-zero following error thus gets an event every poll. The record
644    /// returns the fields ONLY on the cycles it actually re-marked them (it
645    /// reads its own per-cycle state), so a pass that did not recompute them
646    /// does not over-post.
647    ///
648    /// Default: empty — most record types post a field only when it
649    /// changed (or on an alarm transition), which the existing gates cover.
650    fn force_posted_fields(&self) -> &'static [&'static str] {
651        &[]
652    }
653
654    /// Fields the record's C `monitor()` re-posts with `DBE_LOG` ONLY on
655    /// every cycle it names them, regardless of change — the analogue of
656    /// an unconditional `db_post_events(field, DBE_LOG)` sweep.
657    ///
658    /// Distinct from [`Self::force_posted_fields`], which posts with
659    /// `DBE_VALUE | DBE_LOG`: these post with `DBE_LOG` alone, so only a
660    /// `DBE_LOG` (archiver) subscriber receives the unchanged-value
661    /// event. A field that ALSO changed this cycle is already delivered
662    /// by the change-detection post (`DBE_VALUE | DBE_LOG`, which carries
663    /// the LOG bit), so the framework does not double-post it — the LOG
664    /// sweep lands only for the fields that did not change.
665    ///
666    /// C `scalerRecord.c` `monitor()` (scalerRecord.c:770-787) runs on
667    /// every IDLE process and posts each active channel `S1..Snch` with a
668    /// literal `DBE_LOG`. The scaler returns those channel field names
669    /// here ONLY while idle (it reads its own `ss` state), so an archiver
670    /// `camonitor SCALER:Sn` gets an event every idle scan even when the
671    /// count is unchanged — while a counting cycle (which does not run C
672    /// `monitor()`) returns empty.
673    ///
674    /// Default: empty — most record types have no LOG-only sweep.
675    fn log_swept_fields(&self) -> &'static [&'static str] {
676        &[]
677    }
678
679    /// Initialize record (pass 0: field defaults; pass 1: dependent init).
680    fn init_record(&mut self, _pass: u8) -> CaResult<()> {
681        Ok(())
682    }
683
684    /// Post-init finalisation hook with mutable access to the
685    /// framework's UDF flag. Called once after both `init_record`
686    /// passes complete. Default implementation is a no-op.
687    ///
688    /// epics-base PR `dabcf89` (mbboDirect): when VAL is undefined
689    /// at init time but the user populated B0..B1F bits, the bits
690    /// should be folded into VAL and UDF cleared. The framework
691    /// owns `common.udf`, so the record cannot mutate it from
692    /// `init_record` alone — this hook is the controlled point of
693    /// access.
694    fn post_init_finalize_undef(&mut self, _udf: &mut bool) -> CaResult<()> {
695        Ok(())
696    }
697
698    /// Seed the monitor/archive/alarm deadband trackers (MLST/ALST/LALM)
699    /// from the initial value at iocInit, called once by the builder after
700    /// both `init_record` passes and `post_init_finalize_undef`.
701    ///
702    /// Every C value record's `init_record` ends with
703    /// `prec->mlst = prec->alst = prec->lalm = prec->val`
704    /// (e.g. `longinRecord.c:120-122`, `aiRecord.c`), so the first
705    /// `monitor()` evaluates `DELTA(mlst, val) > mdel` with `mlst == val`
706    /// (= 0) and posts no DBE_VALUE/DBE_LOG event when the value is
707    /// unchanged from its initial state. Records expose MLST/ALST/LALM as
708    /// plain `f64` fields default-initialised to `0.0`; that default
709    /// conflates "never published" with "published 0", so a record
710    /// initialised to a *nonzero* value (constant DOL, initial VAL) used
711    /// to post a spurious first-cycle update that C does not.
712    ///
713    /// The default seeds whichever of MLST/ALST/LALM the record actually
714    /// serves from its monitor-deadband value (`val` for most records),
715    /// making the invariant hold by construction for every record rather
716    /// than per-type `init_record` code. It is idempotent for the record
717    /// types that already seed inside `init_record`, and a no-op for
718    /// records that serve none of these fields.
719    fn seed_deadband_tracking(&mut self) {
720        let seed = match self.monitor_deadband_value().and_then(|v| v.to_f64()) {
721            Some(v) if v.is_finite() => v,
722            _ => return,
723        };
724        for field in ["MLST", "ALST", "LALM"] {
725            if self.get_field(field).is_some() {
726                let _ = self.put_field(field, EpicsValue::Double(seed));
727            }
728        }
729    }
730
731    /// Called by the framework immediately after applying this cycle's
732    /// [`Record::multi_input_links`] fetches, before `process()`.
733    ///
734    /// `resolved` lists the `link_field` names (the first element of
735    /// each `multi_input_links` pair) whose fetch actually produced a
736    /// value this cycle — i.e. the link was non-empty and the read
737    /// succeeded. A link field absent from the slice either had no link
738    /// configured or its DB/CA fetch failed.
739    ///
740    /// This is the framework analogue of C device support inspecting
741    /// `RTN_SUCCESS(dbGetLink(...))` — e.g. `epidRecord.c:191-193`
742    /// clears `udf` only when `dbGetLink(&prec->stpl, ...)` returns
743    /// success. A record's `process()` cannot otherwise observe whether
744    /// an input link's fetch succeeded, because a failed fetch simply
745    /// leaves the target field unwritten.
746    ///
747    /// Additive, framework-set-hook pattern (same shape as
748    /// [`Record::set_process_context`]). Default: ignore.
749    fn set_resolved_input_links(&mut self, _resolved: &[&'static str]) {}
750
751    /// Called before/after a field put for side-effect processing.
752    fn special(&mut self, _field: &str, _after: bool) -> CaResult<()> {
753        Ok(())
754    }
755
756    /// Other fields whose monitors must be posted because a put to
757    /// `put_field` changed them as a side effect, without driving a full
758    /// process cycle.
759    ///
760    /// Mirrors the explicit `db_post_events` calls a C `special()` makes:
761    /// e.g. `compressRecord.c::reset` (invoked on a `SPC_RESET` write to
762    /// `RES`) posts `NUSE` and `VAL` even though `RES` is not `pp(TRUE)`
763    /// and so does not process. The framework posts a `VALUE|LOG` monitor
764    /// for each returned field after the put. Default: none.
765    fn monitor_side_effect_fields(&self, _put_field: &str) -> &'static [&'static str] {
766        &[]
767    }
768
769    /// Downcast to concrete type for device support init injection.
770    /// Override in record types that need device support to inject state (e.g., MotorRecord).
771    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
772        None
773    }
774
775    /// Whether processing this record should clear UDF.
776    /// Override to return false for record types that don't produce a valid value every cycle.
777    fn clears_udf(&self) -> bool {
778        true
779    }
780
781    /// Whether the record's current `VAL` is undefined (UDF must
782    /// stay set).
783    ///
784    /// C parity: `aiRecord.c:285` / `calcRecord.c::checkAlarms` /
785    /// `int64inRecord.c:144` clear `UDF` **only** when the computed /
786    /// read value is valid — `if (status == 0)` and, for floating
787    /// records, only when `VAL` is not NaN. The framework owns
788    /// `common.udf`; it calls `clears_udf()` to decide whether this
789    /// record type clears UDF at all, then this method to decide
790    /// whether the *value produced this cycle* is actually defined.
791    ///
792    /// Default: a floating `VAL` that is NaN (e.g. a calc
793    /// divide-by-zero, or a soft input whose link read failed and
794    /// left VAL un-updated) is undefined; everything else is defined.
795    /// A record whose `val()` yields `None` (no primary value) is
796    /// also treated as undefined.
797    fn value_is_undefined(&self) -> bool {
798        match self.val() {
799            Some(EpicsValue::Double(v)) => v.is_nan(),
800            Some(EpicsValue::Float(v)) => v.is_nan(),
801            Some(_) => false,
802            None => true,
803        }
804    }
805
806    /// Per-record alarm hook — evaluate record-type-specific alarms
807    /// (STATE / COS / analog limit / SOFT) and accumulate them into
808    /// `nsta`/`nsev` via `recGblSetSevr`.
809    ///
810    /// The framework centralises the generic alarm machinery (UDF
811    /// check, `recGblResetAlarms` transfer, MS/MSI/MSS link-alarm
812    /// inheritance). The record-type-specific severity logic that C
813    /// puts in each record's `checkAlarms()` belongs here so a record
814    /// can raise its own alarms without the framework hardcoding a
815    /// per-type `match` on `record_type()`.
816    ///
817    /// `common` is the record's [`CommonFields`]; implementations
818    /// raise alarms with [`crate::server::recgbl::rec_gbl_set_sevr`]
819    /// / [`crate::server::recgbl::rec_gbl_set_sevr_msg`].
820    ///
821    /// Default: no-op — records that have not yet migrated their
822    /// `checkAlarms` logic here are still covered by the framework's
823    /// legacy centralised `evaluate_alarms` match.
824    fn check_alarms(&mut self, _common: &mut crate::server::record::CommonFields) {}
825
826    /// Return multi-input link field pairs: (link_field, value_field).
827    /// Override in calc, calcout, sel, sub to return INPA..INPL → A..L mappings.
828    fn multi_input_links(&self) -> &[(&'static str, &'static str)] {
829        &[]
830    }
831
832    /// Return multi-output link field pairs: (link_field, value_field).
833    /// Override in transform to return OUTA..OUTP → A..P mappings.
834    fn multi_output_links(&self) -> &[(&'static str, &'static str)] {
835        &[]
836    }
837
838    /// Internal field write that bypasses read-only checks.
839    /// Used by the framework to write values from ReadDbLink actions
840    /// into fields that are normally read-only (e.g., epid.CVAL).
841    /// Default implementation delegates to put_field().
842    ///
843    /// On the `ReadDbLink` path this is also where a pvalink NTEnum
844    /// carrier ([`EpicsValue::EnumWithChoices`]) is resolved. The
845    /// dbrType-blind link resolver produces it for an NTEnum source;
846    /// pvxs `pvaGetValue` (`pvalink_lset.cpp:330-360`) picks
847    /// label-vs-index by the TARGET field's dbrType — only a DBR_STRING
848    /// target gets the `choices[index]` label, every other type takes
849    /// the numeric index. Route it through [`EpicsValue::convert_to`]
850    /// (the single value-coercion owner) against the target field's
851    /// `db_field_type`, so the transient carrier is consumed before any
852    /// record `put_field` / storage / wire path can see it. The
853    /// single-INP→VAL apply path reaches the same `convert_to` via
854    /// `set_val`'s `TypeMismatch` auto-coerce.
855    fn put_field_internal(&mut self, name: &str, value: EpicsValue) -> CaResult<()> {
856        let value = match value {
857            EpicsValue::EnumWithChoices { .. } => {
858                let target_type = self
859                    .get_field(name)
860                    .map(|v| v.db_field_type())
861                    .unwrap_or(DbFieldType::Long);
862                value.convert_to(target_type)
863            }
864            other => other,
865        };
866        self.put_field(name, value)
867    }
868
869    /// Return pre-process actions (ReadDbLink) that the framework should
870    /// execute BEFORE calling process(). This is called once per cycle.
871    /// Default returns empty. Override in records that need link reads
872    /// to be available during process().
873    fn pre_process_actions(&mut self) -> Vec<ProcessAction> {
874        Vec::new()
875    }
876
877    /// Return actions the framework must execute BEFORE the input-link
878    /// (`multi_input_links`, INP -> value-field) fetch for this cycle.
879    ///
880    /// This is strictly earlier than [`Self::pre_process_actions`]: the
881    /// framework resolves input links *before* it calls
882    /// `pre_process_actions`, so an action that must affect what an
883    /// input link reads cannot be expressed there.
884    ///
885    /// The motivating case is the epid record's `devEpidSoftCallback`
886    /// DB-type TRIG link: C `devEpidSoftCallback.c:120-132` writes the
887    /// readback-trigger link with `dbPutLink` — which synchronously
888    /// processes the triggered source chain — and only *then*
889    /// (`devEpidSoftCallback.c:151`) does `dbGetLink(&pepid->inp, ...)`
890    /// read `CVAL`. The trigger write therefore has to land before the
891    /// `INP -> CVAL` fetch, in the same process pass.
892    ///
893    /// Called once per cycle, while a record write lock is held; the
894    /// framework executes the returned actions (currently `WriteDbLink`
895    /// and `ReadDbLink`) and then performs the input-link fetch.
896    /// Default returns empty.
897    fn pre_input_link_actions(&mut self) -> Vec<ProcessAction> {
898        Vec::new()
899    }
900
901    /// Called by the framework immediately before `process()` to push a
902    /// read-only snapshot of framework-owned [`CommonFields`] state
903    /// ([`ProcessContext`]) that the record's `process()` needs to see.
904    ///
905    /// The framework owns `RecordInstance.common`; a record `process()`
906    /// only gets `&mut self`. C records read `dbCommon` directly — e.g.
907    /// `epidRecord.c:195` checks `pepid->udf` at the top of `process()`,
908    /// `timestampRecord.c:90` branches on `ptimestamp->tse`. This hook
909    /// is the controlled equivalent: a record that needs `udf`/`phas`/
910    /// `tse`/`tsel` during `process()` overrides this to stash the
911    /// values into its own fields.
912    ///
913    /// Additive, framework-set-hook pattern (same shape as
914    /// [`Record::set_device_did_compute`]). Default: ignore — most
915    /// records never need common state during `process()`.
916    fn set_process_context(&mut self, _ctx: &ProcessContext) {}
917
918    /// Called once by the framework when the record is registered
919    /// (`add_record`), delivering the record its own canonical name plus a
920    /// cycle-free [`crate::server::database::AsyncDbHandle`] for driving
921    /// async-side updates from OUTSIDE a `process()` cycle.
922    ///
923    /// The handle wraps a `Weak` reference to the database, so a record
924    /// that stashes it creates no ownership cycle (the database owns the
925    /// record; a stored strong handle would leak it). It is the controlled
926    /// equivalent of C device support capturing `precord` plus the
927    /// dbCommon scan lock for an out-of-band `db_post_events` /
928    /// `callbackRequest`: e.g. the asyn TRACE/exception callback posts
929    /// trace-flag fields immediately from the driver thread, and AQR
930    /// cancels a queued I/O re-entry — neither happens inside `process()`.
931    ///
932    /// The in-band counterpart for a record's *own* process cycle is the
933    /// completion-driven [`ProcessAction`] family
934    /// ([`ProcessAction::WriteDbLinkNotify`],
935    /// [`ProcessAction::CancelReprocess`],
936    /// [`ProcessAction::ReprocessAfter`]); this hook exists for the
937    /// out-of-band path that has no `process()` return to ride on.
938    ///
939    /// Additive, framework-set-hook pattern (same shape as
940    /// [`Self::set_process_context`]). Default: ignore — most records do
941    /// no out-of-band async posting.
942    fn set_async_context(&mut self, _name: String, _db: crate::server::database::AsyncDbHandle) {}
943
944    /// Framework init hook: called once at record load *after* the common
945    /// link fields (`INP`/`OUT`/`FLNK`/...) have been resolved and the
946    /// `init_record` passes have run, with the record's resolved
947    /// [`CommonFields`](crate::server::record::CommonFields).
948    ///
949    /// This is the seam for records that classify their links into status
950    /// diagnostics at init the way C `init_record` does (e.g. calcout's
951    /// `INAV..INUV`/`OUTV` `menu(calcoutINAV)` checkLinks loop): a record's
952    /// *common* link strings (`OUT` is a common field, not a record field)
953    /// are invisible to [`Self::set_async_context`] — which runs at
954    /// `add_record`, *before* the common fields are applied — and to
955    /// `init_record`, which carries no `CommonFields`. The record captures
956    /// whichever common links it needs here so a passive, never-processed
957    /// record already exposes its link status. Records whose links are all
958    /// record-owned (e.g. sseq DOLn/LNKn) do not need this hook.
959    ///
960    /// Additive, framework-set-hook pattern. Default: ignore.
961    fn init_links(&mut self, _common: &crate::server::record::CommonFields) {}
962
963    /// Called by the framework before process() to indicate whether device
964    /// support's read() already performed the record's compute step.
965    /// Override in records that have a built-in compute (e.g., epid PID)
966    /// to skip it when device support already ran it.
967    /// Default: ignore.
968    fn set_device_did_compute(&mut self, _did_compute: bool) {}
969
970    /// Whether this record has a raw-to-engineering (`RVAL → VAL`)
971    /// `convert()` step that must be skipped on a `Soft Channel` input.
972    ///
973    /// C `devAiSoft.c:65` `read_ai` (and the other soft-channel input
974    /// `read_xxx`) always returns 2 ("don't convert"), so `aiRecord.c`'s
975    /// `if (status==0) convert(prec)` is bypassed for a `Soft Channel`
976    /// input record. The framework expresses this by calling
977    /// [`Record::set_device_did_compute(true)`] on the record before
978    /// `process()`.
979    ///
980    /// This hook exists so the framework only suppresses `convert()` —
981    /// NOT a record's entire built-in compute. Records like `epid` also
982    /// override `set_device_did_compute` but interpret it as "skip the
983    /// whole compute step" (the PID loop); those records have no
984    /// `RVAL → VAL` convert and MUST keep the default `false` so a
985    /// `Soft Channel` `epid` still runs `do_pid()` in `process()`.
986    ///
987    /// Default `false`: a record is only opted into the soft-channel
988    /// convert-skip when it explicitly returns `true`.
989    fn soft_channel_skips_convert(&self) -> bool {
990        false
991    }
992}
993
994/// Subroutine function type for sub records.
995pub type SubroutineFn = Box<dyn Fn(&mut dyn Record) -> CaResult<()> + Send + Sync>;