@robotical/raftjs 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/devdocs/devbin-backwards-compatibility.md +105 -0
  2. package/devdocs/pseudocode-to-js-transpiler.md +563 -0
  3. package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
  4. package/dist/react-native/PseudocodeTranspiler.js +115 -0
  5. package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
  6. package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
  7. package/dist/react-native/RaftAttributeHandler.js +108 -32
  8. package/dist/react-native/RaftAttributeHandler.js.map +1 -1
  9. package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
  10. package/dist/react-native/RaftChannelBLE.web.js +59 -21
  11. package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
  12. package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
  13. package/dist/react-native/RaftChannelSimulated.js +9 -5
  14. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  15. package/dist/react-native/RaftChannelWebSocket.js +16 -1
  16. package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
  17. package/dist/react-native/RaftConnector.d.ts +11 -1
  18. package/dist/react-native/RaftConnector.js +75 -9
  19. package/dist/react-native/RaftConnector.js.map +1 -1
  20. package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
  21. package/dist/react-native/RaftCustomAttrHandler.js +32 -44
  22. package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
  23. package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
  24. package/dist/react-native/RaftDeviceInfo.js +8 -0
  25. package/dist/react-native/RaftDeviceInfo.js.map +1 -1
  26. package/dist/react-native/RaftDeviceManager.d.ts +30 -3
  27. package/dist/react-native/RaftDeviceManager.js +618 -107
  28. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  29. package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
  30. package/dist/react-native/RaftDeviceStates.d.ts +27 -3
  31. package/dist/react-native/RaftDeviceStates.js +31 -6
  32. package/dist/react-native/RaftDeviceStates.js.map +1 -1
  33. package/dist/react-native/RaftFileHandler.d.ts +0 -1
  34. package/dist/react-native/RaftFileHandler.js +61 -23
  35. package/dist/react-native/RaftFileHandler.js.map +1 -1
  36. package/dist/react-native/RaftPublish.d.ts +2 -0
  37. package/dist/react-native/RaftPublish.js +81 -0
  38. package/dist/react-native/RaftPublish.js.map +1 -0
  39. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  40. package/dist/react-native/RaftStreamHandler.js +66 -0
  41. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  42. package/dist/react-native/RaftStruct.d.ts +2 -2
  43. package/dist/react-native/RaftStruct.js +97 -26
  44. package/dist/react-native/RaftStruct.js.map +1 -1
  45. package/dist/react-native/RaftSystemType.d.ts +1 -0
  46. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  47. package/dist/react-native/RaftSystemUtils.js +51 -0
  48. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  49. package/dist/react-native/RaftTimezone.d.ts +16 -0
  50. package/dist/react-native/RaftTimezone.js +153 -0
  51. package/dist/react-native/RaftTimezone.js.map +1 -0
  52. package/dist/react-native/RaftTypes.d.ts +27 -1
  53. package/dist/react-native/RaftTypes.js.map +1 -1
  54. package/dist/react-native/RaftUpdateManager.js +1 -1
  55. package/dist/react-native/RaftUpdateManager.js.map +1 -1
  56. package/dist/react-native/main.d.ts +2 -0
  57. package/dist/react-native/main.js +6 -1
  58. package/dist/react-native/main.js.map +1 -1
  59. package/dist/web/PseudocodeTranspiler.d.ts +6 -0
  60. package/dist/web/PseudocodeTranspiler.js +115 -0
  61. package/dist/web/PseudocodeTranspiler.js.map +1 -0
  62. package/dist/web/RaftAttributeHandler.d.ts +1 -1
  63. package/dist/web/RaftAttributeHandler.js +108 -32
  64. package/dist/web/RaftAttributeHandler.js.map +1 -1
  65. package/dist/web/RaftChannelBLE.web.d.ts +4 -0
  66. package/dist/web/RaftChannelBLE.web.js +59 -21
  67. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  68. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  69. package/dist/web/RaftChannelSimulated.js +9 -5
  70. package/dist/web/RaftChannelSimulated.js.map +1 -1
  71. package/dist/web/RaftChannelWebSocket.js +16 -1
  72. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  73. package/dist/web/RaftConnector.d.ts +11 -1
  74. package/dist/web/RaftConnector.js +75 -9
  75. package/dist/web/RaftConnector.js.map +1 -1
  76. package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
  77. package/dist/web/RaftCustomAttrHandler.js +32 -44
  78. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  79. package/dist/web/RaftDeviceInfo.d.ts +18 -0
  80. package/dist/web/RaftDeviceInfo.js +8 -0
  81. package/dist/web/RaftDeviceInfo.js.map +1 -1
  82. package/dist/web/RaftDeviceManager.d.ts +30 -3
  83. package/dist/web/RaftDeviceManager.js +618 -107
  84. package/dist/web/RaftDeviceManager.js.map +1 -1
  85. package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
  86. package/dist/web/RaftDeviceStates.d.ts +27 -3
  87. package/dist/web/RaftDeviceStates.js +31 -6
  88. package/dist/web/RaftDeviceStates.js.map +1 -1
  89. package/dist/web/RaftFileHandler.d.ts +0 -1
  90. package/dist/web/RaftFileHandler.js +61 -23
  91. package/dist/web/RaftFileHandler.js.map +1 -1
  92. package/dist/web/RaftPublish.d.ts +2 -0
  93. package/dist/web/RaftPublish.js +81 -0
  94. package/dist/web/RaftPublish.js.map +1 -0
  95. package/dist/web/RaftStreamHandler.d.ts +11 -0
  96. package/dist/web/RaftStreamHandler.js +66 -0
  97. package/dist/web/RaftStreamHandler.js.map +1 -1
  98. package/dist/web/RaftStruct.d.ts +2 -2
  99. package/dist/web/RaftStruct.js +97 -26
  100. package/dist/web/RaftStruct.js.map +1 -1
  101. package/dist/web/RaftSystemType.d.ts +1 -0
  102. package/dist/web/RaftSystemUtils.d.ts +17 -1
  103. package/dist/web/RaftSystemUtils.js +51 -0
  104. package/dist/web/RaftSystemUtils.js.map +1 -1
  105. package/dist/web/RaftTimezone.d.ts +16 -0
  106. package/dist/web/RaftTimezone.js +153 -0
  107. package/dist/web/RaftTimezone.js.map +1 -0
  108. package/dist/web/RaftTypes.d.ts +27 -1
  109. package/dist/web/RaftTypes.js.map +1 -1
  110. package/dist/web/RaftUpdateManager.js +1 -1
  111. package/dist/web/RaftUpdateManager.js.map +1 -1
  112. package/dist/web/main.d.ts +2 -0
  113. package/dist/web/main.js +6 -1
  114. package/dist/web/main.js.map +1 -1
  115. package/examples/dashboard/package.json +2 -2
  116. package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
  117. package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
  118. package/examples/dashboard/src/DevicePanel.tsx +92 -11
  119. package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
  120. package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
  121. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  122. package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
  123. package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
  124. package/examples/dashboard/src/LoggingPanel.tsx +264 -0
  125. package/examples/dashboard/src/Main.tsx +12 -2
  126. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  127. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
  128. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  129. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  130. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
  131. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
  132. package/examples/dashboard/src/styles.css +766 -1
  133. package/notes/web-ble-reconnect-retry.md +69 -0
  134. package/package.json +10 -7
  135. package/src/PseudocodeTranspiler.test.ts +372 -0
  136. package/src/PseudocodeTranspiler.ts +127 -0
  137. package/src/RaftAttributeHandler.ts +152 -76
  138. package/src/RaftChannelBLE.web.ts +62 -20
  139. package/src/RaftChannelSimulated.ts +10 -5
  140. package/src/RaftChannelWebSocket.ts +16 -2
  141. package/src/RaftConnector.ts +93 -15
  142. package/src/RaftCustomAttrHandler.ts +35 -45
  143. package/src/RaftDeviceInfo.ts +27 -0
  144. package/src/RaftDeviceManager.test.ts +164 -0
  145. package/src/RaftDeviceManager.ts +705 -127
  146. package/src/RaftDeviceMgrIF.ts +13 -2
  147. package/src/RaftDeviceStates.ts +49 -8
  148. package/src/RaftFileHandler.ts +69 -28
  149. package/src/RaftPublish.ts +92 -0
  150. package/src/RaftStreamHandler.ts +84 -1
  151. package/src/RaftStruct.test.ts +229 -0
  152. package/src/RaftStruct.ts +101 -37
  153. package/src/RaftSystemType.ts +1 -0
  154. package/src/RaftSystemUtils.ts +59 -0
  155. package/src/RaftTimezone.ts +151 -0
  156. package/src/RaftTypes.ts +34 -1
  157. package/src/RaftUpdateManager.ts +1 -1
  158. package/src/main.ts +2 -0
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.DeviceManager = void 0;
12
12
  const tslib_1 = require("tslib");
