325 lines
13 KiB
JavaScript
325 lines
13 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { AppBar, Toolbar, Typography, Box, Button, ThemeProvider, IconButton, Snackbar, Alert, Tooltip, FormControl, Select, MenuItem, InputLabel, Chip } from '@mui/material';
|
|
import MavlinkSession from './mavlink_session';
|
|
import dronecan from './dronecan';
|
|
import theme from './theme';
|
|
import NodeList from './NodeList';
|
|
import NodeLogs from './NodeLogs';
|
|
import NodeProperties from './NodeProperties';
|
|
import NodeParam from './NodeParam';
|
|
import About from './About';
|
|
import ToolsMenu from './ToolsMenu';
|
|
import PanelsMenu from './PanelsMenu';
|
|
import ConnectionSettingsModal from './ConnectionSettingsModal';
|
|
import DronecanLogo from './image/dronecan_logo.png';
|
|
import FileServer from './FileServer';
|
|
import './css/index.css';
|
|
import ConnectionIndicators from './ConnectionIndicators';
|
|
import DnsIcon from '@mui/icons-material/Dns';
|
|
import LanIcon from '@mui/icons-material/Lan';
|
|
import LanguageIcon from '@mui/icons-material/Language';
|
|
import CompactSidebar from './CompactSidebar';
|
|
import DynamicNodeIdServer from './services/DynamicNodeIdServer';
|
|
import { LanguageProvider, useTranslation } from './i18n/LanguageContext';
|
|
|
|
window.mavlinkSession = new MavlinkSession();
|
|
window.localNode = new dronecan.Node({name: "com.vimdrones.web_gui"});
|
|
localNode.on('sendFrame', (messageId, data, len) => {
|
|
mavlinkSession.sendCanFrame(localNode.bus, messageId, data, len);
|
|
});
|
|
|
|
|
|
localNode.on('uavcan.protocol.file.Read.Request', (transfer) => {
|
|
FileServer.handleReadRequest(transfer, localNode);
|
|
});
|
|
|
|
const App = () => {
|
|
return (
|
|
<ThemeProvider theme={theme}>
|
|
<LanguageProvider>
|
|
<AppContent />
|
|
</LanguageProvider>
|
|
</ThemeProvider>
|
|
);
|
|
};
|
|
|
|
const AppContent = () => {
|
|
const { t, language, setLanguage } = useTranslation();
|
|
const [nodes, setNodes] = useState({});
|
|
const [nodesUpdateTimestamp, setNodesUpdateTimestamp] = useState(0);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
|
const [multiNodeEditorEnable, setMultiNodeEditorEnable] = useState(false);
|
|
const [subWindowRef, setSubWindowRef] = useState({});
|
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
const [snackbarMessage, setSnackbarMessage] = useState('');
|
|
const [snackbarSeverity, setSnackbarSeverity] = useState('info');
|
|
const [selectedBus, setSelectedBus] = useState(0);
|
|
const [dnaServerActive, setDnaServerActive] = useState(false);
|
|
|
|
const openWindow = (windowTitle, windowPath, windowSize) => {
|
|
if (subWindowRef[windowPath] && !subWindowRef[windowPath].closed) {
|
|
subWindowRef[windowPath].focus();
|
|
return;
|
|
}
|
|
const newWindow = window.open(windowPath, windowTitle, windowSize);
|
|
if (newWindow) {
|
|
setSubWindowRef((prev) => ({ ...prev, [windowPath]: newWindow }));
|
|
|
|
newWindow.addEventListener('beforeunload', () => {
|
|
setSubWindowRef((prev) => {
|
|
const next = { ...prev };
|
|
delete next[windowPath];
|
|
return next;
|
|
});
|
|
});
|
|
} else {
|
|
console.error(`Main: Failed to open ${windowTitle}`);
|
|
}
|
|
};
|
|
|
|
const showMessage = (message, severity = 'info') => {
|
|
setSnackbarMessage(message);
|
|
setSnackbarSeverity(severity);
|
|
setSnackbarOpen(true);
|
|
};
|
|
|
|
const handleConnectionStatusChange = (isConnected) => {
|
|
setIsConnected(isConnected);
|
|
showMessage(
|
|
isConnected ? t('app.connected') : t('app.disconnected'),
|
|
isConnected ? 'success' : 'info'
|
|
);
|
|
};
|
|
|
|
const handleBusChange = (event) => {
|
|
const newBus = event.target.value;
|
|
if (newBus === selectedBus) return;
|
|
setSelectedBus(newBus);
|
|
|
|
if (window.localNode) {
|
|
window.localNode.changeBus(newBus);
|
|
showMessage(t('app.bus_switched', { bus: newBus }), 'info');
|
|
}
|
|
};
|
|
|
|
const handleNodeList = (nodeList) => {
|
|
setNodes(nodeList);
|
|
setNodesUpdateTimestamp(Date.now());
|
|
};
|
|
|
|
useEffect(() => {
|
|
localNode.on('nodeList', handleNodeList);
|
|
return () => {
|
|
localNode.off('nodeList');
|
|
};
|
|
}, []);
|
|
|
|
const handleOpenModal = () => {
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setModalOpen(false);
|
|
};
|
|
|
|
const handleToggleDnaServer = () => {
|
|
if (!window.dnaServer) {
|
|
window.dnaServer = new DynamicNodeIdServer(window.localNode);
|
|
}
|
|
|
|
if (window.dnaServer.getStatus().isActive) {
|
|
window.dnaServer.stop();
|
|
setDnaServerActive(false);
|
|
showMessage(t('app.dna_stopped'), 'info');
|
|
} else {
|
|
const success = window.dnaServer.start(1, 125);
|
|
setDnaServerActive(success);
|
|
showMessage(success ? t('app.dna_started') : t('app.dna_failed'), success ? 'success' : 'error');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<AppBar position="static">
|
|
<Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center'}}>
|
|
<ToolsMenu openWindow={openWindow.bind(this)} />
|
|
<PanelsMenu openWindow={openWindow.bind(this)} />
|
|
</Box>
|
|
<Box sx={{flexGrow: 2, display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center'}}>
|
|
<Box sx={{display: 'flex', flexDirection: 'row', alignItems: 'center'}} ml={0.5} mr={0.5}>
|
|
<a href="https://dev.vimdrones.com" target="_blank" rel="noreferrer" style={{height: 30}}>
|
|
<img src={DronecanLogo} alt="DroneCAN" style={{ height: 30}} />
|
|
</a>
|
|
</Box>
|
|
<Typography variant="caption">
|
|
{t('app.title')}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', gap: 1}}>
|
|
<ConnectionIndicators
|
|
isConnected={isConnected}
|
|
mavlinkSession={window.mavlinkSession}
|
|
localNode={window.localNode}
|
|
/>
|
|
|
|
<FormControl
|
|
size="small"
|
|
sx={{
|
|
minWidth: 80,
|
|
'& .MuiOutlinedInput-root': {
|
|
height: 30,
|
|
},
|
|
'& .MuiSelect-select': {
|
|
paddingTop: 0.5,
|
|
paddingBottom: 0.5,
|
|
fontSize: '0.8rem'
|
|
}
|
|
}}
|
|
>
|
|
<Select
|
|
value={selectedBus}
|
|
onChange={handleBusChange}
|
|
displayEmpty
|
|
variant="outlined"
|
|
sx={{
|
|
backgroundColor: 'background.paper',
|
|
}}
|
|
>
|
|
<MenuItem value={0}>{t('app.bus', { n: 1 })}</MenuItem>
|
|
<MenuItem value={1}>{t('app.bus', { n: 2 })}</MenuItem>
|
|
<MenuItem value={2}>{t('app.bus', { n: 3 })}</MenuItem>
|
|
<MenuItem value={3}>{t('app.bus', { n: 4 })}</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<Button
|
|
variant="outlined"
|
|
color={dnaServerActive ? "success" : "primary"}
|
|
startIcon={
|
|
dnaServerActive ?
|
|
<DnsIcon sx={{
|
|
animation: 'pulse 1.5s infinite',
|
|
'@keyframes pulse': {
|
|
'0%': { opacity: 0.6 },
|
|
'50%': { opacity: 1 },
|
|
'100%': { opacity: 0.6 },
|
|
}
|
|
}} /> :
|
|
<DnsIcon />
|
|
}
|
|
onClick={handleToggleDnaServer}
|
|
sx={dnaServerActive ? {
|
|
borderColor: 'success.main',
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(76, 175, 80, 0.08)',
|
|
borderColor: 'success.dark'
|
|
}
|
|
} : {}}
|
|
>
|
|
{t('app.dna')}
|
|
</Button>
|
|
|
|
<Tooltip title={language === 'en' ? '中文' : 'English'}>
|
|
<IconButton
|
|
size="small"
|
|
color="inherit"
|
|
onClick={() => setLanguage(language === 'en' ? 'zh' : 'en')}
|
|
sx={{ mr: 0.5 }}
|
|
>
|
|
<LanguageIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
startIcon={<LanIcon />}
|
|
onClick={handleOpenModal}
|
|
>
|
|
{t('app.adapter')}
|
|
</Button>
|
|
</Box>
|
|
</Toolbar>
|
|
</AppBar>
|
|
<Box sx={{ display: 'flex', flexDirection: 'row', flexGrow: 1, height: '95vh', gap: 0.5, p: 1 }}>
|
|
<CompactSidebar
|
|
nodes={nodes}
|
|
selectedNodeId={selectedNodeId}
|
|
setSelectedNodeId={setSelectedNodeId}
|
|
nodesUpdateTimestamp={nodesUpdateTimestamp}
|
|
/>
|
|
|
|
<Box
|
|
sx={{
|
|
minWidth: 550,
|
|
display: { xs: 'none', md: 'flex' },
|
|
flexDirection: 'column',
|
|
gap: 0.5,
|
|
}}
|
|
>
|
|
<NodeList
|
|
nodes={nodes}
|
|
selectedNodeId={selectedNodeId}
|
|
setSelectedNodeId={setSelectedNodeId.bind(this)}
|
|
nodesUpdateTimestamp={nodesUpdateTimestamp}
|
|
/>
|
|
<NodeLogs/>
|
|
</Box>
|
|
|
|
<Box
|
|
display="flex"
|
|
flexDirection="column"
|
|
sx={{
|
|
gap: 0.5,
|
|
ml: { xs: 1, md: 0 },
|
|
flexGrow: 1,
|
|
}}
|
|
>
|
|
{selectedNodeId && (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1, gap: 0.5 }}>
|
|
<NodeProperties
|
|
nodeId={selectedNodeId}
|
|
nodes={nodes}
|
|
multiNodeEditorEnable={multiNodeEditorEnable}
|
|
setMultiNodeEditorEnable={setMultiNodeEditorEnable.bind(this)}
|
|
nodesUpdateTimestamp={nodesUpdateTimestamp}
|
|
/>
|
|
<NodeParam
|
|
nodeId={selectedNodeId}
|
|
nodes={nodes}
|
|
multiNodeEditorEnable={multiNodeEditorEnable}
|
|
/>
|
|
</Box>
|
|
)}
|
|
|
|
{selectedNodeId === null && (
|
|
<About />
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
<ConnectionSettingsModal
|
|
open={modalOpen}
|
|
onClose={handleCloseModal}
|
|
onConnectionStatusChange={handleConnectionStatusChange}
|
|
showMessage={showMessage.bind(this)}
|
|
selectedBus={selectedBus}
|
|
onBusChange={handleBusChange}
|
|
/>
|
|
<Snackbar
|
|
open={snackbarOpen}
|
|
autoHideDuration={6000}
|
|
onClose={() => setSnackbarOpen(false)}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
>
|
|
<Alert onClose={() => setSnackbarOpen(false)} severity={snackbarSeverity}>
|
|
{snackbarMessage}
|
|
</Alert>
|
|
</Snackbar>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default App; |