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
!*.hex
/build
/dist
build/
dist/
*.spec

View File

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