826220679@qq.com
4 天以前 352da282b6c21700eb454407b92cabcf169a448e
首次提交AOA自动跟随
已添加37个文件
2673 ■■■■■ 文件已修改
.classpath 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.project 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/AOAFollowSystem$1.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/AOAFollowSystem$HomePanel$1.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/AOAFollowSystem$HomePanel$2.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/AOAFollowSystem$HomePanel.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/AOAFollowSystem.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/ConfigPanel$1.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/ConfigPanel.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/Dell55AA01Parser$ParseResult.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/Dell55AA01Parser.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/Dell55AA1FParser$ParseResult.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/Dell55AA1FParser.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/FirmwareUpgrader$ProgressCallback.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/FirmwareUpgrader.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/HexUtils.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/LogUtil.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/Mains.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/SerialPortService.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/SingleInstanceLock.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/VisualizationPanel$1.class 补丁 | 查看 | 原始文档 | blame | 历史
bin/home/VisualizationPanel.class 补丁 | 查看 | 原始文档 | blame | 历史
lib/jSerialComm-2.10.4.jar 补丁 | 查看 | 原始文档 | blame | 历史
src/home/AOAFollowSystem.java 775 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/ConfigPanel.java 251 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/Dell55AA01Parser.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/Dell55AA1FParser.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/FirmwareUpgrader.java 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/HexUtils.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/LogUtil.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/Mains.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/SerialPortService.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/SingleInstanceLock.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/home/VisualizationPanel.java 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
systemfile/Messages_en.properties 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
systemfile/Messages_zh.properties 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
systemfile/logfile/openlog.txt 382 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.classpath
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="src" path="src"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
        <attributes>
            <attribute name="module" value="true"/>
        </attributes>
    </classpathentry>
    <classpathentry kind="lib" path="D:/eclipseworkspace/GIT/AOAFlow/lib/jSerialComm-2.10.4.jar"/>
    <classpathentry kind="output" path="bin"/>
</classpath>
.project
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
    <name>AOAFlow</name>
    <comment></comment>
    <projects>
    </projects>
    <buildSpec>
        <buildCommand>
            <name>org.eclipse.jdt.core.javabuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
    </buildSpec>
    <natures>
        <nature>org.eclipse.jdt.core.javanature</nature>
    </natures>
