@robotical/raftjs 2.1.0 → 2.1.3
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/devdocs/devbin-backwards-compatibility.md +105 -0
- package/devdocs/pseudocode-to-js-transpiler.md +563 -0
- package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
- package/dist/react-native/PseudocodeTranspiler.js +115 -0
- package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
- package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
- package/dist/react-native/RaftAttributeHandler.js +108 -32
- package/dist/react-native/RaftAttributeHandler.js.map +1 -1
- package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
- package/dist/react-native/RaftChannelBLE.web.js +59 -21
- package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
- package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
- package/dist/react-native/RaftChannelSimulated.js +9 -5
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSocket.js +16 -1
- package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
- package/dist/react-native/RaftConnector.d.ts +29 -1
- package/dist/react-native/RaftConnector.js +177 -11
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/react-native/RaftCustomAttrHandler.js +32 -44
- package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
- package/dist/react-native/RaftDeviceInfo.js +8 -0
- package/dist/react-native/RaftDeviceInfo.js.map +1 -1
- package/dist/react-native/RaftDeviceManager.d.ts +30 -3
- package/dist/react-native/RaftDeviceManager.js +618 -107
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/react-native/RaftDeviceStates.d.ts +27 -3
- package/dist/react-native/RaftDeviceStates.js +31 -6
- package/dist/react-native/RaftDeviceStates.js.map +1 -1
- package/dist/react-native/RaftFileHandler.d.ts +1 -1
- package/dist/react-native/RaftFileHandler.js +101 -34
- package/dist/react-native/RaftFileHandler.js.map +1 -1
- package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/react-native/RaftMsgHandler.d.ts +1 -1
- package/dist/react-native/RaftMsgHandler.js +6 -3
- package/dist/react-native/RaftMsgHandler.js.map +1 -1
- package/dist/react-native/RaftPublish.d.ts +2 -0
- package/dist/react-native/RaftPublish.js +81 -0
- package/dist/react-native/RaftPublish.js.map +1 -0
- package/dist/react-native/RaftStreamHandler.d.ts +11 -0
- package/dist/react-native/RaftStreamHandler.js +66 -0
- package/dist/react-native/RaftStreamHandler.js.map +1 -1
- package/dist/react-native/RaftStruct.d.ts +2 -2
- package/dist/react-native/RaftStruct.js +97 -26
- package/dist/react-native/RaftStruct.js.map +1 -1
- package/dist/react-native/RaftSystemType.d.ts +1 -0
- package/dist/react-native/RaftSystemUtils.d.ts +17 -1
- package/dist/react-native/RaftSystemUtils.js +51 -0
- package/dist/react-native/RaftSystemUtils.js.map +1 -1
- package/dist/react-native/RaftTimezone.d.ts +16 -0
- package/dist/react-native/RaftTimezone.js +153 -0
- package/dist/react-native/RaftTimezone.js.map +1 -0
- package/dist/react-native/RaftTypes.d.ts +46 -1
- package/dist/react-native/RaftTypes.js.map +1 -1
- package/dist/react-native/RaftUpdateManager.js +1 -1
- package/dist/react-native/RaftUpdateManager.js.map +1 -1
- package/dist/react-native/main.d.ts +3 -0
- package/dist/react-native/main.js +8 -1
- package/dist/react-native/main.js.map +1 -1
- package/dist/web/PseudocodeTranspiler.d.ts +6 -0
- package/dist/web/PseudocodeTranspiler.js +115 -0
- package/dist/web/PseudocodeTranspiler.js.map +1 -0
- package/dist/web/RaftAttributeHandler.d.ts +1 -1
- package/dist/web/RaftAttributeHandler.js +108 -32
- package/dist/web/RaftAttributeHandler.js.map +1 -1
- package/dist/web/RaftChannelBLE.web.d.ts +4 -0
- package/dist/web/RaftChannelBLE.web.js +59 -21
- package/dist/web/RaftChannelBLE.web.js.map +1 -1
- package/dist/web/RaftChannelSimulated.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.js +9 -5
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSocket.js +16 -1
- package/dist/web/RaftChannelWebSocket.js.map +1 -1
- package/dist/web/RaftConnector.d.ts +29 -1
- package/dist/web/RaftConnector.js +177 -11
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/web/RaftCustomAttrHandler.js +32 -44
- package/dist/web/RaftCustomAttrHandler.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +18 -0
- package/dist/web/RaftDeviceInfo.js +8 -0
- package/dist/web/RaftDeviceInfo.js.map +1 -1
- package/dist/web/RaftDeviceManager.d.ts +30 -3
- package/dist/web/RaftDeviceManager.js +618 -107
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/web/RaftDeviceStates.d.ts +27 -3
- package/dist/web/RaftDeviceStates.js +31 -6
- package/dist/web/RaftDeviceStates.js.map +1 -1
- package/dist/web/RaftFileHandler.d.ts +1 -1
- package/dist/web/RaftFileHandler.js +101 -34
- package/dist/web/RaftFileHandler.js.map +1 -1
- package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/web/RaftMsgHandler.d.ts +1 -1
- package/dist/web/RaftMsgHandler.js +6 -3
- package/dist/web/RaftMsgHandler.js.map +1 -1
- package/dist/web/RaftPublish.d.ts +2 -0
- package/dist/web/RaftPublish.js +81 -0
- package/dist/web/RaftPublish.js.map +1 -0
- package/dist/web/RaftStreamHandler.d.ts +11 -0
- package/dist/web/RaftStreamHandler.js +66 -0
- package/dist/web/RaftStreamHandler.js.map +1 -1
- package/dist/web/RaftStruct.d.ts +2 -2
- package/dist/web/RaftStruct.js +97 -26
- package/dist/web/RaftStruct.js.map +1 -1
- package/dist/web/RaftSystemType.d.ts +1 -0
- package/dist/web/RaftSystemUtils.d.ts +17 -1
- package/dist/web/RaftSystemUtils.js +51 -0
- package/dist/web/RaftSystemUtils.js.map +1 -1
- package/dist/web/RaftTimezone.d.ts +16 -0
- package/dist/web/RaftTimezone.js +153 -0
- package/dist/web/RaftTimezone.js.map +1 -0
- package/dist/web/RaftTypes.d.ts +46 -1
- package/dist/web/RaftTypes.js.map +1 -1
- package/dist/web/RaftUpdateManager.js +1 -1
- package/dist/web/RaftUpdateManager.js.map +1 -1
- package/dist/web/main.d.ts +3 -0
- package/dist/web/main.js +8 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +177 -17
- package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
- package/examples/dashboard/src/DevicePanel.tsx +92 -11
- package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
- package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
- package/examples/dashboard/src/DevicesPanel.tsx +11 -0
- package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
- package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
- package/examples/dashboard/src/LoggingPanel.tsx +264 -0
- package/examples/dashboard/src/Main.tsx +12 -2
- package/examples/dashboard/src/SettingsScreen.tsx +9 -4
- package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
- package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
- package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
- package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
- package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
- package/examples/dashboard/src/styles.css +766 -1
- package/notes/web-ble-reconnect-retry.md +69 -0
- package/package.json +10 -7
- package/src/PseudocodeTranspiler.test.ts +372 -0
- package/src/PseudocodeTranspiler.ts +127 -0
- package/src/RaftAttributeHandler.ts +152 -76
- package/src/RaftChannelBLE.web.ts +62 -20
- package/src/RaftChannelSimulated.ts +10 -5
- package/src/RaftChannelWebSocket.ts +16 -2
- package/src/RaftConnector.ts +204 -17
- package/src/RaftCustomAttrHandler.ts +35 -45
- package/src/RaftDeviceInfo.ts +27 -0
- package/src/RaftDeviceManager.test.ts +164 -0
- package/src/RaftDeviceManager.ts +705 -127
- package/src/RaftDeviceMgrIF.ts +13 -2
- package/src/RaftDeviceStates.ts +49 -8
- package/src/RaftFileHandler.ts +112 -39
- package/src/RaftMicroPythonConsoleClient.ts +78 -0
- package/src/RaftMsgHandler.ts +8 -4
- package/src/RaftPublish.ts +92 -0
- package/src/RaftStreamHandler.ts +84 -1
- package/src/RaftStruct.test.ts +229 -0
- package/src/RaftStruct.ts +101 -37
- package/src/RaftSystemType.ts +1 -0
- package/src/RaftSystemUtils.ts +59 -0
- package/src/RaftTimezone.ts +151 -0
- package/src/RaftTypes.ts +57 -1
- package/src/RaftUpdateManager.ts +1 -1
- package/src/main.ts +3 -0
package/src/RaftDeviceMgrIF.ts
CHANGED
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
//
|
|
8
8
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
9
9
|
|
|
10
|
-
import { DeviceTypeAction } from "./RaftDeviceInfo";
|
|
11
|
-
import { DeviceAttributeState, DevicesState, DeviceState } from "./RaftDeviceStates";
|
|
10
|
+
import { DeviceTypeAction, SampleRateResult } from "./RaftDeviceInfo";
|
|
11
|
+
import { DeviceAttributeState, DevicesState, DeviceState, DeviceStats } from "./RaftDeviceStates";
|
|
12
12
|
|
|
13
13
|
export default interface RaftDeviceMgrIF {
|
|
14
14
|
|
|
15
15
|
// Get state of devices
|
|
16
16
|
getDevicesState(): DevicesState;
|
|
17
17
|
getDeviceState(deviceKey: string): DeviceState;
|
|
18
|
+
getDeviceStats(deviceKey: string): DeviceStats;
|
|
19
|
+
resetDeviceStats(deviceKey: string): void;
|
|
18
20
|
|
|
19
21
|
// Settings
|
|
20
22
|
setMaxDataPointsToStore(maxDataPointsToStore: number): void;
|
|
@@ -26,8 +28,17 @@ export default interface RaftDeviceMgrIF {
|
|
|
26
28
|
removeNewAttributeCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void;
|
|
27
29
|
addAttributeDataCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void;
|
|
28
30
|
removeAttributeDataCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void;
|
|
31
|
+
addDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void;
|
|
32
|
+
removeDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void;
|
|
29
33
|
|
|
30
34
|
// Send action to device
|
|
31
35
|
sendAction(deviceKey: string, action: DeviceTypeAction, data: number[]): void;
|
|
32
36
|
sendCompoundAction(deviceKey: string, action: DeviceTypeAction, data: number[][]): void;
|
|
37
|
+
|
|
38
|
+
// Set sample rate with coordinated polling parameters
|
|
39
|
+
setSampleRate(deviceKey: string, sampleRateHz: number, options?: {
|
|
40
|
+
numSamples?: number;
|
|
41
|
+
intervalUs?: number;
|
|
42
|
+
maxNumSamples?: number;
|
|
43
|
+
}): Promise<SampleRateResult>;
|
|
33
44
|
}
|
package/src/RaftDeviceStates.ts
CHANGED
|
@@ -14,10 +14,14 @@ export function deviceAttrGetLatestFormatted(attrState: DeviceAttributeState): s
|
|
|
14
14
|
if (attrState.values.length === 0) {
|
|
15
15
|
return 'N/A';
|
|
16
16
|
}
|
|
17
|
+
const value = attrState.values[attrState.values.length - 1];
|
|
18
|
+
// String values are returned directly
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
17
22
|
if (attrState.format.length === 0) {
|
|
18
|
-
return
|
|
23
|
+
return value.toString();
|
|
19
24
|
}
|
|
20
|
-
const value = attrState.values[attrState.values.length - 1];
|
|
21
25
|
let format = attrState.format;
|
|
22
26
|
if (format.startsWith("%")) {
|
|
23
27
|
format = format.slice(1);
|
|
@@ -52,7 +56,7 @@ export interface DeviceAttributeState {
|
|
|
52
56
|
newAttribute: boolean;
|
|
53
57
|
newData: boolean;
|
|
54
58
|
numNewValues: number;
|
|
55
|
-
values: number[];
|
|
59
|
+
values: (number | string)[];
|
|
56
60
|
units: string;
|
|
57
61
|
range: number[];
|
|
58
62
|
format: string;
|
|
@@ -68,15 +72,37 @@ export interface DeviceTimeline {
|
|
|
68
72
|
timestampsUs: number[];
|
|
69
73
|
lastReportTimestampUs: number;
|
|
70
74
|
reportTimestampOffsetUs: number;
|
|
75
|
+
totalSamplesAdded: number;
|
|
76
|
+
// Piecewise EMA timestamp reconstruction state
|
|
77
|
+
emaLastSampleTimeUs: number;
|
|
78
|
+
emaIntervalUs: number;
|
|
79
|
+
emaPrevPollTimeUs: number;
|
|
80
|
+
emaCalibrated: boolean;
|
|
81
|
+
emaCalibrationPolls: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DeviceStats {
|
|
85
|
+
totalSamples: number;
|
|
86
|
+
windowMs: number;
|
|
87
|
+
windowSamples: number;
|
|
88
|
+
sampleRateHz: number;
|
|
89
|
+
lastSampleTimeMs: number | null;
|
|
90
|
+
lastUpdateTimeMs: number | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export enum DeviceOnlineState {
|
|
94
|
+
Offline = 0,
|
|
95
|
+
Online = 1,
|
|
96
|
+
PendingDeletion = 2,
|
|
71
97
|
}
|
|
72
|
-
|
|
98
|
+
|
|
73
99
|
export interface DeviceState {
|
|
74
100
|
deviceTypeInfo: DeviceTypeInfo | undefined;
|
|
75
101
|
deviceTimeline: DeviceTimeline;
|
|
76
102
|
deviceAttributes: DeviceAttributesState;
|
|
77
103
|
deviceIsNew: boolean;
|
|
78
104
|
stateChanged: boolean;
|
|
79
|
-
|
|
105
|
+
onlineState: DeviceOnlineState;
|
|
80
106
|
deviceAddress: string;
|
|
81
107
|
deviceType: string;
|
|
82
108
|
busName: string;
|
|
@@ -86,7 +112,22 @@ export class DevicesState {
|
|
|
86
112
|
[deviceKey: string]: DeviceState;
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
// Format a numeric device address as canonical hex string:
|
|
116
|
+
// lowercase, no "0x" prefix, no leading zeros
|
|
117
|
+
export function formatDeviceAddrHex(addr: number): string {
|
|
118
|
+
return addr.toString(16);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Generate a composite device key from bus number and hex address
|
|
122
|
+
export function getDeviceKey(busNumberAsString: string, devAddrHexStr: string): string {
|
|
123
|
+
return `${busNumberAsString}_${devAddrHexStr}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse a device key into its bus and address components
|
|
127
|
+
export function parseDeviceKey(deviceKey: string): { bus: string; addr: string } {
|
|
128
|
+
const sep = deviceKey.indexOf('_');
|
|
129
|
+
if (sep < 0) {
|
|
130
|
+
return { bus: deviceKey, addr: '' };
|
|
131
|
+
}
|
|
132
|
+
return { bus: deviceKey.substring(0, sep), addr: deviceKey.substring(sep + 1) };
|
|
92
133
|
}
|
package/src/RaftFileHandler.ts
CHANGED
|
@@ -13,6 +13,7 @@ import RaftMsgHandler from './RaftMsgHandler';
|
|
|
13
13
|
import {
|
|
14
14
|
RaftFileDownloadResult,
|
|
15
15
|
RaftFileDownloadStartResp,
|
|
16
|
+
RaftFileDownloadEndResp,
|
|
16
17
|
RaftFileSendType,
|
|
17
18
|
RaftFileStartResp,
|
|
18
19
|
RaftOKFail,
|
|
@@ -64,6 +65,7 @@ export default class RaftFileHandler {
|
|
|
64
65
|
private _ackedFilePos = 0;
|
|
65
66
|
private _batchAckReceived = false;
|
|
66
67
|
private _isTxCancelled = false;
|
|
68
|
+
private _fileTxStreamID = 0;
|
|
67
69
|
|
|
68
70
|
// File receive info
|
|
69
71
|
private _isRxCancelled = false;
|
|
@@ -77,7 +79,6 @@ export default class RaftFileHandler {
|
|
|
77
79
|
private _fileRxLastAckTime = 0;
|
|
78
80
|
private _fileRxLastBlockTime = 0;
|
|
79
81
|
private _fileRxLastAckPos = 0;
|
|
80
|
-
private OVERALL_FILE_TRANSFER_TIMEOUT_MS = 100000;
|
|
81
82
|
private FILE_RX_ACK_RESEND_TIMEOUT_MS = 1000;
|
|
82
83
|
|
|
83
84
|
// RaftCommsStats
|
|
@@ -110,6 +111,7 @@ export default class RaftFileHandler {
|
|
|
110
111
|
progressCallback: ((sent: number, total: number, progress: number) => void) | undefined,
|
|
111
112
|
): Promise<boolean> {
|
|
112
113
|
this._isTxCancelled = false;
|
|
114
|
+
this._fileTxStreamID = 0;
|
|
113
115
|
|
|
114
116
|
// Send file start message
|
|
115
117
|
if (!await this._sendFileStartMsg(fileName, fileType, fileDest, fileContents))
|
|
@@ -165,8 +167,8 @@ export default class RaftFileHandler {
|
|
|
165
167
|
RaftLog.warn(`sendFileStartMsg error ${err}`);
|
|
166
168
|
return false;
|
|
167
169
|
}
|
|
168
|
-
if (fileStartResp.rslt !== 'ok') {
|
|
169
|
-
RaftLog.warn(`sendFileStartMsg error ${fileStartResp
|
|
170
|
+
if (!fileStartResp || fileStartResp.rslt !== 'ok') {
|
|
171
|
+
RaftLog.warn(`sendFileStartMsg error ${fileStartResp?.rslt ?? 'no response'}`);
|
|
170
172
|
return false;
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -181,12 +183,17 @@ export default class RaftFileHandler {
|
|
|
181
183
|
} else {
|
|
182
184
|
this._batchAckSize = this._requestedBatchAckSize;
|
|
183
185
|
}
|
|
186
|
+
const streamID = fileStartResp.streamID;
|
|
187
|
+
this._fileTxStreamID = streamID !== undefined && streamID > 0 && streamID <= 0xff ? streamID : 0;
|
|
184
188
|
RaftLog.debug(
|
|
185
189
|
`_fileSendStartMsg fileBlockSize req ${this._requestedFileBlockSize} resp ${fileStartResp.batchMsgSize} actual ${this._fileBlockSize}`,
|
|
186
190
|
);
|
|
187
191
|
RaftLog.debug(
|
|
188
192
|
`_fileSendStartMsg batchAckSize req ${this._requestedBatchAckSize} resp ${fileStartResp.batchAckSize} actual ${this._batchAckSize}`,
|
|
189
193
|
);
|
|
194
|
+
RaftLog.debug(
|
|
195
|
+
`_fileSendStartMsg streamID ${this._fileTxStreamID}`,
|
|
196
|
+
);
|
|
190
197
|
return true;
|
|
191
198
|
}
|
|
192
199
|
|
|
@@ -202,7 +209,8 @@ export default class RaftFileHandler {
|
|
|
202
209
|
? 'espfwupdate'
|
|
203
210
|
: 'fileupload';
|
|
204
211
|
const fileLen = fileContents.length;
|
|
205
|
-
const
|
|
212
|
+
const streamIDJson = this._fileTxStreamID !== 0 ? `,"streamID":${this._fileTxStreamID}` : '';
|
|
213
|
+
const cmdMsg = `{"cmdName":"ufEnd","reqStr":"${reqStr}","fileType":"${fileDest}","fileName":"${fileName}","fileLen":${fileLen}${streamIDJson}}`;
|
|
206
214
|
|
|
207
215
|
// Await outstanding promises
|
|
208
216
|
try {
|
|
@@ -223,12 +231,13 @@ export default class RaftFileHandler {
|
|
|
223
231
|
RaftLog.warn(`sendFileEndMsg error ${err}`);
|
|
224
232
|
return false;
|
|
225
233
|
}
|
|
226
|
-
return fileEndResp
|
|
234
|
+
return fileEndResp?.rslt === 'ok';
|
|
227
235
|
}
|
|
228
236
|
|
|
229
237
|
async _sendFileCancelMsg(): Promise<void> {
|
|
230
238
|
// File cancel command message
|
|
231
|
-
const
|
|
239
|
+
const streamIDJson = this._fileTxStreamID !== 0 ? `,"streamID":${this._fileTxStreamID}` : '';
|
|
240
|
+
const cmdMsg = `{"cmdName":"ufCancel"${streamIDJson}}`;
|
|
232
241
|
|
|
233
242
|
// Await outstanding promises
|
|
234
243
|
await this.awaitOutstandingMsgPromises(true);
|
|
@@ -367,7 +376,7 @@ export default class RaftFileHandler {
|
|
|
367
376
|
await this.awaitOutstandingMsgPromises(false);
|
|
368
377
|
|
|
369
378
|
// Send
|
|
370
|
-
const promRslt = this._msgHandler.sendFileBlock(fileContents.subarray(blockStart, blockEnd), blockStart);
|
|
379
|
+
const promRslt = this._msgHandler.sendFileBlock(fileContents.subarray(blockStart, blockEnd), blockStart, this._fileTxStreamID);
|
|
371
380
|
if (!promRslt) {
|
|
372
381
|
return false;
|
|
373
382
|
}
|
|
@@ -437,8 +446,8 @@ export default class RaftFileHandler {
|
|
|
437
446
|
// Establish a bridge
|
|
438
447
|
const bridgedDeviceSerialPort = "Serial" + fileSource.slice(bridgeSerialPrefix.length);
|
|
439
448
|
const cmdResp = await this._msgHandler.createCommsBridge(bridgedDeviceSerialPort, "fileSource");
|
|
440
|
-
if (cmdResp.rslt != "ok") {
|
|
441
|
-
RaftLog.warn(`fileReceive - failed to setup bridge ${cmdResp
|
|
449
|
+
if (!cmdResp || cmdResp.rslt != "ok") {
|
|
450
|
+
RaftLog.warn(`fileReceive - failed to setup bridge ${cmdResp?.rslt ?? 'no response'}`);
|
|
442
451
|
return new RaftFileDownloadResult();
|
|
443
452
|
}
|
|
444
453
|
bridgeID = cmdResp.bridgeID;
|
|
@@ -454,8 +463,9 @@ export default class RaftFileHandler {
|
|
|
454
463
|
// Send contents
|
|
455
464
|
const fileContents = await this._receiveFileContents(progressCallback, bridgeID);
|
|
456
465
|
|
|
457
|
-
// Send file end
|
|
458
|
-
await this._receiveFileEnd(fileName, bridgeID)
|
|
466
|
+
// Send file end (and verify deferred CRC if applicable)
|
|
467
|
+
if (!await this._receiveFileEnd(fileName, bridgeID))
|
|
468
|
+
return new RaftFileDownloadResult();
|
|
459
469
|
|
|
460
470
|
// Clean up
|
|
461
471
|
await this.awaitOutstandingMsgPromises(true);
|
|
@@ -471,14 +481,16 @@ export default class RaftFileHandler {
|
|
|
471
481
|
async _receiveFileStart(fileName: string, bridgeID: number | undefined) : Promise<boolean> {
|
|
472
482
|
|
|
473
483
|
const blockMaxSizeRequested = 5000;
|
|
474
|
-
const batchAckSizeRequested =
|
|
484
|
+
const batchAckSizeRequested = 4;
|
|
475
485
|
const fileSrc = "fs";
|
|
476
486
|
|
|
477
487
|
// Request file transfer
|
|
478
488
|
// Frames follow the approach used in the web interface start, block..., end
|
|
489
|
+
// crcAt:end requests the server to defer CRC computation (non-blocking start)
|
|
479
490
|
const cmdMsg = `{"cmdName":"dfStart","reqStr":"getFile","fileType":"${fileSrc}",` +
|
|
480
491
|
`"batchMsgSize":${blockMaxSizeRequested},` +
|
|
481
492
|
`"batchAckSize":${batchAckSizeRequested},` +
|
|
493
|
+
`"crcAt":"end",` +
|
|
482
494
|
`"fileName":"${fileName}"}`
|
|
483
495
|
|
|
484
496
|
// Send
|
|
@@ -494,18 +506,23 @@ export default class RaftFileHandler {
|
|
|
494
506
|
return false;
|
|
495
507
|
}
|
|
496
508
|
RaftLog.info(`_receiveFileStartMsg rslt ${JSON.stringify(cmdResp)}`);
|
|
497
|
-
if (cmdResp
|
|
509
|
+
if (cmdResp?.rslt === 'ok') {
|
|
498
510
|
this._fileRxBatchMsgSize = cmdResp.batchMsgSize;
|
|
499
511
|
this._fileRxBatchAckSize = cmdResp.batchAckSize;
|
|
500
512
|
this._fileRxStreamID = cmdResp.streamID;
|
|
501
513
|
this._fileRxFileLen = cmdResp.fileLen;
|
|
502
|
-
|
|
514
|
+
// CRC may be provided at start (old firmware) or deferred to dfEnd (new firmware)
|
|
515
|
+
this._fileRxCrc16 = cmdResp.crc16 ? parseInt(cmdResp.crc16, 16) : -1;
|
|
503
516
|
this._fileRxBuffer = new Uint8Array(0);
|
|
504
517
|
this._fileRxLastAckTime = 0;
|
|
505
518
|
this._fileRxLastAckPos = 0;
|
|
506
519
|
this._fileRxLastBlockTime = Date.now();
|
|
507
520
|
this._fileRxActive = true;
|
|
508
521
|
}
|
|
522
|
+
if (!cmdResp) {
|
|
523
|
+
RaftLog.warn(`_receiveFileStartMsg failed no response`);
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
509
526
|
return cmdResp.rslt === 'ok';
|
|
510
527
|
}
|
|
511
528
|
|
|
@@ -514,6 +531,18 @@ export default class RaftFileHandler {
|
|
|
514
531
|
bridgeID: number | undefined
|
|
515
532
|
): Promise<RaftFileDownloadResult> {
|
|
516
533
|
|
|
534
|
+
// Calculate dynamic timeouts based on file size and connection speed
|
|
535
|
+
// Use batch message size as proxy for connection type:
|
|
536
|
+
// BLE ~500 byte blocks ~5 KB/s, WebSocket ~5000 byte blocks ~50 KB/s
|
|
537
|
+
const estimatedBytesPerSec = this._fileRxBatchMsgSize <= 500 ? 5000 : 50000;
|
|
538
|
+
const BASE_TIMEOUT_MS = 30000;
|
|
539
|
+
const SAFETY_FACTOR = 2;
|
|
540
|
+
const transferTimeMs = (this._fileRxFileLen / estimatedBytesPerSec) * 1000 * SAFETY_FACTOR;
|
|
541
|
+
const overallTimeoutMs = BASE_TIMEOUT_MS + transferTimeMs;
|
|
542
|
+
const blockTimeoutMs = this.BLOCK_ACK_TIMEOUT_MS;
|
|
543
|
+
RaftLog.info(`_receiveFileContents - fileLen ${this._fileRxFileLen} batchMsgSize ${this._fileRxBatchMsgSize} ` +
|
|
544
|
+
`estimatedBytesPerSec ${estimatedBytesPerSec} overallTimeoutMs ${overallTimeoutMs} blockTimeoutMs ${blockTimeoutMs}`);
|
|
545
|
+
|
|
517
546
|
// Wait for file to be received
|
|
518
547
|
return new Promise<RaftFileDownloadResult>((resolve, reject) => {
|
|
519
548
|
const startTime = Date.now();
|
|
@@ -527,14 +556,16 @@ export default class RaftFileHandler {
|
|
|
527
556
|
progressCallback(this._fileRxBuffer.length, this._fileRxFileLen);
|
|
528
557
|
}
|
|
529
558
|
|
|
530
|
-
// Check CRC
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
559
|
+
// Check CRC if it was provided at start (old firmware)
|
|
560
|
+
if (this._fileRxCrc16 >= 0) {
|
|
561
|
+
const crc16 = RaftMiniHDLC.crc16(this._fileRxBuffer);
|
|
562
|
+
if (crc16 !== this._fileRxCrc16) {
|
|
563
|
+
RaftLog.warn(`_receiveFileContents - CRC error ${crc16} ${this._fileRxCrc16}`);
|
|
564
|
+
reject(new Error('fileReceive CRC error'));
|
|
565
|
+
return;
|
|
566
|
+
} else {
|
|
567
|
+
RaftLog.info(`_receiveFileContents - CRC OK ${crc16} ${this._fileRxCrc16}`);
|
|
568
|
+
}
|
|
538
569
|
}
|
|
539
570
|
resolve(new RaftFileDownloadResult(this._fileRxBuffer));
|
|
540
571
|
return;
|
|
@@ -556,10 +587,13 @@ export default class RaftFileHandler {
|
|
|
556
587
|
const now = Date.now();
|
|
557
588
|
|
|
558
589
|
// Check for overall timeouts
|
|
559
|
-
if ((now - startTime >
|
|
560
|
-
(now - this._fileRxLastBlockTime >
|
|
561
|
-
RaftLog.warn(`_receiveFileContents - time-out no new data received`
|
|
590
|
+
if ((now - startTime > overallTimeoutMs) ||
|
|
591
|
+
(now - this._fileRxLastBlockTime > blockTimeoutMs)) {
|
|
592
|
+
RaftLog.warn(`_receiveFileContents - time-out no new data received ` +
|
|
593
|
+
`elapsed ${now - startTime}ms overallTimeout ${overallTimeoutMs}ms ` +
|
|
594
|
+
`blockGap ${now - this._fileRxLastBlockTime}ms blockTimeout ${blockTimeoutMs}ms`);
|
|
562
595
|
this._fileRxActive = false;
|
|
596
|
+
this._sendFileRxCancelMsg(bridgeID);
|
|
563
597
|
reject(new Error('fileReceive failed'));
|
|
564
598
|
return;
|
|
565
599
|
}
|
|
@@ -574,7 +608,7 @@ export default class RaftFileHandler {
|
|
|
574
608
|
if (this._fileRxBuffer.length - this._fileRxLastAckPos >= this._fileRxBatchAckSize * this._fileRxBatchMsgSize) {
|
|
575
609
|
ackRequired = true;
|
|
576
610
|
}
|
|
577
|
-
// RaftLog.info(`_receiveFileContents ${ackRequired ? "ACK_REQUIRED" : "ACK_NOTREQUIRED"} ${this._fileRxBuffer.length} ${this._fileRxLastAckPos} ${this._fileRxBatchAckSize} ${this._fileRxBatchMsgSize}`);
|
|
611
|
+
// RaftLog.info(`_receiveFileContents ${ackRequired ? "ACK_REQUIRED" : "ACK_NOTREQUIRED"} ${this._fileRxBuffer.length} ${this._fileRxLastAckPos} ${this._fileRxBatchAckSize} ${this._fileRxBatchMsgSize}`);
|
|
578
612
|
|
|
579
613
|
// Check if ack required
|
|
580
614
|
if (ackRequired) {
|
|
@@ -582,7 +616,7 @@ export default class RaftFileHandler {
|
|
|
582
616
|
// Ack timing
|
|
583
617
|
this._fileRxLastAckTime = Date.now();
|
|
584
618
|
this._fileRxLastAckPos = this._fileRxBuffer.length;
|
|
585
|
-
|
|
619
|
+
|
|
586
620
|
// Okto message
|
|
587
621
|
const cmdMsg = `{"cmdName":"dfAck","okto":${this._fileRxBuffer.length},` +
|
|
588
622
|
`"streamID":${this._fileRxStreamID},"rslt":"ok"}`;
|
|
@@ -612,18 +646,45 @@ export default class RaftFileHandler {
|
|
|
612
646
|
|
|
613
647
|
async _receiveFileEnd(fileName: string, bridgeID: number | undefined): Promise<boolean> {
|
|
614
648
|
|
|
615
|
-
// Send
|
|
616
|
-
const cmdMsg = `{"cmdName":"
|
|
617
|
-
`"fileName":"${fileName}","
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
649
|
+
// Send dfEnd and wait for response (may contain deferred CRC)
|
|
650
|
+
const cmdMsg = `{"cmdName":"dfEnd","reqStr":"getFile",` +
|
|
651
|
+
`"fileName":"${fileName}","fileLen":${this._fileRxBuffer.length},` +
|
|
652
|
+
`"streamID":${this._fileRxStreamID}}`
|
|
653
|
+
|
|
654
|
+
let cmdResp = null;
|
|
655
|
+
try {
|
|
656
|
+
cmdResp = await this._msgHandler.sendRICREST<RaftFileDownloadEndResp>(
|
|
657
|
+
cmdMsg,
|
|
658
|
+
RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME,
|
|
659
|
+
bridgeID,
|
|
660
|
+
);
|
|
661
|
+
} catch (err) {
|
|
662
|
+
RaftLog.warn(`_receiveFileEnd error ${err}`);
|
|
663
|
+
this._fileRxActive = false;
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Check deferred CRC if start response didn't include one
|
|
668
|
+
if (!cmdResp) {
|
|
669
|
+
RaftLog.warn(`_receiveFileEnd failed no response`);
|
|
670
|
+
this._fileRxActive = false;
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (this._fileRxCrc16 < 0 && cmdResp.crc16) {
|
|
675
|
+
const expectedCrc = parseInt(cmdResp.crc16, 16);
|
|
676
|
+
const actualCrc = RaftMiniHDLC.crc16(this._fileRxBuffer);
|
|
677
|
+
if (actualCrc !== expectedCrc) {
|
|
678
|
+
RaftLog.warn(`_receiveFileEnd - CRC error actual=${actualCrc} expected=${expectedCrc}`);
|
|
679
|
+
this._fileRxActive = false;
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
RaftLog.info(`_receiveFileEnd - CRC OK ${actualCrc}`);
|
|
683
|
+
}
|
|
623
684
|
|
|
624
685
|
// No longer active
|
|
625
686
|
this._fileRxActive = false;
|
|
626
|
-
return
|
|
687
|
+
return true;
|
|
627
688
|
}
|
|
628
689
|
|
|
629
690
|
async _sendFileRxCancelMsg(bridgeID: number | undefined): Promise<void> {
|
|
@@ -642,8 +703,20 @@ export default class RaftFileHandler {
|
|
|
642
703
|
): void {
|
|
643
704
|
// RaftLog.info(`onFileBlock filePos ${filePos} fileBlockData ${RaftUtils.bufferToHex(fileBlockData)}`);
|
|
644
705
|
|
|
706
|
+
if (!this._fileRxActive) {
|
|
707
|
+
RaftLog.verbose(`onFileBlock ignored inactive transfer filePos ${filePos} len ${fileBlockData.length}`);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const streamID = (filePos >>> 24) & 0xff;
|
|
712
|
+
const streamFilePos = filePos & 0x00ffffff;
|
|
713
|
+
if (streamID !== 0 && streamID !== this._fileRxStreamID) {
|
|
714
|
+
RaftLog.verbose(`onFileBlock ignored stale streamID ${streamID} active ${this._fileRxStreamID}`);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
645
718
|
// Check if this is the next block we are expecting
|
|
646
|
-
if (
|
|
719
|
+
if (streamFilePos === this._fileRxBuffer.length) {
|
|
647
720
|
|
|
648
721
|
// Add to buffer
|
|
649
722
|
const tmpArray = new Uint8Array(this._fileRxBuffer.length + fileBlockData.length);
|
|
@@ -658,11 +731,11 @@ export default class RaftFileHandler {
|
|
|
658
731
|
// RaftLog.info(`onFileBlock filePos ${filePos} fileBlockData ${RaftUtils.bufferToHex(fileBlockData)} added to buffer`);
|
|
659
732
|
|
|
660
733
|
} else {
|
|
661
|
-
RaftLog.warn(`onFileBlock expected streamID ${this._fileRxStreamID} filePos ${
|
|
734
|
+
RaftLog.warn(`onFileBlock expected streamID ${this._fileRxStreamID} filePos ${streamFilePos} fileBlockData ${RaftUtils.bufferToHex(fileBlockData)} out of sequence`);
|
|
662
735
|
}
|
|
663
736
|
}
|
|
664
737
|
|
|
665
738
|
isFileRxActive(): boolean {
|
|
666
739
|
return this._fileRxActive;
|
|
667
740
|
}
|
|
668
|
-
}
|
|
741
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import RaftConnector from "./RaftConnector";
|
|
2
|
+
import { RaftRtStreamDataCBType, RaftRtStreamHandle } from "./RaftTypes";
|
|
3
|
+
|
|
4
|
+
const REPL_NEWLINE = "\r";
|
|
5
|
+
|
|
6
|
+
export type RaftMicroPythonConsoleStatus = {
|
|
7
|
+
rslt?: string;
|
|
8
|
+
running?: string;
|
|
9
|
+
replEnabled?: boolean;
|
|
10
|
+
replRunning?: boolean;
|
|
11
|
+
mode?: string;
|
|
12
|
+
streamAttached?: boolean;
|
|
13
|
+
mirrorToSerial?: boolean;
|
|
14
|
+
inputBuffered?: number;
|
|
15
|
+
outputBuffered?: number;
|
|
16
|
+
outputSeq?: number;
|
|
17
|
+
inputDropped?: number;
|
|
18
|
+
outputDropped?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RaftMicroPythonConsoleOutput = {
|
|
22
|
+
rslt?: string;
|
|
23
|
+
seq?: number;
|
|
24
|
+
missed?: number;
|
|
25
|
+
dropped?: number;
|
|
26
|
+
data?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default class RaftMicroPythonConsoleClient {
|
|
30
|
+
constructor(private _connector: RaftConnector) {}
|
|
31
|
+
private readonly _streamDrainMs = 1200;
|
|
32
|
+
|
|
33
|
+
async status(): Promise<RaftMicroPythonConsoleStatus> {
|
|
34
|
+
return this._connector.sendRICRESTMsg("upy/repl/status", {}) as Promise<RaftMicroPythonConsoleStatus>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async output(since: number): Promise<RaftMicroPythonConsoleOutput> {
|
|
38
|
+
return this._connector.sendRICRESTMsg("upy/repl/output", { since }) as Promise<RaftMicroPythonConsoleOutput>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async input(data: string, newline = true): Promise<{ rslt?: string }> {
|
|
42
|
+
const payload = newline ? `${data}${REPL_NEWLINE}` : data;
|
|
43
|
+
return this._connector.sendRICRESTMsg(
|
|
44
|
+
`upy/repl/input?data=${payload}&newline=0`,
|
|
45
|
+
{}
|
|
46
|
+
) as Promise<{ rslt?: string }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async interrupt(): Promise<{ rslt?: string }> {
|
|
50
|
+
return this._connector.sendRICRESTMsg("upy/repl/interrupt", {}) as Promise<{ rslt?: string }>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async openStream(onData: RaftRtStreamDataCBType): Promise<RaftRtStreamHandle> {
|
|
54
|
+
return this._connector.openRtStream({
|
|
55
|
+
fileName: "upyconsole",
|
|
56
|
+
endpoint: "upyconsole",
|
|
57
|
+
onData,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async streamInput(
|
|
62
|
+
data: string,
|
|
63
|
+
newline = true,
|
|
64
|
+
onData: RaftRtStreamDataCBType = () => {}
|
|
65
|
+
): Promise<boolean> {
|
|
66
|
+
const payload = newline ? `${data}${REPL_NEWLINE}` : data;
|
|
67
|
+
const streamHandle = await this.openStream(onData);
|
|
68
|
+
try {
|
|
69
|
+
return await streamHandle.sendText(payload);
|
|
70
|
+
} finally {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
streamHandle.close().catch(() => {
|
|
73
|
+
// The firmware may close short-lived console sessions first.
|
|
74
|
+
});
|
|
75
|
+
}, this._streamDrainMs);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/RaftMsgHandler.ts
CHANGED
|
@@ -269,8 +269,11 @@ export default class RaftMsgHandler {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
} else {
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
// Some REST APIs return data-only JSON, including an empty object, with no rslt field.
|
|
273
|
+
// Treat parseable JSON as a successful transport response and let callers interpret it.
|
|
274
|
+
msgRsltCode = RaftMsgResultCode.MESSAGE_RESULT_OK;
|
|
275
|
+
RaftLog.verbose(
|
|
276
|
+
`_handleResponseMessages RICREST response without rslt ${rxMsgNum == 0 ? "unnumbered" : "msgNum " + rxMsgNum.toString()} resp ${restStr}`,
|
|
274
277
|
);
|
|
275
278
|
}
|
|
276
279
|
|
|
@@ -726,9 +729,10 @@ export default class RaftMsgHandler {
|
|
|
726
729
|
|
|
727
730
|
async sendFileBlock(
|
|
728
731
|
blockContents: Uint8Array,
|
|
729
|
-
blockStart: number
|
|
732
|
+
blockStart: number,
|
|
733
|
+
streamID = 0,
|
|
730
734
|
): Promise<boolean> {
|
|
731
|
-
const msgBuf = this.encodeFileStreamBlock(blockContents, blockStart,
|
|
735
|
+
const msgBuf = this.encodeFileStreamBlock(blockContents, blockStart, streamID);
|
|
732
736
|
|
|
733
737
|
// // Debug
|
|
734
738
|
// RaftLog.debug(
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
//
|
|
3
|
+
// RaftSystem
|
|
4
|
+
// Part of RaftJS
|
|
5
|
+
//
|
|
6
|
+
// Rob Dobson 2025
|
|
7
|
+
// (C) 2020-2025 All rights reserved
|
|
8
|
+
//
|
|
9
|
+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
10
|
+
|
|
11
|
+
import { RaftPublishFrameMeta } from "./RaftTypes";
|
|
12
|
+
|
|
13
|
+
const RAFT_PUBLISH_PREFIX_LEN = 2;
|
|
14
|
+
const DEVBIN_MAGIC_MIN = 0xDB;
|
|
15
|
+
const DEVBIN_MAGIC_MAX = 0xDF;
|
|
16
|
+
const DEVBIN_VERSION_BASE = 0xDA;
|
|
17
|
+
|
|
18
|
+
export function inspectPublishFrame(
|
|
19
|
+
payload: Uint8Array,
|
|
20
|
+
topicLookup?: (topicIndex: number) => string | undefined,
|
|
21
|
+
): RaftPublishFrameMeta {
|
|
22
|
+
|
|
23
|
+
if (payload.length < RAFT_PUBLISH_PREFIX_LEN) {
|
|
24
|
+
return { frameType: "unknown" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const payloadStartPos = RAFT_PUBLISH_PREFIX_LEN;
|
|
28
|
+
if (payload.length <= payloadStartPos) {
|
|
29
|
+
return { frameType: "unknown" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Try JSON first
|
|
33
|
+
if (payload[payloadStartPos] === 0x7B) {
|
|
34
|
+
try {
|
|
35
|
+
const jsonString = new TextDecoder("utf-8").decode(payload.slice(payloadStartPos));
|
|
36
|
+
const jsonObj = JSON.parse(jsonString) as Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
let topicIndex: number | undefined = undefined;
|
|
39
|
+
let topicName: string | undefined = undefined;
|
|
40
|
+
if (typeof jsonObj._t === "number") {
|
|
41
|
+
topicIndex = jsonObj._t;
|
|
42
|
+
topicName = topicLookup ? topicLookup(topicIndex) : undefined;
|
|
43
|
+
} else if (typeof jsonObj._t === "string") {
|
|
44
|
+
topicName = jsonObj._t;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const version = typeof jsonObj._v === "number" ? jsonObj._v : undefined;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
frameType: "json",
|
|
51
|
+
topicIndex,
|
|
52
|
+
topicName,
|
|
53
|
+
version,
|
|
54
|
+
jsonString,
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return { frameType: "unknown" };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Binary (devbin legacy or enveloped)
|
|
62
|
+
const firstBinaryByte = payload[payloadStartPos];
|
|
63
|
+
if ((firstBinaryByte & 0xF0) === 0xD0) {
|
|
64
|
+
if (firstBinaryByte < DEVBIN_MAGIC_MIN || firstBinaryByte > DEVBIN_MAGIC_MAX) {
|
|
65
|
+
return {
|
|
66
|
+
frameType: "binary",
|
|
67
|
+
binaryHasEnvelope: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const topicIndex = payload.length > payloadStartPos + 1 ? payload[payloadStartPos + 1] : undefined;
|
|
72
|
+
const topicName = (topicIndex !== undefined && topicIndex !== 0xFF && topicLookup)
|
|
73
|
+
? topicLookup(topicIndex)
|
|
74
|
+
: undefined;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
frameType: "binary",
|
|
78
|
+
topicIndex,
|
|
79
|
+
topicName,
|
|
80
|
+
version: firstBinaryByte - DEVBIN_VERSION_BASE,
|
|
81
|
+
binaryHasEnvelope: true,
|
|
82
|
+
binaryPayloadOffset: payloadStartPos + 2,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Legacy binary format (no envelope)
|
|
87
|
+
return {
|
|
88
|
+
frameType: "binary",
|
|
89
|
+
binaryHasEnvelope: false,
|
|
90
|
+
binaryPayloadOffset: payloadStartPos,
|
|
91
|
+
};
|
|
92
|
+
}
|