use std::cell::RefCell;
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use crate::error::{HidError, HidResult};
use crate::{BusType, DeviceInfo};
pub use crate::report_info::{
reconstruct_descriptor, uses_report_ids, CollectionInfo, ReportInfo, ReportItemInfo, UnitSystem,
};
const INPUT_QUEUE_CAPACITY: usize = 64;
fn js_err(context: &str, e: JsValue) -> HidError {
let message = match e.dyn_ref::<web_sys::DomException>() {
Some(ex) => format!("{context}: {}: {}", ex.name(), ex.message()),
None => match e.as_string() {
Some(s) => format!("{context}: {s}"),
None => format!("{context}: {e:?}"),
},
};
HidError::backend(message)
}
fn dataview_to_vec(view: &js_sys::DataView) -> Vec<u8> {
let buffer = view.buffer();
let buffer: &JsValue = buffer.as_ref();
js_sys::Uint8Array::new_with_byte_offset_and_length(
buffer,
view.byte_offset() as u32,
view.byte_length() as u32,
)
.to_vec()
}
pub struct EventListenerHandle {
target: web_sys::EventTarget,
event: &'static str,
closure: Option<Closure<dyn FnMut(web_sys::Event)>>,
}
impl EventListenerHandle {
fn add(
target: &web_sys::EventTarget,
event: &'static str,
closure: Closure<dyn FnMut(web_sys::Event)>,
) -> Self {
let _ = target.add_event_listener_with_callback(event, closure.as_ref().unchecked_ref());
EventListenerHandle {
target: target.clone(),
event,
closure: Some(closure),
}
}
pub fn forget(mut self) {
if let Some(closure) = self.closure.take() {
closure.forget();
}
}
}
impl Drop for EventListenerHandle {
fn drop(&mut self) {
if let Some(closure) = &self.closure {
let _ = self
.target
.remove_event_listener_with_callback(self.event, closure.as_ref().unchecked_ref());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DeviceFilter {
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
pub usage_page: Option<u16>,
pub usage: Option<u16>,
}
impl DeviceFilter {
pub fn new() -> Self {
Self::default()
}
pub fn vendor_id(mut self, vendor_id: u16) -> Self {
self.vendor_id = Some(vendor_id);
self
}
pub fn product_id(mut self, product_id: u16) -> Self {
self.product_id = Some(product_id);
self
}
pub fn usage_page(mut self, usage_page: u16) -> Self {
self.usage_page = Some(usage_page);
self
}
pub fn usage(mut self, usage: u16) -> Self {
self.usage = Some(usage);
self
}
fn to_js(self) -> web_sys::HidDeviceFilter {
let filter = web_sys::HidDeviceFilter::new();
if let Some(vendor_id) = self.vendor_id {
filter.set_vendor_id(vendor_id.into());
}
if let Some(product_id) = self.product_id {
filter.set_product_id(product_id);
}
if let Some(usage_page) = self.usage_page {
filter.set_usage_page(usage_page);
}
if let Some(usage) = self.usage {
filter.set_usage(usage);
}
filter
}
}
pub struct WebHidApi {
hid: web_sys::Hid,
}
impl WebHidApi {
pub fn new() -> HidResult<Self> {
let window = web_sys::window().ok_or_else(|| HidError::Initialization {
message: "no global window object (WebHID requires a window context)".into(),
})?;
let hid = window.navigator().hid();
let hid_js: &JsValue = hid.as_ref();
if hid_js.is_undefined() || hid_js.is_null() {
return Err(HidError::Initialization {
message: "navigator.hid is unavailable (WebHID requires a secure context \
and a browser with WebHID support)"
.into(),
});
}
Ok(WebHidApi { hid })
}
pub async fn request_device(&self, filters: &[DeviceFilter]) -> HidResult<Vec<WebHidDevice>> {
let js_filters: Vec<web_sys::HidDeviceFilter> = filters.iter().map(|f| f.to_js()).collect();
let options = web_sys::HidDeviceRequestOptions::new(&js_filters);
let devices = JsFuture::from(self.hid.request_device(&options))
.await
.map_err(|e| js_err("requestDevice", e))?;
Ok(devices.iter().map(WebHidDevice::from_raw).collect())
}
pub async fn get_devices(&self) -> HidResult<Vec<WebHidDevice>> {
let devices = JsFuture::from(self.hid.get_devices())
.await
.map_err(|e| js_err("getDevices", e))?;
Ok(devices.iter().map(WebHidDevice::from_raw).collect())
}
pub fn on_connect(&self, f: impl FnMut(WebHidDevice) + 'static) -> EventListenerHandle {
self.connection_listener("connect", f)
}
pub fn on_disconnect(&self, f: impl FnMut(WebHidDevice) + 'static) -> EventListenerHandle {
self.connection_listener("disconnect", f)
}
fn connection_listener(
&self,
event: &'static str,
mut f: impl FnMut(WebHidDevice) + 'static,
) -> EventListenerHandle {
let closure = Closure::wrap(Box::new(move |ev: web_sys::Event| {
let ev: web_sys::HidConnectionEvent = ev.unchecked_into();
f(WebHidDevice::from_raw(ev.device()));
}) as Box<dyn FnMut(web_sys::Event)>);
EventListenerHandle::add(self.hid.as_ref(), event, closure)
}
pub fn raw(&self) -> &web_sys::Hid {
&self.hid
}
}
#[derive(Debug, Clone)]
pub struct WebHidDevice {
device: web_sys::HidDevice,
}
impl WebHidDevice {
pub fn from_raw(device: web_sys::HidDevice) -> Self {
WebHidDevice { device }
}
pub fn raw(&self) -> &web_sys::HidDevice {
&self.device
}
pub async fn open(&self) -> HidResult<()> {
JsFuture::from(self.device.open())
.await
.map_err(|e| match js_err("open", e) {
HidError::Backend { message } => HidError::OpenFailed { message },
other => other,
})?;
Ok(())
}
pub async fn close(&self) -> HidResult<()> {
JsFuture::from(self.device.close())
.await
.map_err(|e| js_err("close", e))?;
Ok(())
}
pub async fn forget(&self) -> HidResult<()> {
let device_js: &JsValue = self.device.as_ref();
let method = js_sys::Reflect::get(device_js, &JsValue::from_str("forget"))
.map_err(|e| js_err("forget", e))?;
let method: js_sys::Function = method.dyn_into().map_err(|_| HidError::Unsupported {
message: "HIDDevice.forget() is not supported by this browser".into(),
})?;
let promise: js_sys::Promise = method
.call0(device_js)
.map_err(|e| js_err("forget", e))?
.dyn_into()
.map_err(|_| HidError::backend("forget: did not return a Promise"))?;
JsFuture::from(promise)
.await
.map_err(|e| js_err("forget", e))?;
Ok(())
}
pub fn opened(&self) -> bool {
self.device.opened()
}
pub fn vendor_id(&self) -> u16 {
self.device.vendor_id()
}
pub fn product_id(&self) -> u16 {
self.device.product_id()
}
pub fn product_name(&self) -> Option<String> {
let name = self.device.product_name();
if name.is_empty() {
None
} else {
Some(name)
}
}
pub fn device_info(&self) -> DeviceInfo {
let vendor_id = self.vendor_id();
let product_id = self.product_id();
let product = self.product_name();
let (usage_page, usage) = self
.collections()
.first()
.map(|c| (c.usage_page, c.usage))
.unwrap_or((0, 0));
DeviceInfo {
path: format!(
"webhid:{:04x}:{:04x}:{}",
vendor_id,
product_id,
product.as_deref().unwrap_or("")
),
vendor_id,
product_id,
product_string: product,
usage_page,
usage,
interface_number: -1,
bus_type: BusType::Unknown,
..Default::default()
}
}
pub async fn write(&self, data: &[u8]) -> HidResult<usize> {
let (report_id, payload) = data.split_first().ok_or_else(|| HidError::InvalidData {
message: "write requires at least the report ID byte".into(),
})?;
let mut payload = payload.to_vec();
let promise = self
.device
.send_report_with_u8_slice(*report_id, &mut payload)
.map_err(|e| js_err("sendReport", e))?;
JsFuture::from(promise)
.await
.map_err(|e| js_err("sendReport", e))?;
Ok(data.len())
}
pub async fn send_feature_report(&self, data: &[u8]) -> HidResult<()> {
let (report_id, payload) = data.split_first().ok_or_else(|| HidError::InvalidData {
message: "send_feature_report requires at least the report ID byte".into(),
})?;
let mut payload = payload.to_vec();
let promise = self
.device
.send_feature_report_with_u8_slice(*report_id, &mut payload)
.map_err(|e| js_err("sendFeatureReport", e))?;
JsFuture::from(promise)
.await
.map_err(|e| js_err("sendFeatureReport", e))?;
Ok(())
}
pub async fn get_feature_report(&self, report_id: u8) -> HidResult<Vec<u8>> {
let view = JsFuture::from(self.device.receive_feature_report(report_id))
.await
.map_err(|e| js_err("receiveFeatureReport", e))?;
let data = dataview_to_vec(&view);
if report_id != 0 {
Ok(data)
} else {
let mut report = Vec::with_capacity(data.len() + 1);
report.push(0);
report.extend_from_slice(&data);
Ok(report)
}
}
pub fn on_input_report(&self, mut f: impl FnMut(u8, Vec<u8>) + 'static) -> EventListenerHandle {
let closure = Closure::wrap(Box::new(move |ev: web_sys::Event| {
let ev: web_sys::HidInputReportEvent = ev.unchecked_into();
f(ev.report_id(), dataview_to_vec(&ev.data()));
}) as Box<dyn FnMut(web_sys::Event)>);
EventListenerHandle::add(self.device.as_ref(), "inputreport", closure)
}
pub fn start_reading(&self) -> InputReportStream {
let numbered = uses_report_ids(&self.collections());
let state = Rc::new(RefCell::new(StreamState::default()));
let shared = state.clone();
let closure = Closure::wrap(Box::new(move |ev: web_sys::Event| {
let ev: web_sys::HidInputReportEvent = ev.unchecked_into();
let data = dataview_to_vec(&ev.data());
let report = if numbered {
let mut buf = Vec::with_capacity(data.len() + 1);
buf.push(ev.report_id());
buf.extend_from_slice(&data);
buf
} else {
data
};
let mut state = shared.borrow_mut();
if state.queue.len() >= INPUT_QUEUE_CAPACITY {
state.queue.pop_front();
}
state.queue.push_back(report);
if let Some(waker) = state.waker.take() {
waker.wake();
}
}) as Box<dyn FnMut(web_sys::Event)>);
let listener = EventListenerHandle::add(self.device.as_ref(), "inputreport", closure);
InputReportStream {
state,
_listener: listener,
}
}
pub fn collections(&self) -> Vec<CollectionInfo> {
self.device
.collections()
.iter()
.map(|c| convert_collection(&c))
.collect()
}
pub fn report_descriptor(&self) -> HidResult<Vec<u8>> {
Ok(reconstruct_descriptor(&self.collections()))
}
pub fn parsed_report_descriptor(&self) -> HidResult<crate::descriptor::ReportDescriptor> {
crate::descriptor::ReportDescriptor::parse(&self.report_descriptor()?)
}
}
#[derive(Default)]
struct StreamState {
queue: VecDeque<Vec<u8>>,
waker: Option<Waker>,
}
pub struct InputReportStream {
state: Rc<RefCell<StreamState>>,
_listener: EventListenerHandle,
}
impl InputReportStream {
pub async fn read(&mut self) -> HidResult<Vec<u8>> {
ReadFuture {
state: self.state.clone(),
}
.await
}
pub fn try_read(&mut self) -> Option<Vec<u8>> {
self.state.borrow_mut().queue.pop_front()
}
pub(crate) fn next_report(&self) -> impl Future<Output = HidResult<Vec<u8>>> {
ReadFuture {
state: self.state.clone(),
}
}
}
struct ReadFuture {
state: Rc<RefCell<StreamState>>,
}
impl Future for ReadFuture {
type Output = HidResult<Vec<u8>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut state = self.state.borrow_mut();
match state.queue.pop_front() {
Some(report) => Poll::Ready(Ok(report)),
None => {
state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
}
fn convert_collection(collection: &web_sys::HidCollectionInfo) -> CollectionInfo {
CollectionInfo {
usage_page: collection.get_usage_page().unwrap_or(0),
usage: collection.get_usage().unwrap_or(0),
collection_type: collection.get_type().unwrap_or(0),
children: collection
.get_children()
.map(|children| children.iter().map(|c| convert_collection(&c)).collect())
.unwrap_or_default(),
input_reports: convert_reports(collection.get_input_reports()),
output_reports: convert_reports(collection.get_output_reports()),
feature_reports: convert_reports(collection.get_feature_reports()),
}
}
fn convert_reports(reports: Option<js_sys::Array<web_sys::HidReportInfo>>) -> Vec<ReportInfo> {
reports
.map(|reports| {
reports
.iter()
.map(|report| ReportInfo {
report_id: report.get_report_id().unwrap_or(0),
items: report
.get_items()
.map(|items| items.iter().map(|i| convert_item(&i)).collect())
.unwrap_or_default(),
})
.collect()
})
.unwrap_or_default()
}
fn convert_unit_system(system: web_sys::HidUnitSystem) -> UnitSystem {
match system {
web_sys::HidUnitSystem::None => UnitSystem::None,
web_sys::HidUnitSystem::SiLinear => UnitSystem::SiLinear,
web_sys::HidUnitSystem::SiRotation => UnitSystem::SiRotation,
web_sys::HidUnitSystem::EnglishLinear => UnitSystem::EnglishLinear,
web_sys::HidUnitSystem::EnglishRotation => UnitSystem::EnglishRotation,
web_sys::HidUnitSystem::VendorDefined => UnitSystem::VendorDefined,
web_sys::HidUnitSystem::Reserved => UnitSystem::Reserved,
_ => UnitSystem::Reserved,
}
}
fn convert_item(item: &web_sys::HidReportItem) -> ReportItemInfo {
ReportItemInfo {
usages: item
.get_usages()
.map(|usages| usages.iter().map(|u| u.value_of() as u32).collect())
.unwrap_or_default(),
usage_minimum: item.get_usage_minimum().unwrap_or(0),
usage_maximum: item.get_usage_maximum().unwrap_or(0),
report_size: item.get_report_size().unwrap_or(0),
report_count: item.get_report_count().unwrap_or(0),
logical_minimum: item.get_logical_minimum().unwrap_or(0),
logical_maximum: item.get_logical_maximum().unwrap_or(0),
physical_minimum: item.get_physical_minimum().unwrap_or(0),
physical_maximum: item.get_physical_maximum().unwrap_or(0),
unit_exponent: item.get_unit_exponent().unwrap_or(0),
unit_system: item
.get_unit_system()
.map(convert_unit_system)
.unwrap_or(UnitSystem::None),
unit_factor_length_exponent: item.get_unit_factor_length_exponent().unwrap_or(0),
unit_factor_mass_exponent: item.get_unit_factor_mass_exponent().unwrap_or(0),
unit_factor_time_exponent: item.get_unit_factor_time_exponent().unwrap_or(0),
unit_factor_temperature_exponent: item.get_unit_factor_temperature_exponent().unwrap_or(0),
unit_factor_current_exponent: item.get_unit_factor_current_exponent().unwrap_or(0),
unit_factor_luminous_intensity_exponent: item
.get_unit_factor_luminous_intensity_exponent()
.unwrap_or(0),
is_absolute: item.get_is_absolute().unwrap_or(true),
is_array: item.get_is_array().unwrap_or(false),
is_buffered_bytes: item.get_is_buffered_bytes().unwrap_or(false),
is_constant: item.get_is_constant().unwrap_or(false),
is_linear: item.get_is_linear().unwrap_or(true),
is_range: item.get_is_range().unwrap_or(false),
is_volatile: item.get_is_volatile().unwrap_or(false),
has_null: item.get_has_null().unwrap_or(false),
has_preferred_state: item.get_has_preferred_state().unwrap_or(true),
wrap: item.get_wrap().unwrap_or(false),
}
}