package set; import chuankou.SerialPortNativeLoader; import chuankou.SerialPortPreferences; import chuankou.SerialPortService; import chuankou.sendmessage; import com.fazecast.jSerialComm.SerialPort; import ui.UIConfig; import javax.swing.*; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; import zhuye.buttonset; /** * 系统调试对话框。 */ public class debug extends JDialog { private static final long serialVersionUID = 1L; private static final Dimension DEFAULT_SIZE = new Dimension(UIConfig.DIALOG_WIDTH, UIConfig.DIALOG_HEIGHT); private final JTextArea logArea = new JTextArea(); private final SerialPortService serialService = sendmessage.getActiveService(); private final JComboBox portComboBox = new JComboBox<>(); private final JComboBox baudComboBox = new JComboBox<>(new Integer[]{115200, 921600, 57600}); private JButton connectButton; private JButton pauseButton; private JButton closeButton; private JButton clearButton; private final JCheckBox hexDisplayCheckBox = new JCheckBox("HEX显示"); private final JCheckBox autoConnectCheckBox = new JCheckBox("启动时连接"); private final JLabel statusLabel = new JLabel("未连接"); private final SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm:ss"); private final Color themeColor; private boolean isConnected = false; private boolean isPaused = false; private String activePortName; private int activeBaudRate = 115200; private String preferredPortName; private boolean suppressPreferenceSync = false; public debug(Window owner, Color themeColor) { super(owner, "系统调试", ModalityType.APPLICATION_MODAL); this.themeColor = themeColor != null ? themeColor : new Color(52, 152, 219); // 确保库已加载 try { SerialPortNativeLoader.ensureLibraryPresent(); } catch (Exception e) { JOptionPane.showMessageDialog(owner, "串口库初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } this.connectButton = buttonset.createStyledButton("连接", this.themeColor); this.pauseButton = buttonset.createStyledButton("暂停显示", new Color(120, 120, 120)); this.closeButton = buttonset.createStyledButton("关闭", new Color(240, 240, 240)); this.clearButton = buttonset.createStyledButton("清空日志", this.themeColor); initializeUI(); loadPreferences(); refreshSerialPorts(); syncConnectionStateFromService(); appendLog("系统调试工具已启动"); } private void initializeUI() { setSize(DEFAULT_SIZE); setPreferredSize(DEFAULT_SIZE); setMinimumSize(DEFAULT_SIZE); setResizable(false); setDefaultCloseOperation(DISPOSE_ON_CLOSE); JPanel contentPane = new JPanel(new BorderLayout(12, 12)); contentPane.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16)); contentPane.setBackground(Color.WHITE); contentPane.add(buildControlPanel(), BorderLayout.NORTH); contentPane.add(buildLogPanel(), BorderLayout.CENTER); contentPane.add(buildActionPanel(), BorderLayout.SOUTH); setContentPane(contentPane); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { detachLogListener(); } @Override public void windowClosed(WindowEvent e) { detachLogListener(); } }); updateControlStates(); } private JPanel buildControlPanel() { JPanel panel = new JPanel(new GridBagLayout()); panel.setBackground(new Color(248, 248, 248)); panel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(new Color(225, 225, 225)), BorderFactory.createEmptyBorder(12, 12, 12, 12))); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.insets = new Insets(0, 0, 10, 10); gbc.anchor = GridBagConstraints.WEST; JLabel portLabel = new JLabel("选择串口"); portLabel.setFont(portLabel.getFont().deriveFont(Font.BOLD, 13f)); gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0; panel.add(portLabel, gbc); portComboBox.setPrototypeDisplayValue("COM000 (USB Serial)"); portComboBox.setFont(portComboBox.getFont().deriveFont(13f)); portComboBox.setPreferredSize(new Dimension(220, 28)); portComboBox.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { SwingUtilities.invokeLater(() -> refreshSerialPorts()); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { // no-op } @Override public void popupMenuCanceled(PopupMenuEvent e) { // no-op } }); portComboBox.addActionListener(e -> handlePortSelectionChange()); gbc.gridx = 1; gbc.gridy = 0; gbc.weightx = 1; panel.add(portComboBox, gbc); connectButton.setFont(connectButton.getFont().deriveFont(Font.BOLD, 13f)); connectButton.setBackground(themeColor); connectButton.setForeground(Color.WHITE); connectButton.setFocusPainted(false); connectButton.setPreferredSize(new Dimension(90, 32)); connectButton.addActionListener(this::handleConnectButton); gbc.gridx = 2; gbc.gridy = 0; gbc.weightx = 0; gbc.insets = new Insets(0, 0, 10, 0); panel.add(connectButton, gbc); JLabel baudLabel = new JLabel("波特率"); baudLabel.setFont(baudLabel.getFont().deriveFont(Font.BOLD, 13f)); gbc.gridx = 0; gbc.gridy = 1; gbc.insets = new Insets(0, 0, 0, 10); panel.add(baudLabel, gbc); baudComboBox.setFont(baudComboBox.getFont().deriveFont(13f)); baudComboBox.setSelectedItem(115200); baudComboBox.setPreferredSize(new Dimension(140, 28)); baudComboBox.addActionListener(e -> handleBaudSelectionChange()); gbc.gridx = 1; gbc.gridy = 1; gbc.weightx = 1; gbc.insets = new Insets(0, 0, 0, 10); panel.add(baudComboBox, gbc); statusLabel.setFont(statusLabel.getFont().deriveFont(Font.PLAIN, 12f)); statusLabel.setForeground(new Color(120, 120, 120)); autoConnectCheckBox.setOpaque(false); autoConnectCheckBox.setFont(autoConnectCheckBox.getFont().deriveFont(Font.PLAIN, 12f)); autoConnectCheckBox.setForeground(new Color(90, 90, 90)); autoConnectCheckBox.addActionListener(e -> handleAutoConnectToggle()); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 3; gbc.weightx = 1; gbc.insets = new Insets(12, 0, 0, 0); panel.add(statusLabel, gbc); gbc.gridy = 3; gbc.insets = new Insets(6, 0, 0, 0); panel.add(autoConnectCheckBox, gbc); return panel; } private JScrollPane buildLogPanel() { logArea.setEditable(false); logArea.setLineWrap(true); logArea.setWrapStyleWord(true); logArea.setFont(new Font("Consolas", Font.PLAIN, 13)); logArea.setMargin(new Insets(10, 10, 10, 10)); logArea.setBackground(new Color(253, 253, 253)); JScrollPane scrollPane = new JScrollPane(logArea); scrollPane.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createTitledBorder("调试日志"), BorderFactory.createEmptyBorder(8, 8, 8, 8))); return scrollPane; } private JPanel buildActionPanel() { JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 12, 0)); panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); panel.setOpaque(false); closeButton.setFont(closeButton.getFont().deriveFont(Font.PLAIN, 13f)); closeButton.setPreferredSize(new Dimension(100, 32)); closeButton.setFocusPainted(false); closeButton.setBackground(new Color(240, 240, 240)); closeButton.setForeground(new Color(70, 70, 70)); closeButton.setOpaque(true); closeButton.setBorder(BorderFactory.createLineBorder(new Color(210, 210, 210))); closeButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); closeButton.addActionListener(e -> handleCloseAction()); pauseButton.setFont(pauseButton.getFont().deriveFont(Font.PLAIN, 13f)); pauseButton.setPreferredSize(new Dimension(100, 32)); pauseButton.setFocusPainted(false); pauseButton.setBackground(new Color(120, 120, 120)); pauseButton.setForeground(Color.WHITE); pauseButton.addActionListener(e -> togglePause()); clearButton.setFont(clearButton.getFont().deriveFont(Font.PLAIN, 13f)); clearButton.setPreferredSize(new Dimension(100, 32)); clearButton.setBackground(themeColor); clearButton.setForeground(Color.WHITE); clearButton.setFocusPainted(false); clearButton.setOpaque(true); clearButton.setBorderPainted(false); clearButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); clearButton.addActionListener(e -> logArea.setText("")); JPanel rightGroup = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); rightGroup.setOpaque(false); hexDisplayCheckBox.setOpaque(false); hexDisplayCheckBox.setFont(hexDisplayCheckBox.getFont().deriveFont(Font.PLAIN, 12f)); hexDisplayCheckBox.setForeground(new Color(90, 90, 90)); rightGroup.add(clearButton); rightGroup.add(hexDisplayCheckBox); panel.add(closeButton); panel.add(pauseButton); panel.add(rightGroup); return panel; } private void handleCloseAction() { performDisconnect(false); dispose(); } private void handlePortSelectionChange() { if (suppressPreferenceSync) { return; } String selected = (String) portComboBox.getSelectedItem(); activePortName = selected; preferredPortName = selected; SerialPortPreferences.setPortName(selected); } private void handleBaudSelectionChange() { if (suppressPreferenceSync) { return; } Integer value = (Integer) baudComboBox.getSelectedItem(); if (value == null) { return; } activeBaudRate = value; SerialPortPreferences.setBaudRate(value); } private void handleAutoConnectToggle() { if (suppressPreferenceSync) { return; } boolean enabled = autoConnectCheckBox.isSelected(); SerialPortPreferences.setAutoConnectEnabled(enabled); if (enabled && !serialService.isOpen()) { String selected = (String) portComboBox.getSelectedItem(); if (selected != null && !selected.trim().isEmpty()) { performConnect(); } } } private void loadPreferences() { suppressPreferenceSync = true; try { preferredPortName = SerialPortPreferences.getPortName(); activePortName = preferredPortName; int savedBaud = SerialPortPreferences.getBaudRate(); if (isSupportedBaud(savedBaud)) { activeBaudRate = savedBaud; if (baudComboBox.getSelectedItem() == null || !baudComboBox.getSelectedItem().equals(savedBaud)) { baudComboBox.setSelectedItem(savedBaud); } } boolean autoConnect = SerialPortPreferences.isAutoConnectEnabled(); autoConnectCheckBox.setSelected(autoConnect); } finally { suppressPreferenceSync = false; } } private boolean isSupportedBaud(int baud) { for (int i = 0; i < baudComboBox.getItemCount(); i++) { if (baudComboBox.getItemAt(i) == baud) { return true; } } return false; } private void syncConnectionStateFromService() { isConnected = serialService.isOpen(); if (isConnected) { serialService.setPaused(false); if (activePortName == null) { activePortName = preferredPortName; } if (preferredPortName == null) { preferredPortName = activePortName; } } updateControlStates(); } private void attachLogListener() { serialService.startCapture(this::handleSerialData); serialService.setPaused(false); isPaused = false; } private void detachLogListener() { serialService.stopDataCaptureOnly(); isPaused = false; updateControlStates(); } private void handleConnectButton(ActionEvent event) { if (serialService.isOpen()) { performDisconnect(true); } else { performConnect(); } } private void performConnect() { String selectedPort = (String) portComboBox.getSelectedItem(); if (selectedPort == null || selectedPort.trim().isEmpty()) { JOptionPane.showMessageDialog(this, "未检测到可用串口,请检查连接后重试。", "提示", JOptionPane.WARNING_MESSAGE); return; } Integer baudSelection = (Integer) baudComboBox.getSelectedItem(); int baudRate = baudSelection != null ? baudSelection : activeBaudRate; try { boolean opened = serialService.open(selectedPort, baudRate); if (!opened) { JOptionPane.showMessageDialog(this, "串口打开失败,请确认串口未被占用。", "错误", JOptionPane.ERROR_MESSAGE); appendLog("串口连接失败: " + selectedPort); return; } serialService.startCapture(this::handleSerialData); serialService.setPaused(false); isConnected = true; isPaused = false; activePortName = selectedPort; activeBaudRate = baudRate; preferredPortName = selectedPort; SerialPortPreferences.setPortName(selectedPort); SerialPortPreferences.setBaudRate(baudRate); appendLog("串口已连接: " + selectedPort + " @ " + baudRate); } catch (Exception ex) { appendLog("连接串口时发生异常: " + ex.getMessage()); JOptionPane.showMessageDialog(this, "连接串口时发生异常:\n" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } finally { updateControlStates(); } } private void performDisconnect(boolean logMessage) { if (!serialService.isOpen()) { isConnected = false; updateControlStates(); return; } serialService.close(); serialService.stopDataCaptureOnly(); isConnected = false; isPaused = false; if (logMessage) { appendLog("串口连接已断开"); } updateControlStates(); } private void togglePause() { if (!serialService.isOpen()) { return; } isPaused = !isPaused; serialService.setPaused(isPaused); appendLog(isPaused ? "调试日志显示已暂停" : "调试日志显示已恢复"); updateControlStates(); } private void refreshSerialPorts() { suppressPreferenceSync = true; try { String previousSelection = (String) portComboBox.getSelectedItem(); portComboBox.removeAllItems(); SerialPort[] ports = SerialPort.getCommPorts(); for (SerialPort port : ports) { portComboBox.addItem(port.getSystemPortName()); } if (portComboBox.getItemCount() == 0) { statusLabel.setText("未检测到可用串口"); connectButton.setEnabled(false); } else { connectButton.setEnabled(true); String target = activePortName != null ? activePortName : preferredPortName; if (target != null) { portComboBox.setSelectedItem(target); } if (portComboBox.getSelectedItem() == null && previousSelection != null) { portComboBox.setSelectedItem(previousSelection); } if (portComboBox.getSelectedItem() == null) { portComboBox.setSelectedIndex(0); } } } finally { suppressPreferenceSync = false; } updateControlStates(); } private void handleSerialData(byte[] data) { if (data == null || data.length == 0) { return; } if (hexDisplayCheckBox.isSelected()) { String hex = bytesToHex(data); appendLog(String.format("接收 %d 字节 | HEX: %s", data.length, hex)); } else { String ascii = sanitizeAscii(new String(data, StandardCharsets.UTF_8)); appendLog(String.format("接收 %d 字节 | ASCII: %s", data.length, ascii.isEmpty() ? "(不可打印)" : ascii)); } } private void appendLog(String message) { SwingUtilities.invokeLater(() -> { logArea.append(String.format("[%s] %s%n", timeFormatter.format(new Date()), message)); logArea.setCaretPosition(logArea.getDocument().getLength()); }); } private void updateControlStates() { isConnected = serialService.isOpen(); portComboBox.setEnabled(!isConnected); baudComboBox.setEnabled(!isConnected); pauseButton.setEnabled(isConnected); pauseButton.setText(isPaused ? "开始显示" : "暂停显示"); boolean hasPorts = portComboBox.getItemCount() > 0; connectButton.setEnabled(isConnected || hasPorts); String displayPort = activePortName != null ? activePortName : preferredPortName; if (displayPort == null || displayPort.trim().isEmpty()) { displayPort = "--"; } if (isConnected) { connectButton.setText("断开"); connectButton.setBackground(new Color(220, 68, 55)); statusLabel.setForeground(new Color(46, 139, 87)); statusLabel.setText(String.format("已连接:%s @ %d", displayPort, activeBaudRate)); } else { connectButton.setText("连接"); connectButton.setBackground(themeColor); statusLabel.setForeground(new Color(120, 120, 120)); if (hasPorts) { if (preferredPortName != null && !preferredPortName.trim().isEmpty()) { statusLabel.setText(String.format("未连接(上次:%s @ %d)", displayPort, activeBaudRate)); } else { statusLabel.setText("未连接"); } } else { statusLabel.setText("未检测到可用串口"); } } connectButton.setOpaque(true); connectButton.setBorderPainted(false); clearButton.setBackground(themeColor); clearButton.setForeground(Color.WHITE); clearButton.setOpaque(true); clearButton.setBorderPainted(false); } private String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(bytes.length * 3); for (byte b : bytes) { sb.append(String.format("%02X ", b)); } if (sb.length() > 0) { sb.setLength(sb.length() - 1); } return sb.toString(); } private String sanitizeAscii(String value) { StringBuilder sb = new StringBuilder(value.length()); for (char ch : value.toCharArray()) { if (ch >= 32 && ch < 127) { sb.append(ch); } else if (ch == '\r' || ch == '\n' || ch == '\t') { sb.append(ch); } } return sb.toString().trim(); } @Override public void setVisible(boolean visible) { if (visible) { attachLogListener(); } else { detachLogListener(); } super.setVisible(visible); } @Override public void dispose() { detachLogListener(); super.dispose(); } }