Skip to content

GrantSparks/grafton-visca

grafton‑visca

Crates.io Documentation License CI

Pure Rust VISCA control for PTZ cameras with a unified blocking/async API and multi‑runtime adapters (Tokio, async‑std, smol). For broadcasters/streamers, AV integrators, and Rust developers who need production‑grade, type‑safe VISCA control over IP or serial.


Table of contents


Why this crate

  • Unified API (blocking and async): A single Camera<M, P, Tr, Exec> type with mode‑generic accessors; futures are Send in async mode. The design is documented in camera/mod.rs.
  • Multi‑runtime async: Pluggable executors for Tokio, async‑std, and smol; you enable one or more runtime adapters via features and pass an executor to open_*_async. The runtime adapters live under src/executor, e.g. TokioExecutor.
  • Type‑safe camera profiles: Profiles compile in protocol envelope (Raw VISCA vs Sony encapsulation) and defaults (ports/behaviors). Accessors/controls are provided based on capabilities.
  • Transports: TCP/UDP (blocking and async) plus serial (blocking and async‑Tokio). Serial adds VISCA I/F Clear and Address Set orchestration in the transport.
  • Ergonomics: One‑line Connect::open_* helpers, a builder for advanced configuration, and a blocking wrapper with direct Result returns.

Installation

Default = blocking only. Async is opt‑in by features.

Blocking only (default):

[dependencies]
grafton-visca = "0.7"

Async core + runtime (Tokio shown):

[dependencies]
grafton-visca = { version = "0.7", features = ["mode-async", "runtime-tokio"] }
tokio = { version = "1", features = ["full"] }

Async with async‑std or smol:

# async-std
grafton-visca = { version = "0.7", features = ["mode-async", "runtime-async-std"] }
async-std = { version = "1", features = ["attributes"] }

# smol
grafton-visca = { version = "0.7", features = ["mode-async", "runtime-smol"] }
smol = "1"

Serial (blocking):

grafton-visca = { version = "0.7", features = ["transport-serial"] }

Serial (async, Tokio):

grafton-visca = { version = "0.7", features = ["mode-async", "runtime-tokio", "transport-serial-tokio"] }
tokio = { version = "1", features = ["full"] }

Feature names and defaults are defined in Cargo.toml (default = [], mode-async, runtime-*, transport-serial, transport-serial-tokio, etc.).

Feature flags

Feature Default? Unlocks Affects API? Notes
mode-async No Async mode, async camera/session Yes Required by all runtime-* and async serial.
runtime-tokio No Tokio transport adapters/executor No Implies mode-async.
runtime-async-std No async‑std adapters/executor No Implies mode-async.
runtime-smol No smol adapters/executor No Implies mode-async.
transport-serial No Blocking serial transport No RS‑232/422; I/F Clear & Address Set options.
transport-serial-tokio No Async (Tokio) serial transport No Requires mode-async + runtime-tokio.
test-utils No Internal test helpers No Dev/test only.

Canonical serial flags: use transport-serial (blocking) or transport-serial-tokio (async). These exact names appear in Cargo.toml.


Quick Start

Blocking

The blocking API exposes a convenience wrapper that returns Result directly (no trait imports). Use the blessed path helpers:

use grafton_visca::camera::Connect;
use grafton_visca::profiles::PtzOpticsG2; // or GenericVisca

fn main() -> Result<(), grafton_visca::Error> {
    // port omitted -> profile default is used
    let mut cam = Connect::open_udp_blocking::<PtzOpticsG2>("192.168.0.110")?;
    // high-level blocking helpers:
    cam.power_on()?;
    cam.zoom_tele_std()?;
    let pos = cam.pan_tilt_position()?;
    println!("PT position: {:?}", pos);
    cam.close()?;
    Ok(())
}
  • Connect::open_udp_blocking::<P>(addr) is provided in camera::convenience. A similar helper exists for serial: open_serial_blocking::<P>(port, baud).
  • The blocking wrapper’s helpers (power_on, zoom_tele_std, pan_tilt_position, etc.) are implemented under camera::blocking_api.

