@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.
- 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/RaftChannelSimulated.d.ts +1 -0
- package/dist/react-native/RaftChannelSimulated.js +67 -0
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftConnector.d.ts +19 -1
- package/dist/react-native/RaftConnector.js +102 -2
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +5 -1
- package/dist/react-native/RaftDeviceManager.js +75 -32
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftFileHandler.d.ts +1 -0
- package/dist/react-native/RaftFileHandler.js +40 -11
- package/dist/react-native/RaftFileHandler.js.map +1 -1
- package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/react-native/RaftMsgHandler.d.ts +1 -1
- package/dist/react-native/RaftMsgHandler.js +6 -3
- package/dist/react-native/RaftMsgHandler.js.map +1 -1
- package/dist/react-native/RaftTypes.d.ts +19 -0
- package/dist/react-native/RaftTypes.js.map +1 -1
- package/dist/react-native/main.d.ts +1 -0
- package/dist/react-native/main.js +3 -1
- package/dist/react-native/main.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/RaftChannelSimulated.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.js +67 -0
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftConnector.d.ts +19 -1
- package/dist/web/RaftConnector.js +102 -2
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +5 -1
- package/dist/web/RaftDeviceManager.js +75 -32
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftFileHandler.d.ts +1 -0
- package/dist/web/RaftFileHandler.js +40 -11
- package/dist/web/RaftFileHandler.js.map +1 -1
- package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/web/RaftMsgHandler.d.ts +1 -1
- package/dist/web/RaftMsgHandler.js +6 -3
- package/dist/web/RaftMsgHandler.js.map +1 -1
- package/dist/web/RaftTypes.d.ts +19 -0
- package/dist/web/RaftTypes.js.map +1 -1
- package/dist/web/main.d.ts +1 -0
- package/dist/web/main.js +3 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/src/DeviceActionsForm.tsx +24 -11
- 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/RaftChannelSimulated.test.ts +62 -0
- package/src/RaftChannelSimulated.ts +91 -0
- package/src/RaftConnector.ts +112 -3
- package/src/RaftDeviceInfo.ts +5 -1
- package/src/RaftDeviceManager.test.ts +35 -1
- package/src/RaftDeviceManager.ts +86 -33
- package/src/RaftFileHandler.ts +43 -11
- package/src/RaftMicroPythonConsoleClient.ts +78 -0
- package/src/RaftMsgHandler.ts +8 -4
- package/src/RaftTypes.ts +23 -0
- 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",
|
package/src/RaftConnector.ts
CHANGED
|
@@ -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]
|
|
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
|
-
|
|
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
|
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
|
};
|
|
@@ -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" },
|
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;
|
|
@@ -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)) {
|