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