Prefer TCP for reliability; UDP is supported for environments that require it.

Async (Tokio); notes for async‑std/smol

Enable mode-async and your runtime feature. Pass an executor to the async open helpers:

use grafton_visca::camera::{Connect, CameraBuilder};
use grafton_visca::profiles::GenericVisca;
use grafton_visca::executor::TokioExecutor;

#[tokio::main]
async fn main() -> Result<(), grafton_visca::Error> {
    // One-liner helper (TCP/UDP are available)
    let exec = TokioExecutor::from_current()?; // runtime adapter
    let cam = CameraBuilder::with_executor(exec)
        .open_async::<GenericVisca, _>(grafton_visca::transport::tokio::Tcp::connect("192.168.0.110").await?)
        .await?; // camera session

    // Accessors are mode-unified
    cam.power().power_on().await?;
    let pos = cam.pan_tilt().pan_tilt_position().await?;
    println!("PT position: {:?}", pos);

    cam.close().await?;
    Ok(())
}
  • The async builder entrypoint is CameraBuilder::with_executor(...) .open_async::<Profile, _>(transport).await.
  • Async control methods are available via accessors (e.g., cam.power().power_on().await?) and match the unified control traits.
  • The session exposes close().await in async mode.

For async‑std or smol, enable runtime-async-std or runtime-smol and use the corresponding executor adapter (the pattern is the same). The crate supports enabling multiple runtime adapters at once; you choose at call‑site which one to pass. This is documented in the transport layer and examples.

Multi‑runtime coexistence

As of the current version, you can enable more than one runtime-* feature at the same time; the user picks an executor at runtime (via the adapter type’s from_current() or constructor) when calling open_*_async.


Usage Patterns

1) Simple / blessed path

  • Blocking: Connect::open_udp_blocking::<Profile>(addr) / Connect::open_serial_blocking::<Profile>(port, baud) return the ergonomic blocking wrapper.
  • Async: CameraBuilder::with_executor(exec).open_async::<Profile, _>(transport).await (see Quick Start).

Both modes expose close()/close().await on the camera/session. The docs and examples consistently use close, not shutdown.

2) Advanced (BYO transport / builder)

Build a transport with fine‑grained options, then open the camera:

use grafton_visca::camera::{CameraBuilder, TransportOptions};
use grafton_visca::profiles::PtzOpticsG2;
use std::time::Duration;

// Blocking transport builder (TCP), with knobs:
let transport = grafton_visca::transport::blocking::Tcp::connect("192.168.0.110:5678")?;
let mut cam = CameraBuilder::new()
    .timeouts(Default::default())
    .transport(TransportOptions::TcpBlocking(transport))
    .open_blocking::<PtzOpticsG2>()?;
cam.close()?;

Transport configuration provides connect/read/write timeouts, retry policy, buffer sizing, addressing mode (IP vs serial), and tcp_nodelay. See transport/builder.rs.


Camera Profiles & Capabilities

Profiles define protocol envelope and default ports; most raw VISCA profiles default to TCP 5678 / UDP 1259, while Sony encapsulation profiles default to 52381. The Sony default port appears in the Sony transport/envelope logic and simulator fixtures.

Profile (module) Envelope Default TCP Default UDP Notes / capabilities
GenericVisca Raw VISCA 5678 1259 Conservative baseline.
PtzOpticsG2/G3/30X Raw VISCA 5678 1259 PTZ, presets, exposure, WB, focus.
SonyBRC300 Raw VISCA 5678 1259 Legacy Sony (raw).
NearusBRC300 Raw VISCA 5678 1259 Nearus variant.
SonyEVIH100 Raw VISCA 5678 1259 Sony EVI‑series (raw).
SonyBRCH900 Sony encapsulation 52381 52381 Sony network encapsulation.
SonyFR7 Sony encapsulation 52381 52381 ND filter / direct menu controls.

