import React, { useState, useEffect } from 'react'; import { Paper, Box, Typography, Card, CardContent, Slider, TextField, AppBar, Toolbar, Button, IconButton, FormGroup, FormControlLabel, Checkbox, Dialog, DialogTitle, DialogContent, DialogActions, Select, MenuItem, FormControl, InputLabel, Tab, Tabs, Divider, Grid } from '@mui/material'; import PanToolIcon from '@mui/icons-material/PanTool'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PauseIcon from '@mui/icons-material/Pause'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; import SettingsIcon from '@mui/icons-material/Settings'; const MAX_ACTUATOR_IDS = 256; const COMMAND_TYPES = { UNITLESS: 0, // [-1, 1] POSITION: 1, // meter or radian FORCE: 2, // Newton or Newton metre SPEED: 3 // meter per second or radian per second }; const COMMAND_TYPE_LABELS = { 0: 'Unitless [-1, 1]', 1: 'Position (m/rad)', 2: 'Force (N/Nm)', 3: 'Speed (m/s, rad/s)' }; const DEFAULT_RANGES = { 0: { min: -1, max: 1 }, // Unitless is fixed -1 to 1 1: { min: -2, max: 2 }, // Position range (adjustable) 2: { min: -10, max: 10 }, // Force range (adjustable) 3: { min: -20, max: 20 } // Speed range (adjustable) }; const ActuatorPanel = () => { const [enabledActuatorIds, setEnabledActuatorIds] = useState( Array(MAX_ACTUATOR_IDS).fill(false).map((_, i) => i < 4) ); const [actuatorData, setActuatorData] = useState([]); const [commandValues, setCommandValues] = useState({}); const [showSettingsModal, setShowSettingsModal] = useState(false); const [defaultRanges, setDefaultRanges] = useState({...DEFAULT_RANGES}); const [broadcastRate, setBroadcastRate] = useState(10); const [isPaused, setIsPaused] = useState(false); const [showIdSelector, setShowIdSelector] = useState(false); const nodeId = 0; const activeActuatorIds = enabledActuatorIds .map((enabled, id) => enabled ? id : null) .filter(id => id !== null); const togglePause = () => { setIsPaused(!isPaused); }; const handleBroadcastRateChange = (e) => { const newValue = parseInt(e.target.value) || 0; const safeValue = Math.max(1, Math.min(100, newValue)); setBroadcastRate(safeValue); }; const toggleActuatorId = (id) => { const newEnabledIds = [...enabledActuatorIds]; newEnabledIds[id] = !newEnabledIds[id]; setEnabledActuatorIds(newEnabledIds); }; const handleDefaultRangeChange = (type, field, value) => { const parsedValue = parseFloat(value); if (isNaN(parsedValue)) return; if (field === 'min' && parsedValue >= defaultRanges[type].max) return; if (field === 'max' && parsedValue <= defaultRanges[type].min) return; setDefaultRanges(prev => ({ ...prev, [type]: { ...prev[type], [field]: parsedValue } })); }; const applyRangesToAllOfType = (type) => { if (type === COMMAND_TYPES.UNITLESS) return; // Don't change unitless ranges const newMin = defaultRanges[type].min; const newMax = defaultRanges[type].max; activeActuatorIds.forEach(id => { if ((commandTypes[id] || COMMAND_TYPES.UNITLESS) === type) { setSliderMins(prev => ({ ...prev, [id]: newMin })); setSliderMaxs(prev => ({ ...prev, [id]: newMax })); } }); }; const applyAllRanges = () => { activeActuatorIds.forEach(id => { const type = commandTypes[id] || COMMAND_TYPES.UNITLESS; if (type !== COMMAND_TYPES.UNITLESS) { setSliderMins(prev => ({ ...prev, [id]: defaultRanges[type].min })); setSliderMaxs(prev => ({ ...prev, [id]: defaultRanges[type].max })); } }); }; useEffect(() => { const activeIds = activeActuatorIds; const initialCommandValues = {}; activeIds.forEach(id => { initialCommandValues[id] = commandValues[id] || 0; }); setCommandValues(initialCommandValues); const initialActuatorData = activeIds.map(id => ({ actuator_id: id, position: null, force: null, speed: null, power_rating_pct: null, })); setActuatorData(initialActuatorData); }, [enabledActuatorIds]); const handleCommandChange = (id, value) => { setCommandValues(prev => ({ ...prev, [id]: value })); }; const handleCommandInputChange = (id, event) => { let value = parseFloat(event.target.value); if (isNaN(value)) value = 0; value = Math.max(sliderMins[id] || -1, Math.min(sliderMaxs[id] || 1, value)); handleCommandChange(id, value); }; const handleZeroAll = () => { const newCommandValues = {}; activeActuatorIds.forEach(id => { newCommandValues[id] = 0; }); setCommandValues(newCommandValues); }; const handleZeroOne = (id) => { setCommandValues(prev => ({ ...prev, [id]: 0 })); }; useEffect(() => { const localNode = window.opener?.localNode; if (!localNode) return; const handleActuatorData = (transfer) => { const msg = transfer.payload; if (!msg) return; try { const msgObj = msg.toObj ? msg.toObj() : msg; if (!msgObj || typeof msgObj.actuator_id !== 'number') return; if (enabledActuatorIds[msgObj.actuator_id]) { setActuatorData(prev => { const existingIndex = prev.findIndex(a => a.actuator_id === msgObj.actuator_id); const newData = [...prev]; const updatedActuator = { actuator_id: msgObj.actuator_id, position: msgObj.position, force: msgObj.force, speed: msgObj.speed, power_rating_pct: msgObj.power_rating_pct || null, status_flags: msgObj.status_flags }; if (existingIndex !== -1) { newData[existingIndex] = updatedActuator; } else { newData.push(updatedActuator); } return newData; }); } } catch (error) { console.error('Error processing actuator status:', error); } }; localNode.on('uavcan.equipment.actuator.Status', handleActuatorData); return () => { localNode.off('uavcan.equipment.actuator.Status', handleActuatorData); }; }, [enabledActuatorIds]); useEffect(() => { // Create or retrieve the worker if (!window.ActuatorPanelWorker) { window.ActuatorPanelWorker = new Worker(new URL('./workers/actuator-command-worker.js', import.meta.url)); console.log('Created actuator command worker'); } window.ActuatorPanelWorker.onmessage = (event) => { const localNode = window.opener?.localNode; if (!localNode) return; if (event.data.type === 'requestActuatorCommand') { try { const allCommands = []; activeActuatorIds.forEach(id => { const type = commandTypes[id] || COMMAND_TYPES.UNITLESS; const value = commandValues[id] || 0; if (value < (sliderMins[id] || -1) || value > (sliderMaxs[id] || 1)) { // Value out of range, skipping } else { allCommands.push({ id, type, value }); } }); const MAX_COMMANDS_PER_BATCH = 15; if (allCommands.length > MAX_COMMANDS_PER_BATCH) { const batchCount = Math.ceil(allCommands.length / MAX_COMMANDS_PER_BATCH); for (let i = 0; i < batchCount; i++) { const startIdx = i * MAX_COMMANDS_PER_BATCH; const endIdx = Math.min(startIdx + MAX_COMMANDS_PER_BATCH, allCommands.length); const batchCommands = allCommands.slice(startIdx, endIdx); localNode.sendUavcanEquipmentActuatorArrayCommand(0, batchCommands); } } else if (allCommands.length > 0) { localNode.sendUavcanEquipmentActuatorArrayCommand(0, allCommands); } } catch (error) { console.error('Error sending actuator commands:', error); } } } // Clean up return () => { }; }, [activeActuatorIds, commandTypes, commandValues, sliderMins, sliderMaxs]); useEffect(() => { if (!window.ActuatorPanelWorker) return; if (!isPaused) { console.log(`Starting Actuator commands with rate: ${broadcastRate}Hz`); window.ActuatorPanelWorker.postMessage({ type: 'actuator', command: 'start', rate: broadcastRate }); } else { console.log('Pausing Actuator commands'); window.ActuatorPanelWorker.postMessage({ type: 'actuator', command: 'stop' }); } return () => { window.ActuatorPanelWorker.postMessage({ type: 'actuator', command: 'stop' }); }; }, [isPaused, broadcastRate]); const renderIdSelectorDialog = () => ( setShowIdSelector(false)}> Select Actuator IDs Select Actuator IDs: {Array(MAX_ACTUATOR_IDS).fill(0).map((_, id) => ( toggleActuatorId(id)} /> } label={id} /> ))} ); const renderRangeSettingsDialog = () => ( setShowSettingsModal(false)} maxWidth="sm" fullWidth > Command Type Range Settings Configure default ranges for each command type. These settings can be applied to all actuators. {COMMAND_TYPE_LABELS[0]} Unitless command range is fixed at -1 to 1 {COMMAND_TYPE_LABELS[1]} handleDefaultRangeChange(1, 'min', e.target.value)} InputProps={{ inputProps: { step: 0.1 } }} /> handleDefaultRangeChange(1, 'max', e.target.value)} InputProps={{ inputProps: { step: 0.1 } }} /> {COMMAND_TYPE_LABELS[2]} handleDefaultRangeChange(2, 'min', e.target.value)} InputProps={{ inputProps: { step: 0.5 } }} /> handleDefaultRangeChange(2, 'max', e.target.value)} InputProps={{ inputProps: { step: 0.5 } }} /> {COMMAND_TYPE_LABELS[3]} handleDefaultRangeChange(3, 'min', e.target.value)} InputProps={{ inputProps: { step: 1 } }} /> handleDefaultRangeChange(3, 'max', e.target.value)} InputProps={{ inputProps: { step: 1 } }} /> ); const [commandTypes, setCommandTypes] = useState({}); const [sliderMins, setSliderMins] = useState({}); const [sliderMaxs, setSliderMaxs] = useState({}); const handleActuatorCommandTypeChange = (id, newType) => { setCommandTypes(prev => { const updated = { ...prev, [id]: newType }; return updated; }); if (newType === COMMAND_TYPES.UNITLESS) { setSliderMins(prev => ({ ...prev, [id]: -1 })); setSliderMaxs(prev => ({ ...prev, [id]: 1 })); } else { setSliderMins(prev => ({ ...prev, [id]: defaultRanges[newType].min })); setSliderMaxs(prev => ({ ...prev, [id]: defaultRanges[newType].max })); } setCommandValues(prev => ({ ...prev, [id]: 0 })); }; useEffect(() => { const activeIds = activeActuatorIds; const initialCommandValues = {}; const initialCommandTypes = {}; const initialSliderMins = {}; const initialSliderMaxs = {}; activeIds.forEach(id => { initialCommandValues[id] = commandValues[id] || 0; initialCommandTypes[id] = commandTypes[id] || COMMAND_TYPES.UNITLESS; const type = commandTypes[id] || COMMAND_TYPES.UNITLESS; if (type === COMMAND_TYPES.UNITLESS) { initialSliderMins[id] = -1; initialSliderMaxs[id] = 1; } else { initialSliderMins[id] = sliderMins[id] || defaultRanges[type].min; initialSliderMaxs[id] = sliderMaxs[id] || defaultRanges[type].max; } }); setCommandValues(initialCommandValues); setCommandTypes(initialCommandTypes); setSliderMins(initialSliderMins); setSliderMaxs(initialSliderMaxs); const initialActuatorData = activeIds.map(id => ({ actuator_id: id, position: null, force: null, speed: null, power_rating_pct: null, })); setActuatorData(initialActuatorData); }, [enabledActuatorIds]); return ( Broadcast Rate: {isPaused ? : } {renderIdSelectorDialog()} {renderRangeSettingsDialog()} {actuatorData.map((actuator) => ( ID: {actuator.actuator_id} Pos: {actuator.position !== null ? actuator.position.toFixed(3) : "NC"} Force: {actuator.force !== null ? `${actuator.force.toFixed(2)} N` : "NC"} Speed: {actuator.speed !== null ? `${actuator.speed.toFixed(2)} rad/s` : "NC"} RAT: {actuator.power_rating_pct !== null ? actuator.power_rating_pct === 127 ? "unknown" : `${actuator.power_rating_pct.toFixed(1)} %` : "NC"} handleCommandInputChange(actuator.actuator_id, e)} /> handleCommandChange(actuator.actuator_id, value)} /> ))} cmd: [{ activeActuatorIds .map(id => { const type = commandTypes[id] || COMMAND_TYPES.UNITLESS; const typeLabel = Object.keys(COMMAND_TYPES).find(key => COMMAND_TYPES[key] === type).toLowerCase(); return `${id}:${(commandValues[id] || 0).toFixed(3)}[${typeLabel}]`; }) .join(', ') }] ); }; export default ActuatorPanel;