@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.
Files changed (172) 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 +9 -5
  14. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  15. package/dist/react-native/RaftChannelWebSocket.js +16 -1
  16. package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
  17. package/dist/react-native/RaftConnector.d.ts +29 -1
  18. package/dist/react-native/RaftConnector.js +177 -11
  19. package/dist/react-native/RaftConnector.js.map +1 -1
  20. package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
  21. package/dist/react-native/RaftCustomAttrHandler.js +32 -44
  22. package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
  23. package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
  24. package/dist/react-native/RaftDeviceInfo.js +8 -0
  25. package/dist/react-native/RaftDeviceInfo.js.map +1 -1
  26. package/dist/react-native/RaftDeviceManager.d.ts +30 -3
  27. package/dist/react-native/RaftDeviceManager.js +618 -107
  28. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  29. package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
  30. package/dist/react-native/RaftDeviceStates.d.ts +27 -3
  31. package/dist/react-native/RaftDeviceStates.js +31 -6
  32. package/dist/react-native/RaftDeviceStates.js.map +1 -1
  33. package/dist/react-native/RaftFileHandler.d.ts +1 -1
  34. package/dist/react-native/RaftFileHandler.js +101 -34
  35. package/dist/react-native/RaftFileHandler.js.map +1 -1
  36. package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
  37. package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
  38. package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
  39. package/dist/react-native/RaftMsgHandler.d.ts +1 -1
  40. package/dist/react-native/RaftMsgHandler.js +6 -3
  41. package/dist/react-native/RaftMsgHandler.js.map +1 -1
  42. package/dist/react-native/RaftPublish.d.ts +2 -0
  43. package/dist/react-native/RaftPublish.js +81 -0
  44. package/dist/react-native/RaftPublish.js.map +1 -0
  45. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  46. package/dist/react-native/RaftStreamHandler.js +66 -0
  47. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  48. package/dist/react-native/RaftStruct.d.ts +2 -2
  49. package/dist/react-native/RaftStruct.js +97 -26
  50. package/dist/react-native/RaftStruct.js.map +1 -1
  51. package/dist/react-native/RaftSystemType.d.ts +1 -0
  52. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  53. package/dist/react-native/RaftSystemUtils.js +51 -0
  54. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  55. package/dist/react-native/RaftTimezone.d.ts +16 -0
  56. package/dist/react-native/RaftTimezone.js +153 -0
  57. package/dist/react-native/RaftTimezone.js.map +1 -0
  58. package/dist/react-native/RaftTypes.d.ts +46 -1
  59. package/dist/react-native/RaftTypes.js.map +1 -1
  60. package/dist/react-native/RaftUpdateManager.js +1 -1
  61. package/dist/react-native/RaftUpdateManager.js.map +1 -1
  62. package/dist/react-native/main.d.ts +3 -0
  63. package/dist/react-native/main.js +8 -1
  64. package/dist/react-native/main.js.map +1 -1
  65. package/dist/web/PseudocodeTranspiler.d.ts +6 -0
  66. package/dist/web/PseudocodeTranspiler.js +115 -0
  67. package/dist/web/PseudocodeTranspiler.js.map +1 -0
  68. package/dist/web/RaftAttributeHandler.d.ts +1 -1
  69. package/dist/web/RaftAttributeHandler.js +108 -32
  70. package/dist/web/RaftAttributeHandler.js.map +1 -1
  71. package/dist/web/RaftChannelBLE.web.d.ts +4 -0
  72. package/dist/web/RaftChannelBLE.web.js +59 -21
  73. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  74. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  75. package/dist/web/RaftChannelSimulated.js +9 -5
  76. package/dist/web/RaftChannelSimulated.js.map +1 -1
  77. package/dist/web/RaftChannelWebSocket.js +16 -1
  78. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  79. package/dist/web/RaftConnector.d.ts +29 -1
  80. package/dist/web/RaftConnector.js +177 -11
  81. package/dist/web/RaftConnector.js.map +1 -1
  82. package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
  83. package/dist/web/RaftCustomAttrHandler.js +32 -44
  84. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  85. package/dist/web/RaftDeviceInfo.d.ts +18 -0
  86. package/dist/web/RaftDeviceInfo.js +8 -0
  87. package/dist/web/RaftDeviceInfo.js.map +1 -1
  88. package/dist/web/RaftDeviceManager.d.ts +30 -3
  89. package/dist/web/RaftDeviceManager.js +618 -107
  90. package/dist/web/RaftDeviceManager.js.map +1 -1
  91. package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
  92. package/dist/web/RaftDeviceStates.d.ts +27 -3
  93. package/dist/web/RaftDeviceStates.js +31 -6
  94. package/dist/web/RaftDeviceStates.js.map +1 -1
  95. package/dist/web/RaftFileHandler.d.ts +1 -1
  96. package/dist/web/RaftFileHandler.js +101 -34
  97. package/dist/web/RaftFileHandler.js.map +1 -1
  98. package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
  99. package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
  100. package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
  101. package/dist/web/RaftMsgHandler.d.ts +1 -1
  102. package/dist/web/RaftMsgHandler.js +6 -3
  103. package/dist/web/RaftMsgHandler.js.map +1 -1
  104. package/dist/web/RaftPublish.d.ts +2 -0
  105. package/dist/web/RaftPublish.js +81 -0
  106. package/dist/web/RaftPublish.js.map +1 -0
  107. package/dist/web/RaftStreamHandler.d.ts +11 -0
  108. package/dist/web/RaftStreamHandler.js +66 -0
  109. package/dist/web/RaftStreamHandler.js.map +1 -1
  110. package/dist/web/RaftStruct.d.ts +2 -2
  111. package/dist/web/RaftStruct.js +97 -26
  112. package/dist/web/RaftStruct.js.map +1 -1
  113. package/dist/web/RaftSystemType.d.ts +1 -0
  114. package/dist/web/RaftSystemUtils.d.ts +17 -1
  115. package/dist/web/RaftSystemUtils.js +51 -0
  116. package/dist/web/RaftSystemUtils.js.map +1 -1
  117. package/dist/web/RaftTimezone.d.ts +16 -0
  118. package/dist/web/RaftTimezone.js +153 -0
  119. package/dist/web/RaftTimezone.js.map +1 -0
  120. package/dist/web/RaftTypes.d.ts +46 -1
  121. package/dist/web/RaftTypes.js.map +1 -1
  122. package/dist/web/RaftUpdateManager.js +1 -1
  123. package/dist/web/RaftUpdateManager.js.map +1 -1
  124. package/dist/web/main.d.ts +3 -0
  125. package/dist/web/main.js +8 -1
  126. package/dist/web/main.js.map +1 -1
  127. package/examples/dashboard/package.json +2 -2
  128. package/examples/dashboard/src/DeviceActionsForm.tsx +177 -17
  129. package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
  130. package/examples/dashboard/src/DevicePanel.tsx +92 -11
  131. package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
  132. package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
  133. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  134. package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
  135. package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
  136. package/examples/dashboard/src/LoggingPanel.tsx +264 -0
  137. package/examples/dashboard/src/Main.tsx +12 -2
  138. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  139. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
  140. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  141. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  142. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
  143. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
  144. package/examples/dashboard/src/styles.css +766 -1
  145. package/notes/web-ble-reconnect-retry.md +69 -0
  146. package/package.json +10 -7
  147. package/src/PseudocodeTranspiler.test.ts +372 -0
  148. package/src/PseudocodeTranspiler.ts +127 -0
  149. package/src/RaftAttributeHandler.ts +152 -76
  150. package/src/RaftChannelBLE.web.ts +62 -20
  151. package/src/RaftChannelSimulated.ts +10 -5
  152. package/src/RaftChannelWebSocket.ts +16 -2
  153. package/src/RaftConnector.ts +204 -17
  154. package/src/RaftCustomAttrHandler.ts +35 -45
  155. package/src/RaftDeviceInfo.ts +27 -0
  156. package/src/RaftDeviceManager.test.ts +164 -0
  157. package/src/RaftDeviceManager.ts +705 -127
  158. package/src/RaftDeviceMgrIF.ts +13 -2
  159. package/src/RaftDeviceStates.ts +49 -8
  160. package/src/RaftFileHandler.ts +112 -39
  161. package/src/RaftMicroPythonConsoleClient.ts +78 -0
  162. package/src/RaftMsgHandler.ts +8 -4
  163. package/src/RaftPublish.ts +92 -0
  164. package/src/RaftStreamHandler.ts +84 -1
  165. package/src/RaftStruct.test.ts +229 -0
  166. package/src/RaftStruct.ts +101 -37
  167. package/src/RaftSystemType.ts +1 -0
  168. package/src/RaftSystemUtils.ts +59 -0
  169. package/src/RaftTimezone.ts +151 -0
  170. package/src/RaftTypes.ts +57 -1
  171. package/src/RaftUpdateManager.ts +1 -1
  172. 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,
