@robotical/raftjs 2.1.2 → 2.1.4

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 (70) hide show
  1. package/devdocs/decode-overrun-investigation.md +167 -0
  2. package/devdocs/message-panel-design.md +320 -0
  3. package/dist/react-native/RaftAttributeHandler.d.ts +10 -1
  4. package/dist/react-native/RaftAttributeHandler.js +60 -8
  5. package/dist/react-native/RaftAttributeHandler.js.map +1 -1
  6. package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
  7. package/dist/react-native/RaftChannelSimulated.js +67 -0
  8. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  9. package/dist/react-native/RaftConnector.d.ts +19 -1
  10. package/dist/react-native/RaftConnector.js +102 -2
  11. package/dist/react-native/RaftConnector.js.map +1 -1
  12. package/dist/react-native/RaftDeviceInfo.d.ts +5 -1
  13. package/dist/react-native/RaftDeviceManager.js +75 -32
  14. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  15. package/dist/react-native/RaftFileHandler.d.ts +1 -0
  16. package/dist/react-native/RaftFileHandler.js +40 -11
  17. package/dist/react-native/RaftFileHandler.js.map +1 -1
  18. package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
  19. package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
  20. package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
  21. package/dist/react-native/RaftMsgHandler.d.ts +1 -1
  22. package/dist/react-native/RaftMsgHandler.js +6 -3
  23. package/dist/react-native/RaftMsgHandler.js.map +1 -1
  24. package/dist/react-native/RaftTypes.d.ts +19 -0
  25. package/dist/react-native/RaftTypes.js.map +1 -1
  26. package/dist/react-native/main.d.ts +1 -0
  27. package/dist/react-native/main.js +3 -1
  28. package/dist/react-native/main.js.map +1 -1
  29. package/dist/web/RaftAttributeHandler.d.ts +10 -1
  30. package/dist/web/RaftAttributeHandler.js +60 -8
  31. package/dist/web/RaftAttributeHandler.js.map +1 -1
  32. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  33. package/dist/web/RaftChannelSimulated.js +67 -0
  34. package/dist/web/RaftChannelSimulated.js.map +1 -1
  35. package/dist/web/RaftConnector.d.ts +19 -1
  36. package/dist/web/RaftConnector.js +102 -2
  37. package/dist/web/RaftConnector.js.map +1 -1
  38. package/dist/web/RaftDeviceInfo.d.ts +5 -1
  39. package/dist/web/RaftDeviceManager.js +75 -32
  40. package/dist/web/RaftDeviceManager.js.map +1 -1
  41. package/dist/web/RaftFileHandler.d.ts +1 -0
  42. package/dist/web/RaftFileHandler.js +40 -11
  43. package/dist/web/RaftFileHandler.js.map +1 -1
  44. package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
  45. package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
  46. package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
  47. package/dist/web/RaftMsgHandler.d.ts +1 -1
  48. package/dist/web/RaftMsgHandler.js +6 -3
  49. package/dist/web/RaftMsgHandler.js.map +1 -1
  50. package/dist/web/RaftTypes.d.ts +19 -0
  51. package/dist/web/RaftTypes.js.map +1 -1
  52. package/dist/web/main.d.ts +1 -0
  53. package/dist/web/main.js +3 -1
  54. package/dist/web/main.js.map +1 -1
  55. package/examples/dashboard/src/DeviceActionsForm.tsx +24 -11
  56. package/examples/dashboard/src/DeviceLineChart.tsx +5 -4
  57. package/examples/dashboard/src/DevicePanel.tsx +3 -0
  58. package/package.json +1 -1
  59. package/src/RaftAttributeHandler.ts +89 -9
  60. package/src/RaftChannelSimulated.test.ts +62 -0
  61. package/src/RaftChannelSimulated.ts +91 -0
  62. package/src/RaftConnector.ts +112 -3
  63. package/src/RaftDeviceInfo.ts +5 -1
  64. package/src/RaftDeviceManager.test.ts +35 -1
  65. package/src/RaftDeviceManager.ts +86 -33
  66. package/src/RaftFileHandler.ts +43 -11
  67. package/src/RaftMicroPythonConsoleClient.ts +78 -0
  68. package/src/RaftMsgHandler.ts +8 -4
  69. package/src/RaftTypes.ts +23 -0
  70. package/src/main.ts +1 -0
