@robotical/raftjs 2.0.11 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) 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 +10 -6
  14. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  15. package/dist/react-native/RaftChannelWebSerial.js +1 -1
  16. package/dist/react-native/RaftChannelWebSerial.js.map +1 -1
  17. package/dist/react-native/RaftChannelWebSocket.js +16 -1
  18. package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
  19. package/dist/react-native/RaftConnector.d.ts +11 -1
  20. package/dist/react-native/RaftConnector.js +75 -9
  21. package/dist/react-native/RaftConnector.js.map +1 -1
  22. package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
  23. package/dist/react-native/RaftCustomAttrHandler.js +32 -44
  24. package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
  25. package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
  26. package/dist/react-native/RaftDeviceInfo.js +8 -0
  27. package/dist/react-native/RaftDeviceInfo.js.map +1 -1
  28. package/dist/react-native/RaftDeviceManager.d.ts +47 -2
  29. package/dist/react-native/RaftDeviceManager.js +696 -104
  30. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  31. package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
  32. package/dist/react-native/RaftDeviceStates.d.ts +27 -3
  33. package/dist/react-native/RaftDeviceStates.js +31 -6
  34. package/dist/react-native/RaftDeviceStates.js.map +1 -1
  35. package/dist/react-native/RaftFileHandler.d.ts +0 -1
  36. package/dist/react-native/RaftFileHandler.js +61 -23
  37. package/dist/react-native/RaftFileHandler.js.map +1 -1
  38. package/dist/react-native/RaftPublish.d.ts +2 -0
  39. package/dist/react-native/RaftPublish.js +81 -0
  40. package/dist/react-native/RaftPublish.js.map +1 -0
  41. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  42. package/dist/react-native/RaftStreamHandler.js +66 -0
  43. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  44. package/dist/react-native/RaftStruct.d.ts +2 -2
  45. package/dist/react-native/RaftStruct.js +97 -26
  46. package/dist/react-native/RaftStruct.js.map +1 -1
  47. package/dist/react-native/RaftSystemType.d.ts +1 -0
  48. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  49. package/dist/react-native/RaftSystemUtils.js +51 -0
  50. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  51. package/dist/react-native/RaftTimezone.d.ts +16 -0
  52. package/dist/react-native/RaftTimezone.js +153 -0
  53. package/dist/react-native/RaftTimezone.js.map +1 -0
  54. package/dist/react-native/RaftTypes.d.ts +27 -1
  55. package/dist/react-native/RaftTypes.js.map +1 -1
  56. package/dist/react-native/RaftUpdateManager.js +1 -1
  57. package/dist/react-native/RaftUpdateManager.js.map +1 -1
  58. package/dist/react-native/main.d.ts +3 -0
  59. package/dist/react-native/main.js +6 -1
  60. package/dist/react-native/main.js.map +1 -1
  61. package/dist/web/PseudocodeTranspiler.d.ts +6 -0
  62. package/dist/web/PseudocodeTranspiler.js +115 -0
  63. package/dist/web/PseudocodeTranspiler.js.map +1 -0
  64. package/dist/web/RaftAttributeHandler.d.ts +1 -1
  65. package/dist/web/RaftAttributeHandler.js +108 -32
  66. package/dist/web/RaftAttributeHandler.js.map +1 -1
  67. package/dist/web/RaftChannelBLE.web.d.ts +4 -0
  68. package/dist/web/RaftChannelBLE.web.js +59 -21
  69. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  70. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  71. package/dist/web/RaftChannelSimulated.js +10 -6
  72. package/dist/web/RaftChannelSimulated.js.map +1 -1
  73. package/dist/web/RaftChannelWebSerial.js +1 -1
  74. package/dist/web/RaftChannelWebSerial.js.map +1 -1
  75. package/dist/web/RaftChannelWebSocket.js +16 -1
  76. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  77. package/dist/web/RaftConnector.d.ts +11 -1
  78. package/dist/web/RaftConnector.js +75 -9
  79. package/dist/web/RaftConnector.js.map +1 -1
  80. package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
  81. package/dist/web/RaftCustomAttrHandler.js +32 -44
  82. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  83. package/dist/web/RaftDeviceInfo.d.ts +18 -0
  84. package/dist/web/RaftDeviceInfo.js +8 -0
  85. package/dist/web/RaftDeviceInfo.js.map +1 -1
  86. package/dist/web/RaftDeviceManager.d.ts +47 -2
  87. package/dist/web/RaftDeviceManager.js +696 -104
  88. package/dist/web/RaftDeviceManager.js.map +1 -1
  89. package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
  90. package/dist/web/RaftDeviceStates.d.ts +27 -3
  91. package/dist/web/RaftDeviceStates.js +31 -6
  92. package/dist/web/RaftDeviceStates.js.map +1 -1
  93. package/dist/web/RaftFileHandler.d.ts +0 -1
  94. package/dist/web/RaftFileHandler.js +61 -23
  95. package/dist/web/RaftFileHandler.js.map +1 -1
  96. package/dist/web/RaftPublish.d.ts +2 -0
  97. package/dist/web/RaftPublish.js +81 -0
  98. package/dist/web/RaftPublish.js.map +1 -0
  99. package/dist/web/RaftStreamHandler.d.ts +11 -0
  100. package/dist/web/RaftStreamHandler.js +66 -0
  101. package/dist/web/RaftStreamHandler.js.map +1 -1
  102. package/dist/web/RaftStruct.d.ts +2 -2
  103. package/dist/web/RaftStruct.js +97 -26
  104. package/dist/web/RaftStruct.js.map +1 -1
  105. package/dist/web/RaftSystemType.d.ts +1 -0
  106. package/dist/web/RaftSystemUtils.d.ts +17 -1
  107. package/dist/web/RaftSystemUtils.js +51 -0
  108. package/dist/web/RaftSystemUtils.js.map +1 -1
  109. package/dist/web/RaftTimezone.d.ts +16 -0
  110. package/dist/web/RaftTimezone.js +153 -0
  111. package/dist/web/RaftTimezone.js.map +1 -0
  112. package/dist/web/RaftTypes.d.ts +27 -1
  113. package/dist/web/RaftTypes.js.map +1 -1
  114. package/dist/web/RaftUpdateManager.js +1 -1
  115. package/dist/web/RaftUpdateManager.js.map +1 -1
  116. package/dist/web/main.d.ts +3 -0
  117. package/dist/web/main.js +6 -1
  118. package/dist/web/main.js.map +1 -1
  119. package/examples/dashboard/package.json +2 -2
  120. package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
  121. package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
  122. package/examples/dashboard/src/DevicePanel.tsx +92 -11
  123. package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
  124. package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
  125. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  126. package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
  127. package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
  128. package/examples/dashboard/src/LoggingPanel.tsx +264 -0
  129. package/examples/dashboard/src/Main.tsx +12 -2
  130. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  131. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
  132. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  133. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  134. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
  135. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
  136. package/examples/dashboard/src/styles.css +766 -1
  137. package/notes/web-ble-reconnect-retry.md +69 -0
  138. package/package.json +10 -7
  139. package/src/PseudocodeTranspiler.test.ts +372 -0
  140. package/src/PseudocodeTranspiler.ts +127 -0
  141. package/src/RaftAttributeHandler.ts +152 -76
  142. package/src/RaftChannelBLE.web.ts +62 -20
  143. package/src/RaftChannelSimulated.ts +11 -6
  144. package/src/RaftChannelWebSerial.ts +1 -1
  145. package/src/RaftChannelWebSocket.ts +16 -2
  146. package/src/RaftConnector.ts +93 -15
  147. package/src/RaftCustomAttrHandler.ts +35 -45
  148. package/src/RaftDeviceInfo.ts +27 -0
  149. package/src/RaftDeviceManager.test.ts +164 -0
  150. package/src/RaftDeviceManager.ts +823 -121
  151. package/src/RaftDeviceMgrIF.ts +13 -2
  152. package/src/RaftDeviceStates.ts +49 -8
  153. package/src/RaftFileHandler.ts +69 -28
  154. package/src/RaftPublish.ts +92 -0
  155. package/src/RaftStreamHandler.ts +84 -1
  156. package/src/RaftStruct.test.ts +229 -0
  157. package/src/RaftStruct.ts +101 -37
  158. package/src/RaftSystemType.ts +1 -0
  159. package/src/RaftSystemUtils.ts +59 -0
  160. package/src/RaftTimezone.ts +151 -0
  161. package/src/RaftTypes.ts +34 -1
  162. package/src/RaftUpdateManager.ts +1 -1
  163. package/src/main.ts +3 -0
