13. Common Patterns & Recipes

13.1 Basic & Multi-Camera Control

Pragmatic usage patterns for single and multiple cameras, showing both Pythonic and explicit error-handling approaches.


Simple Pythonic API usage

Automatic device management with context manager:

import duvc_ctl as duvc

# Automatically opens & closes device
with duvc.CameraController(0) as cam:
    cam.brightness = 100
    cam.zoom = 2
    current_zoom = cam.zoom
    print(f"Zoom: {current_zoom}")

No explicit error handling—exceptions propagate. Ideal for scripts where device is guaranteed connected.

Manual context (if device may disconnect):

cam = duvc.CameraController(0)
try:
    cam.set("brightness", 100)
    cam.set("zoom", 2)
finally:
    cam.close()

Result-based error handling

Explicit control via Result types:

import duvc_ctl as duvc

device = duvc.list_devices()[0]
result = duvc.open_camera(device)

if not result.is_ok():
    error = result.error()
    print(f"Failed to open: {error.description()}")
    exit(1)

cam = result.value()

# Read property, handle individually
brightness_result = cam.get_camera_property(duvc.CamProp.Brightness)
if brightness_result.is_ok():
    brightness = brightness_result.value().value
    print(f"Brightness: {brightness}")
else:
    print(f"Failed to read brightness")

cam.close()

No exceptions thrown—check .is_ok() after each operation.


Multi-camera management

Sequential access (process cameras one-by-one):

import duvc_ctl as duvc

devices = duvc.list_devices()
print(f"Found {len(devices)} cameras")

for i, device in enumerate(devices):
    try:
        with duvc.CameraController(i) as cam:
            print(f"Camera {i}: {device.name}")
            cam.brightness = 80 + (i * 10)  # Vary per camera
            print(f"  Brightness set to {cam.brightness}")
    except duvc.DeviceNotFoundError:
        print(f"Camera {i} disconnected")
    except Exception as e:
        print(f"Camera {i} error: {e}")

Each camera opened/closed in sequence.

Parallel management (all cameras at once):

import duvc_ctl as duvc
import threading

devices = duvc.list_devices()
cameras = []

# Open all devices
for i in range(len(devices)):
    try:
        cameras.append(duvc.CameraController(i))
    except duvc.DeviceNotFoundError:
        print(f"Failed to open camera {i}")

# Apply settings in parallel
threads = []
def configure_camera(cam, index):
    cam.brightness = 100
    cam.zoom = 2
    print(f"Camera {index} configured")

for idx, cam in enumerate(cameras):
    thread = threading.Thread(target=configure_camera, args=(cam, idx))
    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()

# Close all
for cam in cameras:
    cam.close()

Thread safety: Each camera operates independently. Device operations may block (e.g., autofocus); use threads to avoid stalls.


Switching between cameras

Change active camera by index:

import duvc_ctl as duvc

cam = duvc.CameraController(0)
print(f"Current device: {cam.device.name}")

# Switch to different camera
cam.close()
cam = duvc.CameraController(1)
print(f"Switched to: {cam.device.name}")

Each CameraController() instance binds to one device.

Query current device before operations:

try:
    cam.brightness = 150
except duvc.DeviceNotFoundError:
    print("Camera was disconnected; switching...")
    cam.close()
    cam = duvc.CameraController(0)  # Rebind to first available
    cam.brightness = 150

Concurrent operations

Avoid interleaved hardware access (may corrupt state):

import duvc_ctl as duvc
import threading

cam = duvc.CameraController(0)
lock = threading.Lock()

def set_exposure(value):
    with lock:
        cam.exposure_mode = "manual"
        cam.exposure = value

def read_exposure():
    with lock:
        mode = cam.exposure_mode
        value = cam.exposure
        return mode, value

# Safe concurrent access
threads = [
    threading.Thread(target=set_exposure, args=(5,)),
    threading.Thread(target=read_exposure),
]
for t in threads:
    t.start()
for t in threads:
    t.join()

Use locks to serialize property access on same device.

Multiple cameras = no locking needed (each device independent):

