@robotical/raftjs 2.1.0 → 2.1.2
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/devbin-backwards-compatibility.md +105 -0
- package/devdocs/pseudocode-to-js-transpiler.md +563 -0
- package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
- package/dist/react-native/PseudocodeTranspiler.js +115 -0
- package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
- package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
- package/dist/react-native/RaftAttributeHandler.js +108 -32
- package/dist/react-native/RaftAttributeHandler.js.map +1 -1
- package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
- package/dist/react-native/RaftChannelBLE.web.js +59 -21
- package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
- package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
- package/dist/react-native/RaftChannelSimulated.js +9 -5
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSocket.js +16 -1
- package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
- package/dist/react-native/RaftConnector.d.ts +11 -1
- package/dist/react-native/RaftConnector.js +75 -9
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/react-native/RaftCustomAttrHandler.js +32 -44
- package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
- package/dist/react-native/RaftDeviceInfo.js +8 -0
- package/dist/react-native/RaftDeviceInfo.js.map +1 -1
- package/dist/react-native/RaftDeviceManager.d.ts +30 -3
- package/dist/react-native/RaftDeviceManager.js +618 -107
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/react-native/RaftDeviceStates.d.ts +27 -3
- package/dist/react-native/RaftDeviceStates.js +31 -6
- package/dist/react-native/RaftDeviceStates.js.map +1 -1
- package/dist/react-native/RaftFileHandler.d.ts +0 -1
- package/dist/react-native/RaftFileHandler.js +61 -23
- package/dist/react-native/RaftFileHandler.js.map +1 -1
- package/dist/react-native/RaftPublish.d.ts +2 -0
- package/dist/react-native/RaftPublish.js +81 -0
- package/dist/react-native/RaftPublish.js.map +1 -0
- package/dist/react-native/RaftStreamHandler.d.ts +11 -0
- package/dist/react-native/RaftStreamHandler.js +66 -0
- package/dist/react-native/RaftStreamHandler.js.map +1 -1
- package/dist/react-native/RaftStruct.d.ts +2 -2
- package/dist/react-native/RaftStruct.js +97 -26
- package/dist/react-native/RaftStruct.js.map +1 -1
- package/dist/react-native/RaftSystemType.d.ts +1 -0
- package/dist/react-native/RaftSystemUtils.d.ts +17 -1
- package/dist/react-native/RaftSystemUtils.js +51 -0
- package/dist/react-native/RaftSystemUtils.js.map +1 -1
- package/dist/react-native/RaftTimezone.d.ts +16 -0
- package/dist/react-native/RaftTimezone.js +153 -0
- package/dist/react-native/RaftTimezone.js.map +1 -0
- package/dist/react-native/RaftTypes.d.ts +27 -1
- package/dist/react-native/RaftTypes.js.map +1 -1
- package/dist/react-native/RaftUpdateManager.js +1 -1
- package/dist/react-native/RaftUpdateManager.js.map +1 -1
- package/dist/react-native/main.d.ts +2 -0
- package/dist/react-native/main.js +6 -1
- package/dist/react-native/main.js.map +1 -1
- package/dist/web/PseudocodeTranspiler.d.ts +6 -0
- package/dist/web/PseudocodeTranspiler.js +115 -0
- package/dist/web/PseudocodeTranspiler.js.map +1 -0
- package/dist/web/RaftAttributeHandler.d.ts +1 -1
- package/dist/web/RaftAttributeHandler.js +108 -32
- package/dist/web/RaftAttributeHandler.js.map +1 -1
- package/dist/web/RaftChannelBLE.web.d.ts +4 -0
- package/dist/web/RaftChannelBLE.web.js +59 -21
- package/dist/web/RaftChannelBLE.web.js.map +1 -1
- package/dist/web/RaftChannelSimulated.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.js +9 -5
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSocket.js +16 -1
- package/dist/web/RaftChannelWebSocket.js.map +1 -1
- package/dist/web/RaftConnector.d.ts +11 -1
- package/dist/web/RaftConnector.js +75 -9
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/web/RaftCustomAttrHandler.js +32 -44
- package/dist/web/RaftCustomAttrHandler.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +18 -0
- package/dist/web/RaftDeviceInfo.js +8 -0
- package/dist/web/RaftDeviceInfo.js.map +1 -1
- package/dist/web/RaftDeviceManager.d.ts +30 -3
- package/dist/web/RaftDeviceManager.js +618 -107
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/web/RaftDeviceStates.d.ts +27 -3
- package/dist/web/RaftDeviceStates.js +31 -6
- package/dist/web/RaftDeviceStates.js.map +1 -1
- package/dist/web/RaftFileHandler.d.ts +0 -1
- package/dist/web/RaftFileHandler.js +61 -23
- package/dist/web/RaftFileHandler.js.map +1 -1
- package/dist/web/RaftPublish.d.ts +2 -0
- package/dist/web/RaftPublish.js +81 -0
- package/dist/web/RaftPublish.js.map +1 -0
- package/dist/web/RaftStreamHandler.d.ts +11 -0
- package/dist/web/RaftStreamHandler.js +66 -0
- package/dist/web/RaftStreamHandler.js.map +1 -1
- package/dist/web/RaftStruct.d.ts +2 -2
- package/dist/web/RaftStruct.js +97 -26
- package/dist/web/RaftStruct.js.map +1 -1
- package/dist/web/RaftSystemType.d.ts +1 -0
- package/dist/web/RaftSystemUtils.d.ts +17 -1
- package/dist/web/RaftSystemUtils.js +51 -0
- package/dist/web/RaftSystemUtils.js.map +1 -1
- package/dist/web/RaftTimezone.d.ts +16 -0
- package/dist/web/RaftTimezone.js +153 -0
- package/dist/web/RaftTimezone.js.map +1 -0
- package/dist/web/RaftTypes.d.ts +27 -1
- package/dist/web/RaftTypes.js.map +1 -1
- package/dist/web/RaftUpdateManager.js +1 -1
- package/dist/web/RaftUpdateManager.js.map +1 -1
- package/dist/web/main.d.ts +2 -0
- package/dist/web/main.js +6 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
- package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
- package/examples/dashboard/src/DevicePanel.tsx +92 -11
- package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
- package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
- package/examples/dashboard/src/DevicesPanel.tsx +11 -0
- package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
- package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
- package/examples/dashboard/src/LoggingPanel.tsx +264 -0
- package/examples/dashboard/src/Main.tsx +12 -2
- package/examples/dashboard/src/SettingsScreen.tsx +9 -4
- package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
- package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
- package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
- package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
- package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
- package/examples/dashboard/src/styles.css +766 -1
- package/notes/web-ble-reconnect-retry.md +69 -0
- package/package.json +10 -7
- package/src/PseudocodeTranspiler.test.ts +372 -0
- package/src/PseudocodeTranspiler.ts +127 -0
- package/src/RaftAttributeHandler.ts +152 -76
- package/src/RaftChannelBLE.web.ts +62 -20
- package/src/RaftChannelSimulated.ts +10 -5
- package/src/RaftChannelWebSocket.ts +16 -2
- package/src/RaftConnector.ts +93 -15
- package/src/RaftCustomAttrHandler.ts +35 -45
- package/src/RaftDeviceInfo.ts +27 -0
- package/src/RaftDeviceManager.test.ts +164 -0
- package/src/RaftDeviceManager.ts +705 -127
- package/src/RaftDeviceMgrIF.ts +13 -2
- package/src/RaftDeviceStates.ts +49 -8
- package/src/RaftFileHandler.ts +69 -28
- package/src/RaftPublish.ts +92 -0
- package/src/RaftStreamHandler.ts +84 -1
- package/src/RaftStruct.test.ts +229 -0
- package/src/RaftStruct.ts +101 -37
- package/src/RaftSystemType.ts +1 -0
- package/src/RaftSystemUtils.ts +59 -0
- package/src/RaftTimezone.ts +151 -0
- package/src/RaftTypes.ts +34 -1
- package/src/RaftUpdateManager.ts +1 -1
- package/src/main.ts +2 -0
package/src/RaftDeviceManager.ts
CHANGED
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
9
9
|
|
|
10
|
-
import { DeviceAttributeState, DeviceAttributesState, DevicesState, DeviceState, getDeviceKey } from "./RaftDeviceStates";
|
|
10
|
+
import { DeviceAttributeState, DeviceAttributesState, DevicesState, DeviceState, DeviceStats, DeviceOnlineState, formatDeviceAddrHex, getDeviceKey, parseDeviceKey } from "./RaftDeviceStates";
|
|
11
11
|
import { DeviceMsgJson } from "./RaftDeviceMsg";
|
|
12
12
|
import { RaftOKFail } from './RaftTypes';
|
|
13
|
-
import { DeviceTypeInfo, DeviceTypeAction, DeviceTypeInfoRecs, RaftDevTypeInfoResponse } from "./RaftDeviceInfo";
|
|
13
|
+
import { DeviceTypeInfo, DeviceTypeAction, DeviceTypeInfoRecs, DeviceTypePollRespMetadata, RaftDevTypeInfoResponse, SampleRateResult, getActionMapHex } from "./RaftDeviceInfo";
|
|
14
14
|
import AttributeHandler from "./RaftAttributeHandler";
|
|
15
15
|
import RaftSystemUtils from "./RaftSystemUtils";
|
|
16
16
|
import RaftDeviceMgrIF from "./RaftDeviceMgrIF";
|
|
17
|
-
import { structPack } from "./RaftStruct";
|
|
17
|
+
import { structPack, structSizeOf } from "./RaftStruct";
|
|
18
18
|
// import RaftUtils from "./RaftUtils";
|
|
19
19
|
|
|
20
20
|
export interface DeviceDecodedData {
|
|
@@ -23,12 +23,18 @@ export interface DeviceDecodedData {
|
|
|
23
23
|
deviceAddress: string;
|
|
24
24
|
deviceType: string;
|
|
25
25
|
attrGroupName?: string;
|
|
26
|
-
attrValues: Record<string, number[]>;
|
|
26
|
+
attrValues: Record<string, (number | string)[]>;
|
|
27
27
|
timestampsUs: number[];
|
|
28
28
|
markers?: Record<string, unknown>;
|
|
29
29
|
fromOfflineBuffer?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
interface DeviceStatsInternal extends DeviceStats {
|
|
33
|
+
windowEvents: Array<{ timeMs: number; samples: number }>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type BinaryRecordPayloadFormat = "lengthPrefixed" | "legacyRaw";
|
|
37
|
+
|
|
32
38
|
export class DeviceManager implements RaftDeviceMgrIF{
|
|
33
39
|
|
|
34
40
|
// Max data points to store
|
|
@@ -58,10 +64,16 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
58
64
|
private _newDeviceAttributeCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
|
|
59
65
|
private _newAttributeDataCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
|
|
60
66
|
private _decodedDataCallbacks: Array<(decoded: DeviceDecodedData) => void> = [];
|
|
67
|
+
private _deviceRemovedCallbacks: Array<(deviceKey: string, state: DeviceState) => void> = [];
|
|
61
68
|
|
|
62
69
|
// Debug message index (to help debug with async messages)
|
|
63
70
|
private _debugMsgIndex = 0;
|
|
64
71
|
|
|
72
|
+
// Device stats (sample counts, rates)
|
|
73
|
+
private _statsWindowMs = 5000;
|
|
74
|
+
private _deviceStats: { [deviceKey: string]: DeviceStatsInternal } = {};
|
|
75
|
+
private _malformedSampleWarnLastMs: { [warningKey: string]: number } = {};
|
|
76
|
+
|
|
65
77
|
public getDevicesState(): DevicesState {
|
|
66
78
|
return this._devicesState;
|
|
67
79
|
}
|
|
@@ -70,6 +82,14 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
70
82
|
return this._devicesState[deviceKey];
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
public getDeviceStats(deviceKey: string): DeviceStats {
|
|
86
|
+
return this.cloneDeviceStats(this.getOrCreateDeviceStats(deviceKey));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public resetDeviceStats(deviceKey: string): void {
|
|
90
|
+
this._deviceStats[deviceKey] = this.createEmptyStats();
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
// Cached device type data
|
|
74
94
|
private _cachedDeviceTypeRecs: DeviceTypeInfoRecs = {};
|
|
75
95
|
|
|
@@ -169,6 +189,16 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
169
189
|
this._decodedDataCallbacks = this._decodedDataCallbacks.filter((cb) => cb !== callback);
|
|
170
190
|
}
|
|
171
191
|
|
|
192
|
+
public addDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
|
|
193
|
+
if (!this._deviceRemovedCallbacks.includes(callback)) {
|
|
194
|
+
this._deviceRemovedCallbacks.push(callback);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public removeDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
|
|
199
|
+
this._deviceRemovedCallbacks = this._deviceRemovedCallbacks.filter((cb) => cb !== callback);
|
|
200
|
+
}
|
|
201
|
+
|
|
172
202
|
////////////////////////////////////////////////////////////////////////////
|
|
173
203
|
// Set the friendly name for the device
|
|
174
204
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -185,93 +215,205 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
185
215
|
public async handleClientMsgBinary(rxMsg: Uint8Array) {
|
|
186
216
|
// console.log(`DeviceManager client1 msg ${RaftUtils.bufferToHex(rxMsg)}`);
|
|
187
217
|
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
218
|
+
// DevBIN message format
|
|
219
|
+
//
|
|
220
|
+
// The rxMsg passed to this function has a 2-byte message type prefix (e.g. 0x0080)
|
|
221
|
+
// added by the transport layer. After that prefix comes a devbin frame:
|
|
222
|
+
//
|
|
223
|
+
// Current devbin envelope (3 bytes):
|
|
224
|
+
// Byte 0: magic+version 0xDB (valid range 0xDB–0xDF)
|
|
225
|
+
// Byte 1: topicIndex 0x00–0xFE = topic index; 0xFF = no topic
|
|
226
|
+
// Byte 2: envelopeSeqNum uint8, wrapping — detects whole-frame drops
|
|
227
|
+
//
|
|
228
|
+
// Then zero or more per-device records, concatenated back-to-back:
|
|
229
|
+
// Bytes 0-1: recordLen uint16 big-endian — number of body bytes that follow (min 8)
|
|
230
|
+
// Byte 2: statusBus bit 7 = online flag, bit 6 = pending deletion, bits 3:0 = bus number
|
|
231
|
+
// Bytes 3-6: address uint32 big-endian — device address on the bus
|
|
232
|
+
// Bytes 7-8: devTypeIdx uint16 big-endian — device type table index
|
|
233
|
+
// Byte 9: deviceSeqNum uint8, wrapping — per-device drop detection
|
|
234
|
+
// Bytes 10+: samples length-prefixed: [sampleLen(1B)][sampleData(sampleLen B)] × N
|
|
235
|
+
//
|
|
236
|
+
// Backwards compatibility:
|
|
237
|
+
// Cog v1.9.5 is already in production and sends the older RaftCore devbin layout:
|
|
238
|
+
// no 3-byte envelope, no deviceSeqNum byte, and raw fixed-size samples
|
|
239
|
+
// [timestamp(2B)][payload] × N. Keep that path separate so current Axiom/Cog
|
|
240
|
+
// frames continue to use the length-prefixed parser above.
|
|
241
|
+
//
|
|
242
|
+
// Example message (two device records; first record has two samples):
|
|
243
|
+
// 0080 DB 01 07 0018 81 0000076a 000b 2a 07feff0000010008 07185707931400 01 000e 80 00000000 001f 05 05030001af01
|
|
244
|
+
// ^^^^ ^^^^
|
|
245
|
+
// | ^^ ^^ ^^ Record 2 ...
|
|
246
|
+
// | | | envelopeSeqNum = 0x07 (same layout as Record 1)
|
|
247
|
+
// | | topicIndex = 0x01
|
|
248
|
+
// | magic+version = 0xDB (devbin v1)
|
|
249
|
+
// msgType prefix (transport layer)
|
|
250
|
+
//
|
|
251
|
+
// Record 1 breakdown:
|
|
252
|
+
// 0018 recordLen = 24 body bytes follow
|
|
253
|
+
// 81 statusBus: online=1, pendDel=0, bus=1
|
|
254
|
+
// 0000076a address = 0x0000076A (slot 7, I2C addr 0x6A)
|
|
255
|
+
// 000b devTypeIdx = 11
|
|
256
|
+
// 2a deviceSeqNum = 42
|
|
257
|
+
// 07 feff0000010008 sample 1: sampleLen=7, 7 bytes of attribute data
|
|
258
|
+
// 07 18570793140001 sample 2: sampleLen=7, 7 bytes of attribute data
|
|
259
|
+
//
|
|
260
|
+
// Record 2 breakdown:
|
|
261
|
+
// 000e recordLen = 14 body bytes follow
|
|
262
|
+
// 80 statusBus: online=1, pendDel=0, bus=0
|
|
263
|
+
// 00000000 address = 0x00000000 (direct-connect)
|
|
264
|
+
// 001f devTypeIdx = 31
|
|
265
|
+
// 05 deviceSeqNum = 5
|
|
266
|
+
// 05 030001af01 sample 1: sampleLen=5, 5 bytes of attribute data
|
|
205
267
|
|
|
206
268
|
// Debug
|
|
207
269
|
// const debugMsgTime = Date.now();
|
|
208
270
|
const debugMsgIndex = this._debugMsgIndex++;
|
|
209
271
|
|
|
210
|
-
// Message layout
|
|
211
|
-
const msgTypeLen = 2; //
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
272
|
+
// Message layout constants
|
|
273
|
+
const msgTypeLen = 2; // Transport-layer message type prefix (first two bytes, e.g. 0x0080)
|
|
274
|
+
const devbinEnvelopeLen = 3; // Devbin envelope: magic+version (1B) + topicIndex (1B) + envelopeSeqNum (1B)
|
|
275
|
+
const legacyDevbinEnvelopeLen = 2; // Intermediate/legacy envelope: magic+version (1B) + topicIndex (1B)
|
|
276
|
+
const devbinMagicMin = 0xDB;
|
|
277
|
+
const devbinMagicMax = 0xDF;
|
|
278
|
+
const recordLenLen = 2; // Per-record length prefix (uint16 big-endian)
|
|
279
|
+
const busInfoLen = 1; // statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
|
|
280
|
+
const deviceAddrLen = 4; // Device address (uint32 big-endian)
|
|
281
|
+
const devTypeIdxLen = 2; // Device type index (uint16 big-endian)
|
|
282
|
+
const deviceSeqNumLen = 1; // Per-device sequence counter
|
|
283
|
+
const currentRecordHeaderLen = busInfoLen + deviceAddrLen + devTypeIdxLen + deviceSeqNumLen; // = 8, minimum record body
|
|
284
|
+
const legacyRecordHeaderLen = busInfoLen + deviceAddrLen + devTypeIdxLen; // = 7, Cog v1.9.5 record body header
|
|
217
285
|
|
|
218
286
|
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} rxMsg.length ${rxMsg.length} rxMsg ${RaftUtils.bufferToHex(rxMsg)}`);
|
|
219
287
|
|
|
220
288
|
// Start after the message type
|
|
221
289
|
let msgPos = msgTypeLen;
|
|
290
|
+
let payloadFormat: BinaryRecordPayloadFormat = "legacyRaw";
|
|
291
|
+
|
|
292
|
+
// Check for devbin envelope (magic+version + topicIndex)
|
|
293
|
+
if (rxMsg.length >= msgTypeLen + legacyDevbinEnvelopeLen) {
|
|
294
|
+
const envelopeMagicVer = rxMsg[msgTypeLen];
|
|
295
|
+
if ((envelopeMagicVer & 0xF0) === 0xD0) {
|
|
296
|
+
if ((envelopeMagicVer < devbinMagicMin) || (envelopeMagicVer > devbinMagicMax)) {
|
|
297
|
+
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid devbin envelope magic/version ${envelopeMagicVer}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
222
300
|
|
|
223
|
-
|
|
301
|
+
const topicIndex = rxMsg[msgTypeLen + 1];
|
|
302
|
+
if (topicIndex !== 0xFF) {
|
|
303
|
+
const topicName = this._systemUtils?.getPublishTopicName(topicIndex);
|
|
304
|
+
if (topicName && topicName !== "devbin") {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const currentMsgPos = msgTypeLen + devbinEnvelopeLen;
|
|
310
|
+
const legacyMsgPos = msgTypeLen + legacyDevbinEnvelopeLen;
|
|
311
|
+
if (this.hasValidRecordAt(rxMsg, currentMsgPos, recordLenLen, currentRecordHeaderLen)) {
|
|
312
|
+
msgPos = currentMsgPos;
|
|
313
|
+
payloadFormat = "lengthPrefixed";
|
|
314
|
+
} else if (this.hasValidRecordAt(rxMsg, legacyMsgPos, recordLenLen, legacyRecordHeaderLen)) {
|
|
315
|
+
msgPos = legacyMsgPos;
|
|
316
|
+
payloadFormat = "legacyRaw";
|
|
317
|
+
} else {
|
|
318
|
+
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid devbin envelope payload`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else if (this.hasValidRecordAt(rxMsg, msgPos, recordLenLen, currentRecordHeaderLen)) {
|
|
323
|
+
payloadFormat = "lengthPrefixed";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Iterate through device records
|
|
224
327
|
while (msgPos < rxMsg.length) {
|
|
225
328
|
|
|
226
|
-
// Check length
|
|
329
|
+
// Check minimum length for record length prefix + record header
|
|
227
330
|
const remainingLen = rxMsg.length - msgPos;
|
|
228
|
-
if (remainingLen <
|
|
229
|
-
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid length ${rxMsg.length} < ${
|
|
331
|
+
if (remainingLen < recordLenLen + legacyRecordHeaderLen) {
|
|
332
|
+
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid length ${rxMsg.length} < ${recordLenLen + legacyRecordHeaderLen + msgPos}`);
|
|
230
333
|
return;
|
|
231
334
|
}
|
|
232
335
|
|
|
233
|
-
// Get the length
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
236
|
-
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos}
|
|
336
|
+
// Get the record body length (bytes that follow the 2-byte length prefix)
|
|
337
|
+
const recordLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
|
|
338
|
+
if ((recordLen < legacyRecordHeaderLen) || (recordLen > remainingLen - recordLenLen)) {
|
|
339
|
+
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} recordLen ${recordLen} remainingAfterLenBytes ${remainingLen - recordLenLen}`);
|
|
237
340
|
return;
|
|
238
341
|
}
|
|
239
342
|
|
|
240
|
-
// Extract
|
|
241
|
-
let
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
343
|
+
// Extract record header fields
|
|
344
|
+
let recordPos = msgPos + recordLenLen;
|
|
345
|
+
|
|
346
|
+
// statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
|
|
347
|
+
const statusByte = rxMsg[recordPos];
|
|
348
|
+
const busNum = statusByte & 0x0f;
|
|
349
|
+
const isOnline = (statusByte & 0x80) !== 0;
|
|
350
|
+
recordPos += busInfoLen;
|
|
351
|
+
|
|
352
|
+
// Device address (uint32 big-endian)
|
|
353
|
+
const devAddr = (rxMsg[recordPos] << 24) + (rxMsg[recordPos + 1] << 16) + (rxMsg[recordPos + 2] << 8) + rxMsg[recordPos + 3];
|
|
354
|
+
recordPos += deviceAddrLen;
|
|
355
|
+
|
|
356
|
+
// Device type index (uint16 big-endian)
|
|
357
|
+
const devTypeIdx = (rxMsg[recordPos] << 8) + rxMsg[recordPos + 1];
|
|
358
|
+
recordPos += devTypeIdxLen;
|
|
359
|
+
|
|
360
|
+
const commonRecordHeaderEndPos = recordPos;
|
|
361
|
+
const samplesEndPos = msgPos + recordLenLen + recordLen;
|
|
362
|
+
let recordPayloadFormat = payloadFormat;
|
|
363
|
+
let recordHeaderLen = recordPayloadFormat === "lengthPrefixed" ? currentRecordHeaderLen : legacyRecordHeaderLen;
|
|
364
|
+
const resolvedDeviceTypeInfo = await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
|
|
365
|
+
if (resolvedDeviceTypeInfo?.resp) {
|
|
366
|
+
recordPayloadFormat = this.resolveRecordPayloadFormat(rxMsg, commonRecordHeaderEndPos,
|
|
367
|
+
samplesEndPos, resolvedDeviceTypeInfo.resp, recordPayloadFormat, deviceSeqNumLen);
|
|
368
|
+
recordHeaderLen = recordPayloadFormat === "lengthPrefixed" ? currentRecordHeaderLen : legacyRecordHeaderLen;
|
|
369
|
+
}
|
|
370
|
+
if (recordLen < recordHeaderLen) {
|
|
371
|
+
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} recordLen ${recordLen} recordHeaderLen ${recordHeaderLen}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const isPendingDeletion = (recordPayloadFormat === "lengthPrefixed") && ((statusByte & 0x40) !== 0);
|
|
376
|
+
recordPos = commonRecordHeaderEndPos;
|
|
377
|
+
if (recordPayloadFormat === "lengthPrefixed") {
|
|
378
|
+
// Per-device sequence counter (reserved for future drop detection)
|
|
379
|
+
// const deviceSeqNum = rxMsg[recordPos];
|
|
380
|
+
recordPos += deviceSeqNumLen;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let pollDataPos = recordPos;
|
|
250
384
|
|
|
251
385
|
// Debug
|
|
252
|
-
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length}
|
|
253
|
-
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex}
|
|
386
|
+
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length} recordStart ${msgPos} recordLen ${recordLen} ${pollDataPos} ${RaftUtils.bufferToHex(rxMsg.slice(msgPos, msgPos + recordLenLen + recordLen))}`);
|
|
387
|
+
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} bus ${busNum} isOnline ${isOnline} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} pollDataLen ${recordLen - recordHeaderLen}`);
|
|
254
388
|
|
|
255
|
-
//
|
|
256
|
-
const
|
|
389
|
+
// Format device address as canonical hex and build device key
|
|
390
|
+
const devAddrHex = formatDeviceAddrHex(devAddr);
|
|
391
|
+
const deviceKey = this.getBinaryDeviceKey(busNum, devAddrHex, devTypeIdx, recordPayloadFormat);
|
|
257
392
|
|
|
258
393
|
// Update the last update time
|
|
259
394
|
this._deviceLastUpdateTime[deviceKey] = Date.now();
|
|
260
395
|
|
|
396
|
+
// Handle pending deletion - remove device and skip further processing
|
|
397
|
+
if (isPendingDeletion) {
|
|
398
|
+
this.removeDevice(deviceKey);
|
|
399
|
+
msgPos += recordLenLen + recordLen;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
261
403
|
// Check if a device state already exists
|
|
262
404
|
if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
|
|
263
405
|
|
|
264
406
|
// Get the device type info
|
|
265
|
-
const deviceTypeInfo = await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
|
|
407
|
+
const deviceTypeInfo = resolvedDeviceTypeInfo ?? await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
|
|
266
408
|
|
|
267
409
|
// Debug
|
|
268
|
-
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex}
|
|
410
|
+
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} busNum ${busNum} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} deviceTypeInfo ${JSON.stringify(deviceTypeInfo)}`);
|
|
269
411
|
|
|
270
412
|
// Handle case where device type info is not available
|
|
271
413
|
if (deviceTypeInfo === undefined) {
|
|
272
|
-
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this
|
|
273
|
-
// Skip to next
|
|
274
|
-
msgPos +=
|
|
414
|
+
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this record`);
|
|
415
|
+
// Skip to next record without processing attributes
|
|
416
|
+
msgPos += recordLenLen + recordLen;
|
|
275
417
|
continue;
|
|
276
418
|
}
|
|
277
419
|
|
|
@@ -281,7 +423,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
281
423
|
this._devicesState[deviceKey].deviceTypeInfo = deviceTypeInfo;
|
|
282
424
|
this._devicesState[deviceKey].deviceType = deviceTypeInfo.name || "";
|
|
283
425
|
this._devicesState[deviceKey].busName = busNum.toString();
|
|
284
|
-
this._devicesState[deviceKey].deviceAddress =
|
|
426
|
+
this._devicesState[deviceKey].deviceAddress = devAddrHex;
|
|
285
427
|
}
|
|
286
428
|
} else {
|
|
287
429
|
// Create device record - device type info may be undefined
|
|
@@ -290,13 +432,19 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
290
432
|
deviceTimeline: {
|
|
291
433
|
timestampsUs: [],
|
|
292
434
|
lastReportTimestampUs: 0,
|
|
293
|
-
reportTimestampOffsetUs: 0
|
|
435
|
+
reportTimestampOffsetUs: 0,
|
|
436
|
+
totalSamplesAdded: 0,
|
|
437
|
+
emaLastSampleTimeUs: 0,
|
|
438
|
+
emaIntervalUs: 0,
|
|
439
|
+
emaPrevPollTimeUs: 0,
|
|
440
|
+
emaCalibrated: false,
|
|
441
|
+
emaCalibrationPolls: 0
|
|
294
442
|
},
|
|
295
443
|
deviceAttributes: {},
|
|
296
444
|
deviceIsNew: true,
|
|
297
445
|
stateChanged: false,
|
|
298
|
-
|
|
299
|
-
deviceAddress:
|
|
446
|
+
onlineState: DeviceOnlineState.Online,
|
|
447
|
+
deviceAddress: devAddrHex,
|
|
300
448
|
deviceType: deviceTypeInfo?.name || "",
|
|
301
449
|
busName: busNum.toString()
|
|
302
450
|
};
|
|
@@ -305,7 +453,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
305
453
|
|
|
306
454
|
// Get device state
|
|
307
455
|
const deviceState = this._devicesState[deviceKey];
|
|
308
|
-
deviceState.
|
|
456
|
+
deviceState.onlineState = isOnline ? DeviceOnlineState.Online : DeviceOnlineState.Offline;
|
|
309
457
|
|
|
310
458
|
// Check if device type info is available and complete
|
|
311
459
|
if (deviceState.deviceTypeInfo && deviceState.deviceTypeInfo.resp) {
|
|
@@ -313,59 +461,106 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
313
461
|
// Iterate over attributes in the group
|
|
314
462
|
const pollRespMetadata = deviceState.deviceTypeInfo!.resp!;
|
|
315
463
|
|
|
316
|
-
//
|
|
317
|
-
const attrGroupDataLen = sectionLen - sectionHeaderLen;
|
|
318
|
-
const attrGroupStartPos = attrGroupPos;
|
|
464
|
+
// Process samples within this record
|
|
319
465
|
const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
|
|
320
466
|
const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (attrGroupPos >= rxMsg.length) {
|
|
325
|
-
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} exceeds message length ${rxMsg.length}`);
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, attrGroupPos,
|
|
330
|
-
deviceState.deviceTimeline, pollRespMetadata,
|
|
331
|
-
deviceState.deviceAttributes,
|
|
332
|
-
this._maxDatapointsToStore);
|
|
467
|
+
const totalSamplesBefore = deviceState.deviceTimeline.totalSamplesAdded;
|
|
468
|
+
if (recordPayloadFormat === "lengthPrefixed") {
|
|
469
|
+
while (pollDataPos < samplesEndPos) {
|
|
333
470
|
|
|
334
|
-
|
|
471
|
+
// Read sample length prefix
|
|
472
|
+
if (pollDataPos >= rxMsg.length) {
|
|
473
|
+
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} exceeds message length ${rxMsg.length}`);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
const sampleLen = rxMsg[pollDataPos];
|
|
477
|
+
pollDataPos += 1;
|
|
335
478
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Prevent infinite loops
|
|
343
|
-
if (newMsgBufIdx <= attrGroupPos) {
|
|
344
|
-
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup didn't advance position from ${attrGroupPos} to ${newMsgBufIdx}`);
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
479
|
+
if (sampleLen === 0 || pollDataPos + sampleLen > samplesEndPos) {
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
347
482
|
|
|
348
|
-
|
|
349
|
-
|
|
483
|
+
const sampleStartPos = pollDataPos;
|
|
484
|
+
const sampleEndPos = pollDataPos + sampleLen;
|
|
485
|
+
const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, sampleStartPos,
|
|
486
|
+
deviceState.deviceTimeline, pollRespMetadata,
|
|
487
|
+
deviceState.deviceAttributes,
|
|
488
|
+
this._maxDatapointsToStore,
|
|
489
|
+
sampleEndPos);
|
|
350
490
|
|
|
351
|
-
|
|
491
|
+
if (newMsgBufIdx < 0)
|
|
492
|
+
{
|
|
493
|
+
this.warnMalformedSample(
|
|
494
|
+
`${deviceKey}:${devTypeIdx}:lengthPrefixed`,
|
|
495
|
+
`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} skipped malformed sample ` +
|
|
496
|
+
`device=${deviceKey} devTypeIdx=${devTypeIdx} sampleLen=${sampleLen} respBytes=${pollRespMetadata.b}`
|
|
497
|
+
);
|
|
498
|
+
pollDataPos += sampleLen;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
352
501
|
|
|
502
|
+
// Advance by sampleLen regardless of how much processMsgAttrGroup consumed
|
|
503
|
+
pollDataPos += sampleLen;
|
|
504
|
+
deviceState.stateChanged = true;
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
const legacySampleLen = this.getLegacyRawSampleLen(pollRespMetadata);
|
|
508
|
+
if (legacySampleLen <= 0) {
|
|
509
|
+
this.warnMalformedSample(
|
|
510
|
+
`${deviceKey}:${devTypeIdx}:legacyRawLen`,
|
|
511
|
+
`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid legacy sample length ` +
|
|
512
|
+
`device=${deviceKey} devTypeIdx=${devTypeIdx} respBytes=${pollRespMetadata.b}`
|
|
513
|
+
);
|
|
514
|
+
} else {
|
|
515
|
+
while (pollDataPos + legacySampleLen <= samplesEndPos) {
|
|
516
|
+
const sampleStartPos = pollDataPos;
|
|
517
|
+
const sampleEndPos = pollDataPos + legacySampleLen;
|
|
518
|
+
const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, sampleStartPos,
|
|
519
|
+
deviceState.deviceTimeline, pollRespMetadata,
|
|
520
|
+
deviceState.deviceAttributes,
|
|
521
|
+
this._maxDatapointsToStore,
|
|
522
|
+
sampleEndPos);
|
|
523
|
+
|
|
524
|
+
if (newMsgBufIdx < 0)
|
|
525
|
+
{
|
|
526
|
+
this.warnMalformedSample(
|
|
527
|
+
`${deviceKey}:${devTypeIdx}:legacyRaw`,
|
|
528
|
+
`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} skipped malformed legacy sample ` +
|
|
529
|
+
`device=${deviceKey} devTypeIdx=${devTypeIdx} sampleLen=${legacySampleLen} respBytes=${pollRespMetadata.b}`
|
|
530
|
+
);
|
|
531
|
+
pollDataPos += legacySampleLen;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
pollDataPos += legacySampleLen;
|
|
536
|
+
deviceState.stateChanged = true;
|
|
537
|
+
}
|
|
353
538
|
|
|
354
|
-
|
|
539
|
+
if (pollDataPos < samplesEndPos) {
|
|
540
|
+
this.warnMalformedSample(
|
|
541
|
+
`${deviceKey}:${devTypeIdx}:legacyRawRemainder`,
|
|
542
|
+
`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} skipped trailing legacy sample bytes ` +
|
|
543
|
+
`device=${deviceKey} devTypeIdx=${devTypeIdx} remaining=${samplesEndPos - pollDataPos} sampleLen=${legacySampleLen} respBytes=${pollRespMetadata.b}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
355
547
|
}
|
|
356
548
|
|
|
357
549
|
// Inform decoded-data callbacks
|
|
358
|
-
this.emitDecodedData(deviceKey, busNum.toString(),
|
|
550
|
+
this.emitDecodedData(deviceKey, busNum.toString(), devAddrHex, deviceState,
|
|
359
551
|
pollRespMetadata, attrLengthsBefore, timelineLenBefore);
|
|
552
|
+
|
|
553
|
+
const newSamples = deviceState.deviceTimeline.totalSamplesAdded - totalSamplesBefore;
|
|
554
|
+
this.updateDeviceStats(deviceKey, newSamples, Date.now());
|
|
360
555
|
} else {
|
|
361
556
|
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceState incomplete for device ${deviceKey}, skipping attribute processing`);
|
|
362
557
|
}
|
|
363
558
|
|
|
364
559
|
// Debug
|
|
365
|
-
// console.log(`DevMan.handleClientMsgBinary
|
|
560
|
+
// console.log(`DevMan.handleClientMsgBinary record done debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} newMsgPos ${msgPos + recordLenLen + recordLen} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
|
|
366
561
|
|
|
367
|
-
//
|
|
368
|
-
msgPos +=
|
|
562
|
+
// Advance past this record (recordLenLen + recordLen bytes)
|
|
563
|
+
msgPos += recordLenLen + recordLen;
|
|
369
564
|
}
|
|
370
565
|
|
|
371
566
|
// Check for devices that have not been updated for a while
|
|
@@ -374,6 +569,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
374
569
|
Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
|
|
375
570
|
if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
|
|
376
571
|
delete this._devicesState[deviceKey];
|
|
572
|
+
delete this._deviceStats[deviceKey];
|
|
377
573
|
}
|
|
378
574
|
});
|
|
379
575
|
}
|
|
@@ -394,6 +590,11 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
394
590
|
// Iterate over the buses
|
|
395
591
|
Object.entries(data).forEach(([busName, devices]) => {
|
|
396
592
|
|
|
593
|
+
// Check the bus name doesn't start with _ which is reserved for non-device information such as topic name
|
|
594
|
+
if (busName.startsWith("_")) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
397
598
|
// Check for bus status info
|
|
398
599
|
if (devices && typeof devices === "object" && "_s" in devices) {
|
|
399
600
|
// console.log(`DeviceManager bus status ${JSON.stringify(devices._s)}`);
|
|
@@ -410,15 +611,20 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
410
611
|
|
|
411
612
|
// Device type name
|
|
412
613
|
let deviceTypeName = "";
|
|
614
|
+
let deviceTypeIdx = -1;
|
|
413
615
|
if (attrGroups && typeof attrGroups === 'object' && "_t" in attrGroups && typeof attrGroups._t === "string") {
|
|
414
616
|
deviceTypeName = attrGroups._t || "";
|
|
415
|
-
} else {
|
|
617
|
+
} else if (attrGroups && typeof attrGroups === 'object' && "_i" in attrGroups && typeof attrGroups._i === "number") {
|
|
618
|
+
deviceTypeIdx = attrGroups._i ?? -1;
|
|
619
|
+
deviceTypeName = deviceTypeIdx.toString();
|
|
620
|
+
} else
|
|
621
|
+
{
|
|
416
622
|
console.warn(`DeviceManager missing device type attrGroups ${JSON.stringify(attrGroups)}`);
|
|
417
623
|
return;
|
|
418
624
|
}
|
|
419
625
|
|
|
420
626
|
// Device key
|
|
421
|
-
const deviceKey = getDeviceKey(busName, devAddr
|
|
627
|
+
const deviceKey = getDeviceKey(busName, devAddr);
|
|
422
628
|
|
|
423
629
|
// Update the last update time
|
|
424
630
|
this._deviceLastUpdateTime[deviceKey] = Date.now();
|
|
@@ -444,12 +650,18 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
444
650
|
deviceTimeline: {
|
|
445
651
|
timestampsUs: [],
|
|
446
652
|
lastReportTimestampUs: 0,
|
|
447
|
-
reportTimestampOffsetUs: 0
|
|
653
|
+
reportTimestampOffsetUs: 0,
|
|
654
|
+
totalSamplesAdded: 0,
|
|
655
|
+
emaLastSampleTimeUs: 0,
|
|
656
|
+
emaIntervalUs: 0,
|
|
657
|
+
emaPrevPollTimeUs: 0,
|
|
658
|
+
emaCalibrated: false,
|
|
659
|
+
emaCalibrationPolls: 0
|
|
448
660
|
},
|
|
449
661
|
deviceAttributes: {},
|
|
450
662
|
deviceIsNew: true,
|
|
451
663
|
stateChanged: false,
|
|
452
|
-
|
|
664
|
+
onlineState: DeviceOnlineState.Online,
|
|
453
665
|
deviceAddress: devAddr,
|
|
454
666
|
deviceType: deviceTypeName,
|
|
455
667
|
busName: busName
|
|
@@ -460,9 +672,15 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
460
672
|
// Get device state
|
|
461
673
|
const deviceState = this._devicesState[deviceKey];
|
|
462
674
|
|
|
463
|
-
// Check for online/offline state information
|
|
675
|
+
// Check for online/offline/pending-deletion state information
|
|
464
676
|
if (attrGroups && typeof attrGroups === "object" && "_o" in attrGroups) {
|
|
465
|
-
|
|
677
|
+
const onlineStateVal = typeof attrGroups._o === 'number' ? attrGroups._o : parseInt(String(attrGroups._o), 10);
|
|
678
|
+
if (onlineStateVal === 2) {
|
|
679
|
+
// Pending deletion - remove device and skip further processing
|
|
680
|
+
this.removeDevice(deviceKey);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
deviceState.onlineState = onlineStateVal === 1 ? DeviceOnlineState.Online : DeviceOnlineState.Offline;
|
|
466
684
|
}
|
|
467
685
|
|
|
468
686
|
// Check if device type info is available
|
|
@@ -496,6 +714,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
496
714
|
|
|
497
715
|
const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
|
|
498
716
|
const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
|
|
717
|
+
const totalSamplesBefore = deviceState.deviceTimeline.totalSamplesAdded;
|
|
499
718
|
|
|
500
719
|
// Loop
|
|
501
720
|
while (msgBufIdx < msgBytes.length) {
|
|
@@ -512,6 +731,9 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
512
731
|
|
|
513
732
|
this.emitDecodedData(deviceKey, busName, devAddr, deviceState, pollRespMetadata,
|
|
514
733
|
attrLengthsBefore, timelineLenBefore, attrGroupName, markers);
|
|
734
|
+
|
|
735
|
+
const newSamples = deviceState.deviceTimeline.totalSamplesAdded - totalSamplesBefore;
|
|
736
|
+
this.updateDeviceStats(deviceKey, newSamples, Date.now());
|
|
515
737
|
});
|
|
516
738
|
});
|
|
517
739
|
});
|
|
@@ -522,6 +744,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
522
744
|
Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
|
|
523
745
|
if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
|
|
524
746
|
delete this._devicesState[deviceKey];
|
|
747
|
+
delete this._deviceStats[deviceKey];
|
|
525
748
|
}
|
|
526
749
|
});
|
|
527
750
|
}
|
|
@@ -559,6 +782,22 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
559
782
|
});
|
|
560
783
|
}
|
|
561
784
|
|
|
785
|
+
////////////////////////////////////////////////////////////////////////////
|
|
786
|
+
// Remove a device (e.g. on pending deletion)
|
|
787
|
+
////////////////////////////////////////////////////////////////////////////
|
|
788
|
+
|
|
789
|
+
private removeDevice(deviceKey: string): void {
|
|
790
|
+
// Snapshot the state before removal for callbacks
|
|
791
|
+
const deviceState = this._devicesState[deviceKey];
|
|
792
|
+
if (deviceState) {
|
|
793
|
+
deviceState.onlineState = DeviceOnlineState.PendingDeletion;
|
|
794
|
+
this._deviceRemovedCallbacks.forEach((cb) => cb(deviceKey, deviceState));
|
|
795
|
+
}
|
|
796
|
+
delete this._devicesState[deviceKey];
|
|
797
|
+
delete this._deviceLastUpdateTime[deviceKey];
|
|
798
|
+
delete this._deviceStats[deviceKey];
|
|
799
|
+
}
|
|
800
|
+
|
|
562
801
|
////////////////////////////////////////////////////////////////////////////
|
|
563
802
|
// Get device type info
|
|
564
803
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -637,6 +876,131 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
637
876
|
}
|
|
638
877
|
}
|
|
639
878
|
|
|
879
|
+
private hasValidRecordAt(rxMsg: Uint8Array, msgPos: number, recordLenLen: number, recordHeaderLen: number): boolean {
|
|
880
|
+
if (msgPos === rxMsg.length) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
if (msgPos < 0 || msgPos > rxMsg.length) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
const remainingLen = rxMsg.length - msgPos;
|
|
887
|
+
if (remainingLen < recordLenLen + recordHeaderLen) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
const recordLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
|
|
891
|
+
return (recordLen >= recordHeaderLen) && (recordLen <= remainingLen - recordLenLen);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private resolveRecordPayloadFormat(rxMsg: Uint8Array, commonRecordHeaderEndPos: number, samplesEndPos: number,
|
|
895
|
+
pollRespMetadata: DeviceTypePollRespMetadata, preferredFormat: BinaryRecordPayloadFormat,
|
|
896
|
+
deviceSeqNumLen: number): BinaryRecordPayloadFormat {
|
|
897
|
+
const lengthPrefixedStartPos = commonRecordHeaderEndPos + deviceSeqNumLen;
|
|
898
|
+
const lengthPrefixedValid = this.areLengthPrefixedSamplesValid(rxMsg, lengthPrefixedStartPos, samplesEndPos, pollRespMetadata);
|
|
899
|
+
const legacySampleLen = this.getLegacyRawSampleLen(pollRespMetadata);
|
|
900
|
+
const legacyRawValid = this.areLegacyRawSamplesValid(commonRecordHeaderEndPos, samplesEndPos, legacySampleLen);
|
|
901
|
+
|
|
902
|
+
if (legacyRawValid && !lengthPrefixedValid) {
|
|
903
|
+
return "legacyRaw";
|
|
904
|
+
}
|
|
905
|
+
if (lengthPrefixedValid && !legacyRawValid) {
|
|
906
|
+
return "lengthPrefixed";
|
|
907
|
+
}
|
|
908
|
+
if (lengthPrefixedValid && legacyRawValid) {
|
|
909
|
+
return preferredFormat;
|
|
910
|
+
}
|
|
911
|
+
return preferredFormat;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private areLengthPrefixedSamplesValid(rxMsg: Uint8Array, pollDataPos: number, samplesEndPos: number,
|
|
915
|
+
pollRespMetadata: DeviceTypePollRespMetadata): boolean {
|
|
916
|
+
if ((pollDataPos < 0) || (pollDataPos > samplesEndPos) || (samplesEndPos > rxMsg.length)) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
if (pollDataPos === samplesEndPos) {
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const fixedSampleLen = pollRespMetadata.c ? 0 : this.getLegacyRawSampleLen(pollRespMetadata);
|
|
924
|
+
let sampleCount = 0;
|
|
925
|
+
while (pollDataPos < samplesEndPos) {
|
|
926
|
+
const sampleLen = rxMsg[pollDataPos];
|
|
927
|
+
pollDataPos += 1;
|
|
928
|
+
if ((sampleLen === 0) || (pollDataPos + sampleLen > samplesEndPos)) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
if ((fixedSampleLen > 0) && (sampleLen !== fixedSampleLen)) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
pollDataPos += sampleLen;
|
|
935
|
+
sampleCount++;
|
|
936
|
+
}
|
|
937
|
+
return sampleCount > 0;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private areLegacyRawSamplesValid(pollDataPos: number, samplesEndPos: number, legacySampleLen: number): boolean {
|
|
941
|
+
if ((legacySampleLen <= 0) || (pollDataPos < 0) || (pollDataPos > samplesEndPos)) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
const payloadLen = samplesEndPos - pollDataPos;
|
|
945
|
+
return (payloadLen > 0) && (payloadLen % legacySampleLen === 0);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private getBinaryDeviceKey(busNum: number, devAddrHex: string, devTypeIdx: number, payloadFormat: BinaryRecordPayloadFormat): string {
|
|
949
|
+
const baseDeviceKey = getDeviceKey(busNum.toString(), devAddrHex);
|
|
950
|
+
if ((payloadFormat === "legacyRaw") && (busNum === 0) && (devAddrHex === "0")) {
|
|
951
|
+
return `${baseDeviceKey}_${devTypeIdx}`;
|
|
952
|
+
}
|
|
953
|
+
return baseDeviceKey;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private getLegacyRawSampleLen(pollRespMetadata: DeviceTypePollRespMetadata): number {
|
|
957
|
+
const legacyTimestampLen = 2;
|
|
958
|
+
return legacyTimestampLen + this.getPollRespPayloadSize(pollRespMetadata);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private getPollRespPayloadSize(pollRespMetadata: DeviceTypePollRespMetadata): number {
|
|
962
|
+
if (pollRespMetadata.c) {
|
|
963
|
+
return pollRespMetadata.b;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
let attrPayloadLen = 0;
|
|
967
|
+
for (const attrDef of pollRespMetadata.a) {
|
|
968
|
+
if (!attrDef.t) {
|
|
969
|
+
return pollRespMetadata.b;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
attrPayloadLen += structSizeOf(attrDef.t);
|
|
973
|
+
} catch {
|
|
974
|
+
return pollRespMetadata.b;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Cog v1.9.5 light metadata reports the direct-sensor payload size doubled,
|
|
979
|
+
// but the legacy raw record contains one fixed payload matching the attribute schema.
|
|
980
|
+
if ((attrPayloadLen > 0) && (pollRespMetadata.b > 0) && (attrPayloadLen <= pollRespMetadata.b)) {
|
|
981
|
+
return attrPayloadLen;
|
|
982
|
+
}
|
|
983
|
+
return pollRespMetadata.b;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
private parseDeviceKeyForCommand(deviceKey: string): { bus: string; addr: string } {
|
|
987
|
+
const deviceState = this._devicesState[deviceKey];
|
|
988
|
+
if (deviceState) {
|
|
989
|
+
return { bus: deviceState.busName, addr: deviceState.deviceAddress };
|
|
990
|
+
}
|
|
991
|
+
return parseDeviceKey(deviceKey);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private warnMalformedSample(warningKey: string, message: string): void {
|
|
995
|
+
const nowMs = Date.now();
|
|
996
|
+
const lastWarnMs = this._malformedSampleWarnLastMs[warningKey] ?? 0;
|
|
997
|
+
if (nowMs - lastWarnMs < 5000) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
this._malformedSampleWarnLastMs[warningKey] = nowMs;
|
|
1001
|
+
console.warn(message);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
640
1004
|
////////////////////////////////////////////////////////////////////////////
|
|
641
1005
|
// Send action to device
|
|
642
1006
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -648,42 +1012,77 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
648
1012
|
}
|
|
649
1013
|
|
|
650
1014
|
public async sendAction(deviceKey: string, action: DeviceTypeAction, data: number[]): Promise<boolean> {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
let writeBytes: Uint8Array;
|
|
1015
|
+
console.log(`DeviceManager sendAction ${deviceKey} action ${action.n} data ${data} map ${JSON.stringify(action.map)} keys ${action.map ? Object.keys(action.map) : 'none'}`);
|
|
654
1016
|
|
|
655
|
-
//
|
|
656
|
-
if (data.length === 1) {
|
|
1017
|
+
// For _conf.* actions, delegate to setSampleRate() which coordinates polling params
|
|
1018
|
+
if (action.n.startsWith('_conf.') && action.map && data.length === 1) {
|
|
1019
|
+
const result = await this.setSampleRate(deviceKey, data[0]);
|
|
1020
|
+
return result.ok;
|
|
1021
|
+
}
|
|
657
1022
|
|
|
658
|
-
|
|
1023
|
+
let writeHexStr: string;
|
|
659
1024
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1025
|
+
// Check if action has a map - use mapped hex value directly
|
|
1026
|
+
if (action.map && data.length === 1) {
|
|
1027
|
+
const mapKey = String(data[0]);
|
|
1028
|
+
const mapEntry = action.map[mapKey];
|
|
1029
|
+
if (!mapEntry) {
|
|
1030
|
+
console.warn(`DeviceManager sendAction: no map entry for value ${mapKey} in action ${action.n}`);
|
|
1031
|
+
return false;
|
|
663
1032
|
}
|
|
664
|
-
|
|
665
|
-
|
|
1033
|
+
const mappedHex = getActionMapHex(mapEntry);
|
|
1034
|
+
// Map values may contain &-separated multi-writes (e.g. "1048&114C&0a26")
|
|
1035
|
+
const writes = mappedHex.split('&');
|
|
1036
|
+
const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
|
|
1037
|
+
try {
|
|
1038
|
+
const msgHandler = this._systemUtils?.getMsgHandler();
|
|
1039
|
+
if (!msgHandler) return false;
|
|
1040
|
+
for (const hexWr of writes) {
|
|
1041
|
+
const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + hexWr;
|
|
1042
|
+
console.log(`DeviceManager sendAction ${action.n} ${cmd}`);
|
|
1043
|
+
const msgRslt = await msgHandler.sendRICRESTURL<RaftOKFail>(cmd);
|
|
1044
|
+
if (msgRslt.rslt !== "ok") return false;
|
|
1045
|
+
}
|
|
1046
|
+
return true;
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
console.warn(`DeviceManager sendAction error ${error}`);
|
|
1049
|
+
return false;
|
|
666
1050
|
}
|
|
1051
|
+
} else {
|
|
1052
|
+
let writeBytes: Uint8Array;
|
|
667
1053
|
|
|
668
|
-
//
|
|
669
|
-
|
|
1054
|
+
// Check for one data item
|
|
1055
|
+
if (data.length === 1) {
|
|
670
1056
|
|
|
671
|
-
|
|
672
|
-
{
|
|
1057
|
+
let value = data[0];
|
|
673
1058
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1059
|
+
// Check for conversion
|
|
1060
|
+
if (action.sub !== undefined) {
|
|
1061
|
+
value = value - action.sub;
|
|
1062
|
+
}
|
|
1063
|
+
if (action.mul !== undefined) {
|
|
1064
|
+
value = value * action.mul;
|
|
1065
|
+
}
|
|
677
1066
|
|
|
678
|
-
|
|
679
|
-
|
|
1067
|
+
// Form the write bytes
|
|
1068
|
+
writeBytes = action.t ? structPack(action.t, [value]) : new Uint8Array(0);
|
|
1069
|
+
|
|
1070
|
+
} else
|
|
1071
|
+
{
|
|
1072
|
+
|
|
1073
|
+
// Form the write bytes which may have multiple data items
|
|
1074
|
+
writeBytes = action.t ? structPack(action.t, data) : new Uint8Array(0);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Convert to hex string
|
|
1078
|
+
writeHexStr = this.toHex(writeBytes);
|
|
1079
|
+
}
|
|
680
1080
|
|
|
681
1081
|
// Add prefix and postfix
|
|
682
1082
|
writeHexStr = (action.w ? action.w : "") + writeHexStr + (action.wz ? action.wz : "");
|
|
683
1083
|
|
|
684
|
-
//
|
|
685
|
-
const devBus =
|
|
686
|
-
const devAddr = deviceKey.split("_")[1]
|
|
1084
|
+
// Parse the device key into bus and address components
|
|
1085
|
+
const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
|
|
687
1086
|
|
|
688
1087
|
// Send the action to the server
|
|
689
1088
|
const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + writeHexStr;
|
|
@@ -738,6 +1137,125 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
738
1137
|
return false;
|
|
739
1138
|
}
|
|
740
1139
|
|
|
1140
|
+
////////////////////////////////////////////////////////////////////////////
|
|
1141
|
+
// Set sample rate with coordinated polling parameters
|
|
1142
|
+
// Finds the closest supported rate from the device's _conf.rate action,
|
|
1143
|
+
// calculates optimal intervalUs and numSamples, and sends a single
|
|
1144
|
+
// /devman/devconfig call to set all parameters atomically.
|
|
1145
|
+
////////////////////////////////////////////////////////////////////////////
|
|
1146
|
+
|
|
1147
|
+
public async setSampleRate(deviceKey: string, sampleRateHz: number, options?: {
|
|
1148
|
+
numSamples?: number;
|
|
1149
|
+
intervalUs?: number;
|
|
1150
|
+
maxNumSamples?: number;
|
|
1151
|
+
}): Promise<SampleRateResult> {
|
|
1152
|
+
// Look up device state and type info
|
|
1153
|
+
const deviceState = this._devicesState[deviceKey];
|
|
1154
|
+
if (!deviceState?.deviceTypeInfo) {
|
|
1155
|
+
return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: 0, intervalUs: 0, numSamples: 0, error: 'Device not found or type info not loaded' };
|
|
1156
|
+
}
|
|
1157
|
+
const typeInfo = deviceState.deviceTypeInfo;
|
|
1158
|
+
|
|
1159
|
+
// Find the _conf.rate action
|
|
1160
|
+
const confRateAction = typeInfo.actions?.find(a => a.n === '_conf.rate');
|
|
1161
|
+
if (!confRateAction?.map) {
|
|
1162
|
+
// No _conf.rate action — use generic sample rate setting
|
|
1163
|
+
// Non-FIFO devices: poll once per sample period, 1 sample per read
|
|
1164
|
+
const samplePeriodUs = Math.round(1000000 / sampleRateHz);
|
|
1165
|
+
const numSamples = options?.numSamples ?? 1;
|
|
1166
|
+
const intervalUs = options?.intervalUs ?? Math.max(5000, samplePeriodUs);
|
|
1167
|
+
|
|
1168
|
+
const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
|
|
1169
|
+
const cmd = `devman/devconfig?bus=${devBus}&addr=${devAddr}&intervalUs=${intervalUs}&numSamples=${numSamples}`;
|
|
1170
|
+
|
|
1171
|
+
try {
|
|
1172
|
+
const msgHandler = this._systemUtils?.getMsgHandler();
|
|
1173
|
+
if (!msgHandler) {
|
|
1174
|
+
return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: sampleRateHz, intervalUs, numSamples, error: 'No message handler available' };
|
|
1175
|
+
}
|
|
1176
|
+
const msgRslt = await msgHandler.sendRICRESTURL<RaftOKFail>(cmd);
|
|
1177
|
+
const ok = msgRslt.rslt === 'ok';
|
|
1178
|
+
return { ok, requestedRateHz: sampleRateHz, actualRateHz: sampleRateHz, intervalUs, numSamples, error: ok ? undefined : `Firmware returned: ${msgRslt.rslt}` };
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: sampleRateHz, intervalUs, numSamples, error: `${error}` };
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Find the closest supported rate from the map keys
|
|
1185
|
+
const supportedRates = Object.keys(confRateAction.map).map(Number).filter(r => !isNaN(r)).sort((a, b) => a - b);
|
|
1186
|
+
if (supportedRates.length === 0) {
|
|
1187
|
+
return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: 0, intervalUs: 0, numSamples: 0, error: 'No valid rates in _conf.rate map' };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
let actualRate = supportedRates[0];
|
|
1191
|
+
let minDist = Math.abs(sampleRateHz - actualRate);
|
|
1192
|
+
for (const rate of supportedRates) {
|
|
1193
|
+
const dist = Math.abs(sampleRateHz - rate);
|
|
1194
|
+
if (dist < minDist) {
|
|
1195
|
+
minDist = dist;
|
|
1196
|
+
actualRate = rate;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Look up map entry for the matched rate — may be object with recommended polling params
|
|
1201
|
+
const mapEntry = confRateAction.map[String(actualRate)];
|
|
1202
|
+
const mapObj = typeof mapEntry === 'object' ? mapEntry : null;
|
|
1203
|
+
const recommendedIntervalUs = mapObj?.i;
|
|
1204
|
+
const recommendedNumSamples = mapObj?.s;
|
|
1205
|
+
|
|
1206
|
+
// Calculate inter-sample period
|
|
1207
|
+
const samplePeriodUs = Math.round(1000000 / actualRate);
|
|
1208
|
+
|
|
1209
|
+
// Calculate optimal numSamples and intervalUs
|
|
1210
|
+
// Priority: explicit options > map entry recommendations > auto-calculation
|
|
1211
|
+
const maxNumSamples = options?.maxNumSamples ?? 20;
|
|
1212
|
+
let numSamples: number;
|
|
1213
|
+
let intervalUs: number;
|
|
1214
|
+
|
|
1215
|
+
if (options?.numSamples !== undefined && options?.intervalUs !== undefined) {
|
|
1216
|
+
// Both explicitly specified — use as-is
|
|
1217
|
+
numSamples = options.numSamples;
|
|
1218
|
+
intervalUs = options.intervalUs;
|
|
1219
|
+
} else if (options?.intervalUs !== undefined) {
|
|
1220
|
+
// intervalUs specified, derive numSamples from it
|
|
1221
|
+
intervalUs = options.intervalUs;
|
|
1222
|
+
numSamples = options?.numSamples ?? recommendedNumSamples ??
|
|
1223
|
+
Math.max(1, Math.min(maxNumSamples, Math.floor(intervalUs / samplePeriodUs)));
|
|
1224
|
+
} else if (options?.numSamples !== undefined) {
|
|
1225
|
+
// numSamples specified, derive intervalUs from it
|
|
1226
|
+
numSamples = options.numSamples;
|
|
1227
|
+
intervalUs = recommendedIntervalUs ??
|
|
1228
|
+
Math.round(numSamples * samplePeriodUs * 0.8);
|
|
1229
|
+
} else if (recommendedIntervalUs !== undefined && recommendedNumSamples !== undefined) {
|
|
1230
|
+
// Use map entry recommendations
|
|
1231
|
+
intervalUs = recommendedIntervalUs;
|
|
1232
|
+
numSamples = recommendedNumSamples;
|
|
1233
|
+
} else {
|
|
1234
|
+
// Auto-calculate: target ~50ms poll interval, bounded by sample rate
|
|
1235
|
+
const targetPollIntervalUs = 50000;
|
|
1236
|
+
numSamples = recommendedNumSamples ??
|
|
1237
|
+
Math.max(1, Math.min(maxNumSamples, Math.floor(targetPollIntervalUs / samplePeriodUs)));
|
|
1238
|
+
intervalUs = recommendedIntervalUs ??
|
|
1239
|
+
Math.max(5000, Math.min(1000000, Math.round(numSamples * samplePeriodUs * 0.8)));
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Send single devconfig call with all parameters
|
|
1243
|
+
const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
|
|
1244
|
+
const cmd = `devman/devconfig?bus=${devBus}&addr=${devAddr}&sampleRateHz=${actualRate}&intervalUs=${intervalUs}&numSamples=${numSamples}`;
|
|
1245
|
+
|
|
1246
|
+
try {
|
|
1247
|
+
const msgHandler = this._systemUtils?.getMsgHandler();
|
|
1248
|
+
if (!msgHandler) {
|
|
1249
|
+
return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: actualRate, intervalUs, numSamples, error: 'No message handler available' };
|
|
1250
|
+
}
|
|
1251
|
+
const msgRslt = await msgHandler.sendRICRESTURL<RaftOKFail>(cmd);
|
|
1252
|
+
const ok = msgRslt.rslt === 'ok';
|
|
1253
|
+
return { ok, requestedRateHz: sampleRateHz, actualRateHz: actualRate, intervalUs, numSamples, error: ok ? undefined : `Firmware returned: ${msgRslt.rslt}` };
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: actualRate, intervalUs, numSamples, error: `${error}` };
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
741
1259
|
////////////////////////////////////////////////////////////////////////////
|
|
742
1260
|
// Convert hex to bytes
|
|
743
1261
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -750,6 +1268,66 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
750
1268
|
return bytes;
|
|
751
1269
|
}
|
|
752
1270
|
|
|
1271
|
+
////////////////////////////////////////////////////////////////////////////
|
|
1272
|
+
// Helpers for device stats
|
|
1273
|
+
////////////////////////////////////////////////////////////////////////////
|
|
1274
|
+
|
|
1275
|
+
private createEmptyStats(): DeviceStatsInternal {
|
|
1276
|
+
return {
|
|
1277
|
+
totalSamples: 0,
|
|
1278
|
+
windowMs: this._statsWindowMs,
|
|
1279
|
+
windowSamples: 0,
|
|
1280
|
+
sampleRateHz: 0,
|
|
1281
|
+
lastSampleTimeMs: null,
|
|
1282
|
+
lastUpdateTimeMs: null,
|
|
1283
|
+
windowEvents: []
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private getOrCreateDeviceStats(deviceKey: string): DeviceStatsInternal {
|
|
1288
|
+
if (!this._deviceStats[deviceKey]) {
|
|
1289
|
+
this._deviceStats[deviceKey] = this.createEmptyStats();
|
|
1290
|
+
}
|
|
1291
|
+
return this._deviceStats[deviceKey];
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
private cloneDeviceStats(stats: DeviceStatsInternal): DeviceStats {
|
|
1295
|
+
return {
|
|
1296
|
+
totalSamples: stats.totalSamples,
|
|
1297
|
+
windowMs: stats.windowMs,
|
|
1298
|
+
windowSamples: stats.windowSamples,
|
|
1299
|
+
sampleRateHz: stats.sampleRateHz,
|
|
1300
|
+
lastSampleTimeMs: stats.lastSampleTimeMs,
|
|
1301
|
+
lastUpdateTimeMs: stats.lastUpdateTimeMs
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
private updateDeviceStats(deviceKey: string, newSamples: number, nowMs: number): void {
|
|
1306
|
+
const stats = this.getOrCreateDeviceStats(deviceKey);
|
|
1307
|
+
stats.lastUpdateTimeMs = nowMs;
|
|
1308
|
+
|
|
1309
|
+
if (newSamples > 0) {
|
|
1310
|
+
stats.totalSamples += newSamples;
|
|
1311
|
+
stats.lastSampleTimeMs = nowMs;
|
|
1312
|
+
stats.windowEvents.push({ timeMs: nowMs, samples: newSamples });
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const windowStartMs = nowMs - stats.windowMs;
|
|
1316
|
+
while (stats.windowEvents.length > 0 && stats.windowEvents[0].timeMs < windowStartMs) {
|
|
1317
|
+
stats.windowEvents.shift();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const windowSamples = stats.windowEvents.reduce((sum, entry) => sum + entry.samples, 0);
|
|
1321
|
+
stats.windowSamples = windowSamples;
|
|
1322
|
+
if (stats.windowEvents.length === 0) {
|
|
1323
|
+
stats.sampleRateHz = 0;
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const actualWindowMs = Math.max(1, nowMs - stats.windowEvents[0].timeMs);
|
|
1328
|
+
stats.sampleRateHz = (windowSamples * 1000) / actualWindowMs;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
753
1331
|
////////////////////////////////////////////////////////////////////////////
|
|
754
1332
|
// Helpers for decoded data callbacks
|
|
755
1333
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -760,7 +1338,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
760
1338
|
return lengths;
|
|
761
1339
|
}
|
|
762
1340
|
pollRespMetadata.a.forEach((attr) => {
|
|
763
|
-
lengths[attr.n] = deviceAttrs[attr.n]?.values.length
|
|
1341
|
+
lengths[attr.n] = deviceAttrs[attr.n]?.values.length ?? 0;
|
|
764
1342
|
});
|
|
765
1343
|
return lengths;
|
|
766
1344
|
}
|
|
@@ -781,7 +1359,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
781
1359
|
return;
|
|
782
1360
|
}
|
|
783
1361
|
|
|
784
|
-
const attrValues: Record<string, number[]> = {};
|
|
1362
|
+
const attrValues: Record<string, (number | string)[]> = {};
|
|
785
1363
|
let hasValues = false;
|
|
786
1364
|
|
|
787
1365
|
pollRespMetadata.a.forEach((attr) => {
|
|
@@ -789,7 +1367,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
789
1367
|
if (!attrState) {
|
|
790
1368
|
return;
|
|
791
1369
|
}
|
|
792
|
-
const prevLen = attrLengthsBefore[attr.n]
|
|
1370
|
+
const prevLen = attrLengthsBefore[attr.n] ?? 0;
|
|
793
1371
|
if (attrState.values.length > prevLen) {
|
|
794
1372
|
attrValues[attr.n] = attrState.values.slice(prevLen);
|
|
795
1373
|
hasValues = hasValues || attrValues[attr.n].length > 0;
|