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.
This crate is currently in pre-release status (< 1.0.0)
Breaking changes may occur until we reach stable API at version 1.0.0
- PTZOptics cameras - Fully tested and supported
- Other VISCA-compliant cameras - May work but untested
- Sony encapsulation profile - No hardware verification yet
- Serial transport - Theoretical implementation, awaiting hardware testing
We can only support what we can test. If you have access to:
- Different VISCA camera models
- Serial-capable hardware
- Sony profile equipment
Please:
- 🐛 Report issues
- 🔧 Submit pull requests
- 📝 Share your test results
This notice will be updated as we verify additional hardware and removed upon reaching v1.0.0
- Unified API (blocking and async): A single
Camera<M, P, Tr, Exec>type with mode‑generic accessors; futures areSendin 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 directResultreturns. - Modern conveniences (0.8.0+): Optional serialization (serde/schemars), built-in inquiry conversions, canonical speed mapping, direct f64 usage, diagnostics module, and 26+ new convenience methods—eliminating hundreds of lines of downstream boilerplate.
- Protocol correctness: VISCA-spec validated protocol fixes, proper inquiry type safety (i8 for tuning offsets, typed wrappers for levels), and baseline VISCA API normalization.
- Performance: Zero-allocation send path (0 heap allocations for standard commands), type-safe operation handles for async cancellation, and efficient protocol framing.
Version 0.8.0 brings powerful ergonomic improvements and protocol correctness fixes while maintaining backward compatibility:
- Direct f64 usage: No more manual casts!
Degrees(45.0)instead ofDegrees(45.0 as f32) - Canonical speed mapping: Built-in
Coarsespeed levels (Slowest → Fastest) withfrom_coarse()methods - Built-in inquiry conversions: Direct methods like
pos.as_degrees()andzoom.normalized_optical() - 26+ new convenience methods: Common operations like
set_image_flip(),set_contrast(), etc. - Profile dispatch helpers:
ProfileId::profile_group()eliminates ~218 lines of boilerplate - Diagnostics module:
ping(),probe(), andmeasure_latency()for health checks
// Enable serde support for all types
grafton-visca = { version = "0.8", features = ["serde", "schemars"] }
// Now all types serialize directly - no wrapper code needed!
let speed = PanSpeed::new(12)?;
let json = serde_json::to_string(&speed)?; // Works!- AWB sensitivity inquiry: Fixed inverted mapping to match VISCA specification
- Tally APIs: Normalized to baseline VISCA (use
tally_red()/tally_green()) - Inquiry type safety: Red/blue tuning now correctly use
i8(wasu8)
- Zero-allocation send path: 0 heap allocations for standard VISCA commands (was 2)
- Type-safe operation handles:
InFlight<C>for async cancellation and completion tracking - Error context: Add context to errors while preserving retry intelligence
- Enhanced types: New
GammaLevel,NoiseReductionLeveltyped wrappers
Most users can upgrade with zero code changes. See CHANGELOG.md for detailed migration guide and examples.
Default = blocking only. Async is opt‑in by features.
Blocking only (default):
[dependencies]
grafton-visca = "0.8"With serialization support (new in 0.8.0):
[dependencies]
grafton-visca = { version = "0.8", features = ["serde", "schemars"] }Async core + runtime (Tokio shown):
[dependencies]
grafton-visca = { version = "0.8", features = ["mode-async", "runtime-tokio"] }
tokio = { version = "1", features = ["full"] }Async with async‑std or smol:
# async-std
grafton-visca = { version = "0.8", features = ["mode-async", "runtime-async-std"] }
async-std = { version = "1", features = ["attributes"] }
# smol
grafton-visca = { version = "0.8", features = ["mode-async", "runtime-smol"] }
smol = "1"Serial (blocking):
grafton-visca = { version = "0.8", features = ["transport-serial"] }Serial (async, Tokio):
grafton-visca = { version = "0.8", 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. |
serde (new in 0.8) |
No | Serde serialization for all types | No | Enables Serialize/Deserialize derives. |
schemars (new in 0.8) |
No | JSON Schema generation | No | Enables JsonSchema derives for API docs. |
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
use grafton_visca::units::Degrees;
use grafton_visca::types::SpeedLevel;
fn main() -> Result<(), grafton_visca::Error> {
// port omitted -> profile default is used
let mut cam = Connect::open_tcp_blocking::<PtzOpticsG2>("192.168.0.110")?;
// High-level blocking helpers with ergonomic types (0.8.0+)
cam.power_on()?;
cam.pan_tilt_absolute(Degrees(45.0), Degrees(15.0), SpeedLevel::Fast)?; // Direct f64!
// Built-in inquiry conversions (0.8.0+)
let pos = cam.pan_tilt_position()?;
let (pan_deg, tilt_deg) = pos.as_degrees(); // No manual conversion needed!
println!("PT position: {:.1}°, {:.1}°", pan_deg.0, tilt_deg.0);
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;
use grafton_visca::profiles::PtzOpticsG2;
use grafton_visca::runtime::TokioRuntime;
use grafton_visca::units::Degrees;
use grafton_visca::types::SpeedLevel;
#[tokio::main]
async fn main() -> Result<(), grafton_visca::Error> {
// One-liner async connection
let runtime = TokioRuntime::from_current()?;
let cam = Connect::open_tcp_async::<PtzOpticsG2, _>("192.168.0.110", runtime).await?;
// Accessors are mode-unified
cam.power().on().await?;
cam.pan_tilt_absolute(Degrees(45.0), Degrees(15.0), SpeedLevel::Fast).await?;
// Built-in inquiry conversions (0.8.0+)
let pos = cam.pan_tilt_position().await?;
let (pan_deg, tilt_deg) = pos.as_degrees();
println!("PT position: {:.1}°, {:.1}°", pan_deg.0, tilt_deg.0);
// NEW in 0.8.0: Type-safe operation handles
let handle = cam.pan_tilt_home_op().await?;
handle.await_completion(std::time::Duration::from_secs(5)).await?;
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().awaitin 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().awaiton 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.
- Zero-allocation send path (0.8.0+): Standard VISCA commands (≤24 bytes) use stack-allocated buffers with
SmallVec, eliminating all heap allocations in the hot path (2 allocations → 0). - Send‑safe, zero‑cost futures for async accessors over a single unified camera type.
- Type-safe operation handles (0.8.0+):
InFlight<C>provides zero-cost abstraction for async cancellation and completion tracking using ZST markers. - 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-asyncand 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).
- "How can I check if the camera is online?" (0.8.0+) Use the diagnostics module:
camera.ping().await?for quick check, orcamera.probe().await?for detailed health report with RTT measurements.
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