@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.
- package/dist/react-native/RaftChannelSimulated.js +4 -3
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftConnector.d.ts +10 -1
- package/dist/react-native/RaftConnector.js +23 -10
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftDeviceManager.d.ts +13 -1
- package/dist/react-native/RaftDeviceManager.js +224 -77
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftDeviceMgrIF.d.ts +5 -1
- package/dist/react-native/RaftDeviceStates.d.ts +20 -2
- package/dist/react-native/RaftDeviceStates.js +25 -4
- package/dist/react-native/RaftDeviceStates.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/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/RaftTypes.d.ts +21 -0
- package/dist/react-native/RaftTypes.js.map +1 -1
- package/dist/react-native/main.d.ts +1 -0
- package/dist/react-native/main.js +1 -0
- package/dist/react-native/main.js.map +1 -1
- package/dist/web/RaftChannelSimulated.js +4 -3
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftConnector.d.ts +10 -1
- package/dist/web/RaftConnector.js +23 -10
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftDeviceManager.d.ts +13 -1
- package/dist/web/RaftDeviceManager.js +224 -77
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftDeviceMgrIF.d.ts +5 -1
- package/dist/web/RaftDeviceStates.d.ts +20 -2
- package/dist/web/RaftDeviceStates.js +25 -4
- package/dist/web/RaftDeviceStates.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/RaftSystemUtils.d.ts +17 -1
- package/dist/web/RaftSystemUtils.js +51 -0
- package/dist/web/RaftSystemUtils.js.map +1 -1
- package/dist/web/RaftTypes.d.ts +21 -0
- package/dist/web/RaftTypes.js.map +1 -1
- package/dist/web/main.d.ts +1 -0
- package/dist/web/main.js +1 -0
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +1 -1
- package/examples/dashboard/src/DeviceActionsForm.tsx +2 -2
- package/examples/dashboard/src/DevicePanel.tsx +79 -3
- package/examples/dashboard/src/DeviceStatsPanel.tsx +65 -0
- package/examples/dashboard/src/DevicesPanel.tsx +11 -0
- package/examples/dashboard/src/SettingsScreen.tsx +9 -4
- package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -2
- 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 +38 -4
- package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +10 -2
- package/examples/dashboard/src/styles.css +162 -0
- package/package.json +49 -49
- package/src/RaftChannelSimulated.ts +4 -3
- package/src/RaftConnector.ts +34 -13
- package/src/RaftDeviceManager.ts +251 -81
- package/src/RaftDeviceMgrIF.ts +5 -1
- package/src/RaftDeviceStates.ts +35 -5
- package/src/RaftPublish.ts +92 -0
- package/src/RaftStreamHandler.ts +84 -1
- package/src/RaftSystemUtils.ts +59 -0
- package/src/RaftTypes.ts +27 -0
- 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?.
|
|
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 (
|
|
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')
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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,
|
|
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<
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
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 =
|
|
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<
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|