@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.
Files changed (163) hide show
  1. package/devdocs/devbin-backwards-compatibility.md +105 -0
  2. package/devdocs/pseudocode-to-js-transpiler.md +563 -0
  3. package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
  4. package/dist/react-native/PseudocodeTranspiler.js +115 -0
  5. package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
  6. package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
  7. package/dist/react-native/RaftAttributeHandler.js +108 -32
  8. package/dist/react-native/RaftAttributeHandler.js.map +1 -1
  9. package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
  10. package/dist/react-native/RaftChannelBLE.web.js +59 -21
  11. package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
  12. package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
  13. package/dist/react-native/RaftChannelSimulated.js +10 -6
  14. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  15. package/dist/react-native/RaftChannelWebSerial.js +1 -1
  16. package/dist/react-native/RaftChannelWebSerial.js.map +1 -1
  17. package/dist/react-native/RaftChannelWebSocket.js +16 -1
  18. package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
  19. package/dist/react-native/RaftConnector.d.ts +11 -1
  20. package/dist/react-native/RaftConnector.js +75 -9
  21. package/dist/react-native/RaftConnector.js.map +1 -1
  22. package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
  23. package/dist/react-native/RaftCustomAttrHandler.js +32 -44
  24. package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
  25. package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
  26. package/dist/react-native/RaftDeviceInfo.js +8 -0
  27. package/dist/react-native/RaftDeviceInfo.js.map +1 -1
  28. package/dist/react-native/RaftDeviceManager.d.ts +47 -2
  29. package/dist/react-native/RaftDeviceManager.js +696 -104
  30. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  31. package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
  32. package/dist/react-native/RaftDeviceStates.d.ts +27 -3
  33. package/dist/react-native/RaftDeviceStates.js +31 -6
  34. package/dist/react-native/RaftDeviceStates.js.map +1 -1
  35. package/dist/react-native/RaftFileHandler.d.ts +0 -1
  36. package/dist/react-native/RaftFileHandler.js +61 -23
  37. package/dist/react-native/RaftFileHandler.js.map +1 -1
  38. package/dist/react-native/RaftPublish.d.ts +2 -0
  39. package/dist/react-native/RaftPublish.js +81 -0
  40. package/dist/react-native/RaftPublish.js.map +1 -0
  41. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  42. package/dist/react-native/RaftStreamHandler.js +66 -0
  43. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  44. package/dist/react-native/RaftStruct.d.ts +2 -2
  45. package/dist/react-native/RaftStruct.js +97 -26
  46. package/dist/react-native/RaftStruct.js.map +1 -1
  47. package/dist/react-native/RaftSystemType.d.ts +1 -0
  48. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  49. package/dist/react-native/RaftSystemUtils.js +51 -0
  50. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  51. package/dist/react-native/RaftTimezone.d.ts +16 -0
  52. package/dist/react-native/RaftTimezone.js +153 -0
  53. package/dist/react-native/RaftTimezone.js.map +1 -0
  54. package/dist/react-native/RaftTypes.d.ts +27 -1
  55. package/dist/react-native/RaftTypes.js.map +1 -1
  56. package/dist/react-native/RaftUpdateManager.js +1 -1
  57. package/dist/react-native/RaftUpdateManager.js.map +1 -1
  58. package/dist/react-native/main.d.ts +3 -0
  59. package/dist/react-native/main.js +6 -1
  60. package/dist/react-native/main.js.map +1 -1
  61. package/dist/web/PseudocodeTranspiler.d.ts +6 -0
  62. package/dist/web/PseudocodeTranspiler.js +115 -0
  63. package/dist/web/PseudocodeTranspiler.js.map +1 -0
  64. package/dist/web/RaftAttributeHandler.d.ts +1 -1
  65. package/dist/web/RaftAttributeHandler.js +108 -32
  66. package/dist/web/RaftAttributeHandler.js.map +1 -1
  67. package/dist/web/RaftChannelBLE.web.d.ts +4 -0
  68. package/dist/web/RaftChannelBLE.web.js +59 -21
  69. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  70. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  71. package/dist/web/RaftChannelSimulated.js +10 -6
  72. package/dist/web/RaftChannelSimulated.js.map +1 -1
  73. package/dist/web/RaftChannelWebSerial.js +1 -1
  74. package/dist/web/RaftChannelWebSerial.js.map +1 -1
  75. package/dist/web/RaftChannelWebSocket.js +16 -1
  76. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  77. package/dist/web/RaftConnector.d.ts +11 -1
  78. package/dist/web/RaftConnector.js +75 -9
  79. package/dist/web/RaftConnector.js.map +1 -1
  80. package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
  81. package/dist/web/RaftCustomAttrHandler.js +32 -44
  82. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  83. package/dist/web/RaftDeviceInfo.d.ts +18 -0
  84. package/dist/web/RaftDeviceInfo.js +8 -0
  85. package/dist/web/RaftDeviceInfo.js.map +1 -1
  86. package/dist/web/RaftDeviceManager.d.ts +47 -2
  87. package/dist/web/RaftDeviceManager.js +696 -104
  88. package/dist/web/RaftDeviceManager.js.map +1 -1
  89. package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
  90. package/dist/web/RaftDeviceStates.d.ts +27 -3
  91. package/dist/web/RaftDeviceStates.js +31 -6
  92. package/dist/web/RaftDeviceStates.js.map +1 -1
  93. package/dist/web/RaftFileHandler.d.ts +0 -1
  94. package/dist/web/RaftFileHandler.js +61 -23
  95. package/dist/web/RaftFileHandler.js.map +1 -1
  96. package/dist/web/RaftPublish.d.ts +2 -0
  97. package/dist/web/RaftPublish.js +81 -0
  98. package/dist/web/RaftPublish.js.map +1 -0
  99. package/dist/web/RaftStreamHandler.d.ts +11 -0
  100. package/dist/web/RaftStreamHandler.js +66 -0
  101. package/dist/web/RaftStreamHandler.js.map +1 -1
  102. package/dist/web/RaftStruct.d.ts +2 -2
  103. package/dist/web/RaftStruct.js +97 -26
  104. package/dist/web/RaftStruct.js.map +1 -1
  105. package/dist/web/RaftSystemType.d.ts +1 -0
  106. package/dist/web/RaftSystemUtils.d.ts +17 -1
  107. package/dist/web/RaftSystemUtils.js +51 -0
  108. package/dist/web/RaftSystemUtils.js.map +1 -1
  109. package/dist/web/RaftTimezone.d.ts +16 -0
  110. package/dist/web/RaftTimezone.js +153 -0
  111. package/dist/web/RaftTimezone.js.map +1 -0
  112. package/dist/web/RaftTypes.d.ts +27 -1
  113. package/dist/web/RaftTypes.js.map +1 -1
  114. package/dist/web/RaftUpdateManager.js +1 -1
  115. package/dist/web/RaftUpdateManager.js.map +1 -1
  116. package/dist/web/main.d.ts +3 -0
  117. package/dist/web/main.js +6 -1
  118. package/dist/web/main.js.map +1 -1
  119. package/examples/dashboard/package.json +2 -2
  120. package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
  121. package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
  122. package/examples/dashboard/src/DevicePanel.tsx +92 -11
  123. package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
  124. package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
  125. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  126. package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
  127. package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
  128. package/examples/dashboard/src/LoggingPanel.tsx +264 -0
  129. package/examples/dashboard/src/Main.tsx +12 -2
  130. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  131. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
  132. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  133. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  134. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
  135. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
  136. package/examples/dashboard/src/styles.css +766 -1
  137. package/notes/web-ble-reconnect-retry.md +69 -0
  138. package/package.json +10 -7
  139. package/src/PseudocodeTranspiler.test.ts +372 -0
  140. package/src/PseudocodeTranspiler.ts +127 -0
  141. package/src/RaftAttributeHandler.ts +152 -76
  142. package/src/RaftChannelBLE.web.ts +62 -20
  143. package/src/RaftChannelSimulated.ts +11 -6
  144. package/src/RaftChannelWebSerial.ts +1 -1
  145. package/src/RaftChannelWebSocket.ts +16 -2
  146. package/src/RaftConnector.ts +93 -15
  147. package/src/RaftCustomAttrHandler.ts +35 -45
  148. package/src/RaftDeviceInfo.ts +27 -0
  149. package/src/RaftDeviceManager.test.ts +164 -0
  150. package/src/RaftDeviceManager.ts +823 -121
  151. package/src/RaftDeviceMgrIF.ts +13 -2
  152. package/src/RaftDeviceStates.ts +49 -8
  153. package/src/RaftFileHandler.ts +69 -28
  154. package/src/RaftPublish.ts +92 -0
  155. package/src/RaftStreamHandler.ts +84 -1
  156. package/src/RaftStruct.test.ts +229 -0
  157. package/src/RaftStruct.ts +101 -37
  158. package/src/RaftSystemType.ts +1 -0
  159. package/src/RaftSystemUtils.ts +59 -0
  160. package/src/RaftTimezone.ts +151 -0
  161. package/src/RaftTypes.ts +34 -1
  162. package/src/RaftUpdateManager.ts +1 -1
  163. package/src/main.ts +3 -0
