Skip to main content

viva_fake_gige/
registers.rs

1//! In-memory bootstrap register map and embedded GenApi XML.
2//!
3//! # Register Address Map
4//!
5//! | Address     | Length | Feature                         | Type     |
6//! |-------------|--------|---------------------------------|----------|
7//! | `0x0000`    | 4      | Version (RO)                    | u32 BE   |
8//! | `0x0004`    | 4      | DeviceMode (RO)                 | u32 BE   |
9//! | `0x0008`    | 4      | DeviceMACAddressHigh (RO)       | u32 BE   |
10//! | `0x000c`    | 4      | DeviceMACAddressLow (RO)        | u32 BE   |
11//! | `0x0010`    | 4      | SupportedIPConfiguration (RO)   | u32 BE   |
12//! | `0x0014`    | 4      | CurrentIPConfiguration          | u32 BE   |
13//! | `0x0024`    | 4      | CurrentIPAddress (RO)           | IPv4     |
14//! | `0x0034`    | 4      | CurrentSubnetMask (RO)          | IPv4     |
15//! | `0x0044`    | 4      | CurrentDefaultGateway (RO)      | IPv4     |
16//! | `0x0900`    | 4      | GevNumberOfMessageChannels (RO) | u32 BE   |
17//! | `0x0904`    | 4      | GevNumberOfStreamChannels (RO)  | u32 BE   |
18//! | `0x0a00`    | 4      | CCP (Control Channel Privilege) | u32 BE   |
19//! | `0x0938`    | 4      | Heartbeat Timeout               | u32 BE   |
20//! | `0x0b00`    | 4      | GevMCP (message channel port)   | u32 BE   |
21//! | `0x0b10`    | 4      | GevMCDA (message channel addr)  | u32 BE   |
22//! | `0x0d00+`   | varies | Stream Channel 0 registers      | u32 BE   |
23//! | `0x20000`   | 4      | Width                           | u32 BE   |
24//! | `0x20004`   | 4      | Height                          | u32 BE   |
25//! | `0x20008`   | 4      | PixelFormat                     | u32 BE   |
26//! | `0x2000c`   | 4      | OffsetX                         | u32 BE   |
27//! | `0x20010`   | 4      | OffsetY                         | u32 BE   |
28//! | `0x20014`   | 4      | SensorWidth (RO)                | u32 BE   |
29//! | `0x20018`   | 4      | SensorHeight (RO)               | u32 BE   |
30//! | `0x20020`   | 4      | AcquisitionMode                 | u32 BE   |
31//! | `0x20024`   | 4      | AcquisitionStart (command)      | u32 BE   |
32//! | `0x20028`   | 4      | AcquisitionStop (command)       | u32 BE   |
33//! | `0x2002c`   | 4      | AcquisitionFrameRate            | f32→u32  |
34//! | `0x20030`   | 8      | ExposureTime                    | f64 BE   |
35//! | `0x20038`   | 4      | ExposureAuto                    | u32 BE   |
36//! | `0x20040`   | 8      | Gain                            | f64 BE   |
37//! | `0x20048`   | 4      | GainAuto                        | u32 BE   |
38//! | `0x20050`   | 4      | BlackLevel                      | u32 BE   |
39//! | `0x20054`   | 4      | AcquisitionFrameRateEnable      | u32 BE   |
40//! | `0x20058`   | 4      | SensorType                      | u32 BE   |
41//! | `0x20060`   | 4      | GevTimestampTickFrequency (RO)  | u32 BE   |
42//! | `0x20068`   | 8      | GevTimestampValue (RO)          | u64 BE   |
43//! | `0x20070`   | 4      | TimestampLatch (command)        | u32 BE   |
44//! | `0x20078`   | 8      | GevIEEE1588OffsetFromMaster…    | u64 BE   |
45//! | `0x20080`   | 4      | ChunkModeActive                 | u32 BE   |
46//! | `0x20084`   | 4      | ChunkSelector                   | u32 BE   |
47//! | `0x20088`   | 4      | ChunkEnable                     | u32 BE   |
48//! | `0x200a0`   | 4      | EventSelector                   | u32 BE   |
49//! | `0x200a4`   | 4      | EventNotification (per selector)| u32 BE   |
50//! | `0x200a8`   | 4      | UserSetSelector                 | u32 BE   |
51//! | `0x200ac`   | 4      | UserSetLoad (command, pValue)   | u32 BE   |
52//! | `0x20100`   | 4      | WidthMin (RO)                   | u32 BE   |
53//! | `0x20104`   | 4      | WidthMax (RO)                   | u32 BE   |
54//! | `0x20108`   | 4      | HeightMin (RO)                  | u32 BE   |
55//! | `0x2010c`   | 4      | HeightMax (RO)                  | u32 BE   |
56//! | `0x20110`   | 8      | ChunkTimestamp_Val (RO)         | u64 **LE** |
57//! | `0x20118`   | 4      | ChunkWidth_Val (RO)             | u32 **LE** |
58//! | `0x20200`   | 32     | DeviceModelName (RO)            | string   |
59//! | `0x20220`   | 32     | DeviceVendorName (RO)           | string   |
60//! | `0x20240`   | 16     | DeviceSerialNumber (RO)         | string   |
61//! | `0x20260`   | 32     | DeviceFirmwareVersion (RO)      | string   |
62//! | `0x20280`   | 32     | DeviceID (RO)                   | string   |
63
64use std::collections::{HashMap, HashSet};
65use std::net::Ipv4Addr;
66use std::time::{Duration, Instant};
67
68use crate::gvcp_server::FAKE_MAC;
69
70/// Bootstrap register addresses (GigE Vision specification).
71pub const VERSION: u64 = 0x0000;
72pub const DEVICE_MODE: u64 = 0x0004;
73/// Top two MAC bytes, right-aligned in the 32-bit register.
74pub const DEVICE_MAC_HIGH: u64 = 0x0008;
75/// Bottom four MAC bytes.
76pub const DEVICE_MAC_LOW: u64 = 0x000C;
77pub const SUPPORTED_IP_CONFIG: u64 = 0x0010;
78pub const CURRENT_IP_CONFIG: u64 = 0x0014;
79pub const CURRENT_IP_ADDRESS: u64 = 0x0024;
80pub const CURRENT_SUBNET_MASK: u64 = 0x0034;
81pub const CURRENT_DEFAULT_GATEWAY: u64 = 0x0044;
82/// `GevNumberOfMessageChannels` — the fake implements one.
83pub const NUMBER_OF_MESSAGE_CHANNELS: u64 = 0x0900;
84/// `GevNumberOfStreamChannels` — the fake implements one.
85pub const NUMBER_OF_STREAM_CHANNELS: u64 = 0x0904;
86
87/// `DeviceMode`: big-endian (bit 31), character set 1 (UTF-8), class
88/// Transmitter (0).
89pub const DEVICE_MODE_VALUE: u32 = 0x8000_0001;
90pub const PERSISTENT_IP_ADDRESS: u64 = 0x064C;
91pub const PERSISTENT_SUBNET_MASK: u64 = 0x065C;
92pub const PERSISTENT_DEFAULT_GATEWAY: u64 = 0x066C;
93pub const CCP: u64 = 0x0a00;
94pub const HEARTBEAT_TIMEOUT: u64 = 0x0938;
95/// Message channel destination port (`GevMCP`), port in the low 16 bits.
96pub const MESSAGE_CHANNEL_PORT: u64 = 0x0b00;
97/// Message channel destination address (`GevMCDA`).
98pub const MESSAGE_CHANNEL_ADDRESS: u64 = 0x0b10;
99pub const STREAM_CHANNEL_BASE: u64 = 0x0d00;
100/// Address stride between GigE Vision stream channel register blocks.
101pub const STREAM_CHANNEL_STRIDE: u64 = 0x40;
102pub const SCP_HOST_PORT: u64 = 0x00;
103pub const SCP_PACKET_SIZE: u64 = 0x04;
104pub const SCP_PACKET_DELAY: u64 = 0x08;
105pub const SCP_DEST_ADDR: u64 = 0x18;
106
107/// First XML URL register address and length.
108pub const FIRST_URL_REG: u64 = 0x0200;
109pub const URL_REG_LEN: usize = 512;
110
111/// Address where the actual XML blob is stored in the register space.
112pub const XML_BLOB_BASE: u64 = 0x1_0000;
113
114// ── Feature register addresses ──────────────────────────────────────────────
115
116/// Image format registers.
117pub const REG_WIDTH: u64 = 0x20000;
118pub const REG_HEIGHT: u64 = 0x20004;
119pub const REG_PIXEL_FORMAT: u64 = 0x20008;
120pub const REG_OFFSET_X: u64 = 0x2000c;
121pub const REG_OFFSET_Y: u64 = 0x20010;
122pub const REG_SENSOR_WIDTH: u64 = 0x20014;
123pub const REG_SENSOR_HEIGHT: u64 = 0x20018;
124
125/// Acquisition registers.
126pub const REG_ACQ_MODE: u64 = 0x20020;
127pub const REG_ACQ_START: u64 = 0x20024;
128pub const REG_ACQ_STOP: u64 = 0x20028;
129pub const REG_ACQ_FRAME_RATE: u64 = 0x2002c;
130
131/// Analog control registers.
132pub const REG_EXPOSURE_TIME: u64 = 0x20030;
133pub const REG_EXPOSURE_AUTO: u64 = 0x20038;
134pub const REG_GAIN: u64 = 0x20040;
135pub const REG_GAIN_AUTO: u64 = 0x20048;
136pub const REG_BLACK_LEVEL: u64 = 0x20050;
137
138/// Predicate-gating registers driving realistic feature behaviour.
139///
140/// `REG_ACQ_FRAME_RATE_ENABLE` backs the SFNC `AcquisitionFrameRateEnable`
141/// Boolean and gates `AcquisitionFrameRate` via `pIsAvailable`.
142/// `REG_SENSOR_TYPE` backs a `SensorType` enumeration (Monochrome / BayerRG /
143/// Color) that gates `PixelFormat` entries via `pIsImplemented`. On real
144/// hardware `SensorType` would be read-only sensor metadata, but exposing it
145/// as RW here keeps the fake camera a configurable simulator.
146pub const REG_ACQ_FRAME_RATE_ENABLE: u64 = 0x20054;
147pub const REG_SENSOR_TYPE: u64 = 0x20058;
148
149/// Device capability inquiry bits, exposed through a `<StructReg>` whose
150/// address is `<pAddress>` (a base node) plus a fixed `<Address>` offset — the
151/// shape Point Grey, FLIR and Hikrobot use for their inquiry blocks.
152pub const REG_DEVICE_CAPS: u64 = 0x2005c;
153
154/// Which stream channel the `GevSCP*` features address.
155///
156/// Backs a `<pIndex>` term, so changing it moves those registers by
157/// [`STREAM_CHANNEL_STRIDE`].
158pub const REG_STREAM_CHANNEL_SELECTOR: u64 = 0x20090;
159
160/// Timestamp registers.
161pub const REG_TIMESTAMP_FREQ: u64 = 0x20060;
162pub const REG_TIMESTAMP_VALUE: u64 = 0x20068;
163pub const REG_TIMESTAMP_LATCH: u64 = 0x20070;
164
165/// A PTP offset-from-master, copied in shape from the FLIR BFS-PGE-31S4C-C
166/// description in the corpus (`<Length>8</Length>`, `<Sign>Unsigned</Sign>`,
167/// `<Endianess>BigEndian</Endianess>`). It is the node issue #140 was filed
168/// against, and it is declared unsigned although a clock offset is signed by
169/// nature — so its top bit is set whenever the slave leads the master. That
170/// combination is what made the node unreadable.
171pub const REG_PTP_OFFSET_LATCHED: u64 = 0x20078;
172
173/// Chunk value registers, declared **little-endian** exactly as FLIR, Point
174/// Grey and Hikrobot declare theirs. 311 plain `<IntReg>` declarations across
175/// 16 of the 38 corpus documents are of this shape; before GA-28 every one of
176/// them decoded byte-swapped, and nothing in the tree could notice.
177pub const REG_CHUNK_TIMESTAMP_LE: u64 = 0x20110;
178pub const REG_CHUNK_WIDTH_LE: u64 = 0x20118;
179
180/// Value latched into [`REG_PTP_OFFSET_LATCHED`]: the slave leading the master
181/// by 1 234 567 ns. Stored as the two's-complement bit pattern a camera would
182/// put on the wire, so the test asserts the bytes and the decoded value
183/// separately rather than round-tripping through our own codec.
184pub const PTP_OFFSET_NS: i64 = -1_234_567;
185
186/// Value latched into [`REG_CHUNK_TIMESTAMP_LE`]. Chosen so that reading it in
187/// the wrong byte order yields a plausible positive number
188/// (`0x7856_3412_0000_0000`) rather than an error — a byte-order defect that
189/// announces itself is not the one that shipped.
190pub const CHUNK_TIMESTAMP_TICKS: u64 = 0x0000_0000_1234_5678;
191
192/// Value latched into [`REG_CHUNK_WIDTH_LE`]. Most of the 311 are 4 bytes
193/// wide, so the common width gets its own node.
194pub const CHUNK_WIDTH_PX: u32 = 1440;
195
196/// Chunk data registers.
197pub const REG_CHUNK_MODE_ACTIVE: u64 = 0x20080;
198pub const REG_CHUNK_SELECTOR: u64 = 0x20084;
199pub const REG_CHUNK_ENABLE: u64 = 0x20088;
200
201/// `EventSelector` backing register: a GigE Vision event identifier.
202/// `REG_FEATURE_STATUS` is a FLIR-shaped feature-status word: one big-endian
203/// register whose individual bits say whether a feature is implemented,
204/// available and locked, addressed by `<MaskedIntReg>` + `<Bit>`.
205///
206/// It exists so the fake can disagree with us about bit numbering. Every other
207/// predicate here is an `<IntSwissKnife>` or a `<StructEntry>`, and both of
208/// those took the code path that was already correct — so the whole suite
209/// passed while `<MaskedIntReg>` read big-endian registers off the wrong end on
210/// real hardware (issue #120). GenICam counts `<Bit>` from the MSB on a
211/// big-endian register, so bit 0 is `0x8000_0000`.
212pub const REG_FEATURE_STATUS: u64 = 0x2009c;
213
214pub const REG_EVENT_SELECTOR: u64 = 0x200a0;
215/// `EventNotification` backing register for the selected event (0 = Off, 1 = On).
216///
217/// A *selected* feature: reads and writes apply to whichever event
218/// [`REG_EVENT_SELECTOR`] currently names, so the register is backed by a set
219/// of enabled event ids rather than by one stored word. Query it with
220/// [`RegisterMap::event_notification_on`].
221pub const REG_EVENT_NOTIFICATION: u64 = 0x200a4;
222
223/// `REG_USER_SET_SELECTOR` and `REG_USER_SET_LOAD` back the SFNC user-set
224/// features, so the fake can answer the workflow issue #121 was filed about.
225///
226/// `UserSetLoad` is also the fake's first command that reaches its register
227/// through `<pValue>` rather than a bare `<Address>`. That matters for backlog
228/// `GA-10`: all 432 `<Command>` nodes in the vendor corpus use `<pValue>`, and
229/// until now all three of the fake's used the direct-address path — so our
230/// integration tests exercised only the path no real camera takes.
231/// Exposure the fake boots with, and returns to on `UserSetLoad`.
232pub const DEFAULT_EXPOSURE_US: f64 = 5000.0;
233
234pub const REG_USER_SET_SELECTOR: u64 = 0x200a8;
235/// See [`REG_USER_SET_SELECTOR`].
236pub const REG_USER_SET_LOAD: u64 = 0x200ac;
237
238/// Limit registers.
239pub const REG_WIDTH_MIN: u64 = 0x20100;
240pub const REG_WIDTH_MAX: u64 = 0x20104;
241pub const REG_HEIGHT_MIN: u64 = 0x20108;
242pub const REG_HEIGHT_MAX: u64 = 0x2010c;
243
244/// Device info string registers.
245pub const REG_DEVICE_MODEL_NAME: u64 = 0x20200;
246pub const REG_DEVICE_VENDOR_NAME: u64 = 0x20220;
247pub const REG_DEVICE_SERIAL_NUMBER: u64 = 0x20240;
248pub const REG_DEVICE_FIRMWARE_VERSION: u64 = 0x20260;
249pub const REG_DEVICE_ID: u64 = 0x20280;
250
251// ── GenApi XML ──────────────────────────────────────────────────────────────
252
253/// GenApi XML describing a realistic fake camera following SFNC conventions.
254///
255/// The XML is organized with proper SFNC category hierarchy:
256///
257/// ```text
258/// Root
259/// ├── DeviceControl        — model name, vendor, serial, firmware, device ID
260/// ├── ImageFormatControl   — width, height, offset, pixel format, sensor size
261/// ├── AcquisitionControl   — start/stop, mode, frame rate, exposure, auto
262/// ├── AnalogControl        — gain, gain auto, black level
263/// ├── TransportLayerControl — timestamp tick frequency, value, latch
264/// └── ChunkDataControl     — chunk mode, selector, enable
265/// ```
266///
267/// All feature registers use big-endian byte order. Register addresses are
268/// documented in the module-level doc comment.
269pub const FAKE_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
270<RegisterDescription
271  ModelName="VivaCam Fake"
272  VendorName="vitavision.dev"
273  ToolTip="Simulated GigE Vision camera for testing"
274  StandardNameSpace="GEV"
275  SchemaMajorVersion="1"
276  SchemaMinorVersion="1"
277  SchemaSubMinorVersion="0"
278  MajorVersion="1"
279  MinorVersion="0"
280  SubMinorVersion="0"
281  ProductGuid="76697661-6361-6d00-0000-000000000000"
282  VersionGuid="76697661-6361-6d00-0000-000000000001">
283
284  <!-- ════════════════════════════════════════════════════════════════════
285       Category Hierarchy (SFNC Standard)
286       ════════════════════════════════════════════════════════════════════ -->
287
288  <Category Name="Root" NameSpace="Standard">
289    <pFeature>DeviceControl</pFeature>
290    <pFeature>ImageFormatControl</pFeature>
291    <pFeature>AcquisitionControl</pFeature>
292    <pFeature>AnalogControl</pFeature>
293    <pFeature>TransportLayerControl</pFeature>
294    <pFeature>ChunkDataControl</pFeature>
295    <pFeature>UserSetControl</pFeature>
296  </Category>
297
298  <Category Name="UserSetControl">
299    <DisplayName>User Set Control</DisplayName>
300    <pFeature>UserSetSelector</pFeature>
301    <pFeature>UserSetLoad</pFeature>
302  </Category>
303
304  <Category Name="DeviceControl">
305    <DisplayName>Device Control</DisplayName>
306    <pFeature>DeviceVendorName</pFeature>
307    <pFeature>DeviceModelName</pFeature>
308    <pFeature>DeviceSerialNumber</pFeature>
309    <pFeature>DeviceFirmwareVersion</pFeature>
310    <pFeature>DeviceID</pFeature>
311  </Category>
312
313  <Category Name="ImageFormatControl">
314    <DisplayName>Image Format Control</DisplayName>
315    <pFeature>SensorType</pFeature>
316    <pFeature>SensorWidth</pFeature>
317    <pFeature>SensorHeight</pFeature>
318    <pFeature>Width</pFeature>
319    <pFeature>Height</pFeature>
320    <pFeature>OffsetX</pFeature>
321    <pFeature>OffsetY</pFeature>
322    <pFeature>PixelFormat</pFeature>
323  </Category>
324
325  <Category Name="AcquisitionControl">
326    <DisplayName>Acquisition Control</DisplayName>
327    <pFeature>AcquisitionMode</pFeature>
328    <pFeature>AcquisitionStart</pFeature>
329    <pFeature>AcquisitionStop</pFeature>
330    <pFeature>AcquisitionFrameRateEnable</pFeature>
331    <pFeature>AcquisitionFrameRate</pFeature>
332    <pFeature>ExposureTime</pFeature>
333    <pFeature>ExposureAuto</pFeature>
334  </Category>
335
336  <Category Name="AnalogControl">
337    <DisplayName>Analog Control</DisplayName>
338    <pFeature>Gain</pFeature>
339    <pFeature>GainAuto</pFeature>
340    <pFeature>BlackLevel</pFeature>
341  </Category>
342
343  <Category Name="TransportLayerControl">
344    <DisplayName>Transport Layer Control</DisplayName>
345    <pFeature>GevTimestampTickFrequency</pFeature>
346    <pFeature>GevTimestampValue</pFeature>
347    <pFeature>TimestampLatch</pFeature>
348    <pFeature>GevIEEE1588OffsetFromMasterLatched_Val</pFeature>
349    <pFeature>ChunkTimestamp_Val</pFeature>
350    <pFeature>ChunkWidth_Val</pFeature>
351  </Category>
352
353  <Category Name="ChunkDataControl">
354    <DisplayName>Chunk Data Control</DisplayName>
355    <pFeature>ChunkModeActive</pFeature>
356    <pFeature>ChunkSelector</pFeature>
357    <pFeature>ChunkEnable</pFeature>
358  </Category>
359
360  <Category Name="EventControl">
361    <DisplayName>Event Control</DisplayName>
362    <pFeature>EventSelector</pFeature>
363    <pFeature>EventNotification</pFeature>
364  </Category>
365
366  <!-- ════════════════════════════════════════════════════════════════════
367       Device Control Features
368       ════════════════════════════════════════════════════════════════════ -->
369
370  <String Name="DeviceVendorName" NameSpace="Standard">
371    <ToolTip>Name of the device vendor</ToolTip>
372    <Address>0x20220</Address>
373    <Length>32</Length>
374    <AccessMode>RO</AccessMode>
375  </String>
376
377  <String Name="DeviceModelName" NameSpace="Standard">
378    <ToolTip>Name of the device model</ToolTip>
379    <Address>0x20200</Address>
380    <Length>32</Length>
381    <AccessMode>RO</AccessMode>
382  </String>
383
384  <String Name="DeviceSerialNumber" NameSpace="Standard">
385    <ToolTip>Serial number of the device</ToolTip>
386    <Address>0x20240</Address>
387    <Length>16</Length>
388    <AccessMode>RO</AccessMode>
389  </String>
390
391  <String Name="DeviceFirmwareVersion" NameSpace="Standard">
392    <ToolTip>Firmware version of the device</ToolTip>
393    <Address>0x20260</Address>
394    <Length>32</Length>
395    <AccessMode>RO</AccessMode>
396  </String>
397
398  <String Name="DeviceID" NameSpace="Standard">
399    <ToolTip>User-configurable device identifier</ToolTip>
400    <Address>0x20280</Address>
401    <Length>32</Length>
402    <AccessMode>RO</AccessMode>
403  </String>
404
405  <!-- ════════════════════════════════════════════════════════════════════
406       Image Format Control Features
407       ════════════════════════════════════════════════════════════════════ -->
408
409  <Integer Name="SensorWidth" NameSpace="Standard">
410    <ToolTip>Physical sensor width in pixels</ToolTip>
411    <Address>0x20014</Address>
412    <Length>4</Length>
413    <AccessMode>RO</AccessMode>
414    <Min>1</Min>
415    <Max>4096</Max>
416    <Sign>Unsigned</Sign>
417    <Endianess>BigEndian</Endianess>
418  </Integer>
419
420  <Integer Name="SensorHeight" NameSpace="Standard">
421    <ToolTip>Physical sensor height in pixels</ToolTip>
422    <Address>0x20018</Address>
423    <Length>4</Length>
424    <AccessMode>RO</AccessMode>
425    <Min>1</Min>
426    <Max>4096</Max>
427    <Sign>Unsigned</Sign>
428    <Endianess>BigEndian</Endianess>
429  </Integer>
430
431  <Integer Name="Width" NameSpace="Standard">
432    <ToolTip>Width of the image in pixels</ToolTip>
433    <Address>0x20000</Address>
434    <Length>4</Length>
435    <AccessMode>RW</AccessMode>
436    <pMin>WidthMin</pMin>
437    <pMax>WidthMax</pMax>
438    <Sign>Unsigned</Sign>
439    <Endianess>BigEndian</Endianess>
440  </Integer>
441  <IntReg Name="WidthMin"><Address>0x20100</Address><Length>4</Length><AccessMode>RO</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
442  <IntReg Name="WidthMax"><Address>0x20104</Address><Length>4</Length><AccessMode>RO</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
443
444  <Integer Name="Height" NameSpace="Standard">
445    <ToolTip>Height of the image in pixels</ToolTip>
446    <Address>0x20004</Address>
447    <Length>4</Length>
448    <AccessMode>RW</AccessMode>
449    <pMin>HeightMin</pMin>
450    <pMax>HeightMax</pMax>
451    <Sign>Unsigned</Sign>
452    <Endianess>BigEndian</Endianess>
453  </Integer>
454  <IntReg Name="HeightMin"><Address>0x20108</Address><Length>4</Length><AccessMode>RO</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
455  <IntReg Name="HeightMax"><Address>0x2010c</Address><Length>4</Length><AccessMode>RO</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
456
457  <Integer Name="OffsetX" NameSpace="Standard">
458    <ToolTip>Horizontal offset from the sensor origin</ToolTip>
459    <Address>0x2000c</Address>
460    <Length>4</Length>
461    <AccessMode>RW</AccessMode>
462    <Min>0</Min>
463    <Max>4096</Max>
464    <Sign>Unsigned</Sign>
465    <Endianess>BigEndian</Endianess>
466  </Integer>
467
468  <Integer Name="OffsetY" NameSpace="Standard">
469    <ToolTip>Vertical offset from the sensor origin</ToolTip>
470    <Address>0x20010</Address>
471    <Length>4</Length>
472    <AccessMode>RW</AccessMode>
473    <Min>0</Min>
474    <Max>4096</Max>
475    <Sign>Unsigned</Sign>
476    <Endianess>BigEndian</Endianess>
477  </Integer>
478
479  <Enumeration Name="SensorType" NameSpace="Standard">
480    <ToolTip>Sensor variant (Monochrome / BayerRG / Color). On real hardware this would be read-only sensor metadata; here it is RW so tests can reconfigure the simulator.</ToolTip>
481    <EnumEntry Name="Monochrome"><Value>0</Value></EnumEntry>
482    <EnumEntry Name="BayerRG"><Value>1</Value></EnumEntry>
483    <EnumEntry Name="Color"><Value>2</Value></EnumEntry>
484    <pValue>SensorTypeReg</pValue>
485  </Enumeration>
486  <IntReg Name="SensorTypeReg"><Address>0x20058</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
487
488  <Enumeration Name="PixelFormat" NameSpace="Standard">
489    <ToolTip>Format of the pixel data — entries are gated by SensorType</ToolTip>
490    <EnumEntry Name="Mono8" NameSpace="Standard">
491      <Value>0x01080001</Value>
492      <pIsImplemented>PfMono8Avail</pIsImplemented>
493    </EnumEntry>
494    <EnumEntry Name="Mono16" NameSpace="Standard">
495      <Value>0x01100007</Value>
496      <pIsImplemented>PfMono16Avail</pIsImplemented>
497    </EnumEntry>
498    <EnumEntry Name="RGB8" NameSpace="Standard">
499      <Value>0x02180014</Value>
500      <pIsImplemented>PfRGB8Avail</pIsImplemented>
501    </EnumEntry>
502    <EnumEntry Name="BayerRG8" NameSpace="Standard">
503      <Value>0x01080009</Value>
504      <pIsImplemented>PfBayerRG8Avail</pIsImplemented>
505    </EnumEntry>
506    <pValue>PixelFormatReg</pValue>
507  </Enumeration>
508  <IntReg Name="PixelFormatReg"><Address>0x20008</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
509
510  <!-- PixelFormat entry availability driven by SensorType:
511         Monochrome (0) → Mono8, Mono16
512         BayerRG    (1) → BayerRG8
513         Color      (2) → RGB8 -->
514  <IntSwissKnife Name="PfMono8Avail">
515    <Formula>ST = 0</Formula>
516    <pVariable Name="ST">SensorTypeReg</pVariable>
517  </IntSwissKnife>
518  <IntSwissKnife Name="PfMono16Avail">
519    <Formula>ST = 0</Formula>
520    <pVariable Name="ST">SensorTypeReg</pVariable>
521  </IntSwissKnife>
522  <IntSwissKnife Name="PfBayerRG8Avail">
523    <Formula>ST = 1</Formula>
524    <pVariable Name="ST">SensorTypeReg</pVariable>
525  </IntSwissKnife>
526  <IntSwissKnife Name="PfRGB8Avail">
527    <Formula>ST = 2</Formula>
528    <pVariable Name="ST">SensorTypeReg</pVariable>
529  </IntSwissKnife>
530
531  <!-- ════════════════════════════════════════════════════════════════════
532       Device capability inquiry bits
533
534       Real inquiry blocks are addressed as `<pAddress>` (the block base) plus
535       a fixed `<Address>` offset, and the individual bits come from a
536       <StructReg>. Both terms contribute: keeping only one reads the wrong
537       register, which is what made issue #35's camera misreport every
538       capability it had.
539       ════════════════════════════════════════════════════════════════════ -->
540  <IntSwissKnife Name="DeviceRegBaseAddress">
541    <Formula>0x20000</Formula>
542  </IntSwissKnife>
543  <StructReg Comment="Device capability inquiry">
544    <pAddress>DeviceRegBaseAddress</pAddress>
545    <Address>0x5C</Address>
546    <Length>4</Length>
547    <AccessMode>RO</AccessMode>
548    <Endianess>BigEndian</Endianess>
549    <StructEntry Name="FrameRateControlInq_Bit"><Bit>0</Bit></StructEntry>
550    <StructEntry Name="ChunkSupportInq_Bit"><Bit>1</Bit></StructEntry>
551    <StructEntry Name="SequencerInq_Bit"><Bit>2</Bit></StructEntry>
552  </StructReg>
553
554  <!-- ════════════════════════════════════════════════════════════════════
555       GigE Vision stream channel registers
556
557       Stream channel N lives at 0x0D00 + N * 0x40, so the packet size
558       register is a fixed <Address> plus a <pIndex> scaled by the channel
559       stride. Ignoring the <pIndex> term always addresses channel 0.
560       ════════════════════════════════════════════════════════════════════ -->
561  <Integer Name="GevStreamChannelSelector" NameSpace="Standard">
562    <ToolTip>Stream channel the GevSCP* features address</ToolTip>
563    <Address>0x20090</Address>
564    <Length>4</Length>
565    <AccessMode>RW</AccessMode>
566    <Sign>Unsigned</Sign>
567    <Min>0</Min>
568    <Max>1</Max>
569    <Endianess>BigEndian</Endianess>
570  </Integer>
571  <Integer Name="GevSCPSPacketSize" NameSpace="Standard">
572    <ToolTip>Stream channel packet size in bytes</ToolTip>
573    <Address>0x0D04</Address>
574    <pIndex Offset="0x40">GevStreamChannelSelector</pIndex>
575    <Length>4</Length>
576    <AccessMode>RW</AccessMode>
577    <Sign>Unsigned</Sign>
578    <Min>0</Min>
579    <Max>65535</Max>
580    <Endianess>BigEndian</Endianess>
581  </Integer>
582
583  <!-- ════════════════════════════════════════════════════════════════════
584       Acquisition Control Features
585       ════════════════════════════════════════════════════════════════════ -->
586
587  <Enumeration Name="AcquisitionMode" NameSpace="Standard">
588    <ToolTip>Camera acquisition mode</ToolTip>
589    <EnumEntry Name="Continuous"><Value>0</Value></EnumEntry>
590    <EnumEntry Name="SingleFrame"><Value>1</Value></EnumEntry>
591    <EnumEntry Name="MultiFrame"><Value>2</Value></EnumEntry>
592    <pValue>AcquisitionModeReg</pValue>
593  </Enumeration>
594  <IntReg Name="AcquisitionModeReg"><Address>0x20020</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
595
596  <Command Name="AcquisitionStart" NameSpace="Standard">
597    <ToolTip>Start image acquisition</ToolTip>
598    <Address>0x20024</Address>
599    <Length>4</Length>
600    <AccessMode>WO</AccessMode>
601    <CommandValue>1</CommandValue>
602    <Endianess>BigEndian</Endianess>
603  </Command>
604
605  <Enumeration Name="UserSetSelector" NameSpace="Standard">
606    <ToolTip>User set that UserSetLoad restores</ToolTip>
607    <EnumEntry Name="Default"><Value>0</Value></EnumEntry>
608    <EnumEntry Name="UserSet0"><Value>1</Value></EnumEntry>
609    <pValue>UserSetSelectorReg</pValue>
610  </Enumeration>
611  <IntReg Name="UserSetSelectorReg"><Address>0x200a8</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
612
613  <!-- Reaches its register through <pValue>, unlike the three commands above.
614       That is the shape every <Command> in the vendor corpus uses, and the one
615       our tests had no example of (backlog GA-10). -->
616  <Command Name="UserSetLoad" NameSpace="Standard">
617    <ToolTip>Restore the selected user set</ToolTip>
618    <pValue>UserSetLoadReg</pValue>
619    <CommandValue>1</CommandValue>
620  </Command>
621  <IntReg Name="UserSetLoadReg"><Address>0x200ac</Address><Length>4</Length><AccessMode>WO</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
622
623  <Command Name="AcquisitionStop" NameSpace="Standard">
624    <ToolTip>Stop image acquisition</ToolTip>
625    <Address>0x20028</Address>
626    <Length>4</Length>
627    <AccessMode>WO</AccessMode>
628    <CommandValue>1</CommandValue>
629    <Endianess>BigEndian</Endianess>
630  </Command>
631
632  <Boolean Name="AcquisitionFrameRateEnable" NameSpace="Standard">
633    <ToolTip>Enable manual control of AcquisitionFrameRate. When false, the frame rate is unavailable for read/write.</ToolTip>
634    <pValue>AcquisitionFrameRateEnableReg</pValue>
635  </Boolean>
636  <IntReg Name="AcquisitionFrameRateEnableReg"><Address>0x20054</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess>
637    <pIsImplemented>FrameRateControlInq_Bit</pIsImplemented>
638  </IntReg>
639
640  <Float Name="AcquisitionFrameRate" NameSpace="Standard">
641    <ToolTip>Target frame rate in Hz</ToolTip>
642    <Address>0x2002c</Address>
643    <Length>4</Length>
644    <AccessMode>RW</AccessMode>
645    <Min>1.0</Min>
646    <Max>120.0</Max>
647    <Endianess>BigEndian</Endianess>
648    <pIsAvailable>AcquisitionFrameRateEnable</pIsAvailable>
649  </Float>
650
651  <Float Name="ExposureTime" NameSpace="Standard">
652    <ToolTip>Exposure time in microseconds — locked to RO when ExposureAuto is not Off</ToolTip>
653    <Address>0x20030</Address>
654    <Length>8</Length>
655    <AccessMode>RW</AccessMode>
656    <Min>10.0</Min>
657    <Max>1000000.0</Max>
658    <Endianess>BigEndian</Endianess>
659    <pIsImplemented>ExposureTime_Imp</pIsImplemented>
660    <pIsAvailable>ExposureTime_Avl</pIsAvailable>
661    <pIsLocked>ExposureAutoActive</pIsLocked>
662  </Float>
663
664  <!-- Feature-status bits in one big-endian word, the shape FLIR ships and the
665       shape that exposed issue #120. GenICam counts <Bit> from the MSB here, so
666       bit 0 is 0x80000000 and bit 1 is 0x40000000; the fake boots this register
667       to 0xC0000000. Read from the wrong end these both come out zero and
668       ExposureTime becomes unavailable, which is what the reporter saw. -->
669  <MaskedIntReg Name="ExposureTime_Imp">
670    <Address>0x2009c</Address><Length>4</Length><AccessMode>RO</AccessMode>
671    <Bit>0</Bit><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess>
672  </MaskedIntReg>
673  <MaskedIntReg Name="ExposureTime_Avl">
674    <Address>0x2009c</Address><Length>4</Length><AccessMode>RO</AccessMode>
675    <Bit>1</Bit><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess>
676  </MaskedIntReg>
677
678  <Enumeration Name="ExposureAuto" NameSpace="Standard">
679    <ToolTip>Automatic exposure control</ToolTip>
680    <EnumEntry Name="Off"><Value>0</Value></EnumEntry>
681    <EnumEntry Name="Once"><Value>1</Value></EnumEntry>
682    <EnumEntry Name="Continuous"><Value>2</Value></EnumEntry>
683    <pValue>ExposureAutoReg</pValue>
684  </Enumeration>
685  <IntReg Name="ExposureAutoReg"><Address>0x20038</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
686
687  <IntSwissKnife Name="ExposureAutoActive">
688    <Formula>EA &lt;&gt; 0</Formula>
689    <pVariable Name="EA">ExposureAutoReg</pVariable>
690  </IntSwissKnife>
691
692  <!-- ════════════════════════════════════════════════════════════════════
693       Analog Control Features
694       ════════════════════════════════════════════════════════════════════ -->
695
696  <Float Name="Gain" NameSpace="Standard">
697    <!-- CDATA-wrapped tooltip, as shipped by several vendors: the literal `&`
698         and `<` inside are legal here and must survive parsing (issue #45). -->
699    <ToolTip><![CDATA[Gain applied to the image in dB — locked to RO when GainAuto is not Off (0 < gain & gain < 48)]]></ToolTip>
700    <Address>0x20040</Address>
701    <Length>8</Length>
702    <AccessMode>RW</AccessMode>
703    <Min>0.0</Min>
704    <Max>48.0</Max>
705    <Endianess>BigEndian</Endianess>
706    <pIsLocked>GainAutoActive</pIsLocked>
707  </Float>
708
709  <Enumeration Name="GainAuto" NameSpace="Standard">
710    <ToolTip>Automatic gain control</ToolTip>
711    <EnumEntry Name="Off"><Value>0</Value></EnumEntry>
712    <EnumEntry Name="Once"><Value>1</Value></EnumEntry>
713    <EnumEntry Name="Continuous"><Value>2</Value></EnumEntry>
714    <pValue>GainAutoReg</pValue>
715  </Enumeration>
716  <IntReg Name="GainAutoReg"><Address>0x20048</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
717
718  <IntSwissKnife Name="GainAutoActive">
719    <Formula>GA &lt;&gt; 0</Formula>
720    <pVariable Name="GA">GainAutoReg</pVariable>
721  </IntSwissKnife>
722
723  <Integer Name="BlackLevel" NameSpace="Standard">
724    <ToolTip>Analog black level offset</ToolTip>
725    <Address>0x20050</Address>
726    <Length>4</Length>
727    <AccessMode>RW</AccessMode>
728    <Min>0</Min>
729    <Max>255</Max>
730    <Sign>Unsigned</Sign>
731    <Endianess>BigEndian</Endianess>
732  </Integer>
733
734  <!-- ════════════════════════════════════════════════════════════════════
735       Transport Layer Control (Timestamp)
736       ════════════════════════════════════════════════════════════════════ -->
737
738  <Integer Name="GevTimestampTickFrequency" NameSpace="Standard">
739    <ToolTip>Device timestamp tick frequency in Hz (1 GHz)</ToolTip>
740    <Address>0x20060</Address>
741    <Length>4</Length>
742    <AccessMode>RO</AccessMode>
743    <Sign>Unsigned</Sign>
744    <Endianess>BigEndian</Endianess>
745  </Integer>
746
747  <Integer Name="GevTimestampValue" NameSpace="Standard">
748    <ToolTip>Current device timestamp in ticks (latched)</ToolTip>
749    <Address>0x20068</Address>
750    <Length>8</Length>
751    <AccessMode>RO</AccessMode>
752    <Sign>Unsigned</Sign>
753    <Endianess>BigEndian</Endianess>
754  </Integer>
755
756  <IntReg Name="GevIEEE1588OffsetFromMasterLatched_Val" NameSpace="Custom">
757    <ToolTip>PTP offset from master in ns, latched</ToolTip>
758    <Address>0x20078</Address>
759    <Length>8</Length>
760    <AccessMode>RO</AccessMode>
761    <Sign>Unsigned</Sign>
762    <Endianess>BigEndian</Endianess>
763  </IntReg>
764
765  <IntReg Name="ChunkTimestamp_Val" NameSpace="Custom">
766    <ToolTip>Chunk timestamp in ticks</ToolTip>
767    <Address>0x20110</Address>
768    <Length>8</Length>
769    <AccessMode>RO</AccessMode>
770    <Sign>Unsigned</Sign>
771    <Endianess>LittleEndian</Endianess>
772  </IntReg>
773
774  <IntReg Name="ChunkWidth_Val" NameSpace="Custom">
775    <ToolTip>Chunk image width in pixels</ToolTip>
776    <Address>0x20118</Address>
777    <Length>4</Length>
778    <AccessMode>RO</AccessMode>
779    <Sign>Unsigned</Sign>
780    <Endianess>LittleEndian</Endianess>
781  </IntReg>
782
783  <Command Name="TimestampLatch" NameSpace="Standard">
784    <ToolTip>Latch the current timestamp into GevTimestampValue</ToolTip>
785    <Address>0x20070</Address>
786    <Length>4</Length>
787    <AccessMode>WO</AccessMode>
788    <CommandValue>1</CommandValue>
789    <Endianess>BigEndian</Endianess>
790  </Command>
791
792  <!-- ════════════════════════════════════════════════════════════════════
793       Chunk Data Control
794       ════════════════════════════════════════════════════════════════════ -->
795
796  <!-- SFNC defines ChunkModeActive and ChunkEnable as IBoolean, and all 23
797       vendor-corpus documents that declare ChunkModeActive use <Boolean> over
798       a backing <IntReg> - none uses <Integer>. Declaring them as <Integer>
799       here made Camera::configure_chunks, which correctly calls set_bool,
800       fail against the only camera we can test with. -->
801  <Boolean Name="ChunkModeActive" NameSpace="Standard">
802    <ToolTip>Enable chunk data in image frames</ToolTip>
803    <pValue>ChunkModeActiveReg</pValue>
804  </Boolean>
805  <IntReg Name="ChunkModeActiveReg"><Address>0x20080</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
806
807  <Enumeration Name="ChunkSelector" NameSpace="Standard">
808    <ToolTip>Select which chunk feature to configure</ToolTip>
809    <EnumEntry Name="Timestamp"><Value>1</Value></EnumEntry>
810    <EnumEntry Name="ExposureTime"><Value>2</Value></EnumEntry>
811    <EnumEntry Name="Gain"><Value>3</Value></EnumEntry>
812    <pValue>ChunkSelectorReg</pValue>
813  </Enumeration>
814  <IntReg Name="ChunkSelectorReg"><Address>0x20084</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
815
816  <Boolean Name="ChunkEnable" NameSpace="Standard">
817    <ToolTip>Enable the selected chunk feature</ToolTip>
818    <pValue>ChunkEnableReg</pValue>
819  </Boolean>
820  <IntReg Name="ChunkEnableReg"><Address>0x20088</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
821
822  <!-- Event delivery is selected through GenApi, not through a bootstrap
823       register: EventSelector picks a GigE Vision event id and
824       EventNotification turns it on. The ids are the standard ones
825       (GEV_EVENT_START_OF_TRANSFER = 0x0005, END_OF_TRANSFER = 0x0006). -->
826  <Enumeration Name="EventSelector" NameSpace="Standard">
827    <ToolTip>Select which event to configure</ToolTip>
828    <EnumEntry Name="StartOfTransfer"><Value>5</Value></EnumEntry>
829    <EnumEntry Name="EndOfTransfer"><Value>6</Value></EnumEntry>
830    <pValue>EventSelectorReg</pValue>
831  </Enumeration>
832  <IntReg Name="EventSelectorReg"><Address>0x200A0</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
833
834  <Enumeration Name="EventNotification" NameSpace="Standard">
835    <ToolTip>Enable notification for the selected event</ToolTip>
836    <EnumEntry Name="Off"><Value>0</Value></EnumEntry>
837    <EnumEntry Name="On"><Value>1</Value></EnumEntry>
838    <pValue>EventNotificationReg</pValue>
839  </Enumeration>
840  <IntReg Name="EventNotificationReg"><Address>0x200A4</Address><Length>4</Length><AccessMode>RW</AccessMode><Sign>Unsigned</Sign><Endianess>BigEndian</Endianess></IntReg>
841
842</RegisterDescription>
843"#;
844
845// ── Register Map ────────────────────────────────────────────────────────────
846
847/// Pre-populated register store for the fake camera.
848///
849/// All feature registers are initialized with realistic defaults.
850/// The register map is thread-safe via external `Mutex` wrapping.
851pub struct RegisterMap {
852    regs: HashMap<u64, Vec<u8>>,
853    xml_blob: Vec<u8>,
854    clock_origin: Instant,
855    /// Event identifiers whose `EventNotification` is `On`.
856    ///
857    /// One word cannot hold this. `EventSelector`/`EventNotification` is a
858    /// selector pair, so a controller enabling two events writes the same
859    /// address twice with a different selector in between — and a single
860    /// stored word would let the second write turn the first event back off.
861    enabled_events: HashSet<u16>,
862    /// When a register-access command was last served, for the heartbeat rule.
863    last_register_command: Instant,
864    /// Whether [`RegisterMap::enforce_heartbeat`] is armed.
865    enforce_heartbeat: bool,
866    /// Largest `GevSCPSPacketSize` this device accepts, if it clamps at all.
867    ///
868    /// Real cameras cap the packet size at what their MAC can emit and reduce a
869    /// larger request silently — the write is acknowledged and the register then
870    /// reads back lower. Nothing distinguishes that from acceptance on the wire,
871    /// which is why a host that trusts its own request reassembles at the wrong
872    /// stride and completes no frame
873    /// ([#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112)).
874    ///
875    /// The fake accepted anything until 0.4.1, so it could not express the
876    /// camera that caused that report, and no test could have caught the defect
877    /// — the ADR-0019 failure mode of a fake that only agrees with its client.
878    /// `None` keeps the old accept-anything behaviour.
879    max_packet_size: Option<u32>,
880    /// Largest GVSP datagram this device's *path* will actually deliver.
881    ///
882    /// Distinct from [`RegisterMap::max_packet_size`], and the distinction is
883    /// the whole of [#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112):
884    /// that camera declares `Max=16366`, stores 16114 without complaint, and
885    /// then streams nothing, because the link tops out at a 9216-byte frame.
886    /// A register read cannot find that; only a test packet can.
887    max_on_wire: Option<u32>,
888    /// Size requested by the most recent write with the fire-test-packet bit,
889    /// for the GVCP server to act on once it has released the register lock.
890    pending_test_packet: Option<u32>,
891}
892
893/// `GevSCPSPacketSize` bit 31: send one test packet of the requested size.
894pub const SCPS_FIRE_TEST_PACKET: u32 = 0x8000_0000;
895/// `GevSCPSPacketSize` bit 30: set do-not-fragment on transmitted packets.
896pub const SCPS_DO_NOT_FRAGMENT: u32 = 0x4000_0000;
897/// The bits of `GevSCPSPacketSize` that hold the size itself.
898pub const STREAM_PACKET_SIZE_MASK: u32 = 0xFFFF;
899
900/// Compress the GenApi XML into a single-entry ZIP archive (deflate).
901fn zip_xml_blob(xml: &[u8]) -> Vec<u8> {
902    use std::io::Write;
903    let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
904    let options = zip::write::SimpleFileOptions::default()
905        .compression_method(zip::CompressionMethod::Deflated);
906    writer
907        .start_file("fake.xml", options)
908        .expect("start XML zip entry");
909    writer.write_all(xml).expect("write XML zip entry");
910    writer.finish().expect("finish XML zip").into_inner()
911}
912
913impl RegisterMap {
914    /// Create a new register map with the given image dimensions.
915    ///
916    /// Initializes all bootstrap, feature, and device info registers with
917    /// sensible defaults. The GenApi XML is embedded at [`XML_BLOB_BASE`],
918    /// served as a ZIP archive when `zip_xml` is set (as many real cameras
919    /// do).
920    pub fn new(width: u32, height: u32, pixel_format: u32, zip_xml: bool) -> Self {
921        let mut regs = HashMap::new();
922
923        // ── Bootstrap registers ─────────────────────────────────────────
924        // The mandatory block every GigE Vision device answers. The fake used
925        // to leave it at zero, which reads as "this camera reports version
926        // 0.0 and has no stream channels" — a diagnostic dump of a real
927        // camera would look nothing like one taken from the fake.
928        regs.insert(VERSION, 0x0002_0000u32.to_be_bytes().to_vec()); // GEV 2.0
929        regs.insert(DEVICE_MODE, DEVICE_MODE_VALUE.to_be_bytes().to_vec());
930        regs.insert(
931            DEVICE_MAC_HIGH,
932            u32::from(u16::from_be_bytes([FAKE_MAC[0], FAKE_MAC[1]]))
933                .to_be_bytes()
934                .to_vec(),
935        );
936        regs.insert(
937            DEVICE_MAC_LOW,
938            u32::from_be_bytes([FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5]])
939                .to_be_bytes()
940                .to_vec(),
941        );
942        // Persistent IP + DHCP + link-local, matching CURRENT_IP_CONFIG below.
943        regs.insert(SUPPORTED_IP_CONFIG, 0x8000_0007u32.to_be_bytes().to_vec());
944        regs.insert(CURRENT_IP_ADDRESS, Ipv4Addr::LOCALHOST.octets().to_vec());
945        regs.insert(CURRENT_SUBNET_MASK, [255, 0, 0, 0].to_vec());
946        regs.insert(CURRENT_DEFAULT_GATEWAY, vec![0, 0, 0, 0]);
947        regs.insert(NUMBER_OF_MESSAGE_CHANNELS, 1u32.to_be_bytes().to_vec());
948        regs.insert(NUMBER_OF_STREAM_CHANNELS, 1u32.to_be_bytes().to_vec());
949        regs.insert(CCP, vec![0, 0, 0, 0]);
950        regs.insert(HEARTBEAT_TIMEOUT, 3000u32.to_be_bytes().to_vec());
951        regs.insert(MESSAGE_CHANNEL_PORT, 0u32.to_be_bytes().to_vec());
952        regs.insert(MESSAGE_CHANNEL_ADDRESS, vec![0, 0, 0, 0]);
953
954        // IP configuration: DHCP + persistent + LLA = 0x07
955        regs.insert(CURRENT_IP_CONFIG, 0x0000_0005u32.to_be_bytes().to_vec());
956        regs.insert(PERSISTENT_IP_ADDRESS, vec![0, 0, 0, 0]);
957        regs.insert(PERSISTENT_SUBNET_MASK, vec![0, 0, 0, 0]);
958        regs.insert(PERSISTENT_DEFAULT_GATEWAY, vec![0, 0, 0, 0]);
959
960        // Stream channel 0
961        let base = STREAM_CHANNEL_BASE;
962        regs.insert(base + SCP_HOST_PORT, vec![0, 0, 0, 0]);
963        regs.insert(base + SCP_PACKET_SIZE, 1500u32.to_be_bytes().to_vec());
964        // A second channel, so a `<pIndex>` term that is ignored is visible:
965        // its packet size differs from channel 0's.
966        regs.insert(
967            base + STREAM_CHANNEL_STRIDE + SCP_PACKET_SIZE,
968            9000u32.to_be_bytes().to_vec(),
969        );
970        regs.insert(REG_STREAM_CHANNEL_SELECTOR, 0u32.to_be_bytes().to_vec());
971        regs.insert(base + SCP_PACKET_DELAY, vec![0, 0, 0, 0]);
972        regs.insert(base + SCP_DEST_ADDR, vec![0, 0, 0, 0]);
973
974        // ── Device info (read-only strings) ─────────────────────────────
975        regs.insert(REG_DEVICE_MODEL_NAME, pad_string("VivaCam Fake", 32));
976        regs.insert(REG_DEVICE_VENDOR_NAME, pad_string("vitavision.dev", 32));
977        regs.insert(REG_DEVICE_SERIAL_NUMBER, pad_string("VIVA-FAKE-001", 16));
978        regs.insert(REG_DEVICE_FIRMWARE_VERSION, pad_string("1.0.0-fake", 32));
979        regs.insert(REG_DEVICE_ID, pad_string("VivaCam-0", 32));
980
981        // ── Image format ────────────────────────────────────────────────
982        regs.insert(REG_WIDTH, width.to_be_bytes().to_vec());
983        regs.insert(REG_HEIGHT, height.to_be_bytes().to_vec());
984        regs.insert(REG_PIXEL_FORMAT, pixel_format.to_be_bytes().to_vec());
985        regs.insert(REG_OFFSET_X, 0u32.to_be_bytes().to_vec());
986        regs.insert(REG_OFFSET_Y, 0u32.to_be_bytes().to_vec());
987        regs.insert(REG_SENSOR_WIDTH, 4096u32.to_be_bytes().to_vec());
988        regs.insert(REG_SENSOR_HEIGHT, 4096u32.to_be_bytes().to_vec());
989
990        // Width/Height limits
991        regs.insert(REG_WIDTH_MIN, 16u32.to_be_bytes().to_vec());
992        regs.insert(REG_WIDTH_MAX, 4096u32.to_be_bytes().to_vec());
993        regs.insert(REG_HEIGHT_MIN, 16u32.to_be_bytes().to_vec());
994        regs.insert(REG_HEIGHT_MAX, 4096u32.to_be_bytes().to_vec());
995
996        // ── Acquisition control ─────────────────────────────────────────
997        regs.insert(REG_ACQ_MODE, 0u32.to_be_bytes().to_vec()); // Continuous
998        regs.insert(REG_ACQ_START, vec![0, 0, 0, 0]);
999        regs.insert(REG_USER_SET_SELECTOR, 0u32.to_be_bytes().to_vec()); // Default
1000        regs.insert(REG_USER_SET_LOAD, 0u32.to_be_bytes().to_vec());
1001        regs.insert(REG_ACQ_STOP, vec![0, 0, 0, 0]);
1002        regs.insert(REG_ACQ_FRAME_RATE, 30.0f32.to_be_bytes().to_vec());
1003        regs.insert(
1004            REG_EXPOSURE_TIME,
1005            DEFAULT_EXPOSURE_US.to_be_bytes().to_vec(),
1006        );
1007        regs.insert(REG_EXPOSURE_AUTO, 0u32.to_be_bytes().to_vec()); // Off
1008
1009        // ── Analog control ──────────────────────────────────────────────
1010        regs.insert(REG_GAIN, 0.0f64.to_be_bytes().to_vec());
1011        regs.insert(REG_GAIN_AUTO, 0u32.to_be_bytes().to_vec()); // Off
1012        regs.insert(REG_BLACK_LEVEL, 0u32.to_be_bytes().to_vec());
1013
1014        // ── Predicate gating ────────────────────────────────────────────
1015        // Frame rate manually controllable by default; sensor boots as a
1016        // monochrome sensor so Mono8/Mono16 PixelFormat entries are
1017        // available at boot.
1018        regs.insert(REG_ACQ_FRAME_RATE_ENABLE, 1u32.to_be_bytes().to_vec());
1019        regs.insert(REG_SENSOR_TYPE, 0u32.to_be_bytes().to_vec());
1020        // Capability bits, MSB-first as GenICam counts them on a big-endian
1021        // register: bit 0 = frame rate control present, bit 1 = chunk support.
1022        regs.insert(REG_DEVICE_CAPS, 0xC000_0000u32.to_be_bytes().to_vec());
1023        // ExposureTime implemented (bit 0) and available (bit 1), MSB-first —
1024        // see REG_FEATURE_STATUS.
1025        regs.insert(REG_FEATURE_STATUS, 0xC000_0000u32.to_be_bytes().to_vec());
1026
1027        // ── Timestamp (1 GHz tick frequency) ────────────────────────────
1028        regs.insert(REG_TIMESTAMP_FREQ, 1_000_000_000u32.to_be_bytes().to_vec());
1029        regs.insert(REG_TIMESTAMP_VALUE, vec![0u8; 8]);
1030        regs.insert(REG_TIMESTAMP_LATCH, vec![0, 0, 0, 0]);
1031        regs.insert(
1032            REG_PTP_OFFSET_LATCHED,
1033            (PTP_OFFSET_NS as u64).to_be_bytes().to_vec(),
1034        );
1035        regs.insert(
1036            REG_CHUNK_TIMESTAMP_LE,
1037            CHUNK_TIMESTAMP_TICKS.to_le_bytes().to_vec(),
1038        );
1039        regs.insert(REG_CHUNK_WIDTH_LE, CHUNK_WIDTH_PX.to_le_bytes().to_vec());
1040
1041        // ── Chunk data ──────────────────────────────────────────────────
1042        regs.insert(REG_CHUNK_MODE_ACTIVE, 0u32.to_be_bytes().to_vec());
1043        regs.insert(REG_CHUNK_SELECTOR, 1u32.to_be_bytes().to_vec()); // Timestamp
1044        regs.insert(REG_CHUNK_ENABLE, 0u32.to_be_bytes().to_vec());
1045        regs.insert(REG_EVENT_SELECTOR, 5u32.to_be_bytes().to_vec());
1046
1047        // ── XML URL register ────────────────────────────────────────────
1048        let (xml_blob, xml_name) = if zip_xml {
1049            (zip_xml_blob(FAKE_XML.as_bytes()), "fake.zip")
1050        } else {
1051            (FAKE_XML.as_bytes().to_vec(), "fake.xml")
1052        };
1053        let url = format!(
1054            "Local:{xml_name};{:x};{:x}\0",
1055            XML_BLOB_BASE,
1056            xml_blob.len()
1057        );
1058        let mut url_bytes = vec![0u8; URL_REG_LEN];
1059        let src = url.as_bytes();
1060        url_bytes[..src.len()].copy_from_slice(src);
1061        regs.insert(FIRST_URL_REG, url_bytes);
1062
1063        Self {
1064            regs,
1065            xml_blob,
1066            clock_origin: Instant::now(),
1067            enabled_events: HashSet::new(),
1068            last_register_command: Instant::now(),
1069            enforce_heartbeat: false,
1070            max_packet_size: None,
1071            max_on_wire: None,
1072            pending_test_packet: None,
1073        }
1074    }
1075
1076    /// Arm the GigE Vision heartbeat rule: release control privilege when the
1077    /// controller goes quiet for longer than `GevHeartbeatTimeout`.
1078    ///
1079    /// Real devices always do this, and it is the entire reason a client needs a
1080    /// keepalive — GVSP image traffic does not refresh the timer, so a camera can
1081    /// be streaming at full rate while its control channel times out underneath.
1082    /// A fake that never expires CCP cannot tell a working keepalive from a
1083    /// missing one, which is how SR-05 stayed open through three app-layer
1084    /// reimplementations of the same loop.
1085    ///
1086    /// **Off by default**, because arming it makes every test that holds control
1087    /// privilege sensitive to a 3 s stall — on a loaded CI runner that is a
1088    /// flake, not a finding. Tests that are *about* the keepalive turn it on.
1089    pub fn enforce_heartbeat(&mut self, enable: bool) {
1090        self.enforce_heartbeat = enable;
1091        self.last_register_command = Instant::now();
1092    }
1093
1094    /// Clamp `GevSCPSPacketSize` writes to `max`, the way a real device does.
1095    ///
1096    /// A request above `max` is acknowledged and silently reduced, so the
1097    /// register reads back lower than what was written — nothing on the wire
1098    /// distinguishes that from acceptance, which is the defect in
1099    /// [#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112).
1100    /// Silently drop any GVSP datagram larger than `max`, as a path with a
1101    /// smaller frame ceiling than either endpoint believes does.
1102    ///
1103    /// See [`RegisterMap::max_on_wire`].
1104    pub fn set_max_on_wire(&mut self, max: u32) {
1105        self.max_on_wire = Some(max);
1106    }
1107
1108    /// The path ceiling, if one was configured.
1109    pub fn max_on_wire(&self) -> Option<u32> {
1110        self.max_on_wire
1111    }
1112
1113    /// Take the size requested by the last fire-test-packet write, if any.
1114    pub fn take_pending_test_packet(&mut self) -> Option<u32> {
1115        self.pending_test_packet.take()
1116    }
1117
1118    pub fn set_max_packet_size(&mut self, max: u32) {
1119        self.max_packet_size = Some(max);
1120        // Apply it to whatever the register already holds, so a device
1121        // configured with a cap never reports a size above it.
1122        let current = self.stream_packet_size();
1123        if current > max {
1124            self.write(STREAM_CHANNEL_BASE + SCP_PACKET_SIZE, &max.to_be_bytes());
1125        }
1126    }
1127
1128    /// Apply the heartbeat rule, and report whether privilege was just revoked.
1129    ///
1130    /// Called for register-access commands only. Discovery and FORCEIP are
1131    /// broadcast by any application on the subnet, so counting them would let an
1132    /// unrelated `viva-camctl list` hold another application's privilege open.
1133    /// Unlike a real device we do not track *which* peer is the controller —
1134    /// there is only ever one in a test.
1135    pub fn note_register_command(&mut self) -> bool {
1136        let elapsed = self.last_register_command.elapsed();
1137        self.last_register_command = Instant::now();
1138        if !self.enforce_heartbeat {
1139            return false;
1140        }
1141        let timeout = Duration::from_millis(u64::from(self.heartbeat_timeout_ms()));
1142        if timeout.is_zero() || elapsed <= timeout {
1143            return false;
1144        }
1145        let ccp = self.read(CCP, 4);
1146        if u32::from_be_bytes([ccp[0], ccp[1], ccp[2], ccp[3]]) == 0 {
1147            return false;
1148        }
1149        self.write(CCP, &0u32.to_be_bytes());
1150        true
1151    }
1152
1153    /// Report a different `GevHeartbeatTimeout` than the 3 000 ms default.
1154    ///
1155    /// Lets a test pick a window short enough to wait out without making the
1156    /// suite slow, and makes the timing it depends on explicit rather than
1157    /// implied by this crate's default.
1158    pub fn set_heartbeat_timeout_ms(&mut self, timeout_ms: u32) {
1159        self.write(HEARTBEAT_TIMEOUT, &timeout_ms.to_be_bytes());
1160    }
1161
1162    /// The heartbeat window this device reports, in milliseconds.
1163    pub fn heartbeat_timeout_ms(&self) -> u32 {
1164        let data = self.read(HEARTBEAT_TIMEOUT, 4);
1165        u32::from_be_bytes([data[0], data[1], data[2], data[3]])
1166    }
1167
1168    /// Read `len` bytes starting at `addr`.
1169    pub fn read(&self, addr: u64, len: usize) -> Vec<u8> {
1170        // XML blob region
1171        if addr >= XML_BLOB_BASE {
1172            let offset = (addr - XML_BLOB_BASE) as usize;
1173            if offset < self.xml_blob.len() {
1174                let end = (offset + len).min(self.xml_blob.len());
1175                let mut result = self.xml_blob[offset..end].to_vec();
1176                result.resize(len, 0);
1177                return result;
1178            }
1179        }
1180
1181        // `EventNotification` reports the selected event, not a stored word.
1182        if addr == REG_EVENT_NOTIFICATION {
1183            let on = u32::from(self.event_notification_on(self.event_selector()));
1184            let mut result = on.to_be_bytes().to_vec();
1185            result.resize(len, 0);
1186            result.truncate(len);
1187            return result;
1188        }
1189
1190        // Exact register match
1191        if let Some(data) = self.regs.get(&addr) {
1192            let mut result = data.clone();
1193            result.resize(len, 0);
1194            result.truncate(len);
1195            return result;
1196        }
1197
1198        // Sub-register access (read within a larger register)
1199        for (&reg_addr, data) in &self.regs {
1200            if addr >= reg_addr && (addr - reg_addr) < data.len() as u64 {
1201                let offset = (addr - reg_addr) as usize;
1202                let end = (offset + len).min(data.len());
1203                let mut result = data[offset..end].to_vec();
1204                result.resize(len, 0);
1205                return result;
1206            }
1207        }
1208
1209        vec![0u8; len]
1210    }
1211
1212    /// Write `data` starting at `addr`.
1213    pub fn write(&mut self, addr: u64, data: &[u8]) {
1214        // `GevSCPSPacketSize` carries two flags above the 16-bit size field:
1215        // bit 31 fires a test packet and bit 30 sets do-not-fragment. Neither
1216        // is part of the stored value — a device that echoed them back would
1217        // report a packet size of over a billion — so they are recorded for
1218        // the caller and stripped here.
1219        let stripped;
1220        let mut data = data;
1221        if addr == STREAM_CHANNEL_BASE + SCP_PACKET_SIZE && data.len() >= 4 {
1222            let raw = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
1223            self.pending_test_packet =
1224                (raw & SCPS_FIRE_TEST_PACKET != 0).then_some(raw & STREAM_PACKET_SIZE_MASK);
1225            if raw & (SCPS_FIRE_TEST_PACKET | SCPS_DO_NOT_FRAGMENT) != 0 {
1226                stripped = (raw & STREAM_PACKET_SIZE_MASK).to_be_bytes();
1227                data = &stripped[..];
1228            }
1229        }
1230
1231        // A capped device reduces an oversized packet size instead of refusing
1232        // it — silently, exactly as the hardware in #112 does.
1233        let clamped;
1234        let data = match self.max_packet_size {
1235            Some(max)
1236                if addr == STREAM_CHANNEL_BASE + SCP_PACKET_SIZE
1237                    && data.len() >= 4
1238                    && u32::from_be_bytes([data[0], data[1], data[2], data[3]]) > max =>
1239            {
1240                clamped = max.to_be_bytes();
1241                &clamped[..]
1242            }
1243            _ => data,
1244        };
1245
1246        // `EventNotification` applies to the selected event. Enabling a second
1247        // event must not disable the first, so the value lands in the set
1248        // keyed by the current selector rather than in a shared register.
1249        if addr == REG_EVENT_NOTIFICATION && data.len() >= 4 {
1250            let event = self.event_selector();
1251            if u32::from_be_bytes([data[0], data[1], data[2], data[3]]) != 0 {
1252                self.enabled_events.insert(event);
1253            } else {
1254                self.enabled_events.remove(&event);
1255            }
1256            return;
1257        }
1258
1259        if let Some(existing) = self.regs.get_mut(&addr) {
1260            let len = existing.len().min(data.len());
1261            existing[..len].copy_from_slice(&data[..len]);
1262            return;
1263        }
1264
1265        // Write within an existing register
1266        let addrs: Vec<u64> = self.regs.keys().copied().collect();
1267        for reg_addr in addrs {
1268            let reg_data = self.regs.get(&reg_addr).unwrap();
1269            if addr >= reg_addr && (addr - reg_addr) < reg_data.len() as u64 {
1270                let offset = (addr - reg_addr) as usize;
1271                let end = (offset + data.len()).min(reg_data.len());
1272                let reg_data = self.regs.get_mut(&reg_addr).unwrap();
1273                reg_data[offset..end].copy_from_slice(&data[..end - offset]);
1274                return;
1275            }
1276        }
1277
1278        self.regs.insert(addr, data.to_vec());
1279    }
1280
1281    /// Handle side effects of register writes.
1282    pub fn handle_special_write(&mut self, addr: u64) {
1283        if addr == REG_TIMESTAMP_LATCH {
1284            let ts = self.device_timestamp();
1285            self.regs
1286                .insert(REG_TIMESTAMP_VALUE, ts.to_be_bytes().to_vec());
1287        }
1288        if addr == REG_USER_SET_LOAD {
1289            self.load_user_set();
1290        }
1291    }
1292
1293    /// Restore the analog-control defaults, as `UserSetLoad` does on a real
1294    /// camera.
1295    ///
1296    /// A command that only acknowledges the write is untestable: a test could
1297    /// assert nothing beyond the absence of an error, which is the fake
1298    /// agreeing with itself. Restoring observable device state means a test can
1299    /// change a feature, execute the command, and read the change back out
1300    /// (ADR-0019).
1301    fn load_user_set(&mut self) {
1302        self.regs.insert(
1303            REG_EXPOSURE_TIME,
1304            DEFAULT_EXPOSURE_US.to_be_bytes().to_vec(),
1305        );
1306        self.regs.insert(REG_GAIN, 0.0f64.to_be_bytes().to_vec());
1307        self.regs
1308            .insert(REG_EXPOSURE_AUTO, 0u32.to_be_bytes().to_vec());
1309        self.regs.insert(REG_GAIN_AUTO, 0u32.to_be_bytes().to_vec());
1310    }
1311
1312    // ── Accessors ───────────────────────────────────────────────────────
1313
1314    /// Current device timestamp in nanoseconds since creation.
1315    pub fn device_timestamp(&self) -> u64 {
1316        self.clock_origin.elapsed().as_nanos() as u64
1317    }
1318
1319    /// Stream destination IP address.
1320    pub fn stream_dest_ip(&self) -> Ipv4Addr {
1321        let data = self.read(STREAM_CHANNEL_BASE + SCP_DEST_ADDR, 4);
1322        Ipv4Addr::new(data[0], data[1], data[2], data[3])
1323    }
1324
1325    /// Stream destination port.
1326    pub fn stream_dest_port(&self) -> u16 {
1327        let data = self.read(STREAM_CHANNEL_BASE + SCP_HOST_PORT, 4);
1328        u16::from_be_bytes([data[2], data[3]])
1329    }
1330
1331    /// Message channel destination address (`GevMCDA`).
1332    pub fn message_dest_ip(&self) -> Ipv4Addr {
1333        let data = self.read(MESSAGE_CHANNEL_ADDRESS, 4);
1334        Ipv4Addr::new(data[0], data[1], data[2], data[3])
1335    }
1336
1337    /// Message channel destination port (`GevMCP`), low 16 bits of the register.
1338    pub fn message_dest_port(&self) -> u16 {
1339        let data = self.read(MESSAGE_CHANNEL_PORT, 4);
1340        u16::from_be_bytes([data[2], data[3]])
1341    }
1342
1343    /// Event identifier currently selected by `EventSelector`.
1344    pub fn event_selector(&self) -> u16 {
1345        let data = self.read(REG_EVENT_SELECTOR, 4);
1346        u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as u16
1347    }
1348
1349    /// Whether `EventNotification` is `On` for `event_id`.
1350    ///
1351    /// Takes the event explicitly: the emitter cares whether *its* event is
1352    /// enabled, which is independent of whichever entry the controller
1353    /// happened to select last.
1354    pub fn event_notification_on(&self, event_id: u16) -> bool {
1355        self.enabled_events.contains(&event_id)
1356    }
1357
1358    /// Stream packet size.
1359    pub fn stream_packet_size(&self) -> u32 {
1360        let data = self.read(STREAM_CHANNEL_BASE + SCP_PACKET_SIZE, 4);
1361        u32::from_be_bytes([data[0], data[1], data[2], data[3]])
1362    }
1363
1364    /// Image width.
1365    pub fn width(&self) -> u32 {
1366        let data = self.read(REG_WIDTH, 4);
1367        u32::from_be_bytes([data[0], data[1], data[2], data[3]])
1368    }
1369
1370    /// Image height.
1371    pub fn height(&self) -> u32 {
1372        let data = self.read(REG_HEIGHT, 4);
1373        u32::from_be_bytes([data[0], data[1], data[2], data[3]])
1374    }
1375
1376    /// Pixel format PFNC code.
1377    pub fn pixel_format_code(&self) -> u32 {
1378        let data = self.read(REG_PIXEL_FORMAT, 4);
1379        u32::from_be_bytes([data[0], data[1], data[2], data[3]])
1380    }
1381
1382    /// Whether chunk mode is active.
1383    pub fn chunk_mode_active(&self) -> bool {
1384        let data = self.read(REG_CHUNK_MODE_ACTIVE, 4);
1385        u32::from_be_bytes([data[0], data[1], data[2], data[3]]) != 0
1386    }
1387
1388    /// Current exposure time in microseconds.
1389    pub fn exposure_time(&self) -> f64 {
1390        let data = self.read(REG_EXPOSURE_TIME, 8);
1391        f64::from_be_bytes([
1392            data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
1393        ])
1394    }
1395}
1396
1397/// Pad a string to a fixed length with null bytes.
1398fn pad_string(s: &str, len: usize) -> Vec<u8> {
1399    let mut buf = vec![0u8; len];
1400    let src = s.as_bytes();
1401    let copy_len = src.len().min(len);
1402    buf[..copy_len].copy_from_slice(&src[..copy_len]);
1403    buf
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408    use super::*;
1409
1410    fn map() -> RegisterMap {
1411        RegisterMap::new(64, 48, 0x0108_0001, false)
1412    }
1413
1414    fn select_and_enable(regs: &mut RegisterMap, event: u32, on: bool) {
1415        regs.write(REG_EVENT_SELECTOR, &event.to_be_bytes());
1416        regs.write(REG_EVENT_NOTIFICATION, &u32::from(on).to_be_bytes());
1417    }
1418
1419    /// `EventNotification` is a selected feature, so a controller enabling two
1420    /// events writes the same address twice. Backing it with a single stored
1421    /// word made the second write turn the first event off, and the fake then
1422    /// silently emitted nothing — the failure a fake exists to prevent.
1423    #[test]
1424    fn enabling_a_second_event_leaves_the_first_enabled() {
1425        let mut regs = map();
1426        select_and_enable(&mut regs, 5, true);
1427        select_and_enable(&mut regs, 6, true);
1428        assert!(regs.event_notification_on(5));
1429        assert!(regs.event_notification_on(6));
1430    }
1431
1432    #[test]
1433    fn disabling_one_event_leaves_the_others_alone() {
1434        let mut regs = map();
1435        select_and_enable(&mut regs, 5, true);
1436        select_and_enable(&mut regs, 6, true);
1437        select_and_enable(&mut regs, 5, false);
1438        assert!(!regs.event_notification_on(5));
1439        assert!(regs.event_notification_on(6));
1440    }
1441
1442    /// The MAC in the bootstrap registers must be the MAC in the Discovery
1443    /// ACK. Two independent copies of one fact is how #57 happened: the ACK
1444    /// layout drifted and nothing compared it against anything.
1445    #[test]
1446    fn the_bootstrap_mac_matches_the_discovery_mac() {
1447        let regs = map();
1448        let high = regs.read(DEVICE_MAC_HIGH, 4);
1449        let low = regs.read(DEVICE_MAC_LOW, 4);
1450        assert_eq!(&high[..2], &[0, 0], "top two bytes are reserved");
1451        let mac = [high[2], high[3], low[0], low[1], low[2], low[3]];
1452        assert_eq!(mac, FAKE_MAC);
1453    }
1454
1455    #[test]
1456    fn the_device_reports_one_channel_of_each_kind() {
1457        let regs = map();
1458        assert_eq!(regs.read(NUMBER_OF_MESSAGE_CHANNELS, 4), vec![0, 0, 0, 1]);
1459        assert_eq!(regs.read(NUMBER_OF_STREAM_CHANNELS, 4), vec![0, 0, 0, 1]);
1460    }
1461
1462    #[test]
1463    fn no_event_is_enabled_by_default() {
1464        let regs = map();
1465        assert!(!regs.event_notification_on(5));
1466        assert!(!regs.event_notification_on(6));
1467    }
1468
1469    /// Reading the register back must report the *selected* event, which is
1470    /// what a GenApi `EventNotification` read does.
1471    #[test]
1472    fn reading_notification_follows_the_selector() {
1473        let mut regs = map();
1474        select_and_enable(&mut regs, 5, true);
1475        regs.write(REG_EVENT_SELECTOR, &6u32.to_be_bytes());
1476        assert_eq!(regs.read(REG_EVENT_NOTIFICATION, 4), vec![0, 0, 0, 0]);
1477        regs.write(REG_EVENT_SELECTOR, &5u32.to_be_bytes());
1478        assert_eq!(regs.read(REG_EVENT_NOTIFICATION, 4), vec![0, 0, 0, 1]);
1479    }
1480}