Skip to main content

viva_genapi/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! GenApi node system: typed feature access backed by register IO.
3
4mod bitops;
5mod conversions;
6mod error;
7mod io;
8mod nodemap;
9mod nodes;
10pub mod swissknife;
11
12pub use error::GenApiError;
13pub use io::{NullIo, RegisterIo};
14pub use nodemap::NodeMap;
15pub use nodes::{
16    BooleanNode, CategoryNode, CommandNode, EnumNode, FloatNode, IntegerNode, Node, NodeMeta,
17    RegisterNode, Representation, SkNode, Visibility,
18};
19pub use swissknife::{AstNode, EvalMode, Value};
20pub use viva_genapi_xml::{AccessMode, SkOutput, SkippedNode};
21
22#[cfg(test)]
23mod tests {
24    use std::cell::RefCell;
25    use std::collections::HashMap;
26
27    use crate::conversions::{bytes_to_i64, i64_to_bytes};
28    use crate::{AccessMode, GenApiError, NodeMap, RegisterIo, Visibility};
29    use viva_genapi_xml::Sign;
30
31    const FIXTURE: &str = r#"
32        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
33            <Integer Name="Width">
34                <Address>0x100</Address>
35                <Length>4</Length>
36                <AccessMode>RW</AccessMode>
37                <Min>16</Min>
38                <Max>4096</Max>
39                <Inc>2</Inc>
40            </Integer>
41            <Float Name="ExposureTime">
42                <Address>0x200</Address>
43                <Length>4</Length>
44                <AccessMode>RW</AccessMode>
45                <Min>10.0</Min>
46                <Max>100000.0</Max>
47                <Scale>1/1000</Scale>
48            </Float>
49            <Enumeration Name="GainSelector">
50                <Address>0x300</Address>
51                <Length>2</Length>
52                <AccessMode>RW</AccessMode>
53                <EnumEntry Name="All" Value="0" />
54                <EnumEntry Name="Red" Value="1" />
55                <EnumEntry Name="Blue" Value="2" />
56            </Enumeration>
57            <Integer Name="Gain">
58                <Length>2</Length>
59                <AccessMode>RW</AccessMode>
60                <Min>0</Min>
61                <Max>48</Max>
62                <pSelected>GainSelector</pSelected>
63                <Selected>All</Selected>
64                <Address>0x310</Address>
65                <Selected>Red</Selected>
66                <Address>0x314</Address>
67                <Selected>Blue</Selected>
68            </Integer>
69            <Boolean Name="GammaEnable">
70                <Address>0x400</Address>
71                <Length>1</Length>
72                <AccessMode>RW</AccessMode>
73            </Boolean>
74            <Command Name="AcquisitionStart">
75                <Address>0x500</Address>
76                <Length>4</Length>
77            </Command>
78        </RegisterDescription>
79    "#;
80
81    const INDIRECT_FIXTURE: &str = r#"
82        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
83            <Integer Name="RegAddr">
84                <Address>0x2000</Address>
85                <Length>4</Length>
86                <AccessMode>RW</AccessMode>
87                <Min>0</Min>
88                <Max>65535</Max>
89            </Integer>
90            <Integer Name="Gain">
91                <pAddress>RegAddr</pAddress>
92                <Length>4</Length>
93                <AccessMode>RW</AccessMode>
94                <Min>0</Min>
95                <Max>255</Max>
96            </Integer>
97        </RegisterDescription>
98    "#;
99
100    const ENUM_PVALUE_FIXTURE: &str = r#"
101        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
102            <Enumeration Name="Mode">
103                <Address>0x4000</Address>
104                <Length>4</Length>
105                <AccessMode>RW</AccessMode>
106                <EnumEntry Name="Fixed10">
107                    <Value>10</Value>
108                </EnumEntry>
109                <EnumEntry Name="DynFromReg">
110                    <pValue>RegModeVal</pValue>
111                </EnumEntry>
112            </Enumeration>
113            <Integer Name="RegModeVal">
114                <Address>0x4100</Address>
115                <Length>4</Length>
116                <AccessMode>RW</AccessMode>
117                <Min>0</Min>
118                <Max>65535</Max>
119            </Integer>
120        </RegisterDescription>
121    "#;
122
123    const BITFIELD_FIXTURE: &str = r#"
124        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
125            <Integer Name="LeByte">
126                <Address>0x5000</Address>
127                <Length>4</Length>
128                <AccessMode>RW</AccessMode>
129                <Min>0</Min>
130                <Max>65535</Max>
131                <Mask>0x0000FF00</Mask>
132            </Integer>
133            <Integer Name="BeBits">
134                <Address>0x5004</Address>
135                <Length>2</Length>
136                <AccessMode>RW</AccessMode>
137                <Min>0</Min>
138                <Max>15</Max>
139                <Lsb>13</Lsb>
140                <Msb>15</Msb>
141                <Endianness>BigEndian</Endianness>
142            </Integer>
143            <Boolean Name="PackedFlag">
144                <Address>0x5006</Address>
145                <Length>4</Length>
146                <AccessMode>RW</AccessMode>
147                <Bit>13</Bit>
148            </Boolean>
149        </RegisterDescription>
150    "#;
151
152    const SWISSKNIFE_FIXTURE: &str = r#"
153        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
154            <Integer Name="GainRaw">
155                <Address>0x3000</Address>
156                <Length>4</Length>
157                <AccessMode>RW</AccessMode>
158                <Min>0</Min>
159                <Max>1000</Max>
160            </Integer>
161            <Float Name="Offset">
162                <Address>0x3008</Address>
163                <Length>4</Length>
164                <AccessMode>RW</AccessMode>
165                <Min>-100.0</Min>
166                <Max>100.0</Max>
167                <Scale>1</Scale>
168            </Float>
169            <Integer Name="B">
170                <Address>0x3010</Address>
171                <Length>4</Length>
172                <AccessMode>RW</AccessMode>
173                <Min>-1000</Min>
174                <Max>1000</Max>
175            </Integer>
176            <SwissKnife Name="ComputedGain">
177                <Formula>(GainRaw * 0.5) + Offset</Formula>
178                <pVariable Name="GainRaw">GainRaw</pVariable>
179                <pVariable Name="Offset">Offset</pVariable>
180            </SwissKnife>
181            <IntSwissKnife Name="DivideInt">
182                <Formula>GainRaw / 3</Formula>
183                <pVariable Name="GainRaw">GainRaw</pVariable>
184            </IntSwissKnife>
185            <IntSwissKnife Name="Unary">
186                <Formula>-GainRaw + 10</Formula>
187                <pVariable Name="GainRaw">GainRaw</pVariable>
188            </IntSwissKnife>
189            <SwissKnife Name="DivideByZero">
190                <Formula>GainRaw / B</Formula>
191                <pVariable Name="GainRaw">GainRaw</pVariable>
192                <pVariable Name="B">B</pVariable>
193            </SwissKnife>
194        </RegisterDescription>
195    "#;
196
197    #[derive(Default)]
198    struct MockIo {
199        regs: RefCell<HashMap<u64, Vec<u8>>>,
200        reads: RefCell<HashMap<u64, usize>>,
201    }
202
203    impl MockIo {
204        fn with_registers(entries: &[(u64, Vec<u8>)]) -> Self {
205            let mut regs = HashMap::new();
206            for (addr, data) in entries {
207                regs.insert(*addr, data.clone());
208            }
209            MockIo {
210                regs: RefCell::new(regs),
211                reads: RefCell::new(HashMap::new()),
212            }
213        }
214
215        fn read_count(&self, addr: u64) -> usize {
216            *self.reads.borrow().get(&addr).unwrap_or(&0)
217        }
218    }
219
220    impl RegisterIo for MockIo {
221        fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
222            let mut reads = self.reads.borrow_mut();
223            *reads.entry(addr).or_default() += 1;
224            let regs = self.regs.borrow();
225            let data = regs
226                .get(&addr)
227                .ok_or_else(|| GenApiError::Io(format!("read miss at 0x{addr:08X}")))?;
228            if data.len() != len {
229                return Err(GenApiError::Io(format!(
230                    "length mismatch at 0x{addr:08X}: expected {len}, have {}",
231                    data.len()
232                )));
233            }
234            Ok(data.clone())
235        }
236
237        fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
238            self.regs.borrow_mut().insert(addr, data.to_vec());
239            Ok(())
240        }
241    }
242
243    fn build_nodemap() -> NodeMap {
244        let model = viva_genapi_xml::parse(FIXTURE).expect("parse fixture");
245        NodeMap::try_from_xml(model).expect("build nodemap")
246    }
247
248    /// A node type we do not implement must reach the nodemap's skip list.
249    ///
250    /// Two separate holes used to swallow this. The XML parser dropped an
251    /// unlisted tag at `skip_element` without recording it, and the nodemap
252    /// then discarded whatever the XML layer *had* recorded — so a consumer
253    /// holding a nodemap could not tell a feature we cannot read from one the
254    /// camera does not have.
255    #[test]
256    fn an_unsupported_node_type_is_visible_in_the_nodemap() {
257        // `<ConfRom>` is a real GenICam node type we do not implement.
258        // This test used `<Register>` until GA-09 landed support for it.
259        const WITH_UNKNOWN: &str = r#"
260            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">
261                <Integer Name="Width">
262                    <Address>0x100</Address>
263                    <Length>4</Length>
264                    <AccessMode>RW</AccessMode>
265                </Integer>
266                <ConfRom Name="DeviceConfRom">
267                    <Address>0x2000</Address>
268                    <Length>512</Length>
269                </ConfRom>
270            </RegisterDescription>
271        "#;
272        let model = viva_genapi_xml::parse(WITH_UNKNOWN).expect("parse");
273        assert_eq!(model.skipped.len(), 1, "XML layer records the unknown tag");
274
275        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
276        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 0, 7])]);
277        assert_eq!(nodemap.get_integer("Width", &io).expect("read Width"), 7);
278
279        let skipped = nodemap.skipped();
280        assert_eq!(skipped.len(), 1, "and the nodemap carries it forward");
281        assert_eq!(skipped[0].tag, "ConfRom");
282        assert_eq!(skipped[0].name.as_deref(), Some("DeviceConfRom"));
283    }
284
285    /// `<Register>` fixture covering the three shapes GA-09's first cut cares
286    /// about: a readable/writable block on the device port, one on a chunk
287    /// port, and one whose length is resolved at runtime.
288    const REGISTER_FIXTURE: &str = r#"
289        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">
290            <Register Name="FileAccessBuffer">
291                <Address>0x2000</Address>
292                <Length>8</Length>
293                <AccessMode>RW</AccessMode>
294            </Register>
295            <Register Name="DeviceSerialBlock">
296                <Address>0x3000</Address>
297                <Length>4</Length>
298                <AccessMode>RO</AccessMode>
299                <pPort>Device</pPort>
300            </Register>
301            <Register Name="ChunkMeasurementResults">
302                <Address>0x0</Address>
303                <Length>4</Length>
304                <AccessMode>RO</AccessMode>
305                <pPort>Chunk4007</pPort>
306            </Register>
307            <Register Name="DynamicBlock">
308                <Address>0x4000</Address>
309                <pLength>BlockLength</pLength>
310            </Register>
311            <StringReg Name="DeviceVendorName">
312                <Address>0x5000</Address>
313                <Length>4</Length>
314                <AccessMode>RO</AccessMode>
315            </StringReg>
316        </RegisterDescription>
317    "#;
318
319    fn build_register_nodemap() -> NodeMap {
320        let model = viva_genapi_xml::parse(REGISTER_FIXTURE).expect("parse register fixture");
321        NodeMap::try_from_xml(model).expect("build nodemap")
322    }
323
324    #[test]
325    fn register_reads_and_writes_raw_bytes() {
326        let nodemap = build_register_nodemap();
327        let payload = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04];
328        let io = MockIo::with_registers(&[(0x2000, payload.clone())]);
329
330        assert_eq!(
331            nodemap
332                .get_register("FileAccessBuffer", &io)
333                .expect("read register"),
334            payload
335        );
336
337        let replacement = vec![9u8; 8];
338        nodemap
339            .set_register("FileAccessBuffer", &replacement, &io)
340            .expect("write register");
341        assert_eq!(
342            nodemap
343                .get_register("FileAccessBuffer", &io)
344                .expect("re-read register"),
345            replacement
346        );
347    }
348
349    /// A short write must be refused, not padded.
350    ///
351    /// `set_string` zero-pads to the declared length, which is right for a
352    /// string. Doing the same to a file-transfer buffer would silently zero
353    /// the rest of the block — data loss dressed as a convenience.
354    #[test]
355    fn register_write_of_the_wrong_length_is_refused() {
356        let nodemap = build_register_nodemap();
357        let io = MockIo::with_registers(&[(0x2000, vec![0u8; 8])]);
358
359        let err = nodemap
360            .set_register("FileAccessBuffer", &[1, 2, 3], &io)
361            .expect_err("a 3-byte write into an 8-byte register must fail");
362        assert!(
363            matches!(err, GenApiError::Range(_)),
364            "expected a range error, got {err:?}"
365        );
366        assert_eq!(
367            io.read(0x2000, 8).expect("register untouched"),
368            vec![0u8; 8],
369            "the refused write must not have reached the device"
370        );
371    }
372
373    /// A chunk-port register is listed but cannot be read.
374    ///
375    /// Its address is relative to a port we do not route. Reading it through
376    /// the device port would not error — it would return whatever lives at
377    /// that address, which for a scanCONTROL's `0x0` is the GVCP bootstrap
378    /// area. Silent wrong answers are what ADR-0018 exists to refuse (GA-12).
379    #[test]
380    fn register_on_a_non_device_port_is_listed_but_not_readable() {
381        let nodemap = build_register_nodemap();
382        let io = MockIo::with_registers(&[(0x0, vec![1, 2, 3, 4])]);
383
384        assert!(
385            nodemap.node_names().any(|n| n == "ChunkMeasurementResults"),
386            "the node must still be visible for introspection"
387        );
388
389        let err = nodemap
390            .get_register("ChunkMeasurementResults", &io)
391            .expect_err("a chunk-port register must not be read through the device port");
392        let text = err.to_string();
393        assert!(
394            text.contains("Chunk4007") && text.contains("GA-12"),
395            "the error must name the port and the reason: {text}"
396        );
397
398        // An explicit <pPort>Device</pPort> is the device port and reads fine.
399        let io = MockIo::with_registers(&[(0x3000, vec![5, 6, 7, 8])]);
400        assert_eq!(
401            nodemap
402                .get_register("DeviceSerialBlock", &io)
403                .expect("explicit Device port reads"),
404            vec![5, 6, 7, 8]
405        );
406    }
407
408    /// `register_address` (#92) keeps working, and now covers `<Register>`.
409    #[test]
410    fn register_address_resolves_for_a_register_node() {
411        let nodemap = build_register_nodemap();
412        let io = MockIo::default();
413        assert_eq!(
414            nodemap
415                .register_address("FileAccessBuffer", &io)
416                .expect("resolve address"),
417            (0x2000, 8)
418        );
419    }
420
421    #[test]
422    fn register_accessors_reject_the_wrong_node_type() {
423        let nodemap = build_register_nodemap();
424        let io = MockIo::with_registers(&[(0x5000, b"AVT\0".to_vec())]);
425
426        assert!(matches!(
427            nodemap.get_register("DeviceVendorName", &io),
428            Err(GenApiError::Type(_))
429        ));
430        assert!(matches!(
431            nodemap.get_register("NoSuchNode", &io),
432            Err(GenApiError::NodeNotFound(_))
433        ));
434    }
435
436    /// The deferred half of GA-09 must name itself in the skip reason.
437    ///
438    /// The corpus allowlist matches on this substring to tell a known gap from
439    /// a regression, so the wording is load-bearing, not cosmetic.
440    #[test]
441    fn a_register_with_p_length_is_skipped_and_says_why() {
442        let nodemap = build_register_nodemap();
443        let skipped = nodemap.skipped();
444
445        let entry = skipped
446            .iter()
447            .find(|s| s.name.as_deref() == Some("DynamicBlock"))
448            .expect("a <pLength> register must be recorded, not silently dropped");
449        assert_eq!(entry.tag, "Register");
450        assert!(
451            entry.error.contains("<pLength>"),
452            "the skip reason must name <pLength>: {}",
453            entry.error
454        );
455
456        // The other four nodes still built.
457        assert_eq!(nodemap.node_names().count(), 4);
458    }
459
460    /// Structural elements are not features and must not be reported as lost.
461    #[test]
462    fn elements_without_a_name_are_not_reported_as_skipped() {
463        const WITH_PORT: &str = r#"
464            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="1" SchemaSubMinorVersion="0">
465                <Port Name="Device"/>
466                <Group Comment="Standard">
467                    <Integer Name="Width">
468                        <Address>0x100</Address>
469                        <Length>4</Length>
470                        <AccessMode>RW</AccessMode>
471                    </Integer>
472                </Group>
473            </RegisterDescription>
474        "#;
475        let model = viva_genapi_xml::parse(WITH_PORT).expect("parse");
476        assert!(model.skipped.is_empty(), "{:?}", model.skipped);
477        assert_eq!(model.nodes.len(), 1);
478    }
479
480    fn build_indirect_nodemap() -> NodeMap {
481        let model = viva_genapi_xml::parse(INDIRECT_FIXTURE).expect("parse indirect fixture");
482        NodeMap::try_from_xml(model).expect("build nodemap")
483    }
484
485    fn build_enum_pvalue_nodemap() -> NodeMap {
486        let model = viva_genapi_xml::parse(ENUM_PVALUE_FIXTURE).expect("parse enum pvalue fixture");
487        NodeMap::try_from_xml(model).expect("build nodemap")
488    }
489
490    fn build_bitfield_nodemap() -> NodeMap {
491        let model = viva_genapi_xml::parse(BITFIELD_FIXTURE).expect("parse bitfield fixture");
492        NodeMap::try_from_xml(model).expect("build nodemap")
493    }
494
495    fn build_swissknife_nodemap() -> NodeMap {
496        let model = viva_genapi_xml::parse(SWISSKNIFE_FIXTURE).expect("parse swissknife fixture");
497        NodeMap::try_from_xml(model).expect("build nodemap")
498    }
499
500    #[test]
501    fn integer_roundtrip_and_cache() {
502        let mut nodemap = build_nodemap();
503        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 4, 0])]);
504        let width = nodemap.get_integer("Width", &io).expect("read width");
505        assert_eq!(width, 1024);
506        assert_eq!(io.read_count(0x100), 1);
507        let width_again = nodemap.get_integer("Width", &io).expect("cached width");
508        assert_eq!(width_again, 1024);
509        assert_eq!(io.read_count(0x100), 1, "cached value should be reused");
510        nodemap
511            .set_integer("Width", 1030, &io)
512            .expect("write width");
513        let width = nodemap
514            .get_integer("Width", &io)
515            .expect("read updated width");
516        assert_eq!(width, 1030);
517        assert_eq!(io.read_count(0x100), 1, "write should update cache");
518    }
519
520    const IEEE754_FIXTURE: &str = r#"
521        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
522            <FloatReg Name="FrameRate">
523                <Address>0x100</Address>
524                <Length>4</Length>
525                <AccessMode>RW</AccessMode>
526                <Min>0.0</Min>
527                <Max>1000.0</Max>
528                <Endianess>BigEndian</Endianess>
529            </FloatReg>
530            <Float Name="ExposureUs">
531                <Address>0x110</Address>
532                <Length>8</Length>
533                <AccessMode>RW</AccessMode>
534                <Min>0.0</Min>
535                <Max>1000000.0</Max>
536                <Endianess>BigEndian</Endianess>
537            </Float>
538        </RegisterDescription>
539    "#;
540
541    fn build_ieee754_nodemap() -> NodeMap {
542        NodeMap::try_from_xml(viva_genapi_xml::parse(IEEE754_FIXTURE).expect("parse ieee754"))
543            .expect("build nodemap")
544    }
545
546    #[test]
547    fn float_ieee754_f32_roundtrip() {
548        let mut nodemap = build_ieee754_nodemap();
549        let io = MockIo::with_registers(&[(0x100, 30.0f32.to_be_bytes().to_vec())]);
550        let v = nodemap.get_float("FrameRate", &io).expect("read rate");
551        assert!((v - 30.0).abs() < 1e-3, "got {v}");
552
553        nodemap
554            .set_float("FrameRate", 42.5, &io)
555            .expect("write rate");
556        let raw = io.read(0x100, 4).expect("read back");
557        assert_eq!(raw, 42.5f32.to_be_bytes());
558    }
559
560    #[test]
561    fn float_ieee754_f64_heuristic_roundtrip() {
562        let mut nodemap = build_ieee754_nodemap();
563        let io = MockIo::with_registers(&[(0x110, 6000.0f64.to_be_bytes().to_vec())]);
564        let v = nodemap.get_float("ExposureUs", &io).expect("read exposure");
565        assert!((v - 6000.0).abs() < 1e-9, "got {v}");
566
567        nodemap
568            .set_float("ExposureUs", 5000.0, &io)
569            .expect("write exposure");
570        let raw = io.read(0x110, 8).expect("read back");
571        assert_eq!(raw, 5000.0f64.to_be_bytes());
572    }
573
574    #[test]
575    fn float_scaled_integer_preserved() {
576        // The classic fixture's ExposureTime uses <Scale>1/1000</Scale>,
577        // so it must stay on the scaled-integer path even after the heuristic.
578        let nodemap = build_nodemap();
579        let raw = 50_000i64;
580        let io = MockIo::with_registers(&[(
581            0x200,
582            i64_to_bytes("ExposureTime", raw, 4, Sign::Signed).unwrap(),
583        )]);
584        let exposure = nodemap
585            .get_float("ExposureTime", &io)
586            .expect("read exposure");
587        assert!((exposure - 50.0).abs() < 1e-6);
588    }
589
590    const PREDICATE_FIXTURE: &str = r#"
591        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
592            <IntReg Name="CtrlReg">
593                <Address>0x400</Address>
594                <Length>4</Length>
595                <AccessMode>RW</AccessMode>
596                <Sign>Unsigned</Sign>
597                <Endianess>BigEndian</Endianess>
598            </IntReg>
599            <IntSwissKnife Name="GateImplemented">
600                <Formula>CTRL &amp; 1</Formula>
601                <pVariable Name="CTRL">CtrlReg</pVariable>
602                <Output>Integer</Output>
603            </IntSwissKnife>
604            <IntSwissKnife Name="GateLocked">
605                <Formula>(CTRL &amp; 2) / 2</Formula>
606                <pVariable Name="CTRL">CtrlReg</pVariable>
607                <Output>Integer</Output>
608            </IntSwissKnife>
609            <IntSwissKnife Name="Entry8Implemented">
610                <Formula>(CTRL &amp; 4) / 4</Formula>
611                <pVariable Name="CTRL">CtrlReg</pVariable>
612                <Output>Integer</Output>
613            </IntSwissKnife>
614            <Integer Name="Gated">
615                <Address>0x410</Address>
616                <Length>4</Length>
617                <AccessMode>RW</AccessMode>
618                <Min>0</Min>
619                <Max>255</Max>
620                <Sign>Unsigned</Sign>
621                <Endianess>BigEndian</Endianess>
622                <pIsImplemented>GateImplemented</pIsImplemented>
623                <pIsLocked>GateLocked</pIsLocked>
624            </Integer>
625            <Enumeration Name="PixelFormat">
626                <EnumEntry Name="Mono8"><Value>1</Value></EnumEntry>
627                <EnumEntry Name="Mono16">
628                    <Value>2</Value>
629                    <pIsImplemented>Entry8Implemented</pIsImplemented>
630                </EnumEntry>
631                <pValue>PixelFormatReg</pValue>
632            </Enumeration>
633            <IntReg Name="PixelFormatReg">
634                <Address>0x420</Address>
635                <Length>4</Length>
636                <AccessMode>RW</AccessMode>
637                <Sign>Unsigned</Sign>
638                <Endianess>BigEndian</Endianess>
639            </IntReg>
640        </RegisterDescription>
641    "#;
642
643    fn build_predicate_nodemap() -> NodeMap {
644        NodeMap::try_from_xml(
645            viva_genapi_xml::parse(PREDICATE_FIXTURE).expect("parse predicate fixture"),
646        )
647        .expect("build nodemap")
648    }
649
650    fn predicate_io(ctrl: u32) -> MockIo {
651        MockIo::with_registers(&[
652            (0x400, ctrl.to_be_bytes().to_vec()),
653            (0x410, 0u32.to_be_bytes().to_vec()),
654            (0x420, 1u32.to_be_bytes().to_vec()),
655        ])
656    }
657
658    #[test]
659    fn predicate_is_implemented_defaults_true() {
660        let nodemap = build_predicate_nodemap();
661        let io = predicate_io(0);
662        // CtrlReg itself has no pIsImplemented → always implemented.
663        assert!(nodemap.is_implemented("CtrlReg", &io).unwrap());
664    }
665
666    #[test]
667    fn predicate_is_implemented_follows_gate() {
668        let nm0 = build_predicate_nodemap();
669        let io0 = predicate_io(0);
670        assert!(!nm0.is_implemented("Gated", &io0).unwrap());
671        let nm1 = build_predicate_nodemap();
672        let io1 = predicate_io(1);
673        assert!(nm1.is_implemented("Gated", &io1).unwrap());
674    }
675
676    #[test]
677    fn predicate_is_available_chains_implemented() {
678        let nm0 = build_predicate_nodemap();
679        let io0 = predicate_io(0);
680        assert!(!nm0.is_available("Gated", &io0).unwrap());
681        let nm1 = build_predicate_nodemap();
682        let io1 = predicate_io(1);
683        assert!(nm1.is_available("Gated", &io1).unwrap());
684    }
685
686    #[test]
687    fn predicate_effective_access_mode_locked_downgrade() {
688        let nodemap = build_predicate_nodemap();
689        // bit 0 set (implemented), bit 1 set (locked) → RW → RO
690        let io = predicate_io(0b11);
691        let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
692        assert_eq!(mode, AccessMode::RO);
693    }
694
695    #[test]
696    fn write_to_a_locked_node_is_refused_locally() {
697        // The #45 shape: the node's static AccessMode is RW and the whole
698        // restriction lives in pIsLocked. Before GA-06 this write went to the
699        // wire and the device answered ACCESS_DENIED.
700        let mut nodemap = build_predicate_nodemap();
701        let io = predicate_io(0b11); // implemented, locked
702        let err = nodemap
703            .set_integer("Gated", 7, &io)
704            .expect_err("a locked node must not be written");
705        match err {
706            GenApiError::Locked { name, locked_by } => {
707                assert_eq!(name, "Gated");
708                // Naming the locking feature is the actionable part.
709                assert_eq!(locked_by, "GateLocked");
710            }
711            other => panic!("expected Locked, got {other:?}"),
712        }
713    }
714
715    #[test]
716    fn write_to_an_unlocked_node_still_succeeds() {
717        let mut nodemap = build_predicate_nodemap();
718        let io = predicate_io(0b01); // implemented, unlocked
719        nodemap
720            .set_integer("Gated", 7, &io)
721            .expect("an unlocked RW node must still be writable");
722    }
723
724    #[test]
725    fn write_to_an_unimplemented_node_reports_unavailable_not_locked() {
726        // The two conditions must stay distinguishable: `effective_access_mode`
727        // collapses both into RO, which is why the setters do not use it.
728        let mut nodemap = build_predicate_nodemap();
729        let io = predicate_io(0b00); // not implemented
730        let err = nodemap
731            .set_integer("Gated", 7, &io)
732            .expect_err("an unavailable node must not be written");
733        assert!(
734            matches!(err, GenApiError::Unavailable(ref n) if n == "Gated"),
735            "expected Unavailable, got {err:?}"
736        );
737    }
738
739    #[test]
740    fn predicate_effective_access_mode_rw_when_unlocked() {
741        let nodemap = build_predicate_nodemap();
742        // implemented, unlocked → base RW
743        let io = predicate_io(0b01);
744        let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
745        assert_eq!(mode, AccessMode::RW);
746    }
747
748    #[test]
749    fn predicate_effective_access_mode_na_for_unavailable() {
750        let nodemap = build_predicate_nodemap();
751        // not implemented → effective access reported as RO (we don't model NA).
752        let io = predicate_io(0);
753        let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
754        assert_eq!(mode, AccessMode::RO);
755    }
756
757    #[test]
758    fn predicate_available_enum_entries_filters() {
759        let nodemap = build_predicate_nodemap();
760        // bit 2 clear → Mono16 gated out; Mono8 has no predicate so it stays.
761        let io = predicate_io(0);
762        let entries = nodemap
763            .available_enum_entries("PixelFormat", &io)
764            .expect("enum entries");
765        assert_eq!(entries, vec!["Mono8".to_string()]);
766    }
767
768    #[test]
769    fn predicate_available_enum_entries_full_when_allowed() {
770        let nodemap = build_predicate_nodemap();
771        // bit 2 set → Mono16 available.
772        let io = predicate_io(0b100);
773        let mut entries = nodemap
774            .available_enum_entries("PixelFormat", &io)
775            .expect("enum entries");
776        entries.sort();
777        assert_eq!(entries, vec!["Mono16".to_string(), "Mono8".to_string()]);
778    }
779
780    #[test]
781    fn predicate_available_enum_entries_fallback_to_static() {
782        // CtrlReg itself isn't an enum; use an enum without entry predicates.
783        let xml = r#"
784            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
785                <Enumeration Name="Mode">
786                    <EnumEntry Name="A"><Value>0</Value></EnumEntry>
787                    <EnumEntry Name="B"><Value>1</Value></EnumEntry>
788                    <pValue>ModeReg</pValue>
789                </Enumeration>
790                <IntReg Name="ModeReg">
791                    <Address>0x500</Address>
792                    <Length>4</Length>
793                    <AccessMode>RW</AccessMode>
794                    <Sign>Unsigned</Sign>
795                    <Endianess>BigEndian</Endianess>
796                </IntReg>
797            </RegisterDescription>
798        "#;
799        let nodemap =
800            NodeMap::try_from_xml(viva_genapi_xml::parse(xml).unwrap()).expect("build nodemap");
801        let io = MockIo::with_registers(&[(0x500, 0u32.to_be_bytes().to_vec())]);
802        let mut entries = nodemap.available_enum_entries("Mode", &io).unwrap();
803        entries.sort();
804        assert_eq!(entries, vec!["A".to_string(), "B".to_string()]);
805    }
806
807    #[test]
808    fn float_conversion_roundtrip() {
809        let mut nodemap = build_nodemap();
810        let raw = 50_000i64; // 50 ms with 1/1000 scale
811        let io = MockIo::with_registers(&[(
812            0x200,
813            i64_to_bytes("ExposureTime", raw, 4, Sign::Signed).unwrap(),
814        )]);
815        let exposure = nodemap
816            .get_float("ExposureTime", &io)
817            .expect("read exposure");
818        assert!((exposure - 50.0).abs() < 1e-6);
819        nodemap
820            .set_float("ExposureTime", 75.0, &io)
821            .expect("write exposure");
822        let raw_back =
823            bytes_to_i64("ExposureTime", &io.read(0x200, 4).unwrap(), Sign::Signed).unwrap();
824        assert_eq!(raw_back, 75_000);
825    }
826
827    #[test]
828    fn selector_address_switching() {
829        let mut nodemap = build_nodemap();
830        let io = MockIo::with_registers(&[
831            (
832                0x300,
833                i64_to_bytes("GainSelector", 0, 2, Sign::Signed).unwrap(),
834            ),
835            (0x310, i64_to_bytes("Gain", 10, 2, Sign::Signed).unwrap()),
836            (0x314, i64_to_bytes("Gain", 24, 2, Sign::Signed).unwrap()),
837        ]);
838
839        let gain_all = nodemap.get_integer("Gain", &io).expect("gain for All");
840        assert_eq!(gain_all, 10);
841        assert_eq!(io.read_count(0x310), 1);
842        assert_eq!(io.read_count(0x314), 0);
843
844        io.write(0x314, &i64_to_bytes("Gain", 32, 2, Sign::Signed).unwrap())
845            .expect("update red gain");
846        nodemap
847            .set_enum("GainSelector", "Red", &io)
848            .expect("set selector to red");
849        let gain_red = nodemap.get_integer("Gain", &io).expect("gain for Red");
850        assert_eq!(gain_red, 32);
851        assert_eq!(
852            io.read_count(0x310),
853            1,
854            "previous address should not be reread"
855        );
856        assert_eq!(io.read_count(0x314), 1);
857
858        let gain_red_cached = nodemap.get_integer("Gain", &io).expect("cached red");
859        assert_eq!(gain_red_cached, 32);
860        assert_eq!(io.read_count(0x314), 1, "selector cache should be reused");
861
862        nodemap
863            .set_enum("GainSelector", "Blue", &io)
864            .expect("set selector to blue");
865        let err = nodemap.get_integer("Gain", &io).unwrap_err();
866        match err {
867            GenApiError::Unavailable(msg) => {
868                assert!(msg.contains("GainSelector=Blue"));
869            }
870            other => panic!("unexpected error: {other:?}"),
871        }
872        assert_eq!(
873            io.read_count(0x314),
874            1,
875            "no read expected for missing mapping"
876        );
877
878        io.write(0x310, &i64_to_bytes("Gain", 12, 2, Sign::Signed).unwrap())
879            .expect("update all gain");
880        nodemap
881            .set_enum("GainSelector", "All", &io)
882            .expect("restore selector to all");
883        let gain_all_updated = nodemap
884            .get_integer("Gain", &io)
885            .expect("gain for All again");
886        assert_eq!(gain_all_updated, 12);
887        assert_eq!(
888            io.read_count(0x310),
889            2,
890            "address switch should invalidate cache"
891        );
892    }
893
894    #[test]
895    fn range_enforcement() {
896        let mut nodemap = build_nodemap();
897        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 0, 16])]);
898        let err = nodemap.set_integer("Width", 17, &io).unwrap_err();
899        assert!(matches!(err, GenApiError::Range(_)));
900    }
901
902    #[test]
903    fn command_exec() {
904        let mut nodemap = build_nodemap();
905        let io = MockIo::with_registers(&[]);
906        nodemap
907            .exec_command("AcquisitionStart", &io)
908            .expect("exec command");
909        let payload = io.read(0x500, 4).expect("command write");
910        assert_eq!(payload, vec![0, 0, 0, 1]);
911    }
912
913    #[test]
914    fn indirect_address_resolution() {
915        let mut nodemap = build_indirect_nodemap();
916        let io = MockIo::with_registers(&[
917            (
918                0x2000,
919                i64_to_bytes("RegAddr", 0x3000, 4, Sign::Signed).unwrap(),
920            ),
921            (0x3000, i64_to_bytes("Gain", 123, 4, Sign::Signed).unwrap()),
922            (0x3100, i64_to_bytes("Gain", 77, 4, Sign::Signed).unwrap()),
923        ]);
924
925        let initial = nodemap.get_integer("Gain", &io).expect("read gain");
926        assert_eq!(initial, 123);
927        assert_eq!(io.read_count(0x2000), 1);
928        assert_eq!(io.read_count(0x3000), 1);
929
930        nodemap
931            .set_integer("RegAddr", 0x3100, &io)
932            .expect("set indirect address");
933        let updated = nodemap
934            .get_integer("Gain", &io)
935            .expect("read gain after change");
936        assert_eq!(updated, 77);
937        assert_eq!(io.read_count(0x2000), 1);
938        assert_eq!(io.read_count(0x3000), 1);
939        assert_eq!(io.read_count(0x3100), 1);
940    }
941
942    /// A `<pAddress>` term that cannot be an address at all is rejected before
943    /// the read.
944    ///
945    /// Zero deliberately is *not* rejected: under the additive address model a
946    /// `<pAddress>` supplies one term of a sum, and a base of zero next to a
947    /// fixed `<Address>` offset is ordinary. Only a value that cannot be a
948    /// register address — a negative one — is a modelling error rather than a
949    /// device state.
950    /// A `<pAddress>` term that cannot be an address at all is rejected before
951    /// the read.
952    ///
953    /// Zero deliberately is *not* rejected: under the additive address model a
954    /// `<pAddress>` supplies one term of a sum, and a base of zero next to a
955    /// fixed `<Address>` offset is ordinary. Only a value that cannot be a
956    /// register address — a negative one, which takes a signed provider — is a
957    /// modelling error rather than a device state.
958    #[test]
959    fn indirect_bad_address() {
960        const XML: &str = r#"
961            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
962                <Integer Name="RegAddr">
963                    <Address>0x2000</Address>
964                    <Length>4</Length>
965                    <AccessMode>RW</AccessMode>
966                    <Sign>Signed</Sign>
967                    <Min>-65535</Min>
968                    <Max>65535</Max>
969                </Integer>
970                <Integer Name="Gain">
971                    <pAddress>RegAddr</pAddress>
972                    <Length>4</Length>
973                    <AccessMode>RW</AccessMode>
974                    <Min>0</Min>
975                    <Max>255</Max>
976                </Integer>
977            </RegisterDescription>
978        "#;
979
980        let model = viva_genapi_xml::parse(XML).expect("parse");
981        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
982        let io = MockIo::with_registers(&[(
983            0x2000,
984            i64_to_bytes("RegAddr", -4, 4, Sign::Signed).unwrap(),
985        )]);
986
987        let err = nodemap.get_integer("Gain", &io).unwrap_err();
988        match err {
989            GenApiError::BadIndirectAddress { name, addr } => {
990                assert_eq!(name, "Gain");
991                assert_eq!(addr, -4);
992            }
993            other => panic!("unexpected error: {other:?}"),
994        }
995    }
996
997    /// GenICam registers are unsigned unless `<Sign>Signed</Sign>` says
998    /// otherwise. Sign-extending everything turned an IPv4 address into a
999    /// negative number and broke every mask comparison downstream.
1000    #[test]
1001    fn unsigned_registers_do_not_sign_extend() {
1002        const XML: &str = r#"
1003            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1004                <Integer Name="GevCurrentIPAddress">
1005                    <Address>0x1000</Address>
1006                    <Length>4</Length>
1007                    <AccessMode>RO</AccessMode>
1008                    <Min>0</Min>
1009                    <Max>4294967295</Max>
1010                </Integer>
1011                <Integer Name="TemperatureOffset">
1012                    <Address>0x1004</Address>
1013                    <Length>4</Length>
1014                    <AccessMode>RO</AccessMode>
1015                    <Sign>Signed</Sign>
1016                    <Min>-2147483648</Min>
1017                    <Max>2147483647</Max>
1018                </Integer>
1019            </RegisterDescription>
1020        "#;
1021
1022        let model = viva_genapi_xml::parse(XML).expect("parse");
1023        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1024        let io = MockIo::with_registers(&[
1025            // 192.168.1.160 — the top bit is set.
1026            (0x1000, vec![0xC0, 0xA8, 0x01, 0xA0]),
1027            (0x1004, vec![0xFF, 0xFF, 0xFF, 0xFB]),
1028        ]);
1029
1030        assert_eq!(
1031            nodemap
1032                .get_integer("GevCurrentIPAddress", &io)
1033                .expect("read address"),
1034            0xC0A8_01A0
1035        );
1036        assert_eq!(
1037            nodemap
1038                .get_integer("TemperatureOffset", &io)
1039                .expect("read offset"),
1040            -5
1041        );
1042    }
1043
1044    /// `<FormulaFrom>` is the read direction and `<FormulaTo>` the write one.
1045    ///
1046    /// With an identity converter both directions look the same, which is how
1047    /// this stayed inverted: reads evaluated `<FormulaTo>` and every non-trivial
1048    /// converter — a Hikrobot `FROM * 100` / `TO / 100` pair, for instance —
1049    /// came back wrong by the square of its scale factor.
1050    #[test]
1051    fn converter_directions_are_not_swapped() {
1052        const XML: &str = r#"
1053            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1054                <IntReg Name="ExposureTimeRaw">
1055                    <Address>0x1000</Address>
1056                    <Length>4</Length>
1057                    <AccessMode>RW</AccessMode>
1058                </IntReg>
1059                <Converter Name="ExposureTime">
1060                    <FormulaTo>FROM * 100</FormulaTo>
1061                    <FormulaFrom>TO / 100</FormulaFrom>
1062                    <pValue>ExposureTimeRaw</pValue>
1063                    <Unit>us</Unit>
1064                </Converter>
1065            </RegisterDescription>
1066        "#;
1067
1068        let model = viva_genapi_xml::parse(XML).expect("parse");
1069        let mut nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1070        let io = MockIo::with_registers(&[(
1071            0x1000,
1072            i64_to_bytes("ExposureTimeRaw", 5000, 4, Sign::Unsigned).unwrap(),
1073        )]);
1074
1075        // Read goes through FormulaFrom: 5000 / 100.
1076        let value = nodemap.get_float("ExposureTime", &io).expect("read");
1077        assert!((value - 50.0).abs() < 1e-9, "got {value}");
1078
1079        // Write goes through FormulaTo: 75 * 100.
1080        nodemap.set_float("ExposureTime", 75.0, &io).expect("write");
1081        assert_eq!(
1082            nodemap
1083                .get_integer("ExposureTimeRaw", &io)
1084                .expect("read raw back"),
1085            7500
1086        );
1087        let roundtrip = nodemap.get_float("ExposureTime", &io).expect("re-read");
1088        assert!((roundtrip - 75.0).abs() < 1e-9, "got {roundtrip}");
1089    }
1090
1091    /// A read-modify-write `<FormulaTo>` sees the register's current contents
1092    /// through `OLD`.
1093    #[test]
1094    fn int_converter_write_preserves_other_bits() {
1095        const XML: &str = r#"
1096            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1097                <IntReg Name="Binning_Reg">
1098                    <Address>0x1000</Address>
1099                    <Length>4</Length>
1100                    <AccessMode>RW</AccessMode>
1101                </IntReg>
1102                <IntConverter Name="BinningHorizontal">
1103                    <FormulaTo>FROM | (OLD &amp; 0xffff0000)</FormulaTo>
1104                    <FormulaFrom>TO &amp; 0x0000ffff</FormulaFrom>
1105                    <pVariable Name="OLD">Binning_Reg</pVariable>
1106                    <pValue>Binning_Reg</pValue>
1107                </IntConverter>
1108            </RegisterDescription>
1109        "#;
1110
1111        let model = viva_genapi_xml::parse(XML).expect("parse");
1112        let mut nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1113        let io = MockIo::with_registers(&[(
1114            0x1000,
1115            i64_to_bytes("Binning_Reg", 0x0004_0002, 4, Sign::Unsigned).unwrap(),
1116        )]);
1117
1118        assert_eq!(
1119            nodemap
1120                .get_integer("BinningHorizontal", &io)
1121                .expect("read low half"),
1122            2
1123        );
1124
1125        nodemap
1126            .set_integer("BinningHorizontal", 3, &io)
1127            .expect("write low half");
1128        // The vertical half in the high word must survive.
1129        assert_eq!(
1130            nodemap
1131                .get_integer("Binning_Reg", &io)
1132                .expect("read raw back"),
1133            0x0004_0003
1134        );
1135    }
1136
1137    /// Signedness comes from `<Sign>` alone — `<Min>` says nothing about it.
1138    ///
1139    /// `<Min>` is optional and defaults to `i64::MIN`, and **no** `<IntReg>` or
1140    /// `<MaskedIntReg>` in the entire vendor corpus declares one: all 9 779 of
1141    /// them omit it. Inferring "signed" from a negative minimum therefore fires
1142    /// on every register-backed integer on every real camera — which is exactly
1143    /// the case `<Sign>` exists to decide. Note the registers here carry no
1144    /// `<Min>`, which is the shape real documents actually use.
1145    #[test]
1146    fn sign_does_not_depend_on_min() {
1147        const XML: &str = r#"
1148            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1149                <IntReg Name="GevCurrentIPAddress">
1150                    <Address>0x1000</Address>
1151                    <Length>4</Length>
1152                    <AccessMode>RO</AccessMode>
1153                    <Sign>Unsigned</Sign>
1154                </IntReg>
1155                <IntReg Name="DeviceTemperatureRaw">
1156                    <Address>0x1004</Address>
1157                    <Length>4</Length>
1158                    <AccessMode>RO</AccessMode>
1159                    <Sign>Signed</Sign>
1160                </IntReg>
1161                <IntReg Name="ImplicitlyUnsigned">
1162                    <Address>0x1008</Address>
1163                    <Length>4</Length>
1164                    <AccessMode>RO</AccessMode>
1165                </IntReg>
1166            </RegisterDescription>
1167        "#;
1168
1169        let model = viva_genapi_xml::parse(XML).expect("parse");
1170        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1171        let io = MockIo::with_registers(&[
1172            (0x1000, vec![0xC0, 0xA8, 0x01, 0xA0]),
1173            (0x1004, vec![0xFF, 0xFF, 0xFF, 0xFB]),
1174            (0x1008, vec![0xFF, 0xFF, 0xFF, 0xFF]),
1175        ]);
1176
1177        assert_eq!(
1178            nodemap
1179                .get_integer("GevCurrentIPAddress", &io)
1180                .expect("read address"),
1181            0xC0A8_01A0
1182        );
1183        assert_eq!(
1184            nodemap
1185                .get_integer("DeviceTemperatureRaw", &io)
1186                .expect("read temperature"),
1187            -5
1188        );
1189        // No `<Sign>` at all: GenICam's default is unsigned.
1190        assert_eq!(
1191            nodemap
1192                .get_integer("ImplicitlyUnsigned", &io)
1193                .expect("read default-signed register"),
1194            0xFFFF_FFFFu32 as i64
1195        );
1196    }
1197
1198    /// A `<StructReg>` bit reads as 1, not -1.
1199    ///
1200    /// Entries used to declare the full `i64` range, which looked like a
1201    /// signed field; a set bit then sign-extended to -1 and every
1202    /// `(INQ = 1)` test in the document silently failed.
1203    #[test]
1204    fn struct_entry_bits_read_unsigned() {
1205        const XML: &str = r#"
1206            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1207                <StructReg Comment="Gain Inquiry Register">
1208                    <Address>0x520</Address>
1209                    <Length>4</Length>
1210                    <AccessMode>RO</AccessMode>
1211                    <Endianess>BigEndian</Endianess>
1212                    <StructEntry Name="GainPresInq_Bit"><Bit>0</Bit></StructEntry>
1213                    <StructEntry Name="GainAutoInq_Bit"><Bit>6</Bit></StructEntry>
1214                </StructReg>
1215            </RegisterDescription>
1216        "#;
1217
1218        let model = viva_genapi_xml::parse(XML).expect("parse");
1219        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1220        // `<Bit>` indices count from the MSB on a big-endian register (the
1221        // same rule the reference implementation applies), so bits 0 and 6 are
1222        // the top byte's 0x80 and 0x02.
1223        let io = MockIo::with_registers(&[(0x520, vec![0x82, 0x00, 0x00, 0x00])]);
1224
1225        assert_eq!(
1226            nodemap
1227                .get_integer("GainPresInq_Bit", &io)
1228                .expect("read presence bit"),
1229            1
1230        );
1231        assert_eq!(
1232            nodemap
1233                .get_integer("GainAutoInq_Bit", &io)
1234                .expect("read auto bit"),
1235            1
1236        );
1237    }
1238
1239    /// The whole point of the additive model: `<Address>` and `<pAddress>` on
1240    /// the same register add up. Keeping only one of them silently read the
1241    /// wrong register on FLIR, Point Grey and Hikrobot cameras (issue #35).
1242    #[test]
1243    fn address_terms_sum() {
1244        const XML: &str = r#"
1245            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1246                <Integer Name="RegBase">
1247                    <Address>0x2000</Address>
1248                    <Length>4</Length>
1249                    <AccessMode>RW</AccessMode>
1250                    <Min>0</Min>
1251                    <Max>65535</Max>
1252                </Integer>
1253                <Integer Name="Gain">
1254                    <pAddress>RegBase</pAddress>
1255                    <Address>0x8</Address>
1256                    <Length>4</Length>
1257                    <AccessMode>RW</AccessMode>
1258                    <Min>0</Min>
1259                    <Max>255</Max>
1260                </Integer>
1261            </RegisterDescription>
1262        "#;
1263
1264        let model = viva_genapi_xml::parse(XML).expect("parse");
1265        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1266        let io = MockIo::with_registers(&[
1267            (
1268                0x2000,
1269                i64_to_bytes("RegBase", 0x3000, 4, Sign::Signed).unwrap(),
1270            ),
1271            // Only the summed address holds the value we expect.
1272            (0x3008, i64_to_bytes("Gain", 77, 4, Sign::Signed).unwrap()),
1273            (0x3000, i64_to_bytes("Gain", 11, 4, Sign::Signed).unwrap()),
1274        ]);
1275
1276        assert_eq!(nodemap.get_integer("Gain", &io).expect("read gain"), 77);
1277
1278        // The same addressing, reached by name for raw register access.
1279        assert_eq!(
1280            nodemap
1281                .register_address("Gain", &io)
1282                .expect("resolve summed address"),
1283            (0x3008, 4)
1284        );
1285        // An unknown name is a named error, not a panic.
1286        assert!(matches!(
1287            nodemap.register_address("NoSuchNode", &io),
1288            Err(GenApiError::NodeNotFound(_))
1289        ));
1290    }
1291
1292    /// `<pIndex Offset="N">` scales an index node into the address. AVT Manta
1293    /// and Prosilica use it for every per-trigger inquiry register.
1294    #[test]
1295    fn p_index_scales_into_the_address() {
1296        const XML: &str = r#"
1297            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1298                <Integer Name="TriggerSelectorIdx">
1299                    <Address>0x2000</Address>
1300                    <Length>4</Length>
1301                    <AccessMode>RW</AccessMode>
1302                    <Min>0</Min>
1303                    <Max>7</Max>
1304                </Integer>
1305                <Integer Name="TriggerInqDelay">
1306                    <Address>0x13400</Address>
1307                    <pIndex Offset="64">TriggerSelectorIdx</pIndex>
1308                    <Length>4</Length>
1309                    <AccessMode>RO</AccessMode>
1310                    <Min>0</Min>
1311                    <Max>255</Max>
1312                </Integer>
1313            </RegisterDescription>
1314        "#;
1315
1316        let model = viva_genapi_xml::parse(XML).expect("parse");
1317        let mut nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1318        let io = MockIo::with_registers(&[
1319            (
1320                0x2000,
1321                i64_to_bytes("TriggerSelectorIdx", 2, 4, Sign::Signed).unwrap(),
1322            ),
1323            (
1324                0x13400,
1325                i64_to_bytes("TriggerInqDelay", 1, 4, Sign::Signed).unwrap(),
1326            ),
1327            (
1328                0x13480,
1329                i64_to_bytes("TriggerInqDelay", 42, 4, Sign::Signed).unwrap(),
1330            ),
1331        ]);
1332
1333        // index 2 * stride 64 = 0x80 past the base.
1334        assert_eq!(
1335            nodemap
1336                .get_integer("TriggerInqDelay", &io)
1337                .expect("read indexed register"),
1338            42
1339        );
1340
1341        // Changing the index moves the register.
1342        nodemap
1343            .set_integer("TriggerSelectorIdx", 0, &io)
1344            .expect("select index 0");
1345        assert_eq!(
1346            nodemap
1347                .get_integer("TriggerInqDelay", &io)
1348                .expect("read indexed register"),
1349            1
1350        );
1351    }
1352
1353    #[test]
1354    fn enum_literal_entry_read() {
1355        let nodemap = build_enum_pvalue_nodemap();
1356        let io = MockIo::with_registers(&[
1357            (0x4000, i64_to_bytes("Mode", 10, 4, Sign::Signed).unwrap()),
1358            (
1359                0x4100,
1360                i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1361            ),
1362        ]);
1363
1364        let value = nodemap.get_enum("Mode", &io).expect("read mode");
1365        assert_eq!(value, "Fixed10");
1366        assert_eq!(
1367            io.read_count(0x4100),
1368            1,
1369            "provider should be read once for mapping"
1370        );
1371    }
1372
1373    #[test]
1374    fn enum_provider_entry_read() {
1375        let nodemap = build_enum_pvalue_nodemap();
1376        let io = MockIo::with_registers(&[
1377            (0x4000, i64_to_bytes("Mode", 42, 4, Sign::Signed).unwrap()),
1378            (
1379                0x4100,
1380                i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1381            ),
1382        ]);
1383
1384        let value = nodemap.get_enum("Mode", &io).expect("read dynamic mode");
1385        assert_eq!(value, "DynFromReg");
1386        assert_eq!(io.read_count(0x4100), 1);
1387    }
1388
1389    #[test]
1390    fn enum_set_uses_provider_value() {
1391        let mut nodemap = build_enum_pvalue_nodemap();
1392        let io = MockIo::with_registers(&[
1393            (0x4000, i64_to_bytes("Mode", 0, 4, Sign::Signed).unwrap()),
1394            (
1395                0x4100,
1396                i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1397            ),
1398        ]);
1399
1400        nodemap
1401            .set_enum("Mode", "DynFromReg", &io)
1402            .expect("write enum");
1403        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap(), Sign::Signed).unwrap();
1404        assert_eq!(raw, 42);
1405        assert_eq!(io.read_count(0x4100), 1);
1406    }
1407
1408    #[test]
1409    fn enum_provider_update_invalidates_mapping() {
1410        let mut nodemap = build_enum_pvalue_nodemap();
1411        let io = MockIo::with_registers(&[
1412            (0x4000, i64_to_bytes("Mode", 42, 4, Sign::Signed).unwrap()),
1413            (
1414                0x4100,
1415                i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1416            ),
1417        ]);
1418
1419        assert_eq!(nodemap.get_enum("Mode", &io).unwrap(), "DynFromReg");
1420        assert_eq!(io.read_count(0x4100), 1);
1421
1422        nodemap
1423            .set_integer("RegModeVal", 17, &io)
1424            .expect("update provider");
1425        io.write(0x4000, &i64_to_bytes("Mode", 0, 4, Sign::Signed).unwrap())
1426            .expect("reset mode register");
1427
1428        nodemap
1429            .set_enum("Mode", "DynFromReg", &io)
1430            .expect("write enum after provider change");
1431        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap(), Sign::Signed).unwrap();
1432        assert_eq!(raw, 17);
1433    }
1434
1435    #[test]
1436    fn enum_unknown_value_error() {
1437        let nodemap = build_enum_pvalue_nodemap();
1438        let io = MockIo::with_registers(&[
1439            (0x4000, i64_to_bytes("Mode", 99, 4, Sign::Signed).unwrap()),
1440            (
1441                0x4100,
1442                i64_to_bytes("RegModeVal", 42, 4, Sign::Signed).unwrap(),
1443            ),
1444        ]);
1445
1446        let err = nodemap.get_enum("Mode", &io).unwrap_err();
1447        match err {
1448            GenApiError::EnumValueUnknown { node, value } => {
1449                assert_eq!(node, "Mode");
1450                assert_eq!(value, 99);
1451            }
1452            other => panic!("unexpected error: {other:?}"),
1453        }
1454    }
1455
1456    #[test]
1457    fn enum_entries_are_sorted() {
1458        let nodemap = build_enum_pvalue_nodemap();
1459        let entries = nodemap.enum_entries("Mode").expect("entries");
1460        assert_eq!(
1461            entries,
1462            vec!["DynFromReg".to_string(), "Fixed10".to_string()]
1463        );
1464    }
1465
1466    #[test]
1467    fn bitfield_le_integer_roundtrip() {
1468        let mut nodemap = build_bitfield_nodemap();
1469        let io = MockIo::with_registers(&[(0x5000, vec![0xAA, 0xBB, 0xCC, 0xDD])]);
1470
1471        let value = nodemap
1472            .get_integer("LeByte", &io)
1473            .expect("read little-endian field");
1474        assert_eq!(value, 0xBB);
1475
1476        nodemap
1477            .set_integer("LeByte", 0x55, &io)
1478            .expect("write little-endian field");
1479        let data = io.read(0x5000, 4).expect("read back register");
1480        assert_eq!(data, vec![0xAA, 0x55, 0xCC, 0xDD]);
1481    }
1482
1483    #[test]
1484    fn bitfield_be_integer_roundtrip() {
1485        let mut nodemap = build_bitfield_nodemap();
1486        let io = MockIo::with_registers(&[(0x5004, vec![0b1010_0000, 0b0000_0000])]);
1487
1488        let value = nodemap
1489            .get_integer("BeBits", &io)
1490            .expect("read big-endian bits");
1491        assert_eq!(value, 0b101);
1492
1493        nodemap
1494            .set_integer("BeBits", 0b010, &io)
1495            .expect("write big-endian bits");
1496        let data = io.read(0x5004, 2).expect("read back register");
1497        assert_eq!(data, vec![0b0100_0000, 0b0000_0000]);
1498    }
1499
1500    #[test]
1501    fn bitfield_boolean_toggle() {
1502        let mut nodemap = build_bitfield_nodemap();
1503        let io = MockIo::with_registers(&[(0x5006, vec![0x00, 0x20, 0x00, 0x00])]);
1504
1505        assert!(nodemap.get_bool("PackedFlag", &io).expect("read flag"));
1506
1507        nodemap
1508            .set_bool("PackedFlag", false, &io)
1509            .expect("clear flag");
1510        let data = io.read(0x5006, 4).expect("read cleared");
1511        assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00]);
1512
1513        nodemap.set_bool("PackedFlag", true, &io).expect("set flag");
1514        let data = io.read(0x5006, 4).expect("read set");
1515        assert_eq!(data, vec![0x00, 0x20, 0x00, 0x00]);
1516    }
1517
1518    #[test]
1519    fn bitfield_value_too_wide() {
1520        let mut nodemap = build_bitfield_nodemap();
1521        let io = MockIo::with_registers(&[(0x5004, vec![0x00, 0x00])]);
1522
1523        let err = nodemap
1524            .set_integer("BeBits", 8, &io)
1525            .expect_err("value too wide");
1526        match err {
1527            GenApiError::ValueTooWide {
1528                name, bit_length, ..
1529            } => {
1530                assert_eq!(name, "BeBits");
1531                assert_eq!(bit_length, 3);
1532            }
1533            other => panic!("unexpected error: {other:?}"),
1534        }
1535    }
1536    #[test]
1537    fn swissknife_evaluates_and_invalidates() {
1538        let mut nodemap = build_swissknife_nodemap();
1539        let io = MockIo::with_registers(&[
1540            (
1541                0x3000,
1542                i64_to_bytes("GainRaw", 100, 4, Sign::Signed).unwrap(),
1543            ),
1544            (0x3008, i64_to_bytes("Offset", 3, 4, Sign::Signed).unwrap()),
1545            (0x3010, i64_to_bytes("B", 1, 4, Sign::Signed).unwrap()),
1546        ]);
1547
1548        let value = nodemap
1549            .get_float("ComputedGain", &io)
1550            .expect("compute gain");
1551        assert!((value - 53.0).abs() < 1e-6);
1552
1553        nodemap
1554            .set_integer("GainRaw", 120, &io)
1555            .expect("update raw gain");
1556        let updated = nodemap
1557            .get_float("ComputedGain", &io)
1558            .expect("recompute gain");
1559        assert!((updated - 63.0).abs() < 1e-6);
1560    }
1561
1562    #[test]
1563    fn swissknife_integer_rounding_and_unary() {
1564        let mut nodemap = build_swissknife_nodemap();
1565        let io = MockIo::with_registers(&[
1566            (0x3000, i64_to_bytes("GainRaw", 5, 4, Sign::Signed).unwrap()),
1567            (0x3008, i64_to_bytes("Offset", 0, 4, Sign::Signed).unwrap()),
1568            (0x3010, i64_to_bytes("B", 1, 4, Sign::Signed).unwrap()),
1569        ]);
1570
1571        // `<IntSwissKnife>` evaluates in integer arithmetic, so 5 / 3 truncates
1572        // to 1. Rounding to 2 would mean the division ran in floating point.
1573        let divided = nodemap
1574            .get_integer("DivideInt", &io)
1575            .expect("integer division");
1576        assert_eq!(divided, 1);
1577
1578        nodemap
1579            .set_integer("GainRaw", 3, &io)
1580            .expect("update gain raw");
1581        let unary = nodemap.get_integer("Unary", &io).expect("unary expression");
1582        assert_eq!(unary, 7);
1583    }
1584
1585    #[test]
1586    fn swissknife_unknown_variable_error() {
1587        const XML: &str = r#"
1588            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1589                <Integer Name="A">
1590                    <Address>0x2000</Address>
1591                    <Length>4</Length>
1592                    <AccessMode>RW</AccessMode>
1593                    <Min>0</Min>
1594                    <Max>100</Max>
1595                </Integer>
1596                <SwissKnife Name="Bad">
1597                    <Expression>A + Missing</Expression>
1598                    <pVariable Name="A">A</pVariable>
1599                </SwissKnife>
1600            </RegisterDescription>
1601        "#;
1602
1603        let model = viva_genapi_xml::parse(XML).expect("parse invalid swissknife");
1604        let nodemap = NodeMap::try_from_xml(model).expect("model builds despite the bad node");
1605
1606        // The unusable node is dropped and recorded, not fatal: issues #35 and
1607        // #45 were both a single odd node making an entire camera unopenable.
1608        let skipped = nodemap.skipped();
1609        assert_eq!(skipped.len(), 1, "expected exactly one dropped node");
1610        assert_eq!(skipped[0].name.as_deref(), Some("Bad"));
1611        assert!(
1612            skipped[0].error.contains("Missing"),
1613            "the record should name the unresolved variable: {}",
1614            skipped[0].error
1615        );
1616        assert!(nodemap.node("Bad").is_none());
1617
1618        // Everything else still works.
1619        assert!(nodemap.node("A").is_some());
1620    }
1621
1622    /// A formula our parser cannot handle costs that feature, not the camera.
1623    #[test]
1624    fn unparsable_formula_is_isolated() {
1625        const XML: &str = r#"
1626            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1627                <Integer Name="Width">
1628                    <Address>0x2000</Address>
1629                    <Length>4</Length>
1630                    <AccessMode>RW</AccessMode>
1631                    <Min>0</Min>
1632                    <Max>4096</Max>
1633                </Integer>
1634                <IntSwissKnife Name="Broken">
1635                    <Formula>Width +</Formula>
1636                    <pVariable Name="Width">Width</pVariable>
1637                </IntSwissKnife>
1638            </RegisterDescription>
1639        "#;
1640
1641        let model = viva_genapi_xml::parse(XML).expect("parse model");
1642        let nodemap = NodeMap::try_from_xml(model).expect("model builds despite the bad formula");
1643        assert_eq!(nodemap.skipped().len(), 1);
1644        assert_eq!(nodemap.skipped()[0].name.as_deref(), Some("Broken"));
1645        assert!(nodemap.node("Width").is_some());
1646    }
1647
1648    #[test]
1649    fn swissknife_division_by_zero() {
1650        let nodemap = build_swissknife_nodemap();
1651        let io = MockIo::with_registers(&[
1652            (
1653                0x3000,
1654                i64_to_bytes("GainRaw", 10, 4, Sign::Signed).unwrap(),
1655            ),
1656            (0x3008, i64_to_bytes("Offset", 0, 4, Sign::Signed).unwrap()),
1657            (0x3010, i64_to_bytes("B", 0, 4, Sign::Signed).unwrap()),
1658        ]);
1659
1660        let err = nodemap
1661            .get_float("DivideByZero", &io)
1662            .expect_err("division by zero");
1663        match err {
1664            GenApiError::ExprEval { name, msg } => {
1665                assert_eq!(name, "DivideByZero");
1666                assert_eq!(msg, "division by zero");
1667            }
1668            other => panic!("unexpected error: {other:?}"),
1669        }
1670    }
1671
1672    // -----------------------------------------------------------------------
1673    // nodes_at_visibility
1674    // -----------------------------------------------------------------------
1675
1676    const VISIBILITY_FIXTURE: &str = r#"
1677        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
1678            <Integer Name="BeginnerNode">
1679                <Address>0x6000</Address>
1680                <Length>4</Length>
1681                <AccessMode>RW</AccessMode>
1682                <Visibility>Beginner</Visibility>
1683                <Min>0</Min>
1684                <Max>100</Max>
1685            </Integer>
1686            <Integer Name="ExpertNode">
1687                <Address>0x6010</Address>
1688                <Length>4</Length>
1689                <AccessMode>RW</AccessMode>
1690                <Visibility>Expert</Visibility>
1691                <Min>0</Min>
1692                <Max>100</Max>
1693            </Integer>
1694            <Integer Name="GuruNode">
1695                <Address>0x6020</Address>
1696                <Length>4</Length>
1697                <AccessMode>RW</AccessMode>
1698                <Visibility>Guru</Visibility>
1699                <Min>0</Min>
1700                <Max>100</Max>
1701            </Integer>
1702            <Integer Name="InvisibleNode">
1703                <Address>0x6030</Address>
1704                <Length>4</Length>
1705                <AccessMode>RW</AccessMode>
1706                <Visibility>Invisible</Visibility>
1707                <Min>0</Min>
1708                <Max>100</Max>
1709            </Integer>
1710        </RegisterDescription>
1711    "#;
1712
1713    #[test]
1714    fn nodes_at_visibility_beginner_returns_only_beginner() {
1715        let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
1716        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1717
1718        let visible = nodemap.nodes_at_visibility(Visibility::Beginner);
1719        assert!(
1720            visible.contains(&"BeginnerNode"),
1721            "Beginner node must be visible at Beginner level"
1722        );
1723        assert!(
1724            !visible.contains(&"ExpertNode"),
1725            "Expert node must NOT be visible at Beginner level"
1726        );
1727        assert!(
1728            !visible.contains(&"GuruNode"),
1729            "Guru node must NOT be visible at Beginner level"
1730        );
1731        assert!(
1732            !visible.contains(&"InvisibleNode"),
1733            "Invisible node must NOT be visible at Beginner level"
1734        );
1735    }
1736
1737    #[test]
1738    fn nodes_at_visibility_guru_includes_beginner_and_expert_but_not_invisible() {
1739        let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
1740        let nodemap = NodeMap::try_from_xml(model).expect("build nodemap");
1741
1742        let visible = nodemap.nodes_at_visibility(Visibility::Guru);
1743        assert!(
1744            visible.contains(&"BeginnerNode"),
1745            "Beginner node must be visible at Guru level"
1746        );
1747        assert!(
1748            visible.contains(&"ExpertNode"),
1749            "Expert node must be visible at Guru level"
1750        );
1751        assert!(
1752            visible.contains(&"GuruNode"),
1753            "Guru node must be visible at Guru level"
1754        );
1755        assert!(
1756            !visible.contains(&"InvisibleNode"),
1757            "Invisible node must NOT be visible at Guru level"
1758        );
1759    }
1760}