@tsplc/opcua 0.0.0-stage → 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/dist/index.js ADDED
@@ -0,0 +1,681 @@
1
+ // packages/opcua/src/native.ts
2
+ import"reflect-metadata";
3
+ var [client, certificates] = await Promise.all([
4
+ import("node-opcua-client"),
5
+ import("node-opcua-certificate-manager")
6
+ ]);
7
+ var {
8
+ AttributeIds,
9
+ DataType,
10
+ VariantArrayType,
11
+ MessageSecurityMode,
12
+ OPCUAClient,
13
+ SecurityPolicy,
14
+ UserTokenType,
15
+ TimestampsToReturn,
16
+ BrowseDirection,
17
+ makeResultMask,
18
+ NodeClass,
19
+ ObjectIds,
20
+ resolveNodeId
21
+ } = client;
22
+ var { OPCUACertificateManager } = certificates;
23
+
24
+ // packages/opcua/src/discovery.ts
25
+ var MAX_DISCOVERY_DEPTH = 15;
26
+ var MAX_DISCOVERY_NODES = 1e4;
27
+ async function* browseReferences(session, nodeId) {
28
+ let result = await session.browse({
29
+ browseDirection: BrowseDirection.Forward,
30
+ includeSubtypes: true,
31
+ nodeId,
32
+ referenceTypeId: "HierarchicalReferences",
33
+ resultMask: makeResultMask("NodeClass | BrowseName")
34
+ });
35
+ try {
36
+ while (true) {
37
+ if (!result.statusCode.isGood()) {
38
+ throw new Error(`Browse failed for ${nodeId}: ${result.statusCode.toString()}`);
39
+ }
40
+ yield* result.references ?? [];
41
+ if (!result.continuationPoint?.length) {
42
+ return;
43
+ }
44
+ result = await session.browseNext(result.continuationPoint, false);
45
+ }
46
+ } finally {
47
+ if (result.continuationPoint?.length) {
48
+ await session.browseNext(result.continuationPoint, true);
49
+ }
50
+ }
51
+ }
52
+ var discoverVariables = async (session, paths) => {
53
+ const expectedPaths = new Set(paths);
54
+ const roots = [...new Set(paths.map((path) => path.split(".")[0]))];
55
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56
+ const globalListMarker = new RegExp(`\\.(?:${roots.map(escape).join("|")})\\.`);
57
+ const nodes = new Map;
58
+ const visited = new Set;
59
+ const queue = [
60
+ { nodeId: ObjectIds.ObjectsFolder, path: "", depth: 0 }
61
+ ];
62
+ for (let cursor = 0;cursor < queue.length; cursor++) {
63
+ const { nodeId, path, depth } = queue[cursor];
64
+ const id = resolveNodeId(nodeId).toString();
65
+ if (visited.has(id)) {
66
+ continue;
67
+ }
68
+ if (depth > MAX_DISCOVERY_DEPTH || visited.size >= MAX_DISCOVERY_NODES) {
69
+ throw new Error("OPC UA address space exceeds the discovery limit");
70
+ }
71
+ visited.add(id);
72
+ const references = [];
73
+ for await (const reference of browseReferences(session, id))
74
+ references.push(reference);
75
+ const rootScope = references.some((reference) => roots.includes(reference.browseName.name ?? ""));
76
+ for (const reference of references) {
77
+ const { name } = reference.browseName;
78
+ if (!name || ["Server", "Types", "Aliases"].includes(name) || rootScope && !roots.includes(name)) {
79
+ continue;
80
+ }
81
+ const parentName = path.slice(path.lastIndexOf(".") + 1);
82
+ const childName = name.startsWith(`${parentName}[`) ? name.slice(parentName.length) : name;
83
+ const childPath = /^\[\d+\]$/.test(childName) ? `${path}${childName}` : `${path}.${name}`;
84
+ const marker = childPath.search(globalListMarker);
85
+ const variablePath = marker < 0 ? "" : childPath.slice(marker + 1);
86
+ const childId = reference.nodeId.toString();
87
+ if (reference.nodeClass === NodeClass.Variable && expectedPaths.has(variablePath)) {
88
+ const existing = nodes.get(variablePath);
89
+ if (existing && existing !== childId) {
90
+ throw new Error(`Ambiguous OPC UA binding: ${variablePath}`);
91
+ }
92
+ nodes.set(variablePath, childId);
93
+ continue;
94
+ }
95
+ if (reference.nodeClass === NodeClass.Object || reference.nodeClass === NodeClass.Variable) {
96
+ if (variablePath && !paths.some((path) => path.startsWith(variablePath + ".") || path.startsWith(variablePath + "[")))
97
+ continue;
98
+ queue.push({ nodeId: childId, path: childPath, depth: depth + 1 });
99
+ }
100
+ }
101
+ }
102
+ return nodes;
103
+ };
104
+ // packages/opcua/src/variables.ts
105
+ var BATCH_SIZE = 50;
106
+ var isInteger = (min, max) => (value) => typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
107
+ var scalarTypes = {
108
+ BOOL: {
109
+ accepts: (value) => typeof value === "boolean",
110
+ dataType: DataType.Boolean
111
+ },
112
+ BYTE: { accepts: isInteger(0, 255), dataType: DataType.Byte },
113
+ DINT: {
114
+ accepts: isInteger(-2147483648, 2147483647),
115
+ dataType: DataType.Int32
116
+ },
117
+ DWORD: { accepts: isInteger(0, 4294967295), dataType: DataType.UInt32 },
118
+ INT: { accepts: isInteger(-32768, 32767), dataType: DataType.Int16 },
119
+ LREAL: {
120
+ accepts: (value) => typeof value === "number" && Number.isFinite(value),
121
+ dataType: DataType.Double
122
+ },
123
+ REAL: {
124
+ accepts: (value) => typeof value === "number" && Number.isFinite(value),
125
+ dataType: DataType.Float
126
+ },
127
+ SINT: { accepts: isInteger(-128, 127), dataType: DataType.SByte },
128
+ UDINT: { accepts: isInteger(0, 4294967295), dataType: DataType.UInt32 },
129
+ UINT: { accepts: isInteger(0, 65535), dataType: DataType.UInt16 },
130
+ USINT: { accepts: isInteger(0, 255), dataType: DataType.Byte },
131
+ WORD: { accepts: isInteger(0, 65535), dataType: DataType.UInt16 }
132
+ };
133
+ var scalarTypeFor = (type) => {
134
+ const capacity = /^STRING\((\d+)\)$/.exec(type)?.[1];
135
+ if (capacity === undefined) {
136
+ return scalarTypes[type];
137
+ }
138
+ return {
139
+ accepts: (value) => typeof value === "string" && !value.includes("\x00") && Buffer.byteLength(value, "utf8") <= Number(capacity),
140
+ dataType: DataType.String
141
+ };
142
+ };
143
+ var byteBufferLength = (type) => {
144
+ const match = /^ARRAY\[0\.\.(\d+)\] OF BYTE$/.exec(type);
145
+ return match ? Number(match[1]) + 1 : null;
146
+ };
147
+ var byteArrayValue = (value, length) => {
148
+ const bytes = value instanceof Uint8Array ? Array.from(value) : value;
149
+ if (!Array.isArray(bytes) || bytes.length !== length) {
150
+ return null;
151
+ }
152
+ return bytes.every(isInteger(0, 255)) ? bytes : null;
153
+ };
154
+ var badReading = (status) => ({
155
+ quality: "bad",
156
+ status,
157
+ value: null
158
+ });
159
+ var toReading = (variable, data) => {
160
+ if (!data.statusCode.isGood()) {
161
+ return badReading(data.statusCode.toString());
162
+ }
163
+ const value = data.value.value;
164
+ const bufferLength = byteBufferLength(variable.type);
165
+ if (bufferLength !== null) {
166
+ const bytes = byteArrayValue(value, bufferLength);
167
+ const typeMatches = data.value.dataType === DataType.Byte && data.value.arrayType === VariantArrayType.Array || data.value.dataType === DataType.ByteString && data.value.arrayType === VariantArrayType.Scalar;
168
+ if (!typeMatches || !bytes) {
169
+ return badReading("Unexpected OPC UA byte buffer type or length");
170
+ }
171
+ return {
172
+ quality: "good",
173
+ status: data.statusCode.toString(),
174
+ value: bytes
175
+ };
176
+ }
177
+ const scalar = typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
178
+ if (!scalar || data.value.dataType !== scalarTypeFor(variable.type)?.dataType || data.value.arrayType !== VariantArrayType.Scalar) {
179
+ return badReading("Unexpected OPC UA data type");
180
+ }
181
+ return { quality: "good", status: data.statusCode.toString(), value };
182
+ };
183
+ var readVariables = async (session, nodes, selected) => {
184
+ const readings = Object.fromEntries(selected.map(({ path }) => [
185
+ path,
186
+ { quality: "unavailable", status: "Symbol not published", value: null }
187
+ ]));
188
+ const bound = selected.flatMap((variable) => {
189
+ const nodeId = nodes.get(variable.path);
190
+ return nodeId ? [{ nodeId, variable }] : [];
191
+ });
192
+ const batches = Array.from({ length: Math.ceil(bound.length / BATCH_SIZE) }, (_, index) => bound.slice(index * BATCH_SIZE, (index + 1) * BATCH_SIZE));
193
+ await Promise.all(batches.map(async (batch) => {
194
+ const values = await session.read(batch.map(({ nodeId }) => ({ attributeId: AttributeIds.Value, nodeId })), 0);
195
+ if (values.length !== batch.length) {
196
+ throw new Error("Incomplete OPC UA read response");
197
+ }
198
+ for (const [index, { variable }] of batch.entries()) {
199
+ const data = values[index];
200
+ if (!data) {
201
+ throw new Error("Missing OPC UA value in read response");
202
+ }
203
+ readings[variable.path] = toReading(variable, data);
204
+ }
205
+ }));
206
+ return readings;
207
+ };
208
+
209
+ class VariableWriteError extends Error {
210
+ results;
211
+ constructor(results, cause) {
212
+ super("PLC write did not succeed for every field; inspect results before retrying", { cause });
213
+ this.name = "VariableWriteError";
214
+ this.results = results;
215
+ }
216
+ }
217
+ var notAttempted = (updates) => updates.map(({ variable }) => ({
218
+ path: variable.path,
219
+ status: "Not attempted",
220
+ written: false
221
+ }));
222
+ var prepareWrite = (session, nodes, { variable, value }) => {
223
+ const nodeId = nodes.get(variable.path);
224
+ if (!nodeId) {
225
+ throw new Error(`Symbol not published: ${variable.path}`);
226
+ }
227
+ const invalid = new Error(`Invalid ${variable.type} value for ${variable.path}`);
228
+ const bufferLength = byteBufferLength(variable.type);
229
+ if (bufferLength === null) {
230
+ const scalarType = scalarTypeFor(variable.type);
231
+ if (!scalarType?.accepts(value)) {
232
+ throw invalid;
233
+ }
234
+ const request = {
235
+ attributeId: AttributeIds.Value,
236
+ nodeId,
237
+ value: {
238
+ value: {
239
+ arrayType: VariantArrayType.Scalar,
240
+ dataType: scalarType.dataType,
241
+ value
242
+ }
243
+ }
244
+ };
245
+ return () => Promise.resolve(request);
246
+ }
247
+ const bytes = byteArrayValue(value, bufferLength);
248
+ if (!bytes) {
249
+ throw invalid;
250
+ }
251
+ return async () => {
252
+ const [current] = await session.read([{ attributeId: AttributeIds.Value, nodeId }], 0);
253
+ if (!current || toReading(variable, current).quality !== "good") {
254
+ throw new Error(`Cannot determine byte-buffer representation: ${variable.path}`);
255
+ }
256
+ return {
257
+ attributeId: AttributeIds.Value,
258
+ nodeId,
259
+ value: {
260
+ value: {
261
+ arrayType: current.value.arrayType,
262
+ dataType: current.value.dataType,
263
+ value: Buffer.from(bytes)
264
+ }
265
+ }
266
+ };
267
+ };
268
+ };
269
+ var writeVariables = async (session, nodes, updates) => {
270
+ const prepared = updates.map((update) => prepareWrite(session, nodes, update));
271
+ const requests = await Promise.all(prepared.map((build) => build()));
272
+ const results = [];
273
+ for (let offset = 0;offset < requests.length; offset += BATCH_SIZE) {
274
+ const batch = requests.slice(offset, offset + BATCH_SIZE);
275
+ const batchUpdates = updates.slice(offset, offset + batch.length);
276
+ const remaining = updates.slice(offset + batch.length);
277
+ const statuses = await session.write(batch).catch((cause) => {
278
+ throw new VariableWriteError([
279
+ ...results,
280
+ ...batchUpdates.map(({ variable }) => ({
281
+ path: variable.path,
282
+ status: "Unknown write outcome",
283
+ written: null
284
+ })),
285
+ ...notAttempted(remaining)
286
+ ], cause);
287
+ });
288
+ for (const [index, { variable }] of batchUpdates.entries()) {
289
+ const status = statuses[index];
290
+ results.push({
291
+ path: variable.path,
292
+ status: status?.toString() ?? "Missing write result",
293
+ written: status ? status.isGood() : null
294
+ });
295
+ }
296
+ if (statuses.length !== batch.length || results.some(({ written }) => written !== true)) {
297
+ throw new VariableWriteError([...results, ...notAttempted(remaining)]);
298
+ }
299
+ }
300
+ return results;
301
+ };
302
+ // packages/opcua/src/variable-manager.ts
303
+ var newBranch = () => ({ children: new Map, kind: "branch" });
304
+ var fieldKeys = (field) => (field.match(/[^.[\]]+|\[\d+\]/g) ?? []).map((part) => part.startsWith("[") ? Number(part.slice(1, -1)) : part);
305
+ var buildSchemas = (variables) => {
306
+ const schemas = new Map;
307
+ for (const variable of variables) {
308
+ if (!variable.field)
309
+ continue;
310
+ const name = variable.path.slice(0, -(variable.field.length + 1));
311
+ const schema = schemas.get(name) ?? { fields: [], root: newBranch() };
312
+ schemas.set(name, schema);
313
+ schema.fields.push(variable);
314
+ const keys = fieldKeys(variable.field);
315
+ const leafKey = keys.pop();
316
+ if (leafKey === undefined) {
317
+ throw new Error(`Empty PLC field: ${variable.path}`);
318
+ }
319
+ const parent = keys.reduce((branch, key) => {
320
+ const child = branch.children.get(key) ?? newBranch();
321
+ if (child.kind !== "branch") {
322
+ throw new Error(`Conflicting PLC field: ${variable.path}`);
323
+ }
324
+ branch.children.set(key, child);
325
+ return child;
326
+ }, schema.root);
327
+ parent.children.set(leafKey, { kind: "leaf", variable });
328
+ }
329
+ return schemas;
330
+ };
331
+ var arrayKeys = (branch) => [...branch.children.keys()].filter((key) => typeof key === "number").sort((a, b) => a - b);
332
+ var assemble = (node, readings) => {
333
+ if (node.kind === "leaf") {
334
+ const reading = readings[node.variable.path];
335
+ if (reading?.quality !== "good" || reading.value === null) {
336
+ throw new Error(`Cannot read ${node.variable.path}: ${reading?.status ?? "Missing value"}`);
337
+ }
338
+ return reading.value;
339
+ }
340
+ const keys = arrayKeys(node);
341
+ if (keys.length) {
342
+ return keys.map((key) => {
343
+ const child = node.children.get(key);
344
+ if (!child) {
345
+ throw new Error(`Missing PLC array element: ${key}`);
346
+ }
347
+ return assemble(child, readings);
348
+ });
349
+ }
350
+ return Object.fromEntries([...node.children].map(([key, child]) => [key, assemble(child, readings)]));
351
+ };
352
+ var collectUpdates = (node, patch, path) => {
353
+ if (node.kind === "leaf") {
354
+ return [{ value: patch, variable: node.variable }];
355
+ }
356
+ if (!patch || typeof patch !== "object" || !Array.isArray(patch) && Object.getPrototypeOf(patch) !== Object.prototype && Object.getPrototypeOf(patch) !== null) {
357
+ throw new Error(`Expected an object patch for ${path}`);
358
+ }
359
+ const keys = arrayKeys(node);
360
+ if (!keys.length && Array.isArray(patch)) {
361
+ throw new Error(`Expected a struct patch for ${path}`);
362
+ }
363
+ return Object.keys(patch).flatMap((key) => {
364
+ const value = Reflect.get(patch, key);
365
+ const index = Number(key);
366
+ const arrayKey = keys.length && Number.isInteger(index) && index >= 0 && String(index) === key ? keys[index] : undefined;
367
+ const child = keys.length ? node.children.get(arrayKey ?? -1) : node.children.get(key);
368
+ if (!child) {
369
+ throw new Error(`Unknown PLC field or array index: ${path}.${key}`);
370
+ }
371
+ return collectUpdates(child, value, `${path}.${key}`);
372
+ });
373
+ };
374
+ var createVariableManager = (connection, variables) => {
375
+ const schemas = buildSchemas(variables);
376
+ const schemaFor = (name) => {
377
+ const schema = schemas.get(name);
378
+ if (!schema) {
379
+ throw new Error(`Unknown PLC variable: ${name}`);
380
+ }
381
+ return schema;
382
+ };
383
+ return {
384
+ async get(name) {
385
+ const schema = schemaFor(name);
386
+ const binding = connection.getVariableSession();
387
+ const readings = await readVariables(binding.session, binding.nodes, schema.fields);
388
+ if (connection.getVariableSession() !== binding) {
389
+ throw new Error("PLC session changed during read");
390
+ }
391
+ return assemble(schema.root, readings);
392
+ },
393
+ async set(name, patch) {
394
+ const schema = schemaFor(name);
395
+ const updates = collectUpdates(schema.root, patch, name);
396
+ if (!updates.length) {
397
+ throw new Error(`Empty PLC patch: ${name}`);
398
+ }
399
+ const { session, nodes } = connection.getVariableSession();
400
+ return await writeVariables(session, nodes, updates);
401
+ }
402
+ };
403
+ };
404
+ // packages/opcua/src/machines.ts
405
+ import { z } from "zod";
406
+ var createMachineClient = (definition, options) => ({
407
+ telemetry: {
408
+ read: async () => {
409
+ const fields = Object.entries(definition.telemetry);
410
+ const values = await options.read([
411
+ definition.versionPath,
412
+ ...fields.map(([, field]) => field.path)
413
+ ]);
414
+ if (values.length !== fields.length + 1 || values[0] !== definition.version)
415
+ throw new Error("Machine interface version differs from the generated contract.");
416
+ const snapshot = Object.fromEntries(fields.map(([name], index) => [name, values[index + 1]]));
417
+ return z.custom((value) => typeof value === "object" && value !== null && fields.every(([name, field]) => {
418
+ const item = Reflect.get(value, name);
419
+ return typeof item === field.kind && (field.kind !== "number" || typeof item === "number" && Number.isFinite(item));
420
+ })).parse(snapshot);
421
+ }
422
+ }
423
+ });
424
+
425
+ // packages/opcua/src/client.ts
426
+ var connectPlc = async (options) => {
427
+ const secure = options.security !== "None";
428
+ const client = OPCUAClient.create({
429
+ applicationName: options.applicationName ?? "tsPLC",
430
+ clientCertificateManager: new OPCUACertificateManager({
431
+ rootFolder: options.certificateDirectory,
432
+ automaticallyAcceptUnknownCertificate: false
433
+ }),
434
+ connectionStrategy: { maxRetry: 0 },
435
+ defaultTransactionTimeout: 5000,
436
+ transportTimeout: 5000,
437
+ keepSessionAlive: true,
438
+ endpointMustExist: false,
439
+ securityMode: secure ? MessageSecurityMode.SignAndEncrypt : MessageSecurityMode.None,
440
+ securityPolicy: secure ? SecurityPolicy.Basic256Sha256 : SecurityPolicy.None
441
+ });
442
+ try {
443
+ await client.connect(options.endpoint);
444
+ const session = await client.createSession(options.credentials ? {
445
+ type: UserTokenType.UserName,
446
+ userName: options.credentials.username,
447
+ password: options.credentials.password
448
+ } : { type: UserTokenType.Anonymous });
449
+ return { session, disconnect: () => client.disconnect() };
450
+ } catch (error) {
451
+ await client.disconnect();
452
+ throw error;
453
+ }
454
+ };
455
+ var connectMachine = async (definition, options) => {
456
+ const connection = await connectPlc(options);
457
+ try {
458
+ const paths = [
459
+ definition.versionPath,
460
+ ...Object.values(definition.telemetry).map((field) => field.path)
461
+ ];
462
+ const nodes = await discoverVariables(connection.session, paths);
463
+ const readNodes = paths.map((path) => {
464
+ const nodeId = nodes.get(path);
465
+ if (!nodeId)
466
+ throw new Error(`Machine symbol is not published: ${path}`);
467
+ return { nodeId, attributeId: AttributeIds.Value };
468
+ });
469
+ const read = async (selected) => {
470
+ const requests = selected.map((path) => {
471
+ const nodeId = nodes.get(path);
472
+ if (!nodeId)
473
+ throw new Error(`Unknown machine symbol: ${path}`);
474
+ return { nodeId, attributeId: AttributeIds.Value };
475
+ });
476
+ const values = await connection.session.read(requests, 0);
477
+ if (values.length !== requests.length || values.some((value) => !value.statusCode.isGood()))
478
+ throw new Error("Machine telemetry read failed");
479
+ return values.map((value) => value.value.value);
480
+ };
481
+ const machine = createMachineClient(definition, { read });
482
+ await machine.telemetry.read();
483
+ return {
484
+ telemetry: {
485
+ ...machine.telemetry,
486
+ subscribe: async (listener, onError) => {
487
+ const subscription = await connection.session.createSubscription2({
488
+ requestedPublishingInterval: 100,
489
+ requestedLifetimeCount: 100,
490
+ requestedMaxKeepAliveCount: 10,
491
+ maxNotificationsPerPublish: 100,
492
+ publishingEnabled: true,
493
+ priority: 0
494
+ });
495
+ let disposed = false, reading = false;
496
+ const refresh = () => {
497
+ if (disposed || reading)
498
+ return;
499
+ reading = true;
500
+ machine.telemetry.read().then((value) => {
501
+ if (!disposed)
502
+ listener(value);
503
+ }).catch(onError).finally(() => {
504
+ reading = false;
505
+ });
506
+ };
507
+ try {
508
+ const monitored = await subscription.monitorItems(readNodes, { samplingInterval: 100, discardOldest: true, queueSize: 1 }, TimestampsToReturn.Both);
509
+ monitored.on("changed", refresh);
510
+ subscription.on("error", onError);
511
+ refresh();
512
+ return {
513
+ unsubscribe: async () => {
514
+ disposed = true;
515
+ await subscription.terminate();
516
+ }
517
+ };
518
+ } catch (error) {
519
+ await subscription.terminate();
520
+ throw error;
521
+ }
522
+ }
523
+ },
524
+ disconnect: connection.disconnect
525
+ };
526
+ } catch (error) {
527
+ await connection.disconnect();
528
+ throw error;
529
+ }
530
+ };
531
+ // packages/opcua/src/plc.ts
532
+ import { setTimeout as delay } from "node:timers/promises";
533
+
534
+ // packages/opcua/src/contract.ts
535
+ var contractVariables = (contract, published) => {
536
+ const selected = published && new Set(published);
537
+ return contract.symbols.filter((symbol) => !selected || selected.has(`${symbol.gvl}.${symbol.name}`)).flatMap((symbol) => {
538
+ const fields = contract.typeFields[symbol.type];
539
+ if (!fields && symbol.type.startsWith("ARRAY["))
540
+ return [
541
+ {
542
+ description: "",
543
+ device: symbol.name,
544
+ field: "",
545
+ path: `${symbol.gvl}.${symbol.name}`,
546
+ type: symbol.type
547
+ }
548
+ ];
549
+ if (!fields)
550
+ throw new Error(`Missing generated fields for ${symbol.type}.`);
551
+ return fields.map(({ field, type, description }) => ({
552
+ description,
553
+ device: symbol.name,
554
+ field,
555
+ path: `${symbol.gvl}.${symbol.name}${field.startsWith("[") ? "" : "."}${field}`,
556
+ type
557
+ }));
558
+ });
559
+ };
560
+
561
+ // packages/opcua/src/snapshot.ts
562
+ var unavailableSnapshot = (error) => ({
563
+ connected: false,
564
+ error,
565
+ updatedAt: null,
566
+ values: {}
567
+ });
568
+
569
+ // packages/opcua/src/plc.ts
570
+ var pollIntervalMs = 1000;
571
+ var reconnectDelayMs = 2000;
572
+ var staleAfterMs = 5000;
573
+ var sleep = (ms, signal) => delay(ms, undefined, { signal }).catch(() => {
574
+ return;
575
+ });
576
+ var createPlc = (options) => {
577
+ const variables = contractVariables(options.contract, options.publication);
578
+ let state = unavailableSnapshot("Waiting for the PLC");
579
+ let binding = null;
580
+ const currentBinding = () => {
581
+ if (!binding)
582
+ throw new Error("PLC is not connected");
583
+ return binding;
584
+ };
585
+ const snapshot = () => state.updatedAt && Date.now() - Date.parse(state.updatedAt) > staleAfterMs ? unavailableSnapshot("PLC data is stale; waiting for a fresh read") : state;
586
+ const poll = async (signal) => {
587
+ const client = await connectPlc(options);
588
+ const clear = () => {
589
+ binding = null;
590
+ };
591
+ signal.addEventListener("abort", clear, { once: true });
592
+ try {
593
+ client.session.on("session_closed", clear);
594
+ signal.throwIfAborted();
595
+ const nodes = await discoverVariables(client.session, variables.map(({ path }) => path));
596
+ if (!nodes.size)
597
+ throw new Error("No declared PLC symbols are published. Check the downloaded program and OPC UA symbol configuration.");
598
+ options.log?.info(`Connected to the PLC (${nodes.size} symbols)`);
599
+ signal.throwIfAborted();
600
+ binding = { nodes, session: client.session };
601
+ while (!signal.aborted) {
602
+ const values = await readVariables(client.session, nodes, variables);
603
+ signal.throwIfAborted();
604
+ state = { connected: true, error: null, updatedAt: new Date().toISOString(), values };
605
+ await sleep(pollIntervalMs, signal);
606
+ }
607
+ } finally {
608
+ clear();
609
+ signal.removeEventListener("abort", clear);
610
+ await client.disconnect();
611
+ }
612
+ };
613
+ const run = async (signal) => {
614
+ while (!signal.aborted) {
615
+ try {
616
+ await poll(signal);
617
+ } catch (error) {
618
+ if (signal.aborted)
619
+ return;
620
+ const message = error instanceof Error ? error.message : "OPC UA connection failed";
621
+ if (state.error !== message)
622
+ options.log?.warn("PLC connection unavailable; retrying", error);
623
+ state = unavailableSnapshot(message);
624
+ }
625
+ await sleep(reconnectDelayMs, signal);
626
+ }
627
+ };
628
+ return {
629
+ run,
630
+ snapshot,
631
+ ...createVariableManager({ getVariableSession: currentBinding }, variables),
632
+ read: async (paths) => {
633
+ const selected = new Set(paths);
634
+ const current = currentBinding();
635
+ const readings = await readVariables(current.session, current.nodes, variables.filter(({ path }) => selected.has(path)));
636
+ if (binding !== current)
637
+ throw new Error("PLC session changed during read");
638
+ return readings;
639
+ },
640
+ write: async (values) => {
641
+ const current = currentBinding();
642
+ const byPath = new Map(variables.map((variable) => [variable.path, variable]));
643
+ return writeVariables(current.session, current.nodes, [...values].map(([path, value]) => {
644
+ const variable = byPath.get(path);
645
+ if (!variable)
646
+ throw new Error(`Unknown or unpublished PLC variable ${path}`);
647
+ return { variable, value };
648
+ }));
649
+ },
650
+ session: () => currentBinding().session,
651
+ machine: (definition) => createMachineClient(definition, {
652
+ read: (paths) => {
653
+ const { values } = snapshot();
654
+ return paths.map((path) => {
655
+ const reading = values[path];
656
+ if (reading?.quality !== "good")
657
+ throw new Error(`PLC telemetry unavailable: ${path}`);
658
+ return reading.value;
659
+ });
660
+ }
661
+ })
662
+ };
663
+ };
664
+ export {
665
+ AttributeIds,
666
+ DataType,
667
+ VariableWriteError,
668
+ connectMachine,
669
+ connectPlc,
670
+ contractVariables,
671
+ createMachineClient,
672
+ createPlc,
673
+ createVariableManager,
674
+ discoverVariables,
675
+ readVariables,
676
+ unavailableSnapshot,
677
+ writeVariables
678
+ };
679
+
680
+ //# debugId=5B6F261ED0979A6864756E2164756E21
681
+ //# sourceMappingURL=index.js.map