</projectDescription>
bin/home/AOAFollowSystem$1.class
Binary files differ
bin/home/AOAFollowSystem$HomePanel$1.class
Binary files differ
bin/home/AOAFollowSystem$HomePanel$2.class
Binary files differ
bin/home/AOAFollowSystem$HomePanel.class
Binary files differ
bin/home/AOAFollowSystem.class
Binary files differ
bin/home/ConfigPanel$1.class
Binary files differ
bin/home/ConfigPanel.class
Binary files differ
bin/home/Dell55AA01Parser$ParseResult.class
Binary files differ
bin/home/Dell55AA01Parser.class
Binary files differ
bin/home/Dell55AA1FParser$ParseResult.class
Binary files differ
bin/home/Dell55AA1FParser.class
Binary files differ
bin/home/FirmwareUpgrader$ProgressCallback.class
Binary files differ
bin/home/FirmwareUpgrader.class
Binary files differ
bin/home/HexUtils.class
Binary files differ
bin/home/LogUtil.class
Binary files differ
bin/home/Mains.class
Binary files differ
bin/home/SerialPortService.class
Binary files differ
bin/home/SingleInstanceLock.class
Binary files differ
bin/home/VisualizationPanel$1.class
Binary files differ
bin/home/VisualizationPanel.class
Binary files differ
lib/jSerialComm-2.10.4.jar
Binary files differ
src/home/AOAFollowSystem.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,775 @@
package home;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.InputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.io.File;
import java.io.FileInputStream;
import java.util.PropertyResourceBundle;
import com.fazecast.jSerialComm.SerialPort;
import home.Dell55AA01Parser.ParseResult;
public class AOAFollowSystem extends JFrame {
    private static final long serialVersionUID = 1L;
    private CardLayout cardLayout;
    private JPanel mainPanel;
    private HomePanel homePanel;
    private ConfigPanel configPanel;
    private ResourceBundle messages;
    private Locale currentLocale;
    private JButton homeBtn;
    private JButton configBtn;
    private JComboBox<String> languageCombo;
    private JLabel languageLabel;
    private SerialPortService serialService;
    public AOAFollowSystem() {
        this.currentLocale = Locale.SIMPLIFIED_CHINESE;
        this.messages = loadResourceBundle(currentLocale);
        this.serialService = new SerialPortService();
        initializeUI();
    }
    private ResourceBundle loadResourceBundle(Locale locale) {
        String fileName = locale.equals(Locale.ENGLISH) ?
                "Messages_en.properties" : "Messages_zh.properties";
        File langFile = new File("systemfile/" + fileName);
        if (!langFile.exists()) {
            System.err.println("默认资源文件未找到: " + langFile.getAbsolutePath());
            return ResourceBundle.getBundle("systemfile.Messages");
        }
        try (InputStream inputStream = new FileInputStream(langFile)) {
            return new PropertyResourceBundle(inputStream);
        } catch (IOException e) {
            System.err.println("无法加载资源文件: " + e.getMessage());
            return ResourceBundle.getBundle("systemfile.Messages");
        }
    }
    public String getString(String key) {
        if (messages != null && messages.containsKey(key)) {
            return messages.getString(key);
        }
        // é»˜è®¤æ–‡æœ¬ï¼ˆä¸­æ–‡ï¼‰
        switch (key) {
        case "home": return "首页";
        case "config": return "配置";
        case "device_id": return "设备编号";
        case "group": return "通信小组";
        case "frequency": return "通信频率";
        case "read_config": return "读取配置";
        case "save_config": return "保存配置";
        case "serial_port": return "串口";
        case "baud_rate": return "波特率";
        case "open_serial": return "打开串口";
        case "close_serial": return "关闭串口";
        case "start": return "开始";
        case "pause": return "暂停";
        case "clear": return "清空";
        case "send": return "发送";
        case "device_id_table": return "设备编号";
        case "distance_table": return "实时距离";
        case "angle_table": return "实时角度";
        case "signal_table": return "信号质量";
        case "power_table": return "电量";
        case "button_table": return "按钮";
        case "time_table": return "时间";
        case "LANGUAGE": return "语言";
        case "log": return "日志";
        case "send_data": return "发送数据";
        case "select_serial_port": return "请选择串口";
        case "error": return "错误";
        case "open_serial_first": return "请先打开串口";
        case "input_data_to_send": return "请输入要发送的数据";
        case "send_failed": return "发送失败";
        case "receive": return "接收";
        case "visualization": return "可视化";
        case "config_read_success": return "配置读取成功";
        case "info": return "信息";
        case "input_valid_number": return "请输入有效的数字";
        case "config_save_success": return "配置保存成功";
        case "select_file": return "选择文件";
        case "upgrade": return "升级";
        case "upgrade_progress": return "升级进度";
        case "select_bin_file": return "请选择bin文件";
        case "upgrade_success": return "升级成功";
        case "upgrade_failed": return "升级失败";
        case "hex": return "HEX";
        case "ascii": return "ASCII";
        case "hex_send": return "HEX发送";
        case "display_format": return "显示格式";
        default: return key;
        }
    }
    private void initializeUI() {
        setTitle("HXZK_AOA");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(1000, 700);
        setLocationRelativeTo(null);
        // åˆ›å»ºé¡¶éƒ¨å¯¼èˆª
        JPanel navPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        homeBtn = createNavButton(getString("home"), new Color(70, 130, 180));
        configBtn = createNavButton(getString("config"), new Color(70, 130, 180));
        languageCombo = new JComboBox<>(new String[]{"中文", "English"});
        languageCombo.setSelectedIndex(0);
        languageCombo.addActionListener(e -> {
            Locale newLocale = languageCombo.getSelectedIndex() == 0 ?
                    Locale.SIMPLIFIED_CHINESE : Locale.ENGLISH;
            switchLanguage(newLocale);
        });
        navPanel.add(homeBtn);
        navPanel.add(configBtn);
        navPanel.add(Box.createHorizontalStrut(20));
        languageLabel = new JLabel(getString("LANGUAGE") + ":");
        navPanel.add(languageLabel);
        navPanel.add(languageCombo);
        // åˆ›å»ºä¸»é¢æ¿ï¼ˆå¡ç‰‡å¸ƒå±€ï¼‰
        cardLayout = new CardLayout();
        mainPanel = new JPanel(cardLayout);
        homePanel = new HomePanel(serialService, this);
        configPanel = new ConfigPanel(serialService, this);
        mainPanel.add(homePanel, "home");
        mainPanel.add(configPanel, "config");
        // æ·»åŠ å¯¼èˆªæŒ‰é’®äº‹ä»¶
        homeBtn.addActionListener(e -> cardLayout.show(mainPanel, "home"));
        configBtn.addActionListener(e -> cardLayout.show(mainPanel, "config"));
        // è®¾ç½®å¸ƒå±€
        setLayout(new BorderLayout());
        add(navPanel, BorderLayout.NORTH);
        add(mainPanel, BorderLayout.CENTER);
        // è®¾ç½®é»˜è®¤æ˜¾ç¤ºé¦–页
        cardLayout.show(mainPanel, "home");
    }
    private void switchLanguage(Locale newLocale) {
        this.currentLocale = newLocale;
        this.messages = loadResourceBundle(currentLocale);
        // æ›´æ–°ç•Œé¢æ–‡æœ¬
        updateUILanguage();
        // æ›´æ–°è¯­è¨€é€‰æ‹©æ¡†
        languageCombo.setSelectedIndex(newLocale.equals(Locale.SIMPLIFIED_CHINESE) ? 0 : 1);
        revalidate();
        repaint();
    }
    private void updateUILanguage() {
        // æ›´æ–°å¯¼èˆªæŒ‰é’®æ–‡æœ¬
        homeBtn.setText(getString("home"));
        configBtn.setText(getString("config"));
        // æ›´æ–°è¯­è¨€æ ‡ç­¾
        languageLabel.setText(getString("LANGUAGE") + ":");
        // æ›´æ–°å„面板文本
        homePanel.updateLanguage();
        configPanel.updateLanguage();
    }
    private JButton createNavButton(String text, Color color) {
        JButton button = new JButton(text);
        button.setBackground(color);
        button.setForeground(Color.WHITE);
        button.setFocusPainted(false);
        button.setBorder(BorderFactory.createEmptyBorder(5, 15, 5, 15));
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                button.setBackground(color.darker());
            }
            @Override
            public void mouseExited(MouseEvent e) {
                button.setBackground(color);
            }
        });
        return button;
    }
    // é¦–页面板
    class HomePanel extends JPanel {
        /**
         *
         */
        private static final long serialVersionUID = 1L;
        private VisualizationPanel visualizationPanel;
        private JTable dataTable;
        private JTextArea logArea;
        private JComboBox<String> serialPortCombo;
        private JComboBox<String> baudRateCombo;
        private JButton openSerialBtn;
        private JButton startPauseBtn;
        private JButton clearBtn;
        private JButton sendBtn;
        private JTextField sendField;
        private JLabel displayFormatLabel; // æ–°å¢žï¼šæ˜¾ç¤ºæ ¼å¼æ ‡ç­¾
        private JLabel serialPortLabel;    // æ–°å¢žï¼šä¸²å£æ ‡ç­¾
        private JLabel baudRateLabel;      // æ–°å¢žï¼šæ³¢ç‰¹çŽ‡æ ‡ç­¾
        private SerialPortService serialService;
        private AOAFollowSystem parentFrame;
        private boolean serialOpened = false;
        private boolean isRunning = false;
        // æ–°å¢žï¼šæ˜¾ç¤ºæ ¼å¼å•选按钮
        private JRadioButton hexRadio;
        private JRadioButton asciiRadio;
        private JCheckBox hexSendCheckBox;
        // è¡¨æ ¼æ¨¡åž‹ - åªæœ‰1行
        private Object[][] tableData = new Object[1][7];
        private String[] columnNames = {
                getString("device_id_table"),
                getString("distance_table"),
                getString("angle_table"),
                getString("signal_table"),
                getString("power_table"),
                getString("button_table"),
                getString("time_table")
        };
        // ä¼˜åŒ–:重用对象减少内存分配
        private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        private StringBuilder hexBuilder = new StringBuilder();
        private StringBuilder displayBuilder = new StringBuilder();
        public HomePanel(SerialPortService serialService, AOAFollowSystem parentFrame) {
            this.serialService = serialService;
            this.parentFrame = parentFrame;
            setLayout(new BorderLayout(10, 10));
            setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
            // åˆ›å»ºå·¦ä¾§é¢æ¿ï¼ˆå¯è§†åŒ–区域和表格)
            JPanel leftPanel = new JPanel(new BorderLayout(10, 10));
            // å¯è§†åŒ–面板
            visualizationPanel = new VisualizationPanel(parentFrame);
            leftPanel.add(visualizationPanel, BorderLayout.CENTER);
            // æ•°æ®è¡¨æ ¼
            JPanel tablePanel = new JPanel(new BorderLayout());
            tablePanel.setBorder(BorderFactory.createTitledBorder(getString("device_id_table")));
            dataTable = new JTable(tableData, columnNames);
            JScrollPane tableScroll = new JScrollPane(dataTable);
            tableScroll.setPreferredSize(new Dimension(0, 80));
            tablePanel.add(tableScroll, BorderLayout.CENTER);
            leftPanel.add(tablePanel, BorderLayout.SOUTH);
            // åˆ›å»ºå³ä¾§é¢æ¿ï¼ˆæ—¥å¿—和控制)
            JPanel rightPanel = new JPanel(new BorderLayout(10, 10));
            // æ—¥å¿—区域
            JPanel logPanel = new JPanel(new BorderLayout());
            logPanel.setBorder(BorderFactory.createTitledBorder(getString("log")));
            logArea = new JTextArea();
            logArea.setEditable(false);
            JScrollPane logScroll = new JScrollPane(logArea);
            logScroll.setPreferredSize(new Dimension(400, 0));
            logPanel.add(logScroll, BorderLayout.CENTER);
            // æ–°å¢žï¼šæ˜¾ç¤ºæ ¼å¼é€‰æ‹©é¢æ¿
            JPanel formatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
            formatPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
            displayFormatLabel = new JLabel(getString("display_format") + ":"); // ä½¿ç”¨getString获取文本
            formatPanel.add(displayFormatLabel);
            ButtonGroup formatGroup = new ButtonGroup();
            hexRadio = new JRadioButton(getString("hex"), true); // é»˜è®¤é€‰ä¸­HEX
            asciiRadio = new JRadioButton(getString("ascii"));
            formatGroup.add(hexRadio);
            formatGroup.add(asciiRadio);
            formatPanel.add(hexRadio);
            formatPanel.add(asciiRadio);
            // å°†æ ¼å¼é€‰æ‹©é¢æ¿æ·»åŠ åˆ°æ—¥å¿—é¢æ¿çš„åº•éƒ¨
            logPanel.add(formatPanel, BorderLayout.SOUTH);
            rightPanel.add(logPanel, BorderLayout.CENTER);
            // æŽ§åˆ¶é¢æ¿
            JPanel controlPanel = new JPanel(new GridBagLayout());
            controlPanel.setBorder(BorderFactory.createTitledBorder(getString("serial_port")));
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.insets = new Insets(5, 2, 5, 5);
            gbc.fill = GridBagConstraints.HORIZONTAL;
            // ä¸²å£é€‰æ‹©
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.weightx = 0;
            serialPortLabel = new JLabel(getString("serial_port") + ":"); // ä½¿ç”¨getString获取文本
            controlPanel.add(serialPortLabel, gbc);
            gbc.gridx = 1;
            gbc.weightx = 1;
            serialPortCombo = new JComboBox<>();
            // æ·»åŠ å¼¹å‡ºèœå•ç›‘å¬å™¨ï¼Œç‚¹å‡»æ—¶åˆ·æ–°ä¸²å£åˆ—è¡¨
            serialPortCombo.addPopupMenuListener(new PopupMenuListener() {
                @Override
                public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                    refreshSerialPorts();
                }
                @Override
                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {}
                @Override
                public void popupMenuCanceled(PopupMenuEvent e) {}
            });
            refreshSerialPorts();
            controlPanel.add(serialPortCombo, gbc);
            // æ³¢ç‰¹çŽ‡é€‰æ‹©
            gbc.gridx = 0;
            gbc.gridy = 1;
            gbc.weightx = 0;
            baudRateLabel = new JLabel(getString("baud_rate") + ":"); // ä½¿ç”¨getString获取文本
            controlPanel.add(baudRateLabel, gbc);
            gbc.gridx = 1;
            gbc.weightx = 1;
            baudRateCombo = new JComboBox<>(new String[]{"115200", "921600","9600", "19200", "38400", "57600"});
            baudRateCombo.setSelectedIndex(0);
            controlPanel.add(baudRateCombo, gbc);
            // ä¸²å£æŽ§åˆ¶æŒ‰é’®
            gbc.gridx = 0;
            gbc.gridy = 2;
            gbc.gridwidth = 1;  // ä¿®æ”¹ï¼šåªå ä¸€åˆ—
            gbc.weightx = 0.5;  // ä¿®æ”¹ï¼šè®¾ç½®æƒé‡
            openSerialBtn = createColoredButton(getString("open_serial"), new Color(70, 130, 180));
            openSerialBtn.setPreferredSize(new Dimension(100, 30));  // ä¿®æ”¹ï¼šè®¾ç½®ä¸Žå¼€å§‹æŒ‰é’®ç›¸åŒå°ºå¯¸
            openSerialBtn.addActionListener(e -> toggleSerialPort());
            controlPanel.add(openSerialBtn, gbc);
            openSerialBtn.setBackground(Color.GRAY);
            // å¯åЍ/暂停按钮(合并为一个)
            gbc.gridx = 1;  // ä¿®æ”¹ï¼šæ”¾åœ¨ç¬¬äºŒåˆ—
            gbc.gridy = 2;
            JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));
            startPauseBtn = createColoredButton(getString("start"), new Color(50, 205, 50));
            startPauseBtn.setPreferredSize(new Dimension(100, 30));
            startPauseBtn.addActionListener(e -> toggleCapture());
            buttonPanel.add(startPauseBtn);
            clearBtn = createColoredButton(getString("clear"), new Color(192, 192, 192));
            clearBtn.setPreferredSize(new Dimension(100, 30));
            clearBtn.addActionListener(e -> clearLog());
            buttonPanel.add(clearBtn);
            controlPanel.add(buttonPanel, gbc);
            rightPanel.add(controlPanel, BorderLayout.SOUTH);
            // å‘送数据面板 - ç§»åˆ°è¡¨æ ¼ä¸‹æ–¹
            JPanel sendDataPanel = new JPanel(new BorderLayout(5, 5));
            sendDataPanel.setBorder(BorderFactory.createTitledBorder(getString("send_data")));
            JPanel sendControlPanel = new JPanel(new BorderLayout(5, 0));
            sendField = new JTextField();
            sendBtn = createColoredButton(getString("send"), new Color(70, 130, 180));
            sendBtn.setPreferredSize(new Dimension(100, 30));
            sendBtn.addActionListener(e -> sendData());
            // æ–°å¢žï¼šHEX发送复选框
            hexSendCheckBox = new JCheckBox(getString("hex_send"), true); // é»˜è®¤å‹¾é€‰
            sendControlPanel.add(hexSendCheckBox, BorderLayout.WEST);
            sendControlPanel.add(sendBtn, BorderLayout.EAST);
            sendDataPanel.add(sendField, BorderLayout.CENTER);
            sendDataPanel.add(sendControlPanel, BorderLayout.EAST);
            // æ·»åŠ å‘é€æ•°æ®é¢æ¿åˆ°å·¦ä¾§é¢æ¿çš„æœ€åº•éƒ¨
            leftPanel.add(sendDataPanel, BorderLayout.SOUTH);
            // æ·»åŠ å·¦å³é¢æ¿
            add(leftPanel, BorderLayout.CENTER);
            add(rightPanel, BorderLayout.EAST);
        }
        private JButton createColoredButton(String text, Color color) {
            JButton button = new JButton(text);
            button.setBackground(color);
            button.setForeground(Color.WHITE);
            button.setFocusPainted(false);
            button.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
            button.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseEntered(MouseEvent e) {
                    button.setBackground(color.darker());
                }
                @Override
                public void mouseExited(MouseEvent e) {
                    button.setBackground(color);
                }
            });
            return button;
        }
        private void refreshSerialPorts() {
            String selectedPort = (String) serialPortCombo.getSelectedItem();
            serialPortCombo.removeAllItems();
            SerialPort[] ports = SerialPort.getCommPorts();
            for (SerialPort port : ports) {
                serialPortCombo.addItem(port.getSystemPortName());
            }
            // å°è¯•恢复之前的选择
            if (selectedPort != null) {
                serialPortCombo.setSelectedItem(selectedPort);
            } else if (ports.length > 0) {
                serialPortCombo.setSelectedIndex(0);
            }
        }
        private void toggleSerialPort() {
            if (serialOpened) {
                closeSerialPort();
                openSerialBtn.setText(getString("open_serial"));
                openSerialBtn.setBackground(Color.GRAY); // å…³é—­æ—¶ç°è‰²
                serialOpened = false;
                // å…³é—­ä¸²å£æ—¶åœæ­¢æ•获
                if (isRunning) {
                    toggleCapture();
                }
            } else {
                if (openSerialPort()) {
                    openSerialBtn.setText(getString("close_serial"));
                    openSerialBtn.setBackground(Color.GREEN);
                    serialOpened = true;
                }
            }
        }
        private boolean openSerialPort() {
            String portName = (String) serialPortCombo.getSelectedItem();
            if (portName == null) {
                JOptionPane.showMessageDialog(this, getString("select_serial_port"), getString("error"), JOptionPane.ERROR_MESSAGE);
                return false;
            }
            int baudRate = Integer.parseInt((String) baudRateCombo.getSelectedItem());
            return serialService.open(portName, baudRate);
        }
        private void closeSerialPort() {
            serialService.close();
        }
        private void toggleCapture() {
            if (!serialOpened) {
                JOptionPane.showMessageDialog(this, getString("open_serial_first"), getString("error"), JOptionPane.ERROR_MESSAGE);
                return;
            }
            isRunning = !isRunning;
            serialService.setPaused(!isRunning);
            if (isRunning) {
                startPauseBtn.setText(getString("pause"));
                startPauseBtn.setBackground(new Color(255, 165, 0));
                // å¯åŠ¨æ•°æ®æ•èŽ·
                serialService.startCapture(this::processReceivedData);
            } else {
                startPauseBtn.setText(getString("start"));
                startPauseBtn.setBackground(new Color(50, 205, 50));
            }
        }
        private void clearLog() {
            logArea.setText("");
        }
        private void sendData() {
            if (!serialOpened) {
                JOptionPane.showMessageDialog(this, getString("open_serial_first"), getString("error"), JOptionPane.ERROR_MESSAGE);
                return;
            }
            String data = sendField.getText();
            if (data.isEmpty()) {
                JOptionPane.showMessageDialog(this, getString("input_data_to_send"), getString("error"), JOptionPane.ERROR_MESSAGE);
                return;
            }
            byte[] sendBytes;
            if (hexSendCheckBox.isSelected()) {
                // HEX发送模式
                try {
                    sendBytes = hexStringToByteArray(data.replaceAll("\\s+", ""));
                    if (sendBytes == null) {
                        JOptionPane.showMessageDialog(this, "无效的HEX格式", getString("error"), JOptionPane.ERROR_MESSAGE);
                        return;
                    }
                } catch (Exception e) {
                    JOptionPane.showMessageDialog(this, "HEX转换错误: " + e.getMessage(), getString("error"), JOptionPane.ERROR_MESSAGE);
                    return;
                }
            } else {
                // ASCII发送模式
                sendBytes = data.getBytes();
            }
            boolean success = serialService.send(sendBytes);
            if (success) {
                appendLog(getString("send") + ": " + (hexSendCheckBox.isSelected() ? bytesToHex(sendBytes) : data));
            } else {
                appendLog(getString("send_failed") + ": " + data);
            }
            sendField.setText("");
        }
        private void processReceivedData(byte[] data) {
            // ä¼˜åŒ–:重用StringBuilder对象
            hexBuilder.setLength(0);
            for (byte b : data) {
                hexBuilder.append(String.format("%02X", b));
            }
            String displayText = hexBuilder.toString();
            String displayText1 = new String(data).replaceAll("\\s+", "");
            if (displayText.startsWith("55AA1F")) {
                Dell55AA1FParser.ParseResult result = Dell55AA1FParser.parse(displayText, "127.0.0.1", 0);
                if (result != null) {
                    updateTable(result);
                    visualizationPanel.updatePosition(result.distance, result.angle);
                    visualizationPanel.setTagId(result.tagId);
                }
            }else if(displayText.startsWith("55AA01")) {
                ParseResult result=Dell55AA01Parser.parse(displayText, "127.0.0.1", 0);
                visualizationPanel.updatePosition(result.distance,270);
                visualizationPanel.setTagId(result.tagId);
            }
            if (hexRadio.isSelected()) {
                appendLog(displayText);
            } else {
                if (displayText.startsWith("55AA1F")) {
                    Dell55AA1FParser.ParseResult result = Dell55AA1FParser.parse(displayText, "127.0.0.1", 0);
                    // ä¼˜åŒ–:重用StringBuilder对象
                    displayBuilder.setLength(0);
                    displayBuilder.append("1F:")
                    .append(result.dataLength)
                    .append(",")
                    .append(result.messageType)
                    .append(",id:")
                    .append(result.tagId)
                    .append(",Dis:")
                    .append(result.distance)
                    .append("cm,Angle:")
                    .append(result.angle)
                    .append("°,Signal:")
                    .append(result.signalQuality)
                    .append(",Button:")
                    .append(result.buttonPressed)
                    .append(",Power:")
                    .append(result.power);
                    appendLog(displayBuilder.toString());
                }else if(displayText.startsWith("55AA01")) {
                    ParseResult result=Dell55AA01Parser.parse(displayText, "127.0.0.1", 0);
                    // ä¼˜åŒ–:重用StringBuilder对象
                    displayBuilder.setLength(0);
                    displayBuilder.append("55AA01 Seq:")
                    .append(result.sequenceNum)
                    .append(",Tagid:")
                    .append(result.tagId)
                    .append(",Anchorid:")
                    .append(result.anchorId)
                    .append(",Distance:")
                    .append(result.distance)
                    .append(",Power:")
                    .append(result.power)
                    .append(",Button:")
                    .append(result.buttonPressed);
                    appendLog(displayBuilder.toString());
                }else {
                    appendLog(displayText1);
                }
            }
        }
        private void appendLog(String message) {
            SwingUtilities.invokeLater(() -> {
                // ä¼˜åŒ–:限制日志长度,防止内存无限增长
                if (logArea.getLineCount() > 1000) {
                    try {
                        int end = logArea.getDocument().getLength();
                        int start = logArea.getDocument().getText(0, end).indexOf("\n") + 1;
                        logArea.getDocument().remove(0, start);
                    } catch (Exception e) {
                        logArea.setText(""); // å¦‚果出错则清空日志
                    }
                }
                String time = sdf.format(new Date());
                logArea.append("[" + time + "] " + message + "\n");
                // è‡ªåŠ¨æ»šåŠ¨åˆ°æœ€åŽ
                logArea.setCaretPosition(logArea.getDocument().getLength());
            });
        }
        private void updateTable(Dell55AA1FParser.ParseResult result) {
            SwingUtilities.invokeLater(() -> {
                // åªæœ‰1行数据,直接更新
                String time = sdf.format(new Date());
                tableData[0] = new Object[]{
                        result.tagId,
                        result.distance + "cm",
                        result.angle + "°",
                        result.signalQuality,
                        result.power + "%",
                        result.buttonPressed,
                        time
                };
                // åˆ·æ–°è¡¨æ ¼
                dataTable.repaint();
            });
        }
        // è¾…助方法:将字节数组转换为HEX字符串
        private String bytesToHex(byte[] bytes) {
            // ä¼˜åŒ–:重用StringBuilder对象
            hexBuilder.setLength(0);
            for (byte b : bytes) {
                hexBuilder.append(String.format("%02X ", b));
            }
            return hexBuilder.toString().trim();
        }
        // è¾…助方法:将HEX字符串转换为字节数组
        private byte[] hexStringToByteArray(String s) {
            int len = s.length();
            if (len % 2 != 0) {
                return null;
            }
            byte[] data = new byte[len / 2];
            for (int i = 0; i < len; i += 2) {
                try {
                    data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                            + Character.digit(s.charAt(i+1), 16));
                } catch (Exception e) {
                    return null;
                }
            }
            return data;
        }
        public void updateLanguage() {
            // æ›´æ–°ç•Œé¢æ–‡æœ¬
            openSerialBtn.setText(serialOpened ? getString("close_serial") : getString("open_serial"));
            startPauseBtn.setText(isRunning ? getString("pause") : getString("start"));
            clearBtn.setText(getString("clear"));
            sendBtn.setText(getString("send"));
            // æ›´æ–°æ ‡ç­¾æ–‡æœ¬
            displayFormatLabel.setText(getString("display_format") + ":");
            serialPortLabel.setText(getString("serial_port") + ":");
            baudRateLabel.setText(getString("baud_rate") + ":");
            // æ›´æ–°åˆ—名
            columnNames[0] = getString("device_id_table");
            columnNames[1] = getString("distance_table");
            columnNames[2] = getString("angle_table");
            columnNames[3] = getString("signal_table");
            columnNames[4] = getString("power_table");
            columnNames[5] = getString("button_table");
            columnNames[6] = getString("time_table");
            // æ›´æ–°æ˜¾ç¤ºæ ¼å¼å•选按钮
            hexRadio.setText(getString("hex"));
            asciiRadio.setText(getString("ascii"));
            hexSendCheckBox.setText(getString("hex_send"));
            // æ›´æ–°è¾¹æ¡†æ ‡é¢˜
            updateBorderTitles(this);
            dataTable.repaint();
        }
        private void updateBorderTitles(Container container) {
            for (Component comp : container.getComponents()) {
                if (comp instanceof JPanel) {
                    JPanel panel = (JPanel) comp;
                    Border border = panel.getBorder();
                    if (border instanceof TitledBorder) {
                        TitledBorder titledBorder = (TitledBorder) border;
                        String title = titledBorder.getTitle();
                        if (title.equals("日志") || title.equals("Log")) {
                            titledBorder.setTitle(getString("log"));
                        } else if (title.equals(getString("device_id_table")) || title.equals("Device ID")) {
                            titledBorder.setTitle(getString("device_id_table"));
                        } else if (title.equals(getString("serial_port")) || title.equals("Serial Port")) {
                            titledBorder.setTitle(getString("serial_port"));
                        } else if (title.equals("发送数据") || title.equals("Send Data")) {
                            titledBorder.setTitle(getString("send_data"));
                        } else if (title.equals("可视化") || title.equals("Visualization")) {
                            titledBorder.setTitle(getString("visualization"));
                        }
                        panel.repaint();
                    }
                    // é€’归更新子组件
                    updateBorderTitles(panel);
                }
            }
        }
        private String getString(String key) {
            if (parentFrame != null) {
                return parentFrame.getString(key);
            } else {
                // è¿”回默认文本(中文)或 key æœ¬èº«ä½œä¸ºå¤‡ç”¨
                switch (key) {
                case "home": return "首页";
                case "config": return "配置";
                case "display_format": return "显示格式";
                case "serial_port": return "串口";
                case "baud_rate": return "波特率";
                case "close_serial": return "关闭串口";
                // å…¶ä»– key çš„默认值...
                default: return key;
                }
            }
        }
    }
}
src/home/ConfigPanel.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,251 @@
package home;
import javax.swing.*;
import javax.swing.Timer;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
public class ConfigPanel extends JPanel {
    private JTextField deviceIdField;
    private JTextField groupField;
    private JTextField frequencyField;
    private JButton readConfigBtn;
    private JButton saveConfigBtn;
    private JTextField filePathField;
    private JButton selectFileBtn;
    private JButton upgradeBtn;
    private JProgressBar progressBar;
    private SerialPortService serialService;
    private FirmwareUpgrader firmwareUpgrader;
    private AOAFollowSystem parentFrame;
    // ç”¨äºŽç•Œé¢æ–‡æœ¬çš„æ ‡ç­¾
    private JLabel deviceIdLabel;
    private JLabel groupLabel;
    private JLabel frequencyLabel;
    private JLabel selectFileLabel;
    public ConfigPanel(SerialPortService serialService, AOAFollowSystem parentFrame) {
        this.serialService = serialService;
        this.parentFrame = parentFrame;
        this.firmwareUpgrader = new FirmwareUpgrader(serialService);
        setLayout(new GridBagLayout());
        setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(10, 10, 10, 10);
        gbc.fill = GridBagConstraints.HORIZONTAL;
        // è®¾å¤‡ç¼–号 - ä½¿ç”¨getString()方法
        gbc.gridx = 0;
        gbc.gridy = 0;
        deviceIdLabel = new JLabel(getString("device_id") + ":");
        add(deviceIdLabel, gbc);
        gbc.gridx = 1;
        deviceIdField = new JTextField(15);
        deviceIdField.setText("2548");
        add(deviceIdField, gbc);
        // é€šä¿¡å°ç»„ - ä½¿ç”¨getString()方法
        gbc.gridx = 0;
        gbc.gridy = 1;
        groupLabel = new JLabel(getString("group") + ":");
        add(groupLabel, gbc);
        gbc.gridx = 1;
        groupField = new JTextField(15);
        groupField.setText("2");
        add(groupField, gbc);
        // é€šä¿¡é¢‘率 - ä½¿ç”¨getString()方法
        gbc.gridx = 0;
        gbc.gridy = 2;
        frequencyLabel = new JLabel(getString("frequency") + ":");
        add(frequencyLabel, gbc);
        gbc.gridx = 1;
        frequencyField = new JTextField(15);
        frequencyField.setText("1");
        add(frequencyField, gbc);
        // é€‰æ‹©æ–‡ä»¶ - ä½¿ç”¨getString()方法
        gbc.gridx = 0;
        gbc.gridy = 3;
        selectFileLabel = new JLabel(getString("select_file") + ":");
        add(selectFileLabel, gbc);
        gbc.gridx = 1;
        JPanel filePanel = new JPanel(new BorderLayout(5, 0));
        filePathField = new JTextField();
        filePathField.setEditable(false);
        selectFileBtn = new JButton("...");
        selectFileBtn.setPreferredSize(new Dimension(30, 25));
        selectFileBtn.addActionListener(e -> selectFile());
        filePanel.add(filePathField, BorderLayout.CENTER);
        filePanel.add(selectFileBtn, BorderLayout.EAST);
        add(filePanel, gbc);
        // å›ºä»¶å‡çº§
        gbc.gridx = 0;
        gbc.gridy = 4;
        gbc.gridwidth = 2;
        upgradeBtn = createColoredButton(getString("upgrade"), new Color(70, 130, 180));
        upgradeBtn.setPreferredSize(new Dimension(100, 30));
        upgradeBtn.addActionListener(e -> upgradeFirmware());
        add(upgradeBtn, gbc);
        // è¿›åº¦æ¡
        gbc.gridx = 0;
        gbc.gridy = 5;
        gbc.gridwidth = 2;
        progressBar = new JProgressBar(0, 100);
        progressBar.setStringPainted(true);
        progressBar.setVisible(false);
        add(progressBar, gbc);
        // æ“ä½œæŒ‰é’®
        gbc.gridx = 0;
        gbc.gridy = 6;
        gbc.gridwidth = 2;
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 10));
        readConfigBtn = createColoredButton(getString("read_config"), new Color(70, 130, 180));
        readConfigBtn.setPreferredSize(new Dimension(100, 30));
        saveConfigBtn = createColoredButton(getString("save_config"), new Color(50, 205, 50));
        saveConfigBtn.setPreferredSize(new Dimension(100, 30));
        readConfigBtn.addActionListener(e -> readConfig());
        saveConfigBtn.addActionListener(e -> saveConfig());
        buttonPanel.add(readConfigBtn);
        buttonPanel.add(saveConfigBtn);
        add(buttonPanel, gbc);
    }
    private JButton createColoredButton(String text, Color color) {
        JButton button = new JButton(text);
        button.setBackground(color);
        button.setForeground(Color.WHITE);
        button.setFocusPainted(false);
        button.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15));
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                button.setBackground(color.darker());
            }
            @Override
            public void mouseExited(MouseEvent e) {
                button.setBackground(color);
            }
        });
        return button;
    }
    private void selectFile() {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("BIN Files", "bin"));
        int result = fileChooser.showOpenDialog(this);
        if (result == JFileChooser.APPROVE_OPTION) {
            File selectedFile = fileChooser.getSelectedFile();
            filePathField.setText(selectedFile.getAbsolutePath());
        }
    }
    private void upgradeFirmware() {
        String filePath = filePathField.getText();
        if (filePath.isEmpty()) {
            JOptionPane.showMessageDialog(this, getString("select_bin_file"), getString("error"), JOptionPane.ERROR_MESSAGE);
            return;
        }
        if (!serialService.isOpen()) {
            JOptionPane.showMessageDialog(this, getString("open_serial_first"), getString("error"), JOptionPane.ERROR_MESSAGE);
            return;
        }
        // è¿›è¡Œå‡çº§ï¼Œæ˜¾ç¤ºè¿›åº¦æ¡
        upgradeBtn.setEnabled(false);
        selectFileBtn.setEnabled(false);
        progressBar.setVisible(true);
        progressBar.setValue(0);
        // åœ¨æ–°çº¿ç¨‹ä¸­æ‰§è¡Œå‡çº§æ“ä½œ
        new Thread(() -> {
            try {
                firmwareUpgrader.upgradeFirmware(filePath, progress -> {
                    SwingUtilities.invokeLater(() -> {
                        progressBar.setValue(progress);
                    });
                });
                SwingUtilities.invokeLater(() -> {
                    JOptionPane.showMessageDialog(this, getString("upgrade_success"), getString("info"), JOptionPane.INFORMATION_MESSAGE);
                    upgradeBtn.setEnabled(true);
                    selectFileBtn.setEnabled(true);
                });
            } catch (Exception e) {
                SwingUtilities.invokeLater(() -> {
                    JOptionPane.showMessageDialog(this, getString("upgrade_failed") + ": " + e.getMessage(), getString("error"), JOptionPane.ERROR_MESSAGE);
                    upgradeBtn.setEnabled(true);
                    selectFileBtn.setEnabled(true);
                });
            }
        }).start();
    }
    private void readConfig() {
        // æ¨¡æ‹Ÿè¯»å–配置
        readConfigBtn.setBackground(Color.GREEN);
        Timer timer = new Timer(500, e -> {
            readConfigBtn.setBackground(new Color(70, 130, 180));
        });
        timer.setRepeats(false);
        timer.start();
        JOptionPane.showMessageDialog(this, getString("config_read_success"), getString("info"), JOptionPane.INFORMATION_MESSAGE);
    }
    private void saveConfig() {
        // éªŒè¯è¾“å…¥
        try {
            Integer.parseInt(deviceIdField.getText());
            Integer.parseInt(groupField.getText());
            Integer.parseInt(frequencyField.getText());
        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this, getString("input_valid_number"), getString("error"), JOptionPane.ERROR_MESSAGE);
            return;
        }
        // æ¨¡æ‹Ÿä¿å­˜é…ç½®
        saveConfigBtn.setBackground(Color.GREEN);
        Timer timer = new Timer(500, e -> {
            saveConfigBtn.setBackground(new Color(50, 205, 50));
        });
        timer.setRepeats(false);
        timer.start();
        JOptionPane.showMessageDialog(this, getString("config_save_success"), getString("info"), JOptionPane.INFORMATION_MESSAGE);
    }
    public void updateLanguage() {
        // æ›´æ–°æŒ‰é’®æ–‡æœ¬
        readConfigBtn.setText(getString("read_config"));
        saveConfigBtn.setText(getString("save_config"));
        upgradeBtn.setText(getString("upgrade"));
        // æ›´æ–°æ ‡ç­¾æ–‡æœ¬
        deviceIdLabel.setText(getString("device_id") + ":");
        groupLabel.setText(getString("group") + ":");
        frequencyLabel.setText(getString("frequency") + ":");
        selectFileLabel.setText(getString("select_file") + ":");
    }
    private String getString(String key) {
        return parentFrame.getString(key);
    }
}
src/home/Dell55AA01Parser.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
package home;
import java.util.Arrays;
public class Dell55AA01Parser {
    // å¸¸é‡å®šä¹‰
    private static final String EXPECTED_HEADER = "55AA01"; // åè®®å¤´
    private static final int MIN_LENGTH = 42; // æœ€å°é•¿åº¦(21字节*2字符)
    private static final ThreadLocal<ParseResult> RESULT_CACHE =
            ThreadLocal.withInitial(ParseResult::new);
    // è§£æžç»“果类
    public static class ParseResult {
        public int sequenceNum;   // åºåˆ—号
        public String tagId;      // æ ‡ç­¾ID(4字节小端序)
        public String anchorId;   // é”šç‚¹ID(4字节小端序)
        public int distance;      // è·ç¦»(毫米)
        public int power;         // ç”µé‡(0-100)
        public int buttonPressed; // æŒ‰é’®çŠ¶æ€
        public boolean buttonPressed2;
        public void reset() {
            sequenceNum = 0;
            tagId = "";
            anchorId = "";
            distance = 0;
            power = 0;
            buttonPressed = 0;
            buttonPressed2=false;
        }
    }
    /**
     * è§£æž55AA01协议数据
     * @param message åå…­è¿›åˆ¶å­—符串
     * @return è§£æžç»“æžœ(错误时返回null)
     */
    public static ParseResult parse(String message, String ip, int port) {
        if (message == null || message.isEmpty()) {
            return null;
        }
        // æ¸…洗数据:移除所有非十六进制字符
        char[] cleanedMessage = cleanMessage(message);
        // æ•°æ®æ ¡éªŒ
        if (cleanedMessage == null || cleanedMessage.length < MIN_LENGTH) {
            return null;
        }
        // åè®®å¤´æ ¡éªŒ (55AA01)
        if (!new String(cleanedMessage, 0, 6).equals(EXPECTED_HEADER)) {
            return null;
        }
        ParseResult result = RESULT_CACHE.get();
        result.reset();
        try {
            if (cleanedMessage.length < 30) { // ç¡®ä¿æœ‰è¶³å¤Ÿé•¿åº¦è®¿é—®charAt(28)
                return null;
            }
            // è§£æžåºåˆ—号 (位置8-9)
            result.sequenceNum = HexUtils.fastHexToByte(cleanedMessage[8], cleanedMessage[9]);
            // è§£æžæ ‡ç­¾ID (位置10-13, å°ç«¯åº)
            result.tagId = new String(new char[]{
                    cleanedMessage[12], cleanedMessage[13], // é«˜ä½
                    cleanedMessage[10], cleanedMessage[11]  // ä½Žä½
            });
            // è§£æžé”šç‚¹ID (位置14-17, å°ç«¯åº)
            result.anchorId = new String(new char[]{
                    cleanedMessage[16], cleanedMessage[17], // é«˜ä½
                    cleanedMessage[14], cleanedMessage[15]  // ä½Žä½
            });
            // è§£æžè·ç¦» (位置18-25, 4字节小端整数)
            int b0 = HexUtils.fastHexToByte(cleanedMessage[18], cleanedMessage[19]); // æœ€ä½Žä½
            int b1 = HexUtils.fastHexToByte(cleanedMessage[20], cleanedMessage[21]);
            int b2 = HexUtils.fastHexToByte(cleanedMessage[22], cleanedMessage[23]);
            int b3 = HexUtils.fastHexToByte(cleanedMessage[24], cleanedMessage[25]); // æœ€é«˜ä½
            int raw = (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
            result.distance = raw; // ä¿æŒåŽŸå§‹æ•´æ•°å€¼
            // è§£æžç”µé‡ (位置26-27)
            result.power = HexUtils.fastHexToByte(cleanedMessage[26], cleanedMessage[27]);
            // è§£æžæŒ‰é’®çŠ¶æ€ (位置28-29)
            result.buttonPressed = HexUtils.fastHexToByte(cleanedMessage[28], cleanedMessage[29]);
            result.buttonPressed2 =result.buttonPressed==1;
        } catch (IndexOutOfBoundsException | NumberFormatException e) {
            System.err.println("Parsing error in packet from " + ip + ":" + port);
            return null;
        }
        return result;
    }
    private static char[] cleanMessage(String message) {
        char[] cleaned = new char[message.length()];
        int j = 0;
        for (char c : message.toCharArray()) {
            if (Character.isDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) {
                cleaned[j++] = Character.toUpperCase(c);
            }
        }
        if (j == 0) return null;
        return Arrays.copyOf(cleaned, j);
    }
}
src/home/Dell55AA1FParser.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,184 @@
package home;
import java.util.Arrays;
public class Dell55AA1FParser {
    // å¸¸é‡å®šä¹‰
    private static final String EXPECTED_HEADER = "55AA1F"; // åè®®å¤´
    private static final int FIXED_FIELDS_LENGTH = 11; // ä»Žæ ‡ç­¾ID到保留字段的固定长度(字节)
    private static final ThreadLocal<ParseResult> RESULT_CACHE =
            ThreadLocal.withInitial(ParseResult::new);
    // è§£æžç»“果类
    public static class ParseResult {
        public int dataLength;    // æ•°æ®é•¿åº¦
        public int messageType;    // æ¶ˆæ¯ç±»åž‹
        public String tagId;       // æ ‡ç­¾ID(2字节)
        public int distance;       // è·ç¦»(厘米)
        public int angle;          // è§’度(度)
        public int signalQuality;  // ä¿¡å·è´¨é‡(0-255)
        public int buttonPressed;  // æŒ‰é’®çŠ¶æ€
        public int power;          // ç”µé‡
        public String reserved;    // ä¿ç•™å­—段
        public String userData;    // ç”¨æˆ·æ•°æ®
        public String checksum;    // æ ¡éªŒå’Œ
        public void reset() {
            dataLength = 0;
            messageType = 0;
            tagId = "";
            distance = 0;
            angle = 0;
            signalQuality = 0;
            buttonPressed = 0;
            power = 0;
            reserved = "";
            userData = "";
            checksum = "";
        }
    }
    /**
     * è§£æž55AA1F协议数据
     * @param message åå…­è¿›åˆ¶å­—符串
     * @return è§£æžç»“æžœ(错误时返回null)
     */
    public static ParseResult parse(String message, String ip, int port) {
        if (message == null || message.isEmpty()) {
            return null;
        }
        // æ¸…洗数据:移除所有非十六进制字符
        char[] cleanedMessage = cleanMessage(message);
        if (cleanedMessage == null) {
            return null;
        }
        // åè®®å¤´æ ¡éªŒ (55AA1F)
        if (cleanedMessage.length < 6 ||
            !new String(cleanedMessage, 0, 6).equals(EXPECTED_HEADER)) {
            return null;
        }
        ParseResult result = RESULT_CACHE.get();
        result.reset();
        try {
            // è§£æžæ•°æ®é•¿åº¦ (位置6-7)
            result.dataLength = HexUtils.fastHexToByte(cleanedMessage[6], cleanedMessage[7]);
            // è®¡ç®—期望的总字符长度
            int expectedCharLength = 8 + // åŒ…头(4字符) + æ¶ˆæ¯ç±»åž‹(2字符) + æ•°æ®é•¿åº¦(2字符)
                                    result.dataLength * 2 + // æ•°æ®éƒ¨åˆ†
                                    4; // æ ¡éªŒå’Œ(4字符)
//            if (cleanedMessage.length != expectedCharLength) {
//                System.err.println("Data length mismatch: expected " + expectedCharLength +
//                                 ", got " + cleanedMessage.length);
//                return null;
//            }
            // è§£æžæ¶ˆæ¯ç±»åž‹ (位置4-5)
            result.messageType = HexUtils.fastHexToByte(cleanedMessage[4], cleanedMessage[5]);
            // è§£æžæ ‡ç­¾ID (位置8-11),直接取字符串
            result.tagId = new String(cleanedMessage, 8, 4);
            // è§£æžè·ç¦» (位置12-15, 2字节小端整数)
            int distLow = HexUtils.fastHexToByte(cleanedMessage[12], cleanedMessage[13]);
            int distHigh = HexUtils.fastHexToByte(cleanedMessage[14], cleanedMessage[15]);
            result.distance = (distHigh << 8) | distLow;
            // è§£æžè§’度 (位置16-19, 2字节小端有符号整数)
            int angleLow = HexUtils.fastHexToByte(cleanedMessage[16], cleanedMessage[17]);
            int angleHigh = HexUtils.fastHexToByte(cleanedMessage[18], cleanedMessage[19]);
            short angleShort = (short) ((angleHigh << 8) | angleLow);
            result.angle = angleShort;
            // è§£æžä¿¡å·è´¨é‡ (位置20-21)
            result.signalQuality = HexUtils.fastHexToByte(cleanedMessage[20], cleanedMessage[21]);
            // è§£æžæŒ‰é’®çŠ¶æ€ (位置22-23)
            result.buttonPressed = HexUtils.fastHexToByte(cleanedMessage[22], cleanedMessage[23]);
            // è§£æžç”µé‡ (位置24-25)
            result.power = HexUtils.fastHexToByte(cleanedMessage[24], cleanedMessage[25]);
            // è§£æžä¿ç•™å­—段 (位置26-29)
            result.reserved = new String(cleanedMessage, 26, 4);
            // è§£æžç”¨æˆ·æ•°æ®
//            int userDataStart = 30;
//            int userDataCharLength = (result.dataLength - FIXED_FIELDS_LENGTH) * 2;
//            if (userDataCharLength > 0) {
//                result.userData = new String(cleanedMessage, userDataStart, userDataCharLength);
//            } else {
//                result.userData = "";
//            }
            // è§£æžæ ¡éªŒå’Œ (最后4个字符)
            result.checksum = new String(cleanedMessage, cleanedMessage.length - 4, 4);
            // éªŒè¯æ ¡éªŒå’Œ
//            byte[] packetBytes = hexStringToByteArray(new String(cleanedMessage));
//            if (!verifyChecksum(packetBytes)) {
//                System.err.println("Checksum verification failed for packet from " + ip + ":" + port);
//                return null;
//            }
        } catch (IndexOutOfBoundsException | NumberFormatException e) {
            System.err.println("Parsing error in 55AA1F packet from " + ip + ":" + port);
            e.printStackTrace();
            return null;
        }
        return result;
    }
    /**
     * å°†åå…­è¿›åˆ¶å­—符串转换为字节数组
     */
    private static byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                                 + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
    /**
     * æ ¡éªŒæ•°æ®åŒ…
     * åŽ»æŽ‰åŒ…å¤´æ±‚å’Œå–åï¼Œæ ¡éªŒç ä¸ºå°ç«¯æ¨¡å¼
     */
    private static boolean verifyChecksum(byte[] packet) {
        int len = packet.length;
        if (len < 4) return false;
        int sum = 0;
        // ä»Žæ¶ˆæ¯ç±»åž‹å¼€å§‹åˆ°æ ¡éªŒç å‰ç»“束 (跳过包头2字节)
        for (int i = 2; i < len - 2; i++) {
            sum += packet[i] & 0xFF;
        }
        sum = ~sum & 0xFFFF; // å–反并保留16位
        // èŽ·å–åŒ…ä¸­çš„æ ¡éªŒç  (小端模式)
        int receivedChecksum = ((packet[len - 2] & 0xFF) << 8) | (packet[len - 1] & 0xFF);
        return sum == receivedChecksum;
    }
    private static char[] cleanMessage(String message) {
        char[] cleaned = new char[message.length()];
        int j = 0;
        for (char c : message.toCharArray()) {
            if (Character.isDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) {
                cleaned[j++] = Character.toUpperCase(c);
            }
        }
        if (j == 0) return null;
        return Arrays.copyOf(cleaned, j);
    }
}
src/home/FirmwareUpgrader.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,216 @@
package home;
import java.io.*;
import java.util.Arrays;
import java.util.function.Consumer;
public class FirmwareUpgrader {
    private static final int SOH = 0x01;   // Start of Header
    private static final int STX = 0x02;   // Start of Text (for 1024-byte blocks)
    private static final int EOT = 0x04;   // End of Transmission
    private static final int ACK = 0x06;   // Acknowledge
    private static final int NAK = 0x15;   // Negative Acknowledge
    private static final int CAN = 0x18;   // Cancel
    private static final int C = 0x43;     // 'C' for CRC mode
    private final SerialPortService serialService;
    public FirmwareUpgrader(SerialPortService serialService) {
        this.serialService = serialService;
    }
    public void upgradeFirmware(String filePath, ProgressCallback progressCallback) throws IOException {
        File file = new File(filePath);
        if (!file.exists() || !file.isFile()) {
            throw new IOException("File not found: " + filePath);
        }
        // å‘送开始信号
        sendStartSignal();
        // å‘送文件
        sendFile(file, progressCallback);
        // å‘送结束信号
        sendEndSignal();
    }
    private void sendStartSignal() throws IOException {
        // å‘送'C'表示使用CRC校验
        serialService.send(new byte[]{C});
        // ç­‰å¾…ACK
        if (!waitForAck()) {
            throw new IOException("Device not ready for YModem transfer");
        }
    }
    private void sendFile(File file, ProgressCallback progressCallback) throws IOException {
        long fileSize = file.length();
        String fileName = file.getName();
        // å‘送文件头块
        byte[] header = new byte[133];
        Arrays.fill(header, (byte)0);
        header[0] = SOH;
        header[1] = 0x00; // å—编号
        header[2] = (byte)0xFF; // å—编号补码
        // æ–‡ä»¶åå’Œå¤§å°
        byte[] fileNameBytes = fileName.getBytes();
        System.arraycopy(fileNameBytes, 0, header, 3, Math.min(fileNameBytes.length, 99));
        String sizeStr = String.valueOf(fileSize);
        byte[] sizeBytes = sizeStr.getBytes();
        System.arraycopy(sizeBytes, 0, header, 3 + 99, Math.min(sizeBytes.length, 99));
        // è®¡ç®—CRC
        int crc = calculateCRC(header, 3, 128);
        header[131] = (byte)((crc >> 8) & 0xFF);
        header[132] = (byte)(crc & 0xFF);
        serialService.send(header);
        // ç­‰å¾…ACK
        if (!waitForAck()) {
            throw new IOException("Header not acknowledged");
        }
        // å‘送文件数据
        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            int blockNum = 1;
            long totalSent = 0;
            while ((bytesRead = fis.read(buffer)) != -1) {
                // å¡«å……不足的部分
                if (bytesRead < 1024) {
                    Arrays.fill(buffer, bytesRead, 1024, (byte)0x1A);
                }
                // å‘送数据块
                sendDataBlock(blockNum, buffer);
                blockNum++;
                totalSent += bytesRead;
                // æ›´æ–°è¿›åº¦
                int progress = (int)((totalSent * 100) / fileSize);
                progressCallback.onProgress(progress);
                // ç­‰å¾…ACK
                if (!waitForAck()) {
                    throw new IOException("Data block not acknowledged");
                }
            }
        }
    }
    private void sendDataBlock(int blockNum, byte[] data) throws IOException {
        byte[] block = new byte[1029];
        block[0] = STX; // 1024字节块
        block[1] = (byte)(blockNum & 0xFF);
        block[2] = (byte)(~blockNum & 0xFF);
        // æ‹·è´æ•°æ®
        System.arraycopy(data, 0, block, 3, data.length);
        // è®¡ç®—CRC
        int crc = calculateCRC(block, 3, 1024);
        block[1027] = (byte)((crc >> 8) & 0xFF);
        block[1028] = (byte)(crc & 0xFF);
        serialService.send(block);
    }
    private void sendEndSignal() throws IOException {
        // å‘送EOT
        serialService.send(new byte[]{EOT});
        // ç­‰å¾…ACK
        if (!waitForAck()) {
            throw new IOException("EOT not acknowledged");
        }
        // å‘送空文件头块表示结束
        byte[] endHeader = new byte[133];
        Arrays.fill(endHeader, (byte)0);
        endHeader[0] = SOH;
        endHeader[1] = 0x00; // å—编号
        endHeader[2] = (byte)0xFF; // å—编号补码
        // è®¡ç®—CRC
        int crc = calculateCRC(endHeader, 3, 128);
        endHeader[131] = (byte)((crc >> 8) & 0xFF);
        endHeader[132] = (byte)(crc & 0xFF);
        serialService.send(endHeader);
        // ç­‰å¾…ACK
        if (!waitForAck()) {
            throw new IOException("End header not acknowledged");
        }
    }
    private boolean waitForAck() {
        final Object lock = new Object();
        final boolean[] result = new boolean[1];
        final boolean[] receivedResponse = new boolean[1];
        // åˆ›å»ºä¸€ä¸ªä¸´æ—¶æ¶ˆè´¹è€…来处理接收到的数据
        Consumer<byte[]> ackConsumer = data -> {
            if (data.length > 0) {
                synchronized (lock) {
                    if (data[0] == ACK) {
                        result[0] = true;
                        receivedResponse[0] = true;
                    } else if (data[0] == NAK) {
                        result[0] = false;
                        receivedResponse[0] = true;
                    } else if (data[0] == CAN) {
                        throw new RuntimeException("Transfer canceled by device");
                    }
                    lock.notifyAll();
                }
            }
        };
        // æ³¨å†Œä¸´æ—¶æ¶ˆè´¹è€…
        serialService.setResponseConsumer(ackConsumer);
        long startTime = System.currentTimeMillis();
        synchronized (lock) {
            while (System.currentTimeMillis() - startTime < 5000 && !receivedResponse[0]) {
                try {
                    lock.wait(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return false;
                }
            }
        }
        // å–消注册临时消费者
        serialService.setResponseConsumer(null);
        return receivedResponse[0] && result[0];
    }
    private int calculateCRC(byte[] data, int offset, int length) {
        int crc = 0;
        for (int i = 0; i < length; i++) {
            crc = crc ^ (data[offset + i] << 8);
            for (int j = 0; j < 8; j++) {
                if ((crc & 0x8000) != 0) {
                    crc = (crc << 1) ^ 0x1021;
                } else {
                    crc = crc << 1;
                }
            }
        }
        return crc & 0xFFFF;
    }
    public interface ProgressCallback {
        void onProgress(int progress);
    }
}
src/home/HexUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package home;
public class HexUtils {
    // åå…­è¿›åˆ¶å­—符快速转换表 (ASCII范围内)
    private static final int[] HEX_VALUES = new int[128];
    static {
        for (int i = 0; i < HEX_VALUES.length; i++) {
            char c = (char) i;
            if (c >= '0' && c <= '9') HEX_VALUES[i] = c - '0';
            else if (c >= 'A' && c <= 'F') HEX_VALUES[i] = 10 + (c - 'A');
            else if (c >= 'a' && c <= 'f') HEX_VALUES[i] = 10 + (c - 'a');
            else HEX_VALUES[i] = -1;
        }
    }
    // çº¿ç¨‹å®‰å…¨çš„字符缓冲区 (初始大小256)
    private static final ThreadLocal<char[]> CHAR_BUF_CACHE =
        ThreadLocal.withInitial(() -> new char[256]);
    /**
     * èŽ·å–çº¿ç¨‹æœ¬åœ°å­—ç¬¦ç¼“å†²åŒº
     * @return å¯å¤ç”¨çš„char[256]缓冲区
     */
    public static char[] getThreadLocalBuffer() {
        return CHAR_BUF_CACHE.get();
    }
    /**
     * å¿«é€Ÿå°†ä¸¤ä¸ªåå…­è¿›åˆ¶å­—符转换为字节
     * @param c1 é«˜ä½å­—符 (0-9, A-F, a-f)
     * @param c2 ä½Žä½å­—符 (0-9, A-F, a-f)
     * @return è½¬æ¢åŽçš„字节值 (无效字符返回0)
     */
    public static int fastHexToByte(char c1, char c2) {
        int high = (c1 < 128) ? HEX_VALUES[c1] : -1;
        int low = (c2 < 128) ? HEX_VALUES[c2] : -1;
        if (high < 0 || low < 0) return 0;
        return (high << 4) | low;
    }
}
src/home/LogUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
package home;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Date; // ä¿®å¤ï¼šä½¿ç”¨ java.util.Date è€Œä¸æ˜¯ java.sql.Date
import java.text.SimpleDateFormat;
/**
 * æ—¥å¿—记录工具类
 */
public class LogUtil {
    private static final String LOG_DIR = "systemfile/logfile";
    private static final String LOG_FILE = LOG_DIR + "/openlog.txt";
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * è®°å½•程序启动日志
     */
    public static void logOpen() {
        ensureLogDirExists();
        String timestamp = DATE_FORMAT.format(new Date());
        writeLog("程序启动: " + timestamp + "\n");
    }
    /**
     * è®°å½•程序关闭日志并计算工作时长
     * @param startTime ç¨‹åºå¯åŠ¨æ—¶é—´æˆ³
     */
    public static void logClose(long startTime) {
        ensureLogDirExists();
        long endTime = System.currentTimeMillis();
        String closeTime = DATE_FORMAT.format(new Date(endTime));
        // è®¡ç®—工作时长
        long duration = endTime - startTime;
        String durationStr = formatDuration(duration);
        String log = "程序关闭: " + closeTime + "\n" +
                     "工作时长: " + durationStr + "\n" +
                     "-----------------------------------\n";
        writeLog(log);
    }
    /**
     * ç¡®ä¿æ—¥å¿—目录存在
     */
    private static void ensureLogDirExists() {
        File dir = new File(LOG_DIR);
        if (!dir.exists()) {
            boolean created = dir.mkdirs(); // åˆ›å»ºå¤šçº§ç›®å½•
            if (!created) {
                System.err.println("无法创建日志目录: " + LOG_DIR);
            }
        }
    }
    /**
     * æ ¼å¼åŒ–时长(毫秒→可读字符串)
     */
    private static String formatDuration(long millis) {
        long seconds = millis / 1000;
        long hours = seconds / 3600;
        long minutes = (seconds % 3600) / 60;
        seconds = seconds % 60;
        return String.format("%d小时 %d分钟 %d秒", hours, minutes, seconds);
    }
    /**
     * å†™å…¥æ—¥å¿—到文件(追加模式)
     */
    private static void writeLog(String content) {
        try (FileWriter fw = new FileWriter(LOG_FILE, true);
             BufferedWriter bw = new BufferedWriter(fw)) {
            bw.write(content);
            bw.flush(); // ç¡®ä¿å†…容立即写入
        } catch (IOException e) {
            System.err.println("写入日志失败: " + e.getMessage());
        }
    }
    public static void log(String string) {
        // TODO è‡ªåŠ¨ç”Ÿæˆçš„æ–¹æ³•å­˜æ ¹
    }
}
src/home/Mains.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package home;
import javax.swing.SwingUtilities;
public class Mains {
    // è®°å½•启动时间
    private static long startTime;
    public static void main(String[] args) {
        // è®°å½•启动时间
        startTime = System.currentTimeMillis();
        // è®°å½•打开日志
        LogUtil.logOpen();
        if (SingleInstanceLock.lock()) {
            try {
                // åœ¨äº‹ä»¶è°ƒåº¦çº¿ç¨‹ä¸­åˆ›å»ºå’Œæ˜¾ç¤ºGUI
                SwingUtilities.invokeLater(() -> {
                    try {
                        // åˆ›å»ºå¹¶æ˜¾ç¤ºä¸»ç•Œé¢
                        AOAFollowSystem system = new AOAFollowSystem();
                        system.setVisible(true);
                    } catch (Exception e) {
                        e.printStackTrace();
                        LogUtil.log("启动可视化界面时发生错误: " + e.getMessage());
                    }
                });
            } finally {
                // æ·»åР关闭钩子
                Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                    // è®°å½•关闭日志
                    LogUtil.logClose(startTime);
                    // é‡Šæ”¾å•实例锁
                    SingleInstanceLock.release();
                }));
            }
        } else {
            // æ˜¾ç¤ºç¨‹åºå·²åœ¨è¿è¡Œçš„警告
            SingleInstanceLock.showAlreadyRunningWarning();
            // é€€å‡ºç¨‹åº
            System.exit(0);
        }
    }
}
src/home/SerialPortService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
// å£°æ˜ŽåŒ…名 dell_system
package home;
// å¯¼å…¥ä¸²å£é€šä¿¡åº“
import com.fazecast.jSerialComm.SerialPort;
// å¯¼å…¥ Java å‡½æ•°å¼æŽ¥å£ Consumer,用于数据接收回调
import java.util.function.Consumer;
// å¯¼å…¥å­—节数组输出流
import java.io.ByteArrayOutputStream;
/**
 * ä¸²å£æœåŠ¡ç±»
 * è´Ÿè´£ä¸²å£çš„æ‰“开、关闭、数据收发
 */
