Skip to main content

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