@robotical/raftjs 2.1.0 → 2.1.2

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 (158) 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 +11 -1
  18. package/dist/react-native/RaftConnector.js +75 -9
  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 +0 -1
  34. package/dist/react-native/RaftFileHandler.js +61 -23
  35. package/dist/react-native/RaftFileHandler.js.map +1 -1
  36. package/dist/react-native/RaftPublish.d.ts +2 -0
  37. package/dist/react-native/RaftPublish.js +81 -0
  38. package/dist/react-native/RaftPublish.js.map +1 -0
  39. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  40. package/dist/react-native/RaftStreamHandler.js +66 -0
  41. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  42. package/dist/react-native/RaftStruct.d.ts +2 -2
  43. package/dist/react-native/RaftStruct.js +97 -26
  44. package/dist/react-native/RaftStruct.js.map +1 -1
  45. package/dist/react-native/RaftSystemType.d.ts +1 -0
  46. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  47. package/dist/react-native/RaftSystemUtils.js +51 -0
  48. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  49. package/dist/react-native/RaftTimezone.d.ts +16 -0
  50. package/dist/react-native/RaftTimezone.js +153 -0
  51. package/dist/react-native/RaftTimezone.js.map +1 -0
  52. package/dist/react-native/RaftTypes.d.ts +27 -1
  53. package/dist/react-native/RaftTypes.js.map +1 -1
  54. package/dist/react-native/RaftUpdateManager.js +1 -1
  55. package/dist/react-native/RaftUpdateManager.js.map +1 -1
  56. package/dist/react-native/main.d.ts +2 -0
  57. package/dist/react-native/main.js +6 -1
  58. package/dist/react-native/main.js.map +1 -1
  59. package/dist/web/PseudocodeTranspiler.d.ts +6 -0
  60. package/dist/web/PseudocodeTranspiler.js +115 -0
  61. package/dist/web/PseudocodeTranspiler.js.map +1 -0
  62. package/dist/web/RaftAttributeHandler.d.ts +1 -1
  63. package/dist/web/RaftAttributeHandler.js +108 -32
  64. package/dist/web/RaftAttributeHandler.js.map +1 -1
  65. package/dist/web/RaftChannelBLE.web.d.ts +4 -0
  66. package/dist/web/RaftChannelBLE.web.js +59 -21
  67. package/dist/web/RaftChannelBLE.web.js.map +1 -1
  68. package/dist/web/RaftChannelSimulated.d.ts +1 -0
  69. package/dist/web/RaftChannelSimulated.js +9 -5
  70. package/dist/web/RaftChannelSimulated.js.map +1 -1
  71. package/dist/web/RaftChannelWebSocket.js +16 -1
  72. package/dist/web/RaftChannelWebSocket.js.map +1 -1
  73. package/dist/web/RaftConnector.d.ts +11 -1
  74. package/dist/web/RaftConnector.js +75 -9
  75. package/dist/web/RaftConnector.js.map +1 -1
  76. package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
  77. package/dist/web/RaftCustomAttrHandler.js +32 -44
  78. package/dist/web/RaftCustomAttrHandler.js.map +1 -1
  79. package/dist/web/RaftDeviceInfo.d.ts +18 -0
  80. package/dist/web/RaftDeviceInfo.js +8 -0
  81. package/dist/web/RaftDeviceInfo.js.map +1 -1
  82. package/dist/web/RaftDeviceManager.d.ts +30 -3
  83. package/dist/web/RaftDeviceManager.js +618 -107
  84. package/dist/web/RaftDeviceManager.js.map +1 -1
  85. package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
  86. package/dist/web/RaftDeviceStates.d.ts +27 -3
  87. package/dist/web/RaftDeviceStates.js +31 -6
  88. package/dist/web/RaftDeviceStates.js.map +1 -1
  89. package/dist/web/RaftFileHandler.d.ts +0 -1
  90. package/dist/web/RaftFileHandler.js +61 -23
  91. package/dist/web/RaftFileHandler.js.map +1 -1
  92. package/dist/web/RaftPublish.d.ts +2 -0
  93. package/dist/web/RaftPublish.js +81 -0
  94. package/dist/web/RaftPublish.js.map +1 -0
  95. package/dist/web/RaftStreamHandler.d.ts +11 -0
  96. package/dist/web/RaftStreamHandler.js +66 -0
  97. package/dist/web/RaftStreamHandler.js.map +1 -1
  98. package/dist/web/RaftStruct.d.ts +2 -2
  99. package/dist/web/RaftStruct.js +97 -26
  100. package/dist/web/RaftStruct.js.map +1 -1
  101. package/dist/web/RaftSystemType.d.ts +1 -0
  102. package/dist/web/RaftSystemUtils.d.ts +17 -1
  103. package/dist/web/RaftSystemUtils.js +51 -0
  104. package/dist/web/RaftSystemUtils.js.map +1 -1
  105. package/dist/web/RaftTimezone.d.ts +16 -0
  106. package/dist/web/RaftTimezone.js +153 -0
  107. package/dist/web/RaftTimezone.js.map +1 -0
  108. package/dist/web/RaftTypes.d.ts +27 -1
  109. package/dist/web/RaftTypes.js.map +1 -1
  110. package/dist/web/RaftUpdateManager.js +1 -1
  111. package/dist/web/RaftUpdateManager.js.map +1 -1
  112. package/dist/web/main.d.ts +2 -0
  113. package/dist/web/main.js +6 -1
  114. package/dist/web/main.js.map +1 -1
  115. package/examples/dashboard/package.json +2 -2
  116. package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
  117. package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
  118. package/examples/dashboard/src/DevicePanel.tsx +92 -11
  119. package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
  120. package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
  121. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  122. package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
  123. package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
  124. package/examples/dashboard/src/LoggingPanel.tsx +264 -0
  125. package/examples/dashboard/src/Main.tsx +12 -2
  126. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  127. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
  128. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  129. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  130. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
  131. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
  132. package/examples/dashboard/src/styles.css +766 -1
  133. package/notes/web-ble-reconnect-retry.md +69 -0
  134. package/package.json +10 -7
  135. package/src/PseudocodeTranspiler.test.ts +372 -0
  136. package/src/PseudocodeTranspiler.ts +127 -0
  137. package/src/RaftAttributeHandler.ts +152 -76
  138. package/src/RaftChannelBLE.web.ts +62 -20
  139. package/src/RaftChannelSimulated.ts +10 -5
  140. package/src/RaftChannelWebSocket.ts +16 -2
  141. package/src/RaftConnector.ts +93 -15
  142. package/src/RaftCustomAttrHandler.ts +35 -45
  143. package/src/RaftDeviceInfo.ts +27 -0
  144. package/src/RaftDeviceManager.test.ts +164 -0
  145. package/src/RaftDeviceManager.ts +705 -127
  146. package/src/RaftDeviceMgrIF.ts +13 -2
  147. package/src/RaftDeviceStates.ts +49 -8
  148. package/src/RaftFileHandler.ts +69 -28
  149. package/src/RaftPublish.ts +92 -0
  150. package/src/RaftStreamHandler.ts +84 -1
  151. package/src/RaftStruct.test.ts +229 -0
  152. package/src/RaftStruct.ts +101 -37
  153. package/src/RaftSystemType.ts +1 -0
  154. package/src/RaftSystemUtils.ts +59 -0
  155. package/src/RaftTimezone.ts +151 -0
  156. package/src/RaftTypes.ts +34 -1
  157. package/src/RaftUpdateManager.ts +1 -1
  158. package/src/main.ts +2 -0
