@robdobsn/raftjs 1.1.1

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 (149) hide show
  1. package/.editorconfig +14 -0
  2. package/.gitattributes +11 -0
  3. package/.nvmrc +1 -0
  4. package/LICENSE +22 -0
  5. package/README.md +11 -0
  6. package/TODO.md +1 -0
  7. package/dist/RaftAttributeHandler.d.ts +12 -0
  8. package/dist/RaftAttributeHandler.js +241 -0
  9. package/dist/RaftAttributeHandler.js.map +1 -0
  10. package/dist/RaftChannel.d.ts +18 -0
  11. package/dist/RaftChannel.js +12 -0
  12. package/dist/RaftChannel.js.map +1 -0
  13. package/dist/RaftChannelWebBLE.d.ts +38 -0
  14. package/dist/RaftChannelWebBLE.js +274 -0
  15. package/dist/RaftChannelWebBLE.js.map +1 -0
  16. package/dist/RaftChannelWebSerial.d.ts +37 -0
  17. package/dist/RaftChannelWebSerial.js +319 -0
  18. package/dist/RaftChannelWebSerial.js.map +1 -0
  19. package/dist/RaftChannelWebSocket.d.ts +28 -0
  20. package/dist/RaftChannelWebSocket.js +197 -0
  21. package/dist/RaftChannelWebSocket.js.map +1 -0
  22. package/dist/RaftCommsStats.d.ts +39 -0
  23. package/dist/RaftCommsStats.js +128 -0
  24. package/dist/RaftCommsStats.js.map +1 -0
  25. package/dist/RaftConnEvents.d.ts +31 -0
  26. package/dist/RaftConnEvents.js +44 -0
  27. package/dist/RaftConnEvents.js.map +1 -0
  28. package/dist/RaftConnector.d.ts +242 -0
  29. package/dist/RaftConnector.js +613 -0
  30. package/dist/RaftConnector.js.map +1 -0
  31. package/dist/RaftCustomAttrHandler.d.ts +4 -0
  32. package/dist/RaftCustomAttrHandler.js +50 -0
  33. package/dist/RaftCustomAttrHandler.js.map +1 -0
  34. package/dist/RaftDeviceInfo.d.ts +64 -0
  35. package/dist/RaftDeviceInfo.js +36 -0
  36. package/dist/RaftDeviceInfo.js.map +1 -0
  37. package/dist/RaftDeviceManager.d.ts +37 -0
  38. package/dist/RaftDeviceManager.js +450 -0
  39. package/dist/RaftDeviceManager.js.map +1 -0
  40. package/dist/RaftDeviceMsg.d.ts +9 -0
  41. package/dist/RaftDeviceMsg.js +11 -0
  42. package/dist/RaftDeviceMsg.js.map +1 -0
  43. package/dist/RaftDeviceStates.d.ts +33 -0
  44. package/dist/RaftDeviceStates.js +60 -0
  45. package/dist/RaftDeviceStates.js.map +1 -0
  46. package/dist/RaftFileHandler.d.ts +52 -0
  47. package/dist/RaftFileHandler.js +502 -0
  48. package/dist/RaftFileHandler.js.map +1 -0
  49. package/dist/RaftLog.d.ts +22 -0
  50. package/dist/RaftLog.js +63 -0
  51. package/dist/RaftLog.js.map +1 -0
  52. package/dist/RaftMiniHDLC.d.ts +18 -0
  53. package/dist/RaftMiniHDLC.js +383 -0
  54. package/dist/RaftMiniHDLC.js.map +1 -0
  55. package/dist/RaftMsgHandler.d.ts +57 -0
  56. package/dist/RaftMsgHandler.js +480 -0
  57. package/dist/RaftMsgHandler.js.map +1 -0
  58. package/dist/RaftMsgTrackInfo.d.ts +17 -0
  59. package/dist/RaftMsgTrackInfo.js +42 -0
  60. package/dist/RaftMsgTrackInfo.js.map +1 -0
  61. package/dist/RaftProtocolDefs.d.ts +30 -0
  62. package/dist/RaftProtocolDefs.js +48 -0
  63. package/dist/RaftProtocolDefs.js.map +1 -0
  64. package/dist/RaftStreamHandler.d.ts +38 -0
  65. package/dist/RaftStreamHandler.js +257 -0
  66. package/dist/RaftStreamHandler.js.map +1 -0
  67. package/dist/RaftSystemType.d.ts +21 -0
  68. package/dist/RaftSystemType.js +3 -0
  69. package/dist/RaftSystemType.js.map +1 -0
  70. package/dist/RaftSystemUtils.d.ts +136 -0
  71. package/dist/RaftSystemUtils.js +410 -0
  72. package/dist/RaftSystemUtils.js.map +1 -0
  73. package/dist/RaftTypes.d.ts +184 -0
  74. package/dist/RaftTypes.js +157 -0
  75. package/dist/RaftTypes.js.map +1 -0
  76. package/dist/RaftUpdateEvents.d.ts +33 -0
  77. package/dist/RaftUpdateEvents.js +46 -0
  78. package/dist/RaftUpdateEvents.js.map +1 -0
  79. package/dist/RaftUpdateManager.d.ts +61 -0
  80. package/dist/RaftUpdateManager.js +618 -0
  81. package/dist/RaftUpdateManager.js.map +1 -0
  82. package/dist/RaftUtils.d.ts +125 -0
  83. package/dist/RaftUtils.js +454 -0
  84. package/dist/RaftUtils.js.map +1 -0
  85. package/dist/RaftWifiTypes.d.ts +23 -0
  86. package/dist/RaftWifiTypes.js +43 -0
  87. package/dist/RaftWifiTypes.js.map +1 -0
  88. package/dist/TestDataGen.d.ts +7 -0
  89. package/dist/TestDataGen.js +133 -0
  90. package/dist/TestDataGen.js.map +1 -0
  91. package/dist/main.d.ts +18 -0
  92. package/dist/main.js +42 -0
  93. package/dist/main.js.map +1 -0
  94. package/eslint.config.mjs +33 -0
  95. package/examples/dashboard/package.json +39 -0
  96. package/examples/dashboard/src/ConnManager.ts +86 -0
  97. package/examples/dashboard/src/Main.tsx +100 -0
  98. package/examples/dashboard/src/StatusScreen.tsx +72 -0
  99. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +144 -0
  100. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +77 -0
  101. package/examples/dashboard/src/SystemTypeMarty/RICAddOn.ts +70 -0
  102. package/examples/dashboard/src/SystemTypeMarty/RICAddOnBase.ts +33 -0
  103. package/examples/dashboard/src/SystemTypeMarty/RICAddOnManager.ts +342 -0
  104. package/examples/dashboard/src/SystemTypeMarty/RICCommsStats.ts +170 -0
  105. package/examples/dashboard/src/SystemTypeMarty/RICHWElem.ts +123 -0
  106. package/examples/dashboard/src/SystemTypeMarty/RICLEDPatternChecker.ts +207 -0
  107. package/examples/dashboard/src/SystemTypeMarty/RICROSSerial.ts +464 -0
  108. package/examples/dashboard/src/SystemTypeMarty/RICServoFaultDetector.ts +146 -0
  109. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +32 -0
  110. package/examples/dashboard/src/SystemTypeMarty/RICSystemUtils.ts +371 -0
  111. package/examples/dashboard/src/SystemTypeMarty/RICTypes.ts +20 -0
  112. package/examples/dashboard/src/SystemTypeMarty/SystemTypeMarty.ts +113 -0
  113. package/examples/dashboard/src/index.html +15 -0
  114. package/examples/dashboard/src/index.tsx +15 -0
  115. package/examples/dashboard/src/styles.css +122 -0
  116. package/examples/dashboard/tsconfig.json +18 -0
  117. package/jest.config.js +11 -0
  118. package/package.json +50 -0
  119. package/src/RaftAttributeHandler.ts +289 -0
  120. package/src/RaftChannel.ts +30 -0
  121. package/src/RaftChannelWebBLE.ts +342 -0
  122. package/src/RaftChannelWebSerial.ts +408 -0
  123. package/src/RaftChannelWebSocket.ts +245 -0
  124. package/src/RaftCommsStats.ts +142 -0
  125. package/src/RaftConnEvents.ts +46 -0
  126. package/src/RaftConnector.ts +745 -0
  127. package/src/RaftCustomAttrHandler.ts +54 -0
  128. package/src/RaftDeviceInfo.ts +104 -0
  129. package/src/RaftDeviceManager.ts +542 -0
  130. package/src/RaftDeviceMsg.ts +20 -0
  131. package/src/RaftDeviceStates.ts +89 -0
  132. package/src/RaftFileHandler.ts +668 -0
  133. package/src/RaftLog.ts +70 -0
  134. package/src/RaftMiniHDLC.ts +396 -0
  135. package/src/RaftMsgHandler.ts +778 -0
  136. package/src/RaftMsgTrackInfo.ts +51 -0
  137. package/src/RaftProtocolDefs.ts +46 -0
  138. package/src/RaftStreamHandler.ts +328 -0
  139. package/src/RaftSystemType.ts +25 -0
  140. package/src/RaftSystemUtils.ts +487 -0
  141. package/src/RaftTypes.ts +250 -0
  142. package/src/RaftUpdateEvents.ts +48 -0
  143. package/src/RaftUpdateManager.ts +778 -0
  144. package/src/RaftUtils.ts +484 -0
  145. package/src/RaftWifiTypes.ts +36 -0
  146. package/src/TestDataGen.ts +157 -0
  147. package/src/main.ts +28 -0
  148. package/testdata/TestDeviceTypeRecs.json +492 -0
  149. package/tsconfig.json +27 -0
