@robotical/raftjs 2.0.11 → 2.1.2
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 +10 -6
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSerial.js +1 -1
- package/dist/react-native/RaftChannelWebSerial.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 +11 -1
- package/dist/react-native/RaftConnector.js +75 -9
- 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 +47 -2
- package/dist/react-native/RaftDeviceManager.js +696 -104
- 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 +0 -1
- package/dist/react-native/RaftFileHandler.js +61 -23
- package/dist/react-native/RaftFileHandler.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 +27 -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 +6 -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 +10 -6
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSerial.js +1 -1
- package/dist/web/RaftChannelWebSerial.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 +11 -1
- package/dist/web/RaftConnector.js +75 -9
- 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 +47 -2
- package/dist/web/RaftDeviceManager.js +696 -104
- 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 +0 -1
- package/dist/web/RaftFileHandler.js +61 -23
- package/dist/web/RaftFileHandler.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 +27 -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 +6 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
- 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 +11 -6
- package/src/RaftChannelWebSerial.ts +1 -1
- package/src/RaftChannelWebSocket.ts +16 -2
- package/src/RaftConnector.ts +93 -15
- package/src/RaftCustomAttrHandler.ts +35 -45
- package/src/RaftDeviceInfo.ts +27 -0
- package/src/RaftDeviceManager.test.ts +164 -0
- package/src/RaftDeviceManager.ts +823 -121
- package/src/RaftDeviceMgrIF.ts +13 -2
- package/src/RaftDeviceStates.ts +49 -8
- package/src/RaftFileHandler.ts +69 -28
- 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 +34 -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,
|
|
@@ -77,7 +78,6 @@ export default class RaftFileHandler {
|
|
|
77
78
|
private _fileRxLastAckTime = 0;
|
|
78
79
|
private _fileRxLastBlockTime = 0;
|
|
79
80
|
private _fileRxLastAckPos = 0;
|
|
80
|
-
private OVERALL_FILE_TRANSFER_TIMEOUT_MS = 100000;
|
|
81
81
|
private FILE_RX_ACK_RESEND_TIMEOUT_MS = 1000;
|
|
82
82
|
|
|
83
83
|
// RaftCommsStats
|
|
@@ -454,8 +454,9 @@ export default class RaftFileHandler {
|
|
|
454
454
|
// Send contents
|
|
455
455
|
const fileContents = await this._receiveFileContents(progressCallback, bridgeID);
|
|
456
456
|
|
|
457
|
-
// Send file end
|
|
458
|
-
await this._receiveFileEnd(fileName, bridgeID)
|
|
457
|
+
// Send file end (and verify deferred CRC if applicable)
|
|
458
|
+
if (!await this._receiveFileEnd(fileName, bridgeID))
|
|
459
|
+
return new RaftFileDownloadResult();
|
|
459
460
|
|
|
460
461
|
// Clean up
|
|
461
462
|
await this.awaitOutstandingMsgPromises(true);
|
|
@@ -471,14 +472,16 @@ export default class RaftFileHandler {
|
|
|
471
472
|
async _receiveFileStart(fileName: string, bridgeID: number | undefined) : Promise<boolean> {
|
|
472
473
|
|
|
473
474
|
const blockMaxSizeRequested = 5000;
|
|
474
|
-
const batchAckSizeRequested =
|
|
475
|
+
const batchAckSizeRequested = 4;
|
|
475
476
|
const fileSrc = "fs";
|
|
476
477
|
|
|
477
478
|
// Request file transfer
|
|
478
479
|
// Frames follow the approach used in the web interface start, block..., end
|
|
480
|
+
// crcAt:end requests the server to defer CRC computation (non-blocking start)
|
|
479
481
|
const cmdMsg = `{"cmdName":"dfStart","reqStr":"getFile","fileType":"${fileSrc}",` +
|
|
480
482
|
`"batchMsgSize":${blockMaxSizeRequested},` +
|
|
481
483
|
`"batchAckSize":${batchAckSizeRequested},` +
|
|
484
|
+
`"crcAt":"end",` +
|
|
482
485
|
`"fileName":"${fileName}"}`
|
|
483
486
|
|
|
484
487
|
// Send
|
|
@@ -499,7 +502,8 @@ export default class RaftFileHandler {
|
|
|
499
502
|
this._fileRxBatchAckSize = cmdResp.batchAckSize;
|
|
500
503
|
this._fileRxStreamID = cmdResp.streamID;
|
|
501
504
|
this._fileRxFileLen = cmdResp.fileLen;
|
|
502
|
-
|
|
505
|
+
// CRC may be provided at start (old firmware) or deferred to dfEnd (new firmware)
|
|
506
|
+
this._fileRxCrc16 = cmdResp.crc16 ? parseInt(cmdResp.crc16, 16) : -1;
|
|
503
507
|
this._fileRxBuffer = new Uint8Array(0);
|
|
504
508
|
this._fileRxLastAckTime = 0;
|
|
505
509
|
this._fileRxLastAckPos = 0;
|
|
@@ -514,6 +518,18 @@ export default class RaftFileHandler {
|
|
|
514
518
|
bridgeID: number | undefined
|
|
515
519
|
): Promise<RaftFileDownloadResult> {
|
|
516
520
|
|
|
521
|
+
// Calculate dynamic timeouts based on file size and connection speed
|
|
522
|
+
// Use batch message size as proxy for connection type:
|
|
523
|
+
// BLE ~500 byte blocks ~5 KB/s, WebSocket ~5000 byte blocks ~50 KB/s
|
|
524
|
+
const estimatedBytesPerSec = this._fileRxBatchMsgSize <= 500 ? 5000 : 50000;
|
|
525
|
+
const BASE_TIMEOUT_MS = 30000;
|
|
526
|
+
const SAFETY_FACTOR = 2;
|
|
527
|
+
const transferTimeMs = (this._fileRxFileLen / estimatedBytesPerSec) * 1000 * SAFETY_FACTOR;
|
|
528
|
+
const overallTimeoutMs = BASE_TIMEOUT_MS + transferTimeMs;
|
|
529
|
+
const blockTimeoutMs = this.BLOCK_ACK_TIMEOUT_MS;
|
|
530
|
+
RaftLog.info(`_receiveFileContents - fileLen ${this._fileRxFileLen} batchMsgSize ${this._fileRxBatchMsgSize} ` +
|
|
531
|
+
`estimatedBytesPerSec ${estimatedBytesPerSec} overallTimeoutMs ${overallTimeoutMs} blockTimeoutMs ${blockTimeoutMs}`);
|
|
532
|
+
|
|
517
533
|
// Wait for file to be received
|
|
518
534
|
return new Promise<RaftFileDownloadResult>((resolve, reject) => {
|
|
519
535
|
const startTime = Date.now();
|
|
@@ -527,14 +543,16 @@ export default class RaftFileHandler {
|
|
|
527
543
|
progressCallback(this._fileRxBuffer.length, this._fileRxFileLen);
|
|
528
544
|
}
|
|
529
545
|
|
|
530
|
-
// Check CRC
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
546
|
+
// Check CRC if it was provided at start (old firmware)
|
|
547
|
+
if (this._fileRxCrc16 >= 0) {
|
|
548
|
+
const crc16 = RaftMiniHDLC.crc16(this._fileRxBuffer);
|
|
549
|
+
if (crc16 !== this._fileRxCrc16) {
|
|
550
|
+
RaftLog.warn(`_receiveFileContents - CRC error ${crc16} ${this._fileRxCrc16}`);
|
|
551
|
+
reject(new Error('fileReceive CRC error'));
|
|
552
|
+
return;
|
|
553
|
+
} else {
|
|
554
|
+
RaftLog.info(`_receiveFileContents - CRC OK ${crc16} ${this._fileRxCrc16}`);
|
|
555
|
+
}
|
|
538
556
|
}
|
|
539
557
|
resolve(new RaftFileDownloadResult(this._fileRxBuffer));
|
|
540
558
|
return;
|
|
@@ -556,9 +574,11 @@ export default class RaftFileHandler {
|
|
|
556
574
|
const now = Date.now();
|
|
557
575
|
|
|
558
576
|
// Check for overall timeouts
|
|
559
|
-
if ((now - startTime >
|
|
560
|
-
(now - this._fileRxLastBlockTime >
|
|
561
|
-
RaftLog.warn(`_receiveFileContents - time-out no new data received`
|
|
577
|
+
if ((now - startTime > overallTimeoutMs) ||
|
|
578
|
+
(now - this._fileRxLastBlockTime > blockTimeoutMs)) {
|
|
579
|
+
RaftLog.warn(`_receiveFileContents - time-out no new data received ` +
|
|
580
|
+
`elapsed ${now - startTime}ms overallTimeout ${overallTimeoutMs}ms ` +
|
|
581
|
+
`blockGap ${now - this._fileRxLastBlockTime}ms blockTimeout ${blockTimeoutMs}ms`);
|
|
562
582
|
this._fileRxActive = false;
|
|
563
583
|
reject(new Error('fileReceive failed'));
|
|
564
584
|
return;
|
|
@@ -574,7 +594,7 @@ export default class RaftFileHandler {
|
|
|
574
594
|
if (this._fileRxBuffer.length - this._fileRxLastAckPos >= this._fileRxBatchAckSize * this._fileRxBatchMsgSize) {
|
|
575
595
|
ackRequired = true;
|
|
576
596
|
}
|
|
577
|
-
// RaftLog.info(`_receiveFileContents ${ackRequired ? "ACK_REQUIRED" : "ACK_NOTREQUIRED"} ${this._fileRxBuffer.length} ${this._fileRxLastAckPos} ${this._fileRxBatchAckSize} ${this._fileRxBatchMsgSize}`);
|
|
597
|
+
// RaftLog.info(`_receiveFileContents ${ackRequired ? "ACK_REQUIRED" : "ACK_NOTREQUIRED"} ${this._fileRxBuffer.length} ${this._fileRxLastAckPos} ${this._fileRxBatchAckSize} ${this._fileRxBatchMsgSize}`);
|
|
578
598
|
|
|
579
599
|
// Check if ack required
|
|
580
600
|
if (ackRequired) {
|
|
@@ -582,7 +602,7 @@ export default class RaftFileHandler {
|
|
|
582
602
|
// Ack timing
|
|
583
603
|
this._fileRxLastAckTime = Date.now();
|
|
584
604
|
this._fileRxLastAckPos = this._fileRxBuffer.length;
|
|
585
|
-
|
|
605
|
+
|
|
586
606
|
// Okto message
|
|
587
607
|
const cmdMsg = `{"cmdName":"dfAck","okto":${this._fileRxBuffer.length},` +
|
|
588
608
|
`"streamID":${this._fileRxStreamID},"rslt":"ok"}`;
|
|
@@ -612,18 +632,39 @@ export default class RaftFileHandler {
|
|
|
612
632
|
|
|
613
633
|
async _receiveFileEnd(fileName: string, bridgeID: number | undefined): Promise<boolean> {
|
|
614
634
|
|
|
615
|
-
// Send
|
|
616
|
-
const cmdMsg = `{"cmdName":"
|
|
617
|
-
`"fileName":"${fileName}","
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
635
|
+
// Send dfEnd and wait for response (may contain deferred CRC)
|
|
636
|
+
const cmdMsg = `{"cmdName":"dfEnd","reqStr":"getFile",` +
|
|
637
|
+
`"fileName":"${fileName}","fileLen":${this._fileRxBuffer.length},` +
|
|
638
|
+
`"streamID":${this._fileRxStreamID}}`
|
|
639
|
+
|
|
640
|
+
let cmdResp = null;
|
|
641
|
+
try {
|
|
642
|
+
cmdResp = await this._msgHandler.sendRICREST<RaftFileDownloadEndResp>(
|
|
643
|
+
cmdMsg,
|
|
644
|
+
RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME,
|
|
645
|
+
bridgeID,
|
|
646
|
+
);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
RaftLog.warn(`_receiveFileEnd error ${err}`);
|
|
649
|
+
this._fileRxActive = false;
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Check deferred CRC if start response didn't include one
|
|
654
|
+
if (this._fileRxCrc16 < 0 && cmdResp.crc16) {
|
|
655
|
+
const expectedCrc = parseInt(cmdResp.crc16, 16);
|
|
656
|
+
const actualCrc = RaftMiniHDLC.crc16(this._fileRxBuffer);
|
|
657
|
+
if (actualCrc !== expectedCrc) {
|
|
658
|
+
RaftLog.warn(`_receiveFileEnd - CRC error actual=${actualCrc} expected=${expectedCrc}`);
|
|
659
|
+
this._fileRxActive = false;
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
RaftLog.info(`_receiveFileEnd - CRC OK ${actualCrc}`);
|
|
663
|
+
}
|
|
623
664
|
|
|
624
665
|
// No longer active
|
|
625
666
|
this._fileRxActive = false;
|
|
626
|
-
return
|
|
667
|
+
return true;
|
|
627
668
|
}
|
|
628
669
|
|
|
629
670
|
async _sendFileRxCancelMsg(bridgeID: number | undefined): Promise<void> {
|
|
@@ -665,4 +706,4 @@ export default class RaftFileHandler {
|
|
|
665
706
|
isFileRxActive(): boolean {
|
|
666
707
|
return this._fileRxActive;
|
|
667
708
|
}
|
|
668
|
-
}
|
|
709
|
+
}
|
|
@@ -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
|
+
}
|
package/src/RaftStreamHandler.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import RaftLog from './RaftLog'
|
|
12
12
|
import RaftMsgHandler from './RaftMsgHandler';
|
|
13
13
|
import RaftCommsStats from './RaftCommsStats';
|
|
14
|
-
import { RaftOKFail, RaftStreamStartResp, RaftStreamType } from './RaftTypes';
|
|
14
|
+
import { RaftOKFail, RaftStreamStartResp, RaftStreamType, RaftStreamDataProgressCBType } from './RaftTypes';
|
|
15
15
|
import RaftConnector from './RaftConnector';
|
|
16
16
|
import { RaftConnEvent } from './RaftConnEvents';
|
|
17
17
|
import { RICRESTElemCode } from './RaftProtocolDefs'
|
|
@@ -77,6 +77,89 @@ export default class RaftStreamHandler {
|
|
|
77
77
|
this._legacySoktoMode = legacyMode;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* streamData - stream arbitrary data to a named firmware endpoint using the RT_STREAM protocol.
|
|
82
|
+
* Returns a promise that resolves when the stream is complete or rejects on failure.
|
|
83
|
+
* @param streamContents data to stream
|
|
84
|
+
* @param fileName logical filename sent in ufStart (used by firmware to detect format, e.g. "pattern.thr")
|
|
85
|
+
* @param targetEndpoint REST API endpoint name registered on the firmware (e.g. "streampattern")
|
|
86
|
+
* @param progressCallback optional callback reporting (bytesSent, totalBytes, progressFraction)
|
|
87
|
+
* @returns Promise<boolean> true if stream completed successfully
|
|
88
|
+
*/
|
|
89
|
+
async streamData(
|
|
90
|
+
streamContents: Uint8Array,
|
|
91
|
+
fileName: string,
|
|
92
|
+
targetEndpoint: string,
|
|
93
|
+
progressCallback?: RaftStreamDataProgressCBType,
|
|
94
|
+
): Promise<boolean> {
|
|
95
|
+
// Reject if another stream is starting
|
|
96
|
+
if (this._streamIsStarting || this._lastStreamStartTime > (Date.now() - 500)) {
|
|
97
|
+
RaftLog.warn(`streamData: unable to start, too soon since last request`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._streamIsStarting = true;
|
|
102
|
+
this._lastStreamStartTime = Date.now();
|
|
103
|
+
this._soktoReceived = false;
|
|
104
|
+
this._soktoPos = 0;
|
|
105
|
+
this._streamPos = 0;
|
|
106
|
+
this._streamBuffer = RaftUtils.toArrayBufferView(streamContents);
|
|
107
|
+
|
|
108
|
+
// Send ufStart
|
|
109
|
+
const startOk = await this._sendStreamStartMsg(
|
|
110
|
+
fileName, targetEndpoint, RaftStreamType.REAL_TIME_STREAM, streamContents,
|
|
111
|
+
);
|
|
112
|
+
this._streamIsStarting = false;
|
|
113
|
+
|
|
114
|
+
if (!startOk) {
|
|
115
|
+
RaftLog.warn(`streamData: ufStart failed`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Send blocks
|
|
120
|
+
if (this._streamID === null) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let pos = 0;
|
|
125
|
+
while (pos < this._streamBuffer.length) {
|
|
126
|
+
// Respect SOKTO feedback — if firmware reported a position mismatch, rewind
|
|
127
|
+
if (this._soktoReceived) {
|
|
128
|
+
RaftLog.verbose(`streamData: sokto received, pos was ${pos}, soktoPos ${this._soktoPos}`);
|
|
129
|
+
this._soktoReceived = false;
|
|
130
|
+
// Slow down on backpressure
|
|
131
|
+
await new Promise(r => setTimeout(r, 50));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const blockSize = Math.min(this._streamBuffer.length - pos, this._maxBlockSize);
|
|
135
|
+
const block = this._streamBuffer.slice(pos, pos + blockSize);
|
|
136
|
+
if (block.length > 0) {
|
|
137
|
+
const sentOk = await this._msgHandler.sendStreamBlock(block, pos, this._streamID);
|
|
138
|
+
this._commsStats.recordStreamBytes(block.length);
|
|
139
|
+
if (!sentOk) {
|
|
140
|
+
RaftLog.warn(`streamData: sendStreamBlock failed at pos ${pos}`);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
pos += blockSize;
|
|
144
|
+
this._streamPos = pos;
|
|
145
|
+
|
|
146
|
+
// Progress callback
|
|
147
|
+
if (progressCallback) {
|
|
148
|
+
const progress = this._streamBuffer.length > 0 ? pos / this._streamBuffer.length : 1;
|
|
149
|
+
progressCallback(pos, this._streamBuffer.length, progress);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Yield to avoid hogging the event loop
|
|
154
|
+
await new Promise(r => setTimeout(r, 1));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Send ufEnd
|
|
158
|
+
const endOk = await this._sendStreamEndMsg(this._streamID);
|
|
159
|
+
RaftLog.debug(`streamData: complete, endOk=${endOk}`);
|
|
160
|
+
return endOk;
|
|
161
|
+
}
|
|
162
|
+
|
|
80
163
|
// Start streaming audio
|
|
81
164
|
streamAudio(streamContents: Uint8Array, clearExisting: boolean, audioDuration: number): void {
|
|
82
165
|
if (!clearExisting)
|