Files
insta360-link-controller/crates/insta360-link-protocol/src/ids.rs
T

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)
}