From 352da282b6c21700eb454407b92cabcf169a448e Mon Sep 17 00:00:00 2001 From: 826220679@qq.com <826220679@qq.com> Date: 星期二, 26 八月 2025 17:21:27 +0800 Subject: [PATCH] 首次提交AOA自动跟随 --- .classpath | 11 bin/home/Mains.class | 0 bin/home/VisualizationPanel$1.class | 0 bin/home/FirmwareUpgrader.class | 0 src/home/SingleInstanceLock.java | 77 + systemfile/logfile/openlog.txt | 382 ++++++++ src/home/FirmwareUpgrader.java | 216 ++++ bin/home/VisualizationPanel.class | 0 bin/home/Dell55AA01Parser.class | 0 src/home/LogUtil.java | 87 + src/home/VisualizationPanel.java | 205 ++++ src/home/AOAFollowSystem.java | 775 +++++++++++++++++ bin/home/ConfigPanel.class | 0 bin/home/AOAFollowSystem$HomePanel$2.class | 0 bin/home/AOAFollowSystem.class | 0 bin/home/Dell55AA1FParser$ParseResult.class | 0 src/home/Dell55AA1FParser.java | 184 ++++ bin/home/Dell55AA01Parser$ParseResult.class | 0 bin/home/Dell55AA1FParser.class | 0 bin/home/ConfigPanel$1.class | 0 bin/home/AOAFollowSystem$HomePanel.class | 0 bin/home/AOAFollowSystem$1.class | 0 lib/jSerialComm-2.10.4.jar | 0 src/home/Dell55AA01Parser.java | 111 ++ src/home/HexUtils.java | 40 .project | 17 bin/home/LogUtil.class | 0 bin/home/FirmwareUpgrader$ProgressCallback.class | 0 bin/home/AOAFollowSystem$HomePanel$1.class | 0 systemfile/Messages_en.properties | 51 + src/home/Mains.java | 44 systemfile/Messages_zh.properties | 51 + src/home/SerialPortService.java | 171 +++ src/home/ConfigPanel.java | 251 +++++ bin/home/SerialPortService.class | 0 bin/home/SingleInstanceLock.class | 0 bin/home/HexUtils.class | 0 37 files changed, 2,673 insertions(+), 0 deletions(-) diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..f7fc6b9 --- /dev/null +++ b/.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> diff --git a/.project b/.project new file mode 100644 index 0000000..12c187d --- /dev/null +++ b/.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> diff --git a/bin/home/AOAFollowSystem$1.class b/bin/home/AOAFollowSystem$1.class new file mode 100644 index 0000000..a4024a2 --- /dev/null +++ b/bin/home/AOAFollowSystem$1.class Binary files differ diff --git a/bin/home/AOAFollowSystem$HomePanel$1.class b/bin/home/AOAFollowSystem$HomePanel$1.class new file mode 100644 index 0000000..d46b37e --- /dev/null +++ b/bin/home/AOAFollowSystem$HomePanel$1.class Binary files differ diff --git a/bin/home/AOAFollowSystem$HomePanel$2.class b/bin/home/AOAFollowSystem$HomePanel$2.class new file mode 100644 index 0000000..8ca4a8f --- /dev/null +++ b/bin/home/AOAFollowSystem$HomePanel$2.class Binary files differ diff --git a/bin/home/AOAFollowSystem$HomePanel.class b/bin/home/AOAFollowSystem$HomePanel.class new file mode 100644 index 0000000..7c2825e --- /dev/null +++ b/bin/home/AOAFollowSystem$HomePanel.class Binary files differ diff --git a/bin/home/AOAFollowSystem.class b/bin/home/AOAFollowSystem.class new file mode 100644 index 0000000..86ebcc8 --- /dev/null +++ b/bin/home/AOAFollowSystem.class Binary files differ diff --git a/bin/home/ConfigPanel$1.class b/bin/home/ConfigPanel$1.class new file mode 100644 index 0000000..d07c2d3 --- /dev/null +++ b/bin/home/ConfigPanel$1.class Binary files differ diff --git a/bin/home/ConfigPanel.class b/bin/home/ConfigPanel.class new file mode 100644 index 0000000..b11a0f7 --- /dev/null +++ b/bin/home/ConfigPanel.class Binary files differ diff --git a/bin/home/Dell55AA01Parser$ParseResult.class b/bin/home/Dell55AA01Parser$ParseResult.class new file mode 100644 index 0000000..db29366 --- /dev/null +++ b/bin/home/Dell55AA01Parser$ParseResult.class Binary files differ diff --git a/bin/home/Dell55AA01Parser.class b/bin/home/Dell55AA01Parser.class new file mode 100644 index 0000000..2549b15 --- /dev/null +++ b/bin/home/Dell55AA01Parser.class Binary files differ diff --git a/bin/home/Dell55AA1FParser$ParseResult.class b/bin/home/Dell55AA1FParser$ParseResult.class new file mode 100644 index 0000000..5179218 --- /dev/null +++ b/bin/home/Dell55AA1FParser$ParseResult.class Binary files differ diff --git a/bin/home/Dell55AA1FParser.class b/bin/home/Dell55AA1FParser.class new file mode 100644 index 0000000..82d8494 --- /dev/null +++ b/bin/home/Dell55AA1FParser.class Binary files differ diff --git a/bin/home/FirmwareUpgrader$ProgressCallback.class b/bin/home/FirmwareUpgrader$ProgressCallback.class new file mode 100644 index 0000000..f7d0ce8 --- /dev/null +++ b/bin/home/FirmwareUpgrader$ProgressCallback.class Binary files differ diff --git a/bin/home/FirmwareUpgrader.class b/bin/home/FirmwareUpgrader.class new file mode 100644 index 0000000..351e115 --- /dev/null +++ b/bin/home/FirmwareUpgrader.class Binary files differ diff --git a/bin/home/HexUtils.class b/bin/home/HexUtils.class new file mode 100644 index 0000000..b382534 --- /dev/null +++ b/bin/home/HexUtils.class Binary files differ diff --git a/bin/home/LogUtil.class b/bin/home/LogUtil.class new file mode 100644 index 0000000..c5e73ad --- /dev/null +++ b/bin/home/LogUtil.class Binary files differ diff --git a/bin/home/Mains.class b/bin/home/Mains.class new file mode 100644 index 0000000..4b597f2 --- /dev/null +++ b/bin/home/Mains.class Binary files differ diff --git a/bin/home/SerialPortService.class b/bin/home/SerialPortService.class new file mode 100644 index 0000000..7f8e9ea --- /dev/null +++ b/bin/home/SerialPortService.class Binary files differ diff --git a/bin/home/SingleInstanceLock.class b/bin/home/SingleInstanceLock.class new file mode 100644 index 0000000..7dd3e7d --- /dev/null +++ b/bin/home/SingleInstanceLock.class Binary files differ diff --git a/bin/home/VisualizationPanel$1.class b/bin/home/VisualizationPanel$1.class new file mode 100644 index 0000000..0fbade4 --- /dev/null +++ b/bin/home/VisualizationPanel$1.class Binary files differ diff --git a/bin/home/VisualizationPanel.class b/bin/home/VisualizationPanel.class new file mode 100644 index 0000000..b288737 --- /dev/null +++ b/bin/home/VisualizationPanel.class Binary files differ diff --git a/lib/jSerialComm-2.10.4.jar b/lib/jSerialComm-2.10.4.jar new file mode 100644 index 0000000..2a29395 --- /dev/null +++ b/lib/jSerialComm-2.10.4.jar Binary files differ diff --git a/src/home/AOAFollowSystem.java b/src/home/AOAFollowSystem.java new file mode 100644 index 0000000..6b1c318 --- /dev/null +++ b/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; // 鏂板锛氫覆鍙f爣绛� + 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<>(); + // 娣诲姞寮瑰嚭鑿滃崟鐩戝惉鍣紝鐐瑰嚮鏃跺埛鏂颁覆鍙e垪琛� + 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()); + + // 鏂板锛欻EX鍙戦�佸閫夋 + 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, "鏃犳晥鐨凥EX鏍煎紡", 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) { + // 浼樺寲锛氶噸鐢⊿tringBuilder瀵硅薄 + 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); + // 浼樺寲锛氶噸鐢⊿tringBuilder瀵硅薄 + 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); + // 浼樺寲锛氶噸鐢⊿tringBuilder瀵硅薄 + 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(); + }); + } + + // 杈呭姪鏂规硶锛氬皢瀛楄妭鏁扮粍杞崲涓篐EX瀛楃涓� + private String bytesToHex(byte[] bytes) { + // 浼樺寲锛氶噸鐢⊿tringBuilder瀵硅薄 + 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; + } + } + } + } +} \ No newline at end of file diff --git a/src/home/ConfigPanel.java b/src/home/ConfigPanel.java new file mode 100644 index 0000000..086ff4f --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/src/home/Dell55AA01Parser.java b/src/home/Dell55AA01Parser.java new file mode 100644 index 0000000..4c86947 --- /dev/null +++ b/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); + } + +} \ No newline at end of file diff --git a/src/home/Dell55AA1FParser.java b/src/home/Dell55AA1FParser.java new file mode 100644 index 0000000..ad2cdf4 --- /dev/null +++ b/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); + } + +} \ No newline at end of file diff --git a/src/home/FirmwareUpgrader.java b/src/home/FirmwareUpgrader.java new file mode 100644 index 0000000..10953c4 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/src/home/HexUtils.java b/src/home/HexUtils.java new file mode 100644 index 0000000..94a3480 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/home/LogUtil.java b/src/home/LogUtil.java new file mode 100644 index 0000000..86465f7 --- /dev/null +++ b/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 自动生成的方法存根 + + } +} \ No newline at end of file diff --git a/src/home/Mains.java b/src/home/Mains.java new file mode 100644 index 0000000..6153836 --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/src/home/SerialPortService.java b/src/home/SerialPortService.java new file mode 100644 index 0000000..dce0fe6 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/home/SingleInstanceLock.java b/src/home/SingleInstanceLock.java new file mode 100644 index 0000000..3d54fcc --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/src/home/VisualizationPanel.java b/src/home/VisualizationPanel.java new file mode 100644 index 0000000..bde7067 --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/systemfile/Messages_en.properties b/systemfile/Messages_en.properties new file mode 100644 index 0000000..002aef8 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/systemfile/Messages_zh.properties b/systemfile/Messages_zh.properties new file mode 100644 index 0000000..2cb98e5 --- /dev/null +++ b/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=鍏抽棴涓插彛 \ No newline at end of file diff --git a/systemfile/logfile/openlog.txt b/systemfile/logfile/openlog.txt new file mode 100644 index 0000000..59defae --- /dev/null +++ b/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 -- Gitblit v1.9.3