diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-08 12:44:10 +0100 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-08 12:44:10 +0100 |
| commit | 0c20fb86633104744dbccf30ad732296694fff1b (patch) | |
| tree | 02ffb8494086960b4a84decf3bdc2c8c61bfc4f6 /src/protocol | |
Initial pipewiremain
Diffstat (limited to 'src/protocol')
| -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 |
4 files changed, 1437 insertions, 0 deletions
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); + } +} |