Profiles and control modules are enumerated under src/camera/controls and src/camera/profiles.

ND filter availability: ND filter controls are compiled only for profiles that support them (e.g., Sony FR7). The ND control module is separate, and inquiry/command types are gated via capability traits.


API Design: Accessor Pattern

Use accessors from the camera/session; they are mode‑generic and don’t require trait imports:

// Blocking
let powered = cam.power_state()?;                  // inquiry helper
cam.power_on()?;                                   // command helper
let zoom = cam.zoom_position()?;                   // inquiry helper
let _ = cam.pan_tilt_home()?;                      // command helper

// Async (equivalent accessors)
let _ = cam.power().power_on().await?;
let pos = cam.pan_tilt().pan_tilt_position().await?;
let v   = cam.system().version().await?;
  • Inquiry/command traits (e.g., PowerControl, PanTiltControl, InquiryControl) are implemented once over the unified camera type via delegation macros, so the accessors compile in both modes.
  • Inquiry accessors include power_state, zoom_position, pan_tilt_position, etc. (see controls/inquiry.rs).

How many control traits? The public API currently exposes 22 control traits (including capability/marker traits like HasMenuControl and HasDirectMenuControl).


Timeouts & Error Handling

Timeout categories

Timeouts are grouped by command category (acknowledgment, quick commands, pan/tilt movement, preset ops, etc.). They’re configured via TimeoutConfig and applied in the runtime/transport. See the timeout builder and command metadata (e.g., timeouts on inquiries).

use std::time::Duration;
use grafton_visca::timeout::TimeoutConfig;

let timeouts = TimeoutConfig::builder()
    .ack_timeout(Duration::from_millis(300))
    .quick_commands(Duration::from_secs(3))
    .movement_commands(Duration::from_secs(20))
    .preset_operations(Duration::from_secs(60))
    .build();

(Names reflect the categories used in command metadata; see typed inquiry definitions referencing CommandCategory::Quick.)

Retries & errors

Errors include helpers for retry decisions and suggested delays (used by tests and transport fixtures). In scripted transports you’ll see injected timeouts and connection loss mapped to error variants.

loop {
    match cam.zoom_absolute(grafton_visca::types::ZoomPosition::MIN) {
        Ok(_) => break,
        Err(e) if e.is_retryable() => {
            if let Some(delay) = e.suggested_retry_delay() {
                std::thread::sleep(delay);
            }
        }
        Err(e) => return Err(e),
    }
}

(Use async sleep with the runtime’s executor in async mode.)


Transports & Protocols

  • TCP/UDP (blocking & async): Provided under transport modules for each runtime. The camera talks VISCA framed either as raw VISCA or as Sony encapsulation, chosen by the profile.
  • Serial (RS‑232/422): Blocking serial (transport-serial) and async serial for Tokio (transport-serial-tokio). Serial does I/F Clear and Address Set during connection if requested (configurable).

Sony encapsulation uses a sequence‑numbered header (port 52381), verified with concurrency tests ensuring unique sequence allocation.


Concurrency & Thread Safety

The camera is designed for concurrent async control (e.g., join multiple inquiries); the unified API produces Send‑safe futures (documented in the camera module). The blocking client performs synchronous I/O; avoid overlapping blocking calls on one instance from multiple threads.


Architecture

flowchart TB
  A[Camera<M,P,Tr,Exec><br/>Session & Accessors] --> B[Executor adapters<br/>Tokio / async-std / smol]
  B --> C[Protocol envelope<br/>Raw VISCA / Sony encapsulation]
  C --> D[Transports<br/>TCP / UDP / Serial]
  A --- E[Profiles<br/>ports & capabilities]

  style A fill:#eef,stroke:#88f
  style B fill:#efe,stroke:#8c8
  style C fill:#ffe,stroke:#cc9
  style D fill:#fee,stroke:#f99
  style E fill:#eef,stroke:#88f
