@robdobsn/raftjs 1.10.7 → 1.11.5

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 (75) hide show
  1. package/dist/react-native/RaftChannelSimulated.js +4 -3
  2. package/dist/react-native/RaftChannelSimulated.js.map +1 -1
  3. package/dist/react-native/RaftConnector.d.ts +10 -1
  4. package/dist/react-native/RaftConnector.js +23 -10
  5. package/dist/react-native/RaftConnector.js.map +1 -1
  6. package/dist/react-native/RaftDeviceManager.d.ts +13 -1
  7. package/dist/react-native/RaftDeviceManager.js +224 -77
  8. package/dist/react-native/RaftDeviceManager.js.map +1 -1
  9. package/dist/react-native/RaftDeviceMgrIF.d.ts +5 -1
  10. package/dist/react-native/RaftDeviceStates.d.ts +20 -2
  11. package/dist/react-native/RaftDeviceStates.js +25 -4
  12. package/dist/react-native/RaftDeviceStates.js.map +1 -1
  13. package/dist/react-native/RaftPublish.d.ts +2 -0
  14. package/dist/react-native/RaftPublish.js +81 -0
  15. package/dist/react-native/RaftPublish.js.map +1 -0
  16. package/dist/react-native/RaftStreamHandler.d.ts +11 -0
  17. package/dist/react-native/RaftStreamHandler.js +66 -0
  18. package/dist/react-native/RaftStreamHandler.js.map +1 -1
  19. package/dist/react-native/RaftSystemUtils.d.ts +17 -1
  20. package/dist/react-native/RaftSystemUtils.js +51 -0
  21. package/dist/react-native/RaftSystemUtils.js.map +1 -1
  22. package/dist/react-native/RaftTypes.d.ts +21 -0
  23. package/dist/react-native/RaftTypes.js.map +1 -1
  24. package/dist/react-native/main.d.ts +1 -0
  25. package/dist/react-native/main.js +1 -0
  26. package/dist/react-native/main.js.map +1 -1
  27. package/dist/web/RaftChannelSimulated.js +4 -3
  28. package/dist/web/RaftChannelSimulated.js.map +1 -1
  29. package/dist/web/RaftConnector.d.ts +10 -1
  30. package/dist/web/RaftConnector.js +23 -10
  31. package/dist/web/RaftConnector.js.map +1 -1
  32. package/dist/web/RaftDeviceManager.d.ts +13 -1
  33. package/dist/web/RaftDeviceManager.js +224 -77
  34. package/dist/web/RaftDeviceManager.js.map +1 -1
  35. package/dist/web/RaftDeviceMgrIF.d.ts +5 -1
  36. package/dist/web/RaftDeviceStates.d.ts +20 -2
  37. package/dist/web/RaftDeviceStates.js +25 -4
  38. package/dist/web/RaftDeviceStates.js.map +1 -1
  39. package/dist/web/RaftPublish.d.ts +2 -0
  40. package/dist/web/RaftPublish.js +81 -0
  41. package/dist/web/RaftPublish.js.map +1 -0
  42. package/dist/web/RaftStreamHandler.d.ts +11 -0
  43. package/dist/web/RaftStreamHandler.js +66 -0
  44. package/dist/web/RaftStreamHandler.js.map +1 -1
  45. package/dist/web/RaftSystemUtils.d.ts +17 -1
  46. package/dist/web/RaftSystemUtils.js +51 -0
  47. package/dist/web/RaftSystemUtils.js.map +1 -1
  48. package/dist/web/RaftTypes.d.ts +21 -0
  49. package/dist/web/RaftTypes.js.map +1 -1
  50. package/dist/web/main.d.ts +1 -0
  51. package/dist/web/main.js +1 -0
  52. package/dist/web/main.js.map +1 -1
  53. package/examples/dashboard/package.json +1 -1
  54. package/examples/dashboard/src/DeviceActionsForm.tsx +2 -2
  55. package/examples/dashboard/src/DevicePanel.tsx +79 -3
  56. package/examples/dashboard/src/DeviceStatsPanel.tsx +65 -0
  57. package/examples/dashboard/src/DevicesPanel.tsx +11 -0
  58. package/examples/dashboard/src/SettingsScreen.tsx +9 -4
  59. package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -2
  60. package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
  61. package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
  62. package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +38 -4
  63. package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +10 -2
  64. package/examples/dashboard/src/styles.css +162 -0
  65. package/package.json +49 -49
  66. package/src/RaftChannelSimulated.ts +4 -3
  67. package/src/RaftConnector.ts +34 -13
  68. package/src/RaftDeviceManager.ts +251 -81
  69. package/src/RaftDeviceMgrIF.ts +5 -1
  70. package/src/RaftDeviceStates.ts +35 -5
  71. package/src/RaftPublish.ts +92 -0
  72. package/src/RaftStreamHandler.ts +84 -1
  73. package/src/RaftSystemUtils.ts +59 -0
  74. package/src/RaftTypes.ts +27 -0
  75. package/src/main.ts +1 -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;
