mirror of
https://github.com/goldenfishs/MRobot.git
synced 2025-07-04 06:34:16 +08:00
单独的代码架构
This commit is contained in:
parent
ffad148fcc
commit
78104b724b
21
LICENSE
21
LICENSE
@ -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.
|
|
2339
MR_Tool.py
2339
MR_Tool.py
File diff suppressed because it is too large
Load Diff
673
MRobot.py
673
MRobot.py
@ -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()
|
|
719
MRobot_old.py
719
MRobot_old.py
@ -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(
|
|
||||||
"<Configure>",
|
|
||||||
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("<MouseWheel>", 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()
|
|
93
README.md
93
README.md
@ -1,93 +0,0 @@
|
|||||||
# MRobot
|
|
||||||
|
|
||||||
更加高效快捷的 STM32 开发架构,诞生于 Robocon 和 Robomaster,但绝不仅限于此。
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="./img/MRobot.png" height="100" alt="MRobot Logo">
|
|
||||||
<p>是时候使用更简洁的方式开发单片机了</p>
|
|
||||||
<p>
|
|
||||||
<!-- <img src="https://img.shields.io/github/license/xrobot-org/XRobot.svg" alt="License">
|
|
||||||
<img src="https://img.shields.io/github/repo-size/xrobot-org/XRobot.svg" alt="Repo Size">
|
|
||||||
<img src="https://img.shields.io/github/last-commit/xrobot-org/XRobot.svg" alt="Last Commit">
|
|
||||||
<img src="https://img.shields.io/badge/language-c/c++-F34B7D.svg" alt="Language"> -->
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 引言
|
|
||||||
|
|
||||||
提起嵌入式开发,绝大多数人对每次繁琐的配置,以及查阅各种文档来写东西感到非常枯燥和浪费使时间,对于小形形目创建优雅的架构又比较费事,那么我们哟u没有办法快速完成基础环境的搭建后直接开始写逻辑代码呢?
|
|
||||||
|
|
||||||
这就是**MRobot**。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 获取源代码
|
|
||||||
|
|
||||||
(此处可补充获取代码的具体方法)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 主要特色
|
|
||||||
|
|
||||||
(此处可补充项目的主要特色)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 组成
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="./image/嵌入式程序层次图.png" alt="嵌入式程序层次图">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
- `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"
|
|
168
fluentui.py
168
fluentui.py
@ -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()
|
|
BIN
img/.DS_Store
vendored
BIN
img/.DS_Store
vendored
Binary file not shown.
BIN
img/MR.ico
BIN
img/MR.ico
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
BIN
img/MR.png
BIN
img/MR.png
Binary file not shown.
Before Width: | Height: | Size: 130 KiB |
BIN
img/MRobot.ico
BIN
img/MRobot.ico
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
BIN
img/MRobot.png
BIN
img/MRobot.png
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
42
pngico.py
42
pngico.py
@ -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}")
|
|
286
polynomial.py
286
polynomial.py
@ -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 <math.h>", "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 <cmath>", "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_())
|
|
131
src/freertos.c
131
src/freertos.c
@ -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 */
|
|
||||||
|
|
105
开发要求.md
105
开发要求.md
@ -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的运行容器,也包含通信、姿态解算等 |
|
|
||||||
|
|
||||||
## 系统介绍
|
|
||||||
|
|
||||||
### 硬件系统框图
|
|
||||||
|
|
||||||
|  |
|
|
||||||
|:--:|
|
|
||||||
| *步兵嵌入式硬件框图* |
|
|
||||||
|
|
||||||
### 软件流程图
|
|
||||||
|
|
||||||
|  |
|
|
||||||
|:--:|
|
|
||||||
| *步兵嵌入式硬件框图* |
|
|
||||||
|
|
||||||
|  |
|
|
||||||
|:--:|
|
|
||||||
| *嵌入式程序结构图* |
|
|
||||||
|
|
||||||
## 原理介绍
|
|
||||||
|
|
||||||
### 云台控制原理
|
|
||||||
|
|
||||||
|  |
|
|
||||||
|:--:|
|
|
||||||
| *云台控制原理(与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. 在步兵上完成所有功能。
|
|
Loading…
Reference in New Issue
Block a user