Skip to main content

RecordInstance

Struct RecordInstance 

Source
pub struct RecordInstance {
Show 15 fields pub name: String, pub record: Box<dyn Record>, pub common: CommonFields, pub subscribers: HashMap<String, Vec<Subscriber>>, pub parsed_inp: ParsedLink, pub parsed_out: ParsedLink, pub parsed_flnk: ParsedLink, pub parsed_sdis: ParsedLink, pub parsed_tsel: ParsedLink, pub device: Option<Box<dyn DeviceSupport>>, pub subroutine: Option<Arc<SubroutineFn>>, pub notify: Option<Arc<NotifyWaitSet>>, pub reprocess_generation: Arc<AtomicU64>, pub watchdog_generation: Arc<AtomicU64>, pub info: HashMap<String, String>, /* private fields */
}
Expand description

A type-erased record instance stored in the database.

Fields§

§name: String§record: Box<dyn Record>§common: CommonFields§subscribers: HashMap<String, Vec<Subscriber>>§parsed_inp: ParsedLink§parsed_out: ParsedLink§parsed_flnk: ParsedLink§parsed_sdis: ParsedLink§parsed_tsel: ParsedLink§device: Option<Box<dyn DeviceSupport>>§subroutine: Option<Arc<SubroutineFn>>§notify: Option<Arc<NotifyWaitSet>>§reprocess_generation: Arc<AtomicU64>

Generation counter for ReprocessAfter timer cancellation. Bumped each process cycle. Spawned timers check this to avoid stale re-processes from accumulated timers.

§watchdog_generation: Arc<AtomicU64>

Generation counter for the monitor watchdog (Record::watchdog_interval / Record::watchdog_fire), bumped by each PvDatabase::arm_watchdog so a re-arm supersedes the tick already in flight — C callbackRequestDelayed replacing an outstanding delayed callback. Deliberately NOT reprocess_generation: C’s histogram wdog is its own epicsCallback, independent of the record’s SDLY/async re-entry, so an SDLY defer must not cancel the watchdog nor vice versa.

§info: HashMap<String, String>

Per-record info tags from info("key", "value") directives in the .db file (epics-base info(…) grammar). Consumers include asyn (asyn:READBACK), record-as-PV bridge tags (Q:group, Q:form), and IOC-specific extensions. Empty for records loaded without info(…) clauses.

Implementations§

Source§

impl RecordInstance

Source

pub fn new(name: String, record: impl Record) -> Self

Source

pub fn new_boxed(name: String, record: Box<dyn Record>) -> Self

Source

pub fn soft_output_value(&self) -> Option<Option<EpicsValue>>

SINGLE OWNER of the DTYP -> soft-output-dset mapping. The dset table decides what a soft OUT-link write carries; no caller may re-derive it.

C ships two soft output dsets per output record type and DTYP picks one: devXxxSoft.c::write_xxx puts VAL/OVAL on the OUT link, while devXxxSoftRaw.c::write_xxx puts the RAW word — dbPutLink(&prec->out, DBR_LONG, &prec->rval, 1) (devAoSoftRaw.c:44, devBoSoftRaw.c:65) or data = prec->rval & prec->mask (devMbboSoftRaw.c:71-75, devMbboDirectSoftRaw.c:71-75).

Record::raw_soft_output_value IS the SoftRaw column of that table: Some exactly for the record types C ships a SoftRaw dset for. A record type C has no SoftRaw dset for keeps the plain soft-channel value — DTYP="Raw Soft Channel" on a longout is a .db error C rejects at init (“no device support”), and the port’s lenient reading of it (the same one crate::server::device_support::is_soft_dtyp already applies on the input side) must not turn the write into a silent no-op.

None means DTYP names device support that owns the write — real hardware, or “Async Soft Channel”, whose dset is a registered async device.

Source

pub fn set_info(&mut self, key: impl Into<String>, value: impl Into<String>)

Set a single info("key", "value") tag on this record. Last write wins. Used by the .db loader (info(...) directive) and dbpf-style tools.

Source

pub fn get_info(&self, key: &str) -> Option<&str>

Look up a single info tag. Returns None when the record has no tag with that key.

Source

pub fn invalidate_metadata_cache(&self)

Invalidate the metadata cache. Called after writing any metadata-class field (EGU, PREC, HOPR/LOPR, alarm limits, DRVH/DRVL, enum strings). The next snapshot will rebuild the cache from the new values.

Source

pub fn notify_field_written(&self, field: &str)

Hook called by the database after a field is written. If the field is in the metadata-class set, the cache is invalidated so the next snapshot picks up the new value.