@@ -11,9 +11,9 @@ 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
- // import RaftUtils from "./RaftUtils";
17
17
  class DeviceManager {
18
18
  getDevicesState() {
19
19
  return this._devicesState;
@@ -21,6 +21,12 @@ class DeviceManager {
21
21
  getDeviceState(deviceKey) {
22
22
  return this._devicesState[deviceKey];
23
23
  }
24
+ getDeviceStats(deviceKey) {
25
+ return this.cloneDeviceStats(this.getOrCreateDeviceStats(deviceKey));
26
+ }
27
+ resetDeviceStats(deviceKey) {
28
+ this._deviceStats[deviceKey] = this.createEmptyStats();
29
+ }
24
30
  // Constructor
25
31
  constructor() {
26
32
  // Max data points to store
@@ -42,8 +48,14 @@ class DeviceManager {
42
48
  this._newDeviceCallbacks = [];
43
49
  this._newDeviceAttributeCallbacks = [];
44
50
  this._newAttributeDataCallbacks = [];
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
@@ -112,6 +124,22 @@ class DeviceManager {
112
124
  removeAttributeDataCallback(callback) {
113
125
  this._newAttributeDataCallbacks = this._newAttributeDataCallbacks.filter((cb) => cb !== callback);
114
126
  }
127
+ addDecodedDataCallback(callback) {
128
+ if (!this._decodedDataCallbacks.includes(callback)) {
129
+ this._decodedDataCallbacks.push(callback);
130
+ }
131
+ }
132
+ removeDecodedDataCallback(callback) {
133
+ this._decodedDataCallbacks = this._decodedDataCallbacks.filter((cb) => cb !== callback);
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
+ }
115
143
  ////////////////////////////////////////////////////////////////////////////
116
144
  // Set the friendly name for the device
117
145
  ////////////////////////////////////////////////////////////////////////////
@@ -125,75 +153,183 @@ class DeviceManager {
125
153
  ////////////////////////////////////////////////////////////////////////////
126
154
  async handleClientMsgBinary(rxMsg) {
127
155
  // console.log(`DeviceManager client1 msg ${RaftUtils.bufferToHex(rxMsg)}`);
128
- // Example messages
129
- // 0080 0015 81 0000006a 0004 53b7 feff00000100081857079314 0011 80 00000000 0011 53b2 075106e400d60054 0010 80 00000000 0012 5231 000d0000010e01
130
- // 0080 0011 80 00000000 0002 4ae1 0787052606240007 000e 80 00000000 0003 0006 030001af01
131
- // 0080 0011 80 00000000 0002 e46e 061e05a206830433 0010 80000000000003e4760006030001c701
132
- // 0080 0010 81 00000015 0004 e4a2 0650fe00305002 0011 80000000000002e4a8061f059f06850438 001080000000000003e4aa0006030001c701
133
- // 0080 0011 80 00000000 0002 31e4 05ea05a506660137 000e 80 00000000 0003 0007 030001d901
134
- // 0080 0011 80 00000000 0002 4d63 0792053e06500061 000e 80 00000000 0003 0005 030001de01
135
- // First two bytes of each message are the message type (0080)
136
- // There are then a series of sections each of which is the data for a device
137
- // First two bytes of each section is the section length (big endian) not including the section length bytes themselves
138
- // 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
139
- // Next is the device address (4 bytes big endian)
140
- // Next is the device type index (2 bytes big endian)
141
- // 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
142
206
  // Debug
143
207
  // const debugMsgTime = Date.now();
144
208
  const debugMsgIndex = this._debugMsgIndex++;
145
- // Message layout
146
- const msgTypeLen = 2; // Length of the message type (first two bytes)
147
- const sectionLengthLen = 2; // Length of the inclusive section length (first two bytes of each section)
148
- const sectionConnectionModeLen = 1; // Length of the connection mode (next byte after section length)
149
- const sectionDeviceAddrLen = 4; // Length of the device address (next 4 bytes after connection mode)
150
- const sectionDeviceTypeIdxLen = 2; // Length of the device type index (next 2 bytes after device address)
151
- 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
152
222
  // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} rxMsg.length ${rxMsg.length} rxMsg ${RaftUtils.bufferToHex(rxMsg)}`);
153
223
  // Start after the message type
154
224
  let msgPos = msgTypeLen;
155
- // 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
156
261
  while (msgPos < rxMsg.length) {
157
- // Check length
262
+ // Check minimum length for record length prefix + record header
158
263
  const remainingLen = rxMsg.length - msgPos;
159
- if (remainingLen < sectionLengthLen + sectionHeaderLen) {
160
- 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}`);
161
266
  return;
162
267
  }
163
- // Get the length of the section
164
- const sectionLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
165
- if (sectionLen > remainingLen + sectionLengthLen) {
166
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} msgLen ${sectionLen} remainingLenAfterLenBytes ${remainingLen - sectionLengthLen}`);
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}`);
167
272
  return;
168
273
  }
169
- // Extract message elements
170
- let sectionPos = msgPos + sectionLengthLen;
171
- const busNum = rxMsg[sectionPos] & 0x7f;
172
- const isOnline = (rxMsg[sectionPos] & 0x80) !== 0;
173
- sectionPos += sectionConnectionModeLen;
174
- // Get the device address and type index
175
- const devAddr = (rxMsg[sectionPos] << 24) + (rxMsg[sectionPos + 1] << 16) + (rxMsg[sectionPos + 2] << 8) + rxMsg[sectionPos + 3];
176
- sectionPos += sectionDeviceAddrLen;
177
- const devTypeIdx = (rxMsg[sectionPos] << 8) + rxMsg[sectionPos + 1];
178
- let attrGroupPos = sectionPos + sectionDeviceTypeIdxLen;
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}`);
298
+ return;
299
+ }
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;
179
308
  // Debug
180
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length} sectionPos ${msgPos} sectionLen ${sectionLen} ${attrGroupPos} ${RaftUtils.bufferToHex(rxMsg.slice(msgPos, msgPos + sectionLen))}`);
181
- // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} connMode ${busNum} isOnline ${isOnline} devAddr ${devAddr} devTypeIdx ${devTypeIdx} attrGroupDataLen ${sectionLen - sectionHeaderLen}`);
182
- // Device key
183
- 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);
184
314
  // Update the last update time
185
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
+ }
186
322
  // Check if a device state already exists
187
323
  if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
188
324
  // Get the device type info
189
- 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());
190
326
  // Debug
