Skip to main content

asyn_rs/
port.rs

1//! Port driver base and trait.
2//!
3//! # I/O Model
4//!
5//! Ports are driven by a `PortActor` running on a dedicated thread.
6//! The actor exclusively owns the driver and processes requests from a channel.
7//!
8//! **Cache path** (default `read_*`/`write_*` methods):
9//! - Default implementations operate on the parameter cache (non-blocking).
10//! - Background tasks update cache via `set_*_param()` + `call_param_callbacks()`.
11//!
12//! **Actor path** (requests submitted via [`crate::port_handle::PortHandle`]):
13//! - Each port gets a dedicated actor thread that dispatches requests to driver methods.
14//! - `can_block` indicates the port may perform blocking I/O.
15
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::time::{Duration, Instant, SystemTime};
19
20use std::any::Any;
21
22/// C `autoConnectDevice` reconnect throttle window (asynManager.c:713).
23/// A disconnected `auto_connect` device is refused a fresh connect attempt
24/// until this much time has elapsed since its last connect/disconnect
25/// transition or attempt, bounding reconnect storms to one attempt per
26/// window.
27const AUTO_CONNECT_THROTTLE: Duration = Duration::from_secs(2);
28
29/// Per-address device state for multi-device ports.
30#[derive(Debug, Clone)]
31pub struct DeviceState {
32    pub connected: bool,
33    pub enabled: bool,
34    pub auto_connect: bool,
35    /// Monotonic instant of the last connect/disconnect transition or
36    /// auto-connect attempt for this device — the anchor for the 2s
37    /// reconnect throttle (C `dpCommon.lastConnectDisconnect`). `None`
38    /// mirrors C's zero-initialised timestamp: the first attempt is
39    /// always permitted.
40    pub last_connect_disconnect: Option<Instant>,
41}
42
43impl Default for DeviceState {
44    fn default() -> Self {
45        Self {
46            connected: true,
47            enabled: true,
48            auto_connect: true,
49            last_connect_disconnect: None,
50        }
51    }
52}
53
54use crate::error::{AsynError, AsynResult, AsynStatus};
55use crate::exception::{AsynException, ExceptionEvent, ExceptionManager};
56use crate::interfaces::InterfaceType;
57use crate::interpose::{EomReason, OctetInterpose, OctetInterposeStack};
58use crate::interrupt::{InterruptManager, InterruptValue};
59use crate::param::{EnumEntry, InterruptReason, ParamList, ParamType, ParamValue};
60use crate::trace::TraceManager;
61use crate::user::AsynUser;
62
63/// C asyn `queueRequest` priority. In asyn-rs this exists as compatibility
64/// metadata only — there is no actual request queue or priority-based scheduling.
65/// Drivers manage their own async tasks directly.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
67pub enum QueuePriority {
68    Low = 0,
69    #[default]
70    Medium = 1,
71    High = 2,
72    /// Connect/disconnect operations — processed even when disabled/disconnected.
73    Connect = 3,
74}
75
76/// Port configuration flags.
77#[derive(Debug, Clone, Copy)]
78pub struct PortFlags {
79    /// True if port supports multiple sub-addresses (ASYN_MULTIDEVICE).
80    pub multi_device: bool,
81    /// True if port can block (ASYN_CANBLOCK).
82    ///
83    /// When `true`, the port gets a dedicated worker thread that serializes I/O via a
84    /// priority queue (matching C asyn's per-port thread model).
85    ///
86    /// When `false`, requests execute synchronously inline on the caller's thread
87    /// (no worker thread is spawned). This is appropriate for non-blocking drivers
88    /// whose `io_*` methods return immediately (e.g., cache-based parameter access).
89    pub can_block: bool,
90    /// True if port can be destroyed via shutdown_port (ASYN_DESTRUCTIBLE).
91    pub destructible: bool,
92}
93
94impl Default for PortFlags {
95    fn default() -> Self {
96        // `destructible: false` is the C asyn convention — see
97        // asynDriver.h:97 (`#define ASYN_DESTRUCTIBLE 0x0004`) — the
98        // attribute is opt-in via `pasynManager->registerPort(..., attr)`
99        // and `asynManager::shutdownPort` refuses to act on ports
100        // that did not opt in. Defaulting to `true` here over-applied
101        // shutdown rights to every driver that built PortFlags via
102        // `..PortFlags::default()`.
103        Self {
104            multi_device: false,
105            can_block: false,
106            destructible: false,
107        }
108    }
109}
110
111/// Base state shared by all port drivers.
112/// Contains the parameter library, interrupt manager, and connection state.
113///
114/// # Interpose concurrency
115///
116/// `interpose_octet` requires `&mut self` for all operations (both `push` and
117/// `dispatch_*`). Since `PortDriverBase` is always behind `Arc<Mutex<dyn PortDriver>>`,
118/// any access to `interpose_octet` requires the port lock. This naturally
119/// serializes interpose modifications with I/O dispatch — no additional
120/// synchronization is needed. **Callers must never modify the interpose stack
121/// without holding the port lock.**
122pub struct PortDriverBase {
123    pub port_name: String,
124    pub max_addr: usize,
125    pub flags: PortFlags,
126    pub params: ParamList,
127    pub interrupts: InterruptManager,
128    pub connected: bool,
129    pub enabled: bool,
130    pub auto_connect: bool,
131    /// `defunct` — set by [`Self::shutdown_lifecycle`] when a
132    /// destructible port is torn down via `shutdown_port`. Once true,
133    /// the port refuses every new request through [`Self::check_ready`].
134    /// Mirrors the `dpCommon.defunct` flag at C asynManager.c:2284
135    /// — once defunct, the port cannot be re-enabled.
136    pub defunct: bool,
137    /// Exception sink injected by [`crate::manager::PortManager`] on registration.
138    pub exception_sink: Option<Arc<ExceptionManager>>,
139    pub options: HashMap<String, String>,
140    /// Input EOS sequence (max 2 bytes). Used by EOS interpose and drivers.
141    pub input_eos: Vec<u8>,
142    /// Output EOS sequence (max 2 bytes). Used by EOS interpose and drivers.
143    pub output_eos: Vec<u8>,
144    pub interpose_octet: OctetInterposeStack,
145    pub trace: Option<Arc<TraceManager>>,
146    /// Per-address device state for multi-device ports.
147    pub device_states: HashMap<i32, DeviceState>,
148    /// Timestamp source callback for custom timestamps.
149    pub timestamp_source: Option<Arc<dyn Fn() -> SystemTime + Send + Sync>>,
150    /// Port-level anchor for the 2s auto-reconnect throttle — the
151    /// monotonic instant of the last connect/disconnect transition or
152    /// auto-connect attempt (C `dpCommon.lastConnectDisconnect`). `None`
153    /// = no transition yet, so the first attempt is always permitted.
154    pub last_connect_disconnect: Option<Instant>,
155}
156
157impl PortDriverBase {
158    pub fn new(port_name: &str, max_addr: usize, flags: PortFlags) -> Self {
159        Self {
160            port_name: port_name.to_string(),
161            max_addr: max_addr.max(1),
162            flags,
163            params: ParamList::new(max_addr, flags.multi_device),
164            interrupts: InterruptManager::new(256),
165            connected: true,
166            enabled: true,
167            auto_connect: true,
168            defunct: false,
169            exception_sink: None,
170            options: HashMap::new(),
171            input_eos: Vec::new(),
172            output_eos: Vec::new(),
173            interpose_octet: OctetInterposeStack::new(),
174            trace: None,
175            device_states: HashMap::new(),
176            timestamp_source: None,
177            last_connect_disconnect: None,
178        }
179    }
180
181    /// Announce an exception through the global exception manager (if injected).
182    pub fn announce_exception(&self, exception: AsynException, addr: i32) {
183        if let Some(ref sink) = self.exception_sink {
184            sink.announce(&ExceptionEvent {
185                port_name: self.port_name.clone(),
186                exception,
187                addr,
188            });
189        }
190    }
191
192    /// Query whether the port is connected.
193    pub fn is_connected(&self) -> bool {
194        self.connected
195    }
196
197    /// Single owner-API for the port-level `connected` transition.
198    ///
199    /// C parity: `exceptionConnect` (asynManager.c:2151-2160) and
200    /// `exceptionDisconnect` (:2174-2185) fire
201    /// `asynExceptionConnect` only when the state actually changes.
202    /// All driver code that toggles connection state MUST go through
203    /// this helper — directly assigning `base.connected = ...`
204    /// followed by `announce_exception(Connect, -1)` bypasses the
205    /// edge guard and fans spurious duplicates out to listeners
206    /// (CA gateway shadow tasks, asynRecord, monitor relays).
207    ///
208    /// Returns `true` if the state actually changed (a fan-out
209    /// happened); `false` if the call was a no-op.
210    pub fn set_connected(&mut self, connected: bool) -> bool {
211        if self.connected == connected {
212            return false;
213        }
214        self.connected = connected;
215        if !connected {
216            // C `exceptionDisconnect` stamps `lastConnectDisconnect` on
217            // every disconnect (asynManager.c:2184) so the auto-reconnect
218            // throttle measures from the moment the link dropped.
219            self.last_connect_disconnect = Some(Instant::now());
220        }
221        self.announce_exception(AsynException::Connect, -1);
222        true
223    }
224
225    /// Per-address variant — for multi-device ports. Same edge
226    /// guarantee as [`Self::set_connected`].
227    pub fn set_addr_connected(&mut self, addr: i32, connected: bool) -> bool {
228        let was = self.device_state(addr).connected;
229        if was == connected {
230            return false;
231        }
232        self.device_state(addr).connected = connected;
233        if !connected {
234            // Per-device disconnect stamp — same throttle anchor as the
235            // port-level path (C `exceptionDisconnect`, asynManager.c:2184).
236            self.device_state(addr).last_connect_disconnect = Some(Instant::now());
237        }
238        self.announce_exception(AsynException::Connect, addr);
239        true
240    }
241
242    /// 2.0s auto-reconnect throttle gate — C `autoConnectDevice`
243    /// (asynManager.c:712-713, 729-730).
244    ///
245    /// Returns `true` when a fresh auto-connect attempt is permitted:
246    /// either no transition has been recorded yet (mirrors C's
247    /// zero-initialised `lastConnectDisconnect`, whose diff against `now`
248    /// is effectively infinite), or at least [`AUTO_CONNECT_THROTTLE`] has
249    /// elapsed since the last transition or attempt. A disconnected
250    /// `auto_connect` device that just dropped — or whose previous
251    /// reconnect just failed — is refused until the window passes, so a
252    /// burst of N queued requests triggers at most one full connect
253    /// attempt per window instead of N back-to-back attempts.
254    ///
255    /// Uses monotonic [`Instant`], not wall clock: the throttle is purely
256    /// internal timing, never serialised, so it must be immune to NTP
257    /// steps. `addr` selects the per-device anchor on multi-device ports;
258    /// single-device ports use the port-level anchor.
259    pub fn auto_connect_throttle_ok(&self, addr: i32, now: Instant) -> bool {
260        let last = if self.flags.multi_device {
261            self.device_states
262                .get(&addr)
263                .and_then(|d| d.last_connect_disconnect)
264        } else {
265            self.last_connect_disconnect
266        };
267        match last {
268            None => true,
269            Some(t) => now.saturating_duration_since(t) >= AUTO_CONNECT_THROTTLE,
270        }
271    }
272
273    /// Single owner for the post-attempt throttle stamp. C
274    /// `autoConnectDevice` stamps `lastConnectDisconnect` immediately after
275    /// every `connectAttempt`, success or failure (asynManager.c:718,
276    /// 735), so the window restarts from the end of the attempt — a failed
277    /// reconnect is not retried until the throttle elapses again.
278    pub fn stamp_auto_connect_attempt(&mut self, addr: i32, now: Instant) {
279        if self.flags.multi_device {
280            self.device_state(addr).last_connect_disconnect = Some(now);
281        } else {
282            self.last_connect_disconnect = Some(now);
283        }
284    }
285
286    /// Query whether the port is enabled.
287    pub fn is_enabled(&self) -> bool {
288        self.enabled
289    }
290
291    /// Single owner-API for the port-level `enabled` transition.
292    ///
293    /// C `enable` (asynManager.c:2222-2249) refuses a shut-down port:
294    /// when `defunct` it returns `asynDisabled` *without* touching
295    /// `enabled` and *without* firing the `asynExceptionEnable` fan-out.
296    /// Otherwise it sets `enabled` and announces unconditionally (no
297    /// state-change guard). The actor's `SetEnable` op is a lifecycle op
298    /// that bypasses [`Self::check_ready`], so this guard is the only
299    /// thing that stops a defunct port from being re-enabled or fanning
300    /// out a spurious exception — it must live here, in the one owner of
301    /// the transition.
302    pub fn set_enabled(&mut self, enabled: bool) -> AsynResult<()> {
303        if self.defunct {
304            return Err(AsynError::Status {
305                status: AsynStatus::Disabled,
306                message: format!("port {} has been shut down (defunct)", self.port_name),
307            });
308        }
309        self.enabled = enabled;
310        self.announce_exception(AsynException::Enable, -1);
311        Ok(())
312    }
313
314    /// Per-address variant — same defunct refusal as [`Self::set_enabled`].
315    /// `defunct` is modelled at the port level (a shut-down port takes its
316    /// devices with it), so a defunct port refuses per-device enable/disable
317    /// too, matching C's `dpCommon.defunct` check on the resolved device.
318    pub fn set_addr_enabled(&mut self, addr: i32, enabled: bool) -> AsynResult<()> {
319        if self.defunct {
320            return Err(AsynError::Status {
321                status: AsynStatus::Disabled,
322                message: format!("port {} has been shut down (defunct)", self.port_name),
323            });
324        }
325        self.device_state(addr).enabled = enabled;
326        self.announce_exception(AsynException::Enable, addr);
327        Ok(())
328    }
329
330    /// Query whether auto-connect is enabled.
331    pub fn is_auto_connect(&self) -> bool {
332        self.auto_connect
333    }
334
335    /// Toggle the auto-connect flag at runtime.
336    ///
337    /// C parity: `autoConnectAsyn` (asynManager.c:2310-2324) always
338    /// fires `asynExceptionAutoConnect` regardless of prior state
339    /// (no state-change guard). Mirror that — every call announces.
340    /// Driver constructors that initialise `base.auto_connect`
341    /// directly during `PortDriver::new()` keep the silent path
342    /// (the port is not yet registered, so no listeners exist).
343    pub fn set_auto_connect(&mut self, yes: bool) {
344        self.auto_connect = yes;
345        self.announce_exception(AsynException::AutoConnect, -1);
346    }
347
348    /// Per-address variant — for multi-device ports. C parity:
349    /// `autoConnectAsyn` walks dpCommon via findDpCommon so a per-
350    /// device pasynUser hits the device's dpc, otherwise the port's
351    /// dpc (asynManager.c:2314 + findDpCommon).
352    pub fn set_auto_connect_addr(&mut self, addr: i32, yes: bool) {
353        self.device_state(addr).auto_connect = yes;
354        self.announce_exception(AsynException::AutoConnect, addr);
355    }
356
357    /// Query whether the port has been marked defunct via
358    /// [`Self::shutdown_lifecycle`] — once true the port is gone for
359    /// good, mirroring C asynManager.c:2266-2269.
360    pub fn is_defunct(&self) -> bool {
361        self.defunct
362    }
363
364    /// Check that the port is enabled, connected, and not defunct.
365    /// Returns `Err(Disabled)`, `Err(Disconnected)`, or `Err(Disabled)`
366    /// (defunct => permanently disabled) otherwise.
367    pub fn check_ready(&self) -> AsynResult<()> {
368        // C asyn parity: a defunct port short-circuits queueRequest
369        // (asynManager.c:2283 comment). Reject *before* the enabled
370        // check so the error message names the lifecycle phase, not
371        // just "disabled".
372        if self.defunct {
373            return Err(AsynError::Status {
374                status: AsynStatus::Disabled,
375                message: format!("port {} has been shut down (defunct)", self.port_name),
376            });
377        }
378        if !self.enabled {
379            return Err(AsynError::Status {
380                status: AsynStatus::Disabled,
381                message: format!("port {} is disabled", self.port_name),
382            });
383        }
384        if !self.connected {
385            return Err(AsynError::Status {
386                status: AsynStatus::Disconnected,
387                message: format!("port {} is disconnected", self.port_name),
388            });
389        }
390        Ok(())
391    }
392
393    /// Run the C `shutdownPort` lifecycle (asynManager.c:2251-2308):
394    ///
395    /// 1. Refuse if the port did not opt into `ASYN_DESTRUCTIBLE`
396    ///    (returns `Err(Status::Error)`).
397    /// 2. Short-circuit if already defunct (idempotent — returns Ok).
398    /// 3. Set `enabled = false`, `defunct = true` — every subsequent
399    ///    request through [`Self::check_ready`] fails.
400    /// 4. Broadcast `AsynException::Shutdown` so registered observers
401    ///    (CA gateways, monitor sinks) tear down their handles.
402    ///
403    /// Drivers should call this from their own shutdown plumbing and
404    /// then release any hardware-owned resources via their
405    /// [`PortDriver::shutdown`] implementation. Callers from outside
406    /// the runtime can drive the same lifecycle via
407    /// [`crate::manager::PortManager::shutdown_port`].
408    pub fn shutdown_lifecycle(&mut self) -> AsynResult<()> {
409        if self.defunct {
410            // Idempotent — C asynManager.c:2266-2269 returns asynSuccess.
411            return Ok(());
412        }
413        if !self.flags.destructible {
414            return Err(AsynError::Status {
415                status: AsynStatus::Error,
416                message: format!(
417                    "port {} does not support shutting down (ASYN_DESTRUCTIBLE not set)",
418                    self.port_name
419                ),
420            });
421        }
422        self.enabled = false;
423        self.defunct = true;
424        self.announce_exception(AsynException::Shutdown, -1);
425        Ok(())
426    }
427
428    /// Check that port + device address are both ready.
429    /// For multi-device ports, checks per-address state in addition to port-level state.
430    pub fn check_ready_addr(&self, addr: i32) -> AsynResult<()> {
431        self.check_ready()?;
432        if self.flags.multi_device {
433            if let Some(ds) = self.device_states.get(&addr) {
434                if !ds.enabled {
435                    return Err(AsynError::Status {
436                        status: AsynStatus::Disabled,
437                        message: format!("port {} addr {} is disabled", self.port_name, addr),
438                    });
439                }
440                if !ds.connected {
441                    return Err(AsynError::Status {
442                        status: AsynStatus::Disconnected,
443                        message: format!("port {} addr {} is disconnected", self.port_name, addr),
444                    });
445                }
446            }
447        }
448        Ok(())
449    }
450
451    /// Get or create a device state for the given address.
452    pub fn device_state(&mut self, addr: i32) -> &mut DeviceState {
453        self.device_states.entry(addr).or_default()
454    }
455
456    /// Check if a specific device address is connected.
457    pub fn is_device_connected(&self, addr: i32) -> bool {
458        self.device_states
459            .get(&addr)
460            .map_or(true, |ds| ds.connected)
461    }
462
463    /// Set a specific device address as connected.
464    ///
465    /// C parity: announce only on actual transition
466    /// (asynManager.c:2151-2160 — `exceptionConnect` rejects
467    /// already-connected; we keep an Ok return for idempotency but
468    /// suppress the duplicate fan-out so subscribers don't see
469    /// spurious connect events). Thin wrapper over
470    /// [`Self::set_addr_connected`] for callers that prefer the
471    /// directional verb.
472    pub fn connect_addr(&mut self, addr: i32) {
473        self.set_addr_connected(addr, true);
474    }
475
476    /// Set a specific device address as disconnected.
477    ///
478    /// C parity: announce only on actual transition
479    /// (asynManager.c:2174-2185). Thin wrapper over
480    /// [`Self::set_addr_connected`].
481    pub fn disconnect_addr(&mut self, addr: i32) {
482        self.set_addr_connected(addr, false);
483    }
484
485    /// Enable a specific device address. Convenience facade over the
486    /// guarded owner [`Self::set_addr_enabled`]; a defunct port no-ops.
487    pub fn enable_addr(&mut self, addr: i32) {
488        let _ = self.set_addr_enabled(addr, true);
489    }
490
491    /// Disable a specific device address. Convenience facade over the
492    /// guarded owner [`Self::set_addr_enabled`]; a defunct port no-ops.
493    pub fn disable_addr(&mut self, addr: i32) {
494        let _ = self.set_addr_enabled(addr, false);
495    }
496
497    /// Set a custom timestamp source callback.
498    pub fn register_timestamp_source<F>(&mut self, source: F)
499    where
500        F: Fn() -> SystemTime + Send + Sync + 'static,
501    {
502        self.timestamp_source = Some(Arc::new(source));
503    }
504
505    /// Get current timestamp from the registered source, or SystemTime::now().
506    pub fn current_timestamp(&self) -> SystemTime {
507        self.timestamp_source
508            .as_ref()
509            .map_or_else(SystemTime::now, |f| f())
510    }
511
512    pub fn create_param(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
513        self.params.create_param(name, param_type)
514    }
515
516    pub fn find_param(&self, name: &str) -> Option<usize> {
517        self.params.find_param(name)
518    }
519
520    // --- Convenience param accessors ---
521
522    pub fn set_int32_param(&mut self, index: usize, addr: i32, value: i32) -> AsynResult<()> {
523        self.params.set_int32(index, addr, value)
524    }
525
526    pub fn get_int32_param(&self, index: usize, addr: i32) -> AsynResult<i32> {
527        self.params.get_int32(index, addr)
528    }
529
530    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
531    /// cache entry has never been set (C parity for `asynParamUndefined`).
532    /// See [`crate::param::ParamList::get_int32_strict`].
533    pub fn get_int32_param_strict(&self, index: usize, addr: i32) -> AsynResult<i32> {
534        self.params.get_int32_strict(index, addr)
535    }
536
537    pub fn set_int64_param(&mut self, index: usize, addr: i32, value: i64) -> AsynResult<()> {
538        self.params.set_int64(index, addr, value)
539    }
540
541    pub fn get_int64_param(&self, index: usize, addr: i32) -> AsynResult<i64> {
542        self.params.get_int64(index, addr)
543    }
544
545    /// Strict variant — see [`crate::param::ParamList::get_int64_strict`].
546    pub fn get_int64_param_strict(&self, index: usize, addr: i32) -> AsynResult<i64> {
547        self.params.get_int64_strict(index, addr)
548    }
549
550    pub fn set_float64_param(&mut self, index: usize, addr: i32, value: f64) -> AsynResult<()> {
551        self.params.set_float64(index, addr, value)
552    }
553
554    pub fn get_float64_param(&self, index: usize, addr: i32) -> AsynResult<f64> {
555        self.params.get_float64(index, addr)
556    }
557
558    /// Strict variant — see [`crate::param::ParamList::get_float64_strict`].
559    pub fn get_float64_param_strict(&self, index: usize, addr: i32) -> AsynResult<f64> {
560        self.params.get_float64_strict(index, addr)
561    }
562
563    pub fn set_string_param(&mut self, index: usize, addr: i32, value: String) -> AsynResult<()> {
564        self.params.set_string(index, addr, value)
565    }
566
567    pub fn get_string_param(&self, index: usize, addr: i32) -> AsynResult<&str> {
568        self.params.get_string(index, addr)
569    }
570
571    /// Strict variant — see [`crate::param::ParamList::get_string_strict`].
572    pub fn get_string_param_strict(&self, index: usize, addr: i32) -> AsynResult<&str> {
573        self.params.get_string_strict(index, addr)
574    }
575
576    /// Set a UInt32Digital parameter. `interrupt_mask` mirrors C
577    /// `setUIntDigitalParam(.., interruptMask)` (asynPortDriver.cpp:1369,
578    /// 1381): bits to force into the I/O Intr callback mask even when the
579    /// stored value did not change. Pass `0` for a plain value set (the
580    /// 3-arg C overload, asynPortDriver.cpp:1347).
581    pub fn set_uint32_param(
582        &mut self,
583        index: usize,
584        addr: i32,
585        value: u32,
586        mask: u32,
587        interrupt_mask: u32,
588    ) -> AsynResult<()> {
589        self.params
590            .set_uint32(index, addr, value, mask, interrupt_mask)
591    }
592
593    pub fn get_uint32_param(&self, index: usize, addr: i32) -> AsynResult<u32> {
594        self.params.get_uint32(index, addr)
595    }
596
597    /// Strict variant — see [`crate::param::ParamList::get_uint32_strict`].
598    pub fn get_uint32_param_strict(&self, index: usize, addr: i32) -> AsynResult<u32> {
599        self.params.get_uint32_strict(index, addr)
600    }
601
602    pub fn get_enum_param(&self, index: usize, addr: i32) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
603        self.params.get_enum(index, addr)
604    }
605
606    pub fn set_enum_index_param(
607        &mut self,
608        index: usize,
609        addr: i32,
610        value: usize,
611    ) -> AsynResult<()> {
612        self.params.set_enum_index(index, addr, value)
613    }
614
615    pub fn set_enum_choices_param(
616        &mut self,
617        index: usize,
618        addr: i32,
619        choices: Arc<[EnumEntry]>,
620    ) -> AsynResult<()> {
621        self.params.set_enum_choices(index, addr, choices)
622    }
623
624    pub fn get_generic_pointer_param(
625        &self,
626        index: usize,
627        addr: i32,
628    ) -> AsynResult<Arc<dyn Any + Send + Sync>> {
629        self.params.get_generic_pointer(index, addr)
630    }
631
632    pub fn set_generic_pointer_param(
633        &mut self,
634        index: usize,
635        addr: i32,
636        value: Arc<dyn Any + Send + Sync>,
637    ) -> AsynResult<()> {
638        self.params.set_generic_pointer(index, addr, value)
639    }
640
641    pub fn set_param_timestamp(
642        &mut self,
643        index: usize,
644        addr: i32,
645        ts: SystemTime,
646    ) -> AsynResult<()> {
647        self.params.set_timestamp(index, addr, ts)
648    }
649
650    pub fn set_param_status(
651        &mut self,
652        index: usize,
653        addr: i32,
654        status: AsynStatus,
655        alarm_status: u16,
656        alarm_severity: u16,
657    ) -> AsynResult<()> {
658        self.params
659            .set_param_status(index, addr, status, alarm_status, alarm_severity)
660    }
661
662    pub fn get_param_status(&self, index: usize, addr: i32) -> AsynResult<(AsynStatus, u16, u16)> {
663        self.params.get_param_status(index, addr)
664    }
665
666    /// Detailed parameter report matching C asynPortDriver::reportParams.
667    pub fn report_params(&self, level: i32) {
668        eprintln!("  Number of parameters is {}", self.params.len());
669        if level < 1 {
670            return;
671        }
672        for i in 0..self.params.len() {
673            let name = self.params.param_name(i).unwrap_or("?");
674            let ptype = self
675                .params
676                .param_type(i)
677                .map(|t| format!("{t:?}"))
678                .unwrap_or("?".into());
679            if level >= 2 {
680                for addr in 0..self.max_addr.max(1) {
681                    let val = self
682                        .params
683                        .get_value(i, addr as i32)
684                        .map(|v| format!("{v:?}"))
685                        .unwrap_or("undefined".into());
686                    let (status, alarm_st, alarm_sev) = self
687                        .params
688                        .get_param_status(i, addr as i32)
689                        .unwrap_or((AsynStatus::Success, 0, 0));
690                    eprintln!(
691                        "  param[{i}] name={name} type={ptype} addr={addr} val={val} status={status:?} alarm=({alarm_st},{alarm_sev})"
692                    );
693                }
694            } else {
695                eprintln!("  param[{i}] name={name} type={ptype}");
696            }
697        }
698    }
699
700    /// Push an interpose layer onto the octet I/O stack.
701    ///
702    /// **Concurrency**: requires `&mut self`, which means the caller must hold
703    /// the port lock (`Arc<Mutex<dyn PortDriver>>`). This ensures
704    /// interpose modifications are serialized with I/O dispatch.
705    pub fn push_octet_interpose(&mut self, layer: Box<dyn OctetInterpose>) {
706        self.interpose_octet.push(layer);
707    }
708
709    /// Flush changed parameters as interrupt notifications.
710    /// Equivalent to C asyn's callParamCallbacks().
711    pub fn call_param_callbacks(&mut self, addr: i32) -> AsynResult<()> {
712        let changed = self.params.take_changed(addr)?;
713        let now = self.current_timestamp();
714        for reason in changed {
715            let value = self.params.get_value(reason, addr)?.clone();
716            // C asynPortDriver.cpp:845 — callCallbacks skips firing for an
717            // undefined param even though its changed flag is consumed
718            // (flags.clear() at :871). A status/alarm change or bare
719            // mark_changed on a never-set scalar must not emit an I/O Intr.
720            // Array/generic-pointer params have no callCallbacks analog
721            // (:846-865 switch is scalar-only) and Rust fires them as a
722            // read-trigger regardless, so gate scalars only.
723            if !value.is_array() && !self.params.is_param_defined(reason, addr).unwrap_or(false) {
724                continue;
725            }
726            let ts = self.params.get_timestamp(reason, addr)?.unwrap_or(now);
727            // C parity: read the accumulated callback mask and reset it
728            // (asynPortDriver.cpp:854-855 fires uint32Callback then sets
729            // uInt32CallbackMask = 0). The flush is the single owner of
730            // this consume, so accumulated bits never leak to the next.
731            let uint32_mask = self
732                .params
733                .take_uint32_interrupt_mask(reason, addr)
734                .unwrap_or(0);
735            // C parity: asynPortDriver.cpp:631-642 sets
736            // `pInterrupt->pasynUser->auxStatus/alarmStatus/alarmSeverity`
737            // from the param's stored status before invoking each
738            // subscriber callback. Pull those here so subscribers see
739            // the same triplet C consumers do.
740            let (aux_status, alarm_status, alarm_severity) = self
741                .params
742                .get_param_status(reason, addr)
743                .unwrap_or((AsynStatus::Success, 0, 0));
744            self.interrupts.notify(InterruptValue {
745                reason,
746                addr,
747                value,
748                timestamp: ts,
749                uint32_changed_mask: uint32_mask,
750                aux_status,
751                alarm_status,
752                alarm_severity,
753                // Untyped: a single cached value per (reason,addr) reaches
754                // every subscribing interface (the pre-per-interface path).
755                iface: None,
756            });
757        }
758        Ok(())
759    }
760
761    /// Flush a single parameter's changed flag and notify if dirty.
762    /// Use this instead of `call_param_callbacks` when you want to avoid
763    /// flushing unrelated parameters (e.g. rapidly-updating CP-linked params).
764    pub fn call_param_callback(&mut self, addr: i32, reason: usize) -> AsynResult<()> {
765        if self.params.take_changed_single(reason, addr)? {
766            let value = self.params.get_value(reason, addr)?.clone();
767            // C asynPortDriver.cpp:845 — see `call_param_callbacks`: an
768            // undefined scalar consumes its changed flag but fires no
769            // callback. Array/generic-pointer triggers fire regardless.
770            if !value.is_array() && !self.params.is_param_defined(reason, addr).unwrap_or(false) {
771                return Ok(());
772            }
773            let now = self.current_timestamp();
774            let ts = self.params.get_timestamp(reason, addr)?.unwrap_or(now);
775            // C parity: read the accumulated callback mask and reset it
776            // (asynPortDriver.cpp:854-855 fires uint32Callback then sets
777            // uInt32CallbackMask = 0). The flush is the single owner of
778            // this consume, so accumulated bits never leak to the next.
779            let uint32_mask = self
780                .params
781                .take_uint32_interrupt_mask(reason, addr)
782                .unwrap_or(0);
783            // C parity: see `call_param_callbacks` above.
784            let (aux_status, alarm_status, alarm_severity) = self
785                .params
786                .get_param_status(reason, addr)
787                .unwrap_or((AsynStatus::Success, 0, 0));
788            self.interrupts.notify(InterruptValue {
789                reason,
790                addr,
791                value,
792                timestamp: ts,
793                uint32_changed_mask: uint32_mask,
794                aux_status,
795                alarm_status,
796                alarm_severity,
797                // Untyped (see `call_param_callbacks`).
798                iface: None,
799            });
800        }
801        Ok(())
802    }
803
804    /// Mark a parameter as changed without modifying its value.
805    ///
806    /// Use this to trigger I/O Intr on params whose data is served via
807    /// `read_*_array()` overrides rather than the param cache (e.g. pixel data).
808    pub fn mark_param_changed(&mut self, index: usize, addr: i32) -> AsynResult<()> {
809        self.params.mark_changed(index, addr)
810    }
811
812    /// Fire one per-interface I/O Intr callback carrying an interface-typed value.
813    ///
814    /// `call_param_callbacks` stores **one** value per `(reason, addr)` and
815    /// notifies it untyped, so every record on that reason — whatever its DTYP's
816    /// interface — receives the same value. For a driver whose single raw datum
817    /// is exposed on several asyn interfaces at once (e.g. a Modbus register read
818    /// by an `asynInt32` ai, an `asynUInt32Digital` bi, and an `asynFloat64` ai
819    /// simultaneously), that collapse delivers a wrong-typed value to all but one
820    /// of them. C's `drvModbusAsyn::readPoller` instead decodes the one register
821    /// block **separately per interface** and invokes each interface's own
822    /// interrupt list (`int32`/`uInt32Digital`/`float64`,
823    /// drvModbusAsyn.cpp:1706/1736/1808). This is the analogue: the driver
824    /// decodes per interface and fires each value tagged with its `iface`, so the
825    /// interrupt filter routes it only to records on that interface
826    /// ([`InterruptFilter::iface`]). `uint32_changed_mask` is the changed-bit
827    /// mask for the `UInt32Digital` interface (a record's `@asynMask` gates on it,
828    /// `asynPortDriver.cpp:720`); pass `0` for the other interfaces, whose
829    /// subscribers carry no mask filter.
830    pub fn notify_interface_value(
831        &self,
832        reason: usize,
833        addr: i32,
834        iface: InterfaceType,
835        value: ParamValue,
836        uint32_changed_mask: u32,
837    ) {
838        let ts = self.current_timestamp();
839        self.interrupts.notify(InterruptValue {
840            reason,
841            addr,
842            value,
843            timestamp: ts,
844            uint32_changed_mask,
845            // A successful per-interface decode (the caller fires only after the
846            // raw read succeeded); a transport failure aborts the poll before
847            // any fire, matching C's `if (ioStatus_) ... return` in readPoller.
848            aux_status: AsynStatus::Success,
849            alarm_status: 0,
850            alarm_severity: 0,
851            iface: Some(iface),
852        });
853    }
854}
855
856/// Result of resolving a record's driver-info string at bind time — the
857/// asyn-rs analogue of what C `drvUserCreate` writes into `pasynUser`.
858///
859/// `reason` is the shared parameter index (every record with the same drvInfo
860/// resolves to it). The remaining fields carry **per-record** driver state the
861/// lookup derived from this particular drvInfo string (C stashes the same in
862/// `pasynUser->drvUser`), which the binding applies to that record's I/O.
863#[derive(Debug, Default)]
864pub struct DrvUserInfo {
865    /// Shared parameter index for this drvInfo (C `pasynUser->reason`).
866    pub reason: usize,
867    /// Optional per-record octet length cap — the asyn-rs home for C's
868    /// `modbusDrvUser_t.len` (`drvUserCreate` parses `TYPE=N`; `getStringLen`
869    /// caps the asyn octet `maxLen` to it, drvModbusAsyn.cpp:2367-2377). `None`
870    /// when the drvInfo carried no cap; the binding then uses the record buffer
871    /// length alone. The binding applies `min(buffer_len, cap)`.
872    pub max_octet_len: Option<usize>,
873}
874
875impl DrvUserInfo {
876    /// A resolution carrying only the shared reason and no per-record cap — the
877    /// default-lookup result.
878    pub fn from_reason(reason: usize) -> Self {
879        Self {
880            reason,
881            ..Self::default()
882        }
883    }
884}
885
886/// Port driver trait. All methods have default implementations that operate
887/// on the parameter cache (no actual I/O).
888///
889/// Drivers performing real hardware I/O should:
890/// 1. Run I/O in a background task (e.g., tokio::spawn)
891/// 2. Update parameters via `base_mut().set_*_param()` + `call_param_callbacks()`
892/// 3. Let the default `read_*` methods return cached values
893///
894/// # LockPort/UnlockPort
895///
896/// C asyn provides `lockPort`/`unlockPort` for direct mutex locking. In asyn-rs,
897/// the port is always behind `Arc<Mutex<dyn PortDriver>>`, so callers hold the
898/// parking_lot mutex directly. For multi-request exclusive access, use
899/// `BlockProcess`/`UnblockProcess` via the worker queue.
900pub trait PortDriver: Send + Sync + 'static {
901    fn base(&self) -> &PortDriverBase;
902    fn base_mut(&mut self) -> &mut PortDriverBase;
903
904    // --- AsynCommon ---
905
906    fn connect(&mut self, _user: &AsynUser) -> AsynResult<()> {
907        // Single owner-API: edge-guarded fire is in PortDriverBase::set_connected.
908        self.base_mut().set_connected(true);
909        Ok(())
910    }
911
912    fn disconnect(&mut self, _user: &AsynUser) -> AsynResult<()> {
913        self.base_mut().set_connected(false);
914        Ok(())
915    }
916
917    fn enable(&mut self, _user: &AsynUser) -> AsynResult<()> {
918        // C `enable` refuses a defunct port (asynManager.c:2236-2241);
919        // the guard lives in the single owner.
920        self.base_mut().set_enabled(true)
921    }
922
923    fn disable(&mut self, _user: &AsynUser) -> AsynResult<()> {
924        self.base_mut().set_enabled(false)
925    }
926
927    fn connect_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
928        self.base_mut().connect_addr(user.addr);
929        Ok(())
930    }
931
932    fn disconnect_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
933        self.base_mut().disconnect_addr(user.addr);
934        Ok(())
935    }
936
937    fn enable_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
938        // Guarded owner — propagates asynDisabled on a defunct port.
939        self.base_mut().set_addr_enabled(user.addr, true)
940    }
941
942    fn disable_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
943        self.base_mut().set_addr_enabled(user.addr, false)
944    }
945
946    fn get_option(&self, key: &str) -> AsynResult<String> {
947        self.base()
948            .options
949            .get(key)
950            .cloned()
951            .ok_or_else(|| AsynError::OptionNotFound(key.to_string()))
952    }
953
954    fn set_option(&mut self, key: &str, value: &str) -> AsynResult<()> {
955        self.base_mut()
956            .options
957            .insert(key.to_string(), value.to_string());
958        Ok(())
959    }
960
961    fn report(&self, level: i32) {
962        let base = self.base();
963        eprintln!("Port: {}", base.port_name);
964        eprintln!(
965            "  connected: {}, max_addr: {}, params: {}, options: {}",
966            base.connected,
967            base.max_addr,
968            base.params.len(),
969            base.options.len()
970        );
971        if level >= 1 {
972            base.report_params(level.saturating_sub(1));
973        }
974        if level >= 2 {
975            for (k, v) in &base.options {
976                eprintln!("  option: {k} = {v}");
977            }
978        }
979    }
980
981    // --- Scalar I/O (cache-based defaults, timeout not applicable) ---
982
983    // Cache-based defaults do NOT check connection state (C parity).
984    // The port actor checks check_ready_addr() before dispatching, matching
985    // C asyn where asynManager checks connection before calling the driver.
986
987    // Default reads use the STRICT getter: an undefined parameter must
988    // surface as ParamUndefined, not success/0. C parity — the default
989    // asynPortDriver::read{Int32,Int64,Float64,Octet,UInt32Digital}
990    // (asynPortDriver.cpp) calls get{Integer,Integer64,Double,String,
991    // UIntDigital}Param, and every paramVal getter throws
992    // ParamValNotDefined → asynParamUndefined for an unset value
993    // (paramVal.cpp:152,181,235,264,292). devAsyn* then routes that status
994    // through asynStatusToEpicsAlarm(READ_ALARM, INVALID_ALARM) instead of
995    // updating RVAL/clearing UDF (e.g. devAsynUInt32Digital.c:898-901,
996    // devAsynInt32.c:844-847). The lax get_*_param accessors stay for
997    // internal callers that explicitly want default-zero behavior.
998
999    fn read_int32(&mut self, user: &AsynUser) -> AsynResult<i32> {
1000        self.base().params.get_int32_strict(user.reason, user.addr)
1001    }
1002
1003    fn write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1004        self.base_mut()
1005            .params
1006            .set_int32(user.reason, user.addr, value)?;
1007        self.base_mut().call_param_callbacks(user.addr)
1008    }
1009
1010    fn read_int64(&mut self, user: &AsynUser) -> AsynResult<i64> {
1011        self.base().params.get_int64_strict(user.reason, user.addr)
1012    }
1013
1014    fn write_int64(&mut self, user: &mut AsynUser, value: i64) -> AsynResult<()> {
1015        self.base_mut()
1016            .params
1017            .set_int64(user.reason, user.addr, value)?;
1018        self.base_mut().call_param_callbacks(user.addr)
1019    }
1020
1021    /// C `asynInt32Base.c:99` default: report `low = high = 0` so a
1022    /// driver that does not implement getBounds makes convertAi/convertAo
1023    /// skip the LINEAR ESLO/EOFF computation (`devAsynInt32.c:444`).
1024    fn get_bounds_int32(&self, _user: &AsynUser) -> AsynResult<(i32, i32)> {
1025        Ok((0, 0))
1026    }
1027
1028    /// C `asynInt64Base.c:99` default: report `low = high = 0` (see
1029    /// `get_bounds_int32`).
1030    fn get_bounds_int64(&self, _user: &AsynUser) -> AsynResult<(i64, i64)> {
1031        Ok((0, 0))
1032    }
1033
1034    fn read_float64(&mut self, user: &AsynUser) -> AsynResult<f64> {
1035        self.base()
1036            .params
1037            .get_float64_strict(user.reason, user.addr)
1038    }
1039
1040    fn write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1041        self.base_mut()
1042            .params
1043            .set_float64(user.reason, user.addr, value)?;
1044        self.base_mut().call_param_callbacks(user.addr)
1045    }
1046
1047    fn read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
1048        let s = self
1049            .base()
1050            .params
1051            .get_string_strict(user.reason, user.addr)?;
1052        let bytes = s.as_bytes();
1053        let n = bytes.len().min(buf.len());
1054        buf[..n].copy_from_slice(&bytes[..n]);
1055        Ok(n)
1056    }
1057
1058    fn write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1059        let s = String::from_utf8_lossy(data).into_owned();
1060        self.base_mut()
1061            .params
1062            .set_string(user.reason, user.addr, s)?;
1063        self.base_mut().call_param_callbacks(user.addr)?;
1064        Ok(data.len())
1065    }
1066
1067    fn read_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<u32> {
1068        let val = self
1069            .base()
1070            .params
1071            .get_uint32_strict(user.reason, user.addr)?;
1072        Ok(val & mask)
1073    }
1074
1075    fn write_uint32_digital(
1076        &mut self,
1077        user: &mut AsynUser,
1078        value: u32,
1079        mask: u32,
1080    ) -> AsynResult<()> {
1081        // The asynUInt32Digital write interface carries no forced interrupt
1082        // mask — changed bits derive from value^old (interrupt_mask = 0).
1083        self.base_mut()
1084            .params
1085            .set_uint32(user.reason, user.addr, value, mask, 0)?;
1086        self.base_mut().call_param_callbacks(user.addr)
1087    }
1088
1089    /// Configure rising / falling interrupt masks for a
1090    /// UInt32Digital parameter. C parity:
1091    /// `asynPortDriver::setInterruptUInt32Digital`
1092    /// (`asynPortDriver.cpp:2346-2369`) → routes to
1093    /// `paramList::setUInt32Interrupt`. The default delegates to the
1094    /// param store; drivers that need to push the configuration to
1095    /// hardware (e.g. real GPIB cards toggling SRQ enable) override
1096    /// it.
1097    fn set_interrupt_uint32_digital(
1098        &mut self,
1099        user: &AsynUser,
1100        mask: u32,
1101        reason: InterruptReason,
1102    ) -> AsynResult<()> {
1103        self.base_mut()
1104            .params
1105            .set_uint32_interrupt(user.reason, user.addr, mask, reason)
1106    }
1107
1108    /// Clear bits from rising AND falling masks. C parity:
1109    /// `asynPortDriver::clearInterruptUInt32Digital`
1110    /// (`asynPortDriver.cpp:2392-2415`). Mirrors C — the call does
1111    /// not take an `interruptReason`; both masks are cleared.
1112    fn clear_interrupt_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<()> {
1113        self.base_mut()
1114            .params
1115            .clear_uint32_interrupt(user.reason, user.addr, mask)
1116    }
1117
1118    /// Read the configured rising / falling / combined mask. C
1119    /// parity: `asynPortDriver::getInterruptUInt32Digital`
1120    /// (`asynPortDriver.cpp:2438-2461`).
1121    fn get_interrupt_uint32_digital(
1122        &self,
1123        user: &AsynUser,
1124        reason: InterruptReason,
1125    ) -> AsynResult<u32> {
1126        self.base()
1127            .params
1128            .get_uint32_interrupt(user.reason, user.addr, reason)
1129    }
1130
1131    // --- Enum I/O (cache-based defaults) ---
1132
1133    fn read_enum(&mut self, user: &AsynUser) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
1134        self.base().params.get_enum(user.reason, user.addr)
1135    }
1136
1137    fn write_enum(&mut self, user: &mut AsynUser, index: usize) -> AsynResult<()> {
1138        self.base_mut()
1139            .params
1140            .set_enum_index(user.reason, user.addr, index)?;
1141        self.base_mut().call_param_callbacks(user.addr)
1142    }
1143
1144    fn write_enum_choices(
1145        &mut self,
1146        user: &mut AsynUser,
1147        choices: Arc<[EnumEntry]>,
1148    ) -> AsynResult<()> {
1149        self.base_mut()
1150            .params
1151            .set_enum_choices(user.reason, user.addr, choices)?;
1152        self.base_mut().call_param_callbacks(user.addr)
1153    }
1154
1155    // --- GenericPointer I/O (cache-based defaults) ---
1156
1157    fn read_generic_pointer(&mut self, user: &AsynUser) -> AsynResult<Arc<dyn Any + Send + Sync>> {
1158        self.base()
1159            .params
1160            .get_generic_pointer(user.reason, user.addr)
1161    }
1162
1163    fn write_generic_pointer(
1164        &mut self,
1165        user: &mut AsynUser,
1166        value: Arc<dyn Any + Send + Sync>,
1167    ) -> AsynResult<()> {
1168        self.base_mut()
1169            .params
1170            .set_generic_pointer(user.reason, user.addr, value)?;
1171        self.base_mut().call_param_callbacks(user.addr)
1172    }
1173
1174    // --- Array I/O (default: not supported) ---
1175
1176    fn read_float64_array(&mut self, _user: &AsynUser, _buf: &mut [f64]) -> AsynResult<usize> {
1177        Err(AsynError::InterfaceNotSupported("asynFloat64Array".into()))
1178    }
1179
1180    fn write_float64_array(&mut self, user: &AsynUser, data: &[f64]) -> AsynResult<()> {
1181        self.base_mut()
1182            .params
1183            .set_float64_array(user.reason, user.addr, data.to_vec())?;
1184        self.base_mut().call_param_callbacks(user.addr)
1185    }
1186
1187    fn read_int32_array(&mut self, _user: &AsynUser, _buf: &mut [i32]) -> AsynResult<usize> {
1188        Err(AsynError::InterfaceNotSupported("asynInt32Array".into()))
1189    }
1190
1191    fn write_int32_array(&mut self, user: &AsynUser, data: &[i32]) -> AsynResult<()> {
1192        self.base_mut()
1193            .params
1194            .set_int32_array(user.reason, user.addr, data.to_vec())?;
1195        self.base_mut().call_param_callbacks(user.addr)
1196    }
1197
1198    fn read_int8_array(&mut self, _user: &AsynUser, _buf: &mut [i8]) -> AsynResult<usize> {
1199        Err(AsynError::InterfaceNotSupported("asynInt8Array".into()))
1200    }
1201
1202    fn write_int8_array(&mut self, user: &AsynUser, data: &[i8]) -> AsynResult<()> {
1203        self.base_mut()
1204            .params
1205            .set_int8_array(user.reason, user.addr, data.to_vec())?;
1206        self.base_mut().call_param_callbacks(user.addr)
1207    }
1208
1209    fn read_int16_array(&mut self, _user: &AsynUser, _buf: &mut [i16]) -> AsynResult<usize> {
1210        Err(AsynError::InterfaceNotSupported("asynInt16Array".into()))
1211    }
1212
1213    fn write_int16_array(&mut self, user: &AsynUser, data: &[i16]) -> AsynResult<()> {
1214        self.base_mut()
1215            .params
1216            .set_int16_array(user.reason, user.addr, data.to_vec())?;
1217        self.base_mut().call_param_callbacks(user.addr)
1218    }
1219
1220    fn read_int64_array(&mut self, _user: &AsynUser, _buf: &mut [i64]) -> AsynResult<usize> {
1221        Err(AsynError::InterfaceNotSupported("asynInt64Array".into()))
1222    }
1223
1224    fn write_int64_array(&mut self, user: &AsynUser, data: &[i64]) -> AsynResult<()> {
1225        self.base_mut()
1226            .params
1227            .set_int64_array(user.reason, user.addr, data.to_vec())?;
1228        self.base_mut().call_param_callbacks(user.addr)
1229    }
1230
1231    fn read_float32_array(&mut self, _user: &AsynUser, _buf: &mut [f32]) -> AsynResult<usize> {
1232        Err(AsynError::InterfaceNotSupported("asynFloat32Array".into()))
1233    }
1234
1235    fn write_float32_array(&mut self, user: &AsynUser, data: &[f32]) -> AsynResult<()> {
1236        self.base_mut()
1237            .params
1238            .set_float32_array(user.reason, user.addr, data.to_vec())?;
1239        self.base_mut().call_param_callbacks(user.addr)
1240    }
1241
1242    // --- I/O methods (worker thread calls these) ---
1243    // Default: delegate to cache-based read_*/write_* for backward compat.
1244    // Real I/O drivers override these for actual hardware access.
1245
1246    fn io_read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
1247        self.read_octet(user, buf)
1248    }
1249
1250    /// Octet read that also reports the end-of-message reason — C
1251    /// parity for `asynOctet::read(... int *eomReason)`
1252    /// (`asynOctet.h:38-40`). The default implementation delegates to
1253    /// [`Self::io_read_octet`] and reconstructs a synthetic
1254    /// [`EomReason`]: `CNT` when the buffer filled, `empty` otherwise.
1255    /// Drivers that have native EOM information
1256    /// (`asynOctetSyncIO::readRaw`, GPIB END, EOS match) must
1257    /// override this method so consumers — `asynRecord::EOMR`,
1258    /// `asynOctetSyncIO::readRaw` mirrors — receive the real flags.
1259    fn io_read_octet_eom(
1260        &mut self,
1261        user: &AsynUser,
1262        buf: &mut [u8],
1263    ) -> AsynResult<(usize, EomReason)> {
1264        let cap = buf.len();
1265        let n = self.io_read_octet(user, buf)?;
1266        let eom = if n >= cap && cap > 0 {
1267            EomReason::CNT
1268        } else {
1269            EomReason::empty()
1270        };
1271        Ok((n, eom))
1272    }
1273
1274    fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1275        self.write_octet(user, data)
1276    }
1277
1278    fn io_read_int32(&mut self, user: &AsynUser) -> AsynResult<i32> {
1279        self.read_int32(user)
1280    }
1281
1282    fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1283        self.write_int32(user, value)
1284    }
1285
1286    fn io_read_int64(&mut self, user: &AsynUser) -> AsynResult<i64> {
1287        self.read_int64(user)
1288    }
1289
1290    fn io_write_int64(&mut self, user: &mut AsynUser, value: i64) -> AsynResult<()> {
1291        self.write_int64(user, value)
1292    }
1293
1294    fn io_read_float64(&mut self, user: &AsynUser) -> AsynResult<f64> {
1295        self.read_float64(user)
1296    }
1297
1298    fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1299        self.write_float64(user, value)
1300    }
1301
1302    fn io_read_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<u32> {
1303        self.read_uint32_digital(user, mask)
1304    }
1305
1306    fn io_write_uint32_digital(
1307        &mut self,
1308        user: &mut AsynUser,
1309        value: u32,
1310        mask: u32,
1311    ) -> AsynResult<()> {
1312        self.write_uint32_digital(user, value, mask)
1313    }
1314
1315    fn io_flush(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
1316        Ok(())
1317    }
1318
1319    // --- Octet EOS (delegates to interpose stack by default) ---
1320    //
1321    // ## EOS connect-wait policy (C asyn issue #103)
1322    //
1323    // C asyn `asynOctetSyncIO::setInputEos` / `setOutputEos`
1324    // (`asynOctetSyncIO.c:300-321`, 346-367) call `lockPort` ahead of
1325    // the actual `setInputEos` — `lockPort` waits up to the user's
1326    // timeout for the port to be connected, by `epicsEventWait`-ing
1327    // on the connect event published from `connectIt`. On IOC init
1328    // and exit this serialises EOS configuration against the connect
1329    // task, but it also means a `setInputEos` issued before the port
1330    // has ever connected blocks the calling thread (issue #103
1331    // captured the symptom: IOC startup pauses for the full asyn
1332    // timeout when the device is off-line).
1333    //
1334    // The Rust path here is purely in-memory: `set_input_eos` and
1335    // `set_output_eos` write the bytes into `PortDriverBase` and the
1336    // EOS interpose stack reads from those fields at next read/write
1337    // time. No connect-wait, no lock contention with the connect
1338    // task — so issue #103's symptom cannot reproduce. If a future
1339    // refactor introduces a connect-gated EOS path (e.g. a driver
1340    // that owns the EOS state inside its connect()-allocated
1341    // resource), authors MUST keep the wait optional / bounded so
1342    // the connect-wait failure mode doesn't return.
1343
1344    fn set_input_eos(&mut self, eos: &[u8]) -> AsynResult<()> {
1345        if eos.len() > 2 {
1346            return Err(AsynError::Status {
1347                status: AsynStatus::Error,
1348                message: format!("illegal eoslen {}", eos.len()),
1349            });
1350        }
1351        // Single write owner for input EOS: `base.input_eos` is the
1352        // queryable cache (`get_input_eos`, binary-suppress save/restore),
1353        // and the same value is forwarded to the interpose stack so an
1354        // installed `EosInterpose` actually terminates reads on it. Empty
1355        // stack = no-op forward; C routes `setInputEos` the same way.
1356        let base = self.base_mut();
1357        base.input_eos = eos.to_vec();
1358        base.interpose_octet.set_input_eos(eos);
1359        Ok(())
1360    }
1361
1362    fn get_input_eos(&self) -> Vec<u8> {
1363        self.base().input_eos.clone()
1364    }
1365
1366    fn set_output_eos(&mut self, eos: &[u8]) -> AsynResult<()> {
1367        if eos.len() > 2 {
1368            return Err(AsynError::Status {
1369                status: AsynStatus::Error,
1370                message: format!("illegal eoslen {}", eos.len()),
1371            });
1372        }
1373        // Single write owner for output EOS (see `set_input_eos`): cache in
1374        // base and forward to the interpose stack so `EosInterpose` appends
1375        // the terminator on write.
1376        let base = self.base_mut();
1377        base.output_eos = eos.to_vec();
1378        base.interpose_octet.set_output_eos(eos);
1379        Ok(())
1380    }
1381
1382    fn get_output_eos(&self) -> Vec<u8> {
1383        self.base().output_eos.clone()
1384    }
1385
1386    // --- Lifecycle ---
1387
1388    /// Called when the port is being shut down. Drivers override this
1389    /// to release hardware resources. Matches C asynPortDriver::shutdownPortDriver().
1390    fn shutdown(&mut self) -> AsynResult<()> {
1391        Ok(())
1392    }
1393
1394    // --- drvUser ---
1395
1396    /// Resolve a record's driver-info string (and its asyn `addr`) to a
1397    /// [`DrvUserInfo`] at bind time — the asyn-rs analogue of C `drvUserCreate`.
1398    ///
1399    /// Takes `&mut self` so a driver can register a parameter on demand from the
1400    /// resolved drvInfo (C Autoparam lazy creation) rather than requiring it be
1401    /// declared up front, and `addr` so a multi-device driver can reject an
1402    /// out-of-range address at bind time (C `drvUserCreate` runs `checkOffset`,
1403    /// drvModbusAsyn.cpp:378-384) instead of alarming on every I/O.
1404    ///
1405    /// Default: look up the shared reason by parameter name; ignore `addr`.
1406    fn drv_user_create(&mut self, drv_info: &str, _addr: i32) -> AsynResult<DrvUserInfo> {
1407        let reason = self
1408            .base()
1409            .params
1410            .find_param(drv_info)
1411            .ok_or_else(|| AsynError::ParamNotFound(drv_info.to_string()))?;
1412        Ok(DrvUserInfo::from_reason(reason))
1413    }
1414
1415    // --- Capabilities ---
1416
1417    /// Declare the capabilities this driver supports.
1418    /// Default implementation includes all scalar read/write operations.
1419    fn capabilities(&self) -> Vec<crate::interfaces::Capability> {
1420        crate::interfaces::default_capabilities()
1421    }
1422
1423    /// Check if this driver supports a specific capability.
1424    fn supports(&self, cap: crate::interfaces::Capability) -> bool {
1425        self.capabilities().contains(&cap)
1426    }
1427
1428    fn init(&mut self) -> AsynResult<()> {
1429        Ok(())
1430    }
1431}
1432
1433#[cfg(test)]
1434mod tests {
1435    use super::*;
1436    struct TestDriver {
1437        base: PortDriverBase,
1438    }
1439
1440    impl TestDriver {
1441        fn new() -> Self {
1442            let mut base = PortDriverBase::new("test", 1, PortFlags::default());
1443            base.create_param("VAL", ParamType::Int32).unwrap();
1444            base.create_param("TEMP", ParamType::Float64).unwrap();
1445            base.create_param("MSG", ParamType::Octet).unwrap();
1446            base.create_param("BITS", ParamType::UInt32Digital).unwrap();
1447            Self { base }
1448        }
1449    }
1450
1451    impl PortDriver for TestDriver {
1452        fn base(&self) -> &PortDriverBase {
1453            &self.base
1454        }
1455        fn base_mut(&mut self) -> &mut PortDriverBase {
1456            &mut self.base
1457        }
1458    }
1459
1460    #[test]
1461    fn test_default_read_write_int32() {
1462        let mut drv = TestDriver::new();
1463        let mut user = AsynUser::new(0);
1464        drv.write_int32(&mut user, 42).unwrap();
1465        let user = AsynUser::new(0);
1466        assert_eq!(drv.read_int32(&user).unwrap(), 42);
1467    }
1468
1469    #[test]
1470    fn test_default_read_write_float64() {
1471        let mut drv = TestDriver::new();
1472        let mut user = AsynUser::new(1);
1473        drv.write_float64(&mut user, 3.14).unwrap();
1474        let user = AsynUser::new(1);
1475        assert!((drv.read_float64(&user).unwrap() - 3.14).abs() < 1e-10);
1476    }
1477
1478    #[test]
1479    fn test_default_read_write_octet() {
1480        let mut drv = TestDriver::new();
1481        let mut user = AsynUser::new(2);
1482        drv.write_octet(&mut user, b"hello").unwrap();
1483        let user = AsynUser::new(2);
1484        let mut buf = [0u8; 32];
1485        let n = drv.read_octet(&user, &mut buf).unwrap();
1486        assert_eq!(&buf[..n], b"hello");
1487    }
1488
1489    #[test]
1490    fn test_default_read_write_uint32() {
1491        let mut drv = TestDriver::new();
1492        let mut user = AsynUser::new(3);
1493        drv.write_uint32_digital(&mut user, 0xFF, 0x0F).unwrap();
1494        let user = AsynUser::new(3);
1495        assert_eq!(drv.read_uint32_digital(&user, 0xFF).unwrap(), 0x0F);
1496    }
1497
1498    #[test]
1499    fn test_connect_disconnect() {
1500        let mut drv = TestDriver::new();
1501        let user = AsynUser::default();
1502        assert!(drv.base().connected);
1503        drv.disconnect(&user).unwrap();
1504        assert!(!drv.base().connected);
1505        drv.connect(&user).unwrap();
1506        assert!(drv.base().connected);
1507    }
1508
1509    #[test]
1510    fn test_drv_user_create() {
1511        let mut drv = TestDriver::new();
1512        assert_eq!(drv.drv_user_create("VAL", 0).unwrap().reason, 0);
1513        assert_eq!(drv.drv_user_create("TEMP", 0).unwrap().reason, 1);
1514        assert!(drv.drv_user_create("NOPE", 0).is_err());
1515    }
1516
1517    #[test]
1518    fn test_call_param_callbacks() {
1519        let mut drv = TestDriver::new();
1520        let mut rx = drv.base_mut().interrupts.subscribe_async();
1521
1522        drv.base_mut().set_int32_param(0, 0, 100).unwrap();
1523        drv.base_mut().set_float64_param(1, 0, 2.0).unwrap();
1524        drv.base_mut().call_param_callbacks(0).unwrap();
1525
1526        let v1 = rx.try_recv().unwrap();
1527        assert_eq!(v1.reason, 0);
1528        let v2 = rx.try_recv().unwrap();
1529        assert_eq!(v2.reason, 1);
1530        assert!(rx.try_recv().is_err());
1531    }
1532
1533    #[test]
1534    fn flush_skips_undefined_scalar_but_keeps_array_trigger() {
1535        // C asynPortDriver.cpp:845 — a status/alarm change (or bare
1536        // mark_changed) on a never-set scalar consumes the changed flag
1537        // but fires no callback. Array/generic-pointer triggers have no
1538        // callCallbacks analog (:846-865 switch is scalar-only) and must
1539        // still fire even while undefined.
1540        let mut drv = TestDriver::new();
1541        let arr = drv
1542            .base_mut()
1543            .create_param("ARR", ParamType::Int32Array)
1544            .unwrap();
1545        let mut rx = drv.base_mut().interrupts.subscribe_async();
1546
1547        // Status change on the never-set scalar VAL (index 0): marks it
1548        // changed but it stays undefined.
1549        drv.base_mut()
1550            .params
1551            .set_param_status(0, 0, AsynStatus::Error, 0, 0)
1552            .unwrap();
1553        // Mark the never-set array param changed: an override-served trigger.
1554        drv.base_mut().mark_param_changed(arr, 0).unwrap();
1555
1556        drv.base_mut().call_param_callbacks(0).unwrap();
1557
1558        // Only the array trigger is delivered; the undefined scalar is gated.
1559        let iv = rx.try_recv().unwrap();
1560        assert_eq!(
1561            iv.reason, arr,
1562            "array trigger must still fire while undefined"
1563        );
1564        assert!(
1565            rx.try_recv().is_err(),
1566            "undefined scalar must not emit an I/O Intr"
1567        );
1568
1569        // Once the scalar is defined, a subsequent change does fire.
1570        drv.base_mut().set_int32_param(0, 0, 7).unwrap();
1571        drv.base_mut().call_param_callbacks(0).unwrap();
1572        let iv2 = rx.try_recv().unwrap();
1573        assert_eq!(iv2.reason, 0, "defined scalar must fire");
1574    }
1575
1576    #[test]
1577    fn uint32_callback_mask_does_not_leak_across_flushes() {
1578        // C resets uInt32CallbackMask = 0 after each uint32Callback
1579        // (asynPortDriver.cpp:855): a second flush must deliver only the
1580        // bits changed since the first, never the accumulated history.
1581        let mut drv = TestDriver::new();
1582        let mut rx = drv.base_mut().interrupts.subscribe_async();
1583
1584        // flush 1: change bit 0 on BITS (param index 3).
1585        drv.base_mut()
1586            .params
1587            .set_uint32(3, 0, 0x01, 0x01, 0)
1588            .unwrap();
1589        drv.base_mut().call_param_callbacks(0).unwrap();
1590        let iv1 = rx.try_recv().unwrap();
1591        assert_eq!(iv1.reason, 3);
1592        assert_eq!(iv1.uint32_changed_mask, 0x01);
1593
1594        // flush 2: change bit 1 only — must deliver 0x02, not 0x03.
1595        drv.base_mut()
1596            .params
1597            .set_uint32(3, 0, 0x02, 0x02, 0)
1598            .unwrap();
1599        drv.base_mut().call_param_callbacks(0).unwrap();
1600        let iv2 = rx.try_recv().unwrap();
1601        assert_eq!(
1602            iv2.uint32_changed_mask, 0x02,
1603            "second flush must not leak flush-1 bits via an un-reset mask"
1604        );
1605        assert_eq!(
1606            drv.base().params.get_uint32_interrupt_mask(3, 0).unwrap(),
1607            0,
1608            "the flush must consume (reset) the callback mask"
1609        );
1610    }
1611
1612    #[test]
1613    fn test_call_param_callbacks_propagates_aux_status_and_alarm() {
1614        // C parity: asynPortDriver.cpp:631-642 writes the param's stored
1615        // status / alarmStatus / alarmSeverity onto the subscriber's
1616        // pasynUser before invoking the callback. The Rust port carries
1617        // those fields on InterruptValue.
1618        let mut drv = TestDriver::new();
1619        let mut rx = drv.base_mut().interrupts.subscribe_async();
1620
1621        drv.base_mut().set_int32_param(0, 0, 99).unwrap();
1622        drv.base_mut()
1623            .params
1624            .set_param_status(0, 0, crate::error::AsynStatus::Timeout, 4, 2)
1625            .unwrap();
1626        drv.base_mut().call_param_callbacks(0).unwrap();
1627
1628        let iv = rx.try_recv().unwrap();
1629        assert_eq!(iv.reason, 0);
1630        assert!(matches!(iv.aux_status, crate::error::AsynStatus::Timeout));
1631        assert_eq!(iv.alarm_status, 4);
1632        assert_eq!(iv.alarm_severity, 2);
1633    }
1634
1635    #[test]
1636    fn test_call_param_callback_single_propagates_aux_status() {
1637        // Mirror for the single-flush path (call_param_callback).
1638        let mut drv = TestDriver::new();
1639        let mut rx = drv.base_mut().interrupts.subscribe_async();
1640
1641        drv.base_mut().set_int32_param(0, 0, 1).unwrap();
1642        drv.base_mut()
1643            .params
1644            .set_param_status(0, 0, crate::error::AsynStatus::Disconnected, 7, 3)
1645            .unwrap();
1646        drv.base_mut().call_param_callback(0, 0).unwrap();
1647
1648        let iv = rx.try_recv().unwrap();
1649        assert!(matches!(
1650            iv.aux_status,
1651            crate::error::AsynStatus::Disconnected
1652        ));
1653        assert_eq!(iv.alarm_status, 7);
1654        assert_eq!(iv.alarm_severity, 3);
1655    }
1656
1657    #[test]
1658    fn test_no_callback_for_unchanged() {
1659        let mut drv = TestDriver::new();
1660        let mut rx = drv.base_mut().interrupts.subscribe_async();
1661
1662        drv.base_mut().set_int32_param(0, 0, 5).unwrap();
1663        drv.base_mut().call_param_callbacks(0).unwrap();
1664        let _ = rx.try_recv().unwrap(); // consume
1665
1666        // Set same value — no interrupt
1667        drv.base_mut().set_int32_param(0, 0, 5).unwrap();
1668        drv.base_mut().call_param_callbacks(0).unwrap();
1669        assert!(rx.try_recv().is_err());
1670    }
1671
1672    #[test]
1673    fn test_array_not_supported_by_default() {
1674        let mut drv = TestDriver::new();
1675        let user = AsynUser::new(0);
1676        let mut buf = [0f64; 10];
1677        assert!(drv.read_float64_array(&user, &mut buf).is_err());
1678        assert!(drv.write_float64_array(&user, &[1.0]).is_err());
1679    }
1680
1681    #[test]
1682    fn test_option_set_get() {
1683        let mut drv = TestDriver::new();
1684        drv.set_option("baud", "9600").unwrap();
1685        assert_eq!(drv.get_option("baud").unwrap(), "9600");
1686        drv.set_option("baud", "115200").unwrap();
1687        assert_eq!(drv.get_option("baud").unwrap(), "115200");
1688    }
1689
1690    #[test]
1691    fn test_option_not_found() {
1692        let drv = TestDriver::new();
1693        let err = drv.get_option("nonexistent").unwrap_err();
1694        assert!(matches!(err, AsynError::OptionNotFound(_)));
1695    }
1696
1697    #[test]
1698    fn test_report_no_panic() {
1699        let mut drv = TestDriver::new();
1700        drv.set_option("testkey", "testval").unwrap();
1701        drv.base_mut().set_int32_param(0, 0, 42).unwrap();
1702        for level in 0..=3 {
1703            drv.report(level);
1704        }
1705    }
1706
1707    #[test]
1708    fn test_callback_uses_param_timestamp() {
1709        let mut drv = TestDriver::new();
1710        let mut rx = drv.base_mut().interrupts.subscribe_async();
1711
1712        let custom_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
1713        drv.base_mut().set_int32_param(0, 0, 77).unwrap();
1714        drv.base_mut().set_param_timestamp(0, 0, custom_ts).unwrap();
1715        drv.base_mut().call_param_callbacks(0).unwrap();
1716
1717        let v = rx.try_recv().unwrap();
1718        assert_eq!(v.reason, 0);
1719        assert_eq!(v.timestamp, custom_ts);
1720    }
1721
1722    #[test]
1723    fn test_default_read_write_enum() {
1724        use crate::param::EnumEntry;
1725
1726        let mut base = PortDriverBase::new("test_enum", 1, PortFlags::default());
1727        base.create_param("MODE", ParamType::Enum).unwrap();
1728
1729        struct EnumDriver {
1730            base: PortDriverBase,
1731        }
1732        impl PortDriver for EnumDriver {
1733            fn base(&self) -> &PortDriverBase {
1734                &self.base
1735            }
1736            fn base_mut(&mut self) -> &mut PortDriverBase {
1737                &mut self.base
1738            }
1739        }
1740
1741        let mut drv = EnumDriver { base };
1742        let choices: Arc<[EnumEntry]> = Arc::from(vec![
1743            EnumEntry {
1744                string: "Off".into(),
1745                value: 0,
1746                severity: 0,
1747            },
1748            EnumEntry {
1749                string: "On".into(),
1750                value: 1,
1751                severity: 0,
1752            },
1753        ]);
1754        let mut user = AsynUser::new(0);
1755        drv.write_enum_choices(&mut user, choices).unwrap();
1756        drv.write_enum(&mut user, 1).unwrap();
1757        let (idx, ch) = drv.read_enum(&AsynUser::new(0)).unwrap();
1758        assert_eq!(idx, 1);
1759        assert_eq!(ch[1].string, "On");
1760    }
1761
1762    #[test]
1763    fn test_enum_callback() {
1764        use crate::param::{EnumEntry, ParamValue};
1765
1766        let mut base = PortDriverBase::new("test_enum_cb", 1, PortFlags::default());
1767        base.create_param("MODE", ParamType::Enum).unwrap();
1768        let mut rx = base.interrupts.subscribe_async();
1769
1770        struct EnumDriver {
1771            base: PortDriverBase,
1772        }
1773        impl PortDriver for EnumDriver {
1774            fn base(&self) -> &PortDriverBase {
1775                &self.base
1776            }
1777            fn base_mut(&mut self) -> &mut PortDriverBase {
1778                &mut self.base
1779            }
1780        }
1781
1782        let mut drv = EnumDriver { base };
1783        let choices: Arc<[EnumEntry]> = Arc::from(vec![
1784            EnumEntry {
1785                string: "A".into(),
1786                value: 0,
1787                severity: 0,
1788            },
1789            EnumEntry {
1790                string: "B".into(),
1791                value: 1,
1792                severity: 0,
1793            },
1794        ]);
1795        drv.base_mut()
1796            .set_enum_choices_param(0, 0, choices)
1797            .unwrap();
1798        drv.base_mut().set_enum_index_param(0, 0, 1).unwrap();
1799        drv.base_mut().call_param_callbacks(0).unwrap();
1800
1801        let v = rx.try_recv().unwrap();
1802        assert_eq!(v.reason, 0);
1803        assert!(matches!(v.value, ParamValue::Enum { index: 1, .. }));
1804    }
1805
1806    #[test]
1807    fn test_default_read_write_generic_pointer() {
1808        let mut base = PortDriverBase::new("test_gp", 1, PortFlags::default());
1809        base.create_param("PTR", ParamType::GenericPointer).unwrap();
1810
1811        struct GpDriver {
1812            base: PortDriverBase,
1813        }
1814        impl PortDriver for GpDriver {
1815            fn base(&self) -> &PortDriverBase {
1816                &self.base
1817            }
1818            fn base_mut(&mut self) -> &mut PortDriverBase {
1819                &mut self.base
1820            }
1821        }
1822
1823        let mut drv = GpDriver { base };
1824        let data: Arc<dyn std::any::Any + Send + Sync> = Arc::new(99i32);
1825        let mut user = AsynUser::new(0);
1826        drv.write_generic_pointer(&mut user, data).unwrap();
1827        let val = drv.read_generic_pointer(&AsynUser::new(0)).unwrap();
1828        assert_eq!(*val.downcast_ref::<i32>().unwrap(), 99);
1829    }
1830
1831    #[test]
1832    fn test_generic_pointer_callback() {
1833        use crate::param::ParamValue;
1834
1835        let mut base = PortDriverBase::new("test_gp_cb", 1, PortFlags::default());
1836        base.create_param("PTR", ParamType::GenericPointer).unwrap();
1837        let mut rx = base.interrupts.subscribe_async();
1838
1839        struct GpDriver {
1840            base: PortDriverBase,
1841        }
1842        impl PortDriver for GpDriver {
1843            fn base(&self) -> &PortDriverBase {
1844                &self.base
1845            }
1846            fn base_mut(&mut self) -> &mut PortDriverBase {
1847                &mut self.base
1848            }
1849        }
1850
1851        let mut drv = GpDriver { base };
1852        let data: Arc<dyn std::any::Any + Send + Sync> = Arc::new(vec![1, 2, 3]);
1853        drv.base_mut()
1854            .set_generic_pointer_param(0, 0, data)
1855            .unwrap();
1856        drv.base_mut().call_param_callbacks(0).unwrap();
1857
1858        let v = rx.try_recv().unwrap();
1859        assert_eq!(v.reason, 0);
1860        assert!(matches!(v.value, ParamValue::GenericPointer(_)));
1861    }
1862
1863    #[test]
1864    fn test_interpose_push_requires_lock() {
1865        use crate::interpose::{OctetInterpose, OctetNext, OctetReadResult};
1866        use parking_lot::Mutex;
1867        use std::sync::Arc;
1868
1869        struct NoopInterpose;
1870        impl OctetInterpose for NoopInterpose {
1871            fn read(
1872                &mut self,
1873                user: &AsynUser,
1874                buf: &mut [u8],
1875                next: &mut dyn OctetNext,
1876            ) -> AsynResult<OctetReadResult> {
1877                next.read(user, buf)
1878            }
1879            fn write(
1880                &mut self,
1881                user: &mut AsynUser,
1882                data: &[u8],
1883                next: &mut dyn OctetNext,
1884            ) -> AsynResult<usize> {
1885                next.write(user, data)
1886            }
1887            fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
1888                next.flush(user)
1889            }
1890        }
1891
1892        let port: Arc<Mutex<dyn PortDriver>> = Arc::new(Mutex::new(TestDriver::new()));
1893
1894        {
1895            let mut guard = port.lock();
1896            guard
1897                .base_mut()
1898                .push_octet_interpose(Box::new(NoopInterpose));
1899            assert_eq!(guard.base().interpose_octet.len(), 1);
1900        }
1901    }
1902
1903    /// The `set_input_eos` write owner must forward the terminator to an
1904    /// installed `EosInterpose`, not just cache it in `base.input_eos` —
1905    /// otherwise a runtime IEOS change never terminates reads (the F7 gap).
1906    #[test]
1907    fn test_set_input_eos_reaches_installed_interpose() {
1908        use crate::interpose::eos::EosInterpose;
1909        use crate::interpose::{EomReason, OctetNext, OctetReadResult};
1910
1911        struct RawSource {
1912            data: Vec<u8>,
1913            pos: usize,
1914        }
1915        impl OctetNext for RawSource {
1916            fn read(&mut self, _u: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
1917                let avail = self.data.len() - self.pos;
1918                let n = avail.min(buf.len());
1919                buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
1920                self.pos += n;
1921                Ok(OctetReadResult {
1922                    nbytes_transferred: n,
1923                    eom_reason: EomReason::CNT,
1924                })
1925            }
1926            fn write(&mut self, _u: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
1927                Ok(data.len())
1928            }
1929            fn flush(&mut self, _u: &mut AsynUser) -> AsynResult<()> {
1930                Ok(())
1931            }
1932        }
1933
1934        let mut drv = TestDriver::new();
1935        drv.base_mut()
1936            .push_octet_interpose(Box::new(EosInterpose::default()));
1937
1938        // Set IEOS through the driver trait: caches in base AND must reach
1939        // the interpose.
1940        drv.set_input_eos(b"\n").unwrap();
1941        assert_eq!(drv.base().input_eos, b"\n");
1942
1943        let user = AsynUser::default();
1944        // "ab\n" exactly: the EOS read returns "ab" and leaves no read-ahead
1945        // in the interpose buffer, so the cleared-EOS read below genuinely
1946        // reads the next source fresh.
1947        let mut src = RawSource {
1948            data: b"ab\n".to_vec(),
1949            pos: 0,
1950        };
1951        let mut buf = [0u8; 16];
1952        let r = drv
1953            .base_mut()
1954            .interpose_octet
1955            .dispatch_read(&user, &mut buf, &mut src)
1956            .unwrap();
1957        assert_eq!(&buf[..r.nbytes_transferred], b"ab");
1958        assert!(r.eom_reason.contains(EomReason::EOS));
1959
1960        // Clearing IEOS (binary-suppress path) must also reach the interpose:
1961        // the read then passes through with no EOS termination.
1962        drv.set_input_eos(b"").unwrap();
1963        assert_eq!(drv.base().input_eos, b"");
1964        let mut src2 = RawSource {
1965            data: b"xy\nz".to_vec(),
1966            pos: 0,
1967        };
1968        let mut buf2 = [0u8; 16];
1969        let r2 = drv
1970            .base_mut()
1971            .interpose_octet
1972            .dispatch_read(&user, &mut buf2, &mut src2)
1973            .unwrap();
1974        assert_eq!(&buf2[..r2.nbytes_transferred], b"xy\nz");
1975        assert!(!r2.eom_reason.contains(EomReason::EOS));
1976    }
1977
1978    #[test]
1979    fn test_default_read_write_int64() {
1980        let mut base = PortDriverBase::new("test_i64", 1, PortFlags::default());
1981        base.create_param("BIG", ParamType::Int64).unwrap();
1982
1983        struct I64Driver {
1984            base: PortDriverBase,
1985        }
1986        impl PortDriver for I64Driver {
1987            fn base(&self) -> &PortDriverBase {
1988                &self.base
1989            }
1990            fn base_mut(&mut self) -> &mut PortDriverBase {
1991                &mut self.base
1992            }
1993        }
1994
1995        let mut drv = I64Driver { base };
1996        let mut user = AsynUser::new(0);
1997        drv.write_int64(&mut user, i64::MAX).unwrap();
1998        assert_eq!(drv.read_int64(&AsynUser::new(0)).unwrap(), i64::MAX);
1999    }
2000
2001    #[test]
2002    fn test_get_bounds_int64_default() {
2003        let base = PortDriverBase::new("test_bounds", 1, PortFlags::default());
2004        struct BoundsDriver {
2005            base: PortDriverBase,
2006        }
2007        impl PortDriver for BoundsDriver {
2008            fn base(&self) -> &PortDriverBase {
2009                &self.base
2010            }
2011            fn base_mut(&mut self) -> &mut PortDriverBase {
2012                &mut self.base
2013            }
2014        }
2015        let drv = BoundsDriver { base };
2016        let (lo, hi) = drv.get_bounds_int64(&AsynUser::default()).unwrap();
2017        // C asynInt64Base.c:99 default: *low = *high = 0 (so a driver
2018        // that does not implement getBounds skips LINEAR ESLO/EOFF).
2019        assert_eq!(lo, 0);
2020        assert_eq!(hi, 0);
2021    }
2022
2023    #[test]
2024    fn test_per_addr_device_state() {
2025        let mut base = PortDriverBase::new(
2026            "multi",
2027            4,
2028            PortFlags {
2029                multi_device: true,
2030                can_block: false,
2031                destructible: true,
2032            },
2033        );
2034        base.create_param("V", ParamType::Int32).unwrap();
2035
2036        // Default: all connected
2037        assert!(base.is_device_connected(0));
2038        assert!(base.is_device_connected(1));
2039
2040        // Disable addr 1
2041        base.device_state(1).enabled = false;
2042        assert!(base.check_ready_addr(0).is_ok());
2043        let err = base.check_ready_addr(1).unwrap_err();
2044        assert!(format!("{err}").contains("disabled"));
2045
2046        // Disconnect addr 2
2047        base.device_state(2).connected = false;
2048        let err = base.check_ready_addr(2).unwrap_err();
2049        assert!(format!("{err}").contains("disconnected"));
2050    }
2051
2052    #[test]
2053    fn test_per_addr_single_device_ignored() {
2054        let mut base = PortDriverBase::new("single", 1, PortFlags::default());
2055        base.create_param("V", ParamType::Int32).unwrap();
2056        // For single-device, per-addr check passes even if no device state
2057        assert!(base.check_ready_addr(0).is_ok());
2058    }
2059
2060    #[test]
2061    fn test_timestamp_source() {
2062        let mut base = PortDriverBase::new("ts_test", 1, PortFlags::default());
2063        base.create_param("V", ParamType::Int32).unwrap();
2064
2065        let fixed_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(999999);
2066        base.register_timestamp_source(move || fixed_ts);
2067
2068        assert_eq!(base.current_timestamp(), fixed_ts);
2069    }
2070
2071    #[test]
2072    fn test_timestamp_source_in_callbacks() {
2073        let mut base = PortDriverBase::new("ts_cb", 1, PortFlags::default());
2074        base.create_param("V", ParamType::Int32).unwrap();
2075        let mut rx = base.interrupts.subscribe_async();
2076
2077        let fixed_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(123456);
2078        base.register_timestamp_source(move || fixed_ts);
2079
2080        struct TsDriver {
2081            base: PortDriverBase,
2082        }
2083        impl PortDriver for TsDriver {
2084            fn base(&self) -> &PortDriverBase {
2085                &self.base
2086            }
2087            fn base_mut(&mut self) -> &mut PortDriverBase {
2088                &mut self.base
2089            }
2090        }
2091        let mut drv = TsDriver { base };
2092        drv.base_mut().set_int32_param(0, 0, 42).unwrap();
2093        drv.base_mut().call_param_callbacks(0).unwrap();
2094
2095        let v = rx.try_recv().unwrap();
2096        // Should use fixed_ts since no per-param timestamp is set
2097        assert_eq!(v.timestamp, fixed_ts);
2098    }
2099
2100    #[test]
2101    fn test_queue_priority_connect() {
2102        assert!(QueuePriority::Connect > QueuePriority::High);
2103    }
2104
2105    #[test]
2106    fn test_port_flags_destructible_default_is_opt_in() {
2107        // C asyn parity: ASYN_DESTRUCTIBLE (0x0004, asynDriver.h:97) is
2108        // a `registerPort` attribute that callers opt into. Default
2109        // must be false so drivers don't accidentally accept a
2110        // shutdownPort call. PortDriver authors that want shutdown
2111        // support set `destructible: true` explicitly.
2112        let flags = PortFlags::default();
2113        assert!(
2114            !flags.destructible,
2115            "destructible must be opt-in (C parity)"
2116        );
2117    }
2118
2119    #[test]
2120    fn shutdown_lifecycle_refuses_non_destructible() {
2121        let mut base = PortDriverBase::new(
2122            "p_nondestr",
2123            1,
2124            PortFlags {
2125                multi_device: false,
2126                can_block: false,
2127                destructible: false,
2128            },
2129        );
2130        match base.shutdown_lifecycle() {
2131            Err(AsynError::Status { message, .. }) => {
2132                assert!(message.contains("ASYN_DESTRUCTIBLE"), "msg={message}");
2133            }
2134            other => panic!("expected ASYN_DESTRUCTIBLE refusal, got {other:?}"),
2135        }
2136        assert!(
2137            !base.is_defunct(),
2138            "non-destructible port must not flip defunct"
2139        );
2140        assert!(base.is_enabled(), "non-destructible port must stay enabled");
2141    }
2142
2143    #[test]
2144    fn shutdown_lifecycle_marks_destructible_defunct_and_idempotent() {
2145        let mut base = PortDriverBase::new(
2146            "p_destr",
2147            1,
2148            PortFlags {
2149                multi_device: false,
2150                can_block: false,
2151                destructible: true,
2152            },
2153        );
2154        assert!(base.is_enabled());
2155        assert!(!base.is_defunct());
2156        base.shutdown_lifecycle().unwrap();
2157        assert!(
2158            !base.is_enabled(),
2159            "shutdown_lifecycle must flip enabled=false"
2160        );
2161        assert!(
2162            base.is_defunct(),
2163            "shutdown_lifecycle must flip defunct=true"
2164        );
2165        // Idempotent — second call is Ok and leaves state unchanged.
2166        base.shutdown_lifecycle().unwrap();
2167        assert!(base.is_defunct());
2168        // check_ready surfaces the defunct state for every request.
2169        match base.check_ready() {
2170            Err(AsynError::Status { message, .. }) => {
2171                assert!(message.contains("defunct"), "msg={message}");
2172            }
2173            other => panic!("expected defunct error, got {other:?}"),
2174        }
2175    }
2176
2177    // --- Phase 2B: per-addr connect/disconnect/enable/disable ---
2178
2179    #[test]
2180    fn test_connect_addr() {
2181        let mut base = PortDriverBase::new(
2182            "multi_conn",
2183            4,
2184            PortFlags {
2185                multi_device: true,
2186                can_block: false,
2187                destructible: true,
2188            },
2189        );
2190        base.create_param("V", ParamType::Int32).unwrap();
2191
2192        base.disconnect_addr(1);
2193        assert!(!base.is_device_connected(1));
2194        assert!(base.check_ready_addr(1).is_err());
2195
2196        base.connect_addr(1);
2197        assert!(base.is_device_connected(1));
2198        assert!(base.check_ready_addr(1).is_ok());
2199    }
2200
2201    #[test]
2202    fn test_enable_disable_addr() {
2203        let mut base = PortDriverBase::new(
2204            "multi_en",
2205            4,
2206            PortFlags {
2207                multi_device: true,
2208                can_block: false,
2209                destructible: true,
2210            },
2211        );
2212        base.create_param("V", ParamType::Int32).unwrap();
2213
2214        base.disable_addr(2);
2215        let err = base.check_ready_addr(2).unwrap_err();
2216        assert!(format!("{err}").contains("disabled"));
2217
2218        base.enable_addr(2);
2219        assert!(base.check_ready_addr(2).is_ok());
2220    }
2221
2222    #[test]
2223    fn test_port_level_overrides_addr() {
2224        let mut base = PortDriverBase::new(
2225            "multi_override",
2226            4,
2227            PortFlags {
2228                multi_device: true,
2229                can_block: false,
2230                destructible: true,
2231            },
2232        );
2233        base.create_param("V", ParamType::Int32).unwrap();
2234
2235        // Port-level disabled overrides addr-level enabled
2236        base.enabled = false;
2237        base.enable_addr(0); // addr 0 is enabled, but port is disabled
2238        let err = base.check_ready_addr(0).unwrap_err();
2239        assert!(format!("{err}").contains("disabled"));
2240    }
2241
2242    #[test]
2243    fn test_per_addr_exception_announced() {
2244        use std::sync::atomic::{AtomicI32, Ordering};
2245
2246        let mut base = PortDriverBase::new(
2247            "multi_exc",
2248            4,
2249            PortFlags {
2250                multi_device: true,
2251                can_block: false,
2252                destructible: true,
2253            },
2254        );
2255        base.create_param("V", ParamType::Int32).unwrap();
2256
2257        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
2258        base.exception_sink = Some(exc_mgr.clone());
2259
2260        let last_addr = Arc::new(AtomicI32::new(-99));
2261        let last_addr2 = last_addr.clone();
2262        exc_mgr.add_callback(move |event| {
2263            last_addr2.store(event.addr, Ordering::Relaxed);
2264        });
2265
2266        base.disconnect_addr(3);
2267        assert_eq!(last_addr.load(Ordering::Relaxed), 3);
2268
2269        base.enable_addr(2);
2270        assert_eq!(last_addr.load(Ordering::Relaxed), 2);
2271    }
2272
2273    /// C parity (asynManager.c:2151-2160 exceptionConnect,
2274    /// :2174-2185 exceptionDisconnect): redundant connect/disconnect
2275    /// on a port already in that state must NOT fan out a duplicate
2276    /// `asynExceptionConnect`. Subscribers depend on the event
2277    /// edge — duplicate fan-out causes them to e.g. re-subscribe or
2278    /// re-arm timers that should fire exactly once per transition.
2279    #[test]
2280    fn test_connect_disconnect_announce_only_on_transition() {
2281        use std::sync::atomic::{AtomicUsize, Ordering};
2282
2283        let mut base = PortDriverBase::new(
2284            "edge",
2285            4,
2286            PortFlags {
2287                multi_device: true,
2288                can_block: false,
2289                destructible: true,
2290            },
2291        );
2292        base.create_param("V", ParamType::Int32).unwrap();
2293        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
2294        base.exception_sink = Some(exc_mgr.clone());
2295
2296        let connect_hits = Arc::new(AtomicUsize::new(0));
2297        let hits2 = connect_hits.clone();
2298        exc_mgr.add_callback(move |event| {
2299            if event.exception == AsynException::Connect {
2300                hits2.fetch_add(1, Ordering::Relaxed);
2301            }
2302        });
2303
2304        // device starts connected by DeviceState::default — a redundant
2305        // connect_addr is a no-op.
2306        base.connect_addr(2);
2307        assert_eq!(
2308            connect_hits.load(Ordering::Relaxed),
2309            0,
2310            "redundant connect_addr must not fan out"
2311        );
2312
2313        // First transition fires once.
2314        base.disconnect_addr(2);
2315        assert_eq!(connect_hits.load(Ordering::Relaxed), 1);
2316
2317        // Redundant disconnect is silent.
2318        base.disconnect_addr(2);
2319        assert_eq!(
2320            connect_hits.load(Ordering::Relaxed),
2321            1,
2322            "redundant disconnect_addr must not fan out"
2323        );
2324
2325        // Re-connect fires the transition.
2326        base.connect_addr(2);
2327        assert_eq!(connect_hits.load(Ordering::Relaxed), 2);
2328    }
2329
2330    /// C parity: `autoConnectAsyn` (asynManager.c:2310-2324) fires
2331    /// `asynExceptionAutoConnect` unconditionally — even setting the
2332    /// same value as the current one. Rust mirrors that so observers
2333    /// can refresh their UI after a re-confirmation, not just an edge.
2334    #[test]
2335    fn test_set_auto_connect_fires_unconditionally() {
2336        use std::sync::atomic::{AtomicUsize, Ordering};
2337
2338        let mut base = PortDriverBase::new("ac", 1, PortFlags::default());
2339        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
2340        base.exception_sink = Some(exc_mgr.clone());
2341        let hits = Arc::new(AtomicUsize::new(0));
2342        let hits2 = hits.clone();
2343        exc_mgr.add_callback(move |event| {
2344            if event.exception == AsynException::AutoConnect {
2345                hits2.fetch_add(1, Ordering::Relaxed);
2346            }
2347        });
2348        // base.auto_connect defaults to true — setting true again
2349        // still must fire (no state-change guard in C).
2350        base.set_auto_connect(true);
2351        base.set_auto_connect(false);
2352        base.set_auto_connect(false);
2353        assert_eq!(hits.load(Ordering::Relaxed), 3);
2354    }
2355
2356    #[test]
2357    fn auto_connect_throttle_gate_boundaries() {
2358        // C autoConnectDevice 2.0s gate (asynManager.c:712-713). Boundary
2359        // cases, not narrative: never-stamped, exactly-2s, just-under-2s.
2360        let mut base = PortDriverBase::new("thr", 1, PortFlags::default());
2361
2362        // No transition recorded yet => always permitted (C's
2363        // zero-initialised lastConnectDisconnect).
2364        let t0 = Instant::now();
2365        assert!(base.auto_connect_throttle_ok(-1, t0));
2366
2367        // Stamp at t0; only `+` arithmetic on Instant (no `- Duration`,
2368        // which panics on Windows when uptime < the span).
2369        base.last_connect_disconnect = Some(t0);
2370        // elapsed 0 < 2s => refused.
2371        assert!(!base.auto_connect_throttle_ok(-1, t0));
2372        // elapsed just under 2s => refused.
2373        assert!(!base.auto_connect_throttle_ok(-1, t0 + Duration::from_millis(1999)));
2374        // elapsed exactly 2s => permitted (>=).
2375        assert!(base.auto_connect_throttle_ok(-1, t0 + Duration::from_secs(2)));
2376        // elapsed well past => permitted.
2377        assert!(base.auto_connect_throttle_ok(-1, t0 + Duration::from_secs(5)));
2378    }
2379
2380    #[test]
2381    fn auto_connect_throttle_stamps_on_disconnect_not_connect() {
2382        // C exceptionDisconnect stamps lastConnectDisconnect (asynManager.c
2383        // :2184); exceptionConnect does not (:2157-2159). Mirror both edges.
2384        let mut base = PortDriverBase::new("thr", 1, PortFlags::default());
2385        // Starts connected, no stamp.
2386        assert!(base.last_connect_disconnect.is_none());
2387
2388        // Disconnect edge stamps.
2389        assert!(base.set_connected(false));
2390        assert!(base.last_connect_disconnect.is_some());
2391
2392        // Clear, then connect edge must NOT re-stamp.
2393        base.last_connect_disconnect = None;
2394        assert!(base.set_connected(true));
2395        assert!(base.last_connect_disconnect.is_none());
2396    }
2397
2398    #[test]
2399    fn auto_connect_throttle_per_device_anchor() {
2400        // Multi-device ports throttle per address (C dpCommon is per-device).
2401        let flags = PortFlags {
2402            multi_device: true,
2403            ..PortFlags::default()
2404        };
2405        let mut base = PortDriverBase::new("thr", 4, flags);
2406        let t0 = Instant::now();
2407
2408        // addr 1 disconnect stamps only addr 1's anchor.
2409        assert!(base.set_addr_connected(1, false));
2410        assert!(base.device_state(1).last_connect_disconnect.is_some());
2411        // addr 1 is throttled; addr 2 (never stamped) is still permitted.
2412        assert!(!base.auto_connect_throttle_ok(1, t0));
2413        assert!(base.auto_connect_throttle_ok(2, t0));
2414
2415        // Post-attempt stamp restarts addr 2's window.
2416        base.stamp_auto_connect_attempt(2, t0);
2417        assert!(!base.auto_connect_throttle_ok(2, t0));
2418        assert!(base.auto_connect_throttle_ok(2, t0 + Duration::from_secs(2)));
2419    }
2420
2421    #[test]
2422    fn set_enabled_refuses_defunct_port() {
2423        use std::sync::atomic::{AtomicUsize, Ordering};
2424        // C `enable` on a defunct port: asynDisabled, no `enabled` toggle,
2425        // no asynExceptionEnable fan-out (asynManager.c:2236-2241).
2426        let flags = PortFlags {
2427            destructible: true,
2428            ..PortFlags::default()
2429        };
2430        let mut base = PortDriverBase::new("def", 1, flags);
2431        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
2432        base.exception_sink = Some(exc_mgr.clone());
2433        let enable_hits = Arc::new(AtomicUsize::new(0));
2434        let h = enable_hits.clone();
2435        exc_mgr.add_callback(move |event| {
2436            if event.exception == AsynException::Enable {
2437                h.fetch_add(1, Ordering::Relaxed);
2438            }
2439        });
2440
2441        // Shut the port down → defunct (shutdown sets enabled=false).
2442        base.shutdown_lifecycle().unwrap();
2443        assert!(base.is_defunct());
2444        assert!(!base.is_enabled());
2445
2446        let err = base.set_enabled(true).unwrap_err();
2447        match err {
2448            AsynError::Status { status, .. } => assert_eq!(status, AsynStatus::Disabled),
2449            other => panic!("expected Disabled, got {other:?}"),
2450        }
2451        assert!(!base.is_enabled(), "defunct port must not re-enable");
2452        assert_eq!(
2453            enable_hits.load(Ordering::Relaxed),
2454            0,
2455            "no Enable exception may fire on a defunct port"
2456        );
2457    }
2458
2459    #[test]
2460    fn set_addr_enabled_refuses_defunct_port() {
2461        use std::sync::atomic::{AtomicUsize, Ordering};
2462        let flags = PortFlags {
2463            multi_device: true,
2464            destructible: true,
2465            ..PortFlags::default()
2466        };
2467        let mut base = PortDriverBase::new("def", 4, flags);
2468        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
2469        base.exception_sink = Some(exc_mgr.clone());
2470        let enable_hits = Arc::new(AtomicUsize::new(0));
2471        let h = enable_hits.clone();
2472        exc_mgr.add_callback(move |event| {
2473            if event.exception == AsynException::Enable {
2474                h.fetch_add(1, Ordering::Relaxed);
2475            }
2476        });
2477
2478        base.shutdown_lifecycle().unwrap();
2479
2480        let err = base.set_addr_enabled(1, false).unwrap_err();
2481        match err {
2482            AsynError::Status { status, .. } => assert_eq!(status, AsynStatus::Disabled),
2483            other => panic!("expected Disabled, got {other:?}"),
2484        }
2485        // The guard returns before `device_state(addr)` would insert an
2486        // entry, so the refused call mutates no per-device state.
2487        assert!(
2488            !base.device_states.contains_key(&1),
2489            "refused per-device enable must not create device state"
2490        );
2491        // The `()` convenience facade also no-ops on a defunct port.
2492        base.disable_addr(1);
2493        assert!(!base.device_states.contains_key(&1));
2494        assert_eq!(
2495            enable_hits.load(Ordering::Relaxed),
2496            0,
2497            "no Enable exception may fire on a defunct port"
2498        );
2499    }
2500
2501    /// C parity: `asynPortDriver::setInterruptUInt32Digital` /
2502    /// `clearInterruptUInt32Digital` / `getInterruptUInt32Digital`
2503    /// (`asynPortDriver.cpp:2346-2461`) route through paramList. The
2504    /// PortDriver trait default delegates to the param store; we
2505    /// verify the round-trip end-to-end through the trait surface.
2506    #[test]
2507    fn test_port_driver_uint32_interrupt_round_trip() {
2508        struct UInt32Drv {
2509            base: PortDriverBase,
2510        }
2511        impl PortDriver for UInt32Drv {
2512            fn base(&self) -> &PortDriverBase {
2513                &self.base
2514            }
2515            fn base_mut(&mut self) -> &mut PortDriverBase {
2516                &mut self.base
2517            }
2518        }
2519
2520        let mut base = PortDriverBase::new("uint32_int", 1, PortFlags::default());
2521        let idx = base
2522            .params
2523            .create_param("BITS", ParamType::UInt32Digital)
2524            .unwrap();
2525        let mut drv = UInt32Drv { base };
2526        let user = AsynUser::new(idx).with_addr(0);
2527
2528        drv.set_interrupt_uint32_digital(&user, 0xF0, InterruptReason::ZeroToOne)
2529            .unwrap();
2530        drv.set_interrupt_uint32_digital(&user, 0x0F, InterruptReason::OneToZero)
2531            .unwrap();
2532        assert_eq!(
2533            drv.get_interrupt_uint32_digital(&user, InterruptReason::Both)
2534                .unwrap(),
2535            0xFF
2536        );
2537        drv.clear_interrupt_uint32_digital(&user, 0x11).unwrap();
2538        assert_eq!(
2539            drv.get_interrupt_uint32_digital(&user, InterruptReason::ZeroToOne)
2540                .unwrap(),
2541            0xE0
2542        );
2543        assert_eq!(
2544            drv.get_interrupt_uint32_digital(&user, InterruptReason::OneToZero)
2545                .unwrap(),
2546            0x0E
2547        );
2548    }
2549
2550    /// C parity: the default `read_int32` / `read_int64` / `read_float64` /
2551    /// `read_octet` / `read_uint32_digital` must surface an *unset*
2552    /// parameter as `ParamUndefined`, not success/0. The default
2553    /// `asynPortDriver::read{Int32,Int64,Float64,Octet,UInt32Digital}` calls
2554    /// `get{Integer,Integer64,Double,String,UIntDigital}Param`, every
2555    /// `paramVal` getter throws `ParamValNotDefined` → `asynParamUndefined`
2556    /// for an unset value (paramVal.cpp:152,181,235,264,292), and the
2557    /// `devAsyn*` device support routes that status through
2558    /// `asynStatusToEpicsAlarm(READ_ALARM, INVALID_ALARM)`. After a write
2559    /// the same reads succeed with the stored value.
2560    #[test]
2561    fn default_scalar_reads_report_undefined_until_set() {
2562        struct AllTypesDrv {
2563            base: PortDriverBase,
2564        }
2565        impl PortDriver for AllTypesDrv {
2566            fn base(&self) -> &PortDriverBase {
2567                &self.base
2568            }
2569            fn base_mut(&mut self) -> &mut PortDriverBase {
2570                &mut self.base
2571            }
2572        }
2573
2574        let mut base = PortDriverBase::new("undef_read", 1, PortFlags::default());
2575        let i32_idx = base.params.create_param("I32", ParamType::Int32).unwrap();
2576        let i64_idx = base.params.create_param("I64", ParamType::Int64).unwrap();
2577        let f64_idx = base.params.create_param("F64", ParamType::Float64).unwrap();
2578        let oct_idx = base.params.create_param("OCT", ParamType::Octet).unwrap();
2579        let u32_idx = base
2580            .params
2581            .create_param("BITS", ParamType::UInt32Digital)
2582            .unwrap();
2583        let mut drv = AllTypesDrv { base };
2584
2585        // Unset → every default scalar read is ParamUndefined, NOT Ok(0).
2586        assert!(matches!(
2587            drv.read_int32(&AsynUser::new(i32_idx).with_addr(0)),
2588            Err(AsynError::ParamUndefined(_))
2589        ));
2590        assert!(matches!(
2591            drv.read_int64(&AsynUser::new(i64_idx).with_addr(0)),
2592            Err(AsynError::ParamUndefined(_))
2593        ));
2594        assert!(matches!(
2595            drv.read_float64(&AsynUser::new(f64_idx).with_addr(0)),
2596            Err(AsynError::ParamUndefined(_))
2597        ));
2598        let mut buf = [0u8; 16];
2599        assert!(matches!(
2600            drv.read_octet(&AsynUser::new(oct_idx).with_addr(0), &mut buf),
2601            Err(AsynError::ParamUndefined(_))
2602        ));
2603        assert!(matches!(
2604            drv.read_uint32_digital(&AsynUser::new(u32_idx).with_addr(0), 0xFFFF_FFFF),
2605            Err(AsynError::ParamUndefined(_))
2606        ));
2607
2608        // After a write the same reads succeed with the stored value.
2609        drv.base_mut().params.set_int32(i32_idx, 0, 7).unwrap();
2610        drv.base_mut().params.set_int64(i64_idx, 0, 9).unwrap();
2611        drv.base_mut().params.set_float64(f64_idx, 0, 1.5).unwrap();
2612        drv.base_mut()
2613            .params
2614            .set_string(oct_idx, 0, "hi".to_string())
2615            .unwrap();
2616        drv.base_mut()
2617            .params
2618            .set_uint32(u32_idx, 0, 0x05, 0xFFFF_FFFF, 0)
2619            .unwrap();
2620
2621        assert_eq!(
2622            drv.read_int32(&AsynUser::new(i32_idx).with_addr(0))
2623                .unwrap(),
2624            7
2625        );
2626        assert_eq!(
2627            drv.read_int64(&AsynUser::new(i64_idx).with_addr(0))
2628                .unwrap(),
2629            9
2630        );
2631        assert_eq!(
2632            drv.read_float64(&AsynUser::new(f64_idx).with_addr(0))
2633                .unwrap(),
2634            1.5
2635        );
2636        let n = drv
2637            .read_octet(&AsynUser::new(oct_idx).with_addr(0), &mut buf)
2638            .unwrap();
2639        assert_eq!(&buf[..n], b"hi");
2640        assert_eq!(
2641            drv.read_uint32_digital(&AsynUser::new(u32_idx).with_addr(0), 0xFFFF_FFFF)
2642                .unwrap(),
2643            0x05
2644        );
2645    }
2646}