Testing without hardware
This tutorial shows how to evaluate the full viva-genicam stack without
physical cameras or external tools. The viva-fake-gige crate provides an
in-process GigE Vision camera simulator that speaks real GVCP/GVSP protocols
on localhost.
Quick start
# Run the self-contained demo
cargo run -p viva-genicam --example demo_fake_camera
The demo starts a camera, discovers it, connects, reads and writes features, and streams five frames — the whole stack, on loopback:
Starting fake GigE Vision camera on 127.0.0.1:3956 ...
Discovering cameras (2 s timeout) ...
Found 1 device(s):
IP: 127.0.0.1 Model: FakeGigE Manufacturer: viva-genicam
Connecting to 127.0.0.1 ...
Connected. GenApi XML: 22246 bytes, 64 features.
Reading camera features:
Width = 640
...
Setting Width = 320, ExposureTime = 10000 ...
Width readback = 320
Streaming 5 frames ...
Frame 1: 320x480 Mono8 payload=153600B ts=7393542
...
Demo complete. All operations succeeded without hardware.
What the fake camera supports
| Feature | Status |
|---|---|
| GVCP discovery (broadcast on loopback) | Supported |
| GenCP register read/write (READREG, WRITEREG, READMEM, WRITEMEM) | Supported |
| Control Channel Privilege (CCP) | Supported |
| GenApi XML with SFNC features | Width, Height, PixelFormat, ExposureTime, Gain |
| GVSP frame streaming | Synthetic gradient images at configurable FPS |
| Device timestamps (1 GHz tick rate) | Supported (ns since acquisition start) |
| Timestamp latch (GevTimestampValue) | Supported |
| Chunk data (Timestamp, ExposureTime) | Supported when ChunkModeActive=1 |
Running integration tests
All integration tests use the fake camera automatically:
# Full workspace test suite (includes fake camera tests)
cargo test --workspace
# Just the camera integration tests
cargo test -p viva-genicam --test fake_camera
# Zenoh service end-to-end tests
cargo test -p viva-service --test fake_camera_e2e
# The USB3 Vision equivalent, against viva-fake-u3v
cargo test -p viva-genicam --test fake_u3v_camera --features u3v
Using the fake camera in your own code
Add viva-fake-gige as a dev-dependency:
[dev-dependencies]
viva-fake-gige = { git = "https://github.com/VitalyVorobyev/viva-genicam" }
Start a fake camera in your test. The value build() returns is a guard: the
camera answers GVCP and streams GVSP for as long as it is alive, and shuts down
when dropped.
// The guard owns the camera's tasks: it answers GVCP and streams GVSP for
// as long as it is alive, and shuts down when dropped.
let _camera_guard = FakeCamera::builder()
.width(640)
.height(480)
.fps(10)
.bind_ip([127, 0, 0, 1].into())
.port(3956)
.build()
.await?;
Then discover it — note discover_all, not discover, because the fake lives
on loopback:
println!("Connecting to {} ...", dev_info.ip);
let (mut camera, xml) = connect_gige_with_xml(dev_info).await?;
println!(
" Connected. GenApi XML: {} bytes, {} features.\n",
xml.len(),
camera.nodemap().node_names().count()
);
From there it is the ordinary API:
// `get` and `set` are synchronous. They block on register I/O, and
// `GigeRegisterIo` steps off the async worker itself, so no `spawn_blocking`
// wrapper is needed even here inside `#[tokio::main]`.
println!("Reading camera features:");
for feature in [
"Width",
"Height",
"PixelFormat",
"ExposureTime",
"Gain",
"GevTimestampTickFrequency",
] {
match camera.get(feature) {
Ok(value) => println!(" {feature} = {value}"),
Err(err) => println!(" {feature} = <error: {err}>"),
}
}
println!();
// ── 5. Write a feature ──────────────────────────────────────────────────
println!("Setting Width = 320, ExposureTime = 10000 ...");
camera.set("Width", "320")?;
camera.set_exposure_time_us(10_000.0)?;
println!(" Width readback = {}\n", camera.get("Width")?);
The full program those excerpts come from is
examples/demo_fake_camera.rs,
which goes on to build a FrameStream and grab five frames.
Running as a standalone server
The viva-fake-gige binary starts a long-running fake camera that stays alive
until Ctrl+C. This is the way to test interactively with viva-camctl, or with
viva-service and Viva Studio.
# Terminal 1: start the fake camera
cargo run -p viva-fake-gige
# Custom dimensions and frame rate
cargo run -p viva-fake-gige -- --width 512 --height 512 --fps 15
Output:
Fake camera running on 127.0.0.1:3956 (640x480 Mono8 @ 30 fps)
Press Ctrl+C to stop.
Using the CLI with the fake camera
With the fake camera running in Terminal 1, use viva-camctl in Terminal 2:
# Discover (use --iface to include loopback)
cargo run -p viva-camctl -- list --iface 127.0.0.1
# Read / write features
cargo run -p viva-camctl -- get --ip 127.0.0.1 --name Width
cargo run -p viva-camctl -- set --ip 127.0.0.1 --name Width --value 512
cargo run -p viva-camctl -- get --ip 127.0.0.1 --name DeviceModelName
E2E testing with Viva Studio
Viva Studio lives in studio/ in this repository, as a separate Cargo
workspace. The full-stack test uses three terminals:
# Terminal 1: fake camera
cargo run -p viva-fake-gige
# Terminal 2: camera service (bridges the camera onto Zenoh).
# --zenoh-config is required on the service side so Studio can connect via TCP.
cargo run -p viva-service -- \
--iface lo0 \
--zenoh-config studio/config/zenoh-local.json5
# On Linux: --iface lo
# Terminal 3: the desktop app (loads its own Zenoh config in dev mode)
cd studio/apps/viva-studio-tauri
cargo tauri dev
Studio should discover the fake camera, show its feature tree, and stream gradient images in the viewer.
For USB3 Vision the service can host its own fake, so two terminals suffice:
cargo run -p viva-service-u3v -- --fake --zenoh-config studio/config/zenoh-local.json5
cd studio/apps/viva-studio-tauri && cargo tauri dev
Fake camera configuration
The FakeCameraBuilder supports:
let camera = FakeCamera::builder()
.width(1920) // default: 640
.height(1080) // default: 480
.fps(60) // default: 30
.bind_ip([127, 0, 0, 1].into()) // default: 127.0.0.1
.port(3956) // default: 3956
.pixel_format(viva_fake_gige::RGB8) // default: MONO8
.zip_xml(true) // default: false
.enforce_heartbeat(true) // default: false
.heartbeat_timeout_ms(3_000) // default: the device's own value
.build()
.await
.unwrap();
The last three are there to reproduce behaviour real cameras have and fakes usually do not:
zip_xmlserves the GenApi document ZIP-compressed, which many vendors do and which exercises a decompression path that would otherwise never be tested.enforce_heartbeatmakes the camera actually revoke control privilege when no GVCP command arrives insideheartbeat_timeout_ms. Without it, a client that forgets to send keepalives passes every test and fails on real hardware.
Image dimensions, exposure and gain can also be changed at runtime through
GenApi writes — the fake responds to Width, Height, ExposureTime and
Gain the way a real camera does.