0.02版本

This commit is contained in:
RB 2025-05-25 02:32:52 +08:00
parent 544b3745d5
commit c4731883f2
3 changed files with 719 additions and 270 deletions

4
.gitignore vendored
View File

@ -29,6 +29,6 @@ Examples/
!*.bin !*.bin
!*.hex !*.hex
/build build/
/dist dist/
*.spec *.spec

View File

@ -1,16 +1,4 @@
import sys import sys
import numpy as np
import pandas as pd
from PyQt5.QtWidgets import (
QApplication, QWidget, QLabel, QPushButton, QTextEdit, QVBoxLayout,
QHBoxLayout, QStackedWidget, QSizePolicy, QFrame, QGraphicsDropShadowEffect,
QSpinBox, QTableWidget, QTableWidgetItem, QFileDialog, QComboBox, QMessageBox, QHeaderView
)
from PyQt5.QtGui import QPixmap, QFont, QIcon
from PyQt5.QtCore import Qt
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import os import os
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -18,10 +6,18 @@ import requests
import webbrowser import webbrowser
import serial import serial
import serial.tools.list_ports import serial.tools.list_ports
from PyQt5.QtWidgets import QGroupBox, QGridLayout, QLineEdit, QTextBrowser from PyQt5.QtWidgets import (
from PyQt5.QtCore import QTimer, pyqtSlot QApplication, QWidget, QLabel, QPushButton, QTextEdit, QVBoxLayout,
from PyQt5.QtWidgets import QCheckBox QHBoxLayout, QStackedWidget, QSizePolicy, QFrame, QGraphicsDropShadowEffect,
from PyQt5.QtCore import QTimer 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
def resource_path(relative_path): def resource_path(relative_path):
"""兼容PyInstaller打包后资源路径""" """兼容PyInstaller打包后资源路径"""
@ -95,7 +91,6 @@ class HomePage(QWidget):
footer.setFixedHeight(100) # 修改为固定高度 footer.setFixedHeight(100) # 修改为固定高度
layout.addWidget(footer) layout.addWidget(footer)
# --------- 功能一:多项式拟合工具页面 --------- # --------- 功能一:多项式拟合工具页面 ---------
class PolyFitApp(QWidget): class PolyFitApp(QWidget):
def __init__(self): def __init__(self):
@ -591,8 +586,10 @@ class DownloadPage(QWidget):
# desc.setStyleSheet("color: #34495e; margin-bottom: 18px;") # desc.setStyleSheet("color: #34495e; margin-bottom: 18px;")
# main_layout.addWidget(desc) # main_layout.addWidget(desc)
# 两大类布局 spacer = QFrame()
from PyQt5.QtWidgets import QGridLayout, QGroupBox spacer.setFixedHeight(4) # 可根据需要调整间隔高度
spacer.setStyleSheet("background: transparent; border: none;")
main_layout.addWidget(spacer)
# 小工具类 # 小工具类
tools_tools = [ tools_tools = [
@ -661,6 +658,10 @@ class DownloadPage(QWidget):
tools_layout.addWidget(btn, row, col) tools_layout.addWidget(btn, row, col)
tools_group.setLayout(tools_layout) tools_group.setLayout(tools_layout)
main_layout.addWidget(tools_group) main_layout.addWidget(tools_group)
spacer = QFrame()
spacer.setFixedHeight(4) # 可根据需要调整间隔高度
spacer.setStyleSheet("background: transparent; border: none;")
main_layout.addWidget(spacer)
# 开发/设计软件类 # 开发/设计软件类
dev_tools = [ dev_tools = [
@ -757,8 +758,8 @@ class SerialAssistant(QWidget):
# 新增HEX模式复选框 # 新增HEX模式复选框
self.hex_send_chk = QCheckBox("HEX发送") self.hex_send_chk = QCheckBox("HEX发送")
self.hex_recv_chk = QCheckBox("HEX接收") self.hex_recv_chk = QCheckBox("HEX接收")
self.hex_send_chk.setFont(QFont("微软雅黑", 10)) self.hex_send_chk.setFont(QFont("微软雅黑", 12))
self.hex_recv_chk.setFont(QFont("微软雅黑", 10)) self.hex_recv_chk.setFont(QFont("微软雅黑", 12))
self.hex_send_chk.setChecked(False) self.hex_send_chk.setChecked(False)
self.hex_recv_chk.setChecked(False) self.hex_recv_chk.setChecked(False)
@ -821,25 +822,29 @@ class SerialAssistant(QWidget):
proto_group = QGroupBox("数据协议配置") proto_group = QGroupBox("数据协议配置")
proto_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) proto_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
proto_group.setStyleSheet(config_group.styleSheet()) proto_group.setStyleSheet(config_group.styleSheet())
proto_layout = QGridLayout() proto_layout = QHBoxLayout()
proto_layout.setSpacing(12) proto_layout.setSpacing(18)
proto_layout.setContentsMargins(16, 16, 16, 16) proto_layout.setContentsMargins(16, 16, 16, 16)
proto_layout.addWidget(QLabel("数据数量:"), 0, 0) proto_layout.addWidget(QLabel("数据数量:"))
self.data_count_spin = QSpinBox() self.data_count_spin = QSpinBox()
self.data_count_spin.setRange(1, 16) self.data_count_spin.setRange(1, 16)
self.data_count_spin.setValue(self.data_count) self.data_count_spin.setValue(self.data_count)
self.data_count_spin.setFixedWidth(80)
self.data_count_spin.valueChanged.connect(self.apply_proto_config) self.data_count_spin.valueChanged.connect(self.apply_proto_config)
proto_layout.addWidget(self.data_count_spin, 0, 1) proto_layout.addWidget(self.data_count_spin)
proto_layout.addWidget(QLabel("数据类型:"), 1, 0) proto_layout.addSpacing(18)
proto_layout.addWidget(QLabel("数据类型:"))
self.data_type_box = QComboBox() self.data_type_box = QComboBox()
self.data_type_box.addItems(self.data_types) self.data_type_box.addItems(self.data_types)
self.data_type_box.setCurrentText(self.data_type) self.data_type_box.setCurrentText(self.data_type)
self.data_type_box.setFixedWidth(100)
self.data_type_box.currentTextChanged.connect(self.apply_proto_config) self.data_type_box.currentTextChanged.connect(self.apply_proto_config)
proto_layout.addWidget(self.data_type_box, 1, 1) proto_layout.addWidget(self.data_type_box)
# proto_layout.addStretch(1)
proto_group.setLayout(proto_layout) proto_group.setLayout(proto_layout)
left_panel.addWidget(proto_group) left_panel.addWidget(proto_group)
# 发送数据区 # 发送数据区美化
send_group = QGroupBox("发送数据") send_group = QGroupBox("发送数据")
send_group.setFont(QFont("微软雅黑", 14, QFont.Bold)) send_group.setFont(QFont("微软雅黑", 14, QFont.Bold))
send_group.setStyleSheet(""" send_group.setStyleSheet("""
@ -860,77 +865,109 @@ class SerialAssistant(QWidget):
} }
""") """)
send_layout = QVBoxLayout() send_layout = QVBoxLayout()
send_layout.setSpacing(14) send_layout.setSpacing(16)
send_layout.setContentsMargins(12, 12, 12, 12) send_layout.setContentsMargins(18, 18, 18, 18)
# 输入框(更大更高字体 # 输入框(多行
self.send_edit = QLineEdit() self.send_edit = QTextEdit()
self.send_edit.setFont(QFont("Consolas", 22)) self.send_edit.setFont(QFont("Consolas", 18))
self.send_edit.setPlaceholderText("输入要发送的数据...") self.send_edit.setPlaceholderText("输入要发送的数据可多行支持HEX/文本)...")
self.send_edit.setMinimumHeight(54) self.send_edit.setMinimumHeight(140)
self.send_edit.setMinimumWidth(360) self.send_edit.setMaximumHeight(220)
self.send_edit.setStyleSheet(""" self.send_edit.setStyleSheet("""
QLineEdit { QTextEdit {
background: #f8fbfd; background: #f8fbfd;
border-radius: 10px; border-radius: 12px;
border: 1.5px solid #d6eaf8; border: 2px solid #d6eaf8;
font-size: 22px; font-size: 18px;
padding: 12px 18px; padding: 14px 20px;
} }
""") """)
send_layout.addWidget(self.send_edit) send_layout.addWidget(self.send_edit)
# HEX复选框行字体更小 # HEX复选框和按钮行
hex_chk_row = QHBoxLayout() row1 = QHBoxLayout()
self.hex_send_chk.setFont(QFont("微软雅黑", 10)) row1.setSpacing(24)
self.hex_recv_chk.setFont(QFont("微软雅黑", 10)) self.hex_send_chk.setStyleSheet("""
for chk in [self.hex_send_chk, self.hex_recv_chk]: QCheckBox {
chk.setStyleSheet(""" color: #2471a3;
QCheckBox { font-size: 15px;
color: #2471a3; }
spacing: 12px; QCheckBox::indicator {
} width: 22px;
QCheckBox::indicator { height: 22px;
width: 18px; }
height: 18px; QCheckBox::indicator:checked {
} background-color: #2980b9;
QCheckBox::indicator:checked { border: 1.5px solid #2980b9;
background-color: #2980b9; }
border: 1.5px solid #2980b9; QCheckBox::indicator:unchecked {
} background-color: #fff;
QCheckBox::indicator:unchecked { border: 1.5px solid #b5d0ea;
background-color: #fff; }
border: 1.5px solid #b5d0ea; """)
} self.hex_recv_chk.setStyleSheet(self.hex_send_chk.styleSheet())
""") row1.addWidget(self.hex_send_chk)
self.hex_send_chk.setChecked(False) row1.addWidget(self.hex_recv_chk)
self.hex_recv_chk.setChecked(False) row1.addStretch(1)
hex_chk_row.addWidget(self.hex_send_chk) send_layout.addLayout(row1)
hex_chk_row.addWidget(self.hex_recv_chk)
hex_chk_row.addStretch(1)
send_layout.addLayout(hex_chk_row)
# 发送按钮 # 发送和持续发送按钮+频率(优化为“每秒发送次数”)
send_btn_row = QHBoxLayout() row2 = QHBoxLayout()
row2.setSpacing(18)
self.send_btn = QPushButton("发送") self.send_btn = QPushButton("发送")
self.send_btn.clicked.connect(self.send_data) self.send_btn.clicked.connect(self.send_data)
self.send_btn.setFont(QFont("微软雅黑", 16, QFont.Bold)) 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")) self.send_btn.setStyleSheet(self._btn_style("#2980b9"))
send_btn_row.addWidget(self.send_btn) row2.addWidget(self.send_btn)
send_layout.addLayout(send_btn_row)
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) send_group.setLayout(send_layout)
left_panel.addWidget(send_group, stretch=1) # 让发送区弹性填充 left_panel.addWidget(send_group, stretch=1) # 让发送区弹性填充
# 清空按钮
self.clear_btn = QPushButton("清空接收和曲线")
self.clear_btn.clicked.connect(self.clear_all)
self.clear_btn.setStyleSheet(self._btn_style("#e74c3c"))
left_panel.addWidget(self.clear_btn)
# 弹性空隙(动态扩展填满)
left_panel.addStretch(1)
# 使用说明始终在最下方 # 使用说明始终在最下方
usage_group = QGroupBox("使用说明") usage_group = QGroupBox("使用说明")
usage_group.setFont(QFont("微软雅黑", 13, QFont.Bold)) usage_group.setFont(QFont("微软雅黑", 13, QFont.Bold))
@ -953,14 +990,11 @@ class SerialAssistant(QWidget):
""") """)
usage_layout = QVBoxLayout() usage_layout = QVBoxLayout()
usage_label = QLabel( usage_label = QLabel(
"1. 选择串口号和波特率,点击“打开串口”。\n" "1. 在“数据协议配置”中选择数据数量和数据类型。\n"
"2. 在“数据协议配置”中选择数据数量和数据类型。\n" "2. 下位机发送格式:\n"
"3. 下位机发送格式:\n"
" 0x55 + 数据数量(1字节) + 数据 + 校验和(1字节)\n" " 0x55 + 数据数量(1字节) + 数据 + 校验和(1字节)\n"
" 校验和为包头到最后一个数据字节的累加和的低8位。\n" " 校验和为包头到最后一个数据字节的累加和的低8位。\n"
"4. 每包数据自动绘制曲线X轴为采样点或时间Y轴为各通道数据。\n" "3. 每包数据自动绘制曲线X轴为采样点或时间Y轴为各通道数据。\n"
"5. 支持float/int16/uint16/int8/uint8类型最多16通道。\n"
"6. 可点击“测试绘图”按钮模拟数据包接收效果。"
) )
usage_label.setWordWrap(True) usage_label.setWordWrap(True)
usage_label.setFont(QFont("微软雅黑", 9)) usage_label.setFont(QFont("微软雅黑", 9))
@ -968,6 +1002,13 @@ class SerialAssistant(QWidget):
usage_group.setLayout(usage_layout) usage_group.setLayout(usage_layout)
left_panel.addWidget(usage_group) 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) main_layout.addLayout(left_panel, 0)
# 右侧面板 # 右侧面板
@ -1031,6 +1072,55 @@ class SerialAssistant(QWidget):
# 默认配置 # 默认配置
self.apply_proto_config() 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.text()
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): def simulate_data(self):
"""模拟一包数据并自动解析绘图""" """模拟一包数据并自动解析绘图"""
@ -1127,23 +1217,6 @@ class SerialAssistant(QWidget):
self.recv_box.append("串口已关闭") self.recv_box.append("串口已关闭")
self.timer.stop() self.timer.stop()
def send_data(self):
if self.ser and self.ser.is_open:
data = self.send_edit.text()
try:
if self.hex_send_chk.isChecked():
# HEX模式发送
data_bytes = bytes.fromhex(data.replace(' ', ''))
self.ser.write(data_bytes)
self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}")
else:
self.ser.write(data.encode('utf-8'))
self.recv_box.append(f"发送: {data}")
except Exception as e:
self.recv_box.append(f"发送失败: {e}")
else:
self.recv_box.append("串口未打开,无法发送。")
def send_multi_data(self): def send_multi_data(self):
if self.ser and self.ser.is_open: if self.ser and self.ser.is_open:
text = self.send_edit.text() text = self.send_edit.text()
@ -1152,9 +1225,9 @@ class SerialAssistant(QWidget):
if line.strip(): if line.strip():
try: try:
if self.hex_send_chk.isChecked(): if self.hex_send_chk.isChecked():
data_bytes = bytes.fromhex(line.strip().replace(' ', '')) data_bytes = self.parse_hex_string(line.strip())
self.ser.write(data_bytes) self.ser.write(data_bytes)
self.recv_box.append(f"发送(HEX): {data_bytes.hex(' ').upper()}") self.recv_box.append(f"发送(HEX): {' '.join(['%02X'%b for b in data_bytes])}")
else: else:
self.ser.write(line.strip().encode('utf-8')) self.ser.write(line.strip().encode('utf-8'))
self.recv_box.append(f"发送: {line.strip()}") self.recv_box.append(f"发送: {line.strip()}")
@ -1295,8 +1368,8 @@ class SerialAssistant(QWidget):
def update_plot(self): def update_plot(self):
self.figure.clear() self.figure.clear()
ax = self.figure.add_subplot(111) ax = self.figure.add_subplot(111)
ax.set_xlabel("采样点", fontsize=14) ax.set_xlabel("Sample", fontsize=14)
ax.set_ylabel("数据值", fontsize=14) ax.set_ylabel("Value", fontsize=14)
has_curve = False has_curve = False
for idx in range(self.data_count): for idx in range(self.data_count):
color = self.curve_colors[idx % len(self.curve_colors)] color = self.curve_colors[idx % len(self.curve_colors)]
@ -1317,6 +1390,8 @@ class SerialAssistant(QWidget):
# --------- 功能四MRobot架构生成 --------- # --------- 功能四MRobot架构生成 ---------
class GenerateMRobotCode(QWidget): class GenerateMRobotCode(QWidget):
repo_ready_signal = pyqtSignal()
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setFont(QFont("微软雅黑", 15)) self.setFont(QFont("微软雅黑", 15))
@ -1327,20 +1402,523 @@ class GenerateMRobotCode(QWidget):
padding: 20px; 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.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): def init_ui(self):
main_layout = QVBoxLayout(self) main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20) main_layout.setSpacing(18)
main_layout.setSpacing(20) 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;
}
""")
# 功能说明 # 顶部标题区
desc_label = QLabel("MRobot架构生成工具帮助您快速生成MRobot项目代码。") title = QLabel("MRobot 架构生成工具")
desc_label.setFont(QFont("微软雅黑", 14)) title.setFont(QFont("微软雅黑", 22, QFont.Bold))
desc_label.setAlignment(Qt.AlignCenter) title.setAlignment(Qt.AlignCenter)
main_layout.addWidget(desc_label) title.setStyleSheet("color: #2980b9; letter-spacing: 2px; margin-bottom: 2px;")
main_layout.addWidget(title)
# 其他UI元素... 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}")
# --------- 主工具箱UI --------- # --------- 主工具箱UI ---------
class ToolboxUI(QWidget): class ToolboxUI(QWidget):
@ -1399,7 +1977,7 @@ class ToolboxUI(QWidget):
left_layout.addWidget(logo_label) left_layout.addWidget(logo_label)
# 按钮区 # 按钮区
self.button_names = ["主页", "曲线拟合", "Mini串口助手", "MR架构配置","软件指南"] self.button_names = ["主页", "曲线拟合", "Mini串口助手(BUG)", "MR架构配置(开发中)","软件指南"]
self.buttons = [] self.buttons = []
for idx, name in enumerate(self.button_names): for idx, name in enumerate(self.button_names):
btn = QPushButton(name) btn = QPushButton(name)
@ -1480,6 +2058,8 @@ class ToolboxUI(QWidget):
self.output_box.append("欢迎使用 MRobot 工具箱!请选择左侧功能。") self.output_box.append("欢迎使用 MRobot 工具箱!请选择左侧功能。")
def placeholder_page(self, text): def placeholder_page(self, text):
page = QWidget() page = QWidget()
layout = QVBoxLayout(page) layout = QVBoxLayout(page)

View File

@ -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 */