@robdobsn/raftjs 1.10.7 → 1.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/react-native/RaftChannelSimulated.js +4 -3
  2. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  3. package/dist/react-native/RaftConnector.d.ts +10 -1
  4. package/dist/react-native/RaftConnector.js +23 -10
  5. package/dist/react-native/RaftConnector.js.map +1 -1
  6. package/dist/react-native/RaftDeviceManager.d.ts +13 -1
  7. package/dist/react-native/RaftDeviceManager.js +224 -77
  8. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  9. package/dist/react-native/RaftDeviceMgrIF.d.ts +5 -1
  10. package/dist/react-native/RaftDeviceStates.d.ts +20 -2
  11. package/dist/react-native/RaftDeviceStates.js +25 -4
  12. package/dist/react-native/RaftDeviceStates.js.map +1 -1
  13. package/dist/react-native/RaftPublish.d.ts +2 -0
  14. package/dist/react-native/RaftPublish.js +81 -0
  15. package/dist/react-native/RaftPublish.js.map +1 -0
  16. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  17. package/dist/react-native/RaftStreamHandler.js +66 -0
  18. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  19. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  20. package/dist/react-native/RaftSystemUtils.js +51 -0
  21. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  22. package/dist/react-native/RaftTypes.d.ts +21 -0
  23. package/dist/react-native/RaftTypes.js.map +1 -1
  24. package/dist/react-native/main.d.ts +1 -0
  25. package/dist/react-native/main.js +1 -0
  26. package/dist/react-native/main.js.map +1 -1
  27. package/dist/web/RaftChannelSimulated.js +4 -3
  28. package/dist/web/RaftChannelSimulated.js.map +1 -1
  29. package/dist/web/RaftConnector.d.ts +10 -1
  30. package/dist/web/RaftConnector.js +23 -10
  31. package/dist/web/RaftConnector.js.map +1 -1
  32. package/dist/web/RaftDeviceManager.d.ts +13 -1
  33. package/dist/web/RaftDeviceManager.js +224 -77
  34. package/dist/web/RaftDeviceManager.js.map +1 -1
  35. package/dist/web/RaftDeviceMgrIF.d.ts +5 -1
  36. package/dist/web/RaftDeviceStates.d.ts +20 -2
  37. package/dist/web/RaftDeviceStates.js +25 -4
  38. package/dist/web/RaftDeviceStates.js.map +1 -1
  39. package/dist/web/RaftPublish.d.ts +2 -0
  40. package/dist/web/RaftPublish.js +81 -0
  41. package/dist/web/RaftPublish.js.map +1 -0
  42. package/dist/web/RaftStreamHandler.d.ts +11 -0
  43. package/dist/web/RaftStreamHandler.js +66 -0
  44. package/dist/web/RaftStreamHandler.js.map +1 -1
  45. package/dist/web/RaftSystemUtils.d.ts +17 -1
  46. package/dist/web/RaftSystemUtils.js +51 -0
  47. package/dist/web/RaftSystemUtils.js.map +1 -1
  48. package/dist/web/RaftTypes.d.ts +21 -0
  49. package/dist/web/RaftTypes.js.map +1 -1
  50. package/dist/web/main.d.ts +1 -0
  51. package/dist/web/main.js +1 -0
  52. package/dist/web/main.js.map +1 -1
  53. package/examples/dashboard/package.json +1 -1
  54. package/examples/dashboard/src/DeviceActionsForm.tsx +2 -2
  55. package/examples/dashboard/src/DevicePanel.tsx +79 -3
  56. package/examples/dashboard/src/DeviceStatsPanel.tsx +65 -0
  57. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  58. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  59. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -2
  60. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  61. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  62. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +38 -4
  63. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +10 -2
  64. package/examples/dashboard/src/styles.css +162 -0
  65. package/package.json +49 -49
  66. package/src/RaftChannelSimulated.ts +4 -3
  67. package/src/RaftConnector.ts +34 -13
  68. package/src/RaftDeviceManager.ts +251 -81
  69. package/src/RaftDeviceMgrIF.ts +5 -1
  70. package/src/RaftDeviceStates.ts +35 -5
  71. package/src/RaftPublish.ts +92 -0
  72. package/src/RaftStreamHandler.ts +84 -1
  73. package/src/RaftSystemUtils.ts +59 -0
  74. package/src/RaftTypes.ts +27 -0
  75. package/src/main.ts +1 -0
@@ -7,7 +7,7 @@
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
13
  import { DeviceTypeInfo, DeviceTypeAction, DeviceTypeInfoRecs, RaftDevTypeInfoResponse } from "./RaftDeviceInfo";