191
- // 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)}`);
192
328
  // Handle case where device type info is not available
193
329
  if (deviceTypeInfo === undefined) {
194
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this section`);
195
- // Skip to next section without processing attributes
196
- 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;
197
333
  continue;
198
334
  }
199
335
  // Check if device record exists
@@ -202,7 +338,7 @@ class DeviceManager {
202
338
  this._devicesState[deviceKey].deviceTypeInfo = deviceTypeInfo;
203
339
  this._devicesState[deviceKey].deviceType = deviceTypeInfo.name || "";
204
340
  this._devicesState[deviceKey].busName = busNum.toString();
205
- this._devicesState[deviceKey].deviceAddress = devAddr.toString();
341
+ this._devicesState[deviceKey].deviceAddress = devAddrHex;
206
342
  }
207
343
  }
208
344
  else {
@@ -212,13 +348,19 @@ class DeviceManager {
212
348
  deviceTimeline: {
213
349
  timestampsUs: [],
214
350
  lastReportTimestampUs: 0,
215
- reportTimestampOffsetUs: 0
351
+ reportTimestampOffsetUs: 0,
352
+ totalSamplesAdded: 0,
353
+ emaLastSampleTimeUs: 0,
354
+ emaIntervalUs: 0,
355
+ emaPrevPollTimeUs: 0,
356
+ emaCalibrated: false,
357
+ emaCalibrationPolls: 0
216
358
  },
217
359
  deviceAttributes: {},
218
360
  deviceIsNew: true,
219
361
  stateChanged: false,
220
- isOnline: true,
221
- deviceAddress: devAddr.toString(),
362
+ onlineState: RaftDeviceStates_1.DeviceOnlineState.Online,
363
+ deviceAddress: devAddrHex,
222
364
  deviceType: (deviceTypeInfo === null || deviceTypeInfo === void 0 ? void 0 : deviceTypeInfo.name) || "",
223
365
  busName: busNum.toString()
224
366
  };
@@ -226,44 +368,79 @@ class DeviceManager {
226
368
  }
227
369
  // Get device state
228
370
  const deviceState = this._devicesState[deviceKey];
229
- deviceState.isOnline = isOnline;
371
+ deviceState.onlineState = isOnline ? RaftDeviceStates_1.DeviceOnlineState.Online : RaftDeviceStates_1.DeviceOnlineState.Offline;
230
372
  // Check if device type info is available and complete
231
373
  if (deviceState.deviceTypeInfo && deviceState.deviceTypeInfo.resp) {
232
374
  // Iterate over attributes in the group
233
375
  const pollRespMetadata = deviceState.deviceTypeInfo.resp;
234
- // Iterate over attribute groups
235
- const attrGroupDataLen = sectionLen - sectionHeaderLen;
236
- const attrGroupStartPos = attrGroupPos;
237
- while (attrGroupPos < attrGroupStartPos + attrGroupDataLen) {
238
- // Add bounds checking
239
- if (attrGroupPos >= rxMsg.length) {
240
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} exceeds message length ${rxMsg.length}`);
241
- break;
376
+ // Process samples within this record
377
+ const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
378
+ const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
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;
242
404
  }
