@robdobsn/raftjs 1.8.5 → 1.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.editorconfig +14 -0
  2. package/.gitattributes +11 -0
  3. package/.nvmrc +1 -0
  4. package/TODO.md +1 -0
  5. package/dist/react-native/RaftAttributeHandler.d.ts +14 -0
  6. package/dist/react-native/RaftAttributeHandler.js +375 -0
  7. package/dist/react-native/RaftAttributeHandler.js.map +1 -0
  8. package/dist/react-native/RaftChannel.d.ts +20 -0
  9. package/dist/react-native/RaftChannel.js +12 -0
  10. package/dist/react-native/RaftChannel.js.map +1 -0
  11. package/dist/react-native/RaftChannelBLE.native.d.ts +95 -0
  12. package/dist/react-native/RaftChannelBLE.native.js +483 -0
  13. package/dist/react-native/RaftChannelBLE.native.js.map +1 -0
  14. package/dist/react-native/RaftChannelBLE.web.d.ts +40 -0
  15. package/dist/react-native/RaftChannelBLE.web.js +302 -0
  16. package/dist/react-native/RaftChannelBLE.web.js.map +1 -0
  17. package/dist/react-native/RaftChannelBLEFactory.d.ts +10 -0
  18. package/dist/react-native/RaftChannelBLEFactory.js +17 -0
  19. package/dist/react-native/RaftChannelBLEFactory.js.map +1 -0
  20. package/dist/react-native/RaftChannelBLEScanner.native.d.ts +18 -0
  21. package/dist/react-native/RaftChannelBLEScanner.native.js +138 -0
  22. package/dist/react-native/RaftChannelBLEScanner.native.js.map +1 -0
  23. package/dist/react-native/RaftChannelSimulated.d.ts +42 -0
  24. package/dist/react-native/RaftChannelSimulated.js +1001 -0
  25. package/dist/react-native/RaftChannelSimulated.js.map +1 -0
  26. package/dist/react-native/RaftChannelWebSerial.d.ts +39 -0
  27. package/dist/react-native/RaftChannelWebSerial.js +329 -0
  28. package/dist/react-native/RaftChannelWebSerial.js.map +1 -0
  29. package/dist/react-native/RaftChannelWebSocket.d.ts +30 -0
  30. package/dist/react-native/RaftChannelWebSocket.js +222 -0
  31. package/dist/react-native/RaftChannelWebSocket.js.map +1 -0
  32. package/dist/react-native/RaftCommsStats.d.ts +39 -0
  33. package/dist/react-native/RaftCommsStats.js +128 -0
  34. package/dist/react-native/RaftCommsStats.js.map +1 -0
  35. package/dist/react-native/RaftConnEvents.d.ts +39 -0
  36. package/dist/react-native/RaftConnEvents.js +54 -0
  37. package/dist/react-native/RaftConnEvents.js.map +1 -0
  38. package/dist/react-native/RaftConnector.d.ts +257 -0
  39. package/dist/react-native/RaftConnector.js +671 -0
  40. package/dist/react-native/RaftConnector.js.map +1 -0
  41. package/dist/react-native/RaftCustomAttrHandler.d.ts +6 -0
  42. package/dist/react-native/RaftCustomAttrHandler.js +93 -0
  43. package/dist/react-native/RaftCustomAttrHandler.js.map +1 -0
  44. package/dist/react-native/RaftDeviceInfo.d.ts +71 -0
  45. package/dist/react-native/RaftDeviceInfo.js +50 -0
  46. package/dist/react-native/RaftDeviceInfo.js.map +1 -0
  47. package/dist/react-native/RaftDeviceManager.d.ts +73 -0
  48. package/dist/react-native/RaftDeviceManager.js +812 -0
  49. package/dist/react-native/RaftDeviceManager.js.map +1 -0
  50. package/dist/react-native/RaftDeviceMgrIF.d.ts +19 -0
  51. package/dist/react-native/RaftDeviceMgrIF.js +11 -0
  52. package/dist/react-native/RaftDeviceMgrIF.js.map +1 -0
  53. package/dist/react-native/RaftDeviceMsg.d.ts +9 -0
  54. package/dist/react-native/RaftDeviceMsg.js +11 -0
  55. package/dist/react-native/RaftDeviceMsg.js.map +1 -0
  56. package/dist/react-native/RaftDeviceStates.d.ts +55 -0
  57. package/dist/react-native/RaftDeviceStates.js +81 -0
  58. package/dist/react-native/RaftDeviceStates.js.map +1 -0
  59. package/dist/react-native/RaftFileHandler.d.ts +52 -0
  60. package/dist/react-native/RaftFileHandler.js +502 -0
  61. package/dist/react-native/RaftFileHandler.js.map +1 -0
  62. package/dist/react-native/RaftLog.d.ts +22 -0
  63. package/dist/react-native/RaftLog.js +63 -0
  64. package/dist/react-native/RaftLog.js.map +1 -0
  65. package/dist/react-native/RaftMiniHDLC.d.ts +18 -0
  66. package/dist/react-native/RaftMiniHDLC.js +383 -0
  67. package/dist/react-native/RaftMiniHDLC.js.map +1 -0
  68. package/dist/react-native/RaftMsgHandler.d.ts +62 -0
  69. package/dist/react-native/RaftMsgHandler.js +511 -0
  70. package/dist/react-native/RaftMsgHandler.js.map +1 -0
  71. package/dist/react-native/RaftMsgTrackInfo.d.ts +17 -0
  72. package/dist/react-native/RaftMsgTrackInfo.js +42 -0
  73. package/dist/react-native/RaftMsgTrackInfo.js.map +1 -0
  74. package/dist/react-native/RaftProtocolDefs.d.ts +30 -0
  75. package/dist/react-native/RaftProtocolDefs.js +48 -0
  76. package/dist/react-native/RaftProtocolDefs.js.map +1 -0
  77. package/dist/react-native/RaftPublish.d.ts +2 -0
  78. package/dist/react-native/RaftPublish.js +81 -0
  79. package/dist/react-native/RaftPublish.js.map +1 -0
  80. package/dist/react-native/RaftStreamHandler.d.ts +49 -0
  81. package/dist/react-native/RaftStreamHandler.js +324 -0
  82. package/dist/react-native/RaftStreamHandler.js.map +1 -0
  83. package/dist/react-native/RaftStruct.d.ts +3 -0
  84. package/dist/react-native/RaftStruct.js +258 -0
  85. package/dist/react-native/RaftStruct.js.map +1 -0
  86. package/dist/react-native/RaftSysTypeManager.d.ts +16 -0
  87. package/dist/react-native/RaftSysTypeManager.js +78 -0
  88. package/dist/react-native/RaftSysTypeManager.js.map +1 -0
  89. package/dist/react-native/RaftSystemType.d.ts +30 -0
  90. package/dist/react-native/RaftSystemType.js +3 -0
  91. package/dist/react-native/RaftSystemType.js.map +1 -0
  92. package/dist/react-native/RaftSystemUtils.d.ts +152 -0
  93. package/dist/react-native/RaftSystemUtils.js +463 -0
  94. package/dist/react-native/RaftSystemUtils.js.map +1 -0
  95. package/dist/react-native/RaftTypes.d.ts +216 -0
  96. package/dist/react-native/RaftTypes.js +153 -0
  97. package/dist/react-native/RaftTypes.js.map +1 -0
  98. package/dist/react-native/RaftUpdateEvents.d.ts +33 -0
  99. package/dist/react-native/RaftUpdateEvents.js +46 -0
  100. package/dist/react-native/RaftUpdateEvents.js.map +1 -0
  101. package/dist/react-native/RaftUpdateManager.d.ts +61 -0
  102. package/dist/react-native/RaftUpdateManager.js +621 -0
  103. package/dist/react-native/RaftUpdateManager.js.map +1 -0
  104. package/dist/react-native/RaftUtils.d.ts +128 -0
  105. package/dist/react-native/RaftUtils.js +487 -0
  106. package/dist/react-native/RaftUtils.js.map +1 -0
  107. package/dist/react-native/RaftWifiTypes.d.ts +23 -0
  108. package/dist/react-native/RaftWifiTypes.js +43 -0
  109. package/dist/react-native/RaftWifiTypes.js.map +1 -0
  110. package/dist/react-native/main.d.ts +27 -0
  111. package/dist/react-native/main.js +52 -0
  112. package/dist/react-native/main.js.map +1 -0
  113. package/dist/web/RaftAttributeHandler.js +1 -1
  114. package/dist/web/RaftAttributeHandler.js.map +1 -1
  115. package/dist/web/RaftChannelBLE.web.js +8 -6
  116. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  117. package/dist/web/RaftChannelSimulated.d.ts +10 -0
  118. package/dist/web/RaftChannelSimulated.js +665 -82
  119. package/dist/web/RaftChannelSimulated.js.map +1 -1
  120. package/dist/web/RaftChannelWebSerial.js +2 -2
  121. package/dist/web/RaftChannelWebSerial.js.map +1 -1
  122. package/dist/web/RaftChannelWebSocket.js +16 -1
  123. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  124. package/dist/web/RaftConnector.d.ts +12 -1
  125. package/dist/web/RaftConnector.js +45 -9
  126. package/dist/web/RaftConnector.js.map +1 -1
  127. package/dist/web/RaftCustomAttrHandler.d.ts +2 -0
  128. package/dist/web/RaftCustomAttrHandler.js +54 -26
  129. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  130. package/dist/web/RaftDeviceInfo.d.ts +3 -1
  131. package/dist/web/RaftDeviceInfo.js +17 -3
  132. package/dist/web/RaftDeviceInfo.js.map +1 -1
  133. package/dist/web/RaftDeviceManager.d.ts +32 -2
  134. package/dist/web/RaftDeviceManager.js +307 -74
  135. package/dist/web/RaftDeviceManager.js.map +1 -1
  136. package/dist/web/RaftDeviceMgrIF.d.ts +5 -1
  137. package/dist/web/RaftDeviceStates.d.ts +20 -2
  138. package/dist/web/RaftDeviceStates.js +25 -4
  139. package/dist/web/RaftDeviceStates.js.map +1 -1
  140. package/dist/web/RaftMsgHandler.js.map +1 -1
  141. package/dist/web/RaftPublish.d.ts +2 -0
  142. package/dist/web/RaftPublish.js +81 -0
  143. package/dist/web/RaftPublish.js.map +1 -0
  144. package/dist/web/RaftStreamHandler.d.ts +11 -0
  145. package/dist/web/RaftStreamHandler.js +68 -1
  146. package/dist/web/RaftStreamHandler.js.map +1 -1
  147. package/dist/web/RaftStruct.js +197 -147
  148. package/dist/web/RaftStruct.js.map +1 -1
  149. package/dist/web/RaftSystemUtils.d.ts +17 -1
  150. package/dist/web/RaftSystemUtils.js +51 -0
  151. package/dist/web/RaftSystemUtils.js.map +1 -1
  152. package/dist/web/RaftTypes.d.ts +21 -0
  153. package/dist/web/RaftTypes.js.map +1 -1
  154. package/dist/web/RaftUpdateManager.js +1 -1
  155. package/dist/web/RaftUpdateManager.js.map +1 -1
  156. package/dist/web/RaftUtils.d.ts +2 -0
  157. package/dist/web/RaftUtils.js +20 -0
  158. package/dist/web/RaftUtils.js.map +1 -1
  159. package/dist/web/main.d.ts +2 -0
  160. package/dist/web/main.js +1 -0
  161. package/dist/web/main.js.map +1 -1
  162. package/eslint.config.mjs +33 -0
  163. package/examples/dashboard/package.json +36 -0
  164. package/examples/dashboard/src/CommandPanel.tsx +147 -0
  165. package/examples/dashboard/src/ConnManager.ts +166 -0
  166. package/examples/dashboard/src/DeviceActionsForm.tsx +133 -0
  167. package/examples/dashboard/src/DeviceAttrsForm.tsx +49 -0
  168. package/examples/dashboard/src/DeviceLineChart.tsx +163 -0
  169. package/examples/dashboard/src/DevicePanel.tsx +247 -0
  170. package/examples/dashboard/src/DeviceStatsPanel.tsx +65 -0
  171. package/examples/dashboard/src/DevicesPanel.tsx +69 -0
  172. package/examples/dashboard/src/DispLedGrid.tsx +110 -0
  173. package/examples/dashboard/src/DispOneLed.tsx +20 -0
  174. package/examples/dashboard/src/LatencyTest.ts +130 -0
  175. package/examples/dashboard/src/LatencyTestPanel.tsx +92 -0
  176. package/examples/dashboard/src/Main.tsx +234 -0
  177. package/examples/dashboard/src/SettingsManager.ts +67 -0
  178. package/examples/dashboard/src/SettingsScreen.tsx +179 -0
  179. package/examples/dashboard/src/StatusPanel.tsx +71 -0
  180. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +170 -0
  181. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +125 -0
  182. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +38 -0
  183. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +125 -0
  184. package/examples/dashboard/src/SystemTypeMarty/RICAddOn.ts +70 -0
  185. package/examples/dashboard/src/SystemTypeMarty/RICAddOnBase.ts +33 -0
  186. package/examples/dashboard/src/SystemTypeMarty/RICAddOnManager.ts +342 -0
  187. package/examples/dashboard/src/SystemTypeMarty/RICCommsStats.ts +170 -0
  188. package/examples/dashboard/src/SystemTypeMarty/RICHWElem.ts +123 -0
  189. package/examples/dashboard/src/SystemTypeMarty/RICLEDPatternChecker.ts +207 -0
  190. package/examples/dashboard/src/SystemTypeMarty/RICROSSerial.ts +464 -0
  191. package/examples/dashboard/src/SystemTypeMarty/RICServoFaultDetector.ts +146 -0
  192. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +105 -0
  193. package/examples/dashboard/src/SystemTypeMarty/RICSystemUtils.ts +371 -0
  194. package/examples/dashboard/src/SystemTypeMarty/RICTypes.ts +20 -0
  195. package/examples/dashboard/src/SystemTypeMarty/SystemTypeMarty.ts +119 -0
  196. package/examples/dashboard/src/index.html +15 -0
  197. package/examples/dashboard/src/index.tsx +13 -0
  198. package/examples/dashboard/src/styles.css +570 -0
  199. package/examples/dashboard/tsconfig.json +18 -0
  200. package/jest.config.js +11 -0
  201. package/package.json +49 -52
  202. package/src/RaftAttributeHandler.ts +450 -0
  203. package/src/RaftChannel.ts +32 -0
  204. package/src/RaftChannelBLE.native.ts +617 -0
  205. package/src/RaftChannelBLE.web.ts +374 -0
  206. package/src/RaftChannelBLEFactory.ts +13 -0
  207. package/src/RaftChannelBLEScanner.native.ts +184 -0
  208. package/src/RaftChannelSimulated.ts +1177 -0
  209. package/src/RaftChannelWebSerial.ts +420 -0
  210. package/src/RaftChannelWebSocket.ts +272 -0
  211. package/src/RaftCommsStats.ts +142 -0
  212. package/src/RaftConnEvents.ts +58 -0
  213. package/src/RaftConnector.ts +806 -0
  214. package/src/RaftCustomAttrHandler.ts +117 -0
  215. package/src/RaftDeviceInfo.ts +125 -0
  216. package/src/RaftDeviceManager.ts +1014 -0
  217. package/src/RaftDeviceMgrIF.ts +37 -0
  218. package/src/RaftDeviceMsg.ts +20 -0
  219. package/src/RaftDeviceStates.ts +122 -0
  220. package/src/RaftFileHandler.ts +668 -0
  221. package/src/RaftLog.ts +70 -0
  222. package/src/RaftMiniHDLC.ts +396 -0
  223. package/src/RaftMsgHandler.ts +812 -0
  224. package/src/RaftMsgTrackInfo.ts +51 -0
  225. package/src/RaftProtocolDefs.ts +46 -0
  226. package/src/RaftPublish.ts +92 -0
  227. package/src/RaftStreamHandler.ts +412 -0
  228. package/src/RaftStruct.ts +282 -0
  229. package/src/RaftSysTypeManager.ts +87 -0
  230. package/src/RaftSystemType.ts +34 -0
  231. package/src/RaftSystemUtils.ts +548 -0
  232. package/src/RaftTypes.ts +306 -0
  233. package/src/RaftUpdateEvents.ts +48 -0
  234. package/src/RaftUpdateManager.ts +781 -0
  235. package/src/RaftUtils.ts +514 -0
  236. package/src/RaftWifiTypes.ts +36 -0
  237. package/src/main.ts +40 -0
  238. package/testdata/TestDeviceTypeRecs.json +492 -0
  239. package/tsconfig.json +30 -0
  240. package/tsconfig.react-native.json +29 -0
