Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 5 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,18 @@ cli = [
"dep:simple_logger",
"dep:log",
]
# Switch hidra to its nusb-based USB backend instead of the per-OS native one.
# Linux always uses nusb (auto-selected via the target-specific dependency
# below); this feature exists to opt into nusb on macOS/Windows as well.
nusb = ["hidra/nusb"]
# No-op, kept so an existing `--features nusb` still resolves: hidra compiles
# both backends in and this crate names the one it wants.
nusb = []

[[bin]]
name = "sinowisp"
path = "src/main.rs"
required-features = ["cli"]

[dependencies]
# Pure-Rust HID. On macOS/Windows this uses hidra's native per-OS backend; on
# Linux the nusb backend is auto-selected via the target-specific dependency
# below (the hidapi `linux-static-libusb` equivalent). wasm uses WebHID.
hidra = "0.0.3"
# Both backends built in; `DeviceSelector` falls back between them at run time.
hidra = "0.0.4"
ihex = "3.0"
thiserror = "2.0"
phf = { version = "0.11", features = ["macros"] }
Expand All @@ -61,11 +58,6 @@ default-features = false
features = ["stderr", "colors", "timestamps"]
optional = true

# On Linux there is no native hidraw path we want; always use hidra's nusb
# backend. Cargo features are additive, so this is unconditional on Linux.
[target.'cfg(target_os = "linux")'.dependencies]
hidra = { version = "0.0.3", features = ["nusb"] }

[dev-dependencies]
assert_cmd = "2.0.17"
chrono = "0.4.41"
Expand Down
137 changes: 95 additions & 42 deletions src/device_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ use std::{thread, time::Duration};

use hidparser::parse_report_descriptor;
use hidra::{
BusType, DeviceInfo, HidDevice, HidError, Hidra, MaybeFuture, MAX_REPORT_DESCRIPTOR_SIZE,
BusType, DeviceInfo, HidError, Hidra, MaybeFuture, Native, Nusb, MAX_REPORT_DESCRIPTOR_SIZE,
};
use indicatif::ProgressBar;
use itertools::Itertools;
use log::{debug, error, info};
use sinowisp::{is_expected_error, DeviceSpec, ISPDevice};
use sinowisp::{is_expected_error, DeviceSpec, ISPDevice, ISPHandle};
use thiserror::Error;

use crate::hid_tree::{DeviceNode, InterfaceNode};
Expand Down Expand Up @@ -40,25 +40,98 @@ pub enum DeviceSelectorError {
UnexpectedDeviceCount,
}

/// The two hidra backends, so one field can hold either.
///
/// Backends are types, so this is the caller's to declare. It forwards the
/// three methods the selector uses; each sees devices the other cannot, and
/// ISP mode swaps which.
enum Api {
Native(Hidra<Native>),
Nusb(Hidra<Nusb>),
}

impl Api {
fn open(which: Backend) -> Result<Self, DeviceSelectorError> {
Ok(match which {
Backend::Native => {
let api = Hidra::<Native>::builder().build()?;
// macOS refuses an exclusive open with a privilege violation.
#[cfg(target_os = "macos")]
api.set_open_exclusive(false);
Api::Native(api)
}
Backend::Nusb => Api::Nusb(Hidra::<Nusb>::builder().build()?),
})
}

fn refresh_devices(&mut self) -> Result<(), DeviceSelectorError> {
match self {
Api::Native(api) => api.refresh_devices()?,
Api::Nusb(api) => api.refresh_devices()?,
}
Ok(())
}

fn device_list(&self) -> Vec<&DeviceInfo> {
match self {
Api::Native(api) => api.device_list().collect(),
Api::Nusb(api) => api.device_list().collect(),
}
}

fn open_path(&self, path: &str) -> Result<ISPHandle, DeviceSelectorError> {
Ok(match self {
Api::Native(api) => ISPHandle::from(api.open_path(path).wait()?),
Api::Nusb(api) => ISPHandle::from(api.open_path(path).wait()?),
})
}
}

/// Which backend to open. `Api` is the value; this is the choice.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Backend {
Native,
Nusb,
}

impl core::fmt::Display for Backend {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
Backend::Native => "native",
Backend::Nusb => "nusb",
})
}
}

const BACKENDS: [Backend; 2] = [Backend::Native, Backend::Nusb];

pub struct DeviceSelector {
api: Hidra,
api: Api,
backend: usize,
}

