add Chinese translate

This commit is contained in:
2026-05-23 09:31:44 +08:00
parent e612c852e5
commit a02925dfd0
17 changed files with 1020 additions and 296 deletions
+36 -34
View File
@@ -10,6 +10,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause'; import PauseIcon from '@mui/icons-material/Pause';
import CheckBoxIcon from '@mui/icons-material/CheckBox'; import CheckBoxIcon from '@mui/icons-material/CheckBox';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import { useTranslation } from './i18n/LanguageContext';
const MAX_ACTUATOR_IDS = 256; const MAX_ACTUATOR_IDS = 256;
@@ -50,6 +51,7 @@ const ActuatorPanel = () => {
const [showIdSelector, setShowIdSelector] = useState(false); const [showIdSelector, setShowIdSelector] = useState(false);
const nodeId = 0; const nodeId = 0;
const { t } = useTranslation();
const activeActuatorIds = enabledActuatorIds const activeActuatorIds = enabledActuatorIds
.map((enabled, id) => enabled ? id : null) .map((enabled, id) => enabled ? id : null)
@@ -282,9 +284,9 @@ const ActuatorPanel = () => {
const renderIdSelectorDialog = () => ( const renderIdSelectorDialog = () => (
<Dialog open={showIdSelector} onClose={() => setShowIdSelector(false)}> <Dialog open={showIdSelector} onClose={() => setShowIdSelector(false)}>
<DialogTitle>Select Actuator IDs</DialogTitle> <DialogTitle>{t('act.select_ids_title')}</DialogTitle>
<DialogContent> <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 }}> <FormGroup sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 1 }}>
{Array(MAX_ACTUATOR_IDS).fill(0).map((_, id) => ( {Array(MAX_ACTUATOR_IDS).fill(0).map((_, id) => (
<FormControlLabel <FormControlLabel
@@ -302,7 +304,7 @@ const ActuatorPanel = () => {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setShowIdSelector(false)} color="primary"> <Button onClick={() => setShowIdSelector(false)} color="primary">
Done {t('act.done')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -316,11 +318,11 @@ const ActuatorPanel = () => {
fullWidth fullWidth
> >
<DialogTitle> <DialogTitle>
Command Type Range Settings {t('act.range_title')}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Typography variant="body2" sx={{ mb: 2, fontStyle: 'italic' }}> <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> </Typography>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
@@ -328,7 +330,7 @@ const ActuatorPanel = () => {
{COMMAND_TYPE_LABELS[0]} {COMMAND_TYPE_LABELS[0]}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Unitless command range is fixed at -1 to 1 {t('act.unitless_fixed')}
</Typography> </Typography>
</Box> </Box>
@@ -339,7 +341,7 @@ const ActuatorPanel = () => {
<Grid container spacing={2} alignItems="center"> <Grid container spacing={2} alignItems="center">
<Grid item xs={5}> <Grid item xs={5}>
<TextField <TextField
label="Min" label={t('act.min')}
type="number" type="number"
size="small" size="small"
fullWidth fullWidth
@@ -350,7 +352,7 @@ const ActuatorPanel = () => {
</Grid> </Grid>
<Grid item xs={5}> <Grid item xs={5}>
<TextField <TextField
label="Max" label={t('act.max')}
type="number" type="number"
size="small" size="small"
fullWidth fullWidth
@@ -366,7 +368,7 @@ const ActuatorPanel = () => {
onClick={() => applyRangesToAllOfType(1)} onClick={() => applyRangesToAllOfType(1)}
fullWidth fullWidth
> >
Apply {t('act.apply')}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
@@ -379,7 +381,7 @@ const ActuatorPanel = () => {
<Grid container spacing={2} alignItems="center"> <Grid container spacing={2} alignItems="center">
<Grid item xs={5}> <Grid item xs={5}>
<TextField <TextField
label="Min" label={t('act.min')}
type="number" type="number"
size="small" size="small"
fullWidth fullWidth
@@ -390,7 +392,7 @@ const ActuatorPanel = () => {
</Grid> </Grid>
<Grid item xs={5}> <Grid item xs={5}>
<TextField <TextField
label="Max" label={t('act.max')}
type="number" type="number"
size="small" size="small"
fullWidth fullWidth
@@ -406,7 +408,7 @@ const ActuatorPanel = () => {
onClick={() => applyRangesToAllOfType(2)} onClick={() => applyRangesToAllOfType(2)}
fullWidth fullWidth
> >
Apply {t('act.apply')}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
@@ -419,7 +421,7 @@ const ActuatorPanel = () => {
<Grid container spacing={2} alignItems="center"> <Grid container spacing={2} alignItems="center">
<Grid item xs={5}> <Grid item xs={5}>
<TextField <TextField
label="Min" label={t('act.min')}
type="number" type="number"
size="small" size="small"
fullWidth fullWidth
@@ -430,7 +432,7 @@ const ActuatorPanel = () => {
</Grid> </Grid>
<Grid item xs={5}> <Grid item xs={5}>
<TextField <TextField
label="Max" label={t('act.max')}
type="number" type="number"
size="small" size="small"
fullWidth fullWidth
@@ -446,7 +448,7 @@ const ActuatorPanel = () => {
onClick={() => applyRangesToAllOfType(3)} onClick={() => applyRangesToAllOfType(3)}
fullWidth fullWidth
> >
Apply {t('act.apply')}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
@@ -458,13 +460,13 @@ const ActuatorPanel = () => {
color="primary" color="primary"
variant="contained" variant="contained"
> >
Apply All Ranges {t('act.apply_all')}
</Button> </Button>
<Button <Button
onClick={() => setShowSettingsModal(false)} onClick={() => setShowSettingsModal(false)}
color="primary" color="primary"
> >
Close {t('act.close')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -553,7 +555,7 @@ const ActuatorPanel = () => {
onClick={() => setShowIdSelector(true)} onClick={() => setShowIdSelector(true)}
sx={{ textTransform: 'none' }} sx={{ textTransform: 'none' }}
> >
Actuator IDs ({activeActuatorIds.length}) {t('act.ids', { count: activeActuatorIds.length })}
</Button> </Button>
</Box> </Box>
@@ -566,14 +568,14 @@ const ActuatorPanel = () => {
onClick={() => setShowSettingsModal(true)} onClick={() => setShowSettingsModal(true)}
sx={{ textTransform: 'none' }} sx={{ textTransform: 'none' }}
> >
Range Settings {t('act.range_settings')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<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 <TextField
type="number" type="number"
size="small" size="small"
@@ -642,22 +644,22 @@ const ActuatorPanel = () => {
width: '50%' width: '50%'
}}> }}>
<Box sx={{flexGrow: 1}}> <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"> <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>
<Typography variant="body2" color="textSecondary"> <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>
<Typography variant="body2" color="textSecondary"> <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>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
RAT: {actuator.power_rating_pct !== null {t('act.rat')} {actuator.power_rating_pct !== null
? actuator.power_rating_pct === 127 ? actuator.power_rating_pct === 127
? "unknown" ? t('act.unknown')
: `${actuator.power_rating_pct.toFixed(1)} %` : `${actuator.power_rating_pct.toFixed(1)} %`
: "NC"} : t('act.nc')}
</Typography> </Typography>
</Box> </Box>
@@ -668,10 +670,10 @@ const ActuatorPanel = () => {
variant="outlined" variant="outlined"
sx={{ height: '30px', fontSize: '0.8rem' }} sx={{ height: '30px', fontSize: '0.8rem' }}
> >
<MenuItem value={COMMAND_TYPES.UNITLESS}>Unitless</MenuItem> <MenuItem value={COMMAND_TYPES.UNITLESS}>{t('act.type_unitless')}</MenuItem>
<MenuItem value={COMMAND_TYPES.POSITION}>Position</MenuItem> <MenuItem value={COMMAND_TYPES.POSITION}>{t('act.type_position')}</MenuItem>
<MenuItem value={COMMAND_TYPES.FORCE}>Force</MenuItem> <MenuItem value={COMMAND_TYPES.FORCE}>{t('act.type_force')}</MenuItem>
<MenuItem value={COMMAND_TYPES.SPEED}>Speed</MenuItem> <MenuItem value={COMMAND_TYPES.SPEED}>{t('act.type_speed')}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
@@ -700,7 +702,7 @@ const ActuatorPanel = () => {
fullWidth fullWidth
size="small" size="small"
> >
Zero {t('act.zero')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -749,7 +751,7 @@ const ActuatorPanel = () => {
}}> }}>
<Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}> <Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
cmd: [{ {t('act.cmd')} [{
activeActuatorIds activeActuatorIds
.map(id => { .map(id => {
const type = commandTypes[id] || COMMAND_TYPES.UNITLESS; const type = commandTypes[id] || COMMAND_TYPES.UNITLESS;
@@ -768,7 +770,7 @@ const ActuatorPanel = () => {
startIcon={<PanToolIcon />} startIcon={<PanToolIcon />}
onClick={handleZeroAll} onClick={handleZeroAll}
> >
Zero All {t('act.zero_all')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
+37 -13
View File
@@ -17,8 +17,10 @@ import './css/index.css';
import ConnectionIndicators from './ConnectionIndicators'; import ConnectionIndicators from './ConnectionIndicators';
import DnsIcon from '@mui/icons-material/Dns'; import DnsIcon from '@mui/icons-material/Dns';
import LanIcon from '@mui/icons-material/Lan'; import LanIcon from '@mui/icons-material/Lan';
import LanguageIcon from '@mui/icons-material/Language';
import CompactSidebar from './CompactSidebar'; import CompactSidebar from './CompactSidebar';
import DynamicNodeIdServer from './services/DynamicNodeIdServer'; import DynamicNodeIdServer from './services/DynamicNodeIdServer';
import { LanguageProvider, useTranslation } from './i18n/LanguageContext';
window.mavlinkSession = new MavlinkSession(); window.mavlinkSession = new MavlinkSession();
window.localNode = new dronecan.Node({name: "com.vimdrones.web_gui"}); window.localNode = new dronecan.Node({name: "com.vimdrones.web_gui"});
@@ -32,6 +34,17 @@ localNode.on('uavcan.protocol.file.Read.Request', (transfer) => {
}); });
const App = () => { const App = () => {
return (
<ThemeProvider theme={theme}>
<LanguageProvider>
<AppContent />
</LanguageProvider>
</ThemeProvider>
);
};
const AppContent = () => {
const { t, language, setLanguage } = useTranslation();
const [nodes, setNodes] = useState({}); const [nodes, setNodes] = useState({});
const [nodesUpdateTimestamp, setNodesUpdateTimestamp] = useState(0); const [nodesUpdateTimestamp, setNodesUpdateTimestamp] = useState(0);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
@@ -75,7 +88,7 @@ const App = () => {
const handleConnectionStatusChange = (isConnected) => { const handleConnectionStatusChange = (isConnected) => {
setIsConnected(isConnected); setIsConnected(isConnected);
showMessage( showMessage(
isConnected ? 'Successfully connected to device' : 'Disconnected from device', isConnected ? t('app.connected') : t('app.disconnected'),
isConnected ? 'success' : 'info' isConnected ? 'success' : 'info'
); );
}; };
@@ -87,7 +100,7 @@ const App = () => {
if (window.localNode) { if (window.localNode) {
window.localNode.changeBus(newBus); 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) { if (window.dnaServer.getStatus().isActive) {
window.dnaServer.stop(); window.dnaServer.stop();
setDnaServerActive(false); setDnaServerActive(false);
showMessage('DNA server stopped', 'info'); showMessage(t('app.dna_stopped'), 'info');
} else { } else {
const success = window.dnaServer.start(1, 125); const success = window.dnaServer.start(1, 125);
setDnaServerActive(success); 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 ( return (
<ThemeProvider theme={theme}> <>
<AppBar position="static"> <AppBar position="static">
<Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}> <Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center'}}> <Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center'}}>
@@ -142,7 +155,7 @@ const App = () => {
</a> </a>
</Box> </Box>
<Typography variant="caption"> <Typography variant="caption">
DroneCAN Web Tools {t('app.title')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', gap: 1}}> <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', backgroundColor: 'background.paper',
}} }}
> >
<MenuItem value={0}>Bus 1</MenuItem> <MenuItem value={0}>{t('app.bus', { n: 1 })}</MenuItem>
<MenuItem value={1}>Bus 2</MenuItem> <MenuItem value={1}>{t('app.bus', { n: 2 })}</MenuItem>
<MenuItem value={2}>Bus 3</MenuItem> <MenuItem value={2}>{t('app.bus', { n: 3 })}</MenuItem>
<MenuItem value={3}>Bus 4</MenuItem> <MenuItem value={3}>{t('app.bus', { n: 4 })}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
@@ -206,16 +219,27 @@ const App = () => {
} }
} : {}} } : {}}
> >
DNA {t('app.dna')}
</Button> </Button>
<Tooltip title={language === 'en' ? '中文' : 'English'}>
<IconButton
size="small"
color="inherit"
onClick={() => setLanguage(language === 'en' ? 'zh' : 'en')}
sx={{ mr: 0.5 }}
>
<LanguageIcon fontSize="small" />
</IconButton>
</Tooltip>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
startIcon={<LanIcon />} startIcon={<LanIcon />}
onClick={handleOpenModal} onClick={handleOpenModal}
> >
Adapter {t('app.adapter')}
</Button> </Button>
</Box> </Box>
</Toolbar> </Toolbar>
@@ -294,7 +318,7 @@ const App = () => {
{snackbarMessage} {snackbarMessage}
</Alert> </Alert>
</Snackbar> </Snackbar>
</ThemeProvider> </>
); );
}; };
+32 -30
View File
@@ -10,6 +10,7 @@ import PauseIcon from '@mui/icons-material/Pause';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from '@mui/icons-material/Save';
import { toYaml } from './dronecan/message_format_utils'; import { toYaml } from './dronecan/message_format_utils';
import { useTranslation } from './i18n/LanguageContext';
const BusMonitor = () => { const BusMonitor = () => {
const [transfers, setTransfers] = useState([]); const [transfers, setTransfers] = useState([]);
@@ -18,6 +19,7 @@ const BusMonitor = () => {
const [selectedTransfer, setSelectedTransfer] = useState(null); const [selectedTransfer, setSelectedTransfer] = useState(null);
const [detailsOpen, setDetailsOpen] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false);
const [messageYaml, setMessageYaml] = useState(''); const [messageYaml, setMessageYaml] = useState('');
const { t } = useTranslation();
const tableContainerRef = useRef(null); const tableContainerRef = useRef(null);
const maxTransfers = 1000; // Maximum number of transfers to store const maxTransfers = 1000; // Maximum number of transfers to store
@@ -29,23 +31,23 @@ const BusMonitor = () => {
let yamlText = ''; let yamlText = '';
if (transfer.data && transfer.data.toObj) { if (transfer.data && transfer.data.toObj) {
const msgObj = transfer.data.toObj(); const msgObj = transfer.data.toObj();
yamlText = `### Message details\n`; yamlText = `${t('bus.details_heading')}\n`;
yamlText += `Direction: ${transfer.direction}\n`; yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`;
yamlText += `Time: ${transfer.timestamp}\n`; yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`;
yamlText += `CAN ID: ${transfer.frameId}\n`; yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`;
yamlText += `Source Node: ${transfer.sourceNodeId}\n`; yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`;
yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`; yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`;
yamlText += `Data Type: ${transfer.dataType}\n\n`; yamlText += `${t('bus.detail_data_type')} ${transfer.dataType}\n\n`;
yamlText += `### Message Payload\n`; yamlText += `${t('bus.payload_heading')}\n`;
yamlText += toYaml(msgObj); yamlText += toYaml(msgObj);
} else { } else {
yamlText = `No detailed payload data available for this transfer.\n\n`; yamlText = `${t('bus.no_payload')}\n\n`;
yamlText += `Direction: ${transfer.direction}\n`; yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`;
yamlText += `Time: ${transfer.timestamp}\n`; yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`;
yamlText += `CAN ID: ${transfer.frameId}\n`; yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`;
yamlText += `Hex Data: ${transfer.hexData}\n`; yamlText += `${t('bus.detail_hex_data')} ${transfer.hexData}\n`;
yamlText += `Source Node: ${transfer.sourceNodeId}\n`; yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`;
yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`; yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`;
} }
setMessageYaml(yamlText); setMessageYaml(yamlText);
@@ -139,7 +141,7 @@ const BusMonitor = () => {
}; };
const exportToCSV = () => { 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 = [ const csvRows = [
headers.join(','), headers.join(','),
...transfers.map(transfer => { ...transfers.map(transfer => {
@@ -182,7 +184,7 @@ const BusMonitor = () => {
<AppBar position="static" color="primary"> <AppBar position="static" color="primary">
<Toolbar variant="dense"> <Toolbar variant="dense">
<Typography variant="h6" sx={{ flexGrow: 1 }}> <Typography variant="h6" sx={{ flexGrow: 1 }}>
Bus Monitor {t('bus.title')}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<FormControlLabel <FormControlLabel
@@ -193,7 +195,7 @@ const BusMonitor = () => {
size="small" size="small"
/> />
} }
label={<Typography variant="body2">Auto Scroll</Typography>} label={<Typography variant="body2">{t('bus.auto_scroll')}</Typography>}
labelPlacement="start" labelPlacement="start"
/> />
<IconButton <IconButton
@@ -218,7 +220,7 @@ const BusMonitor = () => {
variant="outlined" variant="outlined"
sx={{ ml: 1 }} sx={{ ml: 1 }}
> >
Export {t('bus.export')}
</Button> </Button>
</Box> </Box>
</Toolbar> </Toolbar>
@@ -232,13 +234,13 @@ const BusMonitor = () => {
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Dir</TableCell> <TableCell>{t('bus.col_dir')}</TableCell>
<TableCell>Time</TableCell> <TableCell>{t('bus.col_time')}</TableCell>
<TableCell>CAN ID</TableCell> <TableCell>{t('bus.col_can_id')}</TableCell>
<TableCell>Hex Data</TableCell> <TableCell>{t('bus.col_hex_data')}</TableCell>
<TableCell>Src</TableCell> <TableCell>{t('bus.col_src')}</TableCell>
<TableCell>Dst</TableCell> <TableCell>{t('bus.col_dst')}</TableCell>
<TableCell>Data Type</TableCell> <TableCell>{t('bus.col_data_type')}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
@@ -297,7 +299,7 @@ const BusMonitor = () => {
}} }}
> >
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
Showing {transfers.length} of max {maxTransfers} transfers {t('bus.showing', { count: transfers.length, max: maxTransfers })}
</Typography> </Typography>
{isPaused && ( {isPaused && (
<Typography <Typography
@@ -310,7 +312,7 @@ const BusMonitor = () => {
gap: '4px' gap: '4px'
}} }}
> >
<PauseIcon fontSize="small" /> PAUSED <PauseIcon fontSize="small" /> {t('bus.paused')}
</Typography> </Typography>
)} )}
</Box> </Box>
@@ -322,7 +324,7 @@ const BusMonitor = () => {
fullWidth fullWidth
> >
<DialogTitle> <DialogTitle>
Message Details {t('bus.message_details')}
{selectedTransfer && ( {selectedTransfer && (
<Typography variant="subtitle2" color="textSecondary"> <Typography variant="subtitle2" color="textSecondary">
{selectedTransfer.dataType} - {selectedTransfer.frameId} {selectedTransfer.dataType} - {selectedTransfer.frameId}
@@ -347,7 +349,7 @@ const BusMonitor = () => {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleCloseDetails} color="primary"> <Button onClick={handleCloseDetails} color="primary">
Close {t('bus.close')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
+6 -4
View File
@@ -1,21 +1,23 @@
import React from 'react'; import React from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material';
import { useTranslation } from './i18n/LanguageContext';
const ConfirmRestartModal = ({ open, onClose, onConfirm }) => { const ConfirmRestartModal = ({ open, onClose, onConfirm }) => {
const { t } = useTranslation();
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>
<DialogTitle>Confirm Restart</DialogTitle> <DialogTitle>{t('confirm.title')}</DialogTitle>
<DialogContent> <DialogContent>
<Typography variant="body2"> <Typography variant="body2">
Are you sure you want to restart the node? {t('confirm.message')}
</Typography> </Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} color="secondary"> <Button onClick={onClose} color="secondary">
Cancel {t('confirm.cancel')}
</Button> </Button>
<Button onClick={onConfirm} color="primary"> <Button onClick={onConfirm} color="primary">
Confirm {t('confirm.confirm')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
+46 -44
View File
@@ -11,6 +11,7 @@ import CloseIcon from '@mui/icons-material/Close';
import Visibility from '@mui/icons-material/Visibility'; import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff'; import VisibilityOff from '@mui/icons-material/VisibilityOff';
import WebSerial from './web_serial'; import WebSerial from './web_serial';
import { useTranslation } from './i18n/LanguageContext';
// Add this constant at the top of your file, outside the component // Add this constant at the top of your file, outside the component
const USB_DEVICE_NAMES = { const USB_DEVICE_NAMES = {
@@ -115,10 +116,11 @@ const ConnectionSettingsModal = ({
// Add this state variable to track connection attempts in progress // Add this state variable to track connection attempts in progress
const [connectionInProgress, setConnectionInProgress] = useState(false); const [connectionInProgress, setConnectionInProgress] = useState(false);
const { t } = useTranslation();
// Create a function to identify and index duplicate devices // Create a function to identify and index duplicate devices
const getPortDisplayName = (port, allPorts) => { 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 // Try to extract the most user-friendly name possible
if (port.info && port.info.product) { if (port.info && port.info.product) {
@@ -196,7 +198,7 @@ const ConnectionSettingsModal = ({
} }
// Fallback for ports without specific info // Fallback for ports without specific info
return "Serial Port"; return t('conn.serial_port');
}; };
// Moved from App.js - Lists available ports // Moved from App.js - Lists available ports
@@ -249,7 +251,7 @@ const ConnectionSettingsModal = ({
setActiveConnection(null); setActiveConnection(null);
setActiveSerialProtocol(null); setActiveSerialProtocol(null);
onConnectionStatusChange(false); onConnectionStatusChange(false);
showMessage('Serial connection closed', 'info'); showMessage(t('conn.serial_closed'), 'info');
// Clear the forwarding interval // Clear the forwarding interval
if (forwardingInterval) { if (forwardingInterval) {
@@ -293,12 +295,12 @@ const ConnectionSettingsModal = ({
setActiveSerialProtocol(serialProtocol); setActiveSerialProtocol(serialProtocol);
setConnectionInProgress(false); setConnectionInProgress(false);
onConnectionStatusChange(true); 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) => { window.mavlinkSession.addWebSerialErrorHandler((error) => {
console.error('Serial connection error:', 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 // Reset in-progress state on error
setConnectionInProgress(false); setConnectionInProgress(false);
}); });
@@ -306,7 +308,7 @@ const ConnectionSettingsModal = ({
window.mavlinkSession.webSerialConnect(); window.mavlinkSession.webSerialConnect();
} catch (error) { } catch (error) {
console.error('Serial connection error:', 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 // Reset in-progress state on error
setConnectionInProgress(false); setConnectionInProgress(false);
} }
@@ -317,7 +319,7 @@ const ConnectionSettingsModal = ({
} }
} catch (error) { } catch (error) {
console.error('Error with serial connection:', 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 // Reset in-progress state on any error
setConnectionInProgress(false); setConnectionInProgress(false);
} }
@@ -327,7 +329,7 @@ const ConnectionSettingsModal = ({
const validateIpAddress = (input) => { const validateIpAddress = (input) => {
// Check if empty // Check if empty
if (!input) { if (!input) {
return 'IP address is required'; return t('conn.ip_required');
} }
// Allow "localhost" // Allow "localhost"
@@ -342,7 +344,7 @@ const ConnectionSettingsModal = ({
for (const octet of octets) { for (const octet of octets) {
const num = parseInt(octet, 10); const num = parseInt(octet, 10);
if (isNaN(num) || num < 0 || num > 255 || octet !== num.toString()) { 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 ''; return '';
@@ -356,17 +358,17 @@ const ConnectionSettingsModal = ({
return ''; return '';
} }
return 'Invalid IP address or hostname'; return t('conn.ip_invalid');
}; };
const validatePort = (input) => { const validatePort = (input) => {
if (!input) { if (!input) {
return 'Port is required'; return t('conn.port_required');
} }
const port = parseInt(input, 10); const port = parseInt(input, 10);
if (isNaN(port) || port < 1 || port > 65535) { if (isNaN(port) || port < 1 || port > 65535) {
return 'Port must be between 1-65535'; return t('conn.port_range');
} }
return ''; return '';
@@ -375,7 +377,7 @@ const ConnectionSettingsModal = ({
// Add validation for nodeId // Add validation for nodeId
const validateNodeId = (value) => { const validateNodeId = (value) => {
const id = parseInt(value, 10); 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 // Update handler to propagate changes to parent
@@ -417,7 +419,7 @@ const ConnectionSettingsModal = ({
setActiveConnection(null); setActiveConnection(null);
setActiveSerialProtocol(null); setActiveSerialProtocol(null);
onConnectionStatusChange(false); onConnectionStatusChange(false);
showMessage('WebSocket connection closed', 'info'); showMessage(t('conn.ws_closed'), 'info');
if (forwardingInterval) { if (forwardingInterval) {
clearInterval(forwardingInterval); clearInterval(forwardingInterval);
setForwardingInterval(null); setForwardingInterval(null);
@@ -462,7 +464,7 @@ const ConnectionSettingsModal = ({
setActiveSerialProtocol(null); setActiveSerialProtocol(null);
onConnectionStatusChange(true); onConnectionStatusChange(true);
setConnectionInProgress(false); setConnectionInProgress(false);
showMessage('WebSocket connection established', 'success'); showMessage(t('conn.ws_connected'), 'success');
}); });
window.mavlinkSession.addWebSocketErrorHandler((error) => { window.mavlinkSession.addWebSocketErrorHandler((error) => {
@@ -474,11 +476,11 @@ const ConnectionSettingsModal = ({
setActiveConnection(null); setActiveConnection(null);
onConnectionStatusChange(false); onConnectionStatusChange(false);
setConnectionInProgress(false); setConnectionInProgress(false);
let errorMsg = 'Connection failed'; let errorMsg = t('conn.ws_failed');
if (error && error.message) { if (error && error.message) {
errorMsg = `Connection failed: ${error.message}`; errorMsg = t('conn.ws_failed_detail', { error: error.message });
} else if (typeof error === 'string') { } else if (typeof error === 'string') {
errorMsg = `Connection failed: ${error}`; errorMsg = t('conn.ws_failed_detail', { error: error });
} }
showMessage(errorMsg, 'error'); showMessage(errorMsg, 'error');
}); });
@@ -490,7 +492,7 @@ const ConnectionSettingsModal = ({
} }
} catch (error) { } catch (error) {
console.error('Error with WebSocket connection:', 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); setConnectionInProgress(false);
} }
}; };
@@ -521,11 +523,11 @@ const ConnectionSettingsModal = ({
> >
<DialogTitle> <DialogTitle>
<Box display="flex" alignItems="center" justifyContent="space-between"> <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}> <Box display="flex" alignItems="center" gap={1}>
{activeConnection && ( {activeConnection && (
<Chip <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" color="success"
size="small" size="small"
variant="outlined" variant="outlined"
@@ -555,21 +557,21 @@ const ConnectionSettingsModal = ({
width: '100%', width: '100%',
bgcolor: activeConnection === 'serial' ? 'rgba(0, 200, 83, 0.1)' : 'inherit' 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 */} <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> {/* Reduce gap */}
{/* Port and Baud Selection in same row */} {/* Port and Baud Selection in same row */}
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
{/* Port Selection - takes more space */} {/* Port Selection - takes more space */}
<Box sx={{ flex: 3 }}> <Box sx={{ flex: 3 }}>
<FormControl fullWidth size="small" disabled={activeConnection !== null}> <FormControl fullWidth size="small" disabled={activeConnection !== null}>
<InputLabel>Port</InputLabel> <InputLabel>{t('conn.port')}</InputLabel>
<Select <Select
value={selectedPort || ''} value={selectedPort || ''}
onChange={(e) => setSelectedPort(e.target.value)} onChange={(e) => setSelectedPort(e.target.value)}
label="Port" label={t('conn.port')}
> >
{ports.length === 0 ? ( {ports.length === 0 ? (
<MenuItem value="" disabled>No ports available</MenuItem> <MenuItem value="" disabled>{t('conn.no_ports')}</MenuItem>
) : ( ) : (
ports.map((port, index) => ( ports.map((port, index) => (
<MenuItem key={index} value={port}> <MenuItem key={index} value={port}>
@@ -584,11 +586,11 @@ const ConnectionSettingsModal = ({
{/* Baud Rate Selection - takes less space */} {/* Baud Rate Selection - takes less space */}
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<FormControl fullWidth size="small" disabled={activeConnection !== null}> <FormControl fullWidth size="small" disabled={activeConnection !== null}>
<InputLabel>Baud Rate</InputLabel> <InputLabel>{t('conn.baud_rate')}</InputLabel>
<Select <Select
value={baudRate} value={baudRate}
onChange={(e) => setBaudRate(e.target.value)} onChange={(e) => setBaudRate(e.target.value)}
label="Baud Rate" label={t('conn.baud_rate')}
> >
{BAUD_RATES.map((rate) => ( {BAUD_RATES.map((rate) => (
<MenuItem key={rate} value={rate}> <MenuItem key={rate} value={rate}>
@@ -601,14 +603,14 @@ const ConnectionSettingsModal = ({
</Box> </Box>
<FormControl fullWidth size="small" disabled={activeConnection !== null}> <FormControl fullWidth size="small" disabled={activeConnection !== null}>
<InputLabel>Serial Protocol</InputLabel> <InputLabel>{t('conn.serial_protocol')}</InputLabel>
<Select <Select
value={serialProtocol} value={serialProtocol}
onChange={(e) => setSerialProtocol(e.target.value)} 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.MAVLINK}>{t('conn.protocol_mavlink')}</MenuItem>
<MenuItem value={SERIAL_PROTOCOLS.SLCAN}>SLCAN / LAWICEL</MenuItem> <MenuItem value={SERIAL_PROTOCOLS.SLCAN}>{t('conn.protocol_slcan')}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
@@ -621,7 +623,7 @@ const ConnectionSettingsModal = ({
disabled={activeConnection !== null} disabled={activeConnection !== null}
size="small" size="small"
> >
Refresh {t('conn.refresh')}
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
@@ -630,7 +632,7 @@ const ConnectionSettingsModal = ({
disabled={activeConnection !== null} disabled={activeConnection !== null}
size="small" size="small"
> >
Request {t('conn.request')}
</Button> </Button>
<Box sx={{ flexGrow: 1 }} /> <Box sx={{ flexGrow: 1 }} />
<Button <Button
@@ -644,8 +646,8 @@ const ConnectionSettingsModal = ({
} }
size="small" size="small"
> >
{activeConnection === 'serial' ? 'Disconnect' : {activeConnection === 'serial' ? t('conn.disconnect') :
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'} connectionInProgress && !activeConnection ? t('conn.connecting') : t('conn.connect')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -657,11 +659,11 @@ const ConnectionSettingsModal = ({
width: '100%', width: '100%',
bgcolor: activeConnection === 'websocket' ? 'rgba(0, 200, 83, 0.1)' : 'inherit' 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', flexDirection: 'column', gap: 1.5 }}> {/* Reduce gap */}
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{ display: 'flex', gap: 2 }}>
<TextField <TextField
label="Host/IP Address" label={t('conn.host')}
value={wsHost} value={wsHost}
onChange={handleWsHostChange} onChange={handleWsHostChange}
disabled={activeConnection !== null} disabled={activeConnection !== null}
@@ -671,7 +673,7 @@ const ConnectionSettingsModal = ({
helperText={hostError} helperText={hostError}
/> />
<TextField <TextField
label="Port" label={t('conn.ws_port')}
value={wsPort} value={wsPort}
onChange={handleWsPortChange} onChange={handleWsPortChange}
type="number" type="number"
@@ -698,8 +700,8 @@ const ConnectionSettingsModal = ({
} }
size="small" size="small"
> >
{activeConnection === 'websocket' ? 'Disconnect' : {activeConnection === 'websocket' ? t('conn.disconnect') :
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'} connectionInProgress && !activeConnection ? t('conn.connecting') : t('conn.connect')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -712,7 +714,7 @@ const ConnectionSettingsModal = ({
<Box> <Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<TextField <TextField
label="Node ID" label={t('conn.node_id')}
value={nodeId} value={nodeId}
onChange={handleNodeIdChange} onChange={handleNodeIdChange}
disabled={activeConnection !== null} disabled={activeConnection !== null}
@@ -726,18 +728,18 @@ const ConnectionSettingsModal = ({
helperText={validateNodeId(nodeId)} helperText={validateNodeId(nodeId)}
/> />
<TextField <TextField
label="Mavlink Signing" label={t('conn.signing')}
value={mavlinkSigning} value={mavlinkSigning}
onChange={handleMavlinkSigningChange} onChange={handleMavlinkSigningChange}
disabled={activeConnection !== null} disabled={activeConnection !== null}
size="small" size="small"
placeholder="Secret Key" placeholder={t('conn.secret_key')}
type={showMavlinkSigning ? 'text' : 'password'} type={showMavlinkSigning ? 'text' : 'password'}
sx={{ flex: 1 }} sx={{ flex: 1 }}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<IconButton <IconButton
aria-label={showMavlinkSigning ? 'Hide secret' : 'Show secret'} aria-label={showMavlinkSigning ? t('conn.hide_secret') : t('conn.show_secret')}
onClick={handleToggleMavlinkSigning} onClick={handleToggleMavlinkSigning}
edge="end" edge="end"
size="small" size="small"
+27 -25
View File
@@ -13,6 +13,7 @@ import StopIcon from '@mui/icons-material/Stop';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import DynamicNodeIdServer from './services/DynamicNodeIdServer'; import DynamicNodeIdServer from './services/DynamicNodeIdServer';
import { useTranslation } from './i18n/LanguageContext';
const DNAServerModal = ({ open, onClose, showMessage }) => { const DNAServerModal = ({ open, onClose, showMessage }) => {
const [serverEnabled, setServerEnabled] = useState(false); const [serverEnabled, setServerEnabled] = useState(false);
@@ -23,6 +24,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
const [server, setServer] = useState(null); const [server, setServer] = useState(null);
const [operationInProgress, setOperationInProgress] = useState(false); const [operationInProgress, setOperationInProgress] = useState(false);
const [refreshInterval, setRefreshInterval] = useState(null); const [refreshInterval, setRefreshInterval] = useState(null);
const { t } = useTranslation();
const handleAllocationUpdate = () => { const handleAllocationUpdate = () => {
console.log("Allocation update detected, refreshing list"); console.log("Allocation update detected, refreshing list");
@@ -84,9 +86,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}, 1000); // Check every 5 seconds }, 1000); // Check every 5 seconds
setRefreshInterval(interval); setRefreshInterval(interval);
showMessage("DNA server started successfully", "success"); showMessage(t('dna.started'), "success");
} else { } else {
showMessage("Failed to start DNA server", "error"); showMessage(t('dna.failed_start'), "error");
} }
} else { } else {
// Stop the server // Stop the server
@@ -99,11 +101,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
setRefreshInterval(null); setRefreshInterval(null);
} }
showMessage("DNA server stopped", "info"); showMessage(t('dna.stopped'), "info");
} }
} catch (error) { } catch (error) {
console.error("Error toggling DNA server:", error); console.error("Error toggling DNA server:", error);
showMessage(`Error: ${error.message}`, "error"); showMessage(t('dna.error', { error: error.message }), "error");
} finally { } finally {
setOperationInProgress(false); setOperationInProgress(false);
} }
@@ -124,7 +126,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}; };
const validateNodeIdRange = () => { const validateNodeIdRange = () => {
return minNodeId >= maxNodeId ? "Min ID must be less than Max ID" : ""; return minNodeId >= maxNodeId ? t('dna.invalid_range') : "";
}; };
const handleDeleteAllocation = (nodeId) => { const handleDeleteAllocation = (nodeId) => {
@@ -133,15 +135,15 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
const success = server.deleteAllocation(nodeId); const success = server.deleteAllocation(nodeId);
if (success) { if (success) {
fetchCurrentAllocations(); fetchCurrentAllocations();
showMessage(`Node ID ${nodeId} allocation revoked`, "info"); showMessage(t('dna.revoked', { id: nodeId }), "info");
} else { } else {
showMessage(`Failed to revoke allocation for node ID ${nodeId}`, "error"); showMessage(t('dna.revoke_failed', { id: nodeId }), "error");
} }
}; };
const handleRefreshAllocations = () => { const handleRefreshAllocations = () => {
fetchCurrentAllocations(); fetchCurrentAllocations();
showMessage("Allocations refreshed", "info"); showMessage(t('dna.refreshed'), "info");
}; };
// Modify the modal close handler to not stop the server // Modify the modal close handler to not stop the server
@@ -208,11 +210,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
> >
<DialogTitle> <DialogTitle>
<Box display="flex" alignItems="center" justifyContent="space-between"> <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}> <Box display="flex" alignItems="center" gap={1}>
{serverEnabled && ( {serverEnabled && (
<Chip <Chip
label="Server Active" label={t('dna.active')}
color="success" color="success"
size="small" size="small"
variant="outlined" variant="outlined"
@@ -242,7 +244,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
bgcolor: serverEnabled ? 'rgba(0, 200, 83, 0.1)' : 'inherit' bgcolor: serverEnabled ? 'rgba(0, 200, 83, 0.1)' : 'inherit'
}}> }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}> <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 <Button
variant="contained" variant="contained"
startIcon={serverEnabled ? <StopIcon /> : <PlayArrowIcon />} startIcon={serverEnabled ? <StopIcon /> : <PlayArrowIcon />}
@@ -251,8 +253,8 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
disabled={operationInProgress} disabled={operationInProgress}
size="small" size="small"
> >
{operationInProgress ? "Processing..." : {operationInProgress ? t('dna.processing') :
serverEnabled ? "Stop" : "Start"} serverEnabled ? t('dna.stop') : t('dna.start')}
</Button> </Button>
</Box> </Box>
@@ -262,7 +264,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
{/* Min Node ID */} {/* Min Node ID */}
<Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}> <Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}>
<TextField <TextField
label="Min Node ID" label={t('dna.min_node_id')}
type="number" type="number"
value={minNodeId} value={minNodeId}
onChange={handleMinNodeIdChange} onChange={handleMinNodeIdChange}
@@ -273,14 +275,14 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}} }}
size="small" size="small"
error={minNodeId >= maxNodeId} error={minNodeId >= maxNodeId}
helperText={minNodeId >= maxNodeId ? "Must be < Max" : ""} helperText={minNodeId >= maxNodeId ? t('dna.must_lt_max') : ""}
/> />
</Box> </Box>
{/* Max Node ID */} {/* Max Node ID */}
<Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}> <Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}>
<TextField <TextField
label="Max Node ID" label={t('dna.max_node_id')}
type="number" type="number"
value={maxNodeId} value={maxNodeId}
onChange={handleMaxNodeIdChange} onChange={handleMaxNodeIdChange}
@@ -291,7 +293,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}} }}
size="small" size="small"
error={minNodeId >= maxNodeId} error={minNodeId >= maxNodeId}
helperText={minNodeId >= maxNodeId ? "Must be > Min" : ""} helperText={minNodeId >= maxNodeId ? t('dna.must_gt_min') : ""}
/> />
</Box> </Box>
@@ -315,8 +317,8 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
} }
label={ label={
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ mr: 0.5 }}>Persist Allocations</Typography> <Typography variant="body2" sx={{ mr: 0.5 }}>{t('dna.persist')}</Typography>
<Tooltip title="When enabled, node ID allocations are stored and restored when the server restarts"> <Tooltip title={t('dna.persist_tooltip')}>
<HelpOutlineIcon fontSize="small" /> <HelpOutlineIcon fontSize="small" />
</Tooltip> </Tooltip>
</Box> </Box>
@@ -339,9 +341,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
variant="body1" // Using body1 for smaller font size instead of subtitle1 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 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> </Typography>
<Tooltip title="Refresh allocation list"> <Tooltip title={t('dna.refresh_tooltip')}>
<IconButton <IconButton
onClick={handleRefreshAllocations} onClick={handleRefreshAllocations}
size="small" size="small"
@@ -356,7 +358,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
{allocatedNodes.length === 0 ? ( {allocatedNodes.length === 0 ? (
<Box sx={{ p: 2, textAlign: 'center' }}> <Box sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
No node IDs allocated {t('dna.no_allocations')}
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
@@ -374,9 +376,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
<Table size="small" stickyHeader> <Table size="small" stickyHeader>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>NID</TableCell> <TableCell>{t('dna.col_nid')}</TableCell>
<TableCell>UUID</TableCell> <TableCell>{t('dna.col_uuid')}</TableCell>
<TableCell align="right" width="60px">Action</TableCell> <TableCell align="right" width="60px">{t('dna.col_action')}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
+39 -37
View File
@@ -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 StopIcon from '@mui/icons-material/Stop'; // Add this import for the stop button
import MusicNoteIcon from '@mui/icons-material/MusicNote'; import MusicNoteIcon from '@mui/icons-material/MusicNote';
import AM32_Rtttl from './am32_rtttl'; // Updated import to match class name import AM32_Rtttl from './am32_rtttl'; // Updated import to match class name
import { useTranslation } from './i18n/LanguageContext';
const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => { const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
// Add a new state for tracking whether a tune is currently playing // 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 [errorMessage, setErrorMessage] = useState(''); // For validation error messages
const [isValid, setIsValid] = useState(true); // Add a new state variable to track validation status 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 [paramName, setParamName] = useState(""); // Add paramName to the component state
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const localNode = window.localNode; const localNode = window.localNode;
@@ -84,7 +86,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
if (!isValidFormat) { if (!isValidFormat) {
// Warn user but continue with a default tune // 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 // Continue with a minimal valid RTTTL string
const tuneToParse = "Empty:d=4,o=5,b=120:"; const tuneToParse = "Empty:d=4,o=5,b=120:";
result = AM32_Rtttl.to_am32_startup_melody(tuneToParse); 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)); console.log("Binary array values:", Array.from(result.data).slice(0, 30));
} catch (err) { } catch (err) {
console.error("Error converting RTTTL to binary:", 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 // Provide an empty binary string (all zeros) as fallback
const emptyArray = new Uint8Array(128); const emptyArray = new Uint8Array(128);
valueToSave = String.fromCharCode.apply(null, emptyArray); valueToSave = String.fromCharCode.apply(null, emptyArray);
@@ -126,7 +128,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
try { try {
// First validate that the tune has the basic RTTTL format (name:defaults:notes) // First validate that the tune has the basic RTTTL format (name:defaults:notes)
if (!tuneToPlay || !tuneToPlay.includes(':') || tuneToPlay.split(':').length !== 3) { if (!tuneToPlay || !tuneToPlay.includes(':') || tuneToPlay.split(':').length !== 3) {
setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes'); setErrorMessage(t('edit.rtttl_invalid'));
return; return;
} }
@@ -149,7 +151,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
} catch (err) { } catch (err) {
console.error("Error playing tune:", 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); setIsPlaying(false);
} }
}; };
@@ -224,7 +226,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
const isValidFormat = stringValue.includes(':') && stringValue.split(':').length === 3; const isValidFormat = stringValue.includes(':') && stringValue.split(':').length === 3;
if (!isValidFormat) { if (!isValidFormat) {
setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes'); setErrorMessage(t('edit.rtttl_invalid'));
return false; return false;
} }
@@ -275,7 +277,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
// Validate against min/max if they exist // Validate against min/max if they exist
if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) { if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) {
setIsValid(false); 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 { } else {
setIsValid(true); setIsValid(true);
setErrorMessage(''); setErrorMessage('');
@@ -322,7 +324,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
// Validate against min/max if they exist // Validate against min/max if they exist
if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) { if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) {
setIsValid(false); 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 { } else {
setIsValid(true); setIsValid(true);
setErrorMessage(''); 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: 'column', gap: 2, mt: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}> <Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}>
<FormControl fullWidth margin="dense"> <FormControl fullWidth margin="dense">
<InputLabel id="rtttl-preset-label">Select Preset Tune</InputLabel> <InputLabel id="rtttl-preset-label">{t('edit.select_preset')}</InputLabel>
<Select <Select
labelId="rtttl-preset-label" labelId="rtttl-preset-label"
value={selectedPreset} value={selectedPreset}
onChange={(e) => setSelectedPreset(e.target.value)} onChange={(e) => setSelectedPreset(e.target.value)}
> >
<MenuItem value="" disabled> <MenuItem value="" disabled>
<em>Choose a preset tune</em> <em>{t('edit.choose_preset')}</em>
</MenuItem> </MenuItem>
{Object.entries(rtttlPresets).map(([name, tune]) => ( {Object.entries(rtttlPresets).map(([name, tune]) => (
<MenuItem key={name} value={tune}> <MenuItem key={name} value={tune}>
@@ -363,24 +365,24 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
disabled={!selectedPreset} disabled={!selectedPreset}
sx={{ mb: 1 }} sx={{ mb: 1 }}
> >
Apply {t('edit.apply')}
</Button> </Button>
</Box> </Box>
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative' }}>
<TextField <TextField
label="RTTTL Tune" label={t('edit.rtttl_tune')}
value={value || ""} value={value || ""}
onChange={(e) => handleValueChange(e.target.value)} onChange={(e) => handleValueChange(e.target.value)}
fullWidth fullWidth
margin="dense" margin="dense"
multiline multiline
rows={3} rows={3}
placeholder="Format: name:d=duration,o=octave,b=bpm:notes" placeholder={t('edit.rtttl_placeholder')}
error={!isValid && value !== ''} error={!isValid && value !== ''}
// Remove helperText to avoid layout issues // 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 <IconButton
size="small" size="small"
color={isPlaying ? "secondary" : "primary"} color={isPlaying ? "secondary" : "primary"}
@@ -401,21 +403,21 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
{/* Add a simple instruction text below the field */} {/* Add a simple instruction text below the field */}
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}> <Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
Enter RTTTL format tune or select a preset {t('edit.rtttl_instruction')}
</Typography> </Typography>
<Divider /> <Divider />
<Box sx={{ bgcolor: 'action.hover', p: 1, borderRadius: 1 }}> <Box sx={{ bgcolor: 'action.hover', p: 1, borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}> <Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>
RTTTL Format Guide {t('edit.rtttl_guide_title')}
</Typography> </Typography>
<Box sx={{ mt: 0.5 }}> <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"> {t('edit.rtttl_guide_duration')}</Typography>
<Typography variant="caption" display="block"> o=octave (4-7 where 5 is default)</Typography> <Typography variant="caption" display="block"> {t('edit.rtttl_guide_octave')}</Typography>
<Typography variant="caption" display="block"> b=tempo (beats per minute)</Typography> <Typography variant="caption" display="block"> {t('edit.rtttl_guide_tempo')}</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"> {t('edit.rtttl_guide_notes')}</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_example')}</Typography>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@@ -431,7 +433,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
if (param.fields.value.msg.fields.boolean_value) { if (param.fields.value.msg.fields.boolean_value) {
return ( return (
<Box sx={{ display: 'flex', alignItems: 'center'}}> <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 <Checkbox
checked={value === 1 || value === true} checked={value === 1 || value === true}
onChange={(e) => setValue(e.target.checked ? 1 : 0)} onChange={(e) => setValue(e.target.checked ? 1 : 0)}
@@ -462,7 +464,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
return ( return (
<TextField <TextField
label="New Value" label={t('edit.new_value')}
value={value} value={value}
type="number" type="number"
inputProps={{ inputProps={{
@@ -473,7 +475,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
fullWidth fullWidth
margin="dense" margin="dense"
helperText={isOutOfBounds ? helperText={isOutOfBounds ?
`Value must be between ${min !== "" ? min : '-∞'} and ${max !== "" ? max : '∞'}` : t('edit.value_range', { min: min !== "" ? min : '-∞', max: max !== "" ? max : '∞' }) :
null null
} }
/> />
@@ -506,8 +508,8 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
const renderParamNameField = (name) => ( const renderParamNameField = (name) => (
<TextField <TextField
label="Parameter Name" label={t('edit.param_name')}
value={name || "Unknown"} value={name || t('edit.unknown')}
InputProps={{ InputProps={{
readOnly: true, readOnly: true,
}} }}
@@ -542,7 +544,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
/> />
) : ( ) : (
<Typography variant="body2"> <Typography variant="body2">
{value !== undefined && value !== "" && value !== null ? value : "Unknown"} {value !== undefined && value !== "" && value !== null ? value : t('edit.unknown')}
</Typography> </Typography>
)} )}
</Box> </Box>
@@ -554,7 +556,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
onClose={onClose} onClose={onClose}
sx={{ '& .MuiDialog-paper': { minWidth: isRTTTLEditor ? '600px' : '400px' } }} sx={{ '& .MuiDialog-paper': { minWidth: isRTTTLEditor ? '600px' : '400px' } }}
> >
<DialogTitle>Edit Parameter</DialogTitle> <DialogTitle>{t('edit.title')}</DialogTitle>
<DialogContent> <DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}> <Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}>
@@ -565,7 +567,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
{isString && !isRTTTLEditor && ( {isString && !isRTTTLEditor && (
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
<TextField <TextField
label="String Value" label={t('edit.string_value')}
value={value || ""} value={value || ""}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
fullWidth fullWidth
@@ -580,7 +582,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
{paramName === "STARTUP_TUNE" && isString ? ( {paramName === "STARTUP_TUNE" && isString ? (
renderInfoField("Current RTTTL", (() => { renderInfoField(t('edit.current_rtttl'), (() => {
try { try {
// Get the binary string value // Get the binary string value
const binaryString = paramValueField.toString(); const binaryString = paramValueField.toString();
@@ -595,27 +597,27 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
return AM32_Rtttl.from_am32_startup_melody(binaryData, "Tune"); return AM32_Rtttl.from_am32_startup_melody(binaryData, "Tune");
} catch (err) { } catch (err) {
console.error("Error converting binary data to RTTTL:", 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 })(), true) // Pass true to indicate this is an RTTTL value
) : ( ) : (
renderInfoField( renderInfoField(
"Current Value", t('edit.current_value'),
isBoolean isBoolean
? (paramValueField.value ? "True" : "False") ? (paramValueField.value ? t('edit.true') : t('edit.false'))
: isString : isString
? paramValueField.toString() ? paramValueField.toString()
: paramValueField.value : paramValueField.value
) )
)} )}
{/* Only show default value when not STARTUP_TUNE */} {/* 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> </Box>
{!isBoolean && !isString && !isRTTTLEditor && ( {!isBoolean && !isString && !isRTTTLEditor && (
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
{renderInfoField("Min Value", paramMinValue)} {renderInfoField(t('edit.min_value'), paramMinValue)}
{renderInfoField("Max Value", paramMaxValue)} {renderInfoField(t('edit.max_value'), paramMaxValue)}
</Box> </Box>
)} )}
{errorMessage && ( {errorMessage && (
@@ -629,14 +631,14 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose} color="secondary"> <Button onClick={onClose} color="secondary">
Cancel {t('edit.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSave} onClick={handleSave}
color="primary" color="primary"
disabled={!isValid} disabled={!isValid}
> >
Save {t('edit.save')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
+17 -15
View File
@@ -4,6 +4,7 @@ import PanToolIcon from '@mui/icons-material/PanTool';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause'; import PauseIcon from '@mui/icons-material/Pause';
import dronecan from './dronecan'; import dronecan from './dronecan';
import { useTranslation } from './i18n/LanguageContext';
const commandValueType = new dronecan.DSDL.uavcan_equipment_esc_RawCommand().fields.cmd.value_type; const commandValueType = new dronecan.DSDL.uavcan_equipment_esc_RawCommand().fields.cmd.value_type;
const CMD_MAX = Number(commandValueType.value_range.max); const CMD_MAX = Number(commandValueType.value_range.max);
@@ -27,6 +28,7 @@ const EscPanel = () => {
const [sendArming, setSendArming] = useState(false); const [sendArming, setSendArming] = useState(false);
const [broadcastRate, setBroadcastRate] = useState(10); // Changed default to 10 const [broadcastRate, setBroadcastRate] = useState(10); // Changed default to 10
const [isPaused, setIsPaused] = useState(false); // New state for pause toggle const [isPaused, setIsPaused] = useState(false); // New state for pause toggle
const { t } = useTranslation();
// Add toggle pause function // Add toggle pause function
const togglePause = () => { const togglePause = () => {
@@ -269,7 +271,7 @@ const EscPanel = () => {
<Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}> <Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mr: 1 }}> <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 <TextField
type="number" type="number"
size="small" size="small"
@@ -302,7 +304,7 @@ const EscPanel = () => {
fontSize: '0.6rem' fontSize: '0.6rem'
}} }}
> >
REMOVE PROPELLERS! {t('esc.remove_propellers')}
</Typography> </Typography>
</Box> </Box>
@@ -317,7 +319,7 @@ const EscPanel = () => {
sx={{ p: 0.5 }} sx={{ p: 0.5 }}
/> />
} }
label={<Typography variant="body2">Send Safety</Typography>} label={<Typography variant="body2">{t('esc.send_safety')}</Typography>}
labelPlacement="start" labelPlacement="start"
sx={{ ml: 0, mr: 1 }} sx={{ ml: 0, mr: 1 }}
/> />
@@ -330,14 +332,14 @@ const EscPanel = () => {
sx={{ p: 0.5 }} sx={{ p: 0.5 }}
/> />
} }
label={<Typography variant="body2">Send Arming</Typography>} label={<Typography variant="body2">{t('esc.send_arming')}</Typography>}
labelPlacement="start" labelPlacement="start"
sx={{ ml: 0, mr: 2 }} sx={{ ml: 0, mr: 2 }}
/> />
{/* Broadcast Rate moved to the right */} {/* Broadcast Rate moved to the right */}
<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('esc.broadcast_rate')}</Typography>
<TextField <TextField
type="number" type="number"
size="small" size="small"
@@ -403,22 +405,22 @@ const EscPanel = () => {
flexGrow: 1, flexGrow: 1,
}}> }}>
<Box> <Box>
<Typography variant="body2" color="textSecondary">Index: {esc.esc_index}</Typography> <Typography variant="body2" color="textSecondary">{t('esc.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.error')} {esc.error_count !== null ? esc.error_count : t('esc.nc')}</Typography>
<Typography variant="body2" color="textSecondary"> <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>
<Typography variant="body2" color="textSecondary"> <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>
<Typography variant="body2" color="textSecondary"> <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>
<Typography variant="body2" color="textSecondary"> <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>
<Typography variant="body2" color="textSecondary"> <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> </Typography>
</Box> </Box>
@@ -446,7 +448,7 @@ const EscPanel = () => {
fullWidth fullWidth
size="small" size="small"
> >
Stop {t('esc.stop')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -493,7 +495,7 @@ const EscPanel = () => {
}}> }}>
<Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}> <Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
cmd: [{getScaledCommands(thrustValues).join(', ')}] {t('esc.cmd')} [{getScaledCommands(thrustValues).join(', ')}]
</Typography> </Typography>
</Box> </Box>
@@ -504,7 +506,7 @@ const EscPanel = () => {
startIcon={<PanToolIcon />} startIcon={<PanToolIcon />}
onClick={handleStopAll} onClick={handleStopAll}
> >
Stop All {t('esc.stop_all')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
+19 -17
View File
@@ -5,6 +5,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileUploadIcon from '@mui/icons-material/FileUpload';
import FileServer from './FileServer'; import FileServer from './FileServer';
import { useTranslation } from './i18n/LanguageContext';
const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => { const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
const [firmwareFile, setFirmwareFile] = useState(null); const [firmwareFile, setFirmwareFile] = useState(null);
@@ -12,6 +13,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
const [updateProgress, setUpdateProgress] = useState(0); const [updateProgress, setUpdateProgress] = useState(0);
const [updateStatus, setUpdateStatus] = useState(null); // 'idle', 'updating', 'success', 'error' const [updateStatus, setUpdateStatus] = useState(null); // 'idle', 'updating', 'success', 'error'
const [statusMessage, setStatusMessage] = useState(''); const [statusMessage, setStatusMessage] = useState('');
const { t } = useTranslation();
const handleFileChange = (event) => { const handleFileChange = (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
@@ -21,7 +23,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
const fileExtension = file.name.split('.').pop().toLowerCase(); const fileExtension = file.name.split('.').pop().toLowerCase();
if (fileExtension !== 'bin' && fileExtension !== 'hex') { if (fileExtension !== 'bin' && fileExtension !== 'hex') {
setUpdateStatus('error'); setUpdateStatus('error');
setStatusMessage('Invalid file type. Please select a .bin or .hex firmware file.'); setStatusMessage(t('fw.invalid_file'));
return; return;
} }
@@ -42,7 +44,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
.catch(error => { .catch(error => {
console.error('Error loading firmware:', error); console.error('Error loading firmware:', error);
setUpdateStatus('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) { if (!localNode) {
setUpdateStatus('error'); setUpdateStatus('error');
setStatusMessage('Local node not available'); setStatusMessage(t('fw.node_unavailable'));
return; return;
} }
// Update UI state // Update UI state
setUpdateStatus('updating'); setUpdateStatus('updating');
setStatusMessage('Starting firmware update...'); setStatusMessage(t('fw.starting'));
setUpdateProgress(0); setUpdateProgress(0);
// Register a progress callback with the FileServer // Register a progress callback with the FileServer
@@ -95,12 +97,12 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
setUpdateProgress(progress * 100); setUpdateProgress(progress * 100);
// Update status message // 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 // When complete
if (eof) { if (eof) {
setUpdateStatus('success'); setUpdateStatus('success');
setStatusMessage('Firmware update completed successfully!'); setStatusMessage(t('fw.success'));
// Clean up progress tracking // Clean up progress tracking
setTimeout(() => { setTimeout(() => {
@@ -123,7 +125,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
if (!msg || msg.fields.error.value > 0) { if (!msg || msg.fields.error.value > 0) {
// Handle update failure // Handle update failure
setUpdateStatus('error'); 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); FileServer.unregisterProgressCallback(firmwarePath);
} else { } else {
setUpdateStatus('updating'); setUpdateStatus('updating');
@@ -134,16 +136,16 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
} catch (error) { } catch (error) {
console.error('Error initiating firmware update:', error); console.error('Error initiating firmware update:', error);
setUpdateStatus('error'); setUpdateStatus('error');
setStatusMessage(`Failed to start update: ${error.message || 'Unknown error'}`); setStatusMessage(t('fw.start_failed', { error: error.message || t('edit.unknown') }));
} }
}; };
return ( return (
<Dialog open={open} onClose={updateStatus === 'updating' ? null : onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={updateStatus === 'updating' ? null : onClose} maxWidth="sm" fullWidth>
<DialogTitle>Firmware Update</DialogTitle> <DialogTitle>{t('fw.title')}</DialogTitle>
<DialogContent> <DialogContent>
<Typography variant="body2" gutterBottom> <Typography variant="body2" gutterBottom>
Please select the firmware file (.bin|.hex) to upload to node {targetNodeId}. {t('fw.select_hint', { id: targetNodeId })}
</Typography> </Typography>
<Button <Button
@@ -152,7 +154,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
startIcon={<FileUploadIcon />} startIcon={<FileUploadIcon />}
disabled={updateStatus === 'updating'} disabled={updateStatus === 'updating'}
> >
Select Firmware File {t('fw.select_file')}
<input <input
type="file" type="file"
hidden hidden
@@ -164,7 +166,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
{firmwareFile && ( {firmwareFile && (
<Typography variant="body2" sx={{ mt: 2 }}> <Typography variant="body2" sx={{ mt: 2 }}>
Selected File: {firmwareFile.name} Selected File: {firmwareFile.name}
{fileContent && ` (${fileContent.size} bytes)`} {fileContent && ` (${fileContent.size} ${t('fw.bytes', { defaultValue: 'bytes' })})`}
</Typography> </Typography>
)} )}
@@ -183,13 +185,13 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
{updateStatus === 'error' && ( {updateStatus === 'error' && (
<Alert severity="error" sx={{ mt: 2 }}> <Alert severity="error" sx={{ mt: 2 }}>
{statusMessage || 'An error occurred during the update.'} {statusMessage || t('fw.error_occurred')}
</Alert> </Alert>
)} )}
{updateStatus === 'success' && ( {updateStatus === 'success' && (
<Alert severity="success" sx={{ mt: 2 }}> <Alert severity="success" sx={{ mt: 2 }}>
{statusMessage || 'Firmware update completed successfully!'} {statusMessage || t('fw.success')}
</Alert> </Alert>
)} )}
</DialogContent> </DialogContent>
@@ -197,14 +199,14 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
{updateStatus !== 'updating' ? ( {updateStatus !== 'updating' ? (
<> <>
<Button onClick={onClose} color="secondary"> <Button onClick={onClose} color="secondary">
Cancel {t('fw.cancel')}
</Button> </Button>
<Button <Button
onClick={handleUpdate} onClick={handleUpdate}
color="primary" color="primary"
disabled={!firmwareFile || updateStatus === 'updating'} disabled={!firmwareFile || updateStatus === 'updating'}
> >
Update {t('fw.update')}
</Button> </Button>
</> </>
) : ( ) : (
@@ -213,7 +215,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
disabled={updateProgress < 100} disabled={updateProgress < 100}
onClick={onClose} onClick={onClose}
> >
{updateProgress < 100 ? 'Updating...' : 'Close'} {updateProgress < 100 ? t('fw.updating_ellipsis') : t('fw.close')}
</Button> </Button>
)} )}
</DialogActions> </DialogActions>
+8 -6
View File
@@ -3,10 +3,12 @@ import { TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper
import PauseIcon from '@mui/icons-material/Pause'; import PauseIcon from '@mui/icons-material/Pause';
import CleaningServicesIcon from '@mui/icons-material/CleaningServices'; import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { useTranslation } from './i18n/LanguageContext';
const NodeLogs = () => { const NodeLogs = () => {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [paused, setPaused] = useState(false); const [paused, setPaused] = useState(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const localNode = window.localNode; const localNode = window.localNode;
@@ -57,7 +59,7 @@ const NodeLogs = () => {
alignItems: 'center', alignItems: 'center',
height: 20 height: 20
}} margin={1}> }} 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 }}> <Box sx={{ display: 'flex', gap: 0.5 }}>
<IconButton <IconButton
sx={{ sx={{
@@ -90,11 +92,11 @@ const NodeLogs = () => {
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell sx={{ width: '5%' }}>NID</TableCell> <TableCell sx={{ width: '5%' }}>{t('logs.col_nid')}</TableCell>
<TableCell sx={{ width: '15%' }}>Time</TableCell> <TableCell sx={{ width: '15%' }}>{t('logs.col_time')}</TableCell>
<TableCell sx={{ width: '10%' }}>Level</TableCell> <TableCell sx={{ width: '10%' }}>{t('logs.col_level')}</TableCell>
<TableCell sx={{ width: '10%' }}>Source</TableCell> <TableCell sx={{ width: '10%' }}>{t('logs.col_source')}</TableCell>
<TableCell sx={{ width: '60%' }}>Text</TableCell> <TableCell sx={{ width: '60%' }}>{t('logs.col_text')}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
+22 -20
View File
@@ -11,6 +11,7 @@ import FileUploadIcon from '@mui/icons-material/FileUpload';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import ParamEditorSelector from './ParamEditors/ParamEditorSelector'; import ParamEditorSelector from './ParamEditors/ParamEditorSelector';
import AM32_Rtttl from './am32_rtttl'; import AM32_Rtttl from './am32_rtttl';
import { useTranslation } from './i18n/LanguageContext';
const OPCODE_SAVE = 0; const OPCODE_SAVE = 0;
const OPCODE_ERASE = 1; const OPCODE_ERASE = 1;
@@ -21,6 +22,7 @@ const NodeParam = ({ nodeId, nodes }) => {
const [paramsUpdateTimestamp, setParamsUpdateTimestamp] = useState(0); const [paramsUpdateTimestamp, setParamsUpdateTimestamp] = useState(0);
const [fetchingParams, setFetchingParams] = useState(false); const [fetchingParams, setFetchingParams] = useState(false);
const fetchTimeoutRef = useRef(null); const fetchTimeoutRef = useRef(null);
const { t } = useTranslation();
if (!nodeId) return null; if (!nodeId) return null;
const node = nodes[nodeId]; const node = nodes[nodeId];
@@ -97,9 +99,9 @@ const NodeParam = ({ nodeId, nodes }) => {
// Function to format boolean values visually // Function to format boolean values visually
const formatBooleanValue = (value) => { const formatBooleanValue = (value) => {
if (value === 'True') { 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') { } else if (value === 'False') {
return <Chip size="small" label="False" color="error" />; return <Chip size="small" label={t('param.false')} color="error" />;
} }
return value; return value;
}; };
@@ -214,15 +216,15 @@ const NodeParam = ({ nodeId, nodes }) => {
} else if (param.fields.value.msg.fields.boolean_value !== undefined) { } else if (param.fields.value.msg.fields.boolean_value !== undefined) {
paramTypeDisplay = 'boolean'; paramTypeDisplay = 'boolean';
if (param.fields.value.msg.fields.boolean_value.value === 0) { 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) { } else if (param.fields.value.msg.fields.boolean_value.value === 1) {
paramValueDisplay = 'Enabled'; paramValueDisplay = t('param.enabled');
} }
if (paramDefaultValue === 0) { if (paramDefaultValue === 0) {
paramDefaultValueDisplay = 'Disabled'; paramDefaultValueDisplay = t('param.disabled');
} else { } else {
paramDefaultValueDisplay = 'Enabled'; paramDefaultValueDisplay = t('param.enabled');
} }
paramMinValueDisplay = ""; paramMinValueDisplay = "";
paramMaxValueDisplay = ""; paramMaxValueDisplay = "";
@@ -306,7 +308,7 @@ const NodeParam = ({ nodeId, nodes }) => {
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell padding="none" align="center"> <TableCell padding="none" align="center">
<Tooltip title="Edit Parameter"> <Tooltip title={t('param.edit_param')}>
<EditIcon fontSize="small" color="primary" /> <EditIcon fontSize="small" color="primary" />
</Tooltip> </Tooltip>
</TableCell> </TableCell>
@@ -322,13 +324,13 @@ const NodeParam = ({ nodeId, nodes }) => {
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell sx={{width: '5%'}}>Idx</TableCell> <TableCell sx={{width: '5%'}}>{t('param.col_idx')}</TableCell>
<TableCell sx={{width: '28%'}}>Name</TableCell> <TableCell sx={{width: '28%'}}>{t('param.col_name')}</TableCell>
<TableCell sx={{width: '10%'}}>Type</TableCell> <TableCell sx={{width: '10%'}}>{t('param.col_type')}</TableCell>
<TableCell sx={{width: '17%'}}>Value</TableCell> <TableCell sx={{width: '17%'}}>{t('param.col_value')}</TableCell>
<TableCell sx={{width: '10%'}}>Default</TableCell> <TableCell sx={{width: '10%'}}>{t('param.col_default')}</TableCell>
<TableCell sx={{width: '10%'}}>Min</TableCell> <TableCell sx={{width: '10%'}}>{t('param.col_min')}</TableCell>
<TableCell sx={{width: '10%'}}>Max</TableCell> <TableCell sx={{width: '10%'}}>{t('param.col_max')}</TableCell>
<TableCell sx={{width: '10%'}}></TableCell> <TableCell sx={{width: '10%'}}></TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@@ -357,7 +359,7 @@ const NodeParam = ({ nodeId, nodes }) => {
sx={{ width: 80, mr: 2, ml: 0.5 }} sx={{ width: 80, mr: 2, ml: 0.5 }}
variant="caption" variant="caption"
> >
Parameters {t('param.title')}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'space-between', flexDirection: 'row', border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5, mr: 2 }}> <Box sx={{ display: 'flex', alignItems: 'space-between', flexDirection: 'row', border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5, mr: 2 }}>
<Button <Button
@@ -367,7 +369,7 @@ const NodeParam = ({ nodeId, nodes }) => {
startIcon={<SyncIcon />} startIcon={<SyncIcon />}
disabled={fetchingParams} disabled={fetchingParams}
> >
{fetchingParams ? 'Fetching...' : 'Fetch All'} {fetchingParams ? t('param.fetching') : t('param.fetch_all')}
</Button> </Button>
<Button <Button
onClick={handleSaveParams} onClick={handleSaveParams}
@@ -377,7 +379,7 @@ const NodeParam = ({ nodeId, nodes }) => {
startIcon={<SaveIcon />} startIcon={<SaveIcon />}
disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0} disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0}
> >
Store All {t('param.store_all')}
</Button> </Button>
<Button <Button
onClick={handleEraseParams} onClick={handleEraseParams}
@@ -385,7 +387,7 @@ const NodeParam = ({ nodeId, nodes }) => {
color="warning" color="warning"
startIcon={<AutoFixNormalIcon />} startIcon={<AutoFixNormalIcon />}
> >
Erase All {t('param.erase_all')}
</Button> </Button>
</Box> </Box>
<Box sx={{ flexGrow: 1 }}></Box> <Box sx={{ flexGrow: 1 }}></Box>
@@ -398,7 +400,7 @@ const NodeParam = ({ nodeId, nodes }) => {
onClick={handleDownloadParams} onClick={handleDownloadParams}
disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0} disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0}
> >
Download {t('param.download')}
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
@@ -406,7 +408,7 @@ const NodeParam = ({ nodeId, nodes }) => {
startIcon={<FileUploadIcon />} startIcon={<FileUploadIcon />}
disabled={!nodeId} // Disable Load button if no node is selected disabled={!nodeId} // Disable Load button if no node is selected
> >
Load {t('param.load')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
+20 -18
View File
@@ -7,6 +7,7 @@ import CableIcon from '@mui/icons-material/Cable';
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
import FirmwareUpdateModal from './FirmwareUpdateModal'; import FirmwareUpdateModal from './FirmwareUpdateModal';
import ConfirmRestartModal from './ConfirmRestartModal'; import ConfirmRestartModal from './ConfirmRestartModal';
import { useTranslation } from './i18n/LanguageContext';
const VendorSpecificCodeDisplay = (code) => { const VendorSpecificCodeDisplay = (code) => {
code = Math.max(0, Math.floor(code) & 0xFFFF); code = Math.max(0, Math.floor(code) & 0xFFFF);
@@ -19,6 +20,7 @@ const VendorSpecificCodeDisplay = (code) => {
const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEditorEnable }) => { const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEditorEnable }) => {
const [firmwareModalOpen, setFirmwareModalOpen] = useState(false); const [firmwareModalOpen, setFirmwareModalOpen] = useState(false);
const [restartModalOpen, setRestartModalOpen] = useState(false); const [restartModalOpen, setRestartModalOpen] = useState(false);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const localNode = window.localNode; 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' }}> <Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption"> <Typography variant="caption">
Node Properties {t('props.title')}
</Typography> </Typography>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> <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) }} /> <Switch checked={multiNodeEditorEnable} onChange={(e) => { setMultiNodeEditorEnable(e.target.checked) }} />
</Stack> </Stack>
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField <TextField
label="Node ID" label={t('props.node_id')}
value={nodeId} value={nodeId}
InputProps={{ InputProps={{
readOnly: true, readOnly: true,
@@ -88,7 +90,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
/> />
<TextField <TextField
label="Name" label={t('props.name')}
value={name} value={name}
fullWidth fullWidth
InputProps={{ InputProps={{
@@ -98,7 +100,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField <TextField
label="Mode" label={t('props.mode')}
value={mode} value={mode}
fullWidth fullWidth
InputProps={{ InputProps={{
@@ -107,7 +109,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
/> />
<TextField <TextField
label="Health" label={t('props.health')}
value={health} value={health}
fullWidth fullWidth
InputProps={{ InputProps={{
@@ -116,7 +118,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
/> />
<TextField <TextField
label="Uptime" label={t('props.uptime')}
value={uptime} value={uptime}
fullWidth fullWidth
InputProps={{ InputProps={{
@@ -126,7 +128,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField <TextField
label="Vendor Specific Status Code" label={t('props.vendor_code')}
fullWidth fullWidth
value={vendor_specific_status_code} value={vendor_specific_status_code}
InputProps={{ InputProps={{
@@ -136,7 +138,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField <TextField
label="Software Version" label={t('props.sw_version')}
fullWidth fullWidth
value={softwareVersion} value={softwareVersion}
InputProps={{ InputProps={{
@@ -145,7 +147,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
/> />
<TextField <TextField
label="CRC64" label={t('props.crc64')}
fullWidth fullWidth
value={softwareCrc64} value={softwareCrc64}
InputProps={{ InputProps={{
@@ -154,7 +156,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
/> />
<TextField <TextField
label="VCS Commit" label={t('props.vcs_commit')}
fullWidth fullWidth
value={softwareVcsCommit} value={softwareVcsCommit}
InputProps={{ InputProps={{
@@ -164,7 +166,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField <TextField
label="Hardware Version" label={t('props.hw_version')}
value={hardwareVersion} value={hardwareVersion}
InputProps={{ InputProps={{
readOnly: true, readOnly: true,
@@ -172,7 +174,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }} sx={{ mr: 0.5 }}
/> />
<TextField <TextField
label="UID" label={t('props.uid')}
fullWidth fullWidth
value={hardwareUID} value={hardwareUID}
InputProps={{ InputProps={{
@@ -182,7 +184,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box> </Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField <TextField
label="Cert. of authenticity" label={t('props.certificate')}
fullWidth fullWidth
value={certificateOfAuthenticity} value={certificateOfAuthenticity}
InputProps={{ InputProps={{
@@ -195,7 +197,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ width: 80, mr: 2 }} sx={{ width: 80, mr: 2 }}
variant="caption" variant="caption"
> >
Node Controls {t('props.controls')}
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', flexGrow: 1, border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5 }}> <Box sx={{ display: 'flex', flexDirection: 'row', flexGrow: 1, border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5 }}>
<Button <Button
@@ -205,14 +207,14 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
startIcon={<PowerSettingsNewIcon />} startIcon={<PowerSettingsNewIcon />}
onClick={() => setRestartModalOpen(true)} onClick={() => setRestartModalOpen(true)}
> >
Restart {t('props.restart')}
</Button> </Button>
<Button <Button
sx={{ mr: 1 }} sx={{ mr: 1 }}
variant="outlined" variant="outlined"
startIcon={<CableIcon />} startIcon={<CableIcon />}
> >
Get Transport Stats {t('props.transport_stats')}
</Button> </Button>
<Box sx={{ flexGrow: 1 }}></Box> <Box sx={{ flexGrow: 1 }}></Box>
<Button <Button
@@ -220,7 +222,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
startIcon={<SystemUpdateAltIcon />} startIcon={<SystemUpdateAltIcon />}
onClick={() => setFirmwareModalOpen(true)} onClick={() => setFirmwareModalOpen(true)}
> >
Update Firmware {t('props.update_firmware')}
</Button> </Button>
</Box> </Box>
</Box> </Box>
+5 -3
View File
@@ -1,10 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, Button, Menu, MenuItem, Divider } from '@mui/material'; import { Box, Button, Menu, MenuItem, Divider } from '@mui/material';
import VideogameAssetIcon from '@mui/icons-material/VideogameAsset'; import VideogameAssetIcon from '@mui/icons-material/VideogameAsset';
import { useTranslation } from './i18n/LanguageContext';
const PanelsMenu = ({openWindow}) => { const PanelsMenu = ({openWindow}) => {
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const { t } = useTranslation();
const handleClick = (event) => { const handleClick = (event) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -42,7 +44,7 @@ const PanelsMenu = ({openWindow}) => {
color="default" color="default"
startIcon={<VideogameAssetIcon />} startIcon={<VideogameAssetIcon />}
> >
Panels {t('panels.title')}
</Button> </Button>
<Menu <Menu
elevation={0} elevation={0}
@@ -59,10 +61,10 @@ const PanelsMenu = ({openWindow}) => {
onClose={handleClose} onClose={handleClose}
> >
<MenuItem onClick={handleEscPanelClick} disableRipple> <MenuItem onClick={handleEscPanelClick} disableRipple>
ESC {t('panels.esc')}
</MenuItem> </MenuItem>
<MenuItem onClick={handleActuatorPanelClick} disableRipple> <MenuItem onClick={handleActuatorPanelClick} disableRipple>
Actuator {t('panels.actuator')}
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
+7 -5
View File
@@ -7,6 +7,7 @@ import DronecanLogo from './image/dronecan_logo.png';
import { toYaml } from './dronecan/message_format_utils'; import { toYaml } from './dronecan/message_format_utils';
import theme from './theme'; import theme from './theme';
import { useTranslation } from './i18n/LanguageContext';
import './css/subscriber.css'; import './css/subscriber.css';
const SubscriberWindow = () => { const SubscriberWindow = () => {
@@ -19,9 +20,10 @@ const SubscriberWindow = () => {
const [recordingSet, setRecordingSet] = useState([]); const [recordingSet, setRecordingSet] = useState([]);
const [displayRecordText, setDisplayRecordText] = useState(""); const [displayRecordText, setDisplayRecordText] = useState("");
const [recording, setRecording] = useState(true); const [recording, setRecording] = useState(true);
const { t } = useTranslation();
if (window.opener === null) { if (window.opener === null) {
return "Not Allowed To Open Directly"; return t('sub.not_allowed');
} }
useEffect(() => { useEffect(() => {
@@ -61,7 +63,7 @@ const SubscriberWindow = () => {
} }
const msg = transfer.payload; const msg = transfer.payload;
const msgObj = msg.toObj(); const msgObj = msg.toObj();
let destNodeText = "All"; let destNodeText = t('sub.all');
if (transfer.destNodeId && transfer.destNodeId !== 0) { if (transfer.destNodeId && transfer.destNodeId !== 0) {
destNodeText = `${transfer.destNodeId}`; destNodeText = `${transfer.destNodeId}`;
} }
@@ -148,12 +150,12 @@ const SubscriberWindow = () => {
<Box sx={{display: 'flex', flexDirection: 'row', alignItems: 'center', mr: 0.5}}> <Box sx={{display: 'flex', flexDirection: 'row', alignItems: 'center', mr: 0.5}}>
<Box sx={{minWidth: 200, display: 'flex', flexDirection: 'row'}}> <Box sx={{minWidth: 200, display: 'flex', flexDirection: 'row'}}>
<Box sx={{flexGrow: 1}}></Box> <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={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> <Typography variant="caption" mr={1} sx={{minWidth: 30}}>{messageRate.toFixed(0)}</Typography>
</Box> </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} /> <TextField size="small" sx={{width: 80}} type="number" min={1} max={100} value={maxMessageCount} onChange={updateMaxMessageCount} />
<IconButton size="small" onClick={handleClean}> <IconButton size="small" onClick={handleClean}>
<CleaningServicesIcon /> <CleaningServicesIcon />
+5 -3
View File
@@ -3,10 +3,12 @@ import { Box, Button, Menu, MenuItem, Divider } from '@mui/material';
import BuildIcon from '@mui/icons-material/Build'; import BuildIcon from '@mui/icons-material/Build';
import MessageIcon from '@mui/icons-material/Message'; import MessageIcon from '@mui/icons-material/Message';
import SettingsInputCompositeIcon from '@mui/icons-material/SettingsInputComposite'; import SettingsInputCompositeIcon from '@mui/icons-material/SettingsInputComposite';
import { useTranslation } from './i18n/LanguageContext';
const ToolsMenu =({openWindow}) => { const ToolsMenu =({openWindow}) => {
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const { t } = useTranslation();
const handleClick = (event) => { const handleClick = (event) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -44,7 +46,7 @@ const ToolsMenu =({openWindow}) => {
color="default" color="default"
startIcon={<BuildIcon />} startIcon={<BuildIcon />}
> >
Tools {t('tools.title')}
</Button> </Button>
<Menu <Menu
elevation={0} elevation={0}
@@ -61,10 +63,10 @@ const ToolsMenu =({openWindow}) => {
onClose={handleClose} onClose={handleClose}
> >
<MenuItem onClick={handleSubscriberClick} disableRipple> <MenuItem onClick={handleSubscriberClick} disableRipple>
Subscriber {t('tools.subscriber')}
</MenuItem> </MenuItem>
<MenuItem onClick={handleBusMonitorClick} disableRipple> <MenuItem onClick={handleBusMonitorClick} disableRipple>
Bus Monitor {t('tools.bus_monitor')}
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
+67
View 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
View 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": "关闭",
};