From bfac1df57bcfb3df8f0e535fe106ade4b5399a56 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Wed, 2 Sep 2026 08:51:29 +0200 Subject: [PATCH 1/8] Fall back between HID backends within a single run --- Cargo.lock | 2 -- Cargo.toml | 21 +++++++---------- src/device_selector.rs | 51 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fe9ab3..f01e42f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,8 +346,6 @@ checksum = "efb8ae90b8d0165be79ff98d845bebbbc9dfa0295ff2ce86beba525d93b9ba60" [[package]] name = "hidra" version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4861c589ab4c739001e0a62db96286e217561221e838095fcd8d31bf4f155e7b" dependencies = [ "core-foundation-sys", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 0aecbe7..4ed2691 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,8 @@ 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: hidra's nusb backend is always built in and picked at run time. +nusb = [] [[bin]] name = "sinowisp" @@ -37,10 +35,8 @@ 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 = { version = "0.0.3", features = ["nusb"] } ihex = "3.0" thiserror = "2.0" phf = { version = "0.11", features = ["macros"] } @@ -61,14 +57,13 @@ 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" predicates = "3.1.3" serial_test = "3.2.0" stdext = "0.3.3" + +# TODO: drop once the backend-dispatch changes are released to crates.io. +[patch.crates-io] +hidra = { path = "../hidra" } diff --git a/src/device_selector.rs b/src/device_selector.rs index 5e74792..79a01b3 100644 --- a/src/device_selector.rs +++ b/src/device_selector.rs @@ -3,7 +3,8 @@ use std::{thread, time::Duration}; use hidparser::parse_report_descriptor; use hidra::{ - BusType, DeviceInfo, HidDevice, HidError, Hidra, MaybeFuture, MAX_REPORT_DESCRIPTOR_SIZE, + Backend, BusType, DeviceInfo, HidDevice, HidError, Hidra, MaybeFuture, + MAX_REPORT_DESCRIPTOR_SIZE, }; use indicatif::ProgressBar; use itertools::Itertools; @@ -42,17 +43,53 @@ pub enum DeviceSelectorError { pub struct DeviceSelector { api: Hidra, + backends: Vec, + backend: usize, } impl DeviceSelector { pub fn new() -> Result { - let api = Hidra::new().map_err(DeviceSelectorError::from)?; + let backends: Vec = [Backend::Native, Backend::Nusb] + .into_iter() + .filter(|b| b.is_available()) + .collect(); + let backend = 0; + let api = Self::open_api(backends[backend])?; + Ok(Self { + api, + backends, + backend, + }) + } - // 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 + fn open_api(backend: Backend) -> Result { + let api = Hidra::builder() + .backend(backend) + .build() + .map_err(DeviceSelectorError::from)?; + + // macOS refuses an exclusive open with a privilege violation. + #[cfg(target_os = "macos")] + if backend == Backend::Native { + api.set_open_exclusive(false)?; + } - Ok(Self { api }) + Ok(api) + } + + /// Next backend: each sees devices the other cannot, and ISP mode swaps which. + fn rotate_backend(&mut self) -> Result<(), DeviceSelectorError> { + if self.backends.len() < 2 { + return self + .api + .refresh_devices() + .map_err(DeviceSelectorError::from); + } + self.backend = (self.backend + 1) % self.backends.len(); + let backend = self.backends[self.backend]; + info!("Trying the {backend} backend..."); + self.api = Self::open_api(backend)?; + Ok(()) } fn sorted_usb_device_list(&self) -> Vec<&DeviceInfo> { @@ -376,7 +413,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)); } From 0beab793276e230693a34bd843e1a525e1c85066 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Wed, 2 Sep 2026 22:54:54 +0200 Subject: [PATCH 2/8] Track hidra's backend API `Backend::available()` replaces the hand-rolled filter over a hardcoded list, and an empty list now falls back to `Backend::default()` instead of indexing out of bounds. The explicit `nusb` feature is redundant now that hidra enables it by default. --- Cargo.toml | 2 +- src/device_selector.rs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ed2691..d57f5ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ required-features = ["cli"] [dependencies] # Both backends built in; `DeviceSelector` falls back between them at run time. -hidra = { version = "0.0.3", features = ["nusb"] } +hidra = "0.0.3" ihex = "3.0" thiserror = "2.0" phf = { version = "0.11", features = ["macros"] } diff --git a/src/device_selector.rs b/src/device_selector.rs index 79a01b3..7d42ef7 100644 --- a/src/device_selector.rs +++ b/src/device_selector.rs @@ -49,12 +49,9 @@ pub struct DeviceSelector { impl DeviceSelector { pub fn new() -> Result { - let backends: Vec = [Backend::Native, Backend::Nusb] - .into_iter() - .filter(|b| b.is_available()) - .collect(); + let backends: Vec = Backend::available().collect(); let backend = 0; - let api = Self::open_api(backends[backend])?; + let api = Self::open_api(backends.first().copied().unwrap_or_default())?; Ok(Self { api, backends, From a264c4e27afa0cccf96a235f23ba467061028511 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Thu, 3 Sep 2026 07:35:19 +0200 Subject: [PATCH 3/8] Patch hidra from git rather than a local path The path patch only resolved on a checkout that happened to sit next to a hidra clone, so CI failed to read ../hidra/Cargo.toml before it got as far as compiling anything. --- Cargo.lock | 1 + Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f01e42f..daa3918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,6 +346,7 @@ checksum = "efb8ae90b8d0165be79ff98d845bebbbc9dfa0295ff2ce86beba525d93b9ba60" [[package]] name = "hidra" version = "0.0.3" +source = "git+https://github.com/carlossless/hidra?rev=e11ba204159dfc791c4505ef59a0ce759d500c5b#e11ba204159dfc791c4505ef59a0ce759d500c5b" dependencies = [ "core-foundation-sys", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index d57f5ea..570f419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,6 @@ predicates = "3.1.3" serial_test = "3.2.0" stdext = "0.3.3" -# TODO: drop once the backend-dispatch changes are released to crates.io. +# TODO: drop once hidra#8 lands and the backend-dispatch API is on crates.io. [patch.crates-io] -hidra = { path = "../hidra" } +hidra = { git = "https://github.com/carlossless/hidra", rev = "e11ba204159dfc791c4505ef59a0ce759d500c5b" } From 8e04c66e005c4639d3cf35fd55d79bd6aedb02c3 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Thu, 3 Sep 2026 14:17:13 +0200 Subject: [PATCH 4/8] Hold the backend enum here, not in hidra hidra selects its backend by type now, so a field that can hold either is this crate's to declare. That is `Api` in the selector, forwarding the three methods it uses, and `IspHandle` in the library, forwarding the three the protocol needs. Rotating between backends works as before. The macOS exclusive-open call is inherent to the native backend, so it no longer needs a run-time check that the backend is the right one. --- Cargo.lock | 10 ++- Cargo.toml | 2 +- src/device_selector.rs | 153 +++++++++++++++++++++++------------------ src/isp_device.rs | 80 +++++++++++++++++++-- src/lib.rs | 2 +- 5 files changed, 169 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index daa3918..9fb692a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,7 @@ checksum = "efb8ae90b8d0165be79ff98d845bebbbc9dfa0295ff2ce86beba525d93b9ba60" [[package]] name = "hidra" version = "0.0.3" -source = "git+https://github.com/carlossless/hidra?rev=e11ba204159dfc791c4505ef59a0ce759d500c5b#e11ba204159dfc791c4505ef59a0ce759d500c5b" +source = "git+https://github.com/carlossless/hidra?rev=d0e0595e105a47f56f55f1c4ee932a042a1b88ec#d0e0595e105a47f56f55f1c4ee932a042a1b88ec" dependencies = [ "core-foundation-sys", "js-sys", @@ -523,19 +523,23 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nusb" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "215a49c10e8cff30a0da93b258b21846c4e9242f7683748cfa8474fdc2e4d22b" +checksum = "18ef13beb3b3a8fc16fd7aea912ebd3d45dde00a9a5b968d0742297468065845" dependencies = [ "core-foundation", "core-foundation-sys", "futures-core", "io-kit-sys", + "js-sys", "linux-raw-sys", "log", "once_cell", "rustix", "slab", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", "windows-sys 0.61.2", ] diff --git a/Cargo.toml b/Cargo.toml index 570f419..f0ccedd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,4 +66,4 @@ stdext = "0.3.3" # TODO: drop once hidra#8 lands and the backend-dispatch API is on crates.io. [patch.crates-io] -hidra = { git = "https://github.com/carlossless/hidra", rev = "e11ba204159dfc791c4505ef59a0ce759d500c5b" } +hidra = { git = "https://github.com/carlossless/hidra", rev = "d0e0595e105a47f56f55f1c4ee932a042a1b88ec" } diff --git a/src/device_selector.rs b/src/device_selector.rs index 7d42ef7..eef1c9d 100644 --- a/src/device_selector.rs +++ b/src/device_selector.rs @@ -3,13 +3,12 @@ use std::{thread, time::Duration}; use hidparser::parse_report_descriptor; use hidra::{ - Backend, 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}; @@ -41,51 +40,90 @@ pub enum DeviceSelectorError { UnexpectedDeviceCount, } -pub struct DeviceSelector { - api: Hidra, - backends: Vec, - backend: usize, +/// 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), + Nusb(Hidra), } -impl DeviceSelector { - pub fn new() -> Result { - let backends: Vec = Backend::available().collect(); - let backend = 0; - let api = Self::open_api(backends.first().copied().unwrap_or_default())?; - Ok(Self { - api, - backends, - backend, +impl Api { + fn open(which: Backend) -> Result { + Ok(match which { + Backend::Native => { + let api = Hidra::::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::::builder().build()?), }) } - fn open_api(backend: Backend) -> Result { - let api = Hidra::builder() - .backend(backend) - .build() - .map_err(DeviceSelectorError::from)?; + fn refresh_devices(&mut self) -> Result<(), DeviceSelectorError> { + match self { + Api::Native(api) => api.refresh_devices()?, + Api::Nusb(api) => api.refresh_devices()?, + } + Ok(()) + } - // macOS refuses an exclusive open with a privilege violation. - #[cfg(target_os = "macos")] - if backend == Backend::Native { - api.set_open_exclusive(false)?; + 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 { + 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: Api, + backend: usize, +} - Ok(api) +impl DeviceSelector { + pub fn new() -> Result { + Ok(Self { + api: Api::open(BACKENDS[0])?, + backend: 0, + }) } /// Next backend: each sees devices the other cannot, and ISP mode swaps which. fn rotate_backend(&mut self) -> Result<(), DeviceSelectorError> { - if self.backends.len() < 2 { - return self - .api - .refresh_devices() - .map_err(DeviceSelectorError::from); - } - self.backend = (self.backend + 1) % self.backends.len(); - let backend = self.backends[self.backend]; + self.backend = (self.backend + 1) % BACKENDS.len(); + let backend = BACKENDS[self.backend]; info!("Trying the {backend} backend..."); - self.api = Self::open_api(backend)?; + self.api = Api::open(backend)?; Ok(()) } @@ -93,6 +131,7 @@ impl DeviceSelector { 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 @@ -134,17 +173,13 @@ impl DeviceSelector { &self, path: &str, ) -> Result, 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, DeviceSelectorError> { let mut buf: [u8; MAX_REPORT_DESCRIPTOR_SIZE] = [0; MAX_REPORT_DESCRIPTOR_SIZE]; let size: usize = dev @@ -170,7 +205,7 @@ impl DeviceSelector { Ok(res) } - fn get_report_descriptor(&self, dev: &HidDevice) -> Result, DeviceSelectorError> { + fn get_report_descriptor(&self, dev: &IspHandle) -> Result, DeviceSelectorError> { let mut buf: [u8; MAX_REPORT_DESCRIPTOR_SIZE] = [0; MAX_REPORT_DESCRIPTOR_SIZE]; let size: usize = dev .get_report_descriptor(&mut buf) @@ -188,7 +223,7 @@ impl DeviceSelector { ) { let descriptor: Result, DeviceSelectorError>; let feature_report_ids: Result, 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 { @@ -201,7 +236,7 @@ impl DeviceSelector { } } Err(err) => { - descriptor = Err(DeviceSelectorError::from(err)); + descriptor = Err(err); feature_report_ids = Err(DeviceSelectorError::NotFound); } } @@ -289,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)) }; @@ -311,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 { + fn find_device(&self, device_spec: DeviceSpec) -> Result { 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 @@ -351,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 { if let Err(err) = self.enter_isp_mode(&device) { @@ -452,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(()) diff --git a/src/isp_device.rs b/src/isp_device.rs index f46f799..ddd3715 100644 --- a/src/isp_device.rs +++ b/src/isp_device.rs @@ -2,6 +2,8 @@ use core::panic; use std::str::FromStr; use hidra::{HidDevice, HidError}; +#[cfg(not(target_arch = "wasm32"))] +use hidra::{NativeDevice, NusbDevice}; use thiserror::Error; use crate::{device_spec::*, VerificationError}; @@ -20,6 +22,72 @@ const CMD_REBOOT: u8 = 0x5a; const XFER_READ_PAGE: u8 = 0x72; const XFER_WRITE_PAGE: u8 = 0x77; +/// A HID handle an [`ISPDevice`] talks through. +/// +/// hidra backends are types, so holding either is the caller's job; the +/// protocol needs three methods and this forwards them. On wasm there is only +/// WebHID, so it is a plain newtype. +#[cfg(not(target_arch = "wasm32"))] +pub enum IspHandle { + /// A handle on the per-OS backend. + Native(HidDevice), + /// A handle on the raw-USB backend. + Nusb(HidDevice), +} + +/// A HID handle an [`ISPDevice`] talks through. +#[cfg(target_arch = "wasm32")] +pub struct IspHandle(HidDevice); + +#[cfg(not(target_arch = "wasm32"))] +impl From> for IspHandle { + fn from(device: HidDevice) -> Self { + IspHandle::Native(device) + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From> for IspHandle { + fn from(device: HidDevice) -> Self { + IspHandle::Nusb(device) + } +} + +#[cfg(target_arch = "wasm32")] +impl From for IspHandle { + fn from(device: HidDevice) -> Self { + IspHandle(device) + } +} + +/// Forwards the three methods the protocol needs to whichever backend the +/// handle came from. +macro_rules! forward { + ($self:ident, $call:ident($($arg:expr),*)) => {{ + #[cfg(not(target_arch = "wasm32"))] + match $self { + IspHandle::Native(d) => d.$call($($arg),*).await, + IspHandle::Nusb(d) => d.$call($($arg),*).await, + } + #[cfg(target_arch = "wasm32")] + $self.0.$call($($arg),*).await + }}; +} + +impl IspHandle { + pub async fn send_feature_report(&self, data: &[u8]) -> Result<(), HidError> { + forward!(self, send_feature_report(data)) + } + + pub async fn get_report_descriptor(&self, buf: &mut [u8]) -> Result { + forward!(self, get_report_descriptor(buf)) + } + + pub async fn get_feature_report(&self, buf: &mut [u8]) -> Result { + forward!(self, get_feature_report(buf)) + } +} + /// One open connection to a device in ISP bootloader mode. /// /// The methods are the individual protocol operations; they perform no @@ -27,10 +95,10 @@ const XFER_WRITE_PAGE: u8 = 0x77; /// read/write cycles (and insert the settle delays after [`erase`](Self::erase) /// and [`reboot`](Self::reboot)). pub struct ISPDevice { - cmd_device: HidDevice, + cmd_device: IspHandle, /// Some platforms (Windows) expose the transfer report on a separate HID /// handle; everywhere else it is the same handle as `cmd_device`. - xfer_device: Option, + xfer_device: Option, device_spec: DeviceSpec, } @@ -89,11 +157,11 @@ impl ISPDevice { /// platforms that split them across HID collections (Windows). pub fn new( device_spec: DeviceSpec, - cmd_device: HidDevice, - xfer_device: Option, + cmd_device: impl Into, + xfer_device: Option, ) -> Self { Self { - cmd_device, + cmd_device: cmd_device.into(), xfer_device, device_spec, } @@ -104,7 +172,7 @@ impl ISPDevice { &self.device_spec } - fn xfer_device(&self) -> &HidDevice { + fn xfer_device(&self) -> &IspHandle { self.xfer_device.as_ref().unwrap_or(&self.cmd_device) } diff --git a/src/lib.rs b/src/lib.rs index 36eef1f..60b29c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Async primitives for the Sinowealth 8051 HID ISP bootloader protocol. //! //! Each protocol operation ([`ISPDevice::enable_firmware`], [`ISPDevice::erase`], -//! [`ISPDevice::read_page`], ...) is an `async` method over a [`hidra::HidDevice`], +//! [`ISPDevice::read_page`], ...) is an `async` method over an [`IspHandle`], //! so the exact same code drives a native CLI (by calling //! [`hidra::MaybeFuture::wait`] on the returned futures) and a web-based flasher //! (by `.await`-ing them on `wasm32` with the WebHID backend). From c46cc344e641b528d49bbabbe3d37f2432461f03 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Thu, 3 Sep 2026 14:27:40 +0200 Subject: [PATCH 5/8] Reword a stale comment about backend-selected types --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 60b29c0..ff3cf0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,6 @@ pub use isp_device::*; pub use platform_spec::*; pub use util::*; -// hidra types that appear in this crate's public API, re-exported so callers -// use the same backend-selected types this crate was built against. +// hidra types in this crate's public API, re-exported so callers get the same +// ones it was built against. pub use hidra::{HidDevice, HidError}; From 69f0baa605ad04734d51ed27c55ded187399164e Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Thu, 3 Sep 2026 21:40:19 +0200 Subject: [PATCH 6/8] Repin hidra to the branch tip --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9fb692a..1566059 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,7 @@ checksum = "efb8ae90b8d0165be79ff98d845bebbbc9dfa0295ff2ce86beba525d93b9ba60" [[package]] name = "hidra" version = "0.0.3" -source = "git+https://github.com/carlossless/hidra?rev=d0e0595e105a47f56f55f1c4ee932a042a1b88ec#d0e0595e105a47f56f55f1c4ee932a042a1b88ec" +source = "git+https://github.com/carlossless/hidra?rev=a9bff3926598cb8b0c1f6d39a61846b8d954aa82#a9bff3926598cb8b0c1f6d39a61846b8d954aa82" dependencies = [ "core-foundation-sys", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index f0ccedd..0400062 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,4 +66,4 @@ stdext = "0.3.3" # TODO: drop once hidra#8 lands and the backend-dispatch API is on crates.io. [patch.crates-io] -hidra = { git = "https://github.com/carlossless/hidra", rev = "d0e0595e105a47f56f55f1c4ee932a042a1b88ec" } +hidra = { git = "https://github.com/carlossless/hidra", rev = "a9bff3926598cb8b0c1f6d39a61846b8d954aa82" } From 050692987cfc01c4a165b68a3699dcd705e3ffd5 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Thu, 3 Sep 2026 21:53:27 +0200 Subject: [PATCH 7/8] Name it ISPHandle, like ISPDevice and ISPError --- src/device_selector.rs | 18 +++++++++--------- src/isp_device.rs | 32 ++++++++++++++++---------------- src/lib.rs | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/device_selector.rs b/src/device_selector.rs index eef1c9d..e11c248 100644 --- a/src/device_selector.rs +++ b/src/device_selector.rs @@ -8,7 +8,7 @@ use hidra::{ use indicatif::ProgressBar; use itertools::Itertools; use log::{debug, error, info}; -use sinowisp::{is_expected_error, DeviceSpec, ISPDevice, IspHandle}; +use sinowisp::{is_expected_error, DeviceSpec, ISPDevice, ISPHandle}; use thiserror::Error; use crate::hid_tree::{DeviceNode, InterfaceNode}; @@ -79,10 +79,10 @@ impl Api { } } - fn open_path(&self, path: &str) -> Result { + fn open_path(&self, path: &str) -> Result { Ok(match self { - Api::Native(api) => IspHandle::from(api.open_path(path).wait()?), - Api::Nusb(api) => IspHandle::from(api.open_path(path).wait()?), + Api::Native(api) => ISPHandle::from(api.open_path(path).wait()?), + Api::Nusb(api) => ISPHandle::from(api.open_path(path).wait()?), }) } } @@ -179,7 +179,7 @@ impl DeviceSelector { fn get_feature_report_ids_from_device( &self, - dev: &IspHandle, + dev: &ISPHandle, ) -> Result, DeviceSelectorError> { let mut buf: [u8; MAX_REPORT_DESCRIPTOR_SIZE] = [0; MAX_REPORT_DESCRIPTOR_SIZE]; let size: usize = dev @@ -205,7 +205,7 @@ impl DeviceSelector { Ok(res) } - fn get_report_descriptor(&self, dev: &IspHandle) -> Result, DeviceSelectorError> { + fn get_report_descriptor(&self, dev: &ISPHandle) -> Result, DeviceSelectorError> { let mut buf: [u8; MAX_REPORT_DESCRIPTOR_SIZE] = [0; MAX_REPORT_DESCRIPTOR_SIZE]; let size: usize = dev .get_report_descriptor(&mut buf) @@ -349,7 +349,7 @@ impl DeviceSelector { }; } - fn find_device(&self, device_spec: DeviceSpec) -> Result { + fn find_device(&self, device_spec: DeviceSpec) -> Result { 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 @@ -380,7 +380,7 @@ impl DeviceSelector { fn switch_to_isp_device( &mut self, - device: IspHandle, + device: ISPHandle, device_spec: DeviceSpec, ) -> Result { if let Err(err) = self.enter_isp_mode(&device) { @@ -471,7 +471,7 @@ impl DeviceSelector { Err(DeviceSelectorError::NotFound) } - fn enter_isp_mode(&self, handle: &IspHandle) -> 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(()) diff --git a/src/isp_device.rs b/src/isp_device.rs index ddd3715..b991cff 100644 --- a/src/isp_device.rs +++ b/src/isp_device.rs @@ -28,7 +28,7 @@ const XFER_WRITE_PAGE: u8 = 0x77; /// protocol needs three methods and this forwards them. On wasm there is only /// WebHID, so it is a plain newtype. #[cfg(not(target_arch = "wasm32"))] -pub enum IspHandle { +pub enum ISPHandle { /// A handle on the per-OS backend. Native(HidDevice), /// A handle on the raw-USB backend. @@ -37,26 +37,26 @@ pub enum IspHandle { /// A HID handle an [`ISPDevice`] talks through. #[cfg(target_arch = "wasm32")] -pub struct IspHandle(HidDevice); +pub struct ISPHandle(HidDevice); #[cfg(not(target_arch = "wasm32"))] -impl From> for IspHandle { +impl From> for ISPHandle { fn from(device: HidDevice) -> Self { - IspHandle::Native(device) + ISPHandle::Native(device) } } #[cfg(not(target_arch = "wasm32"))] -impl From> for IspHandle { +impl From> for ISPHandle { fn from(device: HidDevice) -> Self { - IspHandle::Nusb(device) + ISPHandle::Nusb(device) } } #[cfg(target_arch = "wasm32")] -impl From for IspHandle { +impl From for ISPHandle { fn from(device: HidDevice) -> Self { - IspHandle(device) + ISPHandle(device) } } @@ -66,15 +66,15 @@ macro_rules! forward { ($self:ident, $call:ident($($arg:expr),*)) => {{ #[cfg(not(target_arch = "wasm32"))] match $self { - IspHandle::Native(d) => d.$call($($arg),*).await, - IspHandle::Nusb(d) => d.$call($($arg),*).await, + ISPHandle::Native(d) => d.$call($($arg),*).await, + ISPHandle::Nusb(d) => d.$call($($arg),*).await, } #[cfg(target_arch = "wasm32")] $self.0.$call($($arg),*).await }}; } -impl IspHandle { +impl ISPHandle { pub async fn send_feature_report(&self, data: &[u8]) -> Result<(), HidError> { forward!(self, send_feature_report(data)) } @@ -95,10 +95,10 @@ impl IspHandle { /// read/write cycles (and insert the settle delays after [`erase`](Self::erase) /// and [`reboot`](Self::reboot)). pub struct ISPDevice { - cmd_device: IspHandle, + cmd_device: ISPHandle, /// Some platforms (Windows) expose the transfer report on a separate HID /// handle; everywhere else it is the same handle as `cmd_device`. - xfer_device: Option, + xfer_device: Option, device_spec: DeviceSpec, } @@ -157,8 +157,8 @@ impl ISPDevice { /// platforms that split them across HID collections (Windows). pub fn new( device_spec: DeviceSpec, - cmd_device: impl Into, - xfer_device: Option, + cmd_device: impl Into, + xfer_device: Option, ) -> Self { Self { cmd_device: cmd_device.into(), @@ -172,7 +172,7 @@ impl ISPDevice { &self.device_spec } - fn xfer_device(&self) -> &IspHandle { + fn xfer_device(&self) -> &ISPHandle { self.xfer_device.as_ref().unwrap_or(&self.cmd_device) } diff --git a/src/lib.rs b/src/lib.rs index ff3cf0c..146ed1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Async primitives for the Sinowealth 8051 HID ISP bootloader protocol. //! //! Each protocol operation ([`ISPDevice::enable_firmware`], [`ISPDevice::erase`], -//! [`ISPDevice::read_page`], ...) is an `async` method over an [`IspHandle`], +//! [`ISPDevice::read_page`], ...) is an `async` method over an [`ISPHandle`], //! so the exact same code drives a native CLI (by calling //! [`hidra::MaybeFuture::wait`] on the returned futures) and a web-based flasher //! (by `.await`-ing them on `wasm32` with the WebHID backend). From 3bce543ad5241110930fbdaa873a4bd5a891a2e9 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Thu, 3 Sep 2026 21:58:45 +0200 Subject: [PATCH 8/8] Depend on the published hidra 0.0.4 The git patch was a stopgap while the backend API was unreleased. --- Cargo.lock | 5 +++-- Cargo.toml | 9 +++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1566059..1f66f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,8 +345,9 @@ checksum = "efb8ae90b8d0165be79ff98d845bebbbc9dfa0295ff2ce86beba525d93b9ba60" [[package]] name = "hidra" -version = "0.0.3" -source = "git+https://github.com/carlossless/hidra?rev=a9bff3926598cb8b0c1f6d39a61846b8d954aa82#a9bff3926598cb8b0c1f6d39a61846b8d954aa82" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec1c50d216cb0f486f6a48f59f70b0abd18396c8e8178e6e9d210cc59d37bf8" dependencies = [ "core-foundation-sys", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 0400062..04b3637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ cli = [ "dep:simple_logger", "dep:log", ] -# No-op: hidra's nusb backend is always built in and picked at run time. +# 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]] @@ -36,7 +37,7 @@ required-features = ["cli"] [dependencies] # Both backends built in; `DeviceSelector` falls back between them at run time. -hidra = "0.0.3" +hidra = "0.0.4" ihex = "3.0" thiserror = "2.0" phf = { version = "0.11", features = ["macros"] } @@ -63,7 +64,3 @@ chrono = "0.4.41" predicates = "3.1.3" serial_test = "3.2.0" stdext = "0.3.3" - -# TODO: drop once hidra#8 lands and the backend-dispatch API is on crates.io. -[patch.crates-io] -hidra = { git = "https://github.com/carlossless/hidra", rev = "a9bff3926598cb8b0c1f6d39a61846b8d954aa82" }