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