@@ -1,10 +1,20 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
2
  import ConnManager from './ConnManager';
3
- import { DeviceTypeAction } from '../../../src/RaftDeviceInfo';
3
+ import { DeviceTypeAction, ActionMapEntry } from '../../../src/RaftDeviceInfo';
4
4
  import DispLEDGrid from './DispLedGrid';
5
5
 
6
6
  const connManager = ConnManager.getInstance();
7
7
 
8
+ // Generic sample rate options for devices without _conf.rate
9
+ const GENERIC_SAMPLE_RATES = [50, 20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.01, 0.001];
10
+
11
+ // Find the closest value in an array to a target
12
+ function findClosest(arr: number[], target: number): number {
13
+ return arr.reduce((prev, curr) =>
14
+ Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev
15
+ );
16
+ }
17
+
8
18
  type DeviceActionsTableProps = {
9
19
  deviceKey: string;
10
20
  };
@@ -17,18 +27,29 @@ const DeviceActionsForm: React.FC<DeviceActionsTableProps> = ({ deviceKey }: Dev
17
27
  const deviceManager = connManager.getConnector().getSystemType()?.deviceMgrIF;
18
28
  const [deviceActions, setDeviceActions] = useState<DeviceTypeAction[]>([]);
19
29
  const [inputValues, setInputValues] = useState<InputValues>({});
30
+ const [actionStatus, setActionStatus] = useState<string>('');
31
+ const [genericRateHz, setGenericRateHz] = useState<number>(10);
32
+ const [isBusDevice, setIsBusDevice] = useState<boolean>(false);
33
+ const [hasConfRate, setHasConfRate] = useState<boolean>(false);
20
34
 
21
35
  useEffect(() => {
22
36
  if (!deviceManager) {
23
37
  return;
24
38
  }
25
39
  // Wait a little while inline for the device to be ready
26
- setTimeout(() => {
40
+ setTimeout(async () => {
27
41
  const deviceState = deviceManager.getDeviceState(deviceKey);
28
42
  const { deviceTypeInfo } = deviceState;
29
43
  const actions: DeviceTypeAction[] = deviceTypeInfo?.actions || [];
30
44
  setDeviceActions(actions);
31
- // Initialize input values
45
+ // Check if this is a bus device (has a valid busName)
46
+ const busName = deviceState?.busName ?? '';
47
+ const isBus = busName !== '' && busName !== '0';
48
+ setIsBusDevice(isBus);
49
+ // Check if device has _conf.rate action
50
+ const confRateAction = actions.find(a => a.n === '_conf.rate');
51
+ setHasConfRate(!!confRateAction);
52
+ // Initialize input values with defaults
32
53
  const initialValues: InputValues = actions.reduce((acc, action) => {
33
54
  acc[action.n] =
34
55
  action.d ??
@@ -39,6 +60,47 @@ const DeviceActionsForm: React.FC<DeviceActionsTableProps> = ({ deviceKey }: Dev
39
60
  : 0);
40
61
  return acc;
41
62
  }, {} as InputValues);
63
+
64
+ // Query current poll config from firmware to initialize rate dropdowns
65
+ if (isBus && deviceState?.deviceAddress) {
66
+ try {
67
+ const cmd = `devman/devconfig?bus=${busName}&addr=${deviceState.deviceAddress}`;
68
+ const resp = await connManager.getConnector().sendRICRESTMsg(cmd, {}) as any;
69
+ if (resp?.rslt === 'ok' && resp.pollIntervalUs > 0) {
70
+ const currentRateHz = 1000000 / resp.pollIntervalUs;
71
+ if (confRateAction?.map) {
72
+ // For _conf.rate: find the map key whose interval and numSamples
73
+ // best match the current config (both i and s needed to disambiguate
74
+ // e.g. 52Hz and 104Hz both use i=50000 but differ in s)
75
+ let bestKey = String(confRateAction.d ?? '');
76
+ let bestDist = Infinity;
77
+ for (const [key, entry] of Object.entries(confRateAction.map)) {
78
+ const mapEntry = entry as ActionMapEntry;
79
+ if (mapEntry.i !== undefined) {
80
+ let dist = Math.abs(mapEntry.i - resp.pollIntervalUs);
81
+ // Add penalty for numSamples mismatch to disambiguate entries with same interval
82
+ if (mapEntry.s !== undefined && resp.numSamples !== undefined) {
83
+ dist += Math.abs(mapEntry.s - resp.numSamples) * 1000000;
84
+ }
85
+ if (dist < bestDist) {
86
+ bestDist = dist;
87
+ bestKey = key;
88
+ }
89
+ }
90
+ }
91
+ if (bestKey) {
92
+ initialValues['_conf.rate'] = parseFloat(bestKey);
93
+ }
94
+ } else {
95
+ // For generic rate dropdown: find closest option
96
+ setGenericRateHz(findClosest(GENERIC_SAMPLE_RATES, currentRateHz));
97
+ }
98
+ }
99
+ } catch (err) {
100
+ // Ignore query errors — keep defaults
101
+ }
102
+ }
103
+
42
104
  setInputValues(initialValues);
43
105
  }, 1000);
44
106
  }, [deviceKey]);
@@ -50,15 +112,44 @@ const DeviceActionsForm: React.FC<DeviceActionsTableProps> = ({ deviceKey }: Dev
50
112
  }));
51
113
  };
