viva-gige — GigE Vision transport (GVCP/GVSP)
viva-gige implements the GigE Vision transport on Windows, Linux and macOS:
discovery and control over GVCP, image data over GVSP, plus interface
enumeration, event and action messages, and device-timestamp mapping.
It sits below viva-genapi — this crate moves bytes, the NodeMap decides which
bytes. Applications normally reach it through viva-genicam.
Module map
| Module | Contents |
|---|---|
gvcp | discover, discover_on_interface, discover_all, force_ip, DeviceInfo, GigeDevice, GigeError |
gvsp | Packet parsing, frame reassembly, StreamDest, StreamConfig, chunk extraction |
nic | Iface — interface enumeration and selection |
action | send_action, ActionParams, AckSummary |
message | The event/message channel |
stats | StreamStats and its accumulator |
time | TimeSync — device ticks to host time |
Selecting the local interface
On a multi-NIC host, bind to the NIC that reaches the camera. Iface offers
four ways to get one, and the difference between them has caused real bugs:
use viva_gige::nic::Iface;
Iface::from_system("eth0")?; // by interface name
Iface::from_ipv4("192.168.0.5".parse()?)?; // by *host* address
Iface::from_remote_ipv4("192.168.0.10".parse()?)?; // by the *camera's* address
Iface::list()?; // everything the library can see
from_ipv4 takes an address on this host. from_remote_ipv4 takes the
camera’s address and probes the routing table for the interface that reaches
it — which is what you want when all you have is the camera’s IP. Passing a
camera address to from_ipv4 is the defect that made acquisition fail on real
hardware in #70: it
only ever worked against the loopback fake, where the two addresses coincide.
Iface::list() is also a diagnostic. It reports interfaces as the library sees
them, which is not always the set the OS shows — on Windows, link-local
169.254.x.x addresses were invisible until the link-local feature of
if-addrs was enabled, and an interface missing from this list is invisible to
discovery no matter what ipconfig says
(#57).
Discovery (GVCP)
A broadcast command, then replies collected for a timeout window. Each reply
becomes a DeviceInfo with IP, MAC, manufacturer, model, version, serial and
user-defined name.
// `discover` broadcasts on the interfaces the library can see; naming one
// pins it to that NIC, which is what you want on a multi-homed host.
// `IfaceSelector` takes either spelling — `--iface 192.168.0.5` or
// `--iface eth0` — so the flag means the same thing here as in
// `viva-camctl` and `viva-service`.
let devices = if let Some(selector) = iface.as_ref() {
let iface = selector.resolve()?;
viva_genicam::gige::discover_on_interface(timeout, iface.name()).await?
} else {
viva_genicam::gige::discover(timeout).await?
};
if devices.is_empty() {
println!("No cameras found.");
return Ok(());
}
println!("{:<16} {:<17} {:<20} Model", "IP", "MAC", "Manufacturer");
for dev in devices {
println!(
"{:<16} {:<17} {:<20} {}",
dev.ip,
format_mac(&dev.mac),
dev.manufacturer.as_deref().unwrap_or("-"),
dev.model.as_deref().unwrap_or("-"),
);
}
| Function | Scans |
|---|---|
discover(timeout) | Every routable interface |
discover_on_interface(timeout, name) | One named interface |
discover_all(timeout) | Every interface including loopback |
Use discover_all only for the fake camera.
One unusable interface no longer aborts the whole call, and a stray non-GVCP
packet on the port no longer discards the replies already collected — both were
real failure modes.
From the CLI:
cargo run -p viva-camctl -- list --iface 192.168.0.5
Control (GVCP)
GigeDevice owns one control channel:
use viva_gige::gvcp::{GigeDevice, GVCP_PORT};
let mut device = GigeDevice::open(SocketAddr::new(camera_ip.into(), GVCP_PORT)).await?;
device.claim_control().await?;
let value = device.read_register(0x0a00).await?;
device.write_register(0x0a00, value | 1).await?;
let bytes = device.read_mem(0x0200, 512).await?;
read_register/write_register are 32-bit at a 32-bit address;
read_mem/write_mem take a 64-bit address and a length, and chunk the
transfer to fit the transport.
Control privilege and the heartbeat
claim_control() takes the Control Channel Privilege. A device revokes it
if no GVCP command arrives within GevHeartbeatTimeout — 3 000 ms is typical —
and GVSP image traffic does not count towards that timer. A camera can therefore
be streaming at full rate while the control channel times out, and the next
write fails with AccessDenied.
You do not have to manage this. GigeRegisterIo in viva-genicam owns a
keepalive: it reads the device’s own GevHeartbeatTimeout and pings at a
quarter of it, so holding a Camera is enough. heartbeat_timeout_ms() and
ping_control_channel() are here for anyone driving GigeDevice directly.
IP configuration
force_ip assigns a temporary address to a camera identified by MAC — useful
when a camera is on the wrong subnet and otherwise unreachable.
write_persistent_ip and enable_persistent_ip make it survive a power cycle.
cargo run -p viva-camctl -- set-ip --mac DE:AD:BE:EF:CA:FE --ip 192.168.1.100 --force
Events and actions
- Events are device-to-host notifications on the message channel (exposure
end, and vendor-defined ones).
set_message_destinationpoints the device at a host socket;viva_genicam::EventStreampresents the result. - Actions are host-to-many-devices:
send_actionbroadcasts an action command so several cameras trigger together, optionally at a scheduled timestamp.AckSummaryreports which devices acknowledged.
Both are vendor-variable. If you schedule actions, keep the time bases
consistent — see TimeSync below.
Streaming (GVSP)
The receiver negotiates stream parameters on the control channel, then receives UDP packets and reassembles frames by block ID.
Application code builds streams through viva-genicam, not this crate
directly:
// Connect to camera (fetches XML, builds nodemap).
let mut camera = connect_gige(&device).await?;
// Configure stream.
let mut stream_device = viva_genicam::gige::GigeDevice::open(std::net::SocketAddr::new(
std::net::IpAddr::V4(device.ip),
viva_genicam::gige::GVCP_PORT,
))
.await?;
let mut builder = StreamBuilder::new(&mut stream_device).iface(iface.clone());
if let Some(group) = args.multicast {
builder = builder.multicast(Some(group));
}
if let Some(port) = args.port {
builder = builder.destination_port(port);
}
if let Some(size) = args.packet_size {
builder = builder.packet_size(size);
} else {
builder = builder.auto_packet_size();
}
let stream = builder.build().await?;
// Create high-level frame stream (handles packet reassembly automatically).
let time_sync = camera.time_sync().clone();
let mut frame_stream = FrameStream::new(stream, Some(time_sync));
// Start acquisition.
camera.acquisition_start()?;
StreamBuilder (in viva_genicam::stream) exposes iface, dest,
target_mtu, packet_size, packet_delay,
destination_port, multicast, rcvbuf_bytes and channel. FrameStream
wraps the result and yields whole frames.
Packet size and MTU
GevSCPSPacketSize is the size of the transmitted IP packet, so it must fit
the path MTU end to end. The probed MTU is used unless packet_size overrides
it. Two caveats
worth knowing:
- Cameras clamp a size they cannot honour, and the write succeeds when they
do.
StreamBuilder::buildreads the register back throughGigeDevice::get_stream_packet_sizeand puts the effective size inStreamParams, so reassembly follows the camera rather than the request. A device that will not answer the read-back keeps the requested value and logs a warning. - On a large-MTU link the requested size must still be clamped to the IPv4
maximum. Linux loopback reports MTU 65536, which would produce a
65 508-byte datagram against the 65 507-byte limit — every
send_tofails. An explicitly configured size above 65 535 is refused rather than truncated:GevSCPSPacketSizeholds the size in 16 bits, so writing 70 000 would configure 4 464.
Resend
GVSP defines packet resend, and the pieces exist here — ResendPlanner,
coalesce_missing, GigeDevice::request_resend. They are not wired into the
receive path (backlog SR-04). Nothing in a live stream requests a resend, and
nothing increments the resends counter, so a summary reading resends=0 means
“not implemented” rather than “none were needed”. drops is the number to
watch. This section changes when resend lands.
Chunk data
With ChunkModeActive set, the payload carries the image followed by chunk
blocks ([id][reserved][length][data]). parse_chunks extracts them and skips
what it does not recognise; viva_genicam::ChunkMap maps the known ones
(timestamp, exposure, gain) to typed values.
Statistics
StreamStats carries frames, bytes, drops, packets, avg_fps,
avg_mbps, avg_latency_ms and the elapsed window. The resend and
backpressure counters are inert for the reason above.
Timestamp mapping
Devices report a tick counter, not wall-clock time. TimeSync maintains a
linear mapping from device ticks to host SystemTime, calibrated by latching
the device timestamp against a host reading. Without that calibration there is
no origin to map from — so treat an uncalibrated host timestamp as absent
rather than as data.
Logging
RUST_LOG=info,viva_gige=debug cargo run -p viva-camctl -- stream --ip 192.168.0.10
viva-camctl maps -v to debug and -vv to trace if you would rather not
set the variable. Useful targets: viva_gige::gvcp (binds, discovery, register
ops), viva_gige::gvsp (packets, reassembly, frame stats), viva_gige::nic
(interface enumeration and socket binding).
Platform notes
Windows. Allow inbound UDP for discovery and the stream port, for both the Private and Public firewall profiles. Enable jumbo frames in the NIC’s advanced settings if the whole path supports them, and keep the power plan on high performance — receive buffers default low on many desktop NICs.
Linux. With firewalld, GVCP replies arrive from source port 3956 and the
GVSP port needs its own rule — see
Link-local (APIPA) cameras.
net.core.rmem_max caps how far rcvbuf_bytes can go.
Link-local. GigE Vision cameras fall back to 169.254.0.0/16 when no DHCP
server answers. That works, but the host needs an address in the same range and
the firewall usually needs telling — the same networking chapter covers it.
See also
viva-gencp— the message layer GVCP carriesviva-genapi— the NodeMap above this transport- Tutorials: Discovery, Registers, Streaming
- Networking Guide — MTU, firewalls, link-local