243
- const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, attrGroupPos, deviceState.deviceTimeline, pollRespMetadata, deviceState.deviceAttributes, this._maxDatapointsToStore);
244
- // 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)}`);
245
- if (newMsgBufIdx < 0) {
246
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup failed newMsgBufIdx ${newMsgBufIdx}`);
247
- 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}`);
248
411
  }
249
- // Prevent infinite loops
250
- if (newMsgBufIdx <= attrGroupPos) {
251
- console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup didn't advance position from ${attrGroupPos} to ${newMsgBufIdx}`);
252
- 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
+ }
253
430
  }
254
- attrGroupPos = newMsgBufIdx;
255
- deviceState.stateChanged = true;
256
- // 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}`);
257
- // console.log(`DevMan.handleClientMsgBinary group done debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} sectionLen ${sectionLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
258
431
  }
432
+ // Inform decoded-data callbacks
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());
259
436
  }
260
437
  else {
261
438
  console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceState incomplete for device ${deviceKey}, skipping attribute processing`);
262
439
  }
263
440
  // Debug
264
- // console.log(`DevMan.handleClientMsgBinary section done debugIdx ${debugMsgIndex} attrGroupPos ${attrGroupPos} sectionLen ${sectionLen} msgPos ${msgPos} newMsgPos ${msgPos + sectionLengthLen + sectionLen} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
265
- // Move to next message
266
- 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;
267
444
  }