13
13
  const RaftDeviceStates_1 = require("./RaftDeviceStates");
14
+ const RaftDeviceInfo_1 = require("./RaftDeviceInfo");
14
15
  const RaftAttributeHandler_1 = tslib_1.__importDefault(require("./RaftAttributeHandler"));
15
16
  const RaftStruct_1 = require("./RaftStruct");
16
17
  class DeviceManager {
@@ -20,6 +21,12 @@ class DeviceManager {
20
21
  getDeviceState(deviceKey) {
21
22
  return this._devicesState[deviceKey];
22
23
  }
24
+ getDeviceStats(deviceKey) {
25
+ return this.cloneDeviceStats(this.getOrCreateDeviceStats(deviceKey));
26
+ }
27
+ resetDeviceStats(deviceKey) {
28
+ this._deviceStats[deviceKey] = this.createEmptyStats();
29
+ }
23
30
  // Constructor
24
31
  constructor() {
25
32
  // Max data points to store
@@ -42,8 +49,13 @@ class DeviceManager {
42
49
  this._newDeviceAttributeCallbacks = [];
43
50
  this._newAttributeDataCallbacks = [];
44
51
  this._decodedDataCallbacks = [];
52
+ this._deviceRemovedCallbacks = [];
45
53
  // Debug message index (to help debug with async messages)
46
54
  this._debugMsgIndex = 0;
55
+ // Device stats (sample counts, rates)
56
+ this._statsWindowMs = 5000;
57
+ this._deviceStats = {};
58
+ this._malformedSampleWarnLastMs = {};
47
59
  // Cached device type data
48
60
  this._cachedDeviceTypeRecs = {};
49
61
  // Cached device type previous attempt times
@@ -120,6 +132,14 @@ class DeviceManager {
120
132
  removeDecodedDataCallback(callback) {
121
133
  this._decodedDataCallbacks = this._decodedDataCallbacks.filter((cb) => cb !== callback);
122
134
  }
135
+ addDeviceRemovedCallback(callback) {
136
+ if (!this._deviceRemovedCallbacks.includes(callback)) {
137
+ this._deviceRemovedCallbacks.push(callback);
138
+ }
139
+ }
140
+ removeDeviceRemovedCallback(callback) {
141
+ this._deviceRemovedCallbacks = this._deviceRemovedCallbacks.filter((cb) => cb !== callback);
142
+ }
123
143
  ////////////////////////////////////////////////////////////////////////////
124
144
  // Set the friendly name for the device
125
145
  ////////////////////////////////////////////////////////////////////////////
@@ -133,75 +153,183 @@ class DeviceManager {
133
153
  ////////////////////////////////////////////////////////////////////////////
134
154
  async handleClientMsgBinary(rxMsg) {
135
155
  // console.log(`DeviceManager client1 msg ${RaftUtils.bufferToHex(rxMsg)}`);
136
- // Example messages
137
- // 0080 0015 81 0000006a 0004 53b7 feff00000100081857079314 0011 80 00000000 0011 53b2 075106e400d60054 0010 80 00000000 0012 5231 000d0000010e01
138
- // 0080 0011 80 00000000 0002 4ae1 0787052606240007 000e 80 00000000 0003 0006 030001af01
139
- // 0080 0011 80 00000000 0002 e46e 061e05a206830433 0010 80000000000003e4760006030001c701
140
- // 0080 0010 81 00000015 0004 e4a2 0650fe00305002 0011 80000000000002e4a8061f059f06850438 001080000000000003e4aa0006030001c701
141
- // 0080 0011 80 00000000 0002 31e4 05ea05a506660137 000e 80 00000000 0003 0007 030001d901
142
- // 0080 0011 80 00000000 0002 4d63 0792053e06500061 000e 80 00000000 0003 0005 030001de01
143
- // First two bytes of each message are the message type (0080)
144
- // There are then a series of sections each of which is the data for a device
145
- // First two bytes of each section is the section length (big endian) not including the section length bytes themselves
146
- // 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
147
- // Next is the device address (4 bytes big endian)
148
- // Next is the device type index (2 bytes big endian)
149
- // Finally the device data which can be one or more groups of attributes defined by the schema
156
+ var _a;
157
+ // DevBIN message format
158
+ //
159
+ // The rxMsg passed to this function has a 2-byte message type prefix (e.g. 0x0080)
160
+ // added by the transport layer. After that prefix comes a devbin frame:
161
+ //
162
+ // Current devbin envelope (3 bytes):
163
+ // Byte 0: magic+version 0xDB (valid range 0xDB–0xDF)
164
+ // Byte 1: topicIndex 0x00–0xFE = topic index; 0xFF = no topic
165
+ // Byte 2: envelopeSeqNum uint8, wrapping detects whole-frame drops
166
+ //
167
+ // Then zero or more per-device records, concatenated back-to-back:
168
+ // Bytes 0-1: recordLen uint16 big-endian number of body bytes that follow (min 8)
169
+ // Byte 2: statusBus bit 7 = online flag, bit 6 = pending deletion, bits 3:0 = bus number
170
+ // Bytes 3-6: address uint32 big-endian — device address on the bus
171
+ // Bytes 7-8: devTypeIdx uint16 big-endian — device type table index
172
+ // Byte 9: deviceSeqNum uint8, wrapping — per-device drop detection
173
+ // Bytes 10+: samples length-prefixed: [sampleLen(1B)][sampleData(sampleLen B)] × N
174
+ //
175
+ // Backwards compatibility:
176
+ // Cog v1.9.5 is already in production and sends the older RaftCore devbin layout:
177
+ // no 3-byte envelope, no deviceSeqNum byte, and raw fixed-size samples
178
+ // [timestamp(2B)][payload] × N. Keep that path separate so current Axiom/Cog
179
+ // frames continue to use the length-prefixed parser above.
180
+ //
181
+ // Example message (two device records; first record has two samples):
182
+ // 0080 DB 01 07 0018 81 0000076a 000b 2a 07feff0000010008 07185707931400 01 000e 80 00000000 001f 05 05030001af01
183
+ // ^^^^ ^^^^
184
+ // | ^^ ^^ ^^ Record 2 ...
185
+ // | | | envelopeSeqNum = 0x07 (same layout as Record 1)
186
+ // | | topicIndex = 0x01
187
+ // | magic+version = 0xDB (devbin v1)
188
+ // msgType prefix (transport layer)
189
+ //
190
+ // Record 1 breakdown:
191
+ // 0018 recordLen = 24 body bytes follow
192
+ // 81 statusBus: online=1, pendDel=0, bus=1
193
+ // 0000076a address = 0x0000076A (slot 7, I2C addr 0x6A)
194
+ // 000b devTypeIdx = 11
195
+ // 2a deviceSeqNum = 42
196
+ // 07 feff0000010008 sample 1: sampleLen=7, 7 bytes of attribute data
197
+ // 07 18570793140001 sample 2: sampleLen=7, 7 bytes of attribute data
198
+ //
199
+ // Record 2 breakdown:
200
+ // 000e recordLen = 14 body bytes follow
201
+ // 80 statusBus: online=1, pendDel=0, bus=0
202
+ // 00000000 address = 0x00000000 (direct-connect)
203
+ // 001f devTypeIdx = 31
204
+ // 05 deviceSeqNum = 5
205
+ // 05 030001af01 sample 1: sampleLen=5, 5 bytes of attribute data
150
206
  // Debug
151
207
  // const debugMsgTime = Date.now();
152
208
  const debugMsgIndex = this._debugMsgIndex++;
153
- // Message layout
154
- const msgTypeLen = 2; // Length of the message type (first two bytes)
155
- const sectionLengthLen = 2; // Length of the inclusive section length (first two bytes of each section)
156
- const sectionConnectionModeLen = 1; // Length of the connection mode (next byte after section length)
157
- const sectionDeviceAddrLen = 4; // Length of the device address (next 4 bytes after connection mode)
158
- const sectionDeviceTypeIdxLen = 2; // Length of the device type index (next 2 bytes after device address)
159
- const sectionHeaderLen = sectionConnectionModeLen + sectionDeviceAddrLen + sectionDeviceTypeIdxLen;
209
+ // Message layout constants
210
+ const msgTypeLen = 2; // Transport-layer message type prefix (first two bytes, e.g. 0x0080)
211
+ const devbinEnvelopeLen = 3; // Devbin envelope: magic+version (1B) + topicIndex (1B) + envelopeSeqNum (1B)
212
+ const legacyDevbinEnvelopeLen = 2; // Intermediate/legacy envelope: magic+version (1B) + topicIndex (1B)
213
+ const devbinMagicMin = 0xDB;
214
+ const devbinMagicMax = 0xDF;
215
+ const recordLenLen = 2; // Per-record length prefix (uint16 big-endian)
216
+ const busInfoLen = 1; // statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
217
+ const deviceAddrLen = 4; // Device address (uint32 big-endian)
218
+ const devTypeIdxLen = 2; // Device type index (uint16 big-endian)
219
+ const deviceSeqNumLen = 1; // Per-device sequence counter
220
+ const currentRecordHeaderLen = busInfoLen + deviceAddrLen + devTypeIdxLen + deviceSeqNumLen; // = 8, minimum record body
221
+ const legacyRecordHeaderLen = busInfoLen + deviceAddrLen + devTypeIdxLen; // = 7, Cog v1.9.5 record body header
160
222
  // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} rxMsg.length ${rxMsg.length} rxMsg ${RaftUtils.bufferToHex(rxMsg)}`);
161
223
  // Start after the message type
162
224
  let msgPos = msgTypeLen;
163
- // Iterate through sections
225
+ let payloadFormat = "legacyRaw";
226
+ // Check for devbin envelope (magic+version + topicIndex)
227
+ if (rxMsg.length >= msgTypeLen + legacyDevbinEnvelopeLen) {
228
+ const envelopeMagicVer = rxMsg[msgTypeLen];
229
+ if ((envelopeMagicVer & 0xF0) === 0xD0) {
230
+ if ((envelopeMagicVer < devbinMagicMin) || (envelopeMagicVer > devbinMagicMax)) {
231
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid devbin envelope magic/version ${envelopeMagicVer}`);
232
+ return;
233
+ }
234
+ const topicIndex = rxMsg[msgTypeLen + 1];
235
+ if (topicIndex !== 0xFF) {
236
+ const topicName = (_a = this._systemUtils) === null || _a === void 0 ? void 0 : _a.getPublishTopicName(topicIndex);
237
+ if (topicName && topicName !== "devbin") {
238
+ return;
239
+ }
240
+ }
241
+ const currentMsgPos = msgTypeLen + devbinEnvelopeLen;
242
+ const legacyMsgPos = msgTypeLen + legacyDevbinEnvelopeLen;
243
+ if (this.hasValidRecordAt(rxMsg, currentMsgPos, recordLenLen, currentRecordHeaderLen)) {
244
+ msgPos = currentMsgPos;
245
+ payloadFormat = "lengthPrefixed";
246
+ }
247
+ else if (this.hasValidRecordAt(rxMsg, legacyMsgPos, recordLenLen, legacyRecordHeaderLen)) {
248
+ msgPos = legacyMsgPos;
249
+ payloadFormat = "legacyRaw";
250
+ }
251
+ else {
252
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid devbin envelope payload`);
253
+ return;
254
+ }
255
+ }
256
+ }
257
+ else if (this.hasValidRecordAt(rxMsg, msgPos, recordLenLen, currentRecordHeaderLen)) {
258
+ payloadFormat = "lengthPrefixed";
259
+ }
260
+ // Iterate through device records
164
261
  while (msgPos < rxMsg.length) {
165
- // Check length
262
+ // Check minimum length for record length prefix + record header
166
263
  const remainingLen = rxMsg.length - msgPos;
167
- if (remainingLen < sectionLengthLen + sectionHeaderLen) {
168
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid length ${rxMsg.length} < ${sectionLengthLen + sectionHeaderLen + msgPos}`);
264
+ if (remainingLen < recordLenLen + legacyRecordHeaderLen) {
265
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid length ${rxMsg.length} < ${recordLenLen + legacyRecordHeaderLen + msgPos}`);
266
+ return;
267
+ }
268
+ // Get the record body length (bytes that follow the 2-byte length prefix)
269
+ const recordLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
270
+ if ((recordLen < legacyRecordHeaderLen) || (recordLen > remainingLen - recordLenLen)) {
271
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} recordLen ${recordLen} remainingAfterLenBytes ${remainingLen - recordLenLen}`);
169
272
  return;
170
273
  }
171
- // Get the length of the section
172
- const sectionLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
173
- if (sectionLen > remainingLen + sectionLengthLen) {
174
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} msgLen ${sectionLen} remainingLenAfterLenBytes ${remainingLen - sectionLengthLen}`);
274
+ // Extract record header fields
275
+ let recordPos = msgPos + recordLenLen;
276
+ // statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
277
+ const statusByte = rxMsg[recordPos];
278
+ const busNum = statusByte & 0x0f;
279
+ const isOnline = (statusByte & 0x80) !== 0;
280
+ recordPos += busInfoLen;
281
+ // Device address (uint32 big-endian)
282
+ const devAddr = (rxMsg[recordPos] << 24) + (rxMsg[recordPos + 1] << 16) + (rxMsg[recordPos + 2] << 8) + rxMsg[recordPos + 3];
283
+ recordPos += deviceAddrLen;
284
+ // Device type index (uint16 big-endian)
285
+ const devTypeIdx = (rxMsg[recordPos] << 8) + rxMsg[recordPos + 1];
286
+ recordPos += devTypeIdxLen;
287
+ const commonRecordHeaderEndPos = recordPos;
288
+ const samplesEndPos = msgPos + recordLenLen + recordLen;
289
+ let recordPayloadFormat = payloadFormat;
290
+ let recordHeaderLen = recordPayloadFormat === "lengthPrefixed" ? currentRecordHeaderLen : legacyRecordHeaderLen;
291
+ const resolvedDeviceTypeInfo = await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
292
+ if (resolvedDeviceTypeInfo === null || resolvedDeviceTypeInfo === void 0 ? void 0 : resolvedDeviceTypeInfo.resp) {
293
+ recordPayloadFormat = this.resolveRecordPayloadFormat(rxMsg, commonRecordHeaderEndPos, samplesEndPos, resolvedDeviceTypeInfo.resp, recordPayloadFormat, deviceSeqNumLen);
294
+ recordHeaderLen = recordPayloadFormat === "lengthPrefixed" ? currentRecordHeaderLen : legacyRecordHeaderLen;
295
+ }
296
+ if (recordLen < recordHeaderLen) {
297
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} recordLen ${recordLen} recordHeaderLen ${recordHeaderLen}`);
175
298
  return;
176
299
  }
177
- // Extract message elements
178
- let sectionPos = msgPos + sectionLengthLen;
179
- const busNum = rxMsg[sectionPos] & 0x7f;
180
- const isOnline = (rxMsg[sectionPos] & 0x80) !== 0;
181
- sectionPos += sectionConnectionModeLen;
182
- // Get the device address and type index
183
- const devAddr = (rxMsg[sectionPos] << 24) + (rxMsg[sectionPos + 1] << 16) + (rxMsg[sectionPos + 2] << 8) + rxMsg[sectionPos + 3];
184
- sectionPos += sectionDeviceAddrLen;
185
- const devTypeIdx = (rxMsg[sectionPos] << 8) + rxMsg[sectionPos + 1];
186
- let attrGroupPos = sectionPos + sectionDeviceTypeIdxLen;
300
+ const isPendingDeletion = (recordPayloadFormat === "lengthPrefixed") && ((statusByte & 0x40) !== 0);
301
+ recordPos = commonRecordHeaderEndPos;
302
+ if (recordPayloadFormat === "lengthPrefixed") {
303
+ // Per-device sequence counter (reserved for future drop detection)
304
+ // const deviceSeqNum = rxMsg[recordPos];
305
+ recordPos += deviceSeqNumLen;
306
+ }
307
+ let pollDataPos = recordPos;
187
308
  // Debug
188
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length} sectionPos ${msgPos} sectionLen ${sectionLen} ${attrGroupPos} ${RaftUtils.bufferToHex(rxMsg.slice(msgPos, msgPos + sectionLen))}`);
189
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} connMode ${busNum} isOnline ${isOnline} devAddr ${devAddr} devTypeIdx ${devTypeIdx} attrGroupDataLen ${sectionLen - sectionHeaderLen}`);
190
- // Device key
191
- const deviceKey = (0, RaftDeviceStates_1.getDeviceKey)(busNum.toString(), devAddr.toString(), devTypeIdx.toString());
309
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length} recordStart ${msgPos} recordLen ${recordLen} ${pollDataPos} ${RaftUtils.bufferToHex(rxMsg.slice(msgPos, msgPos + recordLenLen + recordLen))}`);
310
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} bus ${busNum} isOnline ${isOnline} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} pollDataLen ${recordLen - recordHeaderLen}`);
311
+ // Format device address as canonical hex and build device key
312
+ const devAddrHex = (0, RaftDeviceStates_1.formatDeviceAddrHex)(devAddr);
313
+ const deviceKey = this.getBinaryDeviceKey(busNum, devAddrHex, devTypeIdx, recordPayloadFormat);
192
314
  // Update the last update time
193
315
  this._deviceLastUpdateTime[deviceKey] = Date.now();
316
+ // Handle pending deletion - remove device and skip further processing
317
+ if (isPendingDeletion) {
318
+ this.removeDevice(deviceKey);
319
+ msgPos += recordLenLen + recordLen;
320
+ continue;
321
+ }
194
322
  // Check if a device state already exists
195
323
  if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
196
324
  // Get the device type info
197
- const deviceTypeInfo = await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
325
+ const deviceTypeInfo = resolvedDeviceTypeInfo !== null && resolvedDeviceTypeInfo !== void 0 ? resolvedDeviceTypeInfo : await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
198
326
  // Debug
199
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} busNum ${busNum} devAddr ${devAddr} devTypeIdx ${devTypeIdx} deviceTypeInfo ${JSON.stringify(deviceTypeInfo)}`);
327
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} busNum ${busNum} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} deviceTypeInfo ${JSON.stringify(deviceTypeInfo)}`);
200
328
  // Handle case where device type info is not available
201
329
  if (deviceTypeInfo === undefined) {
202
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this section`);
203
- // Skip to next section without processing attributes
204
- msgPos += sectionLengthLen + sectionLen;
330
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this record`);
331
+ // Skip to next record without processing attributes
332
+ msgPos += recordLenLen + recordLen;
205
333
  continue;
206
334
  }
207
335
  // Check if device record exists
@@ -210,7 +338,7 @@ class DeviceManager {
210
338
  this._devicesState[deviceKey].deviceTypeInfo = deviceTypeInfo;
211
339
  this._devicesState[deviceKey].deviceType = deviceTypeInfo.name || "";
212
340
  this._devicesState[deviceKey].busName = busNum.toString();
213
- this._devicesState[deviceKey].deviceAddress = devAddr.toString();
341
+ this._devicesState[deviceKey].deviceAddress = devAddrHex;
214
342
  }
215
343
  }
216
344
  else {
@@ -220,13 +348,19 @@ class DeviceManager {
220
348
  deviceTimeline: {
221
349
  timestampsUs: [],
222
350
  lastReportTimestampUs: 0,
223
- reportTimestampOffsetUs: 0
351
+ reportTimestampOffsetUs: 0,
352
+ totalSamplesAdded: 0,
353
+ emaLastSampleTimeUs: 0,
354
+ emaIntervalUs: 0,
355
+ emaPrevPollTimeUs: 0,
356
+ emaCalibrated: false,
357
+ emaCalibrationPolls: 0
224
358
  },
225
359
  deviceAttributes: {},
226
360
  deviceIsNew: true,
227
361
  stateChanged: false,
228
- isOnline: true,
229
- deviceAddress: devAddr.toString(),
362
+ onlineState: RaftDeviceStates_1.DeviceOnlineState.Online,
363
+ deviceAddress: devAddrHex,
230
364
  deviceType: (deviceTypeInfo === null || deviceTypeInfo === void 0 ? void 0 : deviceTypeInfo.name) || "",
231
365
  busName: busNum.toString()
232
366
  };
@@ -234,48 +368,79 @@ class DeviceManager {
234
368
  }
235
369
  // Get device state
236
370
  const deviceState = this._devicesState[deviceKey];
237
- deviceState.isOnline = isOnline;
371
+ deviceState.onlineState = isOnline ? RaftDeviceStates_1.DeviceOnlineState.Online : RaftDeviceStates_1.DeviceOnlineState.Offline;
238
372
  // Check if device type info is available and complete
239
373
  if (deviceState.deviceTypeInfo && deviceState.deviceTypeInfo.resp) {
240
374
  // Iterate over attributes in the group
241
375
  const pollRespMetadata = deviceState.deviceTypeInfo.resp;
242
- // Iterate over attribute groups
243
- const attrGroupDataLen = sectionLen - sectionHeaderLen;
244
- const attrGroupStartPos = attrGroupPos;
376
+ // Process samples within this record
245
377
  const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
246
378
  const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
247
- while (attrGroupPos < attrGroupStartPos + attrGroupDataLen) {
248
- // Add bounds checking
249
- if (attrGroupPos >= rxMsg.length) {
250
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} exceeds message length ${rxMsg.length}`);
251
- break;
379
+ const totalSamplesBefore = deviceState.deviceTimeline.totalSamplesAdded;
380
+ if (recordPayloadFormat === "lengthPrefixed") {
381
+ while (pollDataPos < samplesEndPos) {
382
+ // Read sample length prefix
383
+ if (pollDataPos >= rxMsg.length) {
384
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} exceeds message length ${rxMsg.length}`);
385
+ break;
386
+ }
387
+ const sampleLen = rxMsg[pollDataPos];
388
+ pollDataPos += 1;
389
+ if (sampleLen === 0 || pollDataPos + sampleLen > samplesEndPos) {
390
+ break;
391
+ }
392
+ const sampleStartPos = pollDataPos;
393
+ const sampleEndPos = pollDataPos + sampleLen;
394
+ const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, sampleStartPos, deviceState.deviceTimeline, pollRespMetadata, deviceState.deviceAttributes, this._maxDatapointsToStore, sampleEndPos);
395
+ if (newMsgBufIdx < 0) {
396
+ this.warnMalformedSample(`${deviceKey}:${devTypeIdx}:lengthPrefixed`, `DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} skipped malformed sample ` +
397
+ `device=${deviceKey} devTypeIdx=${devTypeIdx} sampleLen=${sampleLen} respBytes=${pollRespMetadata.b}`);
398
+ pollDataPos += sampleLen;
399
+ continue;
400
+ }
401
+ // Advance by sampleLen regardless of how much processMsgAttrGroup consumed
402
+ pollDataPos += sampleLen;
403
+ deviceState.stateChanged = true;
252
404
  }
253
- const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, attrGroupPos, deviceState.deviceTimeline, pollRespMetadata, deviceState.deviceAttributes, this._maxDatapointsToStore);
254
- // 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)}`);
255
- if (newMsgBufIdx < 0) {
256
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup failed newMsgBufIdx ${newMsgBufIdx}`);
257
- break;
405
+ }
406
+ else {
407
+ const legacySampleLen = this.getLegacyRawSampleLen(pollRespMetadata);
408
+ if (legacySampleLen <= 0) {
409
+ this.warnMalformedSample(`${deviceKey}:${devTypeIdx}:legacyRawLen`, `DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid legacy sample length ` +
410
+ `device=${deviceKey} devTypeIdx=${devTypeIdx} respBytes=${pollRespMetadata.b}`);
258
411
  }
259
- // Prevent infinite loops
260
- if (newMsgBufIdx <= attrGroupPos) {
261
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup didn't advance position from ${attrGroupPos} to ${newMsgBufIdx}`);
262
- break;
412
+ else {
413
+ while (pollDataPos + legacySampleLen <= samplesEndPos) {
414
+ const sampleStartPos = pollDataPos;
415
+ const sampleEndPos = pollDataPos + legacySampleLen;
416
+ const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, sampleStartPos, deviceState.deviceTimeline, pollRespMetadata, deviceState.deviceAttributes, this._maxDatapointsToStore, sampleEndPos);
417
+ if (newMsgBufIdx < 0) {
418
+ this.warnMalformedSample(`${deviceKey}:${devTypeIdx}:legacyRaw`, `DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} skipped malformed legacy sample ` +
419
+ `device=${deviceKey} devTypeIdx=${devTypeIdx} sampleLen=${legacySampleLen} respBytes=${pollRespMetadata.b}`);
420
+ pollDataPos += legacySampleLen;
421
+ continue;
422
+ }
423
+ pollDataPos += legacySampleLen;
424
+ deviceState.stateChanged = true;
425
+ }
426
+ if (pollDataPos < samplesEndPos) {
427
+ this.warnMalformedSample(`${deviceKey}:${devTypeIdx}:legacyRawRemainder`, `DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} skipped trailing legacy sample bytes ` +
428
+ `device=${deviceKey} devTypeIdx=${devTypeIdx} remaining=${samplesEndPos - pollDataPos} sampleLen=${legacySampleLen} respBytes=${pollRespMetadata.b}`);
429
+ }
263
430
  }
