import React, { useState, useEffect, useRef } from 'react'; import { Box, Paper, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Toolbar, AppBar, Button, FormControlLabel, Switch, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; import ClearIcon from '@mui/icons-material/Clear'; 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'; const BusMonitor = () => { const [transfers, setTransfers] = useState([]); const [isPaused, setIsPaused] = useState(false); const [autoScroll, setAutoScroll] = useState(true); const [selectedTransfer, setSelectedTransfer] = useState(null); const [detailsOpen, setDetailsOpen] = useState(false); const [messageYaml, setMessageYaml] = useState(''); const tableContainerRef = useRef(null); const maxTransfers = 1000; // Maximum number of transfers to store // Handle row click to show details const handleRowClick = (transfer) => { setSelectedTransfer(transfer); // Convert the message to YAML format if it has payload 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 += 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`; } setMessageYaml(yamlText); setDetailsOpen(true); }; // Close the details dialog const handleCloseDetails = () => { setDetailsOpen(false); }; useEffect(() => { const localNode = window.opener.localNode; if (!localNode) return; // Function to handle incoming transfers const handleTransfer = (transfer, direction) => { if (isPaused) return; const now = new Date(); const timeString = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 }); // Format payload bytes as hex let hexData = ''; if (transfer._payloadBytes && transfer._payloadBytes.length > 0) { hexData = Array.from(transfer._payloadBytes) .map(byte => byte.toString(16).padStart(2, '0').toUpperCase()) .join(' '); } setTransfers(prevTransfers => { const newTransfers = [...prevTransfers, { timestamp: timeString, timestampMs: now.getTime(), transfer: transfer, dataType: transfer.payload ? transfer.payload.name : 'Unknown', sourceNodeId: transfer.sourceNodeId, destNodeId: transfer.destNodeId, frameId: `0x${transfer.messageId.toString(16).toUpperCase()}`, data: transfer.payload, _payloadBytes: transfer._payloadBytes, hexData: hexData, direction: direction, rawData: transfer.payload ? JSON.stringify(transfer.payload.toObj()) : '{}', id: now.getTime() + Math.random().toString(36).substring(2, 9) }]; // Keep only the last maxTransfers items if (newTransfers.length > maxTransfers) { return newTransfers.slice(newTransfers.length - maxTransfers); } return newTransfers; }); }; // Subscribe to both TX and RX transfers const handleTransferRx = (transfer) => handleTransfer(transfer, 'RX'); const handleTransferTx = (transfer) => handleTransfer(transfer, 'TX'); localNode.on('transfer-rx', handleTransferRx); localNode.on('transfer-tx', handleTransferTx); return () => { // Clean up subscriptions localNode.off('transfer-rx', handleTransferRx); localNode.off('transfer-tx', handleTransferTx); }; }, [isPaused, maxTransfers]); useEffect(() => { if (autoScroll && tableContainerRef.current && !isPaused) { tableContainerRef.current.scrollTop = tableContainerRef.current.scrollHeight; } }, [transfers, autoScroll, isPaused]); const handleClearTransfers = () => { setTransfers([]); }; const togglePause = () => { setIsPaused(!isPaused); }; const toggleAutoScroll = () => { setAutoScroll(!autoScroll); }; const exportToCSV = () => { const headers = ['Direction', 'Timestamp', 'CAN ID (Hex)', 'Hex Data', 'Src Node ID', 'Dst Node ID', 'Data Type', 'Raw Data']; const csvRows = [ headers.join(','), ...transfers.map(transfer => { // Format payload bytes for CSV export let hexData = ''; if (transfer._payloadBytes && transfer._payloadBytes.length > 0) { hexData = Array.from(transfer._payloadBytes) .map(byte => byte.toString(16).padStart(2, '0').toUpperCase()) .join(' '); } return [ transfer.direction, transfer.timestamp, transfer.frameId, `"${hexData}"`, transfer.sourceNodeId, transfer.destNodeId, transfer.dataType, `"${transfer.rawData.replace(/"/g, '""')}"` ].join(','); }) ]; const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', `bus-monitor-export-${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.csv`); link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; return ( Bus Monitor } label={Auto Scroll} labelPlacement="start" /> {isPaused ? : } Dir Time CAN ID Hex Data Src Dst Data Type {transfers.map((item) => ( handleRowClick(item)} sx={{ backgroundColor: item.direction === 'TX' ? 'rgba(200, 250, 200, 0.05)' : 'rgba(200, 200, 255, 0.05)', cursor: 'pointer', '&:hover': { backgroundColor: item.direction === 'TX' ? 'rgba(200, 250, 200, 0.2)' : 'rgba(200, 200, 255, 0.2)', } }} > {item.direction} {item.timestamp} {item.frameId} {item.hexData} {item.sourceNodeId} {item.destNodeId || ''} {item.dataType} ))}
Showing {transfers.length} of max {maxTransfers} transfers {isPaused && ( PAUSED )} Message Details {selectedTransfer && ( {selectedTransfer.dataType} - {selectedTransfer.frameId} )}