@@ -29,6 +29,10 @@ export interface DeviceDecodedData {
29
29
  fromOfflineBuffer?: boolean;
30
30
  }
31
31
 
32
+ interface DeviceStatsInternal extends DeviceStats {
33
+ windowEvents: Array<{ timeMs: number; samples: number }>;
34
+ }
35
+
32
36
  export class DeviceManager implements RaftDeviceMgrIF{
33
37
 
34
38
  // Max data points to store
@@ -58,10 +62,15 @@ export class DeviceManager implements RaftDeviceMgrIF{
58
62
  private _newDeviceAttributeCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
59
63
  private _newAttributeDataCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
60
64
  private _decodedDataCallbacks: Array<(decoded: DeviceDecodedData) => void> = [];
65
+ private _deviceRemovedCallbacks: Array<(deviceKey: string, state: DeviceState) => void> = [];
61
66
 
62
67
  // Debug message index (to help debug with async messages)
63
68
  private _debugMsgIndex = 0;
64
69
 
70
+ // Device stats (sample counts, rates)
71
+ private _statsWindowMs = 5000;
72
+ private _deviceStats: { [deviceKey: string]: DeviceStatsInternal } = {};
73
+
65
74
  public getDevicesState(): DevicesState {
66
75
  return this._devicesState;
67
76
  }
@@ -70,6 +79,14 @@ export class DeviceManager implements RaftDeviceMgrIF{
70
79
  return this._devicesState[deviceKey];
71
80
  }
72
81
 
82
+ public getDeviceStats(deviceKey: string): DeviceStats {
83
+ return this.cloneDeviceStats(this.getOrCreateDeviceStats(deviceKey));
84
+ }
85
+
86
+ public resetDeviceStats(deviceKey: string): void {
87
+ this._deviceStats[deviceKey] = this.createEmptyStats();
88
+ }
89
+
73
90
  // Cached device type data
74
91
  private _cachedDeviceTypeRecs: DeviceTypeInfoRecs = {};
75
92
 
@@ -169,6 +186,16 @@ export class DeviceManager implements RaftDeviceMgrIF{
169
186
  this._decodedDataCallbacks = this._decodedDataCallbacks.filter((cb) => cb !== callback);
170
187
  }
171
188
 
189
+ public addDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
190
+ if (!this._deviceRemovedCallbacks.includes(callback)) {
191
+ this._deviceRemovedCallbacks.push(callback);
192
+ }
193
+ }
194
+
195
+ public removeDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
196
+ this._deviceRemovedCallbacks = this._deviceRemovedCallbacks.filter((cb) => cb !== callback);
197
+ }
198
+
172
199
  ////////////////////////////////////////////////////////////////////////////
173
200
  // Set the friendly name for the device
174
201
  ////////////////////////////////////////////////////////////////////////////
