diff --git a/LICENSE b/LICENSE deleted file mode 100644 index cb386d0..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 zucheng Lv - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. 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.py b/MRobot.py deleted file mode 100644 index 7b103fd..0000000 --- a/MRobot.py +++ /dev/null @@ -1,673 +0,0 @@ -import sys -import os -import serial -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, - QFileDialog, QMessageBox, QStackedLayout -) - -from qfluentwidgets import ( - Theme, setTheme, FluentIcon, SwitchButton, BodyLabel, SubtitleLabel, - StrongBodyLabel, HorizontalSeparator, InfoBar, MessageDialog, Dialog, - AvatarWidget, NavigationItemPosition, FluentWindow, NavigationAvatarWidget, - 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 qfluentwidgets import ( - TreeWidget, InfoBar, InfoBarPosition, MessageDialog, TreeItemDelegate -) -from qfluentwidgets import CheckBox -from qfluentwidgets import TreeWidget -from PyQt5.QtCore import QThread, pyqtSignal -from PyQt5.QtWidgets import QFileDialog -from qfluentwidgets import ProgressBar - -# ===================== 页面基类 ===================== -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() - layout.setContentsMargins(60, 60, 60, 60) - layout.setSpacing(32) - self.setLayout(layout) - - # 顶部logo和欢迎区 - top_layout = QHBoxLayout() - 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) - - layout.addWidget(HorizontalSeparator()) - - # 项目简介 - layout.addWidget(BodyLabel( - "MRobot Toolbox 是一款集成化的机器人开发辅助工具," - "支持代码生成、串口终端、主题切换等多种实用功能。\n" - "点击左侧导航栏可快速切换各功能页面。" - )) - - # 开发者与项目目标 - layout.addWidget(HorizontalSeparator()) - layout.addWidget(SubtitleLabel("开发者与项目目标")) - layout.addWidget(BodyLabel("开发团队:QUT 青岛理工大学 MOVE 战队")) - layout.addWidget(BodyLabel("项目目标:为所有 rmer 和 rcer 提供现代化、简单、高效的机器人开发方式," - "让机器人开发变得更轻松、更智能。")) - layout.addWidget(BodyLabel("适用于 RM、RC、各类嵌入式机器人项目。")) - - # layout.addStretch() - -# ===================== 代码生成页面 ===================== -class DataInterface(BaseInterface): - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setObjectName("dataInterface") - self.stacked_layout = QStackedLayout() - 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("选择工程路径") - self.choose_btn.clicked.connect(self.choose_project_folder) - select_layout.addWidget(self.choose_btn) - select_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) - - def choose_project_folder(self): - folder = QFileDialog.getExistingDirectory(self, "请选择代码项目文件夹") - if not folder: - return - ioc_files = [f for f in os.listdir(folder) if f.endswith('.ioc')] - if not ioc_files: - QMessageBox.warning(self, "提示", "未找到.ioc文件,请确认项目文件夹。") - return - self.project_path = folder - self.project_name = os.path.basename(folder) - self.ioc_file = os.path.join(folder, ioc_files[0]) - 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.stacked_layout.setCurrentWidget(self.config_widget) - - def back_to_select(self): - self.stacked_layout.setCurrentWidget(self.select_widget) - -# ===================== 串口终端界面 ===================== -class SerialReadThread(QThread): - data_received = pyqtSignal(str) - - def __init__(self, ser): - super().__init__() - self.ser = ser - self._running = True - - def run(self): - while self._running: - if self.ser and self.ser.is_open and self.ser.in_waiting: - try: - data = self.ser.readline().decode(errors='ignore') - self.data_received.emit(data) - except Exception: - pass - - def stop(self): - self._running = False - self.wait() - -class SerialTerminalInterface(BaseInterface): - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setObjectName("serialTerminalInterface") - main_layout = QVBoxLayout(self) - main_layout.setSpacing(12) - - # 顶部:串口设置区 - top_hbox = QHBoxLayout() - top_hbox.addWidget(BodyLabel("串口:")) - self.port_combo = ComboBox() - self.refresh_ports() - top_hbox.addWidget(self.port_combo) - top_hbox.addWidget(BodyLabel("波特率:")) - self.baud_combo = ComboBox() - self.baud_combo.addItems(['9600', '115200', '57600', '38400', '19200', '4800']) - top_hbox.addWidget(self.baud_combo) - self.connect_btn = PushButton("连接") - self.connect_btn.clicked.connect(self.toggle_connection) - top_hbox.addWidget(self.connect_btn) - self.refresh_btn = PushButton(FluentIcon.SYNC, "刷新") - self.refresh_btn.clicked.connect(self.refresh_ports) - top_hbox.addWidget(self.refresh_btn) - top_hbox.addStretch() - main_layout.addLayout(top_hbox) - - main_layout.addWidget(HorizontalSeparator()) - - # 中部:左侧预设命令,右侧显示区 - center_hbox = QHBoxLayout() - # 左侧:预设命令竖排 - preset_vbox = QVBoxLayout() - preset_vbox.addWidget(SubtitleLabel("快捷指令")) - #快捷指令居中 - preset_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.preset_commands = [ - ("线程监视器", "RESET"), - ("陀螺仪校准", "GET_VERSION"), - ("性能监视", "START"), - ("重启", "STOP"), - ("显示所有设备", "SELF_TEST"), - ("查询id", "STATUS"), - ] - for label, cmd in self.preset_commands: - btn = PushButton(label) - btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - btn.clicked.connect(lambda _, c=cmd: self.send_preset_command(c)) - preset_vbox.addWidget(btn) - preset_vbox.addStretch() - main_layout.addLayout(center_hbox, stretch=1) - - - # 右侧:串口数据显示区 - self.text_edit = TextEdit() - self.text_edit.setReadOnly(True) - self.text_edit.setMinimumWidth(400) - center_hbox.addWidget(self.text_edit, 3) - center_hbox.addLayout(preset_vbox, 1) - - main_layout.addWidget(HorizontalSeparator()) - - # 底部:输入区 - bottom_hbox = QHBoxLayout() - self.input_line = LineEdit() - self.input_line.setPlaceholderText("输入内容,回车发送") - self.input_line.returnPressed.connect(self.send_data) - bottom_hbox.addWidget(self.input_line, 4) - 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.setChecked(True) - bottom_hbox.addWidget(self.auto_enter_checkbox) - bottom_hbox.addStretch() - main_layout.addLayout(bottom_hbox) - - self.ser = None - self.read_thread = None - - def send_preset_command(self, cmd): - self.input_line.setText(cmd) - self.send_data() - - def refresh_ports(self): - self.port_combo.clear() - ports = serial.tools.list_ports.comports() - for port in ports: - self.port_combo.addItem(port.device) - - def toggle_connection(self): - if self.ser and self.ser.is_open: - self.disconnect_serial() - else: - self.connect_serial() - - def connect_serial(self): - port = self.port_combo.currentText() - baud = int(self.baud_combo.currentText()) - try: - self.ser = serial.Serial(port, baud, timeout=0.1) - self.connect_btn.setText("断开") - self.text_edit.append(f"已连接到 {port} @ {baud}") - self.read_thread = SerialReadThread(self.ser) - self.read_thread.data_received.connect(self.display_data) - self.read_thread.start() - except Exception as e: - self.text_edit.append(f"连接失败: {e}") - - def disconnect_serial(self): - if self.read_thread: - self.read_thread.stop() - self.read_thread = None - if self.ser: - self.ser.close() - self.ser = None - self.connect_btn.setText("连接") - self.text_edit.append("已断开连接") - - def display_data(self, data): - self.text_edit.moveCursor(QTextCursor.End) - self.text_edit.insertPlainText(data) - self.text_edit.moveCursor(QTextCursor.End) - - def send_data(self): - if self.ser and self.ser.is_open: - text = self.input_line.text() - try: - if not text: - self.ser.write('\n'.encode()) - else: - for char in text: - self.ser.write(char.encode()) - # 判断是否自动回车 - if self.auto_enter_checkbox.isChecked(): - self.ser.write('\n'.encode()) - except Exception as e: - self.text_edit.append(f"发送失败: {e}") - self.input_line.clear() - - -# ===================== 零件库页面 ===================== - -# ...existing code... -class DownloadThread(QThread): - progressChanged = pyqtSignal(int) - finished = pyqtSignal(list, list) # success, fail - - def __init__(self, files, server_url, secret_key, local_dir, parent=None): - super().__init__(parent) - self.files = files - self.server_url = server_url - self.secret_key = secret_key - self.local_dir = local_dir - - def run(self): - success, fail = [], [] - total = len(self.files) - max_retry = 3 # 最大重试次数 - for idx, rel_path in enumerate(self.files): - retry = 0 - while retry < max_retry: - try: - url = f"{self.server_url}/download/{rel_path}" - params = {"key": self.secret_key} - resp = requests.get(url, params=params, stream=True, timeout=10) - if resp.status_code == 200: - local_path = os.path.join(self.local_dir, rel_path) - os.makedirs(os.path.dirname(local_path), exist_ok=True) - with open(local_path, "wb") as f: - shutil.copyfileobj(resp.raw, f) - success.append(rel_path) - break # 下载成功,跳出重试循环 - else: - print(f"下载失败({resp.status_code}): {rel_path},第{retry+1}次尝试") - retry += 1 - except Exception as e: - print(f"下载异常: {rel_path},第{retry+1}次尝试,错误: {e}") - retry += 1 - else: - fail.append(rel_path) - self.progressChanged.emit(int((idx + 1) / total * 100)) - self.finished.emit(success, fail) - - -class PartLibraryInterface(BaseInterface): - SERVER_URL = "http://154.37.215.220:5000" - SECRET_KEY = "MRobot_Download" - LOCAL_LIB_DIR = "mech_lib" - - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setObjectName("partLibraryInterface") - layout = QVBoxLayout(self) - layout.setSpacing(16) - - layout.addWidget(SubtitleLabel("零件库(在线bate版)")) - layout.addWidget(HorizontalSeparator()) - layout.addWidget(BodyLabel("可浏览服务器零件库,选择需要的文件下载到本地。(如无法使用或者下载失败,请尝试重新下载或检查网络连接)")) - - btn_layout = QHBoxLayout() - refresh_btn = PushButton(FluentIcon.SYNC, "刷新列表") - refresh_btn.clicked.connect(self.refresh_list) - btn_layout.addWidget(refresh_btn) - - # 新增:打开本地零件库按钮 - open_local_btn = PushButton(FluentIcon.FOLDER, "打开本地零件库") - open_local_btn.clicked.connect(self.open_local_lib) - btn_layout.addWidget(open_local_btn) - btn_layout.addStretch() - layout.addLayout(btn_layout) - - self.tree = TreeWidget(self) - - self.tree.setHeaderLabels(["名称", "类型"]) - self.tree.setSelectionMode(self.tree.ExtendedSelection) - self.tree.header().setSectionResizeMode(0, QHeaderView.Stretch) - self.tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) - self.tree.setCheckedColor("#0078d4", "#2d7d9a") - self.tree.setBorderRadius(8) - self.tree.setBorderVisible(True) - layout.addWidget(self.tree, stretch=1) - - download_btn = PushButton(FluentIcon.DOWNLOAD, "下载选中文件") - download_btn.clicked.connect(self.download_selected_files) - layout.addWidget(download_btn) - - self.refresh_list(first=True) - - def refresh_list(self, first=False): - self.tree.clear() - try: - resp = requests.get( - f"{self.SERVER_URL}/list", - params={"key": self.SECRET_KEY}, - timeout=5 - ) - resp.raise_for_status() - tree = resp.json() - self.populate_tree(self.tree, tree, "") - if not first: - InfoBar.success( - title="刷新成功", - content="零件库已经是最新的!", - parent=self, - position=InfoBarPosition.TOP, - duration=2000 - ) - except Exception as e: - InfoBar.error( - title="刷新失败", - content=f"获取零件库失败: {e}", - parent=self, - position=InfoBarPosition.TOP, - duration=3000 - ) - - def populate_tree(self, parent, node, path_prefix): - from PyQt5.QtWidgets import QTreeWidgetItem - for dname, dnode in node.get("dirs", {}).items(): - item = QTreeWidgetItem([dname, "文件夹"]) - if isinstance(parent, TreeWidget): - parent.addTopLevelItem(item) - else: - parent.addChild(item) - self.populate_tree(item, dnode, os.path.join(path_prefix, dname)) - for fname in node.get("files", []): - item = QTreeWidgetItem([fname, "文件"]) - item.setFlags(item.flags() | Qt.ItemIsUserCheckable) - item.setCheckState(0, Qt.Unchecked) - item.setData(0, Qt.UserRole, os.path.join(path_prefix, fname)) - if isinstance(parent, TreeWidget): - parent.addTopLevelItem(item) - else: - parent.addChild(item) - - def get_checked_files(self): - files = [] - def _traverse(item): - for i in range(item.childCount()): - child = item.child(i) - if child.text(1) == "文件" and child.checkState(0) == Qt.Checked: - files.append(child.data(0, Qt.UserRole)) - _traverse(child) - root = self.tree.invisibleRootItem() - for i in range(root.childCount()): - _traverse(root.child(i)) - return files - - def download_selected_files(self): - files = self.get_checked_files() - if not files: - InfoBar.info( - title="提示", - content="请先勾选要下载的文件。", - parent=self, - position=InfoBarPosition.TOP, - duration=2000 - ) - return - - # 进度条对话框 - self.progress_dialog = Dialog( - title="正在下载", - content="正在下载选中文件,请稍候...", - parent=self - ) - self.progress_bar = ProgressBar() - self.progress_bar.setValue(0) - # 插入进度条到内容布局 - self.progress_dialog.textLayout.addWidget(self.progress_bar) - self.progress_dialog.show() - - # 启动下载线程 - self.download_thread = DownloadThread( - files, self.SERVER_URL, self.SECRET_KEY, self.LOCAL_LIB_DIR - ) - self.download_thread.progressChanged.connect(self.progress_bar.setValue) - self.download_thread.finished.connect(self.on_download_finished) - self.download_thread.finished.connect(self.download_thread.deleteLater) - self.download_thread.start() - - def on_download_finished(self, success, fail): - self.progress_dialog.close() - msg = f"成功下载: {len(success)} 个文件\n失败: {len(fail)} 个文件" - dialog = Dialog( - title="下载结果", - content=msg, - parent=self - ) - # 添加“打开文件夹”按钮 - open_btn = PushButton("打开文件夹") - def open_folder(): - folder = os.path.abspath(self.LOCAL_LIB_DIR) - # 打开文件夹(macOS用open,Windows用explorer,Linux用xdg-open) - import platform, subprocess - if platform.system() == "Darwin": - subprocess.call(["open", folder]) - elif platform.system() == "Windows": - subprocess.call(["explorer", folder]) - else: - subprocess.call(["xdg-open", folder]) - dialog.close() - open_btn.clicked.connect(open_folder) - # 添加按钮到Dialog布局 - dialog.textLayout.addWidget(open_btn) - dialog.exec() - - def open_local_lib(self): - folder = os.path.abspath(self.LOCAL_LIB_DIR) - import platform, subprocess - if platform.system() == "Darwin": - subprocess.call(["open", folder]) - elif platform.system() == "Windows": - subprocess.call(["explorer", folder]) - else: - subprocess.call(["xdg-open", folder]) - -# ===================== 设置界面 ===================== -class SettingInterface(BaseInterface): - themeSwitchRequested = pyqtSignal() - - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setObjectName("settingInterface") - layout = QVBoxLayout() - self.setLayout(layout) - - # 标题 - layout.addSpacing(10) - layout.addWidget(SubtitleLabel("设置中心")) - layout.addSpacing(10) - layout.addWidget(HorizontalSeparator()) - - # 主题切换区域 - theme_title = StrongBodyLabel("外观设置") - theme_desc = BodyLabel("切换夜间/白天模式,适应不同环境。") - theme_desc.setWordWrap(True) - layout.addSpacing(10) - layout.addWidget(theme_title) - layout.addWidget(theme_desc) - - theme_box = QHBoxLayout() - self.theme_label = BodyLabel("夜间模式") - self.theme_switch = SwitchButton() - self.theme_switch.setChecked(Theme.DARK == Theme.DARK) - self.theme_switch.checkedChanged.connect(self.on_theme_switch) - theme_box.addWidget(self.theme_label) - theme_box.addWidget(self.theme_switch) - theme_box.addStretch() - layout.addLayout(theme_box) - - layout.addSpacing(15) - layout.addWidget(HorizontalSeparator()) - - # 其它设置区域(示例) - other_title = StrongBodyLabel("其它设置") - other_desc = BodyLabel("更多功能正在开发中,敬请期待。") - other_desc.setWordWrap(True) - layout.addSpacing(10) - layout.addWidget(other_title) - layout.addWidget(other_desc) - - # 版权信息 - layout.addStretch() - copyright_label = BodyLabel("© 2025 MRobot Toolbox") - copyright_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) - layout.addWidget(copyright_label) - layout.addSpacing(10) - - def on_theme_switch(self, checked): - self.themeSwitchRequested.emit() - -# ===================== 帮助与关于界面 ===================== -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) - - # 记录当前主题 - self.current_theme = Theme.DARK - - # 创建页面实例 - 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), - ] - self.initNavigation() - - 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.show_user_info, # 这里改为 self.show_user_info - position=NavigationItemPosition.BOTTOM - ) - - def toggle_theme(self): - # 切换主题 - if self.current_theme == Theme.DARK: - self.current_theme = Theme.LIGHT - else: - self.current_theme = Theme.DARK - setTheme(self.current_theme) - # 同步设置界面按钮状态 - self.setting_page.theme_switch.setChecked(self.current_theme == Theme.DARK) - - def show_user_info(self): - dialog = Dialog( - title="用户信息", - content="用户:MRobot至尊VIP用户", - parent=self - ) - dialog.exec() - -# ===================== 程序入口 ===================== -def main(): - app = QApplication(sys.argv) - setTheme(Theme.DARK) - window = MainWindow() - window.show() - sys.exit(app.exec_()) - -if __name__ == '__main__': - main() \ No newline at end of file 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 deleted file mode 100644 index f1f246a..0000000 --- a/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# MRobot - -更加高效快捷的 STM32 开发架构,诞生于 Robocon 和 Robomaster,但绝不仅限于此。 - -
- MRobot Logo -

是时候使用更简洁的方式开发单片机了

-

- -

-
- ---- - -## 引言 - -提起嵌入式开发,绝大多数人对每次繁琐的配置,以及查阅各种文档来写东西感到非常枯燥和浪费使时间,对于小形形目创建优雅的架构又比较费事,那么我们哟u没有办法快速完成基础环境的搭建后直接开始写逻辑代码呢? - -这就是**MRobot**。 - - - ---- - -## 获取源代码 - -(此处可补充获取代码的具体方法) - ---- - -## 主要特色 - -(此处可补充项目的主要特色) - ---- - -## 组成 - -
- 嵌入式程序层次图 -
- -- `src/bsp` -- `src/component` -- `src/device` -- `src/module` -- `src/task` - ---- - -## 应用案例 - -> **Robomaster** - -- 全向轮步兵 -- 英雄 -- 哨兵 - ---- - -## 机器人展示 - -`以上机器人均使用 MRobot 搭建` - ---- - -## 硬件支持 - -(此处可补充支持的硬件列表) - ---- - -## 图片展示 - - -## 相关依赖 - -(此处可补充项目依赖的具体内容) - ---- - -## 构建 exe - -使用以下命令构建可执行文件: - -```bash -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 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 deleted file mode 100644 index d67ede8..0000000 Binary files a/img/.DS_Store and /dev/null differ diff --git a/img/M.ico b/img/M.ico deleted file mode 100644 index 400b26d..0000000 Binary files a/img/M.ico and /dev/null differ diff --git a/img/M.png b/img/M.png deleted file mode 100644 index 300d879..0000000 Binary files a/img/M.png and /dev/null differ diff --git a/img/MR.ico b/img/MR.ico deleted file mode 100644 index ebad3d5..0000000 Binary files a/img/MR.ico and /dev/null differ diff --git a/img/MR.png b/img/MR.png deleted file mode 100644 index c546f6f..0000000 Binary files a/img/MR.png and /dev/null differ diff --git a/img/MRobot.ico b/img/MRobot.ico deleted file mode 100644 index 004d771..0000000 Binary files a/img/MRobot.ico and /dev/null differ diff --git a/img/MRobot.png b/img/MRobot.png deleted file mode 100644 index 4524089..0000000 Binary files a/img/MRobot.png and /dev/null differ diff --git a/mr_tool_img/MRobot.png b/mr_tool_img/MRobot.png deleted file mode 100644 index 4524089..0000000 Binary files a/mr_tool_img/MRobot.png and /dev/null differ diff --git a/pngico.py b/pngico.py deleted file mode 100644 index 8a74659..0000000 --- a/pngico.py +++ /dev/null @@ -1,42 +0,0 @@ -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}") - -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 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 */ - diff --git a/开发要求.md b/开发要求.md deleted file mode 100644 index 84970c1..0000000 --- a/开发要求.md +++ /dev/null @@ -1,105 +0,0 @@ -# 嵌入式 代码 - -## 软件功能介绍 - -中心思想: - -- 利用好RTOS和中断,释放CPU性能,保证实时性。 -- 一个项目适配不同型号的机器人和不同的操作手。 - -减少维护的工作量,减少出错的可能性。 - -## 依赖&环境 - -- Windows平台下用CubeMX生成项目,然后用Keil uvesrion进行编辑、烧写和调试。 - -## 使用说明 - -- 环境安装 - - [MDK-ARM](https://www.keil.com/) (必备) - - [STM32CubeMX](https://www.st.com/zh/development-tools/stm32cubemx.html) (可选) - - -- 针对不同板子需要到不同的CubeMX工程文件(DevA.ioc、DevC.ioc)。 - -- (可选)利用CubeMX生成对应的外设初始化代码和Keil工程文件。忽略CAN总线相关错误。 - - - 每次生成代码后,请利用Git丢弃Middlewares文件夹中的所有改变。原因如下。 - - 1. 使用了AC6,与CubeMX默认不匹配,会影响到FreeRTOS的移植。 - 2. 使用了比CubeMX更新的FreeRTOS版本,降版本会导致部分代码无法编译。 - - - 因为已经生成过Keil工程文件,所以只会覆盖以前生成的代码,而不会影响手写的代码。 - - - 每次生成代码后,请在HAL_InitTick函数中添加uwTickPrio = TickPriority; - -- 打开MDK-ARM中的DevC.uvprojx即可进行编辑、烧写或调试。 - -- Keil工程中有两个Target,其中Debug用来调试,不包含编译器优化等;DevC/DevA用来编译输出最终固件。 - -## 文件目录结构&文件用途说明 - -| 文件夹 | 来源 | 内容 | -| ---- | ---- | ---- | -| Core | CubeMX | 包含核心代码,外设初始化,系统初始化等 | -| Doc | 开发者 | 文档 | -| Drivers | CubeMX | CMSIS相关库、STM32 HAL | -| Image | 开发者 | 图片 | -| MDK-ARM | CubeMX | Keil uversion 项目相关文件 | -| Middlewares | 开发者 / CubeMX | 中间件 | -| USB_DEVICE | CubeMX | USB相关文件 | -| User | 开发者 | 手动编写的代码 | -| Utils | 开发者 | 使用到的工具,如CubeMonitor, Matlab | - -| User内 | 内容 | -| ---- | ---- | -| bsp | 文件夹内包含开发板信息,基于STM32 HAL对板载的外设进行控制| -| component | 包含各种组件,自成一体,相互依赖,但不依赖于其他文件夹| -| device | 独立于开发板的设备,依赖于HAL和bsp| -| module | 对机器人各模块的抽象,各模块一起组成机器人| -| task | 独立的任务,module的运行容器,也包含通信、姿态解算等 | - -## 系统介绍 - -### 硬件系统框图 - -| ![步兵嵌入式硬件框图](./Image/步兵嵌入式硬件框图.png?raw=true "步兵嵌入式硬件框图") | -|:--:| -| *步兵嵌入式硬件框图* | - -### 软件流程图 - -| ![步兵嵌入式硬件框图](./Image/嵌入式程序流程图.png?raw=true "步兵嵌入式硬件框图") | -|:--:| -| *步兵嵌入式硬件框图* | - -| ![嵌入式程序结构图](./Image/嵌入式程序结构图.png?raw=true "嵌入式程序结构图") | -|:--:| -| *嵌入式程序结构图* | - -## 原理介绍 - -### 云台控制原理 - -| ![云台控制原理(与PX类似)](./Image/云台控制原理.png?raw=true "嵌入式程序结构图") | -|:--:| -| *云台控制原理(与PX类似)* | - -### 其他参考文献 - -- 软件架构参考[PX4 Architectural Overview](https://dev.px4.io/master/en/concept/architecture.html) - -- 云台控制参考[PX4 Controller Diagrams](https://dev.px4.io/master/en/flight_stack/controller_diagrams.html) - -- 底盘Mixer和CAN的Control Group参考[PX4 Mixing and Actuators](https://dev.px4.io/master/en/concept/mixing.html) - -## TODO -- 给BSP USB print加保护,允许不同进程的使用。 - - 给所有BSP加保护 - - device.c里面加上一个Device_Init(),在里面初始化所有mutex -- CAN设备代码优化。消息解析发送方向。 - - CAN设备动态初始化,保存好几组配置。 - -## Roadmap - -1. 在步兵上完成所有功能。