264
- attrGroupPos = newMsgBufIdx;
265
- deviceState.stateChanged = true;
266
- // 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}`);
267
- // console.log(`DevMan.handleClientMsgBinary group done debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} sectionLen ${sectionLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
268
431
  }
269
432
  // Inform decoded-data callbacks
270
- this.emitDecodedData(deviceKey, busNum.toString(), devAddr.toString(), deviceState, pollRespMetadata, attrLengthsBefore, timelineLenBefore);
433
+ this.emitDecodedData(deviceKey, busNum.toString(), devAddrHex, deviceState, pollRespMetadata, attrLengthsBefore, timelineLenBefore);
434
+ const newSamples = deviceState.deviceTimeline.totalSamplesAdded - totalSamplesBefore;
435
+ this.updateDeviceStats(deviceKey, newSamples, Date.now());
271
436
  }
272
437
  else {
273
438
  console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceState incomplete for device ${deviceKey}, skipping attribute processing`);
274
439
  }
275
440
  // Debug
276
- // console.log(`DevMan.handleClientMsgBinary section done debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} sectionLen ${sectionLen} msgPos ${msgPos} newMsgPos ${msgPos + sectionLengthLen + sectionLen} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
277
- // Move to next message
278
- msgPos += sectionLengthLen + sectionLen;
441
+ // console.log(`DevMan.handleClientMsgBinary record done debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} newMsgPos ${msgPos + recordLenLen + recordLen} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
442
+ // Advance past this record (recordLenLen + recordLen bytes)
443
+ msgPos += recordLenLen + recordLen;
279
444
  }
