Remove udev rules, add GNOME desktop entry & SVG icon, and support V4L2-only open on Linux
This commit is contained in:
7 files changed
+165
-94
No files matched your search
@@ -41,9 +41,6 @@ This project includes a [`mise.toml`](mise.toml) configuration that manages the
|
|||||||
mise trust
|
mise trust
|
||||||
mise install
|
mise install
|
||||||
|
|
||||||
# Install Linux udev rules (one-time setup on Linux for non-root USB access)
|
|
||||||
mise run udev-install
|
|
||||||
|
|
||||||
# Build & launch the desktop GUI
|
# Build & launch the desktop GUI
|
||||||
mise run gui
|
mise run gui
|
||||||
|
|
||||||
@@ -70,28 +67,18 @@ The compiled binaries will be placed in `target/release/`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Platform Setup Instructions
|
## Platform Notes
|
||||||
|
|
||||||
### Linux (`udev` Rules)
|
### Linux (`V4L2` & `uvcvideo`)
|
||||||
|
|
||||||
On Linux, `nusb` communicates directly with the kernel `usbfs` device node (`/dev/bus/usb/BBB/DDD`) and detaches/claims only the UVC VideoControl interface while leaving the V4L2 video streaming interface (`/dev/video*`) untouched for OBS, Zoom, or browsers.
|
On Linux, `insta360-link-protocol` communicates directly with the primary V4L2 video capture node (`/dev/video*`) using `UVCIOC_CTRL_QUERY` for proprietary Insta360 Extension Unit (`XU1`/`XU2`) commands and standard V4L2 `VIDIOC_G_CTRL`/`VIDIOC_S_CTRL` ioctls for Processing Unit / Camera Terminal controls, while reading USB descriptors from sysfs. Because `systemd-logind` automatically grants the active desktop user session access to `/dev/video*`, **no custom `udev` rules or `sudo` privileges are required**.
|
||||||
|
|
||||||
To run the CLI, server, and GUI without `sudo`, install the bundled `udev` rules:
|
#### Arch Linux (AUR)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cp udev/99-insta360-link.rules /etc/udev/rules.d/99-insta360-link.rules
|
yay -S insta360-link-controller-bin
|
||||||
sudo udevadm control --reload-rules
|
|
||||||
sudo udevadm trigger
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Unplug and reconnect your Insta360 Link camera if it is already connected.
|
|
||||||
|
|
||||||
### Windows (`WinUSB` / Zadig Note)
|
|
||||||
|
|
||||||
On Windows, the standard `usbvideo.sys` UVC driver claims the composite video interfaces. To allow direct USB control transfers via `nusb`:
|
|
||||||
- Use [Zadig](https://zadig.akeo.ie/) (Options → **List All Devices**, uncheck **Ignore Hubs or Composite Parents**) to bind the camera's **UVC VideoControl interface (Interface 0)** to the **WinUSB** driver while leaving the VideoStreaming interface on `usbvideo.sys`, **or**
|
|
||||||
- Run with `--mock` to test the CLI, server, and GUI using the built-in hardware simulator.
|
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
On macOS, `nusb` uses `IOKit` to send UVC class-specific control requests directly to Endpoint 0 without detaching the macOS `UVCAssistant` / `AVFoundation` camera stream. No kernel extensions or extra permissions are required.
|
On macOS, `nusb` uses `IOKit` to send UVC class-specific control requests directly to Endpoint 0 without detaching the macOS `UVCAssistant` / `AVFoundation` camera stream. No kernel extensions or extra permissions are required.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Name=Insta360 Link Controller
|
||||||
|
GenericName=Webcam Controller
|
||||||
|
Comment=Hardware PTZ, AI tracking, image settings, and live preview controller for Insta360 Link webcams
|
||||||
|
Exec=insta360-link-gui
|
||||||
|
TryExec=insta360-link-gui
|
||||||
|
Icon=insta360-link-controller
|
||||||
|
Terminal=false
|
||||||
|
Categories=AudioVideo;Video;Settings;HardwareSettings;
|
||||||
|
Keywords=insta360;link;webcam;camera;ptz;gimbal;uvc;video;tracking;
|
||||||
|
StartupNotify=true
|
||||||
|
StartupWMClass=insta360-link-controller
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#181c24"/>
|
||||||
|
<stop offset="100%" stop-color="#0e1117"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#00e676"/>
|
||||||
|
<stop offset="100%" stop-color="#00b248"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="8" y="8" width="112" height="112" rx="26" fill="url(#bg)" stroke="#2a3140" stroke-width="3"/>
|
||||||
|
<rect x="34" y="24" width="60" height="46" rx="14" fill="#232936" stroke="url(#ring)" stroke-width="4"/>
|
||||||
|
<circle cx="64" cy="47" r="14" fill="#0d1016" stroke="#00e676" stroke-width="3"/>
|
||||||
|
<circle cx="64" cy="47" r="6" fill="#00e676"/>
|
||||||
|
<path d="M52 70 L52 86 L76 86 L76 70" fill="#232936" stroke="#3a4254" stroke-width="3" stroke-linejoin="round"/>
|
||||||
|
<rect x="36" y="86" width="56" height="14" rx="7" fill="#2b3242" stroke="#00e676" stroke-width="2.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -2393,12 +2393,62 @@ impl eframe::App for Insta360App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_app_icon() -> egui::IconData {
|
||||||
|
let w = 64u32;
|
||||||
|
let h = 64u32;
|
||||||
|
let mut rgba = vec![0u8; (w * h * 4) as usize];
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let idx = ((y * w + x) * 4) as usize;
|
||||||
|
let dx = x as f32 - 31.5_f32;
|
||||||
|
let dy = y as f32 - 26.0_f32;
|
||||||
|
let dist = (dx * dx + dy * dy).sqrt();
|
||||||
|
// Dark rounded card background
|
||||||
|
let in_card = x >= 4 && x < 60 && y >= 4 && y < 60;
|
||||||
|
if !in_card {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (mut r, mut g, mut b) = (20u8, 24u8, 32u8);
|
||||||
|
// Green camera ring
|
||||||
|
if (10.5_f32..=14.0_f32).contains(&dist) {
|
||||||
|
r = 0;
|
||||||
|
g = 230;
|
||||||
|
b = 118;
|
||||||
|
} else if dist < 5.0_f32 {
|
||||||
|
r = 0;
|
||||||
|
g = 230;
|
||||||
|
b = 118;
|
||||||
|
} else if dist < 10.5_f32 {
|
||||||
|
r = 12;
|
||||||
|
g = 15;
|
||||||
|
b = 20;
|
||||||
|
} else if (18..=46).contains(&x) && (44..=52).contains(&y) {
|
||||||
|
// Gimbal base
|
||||||
|
r = 43;
|
||||||
|
g = 50;
|
||||||
|
b = 66;
|
||||||
|
}
|
||||||
|
rgba[idx] = r;
|
||||||
|
rgba[idx + 1] = g;
|
||||||
|
rgba[idx + 2] = b;
|
||||||
|
rgba[idx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
egui::IconData {
|
||||||
|
rgba,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> eframe::Result<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// Matching webcam.qml: width: 1440, height: 714, minimumWidth: 1120, minimumHeight: 536
|
// Matching webcam.qml: width: 1440, height: 714, minimumWidth: 1120, minimumHeight: 536
|
||||||
let native_options = eframe::NativeOptions {
|
let native_options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_app_id("insta360-link-controller")
|
||||||
|
.with_icon(build_app_icon())
|
||||||
.with_inner_size([1440.0_f32, 714.0_f32])
|
.with_inner_size([1440.0_f32, 714.0_f32])
|
||||||
.with_min_inner_size([1120.0_f32, 536.0_f32])
|
.with_min_inner_size([1120.0_f32, 536.0_f32])
|
||||||
.with_title("Insta360 Link Controller"),
|
.with_title("Insta360 Link Controller"),
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ pub fn list_connected_cameras() -> Result<Vec<DiscoveredCamera>, TransportError>
|
|||||||
unit_ids = parse_uvc_vc_descriptors(raw);
|
unit_ids = parse_uvc_vc_descriptors(raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if unit_ids == UvcUnitIds::default() {
|
||||||
|
if let Some((_, parsed_ids)) = open_linux_v4l2_node(vid, pid, unit_ids.extension_unit_id) {
|
||||||
|
unit_ids = parsed_ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out.push(DiscoveredCamera {
|
out.push(DiscoveredCamera {
|
||||||
vendor_id: vid,
|
vendor_id: vid,
|
||||||
@@ -89,10 +95,10 @@ pub fn list_connected_cameras() -> Result<Vec<DiscoveredCamera>, TransportError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Pure-Rust cross-platform USB hardware transport backed by `nusb`
|
/// Pure-Rust cross-platform USB hardware transport backed by `nusb`
|
||||||
/// (Linux `uvcvideo` `UVCIOC_CTRL_QUERY` + `usbfs`, Windows `WinUSB`, macOS `IOKit`).
|
/// (Linux `uvcvideo` `UVCIOC_CTRL_QUERY` + V4L2 ioctls, Windows `WinUSB`, macOS `IOKit`).
|
||||||
pub struct NusbTransport {
|
pub struct NusbTransport {
|
||||||
info: DiscoveredCamera,
|
info: DiscoveredCamera,
|
||||||
device: nusb::Device,
|
device: Option<nusb::Device>,
|
||||||
interface: Option<nusb::Interface>,
|
interface: Option<nusb::Interface>,
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
v4l_file: Option<std::fs::File>,
|
v4l_file: Option<std::fs::File>,
|
||||||
@@ -100,7 +106,7 @@ pub struct NusbTransport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn open_linux_v4l2_node(vid: u16, pid: u16, xu1_id: u8) -> Option<std::fs::File> {
|
fn open_linux_v4l2_node(vid: u16, pid: u16, xu1_id: u8) -> Option<(std::fs::File, UvcUnitIds)> {
|
||||||
let vid_hex = format!("{:04x}", vid);
|
let vid_hex = format!("{:04x}", vid);
|
||||||
let pid_hex = format!("{:04x}", pid);
|
let pid_hex = format!("{:04x}", pid);
|
||||||
let entries = std::fs::read_dir("/sys/class/video4linux").ok()?;
|
let entries = std::fs::read_dir("/sys/class/video4linux").ok()?;
|
||||||
@@ -115,13 +121,13 @@ fn open_linux_v4l2_node(vid: u16, pid: u16, xu1_id: u8) -> Option<std::fs::File>
|
|||||||
}
|
}
|
||||||
candidates.sort_by_key(|(num, _, _)| *num);
|
candidates.sort_by_key(|(num, _, _)| *num);
|
||||||
|
|
||||||
let mut fallback: Option<std::fs::File> = None;
|
let mut fallback: Option<(std::fs::File, UvcUnitIds)> = None;
|
||||||
for (_, path, name) in candidates {
|
for (_, path, name) in candidates {
|
||||||
let dev_link = path.join("device");
|
let dev_link = path.join("device");
|
||||||
let Ok(canon) = std::fs::canonicalize(&dev_link) else {
|
let Ok(canon) = std::fs::canonicalize(&dev_link) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// Parent directory of the USB interface (`B-P:1.0`) holds `idVendor` and `idProduct`.
|
// Parent directory of the USB interface (`B-P:1.0`) holds `idVendor`, `idProduct`, and `descriptors`.
|
||||||
if let Some(usb_dev_dir) = canon.parent() {
|
if let Some(usb_dev_dir) = canon.parent() {
|
||||||
let v = std::fs::read_to_string(usb_dev_dir.join("idVendor"))
|
let v = std::fs::read_to_string(usb_dev_dir.join("idVendor"))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -132,6 +138,23 @@ fn open_linux_v4l2_node(vid: u16, pid: u16, xu1_id: u8) -> Option<std::fs::File>
|
|||||||
.trim()
|
.trim()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
if v == vid_hex && p == pid_hex {
|
if v == vid_hex && p == pid_hex {
|
||||||
|
let parsed_ids = std::fs::read(usb_dev_dir.join("descriptors"))
|
||||||
|
.ok()
|
||||||
|
.map(|raw| {
|
||||||
|
// Skip the 18-byte USB Device Descriptor if present at offset 0
|
||||||
|
let slice = if raw.len() > 18 && raw[0] == 18 && raw[1] == 1 {
|
||||||
|
&raw[18..]
|
||||||
|
} else {
|
||||||
|
&raw[..]
|
||||||
|
};
|
||||||
|
parse_uvc_vc_descriptors(slice)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let probe_xu1 = if parsed_ids.extension_unit_id != 0 {
|
||||||
|
parsed_ids.extension_unit_id
|
||||||
|
} else {
|
||||||
|
xu1_id
|
||||||
|
};
|
||||||
let dev_path = format!("/dev/{}", name);
|
let dev_path = format!("/dev/{}", name);
|
||||||
if let Ok(file) = std::fs::OpenOptions::new()
|
if let Ok(file) = std::fs::OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
@@ -143,17 +166,17 @@ fn open_linux_v4l2_node(vid: u16, pid: u16, xu1_id: u8) -> Option<std::fs::File>
|
|||||||
let mut len_buf = [0u8; 2];
|
let mut len_buf = [0u8; 2];
|
||||||
if linux_uvcioc_ctrl_query(
|
if linux_uvcioc_ctrl_query(
|
||||||
&file,
|
&file,
|
||||||
xu1_id,
|
probe_xu1,
|
||||||
0x03,
|
0x03,
|
||||||
UvcRequest::GetLen as u8,
|
UvcRequest::GetLen as u8,
|
||||||
&mut len_buf,
|
&mut len_buf,
|
||||||
)
|
)
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
return Some(file);
|
return Some((file, parsed_ids));
|
||||||
}
|
}
|
||||||
if fallback.is_none() {
|
if fallback.is_none() {
|
||||||
fallback = Some(file);
|
fallback = Some((file, parsed_ids));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,25 +375,49 @@ impl NusbTransport {
|
|||||||
})
|
})
|
||||||
.ok_or(TransportError::DeviceNotFound)?;
|
.ok_or(TransportError::DeviceNotFound)?;
|
||||||
|
|
||||||
let device = dev_info.open().map_err(|e| TransportError::Usb(e.to_string()))?;
|
let device = dev_info.open().ok();
|
||||||
if let Ok(cfg) = device.active_configuration() {
|
if let Some(ref dev) = device {
|
||||||
let raw: &[u8] = cfg.descriptors().as_bytes();
|
if let Ok(cfg) = dev.active_configuration() {
|
||||||
info.unit_ids = parse_uvc_vc_descriptors(raw);
|
let raw: &[u8] = cfg.descriptors().as_bytes();
|
||||||
|
info.unit_ids = parse_uvc_vc_descriptors(raw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claim the UVC VideoControl interface without detaching the OS kernel UVC video streaming
|
// Claim the UVC VideoControl interface without detaching the OS kernel UVC video streaming
|
||||||
// driver (`uvcvideo` on Linux, `UVCAssistant` on macOS, `usbvideo.sys` on Windows) so that
|
// driver (`uvcvideo` on Linux, `UVCAssistant` on macOS, `usbvideo.sys` on Windows) so that
|
||||||
// live webcam video capture and UVC/XU control transfers operate simultaneously.
|
// live webcam video capture and UVC/XU control transfers operate simultaneously.
|
||||||
let interface = device
|
let interface = device
|
||||||
.claim_interface(info.unit_ids.control_interface)
|
.as_ref()
|
||||||
.ok();
|
.and_then(|d| d.claim_interface(info.unit_ids.control_interface).ok());
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let v4l_file = open_linux_v4l2_node(
|
let v4l_file = match open_linux_v4l2_node(
|
||||||
info.vendor_id,
|
info.vendor_id,
|
||||||
info.product_id,
|
info.product_id,
|
||||||
info.unit_ids.extension_unit_id,
|
info.unit_ids.extension_unit_id,
|
||||||
);
|
) {
|
||||||
|
Some((f, parsed_ids)) => {
|
||||||
|
if device.is_none() {
|
||||||
|
info.unit_ids = parsed_ids;
|
||||||
|
}
|
||||||
|
Some(f)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if device.is_none() && v4l_file.is_none() {
|
||||||
|
return Err(TransportError::Usb(
|
||||||
|
"Could not open V4L2 device node or USB device".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
if device.is_none() {
|
||||||
|
return Err(TransportError::Usb(
|
||||||
|
"Could not open USB device".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
info,
|
info,
|
||||||
@@ -486,18 +533,15 @@ impl UvcControlTransport for NusbTransport {
|
|||||||
.map_err(|e| TransportError::Usb(e.to_string()))
|
.map_err(|e| TransportError::Usb(e.to_string()))
|
||||||
} else {
|
} else {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
if let Some(dev) = &self.device {
|
||||||
self.device
|
return dev
|
||||||
.control_out_blocking(control, payload, self.timeout)
|
.control_out_blocking(control, payload, self.timeout)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|e| TransportError::Usb(e.to_string()))
|
.map_err(|e| TransportError::Usb(e.to_string()));
|
||||||
}
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
Err(TransportError::Usb(
|
|
||||||
"UVC control interface could not be claimed on Windows".to_string(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
Err(TransportError::Usb(
|
||||||
|
"UVC control transfer failed".to_string(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,17 +634,16 @@ impl UvcControlTransport for NusbTransport {
|
|||||||
.map_err(|e| TransportError::Usb(e.to_string()))?
|
.map_err(|e| TransportError::Usb(e.to_string()))?
|
||||||
} else {
|
} else {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
if let Some(dev) = &self.device {
|
||||||
self.device
|
let n = dev
|
||||||
.control_in_blocking(control, &mut buf, self.timeout)
|
.control_in_blocking(control, &mut buf, self.timeout)
|
||||||
.map_err(|e| TransportError::Usb(e.to_string()))?
|
.map_err(|e| TransportError::Usb(e.to_string()))?;
|
||||||
}
|
buf.truncate(n);
|
||||||
#[cfg(target_os = "windows")]
|
return Ok(buf);
|
||||||
{
|
|
||||||
return Err(TransportError::Usb(
|
|
||||||
"UVC control interface could not be claimed on Windows".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
return Err(TransportError::Usb(
|
||||||
|
"UVC control transfer failed".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
buf.truncate(n);
|
buf.truncate(n);
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ run = "cargo test --workspace"
|
|||||||
description = "Run the automated live hardware GUI verification sequence"
|
description = "Run the automated live hardware GUI verification sequence"
|
||||||
run = "INSTA360_GUI_SELF_TEST=1 cargo run --release -p insta360-link-gui"
|
run = "INSTA360_GUI_SELF_TEST=1 cargo run --release -p insta360-link-gui"
|
||||||
|
|
||||||
[tasks.udev-install]
|
|
||||||
description = "Install Linux udev rules for non-root USB access to Insta360 Link webcams"
|
|
||||||
run = "sudo cp udev/99-insta360-link.rules /etc/udev/rules.d/99-insta360-link.rules && sudo udevadm control --reload-rules && sudo udevadm trigger"
|
|
||||||
|
|
||||||
[tasks.package-release]
|
[tasks.package-release]
|
||||||
description = "Build and package release binaries for a target (usage: mise run package-release [target] [version])"
|
description = "Build and package release binaries for a target (usage: mise run package-release [target] [version])"
|
||||||
run = '''
|
run = '''
|
||||||
@@ -96,7 +92,7 @@ else
|
|||||||
cp "target/${TARGET}/release/insta360-link-ctl" "$STAGE_DIR/"
|
cp "target/${TARGET}/release/insta360-link-ctl" "$STAGE_DIR/"
|
||||||
cp "target/${TARGET}/release/insta360-link-server" "$STAGE_DIR/"
|
cp "target/${TARGET}/release/insta360-link-server" "$STAGE_DIR/"
|
||||||
cp README.md "$STAGE_DIR/"
|
cp README.md "$STAGE_DIR/"
|
||||||
cp -r udev "$STAGE_DIR/"
|
cp -r assets "$STAGE_DIR/"
|
||||||
tar -C dist -czf "dist/${STAGE_NAME}.tar.gz" "$STAGE_NAME"
|
tar -C dist -czf "dist/${STAGE_NAME}.tar.gz" "$STAGE_NAME"
|
||||||
echo "Packaged dist/${STAGE_NAME}.tar.gz"
|
echo "Packaged dist/${STAGE_NAME}.tar.gz"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# Linux udev rules for Insta360 Link / Link 2 / Link 2C / Link 2 Pro (PUC3) webcams
|
|
||||||
#
|
|
||||||
# Grants non-root desktop users (via systemd-logind uaccess and plugdev/video groups)
|
|
||||||
# direct read/write access to the USB device nodes so `insta360-link-ctl`,
|
|
||||||
# `insta360-link-server`, and `insta360-link-gui` can issue UVC Extension Unit
|
|
||||||
# control transfers without requiring `sudo`.
|
|
||||||
#
|
|
||||||
# Installation:
|
|
||||||
# sudo cp udev/99-insta360-link.rules /etc/udev/rules.d/99-insta360-link.rules
|
|
||||||
# sudo udevadm control --reload-rules
|
|
||||||
# sudo udevadm trigger
|
|
||||||
|
|
||||||
ACTION!="add|change", GOTO="insta360_link_end"
|
|
||||||
SUBSYSTEM!="usb", GOTO="insta360_link_end"
|
|
||||||
|
|
||||||
# Insta360 Link (1st Gen - 0x4c01)
|
|
||||||
ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c01", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
|
||||||
|
|
||||||
# Insta360 Link 2 Pro / PUC3 Gimbal A (0x4c02)
|
|
||||||
ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c02", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
|
||||||
|
|
||||||
# Insta360 Link 2C (Fixed-Lens 4K - 0x4c03)
|
|
||||||
ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c03", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
|
||||||
|
|
||||||
# Insta360 Link 2 (2-Axis Gimbal 4K - 0x4c04)
|
|
||||||
ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c04", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
|
||||||
|
|
||||||
# Insta360 Link 2C Pro / PUC3 Host (0x4c05)
|
|
||||||
ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c05", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
|
||||||
|
|
||||||
# Insta360 Link 2 Pro / PUC3 Gimbal B (0x4c06)
|
|
||||||
ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="4c06", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
|
||||||
|
|
||||||
# Insta360 Link Simple-Class / Firmware Upgrade Mode (0x00c2)
|
|
||||||
ATTRS{idVendor}=="2e1a", ATTRS{idProduct}=="00c2", MODE="0660", GROUP="plugdev", TAG+="uaccess"
|
|
||||||
|
|
||||||
LABEL="insta360_link_end"
|
|
||||||
Reference in new issue
Block a user