# Safe without locks - different devices
cam0 = duvc.CameraController(0)
cam1 = duvc.CameraController(1)

threading.Thread(target=lambda: setattr(cam0, 'brightness', 100)).start()
threading.Thread(target=lambda: setattr(cam1, 'brightness', 200)).start()
# No contention - different hardware

Basic usage patterns

Quick snapshot setup:

import duvc_ctl as duvc

def setup_camera():
    cam = duvc.CameraController(0)
    cam.exposure_mode = "manual"
    cam.exposure = -2  # Faster shutter
    cam.brightness = 90
    cam.focus_mode = "manual"
    cam.focus = 200  # Far field
    return cam

cam = setup_camera()
# Now ready for video capture

Configuration reload:

config = {
    "brightness": 100,
    "saturation": 64,
    "exposure_mode": "auto",
}

cam = duvc.CameraController(0)
for prop, value in config.items():
    try:
        setattr(cam, prop, value)
    except Exception as e:
        print(f"Warning: {prop} not supported or out of range")

List all available properties:

import duvc_ctl as duvc

cam = duvc.CameraController(0)
caps = cam.get_capabilities()

print("Camera Properties:")
for prop in caps.supported_camera_properties:
    try:
        value = getattr(cam, prop.lower())
        print(f"  {prop}: {value}")
    except:
        print(f"  {prop}: <unavailable>")

print("Video Properties:")
for prop in caps.supported_video_properties:
    try:
        value = getattr(cam, prop.lower())
        print(f"  {prop}: {value}")
    except:
        print(f"  {prop}: <unavailable>")

Persistent Device Reconnection

Problem: You need to reliably reconnect to a specific camera across application sessions, even if multiple cameras of the same model are connected.

Solution: Store and use the device path, which is a stable identifier.

import duvc_ctl as duvc
import json
import os

class CameraSession:
    """Manage persistent camera connection across sessions."""
    
    CONFIG_FILE = "camera_config.json"
    
    @classmethod
    def save_current_camera(cls, camera_controller):
        """Save current camera's path for later reconnection."""
        config = {
            'device_name': camera_controller.device_name,
            'device_path': camera_controller.device_path,
            'timestamp': __import__('datetime').datetime.now().isoformat()
        }
        with open(cls.CONFIG_FILE, 'w') as f:
            json.dump(config, f)
    
    @classmethod
    def reconnect_to_saved_camera(cls):
        """Attempt to reconnect to previously saved camera."""
        if not os.path.exists(cls.CONFIG_FILE):
            return None
        
        with open(cls.CONFIG_FILE) as f:
            config = json.load(f)
        
        try:
            camera = duvc_ctl.CameraController(device_path=config['device_path'])
            return camera
        except Exception as e:
            print(f"Failed to reconnect: {e}")
            return None

# Usage
if __name__ == "__main__":
    # First run
    cam = duvc_ctl.CameraController()
    print(f"Connected to: {cam.device_name}")
    CameraSession.save_current_camera(cam)
    
    # ... time passes ...
    
    # Later run
    cam = CameraSession.reconnect_to_saved_camera()
    if cam and cam.is_connected:
        print(f"Reconnected to: {cam.device_name}")
    else:
        print("Saved camera not available, using first available")
        cam = duvc_ctl.CameraController()

Multi-Camera Management with Path Lookup

For systems with multiple cameras, maintain a mapping of paths to use consistent identifiers.

import duvc_ctl as duvc

class MultiCameraManager:
    """Manage multiple cameras with stable path references."""
    
    def __init__(self):
        self.cameras = {}  # path -> CameraController
    
    def discover_and_register(self):
        """Find all cameras and store by path."""
        for device in duvc.list_devices():
            try:
                cam = duvc.CameraController(device=device)
                self.cameras[cam.device_path] = cam
                print(f"Registered: {cam.device_name}")
            except Exception as e:
                print(f"Failed: {e}")
    
    def get_camera_by_name(self, name_substring):
        """Find camera by name substring among connected cameras."""
        for path, cam in self.cameras.items():
            if name_substring.lower() in cam.device_name.lower():
                return cam
        return None
    
    def verify_all_connected(self):
        """Check connection status of all cameras."""
        results = {}
        for path, cam in self.cameras.items():
            results[cam.device_name] = cam.is_connected
        return results
    
    def close_all(self):
        """Close all camera connections."""
        for cam in self.cameras.values():
            cam.close()
        self.cameras.clear()