public class SerialPortService {
    // ä¸²å£å¯¹è±¡ï¼Œç”¨äºŽä¸Žç¡¬ä»¶é€šä¿¡
    private SerialPort port;
    // æ ‡è®°æ˜¯å¦æ­£åœ¨æ•获数据(线程运行中)
    private volatile boolean capturing = false;
    // æ ‡è®°æ˜¯å¦æš‚停接收数据
    private volatile boolean paused    = false;
    // æ•°æ®è¯»å–线程
    private Thread readerThread;
    private Consumer<byte[]> responseConsumer;
    // ä¼˜åŒ–:重用缓冲区,减少内存分配
    private byte[] readBuffer = new byte[200];
    private ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
    /**
     * æ‰“开串口
     * @param portName ä¸²å£åç§°ï¼ˆå¦‚ COM3)
     * @param baud æ³¢ç‰¹çŽ‡ï¼ˆå¦‚ 9600)
     * @return æ˜¯å¦æˆåŠŸæ‰“å¼€
     */
    public boolean open(String portName, int baud) {
        // å¦‚果串口已打开,直接返回成功
        if (port != null && port.isOpen()) {
            return true;
        }
        // æ ¹æ®åç§°èŽ·å–ä¸²å£å®žä¾‹
        port = SerialPort.getCommPort(portName);
        // è®¾ç½®ä¸²å£å‚数:波特率、数据位8、停止位1、无校验位
        port.setComPortParameters(baud, 8, 1, SerialPort.NO_PARITY);
        // ä¿®æ”¹ä¸ºåŠé˜»å¡žæ¨¡å¼ï¼Œè¯»è¶…æ—¶1ms
        port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 1, 0);
        // æ‰“开串口并返回结果
        return port.openPort();
    }
    public void setResponseConsumer(Consumer<byte[]> consumer) {
        this.responseConsumer = consumer;
    }
    /**
     * å…³é—­ä¸²å£
     */
    public void close() {
        // å…ˆåœæ­¢æ•°æ®æŽ¥æ”¶çº¿ç¨‹
        stopCapture();
        // å¦‚果串口已打开,关闭它
        if (port != null && port.isOpen()) {
            port.closePort();
        }
        // é‡Šæ”¾ä¸²å£å¼•用
        port = null;
    }
    /**
     * å¯åŠ¨æ•°æ®æŽ¥æ”¶çº¿ç¨‹
     * @param onReceived æ•°æ®æŽ¥æ”¶å›žè°ƒå‡½æ•°ï¼Œæ”¶åˆ°æ•°æ®æ—¶è°ƒç”¨
     */
    public void startCapture(Consumer<byte[]> onReceived) {
        if (capturing || port == null || !port.isOpen()) return;
        capturing = true;
        paused = false;
        readerThread = new Thread(() -> {
            buffer.reset();
            long lastReceivedTime = 0;
            while (capturing && port.isOpen()) {
                if (paused) {
                    buffer.reset();
                    int len;
                    do {
                        len = port.readBytes(readBuffer, readBuffer.length);
                    } while (len > 0);
                    try { Thread.sleep(100); } catch (InterruptedException ignore) {}
                    continue;
                }
                long currentTime = System.currentTimeMillis();
                if (buffer.size() > 0 && (currentTime - lastReceivedTime) >= 20) {
                    byte[] data = buffer.toByteArray();
                    onReceived.accept(data);
                    if (responseConsumer != null) {
                        responseConsumer.accept(data);
                    }
                    buffer.reset();
                }
                int len = port.readBytes(readBuffer, readBuffer.length);
                currentTime = System.currentTimeMillis();
                if (len > 0) {
                    buffer.write(readBuffer, 0, len);
                    lastReceivedTime = currentTime;
                }
                if (len <= 0 && buffer.size() == 0) {
                    try { Thread.sleep(1); } catch (InterruptedException ignore) {}
                }
            }
            if (buffer.size() > 0) {
                byte[] data = buffer.toByteArray();
                onReceived.accept(data);
                if (responseConsumer != null) {
                    responseConsumer.accept(data);
                }
            }
        });
        readerThread.setDaemon(true);
        readerThread.start();
    }
    /**
     * åœæ­¢æ•°æ®æŽ¥æ”¶çº¿ç¨‹
     */
    public void stopCapture() {
        // è®¾ç½®æ•获标志为 false,通知线程退出
        capturing = false;
        // å¦‚果线程存在,等待最多500ms确保线程结束
        if (readerThread != null) {
            try { readerThread.join(500); } catch (InterruptedException ignore) {}
            // æ¸…空线程引用
            readerThread = null;
        }
    }
    // è®¾ç½®æš‚停状态
    public void setPaused(boolean paused) {
        this.paused = paused;
    }
    // èŽ·å–å½“å‰æ˜¯å¦æš‚åœ
    public boolean isPaused() {
        return paused;
    }
    // åˆ¤æ–­ä¸²å£æ˜¯å¦å·²æ‰“å¼€
    public boolean isOpen() {
        return port != null && port.isOpen();
    }
    /**
     * å‘送数据
     * @param data è¦å‘送的字节数组
     * @return æ˜¯å¦æˆåŠŸå‘é€
     */
    public boolean send(byte[] data) {
        // å¦‚果串口已初始化且已打开,发送数据并返回结果
        return port != null && port.isOpen() && port.writeBytes(data, data.length) > 0;
    }
}
src/home/SingleInstanceLock.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
package home;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import javax.swing.JOptionPane;
public class SingleInstanceLock {
    private static final String LOCK_FILE = System.getProperty("user.home") +
                                           File.separator + ".myapp.lock";
    private static FileLock lock;
    private static FileChannel channel;
    /**
     * å°è¯•获取应用程序锁
     * @return true-成功获取锁(第一次运行), false-获取失败(已有实例运行)
     */
    @SuppressWarnings("resource")
    public static boolean lock() {
        try {
            // åˆ›å»ºæ–‡ä»¶å¯¹è±¡
            File file = new File(LOCK_FILE);
            // å¦‚果文件存在但无法删除,可能是其他实例正在运行
            if (file.exists() && !file.delete()) {
                return false;
            }
            // åˆ›å»ºæ–‡ä»¶å¹¶èŽ·å–é€šé“
            RandomAccessFile raf = new RandomAccessFile(file, "rw");
            channel = raf.getChannel();
            // å°è¯•获取独占锁
            lock = channel.tryLock();
            // å¦‚果获取到锁,返回true
            return lock != null;
        } catch (IOException e) {
            // å‘生异常说明可能已有实例运行
            return false;
        }
    }
    /**
     * é‡Šæ”¾åº”用程序锁
     */
    public static void release() {
        try {
            // é‡Šæ”¾é”
            if (lock != null && lock.isValid()) {
                lock.release();
            }
            // å…³é—­é€šé“
            if (channel != null && channel.isOpen()) {
                channel.close();
            }
            // åˆ é™¤é”æ–‡ä»¶
            File file = new File(LOCK_FILE);
            if (file.exists()) {
                file.delete();
            }
        } catch (IOException e) {
            // é‡Šæ”¾é”æ—¶å¿½ç•¥å¼‚常
        }
    }
    /**
     * æ£€æŸ¥ç¨‹åºæ˜¯å¦å·²åœ¨è¿è¡Œå¹¶æ˜¾ç¤ºè­¦å‘Š
     */
    public static void showAlreadyRunningWarning() {
        JOptionPane.showMessageDialog(null,
            "程序已经打开,请勿重复打开",
            "警告",
            JOptionPane.WARNING_MESSAGE);
    }
}
src/home/VisualizationPanel.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,205 @@
package home;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
// å¯è§†åŒ–面板类
class VisualizationPanel extends JPanel {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    private int distance = 0;
    private int angle = 0;
    private AOAFollowSystem parentFrame;
    private String anchorid = "Anchor";
    private String tagid = "Tag";
    private double scaleFactor = 1.0; // ç¼©æ”¾å› å­
    private static final double MIN_SCALE = 0.2; // æœ€å°ç¼©æ”¾å€æ•°
    private static final double MAX_SCALE = 5.0; // æœ€å¤§ç¼©æ”¾å€æ•°
    // ä½¿ç”¨æ”¯æŒä¸­æ–‡çš„字体
    private Font boldFont = new Font("Microsoft YaHei", Font.BOLD, 14);
    private Font normalFont = new Font("Microsoft YaHei", Font.PLAIN, 12);
    private Font titleFont = new Font("Microsoft YaHei", Font.BOLD, 12);
    public VisualizationPanel(AOAFollowSystem parentFrame) {
        this.parentFrame = parentFrame;
        setPreferredSize(new Dimension(400, 400));
        setBorder(BorderFactory.createTitledBorder(parentFrame.getString("visualization")));
        // æ·»åŠ é¼ æ ‡æ»šè½®ç›‘å¬å™¨
        addMouseWheelListener(new MouseWheelListener() {
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                int notches = e.getWheelRotation();
                if (notches < 0) {
                    // å‘上滚动,放大
                    scaleFactor = Math.min(scaleFactor * 1.1, MAX_SCALE);
                } else {
                    // å‘下滚动,缩小
                    scaleFactor = Math.max(scaleFactor / 1.1, MIN_SCALE);
                }
                repaint();
            }
        });
    }
    public void updatePosition(int distance, int angle) {
        // ä¼˜åŒ–:只有数据变化时才重绘
        if (this.distance != distance || this.angle != angle) {
            this.distance = distance;
            // å°†è§’度范围转换为-180到180度,正下方为0度
            this.angle = normalizeAngle(angle);
            repaint();
        }
    }
    // è§’度归一化方法,将角度转换为-180到180度范围,正下方为0度
    private int normalizeAngle(int angle) {
        // é¦–先将角度转换为0-360度范围
        angle = angle % 360;
        if (angle < 0) {
            angle += 360;
        }
        // å°†0-360度转换为-180到180度,正下方为0度
        // 0度对应正下方,90度对应正左方,180/-180度对应正上方,-90度对应正右方
        int normalizedAngle = angle - 180;
        if (normalizedAngle > 180) {
            normalizedAngle -= 360;
        } else if (normalizedAngle <= -180) {
            normalizedAngle += 360;
        }
        return normalizedAngle;
    }
    public void setAnchorId(String anchorid) {
        this.anchorid = anchorid;
    }
    public void setTagId(String tagid) {
        this.tagid = tagid;
    }
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        // ç¡®ä¿ä½¿ç”¨æ”¯æŒä¸­æ–‡çš„字体
        g2d.setFont(normalFont);
        int centerX = getWidth() / 2;
        int centerY = getHeight()/ 2 - 30;
        int maxRadius = (int) (Math.min(centerX, centerY) * scaleFactor - 20);
        // è®¾ç½®èƒŒæ™¯é¢œè‰²
        g2d.setColor(new Color(240, 240, 240));
        g2d.fillRect(0, 0, getWidth(), getHeight());
        // ç»˜åˆ¶åæ ‡è½´
        g2d.setColor(Color.LIGHT_GRAY);
        g2d.drawLine(centerX, 0, centerX, getHeight());
        g2d.drawLine(0, centerY, getWidth(), centerY);
        // ç»˜åˆ¶è§’度刻度线 (-180° åˆ° 180°)
        g2d.setColor(new Color(150, 150, 150));
        for (int i = -180; i <= 180; i += 30) {
            // è½¬æ¢ä¸ºå¼§åº¦ï¼Œæ­£ä¸‹æ–¹ä¸º0度
            double rad = Math.toRadians(i);
            int x1 = centerX + (int) (maxRadius * Math.sin(rad));
            int y1 = centerY - (int) (maxRadius * Math.cos(rad));
            g2d.setColor(new Color(100, 100, 100));
            g2d.drawLine(centerX, centerY, x1, y1);
            // ç»˜åˆ¶è§’度标签
            int labelX = centerX + (int) ((maxRadius + 15) * Math.sin(rad));
            int labelY = centerY - (int) ((maxRadius + 15) * Math.cos(rad));
            g2d.setColor(Color.blue);
            g2d.drawString(i + "°", labelX - 10, labelY + 5);
        }
        // ç»˜åˆ¶è·ç¦»åœ†åœˆå’Œæ ‡ç­¾
        g2d.setColor(new Color(100, 100, 100));
        for (int i = 1; i <= 5; i++) {
            int radius = maxRadius * i / 5;
            g2d.drawOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
            // ä¿®æ”¹ï¼šå°†è·ç¦»æ ‡ç­¾æ˜¾ç¤ºåœ¨åœ†åœˆæ­£å—方中间
            String distanceLabel = (int)(i * 200 / scaleFactor) + "cm";
            int labelWidth = g2d.getFontMetrics().stringWidth(distanceLabel);
            g2d.drawString(distanceLabel, centerX - labelWidth / 2, centerY + radius + 15);
        }
        // ç»˜åˆ¶A点(锚点)
        g2d.setColor(Color.BLACK);
        g2d.fillOval(centerX - 5, centerY - 5, 10, 10);
        // A点正上方显示设备编号
        g2d.setFont(titleFont);
        g2d.drawString(anchorid, centerX - g2d.getFontMetrics().stringWidth(anchorid)/2, centerY - 15);
        g2d.setFont(normalFont);
        // ç»˜åˆ¶B点(标签点)
        if (distance > 0) {
            // æ ¹æ®è·ç¦»è®¡ç®—缩放比例(最大1000厘米)
            double scaledDistance = Math.min(distance, 1000) * maxRadius / 1000.0;
            double rad = Math.toRadians(angle);
            int bX = centerX + (int) (scaledDistance * Math.sin(rad));
            int bY = centerY - (int) (scaledDistance * Math.cos(rad));
            // ç»˜åˆ¶è¿žæŽ¥çº¿
            g2d.setColor(new Color(0, 100, 200, 150));
            g2d.drawLine(centerX, centerY, bX, bY);
            // B点正下方显示距离和角度信息
            String distanceText = distance + "cm";
            String angleText = angle + "°";
            // è®¾ç½®åŠ ç²—å­—ä½“å’ŒåŠ å¤§å­—å·
            g2d.setFont(boldFont);
            g2d.setColor(Color.RED);
            // è®¡ç®—文本位置(B点正下方)
            int textY = bY + 25;
            g2d.drawString(distanceText, bX - g2d.getFontMetrics().stringWidth(distanceText)/2, textY);
            g2d.drawString(angleText, bX - g2d.getFontMetrics().stringWidth(angleText)/2, textY + 20);
            // æ¢å¤åŽŸå§‹å­—ä½“
            g2d.setFont(normalFont);
            // åœ¨é¢æ¿åº•部显示距离和角度信息 - å®žæ—¶èŽ·å–å­—ç¬¦ä¸²ï¼Œä¸ä½¿ç”¨ç¼“å­˜
            g2d.setFont(boldFont);
            // ç›´æŽ¥ä»Žçˆ¶æ¡†æž¶èŽ·å–å½“å‰è¯­è¨€çš„å­—ç¬¦ä¸²
            g2d.drawString(parentFrame.getString("distance") + distance + "cm", 10, 40);
            g2d.drawString(parentFrame.getString("angle") + angle + "°", 10, 70);
            g2d.setFont(normalFont);
            g2d.setColor(Color.BLUE);
            g2d.fillOval(bX - 5, bY - 5, 10, 10);
            // B点正上方显示设备编号
            g2d.setFont(titleFont);
            g2d.drawString(tagid, bX - g2d.getFontMetrics().stringWidth(tagid)/2, bY - 15);
            g2d.setFont(normalFont);
        }
        // æ˜¾ç¤ºå½“前缩放比例
        g2d.setColor(Color.DARK_GRAY);
        g2d.drawString(String.format("缩放: %.1fx", scaleFactor), getWidth() - 80, 20);
    }
    public void updateLanguage() {
        setBorder(BorderFactory.createTitledBorder(parentFrame.getString("visualization")));
        // ç§»é™¤å­—符串缓存,改为在paintComponent中实时获取
        repaint();
    }
}
systemfile/Messages_en.properties
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
angle=Angle:
distance=Distance:
LANGUAGE=Language
display_format=Display Format
serial_port=Serial Port
baud_rate=Baud Rate
close_serial=Close Serial
home=Home
config=Configuration
device_id=Device ID
group=Communication Group
frequency=Communication Frequency
read_config=Read Configuration
save_config=Save Configuration
serial_port=Serial Port
baud_rate=Baud Rate
open_serial=Open Serial Port
start=Start
pause=Pause
clear=Clear
send=Send
device_id_table=Device ID
distance_table=Real-time Distance
angle_table=Real-time Angle
signal_table=Signal Quality
power_table=Power Level
button_table=Button
time_table=Time
log=Log
 send_data=Send Data
 select_serial_port=Please select a serial port
 error=Error
 open_serial_first=Please open the serial port first
 input_data_to_send=Please input data to send
 send_failed=Send failed
 receive=Receive
 visualization=Visualization
 config_read_success=Configuration read successfully
 info=Information
 input_valid_number=Please input valid numbers
 config_save_success=Configuration saved successfully
 select_file=Select File
