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