Welcome & Goals
genicam-rs provides pure Rust building blocks for the GenICam ecosystem with an Ethernet-first focus (GigE Vision) and first-class support for Windows, Linux, and macOS.
Who is this book for?
- End‑users building camera applications who want a practical high‑level API and copy‑pasteable examples.
- Contributors extending transports, GenApi features (incl. expression nodes), and streaming—who need a clear mental model of crates and internal boundaries.
What works today
- Device discovery over GigE Vision (GVCP) on a selected network interface.
- Control path: reading/writing device memory via GenCP over GVCP; fetching the device’s GenApi XML.
- GenApi: NodeMap with common node kinds (Integer/Float/Enum/Bool/Command/SwissKnife), ranges, access modes, and selector-based addressing.
- CLI (
gencamctl) for common operations: discovery, feature get/set, streaming, events, chunks, and benchmarks. - Streaming (GVSP): packet reassembly, resend handling, MTU/packet sizing & delay, and stats (evolving).
Details evolve fast—check examples and release notes for the latest capabilities.
What’s coming next
- Additional GenApi nodes (e.g., Converter, complex formulas), dependency evaluation/caching improvements.
- USB3 Vision transport.
- GenTL producer (.cti) and PFNC/SFNC coverage.
How this book is organized
- Start with Quick Start to build, test, and run the first discovery.
- Read the Primer and Architecture to get the big picture.
- Use Crate Guides and Tutorials for hands‑on tasks.
- See Networking and Troubleshooting when packets don’t behave.
Quick Start
This guide gets you from checkout to discovering cameras in minutes.
Prerequisites
- Rust: MSRV 1.75+ (toolchain pinned via
rust-toolchain.toml). - OS: Windows, Linux, or macOS.
- Network (GigE Vision):
- Allow UDP broadcast on the NIC you’ll use for discovery.
- Optional: enable jumbo frames on that NIC for high‑throughput streaming tests.
Build & Test
# From the repo root:
cargo build --workspace
# Run all tests
cargo test --workspace
# Generate local API docs (rustdoc)
cargo doc --workspace --no-deps
First run: Discovery examples
You can try discovery in two ways—either via the high‑level genicam crate example or the gencamctl CLI.
Option A: Example (genicam crate)
# List cameras via GVCP broadcast\ n cargo run -p genicam --example list_cameras
Option B: CLI (gencamctl)
# Discover cameras on the selected interface (IPv4 of your NIC)
cargo run -p gencamctl -- list --iface 192.168.0.5
Control path: read / write & XML
# Read a feature by name
cargo run -p gencamctl -- get --ip 192.168.0.10 --name ExposureTime
# Set a feature value
cargo run -p gencamctl -- set --ip 192.168.0.10 --name ExposureTime --value 5000
# Fetch minimal XML metadata via control path (example)
cargo run -p genicam --example get_set_feature
Streaming (early GVSP)
# Receive a GVSP stream, auto‑negotiate packet size, save first two frames
cargo run -p gencamctl -- stream --ip 192.168.0.10 --iface 192.168.0.5 --auto --save 2
Windows specifics
- Run the terminal as Administrator the first time to let the firewall prompt appear.
- Add inbound UDP rules for discovery and streaming.
- Enable jumbo frames per NIC if your network supports it (helps at high FPS).
Next steps
- Read the Primer for the concepts behind discovery, control, and streaming.
- Jump to the Tutorial: Discover devices for a step‑by‑step walkthrough with troubleshooting tips.
GenICam & Vision Standards Primer
This chapter orients you in the standards and shows how they map to the crates in this repo. If you’re an end‑user, skim the concepts and jump to tutorials. If you’re a contributor, the mappings help you navigate the code.
1) Control vs. Data paths (big picture)
- Control: configure the device, read status, fetch the GenApi XML. In GigE Vision, control is GVCP (GigE Vision Control Protocol, UDP) carrying GenCP (Generic Control Protocol) semantics for register reads/writes and feature access.
- Data: receive image/metadata stream(s). In GigE Vision, data is GVSP (GigE Vision Streaming Protocol, UDP), typically one-way from camera → host.
- Events & Actions: GVCP supports device→host events and host→device action commands for sync/triggering.
+------------------------+ +--------------------+
| Host | | Camera |
| (this repository) | | (GigE Vision) |
+-----------+------------+ +----------+---------+
| GVCP (UDP, control) |
| GenCP (registers/features) |
v ^
Configure, query, XML |
|
^ v
| GVSP (UDP, data) Image/Chunks
| (streaming) |
2) GenApi XML & NodeMap
- The device exposes an XML description of its features (nodes). Nodes form a graph with types like Integer, Float, Boolean, Enumeration, Command, String, Register, and expression nodes like SwissKnife.
- Nodes have AccessMode (RO/RW), Visibility (Beginner/Expert/Guru), Units, Min/Max/Inc, Selector links, and Dependencies (i.e., a node’s value depends on other nodes).
- The host builds a NodeMap from the XML and evaluates nodes on demand: some read/write device registers; others compute values from expressions.
SwissKnife (implemented)
- A SwissKnife node computes its value from an expression referencing other nodes (e.g., arithmetic, logic, conditionals). Typical uses:
- Derive human‑readable features from raw register fields.
- Apply scale/offset and conditionals depending on selectors.
- In this project, SwissKnife is evaluated in the NodeMap, so reads of dependent nodes trigger the calculation transparently.
Selectors
- Selectors (e.g.,
GainSelector) change the addressing or active branch so the same feature name maps to different underlying registers or computed paths.
3) Streaming: GVSP
- UDP packets carry payloads (image data/metadata). The host reassembles frames, handles resend requests, negotiates packet size/MTU, and may introduce packet delay to avoid NIC/driver overflow.
- Chunks: optional metadata blocks (e.g.,
Timestamp,ExposureTime) can be enabled and parsed alongside image data. - Time mapping: devices often use tick counters; the host maintains a mapping between device ticks and host time for cross‑correlation.
4) How standards map to crates
| Concept | Crate | Responsibility |
|---|---|---|
| GenCP (encode/decode, status) | genicp | Message formats, errors, helpers for control-path operations |
| GVCP/GVSP (GigE Vision) | tl-gige | Discovery, control channel, streaming engine, resend/MTU/delay, events/actions |
| GenApi XML loader | genapi-xml | Fetch XML via control path and parse schema‑lite into an internal representation |
| NodeMap & evaluation | genapi-core | Node types (incl. SwissKnife), dependency resolution, selector routing, value get/set |
| Public façade | genicam | End‑user API combining transport + NodeMap + utilities (examples live here) |
5) USB3 Vision (preview)
- Similar split between control and data paths, but with USB3 transport and different discovery/endpoint mechanics. The higher‑level GenApi and NodeMap concepts remain the same.
6) What to read next
- Architecture Overview for a code‑level view of modules, traits, and async/concurrency.
- Crate Guides for deep dives (APIs, examples, edge cases).
- Tutorials to configure features and receive frames end‑to‑end.
Architecture Overview
This section maps the runtime flow, crate boundaries, and key traits so both app developers and contributors can reason about the system.
Layered view
+---------------------------+ End‑user API & examples
| genicam (façade) | - device discovery, feature get/set
| crates/genicam/examples | - streaming helpers, CLI wiring
+-------------+-------------+
|
v
+---------------------------+ GenApi core
| genapi-core | - Node types (Integer/Float/Enum/Bool/Command,
| | Register, String, **SwissKnife**)
| | - NodeMap build & evaluation
| | - Selector routing & dependency graph
+-------------+-------------+
|
v
+---------------------------+ GenApi XML
| genapi-xml | - Fetch XML via control path
| | - Parse schema‑lite → IR used by genapi-core
+-------------+-------------+
|
v
+---------------------------+ Transports
| tl-gige | - GVCP (control): discovery, read/write, events,
| | action commands
| | - GVSP (data): receive, reassembly, resend,
| | MTU/packet size negotiation, delay, stats
+-------------+-------------+
|
v
+---------------------------+ Protocol helpers
| genicp | - GenCP encode/decode, status codes, helpers
+---------------------------+
Data flow
- Discovery (
tl-gige): bind to NIC → broadcast GVCP discovery → parse replies. - Connect: establish control channel (UDP) and prepare stream endpoints if needed.
- GenApi XML (
genapi-xml): read address from device registers → fetch XML → parse to IR. - NodeMap (
genapi-core): build nodes, resolve links (Includes, Pointers, Selectors), set defaults. - Evaluation (
genapi-core):- Direct nodes read/write underlying registers via
tl-gige+genicp. - Computed nodes (e.g., SwissKnife) evaluate expressions that reference other nodes.
- Direct nodes read/write underlying registers via
- Streaming (
tl-gige): configure packet size/delay → receive GVSP → reassemble → expose frames + chunks and timestamps.
Async, threading, and I/O
- Transport uses async UDP sockets (Tokio) and bounded channels for back‑pressure.
- Frame reassembly runs on dedicated tasks; statistics are aggregated periodically.
- Node evaluation is sync from the caller’s perspective; I/O hops are awaited within accessors.
Error handling & tracing
- Errors are categorized by layer (transport/protocol/genapi/eval). Use
anyhow/custom error types at boundaries. - Enable logs with
RUST_LOG=info(ordebug,trace) and consider JSON output for tooling.
Platform considerations
- Windows/Linux/macOS supported. On Windows, run discovery once as admin to authorize firewall; consider jumbo frames per NIC for high FPS.
- Multi‑NIC hosts should explicitly select the interface for discovery/streaming.
Extending the system
- Add nodes in
genapi-coreby implementing the evaluation trait and wiring dependencies. - Add transports as new
tl-*crates behind a trait the façade can select at runtime. - Keep
genicamthin: compose transport + NodeMap + utilities; keep heavy logic in lower crates.
Crates overview
The genicam-rs workspace is split into small crates that mirror the structure of
the GenICam ecosystem:
- Protocols & transport (GenCP, GVCP/GVSP)
- GenApi XML loading & evaluation
- Public “facade” API for applications
- Command-line tooling for everyday camera work
This chapter is the “map of the territory”. It tells you which crate to use for a given task, and where to look if you want to hack on internals.
Quick map
| Crate | Path | Role / responsibility | Primary audience |
|---|---|---|---|
genicp | crates/genicp | GenCP encode/decode + helpers for control path over GVCP | Contributors, protocol nerds |
tl-gige | crates/tl-gige | GigE Vision transport: GVCP (control) + GVSP (streaming) | End-users & contributors |
genapi-xml | crates/genapi-xml | Load GenICam XML from device / disk, parse into IR | Contributors (XML / SFNC work) |
genapi-core | crates/genapi-core | NodeMap implementation, feature access, SwissKnife, selectors | End-users & contributors |
genicam | crates/genicam | High-level “one crate” façade combining transport + GenApi | End-users |
gencamctl | crates/gencamctl | CLI tool for discovery, configuration, streaming, benchmarks | End-users, ops, CI scripts |
If you just want to use a camera from Rust, you’ll usually start with
genicam (or gencamctl from the command line) and ignore the lower layers.
How the crates fit together
At a high level, the crates compose like this:
┌───────────────┐ ┌────────────────┐
│ genicp │ │ genapi-core │
│ GenCP encode │ │ NodeMap, │
│ / decode │ │ SwissKnife, │
└─────┬─────────┘ │ selectors │
│ └──────┬─────────┘
│ │
┌─────▼─────────┐ ┌──────▼─────────┐
│ tl-gige │ │ genapi-xml │
│ GVCP / GVSP │ │ XML loading & │
│ packet I/O │ │ schema-lite IR │
└─────┬─────────┘ └──────┬─────────┘
│ │
└──────────┬────────────┘
│
┌─────▼─────┐
│ genicam │ ← public Rust API
└─────┬─────┘
│
┌─────▼───────┐
│ gencamctl │ ← CLI on top of `genicam`
└─────────────┘
Roughly:
tl-gigeknows how to talk UDP to a GigE Vision device (discovery, register access, image packets, resends, stats, …).genicpprovides the GenCP building blocks used on the control path.genapi-xmlfetches and parses the GenApi XML that describes the device’s features.genapi-coreturns that XML into a NodeMap you can read/write, including SwissKnife expressions and selector-dependent features.genicamstitches all of the above into a reasonably ergonomic API.gencamctlexposes common workflows from genicam ascargo run -p gencamctl -- ….
⸻
When to use which crate
I just want to use my camera from Rust
Use genicam.
Typical tasks:
- Enumerate cameras on a NIC
- Open a device, read/write features by name
- Start a GVSP stream, iterate over frames, look at stats
- Subscribe to events or send action commands
Start with the examples under crates/genicam/examples/ and the Tutorials.
⸻
I want a command-line tool for daily work
Use gencamctl.
Typical tasks:
- Discovery: list all cameras on a given interface
- Register/feature inspection and configuration
- Quick streaming tests and stress benchmarks
- Enabling/disabling chunk data, configuring events
This is also a good reference for how to structure a “real” application on top of genicam.
⸻
I need to touch GigE Vision packets / low-level transport
Use tl-gige (and genicp as needed).
Example reasons:
- You want to experiment with MTU, packet delay, resend logic, or custom stats
- You’re debugging interoperability with a weird device and need raw GVCP/GVSP
- You want to build a non-GenApi tool that only tweaks vendor-specific registers
The tl-gige chapter goes into more detail on discovery,
streaming, events, actions, and tuning.
⸻
I want to work on GenApi / XML internals
Use genapi-xml and genapi-core.
Typical contributor activities:
- Supporting new SFNC features or vendor extensions
- Improving SwissKnife coverage or selector handling
- Adding tests for tricky XML from specific camera families
The following chapters are relevant:
If you’re not sure where a GenApi bug lives, the rule of thumb is:
- “XML can’t be parsed” → genapi-xml
- “Feature exists but behaves wrong” → genapi-core
- “Device returns odd data / status codes” → tl-gige or genicp
⸻
I need a single high-level entry point
Use genicam.
This crate aims to expose just enough control/streaming surface for most applications without making you think about transports, XML, or NodeMap internals.
The genicam crate chapter shows:
- How to go from “no camera” to “frames in memory” in ~20 lines
- How to query and set features safely (with proper types)
- How to plug in your own logging, error handling, and runtime
⸻
Crate deep dives
The rest of this section of the book contains crate-specific chapters:
- GenCP: genicp– control protocol building blocks.
- GigE Vision transport:
tl-gige– discovery, streaming, events, actions. - GenApi XML loader:
genapi-xml– getting from device to IR. - GenApi core & NodeMap:
genapi-core– evaluating features, including SwissKnife. - Facade API:
genicam– the crate most end-users start with. - Future / helper crates – notes on planned additions.
If you’re reading this for the first time, a good path is:
- Skim this page.
- Read the genicam chapter.
- Jump to tl-gige or genapi-core when you hit something you want to tweak.
genicp — Transport‑agnostic GenICam Control Primitives
genicp provides transport‑agnostic control messages and helpers for GenICam device access. It captures the common semantics used by GigE Vision (GVCP control channel), USB3 Vision, and other transports: read/write registers and memory blocks, status codes, request‑ack correlation, and binary utilities (bitfields, masks).
Why a separate crate? It lets higher layers (
genapi-core,genapi-xml, and the publicgenicamfaçade) encode control operations once, while each transport crate (e.g.,tl-gige) focuses on socket/I/O details.
Mental model
- Request → Acknowledge: Every operation has a request (with a unique
req_id) and a matching acknowledge with astatusand optional payload. - Operations (core set):
ReadReg { addr, width }(8/16/32) andWriteReg { addr, value }ReadMem { addr, len }andWriteMem { addr, bytes }
- Status mapping: Acknowledge carries a transport‑neutral
Status(e.g.,Success,AccessDenied,BadAddress,Timeout,Busy). - Segmentation: Large memory operations may be split across multiple requests based on the transport’s MTU/limit.
genicpcan calculate safe chunk sizes; the transport sends the chunks.
Quick API tour (typical shapes)
Note: Names below mirror the crate’s intent. If your local API differs slightly, the concepts—and the surrounding examples—still apply.
#![allow(unused)] fn main() { use genicp::{Request, Reply, Status}; // Build a request let req = Request::read_reg_u32(0x0010_0200); // ... send with a transport adapter (e.g., tl-gige) → get raw bytes ... // Parse reply let reply = Reply::from_bytes(&buf)?; match reply.status() { Status::Success => { let v = reply.as_u32()?; println!("value=0x{v:08X}"); } s => anyhow::bail!("device replied with {s:?}"), } }
High‑level helpers (bit update):
#![allow(unused)] fn main() { use genicp::bitops::{set_bits32, mask32}; // Read‑modify‑write: set bit 7 at address 0x...200 let v = read_u32(adapter, 0x0010_0200).await?; let v2 = set_bits32(v, mask32(7..=7), true); write_u32(adapter, 0x0010_0200, v2).await?; }
Requests & replies
Requests
A Request contains:
- Operation:
ReadReg/WriteReg/ReadMem/WriteMem - Address/Length: 64‑bit addresses supported in the type system; transport may restrict to 32‑bit
- Width (for
ReadReg/WriteReg): 8/16/32 - ReqId: incrementing counter used to match the reply
genicp ensures alignment (e.g., 16‑bit/32‑bit register widths) and payload sizing (e.g., memory read length > 0).
Replies
A Reply provides:
status(): Statuspayload(): &[u8](for reads)- Typed accessors:
as_u8(),as_u16(),as_u32(),as_block()
Status codes
Status is a transport‑neutral enum, e.g.:
SuccessNotImplemented/InvalidCommandBadAddress/BadValueAccessDenied/LockedBusy/TimeoutChecksumError/ProtocolError
The transport adapter maps wire‑specific codes (GVCP/U3V/etc.) to this enum.
Endianness, alignment, and masking
- Wire endianness is handled inside
genicp. Public typed accessors return host‑endian values. - Alignment: register widths must match the address alignment (8→any, 16→2‑byte aligned, 32→4‑byte aligned). Helpers assert this early.
- Bitfields:
bitopsoffers small utilities to extract/update bit ranges without manual shifts/masks.
#![allow(unused)] fn main() { use genicp::bitops::{extract_bits32, mask32}; let flags = extract_bits32(0b1011_0001, mask32(4..=7)); // → 0b1011 }
Chunking large memory operations
Transports cap a single message size (e.g., by MTU). Use the provided splitter to iterate safe chunks:
#![allow(unused)] fn main() { use genicp::chunk::ChunkPlan; let plan = ChunkPlan::for_read(/*addr*/ 0x4000_0000, /*len*/ 64 * 1024, /*max_payload*/ 1400, /*align*/ 4)?; for step in plan { // step.addr(), step.len() // build Request::read_mem(step.addr(), step.len()) and send } }
Recommendations:
- Keep payload under IP fragmentation thresholds (e.g., ≤ 1400 bytes for standard MTU).
- Honour device alignment rules; some devices require 4‑byte granularity for memory access.
Timeouts & retries
genicp defines semantic timeouts (command vs. memory) as hints. The transport enforces socket receive timeouts, retry counts, and backoff. For UDP‑based transports (GigE), retries are essential; for USB3, link‑level reliability reduces the need.
Integrating with transports
Transports implement a minimal trait so genicp can send/receive bytes:
#![allow(unused)] fn main() { pub trait ControlTransport { type Error; fn next_req_id(&mut self) -> u16; async fn send_request(&mut self, req: Request) -> Result<(), Self::Error>; async fn recv_reply(&mut self, expect_req_id: u16) -> Result<Reply, Self::Error>; } }
tl-gige implements this over GVCP sockets; a future tl-u3v would use USB3 endpoints. Higher layers (genapi-core) depend only on this trait to perform feature get/set.
Working with GenApi (selectors & SwissKnife)
- Selectors: When a feature is selector‑dependent, the NodeMap temporarily sets the selector node(s), performs the underlying register op(s) via
genicp, then restores state as needed. - SwissKnife: Expression nodes evaluate by reading inputs (which may be registers or other nodes), computing the result in host code, and returning the computed value to callers.
This means end‑users usually call get("ExposureTime"); the NodeMap and genicp handle all required register transactions.
Examples
Read a 32‑bit register
#![allow(unused)] fn main() { let v = genicp::helpers::read_u32(&mut adapter, 0x0010_0200).await?; }
Write a 32‑bit register
#![allow(unused)] fn main() { genicp::helpers::write_u32(&mut adapter, 0x0010_0200, 0x0000_0001).await?; }
Read a memory block safely
#![allow(unused)] fn main() { let bytes = genicp::helpers::read_block(&mut adapter, 0x4000_0000, 8192).await?; }
Modify a bitfield (read‑modify‑write)
#![allow(unused)] fn main() { use genicp::bitops::{mask32, set_bits32}; let v = genicp::helpers::read_u32(&mut adapter, REG_CTRL).await?; let v2 = set_bits32(v, mask32(3..=5), true); // set bits 3..5 genicp::helpers::write_u32(&mut adapter, REG_CTRL, v2).await?; }
Testing tips
- Unit tests for encode/decode, status mapping, and bitops.
- Golden packets: store a few known request/ack byte arrays and assert round‑trips.
- Fuzzing: run
arbitrary/proptestonReply::from_bytesto harden parsers. - Transport mocks: a simple in‑memory adapter that echoes canned replies makes NodeMap tests deterministic.
Gotchas & best practices
- Always match
req_idin replies; do not accept stale acks. - Use bounded chunk sizes to avoid IP fragmentation on UDP transports.
- Respect alignment; some devices NAK misaligned access.
- Keep timeouts conservative; some devices do heavy work on first access (e.g., on‑the‑fly XML assembly).
- Prefer feature‑level APIs (NodeMap) in apps; use raw
genicponly for diagnostics and vendor escape hatches.
tl-gige — GigE Vision Transport (GVCP/GVSP)
tl-gige implements GigE Vision transport primitives for Windows, Linux, and macOS:
- Discovery (GVCP) bound to a specific local interface
- Control path (GVCP register read/write, GenCP semantics)
- Events/Actions (GVCP)
- Streaming (GVSP) with reassembly, resend requests, MTU/packet-size negotiation, and basic stats
This chapter explains concepts, config knobs, and usage patterns—both via CLI (gencamctl) and via Rust APIs.
⚠️ Names in the snippets reflect the crate import style
tl_gige(Cargo packagetl-gige). If an identifier differs in your codebase, adjust accordingly—we’ll keep this page updated as APIs stabilize.
Feature matrix (current)
| Area | Capability | Notes |
|---|---|---|
| Discovery | Broadcast on chosen NIC | Bind to local IPv4; IPv6 is out of scope for GEV |
| Control | Register read/write | GVCP commands exposing GenCP-like semantics |
| Events | Event channel | Optional; device → host notifications |
| Actions | Action command | Host → device sync/trigger |
| Streaming | GVSP receive | Frame reassembly, missing-packet detection |
| Resend | GVCP resend requests | Windowed resend; vendor-dependent behavior |
| MTU | Negotiation | 1500 default; jumbo frames if NIC/network allow |
| Packet delay | Inter-packet gap | Avoids NIC/driver overrun; per-stream configurable |
| Stats | Per-stream counters | Frames, drops, resends, latency basics |
Selecting the local interface
On multi-NIC hosts, always bind to the NIC connected to your camera network. Two common ways:
- By local IPv4 (recommended for scripts/CLI):
cargo run -p gencamctl -- list --iface 192.168.0.5
- By interface name (if your API exposes it):
#![allow(unused)] fn main() { use tl_gige::net::InterfaceSelector; let sel = InterfaceSelector::ByName("Ethernet 2"); // or ByIpv4("192.168.0.5") }
Windows tip: run the first discovery as Administrator to let the firewall prompt appear and create inbound UDP rules.
Discovery (GVCP)
Discovery sends a broadcast GVCP command and collects replies for a small window (e.g., 200–500 ms). Each reply yields a DeviceInfo record (IP, MAC, manufacturer, model, name, user name, serial, firmware version, etc.).
CLI:
cargo run -p gencamctl -- list --iface 192.168.0.5
Rust pattern:
use tl_gige::{discovery::discover_on, net::InterfaceSelector, Result}; #[tokio::main] async fn main() -> Result<()> { let iface = InterfaceSelector::ByIpv4("192.168.0.5".parse().unwrap()); let devices = discover_on(iface).await?; for d in devices { println!("{} {} @ {}", d.manufacturer, d.model, d.ip); } Ok(()) }
Troubleshooting
- No devices found → check NIC, subnet, firewall, and that you chose the right interface.
- Intermittent replies → disable NIC power saving; avoid Wi‑Fi for GEV.
Control path (GVCP register access)
Most GenICam features eventually map to register reads/writes. tl-gige provides helpers to open a control session and perform 8/16/32‑bit (and block) register operations.
CLI (examples):
# Read a named feature via the high-level stack
cargo run -p gencamctl -- get --ip 192.168.0.10 --name ExposureTime
# Write a value
cargo run -p gencamctl -- set --ip 192.168.0.10 --name ExposureTime --value 5000
# Low-level register read (if exposed by CLI)
cargo run -p gencamctl -- peek --ip 192.168.0.10 --addr 0x0010_0200 --len 4
Rust pattern (low‑level):
use tl_gige::{control::ControlClient, net::InterfaceSelector, Result}; #[tokio::main] async fn main() -> Result<()> { let client = ControlClient::connect("192.168.0.10".parse().unwrap(), InterfaceSelector::ByIpv4("192.168.0.5".parse().unwrap())).await?; let val = client.read_u32(0x0010_0200).await?; client.write_u32(0x0010_0200, val | 0x1).await?; Ok(()) }
Integrates with GenApi: higher layers (NodeMap) compute addressing (incl. SwissKnife expressions and selectors) and call into the control client.
Events & Actions (GVCP)
- Events: device → host notifications (e.g., exposure end). Enable in the device, bind a socket, and poll/await event messages.
- Actions: host → many devices synchronization (same action ID). Configure device keys and send an action command with a scheduled timestamp if supported.
Behavior varies by vendor; keep time bases consistent if you schedule actions.
Streaming (GVSP)
A GVSP receiver must:
- Negotiate stream parameters (channel, packet size, destination host/port).
- Receive UDP packets and reassemble frames by block ID.
- Detect loss, trigger resend via GVCP, and time out stale frames.
- Optionally parse chunk data after the image payload.
CLI (basic):
# Auto-negotiate packet size and store first 2 frames
cargo run -p gencamctl -- stream --ip 192.168.0.10 --iface 192.168.0.5 --auto --save 2
Rust pattern (high‑level sketch):
#![allow(unused)] fn main() { use tl_gige::{stream::{StreamBuilder, ResendPolicy}, net::InterfaceSelector}; let stream = StreamBuilder::new("192.168.0.10".parse().unwrap()) .interface(InterfaceSelector::ByIpv4("192.168.0.5".parse().unwrap())) .packet_size_auto(true) // negotiate MTU / SCPS .inter_packet_delay(Some(3500)) // ns or device units, per API .resend_policy(ResendPolicy::Windowed { max_attempts: 2, window_packets: 64 }) .socket_rcvbuf_bytes(16 * 1024 * 1024) // increase OS receive buffer .build() .await?; while let Some(frame) = stream.next().await { /* reassembled frame bytes + metadata */ } }
Resend
- Windowed resend: track missing packet ranges within a frame and request them once or twice.
- Cut‑loss threshold: abandon a frame when late/missing packets exceed a limit to avoid backpressure.
MTU & Packet Size
- Start with 1500 MTU. If NIC/network support jumbo frames, negotiate 8–9 kB packet size for fewer syscalls.
- Ensure both camera and NIC are configured for the desired MTU.
Inter‑packet Delay (IPD)
- Add a small delay between packets at the source to prevent RX ring overflow on the host/NIC.
- Useful on older NICs, laptops, and Windows where socket buffers may be smaller.
Socket buffers
- Increase
SO_RCVBUFto 8–32 MiB on the receive socket when streaming high‑rate video. - On Linux,
net.core.rmem_maxmay cap this; on Windows/macOS, OS caps also apply.
Chunk mode
- When chunks are enabled, the payload contains image followed by one or more chunk blocks (ID, length, data).
- Parsers should skip unknown chunk IDs gracefully.
Timestamp mapping
- Many devices expose a tick counter. Keep a linear mapping
(tick → host time)using the first packets of each frame or periodic sync.
Configuration knobs (cheat sheet)
| Knob | Purpose | Typical value |
|---|---|---|
--iface <IPv4> | Bind discovery/stream to NIC | Your NIC IP, e.g., 192.168.0.5 |
--packet-size / --auto | Fixed vs. negotiated packet size | --auto first, then pin |
--ipd | Inter‑packet delay | 2000–8000 (units per API) |
--rcvbuf | Socket receive buffer | 8–32 MiB |
--resend | Resend policy | windowed,max=2,win=64 |
--save N | Write first N frames | Debugging/validation |
Map these to environment variables if you prefer config files for deployments.
Error handling & logging
Enable logs with RUST_LOG/tracing_subscriber:
RUST_LOG=info,tl_gige=debug cargo run -p gencamctl -- stream ...
Common categories:
discovery(binds, broadcast, replies)control(register ops, timeouts)stream(packet loss, reorder, resends, frame stats)
Windows specifics
- Firewall: allow inbound UDP for discovery and the chosen stream port.
- Jumbo frames: enable on the NIC Advanced settings (and on switches).
- Buffering: larger
SO_RCVBUFhelps; keep system power plan on High performance.
Minimal end‑to‑end example
use tl_gige::{discovery::discover_on, control::ControlClient, stream::StreamBuilder, net::InterfaceSelector}; #[tokio::main] async fn main() -> anyhow::Result<()> { let iface = InterfaceSelector::ByIpv4("192.168.0.5".parse().unwrap()); let devices = discover_on(iface).await?; let cam = devices.first().expect("no cameras"); let ctrl = ControlClient::connect(cam.ip, iface).await?; // Example: ensure streaming is stopped before reconfiguring // ctrl.write_u32(REGISTER_ACQ_START_STOP, 0)?; // placeholder address let mut stream = StreamBuilder::new(cam.ip) .interface(iface) .packet_size_auto(true) .socket_rcvbuf_bytes(16 * 1024 * 1024) .build() .await?; if let Some(frame) = stream.next().await { println!("got {} bytes", frame.bytes.len()); } Ok(()) }
See also
genicp: message layouts & helpers for control pathgenapi-xmlandgenapi-core: NodeMap, selectors, SwissKnife evaluation- Tutorials: Registers, Streaming
genapi-core
genapi-core builds and evaluates the NodeMap from the device’s GenApi XML. It supports common node kinds and SwissKnife expressions.
Responsibilities
- Parse the
genapi-xmlintermediate representation into in‑memory nodes. - Resolve node references and dependencies (incl. Selectors).
- Provide
get_*/set_*operations that either access registers or evaluate expressions.
Node kinds (current)
- Integer / Float / Boolean / Enumeration / Command / String / Register
- SwissKnife — expression node referencing other nodes (read‑only). Typical syntax supports arithmetic, comparisons, logical ops, and ternary‑like conditionals.
Selectors
When a feature has selectors (e.g., GainSelector), evaluation temporarily switches the active selector context so reads/writes map to the correct addresses.
Read/write examples (via façade)
Below is a minimal flow using the genicam façade.
use genicam::Client; // façade crate #[tokio::main] async fn main() -> anyhow::Result<()> { // 1) Connect over GigE Vision to a known IP let mut cam = Client::connect("192.168.0.10").await?; // 2) Read a numeric feature (backed by register or computed by SwissKnife) let exposure: f64 = cam.get_f64("ExposureTime")?; // microseconds println!("ExposureTime = {} µs", exposure); // 3) Set a feature (register-backed) cam.set_f64("ExposureTime", 5000.0)?; // 5 ms // 4) Read a computed feature (SwissKnife) let derived: f64 = cam.get_f64("LinePeriodUs")?; // example name println!("LinePeriodUs (computed) = {}", derived); Ok(()) }
Any SwissKnife node is evaluated transparently when you call
get_*. If it references other nodes, those nodes are read or computed first.
Caching & invalidation (overview)
- Values are cached within an evaluation pass; writes invalidate dependent caches.
- Selector changes create a new evaluation context so the correct addresses/branches are used.
Errors you may see
- OutOfRange: requested value outside
[Min, Max, Inc]. - AccessDenied: node not writable in the current state.
- DependencyMissing: referenced node not found/visible.
- Transport: I/O error while reading/writing registers.
Tips for contributors
- Implement new node kinds behind a common evaluation trait.
- Keep pure computation separate from transport; call into the transport via a narrow interface.
- Add unit tests with synthetic NodeMaps (no device required) plus integration tests that hit real hardware when available.
Tutorials
This section walks you through typical workflows step by step.
The focus is a GigE Vision camera accessed over Ethernet, using:
- The
gencamctlCLI for quick experiments and ops work. - The
genicamcrate for Rust examples you can copy into your own code.
If you haven’t done so yet, first read:
They explain how to build the workspace and verify that your toolchain works.
Recommended path
If you are new to the project, the recommended reading order is:
-
Discovery
Find cameras on your network, verify that discovery works, and understand basic NIC and firewall requirements. -
Registers & features
Read and write GenApi features (e.g.ExposureTime), understand selectors such asGainSelector, and learn when you might need raw registers. -
GenApi XML
Fetch the GenICam XML from a device, inspect it, and see how it maps to the NodeMap used bygenapi-core. -
Streaming
Start a GVSP stream, receive frames, look at stats, and learn which knobs matter for throughput and robustness.
You can stop after Discovery and Streaming if you only need to verify that your camera works. The other tutorials are useful when you want to build a full application or debug deeper GenApi issues.
What you need before starting
Before running any tutorial, make sure you have:
-
A working Rust toolchain (see
rust-toolchain.tomlfor the pinned version). -
The workspace builds successfully:
cargo build --workspace • At least one GigE Vision camera reachable from your machine: • Either directly connected to a NIC. • Or via a switch on a dedicated subnet.
For networking details (MTU, jumbo frames, Windows specifics, etc.), see Networking once that chapter is filled in.
⸻
Tutorials overview • Discovery Use gencamctl and the genicam examples to find cameras and verify that basic communication is working. • Registers & features Use features by name, work with selectors, and know when to fall back to raw register access. • GenApi XML Fetch XML from the device, inspect it, and understand how genapi-xml and genapi-core use it. • Streaming Start streaming, tune packet size and delay, and interpret statistics and logging output.
Each tutorial has: • A CLI variant using gencamctl. • A Rust variant using the genicam crate and its examples.
Discovery
Goal of this tutorial:
- Verify that your host can see your GigE Vision camera.
- Learn how to run discovery from:
- The
gencamctlCLI. - The
genicamRust examples.
- The
- Understand the most common issues (NIC selection, firewall, subnets).
If discovery does not work, the other tutorials will not help much — fix this first.
Before you begin
Make sure that:
- The workspace builds:
cargo build --workspace
- Your camera and host are physically connected:
- Direct cable: host NIC ↔ camera.
- Or via a switch dedicated to the camera subnet.
- The camera has a valid IPv4 address:
- From DHCP on your camera network, or
- A static address that matches the host NIC’s subnet.
For deeper network discussion (jumbo frames, tuning, etc.), see Networking once that chapter is filled in.
⸻
Step 1 – Discover with gencamctl
The easiest way to test discovery is the gencamctl CLI, which wraps the genicam crate.
1.1. Basic discovery
Run:
cargo run -p gencamctl -- list
What to expect:
- On success, you get a table or list of devices with at least:
- IP address
- MAC address
- Model / manufacturer (if reported)
- If nothing appears:
- Check that the camera is powered and connected.
- Check that your NIC is on the same subnet as the camera.
- Check that your host firewall allows UDP broadcast on that NIC.
1.2. Selecting an interface explicitly
On multi-NIC systems, gencamctl may need to be told which interface to use.
Run:
cargo run -p gencamctl -- list --iface 192.168.0.5
Where 192.168.0.5 is the IPv4 address of your host NIC on the camera network.
If you are not sure which NIC to use:
- On Linux/macOS: use ip addr / ifconfig to inspect addresses.
- On Windows: use ipconfig and your network settings GUI.
If discovery works when --iface is specified but not without it, your machine
likely has multiple active interfaces and the automatic NIC choice is not what
you expect.
⸻
Step 2 – Discover via the genicam examples
The genicam crate comes with examples that exercise the same discovery logic from Rust code.
Run:
cargo run -p genicam --example list_cameras
This example:
- Broadcasts on your camera network.
- Prints basic info about each device it finds.
Use this when you want to:
- See how to embed discovery into your own Rust application.
- Compare behaviour between the CLI and the library (they should match).
The code for list_cameras lives under crates/genicam/examples/ and is a
good starting point for your own experiments.
⸻
Step 3 – Interpreting results
When discovery succeeds, you should record:
- The camera’s IP address (e.g. 192.168.0.10).
- Which host NIC / interface you used (e.g. 192.168.0.5).
You will reuse these values in later tutorials, e.g.:
- Registers & features: --ip 192.168.0.10
- Streaming: --ip 192.168.0.10 --iface 192.168.0.5
If you see multiple devices, you may want to label them (physically or in a note) to avoid confusion later.
⸻
Troubleshooting checklist
If gencamctl -- list or list_cameras find no devices:
- Physical link
- Is the link LED on the NIC / switch / camera lit?
- Try a different Ethernet cable or port.
- Subnets
- Host NIC and camera must be on the same subnet (e.g. both 192.168.0.x/24).
- Avoid having two NICs on the same subnet; this can confuse routing.
- Firewall
- Allow UDP broadcast on the camera NIC.
- On Windows, make sure the executable is allowed for both “Private” and “Public” networks or run inside a network profile that permits broadcast.
- Multiple NICs
- Use --iface
to force the correct interface. - Temporarily disable other NICs to confirm the problem is NIC selection.
- Use --iface
- Vendor tools
- If the vendor’s viewer can see the camera but gencamctl cannot:
- Compare which NIC / IP the vendor tool uses.
- Check whether the vendor tool reconfigured the camera’s IP (e.g. via DHCP or “force IP” features).
If discovery is still failing after this checklist, capture logs with:
RUST_LOG=debug cargo run -p gencamctl -- list --iface <host-ip>
and open an issue with the log output and a short description of your setup. This will also be useful when extending the GigE transport.
Registers & features
Goal of this tutorial:
- Read and write GenApi features such as
ExposureTimeorGain. - Understand how features map to the underlying registers.
- Learn the basics of selectors (e.g.
GainSelector) and how they affect values. - See how to do the same thing from:
- The
gencamctlCLI. - The
genicamRust examples.
- The
If you haven’t done so yet, first go through:
so you know the IP of your camera and which host interface you’re using.
Concepts: features vs registers
GenICam exposes camera configuration through features described in the GenApi XML:
- A feature has a name (
ExposureTime,Gain,PixelFormat, …). - Each feature has a type:
- Integer / Float / Boolean / Enumeration / Command, …
- Under the hood, a feature usually corresponds to one or more registers:
- A simple feature may read/write a single 32-bit register.
- More complex ones may be derived via SwissKnife expressions or depend on selectors.
The genapi-core crate:
- Loads the XML (via
genapi-xml). - Builds a NodeMap.
- Lets you read and write features by name using typed accessors.
The genicam crate and gencamctl CLI sit on top of this NodeMap and try to
hide most of the low-level details.
Step 1 – Inspect features with gencamctl
The gencamctl CLI exposes basic feature access via get and set subcommands. oai_citation:0‡GitHub
You need:
- The camera IP (from the discovery tutorial).
- Optionally, the host interface IP if you have multiple NICs.
1.1. Read a feature by name
Example: read ExposureTime from a camera at 192.168.0.10:
cargo run -p gencamctl -- \
get --ip 192.168.0.10 --name ExposureTime
You should see:
- The current value.
- The type (e.g. Float or Integer).
- Possibly range information (min/max/increment) if available.
If you prefer machine-readable output, add --json:
cargo run -p gencamctl -- \
get --ip 192.168.0.10 --name ExposureTime --json
This is handy for scripting and CI.
1.2. Write a feature by name
To change a value, use the set subcommand. For example, set exposure to 5000 microseconds:
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name ExposureTime --value 5000
Then verify:
cargo run -p gencamctl -- \
get --ip 192.168.0.10 --name ExposureTime
If the value doesn’t change:
- The feature may be read-only (depending on acquisition state).
- There may be constraints (e.g. limited range, alignment).
- Another feature (like ExposureAuto) may be overriding manual control.
Those cases are described in more depth in the genapi-core chapter.
⸻
Step 2 – Work with selectors
Many cameras use selectors to multiplex multiple logical settings onto the same underlying registers. A common pattern is:
- GainSelector = All, Red, Green, Blue, …
- Gain = value for the currently selected channel.
When you change GainSelector, you are effectively changing which “row” you are editing. The NodeMap takes care of switching the right registers.
2.1. Inspect which selectors exist
You can use gencamctl to dump a selector feature and see its possible values. For example, to inspect GainSelector:
cargo run -p gencamctl -- \
get --ip 192.168.0.10 --name GainSelector --json
Look for:
- The current value (e.g. "All").
- The list of allowed values / enum entries.
2.2. Change a feature through a selector
To set different gains for different channels, a typical sequence is:
# Select the red channel, then set Gain
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name GainSelector --value Red
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name Gain --value 5.0
# Select the blue channel, then set Gain
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name GainSelector --value Blue
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name Gain --value 3.0
From your perspective, you are just changing features. Internally, genapi-core:
- Evaluates the selector.
- Resolves which nodes and registers are active.
- Applies any SwissKnife expressions as needed.
The selectors_demoexample in the genicam crate shows this pattern in Rust. 
⸻
Step 3 – Do the same from Rust (genicam examples)
The genicam crate provides examples that mirror the CLI operations. 
3.1. Basic get/set example
Run the get_set_feature example:
cargo run -p genicam --example get_set_feature
This example demonstrates:
- Opening a camera (e.g. by IP or by index).
- Getting a feature by name.
- Printing its value and metadata.
- Setting a new value and verifying it.
Inspect the source under crates/genicam/examples/get_set_feature.rs for a minimal template you can reuse in your own project.
Typical pseudo-flow inside that example (simplified):
#![allow(unused)] fn main() { // Pseudocode sketch — see the actual example for details let mut ctx = genicam::Context::new()?; let cam = ctx.open_by_ip("192.168.0.10".parse()?)?; let mut nodemap = cam.nodemap()?; // Read a float feature let exposure: f64 = nodemap.get_float("ExposureTime")?; println!("ExposureTime = {} us", exposure); // Write a new value nodemap.set_float("ExposureTime", 5000.0)?; }
Types and method names may differ slightly; always follow the real example in the repository for exact signatures.
3.2. Selectors demo
To see selector logic in code, run:
cargo run -p genicam --example selectors_demo
This example walks through:
- Enumerating selector values.
- Looping over them to set/read the associated feature.
- Printing out the effective values per selector.
This is a good reference if you need to build a UI that exposes per-channel settings (e.g. separate gains per color channel).
⸻
Step 4 – When you might need raw register access
Most applications should prefer feature-by-name access via GenApi:
- You get type safety (integers vs floats vs enums).
- You respect vendor constraints and SFNC behaviour.
- Your code is more portable across cameras.
However, there are cases where raw registers are still useful:
- Debugging unusual vendor behaviour or firmware bugs.
- Working with undocumented features that are not in the XML.
- Bringing up very early prototypes where the GenApi XML is incomplete.
The lower-level crates (tl-gige and genicp) expose primitives for reading and writing device memory directly. Refer to:
for details and examples. Be careful: writing to arbitrary registers can easily put the device into an unusable state until power-cycled.
⸻
Recap
After this tutorial you should be able to:
- Read and write GenApi features by name using gencamctl.
- Understand and use selector features (e.g. GainSelector → Gain).
- Locate and run the genicam examples (get_set_feature, selectors_demo) as templates for your own applications.
- Know that raw register access exists, but is usually a last resort.
Next step: GenApi XML— how the XML is fetched and turned into the NodeMap that backs these features.
GenApi XML
Goal of this tutorial:
- Understand what the GenICam XML is and where it lives.
- See how
genapi-xml:- Fetches the XML from the device (via the FirstURL register).
- Parses it into a lightweight internal representation.
- Learn how to call this from Rust using a simple memory reader closure.
- Know when you actually need to look at the XML (and when you don’t).
You should already have:
- Completed Discovery.
- Completed Registers & features, or at least be comfortable with the idea of features (ExposureTime, Gain, etc.) backed by registers.
1. What is the GenICam XML?
Every GenICam-compliant device exposes a self-description XML file:
- It lists all features the device supports (name, type, access mode, range).
- It defines how those features map to device registers.
- It encodes categories, selectors, and SwissKnife expressions.
- It declares which version of the GenApi schema the file uses.
This XML is normally stored in the device’s non-volatile memory. On the control path, the host:
- Reads the FirstURL register at a well-known address (0x0000).
- Interprets it as a URL that tells where the XML actually lives:
- Often a “local” memory address + size.
- In theory, it could be
http://…orfile://…as well.
- Reads the XML bytes from that location.
- Hands the XML string to a GenApi implementation (here:
genapi-core).
The genapi-xml crate encapsulates steps 1–3:
- Discover where to read XML from.
- Read it over the existing memory read primitive.
- Parse it into a simple, Rust-friendly model that the rest of the stack uses.
2. Overview of genapi-xml
At a high level, genapi-xml provides three building blocks:
- A function that fetches the XML from the device using a memory reader:
#![allow(unused)] fn main() { // Rough shape / pseudocode pub async fn fetch_and_load_xml<F, Fut>( read_mem: F, ) -> Result<String, XmlError> }
where
#![allow(unused)] fn main() { F: FnMut(u64, usize) -> Fut, Fut: Future<Output = Result<Vec<u8>, XmlError>>; }
- A function that parses XML into minimal metadata (schema version, top-level features) without understanding every node type:
#![allow(unused)] fn main() { pub fn parse_into_minimal_nodes(xml: &str) -> Result<MinimalXmlInfo, XmlError>; }
- A function that parses XML into a full XmlModel consisting of a flat list of node declarations (Integer, Float, Enum, Boolean, Command, Category, SwissKnife, …), including addressing and selector metadata.
You normally will not call these directly in application code (the genicam crate does this for you), but they are useful when:
- Debugging why a particular feature behaves a certain way.
- Inspecting how a vendor encoded selectors or SwissKnife expressions.
- Adding support for new node types or schema variations in genapi-core.
⸻
3. Fetching XML from a device in Rust
This section shows how you could call genapi-xml directly. The exact types in your code will differ depending on whether you start from genicam or tl-gige, but the pattern is always the same:
- Open a device.
- Provide a read_mem(addr, len) async function/closure.
- Call fetch_and_load_xml(read_mem).
3.1. Memory reader closure concept
fetch_and_load_xml does not know about GVCP, sockets, or cameras. It only knows how to call a function with this shape:
#![allow(unused)] fn main() { async fn read_mem(address: u64, length: usize) -> Result<Vec<u8>, XmlError>; }
Internally it will:
- Read up to a small buffer (e.g. 512 bytes) at address 0x0000.
- Interpret that buffer as a C string containing the FirstURL.
- Parse the URL and decide where to read the XML from.
- Read that region into memory and return it as a String.
Your job is to plug in a closure that uses whatever transport you have:
- A genicam device method (e.g. device.read_memory(address, length)).
- A low-level tl-gige control primitive.
3.2. Example: fetch XML using a genicam-style device
Below is illustrative pseudocode. Use it as a template and adapt to the actual types in your project.
#![allow(unused)] fn main() { use genapi_xml::{fetch_and_load_xml, XmlError}; use std::future::Future; async fn fetch_xml_for_device() -> Result<String, XmlError> { // 1. Open your device using the higher-level API. // Exact API varies; adjust to your real `genicam` / `tl-gige` types. let mut ctx = genicam::Context::new().map_err(|e| XmlError::Transport(e.to_string()))?; let mut dev = ctx .open_by_ip("192.168.0.10".parse().unwrap()) .map_err(|e| XmlError::Transport(e.to_string()))?; // 2. Define a memory reader closure. // It must accept (address, length) and return bytes. let mut read_mem = move |addr: u64, len: usize| { async { // Replace `read_memory` with the actual method you have. let bytes = dev .read_memory(addr, len) .await .map_err(|e| XmlError::Transport(e.to_string()))?; Ok(bytes) } }; // 3. Ask `genapi-xml` to follow FirstURL and return the XML document. let xml = fetch_and_load_xml(&mut read_mem).await?; Ok(xml) } }
Key points:
- The closure is async and can perform chunked transfers internally.
- XmlError::Transport is used to wrap any transport-level errors.
- HTTP / file URLs are currently treated as Unsupported in XmlError; the typical GigE Vision case uses a local memory address.
⸻
4. Inspecting minimal XML metadata
Once you have the XML string, you can parse just enough to answer questions like:
- “Which GenApi schema version does this camera use?”
- “What are the top-level categories / features?”
- “Does this XML look obviously broken?”
genapi-xml exposes a lightweight parse function for that:
#![allow(unused)] fn main() { use genapi_xml::{parse_into_minimal_nodes, XmlError}; fn inspect_xml(xml: &str) -> Result<(), XmlError> { let info = parse_into_minimal_nodes(xml)?; if let Some(schema) = &info.schema_version { println!("GenApi schema version: {schema}"); } else { println!("GenApi schema version: (not found)"); } println!("Top-level features / categories:"); for name in &info.top_level_features { println!(" - {name}"); } Ok(()) } }
This is intentionally lossy: it does not understand every node type. Its job is to be:
- Fast enough for quick sanity checks.
- Robust to schema extensions that are not yet implemented.
Use this when you just need to confirm that:
- The XML is parseable at all.
- It roughly matches expectations for your camera family.
⸻
5. From XML to a full NodeMap
The next step (handled elsewhere in the stack) is:
- Parse XML into an XmlModel: a flat list of NodeDecl entries that
carry:
- Feature name and type (Integer/Float/Enum/Bool/Command/Category/SwissKnife).
- Addressing information (fixed / selector-based / indirect).
- Access mode (RO/WO/RW).
- Bitfield and byte-order information.
- Selector relationships and SwissKnife expressions.
- Feed this XmlModel into genapi-core, which:
- Instantiates a NodeMap.
- Resolves feature dependencies, selectors, and expressions at runtime.
- Exposes typed getters/setters like get_float("ExposureTime").
You do not need to perform this plumbing manually in a typical application:
- The genicam crate will fetch and parse XML as part of its device setup.
- The gencamctl CLI uses that same pipeline when you call get / set on features.
If you want the gory details, see: - GenApi XML loader: genapi-xml - GenApi core & NodeMap: genapi-core
(these chapters go into internal structures and how to extend them).
⸻
6. When should you look at the XML?
Most of the time, you can treat the XML as an implementation detail and just:
- Use gencamctl for manual experimentation.
- Use genicam’s NodeMap accessors from Rust.
You should crack open the XML when:
- A feature behaves differently from the SFNC documentation.
- Selectors are not doing what you expect.
- You hit a SwissKnife or bitfield corner case.
- You are adding support for a new vendor-specific wrinkle to genapi-core.
Typical workflow:
- Use your transport or genicam helper to dump the XML to a file.
- Run parse_into_minimal_nodes to quickly confirm schema and top-level layout.
- Run the “full” XML → XmlModel path (via the crate internals) when working on genapi-core changes.
- Use a normal XML editor / viewer when manually exploring categories and features.
⸻
7. Recap
After this tutorial you should:
- Know what the GenICam XML is and how it relates to features and registers.
- Understand how genapi-xml uses FirstURL and a memory reader closure to retrieve the XML document from the device.
- Be able to write a small Rust helper that:
- Fetches the XML with fetch_and_load_xml.
- Inspects basic metadata with parse_into_minimal_nodes.
- Know when it is worth digging into XML versus staying at the feature level.
Next up: Streaming — actually getting image data out of the camera, now that you know how its configuration is described.
Streaming
Goal of this tutorial:
- Start a GVSP stream from your camera.
- See how to:
- Run streaming from the
gencamctlCLI. - Run a basic streaming example from the
genicamcrate.
- Run streaming from the
- Understand the key knobs for stability:
- Packet size / MTU
- Packet delay
- Resends and backpressure
You should already have:
- Completed Discovery and know:
- The camera IP address (e.g.
192.168.0.10). - The host NIC / interface used for the camera (e.g.
192.168.0.5).
- The camera IP address (e.g.
- Ideally gone through Registers & features so you can configure basic camera settings.
1. Basics: how GVSP streaming works
Very simplified:
- On the control path (GVCP / GenCP), you configure:
- Pixel format, ROI, exposure, etc.
- Streaming destination (host IP / port).
- Whether the camera uses resends, chunk data, etc.
- When you tell the camera to start acquisition, it begins sending:
- GVSP data packets (your image payload).
- Occasionally leader/trailer or event packets, depending on mode.
- The host reassembles packets into complete frames, handles resends and timeouts, and exposes a stream of “frames + stats” to you.
The tl-gige crate owns the low-level GVSP packet handling. The genicam
crate builds on that to present a higher-level streaming API. gencamctl
wraps genicam in a CLI.
2. Streaming with gencamctl
The exact flags may evolve; always check:
cargo run -p gencamctl -- stream --help
for the authoritative list. The examples below illustrate the typical usage pattern.
2.1. Start a basic stream
Start a stream from a camera at 192.168.0.10 using the host interface 192.168.0.5:
cargo run -p gencamctl -- \
stream --ip 192.168.0.10 --iface 192.168.0.5
What you should expect:
- A textual status showing:
- Frames received.
- Drops / incomplete frames.
- Resend statistics (if the camera supports resends).
- Measured throughput (MB/s or similar).
- The tool may run until you interrupt it (Ctrl+C), or it may have:
- A --count option (receive N frames).
- A --duration option (run for N seconds).
If you see no frames:
- Double-check that streaming is enabled on the camera.
- Ensure you haven’t configured a different destination IP / port in a vendor tool.
- Make sure the iface IP you pass is the one the camera can reach.
2.2. Saving frames to disk
Many users want to save frames as a quick sanity check or for offline analysis. If gencamctl stream exposes options like --output / --dir / --save, use them; for example:
cargo run -p gencamctl -- \
stream --ip 192.168.0.10 --iface 192.168.0.5 \
--count 100 --output ./frames
Typical behaviour:
- Create a directory.
- Save each frame as:
- Raw bytes (e.g. .raw), or
- PGM/PPM (.pgm / .ppm), or
- Some simple container format.
If you are unsure which formats are supported, check --help or the gencamctl crate documentation.
Saved frames are useful to:
- Inspect pixel data in an image viewer or with Python/OpenCV.
- Compare against the vendor’s viewer for debugging.
⸻
3. Streaming from Rust using genicam
The genicam crate usually offers one or more streaming examples (search for stream_ in crates/genicam/examples/).
Run the simplest one, for example:
cargo run -p genicam --example stream_basic
(If the actual example name differs, adapt accordingly.)
What such an example typically does:
- Open a device (by IP or index).
- Configure basic streaming parameters if needed (pixel format, ROI, exposure).
- Build a stream (e.g. using a StreamBuilder or similar).
- Start acquisition and iterate over frames in a loop.
- Print per-frame stats or a summary.
A typical pseudo-flow (simplified, not exact code):
// Pseudocode sketch — see the actual example for real API use genicam::prelude::*; fn main() -> anyhow::Result<()> { // 1. Context and device let mut ctx = Context::new()?; let mut dev = ctx.open_by_ip("192.168.0.10".parse()?)?; // 2. Optional: tweak features before streaming let mut nodemap = dev.nodemap()?; nodemap.set_enum("PixelFormat", "Mono8")?; nodemap.set_float("AcquisitionFrameRate", 30.0)?; // 3. Build a stream let mut stream = dev.build_stream()?.start()?; // 4. Receive frames in a loop for (i, frame) in stream.iter().enumerate() { let frame = frame?; println!( "Frame #{i}: {} x {}, ts={:?}, drops={} resends={}", frame.width(), frame.height(), frame.timestamp(), frame.stats().dropped_frames, frame.stats().resends, ); if i >= 99 { break; } } Ok(()) }
Use the real example as the ground truth for API names and error handling.
⸻
4. Tuning for stability and performance
Streaming is where GigE Vision setup matters most. A few knobs you will encounter (some via camera features, some via host configuration):
4.1. Packet size and MTU
- Packet size too large for your NIC / path MTU:
- Packets get fragmented or dropped.
- High drop/resend counts.
- Packet size too small:
- More packets per frame → more overhead.
- Higher chance of bottleneck at CPU or driver level.
Typical approach:
- Enable jumbo frames on the camera network (e.g. MTU 9000) if your switch/NIC support it.
- Set camera packet size slightly below MTU (e.g. 8192 for MTU 9000).
- Observe throughput and drop/resend statistics.
4.2. Packet delay (inter-packet gap)
Some cameras allow setting an inter-packet delay or packet interval:
- Too little delay:
- Bursty traffic, easily overloading switches, NICs, or host buffers.
- Modest delay:
- Smoother traffic at the cost of slightly higher end-to-end latency.
If your stats show frequent drops/resends at high frame rates:
- Try increasing the packet delay slightly.
- Monitor if drops/resends go down while throughput remains acceptable.
4.3. Resends and backpressure
GVSP supports packet resends:
- The host tracks missing packets in a frame.
- It requests resends from the camera.
- The camera re-sends the missing packets.
The tl-gige layer surfaces statistics like:
- Dropped packets.
- Number of resend requests.
- Number of resent packets actually received.
Use these metrics to:
- Detect whether your current network configuration is “healthy”.
- Compare different NICs, cables, or switches.
⸻
5. Troubleshooting streaming issues
If streaming starts but is unreliable, here is a practical checklist:
- Packet drops / resends spike immediately
- Check MTU and packet size alignment.
- Try lowering frame rate or resolution temporarily.
- Use a dedicated NIC and switch if possible.
- No frames arrive, but discovery and feature access work
- Confirm the camera is configured to send to your host IP / port.
- Ensure no other tool (vendor viewer) is already consuming the stream.
- Double-check any firewall rules that might block UDP on the stream port.
- Frames arrive but with wrong size or format
- Verify PixelFormat and ROI in the NodeMap (gencamctl get / set).
- Confirm your code interprets the buffer layout correctly (Mono8 vs Bayer).
- Intermittent hiccups under load
- Look at CPU usage and other traffic on the same NIC.
- Consider enabling jumbo frames and increasing packet delay.
- On Windows, ensure high-performance power profile and up-to-date NIC drivers.
When in doubt:
- Save a small sequence of frames to disk.
- Capture logs at a higher verbosity (e.g. RUST_LOG=debug).
- Compare behaviour with the vendor’s viewer using the same network setup.
⸻
6. Recap
After this tutorial you should be able to:
- Start a GVSP stream using gencamctl.
- Run a streaming example from the genicam crate.
- Interpret basic streaming stats (frames, drops, resends, throughput).
- Know which knobs to tweak first when streaming is unreliable:
- MTU, packet size, packet delay, frame rate, dedicated NIC.
For more detailed background on how GVCP/GVSP packets are handled internally, see the tl-gigecrate chapter.
Next steps:
- Networking — a more systematic look at NIC configuration, MTU, and common deployment topologies.
- Later: dedicated crate chapters for
tl-gigeandgenicamfor contributor-level details.
Networking
This chapter is a practical GigE Vision networking cookbook.
It focuses on:
- Typical topologies (direct cable vs switch, single vs multi-camera).
- NIC and IP configuration on Windows, Linux, and macOS.
- MTU / jumbo frames and packet delay basics.
- Common pitfalls and troubleshooting.
It is not a replacement for vendor or A3 documentation, but gives you enough
background to make gencamctl and the genicam examples work reliably. oai_citation:0‡Wikipedia
If you have not yet done so, first go through:
They show the CLI and Rust-side pieces that depend on a working network setup.
1. Typical topologies
1.1. Single camera, direct connection
The simplest and most robust setup:
[Camera] <── Ethernet cable ──> [Host NIC]
Characteristics:
- One camera, one host, one NIC.
- No other traffic on that link.
- Easy to reason about MTU and packet delay.
Recommended when:
- You’re bringing up a new camera.
- You’re debugging issues and want to remove variables.
1.2. One or more cameras through a switch
Common in real systems:
[Cam A] ──\
\
[Cam B] ────[Switch]──[Host NIC]
/
[Cam C] ─/
Characteristics:
- Multiple cameras share the link to the host.
- Switch must handle the aggregate throughput.
- Switch configuration (buffer sizes, jumbo frames, spanning tree) matters. 
Recommended when:
- You need more than one camera.
- You need long cable runs or multi-drop layouts.
1.3. Host with multiple NICs
For high throughput or separation from office traffic:
[Cam network] <── NIC #1 ──> [Host] <── NIC #2 ──> [Office / internet]
Characteristics:
- Camera traffic isolated from general network.
- Easier to tune MTU, QoS, and firewall rules.
- In discovery and streaming, you may need to specify --iface
.
Recommended for:
- High data rates.
- Multi-camera setups.
- Systems that must not be disturbed by office network traffic.
⸻
2. IP addressing basics
GigE Vision uses standard IPv4 + UDP. Each device needs a valid IPv4 address; the host and camera(s) must share a subnet. 
2.1. Choose a camera subnet
Pick a private network, for example:
- 192.168.0.0/24 (addresses 192.168.0.1–192.168.0.254)
- 10.0.0.0/24
Decide on:
- One address for your host NIC (e.g. 192.168.0.5).
- One address per camera (e.g. 192.168.0.10, 192.168.0.11, …).
Make sure this subnet does not conflict with your office / internet network.
2.2. Windows
- Open Network & Internet Settings → Change adapter options.
- Right-click the NIC used for cameras → Properties.
- Select Internet Protocol Version 4 (TCP/IPv4) → Properties.
- Choose Use the following IP address:
- IP address: e.g. 192.168.0.5
- Subnet mask: 255.255.255.0
- Gateway: leave empty (for isolated camera networks).
- Turn off any “energy saving” features for this NIC in the driver settings if possible (they can introduce latency/jitter).
On first run, Windows firewall may pop up asking whether to allow the binary on Private / Public networks. Allow it on the relevant profile so UDP broadcasts work.
2.3. Linux
Use either NetworkManager or manual configuration.
Manual example:
# Assign IP and bring interface up (replace eth1 with your device)
sudo ip addr add 192.168.0.5/24 dev eth1
sudo ip link set eth1 up
To make this permanent, use your distro’s network configuration tools (e.g. Netplan on Ubuntu, ifcfg files on RHEL, etc.).
2.4. macOS
Use System Settings → Network:
- Select the camera NIC (e.g. USB Ethernet).
- Set “Configure IPv4” to “Manually”.
- Enter:
- IP address: 192.168.0.5
- Subnet mask: 255.255.255.0
- Leave router/gateway empty for a dedicated camera network.
⸻
3. MTU and jumbo frames
MTU (Maximum Transmission Unit) determines the largest Ethernet frame size. Standard MTU is 1500 bytes; jumbo frames extend this (e.g. 9000 bytes). For large images, jumbo frames can significantly reduce protocol overhead and CPU load. 
3.1. When to care
You probably need to look at MTU when:
- Frame sizes are large (multi-megapixel).
- Frame rates are high (tens or hundreds of FPS).
- You see lots of packet drops or resends at otherwise reasonable loads.
For simple bring-up and low/moderate data rates, standard MTU=1500 usually works.
3.2. Enabling jumbo frames
All components in the path must agree:
- Camera
- Switch (if present)
- Host NIC
Typical steps:
- Camera: set
GevSCPSPacketSizeor similar feature to a value below the path MTU (e.g. 8192 for MTU 9000). You can use gencamctl set to adjust this. - Switch: enable jumbo frames in the management UI (name and steps vary by vendor).
- Host NIC:
- Windows: NIC properties → Advanced → Jumbo Packet or similar.
- Linux: sudo ip link set dev eth1 mtu 9000
- macOS: some drivers expose MTU setting in the network settings; others do not support jumbo frames.
After changing MTU, confirm with:
# Linux example
ip link show eth1
and check that TX/RX MTU matches your expectation.
⸻
4. Packet delay and flow control
Some cameras allow configuring inter-packet delay or packet interval:
- Without delay:
- Camera sends packets as fast as possible.
- High instantaneous bursts can overwhelm NICs / switches.
- With modest delay:
- Traffic is smoother at the cost of a small increase in latency.
If you see high packet loss or many resends at high frame rates:
- Try slightly increasing the inter-packet delay.
- Observe:
- Does the drop/resend rate decrease?
- Is overall throughput still sufficient?
Some vendors also expose “frame rate limits” or “burst size” options. These can also be used to ease pressure on the network at the cost of lower peak FPS. 
⸻
5. Multi-camera considerations
When running multiple cameras:
- Total throughput is roughly the sum of each camera’s stream.
- The bottleneck can be:
- The switch’s uplink to the host.
- The host NIC’s capacity.
- Host CPU / memory bandwidth.
Practical tips:
- Prefer a dedicated NIC for cameras.
- For 2–4 high-speed cameras, consider:
- Multi-port NICs.
- Separating cameras onto different NICs if possible.
- Stagger packet timing:
- Slightly different inter-packet delays for each camera.
- Slightly different frame rates, where acceptable.
Monitor:
- Per-camera stats (drops, resends, throughput).
- Host CPU usage.
- Switch port statistics if your hardware exposes them.
⸻
6. Using --iface and discovery quirks
On systems with more than one active NIC, automatic interface selection might pick the wrong one.
- In gencamctl, use --iface
to force the correct NIC. - In Rust examples, pass the desired local address when building the context or stream (see the genicam and tl-gige crate chapters for details).
If discovery only works when you specify --iface, but not without it:
- You likely have:
- Multiple NICs on overlapping subnets, or
- A default route that prefers a different interface.
- This is not unusual; be explicit for production setups.
⸻
7. Troubleshooting checklist
Use this checklist when things don’t work as expected.
7.1. Discovery fails
See also the troubleshooting section in Discovery.
- Check link LEDs on camera, switch, and NIC.
- Confirm IP addressing:
- Host and camera on same subnet.
- No conflicting IPs.
- Check firewall:
- Allow UDP broadcast / unicast on the camera NIC.
- Temporarily:
- Disable other NICs to simplify routing.
- Try a direct cable instead of a switch.
7.2. Streaming is unstable (drops / resends)
- Check MTU vs packet size; avoid exceeding path MTU.
- For high data rates:
- Enable jumbo frames end-to-end (camera, switch, NIC).
- Reduce stress:
- Lower frame rate or ROI.
- Increase inter-packet delay slightly.
- Ensure dedicated NIC and switch where possible.
- Watch host CPU; if it’s near 100%, consider:
- Better NIC / driver.
- Moving processing off to another thread / core.
7.3. Vendor tool works, genicam-rs does not
Compare:
- Which NIC / IP the vendor tool uses.
- The camera’s configured stream destination (IP/port).
- The vendor tool might:
- Use a different MTU / packet size.
- Adjust inter-packet delay automatically.
- Try to replicate those parameters with gencamctl and the NodeMap.
⸻
- Recap
After this chapter you should: • Understand basic GigE Vision network topologies and when to use each. • Be able to configure a host NIC and camera addresses on Windows, Linux, and macOS. • Know when and how to enable jumbo frames and adjust packet delay. • Have a structured approach to debugging discovery and streaming issues.
For protocol-level details and tuning options exposed by this project: • See tl-gige for transport internals. • See the Streaming tutorial for concrete CLI and Rust examples.
FAQ
This page collects short answers to questions that come up often when using
genicam-rs or bringing up a new camera.
If you are stuck, also check:
and the issues in the GitHub repository.
“Discovery finds no cameras. What do I check first?”
Run:
cargo run -p gencamctl -- list
If it shows nothing:
- Physical link
- Are the link LEDs lit on camera, NIC, and switch?
- Try a different cable or port.
- IP addresses
- Host NIC and camera must be on the same subnet (e.g. 192.168.0.x/24).
- Avoid having two NICs on the same subnet; routing will get confused.
- Firewall
- Allow UDP broadcast/unicast on the NIC used for cameras.
- On Windows, make sure the binary is allowed on the relevant network profile (Private / Domain).
- Multiple NICs
- Use --iface
to force the interface:
- Use --iface
cargo run -p gencamctl -- list --iface 192.168.0.5
See also: Discovery tutorial and Networking.
⸻
“The vendor viewer works but genicam-rs doesn’t. Why?”
Common causes:
- Different NIC / interface:
- The vendor tool may be using a different NIC or IP selection strategy.
- Compare which local IP it uses and pass that as --iface to gencamctl.
- Different stream destination:
- The camera might be configured to stream to a specific IP/port.
- Ensure genicam-rs uses the same host IP and port, or reset the camera configuration to defaults.
- Different MTU / packet size / packet delay:
- Vendor tools sometimes auto-tune these.
- Try matching their settings using GenApi features (packet size, frame rate, inter-packet delay).
When in doubt:
- Capture logs with RUST_LOG=debug and compare behaviour at the same frame rate and resolution.
See: Streamingand Networking.
⸻
“Does this work on Windows?”
Yes. Windows is a first-class target alongside Linux and macOS.
Notes:
- Make sure the firewall allows discovery and streaming:
- When Windows asks whether to allow the executable on Private/Public networks, allow it on the profile you use for the camera network.
- Configure the NIC for the camera network with a static IPv4 address, separate from your office/internet NIC.
- For high-throughput setups:
- Consider enabling jumbo frames on the camera NIC.
- Disable power-saving features that can introduce latency.
See: Networking for NIC configuration details.
⸻
“How do I set exposure, gain, pixel format, etc.?”
Use the GenApi features via gencamctl or the genicam crate.
Examples with gencamctl:
# Read ExposureTime
cargo run -p gencamctl -- \
get --ip 192.168.0.10 --name ExposureTime
# Set ExposureTime to 5000 (units depend on camera, often microseconds)
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name ExposureTime --value 5000
# Set PixelFormat by name
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name PixelFormat --value Mono8
For more, see: Registers & features.
⸻
“What are selectors and why do my changes seem to disappear?”
Many cameras use selectors to multiplex multiple logical settings onto one feature. Example:
- GainSelector = All, Red, Green, Blue, …
- Gain = value for the currently selected channel.
If you set Gain without first setting GainSelector, you might be modifying a different “row” than you expect.
Typical sequence:
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name GainSelector --value Red
cargo run -p gencamctl -- \
set --ip 192.168.0.10 --name Gain --value 5.0
See: Registers & features and the selectors_demo
example in the genicam crate.
⸻
“Do I need to care about the GenApi XML?”
For most applications, no:
- You can use features by name and let genapi-core handle the mapping.
You should look at the XML when:
- A feature behaves differently from the SFNC / vendor documentation.
- You are debugging selector or SwissKnife behaviour.
- You are contributing to genapi-core or genapi-xml.
See: GenApi XML tutorialand the crate chapters
for genapi-xml and genapi-core when they are filled in.
⸻
“How do I save frames and look at them?”
With gencamctl:
- Use stream with an option like --count / --output (exact flags depend on the CLI):
cargo run -p gencamctl -- \
stream --ip 192.168.0.10 --iface 192.168.0.5 \
--count 100 --output ./frames
This typically saves a sequence of frames in a simple format (e.g. raw, PGM/PPM) that you can inspect with:
- Image viewers.
- Python + NumPy + OpenCV.
- Your own Rust tools.
See: Streaming.
⸻
“How do I generate documentation?”
- mdBook (this book):
- From the repository root:
cargo install mdbook # if not already installed
mdbook build book
- The rendered HTML will be under book/book/.
- Rust API docs:
- From the repository root:
#![allow(unused)] fn main() { cargo doc --workspace --all-features }
- The rendered HTML will be under target/doc/.
Many users publish these via GitHub Pages or another static host; see the repository CI configuration for details.
⸻
“Where should I report bugs or ask questions?”
- For bugs or feature requests, open an issue in the GitHub repository with:
- A clear description of the problem.
- Your OS, Rust version, and camera model.
- A minimal reproduction if possible (CLI commands or small Rust snippet).
- Relevant logs (e.g. RUST_LOG=debug output).
- For questions that may be general (not specific to this project), link to:
- The camera’s data sheet or GenICam XML snippet if relevant.
- Any vendor tools you used to compare behaviour.
Good issues make it much easier to improve the crates for everyone.
API reference
The API reference is generated with cargo doc and published together with
this book.
For crates in this workspace: