9. Logging, Diagnostics & Callbacks¶
9.1 Logging Functions & Level Management¶
Core logging control with per-level message functions and optional callbacks for structured event handling.
set_log_level() - configure minimum log level¶
Set the minimum log level for library output. Levels: Debug, Info, Warning, Error, Critical.
def set_log_level(level: LogLevel) -> None:
"""Set minimum log level to capture."""
LogLevel enum values:
Level |
Order |
Use Case |
|---|---|---|
|
0 (lowest) |
Verbose tracing of all operations |
|
1 |
General information, device enumeration |
|
2 |
Recoverable issues, degraded operation |
|
3 |
Operation failures, retryable errors |
|
4 (highest) |
Severe failures, unrecoverable states |
Usage:
import duvc_ctl as duvc
# Set to debug level
duvc.set_log_level(duvc.LogLevel.Debug)
# Operations now logged at debug level
devices = duvc.devices()
camera = duvc.CameraController(0)
get_log_level() - query current level¶
Query the currently configured minimum log level.
def get_log_level() -> LogLevel:
"""Get current minimum log level."""
Usage:
current = duvc.get_log_level()
print(f"Current level: {duvc.tostring(current)}")
# Check if debug enabled
if current == duvc.LogLevel.Debug:
print("Debug logging active")
log_message() - emit log message¶
Emit message at specified level. Respects set_log_level() filter.
def log_message(level: LogLevel, message: str) -> None:
"""Log message at specified level."""
Usage:
duvc.log_message(duvc.LogLevel.Info, "Camera operation started")
duvc.log_message(duvc.LogLevel.Warning, "Device may be disconnecting")
log_debug/info/warning/error/critical() - convenience functions¶
Level-specific logging functions. Shorthand for common levels.
def log_debug(message: str) -> None: ...
def log_info(message: str) -> None: ...
def log_warning(message: str) -> None: ...
def log_error(message: str) -> None: ...
def log_critical(message: str) -> None: ...
Usage:
duvc.log_debug("Checking device connection")
duvc.log_info("Device enumeration complete")
duvc.log_warning("Brightness at maximum")
duvc.log_error("Failed to set property")
duvc.log_critical("Device disconnected unexpectedly")
set_log_callback() - custom message handler¶
Register callback to receive all log messages. Callback signature: (level: LogLevel, message: str) -> None.
def set_log_callback(
callback: Callable[[LogLevel, str], None]
) -> None:
"""Register callback for log messages."""
Callback contract:
Called synchronously for each log message
No exceptions should escape callback
Called from library threads (may not be main thread)
Usage:
import logging
# Direct to standard logging module
def logging_adapter(level: duvc.LogLevel, message: str):
level_name = duvc.tostring(level)
level_num = {
"Debug": logging.DEBUG,
"Info": logging.INFO,
"Warning": logging.WARNING,
"Error": logging.ERROR,
"Critical": logging.CRITICAL,
}.get(level_name, logging.INFO)
logging.log(level_num, message)
duvc.set_log_callback(logging_adapter)
# Now all library logs go to Python logging
duvc.set_log_level(duvc.LogLevel.Debug)
camera = duvc.CameraController(0) # Logs appear in logging output
File logging:
log_file = open("duvc.log", "a")
def file_logger(level: duvc.LogLevel, message: str):
level_name = duvc.tostring(level)
log_file.write(f"[{level_name}] {message}\n")
log_file.flush()
duvc.set_log_callback(file_logger)
Thread-safe callback:
from threading import Lock
log_lock = Lock()
messages = []
def thread_safe_logger(level: duvc.LogLevel, message: str):
with log_lock:
messages.append((duvc.tostring(level), message))
# Process outside lock to avoid blocking library threads
duvc.set_log_callback(thread_safe_logger)
9.2 Error Decoding & Windows Diagnostics¶
Platform-level error interpretation and detailed Windows diagnostics. All functions in this section are Windows-only.
decode_system_error() - interpret system error codes¶
Decode system error code to human-readable message. Cross-references Windows error codes with common causes.
def decode_system_error(error_code: int) -> str:
"""Decode system error code to message."""
Usage:
import duvc_ctl as duvc
# When catching SystemError
try:
camera = duvc.CameraController(0)
except duvc.SystemError as e:
if e.error_code:
decoded = duvc.decode_system_error(e.error_code)
print(f"System error: {decoded}")
get_diagnostic_info() - comprehensive system analysis¶
Query detailed system state: device list, driver status, permissions, connection state.
def get_diagnostic_info() -> dict[str, Any]:
"""
Collect system diagnostic information.
Returns:
Dict with keys:
- devices: List of cameras (name, path, connected)
- driver_info: DirectShow driver status
- os_info: Windows version
- permissions: Current user camera permissions
- errors: Any initialization errors
"""
Usage:
import duvc_ctl as duvc
import json
# Capture full diagnostics
diag = duvc.get_diagnostic_info()
print(json.dumps(diag, indent=2))
# Output:
# {
# "devices": [
# {"name": "Logitech USB HD", "path": "...", "connected": true}
# ],
# "driver_info": {"status": "ok", "version": "..."},
# "os_info": {"version": "Windows 10", "build": "19045"},
# "permissions": {"camera_access": "granted"},
# "errors": []
# }
decode_hresult() - HRESULT interpretation (Windows-only)¶
Decode Windows HRESULT value to error category and description.
def decode_hresult(hr: int) -> str:
"""Decode HRESULT to human-readable message."""
Usage:
import duvc_ctl as duvc
# HRESULT from DirectShow operation
hr = 0x80070005 # ACCESS_DENIED
decoded = duvc.decode_hresult(hr)
print(f"HRESULT 0x{hr:08X}: {decoded}")
# Output: "HRESULT 0x80070005: Access Denied"
get_hresult_details() - detailed HRESULT analysis (Windows-only)¶
Analyze HRESULT with recovery suggestions and common causes.
def get_hresult_details(hr: int) -> dict[str, str]:
"""
Analyze HRESULT with detailed information.
Returns:
Dict with:
- code: Hex representation
- facility: HRESULT facility code
- message: Human-readable message
- causes: List of common causes
- suggestions: List of recovery steps
"""
Usage:
import duvc_ctl as duvc
hr = 0x80004005 # E_FAIL
details = duvc.get_hresult_details(hr)
print(f"Code: {details['code']}")
print(f"Message: {details['message']}")
print(f"Common causes:")
for cause in details['causes']:
print(f" - {cause}")
print(f"Suggestions:")
for suggestion in details['suggestions']:
print(f" - {suggestion}")
is_device_error() - device-specific error detection (Windows-only)¶
Check if HRESULT or error code indicates device-level failure (not driver/system).
def is_device_error(hr: int | error_code: int) -> bool:
"""Check if error is device-specific."""
Usage:
import duvc_ctl as duvc
hr = 0x80070015 # DEVICE_NOT_READY
if duvc.is_device_error(hr):
print("Device error - check physical connection")
else:
print("System/driver error - reinstall or restart")
is_permission_error() - permission failure detection (Windows-only)¶
Check if HRESULT or error code indicates permission/access denied.
def is_permission_error(hr: int | error_code: int) -> bool:
"""Check if error is permission-denied."""
Usage:
import duvc_ctl as duvc
try:
camera = duvc.CameraController(0)
except duvc.PermissionDeniedError as e:
if hasattr(e, 'hresult'):
if duvc.is_permission_error(e.hresult):
print("Grant camera access in Settings")
Error message templates and patterns¶
Common Windows errors and their standard meanings:
HRESULT |
Facility |
Meaning |
Recovery |
|---|---|---|---|
0x80070005 |
WIN32 |
ACCESS_DENIED |
Check privacy settings, run as admin |
0x80070002 |
WIN32 |
FILE_NOT_FOUND |
Camera driver missing or corrupted |
0x80070015 |
WIN32 |
DEVICE_NOT_READY |
Camera disconnected, reconnect USB |
0x8004DF00 |
COM |
Device creation failed |
Device in use by other app |
0x80004005 |
COM |
E_FAIL (generic) |
Check driver logs, try USB restart |
Usage:
import duvc_ctl as duvc
try:
camera = duvc.CameraController(0)
except Exception as e:
if hasattr(e, 'error_code'):
details = duvc.get_hresult_details(e.error_code)
for suggestion in details['suggestions']:
print(f"Try: {suggestion}")
Internal error recovery patterns¶
Library uses multi-level recovery when errors occur:
Level 1: Retry with backoff
Transient errors (timeout, busy) → retry 2-3x with 100ms delay
Connection drops → attempt reconnect
Level 2: Query and adapt
Unsupported property → query capabilities, use alternative
Out of range → query range, clamp value
Permission denied → suggest user action
Level 3: Escalate and fallback
Driver error → log, suggest driver reinstall
System error → recommend restart
Unrecoverable → fail gracefully
Example recovery pattern:
import duvc_ctl as duvc
import time
def resilient_set_brightness(device, value, retries=3):
"""Set brightness with automatic recovery."""
for attempt in range(retries):
try:
camera = duvc.CameraController(device)
camera.set_brightness(value)
return True
except duvc.PropertyValueOutOfRangeError as e:
# Level 1: Adapt value
corrected = max(e.min_val, min(e.max_val, value))
camera = duvc.CameraController(device)
camera.set_brightness(corrected)
return True
except duvc.DeviceBusyError:
# Level 2: Retry with backoff
if attempt < retries - 1:
time.sleep(0.1 * (2 ** attempt))
continue
except duvc.PermissionDeniedError:
# Level 3: Escalate
print("Camera access denied. Grant permission in Settings > Privacy")
return False
except duvc.SystemError as e:
# Level 3: Diagnose
details = duvc.get_hresult_details(e.error_code)
print(f"System error: {details['message']}")
for suggestion in details['suggestions']:
print(f" - {suggestion}")
return False
return False
9.3 Device Callbacks & Hot-Plug¶
Event-driven device hot-plug detection with thread-safe callback registration.
register_device_change_callback() - watch for device changes¶
Register callback to receive notifications when cameras connect/disconnect. Callback runs in library thread pool.
def register_device_change_callback(
callback: Callable[[str, str], None]
) -> None:
"""
Register callback for device connect/disconnect events.
Args:
callback: Function(event_type: str, device_name: str)
event_type: "connected" or "disconnected"
device_name: Name of changed device
"""
Callback signature:
event_type(str): Either"connected"or"disconnected"device_name(str): Camera name that changed
Usage:
import duvc_ctl as duvc
def on_device_change(event_type: str, device_name: str):
print(f"{device_name}: {event_type}")
# Register callback
duvc.register_device_change_callback(on_device_change)
# Physical hot-plug events now trigger callback
# When user plugs in camera: "Logitech USB HD: connected"
# When user unplugs camera: "Logitech USB HD: disconnected"
unregister_device_change_callback() - stop watching¶
Unregister previously registered device change callback.
def unregister_device_change_callback(
callback: Callable[[str, str], None]
) -> None:
"""Unregister device change callback."""
Usage:
import duvc_ctl as duvc
def on_device_change(event_type: str, device_name: str):
print(f"{device_name}: {event_type}")
duvc.register_device_change_callback(on_device_change)
# Later: stop listening
duvc.unregister_device_change_callback(on_device_change)
GIL management in callbacks¶
Library automatically acquires Python GIL before calling callback. Safe to call Python APIs from callback. Callback must complete quickly.
Callback execution context:
Called from C++ thread pool
GIL held during callback execution
Other Python threads may block waiting for GIL
Exceptions in callback are logged, not propagated
Quick callback requirement: Keep callbacks under 10ms. Long operations may block device enumeration.
Usage:
import duvc_ctl as duvc
from queue import Queue
# CORRECT: Queue event for processing elsewhere
event_queue = Queue()
def on_device_change(event_type: str, device_name: str):
event_queue.put((event_type, device_name))
duvc.register_device_change_callback(on_device_change)
# Separate thread processes events
while True:
event_type, device_name = event_queue.get()
# Process event (may be slow)
if event_type == "connected":
print(f"Camera {device_name} connected")
Incorrect: Long operations in callback:
import duvc_ctl as duvc
import time
def on_device_change(event_type: str, device_name: str):
# WRONG: This blocks library threads
time.sleep(5) # Don't do this!
print(f"{device_name}: {event_type}")
duvc.register_device_change_callback(on_device_change)
Threading patterns with callbacks¶
Pattern 1: Queue-based event processing
import duvc_ctl as duvc
from queue import Queue
from threading import Thread
event_queue = Queue()
def on_device_change(event_type: str, device_name: str):
event_queue.put((event_type, device_name))
def event_processor():
while True:
event_type, device_name = event_queue.get()
if event_type == "connected":
print(f"Handling new camera: {device_name}")
# Long operations OK here
elif event_type == "disconnected":
print(f"Camera removed: {device_name}")
duvc.register_device_change_callback(on_device_change)
processor_thread = Thread(target=event_processor, daemon=True)
processor_thread.start()
Pattern 2: Device pool with auto-recovery
import duvc_ctl as duvc
from threading import Lock
active_cameras = {}
camera_lock = Lock()
def on_device_change(event_type: str, device_name: str):
if event_type == "connected":
try:
cam = duvc.CameraController(device_name)
with camera_lock:
active_cameras[device_name] = cam
print(f"Auto-connected: {device_name}")
except Exception as e:
print(f"Failed to connect {device_name}: {e}")
elif event_type == "disconnected":
with camera_lock:
if device_name in active_cameras:
active_cameras[device_name].close()
del active_cameras[device_name]
print(f"Auto-closed: {device_name}")
duvc.register_device_change_callback(on_device_change)
# Cameras automatically managed as connected/disconnected
Pattern 3: Listener pattern with state tracking
import duvc_ctl as duvc
from threading import Lock
class DeviceMonitor:
def __init__(self):
self.connected = set()
self.lock = Lock()
duvc.register_device_change_callback(self.on_change)
def on_change(self, event_type: str, device_name: str):
with self.lock:
if event_type == "connected":
self.connected.add(device_name)
else:
self.connected.discard(device_name)
def get_active(self):
with self.lock:
return list(self.connected)
def wait_for(self, device_name, timeout=5):
import time
deadline = time.time() + timeout
while time.time() < deadline:
if device_name in self.get_active():
return True
time.sleep(0.1)
return False
monitor = DeviceMonitor()
# Later: wait for specific camera
if monitor.wait_for("Logitech", timeout=10):
print("Camera connected!")
Pattern 4: Event listener with weak references (Prevents memory leaks)
import duvc_ctl as duvc
import weakref
class CameraListener:
def __init__(self, name):
self.name = name
duvc.register_device_change_callback(self.on_change)
def on_change(self, event_type: str, device_name: str):
print(f"{self.name}: {device_name} {event_type}")
def cleanup(self):
duvc.unregister_device_change_callback(self.on_change)
listener = CameraListener("MyApp")
# Later: clean up properly
listener.cleanup()
del listener