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;
10mod 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    Representation, SkNode, Visibility,
18};
19pub use viva_genapi_xml::{AccessMode, SkOutput};
20
21#[cfg(test)]
22mod tests {
23    use std::cell::RefCell;
24    use std::collections::HashMap;
25
26    use crate::conversions::{bytes_to_i64, i64_to_bytes};
27    use crate::{AccessMode, GenApiError, NodeMap, RegisterIo, Visibility};
28
29    const FIXTURE: &str = r#"
30        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="2" SchemaSubMinorVersion="3">
31            <Integer Name="Width">
32                <Address>0x100</Address>
33                <Length>4</Length>
34                <AccessMode>RW</AccessMode>
35                <Min>16</Min>
36                <Max>4096</Max>
37                <Inc>2</Inc>
38            </Integer>
39            <Float Name="ExposureTime">
40                <Address>0x200</Address>
41                <Length>4</Length>
42                <AccessMode>RW</AccessMode>
43                <Min>10.0</Min>
44                <Max>100000.0</Max>
45                <Scale>1/1000</Scale>
46            </Float>
47            <Enumeration Name="GainSelector">
48                <Address>0x300</Address>
49                <Length>2</Length>
50                <AccessMode>RW</AccessMode>
51                <EnumEntry Name="All" Value="0" />
52                <EnumEntry Name="Red" Value="1" />
53                <EnumEntry Name="Blue" Value="2" />
54            </Enumeration>
55            <Integer Name="Gain">
56                <Length>2</Length>
57                <AccessMode>RW</AccessMode>
58                <Min>0</Min>
59                <Max>48</Max>
60                <pSelected>GainSelector</pSelected>
61                <Selected>All</Selected>
62                <Address>0x310</Address>
63                <Selected>Red</Selected>
64                <Address>0x314</Address>
65                <Selected>Blue</Selected>
66            </Integer>
67            <Boolean Name="GammaEnable">
68                <Address>0x400</Address>
69                <Length>1</Length>
70                <AccessMode>RW</AccessMode>
71            </Boolean>
72            <Command Name="AcquisitionStart">
73                <Address>0x500</Address>
74                <Length>4</Length>
75            </Command>
76        </RegisterDescription>
77    "#;
78
79    const INDIRECT_FIXTURE: &str = r#"
80        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
81            <Integer Name="RegAddr">
82                <Address>0x2000</Address>
83                <Length>4</Length>
84                <AccessMode>RW</AccessMode>
85                <Min>0</Min>
86                <Max>65535</Max>
87            </Integer>
88            <Integer Name="Gain">
89                <pAddress>RegAddr</pAddress>
90                <Length>4</Length>
91                <AccessMode>RW</AccessMode>
92                <Min>0</Min>
93                <Max>255</Max>
94            </Integer>
95        </RegisterDescription>
96    "#;
97
98    const ENUM_PVALUE_FIXTURE: &str = r#"
99        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
100            <Enumeration Name="Mode">
101                <Address>0x4000</Address>
102                <Length>4</Length>
103                <AccessMode>RW</AccessMode>
104                <EnumEntry Name="Fixed10">
105                    <Value>10</Value>
106                </EnumEntry>
107                <EnumEntry Name="DynFromReg">
108                    <pValue>RegModeVal</pValue>
109                </EnumEntry>
110            </Enumeration>
111            <Integer Name="RegModeVal">
112                <Address>0x4100</Address>
113                <Length>4</Length>
114                <AccessMode>RW</AccessMode>
115                <Min>0</Min>
116                <Max>65535</Max>
117            </Integer>
118        </RegisterDescription>
119    "#;
120
121    const BITFIELD_FIXTURE: &str = r#"
122        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
123            <Integer Name="LeByte">
124                <Address>0x5000</Address>
125                <Length>4</Length>
126                <AccessMode>RW</AccessMode>
127                <Min>0</Min>
128                <Max>65535</Max>
129                <Mask>0x0000FF00</Mask>
130            </Integer>
131            <Integer Name="BeBits">
132                <Address>0x5004</Address>
133                <Length>2</Length>
134                <AccessMode>RW</AccessMode>
135                <Min>0</Min>
136                <Max>15</Max>
137                <Lsb>13</Lsb>
138                <Msb>15</Msb>
139                <Endianness>BigEndian</Endianness>
140            </Integer>
141            <Boolean Name="PackedFlag">
142                <Address>0x5006</Address>
143                <Length>4</Length>
144                <AccessMode>RW</AccessMode>
145                <Bit>13</Bit>
146            </Boolean>
147        </RegisterDescription>
148    "#;
149
150    const SWISSKNIFE_FIXTURE: &str = r#"
151        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
152            <Integer Name="GainRaw">
153                <Address>0x3000</Address>
154                <Length>4</Length>
155                <AccessMode>RW</AccessMode>
156                <Min>0</Min>
157                <Max>1000</Max>
158            </Integer>
159            <Float Name="Offset">
160                <Address>0x3008</Address>
161                <Length>4</Length>
162                <AccessMode>RW</AccessMode>
163                <Min>-100.0</Min>
164                <Max>100.0</Max>
165                <Scale>1</Scale>
166            </Float>
167            <Integer Name="B">
168                <Address>0x3010</Address>
169                <Length>4</Length>
170                <AccessMode>RW</AccessMode>
171                <Min>-1000</Min>
172                <Max>1000</Max>
173            </Integer>
174            <SwissKnife Name="ComputedGain">
175                <Expression>(GainRaw * 0.5) + Offset</Expression>
176                <pVariable Name="GainRaw">GainRaw</pVariable>
177                <pVariable Name="Offset">Offset</pVariable>
178                <Output>Float</Output>
179            </SwissKnife>
180            <SwissKnife Name="DivideInt">
181                <Expression>GainRaw / 3</Expression>
182                <pVariable Name="GainRaw">GainRaw</pVariable>
183                <Output>Integer</Output>
184            </SwissKnife>
185            <SwissKnife Name="Unary">
186                <Expression>-GainRaw + 10</Expression>
187                <pVariable Name="GainRaw">GainRaw</pVariable>
188                <Output>Integer</Output>
189            </SwissKnife>
190            <SwissKnife Name="DivideByZero">
191                <Expression>GainRaw / B</Expression>
192                <pVariable Name="GainRaw">GainRaw</pVariable>
193                <pVariable Name="B">B</pVariable>
194                <Output>Float</Output>
195            </SwissKnife>
196        </RegisterDescription>
197    "#;
198
199    #[derive(Default)]
200    struct MockIo {
201        regs: RefCell<HashMap<u64, Vec<u8>>>,
202        reads: RefCell<HashMap<u64, usize>>,
203    }
204
205    impl MockIo {
206        fn with_registers(entries: &[(u64, Vec<u8>)]) -> Self {
207            let mut regs = HashMap::new();
208            for (addr, data) in entries {
209                regs.insert(*addr, data.clone());
210            }
211            MockIo {
212                regs: RefCell::new(regs),
213                reads: RefCell::new(HashMap::new()),
214            }
215        }
216
217        fn read_count(&self, addr: u64) -> usize {
218            *self.reads.borrow().get(&addr).unwrap_or(&0)
219        }
220    }
221
222    impl RegisterIo for MockIo {
223        fn read(&self, addr: u64, len: usize) -> Result<Vec<u8>, GenApiError> {
224            let mut reads = self.reads.borrow_mut();
225            *reads.entry(addr).or_default() += 1;
226            let regs = self.regs.borrow();
227            let data = regs
228                .get(&addr)
229                .ok_or_else(|| GenApiError::Io(format!("read miss at 0x{addr:08X}")))?;
230            if data.len() != len {
231                return Err(GenApiError::Io(format!(
232                    "length mismatch at 0x{addr:08X}: expected {len}, have {}",
233                    data.len()
234                )));
235            }
236            Ok(data.clone())
237        }
238
239        fn write(&self, addr: u64, data: &[u8]) -> Result<(), GenApiError> {
240            self.regs.borrow_mut().insert(addr, data.to_vec());
241            Ok(())
242        }
243    }
244
245    fn build_nodemap() -> NodeMap {
246        let model = viva_genapi_xml::parse(FIXTURE).expect("parse fixture");
247        NodeMap::from(model)
248    }
249
250    fn build_indirect_nodemap() -> NodeMap {
251        let model = viva_genapi_xml::parse(INDIRECT_FIXTURE).expect("parse indirect fixture");
252        NodeMap::from(model)
253    }
254
255    fn build_enum_pvalue_nodemap() -> NodeMap {
256        let model = viva_genapi_xml::parse(ENUM_PVALUE_FIXTURE).expect("parse enum pvalue fixture");
257        NodeMap::from(model)
258    }
259
260    fn build_bitfield_nodemap() -> NodeMap {
261        let model = viva_genapi_xml::parse(BITFIELD_FIXTURE).expect("parse bitfield fixture");
262        NodeMap::from(model)
263    }
264
265    fn build_swissknife_nodemap() -> NodeMap {
266        let model = viva_genapi_xml::parse(SWISSKNIFE_FIXTURE).expect("parse swissknife fixture");
267        NodeMap::from(model)
268    }
269
270    #[test]
271    fn integer_roundtrip_and_cache() {
272        let mut nodemap = build_nodemap();
273        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 4, 0])]);
274        let width = nodemap.get_integer("Width", &io).expect("read width");
275        assert_eq!(width, 1024);
276        assert_eq!(io.read_count(0x100), 1);
277        let width_again = nodemap.get_integer("Width", &io).expect("cached width");
278        assert_eq!(width_again, 1024);
279        assert_eq!(io.read_count(0x100), 1, "cached value should be reused");
280        nodemap
281            .set_integer("Width", 1030, &io)
282            .expect("write width");
283        let width = nodemap
284            .get_integer("Width", &io)
285            .expect("read updated width");
286        assert_eq!(width, 1030);
287        assert_eq!(io.read_count(0x100), 1, "write should update cache");
288    }
289
290    const IEEE754_FIXTURE: &str = r#"
291        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
292            <FloatReg Name="FrameRate">
293                <Address>0x100</Address>
294                <Length>4</Length>
295                <AccessMode>RW</AccessMode>
296                <Min>0.0</Min>
297                <Max>1000.0</Max>
298                <Endianess>BigEndian</Endianess>
299            </FloatReg>
300            <Float Name="ExposureUs">
301                <Address>0x110</Address>
302                <Length>8</Length>
303                <AccessMode>RW</AccessMode>
304                <Min>0.0</Min>
305                <Max>1000000.0</Max>
306                <Endianess>BigEndian</Endianess>
307            </Float>
308        </RegisterDescription>
309    "#;
310
311    fn build_ieee754_nodemap() -> NodeMap {
312        NodeMap::from(viva_genapi_xml::parse(IEEE754_FIXTURE).expect("parse ieee754"))
313    }
314
315    #[test]
316    fn float_ieee754_f32_roundtrip() {
317        let mut nodemap = build_ieee754_nodemap();
318        let io = MockIo::with_registers(&[(0x100, 30.0f32.to_be_bytes().to_vec())]);
319        let v = nodemap.get_float("FrameRate", &io).expect("read rate");
320        assert!((v - 30.0).abs() < 1e-3, "got {v}");
321
322        nodemap
323            .set_float("FrameRate", 42.5, &io)
324            .expect("write rate");
325        let raw = io.read(0x100, 4).expect("read back");
326        assert_eq!(raw, 42.5f32.to_be_bytes());
327    }
328
329    #[test]
330    fn float_ieee754_f64_heuristic_roundtrip() {
331        let mut nodemap = build_ieee754_nodemap();
332        let io = MockIo::with_registers(&[(0x110, 6000.0f64.to_be_bytes().to_vec())]);
333        let v = nodemap.get_float("ExposureUs", &io).expect("read exposure");
334        assert!((v - 6000.0).abs() < 1e-9, "got {v}");
335
336        nodemap
337            .set_float("ExposureUs", 5000.0, &io)
338            .expect("write exposure");
339        let raw = io.read(0x110, 8).expect("read back");
340        assert_eq!(raw, 5000.0f64.to_be_bytes());
341    }
342
343    #[test]
344    fn float_scaled_integer_preserved() {
345        // The classic fixture's ExposureTime uses <Scale>1/1000</Scale>,
346        // so it must stay on the scaled-integer path even after the heuristic.
347        let nodemap = build_nodemap();
348        let raw = 50_000i64;
349        let io = MockIo::with_registers(&[(0x200, i64_to_bytes("ExposureTime", raw, 4).unwrap())]);
350        let exposure = nodemap
351            .get_float("ExposureTime", &io)
352            .expect("read exposure");
353        assert!((exposure - 50.0).abs() < 1e-6);
354    }
355
356    const PREDICATE_FIXTURE: &str = r#"
357        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
358            <IntReg Name="CtrlReg">
359                <Address>0x400</Address>
360                <Length>4</Length>
361                <AccessMode>RW</AccessMode>
362                <Sign>Unsigned</Sign>
363                <Endianess>BigEndian</Endianess>
364            </IntReg>
365            <IntSwissKnife Name="GateImplemented">
366                <Formula>CTRL &amp; 1</Formula>
367                <pVariable Name="CTRL">CtrlReg</pVariable>
368                <Output>Integer</Output>
369            </IntSwissKnife>
370            <IntSwissKnife Name="GateLocked">
371                <Formula>(CTRL &amp; 2) / 2</Formula>
372                <pVariable Name="CTRL">CtrlReg</pVariable>
373                <Output>Integer</Output>
374            </IntSwissKnife>
375            <IntSwissKnife Name="Entry8Implemented">
376                <Formula>(CTRL &amp; 4) / 4</Formula>
377                <pVariable Name="CTRL">CtrlReg</pVariable>
378                <Output>Integer</Output>
379            </IntSwissKnife>
380            <Integer Name="Gated">
381                <Address>0x410</Address>
382                <Length>4</Length>
383                <AccessMode>RW</AccessMode>
384                <Min>0</Min>
385                <Max>255</Max>
386                <Sign>Unsigned</Sign>
387                <Endianess>BigEndian</Endianess>
388                <pIsImplemented>GateImplemented</pIsImplemented>
389                <pIsLocked>GateLocked</pIsLocked>
390            </Integer>
391            <Enumeration Name="PixelFormat">
392                <EnumEntry Name="Mono8"><Value>1</Value></EnumEntry>
393                <EnumEntry Name="Mono16">
394                    <Value>2</Value>
395                    <pIsImplemented>Entry8Implemented</pIsImplemented>
396                </EnumEntry>
397                <pValue>PixelFormatReg</pValue>
398            </Enumeration>
399            <IntReg Name="PixelFormatReg">
400                <Address>0x420</Address>
401                <Length>4</Length>
402                <AccessMode>RW</AccessMode>
403                <Sign>Unsigned</Sign>
404                <Endianess>BigEndian</Endianess>
405            </IntReg>
406        </RegisterDescription>
407    "#;
408
409    fn build_predicate_nodemap() -> NodeMap {
410        NodeMap::from(viva_genapi_xml::parse(PREDICATE_FIXTURE).expect("parse predicate fixture"))
411    }
412
413    fn predicate_io(ctrl: u32) -> MockIo {
414        MockIo::with_registers(&[
415            (0x400, ctrl.to_be_bytes().to_vec()),
416            (0x410, 0u32.to_be_bytes().to_vec()),
417            (0x420, 1u32.to_be_bytes().to_vec()),
418        ])
419    }
420
421    #[test]
422    fn predicate_is_implemented_defaults_true() {
423        let nodemap = build_predicate_nodemap();
424        let io = predicate_io(0);
425        // CtrlReg itself has no pIsImplemented → always implemented.
426        assert!(nodemap.is_implemented("CtrlReg", &io).unwrap());
427    }
428
429    #[test]
430    fn predicate_is_implemented_follows_gate() {
431        let nm0 = build_predicate_nodemap();
432        let io0 = predicate_io(0);
433        assert!(!nm0.is_implemented("Gated", &io0).unwrap());
434        let nm1 = build_predicate_nodemap();
435        let io1 = predicate_io(1);
436        assert!(nm1.is_implemented("Gated", &io1).unwrap());
437    }
438
439    #[test]
440    fn predicate_is_available_chains_implemented() {
441        let nm0 = build_predicate_nodemap();
442        let io0 = predicate_io(0);
443        assert!(!nm0.is_available("Gated", &io0).unwrap());
444        let nm1 = build_predicate_nodemap();
445        let io1 = predicate_io(1);
446        assert!(nm1.is_available("Gated", &io1).unwrap());
447    }
448
449    #[test]
450    fn predicate_effective_access_mode_locked_downgrade() {
451        let nodemap = build_predicate_nodemap();
452        // bit 0 set (implemented), bit 1 set (locked) → RW → RO
453        let io = predicate_io(0b11);
454        let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
455        assert_eq!(mode, AccessMode::RO);
456    }
457
458    #[test]
459    fn predicate_effective_access_mode_rw_when_unlocked() {
460        let nodemap = build_predicate_nodemap();
461        // implemented, unlocked → base RW
462        let io = predicate_io(0b01);
463        let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
464        assert_eq!(mode, AccessMode::RW);
465    }
466
467    #[test]
468    fn predicate_effective_access_mode_na_for_unavailable() {
469        let nodemap = build_predicate_nodemap();
470        // not implemented → effective access reported as RO (we don't model NA).
471        let io = predicate_io(0);
472        let mode = nodemap.effective_access_mode("Gated", &io).unwrap();
473        assert_eq!(mode, AccessMode::RO);
474    }
475
476    #[test]
477    fn predicate_available_enum_entries_filters() {
478        let nodemap = build_predicate_nodemap();
479        // bit 2 clear → Mono16 gated out; Mono8 has no predicate so it stays.
480        let io = predicate_io(0);
481        let entries = nodemap
482            .available_enum_entries("PixelFormat", &io)
483            .expect("enum entries");
484        assert_eq!(entries, vec!["Mono8".to_string()]);
485    }
486
487    #[test]
488    fn predicate_available_enum_entries_full_when_allowed() {
489        let nodemap = build_predicate_nodemap();
490        // bit 2 set → Mono16 available.
491        let io = predicate_io(0b100);
492        let mut entries = nodemap
493            .available_enum_entries("PixelFormat", &io)
494            .expect("enum entries");
495        entries.sort();
496        assert_eq!(entries, vec!["Mono16".to_string(), "Mono8".to_string()]);
497    }
498
499    #[test]
500    fn predicate_available_enum_entries_fallback_to_static() {
501        // CtrlReg itself isn't an enum; use an enum without entry predicates.
502        let xml = r#"
503            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
504                <Enumeration Name="Mode">
505                    <EnumEntry Name="A"><Value>0</Value></EnumEntry>
506                    <EnumEntry Name="B"><Value>1</Value></EnumEntry>
507                    <pValue>ModeReg</pValue>
508                </Enumeration>
509                <IntReg Name="ModeReg">
510                    <Address>0x500</Address>
511                    <Length>4</Length>
512                    <AccessMode>RW</AccessMode>
513                    <Sign>Unsigned</Sign>
514                    <Endianess>BigEndian</Endianess>
515                </IntReg>
516            </RegisterDescription>
517        "#;
518        let nodemap = NodeMap::from(viva_genapi_xml::parse(xml).unwrap());
519        let io = MockIo::with_registers(&[(0x500, 0u32.to_be_bytes().to_vec())]);
520        let mut entries = nodemap.available_enum_entries("Mode", &io).unwrap();
521        entries.sort();
522        assert_eq!(entries, vec!["A".to_string(), "B".to_string()]);
523    }
524
525    #[test]
526    fn float_conversion_roundtrip() {
527        let mut nodemap = build_nodemap();
528        let raw = 50_000i64; // 50 ms with 1/1000 scale
529        let io = MockIo::with_registers(&[(0x200, i64_to_bytes("ExposureTime", raw, 4).unwrap())]);
530        let exposure = nodemap
531            .get_float("ExposureTime", &io)
532            .expect("read exposure");
533        assert!((exposure - 50.0).abs() < 1e-6);
534        nodemap
535            .set_float("ExposureTime", 75.0, &io)
536            .expect("write exposure");
537        let raw_back = bytes_to_i64("ExposureTime", &io.read(0x200, 4).unwrap()).unwrap();
538        assert_eq!(raw_back, 75_000);
539    }
540
541    #[test]
542    fn selector_address_switching() {
543        let mut nodemap = build_nodemap();
544        let io = MockIo::with_registers(&[
545            (0x300, i64_to_bytes("GainSelector", 0, 2).unwrap()),
546            (0x310, i64_to_bytes("Gain", 10, 2).unwrap()),
547            (0x314, i64_to_bytes("Gain", 24, 2).unwrap()),
548        ]);
549
550        let gain_all = nodemap.get_integer("Gain", &io).expect("gain for All");
551        assert_eq!(gain_all, 10);
552        assert_eq!(io.read_count(0x310), 1);
553        assert_eq!(io.read_count(0x314), 0);
554
555        io.write(0x314, &i64_to_bytes("Gain", 32, 2).unwrap())
556            .expect("update red gain");
557        nodemap
558            .set_enum("GainSelector", "Red", &io)
559            .expect("set selector to red");
560        let gain_red = nodemap.get_integer("Gain", &io).expect("gain for Red");
561        assert_eq!(gain_red, 32);
562        assert_eq!(
563            io.read_count(0x310),
564            1,
565            "previous address should not be reread"
566        );
567        assert_eq!(io.read_count(0x314), 1);
568
569        let gain_red_cached = nodemap.get_integer("Gain", &io).expect("cached red");
570        assert_eq!(gain_red_cached, 32);
571        assert_eq!(io.read_count(0x314), 1, "selector cache should be reused");
572
573        nodemap
574            .set_enum("GainSelector", "Blue", &io)
575            .expect("set selector to blue");
576        let err = nodemap.get_integer("Gain", &io).unwrap_err();
577        match err {
578            GenApiError::Unavailable(msg) => {
579                assert!(msg.contains("GainSelector=Blue"));
580            }
581            other => panic!("unexpected error: {other:?}"),
582        }
583        assert_eq!(
584            io.read_count(0x314),
585            1,
586            "no read expected for missing mapping"
587        );
588
589        io.write(0x310, &i64_to_bytes("Gain", 12, 2).unwrap())
590            .expect("update all gain");
591        nodemap
592            .set_enum("GainSelector", "All", &io)
593            .expect("restore selector to all");
594        let gain_all_updated = nodemap
595            .get_integer("Gain", &io)
596            .expect("gain for All again");
597        assert_eq!(gain_all_updated, 12);
598        assert_eq!(
599            io.read_count(0x310),
600            2,
601            "address switch should invalidate cache"
602        );
603    }
604
605    #[test]
606    fn range_enforcement() {
607        let mut nodemap = build_nodemap();
608        let io = MockIo::with_registers(&[(0x100, vec![0, 0, 0, 16])]);
609        let err = nodemap.set_integer("Width", 17, &io).unwrap_err();
610        assert!(matches!(err, GenApiError::Range(_)));
611    }
612
613    #[test]
614    fn command_exec() {
615        let mut nodemap = build_nodemap();
616        let io = MockIo::with_registers(&[]);
617        nodemap
618            .exec_command("AcquisitionStart", &io)
619            .expect("exec command");
620        let payload = io.read(0x500, 4).expect("command write");
621        assert_eq!(payload, vec![0, 0, 0, 1]);
622    }
623
624    #[test]
625    fn indirect_address_resolution() {
626        let mut nodemap = build_indirect_nodemap();
627        let io = MockIo::with_registers(&[
628            (0x2000, i64_to_bytes("RegAddr", 0x3000, 4).unwrap()),
629            (0x3000, i64_to_bytes("Gain", 123, 4).unwrap()),
630            (0x3100, i64_to_bytes("Gain", 77, 4).unwrap()),
631        ]);
632
633        let initial = nodemap.get_integer("Gain", &io).expect("read gain");
634        assert_eq!(initial, 123);
635        assert_eq!(io.read_count(0x2000), 1);
636        assert_eq!(io.read_count(0x3000), 1);
637
638        nodemap
639            .set_integer("RegAddr", 0x3100, &io)
640            .expect("set indirect address");
641        let updated = nodemap
642            .get_integer("Gain", &io)
643            .expect("read gain after change");
644        assert_eq!(updated, 77);
645        assert_eq!(io.read_count(0x2000), 1);
646        assert_eq!(io.read_count(0x3000), 1);
647        assert_eq!(io.read_count(0x3100), 1);
648    }
649
650    #[test]
651    fn indirect_bad_address() {
652        let mut nodemap = build_indirect_nodemap();
653        let io = MockIo::with_registers(&[(0x2000, vec![0, 0, 0, 0])]);
654
655        nodemap
656            .set_integer("RegAddr", 0, &io)
657            .expect("write zero address");
658        let err = nodemap.get_integer("Gain", &io).unwrap_err();
659        match err {
660            GenApiError::BadIndirectAddress { name, addr } => {
661                assert_eq!(name, "Gain");
662                assert_eq!(addr, 0);
663            }
664            other => panic!("unexpected error: {other:?}"),
665        }
666        assert_eq!(io.read_count(0x2000), 0);
667    }
668
669    #[test]
670    fn enum_literal_entry_read() {
671        let nodemap = build_enum_pvalue_nodemap();
672        let io = MockIo::with_registers(&[
673            (0x4000, i64_to_bytes("Mode", 10, 4).unwrap()),
674            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
675        ]);
676
677        let value = nodemap.get_enum("Mode", &io).expect("read mode");
678        assert_eq!(value, "Fixed10");
679        assert_eq!(
680            io.read_count(0x4100),
681            1,
682            "provider should be read once for mapping"
683        );
684    }
685
686    #[test]
687    fn enum_provider_entry_read() {
688        let nodemap = build_enum_pvalue_nodemap();
689        let io = MockIo::with_registers(&[
690            (0x4000, i64_to_bytes("Mode", 42, 4).unwrap()),
691            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
692        ]);
693
694        let value = nodemap.get_enum("Mode", &io).expect("read dynamic mode");
695        assert_eq!(value, "DynFromReg");
696        assert_eq!(io.read_count(0x4100), 1);
697    }
698
699    #[test]
700    fn enum_set_uses_provider_value() {
701        let mut nodemap = build_enum_pvalue_nodemap();
702        let io = MockIo::with_registers(&[
703            (0x4000, i64_to_bytes("Mode", 0, 4).unwrap()),
704            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
705        ]);
706
707        nodemap
708            .set_enum("Mode", "DynFromReg", &io)
709            .expect("write enum");
710        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap()).unwrap();
711        assert_eq!(raw, 42);
712        assert_eq!(io.read_count(0x4100), 1);
713    }
714
715    #[test]
716    fn enum_provider_update_invalidates_mapping() {
717        let mut nodemap = build_enum_pvalue_nodemap();
718        let io = MockIo::with_registers(&[
719            (0x4000, i64_to_bytes("Mode", 42, 4).unwrap()),
720            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
721        ]);
722
723        assert_eq!(nodemap.get_enum("Mode", &io).unwrap(), "DynFromReg");
724        assert_eq!(io.read_count(0x4100), 1);
725
726        nodemap
727            .set_integer("RegModeVal", 17, &io)
728            .expect("update provider");
729        io.write(0x4000, &i64_to_bytes("Mode", 0, 4).unwrap())
730            .expect("reset mode register");
731
732        nodemap
733            .set_enum("Mode", "DynFromReg", &io)
734            .expect("write enum after provider change");
735        let raw = bytes_to_i64("Mode", &io.read(0x4000, 4).unwrap()).unwrap();
736        assert_eq!(raw, 17);
737    }
738
739    #[test]
740    fn enum_unknown_value_error() {
741        let nodemap = build_enum_pvalue_nodemap();
742        let io = MockIo::with_registers(&[
743            (0x4000, i64_to_bytes("Mode", 99, 4).unwrap()),
744            (0x4100, i64_to_bytes("RegModeVal", 42, 4).unwrap()),
745        ]);
746
747        let err = nodemap.get_enum("Mode", &io).unwrap_err();
748        match err {
749            GenApiError::EnumValueUnknown { node, value } => {
750                assert_eq!(node, "Mode");
751                assert_eq!(value, 99);
752            }
753            other => panic!("unexpected error: {other:?}"),
754        }
755    }
756
757    #[test]
758    fn enum_entries_are_sorted() {
759        let nodemap = build_enum_pvalue_nodemap();
760        let entries = nodemap.enum_entries("Mode").expect("entries");
761        assert_eq!(
762            entries,
763            vec!["DynFromReg".to_string(), "Fixed10".to_string()]
764        );
765    }
766
767    #[test]
768    fn bitfield_le_integer_roundtrip() {
769        let mut nodemap = build_bitfield_nodemap();
770        let io = MockIo::with_registers(&[(0x5000, vec![0xAA, 0xBB, 0xCC, 0xDD])]);
771
772        let value = nodemap
773            .get_integer("LeByte", &io)
774            .expect("read little-endian field");
775        assert_eq!(value, 0xBB);
776
777        nodemap
778            .set_integer("LeByte", 0x55, &io)
779            .expect("write little-endian field");
780        let data = io.read(0x5000, 4).expect("read back register");
781        assert_eq!(data, vec![0xAA, 0x55, 0xCC, 0xDD]);
782    }
783
784    #[test]
785    fn bitfield_be_integer_roundtrip() {
786        let mut nodemap = build_bitfield_nodemap();
787        let io = MockIo::with_registers(&[(0x5004, vec![0b1010_0000, 0b0000_0000])]);
788
789        let value = nodemap
790            .get_integer("BeBits", &io)
791            .expect("read big-endian bits");
792        assert_eq!(value, 0b101);
793
794        nodemap
795            .set_integer("BeBits", 0b010, &io)
796            .expect("write big-endian bits");
797        let data = io.read(0x5004, 2).expect("read back register");
798        assert_eq!(data, vec![0b0100_0000, 0b0000_0000]);
799    }
800
801    #[test]
802    fn bitfield_boolean_toggle() {
803        let mut nodemap = build_bitfield_nodemap();
804        let io = MockIo::with_registers(&[(0x5006, vec![0x00, 0x20, 0x00, 0x00])]);
805
806        assert!(nodemap.get_bool("PackedFlag", &io).expect("read flag"));
807
808        nodemap
809            .set_bool("PackedFlag", false, &io)
810            .expect("clear flag");
811        let data = io.read(0x5006, 4).expect("read cleared");
812        assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00]);
813
814        nodemap.set_bool("PackedFlag", true, &io).expect("set flag");
815        let data = io.read(0x5006, 4).expect("read set");
816        assert_eq!(data, vec![0x00, 0x20, 0x00, 0x00]);
817    }
818
819    #[test]
820    fn bitfield_value_too_wide() {
821        let mut nodemap = build_bitfield_nodemap();
822        let io = MockIo::with_registers(&[(0x5004, vec![0x00, 0x00])]);
823
824        let err = nodemap
825            .set_integer("BeBits", 8, &io)
826            .expect_err("value too wide");
827        match err {
828            GenApiError::ValueTooWide {
829                name, bit_length, ..
830            } => {
831                assert_eq!(name, "BeBits");
832                assert_eq!(bit_length, 3);
833            }
834            other => panic!("unexpected error: {other:?}"),
835        }
836    }
837    #[test]
838    fn swissknife_evaluates_and_invalidates() {
839        let mut nodemap = build_swissknife_nodemap();
840        let io = MockIo::with_registers(&[
841            (0x3000, i64_to_bytes("GainRaw", 100, 4).unwrap()),
842            (0x3008, i64_to_bytes("Offset", 3, 4).unwrap()),
843            (0x3010, i64_to_bytes("B", 1, 4).unwrap()),
844        ]);
845
846        let value = nodemap
847            .get_float("ComputedGain", &io)
848            .expect("compute gain");
849        assert!((value - 53.0).abs() < 1e-6);
850
851        nodemap
852            .set_integer("GainRaw", 120, &io)
853            .expect("update raw gain");
854        let updated = nodemap
855            .get_float("ComputedGain", &io)
856            .expect("recompute gain");
857        assert!((updated - 63.0).abs() < 1e-6);
858    }
859
860    #[test]
861    fn swissknife_integer_rounding_and_unary() {
862        let mut nodemap = build_swissknife_nodemap();
863        let io = MockIo::with_registers(&[
864            (0x3000, i64_to_bytes("GainRaw", 5, 4).unwrap()),
865            (0x3008, i64_to_bytes("Offset", 0, 4).unwrap()),
866            (0x3010, i64_to_bytes("B", 1, 4).unwrap()),
867        ]);
868
869        let divided = nodemap
870            .get_integer("DivideInt", &io)
871            .expect("integer division");
872        assert_eq!(divided, 2);
873
874        nodemap
875            .set_integer("GainRaw", 3, &io)
876            .expect("update gain raw");
877        let unary = nodemap.get_integer("Unary", &io).expect("unary expression");
878        assert_eq!(unary, 7);
879    }
880
881    #[test]
882    fn swissknife_unknown_variable_error() {
883        const XML: &str = r#"
884            <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
885                <Integer Name="A">
886                    <Address>0x2000</Address>
887                    <Length>4</Length>
888                    <AccessMode>RW</AccessMode>
889                    <Min>0</Min>
890                    <Max>100</Max>
891                </Integer>
892                <SwissKnife Name="Bad">
893                    <Expression>A + Missing</Expression>
894                    <pVariable Name="A">A</pVariable>
895                </SwissKnife>
896            </RegisterDescription>
897        "#;
898
899        let model = viva_genapi_xml::parse(XML).expect("parse invalid swissknife");
900        let err = NodeMap::try_from_xml(model).expect_err("unknown variable");
901        match err {
902            GenApiError::UnknownVariable { name, var } => {
903                assert_eq!(name, "Bad");
904                assert_eq!(var, "Missing");
905            }
906            other => panic!("unexpected error: {other:?}"),
907        }
908    }
909
910    #[test]
911    fn swissknife_division_by_zero() {
912        let nodemap = build_swissknife_nodemap();
913        let io = MockIo::with_registers(&[
914            (0x3000, i64_to_bytes("GainRaw", 10, 4).unwrap()),
915            (0x3008, i64_to_bytes("Offset", 0, 4).unwrap()),
916            (0x3010, i64_to_bytes("B", 0, 4).unwrap()),
917        ]);
918
919        let err = nodemap
920            .get_float("DivideByZero", &io)
921            .expect_err("division by zero");
922        match err {
923            GenApiError::ExprEval { name, msg } => {
924                assert_eq!(name, "DivideByZero");
925                assert_eq!(msg, "division by zero");
926            }
927            other => panic!("unexpected error: {other:?}"),
928        }
929    }
930
931    // -----------------------------------------------------------------------
932    // nodes_at_visibility
933    // -----------------------------------------------------------------------
934
935    const VISIBILITY_FIXTURE: &str = r#"
936        <RegisterDescription SchemaMajorVersion="1" SchemaMinorVersion="0" SchemaSubMinorVersion="0">
937            <Integer Name="BeginnerNode">
938                <Address>0x6000</Address>
939                <Length>4</Length>
940                <AccessMode>RW</AccessMode>
941                <Visibility>Beginner</Visibility>
942                <Min>0</Min>
943                <Max>100</Max>
944            </Integer>
945            <Integer Name="ExpertNode">
946                <Address>0x6010</Address>
947                <Length>4</Length>
948                <AccessMode>RW</AccessMode>
949                <Visibility>Expert</Visibility>
950                <Min>0</Min>
951                <Max>100</Max>
952            </Integer>
953            <Integer Name="GuruNode">
954                <Address>0x6020</Address>
955                <Length>4</Length>
956                <AccessMode>RW</AccessMode>
957                <Visibility>Guru</Visibility>
958                <Min>0</Min>
959                <Max>100</Max>
960            </Integer>
961            <Integer Name="InvisibleNode">
962                <Address>0x6030</Address>
963                <Length>4</Length>
964                <AccessMode>RW</AccessMode>
965                <Visibility>Invisible</Visibility>
966                <Min>0</Min>
967                <Max>100</Max>
968            </Integer>
969        </RegisterDescription>
970    "#;
971
972    #[test]
973    fn nodes_at_visibility_beginner_returns_only_beginner() {
974        let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
975        let nodemap = NodeMap::from(model);
976
977        let visible = nodemap.nodes_at_visibility(Visibility::Beginner);
978        assert!(
979            visible.contains(&"BeginnerNode"),
980            "Beginner node must be visible at Beginner level"
981        );
982        assert!(
983            !visible.contains(&"ExpertNode"),
984            "Expert node must NOT be visible at Beginner level"
985        );
986        assert!(
987            !visible.contains(&"GuruNode"),
988            "Guru node must NOT be visible at Beginner level"
989        );
990        assert!(
991            !visible.contains(&"InvisibleNode"),
992            "Invisible node must NOT be visible at Beginner level"
993        );
994    }
995
996    #[test]
997    fn nodes_at_visibility_guru_includes_beginner_and_expert_but_not_invisible() {
998        let model = viva_genapi_xml::parse(VISIBILITY_FIXTURE).expect("parse visibility fixture");
999        let nodemap = NodeMap::from(model);
1000
1001        let visible = nodemap.nodes_at_visibility(Visibility::Guru);
1002        assert!(
1003            visible.contains(&"BeginnerNode"),
1004            "Beginner node must be visible at Guru level"
1005        );
1006        assert!(
1007            visible.contains(&"ExpertNode"),
1008            "Expert node must be visible at Guru level"
1009        );
1010        assert!(
1011            visible.contains(&"GuruNode"),
1012            "Guru node must be visible at Guru level"
1013        );
1014        assert!(
1015            !visible.contains(&"InvisibleNode"),
1016            "Invisible node must NOT be visible at Guru level"
1017        );
1018    }
1019}