@@ -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
  }
@@ -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 attrState.values[attrState.values.length - 1].toString();
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
- isOnline: boolean;
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
- // Add the getDeviceKey method to generate a composite key
90
- export function getDeviceKey(busName: string, devAddr: string, devType: string): string {
91
- return `${busName}_${devAddr}_${devType}`;
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
  }
@@ -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 = 10;
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
- this._fileRxCrc16 = parseInt(cmdResp.crc16, 16);
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
- const crc16 = RaftMiniHDLC.crc16(this._fileRxBuffer);
532
- if (crc16 !== this._fileRxCrc16) {
533
- RaftLog.warn(`_receiveFileContents - CRC error ${crc16} ${this._fileRxCrc16}`);
534
- reject(new Error('fileReceive CRC error'));
535
- return;
536
- } else {
537
- RaftLog.info(`_receiveFileContents - CRC OK ${crc16} ${this._fileRxCrc16}`);
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 > this.OVERALL_FILE_TRANSFER_TIMEOUT_MS) ||
560
- (now - this._fileRxLastBlockTime > this.BLOCK_ACK_TIMEOUT_MS)) {
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 file end message
616
- const cmdMsg = `{"cmdName":"dfAck","reqStr":"getFile","okto":${this._fileRxBuffer.length},` +
617
- `"fileName":"${fileName}","streamID":${this._fileRxStreamID},"rslt":"ok"}`
618
- this._msgHandler.sendRICRESTNoResp(
619
- cmdMsg,
620
- RICRESTElemCode.RICREST_ELEM_CODE_COMMAND_FRAME,
621
- bridgeID
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 false;
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
+ }
@@ -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)