@robotical/raftjs 2.1.3 → 2.1.5

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 (32) 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/RaftChannelWebSerial.js +23 -1
  7. package/dist/react-native/RaftChannelWebSerial.js.map +1 -1
  8. package/dist/react-native/RaftConnector.js +23 -0
  9. package/dist/react-native/RaftConnector.js.map +1 -1
  10. package/dist/react-native/RaftDeviceInfo.d.ts +5 -1
  11. package/dist/react-native/RaftDeviceManager.js +68 -32
  12. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  13. package/dist/web/RaftAttributeHandler.d.ts +10 -1
  14. package/dist/web/RaftAttributeHandler.js +60 -8
  15. package/dist/web/RaftAttributeHandler.js.map +1 -1
  16. package/dist/web/RaftChannelWebSerial.js +23 -1
  17. package/dist/web/RaftChannelWebSerial.js.map +1 -1
  18. package/dist/web/RaftConnector.js +23 -0
  19. package/dist/web/RaftConnector.js.map +1 -1
  20. package/dist/web/RaftDeviceInfo.d.ts +5 -1
  21. package/dist/web/RaftDeviceManager.js +68 -32
  22. package/dist/web/RaftDeviceManager.js.map +1 -1
  23. package/examples/dashboard/src/DeviceActionsForm.tsx +3 -0
  24. package/examples/dashboard/src/DeviceLineChart.tsx +5 -4
  25. package/examples/dashboard/src/DevicePanel.tsx +3 -0
  26. package/package.json +1 -1
  27. package/src/RaftAttributeHandler.ts +89 -9
  28. package/src/RaftChannelWebSerial.ts +23 -2
  29. package/src/RaftConnector.ts +30 -0
  30. package/src/RaftDeviceInfo.ts +5 -1
  31. package/src/RaftDeviceManager.ts +78 -33
  32. package/src/main.ts +1 -1
@@ -11,6 +11,23 @@ import CustomAttrHandler from "./RaftCustomAttrHandler";
11
11
  import { DeviceTypeAttribute, DeviceTypePollRespMetadata, decodeAttrUnitsEncoding, isAttrTypeSigned } from "./RaftDeviceInfo";
12
12
  import { DeviceAttributesState, DeviceTimeline } from "./RaftDeviceStates";
13
13
  import { structSizeOf, structUnpack } from "./RaftStruct";
14
+ import RaftUtils from "./RaftUtils";
15
+
16
+ // Optional diagnostic context for attribute decoding. When provided, the
17
+ // handler emits a detailed warning if an attribute would read past the
18
+ // declared sample boundary or the end of the message buffer. This is used to
19
+ // track down firmware/schema mismatches.
20
+ export interface AttrDecodeDiagContext {
21
+ deviceKey?: string;
22
+ deviceType?: string;
23
+ debugMsgIndex?: number;
24
+ // Inclusive start (in msgBuffer) of the current sample's bytes (after the
25
+ // 1-byte sampleLen prefix). When provided together with sampleEndIdx, the
26
+ // handler will bound-check attribute reads against this range.
27
+ sampleStartIdx?: number;
28
+ // Exclusive end (in msgBuffer) of the current sample's bytes.
29
+ sampleEndIdx?: number;
30
+ }
14
31
 