280
445
  // Check for devices that have not been updated for a while
281
446
  if (this._removeDevicesFlag) {
@@ -283,6 +448,7 @@ class DeviceManager {
283
448
  Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
284
449
  if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
285
450
  delete this._devicesState[deviceKey];
451
+ delete this._deviceStats[deviceKey];
286
452
  }
287
453
  });
288
454
  }
@@ -297,6 +463,10 @@ class DeviceManager {
297
463
  // console.log(`DeviceManager client msg ${JSON.stringify(data)}`);
298
464
  // Iterate over the buses
299
465
  Object.entries(data).forEach(([busName, devices]) => {
466
+ // Check the bus name doesn't start with _ which is reserved for non-device information such as topic name
467
+ if (busName.startsWith("_")) {
468
+ return;
469
+ }
300
470
  // Check for bus status info
301
471
  if (devices && typeof devices === "object" && "_s" in devices) {
302
472
  // console.log(`DeviceManager bus status ${JSON.stringify(devices._s)}`);
@@ -304,21 +474,27 @@ class DeviceManager {
304
474
  }
305
475
  // Iterate over the devices
306
476
  Object.entries(devices).forEach(async ([devAddr, attrGroups]) => {
477
+ var _a;
307
478
  // Check for non-device info (starts with _)
308
479
  if (devAddr.startsWith("_")) {
309
480
  return;
310
481
  }
311
482
  // Device type name
312
483
  let deviceTypeName = "";
484
+ let deviceTypeIdx = -1;
313
485
  if (attrGroups && typeof attrGroups === 'object' && "_t" in attrGroups && typeof attrGroups._t === "string") {
314
486
  deviceTypeName = attrGroups._t || "";
315
487
  }
488
+ else if (attrGroups && typeof attrGroups === 'object' && "_i" in attrGroups && typeof attrGroups._i === "number") {
489
+ deviceTypeIdx = (_a = attrGroups._i) !== null && _a !== void 0 ? _a : -1;
490
+ deviceTypeName = deviceTypeIdx.toString();
491
+ }
316
492
  else {
317
493
  console.warn(`DeviceManager missing device type attrGroups ${JSON.stringify(attrGroups)}`);
318
494
  return;
319
495
  }
320
496
  // Device key
321
- const deviceKey = (0, RaftDeviceStates_1.getDeviceKey)(busName, devAddr, deviceTypeName);
497
+ const deviceKey = (0, RaftDeviceStates_1.getDeviceKey)(busName, devAddr);
322
498
  // Update the last update time
323
499
  this._deviceLastUpdateTime[deviceKey] = Date.now();
324
500
  // Check if a device state already exists
@@ -341,12 +517,18 @@ class DeviceManager {
341
517
  deviceTimeline: {
342
518
  timestampsUs: [],
343
519
  lastReportTimestampUs: 0,
344
- reportTimestampOffsetUs: 0
520
+ reportTimestampOffsetUs: 0,
521
+ totalSamplesAdded: 0,
522
+ emaLastSampleTimeUs: 0,
523
+ emaIntervalUs: 0,
524
+ emaPrevPollTimeUs: 0,
525
+ emaCalibrated: false,
526
+ emaCalibrationPolls: 0
345
527
  },
346
528
  deviceAttributes: {},
347
529
  deviceIsNew: true,
348
530
  stateChanged: false,
349
- isOnline: true,
531
+ onlineState: RaftDeviceStates_1.DeviceOnlineState.Online,
350
532
  deviceAddress: devAddr,
351
533
  deviceType: deviceTypeName,
352
534
  busName: busName
@@ -355,9 +537,15 @@ class DeviceManager {
355
537
  }
356
538
  // Get device state
357
539
  const deviceState = this._devicesState[deviceKey];
358
- // Check for online/offline state information
540
+ // Check for online/offline/pending-deletion state information
359
541
  if (attrGroups && typeof attrGroups === "object" && "_o" in attrGroups) {
360
- deviceState.isOnline = ((attrGroups._o === true) || (attrGroups._o === "1") || (attrGroups._o === 1));
542
+ const onlineStateVal = typeof attrGroups._o === 'number' ? attrGroups._o : parseInt(String(attrGroups._o), 10);
543
+ if (onlineStateVal === 2) {
544
+ // Pending deletion - remove device and skip further processing
545
+ this.removeDevice(deviceKey);
546
+ return;
547
+ }
548
+ deviceState.onlineState = onlineStateVal === 1 ? RaftDeviceStates_1.DeviceOnlineState.Online : RaftDeviceStates_1.DeviceOnlineState.Offline;
361
549
  }
362
550
  // Check if device type info is available
363
551
  if (!deviceState.deviceTypeInfo) {
@@ -382,6 +570,7 @@ class DeviceManager {
382
570
  const pollRespMetadata = deviceState.deviceTypeInfo.resp;
383
571
  const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
384
572
  const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
573
+ const totalSamplesBefore = deviceState.deviceTimeline.totalSamplesAdded;
385
574
  // Loop
386
575
  while (msgBufIdx < msgBytes.length) {
387
576
  const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(msgBytes, msgBufIdx, deviceState.deviceTimeline, pollRespMetadata, deviceState.deviceAttributes, this._maxDatapointsToStore);
@@ -391,6 +580,8 @@ class DeviceManager {
391
580
  deviceState.stateChanged = true;
392
581
  }
393
582
  this.emitDecodedData(deviceKey, busName, devAddr, deviceState, pollRespMetadata, attrLengthsBefore, timelineLenBefore, attrGroupName, markers);
583
+ const newSamples = deviceState.deviceTimeline.totalSamplesAdded - totalSamplesBefore;
584
+ this.updateDeviceStats(deviceKey, newSamples, Date.now());
394
585
  });
395
586
  });
396
587
  });
@@ -400,6 +591,7 @@ class DeviceManager {
400
591
  Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
401
592
  if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
402
593
  delete this._devicesState[deviceKey];
594
+ delete this._deviceStats[deviceKey];
403
595
  }
404
596
  });
405
597
  }
@@ -431,6 +623,20 @@ class DeviceManager {
431
623
  });
