diff --git a/.DS_Store b/.DS_Store
index e09c158..3adcc36 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/MR_Tool.py b/MR_Tool.py
deleted file mode 100644
index 29bf82c..0000000
--- a/MR_Tool.py
+++ /dev/null
@@ -1,2339 +0,0 @@
-import sys
-import os
-import numpy as np
-import pandas as pd
-import requests
-import webbrowser
-import serial
-import serial.tools.list_ports
-from PyQt5.QtWidgets import (
- QApplication, QWidget, QLabel, QPushButton, QTextEdit, QVBoxLayout,
- QHBoxLayout, QStackedWidget, QSizePolicy, QFrame, QGraphicsDropShadowEffect,
- QSpinBox, QTableWidget, QTableWidgetItem, QFileDialog, QComboBox, QMessageBox, QHeaderView,
- QGroupBox, QGridLayout, QLineEdit, QTextBrowser, QCheckBox
-)
-from PyQt5.QtGui import QPixmap, QFont, QIcon, QPainter, QPen, QColor
-from PyQt5.QtCore import Qt, QTimer, QPointF, pyqtSlot
-import matplotlib
-from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
-from matplotlib.figure import Figure
-from PyQt5.QtCore import pyqtSignal, pyqtSlot
-from PyQt5.QtWebEngineWidgets import QWebEngineView # 新增
-from PyQt5.QtCore import QUrl
-from PyQt5.QtCore import QThread
-from PyQt5.QtWebEngineWidgets import QWebEngineProfile
-from PyQt5.QtWidgets import QFileDialog
-from PyQt5.QtWebEngineWidgets import QWebEngineView
-from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage
-from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
-from PyQt5.QtWidgets import QSplashScreen
-from PyQt5.QtCore import Qt, QTimer
-from PyQt5.QtGui import QPixmap
-
-
-def resource_path(relative_path):
- """兼容PyInstaller打包后资源路径"""
- if hasattr(sys, '_MEIPASS'):
- return os.path.join(sys._MEIPASS, relative_path)
- return os.path.join(os.path.abspath("."), relative_path)
-
-# --------- 主页 ---------
-class HomePage(QWidget):
- def __init__(self):
- super().__init__()
- layout = QVBoxLayout(self)
- layout.setContentsMargins(60, 60, 60, 60)
- layout.setSpacing(32)
- self.setStyleSheet("""
- QWidget {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #f8fbfd, stop:1 #eaf6fb);
- border-radius: 18px;
- }
- """)
-
- # 欢迎标题
- title = QLabel("欢迎来到 MRobot 工具箱!")
- title.setFont(QFont("微软雅黑", 26, QFont.Bold))
- title.setAlignment(Qt.AlignCenter)
- title.setStyleSheet("color: #2980b9; letter-spacing: 3px;")
- layout.addWidget(title)
- # 设置高度
- title.setFixedHeight(120)
-
- # 分割线
- line = QFrame()
- line.setFrameShape(QFrame.HLine)
- line.setFrameShadow(QFrame.Sunken)
- line.setStyleSheet("color: #d6eaf8; background: #d6eaf8; min-height: 2px;")
- layout.addWidget(line)
-
- # 介绍内容
- desc = QLabel(
- "🤖 本工具箱由青岛理工大学(QUT)机器人战队开发,\n"
- "涵盖沧溟(Robocon)与MOVE(Robomaster)两支队伍。\n\n"
- "集成了常用小工具与助手功能,持续更新中ing!\n"
- "👉 可通过左侧选择不同模块,助力更高效的机器人开发,\n"
- "节约开发时间,减少繁琐操作。\n\n"
- "欢迎反馈建议,共同完善工具箱!"
- )
- desc.setFont(QFont("微软雅黑", 16))
- desc.setAlignment(Qt.AlignCenter)
- desc.setStyleSheet("color: #34495e;")
- desc.setWordWrap(True)
- layout.addWidget(desc)
-
- # 作者&版本信息
- info = QLabel(
- "作者: QUT RMer & RCer | "
- "版本: 0.0.2 | "
- "联系方式: QQ群 : 857466609"
- )
- info.setFont(QFont("微软雅黑", 14))
- info.setAlignment(Qt.AlignCenter)
- info.setStyleSheet("color: #7f8c8d; margin-top: 24px;")
- info.setFixedHeight(100) # 修改为固定高度
- layout.addWidget(info)
-
- # 页脚
- footer = QLabel("© 2025 MRobot. 保留所有权利。")
- footer.setFont(QFont("微软雅黑", 12))
- footer.setAlignment(Qt.AlignCenter)
- footer.setStyleSheet("color: #b2bec3; margin-top: 18px;")
- footer.setFixedHeight(100) # 修改为固定高度
- layout.addWidget(footer)
-
-# --------- 功能一:多项式拟合工具页面 ---------
-class PolyFitApp(QWidget):
- def __init__(self):
- super().__init__()
- self.setFont(QFont("微软雅黑", 15))
- self.data_x = []
- self.data_y = []
- self.last_coeffs = None
- self.last_xmin = None
- self.last_xmax = None
-
- # 统一背景和边框
- self.setStyleSheet("""
- QWidget {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- border-radius: 16px;
- border: 1px solid #d6eaf8;
- }
- """)
-
- main_layout = QHBoxLayout(self)
- main_layout.setContentsMargins(24, 24, 24, 24)
- main_layout.setSpacing(24)
- left_layout = QVBoxLayout()
- left_layout.setSpacing(18)
- right_layout = QVBoxLayout()
- right_layout.setSpacing(18)
- main_layout.addLayout(left_layout, 0)
- main_layout.addLayout(right_layout, 1)
-
- # 标题
- title = QLabel("曲线拟合工具")
- # title.setFont(QFont("微软雅黑", 2, QFont.Bold))
- # 设置文字大小
- title.setFont(QFont("微软雅黑", 14, QFont.Bold))
- title.setAlignment(Qt.AlignCenter)
- title.setStyleSheet("color: #2980b9; letter-spacing: 2px;")
- left_layout.addWidget(title)
-
- # 数据表
- self.table = QTableWidget(0, 2)
- self.table.setFont(QFont("Consolas", 16))
- self.table.setHorizontalHeaderLabels(["x", "y"])
- self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
- self.table.setSelectionBehavior(QTableWidget.SelectRows)
- self.table.setStyleSheet("""
- QTableWidget {
- background: #f8fbfd;
- border-radius: 10px;
- border: 1px solid #d6eaf8;
- font-size: 16px;
- }
- QHeaderView::section {
- background-color: #eaf6fb;
- color: #2980b9;
- font-size: 16px;
- font-weight: bold;
- border: 1px solid #d6eaf8;
- height: 36px;
- }
- """)
- self.table.setMinimumHeight(200) # 设置最小高度
- left_layout.addWidget(self.table, stretch=1) # 让表格尽量撑大
-
- # 添加/删除行
- btn_row = QHBoxLayout()
- self.add_row_btn = QPushButton("添加数据")
- self.add_row_btn.setFont(QFont("微软雅黑", 20, QFont.Bold))
- self.add_row_btn.setMinimumHeight(44)
- self.add_row_btn.clicked.connect(self.add_point_row)
- self.del_row_btn = QPushButton("删除选中行")
- self.del_row_btn.setFont(QFont("微软雅黑", 20, QFont.Bold))
- self.del_row_btn.setMinimumHeight(44)
- self.del_row_btn.clicked.connect(self.delete_selected_rows)
- for btn in [self.add_row_btn, self.del_row_btn]:
- btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2980b9;
- border-radius: 20px;
- font-size: 20px;
- font-weight: 600;
- padding: 10px 0;
- border: 1px solid #d6eaf8;
- }
- QPushButton:hover {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #f8fffe, stop:1 #cfe7fa);
- color: #1a6fae;
- border: 1.5px solid #b5d0ea;
- }
- QPushButton:pressed {
- background: #e3f0fa;
- color: #2471a3;
- border: 1.5px solid #a4cbe3;
- }
- """)
- btn_row.addWidget(self.add_row_btn)
- btn_row.addWidget(self.del_row_btn)
- left_layout.addLayout(btn_row)
-
- # 导入/导出
- file_btn_row = QHBoxLayout()
- self.import_btn = QPushButton("导入Excel文件")
- self.import_btn.setFont(QFont("微软雅黑", 18, QFont.Bold))
- self.import_btn.setMinimumHeight(44)
- self.import_btn.clicked.connect(self.load_excel)
- self.export_btn = QPushButton("导出Excel文件")
- self.export_btn.setFont(QFont("微软雅黑", 18, QFont.Bold))
- self.export_btn.setMinimumHeight(44)
- self.export_btn.clicked.connect(self.export_excel_and_plot)
- for btn in [self.import_btn, self.export_btn]:
- btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2980b9;
- border-radius: 20px;
- font-size: 20px;
- font-weight: 600;
- padding: 10px 0;
- border: 1px solid #d6eaf8;
- }
- QPushButton:hover {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #f8fffe, stop:1 #cfe7fa);
- color: #1a6fae;
- border: 1.5px solid #b5d0ea;
- }
- QPushButton:pressed {
- background: #e3f0fa;
- color: #2471a3;
- border: 1.5px solid #a4cbe3;
- }
- """)
- file_btn_row.addWidget(self.import_btn)
- file_btn_row.addWidget(self.export_btn)
- left_layout.addLayout(file_btn_row)
-
- # 阶数选择
- param_layout = QHBoxLayout()
- label_order = QLabel("多项式阶数:")
- # 文字居中
- label_order.setAlignment(Qt.AlignCenter)
- # 文字加粗
- label_order.setStyleSheet("color: #2980b9;")
- param_layout.addWidget(label_order)
- self.order_spin = QSpinBox()
- self.order_spin.setFont(QFont("微软雅黑", 18))
- self.order_spin.setRange(1, 10)
- self.order_spin.setValue(2)
- self.order_spin.setStyleSheet("""
- QSpinBox {
- background: #f8fbfd;
- border-radius: 10px;
- border: 1px solid #d6eaf8;
- font-size: 18px;
- padding: 4px 12px;
- }
- """)
- param_layout.addWidget(self.order_spin)
- left_layout.addLayout(param_layout)
-
- # 拟合按钮
- self.fit_btn = QPushButton("拟合并显示")
- self.fit_btn.setFont(QFont("微软雅黑", 20, QFont.Bold))
- self.fit_btn.setMinimumHeight(48)
- self.fit_btn.clicked.connect(self.fit_and_plot)
- self.fit_btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2980b9;
- border-radius: 20px;
- font-size: 22px;
- font-weight: 600;
- padding: 12px 0;
- border: 1px solid #d6eaf8;
- }
- QPushButton:hover {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #f8fffe, stop:1 #cfe7fa);
- color: #1a6fae;
- border: 1.5px solid #b5d0ea;
- }
- QPushButton:pressed {
- background: #e3f0fa;
- color: #2471a3;
- border: 1.5px solid #a4cbe3;
- }
- """)
- left_layout.addWidget(self.fit_btn)
- # 输出区
- self.output = QTextEdit()
- self.output.setReadOnly(False)
- self.output.setFont(QFont("Consolas", 16))
- self.output.setMaximumHeight(160)
- self.output.setStyleSheet("""
- QTextEdit {
- background: #f4f6f7;
- border-radius: 8px;
- border: 1px solid #d6eaf8;
- font-size: 16px;
- color: #2c3e50;
- padding: 10px;
- }
- """)
- # self.table.setFixedHeight(400) # 设置表格高度为260像素
- left_layout.addWidget(self.output)
- # 代码生成
- code_layout = QHBoxLayout()
- label_code = QLabel("输出代码格式:")
- # label_code.setFont(QFont("微软雅黑", 18, QFont.Bold))
- label_code.setStyleSheet("color: #2980b9;")
- code_layout.addWidget(label_code)
- self.code_type = QComboBox()
- self.code_type.setFont(QFont("微软雅黑", 18))
- self.code_type.addItems(["C", "C++", "Python"])
- self.code_type.setStyleSheet("""
- QComboBox {
- background: #f8fbfd;
- border-radius: 10px;
- border: 1px solid #d6eaf8;
- font-size: 18px;
- padding: 4px 12px;
- }
- """)
- code_layout.addWidget(self.code_type)
- self.gen_code_btn = QPushButton("生成代码")
- self.gen_code_btn.setFont(QFont("微软雅黑", 18, QFont.Bold))
- self.gen_code_btn.setMinimumHeight(44)
- self.gen_code_btn.clicked.connect(self.generate_code)
- self.gen_code_btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2980b9;
- border-radius: 20px;
- font-size: 20px;
- font-weight: 600;
- padding: 10px 0;
- border: 1px solid #d6eaf8;
- }
- QPushButton:hover {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #f8fffe, stop:1 #cfe7fa);
- color: #1a6fae;
- border: 1.5px solid #b5d0ea;
- }
- QPushButton:pressed {
- background: #e3f0fa;
- color: #2471a3;
- border: 1.5px solid #a4cbe3;
- }
- """)
- code_layout.addWidget(self.gen_code_btn)
- left_layout.addLayout(code_layout)
-
-
- # 曲线区加圆角和阴影
- curve_frame = QFrame()
- curve_frame.setStyleSheet("""
- QFrame {
- background: #fff;
- border-radius: 16px;
- border: 1px solid #d6eaf8;
- }
- """)
- curve_shadow = QGraphicsDropShadowEffect(self)
- curve_shadow.setBlurRadius(24)
- curve_shadow.setOffset(0, 4)
- curve_shadow.setColor(Qt.gray)
- curve_frame.setGraphicsEffect(curve_shadow)
- curve_layout = QVBoxLayout(curve_frame)
- curve_layout.setContentsMargins(10, 10, 10, 10)
- self.figure = Figure(figsize=(6, 5))
- self.canvas = FigureCanvas(self.figure)
- curve_layout.addWidget(self.canvas)
- right_layout.addWidget(curve_frame)
-
- # 默认显示空坐标系
- self.figure.clear()
- ax = self.figure.add_subplot(111)
- # ax.set_title("拟合结果", fontsize=22, fontweight='bold')
- ax.set_xlabel("x", fontsize=18)
- ax.set_ylabel("y", fontsize=18)
- ax.tick_params(labelsize=15)
- # ax.set_title("拟合结果", fontsize=22, fontweight='bold') # 中文标题
- self.canvas.draw()
-
- def add_point_row(self, x_val="", y_val=""):
- row = self.table.rowCount()
- self.table.insertRow(row)
- self.table.setItem(row, 0, QTableWidgetItem(str(x_val)))
- self.table.setItem(row, 1, QTableWidgetItem(str(y_val)))
-
- def delete_selected_rows(self):
- selected = self.table.selectionModel().selectedRows()
- for idx in sorted(selected, reverse=True):
- self.table.removeRow(idx.row())
-
- def load_excel(self):
- file, _ = QFileDialog.getOpenFileName(self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)")
- if file:
- try:
- data = pd.read_excel(file, usecols=[0, 1])
- new_x = data.iloc[:, 0].values.tolist()
- new_y = data.iloc[:, 1].values.tolist()
- for x, y in zip(new_x, new_y):
- self.add_point_row(x, y)
- QMessageBox.information(self, "成功", "数据导入成功!")
- except Exception as e:
- QMessageBox.critical(self, "错误", f"读取Excel失败: {e}")
-
- def export_excel_and_plot(self):
- file, _ = QFileDialog.getSaveFileName(self, "导出Excel文件", "", "Excel Files (*.xlsx *.xls)")
- if file:
- x_list, y_list = [], []
- for row in range(self.table.rowCount()):
- try:
- x = float(self.table.item(row, 0).text())
- y = float(self.table.item(row, 1).text())
- x_list.append(x)
- y_list.append(y)
- except Exception:
- continue
- if not x_list or not y_list:
- QMessageBox.warning(self, "导出失败", "没有可导出的数据!")
- return
- df = pd.DataFrame({'x': x_list, 'y': y_list})
- try:
- df.to_excel(file, index=False)
- png_file = file
- if png_file.lower().endswith('.xlsx') or png_file.lower().endswith('.xls'):
- png_file = png_file.rsplit('.', 1)[0] + '.png'
- else:
- png_file = png_file + '.png'
- self.figure.savefig(png_file, dpi=150, bbox_inches='tight')
- QMessageBox.information(self, "导出成功", f"数据已成功导出到Excel文件!\n图像已导出为:{png_file}")
- except Exception as e:
- QMessageBox.critical(self, "导出错误", f"导出Excel或图像失败: {e}")
-
- def get_manual_points(self):
- x_list, y_list = [], []
- for row in range(self.table.rowCount()):
- try:
- x = float(self.table.item(row, 0).text())
- y = float(self.table.item(row, 1).text())
- x_list.append(x)
- y_list.append(y)
- except Exception:
- continue
- return x_list, y_list
-
- def fit_and_plot(self):
- self.data_x, self.data_y = self.get_manual_points()
- try:
- order = int(self.order_spin.value())
- except ValueError:
- QMessageBox.warning(self, "输入错误", "阶数必须为整数!")
- return
- n_points = len(self.data_x)
- if n_points < order + 1:
- QMessageBox.warning(self, "数据不足", "数据点数量不足以拟合该阶多项式!")
- return
- x = np.array(self.data_x, dtype=np.float64)
- y = np.array(self.data_y, dtype=np.float64)
- x_min, x_max = x.min(), x.max()
- if x_max - x_min == 0:
- QMessageBox.warning(self, "数据错误", "所有x值都相同,无法拟合!")
- return
- try:
- coeffs = np.polyfit(x, y, order)
- except Exception as e:
- QMessageBox.critical(self, "拟合错误", f"多项式拟合失败:{e}")
- return
- poly = np.poly1d(coeffs)
- expr = "y = " + " + ".join([f"{c:.6g}*x^{order-i}" for i, c in enumerate(coeffs)])
- self.output.setPlainText(f"{expr}\n")
- self.figure.clear()
- ax = self.figure.add_subplot(111)
- # ax.set_title("拟合结果", fontsize=22, fontweight='bold')
- ax.set_xlabel("x", fontsize=18)
- ax.set_ylabel("y", fontsize=18)
- ax.scatter(x, y, color='red', label='Data')
- x_fit = np.linspace(x_min, x_max, 200)
- y_fit = poly(x_fit)
- ax.plot(x_fit, y_fit, label='Fit Curve')
- ax.legend()
- self.canvas.draw()
- self.last_coeffs = coeffs
- self.last_xmin = x_min
- self.last_xmax = x_max
-
- def generate_code(self):
- if self.last_coeffs is None:
- QMessageBox.warning(self, "未拟合", "请先拟合数据!")
- return
- coeffs = self.last_coeffs
- code_type = self.code_type.currentText()
- if code_type == "C":
- code = self.create_c_function(coeffs)
- elif code_type == "C++":
- code = self.create_cpp_function(coeffs)
- else:
- code = self.create_py_function(coeffs)
- self.output.setPlainText(code)
-
- def create_c_function(self, coeffs):
- lines = ["#include ", "double polynomial(double x) {", " return "]
- n = len(coeffs)
- terms = []
- for i, c in enumerate(coeffs):
- exp = n - i - 1
- if exp == 0:
- terms.append(f"{c:.8g}")
- elif exp == 1:
- terms.append(f"{c:.8g}*x")
- else:
- terms.append(f"{c:.8g}*pow(x,{exp})")
- lines[-1] += " + ".join(terms) + ";"
- lines.append("}")
- return "\n".join(lines)
-
- def create_cpp_function(self, coeffs):
- lines = ["#include ", "double polynomial(double x) {", " return "]
- n = len(coeffs)
- terms = []
- for i, c in enumerate(coeffs):
- exp = n - i - 1
- if exp == 0:
- terms.append(f"{c:.8g}")
- elif exp == 1:
- terms.append(f"{c:.8g}*x")
- else:
- terms.append(f"{c:.8g}*pow(x,{exp})")
- lines[-1] += " + ".join(terms) + ";"
- lines.append("}")
- return "\n".join(lines)
-
- def create_py_function(self, coeffs):
- n = len(coeffs)
- lines = ["def polynomial(x):", " return "]
- terms = []
- for i, c in enumerate(coeffs):
- exp = n - i - 1
- if exp == 0:
- terms.append(f"{c:.8g}")
- elif exp == 1:
- terms.append(f"{c:.8g}*x")
- else:
- terms.append(f"{c:.8g}*x**{exp}")
- lines[-1] += " + ".join(terms)
- return "\n".join(lines)
-
-# --------- 功能二:下载 ---------
-class DownloadPage(QWidget):
- def __init__(self):
- super().__init__()
- self.setFont(QFont("微软雅黑", 15))
- self.setStyleSheet("""
- QWidget {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- border-radius: 18px;
- border: 1px solid #d6eaf8;
- }
- """)
- main_layout = QVBoxLayout(self)
- main_layout.setContentsMargins(60, 60, 60, 60)
- main_layout.setSpacing(32)
-
- # 标题
- title = QLabel("常用工具下载")
- title.setFont(QFont("微软雅黑", 26, QFont.Bold))
- title.setAlignment(Qt.AlignCenter)
- title.setStyleSheet("color: #2980b9; letter-spacing: 3px; margin-bottom: 10px;")
- main_layout.addWidget(title)
-
- # 分割线
- line = QFrame()
- line.setFrameShape(QFrame.HLine)
- line.setFrameShadow(QFrame.Sunken)
- line.setStyleSheet("color: #d6eaf8; background: #d6eaf8; min-height: 2px;")
- main_layout.addWidget(line)
-
- # # 说明
- # desc = QLabel("点击下方按钮可直接跳转到常用工具或开发软件的官方下载页面:")
- # desc.setFont(QFont("微软雅黑", 17))
- # desc.setAlignment(Qt.AlignCenter)
- # desc.setStyleSheet("color: #34495e; margin-bottom: 18px;")
- # main_layout.addWidget(desc)
-
- spacer = QFrame()
- spacer.setFixedHeight(4) # 可根据需要调整间隔高度
- spacer.setStyleSheet("background: transparent; border: none;")
- main_layout.addWidget(spacer)
-
- # 小工具类
- tools_tools = [
- ("Geek Uninstaller", "https://geekuninstaller.com/download", "🧹"),
- ("Neat Download Manager", "https://www.neatdownloadmanager.com/index.php/en/", "⬇️"),
- ("Everything", "https://www.voidtools.com/zh-cn/downloads/", "🔍"),
- ("Bandizip", "https://www.bandisoft.com/bandizip/", "🗜️"),
- ("PotPlayer", "https://potplayer.daum.net/", "🎬"),
- ("Typora", "https://typora.io/", "📝"),
- ("Git", "https://git-scm.com/download/win", "🟥"),
- ("Python", "https://www.python.org/downloads/", "🐍"),
- ]
- tools_group = QGroupBox("常用小工具")
- tools_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
- tools_group.setStyleSheet("""
- QGroupBox {
- border: 2px solid #b5d0ea;
- border-radius: 12px;
- margin-top: 16px;
- background: #f8fbfd;
- color: #2471a3;
- padding: 10px 0 0 0;
- }
- QGroupBox:title {
- subcontrol-origin: margin;
- left: 18px;
- top: -10px;
- background: transparent;
- padding: 0 8px;
- }
- """)
- tools_layout = QGridLayout()
- tools_layout.setSpacing(18)
- tools_layout.setContentsMargins(24, 24, 24, 24)
- for idx, (name, url, icon) in enumerate(tools_tools):
- btn = QPushButton(f"{icon} {name}")
- btn.setFont(QFont("微软雅黑", 16, QFont.Bold))
- btn.setMinimumHeight(60)
- btn.setCursor(Qt.PointingHandCursor)
- btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2471a3;
- 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: #2471a3;
- border: 2px solid #a4cbe3;
- }
- """)
- btn.clicked.connect(lambda checked, link=url: webbrowser.open(link))
- row, col = divmod(idx, 4)
- tools_layout.addWidget(btn, row, col)
- tools_group.setLayout(tools_layout)
- main_layout.addWidget(tools_group)
- spacer = QFrame()
- spacer.setFixedHeight(4) # 可根据需要调整间隔高度
- spacer.setStyleSheet("background: transparent; border: none;")
- main_layout.addWidget(spacer)
-
- # 开发/设计软件类
- dev_tools = [
- ("STM32CubeMX", "https://www.st.com/zh/development-tools/stm32cubemx.html", "🟦"),
- ("Keil MDK", "https://www.keil.com/download/product/", "🟩"),
- ("Visual Studio Code", "https://code.visualstudio.com/", "🟦"),
- ("CLion", "https://www.jetbrains.com/clion/download/", "🟧"),
- ("MATLAB", "https://www.mathworks.com/downloads/", "🟨"),
- ("SolidWorks", "https://www.solidworks.com/sw/support/downloads.htm", "🟫"),
- ("Altium Designer", "https://www.altium.com/zh/altium-designer/downloads", "🟪"),
- ("原神", "https://download-porter.hoyoverse.com/download-porter/2025/03/27/GenshinImpact_install_202503072011.exe?trace_key=GenshinImpact_install_ua_679d0b4e9b10", "🟫"),
- ]
- dev_group = QGroupBox("开发/设计软件")
- dev_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
- dev_group.setStyleSheet("""
- QGroupBox {
- border: 2px solid #b5d0ea;
- border-radius: 12px;
- margin-top: 16px;
- background: #f8fbfd;
- color: #2471a3;
- padding: 10px 0 0 0;
- }
- QGroupBox:title {
- subcontrol-origin: margin;
- left: 18px;
- top: -10px;
- background: transparent;
- padding: 0 8px;
- }
- """)
- dev_layout = QGridLayout()
- dev_layout.setSpacing(18)
- dev_layout.setContentsMargins(24, 24, 24, 24)
- for idx, (name, url, icon) in enumerate(dev_tools):
- btn = QPushButton(f"{icon} {name}")
- btn.setFont(QFont("微软雅黑", 16, QFont.Bold))
- btn.setMinimumHeight(60)
- btn.setCursor(Qt.PointingHandCursor)
- btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2471a3;
- 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: #2471a3;
- border: 2px solid #a4cbe3;
- }
- """)
- btn.clicked.connect(lambda checked, link=url: webbrowser.open(link))
- row, col = divmod(idx, 4)
- dev_layout.addWidget(btn, row, col)
- dev_group.setLayout(dev_layout)
- main_layout.addWidget(dev_group)
-
- main_layout.addStretch(1)
-
- # 页脚
- footer = QLabel("如有问题或建议,欢迎反馈至QQ群:857466609")
- footer.setFont(QFont("微软雅黑", 13))
- footer.setAlignment(Qt.AlignCenter)
- footer.setStyleSheet("color: #b2bec3; margin-top: 18px;")
- main_layout.addWidget(footer)
-
-# --------- 功能三:串口助手 ---------
-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("微软雅黑", 12))
- self.hex_recv_chk.setFont(QFont("微软雅黑", 12))
- 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 = QHBoxLayout()
- proto_layout.setSpacing(18)
- proto_layout.setContentsMargins(16, 16, 16, 16)
- proto_layout.addWidget(QLabel("数据数量:"))
- self.data_count_spin = QSpinBox()
- self.data_count_spin.setRange(1, 16)
- self.data_count_spin.setValue(self.data_count)
- self.data_count_spin.setFixedWidth(80)
- self.data_count_spin.valueChanged.connect(self.apply_proto_config)
- proto_layout.addWidget(self.data_count_spin)
- proto_layout.addSpacing(18)
- proto_layout.addWidget(QLabel("数据类型:"))
- 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.setFixedWidth(100)
- self.data_type_box.currentTextChanged.connect(self.apply_proto_config)
- proto_layout.addWidget(self.data_type_box)
- # proto_layout.addStretch(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(16)
- send_layout.setContentsMargins(18, 18, 18, 18)
-
- # 输入框(多行)
- self.send_edit = QTextEdit()
- self.send_edit.setFont(QFont("Consolas", 18))
- self.send_edit.setPlaceholderText("输入要发送的数据,可多行(支持HEX/文本)...")
- self.send_edit.setMinimumHeight(140)
- self.send_edit.setMaximumHeight(220)
- self.send_edit.setStyleSheet("""
- QTextEdit {
- background: #f8fbfd;
- border-radius: 12px;
- border: 2px solid #d6eaf8;
- font-size: 18px;
- padding: 14px 20px;
- }
- """)
- send_layout.addWidget(self.send_edit)
-
- # HEX复选框和按钮行
- row1 = QHBoxLayout()
- row1.setSpacing(24)
- self.hex_send_chk.setStyleSheet("""
- QCheckBox {
- color: #2471a3;
- font-size: 15px;
- }
- QCheckBox::indicator {
- width: 22px;
- height: 22px;
- }
- QCheckBox::indicator:checked {
- background-color: #2980b9;
- border: 1.5px solid #2980b9;
- }
- QCheckBox::indicator:unchecked {
- background-color: #fff;
- border: 1.5px solid #b5d0ea;
- }
- """)
- self.hex_recv_chk.setStyleSheet(self.hex_send_chk.styleSheet())
- row1.addWidget(self.hex_send_chk)
- row1.addWidget(self.hex_recv_chk)
- row1.addStretch(1)
- send_layout.addLayout(row1)
-
- # 发送和持续发送按钮+频率(优化为“每秒发送次数”)
- row2 = QHBoxLayout()
- row2.setSpacing(18)
- self.send_btn = QPushButton("发送")
- self.send_btn.clicked.connect(self.send_data)
- self.send_btn.setFont(QFont("微软雅黑", 16, QFont.Bold))
- self.send_btn.setFixedHeight(44)
- self.send_btn.setFixedWidth(120)
- self.send_btn.setStyleSheet(self._btn_style("#2980b9"))
- row2.addWidget(self.send_btn)
-
- self.cont_send_btn = QPushButton("持续发送")
- self.cont_send_btn.setCheckable(True)
- self.cont_send_btn.setFont(QFont("微软雅黑", 15))
- self.cont_send_btn.setFixedHeight(44)
- self.cont_send_btn.setFixedWidth(120)
- self.cont_send_btn.setStyleSheet(self._btn_style("#f1c40f"))
- self.cont_send_btn.clicked.connect(self.toggle_cont_send)
- row2.addWidget(self.cont_send_btn)
-
- freq_label = QLabel(" 每秒发送次数:")
- freq_label.setFont(QFont("微软雅黑", 8))
- freq_label.setFixedWidth(180)
- #文本居中
- # freq_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
- row2.addWidget(freq_label)
-
- self.freq_input = QSpinBox()
- self.freq_input.setRange(1, 1000)
- self.freq_input.setValue(5)
- self.freq_input.setFont(QFont("Consolas", 14))
- self.freq_input.setFixedWidth(100)
- self.freq_input.setStyleSheet("""
- QSpinBox {
- background: #f8fbfd;
- border-radius: 8px;
- border: 1px solid #d6eaf8;
- font-size: 15px;
- padding: 2px 8px;
- }
- """)
- row2.addWidget(self.freq_input)
-
- # freq_unit = QLabel("次/秒")
- # freq_unit.setFont(QFont("微软雅黑", 13))
- # freq_unit.setFixedWidth(40)
- # row2.addWidget(freq_unit)
- row2.addStretch(1)
- send_layout.addLayout(row2)
-
- self.cont_send_timer = QTimer(self)
- self.cont_send_timer.timeout.connect(self.send_data)
-
- send_group.setLayout(send_layout)
- left_panel.addWidget(send_group, stretch=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"
- " 0x55 + 数据数量(1字节) + 数据 + 校验和(1字节)\n"
- " 校验和为包头到最后一个数据字节的累加和的低8位。\n"
- "3. 每包数据自动绘制曲线,X轴为采样点(或时间),Y轴为各通道数据。\n"
- )
- usage_label.setWordWrap(True)
- usage_label.setFont(QFont("微软雅黑", 9))
- usage_layout.addWidget(usage_label)
- usage_group.setLayout(usage_layout)
- left_panel.addWidget(usage_group)
-
- # 清空按钮紧贴使用说明
- self.clear_btn = QPushButton("清空接收和曲线")
- self.clear_btn.clicked.connect(self.clear_all)
- self.clear_btn.setStyleSheet(self._btn_style("#e74c3c"))
- self.clear_btn.setFixedHeight(38)
- left_panel.addWidget(self.clear_btn)
-
- 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 parse_hex_string(self, s):
- """支持 0x11 0x22 33 44 格式转bytes"""
- s = s.strip().replace(',', ' ').replace(';', ' ')
- parts = s.split()
- result = []
- for part in parts:
- if part.startswith('0x') or part.startswith('0X'):
- try:
- result.append(int(part, 16))
- except Exception:
- pass
- else:
- try:
- result.append(int(part, 16))
- except Exception:
- pass
- return bytes(result)
-
- def send_data(self):
- if self.ser and self.ser.is_open:
- data = self.send_edit.toPlainText()
- try:
- if self.hex_send_chk.isChecked():
- # 支持 0x11 0x22 33 44 格式
- data_bytes = self.parse_hex_string(data)
- if not data_bytes:
- self.recv_box.append("HEX格式错误,未发送。")
- return
- self.ser.write(data_bytes)
- self.recv_box.append(f"发送(HEX): {' '.join(['%02X'%b for b in data_bytes])}")
- 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 toggle_cont_send(self):
- if self.cont_send_btn.isChecked():
- try:
- interval = int(self.freq_box.currentText())
- except Exception:
- interval = 200
- self.cont_send_timer.start(interval)
- self.cont_send_btn.setText("停止发送")
- else:
- self.cont_send_timer.stop()
- self.cont_send_btn.setText("持续发送")
-
- 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_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 = self.parse_hex_string(line.strip())
- self.ser.write(data_bytes)
- self.recv_box.append(f"发送(HEX): {' '.join(['%02X'%b for b in data_bytes])}")
- 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("Sample", fontsize=14)
- ax.set_ylabel("Value", 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):
- repo_ready_signal = pyqtSignal()
-
- def __init__(self):
- super().__init__()
- self.setFont(QFont("微软雅黑", 15))
- self.setStyleSheet("""
- QWidget {
- background: #f8fbfd;
- border-radius: 16px;
- padding: 20px;
- }
- """)
- # 变量初始化
- self.repo_dir = "MRobot_repo"
- self.repo_url = "http://gitea.qutrobot.top/robofish/MRobot.git"
- self.header_file_vars = {}
- self.task_vars = []
- self.ioc_data = None
- self.add_gitignore = False
- self.auto_configure = False
- self.repo_ready = False # 标志:仓库是否已准备好
- self.init_ui()
- self.repo_ready_signal.connect(self.on_repo_ready)
-
- def showEvent(self, event):
- super().showEvent(event)
- if not self.repo_ready:
- self.log("首次进入,正在克隆MRobot仓库...")
- self.clone_repo_and_refresh()
-
- def clone_repo_and_refresh(self):
- import threading
- def do_clone():
- self.clone_repo()
- self.repo_ready = True
- self.ioc_data = self.find_and_read_ioc_file()
- self.repo_ready_signal.emit()
- threading.Thread(target=do_clone).start()
-
- @pyqtSlot()
- def on_repo_ready(self):
- self.update_freertos_status()
- self.update_header_files()
- self.update_task_ui()
- self.log("仓库准备完成!")
-
- def init_ui(self):
- main_layout = QVBoxLayout(self)
- main_layout.setSpacing(18)
- main_layout.setContentsMargins(32, 32, 32, 32)
- self.setStyleSheet("""
- QWidget {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- border-radius: 16px;
- }
- """)
-
- # 顶部标题区
- title = QLabel("MRobot 架构生成工具")
- title.setFont(QFont("微软雅黑", 22, QFont.Bold))
- title.setAlignment(Qt.AlignCenter)
- title.setStyleSheet("color: #2980b9; letter-spacing: 2px; margin-bottom: 2px;")
- main_layout.addWidget(title)
-
- desc = QLabel("快速生成 MRobot 项目代码,自动管理模块、任务与环境配置。")
- desc.setFont(QFont("微软雅黑", 13))
- desc.setAlignment(Qt.AlignCenter)
- desc.setStyleSheet("color: #34495e; margin-bottom: 8px;")
- main_layout.addWidget(desc)
-
- # 状态与选项区
- status_opt_row = QHBoxLayout()
- status_opt_row.setSpacing(24)
-
- # 状态区
- status_col = QVBoxLayout()
- self.freertos_status_label = QLabel("FreeRTOS 状态: 检测中...")
- self.freertos_status_label.setFont(QFont("微软雅黑", 12))
- self.freertos_status_label.setStyleSheet("color: #2471a3;")
- status_col.addWidget(self.freertos_status_label)
- status_col.addStretch(1)
- status_opt_row.addLayout(status_col, 1)
-
- # 选项区
- option_col = QVBoxLayout()
- self.gitignore_chk = QCheckBox("生成 .gitignore")
- self.gitignore_chk.setFont(QFont("微软雅黑", 12))
- self.gitignore_chk.stateChanged.connect(lambda x: setattr(self, "add_gitignore", x == Qt.Checked))
- option_col.addWidget(self.gitignore_chk)
- self.auto_env_chk = QCheckBox("自动环境配置")
- self.auto_env_chk.setFont(QFont("微软雅黑", 12))
- self.auto_env_chk.stateChanged.connect(lambda x: setattr(self, "auto_configure", x == Qt.Checked))
- option_col.addWidget(self.auto_env_chk)
- option_col.addStretch(1)
- status_opt_row.addLayout(option_col, 1)
-
- status_opt_row.addStretch(2)
- main_layout.addLayout(status_opt_row)
-
- # 主体分区:左侧模块选择,右侧任务管理
- body_layout = QHBoxLayout()
- body_layout.setSpacing(24)
-
- # 左侧:模块文件选择
- left_col = QVBoxLayout()
- self.header_group = QGroupBox("模块文件选择")
- self.header_group.setFont(QFont("微软雅黑", 15, QFont.Bold))
- self.header_group.setStyleSheet("""
- QGroupBox {
- border: 2px solid #b5d0ea;
- border-radius: 12px;
- margin-top: 8px;
- background: #f8fbfd;
- color: #2471a3;
- padding: 10px 0 0 0;
- }
- QGroupBox:title {
- subcontrol-origin: margin;
- left: 18px;
- top: -10px;
- background: transparent;
- padding: 0 8px;
- }
- """)
- self.header_layout = QVBoxLayout(self.header_group)
- self.header_layout.setSpacing(8)
- left_col.addWidget(self.header_group)
- left_col.addStretch(1)
- body_layout.addLayout(left_col, 2)
-
- # 右侧:任务管理
- right_col = QVBoxLayout()
- self.task_group = QGroupBox("任务管理 (FreeRTOS)")
- self.task_group.setFont(QFont("微软雅黑", 15, QFont.Bold))
- self.task_group.setStyleSheet(self.header_group.styleSheet())
- self.task_layout = QVBoxLayout(self.task_group)
- self.task_layout.setSpacing(8)
- right_col.addWidget(self.task_group)
- right_col.addStretch(1)
- body_layout.addLayout(right_col, 2)
-
- main_layout.addLayout(body_layout)
-
- # 生成按钮区
- btn_row = QHBoxLayout()
- btn_row.addStretch(1)
- self.generate_btn = QPushButton("一键生成 MRobot 代码")
- self.generate_btn.setFont(QFont("微软雅黑", 18, QFont.Bold))
- self.generate_btn.setMinimumHeight(48)
- self.generate_btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2980b9;
- border-radius: 20px;
- font-size: 20px;
- font-weight: 600;
- padding: 12px 0;
- border: 1px solid #d6eaf8;
- }
- QPushButton:hover {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #f8fffe, stop:1 #cfe7fa);
- color: #1a6fae;
- border: 1.5px solid #b5d0ea;
- }
- QPushButton:pressed {
- background: #e3f0fa;
- color: #2471a3;
- border: 1.5px solid #a4cbe3;
- }
- """)
- self.generate_btn.clicked.connect(self.generate_action)
- btn_row.addWidget(self.generate_btn)
- btn_row.addStretch(1)
- main_layout.addLayout(btn_row)
-
- # 日志输出区
- self.msg_box = QTextEdit()
- self.msg_box.setReadOnly(True)
- self.msg_box.setFont(QFont("Consolas", 13))
- self.msg_box.setMaximumHeight(100)
- self.msg_box.setStyleSheet("""
- QTextEdit {
- background: #f4f6f7;
- border-radius: 8px;
- border: 1px solid #d6eaf8;
- font-size: 15px;
- color: #2c3e50;
- padding: 8px;
- }
- """)
- main_layout.addWidget(self.msg_box)
-
- # 页脚
- footer = QLabel("如遇问题请反馈至 QUT 机器人战队")
- footer.setFont(QFont("微软雅黑", 11))
- footer.setAlignment(Qt.AlignCenter)
- footer.setStyleSheet("color: #b2bec3; margin-top: 6px;")
- main_layout.addWidget(footer)
-
- # 初始化内容
- self.update_header_files()
- self.update_task_ui()
-
- # ...其余方法保持不变...
-
- def log(self, msg):
- self.msg_box.append(msg)
-
- def clone_repo(self):
- import shutil
- from git import Repo
- if os.path.exists(self.repo_dir):
- shutil.rmtree(self.repo_dir)
- try:
- self.log("正在克隆仓库...")
- Repo.clone_from(self.repo_url, self.repo_dir, multi_options=["--depth=1"])
- self.log("仓库克隆成功!")
- except Exception as e:
- self.log(f"克隆仓库失败: {e}")
-
- def find_and_read_ioc_file(self):
- for file in os.listdir("."):
- if file.endswith(".ioc"):
- with open(file, "r", encoding="utf-8") as f:
- return f.read()
- self.log("未找到 .ioc 文件!")
- return None
-
- def check_freertos_enabled(self):
- import re
- if not self.ioc_data:
- return False
- return bool(re.search(r"Mcu\.IP\d+=FREERTOS", self.ioc_data))
-
- def update_freertos_status(self):
- if self.ioc_data:
- status = "已启用" if self.check_freertos_enabled() else "未启用"
- else:
- status = "未检测到 .ioc 文件"
- self.freertos_status_label.setText(f"FreeRTOS 状态: {status}")
-
- def update_header_files(self):
- for i in reversed(range(self.header_layout.count())):
- widget = self.header_layout.itemAt(i).widget()
- if widget:
- widget.deleteLater()
- if not self.repo_ready or not os.path.exists(self.repo_dir):
- return
- from collections import defaultdict
- import csv
- folders = ["bsp", "component", "device", "module"]
- dependencies = defaultdict(list)
- for folder in folders:
- folder_dir = os.path.join(self.repo_dir, "User", folder)
- dep_file = os.path.join(folder_dir, "dependencies.csv")
- if os.path.exists(dep_file):
- with open(dep_file, "r", encoding="utf-8") as f:
- reader = csv.reader(f)
- for row in reader:
- if len(row) == 2:
- dependencies[row[0]].append(row[1])
- for folder in folders:
- folder_dir = os.path.join(self.repo_dir, "User", folder)
- if os.path.exists(folder_dir):
- group = QGroupBox(folder)
- g_layout = QHBoxLayout(group)
- for file in os.listdir(folder_dir):
- file_base, file_ext = os.path.splitext(file)
- if file_ext == ".h" and file_base != folder:
- var = QCheckBox(file_base)
- var.stateChanged.connect(lambda x, fb=file_base: self.handle_dependencies(fb, dependencies))
- self.header_file_vars[file_base] = var
- g_layout.addWidget(var)
- self.header_layout.addWidget(group)
-
- def handle_dependencies(self, file_base, dependencies):
- if file_base in self.header_file_vars and self.header_file_vars[file_base].isChecked():
- for dep in dependencies.get(file_base, []):
- dep_base = os.path.basename(dep)
- if dep_base in self.header_file_vars:
- self.header_file_vars[dep_base].setChecked(True)
-
- def update_task_ui(self):
- for i in reversed(range(self.task_layout.count())):
- widget = self.task_layout.itemAt(i).widget()
- if widget:
- widget.deleteLater()
- if not self.repo_ready or not self.check_freertos_enabled():
- self.task_group.setVisible(False)
- return
- self.task_group.setVisible(True)
- for i, (task_var, freq_var) in enumerate(self.task_vars):
- row = QHBoxLayout()
- name_edit = QLineEdit(task_var)
- freq_spin = QSpinBox()
- freq_spin.setRange(1, 1000)
- freq_spin.setValue(freq_var)
- del_btn = QPushButton("删除")
- del_btn.clicked.connect(lambda _, idx=i: self.remove_task(idx))
- row.addWidget(name_edit)
- row.addWidget(QLabel("频率:"))
- row.addWidget(freq_spin)
- row.addWidget(del_btn)
- container = QWidget()
- container.setLayout(row)
- self.task_layout.addWidget(container)
- add_btn = QPushButton("添加任务")
- add_btn.clicked.connect(self.add_task)
- self.task_layout.addWidget(add_btn)
-
- def add_task(self):
- self.task_vars.append([f"Task_{len(self.task_vars)+1}", 100])
- self.update_task_ui()
-
- def remove_task(self, idx):
- if 0 <= idx < len(self.task_vars):
- self.task_vars.pop(idx)
- self.update_task_ui()
-
- def copy_file_from_repo(self, src_path, dest_path):
- import shutil
- if src_path.startswith(self.repo_dir):
- full_src_path = src_path
- else:
- full_src_path = os.path.join(self.repo_dir, src_path.lstrip(os.sep))
- if not os.path.exists(full_src_path):
- self.log(f"文件 {full_src_path} 不存在!")
- return
- dest_dir = os.path.dirname(dest_path)
- if dest_dir and not os.path.exists(dest_dir):
- os.makedirs(dest_dir, exist_ok=True)
- shutil.copy(full_src_path, dest_path)
- self.log(f"已复制 {full_src_path} 到 {dest_path}")
-
- def generate_action(self):
- import threading
- def task():
- self.create_directories()
- if self.add_gitignore:
- self.copy_file_from_repo(".gitignore", ".gitignore")
- if self.ioc_data and self.check_freertos_enabled():
- self.copy_file_from_repo("src/freertos.c", os.path.join("Core", "Src", "freertos.c"))
- folders = ["bsp", "component", "device", "module"]
- for folder in folders:
- folder_dir = os.path.join(self.repo_dir, "User", folder)
- if not os.path.exists(folder_dir):
- continue
- for file_name in os.listdir(folder_dir):
- file_base, file_ext = os.path.splitext(file_name)
- if file_ext not in [".h", ".c"]:
- continue
- if file_base == folder:
- src_path = os.path.join(folder_dir, file_name)
- dest_path = os.path.join("User", folder, file_name)
- self.copy_file_from_repo(src_path, dest_path)
- continue
- if file_base in self.header_file_vars and self.header_file_vars[file_base].isChecked():
- src_path = os.path.join(folder_dir, file_name)
- dest_path = os.path.join("User", folder, file_name)
- self.copy_file_from_repo(src_path, dest_path)
- if self.ioc_data and self.check_freertos_enabled():
- self.modify_user_task_file()
- self.generate_user_task_header()
- self.generate_init_file()
- self.generate_task_files()
- self.log("生成完成!")
- threading.Thread(target=task).start()
-
- def create_directories(self):
- dirs = [
- "User/bsp",
- "User/component",
- "User/device",
- "User/module",
- ]
- if self.ioc_data and self.check_freertos_enabled():
- dirs.append("User/task")
- for d in dirs:
- if not os.path.exists(d):
- os.makedirs(d, exist_ok=True)
- self.log(f"已创建目录: {d}")
-
- def generate_task_files(self):
- try:
- import re
- template_file_path = os.path.join(self.repo_dir, "User", "task", "task.c.template")
- task_dir = os.path.join("User", "task")
- if not os.path.exists(template_file_path):
- self.log(f"模板文件 {template_file_path} 不存在,无法生成 task.c 文件!")
- return
- os.makedirs(task_dir, exist_ok=True)
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
- for task in self.task_vars:
- if isinstance(task, (list, tuple)):
- task_name = str(task[0])
- else:
- task_name = str(task)
- task_file_path = os.path.join(task_dir, f"{task_name.lower()}.c")
- task_content = template_content.replace("{{task_name}}", task_name)
- task_content = task_content.replace("{{task_function}}", task_name)
- task_content = task_content.replace(
- "{{task_frequency}}", f"TASK_FREQ_{task_name.upper()}"
- )
- task_content = task_content.replace("{{task_delay}}", f"TASK_INIT_DELAY_{task_name.upper()}")
- with open(task_file_path, "w", encoding="utf-8") as f2:
- f2.write(task_content)
- self.log(f"已成功生成 {task_file_path} 文件!")
- except Exception as e:
- self.log(f"生成 task.c 文件时出错: {e}")
-
- def modify_user_task_file(self):
- try:
- import re
- template_file_path = os.path.join(self.repo_dir, "User", "task", "user_task.c.template")
- generated_task_file_path = os.path.join("User", "task", "user_task.c")
- if not os.path.exists(template_file_path):
- self.log(f"模板文件 {template_file_path} 不存在,无法生成 user_task.c 文件!")
- return
- os.makedirs(os.path.dirname(generated_task_file_path), exist_ok=True)
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
- task_attr_definitions = "\n".join([
- f"""const osThreadAttr_t attr_{str(task[0]).lower()} = {{
- .name = "{str(task[0])}",
- .priority = osPriorityNormal,
- .stack_size = 128 * 4,
-}};"""
- for task in self.task_vars
- ])
- task_content = template_content.replace("{{task_attr_definitions}}", task_attr_definitions)
- with open(generated_task_file_path, "w", encoding="utf-8") as f2:
- f2.write(task_content)
- self.log(f"已成功生成 {generated_task_file_path} 文件!")
- except Exception as e:
- self.log(f"修改 user_task.c 文件时出错: {e}")
-
- def generate_user_task_header(self):
- try:
- import re
- template_file_path = os.path.join(self.repo_dir, "User", "task", "user_task.h.template")
- header_file_path = os.path.join("User", "task", "user_task.h")
- if not os.path.exists(template_file_path):
- self.log(f"模板文件 {template_file_path} 不存在,无法生成 user_task.h 文件!")
- return
- os.makedirs(os.path.dirname(header_file_path), exist_ok=True)
- existing_msgq_content = ""
- if os.path.exists(header_file_path):
- with open(header_file_path, "r", encoding="utf-8") as f:
- content = f.read()
- match = re.search(r"/\* USER MESSAGE BEGIN \*/\s*(.*?)\s*/\* USER MESSAGE END \*/", content, re.DOTALL)
- if match:
- existing_msgq_content = match.group(1).strip()
- self.log("已存在的 msgq 区域内容已保留")
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
- thread_definitions = "\n".join([f" osThreadId_t {str(task[0]).lower()};" for task in self.task_vars])
- msgq_definitions = existing_msgq_content if existing_msgq_content else " osMessageQueueId_t default_msgq;"
- freq_definitions = "\n".join([f" float {str(task[0]).lower()};" for task in self.task_vars])
- last_up_time_definitions = "\n".join([f" uint32_t {str(task[0]).lower()};" for task in self.task_vars])
- task_attr_declarations = "\n".join([f"extern const osThreadAttr_t attr_{str(task[0]).lower()};" for task in self.task_vars])
- task_function_declarations = "\n".join([f"void {str(task[0])}(void *argument);" for task in self.task_vars])
- task_frequency_definitions = "\n".join([
- f"#define TASK_FREQ_{str(task[0]).upper()} ({int(task[1])}u)"
- for task in self.task_vars
- ])
- task_init_delay_definitions = "\n".join([f"#define TASK_INIT_DELAY_{str(task[0]).upper()} (0u)" for task in self.task_vars])
- task_handle_definitions = "\n".join([f" osThreadId_t {str(task[0]).lower()};" for task in self.task_vars])
- header_content = template_content.replace("{{thread_definitions}}", thread_definitions)
- header_content = header_content.replace("{{msgq_definitions}}", msgq_definitions)
- header_content = header_content.replace("{{freq_definitions}}", freq_definitions)
- header_content = header_content.replace("{{last_up_time_definitions}}", last_up_time_definitions)
- header_content = header_content.replace("{{task_attr_declarations}}", task_attr_declarations)
- header_content = header_content.replace("{{task_function_declarations}}", task_function_declarations)
- header_content = header_content.replace("{{task_frequency_definitions}}", task_frequency_definitions)
- header_content = header_content.replace("{{task_init_delay_definitions}}", task_init_delay_definitions)
- header_content = header_content.replace("{{task_handle_definitions}}", task_handle_definitions)
- if existing_msgq_content:
- header_content = re.sub(
- r"/\* USER MESSAGE BEGIN \*/\s*.*?\s*/\* USER MESSAGE END \*/",
- f"/* USER MESSAGE BEGIN */\n\n {existing_msgq_content}\n\n /* USER MESSAGE END */",
- header_content,
- flags=re.DOTALL
- )
- with open(header_file_path, "w", encoding="utf-8") as f2:
- f2.write(header_content)
- self.log(f"已成功生成 {header_file_path} 文件!")
- except Exception as e:
- self.log(f"生成 user_task.h 文件时出错: {e}")
-
- def generate_init_file(self):
- try:
- import re
- template_file_path = os.path.join(self.repo_dir, "User", "task", "init.c.template")
- generated_file_path = os.path.join("User", "task", "init.c")
- if not os.path.exists(template_file_path):
- self.log(f"模板文件 {template_file_path} 不存在,无法生成 init.c 文件!")
- return
- os.makedirs(os.path.dirname(generated_file_path), exist_ok=True)
- existing_msgq_content = ""
- if os.path.exists(generated_file_path):
- with open(generated_file_path, "r", encoding="utf-8") as f:
- content = f.read()
- match = re.search(r"/\* USER MESSAGE BEGIN \*/\s*(.*?)\s*/\* USER MESSAGE END \*/", content, re.DOTALL)
- if match:
- existing_msgq_content = match.group(1).strip()
- self.log("已存在的消息队列区域内容已保留")
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
- thread_creation_code = "\n".join([
- f" task_runtime.thread.{str(task[0]).lower()} = osThreadNew({str(task[0])}, NULL, &attr_{str(task[0]).lower()});"
- for task in self.task_vars
- ])
- init_content = template_content.replace("{{thread_creation_code}}", thread_creation_code)
- if existing_msgq_content:
- init_content = re.sub(
- r"/\* USER MESSAGE BEGIN \*/\s*.*?\s*/\* USER MESSAGE END \*/",
- f"/* USER MESSAGE BEGIN */\n {existing_msgq_content}\n /* USER MESSAGE END */",
- init_content,
- flags=re.DOTALL
- )
- with open(generated_file_path, "w", encoding="utf-8") as f2:
- f2.write(init_content)
- self.log(f"已成功生成 {generated_file_path} 文件!")
- except Exception as e:
- self.log(f"生成 init.c 文件时出错: {e}")
-
-# --------- 功能五:零件库 ---------
-
-class CustomWebView(QWebEngineView):
- def __init__(self, parent=None, popup_list=None):
- super().__init__(parent)
- self.popup_list = popup_list
- self._progress_dialog = None
- self.page().profile().downloadRequested.connect(self.handle_download)
- self.setStyleSheet("""
- QWebEngineView {
- border-radius: 12px;
- background: #f8fbfd;
- border: 1px solid #d6eaf8;
- }
- """)
-
- def handle_download(self, download_item):
- from PyQt5.QtWidgets import QFileDialog, QProgressDialog
-
- # 防止重复弹窗
- if hasattr(download_item, "_handled") and download_item._handled:
- return
- download_item._handled = True
-
- suggested = download_item.suggestedFileName()
- path, _ = QFileDialog.getSaveFileName(self, "保存文件", suggested)
- if not path:
- download_item.cancel()
- return
-
- download_item.setPath(path)
- download_item.accept()
-
- # 创建进度对话框
- self._progress_dialog = QProgressDialog(f"正在下载: {suggested}", "取消", 0, 100, self)
- self._progress_dialog.setWindowTitle("下载进度")
- self._progress_dialog.setWindowModality(Qt.WindowModal)
- self._progress_dialog.setMinimumDuration(0)
- self._progress_dialog.setValue(0)
- self._progress_dialog.canceled.connect(download_item.cancel)
- self._progress_dialog.show()
-
- def on_progress(received, total):
- if total > 0:
- percent = int(received * 100 / total)
- self._progress_dialog.setValue(percent)
- else:
- self._progress_dialog.setValue(0)
-
- download_item.downloadProgress.connect(on_progress)
-
- def on_finished():
- self._progress_dialog.setValue(100)
- self._progress_dialog.close()
- if self.parent() and isinstance(self.parent(), CustomWebView):
- self.parent().close()
- elif self.popup_list and self in self.popup_list:
- self.close()
- self.popup_list.remove(self)
-
- download_item.finished.connect(on_finished)
-
- def createWindow(self, _type):
- popup = CustomWebView(popup_list=self.popup_list)
- popup.setAttribute(Qt.WA_DeleteOnClose)
- popup.setWindowTitle("下载")
- popup.resize(900, 600)
- popup.show()
- if self.popup_list is not None:
- self.popup_list.append(popup)
- return popup
-
-class MachineryLibrary(QWidget):
- def __init__(self):
- super().__init__()
- self.popup_windows = []
- 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.setSpacing(18)
- main_layout.setContentsMargins(32, 32, 32, 32)
-
- # 标题区
- title = QLabel("MRobot 零件库")
- title.setFont(QFont("微软雅黑", 22, QFont.Bold))
- title.setAlignment(Qt.AlignCenter)
- title.setStyleSheet("color: #2980b9; letter-spacing: 2px; margin-bottom: 2px;")
- main_layout.addWidget(title)
-
- desc = QLabel("零件库账号:Engineer(无密码)")
- desc.setFont(QFont("微软雅黑", 13))
- desc.setAlignment(Qt.AlignCenter)
- desc.setStyleSheet("color: #34495e; margin-bottom: 8px;")
- main_layout.addWidget(desc)
-
- # 加载提示
- self.loading_label = QLabel("正在加载零件库网页,请稍候...")
- self.loading_label.setAlignment(Qt.AlignCenter)
- self.loading_label.setFont(QFont("微软雅黑", 14))
- self.loading_label.setStyleSheet("color: #888; margin-bottom: 8px;")
- main_layout.addWidget(self.loading_label)
-
- # 网页视图
- self.webview = CustomWebView(parent=self, popup_list=self.popup_windows)
- self.webview.setAttribute(Qt.WA_TranslucentBackground, True)
- self.webview.setMinimumHeight(480)
- self.webview.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- self.webview.loadFinished.connect(self.on_webview_loaded)
- main_layout.addWidget(self.webview, stretch=10)
-
- # 刷新按钮
- btn_row = QHBoxLayout()
- btn_row.addStretch(1)
- self.refresh_btn = QPushButton("刷新零件库")
- self.refresh_btn.setFont(QFont("微软雅黑", 13, QFont.Bold))
- self.refresh_btn.setFixedWidth(140)
- self.refresh_btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2980b9;
- border-radius: 14px;
- font-size: 15px;
- font-weight: 600;
- padding: 8px 0;
- border: 1.5px solid #d6eaf8;
- }
- 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: #2471a3;
- border: 2px solid #a4cbe3;
- }
- """)
- self.refresh_btn.clicked.connect(self.reload_webview)
- btn_row.addWidget(self.refresh_btn)
- btn_row.addStretch(1)
- main_layout.addLayout(btn_row)
-
- # 自动加载网页
- QTimer.singleShot(200, lambda: self.webview.setUrl(QUrl("http://alist.qutrobot.top")))
- self.webview.show()
-
- # 定时刷新(可选,防止页面假死)
- self.refresh_timer = QTimer(self)
- self.refresh_timer.setInterval(100)
- self.refresh_timer.timeout.connect(self.webview.update)
- self.refresh_timer.start()
-
- def reload_webview(self):
- self.loading_label.show()
- self.webview.setUrl(QUrl("http://alist.qutrobot.top"))
-
- def on_webview_loaded(self):
- self.loading_label.hide()
-
- def closeEvent(self, event):
- self.refresh_timer.stop()
- super().closeEvent(event)
-# --------- 主工具箱UI ---------
-class ToolboxUI(QWidget):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("MRobot 工具箱")
- self.resize(1920, 1080)
- self.setMinimumSize(900, 600)
- self.setStyleSheet("""
- QWidget {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- border-radius: 16px;
- }
- """)
- self.init_ui()
-
- def init_ui(self):
- main_layout = QHBoxLayout(self)
- main_layout.setContentsMargins(20, 20, 20, 20)
- main_layout.setSpacing(20)
-
- # 左侧导航
- left_frame = QFrame()
- left_frame.setFrameShape(QFrame.StyledPanel)
- left_frame.setStyleSheet("""
- QFrame {
- background: #f8fbfd;
- border-radius: 14px;
- border: 1px solid #d6eaf8;
- }
- """)
- left_shadow = QGraphicsDropShadowEffect(self)
- left_shadow.setBlurRadius(16)
- left_shadow.setOffset(0, 4)
- left_shadow.setColor(Qt.gray)
- left_frame.setGraphicsEffect(left_shadow)
-
- left_layout = QVBoxLayout(left_frame)
- left_layout.setSpacing(24)
- left_layout.setContentsMargins(18, 18, 18, 18)
- left_frame.setFixedWidth(260)
- main_layout.addWidget(left_frame)
-
- # Logo
- logo_label = QLabel()
- logo_pixmap = QPixmap(resource_path("mr_tool_img/MRobot.png"))
- if not logo_pixmap.isNull():
- logo_label.setPixmap(logo_pixmap.scaled(180, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation))
- else:
- logo_label.setText("MRobot")
- logo_label.setAlignment(Qt.AlignCenter)
- logo_label.setFont(QFont("Arial", 36, QFont.Bold))
- logo_label.setStyleSheet("color: #3498db;")
- logo_label.setFixedHeight(120)
- left_layout.addWidget(logo_label)
-
- # 按钮区
- self.button_names = ["主页", "曲线拟合", "Mini串口助手", "MR架构配置", "零件库", "软件指南"]
- self.buttons = []
- for idx, name in enumerate(self.button_names):
- btn = QPushButton(name)
- btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
- btn.setMinimumHeight(48)
- btn.setStyleSheet("""
- QPushButton {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #eaf6fb, stop:1 #d6eaf8);
- color: #2980b9;
- border-radius: 20px;
- font-size: 22px;
- font-weight: 600;
- padding: 14px 0;
- border: 1px solid #d6eaf8;
- letter-spacing: 2px;
- }
- QPushButton:hover {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
- stop:0 #f8fffe, stop:1 #cfe7fa);
- color: #1a6fae;
- border: 1.5px solid #b5d0ea;
- }
- QPushButton:pressed {
- background: #e3f0fa;
- color: #2471a3;
- border: 1.5px solid #a4cbe3;
- }
- """)
- btn.clicked.connect(lambda checked, i=idx: self.switch_function(i))
- self.buttons.append(btn)
- left_layout.addWidget(btn)
-
- left_layout.addStretch(1)
-
- # 文本输出框
- self.output_box = QTextEdit()
- self.output_box.setReadOnly(True)
- self.output_box.setFixedHeight(180)
- self.output_box.setStyleSheet("""
- QTextEdit {
- background: #f4f6f7;
- border-radius: 8px;
- border: 1px solid #d6eaf8;
- font-size: 16px;
- color: #2c3e50;
- padding: 10px;
- }
- """)
- left_layout.addWidget(self.output_box)
-
- # 右侧功能区
- self.stack = QStackedWidget()
- self.stack.setStyleSheet("""
- QStackedWidget {
- background: #fff;
- border-radius: 16px;
- border: 1px solid #d6eaf8;
- }
- """)
- right_shadow = QGraphicsDropShadowEffect(self)
- right_shadow.setBlurRadius(24)
- right_shadow.setOffset(0, 4)
- right_shadow.setColor(Qt.gray)
- self.stack.setGraphicsEffect(right_shadow)
-
- # 功能页面注册(后续扩展只需在这里添加页面类即可)
- self.page_widgets = {
- 0: HomePage(), # 主页
- 1: PolyFitApp(), # 多项式拟合
- 2: SerialAssistant(), # 串口助手
- 3: GenerateMRobotCode(), # MRobot架构生成
- 4: MachineryLibrary(), # 零件库
- 5: DownloadPage(), # 下载页面
- }
- for i in range(len(self.button_names)):
- self.stack.addWidget(self.page_widgets[i])
- main_layout.addWidget(self.stack)
-
- self.output_box.append("欢迎使用 MRobot 工具箱!请选择左侧功能。")
-
-
-
- def placeholder_page(self, text):
- page = QWidget()
- layout = QVBoxLayout(page)
- label = QLabel(text)
- label.setAlignment(Qt.AlignCenter)
- label.setFont(QFont("微软雅黑", 22, QFont.Bold))
- label.setStyleSheet("color: #2980b9;")
- layout.addStretch(1)
- layout.addWidget(label)
- layout.addStretch(1)
- return page
-
- def switch_function(self, idx):
- self.stack.setCurrentIndex(idx)
- self.output_box.append(f"已切换到功能:{self.button_names[idx]}")
-
-if __name__ == "__main__":
-
- from PyQt5.QtWidgets import QSplashScreen, QGraphicsDropShadowEffect
- from PyQt5.QtCore import Qt, QTimer, QRect
- from PyQt5.QtGui import QPixmap, QPainter, QColor, QFont, QLinearGradient, QBrush
-
- class CustomSplash(QSplashScreen):
- def __init__(self, pixmap):
- super().__init__(QPixmap(640, 400))
- self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
- self.setAttribute(Qt.WA_TranslucentBackground)
- self.logo = pixmap.scaled(360, 240, Qt.KeepAspectRatio, Qt.SmoothTransformation)
- self.title = "MRobot 工具箱"
- self.subtitle = "欢迎使用 MRobot,正在启动中..."
- self.title_color = QColor("#2471a3")
- self.subtitle_color = QColor("#2980b9")
- self.bg_gradient = QLinearGradient(0, 0, 0, 400)
- self.bg_gradient.setColorAt(0, QColor("#f8fbfd"))
- self.bg_gradient.setColorAt(1, QColor("#eaf6fb"))
- self.border_color = QColor("#2980b9")
- self.setFixedSize(640, 400)
- # 阴影
- effect = QGraphicsDropShadowEffect(self)
- effect.setBlurRadius(36)
- effect.setOffset(0, 10)
- effect.setColor(QColor(80, 120, 180, 80))
- self.setGraphicsEffect(effect)
-
- def paintEvent(self, event):
- painter = QPainter(self)
- painter.setRenderHint(QPainter.Antialiasing)
- # 渐变背景
- painter.setBrush(QBrush(self.bg_gradient))
- painter.setPen(Qt.NoPen)
- painter.drawRoundedRect(0, 0, self.width(), self.height(), 44, 44)
- # 边框
- pen = painter.pen()
- pen.setColor(self.border_color)
- pen.setWidth(3)
- painter.setPen(pen)
- painter.drawRoundedRect(2, 2, self.width()-4, self.height()-4, 44, 44)
- # LOGO
- logo_x = (self.width() - self.logo.width()) // 2
- logo_y = 80
- painter.drawPixmap(logo_x, logo_y, self.logo)
- # 主标题
- # painter.setFont(QFont("微软雅黑", 24, QFont.Bold))
- # painter.setPen(self.title_color)
- # painter.drawText(QRect(0, 200, self.width(), 48), Qt.AlignCenter, self.title)
- # 副标题
- painter.setFont(QFont("微软雅黑", 12))
- painter.setPen(self.subtitle_color)
- painter.drawText(QRect(0, 250, self.width(), 36), Qt.AlignCenter, self.subtitle)
- # 版权
- painter.setFont(QFont("微软雅黑", 10))
- painter.setPen(QColor("#b2bec3"))
- painter.drawText(QRect(0, 360, self.width(), 30), Qt.AlignCenter, "© 2025 MRobot. All rights reserved.")
-
- app = QApplication(sys.argv)
-
- # 立即显示Splash
- logo_pix = QPixmap(resource_path("mr_tool_img/MRobot.png"))
- splash = CustomSplash(logo_pix)
- splash.show()
- app.processEvents() # 强制立即刷新Splash
-
- # 异步加载主窗口
- def load_main():
- win = ToolboxUI()
- splash.finish(win)
- win.show()
-
- QTimer.singleShot(100, load_main)
-
- sys.exit(app.exec_())
\ No newline at end of file
diff --git a/MRobot.iss b/MRobot.iss
new file mode 100644
index 0000000..450a7f2
--- /dev/null
+++ b/MRobot.iss
@@ -0,0 +1,17 @@
+[Setup]
+AppName=MRobot
+AppVersion=1.0
+DefaultDirName={userappdata}\MRobot
+DefaultGroupName=MRobot
+OutputDir=.
+OutputBaseFilename=MRobotInstaller
+
+[Files]
+Source: "dist\MRobot.exe"; DestDir: "{app}"; Flags: ignoreversion
+Source: "img\*"; DestDir: "{app}\img"; Flags: ignoreversion recursesubdirs
+Source: "User_code\*"; DestDir: "{app}\User_code"; Flags: ignoreversion recursesubdirs
+Source: "mech_lib\*"; DestDir: "{app}\mech_lib"; Flags: ignoreversion recursesubdirs
+
+[Icons]
+Name: "{group}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\img\M.ico"
+Name: "{userdesktop}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\img\M.ico"
\ No newline at end of file
diff --git a/MRobot.py b/MRobot.py
index c22b7df..ecb626e 100644
--- a/MRobot.py
+++ b/MRobot.py
@@ -6,121 +6,375 @@ import serial.tools.list_ports
from PyQt5.QtCore import Qt, pyqtSignal, QThread
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import (
- QApplication, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox,
- QComboBox, QPushButton, QTextEdit, QLineEdit, QLabel, QSizePolicy,
+ QApplication, QWidget, QVBoxLayout, QHBoxLayout,
+ QSizePolicy,
QFileDialog, QMessageBox, QStackedLayout
)
from qfluentwidgets import (
- Theme, setTheme, FluentIcon, SwitchButton, BodyLabel, SubtitleLabel,
+ Theme, setTheme, FluentIcon, SwitchButton, BodyLabel, SubtitleLabel,TitleLabel,
StrongBodyLabel, HorizontalSeparator, InfoBar, MessageDialog, Dialog,
AvatarWidget, NavigationItemPosition, FluentWindow, NavigationAvatarWidget,
- PushButton, TextEdit, LineEdit, ComboBox, ImageLabel
+ PushButton, TextEdit, LineEdit, ComboBox, ImageLabel,
)
from qfluentwidgets import FluentIcon as FIF
import requests
import shutil
from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView, QHeaderView
+from PyQt5.QtWidgets import QHeaderView
from qfluentwidgets import (
- TreeWidget, InfoBar, InfoBarPosition, MessageDialog, TreeItemDelegate
+ TreeWidget, InfoBar, InfoBarPosition,
)
from qfluentwidgets import CheckBox
from qfluentwidgets import TreeWidget
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QFileDialog
from qfluentwidgets import ProgressBar
+import zipfile
+import io
+import jinja2
+from PyQt5.QtWidgets import QTreeWidgetItem as TreeItem
+import yaml # 确保已安装 pyyaml
+import requests
+from PyQt5.QtCore import Qt, QTimer
+from qfluentwidgets import (
+ SettingCardGroup, SettingCard, ExpandSettingCard, HyperlinkButton, PushButton,
+ SubtitleLabel, StrongBodyLabel, BodyLabel, HorizontalSeparator, FluentIcon, InfoBar, InfoBarPosition
+)
+from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
+
+from qfluentwidgets import (
+ SettingCardGroup, ExpandSettingCard, SubtitleLabel, BodyLabel, HorizontalSeparator, FluentIcon, InfoBar
+
+)
+# 添加quote
+from urllib.parse import quote
+
+from packaging.version import parse as vparse
+__version__ = "1.0.0"
# ===================== 页面基类 =====================
class BaseInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
+# ===================== 启动界面 =====================
+from qfluentwidgets import ImageLabel, TitleLabel, BodyLabel, ProgressBar, PushSettingCard, HyperlinkCard
+from PyQt5.QtWidgets import QDialog, QVBoxLayout, QSpacerItem, QSizePolicy
+from PyQt5.QtCore import Qt
+# ...existing code...
+
+from qfluentwidgets import isDarkTheme # 加入主题判断
+
+class SplashScreen(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ # 自动适配主题
+ dark = isDarkTheme()
+ bg_color = "#23272e" if dark else "#f7fafd"
+ text_color = "#e9f6ff" if dark else "#2d7d9a"
+ sub_color = "#b0b8c1" if dark else "#6b7b8c"
+ border_color = "#3a3f4b" if dark else "#e0e6ef"
+
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
+ self.setModal(True)
+ self.setFixedSize(420, 260)
+ self.setStyleSheet(f"""
+ QDialog {{
+ background: {bg_color};
+ border-radius: 18px;
+ border: 1px solid {border_color};
+ }}
+ """)
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(36, 36, 36, 36)
+ layout.setSpacing(18)
+
+ layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding))
+
+ # Logo
+ self.logo = ImageLabel('img/MRobot.png')
+ self.logo.setFixedSize(220, 56)
+ self.logo.setAlignment(Qt.AlignCenter)
+ layout.addWidget(self.logo, alignment=Qt.AlignCenter)
+
+ # 应用名
+ self.title = TitleLabel("MRobot Toolbox")
+ self.title.setAlignment(Qt.AlignCenter)
+ self.title.setStyleSheet(f"font-size: 26px; font-weight: bold; color: {text_color};")
+ layout.addWidget(self.title)
+
+ # 状态文本
+ self.status = BodyLabel("正在启动...")
+ self.status.setAlignment(Qt.AlignCenter)
+ self.status.setStyleSheet(f"font-size: 15px; color: {sub_color};")
+ layout.addWidget(self.status)
+
+ # 进度条
+ self.progress = ProgressBar()
+ self.progress.setRange(0, 100)
+ self.progress.setValue(0)
+ self.progress.setFixedHeight(10)
+ layout.addWidget(self.progress)
+
+ layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding))
+
+ def set_status(self, text, value=None):
+ self.status.setText(text)
+ if value is not None:
+ self.progress.setValue(value)
+
# ===================== 首页界面 =====================
class HomeInterface(BaseInterface):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("homeInterface")
- layout = QVBoxLayout()
- layout.setContentsMargins(60, 60, 60, 60)
- layout.setSpacing(32)
- self.setLayout(layout)
- # 顶部logo和欢迎区
- top_layout = QHBoxLayout()
+ # 外层居中布局
+ outer_layout = QVBoxLayout(self)
+ outer_layout.setContentsMargins(0, 0, 0, 0)
+ outer_layout.setSpacing(0)
+ outer_layout.addStretch()
+
+ # 直接用布局和控件,无卡片
+ content_layout = QVBoxLayout()
+ content_layout.setSpacing(24)
+ content_layout.setContentsMargins(48, 48, 48, 48)
+
+ # Logo
logo = ImageLabel('img/MRobot.png')
- logo.setFixedSize(260, 80)
- top_layout.addWidget(logo, alignment=Qt.AlignmentFlag.AlignTop)
- title_layout = QVBoxLayout()
- title_layout.addWidget(StrongBodyLabel("欢迎使用 MRobot Toolbox"))
- title_layout.addWidget(SubtitleLabel("让你的机器人开发更高效、更智能"))
- top_layout.addLayout(title_layout)
- top_layout.addStretch()
- layout.addLayout(top_layout)
+ logo.setFixedSize(320, 80)
+ logo.setAlignment(Qt.AlignCenter)
+ content_layout.addWidget(logo, alignment=Qt.AlignHCenter)
- layout.addWidget(HorizontalSeparator())
+ content_layout.addSpacing(8)
+ content_layout.addStretch()
+ # 主标题
+ title = SubtitleLabel("MRobot Toolbox")
+ title.setAlignment(Qt.AlignCenter)
+ title.setStyleSheet("font-size: 32px; font-weight: bold;")
+ content_layout.addWidget(title)
- # 项目简介
- layout.addWidget(BodyLabel(
- "MRobot Toolbox 是一款集成化的机器人开发辅助工具,"
- "支持代码生成、串口终端、主题切换等多种实用功能。\n"
- "点击左侧导航栏可快速切换各功能页面。"
- ))
+ # 副标题
+ subtitle = BodyLabel("现代化,多功能机器人开发工具箱")
+ subtitle.setAlignment(Qt.AlignCenter)
+ subtitle.setStyleSheet("font-size: 18px; color: #4a6fa5;")
+ content_layout.addWidget(subtitle)
- # 开发者与项目目标
- layout.addWidget(HorizontalSeparator())
- layout.addWidget(SubtitleLabel("开发者与项目目标"))
- layout.addWidget(BodyLabel("开发团队:QUT 青岛理工大学 MOVE 战队"))
- layout.addWidget(BodyLabel("项目目标:为所有 rmer 和 rcer 提供现代化、简单、高效的机器人开发方式,"
- "让机器人开发变得更轻松、更智能。"))
- layout.addWidget(BodyLabel("适用于 RM、RC、各类嵌入式机器人项目。"))
+ # 欢迎语
+ welcome = BodyLabel("欢迎使用 MRobot Toolbox!一站式支持代码生成、硬件管理、串口调试与零件库下载。")
+ welcome.setAlignment(Qt.AlignCenter)
+ welcome.setStyleSheet("font-size: 15px;")
+ content_layout.addWidget(welcome)
- # layout.addStretch()
+ content_layout.addSpacing(16)
+ content_layout.addStretch()
+
+ # 直接加到主布局
+ outer_layout.addLayout(content_layout)
+ outer_layout.addStretch()
+
+ # 版权信息置底
+ copyright_label = BodyLabel("© 2025 MRobot | Powered by QUT RM&RCer")
+ copyright_label.setAlignment(Qt.AlignCenter)
+ copyright_label.setStyleSheet("font-size: 13px;")
+ outer_layout.addWidget(copyright_label)
+ outer_layout.addSpacing(18)
# ===================== 代码生成页面 =====================
+class IocConfig:
+ def __init__(self, ioc_path):
+ self.ioc_path = ioc_path
+ self.config = {}
+ self._parse()
+
+ def _parse(self):
+ with open(self.ioc_path, encoding='utf-8') as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ if '=' in line:
+ key, value = line.split('=', 1)
+ self.config[key.strip()] = value.strip()
+
+ def is_freertos_enabled(self):
+ # 判断是否开启FreeRTOS
+ ip_keys = [k for k in self.config if k.startswith('Mcu.IP')]
+ for k in ip_keys:
+ if self.config[k] == 'FREERTOS':
+ return True
+ for k in self.config:
+ if k.startswith('FREERTOS.'):
+ return True
+ return False
+
+ # 可扩展:添加更多参数获取方法
+ def get_parameter(self, key, default=None):
+ return self.config.get(key, default)
+
+ def get_all_with_prefix(self, prefix):
+ return {k: v for k, v in self.config.items() if k.startswith(prefix)}
+
class DataInterface(BaseInterface):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("dataInterface")
- self.stacked_layout = QStackedLayout()
+
+ # 属性初始化
+ self.project_path = ""
+ self.project_name = ""
+ self.ioc_file = ""
+ self.freertos_enabled = False # 新增属性
+
+ # 主布局
+ self.stacked_layout = QStackedLayout(self)
self.setLayout(self.stacked_layout)
# --- 页面1:工程路径选择 ---
self.select_widget = QWidget()
- select_layout = QVBoxLayout(self.select_widget)
- select_layout.addSpacing(40)
- select_layout.addWidget(SubtitleLabel("MRobot 代码生成"))
- select_layout.addWidget(HorizontalSeparator())
- select_layout.addSpacing(10)
- select_layout.addWidget(BodyLabel("请选择包含 .ioc 文件的工程文件夹,点击下方按钮进行选择。"))
- select_layout.addSpacing(20)
- self.choose_btn = PushButton("选择工程路径")
+ outer_layout = QVBoxLayout(self.select_widget)
+ outer_layout.setContentsMargins(0, 0, 0, 0)
+ outer_layout.addStretch()
+
+ # 直接用布局和控件,无卡片
+ content_layout = QVBoxLayout()
+ content_layout.setSpacing(28)
+ content_layout.setContentsMargins(48, 48, 48, 48)
+
+ # 主标题
+ title = TitleLabel("MRobot 代码生成")
+ title.setAlignment(Qt.AlignCenter)
+ title.setStyleSheet("font-size: 36px; font-weight: bold; color: #2d7d9a;")
+ content_layout.addWidget(title)
+
+ # 副标题
+ subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径(.ico所在的目录),然后开启代码之旅!")
+ subtitle.setAlignment(Qt.AlignCenter)
+ subtitle.setStyleSheet("font-size: 16px; color: #4a6fa5;")
+ content_layout.addWidget(subtitle)
+
+ # 简要说明
+ desc = BodyLabel("支持自动配置和生成任务,自主选择模块代码倒入,自动识别cubemx配置!")
+ desc.setAlignment(Qt.AlignCenter)
+ desc.setStyleSheet("font-size: 14px; color: #6b7b8c;")
+ content_layout.addWidget(desc)
+
+ content_layout.addSpacing(18)
+
+ # 选择项目路径按钮
+ self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径")
+ self.choose_btn.setFixedWidth(200)
+ self.choose_btn.setStyleSheet("font-size: 17px;")
self.choose_btn.clicked.connect(self.choose_project_folder)
- select_layout.addWidget(self.choose_btn)
- select_layout.addStretch()
+ content_layout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
+
+ # 更新代码库按钮
+ self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
+ self.update_template_btn.setFixedWidth(200)
+ self.update_template_btn.setStyleSheet("font-size: 17px;")
+ self.update_template_btn.clicked.connect(self.update_user_template)
+ content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
+
+ content_layout.addSpacing(10)
+ content_layout.addStretch()
+
+
+ outer_layout.addLayout(content_layout)
+ outer_layout.addStretch()
+
self.stacked_layout.addWidget(self.select_widget)
- # --- 页面2:代码配置 ---
- self.config_widget = QWidget()
- self.config_layout = QVBoxLayout(self.config_widget)
- # 左上角小返回按钮
- top_bar = QHBoxLayout()
- self.back_btn = PushButton('返回', icon=FluentIcon.SKIP_BACK)
- # self.back_btn.setFixedSize(32, 32)
- self.back_btn.clicked.connect(self.back_to_select)
- self.back_btn.setToolTip("返回")
- top_bar.addWidget(self.back_btn, alignment=Qt.AlignmentFlag.AlignLeft)
- top_bar.addStretch()
- self.config_layout.addLayout(top_bar)
- self.config_layout.addWidget(SubtitleLabel("工程配置信息"))
- self.config_layout.addWidget(HorizontalSeparator())
- self.project_info_labels = []
- self.config_layout.addStretch()
- self.stacked_layout.addWidget(self.config_widget)
- # 默认显示选择页面
- self.stacked_layout.setCurrentWidget(self.select_widget)
+ # --- 页面2:主配置页面 ---
+ self.config_widget = QWidget()
+ main_layout = QVBoxLayout(self.config_widget)
+ main_layout.setContentsMargins(32, 32, 32, 32)
+ main_layout.setSpacing(18)
+
+ # 顶部项目信息
+ info_layout = QHBoxLayout()
+ self.back_btn = PushButton(FluentIcon.SKIP_BACK, "返回")
+ self.back_btn.setFixedWidth(90)
+ self.back_btn.clicked.connect(self.back_to_select)
+ info_layout.addWidget(self.back_btn) # 返回按钮放最左
+ self.project_name_label = StrongBodyLabel()
+ self.project_path_label = BodyLabel()
+ self.ioc_file_label = BodyLabel()
+ self.freertos_label = BodyLabel()
+ info_layout.addWidget(self.project_name_label)
+ info_layout.addWidget(self.project_path_label)
+ info_layout.addWidget(self.ioc_file_label)
+ info_layout.addWidget(self.freertos_label)
+ info_layout.addStretch()
+ main_layout.addLayout(info_layout)
+ main_layout.addWidget(HorizontalSeparator())
+
+ # ======= 新增:左右分栏 =======
+ content_hbox = QHBoxLayout()
+ content_hbox.setSpacing(24)
+
+ # 左侧:文件树
+ left_vbox = QVBoxLayout()
+ left_vbox.addWidget(SubtitleLabel("用户代码模块选择"))
+ left_vbox.addWidget(HorizontalSeparator())
+ self.file_tree = TreeWidget()
+ self.file_tree.setHeaderLabels(["模块名"])
+ self.file_tree.setSelectionMode(self.file_tree.ExtendedSelection)
+ self.file_tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
+ self.file_tree.setCheckedColor("#0078d4", "#2d7d9a")
+ self.file_tree.setBorderRadius(8)
+ self.file_tree.setBorderVisible(True)
+ left_vbox.addWidget(self.file_tree, stretch=1)
+ content_hbox.addLayout(left_vbox, 2)
+
+ # 右侧:操作按钮和说明
+ right_vbox = QVBoxLayout()
+ right_vbox.setSpacing(18)
+ right_vbox.addWidget(SubtitleLabel("操作区"))
+ right_vbox.addWidget(HorizontalSeparator())
+
+ # 操作按钮分组
+ btn_group = QVBoxLayout()
+ # 自动环境配置按钮
+ self.env_btn = PushButton("自动环境配置")
+ self.env_btn.setFixedWidth(200)
+ self.env_btn.setToolTip("自动检测并配置常用开发环境(功能开发中)")
+ self.env_btn.clicked.connect(self.auto_env_config)
+ btn_group.addWidget(self.env_btn)
+ # FreeRTOS相关按钮
+ self.freertos_task_btn = PushButton("自动生成FreeRTOS任务")
+ self.freertos_task_btn.setFixedWidth(200)
+ self.freertos_task_btn.setToolTip("自动在 freertos.c 中插入任务创建代码")
+ self.freertos_task_btn.clicked.connect(self.on_freertos_task_btn_clicked)
+ btn_group.addWidget(self.freertos_task_btn)
+ self.task_code_btn = PushButton("配置并生成任务代码")
+ self.task_code_btn.setFixedWidth(200)
+ self.task_code_btn.setToolTip("配置任务参数并一键生成任务代码文件")
+ self.task_code_btn.clicked.connect(self.on_task_code_btn_clicked)
+ btn_group.addWidget(self.task_code_btn)
+ self.generate_btn = PushButton(FluentIcon.CODE, "生成代码")
+ self.generate_btn.setFixedWidth(200)
+ self.generate_btn.setToolTip("将选中的用户模块代码复制到工程 User 目录")
+ self.generate_btn.clicked.connect(self.generate_code)
+ btn_group.addWidget(self.generate_btn)
+ btn_group.addSpacing(10)
+ right_vbox.addLayout(btn_group)
+ right_vbox.addStretch()
+
+ content_hbox.addLayout(right_vbox, 1)
+ main_layout.addLayout(content_hbox, stretch=1)
+ self.stacked_layout.addWidget(self.config_widget)
+ self.file_tree.itemChanged.connect(self.on_tree_item_changed)
+
+ def auto_env_config(self):
+ InfoBar.info(
+ title="敬请期待",
+ content="自动环境配置功能暂未实现,等待后续更新。",
+ parent=self,
+ duration=2000
+ )
def choose_project_folder(self):
folder = QFileDialog.getExistingDirectory(self, "请选择代码项目文件夹")
@@ -128,7 +382,12 @@ class DataInterface(BaseInterface):
return
ioc_files = [f for f in os.listdir(folder) if f.endswith('.ioc')]
if not ioc_files:
- QMessageBox.warning(self, "提示", "未找到.ioc文件,请确认项目文件夹。")
+ InfoBar.warning(
+ title="提示",
+ content="未找到.ioc文件,请确认项目文件夹。",
+ parent=self,
+ duration=2000
+ )
return
self.project_path = folder
self.project_name = os.path.basename(folder)
@@ -136,24 +395,622 @@ class DataInterface(BaseInterface):
self.show_config_page()
def show_config_page(self):
- # 清理旧内容
- for label in self.project_info_labels:
- self.config_layout.removeWidget(label)
- label.deleteLater()
- self.project_info_labels.clear()
- # 显示项目信息
- l1 = BodyLabel(f"项目名称: {self.project_name}")
- l2 = BodyLabel(f"项目路径: {self.project_path}")
- l3 = BodyLabel(f"IOC 文件: {self.ioc_file}")
- self.config_layout.insertWidget(2, l1)
- self.config_layout.insertWidget(3, l2)
- self.config_layout.insertWidget(4, l3)
- self.project_info_labels.extend([l1, l2, l3])
+ # 更新项目信息
+ self.project_name_label.setText(f"项目名称: {self.project_name}")
+ self.project_path_label.setText(f"项目路径: {self.project_path}")
+ # self.ioc_file_label.setText(f"IOC 文件: {self.ioc_file}")
+ try:
+ ioc = IocConfig(self.ioc_file)
+ self.freertos_enabled = ioc.is_freertos_enabled() # 记录状态
+ freertos_status = "已启用" if self.freertos_enabled else "未启用"
+ self.freertos_label.setText(f"FreeRTOS: {freertos_status}")
+ # self.freertos_task_btn.setEnabled(self.freertos_enabled)
+ except Exception as e:
+ self.freertos_label.setText(f"IOC解析失败: {e}")
+ self.freertos_task_btn.hide()
+ self.freertos_enabled = False
+ self.show_user_code_files()
self.stacked_layout.setCurrentWidget(self.config_widget)
+ def on_freertos_task_btn_clicked(self):
+ if not self.freertos_enabled:
+ InfoBar.warning(
+ title="未开启 FreeRTOS",
+ content="请先在 CubeMX 中开启 FreeRTOS!",
+ parent=self,
+ duration=2000
+ )
+ return
+ self.generate_freertos_task()
+
+ def on_task_code_btn_clicked(self):
+ if not self.freertos_enabled:
+ InfoBar.warning(
+ title="未开启 FreeRTOS",
+ content="请先在 CubeMX 中开启 FreeRTOS!",
+ parent=self,
+ duration=2000
+ )
+ return
+ self.open_task_config_dialog()
+
def back_to_select(self):
self.stacked_layout.setCurrentWidget(self.select_widget)
+ def update_user_template(self):
+ url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
+ local_dir = "User_code"
+ try:
+ resp = requests.get(url, timeout=30)
+ resp.raise_for_status()
+ z = zipfile.ZipFile(io.BytesIO(resp.content))
+ if os.path.exists(local_dir):
+ shutil.rmtree(local_dir)
+ for member in z.namelist():
+ rel_path = os.path.relpath(member, z.namelist()[0])
+ if rel_path == ".":
+ continue
+ target_path = os.path.join(local_dir, rel_path)
+ if member.endswith('/'):
+ os.makedirs(target_path, exist_ok=True)
+ else:
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
+ with open(target_path, "wb") as f:
+ f.write(z.read(member))
+ InfoBar.success(
+ title="更新成功",
+ content="用户模板已更新到最新版本!",
+ parent=self,
+ duration=2000
+ )
+ except Exception as e:
+ InfoBar.error(
+ title="更新失败",
+ content=f"用户模板更新失败: {e}",
+ parent=self,
+ duration=3000
+ )
+
+ def show_user_code_files(self):
+ self.file_tree.clear()
+ base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "User_code")
+ user_dir = os.path.join(self.project_path, "User")
+ sub_dirs = ["bsp", "component", "device", "module"]
+
+ # 读取所有 describe.csv 和 dependencies.csv
+ describe_map = {}
+ dependencies_map = {}
+ for sub in sub_dirs:
+ dir_path = os.path.join(base_dir, sub)
+ if not os.path.isdir(dir_path):
+ continue
+ # describe
+ desc_path = os.path.join(dir_path, "describe.csv")
+ if os.path.exists(desc_path):
+ with open(desc_path, encoding="utf-8") as f:
+ for line in f:
+ if "," in line:
+ k, v = line.strip().split(",", 1)
+ describe_map[f"{sub}/{k.strip()}"] = v.strip()
+ # dependencies
+ dep_path = os.path.join(dir_path, "dependencies.csv")
+ if os.path.exists(dep_path):
+ with open(dep_path, encoding="utf-8") as f:
+ for line in f:
+ if "," in line:
+ a, b = line.strip().split(",", 1)
+ dependencies_map.setdefault(f"{sub}/{a.strip()}", []).append(b.strip())
+
+ self._describe_map = describe_map
+ self._dependencies_map = dependencies_map
+
+ self.file_tree.setHeaderLabels(["模块名", "描述"])
+ self.file_tree.setSelectionMode(self.file_tree.ExtendedSelection)
+ self.file_tree.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
+ self.file_tree.header().setSectionResizeMode(1, QHeaderView.Stretch) # 描述列自适应
+ self.file_tree.setCheckedColor("#0078d4", "#2d7d9a")
+ self.file_tree.setBorderRadius(8)
+ self.file_tree.setBorderVisible(True)
+
+ for sub in sub_dirs:
+ dir_path = os.path.join(base_dir, sub)
+ if not os.path.isdir(dir_path):
+ continue
+ group_item = TreeItem([sub, ""])
+ self.file_tree.addTopLevelItem(group_item)
+ has_file = False
+ for root, _, files in os.walk(dir_path):
+ rel_root = os.path.relpath(root, base_dir)
+ for f in sorted(files):
+ if f.endswith(".c"):
+ mod_name = os.path.splitext(f)[0]
+ rel_c = os.path.join(rel_root, f)
+ key = f"{rel_root}/{mod_name}".replace("\\", "/")
+ desc = describe_map.get(key, "")
+ file_item = TreeItem([mod_name, desc])
+ file_item.setFlags(file_item.flags() | Qt.ItemIsUserCheckable)
+ file_item.setData(0, Qt.UserRole, rel_c)
+ file_item.setData(0, Qt.UserRole + 1, key) # 存模块key
+ file_item.setToolTip(1, desc)
+ file_item.setTextAlignment(1, Qt.AlignLeft | Qt.AlignVCenter)
+ group_item.addChild(file_item)
+ dst_c = os.path.join(user_dir, rel_c)
+ if os.path.exists(dst_c):
+ file_item.setCheckState(0, Qt.Unchecked)
+ file_item.setText(0, f"{mod_name}(已存在)")
+ file_item.setForeground(0, Qt.gray)
+ else:
+ file_item.setCheckState(0, Qt.Unchecked)
+ group_item.addChild(file_item)
+ has_file = True
+ if not has_file:
+ empty_item = TreeItem(["(无 .c 文件)", ""])
+ group_item.addChild(empty_item)
+ self.file_tree.expandAll()
+
+ # 勾选依赖自动勾选
+ def on_tree_item_changed(self, item, column):
+ if column != 0:
+ return
+ if item.childCount() > 0:
+ return # 只处理叶子
+ if item.checkState(0) == Qt.Checked:
+ key = item.data(0, Qt.UserRole + 1)
+ deps = self._dependencies_map.get(key, [])
+ if deps:
+ checked = []
+ root = self.file_tree.invisibleRootItem()
+ for i in range(root.childCount()):
+ group = root.child(i)
+ for j in range(group.childCount()):
+ child = group.child(j)
+ ckey = child.data(0, Qt.UserRole + 1)
+ if ckey in deps and child.checkState(0) != Qt.Checked:
+ child.setCheckState(0, Qt.Checked)
+ checked.append(ckey)
+ if checked:
+ descs = [self._describe_map.get(dep, dep) for dep in checked]
+ InfoBar.info(
+ title="依赖自动勾选",
+ content="已自动勾选依赖模块: " + ",".join(descs),
+ parent=self,
+ duration=2000
+ )
+
+
+ def get_checked_files(self):
+ files = []
+ def _traverse(item):
+ for i in range(item.childCount()):
+ child = item.child(i)
+ if child.childCount() == 0 and child.checkState(0) == Qt.Checked:
+ files.append(child.data(0, Qt.UserRole))
+ _traverse(child)
+ root = self.file_tree.invisibleRootItem()
+ for i in range(root.childCount()):
+ _traverse(root.child(i))
+ return files
+
+ def generate_code(self):
+ import shutil
+ base_dir = "User_code"
+ user_dir = os.path.join(self.project_path, "User")
+ copied = []
+ files = self.get_checked_files()
+ skipped = []
+ for rel_c in files:
+ rel_h = rel_c[:-2] + ".h"
+ src_c = os.path.join(base_dir, rel_c)
+ src_h = os.path.join(base_dir, rel_h)
+ dst_c = os.path.join(user_dir, rel_c)
+ dst_h = os.path.join(user_dir, rel_h)
+ # 如果目标文件已存在则跳过
+ if os.path.exists(dst_c):
+ skipped.append(dst_c)
+ else:
+ os.makedirs(os.path.dirname(dst_c), exist_ok=True)
+ shutil.copy2(src_c, dst_c)
+ copied.append(dst_c)
+ if os.path.exists(src_h):
+ if os.path.exists(dst_h):
+ skipped.append(dst_h)
+ else:
+ os.makedirs(os.path.dirname(dst_h), exist_ok=True)
+ shutil.copy2(src_h, dst_h)
+ copied.append(dst_h)
+ msg = f"已拷贝 {len(copied)} 个文件到 User 目录"
+ if skipped:
+ msg += f"\n{len(skipped)} 个文件已存在,未覆盖"
+ InfoBar.success(
+ title="生成完成",
+ content=msg,
+ parent=self,
+ duration=2000
+ )
+ # 生成后刷新文件树,更新标记
+ self.show_user_code_files()
+
+ def generate_freertos_task(self):
+ import re
+ freertos_path = os.path.join(self.project_path, "Core", "Src", "freertos.c")
+ if not os.path.exists(freertos_path):
+ InfoBar.error(
+ title="未找到 freertos.c",
+ content="未找到 Core/Src/freertos.c 文件,请确认工程路径。",
+ parent=self,
+ duration=2500
+ )
+ return
+ with open(freertos_path, "r", encoding="utf-8") as f:
+ code = f.read()
+
+ changed = False
+ error_msgs = []
+
+ # 1. 添加 #include "task/user_task.h"
+ include_line = '#include "task/user_task.h"'
+ if include_line not in code:
+ # 只插入到 USER CODE BEGIN Includes 区域
+ include_pattern = r'(\/\* *USER CODE BEGIN Includes *\*\/\s*)'
+ if re.search(include_pattern, code):
+ code = re.sub(
+ include_pattern,
+ r'\1' + include_line + '\n',
+ code
+ )
+ changed = True
+ else:
+ error_msgs.append("未找到 /* USER CODE BEGIN Includes */ 区域,无法插入 include。")
+
+ # 2. 在 /* USER CODE BEGIN RTOS_THREADS */ 区域添加 osThreadNew(Task_Init, NULL, &attr_init);
+ rtos_threads_pattern = r'(\/\* *USER CODE BEGIN RTOS_THREADS *\*\/\s*)(.*?)(\/\* *USER CODE END RTOS_THREADS *\*\/)'
+ match = re.search(rtos_threads_pattern, code, re.DOTALL)
+ task_line = ' initTaskHandle = osThreadNew(Task_Init, NULL, &attr_init); // 创建初始化任务\n'
+ if match:
+ threads_code = match.group(2)
+ if 'Task_Init' not in threads_code:
+ # 保留原有内容,追加新行
+ new_threads_code = match.group(1) + threads_code + task_line + match.group(3)
+ code = code[:match.start()] + new_threads_code + code[match.end():]
+ changed = True
+ else:
+ error_msgs.append("未找到 /* USER CODE BEGIN RTOS_THREADS */ 区域,无法插入任务创建代码。")
+
+ # 3. 清空 StartDefaultTask 的 USER CODE 区域,只保留 osThreadTerminate
+ sdt_pattern = r'(\/\* *USER CODE BEGIN StartDefaultTask *\*\/\s*)(.*?)(\/\* *USER CODE END StartDefaultTask *\*\/)'
+ match = re.search(sdt_pattern, code, re.DOTALL)
+ if match:
+ if 'osThreadTerminate(osThreadGetId());' not in match.group(2):
+ new_sdt_code = match.group(1) + ' osThreadTerminate(osThreadGetId());\n' + match.group(3)
+ code = code[:match.start()] + new_sdt_code + code[match.end():]
+ changed = True
+ else:
+ error_msgs.append("未找到 /* USER CODE BEGIN StartDefaultTask */ 区域,无法插入终止代码。")
+
+ if changed:
+ with open(freertos_path, "w", encoding="utf-8") as f:
+ f.write(code)
+ InfoBar.success(
+ title="生成成功",
+ content="FreeRTOS任务代码已自动生成!",
+ parent=self,
+ duration=2000
+ )
+ elif error_msgs:
+ InfoBar.error(
+ title="生成失败",
+ content="\n".join(error_msgs),
+ parent=self,
+ duration=3000
+ )
+ else:
+ InfoBar.info(
+ title="无需修改",
+ content="FreeRTOS任务相关代码已存在,无需重复生成。",
+ parent=self,
+ duration=2000
+ )
+
+ def open_task_config_dialog(self):
+ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView
+ import yaml
+ import os
+
+ class TaskConfigDialog(QDialog):
+ def __init__(self, parent=None, config_path=None):
+ super().__init__(parent)
+ self.setWindowTitle("任务配置")
+ self.resize(800, 420)
+ layout = QVBoxLayout(self)
+ self.table = QTableWidget(0, 5)
+ self.table.setHorizontalHeaderLabels(["任务名称", "运行频率", "初始化延迟", "堆栈大小", "任务描述"])
+ self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
+ self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
+ self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
+ self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
+ self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
+ self.table.setColumnWidth(4, 320) # 任务描述更宽
+ layout.addWidget(self.table)
+ btn_layout = QHBoxLayout()
+ add_btn = QPushButton("添加任务")
+ del_btn = QPushButton("删除选中")
+ ok_btn = QPushButton("生成")
+ cancel_btn = QPushButton("取消")
+ btn_layout.addWidget(add_btn)
+ btn_layout.addWidget(del_btn)
+ btn_layout.addStretch()
+ btn_layout.addWidget(ok_btn)
+ btn_layout.addWidget(cancel_btn)
+ layout.addLayout(btn_layout)
+ add_btn.clicked.connect(self.add_row)
+ del_btn.clicked.connect(self.del_row)
+ ok_btn.clicked.connect(self.accept)
+ cancel_btn.clicked.connect(self.reject)
+
+
+ # 自动读取配置文件
+ if config_path and os.path.exists(config_path):
+ try:
+ with open(config_path, "r", encoding="utf-8") as f:
+ tasks = yaml.safe_load(f)
+ if tasks:
+ for t in tasks:
+ row = self.table.rowCount()
+ self.table.insertRow(row)
+ for col, key in enumerate(["name", "frequency", "delay", "stack", "description"]):
+ item = QTableWidgetItem(str(t.get(key, "")))
+ item.setTextAlignment(Qt.AlignCenter)
+ self.table.setItem(row, col, item)
+ except Exception as e:
+ pass # 配置文件损坏时忽略
+
+ def add_row(self):
+ row = self.table.rowCount()
+ self.table.insertRow(row)
+ default_values = [
+ f"Task{row+1}", "500", "0", "256", "不要偷懒,请写清楚每个任务的作用!(如果你看到任务上面是这句话,说明作者是个懒蛋)"
+ ]
+ for col, val in enumerate(default_values):
+ item = QTableWidgetItem(val)
+ item.setTextAlignment(Qt.AlignCenter)
+ self.table.setItem(row, col, item)
+
+ def del_row(self):
+ rows = set([i.row() for i in self.table.selectedItems()])
+ for r in sorted(rows, reverse=True):
+ self.table.removeRow(r)
+
+ def get_tasks(self):
+ tasks = []
+ for row in range(self.table.rowCount()):
+ name = self.table.item(row, 0).text().strip()
+ freq = int(self.table.item(row, 1).text())
+ delay = int(self.table.item(row, 2).text())
+ stack = int(self.table.item(row, 3).text())
+ desc = self.table.item(row, 4).text().strip()
+ # 校验 stack 必须为 128*2^n
+ if stack < 128 or (stack & (stack - 1)) != 0 or stack % 128 != 0:
+ raise ValueError(f"第{row+1}行任务“{name}”的堆栈大小必须为128、256、512、1024等(128*2^n)")
+ tasks.append({
+ "name": name,
+ "function": f"Task_{name}",
+ "frequency": freq,
+ "delay": delay,
+ "stack": stack,
+ "description": desc
+ })
+ return tasks
+
+ config_path = os.path.join(self.project_path, "User", "task", "config.yaml")
+ dlg = TaskConfigDialog(self, config_path=config_path)
+ if dlg.exec() == QDialog.Accepted:
+ try:
+ tasks = dlg.get_tasks()
+ except Exception as e:
+ InfoBar.error(
+ title="参数错误",
+ content=str(e),
+ parent=self,
+ duration=3000
+ )
+ return
+ if not tasks:
+ InfoBar.warning(
+ title="未配置任务",
+ content="请至少添加一个任务!",
+ parent=self,
+ duration=2000
+ )
+ return
+ try:
+ self.generate_task_code(tasks)
+ InfoBar.success(
+ title="生成成功",
+ content="任务代码已生成到 User/task 目录!",
+ parent=self,
+ duration=2000
+ )
+ except Exception as e:
+ InfoBar.error(
+ title="生成失败",
+ content=f"任务代码生成失败: {e}",
+ parent=self,
+ duration=3000
+ )
+
+ def generate_task_code(self, task_list):
+ import os
+ from jinja2 import Template
+ import yaml
+ import re
+ import textwrap
+
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ template_dir = os.path.join(base_dir, "User_code", "task")
+ output_dir = os.path.join(self.project_path, "User", "task")
+ os.makedirs(output_dir, exist_ok=True)
+
+ # 模板路径
+ user_task_h_tpl = os.path.join(template_dir, "user_task.h.template")
+ user_task_c_tpl = os.path.join(template_dir, "user_task.c.template")
+ init_c_tpl = os.path.join(template_dir, "init.c.template")
+ task_c_tpl = os.path.join(template_dir, "task.c.template")
+
+ def render_template(path, context):
+ with open(path, encoding="utf-8") as f:
+ tpl = Template(f.read())
+ return tpl.render(**context)
+
+
+ # 构造模板上下文
+ context_h = {
+ "thread_definitions": "\n".join([f" osThreadId_t {t['name']};" for t in task_list]),
+ "freq_definitions": "\n".join([f" float {t['name']};" for t in task_list]),
+ "stack_definitions": "\n".join([f" UBaseType_t {t['name']};" for t in task_list]),
+ "last_up_time_definitions": "\n".join([f" float {t['name']};" for t in task_list]),
+ "task_frequency_definitions": "\n".join([f"#define {t['name'].upper()}_FREQ ({t['frequency']})" for t in task_list]),
+ "task_init_delay_definitions": "\n".join([f"#define {t['name'].upper()}_INIT_DELAY ({t['delay']})" for t in task_list]),
+ "task_attr_declarations": "\n".join([f"extern const osThreadAttr_t attr_{t['name']};" for t in task_list]),
+ "task_function_declarations": "\n".join([f"void {t['function']}(void *argument);" for t in task_list]),
+ }
+
+ # ----------- 用户区域保护函数 -----------
+ def preserve_user_region(new_code, old_code, region_name):
+ """
+ 替换 new_code 中 region_name 区域为 old_code 中的内容(如果有)
+ region_name: 如 'USER INCLUDE'
+ """
+ pattern = re.compile(
+ rf"/\*\s*{region_name}\s*BEGIN\s*\*/(.*?)/\*\s*{region_name}\s*END\s*\*/",
+ re.DOTALL
+ )
+ old_match = pattern.search(old_code or "")
+ if not old_match:
+ return new_code # 旧文件没有该区域,直接返回新代码
+
+ old_content = old_match.group(1)
+ def repl(m):
+ return m.group(0).replace(m.group(1), old_content)
+ # 替换新代码中的该区域
+ return pattern.sub(repl, new_code, count=1)
+
+ # ----------- 生成 user_task.h -----------
+ user_task_h_path = os.path.join(output_dir, "user_task.h")
+ new_user_task_h = render_template(user_task_h_tpl, context_h)
+
+ # 检查并保留所有用户区域
+ if os.path.exists(user_task_h_path):
+ with open(user_task_h_path, "r", encoding="utf-8") as f:
+ old_code = f.read()
+ # 只保留有内容的用户区域
+ for region in ["USER INCLUDE", "USER MESSAGE", "USER CONFIG"]:
+ # 如果旧文件该区域有内容,则保留
+ pattern = re.compile(
+ rf"/\*\s*{region}\s*BEGIN\s*\*/(.*?)/\*\s*{region}\s*END\s*\*/",
+ re.DOTALL
+ )
+ old_match = pattern.search(old_code)
+ if old_match and old_match.group(1).strip():
+ new_user_task_h = preserve_user_region(new_user_task_h, old_code, region)
+ # 写入
+ with open(user_task_h_path, "w", encoding="utf-8") as f:
+ f.write(new_user_task_h)
+
+ # ----------- 生成 user_task.c -----------
+ context_c = {
+ "task_attr_definitions": "\n".join([
+ f"const osThreadAttr_t attr_{t['name']} = {{\n"
+ f" .name = \"{t['name']}\",\n"
+ f" .priority = osPriorityNormal,\n"
+ f" .stack_size = {t['stack']} * 4,\n"
+ f"}};"
+ for t in task_list
+ ])
+ }
+ user_task_c = render_template(user_task_c_tpl, context_c)
+ with open(os.path.join(output_dir, "user_task.c"), "w", encoding="utf-8") as f:
+ f.write(user_task_c)
+
+ # ----------- 生成 init.c -----------
+ # 线程创建代码
+ thread_creation_code = "\n".join([
+ f" task_runtime.thread.{t['name']} = osThreadNew({t['function']}, NULL, &attr_{t['name']});"
+ for t in task_list
+ ])
+
+ context_init = {
+ "thread_creation_code": thread_creation_code,
+ }
+ # 渲染模板
+ init_c = render_template(init_c_tpl, context_init)
+
+ # 保留 USER MESSAGE 区域
+ def preserve_user_region(new_code, old_code, region_name):
+ pattern = re.compile(
+ rf"/\*\s*{region_name}\s*BEGIN\s*\*/(.*?)/\*\s*{region_name}\s*END\s*\*/",
+ re.DOTALL
+ )
+ old_match = pattern.search(old_code or "")
+ if not old_match:
+ return new_code
+ old_content = old_match.group(1)
+ def repl(m):
+ return m.group(0).replace(m.group(1), old_content)
+ return pattern.sub(repl, new_code, count=1)
+
+ init_c_path = os.path.join(output_dir, "init.c")
+ if os.path.exists(init_c_path):
+ with open(init_c_path, "r", encoding="utf-8") as f:
+ old_code = f.read()
+ # 保留 USER MESSAGE 区域
+ init_c = preserve_user_region(init_c, old_code, "USER MESSAGE")
+
+ with open(init_c_path, "w", encoding="utf-8") as f:
+ f.write(init_c)
+
+ # ----------- 生成 task.c -----------
+ task_c_tpl = os.path.join(template_dir, "task.c.template")
+ for t in task_list:
+ # 自动换行任务描述
+ desc = t.get("description", "")
+ desc_wrapped = "\n ".join(textwrap.wrap(desc, 20))
+ context_task = {
+ "task_name": t["name"],
+ "task_function": t["function"],
+ "task_frequency": f"{t['name'].upper()}_FREQ", # 使用宏定义
+ "task_delay": f"{t['name'].upper()}_INIT_DELAY", # 使用宏定义
+ "task_description": desc_wrapped
+ }
+ # 渲染模板
+ with open(task_c_tpl, encoding="utf-8") as f:
+ tpl = Template(f.read())
+ code = tpl.render(**context_task)
+ # 保留USER区域
+ task_c_path = os.path.join(output_dir, f"{t['name']}.c")
+ if os.path.exists(task_c_path):
+ with open(task_c_path, "r", encoding="utf-8") as f:
+ old_code = f.read()
+ # 只保留USER区域
+ def preserve_user_region(new_code, old_code, region_name):
+ pattern = re.compile(
+ rf"/\*\s*{region_name}\s*BEGIN\s*\*/(.*?)/\*\s*{region_name}\s*END\s*\*/",
+ re.DOTALL
+ )
+ old_match = pattern.search(old_code or "")
+ if not old_match:
+ return new_code
+ old_content = old_match.group(1)
+ def repl(m):
+ return m.group(0).replace(m.group(1), old_content)
+ return pattern.sub(repl, new_code, count=1)
+ for region in ["USER INCLUDE", "USER STRUCT", "USER CODE"]:
+ code = preserve_user_region(code, old_code, region)
+ with open(task_c_path, "w", encoding="utf-8") as f:
+ f.write(code)
+ # ----------- 保存任务配置到 config.yaml -----------
+ config_path = os.path.join(output_dir, "config.yaml")
+ with open(config_path, "w", encoding="utf-8") as f:
+ yaml.dump(task_list, f, allow_unicode=True)
# ===================== 串口终端界面 =====================
class SerialReadThread(QThread):
@@ -213,12 +1070,12 @@ class SerialTerminalInterface(BaseInterface):
#快捷指令居中
preset_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.preset_commands = [
- ("线程监视器", "RESET"),
- ("陀螺仪校准", "GET_VERSION"),
- ("性能监视", "START"),
- ("重启", "STOP"),
- ("显示所有设备", "SELF_TEST"),
- ("查询id", "STATUS"),
+ ("线程监视器", "htop"),
+ ("陀螺仪校准", "cali_gyro"),
+ ("性能监视", "htop"),
+ ("重启", "reset"),
+ ("显示所有设备", "ls /dev"),
+ ("查询id", "id"),
]
for label, cmd in self.preset_commands:
btn = PushButton(label)
@@ -247,7 +1104,7 @@ class SerialTerminalInterface(BaseInterface):
send_btn = PushButton("发送")
send_btn.clicked.connect(self.send_data)
bottom_hbox.addWidget(send_btn, 1)
- self.auto_enter_checkbox = CheckBox("自动回车")
+ self.auto_enter_checkbox = CheckBox("自动回车 ")
self.auto_enter_checkbox.setChecked(True)
bottom_hbox.addWidget(self.auto_enter_checkbox)
bottom_hbox.addStretch()
@@ -315,6 +1172,7 @@ class SerialTerminalInterface(BaseInterface):
except Exception as e:
self.text_edit.append(f"发送失败: {e}")
self.input_line.clear()
+
# ===================== 零件库页面 =====================
class DownloadThread(QThread):
progressChanged = pyqtSignal(int)
@@ -335,7 +1193,10 @@ class DownloadThread(QThread):
retry = 0
while retry < max_retry:
try:
- url = f"{self.server_url}/download/{rel_path}"
+ # 先统一分隔符,再编码
+ rel_path_unix = rel_path.replace("\\", "/")
+ encoded_path = quote(rel_path_unix)
+ url = f"{self.server_url}/download/{encoded_path}"
params = {"key": self.secret_key}
resp = requests.get(url, params=params, stream=True, timeout=10)
if resp.status_code == 200:
@@ -368,7 +1229,7 @@ class PartLibraryInterface(BaseInterface):
layout.addWidget(SubtitleLabel("零件库(在线bate版)"))
layout.addWidget(HorizontalSeparator())
- layout.addWidget(BodyLabel("可浏览服务器零件库,选择需要的文件下载到本地。(如无法使用或者下载失败,请尝试重新下载或检查网络连接)"))
+ layout.addWidget(BodyLabel("感谢重庆邮电大学整理的零件库,选择需要的文件下载到本地。(如无法使用或者下载失败,请尝试重新下载或检查网络连接)"))
btn_layout = QHBoxLayout()
refresh_btn = PushButton(FluentIcon.SYNC, "刷新列表")
@@ -399,6 +1260,8 @@ class PartLibraryInterface(BaseInterface):
self.refresh_list(first=True)
+
+
def refresh_list(self, first=False):
self.tree.clear()
try:
@@ -584,12 +1447,138 @@ class SettingInterface(BaseInterface):
self.themeSwitchRequested.emit()
# ===================== 帮助与关于界面 =====================
+
+# ...existing code...
+
+
+# 注意:PushSettingCard、HyperlinkCard 已由你的 SettingCard 文件定义
+
+# ...existing code...
+
+from PyQt5.QtWidgets import QScrollArea, QWidget, QVBoxLayout, QMessageBox
+from qfluentwidgets import ScrollArea, VBoxLayout
+from qfluentwidgets import VBoxLayout
class HelpInterface(BaseInterface):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("helpInterface")
+
layout = QVBoxLayout()
- self.setLayout(layout)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ self.setLayout(layout)
+
+ content_widget = QWidget()
+ main_layout = VBoxLayout(content_widget)
+ main_layout.setContentsMargins(32, 32, 32, 32)
+ main_layout.setSpacing(18)
+ # 标题
+ main_layout.addWidget(SubtitleLabel("帮助中心"))
+ main_layout.addWidget(HorizontalSeparator())
+
+ # 版本与更新
+ version_card = PushSettingCard(
+ "检查更新",
+ FluentIcon.INFO,
+ f"当前版本:MRobot Toolbox v{__version__}",
+ "点击按钮检查是否有新版本。",
+ parent=self
+ )
+ version_card.clicked.connect(self.check_update)
+ main_layout.addWidget(version_card)
+
+ # FAQ分组
+ faq_group = SettingCardGroup("常见问题", self)
+ faq_card1 = PushSettingCard(
+ "查看解决方法",
+ FluentIcon.HELP,
+ "启动报错/界面异常怎么办?",
+ "遇到启动问题请尝试重启、检查依赖,或加入交流群获取帮助。",
+ parent=self
+ )
+ faq_card1.clicked.connect(lambda: self.show_info(
+ "启动报错/界面异常解决方法",
+ "1. 尝试重启软件。\n2. 检查Python和依赖库版本。\n3. 如仍有问题,请在GitHub提交Issue。"
+ ))
+ faq_group.addSettingCard(faq_card1)
+
+ faq_card2 = PushSettingCard(
+ "查看解决方法",
+ FluentIcon.LIBRARY,
+ "零件库无法下载怎么办?",
+ "如遇网络问题或下载失败,请多次尝试或联系管理员。",
+ parent=self
+ )
+ faq_card2.clicked.connect(lambda: self.show_info(
+ "零件库无法下载解决方法",
+ "1. 检查网络连接。\n2. 多次刷新或重启软件。\n3. 若仍无法下载,请加入QQ群:857466609 反馈。"
+ ))
+ faq_group.addSettingCard(faq_card2)
+
+ # faq_card3 = PushSettingCard(
+ # "获取下载链接",
+ # FluentIcon.DOWNLOAD,
+ # "如何下载最新版?",
+ # "点击按钮获取最新版下载地址。",
+ # parent=self
+ # )
+ # faq_card3.clicked.connect(lambda: self.show_info(
+ # "最新版下载地址",
+ # "GitHub发布页:https://github.com/goldenfishs/MRobot/releases\n如遇下载问题,请联系QQ群:857466609"
+ # ))
+ # faq_group.addSettingCard(faq_card3)
+ main_layout.addWidget(faq_group)
+
+ # 联系方式
+ contact_group = SettingCardGroup("联系方式", self)
+ contact_card = PushSettingCard(
+ "复制邮箱",
+ FluentIcon.MESSAGE,
+ "联系开发团队",
+ "点击按钮复制邮箱地址:support@mrobot.com",
+ parent=self
+ )
+ contact_card.clicked.connect(lambda: self.copy_text("support@mrobot.com", "邮箱已复制:1683502971@qq.com"))
+ contact_group.addSettingCard(contact_card)
+ main_layout.addWidget(contact_group)
+
+ main_layout.addStretch()
+ # 不使用滚动区,直接添加内容区
+ layout.addWidget(content_widget)
+
+ def copy_text(self, text, message):
+ clipboard = QApplication.clipboard()
+ clipboard.setText(text)
+ InfoBar.info(
+ title="已复制",
+ content=message,
+ parent=self,
+ position=InfoBarPosition.TOP,
+ duration=2000
+ )
+
+ def check_update(self):
+ latest = check_update()
+ if latest:
+ self.show_info(
+ "发现新版本",
+ f"检测到新版本 {latest},请前往 GitHub 下载:\nhttps://github.com/goldenfishs/MRobot/releases"
+ )
+ else:
+ InfoBar.info(
+ title="已是最新版",
+ content="当前已是最新版本。",
+ parent=self,
+ duration=2000
+ )
+
+ def show_info(self, title, content):
+ dialog = Dialog(
+ title=title,
+ content=content,
+ parent=self
+ )
+ dialog.exec()
class AboutInterface(BaseInterface):
def __init__(self, parent=None):
@@ -605,24 +1594,31 @@ class MainWindow(FluentWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MR_ToolBox")
- self.resize(1000, 700)
- self.setMinimumSize(800, 600)
-
+ self.resize(800, 600)
+ self.setMinimumSize(640, 480)
+ self.setWindowFlag(Qt.Window)
# 记录当前主题
self.current_theme = Theme.DARK
-
+ latest = check_update()
+ if latest:
+ InfoBar.info(
+ title="发现新版本",
+ content=f"检测到新版本 {latest},请前往 GitHub 下载更新。",
+ parent=self,
+ duration=5000
+ )
# 创建页面实例
self.setting_page = SettingInterface(self)
self.setting_page.themeSwitchRequested.connect(self.toggle_theme)
self.page_registry = [
- (HomeInterface(self), FIF.HOME, "首页", NavigationItemPosition.TOP),
- (DataInterface(self), FIF.LIBRARY, "MRobot代码生成", NavigationItemPosition.SCROLL),
- (SerialTerminalInterface(self), FIF.COMMAND_PROMPT, "Mini_Shell", NavigationItemPosition.SCROLL),
- (PartLibraryInterface(self), FIF.DOWNLOAD, "零件库", NavigationItemPosition.SCROLL), # ← 加上这一行
- (self.setting_page, FIF.SETTING, "设置", NavigationItemPosition.BOTTOM),
- (HelpInterface(self), FIF.HELP, "帮助", NavigationItemPosition.BOTTOM),
- (AboutInterface(self), FIF.INFO, "关于", NavigationItemPosition.BOTTOM),
+ (HomeInterface(self), FluentIcon.HOME, "首页", NavigationItemPosition.TOP),
+ (DataInterface(self), FluentIcon.LIBRARY, "MRobot代码生成", NavigationItemPosition.SCROLL),
+ (SerialTerminalInterface(self), FluentIcon.COMMAND_PROMPT, "Mini_Shell", NavigationItemPosition.SCROLL),
+ (PartLibraryInterface(self), FluentIcon.DOWNLOAD, "零件库", NavigationItemPosition.SCROLL), # ← 加上这一行
+ (self.setting_page, FluentIcon.SETTING, "设置", NavigationItemPosition.BOTTOM),
+ (HelpInterface(self), FluentIcon.HELP, "帮助", NavigationItemPosition.BOTTOM),
+ # (AboutInterface(self), FluentIcon.INFO, "关于", NavigationItemPosition.BOTTOM),
]
self.initNavigation()
@@ -650,18 +1646,82 @@ class MainWindow(FluentWindow):
def show_user_info(self):
dialog = Dialog(
- title="用户信息",
- content="用户:MRobot至尊VIP用户",
+ title="MRobot",
+ content="账号:VIP内测版",
parent=self
)
dialog.exec()
+def check_update():
+ try:
+ repo = "goldenfishs/MRobot"
+ url = f"https://api.github.com/repos/{repo}/releases/latest"
+ resp = requests.get(url, timeout=5)
+ if resp.status_code == 200:
+ latest = resp.json()["tag_name"].lstrip("v")
+ print(f"本地版本: {__version__}, 最新版本: {latest}") # 调试用
+ if vparse(latest) > vparse(__version__):
+ return latest
+ except Exception as e:
+ print(f"检查更新失败: {e}")
+ return None
+
# ===================== 程序入口 =====================
def main():
+ from PyQt5.QtWidgets import QApplication # <-- 移到这里,所有平台都能用
+ import platform
+ if platform.system() == "Windows":
+ try:
+ from PyQt5.QtCore import Qt
+ import ctypes
+ ctypes.windll.shcore.SetProcessDpiAwareness(1)
+ except Exception:
+ pass
+ QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
+ QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
+
app = QApplication(sys.argv)
+ # 跟随系统主题
setTheme(Theme.DARK)
+ splash = SplashScreen()
+ setTheme(Theme.DARK)
+ splash.show()
+ setTheme(Theme.DARK)
+ app.processEvents()
+
+ # 步骤1:获取零件库
+ splash.set_status("正在获取零件仓库...", 20)
+ try:
+ import requests
+ resp = requests.get("http://154.37.215.220:5000/list", params={"key": "MRobot_Download"}, timeout=5)
+ resp.raise_for_status()
+ except Exception:
+ pass
+ app.processEvents()
+
+ # 步骤2:检查更新
+ splash.set_status("正在检查软件更新...", 60)
+ latest = check_update()
+ app.processEvents()
+
+ # 步骤3:加载主窗口
+ splash.set_status("正在加载主界面...", 90)
window = MainWindow()
window.show()
+ setTheme(Theme.DARK)
+ splash.set_status("启动完成", 100)
+ from PyQt5.QtCore import QTimer
+ QTimer.singleShot(500, splash.close)
+
+ # 有新版本弹窗
+ if latest:
+ InfoBar.info(
+ title="发现新版本",
+ content=f"检测到新版本 {latest},请前往帮助页面下载新版。",
+ parent=window,
+ duration=5000
+ )
+
sys.exit(app.exec_())
if __name__ == '__main__':
diff --git a/MRobot_old.py b/MRobot_old.py
deleted file mode 100644
index c85ba8c..0000000
--- a/MRobot_old.py
+++ /dev/null
@@ -1,719 +0,0 @@
-import tkinter as tk
-from tkinter import ttk
-from PIL import Image, ImageTk
-import sys
-import os
-import threading
-import shutil
-import re
-from git import Repo
-from collections import defaultdict
-import csv
-import xml.etree.ElementTree as ET
-
-# 配置常量
-REPO_DIR = "MRobot_repo"
-REPO_URL = "http://gitea.qutrobot.top/robofish/MRobot.git"
-if getattr(sys, 'frozen', False): # 检查是否为打包后的环境
- CURRENT_DIR = os.path.dirname(sys.executable) # 使用可执行文件所在目录
-else:
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) # 使用脚本所在目录
-
-MDK_ARM_DIR = os.path.join(CURRENT_DIR, "MDK-ARM")
-USER_DIR = os.path.join(CURRENT_DIR, "User")
-
-class MRobotApp:
- def __init__(self):
- self.ioc_data = None
- self.add_gitignore_var = None # 延迟初始化
- self.header_file_vars = {}
- self.task_vars = [] # 用于存储任务的变量
-
- # 初始化
- def initialize(self):
- print("初始化中,正在克隆仓库...")
- self.clone_repo()
- self.ioc_data = self.find_and_read_ioc_file()
- print("初始化完成,启动主窗口...")
- self.show_main_window()
-
- # 克隆仓库
- def clone_repo(self):
- try:
- if os.path.exists(REPO_DIR):
- shutil.rmtree(REPO_DIR)
- print(f"正在克隆仓库到 {REPO_DIR}(仅克隆当前文件内容)...")
- Repo.clone_from(REPO_URL, REPO_DIR, multi_options=["--depth=1"])
- print("仓库克隆成功!")
- except Exception as e:
- print(f"克隆仓库时出错: {e}")
-
- # 删除克隆的仓库
- def delete_repo(self):
- try:
- if os.path.exists(REPO_DIR):
- shutil.rmtree(REPO_DIR)
- print(f"已删除克隆的仓库目录: {REPO_DIR}")
- except Exception as e:
- print(f"删除仓库目录时出错: {e}")
-
-
- # 复制文件
- def copy_file_from_repo(self, src_path, dest_path):
- try:
- # 修复路径拼接问题,确保 src_path 不重复包含 REPO_DIR
- if src_path.startswith(REPO_DIR):
- full_src_path = src_path
- else:
- full_src_path = os.path.join(REPO_DIR, src_path.lstrip(os.sep))
-
- # 检查源文件是否存在
- if not os.path.exists(full_src_path):
- print(f"文件 {full_src_path} 不存在!(检查路径或仓库内容)")
- return
-
- # 检查目标路径是否有效
- if not dest_path or not dest_path.strip():
- print("目标路径为空或无效,无法复制文件!")
- return
-
- # 创建目标目录(如果不存在)
- dest_dir = os.path.dirname(dest_path)
- if dest_dir and not os.path.exists(dest_dir):
- os.makedirs(dest_dir, exist_ok=True)
-
- # 执行文件复制
- shutil.copy(full_src_path, dest_path)
- print(f"文件已从 {full_src_path} 复制到 {dest_path}")
- except Exception as e:
- print(f"复制文件时出错: {e}")
-
- # 查找并读取 .ioc 文件
- def find_and_read_ioc_file(self):
- try:
- for file in os.listdir("."):
- if file.endswith(".ioc"):
- print(f"找到 .ioc 文件: {file}")
- with open(file, "r", encoding="utf-8") as f:
- return f.read()
- print("未找到 .ioc 文件!")
- except Exception as e:
- print(f"读取 .ioc 文件时出错: {e}")
- return None
-
- # 检查是否启用了 FreeRTOS
- def check_freertos_enabled(self, ioc_data):
- try:
- return bool(re.search(r"Mcu\.IP\d+=FREERTOS", ioc_data))
- except Exception as e:
- print(f"检查 FreeRTOS 配置时出错: {e}")
- return False
-
- # 生成操作
- def generate_action(self):
- def task():
- # 检查并创建目录
- self.create_directories()
-
- if self.add_gitignore_var.get():
- self.copy_file_from_repo(".gitignore", ".gitignore")
- if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
- self.copy_file_from_repo("src/freertos.c", os.path.join("Core", "Src", "freertos.c"))
-
- # 定义需要处理的文件夹
- folders = ["bsp", "component", "device", "module"]
-
- # 遍历每个文件夹,复制选中的 .h 和 .c 文件
- for folder in folders:
- folder_dir = os.path.join(REPO_DIR, "User", folder)
- if not os.path.exists(folder_dir):
- continue # 如果文件夹不存在,跳过
-
- for file_name in os.listdir(folder_dir):
- file_base, file_ext = os.path.splitext(file_name)
- if file_ext not in [".h", ".c"]:
- continue # 只处理 .h 和 .c 文件
-
- # 强制复制与文件夹同名的文件
- if file_base == folder:
- src_path = os.path.join(folder_dir, file_name)
- dest_path = os.path.join("User", folder, file_name)
- self.copy_file_from_repo(src_path, dest_path)
- continue # 跳过后续检查,直接复制
-
- # 检查是否选中了对应的文件
- if file_base in self.header_file_vars and self.header_file_vars[file_base].get():
- src_path = os.path.join(folder_dir, file_name)
- dest_path = os.path.join("User", folder, file_name)
- self.copy_file_from_repo(src_path, dest_path)
-
- threading.Thread(target=task).start()
-
-
-
- # 创建必要的目录
- def create_directories(self):
- try:
- directories = [
- "User/bsp",
- "User/component",
- "User/device",
- "User/module",
- ]
- # 根据是否启用 FreeRTOS 决定是否创建 User/task
- if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
- directories.append("User/task")
-
- for directory in directories:
- if not os.path.exists(directory):
- os.makedirs(directory, exist_ok=True)
- print(f"已创建目录: {directory}")
- else:
- print(f"目录已存在: {directory}")
- except Exception as e:
- print(f"创建目录时出错: {e}")
-
-
- # 更新 FreeRTOS 状态标签
- def update_freertos_status(self, label):
- if self.ioc_data:
- status = "已启用" if self.check_freertos_enabled(self.ioc_data) else "未启用"
- else:
- status = "未检测到 .ioc 文件"
- label.config(text=f"FreeRTOS 状态: {status}")
-
-
-
- # 显示主窗口
- # ...existing code...
- # ...existing code...
-
- # 显示主窗口
- def show_main_window(self):
- root = tk.Tk()
- root.title("MRobot 自动生成脚本")
- root.geometry("1000x650") # 调整窗口大小以适应布局
-
- # 在窗口关闭时调用 on_closing 方法
- root.protocol("WM_DELETE_WINDOW", lambda: self.on_closing(root))
-
- # 初始化 BooleanVar
- self.add_gitignore_var = tk.BooleanVar(value=False)
- self.auto_configure_var = tk.BooleanVar(value=False) # 新增复选框变量
-
- # 创建主框架
- main_frame = ttk.Frame(root)
- main_frame.pack(fill="both", expand=True)
-
- # 添加标题
- title_label = ttk.Label(main_frame, text="MRobot 自动生成脚本", font=("Arial", 16, "bold"))
- title_label.pack(pady=10)
-
- # 添加 FreeRTOS 状态标签
- freertos_status_label = ttk.Label(main_frame, text="FreeRTOS 状态: 检测中...", font=("Arial", 12))
- freertos_status_label.pack(pady=10)
- self.update_freertos_status(freertos_status_label)
-
- # 模块文件选择和任务管理框架(添加滚动功能)
- module_task_frame = ttk.Frame(main_frame)
- module_task_frame.pack(fill="both", expand=True, padx=10, pady=10)
-
- # 创建 Canvas 和 Scrollbar
- canvas = tk.Canvas(module_task_frame)
- scrollbar = ttk.Scrollbar(module_task_frame, orient="vertical", command=canvas.yview)
- scrollable_frame = ttk.Frame(canvas)
-
- # 配置滚动区域
- scrollable_frame.bind(
- "",
- lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
- )
- canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
- canvas.configure(yscrollcommand=scrollbar.set)
-
- # 绑定鼠标滚轮事件
- def on_mouse_wheel(event):
- canvas.yview_scroll(-1 * int(event.delta / 120), "units")
-
- canvas.bind_all("", on_mouse_wheel)
-
- # 布局 Canvas 和 Scrollbar
- canvas.pack(side="left", fill="both", expand=True)
- scrollbar.pack(side="right", fill="y")
-
- # 左右布局:模块文件选择框和任务管理框
- left_frame = ttk.Frame(scrollable_frame)
- left_frame.pack(side="left", fill="both", expand=True, padx=5, pady=5)
-
- right_frame = ttk.Frame(scrollable_frame)
- right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)
-
- # 模块文件选择框
- header_files_frame = ttk.LabelFrame(left_frame, text="模块文件选择", padding=(10, 10))
- header_files_frame.pack(fill="both", expand=True, padx=5)
- self.header_files_frame = header_files_frame
- self.update_header_files()
-
- # 任务管理框
- if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
- task_frame = ttk.LabelFrame(right_frame, text="任务管理", padding=(10, 10))
- task_frame.pack(fill="both", expand=True, padx=5)
- self.task_frame = task_frame
- self.update_task_ui()
-
- # 添加消息框和生成按钮在同一行
- bottom_frame = ttk.Frame(main_frame)
- bottom_frame.pack(fill="x", pady=10, side="bottom")
-
- # 消息框
- self.message_box = tk.Text(bottom_frame, wrap="word", state="disabled", height=5, width=60)
- self.message_box.pack(side="left", fill="x", expand=True, padx=5, pady=5)
-
- # 生成按钮和复选框选项
- button_frame = ttk.Frame(bottom_frame)
- button_frame.pack(side="right", padx=10)
-
- # 添加复选框容器(横向排列复选框)
- checkbox_frame = ttk.Frame(button_frame)
- checkbox_frame.pack(side="top", pady=5)
-
- # 添加 .gitignore 复选框(左侧)
- ttk.Checkbutton(checkbox_frame, text=".gitignore", variable=self.add_gitignore_var).pack(side="left", padx=5)
-
- # 添加自动配置环境复选框(右侧)
- ttk.Checkbutton(checkbox_frame, text="自动环境", variable=self.auto_configure_var).pack(side="left", padx=5)
-
- # 添加生成按钮(竖向排列在复选框下方)
- generate_button = ttk.Button(button_frame, text="一键生成MRobot代码", command=self.generate_action)
- generate_button.pack(side="top", pady=10)
- generate_button.config(width=25) # 设置按钮宽度
-
- # 重定向输出到消息框
- self.redirect_output()
-
- # 打印欢迎信息
- print("欢迎使用 MRobot 自动生成脚本!")
- print("请根据需要选择模块文件和任务。")
- print("点击“一键生成MRobot代码”按钮开始生成。")
-
- # 启动 Tkinter 主事件循环
- root.mainloop()
-
- # ...existing code...
- # ...existing code...
-
- def redirect_output(self):
- """
- 重定向标准输出到消息框
- """
- class TextRedirector:
- def __init__(self, text_widget):
- self.text_widget = text_widget
-
- def write(self, message):
- self.text_widget.config(state="normal")
- self.text_widget.insert("end", message)
- self.text_widget.see("end")
- self.text_widget.config(state="disabled")
-
- def flush(self):
- pass
-
- sys.stdout = TextRedirector(self.message_box)
- sys.stderr = TextRedirector(self.message_box)
-
- # 修改 update_task_ui 方法
- def update_task_ui(self):
- # 检查是否有已存在的任务文件
- task_dir = os.path.join("User", "task")
- if os.path.exists(task_dir):
- for file_name in os.listdir(task_dir):
- file_base, file_ext = os.path.splitext(file_name)
- if file_ext == ".c" and file_base not in ["init", "user_task"] and file_base not in [task_var.get() for task_var, _ in self.task_vars]:
- frequency = 100 # 默认频率
- user_task_header_path = os.path.join("User", "task", "user_task.h")
- if os.path.exists(user_task_header_path):
- try:
- with open(user_task_header_path, "r", encoding="utf-8") as f:
- content = f.read()
- pattern = rf"#define\s+TASK_FREQ_{file_base.upper()}\s*\((\d+)[uU]?\)"
- match = re.search(pattern, content)
- if match:
- frequency = int(match.group(1))
- print(f"从 user_task.h 文件中读取到任务 {file_base} 的频率: {frequency}")
- except Exception as e:
- print(f"读取 user_task.h 文件时出错: {e}")
-
- new_task_var = tk.StringVar(value=file_base)
- self.task_vars.append((new_task_var, tk.IntVar(value=frequency)))
-
- # 清空任务框架中的所有子组件
- for widget in self.task_frame.winfo_children():
- widget.destroy()
-
-
- # 设置任务管理框的固定宽度
- self.task_frame.config(width=400)
-
- # 显示任务列表
- for i, (task_var, freq_var) in enumerate(self.task_vars):
- task_row = ttk.Frame(self.task_frame, width=400)
- task_row.pack(fill="x", pady=5)
-
- ttk.Entry(task_row, textvariable=task_var, width=20).pack(side="left", padx=5)
- ttk.Label(task_row, text="频率:").pack(side="left", padx=5)
- ttk.Spinbox(task_row, from_=1, to=1000, textvariable=freq_var, width=5).pack(side="left", padx=5)
- ttk.Button(task_row, text="删除", command=lambda idx=i: self.remove_task(idx)).pack(side="left", padx=5)
-
- # 添加新任务按钮
- add_task_button = ttk.Button(self.task_frame, text="添加任务", command=self.add_task)
- add_task_button.pack(pady=10)
-
-
- # 修改 add_task 方法
- def add_task(self):
- new_task_var = tk.StringVar(value=f"Task_{len(self.task_vars) + 1}")
- new_freq_var = tk.IntVar(value=100) # 默认频率为 100
- self.task_vars.append((new_task_var, new_freq_var))
- self.update_task_ui()
-
- # 修改 remove_task 方法
- def remove_task(self, idx):
- del self.task_vars[idx]
- self.update_task_ui()
-
- # 更新文件夹显示
- def update_folder_display(self):
- for widget in self.folder_frame.winfo_children():
- widget.destroy()
-
- folders = ["User/bsp", "User/component", "User/device", "User/module"]
- # if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
- # folders.append("User/task")
-
- for folder in folders:
- # 去掉 "User/" 前缀
- display_name = folder.replace("User/", "")
- tk.Label(self.folder_frame, text=display_name).pack()
-
- # 更新 .h 文件复选框
- def update_header_files(self):
- for widget in self.header_files_frame.winfo_children():
- widget.destroy()
-
- folders = ["bsp", "component", "device", "module"]
- dependencies = defaultdict(list)
-
- for folder in folders:
- folder_dir = os.path.join(REPO_DIR, "User", folder)
- if os.path.exists(folder_dir):
- dependencies_file = os.path.join(folder_dir, "dependencies.csv")
- if os.path.exists(dependencies_file):
- with open(dependencies_file, "r", encoding="utf-8") as f:
- reader = csv.reader(f)
- for row in reader:
- if len(row) == 2:
- dependencies[row[0]].append(row[1])
-
- # 创建复选框
- for folder in folders:
- folder_dir = os.path.join(REPO_DIR, "User", folder)
- if os.path.exists(folder_dir):
- module_frame = ttk.LabelFrame(self.header_files_frame, text=folder.capitalize(), padding=(10, 10))
- module_frame.pack(fill="x", pady=5)
-
- row, col = 0, 0
- for file in os.listdir(folder_dir):
- file_base, file_ext = os.path.splitext(file)
- if file_ext == ".h" and file_base != folder:
- var = tk.BooleanVar(value=False)
- self.header_file_vars[file_base] = var
-
- checkbox = ttk.Checkbutton(
- module_frame,
- text=file_base,
- variable=var,
- command=lambda fb=file_base: self.handle_dependencies(fb, dependencies)
- )
- checkbox.grid(row=row, column=col, padx=5, pady=5, sticky="w")
- col += 1
- if col >= 6:
- col = 0
- row += 1
-
-
-
- def handle_dependencies(self, file_base, dependencies):
- """
- 根据依赖关系自动勾选相关模块
- """
- if file_base in self.header_file_vars and self.header_file_vars[file_base].get():
- # 如果当前模块被选中,自动勾选其依赖项
- for dependency in dependencies.get(file_base, []):
- dep_base = os.path.basename(dependency)
- if dep_base in self.header_file_vars:
- self.header_file_vars[dep_base].set(True)
-
- # 在 MRobotApp 类中添加以下方法
- def generate_task_files(self):
- try:
- template_file_path = os.path.join(REPO_DIR, "User", "task", "task.c.template")
- task_dir = os.path.join("User", "task")
-
- if not os.path.exists(template_file_path):
- print(f"模板文件 {template_file_path} 不存在,无法生成 task.c 文件!")
- return
-
- os.makedirs(task_dir, exist_ok=True)
-
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
-
- # 为每个任务生成对应的 task.c 文件
- for task_var, _ in self.task_vars: # 解包元组
- task_name = f"Task_{task_var.get()}" # 添加前缀 Task_
- task_file_path = os.path.join(task_dir, f"{task_var.get().lower()}.c") # 文件名保持原始小写
-
- # 替换模板中的占位符
- task_content = template_content.replace("{{task_name}}", task_name)
- task_content = task_content.replace("{{task_function}}", task_name)
- task_content = task_content.replace(
- "{{task_frequency}}", f"TASK_FREQ_{task_var.get().upper()}"
- ) # 替换为 user_task.h 中的宏定义
- task_content = task_content.replace("{{task_delay}}", f"TASK_INIT_DELAY_{task_var.get().upper()}")
-
- with open(task_file_path, "w", encoding="utf-8") as f:
- f.write(task_content)
-
- print(f"已成功生成 {task_file_path} 文件!")
- except Exception as e:
- print(f"生成 task.c 文件时出错: {e}")
- # 修改 user_task.c 文件
- def modify_user_task_file(self):
- try:
- template_file_path = os.path.join(REPO_DIR, "User", "task", "user_task.c.template")
- generated_task_file_path = os.path.join("User", "task", "user_task.c")
-
- if not os.path.exists(template_file_path):
- print(f"模板文件 {template_file_path} 不存在,无法生成 user_task.c 文件!")
- return
-
- os.makedirs(os.path.dirname(generated_task_file_path), exist_ok=True)
-
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
-
- # 生成任务属性定义
- task_attr_definitions = "\n".join([
- f"""const osThreadAttr_t attr_{task_var.get().lower()} = {{
- .name = "{task_var.get()}",
- .priority = osPriorityNormal,
- .stack_size = 128 * 4,
- }};"""
- for task_var, _ in self.task_vars # 解包元组
- ])
-
- # 替换模板中的占位符
- task_content = template_content.replace("{{task_attr_definitions}}", task_attr_definitions)
-
- with open(generated_task_file_path, "w", encoding="utf-8") as f:
- f.write(task_content)
-
- print(f"已成功生成 {generated_task_file_path} 文件!")
- except Exception as e:
- print(f"修改 user_task.c 文件时出错: {e}")
- # ...existing code...
-
- def generate_user_task_header(self):
- try:
- template_file_path = os.path.join(REPO_DIR, "User", "task", "user_task.h.template")
- header_file_path = os.path.join("User", "task", "user_task.h")
-
- if not os.path.exists(template_file_path):
- print(f"模板文件 {template_file_path} 不存在,无法生成 user_task.h 文件!")
- return
-
- os.makedirs(os.path.dirname(header_file_path), exist_ok=True)
-
- # 如果 user_task.h 已存在,提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
- existing_msgq_content = ""
- if os.path.exists(header_file_path):
- with open(header_file_path, "r", encoding="utf-8") as f:
- content = f.read()
- # 提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
- match = re.search(r"/\* USER MESSAGE BEGIN \*/\s*(.*?)\s*/\* USER MESSAGE END \*/", content, re.DOTALL)
- if match:
- existing_msgq_content = match.group(1).strip()
- print("已存在的 msgq 区域内容:")
- print(existing_msgq_content)
-
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
-
- # 定义占位符内容
- thread_definitions = "\n".join([f" osThreadId_t {task_var.get().lower()};" for task_var, _ in self.task_vars])
- msgq_definitions = existing_msgq_content if existing_msgq_content else " osMessageQueueId_t default_msgq;"
- freq_definitions = "\n".join([f" float {task_var.get().lower()};" for task_var, _ in self.task_vars])
- last_up_time_definitions = "\n".join([f" uint32_t {task_var.get().lower()};" for task_var, _ in self.task_vars])
- task_attr_declarations = "\n".join([f"extern const osThreadAttr_t attr_{task_var.get().lower()};" for task_var, _ in self.task_vars])
- task_function_declarations = "\n".join([f"void Task_{task_var.get()}(void *argument);" for task_var, _ in self.task_vars])
- task_frequency_definitions = "\n".join([
- f"#define TASK_FREQ_{task_var.get().upper()} ({freq_var.get()}u)"
- for task_var, freq_var in self.task_vars
- ])
- task_init_delay_definitions = "\n".join([f"#define TASK_INIT_DELAY_{task_var.get().upper()} (0u)" for task_var, _ in self.task_vars])
- task_handle_definitions = "\n".join([f" osThreadId_t {task_var.get().lower()};" for task_var, _ in self.task_vars])
-
- # 替换模板中的占位符
- header_content = template_content.replace("{{thread_definitions}}", thread_definitions)
- header_content = header_content.replace("{{msgq_definitions}}", msgq_definitions)
- header_content = header_content.replace("{{freq_definitions}}", freq_definitions)
- header_content = header_content.replace("{{last_up_time_definitions}}", last_up_time_definitions)
- header_content = header_content.replace("{{task_attr_declarations}}", task_attr_declarations)
- header_content = header_content.replace("{{task_function_declarations}}", task_function_declarations)
- header_content = header_content.replace("{{task_frequency_definitions}}", task_frequency_definitions)
- header_content = header_content.replace("{{task_init_delay_definitions}}", task_init_delay_definitions)
- header_content = header_content.replace("{{task_handle_definitions}}", task_handle_definitions)
-
- # 如果存在 /* USER MESSAGE BEGIN */ 区域内容,则保留
- if existing_msgq_content:
- header_content = re.sub(
- r"/\* USER MESSAGE BEGIN \*/\s*.*?\s*/\* USER MESSAGE END \*/",
- f"/* USER MESSAGE BEGIN */\n\n {existing_msgq_content}\n\n /* USER MESSAGE END */",
- header_content,
- flags=re.DOTALL
- )
-
- with open(header_file_path, "w", encoding="utf-8") as f:
- f.write(header_content)
-
- print(f"已成功生成 {header_file_path} 文件!")
- except Exception as e:
- print(f"生成 user_task.h 文件时出错: {e}")
-
- def generate_init_file(self):
- try:
- template_file_path = os.path.join(REPO_DIR, "User", "task", "init.c.template")
- generated_file_path = os.path.join("User", "task", "init.c")
-
- if not os.path.exists(template_file_path):
- print(f"模板文件 {template_file_path} 不存在,无法生成 init.c 文件!")
- return
-
- os.makedirs(os.path.dirname(generated_file_path), exist_ok=True)
-
- # 如果 init.c 已存在,提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
- existing_msgq_content = ""
- if os.path.exists(generated_file_path):
- with open(generated_file_path, "r", encoding="utf-8") as f:
- content = f.read()
- # 提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
- match = re.search(r"/\* USER MESSAGE BEGIN \*/\s*(.*?)\s*/\* USER MESSAGE END \*/", content, re.DOTALL)
- if match:
- existing_msgq_content = match.group(1).strip()
- print("已存在的消息队列区域内容:")
- print(existing_msgq_content)
-
- with open(template_file_path, "r", encoding="utf-8") as f:
- template_content = f.read()
-
- # 生成任务创建代码
- thread_creation_code = "\n".join([
- f" task_runtime.thread.{task_var.get().lower()} = osThreadNew(Task_{task_var.get()}, NULL, &attr_{task_var.get().lower()});"
- for task_var, _ in self.task_vars # 解包元组
- ])
-
- # 替换模板中的占位符
- init_content = template_content.replace("{{thread_creation_code}}", thread_creation_code)
-
- # 如果存在 /* USER MESSAGE BEGIN */ 区域内容,则保留
- if existing_msgq_content:
- init_content = re.sub(
- r"/\* USER MESSAGE BEGIN \*/\s*.*?\s*/\* USER MESSAGE END \*/",
- f"/* USER MESSAGE BEGIN */\n {existing_msgq_content}\n /* USER MESSAGE END */",
- init_content,
- flags=re.DOTALL
- )
-
- with open(generated_file_path, "w", encoding="utf-8") as f:
- f.write(init_content)
-
- print(f"已成功生成 {generated_file_path} 文件!")
- except Exception as e:
- print(f"生成 init.c 文件时出错: {e}")
-
- # 修改 generate_action 方法
-
- def generate_action(self):
- def task():
- # 检查并创建目录(与 FreeRTOS 状态无关的模块始终创建)
- self.create_directories()
-
- # 复制 .gitignore 文件
- if self.add_gitignore_var.get():
- self.copy_file_from_repo(".gitignore", ".gitignore")
-
- # 如果启用了 FreeRTOS,复制相关文件
- if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
- self.copy_file_from_repo("src/freertos.c", os.path.join("Core", "Src", "freertos.c"))
-
- # 定义需要处理的文件夹(与 FreeRTOS 状态无关)
- folders = ["bsp", "component", "device", "module"]
-
- # 遍历每个文件夹,复制选中的 .h 和 .c 文件
- for folder in folders:
- folder_dir = os.path.join(REPO_DIR, "User", folder)
- if not os.path.exists(folder_dir):
- continue # 如果文件夹不存在,跳过
-
- for file_name in os.listdir(folder_dir):
- file_base, file_ext = os.path.splitext(file_name)
- if file_ext not in [".h", ".c"]:
- continue # 只处理 .h 和 .c 文件
-
- # 强制复制与文件夹同名的文件
- if file_base == folder:
- src_path = os.path.join(folder_dir, file_name)
- dest_path = os.path.join("User", folder, file_name)
- self.copy_file_from_repo(src_path, dest_path)
- print(f"强制复制与文件夹同名的文件: {file_name}")
- continue # 跳过后续检查,直接复制
-
- # 检查是否选中了对应的文件
- if file_base in self.header_file_vars and self.header_file_vars[file_base].get():
- src_path = os.path.join(folder_dir, file_name)
- dest_path = os.path.join("User", folder, file_name)
- self.copy_file_from_repo(src_path, dest_path)
-
- # 如果启用了 FreeRTOS,执行任务相关的生成逻辑
- if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
- # 修改 user_task.c 文件
- self.modify_user_task_file()
-
- # 生成 user_task.h 文件
- self.generate_user_task_header()
-
- # 生成 init.c 文件
- self.generate_init_file()
-
- # 生成 task.c 文件
- self.generate_task_files()
-
- # # 自动配置环境
- # if self.auto_configure_var.get():
-
- # self.auto_configure_environment()
-
-
- threading.Thread(target=task).start()
-
- # 程序关闭时清理
- def on_closing(self, root):
- self.delete_repo()
- root.destroy()
-
-
-# 程序入口
-if __name__ == "__main__":
- app = MRobotApp()
- app.initialize()
\ No newline at end of file
diff --git a/README.md b/README.md
index f1f246a..bf02c41 100644
--- a/README.md
+++ b/README.md
@@ -90,4 +90,36 @@
pyinstaller --onefile --windowed
pyinstaller MR_Toolbox.py --onefile --noconsole --icon=img\M.ico --add-data "mr_tool_img\MRobot.png;mr_tool_img"
-pyinstaller MR_Tool.py --onefile --noconsole --icon=img\M.ico --add-data "mr_tool_img\MRobot.png;mr_tool_img" --add-data "src;src" --add-data "User;User"
\ No newline at end of file
+pyinstaller MR_Tool.py --onefile --noconsole --icon=img\M.ico --add-data "mr_tool_img\MRobot.png;mr_tool_img" --add-data "src;src" --add-data "User;User"
+
+pyinstaller --noconfirm --onefile --windowed ^
+ --add-data "User_code;User_code" ^
+ --add-data "img;img" ^
+ --icon "img\M.ico" ^
+ MRobot.py
+
+
+pyinstaller --noconfirm --onefile --windowed --add-data "img;img" --add-data "User_code;User_code" --add-data "mech_lib;mech_lib" --icon=img/MRobot.ico MRobot.py
+
+python3 -m PyInstaller --noconfirm --onefile --windowed \
+ --add-data "img:img" \
+ --add-data "User_code:User_code" \
+ --add-data "mech_lib:mech_lib" \
+ --icon=img/MRobot.ico \
+ MRobot.py
+
+
+python3 -m PyInstaller --windowed --name MRobot \
+ --add-data "img:MRobot.app/Contents/Resources/img" \
+ --add-data "User_code:MRobot.app/Contents/Resources/User_code" \
+ --add-data "mech_lib:MRobot.app/Contents/Resources/mech_lib" \
+ MRobot.py
+
+
+
+pyinstaller --noconfirm --onefile --windowed --add-data "img;img" --add-data "User_code;User_code" --icon=img/M.ico MRobot.py
+
+
+pyinstaller MRobot.py
+
+pyinstaller --noconfirm --onefile --windowed --icon=img/M.ico --add-data "img;img" --add-data "User_code;User_code" --add-data "mech_lib;mech_lib" MRobot.py
\ No newline at end of file
diff --git a/User_code/.DS_Store b/User_code/.DS_Store
index cf5987b..1a99dba 100644
Binary files a/User_code/.DS_Store and b/User_code/.DS_Store differ
diff --git a/User_code/bsp/describe.csv b/User_code/bsp/describe.csv
new file mode 100644
index 0000000..0fee207
--- /dev/null
+++ b/User_code/bsp/describe.csv
@@ -0,0 +1,2 @@
+uart,要求开启dma和中断
+can,要求开启can的中断
\ No newline at end of file
diff --git a/User_code/component/describe.csv b/User_code/component/describe.csv
new file mode 100644
index 0000000..903a492
--- /dev/null
+++ b/User_code/component/describe.csv
@@ -0,0 +1 @@
+pid,好用的
\ No newline at end of file
diff --git a/User_code/device/describe.csv b/User_code/device/describe.csv
new file mode 100644
index 0000000..68b476a
--- /dev/null
+++ b/User_code/device/describe.csv
@@ -0,0 +1 @@
+servo,测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息
\ No newline at end of file
diff --git a/User_code/module/dependencies.csv b/User_code/module/dependencies.csv
new file mode 100644
index 0000000..3f4d215
--- /dev/null
+++ b/User_code/module/dependencies.csv
@@ -0,0 +1,5 @@
+oled_i2c,bsp/i2c
+bmp280_i2c,bsp/i2c
+pc_uart,bsp/uart
+key_gpio,bsp/gpio_exti
+servo,bsp/servo_pwm
\ No newline at end of file
diff --git a/User_code/module/describe.csv b/User_code/module/describe.csv
new file mode 100644
index 0000000..e69de29
diff --git a/User_code/task/init.c.template b/User_code/task/init.c.template
index 7b8cd24..3e9b49d 100644
--- a/User_code/task/init.c.template
+++ b/User_code/task/init.c.template
@@ -1,9 +1,10 @@
/*
- 初始化任务
+ Init Task
+ 任务初始化,创建各个线程任务和消息队列
*/
/* Includes ----------------------------------------------------------------- */
-#include "task\user_task.h"
+#include "task/user_task.h"
/* USER INCLUDE BEGIN */
@@ -23,16 +24,19 @@
*/
void Task_Init(void *argument) {
(void)argument; /* 未使用argument,消除警告 */
+ /* USER CODE BEGIN Task_Init */
- osKernelLock(); // 锁定内核,防止任务切换
-
- // 创建线程
+ /* USER CODE END Task_Init */
+ osKernelLock(); /* 锁定内核,防止任务切换 */
+
+ /* 创建任务线程 */
{{thread_creation_code}}
// 创建消息队列
/* USER MESSAGE BEGIN */
task_runtime.msgq.user_msg= osMessageQueueNew(2u, 10, NULL);
/* USER MESSAGE END */
+
osKernelUnlock(); // 解锁内核
osThreadTerminate(osThreadGetId()); // 任务完成后结束自身
}
diff --git a/User_code/task/task.c.template b/User_code/task/task.c.template
index 78b32bb..95f6a2c 100644
--- a/User_code/task/task.c.template
+++ b/User_code/task/task.c.template
@@ -1,22 +1,24 @@
/*
{{task_name}} Task
+ {{task_description}}
*/
/* Includes ----------------------------------------------------------------- */
-#include "task\user_task.h"
+#include "task/user_task.h"
+/* USER INCLUDE BEGIN*/
+
+/* USER INCLUDE END*/
/* Private typedef ---------------------------------------------------------- */
/* Private define ----------------------------------------------------------- */
/* Private macro ------------------------------------------------------------ */
/* Private variables -------------------------------------------------------- */
+/* USER STRUCT BEGIN*/
+
+/* USER STRUCT END*/
+
/* Private function --------------------------------------------------------- */
/* Exported functions ------------------------------------------------------- */
-
-/**
- * \brief {{task_name}} Task
- *
- * \param argument 未使用
- */
void {{task_function}}(void *argument) {
(void)argument; /* 未使用argument,消除警告 */
@@ -28,11 +30,9 @@ void {{task_function}}(void *argument) {
uint32_t tick = osKernelGetTickCount(); /* 控制任务运行频率的计时 */
while (1) {
tick += delay_tick; /* 计算下一个唤醒时刻 */
-
- /*User code begin*/
-
- /*User code end*/
+ /* USER CODE BEGIN */
+ /* USER CODE END */
osDelayUntil(tick); /* 运行结束,等待下一次唤醒 */
}
}
diff --git a/User_code/task/user_task.c.template b/User_code/task/user_task.c.template
index aaa17ed..69f49f0 100644
--- a/User_code/task/user_task.c.template
+++ b/User_code/task/user_task.c.template
@@ -1,4 +1,4 @@
-#include "task\user_task.h"
+#include "task/user_task.h"
Task_Runtime_t task_runtime;
@@ -8,5 +8,5 @@ const osThreadAttr_t attr_init = {
.stack_size = 256 * 4,
};
-// USER TASK
+/* User_task */
{{task_attr_definitions}}
diff --git a/User_code/task/user_task.h.template b/User_code/task/user_task.h.template
index 0a0a970..79b7476 100644
--- a/User_code/task/user_task.h.template
+++ b/User_code/task/user_task.h.template
@@ -3,12 +3,27 @@
#ifdef __cplusplus
extern "C" {
#endif
-
+/* Includes ----------------------------------------------------------------- */
#include
#include "FreeRTOS.h"
#include "task.h"
-// 定义任务运行时结构体
+/* USER INCLUDE BEGIN */
+
+/* USER INCLUDE END */
+/* Exported constants ------------------------------------------------------- */
+/* 任务运行频率 */
+{{task_frequency_definitions}}
+
+/* 任务初始化延时ms */
+#define TASK_INIT_DELAY (100u)
+{{task_init_delay_definitions}}
+
+/* Exported defines --------------------------------------------------------- */
+/* Exported macro ----------------------------------------------------------- */
+/* Exported types ----------------------------------------------------------- */
+
+/* 任务运行时结构体 */
typedef struct {
/* 各任务,也可以叫做线程 */
struct {
@@ -16,41 +31,47 @@ typedef struct {
} thread;
/* USER MESSAGE BEGIN */
-
struct {
osMessageQueueId_t user_msg; /* 用户自定义任务消息队列 */
} msgq;
-
/* USER MESSAGE END */
+ /* 机器人状态 */
+ struct {
+ float battery; /* 电池电量百分比 */
+ float vbat; /* 电池电压 */
+ float cpu_temp; /* CPU温度 */
+ } status;
+
+ /* USER CONFIG BEGIN */
+
+ /* USER CONFIG END */
+
+ /* 各任务的stack使用 */
+ struct {
+{{stack_definitions}}
+ } stack_water_mark;
+
+ /* 各任务运行频率 */
struct {
{{freq_definitions}}
- } freq; /* 任务运行频率 */
+ } freq;
+ /* 任务最近运行时间 */
struct {
{{last_up_time_definitions}}
- } last_up_time; /* 任务最近运行时间 */
+ } last_up_time;
+
} Task_Runtime_t;
-// 任务频率
-{{task_frequency_definitions}}
-// 任务初始化延时
-#define TASK_INIT_DELAY (100u)
-{{task_init_delay_definitions}}
-
-// 任务句柄
-typedef struct {
-{{task_handle_definitions}}
-} Task_Handles_t;
-
-// 任务运行时结构体
+/* 任务运行时结构体 */
extern Task_Runtime_t task_runtime;
-// 初始化任务句柄
+/* 初始化任务句柄 */
extern const osThreadAttr_t attr_init;
{{task_attr_declarations}}
-// 任务函数声明
+/* 任务函数声明 */
void Task_Init(void *argument);
{{task_function_declarations}}
diff --git a/fluentui.py b/fluentui.py
deleted file mode 100644
index c434363..0000000
--- a/fluentui.py
+++ /dev/null
@@ -1,168 +0,0 @@
-import sys
-import webbrowser
-import serial
-import serial.tools.list_ports
-
-from PyQt5.QtCore import Qt, QSize, pyqtSignal
-from PyQt5.QtGui import QPixmap, QFont
-from PyQt5.QtWidgets import (
- QWidget, QVBoxLayout, QApplication, QLabel, QGroupBox, QGridLayout, QFrame,
- QHBoxLayout, QComboBox, QTextEdit, QLineEdit
-)
-
-from qfluentwidgets import (
- NavigationInterface, NavigationItemPosition, MessageBox,
- setTheme, Theme, FluentWindow, NavigationAvatarWidget,
- InfoBar, InfoBarPosition, PushButton, FluentIcon
-)
-from qfluentwidgets import FluentIcon as FIF
-
-# ===================== 页面基类 =====================
-class BaseInterface(QWidget):
- """所有页面的基类,页面内容完全自定义"""
- def __init__(self, parent=None):
- super().__init__(parent=parent)
-
-# ===================== 首页界面 =====================
-class HomeInterface(BaseInterface):
- def __init__(self, parent=None):
- super().__init__(parent=parent)
- self.setObjectName("homeInterface")
- layout = QVBoxLayout()
- self.setLayout(layout)
-# ===================== 代码生成页面 =====================
-class DataInterface(BaseInterface):
- def __init__(self, parent=None):
- super().__init__(parent=parent)
- self.setObjectName("dataInterface")
- # 空页面示例
- layout = QVBoxLayout()
- self.setLayout(layout)
-
-# ===================== 串口终端界面 =====================
-class SerialTerminalInterface(BaseInterface):
- def __init__(self, parent=None):
- super().__init__(parent=parent)
- self.setObjectName("serialTerminalInterface")
- layout = QVBoxLayout()
-
-# ===================== 设置界面 =====================
-class SettingInterface(BaseInterface):
- def __init__(self, parent=None):
- super().__init__(parent=parent)
- self.setObjectName("settingInterface")
- layout = QVBoxLayout()
- self.themeBtn = PushButton(
- "切换夜间", self, FluentIcon.BRUSH
- )
- self.themeBtn.setFixedWidth(120)
- self.themeBtn.clicked.connect(self.onThemeBtnClicked)
- layout.addWidget(self.themeBtn)
- layout.addStretch(1)
- self.setLayout(layout)
-
- # 监听主题变化
- mw = self.window()
- if hasattr(mw, "themeChanged"):
- mw.themeChanged.connect(self.updateThemeBtn)
-
- def onThemeBtnClicked(self):
- mw = self.window()
- if hasattr(mw, "toggleTheme"):
- mw.toggleTheme()
-
- def updateThemeBtn(self, theme):
- if theme == Theme.LIGHT:
- self.themeBtn.setText("切换夜间")
- else:
- self.themeBtn.setText("切换白天")
-# ===================== 帮助与关于界面 =====================
-class HelpInterface(BaseInterface):
- def __init__(self, parent=None):
- super().__init__(parent=parent)
- self.setObjectName("helpInterface")
- layout = QVBoxLayout()
- self.setLayout(layout)
-
-class AboutInterface(BaseInterface):
- def __init__(self, parent=None):
- super().__init__(parent=parent)
- self.setObjectName("aboutInterface")
- layout = QVBoxLayout()
- self.setLayout(layout)
-
-# ===================== 主窗口与导航 =====================
-class MainWindow(FluentWindow):
- themeChanged = pyqtSignal(Theme)
-
- def __init__(self):
- super().__init__()
- self.setWindowTitle("MR_ToolBox")
- self.resize(1000, 700)
- self.setMinimumSize(800, 600)
- setTheme(Theme.LIGHT)
- self.theme = Theme.LIGHT
-
- self.page_registry = [
- (HomeInterface(self), FIF.HOME, "首页", NavigationItemPosition.TOP),
- (DataInterface(self), FIF.LIBRARY, "MRobot代码生成", NavigationItemPosition.SCROLL),
- (SerialTerminalInterface(self), FIF.COMMAND_PROMPT, "串口终端", NavigationItemPosition.SCROLL),
- (SettingInterface(self), FIF.SETTING, "设置", NavigationItemPosition.BOTTOM),
- (HelpInterface(self), FIF.HELP, "帮助", NavigationItemPosition.BOTTOM),
- (AboutInterface(self), FIF.INFO, "关于", NavigationItemPosition.BOTTOM),
- ]
- self.initNavigation()
-
- # 把切换主题按钮放到标题栏右侧
- self.themeBtn = PushButton("切换夜间", self, FluentIcon.BRUSH)
- self.themeBtn.setFixedWidth(120)
- self.themeBtn.clicked.connect(self.toggleTheme)
- self.addTitleBarWidget(self.themeBtn, align=Qt.AlignRight)
-
- def initNavigation(self):
- for page, icon, name, position in self.page_registry:
- self.addSubInterface(page, icon, name, position)
- self.navigationInterface.addSeparator()
- avatar = NavigationAvatarWidget('用户', ':/qfluentwidgets/images/avatar.png')
- self.navigationInterface.addWidget(
- routeKey='avatar',
- widget=avatar,
- onClick=self.showUserInfo,
- position=NavigationItemPosition.BOTTOM
- )
-
- def toggleTheme(self):
- if self.theme == Theme.LIGHT:
- setTheme(Theme.DARK)
- self.theme = Theme.DARK
- self.themeBtn.setText("切换白天")
- else:
- setTheme(Theme.LIGHT)
- self.theme = Theme.LIGHT
- self.themeBtn.setText("切换夜间")
- self.themeChanged.emit(self.theme)
- self.refreshStyle()
-
- def refreshStyle(self):
- def refresh(widget):
- widget.setStyleSheet(widget.styleSheet())
- for child in widget.findChildren(QWidget):
- refresh(child)
- refresh(self)
-
- def showUserInfo(self):
- MessageBox("用户信息", "当前登录用户:管理员", self).exec()
-# ===================== 程序入口 =====================
-def main():
- QApplication.setHighDpiScaleFactorRoundingPolicy(
- Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
- QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
- QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
-
- app = QApplication(sys.argv)
- window = MainWindow()
- window.show()
- sys.exit(app.exec_())
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
diff --git a/img/.DS_Store b/img/.DS_Store
index d67ede8..77445c9 100644
Binary files a/img/.DS_Store and b/img/.DS_Store differ
diff --git a/img/M.ico b/img/M.ico
index 400b26d..b7df782 100644
Binary files a/img/M.ico and b/img/M.ico differ
diff --git a/img/M2.ico b/img/M2.ico
new file mode 100644
index 0000000..400b26d
Binary files /dev/null and b/img/M2.ico differ
diff --git a/img/m1.png b/img/m1.png
new file mode 100644
index 0000000..4c8bccb
Binary files /dev/null and b/img/m1.png differ
diff --git a/mech_lib/README.md b/mech_lib/README.md
new file mode 100644
index 0000000..e476d43
--- /dev/null
+++ b/mech_lib/README.md
@@ -0,0 +1 @@
+# 机械常用零件库
\ No newline at end of file
diff --git a/pngico.py b/pngico.py
index 8a74659..cc04b03 100644
--- a/pngico.py
+++ b/pngico.py
@@ -1,42 +1,18 @@
from PIL import Image
import os
-def crop_transparent_background(input_path, output_path):
- """
- 裁切 PNG 图片的透明背景并保存。
-
- :param input_path: 输入图片路径
- :param output_path: 输出图片路径
- """
- try:
- # 打开图片
- img = Image.open(input_path)
-
- # 确保图片是 RGBA 模式
- if img.mode != "RGBA":
- img = img.convert("RGBA")
-
- # 获取图片的 alpha 通道
- bbox = img.getbbox()
-
- if bbox:
- # 裁切图片
- cropped_img = img.crop(bbox)
- # 保存裁切后的图片
- cropped_img.save(output_path, format="PNG")
- print(f"图片已保存到: {output_path}")
- else:
- print("图片没有透明背景或为空。")
- except Exception as e:
- print(f"处理图片时出错: {e}")
+def png_to_ico(png_path, ico_path=None, sizes=[(256,256), (128,128), (64,64), (32,32), (16,16)]):
+ if not os.path.isfile(png_path):
+ print(f"文件不存在: {png_path}")
+ return
+ if ico_path is None:
+ ico_path = os.path.splitext(png_path)[0] + ".ico"
+ img = Image.open(png_path)
+ img.save(ico_path, format='ICO', sizes=sizes)
+ print(f"已生成: {ico_path}")
if __name__ == "__main__":
- # 示例:输入和输出路径
- input_file = "C:\Mac\Home\Desktop\MRobot\img\M.png" # 替换为你的输入图片路径
- output_file = "C:\Mac\Home\Desktop\MRobot\img\M.png" # 替换为你的输出图片路径
-
- # 检查文件是否存在
- if os.path.exists(input_file):
- crop_transparent_background(input_file, output_file)
- else:
- print(f"输入文件不存在: {input_file}")
\ No newline at end of file
+ # 直接写死路径
+ png = r"C:\Mac\Home\Documents\R\MRobot\img\m1.png"
+ ico = r"c:\Mac\Home\Documents\R\MRobot\img\M1.ico"
+ png_to_ico(png, ico)
\ No newline at end of file
diff --git a/polynomial.py b/polynomial.py
deleted file mode 100644
index f7057eb..0000000
--- a/polynomial.py
+++ /dev/null
@@ -1,286 +0,0 @@
-import sys
-import numpy as np
-import pandas as pd
-from PyQt5.QtWidgets import (
- QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSpinBox,
- QLabel, QTableWidget, QTableWidgetItem, QFileDialog, QTextEdit,
- QComboBox, QMessageBox, QHeaderView
-)
-from PyQt5.QtGui import QFont
-from PyQt5.QtCore import Qt
-import matplotlib
-from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
-from matplotlib.figure import Figure
-
-class PolyFitApp(QWidget):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("MRobot 多项式拟合工具")
- self.resize(1440, 1280)
- self.setFont(QFont("微软雅黑", 11))
- self.center()
-
- self.data_x = []
- self.data_y = []
- self.last_coeffs = None
- self.last_xmin = None
- self.last_xmax = None
-
- # 主布局
- main_layout = QHBoxLayout(self)
- main_layout.setContentsMargins(20, 20, 20, 20)
- main_layout.setSpacing(20)
- left_layout = QVBoxLayout()
- left_layout.setSpacing(12)
- right_layout = QVBoxLayout()
- right_layout.setSpacing(12)
- main_layout.addLayout(left_layout, 0)
- main_layout.addLayout(right_layout, 1)
-
- # 数据输入区
- self.table = QTableWidget(0, 2)
- self.table.setFont(QFont("Consolas", 11))
- self.table.setHorizontalHeaderLabels(["x", "y"])
- self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
- self.table.setSelectionBehavior(QTableWidget.SelectRows)
- left_layout.addWidget(self.table)
-
- btn_row = QHBoxLayout()
- self.add_row_btn = QPushButton("添加数据")
- self.add_row_btn.setStyleSheet("color: #333;")
- self.add_row_btn.clicked.connect(self.add_point_row)
- btn_row.addWidget(self.add_row_btn)
-
- self.del_row_btn = QPushButton("删除选中行")
- self.del_row_btn.setStyleSheet("color: #333;")
- self.del_row_btn.clicked.connect(self.delete_selected_rows)
- btn_row.addWidget(self.del_row_btn)
- left_layout.addLayout(btn_row)
-
- # 导入导出按钮区
- file_btn_row = QHBoxLayout()
- self.import_btn = QPushButton("导入Excel文件")
- self.import_btn.setStyleSheet("font-weight: bold; color: #333;")
- self.import_btn.clicked.connect(self.load_excel)
- file_btn_row.addWidget(self.import_btn)
-
- self.export_btn = QPushButton("导出Excel文件")
- self.export_btn.setStyleSheet("font-weight: bold; color: #333;")
- self.export_btn.clicked.connect(self.export_excel_and_plot)
- file_btn_row.addWidget(self.export_btn)
- left_layout.addLayout(file_btn_row)
-
- # 拟合参数区
- param_layout = QHBoxLayout()
- param_layout.addWidget(QLabel("多项式阶数:"))
- self.order_spin = QSpinBox()
- self.order_spin.setRange(1, 10)
- self.order_spin.setValue(2)
- param_layout.addWidget(self.order_spin)
- left_layout.addLayout(param_layout)
-
- self.fit_btn = QPushButton("拟合并显示")
- self.fit_btn.setStyleSheet("font-weight: bold; color: #333;")
- self.fit_btn.clicked.connect(self.fit_and_plot)
- left_layout.addWidget(self.fit_btn)
-
- # 输出区
- self.output = QTextEdit()
- self.output.setReadOnly(False)
- self.output.setFont(QFont("Consolas", 10))
- self.output.setMaximumHeight(150)
- left_layout.addWidget(self.output)
-
- code_layout = QHBoxLayout()
- code_layout.addWidget(QLabel("输出代码格式:"))
- self.code_type = QComboBox()
- self.code_type.addItems(["C", "C++", "Python"])
- code_layout.addWidget(self.code_type)
- self.gen_code_btn = QPushButton("生成函数代码")
- self.gen_code_btn.setStyleSheet("color: #333;")
- self.gen_code_btn.clicked.connect(self.generate_code)
- code_layout.addWidget(self.gen_code_btn)
- left_layout.addLayout(code_layout)
-
- # 拟合曲线区
- self.figure = Figure(figsize=(5, 4))
- self.canvas = FigureCanvas(self.figure)
- right_layout.addWidget(self.canvas)
-
- def center(self):
- qr = self.frameGeometry()
- cp = QApplication.desktop().availableGeometry().center()
- qr.moveCenter(cp)
- self.move(qr.topLeft())
-
- def add_point_row(self, x_val="", y_val=""):
- row = self.table.rowCount()
- self.table.insertRow(row)
- self.table.setItem(row, 0, QTableWidgetItem(str(x_val)))
- self.table.setItem(row, 1, QTableWidgetItem(str(y_val)))
-
- def delete_selected_rows(self):
- selected = self.table.selectionModel().selectedRows()
- for idx in sorted(selected, reverse=True):
- self.table.removeRow(idx.row())
-
- def load_excel(self):
- file, _ = QFileDialog.getOpenFileName(self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)")
- if file:
- try:
- data = pd.read_excel(file, usecols=[0, 1])
- new_x = data.iloc[:, 0].values.tolist()
- new_y = data.iloc[:, 1].values.tolist()
- for x, y in zip(new_x, new_y):
- self.add_point_row(x, y)
- QMessageBox.information(self, "成功", "数据导入成功!")
- except Exception as e:
- QMessageBox.critical(self, "错误", f"读取Excel失败: {e}")
-
- def export_excel_and_plot(self):
- file, _ = QFileDialog.getSaveFileName(self, "导出Excel文件", "", "Excel Files (*.xlsx *.xls)")
- if file:
- x_list, y_list = [], []
- for row in range(self.table.rowCount()):
- try:
- x = float(self.table.item(row, 0).text())
- y = float(self.table.item(row, 1).text())
- x_list.append(x)
- y_list.append(y)
- except Exception:
- continue
- if not x_list or not y_list:
- QMessageBox.warning(self, "导出失败", "没有可导出的数据!")
- return
- df = pd.DataFrame({'x': x_list, 'y': y_list})
- try:
- df.to_excel(file, index=False)
- # 导出同名png图像
- png_file = file
- if png_file.lower().endswith('.xlsx') or png_file.lower().endswith('.xls'):
- png_file = png_file.rsplit('.', 1)[0] + '.png'
- else:
- png_file = png_file + '.png'
- self.figure.savefig(png_file, dpi=150, bbox_inches='tight')
- QMessageBox.information(self, "导出成功", f"数据已成功导出到Excel文件!\n图像已导出为:{png_file}")
- except Exception as e:
- QMessageBox.critical(self, "导出错误", f"导出Excel或图像失败: {e}")
-
- def get_manual_points(self):
- x_list, y_list = [], []
- for row in range(self.table.rowCount()):
- try:
- x = float(self.table.item(row, 0).text())
- y = float(self.table.item(row, 1).text())
- x_list.append(x)
- y_list.append(y)
- except Exception:
- continue
- return x_list, y_list
-
- def fit_and_plot(self):
- matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
- matplotlib.rcParams['axes.unicode_minus'] = False
- matplotlib.rcParams['font.size'] = 14
- self.data_x, self.data_y = self.get_manual_points()
- try:
- order = int(self.order_spin.value())
- except ValueError:
- QMessageBox.warning(self, "输入错误", "阶数必须为整数!")
- return
- n_points = len(self.data_x)
- if n_points < order + 1:
- QMessageBox.warning(self, "数据不足", "数据点数量不足以拟合该阶多项式!")
- return
- x = np.array(self.data_x, dtype=np.float64)
- y = np.array(self.data_y, dtype=np.float64)
- x_min, x_max = x.min(), x.max()
- if x_max - x_min == 0:
- QMessageBox.warning(self, "数据错误", "所有x值都相同,无法拟合!")
- return
- try:
- coeffs = np.polyfit(x, y, order)
- except Exception as e:
- QMessageBox.critical(self, "拟合错误", f"多项式拟合失败:{e}")
- return
- poly = np.poly1d(coeffs)
- expr = "y = " + " + ".join([f"{c:.6g}*x^{order-i}" for i, c in enumerate(coeffs)])
- self.output.setPlainText(f"{expr}\n")
- self.figure.clear()
- ax = self.figure.add_subplot(111)
- ax.scatter(x, y, color='red', label='数据点')
- x_fit = np.linspace(x_min, x_max, 200)
- y_fit = poly(x_fit)
- ax.plot(x_fit, y_fit, label='拟合曲线')
- ax.legend()
- self.canvas.draw()
- self.last_coeffs = coeffs
- self.last_xmin = x_min
- self.last_xmax = x_max
-
- def generate_code(self):
- if self.last_coeffs is None:
- QMessageBox.warning(self, "未拟合", "请先拟合数据!")
- return
- coeffs = self.last_coeffs
- code_type = self.code_type.currentText()
- if code_type == "C":
- code = self.create_c_function(coeffs)
- elif code_type == "C++":
- code = self.create_cpp_function(coeffs)
- else:
- code = self.create_py_function(coeffs)
- self.output.setPlainText(code)
-
- def create_c_function(self, coeffs):
- lines = ["#include ", "double polynomial(double x) {", " return "]
- n = len(coeffs)
- terms = []
- for i, c in enumerate(coeffs):
- exp = n - i - 1
- if exp == 0:
- terms.append(f"{c:.8g}")
- elif exp == 1:
- terms.append(f"{c:.8g}*x")
- else:
- terms.append(f"{c:.8g}*pow(x,{exp})")
- lines[-1] += " + ".join(terms) + ";"
- lines.append("}")
- return "\n".join(lines)
-
- def create_cpp_function(self, coeffs):
- lines = ["#include ", "double polynomial(double x) {", " return "]
- n = len(coeffs)
- terms = []
- for i, c in enumerate(coeffs):
- exp = n - i - 1
- if exp == 0:
- terms.append(f"{c:.8g}")
- elif exp == 1:
- terms.append(f"{c:.8g}*x")
- else:
- terms.append(f"{c:.8g}*pow(x,{exp})")
- lines[-1] += " + ".join(terms) + ";"
- lines.append("}")
- return "\n".join(lines)
-
- def create_py_function(self, coeffs):
- n = len(coeffs)
- lines = ["def polynomial(x):", " return "]
- terms = []
- for i, c in enumerate(coeffs):
- exp = n - i - 1
- if exp == 0:
- terms.append(f"{c:.8g}")
- elif exp == 1:
- terms.append(f"{c:.8g}*x")
- else:
- terms.append(f"{c:.8g}*x**{exp}")
- lines[-1] += " + ".join(terms)
- return "\n".join(lines)
-
-if __name__ == "__main__":
- app = QApplication(sys.argv)
- win = PolyFitApp()
- win.show()
- sys.exit(app.exec_())
\ No newline at end of file
diff --git a/src/freertos.c b/src/freertos.c
deleted file mode 100644
index c7fefe5..0000000
--- a/src/freertos.c
+++ /dev/null
@@ -1,131 +0,0 @@
-/* USER CODE BEGIN Header */
-/**
- ******************************************************************************
- * File Name : freertos.c
- * Description : Code for freertos applications
- ******************************************************************************
- * @attention
- *
- * Copyright (c) 2025 STMicroelectronics.
- * All rights reserved.
- *
- * This software is licensed under terms that can be found in the LICENSE file
- * in the root directory of this software component.
- * If no LICENSE file comes with this software, it is provided AS-IS.
- *
- ******************************************************************************
- */
-/* USER CODE END Header */
-
-/* Includes ------------------------------------------------------------------*/
-#include "FreeRTOS.h"
-#include "task.h"
-#include "main.h"
-#include "cmsis_os.h"
-
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
-#include "task/user_task.h"
-/* USER CODE END Includes */
-
-/* Private typedef -----------------------------------------------------------*/
-/* USER CODE BEGIN PTD */
-
-/* USER CODE END PTD */
-
-/* Private define ------------------------------------------------------------*/
-/* USER CODE BEGIN PD */
-
-/* USER CODE END PD */
-
-/* Private macro -------------------------------------------------------------*/
-/* USER CODE BEGIN PM */
-
-/* USER CODE END PM */
-
-/* Private variables ---------------------------------------------------------*/
-/* USER CODE BEGIN Variables */
-osThreadId_t initTaskHandle; // 定义 Task_Init 的任务句柄
-/* USER CODE END Variables */
-/* Definitions for defaultTask */
-osThreadId_t defaultTaskHandle;
-const osThreadAttr_t defaultTask_attributes = {
- .name = "defaultTask",
- .stack_size = 128 * 4,
- .priority = (osPriority_t) osPriorityNormal,
-};
-
-/* Private function prototypes -----------------------------------------------*/
-/* USER CODE BEGIN FunctionPrototypes */
-
-/* USER CODE END FunctionPrototypes */
-
-void StartDefaultTask(void *argument);
-
-void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
-
-/**
- * @brief FreeRTOS initialization
- * @param None
- * @retval None
- */
-void MX_FREERTOS_Init(void) {
- /* USER CODE BEGIN Init */
-
- /* USER CODE END Init */
-
- /* USER CODE BEGIN RTOS_MUTEX */
- /* add mutexes, ... */
- /* USER CODE END RTOS_MUTEX */
-
- /* USER CODE BEGIN RTOS_SEMAPHORES */
- /* add semaphores, ... */
- /* USER CODE END RTOS_SEMAPHORES */
-
- /* USER CODE BEGIN RTOS_TIMERS */
- /* start timers, add new ones, ... */
- /* USER CODE END RTOS_TIMERS */
-
- /* USER CODE BEGIN RTOS_QUEUES */
- /* add queues, ... */
- /* USER CODE END RTOS_QUEUES */
-
- /* Create the thread(s) */
- /* creation of defaultTask */
- defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
-
- /* USER CODE BEGIN RTOS_THREADS */
- initTaskHandle = osThreadNew(Task_Init, NULL, &attr_init); // 创建初始化任务
- /* add threads, ... */
- /* USER CODE END RTOS_THREADS */
-
- /* USER CODE BEGIN RTOS_EVENTS */
- /* add events, ... */
- /* USER CODE END RTOS_EVENTS */
-
-}
-
-/* USER CODE BEGIN Header_StartDefaultTask */
-/**
- * @brief Function implementing the defaultTask thread.
- * @param argument: Not used
- * @retval None
- */
-/* USER CODE END Header_StartDefaultTask */
-void StartDefaultTask(void *argument)
-{
- /* USER CODE BEGIN StartDefaultTask */
- /* Infinite loop */
- // for(;;)
- // {
- // osDelay(1);
- // }
- osThreadTerminate(osThreadGetId()); // 结束自身
- /* USER CODE END StartDefaultTask */
-}
-
-/* Private application code --------------------------------------------------*/
-/* USER CODE BEGIN Application */
-
-/* USER CODE END Application */
-