@@ -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.rslt}`);
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 cmdMsg = `{"cmdName":"ufEnd","reqStr":"${reqStr}","fileType":"${fileDest}","fileName":"${fileName}","fileLen":${fileLen}}`;
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.rslt === 'ok';
234
+ return fileEndResp?.rslt === 'ok';
227
235
  }
228
236
 
229
237
  async _sendFileCancelMsg(): Promise<void> {
230
238
  // File cancel command message
231
- const cmdMsg = `{"cmdName":"ufCancel"}`;
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.rslt}`);
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 = 10;
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.rslt === 'ok') {
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
- this._fileRxCrc16 = parseInt(cmdResp.crc16, 16);
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
- 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}`);
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 > 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`);
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 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
- );
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 false;
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 (filePos === this._fileRxBuffer.length) {
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 ${filePos} fileBlockData ${RaftUtils.bufferToHex(fileBlockData)} out of sequence`);
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
+ }
@@ -269,8 +269,11 @@ export default class RaftMsgHandler {
269
269
  }
270
270
 
271
271
  } else {
272
- RaftLog.warn(
273
- `_handleResponseMessages RICREST response doesn't contain rslt ${rxMsgNum == 0 ? "unnumbered" : "msgNum " + rxMsgNum.toString()}resp ${restStr}`,
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, 0);
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
+ }