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
@@ -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<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Matching webcam.qml: width: 1440, height: 714, minimumWidth: 1120, minimumHeight: 536
|
||||
let native_options = eframe::NativeOptions {
|
||||
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_min_inner_size([1120.0_f32, 536.0_f32])
|
||||
.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);
|
||||
}
|
||||
}
|
||||
#[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 {
|
||||
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`
|
||||
/// (Linux `uvcvideo` `UVCIOC_CTRL_QUERY` + `usbfs`, Windows `WinUSB`, macOS `IOKit`).
|
||||
/// (Linux `uvcvideo` `UVCIOC_CTRL_QUERY` + V4L2 ioctls, Windows `WinUSB`, macOS `IOKit`).
|
||||
pub struct NusbTransport {
|
||||
info: DiscoveredCamera,
|
||||
device: nusb::Device,
|
||||
device: Option<nusb::Device>,
|
||||
interface: Option<nusb::Interface>,
|
||||
#[cfg(target_os = "linux")]
|
||||
v4l_file: Option<std::fs::File>,
|
||||
@@ -100,7 +106,7 @@ pub struct NusbTransport {
|
||||
}
|
||||
|
||||
#[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 pid_hex = format!("{:04x}", pid);
|
||||
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);
|
||||
|
||||
let mut fallback: Option<std::fs::File> = None;
|
||||
let mut fallback: Option<(std::fs::File, UvcUnitIds)> = None;
|
||||
for (_, path, name) in candidates {
|
||||
let dev_link = path.join("device");
|
||||
let Ok(canon) = std::fs::canonicalize(&dev_link) else {
|
||||
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() {
|
||||
let v = std::fs::read_to_string(usb_dev_dir.join("idVendor"))
|
||||
.unwrap_or_default()
|
||||
@@ -132,6 +138,23 @@ fn open_linux_v4l2_node(vid: u16, pid: u16, xu1_id: u8) -> Option<std::fs::File>
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
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);
|
||||
if let Ok(file) = std::fs::OpenOptions::new()
|
||||
.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];
|
||||
if linux_uvcioc_ctrl_query(
|
||||
&file,
|
||||
xu1_id,
|
||||
probe_xu1,
|
||||
0x03,
|
||||
UvcRequest::GetLen as u8,
|
||||
&mut len_buf,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return Some(file);
|
||||
return Some((file, parsed_ids));
|
||||
}
|
||||
if fallback.is_none() {
|
||||
fallback = Some(file);
|
||||
fallback = Some((file, parsed_ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,25 +375,49 @@ impl NusbTransport {
|
||||
})
|
||||
.ok_or(TransportError::DeviceNotFound)?;
|
||||
|
||||
let device = dev_info.open().map_err(|e| TransportError::Usb(e.to_string()))?;
|
||||
if let Ok(cfg) = device.active_configuration() {
|
||||
let raw: &[u8] = cfg.descriptors().as_bytes();
|
||||
info.unit_ids = parse_uvc_vc_descriptors(raw);
|
||||
let device = dev_info.open().ok();
|
||||
if let Some(ref dev) = device {
|
||||
if let Ok(cfg) = dev.active_configuration() {
|
||||
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
|
||||
// driver (`uvcvideo` on Linux, `UVCAssistant` on macOS, `usbvideo.sys` on Windows) so that
|
||||
// live webcam video capture and UVC/XU control transfers operate simultaneously.
|
||||
let interface = device
|
||||
.claim_interface(info.unit_ids.control_interface)
|
||||
.ok();
|
||||
.as_ref()
|
||||
.and_then(|d| d.claim_interface(info.unit_ids.control_interface).ok());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let v4l_file = open_linux_v4l2_node(
|
||||
let v4l_file = match open_linux_v4l2_node(
|
||||
info.vendor_id,
|
||||
info.product_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 {
|
||||
info,
|
||||
@@ -486,18 +533,15 @@ impl UvcControlTransport for NusbTransport {
|
||||
.map_err(|e| TransportError::Usb(e.to_string()))
|
||||
} else {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
self.device
|
||||
if let Some(dev) = &self.device {
|
||||
return dev
|
||||
.control_out_blocking(control, payload, self.timeout)
|
||||
.map(|_| ())
|
||||
.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(),
|
||||
))
|
||||
.map_err(|e| TransportError::Usb(e.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()))?
|
||||
} else {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
self.device
|
||||
if let Some(dev) = &self.device {
|
||||
let n = dev
|
||||
.control_in_blocking(control, &mut buf, self.timeout)
|
||||
.map_err(|e| TransportError::Usb(e.to_string()))?
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return Err(TransportError::Usb(
|
||||
"UVC control interface could not be claimed on Windows".to_string(),
|
||||
));
|
||||
.map_err(|e| TransportError::Usb(e.to_string()))?;
|
||||
buf.truncate(n);
|
||||
return Ok(buf);
|
||||
}
|
||||
return Err(TransportError::Usb(
|
||||
"UVC control transfer failed".to_string(),
|
||||
));
|
||||
};
|
||||
buf.truncate(n);
|
||||
Ok(buf)
|
||||
|
||||
Reference in new issue
Block a user