432
624
  }
433
625
  ////////////////////////////////////////////////////////////////////////////
626
+ // Remove a device (e.g. on pending deletion)
627
+ ////////////////////////////////////////////////////////////////////////////
628
+ removeDevice(deviceKey) {
629
+ // Snapshot the state before removal for callbacks
630
+ const deviceState = this._devicesState[deviceKey];
631
+ if (deviceState) {
632
+ deviceState.onlineState = RaftDeviceStates_1.DeviceOnlineState.PendingDeletion;
633
+ this._deviceRemovedCallbacks.forEach((cb) => cb(deviceKey, deviceState));
634
+ }
635
+ delete this._devicesState[deviceKey];
636
+ delete this._deviceLastUpdateTime[deviceKey];
637
+ delete this._deviceStats[deviceKey];
638
+ }
639
+ ////////////////////////////////////////////////////////////////////////////
434
640
  // Get device type info
435
641
  ////////////////////////////////////////////////////////////////////////////
436
642
  async getDeviceTypeInfo(busName, deviceType) {
@@ -499,6 +705,117 @@ class DeviceManager {
499
705
  return undefined;
500
706
  }
501
707
  }
708
+ hasValidRecordAt(rxMsg, msgPos, recordLenLen, recordHeaderLen) {
709
+ if (msgPos === rxMsg.length) {
710
+ return true;
711
+ }
712
+ if (msgPos < 0 || msgPos > rxMsg.length) {
713
+ return false;
714
+ }
715
+ const remainingLen = rxMsg.length - msgPos;
716
+ if (remainingLen < recordLenLen + recordHeaderLen) {
717
+ return false;
718
+ }
719
+ const recordLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
720
+ return (recordLen >= recordHeaderLen) && (recordLen <= remainingLen - recordLenLen);
721
+ }
722
+ resolveRecordPayloadFormat(rxMsg, commonRecordHeaderEndPos, samplesEndPos, pollRespMetadata, preferredFormat, deviceSeqNumLen) {
723
+ const lengthPrefixedStartPos = commonRecordHeaderEndPos + deviceSeqNumLen;
724
+ const lengthPrefixedValid = this.areLengthPrefixedSamplesValid(rxMsg, lengthPrefixedStartPos, samplesEndPos, pollRespMetadata);
725
+ const legacySampleLen = this.getLegacyRawSampleLen(pollRespMetadata);
726
+ const legacyRawValid = this.areLegacyRawSamplesValid(commonRecordHeaderEndPos, samplesEndPos, legacySampleLen);
727
+ if (legacyRawValid && !lengthPrefixedValid) {
728
+ return "legacyRaw";
729
+ }
730
+ if (lengthPrefixedValid && !legacyRawValid) {
731
+ return "lengthPrefixed";
732
+ }
733
+ if (lengthPrefixedValid && legacyRawValid) {
734
+ return preferredFormat;
735
+ }
736
+ return preferredFormat;
737
+ }
738
+ areLengthPrefixedSamplesValid(rxMsg, pollDataPos, samplesEndPos, pollRespMetadata) {
739
+ if ((pollDataPos < 0) || (pollDataPos > samplesEndPos) || (samplesEndPos > rxMsg.length)) {
740
+ return false;
741
+ }
742
+ if (pollDataPos === samplesEndPos) {
743
+ return true;
744
+ }
745
+ const fixedSampleLen = pollRespMetadata.c ? 0 : this.getLegacyRawSampleLen(pollRespMetadata);
746
+ let sampleCount = 0;
747
+ while (pollDataPos < samplesEndPos) {
748
+ const sampleLen = rxMsg[pollDataPos];
749
+ pollDataPos += 1;
750
+ if ((sampleLen === 0) || (pollDataPos + sampleLen > samplesEndPos)) {
751
+ return false;
752
+ }
753
+ if ((fixedSampleLen > 0) && (sampleLen !== fixedSampleLen)) {
754
+ return false;
755
+ }
756
+ pollDataPos += sampleLen;
757
+ sampleCount++;
758
+ }
759
+ return sampleCount > 0;
760
+ }
761
+ areLegacyRawSamplesValid(pollDataPos, samplesEndPos, legacySampleLen) {
762
+ if ((legacySampleLen <= 0) || (pollDataPos < 0) || (pollDataPos > samplesEndPos)) {
763
+ return false;
764
+ }
765
+ const payloadLen = samplesEndPos - pollDataPos;
766
+ return (payloadLen > 0) && (payloadLen % legacySampleLen === 0);
767
+ }
768
+ getBinaryDeviceKey(busNum, devAddrHex, devTypeIdx, payloadFormat) {
769
+ const baseDeviceKey = (0, RaftDeviceStates_1.getDeviceKey)(busNum.toString(), devAddrHex);
770
+ if ((payloadFormat === "legacyRaw") && (busNum === 0) && (devAddrHex === "0")) {
771
+ return `${baseDeviceKey}_${devTypeIdx}`;
772
+ }
773
+ return baseDeviceKey;
774
+ }
775
+ getLegacyRawSampleLen(pollRespMetadata) {
776
+ const legacyTimestampLen = 2;
777
+ return legacyTimestampLen + this.getPollRespPayloadSize(pollRespMetadata);
778
+ }
779
+ getPollRespPayloadSize(pollRespMetadata) {
780
+ if (pollRespMetadata.c) {
781
+ return pollRespMetadata.b;
782
+ }
783
+ let attrPayloadLen = 0;
784
+ for (const attrDef of pollRespMetadata.a) {
785
+ if (!attrDef.t) {
786
+ return pollRespMetadata.b;
787
+ }
788
+ try {
789
+ attrPayloadLen += (0, RaftStruct_1.structSizeOf)(attrDef.t);
790
+ }
791
+ catch (_a) {
792
+ return pollRespMetadata.b;
793
+ }
794
+ }
795
+ // Cog v1.9.5 light metadata reports the direct-sensor payload size doubled,
796
+ // but the legacy raw record contains one fixed payload matching the attribute schema.
797
+ if ((attrPayloadLen > 0) && (pollRespMetadata.b > 0) && (attrPayloadLen <= pollRespMetadata.b)) {
798
+ return attrPayloadLen;
799
+ }
800
+ return pollRespMetadata.b;
801
+ }
802
+ parseDeviceKeyForCommand(deviceKey) {
803
+ const deviceState = this._devicesState[deviceKey];
804
+ if (deviceState) {
805
+ return { bus: deviceState.busName, addr: deviceState.deviceAddress };
806
+ }
807
+ return (0, RaftDeviceStates_1.parseDeviceKey)(deviceKey);
808
+ }
809
+ warnMalformedSample(warningKey, message) {
810
+ var _a;
811
+ const nowMs = Date.now();
812
+ const lastWarnMs = (_a = this._malformedSampleWarnLastMs[warningKey]) !== null && _a !== void 0 ? _a : 0;
813
+ if (nowMs - lastWarnMs < 5000) {
814
+ return;
815
+ }
816
+ this._malformedSampleWarnLastMs[warningKey] = nowMs;
817
+ console.warn(message);
818
+ }
502
819
  ////////////////////////////////////////////////////////////////////////////