@@ -185,79 +212,129 @@ export class DeviceManager implements RaftDeviceMgrIF{
185
212
  public async handleClientMsgBinary(rxMsg: Uint8Array) {
186
213
  // console.log(`DeviceManager client1 msg ${RaftUtils.bufferToHex(rxMsg)}`);
187
214
 
188
- // Example messages
189
- // 0080 0015 81 0000006a 0004 53b7 feff00000100081857079314 0011 80 00000000 0011 53b2 075106e400d60054 0010 80 00000000 0012 5231 000d0000010e01
190
- // 0080 0011 80 00000000 0002 4ae1 0787052606240007 000e 80 00000000 0003 0006 030001af01
191
- // 0080 0011 80 00000000 0002 e46e 061e05a206830433 0010 80000000000003e4760006030001c701
192
- // 0080 0010 81 00000015 0004 e4a2 0650fe00305002 0011 80000000000002e4a8061f059f06850438 001080000000000003e4aa0006030001c701
193
-
194
- // 0080 0011 80 00000000 0002 31e4 05ea05a506660137 000e 80 00000000 0003 0007 030001d901
195
-
196
- // 0080 0011 80 00000000 0002 4d63 0792053e06500061 000e 80 00000000 0003 0005 030001de01
197
-
198
- // First two bytes of each message are the message type (0080)
199
- // There are then a series of sections each of which is the data for a device
200
- // First two bytes of each section is the section length (big endian) not including the section length bytes themselves
201
- // Next byte is the connection mode (0 for direct connect, 1+ for bus number) and the MSB of this byte is 1 if the device is online
202
- // Next is the device address (4 bytes big endian)
203
- // Next is the device type index (2 bytes big endian)
204
- // Finally the device data which can be one or more groups of attributes defined by the schema
215
+ // DevBIN message format
216
+ //
217
+ // The rxMsg passed to this function has a 2-byte message type prefix (e.g. 0x0080)
218
+ // added by the transport layer. After that prefix comes a devbin frame:
219
+ //
220
+ // Devbin envelope (2 bytes):
221
+ // Byte 0: magic+version 0xDB = devbin v1 (valid range 0xDB–0xDF for v1–v5)
222
+ // Byte 1: topicIndex 0x00–0xFE = topic index; 0xFF = no topic
223
+ //
224
+ // Then zero or more per-device records, concatenated back-to-back:
225
+ // Bytes 0-1: recordLen uint16 big-endian number of body bytes that follow (min 7)
226
+ // Byte 2: statusBus bit 7 = online flag, bit 6 = pending deletion, bits 5:4 = reserved, bits 3:0 = bus number (0-15)
227
+ // Bytes 3-6: address uint32 big-endian device address on the bus
228
+ // Bytes 7-8: devTypeIdx uint16 big-endian device type table index
229
+ // Bytes 9+: pollData variable length (recordLen 7 bytes) device data
230
+ //
231
+ // Example message (with transport prefix):
232
+ // 0080 DB01 0015 81 0000076a 000b bff10000ffffffff7a07d1f1221c 000e 80 00000000 001f bc340000030001
233
+ // ^^^^ ^^^^ ^^^^
234
+ // | | | || | | | Record 2 ...
235
+ // | | | || | | pollData (14 bytes)
236
+ // | | | || | devTypeIdx = 0x000b (11)
237
+ // | | | || address = 0x0000076a (slot 7, I2C addr 0x6a)
238
+ // | | | |busInfo = 0x81 (bus 1, online)
239
+ // | | | recordLen = 0x0015 (21 bytes)
240
+ // | | topicIndex = 0x01
241
+ // | magic+version = 0xDB (devbin v1)
242
+ // msgType prefix (transport layer)
205
243
 
206
244
  // Debug
207
245
  // const debugMsgTime = Date.now();
208
246
  const debugMsgIndex = this._debugMsgIndex++;
209
247
 
210
- // Message layout
211
- const msgTypeLen = 2; // Length of the message type (first two bytes)
212
- const sectionLengthLen = 2; // Length of the inclusive section length (first two bytes of each section)
213
- const sectionConnectionModeLen = 1; // Length of the connection mode (next byte after section length)
214
- const sectionDeviceAddrLen = 4; // Length of the device address (next 4 bytes after connection mode)
215
- const sectionDeviceTypeIdxLen = 2; // Length of the device type index (next 2 bytes after device address)
216
- const sectionHeaderLen = sectionConnectionModeLen + sectionDeviceAddrLen + sectionDeviceTypeIdxLen;
248
+ // Message layout constants
249
+ const msgTypeLen = 2; // Transport-layer message type prefix (first two bytes, e.g. 0x0080)
250
+ const devbinEnvelopeLen = 2; // Devbin envelope: magic+version (1 byte) + topicIndex (1 byte)
251
+ const devbinMagicMin = 0xDB;
252
+ const devbinMagicMax = 0xDF;
253
+ const recordLenLen = 2; // Per-record length prefix (uint16 big-endian)
254
+ const busInfoLen = 1; // statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
255
+ const deviceAddrLen = 4; // Device address (uint32 big-endian)
256
+ const devTypeIdxLen = 2; // Device type index (uint16 big-endian)
257
+ const recordHeaderLen = busInfoLen + deviceAddrLen + devTypeIdxLen; // = 7, minimum record body
217
258
 
218
259
  // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} rxMsg.length ${rxMsg.length} rxMsg ${RaftUtils.bufferToHex(rxMsg)}`);
219
260
 
220
261
  // Start after the message type
221
262
  let msgPos = msgTypeLen;
222
263
 
223
- // Iterate through sections
264
+ // Check for devbin envelope (magic+version + topicIndex)
265
+ if (rxMsg.length >= msgTypeLen + devbinEnvelopeLen) {
266
+ const envelopeMagicVer = rxMsg[msgTypeLen];
267
+ if ((envelopeMagicVer & 0xF0) === 0xD0) {
268
+ if ((envelopeMagicVer < devbinMagicMin) || (envelopeMagicVer > devbinMagicMax)) {
269
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid devbin envelope magic/version ${envelopeMagicVer}`);
270
+ return;
271
+ }
272
+
273
+ const topicIndex = rxMsg[msgTypeLen + 1];
274
+ if (topicIndex !== 0xFF) {
275
+ const topicName = this._systemUtils?.getPublishTopicName(topicIndex);
276
+ if (topicName && topicName !== "devbin") {
277
+ return;
278
+ }
279
+ }
280
+
281
+ msgPos += devbinEnvelopeLen;
282
+ }
283
+ }
284
+
285
+ // Iterate through device records
224
286
  while (msgPos < rxMsg.length) {
225
287
 
226
- // Check length
288
+ // Check minimum length for record length prefix + record header
227
289
  const remainingLen = rxMsg.length - msgPos;
228
- if (remainingLen < sectionLengthLen + sectionHeaderLen) {
229
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid length ${rxMsg.length} < ${sectionLengthLen + sectionHeaderLen + msgPos}`);
290
+ if (remainingLen < recordLenLen + recordHeaderLen) {
291
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid length ${rxMsg.length} < ${recordLenLen + recordHeaderLen + msgPos}`);
230
292
  return;
231
293
  }
232
294
 
233
- // Get the length of the section
234
- const sectionLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
235
- if (sectionLen > remainingLen + sectionLengthLen) {
236
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} msgLen ${sectionLen} remainingLenAfterLenBytes ${remainingLen-sectionLengthLen}`);
295
+ // Get the record body length (bytes that follow the 2-byte length prefix)
296
+ const recordLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
297
+ if (recordLen > remainingLen - recordLenLen) {
298
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} recordLen ${recordLen} remainingAfterLenBytes ${remainingLen - recordLenLen}`);
237
299
  return;
238
300
  }
239
301
 
240
- // Extract message elements
241
- let sectionPos = msgPos + sectionLengthLen;
242
- const busNum = rxMsg[sectionPos] & 0x7f;
243
- const isOnline = (rxMsg[sectionPos] & 0x80) !== 0;
244
- sectionPos += sectionConnectionModeLen;
245
- // Get the device address and type index
246
- const devAddr = (rxMsg[sectionPos] << 24) + (rxMsg[sectionPos + 1] << 16) + (rxMsg[sectionPos + 2] << 8) + rxMsg[sectionPos + 3];
247
- sectionPos += sectionDeviceAddrLen;
248
- const devTypeIdx = (rxMsg[sectionPos] << 8) + rxMsg[sectionPos + 1];
249
- let attrGroupPos = sectionPos + sectionDeviceTypeIdxLen;
302
+ // Extract record header fields
303
+ let recordPos = msgPos + recordLenLen;
304
+
305
+ // statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
306
+ const statusByte = rxMsg[recordPos];
307
+ const busNum = statusByte & 0x0f;
308
+ const isOnline = (statusByte & 0x80) !== 0;
309
+ const isPendingDeletion = (statusByte & 0x40) !== 0;
310
+ recordPos += busInfoLen;
311
+
312
+ // Device address (uint32 big-endian)
313
+ const devAddr = (rxMsg[recordPos] << 24) + (rxMsg[recordPos + 1] << 16) + (rxMsg[recordPos + 2] << 8) + rxMsg[recordPos + 3];
314
+ recordPos += deviceAddrLen;
315
+
316
+ // Device type index (uint16 big-endian)
317
+ const devTypeIdx = (rxMsg[recordPos] << 8) + rxMsg[recordPos + 1];
318
+ let pollDataPos = recordPos + devTypeIdxLen;
250
319
 
251
320
  // Debug
252
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length} sectionPos ${msgPos} sectionLen ${sectionLen} ${attrGroupPos} ${RaftUtils.bufferToHex(rxMsg.slice(msgPos, msgPos + sectionLen))}`);
253
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} connMode ${busNum} isOnline ${isOnline} devAddr ${devAddr} devTypeIdx ${devTypeIdx} attrGroupDataLen ${sectionLen - sectionHeaderLen}`);
321
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length} recordStart ${msgPos} recordLen ${recordLen} ${pollDataPos} ${RaftUtils.bufferToHex(rxMsg.slice(msgPos, msgPos + recordLenLen + recordLen))}`);
322
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} bus ${busNum} isOnline ${isOnline} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} pollDataLen ${recordLen - recordHeaderLen}`);
254
323
 
255
- // Device key
256
- const deviceKey = getDeviceKey(busNum.toString(), devAddr.toString(16));
324
+ // Format device address as canonical hex and build device key
325
+ const devAddrHex = formatDeviceAddrHex(devAddr);
326
+ const deviceKey = getDeviceKey(busNum.toString(), devAddrHex);
257
327
 
258
328
  // Update the last update time
259
329
  this._deviceLastUpdateTime[deviceKey] = Date.now();
260
330
 
331
+ // Handle pending deletion - remove device and skip further processing
332
+ if (isPendingDeletion) {
333
+ this.removeDevice(deviceKey);
334
+ msgPos += recordLenLen + recordLen;
335
+ continue;
336
+ }
337
+
261
338
  // Check if a device state already exists
262
339
  if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
263
340
 
@@ -265,13 +342,13 @@ export class DeviceManager implements RaftDeviceMgrIF{
265
342
  const deviceTypeInfo = await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
266
343
 
267
344
  // Debug
268
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} busNum ${busNum} devAddr ${devAddr} devTypeIdx ${devTypeIdx} deviceTypeInfo ${JSON.stringify(deviceTypeInfo)}`);
345
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} busNum ${busNum} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} deviceTypeInfo ${JSON.stringify(deviceTypeInfo)}`);
269
346
 
270
347
  // Handle case where device type info is not available
271
348
  if (deviceTypeInfo === undefined) {
272
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this section`);
273
- // Skip to next section without processing attributes
274
- msgPos += sectionLengthLen + sectionLen;
349
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this record`);
350
+ // Skip to next record without processing attributes
351
+ msgPos += recordLenLen + recordLen;
275
352
  continue;
276
353
  }
277
354
 
@@ -281,7 +358,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
281
358
  this._devicesState[deviceKey].deviceTypeInfo = deviceTypeInfo;
282
359
  this._devicesState[deviceKey].deviceType = deviceTypeInfo.name || "";
283
360
  this._devicesState[deviceKey].busName = busNum.toString();
284
- this._devicesState[deviceKey].deviceAddress = devAddr.toString();
361
+ this._devicesState[deviceKey].deviceAddress = devAddrHex;
285
362
  }
286
363
  } else {
287
364
  // Create device record - device type info may be undefined
@@ -295,8 +372,8 @@ export class DeviceManager implements RaftDeviceMgrIF{
295
372
  deviceAttributes: {},
296
373
  deviceIsNew: true,
297
374
  stateChanged: false,
298
- isOnline: true,
299
- deviceAddress: devAddr.toString(),
375
+ onlineState: DeviceOnlineState.Online,
376
+ deviceAddress: devAddrHex,
300
377
  deviceType: deviceTypeInfo?.name || "",
301
378
  busName: busNum.toString()
302
379
  };
@@ -305,7 +382,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
305
382
 
306
383
  // Get device state
307
384
  const deviceState = this._devicesState[deviceKey];
308
- deviceState.isOnline = isOnline;
385
+ deviceState.onlineState = isOnline ? DeviceOnlineState.Online : DeviceOnlineState.Offline;
309
386
 
310
387
  // Check if device type info is available and complete
311
388
  if (deviceState.deviceTypeInfo && deviceState.deviceTypeInfo.resp) {
@@ -313,25 +390,25 @@ export class DeviceManager implements RaftDeviceMgrIF{
313
390
  // Iterate over attributes in the group
314
391
  const pollRespMetadata = deviceState.deviceTypeInfo!.resp!;
315
392
 
316
- // Iterate over attribute groups
317
- const attrGroupDataLen = sectionLen - sectionHeaderLen;
318
- const attrGroupStartPos = attrGroupPos;
393
+ // Process poll data (recordLen - recordHeaderLen bytes)
394
+ const pollDataLen = recordLen - recordHeaderLen;
395
+ const pollDataStartPos = pollDataPos;
319
396
  const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
320
397
  const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
321
- while (attrGroupPos < attrGroupStartPos + attrGroupDataLen) {
398
+ while (pollDataPos < pollDataStartPos + pollDataLen) {
322
399
 
323
400
  // Add bounds checking
324
- if (attrGroupPos >= rxMsg.length) {
325
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} exceeds message length ${rxMsg.length}`);
401
+ if (pollDataPos >= rxMsg.length) {
402
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} exceeds message length ${rxMsg.length}`);
326
403
  break;
327
404
  }
328
405
 
329
- const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, attrGroupPos,
406
+ const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, pollDataPos,
330
407
  deviceState.deviceTimeline, pollRespMetadata,
331
408
  deviceState.deviceAttributes,
332
409
  this._maxDatapointsToStore);
333
410
 
334
- // console.log(`DevMan.handleClientMsgBinary decoded debugIdx ${debugMsgIndex} devType ${deviceState.deviceTypeInfo.name} attrGroupDataLen ${attrGroupDataLen} attrGroupPos ${attrGroupPos} sectionLen ${sectionLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen} pollRespMetadata ${JSON.stringify(pollRespMetadata)}`);
411
+ // console.log(`DevMan.handleClientMsgBinary decoded debugIdx ${debugMsgIndex} devType ${deviceState.deviceTypeInfo.name} pollDataLen ${pollDataLen} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen} pollRespMetadata ${JSON.stringify(pollRespMetadata)}`);
335
412
 
336
413
  if (newMsgBufIdx < 0)
337
414
  {
@@ -340,32 +417,34 @@ export class DeviceManager implements RaftDeviceMgrIF{
340
417
  }
341
418
 
342
419
  // Prevent infinite loops
343
- if (newMsgBufIdx <= attrGroupPos) {
344
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup didn't advance position from ${attrGroupPos} to ${newMsgBufIdx}`);
420
+ if (newMsgBufIdx <= pollDataPos) {
421
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup didn't advance position from ${pollDataPos} to ${newMsgBufIdx}`);
345
422
  break;
346
423
  }