52
114
 
53
- const handleSendAction = (action: DeviceTypeAction, value: number) => {
115
+ const handleSendAction = async (action: DeviceTypeAction, value: number) => {
54
116
  // Send action to device
55
117
  if (!deviceManager) {
56
118
  return;
57
119
  }
58
- deviceManager.sendAction(deviceKey, action, [value]);
120
+ // For _conf.rate actions, use setSampleRate for coordinated polling config
121
+ if (action.n === '_conf.rate' && action.map) {
122
+ setActionStatus('Setting sample rate...');
123
+ const result = await deviceManager.setSampleRate(deviceKey, value);
124
+ if (result.ok) {
125
+ setActionStatus(`Rate: ${result.actualRateHz} Hz, poll: ${result.intervalUs} µs, buf: ${result.numSamples}`);
126
+ } else {
127
+ setActionStatus(`Error: ${result.error}`);
128
+ }
129
+ setTimeout(() => setActionStatus(''), 5000);
130
+ } else {
131
+ deviceManager.sendAction(deviceKey, action, [value]);
132
+ }
59
133
  };
60
134
 
61
- if (deviceActions.length === 0) {
135
+ const handleGenericRateSend = async () => {
136
+ if (!deviceManager) {
137
+ return;
138
+ }
139
+ setActionStatus('Setting sample rate...');
140
+ const result = await deviceManager.setSampleRate(deviceKey, genericRateHz);
141
+ if (result.ok) {
142
+ setActionStatus(`Rate: ${result.actualRateHz} Hz, poll: ${result.intervalUs} µs, buf: ${result.numSamples}`);
143
+ } else {
144
+ setActionStatus(`Error: ${result.error}`);
145
+ }
146
+ setTimeout(() => setActionStatus(''), 5000);
147
+ };
148
+
149
+ // Show generic rate control for bus devices without _conf.rate
150
+ const showGenericRate = isBusDevice && !hasConfRate;
151
+
152
+ if (deviceActions.length === 0 && !showGenericRate) {
62
153
  return <></>;
63
154
  }
64
155
 
@@ -80,14 +171,52 @@ const DeviceActionsForm: React.FC<DeviceActionsTableProps> = ({ deviceKey }: Dev
80
171
  <td>{action.n}</td>
81
172
  <td colSpan={2}>
82
173
  <DispLEDGrid
83
- rows={action.NY || 1}
84
- cols={action.NX || 1}
174
+ rows={action.NY ?? 1}
175
+ cols={action.NX ?? 1}
85
176
  deviceKey={deviceKey}
86
177
  deviceAction={action}
87
178
  />
88
179
  </td>
89
180
  </tr>
90
181
  );
182
+ } else if (action.map) {
183
+ const mapKeys = Object.keys(action.map).sort((a, b) => parseFloat(a) - parseFloat(b));
184
+ // Use "Rate Hz" label for _conf.rate actions
185
+ const actionLabel = action.n === '_conf.rate' ? 'Rate Hz' : (action.desc ?? action.n);
186
+ return (
187
+ <tr key={action.n}>
188
+ <td>{actionLabel}</td>
189
+ <td>
190
+ <select
191
+ value={inputValues[action.n]}
192
+ onChange={(e) =>
193
+ handleInputChange(
194
+ action.n,
195
+ parseFloat(e.target.value)
196
+ )
197
+ }
198
+ >
199
+ {mapKeys.map((key) => (
200
+ <option key={key} value={parseFloat(key)}>
201
+ {key}
202
+ </option>
203
+ ))}
204
+ </select>
205
+ </td>
206
+ <td>
207
+ <button
208
+ onClick={() =>
209
+ handleSendAction(
210
+ action,
211
+ inputValues[action.n]
212
+ )
213
+ }
214
+ >
215
+ Send
216
+ </button>
217
+ </td>
218
+ </tr>
219
+ );
91
220
  } else {
92
221
  return (
93
222
  <tr key={action.n}>
@@ -124,8 +253,29 @@ const DeviceActionsForm: React.FC<DeviceActionsTableProps> = ({ deviceKey }: Dev
124
253
  );
125
254
  }
126
255
  })}
