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

View File

@@ -10,6 +10,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import SettingsIcon from '@mui/icons-material/Settings';
import { useTranslation } from './i18n/LanguageContext';
const MAX_ACTUATOR_IDS = 256;
@@ -50,6 +51,7 @@ const ActuatorPanel = () => {
const [showIdSelector, setShowIdSelector] = useState(false);
const nodeId = 0;
const { t } = useTranslation();
const activeActuatorIds = enabledActuatorIds
.map((enabled, id) => enabled ? id : null)
@@ -282,9 +284,9 @@ const ActuatorPanel = () => {
const renderIdSelectorDialog = () => (
<Dialog open={showIdSelector} onClose={() => setShowIdSelector(false)}>
<DialogTitle>Select Actuator IDs</DialogTitle>
<DialogTitle>{t('act.select_ids_title')}</DialogTitle>
<DialogContent>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Select Actuator IDs:</Typography>
<Typography variant="subtitle2" sx={{ mb: 1 }}>{t('act.select_ids_label')}</Typography>
<FormGroup sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 1 }}>
{Array(MAX_ACTUATOR_IDS).fill(0).map((_, id) => (
<FormControlLabel
@@ -302,7 +304,7 @@ const ActuatorPanel = () => {
</DialogContent>
<DialogActions>
<Button onClick={() => setShowIdSelector(false)} color="primary">
Done
{t('act.done')}
</Button>
</DialogActions>
</Dialog>
@@ -316,11 +318,11 @@ const ActuatorPanel = () => {
fullWidth
>
<DialogTitle>
Command Type Range Settings
{t('act.range_title')}
</DialogTitle>
<DialogContent>
<Typography variant="body2" sx={{ mb: 2, fontStyle: 'italic' }}>
Configure default ranges for each command type. These settings can be applied to all actuators.
{t('act.range_instruction')}
</Typography>
<Box sx={{ mb: 3 }}>
@@ -328,7 +330,7 @@ const ActuatorPanel = () => {
{COMMAND_TYPE_LABELS[0]}
</Typography>
<Typography variant="body2" color="text.secondary">
Unitless command range is fixed at -1 to 1
{t('act.unitless_fixed')}
</Typography>
</Box>
@@ -339,7 +341,7 @@ const ActuatorPanel = () => {
<Grid container spacing={2} alignItems="center">
<Grid item xs={5}>
<TextField
label="Min"
label={t('act.min')}
type="number"
size="small"
fullWidth
@@ -350,7 +352,7 @@ const ActuatorPanel = () => {
</Grid>
<Grid item xs={5}>
<TextField
label="Max"
label={t('act.max')}
type="number"
size="small"
fullWidth
@@ -366,7 +368,7 @@ const ActuatorPanel = () => {
onClick={() => applyRangesToAllOfType(1)}
fullWidth
>
Apply
{t('act.apply')}
</Button>
</Grid>
</Grid>
@@ -379,7 +381,7 @@ const ActuatorPanel = () => {
<Grid container spacing={2} alignItems="center">
<Grid item xs={5}>
<TextField
label="Min"
label={t('act.min')}
type="number"
size="small"
fullWidth
@@ -390,7 +392,7 @@ const ActuatorPanel = () => {
</Grid>
<Grid item xs={5}>
<TextField
label="Max"
label={t('act.max')}
type="number"
size="small"
fullWidth
@@ -406,7 +408,7 @@ const ActuatorPanel = () => {
onClick={() => applyRangesToAllOfType(2)}
fullWidth
>
Apply
{t('act.apply')}
</Button>
</Grid>
</Grid>
@@ -419,7 +421,7 @@ const ActuatorPanel = () => {
<Grid container spacing={2} alignItems="center">
<Grid item xs={5}>
<TextField
label="Min"
label={t('act.min')}
type="number"
size="small"
fullWidth
@@ -430,7 +432,7 @@ const ActuatorPanel = () => {
</Grid>
<Grid item xs={5}>
<TextField
label="Max"
label={t('act.max')}
type="number"
size="small"
fullWidth
@@ -446,7 +448,7 @@ const ActuatorPanel = () => {
onClick={() => applyRangesToAllOfType(3)}
fullWidth
>
Apply
{t('act.apply')}
</Button>
</Grid>
</Grid>
@@ -458,13 +460,13 @@ const ActuatorPanel = () => {
color="primary"
variant="contained"
>
Apply All Ranges
{t('act.apply_all')}
</Button>
<Button
onClick={() => setShowSettingsModal(false)}
color="primary"
>
Close
{t('act.close')}
</Button>
</DialogActions>
</Dialog>
@@ -553,7 +555,7 @@ const ActuatorPanel = () => {
onClick={() => setShowIdSelector(true)}
sx={{ textTransform: 'none' }}
>
Actuator IDs ({activeActuatorIds.length})
{t('act.ids', { count: activeActuatorIds.length })}
</Button>
</Box>
@@ -566,14 +568,14 @@ const ActuatorPanel = () => {
onClick={() => setShowSettingsModal(true)}
sx={{ textTransform: 'none' }}
>
Range Settings
{t('act.range_settings')}
</Button>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ mr: 1 }}>Broadcast Rate:</Typography>
<Typography variant="body2" sx={{ mr: 1 }}>{t('act.broadcast_rate')}</Typography>
<TextField
type="number"
size="small"
@@ -642,22 +644,22 @@ const ActuatorPanel = () => {
width: '50%'
}}>
<Box sx={{flexGrow: 1}}>
<Typography variant="body2" color="textSecondary">ID: {actuator.actuator_id}</Typography>
<Typography variant="body2" color="textSecondary">{t('act.id')} {actuator.actuator_id}</Typography>
<Typography variant="body2" color="textSecondary">
Pos: {actuator.position !== null ? actuator.position.toFixed(3) : "NC"}
{t('act.pos')} {actuator.position !== null ? actuator.position.toFixed(3) : t('act.nc')}
</Typography>
<Typography variant="body2" color="textSecondary">
Force: {actuator.force !== null ? `${actuator.force.toFixed(2)} N` : "NC"}
{t('act.force')} {actuator.force !== null ? `${actuator.force.toFixed(2)} N` : t('act.nc')}
</Typography>
<Typography variant="body2" color="textSecondary">
Speed: {actuator.speed !== null ? `${actuator.speed.toFixed(2)} rad/s` : "NC"}
{t('act.speed')} {actuator.speed !== null ? `${actuator.speed.toFixed(2)} rad/s` : t('act.nc')}
</Typography>
<Typography variant="body2" color="textSecondary">
RAT: {actuator.power_rating_pct !== null
? actuator.power_rating_pct === 127
? "unknown"
: `${actuator.power_rating_pct.toFixed(1)} %`
: "NC"}
{t('act.rat')} {actuator.power_rating_pct !== null
? actuator.power_rating_pct === 127
? t('act.unknown')
: `${actuator.power_rating_pct.toFixed(1)} %`
: t('act.nc')}
</Typography>
</Box>
@@ -668,10 +670,10 @@ const ActuatorPanel = () => {
variant="outlined"
sx={{ height: '30px', fontSize: '0.8rem' }}
>
<MenuItem value={COMMAND_TYPES.UNITLESS}>Unitless</MenuItem>
<MenuItem value={COMMAND_TYPES.POSITION}>Position</MenuItem>
<MenuItem value={COMMAND_TYPES.FORCE}>Force</MenuItem>
<MenuItem value={COMMAND_TYPES.SPEED}>Speed</MenuItem>
<MenuItem value={COMMAND_TYPES.UNITLESS}>{t('act.type_unitless')}</MenuItem>
<MenuItem value={COMMAND_TYPES.POSITION}>{t('act.type_position')}</MenuItem>
<MenuItem value={COMMAND_TYPES.FORCE}>{t('act.type_force')}</MenuItem>
<MenuItem value={COMMAND_TYPES.SPEED}>{t('act.type_speed')}</MenuItem>
</Select>
</FormControl>
@@ -700,7 +702,7 @@ const ActuatorPanel = () => {
fullWidth
size="small"
>
Zero
{t('act.zero')}
</Button>
</Box>
</Box>
@@ -749,7 +751,7 @@ const ActuatorPanel = () => {
}}>
<Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}>
<Typography variant="body2" color="textSecondary">
cmd: [{
{t('act.cmd')} [{
activeActuatorIds
.map(id => {
const type = commandTypes[id] || COMMAND_TYPES.UNITLESS;
@@ -768,7 +770,7 @@ const ActuatorPanel = () => {
startIcon={<PanToolIcon />}
onClick={handleZeroAll}
>
Zero All
{t('act.zero_all')}
</Button>
</Box>
</Box>

View File

@@ -17,8 +17,10 @@ import './css/index.css';
import ConnectionIndicators from './ConnectionIndicators';
import DnsIcon from '@mui/icons-material/Dns';
import LanIcon from '@mui/icons-material/Lan';
import LanguageIcon from '@mui/icons-material/Language';
import CompactSidebar from './CompactSidebar';
import DynamicNodeIdServer from './services/DynamicNodeIdServer';
import { LanguageProvider, useTranslation } from './i18n/LanguageContext';
window.mavlinkSession = new MavlinkSession();
window.localNode = new dronecan.Node({name: "com.vimdrones.web_gui"});
@@ -32,6 +34,17 @@ localNode.on('uavcan.protocol.file.Read.Request', (transfer) => {
});
const App = () => {
return (
<ThemeProvider theme={theme}>
<LanguageProvider>
<AppContent />
</LanguageProvider>
</ThemeProvider>
);
};
const AppContent = () => {
const { t, language, setLanguage } = useTranslation();
const [nodes, setNodes] = useState({});
const [nodesUpdateTimestamp, setNodesUpdateTimestamp] = useState(0);
const [isConnected, setIsConnected] = useState(false);
@@ -75,7 +88,7 @@ const App = () => {
const handleConnectionStatusChange = (isConnected) => {
setIsConnected(isConnected);
showMessage(
isConnected ? 'Successfully connected to device' : 'Disconnected from device',
isConnected ? t('app.connected') : t('app.disconnected'),
isConnected ? 'success' : 'info'
);
};
@@ -87,7 +100,7 @@ const App = () => {
if (window.localNode) {
window.localNode.changeBus(newBus);
showMessage(`Switched to CAN bus ${newBus}`, 'info');
showMessage(t('app.bus_switched', { bus: newBus }), 'info');
}
};
@@ -119,16 +132,16 @@ const App = () => {
if (window.dnaServer.getStatus().isActive) {
window.dnaServer.stop();
setDnaServerActive(false);
showMessage('DNA server stopped', 'info');
showMessage(t('app.dna_stopped'), 'info');
} else {
const success = window.dnaServer.start(1, 125);
setDnaServerActive(success);
showMessage(success ? 'DNA server started' : 'Failed to start DNA server', success ? 'success' : 'error');
showMessage(success ? t('app.dna_started') : t('app.dna_failed'), success ? 'success' : 'error');
}
};
return (
<ThemeProvider theme={theme}>
<>
<AppBar position="static">
<Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center'}}>
@@ -142,7 +155,7 @@ const App = () => {
</a>
</Box>
<Typography variant="caption">
DroneCAN Web Tools
{t('app.title')}
</Typography>
</Box>
<Box sx={{width: '30%', flexGrow: 1, display: 'flex', flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', gap: 1}}>
@@ -175,10 +188,10 @@ const App = () => {
backgroundColor: 'background.paper',
}}
>
<MenuItem value={0}>Bus 1</MenuItem>
<MenuItem value={1}>Bus 2</MenuItem>
<MenuItem value={2}>Bus 3</MenuItem>
<MenuItem value={3}>Bus 4</MenuItem>
<MenuItem value={0}>{t('app.bus', { n: 1 })}</MenuItem>
<MenuItem value={1}>{t('app.bus', { n: 2 })}</MenuItem>
<MenuItem value={2}>{t('app.bus', { n: 3 })}</MenuItem>
<MenuItem value={3}>{t('app.bus', { n: 4 })}</MenuItem>
</Select>
</FormControl>
@@ -206,16 +219,27 @@ const App = () => {
}
} : {}}
>
DNA
{t('app.dna')}
</Button>
<Button
variant="contained"
color="primary"
<Tooltip title={language === 'en' ? '中文' : 'English'}>
<IconButton
size="small"
color="inherit"
onClick={() => setLanguage(language === 'en' ? 'zh' : 'en')}
sx={{ mr: 0.5 }}
>
<LanguageIcon fontSize="small" />
</IconButton>
</Tooltip>
<Button
variant="contained"
color="primary"
startIcon={<LanIcon />}
onClick={handleOpenModal}
>
Adapter
{t('app.adapter')}
</Button>
</Box>
</Toolbar>
@@ -294,7 +318,7 @@ const App = () => {
{snackbarMessage}
</Alert>
</Snackbar>
</ThemeProvider>
</>
);
};

View File

@@ -10,6 +10,7 @@ import PauseIcon from '@mui/icons-material/Pause';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import SaveIcon from '@mui/icons-material/Save';
import { toYaml } from './dronecan/message_format_utils';
import { useTranslation } from './i18n/LanguageContext';
const BusMonitor = () => {
const [transfers, setTransfers] = useState([]);
@@ -18,6 +19,7 @@ const BusMonitor = () => {
const [selectedTransfer, setSelectedTransfer] = useState(null);
const [detailsOpen, setDetailsOpen] = useState(false);
const [messageYaml, setMessageYaml] = useState('');
const { t } = useTranslation();
const tableContainerRef = useRef(null);
const maxTransfers = 1000; // Maximum number of transfers to store
@@ -29,23 +31,23 @@ const BusMonitor = () => {
let yamlText = '';
if (transfer.data && transfer.data.toObj) {
const msgObj = transfer.data.toObj();
yamlText = `### Message details\n`;
yamlText += `Direction: ${transfer.direction}\n`;
yamlText += `Time: ${transfer.timestamp}\n`;
yamlText += `CAN ID: ${transfer.frameId}\n`;
yamlText += `Source Node: ${transfer.sourceNodeId}\n`;
yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`;
yamlText += `Data Type: ${transfer.dataType}\n\n`;
yamlText += `### Message Payload\n`;
yamlText = `${t('bus.details_heading')}\n`;
yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`;
yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`;
yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`;
yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`;
yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`;
yamlText += `${t('bus.detail_data_type')} ${transfer.dataType}\n\n`;
yamlText += `${t('bus.payload_heading')}\n`;
yamlText += toYaml(msgObj);
} else {
yamlText = `No detailed payload data available for this transfer.\n\n`;
yamlText += `Direction: ${transfer.direction}\n`;
yamlText += `Time: ${transfer.timestamp}\n`;
yamlText += `CAN ID: ${transfer.frameId}\n`;
yamlText += `Hex Data: ${transfer.hexData}\n`;
yamlText += `Source Node: ${transfer.sourceNodeId}\n`;
yamlText += `Destination Node: ${transfer.destNodeId || 'Broadcast'}\n`;
yamlText = `${t('bus.no_payload')}\n\n`;
yamlText += `${t('bus.detail_direction')} ${transfer.direction}\n`;
yamlText += `${t('bus.detail_time')} ${transfer.timestamp}\n`;
yamlText += `${t('bus.detail_can_id')} ${transfer.frameId}\n`;
yamlText += `${t('bus.detail_hex_data')} ${transfer.hexData}\n`;
yamlText += `${t('bus.detail_source')} ${transfer.sourceNodeId}\n`;
yamlText += `${t('bus.detail_dest')} ${transfer.destNodeId || t('bus.broadcast')}\n`;
}
setMessageYaml(yamlText);
@@ -139,7 +141,7 @@ const BusMonitor = () => {
};
const exportToCSV = () => {
const headers = ['Direction', 'Timestamp', 'CAN ID (Hex)', 'Hex Data', 'Src Node ID', 'Dst Node ID', 'Data Type', 'Raw Data'];
const headers = [t('bus.csv_direction'), t('bus.csv_timestamp'), t('bus.csv_can_id'), t('bus.csv_hex_data'), t('bus.csv_src'), t('bus.csv_dst'), t('bus.csv_data_type'), t('bus.csv_raw')];
const csvRows = [
headers.join(','),
...transfers.map(transfer => {
@@ -182,7 +184,7 @@ const BusMonitor = () => {
<AppBar position="static" color="primary">
<Toolbar variant="dense">
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Bus Monitor
{t('bus.title')}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<FormControlLabel
@@ -193,7 +195,7 @@ const BusMonitor = () => {
size="small"
/>
}
label={<Typography variant="body2">Auto Scroll</Typography>}
label={<Typography variant="body2">{t('bus.auto_scroll')}</Typography>}
labelPlacement="start"
/>
<IconButton
@@ -218,7 +220,7 @@ const BusMonitor = () => {
variant="outlined"
sx={{ ml: 1 }}
>
Export
{t('bus.export')}
</Button>
</Box>
</Toolbar>
@@ -232,13 +234,13 @@ const BusMonitor = () => {
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell>Dir</TableCell>
<TableCell>Time</TableCell>
<TableCell>CAN ID</TableCell>
<TableCell>Hex Data</TableCell>
<TableCell>Src</TableCell>
<TableCell>Dst</TableCell>
<TableCell>Data Type</TableCell>
<TableCell>{t('bus.col_dir')}</TableCell>
<TableCell>{t('bus.col_time')}</TableCell>
<TableCell>{t('bus.col_can_id')}</TableCell>
<TableCell>{t('bus.col_hex_data')}</TableCell>
<TableCell>{t('bus.col_src')}</TableCell>
<TableCell>{t('bus.col_dst')}</TableCell>
<TableCell>{t('bus.col_data_type')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -297,7 +299,7 @@ const BusMonitor = () => {
}}
>
<Typography variant="body2" color="textSecondary">
Showing {transfers.length} of max {maxTransfers} transfers
{t('bus.showing', { count: transfers.length, max: maxTransfers })}
</Typography>
{isPaused && (
<Typography
@@ -310,7 +312,7 @@ const BusMonitor = () => {
gap: '4px'
}}
>
<PauseIcon fontSize="small" /> PAUSED
<PauseIcon fontSize="small" /> {t('bus.paused')}
</Typography>
)}
</Box>
@@ -322,7 +324,7 @@ const BusMonitor = () => {
fullWidth
>
<DialogTitle>
Message Details
{t('bus.message_details')}
{selectedTransfer && (
<Typography variant="subtitle2" color="textSecondary">
{selectedTransfer.dataType} - {selectedTransfer.frameId}
@@ -347,7 +349,7 @@ const BusMonitor = () => {
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDetails} color="primary">
Close
{t('bus.close')}
</Button>
</DialogActions>
</Dialog>

View File

@@ -1,21 +1,23 @@
import React from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material';
import { useTranslation } from './i18n/LanguageContext';
const ConfirmRestartModal = ({ open, onClose, onConfirm }) => {
const { t } = useTranslation();
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Confirm Restart</DialogTitle>
<DialogTitle>{t('confirm.title')}</DialogTitle>
<DialogContent>
<Typography variant="body2">
Are you sure you want to restart the node?
{t('confirm.message')}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="secondary">
Cancel
{t('confirm.cancel')}
</Button>
<Button onClick={onConfirm} color="primary">
Confirm
{t('confirm.confirm')}
</Button>
</DialogActions>
</Dialog>

View File

@@ -11,6 +11,7 @@ import CloseIcon from '@mui/icons-material/Close';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import WebSerial from './web_serial';
import { useTranslation } from './i18n/LanguageContext';
// Add this constant at the top of your file, outside the component
const USB_DEVICE_NAMES = {
@@ -115,10 +116,11 @@ const ConnectionSettingsModal = ({
// Add this state variable to track connection attempts in progress
const [connectionInProgress, setConnectionInProgress] = useState(false);
const { t } = useTranslation();
// Create a function to identify and index duplicate devices
const getPortDisplayName = (port, allPorts) => {
if (!port) return "No port selected";
if (!port) return t('conn.no_port_selected');
// Try to extract the most user-friendly name possible
if (port.info && port.info.product) {
@@ -196,7 +198,7 @@ const ConnectionSettingsModal = ({
}
// Fallback for ports without specific info
return "Serial Port";
return t('conn.serial_port');
};
// Moved from App.js - Lists available ports
@@ -249,7 +251,7 @@ const ConnectionSettingsModal = ({
setActiveConnection(null);
setActiveSerialProtocol(null);
onConnectionStatusChange(false);
showMessage('Serial connection closed', 'info');
showMessage(t('conn.serial_closed'), 'info');
// Clear the forwarding interval
if (forwardingInterval) {
@@ -293,12 +295,12 @@ const ConnectionSettingsModal = ({
setActiveSerialProtocol(serialProtocol);
setConnectionInProgress(false);
onConnectionStatusChange(true);
showMessage(`Serial ${serialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'} connection established`, 'success');
showMessage(serialProtocol === SERIAL_PROTOCOLS.SLCAN ? t('conn.serial_slcan_ok') : t('conn.serial_mavlink_ok'), 'success');
})
window.mavlinkSession.addWebSerialErrorHandler((error) => {
console.error('Serial connection error:', error);
showMessage(`Serial connection failed: ${error.message || 'Could not connect to port'}`, 'error');
showMessage(t('conn.serial_failed', { error: error.message || t('conn.could_not_connect') }), 'error');
// Reset in-progress state on error
setConnectionInProgress(false);
});
@@ -306,7 +308,7 @@ const ConnectionSettingsModal = ({
window.mavlinkSession.webSerialConnect();
} catch (error) {
console.error('Serial connection error:', error);
showMessage(`Serial connection failed: ${error.message || 'Could not connect to port'}`, 'error');
showMessage(t('conn.serial_failed', { error: error.message || t('conn.could_not_connect') }), 'error');
// Reset in-progress state on error
setConnectionInProgress(false);
}
@@ -317,7 +319,7 @@ const ConnectionSettingsModal = ({
}
} catch (error) {
console.error('Error with serial connection:', error);
showMessage(`Serial error: ${error.message || 'Unknown error'}`, 'error');
showMessage(t('conn.serial_error', { error: error.message || t('conn.unknown_error') }), 'error');
// Reset in-progress state on any error
setConnectionInProgress(false);
}
@@ -327,7 +329,7 @@ const ConnectionSettingsModal = ({
const validateIpAddress = (input) => {
// Check if empty
if (!input) {
return 'IP address is required';
return t('conn.ip_required');
}
// Allow "localhost"
@@ -342,7 +344,7 @@ const ConnectionSettingsModal = ({
for (const octet of octets) {
const num = parseInt(octet, 10);
if (isNaN(num) || num < 0 || num > 255 || octet !== num.toString()) {
return 'Each part must be a number between 0-255';
return t('conn.ip_invalid_parts');
}
}
return '';
@@ -356,17 +358,17 @@ const ConnectionSettingsModal = ({
return '';
}
return 'Invalid IP address or hostname';
return t('conn.ip_invalid');
};
const validatePort = (input) => {
if (!input) {
return 'Port is required';
return t('conn.port_required');
}
const port = parseInt(input, 10);
if (isNaN(port) || port < 1 || port > 65535) {
return 'Port must be between 1-65535';
return t('conn.port_range');
}
return '';
@@ -375,7 +377,7 @@ const ConnectionSettingsModal = ({
// Add validation for nodeId
const validateNodeId = (value) => {
const id = parseInt(value, 10);
return (isNaN(id) || id < 1 || id > 127) ? 'Node ID must be between 1-127' : '';
return (isNaN(id) || id < 1 || id > 127) ? t('conn.node_id_range') : '';
};
// Update handler to propagate changes to parent
@@ -417,7 +419,7 @@ const ConnectionSettingsModal = ({
setActiveConnection(null);
setActiveSerialProtocol(null);
onConnectionStatusChange(false);
showMessage('WebSocket connection closed', 'info');
showMessage(t('conn.ws_closed'), 'info');
if (forwardingInterval) {
clearInterval(forwardingInterval);
setForwardingInterval(null);
@@ -462,7 +464,7 @@ const ConnectionSettingsModal = ({
setActiveSerialProtocol(null);
onConnectionStatusChange(true);
setConnectionInProgress(false);
showMessage('WebSocket connection established', 'success');
showMessage(t('conn.ws_connected'), 'success');
});
window.mavlinkSession.addWebSocketErrorHandler((error) => {
@@ -474,11 +476,11 @@ const ConnectionSettingsModal = ({
setActiveConnection(null);
onConnectionStatusChange(false);
setConnectionInProgress(false);
let errorMsg = 'Connection failed';
let errorMsg = t('conn.ws_failed');
if (error && error.message) {
errorMsg = `Connection failed: ${error.message}`;
errorMsg = t('conn.ws_failed_detail', { error: error.message });
} else if (typeof error === 'string') {
errorMsg = `Connection failed: ${error}`;
errorMsg = t('conn.ws_failed_detail', { error: error });
}
showMessage(errorMsg, 'error');
});
@@ -490,7 +492,7 @@ const ConnectionSettingsModal = ({
}
} catch (error) {
console.error('Error with WebSocket connection:', error);
showMessage(`WebSocket error: ${error.message || 'Unknown error'}`, 'error');
showMessage(t('conn.ws_error', { error: error.message || t('conn.unknown_error') }), 'error');
setConnectionInProgress(false);
}
};
@@ -521,11 +523,11 @@ const ConnectionSettingsModal = ({
>
<DialogTitle>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Typography variant="h6">Adapter Settings</Typography>
<Typography variant="h6">{t('conn.title')}</Typography>
<Box display="flex" alignItems="center" gap={1}>
{activeConnection && (
<Chip
label={activeConnection === 'serial' && activeSerialProtocol ? `Connected via serial (${activeSerialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'})` : `Connected via ${activeConnection}`}
label={activeConnection === 'serial' && activeSerialProtocol ? (activeSerialProtocol === SERIAL_PROTOCOLS.SLCAN ? t('conn.connected_serial_slcan') : t('conn.connected_serial_mavlink')) : t('conn.connected_ws', { type: activeConnection })}
color="success"
size="small"
variant="outlined"
@@ -555,21 +557,21 @@ const ConnectionSettingsModal = ({
width: '100%',
bgcolor: activeConnection === 'serial' ? 'rgba(0, 200, 83, 0.1)' : 'inherit'
}}>
<Typography variant="subtitle1" gutterBottom>Serial Connection</Typography>
<Typography variant="subtitle1" gutterBottom>{t('conn.serial_section')}</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> {/* Reduce gap */}
{/* Port and Baud Selection in same row */}
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
{/* Port Selection - takes more space */}
<Box sx={{ flex: 3 }}>
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
<InputLabel>Port</InputLabel>
<InputLabel>{t('conn.port')}</InputLabel>
<Select
value={selectedPort || ''}
onChange={(e) => setSelectedPort(e.target.value)}
label="Port"
label={t('conn.port')}
>
{ports.length === 0 ? (
<MenuItem value="" disabled>No ports available</MenuItem>
<MenuItem value="" disabled>{t('conn.no_ports')}</MenuItem>
) : (
ports.map((port, index) => (
<MenuItem key={index} value={port}>
@@ -584,11 +586,11 @@ const ConnectionSettingsModal = ({
{/* Baud Rate Selection - takes less space */}
<Box sx={{ flex: 1 }}>
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
<InputLabel>Baud Rate</InputLabel>
<InputLabel>{t('conn.baud_rate')}</InputLabel>
<Select
value={baudRate}
onChange={(e) => setBaudRate(e.target.value)}
label="Baud Rate"
label={t('conn.baud_rate')}
>
{BAUD_RATES.map((rate) => (
<MenuItem key={rate} value={rate}>
@@ -601,14 +603,14 @@ const ConnectionSettingsModal = ({
</Box>
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
<InputLabel>Serial Protocol</InputLabel>
<InputLabel>{t('conn.serial_protocol')}</InputLabel>
<Select
value={serialProtocol}
onChange={(e) => setSerialProtocol(e.target.value)}
label="Serial Protocol"
label={t('conn.serial_protocol')}
>
<MenuItem value={SERIAL_PROTOCOLS.MAVLINK}>MAVLink tunnel</MenuItem>
<MenuItem value={SERIAL_PROTOCOLS.SLCAN}>SLCAN / LAWICEL</MenuItem>
<MenuItem value={SERIAL_PROTOCOLS.MAVLINK}>{t('conn.protocol_mavlink')}</MenuItem>
<MenuItem value={SERIAL_PROTOCOLS.SLCAN}>{t('conn.protocol_slcan')}</MenuItem>
</Select>
</FormControl>
@@ -621,7 +623,7 @@ const ConnectionSettingsModal = ({
disabled={activeConnection !== null}
size="small"
>
Refresh
{t('conn.refresh')}
</Button>
<Button
variant="outlined"
@@ -630,7 +632,7 @@ const ConnectionSettingsModal = ({
disabled={activeConnection !== null}
size="small"
>
Request
{t('conn.request')}
</Button>
<Box sx={{ flexGrow: 1 }} />
<Button
@@ -644,8 +646,8 @@ const ConnectionSettingsModal = ({
}
size="small"
>
{activeConnection === 'serial' ? 'Disconnect' :
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'}
{activeConnection === 'serial' ? t('conn.disconnect') :
connectionInProgress && !activeConnection ? t('conn.connecting') : t('conn.connect')}
</Button>
</Box>
</Box>
@@ -657,11 +659,11 @@ const ConnectionSettingsModal = ({
width: '100%',
bgcolor: activeConnection === 'websocket' ? 'rgba(0, 200, 83, 0.1)' : 'inherit'
}}>
<Typography variant="subtitle1" gutterBottom>WebSocket Connection</Typography>
<Typography variant="subtitle1" gutterBottom>{t('conn.ws_section')}</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> {/* Reduce gap */}
<Box sx={{ display: 'flex', gap: 2 }}>
<TextField
label="Host/IP Address"
label={t('conn.host')}
value={wsHost}
onChange={handleWsHostChange}
disabled={activeConnection !== null}
@@ -671,7 +673,7 @@ const ConnectionSettingsModal = ({
helperText={hostError}
/>
<TextField
label="Port"
label={t('conn.ws_port')}
value={wsPort}
onChange={handleWsPortChange}
type="number"
@@ -698,8 +700,8 @@ const ConnectionSettingsModal = ({
}
size="small"
>
{activeConnection === 'websocket' ? 'Disconnect' :
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'}
{activeConnection === 'websocket' ? t('conn.disconnect') :
connectionInProgress && !activeConnection ? t('conn.connecting') : t('conn.connect')}
</Button>
</Box>
</Box>
@@ -712,7 +714,7 @@ const ConnectionSettingsModal = ({
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<TextField
label="Node ID"
label={t('conn.node_id')}
value={nodeId}
onChange={handleNodeIdChange}
disabled={activeConnection !== null}
@@ -726,18 +728,18 @@ const ConnectionSettingsModal = ({
helperText={validateNodeId(nodeId)}
/>
<TextField
label="Mavlink Signing"
label={t('conn.signing')}
value={mavlinkSigning}
onChange={handleMavlinkSigningChange}
disabled={activeConnection !== null}
size="small"
placeholder="Secret Key"
placeholder={t('conn.secret_key')}
type={showMavlinkSigning ? 'text' : 'password'}
sx={{ flex: 1 }}
InputProps={{
endAdornment: (
<IconButton
aria-label={showMavlinkSigning ? 'Hide secret' : 'Show secret'}
aria-label={showMavlinkSigning ? t('conn.hide_secret') : t('conn.show_secret')}
onClick={handleToggleMavlinkSigning}
edge="end"
size="small"

View File

@@ -13,6 +13,7 @@ import StopIcon from '@mui/icons-material/Stop';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import CloseIcon from '@mui/icons-material/Close';
import DynamicNodeIdServer from './services/DynamicNodeIdServer';
import { useTranslation } from './i18n/LanguageContext';
const DNAServerModal = ({ open, onClose, showMessage }) => {
const [serverEnabled, setServerEnabled] = useState(false);
@@ -23,6 +24,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
const [server, setServer] = useState(null);
const [operationInProgress, setOperationInProgress] = useState(false);
const [refreshInterval, setRefreshInterval] = useState(null);
const { t } = useTranslation();
const handleAllocationUpdate = () => {
console.log("Allocation update detected, refreshing list");
@@ -84,9 +86,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}, 1000); // Check every 5 seconds
setRefreshInterval(interval);
showMessage("DNA server started successfully", "success");
showMessage(t('dna.started'), "success");
} else {
showMessage("Failed to start DNA server", "error");
showMessage(t('dna.failed_start'), "error");
}
} else {
// Stop the server
@@ -99,11 +101,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
setRefreshInterval(null);
}
showMessage("DNA server stopped", "info");
showMessage(t('dna.stopped'), "info");
}
} catch (error) {
console.error("Error toggling DNA server:", error);
showMessage(`Error: ${error.message}`, "error");
showMessage(t('dna.error', { error: error.message }), "error");
} finally {
setOperationInProgress(false);
}
@@ -124,7 +126,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
};
const validateNodeIdRange = () => {
return minNodeId >= maxNodeId ? "Min ID must be less than Max ID" : "";
return minNodeId >= maxNodeId ? t('dna.invalid_range') : "";
};
const handleDeleteAllocation = (nodeId) => {
@@ -133,15 +135,15 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
const success = server.deleteAllocation(nodeId);
if (success) {
fetchCurrentAllocations();
showMessage(`Node ID ${nodeId} allocation revoked`, "info");
showMessage(t('dna.revoked', { id: nodeId }), "info");
} else {
showMessage(`Failed to revoke allocation for node ID ${nodeId}`, "error");
showMessage(t('dna.revoke_failed', { id: nodeId }), "error");
}
};
const handleRefreshAllocations = () => {
fetchCurrentAllocations();
showMessage("Allocations refreshed", "info");
showMessage(t('dna.refreshed'), "info");
};
// Modify the modal close handler to not stop the server
@@ -208,11 +210,11 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
>
<DialogTitle>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Typography variant="h6">Dynamic Node ID Allocation Server</Typography>
<Typography variant="h6">{t('dna.title')}</Typography>
<Box display="flex" alignItems="center" gap={1}>
{serverEnabled && (
<Chip
label="Server Active"
label={t('dna.active')}
color="success"
size="small"
variant="outlined"
@@ -242,7 +244,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
bgcolor: serverEnabled ? 'rgba(0, 200, 83, 0.1)' : 'inherit'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle1">Server Control</Typography>
<Typography variant="subtitle1">{t('dna.control')}</Typography>
<Button
variant="contained"
startIcon={serverEnabled ? <StopIcon /> : <PlayArrowIcon />}
@@ -251,8 +253,8 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
disabled={operationInProgress}
size="small"
>
{operationInProgress ? "Processing..." :
serverEnabled ? "Stop" : "Start"}
{operationInProgress ? t('dna.processing') :
serverEnabled ? t('dna.stop') : t('dna.start')}
</Button>
</Box>
@@ -262,7 +264,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
{/* Min Node ID */}
<Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}>
<TextField
label="Min Node ID"
label={t('dna.min_node_id')}
type="number"
value={minNodeId}
onChange={handleMinNodeIdChange}
@@ -273,14 +275,14 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}}
size="small"
error={minNodeId >= maxNodeId}
helperText={minNodeId >= maxNodeId ? "Must be < Max" : ""}
helperText={minNodeId >= maxNodeId ? t('dna.must_lt_max') : ""}
/>
</Box>
{/* Max Node ID */}
<Box sx={{ width: { xs: '100%', sm: '22%' }, minWidth: '100px' }}>
<TextField
label="Max Node ID"
label={t('dna.max_node_id')}
type="number"
value={maxNodeId}
onChange={handleMaxNodeIdChange}
@@ -291,7 +293,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}}
size="small"
error={minNodeId >= maxNodeId}
helperText={minNodeId >= maxNodeId ? "Must be > Min" : ""}
helperText={minNodeId >= maxNodeId ? t('dna.must_gt_min') : ""}
/>
</Box>
@@ -315,8 +317,8 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ mr: 0.5 }}>Persist Allocations</Typography>
<Tooltip title="When enabled, node ID allocations are stored and restored when the server restarts">
<Typography variant="body2" sx={{ mr: 0.5 }}>{t('dna.persist')}</Typography>
<Tooltip title={t('dna.persist_tooltip')}>
<HelpOutlineIcon fontSize="small" />
</Tooltip>
</Box>
@@ -339,9 +341,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
variant="body1" // Using body1 for smaller font size instead of subtitle1
sx={{ fontWeight: 500 }} // Adding some weight to make it still look like a title
>
Allocated Node IDs ({allocatedNodes.length})
{t('dna.allocated', { count: allocatedNodes.length })}
</Typography>
<Tooltip title="Refresh allocation list">
<Tooltip title={t('dna.refresh_tooltip')}>
<IconButton
onClick={handleRefreshAllocations}
size="small"
@@ -356,7 +358,7 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
{allocatedNodes.length === 0 ? (
<Box sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
No node IDs allocated
{t('dna.no_allocations')}
</Typography>
</Box>
) : (
@@ -374,9 +376,9 @@ const DNAServerModal = ({ open, onClose, showMessage }) => {
<Table size="small" stickyHeader>
<TableHead>
<TableRow>
<TableCell>NID</TableCell>
<TableCell>UUID</TableCell>
<TableCell align="right" width="60px">Action</TableCell>
<TableCell>{t('dna.col_nid')}</TableCell>
<TableCell>{t('dna.col_uuid')}</TableCell>
<TableCell align="right" width="60px">{t('dna.col_action')}</TableCell>
</TableRow>
</TableHead>
<TableBody>

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 MusicNoteIcon from '@mui/icons-material/MusicNote';
import AM32_Rtttl from './am32_rtttl'; // Updated import to match class name
import { useTranslation } from './i18n/LanguageContext';
const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
// Add a new state for tracking whether a tune is currently playing
@@ -21,6 +22,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
const [errorMessage, setErrorMessage] = useState(''); // For validation error messages
const [isValid, setIsValid] = useState(true); // Add a new state variable to track validation status
const [paramName, setParamName] = useState(""); // Add paramName to the component state
const { t } = useTranslation();
useEffect(() => {
const localNode = window.localNode;
@@ -84,7 +86,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
if (!isValidFormat) {
// Warn user but continue with a default tune
setErrorMessage('Warning: Invalid RTTTL format! Using a default empty tune instead.');
setErrorMessage(t('edit.rtttl_warning'));
// Continue with a minimal valid RTTTL string
const tuneToParse = "Empty:d=4,o=5,b=120:";
result = AM32_Rtttl.to_am32_startup_melody(tuneToParse);
@@ -102,7 +104,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
console.log("Binary array values:", Array.from(result.data).slice(0, 30));
} catch (err) {
console.error("Error converting RTTTL to binary:", err);
setErrorMessage(`Error saving tune: ${err.message || 'Unknown error'}`);
setErrorMessage(t('edit.error_saving', { error: err.message || t('edit.unknown') }));
// Provide an empty binary string (all zeros) as fallback
const emptyArray = new Uint8Array(128);
valueToSave = String.fromCharCode.apply(null, emptyArray);
@@ -126,30 +128,30 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
try {
// First validate that the tune has the basic RTTTL format (name:defaults:notes)
if (!tuneToPlay || !tuneToPlay.includes(':') || tuneToPlay.split(':').length !== 3) {
setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes');
setErrorMessage(t('edit.rtttl_invalid'));
return;
}
// Clear any previous error when successful
setErrorMessage('');
// Stop any currently playing tune before starting a new one
AM32_Rtttl.stopMelody();
// Play the new tune
AM32_Rtttl.playMelody(tuneToPlay);
setPreviewTune(tuneToPlay);
setIsPlaying(true);
// Set up an event listener to detect when audio context is closed or ends
const estimatedDuration = estimateTuneDuration(tuneToPlay);
setTimeout(() => {
setIsPlaying(false);
}, estimatedDuration + 500); // Add a small buffer
} catch (err) {
console.error("Error playing tune:", err);
setErrorMessage(`Error playing tune: ${err.message || 'Unknown error'}`);
setErrorMessage(t('edit.error_playing', { error: err.message || t('edit.unknown') }));
setIsPlaying(false);
}
};
@@ -224,7 +226,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
const isValidFormat = stringValue.includes(':') && stringValue.split(':').length === 3;
if (!isValidFormat) {
setErrorMessage('Invalid RTTTL format! Format should be: name:defaults:notes');
setErrorMessage(t('edit.rtttl_invalid'));
return false;
}
@@ -275,13 +277,13 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
// Validate against min/max if they exist
if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) {
setIsValid(false);
setErrorMessage(`Value must be between ${min !== null ? min : '-∞'} and ${max !== null ? max : '∞'}`);
setErrorMessage(t('edit.value_range', { min: min !== null ? min : '-∞', max: max !== null ? max : '∞' }));
} else {
setIsValid(true);
setErrorMessage('');
}
}
setValue(newValue);
};
@@ -322,7 +324,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
// Validate against min/max if they exist
if ((min !== null && numericValue < min) || (max !== null && numericValue > max)) {
setIsValid(false);
setErrorMessage(`Value must be between ${min !== null ? min : '-∞'} and ${max !== null ? max : '∞'}`);
setErrorMessage(t('edit.value_range', { min: min !== null ? min : '-∞', max: max !== null ? max : '∞' }));
} else {
setIsValid(true);
setErrorMessage('');
@@ -340,14 +342,14 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}>
<FormControl fullWidth margin="dense">
<InputLabel id="rtttl-preset-label">Select Preset Tune</InputLabel>
<InputLabel id="rtttl-preset-label">{t('edit.select_preset')}</InputLabel>
<Select
labelId="rtttl-preset-label"
value={selectedPreset}
onChange={(e) => setSelectedPreset(e.target.value)}
>
<MenuItem value="" disabled>
<em>Choose a preset tune</em>
<em>{t('edit.choose_preset')}</em>
</MenuItem>
{Object.entries(rtttlPresets).map(([name, tune]) => (
<MenuItem key={name} value={tune}>
@@ -363,24 +365,24 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
disabled={!selectedPreset}
sx={{ mb: 1 }}
>
Apply
{t('edit.apply')}
</Button>
</Box>
<Box sx={{ position: 'relative' }}>
<TextField
label="RTTTL Tune"
label={t('edit.rtttl_tune')}
value={value || ""}
onChange={(e) => handleValueChange(e.target.value)}
fullWidth
margin="dense"
multiline
rows={3}
placeholder="Format: name:d=duration,o=octave,b=bpm:notes"
placeholder={t('edit.rtttl_placeholder')}
error={!isValid && value !== ''}
// Remove helperText to avoid layout issues
/>
<Tooltip title={isPlaying ? "Stop tune" : "Play tune"}>
<Tooltip title={isPlaying ? t('edit.stop_tune') : t('edit.play_tune')}>
<IconButton
size="small"
color={isPlaying ? "secondary" : "primary"}
@@ -401,21 +403,21 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
{/* Add a simple instruction text below the field */}
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
Enter RTTTL format tune or select a preset
{t('edit.rtttl_instruction')}
</Typography>
<Divider />
<Box sx={{ bgcolor: 'action.hover', p: 1, borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>
RTTTL Format Guide
{t('edit.rtttl_guide_title')}
</Typography>
<Box sx={{ mt: 0.5 }}>
<Typography variant="caption" display="block"> d=duration (1=whole, 2=half, 4=quarter, 8=eighth, 16=16th note)</Typography>
<Typography variant="caption" display="block"> o=octave (4-7 where 5 is default)</Typography>
<Typography variant="caption" display="block"> b=tempo (beats per minute)</Typography>
<Typography variant="caption" display="block"> Notes are: c, c#, d, d#, e, f, f#, g, g#, a, a#, b or h</Typography>
<Typography variant="caption" display="block"> Example: Beep:d=4,o=5,b=120:c</Typography>
<Typography variant="caption" display="block"> {t('edit.rtttl_guide_duration')}</Typography>
<Typography variant="caption" display="block"> {t('edit.rtttl_guide_octave')}</Typography>
<Typography variant="caption" display="block"> {t('edit.rtttl_guide_tempo')}</Typography>
<Typography variant="caption" display="block"> {t('edit.rtttl_guide_notes')}</Typography>
<Typography variant="caption" display="block"> {t('edit.rtttl_guide_example')}</Typography>
</Box>
</Box>
</Box>
@@ -431,7 +433,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
if (param.fields.value.msg.fields.boolean_value) {
return (
<Box sx={{ display: 'flex', alignItems: 'center'}}>
<Typography variant="body2" sx={{ mr: 2 }}>Enable/Disable:</Typography>
<Typography variant="body2" sx={{ mr: 2 }}>{t('edit.enable_disable')}</Typography>
<Checkbox
checked={value === 1 || value === true}
onChange={(e) => setValue(e.target.checked ? 1 : 0)}
@@ -462,7 +464,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
return (
<TextField
label="New Value"
label={t('edit.new_value')}
value={value}
type="number"
inputProps={{
@@ -472,8 +474,8 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
onChange={(e) => handleValueChange(e.target.value)}
fullWidth
margin="dense"
helperText={isOutOfBounds ?
`Value must be between ${min !== "" ? min : '-∞'} and ${max !== "" ? max : '∞'}` :
helperText={isOutOfBounds ?
t('edit.value_range', { min: min !== "" ? min : '-∞', max: max !== "" ? max : '∞' }) :
null
}
/>
@@ -506,8 +508,8 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
const renderParamNameField = (name) => (
<TextField
label="Parameter Name"
value={name || "Unknown"}
label={t('edit.param_name')}
value={name || t('edit.unknown')}
InputProps={{
readOnly: true,
}}
@@ -542,7 +544,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
/>
) : (
<Typography variant="body2">
{value !== undefined && value !== "" && value !== null ? value : "Unknown"}
{value !== undefined && value !== "" && value !== null ? value : t('edit.unknown')}
</Typography>
)}
</Box>
@@ -554,7 +556,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
onClose={onClose}
sx={{ '& .MuiDialog-paper': { minWidth: isRTTTLEditor ? '600px' : '400px' } }}
>
<DialogTitle>Edit Parameter</DialogTitle>
<DialogTitle>{t('edit.title')}</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'flex-end' }}>
@@ -565,7 +567,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
{isString && !isRTTTLEditor && (
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
<TextField
label="String Value"
label={t('edit.string_value')}
value={value || ""}
onChange={(e) => setValue(e.target.value)}
fullWidth
@@ -580,7 +582,7 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
{paramName === "STARTUP_TUNE" && isString ? (
renderInfoField("Current RTTTL", (() => {
renderInfoField(t('edit.current_rtttl'), (() => {
try {
// Get the binary string value
const binaryString = paramValueField.toString();
@@ -595,27 +597,27 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
return AM32_Rtttl.from_am32_startup_melody(binaryData, "Tune");
} catch (err) {
console.error("Error converting binary data to RTTTL:", err);
return "Error parsing melody data";
return t('edit.error_parsing_melody');
}
})(), true) // Pass true to indicate this is an RTTTL value
) : (
renderInfoField(
"Current Value",
isBoolean
? (paramValueField.value ? "True" : "False")
t('edit.current_value'),
isBoolean
? (paramValueField.value ? t('edit.true') : t('edit.false'))
: isString
? paramValueField.toString()
: paramValueField.value
)
)}
{/* Only show default value when not STARTUP_TUNE */}
{paramName !== "STARTUP_TUNE" && renderInfoField("Default Value", isBoolean ? (paramDefaultValue ? "True" : "False") : paramDefaultValue)}
{paramName !== "STARTUP_TUNE" && renderInfoField(t('edit.default_value'), isBoolean ? (paramDefaultValue ? t('param.true') : t('param.false')) : paramDefaultValue)}
</Box>
{!isBoolean && !isString && !isRTTTLEditor && (
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
{renderInfoField("Min Value", paramMinValue)}
{renderInfoField("Max Value", paramMaxValue)}
{renderInfoField(t('edit.min_value'), paramMinValue)}
{renderInfoField(t('edit.max_value'), paramMaxValue)}
</Box>
)}
{errorMessage && (
@@ -629,14 +631,14 @@ const EditParamModal = ({ open, onClose, nodeId, paramIndex }) => {
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="secondary">
Cancel
{t('edit.cancel')}
</Button>
<Button
onClick={handleSave}
color="primary"
disabled={!isValid}
>
Save
{t('edit.save')}
</Button>
</DialogActions>
</Dialog>

View File

@@ -4,6 +4,7 @@ import PanToolIcon from '@mui/icons-material/PanTool';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import dronecan from './dronecan';
import { useTranslation } from './i18n/LanguageContext';
const commandValueType = new dronecan.DSDL.uavcan_equipment_esc_RawCommand().fields.cmd.value_type;
const CMD_MAX = Number(commandValueType.value_range.max);
@@ -27,6 +28,7 @@ const EscPanel = () => {
const [sendArming, setSendArming] = useState(false);
const [broadcastRate, setBroadcastRate] = useState(10); // Changed default to 10
const [isPaused, setIsPaused] = useState(false); // New state for pause toggle
const { t } = useTranslation();
// Add toggle pause function
const togglePause = () => {
@@ -269,7 +271,7 @@ const EscPanel = () => {
<Toolbar variant="dense" sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mr: 1 }}>
<Typography variant="body2" sx={{ mr: 1 }}>Channels:</Typography>
<Typography variant="body2" sx={{ mr: 1 }}>{t('esc.channels')}</Typography>
<TextField
type="number"
size="small"
@@ -302,7 +304,7 @@ const EscPanel = () => {
fontSize: '0.6rem'
}}
>
REMOVE PROPELLERS!
{t('esc.remove_propellers')}
</Typography>
</Box>
@@ -317,27 +319,27 @@ const EscPanel = () => {
sx={{ p: 0.5 }}
/>
}
label={<Typography variant="body2">Send Safety</Typography>}
label={<Typography variant="body2">{t('esc.send_safety')}</Typography>}
labelPlacement="start"
sx={{ ml: 0, mr: 1 }}
/>
<FormControlLabel
<FormControlLabel
control={
<Checkbox
checked={sendArming}
onChange={(e) => setSendArming(e.target.checked)}
<Checkbox
checked={sendArming}
onChange={(e) => setSendArming(e.target.checked)}
size="small"
sx={{ p: 0.5 }}
/>
}
label={<Typography variant="body2">Send Arming</Typography>}
}
label={<Typography variant="body2">{t('esc.send_arming')}</Typography>}
labelPlacement="start"
sx={{ ml: 0, mr: 2 }}
/>
{/* Broadcast Rate moved to the right */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ mr: 1 }}>Broadcast Rate:</Typography>
<Typography variant="body2" sx={{ mr: 1 }}>{t('esc.broadcast_rate')}</Typography>
<TextField
type="number"
size="small"
@@ -403,22 +405,22 @@ const EscPanel = () => {
flexGrow: 1,
}}>
<Box>
<Typography variant="body2" color="textSecondary">Index: {esc.esc_index}</Typography>
<Typography variant="body2" color="textSecondary">Err: {esc.error_count !== null ? esc.error_count : "NC"}</Typography>
<Typography variant="body2" color="textSecondary">{t('esc.index')} {esc.esc_index}</Typography>
<Typography variant="body2" color="textSecondary">{t('esc.error')} {esc.error_count !== null ? esc.error_count : t('esc.nc')}</Typography>
<Typography variant="body2" color="textSecondary">
Temp: {esc.temperature !== null ? `${(esc.temperature - 273.15).toFixed(1)} °C` : "NC"}
{t('esc.temp')} {esc.temperature !== null ? `${(esc.temperature - 273.15).toFixed(1)} °C` : t('esc.nc')}
</Typography>
<Typography variant="body2" color="textSecondary">
Volt: {esc.voltage !== null ? `${esc.voltage.toFixed(2)} V` : "NC"}
{t('esc.volt')} {esc.voltage !== null ? `${esc.voltage.toFixed(2)} V` : t('esc.nc')}
</Typography>
<Typography variant="body2" color="textSecondary">
Curr: {esc.current !== null ? `${esc.current.toFixed(2)} A` : "NC"}
{t('esc.curr')} {esc.current !== null ? `${esc.current.toFixed(2)} A` : t('esc.nc')}
</Typography>
<Typography variant="body2" color="textSecondary">
RPM: {esc.rpm !== null ? Math.round(esc.rpm) : "NC"}
{t('esc.rpm')} {esc.rpm !== null ? Math.round(esc.rpm) : t('esc.nc')}
</Typography>
<Typography variant="body2" color="textSecondary">
RAT: {esc.power_rating_pct !== null ? `${esc.power_rating_pct.toFixed(1)} %` : "NC"}
{t('esc.rat')} {esc.power_rating_pct !== null ? `${esc.power_rating_pct.toFixed(1)} %` : t('esc.nc')}
</Typography>
</Box>
@@ -446,7 +448,7 @@ const EscPanel = () => {
fullWidth
size="small"
>
Stop
{t('esc.stop')}
</Button>
</Box>
</Box>
@@ -493,7 +495,7 @@ const EscPanel = () => {
}}>
<Box sx={{ p: 1, border: '1px solid #ddd', borderRadius: 1}}>
<Typography variant="body2" color="textSecondary">
cmd: [{getScaledCommands(thrustValues).join(', ')}]
{t('esc.cmd')} [{getScaledCommands(thrustValues).join(', ')}]
</Typography>
</Box>
@@ -504,7 +506,7 @@ const EscPanel = () => {
startIcon={<PanToolIcon />}
onClick={handleStopAll}
>
Stop All
{t('esc.stop_all')}
</Button>
</Box>
</Box>

View File

@@ -1,10 +1,11 @@
import React, { useState } from 'react';
import {
Dialog, DialogTitle, DialogContent, DialogActions,
import {
Dialog, DialogTitle, DialogContent, DialogActions,
Button, Typography, LinearProgress, Box, Alert
} from '@mui/material';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import FileServer from './FileServer';
import { useTranslation } from './i18n/LanguageContext';
const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
const [firmwareFile, setFirmwareFile] = useState(null);
@@ -12,6 +13,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
const [updateProgress, setUpdateProgress] = useState(0);
const [updateStatus, setUpdateStatus] = useState(null); // 'idle', 'updating', 'success', 'error'
const [statusMessage, setStatusMessage] = useState('');
const { t } = useTranslation();
const handleFileChange = (event) => {
const file = event.target.files[0];
@@ -21,7 +23,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
const fileExtension = file.name.split('.').pop().toLowerCase();
if (fileExtension !== 'bin' && fileExtension !== 'hex') {
setUpdateStatus('error');
setStatusMessage('Invalid file type. Please select a .bin or .hex firmware file.');
setStatusMessage(t('fw.invalid_file'));
return;
}
@@ -42,7 +44,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
.catch(error => {
console.error('Error loading firmware:', error);
setUpdateStatus('error');
setStatusMessage('Failed to load firmware file');
setStatusMessage(t('fw.load_failed'));
});
};
@@ -79,13 +81,13 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
if (!localNode) {
setUpdateStatus('error');
setStatusMessage('Local node not available');
setStatusMessage(t('fw.node_unavailable'));
return;
}
// Update UI state
setUpdateStatus('updating');
setStatusMessage('Starting firmware update...');
setStatusMessage(t('fw.starting'));
setUpdateProgress(0);
// Register a progress callback with the FileServer
@@ -95,12 +97,12 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
setUpdateProgress(progress * 100);
// Update status message
setStatusMessage(`Updating firmware: ${Math.round(progress * 100)}% (${offset}/${total} bytes)`);
setStatusMessage(t('fw.updating', { progress: Math.round(progress * 100), offset: offset, total: total }));
// When complete
if (eof) {
setUpdateStatus('success');
setStatusMessage('Firmware update completed successfully!');
setStatusMessage(t('fw.success'));
// Clean up progress tracking
setTimeout(() => {
@@ -123,7 +125,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
if (!msg || msg.fields.error.value > 0) {
// Handle update failure
setUpdateStatus('error');
setStatusMessage(`Update failed: code: ${msg.fields.error.value} ${msg.fields.optional_error_message.toString() || 'Unknown error'}`);
setStatusMessage(t('fw.update_failed', { code: msg.fields.error.value, message: msg.fields.optional_error_message.toString() || t('edit.unknown') }));
FileServer.unregisterProgressCallback(firmwarePath);
} else {
setUpdateStatus('updating');
@@ -134,16 +136,16 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
} catch (error) {
console.error('Error initiating firmware update:', error);
setUpdateStatus('error');
setStatusMessage(`Failed to start update: ${error.message || 'Unknown error'}`);
setStatusMessage(t('fw.start_failed', { error: error.message || t('edit.unknown') }));
}
};
return (
<Dialog open={open} onClose={updateStatus === 'updating' ? null : onClose} maxWidth="sm" fullWidth>
<DialogTitle>Firmware Update</DialogTitle>
<DialogTitle>{t('fw.title')}</DialogTitle>
<DialogContent>
<Typography variant="body2" gutterBottom>
Please select the firmware file (.bin|.hex) to upload to node {targetNodeId}.
{t('fw.select_hint', { id: targetNodeId })}
</Typography>
<Button
@@ -152,7 +154,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
startIcon={<FileUploadIcon />}
disabled={updateStatus === 'updating'}
>
Select Firmware File
{t('fw.select_file')}
<input
type="file"
hidden
@@ -164,7 +166,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
{firmwareFile && (
<Typography variant="body2" sx={{ mt: 2 }}>
Selected File: {firmwareFile.name}
{fileContent && ` (${fileContent.size} bytes)`}
{fileContent && ` (${fileContent.size} ${t('fw.bytes', { defaultValue: 'bytes' })})`}
</Typography>
)}
@@ -183,13 +185,13 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
{updateStatus === 'error' && (
<Alert severity="error" sx={{ mt: 2 }}>
{statusMessage || 'An error occurred during the update.'}
{statusMessage || t('fw.error_occurred')}
</Alert>
)}
{updateStatus === 'success' && (
<Alert severity="success" sx={{ mt: 2 }}>
{statusMessage || 'Firmware update completed successfully!'}
{statusMessage || t('fw.success')}
</Alert>
)}
</DialogContent>
@@ -197,14 +199,14 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
{updateStatus !== 'updating' ? (
<>
<Button onClick={onClose} color="secondary">
Cancel
{t('fw.cancel')}
</Button>
<Button
<Button
onClick={handleUpdate}
color="primary"
color="primary"
disabled={!firmwareFile || updateStatus === 'updating'}
>
Update
{t('fw.update')}
</Button>
</>
) : (
@@ -213,7 +215,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
disabled={updateProgress < 100}
onClick={onClose}
>
{updateProgress < 100 ? 'Updating...' : 'Close'}
{updateProgress < 100 ? t('fw.updating_ellipsis') : t('fw.close')}
</Button>
)}
</DialogActions>

View File

@@ -3,10 +3,12 @@ import { TableContainer, Table, TableHead, TableBody, TableRow, TableCell, Paper
import PauseIcon from '@mui/icons-material/Pause';
import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { useTranslation } from './i18n/LanguageContext';
const NodeLogs = () => {
const [logs, setLogs] = useState([]);
const [paused, setPaused] = useState(false);
const { t } = useTranslation();
useEffect(() => {
const localNode = window.localNode;
@@ -57,7 +59,7 @@ const NodeLogs = () => {
alignItems: 'center',
height: 20
}} margin={1}>
<Typography variant="caption" flexGrow={1}>Logs</Typography>
<Typography variant="caption" flexGrow={1}>{t('logs.title')}</Typography>
<Box sx={{ display: 'flex', gap: 0.5 }}>
<IconButton
sx={{
@@ -90,11 +92,11 @@ const NodeLogs = () => {
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{ width: '5%' }}>NID</TableCell>
<TableCell sx={{ width: '15%' }}>Time</TableCell>
<TableCell sx={{ width: '10%' }}>Level</TableCell>
<TableCell sx={{ width: '10%' }}>Source</TableCell>
<TableCell sx={{ width: '60%' }}>Text</TableCell>
<TableCell sx={{ width: '5%' }}>{t('logs.col_nid')}</TableCell>
<TableCell sx={{ width: '15%' }}>{t('logs.col_time')}</TableCell>
<TableCell sx={{ width: '10%' }}>{t('logs.col_level')}</TableCell>
<TableCell sx={{ width: '10%' }}>{t('logs.col_source')}</TableCell>
<TableCell sx={{ width: '60%' }}>{t('logs.col_text')}</TableCell>
</TableRow>
</TableHead>
<TableBody>

View File

@@ -11,6 +11,7 @@ import FileUploadIcon from '@mui/icons-material/FileUpload';
import EditIcon from '@mui/icons-material/Edit';
import ParamEditorSelector from './ParamEditors/ParamEditorSelector';
import AM32_Rtttl from './am32_rtttl';
import { useTranslation } from './i18n/LanguageContext';
const OPCODE_SAVE = 0;
const OPCODE_ERASE = 1;
@@ -21,6 +22,7 @@ const NodeParam = ({ nodeId, nodes }) => {
const [paramsUpdateTimestamp, setParamsUpdateTimestamp] = useState(0);
const [fetchingParams, setFetchingParams] = useState(false);
const fetchTimeoutRef = useRef(null);
const { t } = useTranslation();
if (!nodeId) return null;
const node = nodes[nodeId];
@@ -97,9 +99,9 @@ const NodeParam = ({ nodeId, nodes }) => {
// Function to format boolean values visually
const formatBooleanValue = (value) => {
if (value === 'True') {
return <Chip size="small" label="True" color="success" />;
return <Chip size="small" label={t('param.true')} color="success" />;
} else if (value === 'False') {
return <Chip size="small" label="False" color="error" />;
return <Chip size="small" label={t('param.false')} color="error" />;
}
return value;
};
@@ -214,15 +216,15 @@ const NodeParam = ({ nodeId, nodes }) => {
} else if (param.fields.value.msg.fields.boolean_value !== undefined) {
paramTypeDisplay = 'boolean';
if (param.fields.value.msg.fields.boolean_value.value === 0) {
paramValueDisplay = 'Disabled';
paramValueDisplay = t('param.disabled');
} else if (param.fields.value.msg.fields.boolean_value.value === 1) {
paramValueDisplay = 'Enabled';
paramValueDisplay = t('param.enabled');
}
if (paramDefaultValue === 0) {
paramDefaultValueDisplay = 'Disabled';
paramDefaultValueDisplay = t('param.disabled');
} else {
paramDefaultValueDisplay = 'Enabled';
paramDefaultValueDisplay = t('param.enabled');
}
paramMinValueDisplay = "";
paramMaxValueDisplay = "";
@@ -306,7 +308,7 @@ const NodeParam = ({ nodeId, nodes }) => {
</Typography>
</TableCell>
<TableCell padding="none" align="center">
<Tooltip title="Edit Parameter">
<Tooltip title={t('param.edit_param')}>
<EditIcon fontSize="small" color="primary" />
</Tooltip>
</TableCell>
@@ -322,13 +324,13 @@ const NodeParam = ({ nodeId, nodes }) => {
<Table stickyHeader size="small">
<TableHead>
<TableRow>
<TableCell sx={{width: '5%'}}>Idx</TableCell>
<TableCell sx={{width: '28%'}}>Name</TableCell>
<TableCell sx={{width: '10%'}}>Type</TableCell>
<TableCell sx={{width: '17%'}}>Value</TableCell>
<TableCell sx={{width: '10%'}}>Default</TableCell>
<TableCell sx={{width: '10%'}}>Min</TableCell>
<TableCell sx={{width: '10%'}}>Max</TableCell>
<TableCell sx={{width: '5%'}}>{t('param.col_idx')}</TableCell>
<TableCell sx={{width: '28%'}}>{t('param.col_name')}</TableCell>
<TableCell sx={{width: '10%'}}>{t('param.col_type')}</TableCell>
<TableCell sx={{width: '17%'}}>{t('param.col_value')}</TableCell>
<TableCell sx={{width: '10%'}}>{t('param.col_default')}</TableCell>
<TableCell sx={{width: '10%'}}>{t('param.col_min')}</TableCell>
<TableCell sx={{width: '10%'}}>{t('param.col_max')}</TableCell>
<TableCell sx={{width: '10%'}}></TableCell>
</TableRow>
</TableHead>
@@ -357,7 +359,7 @@ const NodeParam = ({ nodeId, nodes }) => {
sx={{ width: 80, mr: 2, ml: 0.5 }}
variant="caption"
>
Parameters
{t('param.title')}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'space-between', flexDirection: 'row', border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5, mr: 2 }}>
<Button
@@ -367,7 +369,7 @@ const NodeParam = ({ nodeId, nodes }) => {
startIcon={<SyncIcon />}
disabled={fetchingParams}
>
{fetchingParams ? 'Fetching...' : 'Fetch All'}
{fetchingParams ? t('param.fetching') : t('param.fetch_all')}
</Button>
<Button
onClick={handleSaveParams}
@@ -377,7 +379,7 @@ const NodeParam = ({ nodeId, nodes }) => {
startIcon={<SaveIcon />}
disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0}
>
Store All
{t('param.store_all')}
</Button>
<Button
onClick={handleEraseParams}
@@ -385,7 +387,7 @@ const NodeParam = ({ nodeId, nodes }) => {
color="warning"
startIcon={<AutoFixNormalIcon />}
>
Erase All
{t('param.erase_all')}
</Button>
</Box>
<Box sx={{ flexGrow: 1 }}></Box>
@@ -398,7 +400,7 @@ const NodeParam = ({ nodeId, nodes }) => {
onClick={handleDownloadParams}
disabled={!localNode.nodeParams[nodeId] || Object.keys(localNode.nodeParams[nodeId]).length === 0}
>
Download
{t('param.download')}
</Button>
<Button
variant="outlined"
@@ -406,7 +408,7 @@ const NodeParam = ({ nodeId, nodes }) => {
startIcon={<FileUploadIcon />}
disabled={!nodeId} // Disable Load button if no node is selected
>
Load
{t('param.load')}
</Button>
</Box>
</Box>

View File

@@ -7,6 +7,7 @@ import CableIcon from '@mui/icons-material/Cable';
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
import FirmwareUpdateModal from './FirmwareUpdateModal';
import ConfirmRestartModal from './ConfirmRestartModal';
import { useTranslation } from './i18n/LanguageContext';
const VendorSpecificCodeDisplay = (code) => {
code = Math.max(0, Math.floor(code) & 0xFFFF);
@@ -19,6 +20,7 @@ const VendorSpecificCodeDisplay = (code) => {
const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEditorEnable }) => {
const [firmwareModalOpen, setFirmwareModalOpen] = useState(false);
const [restartModalOpen, setRestartModalOpen] = useState(false);
const { t } = useTranslation();
useEffect(() => {
const localNode = window.localNode;
@@ -71,16 +73,16 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
>
<Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption">
Node Properties
{t('props.title')}
</Typography>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Typography variant="caption">Multi Node Editor</Typography>
<Typography variant="caption">{t('props.multi_editor')}</Typography>
<Switch checked={multiNodeEditorEnable} onChange={(e) => { setMultiNodeEditorEnable(e.target.checked) }} />
</Stack>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label="Node ID"
label={t('props.node_id')}
value={nodeId}
InputProps={{
readOnly: true,
@@ -88,7 +90,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }}
/>
<TextField
label="Name"
label={t('props.name')}
value={name}
fullWidth
InputProps={{
@@ -98,7 +100,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label="Mode"
label={t('props.mode')}
value={mode}
fullWidth
InputProps={{
@@ -107,7 +109,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }}
/>
<TextField
label="Health"
label={t('props.health')}
value={health}
fullWidth
InputProps={{
@@ -116,7 +118,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }}
/>
<TextField
label="Uptime"
label={t('props.uptime')}
value={uptime}
fullWidth
InputProps={{
@@ -126,7 +128,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label="Vendor Specific Status Code"
label={t('props.vendor_code')}
fullWidth
value={vendor_specific_status_code}
InputProps={{
@@ -136,7 +138,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label="Software Version"
label={t('props.sw_version')}
fullWidth
value={softwareVersion}
InputProps={{
@@ -145,7 +147,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }}
/>
<TextField
label="CRC64"
label={t('props.crc64')}
fullWidth
value={softwareCrc64}
InputProps={{
@@ -154,7 +156,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }}
/>
<TextField
label="VCS Commit"
label={t('props.vcs_commit')}
fullWidth
value={softwareVcsCommit}
InputProps={{
@@ -164,7 +166,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label="Hardware Version"
label={t('props.hw_version')}
value={hardwareVersion}
InputProps={{
readOnly: true,
@@ -172,7 +174,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ mr: 0.5 }}
/>
<TextField
label="UID"
label={t('props.uid')}
fullWidth
value={hardwareUID}
InputProps={{
@@ -182,7 +184,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label="Cert. of authenticity"
label={t('props.certificate')}
fullWidth
value={certificateOfAuthenticity}
InputProps={{
@@ -195,7 +197,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
sx={{ width: 80, mr: 2 }}
variant="caption"
>
Node Controls
{t('props.controls')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', flexGrow: 1, border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5 }}>
<Button
@@ -205,14 +207,14 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
startIcon={<PowerSettingsNewIcon />}
onClick={() => setRestartModalOpen(true)}
>
Restart
{t('props.restart')}
</Button>
<Button
sx={{ mr: 1 }}
variant="outlined"
startIcon={<CableIcon />}
>
Get Transport Stats
{t('props.transport_stats')}
</Button>
<Box sx={{ flexGrow: 1 }}></Box>
<Button
@@ -220,7 +222,7 @@ const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEdit
startIcon={<SystemUpdateAltIcon />}
onClick={() => setFirmwareModalOpen(true)}
>
Update Firmware
{t('props.update_firmware')}
</Button>
</Box>
</Box>

View File

@@ -1,10 +1,12 @@
import React, { useState } from 'react';
import { Box, Button, Menu, MenuItem, Divider } from '@mui/material';
import VideogameAssetIcon from '@mui/icons-material/VideogameAsset';
import { useTranslation } from './i18n/LanguageContext';
const PanelsMenu = ({openWindow}) => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const { t } = useTranslation();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
@@ -42,7 +44,7 @@ const PanelsMenu = ({openWindow}) => {
color="default"
startIcon={<VideogameAssetIcon />}
>
Panels
{t('panels.title')}
</Button>
<Menu
elevation={0}
@@ -59,10 +61,10 @@ const PanelsMenu = ({openWindow}) => {
onClose={handleClose}
>
<MenuItem onClick={handleEscPanelClick} disableRipple>
ESC
{t('panels.esc')}
</MenuItem>
<MenuItem onClick={handleActuatorPanelClick} disableRipple>
Actuator
{t('panels.actuator')}
</MenuItem>
</Menu>
</Box>

View File

@@ -7,6 +7,7 @@ import DronecanLogo from './image/dronecan_logo.png';
import { toYaml } from './dronecan/message_format_utils';
import theme from './theme';
import { useTranslation } from './i18n/LanguageContext';
import './css/subscriber.css';
const SubscriberWindow = () => {
@@ -19,9 +20,10 @@ const SubscriberWindow = () => {
const [recordingSet, setRecordingSet] = useState([]);
const [displayRecordText, setDisplayRecordText] = useState("");
const [recording, setRecording] = useState(true);
const { t } = useTranslation();
if (window.opener === null) {
return "Not Allowed To Open Directly";
return t('sub.not_allowed');
}
useEffect(() => {
@@ -61,7 +63,7 @@ const SubscriberWindow = () => {
}
const msg = transfer.payload;
const msgObj = msg.toObj();
let destNodeText = "All";
let destNodeText = t('sub.all');
if (transfer.destNodeId && transfer.destNodeId !== 0) {
destNodeText = `${transfer.destNodeId}`;
}
@@ -148,12 +150,12 @@ const SubscriberWindow = () => {
<Box sx={{display: 'flex', flexDirection: 'row', alignItems: 'center', mr: 0.5}}>
<Box sx={{minWidth: 200, display: 'flex', flexDirection: 'row'}}>
<Box sx={{flexGrow: 1}}></Box>
<Typography variant="caption" mr={0.5}>RX:</Typography>
<Typography variant="caption" mr={0.5}>{t('sub.rx')}</Typography>
<Typography variant="caption" mr={1} sx={{minWidth: 30}}>{totalRX}</Typography>
<Typography variant="caption" mr={0.5}>Rates(Hz):</Typography>
<Typography variant="caption" mr={0.5}>{t('sub.rates')}</Typography>
<Typography variant="caption" mr={1} sx={{minWidth: 30}}>{messageRate.toFixed(0)}</Typography>
</Box>
<Typography variant="caption" mr={1}> Max:</Typography>
<Typography variant="caption" mr={1}>{t('sub.max')}</Typography>
<TextField size="small" sx={{width: 80}} type="number" min={1} max={100} value={maxMessageCount} onChange={updateMaxMessageCount} />
<IconButton size="small" onClick={handleClean}>
<CleaningServicesIcon />

View File

@@ -3,10 +3,12 @@ import { Box, Button, Menu, MenuItem, Divider } from '@mui/material';
import BuildIcon from '@mui/icons-material/Build';
import MessageIcon from '@mui/icons-material/Message';
import SettingsInputCompositeIcon from '@mui/icons-material/SettingsInputComposite';
import { useTranslation } from './i18n/LanguageContext';
const ToolsMenu =({openWindow}) => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const { t } = useTranslation();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
@@ -44,7 +46,7 @@ const ToolsMenu =({openWindow}) => {
color="default"
startIcon={<BuildIcon />}
>
Tools
{t('tools.title')}
</Button>
<Menu
elevation={0}
@@ -61,10 +63,10 @@ const ToolsMenu =({openWindow}) => {
onClose={handleClose}
>
<MenuItem onClick={handleSubscriberClick} disableRipple>
Subscriber
{t('tools.subscriber')}
</MenuItem>
<MenuItem onClick={handleBusMonitorClick} disableRipple>
Bus Monitor
{t('tools.bus_monitor')}
</MenuItem>
</Menu>
</Box>

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
src/i18n/translations.js Normal file
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": "关闭",
};