Files
DroneCan_WebTools/src/NodeProperties.js
2026-05-23 09:31:44 +08:00

243 lines
9.6 KiB
JavaScript

import React, { useEffect, useState } from 'react';
import dronecan from './dronecan';
import { Paper, Box, Typography, TextField, Button, Stack, Switch, TableContainer, Table, TableHead, TableBody, TableRow, TableCell } from '@mui/material';
import { secondsToTime } from './common';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
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);
let decimal = code.toString();
let hex = `0x${code.toString(16).padStart(4, '0')}`;
let binary = `0b${(code >>> 8).toString(2).padStart(8, '0')}_${(code & 255).toString(2).padStart(8, '0')}`;
return `${decimal} | ${hex} | ${binary}`;
};
const NodeProperties = ({ nodeId, nodes, multiNodeEditorEnable, setMultiNodeEditorEnable }) => {
const [firmwareModalOpen, setFirmwareModalOpen] = useState(false);
const [restartModalOpen, setRestartModalOpen] = useState(false);
const { t } = useTranslation();
useEffect(() => {
const localNode = window.localNode;
const handleNodeParam = (transfer) => {
if (transfer.sourceNodeId !== nodeId) return;
};
localNode.on('uavcan.protocol.param.GetSet.Response', handleNodeParam);
return () => {
localNode.off('uavcan.protocol.param.GetSet.Response', handleNodeParam);
};
}, [nodeId]);
if (!nodeId) return null;
const node = nodes[nodeId];
if (!node) return null;
const handleNodeRestart = (nodeId) => {
const localNode = window.localNode;
localNode.restartNode(nodeId, (transfer) => {
console.log('Restart response:', transfer);
});
};
const handleConfirmRestart = () => {
handleNodeRestart(nodeId);
setRestartModalOpen(false);
};
const status = nodes[nodeId]?.status;
const name = node.name ? node.name : '';
const health = status ? `${status.getConstant('health')} (${status.health})` : '';
const mode = status ? `${status.getConstant('mode')} (${status.mode})` : '';
const uptime = status ? secondsToTime(status.uptime_sec) : 0;
const vendor_specific_status_code = status ? VendorSpecificCodeDisplay(status.vendor_specific_status_code) : 0;
const softwareVersion = node.software_version ? `${node.software_version.major}.${node.software_version.minor}` : '';
const softwareCrc64 = node.software_version ? `0x${node.software_version.image_crc.toString(16).padStart(8, '0')}` : '';
const softwareVcsCommit = node.software_version ? `0x${node.software_version.vcs_commit.toString(16).padStart(4, '0')}` : '';
const hardwareVersion = node.hardware_version ? `${node.hardware_version.major}.${node.hardware_version.minor}` : '';
const hardwareUID = node.hardware_version ? node.hardware_version.unique_id.map((item) => { return item.toString(16).padStart(2, '0') }).join(' ') : '';
const certificateOfAuthenticity = node.certificate_of_authenticity ? node.certificate_of_authenticity : ' ';
return (
<Box
sx={{ flexGrow: 1, bgcolor: 'background.paper', height: 340}}
component={Paper}
p={1}
>
<Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption">
{t('props.title')}
</Typography>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<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={t('props.node_id')}
value={nodeId}
InputProps={{
readOnly: true,
}}
sx={{ mr: 0.5 }}
/>
<TextField
label={t('props.name')}
value={name}
fullWidth
InputProps={{
readOnly: true,
}}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label={t('props.mode')}
value={mode}
fullWidth
InputProps={{
readOnly: true,
}}
sx={{ mr: 0.5 }}
/>
<TextField
label={t('props.health')}
value={health}
fullWidth
InputProps={{
readOnly: true,
}}
sx={{ mr: 0.5 }}
/>
<TextField
label={t('props.uptime')}
value={uptime}
fullWidth
InputProps={{
readOnly: true,
}}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label={t('props.vendor_code')}
fullWidth
value={vendor_specific_status_code}
InputProps={{
readOnly: true,
}}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label={t('props.sw_version')}
fullWidth
value={softwareVersion}
InputProps={{
readOnly: true,
}}
sx={{ mr: 0.5 }}
/>
<TextField
label={t('props.crc64')}
fullWidth
value={softwareCrc64}
InputProps={{
readOnly: true,
}}
sx={{ mr: 0.5 }}
/>
<TextField
label={t('props.vcs_commit')}
fullWidth
value={softwareVcsCommit}
InputProps={{
readOnly: true,
}}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label={t('props.hw_version')}
value={hardwareVersion}
InputProps={{
readOnly: true,
}}
sx={{ mr: 0.5 }}
/>
<TextField
label={t('props.uid')}
fullWidth
value={hardwareUID}
InputProps={{
readOnly: true,
}}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1 }}>
<TextField
label={t('props.certificate')}
fullWidth
value={certificateOfAuthenticity}
InputProps={{
readOnly: true,
}}
/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'row', mt: 1, justifyContent: 'space-between', alignItems: 'center' }}>
<Typography
sx={{ width: 80, mr: 2 }}
variant="caption"
>
{t('props.controls')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', flexGrow: 1, border: 1, borderColor: 'grey.500', borderRadius: 1, p: 0.5 }}>
<Button
sx={{ mr: 1 }}
color="error"
variant="contained"
startIcon={<PowerSettingsNewIcon />}
onClick={() => setRestartModalOpen(true)}
>
{t('props.restart')}
</Button>
<Button
sx={{ mr: 1 }}
variant="outlined"
startIcon={<CableIcon />}
>
{t('props.transport_stats')}
</Button>
<Box sx={{ flexGrow: 1 }}></Box>
<Button
variant="outlined"
startIcon={<SystemUpdateAltIcon />}
onClick={() => setFirmwareModalOpen(true)}
>
{t('props.update_firmware')}
</Button>
</Box>
</Box>
<FirmwareUpdateModal
open={firmwareModalOpen}
onClose={() => setFirmwareModalOpen(false)}
targetNodeId={nodeId}
/>
<ConfirmRestartModal
open={restartModalOpen}
onClose={() => setRestartModalOpen(false)}
onConfirm={handleConfirmRestart}
/>
</Box>
);
};
export default NodeProperties;