# Usage
manager = MultiCameraManager()
manager.discover_and_register()

# Access specific camera by name
cam1 = manager.get_camera_by_name("Logitech")
if cam1:
    cam1.brightness = 150

# Check all cameras still connected
status = manager.verify_all_connected()
for name, connected in status.items():
    print(f"{name}: {'True' if connected else 'False'}")

manager.close_all()

13.2 Bulk & Property Operations

Efficient multi-property access patterns with intelligent failure handling.


set_multiple() with partial failure

Apply multiple properties sequentially with continue-on-error:

Attempt to set all properties even if some fail. Collect errors for logging or UI feedback. Use the pattern: gather unsupported properties, log warnings, continue with remaining operations.

Properties set independently—failure of one doesn’t prevent others. For example: setting brightness fails (unsupported), but zoom, pan, and focus succeed. Application receives partial success indicator and list of failed properties.

Strategy: Sort properties by criticality. Set essential properties first. Mark non-critical failures as warnings rather than fatal errors. This prevents one unsupported property from blocking all configuration.


get_multiple() with failure

Read multiple properties with graceful fallback:

Query property ranges and current values in bulk. Missing ranges fall back to defaults. Device may not support all properties—get_property_range() returns None if unsupported.

Implementation iterates through requested properties, catches PropertyNotSupportedError per property, logs mismatches, continues to next. Return dict with successes and failures mapped separately. Caller can inspect which properties succeeded.


Partial failure handling

Pattern: Distinguish recoverable from fatal failures.

Recoverable (continue):

  • Property unsupported on device

  • Out-of-range value (clamp and retry)

  • Temporary device timeout (reconnect)

Fatal (abort):

  • Device disconnected (needs reconnection)

  • Permission denied (needs elevated privileges)

  • Hardware fault

Implementation: Wrap each property operation in try-catch. Categorize exception type. For recoverable, log and skip. For fatal, escalate or signal caller to handle reconnection.


PTZ coordination

Synchronized Pan/Tilt movement:

Apply pan and tilt together to prevent intermediate invalid states. Use combined PanTilt property if device supports it (single atomic operation). Otherwise, sequence pan first, then tilt (or vice versa based on movement distance).

Timing: Hardware may require delay between pan and tilt adjustments. Add configurable delay (typically 50–100ms) between operations if combined property unavailable.

Centering before PTZ: Reset pan/tilt to center (0° each) before large movements. Prevents jerky motion or hardware limits. Use center_camera() helper which calculates midpoint of each axis range.


Relative vs absolute

Absolute operations (pan = 45): Set precise value. Requires no prior state knowledge. Use for keyframe or preset setup.

Relative operations (pan_relative(15)): Adjust from current position. Avoids explicit state query. Ideal for smooth real-time tracking or incremental UI controls (e.g., joystick input).

Mixing: Don’t alternate absolute and relative on same property without verification. Read current state after relative move if next operation is absolute.

Efficiency: Relative is faster—no redundant read before write. Absolute is safer when target state must be exact regardless of history.


Centering

PTZ centering strategy:

Query pan/tilt ranges, compute center as (min + max) / 2 for each axis. Apply both simultaneously if device supports PanTilt property, otherwise sequence. Verify success by reading back values (may not move if hardware mechanical limits differ from reported range).

Fallback: If centering fails, leave cameras at current position. Log warning but don’t throw—centering is typically a convenience feature, not critical.


Batch efficiency

Minimize I/O round-trips:

Group property reads into single device query when possible. UVC get_device_info() returns all capabilities/properties in one enumeration.

Pipelining: Queue multiple writes before reading confirmations. Reduces latency vs. write-confirm-write-confirm loop.

Threading: Multi-camera batch operations benefit from thread-per-camera. Each camera’s operation doesn’t block others. Use locks only for shared state (e.g., device list).

