@robotical/raftjs 2.1.0 → 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 (158) 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 +11 -1
  18. package/dist/react-native/RaftConnector.js +75 -9
  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 +0 -1
  34. package/dist/react-native/RaftFileHandler.js +61 -23
  35. package/dist/react-native/RaftFileHandler.js.map +1 -1
  36. package/dist/react-native/RaftPublish.d.ts +2 -0
  37. package/dist/react-native/RaftPublish.js +81 -0
  38. package/dist/react-native/RaftPublish.js.map +1 -0
  39. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  40. package/dist/react-native/RaftStreamHandler.js +66 -0
  41. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  42. package/dist/react-native/RaftStruct.d.ts +2 -2
  43. package/dist/react-native/RaftStruct.js +97 -26
  44. package/dist/react-native/RaftStruct.js.map +1 -1
  45. package/dist/react-native/RaftSystemType.d.ts +1 -0
  46. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  47. package/dist/react-native/RaftSystemUtils.js +51 -0
  48. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  49. package/dist/react-native/RaftTimezone.d.ts +16 -0
  50. package/dist/react-native/RaftTimezone.js +153 -0
  51. package/dist/react-native/RaftTimezone.js.map +1 -0
  52. package/dist/react-native/RaftTypes.d.ts +27 -1
  53. package/dist/react-native/RaftTypes.js.map +1 -1
  54. package/dist/react-native/RaftUpdateManager.js +1 -1
  55. package/dist/react-native/RaftUpdateManager.js.map +1 -1
  56. package/dist/react-native/main.d.ts +2 -0
  57. package/dist/react-native/main.js +6 -1
  58. package/dist/react-native/main.js.map +1 -1
  59. package/dist/web/PseudocodeTranspiler.d.ts +6 -0
  60. package/dist/web/PseudocodeTranspiler.js +115 -0
  61. package/dist/web/PseudocodeTranspiler.js.map +1 -0
  62. package/dist/web/RaftAttributeHandler.d.ts +1 -1
  63. package/dist/web/RaftAttributeHandler.js +108 -32
  64. package/dist/web/RaftAttributeHandler.js.map +1 -1
  65. package/dist/web/RaftChannelBLE.web.d.ts +4 -0
  66. package/dist/web/RaftChannelBLE.web.js +59 -21
  67. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  68. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  69. package/dist/web/RaftChannelSimulated.js +9 -5
  70. package/dist/web/RaftChannelSimulated.js.map +1 -1
  71. package/dist/web/RaftChannelWebSocket.js +16 -1
  72. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  73. package/dist/web/RaftConnector.d.ts +11 -1
  74. package/dist/web/RaftConnector.js +75 -9
  75. package/dist/web/RaftConnector.js.map +1 -1
  76. package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
  77. package/dist/web/RaftCustomAttrHandler.js +32 -44
  78. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  79. package/dist/web/RaftDeviceInfo.d.ts +18 -0
  80. package/dist/web/RaftDeviceInfo.js +8 -0
  81. package/dist/web/RaftDeviceInfo.js.map +1 -1
  82. package/dist/web/RaftDeviceManager.d.ts +30 -3
  83. package/dist/web/RaftDeviceManager.js +618 -107
  84. package/dist/web/RaftDeviceManager.js.map +1 -1
  85. package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
  86. package/dist/web/RaftDeviceStates.d.ts +27 -3
  87. package/dist/web/RaftDeviceStates.js +31 -6
  88. package/dist/web/RaftDeviceStates.js.map +1 -1
  89. package/dist/web/RaftFileHandler.d.ts +0 -1
  90. package/dist/web/RaftFileHandler.js +61 -23
  91. package/dist/web/RaftFileHandler.js.map +1 -1
  92. package/dist/web/RaftPublish.d.ts +2 -0
  93. package/dist/web/RaftPublish.js +81 -0
  94. package/dist/web/RaftPublish.js.map +1 -0
  95. package/dist/web/RaftStreamHandler.d.ts +11 -0
  96. package/dist/web/RaftStreamHandler.js +66 -0
  97. package/dist/web/RaftStreamHandler.js.map +1 -1
  98. package/dist/web/RaftStruct.d.ts +2 -2
  99. package/dist/web/RaftStruct.js +97 -26
  100. package/dist/web/RaftStruct.js.map +1 -1
  101. package/dist/web/RaftSystemType.d.ts +1 -0
  102. package/dist/web/RaftSystemUtils.d.ts +17 -1
  103. package/dist/web/RaftSystemUtils.js +51 -0
  104. package/dist/web/RaftSystemUtils.js.map +1 -1
  105. package/dist/web/RaftTimezone.d.ts +16 -0
  106. package/dist/web/RaftTimezone.js +153 -0
  107. package/dist/web/RaftTimezone.js.map +1 -0
  108. package/dist/web/RaftTypes.d.ts +27 -1
  109. package/dist/web/RaftTypes.js.map +1 -1
  110. package/dist/web/RaftUpdateManager.js +1 -1
  111. package/dist/web/RaftUpdateManager.js.map +1 -1
  112. package/dist/web/main.d.ts +2 -0
  113. package/dist/web/main.js +6 -1
  114. package/dist/web/main.js.map +1 -1
  115. package/examples/dashboard/package.json +2 -2
  116. package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
  117. package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
  118. package/examples/dashboard/src/DevicePanel.tsx +92 -11
  119. package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
  120. package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
  121. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  122. package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
  123. package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
  124. package/examples/dashboard/src/LoggingPanel.tsx +264 -0
  125. package/examples/dashboard/src/Main.tsx +12 -2
  126. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  127. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
  128. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  129. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  130. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
  131. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
  132. package/examples/dashboard/src/styles.css +766 -1
  133. package/notes/web-ble-reconnect-retry.md +69 -0
  134. package/package.json +10 -7
  135. package/src/PseudocodeTranspiler.test.ts +372 -0
  136. package/src/PseudocodeTranspiler.ts +127 -0
  137. package/src/RaftAttributeHandler.ts +152 -76
  138. package/src/RaftChannelBLE.web.ts +62 -20
  139. package/src/RaftChannelSimulated.ts +10 -5
  140. package/src/RaftChannelWebSocket.ts +16 -2
  141. package/src/RaftConnector.ts +93 -15
  142. package/src/RaftCustomAttrHandler.ts +35 -45
  143. package/src/RaftDeviceInfo.ts +27 -0
  144. package/src/RaftDeviceManager.test.ts +164 -0
  145. package/src/RaftDeviceManager.ts +705 -127
  146. package/src/RaftDeviceMgrIF.ts +13 -2
  147. package/src/RaftDeviceStates.ts +49 -8
  148. package/src/RaftFileHandler.ts +69 -28
  149. package/src/RaftPublish.ts +92 -0
  150. package/src/RaftStreamHandler.ts +84 -1
  151. package/src/RaftStruct.test.ts +229 -0
  152. package/src/RaftStruct.ts +101 -37
  153. package/src/RaftSystemType.ts +1 -0
  154. package/src/RaftSystemUtils.ts +59 -0
  155. package/src/RaftTimezone.ts +151 -0
  156. package/src/RaftTypes.ts +34 -1
  157. package/src/RaftUpdateManager.ts +1 -1
  158. package/src/main.ts +2 -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 } 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
 
