@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.
- package/devdocs/decode-overrun-investigation.md +167 -0
- package/devdocs/message-panel-design.md +320 -0
- package/dist/react-native/RaftAttributeHandler.d.ts +10 -1
- package/dist/react-native/RaftAttributeHandler.js +60 -8
- package/dist/react-native/RaftAttributeHandler.js.map +1 -1
- package/dist/react-native/RaftChannelWebSerial.js +23 -1
- package/dist/react-native/RaftChannelWebSerial.js.map +1 -1
- package/dist/react-native/RaftConnector.js +23 -0
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +5 -1
- package/dist/react-native/RaftDeviceManager.js +68 -32
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftAttributeHandler.d.ts +10 -1
- package/dist/web/RaftAttributeHandler.js +60 -8
- package/dist/web/RaftAttributeHandler.js.map +1 -1
- package/dist/web/RaftChannelWebSerial.js +23 -1
- package/dist/web/RaftChannelWebSerial.js.map +1 -1
- package/dist/web/RaftConnector.js +23 -0
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +5 -1
- package/dist/web/RaftDeviceManager.js +68 -32
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/examples/dashboard/src/DeviceActionsForm.tsx +3 -0
- package/examples/dashboard/src/DeviceLineChart.tsx +5 -4
- package/examples/dashboard/src/DevicePanel.tsx +3 -0
- package/package.json +1 -1
- package/src/RaftAttributeHandler.ts +89 -9
- package/src/RaftChannelWebSerial.ts +23 -2
- package/src/RaftConnector.ts +30 -0
- package/src/RaftDeviceInfo.ts +5 -1
- package/src/RaftDeviceManager.ts +78 -33
- 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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/RaftConnector.ts
CHANGED
|
@@ -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) {
|
package/src/RaftDeviceInfo.ts
CHANGED
|
@@ -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
|
|
152
|
+
devinfo?: DeviceTypeInfo;
|
|
153
|
+
dtIdx?: number;
|
|
154
|
+
name?: string;
|
|
155
|
+
role?: string;
|
|
152
156
|
};
|
package/src/RaftDeviceManager.ts
CHANGED
|
@@ -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
|
-
//
|
|
407
|
-
|
|
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
|
-
//
|
|
636
|
-
|
|
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(
|
|
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 (
|
|
808
|
-
return this._cachedDeviceTypeRecs[
|
|
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
|
|
812
|
-
if (
|
|
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[
|
|
849
|
+
this._pendingDeviceTypeRequests[cacheKey].waitingQueue.push({ resolve, reject });
|
|
818
850
|
});
|
|
819
851
|
}
|
|
820
852
|
|
|
821
853
|
// Check rate limiting for new requests
|
|
822
|
-
if (
|
|
823
|
-
const timeSinceLastAttempt = Date.now() - this._cachedDeviceTypePreviousAttemptTimes[
|
|
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
|
|
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(
|
|
832
|
-
this._pendingDeviceTypeRequests[
|
|
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[
|
|
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[
|
|
879
|
+
const waitingQueue = this._pendingDeviceTypeRequests[cacheKey].waitingQueue;
|
|
848
880
|
waitingQueue.forEach(({ reject }) => reject(error));
|
|
849
|
-
|
|
850
|
-
console.warn(`DevMan.getDeviceTypeInfo failed for ${
|
|
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[
|
|
886
|
+
delete this._pendingDeviceTypeRequests[cacheKey];
|
|
855
887
|
}
|
|
856
888
|
}
|
|
857
889
|
|
|
858
|
-
private async executeDeviceTypeInfoRequest(
|
|
859
|
-
|
|
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
|
-
|
|
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
|
-
|
|
869
|
-
|
|
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';
|