15
32
  export default class AttributeHandler {
16
33
 
@@ -23,8 +40,18 @@ export default class AttributeHandler {
23
40
  private POLL_RESULT_RESOLUTION_US = 100;
24
41
 
25
42
  public processMsgAttrGroup(msgBuffer: Uint8Array, msgBufIdx: number, deviceTimeline: DeviceTimeline, pollRespMetadata: DeviceTypePollRespMetadata,
26
- devAttrsState: DeviceAttributesState, maxDataPoints: number, msgEndIdx = msgBuffer.length): number {
27
-
43
+ devAttrsState: DeviceAttributesState, maxDataPoints: number,
44
+ msgEndIdxOrDiagCtx: number | AttrDecodeDiagContext = msgBuffer.length,
45
+ diagCtx?: AttrDecodeDiagContext): number {
46
+
47
+ // Merge rationale: Robotical's devbin compatibility parser needs an
48
+ // explicit sample boundary; upstream's diagnostics need per-sample
49
+ // context. Accept both call styles so malformed samples are skipped
50
+ // without losing useful overrun warnings.
51
+ const msgEndIdx = typeof msgEndIdxOrDiagCtx === "number"
52
+ ? msgEndIdxOrDiagCtx
53
+ : msgEndIdxOrDiagCtx.sampleEndIdx ?? msgBuffer.length;
54
+ const effectiveDiagCtx = typeof msgEndIdxOrDiagCtx === "number" ? diagCtx : msgEndIdxOrDiagCtx;
28
55
  // console.log(`processMsgAttrGroup msg ${msgHexStr} timestamp ${timestamp} origTimestamp ${origTimestamp} msgBufIdx ${msgBufIdx}`)
29
56
  const boundedMsgEndIdx = Math.min(Math.max(msgEndIdx, msgBufIdx), msgBuffer.length);
30
57
 
@@ -39,6 +66,11 @@ export default class AttributeHandler {
39
66
 
40
67
  // New attribute values (in order as they appear in the attributes JSON)
41
68
  let newAttrValues: (number | string)[][] = [];
69
+ // Tracks whether any individual attribute decode failed in the non-custom path.
70
+ // When true, the detailed per-attribute overrun warning has already been emitted
71
+ // by processMsgAttribute, so we suppress the redundant downstream length/empty
72
+ // warnings that would otherwise fire every poll.
73
+ let attrDecodeFailed = false;
42
74
  if ("c" in pollRespMetadata) {
43
75
 
44
76
  // Extract attribute values using custom handler
@@ -83,13 +115,15 @@ export default class AttributeHandler {
83
115
  if (!("t" in attrDef)) {
84
116
  console.warn(`DeviceManager msg unknown msgBuffer ${msgBuffer} tsUs ${timestampUs} attrDef ${JSON.stringify(attrDef)}`);
85
117
  newAttrValues.push([]);
118
+ attrDecodeFailed = true;
86
119
  continue;
87
120
  }
88
121
 
89
122
  // console.log(`RaftAttrHdlr.processMsgAttrGroup attr ${attrDef.n} msgBufIdx ${msgBufIdx} timestampUs ${timestampUs} attrDef ${JSON.stringify(attrDef)}`);
90
123
 
91
124
  // Process the attribute
92
- const { values, newMsgBufIdx } = this.processMsgAttribute(attrDef, msgBuffer, msgBufIdx, msgDataStartIdx, boundedMsgEndIdx);
125
+ const { values, newMsgBufIdx } = this.processMsgAttribute(attrDef, msgBuffer, msgBufIdx, msgDataStartIdx,
126
+ boundedMsgEndIdx, pollRespMetadata, effectiveDiagCtx);
93
127
  if (newMsgBufIdx < 0) {
94
128
  return -1;
95
129
  }
@@ -114,14 +148,18 @@ export default class AttributeHandler {
114
148
  const numNewDataPoints = newAttrValues[0].length;
115
149
  for (let i = 1; i < newAttrValues.length; i++) {
116
150
  if (newAttrValues[i].length !== numNewDataPoints) {
117
- console.warn(`DeviceManager msg attrGroup ${pollRespMetadata} attrName ${pollRespMetadata.a[i].n} newAttrValues ${newAttrValues} do not have the same length`);
151
+ if (!attrDecodeFailed) {
152
+ console.warn(`DeviceManager msg attrGroup ${JSON.stringify(pollRespMetadata)} attrName ${pollRespMetadata.a[i].n} newAttrValues lengths ${newAttrValues.map(v => v.length).join(",")} do not match`);
153
+ }
118
154
  return msgDataStartIdx+pollRespSizeBytes;
119
155
  }
120
156
  }
121
157
 
122
158
  // All attributes in the schema should have values
123
159
  if (newAttrValues.length !== pollRespMetadata.a.length) {
124
- console.warn(`DeviceManager msg attrGroup ${pollRespMetadata} newAttrValues ${newAttrValues} length does not match attrGroup.a length`);
160
+ if (!attrDecodeFailed) {
161
+ console.warn(`DeviceManager msg attrGroup ${JSON.stringify(pollRespMetadata)} newAttrValues length ${newAttrValues.length} does not match attrGroup.a length ${pollRespMetadata.a.length}`);
162
+ }
125
163
  return msgDataStartIdx+pollRespSizeBytes;
126
164
  }
127
165
 
@@ -272,7 +310,9 @@ export default class AttributeHandler {
272
310
  }
273
311
  }
274
312
 
275
- private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number, msgEndIdx: number): { values: (number | string)[], newMsgBufIdx: number} {
313
+ private processMsgAttribute(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, msgBufIdx: number, msgDataStartIdx: number,
314
+ msgEndIdx: number, pollRespMetadata?: DeviceTypePollRespMetadata,
315
+ diagCtx?: AttrDecodeDiagContext): { values: (number | string)[], newMsgBufIdx: number} {
276
316
 
277
317
  // Current field message string index
278
318
  let curFieldBufIdx = msgBufIdx;
@@ -295,7 +335,9 @@ export default class AttributeHandler {
295
335
  // Copy bytes from the specified positions
296
336
  for (let i = 0; i < attrDef.at.length && i < elemSize; i++) {
297
337
  const sourceIdx = msgDataStartIdx + attrDef.at[i];
298
- if (sourceIdx >= boundedMsgEndIdx) {
338
+ if (sourceIdx < msgDataStartIdx || sourceIdx >= boundedMsgEndIdx) {
339
+ this.warnAttrOverrun(attrDef, msgBuffer, sourceIdx, elemSize, msgDataStartIdx,
340
+ true, pollRespMetadata, diagCtx, sourceIdx >= msgBuffer.length ? "msgBuffer" : "sample");
299
341
  return { values: [], newMsgBufIdx: -1 };
300
342
  }
301
343
  bytesForType[i] = msgBuffer[sourceIdx];
@@ -312,11 +354,15 @@ export default class AttributeHandler {
312
354
  attrUsesAbsPos = true;
313
355
  }
314
356
 
315
- // Check if outside bounds of message
357
+ // Merge rationale: keep Robotical's hard sample bounds as the source of
358
+ // truth, but emit upstream's one-shot diagnostic when a schema tries to
359
+ // read beyond the sample or buffer.
316
360
  const attrEndIdx = curFieldBufIdx + numBytesConsumed;
317
361
  const effectiveMsgEndIdx = Math.min(Math.max(msgEndIdx, curFieldBufIdx), msgBuffer.length);
318
362
  if (curFieldBufIdx >= effectiveMsgEndIdx || attrEndIdx > effectiveMsgEndIdx) {
319
- // console.warn(`DeviceManager msg outside bounds msgBuffer ${msgBuffer} attrName ${attrDef.n}`);
363
+ this.warnAttrOverrun(attrDef, msgBuffer, curFieldBufIdx, numBytesConsumed, msgDataStartIdx,
364
+ attrUsesAbsPos, pollRespMetadata, diagCtx,
365
+ attrEndIdx > msgBuffer.length ? "msgBuffer" : "sample");
320
366
  return { values: [], newMsgBufIdx: -1 };
321
367
  }
322
368
 
@@ -439,6 +485,40 @@ export default class AttributeHandler {
439
485
  return { values: attrValues, newMsgBufIdx: msgBufIdx };
440
486
  }
441
487
 
488
+ // One-shot detailed warning when an attribute decode would overrun the
489
+ // sample/message bounds. Includes the exact bytes and schema so the
490
+ // firmware vs. registered device-type schema can be reconciled.
491
+ private _overrunWarnSeen: Set<string> = new Set();
492
+ private warnAttrOverrun(attrDef: DeviceTypeAttribute, msgBuffer: Uint8Array, curFieldBufIdx: number,
493
+ attrTypeSize: number, msgDataStartIdx: number, attrUsesAbsPos: boolean,
494
+ pollRespMetadata: DeviceTypePollRespMetadata | undefined,
495
+ diagCtx: AttrDecodeDiagContext | undefined, overrunOf: "msgBuffer" | "sample"): void {
496
+ const sampleStart = diagCtx?.sampleStartIdx ?? msgDataStartIdx;
497
+ const sampleEnd = diagCtx?.sampleEndIdx;
498
+ const dedupeKey = `${diagCtx?.deviceKey ?? "?"}|${diagCtx?.deviceType ?? "?"}|${attrDef.n}|${attrDef.t}|${attrUsesAbsPos ? attrDef.at : "rel"}`;
499
+ if (this._overrunWarnSeen.has(dedupeKey)) {
500
+ return;
501
+ }
502
+ this._overrunWarnSeen.add(dedupeKey);
503
+ const sampleHex = sampleEnd !== undefined
504
+ ? RaftUtils.bufferToHex(msgBuffer.slice(sampleStart, sampleEnd))
505
+ : "<unknown sample bounds>";
506
+ const availableInSample = sampleEnd !== undefined ? Math.max(0, sampleEnd - curFieldBufIdx) : -1;
507
+ const availableInBuffer = Math.max(0, msgBuffer.length - curFieldBufIdx);
508
+ console.warn(
509
+ `AttributeHandler decode overrun (${overrunOf}): ` +
510
+ `deviceKey=${diagCtx?.deviceKey ?? "?"} deviceType=${diagCtx?.deviceType ?? "?"} ` +
511
+ `debugMsgIndex=${diagCtx?.debugMsgIndex ?? "?"} attr.n=${attrDef.n} attr.t=${attrDef.t} ` +
512
+ `attrTypeSize=${attrTypeSize} attrUsesAbsPos=${attrUsesAbsPos} attr.at=${JSON.stringify(attrDef.at)} ` +
513
+ `curFieldBufIdx=${curFieldBufIdx} msgBuffer.length=${msgBuffer.length} ` +
514
+ `sampleStartIdx=${sampleStart} sampleEndIdx=${sampleEnd ?? "?"} ` +
515
+ `availableInSample=${availableInSample} availableInBuffer=${availableInBuffer} ` +
516
+ `sampleHex=${sampleHex} ` +
517
+ `pollRespMetadata.b=${pollRespMetadata?.b} ` +
518
+ `schema=${JSON.stringify(pollRespMetadata?.a)}`
519
+ );
520
+ }
521
+
442
522
  private signExtend(value: number, mask: number): number {
443
523
  const signBitMask = (mask + 1) >> 1;
444
524
  const signBit = value & signBitMask;
@@ -192,9 +192,30 @@ export default class RaftChannelWebSerial implements RaftChannel {
192
192
 
193
193
  RaftLog.debug(`RaftChannelWebSerial.disconnect attempting to close webserial`);
194
194
 
195
- while (this._reader) {
195
+ const reader = this._reader;
196
+ if (reader) {
197
+ try {
198
+ await Promise.race([
199
+ reader.cancel(),
200
+ new Promise((resolve) => setTimeout(resolve, 1000)),
201
+ ]);
202
+ } catch (err) {
203
+ RaftLog.debug(`RaftChannelWebSerial.disconnect reader cancel failed ${err}`);
204
+ }
205
+ }
206
+
207
+ const readerReleaseStartMs = Date.now();
208
+ while (this._reader && Date.now() - readerReleaseStartMs < 1000) {
196
209
  await new Promise((resolve) => setTimeout(resolve, 100));
197
210
  }
211
+ if (this._reader) {
212
+ try {
213
+ this._reader.releaseLock();
214
+ } catch (err) {
215
+ RaftLog.debug(`RaftChannelWebSerial.disconnect reader release failed ${err}`);
216
+ }
217
+ this._reader = undefined;
218
+ }
198
219
 
199
220
  // Disconnect webserial
200
221
  try {
@@ -417,4 +438,4 @@ export default class RaftChannelWebSerial implements RaftChannel {
417
438
  RaftLog.debug(`sendTxMsgRawAndWaitForReply - not implemented`);
418
439
  return null as T;
419
440
  }
420
- }
441
+ }
@@ -25,6 +25,24 @@ import { RaftUpdateEvent, RaftUpdateEventNames } from "./RaftUpdateEvents";
25
25
  import RaftUpdateManager from "./RaftUpdateManager";
26
26
  import { createBLEChannel } from "./RaftChannelBLEFactory";import { getHostPosixTZ } from './RaftTimezone';
27
27
 
28
+ const DISCONNECT_SUBSCRIPTION_TIMEOUT_MS = 1000;
29
+
30
+ function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
31
+ return new Promise<T>((resolve, reject) => {
32
+ const timeout = setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
33
+ promise.then(
34
+ (value) => {
35
+ clearTimeout(timeout);
36
+ resolve(value);
37
+ },
38
+ (error) => {
39
+ clearTimeout(timeout);
40
+ reject(error);
41
+ }
42
+ );
43
+ });
44
+ }
45
+
28
46
  export default class RaftConnector {
29
47
 
30
48
  // Get system type callback
@@ -387,6 +405,18 @@ export default class RaftConnector {
387
405
  const channelToDisconnect = this._raftChannel;
388
406
  this._raftChannel = null;
389
407
 
408
+ if (this._systemType?.subscribeForUpdates && channelToDisconnect.requiresSubscription()) {
409
+ try {
410
+ await withTimeout(
411
+ this._systemType.subscribeForUpdates(this._raftSystemUtils, false),
412
+ DISCONNECT_SUBSCRIPTION_TIMEOUT_MS
413
+ );
414
+ RaftLog.info("disconnect unsubscribed from updates");
415
+ } catch (error) {
416
+ RaftLog.warn(`disconnect unsubscribe for updates failed ${error}`);
417
+ }
418
+ }
419
+
390
420
  // Check if there is a RICREST command to send before disconnecting
391
421
  const ricRestCommand = channelToDisconnect.ricRestCmdBeforeDisconnect();
392
422
  if (ricRestCommand) {
@@ -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
  };
@@ -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;
package/src/main.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  import { createBLEChannel } from './RaftChannelBLEFactory';
12
12
  const raftChannel = createBLEChannel();
13
13
  export { raftChannel as RaftChannelBLE };
14
-
14
+
15
15
  export { default as RaftCommsStats } from './RaftCommsStats';
16
16
  export { default as RaftConnector } from './RaftConnector';
17
17
  export { default as RaftChannel } from './RaftChannel';