asyn_rs/port.rs
1//! Port driver base and trait.
2//!
3//! # I/O Model
4//!
5//! Ports are driven by a `PortActor` running on a dedicated thread.
6//! The actor exclusively owns the driver and processes requests from a channel.
7//!
8//! **Cache path** (default `read_*`/`write_*` methods):
9//! - Default implementations operate on the parameter cache (non-blocking).
10//! - Background tasks update cache via `set_*_param()` + `call_param_callbacks()`.
11//!
12//! **Actor path** (requests submitted via [`crate::port_handle::PortHandle`]):
13//! - Each port gets a dedicated actor thread that dispatches requests to driver methods.
14//! - `can_block` indicates the port may perform blocking I/O.
15
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::time::{Duration, Instant, SystemTime};
20
21use std::any::Any;
22
23/// C `autoConnectDevice` reconnect throttle window (asynManager.c:713).
24/// A disconnected `auto_connect` device is refused a fresh connect attempt
25/// until this much time has elapsed since its last connect/disconnect
26/// transition or attempt, bounding reconnect storms to one attempt per
27/// window.
28const AUTO_CONNECT_THROTTLE: Duration = Duration::from_secs(2);
29
30/// First autonomous connect-retry delay after a port drops. C
31/// `exceptionDisconnect` arms the port's connect timer at `.01` seconds
32/// (asynManager.c:2181-2182), so the reconnect is attempted essentially
33/// immediately and then backs off to [`DEFAULT_SECONDS_BETWEEN_PORT_CONNECT`].
34const CONNECT_RETRY_INITIAL: Duration = Duration::from_millis(10);
35
36/// C `DEFAULT_SECONDS_BETWEEN_PORT_CONNECT` (asynManager.c:48) — the interval
37/// `portConnectProcessCallback` re-arms the connect timer at after a failed
38/// attempt (asynManager.c:3281).
39const DEFAULT_SECONDS_BETWEEN_PORT_CONNECT: Duration = Duration::from_secs(20);
40
41/// Per-address device state for multi-device ports.
42#[derive(Debug, Clone)]
43pub struct DeviceState {
44 pub connected: bool,
45 pub enabled: bool,
46 pub auto_connect: bool,
47 /// Monotonic instant of the last connect/disconnect transition or
48 /// auto-connect attempt for this device — the anchor for the 2s
49 /// reconnect throttle (C `dpCommon.lastConnectDisconnect`). `None`
50 /// mirrors C's zero-initialised timestamp: the first attempt is
51 /// always permitted.
52 pub last_connect_disconnect: Option<Instant>,
53}
54
55impl Default for DeviceState {
56 fn default() -> Self {
57 Self {
58 connected: true,
59 enabled: true,
60 auto_connect: true,
61 last_connect_disconnect: None,
62 }
63 }
64}
65
66/// One device's end-of-string terminators — C's `eosPvt.eosIn` / `eosPvt.eosOut`
67/// (asynInterposeEos.c:44-52), which exist once per (port, addr).
68#[derive(Debug, Clone, Default)]
69pub struct DeviceEos {
70 /// Input EOS sequence (max 2 bytes). Empty = no input EOS detection.
71 pub input: Vec<u8>,
72 /// Output EOS sequence (max 2 bytes). Empty = no output EOS append.
73 pub output: Vec<u8>,
74}
75
76/// The device an EOS hook's `asynUser` selects — the single owner of the rule,
77/// shared by [`PortDriverBase`] and the EOS interpose so the terminator a
78/// `setInputEos` writes is the one the next `read` on that user applies.
79///
80/// C creates the EOS interpose per (port, addr) and every hook takes the
81/// `asynUser` (asynInterposeEos.c:288-296), so on a multi-device port the addr
82/// picks the instance. On a port that never declared `ASYN_MULTIDEVICE` there
83/// are no devices to pick from: `findDpCommon` (asynManager.c:536-544) and
84/// `findInterface` resolve *every* addr to the port itself, so `asynSetEos`
85/// with addr 0 and with addr -1 must reach the same terminator. That collapse
86/// is what the `-1` key below is.
87pub fn eos_device_key(multi_device: bool, addr: i32) -> i32 {
88 dp_common_key(multi_device, addr).unwrap_or(-1)
89}
90
91/// C `findDpCommon` (asynManager.c:538-545) as a key: which `dpCommon` a
92/// `(port, addr)` pair names. `Some(a)` is that device's own slot; `None` is
93/// the port's — C's `!(pport->attributes&ASYN_MULTIDEVICE) || !pdevice` arm,
94/// which a single-device port and a port-level user both take (`connectDevice`
95/// creates a device node only for `addr >= 0`, asynManager.c:1349-1352).
96///
97/// One function for every consumer of the resolution — connection state, the
98/// trace configuration ([`crate::trace`]) and the EOS terminators
99/// ([`eos_device_key`]) — because C asks it once, in `findDpCommon`, and then
100/// reads *every* field off the one struct it returned: `enabled`, `connected`,
101/// `autoConnect`, `numberConnects`, `lastConnectDisconnect` and `trace`
102/// (through `findTracePvt`, :546-551). Separate copies of the rule are how the
103/// trace mask came to be per-device while `enabled` beside it stayed port-wide.
104pub fn dp_common_key(multi_device: bool, addr: i32) -> Option<i32> {
105 (multi_device && addr >= 0).then_some(addr)
106}
107
108use crate::error::{AsynError, AsynResult, AsynStatus};
109use crate::exception::{AsynException, ExceptionEvent, ExceptionManager};
110use crate::interfaces::InterfaceType;
111use crate::interpose::{
112 EomReason, EosSet, OctetInterpose, OctetInterposeStack, OctetNext, OctetReadResult,
113};
114use crate::interrupt::{InterruptManager, InterruptValue, OctetFanOut};
115use crate::param::{EnumEntry, InterruptReason, ParamList, ParamType, ParamValue};
116use crate::trace::TraceManager;
117use crate::user::{AsynUser, ConnectCheck};
118
119/// C asyn `queueRequest` priority. In asyn-rs this exists as compatibility
120/// metadata only — there is no actual request queue or priority-based scheduling.
121/// Drivers manage their own async tasks directly.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
123pub enum QueuePriority {
124 Low = 0,
125 #[default]
126 Medium = 1,
127 High = 2,
128 /// Connect/disconnect operations — processed even when disabled/disconnected.
129 Connect = 3,
130}
131
132/// Port configuration flags.
133#[derive(Debug, Clone, Copy)]
134pub struct PortFlags {
135 /// True if port supports multiple sub-addresses (ASYN_MULTIDEVICE).
136 pub multi_device: bool,
137 /// True if port can block (ASYN_CANBLOCK).
138 ///
139 /// When `true`, the port gets a dedicated worker thread that serializes I/O via a
140 /// priority queue (matching C asyn's per-port thread model).
141 ///
142 /// When `false`, requests execute synchronously inline on the caller's thread
143 /// (no worker thread is spawned). This is appropriate for non-blocking drivers
144 /// whose `io_*` methods return immediately (e.g., cache-based parameter access).
145 pub can_block: bool,
146 /// True if port can be destroyed via shutdown_port (ASYN_DESTRUCTIBLE).
147 pub destructible: bool,
148}
149
150impl Default for PortFlags {
151 fn default() -> Self {
152 // `destructible: false` is the C asyn convention — see
153 // asynDriver.h:97 (`#define ASYN_DESTRUCTIBLE 0x0004`) — the
154 // attribute is opt-in via `pasynManager->registerPort(..., attr)`
155 // and `asynManager::shutdownPort` refuses to act on ports
156 // that did not opt in. Defaulting to `true` here over-applied
157 // shutdown rights to every driver that built PortFlags via
158 // `..PortFlags::default()`.
159 Self {
160 multi_device: false,
161 can_block: false,
162 destructible: false,
163 }
164 }
165}
166
167/// Base state shared by all port drivers.
168/// Contains the parameter library, interrupt manager, and connection state.
169///
170/// # Interpose concurrency
171///
172/// `interpose_octet` requires `&mut self` for all operations (both `push` and
173/// `dispatch_*`). Since `PortDriverBase` is always behind `Arc<Mutex<dyn PortDriver>>`,
174/// any access to `interpose_octet` requires the port lock. This naturally
175/// serializes interpose modifications with I/O dispatch — no additional
176/// synchronization is needed. **Callers must never modify the interpose stack
177/// without holding the port lock.**
178/// Where a port's `connected` truth lives.
179///
180/// `Own` — the port opens and closes its own link (every driver that dials out:
181/// IP, serial, USB-TMC, VXI-11, …), so its own cell is the truth.
182///
183/// `Shared` — the link belongs to another object and this port merely serves it.
184/// C models the case with a real child port whose `connectIt`/`closeConnection`
185/// the *owner* drives (`drvAsynIPServerPort.c:358-360` — the listener calls
186/// `pasynCommonSyncIO->connectDevice` on the child the moment it hands it a
187/// socket). Sharing the owner's cell is the same thing without the round trip,
188/// and it is what makes "the owner holds a live socket, the port says
189/// disconnected" unrepresentable rather than merely unlikely.
190#[derive(Debug, Clone)]
191enum Connection {
192 Own(bool),
193 Shared(Arc<AtomicBool>),
194}
195
196impl Connection {
197 fn get(&self) -> bool {
198 match self {
199 Connection::Own(c) => *c,
200 Connection::Shared(cell) => cell.load(Ordering::Acquire),
201 }
202 }
203}
204
205pub struct PortDriverBase {
206 pub port_name: String,
207 pub max_addr: usize,
208 pub flags: PortFlags,
209 pub params: ParamList,
210 pub interrupts: InterruptManager,
211 /// Whether the port's transport is up — read it with [`Self::is_connected`],
212 /// move it with [`Self::set_connected`].
213 ///
214 /// It is not a plain `bool` because not every port *owns* its link. An
215 /// IP-server child port serves a socket that lives in the parent's
216 /// [`crate::drivers::ip_server_port::ClientSlot`]: the listener assigns and
217 /// clears it, and the child cannot see either edge. A cached copy therefore
218 /// went stale in exactly the way that matters — the slot held a live client
219 /// while the child port still said `asynDisconnected` and refused every
220 /// read and write, forever (R13-50). Such a port shares the owner's cell
221 /// instead of copying it, so that state cannot be constructed.
222 connected: Connection,
223 /// The last value fanned out to listeners. Memory for the edge detector in
224 /// [`Self::sync_connection_edge`], never an answer to "is the port up?" —
225 /// [`Self::is_connected`] is the only thing that answers that, and it reads
226 /// the truth.
227 last_announced: bool,
228 pub enabled: bool,
229 pub auto_connect: bool,
230 /// C `dpCommon.defunct` (asynManager.c:2284) — the port was torn down via
231 /// `shutdownPort` and is gone for good. Read it with [`Self::is_defunct`].
232 ///
233 /// **Invariant: `defunct ⟹ !enabled`.** C establishes it in `shutdownPort`,
234 /// which clears `enabled` *and* sets `defunct` in the same breath (:2283-2284)
235 /// with the comment that disabling is what short-circuits `queueRequest` —
236 /// and indeed `queueRequest` has no defunct branch at all (:1541-1552): a
237 /// defunct port is refused as a *disabled* one, "port %s disabled". C asks
238 /// `defunct` in nine places, and which dpCommon it asks is what our model
239 /// deviates on: `report` (:1041), `findInterface` (:1487),
240 /// `registerInterrupt` (:2424), `getInterruptPvt` (:2475),
241 /// `addInterruptUser` (:2561) and `removeInterruptUser` (:2603) all read
242 /// `pport->dpc.defunct`, while `enable` (:2238, so the port cannot be
243 /// re-enabled), `lockPort` (:1792) and `shutdownPort`'s own idempotence
244 /// check (:2268) read `findDpCommon`'s.
245 ///
246 /// **We model `defunct` at the port, C at whatever dpCommon the shutting-down
247 /// user resolves to, and on a multi-device port that is observably
248 /// different.** `asynShutdownPort` binds at addr 0
249 /// (asynShellCommands.c:1331), so on an `ASYN_MULTIDEVICE` port C flags
250 /// device 0 and never sets `pport->dpc.defunct` at all. Measured against
251 /// libasyn at pin 731d616e, a port registered
252 /// `ASYN_MULTIDEVICE|ASYN_DESTRUCTIBLE` and shut down through an addr-0
253 /// user still answers `isEnabled`=1 at addr -1 and addr 1, still accepts
254 /// `enable` and `lockPort` there, still hands out the interface from
255 /// `findInterface` (with `drvPvt` now NULL — a latent deref, not a
256 /// refusal), and `asynReport` prints the port normally instead of
257 /// "destroyed". Only the single-device case, where `findDpCommon` resolves
258 /// to the port's own dpc, refuses port-wide the way we always do. Taking
259 /// the port down whole is deliberate: the interfaces C nulls belong to the
260 /// port, so the addresses C leaves enabled are enabled onto a driver that
261 /// is gone.
262 ///
263 /// The field is private so [`Self::shutdown_lifecycle`] is the only way to
264 /// set it, which is what makes the invariant hold by construction: nothing can
265 /// build a port that is defunct but still enabled, so no gate needs to ask
266 /// about defunct to refuse it (R15-50).
267 defunct: bool,
268 /// This port's announcement channel — C `dpCommon.exceptionUserList` plus
269 /// the `notifyPortThread` signal that `announceExceptionOccurred` ends with
270 /// (asynManager.c:611-637). Detachable ([`Self::exception_announcer`]) so a
271 /// worker thread the driver owns — the IP-server accept loop — announces
272 /// through the same counter and the same list as the actor does, instead of
273 /// reaching around them.
274 pub(crate) announcer: ExceptionAnnouncer,
275 pub options: HashMap<String, String>,
276 /// The EOS terminators, keyed per device the way C keys them: an `eosPvt`
277 /// is created per `asynInterposeEosConfig(portName, addr, ...)`
278 /// (asynInterposeEos.c:84-120), and every EOS hook takes the `asynUser`
279 /// that selects it (:288-296). Two devices on one multi-device port hold
280 /// two different terminators — a single port-wide pair could not.
281 ///
282 /// Keyed by [`eos_device_key`], so a port that never declared
283 /// `ASYN_MULTIDEVICE` collapses every addr onto one entry (C's
284 /// `findDpCommon`/`findInterface` resolve any addr to the port itself).
285 eos: HashMap<i32, DeviceEos>,
286 pub interpose_octet: OctetInterposeStack,
287 /// Trace configuration — C `dpCommon.trace`, inited by `dpCommonInit`
288 /// (asynManager.c:528). Same
289 /// owner as [`Self::announcer`]: bound by
290 /// [`crate::services::PortServices::bind`] at port creation.
291 pub(crate) trace: Option<Arc<TraceManager>>,
292 /// C `octetPvt.interruptProcess` — the last argument of
293 /// `pasynOctetBase->initialize` (asynOctetBase.c:161-169). When set, every
294 /// successful octet read fans the data out to the port's octet interrupt
295 /// users (`readIt` → `callInterruptUsers`, :224-238). The stream drivers set
296 /// it — drvAsynIPPort.c:1055, drvAsynSerialPort.c:1125-1126,
297 /// drvAsynSerialPortWin32.c:798-799, drvAsynFTDIPort.cpp:616 — and it is what
298 /// makes a `stringin`/`waveform` with `SCAN="I/O Intr"` on such a port
299 /// process at all. Parameter-cache ports (echoDriver, USBTMC, GPIB,
300 /// IP-server) pass 0 and are unaffected.
301 pub octet_interrupt_process: bool,
302 /// Per-address device state for multi-device ports.
303 pub device_states: HashMap<i32, DeviceState>,
304 /// Timestamp source callback for custom timestamps.
305 pub timestamp_source: Option<Arc<dyn Fn() -> SystemTime + Send + Sync>>,
306 /// Port-level anchor for the 2s auto-reconnect throttle — the
307 /// monotonic instant of the last connect/disconnect transition or
308 /// auto-connect attempt (C `dpCommon.lastConnectDisconnect`). `None`
309 /// = no transition yet, so the first attempt is always permitted.
310 pub last_connect_disconnect: Option<Instant>,
311 /// Deadline for the next *autonomous* connect attempt — the Rust
312 /// equivalent of C's per-port `connectTimer` (`port.connectTimer`,
313 /// asynManager.c:223). `None` = disarmed.
314 ///
315 /// Armed by [`Self::set_connected`] on a disconnect (C
316 /// `exceptionDisconnect`, asynManager.c:2181-2182) and re-armed by the
317 /// actor after a failed attempt; cleared on connect. The actor is what
318 /// services it — see `PortActor::service_connect_timer` — so this field
319 /// is the whole handoff between the transition owner and the timer.
320 pub connect_retry_at: Option<Instant>,
321 /// Back-off between failed autonomous connect attempts. C
322 /// `port.secondsBetweenPortConnect`, initialised to
323 /// `DEFAULT_SECONDS_BETWEEN_PORT_CONNECT` = 20 s (asynManager.c:48, 3249)
324 /// and used to re-arm the timer at asynManager.c:3281.
325 pub seconds_between_port_connect: Duration,
326 /// How many times this port's link has come up — C `dpCommon.numberConnects`
327 /// (asynManager.c:150), incremented by `exceptionConnect` (:2158) and printed
328 /// by `asynReport` (:1057-1060). Its owner is
329 /// [`Self::sync_connection_edge`], the same edge owner that raises the
330 /// exception, so a connect that was never published is never counted.
331 pub number_connects: u64,
332}
333
334/// The one way to announce a port exception — C `announceExceptionOccurred`
335/// (asynManager.c:611-637): fan the event out over the port's exception list and
336/// signal `notifyPortThread` (:635-636).
337///
338/// It is a detachable handle rather than a method on [`PortDriverBase`] because
339/// the announcement is not the actor thread's private business: a driver-owned
340/// worker (the IP-server accept loop) also transitions a device, and C's
341/// `connectionListener` thread announces the same way the port thread does. A
342/// clone of this handle is that capability; it carries the wake counter with it,
343/// so an off-thread announcement still wakes the actor
344/// ([`PortDriverBase::exceptions_announced`]).
345#[derive(Clone)]
346pub struct ExceptionAnnouncer {
347 port_name: String,
348 /// Set once by [`crate::services::PortServices::bind`] at port creation, so
349 /// every clone taken afterwards carries the IOC's exception list.
350 sink: Option<Arc<ExceptionManager>>,
351 /// Shared so a clone's announcement is visible to the actor's count.
352 announced: Arc<AtomicU64>,
353}
354
355impl ExceptionAnnouncer {
356 fn new(port_name: &str) -> Self {
357 Self {
358 port_name: port_name.to_string(),
359 sink: None,
360 announced: Arc::new(AtomicU64::new(0)),
361 }
362 }
363
364 /// Announce. A port with no sink still counts the announcement — C's
365 /// fan-out over an empty list still signals the port thread — so the count
366 /// moves before the sink is consulted.
367 pub fn announce(&self, exception: AsynException, addr: i32) {
368 self.announced.fetch_add(1, Ordering::Release);
369 if let Some(ref sink) = self.sink {
370 sink.announce(&ExceptionEvent {
371 port_name: self.port_name.clone(),
372 exception,
373 addr,
374 });
375 }
376 }
377
378 fn count(&self) -> u64 {
379 self.announced.load(Ordering::Acquire)
380 }
381}
382
383impl PortDriverBase {
384 pub fn new(port_name: &str, max_addr: usize, flags: PortFlags) -> Self {
385 Self {
386 port_name: port_name.to_string(),
387 max_addr: max_addr.max(1),
388 flags,
389 params: ParamList::new(max_addr),
390 interrupts: InterruptManager::new(256),
391 connected: Connection::Own(true),
392 last_announced: true,
393 enabled: true,
394 auto_connect: true,
395 defunct: false,
396 announcer: ExceptionAnnouncer::new(port_name),
397 options: HashMap::new(),
398 eos: HashMap::new(),
399 interpose_octet: OctetInterposeStack::new(flags.multi_device),
400 trace: None,
401 octet_interrupt_process: false,
402 device_states: HashMap::new(),
403 timestamp_source: None,
404 last_connect_disconnect: None,
405 connect_retry_at: None,
406 seconds_between_port_connect: DEFAULT_SECONDS_BETWEEN_PORT_CONNECT,
407 number_connects: 0,
408 }
409 }
410
411 /// The EOS entry the given `asynUser` addr selects — see [`eos_device_key`].
412 pub fn eos_key(&self, addr: i32) -> i32 {
413 eos_device_key(self.flags.multi_device, addr)
414 }
415
416 /// This device's input EOS. An addr that has never been configured has an
417 /// empty terminator, C's zero-initialised `eosPvt.eosInLen`.
418 pub fn input_eos(&self, addr: i32) -> &[u8] {
419 self.eos
420 .get(&self.eos_key(addr))
421 .map_or(&[][..], |e| &e.input)
422 }
423
424 /// This device's output EOS (see [`Self::input_eos`]).
425 pub fn output_eos(&self, addr: i32) -> &[u8] {
426 self.eos
427 .get(&self.eos_key(addr))
428 .map_or(&[][..], |e| &e.output)
429 }
430
431 /// Store this device's input terminator in the base's cache — the write
432 /// companion of [`Self::input_eos`], for a driver that implements C's
433 /// `asynOctet::setInputEos` itself instead of leaving it NULL for
434 /// `asynInterposeEos` to serve. A multiDevice port has no choice: C
435 /// refuses to install the interpose there at all (asynOctetBase.c:149-153).
436 pub fn set_input_eos(&mut self, addr: i32, eos: &[u8]) {
437 self.eos_entry(addr).input = eos.to_vec();
438 }
439
440 /// The output half of [`Self::set_input_eos`].
441 pub fn set_output_eos(&mut self, addr: i32, eos: &[u8]) {
442 self.eos_entry(addr).output = eos.to_vec();
443 }
444
445 /// The write owner for this device's terminators — the queryable cache the
446 /// EOS readback (`get_input_eos`, the binary-suppress save/restore) reads.
447 /// The forward to the interpose stack lives in the `PortDriver` hook and in
448 /// the two public setters above.
449 fn eos_entry(&mut self, addr: i32) -> &mut DeviceEos {
450 let key = self.eos_key(addr);
451 self.eos.entry(key).or_default()
452 }
453
454 /// Announce an exception through the global exception manager (if injected).
455 ///
456 /// C `announceExceptionOccurred` (asynManager.c:611-637) ends by signalling
457 /// `notifyPortThread` on a CANBLOCK port (:635-636) — the announcement *is* a
458 /// port-thread wake, which is why `asynEnable(port,1)` on a down port ends in
459 /// a connect attempt. [`Self::exceptions_announced`] is how the actor sees
460 /// that signal, so the count moves here and nowhere else: a port with no
461 /// exception sink still announced (C's fan-out over an empty list still
462 /// signals), so the count is bumped before the sink is even consulted.
463 pub fn announce_exception(&self, exception: AsynException, addr: i32) {
464 self.announcer.announce(exception, addr);
465 }
466
467 /// A clone of this port's announcement capability, for a worker thread the
468 /// driver owns. See [`ExceptionAnnouncer`].
469 pub fn exception_announcer(&self) -> ExceptionAnnouncer {
470 self.announcer.clone()
471 }
472
473 /// Bind the IOC's exception list. [`crate::services::PortServices::bind`] is
474 /// the production caller; tests that drive a driver without a runtime use it
475 /// to stand in for that binding.
476 pub(crate) fn bind_exception_sink(&mut self, sink: Arc<ExceptionManager>) {
477 self.announcer.sink = Some(sink);
478 }
479
480 /// How many exceptions this port has announced. Monotonic; the actor compares
481 /// it against the value it last saw to decide whether C would have signalled
482 /// `notifyPortThread` (asynManager.c:635-636).
483 pub fn exceptions_announced(&self) -> u64 {
484 self.announcer.count()
485 }
486
487 /// How many callbacks are registered on this port's exception list — the
488 /// `exceptionUsers` count `reportPrintPort` prints (asynManager.c:1068-1072).
489 /// `asynReport` itself is the iocsh command (asynShellCommands.c:587); it
490 /// reaches this through `asynManager`'s own `report` (:1136).
491 pub fn exception_callback_count(&self) -> usize {
492 self.announcer
493 .sink
494 .as_ref()
495 .map_or(0, |m| m.callback_count())
496 }
497
498 /// Query whether the port is connected — the truth, wherever it lives.
499 pub fn is_connected(&self) -> bool {
500 self.connected.get()
501 }
502
503 /// The port's initial connection state, set while it is being constructed and
504 /// before it can have a listener. Not a transition: no exception fan-out, no
505 /// retry timer, no `lastConnectDisconnect` stamp. Every *transition* after
506 /// construction goes through [`Self::set_connected`].
507 ///
508 /// A port whose link is owned elsewhere has no initial state of its own to
509 /// set — the owner's cell already holds it — so this is a no-op there rather
510 /// than a silent overwrite of the owner's truth.
511 pub fn init_connected(&mut self, connected: bool) {
512 if let Connection::Own(c) = &mut self.connected {
513 *c = connected;
514 self.last_announced = connected;
515 }
516 }
517
518 /// Bind this port's connection to a cell owned by another object, making that
519 /// cell the port's truth from now on — see [`Connection::Shared`]. Called at
520 /// construction by a port that serves someone else's link (the IP-server
521 /// child port and its `ClientSlot`).
522 pub(crate) fn share_connection(&mut self, cell: Arc<AtomicBool>) {
523 self.last_announced = cell.load(Ordering::Acquire);
524 self.connected = Connection::Shared(cell);
525 }
526
527 /// C `asynPrint(pasynUserSelf, mask, ...)` — the port's own diagnostic
528 /// channel, gated by this port's trace mask and routed to this port's
529 /// trace file.
530 ///
531 /// Public because the drivers that need it are not all in this crate. C's
532 /// drivers are separate modules too (`drvModbusAsyn.cpp`,
533 /// `drvAsynIPPort.c`) and every one of them reports a fault it cannot
534 /// return — a poller has no caller to return to — by `asynPrint`ing it at
535 /// `ASYN_TRACE_ERROR`. With the [`TraceManager`] reachable only inside
536 /// this crate, an out-of-crate driver had no way to say anything at all,
537 /// and the failures it could not return became silent.
538 ///
539 /// A port whose services were never bound has no trace manager and the
540 /// call is a no-op; that is the same port that has no trace file to write
541 /// to and no mask to consult.
542 pub fn trace_print(&self, mask: crate::trace::TraceMask, msg: &str) {
543 if let Some(trace) = &self.trace {
544 trace.output(&self.port_name, mask, msg);
545 }
546 }
547
548 /// Single owner-API for the port-level `connected` transition.
549 ///
550 /// C parity: `exceptionConnect` (asynManager.c:2151-2160) and
551 /// `exceptionDisconnect` (:2174-2185) fire
552 /// `asynExceptionConnect` only when the state actually changes.
553 /// All driver code that toggles connection state MUST go through
554 /// this helper — the `connected` cell is private precisely so that a driver
555 /// cannot assign it and then hand-roll an `announce_exception(Connect, -1)`,
556 /// which bypasses the edge guard and fans spurious duplicates out to
557 /// listeners (CA gateway shadow tasks, asynRecord, monitor relays).
558 ///
559 /// On a port whose link is owned elsewhere (`Connection::Shared`) the write
560 /// is not this port's to make — the owner already moved the truth — so the
561 /// call reduces to publishing whatever edge that produced.
562 ///
563 /// Returns `true` if the state actually changed (a fan-out
564 /// happened); `false` if the call was a no-op.
565 pub fn set_connected(&mut self, connected: bool) -> bool {
566 if let Connection::Own(c) = &mut self.connected {
567 *c = connected;
568 }
569 self.sync_connection_edge()
570 }
571
572 /// Publish the port's connection edge if the truth has moved since the last
573 /// fan-out: the single owner of `exceptionConnect` (asynManager.c:2142-2161)
574 /// and `exceptionDisconnect` (:2165-2188), of the interpose stack's connection
575 /// reset and of the retry timer.
576 ///
577 /// [`Self::set_connected`] is one caller. The other is the actor, on a port
578 /// whose link is owned elsewhere: the owner (an IP-server listener assigning a
579 /// slot) moves the truth without this port's actor running, and C fans that
580 /// edge out from the owner's thread — `pasynCommonSyncIO->connectDevice` on
581 /// the child (drvAsynIPServerPort.c:358-360). Here it is published when the
582 /// child's actor next touches the port, which is the moment it can matter.
583 ///
584 /// Returns `true` if an edge was published.
585 pub fn sync_connection_edge(&mut self) -> bool {
586 let connected = self.connected.get();
587 if self.last_announced == connected {
588 return false;
589 }
590 self.last_announced = connected;
591 if !connected {
592 // C `exceptionDisconnect` stamps `lastConnectDisconnect` on
593 // every disconnect (asynManager.c:2184) so the auto-reconnect
594 // throttle measures from the moment the link dropped.
595 self.last_connect_disconnect = Some(Instant::now());
596 // ...and arms the port's connect timer at .01 s when the port is
597 // auto-connect (asynManager.c:2181-2182), which is what makes the
598 // reconnect *autonomous*: it does not wait for queued traffic.
599 if self.auto_connect {
600 self.connect_retry_at = Some(Instant::now() + CONNECT_RETRY_INITIAL);
601 }
602 } else {
603 // C `exceptionConnect` counts the connects it publishes
604 // (`++pdpCommon->numberConnects`, asynManager.c:2158) — the count
605 // `asynReport` prints, and the operator's only way to see a port that
606 // is flapping. It belongs to this owner because C increments it in the
607 // same function that raises the exception, so a connect that never
608 // fanned out is never counted.
609 self.number_connects += 1;
610 // The link is up — nothing left to retry. (C leaves the timer
611 // running and lets `portConnectTimerCallback` no-op on the
612 // `!connected` guard, asynManager.c:3258; disarming here is the
613 // same observable behaviour without the pointless wakeup.)
614 self.connect_retry_at = None;
615 }
616 // The interpose stack is a subscriber of this transition, exactly as
617 // in C: `asynInterposeEos` registers an exception callback
618 // (asynInterposeEos.c:110) and drops its read-ahead buffer +
619 // partial-EOS match on `asynExceptionConnect`
620 // (asynInterposeEos.c:142-151). Both C edges — `exceptionConnect`
621 // (asynManager.c:2158) and `exceptionDisconnect` (asynManager.c:2185)
622 // — raise that same exception, so both edges reset here. Driving the
623 // hook from this owner (rather than from an out-of-band subscriber)
624 // keeps it impossible to change `connected` without the stack
625 // hearing about it: `interpose_octet` and `connected` live in the
626 // same struct behind the same lock.
627 self.interpose_octet.connection_changed();
628 self.announce_exception(AsynException::Connect, -1);
629 true
630 }
631
632 /// Per-address variant — for multi-device ports. Same edge
633 /// guarantee as [`Self::set_connected`].
634 ///
635 /// Deliberately does *not* reset the interpose stack. In C each
636 /// interpose is installed on one (port, addr) pair and registers its
637 /// exception callback on that address's `dpCommon`, so a device-level
638 /// connect exception only resets *that* device's interpose
639 /// (asynManager.c:611-625 fans out per-`dpCommon`). `interpose_octet`
640 /// here is port-scoped, so clearing it from a per-device transition
641 /// would discard read-ahead belonging to the port's other addresses.
642 /// The port-level transition owner [`Self::set_connected`] carries the
643 /// reset.
644 pub fn set_addr_connected(&mut self, addr: i32, connected: bool) -> bool {
645 // C reaches both levels through one `exceptionConnect`, and
646 // `findDpCommon` is what decides which: an addr the port resolves to
647 // its own `dpCommon` is a PORT transition, and the port owner carries
648 // the interpose reset and the retry timer that belong to it.
649 let Some(addr) = self.dp_addr(addr) else {
650 return self.set_connected(connected);
651 };
652 let was = self.device_state(addr).connected;
653 if was == connected {
654 return false;
655 }
656 self.device_state(addr).connected = connected;
657 if !connected {
658 // Per-device disconnect stamp — same throttle anchor as the
659 // port-level path (C `exceptionDisconnect`, asynManager.c:2184).
660 self.device_state(addr).last_connect_disconnect = Some(Instant::now());
661 }
662 self.announce_exception(AsynException::Connect, addr);
663 true
664 }
665
666 /// 2.0s auto-reconnect throttle gate — C `autoConnectDevice`
667 /// (asynManager.c:712-713, 729-730).
668 ///
669 /// Returns `true` when a fresh auto-connect attempt is permitted:
670 /// either no transition has been recorded yet (mirrors C's
671 /// zero-initialised `lastConnectDisconnect`, whose diff against `now`
672 /// is effectively infinite), or at least `AUTO_CONNECT_THROTTLE` has
673 /// elapsed since the last transition or attempt. A disconnected
674 /// `auto_connect` device that just dropped — or whose previous
675 /// reconnect just failed — is refused until the window passes, so a
676 /// burst of N queued requests triggers at most one full connect
677 /// attempt per window instead of N back-to-back attempts.
678 ///
679 /// Uses monotonic [`Instant`], not wall clock: the throttle is purely
680 /// internal timing, never serialised, so it must be immune to NTP
681 /// steps. `addr` selects the anchor via [`Self::is_device_addr`].
682 pub fn auto_connect_throttle_ok(&self, addr: i32, now: Instant) -> bool {
683 let last = if self.is_device_addr(addr) {
684 self.device_states
685 .get(&addr)
686 .and_then(|d| d.last_connect_disconnect)
687 } else {
688 self.last_connect_disconnect
689 };
690 match last {
691 None => true,
692 Some(t) => now.saturating_duration_since(t) >= AUTO_CONNECT_THROTTLE,
693 }
694 }
695
696 /// Does `addr` name a *device* on this port, or the port itself?
697 ///
698 /// C `findDpCommon` resolves a `pasynUser` to `&pdevice->dpc` only when
699 /// the port is multi-device AND the user is bound to a real address;
700 /// otherwise to `&pport->dpc` (a connectDevice with `addr < 0` leaves
701 /// `pdevice` null). This is the one owner of that resolution, so the
702 /// throttle read [`Self::auto_connect_throttle_ok`] and the throttle
703 /// write [`Self::stamp_auto_connect_attempt`] can never disagree about
704 /// which anchor an address refers to. Keying on the address (rather than
705 /// on `multi_device` alone) is what lets a multi-device port hold a
706 /// *port-level* anchor at `addr = -1`: the old form sent `-1` into
707 /// `device_states`, inventing a phantom device whose anchor no
708 /// disconnect ever stamped.
709 pub fn is_device_addr(&self, addr: i32) -> bool {
710 self.dp_addr(addr).is_some()
711 }
712
713 /// The dpCommon slot this port resolves `addr` to — [`dp_common_key`]
714 /// against the port's own `ASYN_MULTIDEVICE` attribute. The single owner
715 /// of the resolution for every connection-state read and write below.
716 pub fn dp_addr(&self, addr: i32) -> Option<i32> {
717 dp_common_key(self.flags.multi_device, addr)
718 }
719
720 /// C `findDpCommon(puserPvt)->enabled` (asynManager.c:2352) — what
721 /// `pasynManager->isEnabled` answers for this address.
722 pub fn dp_enabled(&self, addr: i32) -> bool {
723 self.device(addr).map_or(self.enabled, |ds| ds.enabled)
724 }
725
726 /// C `findDpCommon(puserPvt)->connected` (asynManager.c:2340) — what
727 /// `pasynManager->isConnected` answers for this address.
728 pub fn dp_connected(&self, addr: i32) -> bool {
729 self.device(addr)
730 .map_or_else(|| self.is_connected(), |ds| ds.connected)
731 }
732
733 /// C `findDpCommon(puserPvt)->autoConnect` (asynManager.c:2370) — what
734 /// `pasynManager->isAutoConnect` answers for this address.
735 pub fn dp_auto_connect(&self, addr: i32) -> bool {
736 self.device(addr)
737 .map_or(self.auto_connect, |ds| ds.auto_connect)
738 }
739
740 /// Single owner for the post-attempt throttle stamp. C
741 /// `autoConnectDevice` stamps `lastConnectDisconnect` immediately after
742 /// every `connectAttempt`, success or failure (asynManager.c:718,
743 /// 735), so the window restarts from the end of the attempt — a failed
744 /// reconnect is not retried until the throttle elapses again.
745 pub fn stamp_auto_connect_attempt(&mut self, addr: i32, now: Instant) {
746 if self.is_device_addr(addr) {
747 self.device_state(addr).last_connect_disconnect = Some(now);
748 } else {
749 self.last_connect_disconnect = Some(now);
750 }
751 }
752
753 /// Query whether the port is enabled.
754 pub fn is_enabled(&self) -> bool {
755 self.enabled
756 }
757
758 /// Single owner-API for the port-level `enabled` transition.
759 ///
760 /// C `enable` (asynManager.c:2222-2249) refuses a shut-down port:
761 /// when `defunct` it returns `asynDisabled` *without* touching
762 /// `enabled` and *without* firing the `asynExceptionEnable` fan-out.
763 /// Otherwise it sets `enabled` and announces unconditionally (no
764 /// state-change guard). The actor's `SetEnable` op is a lifecycle op
765 /// that bypasses [`Self::check_ready`], so this guard is the only
766 /// thing that stops a defunct port from being re-enabled or fanning
767 /// out a spurious exception — it must live here, in the one owner of
768 /// the transition.
769 pub fn set_enabled(&mut self, enabled: bool) -> AsynResult<()> {
770 if self.defunct {
771 return Err(Self::shut_down_error());
772 }
773 self.enabled = enabled;
774 self.announce_exception(AsynException::Enable, -1);
775 Ok(())
776 }
777
778 /// Per-address variant — same defunct refusal as [`Self::set_enabled`].
779 /// `defunct` is modelled at the port level (a shut-down port takes its
780 /// devices with it), so a defunct port refuses per-device enable/disable
781 /// too, matching C's `dpCommon.defunct` check on the resolved device.
782 pub fn set_addr_enabled(&mut self, addr: i32, enabled: bool) -> AsynResult<()> {
783 // C `enable` writes `findDpCommon(puserPvt)->enabled` (asynManager.c:2245),
784 // so an addr that resolves to the port's own slot is the port's enable —
785 // not a device entry no reader would ever resolve to.
786 let Some(addr) = self.dp_addr(addr) else {
787 return self.set_enabled(enabled);
788 };
789 if self.defunct {
790 return Err(Self::shut_down_error());
791 }
792 self.device_state(addr).enabled = enabled;
793 self.announce_exception(AsynException::Enable, addr);
794 Ok(())
795 }
796
797 /// Query whether auto-connect is enabled.
798 pub fn is_auto_connect(&self) -> bool {
799 self.auto_connect
800 }
801
802 /// Toggle the auto-connect flag at runtime.
803 ///
804 /// C parity: `autoConnectAsyn` (asynManager.c:2310-2324) always
805 /// fires `asynExceptionAutoConnect` regardless of prior state
806 /// (no state-change guard). Mirror that — every call announces.
807 /// Driver constructors that initialise `base.auto_connect`
808 /// directly during `PortDriver::new()` keep the silent path
809 /// (the port is not yet registered, so no listeners exist).
810 pub fn set_auto_connect(&mut self, yes: bool) {
811 self.auto_connect = yes;
812 // A DEVIATION from the pinned C, adopting asyn PR #217
813 // (`87918f5e06b7`, "Queue connect timer on autoconnect enable"):
814 // flipping auto-connect ON while the port is down starts the connect
815 // timer, so the flip alone brings the port up. That PR is UNMERGED —
816 // it is on no upstream branch, so `autoConnectAsyn` at the pin
817 // (asynManager.c:2310-2324) sets `autoConnect` and announces, and
818 // nothing else; its `:2322-2324` are `exceptionOccurred` and the
819 // return. Stock asyn's only autonomous attempt is the one
820 // exception-wake pass — one try, then silence until traffic or a
821 // disconnect edge arms the timer (:2181-2182).
822 if yes && !self.connected.get() {
823 self.connect_retry_at = Some(Instant::now() + CONNECT_RETRY_INITIAL);
824 }
825 self.announce_exception(AsynException::AutoConnect, -1);
826 }
827
828 /// Per-address variant — for multi-device ports. C parity:
829 /// `autoConnectAsyn` walks dpCommon via findDpCommon so a per-
830 /// device pasynUser hits the device's dpc, otherwise the port's
831 /// dpc (asynManager.c:2314 + findDpCommon).
832 pub fn set_auto_connect_addr(&mut self, addr: i32, yes: bool) {
833 // Same `findDpCommon` split as [`Self::set_addr_enabled`]
834 // (asynManager.c:2314).
835 let Some(addr) = self.dp_addr(addr) else {
836 self.set_auto_connect(yes);
837 return;
838 };
839 let device_down = {
840 let dev = self.device_state(addr);
841 dev.auto_connect = yes;
842 !dev.connected
843 };
844 // Same PR #217 deviation as `set_auto_connect`, and the PR keys its
845 // check on the dpCommon `findDpCommon` resolved — the DEVICE's —
846 // while the timer it starts is the port's.
847 if yes && device_down {
848 self.connect_retry_at = Some(Instant::now() + CONNECT_RETRY_INITIAL);
849 }
850 self.announce_exception(AsynException::AutoConnect, addr);
851 }
852
853 /// Query whether the port has been marked defunct via
854 /// [`Self::shutdown_lifecycle`] — once true the port is gone for
855 /// good, mirroring C asynManager.c:2266-2269.
856 pub fn is_defunct(&self) -> bool {
857 self.defunct
858 }
859
860 /// The one place a shut-down port is named in an error message, and the
861 /// only one C has: `enable` on a defunct device (asynManager.c:2236-2241).
862 /// Every other gate refuses a defunct port as a disabled one.
863 fn shut_down_error() -> AsynError {
864 AsynError::Status {
865 status: AsynStatus::Disabled,
866 message: "asynManager:enable: port has been shut down".to_string(),
867 }
868 }
869
870 /// C `queueRequest`'s gate (asynManager.c:1541-1552), and the single owner
871 /// of the two refusals it is built from.
872 ///
873 /// They are independent, and the [`ConnectCheck`] the caller hands in
874 /// selects between them — it can waive the *connected* refusal and nothing
875 /// else. There is no argument, op class or priority that waives
876 /// [`Self::check_enabled`]: C's `if(!pport->dpc.enabled) return asynDisabled`
877 /// (:1541-1546) sits *above* `checkPortConnect` and is reached by every
878 /// request, and its port thread refuses to run anything at all on a disabled
879 /// port (`portThread`, :806-809).
880 ///
881 /// The `ConnectCheck` can only come from [`AsynUser::connect_check`], so the
882 /// waiver is available exactly to the requests C gives it to.
883 ///
884 /// Its refusal is [`AsynError::QueueRefused`], not [`AsynError::Status`]:
885 /// C's refusal is `queueRequest`'s *return value*, so the callback never
886 /// runs and nothing it implies happened. A caller must be able to tell that
887 /// from a driver error raised *inside* a callback that did run — the record
888 /// writes the refusal to ERRS and stops, where a driver error still gets the
889 /// callback's readback and `monitorStatus` tail (asynRecord.c:571-578 vs
890 /// :788-900). This is the only place that stamps it.
891 pub fn check_queue(&self, addr: i32, connect: ConnectCheck) -> AsynResult<()> {
892 self.check_queue_inner(addr, connect)
893 .map_err(AsynError::into_queue_refusal)
894 }
895
896 /// The gate's body, before the refusal is stamped as a queue refusal.
897 ///
898 /// Three blocks, in C's order:
899 ///
900 /// 1. `!pport->dpc.enabled → asynDisabled` (:1541-1546) — port level,
901 /// unconditional, reached by every request.
902 /// 2. `checkPortConnect && !pport->dpc.connected → asynDisconnected`
903 /// (:1547-1552) — port level, waived by the request's own user. (C's
904 /// `checkPortConnect == FALSE` reads *no* connected flag: the port thread
905 /// drains the Connect queue before it ever calls `autoConnectDevice`,
906 /// :811-855.)
907 /// 3. The **device** block (:1553-1575), which C runs only for a
908 /// *synchronous* port: it sits bodily inside
909 /// `if(!(pport->attributes & ASYN_CANBLOCK))` at :1553. A CANBLOCK port's
910 /// device-level enabled/connected checks belong to the port THREAD
911 /// (:875-886), which does something else entirely with them — a disabled
912 /// device's request *waits in the queue* (`continue`), it is not refused
913 /// — so applying them here refused requests C parks (R15-47). Every real
914 /// transport port is CANBLOCK.
915 fn check_queue_inner(&self, addr: i32, connect: ConnectCheck) -> AsynResult<()> {
916 self.check_enabled()?;
917 if connect == ConnectCheck::Required {
918 self.check_port_connected()?;
919 }
920 if !self.flags.can_block {
921 // C :1561-1567 — the device-enabled refusal in the synchronous block
922 // is *not* conditioned on `checkPortConnect`; only the connected one
923 // below is (:1568).
924 self.check_device_enabled(addr)?;
925 if connect == ConnectCheck::Required {
926 self.check_device_connected(addr)?;
927 }
928 }
929 Ok(())
930 }
931
932 /// The unconditional half of the queue gate: a disabled port refuses every
933 /// request (asynManager.c:1541-1546).
934 ///
935 /// There is no defunct branch here, and there is none in C's `queueRequest`
936 /// either: `shutdownPort` clears `enabled` alongside setting `defunct`
937 /// (:2283-2284) precisely so this one check answers for both. The invariant
938 /// `defunct ⟹ !enabled` (see the field doc) is what makes that sound.
939 pub fn check_enabled(&self) -> AsynResult<()> {
940 if !self.enabled {
941 return Err(AsynError::Status {
942 status: AsynStatus::Disabled,
943 message: format!("port {} disabled", self.port_name),
944 });
945 }
946 Ok(())
947 }
948
949 /// The port-level connected refusal (asynManager.c:1547-1552).
950 pub fn check_port_connected(&self) -> AsynResult<()> {
951 if !self.is_connected() {
952 return Err(AsynError::Status {
953 status: AsynStatus::Disconnected,
954 message: format!("port {} not connected", self.port_name),
955 });
956 }
957 Ok(())
958 }
959
960 /// The device-level enabled refusal, C's text verbatim
961 /// (asynManager.c:1561-1567). Its *use* differs by port class, which is why
962 /// it is a check and not a gate: `queueRequest` returns it on a synchronous
963 /// port, while on a CANBLOCK port the same condition makes `portThread` park
964 /// the request instead (:875). Both callers ask this one function.
965 ///
966 /// A port that is not `ASYN_MULTIDEVICE`, or an address with no device state,
967 /// resolves to the port's own `dpCommon` in C's `findDpCommon` — already
968 /// checked above — so there is nothing device-level left to refuse.
969 pub fn check_device_enabled(&self, addr: i32) -> AsynResult<()> {
970 if let Some(ds) = self.device(addr) {
971 if !ds.enabled {
972 return Err(AsynError::Status {
973 status: AsynStatus::Disabled,
974 // C's double space is verbatim (asynManager.c:1564).
975 message: format!("port {} or device {} not enabled", self.port_name, addr),
976 });
977 }
978 }
979 Ok(())
980 }
981
982 /// The device-level connected refusal (asynManager.c:1568-1575). Same
983 /// split as [`Self::check_device_enabled`]: `queueRequest` returns it on a
984 /// synchronous port; on a CANBLOCK port `portThread` answers the same
985 /// condition with the request's timeout callback (:884-885).
986 pub fn check_device_connected(&self, addr: i32) -> AsynResult<()> {
987 if let Some(ds) = self.device(addr) {
988 if !ds.connected {
989 return Err(AsynError::Status {
990 status: AsynStatus::Disconnected,
991 message: format!("port {} or device {} not connected", self.port_name, addr),
992 });
993 }
994 }
995 Ok(())
996 }
997
998 /// The device `addr` resolves to, or `None` when C's `findDpCommon` would
999 /// resolve it to the port's own `dpCommon` (not multi-device, or no device
1000 /// created at that address).
1001 fn device(&self, addr: i32) -> Option<&DeviceState> {
1002 self.dp_addr(addr).and_then(|a| self.device_states.get(&a))
1003 }
1004
1005 /// Check that the port is enabled, connected, and not defunct.
1006 /// Returns `Err(Disabled)`, `Err(Disconnected)`, or `Err(Disabled)`
1007 /// (defunct => permanently disabled) otherwise.
1008 pub fn check_ready(&self) -> AsynResult<()> {
1009 self.check_enabled()?;
1010 self.check_port_connected()
1011 }
1012
1013 /// Run the C `shutdownPort` lifecycle (asynManager.c:2251-2308):
1014 ///
1015 /// 1. Refuse if the port did not opt into `ASYN_DESTRUCTIBLE`
1016 /// (returns `Err(Status::Error)`).
1017 /// 2. Short-circuit if already defunct (idempotent — returns Ok).
1018 /// 3. Set `enabled = false`, `defunct = true` — every subsequent
1019 /// request through [`Self::check_ready`] fails.
1020 /// 4. Broadcast `AsynException::Shutdown` so registered observers
1021 /// (CA gateways, monitor sinks) tear down their handles.
1022 ///
1023 /// Drivers should call this from their own shutdown plumbing and
1024 /// then release any hardware-owned resources via their
1025 /// [`PortDriver::shutdown`] implementation. Callers from outside
1026 /// the runtime can drive the same lifecycle via
1027 /// [`crate::manager::PortManager::shutdown_port`].
1028 pub fn shutdown_lifecycle(&mut self) -> AsynResult<()> {
1029 if self.defunct {
1030 // Idempotent — C asynManager.c:2266-2269 returns asynSuccess.
1031 return Ok(());
1032 }
1033 if !self.flags.destructible {
1034 return Err(AsynError::Status {
1035 status: AsynStatus::Error,
1036 message: format!(
1037 "port {} does not support shutting down (ASYN_DESTRUCTIBLE not set)",
1038 self.port_name
1039 ),
1040 });
1041 }
1042 self.enabled = false;
1043 // The invariant is `defunct ⟹ !enabled` on *every* dpCommon this port
1044 // owns, not only the port's own. Clearing the device slots here is what
1045 // makes it hold by construction: [`Self::device_state`] seeds a new slot
1046 // from the port's now-false `enabled`, and [`Self::set_addr_enabled`]
1047 // refuses while defunct, so nothing can raise a slot back — and
1048 // [`Self::dp_enabled`] needs no defunct branch to answer correctly.
1049 // Without this, a slot that happened to exist before the shutdown kept
1050 // `enabled = true`, so `isEnabled(ADDR)` answered true on a port that is
1051 // gone while `isEnabled(-1)` answered false — the same read disagreeing
1052 // with itself depending on whether anything had ever touched that
1053 // address.
1054 for ds in self.device_states.values_mut() {
1055 ds.enabled = false;
1056 }
1057 self.defunct = true;
1058 self.announce_exception(AsynException::Shutdown, -1);
1059 Ok(())
1060 }
1061
1062 /// Check that port + device address are both ready — the whole of C's
1063 /// synchronous-port gate (asynManager.c:1541-1575) in one call. Drivers that
1064 /// re-check inside their own I/O use it; the queue gate reaches the same
1065 /// checks through [`Self::check_queue`], which splits them by port class.
1066 pub fn check_ready_addr(&self, addr: i32) -> AsynResult<()> {
1067 self.check_ready()?;
1068 self.check_device_enabled(addr)?;
1069 self.check_device_connected(addr)
1070 }
1071
1072 /// Get or create a device state for the given address.
1073 ///
1074 /// A device created here inherits the port's own `dpCommon`, as C's
1075 /// `locateDevice` does: `dpCommonInit(pport, pdevice, pport->dpc.autoConnect)`
1076 /// (asynManager.c:586). Defaulting `auto_connect` to `true` on a
1077 /// manual-connect port would make `asynReport`'s per-device
1078 /// `autoConnect Yes` a lie, and would hand `autoConnectDevice` a device it
1079 /// may reconnect on a port whose operator turned auto-connect off.
1080 ///
1081 /// `enabled` and `connected` are seeded the same way, and for a stronger
1082 /// reason: until this slot exists the reads [`Self::dp_enabled`] /
1083 /// [`Self::dp_connected`] answer from the port's own, so seeding from
1084 /// anything else would let an unrelated write — disabling a device on a
1085 /// disconnected port, say — flip what a *different* field of that device
1086 /// reports. The slot's creation is then invisible: every field starts at
1087 /// the value the read it replaces was already giving.
1088 /// `pub(crate)`, not `pub`: this hands out the raw slot, so a caller that
1089 /// wrote through it would skip the `asynException` fan-out C raises from
1090 /// `enable` / `exceptionConnect` (asynManager.c:2247, 2158) *and* could
1091 /// create a device on a port that has none — a state C cannot represent,
1092 /// since `locateDevice` refuses to allocate one unless
1093 /// `attributes&ASYN_MULTIDEVICE` (:576). Outside callers use the three
1094 /// owners [`Self::set_addr_connected`], [`Self::set_addr_enabled`] and
1095 /// [`Self::set_auto_connect_addr`], each of which resolves the addr first.
1096 pub(crate) fn device_state(&mut self, addr: i32) -> &mut DeviceState {
1097 let seed = DeviceState {
1098 connected: self.is_connected(),
1099 enabled: self.enabled,
1100 auto_connect: self.auto_connect,
1101 last_connect_disconnect: None,
1102 };
1103 self.device_states.entry(addr).or_insert(seed)
1104 }
1105
1106 /// Check if a specific device address is connected.
1107 pub fn is_device_connected(&self, addr: i32) -> bool {
1108 self.dp_connected(addr)
1109 }
1110
1111 /// Set a specific device address as connected.
1112 ///
1113 /// C parity: announce only on actual transition
1114 /// (asynManager.c:2151-2160 — `exceptionConnect` rejects
1115 /// already-connected; we keep an Ok return for idempotency but
1116 /// suppress the duplicate fan-out so subscribers don't see
1117 /// spurious connect events). Thin wrapper over
1118 /// [`Self::set_addr_connected`] for callers that prefer the
1119 /// directional verb.
1120 pub fn connect_addr(&mut self, addr: i32) {
1121 self.set_addr_connected(addr, true);
1122 }
1123
1124 /// Set a specific device address as disconnected.
1125 ///
1126 /// C parity: announce only on actual transition
1127 /// (asynManager.c:2174-2185). Thin wrapper over
1128 /// [`Self::set_addr_connected`].
1129 pub fn disconnect_addr(&mut self, addr: i32) {
1130 self.set_addr_connected(addr, false);
1131 }
1132
1133 /// Enable a specific device address. Convenience facade over the
1134 /// guarded owner [`Self::set_addr_enabled`]; a defunct port no-ops.
1135 pub fn enable_addr(&mut self, addr: i32) {
1136 let _ = self.set_addr_enabled(addr, true);
1137 }
1138
1139 /// Disable a specific device address. Convenience facade over the
1140 /// guarded owner [`Self::set_addr_enabled`]; a defunct port no-ops.
1141 pub fn disable_addr(&mut self, addr: i32) {
1142 let _ = self.set_addr_enabled(addr, false);
1143 }
1144
1145 /// Set a custom timestamp source callback.
1146 pub fn register_timestamp_source<F>(&mut self, source: F)
1147 where
1148 F: Fn() -> SystemTime + Send + Sync + 'static,
1149 {
1150 self.timestamp_source = Some(Arc::new(source));
1151 }
1152
1153 /// Drop a registered source — C `unregisterTimeStampSource`
1154 /// (asynManager.c:334), which restores `defaultTimeStampSource` (:332).
1155 pub fn unregister_timestamp_source(&mut self) {
1156 self.timestamp_source = None;
1157 }
1158
1159 /// Get current timestamp from the registered source, or SystemTime::now().
1160 pub fn current_timestamp(&self) -> SystemTime {
1161 self.timestamp_source
1162 .as_ref()
1163 .map_or_else(SystemTime::now, |f| f())
1164 }
1165
1166 pub fn create_param(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
1167 self.params.create_param(name, param_type)
1168 }
1169
1170 pub fn find_param(&self, name: &str) -> Option<usize> {
1171 self.params.find_param(name)
1172 }
1173
1174 // --- Convenience param accessors ---
1175
1176 pub fn set_int32_param(&mut self, index: usize, addr: i32, value: i32) -> AsynResult<()> {
1177 self.params.set_int32(index, addr, value)
1178 }
1179
1180 pub fn get_int32_param(&self, index: usize, addr: i32) -> AsynResult<i32> {
1181 self.params.get_int32(index, addr)
1182 }
1183
1184 /// Strict variant — returns [`AsynError::ParamUndefined`] when the
1185 /// cache entry has never been set (C parity for `asynParamUndefined`).
1186 /// See [`crate::param::ParamList::get_int32_strict`].
1187 pub fn get_int32_param_strict(&self, index: usize, addr: i32) -> AsynResult<i32> {
1188 self.params.get_int32_strict(index, addr)
1189 }
1190
1191 pub fn set_int64_param(&mut self, index: usize, addr: i32, value: i64) -> AsynResult<()> {
1192 self.params.set_int64(index, addr, value)
1193 }
1194
1195 pub fn get_int64_param(&self, index: usize, addr: i32) -> AsynResult<i64> {
1196 self.params.get_int64(index, addr)
1197 }
1198
1199 /// Strict variant — see [`crate::param::ParamList::get_int64_strict`].
1200 pub fn get_int64_param_strict(&self, index: usize, addr: i32) -> AsynResult<i64> {
1201 self.params.get_int64_strict(index, addr)
1202 }
1203
1204 pub fn set_float64_param(&mut self, index: usize, addr: i32, value: f64) -> AsynResult<()> {
1205 self.params.set_float64(index, addr, value)
1206 }
1207
1208 pub fn get_float64_param(&self, index: usize, addr: i32) -> AsynResult<f64> {
1209 self.params.get_float64(index, addr)
1210 }
1211
1212 /// Strict variant — see [`crate::param::ParamList::get_float64_strict`].
1213 pub fn get_float64_param_strict(&self, index: usize, addr: i32) -> AsynResult<f64> {
1214 self.params.get_float64_strict(index, addr)
1215 }
1216
1217 pub fn set_string_param(
1218 &mut self,
1219 index: usize,
1220 addr: i32,
1221 value: impl Into<Vec<u8>>,
1222 ) -> AsynResult<()> {
1223 self.params.set_string(index, addr, value)
1224 }
1225
1226 pub fn get_string_param(&self, index: usize, addr: i32) -> AsynResult<&[u8]> {
1227 self.params.get_string(index, addr)
1228 }
1229
1230 /// Strict variant — see [`crate::param::ParamList::get_string_strict`].
1231 pub fn get_string_param_strict(&self, index: usize, addr: i32) -> AsynResult<&[u8]> {
1232 self.params.get_string_strict(index, addr)
1233 }
1234
1235 /// Set a UInt32Digital parameter. `interrupt_mask` mirrors C
1236 /// `setUIntDigitalParam(.., interruptMask)` (asynPortDriver.cpp:1369,
1237 /// 1381): bits to force into the I/O Intr callback mask even when the
1238 /// stored value did not change. Pass `0` for a plain value set (the
1239 /// 3-arg C overload, asynPortDriver.cpp:1347).
1240 pub fn set_uint32_param(
1241 &mut self,
1242 index: usize,
1243 addr: i32,
1244 value: u32,
1245 mask: u32,
1246 interrupt_mask: u32,
1247 ) -> AsynResult<()> {
1248 self.params
1249 .set_uint32(index, addr, value, mask, interrupt_mask)
1250 }
1251
1252 pub fn get_uint32_param(&self, index: usize, addr: i32) -> AsynResult<u32> {
1253 self.params.get_uint32(index, addr)
1254 }
1255
1256 /// Strict variant — see [`crate::param::ParamList::get_uint32_strict`].
1257 pub fn get_uint32_param_strict(&self, index: usize, addr: i32) -> AsynResult<u32> {
1258 self.params.get_uint32_strict(index, addr)
1259 }
1260
1261 pub fn get_enum_param(&self, index: usize, addr: i32) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
1262 self.params.get_enum(index, addr)
1263 }
1264
1265 pub fn set_enum_index_param(
1266 &mut self,
1267 index: usize,
1268 addr: i32,
1269 value: usize,
1270 ) -> AsynResult<()> {
1271 self.params.set_enum_index(index, addr, value)
1272 }
1273
1274 pub fn set_enum_choices_param(
1275 &mut self,
1276 index: usize,
1277 addr: i32,
1278 choices: Arc<[EnumEntry]>,
1279 ) -> AsynResult<()> {
1280 self.params.set_enum_choices(index, addr, choices)
1281 }
1282
1283 pub fn get_generic_pointer_param(
1284 &self,
1285 index: usize,
1286 addr: i32,
1287 ) -> AsynResult<Arc<dyn Any + Send + Sync>> {
1288 self.params.get_generic_pointer(index, addr)
1289 }
1290
1291 pub fn set_generic_pointer_param(
1292 &mut self,
1293 index: usize,
1294 addr: i32,
1295 value: Arc<dyn Any + Send + Sync>,
1296 ) -> AsynResult<()> {
1297 self.params.set_generic_pointer(index, addr, value)
1298 }
1299
1300 pub fn set_param_timestamp(
1301 &mut self,
1302 index: usize,
1303 addr: i32,
1304 ts: SystemTime,
1305 ) -> AsynResult<()> {
1306 self.params.set_timestamp(index, addr, ts)
1307 }
1308
1309 pub fn set_param_status(
1310 &mut self,
1311 index: usize,
1312 addr: i32,
1313 status: AsynStatus,
1314 alarm_status: u16,
1315 alarm_severity: u16,
1316 ) -> AsynResult<()> {
1317 self.params
1318 .set_param_status(index, addr, status, alarm_status, alarm_severity)
1319 }
1320
1321 pub fn get_param_status(&self, index: usize, addr: i32) -> AsynResult<(AsynStatus, u16, u16)> {
1322 self.params.get_param_status(index, addr)
1323 }
1324
1325 /// C `asynPortDriver::reportParams` (asynPortDriver.cpp:1799-1809) — the
1326 /// parameter block of the driver's report.
1327 ///
1328 /// `details` is the level `report` was called with, **unshifted**: C hands
1329 /// `reportParams` the same number it got (:3693), and the level decides one
1330 /// thing only — how many address lists are printed (`details >= 2` → all
1331 /// `maxAddr` of them, else list 0 alone, :1804). The values are *not* a
1332 /// deeper level: `paramVal::report` prints name, type, value and status for
1333 /// every parameter at every level (paramVal.cpp:296-372).
1334 ///
1335 /// Passing `level - 1` here put the whole block one level late — `asynReport
1336 /// 1` printed a bare count where C prints every parameter with its value, and
1337 /// values only appeared at 3 (R16-46/47).
1338 pub fn report_params(&self, out: &mut dyn std::fmt::Write, details: i32) {
1339 use std::fmt::Write as _;
1340 let num_addr = if details >= 2 {
1341 self.max_addr.max(1)
1342 } else {
1343 1
1344 };
1345 for addr in 0..num_addr {
1346 let _ = writeln!(out, "Parameter list {addr}");
1347 self.params.report(out, addr as i32);
1348 }
1349 }
1350
1351 /// Push an interpose layer onto the **port's** octet I/O stack — C
1352 /// `interposeInterface(portName, -1, ...)`, which every driver's own
1353 /// configure-time install is (the layer serves every device on the port).
1354 ///
1355 /// **Concurrency**: requires `&mut self`, which means the caller must hold
1356 /// the port lock (`Arc<Mutex<dyn PortDriver>>`). This ensures
1357 /// interpose modifications are serialized with I/O dispatch.
1358 pub fn install_octet_interpose(&mut self, layer: Box<dyn OctetInterpose>) {
1359 self.install_octet_interpose_addr(crate::interpose::PORT_CHAIN, layer);
1360 }
1361
1362 /// Push an interpose layer onto the stack of the device `addr` names — C
1363 /// `interposeInterface(portName, addr, ...)` (asynManager.c:2190-2220), which
1364 /// is what the `asynInterposeEcho` / `asynInterposeDelay` iocsh commands call
1365 /// (asynInterposeEcho.c:176, asynInterposeDelay.c:187,200). On a port that is
1366 /// not multi-device every addr resolves to the port itself.
1367 pub fn install_octet_interpose_addr(&mut self, addr: i32, layer: Box<dyn OctetInterpose>) {
1368 self.interpose_octet.install(addr, layer);
1369 }
1370
1371 /// Flush changed parameters as interrupt notifications.
1372 /// Equivalent to C asyn's callParamCallbacks().
1373 pub fn call_param_callbacks(&mut self, addr: i32) -> AsynResult<()> {
1374 // C asynPortDriver.cpp:838 — `callCallbacks` returns before the loop
1375 // AND before `flags.clear()` (:871) while `interruptAccept` is false,
1376 // so every callback fired during IOC build is a no-op that PRESERVES
1377 // the accumulated changed-flags. Returning here, ahead of `take_changed`
1378 // (which both reads and clears), is that early return: a driver's
1379 // acquisition/array task can call this before `iocInit` has wired the
1380 // `_RBV` records, and a read-only seed must survive to the one-shot
1381 // flush the scan facility fires when the gate goes true.
1382 if !epics_libcom_rs::runtime::interrupt_accept::interrupts_accepted() {
1383 return Ok(());
1384 }
1385 let changed = self.params.take_changed(addr)?;
1386 let now = self.current_timestamp();
1387 for reason in changed {
1388 let value = self.params.get_value(reason, addr)?.clone();
1389 // C asynPortDriver.cpp:845 — callCallbacks skips firing for an
1390 // undefined param even though its changed flag is consumed
1391 // (flags.clear() at :871). A status/alarm change or bare
1392 // mark_changed on a never-set scalar must not emit an I/O Intr.
1393 // Array/generic-pointer params have no callCallbacks analog
1394 // (:846-865 switch is scalar-only) and Rust fires them as a
1395 // read-trigger regardless, so gate scalars only.
1396 if !value.is_array() && !self.params.is_param_defined(reason, addr).unwrap_or(false) {
1397 continue;
1398 }
1399 let ts = self.params.get_timestamp(reason, addr)?.unwrap_or(now);
1400 // C parity: read the accumulated callback mask and reset it
1401 // (asynPortDriver.cpp:854-855 fires uint32Callback then sets
1402 // uInt32CallbackMask = 0). The flush is the single owner of
1403 // this consume, so accumulated bits never leak to the next.
1404 let uint32_mask = self
1405 .params
1406 .take_uint32_interrupt_mask(reason, addr)
1407 .unwrap_or(0);
1408 // C parity: asynPortDriver.cpp:633-637 sets
1409 // `pInterrupt->pasynUser->auxStatus/alarmStatus/alarmSeverity`
1410 // from the param's stored status before invoking each
1411 // subscriber callback. Pull those here so subscribers see
1412 // the same triplet C consumers do.
1413 let (aux_status, alarm_status, alarm_severity) = self
1414 .params
1415 .get_param_status(reason, addr)
1416 .unwrap_or((AsynStatus::Success, 0, 0));
1417 self.interrupts.notify(InterruptValue {
1418 reason,
1419 addr,
1420 value,
1421 timestamp: ts,
1422 uint32_changed_mask: uint32_mask,
1423 aux_status,
1424 alarm_status,
1425 alarm_severity,
1426 // Untyped: a single cached value per (reason,addr) reaches
1427 // every subscribing interface (the pre-per-interface path).
1428 iface: None,
1429 });
1430 }
1431 Ok(())
1432 }
1433
1434 /// Flush a single parameter's changed flag and notify if dirty.
1435 /// Use this instead of `call_param_callbacks` when you want to avoid
1436 /// flushing unrelated parameters (e.g. rapidly-updating CP-linked params).
1437 pub fn call_param_callback(&mut self, addr: i32, reason: usize) -> AsynResult<()> {
1438 // C `interruptAccept` gate — see `call_param_callbacks`: preserve the
1439 // changed-flag (do not `take_changed_single`) until the IOC accepts
1440 // interrupts, so a pre-`iocInit` flush cannot strand a seeded value.
1441 if !epics_libcom_rs::runtime::interrupt_accept::interrupts_accepted() {
1442 return Ok(());
1443 }
1444 if self.params.take_changed_single(reason, addr)? {
1445 let value = self.params.get_value(reason, addr)?.clone();
1446 // C asynPortDriver.cpp:845 — see `call_param_callbacks`: an
1447 // undefined scalar consumes its changed flag but fires no
1448 // callback. Array/generic-pointer triggers fire regardless.
1449 if !value.is_array() && !self.params.is_param_defined(reason, addr).unwrap_or(false) {
1450 return Ok(());
1451 }
1452 let now = self.current_timestamp();
1453 let ts = self.params.get_timestamp(reason, addr)?.unwrap_or(now);
1454 // C parity: read the accumulated callback mask and reset it
1455 // (asynPortDriver.cpp:854-855 fires uint32Callback then sets
1456 // uInt32CallbackMask = 0). The flush is the single owner of
1457 // this consume, so accumulated bits never leak to the next.
1458 let uint32_mask = self
1459 .params
1460 .take_uint32_interrupt_mask(reason, addr)
1461 .unwrap_or(0);
1462 // C parity: see `call_param_callbacks` above.
1463 let (aux_status, alarm_status, alarm_severity) = self
1464 .params
1465 .get_param_status(reason, addr)
1466 .unwrap_or((AsynStatus::Success, 0, 0));
1467 self.interrupts.notify(InterruptValue {
1468 reason,
1469 addr,
1470 value,
1471 timestamp: ts,
1472 uint32_changed_mask: uint32_mask,
1473 aux_status,
1474 alarm_status,
1475 alarm_severity,
1476 // Untyped (see `call_param_callbacks`).
1477 iface: None,
1478 });
1479 }
1480 Ok(())
1481 }
1482
1483 /// Mark a parameter as changed without modifying its value.
1484 ///
1485 /// Use this to trigger I/O Intr on params whose data is served via
1486 /// `read_*_array()` overrides rather than the param cache (e.g. pixel data).
1487 pub fn mark_param_changed(&mut self, index: usize, addr: i32) -> AsynResult<()> {
1488 self.params.mark_changed(index, addr)
1489 }
1490
1491 /// Fire one per-interface I/O Intr callback carrying an interface-typed value.
1492 ///
1493 /// `call_param_callbacks` stores **one** value per `(reason, addr)` and
1494 /// notifies it untyped, so every record on that reason — whatever its DTYP's
1495 /// interface — receives the same value. For a driver whose single raw datum
1496 /// is exposed on several asyn interfaces at once (e.g. a Modbus register read
1497 /// by an `asynInt32` ai, an `asynUInt32Digital` bi, and an `asynFloat64` ai
1498 /// simultaneously), that collapse delivers a wrong-typed value to all but one
1499 /// of them. C's `drvModbusAsyn::readPoller` instead decodes the one register
1500 /// block **separately per interface** and invokes each interface's own
1501 /// interrupt list — `interruptStart` on `uInt32Digital`/`int32`/`float64`
1502 /// at drvModbusAsyn.cpp:1674/1716/1788. This is the analogue: the driver
1503 /// decodes per interface and fires each value tagged with its `iface`, so the
1504 /// interrupt filter routes it only to records on that interface
1505 /// ([`InterruptFilter::iface`](crate::interrupt::InterruptFilter::iface)). `uint32_changed_mask` is the changed-bit
1506 /// mask for the `UInt32Digital` interface (a record's `@asynMask` gates on it,
1507 /// `asynPortDriver.cpp:720`); pass `0` for the other interfaces, whose
1508 /// subscribers carry no mask filter.
1509 ///
1510 /// `aux_status` is the device I/O status this fire carries (C
1511 /// `pInterrupt->pasynUser->auxStatus`, set on every callback the poller
1512 /// emits — `drvModbusAsyn.cpp:1699/1738/1774/1810/1845/1880/1916`). A driver whose
1513 /// last acquisition failed still fires its interrupt lists, with the failing
1514 /// status, so I/O-Intr records go to READ/INVALID instead of freezing on the
1515 /// last good value; pass [`AsynStatus::Success`] on a clean acquisition.
1516 pub fn notify_interface_value(
1517 &self,
1518 reason: usize,
1519 addr: i32,
1520 iface: InterfaceType,
1521 value: ParamValue,
1522 uint32_changed_mask: u32,
1523 aux_status: AsynStatus,
1524 ) {
1525 let ts = self.current_timestamp();
1526 self.interrupts.notify(InterruptValue {
1527 reason,
1528 addr,
1529 value,
1530 timestamp: ts,
1531 uint32_changed_mask,
1532 aux_status,
1533 alarm_status: 0,
1534 alarm_severity: 0,
1535 iface: Some(iface),
1536 });
1537 }
1538
1539 /// Publish a new enum table for a parameter — C
1540 /// `asynPortDriver::doCallbacksEnum`. Records bound to the parameter rewrite
1541 /// their state strings/values/severities and post `DBE_PROPERTY`.
1542 ///
1543 /// An `Enum` parameter owns its table, so the table is stored and goes out
1544 /// with the parameter's own callback. A parameter of any other type (C's
1545 /// everyday case: an `asynParamInt32` behind an mbbi) has nowhere to keep
1546 /// one; the table is fired on the asynEnum interface alone, where no value
1547 /// subscriber sees it.
1548 pub fn do_callbacks_enum(
1549 &mut self,
1550 index: usize,
1551 addr: i32,
1552 choices: Arc<[EnumEntry]>,
1553 ) -> AsynResult<()> {
1554 if self.params.get_enum(index, addr).is_ok() {
1555 self.params.set_enum_choices(index, addr, choices)?;
1556 return self.call_param_callbacks(addr);
1557 }
1558 self.notify_interface_value(
1559 index,
1560 addr,
1561 InterfaceType::Enum,
1562 ParamValue::Enum { index: 0, choices },
1563 0,
1564 AsynStatus::Success,
1565 );
1566 Ok(())
1567 }
1568}
1569
1570/// Result of resolving a record's driver-info string at bind time — the
1571/// asyn-rs analogue of what C `drvUserCreate` writes into `pasynUser`.
1572///
1573/// `reason` is the shared parameter index (every record with the same drvInfo
1574/// resolves to it). The remaining fields carry **per-record** driver state the
1575/// lookup derived from this particular drvInfo string (C stashes the same in
1576/// `pasynUser->drvUser`), which the binding applies to that record's I/O.
1577#[derive(Debug, Default)]
1578pub struct DrvUserInfo {
1579 /// Shared parameter index for this drvInfo (C `pasynUser->reason`).
1580 pub reason: usize,
1581 /// Optional per-record octet length cap — the asyn-rs home for C's
1582 /// `modbusDrvUser_t.len` (`drvUserCreate` parses `TYPE=N`; `getStringLen`
1583 /// caps the asyn octet `maxLen` to it, drvModbusAsyn.cpp:2367-2377). `None`
1584 /// when the drvInfo carried no cap; the binding then uses the record buffer
1585 /// length alone. The binding applies `min(buffer_len, cap)`.
1586 pub max_octet_len: Option<usize>,
1587}
1588
1589impl DrvUserInfo {
1590 /// A resolution carrying only the shared reason and no per-record cap — the
1591 /// default-lookup result.
1592 pub fn from_reason(reason: usize) -> Self {
1593 Self {
1594 reason,
1595 ..Self::default()
1596 }
1597 }
1598}
1599
1600/// Everything a binding tells the driver when it resolves a drvInfo string —
1601/// the asyn-rs analogue of the `pasynUser` C `drvUserCreate` receives.
1602///
1603/// This is one carrier rather than a widening argument list because the bind
1604/// request has already grown once (`addr`, for C `checkOffset`) and is now
1605/// growing again (`iface`): a driver that resolves a drvInfo needs to know
1606/// *how the record will use the parameter*, not just its name. The struct is
1607/// `#[non_exhaustive]` so the next field is additive — build it with
1608/// [`DrvUserRequest::new`] and the builder methods.
1609#[derive(Debug, Clone, PartialEq, Eq)]
1610#[non_exhaustive]
1611pub struct DrvUserRequest {
1612 /// The record's driver-info string (C `pasynUser->drvUser` input).
1613 pub drv_info: String,
1614 /// The record's asyn `addr`, so a multi-device driver can reject an
1615 /// out-of-range address at bind time (C `drvUserCreate` `checkOffset`,
1616 /// drvModbusAsyn.cpp:378-384).
1617 pub addr: i32,
1618 /// The asyn interface the record will actually read and write this
1619 /// parameter through, derived from its DTYP (`asynFloat64` → [`InterfaceType::Float64`]).
1620 ///
1621 /// An on-demand driver (C Autoparam lazy creation) must create the
1622 /// parameter with the type the record will read it as, or every value the
1623 /// record takes from it is a coerced type mismatch: C
1624 /// `adsAsynPortDriver::getRecordInfoFromDrvInfo` derives the parameter's
1625 /// asyn type from the bound record's DTYP for exactly this reason — the
1626 /// same PLC symbol may bind as `asynInt32` from one record and
1627 /// `asynFloat64` from another.
1628 ///
1629 /// `None` when the bind has no record behind it (a port-level
1630 /// [`crate::sync_io`] resolve, a driver-to-driver handle call): the driver
1631 /// then has no record type to honour and falls back to its own default.
1632 pub iface: Option<InterfaceType>,
1633}
1634
1635impl DrvUserRequest {
1636 /// A bind request for `drv_info` at `addr` with no record interface — the
1637 /// port-level resolve.
1638 pub fn new(drv_info: impl Into<String>, addr: i32) -> Self {
1639 Self {
1640 drv_info: drv_info.into(),
1641 addr,
1642 iface: None,
1643 }
1644 }
1645
1646 /// Attach the bound record's asyn interface. Accepts an
1647 /// [`InterfaceType`] or an `Option<InterfaceType>`.
1648 pub fn with_iface(mut self, iface: impl Into<Option<InterfaceType>>) -> Self {
1649 self.iface = iface.into();
1650 self
1651 }
1652}
1653
1654/// Port driver trait. All methods have default implementations that operate
1655/// on the parameter cache (no actual I/O).
1656///
1657/// Drivers performing real hardware I/O should:
1658/// 1. Run I/O in a background task (e.g., tokio::spawn)
1659/// 2. Update parameters via `base_mut().set_*_param()` + `call_param_callbacks()`
1660/// 3. Let the default `read_*` methods return cached values
1661///
1662/// # LockPort/UnlockPort
1663///
1664/// C asyn provides `lockPort`/`unlockPort` for direct mutex locking. In asyn-rs,
1665/// the port is always behind `Arc<Mutex<dyn PortDriver>>`, so callers hold the
1666/// parking_lot mutex directly. For multi-request exclusive access, use
1667/// `BlockProcess`/`UnblockProcess` via the worker queue.
1668/// C `epicsTimeToStrftime(buff, ..., "%Y/%m/%d %H:%M:%S.%03f", &timeStamp)` —
1669/// the port timestamp line of `asynPortDriver::report` (asynPortDriver.cpp:3682-3684).
1670/// Local time, as `epicsTimeToStrftime` renders it.
1671fn format_timestamp(ts: SystemTime) -> String {
1672 chrono::DateTime::<chrono::Local>::from(ts)
1673 .format("%Y/%m/%d %H:%M:%S%.3f")
1674 .to_string()
1675}
1676
1677/// C's `details >= 3` block: one line per registered interrupt client
1678/// (`reportInterrupt`, asynPortDriver.cpp:1870-1894, called once per interface at
1679/// :3695-3708). C prints the callback and userPvt pointers with each client; Rust
1680/// mailboxes have no such pointers, so the line carries what identifies a client
1681/// here — the interface it bound, the address and reason it filtered on, and the
1682/// uint32 mask when it set one.
1683fn report_interrupt_clients(out: &mut dyn std::fmt::Write, base: &PortDriverBase) {
1684 use std::fmt::Write as _;
1685 for f in base.interrupts.clients() {
1686 let iface = f
1687 .iface
1688 .map_or("any", |i: crate::interfaces::InterfaceType| {
1689 i.interrupt_label()
1690 });
1691 let addr = f.addr.map_or("any".to_string(), |a| a.to_string());
1692 let reason = f.reason.map_or("any".to_string(), |r| r.to_string());
1693 let _ = write!(
1694 out,
1695 " {iface} callback client addr={addr}, reason={reason}"
1696 );
1697 if let Some(mask) = f.uint32_mask {
1698 let _ = write!(out, ", mask=0x{mask:x}");
1699 }
1700 let _ = writeln!(out);
1701 }
1702}
1703
1704/// C `showFailure` (asynOctetBase.c:370-380) — the message every Fail stub
1705/// produces: the port name, the method, and `not implemented`, returned as
1706/// `asynError`.
1707fn eos_not_implemented(port: &str, method: &str) -> AsynError {
1708 AsynError::Status {
1709 status: AsynStatus::Error,
1710 message: format!("{port} {method} not implemented"),
1711 }
1712}
1713
1714/// C's `switch (eoslen)` refusal, verbatim: `"%s illegal eoslen %d"` with the
1715/// port name and the length (asynInterposeEos.c:301-302, :354-355).
1716fn illegal_eoslen(port: &str, eoslen: usize) -> AsynError {
1717 AsynError::Status {
1718 status: AsynStatus::Error,
1719 message: format!("{port} illegal eoslen {eoslen}"),
1720 }
1721}
1722
1723pub trait PortDriver: Any + Send + Sync {
1724 fn base(&self) -> &PortDriverBase;
1725 fn base_mut(&mut self) -> &mut PortDriverBase;
1726
1727 // --- AsynCommon ---
1728
1729 /// The trivial driver's `pasynCommon->connect`: C's own does its transport
1730 /// work and then calls `pasynManager->exceptionConnect(pasynUser)`, which
1731 /// resolves the slot it marks connected through `findDpCommon` on that same
1732 /// user (asynManager.c:2142-2162). So the addr the request carries selects
1733 /// the slot here too — [`PortDriverBase::set_addr_connected`] is the one
1734 /// owner of that resolution, and a user addressing no device marks the
1735 /// port's own. A driver that overrides this owns its transport's shape.
1736 fn connect(&mut self, user: &AsynUser) -> AsynResult<()> {
1737 // Single owner-API: edge-guarded fire is in PortDriverBase::set_connected.
1738 self.base_mut().set_addr_connected(user.addr, true);
1739 Ok(())
1740 }
1741
1742 fn disconnect(&mut self, user: &AsynUser) -> AsynResult<()> {
1743 self.base_mut().set_addr_connected(user.addr, false);
1744 Ok(())
1745 }
1746
1747 /// C `enable(pasynUser, 1)` (asynManager.c:2224-2251): ONE call, whose
1748 /// `findDpCommon(puserPvt)` picks the device's `dpCommon` or the port's from
1749 /// the addr the caller's user carries (:538-545). There is no second,
1750 /// device-only entry point in C and none here: [`PortDriverBase::
1751 /// set_addr_enabled`] is that resolution, and a user addressing no device
1752 /// lands on the port's own slot.
1753 fn enable(&mut self, user: &AsynUser) -> AsynResult<()> {
1754 // C `enable` refuses a defunct port (asynManager.c:2236-2241);
1755 // the guard lives in the single owner.
1756 self.base_mut().set_addr_enabled(user.addr, true)
1757 }
1758
1759 fn disable(&mut self, user: &AsynUser) -> AsynResult<()> {
1760 self.base_mut().set_addr_enabled(user.addr, false)
1761 }
1762
1763 fn connect_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
1764 self.base_mut().connect_addr(user.addr);
1765 Ok(())
1766 }
1767
1768 fn disconnect_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
1769 self.base_mut().disconnect_addr(user.addr);
1770 Ok(())
1771 }
1772
1773 fn get_option(&self, key: &str) -> AsynResult<String> {
1774 self.base()
1775 .options
1776 .get(key)
1777 .cloned()
1778 .ok_or_else(|| AsynError::OptionNotFound(key.to_string()))
1779 }
1780
1781 /// C `asynOption::setOption(void *drvPvt, asynUser *pasynUser, key, val)`.
1782 ///
1783 /// `user` is the caller's, and its `timeout` is the one that bounds any wire
1784 /// traffic the option write causes — an RFC 2217 negotiation on a COM port
1785 /// runs under it (`asynInterposeCom.c:475,495`). The option layer has no
1786 /// timeout of its own: an asynRecord option put negotiates under TMOT, an
1787 /// iocsh `asynSetOption` under its own 2 s (`asynShellCommands.c:119`).
1788 fn set_option(&mut self, _user: &mut AsynUser, key: &str, value: &str) -> AsynResult<()> {
1789 self.base_mut()
1790 .options
1791 .insert(key.to_string(), value.to_string());
1792 Ok(())
1793 }
1794
1795 /// The driver's own report — C `asynCommon::report`, which the manager calls
1796 /// last (`reportPrintPort`, asynManager.c:1113-1122) after printing the port's
1797 /// manager-level state itself.
1798 ///
1799 /// So this prints only what the *driver* owns. The port's enable / connect /
1800 /// queue / lock / exception / trace state is the manager's to print and is
1801 /// printed by `crate::port_actor::PortActor::report_port`; duplicating it
1802 /// here would give the operator two answers to the same question, from two
1803 /// owners, with no rule for which one wins.
1804 ///
1805 /// The default is C++ `asynPortDriver::report` (asynPortDriver.cpp:3677-3710):
1806 /// the port name; at `details >= 1` the timestamp, the EOS terminators *if the
1807 /// driver registered the octet interface* (`pasynStdInterfaces->octet.pinterface`,
1808 /// :3685) and the parameter library; at `details >= 3` the interrupt clients
1809 /// (:3695-3708).
1810 ///
1811 /// The report goes to `out`, C's `FILE *fp` — the driver never picks the
1812 /// stream. `crate::port_actor::PortActor::report_port` is the one owner that
1813 /// does, and it picks stdout, as `asynReport` does (asynShellCommands.c:589).
1814 fn report(&self, out: &mut dyn std::fmt::Write, level: i32) {
1815 use std::fmt::Write as _;
1816 let base = self.base();
1817 let _ = writeln!(out, "Port: {}", base.port_name);
1818 if level >= 1 {
1819 let _ = writeln!(
1820 out,
1821 " Timestamp: {}",
1822 format_timestamp(base.current_timestamp())
1823 );
1824 // C prints the EOS pair only when the driver registered `asynOctet`
1825 // (:3685) — on a port with no octet interface the two terminators are
1826 // the constructor's zeroed fields, and reporting them invents an EOS
1827 // the port cannot have.
1828 if self.has_octet_interface() {
1829 // C escapes the terminator with `epicsStrPrintEscaped` (:3687,
1830 // :3690) — the whole libCom table, not just CR and LF. A private
1831 // two-case table wrote a binary terminator (`\x03`, ESC, TAB, NUL)
1832 // raw into stdout (R16-48); [`crate::escape`] is the one owner.
1833 let input = base.input_eos(0);
1834 let output = base.output_eos(0);
1835 let _ = writeln!(
1836 out,
1837 " Input EOS[{}]: {}",
1838 input.len(),
1839 crate::escape::print_escaped(input)
1840 );
1841 let _ = writeln!(
1842 out,
1843 " Output EOS[{}]: {}",
1844 output.len(),
1845 crate::escape::print_escaped(output)
1846 );
1847 }
1848 // C hands its own level straight to `reportParams` (:3693).
1849 base.report_params(out, level);
1850 }
1851 // There is no options block: `asynPortDriver::report` never prints one
1852 // (asynPortDriver.cpp:3677-3710), and it could not — `asynOption` is a
1853 // get/set pair keyed by a string the *driver* defines, with nothing to
1854 // enumerate. The `option: k = v` lines this printed at `details >= 2` had
1855 // no C source and no fixed key set to be complete over: they listed
1856 // whatever happened to have been written through `setOption`, which is not
1857 // the port's option state (R16-49).
1858 if level >= 3 {
1859 report_interrupt_clients(out, base);
1860 }
1861 }
1862
1863 /// Whether this driver registered `asynOctet` — C's
1864 /// `pasynStdInterfaces->octet.pinterface != NULL`, which is set from the
1865 /// constructor's `interfaceMask` (asynPortDriver.cpp:4062). The Rust analogue
1866 /// of that mask is [`Self::capabilities`].
1867 fn has_octet_interface(&self) -> bool {
1868 self.capabilities()
1869 .iter()
1870 .any(|c| c.interface_type() == crate::interfaces::InterfaceType::Octet)
1871 }
1872
1873 // --- Scalar I/O (cache-based defaults, timeout not applicable) ---
1874
1875 // Cache-based defaults do NOT check connection state (C parity).
1876 // The port actor checks check_ready_addr() before dispatching, matching
1877 // C asyn where asynManager checks connection before calling the driver.
1878
1879 // Default reads use the STRICT getter: an undefined parameter must
1880 // surface as ParamUndefined, not success/0. C parity — the default
1881 // asynPortDriver::read{Int32,Int64,Float64,Octet,UInt32Digital}
1882 // (asynPortDriver.cpp) calls get{Integer,Integer64,Double,String,
1883 // UIntDigital}Param, and every paramVal getter throws
1884 // ParamValNotDefined → asynParamUndefined for an unset value
1885 // (paramVal.cpp:152,181,235,264,292). devAsyn* then routes that status
1886 // through asynStatusToEpicsAlarm(READ_ALARM, INVALID_ALARM) instead of
1887 // updating RVAL/clearing UDF (e.g. devAsynUInt32Digital.c:898-901,
1888 // devAsynInt32.c:844-847). The lax get_*_param accessors stay for
1889 // internal callers that explicitly want default-zero behavior.
1890
1891 fn read_int32(&mut self, user: &AsynUser) -> AsynResult<i32> {
1892 self.base().params.get_int32_strict(user.reason, user.addr)
1893 }
1894
1895 fn write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1896 self.base_mut()
1897 .params
1898 .set_int32(user.reason, user.addr, value)?;
1899 self.base_mut().call_param_callbacks(user.addr)
1900 }
1901
1902 fn read_int64(&mut self, user: &AsynUser) -> AsynResult<i64> {
1903 self.base().params.get_int64_strict(user.reason, user.addr)
1904 }
1905
1906 fn write_int64(&mut self, user: &mut AsynUser, value: i64) -> AsynResult<()> {
1907 self.base_mut()
1908 .params
1909 .set_int64(user.reason, user.addr, value)?;
1910 self.base_mut().call_param_callbacks(user.addr)
1911 }
1912
1913 /// C `asynInt32Base.c:99` default: report `low = high = 0` so a
1914 /// driver that does not implement getBounds makes convertAi/convertAo
1915 /// skip the LINEAR ESLO/EOFF computation (`devAsynInt32.c:444`).
1916 fn get_bounds_int32(&self, _user: &AsynUser) -> AsynResult<(i32, i32)> {
1917 Ok((0, 0))
1918 }
1919
1920 /// C `asynInt64Base.c:99` default: report `low = high = 0` (see
1921 /// `get_bounds_int32`).
1922 fn get_bounds_int64(&self, _user: &AsynUser) -> AsynResult<(i64, i64)> {
1923 Ok((0, 0))
1924 }
1925
1926 fn read_float64(&mut self, user: &AsynUser) -> AsynResult<f64> {
1927 self.base()
1928 .params
1929 .get_float64_strict(user.reason, user.addr)
1930 }
1931
1932 fn write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1933 self.base_mut()
1934 .params
1935 .set_float64(user.reason, user.addr, value)?;
1936 self.base_mut().call_param_callbacks(user.addr)
1937 }
1938
1939 fn read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
1940 let bytes = self
1941 .base()
1942 .params
1943 .get_string_strict(user.reason, user.addr)?;
1944 let n = bytes.len().min(buf.len());
1945 buf[..n].copy_from_slice(&bytes[..n]);
1946 Ok(n)
1947 }
1948
1949 fn write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1950 self.base_mut()
1951 .params
1952 .set_string(user.reason, user.addr, data.to_vec())?;
1953 self.base_mut().call_param_callbacks(user.addr)?;
1954 Ok(data.len())
1955 }
1956
1957 fn read_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<u32> {
1958 let val = self
1959 .base()
1960 .params
1961 .get_uint32_strict(user.reason, user.addr)?;
1962 Ok(val & mask)
1963 }
1964
1965 fn write_uint32_digital(
1966 &mut self,
1967 user: &mut AsynUser,
1968 value: u32,
1969 mask: u32,
1970 ) -> AsynResult<()> {
1971 // The asynUInt32Digital write interface carries no forced interrupt
1972 // mask — changed bits derive from value^old (interrupt_mask = 0).
1973 self.base_mut()
1974 .params
1975 .set_uint32(user.reason, user.addr, value, mask, 0)?;
1976 self.base_mut().call_param_callbacks(user.addr)
1977 }
1978
1979 /// Configure rising / falling interrupt masks for a
1980 /// UInt32Digital parameter. C parity:
1981 /// `asynPortDriver::setInterruptUInt32Digital`
1982 /// (`asynPortDriver.cpp:2346-2369`) → routes to
1983 /// `paramList::setUInt32Interrupt`. The default delegates to the
1984 /// param store; drivers that need to push the configuration to
1985 /// hardware (e.g. real GPIB cards toggling SRQ enable) override
1986 /// it.
1987 fn set_interrupt_uint32_digital(
1988 &mut self,
1989 user: &AsynUser,
1990 mask: u32,
1991 reason: InterruptReason,
1992 ) -> AsynResult<()> {
1993 self.base_mut()
1994 .params
1995 .set_uint32_interrupt(user.reason, user.addr, mask, reason)
1996 }
1997
1998 /// Clear bits from rising AND falling masks. C parity:
1999 /// `asynPortDriver::clearInterruptUInt32Digital`
2000 /// (`asynPortDriver.cpp:2392-2415`). Mirrors C — the call does
2001 /// not take an `interruptReason`; both masks are cleared.
2002 fn clear_interrupt_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<()> {
2003 self.base_mut()
2004 .params
2005 .clear_uint32_interrupt(user.reason, user.addr, mask)
2006 }
2007
2008 /// Read the configured rising / falling / combined mask. C
2009 /// parity: `asynPortDriver::getInterruptUInt32Digital`
2010 /// (`asynPortDriver.cpp:2438-2461`).
2011 fn get_interrupt_uint32_digital(
2012 &self,
2013 user: &AsynUser,
2014 reason: InterruptReason,
2015 ) -> AsynResult<u32> {
2016 self.base()
2017 .params
2018 .get_uint32_interrupt(user.reason, user.addr, reason)
2019 }
2020
2021 // --- Enum I/O (cache-based defaults) ---
2022
2023 fn read_enum(&mut self, user: &AsynUser) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
2024 self.base().params.get_enum(user.reason, user.addr)
2025 }
2026
2027 fn write_enum(&mut self, user: &mut AsynUser, index: usize) -> AsynResult<()> {
2028 self.base_mut()
2029 .params
2030 .set_enum_index(user.reason, user.addr, index)?;
2031 self.base_mut().call_param_callbacks(user.addr)
2032 }
2033
2034 fn write_enum_choices(
2035 &mut self,
2036 user: &mut AsynUser,
2037 choices: Arc<[EnumEntry]>,
2038 ) -> AsynResult<()> {
2039 self.base_mut()
2040 .params
2041 .set_enum_choices(user.reason, user.addr, choices)?;
2042 self.base_mut().call_param_callbacks(user.addr)
2043 }
2044
2045 // --- GenericPointer I/O (cache-based defaults) ---
2046
2047 fn read_generic_pointer(&mut self, user: &AsynUser) -> AsynResult<Arc<dyn Any + Send + Sync>> {
2048 self.base()
2049 .params
2050 .get_generic_pointer(user.reason, user.addr)
2051 }
2052
2053 fn write_generic_pointer(
2054 &mut self,
2055 user: &mut AsynUser,
2056 value: Arc<dyn Any + Send + Sync>,
2057 ) -> AsynResult<()> {
2058 self.base_mut()
2059 .params
2060 .set_generic_pointer(user.reason, user.addr, value)?;
2061 self.base_mut().call_param_callbacks(user.addr)
2062 }
2063
2064 // --- Array I/O (default: not supported) ---
2065
2066 fn read_float64_array(&mut self, _user: &AsynUser, _buf: &mut [f64]) -> AsynResult<usize> {
2067 Err(AsynError::InterfaceNotSupported("asynFloat64Array".into()))
2068 }
2069
2070 fn write_float64_array(&mut self, user: &AsynUser, data: &[f64]) -> AsynResult<()> {
2071 self.base_mut()
2072 .params
2073 .set_float64_array(user.reason, user.addr, data.to_vec())?;
2074 self.base_mut().call_param_callbacks(user.addr)
2075 }
2076
2077 fn read_int32_array(&mut self, _user: &AsynUser, _buf: &mut [i32]) -> AsynResult<usize> {
2078 Err(AsynError::InterfaceNotSupported("asynInt32Array".into()))
2079 }
2080
2081 fn write_int32_array(&mut self, user: &AsynUser, data: &[i32]) -> AsynResult<()> {
2082 self.base_mut()
2083 .params
2084 .set_int32_array(user.reason, user.addr, data.to_vec())?;
2085 self.base_mut().call_param_callbacks(user.addr)
2086 }
2087
2088 fn read_int8_array(&mut self, _user: &AsynUser, _buf: &mut [i8]) -> AsynResult<usize> {
2089 Err(AsynError::InterfaceNotSupported("asynInt8Array".into()))
2090 }
2091
2092 fn write_int8_array(&mut self, user: &AsynUser, data: &[i8]) -> AsynResult<()> {
2093 self.base_mut()
2094 .params
2095 .set_int8_array(user.reason, user.addr, data.to_vec())?;
2096 self.base_mut().call_param_callbacks(user.addr)
2097 }
2098
2099 fn read_int16_array(&mut self, _user: &AsynUser, _buf: &mut [i16]) -> AsynResult<usize> {
2100 Err(AsynError::InterfaceNotSupported("asynInt16Array".into()))
2101 }
2102
2103 fn write_int16_array(&mut self, user: &AsynUser, data: &[i16]) -> AsynResult<()> {
2104 self.base_mut()
2105 .params
2106 .set_int16_array(user.reason, user.addr, data.to_vec())?;
2107 self.base_mut().call_param_callbacks(user.addr)
2108 }
2109
2110 fn read_int64_array(&mut self, _user: &AsynUser, _buf: &mut [i64]) -> AsynResult<usize> {
2111 Err(AsynError::InterfaceNotSupported("asynInt64Array".into()))
2112 }
2113
2114 fn write_int64_array(&mut self, user: &AsynUser, data: &[i64]) -> AsynResult<()> {
2115 self.base_mut()
2116 .params
2117 .set_int64_array(user.reason, user.addr, data.to_vec())?;
2118 self.base_mut().call_param_callbacks(user.addr)
2119 }
2120
2121 fn read_float32_array(&mut self, _user: &AsynUser, _buf: &mut [f32]) -> AsynResult<usize> {
2122 Err(AsynError::InterfaceNotSupported("asynFloat32Array".into()))
2123 }
2124
2125 fn write_float32_array(&mut self, user: &AsynUser, data: &[f32]) -> AsynResult<()> {
2126 self.base_mut()
2127 .params
2128 .set_float32_array(user.reason, user.addr, data.to_vec())?;
2129 self.base_mut().call_param_callbacks(user.addr)
2130 }
2131
2132 // --- I/O methods (worker thread calls these) ---
2133 // Default: delegate to cache-based read_*/write_* for backward compat.
2134 // Real I/O drivers override these for actual hardware access.
2135
2136 fn io_read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
2137 self.read_octet(user, buf)
2138 }
2139
2140 /// Octet read that also reports the end-of-message reason — C
2141 /// parity for `asynOctet::read(... int *eomReason)`
2142 /// (`asynOctet.h:38-40`). The default implementation delegates to
2143 /// [`Self::io_read_octet`] and reconstructs a synthetic
2144 /// [`EomReason`]: `CNT` when the buffer filled, `empty` otherwise.
2145 /// Drivers that have native EOM information
2146 /// (`asynOctetSyncIO::read`, GPIB END, EOS match) must
2147 /// override this method so consumers — `asynRecord::EOMR`,
2148 /// `asynOctetSyncIO::read` mirrors — receive the real flags.
2149 fn io_read_octet_eom(
2150 &mut self,
2151 user: &AsynUser,
2152 buf: &mut [u8],
2153 ) -> AsynResult<(usize, EomReason)> {
2154 let cap = buf.len();
2155 let n = self.io_read_octet(user, buf)?;
2156 let eom = if n >= cap && cap > 0 {
2157 EomReason::CNT
2158 } else {
2159 EomReason::empty()
2160 };
2161 Ok((n, eom))
2162 }
2163
2164 fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
2165 self.write_octet(user, data)
2166 }
2167
2168 fn io_read_int32(&mut self, user: &AsynUser) -> AsynResult<i32> {
2169 self.read_int32(user)
2170 }
2171
2172 fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
2173 self.write_int32(user, value)
2174 }
2175
2176 fn io_read_int64(&mut self, user: &AsynUser) -> AsynResult<i64> {
2177 self.read_int64(user)
2178 }
2179
2180 fn io_write_int64(&mut self, user: &mut AsynUser, value: i64) -> AsynResult<()> {
2181 self.write_int64(user, value)
2182 }
2183
2184 fn io_read_float64(&mut self, user: &AsynUser) -> AsynResult<f64> {
2185 self.read_float64(user)
2186 }
2187
2188 fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
2189 self.write_float64(user, value)
2190 }
2191
2192 fn io_read_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<u32> {
2193 self.read_uint32_digital(user, mask)
2194 }
2195
2196 fn io_write_uint32_digital(
2197 &mut self,
2198 user: &mut AsynUser,
2199 value: u32,
2200 mask: u32,
2201 ) -> AsynResult<()> {
2202 self.write_uint32_digital(user, value, mask)
2203 }
2204
2205 fn io_flush(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
2206 Ok(())
2207 }
2208
2209 // --- Octet EOS (delegates to interpose stack by default) ---
2210 //
2211 // ## EOS connect-wait policy (C asyn issue #103)
2212 //
2213 // C asyn `asynOctetSyncIO::setInputEos` / `setOutputEos`
2214 // (`asynOctetSyncIO.c:300-321`, `:346-367`) bracket the driver call in
2215 // `lockPort`/`unlockPort`. `lockPort` is a plain mutex take —
2216 // `defunct` check, then `synchronousLock` (asynManager.c:1789-1798) —
2217 // and waits for no connection, so an EOS set blocks only behind
2218 // whoever else holds the port. The connect wait lives elsewhere:
2219 // `registerPort` calls `waitConnect(pport->pconnectUser,
2220 // pasynBase->autoConnectTimeout)` on an autoConnect port
2221 // (asynManager.c:2135), and `waitConnect` `epicsEventWaitWithTimeout`s
2222 // on a connect event signalled from an exception handler (:3292-3336).
2223 // That is what issue #103 sees: IOC startup pausing for the whole
2224 // autoConnect timeout when the device is off-line.
2225 //
2226 // The Rust path here is purely in-memory: `set_input_eos` and
2227 // `set_output_eos` write the bytes into `PortDriverBase` and the
2228 // EOS interpose stack reads from those fields at next read/write
2229 // time. No port lock, no connect-wait — so neither stall can reach an
2230 // EOS call here. If a future refactor introduces a connect-gated EOS
2231 // path (e.g. a driver that owns the EOS state inside its
2232 // connect()-allocated resource), authors MUST keep the wait optional /
2233 // bounded so the connect-wait failure mode doesn't return.
2234
2235 // Every hook takes the `asynUser`, because in C every one of them does
2236 // (`asynOctet::setInputEos(void *ppvt, asynUser *pasynUser, ...)`,
2237 // asynOctetBase.h; asynInterposeEos.c:288-296) and the addr it carries is
2238 // what picks the device's terminator. A port-wide EOS could not hold two.
2239
2240 /// The default is C's **Fail stub**, not a silent cache. `initOverride`
2241 /// swaps a NULL `setInputEos` for `setInputEosFail` (asynOctetBase.c:115),
2242 /// which answers `"<port> setInputEos not implemented"` and `asynError`
2243 /// (:370-380, :401-403) — and serial, IP, IP-server and FTDI really do
2244 /// leave it NULL (drvAsynSerialPort.c:1003, drvAsynIPPort.c:1049-1051,
2245 /// drvAsynIPServerPort.c:118-123, drvAsynFTDIPort.cpp:610-612). What
2246 /// makes IEOS work on those ports is `asynInterposeEos`, installed above
2247 /// the driver by `asynOctetBase::initialize` when `processEosIn` is set
2248 /// (:169-171) — so the terminator is accepted exactly when a layer takes
2249 /// it, and refused otherwise. A `noProcessEos` port reporting success and
2250 /// then never terminating is the failure this refusal exists to make loud.
2251 ///
2252 /// A driver with its own EOS methods overrides this, which is the Rust
2253 /// shape of a non-NULL entry in C's `asynOctet` table.
2254 fn set_input_eos(&mut self, user: &AsynUser, eos: &[u8]) -> AsynResult<()> {
2255 // Single write owner for input EOS: the per-device cache is what
2256 // `get_input_eos` and the binary-suppress save/restore read, and the
2257 // same value is forwarded to the interpose stack so an installed
2258 // `EosInterpose` actually terminates *this device's* reads on it.
2259 let addr = user.addr;
2260 let port = self.base().port_name.clone();
2261 let base = self.base_mut();
2262 // The length rule belongs to the layer that stores the terminator
2263 // (asynInterposeEos.c:299-303), so a port with no EOS support answers
2264 // "not implemented" for every length, exactly as its Fail stub does,
2265 // and a layer that owns the terminator refuses an illegal one without
2266 // storing it.
2267 match base.interpose_octet.set_input_eos(addr, eos) {
2268 EosSet::NotTaken => return Err(eos_not_implemented(&port, "setInputEos")),
2269 EosSet::IllegalLength => return Err(illegal_eoslen(&port, eos.len())),
2270 EosSet::Stored => {}
2271 }
2272 base.eos_entry(addr).input = eos.to_vec();
2273 Ok(())
2274 }
2275
2276 fn get_input_eos(&self, user: &AsynUser) -> Vec<u8> {
2277 self.base().input_eos(user.addr).to_vec()
2278 }
2279
2280 /// The output half of [`Self::set_input_eos`] — C `setOutputEosFail`
2281 /// (asynOctetBase.c:117, :409-411).
2282 fn set_output_eos(&mut self, user: &AsynUser, eos: &[u8]) -> AsynResult<()> {
2283 // Single write owner for output EOS (see `set_input_eos`): cache per
2284 // device and forward to the interpose stack so `EosInterpose` appends
2285 // the terminator on that device's writes.
2286 let addr = user.addr;
2287 let port = self.base().port_name.clone();
2288 let base = self.base_mut();
2289 match base.interpose_octet.set_output_eos(addr, eos) {
2290 EosSet::NotTaken => return Err(eos_not_implemented(&port, "setOutputEos")),
2291 EosSet::IllegalLength => return Err(illegal_eoslen(&port, eos.len())),
2292 EosSet::Stored => {}
2293 }
2294 base.eos_entry(addr).output = eos.to_vec();
2295 Ok(())
2296 }
2297
2298 fn get_output_eos(&self, user: &AsynUser) -> Vec<u8> {
2299 self.base().output_eos(user.addr).to_vec()
2300 }
2301
2302 // --- asynGpib (IEEE-488 bus control) ---
2303 //
2304 // The four command methods of C's `asynGpib` interface (asynGpibDriver.h:47-51),
2305 // which asynGpib.c passes straight through to the driver's `asynGpibPort`
2306 // (asynGpib.c:472-496). A driver that implements them declares
2307 // [`crate::interfaces::Capability::Gpib`], and that declaration is what a
2308 // client's `findInterface(asynGpibType)` answers — asynRecord reads it into
2309 // GPIBIV and refuses UCMD/ACMD when it is 0 (asynRecord.c:1232-1241,
2310 // :1647-1651).
2311 //
2312 // The defaults refuse: a port that has not declared the capability can only
2313 // be reached here by a caller that skipped the registry, and C has nothing
2314 // to call in that case (the interface pointer is NULL).
2315 //
2316 // Not ported: the `asynGpibPort` methods that exist solely to drive
2317 // asynGpib's SRQ poll thread — `srqStatus`, `srqEnable`, `serialPollBegin`,
2318 // `serialPoll`, `serialPollEnd` (asynGpibDriver.h:88-92), plus `pollAddr` /
2319 // `srqHappened` (asynGpib.c:498-559, 633-656). Nothing in this tree polls
2320 // SRQ; asynRecord's own "Serial Poll" ACMD does not use them (it sends SPE,
2321 // reads one octet, sends SPD — asynRecord.c:1717-1746).
2322
2323 /// C `asynGpib::universalCmd` — send one universal command byte with ATN
2324 /// asserted (asynGpib.c:480-484, `vxiUniversalCmd` drvVxi11.c:1406-1424).
2325 fn gpib_universal_cmd(&mut self, _user: &mut AsynUser, _cmd: u8) -> AsynResult<()> {
2326 Err(AsynError::Status {
2327 status: AsynStatus::Error,
2328 message: "port has no asynGpib interface".into(),
2329 })
2330 }
2331
2332 /// C `asynGpib::addressedCmd` — send an addressed-command frame with ATN
2333 /// asserted (asynGpib.c:472-478, `vxiAddressedCmd` drvVxi11.c:1360-1404).
2334 /// The frame is built by [`crate::interfaces::gpib::addressed_request`].
2335 fn gpib_addressed_cmd(&mut self, _user: &mut AsynUser, _data: &[u8]) -> AsynResult<()> {
2336 Err(AsynError::Status {
2337 status: AsynStatus::Error,
2338 message: "port has no asynGpib interface".into(),
2339 })
2340 }
2341
2342 /// C `asynGpib::ifc` — assert Interface Clear (asynGpib.c:486-490).
2343 fn gpib_ifc(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
2344 Err(AsynError::Status {
2345 status: AsynStatus::Error,
2346 message: "port has no asynGpib interface".into(),
2347 })
2348 }
2349
2350 /// C `asynGpib::ren` — set the Remote Enable line (asynGpib.c:492-496).
2351 fn gpib_ren(&mut self, _user: &mut AsynUser, _enable: bool) -> AsynResult<()> {
2352 Err(AsynError::Status {
2353 status: AsynStatus::Error,
2354 message: "port has no asynGpib interface".into(),
2355 })
2356 }
2357
2358 // --- Lifecycle ---
2359
2360 /// Called when the port is being shut down. Drivers override this
2361 /// to release hardware resources. Matches C asynPortDriver::shutdownPortDriver().
2362 fn shutdown(&mut self) -> AsynResult<()> {
2363 Ok(())
2364 }
2365
2366 // --- drvUser ---
2367
2368 /// Resolve a record's bind request ([`DrvUserRequest`]: drvInfo string, asyn
2369 /// `addr`, and the record's asyn interface) to a [`DrvUserInfo`] — the
2370 /// asyn-rs analogue of C `drvUserCreate`.
2371 ///
2372 /// Takes `&mut self` so a driver can register a parameter on demand from the
2373 /// resolved drvInfo (C Autoparam lazy creation) rather than requiring it be
2374 /// declared up front. Such a driver must create the parameter with the type
2375 /// the record will read it as — [`DrvUserRequest::iface`] — the way C
2376 /// `adsAsynPortDriver::getRecordInfoFromDrvInfo` derives the parameter's
2377 /// asyn type from the bound record's DTYP.
2378 ///
2379 /// [`DrvUserRequest::addr`] lets a multi-device driver reject an
2380 /// out-of-range address at bind time (C `drvUserCreate` runs `checkOffset`,
2381 /// drvModbusAsyn.cpp:378-384) instead of alarming on every I/O.
2382 ///
2383 /// Default: look up the shared reason by parameter name; ignore the rest.
2384 fn drv_user_create(&mut self, req: &DrvUserRequest) -> AsynResult<DrvUserInfo> {
2385 let reason = self
2386 .base()
2387 .params
2388 .find_param(&req.drv_info)
2389 .ok_or_else(|| AsynError::ParamNotFound(req.drv_info.clone()))?;
2390 Ok(DrvUserInfo::from_reason(reason))
2391 }
2392
2393 // --- Capabilities ---
2394
2395 /// Declare the capabilities this driver supports.
2396 /// Default implementation includes all scalar read/write operations.
2397 fn capabilities(&self) -> Vec<crate::interfaces::Capability> {
2398 crate::interfaces::default_capabilities()
2399 }
2400
2401 /// Check if this driver supports a specific capability.
2402 fn supports(&self, cap: crate::interfaces::Capability) -> bool {
2403 self.capabilities().contains(&cap)
2404 }
2405
2406 fn init(&mut self) -> AsynResult<()> {
2407 Ok(())
2408 }
2409}
2410
2411/// The driver sitting at the **bottom** of a port's octet interpose chain — C's
2412/// driver-registered `asynOctet` interface, the one `interposeInterface` keeps
2413/// as `pPrev` when it pushes a layer on top (asynManager.c:2190-2220).
2414///
2415/// A driver never knows it is being interposed in C: `findInterface` hands the
2416/// caller the topmost layer, and the layer calls down. The Rust equivalent of
2417/// "the caller" is the port actor, so the chain runs there
2418/// ([`octet_read_chain`] and friends) and the driver's `io_*_octet` are the raw
2419/// device transfer, nothing more.
2420struct DriverOctetLink<'a> {
2421 driver: &'a mut dyn PortDriver,
2422}
2423
2424impl OctetNext for DriverOctetLink<'_> {
2425 /// C `asynOctetBase::readIt` (asynOctetBase.c:224-238): call the driver's
2426 /// own `read`, and on success fan the result out to the port's octet
2427 /// interrupt users.
2428 ///
2429 /// This is *below* the EOS layer by construction, and that is the whole
2430 /// point. C interposes `octetBase` directly on the driver
2431 /// (asynOctetBase.c:156-159) and only then pushes `asynInterposeEos` on top
2432 /// of it (:170-171), so the stack is `EOS → octetBase → driver`: every
2433 /// interrupt user sees the RAW DRIVER CHUNK — terminator included, with the
2434 /// driver's own CNT/END eomReason — and gets one callback per lower-level
2435 /// read, not one per EOS-completed message. Firing above the chain (as the
2436 /// port actor used to) handed an I/O-Intr record `"abc"`/EOMR=EOS where C
2437 /// hands it `"abc\r\n"`/EOMR=CNT|END.
2438 fn read(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
2439 let (nbytes_transferred, eom_reason) = self.driver.io_read_octet_eom(user, buf)?;
2440 if self.driver.base().octet_interrupt_process {
2441 let raw = &buf[..nbytes_transferred];
2442 // C's rule for a device read: `addr` decides, `reason` is never
2443 // consulted (asynOctetBase.c:202-210). `reason` still rides on the
2444 // value — C leaves `pasynUser->reason` on the callback's user — but
2445 // it selects nobody.
2446 self.driver.base().interrupts.notify_octet(
2447 OctetFanOut::ByAddr(user.addr),
2448 InterruptValue {
2449 reason: user.reason,
2450 addr: user.addr,
2451 value: ParamValue::Octet(raw.to_vec()),
2452 timestamp: SystemTime::now(),
2453 iface: Some(InterfaceType::Octet),
2454 ..Default::default()
2455 },
2456 );
2457 }
2458 Ok(OctetReadResult {
2459 nbytes_transferred,
2460 eom_reason,
2461 })
2462 }
2463
2464 fn write(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
2465 self.driver.io_write_octet(user, data)
2466 }
2467
2468 fn flush(&mut self, user: &mut AsynUser) -> AsynResult<()> {
2469 self.driver.io_flush(user)
2470 }
2471}
2472
2473/// Run `f` with the port's chain and the driver below it, both borrowed at once.
2474///
2475/// The chain lives inside the driver (`base.interpose_octet`) while the driver
2476/// is the chain's base, so the two are lifted apart for the duration of one
2477/// transfer and the chain is put straight back. The actor owns the driver and is
2478/// the only caller, so no other code can observe the port between the take and
2479/// the restore.
2480fn with_octet_chain<T>(
2481 driver: &mut dyn PortDriver,
2482 f: impl FnOnce(&mut OctetInterposeStack, &mut DriverOctetLink<'_>) -> T,
2483) -> T {
2484 let multi_device = driver.base().flags.multi_device;
2485 let mut chain = std::mem::replace(
2486 &mut driver.base_mut().interpose_octet,
2487 OctetInterposeStack::new(multi_device),
2488 );
2489 let result = {
2490 let mut link = DriverOctetLink {
2491 driver: &mut *driver,
2492 };
2493 f(&mut chain, &mut link)
2494 };
2495 driver.base_mut().interpose_octet = chain;
2496 result
2497}
2498
2499/// One octet read on the port: through every interpose layer installed on the
2500/// addressed device, ending at the driver.
2501///
2502/// C `asynOctet::read` on the interface `findInterface` resolves — which is the
2503/// outermost interpose whenever one is installed. The chain belongs to the
2504/// **port**, not to the driver: `asynInterposeEos` / `asynInterposeEcho` /
2505/// `asynInterposeDelay` are pushed by the manager (asynManager.c:2190-2220) and
2506/// the driver below is not consulted and cannot opt out. Dispatching inside each
2507/// driver instead — as this crate did — made the chain per-driver opt-in, so the
2508/// EOS layer `drvAsynFTDIPortConfigure` installs (drvAsynFTDIPort.cpp:622-623,
2509/// ftdi.rs) was never run by anything.
2510pub(crate) fn octet_read_chain(
2511 driver: &mut dyn PortDriver,
2512 user: &AsynUser,
2513 buf: &mut [u8],
2514) -> AsynResult<(usize, EomReason)> {
2515 with_octet_chain(driver, |chain, link| chain.dispatch_read(user, buf, link))
2516 .map(|r| (r.nbytes_transferred, r.eom_reason))
2517}
2518
2519/// One octet write on the port, through the addressed device's chain
2520/// (see [`octet_read_chain`]).
2521pub(crate) fn octet_write_chain(
2522 driver: &mut dyn PortDriver,
2523 user: &mut AsynUser,
2524 data: &[u8],
2525) -> AsynResult<usize> {
2526 with_octet_chain(driver, |chain, link| chain.dispatch_write(user, data, link))
2527}
2528
2529/// One octet flush on the port, through the addressed device's chain — C
2530/// `asynInterposeEos::flushIt` resets the layer's read-ahead buffer and then
2531/// flushes the layer below (asynInterposeEos.c:256-268), which only happens if
2532/// the flush enters the chain at the top (see [`octet_read_chain`]).
2533pub(crate) fn octet_flush_chain(
2534 driver: &mut dyn PortDriver,
2535 user: &mut AsynUser,
2536) -> AsynResult<()> {
2537 with_octet_chain(driver, |chain, link| chain.dispatch_flush(user, link))
2538}
2539
2540#[cfg(test)]
2541mod tests {
2542 use super::*;
2543
2544 /// A driver whose `io_read_octet_eom` hands back one scripted chunk per
2545 /// call, with the driver-level eomReason C would report (CNT when the
2546 /// caller's buffer filled, END never — a stream driver has no message
2547 /// boundary). The EOS layer above it is what turns chunks into messages.
2548 struct ChunkDriver {
2549 base: PortDriverBase,
2550 chunks: Vec<Vec<u8>>,
2551 next: usize,
2552 }
2553
2554 impl ChunkDriver {
2555 fn new(chunks: Vec<&[u8]>) -> Self {
2556 let mut base = PortDriverBase::new("chunk", 1, PortFlags::default());
2557 base.octet_interrupt_process = true;
2558 base.init_connected(true);
2559 Self {
2560 base,
2561 chunks: chunks.into_iter().map(|c| c.to_vec()).collect(),
2562 next: 0,
2563 }
2564 }
2565 }
2566
2567 impl PortDriver for ChunkDriver {
2568 fn base(&self) -> &PortDriverBase {
2569 &self.base
2570 }
2571 fn base_mut(&mut self) -> &mut PortDriverBase {
2572 &mut self.base
2573 }
2574 fn io_read_octet_eom(
2575 &mut self,
2576 _user: &AsynUser,
2577 buf: &mut [u8],
2578 ) -> AsynResult<(usize, EomReason)> {
2579 let chunk = self.chunks.get(self.next).cloned().unwrap_or_default();
2580 self.next += 1;
2581 let n = chunk.len().min(buf.len());
2582 buf[..n].copy_from_slice(&chunk[..n]);
2583 let eom = if n == buf.len() {
2584 EomReason::CNT
2585 } else {
2586 EomReason::empty()
2587 };
2588 Ok((n, eom))
2589 }
2590 }
2591
2592 /// The octet interrupt fan-out runs BELOW the interpose chain, at the
2593 /// driver link — C's `asynOctetBase::readIt` (asynOctetBase.c:224-238),
2594 /// which is interposed directly on the driver (:156-159) with the EOS layer
2595 /// pushed on top of it (:170-171).
2596 ///
2597 /// Two boundaries, both invisible when the fan-out ran above the chain:
2598 /// (1) the payload an interrupt user receives is the RAW driver chunk —
2599 /// terminator included — not the EOS-stripped message the caller gets; and
2600 /// (2) a message assembled from two lower-level reads fires TWO callbacks,
2601 /// one per driver read, not one per completed message.
2602 #[test]
2603 fn octet_interrupt_fans_out_below_the_interpose_chain() {
2604 use crate::interpose::eos::EosInterpose;
2605 use crate::interrupt::{InterruptFilter, InterruptValue};
2606 use std::sync::{Arc, Mutex};
2607
2608 // The message "abc\r\n" arrives split across two driver reads.
2609 let mut drv = ChunkDriver::new(vec![b"ab", b"c\r\n"]);
2610 drv.base_mut()
2611 .install_octet_interpose(Box::new(EosInterpose::default()));
2612 drv.set_input_eos(&AsynUser::default(), b"\r\n").unwrap();
2613
2614 let seen: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
2615 let seen_cb = seen.clone();
2616 let _sub = drv.base().interrupts.register_sync_callback(
2617 InterruptFilter::default(),
2618 move |iv: &InterruptValue| {
2619 if let ParamValue::Octet(s) = &iv.value {
2620 seen_cb.lock().unwrap().push(s.clone());
2621 }
2622 },
2623 );
2624
2625 let user = AsynUser::default();
2626 let mut buf = [0u8; 32];
2627 let (n, eom) = octet_read_chain(&mut drv, &user, &mut buf).unwrap();
2628
2629 // The CALLER gets the EOS-terminated message, terminator stripped.
2630 assert_eq!(&buf[..n], b"abc");
2631 assert!(eom.contains(EomReason::EOS), "caller sees EOS, got {eom:?}");
2632
2633 // The INTERRUPT USERS get the raw driver chunks, terminator included,
2634 // one callback per lower-level read.
2635 assert_eq!(
2636 *seen.lock().unwrap(),
2637 vec![b"ab".to_vec(), b"c\r\n".to_vec()],
2638 "each driver read fans out its raw chunk"
2639 );
2640 }
2641
2642 /// A device answer is bytes. C's octet callback hands the subscriber
2643 /// `pasynUser`'s buffer verbatim (`asynOctetBase.c:224-238`) and
2644 /// `asynPortDriver::doCallbacksOctet` hands it `val.c_str()`
2645 /// (asynPortDriver.cpp:2011-2027) — a `char *`, which carries any byte.
2646 /// Carrying the payload in a Rust `String` could not: the fan-out decoded
2647 /// it lossily, so a binary protocol's `0x80..=0xff` reached every I/O Intr
2648 /// record as U+FFFD (`ef bf bd`), longer than the byte it replaced and no
2649 /// longer the device's answer.
2650 #[test]
2651 fn an_octet_interrupt_carries_a_non_utf8_device_byte_verbatim() {
2652 use crate::interrupt::{InterruptFilter, InterruptValue};
2653 use std::sync::{Arc, Mutex};
2654
2655 // A Modbus-shaped answer: a high byte, an embedded NUL, a lone 0xff.
2656 const RAW: &[u8] = &[0x01, 0x80, 0x00, 0xfe, 0xff, 0x41];
2657 let mut drv = ChunkDriver::new(vec![RAW]);
2658
2659 let seen: Arc<Mutex<Vec<Vec<u8>>>> = Arc::new(Mutex::new(Vec::new()));
2660 let seen_cb = seen.clone();
2661 let _sub = drv.base().interrupts.register_sync_callback(
2662 InterruptFilter::default(),
2663 move |iv: &InterruptValue| {
2664 if let ParamValue::Octet(bytes) = &iv.value {
2665 seen_cb.lock().unwrap().push(bytes.clone());
2666 }
2667 },
2668 );
2669
2670 let user = AsynUser::default();
2671 let mut buf = [0u8; 32];
2672 let (n, _eom) = octet_read_chain(&mut drv, &user, &mut buf).unwrap();
2673
2674 assert_eq!(&buf[..n], RAW, "the caller gets the device bytes");
2675 assert_eq!(
2676 *seen.lock().unwrap(),
2677 vec![RAW.to_vec()],
2678 "so does the interrupt user — no lossy decode on the fan-out"
2679 );
2680 }
2681
2682 /// The `interruptAccept` gate: a `call_param_callbacks` fired before the
2683 /// IOC accepts interrupts must PRESERVE the changed-flag, not consume it,
2684 /// so a value seeded at construction still reaches the record on the
2685 /// one-shot flush the scan facility fires at `iocInit`. C
2686 /// `asynPortDriver.cpp:838` returns before `flags.clear()` (`:871`) while
2687 /// `interruptAccept` is false.
2688 ///
2689 /// Before this gate existed, the first `call_param_callbacks` — a driver's
2690 /// own acquisition/array task can fire it before the `_RBV` records
2691 /// subscribe — cleared the flag, and a read-only seed
2692 /// (`Manufacturer`, `MaxSizeX`) was lost for the life of the process.
2693 ///
2694 /// This mutates a process-global flag; nextest isolates each test in its
2695 /// own process, so it does not race with the crate's other port tests.
2696 #[test]
2697 fn call_param_callbacks_before_interrupt_accept_preserves_the_seed() {
2698 use crate::interrupt::{InterruptFilter, InterruptValue};
2699 use epics_libcom_rs::runtime::interrupt_accept::set_interrupts_accepted;
2700 use std::sync::{Arc, Mutex};
2701
2702 let mut base = PortDriverBase::new("gate", 1, PortFlags::default());
2703 let val = base.create_param("VAL", ParamType::Int32).unwrap();
2704
2705 let seen: Arc<Mutex<Vec<i32>>> = Arc::new(Mutex::new(Vec::new()));
2706 let sink = seen.clone();
2707 let _sub = base.interrupts.register_sync_callback(
2708 InterruptFilter {
2709 reason: Some(val),
2710 addr: Some(0),
2711 uint32_mask: None,
2712 iface: None,
2713 },
2714 move |iv: &InterruptValue| {
2715 if let ParamValue::Int32(v) = &iv.value {
2716 sink.lock().unwrap().push(*v);
2717 }
2718 },
2719 );
2720
2721 // Pre-iocInit: interrupts not accepted. The subscriber exists (the
2722 // record registered), the driver seeds a read-only value, and its own
2723 // task fires a callback — which must deliver NOTHING and leave the flag
2724 // pending.
2725 set_interrupts_accepted(false);
2726 base.set_int32_param(val, 0, 640).unwrap();
2727 base.call_param_callbacks(0).unwrap();
2728 assert!(
2729 seen.lock().unwrap().is_empty(),
2730 "a callback fired before interruptAccept must not deliver — got {:?}",
2731 *seen.lock().unwrap()
2732 );
2733
2734 // iocInit accepts interrupts; the flush the scan facility now performs
2735 // delivers the preserved seed exactly once.
2736 set_interrupts_accepted(true);
2737 base.call_param_callbacks(0).unwrap();
2738 assert_eq!(
2739 *seen.lock().unwrap(),
2740 vec![640],
2741 "the seed preserved across the gated call must reach the subscriber \
2742 once interrupts are accepted"
2743 );
2744 }
2745
2746 struct TestDriver {
2747 base: PortDriverBase,
2748 }
2749
2750 impl TestDriver {
2751 fn new() -> Self {
2752 // These tests exercise `call_param_callbacks` delivery with no
2753 // `iocInit` to open the interruptAccept gate. Establish the
2754 // precondition a real port has by the time records process — the
2755 // scan facility has run `scan_run` — which is the direct analogue
2756 // of C's non-IOC translation unit that unit tests link with
2757 // `interruptAccept = 1` (`asynPortDriver.cpp:23-25`). The seed-
2758 // preservation regression test builds its own `PortDriverBase` and
2759 // drives the gate itself, so it is unaffected.
2760 epics_libcom_rs::runtime::interrupt_accept::set_interrupts_accepted(true);
2761 let mut base = PortDriverBase::new("test", 1, PortFlags::default());
2762 base.create_param("VAL", ParamType::Int32).unwrap();
2763 base.create_param("TEMP", ParamType::Float64).unwrap();
2764 base.create_param("MSG", ParamType::Octet).unwrap();
2765 base.create_param("BITS", ParamType::UInt32Digital).unwrap();
2766 Self { base }
2767 }
2768 }
2769
2770 impl PortDriver for TestDriver {
2771 fn base(&self) -> &PortDriverBase {
2772 &self.base
2773 }
2774 fn base_mut(&mut self) -> &mut PortDriverBase {
2775 &mut self.base
2776 }
2777 }
2778
2779 #[test]
2780 fn test_default_read_write_int32() {
2781 let mut drv = TestDriver::new();
2782 let mut user = AsynUser::new(0);
2783 drv.write_int32(&mut user, 42).unwrap();
2784 let user = AsynUser::new(0);
2785 assert_eq!(drv.read_int32(&user).unwrap(), 42);
2786 }
2787
2788 #[test]
2789 fn test_default_read_write_float64() {
2790 let mut drv = TestDriver::new();
2791 let mut user = AsynUser::new(1);
2792 drv.write_float64(&mut user, 3.14).unwrap();
2793 let user = AsynUser::new(1);
2794 assert!((drv.read_float64(&user).unwrap() - 3.14).abs() < 1e-10);
2795 }
2796
2797 #[test]
2798 fn test_default_read_write_octet() {
2799 let mut drv = TestDriver::new();
2800 let mut user = AsynUser::new(2);
2801 drv.write_octet(&mut user, b"hello").unwrap();
2802 let user = AsynUser::new(2);
2803 let mut buf = [0u8; 32];
2804 let n = drv.read_octet(&user, &mut buf).unwrap();
2805 assert_eq!(&buf[..n], b"hello");
2806 }
2807
2808 #[test]
2809 fn test_default_read_write_uint32() {
2810 let mut drv = TestDriver::new();
2811 let mut user = AsynUser::new(3);
2812 drv.write_uint32_digital(&mut user, 0xFF, 0x0F).unwrap();
2813 let user = AsynUser::new(3);
2814 assert_eq!(drv.read_uint32_digital(&user, 0xFF).unwrap(), 0x0F);
2815 }
2816
2817 #[test]
2818 fn test_connect_disconnect() {
2819 let mut drv = TestDriver::new();
2820 let user = AsynUser::default();
2821 assert!(drv.base().is_connected());
2822 drv.disconnect(&user).unwrap();
2823 assert!(!drv.base().is_connected());
2824 drv.connect(&user).unwrap();
2825 assert!(drv.base().is_connected());
2826 }
2827
2828 #[test]
2829 fn test_drv_user_create() {
2830 let mut drv = TestDriver::new();
2831 assert_eq!(
2832 drv.drv_user_create(&DrvUserRequest::new("VAL", 0))
2833 .unwrap()
2834 .reason,
2835 0
2836 );
2837 assert_eq!(
2838 drv.drv_user_create(&DrvUserRequest::new("TEMP", 0))
2839 .unwrap()
2840 .reason,
2841 1
2842 );
2843 assert!(
2844 drv.drv_user_create(&DrvUserRequest::new("NOPE", 0))
2845 .is_err()
2846 );
2847 }
2848
2849 #[test]
2850 fn test_call_param_callbacks() {
2851 let mut drv = TestDriver::new();
2852 let mut rx = drv.base_mut().interrupts.subscribe_async();
2853
2854 drv.base_mut().set_int32_param(0, 0, 100).unwrap();
2855 drv.base_mut().set_float64_param(1, 0, 2.0).unwrap();
2856 drv.base_mut().call_param_callbacks(0).unwrap();
2857
2858 let v1 = rx.try_recv().unwrap();
2859 assert_eq!(v1.reason, 0);
2860 let v2 = rx.try_recv().unwrap();
2861 assert_eq!(v2.reason, 1);
2862 assert!(rx.try_recv().is_err());
2863 }
2864
2865 #[test]
2866 fn flush_skips_undefined_scalar_but_keeps_array_trigger() {
2867 // C asynPortDriver.cpp:845 — a status/alarm change (or bare
2868 // mark_changed) on a never-set scalar consumes the changed flag
2869 // but fires no callback. Array/generic-pointer triggers have no
2870 // callCallbacks analog (:846-865 switch is scalar-only) and must
2871 // still fire even while undefined.
2872 let mut drv = TestDriver::new();
2873 let arr = drv
2874 .base_mut()
2875 .create_param("ARR", ParamType::Int32Array)
2876 .unwrap();
2877 let mut rx = drv.base_mut().interrupts.subscribe_async();
2878
2879 // Status change on the never-set scalar VAL (index 0): marks it
2880 // changed but it stays undefined.
2881 drv.base_mut()
2882 .params
2883 .set_param_status(0, 0, AsynStatus::Error, 0, 0)
2884 .unwrap();
2885 // Mark the never-set array param changed: an override-served trigger.
2886 drv.base_mut().mark_param_changed(arr, 0).unwrap();
2887
2888 drv.base_mut().call_param_callbacks(0).unwrap();
2889
2890 // Only the array trigger is delivered; the undefined scalar is gated.
2891 let iv = rx.try_recv().unwrap();
2892 assert_eq!(
2893 iv.reason, arr,
2894 "array trigger must still fire while undefined"
2895 );
2896 assert!(
2897 rx.try_recv().is_err(),
2898 "undefined scalar must not emit an I/O Intr"
2899 );
2900
2901 // Once the scalar is defined, a subsequent change does fire.
2902 drv.base_mut().set_int32_param(0, 0, 7).unwrap();
2903 drv.base_mut().call_param_callbacks(0).unwrap();
2904 let iv2 = rx.try_recv().unwrap();
2905 assert_eq!(iv2.reason, 0, "defined scalar must fire");
2906 }
2907
2908 #[test]
2909 fn uint32_callback_mask_does_not_leak_across_flushes() {
2910 // C resets uInt32CallbackMask = 0 after each uint32Callback
2911 // (asynPortDriver.cpp:855): a second flush must deliver only the
2912 // bits changed since the first, never the accumulated history.
2913 let mut drv = TestDriver::new();
2914 let mut rx = drv.base_mut().interrupts.subscribe_async();
2915
2916 // flush 1: change bit 0 on BITS (param index 3).
2917 drv.base_mut()
2918 .params
2919 .set_uint32(3, 0, 0x01, 0x01, 0)
2920 .unwrap();
2921 drv.base_mut().call_param_callbacks(0).unwrap();
2922 let iv1 = rx.try_recv().unwrap();
2923 assert_eq!(iv1.reason, 3);
2924 assert_eq!(iv1.uint32_changed_mask, 0x01);
2925
2926 // flush 2: change bit 1 only — must deliver 0x02, not 0x03.
2927 drv.base_mut()
2928 .params
2929 .set_uint32(3, 0, 0x02, 0x02, 0)
2930 .unwrap();
2931 drv.base_mut().call_param_callbacks(0).unwrap();
2932 let iv2 = rx.try_recv().unwrap();
2933 assert_eq!(
2934 iv2.uint32_changed_mask, 0x02,
2935 "second flush must not leak flush-1 bits via an un-reset mask"
2936 );
2937 assert_eq!(
2938 drv.base().params.get_uint32_interrupt_mask(3, 0).unwrap(),
2939 0,
2940 "the flush must consume (reset) the callback mask"
2941 );
2942 }
2943
2944 #[test]
2945 fn test_call_param_callbacks_propagates_aux_status_and_alarm() {
2946 // C parity: asynPortDriver.cpp:633-637 writes the param's stored
2947 // status / alarmStatus / alarmSeverity onto the subscriber's
2948 // pasynUser before invoking the callback. The Rust port carries
2949 // those fields on InterruptValue.
2950 let mut drv = TestDriver::new();
2951 let mut rx = drv.base_mut().interrupts.subscribe_async();
2952
2953 drv.base_mut().set_int32_param(0, 0, 99).unwrap();
2954 drv.base_mut()
2955 .params
2956 .set_param_status(0, 0, crate::error::AsynStatus::Timeout, 4, 2)
2957 .unwrap();
2958 drv.base_mut().call_param_callbacks(0).unwrap();
2959
2960 let iv = rx.try_recv().unwrap();
2961 assert_eq!(iv.reason, 0);
2962 assert!(matches!(iv.aux_status, crate::error::AsynStatus::Timeout));
2963 assert_eq!(iv.alarm_status, 4);
2964 assert_eq!(iv.alarm_severity, 2);
2965 }
2966
2967 #[test]
2968 fn test_call_param_callback_single_propagates_aux_status() {
2969 // Mirror for the single-flush path (call_param_callback).
2970 let mut drv = TestDriver::new();
2971 let mut rx = drv.base_mut().interrupts.subscribe_async();
2972
2973 drv.base_mut().set_int32_param(0, 0, 1).unwrap();
2974 drv.base_mut()
2975 .params
2976 .set_param_status(0, 0, crate::error::AsynStatus::Disconnected, 7, 3)
2977 .unwrap();
2978 drv.base_mut().call_param_callback(0, 0).unwrap();
2979
2980 let iv = rx.try_recv().unwrap();
2981 assert!(matches!(
2982 iv.aux_status,
2983 crate::error::AsynStatus::Disconnected
2984 ));
2985 assert_eq!(iv.alarm_status, 7);
2986 assert_eq!(iv.alarm_severity, 3);
2987 }
2988
2989 #[test]
2990 fn test_no_callback_for_unchanged() {
2991 let mut drv = TestDriver::new();
2992 let mut rx = drv.base_mut().interrupts.subscribe_async();
2993
2994 drv.base_mut().set_int32_param(0, 0, 5).unwrap();
2995 drv.base_mut().call_param_callbacks(0).unwrap();
2996 let _ = rx.try_recv().unwrap(); // consume
2997
2998 // Set same value — no interrupt
2999 drv.base_mut().set_int32_param(0, 0, 5).unwrap();
3000 drv.base_mut().call_param_callbacks(0).unwrap();
3001 assert!(rx.try_recv().is_err());
3002 }
3003
3004 #[test]
3005 fn test_array_not_supported_by_default() {
3006 let mut drv = TestDriver::new();
3007 let user = AsynUser::new(0);
3008 let mut buf = [0f64; 10];
3009 assert!(drv.read_float64_array(&user, &mut buf).is_err());
3010 assert!(drv.write_float64_array(&user, &[1.0]).is_err());
3011 }
3012
3013 #[test]
3014 fn test_option_set_get() {
3015 let mut drv = TestDriver::new();
3016 drv.set_option(&mut AsynUser::default(), "baud", "9600")
3017 .unwrap();
3018 assert_eq!(drv.get_option("baud").unwrap(), "9600");
3019 drv.set_option(&mut AsynUser::default(), "baud", "115200")
3020 .unwrap();
3021 assert_eq!(drv.get_option("baud").unwrap(), "115200");
3022 }
3023
3024 #[test]
3025 fn test_option_not_found() {
3026 let drv = TestDriver::new();
3027 let err = drv.get_option("nonexistent").unwrap_err();
3028 assert!(matches!(err, AsynError::OptionNotFound(_)));
3029 }
3030
3031 #[test]
3032 fn test_report_no_panic() {
3033 let mut drv = TestDriver::new();
3034 drv.set_option(&mut AsynUser::default(), "testkey", "testval")
3035 .unwrap();
3036 drv.base_mut().set_int32_param(0, 0, 42).unwrap();
3037 for level in 0..=3 {
3038 let mut out = String::new();
3039 drv.report(&mut out, level);
3040 }
3041 }
3042
3043 #[test]
3044 fn test_callback_uses_param_timestamp() {
3045 let mut drv = TestDriver::new();
3046 let mut rx = drv.base_mut().interrupts.subscribe_async();
3047
3048 let custom_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
3049 drv.base_mut().set_int32_param(0, 0, 77).unwrap();
3050 drv.base_mut().set_param_timestamp(0, 0, custom_ts).unwrap();
3051 drv.base_mut().call_param_callbacks(0).unwrap();
3052
3053 let v = rx.try_recv().unwrap();
3054 assert_eq!(v.reason, 0);
3055 assert_eq!(v.timestamp, custom_ts);
3056 }
3057
3058 #[test]
3059 fn test_default_read_write_enum() {
3060 use crate::param::EnumEntry;
3061
3062 let mut base = PortDriverBase::new("test_enum", 1, PortFlags::default());
3063 base.create_param("MODE", ParamType::Enum).unwrap();
3064
3065 struct EnumDriver {
3066 base: PortDriverBase,
3067 }
3068 impl PortDriver for EnumDriver {
3069 fn base(&self) -> &PortDriverBase {
3070 &self.base
3071 }
3072 fn base_mut(&mut self) -> &mut PortDriverBase {
3073 &mut self.base
3074 }
3075 }
3076
3077 let mut drv = EnumDriver { base };
3078 let choices: Arc<[EnumEntry]> = Arc::from(vec![
3079 EnumEntry {
3080 string: "Off".into(),
3081 value: 0,
3082 severity: 0,
3083 },
3084 EnumEntry {
3085 string: "On".into(),
3086 value: 1,
3087 severity: 0,
3088 },
3089 ]);
3090 let mut user = AsynUser::new(0);
3091 drv.write_enum_choices(&mut user, choices).unwrap();
3092 drv.write_enum(&mut user, 1).unwrap();
3093 let (idx, ch) = drv.read_enum(&AsynUser::new(0)).unwrap();
3094 assert_eq!(idx, 1);
3095 assert_eq!(ch[1].string, "On");
3096 }
3097
3098 #[test]
3099 fn test_enum_callback() {
3100 use crate::param::{EnumEntry, ParamValue};
3101
3102 // Post-`iocInit` precondition — see `TestDriver::new`.
3103 epics_libcom_rs::runtime::interrupt_accept::set_interrupts_accepted(true);
3104 let mut base = PortDriverBase::new("test_enum_cb", 1, PortFlags::default());
3105 base.create_param("MODE", ParamType::Enum).unwrap();
3106 let mut rx = base.interrupts.subscribe_async();
3107
3108 struct EnumDriver {
3109 base: PortDriverBase,
3110 }
3111 impl PortDriver for EnumDriver {
3112 fn base(&self) -> &PortDriverBase {
3113 &self.base
3114 }
3115 fn base_mut(&mut self) -> &mut PortDriverBase {
3116 &mut self.base
3117 }
3118 }
3119
3120 let mut drv = EnumDriver { base };
3121 let choices: Arc<[EnumEntry]> = Arc::from(vec![
3122 EnumEntry {
3123 string: "A".into(),
3124 value: 0,
3125 severity: 0,
3126 },
3127 EnumEntry {
3128 string: "B".into(),
3129 value: 1,
3130 severity: 0,
3131 },
3132 ]);
3133 drv.base_mut()
3134 .set_enum_choices_param(0, 0, choices)
3135 .unwrap();
3136 drv.base_mut().set_enum_index_param(0, 0, 1).unwrap();
3137 drv.base_mut().call_param_callbacks(0).unwrap();
3138
3139 let v = rx.try_recv().unwrap();
3140 assert_eq!(v.reason, 0);
3141 assert!(matches!(v.value, ParamValue::Enum { index: 1, .. }));
3142 }
3143
3144 /// `do_callbacks_enum` by parameter type: an `Enum` param keeps the table
3145 /// and fires it with its value; any other param has the table fired on the
3146 /// asynEnum interface and keeps its own value.
3147 #[test]
3148 fn do_callbacks_enum_by_param_type() {
3149 use crate::param::{EnumEntry, ParamValue};
3150
3151 epics_libcom_rs::runtime::interrupt_accept::set_interrupts_accepted(true);
3152 let mut base = PortDriverBase::new("test_cb_enum", 1, PortFlags::default());
3153 let owner = base.create_param("MODE", ParamType::Enum).unwrap();
3154 let plain = base.create_param("GAIN", ParamType::Int32).unwrap();
3155 base.set_int32_param(plain, 0, 7).unwrap();
3156 base.call_param_callbacks(0).unwrap();
3157 let mut rx = base.interrupts.subscribe_async();
3158 let choices: Arc<[EnumEntry]> = Arc::from(vec![EnumEntry {
3159 string: "A".into(),
3160 value: 3,
3161 severity: 0,
3162 }]);
3163
3164 base.do_callbacks_enum(owner, 0, choices.clone()).unwrap();
3165 let v = rx.try_recv().unwrap();
3166 assert_eq!((v.reason, v.iface), (owner, None));
3167 assert_eq!(base.get_enum_param(owner, 0).unwrap().1, choices);
3168
3169 base.do_callbacks_enum(plain, 0, choices.clone()).unwrap();
3170 let v = rx.try_recv().unwrap();
3171 assert_eq!((v.reason, v.iface), (plain, Some(InterfaceType::Enum)));
3172 assert!(matches!(v.value, ParamValue::Enum { choices: c, .. } if c == choices));
3173 assert_eq!(base.get_int32_param(plain, 0).unwrap(), 7);
3174 assert!(rx.try_recv().is_err());
3175 }
3176
3177 #[test]
3178 fn test_default_read_write_generic_pointer() {
3179 let mut base = PortDriverBase::new("test_gp", 1, PortFlags::default());
3180 base.create_param("PTR", ParamType::GenericPointer).unwrap();
3181
3182 struct GpDriver {
3183 base: PortDriverBase,
3184 }
3185 impl PortDriver for GpDriver {
3186 fn base(&self) -> &PortDriverBase {
3187 &self.base
3188 }
3189 fn base_mut(&mut self) -> &mut PortDriverBase {
3190 &mut self.base
3191 }
3192 }
3193
3194 let mut drv = GpDriver { base };
3195 let data: Arc<dyn std::any::Any + Send + Sync> = Arc::new(99i32);
3196 let mut user = AsynUser::new(0);
3197 drv.write_generic_pointer(&mut user, data).unwrap();
3198 let val = drv.read_generic_pointer(&AsynUser::new(0)).unwrap();
3199 assert_eq!(*val.downcast_ref::<i32>().unwrap(), 99);
3200 }
3201
3202 #[test]
3203 fn test_generic_pointer_callback() {
3204 use crate::param::ParamValue;
3205
3206 // Post-`iocInit` precondition — see `TestDriver::new`.
3207 epics_libcom_rs::runtime::interrupt_accept::set_interrupts_accepted(true);
3208 let mut base = PortDriverBase::new("test_gp_cb", 1, PortFlags::default());
3209 base.create_param("PTR", ParamType::GenericPointer).unwrap();
3210 let mut rx = base.interrupts.subscribe_async();
3211
3212 struct GpDriver {
3213 base: PortDriverBase,
3214 }
3215 impl PortDriver for GpDriver {
3216 fn base(&self) -> &PortDriverBase {
3217 &self.base
3218 }
3219 fn base_mut(&mut self) -> &mut PortDriverBase {
3220 &mut self.base
3221 }
3222 }
3223
3224 let mut drv = GpDriver { base };
3225 let data: Arc<dyn std::any::Any + Send + Sync> = Arc::new(vec![1, 2, 3]);
3226 drv.base_mut()
3227 .set_generic_pointer_param(0, 0, data)
3228 .unwrap();
3229 drv.base_mut().call_param_callbacks(0).unwrap();
3230
3231 let v = rx.try_recv().unwrap();
3232 assert_eq!(v.reason, 0);
3233 assert!(matches!(v.value, ParamValue::GenericPointer(_)));
3234 }
3235
3236 #[test]
3237 fn test_interpose_push_requires_lock() {
3238 use crate::interpose::{OctetInterpose, OctetNext, OctetReadResult};
3239 use parking_lot::Mutex;
3240 use std::sync::Arc;
3241
3242 struct NoopInterpose;
3243 impl OctetInterpose for NoopInterpose {
3244 fn read(
3245 &mut self,
3246 user: &AsynUser,
3247 buf: &mut [u8],
3248 next: &mut dyn OctetNext,
3249 ) -> AsynResult<OctetReadResult> {
3250 next.read(user, buf)
3251 }
3252 fn write(
3253 &mut self,
3254 user: &mut AsynUser,
3255 data: &[u8],
3256 next: &mut dyn OctetNext,
3257 ) -> AsynResult<usize> {
3258 next.write(user, data)
3259 }
3260 fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
3261 next.flush(user)
3262 }
3263 }
3264
3265 let port: Arc<Mutex<dyn PortDriver>> = Arc::new(Mutex::new(TestDriver::new()));
3266
3267 {
3268 let mut guard = port.lock();
3269 guard
3270 .base_mut()
3271 .install_octet_interpose(Box::new(NoopInterpose));
3272 assert_eq!(guard.base().interpose_octet.len(), 1);
3273 }
3274 }
3275
3276 /// The `set_input_eos` write owner must forward the terminator to an
3277 /// installed `EosInterpose`, not just cache it in `base.input_eos` —
3278 /// otherwise a runtime IEOS change never terminates reads (the F7 gap).
3279 #[test]
3280 fn test_set_input_eos_reaches_installed_interpose() {
3281 use crate::interpose::eos::EosInterpose;
3282 use crate::interpose::{EomReason, OctetNext, OctetReadResult};
3283
3284 struct RawSource {
3285 data: Vec<u8>,
3286 pos: usize,
3287 }
3288 impl OctetNext for RawSource {
3289 fn read(&mut self, _u: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
3290 let avail = self.data.len() - self.pos;
3291 let n = avail.min(buf.len());
3292 buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
3293 self.pos += n;
3294 Ok(OctetReadResult {
3295 nbytes_transferred: n,
3296 eom_reason: EomReason::CNT,
3297 })
3298 }
3299 fn write(&mut self, _u: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
3300 Ok(data.len())
3301 }
3302 fn flush(&mut self, _u: &mut AsynUser) -> AsynResult<()> {
3303 Ok(())
3304 }
3305 }
3306
3307 let mut drv = TestDriver::new();
3308 drv.base_mut()
3309 .install_octet_interpose(Box::new(EosInterpose::default()));
3310
3311 // Set IEOS through the driver trait: caches in base AND must reach
3312 // the interpose.
3313 drv.set_input_eos(&AsynUser::default(), b"\n").unwrap();
3314 assert_eq!(drv.base().input_eos(0), b"\n");
3315
3316 let user = AsynUser::default();
3317 // "ab\n" exactly: the EOS read returns "ab" and leaves no read-ahead
3318 // in the interpose buffer, so the cleared-EOS read below genuinely
3319 // reads the next source fresh.
3320 let mut src = RawSource {
3321 data: b"ab\n".to_vec(),
3322 pos: 0,
3323 };
3324 let mut buf = [0u8; 16];
3325 let r = drv
3326 .base_mut()
3327 .interpose_octet
3328 .dispatch_read(&user, &mut buf, &mut src)
3329 .unwrap();
3330 assert_eq!(&buf[..r.nbytes_transferred], b"ab");
3331 assert!(r.eom_reason.contains(EomReason::EOS));
3332
3333 // Clearing IEOS (binary-suppress path) must also reach the interpose:
3334 // the read then passes through with no EOS termination.
3335 drv.set_input_eos(&AsynUser::default(), b"").unwrap();
3336 assert_eq!(drv.base().input_eos(0), b"");
3337 let mut src2 = RawSource {
3338 data: b"xy\nz".to_vec(),
3339 pos: 0,
3340 };
3341 let mut buf2 = [0u8; 16];
3342 let r2 = drv
3343 .base_mut()
3344 .interpose_octet
3345 .dispatch_read(&user, &mut buf2, &mut src2)
3346 .unwrap();
3347 assert_eq!(&buf2[..r2.nbytes_transferred], b"xy\nz");
3348 assert!(!r2.eom_reason.contains(EomReason::EOS));
3349 }
3350
3351 /// R14-49: the EOS hooks take the `asynUser`, so a multi-device port holds
3352 /// one terminator per device — C's `eosPvt` is created per (port, addr)
3353 /// (asynInterposeEos.c:84-120) and every hook takes the user that selects it
3354 /// (:288-296). A port-wide pair could not answer two devices.
3355 #[test]
3356 fn each_device_on_a_multi_device_port_holds_its_own_eos() {
3357 struct MultiDriver {
3358 base: PortDriverBase,
3359 }
3360 impl PortDriver for MultiDriver {
3361 fn base(&self) -> &PortDriverBase {
3362 &self.base
3363 }
3364 fn base_mut(&mut self) -> &mut PortDriverBase {
3365 &mut self.base
3366 }
3367
3368 // A multiDevice port cannot carry `asynInterposeEos` — C refuses
3369 // to install it (asynOctetBase.c:149-153) — so such a driver owns
3370 // its EOS methods, which is the non-NULL `asynOctet` entry the
3371 // Fail-stub default stands in for (R18-71).
3372 fn set_input_eos(&mut self, user: &AsynUser, eos: &[u8]) -> AsynResult<()> {
3373 self.base.set_input_eos(user.addr, eos);
3374 Ok(())
3375 }
3376 fn set_output_eos(&mut self, user: &AsynUser, eos: &[u8]) -> AsynResult<()> {
3377 self.base.set_output_eos(user.addr, eos);
3378 Ok(())
3379 }
3380 }
3381 let mut drv = MultiDriver {
3382 base: PortDriverBase::new(
3383 "eos_multi",
3384 4,
3385 PortFlags {
3386 multi_device: true,
3387 ..PortFlags::default()
3388 },
3389 ),
3390 };
3391
3392 let dev1 = AsynUser::default().with_addr(1);
3393 let dev2 = AsynUser::default().with_addr(2);
3394 drv.set_input_eos(&dev1, b"\n").unwrap();
3395 drv.set_output_eos(&dev1, b"\r\n").unwrap();
3396 drv.set_input_eos(&dev2, b";").unwrap();
3397
3398 assert_eq!(drv.get_input_eos(&dev1), b"\n");
3399 assert_eq!(drv.get_input_eos(&dev2), b";");
3400 assert_eq!(drv.get_output_eos(&dev1), b"\r\n");
3401 // A device that was never configured has no terminator — C's
3402 // zero-initialised `eosInLen`.
3403 assert!(
3404 drv.get_input_eos(&AsynUser::default().with_addr(3))
3405 .is_empty()
3406 );
3407 assert!(drv.get_output_eos(&dev2).is_empty());
3408 }
3409
3410 /// The other boundary: a port that never declared `ASYN_MULTIDEVICE` has no
3411 /// devices to key by — C's `findDpCommon` (asynManager.c:536-544) and
3412 /// `findInterface` resolve *every* addr to the port itself, so
3413 /// `asynSetEos(port, -1, ...)` and a record at ADDR 0 must reach the same
3414 /// terminator. Splitting them by raw addr would leave the record reading
3415 /// with no EOS at all.
3416 #[test]
3417 fn a_single_device_port_collapses_every_addr_onto_one_eos() {
3418 let mut drv = TestDriver::new();
3419 // A single-device port takes its EOS from `asynInterposeEos`, which C
3420 // installs above the driver when the port is configured with
3421 // `processEosIn/Out` (asynOctetBase.c:170-172); without it the
3422 // driver's own NULL method answers "not implemented" (R18-71).
3423 drv.base
3424 .install_octet_interpose(Box::new(crate::interpose::eos::EosInterpose::default()));
3425 drv.set_input_eos(&AsynUser::default().with_addr(-1), b"\n")
3426 .unwrap();
3427 assert_eq!(drv.get_input_eos(&AsynUser::default().with_addr(0)), b"\n");
3428 assert_eq!(drv.get_input_eos(&AsynUser::default().with_addr(7)), b"\n");
3429 }
3430
3431 /// R18-71. C's `asynOctetBase::initOverride` (asynOctetBase.c:115-118)
3432 /// replaces a NULL `setInputEos`/`setOutputEos` with a Fail stub, and the
3433 /// stub calls `showFailure` (:370-380) — `asynError` and
3434 /// `"<port> <method> not implemented"`. A `noProcessEos` port therefore
3435 /// *refuses* a terminator; it does not accept one and then never
3436 /// terminate. The two boundaries are the whole rule: no layer at all, and
3437 /// a layer that takes only one direction (`asynInterposeEosConfig` stores
3438 /// both flags, asynInterposeEos.c:128-138). C delegates only the *input*
3439 /// direction down when `processEosIn == 0` — setInputEos :293-295,
3440 /// getInputEos :318-320; its output pair (:344-363, :365-390) has no
3441 /// `processEosOut` test and stores unconditionally, that flag gating
3442 /// `writeIt` alone (:161-163).
3443 #[test]
3444 fn a_port_with_no_eos_layer_refuses_a_terminator() {
3445 use crate::interpose::eos::{EosConfig, EosInterpose};
3446
3447 // Boundary 1: no layer took it — both directions are Fail stubs.
3448 let mut bare = TestDriver::new();
3449 let err = bare
3450 .set_input_eos(&AsynUser::default(), b"\n")
3451 .expect_err("a noProcessEos port must refuse IEOS");
3452 assert_eq!(
3453 err.to_string(),
3454 AsynError::Status {
3455 status: AsynStatus::Error,
3456 message: "test setInputEos not implemented".to_string(),
3457 }
3458 .to_string()
3459 );
3460 let err = bare
3461 .set_output_eos(&AsynUser::default(), b"\r\n")
3462 .expect_err("a noProcessEos port must refuse OEOS");
3463 assert!(
3464 err.to_string().contains("setOutputEos not implemented"),
3465 "{err}"
3466 );
3467 // Refused means nothing was cached: a later read must not silently
3468 // believe the port terminates.
3469 assert!(bare.get_input_eos(&AsynUser::default()).is_empty());
3470 assert!(bare.get_output_eos(&AsynUser::default()).is_empty());
3471
3472 // Boundary 2: the layer takes input only — and OEOS still lands.
3473 // C's `setOutputEos` (:344-363) has no `processEosOut` test; the flag
3474 // gates `writeIt` alone (:161-163), so the terminator is stored, read
3475 // back, and simply never appended.
3476 let mut half = TestDriver::new();
3477 half.base_mut()
3478 .install_octet_interpose(Box::new(EosInterpose::with_processing(
3479 EosConfig::default(),
3480 true,
3481 false,
3482 )));
3483 half.set_input_eos(&AsynUser::default(), b"\n").unwrap();
3484 assert_eq!(half.get_input_eos(&AsynUser::default()), b"\n");
3485 half.set_output_eos(&AsynUser::default(), b"\r\n")
3486 .expect("processOut = 0 still stores OEOS");
3487 assert_eq!(half.get_output_eos(&AsynUser::default()), b"\r\n");
3488
3489 // Boundary 3: a length the storing layer refuses. C's `switch
3490 // (eoslen)` answers `"<port> illegal eoslen <n>"` *before* assigning
3491 // (:299-310, :352-362), so the terminator already stored still stands.
3492 let err = half
3493 .set_output_eos(&AsynUser::default(), b"\r\n\0")
3494 .expect_err("eoslen 3 is past eosOut[2]");
3495 assert!(err.to_string().contains("test illegal eoslen 3"), "{err}");
3496 assert_eq!(half.get_output_eos(&AsynUser::default()), b"\r\n");
3497 let err = half
3498 .set_input_eos(&AsynUser::default(), b"abc")
3499 .expect_err("eoslen 3 is past eosIn[2]");
3500 assert!(err.to_string().contains("test illegal eoslen 3"), "{err}");
3501 assert_eq!(half.get_input_eos(&AsynUser::default()), b"\n");
3502
3503 // Boundary 4: no layer at all refuses every length with the Fail
3504 // stub's words, not the length rule's — the rule belongs to the layer
3505 // that stores, and there is none.
3506 let err = bare
3507 .set_output_eos(&AsynUser::default(), b"\r\n\0")
3508 .expect_err("a noProcessEos port refuses whatever the length");
3509 assert!(
3510 err.to_string().contains("setOutputEos not implemented"),
3511 "{err}"
3512 );
3513 }
3514
3515 /// R6-46 owner path: `set_connected` is the single transition owner, so
3516 /// every driver that reconnects through it (serial, IP, prologix …) gets
3517 /// the interpose reset for free. C wires this as an exception callback
3518 /// (`asynInterposeEos.c:110,142-151`); here the owner drives the stack
3519 /// directly. Boundaries: both edges reset (C's `asynExceptionConnect`
3520 /// fires from `exceptionConnect` AND `exceptionDisconnect`), and a
3521 /// no-op call (same state) must not.
3522 #[test]
3523 fn set_connected_resets_interpose_link_state() {
3524 use crate::interpose::{OctetInterpose, OctetNext, OctetReadResult};
3525 use std::sync::Arc;
3526 use std::sync::atomic::{AtomicUsize, Ordering};
3527
3528 struct CountingInterpose(Arc<AtomicUsize>);
3529 impl OctetInterpose for CountingInterpose {
3530 fn read(
3531 &mut self,
3532 user: &AsynUser,
3533 buf: &mut [u8],
3534 next: &mut dyn OctetNext,
3535 ) -> AsynResult<OctetReadResult> {
3536 next.read(user, buf)
3537 }
3538 fn write(
3539 &mut self,
3540 user: &mut AsynUser,
3541 data: &[u8],
3542 next: &mut dyn OctetNext,
3543 ) -> AsynResult<usize> {
3544 next.write(user, data)
3545 }
3546 fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
3547 next.flush(user)
3548 }
3549 fn connection_changed(&mut self) {
3550 self.0.fetch_add(1, Ordering::Relaxed);
3551 }
3552 }
3553
3554 let resets = Arc::new(AtomicUsize::new(0));
3555 let mut base = PortDriverBase::new("reset_test", 1, PortFlags::default());
3556 base.install_octet_interpose(Box::new(CountingInterpose(resets.clone())));
3557
3558 // Port starts connected. Disconnect edge → reset (C exceptionDisconnect).
3559 assert!(base.set_connected(false));
3560 assert_eq!(resets.load(Ordering::Relaxed), 1);
3561
3562 // Redundant call, no state change → no fan-out, no reset.
3563 assert!(!base.set_connected(false));
3564 assert_eq!(resets.load(Ordering::Relaxed), 1);
3565
3566 // Reconnect edge → reset again (C exceptionConnect).
3567 assert!(base.set_connected(true));
3568 assert_eq!(resets.load(Ordering::Relaxed), 2);
3569 }
3570
3571 #[test]
3572 fn test_default_read_write_int64() {
3573 let mut base = PortDriverBase::new("test_i64", 1, PortFlags::default());
3574 base.create_param("BIG", ParamType::Int64).unwrap();
3575
3576 struct I64Driver {
3577 base: PortDriverBase,
3578 }
3579 impl PortDriver for I64Driver {
3580 fn base(&self) -> &PortDriverBase {
3581 &self.base
3582 }
3583 fn base_mut(&mut self) -> &mut PortDriverBase {
3584 &mut self.base
3585 }
3586 }
3587
3588 let mut drv = I64Driver { base };
3589 let mut user = AsynUser::new(0);
3590 drv.write_int64(&mut user, i64::MAX).unwrap();
3591 assert_eq!(drv.read_int64(&AsynUser::new(0)).unwrap(), i64::MAX);
3592 }
3593
3594 #[test]
3595 fn test_get_bounds_int64_default() {
3596 let base = PortDriverBase::new("test_bounds", 1, PortFlags::default());
3597 struct BoundsDriver {
3598 base: PortDriverBase,
3599 }
3600 impl PortDriver for BoundsDriver {
3601 fn base(&self) -> &PortDriverBase {
3602 &self.base
3603 }
3604 fn base_mut(&mut self) -> &mut PortDriverBase {
3605 &mut self.base
3606 }
3607 }
3608 let drv = BoundsDriver { base };
3609 let (lo, hi) = drv.get_bounds_int64(&AsynUser::default()).unwrap();
3610 // C asynInt64Base.c:99 default: *low = *high = 0 (so a driver
3611 // that does not implement getBounds skips LINEAR ESLO/EOFF).
3612 assert_eq!(lo, 0);
3613 assert_eq!(hi, 0);
3614 }
3615
3616 #[test]
3617 fn test_per_addr_device_state() {
3618 let mut base = PortDriverBase::new(
3619 "multi",
3620 4,
3621 PortFlags {
3622 multi_device: true,
3623 can_block: false,
3624 destructible: true,
3625 },
3626 );
3627 base.create_param("V", ParamType::Int32).unwrap();
3628
3629 // Default: all connected
3630 assert!(base.is_device_connected(0));
3631 assert!(base.is_device_connected(1));
3632
3633 // Disable addr 1
3634 base.device_state(1).enabled = false;
3635 assert!(base.check_ready_addr(0).is_ok());
3636 let err = base.check_ready_addr(1).unwrap_err();
3637 assert!(format!("{err}").contains("not enabled"));
3638
3639 // Disconnect addr 2
3640 base.device_state(2).connected = false;
3641 let err = base.check_ready_addr(2).unwrap_err();
3642 assert!(format!("{err}").contains("not connected"));
3643 }
3644
3645 #[test]
3646 fn test_per_addr_single_device_ignored() {
3647 let mut base = PortDriverBase::new("single", 1, PortFlags::default());
3648 base.create_param("V", ParamType::Int32).unwrap();
3649 // For single-device, per-addr check passes even if no device state
3650 assert!(base.check_ready_addr(0).is_ok());
3651 }
3652
3653 #[test]
3654 fn test_timestamp_source() {
3655 let mut base = PortDriverBase::new("ts_test", 1, PortFlags::default());
3656 base.create_param("V", ParamType::Int32).unwrap();
3657
3658 let fixed_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(999999);
3659 base.register_timestamp_source(move || fixed_ts);
3660
3661 assert_eq!(base.current_timestamp(), fixed_ts);
3662 }
3663
3664 #[test]
3665 fn test_timestamp_source_in_callbacks() {
3666 // Post-`iocInit` precondition — see `TestDriver::new`.
3667 epics_libcom_rs::runtime::interrupt_accept::set_interrupts_accepted(true);
3668 let mut base = PortDriverBase::new("ts_cb", 1, PortFlags::default());
3669 base.create_param("V", ParamType::Int32).unwrap();
3670 let mut rx = base.interrupts.subscribe_async();
3671
3672 let fixed_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(123456);
3673 base.register_timestamp_source(move || fixed_ts);
3674
3675 struct TsDriver {
3676 base: PortDriverBase,
3677 }
3678 impl PortDriver for TsDriver {
3679 fn base(&self) -> &PortDriverBase {
3680 &self.base
3681 }
3682 fn base_mut(&mut self) -> &mut PortDriverBase {
3683 &mut self.base
3684 }
3685 }
3686 let mut drv = TsDriver { base };
3687 drv.base_mut().set_int32_param(0, 0, 42).unwrap();
3688 drv.base_mut().call_param_callbacks(0).unwrap();
3689
3690 let v = rx.try_recv().unwrap();
3691 // Should use fixed_ts since no per-param timestamp is set
3692 assert_eq!(v.timestamp, fixed_ts);
3693 }
3694
3695 #[test]
3696 fn test_queue_priority_connect() {
3697 assert!(QueuePriority::Connect > QueuePriority::High);
3698 }
3699
3700 #[test]
3701 fn test_port_flags_destructible_default_is_opt_in() {
3702 // C asyn parity: ASYN_DESTRUCTIBLE (0x0004, asynDriver.h:97) is
3703 // a `registerPort` attribute that callers opt into. Default
3704 // must be false so drivers don't accidentally accept a
3705 // shutdownPort call. PortDriver authors that want shutdown
3706 // support set `destructible: true` explicitly.
3707 let flags = PortFlags::default();
3708 assert!(
3709 !flags.destructible,
3710 "destructible must be opt-in (C parity)"
3711 );
3712 }
3713
3714 #[test]
3715 fn shutdown_lifecycle_refuses_non_destructible() {
3716 let mut base = PortDriverBase::new(
3717 "p_nondestr",
3718 1,
3719 PortFlags {
3720 multi_device: false,
3721 can_block: false,
3722 destructible: false,
3723 },
3724 );
3725 match base.shutdown_lifecycle() {
3726 Err(AsynError::Status { message, .. }) => {
3727 assert!(message.contains("ASYN_DESTRUCTIBLE"), "msg={message}");
3728 }
3729 other => panic!("expected ASYN_DESTRUCTIBLE refusal, got {other:?}"),
3730 }
3731 assert!(
3732 !base.is_defunct(),
3733 "non-destructible port must not flip defunct"
3734 );
3735 assert!(base.is_enabled(), "non-destructible port must stay enabled");
3736 }
3737
3738 #[test]
3739 fn shutdown_lifecycle_marks_destructible_defunct_and_idempotent() {
3740 let mut base = PortDriverBase::new(
3741 "p_destr",
3742 1,
3743 PortFlags {
3744 multi_device: false,
3745 can_block: false,
3746 destructible: true,
3747 },
3748 );
3749 assert!(base.is_enabled());
3750 assert!(!base.is_defunct());
3751 base.shutdown_lifecycle().unwrap();
3752 assert!(
3753 !base.is_enabled(),
3754 "shutdown_lifecycle must flip enabled=false"
3755 );
3756 assert!(
3757 base.is_defunct(),
3758 "shutdown_lifecycle must flip defunct=true"
3759 );
3760 // Idempotent — second call is Ok and leaves state unchanged.
3761 base.shutdown_lifecycle().unwrap();
3762 assert!(base.is_defunct());
3763 // A shut-down port is refused by the queue gate as a *disabled* one:
3764 // C's `queueRequest` has no defunct branch (asynManager.c:1541-1552),
3765 // it only sees the `enabled=FALSE` that `shutdownPort` left behind
3766 // (:2283). "port %s disabled" is the message an operator gets.
3767 match base.check_ready() {
3768 Err(AsynError::Status { status, message }) => {
3769 assert_eq!(status, AsynStatus::Disabled);
3770 assert_eq!(message, "port p_destr disabled");
3771 }
3772 other => panic!("expected the disabled refusal, got {other:?}"),
3773 }
3774 // The one place C names the shutdown is `enable` (:2236-2241).
3775 match base.set_enabled(true) {
3776 Err(AsynError::Status { status, message }) => {
3777 assert_eq!(status, AsynStatus::Disabled);
3778 assert_eq!(message, "asynManager:enable: port has been shut down");
3779 }
3780 other => panic!("expected the shut-down refusal, got {other:?}"),
3781 }
3782 }
3783
3784 // --- Phase 2B: per-addr connect/disconnect/enable/disable ---
3785
3786 #[test]
3787 fn test_connect_addr() {
3788 let mut base = PortDriverBase::new(
3789 "multi_conn",
3790 4,
3791 PortFlags {
3792 multi_device: true,
3793 can_block: false,
3794 destructible: true,
3795 },
3796 );
3797 base.create_param("V", ParamType::Int32).unwrap();
3798
3799 base.disconnect_addr(1);
3800 assert!(!base.is_device_connected(1));
3801 assert!(base.check_ready_addr(1).is_err());
3802
3803 base.connect_addr(1);
3804 assert!(base.is_device_connected(1));
3805 assert!(base.check_ready_addr(1).is_ok());
3806 }
3807
3808 #[test]
3809 fn test_enable_disable_addr() {
3810 let mut base = PortDriverBase::new(
3811 "multi_en",
3812 4,
3813 PortFlags {
3814 multi_device: true,
3815 can_block: false,
3816 destructible: true,
3817 },
3818 );
3819 base.create_param("V", ParamType::Int32).unwrap();
3820
3821 base.disable_addr(2);
3822 let err = base.check_ready_addr(2).unwrap_err();
3823 assert!(format!("{err}").contains("not enabled"));
3824
3825 base.enable_addr(2);
3826 assert!(base.check_ready_addr(2).is_ok());
3827 }
3828
3829 #[test]
3830 fn test_port_level_overrides_addr() {
3831 let mut base = PortDriverBase::new(
3832 "multi_override",
3833 4,
3834 PortFlags {
3835 multi_device: true,
3836 can_block: false,
3837 destructible: true,
3838 },
3839 );
3840 base.create_param("V", ParamType::Int32).unwrap();
3841
3842 // Port-level disabled overrides addr-level enabled
3843 base.enabled = false;
3844 base.enable_addr(0); // addr 0 is enabled, but port is disabled
3845 let err = base.check_ready_addr(0).unwrap_err();
3846 assert!(format!("{err}").contains("disabled"));
3847 }
3848
3849 #[test]
3850 fn test_per_addr_exception_announced() {
3851 use std::sync::atomic::{AtomicI32, Ordering};
3852
3853 let mut base = PortDriverBase::new(
3854 "multi_exc",
3855 4,
3856 PortFlags {
3857 multi_device: true,
3858 can_block: false,
3859 destructible: true,
3860 },
3861 );
3862 base.create_param("V", ParamType::Int32).unwrap();
3863
3864 let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
3865 base.bind_exception_sink(exc_mgr.clone());
3866
3867 let last_addr = Arc::new(AtomicI32::new(-99));
3868 let last_addr2 = last_addr.clone();
3869 exc_mgr.add_callback(move |event| {
3870 last_addr2.store(event.addr, Ordering::Relaxed);
3871 });
3872
3873 base.disconnect_addr(3);
3874 assert_eq!(last_addr.load(Ordering::Relaxed), 3);
3875
3876 base.enable_addr(2);
3877 assert_eq!(last_addr.load(Ordering::Relaxed), 2);
3878 }
3879
3880 /// C parity (asynManager.c:2151-2160 exceptionConnect,
3881 /// :2174-2185 exceptionDisconnect): redundant connect/disconnect
3882 /// on a port already in that state must NOT fan out a duplicate
3883 /// `asynExceptionConnect`. Subscribers depend on the event
3884 /// edge — duplicate fan-out causes them to e.g. re-subscribe or
3885 /// re-arm timers that should fire exactly once per transition.
3886 #[test]
3887 fn test_connect_disconnect_announce_only_on_transition() {
3888 use std::sync::atomic::{AtomicUsize, Ordering};
3889
3890 let mut base = PortDriverBase::new(
3891 "edge",
3892 4,
3893 PortFlags {
3894 multi_device: true,
3895 can_block: false,
3896 destructible: true,
3897 },
3898 );
3899 base.create_param("V", ParamType::Int32).unwrap();
3900 let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
3901 base.bind_exception_sink(exc_mgr.clone());
3902
3903 let connect_hits = Arc::new(AtomicUsize::new(0));
3904 let hits2 = connect_hits.clone();
3905 exc_mgr.add_callback(move |event| {
3906 if event.exception == AsynException::Connect {
3907 hits2.fetch_add(1, Ordering::Relaxed);
3908 }
3909 });
3910
3911 // device starts connected by DeviceState::default — a redundant
3912 // connect_addr is a no-op.
3913 base.connect_addr(2);
3914 assert_eq!(
3915 connect_hits.load(Ordering::Relaxed),
3916 0,
3917 "redundant connect_addr must not fan out"
3918 );
3919
3920 // First transition fires once.
3921 base.disconnect_addr(2);
3922 assert_eq!(connect_hits.load(Ordering::Relaxed), 1);
3923
3924 // Redundant disconnect is silent.
3925 base.disconnect_addr(2);
3926 assert_eq!(
3927 connect_hits.load(Ordering::Relaxed),
3928 1,
3929 "redundant disconnect_addr must not fan out"
3930 );
3931
3932 // Re-connect fires the transition.
3933 base.connect_addr(2);
3934 assert_eq!(connect_hits.load(Ordering::Relaxed), 2);
3935 }
3936
3937 /// C parity: `autoConnectAsyn` (asynManager.c:2310-2324) fires
3938 /// `asynExceptionAutoConnect` unconditionally — even setting the
3939 /// same value as the current one. Rust mirrors that so observers
3940 /// can refresh their UI after a re-confirmation, not just an edge.
3941 #[test]
3942 fn test_set_auto_connect_fires_unconditionally() {
3943 use std::sync::atomic::{AtomicUsize, Ordering};
3944
3945 let mut base = PortDriverBase::new("ac", 1, PortFlags::default());
3946 let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
3947 base.bind_exception_sink(exc_mgr.clone());
3948 let hits = Arc::new(AtomicUsize::new(0));
3949 let hits2 = hits.clone();
3950 exc_mgr.add_callback(move |event| {
3951 if event.exception == AsynException::AutoConnect {
3952 hits2.fetch_add(1, Ordering::Relaxed);
3953 }
3954 });
3955 // base.auto_connect defaults to true — setting true again
3956 // still must fire (no state-change guard in C).
3957 base.set_auto_connect(true);
3958 base.set_auto_connect(false);
3959 base.set_auto_connect(false);
3960 assert_eq!(hits.load(Ordering::Relaxed), 3);
3961 }
3962
3963 /// asyn PR #217 (asynManager.c:2322-2324): enabling auto-connect on a
3964 /// down port/device arms the connect timer; a connected port, or a
3965 /// flip to OFF, arms nothing. Pre-fix no flip armed the timer, so a
3966 /// never-connected port enabled late made one exception-wake attempt
3967 /// and then sat silent.
3968 #[test]
3969 fn auto_connect_enable_arms_connect_timer_boundaries() {
3970 // dropped while auto-connect was OFF (the disconnect edge arms
3971 // nothing then, port.rs sync_connection_edge), enabled late →
3972 // the flip itself must arm
3973 let mut base = PortDriverBase::new("act", 1, PortFlags::default());
3974 base.set_auto_connect(false);
3975 base.set_connected(false);
3976 assert!(base.connect_retry_at.is_none());
3977 base.set_auto_connect(true);
3978 assert!(base.connect_retry_at.is_some());
3979
3980 // down + OFF → untouched (C's timer callback no-ops on
3981 // !autoConnect; the flip itself must not arm)
3982 let mut base = PortDriverBase::new("act2", 1, PortFlags::default());
3983 base.set_auto_connect(false);
3984 base.set_connected(false);
3985 base.set_auto_connect(false);
3986 assert!(base.connect_retry_at.is_none());
3987
3988 // connected + ON → nothing to retry (a fresh base is born
3989 // `Connection::Own(true)`)
3990 let mut base = PortDriverBase::new("act3", 1, PortFlags::default());
3991 base.set_auto_connect(true);
3992 assert!(base.connect_retry_at.is_none());
3993
3994 // device down + ON via the addr variant → the PORT timer arms,
3995 // keyed on the device's dpCommon exactly as C's findDpCommon
3996 // resolution is; the port itself stays connected
3997 let multi = PortFlags {
3998 multi_device: true,
3999 ..PortFlags::default()
4000 };
4001 let mut base = PortDriverBase::new("act4", 2, multi);
4002 base.device_state(1).connected = false;
4003 assert!(base.connect_retry_at.is_none());
4004 base.set_auto_connect_addr(1, true);
4005 assert!(base.connect_retry_at.is_some());
4006
4007 // the same call on a SINGLE-device port is a port write, because
4008 // `findDpCommon` has no device to give it (asynManager.c:541-544): the
4009 // port is connected, so it arms nothing at all.
4010 let mut base = PortDriverBase::new("act5", 2, PortFlags::default());
4011 base.set_auto_connect_addr(1, true);
4012 assert!(base.connect_retry_at.is_none());
4013 assert!(
4014 base.dp_auto_connect(1),
4015 "and it landed on the port's own slot"
4016 );
4017 }
4018
4019 #[test]
4020 fn auto_connect_throttle_gate_boundaries() {
4021 // C autoConnectDevice 2.0s gate (asynManager.c:712-713). Boundary
4022 // cases, not narrative: never-stamped, exactly-2s, just-under-2s.
4023 let mut base = PortDriverBase::new("thr", 1, PortFlags::default());
4024
4025 // No transition recorded yet => always permitted (C's
4026 // zero-initialised lastConnectDisconnect).
4027 let t0 = Instant::now();
4028 assert!(base.auto_connect_throttle_ok(-1, t0));
4029
4030 // Stamp at t0; only `+` arithmetic on Instant (no `- Duration`,
4031 // which panics on Windows when uptime < the span).
4032 base.last_connect_disconnect = Some(t0);
4033 // elapsed 0 < 2s => refused.
4034 assert!(!base.auto_connect_throttle_ok(-1, t0));
4035 // elapsed just under 2s => refused.
4036 assert!(!base.auto_connect_throttle_ok(-1, t0 + Duration::from_millis(1999)));
4037 // elapsed exactly 2s => permitted (>=).
4038 assert!(base.auto_connect_throttle_ok(-1, t0 + Duration::from_secs(2)));
4039 // elapsed well past => permitted.
4040 assert!(base.auto_connect_throttle_ok(-1, t0 + Duration::from_secs(5)));
4041 }
4042
4043 #[test]
4044 fn auto_connect_throttle_stamps_on_disconnect_not_connect() {
4045 // C exceptionDisconnect stamps lastConnectDisconnect (asynManager.c
4046 // :2184); exceptionConnect does not (:2157-2159). Mirror both edges.
4047 let mut base = PortDriverBase::new("thr", 1, PortFlags::default());
4048 // Starts connected, no stamp.
4049 assert!(base.last_connect_disconnect.is_none());
4050
4051 // Disconnect edge stamps.
4052 assert!(base.set_connected(false));
4053 assert!(base.last_connect_disconnect.is_some());
4054
4055 // Clear, then connect edge must NOT re-stamp.
4056 base.last_connect_disconnect = None;
4057 assert!(base.set_connected(true));
4058 assert!(base.last_connect_disconnect.is_none());
4059 }
4060
4061 #[test]
4062 fn auto_connect_throttle_per_device_anchor() {
4063 // Multi-device ports throttle per address (C dpCommon is per-device).
4064 let flags = PortFlags {
4065 multi_device: true,
4066 ..PortFlags::default()
4067 };
4068 let mut base = PortDriverBase::new("thr", 4, flags);
4069 let t0 = Instant::now();
4070
4071 // addr 1 disconnect stamps only addr 1's anchor.
4072 assert!(base.set_addr_connected(1, false));
4073 assert!(base.device_state(1).last_connect_disconnect.is_some());
4074 // addr 1 is throttled; addr 2 (never stamped) is still permitted.
4075 assert!(!base.auto_connect_throttle_ok(1, t0));
4076 assert!(base.auto_connect_throttle_ok(2, t0));
4077
4078 // Post-attempt stamp restarts addr 2's window.
4079 base.stamp_auto_connect_attempt(2, t0);
4080 assert!(!base.auto_connect_throttle_ok(2, t0));
4081 assert!(base.auto_connect_throttle_ok(2, t0 + Duration::from_secs(2)));
4082 }
4083
4084 #[test]
4085 fn set_enabled_refuses_defunct_port() {
4086 use std::sync::atomic::{AtomicUsize, Ordering};
4087 // C `enable` on a defunct port: asynDisabled, no `enabled` toggle,
4088 // no asynExceptionEnable fan-out (asynManager.c:2236-2241).
4089 let flags = PortFlags {
4090 destructible: true,
4091 ..PortFlags::default()
4092 };
4093 let mut base = PortDriverBase::new("def", 1, flags);
4094 let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
4095 base.bind_exception_sink(exc_mgr.clone());
4096 let enable_hits = Arc::new(AtomicUsize::new(0));
4097 let h = enable_hits.clone();
4098 exc_mgr.add_callback(move |event| {
4099 if event.exception == AsynException::Enable {
4100 h.fetch_add(1, Ordering::Relaxed);
4101 }
4102 });
4103
4104 // Shut the port down → defunct (shutdown sets enabled=false).
4105 base.shutdown_lifecycle().unwrap();
4106 assert!(base.is_defunct());
4107 assert!(!base.is_enabled());
4108
4109 let err = base.set_enabled(true).unwrap_err();
4110 match err {
4111 AsynError::Status { status, .. } => assert_eq!(status, AsynStatus::Disabled),
4112 other => panic!("expected Disabled, got {other:?}"),
4113 }
4114 assert!(!base.is_enabled(), "defunct port must not re-enable");
4115 assert_eq!(
4116 enable_hits.load(Ordering::Relaxed),
4117 0,
4118 "no Enable exception may fire on a defunct port"
4119 );
4120 }
4121
4122 #[test]
4123 fn set_addr_enabled_refuses_defunct_port() {
4124 use std::sync::atomic::{AtomicUsize, Ordering};
4125 let flags = PortFlags {
4126 multi_device: true,
4127 destructible: true,
4128 ..PortFlags::default()
4129 };
4130 let mut base = PortDriverBase::new("def", 4, flags);
4131 let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
4132 base.bind_exception_sink(exc_mgr.clone());
4133 let enable_hits = Arc::new(AtomicUsize::new(0));
4134 let h = enable_hits.clone();
4135 exc_mgr.add_callback(move |event| {
4136 if event.exception == AsynException::Enable {
4137 h.fetch_add(1, Ordering::Relaxed);
4138 }
4139 });
4140
4141 base.shutdown_lifecycle().unwrap();
4142
4143 let err = base.set_addr_enabled(1, false).unwrap_err();
4144 match err {
4145 AsynError::Status { status, .. } => assert_eq!(status, AsynStatus::Disabled),
4146 other => panic!("expected Disabled, got {other:?}"),
4147 }
4148 // The guard returns before `device_state(addr)` would insert an
4149 // entry, so the refused call mutates no per-device state.
4150 assert!(
4151 !base.device_states.contains_key(&1),
4152 "refused per-device enable must not create device state"
4153 );
4154 // The `()` convenience facade also no-ops on a defunct port.
4155 base.disable_addr(1);
4156 assert!(!base.device_states.contains_key(&1));
4157 assert_eq!(
4158 enable_hits.load(Ordering::Relaxed),
4159 0,
4160 "no Enable exception may fire on a defunct port"
4161 );
4162 }
4163
4164 /// The `defunct ⟹ !enabled` invariant is over every dpCommon the port
4165 /// owns, so `dp_enabled` must answer the same at every address. The two
4166 /// boundaries are the two ways a device slot can be in the map at
4167 /// shutdown time: one that already existed, and one created afterwards.
4168 #[test]
4169 fn shutdown_lifecycle_disables_every_device_dp_common() {
4170 let flags = PortFlags {
4171 multi_device: true,
4172 destructible: true,
4173 ..PortFlags::default()
4174 };
4175
4176 // Boundary 1: a slot that exists before the shutdown.
4177 let mut base = PortDriverBase::new("shut", 4, flags);
4178 base.set_addr_enabled(1, true).unwrap();
4179 assert!(base.device_states.contains_key(&1));
4180 base.shutdown_lifecycle().unwrap();
4181 assert!(!base.dp_enabled(1), "pre-existing device slot must go down");
4182 assert!(!base.dp_enabled(-1), "the port's own dpCommon goes down");
4183 assert!(!base.dp_enabled(0));
4184
4185 // Boundary 2: no slot at shutdown, one created afterwards — the seed
4186 // in `device_state` takes the port's `enabled`, which is now false.
4187 let mut base = PortDriverBase::new("shut2", 4, flags);
4188 base.shutdown_lifecycle().unwrap();
4189 assert!(!base.dp_enabled(1));
4190 assert!(
4191 !base.device_state(1).enabled,
4192 "a slot seeded after the shutdown must come up disabled"
4193 );
4194 }
4195
4196 /// C parity: `asynPortDriver::setInterruptUInt32Digital` /
4197 /// `clearInterruptUInt32Digital` / `getInterruptUInt32Digital`
4198 /// (`asynPortDriver.cpp:2346-2461`) route through paramList. The
4199 /// PortDriver trait default delegates to the param store; we
4200 /// verify the round-trip end-to-end through the trait surface.
4201 #[test]
4202 fn test_port_driver_uint32_interrupt_round_trip() {
4203 struct UInt32Drv {
4204 base: PortDriverBase,
4205 }
4206 impl PortDriver for UInt32Drv {
4207 fn base(&self) -> &PortDriverBase {
4208 &self.base
4209 }
4210 fn base_mut(&mut self) -> &mut PortDriverBase {
4211 &mut self.base
4212 }
4213 }
4214
4215 let mut base = PortDriverBase::new("uint32_int", 1, PortFlags::default());
4216 let idx = base
4217 .params
4218 .create_param("BITS", ParamType::UInt32Digital)
4219 .unwrap();
4220 let mut drv = UInt32Drv { base };
4221 let user = AsynUser::new(idx).with_addr(0);
4222
4223 drv.set_interrupt_uint32_digital(&user, 0xF0, InterruptReason::ZeroToOne)
4224 .unwrap();
4225 drv.set_interrupt_uint32_digital(&user, 0x0F, InterruptReason::OneToZero)
4226 .unwrap();
4227 assert_eq!(
4228 drv.get_interrupt_uint32_digital(&user, InterruptReason::Both)
4229 .unwrap(),
4230 0xFF
4231 );
4232 drv.clear_interrupt_uint32_digital(&user, 0x11).unwrap();
4233 assert_eq!(
4234 drv.get_interrupt_uint32_digital(&user, InterruptReason::ZeroToOne)
4235 .unwrap(),
4236 0xE0
4237 );
4238 assert_eq!(
4239 drv.get_interrupt_uint32_digital(&user, InterruptReason::OneToZero)
4240 .unwrap(),
4241 0x0E
4242 );
4243 }
4244
4245 /// C parity: the default `read_int32` / `read_int64` / `read_float64` /
4246 /// `read_octet` / `read_uint32_digital` must surface an *unset*
4247 /// parameter as `ParamUndefined`, not success/0. The default
4248 /// `asynPortDriver::read{Int32,Int64,Float64,Octet,UInt32Digital}` calls
4249 /// `get{Integer,Integer64,Double,String,UIntDigital}Param`, every
4250 /// `paramVal` getter throws `ParamValNotDefined` → `asynParamUndefined`
4251 /// for an unset value (paramVal.cpp:152,181,235,264,292), and the
4252 /// `devAsyn*` device support routes that status through
4253 /// `asynStatusToEpicsAlarm(READ_ALARM, INVALID_ALARM)`. After a write
4254 /// the same reads succeed with the stored value.
4255 #[test]
4256 fn default_scalar_reads_report_undefined_until_set() {
4257 struct AllTypesDrv {
4258 base: PortDriverBase,
4259 }
4260 impl PortDriver for AllTypesDrv {
4261 fn base(&self) -> &PortDriverBase {
4262 &self.base
4263 }
4264 fn base_mut(&mut self) -> &mut PortDriverBase {
4265 &mut self.base
4266 }
4267 }
4268
4269 let mut base = PortDriverBase::new("undef_read", 1, PortFlags::default());
4270 let i32_idx = base.params.create_param("I32", ParamType::Int32).unwrap();
4271 let i64_idx = base.params.create_param("I64", ParamType::Int64).unwrap();
4272 let f64_idx = base.params.create_param("F64", ParamType::Float64).unwrap();
4273 let oct_idx = base.params.create_param("OCT", ParamType::Octet).unwrap();
4274 let u32_idx = base
4275 .params
4276 .create_param("BITS", ParamType::UInt32Digital)
4277 .unwrap();
4278 let mut drv = AllTypesDrv { base };
4279
4280 // Unset → every default scalar read is ParamUndefined, NOT Ok(0).
4281 assert!(matches!(
4282 drv.read_int32(&AsynUser::new(i32_idx).with_addr(0)),
4283 Err(AsynError::ParamUndefined(_))
4284 ));
4285 assert!(matches!(
4286 drv.read_int64(&AsynUser::new(i64_idx).with_addr(0)),
4287 Err(AsynError::ParamUndefined(_))
4288 ));
4289 assert!(matches!(
4290 drv.read_float64(&AsynUser::new(f64_idx).with_addr(0)),
4291 Err(AsynError::ParamUndefined(_))
4292 ));
4293 let mut buf = [0u8; 16];
4294 assert!(matches!(
4295 drv.read_octet(&AsynUser::new(oct_idx).with_addr(0), &mut buf),
4296 Err(AsynError::ParamUndefined(_))
4297 ));
4298 assert!(matches!(
4299 drv.read_uint32_digital(&AsynUser::new(u32_idx).with_addr(0), 0xFFFF_FFFF),
4300 Err(AsynError::ParamUndefined(_))
4301 ));
4302
4303 // After a write the same reads succeed with the stored value.
4304 drv.base_mut().params.set_int32(i32_idx, 0, 7).unwrap();
4305 drv.base_mut().params.set_int64(i64_idx, 0, 9).unwrap();
4306 drv.base_mut().params.set_float64(f64_idx, 0, 1.5).unwrap();
4307 drv.base_mut()
4308 .params
4309 .set_string(oct_idx, 0, b"hi".to_vec())
4310 .unwrap();
4311 drv.base_mut()
4312 .params
4313 .set_uint32(u32_idx, 0, 0x05, 0xFFFF_FFFF, 0)
4314 .unwrap();
4315
4316 assert_eq!(
4317 drv.read_int32(&AsynUser::new(i32_idx).with_addr(0))
4318 .unwrap(),
4319 7
4320 );
4321 assert_eq!(
4322 drv.read_int64(&AsynUser::new(i64_idx).with_addr(0))
4323 .unwrap(),
4324 9
4325 );
4326 assert_eq!(
4327 drv.read_float64(&AsynUser::new(f64_idx).with_addr(0))
4328 .unwrap(),
4329 1.5
4330 );
4331 let n = drv
4332 .read_octet(&AsynUser::new(oct_idx).with_addr(0), &mut buf)
4333 .unwrap();
4334 assert_eq!(&buf[..n], b"hi");
4335 assert_eq!(
4336 drv.read_uint32_digital(&AsynUser::new(u32_idx).with_addr(0), 0xFFFF_FFFF)
4337 .unwrap(),
4338 0x05
4339 );
4340 }
4341
4342 /// R16-47: the parameter block is one level *late* no longer.
4343 ///
4344 /// C `asynPortDriver::report` hands `reportParams` the level it was given,
4345 /// unchanged (asynPortDriver.cpp:3693); `reportParams` prints list 0 at any
4346 /// level and all `maxAddr` lists at `details >= 2` (:1804); and
4347 /// `paramVal::report` prints name, type, value and status for every parameter
4348 /// at every level (paramVal.cpp:296-372). Passing `level - 1` made
4349 /// `asynReport 1` print a bare count, and values appear only at 3.
4350 ///
4351 /// One case per threshold boundary: details 0 (no block), 1 (list 0, with
4352 /// values), 2 (every address list).
4353 #[test]
4354 fn report_prints_the_parameter_block_at_the_c_detail_levels() {
4355 struct Drv {
4356 base: PortDriverBase,
4357 }
4358 impl PortDriver for Drv {
4359 fn base(&self) -> &PortDriverBase {
4360 &self.base
4361 }
4362 fn base_mut(&mut self) -> &mut PortDriverBase {
4363 &mut self.base
4364 }
4365 }
4366
4367 let mut base = PortDriverBase::new(
4368 "rep",
4369 2,
4370 PortFlags {
4371 multi_device: true,
4372 ..PortFlags::default()
4373 },
4374 );
4375 let n = base.params.create_param("N", ParamType::Int32).unwrap();
4376 let x = base.params.create_param("X", ParamType::Float64).unwrap();
4377 let s_idx = base.params.create_param("S", ParamType::Octet).unwrap();
4378 let bits = base
4379 .params
4380 .create_param("BITS", ParamType::UInt32Digital)
4381 .unwrap();
4382 base.params.set_int32(n, 0, 7).unwrap();
4383 base.params.set_float64(x, 0, 0.1 + 0.2).unwrap();
4384 base.params.set_string(s_idx, 0, b"hello".to_vec()).unwrap();
4385 base.params.set_uint32(bits, 0, 0xa5, 0xff, 0).unwrap();
4386 base.params
4387 .set_uint32_interrupt(bits, 0, 0x0f, InterruptReason::ZeroToOne)
4388 .unwrap();
4389 // Addr 1 is left untouched: its parameters must report as undefined.
4390 let drv = Drv { base };
4391
4392 // details 0 — C prints the port line and stops (:3678-3680).
4393 let mut out = String::new();
4394 drv.report(&mut out, 0);
4395 assert!(
4396 !out.contains("Parameter"),
4397 "details 0 has no parameter block: {out}"
4398 );
4399
4400 // details 1 — list 0, the count, and every parameter WITH its value.
4401 let mut out = String::new();
4402 drv.report(&mut out, 1);
4403 assert!(
4404 out.contains("Parameter list 0\nNumber of parameters is: 4\n"),
4405 "C's paramList::report header (asynPortDriver.cpp:887): {out}"
4406 );
4407 assert!(
4408 out.contains("Parameter 0 type=asynInt32, name=N, value=7, status=0\n"),
4409 "{out}"
4410 );
4411 // C's `%g`: six significant digits, so 0.1+0.2 is `0.3`, not
4412 // `0.30000000000000004`.
4413 assert!(
4414 out.contains("Parameter 1 type=asynFloat64, name=X, value=0.3, status=0\n"),
4415 "{out}"
4416 );
4417 assert!(
4418 out.contains("Parameter 2 type=string, name=S, value=hello, status=0\n"),
4419 "C calls an octet parameter `string` (paramVal.cpp:328): {out}"
4420 );
4421 assert!(
4422 out.contains(
4423 "Parameter 3 type=asynUInt32Digital, name=BITS, value=0xa5, status=0, \
4424 risingMask=0xf, fallingMask=0x0, callbackMask=0xa5\n"
4425 ),
4426 "C prints the three masks with the value (paramVal.cpp:314-316): {out}"
4427 );
4428 assert!(
4429 !out.contains("Parameter list 1"),
4430 "below details 2 C reports one address list (asynPortDriver.cpp:1804): {out}"
4431 );
4432
4433 // details 2 — every address list, and addr 1 is undefined.
4434 let mut out = String::new();
4435 drv.report(&mut out, 2);
4436 assert!(out.contains("Parameter list 1\n"), "{out}");
4437 assert!(
4438 out.contains("Parameter 0 type=asynInt32, name=N, value is undefined\n"),
4439 "an unset parameter prints C's undefined line (paramVal.cpp:304): {out}"
4440 );
4441 }
4442
4443 /// R16-48: the report escapes a terminator the way C does — the whole libCom
4444 /// table, not just CR and LF.
4445 ///
4446 /// C prints the EOS pair with `epicsStrPrintEscaped` (asynPortDriver.cpp:3687,
4447 /// 3690), whose table is `\a \b \f \n \r \t \v \\ \' \"`, the byte itself
4448 /// when `isprint`, and `\xNN` otherwise (epicsString.c:230-269). The report's
4449 /// own two-case table wrote a binary terminator raw into stdout.
4450 #[test]
4451 fn report_escapes_the_eos_with_the_c_table() {
4452 struct Drv {
4453 base: PortDriverBase,
4454 }
4455 impl PortDriver for Drv {
4456 fn base(&self) -> &PortDriverBase {
4457 &self.base
4458 }
4459 fn base_mut(&mut self) -> &mut PortDriverBase {
4460 &mut self.base
4461 }
4462 fn capabilities(&self) -> Vec<crate::interfaces::Capability> {
4463 crate::interfaces::octet_transport_capabilities()
4464 }
4465 }
4466
4467 let mut base = PortDriverBase::new("eos_rep", 1, PortFlags::default());
4468 // See `a_single_device_port_collapses_every_addr_onto_one_eos`: the
4469 // EOS layer is what makes a terminator settable at all (R18-71).
4470 base.install_octet_interpose(Box::new(crate::interpose::eos::EosInterpose::default()));
4471 let mut drv = Drv { base };
4472 // A real binary terminator (C caps an EOS at two bytes): ESC, then NUL —
4473 // neither of which the old two-case table escaped.
4474 drv.set_input_eos(&AsynUser::default(), b"\x1b\0").unwrap();
4475 // …and the named escapes beyond CR/LF: TAB and BEL.
4476 drv.set_output_eos(&AsynUser::default(), b"\t\x07").unwrap();
4477
4478 let mut out = String::new();
4479 drv.report(&mut out, 1);
4480 assert!(
4481 out.contains(" Input EOS[2]: \\x1b\\0\n"),
4482 "CBUG-D4 refused: C's epicsStrPrintEscaped lacked a `case 0` (epicsString.c:255-260) and printed \\x00; the port supplies it so NUL renders \\0, matching epicsStrnEscapedFromRaw: {out}"
4483 );
4484 assert!(
4485 out.contains(" Output EOS[2]: \\t\\a\n"),
4486 "BEL is `\\a` in C's table, not `\\x07` (epicsString.c:245): {out}"
4487 );
4488 }
4489
4490 /// R16-49: the report has no options block, at any level.
4491 ///
4492 /// C `asynPortDriver::report` (asynPortDriver.cpp:3677-3710) prints the port
4493 /// name, the timestamp, the EOS pair, the parameter lists and — at
4494 /// `details >= 3` — the interrupt clients. It never prints options, and it
4495 /// could not: `asynOption` is a `getOption`/`setOption` pair keyed by a
4496 /// driver-defined string, with no enumeration to walk.
4497 #[test]
4498 fn report_prints_no_options_block() {
4499 struct Drv {
4500 base: PortDriverBase,
4501 }
4502 impl PortDriver for Drv {
4503 fn base(&self) -> &PortDriverBase {
4504 &self.base
4505 }
4506 fn base_mut(&mut self) -> &mut PortDriverBase {
4507 &mut self.base
4508 }
4509 }
4510
4511 let mut drv = Drv {
4512 base: PortDriverBase::new("opt_rep", 1, PortFlags::default()),
4513 };
4514 drv.set_option(&mut AsynUser::default(), "baud", "9600")
4515 .unwrap();
4516
4517 for level in 0..=4 {
4518 let mut out = String::new();
4519 drv.report(&mut out, level);
4520 assert!(
4521 !out.contains("option") && !out.contains("baud"),
4522 "asynReport {level} must print no options block: {out}"
4523 );
4524 }
4525 }
4526}