commit f3f58f9ab568452ce48613adb3ad0467eff6815d Author: G2-Games Date: Tue Jan 30 11:21:31 2024 -0600 First code commit diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..50910bd --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,11 @@ +install: + - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly + - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin + - rustc -V + - cargo -V + +build: false + +test_script: + - cargo test --locked diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..db7e33d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(target_family = "wasm")'] +rustflags = ["--cfg=web_sys_unstable_apis"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be70f45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log + +www diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7a91325 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,69 @@ +language: rust +sudo: false + +cache: cargo + +matrix: + include: + + # Builds with wasm-pack. + - rust: beta + env: RUST_BACKTRACE=1 + addons: + firefox: latest + chrome: stable + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f + script: + - cargo generate --git . --name testing + # Having a broken Cargo.toml (in that it has curlies in fields) anywhere + # in any of our parent dirs is problematic. + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - wasm-pack build + - wasm-pack test --chrome --firefox --headless + + # Builds on nightly. + - rust: nightly + env: RUST_BACKTRACE=1 + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - rustup target add wasm32-unknown-unknown + script: + - cargo generate --git . --name testing + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - cargo check + - cargo check --target wasm32-unknown-unknown + - cargo check --no-default-features + - cargo check --target wasm32-unknown-unknown --no-default-features + - cargo check --no-default-features --features console_error_panic_hook + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook + - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" + - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" + + # Builds on beta. + - rust: beta + env: RUST_BACKTRACE=1 + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - rustup target add wasm32-unknown-unknown + script: + - cargo generate --git . --name testing + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - cargo check + - cargo check --target wasm32-unknown-unknown + - cargo check --no-default-features + - cargo check --target wasm32-unknown-unknown --no-default-features + - cargo check --no-default-features --features console_error_panic_hook + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook + # Note: no enabling the `wee_alloc` feature here because it requires + # nightly for now. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d877a51 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "cross-usb" +version = "0.1.0" +authors = ["G2-Games "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[target.'cfg(target_family = "wasm")'.dependencies] +wasm-bindgen = "0.2.84" +wasm-bindgen-futures = "0.4.39" +js-sys = "0.3.67" + +[target.'cfg(target_family = "wasm")'.dependencies.web-sys] +version = "0.3" +features = [ + "Window", + "Navigator", + "console", + "Usb", + "UsbDevice", + "UsbInterface", + "UsbRecipient", + "UsbRequestType", + "UsbControlTransferParameters", + "UsbDeviceRequestOptions", + "Storage" +] + +[target.'cfg(not(target_family = "wasm"))'.dependencies] +nusb = "0.1.2" + +[profile.release] +opt-level = "s" + +[package.metadata.wasm-pack.profile.dev.wasm-bindgen] +dwarf-debug-info = true diff --git a/src/backend/native.rs b/src/backend/native.rs new file mode 100644 index 0000000..75c2995 --- /dev/null +++ b/src/backend/native.rs @@ -0,0 +1,130 @@ +use std::error::Error; +use nusb; + +use crate::usb::{ControlIn, ControlOut, ControlType, Device, Interface, Recipient}; + +pub struct UsbDevice { + device: nusb::Device, +} + +pub struct UsbInterface { + interface: nusb::Interface, +} + +/// Gets a single device from the VendorID and ProductID +pub async fn get_device(vendor_id: u16, product_id: u16) -> Result> { + let devices = nusb::list_devices().unwrap(); + + let mut device_info = None; + for device in devices { + if device.vendor_id() == vendor_id && device.product_id() == product_id { + device_info = Some(device); + break; + } + } + + let device_info = match device_info { + Some(dev) => dev, + None => return Err("Device not found".into()), + }; + + let device = device_info.open()?; + + Ok(UsbDevice { device }) +} + +impl Device for UsbDevice { + type UsbDevice = UsbDevice; + type UsbInterface = UsbInterface; + + async fn open_interface(&self, number: u8) -> Result> { + let interface = match self.device.claim_interface(number) { + Ok(inter) => inter, + Err(e) => return Err(e.into()), + }; + + Ok(UsbInterface { interface }) + } + + async fn reset(&self) -> Result<(), Box> { + match self.device.reset() { + Ok(_) => Ok(()), + Err(e) => Err(e.into()) + } + } +} + +impl<'a> Interface<'a> for UsbInterface { + async fn control_in(&self, data: ControlIn) -> Result, Box> { + Ok(self.interface.control_in(data.into()).await.into_result()?) + } + + async fn control_out(&self, data: ControlOut<'a>) -> Result<(), Box> { + match self.interface.control_out(data.into()).await.into_result() { + Ok(_) => Ok(()), + Err(e) => Err(e.into()) + } + } + + async fn bulk_in(&self, endpoint: u8, buf: Vec) -> Result, Box> { + let buf_len = buf.len(); + let request_buffer = nusb::transfer::RequestBuffer::reuse(buf, buf_len); + + Ok(self.interface.bulk_in(endpoint, request_buffer).await.into_result()?) + } + + async fn bulk_out(&self, endpoint: u8, buf: Vec) -> Result<(), Box> { + match self.interface.bulk_out(endpoint, buf).await.into_result() { + Ok(_) => Ok(()), + Err(e) => Err(e.into()) + } + } +} + +impl From for nusb::transfer::ControlIn { + fn from(val: ControlIn) -> Self { + nusb::transfer::ControlIn { + control_type: val.control_type.into(), + recipient: val.recipient.into(), + request: val.request, + value: val.value, + index: val.index, + length: val.length, + } + } +} + +impl<'a> From> for nusb::transfer::ControlOut<'a> { + fn from(val: ControlOut<'a>) -> Self { + nusb::transfer::ControlOut { + control_type: val.control_type.into(), + recipient: val.recipient.into(), + request: val.request, + value: val.value, + index: val.index, + data: val.data, + } + } +} + +impl From for nusb::transfer::ControlType { + fn from(val: ControlType) -> Self { + match val { + ControlType::Standard => nusb::transfer::ControlType::Standard, + ControlType::Class => nusb::transfer::ControlType::Class, + ControlType::Vendor => nusb::transfer::ControlType::Vendor, + } + } +} + +impl From for nusb::transfer::Recipient { + fn from(val: Recipient) -> Self { + match val { + + Recipient::Device => nusb::transfer::Recipient::Device, + Recipient::Interface => nusb::transfer::Recipient::Interface, + Recipient::Endpoint => nusb::transfer::Recipient::Endpoint, + Recipient::Other => nusb::transfer::Recipient::Other, + } + } +} diff --git a/src/backend/wasm.rs b/src/backend/wasm.rs new file mode 100644 index 0000000..57b623a --- /dev/null +++ b/src/backend/wasm.rs @@ -0,0 +1,196 @@ +#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] +use std::error::Error; +use wasm_bindgen::prelude::*; +use js_sys::JSON; + +use web_sys::{ + console, + Usb, + UsbDevice as WasmUsbDevice, + UsbInterface as WasmUsbInterface, + UsbControlTransferParameters, + UsbRecipient, + UsbRequestType, + UsbDeviceRequestOptions, +}; +use js_sys::{Array, Uint8Array, Promise}; +use wasm_bindgen_futures::JsFuture; + +// Crate stuff +use crate::usb::{ControlIn, ControlOut, ControlType, Device, Interface, Recipient}; + +#[wasm_bindgen] +pub struct UsbDevice { + device: WasmUsbDevice, +} + +#[wasm_bindgen] +pub struct UsbInterface { + device: WasmUsbDevice, +} + +/// Gets a single device from the VendorID and ProductID +#[wasm_bindgen] +pub async fn get_device(vendor_id: u16, product_id: u16) -> Result { + let window = web_sys::window().unwrap(); + + let navigator = window.navigator(); + let usb = navigator.usb(); + + let arr = Array::new(); + let filter1 = js_sys::Object::new(); + js_sys::Reflect::set( + &filter1, + &JsValue::from_str("vendorId"), + &JsValue::from(vendor_id), + ) + .unwrap(); + js_sys::Reflect::set( + &filter1, + &JsValue::from_str("productId"), + &JsValue::from(product_id), + ) + .unwrap(); + arr.push(&filter1); + let filters = JsValue::from(&arr); + + let filters2 = UsbDeviceRequestOptions::new(&filters); + + let device_promise = JsFuture::from(Promise::resolve(&usb.request_device(&filters2))).await; + + let device: WasmUsbDevice = match device_promise { + Ok(res) => res.into(), + Err(err) => { + console::log_1(&err.clone().into()); + return Err(err.into()) + }, + }; + + let _open_promise = JsFuture::from(Promise::resolve(&device.open())); + + console::log_1(&"got device".into()); + + Ok(UsbDevice { + device + }) +} + +impl Device for UsbDevice { + type UsbDevice = UsbDevice; + type UsbInterface = UsbInterface; + + async fn open_interface(&self, number: u8) -> Result> { + let dev_promise = JsFuture::from(Promise::resolve(&self.device.claim_interface(number))); + + // Wait for the interface to be claimed + let result = dev_promise; + + Ok(UsbInterface { + device: self.device.clone() + }) + } + + async fn reset(&self) -> Result<(), Box> { + let promise = Promise::resolve(&self.device.reset()); + + let result = JsFuture::from(promise).await; + + match result { + Ok(_) => Ok(()), + Err(_) => { + console::log_1(&"Cancelled".into()); + return Err("cancelled".into()) + }, + } + } +} + +impl<'a> Interface<'a> for UsbInterface { + async fn control_in(&self, data: crate::usb::ControlIn) -> Result, Box> { + let length = data.length; + let params = data.into(); + let promise = Promise::resolve(&self.device.control_transfer_in(¶ms, length)); + + let mut result = JsFuture::from(promise).await; + + let data = match result { + Ok(res) => res.into(), + Err(_) => { + console::log_1(&"Cancelled".into()); + return Err("cancelled".into()) + }, + }; + + let unitarray = Uint8Array::new(&data); + + Ok(unitarray.to_vec()) + } + + async fn control_out(&self, data: crate::usb::ControlOut<'a>) -> Result<(), Box> { + let params = data.into(); + let promise = Promise::resolve(&self.device.control_transfer_out(¶ms)); + + let mut result = JsFuture::from(promise).await; + + match result { + Ok(_) => Ok(()), + Err(err) => { + console::log_1(&"Cancelled".into()); + Err(format!("{:?}", err).into()) + }, + } + } + + async fn bulk_in(&self, _endpoint: u8, _buf: Vec) -> Result, Box> { + todo!() + } + + async fn bulk_out(&self, _endpoint: u8, _buf: Vec) -> Result<(), Box> { + todo!() + } +} + +impl From for UsbControlTransferParameters { + fn from(value: ControlIn) -> Self { + UsbControlTransferParameters::new( + value.index, + value.recipient.into(), + value.request.into(), + value.control_type.into(), + value.value + ) + } +} + +impl From> for UsbControlTransferParameters { + fn from(value: ControlOut) -> Self { + UsbControlTransferParameters::new( + value.index, + value.recipient.into(), + value.request.into(), + value.control_type.into(), + value.value + ) + } +} + +impl From for UsbRecipient { + fn from(value: Recipient) -> Self { + match value { + Recipient::Device => UsbRecipient::Device, + Recipient::Interface => UsbRecipient::Interface, + Recipient::Endpoint => UsbRecipient::Endpoint, + Recipient::Other => UsbRecipient::Other, + } + } +} + +impl From for UsbRequestType { + fn from(value: ControlType) -> Self { + match value { + ControlType::Standard => UsbRequestType::Standard, + ControlType::Class => UsbRequestType::Class, + ControlType::Vendor => UsbRequestType::Vendor, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d9f7e2f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod usb; + +#[cfg(not(target_family = "wasm"))] +#[path = "./backend/native.rs"] +pub mod context; + +#[cfg(target_family = "wasm")] +#[path = "./backend/wasm.rs"] +pub mod context; diff --git a/src/usb.rs b/src/usb.rs new file mode 100644 index 0000000..cde8d18 --- /dev/null +++ b/src/usb.rs @@ -0,0 +1,80 @@ +use std::error::Error; + +use crate::context::UsbInterface; + +/// A unique USB device +pub trait Device { + type UsbDevice; + type UsbInterface; + + async fn open_interface(&self, number: u8) -> Result>; + async fn reset(&self) -> Result<(), Box>; + + //TODO: Implement these placeholders + async fn product_id(&self) -> u16 { + 0x00 + } + async fn vendor_id(&self) -> u16 { + 0x00 + } + async fn class(&self) -> u16 { + 0x00 + } + async fn subclass(&self) -> u16 { + 0x00 + } + async fn manufacturer_string(&self) -> Option<&str> { + None + } + async fn product_string(&self) -> Option<&str> { + None + } +} + +/// A specific interface of a USB device +pub trait Interface<'a> { + async fn control_in(&self, data: ControlIn) -> Result, Box>; + async fn control_out(&self, data: ControlOut<'a>) -> Result<(), Box>; + + async fn bulk_in(&self, endpoint: u8, buf: Vec) -> Result, Box>; + async fn bulk_out(&self, endpoint: u8, buf: Vec) -> Result<(), Box>; + + async fn interrupt_in(&self, _endpoint: u8, _buf: Vec) { + unimplemented!() + } + + async fn interrupt_out(&self, _endpoint: u8, _buf: Vec) { + unimplemented!() + } +} + +pub enum ControlType { + Standard = 0, + Class = 1, + Vendor = 2, +} + +pub enum Recipient { + Device = 0, + Interface = 1, + Endpoint = 2, + Other = 3, +} + +pub struct ControlIn { + pub control_type: ControlType, + pub recipient: Recipient, + pub request: u8, + pub value: u16, + pub index: u16, + pub length: u16, +} + +pub struct ControlOut<'a> { + pub control_type: ControlType, + pub recipient: Recipient, + pub request: u8, + pub value: u16, + pub index: u16, + pub data: &'a[u8], +}