268
445
  // Check for devices that have not been updated for a while
269
446
  if (this._removeDevicesFlag) {
@@ -271,6 +448,7 @@ class DeviceManager {
271
448
  Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
272
449
  if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
273
450
  delete this._devicesState[deviceKey];
451
+ delete this._deviceStats[deviceKey];
274
452
  }
275
453
  });
276
454
  }
@@ -285,6 +463,10 @@ class DeviceManager {
285
463
  // console.log(`DeviceManager client msg ${JSON.stringify(data)}`);
286
464
  // Iterate over the buses
287
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
+ }
288
470
  // Check for bus status info
289
471
  if (devices && typeof devices === "object" && "_s" in devices) {
290
472
  // console.log(`DeviceManager bus status ${JSON.stringify(devices._s)}`);
@@ -292,21 +474,27 @@ class DeviceManager {
292
474
  }
293
475
  // Iterate over the devices
294
476
  Object.entries(devices).forEach(async ([devAddr, attrGroups]) => {
477
+ var _a;
295
478
  // Check for non-device info (starts with _)
296
479
  if (devAddr.startsWith("_")) {
297
480
  return;
298
481
  }
299
482
  // Device type name
300
483
  let deviceTypeName = "";
484
+ let deviceTypeIdx = -1;
301
485
  if (attrGroups && typeof attrGroups === 'object' && "_t" in attrGroups && typeof attrGroups._t === "string") {
302
486
  deviceTypeName = attrGroups._t || "";
303
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
+ }
304
492
  else {
305
493
  console.warn(`DeviceManager missing device type attrGroups ${JSON.stringify(attrGroups)}`);
306
494
  return;
307
495
  }
308
496
  // Device key
309
- const deviceKey = (0, RaftDeviceStates_1.getDeviceKey)(busName, devAddr, deviceTypeName);
497
+ const deviceKey = (0, RaftDeviceStates_1.getDeviceKey)(busName, devAddr);
310
498
  // Update the last update time
311
499
  this._deviceLastUpdateTime[deviceKey] = Date.now();
312
500
  // Check if a device state already exists
@@ -329,12 +517,18 @@ class DeviceManager {
329
517
  deviceTimeline: {
330
518
  timestampsUs: [],
331
519
  lastReportTimestampUs: 0,
332
- reportTimestampOffsetUs: 0
520
+ reportTimestampOffsetUs: 0,
521
+ totalSamplesAdded: 0,
522
+ emaLastSampleTimeUs: 0,
523
+ emaIntervalUs: 0,
524
+ emaPrevPollTimeUs: 0,
525
+ emaCalibrated: false,
526
+ emaCalibrationPolls: 0
333
527
  },
334
528
  deviceAttributes: {},
335
529
  deviceIsNew: true,
336
530
  stateChanged: false,
337
- isOnline: true,
531
+ onlineState: RaftDeviceStates_1.DeviceOnlineState.Online,
338
532
  deviceAddress: devAddr,
339
533
  deviceType: deviceTypeName,
340
534
  busName: busName
@@ -343,14 +537,21 @@ class DeviceManager {
343
537
  }
344
538
  // Get device state
345
539
  const deviceState = this._devicesState[deviceKey];
346
- // Check for online/offline state information
540
+ // Check for online/offline/pending-deletion state information
347
541
  if (attrGroups && typeof attrGroups === "object" && "_o" in attrGroups) {
348
- 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;
349
549
  }
350
550
  // Check if device type info is available
351
551
  if (!deviceState.deviceTypeInfo) {
352
552
  return;
353
553
  }
554
+ const markers = this.extractMarkers(attrGroups);
354
555
  // Iterate attribute groups
355
556
  Object.entries(attrGroups).forEach(([attrGroupName, msgHexStr]) => {
356
557
  // Check valid
@@ -367,6 +568,9 @@ class DeviceManager {
367
568
  let msgBufIdx = 0;
368
569
  // Iterate over attributes in the group
369
570
  const pollRespMetadata = deviceState.deviceTypeInfo.resp;
571
+ const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
572
+ const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
573
+ const totalSamplesBefore = deviceState.deviceTimeline.totalSamplesAdded;
370
574
  // Loop
371
575
  while (msgBufIdx < msgBytes.length) {
372
576
  const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(msgBytes, msgBufIdx, deviceState.deviceTimeline, pollRespMetadata, deviceState.deviceAttributes, this._maxDatapointsToStore);
@@ -375,6 +579,9 @@ class DeviceManager {
375
579
  msgBufIdx = newMsgBufIdx;
376
580
  deviceState.stateChanged = true;
377
581
  }
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());
378
585
  });
379
586
  });
380
587
  });
@@ -384,6 +591,7 @@ class DeviceManager {
384
591
  Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
385
592
  if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
386
593
  delete this._devicesState[deviceKey];
594
+ delete this._deviceStats[deviceKey];
387
595
  }
388
596
  });
