@tier0/opcua-client 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BENCHMARK.md +53 -2
- package/README.md +131 -14
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/binding.gyp +1 -1
- package/lib/client.js +48 -7
- package/lib/native.js +25 -10
- package/lib/points.js +187 -21
- package/lib/prebuild.js +42 -0
- package/lib/security.js +96 -0
- package/node_modules/node-addon-api/LICENSE.md +9 -0
- package/node_modules/node-addon-api/README.md +95 -0
- package/node_modules/node-addon-api/common.gypi +21 -0
- package/node_modules/node-addon-api/except.gypi +25 -0
- package/node_modules/node-addon-api/index.js +14 -0
- package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/node_modules/node-addon-api/napi-inl.h +7244 -0
- package/node_modules/node-addon-api/napi.h +3420 -0
- package/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/node_modules/node-addon-api/node_api.gyp +9 -0
- package/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/node_modules/node-addon-api/nothing.c +0 -0
- package/node_modules/node-addon-api/package-support.json +21 -0
- package/node_modules/node-addon-api/package.json +481 -0
- package/node_modules/node-addon-api/tools/README.md +73 -0
- package/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/nodes/connection.html +165 -6
- package/nodes/connection.js +99 -20
- package/nodes/read.html +3 -0
- package/nodes/read.js +3 -1
- package/nodes/subscribe.html +9 -3
- package/nodes/subscribe.js +330 -99
- package/nodes/write.html +3 -0
- package/package.json +15 -4
- package/prebuilds/linux-x64-musl/opcua_open62541.node +0 -0
- package/scripts/build-offline-prebuild.sh +15 -0
- package/scripts/build-open62541.sh +14 -4
- package/scripts/build-test-server.sh +23 -0
- package/scripts/install.js +28 -0
- package/scripts/verify-offline-package.js +20 -0
- package/scripts/verify-runtime.js +20 -0
- package/src/addon.cc +707 -119
- package/vendor/open62541-LICENSE +373 -0
- package/vendor/open62541-v1.4.16.tar.gz +0 -0
package/src/addon.cc
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
#include <algorithm>
|
|
10
10
|
#include <atomic>
|
|
11
11
|
#include <chrono>
|
|
12
|
+
#include <condition_variable>
|
|
12
13
|
#include <cstdint>
|
|
13
14
|
#include <deque>
|
|
15
|
+
#include <exception>
|
|
16
|
+
#include <fstream>
|
|
17
|
+
#include <functional>
|
|
18
|
+
#include <limits>
|
|
14
19
|
#include <map>
|
|
15
20
|
#include <memory>
|
|
16
21
|
#include <mutex>
|
|
@@ -33,6 +38,78 @@ std::string UaString(const UA_String& value) {
|
|
|
33
38
|
return value.data ? std::string(reinterpret_cast<const char*>(value.data), value.length) : "";
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
std::string SecurityPolicyUri(const std::string& policy) {
|
|
42
|
+
if (policy == "None" ||
|
|
43
|
+
policy == "http://opcfoundation.org/UA/SecurityPolicy#None") {
|
|
44
|
+
return "http://opcfoundation.org/UA/SecurityPolicy#None";
|
|
45
|
+
}
|
|
46
|
+
if (policy == "Basic256Sha256" ||
|
|
47
|
+
policy == "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") {
|
|
48
|
+
return "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256";
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
UA_MessageSecurityMode SecurityMode(const std::string& mode) {
|
|
54
|
+
if (mode == "None") return UA_MESSAGESECURITYMODE_NONE;
|
|
55
|
+
if (mode == "Sign") return UA_MESSAGESECURITYMODE_SIGN;
|
|
56
|
+
if (mode == "SignAndEncrypt") return UA_MESSAGESECURITYMODE_SIGNANDENCRYPT;
|
|
57
|
+
return UA_MESSAGESECURITYMODE_INVALID;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
std::string SecurityModeName(UA_MessageSecurityMode mode) {
|
|
61
|
+
if (mode == UA_MESSAGESECURITYMODE_NONE) return "None";
|
|
62
|
+
if (mode == UA_MESSAGESECURITYMODE_SIGN) return "Sign";
|
|
63
|
+
if (mode == UA_MESSAGESECURITYMODE_SIGNANDENCRYPT) return "SignAndEncrypt";
|
|
64
|
+
return "Invalid";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
bool LoadFile(const std::string& path, UA_ByteString* output, std::string* error) {
|
|
68
|
+
UA_ByteString_init(output);
|
|
69
|
+
std::ifstream stream(path, std::ios::binary | std::ios::ate);
|
|
70
|
+
if (!stream) {
|
|
71
|
+
*error = "Cannot open OPC UA security file: " + path;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const std::streamoff end = stream.tellg();
|
|
75
|
+
if (end <= 0) {
|
|
76
|
+
*error = "OPC UA security file is empty: " + path;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const size_t size = static_cast<size_t>(end);
|
|
80
|
+
if (!UA_StatusCode_isGood(UA_ByteString_allocBuffer(output, size))) {
|
|
81
|
+
*error = "Cannot allocate OPC UA security file buffer for: " + path;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
stream.seekg(0, std::ios::beg);
|
|
85
|
+
stream.read(reinterpret_cast<char*>(output->data), static_cast<std::streamsize>(size));
|
|
86
|
+
if (!stream) {
|
|
87
|
+
UA_ByteString_clear(output);
|
|
88
|
+
*error = "Cannot read OPC UA security file: " + path;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void ClearByteStrings(std::vector<UA_ByteString>* values) {
|
|
95
|
+
for (auto& value : *values) UA_ByteString_clear(&value);
|
|
96
|
+
values->clear();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
bool LoadFiles(const std::vector<std::string>& paths, std::vector<UA_ByteString>* output,
|
|
100
|
+
std::string* error) {
|
|
101
|
+
output->reserve(paths.size());
|
|
102
|
+
for (const auto& path : paths) {
|
|
103
|
+
UA_ByteString value;
|
|
104
|
+
if (!LoadFile(path, &value, error)) {
|
|
105
|
+
ClearByteStrings(output);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
output->push_back(value);
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
36
113
|
std::string NodeIdString(const UA_NodeId& node_id) {
|
|
37
114
|
UA_String text = UA_STRING_NULL;
|
|
38
115
|
if (!UA_StatusCode_isGood(UA_NodeId_print(&node_id, &text))) return "";
|
|
@@ -202,6 +279,83 @@ bool JsScalarToVariant(Napi::Env env, const Napi::Value& input, const std::strin
|
|
|
202
279
|
return false;
|
|
203
280
|
}
|
|
204
281
|
|
|
282
|
+
bool FlattenJsArray(Napi::Env env, const Napi::Value& input, size_t depth,
|
|
283
|
+
std::vector<uint32_t>* dimensions,
|
|
284
|
+
std::vector<Napi::Value>* values, size_t* leaf_depth) {
|
|
285
|
+
if (!input.IsArray()) {
|
|
286
|
+
if (*leaf_depth == std::numeric_limits<size_t>::max()) *leaf_depth = depth;
|
|
287
|
+
if (*leaf_depth != depth) {
|
|
288
|
+
Napi::TypeError::New(env, "OPC UA array value must be rectangular").ThrowAsJavaScriptException();
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
values->push_back(input);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
Napi::Array array = input.As<Napi::Array>();
|
|
295
|
+
if (array.Length() == 0) {
|
|
296
|
+
Napi::TypeError::New(env, "OPC UA array value must not be empty").ThrowAsJavaScriptException();
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (dimensions->size() == depth) dimensions->push_back(array.Length());
|
|
300
|
+
else if ((*dimensions)[depth] != array.Length()) {
|
|
301
|
+
Napi::TypeError::New(env, "OPC UA array value must be rectangular").ThrowAsJavaScriptException();
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
for (uint32_t index = 0; index < array.Length(); ++index) {
|
|
305
|
+
if (!FlattenJsArray(env, array.Get(index), depth + 1, dimensions, values, leaf_depth)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
bool JsValueToVariant(Napi::Env env, const Napi::Value& input, const std::string& name,
|
|
313
|
+
UA_Variant* output) {
|
|
314
|
+
if (!input.IsArray()) return JsScalarToVariant(env, input, name, output);
|
|
315
|
+
const UA_DataType* type = DataTypeByName(name);
|
|
316
|
+
if (!type) {
|
|
317
|
+
Napi::TypeError::New(env, "Unsupported OPC UA data type: " + name).ThrowAsJavaScriptException();
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
std::vector<uint32_t> dimensions;
|
|
321
|
+
std::vector<Napi::Value> values;
|
|
322
|
+
size_t leaf_depth = std::numeric_limits<size_t>::max();
|
|
323
|
+
if (!FlattenJsArray(env, input, 0, &dimensions, &values, &leaf_depth)) return false;
|
|
324
|
+
void* data = UA_Array_new(values.size(), type);
|
|
325
|
+
if (!data) {
|
|
326
|
+
Napi::Error::New(env, "Failed to allocate OPC UA array value").ThrowAsJavaScriptException();
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
for (size_t index = 0; index < values.size(); ++index) {
|
|
330
|
+
UA_Variant scalar;
|
|
331
|
+
if (!JsScalarToVariant(env, values[index], name, &scalar)) {
|
|
332
|
+
UA_Variant_clear(&scalar);
|
|
333
|
+
UA_Array_delete(data, values.size(), type);
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
void* target = static_cast<char*>(data) + index * type->memSize;
|
|
337
|
+
const UA_StatusCode status = UA_copy(scalar.data, target, type);
|
|
338
|
+
UA_Variant_clear(&scalar);
|
|
339
|
+
if (!UA_StatusCode_isGood(status)) {
|
|
340
|
+
UA_Array_delete(data, values.size(), type);
|
|
341
|
+
Napi::Error::New(env, "Failed to copy OPC UA array value").ThrowAsJavaScriptException();
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
UA_Variant_init(output);
|
|
346
|
+
UA_Variant_setArray(output, data, values.size(), type);
|
|
347
|
+
output->arrayDimensions = static_cast<UA_UInt32*>(
|
|
348
|
+
UA_Array_new(dimensions.size(), &UA_TYPES[UA_TYPES_UINT32]));
|
|
349
|
+
if (!output->arrayDimensions) {
|
|
350
|
+
UA_Variant_clear(output);
|
|
351
|
+
Napi::Error::New(env, "Failed to allocate OPC UA array dimensions").ThrowAsJavaScriptException();
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
output->arrayDimensionsSize = dimensions.size();
|
|
355
|
+
std::copy(dimensions.begin(), dimensions.end(), output->arrayDimensions);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
205
359
|
std::string GetString(const Napi::Object& object, const char* name, const std::string& fallback = "") {
|
|
206
360
|
if (!object.Has(name)) return fallback;
|
|
207
361
|
Napi::Value value = object.Get(name);
|
|
@@ -227,6 +381,7 @@ bool GetBool(const Napi::Object& object, const char* name, bool fallback) {
|
|
|
227
381
|
|
|
228
382
|
struct RawChange {
|
|
229
383
|
uint32_t monitored_item_id = 0;
|
|
384
|
+
uint32_t client_handle = 0;
|
|
230
385
|
std::string node_id;
|
|
231
386
|
UA_DataValue value;
|
|
232
387
|
std::chrono::steady_clock::time_point enqueued_at;
|
|
@@ -261,19 +416,23 @@ struct MonitoredContext {
|
|
|
261
416
|
std::shared_ptr<NotificationQueue> notifications;
|
|
262
417
|
std::string node_id;
|
|
263
418
|
uint32_t monitored_item_id = 0;
|
|
419
|
+
uint32_t client_handle = 0;
|
|
264
420
|
};
|
|
265
421
|
|
|
266
422
|
struct SubscriptionContext {
|
|
267
423
|
uint32_t subscription_id = 0;
|
|
268
424
|
std::shared_ptr<NotificationQueue> notifications;
|
|
269
425
|
std::map<uint32_t, std::unique_ptr<MonitoredContext>> monitored;
|
|
426
|
+
std::unordered_map<uint32_t, MonitoredContext*> monitored_by_client_handle;
|
|
427
|
+
uint32_t next_client_handle = 1;
|
|
270
428
|
};
|
|
271
429
|
|
|
272
430
|
struct OperationLimits {
|
|
273
|
-
uint32_t max_nodes_per_read
|
|
274
|
-
uint32_t max_nodes_per_write
|
|
275
|
-
uint32_t max_nodes_per_browse
|
|
276
|
-
uint32_t max_monitored_items_per_call
|
|
431
|
+
std::atomic<uint32_t> max_nodes_per_read{0};
|
|
432
|
+
std::atomic<uint32_t> max_nodes_per_write{0};
|
|
433
|
+
std::atomic<uint32_t> max_nodes_per_browse{0};
|
|
434
|
+
std::atomic<uint32_t> max_monitored_items_per_call{0};
|
|
435
|
+
std::atomic<uint32_t> max_monitored_items_per_subscription{0};
|
|
277
436
|
};
|
|
278
437
|
|
|
279
438
|
struct CachedNodeId {
|
|
@@ -284,11 +443,34 @@ struct CachedNodeId {
|
|
|
284
443
|
CachedNodeId& operator=(const CachedNodeId&) = delete;
|
|
285
444
|
};
|
|
286
445
|
|
|
446
|
+
struct ClientCommand {
|
|
447
|
+
std::function<void()> execute;
|
|
448
|
+
std::mutex completion_mutex;
|
|
449
|
+
std::condition_variable completion_cv;
|
|
450
|
+
bool completed = false;
|
|
451
|
+
std::string rejection;
|
|
452
|
+
};
|
|
453
|
+
|
|
287
454
|
struct ClientContext {
|
|
288
455
|
UA_Client* client = nullptr;
|
|
456
|
+
std::string endpoint;
|
|
457
|
+
std::string security_mode;
|
|
458
|
+
std::string security_policy_uri;
|
|
459
|
+
std::string identity_type;
|
|
460
|
+
std::string private_key_password;
|
|
289
461
|
std::atomic<bool> running{false};
|
|
290
462
|
std::thread iterate_thread;
|
|
291
|
-
std::mutex
|
|
463
|
+
std::mutex command_mutex;
|
|
464
|
+
std::condition_variable command_cv;
|
|
465
|
+
std::deque<std::shared_ptr<ClientCommand>> commands;
|
|
466
|
+
size_t command_queue_capacity = 256;
|
|
467
|
+
uint32_t command_queue_timeout_ms = 10000;
|
|
468
|
+
size_t command_queue_max_depth = 0;
|
|
469
|
+
std::atomic<uint64_t> commands_enqueued{0};
|
|
470
|
+
std::atomic<uint64_t> commands_completed{0};
|
|
471
|
+
std::atomic<uint64_t> commands_rejected{0};
|
|
472
|
+
std::atomic<uint64_t> commands_timed_out{0};
|
|
473
|
+
std::atomic<uint32_t> commands_active{0};
|
|
292
474
|
std::map<uint32_t, std::unique_ptr<SubscriptionContext>> subscriptions;
|
|
293
475
|
OperationLimits operation_limits;
|
|
294
476
|
std::mutex node_id_cache_mutex;
|
|
@@ -315,6 +497,18 @@ struct ClientContext {
|
|
|
315
497
|
std::atomic<uint64_t> session_generation{0};
|
|
316
498
|
};
|
|
317
499
|
|
|
500
|
+
#ifdef UA_ENABLE_ENCRYPTION
|
|
501
|
+
UA_StatusCode PrivateKeyPasswordCallback(UA_ClientConfig* config, UA_ByteString* password) {
|
|
502
|
+
auto* context = static_cast<ClientContext*>(config ? config->clientContext : nullptr);
|
|
503
|
+
if (!context || context->private_key_password.empty()) {
|
|
504
|
+
return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
|
|
505
|
+
}
|
|
506
|
+
*password = UA_BYTESTRING_ALLOC(context->private_key_password.c_str());
|
|
507
|
+
return password->length == context->private_key_password.size()
|
|
508
|
+
? UA_STATUSCODE_GOOD : UA_STATUSCODE_BADOUTOFMEMORY;
|
|
509
|
+
}
|
|
510
|
+
#endif
|
|
511
|
+
|
|
318
512
|
struct ClientStateEvent {
|
|
319
513
|
std::string state;
|
|
320
514
|
std::string reason;
|
|
@@ -474,17 +668,14 @@ bool CopyCachedNodeId(ClientContext* context, const std::string& text, UA_NodeId
|
|
|
474
668
|
}
|
|
475
669
|
|
|
476
670
|
void ReadOperationLimits(ClientContext* context) {
|
|
477
|
-
constexpr size_t count =
|
|
671
|
+
constexpr size_t count = 5;
|
|
478
672
|
const UA_UInt32 node_ids[count] = {
|
|
479
673
|
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERREAD,
|
|
480
674
|
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERWRITE,
|
|
481
675
|
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERBROWSE,
|
|
482
|
-
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXMONITOREDITEMSPERCALL
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
&context->operation_limits.max_nodes_per_write,
|
|
486
|
-
&context->operation_limits.max_nodes_per_browse,
|
|
487
|
-
&context->operation_limits.max_monitored_items_per_call};
|
|
676
|
+
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXMONITOREDITEMSPERCALL,
|
|
677
|
+
24104}; // Server.ServerCapabilities.MaxMonitoredItemsPerSubscription
|
|
678
|
+
uint32_t values[count] = {};
|
|
488
679
|
UA_ReadRequest request;
|
|
489
680
|
UA_ReadRequest_init(&request);
|
|
490
681
|
request.timestampsToReturn = UA_TIMESTAMPSTORETURN_NEITHER;
|
|
@@ -503,11 +694,16 @@ void ReadOperationLimits(ClientContext* context) {
|
|
|
503
694
|
const UA_DataValue& value = response.results[index];
|
|
504
695
|
if (value.hasValue && UA_Variant_isScalar(&value.value) &&
|
|
505
696
|
value.value.type == &UA_TYPES[UA_TYPES_UINT32] && value.value.data) {
|
|
506
|
-
|
|
697
|
+
values[index] = *static_cast<UA_UInt32*>(value.value.data);
|
|
507
698
|
}
|
|
508
699
|
}
|
|
509
700
|
}
|
|
510
701
|
UA_ReadResponse_clear(&response);
|
|
702
|
+
context->operation_limits.max_nodes_per_read.store(values[0]);
|
|
703
|
+
context->operation_limits.max_nodes_per_write.store(values[1]);
|
|
704
|
+
context->operation_limits.max_nodes_per_browse.store(values[2]);
|
|
705
|
+
context->operation_limits.max_monitored_items_per_call.store(values[3]);
|
|
706
|
+
context->operation_limits.max_monitored_items_per_subscription.store(values[4]);
|
|
511
707
|
}
|
|
512
708
|
|
|
513
709
|
std::mutex clients_mutex;
|
|
@@ -520,11 +716,106 @@ std::shared_ptr<ClientContext> FindClient(uint64_t client_id) {
|
|
|
520
716
|
return found == clients.end() ? nullptr : found->second;
|
|
521
717
|
}
|
|
522
718
|
|
|
719
|
+
void CloseNotificationQueue(const std::shared_ptr<NotificationQueue>& queue);
|
|
720
|
+
|
|
721
|
+
void CompleteClientCommand(const std::shared_ptr<ClientCommand>& command,
|
|
722
|
+
std::string rejection = {}) {
|
|
723
|
+
{
|
|
724
|
+
std::lock_guard<std::mutex> lock(command->completion_mutex);
|
|
725
|
+
command->rejection = std::move(rejection);
|
|
726
|
+
command->completed = true;
|
|
727
|
+
}
|
|
728
|
+
command->completion_cv.notify_one();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
std::string RunClientCommand(const std::shared_ptr<ClientContext>& context,
|
|
732
|
+
std::function<void()> execute) {
|
|
733
|
+
auto command = std::make_shared<ClientCommand>();
|
|
734
|
+
command->execute = std::move(execute);
|
|
735
|
+
{
|
|
736
|
+
std::lock_guard<std::mutex> lock(context->command_mutex);
|
|
737
|
+
if (!context->running.load()) {
|
|
738
|
+
context->commands_rejected.fetch_add(1);
|
|
739
|
+
return "OPC UA client is closing";
|
|
740
|
+
}
|
|
741
|
+
if (context->commands.size() >= context->command_queue_capacity) {
|
|
742
|
+
context->commands_rejected.fetch_add(1);
|
|
743
|
+
return "OPC UA client command queue is full";
|
|
744
|
+
}
|
|
745
|
+
context->commands.push_back(command);
|
|
746
|
+
context->commands_enqueued.fetch_add(1);
|
|
747
|
+
context->command_queue_max_depth =
|
|
748
|
+
std::max(context->command_queue_max_depth, context->commands.size());
|
|
749
|
+
}
|
|
750
|
+
context->command_cv.notify_one();
|
|
751
|
+
|
|
752
|
+
std::unique_lock<std::mutex> lock(command->completion_mutex);
|
|
753
|
+
if (!command->completion_cv.wait_for(
|
|
754
|
+
lock, std::chrono::milliseconds(context->command_queue_timeout_ms),
|
|
755
|
+
[&] { return command->completed; })) {
|
|
756
|
+
lock.unlock();
|
|
757
|
+
bool removed = false;
|
|
758
|
+
{
|
|
759
|
+
std::lock_guard<std::mutex> queue_lock(context->command_mutex);
|
|
760
|
+
auto found = std::find(context->commands.begin(), context->commands.end(), command);
|
|
761
|
+
if (found != context->commands.end()) {
|
|
762
|
+
context->commands.erase(found);
|
|
763
|
+
removed = true;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (removed) {
|
|
767
|
+
context->commands_rejected.fetch_add(1);
|
|
768
|
+
context->commands_timed_out.fetch_add(1);
|
|
769
|
+
CompleteClientCommand(command, "OPC UA client command timed out while queued");
|
|
770
|
+
}
|
|
771
|
+
lock.lock();
|
|
772
|
+
command->completion_cv.wait(lock, [&] { return command->completed; });
|
|
773
|
+
}
|
|
774
|
+
return command->rejection;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
void RejectPendingClientCommands(ClientContext* context, const std::string& reason) {
|
|
778
|
+
std::deque<std::shared_ptr<ClientCommand>> pending;
|
|
779
|
+
{
|
|
780
|
+
std::lock_guard<std::mutex> lock(context->command_mutex);
|
|
781
|
+
pending.swap(context->commands);
|
|
782
|
+
}
|
|
783
|
+
context->commands_rejected.fetch_add(pending.size());
|
|
784
|
+
for (auto& command : pending) CompleteClientCommand(command, reason);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
void DrainClientCommands(ClientContext* context, size_t maximum) {
|
|
788
|
+
for (size_t index = 0; index < maximum; ++index) {
|
|
789
|
+
std::shared_ptr<ClientCommand> command;
|
|
790
|
+
{
|
|
791
|
+
std::lock_guard<std::mutex> lock(context->command_mutex);
|
|
792
|
+
if (context->commands.empty()) return;
|
|
793
|
+
command = std::move(context->commands.front());
|
|
794
|
+
context->commands.pop_front();
|
|
795
|
+
}
|
|
796
|
+
context->commands_active.store(1);
|
|
797
|
+
try {
|
|
798
|
+
command->execute();
|
|
799
|
+
context->commands_active.store(0);
|
|
800
|
+
context->commands_completed.fetch_add(1);
|
|
801
|
+
CompleteClientCommand(command);
|
|
802
|
+
} catch (const std::exception& error) {
|
|
803
|
+
context->commands_active.store(0);
|
|
804
|
+
context->commands_rejected.fetch_add(1);
|
|
805
|
+
CompleteClientCommand(command, std::string("OPC UA client command failed: ") + error.what());
|
|
806
|
+
} catch (...) {
|
|
807
|
+
context->commands_active.store(0);
|
|
808
|
+
context->commands_rejected.fetch_add(1);
|
|
809
|
+
CompleteClientCommand(command, "OPC UA client command failed unexpectedly");
|
|
810
|
+
}
|
|
811
|
+
if (!context->running.load()) return;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
523
815
|
void Iterate(ClientContext* context) {
|
|
524
816
|
while (context->running.load()) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (!context->client) return;
|
|
817
|
+
DrainClientCommands(context, 32);
|
|
818
|
+
if (context->client) {
|
|
528
819
|
UA_Client_run_iterate(context->client, 20);
|
|
529
820
|
if (context->force_secure_channel_reconnect.exchange(false)) {
|
|
530
821
|
UA_Client_disconnectSecureChannelAsync(context->client);
|
|
@@ -534,7 +825,18 @@ void Iterate(ClientContext* context) {
|
|
|
534
825
|
ReadOperationLimits(context);
|
|
535
826
|
}
|
|
536
827
|
}
|
|
537
|
-
std::
|
|
828
|
+
std::unique_lock<std::mutex> lock(context->command_mutex);
|
|
829
|
+
if (context->commands.empty() && context->running.load()) {
|
|
830
|
+
context->command_cv.wait_for(lock, std::chrono::milliseconds(5));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
RejectPendingClientCommands(context, "OPC UA client is closing");
|
|
834
|
+
for (auto& entry : context->subscriptions) CloseNotificationQueue(entry.second->notifications);
|
|
835
|
+
context->subscriptions.clear();
|
|
836
|
+
if (context->client) {
|
|
837
|
+
UA_Client_disconnect(context->client);
|
|
838
|
+
UA_Client_delete(context->client);
|
|
839
|
+
context->client = nullptr;
|
|
538
840
|
}
|
|
539
841
|
}
|
|
540
842
|
|
|
@@ -553,16 +855,11 @@ void CloseClient(uint64_t client_id) {
|
|
|
553
855
|
context = found->second;
|
|
554
856
|
clients.erase(found);
|
|
555
857
|
}
|
|
556
|
-
|
|
858
|
+
const std::string shutdown_rejection = RunClientCommand(
|
|
859
|
+
context, [context] { context->running.store(false); });
|
|
860
|
+
if (!shutdown_rejection.empty()) context->running.store(false);
|
|
861
|
+
context->command_cv.notify_all();
|
|
557
862
|
if (context->iterate_thread.joinable()) context->iterate_thread.join();
|
|
558
|
-
std::lock_guard<std::mutex> lock(context->mutex);
|
|
559
|
-
for (auto& entry : context->subscriptions) CloseNotificationQueue(entry.second->notifications);
|
|
560
|
-
context->subscriptions.clear();
|
|
561
|
-
if (context->client) {
|
|
562
|
-
UA_Client_disconnect(context->client);
|
|
563
|
-
UA_Client_delete(context->client);
|
|
564
|
-
context->client = nullptr;
|
|
565
|
-
}
|
|
566
863
|
if (context->has_state_callback.load() &&
|
|
567
864
|
!context->state_callback_released.exchange(true)) {
|
|
568
865
|
context->state_callback.Release();
|
|
@@ -624,6 +921,7 @@ void DrainNotifications(Napi::Env env, Napi::Function callback, DrainRequest* re
|
|
|
624
921
|
Napi::Object result = Napi::Object::New(env);
|
|
625
922
|
result.Set("nodeId", data->node_id);
|
|
626
923
|
result.Set("monitoredItemId", data->monitored_item_id);
|
|
924
|
+
result.Set("clientHandle", data->client_handle);
|
|
627
925
|
result.Set("statusCode", StatusName(data->value.status));
|
|
628
926
|
if (data->value.hasValue) {
|
|
629
927
|
result.Set("value", VariantValue(env, &data->value.value));
|
|
@@ -714,6 +1012,7 @@ void DataChange(UA_Client*, UA_UInt32, void*, UA_UInt32 monitored_item_id,
|
|
|
714
1012
|
if (!monitored || !monitored->notifications || !value) return;
|
|
715
1013
|
auto change = std::make_unique<RawChange>();
|
|
716
1014
|
change->monitored_item_id = monitored_item_id;
|
|
1015
|
+
change->client_handle = monitored->client_handle;
|
|
717
1016
|
change->node_id = monitored->node_id;
|
|
718
1017
|
UA_DataValue_copy(value, &change->value);
|
|
719
1018
|
EnqueueNotification(monitored->notifications, std::move(change));
|
|
@@ -728,16 +1027,150 @@ class PromiseWorker : public Napi::AsyncWorker {
|
|
|
728
1027
|
Napi::Promise::Deferred deferred_;
|
|
729
1028
|
};
|
|
730
1029
|
|
|
1030
|
+
std::string UserTokenTypeName(UA_UserTokenType type) {
|
|
1031
|
+
if (type == UA_USERTOKENTYPE_ANONYMOUS) return "Anonymous";
|
|
1032
|
+
if (type == UA_USERTOKENTYPE_USERNAME) return "Username";
|
|
1033
|
+
if (type == UA_USERTOKENTYPE_CERTIFICATE) return "Certificate";
|
|
1034
|
+
if (type == UA_USERTOKENTYPE_ISSUEDTOKEN) return "IssuedToken";
|
|
1035
|
+
return "Unknown";
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
struct DiscoveredUserToken {
|
|
1039
|
+
std::string policy_id;
|
|
1040
|
+
std::string token_type;
|
|
1041
|
+
std::string security_policy_uri;
|
|
1042
|
+
std::string issued_token_type;
|
|
1043
|
+
std::string issuer_endpoint_url;
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
struct DiscoveredEndpoint {
|
|
1047
|
+
std::string endpoint_url;
|
|
1048
|
+
std::string security_mode;
|
|
1049
|
+
std::string security_policy_uri;
|
|
1050
|
+
std::string server_application_uri;
|
|
1051
|
+
std::string product_uri;
|
|
1052
|
+
uint8_t security_level = 0;
|
|
1053
|
+
std::vector<DiscoveredUserToken> user_identity_tokens;
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
class DiscoverEndpointsWorker : public PromiseWorker {
|
|
1057
|
+
public:
|
|
1058
|
+
DiscoverEndpointsWorker(Napi::Promise::Deferred deferred, std::string endpoint,
|
|
1059
|
+
uint32_t timeout)
|
|
1060
|
+
: PromiseWorker(deferred), endpoint_(std::move(endpoint)), timeout_(timeout) {}
|
|
1061
|
+
|
|
1062
|
+
void Execute() override {
|
|
1063
|
+
if (endpoint_.empty()) { SetError("OPC UA endpoint is required"); return; }
|
|
1064
|
+
UA_Client* client = UA_Client_new();
|
|
1065
|
+
if (!client) { SetError("Failed to create open62541 discovery client"); return; }
|
|
1066
|
+
UA_ClientConfig* config = UA_Client_getConfig(client);
|
|
1067
|
+
config->timeout = timeout_;
|
|
1068
|
+
size_t endpoint_count = 0;
|
|
1069
|
+
UA_EndpointDescription* endpoints = nullptr;
|
|
1070
|
+
const UA_StatusCode status = UA_Client_getEndpoints(
|
|
1071
|
+
client, endpoint_.c_str(), &endpoint_count, &endpoints);
|
|
1072
|
+
if (UA_StatusCode_isGood(status)) {
|
|
1073
|
+
endpoints_.reserve(endpoint_count);
|
|
1074
|
+
for (size_t index = 0; index < endpoint_count; ++index) {
|
|
1075
|
+
const UA_EndpointDescription& source = endpoints[index];
|
|
1076
|
+
DiscoveredEndpoint target;
|
|
1077
|
+
target.endpoint_url = UaString(source.endpointUrl);
|
|
1078
|
+
target.security_mode = SecurityModeName(source.securityMode);
|
|
1079
|
+
target.security_policy_uri = UaString(source.securityPolicyUri);
|
|
1080
|
+
target.server_application_uri = UaString(source.server.applicationUri);
|
|
1081
|
+
target.product_uri = UaString(source.server.productUri);
|
|
1082
|
+
target.security_level = source.securityLevel;
|
|
1083
|
+
target.user_identity_tokens.reserve(source.userIdentityTokensSize);
|
|
1084
|
+
for (size_t token_index = 0; token_index < source.userIdentityTokensSize;
|
|
1085
|
+
++token_index) {
|
|
1086
|
+
const UA_UserTokenPolicy& token = source.userIdentityTokens[token_index];
|
|
1087
|
+
target.user_identity_tokens.push_back({
|
|
1088
|
+
UaString(token.policyId),
|
|
1089
|
+
UserTokenTypeName(token.tokenType),
|
|
1090
|
+
UaString(token.securityPolicyUri),
|
|
1091
|
+
UaString(token.issuedTokenType),
|
|
1092
|
+
UaString(token.issuerEndpointUrl),
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
endpoints_.push_back(std::move(target));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (endpoints) {
|
|
1099
|
+
UA_Array_delete(endpoints, endpoint_count, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
|
|
1100
|
+
}
|
|
1101
|
+
UA_Client_delete(client);
|
|
1102
|
+
if (!UA_StatusCode_isGood(status)) {
|
|
1103
|
+
SetError("GetEndpoints failed: " + StatusName(status));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
void OnOK() override {
|
|
1108
|
+
Napi::Array output = Napi::Array::New(Env(), endpoints_.size());
|
|
1109
|
+
for (size_t index = 0; index < endpoints_.size(); ++index) {
|
|
1110
|
+
const auto& endpoint = endpoints_[index];
|
|
1111
|
+
Napi::Object value = Napi::Object::New(Env());
|
|
1112
|
+
value.Set("endpointUrl", endpoint.endpoint_url);
|
|
1113
|
+
value.Set("securityMode", endpoint.security_mode);
|
|
1114
|
+
value.Set("securityPolicyUri", endpoint.security_policy_uri);
|
|
1115
|
+
value.Set("securityLevel", endpoint.security_level);
|
|
1116
|
+
value.Set("serverApplicationUri", endpoint.server_application_uri);
|
|
1117
|
+
value.Set("productUri", endpoint.product_uri);
|
|
1118
|
+
Napi::Array tokens = Napi::Array::New(Env(), endpoint.user_identity_tokens.size());
|
|
1119
|
+
for (size_t token_index = 0; token_index < endpoint.user_identity_tokens.size();
|
|
1120
|
+
++token_index) {
|
|
1121
|
+
const auto& token = endpoint.user_identity_tokens[token_index];
|
|
1122
|
+
Napi::Object token_value = Napi::Object::New(Env());
|
|
1123
|
+
token_value.Set("policyId", token.policy_id);
|
|
1124
|
+
token_value.Set("tokenType", token.token_type);
|
|
1125
|
+
token_value.Set("securityPolicyUri", token.security_policy_uri);
|
|
1126
|
+
token_value.Set("issuedTokenType", token.issued_token_type);
|
|
1127
|
+
token_value.Set("issuerEndpointUrl", token.issuer_endpoint_url);
|
|
1128
|
+
tokens.Set(token_index, token_value);
|
|
1129
|
+
}
|
|
1130
|
+
value.Set("userIdentityTokens", tokens);
|
|
1131
|
+
output.Set(index, value);
|
|
1132
|
+
}
|
|
1133
|
+
deferred_.Resolve(output);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private:
|
|
1137
|
+
std::string endpoint_;
|
|
1138
|
+
uint32_t timeout_;
|
|
1139
|
+
std::vector<DiscoveredEndpoint> endpoints_;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
class ClientCommandWorker : public PromiseWorker {
|
|
1143
|
+
public:
|
|
1144
|
+
ClientCommandWorker(Napi::Promise::Deferred deferred,
|
|
1145
|
+
std::shared_ptr<ClientContext> context)
|
|
1146
|
+
: PromiseWorker(deferred), context_(std::move(context)) {}
|
|
1147
|
+
void Execute() override final {
|
|
1148
|
+
const std::string rejection = RunClientCommand(
|
|
1149
|
+
context_, [this] { ExecuteClientCommand(); });
|
|
1150
|
+
if (!rejection.empty()) SetError(rejection);
|
|
1151
|
+
}
|
|
1152
|
+
protected:
|
|
1153
|
+
virtual void ExecuteClientCommand() = 0;
|
|
1154
|
+
std::shared_ptr<ClientContext> context_;
|
|
1155
|
+
};
|
|
1156
|
+
|
|
731
1157
|
struct ConnectionOptions {
|
|
732
1158
|
std::string endpoint;
|
|
733
1159
|
std::string security_mode;
|
|
734
1160
|
std::string security_policy;
|
|
735
1161
|
std::string user;
|
|
736
1162
|
std::string password;
|
|
1163
|
+
std::string certificate;
|
|
1164
|
+
std::string private_key;
|
|
1165
|
+
std::string private_key_password;
|
|
1166
|
+
std::string application_uri;
|
|
1167
|
+
std::vector<std::string> trusted_certificates;
|
|
1168
|
+
std::vector<std::string> revocation_list;
|
|
737
1169
|
uint32_t timeout = 10000;
|
|
738
1170
|
uint32_t session_timeout = 60000;
|
|
739
1171
|
uint32_t connectivity_check_interval = 5000;
|
|
740
1172
|
uint32_t node_id_cache_size = 100000;
|
|
1173
|
+
uint32_t command_queue_size = 256;
|
|
741
1174
|
};
|
|
742
1175
|
|
|
743
1176
|
class ConnectWorker : public PromiseWorker {
|
|
@@ -749,16 +1182,123 @@ class ConnectWorker : public PromiseWorker {
|
|
|
749
1182
|
has_state_callback_(has_state_callback) {}
|
|
750
1183
|
void Execute() override {
|
|
751
1184
|
if (options_.endpoint.empty()) { SetError("OPC UA endpoint is required"); return; }
|
|
752
|
-
|
|
753
|
-
|
|
1185
|
+
const UA_MessageSecurityMode security_mode = SecurityMode(options_.security_mode);
|
|
1186
|
+
const std::string security_policy_uri = SecurityPolicyUri(options_.security_policy);
|
|
1187
|
+
const bool policy_none = security_policy_uri ==
|
|
1188
|
+
"http://opcfoundation.org/UA/SecurityPolicy#None";
|
|
1189
|
+
if (security_mode == UA_MESSAGESECURITYMODE_INVALID) {
|
|
1190
|
+
SetError("Unsupported OPC UA security mode: " + options_.security_mode); return;
|
|
1191
|
+
}
|
|
1192
|
+
if (security_policy_uri.empty()) {
|
|
1193
|
+
SetError("Unsupported OPC UA security policy: " + options_.security_policy); return;
|
|
1194
|
+
}
|
|
1195
|
+
if (security_mode == UA_MESSAGESECURITYMODE_NONE && !policy_none) {
|
|
1196
|
+
SetError("SecurityMode None requires SecurityPolicy None"); return;
|
|
1197
|
+
}
|
|
1198
|
+
if (security_mode != UA_MESSAGESECURITYMODE_NONE) {
|
|
1199
|
+
if (policy_none) {
|
|
1200
|
+
SetError(options_.security_mode + " requires a non-None security policy"); return;
|
|
1201
|
+
}
|
|
1202
|
+
if (options_.certificate.empty() || options_.private_key.empty()) {
|
|
1203
|
+
SetError("Secure OPC UA connections require a client certificate and private key"); return;
|
|
1204
|
+
}
|
|
1205
|
+
if (options_.trusted_certificates.empty()) {
|
|
1206
|
+
SetError("Secure OPC UA connections require at least one trusted server or CA certificate"); return;
|
|
1207
|
+
}
|
|
754
1208
|
}
|
|
755
1209
|
context_ = std::make_shared<ClientContext>();
|
|
1210
|
+
context_->endpoint = options_.endpoint;
|
|
1211
|
+
context_->security_mode = options_.security_mode;
|
|
1212
|
+
context_->security_policy_uri = security_policy_uri;
|
|
1213
|
+
context_->identity_type = options_.user.empty() ? "Anonymous" : "Username";
|
|
1214
|
+
context_->private_key_password = options_.private_key_password;
|
|
756
1215
|
context_->state_callback = state_callback_;
|
|
757
1216
|
context_->has_state_callback.store(has_state_callback_);
|
|
1217
|
+
context_->node_id_cache_capacity = options_.node_id_cache_size;
|
|
1218
|
+
context_->command_queue_capacity = std::max<uint32_t>(1, options_.command_queue_size);
|
|
1219
|
+
context_->command_queue_timeout_ms = std::max<uint32_t>(1000, options_.timeout);
|
|
1220
|
+
context_->running.store(true);
|
|
1221
|
+
context_->iterate_thread = std::thread(Iterate, context_.get());
|
|
1222
|
+
const std::string rejection = RunClientCommand(
|
|
1223
|
+
context_, [this] { ConnectClient(); });
|
|
1224
|
+
if (!rejection.empty()) SetError(rejection);
|
|
1225
|
+
if (!connected_) {
|
|
1226
|
+
context_->running.store(false);
|
|
1227
|
+
context_->command_cv.notify_all();
|
|
1228
|
+
if (context_->iterate_thread.joinable()) context_->iterate_thread.join();
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
client_id_ = next_client_id.fetch_add(1);
|
|
1232
|
+
{
|
|
1233
|
+
std::lock_guard<std::mutex> lock(clients_mutex);
|
|
1234
|
+
clients[client_id_] = context_;
|
|
1235
|
+
}
|
|
1236
|
+
transferred_ = true;
|
|
1237
|
+
}
|
|
1238
|
+
void OnOK() override { deferred_.Resolve(Napi::Number::New(Env(), static_cast<double>(client_id_))); }
|
|
1239
|
+
void OnError(const Napi::Error& error) override {
|
|
1240
|
+
if (has_state_callback_ && !transferred_) state_callback_.Release();
|
|
1241
|
+
PromiseWorker::OnError(error);
|
|
1242
|
+
}
|
|
1243
|
+
private:
|
|
1244
|
+
void ConnectClient() {
|
|
758
1245
|
context_->client = UA_Client_new();
|
|
759
1246
|
if (!context_->client) { SetError("Failed to create open62541 client"); return; }
|
|
760
1247
|
UA_ClientConfig* config = UA_Client_getConfig(context_->client);
|
|
761
|
-
|
|
1248
|
+
const UA_MessageSecurityMode security_mode = SecurityMode(options_.security_mode);
|
|
1249
|
+
const std::string security_policy_uri = SecurityPolicyUri(options_.security_policy);
|
|
1250
|
+
UA_StatusCode config_status = UA_STATUSCODE_GOOD;
|
|
1251
|
+
if (security_mode == UA_MESSAGESECURITYMODE_NONE) {
|
|
1252
|
+
config_status = UA_ClientConfig_setDefault(config);
|
|
1253
|
+
} else {
|
|
1254
|
+
#ifdef UA_ENABLE_ENCRYPTION
|
|
1255
|
+
UA_ByteString certificate;
|
|
1256
|
+
UA_ByteString private_key;
|
|
1257
|
+
std::vector<UA_ByteString> trust_list;
|
|
1258
|
+
std::vector<UA_ByteString> revocation_list;
|
|
1259
|
+
std::string file_error;
|
|
1260
|
+
if (!LoadFile(options_.certificate, &certificate, &file_error)) {
|
|
1261
|
+
SetError(file_error); return;
|
|
1262
|
+
}
|
|
1263
|
+
if (!LoadFile(options_.private_key, &private_key, &file_error)) {
|
|
1264
|
+
UA_ByteString_clear(&certificate);
|
|
1265
|
+
SetError(file_error); return;
|
|
1266
|
+
}
|
|
1267
|
+
if (!LoadFiles(options_.trusted_certificates, &trust_list, &file_error) ||
|
|
1268
|
+
!LoadFiles(options_.revocation_list, &revocation_list, &file_error)) {
|
|
1269
|
+
UA_ByteString_clear(&certificate);
|
|
1270
|
+
UA_ByteString_clear(&private_key);
|
|
1271
|
+
ClearByteStrings(&trust_list);
|
|
1272
|
+
ClearByteStrings(&revocation_list);
|
|
1273
|
+
SetError(file_error); return;
|
|
1274
|
+
}
|
|
1275
|
+
config->clientContext = context_.get();
|
|
1276
|
+
config->privateKeyPasswordCallback = PrivateKeyPasswordCallback;
|
|
1277
|
+
config_status = UA_ClientConfig_setDefaultEncryption(
|
|
1278
|
+
config, certificate, private_key,
|
|
1279
|
+
trust_list.data(), trust_list.size(),
|
|
1280
|
+
revocation_list.data(), revocation_list.size());
|
|
1281
|
+
std::fill(context_->private_key_password.begin(),
|
|
1282
|
+
context_->private_key_password.end(), '\0');
|
|
1283
|
+
context_->private_key_password.clear();
|
|
1284
|
+
UA_ByteString_clear(&certificate);
|
|
1285
|
+
UA_ByteString_clear(&private_key);
|
|
1286
|
+
ClearByteStrings(&trust_list);
|
|
1287
|
+
ClearByteStrings(&revocation_list);
|
|
1288
|
+
#else
|
|
1289
|
+
SetError("The native open62541 addon was built without encryption support"); return;
|
|
1290
|
+
#endif
|
|
1291
|
+
}
|
|
1292
|
+
if (!UA_StatusCode_isGood(config_status)) {
|
|
1293
|
+
SetError("Configure OPC UA security failed: " + StatusName(config_status)); return;
|
|
1294
|
+
}
|
|
1295
|
+
config->securityMode = security_mode;
|
|
1296
|
+
UA_String_clear(&config->securityPolicyUri);
|
|
1297
|
+
config->securityPolicyUri = UA_STRING_ALLOC(security_policy_uri.c_str());
|
|
1298
|
+
if (!options_.application_uri.empty()) {
|
|
1299
|
+
UA_String_clear(&config->clientDescription.applicationUri);
|
|
1300
|
+
config->clientDescription.applicationUri = UA_STRING_ALLOC(options_.application_uri.c_str());
|
|
1301
|
+
}
|
|
762
1302
|
config->timeout = options_.timeout;
|
|
763
1303
|
config->requestedSessionTimeout = options_.session_timeout;
|
|
764
1304
|
config->connectivityCheckInterval = options_.connectivity_check_interval;
|
|
@@ -773,28 +1313,18 @@ class ConnectWorker : public PromiseWorker {
|
|
|
773
1313
|
: UA_Client_connectUsername(context_->client, options_.endpoint.c_str(), options_.user.c_str(), options_.password.c_str());
|
|
774
1314
|
if (!UA_StatusCode_isGood(status)) {
|
|
775
1315
|
SetError("Connect failed: " + StatusName(status));
|
|
776
|
-
|
|
1316
|
+
return;
|
|
777
1317
|
}
|
|
778
|
-
context_->node_id_cache_capacity = options_.node_id_cache_size;
|
|
779
1318
|
ReadOperationLimits(context_.get());
|
|
780
1319
|
context_->refresh_operation_limits.store(false);
|
|
781
|
-
|
|
782
|
-
context_->running.store(true);
|
|
783
|
-
context_->iterate_thread = std::thread(Iterate, context_.get());
|
|
784
|
-
std::lock_guard<std::mutex> lock(clients_mutex);
|
|
785
|
-
clients[client_id_] = context_;
|
|
1320
|
+
connected_ = true;
|
|
786
1321
|
}
|
|
787
|
-
void OnOK() override { deferred_.Resolve(Napi::Number::New(Env(), static_cast<double>(client_id_))); }
|
|
788
|
-
void OnError(const Napi::Error& error) override {
|
|
789
|
-
if (has_state_callback_ && !transferred_) state_callback_.Release();
|
|
790
|
-
PromiseWorker::OnError(error);
|
|
791
|
-
}
|
|
792
|
-
private:
|
|
793
1322
|
ConnectionOptions options_;
|
|
794
1323
|
std::shared_ptr<ClientContext> context_;
|
|
795
1324
|
Napi::ThreadSafeFunction state_callback_;
|
|
796
1325
|
bool has_state_callback_ = false;
|
|
797
1326
|
bool transferred_ = false;
|
|
1327
|
+
bool connected_ = false;
|
|
798
1328
|
uint64_t client_id_ = 0;
|
|
799
1329
|
};
|
|
800
1330
|
|
|
@@ -808,42 +1338,51 @@ class DisconnectWorker : public PromiseWorker {
|
|
|
808
1338
|
uint64_t client_id_;
|
|
809
1339
|
};
|
|
810
1340
|
|
|
811
|
-
|
|
1341
|
+
struct ReadInput {
|
|
1342
|
+
std::string node_id;
|
|
1343
|
+
std::string index_range;
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
class ReadManyWorker : public ClientCommandWorker {
|
|
812
1347
|
public:
|
|
813
1348
|
ReadManyWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
814
|
-
std::vector<
|
|
815
|
-
:
|
|
1349
|
+
std::vector<ReadInput> inputs)
|
|
1350
|
+
: ClientCommandWorker(deferred, std::move(context)), inputs_(std::move(inputs)) {
|
|
816
1351
|
UA_ReadResponse_init(&response_);
|
|
817
1352
|
}
|
|
818
1353
|
~ReadManyWorker() override { UA_ReadResponse_clear(&response_); }
|
|
819
|
-
void
|
|
1354
|
+
void ExecuteClientCommand() override {
|
|
820
1355
|
UA_ReadRequest request;
|
|
821
1356
|
UA_ReadRequest_init(&request);
|
|
822
1357
|
request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
|
|
823
|
-
request.nodesToReadSize =
|
|
824
|
-
request.nodesToRead = static_cast<UA_ReadValueId*>(UA_Array_new(
|
|
1358
|
+
request.nodesToReadSize = inputs_.size();
|
|
1359
|
+
request.nodesToRead = static_cast<UA_ReadValueId*>(UA_Array_new(inputs_.size(), &UA_TYPES[UA_TYPES_READVALUEID]));
|
|
825
1360
|
if (!request.nodesToRead) { SetError("Failed to allocate batch read request"); return; }
|
|
826
|
-
for (size_t index = 0; index <
|
|
827
|
-
if (!CopyCachedNodeId(context_.get(),
|
|
828
|
-
UA_ReadRequest_clear(&request); SetError("Invalid NodeId: " +
|
|
1361
|
+
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
1362
|
+
if (!CopyCachedNodeId(context_.get(), inputs_[index].node_id, &request.nodesToRead[index].nodeId)) {
|
|
1363
|
+
UA_ReadRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index].node_id); return;
|
|
829
1364
|
}
|
|
830
1365
|
request.nodesToRead[index].attributeId = UA_ATTRIBUTEID_VALUE;
|
|
1366
|
+
if (!inputs_[index].index_range.empty()) {
|
|
1367
|
+
request.nodesToRead[index].indexRange = UA_STRING_ALLOC(inputs_[index].index_range.c_str());
|
|
1368
|
+
if (!request.nodesToRead[index].indexRange.data) {
|
|
1369
|
+
UA_ReadRequest_clear(&request); SetError("Failed to allocate IndexRange"); return;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
831
1372
|
}
|
|
832
|
-
{
|
|
833
|
-
|
|
834
|
-
if (!context_->client) { UA_ReadRequest_clear(&request); SetError("Client disconnected"); return; }
|
|
835
|
-
response_ = UA_Client_Service_read(context_->client, request);
|
|
836
|
-
}
|
|
1373
|
+
if (!context_->client) { UA_ReadRequest_clear(&request); SetError("Client disconnected"); return; }
|
|
1374
|
+
response_ = UA_Client_Service_read(context_->client, request);
|
|
837
1375
|
UA_ReadRequest_clear(&request);
|
|
838
1376
|
if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) {
|
|
839
1377
|
SetError("Batch read failed: " + StatusName(response_.responseHeader.serviceResult));
|
|
840
1378
|
}
|
|
841
1379
|
}
|
|
842
1380
|
void OnOK() override {
|
|
843
|
-
Napi::Array output = Napi::Array::New(Env(),
|
|
844
|
-
for (size_t index = 0; index <
|
|
1381
|
+
Napi::Array output = Napi::Array::New(Env(), inputs_.size());
|
|
1382
|
+
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
845
1383
|
Napi::Object item = Napi::Object::New(Env());
|
|
846
|
-
item.Set("nodeId",
|
|
1384
|
+
item.Set("nodeId", inputs_[index].node_id);
|
|
1385
|
+
if (!inputs_[index].index_range.empty()) item.Set("indexRange", inputs_[index].index_range);
|
|
847
1386
|
if (index < response_.resultsSize) {
|
|
848
1387
|
const UA_DataValue& value = response_.results[index];
|
|
849
1388
|
item.Set("statusCode", StatusName(value.status));
|
|
@@ -856,28 +1395,28 @@ class ReadManyWorker : public PromiseWorker {
|
|
|
856
1395
|
deferred_.Resolve(output);
|
|
857
1396
|
}
|
|
858
1397
|
private:
|
|
859
|
-
std::
|
|
860
|
-
std::vector<std::string> node_ids_;
|
|
1398
|
+
std::vector<ReadInput> inputs_;
|
|
861
1399
|
UA_ReadResponse response_;
|
|
862
1400
|
};
|
|
863
1401
|
|
|
864
1402
|
struct WriteInput {
|
|
865
1403
|
std::string node_id;
|
|
866
1404
|
std::string data_type;
|
|
1405
|
+
std::string index_range;
|
|
867
1406
|
UA_Variant value;
|
|
868
1407
|
WriteInput() { UA_Variant_init(&value); }
|
|
869
1408
|
~WriteInput() { UA_Variant_clear(&value); }
|
|
870
1409
|
};
|
|
871
1410
|
|
|
872
|
-
class WriteManyWorker : public
|
|
1411
|
+
class WriteManyWorker : public ClientCommandWorker {
|
|
873
1412
|
public:
|
|
874
1413
|
WriteManyWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
875
1414
|
std::vector<std::unique_ptr<WriteInput>> inputs)
|
|
876
|
-
:
|
|
1415
|
+
: ClientCommandWorker(deferred, std::move(context)), inputs_(std::move(inputs)) {
|
|
877
1416
|
UA_WriteResponse_init(&response_);
|
|
878
1417
|
}
|
|
879
1418
|
~WriteManyWorker() override { UA_WriteResponse_clear(&response_); }
|
|
880
|
-
void
|
|
1419
|
+
void ExecuteClientCommand() override {
|
|
881
1420
|
UA_WriteRequest request;
|
|
882
1421
|
UA_WriteRequest_init(&request);
|
|
883
1422
|
request.nodesToWriteSize = inputs_.size();
|
|
@@ -889,14 +1428,17 @@ class WriteManyWorker : public PromiseWorker {
|
|
|
889
1428
|
UA_WriteRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index]->node_id); return;
|
|
890
1429
|
}
|
|
891
1430
|
target.attributeId = UA_ATTRIBUTEID_VALUE;
|
|
1431
|
+
if (!inputs_[index]->index_range.empty()) {
|
|
1432
|
+
target.indexRange = UA_STRING_ALLOC(inputs_[index]->index_range.c_str());
|
|
1433
|
+
if (!target.indexRange.data) {
|
|
1434
|
+
UA_WriteRequest_clear(&request); SetError("Failed to allocate IndexRange"); return;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
892
1437
|
target.value.hasValue = true;
|
|
893
1438
|
UA_Variant_copy(&inputs_[index]->value, &target.value.value);
|
|
894
1439
|
}
|
|
895
|
-
{
|
|
896
|
-
|
|
897
|
-
if (!context_->client) { UA_WriteRequest_clear(&request); SetError("Client disconnected"); return; }
|
|
898
|
-
response_ = UA_Client_Service_write(context_->client, request);
|
|
899
|
-
}
|
|
1440
|
+
if (!context_->client) { UA_WriteRequest_clear(&request); SetError("Client disconnected"); return; }
|
|
1441
|
+
response_ = UA_Client_Service_write(context_->client, request);
|
|
900
1442
|
UA_WriteRequest_clear(&request);
|
|
901
1443
|
if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) {
|
|
902
1444
|
SetError("Batch write failed: " + StatusName(response_.responseHeader.serviceResult));
|
|
@@ -907,13 +1449,13 @@ class WriteManyWorker : public PromiseWorker {
|
|
|
907
1449
|
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
908
1450
|
Napi::Object item = Napi::Object::New(Env());
|
|
909
1451
|
item.Set("nodeId", inputs_[index]->node_id);
|
|
1452
|
+
if (!inputs_[index]->index_range.empty()) item.Set("indexRange", inputs_[index]->index_range);
|
|
910
1453
|
item.Set("statusCode", index < response_.resultsSize ? StatusName(response_.results[index]) : "BadUnexpectedError");
|
|
911
1454
|
output.Set(index, item);
|
|
912
1455
|
}
|
|
913
1456
|
deferred_.Resolve(output);
|
|
914
1457
|
}
|
|
915
1458
|
private:
|
|
916
|
-
std::shared_ptr<ClientContext> context_;
|
|
917
1459
|
std::vector<std::unique_ptr<WriteInput>> inputs_;
|
|
918
1460
|
UA_WriteResponse response_;
|
|
919
1461
|
};
|
|
@@ -943,17 +1485,16 @@ struct BrowseOptions {
|
|
|
943
1485
|
std::set<uint16_t> namespaces;
|
|
944
1486
|
};
|
|
945
1487
|
|
|
946
|
-
class BrowseWorker : public
|
|
1488
|
+
class BrowseWorker : public ClientCommandWorker {
|
|
947
1489
|
public:
|
|
948
1490
|
BrowseWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
949
1491
|
BrowseOptions options)
|
|
950
|
-
:
|
|
951
|
-
void
|
|
1492
|
+
: ClientCommandWorker(deferred, std::move(context)), options_(std::move(options)) {}
|
|
1493
|
+
void ExecuteClientCommand() override {
|
|
952
1494
|
UA_NodeId root;
|
|
953
1495
|
UA_NodeId_init(&root);
|
|
954
1496
|
if (!CopyCachedNodeId(context_.get(), options_.root_node_id, &root)) { SetError("Invalid root NodeId: " + options_.root_node_id); return; }
|
|
955
1497
|
UA_NodeId_clear(&root);
|
|
956
|
-
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
957
1498
|
if (!context_->client) { SetError("Client disconnected"); return; }
|
|
958
1499
|
if (!BrowseTree()) return;
|
|
959
1500
|
ReadAttributes();
|
|
@@ -1056,7 +1597,7 @@ class BrowseWorker : public PromiseWorker {
|
|
|
1056
1597
|
void ReadAttributes() {
|
|
1057
1598
|
const size_t attrs = 5;
|
|
1058
1599
|
size_t batch = std::max<size_t>(1, options_.attribute_batch_size) * attrs;
|
|
1059
|
-
uint32_t read_limit = context_->operation_limits.max_nodes_per_read;
|
|
1600
|
+
uint32_t read_limit = context_->operation_limits.max_nodes_per_read.load();
|
|
1060
1601
|
if (read_limit > 0) batch = std::min<size_t>(batch, read_limit);
|
|
1061
1602
|
const size_t total_attributes = points_.size() * attrs;
|
|
1062
1603
|
const UA_UInt32 ids[attrs] = {
|
|
@@ -1104,19 +1645,18 @@ class BrowseWorker : public PromiseWorker {
|
|
|
1104
1645
|
}
|
|
1105
1646
|
}
|
|
1106
1647
|
|
|
1107
|
-
std::shared_ptr<ClientContext> context_;
|
|
1108
1648
|
BrowseOptions options_;
|
|
1109
1649
|
std::vector<BrowsePoint> points_;
|
|
1110
1650
|
};
|
|
1111
1651
|
|
|
1112
|
-
class CreateSubscriptionWorker : public
|
|
1652
|
+
class CreateSubscriptionWorker : public ClientCommandWorker {
|
|
1113
1653
|
public:
|
|
1114
1654
|
CreateSubscriptionWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
1115
1655
|
Napi::Function callback, uint32_t queue_size, double publishing_interval,
|
|
1116
1656
|
uint32_t lifetime, uint32_t keepalive, uint32_t max_notifications,
|
|
1117
1657
|
uint32_t callback_batch_size, std::string queue_mode,
|
|
1118
1658
|
std::string overflow_policy)
|
|
1119
|
-
:
|
|
1659
|
+
: ClientCommandWorker(deferred, std::move(context)), publishing_interval_(publishing_interval),
|
|
1120
1660
|
lifetime_(lifetime), keepalive_(keepalive), max_notifications_(max_notifications),
|
|
1121
1661
|
queue_size_(std::max<uint32_t>(1, queue_size)),
|
|
1122
1662
|
callback_batch_size_(std::max<uint32_t>(1, callback_batch_size)),
|
|
@@ -1124,13 +1664,12 @@ class CreateSubscriptionWorker : public PromiseWorker {
|
|
|
1124
1664
|
overflow_policy_(overflow_policy == "drop-newest" ? "drop-newest" : "drop-oldest") {
|
|
1125
1665
|
callback_ = Napi::ThreadSafeFunction::New(Env(), callback, "OpcUaDataChangeBatches", 1, 1);
|
|
1126
1666
|
}
|
|
1127
|
-
void
|
|
1667
|
+
void ExecuteClientCommand() override {
|
|
1128
1668
|
UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default();
|
|
1129
1669
|
request.requestedPublishingInterval = publishing_interval_;
|
|
1130
1670
|
request.requestedLifetimeCount = lifetime_;
|
|
1131
1671
|
request.requestedMaxKeepAliveCount = keepalive_;
|
|
1132
1672
|
request.maxNotificationsPerPublish = max_notifications_;
|
|
1133
|
-
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
1134
1673
|
if (!context_->client) { SetError("Client disconnected"); return; }
|
|
1135
1674
|
UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(context_->client, request, nullptr, nullptr, nullptr);
|
|
1136
1675
|
status_ = response.responseHeader.serviceResult; subscription_id_ = response.subscriptionId;
|
|
@@ -1149,27 +1688,28 @@ class CreateSubscriptionWorker : public PromiseWorker {
|
|
|
1149
1688
|
void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
|
|
1150
1689
|
void OnError(const Napi::Error& error) override { if (!transferred_) callback_.Release(); PromiseWorker::OnError(error); }
|
|
1151
1690
|
private:
|
|
1152
|
-
|
|
1691
|
+
Napi::ThreadSafeFunction callback_; bool transferred_ = false;
|
|
1153
1692
|
double publishing_interval_; uint32_t lifetime_; uint32_t keepalive_; uint32_t max_notifications_;
|
|
1154
1693
|
uint32_t queue_size_; uint32_t callback_batch_size_; std::string queue_mode_; std::string overflow_policy_;
|
|
1155
1694
|
uint32_t subscription_id_ = 0; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
|
|
1156
1695
|
};
|
|
1157
1696
|
|
|
1158
1697
|
struct MonitorInput { std::string node_id; double sampling_interval; uint32_t queue_size; bool discard_oldest; std::string deadband_type; double deadband_value; };
|
|
1159
|
-
struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
|
|
1698
|
+
struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; uint32_t client_handle = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
|
|
1160
1699
|
|
|
1161
|
-
class AddMonitoredWorker : public
|
|
1700
|
+
class AddMonitoredWorker : public ClientCommandWorker {
|
|
1162
1701
|
public:
|
|
1163
1702
|
AddMonitoredWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
1164
|
-
uint32_t subscription_id, std::vector<MonitorInput> inputs
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1703
|
+
uint32_t subscription_id, std::vector<MonitorInput> inputs,
|
|
1704
|
+
UA_TimestampsToReturn timestamps_to_return)
|
|
1705
|
+
: ClientCommandWorker(deferred, std::move(context)), subscription_id_(subscription_id),
|
|
1706
|
+
inputs_(std::move(inputs)), timestamps_to_return_(timestamps_to_return) {}
|
|
1707
|
+
void ExecuteClientCommand() override {
|
|
1168
1708
|
auto subscription_it = context_->subscriptions.find(subscription_id_);
|
|
1169
1709
|
if (!context_->client || subscription_it == context_->subscriptions.end()) { SetError("Subscription not found"); return; }
|
|
1170
1710
|
auto* subscription = subscription_it->second.get();
|
|
1171
1711
|
UA_CreateMonitoredItemsRequest request; UA_CreateMonitoredItemsRequest_init(&request);
|
|
1172
|
-
request.subscriptionId = subscription_id_; request.timestampsToReturn =
|
|
1712
|
+
request.subscriptionId = subscription_id_; request.timestampsToReturn = timestamps_to_return_;
|
|
1173
1713
|
request.itemsToCreateSize = inputs_.size();
|
|
1174
1714
|
request.itemsToCreate = static_cast<UA_MonitoredItemCreateRequest*>(UA_Array_new(inputs_.size(), &UA_TYPES[UA_TYPES_MONITOREDITEMCREATEREQUEST]));
|
|
1175
1715
|
std::vector<std::unique_ptr<MonitoredContext>> contexts(inputs_.size());
|
|
@@ -1180,7 +1720,13 @@ class AddMonitoredWorker : public PromiseWorker {
|
|
|
1180
1720
|
auto& item = request.itemsToCreate[index];
|
|
1181
1721
|
if (!CopyCachedNodeId(context_.get(), inputs_[index].node_id, &item.itemToMonitor.nodeId)) { UA_CreateMonitoredItemsRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index].node_id); return; }
|
|
1182
1722
|
item.itemToMonitor.attributeId = UA_ATTRIBUTEID_VALUE; item.monitoringMode = UA_MONITORINGMODE_REPORTING;
|
|
1183
|
-
|
|
1723
|
+
uint32_t client_handle = 0;
|
|
1724
|
+
do {
|
|
1725
|
+
client_handle = subscription->next_client_handle++;
|
|
1726
|
+
} while (client_handle == 0 ||
|
|
1727
|
+
subscription->monitored_by_client_handle.find(client_handle) !=
|
|
1728
|
+
subscription->monitored_by_client_handle.end());
|
|
1729
|
+
item.requestedParameters.clientHandle = client_handle;
|
|
1184
1730
|
item.requestedParameters.samplingInterval = inputs_[index].sampling_interval;
|
|
1185
1731
|
item.requestedParameters.queueSize = inputs_[index].queue_size;
|
|
1186
1732
|
item.requestedParameters.discardOldest = inputs_[index].discard_oldest;
|
|
@@ -1193,7 +1739,7 @@ class AddMonitoredWorker : public PromiseWorker {
|
|
|
1193
1739
|
item.requestedParameters.filter.content.decoded.type = &UA_TYPES[UA_TYPES_DATACHANGEFILTER];
|
|
1194
1740
|
item.requestedParameters.filter.content.decoded.data = filters[index].get();
|
|
1195
1741
|
}
|
|
1196
|
-
contexts[index] = std::make_unique<MonitoredContext>(); contexts[index]->notifications = subscription->notifications; contexts[index]->node_id = inputs_[index].node_id; context_ptrs[index] = contexts[index].get();
|
|
1742
|
+
contexts[index] = std::make_unique<MonitoredContext>(); contexts[index]->notifications = subscription->notifications; contexts[index]->node_id = inputs_[index].node_id; contexts[index]->client_handle = client_handle; context_ptrs[index] = contexts[index].get();
|
|
1197
1743
|
}
|
|
1198
1744
|
UA_CreateMonitoredItemsResponse response = UA_Client_MonitoredItems_createDataChanges(context_->client, request, context_ptrs.data(), callbacks.data(), nullptr);
|
|
1199
1745
|
for (size_t index = 0; index < filters.size(); ++index) {
|
|
@@ -1202,46 +1748,46 @@ class AddMonitoredWorker : public PromiseWorker {
|
|
|
1202
1748
|
UA_CreateMonitoredItemsRequest_clear(&request);
|
|
1203
1749
|
if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) { UA_CreateMonitoredItemsResponse_clear(&response); SetError("Create monitored items failed: " + StatusName(response.responseHeader.serviceResult)); return; }
|
|
1204
1750
|
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
1205
|
-
MonitorResult result; result.node_id = inputs_[index].node_id;
|
|
1751
|
+
MonitorResult result; result.node_id = inputs_[index].node_id; result.client_handle = contexts[index]->client_handle;
|
|
1206
1752
|
if (index < response.resultsSize) {
|
|
1207
1753
|
result.status = response.results[index].statusCode; result.monitored_item_id = response.results[index].monitoredItemId;
|
|
1208
|
-
if (UA_StatusCode_isGood(result.status)) { contexts[index]->monitored_item_id = result.monitored_item_id; subscription->monitored[result.monitored_item_id] = std::move(contexts[index]); }
|
|
1754
|
+
if (UA_StatusCode_isGood(result.status)) { contexts[index]->monitored_item_id = result.monitored_item_id; auto* monitored_context = contexts[index].get(); subscription->monitored_by_client_handle[monitored_context->client_handle] = monitored_context; subscription->monitored[result.monitored_item_id] = std::move(contexts[index]); }
|
|
1209
1755
|
}
|
|
1210
1756
|
results_.push_back(result);
|
|
1211
1757
|
}
|
|
1212
1758
|
UA_CreateMonitoredItemsResponse_clear(&response);
|
|
1213
1759
|
}
|
|
1214
|
-
void OnOK() override { Napi::Array output = Napi::Array::New(Env(), results_.size()); for (size_t index = 0; index < results_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("nodeId", results_[index].node_id); item.Set("monitoredItemId", results_[index].monitored_item_id); item.Set("statusCode", StatusName(results_[index].status)); output.Set(index, item); } deferred_.Resolve(output); }
|
|
1760
|
+
void OnOK() override { Napi::Array output = Napi::Array::New(Env(), results_.size()); for (size_t index = 0; index < results_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("nodeId", results_[index].node_id); item.Set("monitoredItemId", results_[index].monitored_item_id); item.Set("clientHandle", results_[index].client_handle); item.Set("statusCode", StatusName(results_[index].status)); output.Set(index, item); } deferred_.Resolve(output); }
|
|
1215
1761
|
private:
|
|
1216
|
-
|
|
1762
|
+
uint32_t subscription_id_; std::vector<MonitorInput> inputs_; std::vector<MonitorResult> results_;
|
|
1763
|
+
UA_TimestampsToReturn timestamps_to_return_;
|
|
1217
1764
|
};
|
|
1218
1765
|
|
|
1219
|
-
class RemoveMonitoredWorker : public
|
|
1766
|
+
class RemoveMonitoredWorker : public ClientCommandWorker {
|
|
1220
1767
|
public:
|
|
1221
1768
|
RemoveMonitoredWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
1222
1769
|
uint32_t subscription_id, std::vector<uint32_t> ids)
|
|
1223
|
-
:
|
|
1770
|
+
: ClientCommandWorker(deferred, std::move(context)), subscription_id_(subscription_id), ids_(std::move(ids)) { UA_DeleteMonitoredItemsResponse_init(&response_); }
|
|
1224
1771
|
~RemoveMonitoredWorker() override { UA_DeleteMonitoredItemsResponse_clear(&response_); }
|
|
1225
|
-
void
|
|
1226
|
-
|
|
1772
|
+
void ExecuteClientCommand() override {
|
|
1773
|
+
auto sub = context_->subscriptions.find(subscription_id_);
|
|
1227
1774
|
if (!context_->client || sub == context_->subscriptions.end()) { SetError("Subscription not found"); return; }
|
|
1228
1775
|
UA_DeleteMonitoredItemsRequest request; UA_DeleteMonitoredItemsRequest_init(&request); request.subscriptionId = subscription_id_; request.monitoredItemIdsSize = ids_.size(); request.monitoredItemIds = static_cast<UA_UInt32*>(UA_Array_new(ids_.size(), &UA_TYPES[UA_TYPES_UINT32]));
|
|
1229
1776
|
std::copy(ids_.begin(), ids_.end(), request.monitoredItemIds); response_ = UA_Client_MonitoredItems_delete(context_->client, request); UA_DeleteMonitoredItemsRequest_clear(&request);
|
|
1230
1777
|
if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) { SetError("Delete monitored items failed: " + StatusName(response_.responseHeader.serviceResult)); return; }
|
|
1231
|
-
for (size_t index = 0; index < ids_.size() && index < response_.resultsSize; ++index) if (UA_StatusCode_isGood(response_.results[index])) sub->second->monitored.
|
|
1778
|
+
for (size_t index = 0; index < ids_.size() && index < response_.resultsSize; ++index) if (UA_StatusCode_isGood(response_.results[index])) { auto monitored = sub->second->monitored.find(ids_[index]); if (monitored != sub->second->monitored.end()) { sub->second->monitored_by_client_handle.erase(monitored->second->client_handle); sub->second->monitored.erase(monitored); } }
|
|
1232
1779
|
}
|
|
1233
1780
|
void OnOK() override { Napi::Array output = Napi::Array::New(Env(), ids_.size()); for (size_t index = 0; index < ids_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("monitoredItemId", ids_[index]); item.Set("statusCode", index < response_.resultsSize ? StatusName(response_.results[index]) : "BadUnexpectedError"); output.Set(index, item); } deferred_.Resolve(output); }
|
|
1234
1781
|
private:
|
|
1235
|
-
|
|
1782
|
+
uint32_t subscription_id_; std::vector<uint32_t> ids_; UA_DeleteMonitoredItemsResponse response_;
|
|
1236
1783
|
};
|
|
1237
1784
|
|
|
1238
|
-
class ValidateSubscriptionWorker : public
|
|
1785
|
+
class ValidateSubscriptionWorker : public ClientCommandWorker {
|
|
1239
1786
|
public:
|
|
1240
1787
|
ValidateSubscriptionWorker(Napi::Promise::Deferred deferred,
|
|
1241
1788
|
std::shared_ptr<ClientContext> context, uint32_t id)
|
|
1242
|
-
:
|
|
1243
|
-
void
|
|
1244
|
-
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
1789
|
+
: ClientCommandWorker(deferred, std::move(context)), id_(id) {}
|
|
1790
|
+
void ExecuteClientCommand() override {
|
|
1245
1791
|
if (!context_->client || !context_->connected.load()) {
|
|
1246
1792
|
SetError("Client reconnecting");
|
|
1247
1793
|
return;
|
|
@@ -1281,19 +1827,18 @@ class ValidateSubscriptionWorker : public PromiseWorker {
|
|
|
1281
1827
|
deferred_.Resolve(output);
|
|
1282
1828
|
}
|
|
1283
1829
|
private:
|
|
1284
|
-
std::shared_ptr<ClientContext> context_;
|
|
1285
1830
|
uint32_t id_;
|
|
1286
1831
|
UA_StatusCode status_ = UA_STATUSCODE_GOOD;
|
|
1287
1832
|
};
|
|
1288
1833
|
|
|
1289
|
-
class DeleteSubscriptionWorker : public
|
|
1834
|
+
class DeleteSubscriptionWorker : public ClientCommandWorker {
|
|
1290
1835
|
public:
|
|
1291
1836
|
DeleteSubscriptionWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context, uint32_t id)
|
|
1292
|
-
:
|
|
1293
|
-
void
|
|
1837
|
+
: ClientCommandWorker(deferred, std::move(context)), id_(id) {}
|
|
1838
|
+
void ExecuteClientCommand() override { auto found = context_->subscriptions.find(id_); if (found == context_->subscriptions.end()) return; status_ = context_->client ? UA_Client_Subscriptions_deleteSingle(context_->client, id_) : UA_STATUSCODE_BADCONNECTIONCLOSED; CloseNotificationQueue(found->second->notifications); context_->subscriptions.erase(found); if (!UA_StatusCode_isGood(status_)) SetError("Delete subscription failed: " + StatusName(status_)); }
|
|
1294
1839
|
void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
|
|
1295
1840
|
private:
|
|
1296
|
-
|
|
1841
|
+
uint32_t id_; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
|
|
1297
1842
|
};
|
|
1298
1843
|
|
|
1299
1844
|
std::shared_ptr<ClientContext> RequireClient(Napi::Env env, const Napi::Value& value) {
|
|
@@ -1309,10 +1854,11 @@ Napi::Value GetClientDiagnostics(const Napi::CallbackInfo& info) {
|
|
|
1309
1854
|
if (!context) return env.Undefined();
|
|
1310
1855
|
Napi::Object result = Napi::Object::New(env);
|
|
1311
1856
|
Napi::Object limits = Napi::Object::New(env);
|
|
1312
|
-
limits.Set("maxNodesPerRead", context->operation_limits.max_nodes_per_read);
|
|
1313
|
-
limits.Set("maxNodesPerWrite", context->operation_limits.max_nodes_per_write);
|
|
1314
|
-
limits.Set("maxNodesPerBrowse", context->operation_limits.max_nodes_per_browse);
|
|
1315
|
-
limits.Set("maxMonitoredItemsPerCall", context->operation_limits.max_monitored_items_per_call);
|
|
1857
|
+
limits.Set("maxNodesPerRead", context->operation_limits.max_nodes_per_read.load());
|
|
1858
|
+
limits.Set("maxNodesPerWrite", context->operation_limits.max_nodes_per_write.load());
|
|
1859
|
+
limits.Set("maxNodesPerBrowse", context->operation_limits.max_nodes_per_browse.load());
|
|
1860
|
+
limits.Set("maxMonitoredItemsPerCall", context->operation_limits.max_monitored_items_per_call.load());
|
|
1861
|
+
limits.Set("maxMonitoredItemsPerSubscription", context->operation_limits.max_monitored_items_per_subscription.load());
|
|
1316
1862
|
result.Set("operationLimits", limits);
|
|
1317
1863
|
|
|
1318
1864
|
Napi::Object connection = Napi::Object::New(env);
|
|
@@ -1326,8 +1872,30 @@ Napi::Value GetClientDiagnostics(const Napi::CallbackInfo& info) {
|
|
|
1326
1872
|
connection.Set("reconnectCount", Napi::Number::New(env, static_cast<double>(context->reconnect_count.load())));
|
|
1327
1873
|
connection.Set("disconnectCount", Napi::Number::New(env, static_cast<double>(context->disconnect_count.load())));
|
|
1328
1874
|
connection.Set("sessionGeneration", Napi::Number::New(env, static_cast<double>(context->session_generation.load())));
|
|
1875
|
+
connection.Set("endpoint", context->endpoint);
|
|
1876
|
+
connection.Set("securityMode", context->security_mode);
|
|
1877
|
+
connection.Set("securityPolicyUri", context->security_policy_uri);
|
|
1878
|
+
connection.Set("identityType", context->identity_type);
|
|
1329
1879
|
result.Set("connection", connection);
|
|
1330
1880
|
|
|
1881
|
+
size_t command_queue_depth = 0;
|
|
1882
|
+
size_t command_queue_max_depth = 0;
|
|
1883
|
+
{
|
|
1884
|
+
std::lock_guard<std::mutex> lock(context->command_mutex);
|
|
1885
|
+
command_queue_depth = context->commands.size();
|
|
1886
|
+
command_queue_max_depth = context->command_queue_max_depth;
|
|
1887
|
+
}
|
|
1888
|
+
Napi::Object command_queue = Napi::Object::New(env);
|
|
1889
|
+
command_queue.Set("depth", Napi::Number::New(env, static_cast<double>(command_queue_depth)));
|
|
1890
|
+
command_queue.Set("capacity", Napi::Number::New(env, static_cast<double>(context->command_queue_capacity)));
|
|
1891
|
+
command_queue.Set("maxDepth", Napi::Number::New(env, static_cast<double>(command_queue_max_depth)));
|
|
1892
|
+
command_queue.Set("active", context->commands_active.load());
|
|
1893
|
+
command_queue.Set("enqueued", Napi::Number::New(env, static_cast<double>(context->commands_enqueued.load())));
|
|
1894
|
+
command_queue.Set("completed", Napi::Number::New(env, static_cast<double>(context->commands_completed.load())));
|
|
1895
|
+
command_queue.Set("rejected", Napi::Number::New(env, static_cast<double>(context->commands_rejected.load())));
|
|
1896
|
+
command_queue.Set("timedOut", Napi::Number::New(env, static_cast<double>(context->commands_timed_out.load())));
|
|
1897
|
+
result.Set("commandQueue", command_queue);
|
|
1898
|
+
|
|
1331
1899
|
size_t cache_size = 0;
|
|
1332
1900
|
{
|
|
1333
1901
|
std::lock_guard<std::mutex> lock(context->node_id_cache_mutex);
|
|
@@ -1347,7 +1915,9 @@ Napi::Value GetClientDiagnostics(const Napi::CallbackInfo& info) {
|
|
|
1347
1915
|
Napi::Value ConnectAsync(const Napi::CallbackInfo& info) {
|
|
1348
1916
|
Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsObject()) { Napi::TypeError::New(env, "Connection options are required").ThrowAsJavaScriptException(); return env.Undefined(); }
|
|
1349
1917
|
Napi::Object object = info[0].As<Napi::Object>(); ConnectionOptions options;
|
|
1350
|
-
options.endpoint = GetString(object, "endpoint"); options.security_mode = GetString(object, "securityMode", "None"); options.security_policy = GetString(object, "securityPolicy", "None"); options.user = GetString(object, "user"); options.password = GetString(object, "password"); options.timeout = GetUInt(object, "requestTimeout", 10000); options.session_timeout = GetUInt(object, "requestedSessionTimeout", 60000); options.connectivity_check_interval = GetUInt(object, "connectivityCheckInterval", 5000); options.node_id_cache_size = GetUInt(object, "nodeIdCacheSize", 100000);
|
|
1918
|
+
options.endpoint = GetString(object, "endpoint"); options.security_mode = GetString(object, "securityMode", "None"); options.security_policy = GetString(object, "securityPolicy", "None"); options.user = GetString(object, "user"); options.password = GetString(object, "password"); options.certificate = GetString(object, "certificate"); options.private_key = GetString(object, "privateKey"); options.private_key_password = GetString(object, "privateKeyPassword"); options.application_uri = GetString(object, "applicationUri"); options.timeout = GetUInt(object, "requestTimeout", 10000); options.session_timeout = GetUInt(object, "requestedSessionTimeout", 60000); options.connectivity_check_interval = GetUInt(object, "connectivityCheckInterval", 5000); options.node_id_cache_size = GetUInt(object, "nodeIdCacheSize", 100000); options.command_queue_size = GetUInt(object, "commandQueueSize", 256);
|
|
1919
|
+
if (object.Has("trustedCertificates") && object.Get("trustedCertificates").IsArray()) { Napi::Array values = object.Get("trustedCertificates").As<Napi::Array>(); for (uint32_t index = 0; index < values.Length(); ++index) if (values.Get(index).IsString()) options.trusted_certificates.push_back(values.Get(index).As<Napi::String>().Utf8Value()); }
|
|
1920
|
+
if (object.Has("revocationList") && object.Get("revocationList").IsArray()) { Napi::Array values = object.Get("revocationList").As<Napi::Array>(); for (uint32_t index = 0; index < values.Length(); ++index) if (values.Get(index).IsString()) options.revocation_list.push_back(values.Get(index).As<Napi::String>().Utf8Value()); }
|
|
1351
1921
|
bool has_state_callback = info.Length() > 1 && info[1].IsFunction();
|
|
1352
1922
|
Napi::ThreadSafeFunction state_callback;
|
|
1353
1923
|
if (has_state_callback) {
|
|
@@ -1357,17 +1927,30 @@ Napi::Value ConnectAsync(const Napi::CallbackInfo& info) {
|
|
|
1357
1927
|
auto deferred = Napi::Promise::Deferred::New(env); (new ConnectWorker(deferred, std::move(options), std::move(state_callback), has_state_callback))->Queue(); return deferred.Promise();
|
|
1358
1928
|
}
|
|
1359
1929
|
|
|
1930
|
+
Napi::Value DiscoverEndpointsAsync(const Napi::CallbackInfo& info) {
|
|
1931
|
+
Napi::Env env = info.Env();
|
|
1932
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
1933
|
+
Napi::TypeError::New(env, "Discovery options are required").ThrowAsJavaScriptException();
|
|
1934
|
+
return env.Undefined();
|
|
1935
|
+
}
|
|
1936
|
+
Napi::Object options = info[0].As<Napi::Object>();
|
|
1937
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
1938
|
+
(new DiscoverEndpointsWorker(
|
|
1939
|
+
deferred, GetString(options, "endpoint"), GetUInt(options, "requestTimeout", 10000)))->Queue();
|
|
1940
|
+
return deferred.Promise();
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1360
1943
|
Napi::Value DisconnectAsync(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); uint64_t id = info[0].As<Napi::Number>().Int64Value(); auto deferred = Napi::Promise::Deferred::New(env); (new DisconnectWorker(deferred, id))->Queue(); return deferred.Promise(); }
|
|
1361
1944
|
|
|
1362
1945
|
Napi::Value ReadManyAsync(const Napi::CallbackInfo& info) {
|
|
1363
|
-
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsArray()) return env.Undefined(); Napi::Array array = info[1].As<Napi::Array>(); std::vector<
|
|
1364
|
-
for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Value value = array.Get(index);
|
|
1365
|
-
auto deferred = Napi::Promise::Deferred::New(env); (new ReadManyWorker(deferred, context, std::move(
|
|
1946
|
+
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsArray()) return env.Undefined(); Napi::Array array = info[1].As<Napi::Array>(); std::vector<ReadInput> inputs; inputs.reserve(array.Length());
|
|
1947
|
+
for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Value value = array.Get(index); ReadInput input; if (value.IsString()) input.node_id = value.As<Napi::String>().Utf8Value(); else { Napi::Object object = value.As<Napi::Object>(); input.node_id = GetString(object, "nodeId"); input.index_range = GetString(object, "indexRange"); } inputs.push_back(std::move(input)); }
|
|
1948
|
+
auto deferred = Napi::Promise::Deferred::New(env); (new ReadManyWorker(deferred, context, std::move(inputs)))->Queue(); return deferred.Promise();
|
|
1366
1949
|
}
|
|
1367
1950
|
|
|
1368
1951
|
Napi::Value WriteManyAsync(const Napi::CallbackInfo& info) {
|
|
1369
1952
|
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsArray()) return env.Undefined(); Napi::Array array = info[1].As<Napi::Array>(); std::vector<std::unique_ptr<WriteInput>> inputs;
|
|
1370
|
-
for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Object object = array.Get(index).As<Napi::Object>(); auto input = std::make_unique<WriteInput>(); input->node_id = GetString(object, "nodeId"); input->data_type = GetString(object, "dataType", "String"); if (!object.Has("value")) { Napi::TypeError::New(env, "Write point is missing value").ThrowAsJavaScriptException(); return env.Undefined(); } if (!
|
|
1953
|
+
for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Object object = array.Get(index).As<Napi::Object>(); auto input = std::make_unique<WriteInput>(); input->node_id = GetString(object, "nodeId"); input->data_type = GetString(object, "dataType", "String"); input->index_range = GetString(object, "indexRange"); if (!object.Has("value")) { Napi::TypeError::New(env, "Write point is missing value").ThrowAsJavaScriptException(); return env.Undefined(); } if (!JsValueToVariant(env, object.Get("value"), input->data_type, &input->value)) return env.Undefined(); inputs.push_back(std::move(input)); }
|
|
1371
1954
|
auto deferred = Napi::Promise::Deferred::New(env); (new WriteManyWorker(deferred, context, std::move(inputs)))->Queue(); return deferred.Promise();
|
|
1372
1955
|
}
|
|
1373
1956
|
|
|
@@ -1386,7 +1969,11 @@ Napi::Value CreateSubscriptionAsync(const Napi::CallbackInfo& info) {
|
|
|
1386
1969
|
Napi::Value AddMonitoredItemsAsync(const Napi::CallbackInfo& info) {
|
|
1387
1970
|
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[2].IsArray()) return env.Undefined(); uint32_t subscription_id = info[1].ToNumber().Uint32Value(); Napi::Array array = info[2].As<Napi::Array>(); Napi::Object defaults = info.Length() > 3 && info[3].IsObject() ? info[3].As<Napi::Object>() : Napi::Object::New(env); std::vector<MonitorInput> inputs;
|
|
1388
1971
|
for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Object object = array.Get(index).As<Napi::Object>(); MonitorInput input; input.node_id = GetString(object, "nodeId"); input.sampling_interval = GetDouble(object, "samplingInterval", GetDouble(defaults, "samplingInterval", 1000)); input.queue_size = GetUInt(object, "queueSize", GetUInt(defaults, "queueSize", 10)); input.discard_oldest = GetBool(object, "discardOldest", GetBool(defaults, "discardOldest", true)); input.deadband_type = GetString(object, "deadbandType", GetString(defaults, "deadbandType", "none")); input.deadband_value = GetDouble(object, "deadbandValue", GetDouble(defaults, "deadbandValue", 0)); inputs.push_back(std::move(input)); }
|
|
1389
|
-
|
|
1972
|
+
std::string timestamp_mode = GetString(defaults, "timestampMode", "source");
|
|
1973
|
+
UA_TimestampsToReturn timestamps_to_return = timestamp_mode == "both" ? UA_TIMESTAMPSTORETURN_BOTH :
|
|
1974
|
+
(timestamp_mode == "server" ? UA_TIMESTAMPSTORETURN_SERVER :
|
|
1975
|
+
(timestamp_mode == "neither" ? UA_TIMESTAMPSTORETURN_NEITHER : UA_TIMESTAMPSTORETURN_SOURCE));
|
|
1976
|
+
auto deferred = Napi::Promise::Deferred::New(env); (new AddMonitoredWorker(deferred, context, subscription_id, std::move(inputs), timestamps_to_return))->Queue(); return deferred.Promise();
|
|
1390
1977
|
}
|
|
1391
1978
|
|
|
1392
1979
|
Napi::Value RemoveMonitoredItemsAsync(const Napi::CallbackInfo& info) {
|
|
@@ -1399,6 +1986,7 @@ Napi::Value ValidateSubscriptionAsync(const Napi::CallbackInfo& info) { Napi::En
|
|
|
1399
1986
|
|
|
1400
1987
|
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1401
1988
|
exports.Set("connectAsync", Napi::Function::New(env, ConnectAsync)); exports.Set("disconnectAsync", Napi::Function::New(env, DisconnectAsync));
|
|
1989
|
+
exports.Set("discoverEndpointsAsync", Napi::Function::New(env, DiscoverEndpointsAsync));
|
|
1402
1990
|
exports.Set("getClientDiagnostics", Napi::Function::New(env, GetClientDiagnostics));
|
|
1403
1991
|
exports.Set("browseAsync", Napi::Function::New(env, BrowseAsync)); exports.Set("readManyAsync", Napi::Function::New(env, ReadManyAsync)); exports.Set("writeManyAsync", Napi::Function::New(env, WriteManyAsync));
|
|
1404
1992
|
exports.Set("createSubscriptionAsync", Napi::Function::New(env, CreateSubscriptionAsync)); exports.Set("addMonitoredItemsAsync", Napi::Function::New(env, AddMonitoredItemsAsync)); exports.Set("removeMonitoredItemsAsync", Napi::Function::New(env, RemoveMonitoredItemsAsync)); exports.Set("deleteSubscriptionAsync", Napi::Function::New(env, DeleteSubscriptionAsync)); exports.Set("validateSubscriptionAsync", Napi::Function::New(env, ValidateSubscriptionAsync));
|