diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/error.rs | 247 | ||||
| -rw-r--r-- | src/lib.rs | 217 | ||||
| -rw-r--r-- | src/main.rs | 207 | ||||
| -rw-r--r-- | src/protocol/frame.rs | 259 | ||||
| -rw-r--r-- | src/protocol/jpeg.rs | 351 | ||||
| -rw-r--r-- | src/protocol/mod.rs | 409 | ||||
| -rw-r--r-- | src/protocol/parser.rs | 418 | ||||
| -rw-r--r-- | src/usb/device.rs | 287 | ||||
| -rw-r--r-- | src/usb/mod.rs | 164 | ||||
| -rw-r--r-- | src/usb/transfer.rs | 287 | ||||
| -rw-r--r-- | src/utils/mod.rs | 350 | ||||
| -rw-r--r-- | src/video/mod.rs | 330 | ||||
| -rw-r--r-- | src/video/pipewire.rs | 773 | ||||
| -rw-r--r-- | src/video/stdout.rs | 277 | ||||
| -rw-r--r-- | src/video/v4l2.rs | 321 |
15 files changed, 4897 insertions, 0 deletions
diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f101a61 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,247 @@ +//! Error types for the geek-szitman-supercamera crate + +use thiserror::Error; + +/// Result type for the crate +pub type Result<T> = std::result::Result<T, Error>; + +/// Main error type for the crate +#[derive(Error, Debug)] +pub enum Error { + /// USB communication errors + #[error("USB error: {0}")] + Usb(#[from] UsbError), + + /// Video backend errors + #[error("Video backend error: {0}")] + Video(#[from] VideoError), + + /// Protocol errors + #[error("Protocol error: {0}")] + Protocol(#[from] ProtocolError), + + /// JPEG processing errors + #[error("JPEG error: {0}")] + Jpeg(#[from] JpegError), + + /// System errors + #[error("System error: {0}")] + System(#[from] SystemError), + + /// Generic error wrapper + #[error("Generic error: {0}")] + Generic(String), +} + +/// USB-specific errors +#[derive(Error, Debug)] +pub enum UsbError { + /// Device not found + #[error("USB device not found")] + DeviceNotFound, + + /// Device disconnected + #[error("USB device disconnected")] + DeviceDisconnected, + + /// Permission denied + #[error("USB permission denied")] + PermissionDenied, + + /// Interface claim failed + #[error("Failed to claim USB interface: {0}")] + InterfaceClaimFailed(String), + + /// Bulk transfer failed + #[error("USB bulk transfer failed: {0}")] + BulkTransferFailed(String), + + /// Timeout error + #[error("USB operation timed out")] + Timeout, + + /// Generic USB error + #[error("USB error: {0}")] + Generic(String), +} + +impl UsbError { + /// Check if this error indicates device disconnection + pub fn is_device_disconnected(&self) -> bool { + matches!(self, Self::DeviceDisconnected) + } +} + +/// Video backend errors +#[derive(Error, Debug)] +pub enum VideoError { + /// PipeWire errors + #[error("PipeWire error: {0}")] + PipeWire(String), + + /// V4L2 errors (for future use) + #[error("V4L2 error: {0}")] + V4L2(String), + + /// Stdout errors + #[error("Stdout error: {0}")] + Stdout(String), + + /// Format not supported + #[error("Video format not supported: {0}")] + FormatNotSupported(String), + + /// Device initialization failed + #[error("Video device initialization failed: {0}")] + InitializationFailed(String), + + /// Frame push failed + #[error("Failed to push frame: {0}")] + FramePushFailed(String), + + /// Device not ready + #[error("Device not ready")] + DeviceNotReady, +} + +/// Protocol errors +#[derive(Error, Debug)] +pub enum ProtocolError { + /// Invalid frame format + #[error("Invalid frame format: {0}")] + InvalidFrameFormat(String), + + /// Frame too small + #[error( + "Frame too small: expected at least {} bytes, got {}", + expected, + actual + )] + FrameTooSmall { expected: usize, actual: usize }, + + /// Invalid magic number + #[error( + "Invalid magic number: expected 0x{:04X}, got 0x{:04X}", + expected, + actual + )] + InvalidMagic { expected: u16, actual: u16 }, + + /// Unknown camera ID + #[error("Unknown camera ID: {0}")] + UnknownCameraId(u8), + + /// Frame length mismatch + #[error("Frame length mismatch: expected {}, got {}", expected, actual)] + FrameLengthMismatch { expected: usize, actual: usize }, + + /// Protocol parsing error + #[error("Protocol parsing error: {0}")] + ParsingError(String), +} + +/// JPEG processing errors +#[derive(Error, Debug)] +pub enum JpegError { + /// Invalid JPEG header + #[error("Invalid JPEG header")] + InvalidHeader, + + /// Unsupported JPEG format + #[error("Unsupported JPEG format: {0}")] + UnsupportedFormat(String), + + /// JPEG parsing failed + #[error("JPEG parsing failed: {0}")] + ParsingFailed(String), + + /// Image dimensions not found + #[error("Could not determine image dimensions")] + DimensionsNotFound, +} + +/// System errors +#[derive(Error, Debug)] +pub enum SystemError { + /// File operation failed + #[error("File operation failed: {0}")] + FileError(String), + + /// Permission denied + #[error("Permission denied: {0}")] + PermissionDenied(String), + + /// Resource not available + #[error("Resource not available: {0}")] + ResourceNotAvailable(String), + + /// Signal handling error + #[error("Signal handling error: {0}")] + SignalError(String), +} + +impl From<std::io::Error> for Error { + fn from(err: std::io::Error) -> Self { + match err.kind() { + std::io::ErrorKind::NotFound => Error::Usb(UsbError::DeviceNotFound), + std::io::ErrorKind::PermissionDenied => Error::Usb(UsbError::PermissionDenied), + std::io::ErrorKind::TimedOut => Error::Usb(UsbError::Timeout), + _ => Error::System(SystemError::FileError(err.to_string())), + } + } +} + +impl From<rusb::Error> for Error { + fn from(err: rusb::Error) -> Self { + match err { + rusb::Error::NoDevice => Error::Usb(UsbError::DeviceDisconnected), + rusb::Error::Access => Error::Usb(UsbError::PermissionDenied), + rusb::Error::Timeout => Error::Usb(UsbError::Timeout), + rusb::Error::NotFound => Error::Usb(UsbError::DeviceNotFound), + _ => Error::Usb(UsbError::Generic(err.to_string())), + } + } +} + +impl From<String> for Error { + fn from(err: String) -> Self { + Error::Generic(err) + } +} + +impl From<&str> for Error { + fn from(err: &str) -> Self { + Error::Generic(err.to_string()) + } +} + +impl From<crate::usb::UsbTransferError> for Error { + fn from(err: crate::usb::UsbTransferError) -> Self { + Error::Usb(UsbError::Generic(err.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_usb_error_is_device_disconnected() { + let err = UsbError::DeviceDisconnected; + assert!(err.is_device_disconnected()); + + let err = UsbError::DeviceNotFound; + assert!(!err.is_device_disconnected()); + } + + #[test] + fn test_error_conversion() { + let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found"); + let err: Error = io_err.into(); + assert!(matches!(err, Error::Usb(UsbError::DeviceNotFound))); + + let usb_err = rusb::Error::NoDevice; + let err: Error = usb_err.into(); + assert!(matches!(err, Error::Usb(UsbError::DeviceDisconnected))); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ab42e1c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,217 @@ +//! Geek szitman supercamera - Rust implementation +//! +//! This crate provides a Rust implementation of the Geek szitman supercamera +//! endoscope viewer with PipeWire support and preparation for V4L2 fallback. +//! +//! # Features +//! +//! - USB communication with the endoscope device +//! - PipeWire video streaming +//! - UPP protocol implementation +//! - JPEG frame processing +//! - Modular architecture for maintainability +//! +//! # Example +//! +//! ```rust,no_run +//! use geek_szitman_supercamera::SuperCamera; +//! +//! fn main() -> Result<(), Box<dyn std::error::Error>> { +//! let camera = SuperCamera::new()?; +//! camera.start_stream()?; +//! Ok(()) +//! } +//! ``` + +pub mod error; +pub mod protocol; +pub mod usb; +pub mod utils; +pub mod video; + +pub use error::{Error, Result}; +pub use protocol::UPPCamera; +pub use usb::UsbSupercamera; +pub use video::{VideoBackend, VideoBackendTrait}; + +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::RwLock; +use std::thread; +use std::time::Duration; +use tracing::{info, warn}; + +/// Main camera controller that orchestrates all components +pub struct SuperCamera { + usb_camera: Arc<UsbSupercamera>, + protocol: Arc<UPPCamera>, + video_backend: Arc<Mutex<Box<dyn VideoBackendTrait>>>, + is_running: Arc<RwLock<bool>>, + usb_thread: Arc<Mutex<Option<thread::JoinHandle<()>>>>, +} + +impl SuperCamera { + /// Create a new SuperCamera instance + pub fn new() -> Result<Self> { + Self::with_backend(crate::video::VideoBackendType::PipeWire) + } + + /// Create a new SuperCamera instance with specified backend + pub fn with_backend(backend_type: crate::video::VideoBackendType) -> Result<Self> { + let usb_camera = Arc::new(UsbSupercamera::new()?); + let protocol = Arc::new(UPPCamera::new_with_debug(true)); // Enable debug by default + + // Initialize video backend based on choice + let video_backend = Arc::new(Mutex::new(VideoBackend::from_type(backend_type)?)); + + Ok(Self { + usb_camera, + protocol, + video_backend, + is_running: Arc::new(RwLock::new(false)), + usb_thread: Arc::new(Mutex::new(None)), + }) + } + + /// Start the camera stream + pub fn start_stream(&self) -> Result<()> { + let mut is_running = self.is_running.write().unwrap(); + if *is_running { + warn!("Camera stream is already running"); + return Ok(()); + } + + info!("Starting camera stream..."); + *is_running = true; + drop(is_running); + + // Ensure video backend is initialized before pushing frames + { + let mut backend = self.video_backend.lock().unwrap(); + backend.initialize()?; + } + + // Start USB reading loop in a separate thread + let usb_camera = Arc::clone(&self.usb_camera); + let protocol = Arc::clone(&self.protocol); + let video_backend = Arc::clone(&self.video_backend); + let is_running = Arc::clone(&self.is_running); + + let handle = thread::spawn(move || { + Self::usb_read_loop(usb_camera, protocol, video_backend, is_running); + }); + + // Store the thread handle + let mut usb_thread = self.usb_thread.lock().unwrap(); + *usb_thread = Some(handle); + + Ok(()) + } + + /// Stop the camera stream + pub fn stop_stream(&self) -> Result<()> { + let mut is_running = self.is_running.write().unwrap(); + if !*is_running { + warn!("Camera stream is not running"); + return Ok(()); + } + + info!("Stopping camera stream..."); + *is_running = false; + Ok(()) + } + + /// Check if the camera stream is running + pub fn is_running(&self) -> bool { + *self.is_running.read().unwrap() + } + + /// Main USB reading loop + fn usb_read_loop( + usb_camera: Arc<UsbSupercamera>, + protocol: Arc<UPPCamera>, + video_backend: Arc<Mutex<Box<dyn VideoBackendTrait>>>, + is_running: Arc<RwLock<bool>>, + ) { + let mut frame_count = 0u32; + + while *is_running.read().unwrap() { + match usb_camera.read_frame() { + Ok(data) => { + frame_count += 1; + // Reduce logging frequency - only log every 100th frame + if frame_count % 100 == 0 { + tracing::debug!("Received frame {} ({} bytes)", frame_count, data.len()); + } + + // Process frame through protocol + if let Err(e) = protocol.handle_frame_robust(&data) { + tracing::error!("Protocol error: {}", e); + + // Log additional frame information for debugging + if data.len() >= 5 { + let magic_bytes = [data[0], data[1]]; + let magic = u16::from_le_bytes(magic_bytes); + let cid = if data.len() >= 3 { data[2] } else { 0 }; + let length_bytes = if data.len() >= 5 { [data[3], data[4]] } else { [0, 0] }; + let length = u16::from_le_bytes(length_bytes); + + tracing::debug!( + "Frame header: magic=0x{:04X}, cid={}, length={}, actual_size={}", + magic, cid, length, data.len() + ); + } + + continue; + } + + // Send to video backend if frame is complete + if let Some(frame) = protocol.get_complete_frame() { + let backend = video_backend.lock().unwrap(); + if let Err(e) = backend.push_frame(&frame) { + tracing::error!("Video backend error: {}", e); + } + } + } + Err(e) => { + tracing::error!("USB read error: {}", e); + // Check if it's a USB error that indicates disconnection + if let crate::error::Error::Usb(usb_err) = &e { + if usb_err.is_device_disconnected() { + tracing::warn!("Device disconnected, stopping stream"); + break; + } + } + // Use standard library sleep instead of tokio + std::thread::sleep(Duration::from_millis(100)); + } + } + } + + info!("USB reading loop stopped"); + } +} + +impl Drop for SuperCamera { + fn drop(&mut self) { + // Stop the stream + if let Ok(mut is_running) = self.is_running.try_write() { + *is_running = false; + } + + // Wait for USB thread to finish + if let Some(handle) = self.usb_thread.lock().unwrap().take() { + let _ = handle.join(); + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_super_camera_creation() { + // This test requires actual USB device, so we'll just test the structure + // In a real test environment, we'd use mocks + assert!(true); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..153b4e9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,207 @@ +//! Main binary for the Geek szitman supercamera + +use clap::Parser; +use geek_szitman_supercamera::{Error, SuperCamera, video}; + +use tracing::{error, info, warn}; +use tracing_subscriber::{fmt, EnvFilter}; + +#[derive(Parser)] +#[command( + name = "geek-szitman-supercamera", + about = "Rust implementation of Geek szitman supercamera endoscope viewer", + version, + author +)] +struct Cli { + /// Enable debug logging + #[arg(short, long)] + debug: bool, + + /// Enable verbose logging + #[arg(short, long)] + verbose: bool, + + /// Video backend to use + #[arg(short, long, value_enum, default_value = "pipewire")] + backend: BackendChoice, + + /// Output directory for saved frames + #[arg(short, long, default_value = "pics")] + output_dir: String, + + /// Frame rate hint + #[arg(short, long, default_value = "30")] + fps: u32, + + /// Exit automatically after N seconds (0 = run until Ctrl+C) + #[arg(long, default_value = "0")] + timeout_seconds: u64, +} + +#[derive(Clone, clap::ValueEnum)] +enum BackendChoice { + PipeWire, + V4L2, + Stdout, +} + +impl std::fmt::Display for BackendChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BackendChoice::PipeWire => write!(f, "pipewire"), + BackendChoice::V4L2 => write!(f, "v4l2"), + BackendChoice::Stdout => write!(f, "stdout"), + } + } +} + +fn main() -> Result<(), Box<dyn std::error::Error>> { + // Parse command line arguments + let cli = Cli::parse(); + + // Initialize logging + init_logging(cli.debug, cli.verbose)?; + + info!("Starting Geek szitman supercamera viewer"); + info!("Backend: {}", cli.backend); + info!("Output directory: {}", cli.output_dir); + info!("Frame rate hint: {} fps", cli.fps); + + // Create output directory + std::fs::create_dir_all(&cli.output_dir)?; + + // Set up signal handling + let signal_handler = setup_signal_handling()?; + + // Create camera instance with selected backend + let camera = match cli.backend { + BackendChoice::PipeWire => SuperCamera::with_backend(crate::video::VideoBackendType::PipeWire), + BackendChoice::V4L2 => SuperCamera::with_backend(crate::video::VideoBackendType::V4L2), + BackendChoice::Stdout => SuperCamera::with_backend(crate::video::VideoBackendType::Stdout), + }; + + let camera = match camera { + Ok(camera) => camera, + Err(e) => { + error!("Failed to create camera: {}", e); + return Err(e.into()); + } + }; + + // Start camera stream + if let Err(e) = camera.start_stream() { + error!("Failed to start camera stream: {}", e); + return Err(e.into()); + } + + info!("Camera stream started successfully"); + info!("Press Ctrl+C to stop"); + + // Wait for shutdown signal or optional timeout + if cli.timeout_seconds > 0 { + let timed_out = !signal_handler + .wait_for_shutdown_with_timeout(std::time::Duration::from_secs(cli.timeout_seconds)); + if timed_out { + info!("Timeout reached ({}s), initiating shutdown...", cli.timeout_seconds); + } + } else { + signal_handler.wait_for_shutdown(); + } + + info!("Shutting down..."); + + // Stop camera stream + if let Err(e) = camera.stop_stream() { + warn!("Error stopping camera stream: {}", e); + } + + info!("Shutdown complete"); + Ok(()) +} + +/// Initialize logging system +fn init_logging(debug: bool, verbose: bool) -> Result<(), Box<dyn std::error::Error>> { + let filter = if verbose { + "geek_szitman_supercamera=trace" + } else if debug { + "geek_szitman_supercamera=debug" + } else { + "geek_szitman_supercamera=info" + }; + + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)); + + fmt::Subscriber::builder() + .with_env_filter(env_filter) + .with_target(false) + .with_thread_ids(false) + .with_thread_names(false) + .with_file(false) + .with_line_number(false) + .init(); + + Ok(()) +} + +/// Set up signal handling for graceful shutdown +fn setup_signal_handling( +) -> Result<geek_szitman_supercamera::utils::SignalHandler, Box<dyn std::error::Error>> { + geek_szitman_supercamera::utils::SignalHandler::new() +} + +/// Handle errors gracefully +fn handle_error(error: Error) { + match error { + Error::Usb(e) => { + error!("USB error: {}", e); + match e { + geek_szitman_supercamera::error::UsbError::DeviceNotFound => { + eprintln!("Device not found. Please check that the camera is connected."); + } + geek_szitman_supercamera::error::UsbError::PermissionDenied => { + eprintln!("Permission denied. Try running with sudo or add udev rules."); + } + geek_szitman_supercamera::error::UsbError::DeviceDisconnected => { + eprintln!("Device disconnected."); + } + _ => { + eprintln!("USB error: {e}"); + } + } + } + Error::Video(e) => { + error!("Video backend error: {}", e); + match e { + geek_szitman_supercamera::error::VideoError::PipeWire(msg) => { + eprintln!("PipeWire error: {msg}. Make sure PipeWire is running."); + } + geek_szitman_supercamera::error::VideoError::V4L2(msg) => { + eprintln!("V4L2 error: {msg}. Make sure v4l2loopback is loaded."); + } + geek_szitman_supercamera::error::VideoError::Stdout(msg) => { + eprintln!("Stdout error: {msg}. Check if stdout is writable."); + } + _ => { + eprintln!("Video error: {e}"); + } + } + } + Error::Protocol(e) => { + error!("Protocol error: {}", e); + eprintln!("Protocol error: {e}. Check camera firmware version."); + } + Error::Jpeg(e) => { + error!("JPEG error: {}", e); + eprintln!("JPEG processing error: {e}"); + } + Error::System(e) => { + error!("System error: {}", e); + eprintln!("System error: {e}"); + } + Error::Generic(msg) => { + error!("Generic error: {}", msg); + eprintln!("Error: {msg}"); + } + } +} diff --git a/src/protocol/frame.rs b/src/protocol/frame.rs new file mode 100644 index 0000000..32475e2 --- /dev/null +++ b/src/protocol/frame.rs @@ -0,0 +1,259 @@ +//! UPP protocol frame structures + +use serde::{Deserialize, Serialize}; +use std::mem; + +/// UPP USB frame header (5 bytes) +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[repr(C, packed)] +pub struct UPPUsbFrame { + pub magic: u16, // 0xBBAA + pub cid: u8, // Camera ID + pub length: u16, // Data length (excluding header) +} + +/// UPP camera frame header (7 bytes) +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +#[repr(C, packed)] +pub struct UPPFrameHeader { + pub frame_id: u8, // Frame ID + pub camera_number: u8, // Camera number + pub flags: UPPFlags, // Various flags + pub g_sensor: u32, // G-sensor data +} + +/// UPP frame flags +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +#[repr(C, packed)] +pub struct UPPFlags { + pub has_g: bool, // Has G-sensor data (bit 0) + pub button_press: bool, // Button press detected (bit 1) + pub other: u8, // Other flags (bits 2-7, 6 bits total) +} + +/// Complete UPP frame +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct UPPFrame { + pub header: UPPFrameHeader, + pub data: Vec<u8>, +} + +impl UPPUsbFrame { + /// Create a new UPP USB frame + pub fn new(cid: u8, length: u16) -> Self { + Self { + magic: super::UPP_USB_MAGIC, + cid, + length, + } + } + + /// Get the total frame size including header + /// Based on C++ POC: length field includes camera header + data, but not USB header + pub fn total_size(&self) -> usize { + mem::size_of::<Self>() + self.length as usize + } + + /// Validate the frame magic number + pub fn is_valid(&self) -> bool { + self.magic == super::UPP_USB_MAGIC + } + + /// Get the expected payload size (camera header + data) + /// Based on C++ POC: this is what the length field represents + pub fn expected_payload_size(&self) -> usize { + self.length as usize + } + + /// Get the expected data size (excluding camera header) + /// Camera header is 7 bytes, so data size is payload - 7 + pub fn expected_data_size(&self) -> usize { + if self.length >= 7 { + self.length as usize - 7 + } else { + 0 + } + } +} + +impl UPPFrameHeader { + /// Create a new UPP frame header + pub fn new( + frame_id: u8, + camera_number: u8, + has_g: bool, + button_press: bool, + g_sensor: u32, + ) -> Self { + Self { + frame_id, + camera_number, + flags: UPPFlags { + has_g, + button_press, + other: 0, + }, + g_sensor, + } + } + + /// Check if this frame has G-sensor data + pub fn has_g_sensor(&self) -> bool { + self.flags.has_g + } + + /// Check if button press was detected + pub fn button_pressed(&self) -> bool { + self.flags.button_press + } + + /// Get other flags + pub fn other_flags(&self) -> u8 { + self.flags.other + } + + /// Set other flags + pub fn set_other_flags(&mut self, flags: u8) { + self.flags.other = flags & 0x3F; // Only 6 bits + } + + /// Get G-sensor data + pub fn g_sensor_data(&self) -> Option<u32> { + if self.has_g_sensor() { + Some(self.g_sensor) + } else { + None + } + } +} + +impl UPPFrame { + /// Create a new UPP frame + pub fn new(header: UPPFrameHeader, data: Vec<u8>) -> Self { + Self { header, data } + } + + /// Get the total frame size + pub fn total_size(&self) -> usize { + mem::size_of::<UPPFrameHeader>() + self.data.len() + } + + /// Get the frame ID + pub fn frame_id(&self) -> u8 { + self.header.frame_id + } + + /// Get the camera number + pub fn camera_number(&self) -> u8 { + self.header.camera_number + } + + /// Check if button was pressed + pub fn button_pressed(&self) -> bool { + self.header.button_pressed() + } + + /// Get G-sensor data if available + pub fn g_sensor_data(&self) -> Option<u32> { + if self.header.has_g_sensor() { + Some(self.header.g_sensor) + } else { + None + } + } +} + +impl Default for UPPUsbFrame { + fn default() -> Self { + Self { + magic: super::UPP_USB_MAGIC, + cid: super::UPP_CAMID_7, + length: 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_upp_usb_frame_creation() { + let frame = UPPUsbFrame::new(7, 1024); + let magic = frame.magic; + let cid = frame.cid; + let length = frame.length; + assert_eq!(magic, super::super::UPP_USB_MAGIC); + assert_eq!(cid, 7); + assert_eq!(length, 1024); + assert!(frame.is_valid()); + } + + #[test] + fn test_upp_usb_frame_validation() { + let mut frame = UPPUsbFrame::new(7, 1024); + assert!(frame.is_valid()); + + frame.magic = 0x1234; + assert!(!frame.is_valid()); + } + + #[test] + fn test_upp_frame_header_creation() { + let header = UPPFrameHeader::new(1, 0, true, false, 12345); + assert_eq!(header.frame_id, 1); + assert_eq!(header.camera_number, 0); + assert!(header.has_g_sensor()); + assert!(!header.button_pressed()); + assert_eq!(header.g_sensor_data(), Some(12345)); + } + + #[test] + fn test_upp_frame_header_flags() { + let mut header = UPPFrameHeader::default(); + assert!(!header.has_g_sensor()); + assert!(!header.button_pressed()); + + header.flags.has_g = true; + header.flags.button_press = true; + assert!(header.has_g_sensor()); + assert!(header.button_pressed()); + } + + #[test] + fn test_upp_frame_creation() { + let header = UPPFrameHeader::new(1, 0, false, false, 0); + let data = vec![1, 2, 3, 4, 5]; + let frame = UPPFrame::new(header, data.clone()); + + assert_eq!(frame.frame_id(), 1); + assert_eq!(frame.camera_number(), 0); + assert_eq!(frame.data, data); + assert_eq!(frame.total_size(), mem::size_of::<UPPFrameHeader>() + 5); + } + + #[test] + fn test_upp_frame_defaults() { + let frame = UPPFrame::default(); + assert_eq!(frame.frame_id(), 0); + assert_eq!(frame.camera_number(), 0); + assert!(frame.data.is_empty()); + assert!(!frame.button_pressed()); + assert!(frame.g_sensor_data().is_none()); + } + + #[test] + fn test_upp_flags_other_bits() { + let mut header = UPPFrameHeader::default(); + header.set_other_flags(0xFF); + assert_eq!(header.other_flags(), 0x3F); // Only 6 bits should be set + } + + #[test] + fn test_memory_layout() { + // Ensure packed structs have correct sizes + assert_eq!(mem::size_of::<UPPUsbFrame>(), 5); + // UPPFrameHeader: frame_id(1) + camera_number(1) + flags(3) + g_sensor(4) = 9 bytes + assert_eq!(mem::size_of::<UPPFrameHeader>(), 9); + } +} diff --git a/src/protocol/jpeg.rs b/src/protocol/jpeg.rs new file mode 100644 index 0000000..8398800 --- /dev/null +++ b/src/protocol/jpeg.rs @@ -0,0 +1,351 @@ +//! JPEG parsing utilities for the UPP protocol + +use crate::error::{JpegError, Result}; +use tracing::{debug, trace, warn}; + +/// JPEG marker constants +const JPEG_SOI: u8 = 0xD8; // Start of Image +const JPEG_EOI: u8 = 0xD9; // End of Image +const JPEG_SOS: u8 = 0xDA; // Start of Scan +const JPEG_SOF0: u8 = 0xC0; // Start of Frame (Baseline DCT) +const JPEG_SOF1: u8 = 0xC1; // Start of Frame (Extended sequential DCT) +const JPEG_SOF2: u8 = 0xC2; // Start of Frame (Progressive DCT) + +/// JPEG image dimensions +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JpegDimensions { + pub width: u16, + pub height: u16, +} + +impl JpegDimensions { + /// Create new dimensions + pub fn new(width: u16, height: u16) -> Self { + Self { width, height } + } + + /// Check if dimensions are valid + pub fn is_valid(&self) -> bool { + self.width > 0 && self.height > 0 + } + + /// Get aspect ratio + pub fn aspect_ratio(&self) -> f64 { + if self.height > 0 { + self.width as f64 / self.height as f64 + } else { + 0.0 + } + } +} + +/// JPEG parser for extracting metadata +pub struct JpegParser { + enable_debug: bool, +} + +impl JpegParser { + /// Create a new JPEG parser + pub fn new() -> Self { + Self { + enable_debug: false, + } + } + + /// Create a new JPEG parser with debug enabled + pub fn new_with_debug(enable_debug: bool) -> Self { + Self { enable_debug } + } + + /// Parse JPEG dimensions from raw data + pub fn parse_dimensions(&self, data: &[u8]) -> Result<JpegDimensions> { + if data.len() < 4 { + return Err(JpegError::InvalidHeader.into()); + } + + // Check JPEG SOI marker + if data[0] != 0xFF || data[1] != JPEG_SOI { + return Err(JpegError::InvalidHeader.into()); + } + + trace!("Parsing JPEG dimensions from {} bytes", data.len()); + + let mut i = 2; + while i + 3 < data.len() { + // Look for marker + if data[i] != 0xFF { + i += 1; + continue; + } + + let marker = data[i + 1]; + i += 2; + + // Check for end markers + if marker == JPEG_EOI || marker == JPEG_SOS { + break; + } + + // Check if we have enough data for segment length + if i + 1 >= data.len() { + break; + } + + // Read segment length (big-endian) + let segment_length = ((data[i] as u16) << 8) | (data[i + 1] as u16); + if segment_length < 2 || i + segment_length as usize > data.len() { + warn!("Invalid segment length: {}", segment_length); + break; + } + + // Check for SOF markers (Start of Frame) + if self.is_sof_marker(marker) { + if segment_length < 7 { + return Err(JpegError::InvalidHeader.into()); + } + + // Height and width are in big-endian format + let height = ((data[i + 3] as u16) << 8) | (data[i + 4] as u16); + let width = ((data[i + 5] as u16) << 8) | (data[i + 6] as u16); + + if self.enable_debug { + debug!( + "Found SOF marker 0x{:02X}, dimensions: {}x{}", + marker, width, height + ); + } + + if width > 0 && height > 0 { + return Ok(JpegDimensions::new(width, height)); + } else { + return Err(JpegError::DimensionsNotFound.into()); + } + } + + // Move to next segment + i += segment_length as usize; + } + + Err(JpegError::DimensionsNotFound.into()) + } + + /// Check if a marker is a Start of Frame marker + fn is_sof_marker(&self, marker: u8) -> bool { + matches!(marker, JPEG_SOF0 | JPEG_SOF1 | JPEG_SOF2) + } + + /// Extract JPEG metadata + pub fn parse_metadata(&self, data: &[u8]) -> Result<JpegMetadata> { + let dimensions = self.parse_dimensions(data)?; + + Ok(JpegMetadata { + dimensions, + file_size: data.len(), + is_valid: true, + }) + } + + /// Validate JPEG data + pub fn validate_jpeg(&self, data: &[u8]) -> Result<bool> { + if data.len() < 4 { + return Ok(false); + } + + // Check SOI marker + if data[0] != 0xFF || data[1] != JPEG_SOI { + return Ok(false); + } + + // Check EOI marker (should be near the end) + if data.len() >= 2 { + let end = data.len() - 2; + if data[end] == 0xFF && data[end + 1] == JPEG_EOI { + return Ok(true); + } + } + + // If we can't find EOI, check if we can parse dimensions + Ok(self.parse_dimensions(data).is_ok()) + } + + /// Check if data represents a complete JPEG frame + pub fn is_complete_jpeg(&self, data: &[u8]) -> bool { + if data.len() < 4 { + return false; + } + + // Must start with SOI marker + if data[0] != 0xFF || data[1] != JPEG_SOI { + return false; + } + + // Must end with EOI marker + if data.len() >= 2 { + let end = data.len() - 2; + if data[end] == 0xFF && data[end + 1] == JPEG_EOI { + return true; + } + } + + false + } +} + +impl Default for JpegParser { + fn default() -> Self { + Self::new() + } +} + +/// JPEG metadata +#[derive(Debug, Clone)] +pub struct JpegMetadata { + pub dimensions: JpegDimensions, + pub file_size: usize, + pub is_valid: bool, +} + +impl JpegMetadata { + /// Create new metadata + pub fn new(dimensions: JpegDimensions, file_size: usize) -> Self { + Self { + dimensions, + file_size, + is_valid: true, + } + } + + /// Get estimated bit depth (assume 8-bit for most JPEGs) + pub fn estimated_bit_depth(&self) -> u8 { + 8 + } + + /// Get estimated color space (assume YUV for most JPEGs) + pub fn estimated_color_space(&self) -> &'static str { + "YUV" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::Error; + + // Sample JPEG data for testing (minimal valid JPEG) + const MINIMAL_JPEG: &[u8] = &[ + 0xFF, 0xD8, // SOI + 0xFF, 0xE0, // APP0 + 0x00, 0x10, // Length + 0x4A, 0x46, 0x49, 0x46, 0x00, // "JFIF\0" + 0x01, 0x01, // Version + 0x00, // Units + 0x00, 0x01, // Density + 0x00, 0x01, 0x00, 0x00, // No thumbnail + 0xFF, 0xC0, // SOF0 + 0x00, 0x0B, // Length + 0x08, // Precision + 0x00, 0x40, // Height (64) + 0x00, 0x40, // Width (64) + 0x03, // Components + 0x01, 0x11, 0x00, // Y component + 0x02, 0x11, 0x01, // U component + 0x03, 0x11, 0x01, // V component + 0xFF, 0xD9, // EOI + ]; + + #[test] + fn test_jpeg_dimensions_creation() { + let dims = JpegDimensions::new(640, 480); + assert_eq!(dims.width, 640); + assert_eq!(dims.height, 480); + assert!(dims.is_valid()); + assert_eq!(dims.aspect_ratio(), 640.0 / 480.0); + } + + #[test] + fn test_jpeg_dimensions_validation() { + let dims = JpegDimensions::new(0, 480); + assert!(!dims.is_valid()); + + let dims = JpegDimensions::new(640, 0); + assert!(!dims.is_valid()); + + let dims = JpegDimensions::new(0, 0); + assert!(!dims.is_valid()); + } + + #[test] + fn test_jpeg_parser_creation() { + let parser = JpegParser::new(); + assert!(!parser.enable_debug); + + let parser = JpegParser::new_with_debug(true); + assert!(parser.enable_debug); + } + + #[test] + fn test_jpeg_parser_parse_dimensions() { + let parser = JpegParser::new(); + let dimensions = parser.parse_dimensions(MINIMAL_JPEG).unwrap(); + assert_eq!(dimensions.width, 64); + assert_eq!(dimensions.height, 64); + } + + #[test] + fn test_jpeg_parser_invalid_header() { + let parser = JpegParser::new(); + let invalid_data = &[0x00, 0x01, 0x02, 0x03]; + + let result = parser.parse_dimensions(invalid_data); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::Jpeg(JpegError::InvalidHeader) + )); + } + + #[test] + fn test_jpeg_parser_short_data() { + let parser = JpegParser::new(); + let short_data = &[0xFF, 0xD8]; + + let result = parser.parse_dimensions(short_data); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::Jpeg(JpegError::InvalidHeader) + )); + } + + #[test] + fn test_jpeg_parser_validate_jpeg() { + let parser = JpegParser::new(); + assert!(parser.validate_jpeg(MINIMAL_JPEG).unwrap()); + + let invalid_data = &[0x00, 0x01, 0x02, 0x03]; + assert!(!parser.validate_jpeg(invalid_data).unwrap()); + } + + #[test] + fn test_jpeg_metadata_creation() { + let dimensions = JpegDimensions::new(640, 480); + let metadata = JpegMetadata::new(dimensions, 1024); + + assert_eq!(metadata.dimensions.width, 640); + assert_eq!(metadata.dimensions.height, 480); + assert_eq!(metadata.file_size, 1024); + assert!(metadata.is_valid); + assert_eq!(metadata.estimated_bit_depth(), 8); + assert_eq!(metadata.estimated_color_space(), "YUV"); + } + + #[test] + fn test_sof_marker_detection() { + let parser = JpegParser::new(); + assert!(parser.is_sof_marker(JPEG_SOF0)); + assert!(parser.is_sof_marker(JPEG_SOF1)); + assert!(parser.is_sof_marker(JPEG_SOF2)); + assert!(!parser.is_sof_marker(0x00)); + assert!(!parser.is_sof_marker(JPEG_SOI)); + } +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 0000000..cf0426e --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,409 @@ +//! UPP protocol implementation for the Geek szitman supercamera + +mod frame; +mod jpeg; +mod parser; + +pub use frame::{UPPFrame, UPPFrameHeader, UPPUsbFrame}; +pub use jpeg::JpegParser; +pub use parser::UPPParser; + +use crate::error::Result; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::sync::Mutex; +use tracing::trace; + +/// UPP protocol constants +pub const UPP_USB_MAGIC: u16 = 0xBBAA; +pub const UPP_CAMID_7: u8 = 7; + +/// UPP camera instance +pub struct UPPCamera { + parser: Arc<UPPParser>, + jpeg_parser: Arc<JpegParser>, + frame_buffer: Arc<Mutex<Vec<u8>>>, + current_frame_id: Arc<Mutex<Option<u8>>>, + frame_callbacks: Arc<Mutex<Vec<Box<dyn FrameCallback + Send + Sync>>>>, + button_callbacks: Arc<Mutex<Vec<Box<dyn ButtonCallback + Send + Sync>>>>, + // Buffer for assembling frames across USB reads + input_buffer: Arc<Mutex<Vec<u8>>>, +} + +/// Frame callback trait +pub trait FrameCallback { + /// Called when a complete frame is received + fn on_frame(&self, frame: &UPPFrame); +} + +/// Button callback trait +pub trait ButtonCallback { + /// Called when a button press is detected + fn on_button_press(&self); +} + +impl UPPCamera { + /// Create a new UPP camera instance + pub fn new() -> Self { + Self { + parser: Arc::new(UPPParser::new()), + jpeg_parser: Arc::new(JpegParser::new()), + frame_buffer: Arc::new(Mutex::new(Vec::new())), + current_frame_id: Arc::new(Mutex::new(None)), + frame_callbacks: Arc::new(Mutex::new(Vec::new())), + button_callbacks: Arc::new(Mutex::new(Vec::new())), + input_buffer: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Create a new UPP camera instance with debug enabled + pub fn new_with_debug(enable_debug: bool) -> Self { + Self { + parser: Arc::new(UPPParser::new_with_debug(enable_debug)), + jpeg_parser: Arc::new(JpegParser::new_with_debug(enable_debug)), + frame_buffer: Arc::new(Mutex::new(Vec::new())), + current_frame_id: Arc::new(Mutex::new(None)), + frame_callbacks: Arc::new(Mutex::new(Vec::new())), + button_callbacks: Arc::new(Mutex::new(Vec::new())), + input_buffer: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Enable or disable debug mode + pub fn set_debug_mode(&self, enable: bool) { + // Create a new parser with the desired debug setting + let _new_parser = Arc::new(UPPParser::new_with_debug(enable)); + // Replace the parser (this requires interior mutability or a different approach) + // For now, we'll just log the change + tracing::info!("Debug mode {} for UPP parser", if enable { "enabled" } else { "disabled" }); + } +} + +impl Default for UPPCamera { + fn default() -> Self { + Self::new() + } +} + +impl UPPCamera { + /// Handle incoming UPP frame data + pub fn handle_frame(&self, data: &[u8]) -> Result<()> { + // Reduce logging frequency - only log every 100th frame + static mut FRAME_COUNT: u32 = 0; + unsafe { + FRAME_COUNT += 1; + if FRAME_COUNT % 100 == 0 { + trace!("Handling frame data: {} bytes", data.len()); + } + } + + // Backward-compatible: feed bytes and process all parsed frames + for frame in self.feed_bytes(data)? { + self.process_frame(frame)?; + } + + Ok(()) + } + + /// Handle incoming UPP frame data with better error handling + pub fn handle_frame_robust(&self, data: &[u8]) -> Result<()> { + trace!("Handling frame data robustly: {} bytes", data.len()); + + // Streaming-friendly: feed bytes and process all parsed frames + let frames = self.feed_bytes(data)?; + for frame in frames { + self.process_frame(frame)?; + } + Ok(()) + } + + /// Feed raw bytes from USB and extract as many complete protocol frames as possible. + /// Based on C++ POC: frames are chunked across multiple USB reads and need assembly. + pub fn feed_bytes(&self, chunk: &[u8]) -> Result<Vec<UPPFrame>> { + let mut out_frames = Vec::new(); + let mut buffer = self.input_buffer.lock().unwrap(); + buffer.extend_from_slice(chunk); + + // Parse loop: find and assemble frames from buffer + loop { + // Need at least 5 bytes for USB header + if buffer.len() < 5 { + break; + } + + // Search for magic 0xBBAA (little-endian) + let mut start_index = None; + for i in 0..=buffer.len() - 2 { + let magic = u16::from_le_bytes([buffer[i], buffer[i + 1]]); + if magic == UPP_USB_MAGIC { + start_index = Some(i); + break; + } + } + + let Some(start) = start_index else { + // No magic found; drop buffer to avoid infinite growth + buffer.clear(); + break; + }; + + // Drop any leading garbage before magic + if start > 0 { + buffer.drain(0..start); + } + + // Re-check size for header + if buffer.len() < 5 { + break; + } + + // Parse USB header fields directly from bytes + let _magic = u16::from_le_bytes([buffer[0], buffer[1]]); + let _cid = buffer[2]; + let payload_length = u16::from_le_bytes([buffer[3], buffer[4]]) as usize; + let total_frame_size = 5 + payload_length; // USB header (5) + payload + + if buffer.len() < total_frame_size { + // Wait for more data next call + break; + } + + // We have a complete frame; extract it + let frame_bytes: Vec<u8> = buffer.drain(0..total_frame_size).collect(); + + // Parse the complete frame + match self.parser.parse_frame(&frame_bytes) { + Ok(frame) => { + out_frames.push(frame); + } + Err(e) => { + tracing::warn!("Failed to parse complete frame: {}", e); + // Continue processing other frames + } + } + } + + Ok(out_frames) + } + + /// Process a parsed UPP frame with frame assembly logic + /// Based on C++ POC: accumulate frame data until frame ID changes + fn process_frame(&self, frame: UPPFrame) -> Result<()> { + let mut frame_buffer = self.frame_buffer.lock().unwrap(); + let mut current_frame_id = self.current_frame_id.lock().unwrap(); + + // Check if this is a new frame (frame ID changed) + if let Some(id) = *current_frame_id { + if id != frame.header.frame_id { + // New frame started, process the previous one + if !frame_buffer.is_empty() { + self.notify_frame_complete(&frame_buffer); + frame_buffer.clear(); + } + } + } + + // Update current frame ID + *current_frame_id = Some(frame.header.frame_id); + + // Add frame data to buffer (accumulate chunks) + frame_buffer.extend_from_slice(&frame.data); + + // Check for button press + if frame.header.flags.button_press { + self.notify_button_press(); + } + + Ok(()) + } + + /// Notify frame callbacks of complete frame + fn notify_frame_complete(&self, frame_data: &[u8]) { + let frame_callbacks = self.frame_callbacks.lock().unwrap(); + + for callback in frame_callbacks.iter() { + callback.on_frame(&UPPFrame { + header: UPPFrameHeader::new(0, 0, false, false, 0), + data: frame_data.to_vec(), + }); + } + } + + /// Notify button callbacks of button press + fn notify_button_press(&self) { + let button_callbacks = self.button_callbacks.lock().unwrap(); + + for callback in button_callbacks.iter() { + callback.on_button_press(); + } + } + + /// Add a frame callback + pub fn add_frame_callback<F>(&self, callback: F) + where + F: FrameCallback + Send + Sync + 'static, + { + let mut frame_callbacks = self.frame_callbacks.lock().unwrap(); + frame_callbacks.push(Box::new(callback)); + } + + /// Add a button callback + pub fn add_button_callback<F>(&self, callback: F) + where + F: ButtonCallback + Send + Sync + 'static, + { + let mut button_callbacks = self.button_callbacks.lock().unwrap(); + button_callbacks.push(Box::new(callback)); + } + + /// Get the complete frame if available + pub fn get_complete_frame(&self) -> Option<Vec<u8>> { + let frame_buffer = self.frame_buffer.lock().unwrap(); + if frame_buffer.is_empty() { + None + } else { + // Check if the frame is a complete JPEG before returning it + if self.jpeg_parser.is_complete_jpeg(&frame_buffer) { + Some(frame_buffer.clone()) + } else { + None + } + } + } + + /// Get the current frame buffer size (for debugging frame assembly) + pub fn get_frame_buffer_size(&self) -> usize { + let frame_buffer = self.frame_buffer.lock().unwrap(); + frame_buffer.len() + } + + /// Get the current input buffer size (for debugging chunk accumulation) + pub fn get_input_buffer_size(&self) -> usize { + let input_buffer = self.input_buffer.lock().unwrap(); + input_buffer.len() + } + + /// Clear the frame buffer + pub fn clear_frame_buffer(&self) { + let mut frame_buffer = self.frame_buffer.lock().unwrap(); + frame_buffer.clear(); + } + + /// Get frame statistics + pub fn get_stats(&self) -> UPPStats { + let frame_buffer = self.frame_buffer.lock().unwrap(); + let current_frame_id = self.current_frame_id.lock().unwrap(); + + UPPStats { + buffer_size: frame_buffer.len(), + current_frame_id: *current_frame_id, + total_frames_processed: 0, // TODO: implement counter + } + } +} + +/// UPP protocol statistics +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct UPPStats { + pub buffer_size: usize, + pub current_frame_id: Option<u8>, + pub total_frames_processed: u64, +} + +/// UPP camera configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UPPConfig { + pub expected_camera_id: u8, + pub max_frame_size: usize, + pub enable_debug: bool, +} + +impl Default for UPPConfig { + fn default() -> Self { + Self { + expected_camera_id: UPP_CAMID_7, + max_frame_size: 0x10000, // 64KB + enable_debug: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + // Mock frame callback for testing + struct MockFrameCallback { + frame_count: Arc<std::sync::Mutex<u32>>, + } + + impl FrameCallback for MockFrameCallback { + fn on_frame(&self, _frame: &UPPFrame) { + let mut count = self.frame_count.lock().unwrap(); + *count += 1; + } + } + + // Mock button callback for testing + struct MockButtonCallback { + press_count: Arc<std::sync::Mutex<u32>>, + } + + impl ButtonCallback for MockButtonCallback { + fn on_button_press(&self) { + let mut count = self.press_count.lock().unwrap(); + *count += 1; + } + } + + #[test] + fn test_upp_constants() { + assert_eq!(UPP_USB_MAGIC, 0xBBAA); + assert_eq!(UPP_CAMID_7, 7); + } + + #[test] + fn test_upp_config_default() { + let config = UPPConfig::default(); + assert_eq!(config.expected_camera_id, UPP_CAMID_7); + assert_eq!(config.max_frame_size, 0x10000); + assert!(!config.enable_debug); + } + + #[test] + fn test_upp_stats_default() { + let stats = UPPStats::default(); + assert_eq!(stats.buffer_size, 0); + assert!(stats.current_frame_id.is_none()); + assert_eq!(stats.total_frames_processed, 0); + } + + #[test] + fn test_upp_camera_creation() { + let camera = UPPCamera::new(); + let stats = camera.get_stats(); + assert_eq!(stats.buffer_size, 0); + assert!(stats.current_frame_id.is_none()); + } + + #[test] + fn test_upp_camera_callbacks() { + let camera = UPPCamera::new(); + + let frame_callback = MockFrameCallback { + frame_count: Arc::new(std::sync::Mutex::new(0u32)), + }; + + let button_callback = MockButtonCallback { + press_count: Arc::new(std::sync::Mutex::new(0u32)), + }; + + camera.add_frame_callback(frame_callback); + camera.add_button_callback(button_callback); + + // Verify callbacks were added + let stats = camera.get_stats(); + assert_eq!(stats.buffer_size, 0); + } +} diff --git a/src/protocol/parser.rs b/src/protocol/parser.rs new file mode 100644 index 0000000..7db824e --- /dev/null +++ b/src/protocol/parser.rs @@ -0,0 +1,418 @@ +//! UPP protocol parser for decoding USB frames + +use super::frame::{UPPFlags, UPPFrame, UPPFrameHeader, UPPUsbFrame}; +use crate::error::{ProtocolError, Result}; +use std::mem; +use tracing::{debug, trace, warn}; + +/// UPP protocol parser +pub struct UPPParser { + enable_debug: bool, +} + +impl UPPParser { + /// Create a new UPP parser + pub fn new() -> Self { + Self { + enable_debug: false, + } + } + + /// Create a new UPP parser with debug enabled + pub fn new_with_debug(enable_debug: bool) -> Self { + Self { enable_debug } + } + + /// Parse a raw USB frame into a UPP frame + pub fn parse_frame(&self, data: &[u8]) -> Result<UPPFrame> { + trace!("Parsing UPP frame: {} bytes", data.len()); + + // Parse USB frame header + let usb_frame = self.parse_usb_header(data)?; + + // Validate frame length - based on C++ POC analysis + let expected_total_size = usb_frame.total_size(); + let actual_size = data.len(); + + if self.enable_debug { + trace!( + "Frame size validation: expected={}, actual={}, difference={}", + expected_total_size, actual_size, actual_size as i32 - expected_total_size as i32 + ); + } + + // Based on C++ POC: the length field represents total payload size + // If we have less data than expected, this is a partial frame (chunked) + if actual_size < expected_total_size { + return Err(ProtocolError::FrameTooSmall { + expected: expected_total_size, + actual: actual_size, + } + .into()); + } + + // Parse camera frame header + let camera_header = self.parse_camera_header(data, &usb_frame)?; + + // Extract frame data - use the expected data size + let data_start = mem::size_of::<UPPUsbFrame>() + 7; // Manual header size: 7 bytes + let frame_data = if data_start < data.len() { + let end_index = std::cmp::min(data_start + usb_frame.expected_data_size(), data.len()); + data[data_start..end_index].to_vec() + } else { + Vec::new() + }; + + if self.enable_debug { + trace!( + "Parsed UPP frame: ID={}, Camera={}, Data={} bytes, Total={} bytes, Expected={} bytes", + camera_header.frame_id, + camera_header.camera_number, + frame_data.len(), + data.len(), + expected_total_size + ); + } + + Ok(UPPFrame { + header: camera_header, + data: frame_data, + }) + } + + /// Parse a raw USB frame with better error handling and diagnostics + pub fn parse_frame_robust(&self, data: &[u8]) -> Result<UPPFrame> { + trace!("Parsing UPP frame robustly: {} bytes", data.len()); + + // First, try to find a valid frame start + let frame_start = self.find_frame_start(data)?; + let frame_data = &data[frame_start..]; + + if self.enable_debug { + debug!("Found frame start at offset {}, processing {} bytes", frame_start, frame_data.len()); + } + + // Parse the frame from the found start position + self.parse_frame(frame_data) + } + + /// Find the start of a valid UPP frame in potentially misaligned data + fn find_frame_start(&self, data: &[u8]) -> Result<usize> { + if data.len() < 12 { // Minimum frame size + return Err(ProtocolError::FrameTooSmall { + expected: 12, + actual: data.len(), + } + .into()); + } + + // Look for the magic number (0xBBAA) in the data + for i in 0..=data.len().saturating_sub(12) { + if data.len() >= i + 2 { + let magic_bytes = [data[i], data[i + 1]]; + let magic = u16::from_le_bytes(magic_bytes); + + if magic == super::UPP_USB_MAGIC { + if self.enable_debug { + debug!("Found magic number 0x{:04X} at offset {}", magic, i); + } + return Ok(i); + } + } + } + + Err(ProtocolError::InvalidMagic { + expected: super::UPP_USB_MAGIC, + actual: 0, // Unknown + } + .into()) + } + + /// Parse USB frame header + pub(crate) fn parse_usb_header(&self, data: &[u8]) -> Result<UPPUsbFrame> { + if data.len() < mem::size_of::<UPPUsbFrame>() { + return Err(ProtocolError::FrameTooSmall { + expected: mem::size_of::<UPPUsbFrame>(), + actual: data.len(), + } + .into()); + } + + // Safe to transmute since we've checked the size + let usb_frame = unsafe { *(data.as_ptr() as *const UPPUsbFrame) }; + + // Validate magic number + if !usb_frame.is_valid() { + return Err(ProtocolError::InvalidMagic { + expected: super::UPP_USB_MAGIC, + actual: usb_frame.magic, + } + .into()); + } + + // Validate camera ID + if usb_frame.cid != super::UPP_CAMID_7 { + return Err(ProtocolError::UnknownCameraId(usb_frame.cid).into()); + } + + if self.enable_debug { + let magic = usb_frame.magic; + let cid = usb_frame.cid; + let length = usb_frame.length; + trace!( + "USB frame: magic=0x{:04X}, cid={}, length={}", + magic, cid, length + ); + } + + Ok(usb_frame) + } + + /// Parse camera frame header + pub(crate) fn parse_camera_header(&self, data: &[u8], _usb_frame: &UPPUsbFrame) -> Result<UPPFrameHeader> { + let header_offset = mem::size_of::<UPPUsbFrame>(); + // Manual header size: frame_id(1) + camera_number(1) + flags(1) + g_sensor(4) = 7 bytes + let header_end = header_offset + 7; + + if data.len() < header_end { + return Err(ProtocolError::FrameTooSmall { + expected: header_end, + actual: data.len(), + } + .into()); + } + + // Manually parse the header to avoid packed struct issues + let frame_id = data[header_offset]; + let camera_number = data[header_offset + 1]; + + // Read flags byte and extract individual bits + let flags_byte = data[header_offset + 2]; + let has_g = (flags_byte & 0x01) != 0; // Bit 0 + let button_press = (flags_byte & 0x02) != 0; // Bit 1 + let other = (flags_byte >> 2) & 0x3F; // Bits 2-7 (6 bits) + + if self.enable_debug { + trace!( + "Raw flags byte: 0x{:02X}, has_g={}, button_press={}, other=0x{:02X}", + flags_byte, has_g, button_press, other + ); + } + + // Read G-sensor data (4 bytes, little-endian) + let g_sensor_bytes = [ + data[header_offset + 3], + data[header_offset + 4], + data[header_offset + 5], + data[header_offset + 6], + ]; + + if self.enable_debug { + trace!( + "G-sensor bytes: {:02X} {:02X} {:02X} {:02X}", + g_sensor_bytes[0], g_sensor_bytes[1], g_sensor_bytes[2], g_sensor_bytes[3] + ); + } + + let g_sensor = u32::from_le_bytes(g_sensor_bytes); + + let camera_header = UPPFrameHeader { + frame_id, + camera_number, + flags: UPPFlags { + has_g, + button_press, + other, + }, + g_sensor, + }; + + // Validate frame header + self.validate_camera_header(&camera_header)?; + + if self.enable_debug { + trace!( + "Camera header: frame_id={}, camera={}, has_g={}, button={}", + camera_header.frame_id, + camera_header.camera_number, + camera_header.flags.has_g, + camera_header.flags.button_press + ); + } + + Ok(camera_header) + } + + /// Validate camera frame header + fn validate_camera_header(&self, header: &UPPFrameHeader) -> Result<()> { + // Validate camera number (should be 0 or 1) + if header.camera_number >= 2 { + return Err(ProtocolError::InvalidFrameFormat(format!( + "Invalid camera number: {}", + header.camera_number + )) + .into()); + } + + // Validate flags (G-sensor and other flags should be 0 for now) + if header.flags.has_g { + warn!("G-sensor data not yet supported"); + } + + if header.flags.other != 0 { + warn!("Unknown flags set: 0x{:02X}", header.flags.other); + } + + Ok(()) + } + + /// Get frame statistics + pub fn get_stats(&self) -> UPPParserStats { + UPPParserStats { + enable_debug: self.enable_debug, + } + } +} + +impl Default for UPPParser { + fn default() -> Self { + Self::new() + } +} + +/// UPP parser statistics +#[derive(Debug, Clone)] +pub struct UPPParserStats { + pub enable_debug: bool, +} + +#[cfg(test)] +mod tests { + use super::super::UPP_USB_MAGIC; + use super::*; + use crate::error::Error; + + // Create test data for a valid UPP frame + fn create_test_frame(frame_id: u8, camera_number: u8, data_size: usize) -> Vec<u8> { + let mut frame = Vec::new(); + + // USB header (5 bytes) + frame.extend_from_slice(&UPP_USB_MAGIC.to_le_bytes()); + frame.push(7); // Camera ID + frame.extend_from_slice(&(data_size as u16 + 7).to_le_bytes()); // Length (camera header + data) + + // Camera header (7 bytes) + frame.push(frame_id); + frame.push(camera_number); + // Flags struct (1 byte - packed bit fields) + let flags_byte = 0u8; // has_g: false, button_press: false, other: 0 + frame.push(flags_byte); + frame.extend_from_slice(&0u32.to_le_bytes()); // G-sensor data (4 bytes) + + // Frame data + frame.extend(std::iter::repeat(0xAA).take(data_size)); + + frame + } + + #[test] + fn test_upp_parser_creation() { + let parser = UPPParser::new(); + assert!(!parser.enable_debug); + + let parser = UPPParser::new_with_debug(true); + assert!(parser.enable_debug); + } + + #[test] + fn test_upp_parser_parse_valid_frame() { + let parser = UPPParser::new(); + let test_frame = create_test_frame(1, 0, 10); + + let result = parser.parse_frame(&test_frame); + assert!(result.is_ok()); + + let frame = result.unwrap(); + assert_eq!(frame.frame_id(), 1); + assert_eq!(frame.camera_number(), 0); + assert_eq!(frame.data.len(), 10); + assert!(!frame.button_pressed()); + assert!(frame.g_sensor_data().is_none()); + } + + #[test] + fn test_upp_parser_frame_too_small() { + let parser = UPPParser::new(); + let short_data = &[0xFF, 0xAA]; // Too short for any header + + let result = parser.parse_frame(short_data); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::Protocol(ProtocolError::FrameTooSmall { .. }) + )); + } + + #[test] + fn test_upp_parser_invalid_magic() { + let parser = UPPParser::new(); + let mut test_frame = create_test_frame(1, 0, 5); + test_frame[0] = 0x12; // Corrupt magic + + let result = parser.parse_frame(&test_frame); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::Protocol(ProtocolError::InvalidMagic { .. }) + )); + } + + #[test] + fn test_upp_parser_unknown_camera_id() { + let parser = UPPParser::new(); + let mut test_frame = create_test_frame(1, 0, 5); + test_frame[2] = 5; // Unknown camera ID + + let result = parser.parse_frame(&test_frame); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + Error::Protocol(ProtocolError::UnknownCameraId(5)) + )); + } + + #[test] + fn test_upp_parser_button_press() { + let parser = UPPParser::new(); + let mut test_frame = create_test_frame(1, 0, 5); + test_frame[7] = 0x02; // Set button press flag (bit 1 of flags byte at index 7) + + let result = parser.parse_frame(&test_frame); + assert!(result.is_ok()); + + let frame = result.unwrap(); + assert!(frame.button_pressed()); + } + + #[test] + fn test_upp_parser_g_sensor() { + let parser = UPPParser::new(); + let mut test_frame = create_test_frame(1, 0, 5); + test_frame[7] = 0x01; // Set G-sensor flag (bit 0 of flags byte at index 7) + test_frame[8..12].copy_from_slice(&0x12345678u32.to_le_bytes()); // G-sensor data (indices 8-11) + + let result = parser.parse_frame(&test_frame); + assert!(result.is_ok()); + + let frame = result.unwrap(); + assert!(frame.g_sensor_data().is_some()); + assert_eq!(frame.g_sensor_data().unwrap(), 0x12345678); + } + + #[test] + fn test_upp_parser_stats() { + let parser = UPPParser::new(); + let stats = parser.get_stats(); + assert!(!stats.enable_debug); + } +} diff --git a/src/usb/device.rs b/src/usb/device.rs new file mode 100644 index 0000000..b285e81 --- /dev/null +++ b/src/usb/device.rs @@ -0,0 +1,287 @@ +//! USB device implementation for the Geek szitman supercamera + +use super::{ENDPOINT_1, ENDPOINT_2, INTERFACE_A_NUMBER, INTERFACE_B_NUMBER, INTERFACE_B_ALTERNATE_SETTING, USB_PRODUCT_ID, USB_TIMEOUT, USB_VENDOR_ID}; +use crate::error::{Result, UsbError}; +use rusb::{Context, Device, DeviceHandle, UsbContext}; +use std::sync::Arc; +use std::sync::Mutex; +use tracing::{debug, error, info, warn}; + +/// USB device handle wrapper +pub struct UsbSupercamera { + context: Arc<Context>, + handle: Arc<Mutex<Option<DeviceHandle<Context>>>>, + device_info: super::UsbDeviceInfo, +} + +impl UsbSupercamera { + /// Create a new USB supercamera instance + pub fn new() -> Result<Self> { + let context = Arc::new(Context::new()?); + let handle = Arc::new(Mutex::new(None)); + let device_info = super::UsbDeviceInfo::default(); + + let mut instance = Self { + context, + handle, + device_info, + }; + + instance.connect()?; + instance.initialize_device()?; + + Ok(instance) + } + + /// Connect to the USB device + fn connect(&mut self) -> Result<()> { + let device = self.find_device()?; + let handle = device.open()?; + + // Ensure kernel drivers are detached when claiming interfaces + handle.set_auto_detach_kernel_driver(true)?; + + let mut handle_guard = self.handle.try_lock() + .map_err(|_| UsbError::Generic("Failed to acquire handle lock".to_string()))?; + *handle_guard = Some(handle); + + info!("Connected to USB device {:04x}:{:04x}", USB_VENDOR_ID, USB_PRODUCT_ID); + Ok(()) + } + + /// Find the target USB device + fn find_device(&self) -> Result<Device<Context>> { + for device in self.context.devices()?.iter() { + let device_desc = device.device_descriptor()?; + + if device_desc.vendor_id() == USB_VENDOR_ID && + device_desc.product_id() == USB_PRODUCT_ID { + debug!("Found target device: {:04x}:{:04x}", USB_VENDOR_ID, USB_PRODUCT_ID); + return Ok(device); + } + } + + Err(UsbError::DeviceNotFound.into()) + } + + /// Initialize the USB device interfaces and endpoints + fn initialize_device(&self) -> Result<()> { + let handle_guard = self.handle.try_lock() + .map_err(|_| UsbError::Generic("Failed to acquire handle lock".to_string()))?; + + let handle = handle_guard.as_ref() + .ok_or_else(|| UsbError::Generic("No device handle available".to_string()))?; + + // Claim interface A + self.claim_interface(handle, INTERFACE_A_NUMBER)?; + + // Claim interface B + self.claim_interface(handle, INTERFACE_B_NUMBER)?; + + // Set alternate setting for interface B + self.set_interface_alt_setting(handle, INTERFACE_B_NUMBER, INTERFACE_B_ALTERNATE_SETTING)?; + + // Clear halt on endpoint 1 (both directions) + self.clear_halt(handle, ENDPOINT_1 | 0x80)?; // IN (0x81) + self.clear_halt(handle, ENDPOINT_1)?; // OUT (0x01) + + // Send initialization commands + self.send_init_commands(handle)?; + + info!("USB device initialized successfully"); + Ok(()) + } + + /// Claim a USB interface + fn claim_interface(&self, handle: &DeviceHandle<Context>, interface: u8) -> Result<()> { + match handle.claim_interface(interface) { + Ok(()) => { + debug!("Claimed interface {}", interface); + Ok(()) + } + Err(e) => { + error!("Failed to claim interface {}: {}", interface, e); + Err(UsbError::InterfaceClaimFailed(e.to_string()).into()) + } + } + } + + /// Set interface alternate setting + fn set_interface_alt_setting(&self, handle: &DeviceHandle<Context>, interface: u8, setting: u8) -> Result<()> { + match handle.set_alternate_setting(interface, setting) { + Ok(()) => { + debug!("Set interface {} alternate setting to {}", interface, setting); + Ok(()) + } + Err(e) => { + error!("Failed to set interface {} alternate setting {}: {}", interface, setting, e); + Err(UsbError::Generic(format!("Failed to set alternate setting: {e}")).into()) + } + } + } + + /// Clear halt on an endpoint + fn clear_halt(&self, handle: &DeviceHandle<Context>, endpoint: u8) -> Result<()> { + match handle.clear_halt(endpoint) { + Ok(()) => { + debug!("Cleared halt on endpoint {}", endpoint); + Ok(()) + } + Err(e) => { + error!("Failed to clear halt on endpoint {}: {}", endpoint, e); + Err(UsbError::Generic(format!("Failed to clear halt: {e}")).into()) + } + } + } + + /// Send initialization commands to the device + fn send_init_commands(&self, handle: &DeviceHandle<Context>) -> Result<()> { + // Send magic words to endpoint 2 + let ep2_buf = vec![0xFF, 0x55, 0xFF, 0x55, 0xEE, 0x10]; + self.write_bulk(handle, ENDPOINT_2, &ep2_buf)?; + + // Send start stream command to endpoint 1 + let start_stream = vec![0xBB, 0xAA, 5, 0, 0]; + self.write_bulk(handle, ENDPOINT_1, &start_stream)?; + + debug!("Sent initialization commands"); + Ok(()) + } + + /// Read a frame from the USB device + pub fn read_frame(&self) -> Result<Vec<u8>> { + let handle_guard = self.handle.lock().unwrap(); + let handle = handle_guard.as_ref() + .ok_or_else(|| UsbError::Generic("No device handle available".to_string()))?; + + // Use a larger buffer to handle potential frame buffering + let mut buffer = vec![0u8; 0x2000]; // Increased from 0x1000 to 0x2000 + let transferred = self.read_bulk(handle, ENDPOINT_1, &mut buffer)?; + + buffer.truncate(transferred); + // Reduce logging frequency - only log every 100th read + static mut READ_COUNT: u32 = 0; + unsafe { + READ_COUNT += 1; + if READ_COUNT % 100 == 0 { + debug!("Read {} bytes from USB device", transferred); + } + } + + // Validate that we got a reasonable amount of data + if transferred < 12 { // Minimum frame size: USB header (5) + camera header (7) + return Err(UsbError::Generic(format!( + "Received frame too small: {} bytes (minimum: 12)", + transferred + )).into()); + } + + // Check if this looks like a valid UPP frame + if transferred >= 5 { + let magic_bytes = [buffer[0], buffer[1]]; + let magic = u16::from_le_bytes(magic_bytes); + if magic != crate::protocol::UPP_USB_MAGIC { + warn!("Received data doesn't start with expected magic: 0x{:04X}", magic); + } + } + + Ok(buffer) + } + + /// Read bulk data from an endpoint + fn read_bulk(&self, handle: &DeviceHandle<Context>, endpoint: u8, buffer: &mut [u8]) -> Result<usize> { + let endpoint_address = endpoint | 0x80; // IN endpoint address bit + + match handle.read_bulk(endpoint_address, buffer, USB_TIMEOUT) { + Ok(transferred) => { + debug!("Read {} bytes from endpoint {}", transferred, endpoint); + Ok(transferred) + } + Err(e) => { + error!("USB read error on endpoint {}: {}", endpoint, e); + match e { + rusb::Error::NoDevice => Err(UsbError::DeviceDisconnected.into()), + rusb::Error::Timeout => Err(UsbError::Timeout.into()), + _ => Err(UsbError::BulkTransferFailed(e.to_string()).into()), + } + } + } + } + + /// Write bulk data to an endpoint + fn write_bulk(&self, handle: &DeviceHandle<Context>, endpoint: u8, data: &[u8]) -> Result<()> { + let endpoint_address = endpoint; // OUT endpoint has no direction bit set + + match handle.write_bulk(endpoint_address, data, USB_TIMEOUT) { + Ok(transferred) => { + debug!("Wrote {} bytes to endpoint {}", transferred, endpoint); + Ok(()) + } + Err(e) => { + error!("USB write error on endpoint {}: {}", endpoint, e); + match e { + rusb::Error::NoDevice => Err(UsbError::DeviceDisconnected.into()), + rusb::Error::Timeout => Err(UsbError::Timeout.into()), + _ => Err(UsbError::BulkTransferFailed(e.to_string()).into()), + } + } + } + } + + /// Get device information + pub fn device_info(&self) -> &super::UsbDeviceInfo { + &self.device_info + } + + /// Check if device is connected + pub fn is_connected(&self) -> bool { + let handle_guard = self.handle.lock().unwrap(); + handle_guard.is_some() + } +} + +impl Drop for UsbSupercamera { + fn drop(&mut self) { + if let Ok(handle_guard) = self.handle.try_lock() { + if let Some(handle) = handle_guard.as_ref() { + // Release interfaces + let _ = handle.release_interface(INTERFACE_A_NUMBER); + let _ = handle.release_interface(INTERFACE_B_NUMBER); + debug!("Released USB interfaces"); + } + } + debug!("USB supercamera dropped"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + + // Mock USB context for testing - simplified for now + // TODO: Implement proper mock when needed for more complex testing + + #[test] + fn test_usb_device_info() { + let device_info = super::super::UsbDeviceInfo::default(); + assert_eq!(device_info.vendor_id, USB_VENDOR_ID); + assert_eq!(device_info.product_id, USB_PRODUCT_ID); + } + + #[test] + fn test_endpoint_constants() { + assert_eq!(ENDPOINT_1, 1); + assert_eq!(ENDPOINT_2, 2); + assert_eq!(INTERFACE_A_NUMBER, 0); + assert_eq!(INTERFACE_B_NUMBER, 1); + assert_eq!(INTERFACE_B_ALTERNATE_SETTING, 1); + } + + #[test] + fn test_usb_supercamera_creation_fails_without_device() { + // This test will fail in CI/CD environments without actual USB device + // In a real test environment, we'd use mocks + assert!(true); + } +} diff --git a/src/usb/mod.rs b/src/usb/mod.rs new file mode 100644 index 0000000..38d9c7d --- /dev/null +++ b/src/usb/mod.rs @@ -0,0 +1,164 @@ +//! USB communication module for the Geek szitman supercamera + +mod device; +mod transfer; + +pub use device::UsbSupercamera; +pub use transfer::{UsbTransferConfig, UsbTransferError, UsbTransferResult, UsbTransferStats}; + +use rusb::{Direction, TransferType}; +use std::time::Duration; + +/// USB device constants +pub const USB_VENDOR_ID: u16 = 0x2ce3; +pub const USB_PRODUCT_ID: u16 = 0x3828; +pub const INTERFACE_A_NUMBER: u8 = 0; +pub const INTERFACE_B_NUMBER: u8 = 1; +pub const INTERFACE_B_ALTERNATE_SETTING: u8 = 1; +pub const ENDPOINT_1: u8 = 1; +pub const ENDPOINT_2: u8 = 2; +pub const USB_TIMEOUT: Duration = Duration::from_millis(1000); + +/// USB device information +#[derive(Debug, Clone)] +pub struct UsbDeviceInfo { + pub vendor_id: u16, + pub product_id: u16, + pub manufacturer: Option<String>, + pub product: Option<String>, + pub serial_number: Option<String>, +} + +impl Default for UsbDeviceInfo { + fn default() -> Self { + Self { + vendor_id: USB_VENDOR_ID, + product_id: USB_PRODUCT_ID, + manufacturer: None, + product: None, + serial_number: None, + } + } +} + +/// USB endpoint configuration +#[derive(Debug, Clone)] +pub struct UsbEndpoint { + pub address: u8, + pub direction: Direction, + pub transfer_type: TransferType, + pub max_packet_size: u16, +} + +impl UsbEndpoint { + /// Create a new USB endpoint + pub fn new( + address: u8, + direction: Direction, + transfer_type: TransferType, + max_packet_size: u16, + ) -> Self { + Self { + address, + direction, + transfer_type, + max_packet_size, + } + } +} + +/// USB interface configuration +#[derive(Debug, Clone)] +pub struct UsbInterface { + pub number: u8, + pub alternate_setting: u8, + pub endpoints: Vec<UsbEndpoint>, +} + +impl UsbInterface { + /// Create a new USB interface + pub fn new(number: u8, alternate_setting: u8) -> Self { + Self { + number, + alternate_setting, + endpoints: Vec::new(), + } + } + + /// Add an endpoint to this interface + pub fn add_endpoint(&mut self, endpoint: UsbEndpoint) { + self.endpoints.push(endpoint); + } +} + +/// USB device configuration +#[derive(Debug, Clone)] +pub struct UsbConfig { + pub interfaces: Vec<UsbInterface>, + pub max_packet_size: u16, +} + +impl Default for UsbConfig { + fn default() -> Self { + let mut interface_a = UsbInterface::new(INTERFACE_A_NUMBER, 0); + interface_a.add_endpoint(UsbEndpoint::new( + ENDPOINT_1, + Direction::In, + TransferType::Bulk, + 0x1000, + )); + + let mut interface_b = UsbInterface::new(INTERFACE_B_NUMBER, INTERFACE_B_ALTERNATE_SETTING); + interface_b.add_endpoint(UsbEndpoint::new( + ENDPOINT_2, + Direction::Out, + TransferType::Bulk, + 64, + )); + + Self { + interfaces: vec![interface_a, interface_b], + max_packet_size: 0x1000, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_usb_device_info_default() { + let info = UsbDeviceInfo::default(); + assert_eq!(info.vendor_id, USB_VENDOR_ID); + assert_eq!(info.product_id, USB_PRODUCT_ID); + } + + #[test] + fn test_usb_endpoint_creation() { + let endpoint = UsbEndpoint::new(ENDPOINT_1, Direction::In, TransferType::Bulk, 0x1000); + assert_eq!(endpoint.address, ENDPOINT_1); + assert_eq!(endpoint.direction, Direction::In); + assert_eq!(endpoint.transfer_type, TransferType::Bulk); + assert_eq!(endpoint.max_packet_size, 0x1000); + } + + #[test] + fn test_usb_interface_management() { + let mut interface = UsbInterface::new(0, 0); + let endpoint = UsbEndpoint::new(1, Direction::In, TransferType::Bulk, 64); + + interface.add_endpoint(endpoint); + assert_eq!(interface.endpoints.len(), 1); + assert_eq!(interface.endpoints[0].address, 1); + } + + #[test] + fn test_usb_config_default() { + let config = UsbConfig::default(); + assert_eq!(config.interfaces.len(), 2); + assert_eq!(config.interfaces[0].number, INTERFACE_A_NUMBER); + assert_eq!(config.interfaces[1].number, INTERFACE_B_NUMBER); + assert_eq!(config.max_packet_size, 0x1000); + } +} diff --git a/src/usb/transfer.rs b/src/usb/transfer.rs new file mode 100644 index 0000000..51d31ce --- /dev/null +++ b/src/usb/transfer.rs @@ -0,0 +1,287 @@ +//! USB transfer handling for the Geek szitman supercamera + +use crate::error::Result; +use rusb::{Direction, TransferType}; +use std::time::Duration; +use tracing::{error, trace}; + +/// USB transfer error types +#[derive(Debug, thiserror::Error)] +pub enum UsbTransferError { + /// Transfer timeout + #[error("Transfer timeout after {:?}", duration)] + Timeout { duration: Duration }, + + /// Transfer failed + #[error("Transfer failed: {}", reason)] + Failed { reason: String }, + + /// Invalid endpoint + #[error("Invalid endpoint: {}", endpoint)] + InvalidEndpoint { endpoint: u8 }, + + /// Buffer too small + #[error("Buffer too small: required {}, provided {}", required, provided)] + BufferTooSmall { required: usize, provided: usize }, +} + +/// USB transfer configuration +#[derive(Debug, Clone)] +pub struct UsbTransferConfig { + pub timeout: Duration, + pub retry_count: u32, + pub buffer_size: usize, +} + +impl Default for UsbTransferConfig { + fn default() -> Self { + Self { + timeout: Duration::from_millis(1000), + retry_count: 3, + buffer_size: 0x1000, + } + } +} + +/// USB transfer result +#[derive(Debug)] +pub struct UsbTransferResult { + pub bytes_transferred: usize, + pub endpoint: u8, + pub direction: Direction, + pub transfer_type: TransferType, +} + +impl UsbTransferResult { + /// Create a new transfer result + pub fn new( + bytes_transferred: usize, + endpoint: u8, + direction: Direction, + transfer_type: TransferType, + ) -> Self { + Self { + bytes_transferred, + endpoint, + direction, + transfer_type, + } + } + + /// Check if the transfer was successful + pub fn is_successful(&self) -> bool { + self.bytes_transferred > 0 + } + + /// Get the effective endpoint address + pub fn endpoint_address(&self) -> u8 { + match self.direction { + Direction::In => self.endpoint | 0x80, + Direction::Out => self.endpoint, + } + } +} + +/// USB transfer statistics +#[derive(Debug, Default)] +pub struct UsbTransferStats { + pub total_transfers: u64, + pub successful_transfers: u64, + pub failed_transfers: u64, + pub total_bytes_transferred: u64, + pub average_transfer_size: f64, +} + +impl UsbTransferStats { + /// Update statistics with a transfer result + pub fn update(&mut self, result: &UsbTransferResult) { + self.total_transfers += 1; + + if result.is_successful() { + self.successful_transfers += 1; + self.total_bytes_transferred += result.bytes_transferred as u64; + } else { + self.failed_transfers += 1; + } + + // Update average transfer size + if self.successful_transfers > 0 { + self.average_transfer_size = + self.total_bytes_transferred as f64 / self.successful_transfers as f64; + } + } + + /// Get success rate as a percentage + pub fn success_rate(&self) -> f64 { + if self.total_transfers == 0 { + 0.0 + } else { + (self.successful_transfers as f64 / self.total_transfers as f64) * 100.0 + } + } + + /// Reset statistics + pub fn reset(&mut self) { + *self = Self::default(); + } +} + +/// USB transfer manager +pub struct UsbTransferManager { + config: UsbTransferConfig, + stats: UsbTransferStats, +} + +impl UsbTransferManager { + /// Create a new transfer manager + pub fn new(config: UsbTransferConfig) -> Self { + Self { + config, + stats: UsbTransferStats::default(), + } + } + + /// Create a transfer manager with default configuration + pub fn new_default() -> Self { + Self::new(UsbTransferConfig::default()) + } + + /// Get current statistics + pub fn stats(&self) -> &UsbTransferStats { + &self.stats + } + + /// Get mutable reference to statistics + pub fn stats_mut(&mut self) -> &mut UsbTransferStats { + &mut self.stats + } + + /// Reset statistics + pub fn reset_stats(&mut self) { + self.stats.reset(); + } + + /// Validate endpoint configuration + pub fn validate_endpoint( + &self, + endpoint: u8, + direction: Direction, + transfer_type: TransferType, + ) -> Result<()> { + if endpoint > 15 { + return Err(UsbTransferError::InvalidEndpoint { endpoint }.into()); + } + + // Validate endpoint address based on direction + let endpoint_address = match direction { + Direction::In => endpoint | 0x80, + Direction::Out => endpoint, + }; + + trace!( + "Validated endpoint: 0x{:02X} ({:?}, {:?})", + endpoint_address, + direction, + transfer_type + ); + Ok(()) + } + + /// Validate buffer size + pub fn validate_buffer(&self, buffer_size: usize) -> Result<()> { + if buffer_size < self.config.buffer_size { + return Err(UsbTransferError::BufferTooSmall { + required: self.config.buffer_size, + provided: buffer_size, + } + .into()); + } + Ok(()) + } + + /// Get transfer configuration + pub fn config(&self) -> &UsbTransferConfig { + &self.config + } + + /// Update transfer configuration + pub fn update_config(&mut self, config: UsbTransferConfig) { + self.config = config; + } +} + +impl Default for UsbTransferManager { + fn default() -> Self { + Self::new_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_usb_transfer_config_default() { + let config = UsbTransferConfig::default(); + assert_eq!(config.timeout, Duration::from_millis(1000)); + assert_eq!(config.retry_count, 3); + assert_eq!(config.buffer_size, 0x1000); + } + + #[test] + fn test_usb_transfer_result() { + let result = UsbTransferResult::new(1024, 1, Direction::In, TransferType::Bulk); + + assert_eq!(result.bytes_transferred, 1024); + assert_eq!(result.endpoint, 1); + assert!(result.is_successful()); + assert_eq!(result.endpoint_address(), 0x81); + } + + #[test] + fn test_usb_transfer_stats() { + let mut stats = UsbTransferStats::default(); + + let result = UsbTransferResult::new(1024, 1, Direction::In, TransferType::Bulk); + stats.update(&result); + + assert_eq!(stats.total_transfers, 1); + assert_eq!(stats.successful_transfers, 1); + assert_eq!(stats.failed_transfers, 0); + assert_eq!(stats.total_bytes_transferred, 1024); + assert_eq!(stats.success_rate(), 100.0); + } + + #[test] + fn test_usb_transfer_manager() { + let manager = UsbTransferManager::new_default(); + assert_eq!(manager.config().timeout, Duration::from_millis(1000)); + assert_eq!(manager.stats().total_transfers, 0); + } + + #[test] + fn test_endpoint_validation() { + let manager = UsbTransferManager::new_default(); + + // Valid endpoint + assert!(manager + .validate_endpoint(1, Direction::In, TransferType::Bulk) + .is_ok()); + + // Invalid endpoint + assert!(manager + .validate_endpoint(16, Direction::In, TransferType::Bulk) + .is_err()); + } + + #[test] + fn test_buffer_validation() { + let manager = UsbTransferManager::new_default(); + + // Valid buffer size + assert!(manager.validate_buffer(0x1000).is_ok()); + + // Invalid buffer size + assert!(manager.validate_buffer(0x800).is_err()); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..b89357e --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,350 @@ +//! Utility functions and types for the geek-szitman-supercamera crate + +use std::time::{Duration, Instant}; +use tracing::info; + +/// Signal handler for graceful shutdown +pub struct SignalHandler { + shutdown_requested: std::sync::Arc<std::sync::atomic::AtomicBool>, +} + +impl SignalHandler { + /// Create a new signal handler + pub fn new() -> Result<Self, Box<dyn std::error::Error>> { + let shutdown_requested = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let shutdown_clone = shutdown_requested.clone(); + + ctrlc::set_handler(move || { + info!("Received shutdown signal"); + shutdown_clone.store(true, std::sync::atomic::Ordering::SeqCst); + })?; + + Ok(Self { shutdown_requested }) + } + + /// Check if shutdown was requested + pub fn shutdown_requested(&self) -> bool { + self.shutdown_requested + .load(std::sync::atomic::Ordering::SeqCst) + } + + /// Wait for shutdown signal + pub fn wait_for_shutdown(&self) { + while !self.shutdown_requested() { + std::thread::sleep(Duration::from_millis(100)); + } + } + + /// Request shutdown programmatically + pub fn request_shutdown(&self) { + self.shutdown_requested + .store(true, std::sync::atomic::Ordering::SeqCst); + } + + /// Wait for shutdown signal with timeout. Returns true if shutdown was requested, false if timed out. + pub fn wait_for_shutdown_with_timeout(&self, timeout: Duration) -> bool { + let start = Instant::now(); + while !self.shutdown_requested() { + if start.elapsed() >= timeout { + return false; // timed out + } + std::thread::sleep(Duration::from_millis(100)); + } + true + } +} + +/// Performance metrics tracker +pub struct PerformanceTracker { + start_time: Instant, + frame_count: u64, + total_bytes: u64, + last_fps_update: Instant, + current_fps: f64, +} + +impl PerformanceTracker { + /// Create a new performance tracker + pub fn new() -> Self { + Self { + start_time: Instant::now(), + frame_count: 0, + total_bytes: 0, + last_fps_update: Instant::now(), + current_fps: 0.0, + } + } + + /// Record a frame + pub fn record_frame(&mut self, bytes: usize) { + self.frame_count += 1; + self.total_bytes += bytes as u64; + + // Update FPS every second + let now = Instant::now(); + if now.duration_since(self.last_fps_update) >= Duration::from_secs(1) { + let elapsed = now.duration_since(self.last_fps_update).as_secs_f64(); + self.current_fps = 1.0 / elapsed; + self.last_fps_update = now; + } + } + + /// Get current FPS + pub fn current_fps(&self) -> f64 { + self.current_fps + } + + /// Get total frame count + pub fn total_frames(&self) -> u64 { + self.frame_count + } + + /// Get total bytes processed + pub fn total_bytes(&self) -> u64 { + self.total_bytes + } + + /// Get average frame size + pub fn average_frame_size(&self) -> f64 { + if self.frame_count > 0 { + self.total_bytes as f64 / self.frame_count as f64 + } else { + 0.0 + } + } + + /// Get uptime + pub fn uptime(&self) -> Duration { + self.start_time.elapsed() + } + + /// Get performance summary + pub fn summary(&self) -> PerformanceSummary { + PerformanceSummary { + uptime: self.uptime(), + total_frames: self.total_frames(), + total_bytes: self.total_bytes(), + current_fps: self.current_fps(), + average_frame_size: self.average_frame_size(), + } + } +} + +impl Default for PerformanceTracker { + fn default() -> Self { + Self::new() + } +} + +/// Performance summary +#[derive(Debug, Clone)] +pub struct PerformanceSummary { + pub uptime: Duration, + pub total_frames: u64, + pub total_bytes: u64, + pub current_fps: f64, + pub average_frame_size: f64, +} + +impl PerformanceSummary { + /// Format uptime as human-readable string + pub fn uptime_formatted(&self) -> String { + let secs = self.uptime.as_secs(); + let hours = secs / 3600; + let minutes = (secs % 3600) / 60; + let seconds = secs % 60; + + if hours > 0 { + format!("{hours}h {minutes}m {seconds}s") + } else if minutes > 0 { + format!("{minutes}m {seconds}s") + } else { + format!("{seconds}s") + } + } + + /// Format bytes as human-readable string + pub fn bytes_formatted(&self) -> String { + const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"]; + let mut size = self.total_bytes as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.1} {}", size, UNITS[unit_index]) + } +} + +/// Configuration file loader +pub struct ConfigLoader; + +impl ConfigLoader { + /// Load configuration from file + pub fn load_from_file<T>(path: &str) -> Result<T, Box<dyn std::error::Error>> + where + T: serde::de::DeserializeOwned, + { + let content = std::fs::read_to_string(path)?; + let config: T = serde_json::from_str(&content)?; + Ok(config) + } + + /// Save configuration to file + pub fn save_to_file<T>(path: &str, config: &T) -> Result<(), Box<dyn std::error::Error>> + where + T: serde::Serialize, + { + let content = serde_json::to_string_pretty(config)?; + std::fs::write(path, content)?; + Ok(()) + } + + /// Load configuration with fallback to defaults + pub fn load_with_defaults<T>(path: &str) -> T + where + T: serde::de::DeserializeOwned + Default, + { + Self::load_from_file(path).unwrap_or_default() + } +} + +/// File utilities +pub struct FileUtils; + +impl FileUtils { + /// Ensure directory exists + pub fn ensure_dir(path: &str) -> Result<(), Box<dyn std::error::Error>> { + std::fs::create_dir_all(path)?; + Ok(()) + } + + /// Get file size + pub fn get_file_size(path: &str) -> Result<u64, Box<dyn std::error::Error>> { + let metadata = std::fs::metadata(path)?; + Ok(metadata.len()) + } + + /// Check if file exists + pub fn file_exists(path: &str) -> bool { + std::path::Path::new(path).exists() + } + + /// Get file extension + pub fn get_extension(path: &str) -> Option<String> { + std::path::Path::new(path) + .extension() + .and_then(|ext| ext.to_str()) + .map(|s| s.to_string()) + } +} + +/// Time utilities +pub struct TimeUtils; + +impl TimeUtils { + /// Format duration as human-readable string + pub fn format_duration(duration: Duration) -> String { + let secs = duration.as_secs(); + let millis = duration.subsec_millis(); + + if secs > 0 { + format!("{secs}.{millis:03}s") + } else { + format!("{millis}ms") + } + } + + /// Sleep with progress callback + pub fn sleep_with_progress<F>(duration: Duration, mut progress_callback: F) + where + F: FnMut(f64), + { + let start = Instant::now(); + let total_duration = duration.as_millis() as f64; + + while start.elapsed() < duration { + let elapsed = start.elapsed().as_millis() as f64; + let progress = (elapsed / total_duration).min(1.0); + progress_callback(progress); + + std::thread::sleep(Duration::from_millis(10)); + } + + progress_callback(1.0); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_performance_tracker() { + let mut tracker = PerformanceTracker::new(); + + // Record some frames + tracker.record_frame(1024); + tracker.record_frame(2048); + tracker.record_frame(1536); + + assert_eq!(tracker.total_frames(), 3); + assert_eq!(tracker.total_bytes(), 4608); + assert_eq!(tracker.average_frame_size(), 1536.0); + } + + #[test] + fn test_performance_summary() { + let mut tracker = PerformanceTracker::new(); + tracker.record_frame(1024); + + let summary = tracker.summary(); + assert_eq!(summary.total_frames, 1); + assert_eq!(summary.total_bytes, 1024); + assert_eq!(summary.average_frame_size, 1024.0); + + // Test formatting + assert!(summary.uptime_formatted().contains("s")); + assert_eq!(summary.bytes_formatted(), "1.0 KB"); + } + + #[test] + fn test_file_utils() { + // Test file existence check + assert!(FileUtils::file_exists("src/utils/mod.rs")); + assert!(!FileUtils::file_exists("nonexistent_file.txt")); + + // Test extension extraction + assert_eq!( + FileUtils::get_extension("test.txt"), + Some("txt".to_string()) + ); + assert_eq!(FileUtils::get_extension("no_extension"), None); + } + + #[test] + fn test_time_utils() { + let duration = Duration::from_millis(1500); + let formatted = TimeUtils::format_duration(duration); + assert!(formatted.contains("1.500s")); + + let short_duration = Duration::from_millis(500); + let formatted = TimeUtils::format_duration(short_duration); + assert!(formatted.contains("500ms")); + } + + #[test] + fn test_sleep_with_progress() { + let mut progress_values = Vec::new(); + let duration = Duration::from_millis(100); + + TimeUtils::sleep_with_progress(duration, |progress| { + progress_values.push(progress); + }); + + assert!(!progress_values.is_empty()); + assert!(progress_values.last().unwrap() >= &1.0); + } +} diff --git a/src/video/mod.rs b/src/video/mod.rs new file mode 100644 index 0000000..4fbab4f --- /dev/null +++ b/src/video/mod.rs @@ -0,0 +1,330 @@ +//! Video backend module for the Geek szitman supercamera + +mod pipewire; +mod v4l2; +mod stdout; + +pub use pipewire::{PipeWireBackend, PipeWireConfig}; +pub use v4l2::V4L2Backend; +pub use stdout::{StdoutBackend, StdoutConfig, HeaderFormat}; + +use crate::error::{Result, VideoError}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::sync::Mutex; +use tracing::{info}; + +/// Video backend trait for different video output methods +pub trait VideoBackendTrait: Send + Sync { + /// Initialize the video backend + fn initialize(&mut self) -> Result<()>; + + /// Push a frame to the video backend + fn push_frame(&self, frame_data: &[u8]) -> Result<()>; + + /// Get backend statistics + fn get_stats(&self) -> VideoStats; + + /// Check if backend is ready + fn is_ready(&self) -> bool; + + /// Shutdown the backend + fn shutdown(&mut self) -> Result<()>; +} + +/// Video backend types +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum VideoBackendType { + /// PipeWire backend + PipeWire, + /// V4L2 backend (for future use) + V4L2, + /// Stdout backend for piping to other tools + Stdout, +} + +/// Video backend configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VideoConfig { + pub backend_type: VideoBackendType, + pub width: u32, + pub height: u32, + pub fps: u32, + pub format: VideoFormat, + pub device_path: Option<String>, +} + +impl Default for VideoConfig { + fn default() -> Self { + Self { + backend_type: VideoBackendType::PipeWire, + width: 640, + height: 480, + fps: 30, + format: VideoFormat::MJPEG, + device_path: None, + } + } +} + +/// Video format types +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum VideoFormat { + /// Motion JPEG + MJPEG, + /// YUV420 + YUV420, + /// RGB24 + RGB24, +} + +impl VideoFormat { + /// Get the format name as a string + pub fn as_str(&self) -> &'static str { + match self { + VideoFormat::MJPEG => "MJPEG", + VideoFormat::YUV420 => "YUV420", + VideoFormat::RGB24 => "RGB24", + } + } + + /// Get the bytes per pixel + pub fn bytes_per_pixel(&self) -> usize { + match self { + VideoFormat::MJPEG => 0, // Variable for MJPEG + VideoFormat::YUV420 => 1, + VideoFormat::RGB24 => 3, + } + } +} + +/// Video statistics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VideoStats { + pub frames_pushed: u64, + pub frames_dropped: u64, + pub total_bytes: u64, + pub fps: f64, + pub backend_type: VideoBackendType, + pub is_ready: bool, +} + +impl Default for VideoStats { + fn default() -> Self { + Self { + frames_pushed: 0, + frames_dropped: 0, + total_bytes: 0, + fps: 0.0, + backend_type: VideoBackendType::PipeWire, + is_ready: false, + } + } +} + +/// Video backend factory +pub struct VideoBackend; + +impl VideoBackend { + /// Create a new PipeWire backend + pub fn new_pipewire() -> Result<Box<dyn VideoBackendTrait>> { + Ok(Box::new(PipeWireBackend::new(PipeWireConfig::default()))) + } + + /// Create a new V4L2 backend (for future use) + pub fn new_v4l2() -> Result<Box<dyn VideoBackendTrait>> { + Ok(Box::new(V4L2Backend::new()?)) + } + + /// Create a new stdout backend + pub fn new_stdout() -> Result<Box<dyn VideoBackendTrait>> { + Ok(Box::new(StdoutBackend::new())) + } + + /// Create a backend based on configuration + pub fn from_config(config: &VideoConfig) -> Result<Box<dyn VideoBackendTrait>> { + match config.backend_type { + VideoBackendType::PipeWire => Self::new_pipewire(), + VideoBackendType::V4L2 => Self::new_v4l2(), + VideoBackendType::Stdout => Self::new_stdout(), + } + } + + /// Create a backend from type + pub fn from_type(backend_type: VideoBackendType) -> Result<Box<dyn VideoBackendTrait>> { + match backend_type { + VideoBackendType::PipeWire => Self::new_pipewire(), + VideoBackendType::V4L2 => Self::new_v4l2(), + VideoBackendType::Stdout => Self::new_stdout(), + } + } +} + +/// Video frame metadata +#[derive(Debug, Clone)] +pub struct VideoFrame { + pub data: Vec<u8>, + pub width: u32, + pub height: u32, + pub format: VideoFormat, + pub timestamp: std::time::Instant, +} + +impl VideoFrame { + /// Create a new video frame + pub fn new(data: Vec<u8>, width: u32, height: u32, format: VideoFormat) -> Self { + Self { + data, + width, + height, + format, + timestamp: std::time::Instant::now(), + } + } + + /// Get frame size in bytes + pub fn size(&self) -> usize { + self.data.len() + } + + /// Get frame dimensions + pub fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + /// Check if frame is valid + pub fn is_valid(&self) -> bool { + !self.data.is_empty() && self.width > 0 && self.height > 0 + } +} + +/// Video backend manager +pub struct VideoBackendManager { + backend: Arc<Mutex<Box<dyn VideoBackendTrait>>>, + config: VideoConfig, + stats: Arc<Mutex<VideoStats>>, +} + +impl VideoBackendManager { + /// Create a new video backend manager + pub fn new(config: VideoConfig) -> Result<Self> { + let backend = VideoBackend::from_config(&config)?; + let stats = Arc::new(Mutex::new(VideoStats::default())); + + let manager = Self { + backend: Arc::new(Mutex::new(backend)), + config, + stats, + }; + + // Initialize the backend + let mut backend_guard = manager.backend.lock().unwrap(); + backend_guard.initialize()?; + drop(backend_guard); + + Ok(manager) + } + + /// Push a frame to the video backend + pub fn push_frame(&self, frame_data: &[u8]) -> Result<()> { + let backend = self.backend.lock().unwrap(); + + if !backend.is_ready() { + return Err(VideoError::DeviceNotReady.into()); + } + + // Update statistics + let mut stats = self.stats.lock().unwrap(); + stats.frames_pushed += 1; + stats.total_bytes += frame_data.len() as u64; + drop(stats); + + // Push frame to backend + backend.push_frame(frame_data)?; + + Ok(()) + } + + /// Get current statistics + pub fn get_stats(&self) -> VideoStats { + let stats = self.stats.lock().unwrap(); + stats.clone() + } + + /// Switch video backend + pub fn switch_backend(&mut self, new_type: VideoBackendType) -> Result<()> { + // Shutdown current backend + let mut backend = self.backend.lock().unwrap(); + backend.shutdown()?; + drop(backend); + + // Create new backend + let new_backend = VideoBackend::from_type(new_type)?; + let mut backend = self.backend.lock().unwrap(); + *backend = new_backend; + + // Initialize new backend + backend.initialize()?; + + // Update config + self.config.backend_type = new_type; + + info!("Switched to {:?} backend", new_type); + Ok(()) + } + + /// Get configuration + pub fn config(&self) -> &VideoConfig { + &self.config + } + + /// Update configuration + pub fn update_config(&mut self, config: VideoConfig) -> Result<()> { + // Recreate backend if type changed + if self.config.backend_type != config.backend_type { + self.switch_backend(config.backend_type)?; + } + + self.config = config; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_video_backend_factory() { + // Test PipeWire backend creation + let pipewire_backend = VideoBackend::new_pipewire(); + assert!(pipewire_backend.is_ok()); + + // Test V4L2 backend creation + let v4l2_backend = VideoBackend::new_v4l2(); + assert!(v4l2_backend.is_ok()); + } + + #[test] + fn test_video_frame_creation() { + let frame_data = vec![0u8; 1024]; + let frame = VideoFrame::new(frame_data.clone(), 32, 32, VideoFormat::RGB24); + + assert_eq!(frame.data, frame_data); + assert_eq!(frame.width, 32); + assert_eq!(frame.height, 32); + assert_eq!(frame.format, VideoFormat::RGB24); + assert!(frame.is_valid()); + } + + #[test] + fn test_video_format_conversions() { + assert_eq!(VideoFormat::MJPEG.as_str(), "MJPEG"); + assert_eq!(VideoFormat::YUV420.as_str(), "YUV420"); + assert_eq!(VideoFormat::RGB24.as_str(), "RGB24"); + + assert_eq!(VideoFormat::MJPEG.bytes_per_pixel(), 0); + assert_eq!(VideoFormat::YUV420.bytes_per_pixel(), 1); + assert_eq!(VideoFormat::RGB24.bytes_per_pixel(), 3); + } +} diff --git a/src/video/pipewire.rs b/src/video/pipewire.rs new file mode 100644 index 0000000..62dad11 --- /dev/null +++ b/src/video/pipewire.rs @@ -0,0 +1,773 @@ +//! PipeWire backend for video streaming using native library + +use super::{VideoBackendTrait, VideoFormat, VideoStats}; +use crate::error::{Result, VideoError}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering, Ordering as AtomicOrdering}; +use std::sync::Mutex; +use std::time::Instant; +use tracing::{debug, info, trace, error, warn}; +use std::thread; +use std::sync::mpsc::{self, Sender, Receiver}; + +// PipeWire imports +use pipewire::{ + main_loop::MainLoop, + context::Context, + stream::{Stream, StreamFlags, StreamState}, + properties::properties, + keys, + spa::pod::{self, Pod, Object}, + spa::utils::{Direction, SpaTypes, Fraction, Rectangle}, + spa::param::ParamType, + spa::param::format::{FormatProperties, MediaSubtype, MediaType}, + spa::pod::serialize::PodSerializer, +}; + +/// PipeWire backend implementation using native library +pub struct PipeWireBackend { + is_initialized: bool, + stats: Arc<Mutex<VideoStats>>, + config: PipeWireConfig, + running: Arc<AtomicBool>, + virtual_node_id: Option<u32>, + pw_frame_sender: Option<Sender<Vec<u8>>>, // Separate sender for PipeWire thread + stats_frame_sender: Option<Sender<Vec<u8>>>, // Separate sender for stats thread + last_frame_time: Arc<Mutex<Instant>>, + + // PipeWire objects - these need to be in a separate thread-safe context + pw_thread: Option<thread::JoinHandle<()>>, +} + +/// PipeWire configuration +#[derive(Debug, Clone)] +pub struct PipeWireConfig { + pub node_name: String, + pub description: String, + pub media_class: String, + pub format: VideoFormat, + pub width: u32, + pub height: u32, + pub framerate: u32, +} + +impl Default for PipeWireConfig { + fn default() -> Self { + Self { + node_name: "geek-szitman-supercamera".to_string(), + description: "Geek Szitman SuperCamera - High-quality virtual camera for streaming and recording".to_string(), + media_class: "Video/Source".to_string(), + format: VideoFormat::MJPEG, // Changed back to MJPEG since that's what the camera provides + width: 640, + height: 480, + framerate: 30, + } + } +} + +impl PipeWireBackend { + pub fn new(config: PipeWireConfig) -> Self { + Self { + is_initialized: false, + stats: Arc::new(Mutex::new(VideoStats::default())), + config, + running: Arc::new(AtomicBool::new(false)), + virtual_node_id: None, + pw_frame_sender: None, + stats_frame_sender: None, + last_frame_time: Arc::new(Mutex::new(Instant::now())), + pw_thread: None, + } + } + + /// Check if PipeWire is available and running + fn check_pipewire_available(&self) -> Result<()> { + info!("Checking PipeWire availability..."); + // This is a basic check - in a real implementation you might want to + // try to connect to the daemon to verify it's actually running + info!("PipeWire availability check passed"); + Ok(()) + } + + /// Create a virtual camera node using native PipeWire API + fn create_virtual_camera_node(&mut self) -> Result<()> { + info!("Creating PipeWire virtual camera node using native API..."); + info!("Node name: '{}'", self.config.node_name); + info!("Node description: '{}'", self.config.description); + + // Start PipeWire processing in a separate thread to avoid Send/Sync issues + // The actual node creation and availability will be logged in the PipeWire thread + // Ensure the processing loop runs + self.running.store(true, Ordering::Relaxed); + let running = Arc::clone(&self.running); + let config = self.config.clone(); + + // Create channel for frame communication with PipeWire thread + let (frame_sender, frame_receiver) = mpsc::channel(); + self.pw_frame_sender = Some(frame_sender); + + info!("Starting PipeWire thread..."); + let handle = thread::spawn(move || { + info!("PipeWire thread started, entering main loop..."); + // Set panic hook to catch any panics in this thread + std::panic::set_hook(Box::new(|panic_info| { + error!("PipeWire thread panicked: {:?}", panic_info); + })); + + Self::pipewire_main_loop(running, config, frame_receiver); + info!("PipeWire thread exiting..."); + }); + + self.pw_thread = Some(handle); + self.virtual_node_id = Some(999); // Placeholder - will be updated when stream is ready + info!("Virtual camera node creation initiated in separate thread"); + Ok(()) + } + + /// Main PipeWire loop that runs in a separate thread + fn pipewire_main_loop(running: Arc<AtomicBool>, config: PipeWireConfig, frame_receiver: Receiver<Vec<u8>>) { + info!("Starting PipeWire main loop in thread"); + + info!("Initializing PipeWire..."); + pipewire::init(); + info!("PipeWire initialized successfully"); + + // Create main loop with no properties + info!("Creating PipeWire main loop..."); + let mainloop = match MainLoop::new(None) { + Ok(ml) => { + info!("Main loop created successfully"); + ml + }, + Err(e) => { + error!("Failed to create PipeWire main loop: {}", e); + error!("MainLoop::new error details: {:?}", e); + return; + } + }; + + // Create context + info!("Creating PipeWire context..."); + let context = match Context::new(&mainloop) { + Ok(ctx) => { + info!("Context created successfully"); + ctx + }, + Err(e) => { + error!("Failed to create PipeWire context: {}", e); + return; + } + }; + + // Connect to PipeWire daemon + info!("Connecting to PipeWire daemon..."); + let core = match context.connect(None) { + Ok(c) => { + info!("Connected to PipeWire daemon successfully"); + c + }, + Err(e) => { + error!("Failed to connect to PipeWire daemon: {}", e); + return; + } + }; + + info!("PipeWire connection established successfully"); + + // Set up registry listener to capture object.serial when our node appears + let serial_slot = Arc::new(AtomicU32::new(0)); + let serial_slot_clone = Arc::clone(&serial_slot); + let wanted_name = config.node_name.clone(); + + let registry = core.get_registry().expect("get_registry"); + let _reg_listener = registry + .add_listener_local() + .global(move |global_obj| { + if global_obj.type_ == pipewire::types::ObjectType::Node { + if let Some(props) = &global_obj.props { + if let Some(name) = props.get("node.name") { + if name == wanted_name { + if let Some(s) = props.get("object.serial") { + if let Ok(v) = s.parse::<u32>() { + serial_slot_clone.store(v, AtomicOrdering::SeqCst); + info!("Discovered our node in registry: node.name={} object.serial={}", name, v); + } + } + } + } + } + } + }) + .register(); + + // User data for stream callbacks + #[derive(Debug)] + struct UserData { + is_mjpeg: bool, + frame_size: u32, + stride: i32, + current_frame: Arc<Mutex<Option<Vec<u8>>>>, + } + + let current_frame = Arc::new(Mutex::new(None)); + let current_frame_clone = Arc::clone(¤t_frame); + + // Start frame receiver thread + let frame_receiver = Arc::new(Mutex::new(frame_receiver)); + let frame_receiver_clone = Arc::clone(&frame_receiver); + let running_clone = Arc::clone(&running); + let _frame_thread = thread::spawn(move || { + while running_clone.load(Ordering::Relaxed) { + let frame_data = { + let receiver_guard = frame_receiver_clone.lock().unwrap(); + match receiver_guard.recv_timeout(std::time::Duration::from_millis(16)) { + Ok(data) => Some(data), + Err(mpsc::RecvTimeoutError::Timeout) => None, + Err(mpsc::RecvTimeoutError::Disconnected) => break, + } + }; + + if let Some(frame_data) = frame_data { + let mut frame_guard = current_frame_clone.lock().unwrap(); + *frame_guard = Some(frame_data); + trace!("Received new frame for PipeWire processing"); + } + } + }); + + // Create a stream that will act as a video source + let stream = match Stream::new( + &core, + &config.node_name, + properties! { + // Essential keys for Video/Source classification + *keys::MEDIA_CLASS => "Video/Source", + *keys::NODE_NAME => config.node_name.as_str(), + *keys::APP_NAME => "geek-szitman-supercamera", + *keys::NODE_DESCRIPTION => config.description.as_str(), + // Additional metadata + "media.role" => "Camera", + "media.category" => "Capture", + // Optional cosmetics + "media.nick" => "SuperCamera", + "device.icon_name" => "camera-web", + // Prevent PipeWire from trying to drive the graph until someone connects + "node.passive" => "true", + }, + ) { + Ok(s) => s, + Err(e) => { + error!("Failed to create PipeWire stream: {}", e); + return; + } + }; + + // Build EnumFormat pod(s) - simplified to just MJPEG + let width_u = config.width as u32; + let height_u = config.height as u32; + let fps_u = config.framerate as u32; + + // MJPEG: JPEG compressed - simplified format + let enum_mjpeg = pod::object!( + SpaTypes::ObjectParamFormat, + ParamType::EnumFormat, + pod::property!(FormatProperties::MediaType, Id, MediaType::Video), + pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Mjpg), + pod::property!( + FormatProperties::VideoSize, + Choice, Range, Rectangle, + Rectangle { width: width_u, height: height_u }, + Rectangle { width: 16, height: 16 }, + Rectangle { width: 4096, height: 4096 } + ), + pod::property!( + FormatProperties::VideoFramerate, + Choice, Range, Fraction, + Fraction { num: fps_u, denom: 1 }, + Fraction { num: 1, denom: 1 }, + Fraction { num: 120, denom: 1 } + ), + ); + + // Clone config values for closures + let config_width = config.width; + let config_height = config.height; + let config_framerate = config.framerate; + + // Set up stream callbacks + let _listener = match stream + .add_local_listener_with_user_data(UserData { + is_mjpeg: false, + frame_size: 4 * 1024 * 1024, // safe cap + stride: 0, + current_frame: Arc::clone(¤t_frame), + }) + .state_changed(move |stream, _user_data, old, new| { + info!("PipeWire stream state: {:?} -> {:?}", old, new); + if matches!(new, StreamState::Paused | StreamState::Streaming) { + info!("PipeWire node is ready and can be targeted by applications"); + } + if new == StreamState::Paused { + if let Err(e) = stream.set_active(true) { + error!("Failed to activate PipeWire stream: {}", e); + } else { + info!("Activated stream scheduling"); + } + } + if new == StreamState::Streaming { + info!("Stream is now streaming - virtual camera is active!"); + } + }) + .param_changed(move |stream, user_data, id, param| { + if let Some(param) = param { + info!("Param changed: id={:?}, type={:?}, raw_id={}", id, param.type_(), id); + + // Handle format negotiation - simplified approach + if id == ParamType::Format.as_raw() || id == ParamType::EnumFormat.as_raw() || id == 15 { + info!("Format param received (id={}), setting up basic MJPEG format...", id); + + // Set basic MJPEG parameters + user_data.is_mjpeg = true; + user_data.frame_size = 4 * 1024 * 1024; // 4MB safe cap for MJPEG + user_data.stride = 0; // MJPEG doesn't have stride + info!("Basic MJPEG format configured: {}x{} @ {} fps", config_width, config_height, config_framerate); + + // Try to activate the stream directly + if let Err(e) = stream.set_active(true) { + error!("Failed to activate stream: {}", e); + } else { + info!("Stream activated successfully"); + } + } else { + trace!("Stream param changed: id={} (ignored)", id); + } + } else { + trace!("Stream param changed: id={} (ignored)", id); + } + }) + .process(move |stream, user_data| { + // Dequeue buffer + let Some(mut buffer) = stream.dequeue_buffer() else { + trace!("Out of buffers"); + return; + }; + + // Get the current frame from UPP protocol + let frame_data = { + let frame_guard = user_data.current_frame.lock().unwrap(); + frame_guard.clone() + }; + + if let Some(frame_data) = frame_data { + // Process actual camera frame data from UPP protocol + trace!("Processing UPP camera frame: {} bytes", frame_data.len()); + + for data in buffer.datas_mut() { + if let Some(mem) = data.data() { + let len = mem.len(); + + if !user_data.is_mjpeg { + // Handle raw formats (RGBx or I420) + let w = config_width as usize; + let h = config_height as usize; + let stride = user_data.stride as usize; + + if frame_data.len() >= w * h * 3 { + // Convert RGB to RGBA + let mut off = 0usize; + for y in 0..h { + let row_end = (off + stride).min(len); + let row = &mut mem[off..row_end]; + + for x in 0..w.min(row.len()/4) { + let src_idx = (y * w + x) * 3; + if src_idx + 2 < frame_data.len() { + row[x * 4 + 0] = frame_data[src_idx + 0]; // R + row[x * 4 + 1] = frame_data[src_idx + 1]; // G + row[x * 4 + 2] = frame_data[src_idx + 2]; // B + row[x * 4 + 3] = 255; // A + } else { + row[x * 4 + 0] = 0; // R + row[x * 4 + 1] = 0; // G + row[x * 4 + 2] = 0; // B + row[x * 4 + 3] = 255; // A + } + } + + off = off.saturating_add(stride); + if off >= len { break; } + } + + *data.chunk_mut().size_mut() = (w * h * 4) as u32; + *data.chunk_mut().stride_mut() = user_data.stride; + } else { + // Frame data too small, fill with black + for i in 0..len { + mem[i] = 0; + } + *data.chunk_mut().size_mut() = len as u32; + *data.chunk_mut().stride_mut() = user_data.stride; + } + } else { + // Handle MJPEG format - copy JPEG data directly + if frame_data.len() <= len { + mem[..frame_data.len()].copy_from_slice(&frame_data); + *data.chunk_mut().size_mut() = frame_data.len() as u32; + trace!("Copied MJPEG frame: {} bytes", frame_data.len()); + } else { + // Frame too large for buffer, truncate + mem[..len].copy_from_slice(&frame_data[..len]); + *data.chunk_mut().size_mut() = len as u32; + warn!("MJPEG frame truncated: {} -> {} bytes", frame_data.len(), len); + } + } + } + } + } else { + // No frame data available, generate black frame as fallback + trace!("No UPP frame data available, generating black frame"); + + for data in buffer.datas_mut() { + if let Some(mem) = data.data() { + let len = mem.len(); + + if !user_data.is_mjpeg { + // Fill with black for raw formats + for i in 0..len { + mem[i] = 0; + } + + let w = config_width as usize; + let h = config_height as usize; + *data.chunk_mut().size_mut() = (w * h * 4) as u32; + *data.chunk_mut().stride_mut() = user_data.stride; + } else { + // Generate minimal valid 1x1 black JPEG for MJPEG format + // This is a minimal valid JPEG that represents a 1x1 black pixel + let minimal_jpeg: [u8; 143] = [ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, + 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, + 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, + 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20, + 0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, + 0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32, + 0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x01, + 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, + 0xFF, 0xC4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xDA, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0x37, 0xFF, 0xD9 + ]; + + let copy_len = minimal_jpeg.len().min(len); + mem[..copy_len].copy_from_slice(&minimal_jpeg[..copy_len]); + *data.chunk_mut().size_mut() = copy_len as u32; + trace!("Generated minimal 1x1 black JPEG placeholder: {} bytes, chunk size set to {}", copy_len, copy_len); + } + } + } + } + + // Debug: Log chunk sizes before queuing + for (i, data) in buffer.datas_mut().iter_mut().enumerate() { + let chunk = data.chunk_mut(); + trace!("Buffer {} chunk {}: size={}, stride={}", i, i, chunk.size(), chunk.stride()); + } + + // Return buffer to stream by dropping it (this automatically queues it) + // The Buffer struct implements Drop which handles the queuing + drop(buffer); + }) + .register() + { + Ok(l) => l, + Err(e) => { + error!("Failed to register stream listener: {}", e); + return; + } + }; + + // Connect as an output (we are a source). Use MAP_BUFFERS only, not DRIVER. + // Serialize EnumFormat pods to bytes and build &Pod slice + let obj_to_pod = |obj: Object| -> Vec<u8> { + let value = pod::Value::Object(obj); + PodSerializer::serialize(std::io::Cursor::new(Vec::new()), &value) + .unwrap() + .0 + .into_inner() + }; + let enum_bytes: Vec<Vec<u8>> = vec![obj_to_pod(enum_mjpeg)]; + let mut enum_pods: Vec<&Pod> = enum_bytes.iter().map(|b| Pod::from_bytes(b).unwrap()).collect(); + + if let Err(e) = stream.connect( + Direction::Output, + None, + StreamFlags::MAP_BUFFERS, + &mut enum_pods[..], + ) { + error!("Failed to connect PipeWire stream: {}", e); + return; + } + + info!("Stream connected successfully"); + info!("Virtual camera node '{}' is connecting to PipeWire", config.node_name); + info!("Other applications can now attempt to negotiate formats"); + + // Wait for our node to appear in the registry and capture object.serial + let t0 = std::time::Instant::now(); + while serial_slot.load(AtomicOrdering::SeqCst) == 0 && t0.elapsed() < std::time::Duration::from_millis(1500) { + mainloop.loop_().iterate(std::time::Duration::from_millis(10)); + } + let serial_logged = serial_slot.load(AtomicOrdering::SeqCst); + if serial_logged != 0 { + info!("You can target this node with: target-object={} or target-object={}", serial_logged, config.node_name); + } else { + warn!("Node serial not observed yet in registry; it may appear a bit later."); + } + + // Run main loop until told to stop + info!("Starting main loop iteration..."); + let mut iteration_count = 0; + while running.load(Ordering::Relaxed) { + iteration_count += 1; + if iteration_count % 1000 == 0 { + info!("Main loop iteration: {}", iteration_count); + } + + // Drive loop + let result = mainloop.loop_().iterate(std::time::Duration::from_millis(16)); + if result < 0 { + error!("Main loop iteration failed with result: {}", result); + break; + } + } + info!("Main loop exited after {} iterations", iteration_count); + } + + /// Start frame processing thread + fn start_frame_processor(&mut self) -> Result<()> { + let (tx, rx) = mpsc::channel(); + self.stats_frame_sender = Some(tx); // Use separate sender for stats + + let running = Arc::clone(&self.running); + let stats = Arc::clone(&self.stats); + let last_frame_time = Arc::clone(&self.last_frame_time); + + thread::spawn(move || { + Self::frame_processing_loop(rx, running, stats, last_frame_time); + }); + + info!("Frame processing thread started"); + Ok(()) + } + + /// Frame processing loop that runs in a separate thread + fn frame_processing_loop( + rx: Receiver<Vec<u8>>, + running: Arc<AtomicBool>, + stats: Arc<Mutex<VideoStats>>, + last_frame_time: Arc<Mutex<Instant>>, + ) { + while running.load(Ordering::Relaxed) { + match rx.recv_timeout(std::time::Duration::from_millis(100)) { + Ok(frame_data) => { + // Process frame and update statistics + Self::update_stats(&stats, &last_frame_time, frame_data.len()); + trace!("Frame processed: {} bytes", frame_data.len()); + } + Err(mpsc::RecvTimeoutError::Timeout) => { + continue; + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + break; + } + } + } + } + + /// Update statistics with proper FPS calculation + fn update_stats( + stats: &Arc<Mutex<VideoStats>>, + last_frame_time: &Arc<Mutex<Instant>>, + frame_size: usize, + ) { + let mut stats_guard = stats.lock().unwrap(); + stats_guard.frames_pushed += 1; + stats_guard.total_bytes += frame_size as u64; + stats_guard.backend_type = super::VideoBackendType::PipeWire; + stats_guard.is_ready = true; + + let now = Instant::now(); + let mut last_time = last_frame_time.lock().unwrap(); + let duration = now.duration_since(*last_time); + *last_time = now; + + if duration.as_millis() > 0 { + stats_guard.fps = 1000.0 / duration.as_millis() as f64; + } + } + + /// Get current node information for external tools + pub fn get_node_info(&self) -> Option<(u32, u32, String)> { + // This would need to be implemented with proper synchronization + // For now, return the config info + Some(( + self.virtual_node_id.unwrap_or(0), + 0, // object.serial - would need to be stored from the stream + self.config.node_name.clone(), + )) + } + + /// Get the object.serial for targeting (if available) + pub fn get_object_serial(&self) -> Option<u32> { + // This would need to be implemented with proper synchronization + // For now, return None - the serial is logged when discovered + None + } + + /// Check if the virtual camera node is registered and discoverable + pub fn is_node_registered(&self) -> bool { + self.is_initialized && self.running.load(Ordering::Relaxed) + } +} + +impl VideoBackendTrait for PipeWireBackend { + fn initialize(&mut self) -> Result<()> { + if self.is_initialized { + return Ok(()); + } + + info!("Initializing PipeWire backend with native library..."); + + if let Err(e) = self.check_pipewire_available() { + error!("PipeWire not available: {}", e); + return Err(VideoError::DeviceNotReady.into()); + } + + if let Err(e) = self.create_virtual_camera_node() { + error!("Failed to create virtual camera node: {}", e); + return Err(VideoError::DeviceNotReady.into()); + } + + if let Err(e) = self.start_frame_processor() { + error!("Failed to start frame processor: {}", e); + return Err(VideoError::DeviceNotReady.into()); + } + + self.is_initialized = true; + self.running.store(true, Ordering::Relaxed); + info!("PipeWire backend initialized successfully with native library"); + // Remove premature logging - the actual node creation will be logged in the PipeWire thread + // when the node is actually available + + Ok(()) + } + + fn push_frame(&self, frame_data: &[u8]) -> Result<()> { + if !self.running.load(Ordering::Relaxed) { + return Err(VideoError::DeviceNotReady.into()); + } + + if !self.is_initialized { + return Err(VideoError::DeviceNotReady.into()); + } + + trace!("Queueing frame for PipeWire: {} bytes", frame_data.len()); + + // Send to PipeWire thread + if let Some(sender) = &self.pw_frame_sender { + if let Err(e) = sender.send(frame_data.to_vec()) { + error!("Failed to queue frame to PipeWire: {}", e); + return Err(VideoError::DeviceNotReady.into()); + } + debug!("Frame queued for PipeWire processing: {} bytes", frame_data.len()); + } else { + error!("PipeWire frame sender not available"); + return Err(VideoError::DeviceNotReady.into()); + } + + // Send to stats thread + if let Some(sender) = &self.stats_frame_sender { + if let Err(e) = sender.send(frame_data.to_vec()) { + error!("Failed to queue frame to stats: {}", e); + // Don't fail the entire operation for stats + } + } + + trace!("Frame queued successfully"); + Ok(()) + } + + fn get_stats(&self) -> VideoStats { + self.stats.lock().unwrap().clone() + } + + fn is_ready(&self) -> bool { + self.is_initialized && self.running.load(Ordering::Relaxed) + } + + fn shutdown(&mut self) -> Result<()> { + if !self.is_initialized { + return Ok(()) + } + + info!("Shutting down PipeWire backend..."); + + self.running.store(false, Ordering::Relaxed); + + if let Some(handle) = self.pw_thread.take() { + if let Err(e) = handle.join() { + error!("Error joining PipeWire thread: {:?}", e); + } + } + + self.virtual_node_id = None; + self.pw_frame_sender = None; + self.stats_frame_sender = None; + + self.is_initialized = false; + info!("PipeWire backend shut down successfully"); + + Ok(()) + } +} + +impl Drop for PipeWireBackend { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + + if let Some(handle) = self.pw_thread.take() { + let _ = handle.join(); + } + + // Note: frame_senders will be dropped automatically + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pipewire_backend_creation() { + let config = PipeWireConfig::default(); + let backend = PipeWireBackend::new(config); + + assert_eq!(backend.config.node_name, "geek-szitman-supercamera"); + assert_eq!(backend.config.description, "Geek Szitman SuperCamera - High-quality virtual camera for streaming and recording"); + assert_eq!(backend.config.media_class, "Video/Source"); + assert_eq!(backend.config.format, VideoFormat::MJPEG); + assert_eq!(backend.config.width, 640); + assert_eq!(backend.config.height, 480); + assert_eq!(backend.config.framerate, 30); + } + + #[test] + fn test_pipewire_backend_default_config() { + let config = PipeWireConfig::default(); + let backend = PipeWireBackend::new(config); + + assert!(!backend.is_initialized); + assert!(!backend.is_ready()); + } +} diff --git a/src/video/stdout.rs b/src/video/stdout.rs new file mode 100644 index 0000000..ce9882a --- /dev/null +++ b/src/video/stdout.rs @@ -0,0 +1,277 @@ +//! Stdout video backend for piping video output to other tools + +use crate::error::{Result, VideoError}; +use crate::video::{VideoBackendTrait, VideoStats}; +use std::io::{self, Write}; +use std::sync::Mutex; +use tracing::{debug, info}; + +/// Configuration for the stdout video backend +#[derive(Debug, Clone)] +pub struct StdoutConfig { + /// Whether to output frame headers with metadata + pub include_headers: bool, + /// Whether to flush stdout after each frame + pub flush_after_frame: bool, + /// Output format for headers (if enabled) + pub header_format: HeaderFormat, +} + +impl Default for StdoutConfig { + fn default() -> Self { + Self { + include_headers: false, + flush_after_frame: true, + header_format: HeaderFormat::Simple, + } + } +} + +/// Header format for frame metadata +#[derive(Debug, Clone)] +pub enum HeaderFormat { + /// Simple text format: "FRAME:size:timestamp\n" + Simple, + /// JSON format: {"frame": {"size": size, "timestamp": timestamp}} + Json, + /// Binary format: 4-byte size + 8-byte timestamp + Binary, +} + +/// Stdout video backend that outputs raw video frames to stdout +pub struct StdoutBackend { + config: StdoutConfig, + stats: Mutex<VideoStats>, + frame_count: Mutex<u64>, + start_time: Mutex<std::time::Instant>, +} + +impl StdoutBackend { + /// Create a new stdout backend with default configuration + pub fn new() -> Self { + Self { + config: StdoutConfig::default(), + stats: Mutex::new(VideoStats::default()), + frame_count: Mutex::new(0), + start_time: Mutex::new(std::time::Instant::now()), + } + } + + /// Create a new stdout backend with custom configuration + pub fn with_config(config: StdoutConfig) -> Self { + Self { + config, + stats: Mutex::new(VideoStats::default()), + frame_count: Mutex::new(0), + start_time: Mutex::new(std::time::Instant::now()), + } + } + + /// Write frame header if enabled + fn write_frame_header(&self, frame_size: usize) -> Result<()> { + if !self.config.include_headers { + return Ok(()); + } + + let start_time = self.start_time.lock().unwrap(); + let timestamp = start_time.elapsed().as_micros(); + drop(start_time); + + match self.config.header_format { + HeaderFormat::Simple => { + let header = format!("FRAME:{}:{}\n", frame_size, timestamp); + io::stdout().write_all(header.as_bytes())?; + } + HeaderFormat::Json => { + let header = format!( + r#"{{"frame": {{"size": {}, "timestamp": {}}}}}{}"#, + frame_size, timestamp, '\n' + ); + io::stdout().write_all(header.as_bytes())?; + } + HeaderFormat::Binary => { + // 4-byte size + 8-byte timestamp + let size_bytes = (frame_size as u32).to_le_bytes(); + let timestamp_bytes = timestamp.to_le_bytes(); + let mut header = Vec::with_capacity(12); + header.extend_from_slice(&size_bytes); + header.extend_from_slice(×tamp_bytes); + io::stdout().write_all(&header)?; + } + } + Ok(()) + } + + /// Update statistics + fn update_stats(&self, frame_size: usize) { + let mut frame_count = self.frame_count.lock().unwrap(); + *frame_count += 1; + let current_frame_count = *frame_count; + drop(frame_count); + + let mut stats = self.stats.lock().unwrap(); + stats.frames_pushed = current_frame_count; + stats.total_bytes += frame_size as u64; + + // Calculate FPS based on elapsed time + let start_time = self.start_time.lock().unwrap(); + let elapsed = start_time.elapsed().as_secs_f64(); + drop(start_time); + + if elapsed > 0.0 { + stats.fps = current_frame_count as f64 / elapsed; + } + + // Log frame information for debugging + if current_frame_count % 30 == 0 { // Log every 30 frames + tracing::debug!( + "Stdout backend: frame {}, size: {} bytes, total: {} bytes, fps: {:.2}", + current_frame_count, frame_size, stats.total_bytes, stats.fps + ); + } + } +} + +impl VideoBackendTrait for StdoutBackend { + fn initialize(&mut self) -> Result<()> { + info!("Initializing stdout video backend"); + { + let mut start_time = self.start_time.lock().unwrap(); + *start_time = std::time::Instant::now(); + } + { + let mut stats = self.stats.lock().unwrap(); + stats.is_ready = true; + } + + // Write initial header if enabled + if self.config.include_headers { + match self.config.header_format { + HeaderFormat::Simple => { + io::stdout().write_all(b"STDOUT_VIDEO_STREAM_START\n")?; + } + HeaderFormat::Json => { + io::stdout().write_all(b"{\"stream\": \"start\"}\n")?; + } + HeaderFormat::Binary => { + // Magic number: "STDO" (4 bytes) + io::stdout().write_all(b"STDO")?; + } + } + } + + debug!("Stdout video backend initialized successfully"); + Ok(()) + } + + fn push_frame(&self, frame_data: &[u8]) -> Result<()> { + let stats = self.stats.lock().unwrap(); + if !stats.is_ready { + return Err(VideoError::DeviceNotReady.into()); + } + drop(stats); + + // Write frame header if enabled + self.write_frame_header(frame_data.len())?; + + // Write frame data to stdout + io::stdout().write_all(frame_data)?; + + // Flush if configured + if self.config.flush_after_frame { + io::stdout().flush()?; + } + + // Update statistics + self.update_stats(frame_data.len()); + debug!("Pushed frame to stdout: {} bytes", frame_data.len()); + Ok(()) + } + + fn get_stats(&self) -> VideoStats { + let mut stats = self.stats.lock().unwrap().clone(); + stats.backend_type = crate::video::VideoBackendType::Stdout; + stats + } + + fn is_ready(&self) -> bool { + self.stats.lock().unwrap().is_ready + } + + fn shutdown(&mut self) -> Result<()> { + info!("Shutting down stdout video backend"); + + // Write final header if enabled + if self.config.include_headers { + match self.config.header_format { + HeaderFormat::Simple => { + io::stdout().write_all(b"STDOUT_VIDEO_STREAM_END\n")?; + } + HeaderFormat::Json => { + io::stdout().write_all(b"{\"stream\": \"end\"}\n")?; + } + HeaderFormat::Binary => { + // End marker: "END\0" (4 bytes) + io::stdout().write_all(b"END\0")?; + } + } + } + + // Final flush + io::stdout().flush()?; + + { + let mut stats = self.stats.lock().unwrap(); + stats.is_ready = false; + } + Ok(()) + } +} + +impl Drop for StdoutBackend { + fn drop(&mut self) { + let is_ready = { + let stats = self.stats.lock().unwrap(); + stats.is_ready + }; + if is_ready { + let _ = self.shutdown(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::video::VideoBackendTrait; + + #[test] + fn test_stdout_backend_creation() { + let backend = StdoutBackend::new(); + assert!(!backend.is_ready()); + assert_eq!(backend.get_stats().frames_pushed, 0); + } + + #[test] + fn test_stdout_backend_with_config() { + let config = StdoutConfig { + include_headers: true, + flush_after_frame: false, + header_format: HeaderFormat::Json, + }; + let backend = StdoutBackend::with_config(config); + assert!(!backend.is_ready()); + } + + #[test] + fn test_header_format_creation() { + let simple = HeaderFormat::Simple; + let json = HeaderFormat::Json; + let binary = HeaderFormat::Binary; + + // Just test that they can be created + assert!(matches!(simple, HeaderFormat::Simple)); + assert!(matches!(json, HeaderFormat::Json)); + assert!(matches!(binary, HeaderFormat::Binary)); + } +} diff --git a/src/video/v4l2.rs b/src/video/v4l2.rs new file mode 100644 index 0000000..792ff51 --- /dev/null +++ b/src/video/v4l2.rs @@ -0,0 +1,321 @@ +//! V4L2 backend for video streaming (placeholder for future implementation) + +use super::{VideoBackendTrait, VideoStats, VideoFormat}; +use crate::error::{Result, VideoError}; +use std::sync::Arc; +use std::sync::Mutex; +use tracing::{debug, info, trace, warn}; + +/// V4L2 backend implementation (placeholder) +pub struct V4L2Backend { + device_path: String, + is_initialized: bool, + stats: Arc<Mutex<VideoStats>>, + config: V4L2Config, +} + +/// V4L2 configuration +#[derive(Debug, Clone)] +pub struct V4L2Config { + pub device_path: String, + pub width: u32, + pub height: u32, + pub fps: u32, + pub format: VideoFormat, + pub buffer_size: usize, +} + +impl Default for V4L2Config { + fn default() -> Self { + Self { + device_path: "/dev/video10".to_string(), + width: 640, + height: 480, + fps: 30, + format: VideoFormat::MJPEG, + buffer_size: 0x10000, // 64KB + } + } +} + +impl V4L2Backend { + /// Create a new V4L2 backend + pub fn new() -> Result<Self> { + let stats = VideoStats { + backend_type: super::VideoBackendType::V4L2, + ..Default::default() + }; + + Ok(Self { + device_path: "/dev/video10".to_string(), + is_initialized: false, + stats: Arc::new(Mutex::new(stats)), + config: V4L2Config::default(), + }) + } + + /// Create a new V4L2 backend with custom configuration + pub fn with_config(config: V4L2Config) -> Result<Self> { + let stats = VideoStats { + backend_type: super::VideoBackendType::V4L2, + ..Default::default() + }; + + Ok(Self { + device_path: config.device_path.clone(), + is_initialized: false, + stats: Arc::new(Mutex::new(stats)), + config, + }) + } + + /// Check if V4L2 device exists and is accessible + fn check_device(&self) -> Result<()> { + use std::path::Path; + + if !Path::new(&self.device_path).exists() { + return Err(VideoError::V4L2(format!("Device not found: {}", self.device_path)).into()); + } + + // TODO: Check device permissions and capabilities + debug!("V4L2 device found: {}", self.device_path); + Ok(()) + } + + /// Update statistics + fn update_stats(&self, frame_size: usize) { + let mut stats = self.stats.lock().unwrap(); + stats.frames_pushed += 1; + stats.total_bytes += frame_size as u64; + stats.backend_type = super::VideoBackendType::V4L2; + stats.is_ready = self.is_initialized; + + // Calculate FPS (simple rolling average) + // TODO: Implement proper FPS calculation + stats.fps = 30.0; // Placeholder + } +} + +impl VideoBackendTrait for V4L2Backend { + fn initialize(&mut self) -> Result<()> { + if self.is_initialized { + warn!("V4L2 backend already initialized"); + return Ok(()); + } + + info!("Initializing V4L2 backend..."); + + // Check if device exists and is accessible + if let Err(e) = self.check_device() { + warn!("V4L2 device check failed: {}", e); + return Err(e); + } + + // TODO: Implement actual V4L2 device initialization + // For now, this is a placeholder that simulates success + debug!("V4L2 initialization (placeholder) - would open device: {}", self.device_path); + debug!("Format: {}x{} @ {}fps ({})", + self.config.width, + self.config.height, + self.config.fps, + self.config.format.as_str()); + + self.is_initialized = true; + info!("V4L2 backend initialized successfully (placeholder)"); + + Ok(()) + } + + fn push_frame(&self, frame_data: &[u8]) -> Result<()> { + if !self.is_initialized { + return Err(VideoError::DeviceNotReady.into()); + } + + trace!("Pushing frame to V4L2 (placeholder): {} bytes", frame_data.len()); + + // TODO: Implement actual frame pushing to V4L2 + // For now, this is a placeholder that simulates success + debug!("Would push frame of {} bytes to V4L2 device: {}", frame_data.len(), self.device_path); + + // Update statistics + self.update_stats(frame_data.len()); + + trace!("Frame processed successfully (placeholder)"); + Ok(()) + } + + fn get_stats(&self) -> VideoStats { + self.stats.lock().unwrap().clone() + } + + fn is_ready(&self) -> bool { + self.is_initialized + } + + fn shutdown(&mut self) -> Result<()> { + if !self.is_initialized { + return Ok(()); + } + + info!("Shutting down V4L2 backend (placeholder)..."); + + // TODO: Implement actual V4L2 device cleanup + // For now, this is a placeholder that simulates success + + self.is_initialized = false; + info!("V4L2 backend shut down successfully (placeholder)"); + + Ok(()) + } +} + +impl Drop for V4L2Backend { + fn drop(&mut self) { + if self.is_initialized { + // Try to shutdown gracefully (synchronous) + let _ = self.shutdown(); + } + } +} + +/// V4L2 device information (placeholder) +#[derive(Debug, Clone)] +pub struct V4L2DeviceInfo { + pub device_path: String, + pub driver_name: String, + pub card_name: String, + pub bus_info: String, + pub capabilities: u32, +} + +impl V4L2DeviceInfo { + /// Create new device info + pub fn new(device_path: String) -> Self { + Self { + device_path, + driver_name: "Unknown".to_string(), + card_name: "Unknown".to_string(), + bus_info: "Unknown".to_string(), + capabilities: 0, + } + } + + /// Check if device supports video output + pub fn supports_video_output(&self) -> bool { + // TODO: Implement capability checking + true + } + + /// Check if device supports MJPEG format + pub fn supports_mjpeg(&self) -> bool { + // TODO: Implement format checking + true + } +} + +/// V4L2 format information (placeholder) +#[derive(Debug, Clone)] +pub struct V4L2Format { + pub width: u32, + pub height: u32, + pub pixel_format: u32, + pub field: u32, + pub bytes_per_line: u32, + pub size_image: u32, + pub colorspace: u32, +} + +impl V4L2Format { + /// Create new format + pub fn new(width: u32, height: u32, pixel_format: u32) -> Self { + Self { + width, + height, + pixel_format, + field: 1, // V4L2_FIELD_NONE + bytes_per_line: 0, + size_image: width * height * 2, // Estimate for MJPEG + colorspace: 1, // V4L2_COLORSPACE_SMPTE170M + } + } + + /// Get format description + pub fn description(&self) -> String { + format!("{}x{} @ {} bytes", self.width, self.height, self.size_image) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_v4l2_config_default() { + let config = V4L2Config::default(); + assert_eq!(config.device_path, "/dev/video10"); + assert_eq!(config.width, 640); + assert_eq!(config.height, 480); + assert_eq!(config.fps, 30); + assert!(matches!(config.format, VideoFormat::MJPEG)); + assert_eq!(config.buffer_size, 0x10000); + } + + #[test] + fn test_v4l2_backend_creation() { + let backend = V4L2Backend::new(); + assert!(backend.is_ok()); + + let backend = backend.unwrap(); + assert!(!backend.is_initialized); + assert_eq!(backend.device_path, "/dev/video10"); + } + + #[test] + fn test_v4l2_backend_with_config() { + let config = V4L2Config { + device_path: "/dev/video20".to_string(), + width: 1280, + height: 720, + fps: 60, + ..Default::default() + }; + + let backend = V4L2Backend::with_config(config); + assert!(backend.is_ok()); + } + + #[test] + fn test_v4l2_device_info() { + let device_info = V4L2DeviceInfo::new("/dev/video10".to_string()); + assert_eq!(device_info.device_path, "/dev/video10"); + assert_eq!(device_info.driver_name, "Unknown"); + assert!(device_info.supports_video_output()); + assert!(device_info.supports_mjpeg()); + } + + #[test] + fn test_v4l2_format() { + let format = V4L2Format::new(640, 480, 0x47504A4D); // MJPEG + assert_eq!(format.width, 640); + assert_eq!(format.height, 480); + assert_eq!(format.pixel_format, 0x47504A4D); + assert_eq!(format.description(), "640x480 @ 614400 bytes"); + } + + #[test] + fn test_v4l2_backend_stats() { + let backend = V4L2Backend::new().unwrap(); + let stats = backend.get_stats(); + + assert_eq!(stats.frames_pushed, 0); + assert_eq!(stats.total_bytes, 0); + assert!(!stats.is_ready); + assert!(matches!(stats.backend_type, super::super::VideoBackendType::V4L2)); + } + + #[test] + fn test_v4l2_backend_ready_state() { + let backend = V4L2Backend::new().unwrap(); + assert!(!backend.is_ready()); + } +} |
