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

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/dist/client.d.ts +20 -0
  3. package/dist/client.js +37 -0
  4. package/dist/constants.d.ts +2 -0
  5. package/dist/constants.js +8 -0
  6. package/dist/generated/Api.d.ts +991 -0
  7. package/dist/generated/Api.js +614 -0
  8. package/dist/generated/data-contracts.d.ts +644 -0
  9. package/dist/generated/data-contracts.js +12 -0
  10. package/dist/generated/http-client.d.ts +60 -0
  11. package/dist/generated/http-client.js +171 -0
  12. package/dist/hex.d.ts +2 -0
  13. package/dist/hex.js +20 -0
  14. package/dist/index.d.ts +10 -7
  15. package/dist/index.js +16 -22
  16. package/dist/mmpd.d.ts +54 -0
  17. package/dist/mmpd.js +323 -0
  18. package/dist/sdo.d.ts +15 -0
  19. package/dist/sdo.js +212 -0
  20. package/dist/somanet.d.ts +5 -0
  21. package/dist/somanet.js +15 -0
  22. package/dist/web-socket-connection.d.ts +69 -0
  23. package/dist/web-socket-connection.js +206 -0
  24. package/package.json +29 -35
  25. package/.vscode/launch.json +0 -23
  26. package/README.md +0 -118
  27. package/box.config.json +0 -7
  28. package/dist/index.d.ts.map +0 -1
  29. package/dist/index.js.map +0 -1
  30. package/dist/motion-master-client-web-socket-connection.d.ts +0 -43
  31. package/dist/motion-master-client-web-socket-connection.d.ts.map +0 -1
  32. package/dist/motion-master-client-web-socket-connection.js +0 -91
  33. package/dist/motion-master-client-web-socket-connection.js.map +0 -1
  34. package/dist/motion-master-client.d.ts +0 -105
  35. package/dist/motion-master-client.d.ts.map +0 -1
  36. package/dist/motion-master-client.js +0 -348
  37. package/dist/motion-master-client.js.map +0 -1
  38. package/dist/motion-master-notification-subscribe-data.d.ts +0 -8
  39. package/dist/motion-master-notification-subscribe-data.d.ts.map +0 -1
  40. package/dist/motion-master-notification-subscribe-data.js +0 -3
  41. package/dist/motion-master-notification-subscribe-data.js.map +0 -1
  42. package/dist/motion-master-notification-web-socket-connection.d.ts +0 -56
  43. package/dist/motion-master-notification-web-socket-connection.d.ts.map +0 -1
  44. package/dist/motion-master-notification-web-socket-connection.js +0 -115
  45. package/dist/motion-master-notification-web-socket-connection.js.map +0 -1
  46. package/dist/motion-master-notification.d.ts +0 -31
  47. package/dist/motion-master-notification.d.ts.map +0 -1
  48. package/dist/motion-master-notification.js +0 -44
  49. package/dist/motion-master-notification.js.map +0 -1
  50. package/dist/util.d.ts +0 -16
  51. package/dist/util.d.ts.map +0 -1
  52. package/dist/util.js +0 -24
  53. package/dist/util.js.map +0 -1
  54. package/licenses.format.json +0 -4
  55. package/motion-master-client.config.json +0 -7
  56. package/smm_data_dic.json +0 -1213
  57. package/src/index.ts +0 -7
  58. package/src/motion-master-client-web-socket-connection.ts +0 -104
  59. package/src/motion-master-client.ts +0 -468
  60. package/src/motion-master-notification-subscribe-data.ts +0 -7
  61. package/src/motion-master-notification-web-socket-connection.ts +0 -143
  62. package/src/motion-master-notification.ts +0 -51
  63. package/src/util.ts +0 -22
  64. package/test.sh +0 -122
  65. package/tsconfig.json +0 -63
  66. package/tslint.json +0 -118
