genapi_core/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! GenApi node system: typed feature access backed by register IO.
3
4use std::cell::{Cell, RefCell};
5use std::collections::{hash_map::Entry as HashMapEntry, HashMap, HashSet};
6
7pub use genapi_xml::SkOutput;
8use genapi_xml::{
9    AccessMode, Addressing, BitField, EnumEntryDecl, EnumValueSrc, NodeDecl, XmlModel,
10};
11use thiserror::Error;
12use tracing::{debug, trace, warn};
13
14mod bitops;
15use crate::bitops::{extract, insert, BitOpsError};
16mod swissknife;
17use crate::swissknife::{
18    collect_identifiers, evaluate as eval_ast, parse_expression, AstNode, EvalError as SkEvalError,
19};
20
21/// Error type produced by GenApi operations.
22#[derive(Debug, Error)]
23pub enum GenApiError {
24    /// The requested node does not exist in the nodemap.
25    #[error("node not found: {0}")]
26    NodeNotFound(String),
27    /// The node exists but has a different type.
28    #[error("type mismatch for node: {0}")]
29    Type(String),
30    /// The node access mode forbids the attempted operation.
31    #[error("access denied for node: {0}")]
32    Access(String),
33    /// The provided value violates the limits declared by the node.
34    #[error("range error for node: {0}")]
35    Range(String),
36    /// The node is currently hidden by selector state.
37    #[error("node unavailable: {0}")]
38    Unavailable(String),
39    /// Underlying register IO failed.
40    #[error("io error: {0}")]
41    Io(String),
42    /// Node metadata or conversion failed.
43    #[error("parse error: {0}")]
44    Parse(String),
45    /// Parsing a SwissKnife expression failed.
46    #[error("failed to parse expression for {name}: {msg}")]
47    ExprParse { name: String, msg: String },
48    /// Evaluating a SwissKnife expression failed at runtime.
49    #[error("failed to evaluate expression for {name}: {msg}")]
50    ExprEval { name: String, msg: String },
51    /// A SwissKnife expression referenced an unknown variable.
52    #[error("unknown variable '{var}' referenced by {name}")]
53    UnknownVariable { name: String, var: String },
54    /// Raw register value did not correspond to any enum entry.
55    #[error("enum {node} has no entry for raw value {value}")]
56    EnumValueUnknown { node: String, value: i64 },
57    /// Attempted to select an enum entry that does not exist.
58    #[error("enum {node} has no entry named {entry}")]
59    EnumNoSuchEntry { node: String, entry: String },
60    /// Indirect addressing resolved to an invalid register.
61    #[error("node {name} resolved invalid indirect address {addr:#X}")]
62    BadIndirectAddress { name: String, addr: i64 },
63    /// Bitfield metadata exceeded the backing register width.
64    #[error(
65        "bitfield for node {name} exceeds register length {len} (offset {bit_offset}, length {bit_length})"
66    )]
67    BitfieldOutOfRange {
68        name: String,
69        bit_offset: u16,
70        bit_length: u16,
71        len: usize,
72    },
73    /// Provided value does not fit into the declared bitfield.
74    #[error("value {value} too wide for {bit_length}-bit field on node {name}")]
75    ValueTooWide {
76        name: String,
77        value: i64,
78        bit_length: u16,
79    },
80}
81
82/// Register access abstraction backed by transports such as GVCP/GenCP.
83pub trait RegisterIo {
84    /// Read `len` bytes starting at `addr`.
85    fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError>;
86    /// Write `data` starting at `addr`.
87    fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError>;
88}
89
90/// Node kinds supported by the Tier-1 subset.
91#[derive(Debug)]
92pub enum Node {
93    /// Signed integer feature stored in a fixed-width register block.
94    Integer(IntegerNode),
95    /// Floating point feature with optional scale/offset conversion.
96    Float(FloatNode),
97    /// Enumeration feature mapping integers to symbolic names.
98    Enum(EnumNode),
99    /// Boolean feature represented as an integer register.
100    Boolean(BooleanNode),
101    /// Command feature triggering a device-side action when written.
102    Command(CommandNode),
103    /// Category organising related features.
104    Category(CategoryNode),
105    /// SwissKnife expression producing a computed value.
106    SwissKnife(SkNode),
107}
108
109impl Node {
110    fn invalidate_cache(&self) {
111        match self {
112            Node::Integer(node) => {
113                node.cache.replace(None);
114                node.raw_cache.replace(None);
115            }
116            Node::Float(node) => {
117                node.cache.replace(None);
118            }
119            Node::Enum(node) => node.invalidate(),
120            Node::Boolean(node) => {
121                node.cache.replace(None);
122                node.raw_cache.replace(None);
123            }
124            Node::SwissKnife(node) => {
125                node.cache.replace(None);
126            }
127            Node::Command(_) | Node::Category(_) => {}
128        }
129    }
130}
131
132fn register_addressing_dependency(
133    dependents: &mut HashMap<String, Vec<String>>,
134    node_name: &str,
135    addressing: &Addressing,
136) {
137    match addressing {
138        Addressing::Fixed { .. } => {}
139        Addressing::BySelector { selector, .. } => {
140            dependents
141                .entry(selector.clone())
142                .or_default()
143                .push(node_name.to_string());
144        }
145        Addressing::Indirect { p_address_node, .. } => {
146            dependents
147                .entry(p_address_node.clone())
148                .or_default()
149                .push(node_name.to_string());
150        }
151    }
152}
153
154/// Integer feature metadata extracted from the XML description.
155#[derive(Debug)]
156pub struct IntegerNode {
157    /// Unique feature name.
158    pub name: String,
159    /// Register addressing metadata (fixed, selector-based, or indirect).
160    pub addressing: Addressing,
161    /// Nominal register length in bytes.
162    pub len: u32,
163    /// Declared access rights.
164    pub access: AccessMode,
165    /// Minimum permitted user value.
166    pub min: i64,
167    /// Maximum permitted user value.
168    pub max: i64,
169    /// Optional increment step the value must respect.
170    pub inc: Option<i64>,
171    /// Optional engineering unit such as "us".
172    pub unit: Option<String>,
173    /// Optional bitfield metadata restricting active bits.
174    pub bitfield: Option<BitField>,
175    /// Selector nodes controlling the visibility of this node.
176    pub selectors: Vec<String>,
177    /// Selector gating rules in the form `(selector, allowed values)`.
178    pub selected_if: Vec<(String, Vec<String>)>,
179    cache: RefCell<Option<i64>>,
180    raw_cache: RefCell<Option<Vec<u8>>>,
181}
182
183/// Floating point feature metadata.
184#[derive(Debug)]
185pub struct FloatNode {
186    pub name: String,
187    pub addressing: Addressing,
188    pub access: AccessMode,
189    pub min: f64,
190    pub max: f64,
191    pub unit: Option<String>,
192    /// Optional rational scale `(numerator, denominator)` applied to the raw value.
193    pub scale: Option<(i64, i64)>,
194    /// Optional offset added after scaling.
195    pub offset: Option<f64>,
196    pub selectors: Vec<String>,
197    pub selected_if: Vec<(String, Vec<String>)>,
198    cache: RefCell<Option<f64>>,
199}
200
201/// Enumeration feature metadata and mapping tables.
202#[derive(Debug)]
203pub struct EnumNode {
204    pub name: String,
205    pub addressing: Addressing,
206    pub access: AccessMode,
207    pub entries: Vec<EnumEntryDecl>,
208    pub default: Option<String>,
209    pub selectors: Vec<String>,
210    pub selected_if: Vec<(String, Vec<String>)>,
211    pub providers: Vec<String>,
212    value_cache: RefCell<Option<String>>,
213    mapping_cache: RefCell<Option<EnumMapping>>,
214}
215
216#[derive(Debug, Clone)]
217struct EnumMapping {
218    by_value: HashMap<i64, String>,
219    by_name: HashMap<String, i64>,
220}
221
222/// Boolean feature metadata.
223#[derive(Debug)]
224pub struct BooleanNode {
225    pub name: String,
226    pub addressing: Addressing,
227    pub len: u32,
228    pub access: AccessMode,
229    pub bitfield: BitField,
230    pub selectors: Vec<String>,
231    pub selected_if: Vec<(String, Vec<String>)>,
232    cache: RefCell<Option<bool>>,
233    raw_cache: RefCell<Option<Vec<u8>>>,
234}
235
236/// SwissKnife node evaluating an arithmetic expression referencing other nodes.
237///
238/// Integer outputs follow round-to-nearest semantics with ties towards zero
239/// after the expression has been evaluated as `f64`.
240#[derive(Debug)]
241pub struct SkNode {
242    /// Unique feature name.
243    pub name: String,
244    /// Desired output type as declared in the XML.
245    pub output: SkOutput,
246    /// Parsed expression AST.
247    pub ast: AstNode,
248    /// Mapping of variable identifiers to provider node names.
249    pub vars: Vec<(String, String)>,
250    /// Cached value alongside the generation it was computed in.
251    pub cache: RefCell<Option<(f64, u64)>>,
252}
253
254impl EnumNode {
255    fn invalidate(&self) {
256        self.value_cache.replace(None);
257        self.mapping_cache.replace(None);
258    }
259}
260
261/// Command feature metadata.
262#[derive(Debug)]
263pub struct CommandNode {
264    pub name: String,
265    pub address: u64,
266    pub len: u32,
267}
268
269/// Category node describing child feature names.
270#[derive(Debug)]
271pub struct CategoryNode {
272    pub name: String,
273    pub children: Vec<String>,
274}
275
276/// Runtime nodemap built from an [`XmlModel`] capable of reading and writing
277/// feature values via a [`RegisterIo`] transport.
278#[derive(Debug)]
279pub struct NodeMap {
280    version: String,
281    nodes: HashMap<String, Node>,
282    dependents: HashMap<String, Vec<String>>,
283    generation: Cell<u64>,
284}
285
286impl NodeMap {
287    /// Return the schema version string associated with the XML description.
288    pub fn version(&self) -> &str {
289        &self.version
290    }
291
292    /// Fetch a node by name for inspection.
293    pub fn node(&self, name: &str) -> Option<&Node> {
294        self.nodes.get(name)
295    }
296
297    /// Construct a [`NodeMap`] from an [`XmlModel`], validating SwissKnife expressions.
298    pub fn try_from_xml(model: XmlModel) -> Result<Self, GenApiError> {
299        let mut nodes = HashMap::new();
300        let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
301        for decl in model.nodes {
302            match decl {
303                NodeDecl::Integer {
304                    name,
305                    addressing,
306                    len,
307                    access,
308                    min,
309                    max,
310                    inc,
311                    unit,
312                    bitfield,
313                    selectors,
314                    selected_if,
315                } => {
316                    register_addressing_dependency(&mut dependents, &name, &addressing);
317                    for (selector, _) in &selected_if {
318                        dependents
319                            .entry(selector.clone())
320                            .or_default()
321                            .push(name.clone());
322                    }
323                    let node = IntegerNode {
324                        name: name.clone(),
325                        addressing,
326                        len,
327                        access,
328                        min,
329                        max,
330                        inc,
331                        unit,
332                        bitfield,
333                        selectors,
334                        selected_if,
335                        cache: RefCell::new(None),
336                        raw_cache: RefCell::new(None),
337                    };
338                    nodes.insert(name, Node::Integer(node));
339                }
340                NodeDecl::Float {
341                    name,
342                    addressing,
343                    access,
344                    min,
345                    max,
346                    unit,
347                    scale,
348                    offset,
349                    selectors,
350                    selected_if,
351                } => {
352                    register_addressing_dependency(&mut dependents, &name, &addressing);
353                    for (selector, _) in &selected_if {
354                        dependents
355                            .entry(selector.clone())
356                            .or_default()
357                            .push(name.clone());
358                    }
359                    let node = FloatNode {
360                        name: name.clone(),
361                        addressing,
362                        access,
363                        min,
364                        max,
365                        unit,
366                        scale,
367                        offset,
368                        selectors,
369                        selected_if,
370                        cache: RefCell::new(None),
371                    };
372                    nodes.insert(name, Node::Float(node));
373                }
374                NodeDecl::Enum {
375                    name,
376                    addressing,
377                    access,
378                    entries,
379                    default,
380                    selectors,
381                    selected_if,
382                } => {
383                    register_addressing_dependency(&mut dependents, &name, &addressing);
384                    for (selector, _) in &selected_if {
385                        dependents
386                            .entry(selector.clone())
387                            .or_default()
388                            .push(name.clone());
389                    }
390                    let mut providers = Vec::new();
391                    let mut provider_set = HashSet::new();
392                    for entry in &entries {
393                        if let EnumValueSrc::FromNode(node_name) = &entry.value {
394                            dependents
395                                .entry(node_name.clone())
396                                .or_default()
397                                .push(name.clone());
398                            if provider_set.insert(node_name.clone()) {
399                                providers.push(node_name.clone());
400                            }
401                        }
402                    }
403                    providers.sort();
404                    let node = EnumNode {
405                        name: name.clone(),
406                        addressing,
407                        access,
408                        entries,
409                        default,
410                        selectors,
411                        selected_if,
412                        providers,
413                        value_cache: RefCell::new(None),
414                        mapping_cache: RefCell::new(None),
415                    };
416                    nodes.insert(name, Node::Enum(node));
417                }
418                NodeDecl::Boolean {
419                    name,
420                    addressing,
421                    len,
422                    access,
423                    bitfield,
424                    selectors,
425                    selected_if,
426                } => {
427                    register_addressing_dependency(&mut dependents, &name, &addressing);
428                    for (selector, _) in &selected_if {
429                        dependents
430                            .entry(selector.clone())
431                            .or_default()
432                            .push(name.clone());
433                    }
434                    let node = BooleanNode {
435                        name: name.clone(),
436                        addressing,
437                        len,
438                        access,
439                        bitfield,
440                        selectors,
441                        selected_if,
442                        cache: RefCell::new(None),
443                        raw_cache: RefCell::new(None),
444                    };
445                    nodes.insert(name, Node::Boolean(node));
446                }
447                NodeDecl::Command { name, address, len } => {
448                    let node = CommandNode {
449                        name: name.clone(),
450                        address,
451                        len,
452                    };
453                    nodes.insert(name, Node::Command(node));
454                }
455                NodeDecl::Category { name, children } => {
456                    let node = CategoryNode {
457                        name: name.clone(),
458                        children,
459                    };
460                    nodes.insert(name, Node::Category(node));
461                }
462                NodeDecl::SwissKnife(decl) => {
463                    let name = decl.name;
464                    let expr = decl.expr;
465                    let variables = decl.variables;
466                    let output = decl.output;
467                    let ast = parse_expression(&expr).map_err(|err| GenApiError::ExprParse {
468                        name: name.clone(),
469                        msg: err.to_string(),
470                    })?;
471                    let mut used = HashSet::new();
472                    collect_identifiers(&ast, &mut used);
473                    for ident in &used {
474                        if !variables.iter().any(|(var, _)| var == ident) {
475                            return Err(GenApiError::UnknownVariable {
476                                name: name.clone(),
477                                var: ident.clone(),
478                            });
479                        }
480                    }
481                    for (_, provider) in &variables {
482                        dependents
483                            .entry(provider.clone())
484                            .or_default()
485                            .push(name.clone());
486                    }
487                    let node = SkNode {
488                        name: name.clone(),
489                        output,
490                        ast,
491                        vars: variables,
492                        cache: RefCell::new(None),
493                    };
494                    nodes.insert(name, Node::SwissKnife(node));
495                }
496            }
497        }
498
499        Ok(NodeMap {
500            version: model.version,
501            nodes,
502            dependents,
503            generation: Cell::new(0),
504        })
505    }
506
507    /// Read an integer feature value using the provided transport.
508    pub fn get_integer(&self, name: &str, io: &dyn RegisterIo) -> Result<i64, GenApiError> {
509        if let Some(output) = self.nodes.get(name).and_then(|node| match node {
510            Node::SwissKnife(sk) => Some(sk.output),
511            _ => None,
512        }) {
513            return match output {
514                SkOutput::Integer => {
515                    let node = match self.nodes.get(name) {
516                        Some(Node::SwissKnife(node)) => node,
517                        _ => unreachable!("node vanished during lookup"),
518                    };
519                    let mut stack = HashSet::new();
520                    let value = self.evaluate_swissknife(node, io, &mut stack)?;
521                    round_to_i64(name, value)
522                }
523                SkOutput::Float => Err(GenApiError::Type(name.to_string())),
524            };
525        }
526        let node = self.get_integer_node(name)?;
527        ensure_readable(&node.access, name)?;
528        self.ensure_selectors(name, &node.selected_if, io)?;
529        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
530        if let Some(value) = *node.cache.borrow() {
531            return Ok(value);
532        }
533        let raw = io.read(address, len as usize).map_err(|err| match err {
534            GenApiError::Io(_) => err,
535            other => other,
536        })?;
537        let value = if let Some(bitfield) = node.bitfield {
538            let extracted = extract(&raw, bitfield).map_err(|err| map_bitops_error(name, err))?;
539            interpret_bitfield_value(name, extracted, bitfield.bit_length, node.min < 0)?
540        } else {
541            bytes_to_i64(name, &raw)?
542        };
543        debug!(node = %name, raw = value, "read integer feature");
544        node.cache.replace(Some(value));
545        node.raw_cache.replace(Some(raw));
546        Ok(value)
547    }
548
549    /// Write an integer feature and update dependent caches.
550    pub fn set_integer(
551        &mut self,
552        name: &str,
553        value: i64,
554        io: &dyn RegisterIo,
555    ) -> Result<(), GenApiError> {
556        let node = self.get_integer_node(name)?;
557        ensure_writable(&node.access, name)?;
558        self.ensure_selectors(name, &node.selected_if, io)?;
559        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
560        if value < node.min || value > node.max {
561            return Err(GenApiError::Range(name.to_string()));
562        }
563        if let Some(inc) = node.inc {
564            if inc != 0 && (value - node.min) % inc != 0 {
565                return Err(GenApiError::Range(name.to_string()));
566            }
567        }
568        if let Some(bitfield) = node.bitfield {
569            let encoded = encode_bitfield_value(name, value, bitfield.bit_length, node.min < 0)?;
570            let cached = node.raw_cache.borrow().clone();
571            let mut raw = if let Some(bytes) = cached {
572                if bytes.len() == len as usize {
573                    bytes
574                } else {
575                    io.read(address, len as usize).map_err(|err| match err {
576                        GenApiError::Io(_) => err,
577                        other => other,
578                    })?
579                }
580            } else {
581                io.read(address, len as usize).map_err(|err| match err {
582                    GenApiError::Io(_) => err,
583                    other => other,
584                })?
585            };
586            insert(&mut raw, bitfield, encoded).map_err(|err| map_bitops_error(name, err))?;
587            debug!(node = %name, raw = value, "write integer feature");
588            io.write(address, &raw).map_err(|err| match err {
589                GenApiError::Io(_) => err,
590                other => other,
591            })?;
592            node.cache.replace(Some(value));
593            node.raw_cache.replace(Some(raw));
594        } else {
595            let bytes = i64_to_bytes(name, value, len)?;
596            debug!(node = %name, raw = value, "write integer feature");
597            io.write(address, &bytes).map_err(|err| match err {
598                GenApiError::Io(_) => err,
599                other => other,
600            })?;
601            node.cache.replace(Some(value));
602            node.raw_cache.replace(Some(bytes));
603        }
604        self.invalidate_dependents(name);
605        Ok(())
606    }
607
608    /// Read a floating point feature.
609    pub fn get_float(&self, name: &str, io: &dyn RegisterIo) -> Result<f64, GenApiError> {
610        if let Some(output) = self.nodes.get(name).and_then(|node| match node {
611            Node::SwissKnife(sk) => Some(sk.output),
612            _ => None,
613        }) {
614            return match output {
615                SkOutput::Float => {
616                    let node = match self.nodes.get(name) {
617                        Some(Node::SwissKnife(node)) => node,
618                        _ => unreachable!("node vanished during lookup"),
619                    };
620                    let mut stack = HashSet::new();
621                    let value = self.evaluate_swissknife(node, io, &mut stack)?;
622                    Ok(value)
623                }
624                SkOutput::Integer => self.get_integer(name, io).map(|v| v as f64),
625            };
626        }
627        let node = self.get_float_node(name)?;
628        ensure_readable(&node.access, name)?;
629        self.ensure_selectors(name, &node.selected_if, io)?;
630        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
631        if let Some(value) = *node.cache.borrow() {
632            return Ok(value);
633        }
634        let raw = io.read(address, len as usize).map_err(|err| match err {
635            GenApiError::Io(_) => err,
636            other => other,
637        })?;
638        let raw_value = bytes_to_i64(name, &raw)?;
639        let value = apply_scale(node, raw_value as f64);
640        debug!(node = %name, raw = raw_value, value, "read float feature");
641        node.cache.replace(Some(value));
642        Ok(value)
643    }
644
645    /// Write a floating point feature using the scale/offset conversion.
646    pub fn set_float(
647        &mut self,
648        name: &str,
649        value: f64,
650        io: &dyn RegisterIo,
651    ) -> Result<(), GenApiError> {
652        let node = self.get_float_node(name)?;
653        ensure_writable(&node.access, name)?;
654        self.ensure_selectors(name, &node.selected_if, io)?;
655        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
656        if value < node.min || value > node.max {
657            return Err(GenApiError::Range(name.to_string()));
658        }
659        let raw = encode_float(node, value)?;
660        let bytes = i64_to_bytes(name, raw, len)?;
661        debug!(node = %name, raw, value, "write float feature");
662        io.write(address, &bytes).map_err(|err| match err {
663            GenApiError::Io(_) => err,
664            other => other,
665        })?;
666        node.cache.replace(Some(value));
667        self.invalidate_dependents(name);
668        Ok(())
669    }
670
671    /// Read an enumeration feature returning the symbolic entry name.
672    pub fn get_enum(&self, name: &str, io: &dyn RegisterIo) -> Result<String, GenApiError> {
673        let node = self.get_enum_node(name)?;
674        ensure_readable(&node.access, name)?;
675        self.ensure_selectors(name, &node.selected_if, io)?;
676        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
677        if let Some(value) = node.value_cache.borrow().clone() {
678            return Ok(value);
679        }
680        let raw = io.read(address, len as usize).map_err(|err| match err {
681            GenApiError::Io(_) => err,
682            other => other,
683        })?;
684        let raw_value = bytes_to_i64(name, &raw)?;
685        let entry = self.lookup_enum_entry(node, raw_value, io)?;
686        debug!(node = %name, raw = raw_value, entry = %entry, "read enum feature");
687        node.value_cache.replace(Some(entry.clone()));
688        Ok(entry)
689    }
690
691    /// Write an enumeration entry.
692    pub fn set_enum(
693        &mut self,
694        name: &str,
695        entry: &str,
696        io: &dyn RegisterIo,
697    ) -> Result<(), GenApiError> {
698        let node = self.get_enum_node(name)?;
699        ensure_writable(&node.access, name)?;
700        self.ensure_selectors(name, &node.selected_if, io)?;
701        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
702        let entry_decl = node
703            .entries
704            .iter()
705            .find(|candidate| candidate.name == entry)
706            .ok_or_else(|| GenApiError::EnumNoSuchEntry {
707                node: name.to_string(),
708                entry: entry.to_string(),
709            })?;
710        let raw = self.resolve_enum_entry_value(node, entry_decl, io)?;
711        let bytes = i64_to_bytes(name, raw, len)?;
712        debug!(node = %name, raw, entry, "write enum feature");
713        io.write(address, &bytes).map_err(|err| match err {
714            GenApiError::Io(_) => err,
715            other => other,
716        })?;
717        node.value_cache.replace(None);
718        self.invalidate_dependents(name);
719        Ok(())
720    }
721
722    /// List the available entry names for an enumeration feature.
723    pub fn enum_entries(&self, name: &str) -> Result<Vec<String>, GenApiError> {
724        let node = self.get_enum_node(name)?;
725        if let Some(mapping) = node.mapping_cache.borrow().as_ref() {
726            let mut names: Vec<_> = mapping.by_name.keys().cloned().collect();
727            names.sort();
728            names.dedup();
729            return Ok(names);
730        }
731        let mut names: Vec<_> = node
732            .entries
733            .iter()
734            .map(|entry| entry.name.clone())
735            .collect();
736        names.sort();
737        names.dedup();
738        Ok(names)
739    }
740
741    /// Read a boolean feature.
742    pub fn get_bool(&self, name: &str, io: &dyn RegisterIo) -> Result<bool, GenApiError> {
743        let node = self.get_bool_node(name)?;
744        ensure_readable(&node.access, name)?;
745        self.ensure_selectors(name, &node.selected_if, io)?;
746        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
747        if let Some(value) = *node.cache.borrow() {
748            return Ok(value);
749        }
750        let raw = io.read(address, len as usize).map_err(|err| match err {
751            GenApiError::Io(_) => err,
752            other => other,
753        })?;
754        let raw_value = extract(&raw, node.bitfield).map_err(|err| map_bitops_error(name, err))?;
755        let value = raw_value != 0;
756        debug!(node = %name, raw = raw_value, value, "read boolean feature");
757        node.cache.replace(Some(value));
758        node.raw_cache.replace(Some(raw));
759        Ok(value)
760    }
761
762    /// Write a boolean feature.
763    pub fn set_bool(
764        &mut self,
765        name: &str,
766        value: bool,
767        io: &dyn RegisterIo,
768    ) -> Result<(), GenApiError> {
769        let node = self.get_bool_node(name)?;
770        ensure_writable(&node.access, name)?;
771        self.ensure_selectors(name, &node.selected_if, io)?;
772        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
773        let encoded = if value { 1 } else { 0 };
774        let cached = node.raw_cache.borrow().clone();
775        let mut raw = if let Some(bytes) = cached {
776            if bytes.len() == len as usize {
777                bytes
778            } else {
779                io.read(address, len as usize).map_err(|err| match err {
780                    GenApiError::Io(_) => err,
781                    other => other,
782                })?
783            }
784        } else {
785            io.read(address, len as usize).map_err(|err| match err {
786                GenApiError::Io(_) => err,
787                other => other,
788            })?
789        };
790        insert(&mut raw, node.bitfield, encoded).map_err(|err| map_bitops_error(name, err))?;
791        debug!(node = %name, raw = encoded, value, "write boolean feature");
792        io.write(address, &raw).map_err(|err| match err {
793            GenApiError::Io(_) => err,
794            other => other,
795        })?;
796        node.cache.replace(Some(value));
797        node.raw_cache.replace(Some(raw));
798        self.invalidate_dependents(name);
799        Ok(())
800    }
801
802    /// Execute a command feature by writing a one-valued payload.
803    pub fn exec_command(&mut self, name: &str, io: &dyn RegisterIo) -> Result<(), GenApiError> {
804        let node = self.get_command_node(name)?;
805        if node.len == 0 {
806            return Err(GenApiError::Parse(format!(
807                "command node {name} has zero length"
808            )));
809        }
810        let mut data = vec![0u8; node.len as usize];
811        if let Some(last) = data.last_mut() {
812            *last = 1;
813        }
814        debug!(node = %name, "execute command");
815        io.write(node.address, &data).map_err(|err| match err {
816            GenApiError::Io(_) => err,
817            other => other,
818        })?;
819        self.invalidate_dependents(name);
820        Ok(())
821    }
822
823    fn get_integer_node(&self, name: &str) -> Result<&IntegerNode, GenApiError> {
824        match self.nodes.get(name) {
825            Some(Node::Integer(node)) => Ok(node),
826            Some(_) => Err(GenApiError::Type(name.to_string())),
827            None => Err(GenApiError::NodeNotFound(name.to_string())),
828        }
829    }
830
831    fn get_float_node(&self, name: &str) -> Result<&FloatNode, GenApiError> {
832        match self.nodes.get(name) {
833            Some(Node::Float(node)) => Ok(node),
834            Some(_) => Err(GenApiError::Type(name.to_string())),
835            None => Err(GenApiError::NodeNotFound(name.to_string())),
836        }
837    }
838
839    fn get_enum_node(&self, name: &str) -> Result<&EnumNode, GenApiError> {
840        match self.nodes.get(name) {
841            Some(Node::Enum(node)) => Ok(node),
842            Some(_) => Err(GenApiError::Type(name.to_string())),
843            None => Err(GenApiError::NodeNotFound(name.to_string())),
844        }
845    }
846
847    fn get_bool_node(&self, name: &str) -> Result<&BooleanNode, GenApiError> {
848        match self.nodes.get(name) {
849            Some(Node::Boolean(node)) => Ok(node),
850            Some(_) => Err(GenApiError::Type(name.to_string())),
851            None => Err(GenApiError::NodeNotFound(name.to_string())),
852        }
853    }
854
855    fn get_command_node(&self, name: &str) -> Result<&CommandNode, GenApiError> {
856        match self.nodes.get(name) {
857            Some(Node::Command(node)) => Ok(node),
858            Some(_) => Err(GenApiError::Type(name.to_string())),
859            None => Err(GenApiError::NodeNotFound(name.to_string())),
860        }
861    }
862
863    fn ensure_selectors(
864        &self,
865        node_name: &str,
866        rules: &[(String, Vec<String>)],
867        io: &dyn RegisterIo,
868    ) -> Result<(), GenApiError> {
869        for (selector, allowed) in rules {
870            if allowed.is_empty() {
871                continue;
872            }
873            let current = self.get_selector_value(selector, io)?;
874            if !allowed.iter().any(|value| value == &current) {
875                return Err(GenApiError::Unavailable(format!(
876                    "node '{node_name}' unavailable for selector '{selector}={current}'"
877                )));
878            }
879        }
880        Ok(())
881    }
882
883    fn lookup_enum_entry(
884        &self,
885        node: &EnumNode,
886        raw_value: i64,
887        io: &dyn RegisterIo,
888    ) -> Result<String, GenApiError> {
889        {
890            let mut cache = node.mapping_cache.borrow_mut();
891            if cache.is_none() {
892                *cache = Some(self.build_enum_mapping(node, io)?);
893            }
894            if let Some(mapping) = cache.as_ref() {
895                if let Some(entry) = mapping.by_value.get(&raw_value) {
896                    return Ok(entry.clone());
897                }
898            }
899            *cache = Some(self.build_enum_mapping(node, io)?);
900            if let Some(mapping) = cache.as_ref() {
901                if let Some(entry) = mapping.by_value.get(&raw_value) {
902                    return Ok(entry.clone());
903                }
904            }
905        }
906        Err(GenApiError::EnumValueUnknown {
907            node: node.name.clone(),
908            value: raw_value,
909        })
910    }
911
912    fn build_enum_mapping(
913        &self,
914        node: &EnumNode,
915        io: &dyn RegisterIo,
916    ) -> Result<EnumMapping, GenApiError> {
917        let mut by_value = HashMap::new();
918        let mut by_name = HashMap::new();
919
920        for entry in &node.entries {
921            let value = self.resolve_enum_entry_value(node, entry, io)?;
922            match by_value.entry(value) {
923                HashMapEntry::Vacant(slot) => {
924                    slot.insert(entry.name.clone());
925                }
926                HashMapEntry::Occupied(existing) => {
927                    warn!(
928                        enum_node = %node.name,
929                        value,
930                        kept = %existing.get(),
931                        dropped = %entry.name,
932                        "duplicate enum value"
933                    );
934                }
935            }
936            by_name.insert(entry.name.clone(), value);
937        }
938
939        let mut summary: Vec<_> = by_value
940            .iter()
941            .map(|(value, name)| (*value, name.clone()))
942            .collect();
943        summary.sort_by_key(|(value, _)| *value);
944        debug!(node = %node.name, entries = ?summary, "build enum mapping");
945
946        Ok(EnumMapping { by_value, by_name })
947    }
948
949    fn resolve_enum_entry_value(
950        &self,
951        node: &EnumNode,
952        entry: &EnumEntryDecl,
953        io: &dyn RegisterIo,
954    ) -> Result<i64, GenApiError> {
955        match &entry.value {
956            EnumValueSrc::Literal(value) => Ok(*value),
957            EnumValueSrc::FromNode(provider) => {
958                let value = self.get_integer(provider, io)?;
959                trace!(
960                    enum_node = %node.name,
961                    entry = %entry.name,
962                    provider = %provider,
963                    value,
964                    "resolved enum entry from provider"
965                );
966                Ok(value)
967            }
968        }
969    }
970
971    fn resolve_address(
972        &self,
973        node_name: &str,
974        addressing: &Addressing,
975        io: &dyn RegisterIo,
976    ) -> Result<(u64, u32), GenApiError> {
977        match addressing {
978            Addressing::Fixed { address, len } => Ok((*address, *len)),
979            Addressing::BySelector { selector, map } => {
980                let value = self.get_selector_value(selector, io)?;
981                if let Some((_, (address, len))) = map.iter().find(|(name, _)| name == &value) {
982                    let addr = *address;
983                    let len = *len;
984                    debug!(
985                        node = %node_name,
986                        selector = %selector,
987                        value = %value,
988                        address = format_args!("0x{addr:X}"),
989                        len,
990                        "resolve address via selector"
991                    );
992                    Ok((addr, len))
993                } else {
994                    Err(GenApiError::Unavailable(format!(
995                        "node '{node_name}' unavailable for selector '{selector}={value}'"
996                    )))
997                }
998            }
999            Addressing::Indirect {
1000                p_address_node,
1001                len,
1002            } => {
1003                let addr_value = self.get_integer(p_address_node, io)?;
1004                if addr_value <= 0 {
1005                    return Err(GenApiError::BadIndirectAddress {
1006                        name: node_name.to_string(),
1007                        addr: addr_value,
1008                    });
1009                }
1010                let addr =
1011                    u64::try_from(addr_value).map_err(|_| GenApiError::BadIndirectAddress {
1012                        name: node_name.to_string(),
1013                        addr: addr_value,
1014                    })?;
1015                if addr == 0 {
1016                    return Err(GenApiError::BadIndirectAddress {
1017                        name: node_name.to_string(),
1018                        addr: addr_value,
1019                    });
1020                }
1021                debug!(
1022                    node = %node_name,
1023                    source = %p_address_node,
1024                    address = format_args!("0x{addr:X}"),
1025                    len = *len,
1026                    "resolve address via pAddress"
1027                );
1028                Ok((addr, *len))
1029            }
1030        }
1031    }
1032
1033    fn get_selector_value(
1034        &self,
1035        selector: &str,
1036        io: &dyn RegisterIo,
1037    ) -> Result<String, GenApiError> {
1038        match self.nodes.get(selector) {
1039            Some(Node::Enum(_)) => self.get_enum(selector, io),
1040            Some(Node::Boolean(_)) => Ok(self.get_bool(selector, io)?.to_string()),
1041            Some(Node::Integer(_)) => Ok(self.get_integer(selector, io)?.to_string()),
1042            Some(_) => Err(GenApiError::Parse(format!(
1043                "selector {selector} has unsupported type"
1044            ))),
1045            None => Err(GenApiError::NodeNotFound(selector.to_string())),
1046        }
1047    }
1048
1049    fn evaluate_swissknife(
1050        &self,
1051        node: &SkNode,
1052        io: &dyn RegisterIo,
1053        stack: &mut HashSet<String>,
1054    ) -> Result<f64, GenApiError> {
1055        if let Some((value, gen)) = *node.cache.borrow() {
1056            if gen == self.generation.get() {
1057                return Ok(value);
1058            }
1059        }
1060        if !stack.insert(node.name.clone()) {
1061            stack.remove(&node.name);
1062            return Err(GenApiError::ExprEval {
1063                name: node.name.clone(),
1064                msg: "cyclic dependency".into(),
1065            });
1066        }
1067        let current_gen = self.generation.get();
1068        let result = (|| {
1069            let mut values: HashMap<String, f64> = HashMap::new();
1070            let mut inputs = Vec::new();
1071            for (var, provider) in &node.vars {
1072                let value = self.resolve_numeric(provider, io, stack)?;
1073                values.insert(var.clone(), value);
1074                inputs.push((var.clone(), value));
1075            }
1076            let mut resolver = |ident: &str| -> Result<f64, SkEvalError> {
1077                values
1078                    .get(ident)
1079                    .copied()
1080                    .ok_or_else(|| SkEvalError::UnknownVariable(ident.to_string()))
1081            };
1082            let value = match eval_ast(&node.ast, &mut resolver) {
1083                Ok(value) => value,
1084                Err(SkEvalError::UnknownVariable(var)) => {
1085                    return Err(GenApiError::UnknownVariable {
1086                        name: node.name.clone(),
1087                        var,
1088                    });
1089                }
1090                Err(SkEvalError::DivisionByZero) => {
1091                    return Err(GenApiError::ExprEval {
1092                        name: node.name.clone(),
1093                        msg: "division by zero".into(),
1094                    });
1095                }
1096            };
1097            debug!(node = %node.name, inputs = ?inputs, output = value, "evaluate SwissKnife");
1098            Ok(value)
1099        })();
1100        stack.remove(&node.name);
1101        match result {
1102            Ok(value) => {
1103                node.cache.replace(Some((value, current_gen)));
1104                Ok(value)
1105            }
1106            Err(err) => Err(err),
1107        }
1108    }
1109
1110    fn resolve_numeric(
1111        &self,
1112        provider: &str,
1113        io: &dyn RegisterIo,
1114        stack: &mut HashSet<String>,
1115    ) -> Result<f64, GenApiError> {
1116        match self.nodes.get(provider) {
1117            Some(Node::Integer(_)) => self.get_integer(provider, io).map(|v| v as f64),
1118            Some(Node::Float(_)) => self.get_float(provider, io),
1119            Some(Node::Boolean(_)) => Ok(if self.get_bool(provider, io)? {
1120                1.0
1121            } else {
1122                0.0
1123            }),
1124            Some(Node::Enum(_)) => self.get_enum_numeric(provider, io).map(|v| v as f64),
1125            Some(Node::SwissKnife(node)) => self.evaluate_swissknife(node, io, stack),
1126            Some(_) => Err(GenApiError::Type(provider.to_string())),
1127            None => Err(GenApiError::NodeNotFound(provider.to_string())),
1128        }
1129    }
1130
1131    fn get_enum_numeric(&self, name: &str, io: &dyn RegisterIo) -> Result<i64, GenApiError> {
1132        let entry = self.get_enum(name, io)?;
1133        let node = self.get_enum_node(name)?;
1134        {
1135            let mut mapping = node.mapping_cache.borrow_mut();
1136            if mapping.is_none() {
1137                *mapping = Some(self.build_enum_mapping(node, io)?);
1138            }
1139            if let Some(map) = mapping.as_ref() {
1140                if let Some(value) = map.by_name.get(&entry) {
1141                    return Ok(*value);
1142                }
1143            }
1144        }
1145        Err(GenApiError::EnumNoSuchEntry {
1146            node: name.to_string(),
1147            entry,
1148        })
1149    }
1150
1151    fn invalidate_dependents(&self, name: &str) {
1152        self.bump_generation();
1153        if let Some(children) = self.dependents.get(name) {
1154            let mut visited = HashSet::new();
1155            for child in children {
1156                self.invalidate_recursive(child, &mut visited);
1157            }
1158        }
1159    }
1160
1161    fn invalidate_recursive(&self, name: &str, visited: &mut HashSet<String>) {
1162        if !visited.insert(name.to_string()) {
1163            return;
1164        }
1165        if let Some(node) = self.nodes.get(name) {
1166            node.invalidate_cache();
1167        }
1168        if let Some(children) = self.dependents.get(name) {
1169            for child in children {
1170                self.invalidate_recursive(child, visited);
1171            }
1172        }
1173    }
1174
1175    fn bump_generation(&self) {
1176        let current = self.generation.get();
1177        self.generation.set(current.wrapping_add(1));
1178    }
1179}
1180
1181impl From<XmlModel> for NodeMap {
1182    fn from(model: XmlModel) -> Self {
1183        NodeMap::try_from_xml(model).expect("invalid GenApi model")
1184    }
1185}
1186
1187fn round_to_i64(name: &str, value: f64) -> Result<i64, GenApiError> {
1188    if !value.is_finite() {
1189        return Err(GenApiError::ExprEval {
1190            name: name.to_string(),
1191            msg: "non-finite result".into(),
1192        });
1193    }
1194    let rounded = round_ties_to_zero(value);
1195    if rounded < i64::MIN as f64 || rounded > i64::MAX as f64 {
1196        return Err(GenApiError::ExprEval {
1197            name: name.to_string(),
1198            msg: "result out of range".into(),
1199        });
1200    }
1201    let truncated = rounded.trunc();
1202    if (rounded - truncated).abs() > 1e-9 {
1203        return Err(GenApiError::ExprEval {
1204            name: name.to_string(),
1205            msg: "unable to represent integer".into(),
1206        });
1207    }
1208    Ok(truncated as i64)
1209}
1210
1211fn round_ties_to_zero(value: f64) -> f64 {
1212    if value >= 0.0 {
1213        let base = value.floor();
1214        let frac = value - base;
1215        if frac > 0.5 {
1216            base + 1.0
1217        } else {
1218            base
1219        }
1220    } else {
1221        let base = value.ceil();
1222        let frac = value - base;
1223        if frac < -0.5 {
1224            base - 1.0
1225        } else {
1226            base
1227        }
1228    }
1229}
1230
1231fn ensure_readable(access: &AccessMode, name: &str) -> Result<(), GenApiError> {
1232    if matches!(access, AccessMode::WO) {
1233        return Err(GenApiError::Access(name.to_string()));
1234    }
1235    Ok(())
1236}
1237
1238fn ensure_writable(access: &AccessMode, name: &str) -> Result<(), GenApiError> {
1239    if matches!(access, AccessMode::RO) {
1240        return Err(GenApiError::Access(name.to_string()));
1241    }
1242    Ok(())
1243}
1244
1245fn bytes_to_i64(name: &str, bytes: &[u8]) -> Result<i64, GenApiError> {
1246    if bytes.is_empty() {
1247        return Err(GenApiError::Parse(format!(
1248            "node {name} returned empty payload"
1249        )));
1250    }
1251    if bytes.len() > 8 {
1252        return Err(GenApiError::Parse(format!(
1253            "node {name} uses unsupported width {}",
1254            bytes.len()
1255        )));
1256    }
1257    let mut buf = [0u8; 8];
1258    let offset = 8 - bytes.len();
1259    buf[offset..].copy_from_slice(bytes);
1260    if !bytes.is_empty() && (bytes[0] & 0x80) != 0 {
1261        for byte in &mut buf[..offset] {
1262            *byte = 0xFF;
1263        }
1264    }
1265    Ok(i64::from_be_bytes(buf))
1266}
1267
1268fn i64_to_bytes(name: &str, value: i64, width: u32) -> Result<Vec<u8>, GenApiError> {
1269    if width == 0 || width > 8 {
1270        return Err(GenApiError::Parse(format!(
1271            "node {name} has unsupported width {width}"
1272        )));
1273    }
1274    let width = width as usize;
1275    let bytes = value.to_be_bytes();
1276    let data = bytes[8 - width..].to_vec();
1277    let roundtrip = bytes_to_i64(name, &data)?;
1278    if roundtrip != value {
1279        return Err(GenApiError::Range(format!(
1280            "value {value} does not fit {width} bytes for {name}"
1281        )));
1282    }
1283    Ok(data)
1284}
1285
1286fn interpret_bitfield_value(
1287    name: &str,
1288    raw: u64,
1289    bit_length: u16,
1290    signed: bool,
1291) -> Result<i64, GenApiError> {
1292    if signed {
1293        Ok(sign_extend(raw, bit_length))
1294    } else {
1295        i64::try_from(raw).map_err(|_| {
1296            GenApiError::Parse(format!(
1297                "bitfield value {raw} exceeds i64 range for node {name}"
1298            ))
1299        })
1300    }
1301}
1302
1303fn encode_bitfield_value(
1304    name: &str,
1305    value: i64,
1306    bit_length: u16,
1307    signed: bool,
1308) -> Result<u64, GenApiError> {
1309    if bit_length == 0 || bit_length > 64 {
1310        return Err(GenApiError::Parse(format!(
1311            "node {name} uses unsupported bitfield width {bit_length}"
1312        )));
1313    }
1314    if signed {
1315        let width = bit_length as u32;
1316        let min_allowed = -(1i128 << (width - 1));
1317        let max_allowed = (1i128 << (width - 1)) - 1;
1318        let value_i128 = value as i128;
1319        if value_i128 < min_allowed || value_i128 > max_allowed {
1320            return Err(GenApiError::ValueTooWide {
1321                name: name.to_string(),
1322                value,
1323                bit_length,
1324            });
1325        }
1326        let mask = mask_u128(bit_length) as i128;
1327        Ok((value_i128 & mask) as u64)
1328    } else {
1329        if value < 0 {
1330            return Err(GenApiError::ValueTooWide {
1331                name: name.to_string(),
1332                value,
1333                bit_length,
1334            });
1335        }
1336        let mask = mask_u128(bit_length);
1337        if (value as u128) > mask {
1338            return Err(GenApiError::ValueTooWide {
1339                name: name.to_string(),
1340                value,
1341                bit_length,
1342            });
1343        }
1344        Ok(value as u64)
1345    }
1346}
1347
1348fn mask_u128(bit_length: u16) -> u128 {
1349    if bit_length == 64 {
1350        u64::MAX as u128
1351    } else {
1352        (1u128 << bit_length) - 1
1353    }
1354}
1355
1356fn sign_extend(value: u64, bits: u16) -> i64 {
1357    let shift = 64 - bits as u32;
1358    ((value << shift) as i64) >> shift
1359}
1360
1361fn map_bitops_error(name: &str, err: BitOpsError) -> GenApiError {
1362    match err {
1363        BitOpsError::UnsupportedWidth { len } => {
1364            GenApiError::Parse(format!("node {name} uses unsupported register width {len}"))
1365        }
1366        BitOpsError::UnsupportedLength { bit_length } => GenApiError::Parse(format!(
1367            "node {name} uses unsupported bitfield length {bit_length}"
1368        )),
1369        BitOpsError::OutOfRange {
1370            len,
1371            bit_offset,
1372            bit_length,
1373        } => GenApiError::BitfieldOutOfRange {
1374            name: name.to_string(),
1375            bit_offset,
1376            bit_length,
1377            len,
1378        },
1379        BitOpsError::ValueTooWide { bit_length, value } => GenApiError::ValueTooWide {
1380            name: name.to_string(),
1381            value: i64::try_from(value).unwrap_or(i64::MAX),
1382            bit_length,
1383        },
1384    }
1385}
1386
1387fn apply_scale(node: &FloatNode, raw: f64) -> f64 {
1388    let mut value = raw;
1389    if let Some((num, den)) = node.scale {
1390        value *= num as f64 / den as f64;
1391    }
1392    if let Some(offset) = node.offset {
1393        value += offset;
1394    }
1395    value
1396}
1397
1398fn encode_float(node: &FloatNode, value: f64) -> Result<i64, GenApiError> {
1399    let mut raw = value;
1400    if let Some(offset) = node.offset {
1401        raw -= offset;
1402    }
1403    if let Some((num, den)) = node.scale {
1404        if num == 0 {
1405            return Err(GenApiError::Parse(format!(
1406                "node {} has zero scale numerator",
1407                node.name
1408            )));
1409        }
1410        raw *= den as f64 / num as f64;
1411    }
1412    let rounded = raw.round();
1413    if (raw - rounded).abs() > 1e-6 {
1414        return Err(GenApiError::Range(node.name.clone()));
1415    }
1416    let raw_i64 = rounded as i64;
1417    Ok(raw_i64)
1418}
1419
1420#[cfg(test)]
1421mod tests {
1422    use super::*;
1423
1424    const FIXTURE: &str = r#"
1425        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
1426            <Integer Name="Width">
1427                <Address>0x100</Address>
1428                <Length>4</Length>
1429                <AccessMode>RW</AccessMode>
1430                <Min>16</Min>
1431                <Max>4096</Max>
1432                <Inc>2</Inc>
1433            </Integer>
1434            <Float Name="ExposureTime">
1435                <Address>0x200</Address>
1436                <Length>4</Length>
1437                <AccessMode>RW</AccessMode>
1438                <Min>10.0</Min>
1439                <Max>100000.0</Max>
1440                <Scale>1/1000</Scale>
1441            </Float>
1442            <Enumeration Name="GainSelector">
1443                <Address>0x300</Address>
1444                <Length>2</Length>
1445                <AccessMode>RW</AccessMode>
1446                <EnumEntry Name="All" Value="0" />
1447                <EnumEntry Name="Red" Value="1" />
1448                <EnumEntry Name="Blue" Value="2" />
1449            </Enumeration>
1450            <Integer Name="Gain">
1451                <Length>2</Length>
1452                <AccessMode>RW</AccessMode>
1453                <Min>0</Min>
1454                <Max>48</Max>
1455                <pSelected>GainSelector</pSelected>
1456                <Selected>All</Selected>
1457                <Address>0x310</Address>
1458                <Selected>Red</Selected>
1459                <Address>0x314</Address>
1460                <Selected>Blue</Selected>
1461            </Integer>
1462            <Boolean Name="GammaEnable">
1463                <Address>0x400</Address>
1464                <Length>1</Length>
1465                <AccessMode>RW</AccessMode>
1466            </Boolean>
1467            <Command Name="AcquisitionStart">
1468                <Address>0x500</Address>
1469                <Length>4</Length>
1470            </Command>
1471        </RegisterDescription>
1472    "#;
1473
1474    const INDIRECT_FIXTURE: &str = r#"
1475        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1476            <Integer Name="RegAddr">
1477                <Address>0x2000</Address>
1478                <Length>4</Length>
1479                <AccessMode>RW</AccessMode>
1480                <Min>0</Min>
1481                <Max>65535</Max>
1482            </Integer>
1483            <Integer Name="Gain">
1484                <pAddress>RegAddr</pAddress>
1485                <Length>4</Length>
1486                <AccessMode>RW</AccessMode>
1487                <Min>0</Min>
1488                <Max>255</Max>
1489            </Integer>
1490        </RegisterDescription>
1491    "#;
1492
1493    const ENUM_PVALUE_FIXTURE: &str = r#"
1494        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1495            <Enumeration Name="Mode">
1496                <Address>0x4000</Address>
1497                <Length>4</Length>
1498                <AccessMode>RW</AccessMode>
1499                <EnumEntry Name="Fixed10">
1500                    <Value>10</Value>
1501                </EnumEntry>
1502                <EnumEntry Name="DynFromReg">
1503                    <pValue>RegModeVal</pValue>
1504                </EnumEntry>
1505            </Enumeration>
1506            <Integer Name="RegModeVal">
1507                <Address>0x4100</Address>
1508                <Length>4</Length>
1509                <AccessMode>RW</AccessMode>
1510                <Min>0</Min>
1511                <Max>65535</Max>
1512            </Integer>
1513        </RegisterDescription>
1514    "#;
1515
1516    const BITFIELD_FIXTURE: &str = r#"
1517        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1518            <Integer Name="LeByte">
1519                <Address>0x5000</Address>
1520                <Length>4</Length>
1521                <AccessMode>RW</AccessMode>
1522                <Min>0</Min>
1523                <Max>65535</Max>
1524                <Mask>0x0000FF00</Mask>
1525            </Integer>
1526            <Integer Name="BeBits">
1527                <Address>0x5004</Address>
1528                <Length>2</Length>
1529                <AccessMode>RW</AccessMode>
1530                <Min>0</Min>
1531                <Max>15</Max>
1532                <Lsb>13</Lsb>
1533                <Msb>15</Msb>
1534                <Endianness>BigEndian</Endianness>
1535            </Integer>
1536            <Boolean Name="PackedFlag">
1537                <Address>0x5006</Address>
1538                <Length>4</Length>
1539                <AccessMode>RW</AccessMode>
1540                <Bit>13</Bit>
1541            </Boolean>
1542        </RegisterDescription>
1543    "#;
1544
1545    const SWISSKNIFE_FIXTURE: &str = r#"
1546        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1547            <Integer Name="GainRaw">
1548                <Address>0x3000</Address>
1549                <Length>4</Length>
1550                <AccessMode>RW</AccessMode>
1551                <Min>0</Min>
1552                <Max>1000</Max>
1553            </Integer>
1554            <Float Name="Offset">
1555                <Address>0x3008</Address>
1556                <Length>4</Length>
1557                <AccessMode>RW</AccessMode>
1558                <Min>-100.0</Min>
1559                <Max>100.0</Max>
1560            </Float>
1561            <Integer Name="B">
1562                <Address>0x3010</Address>
1563                <Length>4</Length>
1564                <AccessMode>RW</AccessMode>
1565                <Min>-1000</Min>
1566                <Max>1000</Max>
1567            </Integer>
1568            <SwissKnife Name="ComputedGain">
1569                <Expression>(GainRaw * 0.5) + Offset</Expression>
1570                <pVariable Name="GainRaw">GainRaw</pVariable>
1571                <pVariable Name="Offset">Offset</pVariable>
1572                <Output>Float</Output>
1573            </SwissKnife>
1574            <SwissKnife Name="DivideInt">
1575                <Expression>GainRaw / 3</Expression>
1576                <pVariable Name="GainRaw">GainRaw</pVariable>
1577                <Output>Integer</Output>
1578            </SwissKnife>
1579            <SwissKnife Name="Unary">
1580                <Expression>-GainRaw + 10</Expression>
1581                <pVariable Name="GainRaw">GainRaw</pVariable>
1582                <Output>Integer</Output>
1583            </SwissKnife>
1584            <SwissKnife Name="DivideByZero">
1585                <Expression>GainRaw / B</Expression>
1586                <pVariable Name="GainRaw">GainRaw</pVariable>
1587                <pVariable Name="B">B</pVariable>
1588                <Output>Float</Output>
1589            </SwissKnife>
1590        </RegisterDescription>
1591    "#;
1592
1593    #[derive(Default)]
1594    struct MockIo {
1595        regs: RefCell<HashMap<u64, Vec<u8>>>,
1596        reads: RefCell<HashMap<u64, usize>>,
1597    }
1598
1599    impl MockIo {
1600        fn with_registers(entries: &[(u64, Vec<u8>)]) -> Self {
1601            let mut regs = HashMap::new();
1602            for (addr, data) in entries {
1603                regs.insert(*addr, data.clone());
1604            }
1605            MockIo {
1606                regs: RefCell::new(regs),
1607                reads: RefCell::new(HashMap::new()),
1608            }
1609        }
1610
1611        fn read_count(&self, addr: u64) -> usize {
1612            *self.reads.borrow().get(&addr).unwrap_or(&0)
1613        }
1614    }
1615
1616    impl RegisterIo for MockIo {
1617        fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
1618            let mut reads = self.reads.borrow_mut();
1619            *reads.entry(addr).or_default() += 1;
1620            let regs = self.regs.borrow();
1621            let data = regs
1622                .get(&addr)
1623                .ok_or_else(|| GenApiError::Io(format!("read miss at 0x{addr:08X}")))?;
1624            if data.len() != len {
1625                return Err(GenApiError::Io(format!(
1626                    "length mismatch at 0x{addr:08X}: expected {len}, have {}",
1627                    data.len()
1628                )));
1629            }
1630            Ok(data.clone())
1631        }
1632
1633        fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
1634            self.regs.borrow_mut().insert(addr, data.to_vec());
1635            Ok(())
1636        }
1637    }
1638
1639    fn build_nodemap() -> NodeMap {
1640        let model = genapi_xml::parse(FIXTURE).expect("parse fixture");
1641        NodeMap::from(model)
1642    }
1643
1644    fn build_indirect_nodemap() -> NodeMap {
1645        let model = genapi_xml::parse(INDIRECT_FIXTURE).expect("parse indirect fixture");
1646        NodeMap::from(model)
1647    }
1648
1649    fn build_enum_pvalue_nodemap() -> NodeMap {
1650        let model = genapi_xml::parse(ENUM_PVALUE_FIXTURE).expect("parse enum pvalue fixture");
1651        NodeMap::from(model)
1652    }
1653
1654    fn build_bitfield_nodemap() -> NodeMap {
1655        let model = genapi_xml::parse(BITFIELD_FIXTURE).expect("parse bitfield fixture");
1656        NodeMap::from(model)
1657    }
1658
1659    fn build_swissknife_nodemap() -> NodeMap {
1660        let model = genapi_xml::parse(SWISSKNIFE_FIXTURE).expect("parse swissknife fixture");
1661        NodeMap::from(model)
1662    }
1663
1664    #[test]
1665    fn integer_roundtrip_and_cache() {
1666        let mut nodemap = build_nodemap();
1667        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 4, 0])]);
1668        let width = nodemap.get_integer("Width", &io).expect("read width");
1669        assert_eq!(width, 1024);
1670        assert_eq!(io.read_count(0x100), 1);
1671        let width_again = nodemap.get_integer("Width", &io).expect("cached width");
1672        assert_eq!(width_again, 1024);
1673        assert_eq!(io.read_count(0x100), 1, "cached value should be reused");
1674        nodemap
1675            .set_integer("Width", 1030, &io)
1676            .expect("write width");
1677        let width = nodemap
1678            .get_integer("Width", &io)
1679            .expect("read updated width");
1680        assert_eq!(width, 1030);
1681        assert_eq!(io.read_count(0x100), 1, "write should update cache");
1682    }
1683
1684    #[test]
1685    fn float_conversion_roundtrip() {
1686        let mut nodemap = build_nodemap();
1687        let raw = 50_000i64; // 50 ms with 1/1000 scale
1688        let io = MockIo::with_registers(&[(0x200, i64_to_bytes("ExposureTime", raw, 4).unwrap())]);
1689        let exposure = nodemap
1690            .get_float("ExposureTime", &io)
1691            .expect("read exposure");
1692        assert!((exposure - 50.0).abs() < 1e-6);
1693        nodemap
1694            .set_float("ExposureTime", 75.0, &io)
1695            .expect("write exposure");
1696        let raw_back = bytes_to_i64("ExposureTime", &io.read(0x200, 4).unwrap()).unwrap();
1697        assert_eq!(raw_back, 75_000);
1698    }
1699
1700    #[test]
1701    fn selector_address_switching() {
1702        let mut nodemap = build_nodemap();
1703        let io = MockIo::with_registers(&[
1704            (0x300, i64_to_bytes("GainSelector", 0, 2).unwrap()),
1705            (0x310, i64_to_bytes("Gain", 10, 2).unwrap()),
1706            (0x314, i64_to_bytes("Gain", 24, 2).unwrap()),
1707        ]);
1708
1709        let gain_all = nodemap.get_integer("Gain", &io).expect("gain for All");
1710        assert_eq!(gain_all, 10);
1711        assert_eq!(io.read_count(0x310), 1);
1712        assert_eq!(io.read_count(0x314), 0);
1713
1714        io.write(0x314, &i64_to_bytes("Gain", 32, 2).unwrap())
1715            .expect("update red gain");
1716        nodemap
1717            .set_enum("GainSelector", "Red", &io)
1718            .expect("set selector to red");
1719        let gain_red = nodemap.get_integer("Gain", &io).expect("gain for Red");
1720        assert_eq!(gain_red, 32);
1721        assert_eq!(
1722            io.read_count(0x310),
1723            1,
1724            "previous address should not be reread"
1725        );
1726        assert_eq!(io.read_count(0x314), 1);
1727
1728        let gain_red_cached = nodemap.get_integer("Gain", &io).expect("cached red");
1729        assert_eq!(gain_red_cached, 32);
1730        assert_eq!(io.read_count(0x314), 1, "selector cache should be reused");
1731
1732        nodemap
1733            .set_enum("GainSelector", "Blue", &io)
1734            .expect("set selector to blue");
1735        let err = nodemap.get_integer("Gain", &io).unwrap_err();
1736        match err {
1737            GenApiError::Unavailable(msg) => {
1738                assert!(msg.contains("GainSelector=Blue"));
1739            }
1740            other => panic!("unexpected error: {other:?}"),
1741        }
1742        assert_eq!(
1743            io.read_count(0x314),
1744            1,
1745            "no read expected for missing mapping"
1746        );
1747
1748        io.write(0x310, &i64_to_bytes("Gain", 12, 2).unwrap())
1749            .expect("update all gain");
1750        nodemap
1751            .set_enum("GainSelector", "All", &io)
1752            .expect("restore selector to all");
1753        let gain_all_updated = nodemap
1754            .get_integer("Gain", &io)
1755            .expect("gain for All again");
1756        assert_eq!(gain_all_updated, 12);
1757        assert_eq!(
1758            io.read_count(0x310),
1759            2,
1760            "address switch should invalidate cache"
1761        );
1762    }
1763
1764    #[test]
1765    fn range_enforcement() {
1766        let mut nodemap = build_nodemap();
1767        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 0, 16])]);
1768        let err = nodemap.set_integer("Width", 17, &io).unwrap_err();
1769        assert!(matches!(err, GenApiError::Range(_)));
1770    }
1771
1772    #[test]
1773    fn command_exec() {
1774        let mut nodemap = build_nodemap();
1775        let io = MockIo::with_registers(&[]);
1776        nodemap
1777            .exec_command("AcquisitionStart", &io)
1778            .expect("exec command");
1779        let payload = io.read(0x500, 4).expect("command write");
1780        assert_eq!(payload, vec![0, 0, 0, 1]);
1781    }
1782
1783    #[test]
1784    fn indirect_address_resolution() {
1785        let mut nodemap = build_indirect_nodemap();
1786        let io = MockIo::with_registers(&[
1787            (0x2000, i64_to_bytes("RegAddr", 0x3000, 4).unwrap()),
1788            (0x3000, i64_to_bytes("Gain", 123, 4).unwrap()),
1789            (0x3100, i64_to_bytes("Gain", 77, 4).unwrap()),
1790        ]);
1791
1792        let initial = nodemap.get_integer("Gain", &io).expect("read gain");
1793        assert_eq!(initial, 123);
1794        assert_eq!(io.read_count(0x2000), 1);
1795        assert_eq!(io.read_count(0x3000), 1);
1796
1797        nodemap
1798            .set_integer("RegAddr", 0x3100, &io)
1799            .expect("set indirect address");
1800        let updated = nodemap
1801            .get_integer("Gain", &io)
1802            .expect("read gain after change");
1803        assert_eq!(updated, 77);
1804        assert_eq!(io.read_count(0x2000), 1);
1805        assert_eq!(io.read_count(0x3000), 1);
1806        assert_eq!(io.read_count(0x3100), 1);
1807    }
1808
1809    #[test]
1810    fn indirect_bad_address() {
1811        let mut nodemap = build_indirect_nodemap();
1812        let io = MockIo::with_registers(&[(0x2000, vec![0, 0, 0, 0])]);
1813
1814        nodemap
1815            .set_integer("RegAddr", 0, &io)
1816            .expect("write zero address");
1817        let err = nodemap.get_integer("Gain", &io).unwrap_err();
1818        match err {
1819            GenApiError::BadIndirectAddress { name, addr } => {
1820                assert_eq!(name, "Gain");
1821                assert_eq!(addr, 0);
1822            }
1823            other => panic!("unexpected error: {other:?}"),
1824        }
1825        assert_eq!(io.read_count(0x2000), 0);
1826    }
1827
1828    #[test]
1829    fn enum_literal_entry_read() {
1830        let nodemap = build_enum_pvalue_nodemap();
1831        let io = MockIo::with_registers(&[
1832            (0x4000, i64_to_bytes("Mode", 10, 4).unwrap()),
1833            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
1834        ]);
1835
1836        let value = nodemap.get_enum("Mode", &io).expect("read mode");
1837        assert_eq!(value, "Fixed10");
1838        assert_eq!(
1839            io.read_count(0x4100),
1840            1,
1841            "provider should be read once for mapping"
1842        );
1843    }
1844
1845    #[test]
1846    fn enum_provider_entry_read() {
1847        let nodemap = build_enum_pvalue_nodemap();
1848        let io = MockIo::with_registers(&[
1849            (0x4000, i64_to_bytes("Mode", 42, 4).unwrap()),
1850            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
1851        ]);
1852
1853        let value = nodemap.get_enum("Mode", &io).expect("read dynamic mode");
1854        assert_eq!(value, "DynFromReg");
1855        assert_eq!(io.read_count(0x4100), 1);
1856    }
1857
1858    #[test]
1859    fn enum_set_uses_provider_value() {
1860        let mut nodemap = build_enum_pvalue_nodemap();
1861        let io = MockIo::with_registers(&[
1862            (0x4000, i64_to_bytes("Mode", 0, 4).unwrap()),
1863            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
1864        ]);
1865
1866        nodemap
1867            .set_enum("Mode", "DynFromReg", &io)
1868            .expect("write enum");
1869        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap()).unwrap();
1870        assert_eq!(raw, 42);
1871        assert_eq!(io.read_count(0x4100), 1);
1872    }
1873
1874    #[test]
1875    fn enum_provider_update_invalidates_mapping() {
1876        let mut nodemap = build_enum_pvalue_nodemap();
1877        let io = MockIo::with_registers(&[
1878            (0x4000, i64_to_bytes("Mode", 42, 4).unwrap()),
1879            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
1880        ]);
1881
1882        assert_eq!(nodemap.get_enum("Mode", &io).unwrap(), "DynFromReg");
1883        assert_eq!(io.read_count(0x4100), 1);
1884
1885        nodemap
1886            .set_integer("RegModeVal", 17, &io)
1887            .expect("update provider");
1888        io.write(0x4000, &i64_to_bytes("Mode", 0, 4).unwrap())
1889            .expect("reset mode register");
1890
1891        nodemap
1892            .set_enum("Mode", "DynFromReg", &io)
1893            .expect("write enum after provider change");
1894        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap()).unwrap();
1895        assert_eq!(raw, 17);
1896    }
1897
1898    #[test]
1899    fn enum_unknown_value_error() {
1900        let nodemap = build_enum_pvalue_nodemap();
1901        let io = MockIo::with_registers(&[
1902            (0x4000, i64_to_bytes("Mode", 99, 4).unwrap()),
1903            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
1904        ]);
1905
1906        let err = nodemap.get_enum("Mode", &io).unwrap_err();
1907        match err {
1908            GenApiError::EnumValueUnknown { node, value } => {
1909                assert_eq!(node, "Mode");
1910                assert_eq!(value, 99);
1911            }
1912            other => panic!("unexpected error: {other:?}"),
1913        }
1914    }
1915
1916    #[test]
1917    fn enum_entries_are_sorted() {
1918        let nodemap = build_enum_pvalue_nodemap();
1919        let entries = nodemap.enum_entries("Mode").expect("entries");
1920        assert_eq!(
1921            entries,
1922            vec!["DynFromReg".to_string(), "Fixed10".to_string()]
1923        );
1924    }
1925
1926    #[test]
1927    fn bitfield_le_integer_roundtrip() {
1928        let mut nodemap = build_bitfield_nodemap();
1929        let io = MockIo::with_registers(&[(0x5000, vec![0xAA, 0xBB, 0xCC, 0xDD])]);
1930
1931        let value = nodemap
1932            .get_integer("LeByte", &io)
1933            .expect("read little-endian field");
1934        assert_eq!(value, 0xBB);
1935
1936        nodemap
1937            .set_integer("LeByte", 0x55, &io)
1938            .expect("write little-endian field");
1939        let data = io.read(0x5000, 4).expect("read back register");
1940        assert_eq!(data, vec![0xAA, 0x55, 0xCC, 0xDD]);
1941    }
1942
1943    #[test]
1944    fn bitfield_be_integer_roundtrip() {
1945        let mut nodemap = build_bitfield_nodemap();
1946        let io = MockIo::with_registers(&[(0x5004, vec![0b1010_0000, 0b0000_0000])]);
1947
1948        let value = nodemap
1949            .get_integer("BeBits", &io)
1950            .expect("read big-endian bits");
1951        assert_eq!(value, 0b101);
1952
1953        nodemap
1954            .set_integer("BeBits", 0b010, &io)
1955            .expect("write big-endian bits");
1956        let data = io.read(0x5004, 2).expect("read back register");
1957        assert_eq!(data, vec![0b0100_0000, 0b0000_0000]);
1958    }
1959
1960    #[test]
1961    fn bitfield_boolean_toggle() {
1962        let mut nodemap = build_bitfield_nodemap();
1963        let io = MockIo::with_registers(&[(0x5006, vec![0x00, 0x20, 0x00, 0x00])]);
1964
1965        assert!(nodemap.get_bool("PackedFlag", &io).expect("read flag"));
1966
1967        nodemap
1968            .set_bool("PackedFlag", false, &io)
1969            .expect("clear flag");
1970        let data = io.read(0x5006, 4).expect("read cleared");
1971        assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00]);
1972
1973        nodemap.set_bool("PackedFlag", true, &io).expect("set flag");
1974        let data = io.read(0x5006, 4).expect("read set");
1975        assert_eq!(data, vec![0x00, 0x20, 0x00, 0x00]);
1976    }
1977
1978    #[test]
1979    fn bitfield_value_too_wide() {
1980        let mut nodemap = build_bitfield_nodemap();
1981        let io = MockIo::with_registers(&[(0x5004, vec![0x00, 0x00])]);
1982
1983        let err = nodemap
1984            .set_integer("BeBits", 8, &io)
1985            .expect_err("value too wide");
1986        match err {
1987            GenApiError::ValueTooWide {
1988                name, bit_length, ..
1989            } => {
1990                assert_eq!(name, "BeBits");
1991                assert_eq!(bit_length, 3);
1992            }
1993            other => panic!("unexpected error: {other:?}"),
1994        }
1995    }
1996    #[test]
1997    fn swissknife_evaluates_and_invalidates() {
1998        let mut nodemap = build_swissknife_nodemap();
1999        let io = MockIo::with_registers(&[
2000            (0x3000, i64_to_bytes("GainRaw", 100, 4).unwrap()),
2001            (0x3008, i64_to_bytes("Offset", 3, 4).unwrap()),
2002            (0x3010, i64_to_bytes("B", 1, 4).unwrap()),
2003        ]);
2004
2005        let value = nodemap
2006            .get_float("ComputedGain", &io)
2007            .expect("compute gain");
2008        assert!((value - 53.0).abs() < 1e-6);
2009
2010        nodemap
2011            .set_integer("GainRaw", 120, &io)
2012            .expect("update raw gain");
2013        let updated = nodemap
2014            .get_float("ComputedGain", &io)
2015            .expect("recompute gain");
2016        assert!((updated - 63.0).abs() < 1e-6);
2017    }
2018
2019    #[test]
2020    fn swissknife_integer_rounding_and_unary() {
2021        let mut nodemap = build_swissknife_nodemap();
2022        let io = MockIo::with_registers(&[
2023            (0x3000, i64_to_bytes("GainRaw", 5, 4).unwrap()),
2024            (0x3008, i64_to_bytes("Offset", 0, 4).unwrap()),
2025            (0x3010, i64_to_bytes("B", 1, 4).unwrap()),
2026        ]);
2027
2028        let divided = nodemap
2029            .get_integer("DivideInt", &io)
2030            .expect("integer division");
2031        assert_eq!(divided, 2);
2032
2033        nodemap
2034            .set_integer("GainRaw", 3, &io)
2035            .expect("update gain raw");
2036        let unary = nodemap.get_integer("Unary", &io).expect("unary expression");
2037        assert_eq!(unary, 7);
2038    }
2039
2040    #[test]
2041    fn swissknife_unknown_variable_error() {
2042        const XML: &str = r#"
2043            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
2044                <Integer Name="A">
2045                    <Address>0x2000</Address>
2046                    <Length>4</Length>
2047                    <AccessMode>RW</AccessMode>
2048                    <Min>0</Min>
2049                    <Max>100</Max>
2050                </Integer>
2051                <SwissKnife Name="Bad">
2052                    <Expression>A + Missing</Expression>
2053                    <pVariable Name="A">A</pVariable>
2054                </SwissKnife>
2055            </RegisterDescription>
2056        "#;
2057
2058        let model = genapi_xml::parse(XML).expect("parse invalid swissknife");
2059        let err = NodeMap::try_from_xml(model).expect_err("unknown variable");
2060        match err {
2061            GenApiError::UnknownVariable { name, var } => {
2062                assert_eq!(name, "Bad");
2063                assert_eq!(var, "Missing");
2064            }
2065            other => panic!("unexpected error: {other:?}"),
2066        }
2067    }
2068
2069    #[test]
2070    fn swissknife_division_by_zero() {
2071        let nodemap = build_swissknife_nodemap();
2072        let io = MockIo::with_registers(&[
2073            (0x3000, i64_to_bytes("GainRaw", 10, 4).unwrap()),
2074            (0x3008, i64_to_bytes("Offset", 0, 4).unwrap()),
2075            (0x3010, i64_to_bytes("B", 0, 4).unwrap()),
2076        ]);
2077
2078        let err = nodemap
2079            .get_float("DivideByZero", &io)
2080            .expect_err("division by zero");
2081        match err {
2082            GenApiError::ExprEval { name, msg } => {
2083                assert_eq!(name, "DivideByZero");
2084                assert_eq!(msg, "division by zero");
2085            }
2086            other => panic!("unexpected error: {other:?}"),
2087        }
2088    }
2089}