@robotical/raftjs 2.1.3 → 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/RaftDeviceInfo.d.ts +5 -1
- package/dist/react-native/RaftDeviceManager.js +75 -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/RaftChannelSimulated.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.js +67 -0
- package/dist/web/RaftChannelSimulated.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/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/RaftChannelSimulated.test.ts +62 -0
- package/src/RaftChannelSimulated.ts +91 -0
- package/src/RaftDeviceInfo.ts +5 -1
- package/src/RaftDeviceManager.test.ts +35 -1
- package/src/RaftDeviceManager.ts +86 -33
|
@@ -148,10 +148,11 @@ const DeviceLineChart: React.FC<DeviceLineChartProps> = memo(({ deviceKey, lastU
|
|
|
148
148
|
min: axis.range[0],
|
|
149
149
|
max: axis.range[1],
|
|
150
150
|
callback: (value: number) => {
|
|
151
|
-
if (typeof value
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return value;
|
|
151
|
+
if (typeof value !== 'number') return value;
|
|
152
|
+
if (value === 0) return '0';
|
|
153
|
+
const abs = Math.abs(value);
|
|
154
|
+
if (abs < 1) return Number(value.toPrecision(2)).toString();
|
|
155
|
+
return Number(value.toPrecision(6)).toString();
|
|
155
156
|
},
|
|
156
157
|
},
|
|
157
158
|
};
|
|
@@ -173,6 +173,9 @@ const DevicePanel = ({ deviceKey, lastUpdated }: DevicePanelProps) => {
|
|
|
173
173
|
if (bracketsAdded) {
|
|
174
174
|
headerText += `)`;
|
|
175
175
|
}
|
|
176
|
+
if (deviceState?.deviceTypeInfo?.role?.toLowerCase() === "system") {
|
|
177
|
+
headerText += " System";
|
|
178
|
+
}
|
|
176
179
|
if (deviceState?.onlineState !== DeviceOnlineState.Online) {
|
|
177
180
|
headerText += " (Offline)";
|
|
178
181
|
}
|
package/package.json
CHANGED
|
@@ -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;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import AttributeHandler from "./RaftAttributeHandler";
|
|
2
|
+
import RaftChannelSimulated from "./RaftChannelSimulated";
|
|
3
|
+
import { DeviceAttributesState, DeviceTimeline } from "./RaftDeviceStates";
|
|
4
|
+
|
|
5
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
6
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
7
|
+
for (let idx = 0; idx < bytes.length; idx++) {
|
|
8
|
+
bytes[idx] = parseInt(hex.slice(idx * 2, idx * 2 + 2), 16);
|
|
9
|
+
}
|
|
10
|
+
return bytes;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createTimeline(): DeviceTimeline {
|
|
14
|
+
return {
|
|
15
|
+
timestampsUs: [],
|
|
16
|
+
lastReportTimestampUs: 0,
|
|
17
|
+
reportTimestampOffsetUs: 0,
|
|
18
|
+
totalSamplesAdded: 0,
|
|
19
|
+
emaLastSampleTimeUs: 0,
|
|
20
|
+
emaIntervalUs: 0,
|
|
21
|
+
emaPrevPollTimeUs: 0,
|
|
22
|
+
emaCalibrated: false,
|
|
23
|
+
emaCalibrationPolls: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("RaftChannelSimulated", () => {
|
|
28
|
+
test("generates decodable SCD30 CO2, temperature and humidity data", () => {
|
|
29
|
+
const channel = new RaftChannelSimulated() as any;
|
|
30
|
+
const deviceTypeInfo = channel._deviceTypeInfo.SCD30;
|
|
31
|
+
expect(deviceTypeInfo).toBeDefined();
|
|
32
|
+
|
|
33
|
+
const msg = channel._createSimulatedDeviceInfoMsg(1000, "SCD30", deviceTypeInfo, 2500) as Uint8Array;
|
|
34
|
+
const publishedJson = JSON.parse(new TextDecoder().decode(msg.slice(2)));
|
|
35
|
+
const payload = hexToBytes(publishedJson["1"].SCD30.pub);
|
|
36
|
+
|
|
37
|
+
const attrs: DeviceAttributesState = {};
|
|
38
|
+
const timeline = createTimeline();
|
|
39
|
+
const handler = new AttributeHandler();
|
|
40
|
+
|
|
41
|
+
const nextIdx = handler.processMsgAttrGroup(payload, 0, timeline, deviceTypeInfo.resp, attrs, 100);
|
|
42
|
+
expect(nextIdx).toBe(26);
|
|
43
|
+
|
|
44
|
+
expect(attrs.CO2.values).toHaveLength(1);
|
|
45
|
+
expect(attrs.temperature.values).toHaveLength(1);
|
|
46
|
+
expect(attrs.humidity.values).toHaveLength(1);
|
|
47
|
+
|
|
48
|
+
const co2 = attrs.CO2.values[0] as number;
|
|
49
|
+
const temperature = attrs.temperature.values[0] as number;
|
|
50
|
+
const humidity = attrs.humidity.values[0] as number;
|
|
51
|
+
|
|
52
|
+
expect(Number.isFinite(co2)).toBe(true);
|
|
53
|
+
expect(Number.isFinite(temperature)).toBe(true);
|
|
54
|
+
expect(Number.isFinite(humidity)).toBe(true);
|
|
55
|
+
expect(co2).toBeGreaterThanOrEqual(400);
|
|
56
|
+
expect(co2).toBeLessThanOrEqual(2000);
|
|
57
|
+
expect(temperature).toBeGreaterThanOrEqual(-40);
|
|
58
|
+
expect(temperature).toBeLessThanOrEqual(125);
|
|
59
|
+
expect(humidity).toBeGreaterThanOrEqual(0);
|
|
60
|
+
expect(humidity).toBeLessThanOrEqual(100);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -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/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" },
|