347
424
 
348
- attrGroupPos = newMsgBufIdx;
425
+ pollDataPos = newMsgBufIdx;
349
426
  deviceState.stateChanged = true;
350
427
 
351
428
  // console.log(`debugMsgTime ${debugMsgTime} newPt debugMsgIdx ${debugMsgIndex} rxMsgLen ${rxMsg.length} devType ${deviceState.deviceTypeInfo!.name} timestampsUs ${deviceState.deviceTimeline.timestampsUs[deviceState.deviceTimeline.timestampsUs.length - 1]} curTimelineLen ${deviceState.deviceTimeline.timestampsUs.length}`);
352
429
 
353
-
354
- // console.log(`DevMan.handleClientMsgBinary group done debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} sectionLen ${sectionLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
430
+ // console.log(`DevMan.handleClientMsgBinary group done debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
355
431
  }
356
432
 
357
433
  // Inform decoded-data callbacks
358
- this.emitDecodedData(deviceKey, busNum.toString(), devAddr.toString(), deviceState,
434
+ this.emitDecodedData(deviceKey, busNum.toString(), devAddrHex, deviceState,
359
435
  pollRespMetadata, attrLengthsBefore, timelineLenBefore);
436
+
437
+ const newSamples = deviceState.deviceTimeline.timestampsUs.length - timelineLenBefore;
438
+ this.updateDeviceStats(deviceKey, newSamples, Date.now());
360
439
  } else {
361
440
  console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceState incomplete for device ${deviceKey}, skipping attribute processing`);
362
441
  }
363
442
 
364
443
  // Debug
365
- // console.log(`DevMan.handleClientMsgBinary section done debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} sectionLen ${sectionLen} msgPos ${msgPos} newMsgPos ${msgPos + sectionLengthLen + sectionLen} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
444
+ // console.log(`DevMan.handleClientMsgBinary record done debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} newMsgPos ${msgPos + recordLenLen + recordLen} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
366
445
 