Field name is automatically uppercased.

Source

pub fn notify_field_written_if_changed( &mut self, field: &str, prev: Option<&EpicsValue>, )

Like [notify_field_written] but skips the invalidation when the put did not actually change the field’s value. Mirrors epics-base faac1df1DBE_PROPERTY events fire only on real changes, not on idempotent writes (the C path compares paddr->pfield against the converted payload before setting the propertyUpdate flag).

prev is the value captured BEFORE the put. Callers that don’t need the change-detection (e.g. internal writers that know the field is non-metadata) can keep using [notify_field_written].

Source

pub fn is_no_mod(&self, field: &str) -> bool

C dbChannelSpecial(chan) == SPC_NOMODthe single owner of the no-modify declaration, for every consumer that needs to know whether a field can be written.

C declares it once, in the .dbd, and reads it in two unrelated places:

  • dbPut (dbAccess.c:123-126, via dbPutSpecial(paddr, 0)) refuses the write — the port’s check_no_mod gate;
  • rsrvCheckPut (rsrv/camessage.c:2540-2551) — if (dbChannelSpecial(pciu->dbch) == SPC_NOMOD) return 0; — which feeds the CA ACCESS_RIGHTS write bit (camessage.c:1123-1124) as well as both put paths, so a client sees Access: read, no write and never sends the doomed write.

Only the first consumer existed in the port, so every dbCommon NOMOD field advertised WRITE on the wire (caput N1.SEVR 2 was refused server-side, after the client had already sent it, with an async exception instead of C’s clean client-side “Write access denied”).

Three sources, one answer:

  1. the dbCommon SPC_NOMOD set below — common fields, so no record’s field_list declares them;
  2. the record type’s declaration, resolved by Self::field_desc — the vendored .dbd whenever one exists, and only for a record type that has no .dbd at all (motor, optics, scaler, std) the record’s own hand-written table, which for those Tier 3 types genuinely is their declaration;
  3. Record::field_no_mod — an SPC_NOMOD a record’s cvt_dbaddr raises from its own state (compress VAL under BALG=LIFO, compressRecord.c:398-407), which a static FieldDesc cannot express.

field may be any case.

Source

pub fn is_processing(&self) -> bool

Check if the record is currently processing (PACT equivalent).

Source

pub fn enter_pact(&self)

C prec->pact = TRUE — the record goes busy for an async device round-trip, an SDLY simulation defer, or an ODLY reprocess window.

Source

pub fn leave_pact(&mut self) -> PactExit

C prec->pact = FALSE — the ONLY release of PACT.

Every PACT→idle transition takes the put-notify parked on that window with it (C dbNotifyCompletion, reached from recGblFwdLink on every path that ends a cycle). The returned PactExit is #[must_use], so a release site cannot strand the deferral the way the open-coded processing.store(false) at the ODLY continuation and the three SIM/SDLY releases did.

Source

pub fn put_notify_busy(&self) -> bool

True when a put-notify already owns this record — an in-flight wait-set (C precord->ppn) or a park from a previous PACT arm. C processNotifyCommon (dbNotify.c:213-217) queues a second notify on the record’s restart list; the port refuses it (S_db_Blocked / ECA_PUTCBINPROG) — see Self::park_notify_put.

Source

pub fn park_notify_put( &mut self, put: DeferredNotifyPut, ) -> Result<(), DeferredNotifyPut>

Park a put-notify that landed on a PACT record (C processNotifyCommon, dbNotify.c:225-231). Err(put) hands the put back when another put-notify already owns the record.

The PactExit invariant — deferred_notify_put.is_some() ⟹ PACT — is established here: this is the only park, and it is only reachable from the caller’s is_processing() arm.

Source

pub fn resolve_field(&self, name: &str) -> Option<EpicsValue>

Unified field resolution: record fields → common fields → virtual fields.

Source

pub fn resolve_string_view_field(&self, name: &str) -> Option<EpicsValue>

Resolve a field for EPICS $ long-string (character-array) access.

The $ channel-name modifier (C dbChannel.c:486-505) re-views a field as a DBR_CHAR array: a DBF_STRING field becomes a char array of field_size elements, a link field a char array of PVLINK_STRINGSZ, and every other field type is rejected with S_dbLib_fieldNotFound. pvxs serves that char view as a form = "String" long-string NTScalar — it reads the DBR_CHAR bytes and NUL-terminates them back into a string (ioc/iocsource.cpp:133-136, ioc/channel.cpp:62-74).

