1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! `interruptAccept` — the process-global gate that says record processing and
//! I/O Intr callbacks are live.
//!
//! # Why this exists
//!
//! C's asyn param library defers every `callParamCallbacks` until the IOC has
//! finished wiring records to their driver. `paramList::callCallbacks` opens
//! with `if (!interruptAccept) return asynSuccess;` and returns **before**
//! `flags.clear()` (`asynPortDriver.cpp:838,871`), so every callback fired
//! while the IOC is still building is a no-op that *preserves* the accumulated
//! changed-flags. A per-port thread then does the callbacks exactly once, the
//! moment `interruptAccept` goes true (`callbackThread::run`,
//! `asynPortDriver.cpp:923-937`), delivering every seeded read-only value
//! (`Manufacturer`, `MaxSizeX`, …) to the `_RBV` records that only just
//! registered their interrupts.
//!
//! Without this gate the port's `call_param_callbacks` clears the changed-flags
//! whenever it happens to run first — a driver's own acquisition/array task can
//! fire it before `iocInit` wires the records — and a read-only parameter set
//! once at construction is then lost for the life of the process: its `_RBV`
//! record sits at the `.db` default forever. This is the owner of the flag that
//! `asyn-rs`'s `PortDriverBase::call_param_callbacks` consults and that the
//! scan facility drives.
//!
//! # The default, and its single owner
//!
//! C initialises the flag `FALSE` (`dbAccess.c:67`); so does this port. The gate
//! is thus closed at process start by construction, and the scan facility is its
//! single owner thereafter: `scan_run` sets it true (C `scanRun`,
//! `dbScan.c:218`), `scan_pause`/`scan_stop` set it false (`dbScan.c:241,165`).
//! No other code writes it, so the seeds a port sets at construction are
//! guaranteed to survive to the `scan_run` boot flush without depending on any
//! bring-up path having lowered a gate first.
//!
//! C's non-IOC translation unit compiles `static int interruptAccept = 1`
//! instead (`asynPortDriver.cpp:23-25`) so a bare `asynPortDriver` used outside
//! an IOC still delivers. This port does not take that convenience default: it
//! serves IOCs, where the gate is opened by `scan_run` at `iocRun`. A unit test
//! that exercises `call_param_callbacks` without an `iocInit` must therefore
//! open the gate itself with [`set_interrupts_accepted`], the precondition that
//! always holds by the time records process in a running IOC.
//!
//! # The one-shot flush
//!
//! [`set_interrupts_accepted`] invokes the callbacks registered through
//! [`on_interrupts_accepted`] on each `false → true` edge — never when the flag
//! is written `true` while already `true`, so a pause→resume does not re-flush.
//! `asyn-rs` registers one such callback that sweeps every port's changed
//! params once; it is this crate's stand-in for C's per-port `callbackThread`.
use Mutex;
use ;
/// C `volatile int interruptAccept`. Defaults `false` (`dbAccess.c:67`); the
/// scan facility is its single owner thereafter — see the module docs.
static ACCEPTED: AtomicBool = new;
/// Callbacks fired on the `false → true` edge — the crate-level analogue of the
/// per-port `callbackThread` C creates in every `asynPortDriver` constructor.
/// `Arc` so the list can be snapshotted and the lock dropped before any
/// callback runs (a callback that registers another must not deadlock), the
/// same discipline as `init_hook_announce`.
type OnAccept = Arc;
static ON_ACCEPT: = new;
/// Whether record processing / I/O Intr callbacks are live — C `interruptAccept`.
///
/// The single reader is `asyn-rs`'s `PortDriverBase::call_param_callbacks`,
/// which returns without consuming its changed-flags while this is `false`.
/// Set the gate, and on a `false → true` edge run every [`on_interrupts_accepted`]
/// callback once.
///
/// C reaches the true edge inside `scanRun` (`dbScan.c:218`) and the false edge
/// inside `scanPause`/`scanStop` (`dbScan.c:241,165`); the IOC's scan facility
/// is the single owner that calls this. The edge guard means a redundant
/// `true`-while-`true` (a pause→resume that never lowered the flag, or two scan
/// starts) fires nothing, matching C's once-per-process `callbackThread`.
/// Register a callback for the next `false → true` edge of the gate.
///
/// Registration does **not** fire the callback, even if the flag is already
/// `true`: the caller (`asyn-rs`'s boot-flush arm) registers this once, and a
/// port that appears after the flag is already up is that caller's concern, not
/// this edge's. Process-global, matching C's single per-port thread list.