367
- // Move to next message
368
- msgPos += sectionLengthLen + sectionLen;
446
+ // Advance past this record (recordLenLen + recordLen bytes)
447
+ msgPos += recordLenLen + recordLen;
369
448
  }
370
449
 
371
450
  // Check for devices that have not been updated for a while
@@ -374,6 +453,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
374
453
  Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
375
454
  if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
376
455
  delete this._devicesState[deviceKey];
456
+ delete this._deviceStats[deviceKey];
377
457
  }
378
458
  });
379
459
  }
@@ -394,6 +474,11 @@ export class DeviceManager implements RaftDeviceMgrIF{
394
474
  // Iterate over the buses
395
475
  Object.entries(data).forEach(([busName, devices]) => {
396
476
 
477
+ // Check the bus name doesn't start with _ which is reserved for non-device information such as topic name
478
+ if (busName.startsWith("_")) {
479
+ return;
480
+ }
481
+
397
482
  // Check for bus status info
398
483
  if (devices && typeof devices === "object" && "_s" in devices) {
399
484
  // console.log(`DeviceManager bus status ${JSON.stringify(devices._s)}`);
@@ -414,7 +499,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
414
499
  if (attrGroups && typeof attrGroups === 'object' && "_t" in attrGroups && typeof attrGroups._t === "string") {
415
500
  deviceTypeName = attrGroups._t || "";
416
501
  } else if (attrGroups && typeof attrGroups === 'object' && "_i" in attrGroups && typeof attrGroups._i === "number") {
417
- deviceTypeIdx = attrGroups._i || -1;
502
+ deviceTypeIdx = attrGroups._i ?? -1;
418
503
  deviceTypeName = deviceTypeIdx.toString();
419
504
  } else
420
505
  {
@@ -454,7 +539,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
454
539
  deviceAttributes: {},
455
540
  deviceIsNew: true,
456
541
  stateChanged: false,
457
- isOnline: true,
542
+ onlineState: DeviceOnlineState.Online,
458
543
  deviceAddress: devAddr,
459
544
  deviceType: deviceTypeName,
460
545
  busName: busName
@@ -465,9 +550,15 @@ export class DeviceManager implements RaftDeviceMgrIF{
465
550
  // Get device state
466
551
  const deviceState = this._devicesState[deviceKey];
467
552
 
468
- // Check for online/offline state information
553
+ // Check for online/offline/pending-deletion state information
469
554
  if (attrGroups && typeof attrGroups === "object" && "_o" in attrGroups) {
470
- deviceState.isOnline = ((attrGroups._o === true) || (attrGroups._o === "1") || (attrGroups._o === 1));
555
+ const onlineStateVal = typeof attrGroups._o === 'number' ? attrGroups._o : parseInt(String(attrGroups._o), 10);
556
+ if (onlineStateVal === 2) {
557
+ // Pending deletion - remove device and skip further processing
558
+ this.removeDevice(deviceKey);
559
+ return;
560
+ }
561
+ deviceState.onlineState = onlineStateVal === 1 ? DeviceOnlineState.Online : DeviceOnlineState.Offline;
471
562
  }
472
563
 
473
564
  // Check if device type info is available
@@ -517,6 +608,9 @@ export class DeviceManager implements RaftDeviceMgrIF{
517
608
 
518
609
  this.emitDecodedData(deviceKey, busName, devAddr, deviceState, pollRespMetadata,
519
610
  attrLengthsBefore, timelineLenBefore, attrGroupName, markers);
611
+
612
+ const newSamples = deviceState.deviceTimeline.timestampsUs.length - timelineLenBefore;
613
+ this.updateDeviceStats(deviceKey, newSamples, Date.now());
520
614
  });
521
615
  });
522
616
  });
@@ -527,6 +621,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
527
621
  Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
528
622
  if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
529
623
  delete this._devicesState[deviceKey];
624
+ delete this._deviceStats[deviceKey];
530
625
  }
