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