@synapticon/motion-master-client 2.3.0-beta.0 → 6.0.0-alpha.23
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/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/generated/Api.d.ts +991 -0
- package/dist/generated/Api.js +614 -0
- package/dist/generated/data-contracts.d.ts +644 -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 +10 -7
- package/dist/index.js +16 -22
- package/dist/mmpd.d.ts +54 -0
- package/dist/mmpd.js +323 -0
- package/dist/sdo.d.ts +15 -0
- package/dist/sdo.js +212 -0
- package/dist/somanet.d.ts +5 -0
- package/dist/somanet.js +15 -0
- package/dist/web-socket-connection.d.ts +69 -0
- package/dist/web-socket-connection.js +206 -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
|
@@ -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
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
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export * from './
|
|
7
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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[];
|