531
626
  });
532
627
  }
@@ -564,6 +659,22 @@ export class DeviceManager implements RaftDeviceMgrIF{
564
659
  });
565
660
  }
566
661
 
662
+ ////////////////////////////////////////////////////////////////////////////
663
+ // Remove a device (e.g. on pending deletion)
664
+ ////////////////////////////////////////////////////////////////////////////
665
+
666
+ private removeDevice(deviceKey: string): void {
667
+ // Snapshot the state before removal for callbacks
668
+ const deviceState = this._devicesState[deviceKey];
669
+ if (deviceState) {
670
+ deviceState.onlineState = DeviceOnlineState.PendingDeletion;
671
+ this._deviceRemovedCallbacks.forEach((cb) => cb(deviceKey, deviceState));
672
+ }
673
+ delete this._devicesState[deviceKey];
674
+ delete this._deviceLastUpdateTime[deviceKey];
675
+ delete this._deviceStats[deviceKey];
676
+ }
677
+
567
678
  ////////////////////////////////////////////////////////////////////////////
568
679
  // Get device type info
569
680
  ////////////////////////////////////////////////////////////////////////////
@@ -686,9 +797,8 @@ export class DeviceManager implements RaftDeviceMgrIF{
686
797
  // Add prefix and postfix
687
798
  writeHexStr = (action.w ? action.w : "") + writeHexStr + (action.wz ? action.wz : "");
688
799
 
689
- // Separate the bus and address in the deviceKey (_ char)
690
- const devBus = deviceKey.split("_")[0]
691
- const devAddr = deviceKey.split("_")[1]
800
+ // Parse the device key into bus and address components
801
+ const { bus: devBus, addr: devAddr } = parseDeviceKey(deviceKey);
692
802
 
693
803
  // Send the action to the server
694
804
  const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + writeHexStr;
@@ -755,6 +865,66 @@ export class DeviceManager implements RaftDeviceMgrIF{
755
865
  return bytes;
756
866
  }
757
867
 
868
+ ////////////////////////////////////////////////////////////////////////////
869
+ // Helpers for device stats
870
+ ////////////////////////////////////////////////////////////////////////////
871
+
872
+ private createEmptyStats(): DeviceStatsInternal {
873
+ return {
874
+ totalSamples: 0,
875
+ windowMs: this._statsWindowMs,
876
+ windowSamples: 0,
877
+ sampleRateHz: 0,
878
+ lastSampleTimeMs: null,
879
+ lastUpdateTimeMs: null,
880
+ windowEvents: []
881
+ };
882
+ }
883
+
884
+ private getOrCreateDeviceStats(deviceKey: string): DeviceStatsInternal {
885
+ if (!this._deviceStats[deviceKey]) {
886
+ this._deviceStats[deviceKey] = this.createEmptyStats();
887
+ }
888
+ return this._deviceStats[deviceKey];
889
+ }
890
+
891
+ private cloneDeviceStats(stats: DeviceStatsInternal): DeviceStats {
892
+ return {
893
+ totalSamples: stats.totalSamples,
894
+ windowMs: stats.windowMs,
895
+ windowSamples: stats.windowSamples,
896
+ sampleRateHz: stats.sampleRateHz,
897
+ lastSampleTimeMs: stats.lastSampleTimeMs,
898
+ lastUpdateTimeMs: stats.lastUpdateTimeMs
899
+ };
900
+ }
901
+
902
+ private updateDeviceStats(deviceKey: string, newSamples: number, nowMs: number): void {
903
+ const stats = this.getOrCreateDeviceStats(deviceKey);
904
+ stats.lastUpdateTimeMs = nowMs;
905
+
906
+ if (newSamples > 0) {
907
+ stats.totalSamples += newSamples;
908
+ stats.lastSampleTimeMs = nowMs;
909
+ stats.windowEvents.push({ timeMs: nowMs, samples: newSamples });
910
+ }
911
+
912
+ const windowStartMs = nowMs - stats.windowMs;
913
+ while (stats.windowEvents.length > 0 && stats.windowEvents[0].timeMs < windowStartMs) {
914
+ stats.windowEvents.shift();
915
+ }
916
+
917
+ const windowSamples = stats.windowEvents.reduce((sum, entry) => sum + entry.samples, 0);
918
+ stats.windowSamples = windowSamples;
919
+ if (stats.windowEvents.length === 0) {
920
+ stats.sampleRateHz = 0;
921
+ return;
922
+ }
923
+
924
+ const actualWindowMs = Math.max(1, nowMs - stats.windowEvents[0].timeMs);
925
+ stats.sampleRateHz = (windowSamples * 1000) / actualWindowMs;
926
+ }
927
+
758
928
  ////////////////////////////////////////////////////////////////////////////
759
929
  // Helpers for decoded data callbacks
760
930
  ////////////////////////////////////////////////////////////////////////////
@@ -765,7 +935,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
765
935
  return lengths;
766
936
  }
