use crate::types::*; use thiserror::Error; #[derive(Debug, Error, Clone, PartialEq)] pub enum CodecError { #[error("payload too short: expected at least {expected} bytes, got {actual}")] PayloadTooShort { expected: usize, actual: usize }, #[error("invalid argument: {0}")] InvalidArgument(&'static str), } /// Encodes UVC `bmRequestType`, `bRequest`, `wValue`, `wIndex` for a UVC control transfer /// matching `MacUVCCameraControl::SetData` (`0x100192880`) and `GetData` (`0x10019282c`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UvcControlPacketHeader { pub bm_request_type: u8, pub b_request: u8, pub w_value: u16, pub w_index: u16, } impl UvcControlPacketHeader { pub fn new(req: UvcRequest, control_selector: u8, unit_or_terminal_id: u8, interface_num: u8) -> Self { let bm_request_type = match req { UvcRequest::SetCur => 0x21, // Host-to-device, Class, Interface (`0x0121` in little-endian u16 with bRequest=0x01) _ => 0xa1, // Device-to-host, Class, Interface }; Self { bm_request_type, b_request: req as u8, w_value: (control_selector as u16) << 8, w_index: ((unit_or_terminal_id as u16) << 8) | (interface_num as u16), } } } // ============================================================================ // XU1 Selector 0x01: System, Presets, Gimbal Reset, Composition Bias // ============================================================================ /// `SetCurPresetPosition(unsigned char)` (`0x100199918`): `[0, index, 0, 0]` pub fn encode_set_cur_preset_position(index: u8) -> [u8; 4] { [0x00, index, 0x00, 0x00] } /// `AddPresetPosition(unsigned char)` (`0x1001999b4`): `[1, index, 0, 0]` pub fn encode_add_preset_position(index: u8) -> [u8; 4] { [0x01, index, 0x00, 0x00] } /// `DeletePresetPosition(unsigned char)` (`0x100199a54`): `[2, index, 0, 0]` pub fn encode_delete_preset_position(index: u8) -> [u8; 4] { [0x02, index, 0x00, 0x00] } /// `ResetGimbal()` (`0x100196fb0`): `[3, 0, 0, 0]` pub fn encode_reset_gimbal() -> [u8; 4] { [0x03, 0x00, 0x00, 0x00] } /// `LoadFactoryDefault()` (`0x10019701c`): `[4, 0, 0, 0]` pub fn encode_load_factory_default() -> [u8; 4] { [0x04, 0x00, 0x00, 0x00] } /// `SetCompositionBias(short, short)` (`0x10019933c`): `[6, x_lo, x_hi, y_lo, y_hi]` pub fn encode_set_composition_bias(x: i16, y: i16) -> [u8; 5] { let xb = x.to_le_bytes(); let yb = y.to_le_bytes(); [0x06, xb[0], xb[1], yb[0], yb[1]] } /// `EnterTrackForbiddenAreaSetting()` (`0x10019bf74`): `[7, 1, 0, 0]` pub fn encode_enter_track_forbidden_area_setting() -> [u8; 4] { [0x07, 0x01, 0x00, 0x00] } /// `ExitTrackForbiddenAreaSetting()` (`0x10019bfb4`): `[8, 1, 0, 0]` pub fn encode_exit_track_forbidden_area_setting() -> [u8; 4] { [0x08, 0x01, 0x00, 0x00] } // ============================================================================ // XU1 Selector 0x02: Video Mode & Auxiliary Data (`0x100197088`, `0x1001978b8`) // ============================================================================ pub fn encode_set_video_mode(mode: VideoMode, aux: &VideoModeAuxData, payload_len: usize) -> Vec { let len = payload_len.clamp(8, 36); let mut buf = vec![0u8; 36]; buf[0] = mode as u8; buf[1] = aux.flags; for (i, &val) in aux.rect.iter().enumerate() { let off = 2 + i * 4; buf[off..off + 4].copy_from_slice(&val.to_le_bytes()); } buf[18..20].copy_from_slice(&aux.pitch.to_le_bytes()); buf[20..22].copy_from_slice(&aux.zoom.to_le_bytes()); buf[22..36].copy_from_slice(&aux.extra); if len == 8 && aux.rect == [0, 0, 0, 0] { buf[2..4].copy_from_slice(&aux.pitch.to_le_bytes()); buf[4..6].copy_from_slice(&aux.zoom.to_le_bytes()); } buf.truncate(len); buf } pub fn decode_get_video_mode(data: &[u8]) -> Result<(VideoMode, VideoModeAuxData), CodecError> { if data.len() < 8 { return Err(CodecError::PayloadTooShort { expected: 8, actual: data.len(), }); } let mode = VideoMode::from_u8(data[0]); let mut aux = VideoModeAuxData { flags: data[1], ..Default::default() }; if data.len() >= 22 { for i in 0..4 { let off = 2 + i * 4; aux.rect[i] = i32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]); } aux.pitch = i16::from_le_bytes([data[18], data[19]]); aux.zoom = u16::from_le_bytes([data[20], data[21]]); } if data.len() >= 36 { aux.extra.copy_from_slice(&data[22..36]); } Ok((mode, aux)) } // ============================================================================ // XU1 Selector 0x03: Tracking Speed, Composition Style, Gestures, Smart Whiteboard // ============================================================================ /// `SetCompositionStyle` (`0x100197498`): 16-byte buffer `[6, style_u8, 0..]` pub fn encode_set_composition_style(style: CompositionStyle) -> [u8; 16] { let mut buf = [0u8; 16]; buf[0] = 0x06; buf[1] = style as u8; buf } /// `GetCompositionStyle` (`0x10019750c`): reads `data[7]` from 16-byte response pub fn decode_get_composition_style(data: &[u8]) -> Result { if data.len() < 8 { return Err(CodecError::PayloadTooShort { expected: 8, actual: data.len(), }); } Ok(CompositionStyle::from_u8(data[7])) } /// `SetTrackSpeed` (`0x1001975bc`): 16-byte buffer `[5, speed_u8, 0..]` pub fn encode_set_track_speed(speed: TrackSpeed) -> [u8; 16] { let mut buf = [0u8; 16]; buf[0] = 0x05; buf[1] = speed as u8; buf } /// `GetTrackSpeed` (`0x100197630`): reads `data[6]` from 16-byte response pub fn decode_get_track_speed(data: &[u8]) -> Result { if data.len() < 7 { return Err(CodecError::PayloadTooShort { expected: 7, actual: data.len(), }); } Ok(TrackSpeed::from_u8(data[6])) } /// `ForceSetSmatrWhiteBoard` (`0x1001980d0`): 16-byte buffer `[3, mode_u8, 0..]` pub fn encode_force_smart_whiteboard(mode: u8) -> [u8; 16] { let mut buf = [0u8; 16]; buf[0] = 0x03; buf[1] = mode; buf } /// `EnableAllGesture` (`0x100197b54`): 16-byte buffer `[0, enable_u8, 0..]` pub fn encode_enable_all_gestures(enable: bool) -> [u8; 16] { let mut buf = [0u8; 16]; buf[0] = 0x00; buf[1] = u8::from(enable); buf } /// `EnableGestureWork` (`0x100197bc8`): /// Reads 6 bytes `[all, ok, palm, l, v, rock]` from selector `0x03`, updates `data[gesture + 1]`, /// and writes `[data[0], data[1], data[2], data[3], data[4], data[5], 0..]` (16B). pub fn encode_enable_gesture_work(current_6b: &[u8], gesture: Gesture, enable: bool) -> [u8; 16] { let mut buf = [0u8; 16]; for i in 0..current_6b.len().min(6) { buf[i] = current_6b[i]; } buf[0] = 0x01; // master gesture enable let idx = (gesture as usize) + 1; if idx < 6 { buf[idx] = u8::from(enable); } buf } /// `GetGestureStatus` (`0x100198144`): decodes `[all, ok, palm, l, v, rock]` from selector `0x03` pub fn decode_gesture_status(data: &[u8]) -> Result<[(Gesture, bool); 5], CodecError> { if data.len() < 6 { return Err(CodecError::PayloadTooShort { expected: 6, actual: data.len(), }); } Ok([ (Gesture::Ok, data[1] != 0), (Gesture::Palm, data[2] != 0), (Gesture::LShape, data[3] != 0), (Gesture::VShape, data[4] != 0), (Gesture::Rock, data[5] != 0), ]) } // ============================================================================ // XU1 Selector 0x04: Audio Noise Reduction & Audio Capture Mode (`0x100197798`, `0x10019bf54`) // ============================================================================ pub fn encode_audio_control(value: u8) -> [u8; 2] { [value, 0x00] } // ============================================================================ // XU1 Selector 0x05 & 0x06 / XU2 Selector 0x0a: Track Object Rectangles (`0x100198a38`) // ============================================================================ pub fn encode_track_rect(rect: &UvcRect) -> [u8; 10] { let mut buf = [0u8; 10]; buf[0..2].copy_from_slice(&rect.x.to_le_bytes()); buf[2..4].copy_from_slice(&rect.y.to_le_bytes()); buf[4..6].copy_from_slice(&rect.width.to_le_bytes()); buf[6..8].copy_from_slice(&rect.height.to_le_bytes()); buf[8..10].copy_from_slice(&rect.track_id.to_le_bytes()); buf } pub fn decode_track_rect_list(data: &[u8]) -> Vec { let mut rects = Vec::new(); for chunk in data.chunks_exact(10) { let x = i16::from_le_bytes([chunk[0], chunk[1]]); let y = i16::from_le_bytes([chunk[2], chunk[3]]); let width = i16::from_le_bytes([chunk[4], chunk[5]]); let height = i16::from_le_bytes([chunk[6], chunk[7]]); let track_id = i16::from_le_bytes([chunk[8], chunk[9]]); if width > 0 || height > 0 { rects.push(UvcRect { x, y, width, height, track_id, }); } } rects } // ============================================================================ // XU1 Selector 0x07: Exposure Curve, Auto Exposure, ISO (`0x10019aaa0`–`0x10019b7d0`) // ============================================================================ pub fn encode_exposure_curve(curve: &ExposureCurve) -> [u8; 259] { let mut buf = [0u8; 259]; buf[0] = curve.mode; buf[1..3].copy_from_slice(&curve.iso.to_le_bytes()); for (i, &pt) in curve.points.iter().take(128).enumerate() { let off = 3 + i * 2; buf[off..off + 2].copy_from_slice(&pt.to_le_bytes()); } buf } pub fn decode_exposure_curve(data: &[u8]) -> Result { if data.len() < 3 { return Err(CodecError::PayloadTooShort { expected: 3, actual: data.len(), }); } let mode = data[0]; let iso = u16::from_le_bytes([data[1], data[2]]); let mut points = Vec::with_capacity(128); for chunk in data[3..].chunks_exact(2).take(128) { points.push(u16::from_le_bytes([chunk[0], chunk[1]])); } Ok(ExposureCurve { mode, iso, points }) } // ============================================================================ // XU1 Selector 0x08: Exposure Compensation (`0x100198f34`) // ============================================================================ pub fn encode_exposure_compensation(ev: f32) -> [u8; 4] { ev.to_le_bytes() } pub fn decode_exposure_compensation(data: &[u8]) -> Result { if data.len() < 4 { return Err(CodecError::PayloadTooShort { expected: 4, actual: data.len(), }); } Ok(f32::from_le_bytes([data[0], data[1], data[2], data[3]])) } // ============================================================================ // XU1 Selector 0x0a: Extended Function Bitmask (`0x100199afc`–`0x10019a530`) // ============================================================================ /// Encodes `(mask: u32, values: u32)` into the 8-byte payload for XU1 selector `0x0a`. /// Verified at `EnableExtendFuncWork` (`0x100199afc`), `EnableVerticalScreen` (`0x10019b7d0`), /// and `EnableLowResolution` (`0x10019b854`). pub fn encode_extend_func_bitmask(func: ExtendFunction, enable: bool, current_values: u32) -> [u8; 8] { let bit = 1u32 << (func as u8); let new_values = if enable { current_values | bit } else { current_values & !bit }; let mut buf = [0u8; 8]; buf[0..4].copy_from_slice(&bit.to_le_bytes()); buf[4..8].copy_from_slice(&new_values.to_le_bytes()); buf } /// Decodes the response from `GetExtendFuncStatus` (`0x100199a20`, XU1 selector `0x1b` or `0x0a`). /// Hardware returns a 2-byte `u16` LE bitmask at `data[0..2]` (or 8-byte `(mask, values)` in 8B format). pub fn decode_extend_func_status(data: &[u8]) -> Result, CodecError> { if data.len() < 2 { return Err(CodecError::PayloadTooShort { expected: 2, actual: data.len(), }); } let bits = if data.len() >= 8 && (data[4] != 0 || data[5] != 0 || (data[0] == 0xff && data[1] == 0xff)) { u32::from_le_bytes([data[4], data[5], data[6], data[7]]) } else { u16::from_le_bytes([data[0], data[1]]) as u32 }; let mut out = Vec::with_capacity(ExtendFunction::ALL.len()); for func in ExtendFunction::ALL { let enabled = (bits & (1u32 << (func as u8))) != 0; out.push((func, enabled)); } Ok(out) } // ============================================================================ // XU1 Selector 0x0b: Relative Pan/Tilt (`0x1001990f4`) // ============================================================================ pub fn encode_pantilt_relative_ext(pan_speed: u8, pan_dir: i8, tilt_speed: u8, tilt_dir: i8) -> [u8; 4] { let p_dir: u8 = if pan_dir == 0 || pan_speed == 0 { 0 } else if pan_dir > 0 { 1 } else { 0xff }; let t_dir: u8 = if tilt_dir == 0 || tilt_speed == 0 { 0 } else if tilt_dir > 0 { 1 } else { 0xff }; let p_spd = if p_dir == 0 { 1 } else { pan_speed.clamp(1, 10) }; let t_spd = if t_dir == 0 { 1 } else { tilt_speed.clamp(1, 10) }; [p_dir, p_spd, t_dir, t_spd] } // ============================================================================ // XU1 Selector 0x0d: PTZ Version & Gimbal Absolute Angles (`0x1001993bc`, `0x1001996cc`) // ============================================================================ pub fn decode_ptz_version_and_abs(data: &[u8]) -> Result { if data.len() < 30 { return Err(CodecError::PayloadTooShort { expected: 30, actual: data.len(), }); } let end = data[..20].iter().position(|&b| b == 0).unwrap_or(20); let version = String::from_utf8_lossy(&data[..end]).into_owned(); let pan = i16::from_le_bytes([data[20], data[21]]); let tilt = i16::from_le_bytes([data[22], data[23]]); let zoom = u16::from_le_bytes([data[24], data[25]]); let status = data[26]; Ok(PtzVersionInfo { version, pan, tilt, zoom, status, }) } // ============================================================================ // XU1 Selector 0x0e: Preset Position Flags (`0x1001997c0`) // ============================================================================ pub fn decode_preset_flags(data: &[u8]) -> Result { if data.len() < 8 { return Err(CodecError::PayloadTooShort { expected: 8, actual: data.len(), }); } let mut raw = [0u8; 8]; raw.copy_from_slice(&data[..8]); Ok(PresetFlag { slot_mask: raw[0], current_slot: raw[1], max_slots: raw[2].max(6), raw, }) } // ============================================================================ // XU2 Selector 0x02: Timestamp, IQ Switch, Factory Restore (`0x10019bd00`–`0x10019c1ac`) // ============================================================================ /// `SetTimestamp(unsigned long long)` (`0x10019be40`): `[1, ts_le[0..8], 0..]` (10 bytes) pub fn encode_set_timestamp(timestamp_ms: u64) -> [u8; 10] { let mut buf = [0u8; 10]; buf[0] = 0x01; buf[1..9].copy_from_slice(×tamp_ms.to_le_bytes()); buf } /// `SetIQSwitch(int, bool)` (`0x100199ce0`): `[2, mask_u32_le[0..4], 0..]` (10 bytes) pub fn encode_set_iq_switch(bit_index: u8, enable: bool, current_mask: u32) -> Result<[u8; 10], CodecError> { if bit_index >= 32 { return Err(CodecError::InvalidArgument("bitset argument out of range")); } let bit = 1u32 << bit_index; let next = if enable { current_mask | bit } else { current_mask & !bit }; let mut buf = [0u8; 10]; buf[0] = 0x02; buf[1..5].copy_from_slice(&next.to_le_bytes()); Ok(buf) } /// `RestoreFactorySet(bool)` (`0x10019c1ac`): `[4, 1, 0..]` if `reboot` else `[5, 1, 0..]` (10 bytes) pub fn encode_restore_factory_set(full_reboot: bool) -> [u8; 10] { let mut buf = [0u8; 10]; buf[0] = if full_reboot { 0x04 } else { 0x05 }; buf[1] = 0x01; buf } // ============================================================================ // XU2 Selector 0x03: DeviceInfo (`GetDeviceInfo` at `0x100196e74`, 150 bytes) // ============================================================================ fn extract_cstr(slice: &[u8]) -> String { let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len()); String::from_utf8_lossy(&slice[..end]).trim().to_string() } pub fn decode_device_info(data: &[u8]) -> Result { if data.len() < 150 { return Err(CodecError::PayloadTooShort { expected: 150, actual: data.len(), }); } // Hardware layout (`MacUVCCameraExtendController::GetDeviceInfo` at `0x100196904`, >= 161 bytes): // [0x00..0x20] = serial_num (32B, e.g. "0AB586531221284"), [0x20] = need_update (1B), // [0x21..0x61] = device_uuid (64B), [0x61..0xa1] = cur_version (64B, e.g. "v1.4.5.8_build1") if data.len() >= 0xa1 { let sn = extract_cstr(&data[0x00..0x20]); let uuid = extract_cstr(&data[0x21..0x61]); let fw = extract_cstr(&data[0x61..0xa1]); if !fw.is_empty() || !sn.is_empty() || !uuid.is_empty() { return Ok(DeviceInfo { device_name: uuid, firmware_version: fw, serial_number: sn, }); } } Ok(DeviceInfo { device_name: extract_cstr(&data[0..50]), firmware_version: extract_cstr(&data[50..100]), serial_number: extract_cstr(&data[100..150]), }) } // ============================================================================ // XU2 Selector 0x04 & 0x05: Image Templates (`0x10019ba4c`–`0x10019bd90`) // ============================================================================ pub fn decode_image_template_info(data: &[u8]) -> Result { if data.len() < 11 { return Err(CodecError::PayloadTooShort { expected: 11, actual: data.len(), }); } let current_id = data[0] as i8; let operation = data[1]; let bitmask = u64::from_le_bytes([ data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], ]); let max_templates = data[10]; Ok(ImageTemplateInfo { current_id, operation, bitmask, max_templates, }) } pub fn encode_set_image_template(template_id: i8, op: ImageOperation, current_mask: u64, max_templates: u8) -> Result<[u8; 11], CodecError> { if template_id < 0 || (template_id as u8) >= 64 { return Err(CodecError::InvalidArgument("template_id out of range (0..64)")); } let next_mask = match op { ImageOperation::SaveOrUpdate => current_mask | (1u64 << (template_id as u8)), _ => current_mask, }; let mut buf = [0u8; 11]; buf[0] = template_id as u8; buf[1] = op as u8; buf[2..10].copy_from_slice(&next_mask.to_le_bytes()); buf[10] = max_templates; Ok(buf) } pub fn encode_delete_image_template(template_id: i8, current: &ImageTemplateInfo) -> Result<[u8; 11], CodecError> { if template_id < 0 || (template_id as u8) >= 64 { return Err(CodecError::InvalidArgument("template_id out of range (0..64)")); } let next_mask = current.bitmask & !(1u64 << (template_id as u8)); let mut buf = [0u8; 11]; buf[0] = current.current_id as u8; buf[1] = current.operation; buf[2..10].copy_from_slice(&next_mask.to_le_bytes()); buf[10] = current.max_templates; Ok(buf) } pub fn encode_image_template_name(template_id: i8, name: &str) -> [u8; 65] { let mut buf = [0u8; 65]; buf[0] = template_id as u8; let bytes = name.as_bytes(); let copy_len = bytes.len().min(63); buf[1..1 + copy_len].copy_from_slice(&bytes[..copy_len]); buf } pub fn decode_image_template_name(data: &[u8]) -> Result<(i8, String), CodecError> { if data.len() < 2 { return Err(CodecError::PayloadTooShort { expected: 2, actual: data.len(), }); } let id = data[0] as i8; let name = extract_cstr(&data[1..]); Ok((id, name)) } // ============================================================================ // XU2 Selector 0x08: Privacy Mode & Fill Light (`0x10019c0a8`–`0x10019c168`) // ============================================================================ pub fn encode_set_privacy_mode(enable: bool) -> [u8; 4] { [0x02, u8::from(enable), 0x00, 0x00] } pub fn decode_privacy_mode(data: &[u8]) -> Result<(i32, bool), CodecError> { if data.len() < 2 { return Err(CodecError::PayloadTooShort { expected: 2, actual: data.len(), }); } Ok((data[0] as i32, data[1] != 0)) } // ============================================================================ // XU2 Selector 0x09: Host Digital PTZ (`SetHostPTZ` at `0x10019bec0`) // ============================================================================ pub fn encode_host_ptz(info: &HostPtzInfo) -> [u8; 8] { let mut buf = [0u8; 8]; buf[0..2].copy_from_slice(&info.x.to_le_bytes()); buf[2..4].copy_from_slice(&info.y.to_le_bytes()); buf[4..6].copy_from_slice(&info.width.to_le_bytes()); buf[6..8].copy_from_slice(&info.height.to_le_bytes()); buf } // ============================================================================ // XU2 Selector 0x01: Gesture Binding Map (`GetGestureBindingMap` / `SetGestureBindingFunc` at `0x100198384`) // ============================================================================ pub fn encode_gesture_binding(gesture: Gesture, func: GestureFunction) -> [u8; 16] { let mut buf = [0u8; 16]; buf[0] = gesture as u8; buf[1] = func as u8; buf } pub fn decode_gesture_binding_map(data: &[u8]) -> Result<[(Gesture, GestureFunction); 5], CodecError> { if data.len() < 5 { return Err(CodecError::PayloadTooShort { expected: 5, actual: data.len(), }); } Ok([ (Gesture::Ok, GestureFunction::from_u8(data[0])), (Gesture::Palm, GestureFunction::from_u8(data[1])), (Gesture::LShape, GestureFunction::from_u8(data[2])), (Gesture::VShape, GestureFunction::from_u8(data[3])), (Gesture::Rock, GestureFunction::from_u8(data[4])), ]) } // ============================================================================ // XU2 Selector 0x0b: Tracking Forbidden Areas (`SetTrackForbiddenArea` / `GetTrackForbiddenArea` at `0x10019bff4`) // ============================================================================ pub fn encode_forbidden_areas(areas: &[ForbiddenAreaInfo]) -> Vec { let count = areas.len().clamp(1, 4); let mut buf = vec![0u8; count * 10]; for (i, area) in areas.iter().take(count).enumerate() { let off = i * 10; buf[off] = u8::from(area.enabled); buf[off + 1] = area.index; buf[off + 2..off + 4].copy_from_slice(&area.pan_min.to_le_bytes()); buf[off + 4..off + 6].copy_from_slice(&area.pan_max.to_le_bytes()); buf[off + 6..off + 8].copy_from_slice(&area.tilt_min.to_le_bytes()); buf[off + 8..off + 10].copy_from_slice(&area.tilt_max.to_le_bytes()); } buf } pub fn decode_forbidden_areas(data: &[u8]) -> Vec { let mut out = Vec::new(); for chunk in data.chunks_exact(10) { out.push(ForbiddenAreaInfo { enabled: chunk[0] != 0, index: chunk[1], pan_min: i16::from_le_bytes([chunk[2], chunk[3]]), pan_max: i16::from_le_bytes([chunk[4], chunk[5]]), tilt_min: i16::from_le_bytes([chunk[6], chunk[7]]), tilt_max: i16::from_le_bytes([chunk[8], chunk[9]]), }); } out } #[cfg(test)] mod tests { use super::*; use crate::ids::CameraModel; #[test] fn test_uvc_header_encoding() { let set_hdr = UvcControlPacketHeader::new(UvcRequest::SetCur, 0x02, 0x09, 0x00); assert_eq!(set_hdr.bm_request_type, 0x21); assert_eq!(set_hdr.b_request, 0x01); assert_eq!(set_hdr.w_value, 0x0200); assert_eq!(set_hdr.w_index, 0x0900); let get_hdr = UvcControlPacketHeader::new(UvcRequest::GetCur, 0x03, 0x0a, 0x00); assert_eq!(get_hdr.bm_request_type, 0xa1); assert_eq!(get_hdr.b_request, 0x81); assert_eq!(get_hdr.w_value, 0x0300); assert_eq!(get_hdr.w_index, 0x0a00); } #[test] fn test_video_mode_payload_lengths() { assert_eq!(CameraModel::Link.video_mode_payload_len(), 8); assert_eq!(CameraModel::Link2.video_mode_payload_len(), 8); assert_eq!(CameraModel::Link2C.video_mode_payload_len(), 36); assert_eq!(CameraModel::Puc3Host.video_mode_payload_len(), 36); } #[test] fn test_extend_func_bitmask_roundtrip() { let encoded = encode_extend_func_bitmask(ExtendFunction::Hdr, true, 0); assert_eq!(u32::from_le_bytes(encoded[0..4].try_into().unwrap()), 1 << 2); assert_eq!(u32::from_le_bytes(encoded[4..8].try_into().unwrap()), 1 << 2); let decoded = decode_extend_func_status(&encoded).unwrap(); let hdr_state = decoded.into_iter().find(|(f, _)| *f == ExtendFunction::Hdr).unwrap().1; assert!(hdr_state); } #[test] fn test_device_info_decode() { let mut raw = [0u8; 150]; raw[0..15].copy_from_slice(b"Insta360 Link 2"); raw[50..65].copy_from_slice(b"v2.5.8.8_build1"); raw[100..114].copy_from_slice(b"INK2A240912345"); let info = decode_device_info(&raw).unwrap(); assert_eq!(info.device_name, "Insta360 Link 2"); assert_eq!(info.firmware_version, "v2.5.8.8_build1"); assert_eq!(info.serial_number, "INK2A240912345"); } }