@@ -0,0 +1,1014 @@
1
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // RaftDeviceManager
4
+ // Device manager for Raft devices
5
+ //
6
+ // Rob Dobson (C) 2024
7
+ //
8
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
+
10
+ import { DeviceAttributeState, DeviceAttributesState, DevicesState, DeviceState, DeviceStats, DeviceOnlineState, formatDeviceAddrHex, getDeviceKey, parseDeviceKey } from "./RaftDeviceStates";
11
+ import { DeviceMsgJson } from "./RaftDeviceMsg";
12
+ import { RaftOKFail } from './RaftTypes';
13
+ import { DeviceTypeInfo, DeviceTypeAction, DeviceTypeInfoRecs, RaftDevTypeInfoResponse } from "./RaftDeviceInfo";
14
+ import AttributeHandler from "./RaftAttributeHandler";
15
+ import RaftSystemUtils from "./RaftSystemUtils";
16
+ import RaftDeviceMgrIF from "./RaftDeviceMgrIF";
17
+ import { structPack } from "./RaftStruct";
18
+ // import RaftUtils from "./RaftUtils";
19
+
20
+ export interface DeviceDecodedData {
21
+ deviceKey: string;
22
+ busName: string;
23
+ deviceAddress: string;
24
+ deviceType: string;
25
+ attrGroupName?: string;
26
+ attrValues: Record<string, number[]>;
27
+ timestampsUs: number[];
28
+ markers?: Record<string, unknown>;
29
+ fromOfflineBuffer?: boolean;
30
+ }
31
+
32
+ interface DeviceStatsInternal extends DeviceStats {
33
+ windowEvents: Array<{ timeMs: number; samples: number }>;
34
+ }
35
+
36
+ export class DeviceManager implements RaftDeviceMgrIF{
37
+
38
+ // Max data points to store
39
+ private _maxDatapointsToStore = 1000;
40
+
41
+ // Min time between attempts to retrieve device type info
42
+ private _minTimeBetweenDeviceTypeInfoRetrievalMs = 60000;
43
+
44
+ // Attribute handler
45
+ private _attributeHandler = new AttributeHandler();
46
+
47
+ // Devices state
48
+ public _devicesState = new DevicesState();
49
+
50
+ // Last time each device was updated - used to detect devices that are no longer present
51
+ private _deviceLastUpdateTime: { [deviceKey: string]: number } = {};
52
+
53
+ // Flag indicating that removed devices should be removed from the state
54
+ private _removeDevicesFlag = true;
55
+ private _removeDevicesTimeMs = 60000;
56
+
57
+ // System utils
58
+ private _systemUtils: RaftSystemUtils | null = null;
59
+
60
+ // Device callbacks
61
+ private _newDeviceCallbacks: Array<(deviceKey: string, state: DeviceState) => void> = [];
62
+ private _newDeviceAttributeCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
63
+ private _newAttributeDataCallbacks: Array<(deviceKey: string, attrState: DeviceAttributeState) => void> = [];
64
+ private _decodedDataCallbacks: Array<(decoded: DeviceDecodedData) => void> = [];
65
+ private _deviceRemovedCallbacks: Array<(deviceKey: string, state: DeviceState) => void> = [];
66
+
67
+ // Debug message index (to help debug with async messages)
68
+ private _debugMsgIndex = 0;
69
+
70
+ // Device stats (sample counts, rates)
71
+ private _statsWindowMs = 5000;
72
+ private _deviceStats: { [deviceKey: string]: DeviceStatsInternal } = {};
73
+
74
+ public getDevicesState(): DevicesState {
75
+ return this._devicesState;
76
+ }
77
+
78
+ public getDeviceState(deviceKey: string): DeviceState {
79
+ return this._devicesState[deviceKey];
80
+ }
81
+
82
+ public getDeviceStats(deviceKey: string): DeviceStats {
83
+ return this.cloneDeviceStats(this.getOrCreateDeviceStats(deviceKey));
84
+ }
85
+
86
+ public resetDeviceStats(deviceKey: string): void {
87
+ this._deviceStats[deviceKey] = this.createEmptyStats();
88
+ }
89
+
90
+ // Cached device type data
91
+ private _cachedDeviceTypeRecs: DeviceTypeInfoRecs = {};
92
+
93
+ // Cached device type previous attempt times
94
+ private _cachedDeviceTypePreviousAttemptTimes: { [deviceType: string]: number } = {};
95
+
96
+ // Pending device type requests - queue-based to maintain order
97
+ private _pendingDeviceTypeRequests: { [deviceType: string]: {
98
+ promise: Promise<DeviceTypeInfo | undefined>;
99
+ waitingQueue: Array<{resolve: (value: DeviceTypeInfo | undefined) => void, reject: (reason?: any) => void}>;
100
+ } } = {};
101
+
102
+ // Constructor
103
+ constructor() {
104
+ }
105
+
106
+ ////////////////////////////////////////////////////////////////////////////
107
+ // Settings
108
+ ////////////////////////////////////////////////////////////////////////////
109
+
110
+ public setMaxDataPointsToStore(maxDatapointsToStore: number): void {
111
+ this._maxDatapointsToStore = maxDatapointsToStore;
112
+ // console.log(`DeviceManager setMaxDataPointsToStore ${maxDatapointsToStore}`);
113
+ }
114
+
115
+ ////////////////////////////////////////////////////////////////////////////
116
+ // Send REST commands
117
+ ////////////////////////////////////////////////////////////////////////////
118
+
119
+ async sendCommand(cmd: string): Promise<boolean> {
120
+ try {
121
+ // Get the msg handler
122
+ const msgHandler = this._systemUtils?.getMsgHandler();
123
+ if (msgHandler) {
124
+ const msgRslt = await msgHandler.sendRICRESTURL<RaftOKFail>(cmd);
125
+ return msgRslt.rslt === "ok";
126
+ }
127
+ return false;
128
+ } catch (error) {
129
+ console.warn(`DeviceManager sendCommand error ${error}`);
130
+ return false;
131
+ }
132
+ }
133
+
134
+ ////////////////////////////////////////////////////////////////////////////
135
+ // Setup
136
+ ////////////////////////////////////////////////////////////////////////////
137
+
138
+ public async setup(systemUtils: RaftSystemUtils): Promise<boolean> {
139
+
140
+ // Save the system utils
141
+ this._systemUtils = systemUtils;
142
+ return true;
143
+ }
144
+
145
+ ////////////////////////////////////////////////////////////////////////////
146
+ // Register callbacks
147
+ ////////////////////////////////////////////////////////////////////////////
148
+
149
+ public addNewDeviceCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
150
+ if (!this._newDeviceCallbacks.includes(callback)) {
151
+ this._newDeviceCallbacks.push(callback);
152
+ }
153
+ }
154
+
155
+ public removeNewDeviceCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
156
+ this._newDeviceCallbacks = this._newDeviceCallbacks.filter((cb) => cb !== callback);
157
+ }
158
+
159
+ public addNewAttributeCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void {
160
+ if (!this._newDeviceAttributeCallbacks.includes(callback)) {
161
+ this._newDeviceAttributeCallbacks.push(callback);
162
+ }
163
+ }
164
+
165
+ public removeNewAttributeCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void {
166
+ this._newDeviceAttributeCallbacks = this._newDeviceAttributeCallbacks.filter((cb) => cb !== callback);
167
+ }
168
+
169
+ public addAttributeDataCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void {
170
+ if (!this._newAttributeDataCallbacks.includes(callback)) {
171
+ this._newAttributeDataCallbacks.push(callback);
172
+ }
173
+ }
174
+
175
+ public removeAttributeDataCallback(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void {
176
+ this._newAttributeDataCallbacks = this._newAttributeDataCallbacks.filter((cb) => cb !== callback);
177
+ }
178
+
179
+ public addDecodedDataCallback(callback: (decoded: DeviceDecodedData) => void): void {
180
+ if (!this._decodedDataCallbacks.includes(callback)) {
181
+ this._decodedDataCallbacks.push(callback);
182
+ }
183
+ }
184
+
185
+ public removeDecodedDataCallback(callback: (decoded: DeviceDecodedData) => void): void {
186
+ this._decodedDataCallbacks = this._decodedDataCallbacks.filter((cb) => cb !== callback);
187
+ }
188
+
189
+ public addDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
190
+ if (!this._deviceRemovedCallbacks.includes(callback)) {
191
+ this._deviceRemovedCallbacks.push(callback);
192
+ }
193
+ }
194
+
195
+ public removeDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
196
+ this._deviceRemovedCallbacks = this._deviceRemovedCallbacks.filter((cb) => cb !== callback);
197
+ }
198
+
199
+ ////////////////////////////////////////////////////////////////////////////
200
+ // Set the friendly name for the device
201
+ ////////////////////////////////////////////////////////////////////////////
202
+
203
+ public async setFriendlyName(friendlyName: string): Promise<void> {
204
+ // Set using utils
205
+ await this._systemUtils?.setRaftName(friendlyName);
206
+ }
207
+
208
+ ////////////////////////////////////////////////////////////////////////////
209
+ // Handle device message binary
210
+ ////////////////////////////////////////////////////////////////////////////
211
+
212
+ public async handleClientMsgBinary(rxMsg: Uint8Array) {
213
+ // console.log(`DeviceManager client1 msg ${RaftUtils.bufferToHex(rxMsg)}`);
214
+
215
+ // DevBIN message format
216
+ //
217
+ // The rxMsg passed to this function has a 2-byte message type prefix (e.g. 0x0080)
218
+ // added by the transport layer. After that prefix comes a devbin frame:
219
+ //
220
+ // Devbin envelope (2 bytes):
221
+ // Byte 0: magic+version 0xDB = devbin v1 (valid range 0xDB–0xDF for v1–v5)
222
+ // Byte 1: topicIndex 0x00–0xFE = topic index; 0xFF = no topic
223
+ //
224
+ // Then zero or more per-device records, concatenated back-to-back:
225
+ // Bytes 0-1: recordLen uint16 big-endian — number of body bytes that follow (min 7)
226
+ // Byte 2: statusBus bit 7 = online flag, bit 6 = pending deletion, bits 5:4 = reserved, bits 3:0 = bus number (0-15)
227
+ // Bytes 3-6: address uint32 big-endian — device address on the bus
228
+ // Bytes 7-8: devTypeIdx uint16 big-endian — device type table index
229
+ // Bytes 9+: pollData variable length (recordLen − 7 bytes) — device data
230
+ //
231
+ // Example message (with transport prefix):
232
+ // 0080 DB01 0015 81 0000076a 000b bff10000ffffffff7a07d1f1221c 000e 80 00000000 001f bc340000030001
233
+ // ^^^^ ^^^^ ^^^^
234
+ // | | | || | | | Record 2 ...
235
+ // | | | || | | pollData (14 bytes)
236
+ // | | | || | devTypeIdx = 0x000b (11)
237
+ // | | | || address = 0x0000076a (slot 7, I2C addr 0x6a)
238
+ // | | | |busInfo = 0x81 (bus 1, online)
239
+ // | | | recordLen = 0x0015 (21 bytes)
240
+ // | | topicIndex = 0x01
241
+ // | magic+version = 0xDB (devbin v1)
242
+ // msgType prefix (transport layer)
243
+
244
+ // Debug
245
+ // const debugMsgTime = Date.now();
246
+ const debugMsgIndex = this._debugMsgIndex++;
247
+
248
+ // Message layout constants
249
+ const msgTypeLen = 2; // Transport-layer message type prefix (first two bytes, e.g. 0x0080)
250
+ const devbinEnvelopeLen = 2; // Devbin envelope: magic+version (1 byte) + topicIndex (1 byte)
251
+ const devbinMagicMin = 0xDB;
252
+ const devbinMagicMax = 0xDF;
253
+ const recordLenLen = 2; // Per-record length prefix (uint16 big-endian)
254
+ const busInfoLen = 1; // statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
255
+ const deviceAddrLen = 4; // Device address (uint32 big-endian)
256
+ const devTypeIdxLen = 2; // Device type index (uint16 big-endian)
257
+ const recordHeaderLen = busInfoLen + deviceAddrLen + devTypeIdxLen; // = 7, minimum record body
258
+
259
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} rxMsg.length ${rxMsg.length} rxMsg ${RaftUtils.bufferToHex(rxMsg)}`);
260
+
261
+ // Start after the message type
262
+ let msgPos = msgTypeLen;
263
+
264
+ // Check for devbin envelope (magic+version + topicIndex)
265
+ if (rxMsg.length >= msgTypeLen + devbinEnvelopeLen) {
266
+ const envelopeMagicVer = rxMsg[msgTypeLen];
267
+ if ((envelopeMagicVer & 0xF0) === 0xD0) {
268
+ if ((envelopeMagicVer < devbinMagicMin) || (envelopeMagicVer > devbinMagicMax)) {
269
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid devbin envelope magic/version ${envelopeMagicVer}`);
270
+ return;
271
+ }
272
+
273
+ const topicIndex = rxMsg[msgTypeLen + 1];
274
+ if (topicIndex !== 0xFF) {
275
+ const topicName = this._systemUtils?.getPublishTopicName(topicIndex);
276
+ if (topicName && topicName !== "devbin") {
277
+ return;
278
+ }
279
+ }
280
+
281
+ msgPos += devbinEnvelopeLen;
282
+ }
283
+ }
284
+
285
+ // Iterate through device records
286
+ while (msgPos < rxMsg.length) {
287
+
288
+ // Check minimum length for record length prefix + record header
289
+ const remainingLen = rxMsg.length - msgPos;
290
+ if (remainingLen < recordLenLen + recordHeaderLen) {
291
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid length ${rxMsg.length} < ${recordLenLen + recordHeaderLen + msgPos}`);
292
+ return;
293
+ }
294
+
295
+ // Get the record body length (bytes that follow the 2-byte length prefix)
296
+ const recordLen = (rxMsg[msgPos] << 8) + rxMsg[msgPos + 1];
297
+ if (recordLen > remainingLen - recordLenLen) {
298
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} invalid msgPos ${msgPos} recordLen ${recordLen} remainingAfterLenBytes ${remainingLen - recordLenLen}`);
299
+ return;
300
+ }
301
+
302
+ // Extract record header fields
303
+ let recordPos = msgPos + recordLenLen;
304
+
305
+ // statusBus byte: bit 7 = online, bit 6 = pending deletion, bits 3:0 = bus number
306
+ const statusByte = rxMsg[recordPos];
307
+ const busNum = statusByte & 0x0f;
308
+ const isOnline = (statusByte & 0x80) !== 0;
309
+ const isPendingDeletion = (statusByte & 0x40) !== 0;
310
+ recordPos += busInfoLen;
311
+
312
+ // Device address (uint32 big-endian)
313
+ const devAddr = (rxMsg[recordPos] << 24) + (rxMsg[recordPos + 1] << 16) + (rxMsg[recordPos + 2] << 8) + rxMsg[recordPos + 3];
314
+ recordPos += deviceAddrLen;
315
+
316
+ // Device type index (uint16 big-endian)
317
+ const devTypeIdx = (rxMsg[recordPos] << 8) + rxMsg[recordPos + 1];
318
+ let pollDataPos = recordPos + devTypeIdxLen;
319
+
320
+ // Debug
321
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} overallLen ${rxMsg.length} recordStart ${msgPos} recordLen ${recordLen} ${pollDataPos} ${RaftUtils.bufferToHex(rxMsg.slice(msgPos, msgPos + recordLenLen + recordLen))}`);
322
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} bus ${busNum} isOnline ${isOnline} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} pollDataLen ${recordLen - recordHeaderLen}`);
323
+
324
+ // Format device address as canonical hex and build device key
325
+ const devAddrHex = formatDeviceAddrHex(devAddr);
326
+ const deviceKey = getDeviceKey(busNum.toString(), devAddrHex);
327
+
328
+ // Update the last update time
329
+ this._deviceLastUpdateTime[deviceKey] = Date.now();
330
+
331
+ // Handle pending deletion - remove device and skip further processing
332
+ if (isPendingDeletion) {
333
+ this.removeDevice(deviceKey);
334
+ msgPos += recordLenLen + recordLen;
335
+ continue;
336
+ }
337
+
338
+ // Check if a device state already exists
339
+ if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
340
+
341
+ // Get the device type info
342
+ const deviceTypeInfo = await this.getDeviceTypeInfo(busNum.toString(), devTypeIdx.toString());
343
+
344
+ // Debug
345
+ // console.log(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} busNum ${busNum} devAddr 0x${devAddr.toString(16)} devTypeIdx ${devTypeIdx} deviceTypeInfo ${JSON.stringify(deviceTypeInfo)}`);
346
+
347
+ // Handle case where device type info is not available
348
+ if (deviceTypeInfo === undefined) {
349
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceType ${devTypeIdx} info not available, skipping attribute processing for this record`);
350
+ // Skip to next record without processing attributes
351
+ msgPos += recordLenLen + recordLen;
352
+ continue;
353
+ }
354
+
355
+ // Check if device record exists
356
+ if (deviceKey in this._devicesState) {
357
+ if (deviceTypeInfo !== undefined) {
358
+ this._devicesState[deviceKey].deviceTypeInfo = deviceTypeInfo;
359
+ this._devicesState[deviceKey].deviceType = deviceTypeInfo.name || "";
360
+ this._devicesState[deviceKey].busName = busNum.toString();
361
+ this._devicesState[deviceKey].deviceAddress = devAddrHex;
362
+ }
363
+ } else {
364
+ // Create device record - device type info may be undefined
365
+ this._devicesState[deviceKey] = {
366
+ deviceTypeInfo: deviceTypeInfo,
367
+ deviceTimeline: {
368
+ timestampsUs: [],
369
+ lastReportTimestampUs: 0,
370
+ reportTimestampOffsetUs: 0
371
+ },
372
+ deviceAttributes: {},
373
+ deviceIsNew: true,
374
+ stateChanged: false,
375
+ onlineState: DeviceOnlineState.Online,
376
+ deviceAddress: devAddrHex,
377
+ deviceType: deviceTypeInfo?.name || "",
378
+ busName: busNum.toString()
379
+ };
380
+ }
381
+ }
382
+
383
+ // Get device state
384
+ const deviceState = this._devicesState[deviceKey];
385
+ deviceState.onlineState = isOnline ? DeviceOnlineState.Online : DeviceOnlineState.Offline;
386
+
387
+ // Check if device type info is available and complete
388
+ if (deviceState.deviceTypeInfo && deviceState.deviceTypeInfo.resp) {
389
+
390
+ // Iterate over attributes in the group
391
+ const pollRespMetadata = deviceState.deviceTypeInfo!.resp!;
392
+
393
+ // Process poll data (recordLen - recordHeaderLen bytes)
394
+ const pollDataLen = recordLen - recordHeaderLen;
395
+ const pollDataStartPos = pollDataPos;
396
+ const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
397
+ const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
398
+ while (pollDataPos < pollDataStartPos + pollDataLen) {
399
+
400
+ // Add bounds checking
401
+ if (pollDataPos >= rxMsg.length) {
402
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} exceeds message length ${rxMsg.length}`);
403
+ break;
404
+ }
405
+
406
+ const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(rxMsg, pollDataPos,
407
+ deviceState.deviceTimeline, pollRespMetadata,
408
+ deviceState.deviceAttributes,
409
+ this._maxDatapointsToStore);
410
+
411
+ // console.log(`DevMan.handleClientMsgBinary decoded debugIdx ${debugMsgIndex} devType ${deviceState.deviceTypeInfo.name} pollDataLen ${pollDataLen} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen} pollRespMetadata ${JSON.stringify(pollRespMetadata)}`);
412
+
413
+ if (newMsgBufIdx < 0)
414
+ {
415
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup failed newMsgBufIdx ${newMsgBufIdx}`);
416
+ break;
417
+ }
418
+
419
+ // Prevent infinite loops
420
+ if (newMsgBufIdx <= pollDataPos) {
421
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} processMsgAttrGroup didn't advance position from ${pollDataPos} to ${newMsgBufIdx}`);
422
+ break;
423
+ }
424
+
425
+ pollDataPos = newMsgBufIdx;
426
+ deviceState.stateChanged = true;
427
+
428
+ // console.log(`debugMsgTime ${debugMsgTime} newPt debugMsgIdx ${debugMsgIndex} rxMsgLen ${rxMsg.length} devType ${deviceState.deviceTypeInfo!.name} timestampsUs ${deviceState.deviceTimeline.timestampsUs[deviceState.deviceTimeline.timestampsUs.length - 1]} curTimelineLen ${deviceState.deviceTimeline.timestampsUs.length}`);
429
+
430
+ // console.log(`DevMan.handleClientMsgBinary group done debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
431
+ }
432
+
433
+ // Inform decoded-data callbacks
434
+ this.emitDecodedData(deviceKey, busNum.toString(), devAddrHex, deviceState,
435
+ pollRespMetadata, attrLengthsBefore, timelineLenBefore);
436
+
437
+ const newSamples = deviceState.deviceTimeline.timestampsUs.length - timelineLenBefore;
438
+ this.updateDeviceStats(deviceKey, newSamples, Date.now());
439
+ } else {
440
+ console.warn(`DevMan.handleClientMsgBinary debugIdx ${debugMsgIndex} deviceState incomplete for device ${deviceKey}, skipping attribute processing`);
441
+ }
442
+
443
+ // Debug
444
+ // console.log(`DevMan.handleClientMsgBinary record done debugIdx ${debugMsgIndex} pollDataPos ${pollDataPos} recordLen ${recordLen} msgPos ${msgPos} newMsgPos ${msgPos + recordLenLen + recordLen} rxMsgLen ${rxMsg.length} remainingLen ${remainingLen}`);
445
+
446
+ // Advance past this record (recordLenLen + recordLen bytes)
447
+ msgPos += recordLenLen + recordLen;
448
+ }
449
+
450
+ // Check for devices that have not been updated for a while
451
+ if (this._removeDevicesFlag) {
452
+ const nowTime = Date.now();
453
+ Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
454
+ if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
455
+ delete this._devicesState[deviceKey];
456
+ delete this._deviceStats[deviceKey];
457
+ }
458
+ });
459
+ }
460
+
461
+ // Process the callback
462
+ this.processStateCallback();
463
+ }
464
+
465
+ ////////////////////////////////////////////////////////////////////////////
466
+ // Handle device message JSON
467
+ ////////////////////////////////////////////////////////////////////////////
468
+
469
+ public async handleClientMsgJson(jsonMsg: string) {
470
+
471
+ const data = JSON.parse(jsonMsg) as DeviceMsgJson;
472
+ // console.log(`DeviceManager client msg ${JSON.stringify(data)}`);
473
+
474
+ // Iterate over the buses
475
+ Object.entries(data).forEach(([busName, devices]) => {
476
+
477
+ // Check the bus name doesn't start with _ which is reserved for non-device information such as topic name
478
+ if (busName.startsWith("_")) {
479
+ return;
480
+ }
481
+
482
+ // Check for bus status info
483
+ if (devices && typeof devices === "object" && "_s" in devices) {
484
+ // console.log(`DeviceManager bus status ${JSON.stringify(devices._s)}`);
485
+ return;
486
+ }
487
+
488
+ // Iterate over the devices
489
+ Object.entries(devices).forEach(async ([devAddr, attrGroups]) => {
490
+
491
+ // Check for non-device info (starts with _)
492
+ if (devAddr.startsWith("_")) {
493
+ return;
494
+ }
495
+
496
+ // Device type name
497
+ let deviceTypeName = "";
498
+ let deviceTypeIdx = -1;
499
+ if (attrGroups && typeof attrGroups === 'object' && "_t" in attrGroups && typeof attrGroups._t === "string") {
500
+ deviceTypeName = attrGroups._t || "";
501
+ } else if (attrGroups && typeof attrGroups === 'object' && "_i" in attrGroups && typeof attrGroups._i === "number") {
502
+ deviceTypeIdx = attrGroups._i ?? -1;
503
+ deviceTypeName = deviceTypeIdx.toString();
504
+ } else
505
+ {
506
+ console.warn(`DeviceManager missing device type attrGroups ${JSON.stringify(attrGroups)}`);
507
+ return;
508
+ }
509
+
510
+ // Device key
511
+ const deviceKey = getDeviceKey(busName, devAddr);
512
+
513
+ // Update the last update time
514
+ this._deviceLastUpdateTime[deviceKey] = Date.now();
515
+
516
+ // Check if a device state already exists
517
+ if (!(deviceKey in this._devicesState) || (this._devicesState[deviceKey].deviceTypeInfo === undefined)) {
518
+
519
+ // Get the device type info
520
+ const deviceTypeInfo = await this.getDeviceTypeInfo(busName, deviceTypeName);
521
+
522
+ // Check if device record exists
523
+ if (deviceKey in this._devicesState) {
524
+ if (deviceTypeInfo !== undefined) {
525
+ this._devicesState[deviceKey].deviceTypeInfo = deviceTypeInfo;
526
+ this._devicesState[deviceKey].deviceType = deviceTypeName;
527
+ this._devicesState[deviceKey].deviceAddress = devAddr;
528
+ this._devicesState[deviceKey].busName = busName;
529
+ }
530
+ } else {
531
+ // Create device record - device type info may be undefined
532
+ this._devicesState[deviceKey] = {
533
+ deviceTypeInfo: deviceTypeInfo,
534
+ deviceTimeline: {
535
+ timestampsUs: [],
536
+ lastReportTimestampUs: 0,
537
+ reportTimestampOffsetUs: 0
538
+ },
539
+ deviceAttributes: {},
540
+ deviceIsNew: true,
541
+ stateChanged: false,
542
+ onlineState: DeviceOnlineState.Online,
543
+ deviceAddress: devAddr,
544
+ deviceType: deviceTypeName,
545
+ busName: busName
546
+ };
547
+ }
548
+ }
549
+
550
+ // Get device state
551
+ const deviceState = this._devicesState[deviceKey];
552
+
553
+ // Check for online/offline/pending-deletion state information
554
+ if (attrGroups && typeof attrGroups === "object" && "_o" in attrGroups) {
555
+ const onlineStateVal = typeof attrGroups._o === 'number' ? attrGroups._o : parseInt(String(attrGroups._o), 10);
556
+ if (onlineStateVal === 2) {
557
+ // Pending deletion - remove device and skip further processing
558
+ this.removeDevice(deviceKey);
559
+ return;
560
+ }
561
+ deviceState.onlineState = onlineStateVal === 1 ? DeviceOnlineState.Online : DeviceOnlineState.Offline;
562
+ }
563
+
564
+ // Check if device type info is available
565
+ if (!deviceState.deviceTypeInfo) {
566
+ return;
567
+ }
568
+
569
+ const markers = this.extractMarkers(attrGroups);
570
+
571
+ // Iterate attribute groups
572
+ Object.entries(attrGroups).forEach(([attrGroupName, msgHexStr]) => {
573
+
574
+ // Check valid
575
+ if (attrGroupName.startsWith("_") || (typeof msgHexStr != 'string')) {
576
+ return;
577
+ }
578
+
579
+ // Check the device type info
580
+ if (!deviceState.deviceTypeInfo!.resp) {
581
+ return;
582
+ }
583
+
584
+ // Convert the hex string to an arraybuffer by converting each pair of hex chars to a byte
585
+ const msgBytes = this.hexToBytes(msgHexStr);
586
+
587
+ // Work through the message which may contain multiple data instances
588
+ let msgBufIdx = 0;
589
+
590
+ // Iterate over attributes in the group
591
+ const pollRespMetadata = deviceState.deviceTypeInfo!.resp!;
592
+
593
+ const attrLengthsBefore = this.snapshotAttrLengths(deviceState.deviceAttributes, pollRespMetadata);
594
+ const timelineLenBefore = deviceState.deviceTimeline.timestampsUs.length;
595
+
596
+ // Loop
597
+ while (msgBufIdx < msgBytes.length) {
598
+
599
+ const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(msgBytes, msgBufIdx,
600
+ deviceState.deviceTimeline, pollRespMetadata,
601
+ deviceState.deviceAttributes,
602
+ this._maxDatapointsToStore);
603
+ if (newMsgBufIdx < 0)
604
+ break;
605
+ msgBufIdx = newMsgBufIdx;
606
+ deviceState.stateChanged = true;
607
+ }
608
+
609
+ this.emitDecodedData(deviceKey, busName, devAddr, deviceState, pollRespMetadata,
610
+ attrLengthsBefore, timelineLenBefore, attrGroupName, markers);
611
+
612
+ const newSamples = deviceState.deviceTimeline.timestampsUs.length - timelineLenBefore;
613
+ this.updateDeviceStats(deviceKey, newSamples, Date.now());
614
+ });
615
+ });
616
+ });
617
+
618
+ // Check for devices that have not been updated for a while
619
+ if (this._removeDevicesFlag) {
620
+ const nowTime = Date.now();
621
+ Object.entries(this._deviceLastUpdateTime).forEach(([deviceKey, lastUpdateTime]) => {
622
+ if ((nowTime - lastUpdateTime) > this._removeDevicesTimeMs) {
623
+ delete this._devicesState[deviceKey];
624
+ delete this._deviceStats[deviceKey];
625
+ }
626
+ });
627
+ }
628
+
629
+ // Process the callback
630
+ this.processStateCallback();
631
+ }
632
+
633
+ ////////////////////////////////////////////////////////////////////////////
634
+ // Process state change callback
635
+ ////////////////////////////////////////////////////////////////////////////
636
+
637
+ private processStateCallback() {
638
+
639
+ // Iterate over the devices
640
+ Object.entries(this._devicesState).forEach(([deviceKey, deviceState]) => {
641
+
642
+ // Check if device record is new
643
+ if (deviceState.deviceIsNew) {
644
+ this._newDeviceCallbacks.forEach((cb) => cb(deviceKey, deviceState));
645
+ deviceState.deviceIsNew = false;
646
+ }
647
+
648
+ // Iterate over the attributes
649
+ Object.entries(deviceState.deviceAttributes).forEach(([, attrState]): void => {
650
+ if (attrState.newAttribute) {
651
+ this._newDeviceAttributeCallbacks.forEach((cb) => cb(deviceKey, attrState));
652
+ attrState.newAttribute = false;
653
+ }
654
+ if (attrState.newData) {
655
+ this._newAttributeDataCallbacks.forEach((cb) => cb(deviceKey, attrState));
656
+ attrState.newData = false;
657
+ }
658
+ });
659
+ });
660
+ }
661
+
662
+ ////////////////////////////////////////////////////////////////////////////
663
+ // Remove a device (e.g. on pending deletion)
664
+ ////////////////////////////////////////////////////////////////////////////
665
+
666
+ private removeDevice(deviceKey: string): void {
667
+ // Snapshot the state before removal for callbacks
668
+ const deviceState = this._devicesState[deviceKey];
669
+ if (deviceState) {
670
+ deviceState.onlineState = DeviceOnlineState.PendingDeletion;
671
+ this._deviceRemovedCallbacks.forEach((cb) => cb(deviceKey, deviceState));
672
+ }
673
+ delete this._devicesState[deviceKey];
674
+ delete this._deviceLastUpdateTime[deviceKey];
675
+ delete this._deviceStats[deviceKey];
676
+ }
677
+
678
+ ////////////////////////////////////////////////////////////////////////////
679
+ // Get device type info
680
+ ////////////////////////////////////////////////////////////////////////////
681
+
682
+ private async getDeviceTypeInfo(busName: string, deviceType: string): Promise<DeviceTypeInfo | undefined> {
683
+ // Check if already in cache
684
+ if (deviceType in this._cachedDeviceTypeRecs) {
685
+ return this._cachedDeviceTypeRecs[deviceType];
686
+ }
687
+
688
+ // Check if there's already a pending request for this device type
689
+ if (deviceType in this._pendingDeviceTypeRequests) {
690
+ // console.log(`DevMan.getDeviceTypeInfo joining existing request queue for deviceType ${deviceType}`);
691
+
692
+ // Add this request to the waiting queue
693
+ return new Promise<DeviceTypeInfo | undefined>((resolve, reject) => {
694
+ this._pendingDeviceTypeRequests[deviceType].waitingQueue.push({ resolve, reject });
695
+ });
696
+ }
697
+
698
+ // Check rate limiting for new requests
699
+ if (deviceType in this._cachedDeviceTypePreviousAttemptTimes) {
700
+ const timeSinceLastAttempt = Date.now() - this._cachedDeviceTypePreviousAttemptTimes[deviceType];
701
+ if (timeSinceLastAttempt < this._minTimeBetweenDeviceTypeInfoRetrievalMs) {
702
+ console.log(`DevMan.getDeviceTypeInfo rate limited for deviceType ${deviceType}`);
703
+ return undefined;
704
+ }
705
+ }
706
+
707
+ // Create and cache the promise with an empty waiting queue
708
+ const requestPromise = this.executeDeviceTypeInfoRequest(busName, deviceType);
709
+ this._pendingDeviceTypeRequests[deviceType] = {
710
+ promise: requestPromise,
711
+ waitingQueue: []
712
+ };
713
+
714
+ try {
715
+ const result = await requestPromise;
716
+
717
+ // Resolve all waiting requests with the same result
718
+ const waitingQueue = this._pendingDeviceTypeRequests[deviceType].waitingQueue;
719
+ waitingQueue.forEach(({ resolve }) => resolve(result));
720
+
721
+ return result;
722
+ } catch (error) {
723
+ // Reject all waiting requests with the same error
724
+ const waitingQueue = this._pendingDeviceTypeRequests[deviceType].waitingQueue;
725
+ waitingQueue.forEach(({ reject }) => reject(error));
726
+
727
+ console.warn(`DevMan.getDeviceTypeInfo failed for ${deviceType}: ${error}`);
728
+ return undefined;
729
+ } finally {
730
+ // Clean up the pending request
731
+ delete this._pendingDeviceTypeRequests[deviceType];
732
+ }
733
+ }
734
+
735
+ private async executeDeviceTypeInfoRequest(busName: string, deviceType: string): Promise<DeviceTypeInfo | undefined> {
736
+ this._cachedDeviceTypePreviousAttemptTimes[deviceType] = Date.now();
737
+
738
+ try {
739
+ const cmd = "devman/typeinfo?bus=" + busName + "&type=" + deviceType;
740
+ const msgHandler = this._systemUtils?.getMsgHandler();
741
+
742
+ if (msgHandler) {
743
+ const msgRslt = await msgHandler.sendRICRESTURL<RaftDevTypeInfoResponse>(cmd);
744
+ if (msgRslt && msgRslt.rslt === "ok") {
745
+ this._cachedDeviceTypeRecs[deviceType] = msgRslt.devinfo;
746
+ return msgRslt.devinfo;
747
+ }
748
+ }
749
+ return undefined;
750
+ } catch (error) {
751
+ console.warn(`DeviceManager getDeviceTypeInfo error ${error}`);
752
+ return undefined;
753
+ }
754
+ }
755
+
756
+ ////////////////////////////////////////////////////////////////////////////
757
+ // Send action to device
758
+ ////////////////////////////////////////////////////////////////////////////
759
+
760
+ private toHex(data: Uint8Array): string {
761
+ return Array.from(data)
762
+ .map(byte => byte.toString(16).padStart(2, "0"))
763
+ .join("");
764
+ }
765
+
766
+ public async sendAction(deviceKey: string, action: DeviceTypeAction, data: number[]): Promise<boolean> {
767
+ // console.log(`DeviceManager sendAction ${deviceKey} action name ${action.n} value ${value} prefix ${action.w}`);
768
+
769
+ let writeBytes: Uint8Array;
770
+
771
+ // Check for one data item
772
+ if (data.length === 1) {
773
+
774
+ let value = data[0];
775
+
776
+ // Check for conversion
777
+ if (action.sub !== undefined) {
778
+ value = value - action.sub;
779
+ }
780
+ if (action.mul !== undefined) {
781
+ value = value * action.mul;
782
+ }
783
+
784
+ // Form the write bytes
785
+ writeBytes = action.t ? structPack(action.t, [value]) : new Uint8Array(0);
786
+
787
+ } else
788
+ {
789
+
790
+ // Form the write bytes which may have multiple data items
791
+ writeBytes = action.t ? structPack(action.t, data) : new Uint8Array(0);
792
+ }
793
+
794
+ // Convert to hex string
795
+ let writeHexStr = this.toHex(writeBytes);
796
+
797
+ // Add prefix and postfix
798
+ writeHexStr = (action.w ? action.w : "") + writeHexStr + (action.wz ? action.wz : "");
799
+
800
+ // Parse the device key into bus and address components
801
+ const { bus: devBus, addr: devAddr } = parseDeviceKey(deviceKey);
802
+
803
+ // Send the action to the server
804
+ const cmd = "devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + writeHexStr;
805
+
806
+ console.log(`DeviceManager deviceKey ${deviceKey} action name ${action.n} value ${data} prefix ${action.w} sendAction ${cmd}`);
807
+
808
+ // Send the command
809
+ try {
810
+
811
+ // Get the msg handler
812
+ const msgHandler = this._systemUtils?.getMsgHandler();
813
+ if (msgHandler) {
814
+ const msgRslt = await msgHandler.sendRICRESTURL<RaftOKFail>(cmd);
815
+ return msgRslt.rslt === "ok";
816
+ }
817
+ return false;
818
+ } catch (error) {
819
+ console.warn(`DeviceManager sendAction error ${error}`);
820
+ return false;
821
+ }
822
+ }
823
+
824
+ ////////////////////////////////////////////////////////////////////////////
825
+ // Send a compound action to the device
826
+ ////////////////////////////////////////////////////////////////////////////
827
+
828
+ public async sendCompoundAction(deviceKey: string, action: DeviceTypeAction, data: number[][]): Promise<boolean> {
829
+ // console.log(`DeviceManager sendAction ${deviceKey} action name ${action.n} value ${value} prefix ${action.w}`);
830
+
831
+ // Check if all data to be sent at once
832
+ if (action.concat) {
833
+ // Form a single list by flattening data
834
+ let dataToWrite: number[] = [];
835
+ for (let dataIdx = 0; dataIdx < data.length; dataIdx++) {
836
+ dataToWrite = dataToWrite.concat(data[dataIdx]);
837
+ }
838
+
839
+ // Use sendAction to send this
840
+ return await this.sendAction(deviceKey, action, dataToWrite);
841
+ } else {
842
+ // Iterate over the data
843
+ let allOk = true;
844
+ for (let dataIdx = 0; dataIdx < data.length; dataIdx++) {
845
+
846
+ // Create the data to write by prepending the index to the data for this index
847
+ const dataToWrite = [dataIdx as number].concat(data[dataIdx]);
848
+
849
+ // Use sendAction to send this
850
+ allOk = allOk && await this.sendAction(deviceKey, action, dataToWrite);
851
+ }
852
+ }
853
+ return false;
854
+ }
855
+
856
+ ////////////////////////////////////////////////////////////////////////////
857
+ // Convert hex to bytes
858
+ ////////////////////////////////////////////////////////////////////////////
859
+
860
+ private hexToBytes(hex: string): Uint8Array {
861
+ const bytes = new Uint8Array(hex.length / 2);
862
+ for (let i = 0; i < bytes.length; i++) {
863
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
864
+ }
865
+ return bytes;
866
+ }
867
+
868
+ ////////////////////////////////////////////////////////////////////////////
869
+ // Helpers for device stats
870
+ ////////////////////////////////////////////////////////////////////////////
871
+
872
+ private createEmptyStats(): DeviceStatsInternal {
873
+ return {
874
+ totalSamples: 0,
875
+ windowMs: this._statsWindowMs,
876
+ windowSamples: 0,
877
+ sampleRateHz: 0,
878
+ lastSampleTimeMs: null,
879
+ lastUpdateTimeMs: null,
880
+ windowEvents: []
881
+ };
882
+ }
883
+
884
+ private getOrCreateDeviceStats(deviceKey: string): DeviceStatsInternal {
885
+ if (!this._deviceStats[deviceKey]) {
886
+ this._deviceStats[deviceKey] = this.createEmptyStats();
887
+ }
888
+ return this._deviceStats[deviceKey];
889
+ }
890
+
891
+ private cloneDeviceStats(stats: DeviceStatsInternal): DeviceStats {
892
+ return {
893
+ totalSamples: stats.totalSamples,
894
+ windowMs: stats.windowMs,
895
+ windowSamples: stats.windowSamples,
896
+ sampleRateHz: stats.sampleRateHz,
897
+ lastSampleTimeMs: stats.lastSampleTimeMs,
898
+ lastUpdateTimeMs: stats.lastUpdateTimeMs
899
+ };
900
+ }
901
+
902
+ private updateDeviceStats(deviceKey: string, newSamples: number, nowMs: number): void {
903
+ const stats = this.getOrCreateDeviceStats(deviceKey);
904
+ stats.lastUpdateTimeMs = nowMs;
905
+
906
+ if (newSamples > 0) {
907
+ stats.totalSamples += newSamples;
908
+ stats.lastSampleTimeMs = nowMs;
909
+ stats.windowEvents.push({ timeMs: nowMs, samples: newSamples });
910
+ }
911
+
912
+ const windowStartMs = nowMs - stats.windowMs;
913
+ while (stats.windowEvents.length > 0 && stats.windowEvents[0].timeMs < windowStartMs) {
914
+ stats.windowEvents.shift();
915
+ }
916
+
917
+ const windowSamples = stats.windowEvents.reduce((sum, entry) => sum + entry.samples, 0);
918
+ stats.windowSamples = windowSamples;
919
+ if (stats.windowEvents.length === 0) {
920
+ stats.sampleRateHz = 0;
921
+ return;
922
+ }
923
+
924
+ const actualWindowMs = Math.max(1, nowMs - stats.windowEvents[0].timeMs);
925
+ stats.sampleRateHz = (windowSamples * 1000) / actualWindowMs;
926
+ }
927
+
928
+ ////////////////////////////////////////////////////////////////////////////
929
+ // Helpers for decoded data callbacks
930
+ ////////////////////////////////////////////////////////////////////////////
931
+
932
+ private snapshotAttrLengths(deviceAttrs: DeviceAttributesState, pollRespMetadata: DeviceTypeInfo["resp"]): Record<string, number> {
933
+ const lengths: Record<string, number> = {};
934
+ if (!pollRespMetadata) {
935
+ return lengths;
936
+ }
937
+ pollRespMetadata.a.forEach((attr) => {
938
+ lengths[attr.n] = deviceAttrs[attr.n]?.values.length ?? 0;
939
+ });
940
+ return lengths;
941
+ }
942
+
943
+ private emitDecodedData(
944
+ deviceKey: string,
945
+ busName: string,
946
+ devAddr: string,
947
+ deviceState: DeviceState,
948
+ pollRespMetadata: DeviceTypeInfo["resp"],
949
+ attrLengthsBefore: Record<string, number>,
950
+ timelineLenBefore: number,
951
+ attrGroupName = "",
952
+ markers?: Record<string, unknown>,
953
+ ): void {
954
+
955
+ if (!pollRespMetadata) {
956
+ return;
957
+ }
958
+
959
+ const attrValues: Record<string, number[]> = {};
960
+ let hasValues = false;
961
+
962
+ pollRespMetadata.a.forEach((attr) => {
963
+ const attrState = deviceState.deviceAttributes[attr.n];
964
+ if (!attrState) {
965
+ return;
966
+ }
967
+ const prevLen = attrLengthsBefore[attr.n] ?? 0;
968
+ if (attrState.values.length > prevLen) {
969
+ attrValues[attr.n] = attrState.values.slice(prevLen);
970
+ hasValues = hasValues || attrValues[attr.n].length > 0;
971
+ }
972
+ });
973
+
974
+ if (!hasValues) {
975
+ return;
976
+ }
977
+
978
+ const timestampsUs = deviceState.deviceTimeline.timestampsUs.slice(timelineLenBefore);
979
+
980
+ const decoded: DeviceDecodedData = {
981
+ deviceKey,
982
+ busName,
983
+ deviceAddress: devAddr,
984
+ deviceType: deviceState.deviceType,
985
+ attrGroupName: attrGroupName || undefined,
986
+ attrValues,
987
+ timestampsUs,
988
+ };
989
+
990
+ if (markers && Object.keys(markers).length > 0) {
991
+ decoded.markers = markers;
992
+ decoded.fromOfflineBuffer = this.isTruthy(markers["_buf"]);
993
+ }
994
+
995
+ this._decodedDataCallbacks.forEach((cb) => cb(decoded));
996
+ }
997
+
998
+ private extractMarkers(attrGroups: any): Record<string, unknown> {
999
+ const markers: Record<string, unknown> = {};
1000
+ if (!attrGroups || typeof attrGroups !== "object") {
1001
+ return markers;
1002
+ }
1003
+ Object.entries(attrGroups).forEach(([key, value]) => {
1004
+ if (key.startsWith("_") && key !== "_t" && key !== "_o") {
1005
+ markers[key] = value;
1006
+ }
1007
+ });
1008
+ return markers;
1009
+ }
1010
+
1011
+ private isTruthy(val: unknown): boolean {
1012
+ return val === true || val === 1 || val === "1";
1013
+ }
1014
+ }