@@ -586,6 +586,34 @@ export default class RaftChannelSimulated implements RaftChannel {
586
586
 
587
587
  return true;
588
588
  }
589
+ case "SCD30": {
590
+ if (dataBlockSizeBytes < 24) {
591
+ return false;
592
+ }
593
+
594
+ const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
595
+ const co2Ppm = clamp(
596
+ 850 + 320 * Math.sin(deviceTimeMs / 6500) + 45 * Math.sin(deviceTimeMs / 1400),
597
+ 400,
598
+ 2000
599
+ );
600
+ const temperatureC = clamp(
601
+ 22 + 2.2 * Math.sin(deviceTimeMs / 11000 + Math.PI / 5),
602
+ -40,
603
+ 125
604
+ );
605
+ const humidityPercent = clamp(
606
+ 48 + 14 * Math.sin(deviceTimeMs / 9000 + Math.PI / 2),
607
+ 0,
608
+ 100
609
+ );
610
+
611
+ return (
612
+ this._writeFloat32BytesAtOffsets(dataView, bytePos, [6, 7, 9, 10], co2Ppm) &&
613
+ this._writeFloat32BytesAtOffsets(dataView, bytePos, [12, 13, 15, 16], temperatureC) &&
614
+ this._writeFloat32BytesAtOffsets(dataView, bytePos, [18, 19, 21, 22], humidityPercent)
615
+ );
616
+ }
589
617
  case "RoboticalLEDRing": {
590
618
  if (dataBlockSizeBytes < 1) {
591
619
  return false;
@@ -665,6 +693,30 @@ export default class RaftChannelSimulated implements RaftChannel {
665
693
  }
666
694
  }
667
695
 
696
+ private _writeFloat32BytesAtOffsets(
697
+ dataView: DataView,
698
+ baseBytePos: number,
699
+ offsets: number[],
700
+ value: number
701
+ ): boolean {
702
+ if (offsets.length < 4) {
703
+ return false;
704
+ }
705
+
706
+ const maxOffset = Math.max(...offsets);
707
+ if (baseBytePos + maxOffset >= dataView.byteLength) {
708
+ return false;
709
+ }
710
+
711
+ const scratchBuffer = new ArrayBuffer(4);
712
+ new DataView(scratchBuffer).setFloat32(0, value, false);
713
+ const bytes = new Uint8Array(scratchBuffer);
714
+ for (let idx = 0; idx < 4; idx++) {
715
+ dataView.setUint8(baseBytePos + offsets[idx], bytes[idx]);
716
+ }
717
+ return true;
718
+ }
719
+
668
720
  private _getGridDimensions(attr: any, repeatCount: number): { rows: number; cols: number } {
669
721
  if (attr && typeof attr.resolution === "string") {
670
722
  const match = attr.resolution.match(/(\d+)\s*x\s*(\d+)/i);
@@ -1017,6 +1069,45 @@ export default class RaftChannelSimulated implements RaftChannel {
1017
1069
  "us": 100000
1018
1070
  }
1019
1071
  },
1072
+ "SCD30": {
1073
+ "name": "CO2 Sensor",
1074
+ "desc": "",
1075
+ "manu": "Sensirion",
1076
+ "type": "SCD30",
1077
+ "clas": ["CO2", "TEMP", "RH"],
1078
+ "resp": {
1079
+ "b": 24,
1080
+ "a": [
1081
+ {
1082
+ "n": "CO2",
1083
+ "t": ">f",
1084
+ "at": [6, 7, 9, 10],
1085
+ "u": "ppm",
1086
+ "r": [0, 40000],
1087
+ "f": ".1f",
1088
+ "o": "float"
1089
+ },
1090
+ {
1091
+ "n": "temperature",
1092
+ "t": ">f",
1093
+ "at": [12, 13, 15, 16],
1094
+ "u": "C",
1095
+ "r": [-40, 125],
1096
+ "f": ".2f",
1097
+ "o": "float"
1098
+ },
1099
+ {
1100
+ "n": "humidity",
1101
+ "t": ">f",
1102
+ "at": [18, 19, 21, 22],
1103
+ "u": "%RH",
1104
+ "r": [0, 100],
1105
+ "f": ".2f",
1106
+ "o": "float"
1107
+ }
1108
+ ]
1109
+ }
1110
+ },
1020
1111
  "RoboticalServo": {
1021
1112
  "name": "Robotical Servo",
1022
1113
  "desc": "Servo",
@@ -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, RaftStreamDataProgressCBType, 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";
@@ -87,6 +87,10 @@ export default class RaftConnector {
87
87
  // Update manager
88
88
  private _raftUpdateManager: RaftUpdateManager | null = null;
89
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
+
90
94
  /**
91
95
  * RaftConnector constructor
92
96
  * @param getSystemTypeCB - callback to get system type
@@ -193,6 +197,22 @@ export default class RaftConnector {
193
197
  return this._commsStats;
194
198
  }
195
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
+
196
216
  /**
197
217
  * Get Raft message handler (to allow message sending and receiving)
198
218
  * @returns RaftMsgHandler - Raft message handler
@@ -408,6 +428,11 @@ export default class RaftConnector {
408
428
  *
409
429
  */
410
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,
411
436
  bridgeID: number | undefined = undefined): Promise<RaftOKFail> {
412
437
  try {
413
438
  // Format the paramList as query string
@@ -415,11 +440,12 @@ export default class RaftConnector {
415
440
  let paramQueryStr = '';
416
441
  for (const param of paramEntries) {
417
442
  if (paramQueryStr.length > 0) paramQueryStr += '&';
418
- paramQueryStr += param[0] + '=' + param[1];
443
+ paramQueryStr += `${encodeURIComponent(param[0])}=${encodeURIComponent(String(param[1]))}`;
419
444
  }
420
445
  // Format the url to send
421
446
  if (paramQueryStr.length > 0) commandName += '?' + paramQueryStr;
422
- return await this._raftMsgHandler.sendRICRESTURL<RaftOKFail>(commandName, bridgeID);
447
+ const response = await this._raftMsgHandler.sendRICRESTURL<RaftOKFail | null>(commandName, bridgeID);
448
+ return response ?? { rslt: 'fail' };
423
449
  } catch (error) {
424
450
  RaftLog.warn(`sendRICRESTMsg failed ${error}`);
425
451
  return { rslt: 'fail' };
@@ -473,6 +499,13 @@ export default class RaftConnector {
473
499
  fileBlockData: Uint8Array
474
500
  ): void {
475
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
+ }
476
509
  this._raftFileHandler.onFileBlock(filePos, fileBlockData);
477
510
  }
478
511
 
@@ -534,6 +567,82 @@ export default class RaftConnector {
534
567
  return false;
535
568
  }
536
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
+
537
646
  /**
538
647
  * streamAudio - stream audio
539
648
  * @param streamContents audio data
@@ -136,6 +136,7 @@ export interface DeviceTypeInfo {
136
136
  desc: string;
137
137
  manu: string;
138
138
  type: string;
139
+ role?: string;
139
140
  resp?: DeviceTypePollRespMetadata;
140
141
  clas?: Array<string>;
141
142
  actions?: DeviceTypeAction[];
@@ -148,5 +149,8 @@ export interface DeviceTypeInfoRecs {
148
149
  export type RaftDevTypeInfoResponse = {
149
150
  req: string;
150
151
  rslt: string;
151
- devinfo: DeviceTypeInfo;
152
+ devinfo?: DeviceTypeInfo;
153
+ dtIdx?: number;
154
+ name?: string;
155
+ role?: string;
152
156
  };
@@ -2,7 +2,7 @@ import { DeviceManager } from "./RaftDeviceManager";
2
2
  import { DeviceTypeInfo } from "./RaftDeviceInfo";
3
3
  import RaftSystemUtils from "./RaftSystemUtils";
4
4
 
5
- function makeTypeInfo(name: string, respBytes: number, attrs: Array<{ n: string; t: string }>): DeviceTypeInfo {
5
+ function makeTypeInfo(name: string, respBytes: number, attrs: Array<{ n: string; t: string; at?: number | number[] }>): DeviceTypeInfo {
6
6
  return {
7
7
  name,
8
8
  desc: name,
@@ -123,6 +123,40 @@ describe("DeviceManager binary devbin parsing", () => {
123
123
  expect(deviceState.deviceAttributes.status.values).toEqual([4]);
124
124
  });
125
125
 
126
+ it("decodes current length-prefixed records with sparse absolute attribute offsets", async () => {
127
+ const scd30Info = makeTypeInfo("SCD30", 24, [
128
+ { n: "CO2", t: ">f", at: [6, 7, 9, 10] },
129
+ { n: "temperature", t: ">f", at: [12, 13, 15, 16] },
130
+ { n: "humidity", t: ">f", at: [18, 19, 21, 22] }
131
+ ]);
132
+ const deviceManager = await makeDeviceManager({ "42": scd30Info });
133
+ const rxMsg = Uint8Array.from([
134
+ 0x00, 0x80,
135
+ 0xDB, 0xFF, 0x00,
136
+ 0x00, 0x23,
137
+ 0x81,
138
+ 0x00, 0x00, 0x02, 0x61,
139
+ 0x00, 0x2a,
140
+ 0x07,
141
+ 0x1a,
142
+ 0x00, 0x01,
143
+ 0x00, 0x01, 0xb0,
144
+ 0x00, 0x01, 0xb0,
145
+ 0x43, 0xfa, 0x00, 0x00, 0x00, 0x00,
146
+ 0x41, 0xc8, 0x00, 0x00, 0x00, 0x00,
147
+ 0x42, 0x5e, 0x00, 0x00, 0x00, 0x00
148
+ ]);
149
+
150
+ await deviceManager.handleClientMsgBinary(rxMsg);
151
+
152
+ const deviceState = deviceManager.getDeviceState("1_261");
153
+ expect(deviceState.deviceType).toBe("SCD30");
154
+ expect(deviceState.deviceTimeline.totalSamplesAdded).toBe(1);
155
+ expect(deviceState.deviceAttributes.CO2.values).toEqual([500]);
156
+ expect(deviceState.deviceAttributes.temperature.values).toEqual([25]);
157
+ expect(deviceState.deviceAttributes.humidity.values).toEqual([55.5]);
158
+ });
159
+
126
160
  it("keeps Cog v1.9.5 direct device records distinct when bus and address are both zero", async () => {
127
161
  const lightInfo = makeTypeInfo("LightSensors", 16, [
128
162
  { n: "ch0", t: ">H" },
@@ -403,8 +403,14 @@ export class DeviceManager implements RaftDeviceMgrIF{
403
403
  // Check if a device state already exists
404
404
  if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
405
405
 
406
- // Get the device type info
407
- const deviceTypeInfo = resolvedDeviceTypeInfo ?? await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
406
+ // Merge rationale: upstream's deviceid lookup carries
407
+ // per-instance name/role fields, while Robotical's simulator
408
+ // and older firmware still expose bus/type lookup. Try the
409
+ // richer endpoint first and fall back to the type record that
410
+ // was already loaded for payload-format detection.
411
+ const deviceTypeInfo = await this.getDeviceTypeInfo(deviceKey)
412
+ ?? resolvedDeviceTypeInfo
413
+ ?? await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
408
414
 
409
415
  // Debug
410
416
  // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} busNum ${busNum} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} deviceTypeInfo ${JSON.stringify(deviceTypeInfo)}`);
@@ -482,11 +488,21 @@ export class DeviceManager implements RaftDeviceMgrIF{
482
488
 
483
489
  const sampleStartPos = pollDataPos;
484
490
  const sampleEndPos = pollDataPos + sampleLen;
491
+ // Merge rationale: keep Robotical's explicit sample
492
+ // boundary and add upstream diagnostics so malformed
493
+ // samples can be skipped with useful context.
485
494
  const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, sampleStartPos,
486
495
  deviceState.deviceTimeline, pollRespMetadata,
487
496
  deviceState.deviceAttributes,
488
497
  this._maxDatapointsToStore,
489
- sampleEndPos);
498
+ sampleEndPos,
499
+ {
500
+ deviceKey,
501
+ deviceType: deviceState.deviceType,
502
+ debugMsgIndex,
503
+ sampleStartIdx: sampleStartPos,
504
+ sampleEndIdx: sampleEndPos,
505
+ });
490
506
 
491
507
  if (newMsgBufIdx < 0)
492
508
  {
@@ -515,11 +531,21 @@ export class DeviceManager implements RaftDeviceMgrIF{
515
531
  while (pollDataPos + legacySampleLen <= samplesEndPos) {
516
532
  const sampleStartPos = pollDataPos;
517
533
  const sampleEndPos = pollDataPos + legacySampleLen;
534
+ // Legacy raw records are not length-prefixed, but
535
+ // the fixed sample span gives the same bound and
536
+ // diagnostic context to the attribute handler.
518
537
  const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, sampleStartPos,
519
538
  deviceState.deviceTimeline, pollRespMetadata,
520
539
  deviceState.deviceAttributes,
521
540
  this._maxDatapointsToStore,
522
- sampleEndPos);
541
+ sampleEndPos,
542
+ {
543
+ deviceKey,
544
+ deviceType: deviceState.deviceType,
545
+ debugMsgIndex,
546
+ sampleStartIdx: sampleStartPos,
547
+ sampleEndIdx: sampleEndPos,
548
+ });
523
549
 
524
550
  if (newMsgBufIdx < 0)
525
551
  {
@@ -632,8 +658,11 @@ export class DeviceManager implements RaftDeviceMgrIF{
632
658
  // Check if a device state already exists
633
659
  if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
634
660
 
635
- // Get the device type info
636
- const deviceTypeInfo = await this.getDeviceTypeInfo(busName, deviceTypeName);
661
+ // Prefer upstream's per-instance deviceid endpoint, but
662
+ // keep Robotical's bus/type fallback for simulated and
663
+ // older JSON publishers.
664
+ const deviceTypeInfo = await this.getDeviceTypeInfo(deviceKey)
665
+ ?? await this.getDeviceTypeInfo(busName, deviceTypeName);
637
666
 
638
667
  // Check if device record exists
639
668
  if (deviceKey in this._devicesState) {
@@ -802,71 +831,87 @@ export class DeviceManager implements RaftDeviceMgrIF{
802
831
  // Get device type info
803
832
  ////////////////////////////////////////////////////////////////////////////
804
833
 
805
- private async getDeviceTypeInfo(busName: string, deviceType: string): Promise<DeviceTypeInfo | undefined> {
834
+ private async getDeviceTypeInfo(deviceKey: string): Promise<DeviceTypeInfo | undefined>;
835
+ private async getDeviceTypeInfo(busName: string, deviceType: string): Promise<DeviceTypeInfo | undefined>;
836
+ private async getDeviceTypeInfo(deviceKeyOrBusName: string, deviceType?: string): Promise<DeviceTypeInfo | undefined> {
837
+ const cacheKey = deviceType === undefined ? deviceKeyOrBusName : deviceType;
838
+ const requestLabel = deviceType === undefined ? `deviceKey ${deviceKeyOrBusName}` : `deviceType ${deviceType}`;
839
+
806
840
  // Check if already in cache
807
- if (deviceType in this._cachedDeviceTypeRecs) {
808
- return this._cachedDeviceTypeRecs[deviceType];
841
+ if (cacheKey in this._cachedDeviceTypeRecs) {
842
+ return this._cachedDeviceTypeRecs[cacheKey];
809
843
  }
810
844
 
811
- // Check if there's already a pending request for this device type
812
- if (deviceType in this._pendingDeviceTypeRequests) {
813
- // console.log(`DevMan.getDeviceTypeInfo joining existing request queue for deviceType ${deviceType}`);
814
-
845
+ // Check if there's already a pending request for this device/type
846
+ if (cacheKey in this._pendingDeviceTypeRequests) {
815
847
  // Add this request to the waiting queue
816
848
  return new Promise<DeviceTypeInfo | undefined>((resolve, reject) => {
817
- this._pendingDeviceTypeRequests[deviceType].waitingQueue.push({ resolve, reject });
849
+ this._pendingDeviceTypeRequests[cacheKey].waitingQueue.push({ resolve, reject });
818
850
  });
819
851
  }
820
852
 
821
853
  // Check rate limiting for new requests
822
- if (deviceType in this._cachedDeviceTypePreviousAttemptTimes) {
823
- const timeSinceLastAttempt = Date.now() - this._cachedDeviceTypePreviousAttemptTimes[deviceType];
854
+ if (cacheKey in this._cachedDeviceTypePreviousAttemptTimes) {
855
+ const timeSinceLastAttempt = Date.now() - this._cachedDeviceTypePreviousAttemptTimes[cacheKey];
824
856
  if (timeSinceLastAttempt < this._minTimeBetweenDeviceTypeInfoRetrievalMs) {
825
- console.log(`DevMan.getDeviceTypeInfo rate limited for deviceType ${deviceType}`);
857
+ console.log(`DevMan.getDeviceTypeInfo rate limited for ${requestLabel}`);
826
858
  return undefined;
827
859
  }
828
860
  }
829
861
 
830
862
  // Create and cache the promise with an empty waiting queue
831
- const requestPromise = this.executeDeviceTypeInfoRequest(busName, deviceType);
832
- this._pendingDeviceTypeRequests[deviceType] = {
863
+ const requestPromise = this.executeDeviceTypeInfoRequest(deviceKeyOrBusName, deviceType);
864
+ this._pendingDeviceTypeRequests[cacheKey] = {
833
865
  promise: requestPromise,
834
866
  waitingQueue: []
835
867
  };
836
868
 
837
869
  try {
838
870
  const result = await requestPromise;
839
-
871
+
840
872
  // Resolve all waiting requests with the same result
841
- const waitingQueue = this._pendingDeviceTypeRequests[deviceType].waitingQueue;
873
+ const waitingQueue = this._pendingDeviceTypeRequests[cacheKey].waitingQueue;
842
874
  waitingQueue.forEach(({ resolve }) => resolve(result));
843
-
875
+
844
876
  return result;
845
877
  } catch (error) {
846
878
  // Reject all waiting requests with the same error
847
- const waitingQueue = this._pendingDeviceTypeRequests[deviceType].waitingQueue;
879
+ const waitingQueue = this._pendingDeviceTypeRequests[cacheKey].waitingQueue;
848
880
  waitingQueue.forEach(({ reject }) => reject(error));
849
-
850
- console.warn(`DevMan.getDeviceTypeInfo failed for ${deviceType}: ${error}`);
881
+
882
+ console.warn(`DevMan.getDeviceTypeInfo failed for ${requestLabel}: ${error}`);
851
883
  return undefined;
852
884
  } finally {
853
885
  // Clean up the pending request
854
- delete this._pendingDeviceTypeRequests[deviceType];
886
+ delete this._pendingDeviceTypeRequests[cacheKey];
855
887
  }
856
888
  }
857
889
 
858
- private async executeDeviceTypeInfoRequest(busName: string, deviceType: string): Promise<DeviceTypeInfo | undefined> {
859
- this._cachedDeviceTypePreviousAttemptTimes[deviceType] = Date.now();
860
-
890
+ private async executeDeviceTypeInfoRequest(deviceKeyOrBusName: string, deviceType?: string): Promise<DeviceTypeInfo | undefined> {
891
+ const cacheKey = deviceType === undefined ? deviceKeyOrBusName : deviceType;
892
+ this._cachedDeviceTypePreviousAttemptTimes[cacheKey] = Date.now();
893
+
861
894
  try {
862
- const cmd = "devman/typeinfo?bus=" + busName + "&type=" + deviceType;
895
+ // Merge rationale: support both upstream's per-device endpoint and
896
+ // Robotical's older bus/type endpoint. The deviceid response may
897
+ // include instance-specific name/role; the bus/type response keeps
898
+ // simulators and older firmware working.
899
+ const cmd = deviceType === undefined
900
+ ? "devman/typeinfo?deviceid=" + deviceKeyOrBusName
901
+ : "devman/typeinfo?bus=" + deviceKeyOrBusName + "&type=" + deviceType;
863
902
  const msgHandler = this._systemUtils?.getMsgHandler();
864
-
903
+
865
904
  if (msgHandler) {
866
905
  const msgRslt = await msgHandler.sendRICRESTURL<RaftDevTypeInfoResponse>(cmd);
867
906
  if (msgRslt && msgRslt.rslt === "ok") {
868
- this._cachedDeviceTypeRecs[deviceType] = msgRslt.devinfo;
869
- return msgRslt.devinfo;
907
+ // Merge devinfo (type-level) with top-level per-instance overrides (name, role)
908
+ const base: DeviceTypeInfo = msgRslt.devinfo
909
+ ? { ...msgRslt.devinfo }
910
+ : { name: "", desc: "", manu: "", type: "" };
911
+ if (msgRslt.name !== undefined) base.name = msgRslt.name;
912
+ if (msgRslt.role !== undefined) base.role = msgRslt.role;
913
+ this._cachedDeviceTypeRecs[cacheKey] = base;
914
+ return base;
870
915
  }
871
916
  }
872
917
  return undefined;
@@ -964,10 +1009,14 @@ export class DeviceManager implements RaftDeviceMgrIF{
964
1009
  }
965
1010
 
966
1011
  let attrPayloadLen = 0;
1012
+ let usesAbsolutePositions = false;
967
1013
  for (const attrDef of pollRespMetadata.a) {
968
1014
  if (!attrDef.t) {
969
1015
  return pollRespMetadata.b;
970
1016
  }
1017
+ if (attrDef.at !== undefined) {
1018
+ usesAbsolutePositions = true;
1019
+ }
971
1020
  try {
972
1021
  attrPayloadLen += structSizeOf(attrDef.t);
973
1022
  } catch {
@@ -975,6 +1024,10 @@ export class DeviceManager implements RaftDeviceMgrIF{
975
1024
  }
976
1025
  }
977
1026
 
1027
+ if (usesAbsolutePositions && pollRespMetadata.b > 0) {
1028
+ return pollRespMetadata.b;
1029
+ }
1030
+
978
1031
  // Cog v1.9.5 light metadata reports the direct-sensor payload size doubled,
979
1032
  // but the legacy raw record contains one fixed payload matching the attribute schema.
980
1033
  if ((attrPayloadLen > 0) && (pollRespMetadata.b > 0) && (attrPayloadLen <= pollRespMetadata.b)) {