Both DBF_STRING fields and link fields resolve to an EpicsValue::String in this database (a link resolves to its textual form, see Self::get_common_field), so a field is $-eligible exactly when it resolves to a string value. Returns that string value for an eligible field, or None for a field the $ modifier cannot view as a char array (the S_dbLib_fieldNotFound case) — the single owner of the dbChannel $-eligibility rule for the channel-resolution layer.

Source

pub fn declared_field_type(&self, field: &str) -> Option<DbFieldType>

The DBF_* type field is SERVED as — the single source of truth for the type on the wire, on every delivery path.

This is the field’s DECLARED type (FieldDesc::dbf_type, from the .dbd), not the type of whatever variant the record happens to store. C resolves a channel’s field_type from the dbFldDes at name-resolution time (dbChannelCreate -> dbNameToAddr, dbAccess.c:184-205) and every later dbGet/db_post_events converts the stored bytes to it — the storage is private to the record, the declaration is the contract.

Two answers are NOT the declaration:

  • a FieldDesc::runtime_typed field — C’s cvt_dbaddr overwrites paddr->field_type from record state (FTVL, FTA, SDEF), and this port’s cvt_dbaddr is the variant the record stores;
  • a field with no FieldDesc at all (a virtual field).

In both cases the value’s own type is the answer, so this returns None and Self::project_to_declared_type leaves the value alone.

Source

pub fn project_to_declared_type( &self, field: &str, value: EpicsValue, ) -> EpicsValue

Project a field’s stored value onto its declared type (Self::declared_field_type) — the single owner of “what type this field goes on the wire as”, run by the CA create-channel path (Self::client_field_value), the GET path (Self::snapshot_for_field) and the MONITOR path (Self::make_monitor_snapshot), so all three announce and serve the same type.

The projection is EpicsValue::convert_to, the one value-coercion owner — the same routine dbGet converts through. Never re-derive a conversion here: C picks its routine from BOTH the source and the destination type, and only convert_to knows that table.

Idempotent: a value already of its declared type is short-circuited by convert_to, and re-projecting a projected value is a no-op. That is what lets the CA path derive the native type from the value it is about to serve.

Source

pub fn client_field_value(&self, field: &str) -> Option<EpicsValue>

The client-facing value of field: the resolved value projected onto the field’s declared type (Self::project_to_declared_type), so a native type derived from the value — which is what the CA create-channel path does — is the DECLARED type, and matches the GET/MONITOR data byte for byte.

Source

pub fn snapshot_for_field(&self, field: &str) -> Option<Snapshot>

Build a Snapshot with full metadata for the given field.

Source

pub fn property_support_for_field(&self, field: &str) -> PropertySupport

The property mask a channel on field supplies, without building a snapshot — what a PVA server needs to decide which NT leaves it may MARK for a channel it has not read yet (QSRV resolves a group’s member masks once, at monitor start, rather than per event).

Same two gates, same owner as Self::assign_property_support: an unknown field supplies nothing.

Source

pub fn get_common_field(&self, name: &str) -> Option<EpicsValue>

Get a common field value.

Source

pub fn put_common_field( &mut self, name: &str, value: EpicsValue, ) -> CaResult<CommonFieldPutResult>

Set a common field value from a runtime dbPut (CA/PVA/dbpf/link). Returns what scan index changes are needed.

A DBF_MENU common field’s string is converted by C’s runtime converter, dbConvert.c::putStringMenu — see [MenuBound::DbPut].

Source

pub fn set_scan(&mut self, new_scan: ScanType) -> CommonFieldPutResult

The single owner of a record’s SCAN transition — C dbPutField on SCAN, which is scanDelete(precord)scanAdd(precord) (dbAccess.c::dbPutSpecial SPC_SCAN, dbScan.c:236-248).

Two callers reach it, and they are the two C sites that move a record between scan lists: a SCAN put (Self::put_common_field) and the simulation-mode scan swap (recGblCheckSimm, recGbl.c:427-437, which calls exactly the same scanDelete/scanAdd pair). Returns the delta for the scan-index owner (PvDatabase::update_scan_index) to apply once the record lock is down; CommonFieldPutResult::NoChange when the scan did not move.

Source

pub fn rec_gbl_save_simm(&mut self)

C recGblSaveSimm (recGbl.c:421-425) — latch the CURRENT simulation mode into OLDSIMM:

void recGblSaveSimm(const epicsEnum16 sscn,
    epicsEnum16 *poldsimm, const epicsEnum16 simm) {
    if (sscn == USHRT_MAX) return;
    *poldsimm = simm;
}

The only writer of CommonFields::oldsimm. Must run BEFORE the SIMM value moves — C calls it from special(SPC_MOD) pass 0 (before the put) and from recGblGetSimm/recGblInitSimm before the SIML read. The sscn == 65535 guard is C’s: with SSCN unset there is no scan to swap to, so the latch is not even taken (and Self::rec_gbl_check_simm bails on the same test, so the stale OLDSIMM is never read).

A record type with no SSCN/OLDSIMM in its C dbd (busy, swait) passes neither pointer to any recGbl helper: no-op here.

Source

pub fn rec_gbl_check_simm(&mut self) -> CommonFieldPutResult

C recGblCheckSimm (recGbl.c:427-437) — on a SIMM transition, swap the record’s SCAN with SSCN:

void recGblCheckSimm(struct dbCommon *pcommon, epicsEnum16 *psscn,
    const epicsEnum16 oldsimm, const epicsEnum16 simm) {
    if (*psscn == USHRT_MAX) return;
    if (simm != oldsimm) {
        epicsUInt16 scan = pcommon->scan;
        scanDelete(pcommon);
        pcommon->scan = *psscn;
        scanAdd(pcommon);
        *psscn = scan;
    }
}

This is what makes SSCN mean anything at all: a record configured field(SCAN,"1 second") field(SSCN,"Passive") stops periodic scanning the moment SIMM leaves NO, and resumes it when SIMM goes back — with the two fields having traded places each time. Both are a genuine swap, not an assignment: SSCN ends up holding the scan the record just left.

The only writer of the SIMM-driven SCAN/SSCN swap. The scan-list move itself goes through the single SCAN owner Self::set_scan, whose CommonFieldPutResult the caller hands to PvDatabase::update_scan_index once the record lock is down. Runs AFTER the SIMM value moved — C special(SPC_MOD) pass 1, and the tail of recGblGetSimm/recGblInitSimm.

Source

pub fn put_ackt(&mut self, value: u16)

C dbAccess.c::putAckt (:1285-1300) — the only writer of ACKT.

Reached from dbPut for a DBR_PUT_ACKT request type (dbAccess.c:1331-1332), ABOVE the SPC_NOMOD gate that refuses every ordinary put to the field. Posts exactly what C posts: the ACKT change, the ACKS it may lower, and the record-wide DBE_ALARM — and only when ackt actually changed (C returns 0 early otherwise).

Source

pub fn put_acks(&mut self, value: u16)

C dbAccess.c::putAcks (:1302-1315) — the only runtime writer of ACKS. Reached from dbPut for a DBR_PUT_ACKS request type, ABOVE the SPC_NOMOD gate.

The acknowledged severity is compared against the STORED unacknowledged severity acks, not the current sevr: an operator acknowledging at the severity that was latched into ACKS clears it even after sevr has since dropped. A too-low acknowledgement changes nothing and posts nothing; an acknowledgement of an already-clear ACKS still posts, which is C’s literal if (*psev >= precord->acks) (0 >= 0 holds).

Source

pub fn put_common_field_db_load( &mut self, name: &str, value: EpicsValue, ) -> CaResult<CommonFieldPutResult>

Set a common field value from the .db loader, which in C is a different converter with a different out-of-menu bound (dbStaticRun.c::dbPutStringNum; see [MenuBound::DbLoad]). It is what lets field(SSCN,"65535") — the menuScan “use SCAN” sentinel, out of the menu’s 0-9 range — load, while caput REC.SSCN 65535 is refused at runtime exactly as C refuses it.

Source

pub fn get_virtual_field(&self, name: &str) -> Option<EpicsValue>

Get virtual fields (NAME, RTYP).

Source

pub fn evaluate_alarms(&mut self)

Evaluate alarms based on record type and current value. Uses rec_gbl_set_sevr to accumulate into nsta/nsev.

CALC_ALARM is NOT raised here. C raises it inside the record’s own process() (calcRecord.c:121-123, calcoutRecord.c:238-241, sCalcoutRecord.c:357-363, aCalcoutRecord.c:304-305, swaitRecord.c:409-410), and in the port Record::check_alarms — which runs immediately before this — is that owner. It used to be raised here instead, keyed on a hardcoded rtype list plus a CALC_ALARM pseudo-field no DBD declares; swait is what that construction cost: it carried the flag but was not on the list, so a failed calcPerform alarmed nowhere.

Source

pub fn process_local( &mut self, ) -> CaResult<(ProcessSnapshot, Vec<(&'static str, EventMask)>)>

Basic process: process record, evaluate alarms, timestamp, build snapshot. This does NOT handle links — see process_with_context in database.rs.

Returns the value/log snapshot plus a list of alarm-field posts (SEVR/STAT/AMSG/ACKS) with their individual C event masks. SEVR is posted DBE_VALUE only; STAT/AMSG carry DBE_ALARM (sevr/amsg change) and/or DBE_VALUE (stat change). The caller must fire these via notify_field so a DBE_VALUE-only .SEVR subscriber is not missed on an alarm-only change and a DBE_ALARM-only subscriber is not wrongly notified — C parity with recGblResetAlarms (recGbl.c:201-220), matching the processing.rs link path.

Source

pub fn check_deadband_ext(&mut self) -> (bool, bool)

Source

pub fn make_monitor_snapshot(&self, field: &str, value: EpicsValue) -> Snapshot

Build a Snapshot for a given value, populated with the record’s display metadata. Uses the metadata cache so the populate cost is paid at most once per metadata-stable interval (cf. cached_metadata).

Source

pub fn notify_from_snapshot(&self, snapshot: &ProcessSnapshot)

Notify subscribers from a snapshot (call outside lock). Each entry carries its own posting mask: only subscribers whose mask intersects that field’s mask are notified, and the delivered MonitorEvent reports exactly that field’s classes (C db_post_events(prec, &field, mask) per-field granularity).

Source

pub fn notify_field(&mut self, field: &str, mask: EventMask)

Notify subscribers of a specific field, filtering by event mask.

Source

pub fn notify_record_alarm(&mut self)

C db_post_events(precord, NULL, DBE_ALARM): post a record-wide alarm event. Delivers to every subscriber on any field whose mask includes DBE_ALARM, each carrying its own monitored field’s current value (the per-field notify_field already filters by mask intersection). Used by the alarm-acknowledge (ACKT/ACKS) put path so an alarm-mask monitor on any field observes the acknowledgement.

Source

pub fn notify_field_with_origin( &mut self, field: &str, mask: EventMask, origin: u64, )

Notify subscribers with an origin tag for self-write filtering.

This is C db_post_events(precord, pfield, mask) for one field, and — per the last_posted contract — the poster that advances the already-published value when mask carries a value class. Taking &mut self is what makes that unbypassable: there is no way to publish a field’s value through the framework without the change detector learning that it was published.

Source

pub fn add_subscriber( &mut self, field: &str, sid: u32, data_type: DbFieldType, mask: u16, ) -> Option<EventReader>

Add a subscriber for a specific field. Returns None when the per-field subscriber cap (EPICS_CAS_MAX_SUBSCRIBERS_PER_PV) is reached. the parallel cap on ProcessVariable defends against a misbehaving client opening many MONITOR ops against one shared PV; the same defence is needed for record fields, which the CA server’s ChannelTarget::RecordField path lands on.

Source

pub fn add_subscriber_on( &mut self, user: &EventUser, field: &str, sid: u32, data_type: DbFieldType, mask: u16, ) -> Option<EventReader>

Add a field subscriber whose events queue on user’s event queue — C db_add_event with the circuit’s event_user as context. Every subscription on one CA circuit shares that queue and therefore its nDuplicates, so a duplicate queued for one of them releases the EVENTS_OFF drain for all of them (dbEvent.c:947). In-process consumers use Self::add_subscriber, which gives each its own event_user.

Source

pub fn attach_filter_to_last_subscriber( &mut self, field: &str, filter: Arc<dyn SubscriptionFilter>, ) -> bool

Attach a filter to the most recently added subscriber for field. Returns false when no subscriber exists yet on that field (call add_subscriber first). The CA / PVA channel-name parsers will use this once .{filter:opts} syntax is wired. Tests can also use it directly to compose filter chains.

Source

pub fn remove_subscriber(&mut self, sid: u32)

Remove a subscriber by subscription ID from all fields.

Source

pub fn set_subscriber_active(&mut self, sid: u32, active: bool)

Pause / resume one subscriber’s event flow at the source (db_event_disable / db_event_enable). active == false suppresses every subsequent post to this subscriber, so the record stops doing per-event work for it. Entries already queued stay queued and are still delivered, exactly as in C: db_event_disable only unlinks the subscription from the record’s monitor list (dbEvent.c:521-533) and never reaches into the event queue. No-op if no subscriber has this sid. The caller holds the record write lock, so this is exclusive with the read-locked post paths that consult Subscriber::active.

Source

pub fn cleanup_subscribers(&mut self)

Clean up subscriber rows whose consumer is gone.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more