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