389
597
  }
@@ -415,6 +623,20 @@ class DeviceManager {
415
623
  });
416
624
  }
417
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
+ ////////////////////////////////////////////////////////////////////////////
418
640
  // Get device type info
419
641
  ////////////////////////////////////////////////////////////////////////////
420
642
  async getDeviceTypeInfo(busName, deviceType) {
@@ -483,6 +705,117 @@ class DeviceManager {
483
705
  return undefined;
484
706
  }
485
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
+ }
486
819
  ////////////////////////////////////////////////////////////////////////////
487
820
  // Send action to device
488
821
  ////////////////////////////////////////////////////////////////////////////
@@ -492,40 +825,77 @@ class DeviceManager {
492
825
  .join("");
493
826
  }
494
827
  async sendAction(deviceKey, action, data) {
495
- // console.log(`DeviceManager sendAction ${deviceKey} action name ${action.n} value ${value} prefix ${action.w}`);
496
- var _a;
497
- let writeBytes;
498
- // Check for one data item
499
- if (data.length === 1) {
500
- let value = data[0];
501
- // Check for conversion
502
- if (action.sub !== undefined) {
503
- 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;
504
860
  }
505
- if (action.mul !== undefined) {
506
- value = value * action.mul;
861
+ catch (error) {
862
+ console.warn(`DeviceManager sendAction error ${error}`);
863
+ return false;
507
864
  }
508
- // Form the write bytes
509
- writeBytes = action.t ? (0, RaftStruct_1.structPack)(action.t, [value]) : new Uint8Array(0);
510
865
  }
511
866
  else {
512
- // Form the write bytes which may have multiple data items
513
- 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);
514
887
  }
515
- // Convert to hex string
516
- let writeHexStr = this.toHex(writeBytes);
517
888
  // Add prefix and postfix
518
889
  writeHexStr = (action.w ? action.w : "") + writeHexStr + (action.wz ? action.wz : "");
519
- // Separate the bus and address in the deviceKey (_ char)
520
- const devBus = deviceKey.split("_")[0];
521
- const devAddr = deviceKey.split("_")[1];
890
+ // Parse the device key into bus and address components
891
+ const { bus: devBus, addr: devAddr } = this.parseDeviceKeyForCommand(deviceKey);
522
892
  // Send the action to the server
523
893
  const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + writeHexStr;
524
894
  console.log(`DeviceManager deviceKey ${deviceKey} action name ${action.n} value ${data} prefix ${action.w} sendAction ${cmd}`);
525
895
  // Send the command
526
896
  try {
527
897
  // Get the msg handler
528
- 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();
529
899
  if (msgHandler) {
530
900
  const msgRslt = await msgHandler.sendRICRESTURL(cmd);
531
901
  return msgRslt.rslt === "ok";
@@ -565,6 +935,111 @@ class DeviceManager {
565
935
  return false;
566
936
  }
567
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
+ ////////////////////////////////////////////////////////////////////////////
568
1043
  // Convert hex to bytes
569
1044
  ////////////////////////////////////////////////////////////////////////////
570
1045
  hexToBytes(hex) {
@@ -574,6 +1049,123 @@ class DeviceManager {
574
1049
  }
575
1050
  return bytes;
576
1051
  }
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
+ ////////////////////////////////////////////////////////////////////////////
1104
+ // Helpers for decoded data callbacks
1105
+ ////////////////////////////////////////////////////////////////////////////
1106
+ snapshotAttrLengths(deviceAttrs, pollRespMetadata) {
1107
+ const lengths = {};
1108
+ if (!pollRespMetadata) {
1109
+ return lengths;
1110
+ }
1111
+ pollRespMetadata.a.forEach((attr) => {
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;
1114
+ });
1115
+ return lengths;
1116
+ }
1117
+ emitDecodedData(deviceKey, busName, devAddr, deviceState, pollRespMetadata, attrLengthsBefore, timelineLenBefore, attrGroupName = "", markers) {
1118
+ if (!pollRespMetadata) {
1119
+ return;
1120
+ }
1121
+ const attrValues = {};
1122
+ let hasValues = false;
1123
+ pollRespMetadata.a.forEach((attr) => {
1124
+ var _a;
1125
+ const attrState = deviceState.deviceAttributes[attr.n];
1126
+ if (!attrState) {
1127
+ return;
1128
+ }
1129
+ const prevLen = (_a = attrLengthsBefore[attr.n]) !== null && _a !== void 0 ? _a : 0;
1130
+ if (attrState.values.length > prevLen) {
1131
+ attrValues[attr.n] = attrState.values.slice(prevLen);
1132
+ hasValues = hasValues || attrValues[attr.n].length > 0;
1133
+ }
1134
+ });
1135
+ if (!hasValues) {
1136
+ return;
1137
+ }
1138
+ const timestampsUs = deviceState.deviceTimeline.timestampsUs.slice(timelineLenBefore);
1139
+ const decoded = {
1140
+ deviceKey,
1141
+ busName,
1142
+ deviceAddress: devAddr,
1143
+ deviceType: deviceState.deviceType,
1144
+ attrGroupName: attrGroupName || undefined,
1145
+ attrValues,
1146
+ timestampsUs,
1147
+ };
1148
+ if (markers && Object.keys(markers).length > 0) {
1149
+ decoded.markers = markers;
1150
+ decoded.fromOfflineBuffer = this.isTruthy(markers["_buf"]);
1151
+ }
1152
+ this._decodedDataCallbacks.forEach((cb) => cb(decoded));
1153
+ }
1154
+ extractMarkers(attrGroups) {
1155
+ const markers = {};
1156
+ if (!attrGroups || typeof attrGroups !== "object") {
1157
+ return markers;
1158
+ }
1159
+ Object.entries(attrGroups).forEach(([key, value]) => {
1160
+ if (key.startsWith("_") && key !== "_t" && key !== "_o") {
1161
+ markers[key] = value;
1162
+ }
1163
+ });
1164
+ return markers;
1165
+ }
1166
+ isTruthy(val) {
1167
+ return val === true || val === 1 || val === "1";
1168
+ }
577
1169
  }
578
1170
  exports.DeviceManager = DeviceManager;
579
1171
  //# sourceMappingURL=RaftDeviceManager.js.map