Added support for Streamdeck Pedal and updated UI to better fit the Packed UI style

This commit is contained in:
2026-02-27 22:47:08 +01:00
committed by erik
parent 5a70f775f1
commit 93faae5cc8
1463 changed files with 306917 additions and 0 deletions

476
node_modules/node-hid/src/HID.cc generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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__