From a679e807e39b08ff31b0f528cc75585fca56c57c Mon Sep 17 00:00:00 2001 From: Nicolas Falesy Date: Thu, 1 Oct 2026 09:04:09 -0400 Subject: [PATCH] Add the Insta360 Link 2 Pro (2e1a:4c06) as its own model Follows the Link 2C shape: Model::Link2Pro with LINK2_PRO_PID = 0x4c06, name "Insta360 Link 2 Pro", serialised as "link2pro". It has a gimbal and speaks the Link 2 vendor protocol. Also adds 4c06 to the udev rule and a README row, plus unit tests for id, name, gimbal, protocol, is_tested and serde. is_tested() is true because it was checked on a real Link 2 Pro: devices, status, tracking on/off with readback, and pan/tilt moves read back at the set angles. The opt-in hardware suite (readonly + movement) passes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0147kQSBSeXsP4bzATy45baX --- README.md | 1 + contrib/99-insta360-link.rules | 3 ++- src/camera/model.rs | 33 +++++++++++++++++++++++++++++---- tests/hardware.rs | 2 +- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ceb32e7..701c466 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Python, and never detaches the `uvcvideo` driver. | model | USB id | status | |-------|--------|--------| | Insta360 Link 2 | `2e1a:4c04` | validated on real hardware | +| Insta360 Link 2 Pro | `2e1a:4c06` | validated on real hardware (detection, `tracking on`/`off`, pan/tilt/zoom; hardware test suite passes) | | Insta360 Link 2C | `2e1a:4c03` | zoom and image controls; detection and read-only checks validated; no motorized pan/tilt | | Insta360 Link (original) | `2e1a:4c01` | recognised, **not tested** | diff --git a/contrib/99-insta360-link.rules b/contrib/99-insta360-link.rules index 952580b..c63433f 100644 --- a/contrib/99-insta360-link.rules +++ b/contrib/99-insta360-link.rules @@ -8,7 +8,8 @@ # Install: sudo cp contrib/99-insta360-link.rules /etc/udev/rules.d/ # sudo udevadm control --reload && sudo udevadm trigger # -# Insta360 Link 2 (validated); Link 2C and original Link (recognised, untested). +# Insta360 Link 2 and Link 2 Pro (validated); Link 2C and original Link (recognised, untested). SUBSYSTEM=="video4linux", ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c03", TAG+="uaccess", GROUP="video", MODE="0660" SUBSYSTEM=="video4linux", ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c04", TAG+="uaccess", GROUP="video", MODE="0660" +SUBSYSTEM=="video4linux", ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c06", TAG+="uaccess", GROUP="video", MODE="0660" SUBSYSTEM=="video4linux", ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c01", TAG+="uaccess", GROUP="video", MODE="0660" diff --git a/src/camera/model.rs b/src/camera/model.rs index 5513896..e673fc9 100644 --- a/src/camera/model.rs +++ b/src/camera/model.rs @@ -8,6 +8,8 @@ pub const INSTA360_VID: u16 = 0x2e1a; pub const LINK2_PID: u16 = 0x4c04; /// USB product id of the Insta360 Link 2C. pub const LINK2C_PID: u16 = 0x4c03; +/// USB product id of the Insta360 Link 2 Pro (validated on real hardware). +pub const LINK2_PRO_PID: u16 = 0x4c06; /// USB product id of the original Insta360 Link (recognised, **not** tested). pub const LINK_PID: u16 = 0x4c01; @@ -20,6 +22,10 @@ pub enum Model { /// Insta360 Link 2C — shares the Link 2 command protocol. #[serde(rename = "link2c")] Link2C, + /// Insta360 Link 2 Pro — shares the Link 2 command protocol and has a + /// motorized gimbal; hardware-validated. + #[serde(rename = "link2pro")] + Link2Pro, /// Original Insta360 Link — recognised by VID/PID only; untested. Link, } @@ -33,6 +39,7 @@ impl Model { match pid { LINK2_PID => Some(Model::Link2), LINK2C_PID => Some(Model::Link2C), + LINK2_PRO_PID => Some(Model::Link2Pro), LINK_PID => Some(Model::Link), _ => None, } @@ -43,23 +50,24 @@ impl Model { match self { Model::Link2 => "Insta360 Link 2", Model::Link2C => "Insta360 Link 2C", + Model::Link2Pro => "Insta360 Link 2 Pro", Model::Link => "Insta360 Link", } } /// Whether pan/tilt controls represent a physical gimbal. pub fn has_gimbal(&self) -> bool { - matches!(self, Model::Link2 | Model::Link) + matches!(self, Model::Link2 | Model::Link2Pro | Model::Link) } /// Whether this model uses the Link 2 vendor command protocol. pub fn supports_link2_protocol(&self) -> bool { - matches!(self, Model::Link2 | Model::Link2C) + matches!(self, Model::Link2 | Model::Link2C | Model::Link2Pro) } /// Whether this model has been validated against physical hardware. pub fn is_tested(&self) -> bool { - matches!(self, Model::Link2) + matches!(self, Model::Link2 | Model::Link2Pro) } } @@ -77,8 +85,10 @@ mod tests { fn matches_known_ids() { assert_eq!(Model::from_usb_ids(0x2e1a, 0x4c04), Some(Model::Link2)); assert_eq!(Model::from_usb_ids(0x2e1a, 0x4c03), Some(Model::Link2C)); + assert_eq!(Model::from_usb_ids(0x2e1a, 0x4c06), Some(Model::Link2Pro)); assert_eq!(Model::from_usb_ids(0x2e1a, 0x4c01), Some(Model::Link)); assert_eq!(Model::from_usb_ids(0x046d, 0x4c03), None); + assert_eq!(Model::from_usb_ids(0x046d, 0x4c06), None); assert_eq!(Model::from_usb_ids(0x2e1a, 0x0000), None); assert_eq!(Model::from_usb_ids(0x046d, 0x4c04), None); } @@ -100,8 +110,23 @@ mod tests { } #[test] - fn only_link2_is_tested() { + fn link2_pro_shares_link2_protocol_and_has_gimbal() { + assert!(Model::Link2Pro.supports_link2_protocol()); + assert!(Model::Link2Pro.has_gimbal()); + assert!(Model::Link2Pro.is_tested()); + assert_eq!(Model::Link2Pro.name(), "Insta360 Link 2 Pro"); + assert_eq!(Model::Link2Pro.to_string(), "Insta360 Link 2 Pro"); + assert_eq!( + serde_json::to_string(&Model::Link2Pro).unwrap(), + "\"link2pro\"" + ); + } + + #[test] + fn only_link2_and_link2_pro_are_tested() { assert!(Model::Link2.is_tested()); + assert!(Model::Link2Pro.is_tested()); + assert!(!Model::Link2C.is_tested()); assert!(!Model::Link.is_tested()); assert_eq!(Model::Link2.name(), "Insta360 Link 2"); } diff --git a/tests/hardware.rs b/tests/hardware.rs index d75e64e..9341692 100644 --- a/tests/hardware.rs +++ b/tests/hardware.rs @@ -1,4 +1,4 @@ -//! Opt-in hardware tests against a physically connected Insta360 Link 2 or 2C. +//! Opt-in hardware tests against a physically connected Insta360 Link 2, 2 Pro or 2C. //! //! These are never run by default: //!