impl DeviceSelector {
pub fn new() -> Result<Self, DeviceSelectorError> {
let api = Hidra::new().map_err(DeviceSelectorError::from)?;

// hidra only exposes this on the native macOS backend (not nusb).
#[cfg(all(target_os = "macos", not(feature = "nusb")))]
api.set_open_exclusive(false); // macOS will throw a privilege violation error otherwise
Ok(Self {
api: Api::open(BACKENDS[0])?,
backend: 0,
})
}

Ok(Self { api })
/// Next backend: each sees devices the other cannot, and ISP mode swaps which.
fn rotate_backend(&mut self) -> Result<(), DeviceSelectorError> {
self.backend = (self.backend + 1) % BACKENDS.len();
let backend = BACKENDS[self.backend];
info!("Trying the {backend} backend...");
self.api = Api::open(backend)?;
Ok(())
}

fn sorted_usb_device_list(&self) -> Vec<&DeviceInfo> {
let mut devices: Vec<_> = self
.api
.device_list()
.into_iter()
.filter(|d| d.bus_type() == BusType::Usb)
.collect();
// TODO: move out the platform specific sorting
Expand Down Expand Up @@ -100,17 +173,13 @@ impl DeviceSelector {
&self,
path: &str,
) -> Result<Vec<u32>, DeviceSelectorError> {
let dev = self
.api
.open_path(path)
.wait()
.map_err(DeviceSelectorError::from)?;
let dev = self.api.open_path(path)?;
self.get_feature_report_ids_from_device(&dev)
}

fn get_feature_report_ids_from_device(
&self,
dev: &HidDevice,
dev: &ISPHandle,
) -> Result<Vec<u32>, DeviceSelectorError> {
let mut buf: [u8; MAX_REPORT_DESCRIPTOR_SIZE] = [0; MAX_REPORT_DESCRIPTOR_SIZE];
let size: usize = dev
Expand All @@ -136,7 +205,7 @@ impl DeviceSelector {
Ok(res)
}

fn get_report_descriptor(&self, dev: &HidDevice) -> Result<Vec<u8>, DeviceSelectorError> {
fn get_report_descriptor(&self, dev: &ISPHandle) -> Result<Vec<u8>, DeviceSelectorError> {
let mut buf: [u8; MAX_REPORT_DESCRIPTOR_SIZE] = [0; MAX_REPORT_DESCRIPTOR_SIZE];
let size: usize = dev
.get_report_descriptor(&mut buf)
Expand All @@ -154,7 +223,7 @@ impl DeviceSelector {
) {
let descriptor: Result<Vec<u8>, DeviceSelectorError>;
let feature_report_ids: Result<Vec<u32>, DeviceSelectorError>;
match self.api.open_path(path).wait() {
match self.api.open_path(path) {
Ok(ref dev) => {
descriptor = self.get_report_descriptor(dev);
match descriptor {
Expand All @@ -167,7 +236,7 @@ impl DeviceSelector {
}
}
Err(err) => {
descriptor = Err(DeviceSelectorError::from(err));
descriptor = Err(err);
feature_report_ids = Err(DeviceSelectorError::NotFound);
}
}
Expand Down Expand Up @@ -255,11 +324,7 @@ impl DeviceSelector {
)?;
debug!("ISP device: {}", device.info());

let handle = self
.api
.open_path(device.path())
.wait()
.map_err(DeviceSelectorError::from)?;
let handle = self.api.open_path(device.path())?;

Ok(ISPDevice::new(device_spec, handle, None))
};
Expand All @@ -277,22 +342,14 @@ impl DeviceSelector {
let xfer_device = devices[1];
debug!("ISP XFER device: {}", xfer_device.info());

let cmd_handle = self
.api
.open_path(cmd_device.path())
.wait()
.map_err(DeviceSelectorError::from)?;
let xfer_handle = self
.api
.open_path(xfer_device.path())
.wait()
.map_err(DeviceSelectorError::from)?;
let cmd_handle = self.api.open_path(cmd_device.path())?;
let xfer_handle = self.api.open_path(xfer_device.path())?;

Ok(ISPDevice::new(device_spec, cmd_handle, Some(xfer_handle)))
};
}

fn find_device(&self, device_spec: DeviceSpec) -> Result<HidDevice, DeviceSelectorError> {
fn find_device(&self, device_spec: DeviceSpec) -> Result<ISPHandle, DeviceSelectorError> {
let filtered_devices = self.unique_usb_device_list().into_iter().filter(|d| {
d.vendor_id() == device_spec.vendor_id
&& d.product_id() == device_spec.product_id
Expand All @@ -317,17 +374,13 @@ impl DeviceSelector {
};

debug!("Opening: {:?}", cmd_device_info.path());
let device = self
.api
.open_path(cmd_device_info.path())
.wait()
.map_err(DeviceSelectorError::from)?;
let device = self.api.open_path(cmd_device_info.path())?;
Ok(device)
}

fn switch_to_isp_device(
&mut self,
device: HidDevice,
device: ISPHandle,
device_spec: DeviceSpec,
) -> Result<ISPDevice, DeviceSelectorError> {
if let Err(err) = self.enter_isp_mode(&device) {
Expand Down Expand Up @@ -376,7 +429,7 @@ impl DeviceSelector {
if attempt > 1 {
bar.set_message(format!("Retrying... Attempt {attempt}/{retries}"));
info!("Retrying... Attempt {attempt}/{retries}");
self.api.refresh_devices()?;
self.rotate_backend()?;
thread::sleep(time::Duration::from_millis(1000));
}

Expand Down Expand Up @@ -418,7 +471,7 @@ impl DeviceSelector {
Err(DeviceSelectorError::NotFound)
}

fn enter_isp_mode(&self, handle: &HidDevice) -> Result<(), DeviceSelectorError> {
fn enter_isp_mode(&self, handle: &ISPHandle) -> Result<(), DeviceSelectorError> {
let cmd: [u8; COMMAND_LENGTH] = [REPORT_ID_ISP, CMD_ISP_MODE, 0x00, 0x00, 0x00, 0x00];
handle.send_feature_report(&cmd).wait()?;
Ok(())
Expand Down
Loading