@@ -0,0 +1,542 @@
1
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // RaftDeviceManager
4
+ // Device manager for Raft devices
5
+ //
6
+ // Rob Dobson (C) 2024
7
+ //
8
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
+
10
+ import { DeviceAttributeState, DevicesState, DeviceState, getDeviceKey } from "./RaftDeviceStates";
11
+ import { DeviceMsgJson } from "./RaftDeviceMsg";
12
+ import { DeviceTypeInfo, DeviceTypeInfoTestJsonFile, DeviceTypeAction, DeviceTypeInfoRecs } from "./RaftDeviceInfo";
13
+ import struct, { DataType } from 'python-struct';
14
+ import TestDataGen from "./TestDataGen";
15
+ import AttributeHandler from "./RaftAttributeHandler";
16
+
17
+ let testingDeviceTypeRecsConditionalLoadPromise: Promise<any> | null = null;
18
+ if (process.env.TEST_DATA) {
19
+ testingDeviceTypeRecsConditionalLoadPromise = import('../testdata/TestDeviceTypeRecs.json');
20
+ }
21
+
22
+ export class DeviceManager {
23
+
24
+ // Singleton
25
+ private static _instance: DeviceManager;
26
+
27
+ // Max data points to store
28
+ private _maxDatapointsToStore = 1000;
29
+
30
+ // Attribute handler
31
+ private _attributeHandler = new AttributeHandler();
32
+
33
+ // Server address
34
+ private _serverAddressPrefix = "";
35
+
36
+ // URL prefix
37
+ private _urlPrefix: string = "/api";
38
+
39
+ // Devices state
40
+ private _devicesState = new DevicesState();
41
+
42
+ // Device callbacks
43
+ private _callbackNewDevice: ((deviceKey: string, state: DeviceState) => void) | null = null;
44
+ private _callbackNewDeviceAttribute: ((deviceKey: string, attrState: DeviceAttributeState) => void) | null = null;
45
+ private _callbackNewAttributeData: ((deviceKey: string, attrState: DeviceAttributeState) => void) | null = null;
46
+
47
+ // Last time we got a state update
48
+ private _lastStateUpdate: number = 0;
49
+ private MAX_TIME_BETWEEN_STATE_UPDATES_MS: number = 60000;
50
+
51
+ // Websocket
52
+ private _websocket: WebSocket | null = null;
53
+
54
+ // Get instance
55
+ public static getInstance(): DeviceManager {
56
+ if (!DeviceManager._instance) {
57
+ DeviceManager._instance = new DeviceManager();
58
+ }
59
+ return DeviceManager._instance;
60
+ }
61
+
62
+ public getDevicesState(): DevicesState {
63
+ return this._devicesState;
64
+ }
65
+
66
+ public getDeviceState(deviceKey: string): DeviceState {
67
+ return this._devicesState[deviceKey];
68
+ }
69
+
70
+ // Cached device type data
71
+ private _cachedDeviceTypeRecs: DeviceTypeInfoRecs = {};
72
+
73
+ // Test device type data
74
+ private _testDeviceTypeRecs: DeviceTypeInfoTestJsonFile | null = null;
75
+ private _testDataGen = new TestDataGen();
76
+
77
+ // Constructor
78
+ private constructor() {
79
+ // Check if test mode
80
+ // if (window.location.hostname === "localhost") {
81
+ if (process.env.TEST_DATA) {
82
+ this._testDataGen.start((msg: string) => {
83
+ this.handleClientMsgJson(msg);
84
+ });
85
+ }
86
+ }
87
+
88
+ ////////////////////////////////////////////////////////////////////////////
89
+ // Send REST commands
90
+ ////////////////////////////////////////////////////////////////////////////
91
+
92
+ async sendCommand(cmd: string): Promise<boolean> {
93
+ try {
94
+ const sendCommandResponse = await fetch(this._serverAddressPrefix + this._urlPrefix + cmd);
95
+ if (!sendCommandResponse.ok) {
96
+ console.warn(`DeviceManager sendCommand response not ok ${sendCommandResponse.status}`);
97
+ }
98
+ return sendCommandResponse.ok;
99
+ } catch (error) {
100
+ console.warn(`DeviceManager sendCommand error ${error}`);
101
+ return false;
102
+ }
103
+ }
104
+
105
+ ////////////////////////////////////////////////////////////////////////////
106
+ // Init
107
+ ////////////////////////////////////////////////////////////////////////////
108
+
109
+ public async init(): Promise<boolean> {
110
+ // Check if already initialized
111
+ if (this._websocket) {
112
+ console.warn(`DeviceManager init already initialized`)
113
+ return true;
114
+ }
115
+ // console.log(`DeviceManager init - first time`)
116
+
117
+ // Conditionally load the device type records
118
+ if (testingDeviceTypeRecsConditionalLoadPromise) {
119
+ testingDeviceTypeRecsConditionalLoadPromise.then((jsonData) => {
120
+ this._testDeviceTypeRecs = jsonData as DeviceTypeInfoTestJsonFile;
121
+ });
122
+ }
123
+
124
+ // Websocket if not in test mode
125
+ if (!process.env.TEST_DATA) {
126
+ // Open websocket
127
+ const rslt = await this.connectWebSocket();
128
+
129
+ // Start timer to check for websocket reconnection
130
+ setInterval(async () => {
131
+ if (!this._websocket) {
132
+ console.log(`DeviceManager init - reconnecting websocket`);
133
+ await this.connectWebSocket();
134
+ }
135
+ else if ((Date.now() - this._lastStateUpdate) > this.MAX_TIME_BETWEEN_STATE_UPDATES_MS) {
136
+ const inactiveTimeSecs = ((Date.now() - this._lastStateUpdate) / 1000).toFixed(1);
137
+ if (this._websocket) {
138
+ console.log(`DeviceManager init - closing websocket due to ${inactiveTimeSecs}s inactivity`);
139
+ this._websocket.close();
140
+ this._websocket = null;
141
+ }
142
+ }
143
+ console.log(`websocket state ${this._websocket?.readyState}`);
144
+ }, 5000);
145
+ return rslt;
146
+ }
147
+
148
+ // Test mode
149
+ return true;
150
+ }
151
+
152
+ ////////////////////////////////////////////////////////////////////////////
153
+ // Open websocket
154
+ ////////////////////////////////////////////////////////////////////////////
155
+
156
+ private async connectWebSocket(): Promise<boolean> {
157
+ // Open a websocket to the server
158
+ try {
159
+ console.log(`DeviceManager init location.origin ${window.location.origin} ${window.location.protocol} ${window.location.host} ${window.location.hostname} ${window.location.port} ${window.location.pathname} ${window.location.search} ${window.location.hash}`)
160
+ let webSocketURL = this._serverAddressPrefix;
161
+ if (webSocketURL.startsWith("http")) {
162
+ webSocketURL = webSocketURL.replace(/^http/, 'ws');
163
+ } else {
164
+ webSocketURL = window.location.origin.replace(/^http/, 'ws');
165
+ }
166
+ webSocketURL += "/devjson";
167
+ console.log(`DeviceManager init opening websocket ${webSocketURL}`);
168
+ this._websocket = new WebSocket(webSocketURL);
169
+ if (!this._websocket) {
170
+ console.error("DeviceManager init unable to create websocket");
171
+ return false;
172
+ }
173
+ this._websocket.binaryType = "arraybuffer";
174
+ this._lastStateUpdate = Date.now();
175
+ this._websocket.onopen = () => {
176
+ // Debug
177
+ console.log(`DeviceManager init websocket opened to ${webSocketURL}`);
178
+
179
+ // Send subscription request messages after a short delay
180
+ setTimeout(() => {
181
+
182
+ // Subscribe to device messages
183
+ const subscribeName = "devices";
184
+ console.log(`DeviceManager init subscribing to ${subscribeName}`);
185
+ if (this._websocket) {
186
+ this._websocket.send(JSON.stringify({
187
+ cmdName: "subscription",
188
+ action: "update",
189
+ pubRecs: [
190
+ {name: subscribeName, msgID: subscribeName, rateHz: 0.1},
191
+ ]
192
+ }));
193
+ }
194
+ }, 1000);
195
+ }
196
+ this._websocket.onmessage = (event) => {
197
+ this.handleClientMsgJson(event.data);
198
+ }
199
+ this._websocket.onclose = () => {
200
+ console.log(`DeviceManager websocket closed`);
201
+ this._websocket = null;
202
+ }
203
+ this._websocket.onerror = (error) => {
204
+ console.warn(`DeviceManager websocket error ${error}`);
205
+ }
206
+ }
207
+ catch (error) {
208
+ console.warn(`DeviceManager websocket error ${error}`);
209
+ return false;
210
+ }
211
+ return true;
212
+ }
213
+
214
+ ////////////////////////////////////////////////////////////////////////////
215
+ // Callbacks
216
+ ////////////////////////////////////////////////////////////////////////////
217
+
218
+ // Register state change callbacks
219
+ public onNewDevice(callback: (deviceKey: string, state: DeviceState) => void): void {
220
+ // Save the callback
221
+ this._callbackNewDevice = callback;
222
+ }
223
+ public onNewDeviceAttribute(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void {
224
+ // Save the callback
225
+ this._callbackNewDeviceAttribute = callback;
226
+ }
227
+ public onNewAttributeData(callback: (deviceKey: string, attrState: DeviceAttributeState) => void): void {
228
+ // Save the callback
229
+ this._callbackNewAttributeData = callback;
230
+ }
231
+
232
+ ////////////////////////////////////////////////////////////////////////////
233
+ // Set the friendly name for the device
234
+ ////////////////////////////////////////////////////////////////////////////
235
+
236
+ public async setFriendlyName(friendlyName:string): Promise<void> {
237
+ try {
238
+ await fetch(this._serverAddressPrefix + this._urlPrefix + "/friendlyname/" + friendlyName);
239
+ } catch (error) {
240
+ console.log(`DeviceManager setFriendlyName ${error}`);
241
+ }
242
+ }
243
+
244
+ ////////////////////////////////////////////////////////////////////////////
245
+ // Handle device message JSON
246
+ ////////////////////////////////////////////////////////////////////////////
247
+
248
+ private handleClientMsgJson(jsonMsg: string) {
249
+
250
+ const removeDevicesNoLongerPresent = true;
251
+
252
+ let data = JSON.parse(jsonMsg) as DeviceMsgJson;
253
+ // console.log(`DeviceManager websocket message ${JSON.stringify(data)}`);
254
+
255
+ // Iterate over the buses
256
+ Object.entries(data).forEach(([busName, devices]) => {
257
+
258
+ // Check for bus status info
259
+ if (devices && typeof devices === "object" && "_s" in devices) {
260
+ // console.log(`DeviceManager bus status ${JSON.stringify(devices._s)}`);
261
+ return;
262
+ }
263
+
264
+ // Get a list of keys for the current devicesState
265
+ const deviceKeysToRemove = Object.keys(this._devicesState);
266
+
267
+ // Iterate over the devices
268
+ Object.entries(devices).forEach(async ([devAddr, attrGroups]) => {
269
+
270
+ // Check for non-device info (starts with _)
271
+ if (devAddr.startsWith("_")) {
272
+ return;
273
+ }
274
+
275
+ // Device key
276
+ const deviceKey = getDeviceKey(busName, devAddr);
277
+
278
+ // Remove from the list of keys for the current devicesState
279
+ const idx = deviceKeysToRemove.indexOf(deviceKey);
280
+ if (idx >= 0) {
281
+ deviceKeysToRemove.splice(idx, 1);
282
+ }
283
+
284
+ // Check if a device state already exists
285
+ if (!(deviceKey in this._devicesState)) {
286
+
287
+ let deviceTypeName = "";
288
+ if (attrGroups && typeof attrGroups === 'object' && "_t" in attrGroups && typeof attrGroups._t === "string") {
289
+ deviceTypeName = attrGroups._t || "";
290
+ } else {
291
+ console.warn(`DeviceManager missing device type attrGroups ${JSON.stringify(attrGroups)}`);
292
+ return;
293
+ }
294
+
295
+ // Create device record
296
+ this._devicesState[deviceKey] = {
297
+ deviceTypeInfo: await this.getDeviceTypeInfo(busName, devAddr, deviceTypeName),
298
+ deviceTimeline: {
299
+ timestampsUs: [],
300
+ lastReportTimestampUs: 0,
301
+ reportTimestampOffsetUs: 0
302
+ },
303
+ deviceAttributes: {},
304
+ deviceIsNew: true,
305
+ stateChanged: false,
306
+ isOnline: true
307
+ };
308
+ }
309
+
310
+ // Get device state
311
+ const deviceState = this._devicesState[deviceKey];
312
+
313
+ // Check for online/offline state information
314
+ if (attrGroups && typeof attrGroups === "object" && "_o" in attrGroups) {
315
+ deviceState.isOnline = ((attrGroups._o === true) || (attrGroups._o === "1") || (attrGroups._o === 1));
316
+ }
317
+
318
+ // Iterate attribute groups
319
+ Object.entries(attrGroups).forEach(([attrGroupName, msgHexStr]) => {
320
+
321
+ // Check valid
322
+ if (attrGroupName.startsWith("_") || (typeof msgHexStr != 'string')) {
323
+ return;
324
+ }
325
+
326
+ // Check the device type info
327
+ if (!deviceState.deviceTypeInfo.resp) {
328
+ return;
329
+ }
330
+
331
+ // Convert the hex string to an arraybuffer by converting each pair of hex chars to a byte
332
+ const msgBytes = this.hexToBytes(msgHexStr);
333
+
334
+ // Convert to a Buffer
335
+ const msgBuffer = Buffer.from(msgBytes);
336
+
337
+ // Work through the message which may contain multiple data instances
338
+ let msgBufIdx = 0;
339
+
340
+ // Iterate over attributes in the group
341
+ const pollRespMetadata = deviceState.deviceTypeInfo.resp!;
342
+
343
+ // Loop
344
+ while (msgBufIdx < msgBytes.length) {
345
+
346
+ const curTimelineLen = deviceState.deviceTimeline.timestampsUs.length;
347
+ const newMsgBufIdx = this._attributeHandler.processMsgAttrGroup(msgBuffer, msgBufIdx,
348
+ deviceState.deviceTimeline, pollRespMetadata,
349
+ deviceState.deviceAttributes,
350
+ this._maxDatapointsToStore);
351
+ if (newMsgBufIdx < 0)
352
+ break;
353
+ msgBufIdx = newMsgBufIdx;
354
+ if (deviceState.deviceTimeline.timestampsUs.length !== curTimelineLen) {
355
+ deviceState.stateChanged = true;
356
+ }
357
+ }
358
+ });
359
+ });
360
+
361
+ // Remove devices no longer present
362
+ if (removeDevicesNoLongerPresent) {
363
+ deviceKeysToRemove.forEach((deviceKey) => {
364
+ delete this._devicesState[deviceKey];
365
+ });
366
+ }
367
+
368
+ });
369
+
370
+ // Update the last state update time
371
+ this._lastStateUpdate = Date.now();
372
+
373
+ // Process the callback
374
+ this.processStateCallback();
375
+
376
+ }
377
+
378
+ ////////////////////////////////////////////////////////////////////////////
379
+ // Process state change callback
380
+ ////////////////////////////////////////////////////////////////////////////
381
+
382
+ private processStateCallback() {
383
+
384
+ // Iterate over the devices
385
+ Object.entries(this._devicesState).forEach(([deviceKey, deviceState]) => {
386
+
387
+ // Check if device record is new
388
+ if (deviceState.deviceIsNew) {
389
+ if (this._callbackNewDevice) {
390
+ this._callbackNewDevice(
391
+ deviceKey,
392
+ deviceState
393
+ );
394
+ }
395
+ deviceState.deviceIsNew = false;
396
+ }
397
+
398
+ // Iterate over the attributes
399
+ Object.entries(deviceState.deviceAttributes).forEach(([_attrKey, attrState]) => {
400
+ if (attrState.newAttribute) {
401
+ if (this._callbackNewDeviceAttribute) {
402
+ this._callbackNewDeviceAttribute(
403
+ deviceKey,
404
+ attrState
405
+ );
406
+ }
407
+ attrState.newAttribute = false;
408
+ }
409
+ if (attrState.newData) {
410
+ if (this._callbackNewAttributeData) {
411
+ this._callbackNewAttributeData(
412
+ deviceKey,
413
+ attrState
414
+ );
415
+ }
416
+ attrState.newData = false;
417
+ }
418
+ });
419
+ });
420
+ }
421
+
422
+ ////////////////////////////////////////////////////////////////////////////
423
+ // Get device type info
424
+ ////////////////////////////////////////////////////////////////////////////
425
+
426
+ private async getDeviceTypeInfo(busName: string, _devAddr: string, deviceType: string): Promise<DeviceTypeInfo> {
427
+
428
+ const emptyRec = {
429
+ "name": "Unknown",
430
+ "desc": "Unknown",
431
+ "manu": "Unknown",
432
+ "type": "Unknown"
433
+ };
434
+ // Ensure that this._testDeviceTypeRecs and devTypes[deviceType] are properly initialized
435
+ if (process.env.TEST_DATA) {
436
+ if (this._testDeviceTypeRecs && this._testDeviceTypeRecs.devTypes[deviceType]) {
437
+ return this._testDeviceTypeRecs.devTypes[deviceType].devInfoJson;
438
+ } else {
439
+ // Handle the case where the necessary data isn't available
440
+ console.error("Device type info not available for:", deviceType);
441
+ return emptyRec;
442
+ }
443
+ }
444
+
445
+ // Check if already in the cache
446
+ if (deviceType in this._cachedDeviceTypeRecs) {
447
+ return this._cachedDeviceTypeRecs[deviceType];
448
+ }
449
+
450
+ // Get the device type info from the server
451
+ try {
452
+ const getDevTypeInfoResponse = await fetch(this._serverAddressPrefix + this._urlPrefix + "/devman/typeinfo?bus=" + busName + "&type=" + deviceType);
453
+ if (!getDevTypeInfoResponse.ok) {
454
+ console.error(`DeviceManager getDeviceTypeInfo response not ok ${getDevTypeInfoResponse.status}`);
455
+ return emptyRec;
456
+ }
457
+ const devTypeInfo = await getDevTypeInfoResponse.json();
458
+ if ("devinfo" in devTypeInfo) {
459
+ this._cachedDeviceTypeRecs[deviceType] = devTypeInfo.devinfo;
460
+ return devTypeInfo.devinfo;
461
+ }
462
+ return emptyRec;
463
+ } catch (error) {
464
+ console.error(`DeviceManager getDeviceTypeInfo error ${error}`);
465
+ return emptyRec;
466
+ }
467
+ }
468
+
469
+ ////////////////////////////////////////////////////////////////////////////
470
+ // Send action to device
471
+ ////////////////////////////////////////////////////////////////////////////
472
+
473
+ public sendAction(deviceKey: string, action: DeviceTypeAction, data: DataType[]): void {
474
+ // console.log(`DeviceManager sendAction ${deviceKey} action name ${action.n} value ${value} prefix ${action.w}`);
475
+
476
+ // Form the write bytes
477
+ let writeBytes = action.t ? struct.pack(action.t, data) : Buffer.from([]);
478
+
479
+ // Convert to hex string
480
+ let writeHexStr = Buffer.from(writeBytes).toString('hex');
481
+
482
+ // Add prefix
483
+ writeHexStr = action.w + writeHexStr;
484
+
485
+ // Separate the bus and address in the deviceKey (_ char)
486
+ const devBus = deviceKey.split("_")[0]
487
+ const devAddr = deviceKey.split("_")[1]
488
+
489
+ // Send the action to the server
490
+ const url = this._serverAddressPrefix + this._urlPrefix + "/devman/cmdraw?bus=" + devBus + "&addr=" + devAddr + "&hexWr=" + writeHexStr;
491
+
492
+ console.log(`DeviceManager deviceKey ${deviceKey} action name ${action.n} value ${data} prefix ${action.w} sendAction ${url}`);
493
+ fetch(url)
494
+ .then(response => {
495
+ if (!response.ok) {
496
+ console.error(`DeviceManager sendAction response not ok ${response.status}`);
497
+ }
498
+ })
499
+ .catch(error => {
500
+ console.error(`DeviceManager sendAction error ${error}`);
501
+ });
502
+ }
503
+
504
+ ////////////////////////////////////////////////////////////////////////////
505
+ // Send a compound action to the device
506
+ ////////////////////////////////////////////////////////////////////////////
507
+
508
+ public sendCompoundAction(deviceKey: string, action: DeviceTypeAction, data: DataType[][]): void {
509
+ // console.log(`DeviceManager sendAction ${deviceKey} action name ${action.n} value ${value} prefix ${action.w}`);
510
+
511
+ // Check if all data to be sent at once
512
+ if (action.concat) {
513
+ // Form a single list by flattening data
514
+ let dataToWrite: DataType[] = [];
515
+ for (let dataIdx = 0; dataIdx < data.length; dataIdx++) {
516
+ dataToWrite = dataToWrite.concat(data[dataIdx]);
517
+ }
518
+
519
+ // Use sendAction to send this
520
+ this.sendAction(deviceKey, action, dataToWrite);
521
+ } else {
522
+ // Iterate over the data
523
+ for (let dataIdx = 0; dataIdx < data.length; dataIdx++) {
524
+
525
+ // Create the data to write by prepending the index to the data for this index
526
+ let dataToWrite = [dataIdx as DataType].concat(data[dataIdx]);
527
+
528
+ // Use sendAction to send this
529
+ this.sendAction(deviceKey, action, dataToWrite);
530
+ }
531
+ }
532
+ }
533
+
534
+ private hexToBytes(hex: string): Uint8Array {
535
+ const bytes = new Uint8Array(hex.length / 2);
536
+ for (let i = 0; i < bytes.length; i++) {
537
+ bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
538
+ }
539
+ return bytes;
540
+ }
541
+
542
+ }
@@ -0,0 +1,20 @@
1
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // RaftDeviceMsg
4
+ // Device message for Raft devices
5
+ //
6
+ // Rob Dobson (C) 2024
7
+ //
8
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
+
10
+ export interface DeviceMsgJsonElem {
11
+ [attrGroupName: string]: string | number | boolean; // Attribute group name and value
12
+ }
13
+
14
+ export interface DeviceMsgJsonBus {
15
+ [devAddr: string]: DeviceMsgJsonElem;
16
+ }
17
+
18
+ export interface DeviceMsgJson {
19
+ [busName: string]: DeviceMsgJsonBus;
20
+ }
@@ -0,0 +1,89 @@
1
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2
+ //
3
+ // RaftDeviceStates
4
+ // Device states for Raft devices
5
+ //
6
+ // Rob Dobson (C) 2024
7
+ //
8
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
+
10
+ import { DeviceTypeInfo } from "./RaftDeviceInfo";
11
+
12
+ export function deviceAttrGetLatestFormatted(attrState: DeviceAttributeState): string {
13
+
14
+ if (attrState.values.length === 0) {
15
+ return 'N/A';
16
+ }
17
+ if (attrState.format.length === 0) {
18
+ return attrState.values[attrState.values.length - 1].toString();
19
+ }
20
+ const value = attrState.values[attrState.values.length - 1];
21
+ let format = attrState.format;
22
+ if (format.startsWith("%")) {
23
+ format = format.slice(1);
24
+ }
25
+ if (format.endsWith('f')) {
26
+ // Floating point number formatting
27
+ const parts = format.split('.');
28
+ let decimalPlaces = 0;
29
+ if (parts.length === 2) {
30
+ decimalPlaces = parseInt(parts[1], 10);
31
+ }
32
+ const formattedNumber = value.toFixed(decimalPlaces);
33
+ let fieldWidth = parseInt(parts[0], 10);
34
+ return fieldWidth ? formattedNumber.padStart(fieldWidth, ' ') : formattedNumber;
35
+ } else if (format.endsWith('x')) {
36
+ // Hexadecimal formatting
37
+ const totalLength = parseInt(format.slice(0, -1), 10);
38
+ return Math.floor(value).toString(16).padStart(totalLength, format.startsWith('0') ? '0' : ' ');
39
+ } else if (format.endsWith('d')) {
40
+ // Decimal integer formatting
41
+ const totalLength = parseInt(format.slice(0, -1), 10);
42
+ return Math.floor(value).toString(10).padStart(totalLength, format.startsWith('0') ? '0' : ' ');
43
+ } else if (format.endsWith('b')) {
44
+ // Binary formatting
45
+ return Math.floor(value) === 0 ? 'no' : 'yes';
46
+ }
47
+ return value.toString();
48
+ }
49
+
50
+ export interface DeviceAttributeState {
51
+ name: string;
52
+ newAttribute: boolean;
53
+ newData: boolean;
54
+ values: number[];
55
+ units: string;
56
+ range: number[];
57
+ format: string;
58
+ visibleSeries: boolean;
59
+ visibleForm: boolean;
60
+ }
61
+
62
+ export interface DeviceAttributesState {
63
+ [attributeName: string]: DeviceAttributeState;
64
+ }
65
+
66
+ export interface DeviceTimeline {
67
+ timestampsUs: number[];
68
+ lastReportTimestampUs: number;
69
+ reportTimestampOffsetUs: number;
70
+ }
71
+
72
+ export interface DeviceState {
73
+ deviceTypeInfo: DeviceTypeInfo;
74
+ deviceTimeline: DeviceTimeline;
75
+ deviceAttributes: DeviceAttributesState;
76
+ deviceIsNew: boolean;
77
+ stateChanged: boolean;
78
+ isOnline: boolean;
79
+ }
80
+
81
+ export class DevicesState {
82
+ [deviceKey: string]: DeviceState;
83
+ }
84
+
85
+ // Add the getDeviceKey method to generate a composite key
86
+ export function getDeviceKey(busName: string, devAddr: string): string {
87
+ return `${busName}_${devAddr}`;
88
+ }
89
+