@robotical/raftjs 2.1.0 → 2.1.3
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/devdocs/devbin-backwards-compatibility.md +105 -0
- package/devdocs/pseudocode-to-js-transpiler.md +563 -0
- package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
- package/dist/react-native/PseudocodeTranspiler.js +115 -0
- package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
- package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
- package/dist/react-native/RaftAttributeHandler.js +108 -32
- package/dist/react-native/RaftAttributeHandler.js.map +1 -1
- package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
- package/dist/react-native/RaftChannelBLE.web.js +59 -21
- package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
- package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
- package/dist/react-native/RaftChannelSimulated.js +9 -5
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSocket.js +16 -1
- package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
- package/dist/react-native/RaftConnector.d.ts +29 -1
- package/dist/react-native/RaftConnector.js +177 -11
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/react-native/RaftCustomAttrHandler.js +32 -44
- package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
- package/dist/react-native/RaftDeviceInfo.js +8 -0
- package/dist/react-native/RaftDeviceInfo.js.map +1 -1
- package/dist/react-native/RaftDeviceManager.d.ts +30 -3
- package/dist/react-native/RaftDeviceManager.js +618 -107
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/react-native/RaftDeviceStates.d.ts +27 -3
- package/dist/react-native/RaftDeviceStates.js +31 -6
- package/dist/react-native/RaftDeviceStates.js.map +1 -1
- package/dist/react-native/RaftFileHandler.d.ts +1 -1
- package/dist/react-native/RaftFileHandler.js +101 -34
- package/dist/react-native/RaftFileHandler.js.map +1 -1
- package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/react-native/RaftMsgHandler.d.ts +1 -1
- package/dist/react-native/RaftMsgHandler.js +6 -3
- package/dist/react-native/RaftMsgHandler.js.map +1 -1
- package/dist/react-native/RaftPublish.d.ts +2 -0
- package/dist/react-native/RaftPublish.js +81 -0
- package/dist/react-native/RaftPublish.js.map +1 -0
- package/dist/react-native/RaftStreamHandler.d.ts +11 -0
- package/dist/react-native/RaftStreamHandler.js +66 -0
- package/dist/react-native/RaftStreamHandler.js.map +1 -1
- package/dist/react-native/RaftStruct.d.ts +2 -2
- package/dist/react-native/RaftStruct.js +97 -26
- package/dist/react-native/RaftStruct.js.map +1 -1
- package/dist/react-native/RaftSystemType.d.ts +1 -0
- package/dist/react-native/RaftSystemUtils.d.ts +17 -1
- package/dist/react-native/RaftSystemUtils.js +51 -0
- package/dist/react-native/RaftSystemUtils.js.map +1 -1
- package/dist/react-native/RaftTimezone.d.ts +16 -0
- package/dist/react-native/RaftTimezone.js +153 -0
- package/dist/react-native/RaftTimezone.js.map +1 -0
- package/dist/react-native/RaftTypes.d.ts +46 -1
- package/dist/react-native/RaftTypes.js.map +1 -1
- package/dist/react-native/RaftUpdateManager.js +1 -1
- package/dist/react-native/RaftUpdateManager.js.map +1 -1
- package/dist/react-native/main.d.ts +3 -0
- package/dist/react-native/main.js +8 -1
- package/dist/react-native/main.js.map +1 -1
- package/dist/web/PseudocodeTranspiler.d.ts +6 -0
- package/dist/web/PseudocodeTranspiler.js +115 -0
- package/dist/web/PseudocodeTranspiler.js.map +1 -0
- package/dist/web/RaftAttributeHandler.d.ts +1 -1
- package/dist/web/RaftAttributeHandler.js +108 -32
- package/dist/web/RaftAttributeHandler.js.map +1 -1
- package/dist/web/RaftChannelBLE.web.d.ts +4 -0
- package/dist/web/RaftChannelBLE.web.js +59 -21
- package/dist/web/RaftChannelBLE.web.js.map +1 -1
- package/dist/web/RaftChannelSimulated.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.js +9 -5
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSocket.js +16 -1
- package/dist/web/RaftChannelWebSocket.js.map +1 -1
- package/dist/web/RaftConnector.d.ts +29 -1
- package/dist/web/RaftConnector.js +177 -11
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/web/RaftCustomAttrHandler.js +32 -44
- package/dist/web/RaftCustomAttrHandler.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +18 -0
- package/dist/web/RaftDeviceInfo.js +8 -0
- package/dist/web/RaftDeviceInfo.js.map +1 -1
- package/dist/web/RaftDeviceManager.d.ts +30 -3
- package/dist/web/RaftDeviceManager.js +618 -107
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/web/RaftDeviceStates.d.ts +27 -3
- package/dist/web/RaftDeviceStates.js +31 -6
- package/dist/web/RaftDeviceStates.js.map +1 -1
- package/dist/web/RaftFileHandler.d.ts +1 -1
- package/dist/web/RaftFileHandler.js +101 -34
- package/dist/web/RaftFileHandler.js.map +1 -1
- package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/web/RaftMsgHandler.d.ts +1 -1
- package/dist/web/RaftMsgHandler.js +6 -3
- package/dist/web/RaftMsgHandler.js.map +1 -1
- package/dist/web/RaftPublish.d.ts +2 -0
- package/dist/web/RaftPublish.js +81 -0
- package/dist/web/RaftPublish.js.map +1 -0
- package/dist/web/RaftStreamHandler.d.ts +11 -0
- package/dist/web/RaftStreamHandler.js +66 -0
- package/dist/web/RaftStreamHandler.js.map +1 -1
- package/dist/web/RaftStruct.d.ts +2 -2
- package/dist/web/RaftStruct.js +97 -26
- package/dist/web/RaftStruct.js.map +1 -1
- package/dist/web/RaftSystemType.d.ts +1 -0
- package/dist/web/RaftSystemUtils.d.ts +17 -1
- package/dist/web/RaftSystemUtils.js +51 -0
- package/dist/web/RaftSystemUtils.js.map +1 -1
- package/dist/web/RaftTimezone.d.ts +16 -0
- package/dist/web/RaftTimezone.js +153 -0
- package/dist/web/RaftTimezone.js.map +1 -0
- package/dist/web/RaftTypes.d.ts +46 -1
- package/dist/web/RaftTypes.js.map +1 -1
- package/dist/web/RaftUpdateManager.js +1 -1
- package/dist/web/RaftUpdateManager.js.map +1 -1
- package/dist/web/main.d.ts +3 -0
- package/dist/web/main.js +8 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +177 -17
- package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
- package/examples/dashboard/src/DevicePanel.tsx +92 -11
- package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
- package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
- package/examples/dashboard/src/DevicesPanel.tsx +11 -0
- package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
- package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
- package/examples/dashboard/src/LoggingPanel.tsx +264 -0
- package/examples/dashboard/src/Main.tsx +12 -2
- package/examples/dashboard/src/SettingsScreen.tsx +9 -4
- package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
- package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
- package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
- package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
- package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
- package/examples/dashboard/src/styles.css +766 -1
- package/notes/web-ble-reconnect-retry.md +69 -0
- package/package.json +10 -7
- package/src/PseudocodeTranspiler.test.ts +372 -0
- package/src/PseudocodeTranspiler.ts +127 -0
- package/src/RaftAttributeHandler.ts +152 -76
- package/src/RaftChannelBLE.web.ts +62 -20
- package/src/RaftChannelSimulated.ts +10 -5
- package/src/RaftChannelWebSocket.ts +16 -2
- package/src/RaftConnector.ts +204 -17
- package/src/RaftCustomAttrHandler.ts +35 -45
- package/src/RaftDeviceInfo.ts +27 -0
- package/src/RaftDeviceManager.test.ts +164 -0
- package/src/RaftDeviceManager.ts +705 -127
- package/src/RaftDeviceMgrIF.ts +13 -2
- package/src/RaftDeviceStates.ts +49 -8
- package/src/RaftFileHandler.ts +112 -39
- package/src/RaftMicroPythonConsoleClient.ts +78 -0
- package/src/RaftMsgHandler.ts +8 -4
- package/src/RaftPublish.ts +92 -0
- package/src/RaftStreamHandler.ts +84 -1
- package/src/RaftStruct.test.ts +229 -0
- package/src/RaftStruct.ts +101 -37
- package/src/RaftSystemType.ts +1 -0
- package/src/RaftSystemUtils.ts +59 -0
- package/src/RaftTimezone.ts +151 -0
- package/src/RaftTypes.ts +57 -1
- package/src/RaftUpdateManager.ts +1 -1
- package/src/main.ts +3 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import './styles.css';
|
|
3
|
-
import { DeviceState } from '../../../src/RaftDeviceStates';
|
|
3
|
+
import { DeviceState, DeviceOnlineState } from '../../../src/RaftDeviceStates';
|
|
4
4
|
import DeviceAttrsForm from './DeviceAttrsForm';
|
|
5
5
|
import DeviceActionsForm from './DeviceActionsForm';
|
|
6
6
|
import DeviceLineChart from './DeviceLineChart';
|
|
7
|
+
import DeviceStatsPanel from './DeviceStatsPanel';
|
|
7
8
|
import ConnManager from './ConnManager';
|
|
8
9
|
import SettingsManager from './SettingsManager';
|
|
9
10
|
|
|
@@ -19,10 +20,14 @@ const DevicePanel = ({ deviceKey, lastUpdated }: DevicePanelProps) => {
|
|
|
19
20
|
const deviceState: DeviceState | undefined = deviceManager?.getDeviceState(deviceKey);
|
|
20
21
|
|
|
21
22
|
// Gray out the device panel if the device is offline
|
|
22
|
-
const offlineClass = deviceState?.
|
|
23
|
+
const offlineClass = deviceState?.onlineState === DeviceOnlineState.Online ? '' : 'offline';
|
|
23
24
|
|
|
24
25
|
const [timedChartUpdate, setTimedChartUpdate] = useState<number>(0);
|
|
25
26
|
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
|
27
|
+
const [showPollRateDialog, setShowPollRateDialog] = useState<boolean>(false);
|
|
28
|
+
const [pollRateInput, setPollRateInput] = useState<string>('');
|
|
29
|
+
const [pollRateStatus, setPollRateStatus] = useState<string>('');
|
|
30
|
+
const [showStats, setShowStats] = useState<boolean>(false);
|
|
26
31
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
27
32
|
|
|
28
33
|
const settingsManager = SettingsManager.getInstance();
|
|
@@ -92,6 +97,34 @@ const DevicePanel = ({ deviceKey, lastUpdated }: DevicePanelProps) => {
|
|
|
92
97
|
setMenuOpen(false);
|
|
93
98
|
};
|
|
94
99
|
|
|
100
|
+
const handleSetPollRateClick = () => {
|
|
101
|
+
setMenuOpen(false);
|
|
102
|
+
setPollRateInput('');
|
|
103
|
+
setPollRateStatus('');
|
|
104
|
+
setShowPollRateDialog(true);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handlePollRateSubmit = () => {
|
|
108
|
+
const rateHz = parseFloat(pollRateInput);
|
|
109
|
+
if (isNaN(rateHz) || rateHz <= 0) {
|
|
110
|
+
setPollRateStatus('Invalid rate — enter a positive number');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const intervalUs = Math.round(1000000 / rateHz);
|
|
114
|
+
const busName = deviceState?.busName ?? '0';
|
|
115
|
+
const addr = deviceState?.deviceAddress ?? '0';
|
|
116
|
+
const cmd = `devman/devconfig?bus=${busName}&addr=${addr}&intervalUs=${intervalUs}`;
|
|
117
|
+
setPollRateStatus('Sending...');
|
|
118
|
+
connManager.getConnector().sendRICRESTMsg(cmd, {}).then((response: object) => {
|
|
119
|
+
console.log(`Poll rate set: ${rateHz} Hz (${intervalUs} us)`, response);
|
|
120
|
+
setPollRateStatus(`Set to ${rateHz} Hz (${intervalUs} µs)`);
|
|
121
|
+
setTimeout(() => setShowPollRateDialog(false), 1500);
|
|
122
|
+
}).catch((error: unknown) => {
|
|
123
|
+
console.warn('Error setting poll rate', error);
|
|
124
|
+
setPollRateStatus('Error setting poll rate');
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
95
128
|
const fallbackCopyTextToClipboard = (text: string) => {
|
|
96
129
|
const textArea = document.createElement("textarea");
|
|
97
130
|
textArea.value = text;
|
|
@@ -117,25 +150,30 @@ const DevicePanel = ({ deviceKey, lastUpdated }: DevicePanelProps) => {
|
|
|
117
150
|
};
|
|
118
151
|
|
|
119
152
|
let headerText = `Device ${deviceState?.deviceTypeInfo?.name}`;
|
|
153
|
+
let bracketsAdded = false;
|
|
154
|
+
if ((deviceState?.busName !== undefined) && (deviceState?.busName !== "") && (deviceState?.busName !== "0")) {
|
|
155
|
+
headerText += ` (Bus ${deviceState?.busName}`;
|
|
156
|
+
bracketsAdded = true;
|
|
157
|
+
}
|
|
120
158
|
if ((deviceState?.deviceAddress !== undefined) && (deviceState?.deviceAddress !== "") && (deviceState?.deviceAddress !== "0")) {
|
|
121
159
|
// See if we can identify I2C addresses - should start with two bytes of 0s and then have a byte which is slot and a byte which is address
|
|
122
|
-
const addrInt = parseInt(deviceState?.deviceAddress,
|
|
160
|
+
const addrInt = parseInt(deviceState?.deviceAddress, 16);
|
|
123
161
|
if (addrInt < 65536) {
|
|
124
162
|
const slot = addrInt >> 8;
|
|
125
|
-
const address = ("00" + (addrInt & 0xFF).toString(16)).slice(-2);
|
|
126
|
-
headerText += ` I2C Address 0x${address}`;
|
|
127
163
|
if (slot === 0)
|
|
128
|
-
headerText += `
|
|
164
|
+
headerText += ` Main Bus`;
|
|
129
165
|
else
|
|
130
|
-
headerText += `
|
|
166
|
+
headerText += ` Slot ${slot}`;
|
|
167
|
+
const address = ("00" + (addrInt & 0xFF).toString(16)).slice(-2);
|
|
168
|
+
headerText += ` Addr 0x${address}`;
|
|
131
169
|
} else {
|
|
132
|
-
headerText += `
|
|
170
|
+
headerText += ` Addr ${deviceState?.deviceAddress}`;
|
|
133
171
|
}
|
|
134
172
|
}
|
|
135
|
-
if (
|
|
136
|
-
headerText += `
|
|
173
|
+
if (bracketsAdded) {
|
|
174
|
+
headerText += `)`;
|
|
137
175
|
}
|
|
138
|
-
if (
|
|
176
|
+
if (deviceState?.onlineState !== DeviceOnlineState.Online) {
|
|
139
177
|
headerText += " (Offline)";
|
|
140
178
|
}
|
|
141
179
|
|
|
@@ -147,14 +185,57 @@ const DevicePanel = ({ deviceKey, lastUpdated }: DevicePanelProps) => {
|
|
|
147
185
|
{menuOpen && (
|
|
148
186
|
<div className="dropdown-menu" ref={menuRef}>
|
|
149
187
|
<div className="menu-item always-enabled" onClick={handleCopyToClipboard}>Copy Values to Clipboard</div>
|
|
188
|
+
<div className="menu-item always-enabled" onClick={handleSetPollRateClick}>Set Poll Rate</div>
|
|
189
|
+
<div className="menu-item always-enabled menu-item-toggle">
|
|
190
|
+
<label className="menu-toggle">
|
|
191
|
+
<input
|
|
192
|
+
type="checkbox"
|
|
193
|
+
checked={showStats}
|
|
194
|
+
onChange={(e) => setShowStats(e.target.checked)}
|
|
195
|
+
/>
|
|
196
|
+
<span>Show Stats</span>
|
|
197
|
+
</label>
|
|
198
|
+
</div>
|
|
150
199
|
</div>
|
|
151
200
|
)}
|
|
152
201
|
</div>
|
|
202
|
+
{showPollRateDialog && (
|
|
203
|
+
<div className="poll-rate-dialog-overlay" onClick={() => setShowPollRateDialog(false)}>
|
|
204
|
+
<div className="poll-rate-dialog" onClick={(e) => e.stopPropagation()}>
|
|
205
|
+
<div className="poll-rate-dialog-title">Set Poll Rate</div>
|
|
206
|
+
<div className="poll-rate-dialog-row">
|
|
207
|
+
<input
|
|
208
|
+
className="poll-rate-input"
|
|
209
|
+
type="number"
|
|
210
|
+
min="0.001"
|
|
211
|
+
step="any"
|
|
212
|
+
placeholder="Rate (Hz)"
|
|
213
|
+
value={pollRateInput}
|
|
214
|
+
onChange={(e) => setPollRateInput(e.target.value)}
|
|
215
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handlePollRateSubmit(); if (e.key === 'Escape') setShowPollRateDialog(false); }}
|
|
216
|
+
autoFocus
|
|
217
|
+
/>
|
|
218
|
+
<span className="poll-rate-unit">Hz</span>
|
|
219
|
+
</div>
|
|
220
|
+
{pollRateInput && !isNaN(parseFloat(pollRateInput)) && parseFloat(pollRateInput) > 0 && (
|
|
221
|
+
<div className="poll-rate-preview">{Math.round(1000000 / parseFloat(pollRateInput))} µs interval</div>
|
|
222
|
+
)}
|
|
223
|
+
{pollRateStatus && <div className="poll-rate-status">{pollRateStatus}</div>}
|
|
224
|
+
<div className="poll-rate-dialog-buttons">
|
|
225
|
+
<button className="poll-rate-btn poll-rate-btn-set" onClick={handlePollRateSubmit}>Set</button>
|
|
226
|
+
<button className="poll-rate-btn poll-rate-btn-cancel" onClick={() => setShowPollRateDialog(false)}>Cancel</button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
153
231
|
<div className={`device-block-data`}>
|
|
154
232
|
<div className="device-attrs-and-actions">
|
|
155
233
|
<DeviceAttrsForm deviceKey={deviceKey} lastUpdated={lastUpdated} />
|
|
156
234
|
<DeviceActionsForm deviceKey={deviceKey} />
|
|
157
235
|
</div>
|
|
236
|
+
{showStats && (
|
|
237
|
+
<DeviceStatsPanel deviceKey={deviceKey} lastUpdated={timedChartUpdate} />
|
|
238
|
+
)}
|
|
158
239
|
{showCharts &&
|
|
159
240
|
<DeviceLineChart deviceKey={deviceKey} lastUpdated={timedChartUpdate} />
|
|
160
241
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import './styles.css';
|
|
3
|
+
|
|
4
|
+
// Rate presets: label + milliseconds
|
|
5
|
+
const RATE_PRESETS = [
|
|
6
|
+
{ label: 'Max (poll rate)', ms: 0 },
|
|
7
|
+
{ label: '10 Hz', ms: 100 },
|
|
8
|
+
{ label: '1 Hz', ms: 1000 },
|
|
9
|
+
{ label: '0.1 Hz (10s)', ms: 10000 },
|
|
10
|
+
{ label: '1/min', ms: 60000 },
|
|
11
|
+
{ label: '1/10min', ms: 600000 },
|
|
12
|
+
{ label: '1/hour', ms: 3600000 },
|
|
13
|
+
{ label: '1/day', ms: 86400000 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Log-scale slider range
|
|
17
|
+
const LOG_RATE_MIN_MS = 50;
|
|
18
|
+
const LOG_RATE_MAX_MS = 360000000;
|
|
19
|
+
|
|
20
|
+
function msToSliderValue(ms: number): number {
|
|
21
|
+
if (ms <= 0) return 0;
|
|
22
|
+
const minLog = Math.log10(LOG_RATE_MIN_MS);
|
|
23
|
+
const maxLog = Math.log10(LOG_RATE_MAX_MS);
|
|
24
|
+
const val = (Math.log10(Math.max(ms, LOG_RATE_MIN_MS)) - minLog) / (maxLog - minLog);
|
|
25
|
+
return Math.min(1, Math.max(0, val));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sliderValueToMs(val: number): number {
|
|
29
|
+
if (val <= 0) return 0;
|
|
30
|
+
const minLog = Math.log10(LOG_RATE_MIN_MS);
|
|
31
|
+
const maxLog = Math.log10(LOG_RATE_MAX_MS);
|
|
32
|
+
return Math.round(Math.pow(10, minLog + val * (maxLog - minLog)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatRateMs(ms: number): string {
|
|
36
|
+
if (ms <= 0) return 'Max (every poll)';
|
|
37
|
+
if (ms < 1000) return `${ms} ms (${(1000 / ms).toFixed(1)} Hz)`;
|
|
38
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)} s (${(1000 / ms).toFixed(2)} Hz)`;
|
|
39
|
+
if (ms < 3600000) return `${(ms / 60000).toFixed(1)} min`;
|
|
40
|
+
if (ms < 86400000) return `${(ms / 3600000).toFixed(1)} hr`;
|
|
41
|
+
return `${(ms / 86400000).toFixed(1)} days`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DeviceLogEntry {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
busName: string;
|
|
47
|
+
addr: string;
|
|
48
|
+
typeName: string;
|
|
49
|
+
rateMs: number;
|
|
50
|
+
pollIntervalMs: number;
|
|
51
|
+
availableAttrs: string[];
|
|
52
|
+
selectedAttrs: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface DeviceSelectDialogProps {
|
|
56
|
+
entries: DeviceLogEntry[];
|
|
57
|
+
format: string;
|
|
58
|
+
onSave: (entries: DeviceLogEntry[]) => void;
|
|
59
|
+
onCancel: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default function DeviceSelectDialog({ entries, format, onSave, onCancel }: DeviceSelectDialogProps) {
|
|
63
|
+
const [localEntries, setLocalEntries] = useState<DeviceLogEntry[]>(
|
|
64
|
+
entries.map(e => ({ ...e, selectedAttrs: [...e.selectedAttrs] }))
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const toggleDevice = (index: number) => {
|
|
68
|
+
setLocalEntries(prev => {
|
|
69
|
+
const next = [...prev];
|
|
70
|
+
next[index] = { ...next[index], enabled: !next[index].enabled };
|
|
71
|
+
return next;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const setRate = (index: number, rateMs: number) => {
|
|
76
|
+
setLocalEntries(prev => {
|
|
77
|
+
const next = [...prev];
|
|
78
|
+
next[index] = { ...next[index], rateMs };
|
|
79
|
+
return next;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const selectAll = () => {
|
|
84
|
+
setLocalEntries(prev => prev.map(d => ({ ...d, enabled: true })));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const selectNone = () => {
|
|
88
|
+
setLocalEntries(prev => prev.map(d => ({ ...d, enabled: false })));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const toggleAttr = (deviceIndex: number, attrName: string) => {
|
|
92
|
+
setLocalEntries(prev => {
|
|
93
|
+
const next = [...prev];
|
|
94
|
+
const entry = { ...next[deviceIndex] };
|
|
95
|
+
const selected = [...entry.selectedAttrs];
|
|
96
|
+
const idx = selected.indexOf(attrName);
|
|
97
|
+
if (idx >= 0) {
|
|
98
|
+
selected.splice(idx, 1);
|
|
99
|
+
} else {
|
|
100
|
+
selected.push(attrName);
|
|
101
|
+
}
|
|
102
|
+
entry.selectedAttrs = selected;
|
|
103
|
+
next[deviceIndex] = entry;
|
|
104
|
+
return next;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="dev-select-overlay" onClick={onCancel}>
|
|
110
|
+
<div className="dev-select-dialog" onClick={e => e.stopPropagation()}>
|
|
111
|
+
<h3 className="dev-select-title">Select Devices & Attributes</h3>
|
|
112
|
+
|
|
113
|
+
<div className="log-config-select-buttons">
|
|
114
|
+
<button className="log-config-select-btn" onClick={selectAll}>All</button>
|
|
115
|
+
<button className="log-config-select-btn" onClick={selectNone}>None</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="dev-select-list">
|
|
119
|
+
{localEntries.map((entry, idx) => (
|
|
120
|
+
<div key={`${entry.busName}_${entry.addr}`} className={`log-config-device ${entry.enabled ? '' : 'log-config-device-disabled'}`}>
|
|
121
|
+
<div className="log-config-device-header">
|
|
122
|
+
<label className="log-config-checkbox-label">
|
|
123
|
+
<input
|
|
124
|
+
type="checkbox"
|
|
125
|
+
checked={entry.enabled}
|
|
126
|
+
onChange={() => toggleDevice(idx)}
|
|
127
|
+
/>
|
|
128
|
+
<span className="log-config-device-name">{entry.typeName}</span>
|
|
129
|
+
</label>
|
|
130
|
+
<span className="log-config-device-addr">
|
|
131
|
+
Bus {entry.busName} · 0x{entry.addr}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{entry.enabled && (
|
|
136
|
+
<div className="log-config-rate-control">
|
|
137
|
+
<div className="log-config-rate-row">
|
|
138
|
+
<label className="log-config-rate-label">Log rate:</label>
|
|
139
|
+
<select
|
|
140
|
+
className="log-config-rate-preset"
|
|
141
|
+
value={RATE_PRESETS.find(p => p.ms === entry.rateMs) ? entry.rateMs : 'custom'}
|
|
142
|
+
onChange={e => {
|
|
143
|
+
const val = e.target.value;
|
|
144
|
+
if (val !== 'custom') setRate(idx, parseInt(val, 10));
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{RATE_PRESETS.map(p => (
|
|
148
|
+
<option key={p.ms} value={p.ms}>{p.label}</option>
|
|
149
|
+
))}
|
|
150
|
+
{!RATE_PRESETS.find(p => p.ms === entry.rateMs) && (
|
|
151
|
+
<option value="custom">Custom</option>
|
|
152
|
+
)}
|
|
153
|
+
</select>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="log-config-slider-row">
|
|
157
|
+
<span className="log-config-slider-label">Fast</span>
|
|
158
|
+
<input
|
|
159
|
+
type="range"
|
|
160
|
+
className="log-config-slider"
|
|
161
|
+
min="0"
|
|
162
|
+
max="1"
|
|
163
|
+
step="0.005"
|
|
164
|
+
value={msToSliderValue(entry.rateMs)}
|
|
165
|
+
onChange={e => {
|
|
166
|
+
const ms = sliderValueToMs(parseFloat(e.target.value));
|
|
167
|
+
setRate(idx, ms);
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
<span className="log-config-slider-label">Slow</span>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="log-config-rate-display">
|
|
174
|
+
{formatRateMs(entry.rateMs)}
|
|
175
|
+
{entry.pollIntervalMs > 0 && entry.rateMs === 0 && (
|
|
176
|
+
<span className="log-config-poll-rate"> · poll: {(1000 / entry.pollIntervalMs).toFixed(1)} Hz</span>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{format === 'csv' && entry.availableAttrs.length > 0 && (
|
|
181
|
+
<div className="log-config-attrs">
|
|
182
|
+
<div className="log-config-attrs-label">
|
|
183
|
+
Attributes {entry.selectedAttrs.length === 0 ? '(all)' : `(${entry.selectedAttrs.length}/${entry.availableAttrs.length})`}:
|
|
184
|
+
</div>
|
|
185
|
+
<div className="log-config-attrs-list">
|
|
186
|
+
{entry.availableAttrs.map(attrName => (
|
|
187
|
+
<label key={attrName} className="log-config-attr-checkbox">
|
|
188
|
+
<input
|
|
189
|
+
type="checkbox"
|
|
190
|
+
checked={entry.selectedAttrs.length === 0 || entry.selectedAttrs.includes(attrName)}
|
|
191
|
+
onChange={() => {
|
|
192
|
+
if (entry.selectedAttrs.length === 0) {
|
|
193
|
+
setLocalEntries(prev => {
|
|
194
|
+
const next = [...prev];
|
|
195
|
+
const e = { ...next[idx] };
|
|
196
|
+
e.selectedAttrs = entry.availableAttrs.filter(a => a !== attrName);
|
|
197
|
+
next[idx] = e;
|
|
198
|
+
return next;
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
toggleAttr(idx, attrName);
|
|
202
|
+
}
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
{attrName}
|
|
206
|
+
</label>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="dev-select-buttons">
|
|
218
|
+
<button className="dev-select-btn dev-select-btn-cancel" onClick={onCancel}>Cancel</button>
|
|
219
|
+
<button className="dev-select-btn dev-select-btn-save" onClick={() => onSave(localEntries)}>OK</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import ConnManager from './ConnManager';
|
|
3
|
+
import './styles.css';
|
|
4
|
+
|
|
5
|
+
const connManager = ConnManager.getInstance();
|
|
6
|
+
|
|
7
|
+
const LAST_SAMPLE_AVG_COUNT = 5;
|
|
8
|
+
|
|
9
|
+
export interface DeviceStatsPanelProps {
|
|
10
|
+
deviceKey: string;
|
|
11
|
+
lastUpdated: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DeviceStatsPanel: React.FC<DeviceStatsPanelProps> = ({ deviceKey, lastUpdated }: DeviceStatsPanelProps) => {
|
|
15
|
+
const deviceManager = connManager.getConnector().getSystemType()?.deviceMgrIF;
|
|
16
|
+
const stats = deviceManager?.getDeviceStats(deviceKey);
|
|
17
|
+
const recentAgesRef = useRef<number[]>([]);
|
|
18
|
+
|
|
19
|
+
if (!stats) {
|
|
20
|
+
return <></>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const windowSeconds = stats.windowMs / 1000;
|
|
24
|
+
const sampleRateHz = Number.isFinite(stats.sampleRateHz) ? stats.sampleRateHz : 0;
|
|
25
|
+
const nowMs = Math.max(lastUpdated || 0, Date.now());
|
|
26
|
+
|
|
27
|
+
let lastSampleAgeMs: number | null = null;
|
|
28
|
+
if (stats.lastSampleTimeMs) {
|
|
29
|
+
const ageMs = nowMs - stats.lastSampleTimeMs;
|
|
30
|
+
const ages = recentAgesRef.current;
|
|
31
|
+
ages.push(ageMs);
|
|
32
|
+
if (ages.length > LAST_SAMPLE_AVG_COUNT) {
|
|
33
|
+
ages.shift();
|
|
34
|
+
}
|
|
35
|
+
lastSampleAgeMs = ages.reduce((sum, v) => sum + v, 0) / ages.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleReset = () => {
|
|
39
|
+
deviceManager?.resetDeviceStats(deviceKey);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="device-stats-panel">
|
|
44
|
+
<div className="device-stats-header">
|
|
45
|
+
<span>Sampling Stats</span>
|
|
46
|
+
<button className="device-stats-reset" onClick={handleReset}>Reset Samples</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="device-stats-grid">
|
|
49
|
+
<div className="device-stats-item">
|
|
50
|
+
<div className="device-stats-label">Sample Rate</div>
|
|
51
|
+
<div className="device-stats-value">{sampleRateHz.toFixed(2)} Hz</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="device-stats-item">
|
|
54
|
+
<div className="device-stats-label">Window</div>
|
|
55
|
+
<div className="device-stats-value">{windowSeconds.toFixed(1)} s</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="device-stats-item">
|
|
58
|
+
<div className="device-stats-label">Window Samples</div>
|
|
59
|
+
<div className="device-stats-value">{stats.windowSamples}</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="device-stats-item">
|
|
62
|
+
<div className="device-stats-label">Total Samples</div>
|
|
63
|
+
<div className="device-stats-value">{stats.totalSamples}</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="device-stats-item">
|
|
66
|
+
<div className="device-stats-label">Last Sample</div>
|
|
67
|
+
<div className="device-stats-value">
|
|
68
|
+
{lastSampleAgeMs === null ? 'N/A' : `${lastSampleAgeMs.toFixed(0)} ms ago`}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default DeviceStatsPanel;
|
|
@@ -41,6 +41,17 @@ export default function DevicesPanel(props: DevicesPanelProps) {
|
|
|
41
41
|
}
|
|
42
42
|
deviceManager.addAttributeDataCallback(onNewAttributeData);
|
|
43
43
|
|
|
44
|
+
const onDeviceRemoved = (deviceKey: string, state: DeviceState) => {
|
|
45
|
+
setLastUpdated(Date.now());
|
|
46
|
+
};
|
|
47
|
+
deviceManager.addDeviceRemovedCallback(onDeviceRemoved);
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
deviceManager.removeNewDeviceCallback(onNewDevice);
|
|
51
|
+
deviceManager.removeNewAttributeCallback(onNewAttribute);
|
|
52
|
+
deviceManager.removeAttributeDataCallback(onNewAttributeData);
|
|
53
|
+
deviceManager.removeDeviceRemovedCallback(onDeviceRemoved);
|
|
54
|
+
};
|
|
44
55
|
}, []);
|
|
45
56
|
|
|
46
57
|
const deviceManager = connManager.getConnector().getSystemType()?.deviceMgrIF;
|