Caching: Store property ranges locally after first query. Avoids redundant hardware I/O during repeated property validation.


13.3 Advanced Patterns

Resilience, state management, and runtime adaptation.


Device hotplug handling via callbacks

Register callback for device attach/detach events:

Library provides register_device_change_callback() which fires when cameras connect or disconnect. Callback receives device info and event type (attach/detach).

Callback responsibilities:

  • Update UI to reflect device list changes

  • Gracefully close handles to disconnected devices

  • Trigger reconnection logic if application was using that device

  • Do NOT perform blocking operations in callback; defer heavy work to event queue

Pattern: Callback sets flag, main thread polls flag and handles reconnection asynchronously. Prevents deadlock if callback tries to reacquire locks or enumerate devices.


Reconnection logic

Detect disconnection and retry:

Catch DeviceNotFoundError during property access. Attempt to reopen device via CameraController(device_index) or CameraController(device_name_pattern). If reopen succeeds, resume operations. If fails repeatedly, mark device offline.

Backoff strategy: First retry immediately. If still fails, wait 100ms, retry. Exponential backoff up to 5 seconds. After N failures (typically 3–5), escalate to user (notify, don’t auto-retry forever).

Device lookup by path: Prefer matching by device path (stored before disconnect) rather than index. Indices shift when cameras are plugged/unplugged; paths remain stable within OS session.


Device path tracking

Stable device identification:

Each Device object has name (human-readable) and path (system identifier). Path persists across reconnection (unless device is physically moved to different USB port).

Usage: Store device.path when camera connects. On disconnect/reconnect, search new device list by path to rebind to same physical camera. This prevents accidentally switching to a different camera if user plugs/unplugs multiple devices.

Implementation: Maintain map {path: CameraController_instance} for multi-camera applications.


Preset persistence & loading

Save/restore camera configurations:

Store property values (brightness, zoom, focus, etc.) as dict. On reconnection, reapply same dict to camera.

Format: JSON or YAML for human readability and version compatibility.

{
  "device_path": "/path/to/device",
  "properties": {
    "brightness": 100,
    "exposure_mode": "manual",
    "exposure": -2,
    "focus_mode": "manual",
    "focus": 200
  },
  "timestamp": "2025-11-06T14:30:00Z"
}

Versioning: Include schema version in preset. On load mismatch (e.g., property removed from device), skip unsupported properties with warning, apply remainder.

Partial application: If device doesn’t support all saved properties, apply only supported subset. Don’t fail entire preset load.


Property introspection

Query device capability at runtime:

Call get_supported_properties() or get_device_info() to discover which properties camera supports. Iterate through returned lists and conditionally enable UI controls or log availability.

Range introspection: Call get_property_range(property_name) for each property to determine slider/spinner bounds dynamically. Don’t hardcode ranges—devices vary widely.

Example: Brightness range on USB 2 camera is ; on USB 3, . Detect at runtime and adjust UI accordingly.


Range validation & capability

Validate before setting:

Query range, clamp value to [min, max], check step alignment if applicable. Some properties require values to be multiples of step (e.g., step=10 means only values 0, 10, 20… allowed).

Clamping: clamp(value, range.min, range.max) ensures in bounds. If out-of-range error still occurs, log warning (device range may be dynamic or misreported).

Capability gates: Wrap advanced features behind capability check. Only offer PTZ UI if device reports Pan/Tilt support. Only show autofocus if Focus property supported in Auto mode.


Real-time monitoring

Poll or stream property changes:

Periodically read property values to detect hardware changes (e.g., autofocus converged, brightness adjusted by user via physical controls on camera).

Polling interval: Typically 200–500ms balances responsiveness vs. I/O overhead. Avoid sub-100ms polling—causes thread contention and power drain.

Change detection: Compare current value to previous. Log or trigger callback only on actual change. Prevents redundant updates.

Threading: Monitor in separate thread to avoid blocking main UI thread. Use thread-safe queue to post changes to UI thread for display.

Graceful degradation: If property read fails during monitoring, skip that property in current iteration; retry next cycle. Don’t crash monitor thread.