add Chinese translate
This commit is contained in:
@@ -10,6 +10,7 @@ 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';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const MAX_ACTUATOR_IDS = 256;
|
||||
|
||||
@@ -50,6 +51,7 @@ const ActuatorPanel = () => {
|
||||
const [showIdSelector, setShowIdSelector] = useState(false);
|
||||
|
||||
const nodeId = 0;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const activeActuatorIds = enabledActuatorIds
|
||||
.map((enabled, id) => enabled ? id : null)
|
||||
@@ -282,9 +284,9 @@ const ActuatorPanel = () => {
|
||||
|
||||
const renderIdSelectorDialog = () => (
|
||||
<Dialog open={showIdSelector} onClose={() => setShowIdSelector(false)}>
|
||||
<DialogTitle>Select Actuator IDs</DialogTitle>
|
||||
<DialogTitle>{t('act.select_ids_title')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Select Actuator IDs:</Typography>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>{t('act.select_ids_label')}</Typography>
|
||||
<FormGroup sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 1 }}>
|
||||
{Array(MAX_ACTUATOR_IDS).fill(0).map((_, id) => (
|
||||
<FormControlLabel
|
||||
@@ -302,7 +304,7 @@ const ActuatorPanel = () => {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowIdSelector(false)} color="primary">
|
||||
Done
|
||||
{t('act.done')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -316,11 +318,11 @@ const ActuatorPanel = () => {
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
Command Type Range Settings
|
||||
{t('act.range_title')}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" sx={{ mb: 2, fontStyle: 'italic' }}>
|
||||
Configure default ranges for each command type. These settings can be applied to all actuators.
|
||||
{t('act.range_instruction')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
@@ -328,7 +330,7 @@ const ActuatorPanel = () => {
|
||||
{COMMAND_TYPE_LABELS[0]}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Unitless command range is fixed at -1 to 1
|
||||
{t('act.unitless_fixed')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -339,7 +341,7 @@ const ActuatorPanel = () => {
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
label="Min"
|
||||
label={t('act.min')}
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
@@ -350,7 +352,7 @@ const ActuatorPanel = () => {
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
label="Max"
|
||||
label={t('act.max')}
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
@@ -366,7 +368,7 @@ const ActuatorPanel = () => {
|
||||
onClick={() => applyRangesToAllOfType(1)}
|
||||
fullWidth
|
||||
>
|
||||
Apply
|
||||
{t('act.apply')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -379,7 +381,7 @@ const ActuatorPanel = () => {
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
label="Min"
|
||||
label={t('act.min')}
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
@@ -390,7 +392,7 @@ const ActuatorPanel = () => {
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
label="Max"
|
||||
label={t('act.max')}
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
@@ -406,7 +408,7 @@ const ActuatorPanel = () => {
|
||||
onClick={() => applyRangesToAllOfType(2)}
|
||||
fullWidth
|
||||
>
|
||||
Apply
|
||||
{t('act.apply')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -419,7 +421,7 @@ const ActuatorPanel = () => {
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
label="Min"
|
||||
label={t('act.min')}
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
@@ -430,7 +432,7 @@ const ActuatorPanel = () => {
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
label="Max"
|
||||
label={t('act.max')}
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
@@ -446,7 +448,7 @@ const ActuatorPanel = () => {
|
||||
onClick={() => applyRangesToAllOfType(3)}
|
||||
fullWidth
|
||||
>
|
||||
Apply
|
||||
{t('act.apply')}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -458,13 +460,13 @@ const ActuatorPanel = () => {
|
||||
color="primary"
|
||||
variant="contained"
|
||||
>
|
||||
Apply All Ranges
|
||||
{t('act.apply_all')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowSettingsModal(false)}
|
||||
color="primary"
|
||||
>
|
||||
Close
|
||||
{t('act.close')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -553,7 +555,7 @@ const ActuatorPanel = () => {
|
||||
onClick={() => setShowIdSelector(true)}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Actuator IDs ({activeActuatorIds.length})
|
||||
{t('act.ids', { count: activeActuatorIds.length })}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -566,14 +568,14 @@ const ActuatorPanel = () => {
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Range Settings
|
||||
{t('act.range_settings')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>Broadcast Rate:</Typography>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>{t('act.broadcast_rate')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
@@ -642,22 +644,22 @@ const ActuatorPanel = () => {
|
||||
width: '50%'
|
||||
}}>
|
||||
<Box sx={{flexGrow: 1}}>
|
||||
<Typography variant="body2" color="textSecondary">ID: {actuator.actuator_id}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">{t('act.id')} {actuator.actuator_id}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Pos: {actuator.position !== null ? actuator.position.toFixed(3) : "NC"}
|
||||
{t('act.pos')} {actuator.position !== null ? actuator.position.toFixed(3) : t('act.nc')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Force: {actuator.force !== null ? `${actuator.force.toFixed(2)} N` : "NC"}
|
||||
{t('act.force')} {actuator.force !== null ? `${actuator.force.toFixed(2)} N` : t('act.nc')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Speed: {actuator.speed !== null ? `${actuator.speed.toFixed(2)} rad/s` : "NC"}
|
||||
{t('act.speed')} {actuator.speed !== null ? `${actuator.speed.toFixed(2)} rad/s` : t('act.nc')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
RAT: {actuator.power_rating_pct !== null
|
||||
? actuator.power_rating_pct === 127
|
||||
? "unknown"
|
||||
: `${actuator.power_rating_pct.toFixed(1)} %`
|
||||
: "NC"}
|
||||
{t('act.rat')} {actuator.power_rating_pct !== null
|
||||
? actuator.power_rating_pct === 127
|
||||
? t('act.unknown')
|
||||
: `${actuator.power_rating_pct.toFixed(1)} %`
|
||||
: t('act.nc')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -668,10 +670,10 @@ const ActuatorPanel = () => {
|
||||
variant="outlined"
|
||||
sx={{ height: '30px', fontSize: '0.8rem' }}
|
||||
>
|
||||
<MenuItem value={COMMAND_TYPES.UNITLESS}>Unitless</MenuItem>
|
||||
<MenuItem value={COMMAND_TYPES.POSITION}>Position</MenuItem>
|
||||
<MenuItem value={COMMAND_TYPES.FORCE}>Force</MenuItem>
|
||||
<MenuItem value={COMMAND_TYPES.SPEED}>Speed</MenuItem>
|
||||
<MenuItem value={COMMAND_TYPES.UNITLESS}>{t('act.type_unitless')}</MenuItem>
|
||||
<MenuItem value={COMMAND_TYPES.POSITION}>{t('act.type_position')}</MenuItem>
|
||||
<MenuItem value={COMMAND_TYPES.FORCE}>{t('act.type_force')}</MenuItem>
|
||||
<MenuItem value={COMMAND_TYPES.SPEED}>{t('act.type_speed')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -700,7 +702,7 @@ const ActuatorPanel = () => {
|
||||
fullWidth
|
||||
size="small"
|
||||
>
|
||||
Zero
|
||||
{t('act.zero')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -749,7 +751,7 @@ const ActuatorPanel = () => {
|
||||
}}>
|
||||
<Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
cmd: [{
|
||||
{t('act.cmd')} [{
|
||||
activeActuatorIds
|
||||
.map(id => {
|
||||
const type = commandTypes[id] || COMMAND_TYPES.UNITLESS;
|
||||
@@ -768,7 +770,7 @@ const ActuatorPanel = () => {
|
||||
startIcon={<PanToolIcon />}
|
||||
onClick={handleZeroAll}
|
||||
>
|
||||
Zero All
|
||||
{t('act.zero_all')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
56
src/App.js
56
src/App.js
@@ -17,8 +17,10 @@ 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"});
|
||||
@@ -32,6 +34,17 @@ localNode.on('uavcan.protocol.file.Read.Request', (transfer) => {
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -75,7 +88,7 @@ const App = () => {
|
||||
const handleConnectionStatusChange = (isConnected) => {
|
||||
setIsConnected(isConnected);
|
||||
showMessage(
|
||||
isConnected ? 'Successfully connected to device' : 'Disconnected from device',
|
||||
isConnected ? t('app.connected') : t('app.disconnected'),
|
||||
isConnected ? 'success' : 'info'
|
||||
);
|
||||
};
|
||||
@@ -87,7 +100,7 @@ const App = () => {
|
||||
|
||||
if (window.localNode) {
|
||||
window.localNode.changeBus(newBus);
|
||||
showMessage(`Switched to CAN bus ${newBus}`, 'info');
|
||||
showMessage(t('app.bus_switched', { bus: newBus }), 'info');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -119,16 +132,16 @@ const App = () => {
|
||||
if (window.dnaServer.getStatus().isActive) {
|
||||
window.dnaServer.stop();
|
||||
setDnaServerActive(false);
|
||||
showMessage('DNA server stopped', 'info');
|
||||
showMessage(t('app.dna_stopped'), 'info');
|
||||
} else {
|
||||
const success = window.dnaServer.start(1, 125);
|
||||
setDnaServerActive(success);
|
||||
showMessage(success ? 'DNA server started' : 'Failed to start DNA server', success ? 'success' : 'error');
|
||||
showMessage(success ? t('app.dna_started') : t('app.dna_failed'), success ? 'success' : 'error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<>
|
||||
<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'}}>
|
||||
@@ -142,7 +155,7 @@ const App = () => {
|
||||
</a>
|
||||
</Box>
|
||||
<Typography variant="caption">
|
||||
DroneCAN Web Tools
|
||||
{t('app.title')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', gap: 1}}>
|
||||
@@ -175,10 +188,10 @@ const App = () => {
|
||||
backgroundColor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<MenuItem value={0}>Bus 1</MenuItem>
|
||||
<MenuItem value={1}>Bus 2</MenuItem>
|
||||
<MenuItem value={2}>Bus 3</MenuItem>
|
||||
<MenuItem value={3}>Bus 4</MenuItem>
|
||||
<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>
|
||||
|
||||
@@ -206,16 +219,27 @@ const App = () => {
|
||||
}
|
||||
} : {}}
|
||||
>
|
||||
DNA
|
||||
{t('app.dna')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
<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}
|
||||
>
|
||||
Adapter
|
||||
{t('app.adapter')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
@@ -294,7 +318,7 @@ const App = () => {
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import PauseIcon from '@mui/icons-material/Pause';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import { toYaml } from './dronecan/message_format_utils';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const BusMonitor = () => {
|
||||
const [transfers, setTransfers] = useState([]);
|
||||
@@ -18,6 +19,7 @@ const BusMonitor = () => {
|
||||
const [selectedTransfer, setSelectedTransfer] = useState(null);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [messageYaml, setMessageYaml] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const tableContainerRef = useRef(null);
|
||||
const maxTransfers = 1000; // Maximum number of transfers to store
|
||||
|
||||
@@ -29,23 +31,23 @@ const BusMonitor = () => {
|
||||
let yamlText = '';
|
||||
if (transfer.data && transfer.data.toObj) {
|
||||
const msgObj = transfer.data.toObj();
|
||||
yamlText = `### Message details\n`;
|
||||
yamlText += `Direction: ${transfer.direction}\n`;
|
||||
yamlText += `Time: ${transfer.timestamp}\n`;
|
||||
yamlText += `CAN ID: ${transfer.frameId}\n`;
|
||||
yamlText += `Source Node: ${transfer.sourceNodeId}\n`;
|
||||
yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`;
|
||||
yamlText += `Data Type: ${transfer.dataType}\n\n`;
|
||||
yamlText += `### Message Payload\n`;
|
||||
yamlText = `${t('bus.details_heading')}\n`;
|
||||
yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`;
|
||||
yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`;
|
||||
yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`;
|
||||
yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`;
|
||||
yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`;
|
||||
yamlText += `${t('bus.detail_data_type')} ${transfer.dataType}\n\n`;
|
||||
yamlText += `${t('bus.payload_heading')}\n`;
|
||||
yamlText += toYaml(msgObj);
|
||||
} else {
|
||||
yamlText = `No detailed payload data available for this transfer.\n\n`;
|
||||
yamlText += `Direction: ${transfer.direction}\n`;
|
||||
yamlText += `Time: ${transfer.timestamp}\n`;
|
||||
yamlText += `CAN ID: ${transfer.frameId}\n`;
|
||||
yamlText += `Hex Data: ${transfer.hexData}\n`;
|
||||
yamlText += `Source Node: ${transfer.sourceNodeId}\n`;
|
||||
yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`;
|
||||
yamlText = `${t('bus.no_payload')}\n\n`;
|
||||
yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`;
|
||||
yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`;
|
||||
yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`;
|
||||
yamlText += `${t('bus.detail_hex_data')} ${transfer.hexData}\n`;
|
||||
yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`;
|
||||
yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`;
|
||||
}
|
||||
|
||||
setMessageYaml(yamlText);
|
||||
@@ -139,7 +141,7 @@ const BusMonitor = () => {
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
const headers = ['Direction', 'Timestamp', 'CAN ID (Hex)', 'Hex Data', 'Src Node ID', 'Dst Node ID', 'Data Type', 'Raw Data'];
|
||||
const headers = [t('bus.csv_direction'), t('bus.csv_timestamp'), t('bus.csv_can_id'), t('bus.csv_hex_data'), t('bus.csv_src'), t('bus.csv_dst'), t('bus.csv_data_type'), t('bus.csv_raw')];
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...transfers.map(transfer => {
|
||||
@@ -182,7 +184,7 @@ const BusMonitor = () => {
|
||||
<AppBar position="static" color="primary">
|
||||
<Toolbar variant="dense">
|
||||
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
||||
Bus Monitor
|
||||
{t('bus.title')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<FormControlLabel
|
||||
@@ -193,7 +195,7 @@ const BusMonitor = () => {
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">Auto Scroll</Typography>}
|
||||
label={<Typography variant="body2">{t('bus.auto_scroll')}</Typography>}
|
||||
labelPlacement="start"
|
||||
/>
|
||||
<IconButton
|
||||
@@ -218,7 +220,7 @@ const BusMonitor = () => {
|
||||
variant="outlined"
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
Export
|
||||
{t('bus.export')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
@@ -232,13 +234,13 @@ const BusMonitor = () => {
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Dir</TableCell>
|
||||
<TableCell>Time</TableCell>
|
||||
<TableCell>CAN ID</TableCell>
|
||||
<TableCell>Hex Data</TableCell>
|
||||
<TableCell>Src</TableCell>
|
||||
<TableCell>Dst</TableCell>
|
||||
<TableCell>Data Type</TableCell>
|
||||
<TableCell>{t('bus.col_dir')}</TableCell>
|
||||
<TableCell>{t('bus.col_time')}</TableCell>
|
||||
<TableCell>{t('bus.col_can_id')}</TableCell>
|
||||
<TableCell>{t('bus.col_hex_data')}</TableCell>
|
||||
<TableCell>{t('bus.col_src')}</TableCell>
|
||||
<TableCell>{t('bus.col_dst')}</TableCell>
|
||||
<TableCell>{t('bus.col_data_type')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -297,7 +299,7 @@ const BusMonitor = () => {
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Showing {transfers.length} of max {maxTransfers} transfers
|
||||
{t('bus.showing', { count: transfers.length, max: maxTransfers })}
|
||||
</Typography>
|
||||
{isPaused && (
|
||||
<Typography
|
||||
@@ -310,7 +312,7 @@ const BusMonitor = () => {
|
||||
gap: '4px'
|
||||
}}
|
||||
>
|
||||
<PauseIcon fontSize="small" /> PAUSED
|
||||
<PauseIcon fontSize="small" /> {t('bus.paused')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -322,7 +324,7 @@ const BusMonitor = () => {
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
Message Details
|
||||
{t('bus.message_details')}
|
||||
{selectedTransfer && (
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
{selectedTransfer.dataType} - {selectedTransfer.frameId}
|
||||
@@ -347,7 +349,7 @@ const BusMonitor = () => {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDetails} color="primary">
|
||||
Close
|
||||
{t('bus.close')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const ConfirmRestartModal = ({ open, onClose, onConfirm }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>Confirm Restart</DialogTitle>
|
||||
<DialogTitle>{t('confirm.title')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2">
|
||||
Are you sure you want to restart the node?
|
||||
{t('confirm.message')}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="secondary">
|
||||
Cancel
|
||||
{t('confirm.cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} color="primary">
|
||||
Confirm
|
||||
{t('confirm.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -11,6 +11,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import WebSerial from './web_serial';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
// Add this constant at the top of your file, outside the component
|
||||
const USB_DEVICE_NAMES = {
|
||||
@@ -115,10 +116,11 @@ const ConnectionSettingsModal = ({
|
||||
|
||||
// Add this state variable to track connection attempts in progress
|
||||
const [connectionInProgress, setConnectionInProgress] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Create a function to identify and index duplicate devices
|
||||
const getPortDisplayName = (port, allPorts) => {
|
||||
if (!port) return "No port selected";
|
||||
if (!port) return t('conn.no_port_selected');
|
||||
|
||||
// Try to extract the most user-friendly name possible
|
||||
if (port.info && port.info.product) {
|
||||
@@ -196,7 +198,7 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
|
||||
// Fallback for ports without specific info
|
||||
return "Serial Port";
|
||||
return t('conn.serial_port');
|
||||
};
|
||||
|
||||
// Moved from App.js - Lists available ports
|
||||
@@ -249,7 +251,7 @@ const ConnectionSettingsModal = ({
|
||||
setActiveConnection(null);
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(false);
|
||||
showMessage('Serial connection closed', 'info');
|
||||
showMessage(t('conn.serial_closed'), 'info');
|
||||
|
||||
// Clear the forwarding interval
|
||||
if (forwardingInterval) {
|
||||
@@ -293,12 +295,12 @@ const ConnectionSettingsModal = ({
|
||||
setActiveSerialProtocol(serialProtocol);
|
||||
setConnectionInProgress(false);
|
||||
onConnectionStatusChange(true);
|
||||
showMessage(`Serial ${serialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'} connection established`, 'success');
|
||||
showMessage(serialProtocol === SERIAL_PROTOCOLS.SLCAN ? t('conn.serial_slcan_ok') : t('conn.serial_mavlink_ok'), 'success');
|
||||
})
|
||||
|
||||
window.mavlinkSession.addWebSerialErrorHandler((error) => {
|
||||
console.error('Serial connection error:', error);
|
||||
showMessage(`Serial connection failed: ${error.message || 'Could not connect to port'}`, 'error');
|
||||
showMessage(t('conn.serial_failed', { error: error.message || t('conn.could_not_connect') }), 'error');
|
||||
// Reset in-progress state on error
|
||||
setConnectionInProgress(false);
|
||||
});
|
||||
@@ -306,7 +308,7 @@ const ConnectionSettingsModal = ({
|
||||
window.mavlinkSession.webSerialConnect();
|
||||
} catch (error) {
|
||||
console.error('Serial connection error:', error);
|
||||
showMessage(`Serial connection failed: ${error.message || 'Could not connect to port'}`, 'error');
|
||||
showMessage(t('conn.serial_failed', { error: error.message || t('conn.could_not_connect') }), 'error');
|
||||
// Reset in-progress state on error
|
||||
setConnectionInProgress(false);
|
||||
}
|
||||
@@ -317,7 +319,7 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error with serial connection:', error);
|
||||
showMessage(`Serial error: ${error.message || 'Unknown error'}`, 'error');
|
||||
showMessage(t('conn.serial_error', { error: error.message || t('conn.unknown_error') }), 'error');
|
||||
// Reset in-progress state on any error
|
||||
setConnectionInProgress(false);
|
||||
}
|
||||
@@ -327,7 +329,7 @@ const ConnectionSettingsModal = ({
|
||||
const validateIpAddress = (input) => {
|
||||
// Check if empty
|
||||
if (!input) {
|
||||
return 'IP address is required';
|
||||
return t('conn.ip_required');
|
||||
}
|
||||
|
||||
// Allow "localhost"
|
||||
@@ -342,7 +344,7 @@ const ConnectionSettingsModal = ({
|
||||
for (const octet of octets) {
|
||||
const num = parseInt(octet, 10);
|
||||
if (isNaN(num) || num < 0 || num > 255 || octet !== num.toString()) {
|
||||
return 'Each part must be a number between 0-255';
|
||||
return t('conn.ip_invalid_parts');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
@@ -356,17 +358,17 @@ const ConnectionSettingsModal = ({
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'Invalid IP address or hostname';
|
||||
return t('conn.ip_invalid');
|
||||
};
|
||||
|
||||
const validatePort = (input) => {
|
||||
if (!input) {
|
||||
return 'Port is required';
|
||||
return t('conn.port_required');
|
||||
}
|
||||
|
||||
const port = parseInt(input, 10);
|
||||
if (isNaN(port) || port < 1 || port > 65535) {
|
||||
return 'Port must be between 1-65535';
|
||||
return t('conn.port_range');
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -375,7 +377,7 @@ const ConnectionSettingsModal = ({
|
||||
// Add validation for nodeId
|
||||
const validateNodeId = (value) => {
|
||||
const id = parseInt(value, 10);
|
||||
return (isNaN(id) || id < 1 || id > 127) ? 'Node ID must be between 1-127' : '';
|
||||
return (isNaN(id) || id < 1 || id > 127) ? t('conn.node_id_range') : '';
|
||||
};
|
||||
|
||||
// Update handler to propagate changes to parent
|
||||
@@ -417,7 +419,7 @@ const ConnectionSettingsModal = ({
|
||||
setActiveConnection(null);
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(false);
|
||||
showMessage('WebSocket connection closed', 'info');
|
||||
showMessage(t('conn.ws_closed'), 'info');
|
||||
if (forwardingInterval) {
|
||||
clearInterval(forwardingInterval);
|
||||
setForwardingInterval(null);
|
||||
@@ -462,7 +464,7 @@ const ConnectionSettingsModal = ({
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(true);
|
||||
setConnectionInProgress(false);
|
||||
showMessage('WebSocket connection established', 'success');
|
||||
showMessage(t('conn.ws_connected'), 'success');
|
||||
});
|
||||
|
||||
window.mavlinkSession.addWebSocketErrorHandler((error) => {
|
||||
@@ -474,11 +476,11 @@ const ConnectionSettingsModal = ({
|
||||
setActiveConnection(null);
|
||||
onConnectionStatusChange(false);
|
||||
setConnectionInProgress(false);
|
||||
let errorMsg = 'Connection failed';
|
||||
let errorMsg = t('conn.ws_failed');
|
||||
if (error && error.message) {
|
||||
errorMsg = `Connection failed: ${error.message}`;
|
||||
errorMsg = t('conn.ws_failed_detail', { error: error.message });
|
||||
} else if (typeof error === 'string') {
|
||||
errorMsg = `Connection failed: ${error}`;
|
||||
errorMsg = t('conn.ws_failed_detail', { error: error });
|
||||
}
|
||||
showMessage(errorMsg, 'error');
|
||||
});
|
||||
@@ -490,7 +492,7 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error with WebSocket connection:', error);
|
||||
showMessage(`WebSocket error: ${error.message || 'Unknown error'}`, 'error');
|
||||
showMessage(t('conn.ws_error', { error: error.message || t('conn.unknown_error') }), 'error');
|
||||
setConnectionInProgress(false);
|
||||
}
|
||||
};
|
||||
@@ -521,11 +523,11 @@ const ConnectionSettingsModal = ({
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="h6">Adapter Settings</Typography>
|
||||
<Typography variant="h6">{t('conn.title')}</Typography>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{activeConnection && (
|
||||
<Chip
|
||||
label={activeConnection === 'serial' && activeSerialProtocol ? `Connected via serial (${activeSerialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'})` : `Connected via ${activeConnection}`}
|
||||
label={activeConnection === 'serial' && activeSerialProtocol ? (activeSerialProtocol === SERIAL_PROTOCOLS.SLCAN ? t('conn.connected_serial_slcan') : t('conn.connected_serial_mavlink')) : t('conn.connected_ws', { type: activeConnection })}
|
||||
color="success"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -555,21 +557,21 @@ const ConnectionSettingsModal = ({
|
||||
width: '100%',
|
||||
bgcolor: activeConnection === 'serial' ? 'rgba(0, 200, 83, 0.1)' : 'inherit'
|
||||
}}>
|
||||
<Typography variant="subtitle1" gutterBottom>Serial Connection</Typography>
|
||||
<Typography variant="subtitle1" gutterBottom>{t('conn.serial_section')}</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> {/* Reduce gap */}
|
||||
{/* Port and Baud Selection in same row */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
{/* Port Selection - takes more space */}
|
||||
<Box sx={{ flex: 3 }}>
|
||||
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
|
||||
<InputLabel>Port</InputLabel>
|
||||
<InputLabel>{t('conn.port')}</InputLabel>
|
||||
<Select
|
||||
value={selectedPort || ''}
|
||||
onChange={(e) => setSelectedPort(e.target.value)}
|
||||
label="Port"
|
||||
label={t('conn.port')}
|
||||
>
|
||||
{ports.length === 0 ? (
|
||||
<MenuItem value="" disabled>No ports available</MenuItem>
|
||||
<MenuItem value="" disabled>{t('conn.no_ports')}</MenuItem>
|
||||
) : (
|
||||
ports.map((port, index) => (
|
||||
<MenuItem key={index} value={port}>
|
||||
@@ -584,11 +586,11 @@ const ConnectionSettingsModal = ({
|
||||
{/* Baud Rate Selection - takes less space */}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
|
||||
<InputLabel>Baud Rate</InputLabel>
|
||||
<InputLabel>{t('conn.baud_rate')}</InputLabel>
|
||||
<Select
|
||||
value={baudRate}
|
||||
onChange={(e) => setBaudRate(e.target.value)}
|
||||
label="Baud Rate"
|
||||
label={t('conn.baud_rate')}
|
||||
>
|
||||
{BAUD_RATES.map((rate) => (
|
||||
<MenuItem key={rate} value={rate}>
|
||||
@@ -601,14 +603,14 @@ const ConnectionSettingsModal = ({
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
|
||||
<InputLabel>Serial Protocol</InputLabel>
|
||||
<InputLabel>{t('conn.serial_protocol')}</InputLabel>
|
||||
<Select
|
||||
value={serialProtocol}
|
||||
onChange={(e) => setSerialProtocol(e.target.value)}
|
||||
label="Serial Protocol"
|
||||
label={t('conn.serial_protocol')}
|
||||
>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.MAVLINK}>MAVLink tunnel</MenuItem>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.SLCAN}>SLCAN / LAWICEL</MenuItem>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.MAVLINK}>{t('conn.protocol_mavlink')}</MenuItem>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.SLCAN}>{t('conn.protocol_slcan')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -621,7 +623,7 @@ const ConnectionSettingsModal = ({
|
||||
disabled={activeConnection !== null}
|
||||
size="small"
|
||||
>
|
||||
Refresh
|
||||
{t('conn.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -630,7 +632,7 @@ const ConnectionSettingsModal = ({
|
||||
disabled={activeConnection !== null}
|
||||
size="small"
|
||||
>
|
||||
Request
|
||||
{t('conn.request')}
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button
|
||||
@@ -644,8 +646,8 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
{activeConnection === 'serial' ? 'Disconnect' :
|
||||
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'}
|
||||
{activeConnection === 'serial' ? t('conn.disconnect') :
|
||||
connectionInProgress && !activeConnection ? t('conn.connecting') : t('conn.connect')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -657,11 +659,11 @@ const ConnectionSettingsModal = ({
|
||||
width: '100%',
|
||||
bgcolor: activeConnection === 'websocket' ? 'rgba(0, 200, 83, 0.1)' : 'inherit'
|
||||
}}>
|
||||
<Typography variant="subtitle1" gutterBottom>WebSocket Connection</Typography>
|
||||
<Typography variant="subtitle1" gutterBottom>{t('conn.ws_section')}</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> {/* Reduce gap */}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<TextField
|
||||
label="Host/IP Address"
|
||||
label={t('conn.host')}
|
||||
value={wsHost}
|
||||
onChange={handleWsHostChange}
|
||||
disabled={activeConnection !== null}
|
||||
@@ -671,7 +673,7 @@ const ConnectionSettingsModal = ({
|
||||
helperText={hostError}
|
||||
/>
|
||||
<TextField
|
||||
label="Port"
|
||||
label={t('conn.ws_port')}
|
||||
value={wsPort}
|
||||
onChange={handleWsPortChange}
|
||||
type="number"
|
||||
@@ -698,8 +700,8 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
{activeConnection === 'websocket' ? 'Disconnect' :
|
||||
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'}
|
||||
{activeConnection === 'websocket' ? t('conn.disconnect') :
|
||||
connectionInProgress && !activeConnection ? t('conn.connecting') : t('conn.connect')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -712,7 +714,7 @@ const ConnectionSettingsModal = ({
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<TextField
|
||||
label="Node ID"
|
||||
label={t('conn.node_id')}
|
||||
value={nodeId}
|
||||
onChange={handleNodeIdChange}
|
||||
disabled={activeConnection !== null}
|
||||
@@ -726,18 +728,18 @@ const ConnectionSettingsModal = ({
|
||||
helperText={validateNodeId(nodeId)}
|
||||
/>
|
||||
<TextField
|
||||
label="Mavlink Signing"
|
||||
label={t('conn.signing')}
|
||||
value={mavlinkSigning}
|
||||
onChange={handleMavlinkSigningChange}
|
||||
disabled={activeConnection !== null}
|
||||
size="small"
|
||||
placeholder="Secret Key"
|
||||
placeholder={t('conn.secret_key')}
|
||||
type={showMavlinkSigning ? 'text' : 'password'}
|
||||
sx={{ flex: 1 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton
|
||||
aria-label={showMavlinkSigning ? 'Hide secret' : 'Show secret'}
|
||||
aria-label={showMavlinkSigning ? t('conn.hide_secret') : t('conn.show_secret')}
|
||||
onClick={handleToggleMavlinkSigning}
|
||||
edge="end"
|
||||
size="small"
|
||||
|
||||
@@ -13,6 +13,7 @@ import StopIcon from '@mui/icons-material/Stop';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DynamicNodeIdServer from './services/DynamicNodeIdServer';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
const [serverEnabled, setServerEnabled] = useState(false);
|
||||
@@ -23,6 +24,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
const [server, setServer] = useState(null);
|
||||
const [operationInProgress, setOperationInProgress] = useState(false);
|
||||
const [refreshInterval, setRefreshInterval] = useState(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleAllocationUpdate = () => {
|
||||
console.log("Allocation update detected, refreshing list");
|
||||
@@ -84,9 +86,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
}, 1000); // Check every 5 seconds
|
||||
setRefreshInterval(interval);
|
||||
|
||||
showMessage("DNA server started successfully", "success");
|
||||
showMessage(t('dna.started'), "success");
|
||||
} else {
|
||||
showMessage("Failed to start DNA server", "error");
|
||||
showMessage(t('dna.failed_start'), "error");
|
||||
}
|
||||
} else {
|
||||
// Stop the server
|
||||
@@ -99,11 +101,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
setRefreshInterval(null);
|
||||
}
|
||||
|
||||
showMessage("DNA server stopped", "info");
|
||||
showMessage(t('dna.stopped'), "info");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error toggling DNA server:", error);
|
||||
showMessage(`Error: ${error.message}`, "error");
|
||||
showMessage(t('dna.error', { error: error.message }), "error");
|
||||
} finally {
|
||||
setOperationInProgress(false);
|
||||
}
|
||||
@@ -124,7 +126,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
};
|
||||
|
||||
const validateNodeIdRange = () => {
|
||||
return minNodeId >= maxNodeId ? "Min ID must be less than Max ID" : "";
|
||||
return minNodeId >= maxNodeId ? t('dna.invalid_range') : "";
|
||||
};
|
||||
|
||||
const handleDeleteAllocation = (nodeId) => {
|
||||
@@ -133,15 +135,15 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
const success = server.deleteAllocation(nodeId);
|
||||
if (success) {
|
||||
fetchCurrentAllocations();
|
||||
showMessage(`Node ID ${nodeId} allocation revoked`, "info");
|
||||
showMessage(t('dna.revoked', { id: nodeId }), "info");
|
||||
} else {
|
||||
showMessage(`Failed to revoke allocation for node ID ${nodeId}`, "error");
|
||||
showMessage(t('dna.revoke_failed', { id: nodeId }), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshAllocations = () => {
|
||||
fetchCurrentAllocations();
|
||||
showMessage("Allocations refreshed", "info");
|
||||
showMessage(t('dna.refreshed'), "info");
|
||||
};
|
||||
|
||||
// Modify the modal close handler to not stop the server
|
||||
@@ -208,11 +210,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="h6">Dynamic Node ID Allocation Server</Typography>
|
||||
<Typography variant="h6">{t('dna.title')}</Typography>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{serverEnabled && (
|
||||
<Chip
|
||||
label="Server Active"
|
||||
label={t('dna.active')}
|
||||
color="success"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -242,7 +244,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
bgcolor: serverEnabled ? 'rgba(0, 200, 83, 0.1)' : 'inherit'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography variant="subtitle1">Server Control</Typography>
|
||||
<Typography variant="subtitle1">{t('dna.control')}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={serverEnabled ? <StopIcon /> : <PlayArrowIcon />}
|
||||
@@ -251,8 +253,8 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
disabled={operationInProgress}
|
||||
size="small"
|
||||
>
|
||||
{operationInProgress ? "Processing..." :
|
||||
serverEnabled ? "Stop" : "Start"}
|
||||
{operationInProgress ? t('dna.processing') :
|
||||
serverEnabled ? t('dna.stop') : t('dna.start')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -262,7 +264,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
{/* Min Node ID */}
|
||||
<Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}>
|
||||
<TextField
|
||||
label="Min Node ID"
|
||||
label={t('dna.min_node_id')}
|
||||
type="number"
|
||||
value={minNodeId}
|
||||
onChange={handleMinNodeIdChange}
|
||||
@@ -273,14 +275,14 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
}}
|
||||
size="small"
|
||||
error={minNodeId >= maxNodeId}
|
||||
helperText={minNodeId >= maxNodeId ? "Must be < Max" : ""}
|
||||
helperText={minNodeId >= maxNodeId ? t('dna.must_lt_max') : ""}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Max Node ID */}
|
||||
<Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}>
|
||||
<TextField
|
||||
label="Max Node ID"
|
||||
label={t('dna.max_node_id')}
|
||||
type="number"
|
||||
value={maxNodeId}
|
||||
onChange={handleMaxNodeIdChange}
|
||||
@@ -291,7 +293,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
}}
|
||||
size="small"
|
||||
error={minNodeId >= maxNodeId}
|
||||
helperText={minNodeId >= maxNodeId ? "Must be > Min" : ""}
|
||||
helperText={minNodeId >= maxNodeId ? t('dna.must_gt_min') : ""}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -315,8 +317,8 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="body2" sx={{ mr: 0.5 }}>Persist Allocations</Typography>
|
||||
<Tooltip title="When enabled, node ID allocations are stored and restored when the server restarts">
|
||||
<Typography variant="body2" sx={{ mr: 0.5 }}>{t('dna.persist')}</Typography>
|
||||
<Tooltip title={t('dna.persist_tooltip')}>
|
||||
<HelpOutlineIcon fontSize="small" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -339,9 +341,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
variant="body1" // Using body1 for smaller font size instead of subtitle1
|
||||
sx={{ fontWeight: 500 }} // Adding some weight to make it still look like a title
|
||||
>
|
||||
Allocated Node IDs ({allocatedNodes.length})
|
||||
{t('dna.allocated', { count: allocatedNodes.length })}
|
||||
</Typography>
|
||||
<Tooltip title="Refresh allocation list">
|
||||
<Tooltip title={t('dna.refresh_tooltip')}>
|
||||
<IconButton
|
||||
onClick={handleRefreshAllocations}
|
||||
size="small"
|
||||
@@ -356,7 +358,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
{allocatedNodes.length === 0 ? (
|
||||
<Box sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No node IDs allocated
|
||||
{t('dna.no_allocations')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -374,9 +376,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>NID</TableCell>
|
||||
<TableCell>UUID</TableCell>
|
||||
<TableCell align="right" width="60px">Action</TableCell>
|
||||
<TableCell>{t('dna.col_nid')}</TableCell>
|
||||
<TableCell>{t('dna.col_uuid')}</TableCell>
|
||||
<TableCell align="right" width="60px">{t('dna.col_action')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
||||
@@ -9,6 +9,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import StopIcon from '@mui/icons-material/Stop'; // Add this import for the stop button
|
||||
import MusicNoteIcon from '@mui/icons-material/MusicNote';
|
||||
import AM32_Rtttl from './am32_rtttl'; // Updated import to match class name
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
// Add a new state for tracking whether a tune is currently playing
|
||||
@@ -21,6 +22,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
const [errorMessage, setErrorMessage] = useState(''); // For validation error messages
|
||||
const [isValid, setIsValid] = useState(true); // Add a new state variable to track validation status
|
||||
const [paramName, setParamName] = useState(""); // Add paramName to the component state
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const localNode = window.localNode;
|
||||
@@ -84,7 +86,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
|
||||
if (!isValidFormat) {
|
||||
// Warn user but continue with a default tune
|
||||
setErrorMessage('Warning: Invalid RTTTL format! Using a default empty tune instead.');
|
||||
setErrorMessage(t('edit.rtttl_warning'));
|
||||
// Continue with a minimal valid RTTTL string
|
||||
const tuneToParse = "Empty:d=4,o=5,b=120:";
|
||||
result = AM32_Rtttl.to_am32_startup_melody(tuneToParse);
|
||||
@@ -102,7 +104,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
console.log("Binary array values:", Array.from(result.data).slice(0, 30));
|
||||
} catch (err) {
|
||||
console.error("Error converting RTTTL to binary:", err);
|
||||
setErrorMessage(`Error saving tune: ${err.message || 'Unknown error'}`);
|
||||
setErrorMessage(t('edit.error_saving', { error: err.message || t('edit.unknown') }));
|
||||
// Provide an empty binary string (all zeros) as fallback
|
||||
const emptyArray = new Uint8Array(128);
|
||||
valueToSave = String.fromCharCode.apply(null, emptyArray);
|
||||
@@ -126,30 +128,30 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
try {
|
||||
// First validate that the tune has the basic RTTTL format (name:defaults:notes)
|
||||
if (!tuneToPlay || !tuneToPlay.includes(':') || tuneToPlay.split(':').length !== 3) {
|
||||
setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes');
|
||||
setErrorMessage(t('edit.rtttl_invalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Clear any previous error when successful
|
||||
setErrorMessage('');
|
||||
|
||||
|
||||
// Stop any currently playing tune before starting a new one
|
||||
AM32_Rtttl.stopMelody();
|
||||
|
||||
|
||||
// Play the new tune
|
||||
AM32_Rtttl.playMelody(tuneToPlay);
|
||||
setPreviewTune(tuneToPlay);
|
||||
setIsPlaying(true);
|
||||
|
||||
|
||||
// Set up an event listener to detect when audio context is closed or ends
|
||||
const estimatedDuration = estimateTuneDuration(tuneToPlay);
|
||||
setTimeout(() => {
|
||||
setIsPlaying(false);
|
||||
}, estimatedDuration + 500); // Add a small buffer
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error playing tune:", err);
|
||||
setErrorMessage(`Error playing tune: ${err.message || 'Unknown error'}`);
|
||||
setErrorMessage(t('edit.error_playing', { error: err.message || t('edit.unknown') }));
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
@@ -224,7 +226,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
const isValidFormat = stringValue.includes(':') && stringValue.split(':').length === 3;
|
||||
|
||||
if (!isValidFormat) {
|
||||
setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes');
|
||||
setErrorMessage(t('edit.rtttl_invalid'));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -275,13 +277,13 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
// Validate against min/max if they exist
|
||||
if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) {
|
||||
setIsValid(false);
|
||||
setErrorMessage(`Value must be between ${min !== null ? min : '-∞'} and ${max !== null ? max : '∞'}`);
|
||||
setErrorMessage(t('edit.value_range', { min: min !== null ? min : '-∞', max: max !== null ? max : '∞' }));
|
||||
} else {
|
||||
setIsValid(true);
|
||||
setErrorMessage('');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
@@ -322,7 +324,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
// Validate against min/max if they exist
|
||||
if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) {
|
||||
setIsValid(false);
|
||||
setErrorMessage(`Value must be between ${min !== null ? min : '-∞'} and ${max !== null ? max : '∞'}`);
|
||||
setErrorMessage(t('edit.value_range', { min: min !== null ? min : '-∞', max: max !== null ? max : '∞' }));
|
||||
} else {
|
||||
setIsValid(true);
|
||||
setErrorMessage('');
|
||||
@@ -340,14 +342,14 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}>
|
||||
<FormControl fullWidth margin="dense">
|
||||
<InputLabel id="rtttl-preset-label">Select Preset Tune</InputLabel>
|
||||
<InputLabel id="rtttl-preset-label">{t('edit.select_preset')}</InputLabel>
|
||||
<Select
|
||||
labelId="rtttl-preset-label"
|
||||
value={selectedPreset}
|
||||
onChange={(e) => setSelectedPreset(e.target.value)}
|
||||
>
|
||||
<MenuItem value="" disabled>
|
||||
<em>Choose a preset tune</em>
|
||||
<em>{t('edit.choose_preset')}</em>
|
||||
</MenuItem>
|
||||
{Object.entries(rtttlPresets).map(([name, tune]) => (
|
||||
<MenuItem key={name} value={tune}>
|
||||
@@ -363,24 +365,24 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
disabled={!selectedPreset}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Apply
|
||||
{t('edit.apply')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<TextField
|
||||
label="RTTTL Tune"
|
||||
label={t('edit.rtttl_tune')}
|
||||
value={value || ""}
|
||||
onChange={(e) => handleValueChange(e.target.value)}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="Format: name:d=duration,o=octave,b=bpm:notes"
|
||||
placeholder={t('edit.rtttl_placeholder')}
|
||||
error={!isValid && value !== ''}
|
||||
// Remove helperText to avoid layout issues
|
||||
/>
|
||||
<Tooltip title={isPlaying ? "Stop tune" : "Play tune"}>
|
||||
<Tooltip title={isPlaying ? t('edit.stop_tune') : t('edit.play_tune')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color={isPlaying ? "secondary" : "primary"}
|
||||
@@ -401,21 +403,21 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
|
||||
{/* Add a simple instruction text below the field */}
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
|
||||
Enter RTTTL format tune or select a preset
|
||||
{t('edit.rtttl_instruction')}
|
||||
</Typography>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box sx={{ bgcolor: 'action.hover', p: 1, borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>
|
||||
RTTTL Format Guide
|
||||
{t('edit.rtttl_guide_title')}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
<Typography variant="caption" display="block">• d=duration (1=whole, 2=half, 4=quarter, 8=eighth, 16=16th note)</Typography>
|
||||
<Typography variant="caption" display="block">• o=octave (4-7 where 5 is default)</Typography>
|
||||
<Typography variant="caption" display="block">• b=tempo (beats per minute)</Typography>
|
||||
<Typography variant="caption" display="block">• Notes are: c, c#, d, d#, e, f, f#, g, g#, a, a#, b or h</Typography>
|
||||
<Typography variant="caption" display="block">• Example: Beep:d=4,o=5,b=120:c</Typography>
|
||||
<Typography variant="caption" display="block">• {t('edit.rtttl_guide_duration')}</Typography>
|
||||
<Typography variant="caption" display="block">• {t('edit.rtttl_guide_octave')}</Typography>
|
||||
<Typography variant="caption" display="block">• {t('edit.rtttl_guide_tempo')}</Typography>
|
||||
<Typography variant="caption" display="block">• {t('edit.rtttl_guide_notes')}</Typography>
|
||||
<Typography variant="caption" display="block">• {t('edit.rtttl_guide_example')}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -431,7 +433,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
if (param.fields.value.msg.fields.boolean_value) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center'}}>
|
||||
<Typography variant="body2" sx={{ mr: 2 }}>Enable/Disable:</Typography>
|
||||
<Typography variant="body2" sx={{ mr: 2 }}>{t('edit.enable_disable')}</Typography>
|
||||
<Checkbox
|
||||
checked={value === 1 || value === true}
|
||||
onChange={(e) => setValue(e.target.checked ? 1 : 0)}
|
||||
@@ -462,7 +464,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="New Value"
|
||||
label={t('edit.new_value')}
|
||||
value={value}
|
||||
type="number"
|
||||
inputProps={{
|
||||
@@ -472,8 +474,8 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
onChange={(e) => handleValueChange(e.target.value)}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
helperText={isOutOfBounds ?
|
||||
`Value must be between ${min !== "" ? min : '-∞'} and ${max !== "" ? max : '∞'}` :
|
||||
helperText={isOutOfBounds ?
|
||||
t('edit.value_range', { min: min !== "" ? min : '-∞', max: max !== "" ? max : '∞' }) :
|
||||
null
|
||||
}
|
||||
/>
|
||||
@@ -506,8 +508,8 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
|
||||
const renderParamNameField = (name) => (
|
||||
<TextField
|
||||
label="Parameter Name"
|
||||
value={name || "Unknown"}
|
||||
label={t('edit.param_name')}
|
||||
value={name || t('edit.unknown')}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
@@ -542,7 +544,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2">
|
||||
{value !== undefined && value !== "" && value !== null ? value : "Unknown"}
|
||||
{value !== undefined && value !== "" && value !== null ? value : t('edit.unknown')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -554,7 +556,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
onClose={onClose}
|
||||
sx={{ '& .MuiDialog-paper': { minWidth: isRTTTLEditor ? '600px' : '400px' } }}
|
||||
>
|
||||
<DialogTitle>Edit Parameter</DialogTitle>
|
||||
<DialogTitle>{t('edit.title')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}>
|
||||
@@ -565,7 +567,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
{isString && !isRTTTLEditor && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
|
||||
<TextField
|
||||
label="String Value"
|
||||
label={t('edit.string_value')}
|
||||
value={value || ""}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
fullWidth
|
||||
@@ -580,7 +582,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
{paramName === "STARTUP_TUNE" && isString ? (
|
||||
renderInfoField("Current RTTTL", (() => {
|
||||
renderInfoField(t('edit.current_rtttl'), (() => {
|
||||
try {
|
||||
// Get the binary string value
|
||||
const binaryString = paramValueField.toString();
|
||||
@@ -595,27 +597,27 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
return AM32_Rtttl.from_am32_startup_melody(binaryData, "Tune");
|
||||
} catch (err) {
|
||||
console.error("Error converting binary data to RTTTL:", err);
|
||||
return "Error parsing melody data";
|
||||
return t('edit.error_parsing_melody');
|
||||
}
|
||||
})(), true) // Pass true to indicate this is an RTTTL value
|
||||
) : (
|
||||
renderInfoField(
|
||||
"Current Value",
|
||||
isBoolean
|
||||
? (paramValueField.value ? "True" : "False")
|
||||
t('edit.current_value'),
|
||||
isBoolean
|
||||
? (paramValueField.value ? t('edit.true') : t('edit.false'))
|
||||
: isString
|
||||
? paramValueField.toString()
|
||||
: paramValueField.value
|
||||
)
|
||||
)}
|
||||
{/* Only show default value when not STARTUP_TUNE */}
|
||||
{paramName !== "STARTUP_TUNE" && renderInfoField("Default Value", isBoolean ? (paramDefaultValue ? "True" : "False") : paramDefaultValue)}
|
||||
{paramName !== "STARTUP_TUNE" && renderInfoField(t('edit.default_value'), isBoolean ? (paramDefaultValue ? t('param.true') : t('param.false')) : paramDefaultValue)}
|
||||
</Box>
|
||||
|
||||
{!isBoolean && !isString && !isRTTTLEditor && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
{renderInfoField("Min Value", paramMinValue)}
|
||||
{renderInfoField("Max Value", paramMaxValue)}
|
||||
{renderInfoField(t('edit.min_value'), paramMinValue)}
|
||||
{renderInfoField(t('edit.max_value'), paramMaxValue)}
|
||||
</Box>
|
||||
)}
|
||||
{errorMessage && (
|
||||
@@ -629,14 +631,14 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="secondary">
|
||||
Cancel
|
||||
{t('edit.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
color="primary"
|
||||
disabled={!isValid}
|
||||
>
|
||||
Save
|
||||
{t('edit.save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,6 +4,7 @@ import PanToolIcon from '@mui/icons-material/PanTool';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import PauseIcon from '@mui/icons-material/Pause';
|
||||
import dronecan from './dronecan';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const commandValueType = new dronecan.DSDL.uavcan_equipment_esc_RawCommand().fields.cmd.value_type;
|
||||
const CMD_MAX = Number(commandValueType.value_range.max);
|
||||
@@ -27,6 +28,7 @@ const EscPanel = () => {
|
||||
const [sendArming, setSendArming] = useState(false);
|
||||
const [broadcastRate, setBroadcastRate] = useState(10); // Changed default to 10
|
||||
const [isPaused, setIsPaused] = useState(false); // New state for pause toggle
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Add toggle pause function
|
||||
const togglePause = () => {
|
||||
@@ -269,7 +271,7 @@ const EscPanel = () => {
|
||||
<Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mr: 1 }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>Channels:</Typography>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>{t('esc.channels')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
@@ -302,7 +304,7 @@ const EscPanel = () => {
|
||||
fontSize: '0.6rem'
|
||||
}}
|
||||
>
|
||||
⚠️ REMOVE PROPELLERS!
|
||||
{t('esc.remove_propellers')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -317,27 +319,27 @@ const EscPanel = () => {
|
||||
sx={{ p: 0.5 }}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">Send Safety</Typography>}
|
||||
label={<Typography variant="body2">{t('esc.send_safety')}</Typography>}
|
||||
labelPlacement="start"
|
||||
sx={{ ml: 0, mr: 1 }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={sendArming}
|
||||
onChange={(e) => setSendArming(e.target.checked)}
|
||||
<Checkbox
|
||||
checked={sendArming}
|
||||
onChange={(e) => setSendArming(e.target.checked)}
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
/>
|
||||
}
|
||||
label={<Typography variant="body2">Send Arming</Typography>}
|
||||
}
|
||||
label={<Typography variant="body2">{t('esc.send_arming')}</Typography>}
|
||||
labelPlacement="start"
|
||||
sx={{ ml: 0, mr: 2 }}
|
||||
/>
|
||||
|
||||
{/* Broadcast Rate moved to the right */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>Broadcast Rate:</Typography>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>{t('esc.broadcast_rate')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
@@ -403,22 +405,22 @@ const EscPanel = () => {
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="textSecondary">Index: {esc.esc_index}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">Err: {esc.error_count !== null ? esc.error_count : "NC"}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">{t('esc.index')} {esc.esc_index}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">{t('esc.error')} {esc.error_count !== null ? esc.error_count : t('esc.nc')}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Temp: {esc.temperature !== null ? `${(esc.temperature - 273.15).toFixed(1)} °C` : "NC"}
|
||||
{t('esc.temp')} {esc.temperature !== null ? `${(esc.temperature - 273.15).toFixed(1)} °C` : t('esc.nc')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Volt: {esc.voltage !== null ? `${esc.voltage.toFixed(2)} V` : "NC"}
|
||||
{t('esc.volt')} {esc.voltage !== null ? `${esc.voltage.toFixed(2)} V` : t('esc.nc')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Curr: {esc.current !== null ? `${esc.current.toFixed(2)} A` : "NC"}
|
||||
{t('esc.curr')} {esc.current !== null ? `${esc.current.toFixed(2)} A` : t('esc.nc')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
RPM: {esc.rpm !== null ? Math.round(esc.rpm) : "NC"}
|
||||
{t('esc.rpm')} {esc.rpm !== null ? Math.round(esc.rpm) : t('esc.nc')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
RAT: {esc.power_rating_pct !== null ? `${esc.power_rating_pct.toFixed(1)} %` : "NC"}
|
||||
{t('esc.rat')} {esc.power_rating_pct !== null ? `${esc.power_rating_pct.toFixed(1)} %` : t('esc.nc')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -446,7 +448,7 @@ const EscPanel = () => {
|
||||
fullWidth
|
||||
size="small"
|
||||
>
|
||||
Stop
|
||||
{t('esc.stop')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -493,7 +495,7 @@ const EscPanel = () => {
|
||||
}}>
|
||||
<Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
cmd: [{getScaledCommands(thrustValues).join(', ')}]
|
||||
{t('esc.cmd')} [{getScaledCommands(thrustValues).join(', ')}]
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -504,7 +506,7 @@ const EscPanel = () => {
|
||||
startIcon={<PanToolIcon />}
|
||||
onClick={handleStopAll}
|
||||
>
|
||||
Stop All
|
||||
{t('esc.stop_all')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
import {
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
Button, Typography, LinearProgress, Box, Alert
|
||||
} from '@mui/material';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import FileServer from './FileServer';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
const [firmwareFile, setFirmwareFile] = useState(null);
|
||||
@@ -12,6 +13,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
const [updateProgress, setUpdateProgress] = useState(0);
|
||||
const [updateStatus, setUpdateStatus] = useState(null); // 'idle', 'updating', 'success', 'error'
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
@@ -21,7 +23,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||
if (fileExtension !== 'bin' && fileExtension !== 'hex') {
|
||||
setUpdateStatus('error');
|
||||
setStatusMessage('Invalid file type. Please select a .bin or .hex firmware file.');
|
||||
setStatusMessage(t('fw.invalid_file'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +44,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
.catch(error => {
|
||||
console.error('Error loading firmware:', error);
|
||||
setUpdateStatus('error');
|
||||
setStatusMessage('Failed to load firmware file');
|
||||
setStatusMessage(t('fw.load_failed'));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,13 +81,13 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
|
||||
if (!localNode) {
|
||||
setUpdateStatus('error');
|
||||
setStatusMessage('Local node not available');
|
||||
setStatusMessage(t('fw.node_unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI state
|
||||
setUpdateStatus('updating');
|
||||
setStatusMessage('Starting firmware update...');
|
||||
setStatusMessage(t('fw.starting'));
|
||||
setUpdateProgress(0);
|
||||
|
||||
// Register a progress callback with the FileServer
|
||||
@@ -95,12 +97,12 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
setUpdateProgress(progress * 100);
|
||||
|
||||
// Update status message
|
||||
setStatusMessage(`Updating firmware: ${Math.round(progress * 100)}% (${offset}/${total} bytes)`);
|
||||
setStatusMessage(t('fw.updating', { progress: Math.round(progress * 100), offset: offset, total: total }));
|
||||
|
||||
// When complete
|
||||
if (eof) {
|
||||
setUpdateStatus('success');
|
||||
setStatusMessage('Firmware update completed successfully!');
|
||||
setStatusMessage(t('fw.success'));
|
||||
|
||||
// Clean up progress tracking
|
||||
setTimeout(() => {
|
||||
@@ -123,7 +125,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
if (!msg || msg.fields.error.value > 0) {
|
||||
// Handle update failure
|
||||
setUpdateStatus('error');
|
||||
setStatusMessage(`Update failed: code: ${msg.fields.error.value} ${msg.fields.optional_error_message.toString() || 'Unknown error'}`);
|
||||
setStatusMessage(t('fw.update_failed', { code: msg.fields.error.value, message: msg.fields.optional_error_message.toString() || t('edit.unknown') }));
|
||||
FileServer.unregisterProgressCallback(firmwarePath);
|
||||
} else {
|
||||
setUpdateStatus('updating');
|
||||
@@ -134,16 +136,16 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
} catch (error) {
|
||||
console.error('Error initiating firmware update:', error);
|
||||
setUpdateStatus('error');
|
||||
setStatusMessage(`Failed to start update: ${error.message || 'Unknown error'}`);
|
||||
setStatusMessage(t('fw.start_failed', { error: error.message || t('edit.unknown') }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={updateStatus === 'updating' ? null : onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Firmware Update</DialogTitle>
|
||||
<DialogTitle>{t('fw.title')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Please select the firmware file (.bin|.hex) to upload to node {targetNodeId}.
|
||||
{t('fw.select_hint', { id: targetNodeId })}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
@@ -152,7 +154,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
startIcon={<FileUploadIcon />}
|
||||
disabled={updateStatus === 'updating'}
|
||||
>
|
||||
Select Firmware File
|
||||
{t('fw.select_file')}
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
@@ -164,7 +166,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
{firmwareFile && (
|
||||
<Typography variant="body2" sx={{ mt: 2 }}>
|
||||
Selected File: {firmwareFile.name}
|
||||
{fileContent && ` (${fileContent.size} bytes)`}
|
||||
{fileContent && ` (${fileContent.size} ${t('fw.bytes', { defaultValue: 'bytes' })})`}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -183,13 +185,13 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
|
||||
{updateStatus === 'error' && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{statusMessage || 'An error occurred during the update.'}
|
||||
{statusMessage || t('fw.error_occurred')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{updateStatus === 'success' && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
{statusMessage || 'Firmware update completed successfully!'}
|
||||
{statusMessage || t('fw.success')}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
@@ -197,14 +199,14 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
{updateStatus !== 'updating' ? (
|
||||
<>
|
||||
<Button onClick={onClose} color="secondary">
|
||||
Cancel
|
||||
{t('fw.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={handleUpdate}
|
||||
color="primary"
|
||||
color="primary"
|
||||
disabled={!firmwareFile || updateStatus === 'updating'}
|
||||
>
|
||||
Update
|
||||
{t('fw.update')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -213,7 +215,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
disabled={updateProgress < 100}
|
||||
onClick={onClose}
|
||||
>
|
||||
{updateProgress < 100 ? 'Updating...' : 'Close'}
|
||||
{updateProgress < 100 ? t('fw.updating_ellipsis') : t('fw.close')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
|
||||
@@ -3,10 +3,12 @@ import { TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper
|
||||
import PauseIcon from '@mui/icons-material/Pause';
|
||||
import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const NodeLogs = () => {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const localNode = window.localNode;
|
||||
@@ -57,7 +59,7 @@ const NodeLogs = () => {
|
||||
alignItems: 'center',
|
||||
height: 20
|
||||
}} margin={1}>
|
||||
<Typography variant="caption" flexGrow={1}>Logs</Typography>
|
||||
<Typography variant="caption" flexGrow={1}>{t('logs.title')}</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<IconButton
|
||||
sx={{
|
||||
@@ -90,11 +92,11 @@ const NodeLogs = () => {
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ width: '5%' }}>NID</TableCell>
|
||||
<TableCell sx={{ width: '15%' }}>Time</TableCell>
|
||||
<TableCell sx={{ width: '10%' }}>Level</TableCell>
|
||||
<TableCell sx={{ width: '10%' }}>Source</TableCell>
|
||||
<TableCell sx={{ width: '60%' }}>Text</TableCell>
|
||||
<TableCell sx={{ width: '5%' }}>{t('logs.col_nid')}</TableCell>
|
||||
<TableCell sx={{ width: '15%' }}>{t('logs.col_time')}</TableCell>
|
||||
<TableCell sx={{ width: '10%' }}>{t('logs.col_level')}</TableCell>
|
||||
<TableCell sx={{ width: '10%' }}>{t('logs.col_source')}</TableCell>
|
||||
<TableCell sx={{ width: '60%' }}>{t('logs.col_text')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
||||
@@ -11,6 +11,7 @@ import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import ParamEditorSelector from './ParamEditors/ParamEditorSelector';
|
||||
import AM32_Rtttl from './am32_rtttl';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const OPCODE_SAVE = 0;
|
||||
const OPCODE_ERASE = 1;
|
||||
@@ -21,6 +22,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
const [paramsUpdateTimestamp, setParamsUpdateTimestamp] = useState(0);
|
||||
const [fetchingParams, setFetchingParams] = useState(false);
|
||||
const fetchTimeoutRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!nodeId) return null;
|
||||
const node = nodes[nodeId];
|
||||
@@ -97,9 +99,9 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
// Function to format boolean values visually
|
||||
const formatBooleanValue = (value) => {
|
||||
if (value === 'True') {
|
||||
return <Chip size="small" label="True" color="success" />;
|
||||
return <Chip size="small" label={t('param.true')} color="success" />;
|
||||
} else if (value === 'False') {
|
||||
return <Chip size="small" label="False" color="error" />;
|
||||
return <Chip size="small" label={t('param.false')} color="error" />;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -214,15 +216,15 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
} else if (param.fields.value.msg.fields.boolean_value !== undefined) {
|
||||
paramTypeDisplay = 'boolean';
|
||||
if (param.fields.value.msg.fields.boolean_value.value === 0) {
|
||||
paramValueDisplay = 'Disabled';
|
||||
paramValueDisplay = t('param.disabled');
|
||||
} else if (param.fields.value.msg.fields.boolean_value.value === 1) {
|
||||
paramValueDisplay = 'Enabled';
|
||||
paramValueDisplay = t('param.enabled');
|
||||
}
|
||||
|
||||
if (paramDefaultValue === 0) {
|
||||
paramDefaultValueDisplay = 'Disabled';
|
||||
paramDefaultValueDisplay = t('param.disabled');
|
||||
} else {
|
||||
paramDefaultValueDisplay = 'Enabled';
|
||||
paramDefaultValueDisplay = t('param.enabled');
|
||||
}
|
||||
paramMinValueDisplay = "";
|
||||
paramMaxValueDisplay = "";
|
||||
@@ -306,7 +308,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell padding="none" align="center">
|
||||
<Tooltip title="Edit Parameter">
|
||||
<Tooltip title={t('param.edit_param')}>
|
||||
<EditIcon fontSize="small" color="primary" />
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
@@ -322,13 +324,13 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{width: '5%'}}>Idx</TableCell>
|
||||
<TableCell sx={{width: '28%'}}>Name</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>Type</TableCell>
|
||||
<TableCell sx={{width: '17%'}}>Value</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>Default</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>Min</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>Max</TableCell>
|
||||
<TableCell sx={{width: '5%'}}>{t('param.col_idx')}</TableCell>
|
||||
<TableCell sx={{width: '28%'}}>{t('param.col_name')}</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>{t('param.col_type')}</TableCell>
|
||||
<TableCell sx={{width: '17%'}}>{t('param.col_value')}</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>{t('param.col_default')}</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>{t('param.col_min')}</TableCell>
|
||||
<TableCell sx={{width: '10%'}}>{t('param.col_max')}</TableCell>
|
||||
<TableCell sx={{width: '10%'}}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -357,7 +359,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
sx={{ width: 80, mr: 2, ml: 0.5 }}
|
||||
variant="caption"
|
||||
>
|
||||
Parameters
|
||||
{t('param.title')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'space-between', flexDirection: 'row', border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5, mr: 2 }}>
|
||||
<Button
|
||||
@@ -367,7 +369,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
startIcon={<SyncIcon />}
|
||||
disabled={fetchingParams}
|
||||
>
|
||||
{fetchingParams ? 'Fetching...' : 'Fetch All'}
|
||||
{fetchingParams ? t('param.fetching') : t('param.fetch_all')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveParams}
|
||||
@@ -377,7 +379,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
startIcon={<SaveIcon />}
|
||||
disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0}
|
||||
>
|
||||
Store All
|
||||
{t('param.store_all')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleEraseParams}
|
||||
@@ -385,7 +387,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
color="warning"
|
||||
startIcon={<AutoFixNormalIcon />}
|
||||
>
|
||||
Erase All
|
||||
{t('param.erase_all')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}></Box>
|
||||
@@ -398,7 +400,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
onClick={handleDownloadParams}
|
||||
disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0}
|
||||
>
|
||||
Download
|
||||
{t('param.download')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -406,7 +408,7 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
startIcon={<FileUploadIcon />}
|
||||
disabled={!nodeId} // Disable Load button if no node is selected
|
||||
>
|
||||
Load
|
||||
{t('param.load')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -7,6 +7,7 @@ import CableIcon from '@mui/icons-material/Cable';
|
||||
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||
import FirmwareUpdateModal from './FirmwareUpdateModal';
|
||||
import ConfirmRestartModal from './ConfirmRestartModal';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const VendorSpecificCodeDisplay = (code) => {
|
||||
code = Math.max(0, Math.floor(code) & 0xFFFF);
|
||||
@@ -19,6 +20,7 @@ const VendorSpecificCodeDisplay = (code) => {
|
||||
const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEditorEnable }) => {
|
||||
const [firmwareModalOpen, setFirmwareModalOpen] = useState(false);
|
||||
const [restartModalOpen, setRestartModalOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const localNode = window.localNode;
|
||||
@@ -71,16 +73,16 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="caption">
|
||||
Node Properties
|
||||
{t('props.title')}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<Typography variant="caption">Multi Node Editor</Typography>
|
||||
<Typography variant="caption">{t('props.multi_editor')}</Typography>
|
||||
<Switch checked={multiNodeEditorEnable} onChange={(e) => { setMultiNodeEditorEnable(e.target.checked) }} />
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
|
||||
<TextField
|
||||
label="Node ID"
|
||||
label={t('props.node_id')}
|
||||
value={nodeId}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
@@ -88,7 +90,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Name"
|
||||
label={t('props.name')}
|
||||
value={name}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
@@ -98,7 +100,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
|
||||
<TextField
|
||||
label="Mode"
|
||||
label={t('props.mode')}
|
||||
value={mode}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
@@ -107,7 +109,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Health"
|
||||
label={t('props.health')}
|
||||
value={health}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
@@ -116,7 +118,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Uptime"
|
||||
label={t('props.uptime')}
|
||||
value={uptime}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
@@ -126,7 +128,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
|
||||
<TextField
|
||||
label="Vendor Specific Status Code"
|
||||
label={t('props.vendor_code')}
|
||||
fullWidth
|
||||
value={vendor_specific_status_code}
|
||||
InputProps={{
|
||||
@@ -136,7 +138,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
|
||||
<TextField
|
||||
label="Software Version"
|
||||
label={t('props.sw_version')}
|
||||
fullWidth
|
||||
value={softwareVersion}
|
||||
InputProps={{
|
||||
@@ -145,7 +147,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<TextField
|
||||
label="CRC64"
|
||||
label={t('props.crc64')}
|
||||
fullWidth
|
||||
value={softwareCrc64}
|
||||
InputProps={{
|
||||
@@ -154,7 +156,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<TextField
|
||||
label="VCS Commit"
|
||||
label={t('props.vcs_commit')}
|
||||
fullWidth
|
||||
value={softwareVcsCommit}
|
||||
InputProps={{
|
||||
@@ -164,7 +166,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
|
||||
<TextField
|
||||
label="Hardware Version"
|
||||
label={t('props.hw_version')}
|
||||
value={hardwareVersion}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
@@ -172,7 +174,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<TextField
|
||||
label="UID"
|
||||
label={t('props.uid')}
|
||||
fullWidth
|
||||
value={hardwareUID}
|
||||
InputProps={{
|
||||
@@ -182,7 +184,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
|
||||
<TextField
|
||||
label="Cert. of authenticity"
|
||||
label={t('props.certificate')}
|
||||
fullWidth
|
||||
value={certificateOfAuthenticity}
|
||||
InputProps={{
|
||||
@@ -195,7 +197,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
sx={{ width: 80, mr: 2 }}
|
||||
variant="caption"
|
||||
>
|
||||
Node Controls
|
||||
{t('props.controls')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', flexGrow: 1, border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5 }}>
|
||||
<Button
|
||||
@@ -205,14 +207,14 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
startIcon={<PowerSettingsNewIcon />}
|
||||
onClick={() => setRestartModalOpen(true)}
|
||||
>
|
||||
Restart
|
||||
{t('props.restart')}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ mr: 1 }}
|
||||
variant="outlined"
|
||||
startIcon={<CableIcon />}
|
||||
>
|
||||
Get Transport Stats
|
||||
{t('props.transport_stats')}
|
||||
</Button>
|
||||
<Box sx={{ flexGrow: 1 }}></Box>
|
||||
<Button
|
||||
@@ -220,7 +222,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
|
||||
startIcon={<SystemUpdateAltIcon />}
|
||||
onClick={() => setFirmwareModalOpen(true)}
|
||||
>
|
||||
Update Firmware
|
||||
{t('props.update_firmware')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Button, Menu, MenuItem, Divider } from '@mui/material';
|
||||
import VideogameAssetIcon from '@mui/icons-material/VideogameAsset';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const PanelsMenu = ({openWindow}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -42,7 +44,7 @@ const PanelsMenu = ({openWindow}) => {
|
||||
color="default"
|
||||
startIcon={<VideogameAssetIcon />}
|
||||
>
|
||||
Panels
|
||||
{t('panels.title')}
|
||||
</Button>
|
||||
<Menu
|
||||
elevation={0}
|
||||
@@ -59,10 +61,10 @@ const PanelsMenu = ({openWindow}) => {
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleEscPanelClick} disableRipple>
|
||||
ESC
|
||||
{t('panels.esc')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleActuatorPanelClick} disableRipple>
|
||||
Actuator
|
||||
{t('panels.actuator')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
@@ -7,6 +7,7 @@ import DronecanLogo from './image/dronecan_logo.png';
|
||||
import { toYaml } from './dronecan/message_format_utils';
|
||||
|
||||
import theme from './theme';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
import './css/subscriber.css';
|
||||
|
||||
const SubscriberWindow = () => {
|
||||
@@ -19,9 +20,10 @@ const SubscriberWindow = () => {
|
||||
const [recordingSet, setRecordingSet] = useState([]);
|
||||
const [displayRecordText, setDisplayRecordText] = useState("");
|
||||
const [recording, setRecording] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (window.opener === null) {
|
||||
return "Not Allowed To Open Directly";
|
||||
return t('sub.not_allowed');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,7 +63,7 @@ const SubscriberWindow = () => {
|
||||
}
|
||||
const msg = transfer.payload;
|
||||
const msgObj = msg.toObj();
|
||||
let destNodeText = "All";
|
||||
let destNodeText = t('sub.all');
|
||||
if (transfer.destNodeId && transfer.destNodeId !== 0) {
|
||||
destNodeText = `${transfer.destNodeId}`;
|
||||
}
|
||||
@@ -148,12 +150,12 @@ const SubscriberWindow = () => {
|
||||
<Box sx={{display: 'flex', flexDirection: 'row', alignItems: 'center', mr: 0.5}}>
|
||||
<Box sx={{minWidth: 200, display: 'flex', flexDirection: 'row'}}>
|
||||
<Box sx={{flexGrow: 1}}></Box>
|
||||
<Typography variant="caption" mr={0.5}>RX:</Typography>
|
||||
<Typography variant="caption" mr={0.5}>{t('sub.rx')}</Typography>
|
||||
<Typography variant="caption" mr={1} sx={{minWidth: 30}}>{totalRX}</Typography>
|
||||
<Typography variant="caption" mr={0.5}>Rates(Hz):</Typography>
|
||||
<Typography variant="caption" mr={0.5}>{t('sub.rates')}</Typography>
|
||||
<Typography variant="caption" mr={1} sx={{minWidth: 30}}>{messageRate.toFixed(0)}</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" mr={1}> Max:</Typography>
|
||||
<Typography variant="caption" mr={1}>{t('sub.max')}</Typography>
|
||||
<TextField size="small" sx={{width: 80}} type="number" min={1} max={100} value={maxMessageCount} onChange={updateMaxMessageCount} />
|
||||
<IconButton size="small" onClick={handleClean}>
|
||||
<CleaningServicesIcon />
|
||||
|
||||
@@ -3,10 +3,12 @@ import { Box, Button, Menu, MenuItem, Divider } from '@mui/material';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import MessageIcon from '@mui/icons-material/Message';
|
||||
import SettingsInputCompositeIcon from '@mui/icons-material/SettingsInputComposite';
|
||||
import { useTranslation } from './i18n/LanguageContext';
|
||||
|
||||
const ToolsMenu =({openWindow}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -44,7 +46,7 @@ const ToolsMenu =({openWindow}) => {
|
||||
color="default"
|
||||
startIcon={<BuildIcon />}
|
||||
>
|
||||
Tools
|
||||
{t('tools.title')}
|
||||
</Button>
|
||||
<Menu
|
||||
elevation={0}
|
||||
@@ -61,10 +63,10 @@ const ToolsMenu =({openWindow}) => {
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleSubscriberClick} disableRipple>
|
||||
Subscriber
|
||||
{t('tools.subscriber')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleBusMonitorClick} disableRipple>
|
||||
Bus Monitor
|
||||
{t('tools.bus_monitor')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
67
src/i18n/LanguageContext.js
Normal file
67
src/i18n/LanguageContext.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useMemo } from "react";
|
||||
import { en, zh } from "./translations";
|
||||
|
||||
const allTranslations = { en, zh };
|
||||
const LanguageContext = createContext();
|
||||
const STORAGE_KEY = "language";
|
||||
|
||||
function getInitialLanguage() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === "zh" || stored === "en") return stored;
|
||||
} catch (e) {}
|
||||
return "en";
|
||||
}
|
||||
|
||||
export function LanguageProvider({ children }) {
|
||||
const [language, setLanguage] = useState(getInitialLanguage);
|
||||
const strings = allTranslations[language];
|
||||
|
||||
const handleSetLanguage = useCallback((lang) => {
|
||||
setLanguage(lang);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, lang);
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
|
||||
const t = useCallback(
|
||||
(key, params) => {
|
||||
let str = strings[key] || en[key] || key;
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
str = str.replace(new RegExp(`\\{${k}\\}`, "g"), v);
|
||||
});
|
||||
}
|
||||
return str;
|
||||
},
|
||||
[strings]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ language, setLanguage: handleSetLanguage, t }),
|
||||
[language, handleSetLanguage, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={value}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTranslation() {
|
||||
const ctx = useContext(LanguageContext);
|
||||
if (!ctx) {
|
||||
const t = (key, params) => {
|
||||
let str = en[key] || key;
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([k, v]) => {
|
||||
str = str.replace(new RegExp(`\\{${k}\\}`, "g"), v);
|
||||
});
|
||||
}
|
||||
return str;
|
||||
};
|
||||
return { language: "en", setLanguage: () => {}, t };
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
605
src/i18n/translations.js
Normal file
605
src/i18n/translations.js
Normal file
@@ -0,0 +1,605 @@
|
||||
export const en = {
|
||||
// App.js
|
||||
"app.title": "DroneCAN Web Tools",
|
||||
"app.bus": "Bus {n}",
|
||||
"app.dna": "DNA",
|
||||
"app.adapter": "Adapter",
|
||||
"app.connected": "Successfully connected to device",
|
||||
"app.disconnected": "Disconnected from device",
|
||||
"app.bus_switched": "Switched to CAN bus {bus}",
|
||||
"app.dna_stopped": "DNA server stopped",
|
||||
"app.dna_started": "DNA server started",
|
||||
"app.dna_failed": "Failed to start DNA server",
|
||||
|
||||
// ConnectionSettingsModal.js
|
||||
"conn.title": "Adapter Settings",
|
||||
"conn.connected_serial_slcan": "Connected via serial (SLCAN)",
|
||||
"conn.connected_serial_mavlink": "Connected via serial (MAVLink)",
|
||||
"conn.connected_ws": "Connected via {type}",
|
||||
"conn.serial_section": "Serial Connection",
|
||||
"conn.port": "Port",
|
||||
"conn.no_ports": "No ports available",
|
||||
"conn.baud_rate": "Baud Rate",
|
||||
"conn.serial_protocol": "Serial Protocol",
|
||||
"conn.protocol_mavlink": "MAVLink tunnel",
|
||||
"conn.protocol_slcan": "SLCAN / LAWICEL",
|
||||
"conn.refresh": "Refresh",
|
||||
"conn.request": "Request",
|
||||
"conn.disconnect": "Disconnect",
|
||||
"conn.connecting": "Connecting...",
|
||||
"conn.connect": "Connect",
|
||||
"conn.ws_section": "WebSocket Connection",
|
||||
"conn.host": "Host/IP Address",
|
||||
"conn.ws_port": "Port",
|
||||
"conn.node_id": "Node ID",
|
||||
"conn.signing": "Mavlink Signing",
|
||||
"conn.secret_key": "Secret Key",
|
||||
"conn.show_secret": "Show secret",
|
||||
"conn.hide_secret": "Hide secret",
|
||||
"conn.serial_closed": "Serial connection closed",
|
||||
"conn.serial_slcan_ok": "Serial SLCAN connection established",
|
||||
"conn.serial_mavlink_ok": "Serial MAVLink connection established",
|
||||
"conn.serial_failed": "Serial connection failed: {error}",
|
||||
"conn.could_not_connect": "Could not connect to port",
|
||||
"conn.serial_error": "Serial error: {error}",
|
||||
"conn.unknown_error": "Unknown error",
|
||||
"conn.ws_closed": "WebSocket connection closed",
|
||||
"conn.ws_connected": "WebSocket connection established",
|
||||
"conn.ws_failed": "Connection failed",
|
||||
"conn.ws_failed_detail": "Connection failed: {error}",
|
||||
"conn.ws_error": "WebSocket error: {error}",
|
||||
"conn.ip_required": "IP address is required",
|
||||
"conn.ip_invalid_parts": "Each part must be a number between 0-255",
|
||||
"conn.ip_invalid": "Invalid IP address or hostname",
|
||||
"conn.port_required": "Port is required",
|
||||
"conn.port_range": "Port must be between 1-65535",
|
||||
"conn.node_id_range": "Node ID must be between 1-127",
|
||||
"conn.no_port_selected": "No port selected",
|
||||
"conn.serial_port": "Serial Port",
|
||||
|
||||
// NodeParam.js
|
||||
"param.title": "Parameters",
|
||||
"param.fetching": "Fetching...",
|
||||
"param.fetch_all": "Fetch All",
|
||||
"param.store_all": "Store All",
|
||||
"param.erase_all": "Erase All",
|
||||
"param.download": "Download",
|
||||
"param.load": "Load",
|
||||
"param.col_idx": "Idx",
|
||||
"param.col_name": "Name",
|
||||
"param.col_type": "Type",
|
||||
"param.col_value": "Value",
|
||||
"param.col_default": "Default",
|
||||
"param.col_min": "Min",
|
||||
"param.col_max": "Max",
|
||||
"param.edit_param": "Edit Parameter",
|
||||
"param.true": "True",
|
||||
"param.false": "False",
|
||||
"param.disabled": "Disabled",
|
||||
"param.enabled": "Enabled",
|
||||
|
||||
// EditParamModal.js
|
||||
"edit.title": "Edit Parameter",
|
||||
"edit.param_name": "Parameter Name",
|
||||
"edit.unknown": "Unknown",
|
||||
"edit.string_value": "String Value",
|
||||
"edit.enable_disable": "Enable/Disable:",
|
||||
"edit.new_value": "New Value",
|
||||
"edit.value_range": "Value must be between {min} and {max}",
|
||||
"edit.current_value": "Current Value",
|
||||
"edit.current_rtttl": "Current RTTTL",
|
||||
"edit.default_value": "Default Value",
|
||||
"edit.min_value": "Min Value",
|
||||
"edit.max_value": "Max Value",
|
||||
"edit.error_parsing_melody": "Error parsing melody data",
|
||||
"edit.select_preset": "Select Preset Tune",
|
||||
"edit.choose_preset": "Choose a preset tune",
|
||||
"edit.apply": "Apply",
|
||||
"edit.rtttl_tune": "RTTTL Tune",
|
||||
"edit.rtttl_placeholder": "Format: name:d=duration,o=octave,b=bpm:notes",
|
||||
"edit.stop_tune": "Stop tune",
|
||||
"edit.play_tune": "Play tune",
|
||||
"edit.rtttl_instruction": "Enter RTTTL format tune or select a preset",
|
||||
"edit.rtttl_guide_title": "RTTTL Format Guide",
|
||||
"edit.rtttl_guide_duration": "d=duration (1=whole, 2=half, 4=quarter, 8=eighth, 16=16th note)",
|
||||
"edit.rtttl_guide_octave": "o=octave (4-7 where 5 is default)",
|
||||
"edit.rtttl_guide_tempo": "b=tempo (beats per minute)",
|
||||
"edit.rtttl_guide_notes": "Notes are: c, c#, d, d#, e, f, f#, g, g#, a, a#, b or h",
|
||||
"edit.rtttl_guide_example": "Example: Beep:d=4,o=5,b=120:c",
|
||||
"edit.rtttl_warning": "Warning: Invalid RTTTL format! Using a default empty tune instead.",
|
||||
"edit.rtttl_invalid": "Invalid RTTTL format! Format should be: name:defaults:notes",
|
||||
"edit.error_saving": "Error saving tune: {error}",
|
||||
"edit.error_playing": "Error playing tune: {error}",
|
||||
"edit.cancel": "Cancel",
|
||||
"edit.save": "Save",
|
||||
"edit.true": "True",
|
||||
"edit.false": "False",
|
||||
|
||||
// BusMonitor.js
|
||||
"bus.title": "Bus Monitor",
|
||||
"bus.auto_scroll": "Auto Scroll",
|
||||
"bus.export": "Export",
|
||||
"bus.col_dir": "Dir",
|
||||
"bus.col_time": "Time",
|
||||
"bus.col_can_id": "CAN ID",
|
||||
"bus.col_hex_data": "Hex Data",
|
||||
"bus.col_src": "Src",
|
||||
"bus.col_dst": "Dst",
|
||||
"bus.col_data_type": "Data Type",
|
||||
"bus.showing": "Showing {count} of max {max} transfers",
|
||||
"bus.paused": "PAUSED",
|
||||
"bus.message_details": "Message Details",
|
||||
"bus.close": "Close",
|
||||
"bus.broadcast": "Broadcast",
|
||||
"bus.no_payload": "No detailed payload data available for this transfer.",
|
||||
"bus.details_heading": "### Message details",
|
||||
"bus.payload_heading": "### Message Payload",
|
||||
"bus.detail_direction": "Direction:",
|
||||
"bus.detail_time": "Time:",
|
||||
"bus.detail_can_id": "CAN ID:",
|
||||
"bus.detail_source": "Source Node:",
|
||||
"bus.detail_dest": "Destination Node:",
|
||||
"bus.detail_data_type": "Data Type:",
|
||||
"bus.detail_hex_data": "Hex Data:",
|
||||
"bus.csv_direction": "Direction",
|
||||
"bus.csv_timestamp": "Timestamp",
|
||||
"bus.csv_can_id": "CAN ID (Hex)",
|
||||
"bus.csv_hex_data": "Hex Data",
|
||||
"bus.csv_src": "Src Node ID",
|
||||
"bus.csv_dst": "Dst Node ID",
|
||||
"bus.csv_data_type": "Data Type",
|
||||
"bus.csv_raw": "Raw Data",
|
||||
|
||||
// SubscriberWindow.js
|
||||
"sub.not_allowed": "Not Allowed To Open Directly",
|
||||
"sub.rx": "RX:",
|
||||
"sub.rates": "Rates(Hz):",
|
||||
"sub.max": "Max:",
|
||||
"sub.all": "All",
|
||||
|
||||
// EscPanel.js
|
||||
"esc.channels": "Channels:",
|
||||
"esc.remove_propellers": "REMOVE PROPELLERS!",
|
||||
"esc.send_safety": "Send Safety",
|
||||
"esc.send_arming": "Send Arming",
|
||||
"esc.broadcast_rate": "Broadcast Rate:",
|
||||
"esc.index": "Index:",
|
||||
"esc.error": "Err:",
|
||||
"esc.temp": "Temp:",
|
||||
"esc.volt": "Volt:",
|
||||
"esc.curr": "Curr:",
|
||||
"esc.rpm": "RPM:",
|
||||
"esc.rat": "RAT:",
|
||||
"esc.nc": "NC",
|
||||
"esc.stop": "Stop",
|
||||
"esc.cmd": "cmd:",
|
||||
"esc.stop_all": "Stop All",
|
||||
|
||||
// ActuatorPanel.js
|
||||
"act.ids": "Actuator IDs ({count})",
|
||||
"act.range_settings": "Range Settings",
|
||||
"act.broadcast_rate": "Broadcast Rate:",
|
||||
"act.select_ids_title": "Select Actuator IDs",
|
||||
"act.select_ids_label": "Select Actuator IDs:",
|
||||
"act.done": "Done",
|
||||
"act.range_title": "Command Type Range Settings",
|
||||
"act.range_instruction": "Configure default ranges for each command type. These settings can be applied to all actuators.",
|
||||
"act.unitless_fixed": "Unitless command range is fixed at -1 to 1",
|
||||
"act.min": "Min",
|
||||
"act.max": "Max",
|
||||
"act.apply": "Apply",
|
||||
"act.apply_all": "Apply All Ranges",
|
||||
"act.close": "Close",
|
||||
"act.id": "ID:",
|
||||
"act.pos": "Pos:",
|
||||
"act.force": "Force:",
|
||||
"act.speed": "Speed:",
|
||||
"act.rat": "RAT:",
|
||||
"act.nc": "NC",
|
||||
"act.unknown": "unknown",
|
||||
"act.type_unitless": "Unitless",
|
||||
"act.type_position": "Position",
|
||||
"act.type_force": "Force",
|
||||
"act.type_speed": "Speed",
|
||||
"act.type_unitless_label": "Unitless [-1, 1]",
|
||||
"act.type_position_label": "Position (m/rad)",
|
||||
"act.type_force_label": "Force (N/Nm)",
|
||||
"act.type_speed_label": "Speed (m/s, rad/s)",
|
||||
"act.zero": "Zero",
|
||||
"act.cmd": "cmd:",
|
||||
"act.zero_all": "Zero All",
|
||||
|
||||
// DnaServerModal.js
|
||||
"dna.title": "Dynamic Node ID Allocation Server",
|
||||
"dna.active": "Server Active",
|
||||
"dna.control": "Server Control",
|
||||
"dna.processing": "Processing...",
|
||||
"dna.stop": "Stop",
|
||||
"dna.start": "Start",
|
||||
"dna.min_node_id": "Min Node ID",
|
||||
"dna.must_lt_max": "Must be < Max",
|
||||
"dna.max_node_id": "Max Node ID",
|
||||
"dna.must_gt_min": "Must be > Min",
|
||||
"dna.persist": "Persist Allocations",
|
||||
"dna.persist_tooltip": "When enabled, node ID allocations are stored and restored when the server restarts",
|
||||
"dna.allocated": "Allocated Node IDs ({count})",
|
||||
"dna.no_allocations": "No node IDs allocated",
|
||||
"dna.col_nid": "NID",
|
||||
"dna.col_uuid": "UUID",
|
||||
"dna.col_action": "Action",
|
||||
"dna.refresh_tooltip": "Refresh allocation list",
|
||||
"dna.started": "DNA server started successfully",
|
||||
"dna.failed_start": "Failed to start DNA server",
|
||||
"dna.stopped": "DNA server stopped",
|
||||
"dna.revoked": "Node ID {id} allocation revoked",
|
||||
"dna.revoke_failed": "Failed to revoke allocation for node ID {id}",
|
||||
"dna.refreshed": "Allocations refreshed",
|
||||
"dna.error": "Error: {error}",
|
||||
"dna.invalid_range": "Min ID must be less than Max ID",
|
||||
|
||||
// NodeProperties.js
|
||||
"props.title": "Node Properties",
|
||||
"props.multi_editor": "Multi Node Editor",
|
||||
"props.node_id": "Node ID",
|
||||
"props.name": "Name",
|
||||
"props.mode": "Mode",
|
||||
"props.health": "Health",
|
||||
"props.uptime": "Uptime",
|
||||
"props.vendor_code": "Vendor Specific Status Code",
|
||||
"props.sw_version": "Software Version",
|
||||
"props.crc64": "CRC64",
|
||||
"props.vcs_commit": "VCS Commit",
|
||||
"props.hw_version": "Hardware Version",
|
||||
"props.uid": "UID",
|
||||
"props.certificate": "Cert. of authenticity",
|
||||
"props.controls": "Node Controls",
|
||||
"props.restart": "Restart",
|
||||
"props.transport_stats": "Get Transport Stats",
|
||||
"props.update_firmware": "Update Firmware",
|
||||
|
||||
// NodeLogs.js
|
||||
"logs.title": "Logs",
|
||||
"logs.col_nid": "NID",
|
||||
"logs.col_time": "Time",
|
||||
"logs.col_level": "Level",
|
||||
"logs.col_source": "Source",
|
||||
"logs.col_text": "Text",
|
||||
|
||||
// ToolsMenu.js
|
||||
"tools.title": "Tools",
|
||||
"tools.subscriber": "Subscriber",
|
||||
"tools.bus_monitor": "Bus Monitor",
|
||||
|
||||
// PanelsMenu.js
|
||||
"panels.title": "Panels",
|
||||
"panels.esc": "ESC",
|
||||
"panels.actuator": "Actuator",
|
||||
|
||||
// ConfirmRestartModal.js
|
||||
"confirm.title": "Confirm Restart",
|
||||
"confirm.message": "Are you sure you want to restart the node?",
|
||||
"confirm.cancel": "Cancel",
|
||||
"confirm.confirm": "Confirm",
|
||||
|
||||
// FirmwareUpdateModal.js
|
||||
"fw.title": "Firmware Update",
|
||||
"fw.select_hint": "Please select the firmware file (.bin|.hex) to upload to node {id}.",
|
||||
"fw.select_file": "Select Firmware File",
|
||||
"fw.selected_file": "Selected File: {name} ({size} bytes)",
|
||||
"fw.invalid_file": "Invalid file type. Please select a .bin or .hex firmware file.",
|
||||
"fw.load_failed": "Failed to load firmware file",
|
||||
"fw.starting": "Starting firmware update...",
|
||||
"fw.updating": "Updating firmware: {progress}% ({offset}/{total} bytes)",
|
||||
"fw.success": "Firmware update completed successfully!",
|
||||
"fw.update_failed": "Update failed: code: {code} {message}",
|
||||
"fw.start_failed": "Failed to start update: {error}",
|
||||
"fw.node_unavailable": "Local node not available",
|
||||
"fw.error_occurred": "An error occurred during the update.",
|
||||
"fw.cancel": "Cancel",
|
||||
"fw.update": "Update",
|
||||
"fw.updating_ellipsis": "Updating...",
|
||||
"fw.close": "Close",
|
||||
};
|
||||
|
||||
export const zh = {
|
||||
// App.js
|
||||
"app.title": "DroneCAN Web Tools",
|
||||
"app.bus": "总线 {n}",
|
||||
"app.dna": "DNA",
|
||||
"app.adapter": "适配器",
|
||||
"app.connected": "设备连接成功",
|
||||
"app.disconnected": "设备已断开",
|
||||
"app.bus_switched": "已切换到 CAN 总线 {bus}",
|
||||
"app.dna_stopped": "DNA 服务器已停止",
|
||||
"app.dna_started": "DNA 服务器已启动",
|
||||
"app.dna_failed": "DNA 服务器启动失败",
|
||||
|
||||
// ConnectionSettingsModal.js
|
||||
"conn.title": "适配器设置",
|
||||
"conn.connected_serial_slcan": "已通过串口连接 (SLCAN)",
|
||||
"conn.connected_serial_mavlink": "已通过串口连接 (MAVLink)",
|
||||
"conn.connected_ws": "已通过 {type} 连接",
|
||||
"conn.serial_section": "串口连接",
|
||||
"conn.port": "端口",
|
||||
"conn.no_ports": "无可用端口",
|
||||
"conn.baud_rate": "波特率",
|
||||
"conn.serial_protocol": "串口协议",
|
||||
"conn.protocol_mavlink": "MAVLink 隧道",
|
||||
"conn.protocol_slcan": "SLCAN / LAWICEL",
|
||||
"conn.refresh": "刷新",
|
||||
"conn.request": "请求",
|
||||
"conn.disconnect": "断开",
|
||||
"conn.connecting": "连接中...",
|
||||
"conn.connect": "连接",
|
||||
"conn.ws_section": "WebSocket 连接",
|
||||
"conn.host": "主机/IP 地址",
|
||||
"conn.ws_port": "端口",
|
||||
"conn.node_id": "节点 ID",
|
||||
"conn.signing": "MAVLink 签名",
|
||||
"conn.secret_key": "密钥",
|
||||
"conn.show_secret": "显示密钥",
|
||||
"conn.hide_secret": "隐藏密钥",
|
||||
"conn.serial_closed": "串口连接已关闭",
|
||||
"conn.serial_slcan_ok": "SLCAN 串口连接已建立",
|
||||
"conn.serial_mavlink_ok": "MAVLink 串口连接已建立",
|
||||
"conn.serial_failed": "串口连接失败: {error}",
|
||||
"conn.could_not_connect": "无法连接到端口",
|
||||
"conn.serial_error": "串口错误: {error}",
|
||||
"conn.unknown_error": "未知错误",
|
||||
"conn.ws_closed": "WebSocket 连接已关闭",
|
||||
"conn.ws_connected": "WebSocket 连接已建立",
|
||||
"conn.ws_failed": "连接失败",
|
||||
"conn.ws_failed_detail": "连接失败: {error}",
|
||||
"conn.ws_error": "WebSocket 错误: {error}",
|
||||
"conn.ip_required": "IP 地址为必填项",
|
||||
"conn.ip_invalid_parts": "每段必须是 0-255 之间的数字",
|
||||
"conn.ip_invalid": "无效的 IP 地址或主机名",
|
||||
"conn.port_required": "端口为必填项",
|
||||
"conn.port_range": "端口必须在 1-65535 之间",
|
||||
"conn.node_id_range": "节点 ID 必须在 1-127 之间",
|
||||
"conn.no_port_selected": "未选择端口",
|
||||
"conn.serial_port": "串口",
|
||||
|
||||
// NodeParam.js
|
||||
"param.title": "参数",
|
||||
"param.fetching": "获取中...",
|
||||
"param.fetch_all": "获取全部",
|
||||
"param.store_all": "保存全部",
|
||||
"param.erase_all": "擦除全部",
|
||||
"param.download": "下载",
|
||||
"param.load": "加载",
|
||||
"param.col_idx": "索引",
|
||||
"param.col_name": "名称",
|
||||
"param.col_type": "类型",
|
||||
"param.col_value": "值",
|
||||
"param.col_default": "默认值",
|
||||
"param.col_min": "最小值",
|
||||
"param.col_max": "最大值",
|
||||
"param.edit_param": "编辑参数",
|
||||
"param.true": "是",
|
||||
"param.false": "否",
|
||||
"param.disabled": "禁用",
|
||||
"param.enabled": "启用",
|
||||
|
||||
// EditParamModal.js
|
||||
"edit.title": "编辑参数",
|
||||
"edit.param_name": "参数名称",
|
||||
"edit.unknown": "未知",
|
||||
"edit.string_value": "字符串值",
|
||||
"edit.enable_disable": "启用/禁用:",
|
||||
"edit.new_value": "新值",
|
||||
"edit.value_range": "值必须在 {min} 和 {max} 之间",
|
||||
"edit.current_value": "当前值",
|
||||
"edit.current_rtttl": "当前 RTTTL",
|
||||
"edit.default_value": "默认值",
|
||||
"edit.min_value": "最小值",
|
||||
"edit.max_value": "最大值",
|
||||
"edit.error_parsing_melody": "解析旋律数据出错",
|
||||
"edit.select_preset": "选择预设铃声",
|
||||
"edit.choose_preset": "选择一个预设铃声",
|
||||
"edit.apply": "应用",
|
||||
"edit.rtttl_tune": "RTTTL 铃声",
|
||||
"edit.rtttl_placeholder": "格式: name:d=duration,o=octave,b=bpm:notes",
|
||||
"edit.stop_tune": "停止播放",
|
||||
"edit.play_tune": "播放铃声",
|
||||
"edit.rtttl_instruction": "输入 RTTTL 格式铃声或选择预设",
|
||||
"edit.rtttl_guide_title": "RTTTL 格式指南",
|
||||
"edit.rtttl_guide_duration": "d=时值 (1=全音符, 2=二分, 4=四分, 8=八分, 16=十六分音符)",
|
||||
"edit.rtttl_guide_octave": "o=八度 (4-7, 默认为 5)",
|
||||
"edit.rtttl_guide_tempo": "b=速度 (每分钟节拍数)",
|
||||
"edit.rtttl_guide_notes": "音符: c, c#, d, d#, e, f, f#, g, g#, a, a#, b 或 h",
|
||||
"edit.rtttl_guide_example": "示例: Beep:d=4,o=5,b=120:c",
|
||||
"edit.rtttl_warning": "警告: 无效的 RTTTL 格式! 使用默认空铃声代替。",
|
||||
"edit.rtttl_invalid": "无效的 RTTTL 格式! 格式应为: name:defaults:notes",
|
||||
"edit.error_saving": "保存铃声出错: {error}",
|
||||
"edit.error_playing": "播放铃声出错: {error}",
|
||||
"edit.cancel": "取消",
|
||||
"edit.save": "保存",
|
||||
"edit.true": "是",
|
||||
"edit.false": "否",
|
||||
|
||||
// BusMonitor.js
|
||||
"bus.title": "总线监视器",
|
||||
"bus.auto_scroll": "自动滚动",
|
||||
"bus.export": "导出",
|
||||
"bus.col_dir": "方向",
|
||||
"bus.col_time": "时间",
|
||||
"bus.col_can_id": "CAN ID",
|
||||
"bus.col_hex_data": "十六进制数据",
|
||||
"bus.col_src": "源",
|
||||
"bus.col_dst": "目标",
|
||||
"bus.col_data_type": "数据类型",
|
||||
"bus.showing": "显示 {count} 条,最大 {max} 条",
|
||||
"bus.paused": "已暂停",
|
||||
"bus.message_details": "消息详情",
|
||||
"bus.close": "关闭",
|
||||
"bus.broadcast": "广播",
|
||||
"bus.no_payload": "该传输无详细负载数据。",
|
||||
"bus.details_heading": "### 消息详情",
|
||||
"bus.payload_heading": "### 消息负载",
|
||||
"bus.detail_direction": "方向:",
|
||||
"bus.detail_time": "时间:",
|
||||
"bus.detail_can_id": "CAN ID:",
|
||||
"bus.detail_source": "源节点:",
|
||||
"bus.detail_dest": "目标节点:",
|
||||
"bus.detail_data_type": "数据类型:",
|
||||
"bus.detail_hex_data": "十六进制数据:",
|
||||
"bus.csv_direction": "方向",
|
||||
"bus.csv_timestamp": "时间戳",
|
||||
"bus.csv_can_id": "CAN ID (十六进制)",
|
||||
"bus.csv_hex_data": "十六进制数据",
|
||||
"bus.csv_src": "源节点 ID",
|
||||
"bus.csv_dst": "目标节点 ID",
|
||||
"bus.csv_data_type": "数据类型",
|
||||
"bus.csv_raw": "原始数据",
|
||||
|
||||
// SubscriberWindow.js
|
||||
"sub.not_allowed": "不允许直接打开",
|
||||
"sub.rx": "接收:",
|
||||
"sub.rates": "频率(Hz):",
|
||||
"sub.max": "最大:",
|
||||
"sub.all": "全部",
|
||||
|
||||
// EscPanel.js
|
||||
"esc.channels": "通道数:",
|
||||
"esc.remove_propellers": "请拆除螺旋桨!",
|
||||
"esc.send_safety": "发送安全指令",
|
||||
"esc.send_arming": "发送解锁指令",
|
||||
"esc.broadcast_rate": "广播速率:",
|
||||
"esc.index": "索引:",
|
||||
"esc.error": "错误:",
|
||||
"esc.temp": "温度:",
|
||||
"esc.volt": "电压:",
|
||||
"esc.curr": "电流:",
|
||||
"esc.rpm": "转速:",
|
||||
"esc.rat": "功率:",
|
||||
"esc.nc": "无连接",
|
||||
"esc.stop": "停止",
|
||||
"esc.cmd": "指令:",
|
||||
"esc.stop_all": "全部停止",
|
||||
|
||||
// ActuatorPanel.js
|
||||
"act.ids": "执行器 ID ({count})",
|
||||
"act.range_settings": "范围设置",
|
||||
"act.broadcast_rate": "广播速率:",
|
||||
"act.select_ids_title": "选择执行器 ID",
|
||||
"act.select_ids_label": "选择执行器 ID:",
|
||||
"act.done": "完成",
|
||||
"act.range_title": "指令类型范围设置",
|
||||
"act.range_instruction": "配置每种指令类型的默认范围。这些设置可以应用到所有执行器。",
|
||||
"act.unitless_fixed": "无量纲指令范围固定为 -1 到 1",
|
||||
"act.min": "最小值",
|
||||
"act.max": "最大值",
|
||||
"act.apply": "应用",
|
||||
"act.apply_all": "应用所有范围",
|
||||
"act.close": "关闭",
|
||||
"act.id": "ID:",
|
||||
"act.pos": "位置:",
|
||||
"act.force": "力:",
|
||||
"act.speed": "速度:",
|
||||
"act.rat": "功率:",
|
||||
"act.nc": "无连接",
|
||||
"act.unknown": "未知",
|
||||
"act.type_unitless": "无量纲",
|
||||
"act.type_position": "位置",
|
||||
"act.type_force": "力",
|
||||
"act.type_speed": "速度",
|
||||
"act.type_unitless_label": "无量纲 [-1, 1]",
|
||||
"act.type_position_label": "位置 (m/rad)",
|
||||
"act.type_force_label": "力 (N/Nm)",
|
||||
"act.type_speed_label": "速度 (m/s, rad/s)",
|
||||
"act.zero": "归零",
|
||||
"act.cmd": "指令:",
|
||||
"act.zero_all": "全部归零",
|
||||
|
||||
// DnaServerModal.js
|
||||
"dna.title": "动态节点 ID 分配服务器",
|
||||
"dna.active": "服务器运行中",
|
||||
"dna.control": "服务器控制",
|
||||
"dna.processing": "处理中...",
|
||||
"dna.stop": "停止",
|
||||
"dna.start": "启动",
|
||||
"dna.min_node_id": "最小节点 ID",
|
||||
"dna.must_lt_max": "必须小于最大值",
|
||||
"dna.max_node_id": "最大节点 ID",
|
||||
"dna.must_gt_min": "必须大于最小值",
|
||||
"dna.persist": "持久化分配",
|
||||
"dna.persist_tooltip": "启用后,节点 ID 分配将被保存,服务器重启时自动恢复",
|
||||
"dna.allocated": "已分配节点 ID ({count})",
|
||||
"dna.no_allocations": "暂无已分配的节点 ID",
|
||||
"dna.col_nid": "节点ID",
|
||||
"dna.col_uuid": "UUID",
|
||||
"dna.col_action": "操作",
|
||||
"dna.refresh_tooltip": "刷新分配列表",
|
||||
"dna.started": "DNA 服务器启动成功",
|
||||
"dna.failed_start": "DNA 服务器启动失败",
|
||||
"dna.stopped": "DNA 服务器已停止",
|
||||
"dna.revoked": "节点 ID {id} 分配已撤销",
|
||||
"dna.revoke_failed": "撤销节点 ID {id} 分配失败",
|
||||
"dna.refreshed": "分配列表已刷新",
|
||||
"dna.error": "错误: {error}",
|
||||
"dna.invalid_range": "最小 ID 必须小于最大 ID",
|
||||
|
||||
// NodeProperties.js
|
||||
"props.title": "节点属性",
|
||||
"props.multi_editor": "多节点编辑器",
|
||||
"props.node_id": "节点 ID",
|
||||
"props.name": "名称",
|
||||
"props.mode": "模式",
|
||||
"props.health": "健康状态",
|
||||
"props.uptime": "运行时间",
|
||||
"props.vendor_code": "供应商状态码",
|
||||
"props.sw_version": "软件版本",
|
||||
"props.crc64": "CRC64",
|
||||
"props.vcs_commit": "VCS 提交",
|
||||
"props.hw_version": "硬件版本",
|
||||
"props.uid": "UID",
|
||||
"props.certificate": "认证证书",
|
||||
"props.controls": "节点控制",
|
||||
"props.restart": "重启",
|
||||
"props.transport_stats": "获取传输统计",
|
||||
"props.update_firmware": "更新固件",
|
||||
|
||||
// NodeLogs.js
|
||||
"logs.title": "日志",
|
||||
"logs.col_nid": "节点ID",
|
||||
"logs.col_time": "时间",
|
||||
"logs.col_level": "级别",
|
||||
"logs.col_source": "来源",
|
||||
"logs.col_text": "内容",
|
||||
|
||||
// ToolsMenu.js
|
||||
"tools.title": "工具",
|
||||
"tools.subscriber": "订阅器",
|
||||
"tools.bus_monitor": "总线监视器",
|
||||
|
||||
// PanelsMenu.js
|
||||
"panels.title": "面板",
|
||||
"panels.esc": "ESC",
|
||||
"panels.actuator": "执行器",
|
||||
|
||||
// ConfirmRestartModal.js
|
||||
"confirm.title": "确认重启",
|
||||
"confirm.message": "确定要重启该节点吗?",
|
||||
"confirm.cancel": "取消",
|
||||
"confirm.confirm": "确认",
|
||||
|
||||
// FirmwareUpdateModal.js
|
||||
"fw.title": "固件更新",
|
||||
"fw.select_hint": "请选择要上传到节点 {id} 的固件文件 (.bin|.hex)。",
|
||||
"fw.select_file": "选择固件文件",
|
||||
"fw.selected_file": "已选文件: {name} ({size} 字节)",
|
||||
"fw.invalid_file": "无效的文件类型。请选择 .bin 或 .hex 固件文件。",
|
||||
"fw.load_failed": "加载固件文件失败",
|
||||
"fw.starting": "正在开始固件更新...",
|
||||
"fw.updating": "正在更新固件: {progress}% ({offset}/{total} 字节)",
|
||||
"fw.success": "固件更新成功完成!",
|
||||
"fw.update_failed": "更新失败: 代码: {code} {message}",
|
||||
"fw.start_failed": "启动更新失败: {error}",
|
||||
"fw.node_unavailable": "本地节点不可用",
|
||||
"fw.error_occurred": "更新过程中发生错误。",
|
||||
"fw.cancel": "取消",
|
||||
"fw.update": "更新",
|
||||
"fw.updating_ellipsis": "更新中...",
|
||||
"fw.close": "关闭",
|
||||
};
|
||||
Reference in New Issue
Block a user