@tier0/opcua-client 0.1.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 +83 -0
- package/LICENSE +17 -0
- package/README.md +141 -0
- package/binding.gyp +24 -0
- package/index.js +5 -0
- package/lib/browse-tree.js +55 -0
- package/lib/client.js +321 -0
- package/lib/native.js +32 -0
- package/lib/points.js +231 -0
- package/nodes/browse.html +258 -0
- package/nodes/browse.js +121 -0
- package/nodes/connection.html +44 -0
- package/nodes/connection.js +49 -0
- package/nodes/read.html +187 -0
- package/nodes/read.js +66 -0
- package/nodes/subscribe.html +44 -0
- package/nodes/subscribe.js +285 -0
- package/nodes/write.html +209 -0
- package/nodes/write.js +46 -0
- package/package.json +61 -0
- package/scripts/build-open62541.sh +26 -0
- package/src/addon.cc +1410 -0
package/src/addon.cc
ADDED
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
|
|
3
|
+
#include <open62541/client.h>
|
|
4
|
+
#include <open62541/client_config_default.h>
|
|
5
|
+
#include <open62541/client_highlevel.h>
|
|
6
|
+
#include <open62541/client_subscriptions.h>
|
|
7
|
+
#include <open62541/nodeids.h>
|
|
8
|
+
|
|
9
|
+
#include <algorithm>
|
|
10
|
+
#include <atomic>
|
|
11
|
+
#include <chrono>
|
|
12
|
+
#include <cstdint>
|
|
13
|
+
#include <deque>
|
|
14
|
+
#include <map>
|
|
15
|
+
#include <memory>
|
|
16
|
+
#include <mutex>
|
|
17
|
+
#include <set>
|
|
18
|
+
#include <string>
|
|
19
|
+
#include <thread>
|
|
20
|
+
#include <unordered_map>
|
|
21
|
+
#include <utility>
|
|
22
|
+
#include <vector>
|
|
23
|
+
|
|
24
|
+
namespace {
|
|
25
|
+
|
|
26
|
+
std::string StatusName(UA_StatusCode status) {
|
|
27
|
+
if (status == UA_STATUSCODE_GOOD) return "Good";
|
|
28
|
+
const char* name = UA_StatusCode_name(status);
|
|
29
|
+
return name ? std::string(name) : "Unknown";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
std::string UaString(const UA_String& value) {
|
|
33
|
+
return value.data ? std::string(reinterpret_cast<const char*>(value.data), value.length) : "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
std::string NodeIdString(const UA_NodeId& node_id) {
|
|
37
|
+
UA_String text = UA_STRING_NULL;
|
|
38
|
+
if (!UA_StatusCode_isGood(UA_NodeId_print(&node_id, &text))) return "";
|
|
39
|
+
std::string result = UaString(text);
|
|
40
|
+
UA_String_clear(&text);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
bool ParseNodeId(const std::string& value, UA_NodeId* output) {
|
|
45
|
+
UA_NodeId_init(output);
|
|
46
|
+
UA_String input = UA_STRING(const_cast<char*>(value.c_str()));
|
|
47
|
+
return UA_StatusCode_isGood(UA_NodeId_parse(output, input));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
std::string QualifiedNameString(const UA_QualifiedName& value) {
|
|
51
|
+
return UaString(value.name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
std::string LocalizedTextString(const UA_LocalizedText& value) {
|
|
55
|
+
return UaString(value.text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Napi::Value DateTimeValue(Napi::Env env, UA_DateTime value) {
|
|
59
|
+
constexpr UA_Int64 epoch_offset = 116444736000000000LL;
|
|
60
|
+
return Napi::Date::New(env, static_cast<double>(value - epoch_offset) / 10000.0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const UA_DataType* DataTypeByName(const std::string& name) {
|
|
64
|
+
if (name == "Boolean") return &UA_TYPES[UA_TYPES_BOOLEAN];
|
|
65
|
+
if (name == "SByte") return &UA_TYPES[UA_TYPES_SBYTE];
|
|
66
|
+
if (name == "Byte") return &UA_TYPES[UA_TYPES_BYTE];
|
|
67
|
+
if (name == "Int16") return &UA_TYPES[UA_TYPES_INT16];
|
|
68
|
+
if (name == "UInt16") return &UA_TYPES[UA_TYPES_UINT16];
|
|
69
|
+
if (name == "Int32") return &UA_TYPES[UA_TYPES_INT32];
|
|
70
|
+
if (name == "UInt32") return &UA_TYPES[UA_TYPES_UINT32];
|
|
71
|
+
if (name == "Int64") return &UA_TYPES[UA_TYPES_INT64];
|
|
72
|
+
if (name == "UInt64") return &UA_TYPES[UA_TYPES_UINT64];
|
|
73
|
+
if (name == "Float") return &UA_TYPES[UA_TYPES_FLOAT];
|
|
74
|
+
if (name == "Double") return &UA_TYPES[UA_TYPES_DOUBLE];
|
|
75
|
+
if (name == "String") return &UA_TYPES[UA_TYPES_STRING];
|
|
76
|
+
if (name == "DateTime") return &UA_TYPES[UA_TYPES_DATETIME];
|
|
77
|
+
if (name == "ByteString") return &UA_TYPES[UA_TYPES_BYTESTRING];
|
|
78
|
+
if (name == "LocalizedText") return &UA_TYPES[UA_TYPES_LOCALIZEDTEXT];
|
|
79
|
+
if (name == "QualifiedName") return &UA_TYPES[UA_TYPES_QUALIFIEDNAME];
|
|
80
|
+
return nullptr;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
std::string DataTypeName(const UA_DataType* type) {
|
|
84
|
+
if (!type) return "Unknown";
|
|
85
|
+
if (type == &UA_TYPES[UA_TYPES_BOOLEAN]) return "Boolean";
|
|
86
|
+
if (type == &UA_TYPES[UA_TYPES_SBYTE]) return "SByte";
|
|
87
|
+
if (type == &UA_TYPES[UA_TYPES_BYTE]) return "Byte";
|
|
88
|
+
if (type == &UA_TYPES[UA_TYPES_INT16]) return "Int16";
|
|
89
|
+
if (type == &UA_TYPES[UA_TYPES_UINT16]) return "UInt16";
|
|
90
|
+
if (type == &UA_TYPES[UA_TYPES_INT32]) return "Int32";
|
|
91
|
+
if (type == &UA_TYPES[UA_TYPES_UINT32]) return "UInt32";
|
|
92
|
+
if (type == &UA_TYPES[UA_TYPES_INT64]) return "Int64";
|
|
93
|
+
if (type == &UA_TYPES[UA_TYPES_UINT64]) return "UInt64";
|
|
94
|
+
if (type == &UA_TYPES[UA_TYPES_FLOAT]) return "Float";
|
|
95
|
+
if (type == &UA_TYPES[UA_TYPES_DOUBLE]) return "Double";
|
|
96
|
+
if (type == &UA_TYPES[UA_TYPES_STRING]) return "String";
|
|
97
|
+
if (type == &UA_TYPES[UA_TYPES_DATETIME]) return "DateTime";
|
|
98
|
+
if (type == &UA_TYPES[UA_TYPES_BYTESTRING]) return "ByteString";
|
|
99
|
+
if (type == &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]) return "LocalizedText";
|
|
100
|
+
if (type == &UA_TYPES[UA_TYPES_QUALIFIEDNAME]) return "QualifiedName";
|
|
101
|
+
return type->typeName ? std::string(type->typeName) : "Unknown";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
std::string BuiltinTypeName(const UA_NodeId& node_id) {
|
|
105
|
+
if (node_id.namespaceIndex != 0 || node_id.identifierType != UA_NODEIDTYPE_NUMERIC) {
|
|
106
|
+
return NodeIdString(node_id);
|
|
107
|
+
}
|
|
108
|
+
switch (node_id.identifier.numeric) {
|
|
109
|
+
case UA_NS0ID_BOOLEAN: return "Boolean";
|
|
110
|
+
case UA_NS0ID_SBYTE: return "SByte";
|
|
111
|
+
case UA_NS0ID_BYTE: return "Byte";
|
|
112
|
+
case UA_NS0ID_INT16: return "Int16";
|
|
113
|
+
case UA_NS0ID_UINT16: return "UInt16";
|
|
114
|
+
case UA_NS0ID_INT32: return "Int32";
|
|
115
|
+
case UA_NS0ID_UINT32: return "UInt32";
|
|
116
|
+
case UA_NS0ID_INT64: return "Int64";
|
|
117
|
+
case UA_NS0ID_UINT64: return "UInt64";
|
|
118
|
+
case UA_NS0ID_FLOAT: return "Float";
|
|
119
|
+
case UA_NS0ID_DOUBLE: return "Double";
|
|
120
|
+
case UA_NS0ID_STRING: return "String";
|
|
121
|
+
case UA_NS0ID_DATETIME: return "DateTime";
|
|
122
|
+
case UA_NS0ID_BYTESTRING: return "ByteString";
|
|
123
|
+
default: return NodeIdString(node_id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Napi::Value VariantValue(Napi::Env env, const UA_Variant* variant) {
|
|
128
|
+
if (!variant || !variant->type || !variant->data) return env.Null();
|
|
129
|
+
const UA_DataType* type = variant->type;
|
|
130
|
+
|
|
131
|
+
auto scalar = [&](const void* data) -> Napi::Value {
|
|
132
|
+
if (type == &UA_TYPES[UA_TYPES_BOOLEAN]) return Napi::Boolean::New(env, *static_cast<const UA_Boolean*>(data));
|
|
133
|
+
if (type == &UA_TYPES[UA_TYPES_SBYTE]) return Napi::Number::New(env, *static_cast<const UA_SByte*>(data));
|
|
134
|
+
if (type == &UA_TYPES[UA_TYPES_BYTE]) return Napi::Number::New(env, *static_cast<const UA_Byte*>(data));
|
|
135
|
+
if (type == &UA_TYPES[UA_TYPES_INT16]) return Napi::Number::New(env, *static_cast<const UA_Int16*>(data));
|
|
136
|
+
if (type == &UA_TYPES[UA_TYPES_UINT16]) return Napi::Number::New(env, *static_cast<const UA_UInt16*>(data));
|
|
137
|
+
if (type == &UA_TYPES[UA_TYPES_INT32]) return Napi::Number::New(env, *static_cast<const UA_Int32*>(data));
|
|
138
|
+
if (type == &UA_TYPES[UA_TYPES_UINT32]) return Napi::Number::New(env, *static_cast<const UA_UInt32*>(data));
|
|
139
|
+
if (type == &UA_TYPES[UA_TYPES_INT64]) return Napi::Number::New(env, static_cast<double>(*static_cast<const UA_Int64*>(data)));
|
|
140
|
+
if (type == &UA_TYPES[UA_TYPES_UINT64]) return Napi::Number::New(env, static_cast<double>(*static_cast<const UA_UInt64*>(data)));
|
|
141
|
+
if (type == &UA_TYPES[UA_TYPES_FLOAT]) return Napi::Number::New(env, *static_cast<const UA_Float*>(data));
|
|
142
|
+
if (type == &UA_TYPES[UA_TYPES_DOUBLE]) return Napi::Number::New(env, *static_cast<const UA_Double*>(data));
|
|
143
|
+
if (type == &UA_TYPES[UA_TYPES_STRING]) return Napi::String::New(env, UaString(*static_cast<const UA_String*>(data)));
|
|
144
|
+
if (type == &UA_TYPES[UA_TYPES_DATETIME]) return DateTimeValue(env, *static_cast<const UA_DateTime*>(data));
|
|
145
|
+
if (type == &UA_TYPES[UA_TYPES_BYTESTRING]) {
|
|
146
|
+
const auto* value = static_cast<const UA_ByteString*>(data);
|
|
147
|
+
return Napi::Buffer<UA_Byte>::Copy(env, value->data, value->length);
|
|
148
|
+
}
|
|
149
|
+
if (type == &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]) {
|
|
150
|
+
const auto* value = static_cast<const UA_LocalizedText*>(data);
|
|
151
|
+
Napi::Object result = Napi::Object::New(env);
|
|
152
|
+
result.Set("locale", UaString(value->locale));
|
|
153
|
+
result.Set("text", UaString(value->text));
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
if (type == &UA_TYPES[UA_TYPES_QUALIFIEDNAME]) {
|
|
157
|
+
const auto* value = static_cast<const UA_QualifiedName*>(data);
|
|
158
|
+
Napi::Object result = Napi::Object::New(env);
|
|
159
|
+
result.Set("namespaceIndex", value->namespaceIndex);
|
|
160
|
+
result.Set("name", UaString(value->name));
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
return env.Null();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (variant->arrayLength == 0) return scalar(variant->data);
|
|
167
|
+
Napi::Array result = Napi::Array::New(env, variant->arrayLength);
|
|
168
|
+
for (size_t index = 0; index < variant->arrayLength; ++index) {
|
|
169
|
+
result.Set(index, scalar(static_cast<const char*>(variant->data) + index * type->memSize));
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
bool JsScalarToVariant(Napi::Env env, const Napi::Value& input, const std::string& name,
|
|
175
|
+
UA_Variant* output) {
|
|
176
|
+
UA_Variant_init(output);
|
|
177
|
+
const UA_DataType* type = DataTypeByName(name);
|
|
178
|
+
if (!type) {
|
|
179
|
+
Napi::TypeError::New(env, "Unsupported OPC UA data type: " + name).ThrowAsJavaScriptException();
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if (name == "Boolean") {
|
|
183
|
+
UA_Boolean value = input.ToBoolean();
|
|
184
|
+
return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type));
|
|
185
|
+
}
|
|
186
|
+
if (name == "SByte") { UA_SByte value = static_cast<UA_SByte>(input.ToNumber().Int32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
187
|
+
if (name == "Byte") { UA_Byte value = static_cast<UA_Byte>(input.ToNumber().Uint32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
188
|
+
if (name == "Int16") { UA_Int16 value = static_cast<UA_Int16>(input.ToNumber().Int32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
189
|
+
if (name == "UInt16") { UA_UInt16 value = static_cast<UA_UInt16>(input.ToNumber().Uint32Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
190
|
+
if (name == "Int32") { UA_Int32 value = input.ToNumber().Int32Value(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
191
|
+
if (name == "UInt32") { UA_UInt32 value = input.ToNumber().Uint32Value(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
192
|
+
if (name == "Int64") { bool lossless = false; UA_Int64 value = input.IsBigInt() ? input.As<Napi::BigInt>().Int64Value(&lossless) : static_cast<UA_Int64>(input.ToNumber().Int64Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
193
|
+
if (name == "UInt64") { bool lossless = false; UA_UInt64 value = input.IsBigInt() ? input.As<Napi::BigInt>().Uint64Value(&lossless) : static_cast<UA_UInt64>(input.ToNumber().Int64Value()); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
194
|
+
if (name == "Float") { UA_Float value = input.ToNumber().FloatValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
195
|
+
if (name == "Double") { UA_Double value = input.ToNumber().DoubleValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
196
|
+
if (name == "String") { std::string text = input.ToString().Utf8Value(); UA_String value = UA_STRING(const_cast<char*>(text.c_str())); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
197
|
+
if (name == "DateTime") { constexpr UA_Int64 offset = 116444736000000000LL; double ms = input.IsDate() ? input.As<Napi::Date>().ValueOf() : input.ToNumber().DoubleValue(); UA_DateTime value = static_cast<UA_DateTime>(ms * 10000.0) + offset; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
198
|
+
if (name == "ByteString") {
|
|
199
|
+
if (!input.IsBuffer()) { Napi::TypeError::New(env, "ByteString value must be a Buffer").ThrowAsJavaScriptException(); return false; }
|
|
200
|
+
auto buffer = input.As<Napi::Buffer<UA_Byte>>(); UA_ByteString value{buffer.Length(), buffer.Data()}; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type));
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
std::string GetString(const Napi::Object& object, const char* name, const std::string& fallback = "") {
|
|
206
|
+
if (!object.Has(name)) return fallback;
|
|
207
|
+
Napi::Value value = object.Get(name);
|
|
208
|
+
return value.IsString() ? value.As<Napi::String>().Utf8Value() : fallback;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
double GetDouble(const Napi::Object& object, const char* name, double fallback) {
|
|
212
|
+
if (!object.Has(name)) return fallback;
|
|
213
|
+
Napi::Value value = object.Get(name);
|
|
214
|
+
return value.IsNumber() ? value.As<Napi::Number>().DoubleValue() : fallback;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
uint32_t GetUInt(const Napi::Object& object, const char* name, uint32_t fallback) {
|
|
218
|
+
double value = GetDouble(object, name, fallback);
|
|
219
|
+
return value >= 0 ? static_cast<uint32_t>(value) : fallback;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
bool GetBool(const Napi::Object& object, const char* name, bool fallback) {
|
|
223
|
+
if (!object.Has(name)) return fallback;
|
|
224
|
+
Napi::Value value = object.Get(name);
|
|
225
|
+
return value.IsBoolean() ? value.As<Napi::Boolean>().Value() : fallback;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
struct RawChange {
|
|
229
|
+
uint32_t monitored_item_id = 0;
|
|
230
|
+
std::string node_id;
|
|
231
|
+
UA_DataValue value;
|
|
232
|
+
std::chrono::steady_clock::time_point enqueued_at;
|
|
233
|
+
RawChange() : enqueued_at(std::chrono::steady_clock::now()) { UA_DataValue_init(&value); }
|
|
234
|
+
~RawChange() { UA_DataValue_clear(&value); }
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
struct NotificationQueue {
|
|
238
|
+
Napi::ThreadSafeFunction callback;
|
|
239
|
+
std::mutex mutex;
|
|
240
|
+
std::deque<std::unique_ptr<RawChange>> fifo;
|
|
241
|
+
std::unordered_map<uint32_t, std::unique_ptr<RawChange>> latest;
|
|
242
|
+
std::deque<uint32_t> latest_order;
|
|
243
|
+
size_t capacity = 10000;
|
|
244
|
+
size_t batch_size = 500;
|
|
245
|
+
std::string mode = "all";
|
|
246
|
+
std::string overflow_policy = "drop-oldest";
|
|
247
|
+
std::atomic<bool> scheduled{false};
|
|
248
|
+
std::atomic<bool> closing{false};
|
|
249
|
+
std::atomic<bool> released{false};
|
|
250
|
+
std::atomic<uint64_t> enqueued{0};
|
|
251
|
+
std::atomic<uint64_t> delivered{0};
|
|
252
|
+
std::atomic<uint64_t> dropped{0};
|
|
253
|
+
std::atomic<uint64_t> coalesced{0};
|
|
254
|
+
std::atomic<uint64_t> batches{0};
|
|
255
|
+
size_t max_depth = 0;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
struct SubscriptionContext;
|
|
259
|
+
|
|
260
|
+
struct MonitoredContext {
|
|
261
|
+
std::shared_ptr<NotificationQueue> notifications;
|
|
262
|
+
std::string node_id;
|
|
263
|
+
uint32_t monitored_item_id = 0;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
struct SubscriptionContext {
|
|
267
|
+
uint32_t subscription_id = 0;
|
|
268
|
+
std::shared_ptr<NotificationQueue> notifications;
|
|
269
|
+
std::map<uint32_t, std::unique_ptr<MonitoredContext>> monitored;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
struct OperationLimits {
|
|
273
|
+
uint32_t max_nodes_per_read = 0;
|
|
274
|
+
uint32_t max_nodes_per_write = 0;
|
|
275
|
+
uint32_t max_nodes_per_browse = 0;
|
|
276
|
+
uint32_t max_monitored_items_per_call = 0;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
struct CachedNodeId {
|
|
280
|
+
UA_NodeId value;
|
|
281
|
+
CachedNodeId() { UA_NodeId_init(&value); }
|
|
282
|
+
~CachedNodeId() { UA_NodeId_clear(&value); }
|
|
283
|
+
CachedNodeId(const CachedNodeId&) = delete;
|
|
284
|
+
CachedNodeId& operator=(const CachedNodeId&) = delete;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
struct ClientContext {
|
|
288
|
+
UA_Client* client = nullptr;
|
|
289
|
+
std::atomic<bool> running{false};
|
|
290
|
+
std::thread iterate_thread;
|
|
291
|
+
std::mutex mutex;
|
|
292
|
+
std::map<uint32_t, std::unique_ptr<SubscriptionContext>> subscriptions;
|
|
293
|
+
OperationLimits operation_limits;
|
|
294
|
+
std::mutex node_id_cache_mutex;
|
|
295
|
+
std::unordered_map<std::string, std::unique_ptr<CachedNodeId>> node_id_cache;
|
|
296
|
+
std::deque<std::string> node_id_cache_order;
|
|
297
|
+
size_t node_id_cache_capacity = 100000;
|
|
298
|
+
std::atomic<uint64_t> node_id_cache_hits{0};
|
|
299
|
+
std::atomic<uint64_t> node_id_cache_misses{0};
|
|
300
|
+
std::atomic<uint64_t> node_id_cache_bypasses{0};
|
|
301
|
+
std::atomic<uint64_t> node_id_cache_evictions{0};
|
|
302
|
+
Napi::ThreadSafeFunction state_callback;
|
|
303
|
+
std::atomic<bool> has_state_callback{false};
|
|
304
|
+
std::atomic<bool> state_callback_released{false};
|
|
305
|
+
std::atomic<uint32_t> channel_state{UA_SECURECHANNELSTATE_CLOSED};
|
|
306
|
+
std::atomic<uint32_t> session_state{UA_SESSIONSTATE_CLOSED};
|
|
307
|
+
std::atomic<uint32_t> connect_status{UA_STATUSCODE_GOOD};
|
|
308
|
+
std::atomic<bool> connected{false};
|
|
309
|
+
std::atomic<bool> ever_connected{false};
|
|
310
|
+
std::atomic<bool> force_secure_channel_reconnect{false};
|
|
311
|
+
std::atomic<bool> refresh_operation_limits{false};
|
|
312
|
+
std::atomic<uint64_t> state_sequence{0};
|
|
313
|
+
std::atomic<uint64_t> reconnect_count{0};
|
|
314
|
+
std::atomic<uint64_t> disconnect_count{0};
|
|
315
|
+
std::atomic<uint64_t> session_generation{0};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
struct ClientStateEvent {
|
|
319
|
+
std::string state;
|
|
320
|
+
std::string reason;
|
|
321
|
+
uint32_t channel_state = UA_SECURECHANNELSTATE_CLOSED;
|
|
322
|
+
uint32_t session_state = UA_SESSIONSTATE_CLOSED;
|
|
323
|
+
UA_StatusCode status = UA_STATUSCODE_GOOD;
|
|
324
|
+
uint32_t subscription_id = 0;
|
|
325
|
+
uint64_t sequence = 0;
|
|
326
|
+
uint64_t reconnect_count = 0;
|
|
327
|
+
uint64_t session_generation = 0;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const char* ChannelStateName(uint32_t state) {
|
|
331
|
+
switch (state) {
|
|
332
|
+
case UA_SECURECHANNELSTATE_CLOSED: return "Closed";
|
|
333
|
+
case UA_SECURECHANNELSTATE_REVERSE_LISTENING: return "ReverseListening";
|
|
334
|
+
case UA_SECURECHANNELSTATE_CONNECTING: return "Connecting";
|
|
335
|
+
case UA_SECURECHANNELSTATE_CONNECTED: return "Connected";
|
|
336
|
+
case UA_SECURECHANNELSTATE_REVERSE_CONNECTED: return "ReverseConnected";
|
|
337
|
+
case UA_SECURECHANNELSTATE_RHE_SENT: return "ReverseHelloSent";
|
|
338
|
+
case UA_SECURECHANNELSTATE_HEL_SENT: return "HelloSent";
|
|
339
|
+
case UA_SECURECHANNELSTATE_HEL_RECEIVED: return "HelloReceived";
|
|
340
|
+
case UA_SECURECHANNELSTATE_ACK_SENT: return "AcknowledgeSent";
|
|
341
|
+
case UA_SECURECHANNELSTATE_ACK_RECEIVED: return "AcknowledgeReceived";
|
|
342
|
+
case UA_SECURECHANNELSTATE_OPN_SENT: return "OpenSent";
|
|
343
|
+
case UA_SECURECHANNELSTATE_OPEN: return "Open";
|
|
344
|
+
case UA_SECURECHANNELSTATE_CLOSING: return "Closing";
|
|
345
|
+
default: return "Unknown";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const char* SessionStateName(uint32_t state) {
|
|
350
|
+
switch (state) {
|
|
351
|
+
case UA_SESSIONSTATE_CLOSED: return "Closed";
|
|
352
|
+
case UA_SESSIONSTATE_CREATE_REQUESTED: return "CreateRequested";
|
|
353
|
+
case UA_SESSIONSTATE_CREATED: return "Created";
|
|
354
|
+
case UA_SESSIONSTATE_ACTIVATE_REQUESTED: return "ActivateRequested";
|
|
355
|
+
case UA_SESSIONSTATE_ACTIVATED: return "Activated";
|
|
356
|
+
case UA_SESSIONSTATE_CLOSING: return "Closing";
|
|
357
|
+
default: return "Unknown";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
void QueueStateEvent(ClientContext* context, std::unique_ptr<ClientStateEvent> event) {
|
|
362
|
+
if (!context || !context->has_state_callback.load() ||
|
|
363
|
+
context->state_callback_released.load()) return;
|
|
364
|
+
auto* raw = event.release();
|
|
365
|
+
napi_status queued = context->state_callback.NonBlockingCall(
|
|
366
|
+
raw, [](Napi::Env env, Napi::Function callback, ClientStateEvent* data) {
|
|
367
|
+
std::unique_ptr<ClientStateEvent> event(data);
|
|
368
|
+
Napi::Object value = Napi::Object::New(env);
|
|
369
|
+
value.Set("state", event->state);
|
|
370
|
+
value.Set("reason", event->reason);
|
|
371
|
+
value.Set("connected", event->state == "connected");
|
|
372
|
+
value.Set("recoverable", UA_StatusCode_isGood(event->status));
|
|
373
|
+
value.Set("terminal", !UA_StatusCode_isGood(event->status));
|
|
374
|
+
value.Set("statusCode", StatusName(event->status));
|
|
375
|
+
value.Set("channelState", ChannelStateName(event->channel_state));
|
|
376
|
+
value.Set("sessionState", SessionStateName(event->session_state));
|
|
377
|
+
value.Set("sequence", Napi::Number::New(env, static_cast<double>(event->sequence)));
|
|
378
|
+
value.Set("reconnectCount", Napi::Number::New(env, static_cast<double>(event->reconnect_count)));
|
|
379
|
+
value.Set("sessionGeneration", Napi::Number::New(env, static_cast<double>(event->session_generation)));
|
|
380
|
+
if (event->subscription_id) value.Set("subscriptionId", event->subscription_id);
|
|
381
|
+
callback.Call({value});
|
|
382
|
+
});
|
|
383
|
+
if (queued != napi_ok) delete raw;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
void ClientStateChanged(UA_Client* client, UA_SecureChannelState channel_state,
|
|
387
|
+
UA_SessionState session_state, UA_StatusCode connect_status) {
|
|
388
|
+
auto* context = static_cast<ClientContext*>(UA_Client_getContext(client));
|
|
389
|
+
if (!context) return;
|
|
390
|
+
context->channel_state.store(channel_state);
|
|
391
|
+
context->session_state.store(session_state);
|
|
392
|
+
context->connect_status.store(connect_status);
|
|
393
|
+
bool now_connected = channel_state == UA_SECURECHANNELSTATE_OPEN &&
|
|
394
|
+
session_state == UA_SESSIONSTATE_ACTIVATED &&
|
|
395
|
+
UA_StatusCode_isGood(connect_status);
|
|
396
|
+
bool was_connected = context->connected.exchange(now_connected);
|
|
397
|
+
if (was_connected && !now_connected) context->disconnect_count.fetch_add(1);
|
|
398
|
+
if (now_connected && !was_connected) {
|
|
399
|
+
bool was_ever_connected = context->ever_connected.exchange(true);
|
|
400
|
+
if (was_ever_connected) context->reconnect_count.fetch_add(1);
|
|
401
|
+
context->session_generation.fetch_add(1);
|
|
402
|
+
context->refresh_operation_limits.store(true);
|
|
403
|
+
}
|
|
404
|
+
auto event = std::make_unique<ClientStateEvent>();
|
|
405
|
+
event->state = now_connected ? "connected" :
|
|
406
|
+
(UA_StatusCode_isGood(connect_status) ? "reconnecting" : "failed");
|
|
407
|
+
event->reason = now_connected ? (was_connected ? "state" : "session-activated") :
|
|
408
|
+
(UA_StatusCode_isGood(connect_status) ? "transport-or-session" : "terminal");
|
|
409
|
+
event->channel_state = channel_state;
|
|
410
|
+
event->session_state = session_state;
|
|
411
|
+
event->status = connect_status;
|
|
412
|
+
event->sequence = context->state_sequence.fetch_add(1) + 1;
|
|
413
|
+
event->reconnect_count = context->reconnect_count.load();
|
|
414
|
+
event->session_generation = context->session_generation.load();
|
|
415
|
+
QueueStateEvent(context, std::move(event));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
void ClientInactivity(UA_Client* client) {
|
|
419
|
+
auto* context = static_cast<ClientContext*>(UA_Client_getContext(client));
|
|
420
|
+
if (!context) return;
|
|
421
|
+
context->force_secure_channel_reconnect.store(true);
|
|
422
|
+
auto event = std::make_unique<ClientStateEvent>();
|
|
423
|
+
event->state = "reconnecting";
|
|
424
|
+
event->reason = "connectivity-inactive";
|
|
425
|
+
event->channel_state = context->channel_state.load();
|
|
426
|
+
event->session_state = context->session_state.load();
|
|
427
|
+
event->sequence = context->state_sequence.fetch_add(1) + 1;
|
|
428
|
+
event->reconnect_count = context->reconnect_count.load();
|
|
429
|
+
event->session_generation = context->session_generation.load();
|
|
430
|
+
QueueStateEvent(context, std::move(event));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
void SubscriptionInactivity(UA_Client* client, UA_UInt32 subscription_id, void*) {
|
|
434
|
+
auto* context = static_cast<ClientContext*>(UA_Client_getContext(client));
|
|
435
|
+
if (!context) return;
|
|
436
|
+
context->force_secure_channel_reconnect.store(true);
|
|
437
|
+
auto event = std::make_unique<ClientStateEvent>();
|
|
438
|
+
event->state = "subscription-inactive";
|
|
439
|
+
event->reason = "publish-timeout";
|
|
440
|
+
event->channel_state = context->channel_state.load();
|
|
441
|
+
event->session_state = context->session_state.load();
|
|
442
|
+
event->subscription_id = subscription_id;
|
|
443
|
+
event->sequence = context->state_sequence.fetch_add(1) + 1;
|
|
444
|
+
event->reconnect_count = context->reconnect_count.load();
|
|
445
|
+
event->session_generation = context->session_generation.load();
|
|
446
|
+
QueueStateEvent(context, std::move(event));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
bool CopyCachedNodeId(ClientContext* context, const std::string& text, UA_NodeId* output) {
|
|
450
|
+
if (!context || context->node_id_cache_capacity == 0) {
|
|
451
|
+
if (context) context->node_id_cache_bypasses.fetch_add(1);
|
|
452
|
+
return ParseNodeId(text, output);
|
|
453
|
+
}
|
|
454
|
+
std::lock_guard<std::mutex> lock(context->node_id_cache_mutex);
|
|
455
|
+
auto found = context->node_id_cache.find(text);
|
|
456
|
+
if (found != context->node_id_cache.end()) {
|
|
457
|
+
context->node_id_cache_hits.fetch_add(1);
|
|
458
|
+
return UA_StatusCode_isGood(UA_NodeId_copy(&found->second->value, output));
|
|
459
|
+
}
|
|
460
|
+
context->node_id_cache_misses.fetch_add(1);
|
|
461
|
+
auto parsed = std::make_unique<CachedNodeId>();
|
|
462
|
+
if (!ParseNodeId(text, &parsed->value)) return false;
|
|
463
|
+
if (context->node_id_cache.size() >= context->node_id_cache_capacity) {
|
|
464
|
+
context->node_id_cache.erase(context->node_id_cache_order.front());
|
|
465
|
+
context->node_id_cache_order.pop_front();
|
|
466
|
+
context->node_id_cache_evictions.fetch_add(1);
|
|
467
|
+
}
|
|
468
|
+
bool copied = UA_StatusCode_isGood(UA_NodeId_copy(&parsed->value, output));
|
|
469
|
+
if (copied) {
|
|
470
|
+
context->node_id_cache.emplace(text, std::move(parsed));
|
|
471
|
+
context->node_id_cache_order.push_back(text);
|
|
472
|
+
}
|
|
473
|
+
return copied;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
void ReadOperationLimits(ClientContext* context) {
|
|
477
|
+
constexpr size_t count = 4;
|
|
478
|
+
const UA_UInt32 node_ids[count] = {
|
|
479
|
+
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERREAD,
|
|
480
|
+
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERWRITE,
|
|
481
|
+
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXNODESPERBROWSE,
|
|
482
|
+
UA_NS0ID_SERVER_SERVERCAPABILITIES_OPERATIONLIMITS_MAXMONITOREDITEMSPERCALL};
|
|
483
|
+
uint32_t* targets[count] = {
|
|
484
|
+
&context->operation_limits.max_nodes_per_read,
|
|
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};
|
|
488
|
+
UA_ReadRequest request;
|
|
489
|
+
UA_ReadRequest_init(&request);
|
|
490
|
+
request.timestampsToReturn = UA_TIMESTAMPSTORETURN_NEITHER;
|
|
491
|
+
request.nodesToReadSize = count;
|
|
492
|
+
request.nodesToRead = static_cast<UA_ReadValueId*>(
|
|
493
|
+
UA_Array_new(count, &UA_TYPES[UA_TYPES_READVALUEID]));
|
|
494
|
+
if (!request.nodesToRead) return;
|
|
495
|
+
for (size_t index = 0; index < count; ++index) {
|
|
496
|
+
request.nodesToRead[index].nodeId = UA_NODEID_NUMERIC(0, node_ids[index]);
|
|
497
|
+
request.nodesToRead[index].attributeId = UA_ATTRIBUTEID_VALUE;
|
|
498
|
+
}
|
|
499
|
+
UA_ReadResponse response = UA_Client_Service_read(context->client, request);
|
|
500
|
+
UA_ReadRequest_clear(&request);
|
|
501
|
+
if (UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
|
|
502
|
+
for (size_t index = 0; index < count && index < response.resultsSize; ++index) {
|
|
503
|
+
const UA_DataValue& value = response.results[index];
|
|
504
|
+
if (value.hasValue && UA_Variant_isScalar(&value.value) &&
|
|
505
|
+
value.value.type == &UA_TYPES[UA_TYPES_UINT32] && value.value.data) {
|
|
506
|
+
*targets[index] = *static_cast<UA_UInt32*>(value.value.data);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
UA_ReadResponse_clear(&response);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
std::mutex clients_mutex;
|
|
514
|
+
std::map<uint64_t, std::shared_ptr<ClientContext>> clients;
|
|
515
|
+
std::atomic<uint64_t> next_client_id{1};
|
|
516
|
+
|
|
517
|
+
std::shared_ptr<ClientContext> FindClient(uint64_t client_id) {
|
|
518
|
+
std::lock_guard<std::mutex> lock(clients_mutex);
|
|
519
|
+
auto found = clients.find(client_id);
|
|
520
|
+
return found == clients.end() ? nullptr : found->second;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
void Iterate(ClientContext* context) {
|
|
524
|
+
while (context->running.load()) {
|
|
525
|
+
{
|
|
526
|
+
std::lock_guard<std::mutex> lock(context->mutex);
|
|
527
|
+
if (!context->client) return;
|
|
528
|
+
UA_Client_run_iterate(context->client, 20);
|
|
529
|
+
if (context->force_secure_channel_reconnect.exchange(false)) {
|
|
530
|
+
UA_Client_disconnectSecureChannelAsync(context->client);
|
|
531
|
+
}
|
|
532
|
+
if (context->refresh_operation_limits.exchange(false) &&
|
|
533
|
+
context->session_state.load() == UA_SESSIONSTATE_ACTIVATED) {
|
|
534
|
+
ReadOperationLimits(context);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
void CloseNotificationQueue(const std::shared_ptr<NotificationQueue>& queue) {
|
|
542
|
+
if (!queue || queue->released.exchange(true)) return;
|
|
543
|
+
queue->closing.store(true);
|
|
544
|
+
queue->callback.Release();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
void CloseClient(uint64_t client_id) {
|
|
548
|
+
std::shared_ptr<ClientContext> context;
|
|
549
|
+
{
|
|
550
|
+
std::lock_guard<std::mutex> lock(clients_mutex);
|
|
551
|
+
auto found = clients.find(client_id);
|
|
552
|
+
if (found == clients.end()) return;
|
|
553
|
+
context = found->second;
|
|
554
|
+
clients.erase(found);
|
|
555
|
+
}
|
|
556
|
+
context->running.store(false);
|
|
557
|
+
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
|
+
if (context->has_state_callback.load() &&
|
|
567
|
+
!context->state_callback_released.exchange(true)) {
|
|
568
|
+
context->state_callback.Release();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
struct DrainRequest { std::shared_ptr<NotificationQueue> queue; };
|
|
573
|
+
|
|
574
|
+
size_t NotificationQueueDepthLocked(const NotificationQueue& queue) {
|
|
575
|
+
return queue.mode == "latest" ? queue.latest.size() : queue.fifo.size();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
void ScheduleNotificationDrain(const std::shared_ptr<NotificationQueue>& queue);
|
|
579
|
+
|
|
580
|
+
void DrainNotifications(Napi::Env env, Napi::Function callback, DrainRequest* request) {
|
|
581
|
+
auto queue = request->queue;
|
|
582
|
+
delete request;
|
|
583
|
+
std::vector<std::unique_ptr<RawChange>> changes;
|
|
584
|
+
size_t queue_depth = 0;
|
|
585
|
+
size_t max_queue_depth = 0;
|
|
586
|
+
double max_lag_ms = 0;
|
|
587
|
+
{
|
|
588
|
+
std::lock_guard<std::mutex> lock(queue->mutex);
|
|
589
|
+
const size_t count = std::min(queue->batch_size, NotificationQueueDepthLocked(*queue));
|
|
590
|
+
changes.reserve(count);
|
|
591
|
+
for (size_t index = 0; index < count; ++index) {
|
|
592
|
+
std::unique_ptr<RawChange> change;
|
|
593
|
+
if (queue->mode == "latest") {
|
|
594
|
+
const uint32_t monitored_item_id = queue->latest_order.front();
|
|
595
|
+
queue->latest_order.pop_front();
|
|
596
|
+
auto found = queue->latest.find(monitored_item_id);
|
|
597
|
+
if (found != queue->latest.end()) {
|
|
598
|
+
change = std::move(found->second);
|
|
599
|
+
queue->latest.erase(found);
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
change = std::move(queue->fifo.front());
|
|
603
|
+
queue->fifo.pop_front();
|
|
604
|
+
}
|
|
605
|
+
if (change) {
|
|
606
|
+
const auto lag = std::chrono::duration<double, std::milli>(
|
|
607
|
+
std::chrono::steady_clock::now() - change->enqueued_at).count();
|
|
608
|
+
max_lag_ms = std::max(max_lag_ms, lag);
|
|
609
|
+
changes.push_back(std::move(change));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
queue_depth = NotificationQueueDepthLocked(*queue);
|
|
613
|
+
max_queue_depth = queue->max_depth;
|
|
614
|
+
queue->scheduled.store(false);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
queue->delivered.fetch_add(changes.size());
|
|
618
|
+
const uint64_t batch_sequence = queue->batches.fetch_add(1) + 1;
|
|
619
|
+
if (queue_depth > 0 && !queue->closing.load()) ScheduleNotificationDrain(queue);
|
|
620
|
+
|
|
621
|
+
Napi::Array output = Napi::Array::New(env, changes.size());
|
|
622
|
+
for (size_t index = 0; index < changes.size(); ++index) {
|
|
623
|
+
RawChange* data = changes[index].get();
|
|
624
|
+
Napi::Object result = Napi::Object::New(env);
|
|
625
|
+
result.Set("nodeId", data->node_id);
|
|
626
|
+
result.Set("monitoredItemId", data->monitored_item_id);
|
|
627
|
+
result.Set("statusCode", StatusName(data->value.status));
|
|
628
|
+
if (data->value.hasValue) {
|
|
629
|
+
result.Set("value", VariantValue(env, &data->value.value));
|
|
630
|
+
result.Set("dataType", DataTypeName(data->value.value.type));
|
|
631
|
+
}
|
|
632
|
+
if (data->value.hasSourceTimestamp) result.Set("sourceTimestamp", DateTimeValue(env, data->value.sourceTimestamp));
|
|
633
|
+
if (data->value.hasServerTimestamp) result.Set("serverTimestamp", DateTimeValue(env, data->value.serverTimestamp));
|
|
634
|
+
output.Set(index, result);
|
|
635
|
+
}
|
|
636
|
+
Napi::Object diagnostics = Napi::Object::New(env);
|
|
637
|
+
diagnostics.Set("queueDepth", Napi::Number::New(env, static_cast<double>(queue_depth)));
|
|
638
|
+
diagnostics.Set("queueCapacity", Napi::Number::New(env, static_cast<double>(queue->capacity)));
|
|
639
|
+
diagnostics.Set("maxQueueDepth", Napi::Number::New(env, static_cast<double>(max_queue_depth)));
|
|
640
|
+
diagnostics.Set("enqueued", Napi::Number::New(env, static_cast<double>(queue->enqueued.load())));
|
|
641
|
+
diagnostics.Set("delivered", Napi::Number::New(env, static_cast<double>(queue->delivered.load())));
|
|
642
|
+
diagnostics.Set("dropped", Napi::Number::New(env, static_cast<double>(queue->dropped.load())));
|
|
643
|
+
diagnostics.Set("coalesced", Napi::Number::New(env, static_cast<double>(queue->coalesced.load())));
|
|
644
|
+
diagnostics.Set("batchSequence", Napi::Number::New(env, static_cast<double>(batch_sequence)));
|
|
645
|
+
diagnostics.Set("batchSize", Napi::Number::New(env, static_cast<double>(changes.size())));
|
|
646
|
+
diagnostics.Set("maxLagMs", Napi::Number::New(env, max_lag_ms));
|
|
647
|
+
diagnostics.Set("mode", queue->mode);
|
|
648
|
+
diagnostics.Set("overflowPolicy", queue->overflow_policy);
|
|
649
|
+
Napi::Object batch = Napi::Object::New(env);
|
|
650
|
+
batch.Set("changes", output);
|
|
651
|
+
batch.Set("diagnostics", diagnostics);
|
|
652
|
+
callback.Call({batch});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
void ScheduleNotificationDrain(const std::shared_ptr<NotificationQueue>& queue) {
|
|
656
|
+
if (!queue || queue->closing.load() || queue->scheduled.exchange(true)) return;
|
|
657
|
+
auto* request = new DrainRequest{queue};
|
|
658
|
+
const napi_status status = queue->callback.NonBlockingCall(request, DrainNotifications);
|
|
659
|
+
if (status != napi_ok) {
|
|
660
|
+
queue->scheduled.store(false);
|
|
661
|
+
delete request;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
void EnqueueNotification(const std::shared_ptr<NotificationQueue>& queue,
|
|
666
|
+
std::unique_ptr<RawChange> change) {
|
|
667
|
+
if (!queue || queue->closing.load()) return;
|
|
668
|
+
bool accepted = true;
|
|
669
|
+
{
|
|
670
|
+
std::lock_guard<std::mutex> lock(queue->mutex);
|
|
671
|
+
if (queue->mode == "latest") {
|
|
672
|
+
auto found = queue->latest.find(change->monitored_item_id);
|
|
673
|
+
if (found != queue->latest.end()) {
|
|
674
|
+
found->second = std::move(change);
|
|
675
|
+
queue->coalesced.fetch_add(1);
|
|
676
|
+
} else {
|
|
677
|
+
if (queue->latest.size() >= queue->capacity) {
|
|
678
|
+
if (queue->overflow_policy == "drop-newest") {
|
|
679
|
+
queue->dropped.fetch_add(1);
|
|
680
|
+
accepted = false;
|
|
681
|
+
} else {
|
|
682
|
+
const uint32_t oldest = queue->latest_order.front();
|
|
683
|
+
queue->latest_order.pop_front();
|
|
684
|
+
queue->latest.erase(oldest);
|
|
685
|
+
queue->dropped.fetch_add(1);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (accepted) {
|
|
689
|
+
queue->latest_order.push_back(change->monitored_item_id);
|
|
690
|
+
queue->latest.emplace(change->monitored_item_id, std::move(change));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
if (queue->fifo.size() >= queue->capacity) {
|
|
695
|
+
if (queue->overflow_policy == "drop-newest") {
|
|
696
|
+
queue->dropped.fetch_add(1);
|
|
697
|
+
accepted = false;
|
|
698
|
+
} else {
|
|
699
|
+
queue->fifo.pop_front();
|
|
700
|
+
queue->dropped.fetch_add(1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (accepted) queue->fifo.push_back(std::move(change));
|
|
704
|
+
}
|
|
705
|
+
if (accepted) queue->enqueued.fetch_add(1);
|
|
706
|
+
queue->max_depth = std::max(queue->max_depth, NotificationQueueDepthLocked(*queue));
|
|
707
|
+
}
|
|
708
|
+
ScheduleNotificationDrain(queue);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
void DataChange(UA_Client*, UA_UInt32, void*, UA_UInt32 monitored_item_id,
|
|
712
|
+
void* monitored_context, UA_DataValue* value) {
|
|
713
|
+
auto* monitored = static_cast<MonitoredContext*>(monitored_context);
|
|
714
|
+
if (!monitored || !monitored->notifications || !value) return;
|
|
715
|
+
auto change = std::make_unique<RawChange>();
|
|
716
|
+
change->monitored_item_id = monitored_item_id;
|
|
717
|
+
change->node_id = monitored->node_id;
|
|
718
|
+
UA_DataValue_copy(value, &change->value);
|
|
719
|
+
EnqueueNotification(monitored->notifications, std::move(change));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
class PromiseWorker : public Napi::AsyncWorker {
|
|
723
|
+
public:
|
|
724
|
+
explicit PromiseWorker(Napi::Promise::Deferred deferred)
|
|
725
|
+
: Napi::AsyncWorker(deferred.Env()), deferred_(deferred) {}
|
|
726
|
+
void OnError(const Napi::Error& error) override { deferred_.Reject(error.Value()); }
|
|
727
|
+
protected:
|
|
728
|
+
Napi::Promise::Deferred deferred_;
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
struct ConnectionOptions {
|
|
732
|
+
std::string endpoint;
|
|
733
|
+
std::string security_mode;
|
|
734
|
+
std::string security_policy;
|
|
735
|
+
std::string user;
|
|
736
|
+
std::string password;
|
|
737
|
+
uint32_t timeout = 10000;
|
|
738
|
+
uint32_t session_timeout = 60000;
|
|
739
|
+
uint32_t connectivity_check_interval = 5000;
|
|
740
|
+
uint32_t node_id_cache_size = 100000;
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
class ConnectWorker : public PromiseWorker {
|
|
744
|
+
public:
|
|
745
|
+
ConnectWorker(Napi::Promise::Deferred deferred, ConnectionOptions options,
|
|
746
|
+
Napi::ThreadSafeFunction state_callback, bool has_state_callback)
|
|
747
|
+
: PromiseWorker(deferred), options_(std::move(options)),
|
|
748
|
+
state_callback_(std::move(state_callback)),
|
|
749
|
+
has_state_callback_(has_state_callback) {}
|
|
750
|
+
void Execute() override {
|
|
751
|
+
if (options_.endpoint.empty()) { SetError("OPC UA endpoint is required"); return; }
|
|
752
|
+
if (!options_.security_mode.empty() && options_.security_mode != "None") {
|
|
753
|
+
SetError("Sign and SignAndEncrypt require an encryption-enabled open62541 build"); return;
|
|
754
|
+
}
|
|
755
|
+
context_ = std::make_shared<ClientContext>();
|
|
756
|
+
context_->state_callback = state_callback_;
|
|
757
|
+
context_->has_state_callback.store(has_state_callback_);
|
|
758
|
+
context_->client = UA_Client_new();
|
|
759
|
+
if (!context_->client) { SetError("Failed to create open62541 client"); return; }
|
|
760
|
+
UA_ClientConfig* config = UA_Client_getConfig(context_->client);
|
|
761
|
+
UA_ClientConfig_setDefault(config);
|
|
762
|
+
config->timeout = options_.timeout;
|
|
763
|
+
config->requestedSessionTimeout = options_.session_timeout;
|
|
764
|
+
config->connectivityCheckInterval = options_.connectivity_check_interval;
|
|
765
|
+
config->noReconnect = false;
|
|
766
|
+
config->noNewSession = false;
|
|
767
|
+
config->clientContext = context_.get();
|
|
768
|
+
config->stateCallback = ClientStateChanged;
|
|
769
|
+
config->inactivityCallback = ClientInactivity;
|
|
770
|
+
config->subscriptionInactivityCallback = SubscriptionInactivity;
|
|
771
|
+
UA_StatusCode status = options_.user.empty()
|
|
772
|
+
? UA_Client_connect(context_->client, options_.endpoint.c_str())
|
|
773
|
+
: UA_Client_connectUsername(context_->client, options_.endpoint.c_str(), options_.user.c_str(), options_.password.c_str());
|
|
774
|
+
if (!UA_StatusCode_isGood(status)) {
|
|
775
|
+
SetError("Connect failed: " + StatusName(status));
|
|
776
|
+
UA_Client_delete(context_->client); context_->client = nullptr; return;
|
|
777
|
+
}
|
|
778
|
+
context_->node_id_cache_capacity = options_.node_id_cache_size;
|
|
779
|
+
ReadOperationLimits(context_.get());
|
|
780
|
+
context_->refresh_operation_limits.store(false);
|
|
781
|
+
client_id_ = next_client_id.fetch_add(1);
|
|
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_;
|
|
786
|
+
}
|
|
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
|
+
ConnectionOptions options_;
|
|
794
|
+
std::shared_ptr<ClientContext> context_;
|
|
795
|
+
Napi::ThreadSafeFunction state_callback_;
|
|
796
|
+
bool has_state_callback_ = false;
|
|
797
|
+
bool transferred_ = false;
|
|
798
|
+
uint64_t client_id_ = 0;
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
class DisconnectWorker : public PromiseWorker {
|
|
802
|
+
public:
|
|
803
|
+
DisconnectWorker(Napi::Promise::Deferred deferred, uint64_t client_id)
|
|
804
|
+
: PromiseWorker(deferred), client_id_(client_id) {}
|
|
805
|
+
void Execute() override { CloseClient(client_id_); }
|
|
806
|
+
void OnOK() override { deferred_.Resolve(Env().Undefined()); }
|
|
807
|
+
private:
|
|
808
|
+
uint64_t client_id_;
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
class ReadManyWorker : public PromiseWorker {
|
|
812
|
+
public:
|
|
813
|
+
ReadManyWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
814
|
+
std::vector<std::string> node_ids)
|
|
815
|
+
: PromiseWorker(deferred), context_(std::move(context)), node_ids_(std::move(node_ids)) {
|
|
816
|
+
UA_ReadResponse_init(&response_);
|
|
817
|
+
}
|
|
818
|
+
~ReadManyWorker() override { UA_ReadResponse_clear(&response_); }
|
|
819
|
+
void Execute() override {
|
|
820
|
+
UA_ReadRequest request;
|
|
821
|
+
UA_ReadRequest_init(&request);
|
|
822
|
+
request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
|
|
823
|
+
request.nodesToReadSize = node_ids_.size();
|
|
824
|
+
request.nodesToRead = static_cast<UA_ReadValueId*>(UA_Array_new(node_ids_.size(), &UA_TYPES[UA_TYPES_READVALUEID]));
|
|
825
|
+
if (!request.nodesToRead) { SetError("Failed to allocate batch read request"); return; }
|
|
826
|
+
for (size_t index = 0; index < node_ids_.size(); ++index) {
|
|
827
|
+
if (!CopyCachedNodeId(context_.get(), node_ids_[index], &request.nodesToRead[index].nodeId)) {
|
|
828
|
+
UA_ReadRequest_clear(&request); SetError("Invalid NodeId: " + node_ids_[index]); return;
|
|
829
|
+
}
|
|
830
|
+
request.nodesToRead[index].attributeId = UA_ATTRIBUTEID_VALUE;
|
|
831
|
+
}
|
|
832
|
+
{
|
|
833
|
+
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
834
|
+
if (!context_->client) { UA_ReadRequest_clear(&request); SetError("Client disconnected"); return; }
|
|
835
|
+
response_ = UA_Client_Service_read(context_->client, request);
|
|
836
|
+
}
|
|
837
|
+
UA_ReadRequest_clear(&request);
|
|
838
|
+
if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) {
|
|
839
|
+
SetError("Batch read failed: " + StatusName(response_.responseHeader.serviceResult));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
void OnOK() override {
|
|
843
|
+
Napi::Array output = Napi::Array::New(Env(), node_ids_.size());
|
|
844
|
+
for (size_t index = 0; index < node_ids_.size(); ++index) {
|
|
845
|
+
Napi::Object item = Napi::Object::New(Env());
|
|
846
|
+
item.Set("nodeId", node_ids_[index]);
|
|
847
|
+
if (index < response_.resultsSize) {
|
|
848
|
+
const UA_DataValue& value = response_.results[index];
|
|
849
|
+
item.Set("statusCode", StatusName(value.status));
|
|
850
|
+
if (value.hasValue) { item.Set("value", VariantValue(Env(), &value.value)); item.Set("dataType", DataTypeName(value.value.type)); }
|
|
851
|
+
if (value.hasSourceTimestamp) item.Set("sourceTimestamp", DateTimeValue(Env(), value.sourceTimestamp));
|
|
852
|
+
if (value.hasServerTimestamp) item.Set("serverTimestamp", DateTimeValue(Env(), value.serverTimestamp));
|
|
853
|
+
} else item.Set("statusCode", "BadUnexpectedError");
|
|
854
|
+
output.Set(index, item);
|
|
855
|
+
}
|
|
856
|
+
deferred_.Resolve(output);
|
|
857
|
+
}
|
|
858
|
+
private:
|
|
859
|
+
std::shared_ptr<ClientContext> context_;
|
|
860
|
+
std::vector<std::string> node_ids_;
|
|
861
|
+
UA_ReadResponse response_;
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
struct WriteInput {
|
|
865
|
+
std::string node_id;
|
|
866
|
+
std::string data_type;
|
|
867
|
+
UA_Variant value;
|
|
868
|
+
WriteInput() { UA_Variant_init(&value); }
|
|
869
|
+
~WriteInput() { UA_Variant_clear(&value); }
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
class WriteManyWorker : public PromiseWorker {
|
|
873
|
+
public:
|
|
874
|
+
WriteManyWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
875
|
+
std::vector<std::unique_ptr<WriteInput>> inputs)
|
|
876
|
+
: PromiseWorker(deferred), context_(std::move(context)), inputs_(std::move(inputs)) {
|
|
877
|
+
UA_WriteResponse_init(&response_);
|
|
878
|
+
}
|
|
879
|
+
~WriteManyWorker() override { UA_WriteResponse_clear(&response_); }
|
|
880
|
+
void Execute() override {
|
|
881
|
+
UA_WriteRequest request;
|
|
882
|
+
UA_WriteRequest_init(&request);
|
|
883
|
+
request.nodesToWriteSize = inputs_.size();
|
|
884
|
+
request.nodesToWrite = static_cast<UA_WriteValue*>(UA_Array_new(inputs_.size(), &UA_TYPES[UA_TYPES_WRITEVALUE]));
|
|
885
|
+
if (!request.nodesToWrite) { SetError("Failed to allocate batch write request"); return; }
|
|
886
|
+
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
887
|
+
UA_WriteValue& target = request.nodesToWrite[index];
|
|
888
|
+
if (!CopyCachedNodeId(context_.get(), inputs_[index]->node_id, &target.nodeId)) {
|
|
889
|
+
UA_WriteRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index]->node_id); return;
|
|
890
|
+
}
|
|
891
|
+
target.attributeId = UA_ATTRIBUTEID_VALUE;
|
|
892
|
+
target.value.hasValue = true;
|
|
893
|
+
UA_Variant_copy(&inputs_[index]->value, &target.value.value);
|
|
894
|
+
}
|
|
895
|
+
{
|
|
896
|
+
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
897
|
+
if (!context_->client) { UA_WriteRequest_clear(&request); SetError("Client disconnected"); return; }
|
|
898
|
+
response_ = UA_Client_Service_write(context_->client, request);
|
|
899
|
+
}
|
|
900
|
+
UA_WriteRequest_clear(&request);
|
|
901
|
+
if (!UA_StatusCode_isGood(response_.responseHeader.serviceResult)) {
|
|
902
|
+
SetError("Batch write failed: " + StatusName(response_.responseHeader.serviceResult));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
void OnOK() override {
|
|
906
|
+
Napi::Array output = Napi::Array::New(Env(), inputs_.size());
|
|
907
|
+
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
908
|
+
Napi::Object item = Napi::Object::New(Env());
|
|
909
|
+
item.Set("nodeId", inputs_[index]->node_id);
|
|
910
|
+
item.Set("statusCode", index < response_.resultsSize ? StatusName(response_.results[index]) : "BadUnexpectedError");
|
|
911
|
+
output.Set(index, item);
|
|
912
|
+
}
|
|
913
|
+
deferred_.Resolve(output);
|
|
914
|
+
}
|
|
915
|
+
private:
|
|
916
|
+
std::shared_ptr<ClientContext> context_;
|
|
917
|
+
std::vector<std::unique_ptr<WriteInput>> inputs_;
|
|
918
|
+
UA_WriteResponse response_;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
struct BrowsePoint {
|
|
922
|
+
std::string node_id;
|
|
923
|
+
std::string parent_node_id;
|
|
924
|
+
std::string browse_name;
|
|
925
|
+
std::string display_name;
|
|
926
|
+
std::string browse_path;
|
|
927
|
+
std::string type_definition;
|
|
928
|
+
uint32_t node_class = 0;
|
|
929
|
+
std::string data_type;
|
|
930
|
+
int32_t value_rank = -2;
|
|
931
|
+
uint8_t access_level = 0;
|
|
932
|
+
uint8_t user_access_level = 0;
|
|
933
|
+
double minimum_sampling_interval = 0;
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
struct BrowseOptions {
|
|
937
|
+
std::string root_node_id = "ns=0;i=85";
|
|
938
|
+
bool recursive = true;
|
|
939
|
+
bool variables_only = true;
|
|
940
|
+
uint32_t max_depth = 10;
|
|
941
|
+
uint32_t max_nodes = 10000;
|
|
942
|
+
uint32_t attribute_batch_size = 200;
|
|
943
|
+
std::set<uint16_t> namespaces;
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
class BrowseWorker : public PromiseWorker {
|
|
947
|
+
public:
|
|
948
|
+
BrowseWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
949
|
+
BrowseOptions options)
|
|
950
|
+
: PromiseWorker(deferred), context_(std::move(context)), options_(std::move(options)) {}
|
|
951
|
+
void Execute() override {
|
|
952
|
+
UA_NodeId root;
|
|
953
|
+
UA_NodeId_init(&root);
|
|
954
|
+
if (!CopyCachedNodeId(context_.get(), options_.root_node_id, &root)) { SetError("Invalid root NodeId: " + options_.root_node_id); return; }
|
|
955
|
+
UA_NodeId_clear(&root);
|
|
956
|
+
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
957
|
+
if (!context_->client) { SetError("Client disconnected"); return; }
|
|
958
|
+
if (!BrowseTree()) return;
|
|
959
|
+
ReadAttributes();
|
|
960
|
+
}
|
|
961
|
+
void OnOK() override {
|
|
962
|
+
Napi::Array output = Napi::Array::New(Env(), points_.size());
|
|
963
|
+
for (size_t index = 0; index < points_.size(); ++index) {
|
|
964
|
+
const BrowsePoint& point = points_[index];
|
|
965
|
+
Napi::Object item = Napi::Object::New(Env());
|
|
966
|
+
item.Set("nodeId", point.node_id); item.Set("parentNodeId", point.parent_node_id);
|
|
967
|
+
item.Set("browseName", point.browse_name); item.Set("displayName", point.display_name);
|
|
968
|
+
item.Set("browsePath", point.browse_path); item.Set("nodeClass", point.node_class == UA_NODECLASS_VARIABLE ? "Variable" : "Object");
|
|
969
|
+
item.Set("typeDefinition", point.type_definition); item.Set("dataType", point.data_type);
|
|
970
|
+
item.Set("valueRank", point.value_rank); item.Set("accessLevel", point.access_level);
|
|
971
|
+
item.Set("userAccessLevel", point.user_access_level); item.Set("minimumSamplingInterval", point.minimum_sampling_interval);
|
|
972
|
+
output.Set(index, item);
|
|
973
|
+
}
|
|
974
|
+
deferred_.Resolve(output);
|
|
975
|
+
}
|
|
976
|
+
private:
|
|
977
|
+
struct QueueItem { std::string node_id; std::string path; uint32_t depth; };
|
|
978
|
+
|
|
979
|
+
bool NamespaceAllowed(uint16_t value) const { return options_.namespaces.empty() || options_.namespaces.count(value) > 0; }
|
|
980
|
+
|
|
981
|
+
void ProcessBrowseResult(const QueueItem& parent, const UA_BrowseResult& result,
|
|
982
|
+
std::deque<QueueItem>* queue, std::set<std::string>* visited) {
|
|
983
|
+
for (size_t index = 0; index < result.referencesSize && points_.size() < options_.max_nodes; ++index) {
|
|
984
|
+
const UA_ReferenceDescription& reference = result.references[index];
|
|
985
|
+
if (reference.nodeId.serverIndex != 0 || reference.nodeId.namespaceUri.length != 0) continue;
|
|
986
|
+
const UA_NodeId& node_id = reference.nodeId.nodeId;
|
|
987
|
+
std::string node_id_text = NodeIdString(node_id);
|
|
988
|
+
if (node_id_text.empty()) continue;
|
|
989
|
+
std::string name = LocalizedTextString(reference.displayName);
|
|
990
|
+
if (name.empty()) name = QualifiedNameString(reference.browseName);
|
|
991
|
+
std::string path = parent.path.empty() ? name : parent.path + "/" + name;
|
|
992
|
+
bool output = !options_.variables_only || reference.nodeClass == UA_NODECLASS_VARIABLE;
|
|
993
|
+
if (output && NamespaceAllowed(node_id.namespaceIndex)) {
|
|
994
|
+
BrowsePoint point;
|
|
995
|
+
point.node_id = node_id_text; point.parent_node_id = parent.node_id;
|
|
996
|
+
point.browse_name = QualifiedNameString(reference.browseName);
|
|
997
|
+
point.display_name = LocalizedTextString(reference.displayName); point.browse_path = path;
|
|
998
|
+
point.node_class = reference.nodeClass; point.type_definition = NodeIdString(reference.typeDefinition.nodeId);
|
|
999
|
+
points_.push_back(std::move(point));
|
|
1000
|
+
}
|
|
1001
|
+
if (options_.recursive && parent.depth < options_.max_depth && visited->insert(node_id_text).second) {
|
|
1002
|
+
queue->push_back({node_id_text, path, parent.depth + 1});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
bool BrowseTree() {
|
|
1008
|
+
std::deque<QueueItem> queue{{options_.root_node_id, "", 0}};
|
|
1009
|
+
std::set<std::string> visited{options_.root_node_id};
|
|
1010
|
+
while (!queue.empty() && points_.size() < options_.max_nodes) {
|
|
1011
|
+
QueueItem current = std::move(queue.front()); queue.pop_front();
|
|
1012
|
+
UA_BrowseRequest request; UA_BrowseRequest_init(&request);
|
|
1013
|
+
request.requestedMaxReferencesPerNode = 1000;
|
|
1014
|
+
request.nodesToBrowseSize = 1;
|
|
1015
|
+
request.nodesToBrowse = static_cast<UA_BrowseDescription*>(UA_Array_new(1, &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]));
|
|
1016
|
+
if (!request.nodesToBrowse || !CopyCachedNodeId(context_.get(), current.node_id, &request.nodesToBrowse[0].nodeId)) {
|
|
1017
|
+
UA_BrowseRequest_clear(&request); SetError("Invalid browse NodeId: " + current.node_id); return false;
|
|
1018
|
+
}
|
|
1019
|
+
auto& description = request.nodesToBrowse[0];
|
|
1020
|
+
description.browseDirection = UA_BROWSEDIRECTION_FORWARD;
|
|
1021
|
+
description.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HIERARCHICALREFERENCES);
|
|
1022
|
+
description.includeSubtypes = true;
|
|
1023
|
+
description.nodeClassMask = UA_NODECLASS_OBJECT | UA_NODECLASS_VARIABLE;
|
|
1024
|
+
description.resultMask = UA_BROWSERESULTMASK_ALL;
|
|
1025
|
+
UA_BrowseResponse response = UA_Client_Service_browse(context_->client, request);
|
|
1026
|
+
UA_BrowseRequest_clear(&request);
|
|
1027
|
+
if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
|
|
1028
|
+
UA_BrowseResponse_clear(&response); SetError("Browse failed: " + StatusName(response.responseHeader.serviceResult)); return false;
|
|
1029
|
+
}
|
|
1030
|
+
if (response.resultsSize) {
|
|
1031
|
+
ProcessBrowseResult(current, response.results[0], &queue, &visited);
|
|
1032
|
+
UA_ByteString continuation = UA_BYTESTRING_NULL;
|
|
1033
|
+
UA_ByteString_copy(&response.results[0].continuationPoint, &continuation);
|
|
1034
|
+
while (continuation.length && points_.size() < options_.max_nodes) {
|
|
1035
|
+
UA_BrowseNextRequest next; UA_BrowseNextRequest_init(&next);
|
|
1036
|
+
next.continuationPointsSize = 1;
|
|
1037
|
+
next.continuationPoints = static_cast<UA_ByteString*>(UA_Array_new(1, &UA_TYPES[UA_TYPES_BYTESTRING]));
|
|
1038
|
+
UA_ByteString_copy(&continuation, &next.continuationPoints[0]);
|
|
1039
|
+
UA_ByteString_clear(&continuation);
|
|
1040
|
+
UA_BrowseNextResponse next_response = UA_Client_Service_browseNext(context_->client, next);
|
|
1041
|
+
UA_BrowseNextRequest_clear(&next);
|
|
1042
|
+
if (!UA_StatusCode_isGood(next_response.responseHeader.serviceResult) || !next_response.resultsSize) {
|
|
1043
|
+
UA_BrowseNextResponse_clear(&next_response); break;
|
|
1044
|
+
}
|
|
1045
|
+
ProcessBrowseResult(current, next_response.results[0], &queue, &visited);
|
|
1046
|
+
UA_ByteString_copy(&next_response.results[0].continuationPoint, &continuation);
|
|
1047
|
+
UA_BrowseNextResponse_clear(&next_response);
|
|
1048
|
+
}
|
|
1049
|
+
UA_ByteString_clear(&continuation);
|
|
1050
|
+
}
|
|
1051
|
+
UA_BrowseResponse_clear(&response);
|
|
1052
|
+
}
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
void ReadAttributes() {
|
|
1057
|
+
const size_t attrs = 5;
|
|
1058
|
+
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;
|
|
1060
|
+
if (read_limit > 0) batch = std::min<size_t>(batch, read_limit);
|
|
1061
|
+
const size_t total_attributes = points_.size() * attrs;
|
|
1062
|
+
const UA_UInt32 ids[attrs] = {
|
|
1063
|
+
UA_ATTRIBUTEID_DATATYPE, UA_ATTRIBUTEID_VALUERANK,
|
|
1064
|
+
UA_ATTRIBUTEID_ACCESSLEVEL, UA_ATTRIBUTEID_USERACCESSLEVEL,
|
|
1065
|
+
UA_ATTRIBUTEID_MINIMUMSAMPLINGINTERVAL};
|
|
1066
|
+
for (size_t start = 0; start < total_attributes; start += batch) {
|
|
1067
|
+
size_t count = std::min(batch, total_attributes - start);
|
|
1068
|
+
UA_ReadRequest request; UA_ReadRequest_init(&request);
|
|
1069
|
+
request.timestampsToReturn = UA_TIMESTAMPSTORETURN_NEITHER;
|
|
1070
|
+
request.nodesToReadSize = count;
|
|
1071
|
+
request.nodesToRead = static_cast<UA_ReadValueId*>(UA_Array_new(request.nodesToReadSize, &UA_TYPES[UA_TYPES_READVALUEID]));
|
|
1072
|
+
if (!request.nodesToRead) return;
|
|
1073
|
+
for (size_t index = 0; index < count; ++index) {
|
|
1074
|
+
size_t flat_index = start + index;
|
|
1075
|
+
size_t point_index = flat_index / attrs;
|
|
1076
|
+
size_t attr_index = flat_index % attrs;
|
|
1077
|
+
auto& target = request.nodesToRead[index];
|
|
1078
|
+
CopyCachedNodeId(context_.get(), points_[point_index].node_id, &target.nodeId);
|
|
1079
|
+
target.attributeId = ids[attr_index];
|
|
1080
|
+
}
|
|
1081
|
+
UA_ReadResponse response = UA_Client_Service_read(context_->client, request);
|
|
1082
|
+
UA_ReadRequest_clear(&request);
|
|
1083
|
+
if (UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
|
|
1084
|
+
for (size_t index = 0; index < count && index < response.resultsSize; ++index) {
|
|
1085
|
+
size_t flat_index = start + index;
|
|
1086
|
+
BrowsePoint& point = points_[flat_index / attrs];
|
|
1087
|
+
size_t attr_index = flat_index % attrs;
|
|
1088
|
+
const auto& value = response.results[index];
|
|
1089
|
+
if (!value.hasValue) continue;
|
|
1090
|
+
if (attr_index == 0 && value.value.type == &UA_TYPES[UA_TYPES_NODEID]) {
|
|
1091
|
+
point.data_type = BuiltinTypeName(*static_cast<UA_NodeId*>(value.value.data));
|
|
1092
|
+
} else if (attr_index == 1 && value.value.type == &UA_TYPES[UA_TYPES_INT32]) {
|
|
1093
|
+
point.value_rank = *static_cast<UA_Int32*>(value.value.data);
|
|
1094
|
+
} else if (attr_index == 2 && value.value.type == &UA_TYPES[UA_TYPES_BYTE]) {
|
|
1095
|
+
point.access_level = *static_cast<UA_Byte*>(value.value.data);
|
|
1096
|
+
} else if (attr_index == 3 && value.value.type == &UA_TYPES[UA_TYPES_BYTE]) {
|
|
1097
|
+
point.user_access_level = *static_cast<UA_Byte*>(value.value.data);
|
|
1098
|
+
} else if (attr_index == 4 && value.value.type == &UA_TYPES[UA_TYPES_DOUBLE]) {
|
|
1099
|
+
point.minimum_sampling_interval = *static_cast<UA_Double*>(value.value.data);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
UA_ReadResponse_clear(&response);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
std::shared_ptr<ClientContext> context_;
|
|
1108
|
+
BrowseOptions options_;
|
|
1109
|
+
std::vector<BrowsePoint> points_;
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
class CreateSubscriptionWorker : public PromiseWorker {
|
|
1113
|
+
public:
|
|
1114
|
+
CreateSubscriptionWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
1115
|
+
Napi::Function callback, uint32_t queue_size, double publishing_interval,
|
|
1116
|
+
uint32_t lifetime, uint32_t keepalive, uint32_t max_notifications,
|
|
1117
|
+
uint32_t callback_batch_size, std::string queue_mode,
|
|
1118
|
+
std::string overflow_policy)
|
|
1119
|
+
: PromiseWorker(deferred), context_(std::move(context)), publishing_interval_(publishing_interval),
|
|
1120
|
+
lifetime_(lifetime), keepalive_(keepalive), max_notifications_(max_notifications),
|
|
1121
|
+
queue_size_(std::max<uint32_t>(1, queue_size)),
|
|
1122
|
+
callback_batch_size_(std::max<uint32_t>(1, callback_batch_size)),
|
|
1123
|
+
queue_mode_(queue_mode == "latest" ? "latest" : "all"),
|
|
1124
|
+
overflow_policy_(overflow_policy == "drop-newest" ? "drop-newest" : "drop-oldest") {
|
|
1125
|
+
callback_ = Napi::ThreadSafeFunction::New(Env(), callback, "OpcUaDataChangeBatches", 1, 1);
|
|
1126
|
+
}
|
|
1127
|
+
void Execute() override {
|
|
1128
|
+
UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default();
|
|
1129
|
+
request.requestedPublishingInterval = publishing_interval_;
|
|
1130
|
+
request.requestedLifetimeCount = lifetime_;
|
|
1131
|
+
request.requestedMaxKeepAliveCount = keepalive_;
|
|
1132
|
+
request.maxNotificationsPerPublish = max_notifications_;
|
|
1133
|
+
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
1134
|
+
if (!context_->client) { SetError("Client disconnected"); return; }
|
|
1135
|
+
UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(context_->client, request, nullptr, nullptr, nullptr);
|
|
1136
|
+
status_ = response.responseHeader.serviceResult; subscription_id_ = response.subscriptionId;
|
|
1137
|
+
UA_CreateSubscriptionResponse_clear(&response);
|
|
1138
|
+
if (!UA_StatusCode_isGood(status_)) { SetError("Create subscription failed: " + StatusName(status_)); return; }
|
|
1139
|
+
auto subscription = std::make_unique<SubscriptionContext>();
|
|
1140
|
+
subscription->subscription_id = subscription_id_;
|
|
1141
|
+
subscription->notifications = std::make_shared<NotificationQueue>();
|
|
1142
|
+
subscription->notifications->callback = callback_;
|
|
1143
|
+
subscription->notifications->capacity = queue_size_;
|
|
1144
|
+
subscription->notifications->batch_size = callback_batch_size_;
|
|
1145
|
+
subscription->notifications->mode = queue_mode_;
|
|
1146
|
+
subscription->notifications->overflow_policy = overflow_policy_;
|
|
1147
|
+
context_->subscriptions[subscription_id_] = std::move(subscription); transferred_ = true;
|
|
1148
|
+
}
|
|
1149
|
+
void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
|
|
1150
|
+
void OnError(const Napi::Error& error) override { if (!transferred_) callback_.Release(); PromiseWorker::OnError(error); }
|
|
1151
|
+
private:
|
|
1152
|
+
std::shared_ptr<ClientContext> context_; Napi::ThreadSafeFunction callback_; bool transferred_ = false;
|
|
1153
|
+
double publishing_interval_; uint32_t lifetime_; uint32_t keepalive_; uint32_t max_notifications_;
|
|
1154
|
+
uint32_t queue_size_; uint32_t callback_batch_size_; std::string queue_mode_; std::string overflow_policy_;
|
|
1155
|
+
uint32_t subscription_id_ = 0; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
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; };
|
|
1160
|
+
|
|
1161
|
+
class AddMonitoredWorker : public PromiseWorker {
|
|
1162
|
+
public:
|
|
1163
|
+
AddMonitoredWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
1164
|
+
uint32_t subscription_id, std::vector<MonitorInput> inputs)
|
|
1165
|
+
: PromiseWorker(deferred), context_(std::move(context)), subscription_id_(subscription_id), inputs_(std::move(inputs)) {}
|
|
1166
|
+
void Execute() override {
|
|
1167
|
+
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
1168
|
+
auto subscription_it = context_->subscriptions.find(subscription_id_);
|
|
1169
|
+
if (!context_->client || subscription_it == context_->subscriptions.end()) { SetError("Subscription not found"); return; }
|
|
1170
|
+
auto* subscription = subscription_it->second.get();
|
|
1171
|
+
UA_CreateMonitoredItemsRequest request; UA_CreateMonitoredItemsRequest_init(&request);
|
|
1172
|
+
request.subscriptionId = subscription_id_; request.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
|
|
1173
|
+
request.itemsToCreateSize = inputs_.size();
|
|
1174
|
+
request.itemsToCreate = static_cast<UA_MonitoredItemCreateRequest*>(UA_Array_new(inputs_.size(), &UA_TYPES[UA_TYPES_MONITOREDITEMCREATEREQUEST]));
|
|
1175
|
+
std::vector<std::unique_ptr<MonitoredContext>> contexts(inputs_.size());
|
|
1176
|
+
std::vector<void*> context_ptrs(inputs_.size());
|
|
1177
|
+
std::vector<UA_Client_DataChangeNotificationCallback> callbacks(inputs_.size(), DataChange);
|
|
1178
|
+
std::vector<std::unique_ptr<UA_DataChangeFilter>> filters(inputs_.size());
|
|
1179
|
+
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
1180
|
+
auto& item = request.itemsToCreate[index];
|
|
1181
|
+
if (!CopyCachedNodeId(context_.get(), inputs_[index].node_id, &item.itemToMonitor.nodeId)) { UA_CreateMonitoredItemsRequest_clear(&request); SetError("Invalid NodeId: " + inputs_[index].node_id); return; }
|
|
1182
|
+
item.itemToMonitor.attributeId = UA_ATTRIBUTEID_VALUE; item.monitoringMode = UA_MONITORINGMODE_REPORTING;
|
|
1183
|
+
item.requestedParameters.clientHandle = static_cast<uint32_t>(index + 1);
|
|
1184
|
+
item.requestedParameters.samplingInterval = inputs_[index].sampling_interval;
|
|
1185
|
+
item.requestedParameters.queueSize = inputs_[index].queue_size;
|
|
1186
|
+
item.requestedParameters.discardOldest = inputs_[index].discard_oldest;
|
|
1187
|
+
if (inputs_[index].deadband_type != "none") {
|
|
1188
|
+
filters[index] = std::make_unique<UA_DataChangeFilter>(); UA_DataChangeFilter_init(filters[index].get());
|
|
1189
|
+
filters[index]->trigger = UA_DATACHANGETRIGGER_STATUSVALUE;
|
|
1190
|
+
filters[index]->deadbandType = inputs_[index].deadband_type == "percent" ? UA_DEADBANDTYPE_PERCENT : UA_DEADBANDTYPE_ABSOLUTE;
|
|
1191
|
+
filters[index]->deadbandValue = inputs_[index].deadband_value;
|
|
1192
|
+
item.requestedParameters.filter.encoding = UA_EXTENSIONOBJECT_DECODED;
|
|
1193
|
+
item.requestedParameters.filter.content.decoded.type = &UA_TYPES[UA_TYPES_DATACHANGEFILTER];
|
|
1194
|
+
item.requestedParameters.filter.content.decoded.data = filters[index].get();
|
|
1195
|
+
}
|
|
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();
|
|
1197
|
+
}
|
|
1198
|
+
UA_CreateMonitoredItemsResponse response = UA_Client_MonitoredItems_createDataChanges(context_->client, request, context_ptrs.data(), callbacks.data(), nullptr);
|
|
1199
|
+
for (size_t index = 0; index < filters.size(); ++index) {
|
|
1200
|
+
if (filters[index]) UA_ExtensionObject_init(&request.itemsToCreate[index].requestedParameters.filter);
|
|
1201
|
+
}
|
|
1202
|
+
UA_CreateMonitoredItemsRequest_clear(&request);
|
|
1203
|
+
if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) { UA_CreateMonitoredItemsResponse_clear(&response); SetError("Create monitored items failed: " + StatusName(response.responseHeader.serviceResult)); return; }
|
|
1204
|
+
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
1205
|
+
MonitorResult result; result.node_id = inputs_[index].node_id;
|
|
1206
|
+
if (index < response.resultsSize) {
|
|
1207
|
+
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]); }
|
|
1209
|
+
}
|
|
1210
|
+
results_.push_back(result);
|
|
1211
|
+
}
|
|
1212
|
+
UA_CreateMonitoredItemsResponse_clear(&response);
|
|
1213
|
+
}
|
|
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); }
|
|
1215
|
+
private:
|
|
1216
|
+
std::shared_ptr<ClientContext> context_; uint32_t subscription_id_; std::vector<MonitorInput> inputs_; std::vector<MonitorResult> results_;
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
class RemoveMonitoredWorker : public PromiseWorker {
|
|
1220
|
+
public:
|
|
1221
|
+
RemoveMonitoredWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context,
|
|
1222
|
+
uint32_t subscription_id, std::vector<uint32_t> ids)
|
|
1223
|
+
: PromiseWorker(deferred), context_(std::move(context)), subscription_id_(subscription_id), ids_(std::move(ids)) { UA_DeleteMonitoredItemsResponse_init(&response_); }
|
|
1224
|
+
~RemoveMonitoredWorker() override { UA_DeleteMonitoredItemsResponse_clear(&response_); }
|
|
1225
|
+
void Execute() override {
|
|
1226
|
+
std::lock_guard<std::mutex> lock(context_->mutex); auto sub = context_->subscriptions.find(subscription_id_);
|
|
1227
|
+
if (!context_->client || sub == context_->subscriptions.end()) { SetError("Subscription not found"); return; }
|
|
1228
|
+
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
|
+
std::copy(ids_.begin(), ids_.end(), request.monitoredItemIds); response_ = UA_Client_MonitoredItems_delete(context_->client, request); UA_DeleteMonitoredItemsRequest_clear(&request);
|
|
1230
|
+
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.erase(ids_[index]);
|
|
1232
|
+
}
|
|
1233
|
+
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
|
+
private:
|
|
1235
|
+
std::shared_ptr<ClientContext> context_; uint32_t subscription_id_; std::vector<uint32_t> ids_; UA_DeleteMonitoredItemsResponse response_;
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
class ValidateSubscriptionWorker : public PromiseWorker {
|
|
1239
|
+
public:
|
|
1240
|
+
ValidateSubscriptionWorker(Napi::Promise::Deferred deferred,
|
|
1241
|
+
std::shared_ptr<ClientContext> context, uint32_t id)
|
|
1242
|
+
: PromiseWorker(deferred), context_(std::move(context)), id_(id) {}
|
|
1243
|
+
void Execute() override {
|
|
1244
|
+
std::lock_guard<std::mutex> lock(context_->mutex);
|
|
1245
|
+
if (!context_->client || !context_->connected.load()) {
|
|
1246
|
+
SetError("Client reconnecting");
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (context_->subscriptions.find(id_) == context_->subscriptions.end()) {
|
|
1250
|
+
status_ = UA_STATUSCODE_BADSUBSCRIPTIONIDINVALID;
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
UA_SetPublishingModeRequest request;
|
|
1254
|
+
UA_SetPublishingModeRequest_init(&request);
|
|
1255
|
+
request.publishingEnabled = true;
|
|
1256
|
+
request.subscriptionIdsSize = 1;
|
|
1257
|
+
request.subscriptionIds = static_cast<UA_UInt32*>(
|
|
1258
|
+
UA_Array_new(1, &UA_TYPES[UA_TYPES_UINT32]));
|
|
1259
|
+
if (!request.subscriptionIds) {
|
|
1260
|
+
SetError("Failed to allocate subscription validation request");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
request.subscriptionIds[0] = id_;
|
|
1264
|
+
UA_SetPublishingModeResponse response =
|
|
1265
|
+
UA_Client_Subscriptions_setPublishingMode(context_->client, request);
|
|
1266
|
+
UA_SetPublishingModeRequest_clear(&request);
|
|
1267
|
+
if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
|
|
1268
|
+
UA_StatusCode service_status = response.responseHeader.serviceResult;
|
|
1269
|
+
UA_SetPublishingModeResponse_clear(&response);
|
|
1270
|
+
SetError("Validate subscription failed: " + StatusName(service_status));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
status_ = response.resultsSize ? response.results[0] : UA_STATUSCODE_BADUNEXPECTEDERROR;
|
|
1274
|
+
UA_SetPublishingModeResponse_clear(&response);
|
|
1275
|
+
}
|
|
1276
|
+
void OnOK() override {
|
|
1277
|
+
Napi::Object output = Napi::Object::New(Env());
|
|
1278
|
+
output.Set("subscriptionId", id_);
|
|
1279
|
+
output.Set("valid", UA_StatusCode_isGood(status_));
|
|
1280
|
+
output.Set("statusCode", StatusName(status_));
|
|
1281
|
+
deferred_.Resolve(output);
|
|
1282
|
+
}
|
|
1283
|
+
private:
|
|
1284
|
+
std::shared_ptr<ClientContext> context_;
|
|
1285
|
+
uint32_t id_;
|
|
1286
|
+
UA_StatusCode status_ = UA_STATUSCODE_GOOD;
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
class DeleteSubscriptionWorker : public PromiseWorker {
|
|
1290
|
+
public:
|
|
1291
|
+
DeleteSubscriptionWorker(Napi::Promise::Deferred deferred, std::shared_ptr<ClientContext> context, uint32_t id)
|
|
1292
|
+
: PromiseWorker(deferred), context_(std::move(context)), id_(id) {}
|
|
1293
|
+
void Execute() override { std::lock_guard<std::mutex> lock(context_->mutex); 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
|
+
void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
|
|
1295
|
+
private:
|
|
1296
|
+
std::shared_ptr<ClientContext> context_; uint32_t id_; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
std::shared_ptr<ClientContext> RequireClient(Napi::Env env, const Napi::Value& value) {
|
|
1300
|
+
if (!value.IsNumber()) { Napi::TypeError::New(env, "clientId must be a number").ThrowAsJavaScriptException(); return nullptr; }
|
|
1301
|
+
auto context = FindClient(static_cast<uint64_t>(value.As<Napi::Number>().Int64Value()));
|
|
1302
|
+
if (!context) Napi::Error::New(env, "OPC UA client not found").ThrowAsJavaScriptException();
|
|
1303
|
+
return context;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
Napi::Value GetClientDiagnostics(const Napi::CallbackInfo& info) {
|
|
1307
|
+
Napi::Env env = info.Env();
|
|
1308
|
+
auto context = RequireClient(env, info[0]);
|
|
1309
|
+
if (!context) return env.Undefined();
|
|
1310
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1311
|
+
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);
|
|
1316
|
+
result.Set("operationLimits", limits);
|
|
1317
|
+
|
|
1318
|
+
Napi::Object connection = Napi::Object::New(env);
|
|
1319
|
+
connection.Set("state", context->connected.load() ? "connected" :
|
|
1320
|
+
(UA_StatusCode_isGood(context->connect_status.load()) ? "reconnecting" : "failed"));
|
|
1321
|
+
connection.Set("connected", context->connected.load());
|
|
1322
|
+
connection.Set("channelState", ChannelStateName(context->channel_state.load()));
|
|
1323
|
+
connection.Set("sessionState", SessionStateName(context->session_state.load()));
|
|
1324
|
+
connection.Set("statusCode", StatusName(context->connect_status.load()));
|
|
1325
|
+
connection.Set("stateSequence", Napi::Number::New(env, static_cast<double>(context->state_sequence.load())));
|
|
1326
|
+
connection.Set("reconnectCount", Napi::Number::New(env, static_cast<double>(context->reconnect_count.load())));
|
|
1327
|
+
connection.Set("disconnectCount", Napi::Number::New(env, static_cast<double>(context->disconnect_count.load())));
|
|
1328
|
+
connection.Set("sessionGeneration", Napi::Number::New(env, static_cast<double>(context->session_generation.load())));
|
|
1329
|
+
result.Set("connection", connection);
|
|
1330
|
+
|
|
1331
|
+
size_t cache_size = 0;
|
|
1332
|
+
{
|
|
1333
|
+
std::lock_guard<std::mutex> lock(context->node_id_cache_mutex);
|
|
1334
|
+
cache_size = context->node_id_cache.size();
|
|
1335
|
+
}
|
|
1336
|
+
Napi::Object cache = Napi::Object::New(env);
|
|
1337
|
+
cache.Set("size", Napi::Number::New(env, static_cast<double>(cache_size)));
|
|
1338
|
+
cache.Set("capacity", Napi::Number::New(env, static_cast<double>(context->node_id_cache_capacity)));
|
|
1339
|
+
cache.Set("hits", Napi::Number::New(env, static_cast<double>(context->node_id_cache_hits.load())));
|
|
1340
|
+
cache.Set("misses", Napi::Number::New(env, static_cast<double>(context->node_id_cache_misses.load())));
|
|
1341
|
+
cache.Set("bypasses", Napi::Number::New(env, static_cast<double>(context->node_id_cache_bypasses.load())));
|
|
1342
|
+
cache.Set("evictions", Napi::Number::New(env, static_cast<double>(context->node_id_cache_evictions.load())));
|
|
1343
|
+
result.Set("nodeIdCache", cache);
|
|
1344
|
+
return result;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
Napi::Value ConnectAsync(const Napi::CallbackInfo& info) {
|
|
1348
|
+
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
|
+
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);
|
|
1351
|
+
bool has_state_callback = info.Length() > 1 && info[1].IsFunction();
|
|
1352
|
+
Napi::ThreadSafeFunction state_callback;
|
|
1353
|
+
if (has_state_callback) {
|
|
1354
|
+
state_callback = Napi::ThreadSafeFunction::New(
|
|
1355
|
+
env, info[1].As<Napi::Function>(), "OPC UA connection state", 64, 1);
|
|
1356
|
+
}
|
|
1357
|
+
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
|
+
}
|
|
1359
|
+
|
|
1360
|
+
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
|
+
|
|
1362
|
+
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<std::string> ids; ids.reserve(array.Length());
|
|
1364
|
+
for (uint32_t index = 0; index < array.Length(); ++index) { Napi::Value value = array.Get(index); ids.push_back(value.IsString() ? value.As<Napi::String>().Utf8Value() : GetString(value.As<Napi::Object>(), "nodeId")); }
|
|
1365
|
+
auto deferred = Napi::Promise::Deferred::New(env); (new ReadManyWorker(deferred, context, std::move(ids)))->Queue(); return deferred.Promise();
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
Napi::Value WriteManyAsync(const Napi::CallbackInfo& info) {
|
|
1369
|
+
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 (!JsScalarToVariant(env, object.Get("value"), input->data_type, &input->value)) return env.Undefined(); inputs.push_back(std::move(input)); }
|
|
1371
|
+
auto deferred = Napi::Promise::Deferred::New(env); (new WriteManyWorker(deferred, context, std::move(inputs)))->Queue(); return deferred.Promise();
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
Napi::Value BrowseAsync(const Napi::CallbackInfo& info) {
|
|
1375
|
+
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsObject()) return env.Undefined(); Napi::Object object = info[1].As<Napi::Object>(); BrowseOptions options;
|
|
1376
|
+
options.root_node_id = GetString(object, "rootNodeId", "ns=0;i=85"); options.recursive = GetBool(object, "recursive", true); options.variables_only = GetBool(object, "variablesOnly", true); options.max_depth = GetUInt(object, "maxDepth", 10); options.max_nodes = GetUInt(object, "maxNodes", 10000); options.attribute_batch_size = GetUInt(object, "attributeBatchSize", 200);
|
|
1377
|
+
if (object.Has("namespaceIndexes") && object.Get("namespaceIndexes").IsArray()) { Napi::Array values = object.Get("namespaceIndexes").As<Napi::Array>(); for (uint32_t index = 0; index < values.Length(); ++index) options.namespaces.insert(static_cast<uint16_t>(values.Get(index).ToNumber().Uint32Value())); }
|
|
1378
|
+
auto deferred = Napi::Promise::Deferred::New(env); (new BrowseWorker(deferred, context, std::move(options)))->Queue(); return deferred.Promise();
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
Napi::Value CreateSubscriptionAsync(const Napi::CallbackInfo& info) {
|
|
1382
|
+
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsObject() || !info[2].IsFunction()) return env.Undefined(); Napi::Object options = info[1].As<Napi::Object>(); auto deferred = Napi::Promise::Deferred::New(env);
|
|
1383
|
+
(new CreateSubscriptionWorker(deferred, context, info[2].As<Napi::Function>(), GetUInt(options, "callbackQueueSize", 10000), GetDouble(options, "publishingInterval", 1000), GetUInt(options, "lifetimeCount", 120), GetUInt(options, "maxKeepAliveCount", 10), GetUInt(options, "maxNotificationsPerPublish", 0), GetUInt(options, "callbackBatchSize", 500), GetString(options, "queueMode", "all"), GetString(options, "overflowPolicy", "drop-oldest")))->Queue(); return deferred.Promise();
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
Napi::Value AddMonitoredItemsAsync(const Napi::CallbackInfo& info) {
|
|
1387
|
+
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
|
+
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
|
+
auto deferred = Napi::Promise::Deferred::New(env); (new AddMonitoredWorker(deferred, context, subscription_id, std::move(inputs)))->Queue(); return deferred.Promise();
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
Napi::Value RemoveMonitoredItemsAsync(const Napi::CallbackInfo& info) {
|
|
1393
|
+
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[2].IsArray()) return env.Undefined(); Napi::Array array = info[2].As<Napi::Array>(); std::vector<uint32_t> ids; for (uint32_t index = 0; index < array.Length(); ++index) ids.push_back(array.Get(index).ToNumber().Uint32Value()); auto deferred = Napi::Promise::Deferred::New(env); (new RemoveMonitoredWorker(deferred, context, info[1].ToNumber().Uint32Value(), std::move(ids)))->Queue(); return deferred.Promise();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
Napi::Value DeleteSubscriptionAsync(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context) return env.Undefined(); auto deferred = Napi::Promise::Deferred::New(env); (new DeleteSubscriptionWorker(deferred, context, info[1].ToNumber().Uint32Value()))->Queue(); return deferred.Promise(); }
|
|
1397
|
+
|
|
1398
|
+
Napi::Value ValidateSubscriptionAsync(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context) return env.Undefined(); auto deferred = Napi::Promise::Deferred::New(env); (new ValidateSubscriptionWorker(deferred, context, info[1].ToNumber().Uint32Value()))->Queue(); return deferred.Promise(); }
|
|
1399
|
+
|
|
1400
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1401
|
+
exports.Set("connectAsync", Napi::Function::New(env, ConnectAsync)); exports.Set("disconnectAsync", Napi::Function::New(env, DisconnectAsync));
|
|
1402
|
+
exports.Set("getClientDiagnostics", Napi::Function::New(env, GetClientDiagnostics));
|
|
1403
|
+
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
|
+
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));
|
|
1405
|
+
return exports;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
} // namespace
|
|
1409
|
+
|
|
1410
|
+
NODE_API_MODULE(opcua_open62541, Init)
|