Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

ModuleContents
gvcpdiscover, discover_on_interface, discover_all, force_ip, DeviceInfo, GigeDevice, GigeError
gvspPacket parsing, frame reassembly, StreamDest, StreamConfig, chunk extraction
nicIface — interface enumeration and selection
actionsend_action, ActionParams, AckSummary
messageThe event/message channel
statsStreamStats and its accumulator
timeTimeSync — 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("-"),
        );
    }
FunctionScans
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_destination points the device at a host socket; viva_genicam::EventStream presents the result.
  • Actions are host-to-many-devices: send_action broadcasts an action command so several cameras trigger together, optionally at a scheduled timestamp. AckSummary reports 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::build reads the register back through GigeDevice::get_stream_packet_size and puts the effective size in StreamParams, 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_to fails. An explicitly configured size above 65 535 is refused rather than truncated: GevSCPSPacketSize holds 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