diff --git a/Cargo.lock b/Cargo.lock index daa3918..a9022a2 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=b1d19bc86809932f6ab9192b24ae11fb649646c4#b1d19bc86809932f6ab9192b24ae11fb649646c4" 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..434fde9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ cli = [ ] # No-op: hidra's nusb backend is always built in and picked at run time. nusb = [] +# On wasm, lets an ISPDevice be driven over WebUSB as well as WebHID. +webusb = ["hidra/nusb"] [[bin]] name = "sinowisp" @@ -66,4 +68,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 = "b1d19bc86809932f6ab9192b24ae11fb649646c4" } diff --git a/src/device_selector.rs b/src/device_selector.rs index 7d42ef7..1668310 100644 --- a/src/device_selector.rs +++ b/src/device_selector.rs @@ -9,7 +9,7 @@ use hidra::{ 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}; @@ -322,7 +322,11 @@ impl DeviceSelector { .wait() .map_err(DeviceSelectorError::from)?; - Ok(ISPDevice::new(device_spec, cmd_handle, Some(xfer_handle))) + Ok(ISPDevice::new( + device_spec, + cmd_handle, + Some(ISPHandle::from(xfer_handle)), + )) }; } diff --git a/src/isp_device.rs b/src/isp_device.rs index f46f799..2744888 100644 --- a/src/isp_device.rs +++ b/src/isp_device.rs @@ -20,6 +20,49 @@ const CMD_REBOOT: u8 = 0x5a; const XFER_READ_PAGE: u8 = 0x72; const XFER_WRITE_PAGE: u8 = 0x77; +/// A HID handle an [`ISPDevice`] talks through. +/// +/// On wasm a device whose interface declares a vendor-specific class is +/// reachable over `WebUSB` but never appears in `WebHID`, so both are accepted. +pub enum ISPHandle { + /// A `WebHID` handle natively, or a per-OS/nusb one. + Hid(HidDevice), + /// A `WebUSB` handle. + #[cfg(all(target_arch = "wasm32", feature = "webusb"))] + WebUsb(hidra::webusb::HidDevice), +} + +impl From for ISPHandle { + fn from(device: HidDevice) -> Self { + ISPHandle::Hid(device) + } +} + +#[cfg(all(target_arch = "wasm32", feature = "webusb"))] +impl From for ISPHandle { + fn from(device: hidra::webusb::HidDevice) -> Self { + ISPHandle::WebUsb(device) + } +} + +impl ISPHandle { + async fn send_feature_report(&self, data: &[u8]) -> Result<(), HidError> { + match self { + ISPHandle::Hid(device) => device.send_feature_report(data).await, + #[cfg(all(target_arch = "wasm32", feature = "webusb"))] + ISPHandle::WebUsb(device) => device.send_feature_report(data).await, + } + } + + async fn get_feature_report(&self, buf: &mut [u8]) -> Result { + match self { + ISPHandle::Hid(device) => device.get_feature_report(buf).await, + #[cfg(all(target_arch = "wasm32", feature = "webusb"))] + ISPHandle::WebUsb(device) => device.get_feature_report(buf).await, + } + } +} + /// One open connection to a device in ISP bootloader mode. /// /// The methods are the individual protocol operations; they perform no @@ -27,10 +70,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 +132,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 +147,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..c2c7386 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,11 @@ //! 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). +//! (by `.await`-ing them on `wasm32`). On wasm the handle is a WebHID device, +//! or a WebUSB one for devices the browser will not expose as HID. //! //! The crate carries no orchestration, timing, or UI: composing these //! primitives into full read/write cycles, inserting the post-erase/reboot