292 lines
12 KiB
Rust
292 lines
12 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
/// Arashi Vision / Insta360 USB Vendor ID (`0x2e1a` = 11802)
|
|
/// Verified at `MacUVCCameraControlImp::GetUVCCameraList` (`0x100191478`)
|
|
/// and `UVCCameraDeviceWatcherImp::StartWatch` (`0x10019e29c`).
|
|
pub const INSTA360_VENDOR_ID: u16 = 0x2e1a;
|
|
|
|
/// Supported USB Product IDs for Insta360 Link family cameras.
|
|
/// Verified at `0x100191480` (`0x4c01..=0x4c06`) and `0x100191664` (`0x00c2`),
|
|
/// as well as `UVCCameraExtendControllerImp::FirmwareUpgrade` jump table at `0x10286f37d`.
|
|
pub mod pids {
|
|
/// Insta360 Link (1st Generation) - `link1`, firmware `a:\Insta360WebCamFW.bin`
|
|
pub const LINK_1: u16 = 0x4c01;
|
|
/// Insta360 PUC3 Gimbal - firmware `a:\Insta360PUC3FW_GIMBAL.bin`
|
|
pub const PUC3_GIMBAL_A: u16 = 0x4c02;
|
|
/// Insta360 Link 2C (Fixed-lens 4K webcam) - `link2_main`, firmware `a:\Insta360LINK2CFW_HOST.bin`
|
|
pub const LINK_2C: u16 = 0x4c03;
|
|
/// Insta360 Link 2 (2nd Gen 2-axis gimbal 4K webcam) - `link2_gimbal`, firmware `a:\Insta360LINK2FW_GIMBAL.bin`
|
|
pub const LINK_2: u16 = 0x4c04;
|
|
/// Insta360 PUC3 Host - firmware `a:\Insta360PUC3FW_HOST.bin`
|
|
pub const PUC3_HOST: u16 = 0x4c05;
|
|
/// Insta360 PUC3 Gimbal Variant - firmware `a:\Insta360PUC3FW_GIMBAL.bin`
|
|
pub const PUC3_GIMBAL_B: u16 = 0x4c06;
|
|
/// Insta360 Link Simple-Class / Firmware Upgrade Mode
|
|
pub const LINK_SIMPLE_CLASS: u16 = 0x00c2;
|
|
}
|
|
|
|
/// Extension Unit 1 GUID: `FAF1672D-B71B-4793-8C91-7B1C9B7F95F8`
|
|
/// Verified at CFString `0x102a9d098` and `FindControllerInterfaceForServiceObject` (`0x100191140`).
|
|
/// Mixed-endian USB UVC `guidExtensionCode` 16-byte representation:
|
|
/// `2d 67 f1 fa 1b b7 93 47 8c 91 7b 1c 9b 7f 95 f8`
|
|
pub const XU1_GUID_STR: &str = "FAF1672D-B71B-4793-8C91-7B1C9B7F95F8";
|
|
pub const XU1_GUID_BYTES: [u8; 16] = [
|
|
0x2d, 0x67, 0xf1, 0xfa, 0x1b, 0xb7, 0x93, 0x47,
|
|
0x8c, 0x91, 0x7b, 0x1c, 0x9b, 0x7f, 0x95, 0xf8,
|
|
];
|
|
|
|
/// Extension Unit 2 GUID: `E307E649-4618-A3FF-82FC-2D8B5F216773`
|
|
/// Verified at CFString `0x102a9d0b8` and `FindControllerInterfaceForServiceObject` (`0x100191168`).
|
|
/// Mixed-endian USB UVC `guidExtensionCode` 16-byte representation:
|
|
/// `49 e6 07 e3 18 46 ff a3 82 fc 2d 8b 5f 21 67 73`
|
|
pub const XU2_GUID_STR: &str = "E307E649-4618-A3FF-82FC-2D8B5F216773";
|
|
pub const XU2_GUID_BYTES: [u8; 16] = [
|
|
0x49, 0xe6, 0x07, 0xe3, 0x18, 0x46, 0xff, 0xa3,
|
|
0x82, 0xfc, 0x2d, 0x8b, 0x5f, 0x21, 0x67, 0x73,
|
|
];
|
|
|
|
/// Default UVC unit IDs observed on Insta360 Link family cameras when descriptors are not manually overridden.
|
|
pub const DEFAULT_CONTROL_INTERFACE: u8 = 0;
|
|
pub const DEFAULT_INPUT_TERMINAL_ID: u8 = 1;
|
|
pub const DEFAULT_PROCESSING_UNIT_ID: u8 = 2;
|
|
pub const DEFAULT_EXTENSION_UNIT_ID: u8 = 9;
|
|
pub const DEFAULT_EXTENSION_UNIT2_ID: u8 = 10;
|
|
|
|
/// Identified Insta360 Link camera model.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum CameraModel {
|
|
/// Insta360 Link (`0x4c01`)
|
|
Link,
|
|
/// Insta360 Link 2 (`0x4c04`)
|
|
Link2,
|
|
/// Insta360 Link 2C (`0x4c03`, gimbal-less 4K)
|
|
Link2C,
|
|
/// Insta360 PUC3 Gimbal (`0x4c02` / `0x4c06`)
|
|
Puc3Gimbal(u16),
|
|
/// Insta360 PUC3 Host (`0x4c05`)
|
|
Puc3Host,
|
|
/// Insta360 Link Simple-Class / DFU (`0x00c2`)
|
|
LinkSimpleClass,
|
|
/// Unknown Insta360 PID
|
|
Unknown(u16),
|
|
}
|
|
|
|
impl CameraModel {
|
|
pub fn from_pid(pid: u16) -> Self {
|
|
match pid {
|
|
pids::LINK_1 => Self::Link,
|
|
pids::LINK_2C => Self::Link2C,
|
|
pids::LINK_2 => Self::Link2,
|
|
pids::PUC3_GIMBAL_A | pids::PUC3_GIMBAL_B => Self::Puc3Gimbal(pid),
|
|
pids::PUC3_HOST => Self::Puc3Host,
|
|
pids::LINK_SIMPLE_CLASS => Self::LinkSimpleClass,
|
|
other => Self::Unknown(other),
|
|
}
|
|
}
|
|
|
|
pub fn pid(&self) -> u16 {
|
|
match *self {
|
|
Self::Link => pids::LINK_1,
|
|
Self::Link2C => pids::LINK_2C,
|
|
Self::Link2 => pids::LINK_2,
|
|
Self::Puc3Gimbal(pid) => pid,
|
|
Self::Puc3Host => pids::PUC3_HOST,
|
|
Self::LinkSimpleClass => pids::LINK_SIMPLE_CLASS,
|
|
Self::Unknown(pid) => pid,
|
|
}
|
|
}
|
|
|
|
/// Returns the internal model identifier used by `Webcam::CameraInsta::GetCameraModel()` (`0x10005f92c`).
|
|
pub fn internal_code(&self) -> &'static str {
|
|
match self {
|
|
Self::Link | Self::LinkSimpleClass => "link1",
|
|
Self::Link2C | Self::Puc3Host => "link2_main",
|
|
Self::Link2 | Self::Puc3Gimbal(_) => "link2_gimbal",
|
|
Self::Unknown(_) => "unknown",
|
|
}
|
|
}
|
|
|
|
/// Returns the human-readable display name.
|
|
pub fn display_name(&self) -> &'static str {
|
|
match self {
|
|
Self::Link => "Insta360 Link",
|
|
Self::Link2 => "Insta360 Link 2",
|
|
Self::Link2C => "Insta360 Link 2C",
|
|
Self::Puc3Gimbal(_) => "Insta360 Link 2 Pro (Gimbal)",
|
|
Self::Puc3Host => "Insta360 Link 2C Pro (Host)",
|
|
Self::LinkSimpleClass => "Insta360 Link (Firmware Mode)",
|
|
Self::Unknown(_) => "Insta360 UVC Camera",
|
|
}
|
|
}
|
|
|
|
/// Returns the target firmware filename on the camera's virtual MSC drive (`0x1001a0a0c`).
|
|
pub fn firmware_filename(&self) -> Option<&'static str> {
|
|
match self {
|
|
Self::Link | Self::LinkSimpleClass => Some(r"a:\Insta360WebCamFW.bin"),
|
|
Self::Link2 => Some(r"a:\Insta360LINK2FW_GIMBAL.bin"),
|
|
Self::Link2C => Some(r"a:\Insta360LINK2CFW_HOST.bin"),
|
|
Self::Puc3Gimbal(_) => Some(r"a:\Insta360PUC3FW_GIMBAL.bin"),
|
|
Self::Puc3Host => Some(r"a:\Insta360PUC3FW_HOST.bin"),
|
|
Self::Unknown(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Whether this camera uses 36-byte extended `SetVideoMode` payloads (`0x1001970f4`: bitmask `0x2b` for `pid - 0x4c01`).
|
|
/// Wait: in `SetVideoMode` (`0x1001970f4`), if `len == 0x24` and `((1 << (pid - 0x4c01)) & 0x2b) != 0`
|
|
/// (`0x4c01`, `0x4c02`, `0x4c04`, `0x4c06` = bits 0, 1, 3, 5 = `0b101011` = `0x2b`),
|
|
/// it sends 8 bytes (`w4 = 8`) instead of 36 bytes (`0x24`), whereas `0x4c03` (Link 2C) and `0x4c05` (PUC3 Host)
|
|
/// send the full 36-byte `SetVideoMode` buffer!
|
|
pub fn video_mode_payload_len(&self) -> usize {
|
|
let pid = self.pid();
|
|
if (0x4c01..=0x4c06).contains(&pid) {
|
|
let bit = 1u32 << (pid - 0x4c01);
|
|
if (bit & 0x2b) != 0 {
|
|
return 8;
|
|
}
|
|
}
|
|
36
|
|
}
|
|
}
|
|
|
|
/// Hardware capability matrix derived from `Webcam::CameraDeviceSupporter::UpdateUIStatus` (`0x10004a8b8`).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct DeviceCapabilities {
|
|
pub has_physical_gimbal: bool,
|
|
pub support_ptz_drag: bool,
|
|
pub support_auto_framing: bool,
|
|
pub support_highlighting: bool,
|
|
pub support_mirror_vertically: bool,
|
|
pub support_track_speed: bool,
|
|
pub support_image_template: bool,
|
|
pub support_privacy_mode: bool,
|
|
pub support_area_track: bool,
|
|
pub support_roll_adjustment: bool,
|
|
pub support_ai_denoise: bool,
|
|
pub support_sound_wall: bool,
|
|
pub support_speak_tracking: bool,
|
|
pub support_auto_track: bool,
|
|
pub support_smart_adjustment: bool,
|
|
pub support_audio_capture_mode: bool,
|
|
pub support_horizontal_correction: bool,
|
|
}
|
|
|
|
impl DeviceCapabilities {
|
|
pub fn for_pid(pid: u16) -> Self {
|
|
let is_not_link2c_or_host = pid != pids::LINK_2C && pid != pids::PUC3_HOST;
|
|
let is_not_link1 = pid != pids::LINK_1;
|
|
let is_link2_or_puc3_gimbal = (pid & 0xfffd) == pids::LINK_2; // 0x4c04 or 0x4c06
|
|
let is_puc3 = pid == pids::PUC3_HOST || pid == pids::PUC3_GIMBAL_B;
|
|
|
|
Self {
|
|
has_physical_gimbal: is_not_link2c_or_host,
|
|
support_ptz_drag: is_not_link2c_or_host,
|
|
support_auto_framing: is_not_link1,
|
|
support_highlighting: is_not_link1,
|
|
support_mirror_vertically: is_not_link1,
|
|
support_track_speed: is_not_link2c_or_host,
|
|
support_image_template: is_not_link1,
|
|
support_privacy_mode: is_not_link1,
|
|
support_area_track: is_link2_or_puc3_gimbal,
|
|
support_roll_adjustment: pid == pids::LINK_1,
|
|
support_ai_denoise: is_not_link1,
|
|
support_sound_wall: is_puc3,
|
|
support_speak_tracking: is_link2_or_puc3_gimbal,
|
|
support_auto_track: is_not_link2c_or_host,
|
|
support_smart_adjustment: is_not_link2c_or_host,
|
|
support_audio_capture_mode: is_not_link1,
|
|
support_horizontal_correction: pid == pids::LINK_2,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Discovered UVC unit IDs and interface number for an Insta360 Link camera.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct UvcUnitIds {
|
|
pub control_interface: u8,
|
|
pub input_terminal_id: u8,
|
|
pub processing_unit_id: u8,
|
|
pub extension_unit_id: u8,
|
|
pub extension_unit2_id: u8,
|
|
}
|
|
|
|
impl Default for UvcUnitIds {
|
|
fn default() -> Self {
|
|
Self {
|
|
control_interface: DEFAULT_CONTROL_INTERFACE,
|
|
input_terminal_id: DEFAULT_INPUT_TERMINAL_ID,
|
|
processing_unit_id: DEFAULT_PROCESSING_UNIT_ID,
|
|
extension_unit_id: DEFAULT_EXTENSION_UNIT_ID,
|
|
extension_unit2_id: DEFAULT_EXTENSION_UNIT2_ID,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses raw USB configuration descriptor bytes to extract UVC Input Terminal, Processing Unit,
|
|
/// and Insta360 Extension Unit 1 & 2 IDs (`FindControllerInterfaceForServiceObject` at `0x100190fe0`).
|
|
pub fn parse_uvc_vc_descriptors(config_desc: &[u8]) -> UvcUnitIds {
|
|
let mut ids = UvcUnitIds::default();
|
|
let mut offset = 0usize;
|
|
let mut in_vc_interface = false;
|
|
|
|
while offset + 2 <= config_desc.len() {
|
|
let b_length = config_desc[offset] as usize;
|
|
let b_descriptor_type = config_desc[offset + 1];
|
|
if b_length < 2 || offset + b_length > config_desc.len() {
|
|
break;
|
|
}
|
|
let desc = &config_desc[offset..offset + b_length];
|
|
|
|
// USB_DT_INTERFACE = 0x04
|
|
if b_descriptor_type == 0x04 && desc.len() >= 9 {
|
|
let b_interface_number = desc[2];
|
|
let b_interface_class = desc[5];
|
|
let b_interface_subclass = desc[6];
|
|
// CC_VIDEO = 0x0e, SC_VIDEOCONTROL = 0x01
|
|
if b_interface_class == 0x0e && b_interface_subclass == 0x01 {
|
|
in_vc_interface = true;
|
|
ids.control_interface = b_interface_number;
|
|
} else {
|
|
in_vc_interface = false;
|
|
}
|
|
} else if in_vc_interface && b_descriptor_type == 0x24 && desc.len() >= 4 {
|
|
// CS_INTERFACE = 0x24
|
|
let b_descriptor_subtype = desc[2];
|
|
let unit_or_terminal_id = desc[3];
|
|
match b_descriptor_subtype {
|
|
// VC_INPUT_TERMINAL = 0x02
|
|
0x02 if desc.len() >= 6 => {
|
|
let w_terminal_type = u16::from_le_bytes([desc[4], desc[5]]);
|
|
// ITT_CAMERA = 0x0201
|
|
if w_terminal_type == 0x0201 || ids.input_terminal_id == DEFAULT_INPUT_TERMINAL_ID {
|
|
ids.input_terminal_id = unit_or_terminal_id;
|
|
}
|
|
}
|
|
// VC_PROCESSING_UNIT = 0x05
|
|
0x05 => {
|
|
ids.processing_unit_id = unit_or_terminal_id;
|
|
}
|
|
// VC_EXTENSION_UNIT = 0x06
|
|
0x06 if desc.len() >= 20 => {
|
|
let guid = &desc[4..20];
|
|
if guid == XU1_GUID_BYTES {
|
|
ids.extension_unit_id = unit_or_terminal_id;
|
|
} else if guid == XU2_GUID_BYTES {
|
|
ids.extension_unit2_id = unit_or_terminal_id;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
offset += b_length;
|
|
}
|
|
|
|
ids
|
|
}
|
|
|
|
/// Returns true if a `(vendor_id, product_id)` pair is an Insta360 Link family device.
|
|
pub fn is_insta360_link_device(vid: u16, pid: u16) -> bool {
|
|
vid == INSTA360_VENDOR_ID
|
|
&& ((pids::LINK_1..=pids::PUC3_GIMBAL_B).contains(&pid) || pid == pids::LINK_SIMPLE_CLASS)
|
|
}
|