@robotical/raftjs 2.0.11 → 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 +10 -6
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSerial.js +1 -1
- package/dist/react-native/RaftChannelWebSerial.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 +47 -2
- package/dist/react-native/RaftDeviceManager.js +696 -104
- 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 +3 -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 +10 -6
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSerial.js +1 -1
- package/dist/web/RaftChannelWebSerial.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 +47 -2
- package/dist/web/RaftDeviceManager.js +696 -104
- 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 +3 -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 +11 -6
- package/src/RaftChannelWebSerial.ts +1 -1
- 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 +823 -121
- 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 +3 -0
package/src/RaftDeviceManager.ts
CHANGED
|
@@ -7,16 +7,34 @@
|
|
|
7
7
|
//
|
|
8
8
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
9
9
|
|
|
10
|
-
import { DeviceAttributeState, 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
|
+
export interface DeviceDecodedData {
|
|
21
|
+
deviceKey: string;
|
|
22
|
+
busName: string;
|
|
23
|
+
deviceAddress: string;
|
|
24
|
+
deviceType: string;
|
|
25
|
+
attrGroupName?: string;
|
|
26
|
+
attrValues: Record<string, (number | string)[]>;
|
|
27
|
+
timestampsUs: number[];
|
|
28
|
+
markers?: Record<string, unknown>;
|
|
29
|
+
fromOfflineBuffer?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DeviceStatsInternal extends DeviceStats {
|
|
33
|
+
windowEvents: Array<{ timeMs: number; samples: number }>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type BinaryRecordPayloadFormat = "lengthPrefixed" | "legacyRaw";
|
|
37
|
+
|
|
20
38
|
export class DeviceManager implements RaftDeviceMgrIF{
|
|
21
39
|
|
|
22
40
|
// Max data points to store
|
|
@@ -45,10 +63,17 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
45
63
|
private _newDeviceCallbacks: Array<(deviceKey: string, state: DeviceState) => void> = [];
|
|
46
64
|
private _newDeviceAttributeCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
|
|
47
65
|
private _newAttributeDataCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
|
|
66
|
+
private _decodedDataCallbacks: Array<(decoded: DeviceDecodedData) => void> = [];
|
|
67
|
+
private _deviceRemovedCallbacks: Array<(deviceKey: string, state: DeviceState) => void> = [];
|
|
48
68
|
|
|
49
69
|
// Debug message index (to help debug with async messages)
|
|
50
70
|
private _debugMsgIndex = 0;
|
|
51
71
|
|
|
72
|
+
// Device stats (sample counts, rates)
|
|
73
|
+
private _statsWindowMs = 5000;
|
|
74
|
+
private _deviceStats: { [deviceKey: string]: DeviceStatsInternal } = {};
|
|
75
|
+
private _malformedSampleWarnLastMs: { [warningKey: string]: number } = {};
|
|
76
|
+
|
|
52
77
|
public getDevicesState(): DevicesState {
|
|
53
78
|
return this._devicesState;
|
|
54
79
|
}
|
|
@@ -57,6 +82,14 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
57
82
|
return this._devicesState[deviceKey];
|
|
58
83
|
}
|
|
59
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
|
+
|
|
60
93
|
// Cached device type data
|
|
61
94
|
private _cachedDeviceTypeRecs: DeviceTypeInfoRecs = {};
|
|
62
95
|
|
|
@@ -146,6 +179,26 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
146
179
|
this._newAttributeDataCallbacks = this._newAttributeDataCallbacks.filter((cb) => cb !== callback);
|
|
147
180
|
}
|
|
148
181
|
|
|
182
|
+
public addDecodedDataCallback(callback: (decoded: DeviceDecodedData) => void): void {
|
|
183
|
+
if (!this._decodedDataCallbacks.includes(callback)) {
|
|
184
|
+
this._decodedDataCallbacks.push(callback);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public removeDecodedDataCallback(callback: (decoded: DeviceDecodedData) => void): void {
|
|
189
|
+
this._decodedDataCallbacks = this._decodedDataCallbacks.filter((cb) => cb !== callback);
|
|
190
|
+
}
|
|
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
|
+
|
|
149
202
|
////////////////////////////////////////////////////////////////////////////
|
|
150
203
|
// Set the friendly name for the device
|
|
151
204
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -162,93 +215,205 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
162
215
|
public async handleClientMsgBinary(rxMsg: Uint8Array) {
|
|
163
216
|
// console.log(`DeviceManager client1 msg ${RaftUtils.bufferToHex(rxMsg)}`);
|
|
164
217
|
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
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
|
|
182
267
|
|
|
183
268
|
// Debug
|
|
184
269
|
// const debugMsgTime = Date.now();
|
|
185
270
|
const debugMsgIndex = this._debugMsgIndex++;
|
|
186
271
|
|
|
187
|
-
// Message layout
|
|
188
|
-
const msgTypeLen = 2; //
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
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
|
|
194
285
|
|
|
195
286
|
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} rxMsg.length ${rxMsg.length} rxMsg ${RaftUtils.bufferToHex(rxMsg)}`);
|
|
196
287
|
|
|
197
288
|
// Start after the message type
|
|
198
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
|
+
}
|
|
300
|
+
|
|
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
|
+
}
|
|
199
325
|
|
|
200
|
-
// Iterate through
|
|
326
|
+
// Iterate through device records
|
|
201
327
|
while (msgPos < rxMsg.length) {
|
|
202
328
|
|
|
203
|
-
// Check length
|
|
329
|
+
// Check minimum length for record length prefix + record header
|
|
204
330
|
const remainingLen = rxMsg.length - msgPos;
|
|
205
|
-
if (remainingLen <
|
|
206
|
-
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}`);
|
|
207
333
|
return;
|
|
208
334
|
}
|
|
209
335
|
|
|
210
|
-
// Get the length
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
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}`);
|
|
214
340
|
return;
|
|
215
341
|
}
|
|
216
342
|
|
|
217
|
-
// Extract
|
|
218
|
-
let
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
227
384
|
|
|
228
385
|
// Debug
|
|
229
|
-
// console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length}
|
|
230
|
-
// 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}`);
|
|
231
388
|
|
|
232
|
-
//
|
|
233
|
-
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);
|
|
234
392
|
|
|
235
393
|
// Update the last update time
|
|
236
394
|
this._deviceLastUpdateTime[deviceKey] = Date.now();
|
|
237
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
|
+
|
|
238
403
|
// Check if a device state already exists
|
|
239
404
|
if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
|
|
240
405
|
|
|
241
406
|
// Get the device type info
|
|
242
|
-
const deviceTypeInfo = await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
|
|
407
|
+
const deviceTypeInfo = resolvedDeviceTypeInfo ?? await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
|
|
243
408
|
|
|
244
409
|
// Debug
|
|
245
|
-
// 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)}`);
|
|
246
411
|
|
|
247
412
|
// Handle case where device type info is not available
|
|
248
413
|
if (deviceTypeInfo === undefined) {
|
|
249
|
-
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this
|
|
250
|
-
// Skip to next
|
|
251
|
-
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;
|
|
252
417
|
continue;
|
|
253
418
|
}
|
|
254
419
|
|
|
@@ -258,7 +423,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
258
423
|
this._devicesState[deviceKey].deviceTypeInfo = deviceTypeInfo;
|
|
259
424
|
this._devicesState[deviceKey].deviceType = deviceTypeInfo.name || "";
|
|
260
425
|
this._devicesState[deviceKey].busName = busNum.toString();
|
|
261
|
-
this._devicesState[deviceKey].deviceAddress =
|
|
426
|
+
this._devicesState[deviceKey].deviceAddress = devAddrHex;
|
|
262
427
|
}
|
|
263
428
|
} else {
|
|
264
429
|
// Create device record - device type info may be undefined
|
|
@@ -267,13 +432,19 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
267
432
|
deviceTimeline: {
|
|
268
433
|
timestampsUs: [],
|
|
269
434
|
lastReportTimestampUs: 0,
|
|
270
|
-
reportTimestampOffsetUs: 0
|
|
435
|
+
reportTimestampOffsetUs: 0,
|
|
436
|
+
totalSamplesAdded: 0,
|
|
437
|
+
emaLastSampleTimeUs: 0,
|
|
438
|
+
emaIntervalUs: 0,
|
|
439
|
+
emaPrevPollTimeUs: 0,
|
|
440
|
+
emaCalibrated: false,
|
|
441
|
+
emaCalibrationPolls: 0
|
|
271
442
|
},
|
|
272
443
|
deviceAttributes: {},
|
|
273
444
|
deviceIsNew: true,
|
|
274
445
|
stateChanged: false,
|
|
275
|
-
|
|
276
|
-
deviceAddress:
|
|
446
|
+
onlineState: DeviceOnlineState.Online,
|
|
447
|
+
deviceAddress: devAddrHex,
|
|
277
448
|
deviceType: deviceTypeInfo?.name || "",
|
|
278
449
|
busName: busNum.toString()
|
|
279
450
|
};
|
|
@@ -282,7 +453,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
282
453
|
|
|
283
454
|
// Get device state
|
|
284
455
|
const deviceState = this._devicesState[deviceKey];
|
|
285
|
-
deviceState.
|
|
456
|
+
deviceState.onlineState = isOnline ? DeviceOnlineState.Online : DeviceOnlineState.Offline;
|
|
286
457
|
|
|
287
458
|
// Check if device type info is available and complete
|
|
288
459
|
if (deviceState.deviceTypeInfo && deviceState.deviceTypeInfo.resp) {
|
|
@@ -290,53 +461,106 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
290
461
|
// Iterate over attributes in the group
|
|
291
462
|
const pollRespMetadata = deviceState.deviceTypeInfo!.resp!;
|
|
292
463
|
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
464
|
+
// Process samples within this record
|
|
465
|
+
const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
|
|
466
|
+
const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
|
|
467
|
+
const totalSamplesBefore = deviceState.deviceTimeline.totalSamplesAdded;
|
|
468
|
+
if (recordPayloadFormat === "lengthPrefixed") {
|
|
469
|
+
while (pollDataPos < samplesEndPos) {
|
|
297
470
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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;
|
|
303
478
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
this._maxDatapointsToStore);
|
|
479
|
+
if (sampleLen === 0 || pollDataPos + sampleLen > samplesEndPos) {
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
308
482
|
|
|
309
|
-
|
|
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);
|
|
310
490
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
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
|
+
}
|
|
322
501
|
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
538
|
|
|
326
|
-
|
|
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
|
+
}
|
|
547
|
+
}
|
|
327
548
|
|
|
549
|
+
// Inform decoded-data callbacks
|
|
550
|
+
this.emitDecodedData(deviceKey, busNum.toString(), devAddrHex, deviceState,
|
|
551
|
+
pollRespMetadata, attrLengthsBefore, timelineLenBefore);
|
|
328
552
|
|
|
329
|
-
|
|
330
|
-
|
|
553
|
+
const newSamples = deviceState.deviceTimeline.totalSamplesAdded - totalSamplesBefore;
|
|
554
|
+
this.updateDeviceStats(deviceKey, newSamples, Date.now());
|
|
331
555
|
} else {
|
|
332
556
|
console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceState incomplete for device ${deviceKey}, skipping attribute processing`);
|
|
333
557
|
}
|
|
334
558
|
|
|
335
559
|
// Debug
|
|
336
|
-
// 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}`);
|
|
337
561
|
|
|
338
|
-
//
|
|
339
|
-
msgPos +=
|
|
562
|
+
// Advance past this record (recordLenLen + recordLen bytes)
|
|
563
|
+
msgPos += recordLenLen + recordLen;
|
|
340
564
|
}
|
|
341
565
|
|
|
342
566
|
// Check for devices that have not been updated for a while
|
|
@@ -345,6 +569,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
345
569
|
Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
|
|
346
570
|
if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
|
|
347
571
|
delete this._devicesState[deviceKey];
|
|
572
|
+
delete this._deviceStats[deviceKey];
|
|
348
573
|
}
|
|
349
574
|
});
|
|
350
575
|
}
|
|
@@ -365,6 +590,11 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
365
590
|
// Iterate over the buses
|
|
366
591
|
Object.entries(data).forEach(([busName, devices]) => {
|
|
367
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
|
+
|
|
368
598
|
// Check for bus status info
|
|
369
599
|
if (devices && typeof devices === "object" && "_s" in devices) {
|
|
370
600
|
// console.log(`DeviceManager bus status ${JSON.stringify(devices._s)}`);
|
|
@@ -381,15 +611,20 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
381
611
|
|
|
382
612
|
// Device type name
|
|
383
613
|
let deviceTypeName = "";
|
|
614
|
+
let deviceTypeIdx = -1;
|
|
384
615
|
if (attrGroups && typeof attrGroups === 'object' && "_t" in attrGroups && typeof attrGroups._t === "string") {
|
|
385
616
|
deviceTypeName = attrGroups._t || "";
|
|
386
|
-
} 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
|
+
{
|
|
387
622
|
console.warn(`DeviceManager missing device type attrGroups ${JSON.stringify(attrGroups)}`);
|
|
388
623
|
return;
|
|
389
624
|
}
|
|
390
625
|
|
|
391
626
|
// Device key
|
|
392
|
-
const deviceKey = getDeviceKey(busName, devAddr
|
|
627
|
+
const deviceKey = getDeviceKey(busName, devAddr);
|
|
393
628
|
|
|
394
629
|
// Update the last update time
|
|
395
630
|
this._deviceLastUpdateTime[deviceKey] = Date.now();
|
|
@@ -415,12 +650,18 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
415
650
|
deviceTimeline: {
|
|
416
651
|
timestampsUs: [],
|
|
417
652
|
lastReportTimestampUs: 0,
|
|
418
|
-
reportTimestampOffsetUs: 0
|
|
653
|
+
reportTimestampOffsetUs: 0,
|
|
654
|
+
totalSamplesAdded: 0,
|
|
655
|
+
emaLastSampleTimeUs: 0,
|
|
656
|
+
emaIntervalUs: 0,
|
|
657
|
+
emaPrevPollTimeUs: 0,
|
|
658
|
+
emaCalibrated: false,
|
|
659
|
+
emaCalibrationPolls: 0
|
|
419
660
|
},
|
|
420
661
|
deviceAttributes: {},
|
|
421
662
|
deviceIsNew: true,
|
|
422
663
|
stateChanged: false,
|
|
423
|
-
|
|
664
|
+
onlineState: DeviceOnlineState.Online,
|
|
424
665
|
deviceAddress: devAddr,
|
|
425
666
|
deviceType: deviceTypeName,
|
|
426
667
|
busName: busName
|
|
@@ -431,9 +672,15 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
431
672
|
// Get device state
|
|
432
673
|
const deviceState = this._devicesState[deviceKey];
|
|
433
674
|
|
|
434
|
-
// Check for online/offline state information
|
|
675
|
+
// Check for online/offline/pending-deletion state information
|
|
435
676
|
if (attrGroups && typeof attrGroups === "object" && "_o" in attrGroups) {
|
|
436
|
-
|
|
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;
|
|
437
684
|
}
|
|
438
685
|
|
|
439
686
|
// Check if device type info is available
|
|
@@ -441,6 +688,8 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
441
688
|
return;
|
|
442
689
|
}
|
|
443
690
|
|
|
691
|
+
const markers = this.extractMarkers(attrGroups);
|
|
692
|
+
|
|
444
693
|
// Iterate attribute groups
|
|
445
694
|
Object.entries(attrGroups).forEach(([attrGroupName, msgHexStr]) => {
|
|
446
695
|
|
|
@@ -463,6 +712,10 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
463
712
|
// Iterate over attributes in the group
|
|
464
713
|
const pollRespMetadata = deviceState.deviceTypeInfo!.resp!;
|
|
465
714
|
|
|
715
|
+
const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
|
|
716
|
+
const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
|
|
717
|
+
const totalSamplesBefore = deviceState.deviceTimeline.totalSamplesAdded;
|
|
718
|
+
|
|
466
719
|
// Loop
|
|
467
720
|
while (msgBufIdx < msgBytes.length) {
|
|
468
721
|
|
|
@@ -475,6 +728,12 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
475
728
|
msgBufIdx = newMsgBufIdx;
|
|
476
729
|
deviceState.stateChanged = true;
|
|
477
730
|
}
|
|
731
|
+
|
|
732
|
+
this.emitDecodedData(deviceKey, busName, devAddr, deviceState, pollRespMetadata,
|
|
733
|
+
attrLengthsBefore, timelineLenBefore, attrGroupName, markers);
|
|
734
|
+
|
|
735
|
+
const newSamples = deviceState.deviceTimeline.totalSamplesAdded - totalSamplesBefore;
|
|
736
|
+
this.updateDeviceStats(deviceKey, newSamples, Date.now());
|
|
478
737
|
});
|
|
479
738
|
});
|
|
480
739
|
});
|
|
@@ -485,6 +744,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
485
744
|
Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
|
|
486
745
|
if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
|
|
487
746
|
delete this._devicesState[deviceKey];
|
|
747
|
+
delete this._deviceStats[deviceKey];
|
|
488
748
|
}
|
|
489
749
|
});
|
|
490
750
|
}
|
|
@@ -522,6 +782,22 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
522
782
|
});
|
|
523
783
|
}
|
|
524
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
|
+
|
|
525
801
|
////////////////////////////////////////////////////////////////////////////
|
|
526
802
|
// Get device type info
|
|
527
803
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -600,6 +876,131 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
600
876
|
}
|
|
601
877
|
}
|
|
602
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
|
+
|
|
603
1004
|
////////////////////////////////////////////////////////////////////////////
|
|
604
1005
|
// Send action to device
|
|
605
1006
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -611,42 +1012,77 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
611
1012
|
}
|
|
612
1013
|
|
|
613
1014
|
public async sendAction(deviceKey: string, action: DeviceTypeAction, data: number[]): Promise<boolean> {
|
|
614
|
-
|
|
1015
|
+
console.log(`DeviceManager sendAction ${deviceKey} action ${action.n} data ${data} map ${JSON.stringify(action.map)} keys ${action.map ? Object.keys(action.map) : 'none'}`);
|
|
615
1016
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
}
|
|
620
1022
|
|
|
621
|
-
|
|
1023
|
+
let writeHexStr: string;
|
|
622
1024
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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;
|
|
626
1032
|
}
|
|
627
|
-
|
|
628
|
-
|
|
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;
|
|
629
1050
|
}
|
|
1051
|
+
} else {
|
|
1052
|
+
let writeBytes: Uint8Array;
|
|
630
1053
|
|
|
631
|
-
//
|
|
632
|
-
|
|
1054
|
+
// Check for one data item
|
|
1055
|
+
if (data.length === 1) {
|
|
633
1056
|
|
|
634
|
-
|
|
635
|
-
{
|
|
1057
|
+
let value = data[0];
|
|
636
1058
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Form the write bytes
|
|
1068
|
+
writeBytes = action.t ? structPack(action.t, [value]) : new Uint8Array(0);
|
|
1069
|
+
|
|
1070
|
+
} else
|
|
1071
|
+
{
|
|
640
1072
|
|
|
641
|
-
|
|
642
|
-
|
|
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
|
+
}
|
|
643
1080
|
|
|
644
1081
|
// Add prefix and postfix
|
|
645
1082
|
writeHexStr = (action.w ? action.w : "") + writeHexStr + (action.wz ? action.wz : "");
|
|
646
1083
|
|
|
647
|
-
//
|
|
648
|
-
const devBus =
|
|
649
|
-
const devAddr = deviceKey.split("_")[1]
|
|
1084
|
+
// Parse the device key into bus and address components
|
|
1085
|
+
const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
|
|
650
1086
|
|
|
651
1087
|
// Send the action to the server
|
|
652
1088
|
const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + writeHexStr;
|
|
@@ -701,6 +1137,125 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
701
1137
|
return false;
|
|
702
1138
|
}
|
|
703
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
|
+
|
|
704
1259
|
////////////////////////////////////////////////////////////////////////////
|
|
705
1260
|
// Convert hex to bytes
|
|
706
1261
|
////////////////////////////////////////////////////////////////////////////
|
|
@@ -712,4 +1267,151 @@ export class DeviceManager implements RaftDeviceMgrIF{
|
|
|
712
1267
|
}
|
|
713
1268
|
return bytes;
|
|
714
1269
|
}
|
|
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
|
+
|
|
1331
|
+
////////////////////////////////////////////////////////////////////////////
|
|
1332
|
+
// Helpers for decoded data callbacks
|
|
1333
|
+
////////////////////////////////////////////////////////////////////////////
|
|
1334
|
+
|
|
1335
|
+
private snapshotAttrLengths(deviceAttrs: DeviceAttributesState, pollRespMetadata: DeviceTypeInfo["resp"]): Record<string, number> {
|
|
1336
|
+
const lengths: Record<string, number> = {};
|
|
1337
|
+
if (!pollRespMetadata) {
|
|
1338
|
+
return lengths;
|
|
1339
|
+
}
|
|
1340
|
+
pollRespMetadata.a.forEach((attr) => {
|
|
1341
|
+
lengths[attr.n] = deviceAttrs[attr.n]?.values.length ?? 0;
|
|
1342
|
+
});
|
|
1343
|
+
return lengths;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
private emitDecodedData(
|
|
1347
|
+
deviceKey: string,
|
|
1348
|
+
busName: string,
|
|
1349
|
+
devAddr: string,
|
|
1350
|
+
deviceState: DeviceState,
|
|
1351
|
+
pollRespMetadata: DeviceTypeInfo["resp"],
|
|
1352
|
+
attrLengthsBefore: Record<string, number>,
|
|
1353
|
+
timelineLenBefore: number,
|
|
1354
|
+
attrGroupName = "",
|
|
1355
|
+
markers?: Record<string, unknown>,
|
|
1356
|
+
): void {
|
|
1357
|
+
|
|
1358
|
+
if (!pollRespMetadata) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const attrValues: Record<string, (number | string)[]> = {};
|
|
1363
|
+
let hasValues = false;
|
|
1364
|
+
|
|
1365
|
+
pollRespMetadata.a.forEach((attr) => {
|
|
1366
|
+
const attrState = deviceState.deviceAttributes[attr.n];
|
|
1367
|
+
if (!attrState) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const prevLen = attrLengthsBefore[attr.n] ?? 0;
|
|
1371
|
+
if (attrState.values.length > prevLen) {
|
|
1372
|
+
attrValues[attr.n] = attrState.values.slice(prevLen);
|
|
1373
|
+
hasValues = hasValues || attrValues[attr.n].length > 0;
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
if (!hasValues) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const timestampsUs = deviceState.deviceTimeline.timestampsUs.slice(timelineLenBefore);
|
|
1382
|
+
|
|
1383
|
+
const decoded: DeviceDecodedData = {
|
|
1384
|
+
deviceKey,
|
|
1385
|
+
busName,
|
|
1386
|
+
deviceAddress: devAddr,
|
|
1387
|
+
deviceType: deviceState.deviceType,
|
|
1388
|
+
attrGroupName: attrGroupName || undefined,
|
|
1389
|
+
attrValues,
|
|
1390
|
+
timestampsUs,
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
if (markers && Object.keys(markers).length > 0) {
|
|
1394
|
+
decoded.markers = markers;
|
|
1395
|
+
decoded.fromOfflineBuffer = this.isTruthy(markers["_buf"]);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
this._decodedDataCallbacks.forEach((cb) => cb(decoded));
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
private extractMarkers(attrGroups: any): Record<string, unknown> {
|
|
1402
|
+
const markers: Record<string, unknown> = {};
|
|
1403
|
+
if (!attrGroups || typeof attrGroups !== "object") {
|
|
1404
|
+
return markers;
|
|
1405
|
+
}
|
|
1406
|
+
Object.entries(attrGroups).forEach(([key, value]) => {
|
|
1407
|
+
if (key.startsWith("_") && key !== "_t" && key !== "_o") {
|
|
1408
|
+
markers[key] = value;
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
return markers;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private isTruthy(val: unknown): boolean {
|
|
1415
|
+
return val === true || val === 1 || val === "1";
|
|
1416
|
+
}
|
|
715
1417
|
}
|