diff --git a/ui/social-bot/src/components/SettingsPanel.jsx b/ui/social-bot/src/components/SettingsPanel.jsx index 5da9a21..4668d25 100644 --- a/ui/social-bot/src/components/SettingsPanel.jsx +++ b/ui/social-bot/src/components/SettingsPanel.jsx @@ -15,7 +15,7 @@ import { simulateStepResponse, validatePID, } from '../hooks/useSettings.js'; -const VIEWS = ['PID', 'Sensors', 'Network', 'Firmware', 'Diagnostics', 'Backup']; +const VIEWS = ['Parameters', 'PID', 'Sensors', 'Network', 'Firmware', 'Diagnostics', 'Backup']; function ValidationBadges({ warnings }) { if (!warnings?.length) return ( @@ -377,6 +377,204 @@ function DiagnosticsView({ exportDiagnosticsBundle, subscribe, connected }) { ); } +function ParametersView({ callService, subscribe, connected }) { + const [params, setParams] = useState({}); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState(null); + const [result, setResult] = useState(null); + const [searchFilter, setSearchFilter] = useState(''); + + // Fetch all ROS parameters on mount + useEffect(() => { + if (!connected || !callService) return; + + setLoading(true); + // Call get_parameters service to list all params + callService('/rcl_interfaces/srv/GetParameters', { + names: [] // Empty list means get all parameters + }).then((resp) => { + if (resp.values) { + const newParams = {}; + resp.names.forEach((name, i) => { + newParams[name] = resp.values[i]; + }); + setParams(newParams); + } + setLoading(false); + }).catch((err) => { + console.error('Failed to fetch parameters:', err); + setLoading(false); + }); + }, [connected, callService]); + + // Handle parameter edit + const handleParamChange = (paramName, newValue) => { + setParams(p => ({ ...p, [paramName]: newValue })); + }; + + // Apply parameter update + const applyParam = async (paramName, value) => { + if (!callService) return; + setUpdating(paramName); + try { + const resp = await callService('/rcl_interfaces/srv/SetParameters', { + parameters: [{ + name: paramName, + value: { + type: detectParamType(value), + ...(detectParamType(value) === 4 ? { integer_value: parseInt(value) } : + detectParamType(value) === 1 ? { double_value: parseFloat(value) } : + detectParamType(value) === 5 ? { bool_value: Boolean(value) } : + detectParamType(value) === 3 ? { string_value: String(value) } : {}) + } + }] + }); + if (resp.results?.[0]?.successful) { + setResult({ ok: true, msg: `${paramName} updated` }); + } else { + setResult({ ok: false, msg: `Failed to update ${paramName}` }); + } + setTimeout(() => setResult(null), 3000); + } catch (err) { + setResult({ ok: false, msg: 'Update failed: ' + err.message }); + setTimeout(() => setResult(null), 3000); + } finally { + setUpdating(null); + } + }; + + // Group parameters by node name (part before /) + const grouped = {}; + Object.keys(params).forEach(name => { + const parts = name.split('/'); + const node = parts.length > 1 ? parts[1] : 'root'; + if (!grouped[node]) grouped[node] = []; + grouped[node].push(name); + }); + + // Filter by search + const filteredGroups = {}; + Object.entries(grouped).forEach(([node, names]) => { + const filtered = names.filter(n => n.toLowerCase().includes(searchFilter.toLowerCase())); + if (filtered.length > 0) { + filteredGroups[node] = filtered; + } + }); + + const detectParamType = (val) => { + if (typeof val === 'boolean') return 5; // bool + if (Number.isInteger(val)) return 4; // int64 + if (typeof val === 'number') return 1; // double + return 3; // string + }; + + const renderParamInput = (name, value) => { + const type = detectParamType(value); + if (type === 5) { // bool + return ( + + ); + } + return ( + handleParamChange(name, e.target.value)} + className="flex-1 bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-cyan-200 focus:outline-none focus:border-cyan-700" /> + ); + }; + + return ( +