@synapticon/motion-master-client 2.3.0-beta.0 → 6.0.0-alpha.110

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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api-error.d.ts +25 -0
  3. package/dist/api-error.js +52 -0
  4. package/dist/client.d.ts +20 -0
  5. package/dist/client.js +37 -0
  6. package/dist/constants.d.ts +2 -0
  7. package/dist/constants.js +8 -0
  8. package/dist/eni.d.ts +197 -0
  9. package/dist/eni.js +61 -0
  10. package/dist/generated/Api.d.ts +1886 -0
  11. package/dist/generated/Api.js +1293 -0
  12. package/dist/generated/data-contracts.d.ts +2282 -0
  13. package/dist/generated/data-contracts.js +12 -0
  14. package/dist/generated/http-client.d.ts +60 -0
  15. package/dist/generated/http-client.js +171 -0
  16. package/dist/hex.d.ts +2 -0
  17. package/dist/hex.js +20 -0
  18. package/dist/index.d.ts +13 -7
  19. package/dist/index.js +20 -22
  20. package/dist/lan.d.ts +4 -0
  21. package/dist/lan.js +59 -0
  22. package/dist/mmpd.d.ts +53 -0
  23. package/dist/mmpd.js +322 -0
  24. package/dist/sdo.d.ts +17 -0
  25. package/dist/sdo.js +219 -0
  26. package/dist/web-socket-connection.d.ts +70 -0
  27. package/dist/web-socket-connection.js +237 -0
  28. package/dist/wire-time.d.ts +2 -0
  29. package/dist/wire-time.js +18 -0
  30. package/package.json +29 -35
  31. package/.vscode/launch.json +0 -23
  32. package/README.md +0 -118
  33. package/box.config.json +0 -7
  34. package/dist/index.d.ts.map +0 -1
  35. package/dist/index.js.map +0 -1
  36. package/dist/motion-master-client-web-socket-connection.d.ts +0 -43
  37. package/dist/motion-master-client-web-socket-connection.d.ts.map +0 -1
  38. package/dist/motion-master-client-web-socket-connection.js +0 -91
  39. package/dist/motion-master-client-web-socket-connection.js.map +0 -1
  40. package/dist/motion-master-client.d.ts +0 -105
  41. package/dist/motion-master-client.d.ts.map +0 -1
  42. package/dist/motion-master-client.js +0 -348
  43. package/dist/motion-master-client.js.map +0 -1
  44. package/dist/motion-master-notification-subscribe-data.d.ts +0 -8
  45. package/dist/motion-master-notification-subscribe-data.d.ts.map +0 -1
  46. package/dist/motion-master-notification-subscribe-data.js +0 -3
  47. package/dist/motion-master-notification-subscribe-data.js.map +0 -1
  48. package/dist/motion-master-notification-web-socket-connection.d.ts +0 -56
  49. package/dist/motion-master-notification-web-socket-connection.d.ts.map +0 -1
  50. package/dist/motion-master-notification-web-socket-connection.js +0 -115
  51. package/dist/motion-master-notification-web-socket-connection.js.map +0 -1
  52. package/dist/motion-master-notification.d.ts +0 -31
  53. package/dist/motion-master-notification.d.ts.map +0 -1
  54. package/dist/motion-master-notification.js +0 -44
  55. package/dist/motion-master-notification.js.map +0 -1
  56. package/dist/util.d.ts +0 -16
  57. package/dist/util.d.ts.map +0 -1
  58. package/dist/util.js +0 -24
  59. package/dist/util.js.map +0 -1
  60. package/licenses.format.json +0 -4
  61. package/motion-master-client.config.json +0 -7
  62. package/smm_data_dic.json +0 -1213
  63. package/src/index.ts +0 -7
  64. package/src/motion-master-client-web-socket-connection.ts +0 -104
  65. package/src/motion-master-client.ts +0 -468
  66. package/src/motion-master-notification-subscribe-data.ts +0 -7
  67. package/src/motion-master-notification-web-socket-connection.ts +0 -143
  68. package/src/motion-master-notification.ts +0 -51
  69. package/src/util.ts +0 -22
  70. package/test.sh +0 -122
  71. package/tsconfig.json +0 -63
  72. package/tslint.json +0 -118
