@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.
Files changed (172) hide show
  1. package/devdocs/devbin-backwards-compatibility.md +105 -0
  2. package/devdocs/pseudocode-to-js-transpiler.md +563 -0
  3. package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
  4. package/dist/react-native/PseudocodeTranspiler.js +115 -0
  5. package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
  6. package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
  7. package/dist/react-native/RaftAttributeHandler.js +108 -32
  8. package/dist/react-native/RaftAttributeHandler.js.map +1 -1
  9. package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
  10. package/dist/react-native/RaftChannelBLE.web.js +59 -21
  11. package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
  12. package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
  13. package/dist/react-native/RaftChannelSimulated.js +9 -5
  14. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  15. package/dist/react-native/RaftChannelWebSocket.js +16 -1
  16. package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
  17. package/dist/react-native/RaftConnector.d.ts +29 -1
  18. package/dist/react-native/RaftConnector.js +177 -11
  19. package/dist/react-native/RaftConnector.js.map +1 -1
  20. package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
  21. package/dist/react-native/RaftCustomAttrHandler.js +32 -44
  22. package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
  23. package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
  24. package/dist/react-native/RaftDeviceInfo.js +8 -0
  25. package/dist/react-native/RaftDeviceInfo.js.map +1 -1
  26. package/dist/react-native/RaftDeviceManager.d.ts +30 -3
  27. package/dist/react-native/RaftDeviceManager.js +618 -107
  28. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  29. package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
  30. package/dist/react-native/RaftDeviceStates.d.ts +27 -3
  31. package/dist/react-native/RaftDeviceStates.js +31 -6
  32. package/dist/react-native/RaftDeviceStates.js.map +1 -1
  33. package/dist/react-native/RaftFileHandler.d.ts +1 -1
  34. package/dist/react-native/RaftFileHandler.js +101 -34
  35. package/dist/react-native/RaftFileHandler.js.map +1 -1
  36. package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
  37. package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
  38. package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
  39. package/dist/react-native/RaftMsgHandler.d.ts +1 -1
  40. package/dist/react-native/RaftMsgHandler.js +6 -3
  41. package/dist/react-native/RaftMsgHandler.js.map +1 -1
  42. package/dist/react-native/RaftPublish.d.ts +2 -0
  43. package/dist/react-native/RaftPublish.js +81 -0
  44. package/dist/react-native/RaftPublish.js.map +1 -0
  45. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  46. package/dist/react-native/RaftStreamHandler.js +66 -0
  47. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  48. package/dist/react-native/RaftStruct.d.ts +2 -2
  49. package/dist/react-native/RaftStruct.js +97 -26
  50. package/dist/react-native/RaftStruct.js.map +1 -1
  51. package/dist/react-native/RaftSystemType.d.ts +1 -0
  52. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  53. package/dist/react-native/RaftSystemUtils.js +51 -0
  54. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  55. package/dist/react-native/RaftTimezone.d.ts +16 -0
  56. package/dist/react-native/RaftTimezone.js +153 -0
  57. package/dist/react-native/RaftTimezone.js.map +1 -0
  58. package/dist/react-native/RaftTypes.d.ts +46 -1
  59. package/dist/react-native/RaftTypes.js.map +1 -1
  60. package/dist/react-native/RaftUpdateManager.js +1 -1
  61. package/dist/react-native/RaftUpdateManager.js.map +1 -1
  62. package/dist/react-native/main.d.ts +3 -0
  63. package/dist/react-native/main.js +8 -1
  64. package/dist/react-native/main.js.map +1 -1
  65. package/dist/web/PseudocodeTranspiler.d.ts +6 -0
  66. package/dist/web/PseudocodeTranspiler.js +115 -0
  67. package/dist/web/PseudocodeTranspiler.js.map +1 -0
  68. package/dist/web/RaftAttributeHandler.d.ts +1 -1
  69. package/dist/web/RaftAttributeHandler.js +108 -32
  70. package/dist/web/RaftAttributeHandler.js.map +1 -1
  71. package/dist/web/RaftChannelBLE.web.d.ts +4 -0
  72. package/dist/web/RaftChannelBLE.web.js +59 -21
  73. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  74. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  75. package/dist/web/RaftChannelSimulated.js +9 -5
  76. package/dist/web/RaftChannelSimulated.js.map +1 -1
  77. package/dist/web/RaftChannelWebSocket.js +16 -1
  78. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  79. package/dist/web/RaftConnector.d.ts +29 -1
  80. package/dist/web/RaftConnector.js +177 -11
  81. package/dist/web/RaftConnector.js.map +1 -1
  82. package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
  83. package/dist/web/RaftCustomAttrHandler.js +32 -44
  84. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  85. package/dist/web/RaftDeviceInfo.d.ts +18 -0
  86. package/dist/web/RaftDeviceInfo.js +8 -0
  87. package/dist/web/RaftDeviceInfo.js.map +1 -1
  88. package/dist/web/RaftDeviceManager.d.ts +30 -3
  89. package/dist/web/RaftDeviceManager.js +618 -107
  90. package/dist/web/RaftDeviceManager.js.map +1 -1
  91. package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
  92. package/dist/web/RaftDeviceStates.d.ts +27 -3
  93. package/dist/web/RaftDeviceStates.js +31 -6
  94. package/dist/web/RaftDeviceStates.js.map +1 -1
  95. package/dist/web/RaftFileHandler.d.ts +1 -1
  96. package/dist/web/RaftFileHandler.js +101 -34
  97. package/dist/web/RaftFileHandler.js.map +1 -1
  98. package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
  99. package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
  100. package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
  101. package/dist/web/RaftMsgHandler.d.ts +1 -1
  102. package/dist/web/RaftMsgHandler.js +6 -3
  103. package/dist/web/RaftMsgHandler.js.map +1 -1
  104. package/dist/web/RaftPublish.d.ts +2 -0
  105. package/dist/web/RaftPublish.js +81 -0
  106. package/dist/web/RaftPublish.js.map +1 -0
  107. package/dist/web/RaftStreamHandler.d.ts +11 -0
  108. package/dist/web/RaftStreamHandler.js +66 -0
  109. package/dist/web/RaftStreamHandler.js.map +1 -1
  110. package/dist/web/RaftStruct.d.ts +2 -2
  111. package/dist/web/RaftStruct.js +97 -26
  112. package/dist/web/RaftStruct.js.map +1 -1
  113. package/dist/web/RaftSystemType.d.ts +1 -0
  114. package/dist/web/RaftSystemUtils.d.ts +17 -1
  115. package/dist/web/RaftSystemUtils.js +51 -0
  116. package/dist/web/RaftSystemUtils.js.map +1 -1
  117. package/dist/web/RaftTimezone.d.ts +16 -0
  118. package/dist/web/RaftTimezone.js +153 -0
  119. package/dist/web/RaftTimezone.js.map +1 -0
  120. package/dist/web/RaftTypes.d.ts +46 -1
  121. package/dist/web/RaftTypes.js.map +1 -1
  122. package/dist/web/RaftUpdateManager.js +1 -1
  123. package/dist/web/RaftUpdateManager.js.map +1 -1
  124. package/dist/web/main.d.ts +3 -0
  125. package/dist/web/main.js +8 -1
  126. package/dist/web/main.js.map +1 -1
  127. package/examples/dashboard/package.json +2 -2
  128. package/examples/dashboard/src/DeviceActionsForm.tsx +177 -17
  129. package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
  130. package/examples/dashboard/src/DevicePanel.tsx +92 -11
  131. package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
  132. package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
  133. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  134. package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
  135. package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
  136. package/examples/dashboard/src/LoggingPanel.tsx +264 -0
  137. package/examples/dashboard/src/Main.tsx +12 -2
  138. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  139. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
  140. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  141. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  142. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
  143. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
  144. package/examples/dashboard/src/styles.css +766 -1
  145. package/notes/web-ble-reconnect-retry.md +69 -0
  146. package/package.json +10 -7
  147. package/src/PseudocodeTranspiler.test.ts +372 -0
  148. package/src/PseudocodeTranspiler.ts +127 -0
  149. package/src/RaftAttributeHandler.ts +152 -76
  150. package/src/RaftChannelBLE.web.ts +62 -20
  151. package/src/RaftChannelSimulated.ts +10 -5
  152. package/src/RaftChannelWebSocket.ts +16 -2
  153. package/src/RaftConnector.ts +204 -17
  154. package/src/RaftCustomAttrHandler.ts +35 -45
  155. package/src/RaftDeviceInfo.ts +27 -0
  156. package/src/RaftDeviceManager.test.ts +164 -0
  157. package/src/RaftDeviceManager.ts +705 -127
  158. package/src/RaftDeviceMgrIF.ts +13 -2
  159. package/src/RaftDeviceStates.ts +49 -8
  160. package/src/RaftFileHandler.ts +112 -39
  161. package/src/RaftMicroPythonConsoleClient.ts +78 -0
  162. package/src/RaftMsgHandler.ts +8 -4
  163. package/src/RaftPublish.ts +92 -0
  164. package/src/RaftStreamHandler.ts +84 -1
  165. package/src/RaftStruct.test.ts +229 -0
  166. package/src/RaftStruct.ts +101 -37
  167. package/src/RaftSystemType.ts +1 -0
  168. package/src/RaftSystemUtils.ts +59 -0
  169. package/src/RaftTimezone.ts +151 -0
  170. package/src/RaftTypes.ts +57 -1
  171. package/src/RaftUpdateManager.ts +1 -1
  172. 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?.isOnline ? '' : 'offline';
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, 10);
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 += ` (Main Bus)`;
164
+ headerText += ` Main Bus`;
129
165
  else
130
- headerText += ` (Slot ${slot})`;
166
+ headerText += ` Slot ${slot}`;
167
+ const address = ("00" + (addrInt & 0xFF).toString(16)).slice(-2);
168
+ headerText += ` Addr 0x${address}`;
131
169
  } else {
132
- headerText += ` Address ${deviceState?.deviceAddress}`;
170
+ headerText += ` Addr ${deviceState?.deviceAddress}`;
133
171
  }
134
172
  }
135
- if ((deviceState?.busName !== undefined) && (deviceState?.busName !== "") && (deviceState?.busName !== "0")) {
136
- headerText += ` Bus ${deviceState?.busName}`;
173
+ if (bracketsAdded) {
174
+ headerText += `)`;
137
175
  }
138
- if (!deviceState?.isOnline) {
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 &amp; 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;