From 544b3745d5f319e043727a26c708d22253fc6ae5 Mon Sep 17 00:00:00 2001
From: RB <robofish>
Date: Sun, 25 May 2025 01:11:07 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0MRtool?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 MR_Tool.py | 616 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 611 insertions(+), 5 deletions(-)

diff --git a/MR_Tool.py b/MR_Tool.py
index c57524e..71f3e57 100644
--- a/MR_Tool.py
+++ b/MR_Tool.py
@@ -16,7 +16,12 @@ import numpy as np
 import pandas as pd
 import requests
 import webbrowser
-
+import serial
+import serial.tools.list_ports
+from PyQt5.QtWidgets import QGroupBox, QGridLayout, QLineEdit, QTextBrowser
+from PyQt5.QtCore import QTimer, pyqtSlot
+from PyQt5.QtWidgets import QCheckBox
+from PyQt5.QtCore import QTimer
 
 def resource_path(relative_path):
     """兼容PyInstaller打包后资源路径"""
@@ -735,7 +740,607 @@ class DownloadPage(QWidget):
         main_layout.addWidget(footer)
 
 # --------- 功能三:串口助手 ---------
-# class SerialAssistant(QWidget):
+class SerialAssistant(QWidget):
+    def __init__(self):
+        super().__init__()
+        self.setFont(QFont("微软雅黑", 15))
+        self.ser = None
+        self.timer = None
+        self.recv_buffer = b""
+        self.plot_data = {}
+        self.curve_colors = ["#e74c3c", "#2980b9", "#27ae60", "#f1c40f", "#8e44ad", "#16a085"]
+        self.data_types = ["float", "int16", "uint16", "int8", "uint8"]
+        self.data_type = "float"
+        self.data_count = 2
+        self.sample_idx = 0
+
+        # 新增:HEX模式复选框
+        self.hex_send_chk = QCheckBox("HEX发送")
+        self.hex_recv_chk = QCheckBox("HEX接收")
+        self.hex_send_chk.setFont(QFont("微软雅黑", 10))
+        self.hex_recv_chk.setFont(QFont("微软雅黑", 10))
+        self.hex_send_chk.setChecked(False)
+        self.hex_recv_chk.setChecked(False)
+
+        # 主体布局
+        main_layout = QHBoxLayout(self)
+        main_layout.setContentsMargins(32, 32, 32, 32)
+        main_layout.setSpacing(28)
+
+        # 左侧面板
+        left_panel = QVBoxLayout()
+        left_panel.setSpacing(20)
+        left_panel.setContentsMargins(0, 0, 0, 0)
+
+        # 串口配置区
+        config_group = QGroupBox("串口配置")
+        config_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
+        config_group.setStyleSheet("""
+            QGroupBox {
+                border: 2px solid #b5d0ea;
+                border-radius: 12px;
+                margin-top: 12px;
+                background: #f8fbfd;
+                color: #2471a3;
+                padding: 8px 0 0 0;
+            }
+            QGroupBox:title {
+                subcontrol-origin: margin;
+                left: 16px;
+                top: -8px;
+                background: transparent;
+                padding: 0 8px;
+            }
+        """)
+        config_layout = QGridLayout()
+        config_layout.setSpacing(12)
+        config_layout.setContentsMargins(16, 16, 16, 16)
+        config_layout.addWidget(QLabel("串口号:"), 0, 0)
+        self.port_box = QComboBox()
+        self.port_box.setMinimumWidth(120)
+        self.refresh_ports()
+        config_layout.addWidget(self.port_box, 0, 1)
+        config_layout.addWidget(QLabel("波特率:"), 1, 0)
+        self.baud_box = QComboBox()
+        self.baud_box.addItems(["9600", "115200", "57600", "38400", "19200", "4800"])
+        self.baud_box.setCurrentText("115200")
+        config_layout.addWidget(self.baud_box, 1, 1)
+        self.refresh_btn = QPushButton("刷新串口")
+        self.refresh_btn.clicked.connect(self.refresh_ports)
+        self.refresh_btn.setStyleSheet(self._btn_style())
+        config_layout.addWidget(self.refresh_btn, 2, 0)
+        self.open_btn = QPushButton("打开串口")
+        self.open_btn.setCheckable(True)
+        self.open_btn.clicked.connect(self.toggle_serial)
+        self.open_btn.setStyleSheet(self._btn_style("#27ae60"))
+        config_layout.addWidget(self.open_btn, 2, 1)
+        config_group.setLayout(config_layout)
+        left_panel.addWidget(config_group)
+
+        # 数据协议配置区
+        proto_group = QGroupBox("数据协议配置")
+        proto_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
+        proto_group.setStyleSheet(config_group.styleSheet())
+        proto_layout = QGridLayout()
+        proto_layout.setSpacing(12)
+        proto_layout.setContentsMargins(16, 16, 16, 16)
+        proto_layout.addWidget(QLabel("数据数量:"), 0, 0)
+        self.data_count_spin = QSpinBox()
+        self.data_count_spin.setRange(1, 16)
+        self.data_count_spin.setValue(self.data_count)
+        self.data_count_spin.valueChanged.connect(self.apply_proto_config)
+        proto_layout.addWidget(self.data_count_spin, 0, 1)
+        proto_layout.addWidget(QLabel("数据类型:"), 1, 0)
+        self.data_type_box = QComboBox()
+        self.data_type_box.addItems(self.data_types)
+        self.data_type_box.setCurrentText(self.data_type)
+        self.data_type_box.currentTextChanged.connect(self.apply_proto_config)
+        proto_layout.addWidget(self.data_type_box, 1, 1)
+        proto_group.setLayout(proto_layout)
+        left_panel.addWidget(proto_group)
+
+        # 发送数据区
+        send_group = QGroupBox("发送数据")
+        send_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
+        send_group.setStyleSheet("""
+            QGroupBox {
+                border: 2px solid #b5d0ea;
+                border-radius: 12px;
+                margin-top: 12px;
+                background: #f8fbfd;
+                color: #2471a3;
+                padding: 8px 0 0 0;
+            }
+            QGroupBox:title {
+                subcontrol-origin: margin;
+                left: 16px;
+                top: -8px;
+                background: transparent;
+                padding: 0 8px;
+            }
+        """)
+        send_layout = QVBoxLayout()
+        send_layout.setSpacing(14)
+        send_layout.setContentsMargins(12, 12, 12, 12)
+
+        # 输入框(更大更高字体)
+        self.send_edit = QLineEdit()
+        self.send_edit.setFont(QFont("Consolas", 22))
+        self.send_edit.setPlaceholderText("输入要发送的数据...")
+        self.send_edit.setMinimumHeight(54)
+        self.send_edit.setMinimumWidth(360)
+        self.send_edit.setStyleSheet("""
+            QLineEdit {
+                background: #f8fbfd;
+                border-radius: 10px;
+                border: 1.5px solid #d6eaf8;
+                font-size: 22px;
+                padding: 12px 18px;
+            }
+        """)
+        send_layout.addWidget(self.send_edit)
+
+        # HEX复选框行(字体更小)
+        hex_chk_row = QHBoxLayout()
+        self.hex_send_chk.setFont(QFont("微软雅黑", 10))
+        self.hex_recv_chk.setFont(QFont("微软雅黑", 10))
+        for chk in [self.hex_send_chk, self.hex_recv_chk]:
+            chk.setStyleSheet("""
+                QCheckBox {
+                    color: #2471a3;
+                    spacing: 12px;
+                }
+                QCheckBox::indicator {
+                    width: 18px;
+                    height: 18px;
+                }
+                QCheckBox::indicator:checked {
+                    background-color: #2980b9;
+                    border: 1.5px solid #2980b9;
+                }
+                QCheckBox::indicator:unchecked {
+                    background-color: #fff;
+                    border: 1.5px solid #b5d0ea;
+                }
+            """)
+        self.hex_send_chk.setChecked(False)
+        self.hex_recv_chk.setChecked(False)
+        hex_chk_row.addWidget(self.hex_send_chk)
+        hex_chk_row.addWidget(self.hex_recv_chk)
+        hex_chk_row.addStretch(1)
+        send_layout.addLayout(hex_chk_row)
+
+        # 发送按钮
+        send_btn_row = QHBoxLayout()
+        self.send_btn = QPushButton("发送")
+        self.send_btn.clicked.connect(self.send_data)
+        self.send_btn.setFont(QFont("微软雅黑", 16, QFont.Bold))
+        self.send_btn.setStyleSheet(self._btn_style("#2980b9"))
+        send_btn_row.addWidget(self.send_btn)
+        send_layout.addLayout(send_btn_row)
+
+        send_group.setLayout(send_layout)
+        left_panel.addWidget(send_group, stretch=1)  # 让发送区弹性填充
+
+        # 清空按钮
+        self.clear_btn = QPushButton("清空接收和曲线")
+        self.clear_btn.clicked.connect(self.clear_all)
+        self.clear_btn.setStyleSheet(self._btn_style("#e74c3c"))
+        left_panel.addWidget(self.clear_btn)
+
+        # 弹性空隙(动态扩展填满)
+        left_panel.addStretch(1)
+
+        # 使用说明始终在最下方
+        usage_group = QGroupBox("使用说明")
+        usage_group.setFont(QFont("微软雅黑", 13, QFont.Bold))
+        usage_group.setStyleSheet("""
+            QGroupBox {
+                border: 2px solid #b5d0ea;
+                border-radius: 10px;
+                margin-top: 10px;
+                background: #f8fbfd;
+                color: #2471a3;
+                padding: 6px 0 0 0;
+            }
+            QGroupBox:title {
+                subcontrol-origin: margin;
+                left: 10px;
+                top: -8px;
+                background: transparent;
+                padding: 0 6px;
+            }
+        """)
+        usage_layout = QVBoxLayout()
+        usage_label = QLabel(
+            "1. 选择串口号和波特率,点击“打开串口”。\n"
+            "2. 在“数据协议配置”中选择数据数量和数据类型。\n"
+            "3. 下位机发送格式:\n"
+            "   0x55 + 数据数量(1字节) + 数据 + 校验和(1字节)\n"
+            "   校验和为包头到最后一个数据字节的累加和的低8位。\n"
+            "4. 每包数据自动绘制曲线,X轴为采样点(或时间),Y轴为各通道数据。\n"
+            "5. 支持float/int16/uint16/int8/uint8类型,最多16通道。\n"
+            "6. 可点击“测试绘图”按钮模拟数据包接收效果。"
+        )
+        usage_label.setWordWrap(True)
+        usage_label.setFont(QFont("微软雅黑", 9))
+        usage_layout.addWidget(usage_label)
+        usage_group.setLayout(usage_layout)
+        left_panel.addWidget(usage_group)
+
+        main_layout.addLayout(left_panel, 0)
+
+        # 右侧面板
+        right_panel = QVBoxLayout()
+        right_panel.setSpacing(20)
+
+        # 接收区
+        recv_group = QGroupBox("串口接收区")
+        recv_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
+        recv_group.setStyleSheet(config_group.styleSheet())
+        recv_layout = QVBoxLayout()
+        self.recv_box = QTextEdit()
+        self.recv_box.setFont(QFont("Consolas", 13))
+        self.recv_box.setReadOnly(True)
+        self.recv_box.setMinimumHeight(120)
+        self.recv_box.setStyleSheet("""
+            QTextEdit {
+                background: #f8fbfd;
+                border-radius: 10px;
+                border: 1px solid #d6eaf8;
+                font-size: 15px;
+                color: #2c3e50;
+                padding: 8px;
+            }
+        """)
+        recv_layout.addWidget(self.recv_box)
+        recv_group.setLayout(recv_layout)
+        right_panel.addWidget(recv_group)
+
+        # 曲线绘图区
+        plot_frame = QFrame()
+        plot_frame.setStyleSheet("""
+            QFrame {
+                background: #fff;
+                border-radius: 16px;
+                border: 1px solid #d6eaf8;
+            }
+        """)
+        plot_shadow = QGraphicsDropShadowEffect(self)
+        plot_shadow.setBlurRadius(18)
+        plot_shadow.setOffset(0, 4)
+        plot_shadow.setColor(Qt.gray)
+        plot_frame.setGraphicsEffect(plot_shadow)
+        plot_layout2 = QVBoxLayout(plot_frame)
+        plot_layout2.setContentsMargins(10, 10, 10, 10)
+        self.figure = Figure(figsize=(7, 4))
+        self.canvas = FigureCanvas(self.figure)
+        plot_layout2.addWidget(self.canvas)
+        right_panel.addWidget(plot_frame, 2)
+
+        main_layout.addLayout(right_panel, 1)
+
+        # 定时器接收
+        self.timer = QTimer(self)
+        self.timer.timeout.connect(self.read_serial)
+
+        # 新增:正弦波测试定时器
+        self.sine_timer = QTimer(self)
+        self.sine_timer.timeout.connect(self.send_sine_data)
+        self.sine_phase = 0
+        # 默认配置
+        self.apply_proto_config()
+
+
+    def simulate_data(self):
+        """模拟一包数据并自动解析绘图"""
+        import struct
+        import random
+        # 构造协议包
+        head = 0x55
+        count = self.data_count
+        dtype = self.data_type
+        # 随机生成数据
+        if dtype == "float":
+            vals = [random.uniform(-10, 10) for _ in range(count)]
+            data_bytes = struct.pack(f"<{count}f", *vals)
+        elif dtype == "int16":
+            vals = [random.randint(-30000, 30000) for _ in range(count)]
+            data_bytes = struct.pack(f"<{count}h", *vals)
+        elif dtype == "uint16":
+            vals = [random.randint(0, 65535) for _ in range(count)]
+            data_bytes = struct.pack(f"<{count}H", *vals)
+        elif dtype == "int8":
+            vals = [random.randint(-128, 127) for _ in range(count)]
+            data_bytes = struct.pack(f"<{count}b", *vals)
+        elif dtype == "uint8":
+            vals = [random.randint(0, 255) for _ in range(count)]
+            data_bytes = struct.pack(f"<{count}B", *vals)
+        else:
+            vals = [0] * count
+            data_bytes = b"\x00" * (count * self._type_size())
+        # 拼包
+        pkt = bytes([head, count]) + data_bytes
+        checksum = sum(pkt) & 0xFF
+        pkt += bytes([checksum])
+        # 加入接收缓冲区并解析
+        self.recv_buffer += pkt
+        self.parse_and_plot_bin()
+
+    def _btn_style(self, color="#2980b9"):
+        return f"""
+            QPushButton {{
+                background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
+                    stop:0 #eaf6fb, stop:1 #d6eaf8);
+                color: {color};
+                border-radius: 14px;
+                font-size: 16px;
+                font-weight: 600;
+                padding: 8px 0;
+                border: 1.5px solid #d6eaf8;
+                letter-spacing: 1px;
+            }}
+            QPushButton:hover {{
+                background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
+                    stop:0 #f8fffe, stop:1 #cfe7fa);
+                color: #1a6fae;
+                border: 2px solid #b5d0ea;
+            }}
+            QPushButton:pressed {{
+                background: #e3f0fa;
+                color: {color};
+                border: 2px solid #a4cbe3;
+            }}
+        """
+
+    def apply_proto_config(self):
+        self.data_count = self.data_count_spin.value()
+        self.data_type = self.data_type_box.currentText()
+        self.plot_data = {i: [[], []] for i in range(self.data_count)}
+        self.sample_idx = 0
+        self.update_plot()
+
+    def refresh_ports(self):
+        self.port_box.clear()
+        ports = serial.tools.list_ports.comports()
+        for port in ports:
+            self.port_box.addItem(port.device)
+        if self.port_box.count() == 0:
+            self.port_box.addItem("无可用串口")
+
+    def toggle_serial(self):
+        if self.open_btn.isChecked():
+            port = self.port_box.currentText()
+            baud = int(self.baud_box.currentText())
+            try:
+                self.ser = serial.Serial(port, baud, timeout=0.1)
+                self.open_btn.setText("关闭串口")
+                self.recv_box.append(f"已打开串口 {port} @ {baud}bps")
+                self.timer.start(50)
+            except Exception as e:
+                self.recv_box.append(f"打开串口失败: {e}")
+                self.open_btn.setChecked(False)
+        else:
+            if self.ser and self.ser.is_open:
+                self.ser.close()
+            self.open_btn.setText("打开串口")
+            self.recv_box.append("串口已关闭")
+            self.timer.stop()
+
+    def send_data(self):
+        if self.ser and self.ser.is_open:
+            data = self.send_edit.text()
+            try:
+                if self.hex_send_chk.isChecked():
+                    # HEX模式发送
+                    data_bytes = bytes.fromhex(data.replace(' ', ''))
+                    self.ser.write(data_bytes)
+                    self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}")
+                else:
+                    self.ser.write(data.encode('utf-8'))
+                    self.recv_box.append(f"发送: {data}")
+            except Exception as e:
+                self.recv_box.append(f"发送失败: {e}")
+        else:
+            self.recv_box.append("串口未打开,无法发送。")
+
+    def send_multi_data(self):
+        if self.ser and self.ser.is_open:
+            text = self.send_edit.text()
+            lines = text.split(";")
+            for line in lines:
+                if line.strip():
+                    try:
+                        if self.hex_send_chk.isChecked():
+                            data_bytes = bytes.fromhex(line.strip().replace(' ', ''))
+                            self.ser.write(data_bytes)
+                            self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}")
+                        else:
+                            self.ser.write(line.strip().encode('utf-8'))
+                            self.recv_box.append(f"发送: {line.strip()}")
+                    except Exception as e:
+                        self.recv_box.append(f"发送失败: {e}")
+        else:
+            self.recv_box.append("串口未打开,无法发送。")
+
+    def read_serial(self):
+        if self.ser and self.ser.is_open:
+            try:
+                data = self.ser.read_all()
+                if data:
+                    if self.hex_recv_chk.isChecked():
+                        self.recv_box.append(f"接收(HEX): {data.hex(' ').upper()}")
+                    else:
+                        try:
+                            self.recv_box.append(f"接收: {data.decode('utf-8', errors='replace')}")
+                        except Exception:
+                            self.recv_box.append(f"接收(HEX): {data.hex(' ').upper()}")
+                    self.recv_buffer += data
+                    self.parse_and_plot_bin()
+            except Exception as e:
+                self.recv_box.append(f"接收失败: {e}")
+
+    def toggle_sine_test(self):
+        if self.test_btn.isChecked():
+            self.test_btn.setText("停止测试")
+            self.sine_phase = 0
+            self.sine_timer.start(80)  # 80ms周期
+        else:
+            self.test_btn.setText("测试正弦波(持续)")
+            self.sine_timer.stop()
+
+    def send_sine_data(self):
+        import struct, math
+        head = 0x55
+        count = self.data_count
+        dtype = self.data_type
+        t = self.sine_phase
+        vals = []
+        for i in range(count):
+            # 多通道不同相位
+            val = math.sin(t / 10.0 + i * math.pi / 4) * 10
+            if dtype == "float":
+                vals.append(float(val))
+            elif dtype == "int16":
+                vals.append(int(val * 1000))
+            elif dtype == "uint16":
+                vals.append(int(val * 1000 + 20000))
+            elif dtype == "int8":
+                vals.append(int(val * 10))
+            elif dtype == "uint8":
+                vals.append(int(val * 10 + 100))
+        # 打包
+        if dtype == "float":
+            data_bytes = struct.pack(f"<{count}f", *vals)
+        elif dtype == "int16":
+            data_bytes = struct.pack(f"<{count}h", *vals)
+        elif dtype == "uint16":
+            data_bytes = struct.pack(f"<{count}H", *vals)
+        elif dtype == "int8":
+            data_bytes = struct.pack(f"<{count}b", *vals)
+        elif dtype == "uint8":
+            data_bytes = struct.pack(f"<{count}B", *vals)
+        else:
+            data_bytes = b"\x00" * (count * self._type_size())
+        pkt = bytes([head, count]) + data_bytes
+        checksum = sum(pkt) & 0xFF
+        pkt += bytes([checksum])
+        # 直接走接收流程模拟
+        self.recv_buffer += pkt
+        self.parse_and_plot_bin()
+        # 在接收区实时显示理论值
+        self.recv_box.append(f"理论: {['%.3f'%v for v in vals]}")
+        self.sine_phase += 1
+
+    def parse_and_plot_bin(self):
+        # 协议:0x55 + 数据数量(1B) + 数据 + 校验(1B)
+        min_len = 1 + 1 + self.data_count * self._type_size() + 1
+        while len(self.recv_buffer) >= min_len:
+            idx = self.recv_buffer.find(b'\x55')
+            if idx == -1:
+                self.recv_buffer = b""
+                break
+            if idx > 0:
+                self.recv_buffer = self.recv_buffer[idx:]
+            if len(self.recv_buffer) < min_len:
+                break
+            # 检查数量
+            count = self.recv_buffer[1]
+            if count != self.data_count:
+                self.recv_buffer = self.recv_buffer[2:]
+                continue
+            data_bytes = self.recv_buffer[2:2+count*self._type_size()]
+            checksum = self.recv_buffer[2+count*self._type_size()]
+            calc_sum = (sum(self.recv_buffer[:2+count*self._type_size()])) & 0xFF
+            if checksum != calc_sum:
+                self.recv_box.append("校验和错误,丢弃包")
+                self.recv_buffer = self.recv_buffer[1:]
+                continue
+            # 解析数据
+            values = self._unpack_data(data_bytes, count)
+            self.recv_box.append(f"接收: {values}")
+            for i, v in enumerate(values):
+                self.plot_data[i][0].append(self.sample_idx)
+                self.plot_data[i][1].append(v)
+                if len(self.plot_data[i][0]) > 200:
+                    self.plot_data[i][0].pop(0)
+                    self.plot_data[i][1].pop(0)
+            self.sample_idx += 1
+            self.recv_buffer = self.recv_buffer[min_len:]
+            self.update_plot()
+
+    def _type_size(self):
+        if self.data_type == "float":
+            return 4
+        elif self.data_type in ("int16", "uint16"):
+            return 2
+        elif self.data_type in ("int8", "uint8"):
+            return 1
+        return 4
+
+    def _unpack_data(self, data_bytes, count):
+        import struct
+        fmt = {
+            "float": f"<{count}f",
+            "int16": f"<{count}h",
+            "uint16": f"<{count}H",
+            "int8": f"<{count}b",
+            "uint8": f"<{count}B"
+        }[self.data_type]
+        try:
+            return struct.unpack(fmt, data_bytes)
+        except Exception:
+            return [0] * count
+
+    def update_plot(self):
+        self.figure.clear()
+        ax = self.figure.add_subplot(111)
+        ax.set_xlabel("采样点", fontsize=14)
+        ax.set_ylabel("数据值", fontsize=14)
+        has_curve = False
+        for idx in range(self.data_count):
+            color = self.curve_colors[idx % len(self.curve_colors)]
+            x_list, y_list = self.plot_data.get(idx, ([], []))
+            if x_list and y_list:
+                ax.plot(x_list, y_list, label=f"CH{idx+1}", color=color, linewidth=2)
+                has_curve = True
+        if has_curve:
+            ax.legend()
+        ax.grid(True, linestyle="--", alpha=0.5)
+        self.canvas.draw()
+
+    def clear_all(self):
+        self.recv_box.clear()
+        self.plot_data = {i: [[], []] for i in range(self.data_count)}
+        self.sample_idx = 0
+        self.update_plot()
+
+# --------- 功能四:MRobot架构生成 ---------
+class GenerateMRobotCode(QWidget):
+    def __init__(self):
+        super().__init__()
+        self.setFont(QFont("微软雅黑", 15))
+        self.setStyleSheet("""
+            QWidget {
+                background: #f8fbfd;
+                border-radius: 16px;
+                padding: 20px;
+            }
+        """)
+        self.init_ui()
+
+    def init_ui(self):
+        main_layout = QVBoxLayout(self)
+        main_layout.setContentsMargins(20, 20, 20, 20)
+        main_layout.setSpacing(20)
+
+        # 功能说明
+        desc_label = QLabel("MRobot架构生成工具,帮助您快速生成MRobot项目代码。")
+        desc_label.setFont(QFont("微软雅黑", 14))
+        desc_label.setAlignment(Qt.AlignCenter)
+        main_layout.addWidget(desc_label)
+
+        # 其他UI元素...
 
 # --------- 主工具箱UI ---------
 class ToolboxUI(QWidget):
@@ -794,7 +1399,7 @@ class ToolboxUI(QWidget):
         left_layout.addWidget(logo_label)
 
         # 按钮区
-        self.button_names = ["主页", "曲线拟合", "功能三", "软件指南"]
+        self.button_names = ["主页", "曲线拟合", "Mini串口助手", "MR架构配置","软件指南"]
         self.buttons = []
         for idx, name in enumerate(self.button_names):
             btn = QPushButton(name)
@@ -865,8 +1470,9 @@ class ToolboxUI(QWidget):
         self.page_widgets = {
             0: HomePage(),  # 主页
             1: PolyFitApp(),  # 多项式拟合
-            2: self.placeholder_page("功能三开发中..."),
-            3: DownloadPage(),  # 下载页面
+            2: SerialAssistant(),  # 串口助手
+            3: GenerateMRobotCode(),  # MRobot架构生成
+            4: DownloadPage(),  # 下载页面
         }
         for i in range(len(self.button_names)):
             self.stack.addWidget(self.page_widgets[i])