upgrade=Upgrade
upgrade_progress=Upgrade Progress
select_bin_file=Please select bin file
upgrade_success=Upgrade Success
upgrade_failed=Upgrade Failed
hex=HEX
ascii=ASCII
hex_send=HEX Send
close_serial=Close Serial
systemfile/Messages_zh.properties
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
angle=角度:
distance=距离:
LANGUAGE=语言
display_format=显示格式
serial_port=串口
baud_rate=波特率
close_serial=关闭串口
home=首页
config=配置
device_id=设备编号
group=通信小组
frequency=通信频率
read_config=读取配置
save_config=保存配置
serial_port=串口
baud_rate=波特率
open_serial=打开串口
start=开始
pause=暂停
clear=清空
send=发送
device_id_table=设备编号
distance_table=实时距离
angle_table=实时角度
signal_table=信号质量
power_table=电量
button_table=按键
time_table=时间
log=日志
 send_data=发送数据
 select_serial_port=请选择串口
 error=错误
 open_serial_first=请先打开串口
 input_data_to_send=请输入要发送的数据
 send_failed=发送失败
 receive=接收
 visualization=可视化
 config_read_success=配置读取成功
 info=提示
 input_valid_number=请输入有效的数字
 config_save_success=配置保存成功
 select_file=选择文件
