@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.
- package/.editorconfig +14 -0
- package/.gitattributes +11 -0
- package/.nvmrc +1 -0
- package/LICENSE +22 -0
- package/README.md +11 -0
- package/TODO.md +1 -0
- package/dist/RaftAttributeHandler.d.ts +12 -0
- package/dist/RaftAttributeHandler.js +241 -0
- package/dist/RaftAttributeHandler.js.map +1 -0
- package/dist/RaftChannel.d.ts +18 -0
- package/dist/RaftChannel.js +12 -0
- package/dist/RaftChannel.js.map +1 -0
- package/dist/RaftChannelWebBLE.d.ts +38 -0
- package/dist/RaftChannelWebBLE.js +274 -0
- package/dist/RaftChannelWebBLE.js.map +1 -0
- package/dist/RaftChannelWebSerial.d.ts +37 -0
- package/dist/RaftChannelWebSerial.js +319 -0
- package/dist/RaftChannelWebSerial.js.map +1 -0
- package/dist/RaftChannelWebSocket.d.ts +28 -0
- package/dist/RaftChannelWebSocket.js +197 -0
- package/dist/RaftChannelWebSocket.js.map +1 -0
- package/dist/RaftCommsStats.d.ts +39 -0
- package/dist/RaftCommsStats.js +128 -0
- package/dist/RaftCommsStats.js.map +1 -0
- package/dist/RaftConnEvents.d.ts +31 -0
- package/dist/RaftConnEvents.js +44 -0
- package/dist/RaftConnEvents.js.map +1 -0
- package/dist/RaftConnector.d.ts +242 -0
- package/dist/RaftConnector.js +613 -0
- package/dist/RaftConnector.js.map +1 -0
- package/dist/RaftCustomAttrHandler.d.ts +4 -0
- package/dist/RaftCustomAttrHandler.js +50 -0
- package/dist/RaftCustomAttrHandler.js.map +1 -0
- package/dist/RaftDeviceInfo.d.ts +64 -0
- package/dist/RaftDeviceInfo.js +36 -0
- package/dist/RaftDeviceInfo.js.map +1 -0
- package/dist/RaftDeviceManager.d.ts +37 -0
- package/dist/RaftDeviceManager.js +450 -0
- package/dist/RaftDeviceManager.js.map +1 -0
- package/dist/RaftDeviceMsg.d.ts +9 -0
- package/dist/RaftDeviceMsg.js +11 -0
- package/dist/RaftDeviceMsg.js.map +1 -0
- package/dist/RaftDeviceStates.d.ts +33 -0
- package/dist/RaftDeviceStates.js +60 -0
- package/dist/RaftDeviceStates.js.map +1 -0
- package/dist/RaftFileHandler.d.ts +52 -0
- package/dist/RaftFileHandler.js +502 -0
- package/dist/RaftFileHandler.js.map +1 -0
- package/dist/RaftLog.d.ts +22 -0
- package/dist/RaftLog.js +63 -0
- package/dist/RaftLog.js.map +1 -0
- package/dist/RaftMiniHDLC.d.ts +18 -0
- package/dist/RaftMiniHDLC.js +383 -0
- package/dist/RaftMiniHDLC.js.map +1 -0
- package/dist/RaftMsgHandler.d.ts +57 -0
- package/dist/RaftMsgHandler.js +480 -0
- package/dist/RaftMsgHandler.js.map +1 -0
- package/dist/RaftMsgTrackInfo.d.ts +17 -0
- package/dist/RaftMsgTrackInfo.js +42 -0
- package/dist/RaftMsgTrackInfo.js.map +1 -0
- package/dist/RaftProtocolDefs.d.ts +30 -0
- package/dist/RaftProtocolDefs.js +48 -0
- package/dist/RaftProtocolDefs.js.map +1 -0
- package/dist/RaftStreamHandler.d.ts +38 -0
- package/dist/RaftStreamHandler.js +257 -0
- package/dist/RaftStreamHandler.js.map +1 -0
- package/dist/RaftSystemType.d.ts +21 -0
- package/dist/RaftSystemType.js +3 -0
- package/dist/RaftSystemType.js.map +1 -0
- package/dist/RaftSystemUtils.d.ts +136 -0
- package/dist/RaftSystemUtils.js +410 -0
- package/dist/RaftSystemUtils.js.map +1 -0
- package/dist/RaftTypes.d.ts +184 -0
- package/dist/RaftTypes.js +157 -0
- package/dist/RaftTypes.js.map +1 -0
- package/dist/RaftUpdateEvents.d.ts +33 -0
- package/dist/RaftUpdateEvents.js +46 -0
- package/dist/RaftUpdateEvents.js.map +1 -0
- package/dist/RaftUpdateManager.d.ts +61 -0
- package/dist/RaftUpdateManager.js +618 -0
- package/dist/RaftUpdateManager.js.map +1 -0
- package/dist/RaftUtils.d.ts +125 -0
- package/dist/RaftUtils.js +454 -0
- package/dist/RaftUtils.js.map +1 -0
- package/dist/RaftWifiTypes.d.ts +23 -0
- package/dist/RaftWifiTypes.js +43 -0
- package/dist/RaftWifiTypes.js.map +1 -0
- package/dist/TestDataGen.d.ts +7 -0
- package/dist/TestDataGen.js +133 -0
- package/dist/TestDataGen.js.map +1 -0
- package/dist/main.d.ts +18 -0
- package/dist/main.js +42 -0
- package/dist/main.js.map +1 -0
- package/eslint.config.mjs +33 -0
- package/examples/dashboard/package.json +39 -0
- package/examples/dashboard/src/ConnManager.ts +86 -0
- package/examples/dashboard/src/Main.tsx +100 -0
- package/examples/dashboard/src/StatusScreen.tsx +72 -0
- package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +144 -0
- package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +77 -0
- package/examples/dashboard/src/SystemTypeMarty/RICAddOn.ts +70 -0
- package/examples/dashboard/src/SystemTypeMarty/RICAddOnBase.ts +33 -0
- package/examples/dashboard/src/SystemTypeMarty/RICAddOnManager.ts +342 -0
- package/examples/dashboard/src/SystemTypeMarty/RICCommsStats.ts +170 -0
- package/examples/dashboard/src/SystemTypeMarty/RICHWElem.ts +123 -0
- package/examples/dashboard/src/SystemTypeMarty/RICLEDPatternChecker.ts +207 -0
- package/examples/dashboard/src/SystemTypeMarty/RICROSSerial.ts +464 -0
- package/examples/dashboard/src/SystemTypeMarty/RICServoFaultDetector.ts +146 -0
- package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +32 -0
- package/examples/dashboard/src/SystemTypeMarty/RICSystemUtils.ts +371 -0
- package/examples/dashboard/src/SystemTypeMarty/RICTypes.ts +20 -0
- package/examples/dashboard/src/SystemTypeMarty/SystemTypeMarty.ts +113 -0
- package/examples/dashboard/src/index.html +15 -0
- package/examples/dashboard/src/index.tsx +15 -0
- package/examples/dashboard/src/styles.css +122 -0
- package/examples/dashboard/tsconfig.json +18 -0
- package/jest.config.js +11 -0
- package/package.json +50 -0
- package/src/RaftAttributeHandler.ts +289 -0
- package/src/RaftChannel.ts +30 -0
- package/src/RaftChannelWebBLE.ts +342 -0
- package/src/RaftChannelWebSerial.ts +408 -0
- package/src/RaftChannelWebSocket.ts +245 -0
- package/src/RaftCommsStats.ts +142 -0
- package/src/RaftConnEvents.ts +46 -0
- package/src/RaftConnector.ts +745 -0
- package/src/RaftCustomAttrHandler.ts +54 -0
- package/src/RaftDeviceInfo.ts +104 -0
- package/src/RaftDeviceManager.ts +542 -0
- package/src/RaftDeviceMsg.ts +20 -0
- package/src/RaftDeviceStates.ts +89 -0
- package/src/RaftFileHandler.ts +668 -0
- package/src/RaftLog.ts +70 -0
- package/src/RaftMiniHDLC.ts +396 -0
- package/src/RaftMsgHandler.ts +778 -0
- package/src/RaftMsgTrackInfo.ts +51 -0
- package/src/RaftProtocolDefs.ts +46 -0
- package/src/RaftStreamHandler.ts +328 -0
- package/src/RaftSystemType.ts +25 -0
- package/src/RaftSystemUtils.ts +487 -0
- package/src/RaftTypes.ts +250 -0
- package/src/RaftUpdateEvents.ts +48 -0
- package/src/RaftUpdateManager.ts +778 -0
- package/src/RaftUtils.ts +484 -0
- package/src/RaftWifiTypes.ts +36 -0
- package/src/TestDataGen.ts +157 -0
- package/src/main.ts +28 -0
- package/testdata/TestDeviceTypeRecs.json +492 -0
- 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
|
+
|