@robotical/raftjs 2.1.0 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +29 -1
- package/dist/react-native/RaftConnector.js +177 -11
- 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 +1 -1
- package/dist/react-native/RaftFileHandler.js +101 -34
- package/dist/react-native/RaftFileHandler.js.map +1 -1
- package/dist/react-native/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/react-native/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/react-native/RaftMsgHandler.d.ts +1 -1
- package/dist/react-native/RaftMsgHandler.js +6 -3
- package/dist/react-native/RaftMsgHandler.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 +46 -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 +3 -0
- package/dist/react-native/main.js +8 -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 +29 -1
- package/dist/web/RaftConnector.js +177 -11
- 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 +1 -1
- package/dist/web/RaftFileHandler.js +101 -34
- package/dist/web/RaftFileHandler.js.map +1 -1
- package/dist/web/RaftMicroPythonConsoleClient.d.ts +38 -0
- package/dist/web/RaftMicroPythonConsoleClient.js +45 -0
- package/dist/web/RaftMicroPythonConsoleClient.js.map +1 -0
- package/dist/web/RaftMsgHandler.d.ts +1 -1
- package/dist/web/RaftMsgHandler.js +6 -3
- package/dist/web/RaftMsgHandler.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 +46 -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 +3 -0
- package/dist/web/main.js +8 -1
- package/dist/web/main.js.map +1 -1
- package/examples/dashboard/package.json +2 -2
- package/examples/dashboard/src/DeviceActionsForm.tsx +177 -17
- 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 +204 -17
- 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 +112 -39
- package/src/RaftMicroPythonConsoleClient.ts +78 -0
- package/src/RaftMsgHandler.ts +8 -4
- 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 +57 -1
- package/src/RaftUpdateManager.ts +1 -1
- package/src/main.ts +3 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import ConnManager from './ConnManager';
|
|
3
|
+
import { DeviceOnlineState, DeviceState, DevicesState } from '../../../src/RaftDeviceStates';
|
|
4
|
+
import DeviceSelectDialog, { DeviceLogEntry } from './DeviceSelectDialog';
|
|
5
|
+
import './styles.css';
|
|
6
|
+
|
|
7
|
+
const connManager = ConnManager.getInstance();
|
|
8
|
+
|
|
9
|
+
// Duration presets: label + milliseconds (0 = unlimited)
|
|
10
|
+
const DURATION_PRESETS = [
|
|
11
|
+
{ label: '1 min', ms: 60000 },
|
|
12
|
+
{ label: '5 min', ms: 300000 },
|
|
13
|
+
{ label: '10 min', ms: 600000 },
|
|
14
|
+
{ label: '30 min', ms: 1800000 },
|
|
15
|
+
{ label: '1 hour', ms: 3600000 },
|
|
16
|
+
{ label: '6 hours', ms: 21600000 },
|
|
17
|
+
{ label: '24 hours', ms: 86400000 },
|
|
18
|
+
{ label: 'Unlimited', ms: 0 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Duration slider range (log scale): 1 min to 7 days
|
|
22
|
+
const DUR_MIN_MS = 60000;
|
|
23
|
+
const DUR_MAX_MS = 604800000;
|
|
24
|
+
|
|
25
|
+
function durationToSlider(ms: number): number {
|
|
26
|
+
if (ms <= 0) return 1; // unlimited = max
|
|
27
|
+
const minLog = Math.log10(DUR_MIN_MS);
|
|
28
|
+
const maxLog = Math.log10(DUR_MAX_MS);
|
|
29
|
+
return Math.min(1, Math.max(0, (Math.log10(Math.max(ms, DUR_MIN_MS)) - minLog) / (maxLog - minLog)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sliderToDuration(val: number): number {
|
|
33
|
+
if (val >= 0.99) return 0; // unlimited
|
|
34
|
+
const minLog = Math.log10(DUR_MIN_MS);
|
|
35
|
+
const maxLog = Math.log10(DUR_MAX_MS);
|
|
36
|
+
return Math.round(Math.pow(10, minLog + val * (maxLog - minLog)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatDurationLabel(ms: number): string {
|
|
40
|
+
if (ms <= 0) return 'Unlimited';
|
|
41
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(0)}s`;
|
|
42
|
+
if (ms < 3600000) return `${(ms / 60000).toFixed(1)} min`;
|
|
43
|
+
if (ms < 86400000) return `${(ms / 3600000).toFixed(1)} hr`;
|
|
44
|
+
return `${(ms / 86400000).toFixed(1)} days`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LogConfig {
|
|
48
|
+
format: string; // "csv" or "jsonl"
|
|
49
|
+
csvHeader?: boolean; // include metadata comment block in CSV
|
|
50
|
+
durationMs: number; // logging duration in ms (0 = unlimited)
|
|
51
|
+
devices: Array<{
|
|
52
|
+
bus: string;
|
|
53
|
+
addr: string;
|
|
54
|
+
rateMs: number;
|
|
55
|
+
attrs?: string[];
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface LogConfigPanelProps {
|
|
60
|
+
onConfigChanged?: (config: LogConfig | null) => void;
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default function LogConfigPanel({ onConfigChanged, disabled }: LogConfigPanelProps) {
|
|
65
|
+
const [deviceEntries, setDeviceEntries] = useState<DeviceLogEntry[]>([]);
|
|
66
|
+
const [lastUpdated, setLastUpdated] = useState(0);
|
|
67
|
+
const [format, setFormat] = useState<'csv' | 'jsonl'>('csv');
|
|
68
|
+
const [csvHeader, setCsvHeader] = useState(true);
|
|
69
|
+
const [durationMs, setDurationMs] = useState(600000); // 10 minutes default
|
|
70
|
+
const [fsFreeBytes, setFsFreeBytes] = useState<number | null>(null);
|
|
71
|
+
|
|
72
|
+
// Fetch filesystem info from device
|
|
73
|
+
const fetchFsInfo = async () => {
|
|
74
|
+
if (!connManager.getConnector().isConnected()) return;
|
|
75
|
+
try {
|
|
76
|
+
const resp = await connManager.getConnector().sendRICRESTMsg('filelist/local', {});
|
|
77
|
+
if (resp && typeof resp === 'object') {
|
|
78
|
+
const r = resp as any;
|
|
79
|
+
const size = r.diskSize ?? 0;
|
|
80
|
+
const used = r.diskUsed ?? 0;
|
|
81
|
+
if (size > 0) setFsFreeBytes(size - used);
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Estimate bytes per minute based on current config
|
|
89
|
+
const estimateBytesPerMin = (): number => {
|
|
90
|
+
const enabledDevices = deviceEntries.filter(d => d.enabled);
|
|
91
|
+
if (enabledDevices.length === 0) return 0;
|
|
92
|
+
|
|
93
|
+
let totalBytesPerMin = 0;
|
|
94
|
+
for (const d of enabledDevices) {
|
|
95
|
+
// Effective rate: if 0 (max poll rate), use pollIntervalMs
|
|
96
|
+
const effectiveRateMs = d.rateMs > 0 ? d.rateMs : d.pollIntervalMs;
|
|
97
|
+
if (effectiveRateMs <= 0) continue;
|
|
98
|
+
const samplesPerMin = 60000 / effectiveRateMs;
|
|
99
|
+
|
|
100
|
+
// Estimate row size based on format
|
|
101
|
+
const numAttrs = d.selectedAttrs.length > 0 ? d.selectedAttrs.length : d.availableAttrs.length;
|
|
102
|
+
let bytesPerSample: number;
|
|
103
|
+
if (format === 'csv') {
|
|
104
|
+
// time field (~8 chars) + comma + ~8 chars per attr value + commas for other devices
|
|
105
|
+
bytesPerSample = 10 + numAttrs * 9 + enabledDevices.length * 2;
|
|
106
|
+
} else {
|
|
107
|
+
// JSONL: ~60 overhead + ~2 per raw byte (hex) * ~2 bytes per attr
|
|
108
|
+
bytesPerSample = 60 + numAttrs * 4;
|
|
109
|
+
}
|
|
110
|
+
totalBytesPerMin += samplesPerMin * bytesPerSample;
|
|
111
|
+
}
|
|
112
|
+
return totalBytesPerMin;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const bytesPerMin = estimateBytesPerMin();
|
|
116
|
+
const kbPerMin = bytesPerMin / 1024;
|
|
117
|
+
const maxDurationSecs = fsFreeBytes !== null && bytesPerMin > 0
|
|
118
|
+
? (fsFreeBytes / bytesPerMin) * 60
|
|
119
|
+
: null;
|
|
120
|
+
|
|
121
|
+
// Refresh device list from device manager
|
|
122
|
+
const refreshDeviceList = () => {
|
|
123
|
+
const deviceManager = connManager.getConnector().getSystemType()?.deviceMgrIF;
|
|
124
|
+
if (!deviceManager) return;
|
|
125
|
+
|
|
126
|
+
const devicesState: DevicesState = deviceManager.getDevicesState();
|
|
127
|
+
const entries: DeviceLogEntry[] = [];
|
|
128
|
+
|
|
129
|
+
for (const [deviceKey, devState] of Object.entries(devicesState)) {
|
|
130
|
+
if (deviceKey === 'getDeviceKey') continue;
|
|
131
|
+
const ds = devState as DeviceState;
|
|
132
|
+
if (ds.onlineState !== DeviceOnlineState.Online) continue; // only online devices
|
|
133
|
+
if (ds.busName === '0') continue; // skip non-bus (direct-connected) devices
|
|
134
|
+
|
|
135
|
+
// Get poll interval from device type info
|
|
136
|
+
let pollIntervalMs = 50; // default
|
|
137
|
+
if (ds.deviceTypeInfo?.resp?.us) {
|
|
138
|
+
pollIntervalMs = ds.deviceTypeInfo.resp.us / 1000;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if already in entries (preserve user's enabled/rate settings)
|
|
142
|
+
const existing = deviceEntries.find(
|
|
143
|
+
e => e.busName === ds.busName && e.addr === ds.deviceAddress
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Extract available attribute names from devInfoJson
|
|
147
|
+
const availableAttrs: string[] = [];
|
|
148
|
+
if (ds.deviceTypeInfo?.resp?.a) {
|
|
149
|
+
for (const attr of ds.deviceTypeInfo.resp.a) {
|
|
150
|
+
if (attr.n) availableAttrs.push(attr.n);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
entries.push({
|
|
155
|
+
enabled: existing?.enabled ?? true,
|
|
156
|
+
busName: ds.busName,
|
|
157
|
+
addr: ds.deviceAddress,
|
|
158
|
+
typeName: ds.deviceTypeInfo?.name ?? ds.deviceType ?? 'Unknown',
|
|
159
|
+
rateMs: existing?.rateMs ?? 10000,
|
|
160
|
+
pollIntervalMs,
|
|
161
|
+
availableAttrs,
|
|
162
|
+
selectedAttrs: existing?.selectedAttrs ?? [],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setDeviceEntries(entries);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Listen for device changes
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const deviceManager = connManager.getConnector().getSystemType()?.deviceMgrIF;
|
|
172
|
+
if (!deviceManager) return;
|
|
173
|
+
|
|
174
|
+
const onNewDevice = () => setLastUpdated(Date.now());
|
|
175
|
+
const onDeviceRemoved = () => setLastUpdated(Date.now());
|
|
176
|
+
deviceManager.addNewDeviceCallback(onNewDevice);
|
|
177
|
+
deviceManager.addDeviceRemovedCallback(onDeviceRemoved);
|
|
178
|
+
|
|
179
|
+
refreshDeviceList();
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
deviceManager.removeNewDeviceCallback(onNewDevice);
|
|
183
|
+
deviceManager.removeDeviceRemovedCallback(onDeviceRemoved);
|
|
184
|
+
};
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// Refresh when devices change
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
refreshDeviceList();
|
|
190
|
+
fetchFsInfo();
|
|
191
|
+
}, [lastUpdated]);
|
|
192
|
+
|
|
193
|
+
// Notify parent of config changes
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const enabledDevices = deviceEntries.filter(d => d.enabled);
|
|
196
|
+
if (enabledDevices.length === 0) {
|
|
197
|
+
onConfigChanged?.(null);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const config: LogConfig = {
|
|
202
|
+
format,
|
|
203
|
+
csvHeader: format === 'csv' ? csvHeader : undefined,
|
|
204
|
+
durationMs,
|
|
205
|
+
devices: enabledDevices.map(d => {
|
|
206
|
+
const entry: LogConfig['devices'][0] = {
|
|
207
|
+
bus: d.busName,
|
|
208
|
+
addr: d.addr,
|
|
209
|
+
rateMs: d.rateMs,
|
|
210
|
+
};
|
|
211
|
+
if (format === 'csv' && d.selectedAttrs.length > 0) {
|
|
212
|
+
entry.attrs = d.selectedAttrs;
|
|
213
|
+
}
|
|
214
|
+
return entry;
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
onConfigChanged?.(config);
|
|
218
|
+
}, [deviceEntries, format, csvHeader, durationMs]);
|
|
219
|
+
|
|
220
|
+
const [showDeviceDialog, setShowDeviceDialog] = useState(false);
|
|
221
|
+
|
|
222
|
+
const handleDeviceSave = (entries: DeviceLogEntry[]) => {
|
|
223
|
+
setDeviceEntries(entries);
|
|
224
|
+
setShowDeviceDialog(false);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Build a short summary of what is being logged
|
|
228
|
+
const buildSummary = (): string => {
|
|
229
|
+
const enabled = deviceEntries.filter(d => d.enabled);
|
|
230
|
+
if (enabled.length === 0) return 'No devices selected';
|
|
231
|
+
return enabled.map(d => {
|
|
232
|
+
const attrInfo = d.selectedAttrs.length === 0
|
|
233
|
+
? 'all attrs'
|
|
234
|
+
: `${d.selectedAttrs.length}/${d.availableAttrs.length} attrs`;
|
|
235
|
+
return `${d.typeName} (0x${d.addr}) — ${attrInfo}`;
|
|
236
|
+
}).join('\n');
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (deviceEntries.length === 0) {
|
|
240
|
+
return (
|
|
241
|
+
<div className="info-box log-config-panel">
|
|
242
|
+
<h3>Logging Settings</h3>
|
|
243
|
+
<p className="log-config-empty">No devices connected</p>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div className="info-box log-config-panel">
|
|
250
|
+
<h3>Logging Settings</h3>
|
|
251
|
+
|
|
252
|
+
<div className="log-config-format-row">
|
|
253
|
+
<label className="log-config-rate-label">Format:</label>
|
|
254
|
+
<select
|
|
255
|
+
className="log-config-mode-select"
|
|
256
|
+
value={format}
|
|
257
|
+
onChange={e => setFormat(e.target.value as 'csv' | 'jsonl')}
|
|
258
|
+
disabled={disabled}
|
|
259
|
+
>
|
|
260
|
+
<option value="csv">CSV (decoded)</option>
|
|
261
|
+
<option value="jsonl">JSONL (raw polls)</option>
|
|
262
|
+
</select>
|
|
263
|
+
{format === 'csv' && (
|
|
264
|
+
<label className="log-config-csv-header-label">
|
|
265
|
+
<input
|
|
266
|
+
type="checkbox"
|
|
267
|
+
checked={csvHeader}
|
|
268
|
+
onChange={e => setCsvHeader(e.target.checked)}
|
|
269
|
+
disabled={disabled}
|
|
270
|
+
/>
|
|
271
|
+
Include metadata
|
|
272
|
+
</label>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div className="log-config-duration-row">
|
|
277
|
+
<label className="log-config-rate-label">Duration:</label>
|
|
278
|
+
<select
|
|
279
|
+
className="log-config-mode-select"
|
|
280
|
+
value={DURATION_PRESETS.find(p => p.ms === durationMs) ? durationMs : 'custom'}
|
|
281
|
+
onChange={e => {
|
|
282
|
+
const val = e.target.value;
|
|
283
|
+
if (val !== 'custom') setDurationMs(parseInt(val, 10));
|
|
284
|
+
}}
|
|
285
|
+
disabled={disabled}
|
|
286
|
+
>
|
|
287
|
+
{DURATION_PRESETS.map(p => (
|
|
288
|
+
<option key={p.ms} value={p.ms}>{p.label}</option>
|
|
289
|
+
))}
|
|
290
|
+
{!DURATION_PRESETS.find(p => p.ms === durationMs) && (
|
|
291
|
+
<option value="custom">Custom</option>
|
|
292
|
+
)}
|
|
293
|
+
</select>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div className="log-config-duration-slider-row">
|
|
297
|
+
<span className="log-config-slider-label">Short</span>
|
|
298
|
+
<input
|
|
299
|
+
type="range"
|
|
300
|
+
className="log-config-slider"
|
|
301
|
+
min="0"
|
|
302
|
+
max="1"
|
|
303
|
+
step="0.005"
|
|
304
|
+
value={durationToSlider(durationMs)}
|
|
305
|
+
onChange={e => {
|
|
306
|
+
const ms = sliderToDuration(parseFloat(e.target.value));
|
|
307
|
+
setDurationMs(ms);
|
|
308
|
+
}}
|
|
309
|
+
disabled={disabled}
|
|
310
|
+
/>
|
|
311
|
+
<span className="log-config-slider-label">Long</span>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div className="log-config-duration-display">
|
|
315
|
+
{formatDurationLabel(durationMs)}
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
{bytesPerMin > 0 && (
|
|
319
|
+
<div className="log-config-estimates">
|
|
320
|
+
<span>~{kbPerMin < 1 ? `${(kbPerMin * 1024).toFixed(0)} B/min` : kbPerMin < 1024 ? `${kbPerMin.toFixed(1)} KB/min` : `${(kbPerMin / 1024).toFixed(2)} MB/min`}</span>
|
|
321
|
+
{maxDurationSecs !== null && (
|
|
322
|
+
<span className="log-config-max-duration">
|
|
323
|
+
· Max: {formatDurationLabel(maxDurationSecs * 1000)} ({fsFreeBytes !== null ? `${(fsFreeBytes / 1024).toFixed(0)} KB free` : ''})
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
<div className="log-config-devices-summary">
|
|
330
|
+
<div className="log-config-summary-header">
|
|
331
|
+
<span className="log-config-rate-label">Devices:</span>
|
|
332
|
+
<button
|
|
333
|
+
className="log-config-edit-btn"
|
|
334
|
+
onClick={() => setShowDeviceDialog(true)}
|
|
335
|
+
disabled={disabled}
|
|
336
|
+
>
|
|
337
|
+
Edit…
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
<div className="log-config-summary-text">
|
|
341
|
+
{buildSummary().split('\n').map((line, i) => (
|
|
342
|
+
<div key={i}>{line}</div>
|
|
343
|
+
))}
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
{showDeviceDialog && (
|
|
348
|
+
<DeviceSelectDialog
|
|
349
|
+
entries={deviceEntries}
|
|
350
|
+
format={format}
|
|
351
|
+
onSave={handleDeviceSave}
|
|
352
|
+
onCancel={() => setShowDeviceDialog(false)}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import ConnManager from './ConnManager';
|
|
3
|
+
import './styles.css';
|
|
4
|
+
|
|
5
|
+
const connManager = ConnManager.getInstance();
|
|
6
|
+
|
|
7
|
+
interface LogFilesPanelProps {
|
|
8
|
+
refreshTrigger?: number;
|
|
9
|
+
onDownloadActiveChange?: (active: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function LogFilesPanel({ refreshTrigger, onDownloadActiveChange }: LogFilesPanelProps) {
|
|
13
|
+
const [files, setFiles] = useState<{name: string, size: number}[]>([]);
|
|
14
|
+
const [diskSize, setDiskSize] = useState(0);
|
|
15
|
+
const [diskUsed, setDiskUsed] = useState(0);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
|
|
18
|
+
const [downloadProgress, setDownloadProgress] = useState(0);
|
|
19
|
+
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
|
20
|
+
const [lastError, setLastError] = useState('');
|
|
21
|
+
|
|
22
|
+
const fetchFiles = async () => {
|
|
23
|
+
if (!connManager.getConnector().isConnected()) return;
|
|
24
|
+
setIsLoading(true);
|
|
25
|
+
setLastError('');
|
|
26
|
+
try {
|
|
27
|
+
// Request file listing for the logs folder on local filesystem
|
|
28
|
+
const resp = await connManager.getConnector().sendRICRESTMsg(
|
|
29
|
+
'filelist/local/logs', {}
|
|
30
|
+
);
|
|
31
|
+
const fileList = typeof resp === 'string' ? JSON.parse(resp) : resp;
|
|
32
|
+
setFiles((fileList.files || []).sort((a: {name: string}, b: {name: string}) => b.name.localeCompare(a.name)));
|
|
33
|
+
setDiskSize(fileList.diskSize || 0);
|
|
34
|
+
setDiskUsed(fileList.diskUsed || 0);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.warn('Failed to get file list', e);
|
|
37
|
+
setLastError('Failed to get file list');
|
|
38
|
+
}
|
|
39
|
+
setIsLoading(false);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
fetchFiles();
|
|
44
|
+
}, [refreshTrigger]);
|
|
45
|
+
|
|
46
|
+
const formatBytes = (bytes: number): string => {
|
|
47
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
48
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
49
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleDownload = async (file: {name: string, size: number}) => {
|
|
53
|
+
// Estimate download time and warn user for large transfers
|
|
54
|
+
const connMethod = connManager.getConnector().getConnMethod();
|
|
55
|
+
const isBLE = connMethod === 'WebBLE' || connMethod === 'PhoneBLE';
|
|
56
|
+
const estimatedBytesPerSec = isBLE ? 5000 : 50000;
|
|
57
|
+
const estimatedTimeSec = file.size / estimatedBytesPerSec;
|
|
58
|
+
|
|
59
|
+
if (estimatedTimeSec > 30) {
|
|
60
|
+
const timeStr = estimatedTimeSec >= 60
|
|
61
|
+
? `${Math.round(estimatedTimeSec / 60)} min ${Math.round(estimatedTimeSec % 60)} sec`
|
|
62
|
+
: `${Math.round(estimatedTimeSec)} sec`;
|
|
63
|
+
const confirmed = window.confirm(
|
|
64
|
+
`Download ${file.name} (${formatBytes(file.size)})?\n\n` +
|
|
65
|
+
`Estimated time over ${connMethod}: ~${timeStr}\n\n` +
|
|
66
|
+
`Continue?`
|
|
67
|
+
);
|
|
68
|
+
if (!confirmed) return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setDownloadingFile(file.name);
|
|
72
|
+
setDownloadProgress(0);
|
|
73
|
+
setLastError('');
|
|
74
|
+
onDownloadActiveChange?.(true);
|
|
75
|
+
try {
|
|
76
|
+
// Download from local/logs/<filename>
|
|
77
|
+
const filePath = `local/logs/${file.name}`;
|
|
78
|
+
const result = await connManager.getConnector().fsGetContents(
|
|
79
|
+
filePath,
|
|
80
|
+
'fs',
|
|
81
|
+
(received: number, total: number) => {
|
|
82
|
+
if (total > 0) {
|
|
83
|
+
setDownloadProgress(Math.round((received / total) * 100));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (result.downloadedOk && result.fileData) {
|
|
89
|
+
// Trigger browser download
|
|
90
|
+
const blob = new Blob([result.fileData], { type: 'application/octet-stream' });
|
|
91
|
+
const url = URL.createObjectURL(blob);
|
|
92
|
+
const a = document.createElement('a');
|
|
93
|
+
a.href = url;
|
|
94
|
+
a.download = file.name;
|
|
95
|
+
document.body.appendChild(a);
|
|
96
|
+
a.click();
|
|
97
|
+
document.body.removeChild(a);
|
|
98
|
+
URL.revokeObjectURL(url);
|
|
99
|
+
} else {
|
|
100
|
+
setLastError(`Failed to download ${file.name}`);
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.warn('Download failed', e);
|
|
104
|
+
setLastError(`Download error: ${file.name}`);
|
|
105
|
+
}
|
|
106
|
+
setDownloadingFile(null);
|
|
107
|
+
setDownloadProgress(0);
|
|
108
|
+
onDownloadActiveChange?.(false);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleDelete = async (file: {name: string, size: number}) => {
|
|
112
|
+
const confirmed = window.confirm(`Delete ${file.name} (${formatBytes(file.size)})?`);
|
|
113
|
+
if (!confirmed) return;
|
|
114
|
+
|
|
115
|
+
setDeletingFile(file.name);
|
|
116
|
+
setLastError('');
|
|
117
|
+
try {
|
|
118
|
+
const resp = await connManager.getConnector().sendRICRESTMsg(
|
|
119
|
+
`filedelete/local/logs/${file.name}`, {}
|
|
120
|
+
);
|
|
121
|
+
const r = resp as any;
|
|
122
|
+
if (r?.rslt === 'ok') {
|
|
123
|
+
await fetchFiles();
|
|
124
|
+
} else {
|
|
125
|
+
setLastError(`Failed to delete ${file.name}`);
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.warn('Delete failed', e);
|
|
129
|
+
setLastError(`Delete error: ${file.name}`);
|
|
130
|
+
}
|
|
131
|
+
setDeletingFile(null);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="info-box log-files-panel">
|
|
136
|
+
<div className="log-files-header">
|
|
137
|
+
<h3>Log Files</h3>
|
|
138
|
+
<button
|
|
139
|
+
className="log-files-refresh-button"
|
|
140
|
+
onClick={fetchFiles}
|
|
141
|
+
disabled={isLoading}
|
|
142
|
+
title="Refresh file list"
|
|
143
|
+
>
|
|
144
|
+
↻
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{diskSize > 0 && (
|
|
149
|
+
<div className="log-files-disk-info">
|
|
150
|
+
{formatBytes(diskUsed)} / {formatBytes(diskSize)} used
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{isLoading ? (
|
|
155
|
+
<div className="log-files-loading">Loading...</div>
|
|
156
|
+
) : files.length === 0 ? (
|
|
157
|
+
<div className="log-files-empty">No log files found</div>
|
|
158
|
+
) : (
|
|
159
|
+
<div className="log-files-list">
|
|
160
|
+
{files.map((file) => {
|
|
161
|
+
const isDownloading = downloadingFile === file.name;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div key={file.name} className="log-file-item">
|
|
165
|
+
<div className="log-file-info">
|
|
166
|
+
<div className="log-file-name" title={file.name}>
|
|
167
|
+
{file.name}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="log-file-size">{formatBytes(file.size)}</div>
|
|
170
|
+
</div>
|
|
171
|
+
<div className="log-file-actions">
|
|
172
|
+
<button
|
|
173
|
+
className="log-file-download-button"
|
|
174
|
+
onClick={() => handleDownload(file)}
|
|
175
|
+
disabled={isDownloading || downloadingFile !== null || deletingFile !== null}
|
|
176
|
+
title={`Download ${file.name}`}
|
|
177
|
+
>
|
|
178
|
+
{isDownloading ? `${downloadProgress}%` : '⬇'}
|
|
179
|
+
</button>
|
|
180
|
+
<button
|
|
181
|
+
className="log-file-delete-button"
|
|
182
|
+
onClick={() => handleDelete(file)}
|
|
183
|
+
disabled={isDownloading || downloadingFile !== null || deletingFile === file.name}
|
|
184
|
+
title={`Delete ${file.name}`}
|
|
185
|
+
>
|
|
186
|
+
{deletingFile === file.name ? '...' : '🗑'}
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
{lastError && (
|
|
196
|
+
<div className="logging-error">{lastError}</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|