503
820
  // Send action to device
504
821
  ////////////////////////////////////////////////////////////////////////////
@@ -508,40 +825,77 @@ class DeviceManager {
508
825
  .join("");
509
826
  }
510
827
  async sendAction(deviceKey, action, data) {
511
- // console.log(`DeviceManager sendAction ${deviceKey} action name ${action.n} value ${value} prefix ${action.w}`);
512
- var _a;
513
- let writeBytes;
514
- // Check for one data item
515
- if (data.length === 1) {
516
- let value = data[0];
517
- // Check for conversion
518
- if (action.sub !== undefined) {
519
- value = value - action.sub;
828
+ var _a, _b;
829
+ console.log(`DeviceManager sendAction ${deviceKey} action ${action.n} data ${data} map ${JSON.stringify(action.map)} keys ${action.map ? Object.keys(action.map) : 'none'}`);
830
+ // For _conf.* actions, delegate to setSampleRate() which coordinates polling params
831
+ if (action.n.startsWith('_conf.') && action.map && data.length === 1) {
832
+ const result = await this.setSampleRate(deviceKey, data[0]);
833
+ return result.ok;
834
+ }
835
+ let writeHexStr;
836
+ // Check if action has a map - use mapped hex value directly
837
+ if (action.map && data.length === 1) {
838
+ const mapKey = String(data[0]);
839
+ const mapEntry = action.map[mapKey];
840
+ if (!mapEntry) {
841
+ console.warn(`DeviceManager sendAction: no map entry for value ${mapKey} in action ${action.n}`);
842
+ return false;
843
+ }
844
+ const mappedHex = (0, RaftDeviceInfo_1.getActionMapHex)(mapEntry);
845
+ // Map values may contain &-separated multi-writes (e.g. "1048&114C&0a26")
846
+ const writes = mappedHex.split('&');
847
+ const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
848
+ try {
849
+ const msgHandler = (_a = this._systemUtils) === null || _a === void 0 ? void 0 : _a.getMsgHandler();
850
+ if (!msgHandler)
851
+ return false;
852
+ for (const hexWr of writes) {
853
+ const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + hexWr;
854
+ console.log(`DeviceManager sendAction ${action.n} ${cmd}`);
855
+ const msgRslt = await msgHandler.sendRICRESTURL(cmd);
856
+ if (msgRslt.rslt !== "ok")
857
+ return false;
858
+ }
859
+ return true;
520
860
  }
521
- if (action.mul !== undefined) {
522
- value = value * action.mul;
861
+ catch (error) {
862
+ console.warn(`DeviceManager sendAction error ${error}`);
863
+ return false;
523
864
  }
524
- // Form the write bytes
525
- writeBytes = action.t ? (0, RaftStruct_1.structPack)(action.t, [value]) : new Uint8Array(0);
526
865
  }
527
866
  else {
528
- // Form the write bytes which may have multiple data items
529
- writeBytes = action.t ? (0, RaftStruct_1.structPack)(action.t, data) : new Uint8Array(0);
867
+ let writeBytes;
868
+ // Check for one data item
869
+ if (data.length === 1) {
870
+ let value = data[0];
871
+ // Check for conversion
872
+ if (action.sub !== undefined) {
873
+ value = value - action.sub;
874
+ }
875
+ if (action.mul !== undefined) {
876
+ value = value * action.mul;
877
+ }
878
+ // Form the write bytes
879
+ writeBytes = action.t ? (0, RaftStruct_1.structPack)(action.t, [value]) : new Uint8Array(0);
880
+ }
881
+ else {
882
+ // Form the write bytes which may have multiple data items
883
+ writeBytes = action.t ? (0, RaftStruct_1.structPack)(action.t, data) : new Uint8Array(0);
884
+ }
885
+ // Convert to hex string
886
+ writeHexStr = this.toHex(writeBytes);
530
887
  }
531
- // Convert to hex string
532
- let writeHexStr = this.toHex(writeBytes);
533
888
  // Add prefix and postfix
534
889
  writeHexStr = (action.w ? action.w : "") + writeHexStr + (action.wz ? action.wz : "");
535
- // Separate the bus and address in the deviceKey (_ char)
536
- const devBus = deviceKey.split("_")[0];
537
- const devAddr = deviceKey.split("_")[1];
890
+ // Parse the device key into bus and address components
891
+ const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
538
892
  // Send the action to the server
539
893
  const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + writeHexStr;
540
894
  console.log(`DeviceManager deviceKey ${deviceKey} action name ${action.n} value ${data} prefix ${action.w} sendAction ${cmd}`);
541
895
  // Send the command
542
896
  try {
543
897
  // Get the msg handler
544
- const msgHandler = (_a = this._systemUtils) === null || _a === void 0 ? void 0 : _a.getMsgHandler();
898
+ const msgHandler = (_b = this._systemUtils) === null || _b === void 0 ? void 0 : _b.getMsgHandler();
545
899
  if (msgHandler) {
546
900
  const msgRslt = await msgHandler.sendRICRESTURL(cmd);
547
901
  return msgRslt.rslt === "ok";
@@ -581,6 +935,111 @@ class DeviceManager {
581
935
  return false;
582
936
  }
583
937
  ////////////////////////////////////////////////////////////////////////////
938
+ // Set sample rate with coordinated polling parameters
939
+ // Finds the closest supported rate from the device's _conf.rate action,
940
+ // calculates optimal intervalUs and numSamples, and sends a single
941
+ // /devman/devconfig call to set all parameters atomically.
942
+ ////////////////////////////////////////////////////////////////////////////
943
+ async setSampleRate(deviceKey, sampleRateHz, options) {
944
+ var _a, _b, _c, _d, _e, _f, _g, _h;
945
+ // Look up device state and type info
946
+ const deviceState = this._devicesState[deviceKey];
947
+ if (!(deviceState === null || deviceState === void 0 ? void 0 : deviceState.deviceTypeInfo)) {
948
+ return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: 0, intervalUs: 0, numSamples: 0, error: 'Device not found or type info not loaded' };
949
+ }
950
+ const typeInfo = deviceState.deviceTypeInfo;
951
+ // Find the _conf.rate action
952
+ const confRateAction = (_a = typeInfo.actions) === null || _a === void 0 ? void 0 : _a.find(a => a.n === '_conf.rate');
953
+ if (!(confRateAction === null || confRateAction === void 0 ? void 0 : confRateAction.map)) {
954
+ // No _conf.rate action — use generic sample rate setting
955
+ // Non-FIFO devices: poll once per sample period, 1 sample per read
956
+ const samplePeriodUs = Math.round(1000000 / sampleRateHz);
957
+ const numSamples = (_b = options === null || options === void 0 ? void 0 : options.numSamples) !== null && _b !== void 0 ? _b : 1;
958
+ const intervalUs = (_c = options === null || options === void 0 ? void 0 : options.intervalUs) !== null && _c !== void 0 ? _c : Math.max(5000, samplePeriodUs);
959
+ const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
960
+ const cmd = `devman/devconfig?bus=${devBus}&addr=${devAddr}&intervalUs=${intervalUs}&numSamples=${numSamples}`;
961
+ try {
962
+ const msgHandler = (_d = this._systemUtils) === null || _d === void 0 ? void 0 : _d.getMsgHandler();
963
+ if (!msgHandler) {
964
+ return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: sampleRateHz, intervalUs, numSamples, error: 'No message handler available' };
965
+ }
966
+ const msgRslt = await msgHandler.sendRICRESTURL(cmd);
967
+ const ok = msgRslt.rslt === 'ok';
968
+ return { ok, requestedRateHz: sampleRateHz, actualRateHz: sampleRateHz, intervalUs, numSamples, error: ok ? undefined : `Firmware returned: ${msgRslt.rslt}` };
969
+ }
970
+ catch (error) {
971
+ return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: sampleRateHz, intervalUs, numSamples, error: `${error}` };
972
+ }
973
+ }
974
+ // Find the closest supported rate from the map keys
975
+ const supportedRates = Object.keys(confRateAction.map).map(Number).filter(r => !isNaN(r)).sort((a, b) => a - b);
976
+ if (supportedRates.length === 0) {
977
+ return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: 0, intervalUs: 0, numSamples: 0, error: 'No valid rates in _conf.rate map' };
978
+ }
979
+ let actualRate = supportedRates[0];
980
+ let minDist = Math.abs(sampleRateHz - actualRate);
981
+ for (const rate of supportedRates) {
982
+ const dist = Math.abs(sampleRateHz - rate);
983
+ if (dist < minDist) {
984
+ minDist = dist;
985
+ actualRate = rate;
986
+ }
987
+ }
988
+ // Look up map entry for the matched rate — may be object with recommended polling params
989
+ const mapEntry = confRateAction.map[String(actualRate)];
990
+ const mapObj = typeof mapEntry === 'object' ? mapEntry : null;
991
+ const recommendedIntervalUs = mapObj === null || mapObj === void 0 ? void 0 : mapObj.i;
992
+ const recommendedNumSamples = mapObj === null || mapObj === void 0 ? void 0 : mapObj.s;
993
+ // Calculate inter-sample period
994
+ const samplePeriodUs = Math.round(1000000 / actualRate);
995
+ // Calculate optimal numSamples and intervalUs
996
+ // Priority: explicit options > map entry recommendations > auto-calculation
997
+ const maxNumSamples = (_e = options === null || options === void 0 ? void 0 : options.maxNumSamples) !== null && _e !== void 0 ? _e : 20;
998
+ let numSamples;
999
+ let intervalUs;
1000
+ if ((options === null || options === void 0 ? void 0 : options.numSamples) !== undefined && (options === null || options === void 0 ? void 0 : options.intervalUs) !== undefined) {
1001
+ // Both explicitly specified — use as-is
1002
+ numSamples = options.numSamples;
1003
+ intervalUs = options.intervalUs;
1004
+ }
1005
+ else if ((options === null || options === void 0 ? void 0 : options.intervalUs) !== undefined) {
1006
+ // intervalUs specified, derive numSamples from it
1007
+ intervalUs = options.intervalUs;
1008
+ numSamples = (_g = (_f = options === null || options === void 0 ? void 0 : options.numSamples) !== null && _f !== void 0 ? _f : recommendedNumSamples) !== null && _g !== void 0 ? _g : Math.max(1, Math.min(maxNumSamples, Math.floor(intervalUs / samplePeriodUs)));
1009
+ }
1010
+ else if ((options === null || options === void 0 ? void 0 : options.numSamples) !== undefined) {
1011
+ // numSamples specified, derive intervalUs from it
1012
+ numSamples = options.numSamples;
1013
+ intervalUs = recommendedIntervalUs !== null && recommendedIntervalUs !== void 0 ? recommendedIntervalUs : Math.round(numSamples * samplePeriodUs * 0.8);
1014
+ }
1015
+ else if (recommendedIntervalUs !== undefined && recommendedNumSamples !== undefined) {
1016
+ // Use map entry recommendations
1017
+ intervalUs = recommendedIntervalUs;
1018
+ numSamples = recommendedNumSamples;
1019
+ }
1020
+ else {
1021
+ // Auto-calculate: target ~50ms poll interval, bounded by sample rate
1022
+ const targetPollIntervalUs = 50000;
1023
+ numSamples = recommendedNumSamples !== null && recommendedNumSamples !== void 0 ? recommendedNumSamples : Math.max(1, Math.min(maxNumSamples, Math.floor(targetPollIntervalUs / samplePeriodUs)));
1024
+ intervalUs = recommendedIntervalUs !== null && recommendedIntervalUs !== void 0 ? recommendedIntervalUs : Math.max(5000, Math.min(1000000, Math.round(numSamples * samplePeriodUs * 0.8)));
1025
+ }
1026
+ // Send single devconfig call with all parameters
1027
+ const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
1028
+ const cmd = `devman/devconfig?bus=${devBus}&addr=${devAddr}&sampleRateHz=${actualRate}&intervalUs=${intervalUs}&numSamples=${numSamples}`;
1029
+ try {
1030
+ const msgHandler = (_h = this._systemUtils) === null || _h === void 0 ? void 0 : _h.getMsgHandler();
1031
+ if (!msgHandler) {
1032
+ return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: actualRate, intervalUs, numSamples, error: 'No message handler available' };
1033
+ }
1034
+ const msgRslt = await msgHandler.sendRICRESTURL(cmd);
1035
+ const ok = msgRslt.rslt === 'ok';
1036
+ return { ok, requestedRateHz: sampleRateHz, actualRateHz: actualRate, intervalUs, numSamples, error: ok ? undefined : `Firmware returned: ${msgRslt.rslt}` };
1037
+ }
1038
+ catch (error) {
1039
+ return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: actualRate, intervalUs, numSamples, error: `${error}` };
1040
+ }
1041
+ }
1042
+ ////////////////////////////////////////////////////////////////////////////
584
1043
  // Convert hex to bytes
