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}