Skip to main content

asyn_rs/
param.rs

1use std::any::Any;
2use std::collections::HashMap;
3use std::fmt;
4use std::sync::Arc;
5use std::time::SystemTime;
6
7use crate::error::{AsynError, AsynResult, AsynStatus};
8
9/// A single entry in an enumeration parameter's choice list.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EnumEntry {
12    pub string: String,
13    pub value: i32,
14    pub severity: u16,
15}
16
17/// UInt32Digital interrupt-reason selector — C parity for
18/// `interruptReason` (`interfaces/asynUInt32Digital.h:25-27`).
19///
20/// `ZeroToOne` (rising) and `OneToZero` (falling) configure each
21/// mask in isolation; `Both` overwrites them together on set and
22/// returns `rising | falling` on get (matching
23/// `asynPortDriver.cpp:480-535`).
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum InterruptReason {
26    ZeroToOne,
27    OneToZero,
28    Both,
29}
30
31/// Parameter data types.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ParamType {
34    Int32,
35    Int64,
36    UInt64,
37    Float64,
38    Octet,
39    UInt32Digital,
40    Int8Array,
41    Int16Array,
42    Int32Array,
43    Int64Array,
44    UInt64Array,
45    Float32Array,
46    Float64Array,
47    Enum,
48    GenericPointer,
49}
50
51/// Parameter value. Arrays use `Arc<[T]>` for cheap cloning during interrupt broadcast.
52/// Octet uses String (typically short; not intended for large binary data).
53#[derive(Clone)]
54pub enum ParamValue {
55    Int32(i32),
56    Int64(i64),
57    /// Unsigned 64-bit integer (asyn upstream issue #231).
58    UInt64(u64),
59    Float64(f64),
60    Octet(String),
61    UInt32Digital(u32),
62    Int8Array(Arc<[i8]>),
63    Int16Array(Arc<[i16]>),
64    Int32Array(Arc<[i32]>),
65    Int64Array(Arc<[i64]>),
66    /// Unsigned 64-bit integer array (asyn upstream issue #231).
67    UInt64Array(Arc<[u64]>),
68    Float32Array(Arc<[f32]>),
69    Float64Array(Arc<[f64]>),
70    Enum {
71        index: usize,
72        choices: Arc<[EnumEntry]>,
73    },
74    GenericPointer(Arc<dyn Any + Send + Sync>),
75    Undefined,
76}
77
78impl fmt::Debug for ParamValue {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::Int32(v) => write!(f, "Int32({v:?})"),
82            Self::Int64(v) => write!(f, "Int64({v:?})"),
83            Self::UInt64(v) => write!(f, "UInt64({v:?})"),
84            Self::Float64(v) => write!(f, "Float64({v:?})"),
85            Self::Octet(v) => write!(f, "Octet({v:?})"),
86            Self::UInt32Digital(v) => write!(f, "UInt32Digital({v:?})"),
87            Self::Int8Array(v) => write!(f, "Int8Array({v:?})"),
88            Self::Int16Array(v) => write!(f, "Int16Array({v:?})"),
89            Self::Int32Array(v) => write!(f, "Int32Array({v:?})"),
90            Self::Int64Array(v) => write!(f, "Int64Array({v:?})"),
91            Self::UInt64Array(v) => write!(f, "UInt64Array({v:?})"),
92            Self::Float32Array(v) => write!(f, "Float32Array({v:?})"),
93            Self::Float64Array(v) => write!(f, "Float64Array({v:?})"),
94            Self::Enum { index, choices } => write!(f, "Enum(index={index}, choices={choices:?})"),
95            Self::GenericPointer(v) => write!(f, "GenericPointer(<{:?}>)", (*v).type_id()),
96            Self::Undefined => write!(f, "Undefined"),
97        }
98    }
99}
100
101impl ParamValue {
102    pub fn type_name(&self) -> &'static str {
103        match self {
104            Self::Int32(_) => "Int32",
105            Self::Int64(_) => "Int64",
106            Self::UInt64(_) => "UInt64",
107            Self::Float64(_) => "Float64",
108            Self::Octet(_) => "Octet",
109            Self::UInt32Digital(_) => "UInt32Digital",
110            Self::Int8Array(_) => "Int8Array",
111            Self::Int16Array(_) => "Int16Array",
112            Self::Int32Array(_) => "Int32Array",
113            Self::Int64Array(_) => "Int64Array",
114            Self::UInt64Array(_) => "UInt64Array",
115            Self::Float32Array(_) => "Float32Array",
116            Self::Float64Array(_) => "Float64Array",
117            Self::Enum { .. } => "Enum",
118            Self::GenericPointer(_) => "GenericPointer",
119            Self::Undefined => "Undefined",
120        }
121    }
122
123    /// Read this value through the **asynInt32** interface — the single owner
124    /// of that interface's read rule, shared by the parameter-store getters
125    /// ([`ParamList::get_int32`]) and the device-support I/O-Intr path, so a
126    /// value delivered to a record cannot be typed differently from the same
127    /// value read by a poll.
128    ///
129    /// A parameter whose type the interface cannot read is an
130    /// [`AsynError::TypeMismatch`], never a coercion: C keeps one interrupt
131    /// list per interface, so a record on `asynInt32` is only ever handed an
132    /// int32 (an enum's index included — asynInt32 reads an enum's index
133    /// transparently).
134    pub fn as_int32(&self) -> AsynResult<i32> {
135        match self {
136            Self::Int32(v) => Ok(*v),
137            Self::Enum { index, .. } => Ok(*index as i32),
138            other => Err(AsynError::TypeMismatch {
139                expected: "Int32",
140                actual: other.type_name(),
141            }),
142        }
143    }
144
145    /// Read this value through the **asynInt64** interface. See [`Self::as_int32`].
146    pub fn as_int64(&self) -> AsynResult<i64> {
147        match self {
148            Self::Int64(v) => Ok(*v),
149            other => Err(AsynError::TypeMismatch {
150                expected: "Int64",
151                actual: other.type_name(),
152            }),
153        }
154    }
155
156    /// Read this value through the **asynFloat64** interface. See [`Self::as_int32`].
157    pub fn as_float64(&self) -> AsynResult<f64> {
158        match self {
159            Self::Float64(v) => Ok(*v),
160            other => Err(AsynError::TypeMismatch {
161                expected: "Float64",
162                actual: other.type_name(),
163            }),
164        }
165    }
166
167    /// Read this value through the **asynOctet** interface. See [`Self::as_int32`].
168    pub fn as_octet(&self) -> AsynResult<&str> {
169        match self {
170            Self::Octet(s) => Ok(s),
171            other => Err(AsynError::TypeMismatch {
172                expected: "Octet",
173                actual: other.type_name(),
174            }),
175        }
176    }
177
178    /// Read this value through the **asynUInt32Digital** interface. See
179    /// [`Self::as_int32`].
180    pub fn as_uint32(&self) -> AsynResult<u32> {
181        match self {
182            Self::UInt32Digital(v) => Ok(*v),
183            other => Err(AsynError::TypeMismatch {
184                expected: "UInt32Digital",
185                actual: other.type_name(),
186            }),
187        }
188    }
189
190    /// Read this value through the **asynEnum** interface. See [`Self::as_int32`].
191    pub fn as_enum(&self) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
192        match self {
193            Self::Enum { index, choices } => Ok((*index, choices.clone())),
194            other => Err(AsynError::TypeMismatch {
195                expected: "Enum",
196                actual: other.type_name(),
197            }),
198        }
199    }
200
201    /// Whether this is an array or generic-pointer value. C asynPortDriver
202    /// fires interrupts for these through the per-type doCallbacks*Array /
203    /// doCallbacksGenericPointer paths, NOT `paramList::callCallbacks`
204    /// (whose switch at asynPortDriver.cpp:846-865 handles only scalars).
205    /// The callCallbacks isDefined gate (:845) therefore does not govern
206    /// them, so the Rust flush must keep firing array triggers regardless
207    /// of the `defined` flag.
208    pub fn is_array(&self) -> bool {
209        matches!(
210            self,
211            Self::Int8Array(_)
212                | Self::Int16Array(_)
213                | Self::Int32Array(_)
214                | Self::Int64Array(_)
215                | Self::UInt64Array(_)
216                | Self::Float32Array(_)
217                | Self::Float64Array(_)
218                | Self::GenericPointer(_)
219        )
220    }
221}
222
223#[derive(Debug, Clone)]
224struct ParamEntry {
225    name: String,
226    param_type: ParamType,
227    value: ParamValue,
228    /// Whether the value has been explicitly set (C parity: valueDefined).
229    defined: bool,
230    status: AsynStatus,
231    alarm_status: u16,
232    alarm_severity: u16,
233    value_changed: bool,
234    timestamp: Option<SystemTime>,
235    /// UInt32Digital: bitmask of bits that changed (for interrupt filtering).
236    /// C parity: uInt32CallbackMask in paramVal.
237    uint32_interrupt_mask: u32,
238    /// UInt32Digital: bits the driver wants to fire callbacks on when
239    /// they transition `0 → 1`. C parity: `paramVal::uInt32RisingMask`
240    /// (`asynPortDriver/paramVal.h:45`) — written by
241    /// `paramList::setUInt32Interrupt(idx, mask, interruptOnZeroToOne)`
242    /// (`asynPortDriver.cpp:480-497`), read by `report`/`getInterrupt`.
243    uint32_rising_mask: u32,
244    /// UInt32Digital: bits the driver wants to fire callbacks on when
245    /// they transition `1 → 0`. C parity: `paramVal::uInt32FallingMask`
246    /// (`asynPortDriver/paramVal.h:46`).
247    uint32_falling_mask: u32,
248}
249
250impl ParamEntry {
251    fn new(name: String, param_type: ParamType) -> Self {
252        // C parity: parameters start with default values but are marked as
253        // "not yet defined" (valueDefined=false in C). In C, getters still
254        // return the default value but also return asynParamUndefined status.
255        // In Rust, we keep the values accessible but track the defined flag.
256        let value = match param_type {
257            ParamType::Int32 => ParamValue::Int32(0),
258            ParamType::Int64 => ParamValue::Int64(0),
259            ParamType::UInt64 => ParamValue::UInt64(0),
260            ParamType::Float64 => ParamValue::Float64(0.0),
261            ParamType::Octet => ParamValue::Octet(String::new()),
262            ParamType::UInt32Digital => ParamValue::UInt32Digital(0),
263            ParamType::Int8Array => ParamValue::Int8Array(Arc::from([] as [i8; 0])),
264            ParamType::Int16Array => ParamValue::Int16Array(Arc::from([] as [i16; 0])),
265            ParamType::Int32Array => ParamValue::Int32Array(Arc::from([] as [i32; 0])),
266            ParamType::Int64Array => ParamValue::Int64Array(Arc::from([] as [i64; 0])),
267            ParamType::UInt64Array => ParamValue::UInt64Array(Arc::from([] as [u64; 0])),
268            ParamType::Float32Array => ParamValue::Float32Array(Arc::from([] as [f32; 0])),
269            ParamType::Float64Array => ParamValue::Float64Array(Arc::from([] as [f64; 0])),
270            ParamType::Enum => ParamValue::Enum {
271                index: 0,
272                choices: Arc::from([EnumEntry {
273                    string: String::new(),
274                    value: 0,
275                    severity: 0,
276                }]),
277            },
278            ParamType::GenericPointer => ParamValue::GenericPointer(Arc::new(())),
279        };
280        Self {
281            name,
282            param_type,
283            value,
284            defined: false,
285            status: AsynStatus::Success,
286            alarm_status: 0,
287            alarm_severity: 0,
288            value_changed: false,
289            timestamp: None,
290            uint32_interrupt_mask: 0,
291            uint32_rising_mask: 0,
292            uint32_falling_mask: 0,
293        }
294    }
295}
296
297/// Parameter library managing named parameters indexed by integer.
298/// Supports multiple addresses (addr 0..max_addr).
299pub struct ParamList {
300    max_addr: usize,
301    multi_device: bool,
302    /// `params[addr][index] = ParamEntry`
303    params: Vec<Vec<ParamEntry>>,
304    name_to_index: HashMap<String, usize>,
305}
306
307impl ParamList {
308    pub fn new(max_addr: usize, multi_device: bool) -> Self {
309        let max_addr = max_addr.max(1);
310        Self {
311            max_addr,
312            multi_device,
313            params: (0..max_addr).map(|_| Vec::new()).collect(),
314            name_to_index: HashMap::new(),
315        }
316    }
317
318    /// Normalize and validate address.
319    /// - multi_device=false: any addr normalizes to 0
320    /// - multi_device=true: addr must be in [0, max_addr)
321    fn validate_addr(&self, addr: i32) -> AsynResult<usize> {
322        if !self.multi_device {
323            return Ok(0);
324        }
325        if addr < 0 || (addr as usize) >= self.max_addr {
326            return Err(AsynError::AddressOutOfRange(addr));
327        }
328        Ok(addr as usize)
329    }
330
331    fn get_entry(&self, index: usize, addr: i32) -> AsynResult<&ParamEntry> {
332        let a = self.validate_addr(addr)?;
333        self.params[a]
334            .get(index)
335            .ok_or(AsynError::ParamIndexOutOfRange(index))
336    }
337
338    fn get_entry_mut(&mut self, index: usize, addr: i32) -> AsynResult<&mut ParamEntry> {
339        let a = self.validate_addr(addr)?;
340        self.params[a]
341            .get_mut(index)
342            .ok_or(AsynError::ParamIndexOutOfRange(index))
343    }
344
345    /// Create a parameter. Returns its index.
346    /// The parameter is created at all addresses.
347    ///
348    /// Lax variant — when `name` already exists this returns the
349    /// existing index silently (`asynSuccess`). That matches the
350    /// idempotent build pattern used by `ad-core-rs::ADDriverParams`
351    /// and `ad-plugins-rs::NDArrayDriverParams` (the latter create the
352    /// shared `ACQUIRE`/`ACQUIRE_BUSY`/`WAIT_FOR_PLUGINS` params, then
353    /// `ADDriverParams::create` re-issues `create_param` on the same
354    /// names).
355    ///
356    /// Use [`Self::create_param_strict`] when you need C parity for
357    /// `asynParamAlreadyExists` — `paramList::createParam`
358    /// (`asynPortDriver/asynPortDriver.cpp:126-138`) returns that
359    /// status on duplicate and the wrapper at
360    /// `asynPortDriver::createParam(int list, ...)` (lines 991-1011)
361    /// translates it to `asynError` with an `ASYN_TRACE_ERROR` log.
362    pub fn create_param(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
363        if let Some(&idx) = self.name_to_index.get(name) {
364            return Ok(idx);
365        }
366        self.append_param(name, param_type)
367    }
368
369    /// Strict variant of [`Self::create_param`] — surfaces
370    /// [`AsynError::ParamAlreadyExists`] on duplicate, matching the C
371    /// `asynParamAlreadyExists` status returned by
372    /// `paramList::createParam` (`asynPortDriver.cpp:126-138`).
373    ///
374    /// Use this from new callers that should match the C semantics.
375    /// Existing callers (`ad-core-rs`, `ad-plugins-rs`) keep using the
376    /// lax [`Self::create_param`] until they migrate; switching them
377    /// requires teaching the duplicate-name shared-base-class build
378    /// to look up the existing index instead of re-issuing
379    /// `create_param`.
380    pub fn create_param_strict(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
381        if self.name_to_index.contains_key(name) {
382            return Err(AsynError::ParamAlreadyExists(name.to_string()));
383        }
384        self.append_param(name, param_type)
385    }
386
387    fn append_param(&mut self, name: &str, param_type: ParamType) -> AsynResult<usize> {
388        let index = self.params[0].len();
389        for addr_params in &mut self.params {
390            addr_params.push(ParamEntry::new(name.to_string(), param_type));
391        }
392        self.name_to_index.insert(name.to_string(), index);
393        Ok(index)
394    }
395
396    /// Find parameter index by name.
397    pub fn find_param(&self, name: &str) -> Option<usize> {
398        self.name_to_index.get(name).copied()
399    }
400
401    /// Get parameter name by index.
402    pub fn param_name(&self, index: usize) -> Option<&str> {
403        self.params[0].get(index).map(|e| e.name.as_str())
404    }
405
406    /// Get parameter type by index.
407    pub fn param_type(&self, index: usize) -> Option<ParamType> {
408        self.params[0].get(index).map(|e| e.param_type)
409    }
410
411    /// Get the raw ParamValue.
412    pub fn get_value(&self, index: usize, addr: i32) -> AsynResult<&ParamValue> {
413        Ok(&self.get_entry(index, addr)?.value)
414    }
415
416    // --- Scalar getters/setters ---
417
418    /// Read the cached Int32 value, falling back to the type default
419    /// (`0`) if the parameter has not been set.
420    ///
421    /// Use [`Self::get_int32_strict`] when you need C parity for
422    /// `asynParamUndefined` — the lax variant matches the
423    /// `.unwrap_or(0)` convention used throughout the workspace and
424    /// stays callable on never-set params.
425    pub fn get_int32(&self, index: usize, addr: i32) -> AsynResult<i32> {
426        self.get_entry(index, addr)?.value.as_int32()
427    }
428
429    /// Strict variant of [`Self::get_int32`] — returns
430    /// [`AsynError::ParamUndefined`] when the entry has never been set.
431    ///
432    /// C parity: `paramVal::getInteger` (`paramVal.cpp:147-155`) checks
433    /// `WrongType` first then `NotDefined`; the wrapping
434    /// `paramList::getInteger` (`asynPortDriver.cpp:301-322`) translates
435    /// `ParamValNotDefined` to `asynParamUndefined`.
436    pub fn get_int32_strict(&self, index: usize, addr: i32) -> AsynResult<i32> {
437        let entry = self.get_entry(index, addr)?;
438        // C order: WrongType first (`as_int32`), then NotDefined.
439        let value = entry.value.as_int32()?;
440        if !entry.defined {
441            return Err(AsynError::ParamUndefined(index));
442        }
443        Ok(value)
444    }
445
446    pub fn set_int32(&mut self, index: usize, addr: i32, value: i32) -> AsynResult<()> {
447        let entry = self.get_entry_mut(index, addr)?;
448        match entry.value {
449            ParamValue::Int32(ref old) => {
450                // C parity: paramVal::setInteger gates on
451                // `!isDefined() || (data.ival != value)` (paramVal.cpp:130-138);
452                // a first set with `value == default` still flips `defined`.
453                if !entry.defined || *old != value {
454                    entry.value = ParamValue::Int32(value);
455                    entry.value_changed = true;
456                    entry.defined = true;
457                }
458            }
459            // C EPICS asyn: asynInt32 interface writes enum index transparently
460            ParamValue::Enum {
461                ref choices,
462                ref mut index,
463            } => {
464                let new_idx = value as usize;
465                if new_idx >= choices.len() {
466                    return Err(AsynError::Status {
467                        status: AsynStatus::Error,
468                        message: format!(
469                            "enum index {new_idx} out of range (0..{})",
470                            choices.len()
471                        ),
472                    });
473                }
474                if !entry.defined || *index != new_idx {
475                    *index = new_idx;
476                    entry.value_changed = true;
477                    entry.defined = true;
478                }
479            }
480            _ => {
481                return Err(AsynError::TypeMismatch {
482                    expected: "Int32",
483                    actual: entry.value.type_name(),
484                });
485            }
486        }
487        Ok(())
488    }
489
490    /// Lax variant — returns `0.0` for an undefined Float64. Use
491    /// [`Self::get_float64_strict`] for C parity.
492    pub fn get_float64(&self, index: usize, addr: i32) -> AsynResult<f64> {
493        self.get_entry(index, addr)?.value.as_float64()
494    }
495
496    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
497    /// entry has never been set. C parity:
498    /// `paramVal::getDouble` (`paramVal.cpp:258-266`) +
499    /// `paramList::getDouble` (`asynPortDriver.cpp:383-401`).
500    pub fn get_float64_strict(&self, index: usize, addr: i32) -> AsynResult<f64> {
501        let entry = self.get_entry(index, addr)?;
502        let value = entry.value.as_float64()?;
503        if !entry.defined {
504            return Err(AsynError::ParamUndefined(index));
505        }
506        Ok(value)
507    }
508
509    pub fn set_float64(&mut self, index: usize, addr: i32, value: f64) -> AsynResult<()> {
510        let entry = self.get_entry_mut(index, addr)?;
511        if let ParamValue::Float64(ref old) = entry.value {
512            // C parity: paramVal::setDouble (paramVal.cpp:241-252) —
513            // `!isDefined() || data.dval != value` flips defined on the
514            // first set even when `value` equals the type default.
515            if !entry.defined || *old != value {
516                entry.value = ParamValue::Float64(value);
517                entry.value_changed = true;
518                entry.defined = true;
519            }
520        } else {
521            return Err(AsynError::TypeMismatch {
522                expected: "Float64",
523                actual: entry.value.type_name(),
524            });
525        }
526        Ok(())
527    }
528
529    /// Lax variant — returns `0` for an undefined Int64. Use
530    /// [`Self::get_int64_strict`] for C parity.
531    pub fn get_int64(&self, index: usize, addr: i32) -> AsynResult<i64> {
532        self.get_entry(index, addr)?.value.as_int64()
533    }
534
535    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
536    /// entry has never been set. C parity: `paramVal::getInteger64`
537    /// (`paramVal.cpp:176-184`) + `paramList::getInteger64`
538    /// (`asynPortDriver.cpp:328-349`).
539    pub fn get_int64_strict(&self, index: usize, addr: i32) -> AsynResult<i64> {
540        let entry = self.get_entry(index, addr)?;
541        let value = entry.value.as_int64()?;
542        if !entry.defined {
543            return Err(AsynError::ParamUndefined(index));
544        }
545        Ok(value)
546    }
547
548    pub fn set_int64(&mut self, index: usize, addr: i32, value: i64) -> AsynResult<()> {
549        let entry = self.get_entry_mut(index, addr)?;
550        if let ParamValue::Int64(ref old) = entry.value {
551            // C parity: paramVal::setInteger64 (paramVal.cpp:189-200).
552            if !entry.defined || *old != value {
553                entry.value = ParamValue::Int64(value);
554                entry.value_changed = true;
555                entry.defined = true;
556            }
557        } else {
558            return Err(AsynError::TypeMismatch {
559                expected: "Int64",
560                actual: entry.value.type_name(),
561            });
562        }
563        Ok(())
564    }
565
566    /// Lax variant — returns the empty string for an undefined Octet.
567    /// Use [`Self::get_string_strict`] for C parity.
568    pub fn get_string(&self, index: usize, addr: i32) -> AsynResult<&str> {
569        self.get_entry(index, addr)?.value.as_octet()
570    }
571
572    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
573    /// entry has never been set. C parity: `paramVal::getString`
574    /// (`paramVal.cpp:283-292`) + `paramList::getString`
575    /// (`asynPortDriver.cpp:543-566`).
576    pub fn get_string_strict(&self, index: usize, addr: i32) -> AsynResult<&str> {
577        let entry = self.get_entry(index, addr)?;
578        let value = entry.value.as_octet()?;
579        if !entry.defined {
580            return Err(AsynError::ParamUndefined(index));
581        }
582        Ok(value)
583    }
584
585    pub fn set_string(&mut self, index: usize, addr: i32, value: String) -> AsynResult<()> {
586        let entry = self.get_entry_mut(index, addr)?;
587        if let ParamValue::Octet(ref old) = entry.value {
588            // C parity: paramVal::setString (paramVal.cpp:271-281) —
589            // `!isDefined() || sval != value`.
590            if !entry.defined || *old != value {
591                entry.value = ParamValue::Octet(value);
592                entry.value_changed = true;
593                entry.defined = true;
594            }
595        } else {
596            return Err(AsynError::TypeMismatch {
597                expected: "Octet",
598                actual: entry.value.type_name(),
599            });
600        }
601        Ok(())
602    }
603
604    /// Lax variant — returns `0` for an undefined UInt32Digital. Use
605    /// [`Self::get_uint32_strict`] for C parity.
606    pub fn get_uint32(&self, index: usize, addr: i32) -> AsynResult<u32> {
607        self.get_entry(index, addr)?.value.as_uint32()
608    }
609
610    /// Strict variant — returns [`AsynError::ParamUndefined`] when the
611    /// entry has never been set. C parity: `paramVal::getUInt32`
612    /// (`paramVal.cpp:215-223`) + `paramList::getUInt32`
613    /// (`asynPortDriver.cpp:355-376`).
614    pub fn get_uint32_strict(&self, index: usize, addr: i32) -> AsynResult<u32> {
615        let entry = self.get_entry(index, addr)?;
616        let value = entry.value.as_uint32()?;
617        if !entry.defined {
618            return Err(AsynError::ParamUndefined(index));
619        }
620        Ok(value)
621    }
622
623    pub fn set_uint32(
624        &mut self,
625        index: usize,
626        addr: i32,
627        value: u32,
628        mask: u32,
629        interrupt_mask: u32,
630    ) -> AsynResult<()> {
631        let entry = self.get_entry_mut(index, addr)?;
632        if let ParamValue::UInt32Digital(ref old) = entry.value {
633            // C parity: paramVal::setUInt32 (paramVal.cpp:198-225).
634            //   if (!isDefined()) { uival = 0; setDefined(true); setValueChanged(); }
635            //   newValue = (uival & ~mask) | (value & mask);
636            //   if (uival != newValue) { callbackMask |= (uival ^ newValue); ... }
637            // The first write must flip `defined` and mark `value_changed`
638            // BEFORE any merge, even if `value & mask == 0` — otherwise
639            // the equivalent of `setUIntDigitalParam(idx, 0, mask)` on a
640            // fresh param leaves the entry undefined and never notifies.
641            let was_defined = entry.defined;
642            let starting = if was_defined { *old } else { 0 };
643            let new_val = (starting & !mask) | (value & mask);
644            let changed_bits = if was_defined {
645                starting ^ new_val
646            } else {
647                new_val
648            };
649            if !was_defined || starting != new_val {
650                // C parity: `uInt32CallbackMask |= (data.uival ^ newValue)`
651                // (paramVal.cpp:215) — accumulate the union of changed bits
652                // across every setUInt32 before one callParamCallbacks, not
653                // just the last set's bits. The flush owner resets the mask
654                // to 0 after firing (see take_uint32_interrupt_mask).
655                entry.uint32_interrupt_mask |= changed_bits;
656                entry.value = ParamValue::UInt32Digital(new_val);
657                entry.value_changed = true;
658                entry.defined = true;
659            }
660            // C parity: paramVal.cpp:220-224 — a non-zero `interruptMask`
661            // forces those bits into the callback mask and marks the value
662            // changed EVEN when the stored value is unchanged, so I/O Intr
663            // callbacks gated on those bits still fire. This is the
664            // `setUIntDigitalParam(.., interruptMask)` overload path; the
665            // first-write `defined` flip above already ran when needed.
666            if interrupt_mask != 0 {
667                entry.uint32_interrupt_mask |= interrupt_mask;
668                entry.value_changed = true;
669            }
670        } else {
671            return Err(AsynError::TypeMismatch {
672                expected: "UInt32Digital",
673                actual: entry.value.type_name(),
674            });
675        }
676        Ok(())
677    }
678
679    /// Get the UInt32Digital interrupt mask (accumulated changed bits).
680    pub fn get_uint32_interrupt_mask(&self, index: usize, addr: i32) -> AsynResult<u32> {
681        Ok(self.get_entry(index, addr)?.uint32_interrupt_mask)
682    }
683
684    /// Read the accumulated UInt32Digital callback mask AND reset it to 0.
685    ///
686    /// C parity: `uint32Callback` clears `uInt32CallbackMask = 0`
687    /// immediately after firing (`asynPortDriver.cpp:855`). The flush is
688    /// the single owner of this transition — it consumes the mask so the
689    /// next flush starts clean and accumulated bits do not leak forward.
690    /// A no-op returning 0 for non-UInt32 params, whose mask is always 0.
691    pub fn take_uint32_interrupt_mask(&mut self, index: usize, addr: i32) -> AsynResult<u32> {
692        let entry = self.get_entry_mut(index, addr)?;
693        let mask = entry.uint32_interrupt_mask;
694        entry.uint32_interrupt_mask = 0;
695        Ok(mask)
696    }
697
698    /// Configure which bits of a UInt32Digital parameter should fire
699    /// interrupts on transition.
700    ///
701    /// C parity: `paramList::setUInt32Interrupt`
702    /// (`asynPortDriver/asynPortDriver.cpp:480-497`). The reason
703    /// argument decides which mask (rising-only, falling-only, or
704    /// both) is overwritten.
705    pub fn set_uint32_interrupt(
706        &mut self,
707        index: usize,
708        addr: i32,
709        mask: u32,
710        reason: InterruptReason,
711    ) -> AsynResult<()> {
712        let entry = self.get_entry_mut(index, addr)?;
713        if entry.param_type != ParamType::UInt32Digital {
714            return Err(AsynError::TypeMismatch {
715                expected: "UInt32Digital",
716                actual: entry.value.type_name(),
717            });
718        }
719        match reason {
720            InterruptReason::ZeroToOne => entry.uint32_rising_mask = mask,
721            InterruptReason::OneToZero => entry.uint32_falling_mask = mask,
722            InterruptReason::Both => {
723                entry.uint32_rising_mask = mask;
724                entry.uint32_falling_mask = mask;
725            }
726        }
727        Ok(())
728    }
729
730    /// Clear bits from BOTH rising and falling masks for a
731    /// UInt32Digital parameter — C parity:
732    /// `paramList::clearUInt32Interrupt`
733    /// (`asynPortDriver.cpp:504-511`). The C function does not
734    /// accept an `interruptReason`; rising and falling masks are
735    /// always cleared together.
736    pub fn clear_uint32_interrupt(&mut self, index: usize, addr: i32, mask: u32) -> AsynResult<()> {
737        let entry = self.get_entry_mut(index, addr)?;
738        if entry.param_type != ParamType::UInt32Digital {
739            return Err(AsynError::TypeMismatch {
740                expected: "UInt32Digital",
741                actual: entry.value.type_name(),
742            });
743        }
744        entry.uint32_rising_mask &= !mask;
745        entry.uint32_falling_mask &= !mask;
746        Ok(())
747    }
748
749    /// Read the configured rising / falling / combined mask.
750    ///
751    /// C parity: `paramList::getUInt32Interrupt`
752    /// (`asynPortDriver.cpp:519-535`). For `Both`, the combined mask
753    /// is `rising | falling` (matching the C semantics).
754    pub fn get_uint32_interrupt(
755        &self,
756        index: usize,
757        addr: i32,
758        reason: InterruptReason,
759    ) -> AsynResult<u32> {
760        let entry = self.get_entry(index, addr)?;
761        if entry.param_type != ParamType::UInt32Digital {
762            return Err(AsynError::TypeMismatch {
763                expected: "UInt32Digital",
764                actual: entry.value.type_name(),
765            });
766        }
767        Ok(match reason {
768            InterruptReason::ZeroToOne => entry.uint32_rising_mask,
769            InterruptReason::OneToZero => entry.uint32_falling_mask,
770            InterruptReason::Both => entry.uint32_rising_mask | entry.uint32_falling_mask,
771        })
772    }
773
774    // --- Array getters/setters ---
775
776    pub fn get_float64_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[f64]>> {
777        match &self.get_entry(index, addr)?.value {
778            ParamValue::Float64Array(v) => Ok(v.clone()),
779            other => Err(AsynError::TypeMismatch {
780                expected: "Float64Array",
781                actual: other.type_name(),
782            }),
783        }
784    }
785
786    pub fn set_float64_array(&mut self, index: usize, addr: i32, data: Vec<f64>) -> AsynResult<()> {
787        let entry = self.get_entry_mut(index, addr)?;
788        if matches!(entry.value, ParamValue::Float64Array(_)) {
789            entry.value = ParamValue::Float64Array(Arc::from(data));
790            entry.value_changed = true;
791            entry.defined = true;
792            Ok(())
793        } else {
794            Err(AsynError::TypeMismatch {
795                expected: "Float64Array",
796                actual: entry.value.type_name(),
797            })
798        }
799    }
800
801    pub fn get_int32_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i32]>> {
802        match &self.get_entry(index, addr)?.value {
803            ParamValue::Int32Array(v) => Ok(v.clone()),
804            other => Err(AsynError::TypeMismatch {
805                expected: "Int32Array",
806                actual: other.type_name(),
807            }),
808        }
809    }
810
811    pub fn set_int32_array(&mut self, index: usize, addr: i32, data: Vec<i32>) -> AsynResult<()> {
812        let entry = self.get_entry_mut(index, addr)?;
813        if matches!(entry.value, ParamValue::Int32Array(_)) {
814            entry.value = ParamValue::Int32Array(Arc::from(data));
815            entry.value_changed = true;
816            entry.defined = true;
817            Ok(())
818        } else {
819            Err(AsynError::TypeMismatch {
820                expected: "Int32Array",
821                actual: entry.value.type_name(),
822            })
823        }
824    }
825
826    pub fn get_int8_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i8]>> {
827        match &self.get_entry(index, addr)?.value {
828            ParamValue::Int8Array(v) => Ok(v.clone()),
829            other => Err(AsynError::TypeMismatch {
830                expected: "Int8Array",
831                actual: other.type_name(),
832            }),
833        }
834    }
835
836    pub fn set_int8_array(&mut self, index: usize, addr: i32, data: Vec<i8>) -> AsynResult<()> {
837        let entry = self.get_entry_mut(index, addr)?;
838        if matches!(entry.value, ParamValue::Int8Array(_)) {
839            entry.value = ParamValue::Int8Array(Arc::from(data));
840            entry.value_changed = true;
841            entry.defined = true;
842            Ok(())
843        } else {
844            Err(AsynError::TypeMismatch {
845                expected: "Int8Array",
846                actual: entry.value.type_name(),
847            })
848        }
849    }
850
851    pub fn get_int16_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i16]>> {
852        match &self.get_entry(index, addr)?.value {
853            ParamValue::Int16Array(v) => Ok(v.clone()),
854            other => Err(AsynError::TypeMismatch {
855                expected: "Int16Array",
856                actual: other.type_name(),
857            }),
858        }
859    }
860
861    pub fn set_int16_array(&mut self, index: usize, addr: i32, data: Vec<i16>) -> AsynResult<()> {
862        let entry = self.get_entry_mut(index, addr)?;
863        if matches!(entry.value, ParamValue::Int16Array(_)) {
864            entry.value = ParamValue::Int16Array(Arc::from(data));
865            entry.value_changed = true;
866            entry.defined = true;
867            Ok(())
868        } else {
869            Err(AsynError::TypeMismatch {
870                expected: "Int16Array",
871                actual: entry.value.type_name(),
872            })
873        }
874    }
875
876    pub fn get_int64_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[i64]>> {
877        match &self.get_entry(index, addr)?.value {
878            ParamValue::Int64Array(v) => Ok(v.clone()),
879            other => Err(AsynError::TypeMismatch {
880                expected: "Int64Array",
881                actual: other.type_name(),
882            }),
883        }
884    }
885
886    pub fn set_int64_array(&mut self, index: usize, addr: i32, data: Vec<i64>) -> AsynResult<()> {
887        let entry = self.get_entry_mut(index, addr)?;
888        if matches!(entry.value, ParamValue::Int64Array(_)) {
889            entry.value = ParamValue::Int64Array(Arc::from(data));
890            entry.value_changed = true;
891            entry.defined = true;
892            Ok(())
893        } else {
894            Err(AsynError::TypeMismatch {
895                expected: "Int64Array",
896                actual: entry.value.type_name(),
897            })
898        }
899    }
900
901    pub fn get_float32_array(&self, index: usize, addr: i32) -> AsynResult<Arc<[f32]>> {
902        match &self.get_entry(index, addr)?.value {
903            ParamValue::Float32Array(v) => Ok(v.clone()),
904            other => Err(AsynError::TypeMismatch {
905                expected: "Float32Array",
906                actual: other.type_name(),
907            }),
908        }
909    }
910
911    pub fn set_float32_array(&mut self, index: usize, addr: i32, data: Vec<f32>) -> AsynResult<()> {
912        let entry = self.get_entry_mut(index, addr)?;
913        if matches!(entry.value, ParamValue::Float32Array(_)) {
914            entry.value = ParamValue::Float32Array(Arc::from(data));
915            entry.value_changed = true;
916            entry.defined = true;
917            Ok(())
918        } else {
919            Err(AsynError::TypeMismatch {
920                expected: "Float32Array",
921                actual: entry.value.type_name(),
922            })
923        }
924    }
925
926    // --- Enum getters/setters ---
927
928    pub fn get_enum(&self, index: usize, addr: i32) -> AsynResult<(usize, Arc<[EnumEntry]>)> {
929        self.get_entry(index, addr)?.value.as_enum()
930    }
931
932    pub fn set_enum_index(&mut self, index: usize, addr: i32, value: usize) -> AsynResult<()> {
933        let entry = self.get_entry_mut(index, addr)?;
934        if let ParamValue::Enum {
935            ref choices,
936            index: ref mut idx,
937        } = entry.value
938        {
939            if value >= choices.len() {
940                return Err(AsynError::Status {
941                    status: AsynStatus::Error,
942                    message: format!("enum index {value} out of range (0..{})", choices.len()),
943                });
944            }
945            // C parity: enum index is stored through paramVal::setInteger,
946            // which gates on `!isDefined() || data != value` — a first set
947            // to index 0 (the default) must still flip `defined`.
948            if !entry.defined || *idx != value {
949                *idx = value;
950                entry.value_changed = true;
951                entry.defined = true;
952            }
953        } else {
954            return Err(AsynError::TypeMismatch {
955                expected: "Enum",
956                actual: entry.value.type_name(),
957            });
958        }
959        Ok(())
960    }
961
962    pub fn set_enum_choices(
963        &mut self,
964        index: usize,
965        addr: i32,
966        choices: Arc<[EnumEntry]>,
967    ) -> AsynResult<()> {
968        let entry = self.get_entry_mut(index, addr)?;
969        if let ParamValue::Enum {
970            index: ref mut idx,
971            choices: ref mut ch,
972        } = entry.value
973        {
974            *ch = choices;
975            // Reset index if out of range
976            if *idx >= ch.len() {
977                *idx = 0;
978            }
979            entry.value_changed = true;
980            entry.defined = true;
981        } else {
982            return Err(AsynError::TypeMismatch {
983                expected: "Enum",
984                actual: entry.value.type_name(),
985            });
986        }
987        Ok(())
988    }
989
990    // --- GenericPointer getters/setters ---
991
992    pub fn get_generic_pointer(
993        &self,
994        index: usize,
995        addr: i32,
996    ) -> AsynResult<Arc<dyn Any + Send + Sync>> {
997        match &self.get_entry(index, addr)?.value {
998            ParamValue::GenericPointer(v) => Ok(v.clone()),
999            other => Err(AsynError::TypeMismatch {
1000                expected: "GenericPointer",
1001                actual: other.type_name(),
1002            }),
1003        }
1004    }
1005
1006    pub fn set_generic_pointer(
1007        &mut self,
1008        index: usize,
1009        addr: i32,
1010        value: Arc<dyn Any + Send + Sync>,
1011    ) -> AsynResult<()> {
1012        let entry = self.get_entry_mut(index, addr)?;
1013        if matches!(entry.value, ParamValue::GenericPointer(_)) {
1014            entry.value = ParamValue::GenericPointer(value);
1015            entry.value_changed = true;
1016            entry.defined = true; // Any is not comparable
1017            Ok(())
1018        } else {
1019            Err(AsynError::TypeMismatch {
1020                expected: "GenericPointer",
1021                actual: entry.value.type_name(),
1022            })
1023        }
1024    }
1025
1026    /// Store a value of **any** parameter type — the single owner of the
1027    /// "set a parameter from a `ParamValue`" dispatch.
1028    ///
1029    /// Every caller that holds a value whose type is only known at runtime (the
1030    /// port actor applying a [`crate::request::ParamSetValue`], above all) goes
1031    /// through this instead of re-enumerating the type set at the call site. The
1032    /// match is exhaustive, so a new [`ParamValue`] variant is a compile error
1033    /// here rather than a value that silently never reaches the store.
1034    ///
1035    /// `UInt32Digital` is stored with a full write mask and no forced interrupt
1036    /// bits; use [`Self::set_uint32`] directly for C `setUIntDigitalParam`'s
1037    /// masked set.
1038    pub fn set_value(&mut self, index: usize, addr: i32, value: ParamValue) -> AsynResult<()> {
1039        let type_name = value.type_name();
1040        match value {
1041            ParamValue::Int32(v) => self.set_int32(index, addr, v),
1042            ParamValue::Int64(v) => self.set_int64(index, addr, v),
1043            ParamValue::Float64(v) => self.set_float64(index, addr, v),
1044            ParamValue::Octet(s) => self.set_string(index, addr, s),
1045            ParamValue::UInt32Digital(v) => self.set_uint32(index, addr, v, u32::MAX, 0),
1046            ParamValue::Int8Array(a) => self.set_int8_array(index, addr, a.to_vec()),
1047            ParamValue::Int16Array(a) => self.set_int16_array(index, addr, a.to_vec()),
1048            ParamValue::Int32Array(a) => self.set_int32_array(index, addr, a.to_vec()),
1049            ParamValue::Int64Array(a) => self.set_int64_array(index, addr, a.to_vec()),
1050            ParamValue::Float32Array(a) => self.set_float32_array(index, addr, a.to_vec()),
1051            ParamValue::Float64Array(a) => self.set_float64_array(index, addr, a.to_vec()),
1052            ParamValue::Enum {
1053                index: idx,
1054                choices,
1055            } => {
1056                // Choices first — `set_enum_choices` clamps the index to the new
1057                // table, so setting the index afterwards is what sticks. An empty
1058                // table means "index only", leaving the parameter's own choices.
1059                if !choices.is_empty() {
1060                    self.set_enum_choices(index, addr, choices)?;
1061                }
1062                self.set_enum_index(index, addr, idx)
1063            }
1064            ParamValue::GenericPointer(p) => self.set_generic_pointer(index, addr, p),
1065            // `UInt64`/`UInt64Array` are declared in `ParamValue`/`ParamType` but
1066            // the store has no setter *or* getter for them (they are a Rust
1067            // extension with no upstream asyn interface — see
1068            // `interfaces::InterfaceType::UInt64`), so there is no store state a
1069            // caller could reach through them. Refuse loudly rather than pretend
1070            // the value landed.
1071            ParamValue::UInt64(_) | ParamValue::UInt64Array(_) => Err(AsynError::Status {
1072                status: AsynStatus::Error,
1073                message: format!("{type_name} parameters have no store accessor; nothing to set"),
1074            }),
1075            ParamValue::Undefined => Err(AsynError::Status {
1076                status: AsynStatus::Error,
1077                message: "cannot set a parameter to Undefined".into(),
1078            }),
1079        }
1080    }
1081
1082    /// Check if a parameter has been explicitly set (C parity: valueDefined).
1083    pub fn is_param_defined(&self, index: usize, addr: i32) -> AsynResult<bool> {
1084        Ok(self.get_entry(index, addr)?.defined)
1085    }
1086
1087    // --- Status ---
1088
1089    pub fn set_param_status(
1090        &mut self,
1091        index: usize,
1092        addr: i32,
1093        status: AsynStatus,
1094        alarm_status: u16,
1095        alarm_severity: u16,
1096    ) -> AsynResult<()> {
1097        let entry = self.get_entry_mut(index, addr)?;
1098        // C paramVal::setStatus / setAlarmStatus / setAlarmSeverity
1099        // (paramVal.cpp:71-104) each call setValueChanged() when their
1100        // field actually changes; the paramList wrappers
1101        // (asynPortDriver.cpp:420-472) then run registerParameterChange ->
1102        // setFlag, marking the param dirty. So a STATUS- or ALARM-only
1103        // transition (no value change) is still delivered on the next
1104        // callParamCallbacks. Each changed setter also forces the full
1105        // UInt32Digital callback mask (0xFFFFFFFF) so every subscriber bit
1106        // fires. Pre-fix Rust assigned the fields without marking
1107        // value_changed, so a status/alarm-only transition was silently
1108        // dropped by take_changed (the standard setParamStatus() +
1109        // callParamCallbacks() alarm-push pattern never fired).
1110        let changed = entry.status != status
1111            || entry.alarm_status != alarm_status
1112            || entry.alarm_severity != alarm_severity;
1113        entry.status = status;
1114        entry.alarm_status = alarm_status;
1115        entry.alarm_severity = alarm_severity;
1116        if changed {
1117            entry.value_changed = true;
1118            if entry.param_type == ParamType::UInt32Digital {
1119                entry.uint32_interrupt_mask = 0xFFFF_FFFF;
1120            }
1121        }
1122        Ok(())
1123    }
1124
1125    pub fn get_param_status(&self, index: usize, addr: i32) -> AsynResult<(AsynStatus, u16, u16)> {
1126        let entry = self.get_entry(index, addr)?;
1127        Ok((entry.status, entry.alarm_status, entry.alarm_severity))
1128    }
1129
1130    // --- Timestamp ---
1131
1132    pub fn set_timestamp(&mut self, index: usize, addr: i32, ts: SystemTime) -> AsynResult<()> {
1133        self.get_entry_mut(index, addr)?.timestamp = Some(ts);
1134        Ok(())
1135    }
1136
1137    pub fn get_timestamp(&self, index: usize, addr: i32) -> AsynResult<Option<SystemTime>> {
1138        Ok(self.get_entry(index, addr)?.timestamp)
1139    }
1140
1141    // --- Change tracking ---
1142
1143    /// Clear the changed flag for a single parameter. Returns true if it was changed.
1144    pub fn take_changed_single(&mut self, index: usize, addr: i32) -> AsynResult<bool> {
1145        let entry = self.get_entry_mut(index, addr)?;
1146        let was_changed = entry.value_changed;
1147        entry.value_changed = false;
1148        Ok(was_changed)
1149    }
1150
1151    /// Mark a parameter as changed without modifying its value.
1152    /// Useful for triggering I/O Intr on params whose data is served via
1153    /// read_*_array overrides rather than the param cache.
1154    pub fn mark_changed(&mut self, index: usize, addr: i32) -> AsynResult<()> {
1155        self.get_entry_mut(index, addr)?.value_changed = true;
1156        Ok(())
1157    }
1158
1159    /// Returns indices of parameters whose values changed since last call, then clears flags.
1160    pub fn take_changed(&mut self, addr: i32) -> AsynResult<Vec<usize>> {
1161        let a = self.validate_addr(addr)?;
1162        let mut changed = Vec::new();
1163        for (i, entry) in self.params[a].iter_mut().enumerate() {
1164            if entry.value_changed {
1165                entry.value_changed = false;
1166                changed.push(i);
1167            }
1168        }
1169        Ok(changed)
1170    }
1171
1172    /// Number of parameters.
1173    pub fn len(&self) -> usize {
1174        self.params[0].len()
1175    }
1176
1177    pub fn is_empty(&self) -> bool {
1178        self.params[0].is_empty()
1179    }
1180
1181    /// C `paramList::report` (asynPortDriver.cpp:885-892) — the count line, then
1182    /// every parameter through `ParamEntry::report`.
1183    ///
1184    /// C has one `paramList` per address and reports the one it was handed; here
1185    /// one list holds every address, so the address is the argument. It takes no
1186    /// detail level because C's does not use it: `paramVal::report` prints the
1187    /// same line whatever `details` says (paramVal.cpp:296-330), and the only
1188    /// thing the level decides is *how many* lists are printed — which is
1189    /// [`crate::port::PortDriverBase::report_params`]'s decision, and stays there.
1190    pub fn report(&self, out: &mut dyn std::fmt::Write, addr: i32) {
1191        use std::fmt::Write as _;
1192        let _ = writeln!(out, "Number of parameters is: {}", self.len());
1193        // A port whose `max_addr` outruns its list count (single-device, so every
1194        // addr collapses onto list 0) reports list 0 again, exactly as C does with
1195        // its `maxAddr` copies of the same values.
1196        let a = self.validate_addr(addr).unwrap_or(0);
1197        for (i, entry) in self.params[a].iter().enumerate() {
1198            entry.report(out, i);
1199        }
1200    }
1201}
1202
1203impl ParamEntry {
1204    /// C `paramVal::report` (paramVal.cpp:296-372) — one line per parameter,
1205    /// name + type + value + status, *whatever* the detail level. C passes
1206    /// `details` in and never reads it; the only branch is defined vs undefined.
1207    fn report(&self, out: &mut dyn std::fmt::Write, id: usize) {
1208        use std::fmt::Write as _;
1209        let name = &self.name;
1210        // C's `asynStatus` is an unadorned enum and `%d` prints its ordinal;
1211        // [`AsynStatus`] declares the same six in the same order (asynDriver.h:48).
1212        let status = self.status as i32;
1213
1214        // C's `switch(type)` has no case for `asynParamGenericPointer`, so it
1215        // lands in `default:` (paramVal.cpp:368-370) — a pointer parameter has no
1216        // printable value, and C says so rather than inventing one.
1217        if self.param_type == ParamType::GenericPointer {
1218            let _ = writeln!(out, "Parameter {id} is undefined, name={name}");
1219            return;
1220        }
1221        let type_name = c_param_type_name(self.param_type);
1222        if !self.defined {
1223            let _ = writeln!(
1224                out,
1225                "Parameter {id} type={type_name}, name={name}, value is undefined"
1226            );
1227            return;
1228        }
1229        match &self.value {
1230            ParamValue::Int32(v) => {
1231                let _ = writeln!(
1232                    out,
1233                    "Parameter {id} type={type_name}, name={name}, value={v}, status={status}"
1234                );
1235            }
1236            ParamValue::Int64(v) => {
1237                let _ = writeln!(
1238                    out,
1239                    "Parameter {id} type={type_name}, name={name}, value={v}, status={status}"
1240                );
1241            }
1242            ParamValue::UInt64(v) => {
1243                let _ = writeln!(
1244                    out,
1245                    "Parameter {id} type={type_name}, name={name}, value={v}, status={status}"
1246                );
1247            }
1248            // C prints the three masks alongside the value (paramVal.cpp:314-316):
1249            // a UInt32Digital parameter's callback behaviour is half its state.
1250            ParamValue::UInt32Digital(v) => {
1251                let _ = writeln!(
1252                    out,
1253                    "Parameter {id} type={type_name}, name={name}, value=0x{v:x}, \
1254                     status={status}, risingMask=0x{:x}, fallingMask=0x{:x}, callbackMask=0x{:x}",
1255                    self.uint32_rising_mask, self.uint32_falling_mask, self.uint32_interrupt_mask
1256                );
1257            }
1258            ParamValue::Float64(v) => {
1259                let _ = writeln!(
1260                    out,
1261                    "Parameter {id} type={type_name}, name={name}, value={}, status={status}",
1262                    format_g(*v)
1263                );
1264            }
1265            ParamValue::Octet(v) => {
1266                let _ = writeln!(
1267                    out,
1268                    "Parameter {id} type={type_name}, name={name}, value={v}, status={status}"
1269                );
1270            }
1271            // C prints the array's data pointer (`%p` over `data.pi8` &c.), not its
1272            // contents — the report says *that* there is an array and where, and
1273            // leaves the elements to the record that reads them.
1274            ParamValue::Int8Array(v) => report_array(out, id, type_name, name, v.as_ptr(), status),
1275            ParamValue::Int16Array(v) => report_array(out, id, type_name, name, v.as_ptr(), status),
1276            ParamValue::Int32Array(v) => report_array(out, id, type_name, name, v.as_ptr(), status),
1277            ParamValue::Int64Array(v) => report_array(out, id, type_name, name, v.as_ptr(), status),
1278            ParamValue::UInt64Array(v) => {
1279                report_array(out, id, type_name, name, v.as_ptr(), status)
1280            }
1281            ParamValue::Float32Array(v) => {
1282                report_array(out, id, type_name, name, v.as_ptr(), status)
1283            }
1284            ParamValue::Float64Array(v) => {
1285                report_array(out, id, type_name, name, v.as_ptr(), status)
1286            }
1287            ParamValue::Enum { index, .. } => {
1288                let _ = writeln!(
1289                    out,
1290                    "Parameter {id} type={type_name}, name={name}, value={index}, status={status}"
1291                );
1292            }
1293            // C's `asynParamNotDefined` — the type itself is unset, so the
1294            // `switch` falls to the same `default:` a generic pointer does
1295            // (paramVal.cpp:368-370).
1296            ParamValue::Undefined | ParamValue::GenericPointer(_) => {
1297                let _ = writeln!(out, "Parameter {id} is undefined, name={name}");
1298            }
1299        }
1300    }
1301}
1302
1303fn report_array<T>(
1304    out: &mut dyn std::fmt::Write,
1305    id: usize,
1306    type_name: &str,
1307    name: &str,
1308    ptr: *const T,
1309    status: i32,
1310) {
1311    use std::fmt::Write as _;
1312    let _ = writeln!(
1313        out,
1314        "Parameter {id} type={type_name}, name={name}, value={ptr:p}, status={status}"
1315    );
1316}
1317
1318/// The type name C's `paramVal::report` prints — note it is *not*
1319/// `paramVal::typeNames` (`asynParamInt32`, …): the report writes the interface
1320/// name (`asynInt32`), and calls octet `string` (paramVal.cpp:302-330).
1321///
1322/// `UInt64`, `UInt64Array` and `Enum` have no C `paramVal` case — they are this
1323/// port's extensions (upstream asyn issue #231 for the 64-bit unsigned pair) — so
1324/// they take the name C would have given them in the same scheme.
1325fn c_param_type_name(t: ParamType) -> &'static str {
1326    match t {
1327        ParamType::Int32 => "asynInt32",
1328        ParamType::Int64 => "asynInt64",
1329        ParamType::UInt64 => "asynUInt64",
1330        ParamType::Float64 => "asynFloat64",
1331        ParamType::Octet => "string",
1332        ParamType::UInt32Digital => "asynUInt32Digital",
1333        ParamType::Int8Array => "asynInt8Array",
1334        ParamType::Int16Array => "asynInt16Array",
1335        ParamType::Int32Array => "asynInt32Array",
1336        ParamType::Int64Array => "asynInt64Array",
1337        ParamType::UInt64Array => "asynUInt64Array",
1338        ParamType::Float32Array => "asynFloat32Array",
1339        ParamType::Float64Array => "asynFloat64Array",
1340        ParamType::Enum => "asynEnum",
1341        ParamType::GenericPointer => "asynGenericPointer",
1342    }
1343}
1344
1345/// C `printf("%g")` over a double, which is what `paramVal::report` prints a
1346/// `Float64` with (paramVal.cpp:322): six significant digits, `%e` form when the
1347/// exponent is below -4 or at least 6, and trailing zeros stripped either way.
1348/// Rust's `{}` prints the shortest round-trip form instead — `1e-7` as
1349/// `0.0000001`, `0.1+0.2` as `0.30000000000000004` — so the line has to be built.
1350fn format_g(v: f64) -> String {
1351    if v.is_nan() {
1352        return "nan".to_string();
1353    }
1354    if v.is_infinite() {
1355        return if v < 0.0 { "-inf" } else { "inf" }.to_string();
1356    }
1357    const PRECISION: i32 = 6;
1358    // Round to six significant digits first, and read the decimal exponent back
1359    // off the result: it is the rounded exponent that picks the form (9.999995
1360    // rounds to 1e+01, and C prints `10`, not `9.99999`).
1361    let sci = format!("{:.*e}", (PRECISION - 1) as usize, v);
1362    let (mantissa, exp) = sci.split_once('e').expect("Rust {:e} always emits one");
1363    let exp: i32 = exp.parse().expect("…followed by a decimal exponent");
1364    let strip = |s: &str| -> String {
1365        if s.contains('.') {
1366            s.trim_end_matches('0').trim_end_matches('.').to_string()
1367        } else {
1368            s.to_string()
1369        }
1370    };
1371    if exp < -4 || exp >= PRECISION {
1372        // C's `%e` exponent is signed and at least two digits: `1.5e+10`, `1e-05`.
1373        format!(
1374            "{}e{}{:02}",
1375            strip(mantissa),
1376            if exp < 0 { '-' } else { '+' },
1377            exp.abs()
1378        )
1379    } else {
1380        strip(&format!("{:.*}", (PRECISION - 1 - exp).max(0) as usize, v))
1381    }
1382}
1383
1384/// Flat parameter-group helper — C++ `asynParamSet` equivalent.
1385///
1386/// C asyn (`asynPortDriver/asynParamSet.h`) defines:
1387///
1388/// ```cpp
1389/// struct asynParam {
1390///     const char* name;
1391///     asynParamType type;
1392///     int* index;
1393/// };
1394/// class asynParamSet {
1395/// protected:
1396///     void add(const char* name, asynParamType type, int* index);
1397/// public:
1398///     std::vector<asynParam> getParamDefinitions();
1399/// };
1400/// ```
1401///
1402/// and `asynPortDriver::createParams` (asynPortDriver.cpp:4115-4126)
1403/// iterates the vector calling `createParam(name, type, index)` so the
1404/// driver subclass gets its int member back-filled.
1405///
1406/// In Rust we cannot stash a `&mut i32` for the duration of the build,
1407/// so the idiomatic equivalent is "register names + types, then run
1408/// `create_all` and read back the assigned indices as a Vec keyed by
1409/// add-order." Callers persist the returned `Vec<usize>` (or wrap it
1410/// in a domain-specific struct) the same way C++ drivers persist
1411/// their `int paramIdx` members.
1412///
1413/// The structure is intentionally a **flat list** — C asyn has no
1414/// tree / topology layer for parameter groups; that idea is invented
1415/// (see `docs/asyn-missing.md` notes on PVI).
1416#[derive(Default, Debug, Clone)]
1417pub struct AsynParamSet {
1418    defs: Vec<(String, ParamType)>,
1419}
1420
1421impl AsynParamSet {
1422    pub fn new() -> Self {
1423        Self { defs: Vec::new() }
1424    }
1425
1426    /// Append a parameter definition. Mirrors C++ `asynParamSet::add`.
1427    /// Returns the slot index — the position the param will occupy in
1428    /// the `Vec<usize>` returned by [`Self::create_all`].
1429    pub fn add(&mut self, name: &str, ty: ParamType) -> usize {
1430        let slot = self.defs.len();
1431        self.defs.push((name.to_string(), ty));
1432        slot
1433    }
1434
1435    /// C++ `asynPortDriver::createParams` (asynPortDriver.cpp:4119-4125):
1436    /// iterate the definitions in registration order, call
1437    /// `createParam` for each, abort with `asynError` on the first
1438    /// failure. Returns the assigned indices in the same order as
1439    /// [`Self::add`] calls so the caller can recover slot→index
1440    /// without name lookup.
1441    pub fn create_all(&self, params: &mut ParamList) -> AsynResult<Vec<usize>> {
1442        let mut indices = Vec::with_capacity(self.defs.len());
1443        for (name, ty) in &self.defs {
1444            indices.push(params.create_param(name, *ty)?);
1445        }
1446        Ok(indices)
1447    }
1448
1449    /// Number of definitions registered. Cheap inspector for tests.
1450    pub fn len(&self) -> usize {
1451        self.defs.len()
1452    }
1453
1454    pub fn is_empty(&self) -> bool {
1455        self.defs.is_empty()
1456    }
1457
1458    /// Iterate definitions in registration order — read-only view.
1459    pub fn iter(&self) -> impl Iterator<Item = (&str, ParamType)> {
1460        self.defs.iter().map(|(n, t)| (n.as_str(), *t))
1461    }
1462}
1463
1464#[cfg(test)]
1465mod tests {
1466    use super::*;
1467
1468    /// C `%g` (paramVal.cpp:322 prints a Float64 with it): six significant
1469    /// digits, `%e` form when the decimal exponent is < -4 or >= 6, trailing
1470    /// zeros stripped, two-digit signed exponent. One case per boundary of that
1471    /// rule — Rust's own `{}` matches none of them.
1472    #[test]
1473    fn format_g_matches_c_printf_g() {
1474        for (v, expect) in [
1475            (0.0f64, "0"),
1476            (7.0, "7"),
1477            (0.1 + 0.2, "0.3"),
1478            (1.5, "1.5"),
1479            // Six significant digits, then the trailing zeros go.
1480            (1.0 / 3.0, "0.333333"),
1481            (123456.0, "123456"),
1482            // exp == 6 → the %e form starts here.
1483            (1234567.0, "1.23457e+06"),
1484            // exp == -4 is still the %f form; -5 is not.
1485            (0.000123456789, "0.000123457"),
1486            (0.0000123456789, "1.23457e-05"),
1487            (-2.5e-9, "-2.5e-09"),
1488            (f64::INFINITY, "inf"),
1489            (f64::NAN, "nan"),
1490        ] {
1491            assert_eq!(format_g(v), expect, "%g of {v}");
1492        }
1493    }
1494
1495    #[test]
1496    fn test_create_and_find() {
1497        let mut pl = ParamList::new(1, false);
1498        let i0 = pl.create_param("TEMP", ParamType::Float64).unwrap();
1499        let i1 = pl.create_param("COUNT", ParamType::Int32).unwrap();
1500        assert_eq!(i0, 0);
1501        assert_eq!(i1, 1);
1502        assert_eq!(pl.find_param("TEMP"), Some(0));
1503        assert_eq!(pl.find_param("COUNT"), Some(1));
1504        assert_eq!(pl.find_param("NOPE"), None);
1505        // Duplicate create returns same index
1506        assert_eq!(pl.create_param("TEMP", ParamType::Float64).unwrap(), 0);
1507    }
1508
1509    #[test]
1510    fn test_create_param_strict_duplicate_returns_already_exists() {
1511        // C parity: paramList::createParam (asynPortDriver.cpp:130) —
1512        // a second createParam with an already-known name returns
1513        // `asynParamAlreadyExists`; the asynPortDriver wrapper
1514        // (asynPortDriver.cpp:991-1011) translates that to
1515        // `asynError` with a TRACE_ERROR log. The lax `create_param`
1516        // intentionally absorbs the duplicate for `ad-core-rs` /
1517        // `ad-plugins-rs` idempotent build chains; the strict
1518        // variant is what C-parity-sensitive callers reach for.
1519        let mut pl = ParamList::new(1, false);
1520        let idx = pl.create_param_strict("VAL", ParamType::Int32).unwrap();
1521        assert_eq!(idx, 0);
1522        match pl.create_param_strict("VAL", ParamType::Int32) {
1523            Err(AsynError::ParamAlreadyExists(name)) => assert_eq!(name, "VAL"),
1524            other => panic!("expected ParamAlreadyExists, got {other:?}"),
1525        }
1526        // Strict and lax variants share the registry: a name created
1527        // via the lax path must still be visible to strict and
1528        // produce ParamAlreadyExists.
1529        match pl.create_param_strict("VAL", ParamType::Int32) {
1530            Err(AsynError::ParamAlreadyExists(_)) => {}
1531            other => panic!("strict must observe lax-created names, got {other:?}"),
1532        }
1533        // ...and vice versa.
1534        assert_eq!(pl.create_param("VAL", ParamType::Int32).unwrap(), 0);
1535    }
1536
1537    #[test]
1538    fn test_create_param_strict_distinct_names_succeed() {
1539        let mut pl = ParamList::new(1, false);
1540        let a = pl.create_param_strict("A", ParamType::Int32).unwrap();
1541        let b = pl.create_param_strict("B", ParamType::Float64).unwrap();
1542        assert_eq!(a, 0);
1543        assert_eq!(b, 1);
1544        assert_eq!(pl.find_param("A"), Some(0));
1545        assert_eq!(pl.find_param("B"), Some(1));
1546    }
1547
1548    #[test]
1549    fn test_uint32_set_get_clear_interrupt_masks() {
1550        // C parity: paramList::setUInt32Interrupt /
1551        // paramList::clearUInt32Interrupt / paramList::getUInt32Interrupt
1552        // at asynPortDriver.cpp:480-535. ZeroToOne writes the rising
1553        // mask only; OneToZero writes the falling mask only; Both
1554        // overwrites them together. clear strips bits from BOTH
1555        // masks. get(Both) returns rising | falling.
1556        let mut pl = ParamList::new(1, false);
1557        let idx = pl
1558            .create_param_strict("BITS", ParamType::UInt32Digital)
1559            .unwrap();
1560
1561        pl.set_uint32_interrupt(idx, 0, 0xF0, InterruptReason::ZeroToOne)
1562            .unwrap();
1563        assert_eq!(
1564            pl.get_uint32_interrupt(idx, 0, InterruptReason::ZeroToOne)
1565                .unwrap(),
1566            0xF0
1567        );
1568        assert_eq!(
1569            pl.get_uint32_interrupt(idx, 0, InterruptReason::OneToZero)
1570                .unwrap(),
1571            0x00
1572        );
1573
1574        pl.set_uint32_interrupt(idx, 0, 0x0F, InterruptReason::OneToZero)
1575            .unwrap();
1576        assert_eq!(
1577            pl.get_uint32_interrupt(idx, 0, InterruptReason::Both)
1578                .unwrap(),
1579            0xFF
1580        );
1581
1582        // clear strips from both masks.
1583        pl.clear_uint32_interrupt(idx, 0, 0x10).unwrap();
1584        assert_eq!(
1585            pl.get_uint32_interrupt(idx, 0, InterruptReason::ZeroToOne)
1586                .unwrap(),
1587            0xE0
1588        );
1589        // No falling bit at 0x10 to begin with; clear is a no-op there.
1590        assert_eq!(
1591            pl.get_uint32_interrupt(idx, 0, InterruptReason::OneToZero)
1592                .unwrap(),
1593            0x0F
1594        );
1595
1596        // Both overwrites symmetric.
1597        pl.set_uint32_interrupt(idx, 0, 0xAA, InterruptReason::Both)
1598            .unwrap();
1599        assert_eq!(
1600            pl.get_uint32_interrupt(idx, 0, InterruptReason::ZeroToOne)
1601                .unwrap(),
1602            0xAA
1603        );
1604        assert_eq!(
1605            pl.get_uint32_interrupt(idx, 0, InterruptReason::OneToZero)
1606                .unwrap(),
1607            0xAA
1608        );
1609        assert_eq!(
1610            pl.get_uint32_interrupt(idx, 0, InterruptReason::Both)
1611                .unwrap(),
1612            0xAA
1613        );
1614    }
1615
1616    #[test]
1617    fn test_uint32_interrupt_type_mismatch_rejects_non_uint32() {
1618        // C parity: setUInt32Interrupt/getUInt32Interrupt/clearUInt32Interrupt
1619        // return `asynParamWrongType` when called on a non-UInt32Digital
1620        // param (asynPortDriver.cpp:483/507/522).
1621        let mut pl = ParamList::new(1, false);
1622        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
1623        match pl.set_uint32_interrupt(idx, 0, 0xFF, InterruptReason::Both) {
1624            Err(AsynError::TypeMismatch { expected, .. }) => {
1625                assert_eq!(expected, "UInt32Digital")
1626            }
1627            other => panic!("expected TypeMismatch, got {other:?}"),
1628        }
1629        match pl.clear_uint32_interrupt(idx, 0, 0xFF) {
1630            Err(AsynError::TypeMismatch { .. }) => {}
1631            other => panic!("expected TypeMismatch, got {other:?}"),
1632        }
1633        match pl.get_uint32_interrupt(idx, 0, InterruptReason::Both) {
1634            Err(AsynError::TypeMismatch { .. }) => {}
1635            other => panic!("expected TypeMismatch, got {other:?}"),
1636        }
1637    }
1638
1639    #[test]
1640    fn test_get_set_int32() {
1641        let mut pl = ParamList::new(1, false);
1642        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
1643        assert_eq!(pl.get_int32(idx, 0).unwrap(), 0);
1644        pl.set_int32(idx, 0, 42).unwrap();
1645        assert_eq!(pl.get_int32(idx, 0).unwrap(), 42);
1646    }
1647
1648    #[test]
1649    fn test_get_set_float64() {
1650        let mut pl = ParamList::new(1, false);
1651        let idx = pl.create_param("TEMP", ParamType::Float64).unwrap();
1652        pl.set_float64(idx, 0, 3.14).unwrap();
1653        assert!((pl.get_float64(idx, 0).unwrap() - 3.14).abs() < 1e-10);
1654    }
1655
1656    #[test]
1657    fn test_get_set_string() {
1658        let mut pl = ParamList::new(1, false);
1659        let idx = pl.create_param("MSG", ParamType::Octet).unwrap();
1660        pl.set_string(idx, 0, "hello".into()).unwrap();
1661        assert_eq!(pl.get_string(idx, 0).unwrap(), "hello");
1662    }
1663
1664    #[test]
1665    fn test_get_set_uint32_mask() {
1666        let mut pl = ParamList::new(1, false);
1667        let idx = pl.create_param("BITS", ParamType::UInt32Digital).unwrap();
1668        pl.set_uint32(idx, 0, 0xFF, 0x0F, 0).unwrap();
1669        assert_eq!(pl.get_uint32(idx, 0).unwrap(), 0x0F);
1670        pl.set_uint32(idx, 0, 0xFF, 0xF0, 0).unwrap();
1671        assert_eq!(pl.get_uint32(idx, 0).unwrap(), 0xFF);
1672    }
1673
1674    #[test]
1675    fn test_multi_addr_isolation() {
1676        let mut pl = ParamList::new(3, true);
1677        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
1678        pl.set_int32(idx, 0, 10).unwrap();
1679        pl.set_int32(idx, 1, 20).unwrap();
1680        pl.set_int32(idx, 2, 30).unwrap();
1681        assert_eq!(pl.get_int32(idx, 0).unwrap(), 10);
1682        assert_eq!(pl.get_int32(idx, 1).unwrap(), 20);
1683        assert_eq!(pl.get_int32(idx, 2).unwrap(), 30);
1684    }
1685
1686    #[test]
1687    fn test_addr_out_of_range() {
1688        let pl = ParamList::new(2, true);
1689        assert!(pl.validate_addr(-1).is_err());
1690        assert!(pl.validate_addr(2).is_err());
1691        assert!(pl.validate_addr(0).is_ok());
1692        assert!(pl.validate_addr(1).is_ok());
1693    }
1694
1695    #[test]
1696    fn test_addr_normalize_single_device() {
1697        let mut pl = ParamList::new(1, false);
1698        let idx = pl.create_param("V", ParamType::Int32).unwrap();
1699        pl.set_int32(idx, 0, 99).unwrap();
1700        // Any addr normalizes to 0 for single-device
1701        assert_eq!(pl.get_int32(idx, 5).unwrap(), 99);
1702        assert_eq!(pl.get_int32(idx, -1).unwrap(), 99);
1703    }
1704
1705    #[test]
1706    fn test_index_out_of_range() {
1707        let pl = ParamList::new(1, false);
1708        assert!(pl.get_int32(999, 0).is_err());
1709    }
1710
1711    #[test]
1712    fn test_type_mismatch() {
1713        let mut pl = ParamList::new(1, false);
1714        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
1715        assert!(pl.get_float64(idx, 0).is_err());
1716        assert!(pl.set_float64(idx, 0, 1.0).is_err());
1717    }
1718
1719    #[test]
1720    fn test_change_tracking() {
1721        let mut pl = ParamList::new(1, false);
1722        let i0 = pl.create_param("A", ParamType::Int32).unwrap();
1723        let i1 = pl.create_param("B", ParamType::Float64).unwrap();
1724
1725        pl.set_int32(i0, 0, 1).unwrap();
1726        pl.set_float64(i1, 0, 2.0).unwrap();
1727
1728        let changed = pl.take_changed(0).unwrap();
1729        assert_eq!(changed.len(), 2);
1730        assert_eq!(changed[0], 0);
1731        assert_eq!(changed[1], 1);
1732
1733        // Second call returns empty
1734        let changed2 = pl.take_changed(0).unwrap();
1735        assert!(changed2.is_empty());
1736    }
1737
1738    #[test]
1739    fn test_same_value_no_change() {
1740        let mut pl = ParamList::new(1, false);
1741        let idx = pl.create_param("V", ParamType::Int32).unwrap();
1742        pl.set_int32(idx, 0, 42).unwrap();
1743        let _ = pl.take_changed(0).unwrap(); // clear
1744
1745        // Set same value
1746        pl.set_int32(idx, 0, 42).unwrap();
1747        let changed = pl.take_changed(0).unwrap();
1748        assert!(changed.is_empty());
1749    }
1750
1751    #[test]
1752    fn test_array_params() {
1753        let mut pl = ParamList::new(1, false);
1754        let idx = pl.create_param("WF", ParamType::Float64Array).unwrap();
1755        pl.set_float64_array(idx, 0, vec![1.0, 2.0, 3.0]).unwrap();
1756        let arr = pl.get_float64_array(idx, 0).unwrap();
1757        assert_eq!(&*arr, &[1.0, 2.0, 3.0]);
1758    }
1759
1760    #[test]
1761    fn test_param_status() {
1762        let mut pl = ParamList::new(1, false);
1763        let idx = pl.create_param("V", ParamType::Int32).unwrap();
1764        pl.set_param_status(idx, 0, AsynStatus::Timeout, 1, 2)
1765            .unwrap();
1766        let (st, as_, sev) = pl.get_param_status(idx, 0).unwrap();
1767        assert_eq!(st, AsynStatus::Timeout);
1768        assert_eq!(as_, 1);
1769        assert_eq!(sev, 2);
1770    }
1771
1772    #[test]
1773    fn test_param_name_and_type() {
1774        let mut pl = ParamList::new(1, false);
1775        pl.create_param("TEMP", ParamType::Float64).unwrap();
1776        assert_eq!(pl.param_name(0), Some("TEMP"));
1777        assert_eq!(pl.param_type(0), Some(ParamType::Float64));
1778        assert_eq!(pl.param_name(99), None);
1779    }
1780
1781    #[test]
1782    fn test_timestamp_none_by_default() {
1783        let mut pl = ParamList::new(1, false);
1784        pl.create_param("V", ParamType::Int32).unwrap();
1785        assert_eq!(pl.get_timestamp(0, 0).unwrap(), None);
1786    }
1787
1788    #[test]
1789    fn test_timestamp_set_get() {
1790        let mut pl = ParamList::new(1, false);
1791        pl.create_param("V", ParamType::Int32).unwrap();
1792        let ts = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(12345);
1793        pl.set_timestamp(0, 0, ts).unwrap();
1794        assert_eq!(pl.get_timestamp(0, 0).unwrap(), Some(ts));
1795    }
1796
1797    #[test]
1798    fn test_take_changed_returns_indices() {
1799        let mut pl = ParamList::new(1, false);
1800        pl.create_param("A", ParamType::Int32).unwrap();
1801        pl.create_param("B", ParamType::Float64).unwrap();
1802        pl.create_param("C", ParamType::Octet).unwrap();
1803
1804        pl.set_int32(0, 0, 1).unwrap();
1805        pl.set_string(2, 0, "x".into()).unwrap();
1806
1807        let changed = pl.take_changed(0).unwrap();
1808        assert_eq!(changed, vec![0, 2]);
1809    }
1810
1811    // --- Enum tests ---
1812
1813    #[test]
1814    fn test_enum_default_sentinel() {
1815        let mut pl = ParamList::new(1, false);
1816        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
1817        let (index, choices) = pl.get_enum(idx, 0).unwrap();
1818        assert_eq!(index, 0);
1819        assert_eq!(choices.len(), 1);
1820        assert_eq!(choices[0].string, "");
1821        assert_eq!(choices[0].value, 0);
1822        assert_eq!(choices[0].severity, 0);
1823    }
1824
1825    #[test]
1826    fn test_enum_set_get_index() {
1827        let mut pl = ParamList::new(1, false);
1828        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
1829        let choices: Arc<[EnumEntry]> = Arc::from(vec![
1830            EnumEntry {
1831                string: "Off".into(),
1832                value: 0,
1833                severity: 0,
1834            },
1835            EnumEntry {
1836                string: "On".into(),
1837                value: 1,
1838                severity: 0,
1839            },
1840        ]);
1841        pl.set_enum_choices(idx, 0, choices).unwrap();
1842        pl.set_enum_index(idx, 0, 1).unwrap();
1843        let (index, _) = pl.get_enum(idx, 0).unwrap();
1844        assert_eq!(index, 1);
1845    }
1846
1847    #[test]
1848    fn test_enum_index_out_of_range() {
1849        let mut pl = ParamList::new(1, false);
1850        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
1851        // Default has 1 choice (sentinel), so index=1 is out of range
1852        assert!(pl.set_enum_index(idx, 0, 1).is_err());
1853    }
1854
1855    #[test]
1856    fn test_enum_type_mismatch() {
1857        let mut pl = ParamList::new(1, false);
1858        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
1859        assert!(pl.get_enum(idx, 0).is_err());
1860    }
1861
1862    #[test]
1863    fn test_enum_choices_update_resets_index() {
1864        let mut pl = ParamList::new(1, false);
1865        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
1866        let choices: Arc<[EnumEntry]> = Arc::from(vec![
1867            EnumEntry {
1868                string: "A".into(),
1869                value: 0,
1870                severity: 0,
1871            },
1872            EnumEntry {
1873                string: "B".into(),
1874                value: 1,
1875                severity: 0,
1876            },
1877            EnumEntry {
1878                string: "C".into(),
1879                value: 2,
1880                severity: 0,
1881            },
1882        ]);
1883        pl.set_enum_choices(idx, 0, choices).unwrap();
1884        pl.set_enum_index(idx, 0, 2).unwrap();
1885        // Now shrink choices — index 2 is out of range, should reset to 0
1886        let new_choices: Arc<[EnumEntry]> = Arc::from(vec![EnumEntry {
1887            string: "X".into(),
1888            value: 0,
1889            severity: 0,
1890        }]);
1891        pl.set_enum_choices(idx, 0, new_choices).unwrap();
1892        let (index, choices) = pl.get_enum(idx, 0).unwrap();
1893        assert_eq!(index, 0);
1894        assert_eq!(choices.len(), 1);
1895    }
1896
1897    #[test]
1898    fn test_enum_set_choices_marks_changed() {
1899        let mut pl = ParamList::new(1, false);
1900        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
1901        let _ = pl.take_changed(0).unwrap(); // clear initial
1902        let choices: Arc<[EnumEntry]> = Arc::from(vec![EnumEntry {
1903            string: "A".into(),
1904            value: 0,
1905            severity: 0,
1906        }]);
1907        pl.set_enum_choices(idx, 0, choices).unwrap();
1908        let changed = pl.take_changed(0).unwrap();
1909        assert!(changed.contains(&idx));
1910    }
1911
1912    // --- GenericPointer tests ---
1913
1914    #[test]
1915    fn test_generic_pointer_default() {
1916        let mut pl = ParamList::new(1, false);
1917        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
1918        let val = pl.get_generic_pointer(idx, 0).unwrap();
1919        assert!(val.downcast_ref::<()>().is_some());
1920    }
1921
1922    #[test]
1923    fn test_generic_pointer_set_get_downcast() {
1924        let mut pl = ParamList::new(1, false);
1925        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
1926        let data: Arc<dyn Any + Send + Sync> = Arc::new(42i32);
1927        pl.set_generic_pointer(idx, 0, data).unwrap();
1928        let val = pl.get_generic_pointer(idx, 0).unwrap();
1929        assert_eq!(*val.downcast_ref::<i32>().unwrap(), 42);
1930    }
1931
1932    #[test]
1933    fn test_generic_pointer_downcast_wrong_type() {
1934        let mut pl = ParamList::new(1, false);
1935        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
1936        let data: Arc<dyn Any + Send + Sync> = Arc::new(42i32);
1937        pl.set_generic_pointer(idx, 0, data).unwrap();
1938        let val = pl.get_generic_pointer(idx, 0).unwrap();
1939        assert!(val.downcast_ref::<String>().is_none());
1940    }
1941
1942    #[test]
1943    fn test_generic_pointer_type_mismatch() {
1944        let mut pl = ParamList::new(1, false);
1945        let idx = pl.create_param("VAL", ParamType::Int32).unwrap();
1946        assert!(pl.get_generic_pointer(idx, 0).is_err());
1947    }
1948
1949    #[test]
1950    fn test_generic_pointer_debug_shows_type_id() {
1951        let mut pl = ParamList::new(1, false);
1952        let idx = pl.create_param("PTR", ParamType::GenericPointer).unwrap();
1953        let data: Arc<dyn Any + Send + Sync> = Arc::new(vec![1, 2, 3]);
1954        pl.set_generic_pointer(idx, 0, data).unwrap();
1955        let val = pl.get_value(idx, 0).unwrap();
1956        let s = format!("{val:?}");
1957        assert!(s.contains("GenericPointer"));
1958        assert!(s.contains("TypeId"));
1959    }
1960
1961    // --- Int64 tests ---
1962
1963    #[test]
1964    fn test_get_set_int64() {
1965        let mut pl = ParamList::new(1, false);
1966        let idx = pl.create_param("BIG", ParamType::Int64).unwrap();
1967        assert_eq!(pl.get_int64(idx, 0).unwrap(), 0);
1968        pl.set_int64(idx, 0, i64::MAX).unwrap();
1969        assert_eq!(pl.get_int64(idx, 0).unwrap(), i64::MAX);
1970    }
1971
1972    #[test]
1973    fn test_int64_type_mismatch() {
1974        let mut pl = ParamList::new(1, false);
1975        let idx = pl.create_param("V", ParamType::Int32).unwrap();
1976        assert!(pl.get_int64(idx, 0).is_err());
1977        assert!(pl.set_int64(idx, 0, 1).is_err());
1978    }
1979
1980    #[test]
1981    fn test_int64_same_value_no_change() {
1982        let mut pl = ParamList::new(1, false);
1983        let idx = pl.create_param("V", ParamType::Int64).unwrap();
1984        pl.set_int64(idx, 0, 42).unwrap();
1985        let _ = pl.take_changed(0).unwrap();
1986        pl.set_int64(idx, 0, 42).unwrap();
1987        let changed = pl.take_changed(0).unwrap();
1988        assert!(changed.is_empty());
1989    }
1990
1991    #[test]
1992    fn test_int64_change_tracking() {
1993        let mut pl = ParamList::new(1, false);
1994        let idx = pl.create_param("V", ParamType::Int64).unwrap();
1995        pl.set_int64(idx, 0, 100).unwrap();
1996        let changed = pl.take_changed(0).unwrap();
1997        assert_eq!(changed, vec![idx]);
1998    }
1999
2000    // --- AsynParamSet (C++ asynParamSet equivalent) ---
2001
2002    #[test]
2003    fn asyn_param_set_add_returns_slot_index_in_order() {
2004        let mut set = AsynParamSet::new();
2005        // C++ `add()` returns void; we expose the slot index so a
2006        // Rust caller can recover indices via the Vec returned by
2007        // create_all(). Order matters: registration order == slot
2008        // order == result-Vec order.
2009        assert_eq!(set.add("Temperature", ParamType::Float64), 0);
2010        assert_eq!(set.add("Status", ParamType::Int32), 1);
2011        assert_eq!(set.add("Tag", ParamType::Octet), 2);
2012        assert_eq!(set.len(), 3);
2013    }
2014
2015    #[test]
2016    fn asyn_param_set_create_all_assigns_indices_in_order() {
2017        let mut set = AsynParamSet::new();
2018        let temp_slot = set.add("Temperature", ParamType::Float64);
2019        let status_slot = set.add("Status", ParamType::Int32);
2020        let tag_slot = set.add("Tag", ParamType::Octet);
2021
2022        let mut pl = ParamList::new(1, false);
2023        let indices = set.create_all(&mut pl).unwrap();
2024        assert_eq!(indices.len(), 3);
2025        assert_eq!(indices[temp_slot], 0);
2026        assert_eq!(indices[status_slot], 1);
2027        assert_eq!(indices[tag_slot], 2);
2028        // ParamList agrees on the names.
2029        assert_eq!(pl.find_param("Temperature"), Some(0));
2030        assert_eq!(pl.find_param("Status"), Some(1));
2031        assert_eq!(pl.find_param("Tag"), Some(2));
2032    }
2033
2034    #[test]
2035    fn asyn_param_set_iter_preserves_registration_order() {
2036        let mut set = AsynParamSet::new();
2037        set.add("A", ParamType::Int32);
2038        set.add("B", ParamType::Float64);
2039        set.add("C", ParamType::Octet);
2040        let names: Vec<&str> = set.iter().map(|(n, _)| n).collect();
2041        assert_eq!(names, vec!["A", "B", "C"]);
2042    }
2043
2044    #[test]
2045    fn asyn_param_set_empty_create_all_is_noop() {
2046        let set = AsynParamSet::new();
2047        let mut pl = ParamList::new(1, false);
2048        let idx = set.create_all(&mut pl).unwrap();
2049        assert!(idx.is_empty());
2050        assert!(pl.is_empty());
2051    }
2052
2053    #[test]
2054    fn asyn_param_set_duplicate_name_returns_existing_index() {
2055        // ParamList::create_param returns the existing index on dup
2056        // (silent dedup); AsynParamSet inherits that — two add()
2057        // calls with the same name resolve to the same final index.
2058        // Matches C asyn where createParam dedupes via name_to_index.
2059        let mut set = AsynParamSet::new();
2060        set.add("X", ParamType::Int32);
2061        set.add("X", ParamType::Int32);
2062        let mut pl = ParamList::new(1, false);
2063        let idx = set.create_all(&mut pl).unwrap();
2064        assert_eq!(idx, vec![0, 0]);
2065    }
2066
2067    // ------------------------------------------------------------------
2068    // Strict-getter / C-parity tests for asynParamUndefined.
2069    // The lax `get_*` variants stay at the type default; the `_strict`
2070    // variants surface the C status. The setters now flip `defined` on a
2071    // first set even when the value equals the type default
2072    // (C `paramVal::setInteger/setDouble/setString/setUInt32`).
2073    // ------------------------------------------------------------------
2074
2075    #[test]
2076    fn get_int32_strict_undefined_returns_param_undefined() {
2077        let mut pl = ParamList::new(1, false);
2078        let idx = pl.create_param("U", ParamType::Int32).unwrap();
2079        // Lax getter still works (returns the type default).
2080        assert_eq!(pl.get_int32(idx, 0).unwrap(), 0);
2081        // Strict getter surfaces asynParamUndefined.
2082        let err = pl.get_int32_strict(idx, 0).unwrap_err();
2083        assert!(matches!(err, AsynError::ParamUndefined(i) if i == idx));
2084        pl.set_int32(idx, 0, 7).unwrap();
2085        assert_eq!(pl.get_int32_strict(idx, 0).unwrap(), 7);
2086    }
2087
2088    #[test]
2089    fn get_float64_strict_undefined_returns_param_undefined() {
2090        let mut pl = ParamList::new(1, false);
2091        let idx = pl.create_param("U", ParamType::Float64).unwrap();
2092        assert_eq!(pl.get_float64(idx, 0).unwrap(), 0.0);
2093        assert!(matches!(
2094            pl.get_float64_strict(idx, 0).unwrap_err(),
2095            AsynError::ParamUndefined(i) if i == idx
2096        ));
2097    }
2098
2099    #[test]
2100    fn get_int64_strict_undefined_returns_param_undefined() {
2101        let mut pl = ParamList::new(1, false);
2102        let idx = pl.create_param("U", ParamType::Int64).unwrap();
2103        assert!(matches!(
2104            pl.get_int64_strict(idx, 0).unwrap_err(),
2105            AsynError::ParamUndefined(i) if i == idx
2106        ));
2107    }
2108
2109    #[test]
2110    fn get_uint32_strict_undefined_returns_param_undefined() {
2111        let mut pl = ParamList::new(1, false);
2112        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
2113        assert!(matches!(
2114            pl.get_uint32_strict(idx, 0).unwrap_err(),
2115            AsynError::ParamUndefined(i) if i == idx
2116        ));
2117    }
2118
2119    #[test]
2120    fn get_string_strict_undefined_returns_param_undefined() {
2121        let mut pl = ParamList::new(1, false);
2122        let idx = pl.create_param("U", ParamType::Octet).unwrap();
2123        assert!(matches!(
2124            pl.get_string_strict(idx, 0).unwrap_err(),
2125            AsynError::ParamUndefined(i) if i == idx
2126        ));
2127    }
2128
2129    #[test]
2130    fn get_strict_checks_type_before_undefined_c_parity() {
2131        // C parity: paramVal::getInteger throws ParamValWrongType before
2132        // ParamValNotDefined (paramVal.cpp:147-155). Reading a Float64
2133        // param via get_int32_strict on a never-set entry must surface
2134        // TypeMismatch, not ParamUndefined.
2135        let mut pl = ParamList::new(1, false);
2136        let idx = pl.create_param("F", ParamType::Float64).unwrap();
2137        assert!(matches!(
2138            pl.get_int32_strict(idx, 0).unwrap_err(),
2139            AsynError::TypeMismatch {
2140                expected: "Int32",
2141                ..
2142            }
2143        ));
2144    }
2145
2146    #[test]
2147    fn set_int32_first_write_with_default_value_flips_defined() {
2148        // C parity: paramVal::setInteger checks `!isDefined() || value != old`
2149        // so writing `0` to a fresh Int32 still flips defined+value_changed.
2150        let mut pl = ParamList::new(1, false);
2151        let idx = pl.create_param("V", ParamType::Int32).unwrap();
2152        assert!(!pl.is_param_defined(idx, 0).unwrap());
2153        pl.set_int32(idx, 0, 0).unwrap();
2154        assert!(pl.is_param_defined(idx, 0).unwrap());
2155        assert!(pl.take_changed_single(idx, 0).unwrap());
2156        assert_eq!(pl.get_int32_strict(idx, 0).unwrap(), 0);
2157    }
2158
2159    #[test]
2160    fn set_float64_first_write_with_default_value_flips_defined() {
2161        let mut pl = ParamList::new(1, false);
2162        let idx = pl.create_param("V", ParamType::Float64).unwrap();
2163        pl.set_float64(idx, 0, 0.0).unwrap();
2164        assert!(pl.is_param_defined(idx, 0).unwrap());
2165        assert_eq!(pl.get_float64_strict(idx, 0).unwrap(), 0.0);
2166    }
2167
2168    #[test]
2169    fn set_uint32_first_write_zero_mask_zero_flips_defined() {
2170        // C parity: paramVal::setUInt32 (paramVal.cpp:200-208) flips
2171        // defined+value_changed on the first set even when the resulting
2172        // value is 0 — driver code commonly does `setUIntDigitalParam(idx, 0, mask)`
2173        // to publish an initial zero state and expects the callback to fire.
2174        let mut pl = ParamList::new(1, false);
2175        let idx = pl.create_param("V", ParamType::UInt32Digital).unwrap();
2176        pl.set_uint32(idx, 0, 0, 0xFFFF, 0).unwrap();
2177        assert!(pl.is_param_defined(idx, 0).unwrap());
2178        assert!(pl.take_changed_single(idx, 0).unwrap());
2179        assert_eq!(pl.get_uint32_strict(idx, 0).unwrap(), 0);
2180    }
2181
2182    #[test]
2183    fn set_string_first_write_empty_flips_defined() {
2184        let mut pl = ParamList::new(1, false);
2185        let idx = pl.create_param("V", ParamType::Octet).unwrap();
2186        pl.set_string(idx, 0, String::new()).unwrap();
2187        assert!(pl.is_param_defined(idx, 0).unwrap());
2188        assert_eq!(pl.get_string_strict(idx, 0).unwrap(), "");
2189    }
2190
2191    #[test]
2192    fn set_enum_index_first_write_to_default_index_flips_defined() {
2193        // C parity: enum index is stored through paramVal::setInteger,
2194        // which gates on `!isDefined() || data != value` — a first set
2195        // to index 0 (the type default) must still flip defined and
2196        // value_changed so the initial selection fires its I/O Intr.
2197        // A freshly created Enum param carries one sentinel choice and
2198        // is undefined; set_enum_index(.., 0) must define it.
2199        let mut pl = ParamList::new(1, false);
2200        let idx = pl.create_param("MODE", ParamType::Enum).unwrap();
2201        assert!(!pl.is_param_defined(idx, 0).unwrap());
2202        pl.set_enum_index(idx, 0, 0).unwrap();
2203        assert!(pl.is_param_defined(idx, 0).unwrap());
2204        assert!(pl.take_changed_single(idx, 0).unwrap());
2205    }
2206
2207    #[test]
2208    fn set_param_status_only_change_marks_value_changed() {
2209        // C paramVal::setStatus (paramVal.cpp:71-78) calls setValueChanged()
2210        // on a status transition with no value change, so the standard
2211        // setParamStatus() + callParamCallbacks() alarm-push delivers an
2212        // I/O Intr. The param value itself is never touched here.
2213        let mut pl = ParamList::new(1, false);
2214        let idx = pl.create_param("S", ParamType::Int32).unwrap();
2215        let _ = pl.take_changed(0).unwrap();
2216        pl.set_param_status(idx, 0, AsynStatus::Timeout, 0, 0)
2217            .unwrap();
2218        let changed = pl.take_changed(0).unwrap();
2219        assert!(
2220            changed.contains(&idx),
2221            "a status-only transition must mark the param value_changed"
2222        );
2223    }
2224
2225    #[test]
2226    fn set_param_alarm_only_change_marks_value_changed() {
2227        // C setAlarmStatus / setAlarmSeverity (paramVal.cpp:84-104) also
2228        // call setValueChanged() on change — status can stay Success.
2229        let mut pl = ParamList::new(1, false);
2230        let idx = pl.create_param("S", ParamType::Float64).unwrap();
2231        let _ = pl.take_changed(0).unwrap();
2232        pl.set_param_status(idx, 0, AsynStatus::Success, 7, 2)
2233            .unwrap();
2234        assert!(
2235            pl.take_changed_single(idx, 0).unwrap(),
2236            "an alarm-status/severity-only transition must mark the param value_changed"
2237        );
2238    }
2239
2240    #[test]
2241    fn set_param_status_change_forces_full_uint32_mask() {
2242        // C setStatus on a UInt32Digital param forces uInt32CallbackMask =
2243        // 0xFFFFFFFF (paramVal.cpp:76) so every subscriber bit fires.
2244        let mut pl = ParamList::new(1, false);
2245        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
2246        let _ = pl.take_changed(0).unwrap();
2247        pl.set_param_status(idx, 0, AsynStatus::Error, 0, 0)
2248            .unwrap();
2249        assert!(pl.take_changed_single(idx, 0).unwrap());
2250        assert_eq!(
2251            pl.get_uint32_interrupt_mask(idx, 0).unwrap(),
2252            0xFFFF_FFFF,
2253            "a UInt32Digital status change must force the full callback mask"
2254        );
2255    }
2256
2257    #[test]
2258    fn set_uint32_force_interrupt_mask_on_unchanged_value_notifies() {
2259        // C paramVal::setUInt32 (paramVal.cpp:220-224): a non-zero
2260        // `interruptMask` ORs those bits into uInt32CallbackMask AND calls
2261        // setValueChanged() UNCONDITIONALLY — even when the merged value is
2262        // identical to the stored value. This is the setUIntDigitalParam
2263        // interruptMask overload (asynPortDriver.cpp:1381): a driver forces
2264        // an I/O Intr on specific bits without changing the value.
2265        let mut pl = ParamList::new(1, false);
2266        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
2267        // Define the param at 0x05 and drain the resulting change + mask.
2268        pl.set_uint32(idx, 0, 0x05, 0x0F, 0).unwrap();
2269        let _ = pl.take_changed(0).unwrap();
2270        let _ = pl.take_uint32_interrupt_mask(idx, 0).unwrap();
2271        // Re-set the SAME value (no value-bit change) but force bit 0x02.
2272        pl.set_uint32(idx, 0, 0x05, 0x0F, 0x02).unwrap();
2273        assert_eq!(
2274            pl.get_uint32(idx, 0).unwrap(),
2275            0x05,
2276            "a forced-interrupt-only set must not change the stored value"
2277        );
2278        assert!(
2279            pl.take_changed(0).unwrap().contains(&idx),
2280            "a forced interruptMask must mark value_changed even on an unchanged value"
2281        );
2282        assert_eq!(
2283            pl.take_uint32_interrupt_mask(idx, 0).unwrap(),
2284            0x02,
2285            "the forced interruptMask bits must land in the callback mask"
2286        );
2287    }
2288
2289    #[test]
2290    fn set_uint32_accumulates_callback_mask_across_sets() {
2291        // C `uInt32CallbackMask |= (uival ^ newValue)` (paramVal.cpp:215):
2292        // two setUInt32 calls before one callParamCallbacks must accumulate
2293        // the union of changed bits, not keep only the last set's bits.
2294        let mut pl = ParamList::new(1, false);
2295        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
2296        pl.set_uint32(idx, 0, 0x01, 0x01, 0).unwrap(); // define + change bit 0
2297        pl.set_uint32(idx, 0, 0x02, 0x02, 0).unwrap(); // change bit 1
2298        assert_eq!(
2299            pl.get_uint32_interrupt_mask(idx, 0).unwrap(),
2300            0x03,
2301            "callback mask must accumulate the union of changed bits since the last flush"
2302        );
2303    }
2304
2305    #[test]
2306    fn take_uint32_interrupt_mask_reads_and_resets() {
2307        // C uint32Callback resets uInt32CallbackMask = 0 after firing
2308        // (asynPortDriver.cpp:855).
2309        let mut pl = ParamList::new(1, false);
2310        let idx = pl.create_param("U", ParamType::UInt32Digital).unwrap();
2311        pl.set_uint32(idx, 0, 0x05, 0x0F, 0).unwrap();
2312        assert_eq!(pl.take_uint32_interrupt_mask(idx, 0).unwrap(), 0x05);
2313        assert_eq!(
2314            pl.get_uint32_interrupt_mask(idx, 0).unwrap(),
2315            0,
2316            "callback mask must be reset to 0 after take"
2317        );
2318    }
2319
2320    #[test]
2321    fn set_param_status_no_change_does_not_mark_value_changed() {
2322        // C gates on `status_ != status` etc., so re-asserting the
2323        // existing status/alarm (the fresh-param default Success/0/0) is a
2324        // no-op and must NOT mark the param dirty.
2325        let mut pl = ParamList::new(1, false);
2326        let idx = pl.create_param("S", ParamType::Int32).unwrap();
2327        let _ = pl.take_changed(0).unwrap();
2328        pl.set_param_status(idx, 0, AsynStatus::Success, 0, 0)
2329            .unwrap();
2330        assert!(
2331            !pl.take_changed_single(idx, 0).unwrap(),
2332            "re-asserting the same status/alarm must not mark the param value_changed"
2333        );
2334    }
2335}