@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
@@ -175,6 +175,16 @@ export default class RaftChannelWebSocket implements RaftChannel {
175
175
  // return false;
176
176
  // }
177
177
  this._webSocket = null;
178
+ // Close any existing WebSocket before creating new one
179
+ RaftLog.verbose(`[RaftChannelWebSocket._wsConnect] START existing WebSocket?, ${!!this._webSocket}`);
180
+ if (this._webSocket) {
181
+ RaftLog.verbose(`[RaftChannelWebSocket._wsConnect] Closing existing WebSocket...`);
182
+ try {
183
+ this._webSocket.close(1000);
184
+ } catch (e) {
185
+ RaftLog.warn(`[RaftChannelWebSocket._wsConnect] Error closing existing WebSocket: ${e}`);
186
+ }
187
+ }
178
188
  return new Promise((resolve: (value: boolean | PromiseLike<boolean>) => void,
179
189
  reject: (reason?: unknown) => void) => {
180
190
  this._webSocketOpen(wsURL).then((ws) => {
@@ -222,6 +232,7 @@ export default class RaftChannelWebSocket implements RaftChannel {
222
232
 
223
233
  // Open the socket
224
234
  try {
235
+ RaftLog.verbose(`[RaftChannelWebSocket._webSocketOpen] Creating WebSocket: ${url}`);
225
236
  const webSocket = new WebSocket(url);
226
237
 
227
238
  // Open socket
@@ -232,10 +243,13 @@ export default class RaftChannelWebSocket implements RaftChannel {
232
243
  this._isConnected = true;
233
244
  resolve(webSocket);
234
245
  };
235
- webSocket.onerror = function (evt: WebSocket.ErrorEvent) {
246
+ webSocket.onerror = (evt: WebSocket.ErrorEvent) => {
236
247
  RaftLog.warn(`RaftChannelWebSocket._webSocketOpen - onerror: ${evt.message}`);
237
248
  reject(evt);
238
- }
249
+ };
250
+ webSocket.onclose = (evt: WebSocket.CloseEvent) => {
251
+ RaftLog.info(`[RaftChannelWebSocket._webSocketOpen] onclose fired! code: ${evt.code} reason: ${evt.reason} wasClean: ${evt.wasClean}`);
252
+ };
239
253
  } catch (error: unknown) {
240
254
  RaftLog.warn(`RaftChannelWebSocket._webSocketOpen - open failed ${error}`);
241
255
  reject(error);
@@ -14,7 +14,7 @@ import RaftChannelWebSocket from "./RaftChannelWebSocket";
14
14
  import RaftChannelWebSerial from "./RaftChannelWebSerial";
15
15
  import RaftChannelSimulated from "./RaftChannelSimulated";
16
16
  import RaftCommsStats from "./RaftCommsStats";
17
- import { RaftEventFn, RaftOKFail, RaftFileSendType, RaftFileDownloadResult, RaftProgressCBType, RaftBridgeSetupResp, RaftFileDownloadFn, RaftReportMsg } from "./RaftTypes";
17
+ import { RaftEventFn, RaftOKFail, RaftFileSendType, RaftFileDownloadResult, RaftProgressCBType, RaftStreamDataProgressCBType, RaftBridgeSetupResp, RaftFileDownloadFn, RaftReportMsg, RaftRtStreamDataCBType, RaftRtStreamHandle, RaftRtStreamOptions, RaftRtStreamStartResp } from "./RaftTypes";
18
18
  import RaftSystemUtils from "./RaftSystemUtils";
19
19
  import RaftFileHandler from "./RaftFileHandler";
20
20
  import RaftStreamHandler from "./RaftStreamHandler";
@@ -23,8 +23,7 @@ import { RaftConnEvent, RaftConnEventNames } from "./RaftConnEvents";
23
23
  import { RaftGetSystemTypeCBType, RaftSystemType } from "./RaftSystemType";
24
24
  import { RaftUpdateEvent, RaftUpdateEventNames } from "./RaftUpdateEvents";
25
25
  import RaftUpdateManager from "./RaftUpdateManager";
26
- import { createBLEChannel } from "./RaftChannelBLEFactory";
27
-
26
+ import { createBLEChannel } from "./RaftChannelBLEFactory";import { getHostPosixTZ } from './RaftTimezone';
28
27
 
29
28
  export default class RaftConnector {
30
29
 
@@ -88,6 +87,10 @@ export default class RaftConnector {
88
87
  // Update manager
89
88
  private _raftUpdateManager: RaftUpdateManager | null = null;
90
89
 
90
+ // Open-ended RT stream callbacks keyed by streamID
91
+ private _rtStreamCallbacks = new Map<number, RaftRtStreamDataCBType>();
92
+ private _fallbackRtStreamCallback: { streamID: number, callback: RaftRtStreamDataCBType } | null = null;
93
+
91
94
  /**
92
95
  * RaftConnector constructor
93
96
  * @param getSystemTypeCB - callback to get system type
@@ -194,6 +197,22 @@ export default class RaftConnector {
194
197
  return this._commsStats;
195
198
  }
196
199
 
200
+ /**
201
+ * getOperationQueueDepth
202
+ * @returns number of high-level device operations queued or running
203
+ */
204
+ getOperationQueueDepth(): number {
205
+ return 0;
206
+ }
207
+
208
+ /**
209
+ * isOperationBusy
210
+ * @returns true when a high-level device operation is queued or running
211
+ */
212
+ isOperationBusy(): boolean {
213
+ return false;
214
+ }
215
+
197
216
  /**
198
217
  * Get Raft message handler (to allow message sending and receiving)
199
218
  * @returns RaftMsgHandler - Raft message handler
@@ -214,7 +233,22 @@ export default class RaftConnector {
214
233
  * Initialize the Raft channel
215
234
  */
216
235
  async initializeChannel(method: string): Promise<boolean> {
236
+ // Disconnect any existing channel first to avoid stale connections
237
+ RaftLog.verbose(`[RaftConnector.initializeChannel] START method=${method} hasExistingChannel=${!!this._raftChannel}`);
238
+ if (this._raftChannel) {
239
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Disconnecting existing channel...`);
240
+ try {
241
+ await this._raftChannel.disconnect();
242
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Existing channel disconnected successfully`);
243
+ } catch (e) {
244
+ RaftLog.warn(`[RaftConnector.initializeChannel] Error disconnecting existing channel: ${e}`);
245
+ }
246
+ this._raftChannel = null;
247
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Existing channel nulled`);
248
+ }
249
+
217
250
  // Initialize raft channel
251
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Creating new channel...`);
218
252
  if (method === 'WebBLE' || method === 'PhoneBLE') {
219
253
  const RaftChannelBLE = createBLEChannel();
220
254
  this._raftChannel = new RaftChannelBLE();
@@ -226,7 +260,7 @@ export default class RaftConnector {
226
260
  this._raftChannel = new RaftChannelWebSerial();
227
261
  this._channelConnMethod = 'WebSerial';
228
262
  } else if (method === 'Simulated') {
229
- this._raftChannel = new RaftChannelSimulated();
263
+ this._raftChannel = new RaftChannelSimulated();
230
264
  this._channelConnMethod = 'Simulated';
231
265
  } else {
232
266
  RaftLog.warn('Unknown method: ' + method);
@@ -268,7 +302,7 @@ export default class RaftConnector {
268
302
  // Store locator
269
303
  this._channelConnLocator = locator;
270
304
 
271
- // Connect
305
+ // Connect channel first (system type resolution needs a live connection)
272
306
  let connOk = false;
273
307
  try {
274
308
  // Event
@@ -281,9 +315,9 @@ export default class RaftConnector {
281
315
  }
282
316
 
283
317
  if (connOk) {
284
- // Get system type
318
+
319
+ // Resolve system type now that the channel is connected
285
320
  if (this._getSystemTypeCB) {
286
- // Get system type
287
321
  this._systemType = await this._getSystemTypeCB(this._raftSystemUtils);
288
322
 
289
323
  // Set defaults
@@ -310,14 +344,36 @@ export default class RaftConnector {
310
344
  }
311
345
  }
312
346
 
347
+ // configure file handler
348
+ this.configureFileHandler(this._raftChannel.fhFileBlockSize(), this._raftChannel.fhBatchAckSize());
349
+
350
+ // Sync time to device if enabled (default: true)
351
+ const syncTime = this._systemType?.connectorOptions?.syncTimeOnConnect ?? true;
352
+ if (syncTime) {
353
+ try {
354
+ const now = new Date();
355
+ const utc = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
356
+ const params: Record<string, string> = { UTC: utc };
357
+ const posixTZ = getHostPosixTZ();
358
+ if (posixTZ) {
359
+ params.tz = posixTZ;
360
+ }
361
+ await this.sendRICRESTMsg('datetime', params);
362
+ RaftLog.info(`connect synced time to device: ${utc}${posixTZ ? ` tz=${posixTZ}` : ''}`);
363
+ } catch (error) {
364
+ RaftLog.warn(`connect time sync failed: ${error}`);
365
+ }
366
+ }
367
+
313
368
  // Send connected event
314
369
  this.onConnEvent(RaftConnEvent.CONN_CONNECTED);
315
370
 
316
371
  } else {
317
372
  // Failed Event
318
373
  this.onConnEvent(RaftConnEvent.CONN_CONNECTION_FAILED);
319
- // configure file handler
320
- this.configureFileHandler(this._raftChannel.fhFileBlockSize(), this._raftChannel.fhBatchAckSize());
374
+
375
+ // Clear system type
376
+ this._systemType = null;
321
377
  }
322
378
 
323
379
  return connOk;
@@ -327,8 +383,12 @@ export default class RaftConnector {
327
383
  // Disconnect
328
384
  this._retryIfLostIsConnected = false;
329
385
  if (this._raftChannel) {
386
+ // Store reference to channel before async operations to avoid race condition
387
+ const channelToDisconnect = this._raftChannel;
388
+ this._raftChannel = null;
389
+
330
390
  // Check if there is a RICREST command to send before disconnecting
331
- const ricRestCommand = this._raftChannel.ricRestCmdBeforeDisconnect();
391
+ const ricRestCommand = channelToDisconnect.ricRestCmdBeforeDisconnect();
332
392
  if (ricRestCommand) {
333
393
  console.log(`sending RICREST command before disconnect: ${ricRestCommand}`);
334
394
  await this.sendRICRESTMsg(ricRestCommand, {});
@@ -336,8 +396,24 @@ export default class RaftConnector {
336
396
  // Pause a little before disconnecting
337
397
  await new Promise(resolve => setTimeout(resolve, 1000));
338
398
  // await this.sendRICRESTMsg("bledisc", {});
339
- await this._raftChannel.disconnect();
340
- this._raftChannel = null;
399
+ await channelToDisconnect.disconnect();
400
+ }
401
+ }
402
+
403
+ disconnectForPageUnload(): void {
404
+ this._retryIfLostIsConnected = false;
405
+
406
+ if (!this._raftChannel) {
407
+ return;
408
+ }
409
+
410
+ const channelToDisconnect = this._raftChannel;
411
+ this._raftChannel = null;
412
+
413
+ try {
414
+ void channelToDisconnect.disconnect();
415
+ } catch (error) {
416
+ RaftLog.warn(`RaftConnector.disconnectForPageUnload failed ${error}`);
341
417
  }
342
418
  }
343
419
 
@@ -352,6 +428,11 @@ export default class RaftConnector {
352
428
  *
353
429
  */
354
430
  async sendRICRESTMsg(commandName: string, params: object,
431
+ bridgeID: number | undefined = undefined): Promise<RaftOKFail> {
432
+ return this._sendRICRESTMsg(commandName, params, bridgeID);
433
+ }
434
+
435
+ private async _sendRICRESTMsg(commandName: string, params: object,
355
436
  bridgeID: number | undefined = undefined): Promise<RaftOKFail> {
356
437
  try {
357
438
  // Format the paramList as query string
@@ -359,11 +440,12 @@ export default class RaftConnector {
359
440
  let paramQueryStr = '';
360
441
  for (const param of paramEntries) {
361
442
  if (paramQueryStr.length > 0) paramQueryStr += '&';
362
- paramQueryStr += param[0] + '=' + param[1];
443
+ paramQueryStr += `${encodeURIComponent(param[0])}=${encodeURIComponent(String(param[1]))}`;
363
444
  }
364
445
  // Format the url to send
365
446
  if (paramQueryStr.length > 0) commandName += '?' + paramQueryStr;
366
- return await this._raftMsgHandler.sendRICRESTURL<RaftOKFail>(commandName, bridgeID);
447
+ const response = await this._raftMsgHandler.sendRICRESTURL<RaftOKFail | null>(commandName, bridgeID);
448
+ return response ?? { rslt: 'fail' };
367
449
  } catch (error) {
368
450
  RaftLog.warn(`sendRICRESTMsg failed ${error}`);
369
451
  return { rslt: 'fail' };
@@ -417,6 +499,13 @@ export default class RaftConnector {
417
499
  fileBlockData: Uint8Array
418
500
  ): void {
419
501
  // RaftLog.info(`onRxFileBlock filePos ${filePos} fileBlockData ${RaftUtils.bufferToHex(fileBlockData)}`);
502
+ const streamID = (filePos >>> 24) & 0xff;
503
+ const streamFilePos = filePos & 0x00ffffff;
504
+ const streamCallback = this._rtStreamCallbacks.get(streamID);
505
+ if (streamID !== 0 && streamCallback) {
506
+ streamCallback(fileBlockData, streamFilePos, streamID);
507
+ return;
508
+ }
420
509
  this._raftFileHandler.onFileBlock(filePos, fileBlockData);
421
510
  }
422
511
 
@@ -456,6 +545,104 @@ export default class RaftConnector {
456
545
 
457
546
  // Mark: Streaming --------------------------------------------------------------------------------
458
547
 
548
+ /**
549
+ * streamData - stream arbitrary data to a named firmware endpoint using the RT_STREAM protocol.
550
+ * @param streamContents data to stream
551
+ * @param fileName logical filename sent in ufStart (e.g. "pattern.thr")
552
+ * @param targetEndpoint REST API endpoint name on the firmware (e.g. "streampattern")
553
+ * @param progressCallback optional (sent, total, progress) callback
554
+ * @returns Promise<boolean> true on success
555
+ */
556
+ async streamData(
557
+ streamContents: Uint8Array,
558
+ fileName: string,
559
+ targetEndpoint: string,
560
+ progressCallback?: RaftStreamDataProgressCBType,
561
+ ): Promise<boolean> {
562
+ if (this._raftStreamHandler && this.isConnected()) {
563
+ return this._raftStreamHandler.streamData(
564
+ streamContents, fileName, targetEndpoint, progressCallback,
565
+ );
566
+ }
567
+ return false;
568
+ }
569
+
570
+ /**
571
+ * openRtStream - open an indefinite bidirectional RT stream.
572
+ * The returned handle can send byte blocks and closes with ufEnd.
573
+ */
574
+ async openRtStream(options: RaftRtStreamOptions): Promise<RaftRtStreamHandle> {
575
+ const cmdMsg = JSON.stringify({
576
+ cmdName: "ufStart",
577
+ reqStr: "ufStart",
578
+ fileType: "rtstream",
579
+ fileName: options.fileName,
580
+ endpoint: options.endpoint,
581
+ fileLen: 0,
582
+ });
583
+
584
+ const startResp = await this._raftMsgHandler.sendRICRESTCmdFrame<RaftRtStreamStartResp>(cmdMsg);
585
+ if (!startResp || startResp.rslt !== "ok" || startResp.streamID === undefined) {
586
+ throw new Error(`openRtStream failed ${startResp?.rslt ?? "no response"}`);
587
+ }
588
+
589
+ const streamID = startResp.streamID;
590
+ const maxBlockSize = startResp.maxBlockSize || this._raftStreamHandler.maxBlockSize;
591
+ let txFilePos = 0;
592
+ let sendQueue = Promise.resolve();
593
+ this._rtStreamCallbacks.set(streamID, options.onData);
594
+ this._fallbackRtStreamCallback = { streamID, callback: options.onData };
595
+
596
+ const sendBytes = async (bytes: Uint8Array): Promise<boolean> => {
597
+ let sentOk = false;
598
+ sendQueue = sendQueue
599
+ .catch(() => {
600
+ // Keep later terminal input flowing even if an earlier block failed.
601
+ })
602
+ .then(async () => {
603
+ sentOk = await this._raftMsgHandler.sendStreamBlock(bytes, txFilePos, streamID);
604
+ if (sentOk) {
605
+ txFilePos = (txFilePos + bytes.length) & 0x00ffffff;
606
+ }
607
+ });
608
+ await sendQueue;
609
+ return sentOk;
610
+ };
611
+
612
+ const close = async (): Promise<boolean> => {
613
+ this._rtStreamCallbacks.delete(streamID);
614
+ if (this._fallbackRtStreamCallback?.streamID === streamID) {
615
+ this._fallbackRtStreamCallback = null;
616
+ }
617
+ const endMsg = JSON.stringify({
618
+ cmdName: "ufEnd",
619
+ reqStr: "ufEnd",
620
+ streamID,
621
+ });
622
+ try {
623
+ const endResp = await this._raftMsgHandler.sendRICRESTCmdFrame<RaftOKFail>(endMsg);
624
+ return endResp?.rslt === "ok";
625
+ } catch (error) {
626
+ RaftLog.warn(`closeRtStream failed ${streamID}: ${error}`);
627
+ return false;
628
+ }
629
+ };
630
+
631
+ // Some endpoints use an initial empty block as an attach signal.
632
+ // Keep this opt-in because older endpoints reject zero-length ufBlock frames.
633
+ if (options.sendInitialEmptyBlock) {
634
+ await sendBytes(new Uint8Array());
635
+ }
636
+
637
+ return {
638
+ streamID,
639
+ maxBlockSize,
640
+ sendBytes,
641
+ sendText: (text: string) => sendBytes(new TextEncoder().encode(text)),
642
+ close,
643
+ };
644
+ }
645
+
459
646
  /**
460
647
  * streamAudio - stream audio
461
648
  * @param streamContents audio data
@@ -534,7 +721,7 @@ export default class RaftConnector {
534
721
  */
535
722
  async checkConnPerformance(): Promise<number | undefined> {
536
723
 
537
- // Sends a magic sequence of bytes followed by blocks of random data
724
+ // Sends a magic sequence of bytes followed by blocks of random data
538
725
  // these will be ignored by the Raft library (as it recognises magic sequence)
539
726
  // and is used performance evaluation
540
727
  let prbsState = 1;
@@ -681,8 +868,8 @@ export default class RaftConnector {
681
868
 
682
869
  /**
683
870
  * onUpdateEvent - handle update event
684
- * @param eventEnum
685
- * @param data
871
+ * @param eventEnum
872
+ * @param data
686
873
  */
687
874
  _onUpdateEvent(eventEnum: RaftUpdateEvent, data: object | string | null | undefined = undefined): void {
688
875
  // Notify
@@ -8,6 +8,7 @@
8
8
  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
9
 
10
10
  import { CustomFunctionDefinition, DeviceTypePollRespMetadata } from "./RaftDeviceInfo";
11
+ import { transpilePseudocodeToJs } from "./PseudocodeTranspiler";
11
12
 
12
13
  type CustomAttrJsFn = (
13
14
  buf: Uint8Array,
@@ -22,8 +23,8 @@ type CustomAttrJsFn = (
22
23
  export default class CustomAttrHandler {
23
24
 
24
25
  private _jsFunctionCache = new Map<string, CustomAttrJsFn>();
25
-
26
- public handleAttr(pollRespMetadata: DeviceTypePollRespMetadata, msgBuffer: Uint8Array, msgBufIdx: number): number[][] {
26
+
27
+ public handleAttr(pollRespMetadata: DeviceTypePollRespMetadata, msgBuffer: Uint8Array, msgBufIdx: number, msgEndIdx = msgBuffer.length): number[][] {
27
28
 
28
29
  // Number of bytes in each message
29
30
  const numMsgBytes = pollRespMetadata.b;
@@ -45,59 +46,48 @@ export default class CustomAttrHandler {
45
46
  return attrValueVecs;
46
47
  }
47
48
 
48
- // Provide the message buffer sliced to the data portion
49
- const buf = msgBuffer.slice(msgBufIdx);
50
- if (buf.length < numMsgBytes) {
49
+ // Provide this poll block to the custom handler. Use the smaller of
50
+ // pollRespMetadata.b and the bytes actually available — variable-length
51
+ // samples may be shorter than b, and the last sample in a frame may not
52
+ // have b bytes remaining in the buffer.
53
+ const boundedMsgEndIdx = Math.min(Math.max(msgEndIdx, msgBufIdx), msgBuffer.length);
54
+ const availableBytes = Math.min(numMsgBytes, boundedMsgEndIdx - msgBufIdx);
55
+ if (availableBytes <= 0) {
51
56
  return [];
52
57
  }
58
+ const buf = msgBuffer.slice(msgBufIdx, msgBufIdx + availableBytes);
53
59
 
54
- // Execute supplied JS implementation if provided
55
- if (customFnDef.j && customFnDef.j.trim().length > 0) {
56
- const jsFn = this.getOrCompileJsFunction(customFnDef);
57
- if (!jsFn) {
58
- return attrValueVecs;
59
- }
60
- try {
61
- jsFn(buf, attrValues, attrValueVecs, pollRespMetadata, msgBuffer, msgBufIdx, numMsgBytes);
62
- } catch (err) {
63
- console.error(`CustomAttrHandler JS function ${customFnDef.n} execution failed`, err);
64
- }
60
+ const fn = this.getOrCompileFunction(customFnDef);
61
+ if (!fn) {
65
62
  return attrValueVecs;
66
63
  }
67
64
 
68
- // Custom code for each device type handled natively
69
- if (customFnDef.n === "max30101_fifo") {
70
- // Generated code ...
71
- const N = (buf[0] + 32 - buf[2]) % 32;
72
- let k = 3;
73
- let i = 0;
74
- while (i < N) {
75
- attrValues["Red"].push(0);
76
- attrValues["Red"][attrValues["Red"].length - 1] = (buf[k] << 16) | (buf[k + 1] << 8) | buf[k + 2];
77
- attrValues["IR"].push(0);
78
- attrValues["IR"][attrValues["IR"].length - 1] = (buf[k + 3] << 16) | (buf[k + 4] << 8) | buf[k + 5];
79
- k += 6;
80
- i++;
81
- }
82
- } else if (customFnDef.n === "gravity_o2_calc") {
83
- const key = 20.9 / 120.0;
84
- const val = key * (buf[0] + buf[1] / 10.0 + buf[2] / 100.0);
85
- attrValues["oxygen"].push(val);
65
+ try {
66
+ fn(buf, attrValues, attrValueVecs, pollRespMetadata, msgBuffer, msgBufIdx, numMsgBytes);
67
+ } catch (err) {
68
+ console.error(`CustomAttrHandler function ${customFnDef.n} execution failed`, err);
86
69
  }
87
70
  return attrValueVecs;
88
71
  }
89
72
 
90
- private getOrCompileJsFunction(customFnDef: CustomFunctionDefinition): CustomAttrJsFn | null {
91
- if (!customFnDef.j) {
73
+ private getOrCompileFunction(customFnDef: CustomFunctionDefinition): CustomAttrJsFn | null {
74
+ // Prefer explicit JS if provided, otherwise transpile from pseudocode
75
+ let jsSource = customFnDef.j?.trim();
76
+ if (!jsSource && customFnDef.c) {
77
+ jsSource = transpilePseudocodeToJs(customFnDef.c);
78
+ }
79
+ if (!jsSource) {
92
80
  return null;
93
81
  }
94
- const cacheKey = `${customFnDef.n}::${customFnDef.j}`;
95
- const cachedFn = this._jsFunctionCache.get(cacheKey);
96
- if (cachedFn) {
97
- return cachedFn;
82
+
83
+ const cacheKey = `${customFnDef.n}::${jsSource}`;
84
+ const cached = this._jsFunctionCache.get(cacheKey);
85
+ if (cached) {
86
+ return cached;
98
87
  }
88
+
99
89
  try {
100
- const compiledFn = new Function(
90
+ const fn = new Function(
101
91
  "buf",
102
92
  "attrValues",
103
93
  "attrValueVecs",
@@ -105,12 +95,12 @@ export default class CustomAttrHandler {
105
95
  "msgBuffer",
106
96
  "msgBufIdx",
107
97
  "numMsgBytes",
108
- customFnDef.j
98
+ jsSource
109
99
  ) as CustomAttrJsFn;
110
- this._jsFunctionCache.set(cacheKey, compiledFn);
111
- return compiledFn;
100
+ this._jsFunctionCache.set(cacheKey, fn);
101
+ return fn;
112
102
  } catch (err) {
113
- console.error(`CustomAttrHandler failed to compile JS function ${customFnDef.n}`, err);
103
+ console.error(`CustomAttrHandler failed to compile function ${customFnDef.n}`, err);
114
104
  return null;
115
105
  }
116
106
  }
@@ -89,6 +89,31 @@ export interface DeviceTypePollRespMetadata {
89
89
  us?: number; // Time between consecutive samples in microseconds
90
90
  }
91
91
 
92
+ export interface SampleRateResult {
93
+ ok: boolean;
94
+ requestedRateHz: number;
95
+ actualRateHz: number; // Closest supported rate from _conf.rate map
96
+ intervalUs: number; // Polling interval set
97
+ numSamples: number; // Buffer depth set
98
+ error?: string;
99
+ }
100
+
101
+ export interface ActionMapEntry {
102
+ w: string; // Hex bytes to write (may contain &-separated multi-writes)
103
+ i?: number; // Recommended polling interval in microseconds
104
+ s?: number; // Recommended number of poll result samples (buffer depth)
105
+ }
106
+
107
+ export type ActionMapValue = string | ActionMapEntry;
108
+
109
+ export function getActionMapHex(entry: ActionMapValue): string {
110
+ return typeof entry === 'string' ? entry : entry.w;
111
+ }
112
+
113
+ export function getActionMapEntry(entry: ActionMapValue): ActionMapEntry | null {
114
+ return typeof entry === 'object' ? entry : null;
115
+ }
116
+
92
117
  export interface DeviceTypeAction {
93
118
  n: string; // Action name
94
119
  t?: string; // Action type using python struct module format (e.g. 'H' for unsigned short, 'h' for signed short, 'f' for float etc.)
@@ -102,6 +127,8 @@ export interface DeviceTypeAction {
102
127
  mul?: number; // Multiplier to apply
103
128
  sub?: number; // Value to subtract before multiplying
104
129
  d?: number; // Default value
130
+ map?: Record<string, ActionMapValue>; // Discrete value mapping (display value -> hex string or {w, i, s} object)
131
+ desc?: string; // Human-readable description
105
132
  }
106
133
 
107
134
  export interface DeviceTypeInfo {