@synapticon/motion-master-client 2.3.0-beta.0 → 6.0.0-alpha.100
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/LICENSE +21 -0
- package/dist/api-error.d.ts +25 -0
- package/dist/api-error.js +52 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.js +37 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +8 -0
- package/dist/eni.d.ts +197 -0
- package/dist/eni.js +61 -0
- package/dist/generated/Api.d.ts +1886 -0
- package/dist/generated/Api.js +1293 -0
- package/dist/generated/data-contracts.d.ts +2282 -0
- package/dist/generated/data-contracts.js +12 -0
- package/dist/generated/http-client.d.ts +60 -0
- package/dist/generated/http-client.js +171 -0
- package/dist/hex.d.ts +2 -0
- package/dist/hex.js +20 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.js +20 -22
- package/dist/lan.d.ts +4 -0
- package/dist/lan.js +59 -0
- package/dist/mmpd.d.ts +53 -0
- package/dist/mmpd.js +322 -0
- package/dist/sdo.d.ts +17 -0
- package/dist/sdo.js +219 -0
- package/dist/web-socket-connection.d.ts +70 -0
- package/dist/web-socket-connection.js +237 -0
- package/dist/wire-time.d.ts +2 -0
- package/dist/wire-time.js +18 -0
- package/package.json +29 -35
- package/.vscode/launch.json +0 -23
- package/README.md +0 -118
- package/box.config.json +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/motion-master-client-web-socket-connection.d.ts +0 -43
- package/dist/motion-master-client-web-socket-connection.d.ts.map +0 -1
- package/dist/motion-master-client-web-socket-connection.js +0 -91
- package/dist/motion-master-client-web-socket-connection.js.map +0 -1
- package/dist/motion-master-client.d.ts +0 -105
- package/dist/motion-master-client.d.ts.map +0 -1
- package/dist/motion-master-client.js +0 -348
- package/dist/motion-master-client.js.map +0 -1
- package/dist/motion-master-notification-subscribe-data.d.ts +0 -8
- package/dist/motion-master-notification-subscribe-data.d.ts.map +0 -1
- package/dist/motion-master-notification-subscribe-data.js +0 -3
- package/dist/motion-master-notification-subscribe-data.js.map +0 -1
- package/dist/motion-master-notification-web-socket-connection.d.ts +0 -56
- package/dist/motion-master-notification-web-socket-connection.d.ts.map +0 -1
- package/dist/motion-master-notification-web-socket-connection.js +0 -115
- package/dist/motion-master-notification-web-socket-connection.js.map +0 -1
- package/dist/motion-master-notification.d.ts +0 -31
- package/dist/motion-master-notification.d.ts.map +0 -1
- package/dist/motion-master-notification.js +0 -44
- package/dist/motion-master-notification.js.map +0 -1
- package/dist/util.d.ts +0 -16
- package/dist/util.d.ts.map +0 -1
- package/dist/util.js +0 -24
- package/dist/util.js.map +0 -1
- package/licenses.format.json +0 -4
- package/motion-master-client.config.json +0 -7
- package/smm_data_dic.json +0 -1213
- package/src/index.ts +0 -7
- package/src/motion-master-client-web-socket-connection.ts +0 -104
- package/src/motion-master-client.ts +0 -468
- package/src/motion-master-notification-subscribe-data.ts +0 -7
- package/src/motion-master-notification-web-socket-connection.ts +0 -143
- package/src/motion-master-notification.ts +0 -51
- package/src/util.ts +0 -22
- package/test.sh +0 -122
- package/tsconfig.json +0 -63
- 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 {};
|