diff --git a/Cargo.lock b/Cargo.lock index 7fe9ab3..1f66f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,9 +345,9 @@ checksum = "efb8ae90b8d0165be79ff98d845bebbbc9dfa0295ff2ce86beba525d93b9ba60" [[package]] name = "hidra" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4861c589ab4c739001e0a62db96286e217561221e838095fcd8d31bf4f155e7b" +checksum = "8ec1c50d216cb0f486f6a48f59f70b0abd18396c8e8178e6e9d210cc59d37bf8" dependencies = [ "core-foundation-sys", "js-sys", @@ -524,19 +524,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 0aecbe7..04b3637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,9 @@ 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" @@ -37,10 +36,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 = "0.0.4" ihex = "3.0" thiserror = "2.0" phf = { version = "0.11", features = ["macros"] } @@ -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" diff --git a/src/device_selector.rs b/src/device_selector.rs index 5e74792..e11c248 100644 --- a/src/device_selector.rs +++ b/src/device_selector.rs @@ -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}; @@ -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), + Nusb(Hidra), +} + +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 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 { + 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 { - 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 @@ -100,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 @@ -136,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) @@ -154,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 { @@ -167,7 +236,7 @@ impl DeviceSelector { } } Err(err) => { - descriptor = Err(DeviceSelectorError::from(err)); + descriptor = Err(err); feature_report_ids = Err(DeviceSelectorError::NotFound); } } @@ -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)) }; @@ -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 { + 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 @@ -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 { if let Err(err) = self.enter_isp_mode(&device) { @@ -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)); } @@ -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(()) diff --git a/src/isp_device.rs b/src/isp_device.rs index f46f799..b991cff 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..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 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). @@ -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};