585
1044
  ////////////////////////////////////////////////////////////////////////////
586
1045
  hexToBytes(hex) {
@@ -591,6 +1050,57 @@ class DeviceManager {
591
1050
  return bytes;
592
1051
  }
593
1052
  ////////////////////////////////////////////////////////////////////////////
1053
+ // Helpers for device stats
1054
+ ////////////////////////////////////////////////////////////////////////////
1055
+ createEmptyStats() {
1056
+ return {
1057
+ totalSamples: 0,
1058
+ windowMs: this._statsWindowMs,
1059
+ windowSamples: 0,
1060
+ sampleRateHz: 0,
1061
+ lastSampleTimeMs: null,
1062
+ lastUpdateTimeMs: null,
1063
+ windowEvents: []
1064
+ };
1065
+ }
1066
+ getOrCreateDeviceStats(deviceKey) {
1067
+ if (!this._deviceStats[deviceKey]) {
1068
+ this._deviceStats[deviceKey] = this.createEmptyStats();
1069
+ }
1070
+ return this._deviceStats[deviceKey];
1071
+ }
1072
+ cloneDeviceStats(stats) {
1073
+ return {
1074
+ totalSamples: stats.totalSamples,
1075
+ windowMs: stats.windowMs,
1076
+ windowSamples: stats.windowSamples,
1077
+ sampleRateHz: stats.sampleRateHz,
1078
+ lastSampleTimeMs: stats.lastSampleTimeMs,
1079
+ lastUpdateTimeMs: stats.lastUpdateTimeMs
1080
+ };
1081
+ }
1082
+ updateDeviceStats(deviceKey, newSamples, nowMs) {
1083
+ const stats = this.getOrCreateDeviceStats(deviceKey);
1084
+ stats.lastUpdateTimeMs = nowMs;
1085
+ if (newSamples > 0) {
1086
+ stats.totalSamples += newSamples;
1087
+ stats.lastSampleTimeMs = nowMs;
1088
+ stats.windowEvents.push({ timeMs: nowMs, samples: newSamples });
1089
+ }
1090
+ const windowStartMs = nowMs - stats.windowMs;
1091
+ while (stats.windowEvents.length > 0 && stats.windowEvents[0].timeMs < windowStartMs) {
1092
+ stats.windowEvents.shift();
1093
+ }
1094
+ const windowSamples = stats.windowEvents.reduce((sum, entry) => sum + entry.samples, 0);
1095
+ stats.windowSamples = windowSamples;
1096
+ if (stats.windowEvents.length === 0) {
1097
+ stats.sampleRateHz = 0;
1098
+ return;
1099
+ }
1100
+ const actualWindowMs = Math.max(1, nowMs - stats.windowEvents[0].timeMs);
1101
+ stats.sampleRateHz = (windowSamples * 1000) / actualWindowMs;
1102
+ }
1103
+ ////////////////////////////////////////////////////////////////////////////
594
1104
  // Helpers for decoded data callbacks
595
1105
  ////////////////////////////////////////////////////////////////////////////
596
1106
  snapshotAttrLengths(deviceAttrs, pollRespMetadata) {
@@ -599,8 +1109,8 @@ class DeviceManager {
599
1109
  return lengths;
600
1110
  }
601
1111
  pollRespMetadata.a.forEach((attr) => {
602
- var _a;
603
- lengths[attr.n] = ((_a = deviceAttrs[attr.n]) === null || _a === void 0 ? void 0 : _a.values.length) || 0;
1112
+ var _a, _b;
1113
+ lengths[attr.n] = (_b = (_a = deviceAttrs[attr.n]) === null || _a === void 0 ? void 0 : _a.values.length) !== null && _b !== void 0 ? _b : 0;
604
1114
  });
605
1115
  return lengths;
606
1116
  }
@@ -611,11 +1121,12 @@ class DeviceManager {
611
1121
  const attrValues = {};
612
1122
  let hasValues = false;
613
1123
  pollRespMetadata.a.forEach((attr) => {
1124
+ var _a;
614
1125
  const attrState = deviceState.deviceAttributes[attr.n];
615
1126
  if (!attrState) {
616
1127
  return;
617
1128
  }
618
- const prevLen = attrLengthsBefore[attr.n] || 0;
1129
+ const prevLen = (_a = attrLengthsBefore[attr.n]) !== null && _a !== void 0 ? _a : 0;
619
1130
  if (attrState.values.length > prevLen) {
620
1131
  attrValues[attr.n] = attrState.values.slice(prevLen);
621
1132
  hasValues = hasValues || attrValues[attr.n].length > 0;