upgrade=升级
upgrade_progress=升级进度
select_bin_file=请选择bin文件
upgrade_success=升级成功
upgrade_failed=升级失败
hex=HEX
ascii=ASCII
hex_send=HEX发送
close_serial=关闭串口
systemfile/logfile/openlog.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,382 @@
程序启动: 2025-08-20 22:25:39
程序关闭: 2025-08-20 22:27:05
工作时长: 0小时 1分钟 26秒
-----------------------------------
程序启动: 2025-08-20 22:27:08
程序关闭: 2025-08-20 22:27:10
工作时长: 0小时 0分钟 1秒
-----------------------------------
程序启动: 2025-08-21 22:06:32
程序启动: 2025-08-21 22:22:27
程序关闭: 2025-08-21 22:22:31
工作时长: 0小时 15分钟 59秒
-----------------------------------
程序启动: 2025-08-21 22:22:39
程序关闭: 2025-08-21 22:23:26
工作时长: 0小时 0分钟 46秒
-----------------------------------
程序启动: 2025-08-21 22:33:26
程序关闭: 2025-08-21 22:34:02
工作时长: 0小时 0分钟 35秒
-----------------------------------
程序启动: 2025-08-21 22:52:48
程序关闭: 2025-08-21 22:53:09
工作时长: 0小时 0分钟 21秒
-----------------------------------
程序启动: 2025-08-21 23:02:51
程序关闭: 2025-08-21 23:03:18
工作时长: 0小时 0分钟 26秒
-----------------------------------
程序启动: 2025-08-21 23:03:48
程序关闭: 2025-08-21 23:04:58
工作时长: 0小时 1分钟 10秒
-----------------------------------
程序启动: 2025-08-21 23:07:06
程序关闭: 2025-08-21 23:10:42
工作时长: 0小时 3分钟 35秒
-----------------------------------
程序启动: 2025-08-21 23:10:43
程序关闭: 2025-08-21 23:16:26
工作时长: 0小时 5分钟 42秒
-----------------------------------
程序启动: 2025-08-21 23:16:30
程序关闭: 2025-08-21 23:16:53
工作时长: 0小时 0分钟 23秒
-----------------------------------
程序启动: 2025-08-23 09:40:59
程序关闭: 2025-08-23 10:18:56
工作时长: 0小时 37分钟 56秒
-----------------------------------
程序启动: 2025-08-23 10:19:01
程序启动: 2025-08-23 10:32:55
程序关闭: 2025-08-23 10:32:59
工作时长: 0小时 13分钟 58秒
-----------------------------------
程序启动: 2025-08-23 10:33:01
程序启动: 2025-08-23 10:40:28
程序关闭: 2025-08-23 10:40:31
工作时长: 0小时 7分钟 30秒
-----------------------------------
程序启动: 2025-08-23 10:40:33
程序关闭: 2025-08-23 10:40:40
工作时长: 0小时 0分钟 7秒
-----------------------------------
程序启动: 2025-08-23 10:44:02
程序关闭: 2025-08-23 10:44:10
工作时长: 0小时 0分钟 8秒
-----------------------------------
程序启动: 2025-08-23 11:02:49
程序关闭: 2025-08-23 11:02:50
工作时长: 0小时 0分钟 1秒
-----------------------------------
程序启动: 2025-08-23 11:03:13
程序关闭: 2025-08-23 11:03:15
工作时长: 0小时 0分钟 1秒
-----------------------------------
程序启动: 2025-08-23 11:08:59
程序关闭: 2025-08-23 11:20:47
工作时长: 0小时 11分钟 48秒
-----------------------------------
程序启动: 2025-08-23 11:20:49
程序关闭: 2025-08-23 11:45:08
工作时长: 0小时 24分钟 19秒
-----------------------------------
程序启动: 2025-08-23 11:45:10
程序关闭: 2025-08-23 11:45:11
工作时长: 0小时 0分钟 1秒
-----------------------------------
程序启动: 2025-08-23 11:46:25
程序关闭: 2025-08-23 11:52:37
工作时长: 0小时 6分钟 11秒
-----------------------------------
程序启动: 2025-08-23 11:52:38
程序启动: 2025-08-23 11:53:48
程序关闭: 2025-08-23 11:53:51
工作时长: 0小时 1分钟 12秒
-----------------------------------
程序启动: 2025-08-23 11:53:53
程序关闭: 2025-08-23 12:20:32
工作时长: 0小时 26分钟 39秒
-----------------------------------
程序启动: 2025-08-23 12:20:34
程序关闭: 2025-08-23 12:21:04
工作时长: 0小时 0分钟 30秒
-----------------------------------
程序启动: 2025-08-23 12:24:07
程序关闭: 2025-08-23 12:32:02
工作时长: 0小时 7分钟 54秒
-----------------------------------
程序启动: 2025-08-23 12:32:05
程序关闭: 2025-08-23 12:32:20
工作时长: 0小时 0分钟 15秒
-----------------------------------
程序启动: 2025-08-23 12:32:52
程序启动: 2025-08-23 12:34:29
程序关闭: 2025-08-23 12:34:46
工作时长: 0小时 0分钟 16秒
-----------------------------------
程序启动: 2025-08-23 12:35:10
程序启动: 2025-08-23 12:36:54
程序关闭: 2025-08-23 12:36:55
工作时长: 0小时 0分钟 1秒
-----------------------------------
程序启动: 2025-08-23 12:37:21
程序关闭: 2025-08-23 12:50:13
工作时长: 0小时 12分钟 52秒
-----------------------------------
程序启动: 2025-08-23 12:50:15
程序关闭: 2025-08-23 12:55:28
工作时长: 0小时 5分钟 12秒
-----------------------------------
程序启动: 2025-08-23 12:55:30
程序关闭: 2025-08-23 12:55:45
工作时长: 0小时 0分钟 15秒
-----------------------------------
程序启动: 2025-08-23 12:59:36
程序启动: 2025-08-23 12:59:46
程序启动: 2025-08-23 12:59:50
程序启动: 2025-08-23 13:00:14
程序关闭: 2025-08-23 13:00:31
工作时长: 0小时 0分钟 16秒
-----------------------------------
程序启动: 2025-08-23 13:01:24
程序启动: 2025-08-23 13:07:11
程序关闭: 2025-08-23 13:07:14
工作时长: 0小时 5分钟 50秒
-----------------------------------
程序启动: 2025-08-23 13:07:16
程序关闭: 2025-08-23 13:09:02
工作时长: 0小时 1分钟 45秒
-----------------------------------
程序启动: 2025-08-23 13:11:28
程序关闭: 2025-08-23 13:11:50
工作时长: 0小时 0分钟 22秒
-----------------------------------
程序启动: 2025-08-23 13:15:40
程序关闭: 2025-08-23 13:16:03
工作时长: 0小时 0分钟 23秒
-----------------------------------
程序启动: 2025-08-23 13:18:57
程序启动: 2025-08-23 13:26:38
程序关闭: 2025-08-23 13:27:34
工作时长: 0小时 0分钟 56秒
-----------------------------------
程序启动: 2025-08-23 13:30:01
程序关闭: 2025-08-23 13:43:02
工作时长: 0小时 13分钟 1秒
-----------------------------------
程序启动: 2025-08-23 13:47:21
程序关闭: 2025-08-23 13:47:27
工作时长: 0小时 0分钟 6秒
-----------------------------------
程序启动: 2025-08-23 13:49:29
程序关闭: 2025-08-23 13:55:16
工作时长: 0小时 5分钟 47秒
-----------------------------------
程序启动: 2025-08-23 13:55:18
程序关闭: 2025-08-23 13:55:42
工作时长: 0小时 0分钟 23秒
-----------------------------------
程序启动: 2025-08-23 13:57:28
程序关闭: 2025-08-23 14:00:15
工作时长: 0小时 2分钟 47秒
-----------------------------------
程序启动: 2025-08-23 14:04:46
程序关闭: 2025-08-23 14:06:11
工作时长: 0小时 1分钟 24秒
-----------------------------------
程序启动: 2025-08-23 14:10:00
程序关闭: 2025-08-23 14:10:50
工作时长: 0小时 0分钟 50秒
-----------------------------------
程序启动: 2025-08-23 14:11:46
程序关闭: 2025-08-23 14:12:00
工作时长: 0小时 0分钟 14秒
-----------------------------------
程序启动: 2025-08-23 14:12:45
程序关闭: 2025-08-23 14:14:12
工作时长: 0小时 1分钟 27秒
-----------------------------------
程序启动: 2025-08-23 14:15:18
程序关闭: 2025-08-23 14:15:41
工作时长: 0小时 0分钟 23秒
-----------------------------------
程序启动: 2025-08-23 14:16:02
程序关闭: 2025-08-23 14:16:33
工作时长: 0小时 0分钟 30秒
-----------------------------------
程序启动: 2025-08-23 14:16:45
程序关闭: 2025-08-23 14:20:49
工作时长: 0小时 4分钟 4秒
-----------------------------------
程序启动: 2025-08-23 14:20:52
程序关闭: 2025-08-23 14:22:58
工作时长: 0小时 2分钟 6秒
-----------------------------------
程序启动: 2025-08-23 14:24:55
程序关闭: 2025-08-23 14:36:39
工作时长: 0小时 11分钟 43秒
-----------------------------------
程序启动: 2025-08-23 14:36:41
程序关闭: 2025-08-23 14:51:47
工作时长: 0小时 15分钟 5秒
-----------------------------------
程序启动: 2025-08-23 14:51:48
程序关闭: 2025-08-23 14:52:27
工作时长: 0小时 0分钟 38秒
-----------------------------------
程序启动: 2025-08-23 14:53:43
程序关闭: 2025-08-23 14:56:54
工作时长: 0小时 3分钟 10秒
-----------------------------------
程序启动: 2025-08-23 14:56:56
程序关闭: 2025-08-23 14:58:50
工作时长: 0小时 1分钟 54秒
-----------------------------------
程序启动: 2025-08-23 15:01:18
程序关闭: 2025-08-23 15:01:46
工作时长: 0小时 0分钟 27秒
-----------------------------------
程序启动: 2025-08-23 15:02:42
程序关闭: 2025-08-23 15:02:55
工作时长: 0小时 0分钟 13秒
-----------------------------------
程序启动: 2025-08-23 15:07:22
程序关闭: 2025-08-23 15:07:43
工作时长: 0小时 0分钟 21秒
-----------------------------------
程序启动: 2025-08-23 15:09:08
程序关闭: 2025-08-23 15:09:20
工作时长: 0小时 0分钟 11秒
-----------------------------------
程序启动: 2025-08-23 15:13:00
程序关闭: 2025-08-23 15:13:14
工作时长: 0小时 0分钟 14秒
-----------------------------------
程序启动: 2025-08-23 15:16:58
程序关闭: 2025-08-23 15:17:26
工作时长: 0小时 0分钟 28秒
-----------------------------------
程序启动: 2025-08-23 15:21:38
程序关闭: 2025-08-23 15:30:58
工作时长: 0小时 9分钟 20秒
-----------------------------------
程序启动: 2025-08-24 22:40:40
程序关闭: 2025-08-24 22:40:51
工作时长: 0小时 0分钟 11秒
-----------------------------------
程序启动: 2025-08-25 14:33:34
程序启动: 2025-08-25 15:38:47
程序启动: 2025-08-25 16:02:07
程序启动: 2025-08-25 16:08:36
程序关闭: 2025-08-25 16:20:36
工作时长: 0小时 11分钟 59秒
-----------------------------------
程序启动: 2025-08-25 16:20:38
程序关闭: 2025-08-25 16:20:54
工作时长: 0小时 0分钟 16秒
-----------------------------------
程序启动: 2025-08-25 16:21:39
程序关闭: 2025-08-25 16:21:48
工作时长: 0小时 0分钟 8秒
-----------------------------------
程序启动: 2025-08-25 16:26:43
程序关闭: 2025-08-25 16:27:15
工作时长: 0小时 0分钟 31秒
-----------------------------------
程序启动: 2025-08-25 16:36:31
程序关闭: 2025-08-25 16:44:31
工作时长: 0小时 7分钟 59秒
-----------------------------------
程序启动: 2025-08-25 21:47:03
程序关闭: 2025-08-25 21:47:25
工作时长: 0小时 0分钟 22秒
-----------------------------------
程序启动: 2025-08-25 21:47:39
程序关闭: 2025-08-25 21:48:51
工作时长: 0小时 1分钟 11秒
-----------------------------------
程序启动: 2025-08-25 21:49:22
程序关闭: 2025-08-25 21:52:09
工作时长: 0小时 2分钟 46秒
-----------------------------------
程序启动: 2025-08-25 21:52:10
程序关闭: 2025-08-25 21:53:22
工作时长: 0小时 1分钟 11秒
-----------------------------------
程序启动: 2025-08-25 21:53:28
程序关闭: 2025-08-25 21:53:46
工作时长: 0小时 0分钟 17秒
-----------------------------------
程序启动: 2025-08-25 21:54:00
程序关闭: 2025-08-25 22:13:45
工作时长: 0小时 19分钟 44秒
-----------------------------------
程序启动: 2025-08-25 22:14:44
程序关闭: 2025-08-25 22:14:56
工作时长: 0小时 0分钟 12秒
-----------------------------------
程序启动: 2025-08-25 22:15:26
程序关闭: 2025-08-25 22:15:54
工作时长: 0小时 0分钟 27秒
-----------------------------------
程序启动: 2025-08-25 22:15:58
程序关闭: 2025-08-25 22:16:19
工作时长: 0小时 0分钟 20秒
-----------------------------------
程序启动: 2025-08-25 22:18:58
程序启动: 2025-08-25 22:24:19
程序关闭: 2025-08-25 22:27:56
工作时长: 0小时 8分钟 58秒
-----------------------------------
程序启动: 2025-08-25 22:27:58
程序启动: 2025-08-25 22:31:32
程序关闭: 2025-08-25 22:31:42
工作时长: 0小时 0分钟 9秒
-----------------------------------
程序启动: 2025-08-25 22:33:09
程序关闭: 2025-08-25 22:33:36
工作时长: 0小时 0分钟 26秒
-----------------------------------
程序启动: 2025-08-25 22:35:48
程序启动: 2025-08-25 22:36:24
程序关闭: 2025-08-25 22:36:28
工作时长: 0小时 0分钟 40秒
-----------------------------------
程序启动: 2025-08-25 22:36:30
程序关闭: 2025-08-25 22:37:35
工作时长: 0小时 1分钟 5秒
-----------------------------------
程序启动: 2025-08-25 22:48:24
程序关闭: 2025-08-25 22:59:07
工作时长: 0小时 10分钟 43秒
-----------------------------------
程序启动: 2025-08-25 23:04:12
程序关闭: 2025-08-25 23:04:23
工作时长: 0小时 0分钟 11秒
-----------------------------------
程序启动: 2025-08-25 23:04:36
程序关闭: 2025-08-25 23:04:44
工作时长: 0小时 0分钟 8秒
-----------------------------------
程序启动: 2025-08-26 14:02:07
程序关闭: 2025-08-26 14:03:39
工作时长: 0小时 1分钟 31秒
-----------------------------------
程序启动: 2025-08-26 14:08:31
程序关闭: 2025-08-26 14:08:42
工作时长: 0小时 0分钟 10秒
-----------------------------------
程序启动: 2025-08-26 14:15:00
程序关闭: 2025-08-26 14:16:29
工作时长: 0小时 1分钟 29秒
-----------------------------------
程序启动: 2025-08-26 16:01:22
程序关闭: 2025-08-26 16:02:35
工作时长: 0小时 1分钟 13秒
-----------------------------------
程序启动: 2025-08-26 16:02:37
程序关闭: 2025-08-26 17:12:18
工作时长: 1小时 9分钟 40秒
-----------------------------------
程序启动: 2025-08-26 17:12:20