767
937
  pollRespMetadata.a.forEach((attr) => {
768
- lengths[attr.n] = deviceAttrs[attr.n]?.values.length || 0;
938
+ lengths[attr.n] = deviceAttrs[attr.n]?.values.length ?? 0;
769
939
  });
770
940
  return lengths;
771
941
  }
@@ -794,7 +964,7 @@ export class DeviceManager implements RaftDeviceMgrIF{
794
964
  if (!attrState) {
795
965
  return;
796
966
  }
797
- const prevLen = attrLengthsBefore[attr.n] || 0;
967
+ const prevLen = attrLengthsBefore[attr.n] ?? 0;
798
968
  if (attrState.values.length > prevLen) {
799
969
  attrValues[attr.n] = attrState.values.slice(prevLen);
800
970
  hasValues = hasValues || attrValues[attr.n].length > 0;
@@ -8,13 +8,15 @@
8
8
  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
9
 
10
10
  import { DeviceTypeAction } from "./RaftDeviceInfo";
11
- import { DeviceAttributeState, DevicesState, DeviceState } from "./RaftDeviceStates";
11
+ import { DeviceAttributeState, DevicesState, DeviceState, DeviceStats } from "./RaftDeviceStates";
12
12
 
13
13
  export default interface RaftDeviceMgrIF {
14
14
 
15
15
  // Get state of devices
16
16
  getDevicesState(): DevicesState;
17
17
  getDeviceState(deviceKey: string): DeviceState;
18
+ getDeviceStats(deviceKey: string): DeviceStats;
19
+ resetDeviceStats(deviceKey: string): void;
18
20
 
19
21
  // Settings
20
22
  setMaxDataPointsToStore(maxDataPointsToStore: number): void;
@@ -26,6 +28,8 @@ export default interface RaftDeviceMgrIF {
26
28
  removeNewAttributeCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void;
27
29
  addAttributeDataCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void;
28
30
  removeAttributeDataCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void;
31
+ addDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void;
32
+ removeDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void;
29
33
 
30
34
  // Send action to device
31
35
  sendAction(deviceKey: string, action: DeviceTypeAction, data: number[]): void;