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<String> portComboBox = new JComboBox<>();
|
private final JComboBox<Integer> 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();
|
}
|
}
|