package/dist/mmpd.js ADDED
@@ -0,0 +1,322 @@
1
+ // Parser for the `.mmpd` process-data recorder dump (see libs/node/process_data_dump.{h,cc}). The
2
+ // format embeds the process image as a header so a run decodes fully offline — no running Motion
3
+ // Master and no live bus. All multi-byte integers are little-endian.
4
+ //
5
+ // Layout:
6
+ // Prefix (36 bytes): magic 'MMPD', u16 formatVersion, u16 flags,
7
+ // u64 startSequence, u64 rowCount, u32 inputBytes, u32 outputBytes, u32 deviceCount.
8
+ // Device table (deviceCount): u16 slavePosition, u32 vendorId/productCode/revisionNumber/
9
+ // serialNumber, str name, u32 entryCount, then per entry: u16 index, u8 subindex,
10
+ // u8 direction (1=output/RxPDO, 0=input/TxPDO), u16 dataType, u16 bitLength, u32 bitOffset,
11
+ // str name. (str = u16 byte length + UTF-8 bytes.)
12
+ // Rows (rowCount, fixed stride 16 + inputBytes + outputBytes): u64 sequence, u64 timestampNs,
13
+ // inputs[inputBytes], outputs[outputBytes].
14
+ const MAGIC = 0x4450_4d4d; // 'MMPD' as a little-endian u32 ('M','M','P','D')
15
+ const SUPPORTED_FORMAT_VERSION = 1;
16
+ const utf8 = new TextDecoder();
17
+ // Cursor over a DataView for sequential little-endian reads while parsing the header.
18
+ class Cursor {
19
+ dv;
20
+ u8;
21
+ offset;
22
+ constructor(dv, u8, offset = 0) {
23
+ this.dv = dv;
24
+ this.u8 = u8;
25
+ this.offset = offset;
26
+ }
27
+ u8val() {
28
+ return this.dv.getUint8(this.offset++);
29
+ }
30
+ u16() {
31
+ const v = this.dv.getUint16(this.offset, true);
32
+ this.offset += 2;
33
+ return v;
34
+ }
35
+ u32() {
36
+ const v = this.dv.getUint32(this.offset, true);
37
+ this.offset += 4;
38
+ return v;
39
+ }
40
+ u64() {
41
+ const v = this.dv.getBigUint64(this.offset, true);
42
+ this.offset += 8;
43
+ return v;
44
+ }
45
+ str() {
46
+ const len = this.u16();
47
+ const s = utf8.decode(this.u8.subarray(this.offset, this.offset + len));
48
+ this.offset += len;
49
+ return s;
50
+ }
51
+ }
52
+ /// A parsed `.mmpd` dump. The header is read eagerly; row values are decoded on demand so a
53
+ /// large file (a deep recorder ring) is not fully materialised up front.
54
+ export class MmpdFile {
55
+ header;
56
+ u8;
57
+ dv;
58
+ rowsStart;
59
+ constructor(header, u8, dv,
60
+ /// Byte offset of the first row within the buffer.
61
+ rowsStart) {
62
+ this.header = header;
63
+ this.u8 = u8;
64
+ this.dv = dv;
65
+ this.rowsStart = rowsStart;
66
+ }
67
+ get rowCount() {
68
+ return this.header.rowCount;
69
+ }
70
+ /// Per-row byte stride: u64 sequence + u64 timestampNs + inputs + outputs.
71
+ get rowStride() {
72
+ return 16 + this.header.inputBytes + this.header.outputBytes;
73
+ }
74
+ /// The raw record at row `i` (views into the source buffer; do not retain past the buffer's life).
75
+ rowAt(i) {
76
+ const base = this.rowsStart + i * this.rowStride;
77
+ const inputsAt = base + 16;
78
+ const outputsAt = inputsAt + this.header.inputBytes;
79
+ return {
80
+ sequence: this.dv.getBigUint64(base, true),
81
+ timestampNs: this.dv.getBigUint64(base + 8, true),
82
+ inputs: this.u8.subarray(inputsAt, outputsAt),
83
+ outputs: this.u8.subarray(outputsAt, outputsAt + this.header.outputBytes),
84
+ };
85
+ }
86
+ /// Epoch microseconds for every row (JS-exact in a Number until ~year 2255, matching the live
87
+ /// monitoring protocol). Sub-microsecond nanoseconds are dropped.
88
+ timestampsUs() {
89
+ const out = new Float64Array(this.header.rowCount);
90
+ for (let i = 0; i < out.length; i++) {
91
+ out[i] = Number(this.dv.getBigUint64(this.rowsStart + i * this.rowStride + 8, true) / 1000n);
92
+ }
93
+ return out;
94
+ }
95
+ /// Decodes one entry's value for every row. Numeric types yield numbers (64-bit ints are coerced
96
+ /// to Number, lossy beyond 2^53); strings, undecodable (dataType 0), and unsupported types yield
97
+ /// null — so the result is always plottable.
98
+ decodeSeries(entry) {
99
+ const out = new Array(this.header.rowCount);
100
+ const reader = numericReader(entry.dataType);
101
+ const byteAligned = entry.bitOffset % 8 === 0 && entry.bitLength % 8 === 0;
102
+ const regionStart = entry.isOutput ? 16 + this.header.inputBytes : 16;
103
+ if (reader && byteAligned) {
104
+ // Fast path: read straight from the buffer with no per-row allocation.
105
+ const valueOffset = regionStart + (entry.bitOffset >> 3);
106
+ for (let i = 0; i < out.length; i++) {
107
+ const v = reader(this.dv, this.rowsStart + i * this.rowStride + valueOffset);
108
+ out[i] = typeof v === 'bigint' ? Number(v) : v;
109
+ }
110
+ return out;
111
+ }
112
+ // Slow path: sub-byte fields and strings — extract the bits, then decode.
113
+ for (let i = 0; i < out.length; i++) {
114
+ const row = this.rowAt(i);
115
+ const bytes = extractBits(entry.isOutput ? row.outputs : row.inputs, entry.bitOffset, entry.bitLength);
116
+ const v = decodeMmpdValue(entry.dataType, bytes);
117
+ out[i] = typeof v === 'number' ? v : typeof v === 'bigint' ? Number(v) : null;
118
+ }
119
+ return out;
120
+ }
121
+ }
122
+ /// Parses a `.mmpd` dump. Throws on a bad magic or an unsupported format version. A file whose row
123
+ /// region is shorter than `rowCount` implies (truncated transfer) is clamped to the rows present.
124
+ export function parseMmpd(data) {
125
+ const u8 = data instanceof Uint8Array ? data : new Uint8Array(data);
126
+ const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
127
+ if (u8.byteLength < 36 || dv.getUint32(0, true) !== MAGIC) {
128
+ throw new Error('Not a .mmpd file (bad magic)');
129
+ }
130
+ const formatVersion = dv.getUint16(4, true);
131
+ if (formatVersion !== SUPPORTED_FORMAT_VERSION) {
132
+ throw new Error(`Unsupported .mmpd format version ${formatVersion} (expected ${SUPPORTED_FORMAT_VERSION})`);
133
+ }
134
+ const header = {
135
+ formatVersion,
136
+ flags: dv.getUint16(6, true),
137
+ startSequence: dv.getBigUint64(8, true),
138
+ rowCount: Number(dv.getBigUint64(16, true)),
139
+ inputBytes: dv.getUint32(24, true),
140
+ outputBytes: dv.getUint32(28, true),
141
+ devices: [],
142
+ };
143
+ const deviceCount = dv.getUint32(32, true);
144
+ const cursor = new Cursor(dv, u8, 36);
145
+ for (let d = 0; d < deviceCount; d++) {
146
+ const device = {
147
+ slavePosition: cursor.u16(),
148
+ vendorId: cursor.u32(),
149
+ productCode: cursor.u32(),
150
+ revisionNumber: cursor.u32(),
151
+ serialNumber: cursor.u32(),
152
+ name: cursor.str(),
153
+ entries: [],
154
+ };
155
+ const entryCount = cursor.u32();
156
+ for (let e = 0; e < entryCount; e++) {
157
+ device.entries.push({
158
+ index: cursor.u16(),
159
+ subindex: cursor.u8val(),
160
+ isOutput: cursor.u8val() === 1,
161
+ dataType: cursor.u16(),
162
+ bitLength: cursor.u16(),
163
+ bitOffset: cursor.u32(),
164
+ name: cursor.str(),
165
+ });
166
+ }
167
+ header.devices.push(device);
168
+ }
169
+ const rowsStart = cursor.offset;
170
+ const rowStride = 16 + header.inputBytes + header.outputBytes;
171
+ const available = Math.floor((u8.byteLength - rowsStart) / rowStride);
172
+ if (available < header.rowCount) {
173
+ header.rowCount = Math.max(0, available);
174
+ }
175
+ return new MmpdFile(header, u8, dv, rowsStart);
176
+ }
177
+ // A byte-aligned reader for the fixed-width numeric types, or null for variable/sub-byte/string
178
+ // types that need the bit-extraction path.
179
+ function numericReader(dataType) {
180
+ switch (dataType) {
181
+ case 1 /* DataType.BOOLEAN */:
182
+ case 5 /* DataType.UNSIGNED8 */:
183
+ case 30 /* DataType.BYTE */:
184
+ case 45 /* DataType.BITARR8 */:
185
+ return (dv, o) => dv.getUint8(o);
186
+ case 2 /* DataType.INTEGER8 */:
187
+ return (dv, o) => dv.getInt8(o);
188
+ case 3 /* DataType.INTEGER16 */:
189
+ return (dv, o) => dv.getInt16(o, true);
190
+ case 6 /* DataType.UNSIGNED16 */:
191
+ case 31 /* DataType.WORD */:
192
+ case 46 /* DataType.BITARR16 */:
193
+ return (dv, o) => dv.getUint16(o, true);
194
+ case 4 /* DataType.INTEGER32 */:
195
+ return (dv, o) => dv.getInt32(o, true);
196
+ case 7 /* DataType.UNSIGNED32 */:
197
+ case 32 /* DataType.DWORD */:
198
+ case 47 /* DataType.BITARR32 */:
199
+ return (dv, o) => dv.getUint32(o, true);
200
+ case 8 /* DataType.REAL32 */:
201
+ return (dv, o) => dv.getFloat32(o, true);
202
+ case 17 /* DataType.REAL64 */:
203
+ return (dv, o) => dv.getFloat64(o, true);
204
+ case 21 /* DataType.INTEGER64 */:
205
+ return (dv, o) => dv.getBigInt64(o, true);
206
+ case 27 /* DataType.UNSIGNED64 */:
207
+ return (dv, o) => dv.getBigUint64(o, true);
208
+ default:
209
+ return null;
210
+ }
211
+ }
212
+ /// Fetches the live recorder dump (`GET /api/process-data/dump`) and parses it. `baseUrl` is the
213
+ /// HTTP API origin (e.g. `https://host:61447`). Throws on a non-OK response (the response body's
214
+ /// `error` message when present). Pass `fetch` to inject an implementation (Node, request logging).
215
+ export async function fetchProcessDataDump(baseUrl, options = {}) {
216
+ const doFetch = options.fetch ?? fetch;
217
+ const res = await doFetch(`${baseUrl.replace(/\/+$/, '')}/api/process-data/dump`, {
218
+ headers: { Accept: 'application/octet-stream' },
219
+ signal: options.signal,
220
+ });
221
+ if (!res.ok) {
222
+ let message = `HTTP ${res.status}`;
223
+ try {
224
+ const body = (await res.json());
225
+ if (body?.error) {
226
+ message = body.error;
227
+ }
228
+ }
229
+ catch {
230
+ // Non-JSON error body — keep the status message.
231
+ }
232
+ throw new Error(message);
233
+ }
234
+ return parseMmpd(await res.arrayBuffer());
235
+ }
236
+ /// Extracts `bitLength` bits starting at `bitOffset` from `src`, returning them LSB-aligned and
237
+ /// little-endian (padded to a byte boundary). Mirrors `extractBits` in libs/node/process_image.cc.
238
+ export function extractBits(src, bitOffset, bitLength) {
239
+ const out = new Uint8Array((bitLength + 7) >> 3);
240
+ if (bitOffset % 8 === 0 && bitLength % 8 === 0) {
241
+ const byteOffset = bitOffset >> 3;
242
+ for (let i = 0; i < out.length && byteOffset + i < src.length; i++) {
243
+ out[i] = src[byteOffset + i];
244
+ }
245
+ return out;
246
+ }
247
+ for (let i = 0; i < bitLength; i++) {
248
+ const srcBit = bitOffset + i;
249
+ const srcByte = srcBit >> 3;
250
+ if (srcByte >= src.length) {
251
+ break;
252
+ }
253
+ const bit = (src[srcByte] >> (srcBit & 7)) & 1;
254
+ out[i >> 3] |= bit << (i & 7);
255
+ }
256
+ return out;
257
+ }
258
+ /// Decodes LSB-aligned little-endian `bytes` (as produced by `extractBits`) for the given ETG.1020
259
+ /// `dataType`. Returns a number for fixed-width numerics, a bigint for 64-bit integers, a string
260
+ /// for VISIBLE/UNICODE strings (trailing NULs/spaces stripped), or null when the type is unknown
261
+ /// (dataType 0) or there are too few bytes. Unmapped codes fall back to an unsigned read of the
262
+ /// available bytes (covers BITn fields).
263
+ export function decodeMmpdValue(dataType, bytes) {
264
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
265
+ const n = bytes.byteLength;
266
+ switch (dataType) {
267
+ case 1 /* DataType.BOOLEAN */:
268
+ case 5 /* DataType.UNSIGNED8 */:
269
+ case 30 /* DataType.BYTE */:
270
+ case 45 /* DataType.BITARR8 */:
271
+ return n >= 1 ? dv.getUint8(0) : null;
272
+ case 2 /* DataType.INTEGER8 */:
273
+ return n >= 1 ? dv.getInt8(0) : null;
274
+ case 3 /* DataType.INTEGER16 */:
275
+ return n >= 2 ? dv.getInt16(0, true) : null;
276
+ case 6 /* DataType.UNSIGNED16 */:
277
+ case 31 /* DataType.WORD */:
278
+ case 46 /* DataType.BITARR16 */:
279
+ return n >= 2 ? dv.getUint16(0, true) : null;
280
+ case 4 /* DataType.INTEGER32 */:
281
+ return n >= 4 ? dv.getInt32(0, true) : null;
282
+ case 7 /* DataType.UNSIGNED32 */:
283
+ case 32 /* DataType.DWORD */:
284
+ case 47 /* DataType.BITARR32 */:
285
+ return n >= 4 ? dv.getUint32(0, true) : null;
286
+ case 8 /* DataType.REAL32 */:
287
+ return n >= 4 ? dv.getFloat32(0, true) : null;
288
+ case 17 /* DataType.REAL64 */:
289
+ return n >= 8 ? dv.getFloat64(0, true) : null;
290
+ case 21 /* DataType.INTEGER64 */:
291
+ return n >= 8 ? dv.getBigInt64(0, true) : null;
292
+ case 27 /* DataType.UNSIGNED64 */:
293
+ return n >= 8 ? dv.getBigUint64(0, true) : null;
294
+ case 9 /* DataType.VISIBLE_STRING */:
295
+ case 11 /* DataType.UNICODE_STRING */: {
296
+ let end = n;
297
+ while (end > 0 && (bytes[end - 1] === 0x00 || bytes[end - 1] === 0x20)) {
298
+ end--;
299
+ }
300
+ return utf8.decode(bytes.subarray(0, end));
301
+ }
302
+ case 0x0000:
303
+ return null; // OD not enumerated — undecodable
304
+ default: {
305
+ // Unknown/sub-byte (e.g. BIT1..BIT16): read the available bytes as a little-endian unsigned.
306
+ if (n === 0 || n > 4) {
307
+ return null;
308
+ }
309
+ let v = 0;
310
+ for (let i = 0; i < n; i++) {
311
+ v |= bytes[i] << (8 * i);
312
+ }
313
+ return v >>> 0;
314
+ }
315
+ }
316
+ }
317
+ /// Whether `decodeSeries` yields plottable numbers for this ETG.1020 data-type code — a
318
+ /// fixed-width integer/real or a bit field, but not a string, struct, or unenumerated type
319
+ /// (dataType 0). Useful for offering only chartable objects in a UI.
320
+ export function isPlottableDataType(dataType) {
321
+ return numericReader(dataType) !== null || (dataType >= 0x0030 && dataType <= 0x003f);
322
+ }
package/dist/sdo.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export declare const SDO_TYPES: readonly ["uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64", "float32", "float64", "string", "raw"];
2
+ export type SdoType = (typeof SDO_TYPES)[number];
3
+ export declare const SDO_TYPE_HINT: Record<SdoType, string>;
4
+ export declare function parseIntegerFlexible(s: string): bigint | null;
5
+ export declare function isIntegerSdoType(type: SdoType): boolean;
6
+ export declare function encodeSdoValue(type: SdoType, raw: string): {
7
+ bytes: number[];
8
+ } | {
9
+ error: string;
10
+ };
11
+ export declare function sdoTypeForDataTypeName(dataTypeName: string): SdoType;
12
+ export interface SdoInterpretation {
13
+ label: string;
14
+ value: string;
15
+ }
16
+ export declare function interpretSdoBytes(bytes: number[]): SdoInterpretation[];
17
+ export declare function decodeSdoBytes(dataTypeName: string, bytes: number[]): number | string | number[];
package/dist/sdo.js ADDED
@@ -0,0 +1,219 @@
1
+ // Encoding and decoding helpers for CoE SDO values: turning the raw little-endian
2
+ // byte arrays exchanged over the wire into human-readable values and back.
3
+ import { parseHexBytes } from './hex.js';
4
+ // CoE data types the SDO download card can encode a value into. `raw` is an
5
+ // escape hatch for entering bytes directly.
6
+ export const SDO_TYPES = [
7
+ 'uint8', 'int8', 'uint16', 'int16', 'uint32', 'int32',
8
+ 'uint64', 'int64', 'float32', 'float64', 'string', 'raw',
9
+ ];
10
+ // Placeholder text shown in the value input for each type.
11
+ export const SDO_TYPE_HINT = {
12
+ uint8: 'e.g. 200 or 0xC8',
13
+ int8: 'e.g. -42',
14
+ uint16: 'e.g. 1000 or 0x03E8',
15
+ int16: 'e.g. -1000',
16
+ uint32: 'e.g. 100000',
17
+ int32: 'e.g. -100000',
18
+ uint64: 'e.g. 4294967296',
19
+ int64: 'e.g. -4294967296',
20
+ float32: 'e.g. 3.14',
21
+ float64: 'e.g. 3.141592653589793',
22
+ string: 'e.g. SOMANET',
23
+ raw: 'hex bytes, e.g. 01 02 0A FF',
24
+ };
25
+ const INT_ENC = {
26
+ uint8: { size: 1, signed: false }, int8: { size: 1, signed: true },
27
+ uint16: { size: 2, signed: false }, int16: { size: 2, signed: true },
28
+ uint32: { size: 4, signed: false }, int32: { size: 4, signed: true },
29
+ uint64: { size: 8, signed: false }, int64: { size: 8, signed: true },
30
+ };
31
+ // Accepts decimal (with optional leading '-') or 0x-prefixed hex; returns null on anything else.
32
+ // Signed hex is signed-magnitude ("-0x1"), not two's-complement — so a value round-trips through
33
+ // the range check in encodeSdoValue regardless of the base it was entered in.
34
+ export function parseIntegerFlexible(s) {
35
+ if (/^-?[0-9]+$/.test(s))
36
+ return BigInt(s);
37
+ if (/^0[xX][0-9a-fA-F]+$/.test(s))
38
+ return BigInt(s);
39
+ if (/^-0[xX][0-9a-fA-F]+$/.test(s))
40
+ return -BigInt(s.slice(1));
41
+ return null;
42
+ }
43
+ // True for the fixed-width integer types — the ones a value can meaningfully be shown and edited
44
+ // in hexadecimal (floats and strings have no base; `raw` is already entered as hex bytes).
45
+ export function isIntegerSdoType(type) {
46
+ return type in INT_ENC;
47
+ }
48
+ // Encodes a human-entered value into the little-endian byte sequence for an SDO download.
49
+ export function encodeSdoValue(type, raw) {
50
+ const s = raw.trim();
51
+ const meta = INT_ENC[type];
52
+ if (meta) {
53
+ const n = parseIntegerFlexible(s);
54
+ if (n === null)
55
+ return { error: 'Not a valid integer' };
56
+ const bits = BigInt(meta.size * 8);
57
+ const min = meta.signed ? -(1n << (bits - 1n)) : 0n;
58
+ const max = meta.signed ? (1n << (bits - 1n)) - 1n : (1n << bits) - 1n;
59
+ if (n < min || n > max)
60
+ return { error: `Out of range for ${type} [${min}, ${max}]` };
61
+ const view = new DataView(new ArrayBuffer(meta.size));
62
+ if (meta.size === 1)
63
+ meta.signed ? view.setInt8(0, Number(n)) : view.setUint8(0, Number(n));
64
+ else if (meta.size === 2)
65
+ meta.signed ? view.setInt16(0, Number(n), true) : view.setUint16(0, Number(n), true);
66
+ else if (meta.size === 4)
67
+ meta.signed ? view.setInt32(0, Number(n), true) : view.setUint32(0, Number(n), true);
68
+ else
69
+ meta.signed ? view.setBigInt64(0, n, true) : view.setBigUint64(0, n, true);
70
+ return { bytes: [...new Uint8Array(view.buffer)] };
71
+ }
72
+ if (type === 'float32' || type === 'float64') {
73
+ if (s === '')
74
+ return { error: 'Value is empty' };
75
+ const f = Number(s);
76
+ if (Number.isNaN(f))
77
+ return { error: 'Not a valid number' };
78
+ const size = type === 'float32' ? 4 : 8;
79
+ const view = new DataView(new ArrayBuffer(size));
80
+ if (type === 'float32')
81
+ view.setFloat32(0, f, true);
82
+ else
83
+ view.setFloat64(0, f, true);
84
+ return { bytes: [...new Uint8Array(view.buffer)] };
85
+ }
86
+ if (type === 'string') {
87
+ return { bytes: [...new TextEncoder().encode(raw)] };
88
+ }
89
+ // raw: whitespace- or comma-separated hex bytes
90
+ const bytes = parseHexBytes(s);
91
+ if (bytes === null)
92
+ return { error: 'Not a valid even-length hex byte string' };
93
+ return { bytes };
94
+ }
95
+ // Maps a CoE object's data-type name (as reported by SDO Info) to the SdoType the
96
+ // encoder understands, so a value typed inline against an object can be encoded
97
+ // without the user picking a type. Falls back to `raw` for unknown/complex types.
98
+ export function sdoTypeForDataTypeName(dataTypeName) {
99
+ switch (dataTypeName) {
100
+ case 'BOOLEAN':
101
+ case 'UNSIGNED8':
102
+ case 'BYTE':
103
+ return 'uint8';
104
+ case 'INTEGER8':
105
+ return 'int8';
106
+ case 'UNSIGNED16':
107
+ case 'WORD':
108
+ return 'uint16';
109
+ case 'INTEGER16':
110
+ return 'int16';
111
+ case 'UNSIGNED32':
112
+ case 'DWORD':
113
+ return 'uint32';
114
+ case 'INTEGER32':
115
+ return 'int32';
116
+ case 'UNSIGNED64':
117
+ return 'uint64';
118
+ case 'INTEGER64':
119
+ return 'int64';
120
+ case 'REAL32':
121
+ return 'float32';
122
+ case 'REAL64':
123
+ return 'float64';
124
+ case 'VISIBLE_STRING':
125
+ case 'UNICODE_STRING':
126
+ return 'string';
127
+ default:
128
+ return 'raw';
129
+ }
130
+ }
131
+ // Best-effort interpretation of raw SDO bytes as every fixed-width type that fits,
132
+ // plus a printable-string reading. Used to show what an uploaded value could mean.
133
+ export function interpretSdoBytes(bytes) {
134
+ const view = new DataView(new Uint8Array(bytes).buffer);
135
+ const out = [];
136
+ if (bytes.length === 1) {
137
+ out.push({ label: 'int8', value: String(view.getInt8(0)) });
138
+ out.push({ label: 'uint8', value: String(view.getUint8(0)) });
139
+ }
140
+ else if (bytes.length === 2) {
141
+ out.push({ label: 'int16 LE', value: String(view.getInt16(0, true)) });
142
+ out.push({ label: 'uint16 LE', value: String(view.getUint16(0, true)) });
143
+ }
144
+ else if (bytes.length === 4) {
145
+ out.push({ label: 'int32 LE', value: String(view.getInt32(0, true)) });
146
+ out.push({ label: 'uint32 LE', value: String(view.getUint32(0, true)) });
147
+ out.push({ label: 'float32 LE', value: String(view.getFloat32(0, true)) });
148
+ }
149
+ else if (bytes.length === 8) {
150
+ out.push({ label: 'int64 LE', value: view.getBigInt64(0, true).toString() });
151
+ out.push({ label: 'uint64 LE', value: view.getBigUint64(0, true).toString() });
152
+ out.push({ label: 'float64 LE', value: String(view.getFloat64(0, true)) });
153
+ }
154
+ if (bytes.length > 0 && bytes.every(b => (b >= 0x20 && b <= 0x7e) || b === 0)) {
155
+ // VISIBLE_STRING is space-padded to a fixed width and may carry a terminating
156
+ // NUL — neither is significant, so strip trailing 0x20/0x00 before display.
157
+ let len = bytes.length;
158
+ while (len > 0 && (bytes[len - 1] === 0x00 || bytes[len - 1] === 0x20))
159
+ len--;
160
+ const str = new TextDecoder().decode(new Uint8Array(bytes.slice(0, len).filter(b => b !== 0)));
161
+ out.push({ label: 'string', value: `"${str}"` });
162
+ }
163
+ return out;
164
+ }
165
+ // Decodes raw SDO bytes into a typed value, given the object's CoE data-type name.
166
+ // Falls back to the raw byte array for unknown types or short reads.
167
+ export function decodeSdoBytes(dataTypeName, bytes) {
168
+ const view = new DataView(new Uint8Array(bytes).buffer);
169
+ switch (dataTypeName) {
170
+ case 'BOOLEAN':
171
+ case 'UNSIGNED8':
172
+ case 'BYTE':
173
+ if (bytes.length >= 1)
174
+ return view.getUint8(0);
175
+ break;
176
+ case 'INTEGER8':
177
+ if (bytes.length >= 1)
178
+ return view.getInt8(0);
179
+ break;
180
+ case 'INTEGER16':
181
+ if (bytes.length >= 2)
182
+ return view.getInt16(0, true);
183
+ break;
184
+ case 'INTEGER32':
185
+ if (bytes.length >= 4)
186
+ return view.getInt32(0, true);
187
+ break;
188
+ case 'UNSIGNED16':
189
+ case 'WORD':
190
+ if (bytes.length >= 2)
191
+ return view.getUint16(0, true);
192
+ break;
193
+ case 'UNSIGNED32':
194
+ case 'DWORD':
195
+ if (bytes.length >= 4)
196
+ return view.getUint32(0, true);
197
+ break;
198
+ case 'REAL32':
199
+ if (bytes.length >= 4)
200
+ return view.getFloat32(0, true);
201
+ break;
202
+ case 'REAL64':
203
+ if (bytes.length >= 8)
204
+ return view.getFloat64(0, true);
205
+ break;
206
+ case 'INTEGER64':
207
+ if (bytes.length >= 8)
208
+ return Number(view.getBigInt64(0, true));
209
+ break;
210
+ case 'UNSIGNED64':
211
+ if (bytes.length >= 8)
212
+ return Number(view.getBigUint64(0, true));
213
+ break;
214
+ case 'VISIBLE_STRING':
215
+ case 'UNICODE_STRING':
216
+ return new TextDecoder().decode(new Uint8Array(bytes.filter(b => b !== 0)));
217
+ }
218
+ return bytes;
219
+ }
@@ -0,0 +1,70 @@
1
+ export type ReadyState = 'connecting' | 'open' | 'closed';
2
+ export type MonitoringRow = (number | null)[];
3
+ export type MonitoringBatch = MonitoringRow[];
4
+ export interface MonitoringMessage {
5
+ type: 'monitoring';
6
+ topic: string;
7
+ data: MonitoringBatch;
8
+ }
9
+ export interface NotificationMessage {
10
+ type: 'notification';
11
+ data: unknown;
12
+ }
13
+ export interface ProgressMessage {
14
+ type: 'progress';
15
+ data: unknown;
16
+ }
17
+ export type ServerMessage = MonitoringMessage | NotificationMessage | ProgressMessage;
18
+ type MonitoringListener = (rows: MonitoringBatch) => void;
19
+ type NotificationListener = (data: unknown) => void;
20
+ type ProgressListener = (data: unknown) => void;
21
+ type StateListener = (state: ReadyState) => void;
22
+ export declare const NOTIFICATION_TOPIC = "notifications";
23
+ export interface WebSocketLike {
24
+ readyState: number;
25
+ send(data: string): void;
26
+ close(): void;
27
+ onopen: ((event: unknown) => void) | null;
28
+ onclose: ((event: unknown) => void) | null;
29
+ onerror: ((event: unknown) => void) | null;
30
+ onmessage: ((event: {
31
+ data: unknown;
32
+ }) => void) | null;
33
+ }
34
+ export type WebSocketConstructor = new (url: string, protocolsOrOptions?: unknown) => WebSocketLike;
35
+ export interface WebSocketConnectionOptions {
36
+ url: string;
37
+ WebSocket?: WebSocketConstructor;
38
+ protocolsOrOptions?: unknown;
39
+ reconnectDelayMs?: number;
40
+ maxReconnectDelayMs?: number;
41
+ }
42
+ export declare class WebSocketConnection {
43
+ private readonly url;
44
+ private readonly WebSocketImpl?;
45
+ private readonly protocolsOrOptions?;
46
+ private readonly reconnectDelayMs;
47
+ private readonly maxReconnectDelayMs;
48
+ private socket;
49
+ private state;
50
+ private closedByUser;
51
+ private reconnectAttempts;
52
+ private reconnectTimer;
53
+ private readonly topicListeners;
54
+ private readonly notificationListeners;
55
+ private readonly progressListeners;
56
+ private readonly stateListeners;
57
+ constructor(options: WebSocketConnectionOptions);
58
+ get readyState(): ReadyState;
59
+ connect(): void;
60
+ close(): void;
61
+ subscribe(topic: string, listener: MonitoringListener): () => void;
62
+ onNotification(listener: NotificationListener): () => void;
63
+ onProgress(listener: ProgressListener): () => void;
64
+ onStateChange(listener: StateListener): () => void;
65
+ private setState;
66
+ private send;
67
+ private handleDisconnect;
68
+ private route;
69
+ }
70
+ export {};