Expand description
§grafton-visca
Rust library for VISCA over IP protocol to control PTZ cameras.
§What is VISCA?
VISCA (Video System Control Architecture) is a protocol developed by Sony for controlling Ptz cameras commonly used in robotics, broadcasting, video conferencing, and surveillance applications. This crate implements VISCA over IP, allowing you to control networked Ptz cameras from Rust applications.
§Features
- Unified API Architecture: Single consistent interface across blocking and async modes
- Type-Safe Camera Profiles: Compile-time validation with camera-specific profiles
- Feature-Gated Methods: Choose blocking or async at compile time with zero runtime overhead
- Multi-Runtime Support: Tokio, async-std, smol can coexist with priority-based selection
- Complete Command Coverage: Full VISCA protocol support across all camera types
- Profile-Aware Conversions: Automatic unit conversions based on camera model
- Comprehensive Inquiry: Query camera state for all supported features
- Transport Abstraction: TCP, UDP, Serial, and custom transport implementations
- Builder Patterns: Flexible camera and transport configuration
- Unified Error Handling: Consistent error mapping across all transport types
- Configurable Timeouts: Per-category timeout configuration for different command types
- Command Cancellation: Cancel specific commands or entire socket operations
- Async Completion Tracking: Wait for camera movements to complete with await methods
- Serialization Support: Optional serde/schemars integration for all value types
§Serialization Support
All public value types support optional serialization through feature-gated serde and schemars derives:
[dependencies]
grafton-visca = { version = "*", features = ["serde", "schemars"] }With these features enabled, you can serialize/deserialize all value types directly:
use grafton_visca::types::{PanSpeed, ZoomPosition, SpeedLevel};
// Serialize to JSON
let speed = PanSpeed::new(12).unwrap();
let json = serde_json::to_string(&speed).unwrap();
assert_eq!(json, "12");
// Deserialize from JSON
let speed: PanSpeed = serde_json::from_str("15").unwrap();
assert_eq!(speed.value(), 15);
// Works with enums too
let level = SpeedLevel::Medium;
let json = serde_json::to_string(&level).unwrap();
assert_eq!(json, "\"medium\"");§Configuration Types with Serialization
Camera configuration types also support serialization, making it easy to save and load camera setups from configuration files or APIs:
use grafton_visca::camera::config::TransportOptions;
use grafton_visca::camera::profiles::ProfileId;
// Serialize camera profile
let profile = ProfileId::PtzOpticsG2;
let json = serde_json::to_string(&profile).unwrap();
assert_eq!(json, "\"ptz-optics-g2\"");
// Serialize transport configuration
let transport = TransportOptions::Tcp {
address: "192.168.0.110:5678".to_string(),
};
let json = serde_json::to_string(&transport).unwrap();
// Can be loaded from config files, environment variables, etc.With schemars feature, you can also generate JSON schemas for API documentation:
use grafton_visca::types::PanSpeed;
use schemars::schema_for;
let schema = schema_for!(PanSpeed);
// Use schema for API documentation, validation, etc.§Model-Aware Parameter Validation
The library provides comprehensive parameter validation at multiple levels, ensuring commands are correct before being sent to the camera:
§Type-Safe Parameters with Conservative Defaults
All parameter types provide conservative VISCA-compliant ranges by default:
use grafton_visca::types::{PanSpeed, ZoomPosition, ZoomSpeed};
// All range types expose MIN/MAX constants for validation
assert_eq!(PanSpeed::MIN.value(), 0);
assert_eq!(PanSpeed::MAX.value(), 24);
// Validated constructors provide clear error messages
let speed = PanSpeed::new(15)?; // Valid: 0-24
match PanSpeed::new(30) {
Err(e) => println!("{}", e), // "PanSpeed must be between 0 and 24"
_ => {}
}
// Speed types work seamlessly with SpeedLevel enum
let zoom = ZoomSpeed::from(SpeedLevel::Fast); // Automatic conversion
assert_eq!(zoom.value(), 6); // Fast = 6 for zoom§Model-Specific Validation
For precise control, use model-aware constructors that validate against specific camera capabilities:
use grafton_visca::types::{PanPosition, TiltSpeed};
use grafton_visca::constants::CameraVariant;
// Model-specific validation for PTZOptics G2
let model = CameraVariant::PtzOpticsG2;
let pan = PanPosition::new_for_model(2000, model)?; // Validates against G2 pan range
let speed = TiltSpeed::new_for_model(18, model)?; // Validates against G2 tilt speed§Profile-Based Compile-Time Safety
When using camera profiles, validation happens at compile time through capability traits:
use grafton_visca::{Camera, camera::profiles::PtzOpticsG2};
// Profile provides model-specific constants at compile time
let camera = Camera::<PtzOpticsG2, _>::new(transport);
// Methods automatically use profile's validated ranges
camera.pan_tilt_absolute(1000, 500, 10, 10)?; // Validated against G2 limits§Compile-Time Capability Gating
Vendor-specific commands are gated by marker traits, ensuring compile-time safety:
use grafton_visca::{FocusLockControl, camera::profiles::PtzOpticsG2};
// FocusLockControl is only available for profiles with HasFocusLock
// PtzOpticsG2 implements HasFocusLock, so these methods are available
camera.enable_focus_lock()?; // Compiles with PtzOpticsG2
camera.disable_focus_lock()?;
// With a different profile that doesn't have HasFocusLock, this wouldn't compileThis multi-layered approach ensures:
- Early error detection at construction time
- Model-specific precision when needed
- Conservative defaults for generic usage
- Zero-cost abstractions through compile-time validation
§Quick Start
§Blocking Example
use grafton_visca::{
camera::Connect,
camera::profiles::PtzOpticsG2,
Error,
};
fn main() -> Result<(), Error> {
// Create camera using convenience Connect helper
let camera = Connect::open_tcp_blocking::<PtzOpticsG2>("192.168.0.110")?;
// Use accessor-style API
camera.power().on()?;
camera.zoom().tele()?;
camera.pan_tilt().home()?;
Ok(())
}§Async Example with Multi-Runtime Support
use grafton_visca::{
camera::Connect,
camera::profiles::PtzOpticsG2,
runtime::{Runtime, TokioRuntime},
Error,
};
#[tokio::main]
async fn main() -> Result<(), Error> {
// Create camera using Connect helper with runtime
let runtime = TokioRuntime::from_current()?;
let camera = Connect::open_tcp_async::<PtzOpticsG2, _>(
"192.168.0.110",
runtime
).await?;
// Use accessor-style API with async
camera.power().on().await?;
camera.zoom().tele().await?;
camera.pan_tilt().home().await?;
// Wait for movements to complete
camera.await_idle().await?;
Ok(())
}§Runtime Coexistence Example
// Multiple runtimes can coexist! Priority: tokio → async-std → smol
[dependencies]
grafton-visca = { version = "*", features = ["runtime-tokio", "runtime-async-std"] }
use grafton_visca::{
CameraBuilder, Error,
camera::profiles::PtzOpticsG2,
transport::Transport,
PowerControl, ZoomControl,
};
#[async_std::main]
async fn main() -> Result<(), Error> {
// Same Transport API - runtime auto-selected based on priority
let transport = Transport::tcp()
.address("192.168.0.110:5678")
.connect() // Uses tokio if available, async-std otherwise
.await?;
use grafton_visca::runtime::{AsyncStdRuntime, Runtime};
let runtime = AsyncStdRuntime::new();
let camera = CameraBuilder::with_executor(runtime)
.open_async::<PtzOpticsG2, _>(transport)
.await?;
// Use accessor-style API across all runtimes
camera.power().on().await?;
camera.zoom().tele().await?;
Ok(())
}§Unified Trait Usage Example
use grafton_visca::{
CameraBuilder, Error,
camera::profiles::PtzOpticsG2,
transport::Transport,
PowerControl, ZoomControl, PanTiltControl,
};
fn main() -> Result<(), Error> {
smol::block_on(async {
// Same unified Transport API works across all runtimes
let transport = Transport::tcp()
.address("192.168.0.110:5678")
.connect() // Runtime auto-selected based on enabled features
.await?;
use grafton_visca::runtime::{Runtime, SmolRuntime};
let runtime = SmolRuntime::new();
let camera = CameraBuilder::with_executor(runtime)
.open_async::<PtzOpticsG2, _>(transport)
.await?;
// Use accessor-style API consistently across runtimes
camera.power().on().await?;
camera.pan_tilt().home().await?;
camera.zoom().tele().await?;
Ok(())
})
}§Camera Profiles
The library includes pre-defined profiles with type aliases:
PtzOpticsG2Cam<T>- PtzOptics G2 series camerasSonyFR7Cam<T>- Sony FR7 cameras with ND filter supportGenericViscaCam<T>- Generic VISCA-compatible cameras (conservative feature set)
§Compile-Time Type Safety
The generic API ensures type safety at compile time:
use grafton_visca::prelude::blocking::*;
// This function only accepts cameras with ND filter support
fn adjust_nd_filter<P, T>(camera: &Camera<P, T>) -> Result<(), Error>
where
P: Profile + NdFilter,
T: Transport + Send + Sync,
{
camera.set_nd_filter_mode(NdFilterMode::Clear)
}
// This would compile for SonyFR7 but not for PtzOpticsG2
let sony = SonyFR7Cam::new(transport);
adjust_nd_filter(&sony)?; // OK - Sony FR7 has ND filter
let g2 = PtzOpticsG2Cam::new(transport);
// adjust_nd_filter(&g2)?; // Compile error - G2 doesn't have ND filter§Transport Implementation
The library provides transport traits that you can implement for any communication method:
use grafton_visca::{transport::BlockingTransport, command::CommandKind, Error};
use bytes::Bytes;
use std::time::Duration;
struct MyTransport {
// Your transport state
}
impl BlockingTransport for MyTransport {
fn send_with_kind(&mut self, data: &[u8], kind: CommandKind) -> Result<(), Error> {
// Send data over your transport with proper framing based on kind
Ok(())
}
fn recv(&mut self) -> Result<Bytes, Error> {
// Receive response from your transport
Ok(Bytes::new())
}
fn recv_with_timeout(&mut self, timeout: Duration) -> Result<Bytes, Error> {
// Receive response with timeout
Ok(Bytes::new())
}
}Example transport implementations are demonstrated in:
examples/quickstart.rs- TCP/IP transport with blocking APIexamples/quickstart_async.rs- TCP/IP transport with async APIexamples-advanced/transports.rs- Custom and advanced transport examples
§Async Support
The library provides runtime-agnostic async support, allowing you to use ANY async runtime (tokio, async-std, smol, etc.) or even create your own.
§Feature Flags
mode-async- Enables async support without any specific runtime. You must provide your own runtime.mode-blocking- Explicit feature flag for blocking mode (blocking is always available, this is for feature detection).runtime-tokio- Enables async with built-in Tokio runtime support (impliesmode-async).runtime-async-std- Enables async with built-in async-std runtime support (impliesmode-async).runtime-smol- Enables async with built-in smol runtime support (impliesmode-async).transport-serial- Enables serial port support for blocking mode.transport-serial-tokio- Enables serial port support with Tokio (impliesruntime-tokio).test-utils- Testing utilities including ScriptedTransport and DeterministicExecutor (not for production).
Multiple Runtime Support: As of version 0.7.0, runtime features can be enabled simultaneously.
This allows libraries to support multiple runtime ecosystems without forcing users to choose.
Use explicit executor selection (CameraBuilder::with_executor(TokioRuntime::from_current()), etc.) when multiple runtimes are available.
§Send Future Guarantees
All public async traits in this crate guarantee that their returned futures are Send.
This is enforced through explicit + Send bounds in trait signatures using
return-position impl trait in traits (RPITIT).
This guarantee ensures spawn-safety across all async runtimes and prevents
subtle !Send future errors in multi-threaded executors.
§⚠️ Important: Runtime Requirements for Async
The async API REQUIRES a runtime to be configured. Without a runtime, ALL async operations
will fail with: Error::InvalidState("No runtime configured for async operations").
The runtime is essential for:
- Timeout handling - All camera commands have configurable timeouts
- Power sequences - Power on/off operations require delays
- Movement detection - Polling for pan/tilt/zoom completion
- Background tasks - Socket manager for concurrent operations
§Runtime Requirements for Async
You have multiple options for configuring a runtime:
§Option 1: Use built-in runtime support (Easiest)
Choose your runtime(s) and enable the corresponding feature(s) in Cargo.toml:
[dependencies]
# Single runtime:
grafton-visca = { version = "*", features = ["runtime-tokio"] }
grafton-visca = { version = "*", features = ["runtime-async-std"] }
grafton-visca = { version = "*", features = ["runtime-smol"] }
# Multiple runtimes (choose executor at construction time):
grafton-visca = { version = "*", features = ["runtime-tokio", "runtime-async-std"] }
grafton-visca = { version = "*", features = ["runtime-tokio", "runtime-smol", "runtime-async-std"] }Then use the corresponding CameraBuilder method:
// Tokio
use grafton_visca::runtime::{Runtime, TokioRuntime};
let runtime = TokioRuntime::from_current()?;
let camera = CameraBuilder::with_executor(runtime)
.open_async::<PtzOpticsG2, _>(transport)
.await?;
// async-std
use grafton_visca::runtime::{AsyncStdRuntime, Runtime};
let runtime = AsyncStdRuntime::new();
let camera = CameraBuilder::with_executor(runtime)
.open_async::<PtzOpticsG2, _>(transport)
.await?;
// smol
use grafton_visca::runtime::{Runtime, SmolRuntime};
let runtime = SmolRuntime::new();
let camera = CameraBuilder::with_executor(runtime)
.open_async::<PtzOpticsG2, _>(transport)
.await?;§Option 2: Provide your own runtime (Advanced)
For complete runtime independence, use the unified Executor trait:
use grafton_visca::{
Camera, CameraBuilder, Executor,
prelude::r#async::*,
};
use std::{pin::Pin, time::Duration, future::Future};
// Example: Custom executor implementation for async-std
#[derive(Debug, Clone)]
struct AsyncStdExecutor;
impl Executor for AsyncStdExecutor {
type Join<T> = Pin<Box<dyn Future<Output = Result<T, ExecError>> + Send + 'static>>
where T: Send + 'static;
fn spawn<F>(&self, fut: F) -> Self::Join<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
// Implementation using async-std
// ...
}
fn block_on<F: Future>(&self, fut: F) -> F::Output {
async_std::task::block_on(fut)
}
fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async_std::task::sleep(duration))
}
fn timeout<'a, F, T>(
&'a self,
duration: Duration,
fut: F,
) -> Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'a>>
where
F: Future<Output = T> + Send + 'a,
T: Send + 'a,
{
// Implementation using async-std timeout
// ...
}
}
#[async_std::main]
async fn main() -> Result<(), Error> {
// Create camera with custom executor
let executor = AsyncStdExecutor;
let camera = CameraBuilder::with_executor(executor)
.open_async::<PtzOpticsG2, _>(transport)?;
// All async operations now use async-std
camera.power_on().await?;
camera.zoom_in().await?;
Ok(())
}§Common Runtime Errors and Solutions
§Error: InvalidState("No runtime configured for async operations")
Cause: You’re using async mode but haven’t configured a runtime. Solution: Either:
- Enable
runtime-tokiofeature and useCameraBuilder::with_executor(TokioRuntime::from_current()) - Use
CameraBuilder::with_executor()with your own runtime implementation
§Error: InvalidState("Operation requires runtime for timeout handling")
Cause: The operation needs timeout support but no runtime is available. Solution: Same as above - configure a runtime.
§Error: Socket manager initialization issues
Cause: The socket manager requires a runtime to spawn background tasks.
Solution: Ensure your runtime’s Spawner implementation is working correctly.
§Blocking vs Async Mode
The library provides a clean separation between blocking and async APIs:
-
Blocking mode (default): No async dependencies, uses synchronous I/O
- When no features are enabled, only blocking types are available
- Zero async runtime overhead or dependencies
-
Async mode (
mode-asyncfeature): Native async implementation- When
mode-asyncfeature is enabled, blocking types are NOT exported - Provides true async I/O without blocking thread pools
- REQUIRES runtime configuration (see Async Support section above)
- When
The API surface changes based on your feature selection - you get either blocking OR async types, never both. This ensures a clean, focused API for your use case.
§Supported Commands
§Camera Movement
- Pan/Tilt/Zoom control with absolute and relative positioning
- Variable speed control for smooth movements
- Home position and preset management
§Exposure & Color
- Exposure modes: Auto, Manual, Shutter Priority, Iris Priority, Bright
- White balance modes including manual color temperature
- Color adjustments: saturation, hue, RGB gain tuning
§Image Control
- Focus control with auto/manual modes
- Sharpness, brightness, and contrast adjustment
- Noise reduction (2D and 3D)
- Image flip and other effects
§Position Units
The Camera API supports multiple position unit types with automatic conversion:
// Work in degrees (recommended)
camera.set_position(Degrees(45.0), Degrees(-15.0))?;
// Stop all movement
camera.stop()?;
// Move to home position
camera.home()?;§Timeout Configuration
Configure timeouts per command category based on your network and camera:
use grafton_visca::{CameraBuilder, TimeoutConfig};
use std::time::Duration;
let config = 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))
.open();
// For async mode (default)
use grafton_visca::runtime::{Runtime, TokioRuntime};
let runtime = TokioRuntime::from_current()?;
let camera = CameraBuilder::with_executor(runtime)
.timeout_config(config)
.open_async::<PtzOpticsG2, _>(transport).await?;
// For blocking mode (when async feature is disabled)
#[cfg(not(feature = "mode-async"))]
let camera = CameraBuilder::new()
.timeout_config(config)
.build_blocking::<PtzOpticsG2, _>(transport)?;§Command Cancellation (Async)
Cancel specific commands or entire socket operations:
// Send a command and get its ID for cancellation
let (cmd_id, response_future) = camera.send_command_with_id(command).await?;
// Cancel the specific command
camera.cancel_command(cmd_id).await?;
// Or cancel all commands on a socket
use grafton_visca::ViscaSocket;
camera.cancel_socket(ViscaSocket::S1).await?;§Movement Completion Tracking
Wait for camera movements to complete using AwaitConfig:
use std::time::Duration;
use grafton_visca::camera::{AwaitConfig, Axes};
// Start a pan/tilt movement
camera.pan_tilt_absolute(45.0, 15.0, 10, 10).await?;
// Wait for all movements to complete (pan/tilt, zoom, focus)
camera.await_idle(Duration::from_secs(30)).await?;
// Or wait for specific axes with custom configuration
let config = AwaitConfig::new(Duration::from_secs(10))
.with_axes(Axes::PAN_TILT)
.with_debug();
camera.await_with_config(&config).await?;
// Convenience methods for common scenarios
camera.await_pan_tilt_idle(Duration::from_secs(20)).await?;
camera.await_zoom_idle(Duration::from_secs(15)).await?;§Error Handling
The library provides comprehensive error types for all VISCA error conditions:
match camera.pan_tilt_absolute(180.0, 0.0, 10, 10).await {
Ok(_) => println!("Position set successfully"),
Err(Error::SyntaxError) => println!("Position out of range"),
Err(Error::CommandNotExecutable) => println!("Camera busy or powered off"),
Err(Error::CommandBufferFull) => {
// This error is automatically retried by the runtime
println!("Camera buffer full, command will retry");
}
Err(e) => println!("Other error: {e}"),
}Re-exports§
pub use crate::cache::PanTiltLimits;pub use crate::cache::StateCache;pub use crate::camera::Camera;pub use crate::camera::CameraBuilder;pub use crate::camera_id::CameraId;pub use crate::command::exposure::ExposureMode;pub use crate::command::focus::AutoFocusSensitivity;pub use crate::command::focus::FocusMode;pub use crate::command::nd_filter::NdFilterMode;pub use crate::command::pan_tilt::PanTiltDirection;pub use crate::command::pan_tilt::PanTiltLimitCorner;pub use crate::command::preset::PresetNumber;pub use crate::command::resolution::PictureEffectMode;pub use crate::command::resolution::ResolutionMode;pub use crate::command::system::MotionSyncMode;pub use crate::command::system::MotionSyncPreset;pub use crate::command::white_balance::AutoWhiteBalanceSensitivity;pub use crate::command::white_balance::WhiteBalanceMode;pub use crate::inquiry_conversions::zoom_from_normalized;pub use crate::inquiry_conversions::Normalized;pub use crate::inquiry_conversions::PanTiltPositionDeg;pub use crate::inquiry_conversions::PanTiltPositionRaw;pub use crate::inquiry_conversions::ZoomDomain;pub use crate::inquiry_conversions::ZoomPositionExt;pub use crate::types::Coarse;pub use crate::types::FocusSpeed;pub use crate::types::MotionSyncSpeed;pub use crate::types::SpeedLevel;pub use crate::types::ZoomSpeed;pub use crate::visca_socket::ViscaSocket;pub use crate::camera::blocking_api::BlockingClient;pub use crate::camera::BlockingCamera;
Modules§
- cache
- State cache for write-only VISCA properties.
- camera
- Camera profile system for type-safe, model-specific control Camera module with Send-safe, profile-centric VISCA API.
- camera_
id - Camera ID type for VISCA protocol addressing Camera ID type for VISCA protocol addressing.
- capabilities
- Capability traits for camera feature composition Fine-grained capability traits for VISCA camera features.
- command
- Command definitions for VISCA protocol (advanced use only)
- constants
- Constants for VISCA protocol including default ports Camera-specific constants and conversion utilities for VISCA protocol.
- diagnostics
- Diagnostics and health check utilities Diagnostics and health check utilities for PTZ cameras.
- inquiry_
conversions - Inquiry conversion utilities for raw to user-friendly values Inquiry conversion utilities for converting raw VISCA values to user-friendly formats.
- mode
- Mode trait system for unified async/blocking API.
- prelude
- Prelude modules for convenient imports.
- profiles
- Camera profiles with compositional capabilities
- protocol
- Protocol encoding and decoding utilities (internal use)
- runtime
- VISCA runtime with flume-based scheduling VISCA runtime implementation using flume channels.
- timeout
- Unified timeout configuration and management for VISCA commands.
- transport
- Transport layer for implementing custom transports Transport layer for VISCA communication.
- types
- Type definitions and abstractions Type-safe wrappers for VISCA protocol values.
- units
- Semantic unit types for intuitive API usage Semantic unit types for VISCA protocol values.
- visca_
socket - Unified VISCA socket type Unified VISCA socket type.
Macros§
- impl_
camera_ ops - Generate trait implementations that forward to inherent methods on Camera.
- visca_
command - Create a VISCA command that expects ACK/Completion responses.
- visca_
range_ type - Create a type with range validation.
Enums§
Type Aliases§
- Result
- Custom result type for VISCA operations.
Derive Macros§
- Visca
Enum - Derive macro for automatic enum/u8 conversions in VISCA protocol
- Visca
Inquiry - Derive macro for generating ViscaInquiry implementations with parser support
- Visca
Value - Derive macro for implementing ViscaValue trait for command value types