From 8e54ba5eb502eac6603d5a4143952d7a6e7585ca Mon Sep 17 00:00:00 2001 From: Piyush Thapa Date: Sun, 20 Sep 2026 10:02:02 +0545 Subject: [PATCH] Add support for the Insta360 Link 2C (2e1a:4c03) The Link 2C enumerates as USB 2e1a:4c03, which was not in the product-id allowlist, so discovery rejected it with "No supported Insta360 Link camera found." even though the camera was present and fully functional. Passing --device did not help, since the same check runs on the explicit path. Add a Model::Link2C variant and widen the tracking gate to accept it. The gate change is safe: check_unit still verifies that extension unit 11 carries the AI GUID afterwards, so a device without the unit fails cleanly rather than issuing vendor requests blindly. Verified on real Link 2C hardware (uvcvideo 7.2.5, kernel 7.2.5): linkctl devices -> 1 Insta360 Link 2C /dev/video4 2e1a:4c03 pan -145..145 deg, tilt -90..100 deg, zoom 1.0x..4.0x zoom 2 / right 20 / center / zoom 1 all applied while streaming info reports unit 9 [info/mode], unit 10 [image], unit 11 [ai] with the expected AI GUID, and `tracking` reads state successfully Left is_tested() false for Link2C so the "recognised, not validated" note still prints; flipping it is the maintainer's call. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 1 + contrib/99-insta360-link.rules | 3 ++- src/camera/model.rs | 9 +++++++++ src/commands/tracking.rs | 6 +++--- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9384ca4..b06011f 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 2C | `2e1a:4c03` | recognised, **not tested** | | Insta360 Link (original) | `2e1a:4c01` | recognised, **not tested** | ## Features diff --git a/contrib/99-insta360-link.rules b/contrib/99-insta360-link.rules index 9427f6c..3aceb0c 100644 --- a/contrib/99-insta360-link.rules +++ b/contrib/99-insta360-link.rules @@ -8,6 +8,7 @@ # Install: sudo cp contrib/99-insta360-link.rules /etc/udev/rules.d/ # sudo udevadm control --reload && sudo udevadm trigger # -# Insta360 Link 2 (validated) and original Link (recognised, untested). +# Insta360 Link 2 (validated); Link 2C and original Link (recognised, untested). SUBSYSTEM=="video4linux", ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c04", TAG+="uaccess", GROUP="video", MODE="0660" +SUBSYSTEM=="video4linux", ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c03", 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 0c4a506..9d5dcc6 100644 --- a/src/camera/model.rs +++ b/src/camera/model.rs @@ -6,6 +6,8 @@ use std::fmt; pub const INSTA360_VID: u16 = 0x2e1a; /// USB product id of the Insta360 Link 2 (validated on real hardware). pub const LINK2_PID: u16 = 0x4c04; +/// USB product id of the Insta360 Link 2C (recognised, **not** tested). +pub const LINK2C_PID: u16 = 0x4c03; /// USB product id of the original Insta360 Link (recognised, **not** tested). pub const LINK_PID: u16 = 0x4c01; @@ -15,6 +17,8 @@ pub const LINK_PID: u16 = 0x4c01; pub enum Model { /// Insta360 Link 2 — the primary, hardware-validated target. Link2, + /// Insta360 Link 2C — same control surface as the Link 2; untested. + Link2C, /// Original Insta360 Link — recognised by VID/PID only; untested. Link, } @@ -27,6 +31,7 @@ impl Model { } match pid { LINK2_PID => Some(Model::Link2), + LINK2C_PID => Some(Model::Link2C), LINK_PID => Some(Model::Link), _ => None, } @@ -36,6 +41,7 @@ impl Model { pub fn name(&self) -> &'static str { match self { Model::Link2 => "Insta360 Link 2", + Model::Link2C => "Insta360 Link 2C", Model::Link => "Insta360 Link", } } @@ -59,6 +65,7 @@ mod tests { #[test] 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, 0x4c01), Some(Model::Link)); assert_eq!(Model::from_usb_ids(0x2e1a, 0x0000), None); assert_eq!(Model::from_usb_ids(0x046d, 0x4c04), None); @@ -67,7 +74,9 @@ mod tests { #[test] fn only_link2_is_tested() { assert!(Model::Link2.is_tested()); + assert!(!Model::Link2C.is_tested()); assert!(!Model::Link.is_tested()); assert_eq!(Model::Link2.name(), "Insta360 Link 2"); + assert_eq!(Model::Link2C.name(), "Insta360 Link 2C"); } } diff --git a/src/commands/tracking.rs b/src/commands/tracking.rs index b6b34b6..540d8bc 100644 --- a/src/commands/tracking.rs +++ b/src/commands/tracking.rs @@ -35,12 +35,12 @@ fn emit(ctx: &Context, state: link2::TrackingState, warning: Option) { ); } -/// Make sure the device really is a Link 2 whose unit 11 carries the AI GUID. +/// Make sure the device really is a Link 2 / 2C whose unit 11 carries the AI GUID. fn check_unit(cam: &Camera) -> Result<()> { let info = cam.info(); - if info.model != Model::Link2 { + if !matches!(info.model, Model::Link2 | Model::Link2C) { return Err(Error::Vendor(format!( - "tracking is only implemented for the Insta360 Link 2 (found {})", + "tracking is only implemented for the Insta360 Link 2 / Link 2C (found {})", info.model ))); }