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.
- Unified API (blocking and async): A single
Camera<M, P, Tr, Exec>
type with mode‑generic accessors; futures areSend
in async mode. The design is documented incamera/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 undersrc/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 directResult
returns.
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 | 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) ortransport-serial-tokio
(async). These exact names appear inCargo.toml
.
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 incamera::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 undercamera::blocking_api
.
Prefer TCP for reliability; UDP is supported for environments that require it.
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.
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
.
- 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 useclose
, notshutdown
.
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
.
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.
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. (seecontrols/inquiry.rs
).
How many control traits? The public API currently exposes 22 control traits (including capability/marker traits like HasMenuControl
and HasDirectMenuControl
).
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
.)
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.)
- 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.
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.
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
Source layout (camera/mod.rs
, transport/*
, camera/controls/*
, camera/profiles/*
).
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" |
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.
- 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.
- “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 aruntime-*
feature and pass the executor adapter toopen_*_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.
- MSRV: Rust 1.80.0 (
rust-version = "1.80"
inCargo.toml
). - OS: Builds on Linux/macOS/Windows; CI covers these OSes. (See GitHub Actions workflow.) CI badge → workflow
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.
Licensed under either of:
- Apache License, Version 2.0 —
LICENSE-APACHE
- MIT License —
LICENSE-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.
- API docs: https://docs.rs/grafton-visca
- Examples: see
examples/
andexamples-advanced/
in this repo (table above). - Issues: https://github.com/GrantSparks/grafton-visca/issues