Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style
This commit is contained in:
476
node_modules/node-hid/src/HID.cc
generated
vendored
Normal file
476
node_modules/node-hid/src/HID.cc
generated
vendored
Normal file
@@ -0,0 +1,476 @@
|
||||
// -*- 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 <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include "devices.h"
|
||||
#include "util.h"
|
||||
#include "HID.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "../hidapi/mac/hidapi_darwin.h"
|
||||
#endif
|
||||
|
||||
HID::HID(const Napi::CallbackInfo &info)
|
||||
: Napi::ObjectWrap<HID>(info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (!info.IsConstructCall())
|
||||
{
|
||||
Napi::TypeError::New(env, "HID function can only be used as a constructor").ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
|
||||
auto appCtx = ApplicationContext::get();
|
||||
if (!appCtx)
|
||||
{
|
||||
Napi::TypeError::New(env, "hidapi not initialized").ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
|
||||
auto argsLength = info.Length();
|
||||
|
||||
if (argsLength < 1)
|
||||
{
|
||||
Napi::TypeError::New(env, "HID constructor requires at least one argument").ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
|
||||
bool isNonExclusiveBool = false;
|
||||
if (argsLength > 1)
|
||||
{
|
||||
// We check if we have an optional param
|
||||
if (info[argsLength - 1].IsObject())
|
||||
{
|
||||
argsLength -= 1;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
Napi::Object options = info[argsLength].As<Napi::Object>();
|
||||
Napi::Value isNonExclusiveMode = options.Get("nonExclusive");
|
||||
if (!isNonExclusiveMode.IsBoolean())
|
||||
{
|
||||
Napi::TypeError::New(env, "nonExclusive flag should be a boolean").ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
isNonExclusiveBool = isNonExclusiveMode.As<Napi::Boolean>().Value();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
hid_darwin_set_open_exclusive(isNonExclusiveBool ? 0 : 1);
|
||||
#else
|
||||
// silence unused variable warning
|
||||
(void)isNonExclusiveBool;
|
||||
#endif
|
||||
|
||||
if (argsLength == 1)
|
||||
{
|
||||
// open by path
|
||||
if (!info[0].IsString())
|
||||
{
|
||||
Napi::TypeError::New(env, "Device path must be a string").ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string path = info[0].As<Napi::String>().Utf8Value();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(appCtx->enumerateLock);
|
||||
_hidHandle = hid_open_path(path.c_str());
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << "cannot open device with path " << path;
|
||||
Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int32_t vendorId = info[0].As<Napi::Number>().Int32Value();
|
||||
int32_t productId = info[1].As<Napi::Number>().Int32Value();
|
||||
std::wstring wserialstr;
|
||||
const wchar_t *wserialptr = nullptr;
|
||||
if (argsLength > 2)
|
||||
{
|
||||
std::string serialstr = info[2].As<Napi::String>().Utf8Value();
|
||||
wserialstr = utf8_decode(serialstr);
|
||||
wserialptr = wserialstr.c_str();
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(appCtx->enumerateLock);
|
||||
_hidHandle = hid_open(vendorId, productId, wserialptr);
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << "cannot open device with vendor id 0x" << std::hex << vendorId << " and product id 0x" << productId;
|
||||
Napi::TypeError::New(env, os.str()).ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HID::closeHandle()
|
||||
{
|
||||
if (_hidHandle)
|
||||
{
|
||||
hid_close(_hidHandle);
|
||||
_hidHandle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class ReadWorker : public Napi::AsyncWorker
|
||||
{
|
||||
public:
|
||||
ReadWorker(HID *hid, Napi::Function &callback)
|
||||
: Napi::AsyncWorker(hid->Value(), callback), _hid(hid) {}
|
||||
|
||||
~ReadWorker()
|
||||
{
|
||||
if (buf != nullptr)
|
||||
{
|
||||
delete[] buf;
|
||||
}
|
||||
}
|
||||
// This code will be executed on the worker thread
|
||||
void Execute() override
|
||||
{
|
||||
if (_hid->_readRunning.exchange(true))
|
||||
{
|
||||
SetError("read is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
int mswait = 50;
|
||||
while (len == 0 && !_hid->_readInterrupt && _hid->_hidHandle != nullptr)
|
||||
{
|
||||
len = hid_read_timeout(_hid->_hidHandle, buf, READ_BUFF_MAXSIZE, mswait);
|
||||
}
|
||||
if (len <= 0)
|
||||
{
|
||||
SetError("could not read from HID device");
|
||||
}
|
||||
|
||||
_hid->_readRunning = false;
|
||||
}
|
||||
|
||||
void OnOK() override
|
||||
{
|
||||
auto buffer = Napi::Buffer<unsigned char>::Copy(Env(), buf, len);
|
||||
Callback().Call({Env().Null(), buffer});
|
||||
}
|
||||
|
||||
private:
|
||||
HID *_hid;
|
||||
unsigned char *buf = new unsigned char[READ_BUFF_MAXSIZE];
|
||||
int len = 0;
|
||||
};
|
||||
|
||||
Napi::Value HID::read(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (info.Length() != 1 || !info[0].IsFunction())
|
||||
{
|
||||
Napi::TypeError::New(env, "need one callback function argument in read").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
this->_readInterrupt = false;
|
||||
|
||||
auto callback = info[0].As<Napi::Function>();
|
||||
auto job = new ReadWorker(this, callback);
|
||||
job->Queue();
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value HID::readInterrupt(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
this->_readInterrupt = true;
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value HID::readSync(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (info.Length() != 0)
|
||||
{
|
||||
Napi::TypeError::New(env, "readSync needs zero length parameter").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot access closed device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
unsigned char buff_read[READ_BUFF_MAXSIZE];
|
||||
int returnedLength = hid_read(_hidHandle, buff_read, sizeof buff_read);
|
||||
if (returnedLength == -1)
|
||||
{
|
||||
Napi::TypeError::New(env, "could not read data from device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Array retval = Napi::Array::New(env, returnedLength);
|
||||
for (int i = 0; i < returnedLength; i++)
|
||||
{
|
||||
retval.Set(i, Napi::Number::New(env, buff_read[i]));
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
Napi::Value HID::readTimeout(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (info.Length() != 1 || !info[0].IsNumber())
|
||||
{
|
||||
Napi::TypeError::New(env, "readTimeout needs time out parameter").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot access closed device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
const int timeout = info[0].As<Napi::Number>().Uint32Value();
|
||||
unsigned char buff_read[READ_BUFF_MAXSIZE];
|
||||
int returnedLength = hid_read_timeout(_hidHandle, buff_read, sizeof buff_read, timeout);
|
||||
if (returnedLength == -1)
|
||||
{
|
||||
Napi::TypeError::New(env, "could not read data from device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Array retval = Napi::Array::New(env, returnedLength);
|
||||
for (int i = 0; i < returnedLength; i++)
|
||||
{
|
||||
retval.Set(i, Napi::Number::New(env, buff_read[i]));
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
Napi::Value HID::getFeatureReport(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot access closed device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
const uint8_t reportId = info[0].As<Napi::Number>().Uint32Value();
|
||||
const int bufSize = info[1].As<Napi::Number>().Uint32Value();
|
||||
if (bufSize == 0)
|
||||
{
|
||||
Napi::TypeError::New(env, "Length parameter cannot be zero in getFeatureReport").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
std::vector<unsigned char> buf(bufSize);
|
||||
buf[0] = reportId;
|
||||
|
||||
int returnedLength = hid_get_feature_report(_hidHandle, buf.data(), bufSize);
|
||||
if (returnedLength == -1)
|
||||
{
|
||||
Napi::TypeError::New(env, "could not get feature report from device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Array retval = Napi::Array::New(env, returnedLength);
|
||||
for (int i = 0; i < returnedLength; i++)
|
||||
{
|
||||
retval.Set(i, Napi::Number::New(env, buf[i]));
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
Napi::Value HID::sendFeatureReport(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (info.Length() != 1)
|
||||
{
|
||||
Napi::TypeError::New(env, "need report (including id in first byte) only in sendFeatureReport").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot access closed device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
std::vector<unsigned char> message;
|
||||
std::string copyError = copyArrayOrBufferIntoVector(info[0], message);
|
||||
if (copyError != "")
|
||||
{
|
||||
Napi::TypeError::New(env, copyError).ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
int returnedLength = hid_send_feature_report(_hidHandle, message.data(), message.size());
|
||||
if (returnedLength == -1)
|
||||
{ // Not sure if there would ever be a valid return value of 0.
|
||||
Napi::TypeError::New(env, "could not send feature report to device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
return Napi::Number::New(env, returnedLength);
|
||||
}
|
||||
|
||||
Napi::Value HID::close(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (this->_readRunning)
|
||||
{
|
||||
Napi::TypeError::New(env, "read is still running").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
this->closeHandle();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value HID::setNonBlocking(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (info.Length() != 1)
|
||||
{
|
||||
Napi::TypeError::New(env, "Expecting a 1 to enable, 0 to disable as the first argument.").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot access closed device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
int blockStatus = info[0].As<Napi::Number>().Int32Value();
|
||||
int res = hid_set_nonblocking(_hidHandle, blockStatus);
|
||||
if (res < 0)
|
||||
{
|
||||
Napi::TypeError::New(env, "Error setting non-blocking mode.").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
Napi::Value HID::write(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (info.Length() != 1)
|
||||
{
|
||||
Napi::TypeError::New(env, "HID write requires one argument").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
std::vector<unsigned char> message;
|
||||
std::string copyError = copyArrayOrBufferIntoVector(info[0], message);
|
||||
if (copyError != "")
|
||||
{
|
||||
Napi::TypeError::New(env, copyError).ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot write to closed device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
int returnedLength = hid_write(_hidHandle, message.data(), message.size());
|
||||
if (returnedLength < 0)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot write to hid device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
return Napi::Number::New(env, returnedLength);
|
||||
}
|
||||
|
||||
Napi::Value HID::getDeviceInfo(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (!_hidHandle)
|
||||
{
|
||||
Napi::TypeError::New(env, "Cannot access closed device").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
hid_device_info *dev = hid_get_device_info(_hidHandle);
|
||||
if (!dev)
|
||||
{
|
||||
Napi::TypeError::New(env, "Unable to get device info").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
return generateDeviceInfo(env, dev);
|
||||
}
|
||||
|
||||
Napi::Value HID::Initialize(Napi::Env &env)
|
||||
{
|
||||
|
||||
Napi::Function ctor = DefineClass(env, "HID", {
|
||||
InstanceMethod("close", &HID::close),
|
||||
InstanceMethod("read", &HID::read),
|
||||
InstanceMethod("readInterrupt", &HID::readInterrupt),
|
||||
InstanceMethod("write", &HID::write, napi_enumerable),
|
||||
InstanceMethod("getFeatureReport", &HID::getFeatureReport, napi_enumerable),
|
||||
InstanceMethod("sendFeatureReport", &HID::sendFeatureReport, napi_enumerable),
|
||||
InstanceMethod("setNonBlocking", &HID::setNonBlocking, napi_enumerable),
|
||||
InstanceMethod("readSync", &HID::readSync, napi_enumerable),
|
||||
InstanceMethod("readTimeout", &HID::readTimeout, napi_enumerable),
|
||||
InstanceMethod("getDeviceInfo", &HID::getDeviceInfo, napi_enumerable),
|
||||
});
|
||||
|
||||
return ctor;
|
||||
}
|
||||
33
node_modules/node-hid/src/HID.h
generated
vendored
Normal file
33
node_modules/node-hid/src/HID.h
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "util.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
class HID : public Napi::ObjectWrap<HID>
|
||||
{
|
||||
public:
|
||||
static Napi::Value Initialize(Napi::Env &env);
|
||||
|
||||
void closeHandle();
|
||||
|
||||
HID(const Napi::CallbackInfo &info);
|
||||
~HID() { closeHandle(); }
|
||||
|
||||
hid_device *_hidHandle;
|
||||
|
||||
std::atomic<bool> _readRunning = {false};
|
||||
std::atomic<bool> _readInterrupt = {false};
|
||||
|
||||
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 readInterrupt(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);
|
||||
};
|
||||
814
node_modules/node-hid/src/HIDAsync.cc
generated
vendored
Normal file
814
node_modules/node-hid/src/HIDAsync.cc
generated
vendored
Normal file
@@ -0,0 +1,814 @@
|
||||
// -*- 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 <sstream>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
|
||||
#include "devices.h"
|
||||
#include "util.h"
|
||||
#include "HIDAsync.h"
|
||||
#include "read.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "../hidapi/mac/hidapi_darwin.h"
|
||||
#endif
|
||||
|
||||
HIDAsync::HIDAsync(const Napi::CallbackInfo &info)
|
||||
: Napi::ObjectWrap<HIDAsync>(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<Napi::External<hid_device>>().Data();
|
||||
_hidHandle = std::make_shared<DeviceContext>(appCtx, ptr);
|
||||
read_state = nullptr;
|
||||
}
|
||||
|
||||
class CloseWorker : public PromiseAsyncWorker<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
CloseWorker(
|
||||
Napi::Env &env, std::shared_ptr<DeviceContext> hid, std::shared_ptr<ReadThreadState> read_state)
|
||||
: PromiseAsyncWorker(env, hid),
|
||||
read_state(std::move(read_state)) {}
|
||||
|
||||
// This code will be executed on the worker thread. Note: Napi types cannot be used
|
||||
void Execute() override
|
||||
{
|
||||
if (read_state)
|
||||
{
|
||||
read_state->abort = true;
|
||||
|
||||
// Wait for the thread to terminate
|
||||
read_state->wait();
|
||||
|
||||
read_state = nullptr;
|
||||
}
|
||||
|
||||
if (context->hid)
|
||||
{
|
||||
hid_close(context->hid);
|
||||
context->hid = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Napi::Value GetPromiseResult(const Napi::Env &env) override
|
||||
{
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ReadThreadState> read_state;
|
||||
};
|
||||
|
||||
void HIDAsync::closeHandle()
|
||||
{
|
||||
if (read_state)
|
||||
{
|
||||
read_state->abort = true;
|
||||
read_state = nullptr;
|
||||
}
|
||||
|
||||
// hid_close is called by the destructor
|
||||
_hidHandle = nullptr;
|
||||
}
|
||||
|
||||
class OpenByPathWorker : public PromiseAsyncWorker<ContextState *>
|
||||
{
|
||||
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<std::mutex> 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<hid_device>::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<ContextState *>
|
||||
{
|
||||
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<std::mutex> 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<hid_device>::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();
|
||||
|
||||
auto argsLength = info.Length();
|
||||
|
||||
if (argsLength < 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;
|
||||
|
||||
bool isNonExclusiveBool = false;
|
||||
if (argsLength > 1)
|
||||
{
|
||||
// We check if we have an optional param
|
||||
if (info[argsLength - 1].IsObject())
|
||||
{
|
||||
argsLength -= 1;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
Napi::Object options = info[argsLength].As<Napi::Object>();
|
||||
Napi::Value isNonExclusiveMode = options.Get("nonExclusive");
|
||||
if (!isNonExclusiveMode.IsBoolean())
|
||||
{
|
||||
Napi::TypeError::New(env, "nonExclusive flag should be a boolean").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
isNonExclusiveBool = isNonExclusiveMode.As<Napi::Boolean>().Value();
|
||||
hid_darwin_set_open_exclusive(isNonExclusiveBool ? 0 : 1);
|
||||
#else
|
||||
// silence unused variable warning
|
||||
(void)isNonExclusiveBool;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (argsLength == 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<Napi::String>().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 (argsLength > 2)
|
||||
{
|
||||
if (!info[2].IsString())
|
||||
{
|
||||
Napi::TypeError::New(env, "Serial must be a string").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
serial = info[2].As<Napi::String>().Utf8Value();
|
||||
}
|
||||
|
||||
int32_t vendorId = info[0].As<Napi::Number>().Int32Value();
|
||||
int32_t productId = info[1].As<Napi::Number>().Int32Value();
|
||||
|
||||
return (new OpenByUsbIdsWorker(env, context, vendorId, productId, serial))->QueueAndRun();
|
||||
}
|
||||
}
|
||||
|
||||
Napi::Value HIDAsync::readStart(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (_hidHandle->is_closed)
|
||||
{
|
||||
Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
if (read_state && read_state->is_running())
|
||||
{
|
||||
Napi::TypeError::New(env, "read is already running").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
auto callback = info[0].As<Napi::Function>();
|
||||
read_state = start_read_helper(env, _hidHandle, callback);
|
||||
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
class ReadStopWorker : public PromiseAsyncWorker<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
ReadStopWorker(
|
||||
Napi::Env &env,
|
||||
std::shared_ptr<DeviceContext> hid,
|
||||
std::shared_ptr<ReadThreadState> read_state)
|
||||
: PromiseAsyncWorker(env, hid),
|
||||
read_state(std::move(read_state))
|
||||
{
|
||||
}
|
||||
~ReadStopWorker()
|
||||
{
|
||||
}
|
||||
|
||||
// This code will be executed on the worker thread. Note: Napi types cannot be used
|
||||
void Execute() override
|
||||
{
|
||||
read_state->abort = true;
|
||||
|
||||
// Wait for the thread to terminate
|
||||
read_state->wait();
|
||||
|
||||
read_state = nullptr;
|
||||
}
|
||||
|
||||
Napi::Value GetPromiseResult(const Napi::Env &env) override
|
||||
{
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ReadThreadState> read_state;
|
||||
};
|
||||
|
||||
Napi::Value HIDAsync::readStop(const Napi::CallbackInfo &info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (!read_state || !read_state->is_running())
|
||||
{
|
||||
// Napi::TypeError::New(env, "device has been closed").ThrowAsJavaScriptException();
|
||||
return env.Null();
|
||||
}
|
||||
|
||||
auto result = (new ReadStopWorker(env, std::move(_hidHandle), std::move(read_state)))->QueueAndRun();
|
||||
|
||||
// Ownership is transferred to ReadStopWorker
|
||||
read_state = nullptr;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
class ReadOnceWorker : public PromiseAsyncWorker<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
ReadOnceWorker(
|
||||
Napi::Env &env,
|
||||
std::shared_ptr<DeviceContext> 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<unsigned char>::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 (read_state != nullptr && read_state->is_running())
|
||||
{
|
||||
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<Napi::Number>().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<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
GetFeatureReportWorker(
|
||||
Napi::Env &env,
|
||||
std::shared_ptr<DeviceContext> 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<unsigned char>::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<Napi::Number>().Uint32Value();
|
||||
const int bufSize = info[1].As<Napi::Number>().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<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
SendFeatureReportWorker(
|
||||
Napi::Env &env,
|
||||
std::shared_ptr<DeviceContext> hid,
|
||||
std::vector<unsigned char> 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<unsigned char> 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<unsigned char> 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(read_state)))->QueueAndRun();
|
||||
|
||||
// Ownership is transferred to CloseWorker
|
||||
_hidHandle = nullptr;
|
||||
read_state = nullptr;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class SetNonBlockingWorker : public PromiseAsyncWorker<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
SetNonBlockingWorker(
|
||||
Napi::Env &env,
|
||||
std::shared_ptr<DeviceContext> 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<Napi::Number>().Int32Value();
|
||||
|
||||
return (new SetNonBlockingWorker(env, _hidHandle, blockStatus))->QueueAndRun();
|
||||
}
|
||||
|
||||
class WriteWorker : public PromiseAsyncWorker<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
WriteWorker(
|
||||
Napi::Env &env,
|
||||
std::shared_ptr<DeviceContext> hid,
|
||||
std::vector<unsigned char> 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<unsigned char> 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<unsigned char> 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<std::shared_ptr<DeviceContext>>
|
||||
{
|
||||
public:
|
||||
GetDeviceInfoWorker(
|
||||
Napi::Env &env,
|
||||
std::shared_ptr<DeviceContext> 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;
|
||||
}
|
||||
29
node_modules/node-hid/src/HIDAsync.h
generated
vendored
Normal file
29
node_modules/node-hid/src/HIDAsync.h
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "util.h"
|
||||
#include "read.h"
|
||||
|
||||
class HIDAsync : public Napi::ObjectWrap<HIDAsync>
|
||||
{
|
||||
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<DeviceContext> _hidHandle;
|
||||
std::shared_ptr<ReadThreadState> read_state;
|
||||
|
||||
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);
|
||||
};
|
||||
158
node_modules/node-hid/src/devices.cc
generated
vendored
Normal file
158
node_modules/node-hid/src/devices.cc
generated
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
#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<Napi::Number>().Int32Value();
|
||||
*productId = info[1].As<Napi::Number>().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<std::mutex> lock(appCtx->enumerateLock);
|
||||
devs = hid_enumerate(vendorId, productId);
|
||||
}
|
||||
return generateDevicesResultAndFree(env, devs);
|
||||
}
|
||||
|
||||
class DevicesWorker : public PromiseAsyncWorker<ContextState *>
|
||||
{
|
||||
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<std::mutex> 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();
|
||||
}
|
||||
7
node_modules/node-hid/src/devices.h
generated
vendored
Normal file
7
node_modules/node-hid/src/devices.h
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
#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);
|
||||
45
node_modules/node-hid/src/exports.cc
generated
vendored
Normal file
45
node_modules/node-hid/src/exports.cc
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
#include <mutex>
|
||||
|
||||
#include "util.h"
|
||||
|
||||
#include "HID.h"
|
||||
#include "HIDAsync.h"
|
||||
#include "devices.h"
|
||||
|
||||
static void
|
||||
deinitialize(void *ptr)
|
||||
{
|
||||
auto ptr2 = static_cast<ContextState *>(ptr);
|
||||
delete ptr2;
|
||||
}
|
||||
|
||||
Napi::Object
|
||||
Init(Napi::Env env, Napi::Object exports)
|
||||
{
|
||||
std::shared_ptr<ApplicationContext> 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
|
||||
|
||||
exports.Set("hidapiVersion", Napi::String::New(env, HID_API_VERSION_STR));
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
|
||||
151
node_modules/node-hid/src/read.cc
generated
vendored
Normal file
151
node_modules/node-hid/src/read.cc
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
#include "read.h"
|
||||
|
||||
struct ReadCallbackContext;
|
||||
|
||||
struct ReadCallbackProps
|
||||
{
|
||||
unsigned char *buf;
|
||||
int len;
|
||||
};
|
||||
|
||||
using Context = ReadCallbackContext;
|
||||
using DataType = ReadCallbackProps;
|
||||
void ReadCallback(Napi::Env env, Napi::Function callback, Context *context, DataType *data);
|
||||
using TSFN = Napi::TypedThreadSafeFunction<Context, DataType, ReadCallback>;
|
||||
using FinalizerDataType = void;
|
||||
|
||||
struct ReadCallbackContext
|
||||
{
|
||||
std::shared_ptr<ReadThreadState> state;
|
||||
|
||||
std::shared_ptr<DeviceContext> _hidHandle;
|
||||
std::thread read_thread;
|
||||
|
||||
TSFN read_callback;
|
||||
};
|
||||
|
||||
void ReadCallback(Napi::Env env, Napi::Function callback, Context *context, DataType *data)
|
||||
{
|
||||
if (env != nullptr && callback != nullptr) //&& context != nullptr)
|
||||
{
|
||||
if (data == nullptr)
|
||||
{
|
||||
auto error = Napi::String::New(env, "could not read from HID device");
|
||||
|
||||
callback.Call({error, env.Null()});
|
||||
}
|
||||
else
|
||||
{
|
||||
auto buffer = Napi::Buffer<unsigned char>::Copy(env, data->buf, data->len);
|
||||
|
||||
callback.Call({env.Null(), buffer});
|
||||
}
|
||||
}
|
||||
|
||||
if (data != nullptr)
|
||||
{
|
||||
delete data->buf;
|
||||
delete data;
|
||||
}
|
||||
};
|
||||
|
||||
bool ReadThreadState::is_running()
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(lock);
|
||||
return running;
|
||||
}
|
||||
void ReadThreadState::wait()
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(lock);
|
||||
if (running)
|
||||
{
|
||||
wait_for_end.wait(lk, [this]
|
||||
{ return !running; });
|
||||
}
|
||||
}
|
||||
|
||||
void ReadThreadState::release()
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(lock);
|
||||
running = false;
|
||||
wait_for_end.notify_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting the thread safety of this correct has been challenging.
|
||||
* There is a problem that the read thread can take 50ms to exit once run_read becomes false, and we don't want to block the event loop waiting for it.
|
||||
* And a problem of that either the read_thread can decide to abort by itself.
|
||||
* This means that either read_thread or the class that spawned it could outlive the other.
|
||||
*
|
||||
* Doing this in a class is hard, as the tsfn needs to be the one to own and free the class, but the caller needs to be able to 'ask' for an abort.
|
||||
* This method is a simplified form, using a 'context' class whose ownership gets assigned to the tsfn, with the caller only getting the atomic<bool>
|
||||
* to both be able to know if the loop is running and to 'ask' it to stop
|
||||
*
|
||||
* While this does now return a struct to handle the shared state, the tsfn and thread are importantly not on this class.
|
||||
*/
|
||||
std::shared_ptr<ReadThreadState> start_read_helper(Napi::Env env, std::shared_ptr<DeviceContext> hidHandle, Napi::Function callback)
|
||||
{
|
||||
auto state = std::make_shared<ReadThreadState>();
|
||||
|
||||
auto context = new ReadCallbackContext;
|
||||
context->state = state;
|
||||
context->_hidHandle = std::move(hidHandle);
|
||||
|
||||
context->read_thread = std::thread([context]()
|
||||
{
|
||||
int mswait = 50;
|
||||
int len = 0;
|
||||
unsigned char *buf = new unsigned char[READ_BUFF_MAXSIZE];
|
||||
|
||||
while (!context->state->abort)
|
||||
{
|
||||
len = hid_read_timeout(context->_hidHandle->hid, buf, READ_BUFF_MAXSIZE, mswait);
|
||||
if (context->state->abort)
|
||||
break;
|
||||
|
||||
if (len < 0)
|
||||
{
|
||||
// Emit and error and stop reading
|
||||
context->read_callback.BlockingCall(nullptr);
|
||||
break;
|
||||
}
|
||||
else if (len > 0)
|
||||
{
|
||||
auto data = new ReadCallbackProps;
|
||||
data->buf = buf;
|
||||
data->len = len;
|
||||
|
||||
context->read_callback.BlockingCall(data);
|
||||
// buf is now owned by ReadCallback
|
||||
buf = new unsigned char[READ_BUFF_MAXSIZE];
|
||||
}
|
||||
}
|
||||
|
||||
delete[] buf;
|
||||
|
||||
// Mark the state and used hidHandle as released
|
||||
context->state->release();
|
||||
|
||||
// Cleanup the function
|
||||
context->read_callback.Release(); });
|
||||
|
||||
context->read_callback = TSFN::New(
|
||||
env,
|
||||
callback, // JavaScript function called asynchronously
|
||||
"HID:read", // Name
|
||||
0, // Unlimited queue
|
||||
1, // Only one thread will use this initially
|
||||
context, // Context
|
||||
[](Napi::Env, void *, Context *context) { // Finalizer used to clean threads up
|
||||
if (context->read_thread.joinable())
|
||||
{
|
||||
// Ensure the thread has terminated
|
||||
context->read_thread.join();
|
||||
}
|
||||
|
||||
// Free the context
|
||||
delete context;
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
28
node_modules/node-hid/src/read.h
generated
vendored
Normal file
28
node_modules/node-hid/src/read.h
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef NODEHID_READ_H__
|
||||
#define NODEHID_READ_H__
|
||||
|
||||
#include "util.h"
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
|
||||
struct ReadThreadState
|
||||
{
|
||||
std::atomic<bool> abort = {false};
|
||||
|
||||
bool is_running();
|
||||
void wait();
|
||||
|
||||
void release();
|
||||
|
||||
private:
|
||||
std::mutex lock;
|
||||
bool running = true;
|
||||
std::condition_variable wait_for_end;
|
||||
};
|
||||
|
||||
std::shared_ptr<ReadThreadState>
|
||||
start_read_helper(Napi::Env env, std::shared_ptr<DeviceContext> hidHandle, Napi::Function callback);
|
||||
|
||||
#endif // NODEHID_READ_H__
|
||||
13
node_modules/node-hid/src/show-devices.js
generated
vendored
Executable file
13
node_modules/node-hid/src/show-devices.js
generated
vendored
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var HID = require('../');
|
||||
|
||||
// Linux: choose driverType
|
||||
// default is 'hidraw', can also be 'libusb'
|
||||
if( process.argv[2] ) {
|
||||
var type = process.argv[2];
|
||||
console.log("driverType:",type);
|
||||
HID.setDriverType( type );
|
||||
}
|
||||
|
||||
console.log('devices:', HID.devices());
|
||||
126
node_modules/node-hid/src/util.cc
generated
vendored
Normal file
126
node_modules/node-hid/src/util.cc
generated
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
#include <sstream>
|
||||
#include <locale>
|
||||
#include <codecvt>
|
||||
|
||||
#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<ApplicationContext> 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<std::mutex> lock(lockApplicationContext);
|
||||
|
||||
if (hid_exit())
|
||||
{
|
||||
// thread is exiting, can't log?
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<ApplicationContext> 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<std::mutex> lock(lockApplicationContext);
|
||||
|
||||
auto ref = weakApplicationContext.lock();
|
||||
if (!ref)
|
||||
{
|
||||
// Not initialised, so lets do that
|
||||
if (hid_init())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ref = std::make_shared<ApplicationContext>();
|
||||
weakApplicationContext = ref;
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
std::string utf8_encode(const std::wstring &source)
|
||||
{
|
||||
return std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(source);
|
||||
}
|
||||
|
||||
std::wstring utf8_decode(const std::string &source)
|
||||
{
|
||||
return std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(source);
|
||||
}
|
||||
|
||||
std::string copyArrayOrBufferIntoVector(const Napi::Value &val, std::vector<unsigned char> &message)
|
||||
{
|
||||
if (val.IsBuffer())
|
||||
{
|
||||
Napi::Buffer<unsigned char> buffer = val.As<Napi::Buffer<unsigned char>>();
|
||||
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<Napi::Array>();
|
||||
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<Napi::Number>().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<std::mutex> lock(jobQueueMutex);
|
||||
if (!isRunning)
|
||||
{
|
||||
isRunning = true;
|
||||
job->Queue();
|
||||
}
|
||||
else
|
||||
{
|
||||
jobQueue.push(job);
|
||||
}
|
||||
}
|
||||
|
||||
void AsyncWorkerQueue::JobFinished(const Napi::Env &)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(jobQueueMutex);
|
||||
|
||||
if (jobQueue.size() == 0)
|
||||
{
|
||||
isRunning = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto newJob = jobQueue.front();
|
||||
jobQueue.pop();
|
||||
newJob->Queue();
|
||||
}
|
||||
}
|
||||
153
node_modules/node-hid/src/util.h
generated
vendored
Normal file
153
node_modules/node-hid/src/util.h
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
#ifndef NODEHID_UTIL_H__
|
||||
#define NODEHID_UTIL_H__
|
||||
|
||||
#define NAPI_VERSION 4
|
||||
#include <napi.h>
|
||||
|
||||
#include <queue>
|
||||
|
||||
#include <hidapi.h>
|
||||
|
||||
#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<unsigned char> &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<ApplicationContext> 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<Napi::AsyncWorker *> 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<ApplicationContext> appCtx, Napi::FunctionReference asyncCtor) : AsyncWorkerQueue(), appCtx(appCtx), asyncCtor(std::move(asyncCtor)) {}
|
||||
|
||||
// Keep the ApplicationContext alive for longer than this state
|
||||
std::shared_ptr<ApplicationContext> appCtx;
|
||||
|
||||
// Constructor for the HIDAsync class
|
||||
Napi::FunctionReference asyncCtor;
|
||||
};
|
||||
|
||||
class DeviceContext : public AsyncWorkerQueue
|
||||
{
|
||||
public:
|
||||
DeviceContext(std::shared_ptr<ApplicationContext> 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<ApplicationContext> appCtx;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
class PromiseAsyncWorker : public Napi::AsyncWorker
|
||||
{
|
||||
public:
|
||||
PromiseAsyncWorker(
|
||||
const Napi::Env &env, T context)
|
||||
: Napi::AsyncWorker(env),
|
||||
context(context),
|
||||
deferred(Napi::Promise::Deferred::New(env)),
|
||||
// Create an error now, to store the stack trace
|
||||
errorResult(Napi::Error::New(env, "Unknown error"))
|
||||
{
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
// Inject the the error message with the actual error
|
||||
errorResult.Value().Set("message", error.Message());
|
||||
|
||||
context->JobFinished(Env());
|
||||
deferred.Reject(errorResult.Value());
|
||||
}
|
||||
|
||||
Napi::Promise QueueAndRun()
|
||||
{
|
||||
auto promise = deferred.Promise();
|
||||
|
||||
context->QueueJob(Env(), this);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
protected:
|
||||
T context;
|
||||
|
||||
private:
|
||||
Napi::Promise::Deferred deferred;
|
||||
Napi::Error errorResult;
|
||||
};
|
||||
|
||||
#endif // NODEHID_UTIL_H__
|
||||
Reference in New Issue
Block a user