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 = () => (
);
const renderRangeSettingsDialog = () => (
);
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 (
}
onClick={() => setShowIdSelector(true)}
sx={{ textTransform: 'none' }}
>
Actuator IDs ({activeActuatorIds.length})
}
onClick={() => setShowSettingsModal(true)}
sx={{ textTransform: 'none' }}
>
Range Settings
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(', ')
}]
}
onClick={handleZeroAll}
>
Zero All
);
};
export default ActuatorPanel;