@@ -140,7 +173,7 @@ const DevicePanel = ({ deviceKey, lastUpdated }: DevicePanelProps) => {
140
173
  if (bracketsAdded) {
141
174
  headerText += `)`;
142
175
  }
143
- if (!deviceState?.isOnline) {
176
+ if (deviceState?.onlineState !== DeviceOnlineState.Online) {
144
177
  headerText += " (Offline)";
145
178
  }
146
179
 
@@ -152,14 +185,57 @@ const DevicePanel = ({ deviceKey, lastUpdated }: DevicePanelProps) => {
152
185
  {menuOpen && (
153
186
  <div className="dropdown-menu" ref={menuRef}>
154
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>
155
199
  </div>
156
200
  )}
157
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
+ )}
158
231
  <div className={`device-block-data`}>
159
232
  <div className="device-attrs-and-actions">
160
233
  <DeviceAttrsForm deviceKey={deviceKey} lastUpdated={lastUpdated} />
161
234
  <DeviceActionsForm deviceKey={deviceKey} />
162
235
  </div>
236
+ {showStats && (
237
+ <DeviceStatsPanel deviceKey={deviceKey} lastUpdated={timedChartUpdate} />
238
+ )}
163
239
  {showCharts &&
164
240
  <DeviceLineChart deviceKey={deviceKey} lastUpdated={timedChartUpdate} />
165
241
  }
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import ConnManager from './ConnManager';
3
+ import './styles.css';
4
+
5
+ const connManager = ConnManager.getInstance();
6
+
7
+ export interface DeviceStatsPanelProps {
8
+ deviceKey: string;
9
+ lastUpdated: number;
10
+ }
11
+
12
+ const DeviceStatsPanel: React.FC<DeviceStatsPanelProps> = ({ deviceKey, lastUpdated }: DeviceStatsPanelProps) => {
13
+ const deviceManager = connManager.getConnector().getSystemType()?.deviceMgrIF;
14
+ const stats = deviceManager?.getDeviceStats(deviceKey);
15
+
16
+ if (!stats) {
17
+ return <></>;
18
+ }
19
+
20
+ const windowSeconds = stats.windowMs / 1000;
21
+ const sampleRateHz = Number.isFinite(stats.sampleRateHz) ? stats.sampleRateHz : 0;
22
+ const nowMs = lastUpdated || Date.now();
23
+ const lastSampleAgeSec = stats.lastSampleTimeMs
24
+ ? (nowMs - stats.lastSampleTimeMs) / 1000
25
+ : null;
26
+
27
+ const handleReset = () => {
28
+ deviceManager?.resetDeviceStats(deviceKey);
29
+ };
30
+
31
+ return (
32
+ <div className="device-stats-panel">
33
+ <div className="device-stats-header">
34
+ <span>Sampling Stats</span>
35
+ <button className="device-stats-reset" onClick={handleReset}>Reset Samples</button>
36
+ </div>
37
+ <div className="device-stats-grid">
38
+ <div className="device-stats-item">
39
+ <div className="device-stats-label">Sample Rate</div>
40
+ <div className="device-stats-value">{sampleRateHz.toFixed(2)} Hz</div>
41
+ </div>
42
+ <div className="device-stats-item">
43
+ <div className="device-stats-label">Window</div>
44
+ <div className="device-stats-value">{windowSeconds.toFixed(1)} s</div>
45
+ </div>
46
+ <div className="device-stats-item">
47
+ <div className="device-stats-label">Window Samples</div>
48
+ <div className="device-stats-value">{stats.windowSamples}</div>
49
+ </div>
50
+ <div className="device-stats-item">
51
+ <div className="device-stats-label">Total Samples</div>
52
+ <div className="device-stats-value">{stats.totalSamples}</div>
53
+ </div>
54
+ <div className="device-stats-item">
55
+ <div className="device-stats-label">Last Sample</div>
56
+ <div className="device-stats-value">
57
+ {lastSampleAgeSec === null ? 'N/A' : `${lastSampleAgeSec.toFixed(1)} s ago`}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ 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;
@@ -24,9 +24,14 @@ const SettingsScreen = ({ onBack }: { onBack: () => void }) => {
24
24
  settingsManager.getSetting('latencyAttributeName') || 'amb0'
25
25
  );
26
26
  const [latencyChangeThreshold, setLatencyChangeThreshold] = useState<number>(
27
- settingsManager.getSetting('latencyChangeThreshold') || 100
27
+ settingsManager.getSetting('latencyChangeThreshold') ?? 100
28
28
  );
29
29
 
30
+ const parseIntOrDefault = (value: string, fallback: number): number => {
31
+ const parsed = Number.parseInt(value, 10);
32
+ return Number.isNaN(parsed) ? fallback : parsed;
33
+ };
34
+
30
35
  const handleSaveAndReturn = () => {
31
36
  // Save settings to SettingsManager
32
37
  settingsManager.setSetting('latencyTest', latencyTest);
@@ -83,7 +88,7 @@ const SettingsScreen = ({ onBack }: { onBack: () => void }) => {
83
88
  value={maxChartDataPoints}
84
89
  onChange={(e) =>
85
90
  setMaxChartDataPoints(
86
- Math.min(parseInt(e.target.value, 10) || 1, 500)
91
+ Math.min(parseIntOrDefault(e.target.value, 1), 500)
87
92
  )
88
93
  }
89
94
  style={{ width: '50px', marginLeft: '10px' }}
@@ -101,7 +106,7 @@ const SettingsScreen = ({ onBack }: { onBack: () => void }) => {
101
106
  value={maxDatapointsToStore}
102
107
  onChange={(e) =>
103
108
  setMaxDatapointsToStore(
104
- Math.min(parseInt(e.target.value, 10) || 1, 100000)
109
+ Math.min(parseIntOrDefault(e.target.value, 1), 100000)
105
110
  )
106
111
  }
107
112
  style={{ width: '50px', marginLeft: '10px' }}
@@ -141,7 +146,7 @@ const SettingsScreen = ({ onBack }: { onBack: () => void }) => {
141
146
  min="1"
142
147
  value={latencyChangeThreshold}
143
148
  onChange={(e) =>
144
- setLatencyChangeThreshold(parseInt(e.target.value, 10) || 1)
149
+ setLatencyChangeThreshold(parseIntOrDefault(e.target.value, 1))
145
150
  }
146
151
  style={{ width: '60px', marginLeft: '10px' }}
147
152
  />
@@ -46,6 +46,14 @@ export class CogStateInfo {
46
46
  return this._deviceManager;
47
47
  }
48
48
 
49
+ async handleBinaryPayload(rxMsg: Uint8Array): Promise<void> {
50
+ await this._deviceManager.handleClientMsgBinary(rxMsg);
51
+ }
52
+
53
+ async handleJsonPayload(jsonString: string): Promise<void> {
54
+ await this._deviceManager.handleClientMsgJson(jsonString);
55
+ }
56
+
49
57
  updateFromMsg(rxMsg: Uint8Array, frameTimeMs: number, isBinary: boolean): Array<string> {
50
58
 
51
59
  // Debug
@@ -53,14 +61,14 @@ export class CogStateInfo {
53
61
 
54
62
  if (isBinary) {
55
63
  // console.log(`CogStateInfo: updateFromMsg: ${RaftUtils.bufferToHex(rxMsg)}`);
56
- this._deviceManager.handleClientMsgBinary(rxMsg);
64
+ this.handleBinaryPayload(rxMsg);
57
65
  } else {
58
66
  // Convert Uint8Array to string
59
67
  const decoder = new TextDecoder('utf-8');
60
68
  const jsonString = decoder.decode(rxMsg.slice(2));
61
69
 
62
70
  // Handle using device manager
63
- this._deviceManager.handleClientMsgJson(jsonString);
71
+ this.handleJsonPayload(jsonString);
64
72
  }
65
73
 
66
74
  // // Debug
@@ -1,5 +1,5 @@
1
1
  import { RaftSubscribeForUpdatesCBType, RaftSystemType } from "../../../../src/RaftSystemType";
2
- import { RaftEventFn, RaftLog, RaftOKFail, RaftPublishEvent, RaftPublishEventNames, RaftSystemUtils } from "../../../../src/main";
2
+ import { inspectPublishFrame, RaftEventFn, RaftLog, RaftPublishEvent, RaftPublishEventNames, RaftSubscriptionUpdateResponse, RaftSystemUtils } from "../../../../src/main";
3
3
  import { CogStateInfo } from "./CogStateInfo";
4
4
  import { DeviceManager } from "../../../../src/RaftDeviceManager";
5
5
 
@@ -53,10 +53,16 @@ export default class SystemTypeCog implements RaftSystemType {
53
53
  ']}';
54
54
 
55
55
  const msgHandler = systemUtils.getMsgHandler();
56
- const ricResp = await msgHandler.sendRICRESTCmdFrame<RaftOKFail>(
56
+ const ricResp = await msgHandler.sendRICRESTCmdFrame<RaftSubscriptionUpdateResponse>(
57
57
  enable ? subscribeEnable : subscribeDisable
58
58
  );
59
59
 
60
+ // Cache topic index->name map from response, then refresh from pubtopics endpoint when enabling
61
+ systemUtils.updatePublishTopicMapFromSubscriptionResponse(ricResp);
62
+ if (enable) {
63
+ await systemUtils.refreshPublishTopicMap();
64
+ }
65
+
60
66
  // Debug
61
67
  RaftLog.debug(`subscribe enable/disable returned ${JSON.stringify(ricResp)}`);
62
68
  } catch (error: unknown) {
@@ -72,13 +78,41 @@ export default class SystemTypeCog implements RaftSystemType {
72
78
 
73
79
  // RICLog.debug(`rxOtherMsgType payload ${RaftUtils.bufferToHex(payload)}`);
74
80
  RaftLog.verbose(`rxOtherMsgType payloadLen ${payload.length}`);
75
- const topicIDs = this._stateInfo.updateFromMsg(payload, frameTimeMs, SUBSCRIBE_BINARY_MSGS);
81
+
82
+ const frameMeta = inspectPublishFrame(payload, (idx) => this._systemUtils?.getPublishTopicName(idx));
83
+ let handledByDeviceManager = false;
84
+
85
+ if (frameMeta.frameType === "binary") {
86
+ if (frameMeta.binaryHasEnvelope) {
87
+ if (frameMeta.topicName === "devbin") {
88
+ this._stateInfo.handleBinaryPayload(payload);
89
+ handledByDeviceManager = true;
90
+ }
91
+ } else if (SUBSCRIBE_BINARY_MSGS) {
92
+ this._stateInfo.handleBinaryPayload(payload);
93
+ handledByDeviceManager = true;
94
+ }
95
+ } else if (frameMeta.frameType === "json") {
96
+ if (frameMeta.topicName === "devjson" || frameMeta.topicName === undefined) {
97
+ if (frameMeta.jsonString !== undefined) {
98
+ this._stateInfo.handleJsonPayload(frameMeta.jsonString);
99
+ handledByDeviceManager = true;
100
+ }
101
+ }
102
+ }
103
+
104
+ const topicIDs = frameMeta.topicIndex !== undefined ? [frameMeta.topicIndex.toString()] : [];
76
105
 
77
106
  // Call event handler if registered
78
107
  if (this._onEvent) {
79
108
  this._onEvent("pub", RaftPublishEvent.PUBLISH_EVENT_DATA, RaftPublishEventNames[RaftPublishEvent.PUBLISH_EVENT_DATA],
80
109
  {
81
110
  topicIDs: topicIDs,
111
+ topicName: frameMeta.topicName,
112
+ topicIndex: frameMeta.topicIndex,
113
+ topicVersion: frameMeta.version,
114
+ frameType: frameMeta.frameType,
115
+ handledByDeviceManager,
82
116
  payload: payload,
83
117
  frameTimeMs: frameTimeMs,
84
118
  isBinary: SUBSCRIBE_BINARY_MSGS
@@ -7,6 +7,14 @@ export class StateInfoGeneric {
7
7
  public constructor(private _deviceManager: DeviceManager) {
8
8
  }
9
9
 
10
+ async handleBinaryPayload(rxMsg: Uint8Array): Promise<void> {
11
+ await this._deviceManager.handleClientMsgBinary(rxMsg);
12
+ }
13
+
14
+ async handleJsonPayload(jsonString: string): Promise<void> {
15
+ await this._deviceManager.handleClientMsgJson(jsonString);
16
+ }
17
+
10
18
  updateFromMsg(rxMsg: Uint8Array, frameTimeMs: number, isBinary: boolean): Array<string> {
11
19
 
12
20
  // Debug
@@ -15,14 +23,14 @@ export class StateInfoGeneric {
15
23
  // Handle binary or JSON
16
24
  if (isBinary) {
17
25
  // Handle using device manager
18
- this._deviceManager.handleClientMsgBinary(rxMsg);
26
+ this.handleBinaryPayload(rxMsg);
19
27
  } else {
20
28
  // Convert Uint8Array to string
21
29
  const decoder = new TextDecoder('utf-8');
22
30
  const jsonString = decoder.decode(rxMsg.slice(2));
23
31
 
24
32
  // Handle using device manager
25
- this._deviceManager.handleClientMsgJson(jsonString);
33
+ this.handleJsonPayload(jsonString);
26
34
  }
27
35
  return [];
28
36
 
@@ -1,9 +1,9 @@
1
1
  import { RaftSubscribeForUpdatesCBType, RaftSystemType } from "../../../../src/RaftSystemType";
2
- import { RaftEventFn, RaftLog, RaftOKFail, RaftPublishEvent, RaftPublishEventNames, RaftSystemUtils } from "../../../../src/main";
2
+ import { inspectPublishFrame, RaftEventFn, RaftLog, RaftPublishEvent, RaftPublishEventNames, RaftSubscriptionUpdateResponse, RaftSystemUtils } from "../../../../src/main";
3
3
  import { StateInfoGeneric } from "./StateInfoGeneric";
4
4
  import { DeviceManager } from "../../../../src/RaftDeviceManager";
5
5
 
6
- const SUBSCRIBE_BINARY_MSGS = false;
6
+ const SUBSCRIBE_BINARY_MSGS = true;
7
7
 
8
8
  export default class SystemTypeGeneric implements RaftSystemType {
9
9
  nameForDialogs = "Generic System";
@@ -53,10 +53,16 @@ export default class SystemTypeGeneric implements RaftSystemType {
53
53
  ']}';
54
54
 
55
55
  const msgHandler = systemUtils.getMsgHandler();
56
- const ricResp = await msgHandler.sendRICRESTCmdFrame<RaftOKFail>(
56
+ const ricResp = await msgHandler.sendRICRESTCmdFrame<RaftSubscriptionUpdateResponse>(
57
57
  enable ? subscribeEnable : subscribeDisable
58
58
  );
59
59
 
60
+ // Cache topic index->name map from response, then refresh from pubtopics endpoint when enabling
61
+ systemUtils.updatePublishTopicMapFromSubscriptionResponse(ricResp);
62
+ if (enable) {
63
+ await systemUtils.refreshPublishTopicMap();
64
+ }
65
+
60
66
  // Debug
61
67
  RaftLog.debug(`subscribe enable/disable returned ${JSON.stringify(ricResp)}`);
62
68
  } catch (error: unknown) {
@@ -72,13 +78,41 @@ export default class SystemTypeGeneric implements RaftSystemType {
72
78
 
73
79
  // RICLog.debug(`rxOtherMsgType payload ${RaftUtils.bufferToHex(payload)}`);
74
80
  RaftLog.verbose(`rxOtherMsgType payloadLen ${payload.length}`);
75
- const topicIDs = this._stateInfo.updateFromMsg(payload, frameTimeMs, SUBSCRIBE_BINARY_MSGS);
81
+
82
+ const frameMeta = inspectPublishFrame(payload, (idx) => this._systemUtils?.getPublishTopicName(idx));
83
+ let handledByDeviceManager = false;
84
+
85
+ if (frameMeta.frameType === "binary") {
86
+ if (frameMeta.binaryHasEnvelope) {
87
+ if (frameMeta.topicName === "devbin") {
88
+ this._stateInfo.handleBinaryPayload(payload);
89
+ handledByDeviceManager = true;
90
+ }
91
+ } else if (SUBSCRIBE_BINARY_MSGS) {
92
+ this._stateInfo.handleBinaryPayload(payload);
93
+ handledByDeviceManager = true;
94
+ }
95
+ } else if (frameMeta.frameType === "json") {
96
+ if (frameMeta.topicName === "devjson" || frameMeta.topicName === undefined) {
97
+ if (frameMeta.jsonString !== undefined) {
98
+ this._stateInfo.handleJsonPayload(frameMeta.jsonString);
99
+ handledByDeviceManager = true;
100
+ }
101
+ }
102
+ }
103
+
104
+ const topicIDs = frameMeta.topicIndex !== undefined ? [frameMeta.topicIndex.toString()] : [];
76
105
 
77
106
  // Call event handler if registered
78
107
  if (this._onEvent) {
79
108
  this._onEvent("pub", RaftPublishEvent.PUBLISH_EVENT_DATA, RaftPublishEventNames[RaftPublishEvent.PUBLISH_EVENT_DATA],
80
109
  {
81
110
  topicIDs: topicIDs,
111
+ topicName: frameMeta.topicName,
112
+ topicIndex: frameMeta.topicIndex,
113
+ topicVersion: frameMeta.version,
114
+ frameType: frameMeta.frameType,
115
+ handledByDeviceManager,
82
116
  payload: payload,
83
117
  frameTimeMs: frameTimeMs,
84
118
  isBinary: SUBSCRIBE_BINARY_MSGS
@@ -1,5 +1,5 @@
1
1
  import RaftDeviceMgrIF from "../../../../src/RaftDeviceMgrIF";
2
- import { DeviceAttributeState, DevicesState, DeviceState } from "../../../../src/RaftDeviceStates";
2
+ import { DeviceAttributeState, DevicesState, DeviceState, DeviceOnlineState } from '../../../../src/RaftDeviceStates';
3
3
  import { RICSERIAL_PAYLOAD_POS } from "../../../../src/RaftProtocolDefs";
4
4
  import RICAddOnManager from "./RICAddOnManager";
5
5
  import RICCommsStats from "./RICCommsStats";
@@ -50,7 +50,7 @@ export class RICStateInfo implements RaftDeviceMgrIF {
50
50
  deviceAttributes: {},
51
51
  deviceIsNew: false,
52
52
  stateChanged: false,
53
- isOnline: false,
53
+ onlineState: DeviceOnlineState.Offline,
54
54
  deviceAddress: "",
55
55
  deviceType: "",
56
56
  busName: ""
@@ -85,6 +85,14 @@ export class RICStateInfo implements RaftDeviceMgrIF {
85
85
  // TODO - implement if RICStateInfo is to be used as a DeviceMgr
86
86
  }
87
87
 
88
+ addDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
89
+ // TODO - implement if RICStateInfo is to be used as a DeviceMgr
90
+ }
91
+
92
+ removeDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
93
+ // TODO - implement if RICStateInfo is to be used as a DeviceMgr
94
+ }
95
+
88
96
  sendAction(deviceKey: string, action: any, data: any): void {
89
97
  // TODO - implement if RICStateInfo is to be used as a DeviceMgr
90
98
  }
@@ -381,6 +381,168 @@ h1 {
381
381
  background-color: #555;
382
382
  }
383
383
 
384
+ .menu-item-toggle {
385
+ padding: 6px 10px;
386
+ }
387
+
388
+ .menu-toggle {
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 8px;
392
+ cursor: pointer;
393
+ }
394
+
395
+ .menu-toggle input {
396
+ margin: 0;
397
+ }
398
+
399
+ .device-stats-panel {
400
+ flex: 0 0 220px;
401
+ min-width: 220px;
402
+ padding: 10px;
403
+ border: 1px solid #666;
404
+ border-radius: 4px;
405
+ background: #444;
406
+ display: flex;
407
+ flex-direction: column;
408
+ gap: 10px;
409
+ }
410
+
411
+ .device-stats-header {
412
+ display: flex;
413
+ justify-content: space-between;
414
+ align-items: center;
415
+ font-weight: bold;
416
+ }
417
+
418
+ .device-stats-reset {
419
+ background: #555;
420
+ color: #fff;
421
+ border: none;
422
+ border-radius: 4px;
423
+ padding: 4px 8px;
424
+ cursor: pointer;
425
+ font-size: 0.8em;
426
+ }
427
+
428
+ .device-stats-reset:hover {
429
+ background: #666;
430
+ }
431
+
432
+ .device-stats-grid {
433
+ display: grid;
434
+ grid-template-columns: 1fr;
435
+ gap: 8px;
436
+ }
437
+
438
+ .device-stats-item {
439
+ display: flex;
440
+ justify-content: space-between;
441
+ gap: 10px;
442
+ }
443
+
444
+ .device-stats-label {
445
+ color: #bbb;
446
+ font-size: 0.85em;
447
+ }
448
+
449
+ .device-stats-value {
450
+ font-weight: 600;
451
+ font-size: 0.9em;
452
+ }
453
+
454
+ .poll-rate-dialog-overlay {
455
+ position: fixed;
456
+ inset: 0;
457
+ background: rgba(0, 0, 0, 0.5);
458
+ z-index: 100;
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: center;
462
+ }
463
+
464
+ .poll-rate-dialog {
465
+ background: #333;
466
+ border: 1px solid #666;
467
+ border-radius: 6px;
468
+ padding: 18px 22px;
469
+ min-width: 240px;
470
+ display: flex;
471
+ flex-direction: column;
472
+ gap: 10px;
473
+ color: #fff;
474
+ }
475
+
476
+ .poll-rate-dialog-title {
477
+ font-size: 1em;
478
+ font-weight: bold;
479
+ margin-bottom: 4px;
480
+ }
481
+
482
+ .poll-rate-dialog-row {
483
+ display: flex;
484
+ align-items: center;
485
+ gap: 8px;
486
+ }
487
+
488
+ .poll-rate-input {
489
+ flex: 1;
490
+ background: #222;
491
+ border: 1px solid #666;
492
+ border-radius: 4px;
493
+ color: #fff;
494
+ padding: 6px 8px;
495
+ font-size: 1em;
496
+ width: 100%;
497
+ }
498
+
499
+ .poll-rate-unit {
500
+ color: #aaa;
501
+ font-size: 0.9em;
502
+ }
503
+
504
+ .poll-rate-preview {
505
+ font-size: 0.8em;
506
+ color: #aaa;
507
+ }
508
+
509
+ .poll-rate-status {
510
+ font-size: 0.85em;
511
+ color: #f90;
512
+ }
513
+
514
+ .poll-rate-dialog-buttons {
515
+ display: flex;
516
+ gap: 8px;
517
+ justify-content: flex-end;
518
+ }
519
+
520
+ .poll-rate-btn {
521
+ padding: 6px 14px;
522
+ border: none;
523
+ border-radius: 4px;
524
+ cursor: pointer;
525
+ font-size: 0.9em;
526
+ }
527
+
528
+ .poll-rate-btn-set {
529
+ background: #4a7;
530
+ color: #fff;
531
+ }
532
+
533
+ .poll-rate-btn-set:hover {
534
+ background: #5b8;
535
+ }
536
+
537
+ .poll-rate-btn-cancel {
538
+ background: #555;
539
+ color: #fff;
540
+ }
541
+
542
+ .poll-rate-btn-cancel:hover {
543
+ background: #666;
544
+ }
545
+
384
546
  /* Adjust height for portrait orientation */
385
547
  @media (orientation: portrait) {
386
548
  .device-line-chart {