@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.
- package/devdocs/devbin-backwards-compatibility.md +105 -0
- package/devdocs/pseudocode-to-js-transpiler.md +563 -0
- package/dist/react-native/PseudocodeTranspiler.d.ts +6 -0
- package/dist/react-native/PseudocodeTranspiler.js +115 -0
- package/dist/react-native/PseudocodeTranspiler.js.map +1 -0
- package/dist/react-native/RaftAttributeHandler.d.ts +1 -1
- package/dist/react-native/RaftAttributeHandler.js +108 -32
- package/dist/react-native/RaftAttributeHandler.js.map +1 -1
- package/dist/react-native/RaftChannelBLE.web.d.ts +4 -0
- package/dist/react-native/RaftChannelBLE.web.js +59 -21
- package/dist/react-native/RaftChannelBLE.web.js.map +1 -1
- package/dist/react-native/RaftChannelSimulated.d.ts +1 -0
- package/dist/react-native/RaftChannelSimulated.js +9 -5
- package/dist/react-native/RaftChannelSimulated.js.map +1 -1
- package/dist/react-native/RaftChannelWebSocket.js +16 -1
- package/dist/react-native/RaftChannelWebSocket.js.map +1 -1
- package/dist/react-native/RaftConnector.d.ts +11 -1
- package/dist/react-native/RaftConnector.js +75 -9
- package/dist/react-native/RaftConnector.js.map +1 -1
- package/dist/react-native/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/react-native/RaftCustomAttrHandler.js +32 -44
- package/dist/react-native/RaftCustomAttrHandler.js.map +1 -1
- package/dist/react-native/RaftDeviceInfo.d.ts +18 -0
- package/dist/react-native/RaftDeviceInfo.js +8 -0
- package/dist/react-native/RaftDeviceInfo.js.map +1 -1
- package/dist/react-native/RaftDeviceManager.d.ts +30 -3
- package/dist/react-native/RaftDeviceManager.js +618 -107
- package/dist/react-native/RaftDeviceManager.js.map +1 -1
- package/dist/react-native/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/react-native/RaftDeviceStates.d.ts +27 -3
- package/dist/react-native/RaftDeviceStates.js +31 -6
- package/dist/react-native/RaftDeviceStates.js.map +1 -1
- package/dist/react-native/RaftFileHandler.d.ts +0 -1
- package/dist/react-native/RaftFileHandler.js +61 -23
- package/dist/react-native/RaftFileHandler.js.map +1 -1
- package/dist/react-native/RaftPublish.d.ts +2 -0
- package/dist/react-native/RaftPublish.js +81 -0
- package/dist/react-native/RaftPublish.js.map +1 -0
- package/dist/react-native/RaftStreamHandler.d.ts +11 -0
- package/dist/react-native/RaftStreamHandler.js +66 -0
- package/dist/react-native/RaftStreamHandler.js.map +1 -1
- package/dist/react-native/RaftStruct.d.ts +2 -2
- package/dist/react-native/RaftStruct.js +97 -26
- package/dist/react-native/RaftStruct.js.map +1 -1
- package/dist/react-native/RaftSystemType.d.ts +1 -0
- package/dist/react-native/RaftSystemUtils.d.ts +17 -1
- package/dist/react-native/RaftSystemUtils.js +51 -0
- package/dist/react-native/RaftSystemUtils.js.map +1 -1
- package/dist/react-native/RaftTimezone.d.ts +16 -0
- package/dist/react-native/RaftTimezone.js +153 -0
- package/dist/react-native/RaftTimezone.js.map +1 -0
- package/dist/react-native/RaftTypes.d.ts +27 -1
- package/dist/react-native/RaftTypes.js.map +1 -1
- package/dist/react-native/RaftUpdateManager.js +1 -1
- package/dist/react-native/RaftUpdateManager.js.map +1 -1
- package/dist/react-native/main.d.ts +2 -0
- package/dist/react-native/main.js +6 -1
- package/dist/react-native/main.js.map +1 -1
- package/dist/web/PseudocodeTranspiler.d.ts +6 -0
- package/dist/web/PseudocodeTranspiler.js +115 -0
- package/dist/web/PseudocodeTranspiler.js.map +1 -0
- package/dist/web/RaftAttributeHandler.d.ts +1 -1
- package/dist/web/RaftAttributeHandler.js +108 -32
- package/dist/web/RaftAttributeHandler.js.map +1 -1
- package/dist/web/RaftChannelBLE.web.d.ts +4 -0
- package/dist/web/RaftChannelBLE.web.js +59 -21
- package/dist/web/RaftChannelBLE.web.js.map +1 -1
- package/dist/web/RaftChannelSimulated.d.ts +1 -0
- package/dist/web/RaftChannelSimulated.js +9 -5
- package/dist/web/RaftChannelSimulated.js.map +1 -1
- package/dist/web/RaftChannelWebSocket.js +16 -1
- package/dist/web/RaftChannelWebSocket.js.map +1 -1
- package/dist/web/RaftConnector.d.ts +11 -1
- package/dist/web/RaftConnector.js +75 -9
- package/dist/web/RaftConnector.js.map +1 -1
- package/dist/web/RaftCustomAttrHandler.d.ts +2 -2
- package/dist/web/RaftCustomAttrHandler.js +32 -44
- package/dist/web/RaftCustomAttrHandler.js.map +1 -1
- package/dist/web/RaftDeviceInfo.d.ts +18 -0
- package/dist/web/RaftDeviceInfo.js +8 -0
- package/dist/web/RaftDeviceInfo.js.map +1 -1
- package/dist/web/RaftDeviceManager.d.ts +30 -3
- package/dist/web/RaftDeviceManager.js +618 -107
- package/dist/web/RaftDeviceManager.js.map +1 -1
- package/dist/web/RaftDeviceMgrIF.d.ts +11 -2
- package/dist/web/RaftDeviceStates.d.ts +27 -3
- package/dist/web/RaftDeviceStates.js +31 -6
- package/dist/web/RaftDeviceStates.js.map +1 -1
- package/dist/web/RaftFileHandler.d.ts +0 -1
- package/dist/web/RaftFileHandler.js +61 -23
- package/dist/web/RaftFileHandler.js.map +1 -1
- package/dist/web/RaftPublish.d.ts +2 -0
- package/dist/web/RaftPublish.js +81 -0
- package/dist/web/RaftPublish.js.map +1 -0
- package/dist/web/RaftStreamHandler.d.ts +11 -0
- package/dist/web/RaftStreamHandler.js +66 -0
- package/dist/web/RaftStreamHandler.js.map +1 -1
- package/dist/web/RaftStruct.d.ts +2 -2
- package/dist/web/RaftStruct.js +97 -26
- package/dist/web/RaftStruct.js.map +1 -1
- package/dist/web/RaftSystemType.d.ts +1 -0
- package/dist/web/RaftSystemUtils.d.ts +17 -1
- package/dist/web/RaftSystemUtils.js +51 -0
- package/dist/web/RaftSystemUtils.js.map +1 -1
- package/dist/web/RaftTimezone.d.ts +16 -0
- package/dist/web/RaftTimezone.js +153 -0
- package/dist/web/RaftTimezone.js.map +1 -0
- package/dist/web/RaftTypes.d.ts +27 -1
- package/dist/web/RaftTypes.js.map +1 -1
- package/dist/web/RaftUpdateManager.js +1 -1
- package/dist/web/RaftUpdateManager.js.map +1 -1
- package/dist/web/main.d.ts +2 -0
- package/dist/web/main.js +6 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +158 -8
- package/examples/dashboard/src/DeviceLineChart.tsx +16 -3
- package/examples/dashboard/src/DevicePanel.tsx +92 -11
- package/examples/dashboard/src/DeviceSelectDialog.tsx +224 -0
- package/examples/dashboard/src/DeviceStatsPanel.tsx +76 -0
- package/examples/dashboard/src/DevicesPanel.tsx +11 -0
- package/examples/dashboard/src/LogConfigPanel.tsx +357 -0
- package/examples/dashboard/src/LogFilesPanel.tsx +200 -0
- package/examples/dashboard/src/LoggingPanel.tsx +264 -0
- package/examples/dashboard/src/Main.tsx +12 -2
- package/examples/dashboard/src/SettingsScreen.tsx +9 -4
- package/examples/dashboard/src/SystemTypeCog/CogStateInfo.ts +10 -3
- package/examples/dashboard/src/SystemTypeCog/SystemTypeCog.ts +37 -3
- package/examples/dashboard/src/SystemTypeGeneric/StateInfoGeneric.ts +10 -2
- package/examples/dashboard/src/SystemTypeGeneric/SystemTypeGeneric.ts +41 -7
- package/examples/dashboard/src/SystemTypeMarty/RICStateInfo.ts +34 -3
- package/examples/dashboard/src/styles.css +766 -1
- package/notes/web-ble-reconnect-retry.md +69 -0
- package/package.json +10 -7
- package/src/PseudocodeTranspiler.test.ts +372 -0
- package/src/PseudocodeTranspiler.ts +127 -0
- package/src/RaftAttributeHandler.ts +152 -76
- package/src/RaftChannelBLE.web.ts +62 -20
- package/src/RaftChannelSimulated.ts +10 -5
- package/src/RaftChannelWebSocket.ts +16 -2
- package/src/RaftConnector.ts +93 -15
- package/src/RaftCustomAttrHandler.ts +35 -45
- package/src/RaftDeviceInfo.ts +27 -0
- package/src/RaftDeviceManager.test.ts +164 -0
- package/src/RaftDeviceManager.ts +705 -127
- package/src/RaftDeviceMgrIF.ts +13 -2
- package/src/RaftDeviceStates.ts +49 -8
- package/src/RaftFileHandler.ts +69 -28
- package/src/RaftPublish.ts +92 -0
- package/src/RaftStreamHandler.ts +84 -1
- package/src/RaftStruct.test.ts +229 -0
- package/src/RaftStruct.ts +101 -37
- package/src/RaftSystemType.ts +1 -0
- package/src/RaftSystemUtils.ts +59 -0
- package/src/RaftTimezone.ts +151 -0
- package/src/RaftTypes.ts +34 -1
- package/src/RaftUpdateManager.ts +1 -1
- package/src/main.ts +2 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import ConnManager from './ConnManager';
|
|
3
|
+
import { LogConfig } from './LogConfigPanel';
|
|
4
|
+
import { getHostPosixTZ } from '../../../src/RaftTimezone';
|
|
5
|
+
import './styles.css';
|
|
6
|
+
|
|
7
|
+
// Minimal query-value encoder: only encode characters that break query string parsing.
|
|
8
|
+
// Unlike encodeURIComponent (which encodes ~40 chars), this keeps JSON, colons, commas
|
|
9
|
+
// etc. as-is, significantly reducing message size for BLE transport.
|
|
10
|
+
function encodeQueryValue(s: string): string {
|
|
11
|
+
return s.replace(/%/g, '%25').replace(/&/g, '%26').replace(/=/g, '%3D');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const connManager = ConnManager.getInstance();
|
|
15
|
+
|
|
16
|
+
interface LogStatus {
|
|
17
|
+
isLogging: boolean;
|
|
18
|
+
fileName: string;
|
|
19
|
+
elapsedSecs: number;
|
|
20
|
+
bytesWritten: number;
|
|
21
|
+
samples: number;
|
|
22
|
+
flushCount: number;
|
|
23
|
+
bufferOverflows: number;
|
|
24
|
+
avgWriteMs: number;
|
|
25
|
+
maxWriteMs: number;
|
|
26
|
+
bytesPerSec: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const emptyStatus: LogStatus = {
|
|
30
|
+
isLogging: false,
|
|
31
|
+
fileName: '',
|
|
32
|
+
elapsedSecs: 0,
|
|
33
|
+
bytesWritten: 0,
|
|
34
|
+
samples: 0,
|
|
35
|
+
flushCount: 0,
|
|
36
|
+
bufferOverflows: 0,
|
|
37
|
+
avgWriteMs: 0,
|
|
38
|
+
maxWriteMs: 0,
|
|
39
|
+
bytesPerSec: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
interface LoggingPanelProps {
|
|
43
|
+
onLogStopped?: () => void;
|
|
44
|
+
pausePolling?: boolean;
|
|
45
|
+
logConfig?: LogConfig | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function LoggingPanel({ onLogStopped, pausePolling, logConfig }: LoggingPanelProps) {
|
|
49
|
+
const [status, setStatus] = useState<LogStatus>(emptyStatus);
|
|
50
|
+
const [label, setLabel] = useState('');
|
|
51
|
+
const [isBusy, setIsBusy] = useState(false);
|
|
52
|
+
const [lastError, setLastError] = useState('');
|
|
53
|
+
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
54
|
+
const wasLoggingRef = useRef(false);
|
|
55
|
+
|
|
56
|
+
const fetchStatus = async () => {
|
|
57
|
+
if (!connManager.getConnector().isConnected()) return;
|
|
58
|
+
try {
|
|
59
|
+
const resp = await connManager.getConnector().sendRICRESTMsg(
|
|
60
|
+
'datalog?action=status', {}
|
|
61
|
+
);
|
|
62
|
+
if (resp && typeof resp === 'object') {
|
|
63
|
+
const r = resp as any;
|
|
64
|
+
const flushLatency = r.flushLatency ?? {};
|
|
65
|
+
const nowLogging = r.active ?? false;
|
|
66
|
+
setStatus({
|
|
67
|
+
isLogging: nowLogging,
|
|
68
|
+
fileName: r.fileName ?? '',
|
|
69
|
+
elapsedSecs: (r.durationMs ?? 0) / 1000,
|
|
70
|
+
bytesWritten: r.totalBytesWritten ?? 0,
|
|
71
|
+
samples: r.samples ?? 0,
|
|
72
|
+
flushCount: r.flushCount ?? 0,
|
|
73
|
+
bufferOverflows: r.bufferOverflows ?? 0,
|
|
74
|
+
avgWriteMs: (flushLatency.avgUs ?? 0) / 1000,
|
|
75
|
+
maxWriteMs: (flushLatency.maxUs ?? 0) / 1000,
|
|
76
|
+
bytesPerSec: r.bytesPerSec ?? 0,
|
|
77
|
+
});
|
|
78
|
+
// Detect timed logging session that finished on its own
|
|
79
|
+
if (wasLoggingRef.current && !nowLogging) {
|
|
80
|
+
onLogStopped?.();
|
|
81
|
+
}
|
|
82
|
+
wasLoggingRef.current = nowLogging;
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.warn('Failed to fetch logging status', e);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Poll status every 2 seconds (paused during file downloads)
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (pausePolling) {
|
|
92
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current);
|
|
93
|
+
pollTimerRef.current = null;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
fetchStatus();
|
|
97
|
+
pollTimerRef.current = setInterval(fetchStatus, 2000);
|
|
98
|
+
return () => {
|
|
99
|
+
if (pollTimerRef.current) clearInterval(pollTimerRef.current);
|
|
100
|
+
};
|
|
101
|
+
}, [pausePolling]);
|
|
102
|
+
|
|
103
|
+
const handleStart = async () => {
|
|
104
|
+
setIsBusy(true);
|
|
105
|
+
setLastError('');
|
|
106
|
+
try {
|
|
107
|
+
const labelParam = label.trim() ? `&label=${encodeQueryValue(label.trim())}` : '';
|
|
108
|
+
let configParam = '';
|
|
109
|
+
if (logConfig && logConfig.devices.length > 0) {
|
|
110
|
+
configParam = `&config=${encodeQueryValue(JSON.stringify(logConfig))}`;
|
|
111
|
+
}
|
|
112
|
+
// Include current UTC time so firmware can timestamp the log even without NTP
|
|
113
|
+
const utcParam = `&UTC=${encodeQueryValue(new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'))}`;
|
|
114
|
+
const posixTZ = getHostPosixTZ();
|
|
115
|
+
const tzParam = posixTZ ? `&tz=${encodeQueryValue(posixTZ)}` : '';
|
|
116
|
+
const resp = await connManager.getConnector().sendRICRESTMsg(
|
|
117
|
+
`datalog?action=start${labelParam}${configParam}${utcParam}${tzParam}`, {}
|
|
118
|
+
);
|
|
119
|
+
const r = resp as any;
|
|
120
|
+
if (r?.rslt !== 'ok') {
|
|
121
|
+
setLastError(r?.error || 'Start failed');
|
|
122
|
+
}
|
|
123
|
+
await fetchStatus();
|
|
124
|
+
} catch (e) {
|
|
125
|
+
setLastError('Failed to send start command');
|
|
126
|
+
}
|
|
127
|
+
setIsBusy(false);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleStop = async () => {
|
|
131
|
+
setIsBusy(true);
|
|
132
|
+
setLastError('');
|
|
133
|
+
try {
|
|
134
|
+
const resp = await connManager.getConnector().sendRICRESTMsg(
|
|
135
|
+
'datalog?action=stop', {}
|
|
136
|
+
);
|
|
137
|
+
const r = resp as any;
|
|
138
|
+
if (r?.rslt !== 'ok') {
|
|
139
|
+
setLastError(r?.error || 'Stop failed');
|
|
140
|
+
}
|
|
141
|
+
await fetchStatus();
|
|
142
|
+
onLogStopped?.();
|
|
143
|
+
} catch (e) {
|
|
144
|
+
setLastError('Failed to send stop command');
|
|
145
|
+
}
|
|
146
|
+
setIsBusy(false);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleSimulate = async () => {
|
|
150
|
+
setIsBusy(true);
|
|
151
|
+
setLastError('');
|
|
152
|
+
try {
|
|
153
|
+
const resp = await connManager.getConnector().sendRICRESTMsg(
|
|
154
|
+
'datalog?action=simulate', {}
|
|
155
|
+
);
|
|
156
|
+
const r = resp as any;
|
|
157
|
+
if (r?.rslt !== 'ok') {
|
|
158
|
+
setLastError(r?.error || 'Simulate failed');
|
|
159
|
+
}
|
|
160
|
+
await fetchStatus();
|
|
161
|
+
} catch (e) {
|
|
162
|
+
setLastError('Failed to send simulate command');
|
|
163
|
+
}
|
|
164
|
+
setIsBusy(false);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const formatBytes = (bytes: number): string => {
|
|
168
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
169
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
170
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const formatDuration = (secs: number): string => {
|
|
174
|
+
const h = Math.floor(secs / 3600);
|
|
175
|
+
const m = Math.floor((secs % 3600) / 60);
|
|
176
|
+
const s = Math.floor(secs % 60);
|
|
177
|
+
if (h > 0) return `${h}h ${m}m ${s}s`;
|
|
178
|
+
if (m > 0) return `${m}m ${s}s`;
|
|
179
|
+
return `${s}s`;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="info-box logging-panel">
|
|
184
|
+
<h3>Data Logging</h3>
|
|
185
|
+
|
|
186
|
+
{status.isLogging ? (
|
|
187
|
+
<>
|
|
188
|
+
<div className="logging-status-active">
|
|
189
|
+
<div className="logging-indicator" />
|
|
190
|
+
<span>Logging Active</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div className="info">
|
|
193
|
+
<div className="info-line">
|
|
194
|
+
<div className="info-label">File:</div>
|
|
195
|
+
<div className="info-value">{status.fileName}</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="info-line">
|
|
198
|
+
<div className="info-label">Duration:</div>
|
|
199
|
+
<div className="info-value">{formatDuration(status.elapsedSecs)}</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="info-line">
|
|
202
|
+
<div className="info-label">Written:</div>
|
|
203
|
+
<div className="info-value">{formatBytes(status.bytesWritten)}</div>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="info-line">
|
|
206
|
+
<div className="info-label">Writes:</div>
|
|
207
|
+
<div className="info-value">{status.flushCount} flushes, {status.samples} samples (overflows: {status.bufferOverflows})</div>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="info-line">
|
|
210
|
+
<div className="info-label">Write time:</div>
|
|
211
|
+
<div className="info-value">avg {status.avgWriteMs.toFixed(1)}ms, max {status.maxWriteMs.toFixed(1)}ms</div>
|
|
212
|
+
</div>
|
|
213
|
+
{status.bytesPerSec > 0 && (
|
|
214
|
+
<div className="info-line">
|
|
215
|
+
<div className="info-label">Rate:</div>
|
|
216
|
+
<div className="info-value">{formatBytes(status.bytesPerSec)}/s</div>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
<button
|
|
221
|
+
className="action-button logging-stop-button"
|
|
222
|
+
onClick={handleStop}
|
|
223
|
+
disabled={isBusy}
|
|
224
|
+
>
|
|
225
|
+
Stop Logging
|
|
226
|
+
</button>
|
|
227
|
+
</>
|
|
228
|
+
) : (
|
|
229
|
+
<>
|
|
230
|
+
<div className="logging-start-controls">
|
|
231
|
+
<input
|
|
232
|
+
type="text"
|
|
233
|
+
className="logging-label-input"
|
|
234
|
+
placeholder="Session label (optional)"
|
|
235
|
+
value={label}
|
|
236
|
+
onChange={(e) => setLabel(e.target.value)}
|
|
237
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleStart(); }}
|
|
238
|
+
/>
|
|
239
|
+
<div className="logging-button-row">
|
|
240
|
+
<button
|
|
241
|
+
className="action-button"
|
|
242
|
+
onClick={handleStart}
|
|
243
|
+
disabled={isBusy || (logConfig !== undefined && (!logConfig || logConfig.devices.length === 0))}
|
|
244
|
+
>
|
|
245
|
+
Start Logging
|
|
246
|
+
</button>
|
|
247
|
+
<button
|
|
248
|
+
className="action-button logging-simulate-button"
|
|
249
|
+
onClick={handleSimulate}
|
|
250
|
+
disabled={isBusy}
|
|
251
|
+
>
|
|
252
|
+
Simulate
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{lastError && (
|
|
260
|
+
<div className="logging-error">{lastError}</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
import StatusPanel from './StatusPanel';
|
|
12
12
|
import DevicesPanel from './DevicesPanel';
|
|
13
13
|
import CommandPanel from './CommandPanel';
|
|
14
|
+
import LoggingPanel from './LoggingPanel';
|
|
15
|
+
import LogFilesPanel from './LogFilesPanel';
|
|
16
|
+
import LogConfigPanel, { LogConfig } from './LogConfigPanel';
|
|
17
|
+
|
|
14
18
|
import LatencyTestPanel from './LatencyTestPanel';
|
|
15
19
|
import SettingsManager from './SettingsManager';
|
|
16
20
|
|
|
@@ -34,6 +38,9 @@ export default function Main() {
|
|
|
34
38
|
const [ipAddress, setIpAddress] = useState<string>(
|
|
35
39
|
localStorage.getItem('lastIpAddress') || ''
|
|
36
40
|
);
|
|
41
|
+
const [fileRefreshTrigger, setFileRefreshTrigger] = useState(0);
|
|
42
|
+
const [downloadActive, setDownloadActive] = useState(false);
|
|
43
|
+
const [logConfig, setLogConfig] = useState<LogConfig | null>(null);
|
|
37
44
|
|
|
38
45
|
const [serialNo, setSerialNo] = useState<string>('');
|
|
39
46
|
|
|
@@ -148,10 +155,10 @@ export default function Main() {
|
|
|
148
155
|
{connectionStatus === RaftConnEvent.CONN_CONNECTED ? (
|
|
149
156
|
<>
|
|
150
157
|
<div className="connected-panel">
|
|
151
|
-
<div className="info-boxes">
|
|
158
|
+
<div className="info-boxes connection-info">
|
|
152
159
|
<div className="info-box">
|
|
153
160
|
<div className="conn-indication">
|
|
154
|
-
<h3>Connected</h3>
|
|
161
|
+
<h3>Connected via {connManager.getConnector().getConnMethod() || 'Unknown'}</h3>
|
|
155
162
|
</div>
|
|
156
163
|
<div>
|
|
157
164
|
<button
|
|
@@ -169,6 +176,9 @@ export default function Main() {
|
|
|
169
176
|
<StatusPanel />
|
|
170
177
|
{latencyTestEnabled && <LatencyTestPanel />}
|
|
171
178
|
<CommandPanel />
|
|
179
|
+
<LogConfigPanel onConfigChanged={setLogConfig} disabled={false} />
|
|
180
|
+
<LoggingPanel onLogStopped={() => setFileRefreshTrigger(n => n + 1)} pausePolling={downloadActive} logConfig={logConfig} />
|
|
181
|
+
<LogFilesPanel refreshTrigger={fileRefreshTrigger} onDownloadActiveChange={setDownloadActive} />
|
|
172
182
|
</div>
|
|
173
183
|
<DevicesPanel />
|
|
174
184
|
</>
|
|
@@ -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
|
/>
|
|
@@ -2,7 +2,6 @@ import { time } from "console";
|
|
|
2
2
|
import RaftLog from "../../../../src/RaftLog";
|
|
3
3
|
import { DeviceManager } from "../../../../src/RaftDeviceManager";
|
|
4
4
|
import RaftUtils from "../../../../src/RaftUtils";
|
|
5
|
-
import { TextDecoder } from 'text-encoding';
|
|
6
5
|
|
|
7
6
|
// export interface IMUStateInfo {
|
|
8
7
|
// gx: number;
|
|
@@ -47,6 +46,14 @@ export class CogStateInfo {
|
|
|
47
46
|
return this._deviceManager;
|
|
48
47
|
}
|
|
49
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
|
+
|
|
50
57
|
updateFromMsg(rxMsg: Uint8Array, frameTimeMs: number, isBinary: boolean): Array<string> {
|
|
51
58
|
|
|
52
59
|
// Debug
|
|
@@ -54,14 +61,14 @@ export class CogStateInfo {
|
|
|
54
61
|
|
|
55
62
|
if (isBinary) {
|
|
56
63
|
// console.log(`CogStateInfo: updateFromMsg: ${RaftUtils.bufferToHex(rxMsg)}`);
|
|
57
|
-
this.
|
|
64
|
+
this.handleBinaryPayload(rxMsg);
|
|
58
65
|
} else {
|
|
59
66
|
// Convert Uint8Array to string
|
|
60
67
|
const decoder = new TextDecoder('utf-8');
|
|
61
68
|
const jsonString = decoder.decode(rxMsg.slice(2));
|
|
62
69
|
|
|
63
70
|
// Handle using device manager
|
|
64
|
-
this.
|
|
71
|
+
this.handleJsonPayload(jsonString);
|
|
65
72
|
}
|
|
66
73
|
|
|
67
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";
|
|
@@ -39,24 +39,30 @@ export default class SystemTypeGeneric implements RaftSystemType {
|
|
|
39
39
|
|
|
40
40
|
// Subscribe for updates
|
|
41
41
|
subscribeForUpdates: RaftSubscribeForUpdatesCBType | null = async (systemUtils: RaftSystemUtils, enable: boolean) => {
|
|
42
|
-
// Subscription rate
|
|
42
|
+
// Subscription rate — must be high enough to match max polling rate
|
|
43
43
|
const subscribeRateHz = 0.1;
|
|
44
44
|
try {
|
|
45
45
|
const topic = SUBSCRIBE_BINARY_MSGS ? "devbin" : "devjson";
|
|
46
46
|
const subscribeDisable = '{"cmdName":"subscription","action":"update",' +
|
|
47
47
|
'"pubRecs":[' +
|
|
48
|
-
`{"name":"${topic}","rateHz":0
|
|
48
|
+
`{"name":"${topic}","rateHz":0}` +
|
|
49
49
|
']}';
|
|
50
50
|
const subscribeEnable = '{"cmdName":"subscription","action":"update",' +
|
|
51
51
|
'"pubRecs":[' +
|
|
52
|
-
`{"name":"${topic}","trigger":"timeorchange","rateHz":${subscribeRateHz.toString()}}` +
|
|
52
|
+
`{"name":"${topic}","trigger":"timeorchange","rateHz":${subscribeRateHz.toString()},"minMs":10}` +
|
|
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,6 @@
|
|
|
1
1
|
import RaftDeviceMgrIF from "../../../../src/RaftDeviceMgrIF";
|
|
2
|
-
import {
|
|
2
|
+
import { SampleRateResult } from "../../../../src/RaftDeviceInfo";
|
|
3
|
+
import { DeviceAttributeState, DevicesState, DeviceState, DeviceOnlineState } from '../../../../src/RaftDeviceStates';
|
|
3
4
|
import { RICSERIAL_PAYLOAD_POS } from "../../../../src/RaftProtocolDefs";
|
|
4
5
|
import RICAddOnManager from "./RICAddOnManager";
|
|
5
6
|
import RICCommsStats from "./RICCommsStats";
|
|
@@ -45,12 +46,18 @@ export class RICStateInfo implements RaftDeviceMgrIF {
|
|
|
45
46
|
deviceTimeline: {
|
|
46
47
|
timestampsUs: [],
|
|
47
48
|
lastReportTimestampUs: 0,
|
|
48
|
-
reportTimestampOffsetUs: 0
|
|
49
|
+
reportTimestampOffsetUs: 0,
|
|
50
|
+
totalSamplesAdded: 0,
|
|
51
|
+
emaLastSampleTimeUs: 0,
|
|
52
|
+
emaIntervalUs: 0,
|
|
53
|
+
emaPrevPollTimeUs: 0,
|
|
54
|
+
emaCalibrated: false,
|
|
55
|
+
emaCalibrationPolls: 0
|
|
49
56
|
},
|
|
50
57
|
deviceAttributes: {},
|
|
51
58
|
deviceIsNew: false,
|
|
52
59
|
stateChanged: false,
|
|
53
|
-
|
|
60
|
+
onlineState: DeviceOnlineState.Offline,
|
|
54
61
|
deviceAddress: "",
|
|
55
62
|
deviceType: "",
|
|
56
63
|
busName: ""
|
|
@@ -85,6 +92,14 @@ export class RICStateInfo implements RaftDeviceMgrIF {
|
|
|
85
92
|
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
addDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
|
|
96
|
+
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
removeDeviceRemovedCallback(callback: (deviceKey: string, state: DeviceState) => void): void {
|
|
100
|
+
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
sendAction(deviceKey: string, action: any, data: any): void {
|
|
89
104
|
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
90
105
|
}
|
|
@@ -93,5 +108,21 @@ export class RICStateInfo implements RaftDeviceMgrIF {
|
|
|
93
108
|
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
async setSampleRate(deviceKey: string, sampleRateHz: number, options?: {
|
|
112
|
+
numSamples?: number; intervalUs?: number; maxNumSamples?: number;
|
|
113
|
+
}): Promise<SampleRateResult> {
|
|
114
|
+
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
115
|
+
return { ok: false, requestedRateHz: sampleRateHz, actualRateHz: 0, intervalUs: 0, numSamples: 0, error: 'Not implemented' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getDeviceStats(deviceKey: string): any {
|
|
119
|
+
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resetDeviceStats(deviceKey: string): void {
|
|
124
|
+
// TODO - implement if RICStateInfo is to be used as a DeviceMgr
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
}
|
|
97
128
|
|