@@ -0,0 +1,171 @@
1
+ /* eslint-disable */
2
+ /* tslint:disable */
3
+ // @ts-nocheck
4
+ /*
5
+ * ---------------------------------------------------------------
6
+ * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
7
+ * ## ##
8
+ * ## AUTHOR: acacode ##
9
+ * ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
10
+ * ---------------------------------------------------------------
11
+ */
12
+ export var ContentType;
13
+ (function (ContentType) {
14
+ ContentType["Json"] = "application/json";
15
+ ContentType["JsonApi"] = "application/vnd.api+json";
16
+ ContentType["FormData"] = "multipart/form-data";
17
+ ContentType["UrlEncoded"] = "application/x-www-form-urlencoded";
18
+ ContentType["Text"] = "text/plain";
19
+ })(ContentType || (ContentType = {}));
20
+ export class HttpClient {
21
+ baseUrl = "https://local.motion-master.synapticon.com:61447";
22
+ securityData = null;
23
+ securityWorker;
24
+ abortControllers = new Map();
25
+ customFetch = (...fetchParams) => fetch(...fetchParams);
26
+ baseApiParams = {
27
+ credentials: "same-origin",
28
+ headers: {},
29
+ redirect: "follow",
30
+ referrerPolicy: "no-referrer",
31
+ };
32
+ constructor(apiConfig = {}) {
33
+ Object.assign(this, apiConfig);
34
+ }
35
+ setSecurityData = (data) => {
36
+ this.securityData = data;
37
+ };
38
+ encodeQueryParam(key, value) {
39
+ const encodedKey = encodeURIComponent(key);
40
+ return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
41
+ }
42
+ addQueryParam(query, key) {
43
+ return this.encodeQueryParam(key, query[key]);
44
+ }
45
+ addArrayQueryParam(query, key) {
46
+ const value = query[key];
47
+ return value.map((v) => this.encodeQueryParam(key, v)).join("&");
48
+ }
49
+ toQueryString(rawQuery) {
50
+ const query = rawQuery || {};
51
+ const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
52
+ return keys
53
+ .map((key) => Array.isArray(query[key])
54
+ ? this.addArrayQueryParam(query, key)
55
+ : this.addQueryParam(query, key))
56
+ .join("&");
57
+ }
58
+ addQueryParams(rawQuery) {
59
+ const queryString = this.toQueryString(rawQuery);
60
+ return queryString ? `?${queryString}` : "";
61
+ }
62
+ contentFormatters = {
63
+ [ContentType.Json]: (input) => input !== null && (typeof input === "object" || typeof input === "string")
64
+ ? JSON.stringify(input)
65
+ : input,
66
+ [ContentType.JsonApi]: (input) => input !== null && (typeof input === "object" || typeof input === "string")
67
+ ? JSON.stringify(input)
68
+ : input,
69
+ [ContentType.Text]: (input) => input !== null && typeof input !== "string"
70
+ ? JSON.stringify(input)
71
+ : input,
72
+ [ContentType.FormData]: (input) => {
73
+ if (input instanceof FormData) {
74
+ return input;
75
+ }
76
+ return Object.keys(input || {}).reduce((formData, key) => {
77
+ const property = input[key];
78
+ formData.append(key, property instanceof Blob
79
+ ? property
80
+ : typeof property === "object" && property !== null
81
+ ? JSON.stringify(property)
82
+ : `${property}`);
83
+ return formData;
84
+ }, new FormData());
85
+ },
86
+ [ContentType.UrlEncoded]: (input) => this.toQueryString(input),
87
+ };
88
+ mergeRequestParams(params1, params2) {
89
+ return {
90
+ ...this.baseApiParams,
91
+ ...params1,
92
+ ...(params2 || {}),
93
+ headers: {
94
+ ...(this.baseApiParams.headers || {}),
95
+ ...(params1.headers || {}),
96
+ ...((params2 && params2.headers) || {}),
97
+ },
98
+ };
99
+ }
100
+ createAbortSignal = (cancelToken) => {
101
+ if (this.abortControllers.has(cancelToken)) {
102
+ const abortController = this.abortControllers.get(cancelToken);
103
+ if (abortController) {
104
+ return abortController.signal;
105
+ }
106
+ return void 0;
107
+ }
108
+ const abortController = new AbortController();
109
+ this.abortControllers.set(cancelToken, abortController);
110
+ return abortController.signal;
111
+ };
112
+ abortRequest = (cancelToken) => {
113
+ const abortController = this.abortControllers.get(cancelToken);
114
+ if (abortController) {
115
+ abortController.abort();
116
+ this.abortControllers.delete(cancelToken);
117
+ }
118
+ };
119
+ request = async ({ body, secure, path, type, query, format, baseUrl, cancelToken, ...params }) => {
120
+ const secureParams = ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
121
+ this.securityWorker &&
122
+ (await this.securityWorker(this.securityData))) ||
123
+ {};
124
+ const requestParams = this.mergeRequestParams(params, secureParams);
125
+ const queryString = query && this.toQueryString(query);
126
+ const payloadFormatter = this.contentFormatters[type || ContentType.Json];
127
+ const responseFormat = format || requestParams.format;
128
+ return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, {
129
+ ...requestParams,
130
+ headers: {
131
+ ...(requestParams.headers || {}),
132
+ ...(type && type !== ContentType.FormData
133
+ ? { "Content-Type": type }
134
+ : {}),
135
+ },
136
+ signal: (cancelToken
137
+ ? this.createAbortSignal(cancelToken)
138
+ : requestParams.signal) || null,
139
+ body: typeof body === "undefined" || body === null
140
+ ? null
141
+ : payloadFormatter(body),
142
+ }).then(async (response) => {
143
+ const r = response;
144
+ r.data = null;
145
+ r.error = null;
146
+ const responseToParse = responseFormat ? response.clone() : response;
147
+ const data = !responseFormat
148
+ ? r
149
+ : await responseToParse[responseFormat]()
150
+ .then((data) => {
151
+ if (r.ok) {
152
+ r.data = data;
153
+ }
154
+ else {
155
+ r.error = data;
156
+ }
157
+ return r;
158
+ })
159
+ .catch((e) => {
160
+ r.error = e;
161
+ return r;
162
+ });
163
+ if (cancelToken) {
164
+ this.abortControllers.delete(cancelToken);
165
+ }
166
+ if (!response.ok)
167
+ throw data;
168
+ return data;
169
+ });
170
+ };
171
+ }
package/dist/hex.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function formatHex(value: number, width?: number, prefix?: boolean): string;
2
+ export declare function parseHexBytes(input: string): number[] | null;
package/dist/hex.js ADDED
@@ -0,0 +1,20 @@
1
+ // Format a number as an uppercase hex string, zero-padded to `width` digits. With `prefix`
2
+ // (the default) it carries the conventional "0x"; pass false for bare digits — e.g. a subindex
3
+ // rendered after a colon ("0x6040:00"). The display inverse of parseHexBytes.
4
+ export function formatHex(value, width = 4, prefix = true) {
5
+ return `${prefix ? '0x' : ''}${value.toString(16).toUpperCase().padStart(width, '0')}`;
6
+ }
7
+ // Parse a string of hex bytes ("0A FF", "0x0AFF", "0a,ff") into a byte array.
8
+ // Returns null when the input is empty or not a valid even-length hex string.
9
+ export function parseHexBytes(input) {
10
+ const cleaned = input.replace(/0x/gi, '').replace(/[\s,]+/g, '');
11
+ if (cleaned.length === 0 || cleaned.length % 2 !== 0)
12
+ return null;
13
+ if (!/^[0-9a-fA-F]+$/.test(cleaned))
14
+ return null;
15
+ const bytes = [];
16
+ for (let i = 0; i < cleaned.length; i += 2) {
17
+ bytes.push(parseInt(cleaned.slice(i, i + 2), 16));
18
+ }
19
+ return bytes;
20
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
- export { MotionMasterClientWebSocketConnection } from './motion-master-client-web-socket-connection';
2
- export { MotionMasterClient } from './motion-master-client';
3
- export { IMotionMasterNotificationSubscribeData } from './motion-master-notification-subscribe-data';
4
- export { MotionMasterNotificationWebSocketConnection } from './motion-master-notification-web-socket-connection';
5
- export { MotionMasterNotification } from './motion-master-notification';
6
- export * from './util';
7
- //# sourceMappingURL=index.d.ts.map
1
+ export * from './generated/http-client.js';
2
+ export * from './generated/data-contracts.js';
3
+ export * from './generated/Api.js';
4
+ export * from './constants.js';
5
+ export * from './web-socket-connection.js';
6
+ export * from './client.js';
7
+ export * from './hex.js';
8
+ export * from './sdo.js';
9
+ export * from './somanet.js';
10
+ export * from './mmpd.js';
package/dist/index.js CHANGED
@@ -1,22 +1,16 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
- }) : (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- o[k2] = m[k];
8
- }));
9
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
- for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p);
11
- };
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- var motion_master_client_web_socket_connection_1 = require("./motion-master-client-web-socket-connection");
14
- Object.defineProperty(exports, "MotionMasterClientWebSocketConnection", { enumerable: true, get: function () { return motion_master_client_web_socket_connection_1.MotionMasterClientWebSocketConnection; } });
15
- var motion_master_client_1 = require("./motion-master-client");
16
- Object.defineProperty(exports, "MotionMasterClient", { enumerable: true, get: function () { return motion_master_client_1.MotionMasterClient; } });
17
- var motion_master_notification_web_socket_connection_1 = require("./motion-master-notification-web-socket-connection");
18
- Object.defineProperty(exports, "MotionMasterNotificationWebSocketConnection", { enumerable: true, get: function () { return motion_master_notification_web_socket_connection_1.MotionMasterNotificationWebSocketConnection; } });
19
- var motion_master_notification_1 = require("./motion-master-notification");
20
- Object.defineProperty(exports, "MotionMasterNotification", { enumerable: true, get: function () { return motion_master_notification_1.MotionMasterNotification; } });
21
- __exportStar(require("./util"), exports);
22
- //# sourceMappingURL=index.js.map
1
+ // Public entry point for @synapticon/motion-master-client.
2
+ //
3
+ // `src/generated/` is the swagger-typescript-api output — regenerate with `pnpm generate`, never
4
+ // hand-edit. Everything else here is the hand-written SDK layer (facade + WebSocket connection).
5
+ export * from './generated/http-client.js';
6
+ export * from './generated/data-contracts.js';
7
+ export * from './generated/Api.js';
8
+ export * from './constants.js';
9
+ export * from './web-socket-connection.js';
10
+ export * from './client.js';
11
+ // Pure, framework-agnostic helpers (no DOM/React): hex formatting, CoE SDO value
12
+ // encode/decode, and SOMANET file-list parsing.
13
+ export * from './hex.js';
14
+ export * from './sdo.js';
15
+ export * from './somanet.js';
16
+ export * from './mmpd.js';
package/dist/mmpd.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ export interface MmpdPdoEntry {
2
+ index: number;
3
+ subindex: number;
4
+ isOutput: boolean;
5
+ dataType: number;
6
+ bitLength: number;
7
+ bitOffset: number;
8
+ name: string;
9
+ }
10
+ export interface MmpdDevice {
11
+ slavePosition: number;
12
+ vendorId: number;
13
+ productCode: number;
14
+ revisionNumber: number;
15
+ serialNumber: number;
16
+ name: string;
17
+ entries: MmpdPdoEntry[];
18
+ }
19
+ export interface MmpdHeader {
20
+ formatVersion: number;
21
+ flags: number;
22
+ cyclePeriodUs: number;
23
+ startSequence: bigint;
24
+ rowCount: number;
25
+ inputBytes: number;
26
+ outputBytes: number;
27
+ devices: MmpdDevice[];
28
+ }
29
+ export interface MmpdRow {
30
+ sequence: bigint;
31
+ timestampNs: bigint;
32
+ inputs: Uint8Array;
33
+ outputs: Uint8Array;
34
+ }
35
+ export declare class MmpdFile {
36
+ readonly header: MmpdHeader;
37
+ private readonly u8;
38
+ private readonly dv;
39
+ private readonly rowsStart;
40
+ constructor(header: MmpdHeader, u8: Uint8Array, dv: DataView, rowsStart: number);
41
+ get rowCount(): number;
42
+ get rowStride(): number;
43
+ rowAt(i: number): MmpdRow;
44
+ timestampsUs(): Float64Array;
45
+ decodeSeries(entry: MmpdPdoEntry): (number | null)[];
46
+ }
47
+ export declare function parseMmpd(data: ArrayBuffer | Uint8Array): MmpdFile;
48
+ export declare function fetchProcessDataDump(baseUrl: string, options?: {
49
+ fetch?: typeof fetch;
50
+ signal?: AbortSignal;
51
+ }): Promise<MmpdFile>;
52
+ export declare function extractBits(src: Uint8Array, bitOffset: number, bitLength: number): Uint8Array;
53
+ export declare function decodeMmpdValue(dataType: number, bytes: Uint8Array): number | bigint | string | null;
54
+ export declare function isPlottableDataType(dataType: number): boolean;
package/dist/mmpd.js ADDED
@@ -0,0 +1,323 @@
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 (40 bytes): magic 'MMPD', u16 formatVersion, u16 flags, u32 cyclePeriodUs,
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 < 40 || 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
+ cyclePeriodUs: dv.getUint32(8, true),
138
+ startSequence: dv.getBigUint64(12, true),
139
+ rowCount: Number(dv.getBigUint64(20, true)),
140
+ inputBytes: dv.getUint32(28, true),
141
+ outputBytes: dv.getUint32(32, true),
142
+ devices: [],
143
+ };
144
+ const deviceCount = dv.getUint32(36, true);
145
+ const cursor = new Cursor(dv, u8, 40);
146
+ for (let d = 0; d < deviceCount; d++) {
147
+ const device = {
148
+ slavePosition: cursor.u16(),
149
+ vendorId: cursor.u32(),
150
+ productCode: cursor.u32(),
151
+ revisionNumber: cursor.u32(),
152
+ serialNumber: cursor.u32(),
153
+ name: cursor.str(),
154
+ entries: [],
155
+ };
156
+ const entryCount = cursor.u32();
157
+ for (let e = 0; e < entryCount; e++) {
158
+ device.entries.push({
159
+ index: cursor.u16(),
160
+ subindex: cursor.u8val(),
161
+ isOutput: cursor.u8val() === 1,
162
+ dataType: cursor.u16(),
163
+ bitLength: cursor.u16(),
164
+ bitOffset: cursor.u32(),
165
+ name: cursor.str(),
166
+ });
167
+ }
168
+ header.devices.push(device);
169
+ }
170
+ const rowsStart = cursor.offset;
171
+ const rowStride = 16 + header.inputBytes + header.outputBytes;
172
+ const available = Math.floor((u8.byteLength - rowsStart) / rowStride);
173
+ if (available < header.rowCount) {
174
+ header.rowCount = Math.max(0, available);
175
+ }
176
+ return new MmpdFile(header, u8, dv, rowsStart);
177
+ }
178
+ // A byte-aligned reader for the fixed-width numeric types, or null for variable/sub-byte/string
179
+ // types that need the bit-extraction path.
180
+ function numericReader(dataType) {
181
+ switch (dataType) {
182
+ case 1 /* DataType.BOOLEAN */:
183
+ case 5 /* DataType.UNSIGNED8 */:
184
+ case 30 /* DataType.BYTE */:
185
+ case 45 /* DataType.BITARR8 */:
186
+ return (dv, o) => dv.getUint8(o);
187
+ case 2 /* DataType.INTEGER8 */:
188
+ return (dv, o) => dv.getInt8(o);
189
+ case 3 /* DataType.INTEGER16 */:
190
+ return (dv, o) => dv.getInt16(o, true);
191
+ case 6 /* DataType.UNSIGNED16 */:
192
+ case 31 /* DataType.WORD */:
193
+ case 46 /* DataType.BITARR16 */:
194
+ return (dv, o) => dv.getUint16(o, true);
195
+ case 4 /* DataType.INTEGER32 */:
196
+ return (dv, o) => dv.getInt32(o, true);
197
+ case 7 /* DataType.UNSIGNED32 */:
198
+ case 32 /* DataType.DWORD */:
199
+ case 47 /* DataType.BITARR32 */:
200
+ return (dv, o) => dv.getUint32(o, true);
201
+ case 8 /* DataType.REAL32 */:
202
+ return (dv, o) => dv.getFloat32(o, true);
203
+ case 17 /* DataType.REAL64 */:
204
+ return (dv, o) => dv.getFloat64(o, true);
205
+ case 21 /* DataType.INTEGER64 */:
206
+ return (dv, o) => dv.getBigInt64(o, true);
207
+ case 27 /* DataType.UNSIGNED64 */:
208
+ return (dv, o) => dv.getBigUint64(o, true);
209
+ default:
210
+ return null;
211
+ }
212
+ }
213
+ /// Fetches the live recorder dump (`GET /api/process-data/dump`) and parses it. `baseUrl` is the
214
+ /// HTTP API origin (e.g. `https://host:61447`). Throws on a non-OK response (the response body's
215
+ /// `error` message when present). Pass `fetch` to inject an implementation (Node, request logging).
216
+ export async function fetchProcessDataDump(baseUrl, options = {}) {
217
+ const doFetch = options.fetch ?? fetch;
218
+ const res = await doFetch(`${baseUrl.replace(/\/+$/, '')}/api/process-data/dump`, {
219
+ headers: { Accept: 'application/octet-stream' },
220
+ signal: options.signal,
221
+ });
222
+ if (!res.ok) {
223
+ let message = `HTTP ${res.status}`;
224
+ try {
225
+ const body = (await res.json());
226
+ if (body?.error) {
227
+ message = body.error;
228
+ }
229
+ }
230
+ catch {
231
+ // Non-JSON error body — keep the status message.
232
+ }
233
+ throw new Error(message);
234
+ }
235
+ return parseMmpd(await res.arrayBuffer());
236
+ }
237
+ /// Extracts `bitLength` bits starting at `bitOffset` from `src`, returning them LSB-aligned and
238
+ /// little-endian (padded to a byte boundary). Mirrors `extractBits` in libs/node/process_image.cc.
239
+ export function extractBits(src, bitOffset, bitLength) {
240
+ const out = new Uint8Array((bitLength + 7) >> 3);
241
+ if (bitOffset % 8 === 0 && bitLength % 8 === 0) {
242
+ const byteOffset = bitOffset >> 3;
243
+ for (let i = 0; i < out.length && byteOffset + i < src.length; i++) {
244
+ out[i] = src[byteOffset + i];
245
+ }
246
+ return out;
247
+ }
248
+ for (let i = 0; i < bitLength; i++) {
249
+ const srcBit = bitOffset + i;
250
+ const srcByte = srcBit >> 3;
251
+ if (srcByte >= src.length) {
252
+ break;
253
+ }
254
+ const bit = (src[srcByte] >> (srcBit & 7)) & 1;
255
+ out[i >> 3] |= bit << (i & 7);
256
+ }
257
+ return out;
258
+ }
259
+ /// Decodes LSB-aligned little-endian `bytes` (as produced by `extractBits`) for the given ETG.1020
260
+ /// `dataType`. Returns a number for fixed-width numerics, a bigint for 64-bit integers, a string
261
+ /// for VISIBLE/UNICODE strings (trailing NULs/spaces stripped), or null when the type is unknown
262
+ /// (dataType 0) or there are too few bytes. Unmapped codes fall back to an unsigned read of the
263
+ /// available bytes (covers BITn fields).
264
+ export function decodeMmpdValue(dataType, bytes) {
265
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
266
+ const n = bytes.byteLength;
267
+ switch (dataType) {
268
+ case 1 /* DataType.BOOLEAN */:
269
+ case 5 /* DataType.UNSIGNED8 */:
270
+ case 30 /* DataType.BYTE */:
271
+ case 45 /* DataType.BITARR8 */:
272
+ return n >= 1 ? dv.getUint8(0) : null;
273
+ case 2 /* DataType.INTEGER8 */:
274
+ return n >= 1 ? dv.getInt8(0) : null;
275
+ case 3 /* DataType.INTEGER16 */:
276
+ return n >= 2 ? dv.getInt16(0, true) : null;
277
+ case 6 /* DataType.UNSIGNED16 */:
278
+ case 31 /* DataType.WORD */:
279
+ case 46 /* DataType.BITARR16 */:
280
+ return n >= 2 ? dv.getUint16(0, true) : null;
281
+ case 4 /* DataType.INTEGER32 */:
282
+ return n >= 4 ? dv.getInt32(0, true) : null;
283
+ case 7 /* DataType.UNSIGNED32 */:
284
+ case 32 /* DataType.DWORD */:
285
+ case 47 /* DataType.BITARR32 */:
286
+ return n >= 4 ? dv.getUint32(0, true) : null;
287
+ case 8 /* DataType.REAL32 */:
288
+ return n >= 4 ? dv.getFloat32(0, true) : null;
289
+ case 17 /* DataType.REAL64 */:
290
+ return n >= 8 ? dv.getFloat64(0, true) : null;
291
+ case 21 /* DataType.INTEGER64 */:
292
+ return n >= 8 ? dv.getBigInt64(0, true) : null;
293
+ case 27 /* DataType.UNSIGNED64 */:
294
+ return n >= 8 ? dv.getBigUint64(0, true) : null;
295
+ case 9 /* DataType.VISIBLE_STRING */:
296
+ case 11 /* DataType.UNICODE_STRING */: {
297
+ let end = n;
298
+ while (end > 0 && (bytes[end - 1] === 0x00 || bytes[end - 1] === 0x20)) {
299
+ end--;
300
+ }
301
+ return utf8.decode(bytes.subarray(0, end));
302
+ }
303
+ case 0x0000:
304
+ return null; // OD not enumerated — undecodable
305
+ default: {
306
+ // Unknown/sub-byte (e.g. BIT1..BIT16): read the available bytes as a little-endian unsigned.
307
+ if (n === 0 || n > 4) {
308
+ return null;
309
+ }
310
+ let v = 0;
311
+ for (let i = 0; i < n; i++) {
312
+ v |= bytes[i] << (8 * i);
313
+ }
314
+ return v >>> 0;
315
+ }
316
+ }
317
+ }
318
+ /// Whether `decodeSeries` yields plottable numbers for this ETG.1020 data-type code — a
319
+ /// fixed-width integer/real or a bit field, but not a string, struct, or unenumerated type
320
+ /// (dataType 0). Useful for offering only chartable objects in a UI.
321
+ export function isPlottableDataType(dataType) {
322
+ return numericReader(dataType) !== null || (dataType >= 0x0030 && dataType <= 0x003f);
323
+ }
package/dist/sdo.d.ts ADDED
@@ -0,0 +1,15 @@
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 encodeSdoValue(type: SdoType, raw: string): {
5
+ bytes: number[];
6
+ } | {
7
+ error: string;
8
+ };
9
+ export declare function sdoTypeForDataTypeName(dataTypeName: string): SdoType;
10
+ export interface SdoInterpretation {
11
+ label: string;
12
+ value: string;
13
+ }
14
+ export declare function interpretSdoBytes(bytes: number[]): SdoInterpretation[];
15
+ export declare function decodeSdoBytes(dataTypeName: string, bytes: number[]): number | string | number[];