256
+ {showGenericRate && (
257
+ <tr key="__generic_rate">
258
+ <td>Rate Hz</td>
259
+ <td>
260
+ <select
261
+ value={genericRateHz}
262
+ onChange={(e) => setGenericRateHz(parseFloat(e.target.value))}
263
+ >
264
+ {GENERIC_SAMPLE_RATES.map((rate) => (
265
+ <option key={rate} value={rate}>
266
+ {rate >= 1 ? `${rate} Hz` : `${rate} Hz`}
267
+ </option>
268
+ ))}
269
+ </select>
270
+ </td>
271
+ <td>
272
+ <button onClick={handleGenericRateSend}>Send</button>
273
+ </td>
274
+ </tr>
275
+ )}
127
276
  </tbody>
128
277
  </table>
278
+ {actionStatus && <div className="action-status">{actionStatus}</div>}
129
279
  </div>
130
280
  );
131
281
  };
@@ -47,13 +47,15 @@ const DeviceLineChart: React.FC<DeviceLineChartProps> = memo(({ deviceKey, lastU
47
47
  datasets: []
48
48
  });
49
49
 
50
+ const [chartScales, setChartScales] = useState<{ [key: string]: any }>({});
51
+
50
52
  const options = {
51
53
  responsive: true,
52
54
  maintainAspectRatio: false,
53
55
  animation: {
54
56
  duration: 1, // default is 1000ms
55
57
  },
56
- scales: {}
58
+ scales: chartScales
57
59
  };
58
60
 
59
61
  const colourMapRef = useRef<{ [key: string]: string }>({
@@ -100,7 +102,12 @@ const DeviceLineChart: React.FC<DeviceLineChartProps> = memo(({ deviceKey, lastU
100
102
 
101
103
  const uniqueAxes = new Map<string, { range: [number, number], units: string }>();
102
104
  const datasets = Object.entries(deviceState.deviceAttributes)
103
- .filter(([attributeName, attributeDetails]) => attributeDetails.visibleSeries !== false)
105
+ .filter(([attributeName, attributeDetails]) => {
106
+ if (attributeDetails.visibleSeries === false) return false;
107
+ // Exclude string-valued attributes from chart
108
+ if (attributeDetails.values.length > 0 && typeof attributeDetails.values[attributeDetails.values.length - 1] === 'string') return false;
109
+ return true;
110
+ })
104
111
  .map(([attributeName, attributeDetails]) => {
105
112
  const data = attributeDetails.values.slice(-maxChartDataPoints);
106
113
  let colour = colourMapRef.current[attributeName];
@@ -140,12 +147,18 @@ const DeviceLineChart: React.FC<DeviceLineChartProps> = memo(({ deviceKey, lastU
140
147
  ticks: {
141
148
  min: axis.range[0],
142
149
  max: axis.range[1],
150
+ callback: (value: number) => {
151
+ if (typeof value === 'number' && Math.abs(value) > 0 && Math.abs(value) < 1) {
152
+ return value.toPrecision(2);
153
+ }
154
+ return value;
155
+ },
143
156
  },
144
157
  };
145
158
  });
146
159
 
147
160
  // Update options and chart data
148
- options.scales = scales;
161
+ setChartScales(scales);
149
162
  setChartData({ labels: labels.length ? labels : ['0.000'], datasets });
150
163
  }, [lastUpdated]);
151
164
 
@@ -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
+ }