1#![cfg_attr(docsrs, feature(doc_cfg))]
2mod 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 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 & 1</Formula>
367 <pVariable Name="CTRL">CtrlReg</pVariable>
368 <Output>Integer</Output>
369 </IntSwissKnife>
370 <IntSwissKnife Name="GateLocked">
371 <Formula>(CTRL & 2) / 2</Formula>
372 <pVariable Name="CTRL">CtrlReg</pVariable>
373 <Output>Integer</Output>
374 </IntSwissKnife>
375 <IntSwissKnife Name="Entry8Implemented">
376 <Formula>(CTRL & 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 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 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 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 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 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 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 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; 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 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}