Skip to main content

viva_fake_gige/
lib.rs

1//! In-process fake GigE Vision camera for testing and demos.
2//!
3//! This crate provides a simulated GigE Vision camera that speaks real GVCP/GVSP
4//! protocols over UDP on localhost. It is intended for integration testing and
5//! demonstrations without requiring physical camera hardware or external tools
6//! like aravis.
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! use viva_fake_gige::FakeCamera;
12//!
13//! # async fn example() {
14//! let camera = FakeCamera::builder()
15//!     .width(640)
16//!     .height(480)
17//!     .fps(30)
18//!     .bind_ip([127, 0, 0, 1].into())
19//!     .build()
20//!     .await
21//!     .expect("failed to start fake camera");
22//!
23//! // Camera is now discoverable on the configured port.
24//! // Use viva_genicam::gige::discover() to find it.
25//!
26//! // When done:
27//! camera.stop().await;
28//! # }
29//! ```
30
31mod gvcp_server;
32
33pub use gvcp_server::{
34    FAKE_DEVICE_KEY, FAKE_GROUP_KEY, FAKE_GROUP_MASK, FAKE_MAC, FAKE_MANUFACTURER, FAKE_MODEL,
35    FAKE_SERIAL, FAKE_USER_NAME, FAKE_VERSION,
36};
37mod gvsp_sender;
38pub mod registers;
39
40use std::net::Ipv4Addr;
41use std::sync::Arc;
42use std::sync::atomic::AtomicBool;
43
44use socket2::{Domain, Protocol, Socket, Type};
45use tokio::net::UdpSocket;
46use tokio::sync::{Mutex, Notify};
47use tokio::task::JoinHandle;
48use tracing::info;
49
50/// Builder for configuring and starting a fake GigE Vision camera.
51pub struct FakeCameraBuilder {
52    width: u32,
53    height: u32,
54    fps: u32,
55    bind_ip: Ipv4Addr,
56    port: u16,
57    pixel_format: u32,
58    zip_xml: bool,
59    enforce_heartbeat: bool,
60    heartbeat_timeout_ms: Option<u32>,
61    max_packet_size: Option<u32>,
62    max_on_wire: Option<u32>,
63}
64
65/// PFNC pixel format codes.
66pub const MONO8: u32 = 0x0108_0001;
67pub const RGB8: u32 = 0x0218_0014;
68
69impl Default for FakeCameraBuilder {
70    fn default() -> Self {
71        Self {
72            width: 640,
73            height: 480,
74            fps: 30,
75            bind_ip: Ipv4Addr::LOCALHOST,
76            port: 3956,
77            pixel_format: MONO8,
78            zip_xml: false,
79            enforce_heartbeat: false,
80            heartbeat_timeout_ms: None,
81            max_packet_size: None,
82            max_on_wire: None,
83        }
84    }
85}
86
87impl FakeCameraBuilder {
88    /// Set the image width in pixels.
89    pub fn width(mut self, width: u32) -> Self {
90        self.width = width;
91        self
92    }
93
94    /// Set the image height in pixels.
95    pub fn height(mut self, height: u32) -> Self {
96        self.height = height;
97        self
98    }
99
100    /// Set the target frame rate.
101    pub fn fps(mut self, fps: u32) -> Self {
102        self.fps = fps;
103        self
104    }
105
106    /// Set the IPv4 address to bind the GVCP socket to.
107    pub fn bind_ip(mut self, ip: Ipv4Addr) -> Self {
108        self.bind_ip = ip;
109        self
110    }
111
112    /// Set the GVCP control port (default: 3956).
113    pub fn port(mut self, port: u16) -> Self {
114        self.port = port;
115        self
116    }
117
118    /// Set the initial pixel format (PFNC code). Default: Mono8.
119    ///
120    /// Use [`MONO8`] or [`RGB8`] constants.
121    pub fn pixel_format(mut self, code: u32) -> Self {
122        self.pixel_format = code;
123        self
124    }
125
126    /// Serve the GenApi XML as a ZIP archive (default: plain XML).
127    ///
128    /// Many real cameras (Basler, FLIR, Hikrobot, ...) publish their
129    /// register-description XML zipped; enable this to exercise that path.
130    pub fn zip_xml(mut self, enable: bool) -> Self {
131        self.zip_xml = enable;
132        self
133    }
134
135    /// Release control privilege when the controller stops sending GVCP commands
136    /// for longer than `GevHeartbeatTimeout` (default: off).
137    ///
138    /// See [`registers::RegisterMap::enforce_heartbeat`] for why this is not the
139    /// default even though every real device behaves this way.
140    pub fn enforce_heartbeat(mut self, enable: bool) -> Self {
141        self.enforce_heartbeat = enable;
142        self
143    }
144
145    /// Clamp `GevSCPSPacketSize` to `max`, the way a real camera caps it.
146    ///
147    /// A request above `max` is acknowledged and silently reduced, so the
148    /// register reads back lower than what was written. Off by default: the
149    /// fake accepts any size, which is what every existing test expects.
150    ///
151    /// The fake accepted anything until 0.4.1, so it could not express the
152    /// camera behind
153    /// [#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112) and no
154    /// test could have caught that defect — the ADR-0019 failure mode of a fake
155    /// that only ever agrees with its client.
156    pub fn max_packet_size(mut self, max: u32) -> Self {
157        self.max_packet_size = Some(max);
158        self
159    }
160
161    /// Silently drop any GVSP datagram larger than `max`, as a network path
162    /// with a smaller frame ceiling than either endpoint believes does.
163    ///
164    /// Different from [`FakeCameraBuilder::max_packet_size`], and the
165    /// difference is the whole of
166    /// [#112](https://github.com/VitalyVorobyev/viva-genicam/issues/112): that
167    /// camera accepts and *stores* 16114, then streams nothing, because the
168    /// link tops out at a 9216-byte frame. No register read can find that —
169    /// only a test packet can.
170    pub fn max_on_wire(mut self, max: u32) -> Self {
171        self.max_on_wire = Some(max);
172        self
173    }
174
175    /// Report a different `GevHeartbeatTimeout` than the 3 000 ms default.
176    ///
177    /// A shorter window keeps a test that has to wait one out from dominating
178    /// the suite's runtime.
179    pub fn heartbeat_timeout_ms(mut self, timeout_ms: u32) -> Self {
180        self.heartbeat_timeout_ms = Some(timeout_ms);
181        self
182    }
183
184    /// Start the fake camera and return a handle.
185    pub async fn build(self) -> Result<FakeCamera, std::io::Error> {
186        let mut register_map =
187            registers::RegisterMap::new(self.width, self.height, self.pixel_format, self.zip_xml);
188        if let Some(timeout_ms) = self.heartbeat_timeout_ms {
189            register_map.set_heartbeat_timeout_ms(timeout_ms);
190        }
191        if let Some(max) = self.max_packet_size {
192            register_map.set_max_packet_size(max);
193        }
194        if let Some(max) = self.max_on_wire {
195            register_map.set_max_on_wire(max);
196        }
197        register_map.enforce_heartbeat(self.enforce_heartbeat);
198        let regs = Arc::new(Mutex::new(register_map));
199
200        let acq_start = Arc::new(Notify::new());
201        let acq_stop_flag = Arc::new(AtomicBool::new(false));
202
203        // Bind GVCP control socket. On macOS `SO_REUSEADDR` is a no-op for UDP,
204        // so also set `SO_REUSEPORT` to let a fresh socket rebind the port while
205        // the previous camera's tokio task is still shutting down (matters for
206        // back-to-back module-scoped pytest fixtures).
207        let bind_addr: std::net::SocketAddr =
208            format!("{}:{}", self.bind_ip, self.port).parse().unwrap();
209        let sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
210        sock.set_reuse_address(true)?;
211        #[cfg(unix)]
212        sock.set_reuse_port(true)?;
213        sock.set_nonblocking(true)?;
214        sock.bind(&bind_addr.into())?;
215        let std_sock: std::net::UdpSocket = sock.into();
216        let socket = Arc::new(UdpSocket::from_std(std_sock)?);
217        let local_addr = socket.local_addr()?;
218        info!(%local_addr, "fake camera GVCP listening");
219
220        // Spawn GVCP server task.
221        let gvcp_handle = {
222            let socket = socket.clone();
223            let regs = regs.clone();
224            let acq_start = acq_start.clone();
225            let acq_stop = acq_stop_flag.clone();
226            let bind_ip = self.bind_ip;
227            tokio::spawn(async move {
228                gvcp_server::run(socket, regs, acq_start, acq_stop, bind_ip).await;
229            })
230        };
231
232        // Spawn GVSP streaming task.
233        let gvsp_handle = {
234            let regs = regs.clone();
235            let acq_start = acq_start.clone();
236            let acq_stop = acq_stop_flag.clone();
237            let fps = self.fps;
238            tokio::spawn(async move {
239                gvsp_sender::run(regs, acq_start, acq_stop, fps).await;
240            })
241        };
242
243        Ok(FakeCamera {
244            gvcp_handle: Some(gvcp_handle),
245            gvsp_handle: Some(gvsp_handle),
246            _regs: regs,
247            local_addr,
248        })
249    }
250}
251
252/// Handle to a running fake GigE Vision camera.
253///
254/// The camera runs as background tokio tasks. Call [`stop`](FakeCamera::stop) or
255/// drop the handle to shut down the camera.
256pub struct FakeCamera {
257    gvcp_handle: Option<JoinHandle<()>>,
258    gvsp_handle: Option<JoinHandle<()>>,
259    _regs: Arc<Mutex<registers::RegisterMap>>,
260    local_addr: std::net::SocketAddr,
261}
262
263impl FakeCamera {
264    /// Create a new builder.
265    pub fn builder() -> FakeCameraBuilder {
266        FakeCameraBuilder::default()
267    }
268
269    /// Start a fake camera with default settings on 127.0.0.1:3956.
270    pub async fn start() -> Result<Self, std::io::Error> {
271        Self::builder().build().await
272    }
273
274    /// The local address the GVCP socket is bound to.
275    pub fn local_addr(&self) -> std::net::SocketAddr {
276        self.local_addr
277    }
278
279    /// The port the GVCP socket is listening on.
280    pub fn port(&self) -> u16 {
281        self.local_addr.port()
282    }
283
284    /// Stop the fake camera and wait for its background tasks to exit.
285    ///
286    /// Awaiting the `JoinHandle`s after `abort()` ensures the tokio tasks have
287    /// dropped their `Arc<UdpSocket>` clones and the GVCP port is actually
288    /// released before the call returns — otherwise a subsequent rebind on
289    /// the same port can hit `EADDRINUSE`.
290    pub async fn stop(mut self) {
291        if let Some(h) = self.gvcp_handle.take() {
292            h.abort();
293            let _ = h.await;
294        }
295        if let Some(h) = self.gvsp_handle.take() {
296            h.abort();
297            let _ = h.await;
298        }
299    }
300}
301
302impl Drop for FakeCamera {
303    /// Best-effort cleanup when the handle is dropped without calling `stop`.
304    /// Does not wait for tasks to exit — use `stop().await` for that.
305    fn drop(&mut self) {
306        if let Some(h) = self.gvcp_handle.take() {
307            h.abort();
308        }
309        if let Some(h) = self.gvsp_handle.take() {
310            h.abort();
311        }
312    }
313}