@@ -214,7 +213,22 @@ export default class RaftConnector {
214
213
  * Initialize the Raft channel
215
214
  */
216
215
  async initializeChannel(method: string): Promise<boolean> {
216
+ // Disconnect any existing channel first to avoid stale connections
217
+ RaftLog.verbose(`[RaftConnector.initializeChannel] START method=${method} hasExistingChannel=${!!this._raftChannel}`);
218
+ if (this._raftChannel) {
219
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Disconnecting existing channel...`);
220
+ try {
221
+ await this._raftChannel.disconnect();
222
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Existing channel disconnected successfully`);
223
+ } catch (e) {
224
+ RaftLog.warn(`[RaftConnector.initializeChannel] Error disconnecting existing channel: ${e}`);
225
+ }
226
+ this._raftChannel = null;
227
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Existing channel nulled`);
228
+ }
229
+
217
230
  // Initialize raft channel
231
+ RaftLog.verbose(`[RaftConnector.initializeChannel] Creating new channel...`);
218
232
  if (method === 'WebBLE' || method === 'PhoneBLE') {
219
233
  const RaftChannelBLE = createBLEChannel();
220
234
  this._raftChannel = new RaftChannelBLE();
@@ -226,7 +240,7 @@ export default class RaftConnector {
226
240
  this._raftChannel = new RaftChannelWebSerial();
227
241
  this._channelConnMethod = 'WebSerial';
228
242
  } else if (method === 'Simulated') {
229
- this._raftChannel = new RaftChannelSimulated();
243
+ this._raftChannel = new RaftChannelSimulated();
230
244
  this._channelConnMethod = 'Simulated';
231
245
  } else {
232
246
  RaftLog.warn('Unknown method: ' + method);
@@ -268,7 +282,7 @@ export default class RaftConnector {
268
282
  // Store locator
269
283
  this._channelConnLocator = locator;
270
284
 
271
- // Connect
285
+ // Connect channel first (system type resolution needs a live connection)
272
286
  let connOk = false;
273
287
  try {
274
288
  // Event
@@ -281,9 +295,9 @@ export default class RaftConnector {
281
295
  }
282
296
 
283
297
  if (connOk) {
284
- // Get system type
298
+
299
+ // Resolve system type now that the channel is connected
285
300
  if (this._getSystemTypeCB) {
286
- // Get system type
287
301
  this._systemType = await this._getSystemTypeCB(this._raftSystemUtils);
288
302
 
289
303
  // Set defaults
@@ -310,14 +324,36 @@ export default class RaftConnector {
310
324
  }
311
325
  }
312
326
 
327
+ // configure file handler
328
+ this.configureFileHandler(this._raftChannel.fhFileBlockSize(), this._raftChannel.fhBatchAckSize());
329
+
330
+ // Sync time to device if enabled (default: true)
331
+ const syncTime = this._systemType?.connectorOptions?.syncTimeOnConnect ?? true;
332
+ if (syncTime) {
333
+ try {
334
+ const now = new Date();
335
+ const utc = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
336
+ const params: Record<string, string> = { UTC: utc };
337
+ const posixTZ = getHostPosixTZ();
338
+ if (posixTZ) {
339
+ params.tz = posixTZ;
340
+ }
341
+ await this.sendRICRESTMsg('datetime', params);
342
+ RaftLog.info(`connect synced time to device: ${utc}${posixTZ ? ` tz=${posixTZ}` : ''}`);
343
+ } catch (error) {
344
+ RaftLog.warn(`connect time sync failed: ${error}`);
345
+ }
346
+ }
347
+
313
348
  // Send connected event
314
349
  this.onConnEvent(RaftConnEvent.CONN_CONNECTED);
315
350
 
316
351
  } else {
317
352
  // Failed Event
318
353
  this.onConnEvent(RaftConnEvent.CONN_CONNECTION_FAILED);
319
- // configure file handler
320
- this.configureFileHandler(this._raftChannel.fhFileBlockSize(), this._raftChannel.fhBatchAckSize());
354
+
355
+ // Clear system type
356
+ this._systemType = null;
321
357
  }
322
358
 
323
359
  return connOk;
@@ -327,8 +363,12 @@ export default class RaftConnector {
327
363
  // Disconnect
328
364
  this._retryIfLostIsConnected = false;
329
365
  if (this._raftChannel) {
366
+ // Store reference to channel before async operations to avoid race condition
367
+ const channelToDisconnect = this._raftChannel;
368
+ this._raftChannel = null;
369
+
330
370
  // Check if there is a RICREST command to send before disconnecting
331
- const ricRestCommand = this._raftChannel.ricRestCmdBeforeDisconnect();
371
+ const ricRestCommand = channelToDisconnect.ricRestCmdBeforeDisconnect();
332
372
  if (ricRestCommand) {
333
373
  console.log(`sending RICREST command before disconnect: ${ricRestCommand}`);
334
374
  await this.sendRICRESTMsg(ricRestCommand, {});
@@ -336,8 +376,24 @@ export default class RaftConnector {
336
376
  // Pause a little before disconnecting
337
377
  await new Promise(resolve => setTimeout(resolve, 1000));
338
378
  // await this.sendRICRESTMsg("bledisc", {});
339
- await this._raftChannel.disconnect();
340
- this._raftChannel = null;
379
+ await channelToDisconnect.disconnect();
380
+ }
381
+ }
382
+
383
+ disconnectForPageUnload(): void {
384
+ this._retryIfLostIsConnected = false;
385
+
386
+ if (!this._raftChannel) {
387
+ return;
388
+ }
389
+
390
+ const channelToDisconnect = this._raftChannel;
391
+ this._raftChannel = null;
392
+
393
+ try {
394
+ void channelToDisconnect.disconnect();
395
+ } catch (error) {
396
+ RaftLog.warn(`RaftConnector.disconnectForPageUnload failed ${error}`);
341
397
  }
342
398
  }
343
399
 
@@ -456,6 +512,28 @@ export default class RaftConnector {
456
512
 
457
513
  // Mark: Streaming --------------------------------------------------------------------------------
458
514
 
515
+ /**
516
+ * streamData - stream arbitrary data to a named firmware endpoint using the RT_STREAM protocol.
517
+ * @param streamContents data to stream
518
+ * @param fileName logical filename sent in ufStart (e.g. "pattern.thr")
519
+ * @param targetEndpoint REST API endpoint name on the firmware (e.g. "streampattern")
520
+ * @param progressCallback optional (sent, total, progress) callback
521
+ * @returns Promise<boolean> true on success
522
+ */
523
+ async streamData(
524
+ streamContents: Uint8Array,
525
+ fileName: string,
526
+ targetEndpoint: string,
527
+ progressCallback?: RaftStreamDataProgressCBType,
528
+ ): Promise<boolean> {
529
+ if (this._raftStreamHandler && this.isConnected()) {
530
+ return this._raftStreamHandler.streamData(
531
+ streamContents, fileName, targetEndpoint, progressCallback,
532
+ );
533
+ }
534
+ return false;
535
+ }
536
+
459
537
  /**
460
538
  * streamAudio - stream audio
461
539
  * @param streamContents audio data
@@ -534,7 +612,7 @@ export default class RaftConnector {
534
612
  */
535
613
  async checkConnPerformance(): Promise<number | undefined> {
536
614
 
537
- // Sends a magic sequence of bytes followed by blocks of random data
615
+ // Sends a magic sequence of bytes followed by blocks of random data
538
616
  // these will be ignored by the Raft library (as it recognises magic sequence)
539
617
  // and is used performance evaluation
540
618
  let prbsState = 1;
@@ -681,8 +759,8 @@ export default class RaftConnector {
681
759
 
682
760
  /**
683
761
  * onUpdateEvent - handle update event
684
- * @param eventEnum
685
- * @param data
762
+ * @param eventEnum
763
+ * @param data
686
764
  */
687
765
  _onUpdateEvent(eventEnum: RaftUpdateEvent, data: object | string | null | undefined = undefined): void {
688
766
  // 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 {
@@ -0,0 +1,164 @@
1
+ import { DeviceManager } from "./RaftDeviceManager";
2
+ import { DeviceTypeInfo } from "./RaftDeviceInfo";
3
+ import RaftSystemUtils from "./RaftSystemUtils";
4
+
5
+ function makeTypeInfo(name: string, respBytes: number, attrs: Array<{ n: string; t: string }>): DeviceTypeInfo {
6
+ return {
7
+ name,
8
+ desc: name,
9
+ manu: "Robotical",
10
+ type: name,
11
+ resp: {
12
+ b: respBytes,
13
+ a: attrs.map(attr => ({ ...attr, u: "", r: [0, 0] }))
14
+ }
15
+ };
16
+ }
17
+
18
+ async function makeDeviceManager(typeInfos: Record<string, DeviceTypeInfo>): Promise<DeviceManager> {
19
+ const msgHandler = {
20
+ sendRICRESTURL: jest.fn(async (cmd: string) => {
21
+ const deviceType = new URLSearchParams(cmd.split("?")[1]).get("type");
22
+ const devinfo = deviceType ? typeInfos[deviceType] : undefined;
23
+ return devinfo ? { rslt: "ok", devinfo } : { rslt: "fail" };
24
+ })
25
+ };
26
+ const systemUtils = {
27
+ getMsgHandler: () => msgHandler,
28
+ getPublishTopicName: () => "devbin"
29
+ } as unknown as RaftSystemUtils;
30
+
31
+ const deviceManager = new DeviceManager();
32
+ await deviceManager.setup(systemUtils);
33
+ return deviceManager;
34
+ }
35
+
36
+ describe("DeviceManager binary devbin parsing", () => {
37
+ const accelInfo = makeTypeInfo("MXC400xXC", 7, [
38
+ { n: "x", t: ">h" },
39
+ { n: "y", t: ">h" },
40
+ { n: "z", t: ">h" },
41
+ { n: "status", t: "B" }
42
+ ]);
43
+
44
+ it("decodes current length-prefixed records", async () => {
45
+ const deviceManager = await makeDeviceManager({ "4": accelInfo });
46
+ const rxMsg = Uint8Array.from([
47
+ 0x00, 0x80,
48
+ 0xDB, 0xFF, 0x00,
49
+ 0x00, 0x12,
50
+ 0x81,
51
+ 0x00, 0x00, 0x00, 0x15,
52
+ 0x00, 0x04,
53
+ 0x05,
54
+ 0x09,
55
+ 0x00, 0x01,
56
+ 0x00, 0x01,
57
+ 0x00, 0x02,
58
+ 0x00, 0x03,
59
+ 0x04
60
+ ]);
61
+
62
+ await deviceManager.handleClientMsgBinary(rxMsg);
63
+
64
+ const deviceState = deviceManager.getDeviceState("1_15");
65
+ expect(deviceState.deviceType).toBe("MXC400xXC");
66
+ expect(deviceState.deviceTimeline.totalSamplesAdded).toBe(1);
67
+ expect(deviceState.deviceAttributes.x.values).toEqual([1]);
68
+ expect(deviceState.deviceAttributes.y.values).toEqual([2]);
69
+ expect(deviceState.deviceAttributes.z.values).toEqual([3]);
70
+ expect(deviceState.deviceAttributes.status.values).toEqual([4]);
71
+ });
72
+
73
+ it("decodes Cog v1.9.5 legacy raw accelerometer records", async () => {
74
+ const deviceManager = await makeDeviceManager({ "4": accelInfo });
75
+ const rxMsg = Uint8Array.from([
76
+ 0x00, 0x80,
77
+ 0x00, 0x10,
78
+ 0x81,
79
+ 0x00, 0x00, 0x00, 0x15,
80
+ 0x00, 0x04,
81
+ 0x00, 0x01,
82
+ 0x00, 0x01,
83
+ 0x00, 0x02,
84
+ 0x00, 0x03,
85
+ 0x04
86
+ ]);
87
+
88
+ await deviceManager.handleClientMsgBinary(rxMsg);
89
+
90
+ const deviceState = deviceManager.getDeviceState("1_15");
91
+ expect(deviceState.deviceType).toBe("MXC400xXC");
92
+ expect(deviceState.deviceTimeline.totalSamplesAdded).toBe(1);
93
+ expect(deviceState.deviceAttributes.x.values).toEqual([1]);
94
+ expect(deviceState.deviceAttributes.y.values).toEqual([2]);
95
+ expect(deviceState.deviceAttributes.z.values).toEqual([3]);
96
+ expect(deviceState.deviceAttributes.status.values).toEqual([4]);
97
+ });
98
+
99
+ it("decodes Cog v1.9.5 legacy raw records inside a devbin envelope", async () => {
100
+ const deviceManager = await makeDeviceManager({ "4": accelInfo });
101
+ const rxMsg = Uint8Array.from([
102
+ 0x00, 0x80,
103
+ 0xDB, 0xFF, 0x00,
104
+ 0x00, 0x10,
105
+ 0x81,
106
+ 0x00, 0x00, 0x00, 0x15,
107
+ 0x00, 0x04,
108
+ 0x00, 0x01,
109
+ 0x00, 0x01,
110
+ 0x00, 0x02,
111
+ 0x00, 0x03,
112
+ 0x04
113
+ ]);
114
+
115
+ await deviceManager.handleClientMsgBinary(rxMsg);
116
+
117
+ const deviceState = deviceManager.getDeviceState("1_15");
118
+ expect(deviceState.deviceType).toBe("MXC400xXC");
119
+ expect(deviceState.deviceTimeline.totalSamplesAdded).toBe(1);
120
+ expect(deviceState.deviceAttributes.x.values).toEqual([1]);
121
+ expect(deviceState.deviceAttributes.y.values).toEqual([2]);
122
+ expect(deviceState.deviceAttributes.z.values).toEqual([3]);
123
+ expect(deviceState.deviceAttributes.status.values).toEqual([4]);
124
+ });
125
+
126
+ it("keeps Cog v1.9.5 direct device records distinct when bus and address are both zero", async () => {
127
+ const lightInfo = makeTypeInfo("LightSensors", 16, [
128
+ { n: "ch0", t: ">H" },
129
+ { n: "ch1", t: ">H" },
130
+ { n: "ch2", t: ">H" },
131
+ { n: "ch3", t: ">H" }
132
+ ]);
133
+ const powerInfo = makeTypeInfo("Power", 1, [
134
+ { n: "battery", t: "B" }
135
+ ]);
136
+ const deviceManager = await makeDeviceManager({ "2": lightInfo, "3": powerInfo });
137
+ const rxMsg = Uint8Array.from([
138
+ 0x00, 0x80,
139
+ 0x00, 0x11,
140
+ 0x80,
141
+ 0x00, 0x00, 0x00, 0x00,
142
+ 0x00, 0x02,
143
+ 0x00, 0x01,
144
+ 0x00, 0x0a,
145
+ 0x00, 0x0b,
146
+ 0x00, 0x0c,
147
+ 0x00, 0x0d,
148
+ 0x00, 0x0a,
149
+ 0x80,
150
+ 0x00, 0x00, 0x00, 0x00,
151
+ 0x00, 0x03,
152
+ 0x00, 0x02,
153
+ 0x63
154
+ ]);
155
+
156
+ await deviceManager.handleClientMsgBinary(rxMsg);
157
+
158
+ const devicesState = deviceManager.getDevicesState();
159
+ expect(devicesState["0_0_2"].deviceType).toBe("LightSensors");
160
+ expect(devicesState["0_0_2"].deviceAttributes.ch0.values).toEqual([10]);
161
+ expect(devicesState["0_0_3"].deviceType).toBe("Power");
162
+ expect(devicesState["0_0_3"].deviceAttributes.battery.values).toEqual([99]);
163
+ });
164
+ });