Loading

Source layout (camera/mod.rs, transport/*, camera/controls/*, camera/profiles/*).


Examples

Examples compile under the listed features; see Cargo.toml [[example]] entries.

Example Path How to run
Blocking quickstart examples/quickstart.rs cargo run --example quickstart
Inquiry quickstart (Tokio) examples/inquiry_quickstart.rs cargo run --example inquiry_quickstart --features "mode-async,runtime-tokio"
Runtime demo (low‑level) examples/runtime_demo_lowlevel.rs cargo run --example runtime_demo_lowlevel --features "mode-async,runtime-tokio"
Runtime‑agnostic patterns examples/runtime_agnostic.rs cargo run --example runtime_agnostic --features "mode-async"
Builder API (blocking) examples-advanced/builder_api.rs cargo run --example builder_api
Transports overview examples-advanced/transports.rs cargo run --example transports
Error handling (Tokio) examples-advanced/error_handling.rs cargo run --example error_handling --features "mode-async,runtime-tokio"
Sony encapsulation examples-advanced/sony_encapsulation.rs cargo run --example sony_encapsulation --features "mode-async,runtime-tokio"
Serial (async, Tokio) examples/serial_async_demo.rs cargo run --example serial_async_demo --features "mode-async,runtime-tokio,transport-serial-tokio"

Testing

Common invocations:

# Blocking only (default)
cargo test

# Async with Tokio
cargo test --features "mode-async,runtime-tokio"

# Async with async-std
cargo test --features "mode-async,runtime-async-std"

# Async with smol
cargo test --features "mode-async,runtime-smol"

# Serial (blocking)
cargo test --features "transport-serial"

# Serial (async, Tokio)
cargo test --features "mode-async,runtime-tokio,transport-serial-tokio"

Current test surface: The repo contains 437 #[test] functions plus 186 macro‑generated tests (e.g., protocol/encoding fixtures), for ~623 unit tests (scan of the source tree). The scripted transport also injects timeouts/errors used in tests.


Performance Notes

  • Send‑safe, zero‑cost futures for async accessors over a single unified camera type.
  • Configurable buffering/retries via transport config: connect/read/write timeouts, retry policy, and tcp_nodelay.
  • Efficient framing/envelope: Sony encapsulation sequence numbers are verified for uniqueness under concurrency.

Troubleshooting / FAQ

  • “It compiles but nothing happens on UDP!” UDP is connectionless; lack of replies will surface as timeouts. Prefer TCP unless UDP is required.
  • “Which port do I use?” If you omit the port, helpers use the profile’s default (raw VISCA: TCP 5678 / UDP 1259; Sony encapsulation: 52381).
  • “Async example says no executor/runtime.” Ensure you enabled mode-async and a runtime-* feature and pass the executor adapter to open_*_async.
  • “Serial doesn’t connect.” Confirm port, permissions, and baud; async serial requires mode-async + runtime-tokio + transport-serial-tokio. The example prints targeted guidance.
  • “Camera busy / buffer full.” Use a retry loop; errors include retryability and suggested delay (see scripted transport error injection for mapping).

Safety: PTZ motion physically moves hardware. Ensure clearances and a safe operating area before issuing movement commands.


Compatibility

  • MSRV: Rust 1.80.0 (rust-version = "1.80" in Cargo.toml).
  • OS: Builds on Linux/macOS/Windows; CI covers these OSes. (See GitHub Actions workflow.) CI badge → workflow

Contributing

Contributions are welcome! Please see CONTRIBUTING.md (standard formatting/lints apply). The crate uses feature gating to keep default builds lean; please keep examples runnable across modes.


License

Licensed under either of:

  • Apache License, Version 2.0LICENSE-APACHE
  • MIT LicenseLICENSE-MIT

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work shall be dual‑licensed as above, without additional terms or conditions.


Resources

About

Rust based VISCA over IP implementation for controlling PTZ Cameras

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •