pub struct RecordInstance {Show 14 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 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>>§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
impl RecordInstance
pub fn new(name: String, record: impl Record) -> Self
Sourcepub fn common_link_text(&self, field: &str) -> Option<&str>
pub fn common_link_text(&self, field: &str) -> Option<&str>
The raw text of one COMMON_LINK_FIELDS entry, or None for any
other field name.
Sourcepub fn common_link_cache_mut(&mut self, field: &str) -> Option<&mut ParsedLink>
pub fn common_link_cache_mut(&mut self, field: &str) -> Option<&mut ParsedLink>
The parse cache of one COMMON_LINK_FIELDS entry, or None for any
other field name. The only mutable handle on the cache outside
put_common_field, so the iocInit locality commit cannot reach a slot
that has no matching raw text.
Sourcepub fn link_backed_metadata_links(&self) -> &[String]
pub fn link_backed_metadata_links(&self) -> &[String]
The link fields whose target metadata this record’s rset serves — the
work list PvDatabase::resolve_link_backed_metadata resolves for a
batch post, and the set Self::link_backed_metadata_field_of answers
one field out of.
pub fn new_boxed(name: String, record: Box<dyn Record>) -> Self
Sourcepub fn soft_output_value(&self) -> Option<Option<EpicsValue>>
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. “Async Soft Channel” is NOT that: C’s
devXxxSoftCallback.c::write_xxx puts the same VAL/OVAL the plain soft
dset puts, only through dbPutLinkAsync (devAoSoftCallback.c:49,
devLoSoftCallback.c:49), and falls back to a synchronous dbPutLink
when the link has no LSET. Returning None for it made every
DTYP("Async Soft Channel") output record write nothing at all —
measured on pva2pva/testApp/testpvalink.db:30-35, whose longout
drives a pva OUT link that never fired.
Sourcepub fn set_info(&mut self, key: impl Into<String>, value: impl Into<String>)
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.
Sourcepub fn get_info(&self, key: &str) -> Option<&str>
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.
Sourcepub fn invalidate_metadata_cache(&self)
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.
Sourcepub fn notify_field_written(&self, field: &str)
pub fn notify_field_written(&self, field: &str)
Hook called by the database after a field is written. If the field is a
metadata-cache source, the cache is invalidated so the next snapshot
picks up the new value. Posts nothing — a caller that also owes the
DBE_PROPERTY event uses Self::notify_field_written_if_changed.
Field name is automatically uppercased.
Sourcepub fn notify_field_written_if_changed(
&mut self,
field: &str,
prev: Option<&EpicsValue>,
backing: LinkBacking<'_>,
)
pub fn notify_field_written_if_changed( &mut self, field: &str, prev: Option<&EpicsValue>, backing: LinkBacking<'_>, )
Like Self::notify_field_written, plus the DBE_PROPERTY post C
makes from dbPut — and both are skipped when the put did not actually
change the field’s value. Mirrors epics-base faac1df1: 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).
The two effects have independent gates. Invalidation follows
is_metadata_cache_source (what this port’s cache reads); the post
follows Self::field_posts_property (what the .dbd declares). A
field can be either without being both.
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 neither)
can keep using Self::notify_field_written.
backing is what the sweep needs and could not have: the post below
names EVERY subscribed field, so it reaches a link-backed one whenever a
client is monitoring it, and this method runs under the record’s own
write lock where the target’s lock cannot be taken. The put path that
calls it has already resolved one at its no-lock point.
Sourcepub fn is_no_mod(&self, field: &str) -> bool
pub fn is_no_mod(&self, field: &str) -> bool
C dbChannelSpecial(chan) == SPC_NOMOD — the 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, viadbPutSpecial(paddr, 0)) refuses the write — the port’scheck_no_modgate;rsrvCheckPut(rsrv/camessage.c:2540-2551) —if (dbChannelSpecial(pciu->dbch) == SPC_NOMOD) return 0;— which feeds the CAACCESS_RIGHTSwrite bit (camessage.c:1154-1156) as well as both put paths, so a client seesAccess: read, no writeand 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:
- the dbCommon
SPC_NOMODset below — common fields, so no record’sfield_listdeclares them; - the record type’s declaration, resolved by
Self::field_desc— the vendored.dbdwhenever one exists, and only for a record type that has no.dbdat all (motor,optics,scaler,std) the record’s own hand-written table, which for those Tier 3 types genuinely is their declaration; Record::field_no_mod— an SPC_NOMOD a record’scvt_dbaddrraises from its own state (compress VAL under BALG=LIFO,compressRecord.c:404-405), which a staticFieldDesccannot express.
field may be any case.
Sourcepub fn is_processing(&self) -> bool
pub fn is_processing(&self) -> bool
Check if the record is currently processing (PACT equivalent).
Sourcepub fn enter_pact(&self)
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.
Sourcepub fn leave_pact(&mut self) -> PactExit
pub fn leave_pact(&mut self) -> PactExit
C prec->pact = FALSE — the ONLY release of PACT.
The returned PactExit carries the release’s debt to the cycle tail,
where a queued put-notify is restarted — the omission the open-coded
processing.store(false) at the ODLY continuation and the three SIM/SDLY
releases made.
#[must_use] does NOT enforce that debt and never did: the lint fires on
an unused expression, so a site that binds the token with let and then
leaves by ? or an early return warns about nothing. The enforcement is
processing::CycleEndGuard, whose Drop pays the tail for every exit
that did not.
Sourcepub fn pact_exit_without_release(&self) -> PactExit
pub fn pact_exit_without_release(&self) -> PactExit
The cycle-tail token for a record this cycle did NOT release PACT on.
Still consults the queue: a notify parked behind an in-flight wait-set on an idle record is freed by the wait-set completion, and the tail is what promotes it.
Sourcepub fn notify_put_is_owned(&self) -> bool
pub fn notify_put_is_owned(&self) -> bool
C processNotifyCommon’s two defer tests (dbNotify.c:213, 225), as one
question: may a NEWLY ARRIVING put-notify take this record now?
true for an in-flight wait-set (precord->ppn), for PACT, and for a
non-empty restart list — the last so a notify arriving in the window
between a completion and the restart check cannot jump the queue.
A RESTARTED put is not asked this: it is already the record’s owner (C
precord->ppn == ppn, state notifyRestartCallbackRequested, which
dbNotify.c:213 exempts by name) and only PACT can stop it — see
Self::requeue_notify_put.
Sourcepub fn notify_put_has_owner(&self) -> bool
pub fn notify_put_has_owner(&self) -> bool
C processNotifyCommon’s FIRST defer test alone (dbNotify.c:213):
another processNotify owns this record, or one is already queued
behind it. Self::notify_put_is_owned folds in the PACT arm
(:225) as well.
A DBF link-field put waits on ownership but NOT on PACT. A bare sub
with an empty SNAM parks PACT=TRUE forever (subRecord.c:119-122), so
a link put that waited on the PACT arm there would never be written and
caput <sub>.INPA 0 would read back empty. Ownership carries no such
trap: the restart check drains the queue at every cycle end.
Sourcepub fn install_or_queue_notify(
&mut self,
completion: Sender<()>,
) -> Option<Arc<NotifyWaitSet>>
pub fn install_or_queue_notify( &mut self, completion: Sender<()>, ) -> Option<Arc<NotifyWaitSet>>
C ellSafeAdd(&precord->ppnr->restartList, &ppn->restartNode) — the
arriving put-notify joins the back of the queue, unwritten.
Infallible: C has no “refuse” arm here, and a refusal loses the client’s
write. Call only under Self::notify_put_is_owned.
Take this record’s put-notify slot, or queue behind whoever holds it.
C processNotifyCommon (dbNotify.c:211-231) has exactly two outcomes
and no third: the record is free and the notify takes it, or it is
owned and the notify joins precord->ppnr->restartList. There is no
refusal arm — ECA_PUTCBINPROG has one sender in all of base, the
60-second put-callback timeout in write_notify_action
(rsrv/camessage.c:1701 at R7.0.10).
None means queued, and the caller MUST NOT process: the replay
drives the record and fires the callback, so processing here would
run the cycle twice for one client request.
Ownership alone decides — NOT Self::notify_put_has_owner. A
non-empty restart list stops a fresh arrival at the entry gate, but a
replay reaching here has already been popped off that list and must
take the slot with its successors still queued behind it, exactly as
C restartCheck (dbNotify.c:158-168) assigns precord->ppn = pfirst
while leaving the rest of restartList in place.
Sourcepub fn join_put_notify(&mut self, src: Option<&Arc<NotifyWaitSet>>)
pub fn join_put_notify(&mut self, src: Option<&Arc<NotifyWaitSet>>)
C dbNotifyAdd (dbNotify.c:477-501): a link target joins the wait-set
of the put-notify driving the chain, so the initiator’s completion
waits for this record’s cycle too.
One of the two callers of take_notify_slot, the sole writer; the
other is Self::install_or_queue_notify. All three live here so
the slot has no assignment site outside this module — an open-coded one
elsewhere is how a wait-set came to be installed without the record’s
write gate.
A record already carrying a wait-set keeps it (C’s if (!pto->ppn …)
at :492), so this never displaces a live one, and the enter is
paired with the leave the target’s own cycle tail performs.
Sourcepub fn has_notify(&self) -> bool
pub fn has_notify(&self) -> bool
Whether a put-notify owns this record — C precord->ppn != NULL.
The public read of the slot. The wait-set itself stays crate-private so
no caller outside this crate can enter/leave a set it does not own,
which is the accounting NotifyWaitSet exists to keep.
pub fn queue_notify_put(&mut self, put: DeferredNotify)
Sourcepub fn resolve_field(&self, name: &str) -> Option<EpicsValue>
pub fn resolve_field(&self, name: &str) -> Option<EpicsValue>
Unified field resolution: record fields → common fields → virtual
fields — and, for a link field, C dbGet’s rendering of it.
This is the port’s dbGet (dbAccess.c:625-961): the read every
external reader arrives at, whether it came from
PvDatabase::get_pv
on behalf of a CA client, from dbgf, or from dbpr. C’s dbGet
sends DBF_INLINK/DBF_OUTLINK/DBF_FWDLINK to getLinkValue
(:944-947), which renders the link with dbGetString (:850-856),
so applying that here is what makes every reader agree without any of
them knowing the rule.
The STORE is still the text — Record::get_field — and that is what
the link layer parses. The two are not the same value and do not share
a name: C likewise reads precord->inp directly when it wants the
link and dbGet when it wants what a client would see.
Sourcepub fn resolve_field_stored(&self, name: &str) -> Option<EpicsValue>
pub fn resolve_field_stored(&self, name: &str) -> Option<EpicsValue>
Self::resolve_field without the reader’s view — what the field
HOLDS, which for a link field is the text C’s dbParseLink takes
(dbStaticLib.c:2246) rather than what dbGetString renders
(:1906-2050).
dbpr needs both of the same field, and in C they come from one
address: it prints the link’s resolved TYPE in front of the rendered
text (dbTest.c:1205-1224). Splitting the accessor chain here keeps
that one address — a second walk to find the stored text would be a
second answer to “which field is this”, and the round before this one
is what happens when those two disagree.
name must already be upper-case.
Sourcepub fn resolve_string_view_field(&self, name: &str) -> Option<EpicsValue>
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.
Sourcepub fn declared_field_type(&self, field: &str) -> Option<DbFieldType>
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_typedfield — C’scvt_dbaddroverwritespaddr->field_typefrom record state (FTVL,FTA,SDEF), and this port’scvt_dbaddris the variant the record stores; - a field with no
FieldDescat 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.
Sourcepub fn project_to_declared_type(
&self,
field: &str,
value: EpicsValue,
) -> EpicsValue
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.
Sourcepub fn client_field_value(&self, field: &str) -> Option<EpicsValue>
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.
Sourcepub fn snapshot_for_field(&self, field: &str) -> Option<Snapshot>
pub fn snapshot_for_field(&self, field: &str) -> Option<Snapshot>
Build a Snapshot with full metadata for the given field — for a field no link backs.
A link-backed field answers None here on purpose. Its metadata has to
be resolved from the target record, which needs a
PvDatabase and, because the
port has one lock per record instead of C’s per-lock-set recursive
mutex, has to happen with no record lock held. That is
PvDatabase::channel_snapshot_for_field,
and it is the only entry point that can serve one. Answering None
rather than a seeded snapshot is what makes a caller that reached for
the wrong door serve nothing instead of something stale.
Sourcepub fn snapshot_for_field_with(
&self,
field: &str,
backing: LinkBacking<'_>,
) -> Option<Snapshot>
pub fn snapshot_for_field_with( &self, field: &str, backing: LinkBacking<'_>, ) -> Option<Snapshot>
Self::snapshot_for_field with the link metadata the caller resolved
for this build. PvDatabase is the intended caller; see LinkBacking.
Sourcepub fn channel_field_value(
&self,
field: &str,
string_view: bool,
) -> Option<EpicsValue>
pub fn channel_field_value( &self, field: &str, string_view: bool, ) -> Option<EpicsValue>
The value a channel bound to field serves, through the $ view
the channel was bound with.
dbChannelCreate decides the view ONCE, at bind time
(dbChannel.c:486-505), and every delivery path then reads through
the dbChannel it produced; this is that single read. Callers must
not re-derive it: resolving the bare field name answers “yes” for
VAL whatever its type, so a path that does drops the eligibility
half of the view entirely and admits REC.VAL$ on a DBF_DOUBLE.
None is S_dbLib_fieldNotFound: the record has no such field, or
$ was applied to a field that cannot be re-viewed as a character
array (see Self::resolve_string_view_field).
Sourcepub fn channel_snapshot_for_field(
&self,
field: &str,
string_view: bool,
backing: LinkBacking<'_>,
) -> Option<Snapshot>
pub fn channel_snapshot_for_field( &self, field: &str, string_view: bool, backing: LinkBacking<'_>, ) -> Option<Snapshot>
Self::snapshot_for_field_with through the same $ view as
Self::channel_field_value — the metadata is the field’s either
way, only the value is re-viewed.
This is the _with variant deliberately: the view decides the VALUE,
backing decides the METADATA, and the two are independent. A caller
that has resolved a LinkBacking passes it straight through, so a
link-backed $ member keeps its target’s units/precision.
Sourcepub fn property_support_for_field(&self, field: &str) -> PropertySupport
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.
Sourcepub fn get_common_field(&self, name: &str) -> Option<EpicsValue>
pub fn get_common_field(&self, name: &str) -> Option<EpicsValue>
Get a common field value.
Sourcepub fn put_common_field(
&mut self,
name: &str,
value: EpicsValue,
) -> CaResult<CommonFieldPutResult>
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.
Sourcepub fn set_scan(&mut self, new_scan: ScanType) -> CommonFieldPutResult
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.
Sourcepub fn rec_gbl_save_simm(&mut self)
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.
Sourcepub fn rec_gbl_check_simm(&mut self) -> CommonFieldPutResult
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.
Sourcepub fn put_ackt(&mut self, value: u16, backing: LinkBacking<'_>)
pub fn put_ackt(&mut self, value: u16, backing: LinkBacking<'_>)
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).
Sourcepub fn put_acks(&mut self, value: u16, backing: LinkBacking<'_>)
pub fn put_acks(&mut self, value: u16, backing: LinkBacking<'_>)
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).
Sourcepub fn put_common_field_db_load(
&mut self,
name: &str,
value: EpicsValue,
) -> CaResult<CommonFieldPutResult>
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.
Sourcepub fn get_virtual_field(&self, name: &str) -> Option<EpicsValue>
pub fn get_virtual_field(&self, name: &str) -> Option<EpicsValue>
Get virtual fields (NAME, RTYP).
Sourcepub fn evaluate_alarms(&mut self)
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.
Sourcepub fn process_local(
&mut self,
) -> CaResult<(ProcessSnapshot, Vec<(&'static str, EventMask)>)>
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:202-222), matching the
processing.rs link path.
pub fn check_deadband_ext(&mut self) -> (bool, bool)
Sourcepub fn make_monitor_snapshot(
&self,
field: &str,
value: EpicsValue,
backing: LinkBacking<'_>,
) -> Snapshot
pub fn make_monitor_snapshot( &self, field: &str, value: EpicsValue, backing: LinkBacking<'_>, ) -> Snapshot
Build a Snapshot for a given value, populated with the record’s display
metadata and the link metadata the poster resolved for this batch. Uses
the metadata cache so the populate cost is paid at most once per
metadata-stable interval (cf. cached_metadata).
There is deliberately no backing-less form. One existed, defaulting to
LinkBacking::none, and it made “nothing was resolved” the thing a
caller says by saying nothing — which is how the DBE_PROPERTY sweep
came to post CALC.A with the calc’s own precision (see
link_backed_metadata_is_read_live.rs). A caller with nothing to
resolve still writes LinkBacking::none(), and then it is a claim a
reviewer can see and check.
The monitor path reaches the same one consumer the GET path does
(finish_field_snapshot -> route_field_metadata), so it carried the
same defect: measured on the wire, a camonitor -s on a calc’s A
after caput TARGET.PREC 4 with the source never processed printed
5.0 where C printed 5.0000. The resolve cannot happen here — the
post runs with the record’s own lock held — so the caller that owns the
process/put cycle resolves it at a point where no lock is held and
hands it in.
Sourcepub fn notify_from_snapshot(
&self,
snapshot: &ProcessSnapshot,
backing: LinkBacking<'_>,
)
pub fn notify_from_snapshot( &self, snapshot: &ProcessSnapshot, backing: LinkBacking<'_>, )
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 that intersection — C
db_post_events(prec, &field, mask) per-field granularity, then
pLog->mask = caEventMask & pevent->select per subscriber.
backing is the link metadata the process cycle resolved for this
batch, at its own no-lock-held point. See Self::make_monitor_snapshot
for why it has no default.
Sourcepub fn notify_field(&mut self, field: &str, mask: EventMask)
pub fn notify_field(&mut self, field: &str, mask: EventMask)
Notify subscribers of a specific field, filtering by event mask.
The last wrapper that still answers for its callers: none() here is a
claim that no caller of this function names a link-backed field, and it
is made once for 25 production call sites rather than at each of them.
Self::notify_field_backed is the form for a caller that cannot make
that claim.
Sourcepub fn notify_field_backed(
&mut self,
field: &str,
mask: EventMask,
backing: LinkBacking<'_>,
)
pub fn notify_field_backed( &mut self, field: &str, mask: EventMask, backing: LinkBacking<'_>, )
Self::notify_field for a poster that may name a link-backed field
and has resolved its backing.
Sourcepub fn notify_record_alarm(&mut self, backing: LinkBacking<'_>)
pub fn notify_record_alarm(&mut self, backing: LinkBacking<'_>)
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.
Sourcepub fn notify_field_with_origin(
&mut self,
field: &str,
mask: EventMask,
origin: u64,
backing: LinkBacking<'_>,
)
pub fn notify_field_with_origin( &mut self, field: &str, mask: EventMask, origin: u64, backing: LinkBacking<'_>, )
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.
backing is the link metadata the put path resolved for this post, at
its own no-lock-held point. See Self::make_monitor_snapshot for why
it has no default.
Sourcepub fn add_subscriber(
&mut self,
field: &str,
sid: u32,
data_type: DbFieldType,
mask: u16,
) -> Option<EventReader>
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.
Sourcepub fn add_subscriber_on(
&mut self,
user: &EventUser,
field: &str,
sid: u32,
data_type: DbFieldType,
mask: u16,
) -> Option<EventReader>
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.
Sourcepub fn attach_filter_to_last_subscriber(
&mut self,
field: &str,
filter: Arc<dyn SubscriptionFilter>,
) -> bool
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.
Sourcepub fn remove_subscriber(&mut self, sid: u32)
pub fn remove_subscriber(&mut self, sid: u32)
Remove a subscriber by subscription ID from all fields.
Sourcepub fn is_destroyed(&self) -> bool
pub fn is_destroyed(&self) -> bool
Whether Self::destroy has run.
Sourcepub fn set_subscriber_active(&mut self, sid: u32, active: bool)
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:524-535) 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.
Sourcepub fn cleanup_subscribers(&mut self)
pub fn cleanup_subscribers(&mut self)
Clean up subscriber rows whose consumer is gone.