From d8cbc7c9e481d8fea11c3f435e2055fec84236ac Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 24 Jan 2024 12:57:49 -0500 Subject: [PATCH 1/5] Cache the HID instance and gaurd against reopening the device --- nodehid.js | 49 +++++++++++++++++++------------------------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/nodehid.js b/nodehid.js index 507038a..2e5f4a7 100644 --- a/nodehid.js +++ b/nodehid.js @@ -29,54 +29,40 @@ function loadBinding() { //This class is a wrapper for `binding.HID` class function HID() { - - // see issue #150 (enhancement, solves issue #149) - // throw an error for those who forget to instantiate, i.e. by "*new* HID.HID()" - // and who would otherwise be left trying to figure out why "self.on is not a function" if (!new.target) { throw new Error('HID() must be called with \'new\' operator'); } - //Inherit from EventEmitter EventEmitter.call(this); - loadBinding(); - - /* We also want to inherit from `binding.HID`, but unfortunately, - it's not so easy for native Objects. For example, the - following won't work since `new` keyword isn't used: + // Check if an instance already exists in the cache + if (HID.cachedInstance) { + return HID.cachedInstance; + } - `binding.HID.apply(this, arguments);` + loadBinding(); - So... we do this craziness instead... - */ var thisPlusArgs = new Array(arguments.length + 1); thisPlusArgs[0] = null; for(var i = 0; i < arguments.length; i++) thisPlusArgs[i + 1] = arguments[i]; - this._raw = new (Function.prototype.bind.apply(binding.HID, - thisPlusArgs) )(); - - /* Now we have `this._raw` Object from which we need to - inherit. So, one solution is to simply copy all - prototype methods over to `this` and binding them to - `this._raw` - */ - for(var i in binding.HID.prototype) - this[i] = binding.HID.prototype[i].bind(this._raw); - - /* We are now done inheriting from `binding.HID` and EventEmitter. - - Now upon adding a new listener for "data" events, we start - polling the HID device using `read(...)` - See `resume()` for more details. */ + + this._raw = new (Function.prototype.bind.apply(binding.HID, thisPlusArgs))(); + + // Cache this instance for future calls + HID.cachedInstance = this; + + for(var key in binding.HID.prototype) + this[key] = binding.HID.prototype[key].bind(this._raw); + this._paused = true; var self = this; self.on("newListener", function(eventName, listener) { if(eventName == "data") - process.nextTick(self.resume.bind(self) ); + process.nextTick(self.resume.bind(self)); }); } + //Inherit prototype methods util.inherits(HID, EventEmitter); //Don't inherit from `binding.HID`; that's done above already! @@ -135,6 +121,9 @@ function showdevices() { return binding.devices.apply(HID,arguments); } +// Static property for caching the instance +HID.cachedInstance = null; + //Expose API exports.HID = HID; exports.devices = showdevices; From 5338f62579e5e51be3ac09e75b39bd5810b7d315 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 24 Jan 2024 13:38:26 -0500 Subject: [PATCH 2/5] Add the deleted comments back --- nodehid.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nodehid.js b/nodehid.js index 2e5f4a7..0e13742 100644 --- a/nodehid.js +++ b/nodehid.js @@ -29,10 +29,15 @@ function loadBinding() { //This class is a wrapper for `binding.HID` class function HID() { + + // see issue #150 (enhancement, solves issue #149) + // throw an error for those who forget to instantiate, i.e. by "*new* HID.HID()" + // and who would otherwise be left trying to figure out why "self.on is not a function" if (!new.target) { throw new Error('HID() must be called with \'new\' operator'); } + //Inherit from EventEmitter EventEmitter.call(this); // Check if an instance already exists in the cache @@ -52,9 +57,19 @@ function HID() { // Cache this instance for future calls HID.cachedInstance = this; + /* Now we have `this._raw` Object from which we need to + inherit. So, one solution is to simply copy all + prototype methods over to `this` and binding them to + `this._raw` + */ for(var key in binding.HID.prototype) this[key] = binding.HID.prototype[key].bind(this._raw); + /* We are now done inheriting from `binding.HID` and EventEmitter. + Now upon adding a new listener for "data" events, we start + polling the HID device using `read(...)` + See `resume()` for more details. */ + this._paused = true; var self = this; self.on("newListener", function(eventName, listener) { From 78601b855a3f5ea8bf0fa5f24bc3cd1d97d29060 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 24 Jan 2024 13:40:04 -0500 Subject: [PATCH 3/5] Cleanup --- nodehid.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nodehid.js b/nodehid.js index 0e13742..647da3c 100644 --- a/nodehid.js +++ b/nodehid.js @@ -58,17 +58,17 @@ function HID() { HID.cachedInstance = this; /* Now we have `this._raw` Object from which we need to - inherit. So, one solution is to simply copy all - prototype methods over to `this` and binding them to - `this._raw` + inherit. So, one solution is to simply copy all + prototype methods over to `this` and binding them to + `this._raw` */ for(var key in binding.HID.prototype) this[key] = binding.HID.prototype[key].bind(this._raw); /* We are now done inheriting from `binding.HID` and EventEmitter. - Now upon adding a new listener for "data" events, we start - polling the HID device using `read(...)` - See `resume()` for more details. */ + Now upon adding a new listener for "data" events, we start + polling the HID device using `read(...)` + See `resume()` for more details. */ this._paused = true; var self = this; From 8aa991bb5d7850d9226026943af6efb8e56615b6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 28 Mar 2024 21:35:12 -0400 Subject: [PATCH 4/5] Make sure that the correct version of node-hid is used that cached HID instance --- nodehid.js | 242 ++++++++++++++++------------------------------------- 1 file changed, 71 insertions(+), 171 deletions(-) diff --git a/nodehid.js b/nodehid.js index 5b4ac69..8c9cc37 100644 --- a/nodehid.js +++ b/nodehid.js @@ -1,79 +1,62 @@ +var os = require('os'); -const EventEmitter = require("events").EventEmitter; -const util = require("util"); +var EventEmitter = require('events').EventEmitter, + util = require('util'); -let driverType = null; +var driverType = null; function setDriverType(type) { - driverType = type; + driverType = type; } // lazy load the C++ binding -let binding = null; +var binding = null; function loadBinding() { - if (!binding) { - const options = require('./binding-options'); - if (process.platform === "linux" && (!driverType || driverType === "hidraw")) { - options.name = 'HID_hidraw'; - } - binding = require("pkg-prebuilds/bindings")(__dirname, options); + if (!binding) { + if (os.platform() === 'linux') { + // Linux defaults to hidraw + if (!driverType || driverType === 'hidraw') { + binding = require('bindings')('HID_hidraw.node'); + } else { + binding = require('bindings')('HID.node'); + } + } else { + binding = require('bindings')('HID.node'); } + } } //This class is a wrapper for `binding.HID` class function HID() { + if (!new.target) { + throw new Error("HID() must be called with 'new' operator"); + } - // see issue #150 (enhancement, solves issue #149) - // throw an error for those who forget to instantiate, i.e. by "*new* HID.HID()" - // and who would otherwise be left trying to figure out why "self.on is not a function" - if (!new.target) { - throw new Error('HID() must be called with \'new\' operator'); - } + EventEmitter.call(this); - //Inherit from EventEmitter - EventEmitter.call(this); + // Check if an instance already exists in the cache + if (HID.cachedInstance) { + return HID.cachedInstance; + } - // Check if an instance already exists in the cache - if (HID.cachedInstance) { - return HID.cachedInstance; - } + loadBinding(); - loadBinding(); - - /* We also want to inherit from `binding.HID`, but unfortunately, - it's not so easy for native Objects. For example, the - following won't work since `new` keyword isn't used: - `binding.HID.apply(this, arguments);` - So... we do this craziness instead... - */ - var thisPlusArgs = new Array(arguments.length + 1); - thisPlusArgs[0] = null; - for(var i = 0; i < arguments.length; i++) - thisPlusArgs[i + 1] = arguments[i]; - - this._raw = new (Function.prototype.bind.apply(binding.HID, thisPlusArgs))(); - - // Cache this instance for future calls - HID.cachedInstance = this; - - /* Now we have `this._raw` Object from which we need to - inherit. So, one solution is to simply copy all - prototype methods over to `this` and binding them to - `this._raw` - */ - for(var key in binding.HID.prototype) - this[key] = binding.HID.prototype[key].bind(this._raw); - - /* We are now done inheriting from `binding.HID` and EventEmitter. - Now upon adding a new listener for "data" events, we start - polling the HID device using `read(...)` - See `resume()` for more details. */ - - this._paused = true; - var self = this; - self.on("newListener", function(eventName, listener) { - if(eventName == "data") - process.nextTick(self.resume.bind(self)); - }); + var thisPlusArgs = new Array(arguments.length + 1); + thisPlusArgs[0] = null; + for (var i = 0; i < arguments.length; i++) thisPlusArgs[i + 1] = arguments[i]; + + this._raw = new (Function.prototype.bind.apply(binding.HID, thisPlusArgs))(); + + // Cache this instance for future calls + HID.cachedInstance = this; + + for (var key in binding.HID.prototype) + this[key] = binding.HID.prototype[key].bind(this._raw); + + this._paused = true; + var self = this; + self.on('newListener', function (eventName, listener) { + if (eventName == 'data') process.nextTick(self.resume.bind(self)); + }); } //Inherit prototype methods @@ -81,18 +64,18 @@ util.inherits(HID, EventEmitter); //Don't inherit from `binding.HID`; that's done above already! HID.prototype.close = function close() { - this._closing = true; - this.removeAllListeners(); - this._raw.close(); - this._closed = true; + this._closing = true; + this.removeAllListeners(); + this._raw.close(); + this._closed = true; }; //Pauses the reader, which stops "data" events from being emitted HID.prototype.pause = function pause() { - this._paused = true; + this._paused = true; }; HID.prototype.read = function read(callback) { - if (this._closed) { + if (this._closed) { throw new Error('Unable to read from a closed HID device'); } else { return this._raw.read(callback); @@ -100,112 +83,31 @@ HID.prototype.read = function read(callback) { }; HID.prototype.resume = function resume() { - var self = this; - if(self._paused && self.listeners("data").length > 0) - { - //Start polling & reading loop - self._paused = false; - self.read(function readFunc(err, data) { - if(err) - { - //Emit error and pause reading - self._paused = true; - if(!self._closing) - self.emit("error", err); - //else ignore any errors if I'm closing the device - } - else - { - //If there are no "data" listeners, we pause - if(self.listeners("data").length <= 0) - self._paused = true; - //Keep reading if we aren't paused - if(!self._paused) - self.read(readFunc); - //Now emit the event - self.emit("data", data); - } - }); - } + var self = this; + if (self._paused && self.listeners('data').length > 0) { + //Start polling & reading loop + self._paused = false; + self.read(function readFunc(err, data) { + if (err) { + //Emit error and pause reading + self._paused = true; + if (!self._closing) self.emit('error', err); + //else ignore any errors if I'm closing the device + } else { + //If there are no "data" listeners, we pause + if (self.listeners('data').length <= 0) self._paused = true; + //Keep reading if we aren't paused + if (!self._paused) self.read(readFunc); + //Now emit the event + self.emit('data', data); + } + }); + } }; -class HIDAsync extends EventEmitter { - constructor(raw) { - super() - - if (!(raw instanceof binding.HIDAsync)) { - throw new Error(`HIDAsync cannot be constructed directly. Use HIDAsync.open() instead`) - } - - this._raw = raw - - /* Now we have `this._raw` Object from which we need to - inherit. So, one solution is to simply copy all - prototype methods over to `this` and binding them to - `this._raw`. - We explicitly wrap them in an async method, to ensure - that any thrown errors are promise rejections - */ - for (let i in this._raw) { - this[i] = async (...args) => this._raw[i](...args); - } - - /* Now upon adding a new listener for "data" events, we start - the read thread executing. See `resume()` for more details. - */ - this.on("newListener", (eventName, listener) =>{ - if(eventName == "data") - process.nextTick(this.resume.bind(this) ); - }); - this.on("removeListener", (eventName, listener) => { - if(eventName == "data" && this.listenerCount("data") == 0) - process.nextTick(this.pause.bind(this) ); - }) - } - - static async open(...args) { - loadBinding(); - const native = await binding.openAsyncHIDDevice(...args); - return new HIDAsync(native) - } - - async close() { - this._closing = true; - this.removeAllListeners(); - await this._raw.close(); - this._closed = true; - } - - //Pauses the reader, which stops "data" events from being emitted - pause() { - this._raw.readStop(); - } - - resume() { - if(this.listenerCount("data") > 0) - { - //Start polling & reading loop - this._raw.readStart((err, data) => { - if (err) { - if(!this._closing) - this.emit("error", err); - //else ignore any errors if I'm closing the device - } else { - this.emit("data", data); - } - }) - } - } -} - function showdevices() { - loadBinding(); - return binding.devices.apply(HID,arguments); -} - -function showdevicesAsync(...args) { - loadBinding(); - return binding.devicesAsync(...args); + loadBinding(); + return binding.devices.apply(HID, arguments); } // Static property for caching the instance @@ -213,7 +115,5 @@ HID.cachedInstance = null; //Expose API exports.HID = HID; -exports.HIDAsync = HIDAsync; exports.devices = showdevices; -exports.devicesAsync = showdevicesAsync; exports.setDriverType = setDriverType; From 99e7e3d3c4a8a4190f60332ac191949ccb2a5dd4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 28 Mar 2024 21:40:53 -0400 Subject: [PATCH 5/5] Use exact working version for this fork for package.json resolutions --- Publishing.md | 26 +- README.md | 285 +++----------- binding-options.js | 4 - binding.gyp | 18 +- nodehid.d.ts | 62 --- nodehid.js | 143 +++---- package.json | 38 +- src/.gitignore | 3 - src/HID.cc | 241 ++++++++++-- src/HID.h | 27 -- src/HIDAsync.cc | 769 -------------------------------------- src/HIDAsync.h | 29 -- src/buzzers.js | 7 +- src/devices.cc | 158 -------- src/devices.h | 7 - src/exports.cc | 43 --- src/powermate.js | 5 +- src/read.cc | 97 ----- src/read.h | 26 -- src/test-ps3-rumbleled.js | 7 +- src/test-ps3.js | 7 +- src/util.cc | 126 ------- src/util.h | 147 -------- 23 files changed, 383 insertions(+), 1892 deletions(-) delete mode 100644 binding-options.js delete mode 100644 nodehid.d.ts delete mode 100644 src/.gitignore delete mode 100644 src/HID.h delete mode 100644 src/HIDAsync.cc delete mode 100644 src/HIDAsync.h delete mode 100644 src/devices.cc delete mode 100644 src/devices.h delete mode 100644 src/exports.cc delete mode 100644 src/read.cc delete mode 100644 src/read.h delete mode 100644 src/util.cc delete mode 100644 src/util.h diff --git a/Publishing.md b/Publishing.md index 6036414..56eb495 100644 --- a/Publishing.md +++ b/Publishing.md @@ -4,23 +4,7 @@ How to publish node-hid 1. First, make sure version is bumped to new version. (if code change) -2. Push changes to Github, and wait for the prebuild workflow to complete - -3. Run: -``` -npm run clean # clean out directory -``` - -4. Download the `all-prebuilds` artifact, and extract as a `prebuilds` folder in the root of the repository - -5. Run: -``` -npm publish # update npmjs, be sure to have Authy app for OTP code -``` - ------ - -If desired, manual testing can be down on each of MacOS, Windows, Linux, do: +2. Then, on each of MacOS, Windows, Linux, do: ``` git clone https://github.com/node-hid/node-hid.git cd node-hid @@ -29,4 +13,12 @@ npm run prepublishOnly # get the hidapi submodule npm install --build-from-source # rebuilds the C code npm run showdevices # simple test node ./src/test-blink1.js # simple test +npm run prebuild # build all the versions +npm run prebuild-upload # upload all the versions using github token +``` + +3. And then on master dev box: +``` +npm run clean # clean out directory +npm publish # update npmjs, be sure to have Authy app for OTP code ``` diff --git a/README.md b/README.md index 5895244..88a313d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # node-hid - Access USB HID devices from Node.js # [![npm](https://img.shields.io/npm/dm/node-hid.svg?maxAge=2592000)](http://npmjs.com/package/node-hid) -[![Prebuild](https://github.com/node-hid/node-hid/actions/workflows/build.yml/badge.svg)](https://github.com/node-hid/node-hid/actions/workflows/build.yml) +[![build macos](https://github.com/node-hid/node-hid/workflows/macos/badge.svg)](https://github.com/node-hid/node-hid/actions?query=workflow%3Amacos) +[![build windows](https://github.com/node-hid/node-hid/workflows/windows/badge.svg)](https://github.com/node-hid/node-hid/actions?query=workflow%3Awindows) +[![build linux](https://github.com/node-hid/node-hid/workflows/linux/badge.svg)](https://github.com/node-hid/node-hid/actions?query=workflow%3Alinux) * [node-hid - Access USB HID devices from Node.js](#node-hid---access-usb-hid-devices-from-nodejs) @@ -12,10 +14,30 @@ * [Installation](#installation) * [Installation Special Cases](#installation-special-cases) * [Examples](#examples) - * [Async API Usage](#async-api-usage) - * [Sync API Usage](#sync-api-usage) - * [Complete Async API](#complete-async-api) - * [Complete Sync API](#complete-sync-api) + * [Usage](#usage) + * [List all HID devices connected](#list-all-hid-devices-connected) + * [Cost of HID.devices() and new HID.HID() for detecting device plug/unplug](#cost-of-hiddevices-and-new-hidhid-for-detecting-device-plugunplug) + * [Opening a device](#opening-a-device) + * [Picking a device from the device list](#picking-a-device-from-the-device-list) + * [Reading from a device](#reading-from-a-device) + * [Writing to a device](#writing-to-a-device) + * [Complete API](#complete-api) + * [devices = HID.devices()](#devices--hiddevices) + * [HID.setDriverType(type)](#hidsetdrivertypetype) + * [device = new HID.HID(path)](#device--new-hidhidpath) + * [device = new HID.HID(vid,pid)](#device--new-hidhidvidpid) + * [device.on('data', function(data) {} )](#deviceondata-functiondata--) + * [device.on('error, function(error) {} )](#deviceonerror-functionerror--) + * [device.write(data)](#devicewritedata) + * [device.close()](#deviceclose) + * [device.pause()](#devicepause) + * [device.resume()](#deviceresume) + * [device.read(callback)](#devicereadcallback) + * [device.readSync()](#devicereadsync) + * [device.readTimeout(time_out)](#devicereadtimeouttime_out) + * [device.sendFeatureReport(data)](#devicesendfeaturereportdata) + * [device.getFeatureReport(report_id, report_length)](#devicegetfeaturereportreport_id-report_length) + * [device.setNonBlocking(no_block)](#devicesetnonblockingno_block) * [General notes:](#general-notes) * [Thread safety, Worker threads, Context-aware modules](#thread-safety-worker-threads-context-aware-modules) * [Devices node-hid cannot read](#devices-node-hid-cannot-read) @@ -38,38 +60,37 @@ * [Support](#support) ## Platform Support -`node-hid` currently supports Node.js v10 and upwards. For versions before that, you will need to use an older version. -The platforms, architectures and node versions `node-hid` supports are the following. +`node-hid` supports Node.js v6 and upwards. For versions before that, +you will need to build from source. The platforms, architectures and node versions `node-hid` supports are the following. In general we try to provide pre-built native library binaries for the most common platforms, Node and Electron versions. We strive to make `node-hid` cross-platform so there's a good chance any combination not listed here will compile and work. ### Supported Platforms ### -- Windows x86 (32-bit) +- Windows x86 (32-bit) (¹) - Windows x64 (64-bit) - Mac OSX 10.9+ - Linux x64 (²) - Linux x86 (¹) -- Linux ARM / Raspberry Pi / Various SBC (²) -- Linux ARM64 / Various SBC (²) +- Linux ARM / Raspberry Pi (¹) - Linux MIPSel (¹) - Linux PPC64 (¹) ¹ prebuilt-binaries not provided for these platforms -² prebuilt binary built on Debian 10 Buster +² prebuilt binary built on Ubuntu 18.04 x64 ### Supported Node versions ### -* Node v10 to -* Node v20 +* Node v8 to +* Node v16 ### Supported Electron versions ### * Electron v3 to -* Electron v24 +* Electron v16 -Future versions of Node or Electron should work with no extra work, since `node-hid` is now based on NAPI. +Future versions of Node or Electron should work, since `node-hid` is now based on NAPI. ## Installation @@ -104,144 +125,8 @@ that talk to specific devices in some way. Some interesting ones: To try them out, run them with `node src/showdevices.js` from within the node-hid directory. ---- -## Async vs sync API -Since 3.0.0, `node-hid` supports both the old synchronous api, and a newer async api. -It is recommended to use the async api to avoid `node-hid` from blocking your code from executing. For prototyping or tiny applications, this likely will not matter, but for npm libraries or larger applications it can be problematic. - -Additionally, the sync api is limited to only beind able to read up to the `UV_THREADPOOL_SIZE` (default is 4) number of devices at once. Reading from multiple could degrade performance of your application, as there will be fewer than expected uv workers available for nodejs and other libraries to use for other tasks. - -The async API is identical to the sync API described below, except every method returns a `Promise` that must be handled. Any unhandled promise can crash your application. - -It is safe to use the sync api for some devices in an application, and the async api for other devices. The thread safety of `hidapi` is handled for you here to avoid crashes. - -## Cost of `HID.devices()`, `HID.devicesAsync()`, `new HID.HID()` and `HIDAsync.open()` for detecting device plug/unplug -All of `HID.devices()`, `HID.devicesAsync()`, `new HID.HID()` and `HIDAsync.open()` are relatively costly, each causing a USB (and potentially Bluetooth) enumeration. This takes time and OS resources. Doing either can slow down the read/write that you do in parallel with a device, and cause other USB devices to slow down too. This is how USB works. - -If you are polling `HID.devices()` or `HID.devicesAsync()` or other inefficient methods to detect device plug / unplug, consider instead using [node-usb](https://github.com/node-usb/node-usb#usbdetection). `node-usb` uses OS-specific, non-bus enumeration ways to detect device plug / unplug. - - -## Async API Usage - -### List all HID devices connected - -```js -var HID = require('node-hid'); -var devices = await HID.devicesAsync(); -``` - -`devices` will contain an array of objects, one for each HID device -available. Of particular interest are the `vendorId` and -`productId`, as they uniquely identify a device, and the -`path`, which is needed to open a particular device. - -Sample output: - -```js -await HID.devicesAsync(); -{ vendorId: 10168, - productId: 493, - path: 'IOService:/AppleACPIPl...HIDDevice@14210000,0', - serialNumber: '20002E8C', - manufacturer: 'ThingM', - product: 'blink(1) mk2', - release: 2, - interface: -1, - usagePage: 65280, - usage: 1 }, - { vendorId: 1452, - productId: 610, - path: 'IOService:/AppleACPIPl...Keyboard@14400000,0', - serialNumber: '', - manufacturer: 'Apple Inc.', - product: 'Apple Internal Keyboard / Trackpad', - release: 549, - interface: -1, - usagePage: 1, - usage: 6 }, - -``` - - -### Opening a device - -Before a device can be read from or written to, it must be opened. -Use either the `path` from the list returned by a prior call to `HID.devicesAsync()`: - -```js -var device = await HID.HIDAsync.open(path); -``` - -or open the first device matching a VID/PID pair: - -```js -var device = await HID.HIDAsync.open(vid,pid); -``` - -The `device` variable will contain a handle to the device. -If an error occurs opening the device, an exception will be thrown. - -A `node-hid` device is an `EventEmitter`. -While it shares some method names and usage patterns with -`Readable` and `Writable` streams, it is not a stream and the semantics vary. -For example, `device.write` does not take encoding or callback args and -`device.pause` does not do the same thing as `readable.pause`. -There is also no `pipe` method. - -### Reading from a device - -To receive FEATURE reports, use `await device.getFeatureReport()`. - -To receive INPUT reports, use `device.on("data",...)`. -A `node-hid` device is an EventEmitter. -Reading from a device is performed by registering a "data" event handler: - -```js -device.on("data", function(data) {}); -``` - -You can also listen for errors like this: - -```js -device.on("error", function(err) {}); -``` -For FEATURE reports: - -```js -var buf = await device.getFeatureReport(reportId, reportLength) -``` - - -Notes: -- Reads via `device.on("data")` are asynchronous -- To remove an event handler, close the device with `device.close()` -- When there is not yet a data handler or no data handler exists, - data is not read at all -- there is no buffer. - -### Writing to a device - -To send FEATURE reports, use `device.sendFeatureReport()`. - -To send OUTPUT reports, use `device.write()`. - -The ReportId is the first byte of the array sent to `device.sendFeatureReport()` or `device.write()`, meaning the array should be one byte bigger than your report. -If your device does NOT use numbered reports, set the first byte of the 0x00. - - -```js -device.write([0x00, 0x01, 0x01, 0x05, 0xff, 0xff]); -``` -```js -device.sendFeatureReport( [0x01, 'c', 0, 0xff,0x33,0x00, 70,0, 0] ); -``` -Notes: -- All writes and other operations performed with the HIDAsync device are done in a work-queue, so will happen in the order you issue them with the returned `Promise` resolving once the operation is completed -- You must send the exact number of bytes for your chosen OUTPUT or FEATURE report. -- Both `device.write()` and `device.sendFeatureReport()` return a Promise containing the number of bytes written + 1. -- For devices using Report Ids, the first byte of the array to `write()` or `sendFeatureReport()` must be the Report Id. - - -## Sync API Usage +## Usage ### List all HID devices connected @@ -282,10 +167,15 @@ HID.devices(); ``` +#### Cost of `HID.devices()` and `new HID.HID()` for detecting device plug/unplug +Both `HID.devices()` and `new HID.HID()` are relatively costly, each causing a USB (and potentially Bluetooth) enumeration. This takes time and OS resources. Doing either can slow down the read/write that you do in parallel with a device, and cause other USB devices to slow down too. This is how USB works. + +If you are polling `HID.devices()` or doing repeated `new HID.HID(vid,pid)` to detect device plug / unplug, consider instead using [node-usb-detection](https://github.com/MadLittleMods/node-usb-detection). `node-usb-detection` uses OS-specific, non-bus enumeration ways to detect device plug / unplug. ### Opening a device Before a device can be read from or written to, it must be opened. +The `path` can be determined by a prior HID.devices() call. Use either the `path` from the list returned by a prior call to `HID.devices()`: ```js @@ -385,80 +275,7 @@ number of bytes written + 1. `sendFeatureReport()` must be the Report Id. -## Complete Async API - -### `devices = await HID.devicesAsync()` - -- Return array listing all connected HID devices - -### `devices = await HID.devicesAsync(vid,pid)` - -- Return array listing all connected HID devices with specific VendorId and ProductId - -### `device = await HID.HIDAsync.open(path)` - -- Open a HID device at the specified platform-specific path - -### `device = await HID.HIDAsync.open(vid,pid)` - -- Open first HID device with specific VendorId and ProductId - -### `device.on('data', function(data) {} )` - -- `data` - Buffer - the data read from the device - -### `device.on('error, function(error) {} )` - -- `error` - The error Object emitted - -### `device.write(data)` - -- `data` - the data to be synchronously written to the device, -first byte is Report Id or 0x00 if not using numbered reports. -- Returns number of bytes actually written - -### `device.close()` - -- Closes the device. Subsequent reads will raise an error. - -### `device.pause()` - -- Pauses reading and the emission of `data` events. -This means the underlying device is _silenced_ until resumption -- -it is not like pausing a stream, where data continues to accumulate. - -### `device.resume()` - -- This method will cause the HID device to resume emmitting `data` events. -If no listeners are registered for the `data` event, data will be lost. - -- When a `data` event is registered for this HID device, this method will -be automatically called. - -### `device.read(time_out)` - -- (optional) `time_out` - timeout in milliseconds -- Low-level function call to initiate an asynchronous read from the device. -- Returns a Promise containing a Buffer or the Promise will reject upon error. -- This can only be used when `device.on('data', () => {})` is not being used. It will fail if a data handler is registered - -### `device.sendFeatureReport(data)` - -- `data` - data of HID feature report, with 0th byte being report_id (`[report_id,...]`) -- Returns number of bytes actually written - -### `device.getFeatureReport(report_id, report_length)` - -- `report_id` - HID feature report id to get -- `report_length` - length of report - -### `device.setNonBlocking(no_block)` - -- `no_block` - boolean. Set to `true` to enable non-blocking reads -- exactly mirrors `hid_set_nonblocking()` in [`hidapi`](https://github.com/libusb/hidapi) - - -## Complete Sync API +## Complete API ### `devices = HID.devices()` @@ -548,7 +365,8 @@ be automatically called. ### Thread safety, Worker threads, Context-aware modules In general `node-hid` is not thread-safe because the underlying C-library it wraps (`hidapi`) is not thread-safe. -To mitigate this we are doing locking to ensure operations are performed safely. If you are using the sync api from multiple worker_threads, this will result in them waiting on each other at times. +However, `node-hid` is now reporting as minimally Context Aware to allow use in Electron v9+. +Until `node-hid` (or `hidapi`) is rewritten to be thread-safe, please constrain all accesses to it via a single thread. ### Devices `node-hid` cannot read The following devices are unavailable to `node-hid` because the OS owns them: @@ -712,6 +530,17 @@ or ## Electron projects using `node-hid` +In your electron project, add `electron-rebuild` to your `devDependencies`. +Then in your package.json `scripts` add: + +``` + "postinstall": "electron-rebuild" +``` +This will cause `npm` to rebuild `node-hid` for the version of Node that is in Electron. +If you get an error similar to `The module "HID.node" was compiled against a different version of Node.js` +then `electron-rebuild` hasn't been run and Electron is trying to use `node-hid` +compiled for Node.js and not for Electron. + If using `node-hid` with `webpack` or similar bundler, you may need to exclude `node-hid` and other libraries with native code. In webpack, you say which @@ -747,4 +576,4 @@ nwjs . ## Support Please use the [node-hid github issues page](https://github.com/node-hid/node-hid/issues) -for support questions and issues. \ No newline at end of file +for support questions and issues. diff --git a/binding-options.js b/binding-options.js deleted file mode 100644 index c86282d..0000000 --- a/binding-options.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - name: 'HID', - napi_versions: [3], -} \ No newline at end of file diff --git a/binding.gyp b/binding.gyp index 1e1d2bd..cf3e891 100644 --- a/binding.gyp +++ b/binding.gyp @@ -6,14 +6,7 @@ 'targets': [ { 'target_name': 'HID', - 'sources': [ - 'src/exports.cc', - 'src/HID.cc', - 'src/HIDAsync.cc', - 'src/devices.cc', - 'src/read.cc', - 'src/util.cc' - ], + 'sources': [ 'src/HID.cc' ], 'dependencies': ['hidapi'], 'defines': [ '_LARGEFILE_SOURCE', @@ -132,14 +125,7 @@ 'targets': [ { 'target_name': 'HID_hidraw', - 'sources': [ - 'src/exports.cc', - 'src/HID.cc', - 'src/HIDAsync.cc', - 'src/devices.cc', - 'src/read.cc', - 'src/util.cc' - ], + 'sources': [ 'src/HID.cc' ], 'dependencies': ['hidapi-linux-hidraw'], 'defines': [ '_LARGEFILE_SOURCE', diff --git a/nodehid.d.ts b/nodehid.d.ts deleted file mode 100644 index ca88994..0000000 --- a/nodehid.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Type definitions for node-hid 1.3 -// Project: https://github.com/node-hid/node-hid#readme -// Definitions by: Mohamed Hegazy -// Robert Kiss -// Rob Moran -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -import { EventEmitter } from 'events' - -export interface Device { - vendorId: number - productId: number - path?: string | undefined - serialNumber?: string | undefined - manufacturer?: string | undefined - product?: string | undefined - release: number - interface: number - usagePage?: number | undefined - usage?: number | undefined -} - -export class HID extends EventEmitter { - constructor(path: string) - constructor(vid: number, pid: number) - close(): void - pause(): void - read(callback: (err: any, data: number[]) => void): void - readSync(): number[] - readTimeout(time_out: number): number[] - sendFeatureReport(data: number[] | Buffer): number - getFeatureReport(report_id: number, report_length: number): number[] - resume(): void - write(values: number[] | Buffer): number - setNonBlocking(no_block: boolean): void - getDeviceInfo(): Device -} -export function devices(vid: number, pid: number): Device[] -export function devices(): Device[] - -export function devicesAsync(vid: number, pid: number): Promise -export function devicesAsync(): Promise - -export class HIDAsync extends EventEmitter { - private constructor() - - static open(path: string): Promise - static open(vid: number, pid: number): Promise - - close(): Promise - pause(): void - read(time_out?: number | undefined): Promise - sendFeatureReport(data: number[] | Buffer): Promise - getFeatureReport(report_id: number, report_length: number): Promise - resume(): void - write(values: number[] | Buffer): Promise - setNonBlocking(no_block: boolean): Promise - generateDeviceInfo(): Promise -} - -export function setDriverType(type: 'hidraw' | 'libusb'): void - diff --git a/nodehid.js b/nodehid.js index 8c9cc37..feb7a36 100644 --- a/nodehid.js +++ b/nodehid.js @@ -1,62 +1,66 @@ -var os = require('os'); -var EventEmitter = require('events').EventEmitter, - util = require('util'); +var os = require('os') + +var EventEmitter = require("events").EventEmitter, + util = require("util"); var driverType = null; function setDriverType(type) { - driverType = type; + driverType = type; } // lazy load the C++ binding var binding = null; function loadBinding() { - if (!binding) { - if (os.platform() === 'linux') { - // Linux defaults to hidraw - if (!driverType || driverType === 'hidraw') { - binding = require('bindings')('HID_hidraw.node'); - } else { - binding = require('bindings')('HID.node'); - } - } else { - binding = require('bindings')('HID.node'); + if( !binding ) { + if( os.platform() === 'linux' ) { + // Linux defaults to hidraw + if( !driverType || driverType === 'hidraw' ) { + binding = require('bindings')('HID_hidraw.node'); + } else { + binding = require('bindings')('HID.node'); + } + } + else { + binding = require('bindings')('HID.node'); + } } - } } //This class is a wrapper for `binding.HID` class function HID() { - if (!new.target) { - throw new Error("HID() must be called with 'new' operator"); - } + if (!new.target) { + throw new Error('HID() must be called with \'new\' operator'); + } - EventEmitter.call(this); + EventEmitter.call(this); - // Check if an instance already exists in the cache - if (HID.cachedInstance) { - return HID.cachedInstance; - } + // Check if an instance already exists in the cache + if (HID.cachedInstance) { + return HID.cachedInstance; + } - loadBinding(); + loadBinding(); - var thisPlusArgs = new Array(arguments.length + 1); - thisPlusArgs[0] = null; - for (var i = 0; i < arguments.length; i++) thisPlusArgs[i + 1] = arguments[i]; + var thisPlusArgs = new Array(arguments.length + 1); + thisPlusArgs[0] = null; + for(var i = 0; i < arguments.length; i++) + thisPlusArgs[i + 1] = arguments[i]; - this._raw = new (Function.prototype.bind.apply(binding.HID, thisPlusArgs))(); + this._raw = new (Function.prototype.bind.apply(binding.HID, thisPlusArgs))(); - // Cache this instance for future calls - HID.cachedInstance = this; + // Cache this instance for future calls + HID.cachedInstance = this; - for (var key in binding.HID.prototype) - this[key] = binding.HID.prototype[key].bind(this._raw); + for(var key in binding.HID.prototype) + this[key] = binding.HID.prototype[key].bind(this._raw); - this._paused = true; - var self = this; - self.on('newListener', function (eventName, listener) { - if (eventName == 'data') process.nextTick(self.resume.bind(self)); - }); + this._paused = true; + var self = this; + self.on("newListener", function(eventName, listener) { + if(eventName == "data") + process.nextTick(self.resume.bind(self)); + }); } //Inherit prototype methods @@ -64,18 +68,18 @@ util.inherits(HID, EventEmitter); //Don't inherit from `binding.HID`; that's done above already! HID.prototype.close = function close() { - this._closing = true; - this.removeAllListeners(); - this._raw.close(); - this._closed = true; + this._closing = true; + this.removeAllListeners(); + this._raw.close(); + this._closed = true; }; //Pauses the reader, which stops "data" events from being emitted HID.prototype.pause = function pause() { - this._paused = true; + this._paused = true; }; HID.prototype.read = function read(callback) { - if (this._closed) { + if (this._closed) { throw new Error('Unable to read from a closed HID device'); } else { return this._raw.read(callback); @@ -83,31 +87,38 @@ HID.prototype.read = function read(callback) { }; HID.prototype.resume = function resume() { - var self = this; - if (self._paused && self.listeners('data').length > 0) { - //Start polling & reading loop - self._paused = false; - self.read(function readFunc(err, data) { - if (err) { - //Emit error and pause reading - self._paused = true; - if (!self._closing) self.emit('error', err); - //else ignore any errors if I'm closing the device - } else { - //If there are no "data" listeners, we pause - if (self.listeners('data').length <= 0) self._paused = true; - //Keep reading if we aren't paused - if (!self._paused) self.read(readFunc); - //Now emit the event - self.emit('data', data); - } - }); - } + var self = this; + if(self._paused && self.listeners("data").length > 0) + { + //Start polling & reading loop + self._paused = false; + self.read(function readFunc(err, data) { + if(err) + { + //Emit error and pause reading + self._paused = true; + if(!self._closing) + self.emit("error", err); + //else ignore any errors if I'm closing the device + } + else + { + //If there are no "data" listeners, we pause + if(self.listeners("data").length <= 0) + self._paused = true; + //Keep reading if we aren't paused + if(!self._paused) + self.read(readFunc); + //Now emit the event + self.emit("data", data); + } + }); + } }; function showdevices() { - loadBinding(); - return binding.devices.apply(HID, arguments); + loadBinding(); + return binding.devices.apply(HID,arguments); } // Static property for caching the instance @@ -116,4 +127,4 @@ HID.cachedInstance = null; //Expose API exports.HID = HID; exports.devices = showdevices; -exports.setDriverType = setDriverType; +exports.setDriverType = setDriverType; \ No newline at end of file diff --git a/package.json b/package.json index a4f3e2c..b84900b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-hid", "description": "USB HID device access library", - "version": "3.0.0", + "version": "2.2.0", "author": "Hans Hübner (https://github.com/hanshuebner)", "bugs": "https://github.com/node-hid/node-hid/issues", "homepage": "https://github.com/node-hid/node-hid#readme", @@ -18,9 +18,14 @@ "test": "node src/test-ci.js", "showdevices": "node src/show-devices.js", "prepublishOnly": "git submodule update --init", - "install": "pkg-prebuilds-verify ./binding-options.js || node-gyp rebuild", - "build": "node-gyp build", - "rebuild": "node-gyp clean configure build", + "install": "prebuild-install --runtime napi || node-gyp rebuild", + "prebuild": "prebuild --runtime napi --all --verbose --include-regex \"HID.*node$\"", + "prebuild-upload": "prebuild --runtime napi --upload-all", + "prebuild-ci": "prebuild-ci", + "gypclean": "node-gyp clean", + "gypconfigure": "node-gyp configure", + "gypbuild": "node-gyp build", + "gyprebuild": "node-gyp rebuild", "clean": "rimraf build node_modules prebuilds package-lock.json", "distclean": "npm run clean && rimraf hidapi" }, @@ -28,34 +33,23 @@ "hid-showdevices": "./src/show-devices.js" }, "main": "./nodehid.js", - "types": "./nodehid.d.ts", "binary": { "napi_versions": [ - 4 + 3 ] }, "engines": { - "node": ">=10.16" + "node": ">=10" }, "license": "(MIT OR X11)", "dependencies": { - "node-addon-api": "^3.2.1", - "pkg-prebuilds": "^0.2.1" + "bindings": "^1.5.0", + "node-addon-api": "^3.0.2", + "prebuild-install": "^7.1.1" }, "devDependencies": { + "prebuild": "^12.1.0", "rimraf": "^2.6.2" }, - "gypfile": true, - "files": [ - "nodehid.js", - "nodehid.d.ts", - "binding-options.js", - "hidapi", - "prebuilds", - "src/*.cc", - "src/*.h", - "LICENSE*", - "README.md", - "binding.gyp" - ] + "gypfile": true } diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 9f0188f..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*~ -build -.lock-wscript diff --git a/src/HID.cc b/src/HID.cc index ddf5da6..b425fb4 100644 --- a/src/HID.cc +++ b/src/HID.cc @@ -20,12 +20,45 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. +#include +#include #include #include -#include "devices.h" -#include "util.h" -#include "HID.h" +#include + +#define NAPI_VERSION 3 +#include + +#include + +#define READ_BUFF_MAXSIZE 2048 + +class HID : public Napi::ObjectWrap +{ +public: + static void Initialize(Napi::Env &env, Napi::Object &exports); + + void closeHandle(); + + HID(const Napi::CallbackInfo &info); + ~HID() { closeHandle(); } + + hid_device *_hidHandle; + +private: + static Napi::Value devices(const Napi::CallbackInfo &info); + + Napi::Value close(const Napi::CallbackInfo &info); + Napi::Value read(const Napi::CallbackInfo &info); + Napi::Value write(const Napi::CallbackInfo &info); + Napi::Value setNonBlocking(const Napi::CallbackInfo &info); + Napi::Value getFeatureReport(const Napi::CallbackInfo &info); + Napi::Value sendFeatureReport(const Napi::CallbackInfo &info); + Napi::Value readSync(const Napi::CallbackInfo &info); + Napi::Value readTimeout(const Napi::CallbackInfo &info); + Napi::Value getDeviceInfo(const Napi::CallbackInfo &info); +}; HID::HID(const Napi::CallbackInfo &info) : Napi::ObjectWrap(info) @@ -38,13 +71,6 @@ HID::HID(const Napi::CallbackInfo &info) return; } - auto appCtx = ApplicationContext::get(); - if (!appCtx) - { - Napi::TypeError::New(env, "hidapi not initialized").ThrowAsJavaScriptException(); - return; - } - if (info.Length() < 1) { Napi::TypeError::New(env, "HID constructor requires at least one argument").ThrowAsJavaScriptException(); @@ -61,11 +87,7 @@ HID::HID(const Napi::CallbackInfo &info) } std::string path = info[0].As().Utf8Value(); - { - std::unique_lock lock(appCtx->enumerateLock); - _hidHandle = hid_open_path(path.c_str()); - } - + _hidHandle = hid_open_path(path.c_str()); if (!_hidHandle) { std::ostringstream os; @@ -78,20 +100,16 @@ HID::HID(const Napi::CallbackInfo &info) { int32_t vendorId = info[0].As().Int32Value(); int32_t productId = info[1].As().Int32Value(); - std::wstring wserialstr; - const wchar_t *wserialptr = nullptr; + wchar_t wserialstr[100]; // FIXME: is there a better way? + wchar_t *wserialptr = NULL; if (info.Length() > 2) { std::string serialstr = info[2].As().Utf8Value(); - wserialstr = utf8_decode(serialstr); - wserialptr = wserialstr.c_str(); - } - - { - std::unique_lock lock(appCtx->enumerateLock); - _hidHandle = hid_open(vendorId, productId, wserialptr); + mbstowcs(wserialstr, serialstr.c_str(), 100); + wserialptr = wserialstr; } + _hidHandle = hid_open(vendorId, productId, wserialptr); if (!_hidHandle) { std::ostringstream os; @@ -267,10 +285,33 @@ Napi::Value HID::sendFeatureReport(const Napi::CallbackInfo &info) } std::vector message; - std::string copyError = copyArrayOrBufferIntoVector(info[0], message); - if (copyError != "") + if (info[0].IsBuffer()) + { + Napi::Buffer buffer = info[0].As>(); + uint32_t len = buffer.Length(); + unsigned char *data = buffer.Data(); + message.assign(data, data + len); + } + else if (info[0].IsArray()) { - Napi::TypeError::New(env, copyError).ThrowAsJavaScriptException(); + Napi::Array messageArray = info[0].As(); + message.reserve(messageArray.Length()); + + for (unsigned i = 0; i < messageArray.Length(); i++) + { + Napi::Value v = messageArray.Get(i); + if (!v.IsNumber()) + { + Napi::TypeError::New(env, "unexpected array element in array to send, expecting only integers").ThrowAsJavaScriptException(); + return env.Null(); + } + uint32_t b = v.As().Uint32Value(); + message.push_back((unsigned char)b); + } + } + else + { + Napi::TypeError::New(env, "unexpected data to send, expecting an array or buffer").ThrowAsJavaScriptException(); return env.Null(); } @@ -324,10 +365,33 @@ Napi::Value HID::write(const Napi::CallbackInfo &info) } std::vector message; - std::string copyError = copyArrayOrBufferIntoVector(info[0], message); - if (copyError != "") + if (info[0].IsBuffer()) + { + Napi::Buffer buffer = info[0].As>(); + uint32_t len = buffer.Length(); + unsigned char *data = buffer.Data(); + message.assign(data, data + len); + } + else if (info[0].IsArray()) { - Napi::TypeError::New(env, copyError).ThrowAsJavaScriptException(); + Napi::Array messageArray = info[0].As(); + message.reserve(messageArray.Length()); + + for (unsigned i = 0; i < messageArray.Length(); i++) + { + Napi::Value v = messageArray.Get(i); + if (!v.IsNumber()) + { + Napi::TypeError::New(env, "unexpected array element in array to send, expecting only integers").ThrowAsJavaScriptException(); + return env.Null(); + } + uint32_t b = v.As().Uint32Value(); + message.push_back((unsigned char)b); + } + } + else + { + Napi::TypeError::New(env, "unexpected data to send, expecting an array or buffer").ThrowAsJavaScriptException(); return env.Null(); } @@ -347,22 +411,117 @@ Napi::Value HID::write(const Napi::CallbackInfo &info) return Napi::Number::New(env, returnedLength); } +static std::string narrow(wchar_t *wide) +{ + std::wstring ws(wide); + std::ostringstream os; + for (size_t i = 0; i < ws.size(); i++) + { + os << os.narrow(ws[i], '?'); + } + return os.str(); +} + Napi::Value HID::getDeviceInfo(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); - hid_device_info *dev = hid_get_device_info(_hidHandle); - if (!dev) + const int maxlen = 256; + wchar_t wstr[maxlen]; // FIXME: use new & delete + + Napi::Object deviceInfo = Napi::Object::New(env); + + hid_get_manufacturer_string(_hidHandle, wstr, maxlen); + deviceInfo.Set("manufacturer", Napi::String::New(env, narrow(wstr))); + + hid_get_product_string(_hidHandle, wstr, maxlen); + deviceInfo.Set("product", Napi::String::New(env, narrow(wstr))); + + hid_get_serial_number_string(_hidHandle, wstr, maxlen); + deviceInfo.Set("serialNumber", Napi::String::New(env, narrow(wstr))); + + return deviceInfo; +} + +Napi::Value HID::devices(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + + int vendorId = 0; + int productId = 0; + + switch (info.Length()) { - Napi::TypeError::New(env, "Unable to get device info").ThrowAsJavaScriptException(); + case 0: + break; + case 2: + vendorId = info[0].As().Int32Value(); + productId = info[1].As().Int32Value(); + break; + default: + Napi::TypeError::New(env, "unexpected number of arguments to HID.devices() call, expecting either no arguments or vendor and product ID").ThrowAsJavaScriptException(); return env.Null(); } - return generateDeviceInfo(env, dev); + hid_device_info *devs = hid_enumerate(vendorId, productId); + Napi::Array retval = Napi::Array::New(env); + int count = 0; + for (hid_device_info *dev = devs; dev; dev = dev->next) + { + Napi::Object deviceInfo = Napi::Object::New(env); + deviceInfo.Set("vendorId", Napi::Number::New(env, dev->vendor_id)); + deviceInfo.Set("productId", Napi::Number::New(env, dev->product_id)); + if (dev->path) + { + deviceInfo.Set("path", Napi::String::New(env, dev->path)); + } + if (dev->serial_number) + { + deviceInfo.Set("serialNumber", Napi::String::New(env, narrow(dev->serial_number))); + } + if (dev->manufacturer_string) + { + deviceInfo.Set("manufacturer", Napi::String::New(env, narrow(dev->manufacturer_string))); + } + if (dev->product_string) + { + deviceInfo.Set("product", Napi::String::New(env, narrow(dev->product_string))); + } + deviceInfo.Set("release", Napi::Number::New(env, dev->release_number)); + deviceInfo.Set("interface", Napi::Number::New(env, dev->interface_number)); + if (dev->usage_page) + { + deviceInfo.Set("usagePage", Napi::Number::New(env, dev->usage_page)); + } + if (dev->usage) + { + deviceInfo.Set("usage", Napi::Number::New(env, dev->usage)); + } + retval.Set(count++, deviceInfo); + } + hid_free_enumeration(devs); + return retval; } -Napi::Value HID::Initialize(Napi::Env &env) +static void +deinitialize(void *) +{ + if (hid_exit()) + { + // Process is exiting, no need to log? TODO + // Napi::TypeError::New(env, "cannot uninitialize hidapi (hid_exit failed)").ThrowAsJavaScriptException(); + return; + } +} +void HID::Initialize(Napi::Env &env, Napi::Object &exports) { + if (hid_init()) + { + Napi::TypeError::New(env, "cannot initialize hidapi (hid_init failed)").ThrowAsJavaScriptException(); + return; + } + + napi_add_env_cleanup_hook(env, deinitialize, nullptr); Napi::Function ctor = DefineClass(env, "HID", { InstanceMethod("close", &HID::close), @@ -376,5 +535,15 @@ Napi::Value HID::Initialize(Napi::Env &env) InstanceMethod("getDeviceInfo", &HID::getDeviceInfo, napi_enumerable), }); - return ctor; + exports.Set("HID", ctor); + exports.Set("devices", Napi::Function::New(env, &HID::devices)); } + +Napi::Object Init(Napi::Env env, Napi::Object exports) +{ + HID::Initialize(env, exports); + + return exports; +} + +NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/src/HID.h b/src/HID.h deleted file mode 100644 index 59148ac..0000000 --- a/src/HID.h +++ /dev/null @@ -1,27 +0,0 @@ -#include "util.h" - -class HID : public Napi::ObjectWrap -{ -public: - static Napi::Value Initialize(Napi::Env &env); - - void closeHandle(); - - HID(const Napi::CallbackInfo &info); - ~HID() { closeHandle(); } - - hid_device *_hidHandle; - -private: - static Napi::Value devices(const Napi::CallbackInfo &info); - - Napi::Value close(const Napi::CallbackInfo &info); - Napi::Value read(const Napi::CallbackInfo &info); - Napi::Value write(const Napi::CallbackInfo &info); - Napi::Value setNonBlocking(const Napi::CallbackInfo &info); - Napi::Value getFeatureReport(const Napi::CallbackInfo &info); - Napi::Value sendFeatureReport(const Napi::CallbackInfo &info); - Napi::Value readSync(const Napi::CallbackInfo &info); - Napi::Value readTimeout(const Napi::CallbackInfo &info); - Napi::Value getDeviceInfo(const Napi::CallbackInfo &info); -}; diff --git a/src/HIDAsync.cc b/src/HIDAsync.cc deleted file mode 100644 index 71fa09b..0000000 --- a/src/HIDAsync.cc +++ /dev/null @@ -1,769 +0,0 @@ -// -*- C++ -*- - -// Copyright Hans Huebner and contributors. All rights reserved. -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, copy, -// modify, merge, publish, distribute, sublicense, and/or sell copies -// of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -// IN THE SOFTWARE. - -#include -#include -#include - -#include "devices.h" -#include "util.h" -#include "HIDAsync.h" -#include "read.h" - -HIDAsync::HIDAsync(const Napi::CallbackInfo &info) - : Napi::ObjectWrap(info) -{ - Napi::Env env = info.Env(); - - if (info.Length() != 1 || !info[0].IsExternal()) - { - Napi::TypeError::New(env, "HIDAsync constructor is not supported").ThrowAsJavaScriptException(); - return; - } - - auto appCtx = ApplicationContext::get(); - if (!appCtx) - { - Napi::TypeError::New(env, "hidapi not initialized").ThrowAsJavaScriptException(); - return; - } - - auto ptr = info[0].As>().Data(); - _hidHandle = std::make_shared(appCtx, ptr); - helper = std::make_unique(_hidHandle); -} - -class CloseWorker : public PromiseAsyncWorker> -{ -public: - CloseWorker( - Napi::Env &env, std::shared_ptr hid, std::unique_ptr helper) - : PromiseAsyncWorker(env, hid), - helper(std::move(helper)) {} - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - if (helper) - { - helper->stop_and_join(); - helper = nullptr; - } - - if (context->hid) - { - hid_close(context->hid); - context->hid = nullptr; - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - return env.Undefined(); - } - -private: - std::unique_ptr helper; -}; - -void HIDAsync::closeHandle() -{ - if (helper) - { - helper->stop_and_join(); - helper = nullptr; - } - - // hid_close is called by the destructor - _hidHandle = nullptr; -} - -class OpenByPathWorker : public PromiseAsyncWorker -{ -public: - OpenByPathWorker(const Napi::Env &env, ContextState *context, std::string path) - : PromiseAsyncWorker(env, context), - path(path) {} - - ~OpenByPathWorker() - { - if (dev) - { - // dev wasn't claimed - hid_close(dev); - dev = nullptr; - } - } - - // This code will be executed on the worker thread - void Execute() override - { - std::unique_lock lock(context->appCtx->enumerateLock); - dev = hid_open_path(path.c_str()); - if (!dev) - { - std::ostringstream os; - os << "cannot open device with path " << path; - SetError(os.str()); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - auto ptr = Napi::External::New(env, dev); - dev = nullptr; // devs has already been freed - return context->asyncCtor.New({ptr}); - } - -private: - std::string path; - hid_device *dev; -}; - -class OpenByUsbIdsWorker : public PromiseAsyncWorker -{ -public: - OpenByUsbIdsWorker(const Napi::Env &env, ContextState *context, int vendorId, int productId, std::string serial) - : PromiseAsyncWorker(env, context), - vendorId(vendorId), - productId(productId), - serial(serial) {} - - ~OpenByUsbIdsWorker() - { - if (dev) - { - // dev wasn't claimed - hid_close(dev); - dev = nullptr; - } - } - - // This code will be executed on the worker thread - void Execute() override - { - std::unique_lock lock(context->appCtx->enumerateLock); - - std::wstring wserialstr; - const wchar_t *wserialptr = nullptr; - if (serial != "") - { - wserialstr = utf8_decode(serial); - wserialptr = wserialstr.c_str(); - } - - dev = hid_open(vendorId, productId, wserialptr); - if (!dev) - { - std::ostringstream os; - os << "cannot open device with vendor id 0x" << std::hex << vendorId << " and product id 0x" << productId; - SetError(os.str()); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - auto ptr = Napi::External::New(env, dev); - dev = nullptr; // devs has already been freed - return context->asyncCtor.New({ptr}); - } - -private: - int vendorId; - int productId; - std::string serial; - hid_device *dev; -}; - -Napi::Value HIDAsync::Create(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (info.Length() < 1) - { - Napi::TypeError::New(env, "HIDAsync::Create requires at least one arguments").ThrowAsJavaScriptException(); - return env.Null(); - } - - void *data = info.Data(); - if (!data) - { - Napi::TypeError::New(env, "HIDAsync::Create missing constructor data").ThrowAsJavaScriptException(); - return env.Null(); - } - ContextState *context = (ContextState *)data; - - if (info.Length() == 1) - { - // open by path - if (!info[0].IsString()) - { - Napi::TypeError::New(env, "Device path must be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string path = info[0].As().Utf8Value(); - - return (new OpenByPathWorker(env, context, path))->QueueAndRun(); - } - else - { - if (!info[0].IsNumber() || !info[1].IsNumber()) - { - Napi::TypeError::New(env, "VendorId and ProductId must be integers").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::string serial; - if (info.Length() > 2) - { - if (!info[2].IsString()) - { - Napi::TypeError::New(env, "Serial must be a string").ThrowAsJavaScriptException(); - return env.Null(); - } - - serial = info[2].As().Utf8Value(); - } - - int32_t vendorId = info[0].As().Int32Value(); - int32_t productId = info[1].As().Int32Value(); - - return (new OpenByUsbIdsWorker(env, context, vendorId, productId, serial))->QueueAndRun(); - } -} - -Napi::Value HIDAsync::readStart(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!helper || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - auto callback = info[0].As(); - helper->start(env, callback); - - return env.Null(); -} - -class ReadStopWorker : public PromiseAsyncWorker> -{ -public: - ReadStopWorker( - Napi::Env &env, - std::shared_ptr hid, - std::unique_ptr helper) - : PromiseAsyncWorker(env, hid), - helper(std::move(helper)) - { - } - ~ReadStopWorker() - { - } - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - helper->stop_and_join(); - helper = nullptr; - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - return env.Undefined(); - } - -private: - std::unique_ptr helper; -}; - -Napi::Value HIDAsync::readStop(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!helper) - { - // Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - auto result = (new ReadStopWorker(env, std::move(_hidHandle), std::move(helper)))->QueueAndRun(); - - // Ownership is transferred to CloseWorker - helper = nullptr; - - return result; -}; - -class ReadOnceWorker : public PromiseAsyncWorker> -{ -public: - ReadOnceWorker( - Napi::Env &env, - std::shared_ptr hid, - int timeout) - : PromiseAsyncWorker(env, hid), - _timeout(timeout) - { - } - ~ReadOnceWorker() - { - if (buffer) - { - delete[] buffer; - } - } - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - if (context->hid) - { - buffer = new unsigned char[READ_BUFF_MAXSIZE]; - // This is wordy, but necessary to get the correct non-blocking behaviour - if (_timeout == -1) - { - returnedLength = hid_read(context->hid, buffer, READ_BUFF_MAXSIZE); - } - else - { - returnedLength = hid_read_timeout(context->hid, buffer, READ_BUFF_MAXSIZE, _timeout); - } - - if (returnedLength < 0) - { - SetError("could not read data from device"); - } - } - else - { - SetError("device has been closed"); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - auto result = Napi::Buffer::Copy(env, buffer, returnedLength); - - return result; - } - -private: - int returnedLength = 0; - unsigned char *buffer; - int _timeout; -}; - -Napi::Value HIDAsync::read(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!_hidHandle || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (helper != nullptr && helper->run_read) - { - Napi::TypeError::New(env, "Cannot use read while async read is running").ThrowAsJavaScriptException(); - return env.Null(); - } - - int timeout = -1; - if (info.Length() != 0) - { - if (info[0].IsNumber()) - { - timeout = info[0].As().Uint32Value(); - } - else - { - Napi::TypeError::New(env, "time out parameter must be a number").ThrowAsJavaScriptException(); - return env.Null(); - } - } - - return (new ReadOnceWorker(env, _hidHandle, timeout))->QueueAndRun(); -} - -class GetFeatureReportWorker : public PromiseAsyncWorker> -{ -public: - GetFeatureReportWorker( - Napi::Env &env, - std::shared_ptr hid, - uint8_t reportId, - int bufSize) - : PromiseAsyncWorker(env, hid), - bufferLength(bufSize) - { - buffer = new unsigned char[bufSize]; - buffer[0] = reportId; - } - ~GetFeatureReportWorker() - { - if (buffer) - { - delete[] buffer; - } - } - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - if (context->hid) - { - bufferLength = hid_get_feature_report(context->hid, buffer, bufferLength); - if (bufferLength < 0) - { - SetError("could not get feature report from device"); - } - } - else - { - SetError("device has been closed"); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - auto result = Napi::Buffer::Copy(env, buffer, bufferLength); - - return result; - } - -private: - unsigned char *buffer; - int bufferLength; -}; - -Napi::Value HIDAsync::getFeatureReport(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!_hidHandle || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (info.Length() != 2 || !info[0].IsNumber() || !info[1].IsNumber()) - { - Napi::TypeError::New(env, "need report ID and length parameters in getFeatureReport").ThrowAsJavaScriptException(); - return env.Null(); - } - - const uint8_t reportId = info[0].As().Uint32Value(); - const int bufSize = info[1].As().Uint32Value(); - if (bufSize <= 0) - { - Napi::TypeError::New(env, "Length parameter cannot be zero in getFeatureReport").ThrowAsJavaScriptException(); - return env.Null(); - } - - return (new GetFeatureReportWorker(env, _hidHandle, reportId, bufSize))->QueueAndRun(); -} - -class SendFeatureReportWorker : public PromiseAsyncWorker> -{ -public: - SendFeatureReportWorker( - Napi::Env &env, - std::shared_ptr hid, - std::vector srcBuffer) - : PromiseAsyncWorker(env, hid), - srcBuffer(srcBuffer) {} - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - if (context->hid) - { - written = hid_send_feature_report(context->hid, srcBuffer.data(), srcBuffer.size()); - if (written < 0) - { - SetError("could not send feature report to device"); - } - } - else - { - SetError("device has been closed"); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - return Napi::Number::New(env, written); - } - -private: - int written = 0; - std::vector srcBuffer; -}; - -Napi::Value HIDAsync::sendFeatureReport(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!_hidHandle || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (info.Length() != 1) - { - Napi::TypeError::New(env, "need report (including id in first byte) only in sendFeatureReportAsync").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::vector message; - std::string copyError = copyArrayOrBufferIntoVector(info[0], message); - if (copyError != "") - { - Napi::TypeError::New(env, copyError).ThrowAsJavaScriptException(); - return env.Null(); - } - - return (new SendFeatureReportWorker(env, _hidHandle, message))->QueueAndRun(); -} - -Napi::Value HIDAsync::close(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!_hidHandle || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device is already closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - // TODO - option to flush or purge queued operations - - // Mark it as closed, to stop new jobs being pushed to the queue - _hidHandle->is_closed = true; - - auto result = (new CloseWorker(env, std::move(_hidHandle), std::move(helper)))->QueueAndRun(); - - // Ownership is transferred to CloseWorker - _hidHandle = nullptr; - helper = nullptr; - - return result; -} - -class SetNonBlockingWorker : public PromiseAsyncWorker> -{ -public: - SetNonBlockingWorker( - Napi::Env &env, - std::shared_ptr hid, - int mode) - : PromiseAsyncWorker(env, hid), - mode(mode) {} - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - if (context->hid) - { - int res = hid_set_nonblocking(context->hid, mode); - if (res < 0) - { - SetError("Error setting non-blocking mode."); - } - } - else - { - SetError("device has been closed"); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - return env.Undefined(); - } - -private: - int mode; -}; - -Napi::Value HIDAsync::setNonBlocking(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!_hidHandle || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (info.Length() != 1) - { - Napi::TypeError::New(env, "Expecting a 1 to enable, 0 to disable as the first argument.").ThrowAsJavaScriptException(); - return env.Null(); - } - - int blockStatus = info[0].As().Int32Value(); - - return (new SetNonBlockingWorker(env, _hidHandle, blockStatus))->QueueAndRun(); -} - -class WriteWorker : public PromiseAsyncWorker> -{ -public: - WriteWorker( - Napi::Env &env, - std::shared_ptr hid, - std::vector srcBuffer) - : PromiseAsyncWorker(env, hid), - srcBuffer(srcBuffer) {} - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - if (context->hid) - { - written = hid_write(context->hid, srcBuffer.data(), srcBuffer.size()); - if (written < 0) - { - SetError("Cannot write to hid device"); - } - } - else - { - SetError("device has been closed"); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - return Napi::Number::New(env, written); - } - -private: - int written = 0; - std::vector srcBuffer; -}; - -Napi::Value HIDAsync::write(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!_hidHandle || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - if (info.Length() != 1) - { - Napi::TypeError::New(env, "HID write requires one argument").ThrowAsJavaScriptException(); - return env.Null(); - } - - std::vector message; - std::string copyError = copyArrayOrBufferIntoVector(info[0], message); - if (copyError != "") - { - Napi::TypeError::New(env, copyError).ThrowAsJavaScriptException(); - return env.Null(); - } - - return (new WriteWorker(env, _hidHandle, std::move(message)))->QueueAndRun(); -} - -class GetDeviceInfoWorker : public PromiseAsyncWorker> -{ -public: - GetDeviceInfoWorker( - Napi::Env &env, - std::shared_ptr hid) - : PromiseAsyncWorker(env, hid) {} - - // This code will be executed on the worker thread. Note: Napi types cannot be used - void Execute() override - { - if (context->hid) - { - dev = hid_get_device_info(context->hid); - if (!dev) - { - SetError("Unable to get device info"); - } - } - else - { - SetError("device has been closed"); - } - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - // if the hid device has somehow been deleted, the hid_device_info is no longer valid - if (context->hid) - { - return generateDeviceInfo(env, dev); - } - else - { - return env.Null(); - } - } - -private: - // this is owned by context->hid - hid_device_info *dev; -}; -Napi::Value HIDAsync::getDeviceInfo(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - if (!_hidHandle || _hidHandle->is_closed) - { - Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException(); - return env.Null(); - } - - return (new GetDeviceInfoWorker(env, _hidHandle))->QueueAndRun(); -} - -Napi::Function HIDAsync::Initialize(Napi::Env &env) -{ - Napi::Function ctor = DefineClass(env, "HIDAsync", { - InstanceMethod("close", &HIDAsync::close), - InstanceMethod("readStart", &HIDAsync::readStart), - InstanceMethod("readStop", &HIDAsync::readStop), - InstanceMethod("write", &HIDAsync::write, napi_enumerable), - InstanceMethod("getFeatureReport", &HIDAsync::getFeatureReport, napi_enumerable), - InstanceMethod("sendFeatureReport", &HIDAsync::sendFeatureReport, napi_enumerable), - InstanceMethod("setNonBlocking", &HIDAsync::setNonBlocking, napi_enumerable), - InstanceMethod("read", &HIDAsync::read, napi_enumerable), - InstanceMethod("getDeviceInfo", &HIDAsync::getDeviceInfo, napi_enumerable), - }); - - return ctor; -} diff --git a/src/HIDAsync.h b/src/HIDAsync.h deleted file mode 100644 index 0d8836e..0000000 --- a/src/HIDAsync.h +++ /dev/null @@ -1,29 +0,0 @@ -#include "util.h" -#include "read.h" - -class HIDAsync : public Napi::ObjectWrap -{ -public: - static Napi::Function Initialize(Napi::Env &env); - - static Napi::Value Create(const Napi::CallbackInfo &info); - - HIDAsync(const Napi::CallbackInfo &info); - ~HIDAsync() { closeHandle(); } - -private: - std::shared_ptr _hidHandle; - std::unique_ptr helper; - - void closeHandle(); - - Napi::Value close(const Napi::CallbackInfo &info); - Napi::Value readStart(const Napi::CallbackInfo &info); - Napi::Value readStop(const Napi::CallbackInfo &info); - Napi::Value write(const Napi::CallbackInfo &info); - Napi::Value setNonBlocking(const Napi::CallbackInfo &info); - Napi::Value getFeatureReport(const Napi::CallbackInfo &info); - Napi::Value sendFeatureReport(const Napi::CallbackInfo &info); - Napi::Value read(const Napi::CallbackInfo &info); - Napi::Value getDeviceInfo(const Napi::CallbackInfo &info); -}; \ No newline at end of file diff --git a/src/buzzers.js b/src/buzzers.js index 67517c8..ab05aad 100644 --- a/src/buzzers.js +++ b/src/buzzers.js @@ -30,7 +30,7 @@ function BuzzerController(index) // Initialize buzzers this.hid.write([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - this.hid.on('data', this.buzzerData.bind(this)); + this.hid.read(this.buzzerData.bind(this)); } util.inherits(BuzzerController, events.EventEmitter); @@ -48,13 +48,14 @@ BuzzerController.prototype.handleBuzzer = function (buzzerNumber, bits) } } -BuzzerController.prototype.buzzerData = function ( data) { - console.log('data', data); +BuzzerController.prototype.buzzerData = function (error, data) { + console.log('error', error, 'data', data); var bits = (data[4] << 16) | (data[3] << 8) | data[2]; for (var i = 0; i < 4; i++) { this.handleBuzzer(i, bits); } this.oldBits = bits; + this.hid.read(this.buzzerData.bind(this)); } BuzzerController.prototype.led = function(buzzer, state) { diff --git a/src/devices.cc b/src/devices.cc deleted file mode 100644 index ac873cf..0000000 --- a/src/devices.cc +++ /dev/null @@ -1,158 +0,0 @@ -#include "devices.h" - -bool parseDevicesParameters(const Napi::CallbackInfo &info, int *vendorId, int *productId) -{ - switch (info.Length()) - { - case 0: - return true; - case 2: - *vendorId = info[0].As().Int32Value(); - *productId = info[1].As().Int32Value(); - return true; - default: - return false; - } -} - -Napi::Value generateDevicesResultAndFree(const Napi::Env &env, hid_device_info *devs) -{ - Napi::Array retval = Napi::Array::New(env); - int count = 0; - for (hid_device_info *dev = devs; dev; dev = dev->next) - { - retval.Set(count++, generateDeviceInfo(env, dev)); - } - hid_free_enumeration(devs); - return retval; -} - -Napi::Value generateDeviceInfo(const Napi::Env &env, hid_device_info *dev) -{ - Napi::Object deviceInfo = Napi::Object::New(env); - deviceInfo.Set("vendorId", Napi::Number::New(env, dev->vendor_id)); - deviceInfo.Set("productId", Napi::Number::New(env, dev->product_id)); - if (dev->path) - { - deviceInfo.Set("path", Napi::String::New(env, dev->path)); - } - if (dev->serial_number) - { - deviceInfo.Set("serialNumber", Napi::String::New(env, utf8_encode(dev->serial_number))); - } - if (dev->manufacturer_string) - { - deviceInfo.Set("manufacturer", Napi::String::New(env, utf8_encode(dev->manufacturer_string))); - } - if (dev->product_string) - { - deviceInfo.Set("product", Napi::String::New(env, utf8_encode(dev->product_string))); - } - deviceInfo.Set("release", Napi::Number::New(env, dev->release_number)); - deviceInfo.Set("interface", Napi::Number::New(env, dev->interface_number)); - if (dev->usage_page) - { - deviceInfo.Set("usagePage", Napi::Number::New(env, dev->usage_page)); - } - if (dev->usage) - { - deviceInfo.Set("usage", Napi::Number::New(env, dev->usage)); - } - return deviceInfo; -} - -Napi::Value devices(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - int vendorId = 0; - int productId = 0; - if (!parseDevicesParameters(info, &vendorId, &productId)) - { - Napi::TypeError::New(env, "unexpected number of arguments to HID.devices() call, expecting either no arguments or vendor and product ID").ThrowAsJavaScriptException(); - return env.Null(); - } - - auto appCtx = ApplicationContext::get(); - if (!appCtx) - { - Napi::TypeError::New(env, "hidapi not initialized").ThrowAsJavaScriptException(); - return env.Null(); - } - - hid_device_info *devs; - { - std::unique_lock lock(appCtx->enumerateLock); - devs = hid_enumerate(vendorId, productId); - } - return generateDevicesResultAndFree(env, devs); -} - -class DevicesWorker : public PromiseAsyncWorker -{ -public: - DevicesWorker(const Napi::Env &env, ContextState *context, int vendorId, int productId) - : PromiseAsyncWorker(env, context), - vendorId(vendorId), - productId(productId) {} - - ~DevicesWorker() - { - if (devs) - { - // ensure devs is freed if not done by OnOK - hid_free_enumeration(devs); - devs = nullptr; - } - } - - // This code will be executed on the worker thread - void Execute() override - { - std::unique_lock lock(context->appCtx->enumerateLock); - devs = hid_enumerate(vendorId, productId); - } - - Napi::Value GetPromiseResult(const Napi::Env &env) override - { - if (devs) - { - auto result = generateDevicesResultAndFree(env, devs); - devs = nullptr; // devs has already been freed - return result; - } - else - { - return Napi::Array::New(env, 0); - } - } - -private: - int vendorId; - int productId; - hid_device_info *devs; -}; - -Napi::Value devicesAsync(const Napi::CallbackInfo &info) -{ - Napi::Env env = info.Env(); - - void *data = info.Data(); - if (!data) - { - Napi::TypeError::New(env, "devicesAsync missing context").ThrowAsJavaScriptException(); - return env.Null(); - } - ContextState *context = (ContextState *)data; - - int vendorId = 0; - int productId = 0; - if (!parseDevicesParameters(info, &vendorId, &productId)) - { - - Napi::TypeError::New(env, "unexpected number of arguments to HID.devicesAsync() call, expecting either no arguments or vendor and product ID").ThrowAsJavaScriptException(); - return env.Null(); - } - - return (new DevicesWorker(env, context, vendorId, productId))->QueueAndRun(); -} \ No newline at end of file diff --git a/src/devices.h b/src/devices.h deleted file mode 100644 index 1df6ce9..0000000 --- a/src/devices.h +++ /dev/null @@ -1,7 +0,0 @@ -#include "util.h" - -Napi::Value generateDeviceInfo(const Napi::Env &env, hid_device_info *dev); - -Napi::Value devices(const Napi::CallbackInfo &info); - -Napi::Value devicesAsync(const Napi::CallbackInfo &info); diff --git a/src/exports.cc b/src/exports.cc deleted file mode 100644 index 9d99e31..0000000 --- a/src/exports.cc +++ /dev/null @@ -1,43 +0,0 @@ -#include - -#include "util.h" - -#include "HID.h" -#include "HIDAsync.h" -#include "devices.h" - -static void -deinitialize(void *ptr) -{ - auto ptr2 = static_cast(ptr); - delete ptr2; -} - -Napi::Object -Init(Napi::Env env, Napi::Object exports) -{ - std::shared_ptr appCtx = ApplicationContext::get(); - if (appCtx == nullptr) - { - Napi::TypeError::New(env, "cannot initialize hidapi (hid_init failed)").ThrowAsJavaScriptException(); - return exports; - } - - auto ctor = HIDAsync::Initialize(env); - - // Future: Once targetting node-api v6, this ContextState flow can be replaced with instanceData - auto context = new ContextState(appCtx, Napi::Persistent(ctor)); - napi_add_env_cleanup_hook(env, deinitialize, context); - - exports.Set("HID", HID::Initialize(env)); - exports.Set("HIDAsync", ctor); - - exports.Set("openAsyncHIDDevice", Napi::Function::New(env, &HIDAsync::Create, nullptr, context)); // TODO: verify context will be alive long enough - - exports.Set("devices", Napi::Function::New(env, &devices)); - exports.Set("devicesAsync", Napi::Function::New(env, &devicesAsync, nullptr, context)); // TODO: verify context will be alive long enough - - return exports; -} - -NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/src/powermate.js b/src/powermate.js index d9db005..6443468 100644 --- a/src/powermate.js +++ b/src/powermate.js @@ -35,7 +35,7 @@ function PowerMate(index) this.hid = new HID.HID(powerMates[index].path); this.position = 0; this.button = 0; - this.hid.on('data', this.interpretData.bind(this)) + this.hid.read(this.interpretData.bind(this)); } util.inherits(PowerMate, events.EventEmitter); @@ -44,7 +44,7 @@ PowerMate.prototype.setLed = function(brightness) { this.hid.write([0, brightness]); } -PowerMate.prototype.interpretData = function(data) { +PowerMate.prototype.interpretData = function(error, data) { var button = data[0]; if (button ^ this.button) { this.emit(button ? 'buttonDown' : 'buttonUp'); @@ -58,6 +58,7 @@ PowerMate.prototype.interpretData = function(data) { this.position += delta; this.emit('turn', delta, this.position); } + this.hid.read(this.interpretData.bind(this)); } exports.PowerMate = PowerMate; diff --git a/src/read.cc b/src/read.cc deleted file mode 100644 index 265e0eb..0000000 --- a/src/read.cc +++ /dev/null @@ -1,97 +0,0 @@ -#include "read.h" - -struct ReadCallbackProps -{ - unsigned char *buf; - int len; -}; - -static void ReadCallback(Napi::Env env, Napi::Function jsCallback, ReadCallbackProps *data) -{ - auto buffer = Napi::Buffer::Copy(env, data->buf, data->len); - delete data->buf; - delete data; - - jsCallback.Call({env.Null(), buffer}); -}; -static void ReadErrorCallback(Napi::Env env, Napi::Function jsCallback, void *data) -{ - auto error = Napi::String::New(env, "could not read from HID device"); - - jsCallback.Call({error, env.Null()}); -}; - -ReadHelper::ReadHelper(std::shared_ptr hidHandle) -{ - _hidHandle = hidHandle; -} -ReadHelper::~ReadHelper() -{ - stop_and_join(); -} - -void ReadHelper::stop_and_join() -{ - run_read = false; - - if (read_thread.joinable()) - { - read_thread.join(); - } -} - -void ReadHelper::start(Napi::Env env, Napi::Function callback) -{ - // If the read is already running, then abort - if (run_read) - return; - run_read = true; - - read_thread = std::thread([&]() - { - int mswait = 50; - int len = 0; - unsigned char *buf = new unsigned char[READ_BUFF_MAXSIZE]; - - run_read = true; - while (run_read) - { - len = hid_read_timeout(_hidHandle->hid, buf, READ_BUFF_MAXSIZE, mswait); - if (len < 0) - { - // Emit and error and stop reading - read_callback.BlockingCall((void *)nullptr, ReadErrorCallback); - break; - } - else if (len > 0) - { - auto data = new ReadCallbackProps; - data->buf = buf; - data->len = len; - - read_callback.BlockingCall(data, ReadCallback); - // buf is now owned by ReadCallback - buf = new unsigned char[READ_BUFF_MAXSIZE]; - } - } - - run_read = false; - delete[] buf; - - // Cleanup the function - read_callback.Release(); }); - - read_callback = Napi::ThreadSafeFunction::New( - env, - callback, // JavaScript function called asynchronously - "HID:read", // Name - 0, // Unlimited queue - 1, // Only one thread will use this initially - [&](Napi::Env) { // Finalizer used to clean threads up - // Wait for end of the thread, if it wasnt the one to close up - if (read_thread.joinable()) - { - read_thread.join(); - } - }); -} diff --git a/src/read.h b/src/read.h deleted file mode 100644 index 9fbc9cc..0000000 --- a/src/read.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef NODEHID_READ_H__ -#define NODEHID_READ_H__ - -#include "util.h" - -#include -#include - -class ReadHelper -{ -public: - ReadHelper(std::shared_ptr hidHandle); - ~ReadHelper(); - - void start(Napi::Env env, Napi::Function callback); - void stop_and_join(); - - std::atomic run_read = {false}; - -private: - std::shared_ptr _hidHandle; - std::thread read_thread; - Napi::ThreadSafeFunction read_callback; -}; - -#endif // NODEHID_READ_H__ \ No newline at end of file diff --git a/src/test-ps3-rumbleled.js b/src/test-ps3-rumbleled.js index d9632c9..6043048 100644 --- a/src/test-ps3-rumbleled.js +++ b/src/test-ps3-rumbleled.js @@ -36,11 +36,14 @@ function setRumbleLed(hidDevice, rumbleL, rumbleR, led_cmd ) ]); } -hid.on('data', (data) => { +hid.gotData = function (err, data) { console.log('got ps3 data', data); // map left & right d-pad to rumble, and right action buttons to LEDs setRumbleLed( hid, data[15], data[17], data[3]>>3 ); -}) + this.read(this.gotData.bind(this)); +}; + +hid.read(hid.gotData.bind(hid)); /* * data is 48-byte Buffer with byte values: diff --git a/src/test-ps3.js b/src/test-ps3.js index 200d2b9..c75bc89 100644 --- a/src/test-ps3.js +++ b/src/test-ps3.js @@ -6,8 +6,11 @@ var hid = new HID.HID(1356, 616); console.log('features', hid.getFeatureReport(0xf2, 17)); -hid.on('data', (data) => { +hid.gotData = function (err, data) { console.log('got ps3 data', data); -}) + this.read(this.gotData.bind(this)); +} + +hid.read(hid.gotData.bind(hid)); repl.context.hid = hid; diff --git a/src/util.cc b/src/util.cc deleted file mode 100644 index 1fee8dd..0000000 --- a/src/util.cc +++ /dev/null @@ -1,126 +0,0 @@ -#include -#include -#include - -#include "util.h" - -// Ensure hid_init/hid_exit is coordinated across all threads. Global data is bad for context-aware modules, but this is designed to be safe -std::mutex lockApplicationContext; -std::weak_ptr weakApplicationContext; // This will let it be garbage collected when it goes out of scope in the last thread - -ApplicationContext::~ApplicationContext() -{ - // Make sure we dont try to aquire it or run init at the same time - std::unique_lock lock(lockApplicationContext); - - if (hid_exit()) - { - // thread is exiting, can't log? - } -} - -std::shared_ptr ApplicationContext::get() -{ - // Make sure that we don't try to lock the pointer while it is being freed - // and that two threads don't try to create it concurrently - std::unique_lock lock(lockApplicationContext); - - auto ref = weakApplicationContext.lock(); - if (!ref) - { - // Not initialised, so lets do that - if (hid_init()) - { - return nullptr; - } - - ref = std::make_shared(); - weakApplicationContext = ref; - } - return ref; -} - -std::string utf8_encode(const std::wstring &source) -{ - return std::wstring_convert>().to_bytes(source); -} - -std::wstring utf8_decode(const std::string &source) -{ - return std::wstring_convert>().from_bytes(source); -} - -std::string copyArrayOrBufferIntoVector(const Napi::Value &val, std::vector &message) -{ - if (val.IsBuffer()) - { - Napi::Buffer buffer = val.As>(); - uint32_t len = buffer.Length(); - unsigned char *data = buffer.Data(); - message.assign(data, data + len); - - return ""; - } - else if (val.IsArray()) - { - Napi::Array messageArray = val.As(); - message.reserve(messageArray.Length()); - - for (unsigned i = 0; i < messageArray.Length(); i++) - { - Napi::Value v = messageArray.Get(i); - if (!v.IsNumber()) - { - return "unexpected array element in array to send, expecting only integers"; - } - uint32_t b = v.As().Uint32Value(); - message.push_back((unsigned char)b); - } - - return ""; - } - else - { - return "unexpected data to send, expecting an array or buffer"; - } -} - -DeviceContext::~DeviceContext() -{ - if (hid) - { - // We shouldn't ever get here, but lets make sure it was freed - hid_close(hid); - hid = nullptr; - } -} - -void AsyncWorkerQueue::QueueJob(const Napi::Env &, Napi::AsyncWorker *job) -{ - std::unique_lock lock(jobQueueMutex); - if (!isRunning) - { - isRunning = true; - job->Queue(); - } - else - { - jobQueue.push(job); - } -} - -void AsyncWorkerQueue::JobFinished(const Napi::Env &) -{ - std::unique_lock lock(jobQueueMutex); - - if (jobQueue.size() == 0) - { - isRunning = false; - } - else - { - auto newJob = jobQueue.front(); - jobQueue.pop(); - newJob->Queue(); - } -} diff --git a/src/util.h b/src/util.h deleted file mode 100644 index 8c7368a..0000000 --- a/src/util.h +++ /dev/null @@ -1,147 +0,0 @@ -#ifndef NODEHID_UTIL_H__ -#define NODEHID_UTIL_H__ - -#define NAPI_VERSION 4 -#include - -#include - -#include - -#define READ_BUFF_MAXSIZE 2048 - -std::string utf8_encode(const std::wstring &source); -std::wstring utf8_decode(const std::string &source); - -/** - * Convert a js value (either a buffer ot array of numbers) into a vector of bytes. - * Returns a non-empty string upon failure - */ -std::string copyArrayOrBufferIntoVector(const Napi::Value &val, std::vector &message); - -/** - * Application-wide shared state. - * This is referenced by the main thread and every worker_thread where node-hid has been loaded and not yet unloaded. - */ -class ApplicationContext -{ -public: - ~ApplicationContext(); - - static std::shared_ptr get(); - - // A lock for any enumerate/open operations, as they are not thread safe - // In async land, these are also done in a single-threaded queue, this lock is used to link up with the sync side - std::mutex enumerateLock; -}; - -class AsyncWorkerQueue -{ - // TODO - discard the jobQueue in a safe manner - // there should be a destructor which ensures that the queue is empty - // when we 'unref' it from the parent, we should mark it as dead, and tell any remaining workers to abort - -public: - /** - * Push a job onto the queue. - * Note: This must only be run from the main thread - */ - void QueueJob(const Napi::Env &, Napi::AsyncWorker *job); - - /** - * The job has finished, start the next in the queue. - * Note: This must only be run from the main thread - */ - void JobFinished(const Napi::Env &); - -private: - bool isRunning = false; - std::queue jobQueue; - std::mutex jobQueueMutex; -}; - -/** - * Context-wide shared state. - * One of these will be created for each Napi::Env (main thread and each worker_thread) - */ -class ContextState : public AsyncWorkerQueue -{ -public: - ContextState(std::shared_ptr appCtx, Napi::FunctionReference asyncCtor) : AsyncWorkerQueue(), appCtx(appCtx), asyncCtor(std::move(asyncCtor)) {} - - // Keep the ApplicationContext alive for longer than this state - std::shared_ptr appCtx; - - // Constructor for the HIDAsync class - Napi::FunctionReference asyncCtor; -}; - -class DeviceContext : public AsyncWorkerQueue -{ -public: - DeviceContext(std::shared_ptr appCtx, hid_device *hidHandle) : AsyncWorkerQueue(), hid(hidHandle), appCtx(appCtx) - { - } - - ~DeviceContext(); - - hid_device *hid; - - bool is_closed = false; - -private: - // Hold a reference to the ApplicationContext, - std::shared_ptr appCtx; -}; - -template -class PromiseAsyncWorker : public Napi::AsyncWorker -{ -public: - PromiseAsyncWorker( - const Napi::Env &env, T context) - : Napi::AsyncWorker(env), - context(context), - deferred(Napi::Promise::Deferred::New(env)) - { - } - - // This code will be executed on the worker thread. Note: Napi types cannot be used - virtual void Execute() override = 0; - - virtual Napi::Value GetPromiseResult(const Napi::Env &env) = 0; - - void OnOK() override - { - Napi::Env env = Env(); - - // Collect the result before finishing the job, in case the result relies on the hid object - Napi::Value result = GetPromiseResult(env); - - context->JobFinished(env); - - deferred.Resolve(result); - } - void OnError(Napi::Error const &error) override - { - context->JobFinished(Env()); - deferred.Reject(error.Value()); - } - - Napi::Promise QueueAndRun() - { - auto promise = deferred.Promise(); - - context->QueueJob(Env(), this); - - return promise; - } - -protected: - T context; - -private: - Napi::Promise::Deferred deferred; -}; - -#endif // NODEHID_UTIL_H__ \ No newline at end of file