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    pub fn set_uint32_param(
462        &mut self,
463        index: usize,
464        addr: i32,
465        value: u32,
466        mask: u32,
467    ) -> AsynResult<()> {
468        self.params.set_uint32(index, addr, value, mask)
469    }
470
471    pub fn get_uint32_param(&self, index: usize, addr: i32) -> AsynResult<u32> {
472        self.params.get_uint32(index, addr)
473    }
474
475    /// Strict variant — see [`crate::param::ParamList::get_uint32_strict`].
476    pub fn get_uint32_param_strict(&self, index: usize, addr: i32) -> AsynResult<u32> {
477        self.params.get_uint32_strict(index, addr)
478    }
479
480    pub fn get_enum_param(&self, index: usize, addr: i32) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
481        self.params.get_enum(index, addr)
482    }
483
484    pub fn set_enum_index_param(
485        &mut self,
486        index: usize,
487        addr: i32,
488        value: usize,
489    ) -> AsynResult<()> {
490        self.params.set_enum_index(index, addr, value)
491    }
492
493    pub fn set_enum_choices_param(
494        &mut self,
495        index: usize,
496        addr: i32,
497        choices: Arc<[EnumEntry]>,
498    ) -> AsynResult<()> {
499        self.params.set_enum_choices(index, addr, choices)
500    }
501
502    pub fn get_generic_pointer_param(
503        &self,
504        index: usize,
505        addr: i32,
506    ) -> AsynResult<Arc<dyn Any + Send + Sync>> {
507        self.params.get_generic_pointer(index, addr)
508    }
509
510    pub fn set_generic_pointer_param(
511        &mut self,
512        index: usize,
513        addr: i32,
514        value: Arc<dyn Any + Send + Sync>,
515    ) -> AsynResult<()> {
516        self.params.set_generic_pointer(index, addr, value)
517    }
518
519    pub fn set_param_timestamp(
520        &mut self,
521        index: usize,
522        addr: i32,
523        ts: SystemTime,
524    ) -> AsynResult<()> {
525        self.params.set_timestamp(index, addr, ts)
526    }
527
528    pub fn set_param_status(
529        &mut self,
530        index: usize,
531        addr: i32,
532        status: AsynStatus,
533        alarm_status: u16,
534        alarm_severity: u16,
535    ) -> AsynResult<()> {
536        self.params
537            .set_param_status(index, addr, status, alarm_status, alarm_severity)
538    }
539
540    pub fn get_param_status(&self, index: usize, addr: i32) -> AsynResult<(AsynStatus, u16, u16)> {
541        self.params.get_param_status(index, addr)
542    }
543
544    /// Detailed parameter report matching C asynPortDriver::reportParams.
545    pub fn report_params(&self, level: i32) {
546        eprintln!("  Number of parameters is {}", self.params.len());
547        if level < 1 {
548            return;
549        }
550        for i in 0..self.params.len() {
551            let name = self.params.param_name(i).unwrap_or("?");
552            let ptype = self
553                .params
554                .param_type(i)
555                .map(|t| format!("{t:?}"))
556                .unwrap_or("?".into());
557            if level >= 2 {
558                for addr in 0..self.max_addr.max(1) {
559                    let val = self
560                        .params
561                        .get_value(i, addr as i32)
562                        .map(|v| format!("{v:?}"))
563                        .unwrap_or("undefined".into());
564                    let (status, alarm_st, alarm_sev) = self
565                        .params
566                        .get_param_status(i, addr as i32)
567                        .unwrap_or((AsynStatus::Success, 0, 0));
568                    eprintln!(
569                        "  param[{i}] name={name} type={ptype} addr={addr} val={val} status={status:?} alarm=({alarm_st},{alarm_sev})"
570                    );
571                }
572            } else {
573                eprintln!("  param[{i}] name={name} type={ptype}");
574            }
575        }
576    }
577
578    /// Push an interpose layer onto the octet I/O stack.
579    ///
580    /// **Concurrency**: requires `&mut self`, which means the caller must hold
581    /// the port lock (`Arc<Mutex<dyn PortDriver>>`). This ensures
582    /// interpose modifications are serialized with I/O dispatch.
583    pub fn push_octet_interpose(&mut self, layer: Box<dyn OctetInterpose>) {
584        self.interpose_octet.push(layer);
585    }
586
587    /// Flush changed parameters as interrupt notifications.
588    /// Equivalent to C asyn's callParamCallbacks().
589    pub fn call_param_callbacks(&mut self, addr: i32) -> AsynResult<()> {
590        let changed = self.params.take_changed(addr)?;
591        let now = self.current_timestamp();
592        for reason in changed {
593            let value = self.params.get_value(reason, addr)?.clone();
594            let ts = self.params.get_timestamp(reason, addr)?.unwrap_or(now);
595            let uint32_mask = self
596                .params
597                .get_uint32_interrupt_mask(reason, addr)
598                .unwrap_or(0);
599            // C parity: asynPortDriver.cpp:631-642 sets
600            // `pInterrupt->pasynUser->auxStatus/alarmStatus/alarmSeverity`
601            // from the param's stored status before invoking each
602            // subscriber callback. Pull those here so subscribers see
603            // the same triplet C consumers do.
604            let (aux_status, alarm_status, alarm_severity) = self
605                .params
606                .get_param_status(reason, addr)
607                .unwrap_or((AsynStatus::Success, 0, 0));
608            self.interrupts.notify(InterruptValue {
609                reason,
610                addr,
611                value,
612                timestamp: ts,
613                uint32_changed_mask: uint32_mask,
614                aux_status,
615                alarm_status,
616                alarm_severity,
617            });
618        }
619        Ok(())
620    }
621
622    /// Flush a single parameter's changed flag and notify if dirty.
623    /// Use this instead of `call_param_callbacks` when you want to avoid
624    /// flushing unrelated parameters (e.g. rapidly-updating CP-linked params).
625    pub fn call_param_callback(&mut self, addr: i32, reason: usize) -> AsynResult<()> {
626        if self.params.take_changed_single(reason, addr)? {
627            let now = self.current_timestamp();
628            let value = self.params.get_value(reason, addr)?.clone();
629            let ts = self.params.get_timestamp(reason, addr)?.unwrap_or(now);
630            let uint32_mask = self
631                .params
632                .get_uint32_interrupt_mask(reason, addr)
633                .unwrap_or(0);
634            // C parity: see `call_param_callbacks` above.
635            let (aux_status, alarm_status, alarm_severity) = self
636                .params
637                .get_param_status(reason, addr)
638                .unwrap_or((AsynStatus::Success, 0, 0));
639            self.interrupts.notify(InterruptValue {
640                reason,
641                addr,
642                value,
643                timestamp: ts,
644                uint32_changed_mask: uint32_mask,
645                aux_status,
646                alarm_status,
647                alarm_severity,
648            });
649        }
650        Ok(())
651    }
652
653    /// Mark a parameter as changed without modifying its value.
654    ///
655    /// Use this to trigger I/O Intr on params whose data is served via
656    /// `read_*_array()` overrides rather than the param cache (e.g. pixel data).
657    pub fn mark_param_changed(&mut self, index: usize, addr: i32) -> AsynResult<()> {
658        self.params.mark_changed(index, addr)
659    }
660}
661
662/// Port driver trait. All methods have default implementations that operate
663/// on the parameter cache (no actual I/O).
664///
665/// Drivers performing real hardware I/O should:
666/// 1. Run I/O in a background task (e.g., tokio::spawn)
667/// 2. Update parameters via `base_mut().set_*_param()` + `call_param_callbacks()`
668/// 3. Let the default `read_*` methods return cached values
669///
670/// # LockPort/UnlockPort
671///
672/// C asyn provides `lockPort`/`unlockPort` for direct mutex locking. In asyn-rs,
673/// the port is always behind `Arc<Mutex<dyn PortDriver>>`, so callers hold the
674/// parking_lot mutex directly. For multi-request exclusive access, use
675/// `BlockProcess`/`UnblockProcess` via the worker queue.
676pub trait PortDriver: Send + Sync + 'static {
677    fn base(&self) -> &PortDriverBase;
678    fn base_mut(&mut self) -> &mut PortDriverBase;
679
680    // --- AsynCommon ---
681
682    fn connect(&mut self, _user: &AsynUser) -> AsynResult<()> {
683        // Single owner-API: edge-guarded fire is in PortDriverBase::set_connected.
684        self.base_mut().set_connected(true);
685        Ok(())
686    }
687
688    fn disconnect(&mut self, _user: &AsynUser) -> AsynResult<()> {
689        self.base_mut().set_connected(false);
690        Ok(())
691    }
692
693    fn enable(&mut self, _user: &AsynUser) -> AsynResult<()> {
694        self.base_mut().enabled = true;
695        self.base().announce_exception(AsynException::Enable, -1);
696        Ok(())
697    }
698
699    fn disable(&mut self, _user: &AsynUser) -> AsynResult<()> {
700        self.base_mut().enabled = false;
701        self.base().announce_exception(AsynException::Enable, -1);
702        Ok(())
703    }
704
705    fn connect_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
706        self.base_mut().connect_addr(user.addr);
707        Ok(())
708    }
709
710    fn disconnect_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
711        self.base_mut().disconnect_addr(user.addr);
712        Ok(())
713    }
714
715    fn enable_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
716        self.base_mut().enable_addr(user.addr);
717        Ok(())
718    }
719
720    fn disable_addr(&mut self, user: &AsynUser) -> AsynResult<()> {
721        self.base_mut().disable_addr(user.addr);
722        Ok(())
723    }
724
725    fn get_option(&self, key: &str) -> AsynResult<String> {
726        self.base()
727            .options
728            .get(key)
729            .cloned()
730            .ok_or_else(|| AsynError::OptionNotFound(key.to_string()))
731    }
732
733    fn set_option(&mut self, key: &str, value: &str) -> AsynResult<()> {
734        self.base_mut()
735            .options
736            .insert(key.to_string(), value.to_string());
737        Ok(())
738    }
739
740    fn report(&self, level: i32) {
741        let base = self.base();
742        eprintln!("Port: {}", base.port_name);
743        eprintln!(
744            "  connected: {}, max_addr: {}, params: {}, options: {}",
745            base.connected,
746            base.max_addr,
747            base.params.len(),
748            base.options.len()
749        );
750        if level >= 1 {
751            base.report_params(level.saturating_sub(1));
752        }
753        if level >= 2 {
754            for (k, v) in &base.options {
755                eprintln!("  option: {k} = {v}");
756            }
757        }
758    }
759
760    // --- Scalar I/O (cache-based defaults, timeout not applicable) ---
761
762    // Cache-based defaults do NOT check connection state (C parity).
763    // The port actor checks check_ready_addr() before dispatching, matching
764    // C asyn where asynManager checks connection before calling the driver.
765
766    fn read_int32(&mut self, user: &AsynUser) -> AsynResult<i32> {
767        self.base().params.get_int32(user.reason, user.addr)
768    }
769
770    fn write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
771        self.base_mut()
772            .params
773            .set_int32(user.reason, user.addr, value)?;
774        self.base_mut().call_param_callbacks(user.addr)
775    }
776
777    fn read_int64(&mut self, user: &AsynUser) -> AsynResult<i64> {
778        self.base().params.get_int64(user.reason, user.addr)
779    }
780
781    fn write_int64(&mut self, user: &mut AsynUser, value: i64) -> AsynResult<()> {
782        self.base_mut()
783            .params
784            .set_int64(user.reason, user.addr, value)?;
785        self.base_mut().call_param_callbacks(user.addr)
786    }
787
788    fn get_bounds_int32(&self, _user: &AsynUser) -> AsynResult<(i32, i32)> {
789        Ok((i32::MIN, i32::MAX))
790    }
791
792    fn get_bounds_int64(&self, _user: &AsynUser) -> AsynResult<(i64, i64)> {
793        Ok((i64::MIN, i64::MAX))
794    }
795
796    fn read_float64(&mut self, user: &AsynUser) -> AsynResult<f64> {
797        self.base().params.get_float64(user.reason, user.addr)
798    }
799
800    fn write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
801        self.base_mut()
802            .params
803            .set_float64(user.reason, user.addr, value)?;
804        self.base_mut().call_param_callbacks(user.addr)
805    }
806
807    fn read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
808        let s = self.base().params.get_string(user.reason, user.addr)?;
809        let bytes = s.as_bytes();
810        let n = bytes.len().min(buf.len());
811        buf[..n].copy_from_slice(&bytes[..n]);
812        Ok(n)
813    }
814
815    fn write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<()> {
816        let s = String::from_utf8_lossy(data).into_owned();
817        self.base_mut()
818            .params
819            .set_string(user.reason, user.addr, s)?;
820        self.base_mut().call_param_callbacks(user.addr)
821    }
822
823    fn read_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<u32> {
824        let val = self.base().params.get_uint32(user.reason, user.addr)?;
825        Ok(val & mask)
826    }
827
828    fn write_uint32_digital(
829        &mut self,
830        user: &mut AsynUser,
831        value: u32,
832        mask: u32,
833    ) -> AsynResult<()> {
834        self.base_mut()
835            .params
836            .set_uint32(user.reason, user.addr, value, mask)?;
837        self.base_mut().call_param_callbacks(user.addr)
838    }
839
840    /// Configure rising / falling interrupt masks for a
841    /// UInt32Digital parameter. C parity:
842    /// `asynPortDriver::setInterruptUInt32Digital`
843    /// (`asynPortDriver.cpp:2346-2369`) → routes to
844    /// `paramList::setUInt32Interrupt`. The default delegates to the
845    /// param store; drivers that need to push the configuration to
846    /// hardware (e.g. real GPIB cards toggling SRQ enable) override
847    /// it.
848    fn set_interrupt_uint32_digital(
849        &mut self,
850        user: &AsynUser,
851        mask: u32,
852        reason: InterruptReason,
853    ) -> AsynResult<()> {
854        self.base_mut()
855            .params
856            .set_uint32_interrupt(user.reason, user.addr, mask, reason)
857    }
858
859    /// Clear bits from rising AND falling masks. C parity:
860    /// `asynPortDriver::clearInterruptUInt32Digital`
861    /// (`asynPortDriver.cpp:2392-2415`). Mirrors C — the call does
862    /// not take an `interruptReason`; both masks are cleared.
863    fn clear_interrupt_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<()> {
864        self.base_mut()
865            .params
866            .clear_uint32_interrupt(user.reason, user.addr, mask)
867    }
868
869    /// Read the configured rising / falling / combined mask. C
870    /// parity: `asynPortDriver::getInterruptUInt32Digital`
871    /// (`asynPortDriver.cpp:2438-2461`).
872    fn get_interrupt_uint32_digital(
873        &self,
874        user: &AsynUser,
875        reason: InterruptReason,
876    ) -> AsynResult<u32> {
877        self.base()
878            .params
879            .get_uint32_interrupt(user.reason, user.addr, reason)
880    }
881
882    // --- Enum I/O (cache-based defaults) ---
883
884    fn read_enum(&mut self, user: &AsynUser) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
885        self.base().params.get_enum(user.reason, user.addr)
886    }
887
888    fn write_enum(&mut self, user: &mut AsynUser, index: usize) -> AsynResult<()> {
889        self.base_mut()
890            .params
891            .set_enum_index(user.reason, user.addr, index)?;
892        self.base_mut().call_param_callbacks(user.addr)
893    }
894
895    fn write_enum_choices(
896        &mut self,
897        user: &mut AsynUser,
898        choices: Arc<[EnumEntry]>,
899    ) -> AsynResult<()> {
900        self.base_mut()
901            .params
902            .set_enum_choices(user.reason, user.addr, choices)?;
903        self.base_mut().call_param_callbacks(user.addr)
904    }
905
906    // --- GenericPointer I/O (cache-based defaults) ---
907
908    fn read_generic_pointer(&mut self, user: &AsynUser) -> AsynResult<Arc<dyn Any + Send + Sync>> {
909        self.base()
910            .params
911            .get_generic_pointer(user.reason, user.addr)
912    }
913
914    fn write_generic_pointer(
915        &mut self,
916        user: &mut AsynUser,
917        value: Arc<dyn Any + Send + Sync>,
918    ) -> AsynResult<()> {
919        self.base_mut()
920            .params
921            .set_generic_pointer(user.reason, user.addr, value)?;
922        self.base_mut().call_param_callbacks(user.addr)
923    }
924
925    // --- Array I/O (default: not supported) ---
926
927    fn read_float64_array(&mut self, _user: &AsynUser, _buf: &mut [f64]) -> AsynResult<usize> {
928        Err(AsynError::InterfaceNotSupported("asynFloat64Array".into()))
929    }
930
931    fn write_float64_array(&mut self, user: &AsynUser, data: &[f64]) -> AsynResult<()> {
932        self.base_mut()
933            .params
934            .set_float64_array(user.reason, user.addr, data.to_vec())?;
935        self.base_mut().call_param_callbacks(user.addr)
936    }
937
938    fn read_int32_array(&mut self, _user: &AsynUser, _buf: &mut [i32]) -> AsynResult<usize> {
939        Err(AsynError::InterfaceNotSupported("asynInt32Array".into()))
940    }
941
942    fn write_int32_array(&mut self, user: &AsynUser, data: &[i32]) -> AsynResult<()> {
943        self.base_mut()
944            .params
945            .set_int32_array(user.reason, user.addr, data.to_vec())?;
946        self.base_mut().call_param_callbacks(user.addr)
947    }
948
949    fn read_int8_array(&mut self, _user: &AsynUser, _buf: &mut [i8]) -> AsynResult<usize> {
950        Err(AsynError::InterfaceNotSupported("asynInt8Array".into()))
951    }
952
953    fn write_int8_array(&mut self, user: &AsynUser, data: &[i8]) -> AsynResult<()> {
954        self.base_mut()
955            .params
956            .set_int8_array(user.reason, user.addr, data.to_vec())?;
957        self.base_mut().call_param_callbacks(user.addr)
958    }
959
960    fn read_int16_array(&mut self, _user: &AsynUser, _buf: &mut [i16]) -> AsynResult<usize> {
961        Err(AsynError::InterfaceNotSupported("asynInt16Array".into()))
962    }
963
964    fn write_int16_array(&mut self, user: &AsynUser, data: &[i16]) -> AsynResult<()> {
965        self.base_mut()
966            .params
967            .set_int16_array(user.reason, user.addr, data.to_vec())?;
968        self.base_mut().call_param_callbacks(user.addr)
969    }
970
971    fn read_int64_array(&mut self, _user: &AsynUser, _buf: &mut [i64]) -> AsynResult<usize> {
972        Err(AsynError::InterfaceNotSupported("asynInt64Array".into()))
973    }
974
975    fn write_int64_array(&mut self, user: &AsynUser, data: &[i64]) -> AsynResult<()> {
976        self.base_mut()
977            .params
978            .set_int64_array(user.reason, user.addr, data.to_vec())?;
979        self.base_mut().call_param_callbacks(user.addr)
980    }
981
982    fn read_float32_array(&mut self, _user: &AsynUser, _buf: &mut [f32]) -> AsynResult<usize> {
983        Err(AsynError::InterfaceNotSupported("asynFloat32Array".into()))
984    }
985
986    fn write_float32_array(&mut self, user: &AsynUser, data: &[f32]) -> AsynResult<()> {
987        self.base_mut()
988            .params
989            .set_float32_array(user.reason, user.addr, data.to_vec())?;
990        self.base_mut().call_param_callbacks(user.addr)
991    }
992
993    // --- I/O methods (worker thread calls these) ---
994    // Default: delegate to cache-based read_*/write_* for backward compat.
995    // Real I/O drivers override these for actual hardware access.
996
997    fn io_read_octet(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<usize> {
998        self.read_octet(user, buf)
999    }
1000
1001    /// Octet read that also reports the end-of-message reason — C
1002    /// parity for `asynOctet::read(... int *eomReason)`
1003    /// (`asynOctet.h:38-40`). The default implementation delegates to
1004    /// [`Self::io_read_octet`] and reconstructs a synthetic
1005    /// [`EomReason`]: `CNT` when the buffer filled, `empty` otherwise.
1006    /// Drivers that have native EOM information
1007    /// (`asynOctetSyncIO::readRaw`, GPIB END, EOS match) must
1008    /// override this method so consumers — `asynRecord::EOMR`,
1009    /// `asynOctetSyncIO::readRaw` mirrors — receive the real flags.
1010    fn io_read_octet_eom(
1011        &mut self,
1012        user: &AsynUser,
1013        buf: &mut [u8],
1014    ) -> AsynResult<(usize, EomReason)> {
1015        let cap = buf.len();
1016        let n = self.io_read_octet(user, buf)?;
1017        let eom = if n >= cap && cap > 0 {
1018            EomReason::CNT
1019        } else {
1020            EomReason::empty()
1021        };
1022        Ok((n, eom))
1023    }
1024
1025    fn io_write_octet(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<()> {
1026        self.write_octet(user, data)
1027    }
1028
1029    fn io_read_int32(&mut self, user: &AsynUser) -> AsynResult<i32> {
1030        self.read_int32(user)
1031    }
1032
1033    fn io_write_int32(&mut self, user: &mut AsynUser, value: i32) -> AsynResult<()> {
1034        self.write_int32(user, value)
1035    }
1036
1037    fn io_read_int64(&mut self, user: &AsynUser) -> AsynResult<i64> {
1038        self.read_int64(user)
1039    }
1040
1041    fn io_write_int64(&mut self, user: &mut AsynUser, value: i64) -> AsynResult<()> {
1042        self.write_int64(user, value)
1043    }
1044
1045    fn io_read_float64(&mut self, user: &AsynUser) -> AsynResult<f64> {
1046        self.read_float64(user)
1047    }
1048
1049    fn io_write_float64(&mut self, user: &mut AsynUser, value: f64) -> AsynResult<()> {
1050        self.write_float64(user, value)
1051    }
1052
1053    fn io_read_uint32_digital(&mut self, user: &AsynUser, mask: u32) -> AsynResult<u32> {
1054        self.read_uint32_digital(user, mask)
1055    }
1056
1057    fn io_write_uint32_digital(
1058        &mut self,
1059        user: &mut AsynUser,
1060        value: u32,
1061        mask: u32,
1062    ) -> AsynResult<()> {
1063        self.write_uint32_digital(user, value, mask)
1064    }
1065
1066    fn io_flush(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
1067        Ok(())
1068    }
1069
1070    // --- Octet EOS (delegates to interpose stack by default) ---
1071    //
1072    // ## EOS connect-wait policy (C asyn issue #103)
1073    //
1074    // C asyn `asynOctetSyncIO::setInputEos` / `setOutputEos`
1075    // (`asynOctetSyncIO.c:300-321`, 346-367) call `lockPort` ahead of
1076    // the actual `setInputEos` — `lockPort` waits up to the user's
1077    // timeout for the port to be connected, by `epicsEventWait`-ing
1078    // on the connect event published from `connectIt`. On IOC init
1079    // and exit this serialises EOS configuration against the connect
1080    // task, but it also means a `setInputEos` issued before the port
1081    // has ever connected blocks the calling thread (issue #103
1082    // captured the symptom: IOC startup pauses for the full asyn
1083    // timeout when the device is off-line).
1084    //
1085    // The Rust path here is purely in-memory: `set_input_eos` and
1086    // `set_output_eos` write the bytes into `PortDriverBase` and the
1087    // EOS interpose stack reads from those fields at next read/write
1088    // time. No connect-wait, no lock contention with the connect
1089    // task — so issue #103's symptom cannot reproduce. If a future
1090    // refactor introduces a connect-gated EOS path (e.g. a driver
1091    // that owns the EOS state inside its connect()-allocated
1092    // resource), authors MUST keep the wait optional / bounded so
1093    // the connect-wait failure mode doesn't return.
1094
1095    fn set_input_eos(&mut self, eos: &[u8]) -> AsynResult<()> {
1096        if eos.len() > 2 {
1097            return Err(AsynError::Status {
1098                status: AsynStatus::Error,
1099                message: format!("illegal eoslen {}", eos.len()),
1100            });
1101        }
1102        self.base_mut().input_eos = eos.to_vec();
1103        Ok(())
1104    }
1105
1106    fn get_input_eos(&self) -> Vec<u8> {
1107        self.base().input_eos.clone()
1108    }
1109
1110    fn set_output_eos(&mut self, eos: &[u8]) -> AsynResult<()> {
1111        if eos.len() > 2 {
1112            return Err(AsynError::Status {
1113                status: AsynStatus::Error,
1114                message: format!("illegal eoslen {}", eos.len()),
1115            });
1116        }
1117        self.base_mut().output_eos = eos.to_vec();
1118        Ok(())
1119    }
1120
1121    fn get_output_eos(&self) -> Vec<u8> {
1122        self.base().output_eos.clone()
1123    }
1124
1125    // --- Lifecycle ---
1126
1127    /// Called when the port is being shut down. Drivers override this
1128    /// to release hardware resources. Matches C asynPortDriver::shutdownPortDriver().
1129    fn shutdown(&mut self) -> AsynResult<()> {
1130        Ok(())
1131    }
1132
1133    // --- drvUser ---
1134
1135    /// Resolve a driver info string to a parameter index.
1136    /// Default: look up by parameter name.
1137    fn drv_user_create(&self, drv_info: &str) -> AsynResult<usize> {
1138        self.base()
1139            .params
1140            .find_param(drv_info)
1141            .ok_or_else(|| AsynError::ParamNotFound(drv_info.to_string()))
1142    }
1143
1144    // --- Capabilities ---
1145
1146    /// Declare the capabilities this driver supports.
1147    /// Default implementation includes all scalar read/write operations.
1148    fn capabilities(&self) -> Vec<crate::interfaces::Capability> {
1149        crate::interfaces::default_capabilities()
1150    }
1151
1152    /// Check if this driver supports a specific capability.
1153    fn supports(&self, cap: crate::interfaces::Capability) -> bool {
1154        self.capabilities().contains(&cap)
1155    }
1156
1157    fn init(&mut self) -> AsynResult<()> {
1158        Ok(())
1159    }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165    struct TestDriver {
1166        base: PortDriverBase,
1167    }
1168
1169    impl TestDriver {
1170        fn new() -> Self {
1171            let mut base = PortDriverBase::new("test", 1, PortFlags::default());
1172            base.create_param("VAL", ParamType::Int32).unwrap();
1173            base.create_param("TEMP", ParamType::Float64).unwrap();
1174            base.create_param("MSG", ParamType::Octet).unwrap();
1175            base.create_param("BITS", ParamType::UInt32Digital).unwrap();
1176            Self { base }
1177        }
1178    }
1179
1180    impl PortDriver for TestDriver {
1181        fn base(&self) -> &PortDriverBase {
1182            &self.base
1183        }
1184        fn base_mut(&mut self) -> &mut PortDriverBase {
1185            &mut self.base
1186        }
1187    }
1188
1189    #[test]
1190    fn test_default_read_write_int32() {
1191        let mut drv = TestDriver::new();
1192        let mut user = AsynUser::new(0);
1193        drv.write_int32(&mut user, 42).unwrap();
1194        let user = AsynUser::new(0);
1195        assert_eq!(drv.read_int32(&user).unwrap(), 42);
1196    }
1197
1198    #[test]
1199    fn test_default_read_write_float64() {
1200        let mut drv = TestDriver::new();
1201        let mut user = AsynUser::new(1);
1202        drv.write_float64(&mut user, 3.14).unwrap();
1203        let user = AsynUser::new(1);
1204        assert!((drv.read_float64(&user).unwrap() - 3.14).abs() < 1e-10);
1205    }
1206
1207    #[test]
1208    fn test_default_read_write_octet() {
1209        let mut drv = TestDriver::new();
1210        let mut user = AsynUser::new(2);
1211        drv.write_octet(&mut user, b"hello").unwrap();
1212        let user = AsynUser::new(2);
1213        let mut buf = [0u8; 32];
1214        let n = drv.read_octet(&user, &mut buf).unwrap();
1215        assert_eq!(&buf[..n], b"hello");
1216    }
1217
1218    #[test]
1219    fn test_default_read_write_uint32() {
1220        let mut drv = TestDriver::new();
1221        let mut user = AsynUser::new(3);
1222        drv.write_uint32_digital(&mut user, 0xFF, 0x0F).unwrap();
1223        let user = AsynUser::new(3);
1224        assert_eq!(drv.read_uint32_digital(&user, 0xFF).unwrap(), 0x0F);
1225    }
1226
1227    #[test]
1228    fn test_connect_disconnect() {
1229        let mut drv = TestDriver::new();
1230        let user = AsynUser::default();
1231        assert!(drv.base().connected);
1232        drv.disconnect(&user).unwrap();
1233        assert!(!drv.base().connected);
1234        drv.connect(&user).unwrap();
1235        assert!(drv.base().connected);
1236    }
1237
1238    #[test]
1239    fn test_drv_user_create() {
1240        let drv = TestDriver::new();
1241        assert_eq!(drv.drv_user_create("VAL").unwrap(), 0);
1242        assert_eq!(drv.drv_user_create("TEMP").unwrap(), 1);
1243        assert!(drv.drv_user_create("NOPE").is_err());
1244    }
1245
1246    #[test]
1247    fn test_call_param_callbacks() {
1248        let mut drv = TestDriver::new();
1249        let mut rx = drv.base_mut().interrupts.subscribe_async();
1250
1251        drv.base_mut().set_int32_param(0, 0, 100).unwrap();
1252        drv.base_mut().set_float64_param(1, 0, 2.0).unwrap();
1253        drv.base_mut().call_param_callbacks(0).unwrap();
1254
1255        let v1 = rx.try_recv().unwrap();
1256        assert_eq!(v1.reason, 0);
1257        let v2 = rx.try_recv().unwrap();
1258        assert_eq!(v2.reason, 1);
1259        assert!(rx.try_recv().is_err());
1260    }
1261
1262    #[test]
1263    fn test_call_param_callbacks_propagates_aux_status_and_alarm() {
1264        // C parity: asynPortDriver.cpp:631-642 writes the param's stored
1265        // status / alarmStatus / alarmSeverity onto the subscriber's
1266        // pasynUser before invoking the callback. The Rust port carries
1267        // those fields on InterruptValue.
1268        let mut drv = TestDriver::new();
1269        let mut rx = drv.base_mut().interrupts.subscribe_async();
1270
1271        drv.base_mut().set_int32_param(0, 0, 99).unwrap();
1272        drv.base_mut()
1273            .params
1274            .set_param_status(0, 0, crate::error::AsynStatus::Timeout, 4, 2)
1275            .unwrap();
1276        drv.base_mut().call_param_callbacks(0).unwrap();
1277
1278        let iv = rx.try_recv().unwrap();
1279        assert_eq!(iv.reason, 0);
1280        assert!(matches!(iv.aux_status, crate::error::AsynStatus::Timeout));
1281        assert_eq!(iv.alarm_status, 4);
1282        assert_eq!(iv.alarm_severity, 2);
1283    }
1284
1285    #[test]
1286    fn test_call_param_callback_single_propagates_aux_status() {
1287        // Mirror for the single-flush path (call_param_callback).
1288        let mut drv = TestDriver::new();
1289        let mut rx = drv.base_mut().interrupts.subscribe_async();
1290
1291        drv.base_mut().set_int32_param(0, 0, 1).unwrap();
1292        drv.base_mut()
1293            .params
1294            .set_param_status(0, 0, crate::error::AsynStatus::Disconnected, 7, 3)
1295            .unwrap();
1296        drv.base_mut().call_param_callback(0, 0).unwrap();
1297
1298        let iv = rx.try_recv().unwrap();
1299        assert!(matches!(
1300            iv.aux_status,
1301            crate::error::AsynStatus::Disconnected
1302        ));
1303        assert_eq!(iv.alarm_status, 7);
1304        assert_eq!(iv.alarm_severity, 3);
1305    }
1306
1307    #[test]
1308    fn test_no_callback_for_unchanged() {
1309        let mut drv = TestDriver::new();
1310        let mut rx = drv.base_mut().interrupts.subscribe_async();
1311
1312        drv.base_mut().set_int32_param(0, 0, 5).unwrap();
1313        drv.base_mut().call_param_callbacks(0).unwrap();
1314        let _ = rx.try_recv().unwrap(); // consume
1315
1316        // Set same value — no interrupt
1317        drv.base_mut().set_int32_param(0, 0, 5).unwrap();
1318        drv.base_mut().call_param_callbacks(0).unwrap();
1319        assert!(rx.try_recv().is_err());
1320    }
1321
1322    #[test]
1323    fn test_array_not_supported_by_default() {
1324        let mut drv = TestDriver::new();
1325        let user = AsynUser::new(0);
1326        let mut buf = [0f64; 10];
1327        assert!(drv.read_float64_array(&user, &mut buf).is_err());
1328        assert!(drv.write_float64_array(&user, &[1.0]).is_err());
1329    }
1330
1331    #[test]
1332    fn test_option_set_get() {
1333        let mut drv = TestDriver::new();
1334        drv.set_option("baud", "9600").unwrap();
1335        assert_eq!(drv.get_option("baud").unwrap(), "9600");
1336        drv.set_option("baud", "115200").unwrap();
1337        assert_eq!(drv.get_option("baud").unwrap(), "115200");
1338    }
1339
1340    #[test]
1341    fn test_option_not_found() {
1342        let drv = TestDriver::new();
1343        let err = drv.get_option("nonexistent").unwrap_err();
1344        assert!(matches!(err, AsynError::OptionNotFound(_)));
1345    }
1346
1347    #[test]
1348    fn test_report_no_panic() {
1349        let mut drv = TestDriver::new();
1350        drv.set_option("testkey", "testval").unwrap();
1351        drv.base_mut().set_int32_param(0, 0, 42).unwrap();
1352        for level in 0..=3 {
1353            drv.report(level);
1354        }
1355    }
1356
1357    #[test]
1358    fn test_callback_uses_param_timestamp() {
1359        let mut drv = TestDriver::new();
1360        let mut rx = drv.base_mut().interrupts.subscribe_async();
1361
1362        let custom_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
1363        drv.base_mut().set_int32_param(0, 0, 77).unwrap();
1364        drv.base_mut().set_param_timestamp(0, 0, custom_ts).unwrap();
1365        drv.base_mut().call_param_callbacks(0).unwrap();
1366
1367        let v = rx.try_recv().unwrap();
1368        assert_eq!(v.reason, 0);
1369        assert_eq!(v.timestamp, custom_ts);
1370    }
1371
1372    #[test]
1373    fn test_default_read_write_enum() {
1374        use crate::param::EnumEntry;
1375
1376        let mut base = PortDriverBase::new("test_enum", 1, PortFlags::default());
1377        base.create_param("MODE", ParamType::Enum).unwrap();
1378
1379        struct EnumDriver {
1380            base: PortDriverBase,
1381        }
1382        impl PortDriver for EnumDriver {
1383            fn base(&self) -> &PortDriverBase {
1384                &self.base
1385            }
1386            fn base_mut(&mut self) -> &mut PortDriverBase {
1387                &mut self.base
1388            }
1389        }
1390
1391        let mut drv = EnumDriver { base };
1392        let choices: Arc<[EnumEntry]> = Arc::from(vec![
1393            EnumEntry {
1394                string: "Off".into(),
1395                value: 0,
1396                severity: 0,
1397            },
1398            EnumEntry {
1399                string: "On".into(),
1400                value: 1,
1401                severity: 0,
1402            },
1403        ]);
1404        let mut user = AsynUser::new(0);
1405        drv.write_enum_choices(&mut user, choices).unwrap();
1406        drv.write_enum(&mut user, 1).unwrap();
1407        let (idx, ch) = drv.read_enum(&AsynUser::new(0)).unwrap();
1408        assert_eq!(idx, 1);
1409        assert_eq!(ch[1].string, "On");
1410    }
1411
1412    #[test]
1413    fn test_enum_callback() {
1414        use crate::param::{EnumEntry, ParamValue};
1415
1416        let mut base = PortDriverBase::new("test_enum_cb", 1, PortFlags::default());
1417        base.create_param("MODE", ParamType::Enum).unwrap();
1418        let mut rx = base.interrupts.subscribe_async();
1419
1420        struct EnumDriver {
1421            base: PortDriverBase,
1422        }
1423        impl PortDriver for EnumDriver {
1424            fn base(&self) -> &PortDriverBase {
1425                &self.base
1426            }
1427            fn base_mut(&mut self) -> &mut PortDriverBase {
1428                &mut self.base
1429            }
1430        }
1431
1432        let mut drv = EnumDriver { base };
1433        let choices: Arc<[EnumEntry]> = Arc::from(vec![
1434            EnumEntry {
1435                string: "A".into(),
1436                value: 0,
1437                severity: 0,
1438            },
1439            EnumEntry {
1440                string: "B".into(),
1441                value: 1,
1442                severity: 0,
1443            },
1444        ]);
1445        drv.base_mut()
1446            .set_enum_choices_param(0, 0, choices)
1447            .unwrap();
1448        drv.base_mut().set_enum_index_param(0, 0, 1).unwrap();
1449        drv.base_mut().call_param_callbacks(0).unwrap();
1450
1451        let v = rx.try_recv().unwrap();
1452        assert_eq!(v.reason, 0);
1453        assert!(matches!(v.value, ParamValue::Enum { index: 1, .. }));
1454    }
1455
1456    #[test]
1457    fn test_default_read_write_generic_pointer() {
1458        let mut base = PortDriverBase::new("test_gp", 1, PortFlags::default());
1459        base.create_param("PTR", ParamType::GenericPointer).unwrap();
1460
1461        struct GpDriver {
1462            base: PortDriverBase,
1463        }
1464        impl PortDriver for GpDriver {
1465            fn base(&self) -> &PortDriverBase {
1466                &self.base
1467            }
1468            fn base_mut(&mut self) -> &mut PortDriverBase {
1469                &mut self.base
1470            }
1471        }
1472
1473        let mut drv = GpDriver { base };
1474        let data: Arc<dyn std::any::Any + Send + Sync> = Arc::new(99i32);
1475        let mut user = AsynUser::new(0);
1476        drv.write_generic_pointer(&mut user, data).unwrap();
1477        let val = drv.read_generic_pointer(&AsynUser::new(0)).unwrap();
1478        assert_eq!(*val.downcast_ref::<i32>().unwrap(), 99);
1479    }
1480
1481    #[test]
1482    fn test_generic_pointer_callback() {
1483        use crate::param::ParamValue;
1484
1485        let mut base = PortDriverBase::new("test_gp_cb", 1, PortFlags::default());
1486        base.create_param("PTR", ParamType::GenericPointer).unwrap();
1487        let mut rx = base.interrupts.subscribe_async();
1488
1489        struct GpDriver {
1490            base: PortDriverBase,
1491        }
1492        impl PortDriver for GpDriver {
1493            fn base(&self) -> &PortDriverBase {
1494                &self.base
1495            }
1496            fn base_mut(&mut self) -> &mut PortDriverBase {
1497                &mut self.base
1498            }
1499        }
1500
1501        let mut drv = GpDriver { base };
1502        let data: Arc<dyn std::any::Any + Send + Sync> = Arc::new(vec![1, 2, 3]);
1503        drv.base_mut()
1504            .set_generic_pointer_param(0, 0, data)
1505            .unwrap();
1506        drv.base_mut().call_param_callbacks(0).unwrap();
1507
1508        let v = rx.try_recv().unwrap();
1509        assert_eq!(v.reason, 0);
1510        assert!(matches!(v.value, ParamValue::GenericPointer(_)));
1511    }
1512
1513    #[test]
1514    fn test_interpose_push_requires_lock() {
1515        use crate::interpose::{OctetInterpose, OctetNext, OctetReadResult};
1516        use parking_lot::Mutex;
1517        use std::sync::Arc;
1518
1519        struct NoopInterpose;
1520        impl OctetInterpose for NoopInterpose {
1521            fn read(
1522                &mut self,
1523                user: &AsynUser,
1524                buf: &mut [u8],
1525                next: &mut dyn OctetNext,
1526            ) -> AsynResult<OctetReadResult> {
1527                next.read(user, buf)
1528            }
1529            fn write(
1530                &mut self,
1531                user: &mut AsynUser,
1532                data: &[u8],
1533                next: &mut dyn OctetNext,
1534            ) -> AsynResult<usize> {
1535                next.write(user, data)
1536            }
1537            fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
1538                next.flush(user)
1539            }
1540        }
1541
1542        let port: Arc<Mutex<dyn PortDriver>> = Arc::new(Mutex::new(TestDriver::new()));
1543
1544        {
1545            let mut guard = port.lock();
1546            guard
1547                .base_mut()
1548                .push_octet_interpose(Box::new(NoopInterpose));
1549            assert_eq!(guard.base().interpose_octet.len(), 1);
1550        }
1551    }
1552
1553    #[test]
1554    fn test_default_read_write_int64() {
1555        let mut base = PortDriverBase::new("test_i64", 1, PortFlags::default());
1556        base.create_param("BIG", ParamType::Int64).unwrap();
1557
1558        struct I64Driver {
1559            base: PortDriverBase,
1560        }
1561        impl PortDriver for I64Driver {
1562            fn base(&self) -> &PortDriverBase {
1563                &self.base
1564            }
1565            fn base_mut(&mut self) -> &mut PortDriverBase {
1566                &mut self.base
1567            }
1568        }
1569
1570        let mut drv = I64Driver { base };
1571        let mut user = AsynUser::new(0);
1572        drv.write_int64(&mut user, i64::MAX).unwrap();
1573        assert_eq!(drv.read_int64(&AsynUser::new(0)).unwrap(), i64::MAX);
1574    }
1575
1576    #[test]
1577    fn test_get_bounds_int64_default() {
1578        let base = PortDriverBase::new("test_bounds", 1, PortFlags::default());
1579        struct BoundsDriver {
1580            base: PortDriverBase,
1581        }
1582        impl PortDriver for BoundsDriver {
1583            fn base(&self) -> &PortDriverBase {
1584                &self.base
1585            }
1586            fn base_mut(&mut self) -> &mut PortDriverBase {
1587                &mut self.base
1588            }
1589        }
1590        let drv = BoundsDriver { base };
1591        let (lo, hi) = drv.get_bounds_int64(&AsynUser::default()).unwrap();
1592        assert_eq!(lo, i64::MIN);
1593        assert_eq!(hi, i64::MAX);
1594    }
1595
1596    #[test]
1597    fn test_per_addr_device_state() {
1598        let mut base = PortDriverBase::new(
1599            "multi",
1600            4,
1601            PortFlags {
1602                multi_device: true,
1603                can_block: false,
1604                destructible: true,
1605            },
1606        );
1607        base.create_param("V", ParamType::Int32).unwrap();
1608
1609        // Default: all connected
1610        assert!(base.is_device_connected(0));
1611        assert!(base.is_device_connected(1));
1612
1613        // Disable addr 1
1614        base.device_state(1).enabled = false;
1615        assert!(base.check_ready_addr(0).is_ok());
1616        let err = base.check_ready_addr(1).unwrap_err();
1617        assert!(format!("{err}").contains("disabled"));
1618
1619        // Disconnect addr 2
1620        base.device_state(2).connected = false;
1621        let err = base.check_ready_addr(2).unwrap_err();
1622        assert!(format!("{err}").contains("disconnected"));
1623    }
1624
1625    #[test]
1626    fn test_per_addr_single_device_ignored() {
1627        let mut base = PortDriverBase::new("single", 1, PortFlags::default());
1628        base.create_param("V", ParamType::Int32).unwrap();
1629        // For single-device, per-addr check passes even if no device state
1630        assert!(base.check_ready_addr(0).is_ok());
1631    }
1632
1633    #[test]
1634    fn test_timestamp_source() {
1635        let mut base = PortDriverBase::new("ts_test", 1, PortFlags::default());
1636        base.create_param("V", ParamType::Int32).unwrap();
1637
1638        let fixed_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(999999);
1639        base.register_timestamp_source(move || fixed_ts);
1640
1641        assert_eq!(base.current_timestamp(), fixed_ts);
1642    }
1643
1644    #[test]
1645    fn test_timestamp_source_in_callbacks() {
1646        let mut base = PortDriverBase::new("ts_cb", 1, PortFlags::default());
1647        base.create_param("V", ParamType::Int32).unwrap();
1648        let mut rx = base.interrupts.subscribe_async();
1649
1650        let fixed_ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(123456);
1651        base.register_timestamp_source(move || fixed_ts);
1652
1653        struct TsDriver {
1654            base: PortDriverBase,
1655        }
1656        impl PortDriver for TsDriver {
1657            fn base(&self) -> &PortDriverBase {
1658                &self.base
1659            }
1660            fn base_mut(&mut self) -> &mut PortDriverBase {
1661                &mut self.base
1662            }
1663        }
1664        let mut drv = TsDriver { base };
1665        drv.base_mut().set_int32_param(0, 0, 42).unwrap();
1666        drv.base_mut().call_param_callbacks(0).unwrap();
1667
1668        let v = rx.try_recv().unwrap();
1669        // Should use fixed_ts since no per-param timestamp is set
1670        assert_eq!(v.timestamp, fixed_ts);
1671    }
1672
1673    #[test]
1674    fn test_queue_priority_connect() {
1675        assert!(QueuePriority::Connect > QueuePriority::High);
1676    }
1677
1678    #[test]
1679    fn test_port_flags_destructible_default_is_opt_in() {
1680        // C asyn parity: ASYN_DESTRUCTIBLE (0x0004, asynDriver.h:97) is
1681        // a `registerPort` attribute that callers opt into. Default
1682        // must be false so drivers don't accidentally accept a
1683        // shutdownPort call. PortDriver authors that want shutdown
1684        // support set `destructible: true` explicitly.
1685        let flags = PortFlags::default();
1686        assert!(
1687            !flags.destructible,
1688            "destructible must be opt-in (C parity)"
1689        );
1690    }
1691
1692    #[test]
1693    fn shutdown_lifecycle_refuses_non_destructible() {
1694        let mut base = PortDriverBase::new(
1695            "p_nondestr",
1696            1,
1697            PortFlags {
1698                multi_device: false,
1699                can_block: false,
1700                destructible: false,
1701            },
1702        );
1703        match base.shutdown_lifecycle() {
1704            Err(AsynError::Status { message, .. }) => {
1705                assert!(message.contains("ASYN_DESTRUCTIBLE"), "msg={message}");
1706            }
1707            other => panic!("expected ASYN_DESTRUCTIBLE refusal, got {other:?}"),
1708        }
1709        assert!(
1710            !base.is_defunct(),
1711            "non-destructible port must not flip defunct"
1712        );
1713        assert!(base.is_enabled(), "non-destructible port must stay enabled");
1714    }
1715
1716    #[test]
1717    fn shutdown_lifecycle_marks_destructible_defunct_and_idempotent() {
1718        let mut base = PortDriverBase::new(
1719            "p_destr",
1720            1,
1721            PortFlags {
1722                multi_device: false,
1723                can_block: false,
1724                destructible: true,
1725            },
1726        );
1727        assert!(base.is_enabled());
1728        assert!(!base.is_defunct());
1729        base.shutdown_lifecycle().unwrap();
1730        assert!(
1731            !base.is_enabled(),
1732            "shutdown_lifecycle must flip enabled=false"
1733        );
1734        assert!(
1735            base.is_defunct(),
1736            "shutdown_lifecycle must flip defunct=true"
1737        );
1738        // Idempotent — second call is Ok and leaves state unchanged.
1739        base.shutdown_lifecycle().unwrap();
1740        assert!(base.is_defunct());
1741        // check_ready surfaces the defunct state for every request.
1742        match base.check_ready() {
1743            Err(AsynError::Status { message, .. }) => {
1744                assert!(message.contains("defunct"), "msg={message}");
1745            }
1746            other => panic!("expected defunct error, got {other:?}"),
1747        }
1748    }
1749
1750    // --- Phase 2B: per-addr connect/disconnect/enable/disable ---
1751
1752    #[test]
1753    fn test_connect_addr() {
1754        let mut base = PortDriverBase::new(
1755            "multi_conn",
1756            4,
1757            PortFlags {
1758                multi_device: true,
1759                can_block: false,
1760                destructible: true,
1761            },
1762        );
1763        base.create_param("V", ParamType::Int32).unwrap();
1764
1765        base.disconnect_addr(1);
1766        assert!(!base.is_device_connected(1));
1767        assert!(base.check_ready_addr(1).is_err());
1768
1769        base.connect_addr(1);
1770        assert!(base.is_device_connected(1));
1771        assert!(base.check_ready_addr(1).is_ok());
1772    }
1773
1774    #[test]
1775    fn test_enable_disable_addr() {
1776        let mut base = PortDriverBase::new(
1777            "multi_en",
1778            4,
1779            PortFlags {
1780                multi_device: true,
1781                can_block: false,
1782                destructible: true,
1783            },
1784        );
1785        base.create_param("V", ParamType::Int32).unwrap();
1786
1787        base.disable_addr(2);
1788        let err = base.check_ready_addr(2).unwrap_err();
1789        assert!(format!("{err}").contains("disabled"));
1790
1791        base.enable_addr(2);
1792        assert!(base.check_ready_addr(2).is_ok());
1793    }
1794
1795    #[test]
1796    fn test_port_level_overrides_addr() {
1797        let mut base = PortDriverBase::new(
1798            "multi_override",
1799            4,
1800            PortFlags {
1801                multi_device: true,
1802                can_block: false,
1803                destructible: true,
1804            },
1805        );
1806        base.create_param("V", ParamType::Int32).unwrap();
1807
1808        // Port-level disabled overrides addr-level enabled
1809        base.enabled = false;
1810        base.enable_addr(0); // addr 0 is enabled, but port is disabled
1811        let err = base.check_ready_addr(0).unwrap_err();
1812        assert!(format!("{err}").contains("disabled"));
1813    }
1814
1815    #[test]
1816    fn test_per_addr_exception_announced() {
1817        use std::sync::atomic::{AtomicI32, Ordering};
1818
1819        let mut base = PortDriverBase::new(
1820            "multi_exc",
1821            4,
1822            PortFlags {
1823                multi_device: true,
1824                can_block: false,
1825                destructible: true,
1826            },
1827        );
1828        base.create_param("V", ParamType::Int32).unwrap();
1829
1830        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
1831        base.exception_sink = Some(exc_mgr.clone());
1832
1833        let last_addr = Arc::new(AtomicI32::new(-99));
1834        let last_addr2 = last_addr.clone();
1835        exc_mgr.add_callback(move |event| {
1836            last_addr2.store(event.addr, Ordering::Relaxed);
1837        });
1838
1839        base.disconnect_addr(3);
1840        assert_eq!(last_addr.load(Ordering::Relaxed), 3);
1841
1842        base.enable_addr(2);
1843        assert_eq!(last_addr.load(Ordering::Relaxed), 2);
1844    }
1845
1846    /// C parity (asynManager.c:2151-2160 exceptionConnect,
1847    /// :2174-2185 exceptionDisconnect): redundant connect/disconnect
1848    /// on a port already in that state must NOT fan out a duplicate
1849    /// `asynExceptionConnect`. Subscribers depend on the event
1850    /// edge — duplicate fan-out causes them to e.g. re-subscribe or
1851    /// re-arm timers that should fire exactly once per transition.
1852    #[test]
1853    fn test_connect_disconnect_announce_only_on_transition() {
1854        use std::sync::atomic::{AtomicUsize, Ordering};
1855
1856        let mut base = PortDriverBase::new(
1857            "edge",
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        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
1867        base.exception_sink = Some(exc_mgr.clone());
1868
1869        let connect_hits = Arc::new(AtomicUsize::new(0));
1870        let hits2 = connect_hits.clone();
1871        exc_mgr.add_callback(move |event| {
1872            if event.exception == AsynException::Connect {
1873                hits2.fetch_add(1, Ordering::Relaxed);
1874            }
1875        });
1876
1877        // device starts connected by DeviceState::default — a redundant
1878        // connect_addr is a no-op.
1879        base.connect_addr(2);
1880        assert_eq!(
1881            connect_hits.load(Ordering::Relaxed),
1882            0,
1883            "redundant connect_addr must not fan out"
1884        );
1885
1886        // First transition fires once.
1887        base.disconnect_addr(2);
1888        assert_eq!(connect_hits.load(Ordering::Relaxed), 1);
1889
1890        // Redundant disconnect is silent.
1891        base.disconnect_addr(2);
1892        assert_eq!(
1893            connect_hits.load(Ordering::Relaxed),
1894            1,
1895            "redundant disconnect_addr must not fan out"
1896        );
1897
1898        // Re-connect fires the transition.
1899        base.connect_addr(2);
1900        assert_eq!(connect_hits.load(Ordering::Relaxed), 2);
1901    }
1902
1903    /// C parity: `autoConnectAsyn` (asynManager.c:2310-2324) fires
1904    /// `asynExceptionAutoConnect` unconditionally — even setting the
1905    /// same value as the current one. Rust mirrors that so observers
1906    /// can refresh their UI after a re-confirmation, not just an edge.
1907    #[test]
1908    fn test_set_auto_connect_fires_unconditionally() {
1909        use std::sync::atomic::{AtomicUsize, Ordering};
1910
1911        let mut base = PortDriverBase::new("ac", 1, PortFlags::default());
1912        let exc_mgr = Arc::new(crate::exception::ExceptionManager::new());
1913        base.exception_sink = Some(exc_mgr.clone());
1914        let hits = Arc::new(AtomicUsize::new(0));
1915        let hits2 = hits.clone();
1916        exc_mgr.add_callback(move |event| {
1917            if event.exception == AsynException::AutoConnect {
1918                hits2.fetch_add(1, Ordering::Relaxed);
1919            }
1920        });
1921        // base.auto_connect defaults to true — setting true again
1922        // still must fire (no state-change guard in C).
1923        base.set_auto_connect(true);
1924        base.set_auto_connect(false);
1925        base.set_auto_connect(false);
1926        assert_eq!(hits.load(Ordering::Relaxed), 3);
1927    }
1928
1929    /// C parity: `asynPortDriver::setInterruptUInt32Digital` /
1930    /// `clearInterruptUInt32Digital` / `getInterruptUInt32Digital`
1931    /// (`asynPortDriver.cpp:2346-2461`) route through paramList. The
1932    /// PortDriver trait default delegates to the param store; we
1933    /// verify the round-trip end-to-end through the trait surface.
1934    #[test]
1935    fn test_port_driver_uint32_interrupt_round_trip() {
1936        struct UInt32Drv {
1937            base: PortDriverBase,
1938        }
1939        impl PortDriver for UInt32Drv {
1940            fn base(&self) -> &PortDriverBase {
1941                &self.base
1942            }
1943            fn base_mut(&mut self) -> &mut PortDriverBase {
1944                &mut self.base
1945            }
1946        }
1947
1948        let mut base = PortDriverBase::new("uint32_int", 1, PortFlags::default());
1949        let idx = base
1950            .params
1951            .create_param("BITS", ParamType::UInt32Digital)
1952            .unwrap();
1953        let mut drv = UInt32Drv { base };
1954        let user = AsynUser::new(idx).with_addr(0);
1955
1956        drv.set_interrupt_uint32_digital(&user, 0xF0, InterruptReason::ZeroToOne)
1957            .unwrap();
1958        drv.set_interrupt_uint32_digital(&user, 0x0F, InterruptReason::OneToZero)
1959            .unwrap();
1960        assert_eq!(
1961            drv.get_interrupt_uint32_digital(&user, InterruptReason::Both)
1962                .unwrap(),
1963            0xFF
1964        );
1965        drv.clear_interrupt_uint32_digital(&user, 0x11).unwrap();
1966        assert_eq!(
1967            drv.get_interrupt_uint32_digital(&user, InterruptReason::ZeroToOne)
1968                .unwrap(),
1969            0xE0
1970        );
1971        assert_eq!(
1972            drv.get_interrupt_uint32_digital(&user, InterruptReason::OneToZero)
1973                .unwrap(),
1974            0x0E
1975        );
1976    }
1977}