Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2517d8b9ec | |||
| 4cf349cf14 | |||
| b7f2539321 | |||
| b03ce04217 | |||
| 17847459b4 | |||
| c3faf353e9 | |||
| 12db6ce44a | |||
| 20b6af6c34 | |||
| 05ad2226de | |||
| 9d6a10135d | |||
| 8d741c6d61 | |||
| 83aff179ee | |||
| b56d7bf8e0 | |||
| e22ed7f393 | |||
| ab3445ef6d | |||
| 09eabb804a | |||
| ab0a95b0af | |||
| b36738ecac | |||
| a91f175e9e | |||
| 5369861c88 | |||
| c13d3a5e44 | |||
| 345743a1c6 | |||
| 91eeda0e07 | |||
| d467318505 | |||
| 69c200d4c9 | |||
| 22bddbcda7 | |||
| fc94a3fa33 | |||
| 5cabd8c3b6 | |||
| 94841a02dd | |||
| 78104b724b |
35
.gitignore
vendored
@ -1,35 +0,0 @@
|
||||
*.rar
|
||||
*.o
|
||||
*.d
|
||||
*.crf
|
||||
*.htm
|
||||
*.dep
|
||||
*.map
|
||||
*.bak
|
||||
*.lnp
|
||||
*.lst
|
||||
*.ini
|
||||
*.iex
|
||||
*.sct
|
||||
*.scvd
|
||||
*.uvguix
|
||||
*.dbg*
|
||||
*.uvguix.*
|
||||
.mxproject
|
||||
|
||||
RTE/
|
||||
Templates/
|
||||
Examples/
|
||||
|
||||
!*.uvprojx
|
||||
!*.h
|
||||
!*.c
|
||||
!*.ioc
|
||||
!*.axf
|
||||
!*.bin
|
||||
!*.hex
|
||||
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
*.exe
|
||||
22
.vscode/settings.json
vendored
@ -1,22 +0,0 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"user_math.h": "c",
|
||||
"bsp.h": "c",
|
||||
"stdint.h": "c",
|
||||
"array": "c",
|
||||
"string": "c",
|
||||
"string_view": "c",
|
||||
"vector": "c",
|
||||
"can.h": "c",
|
||||
"device.h": "c",
|
||||
"gpio.h": "c",
|
||||
"uart.h": "c",
|
||||
"motor_rm.h": "c",
|
||||
"mm.h": "c",
|
||||
"capacity.h": "c",
|
||||
"error_detect.h": "c",
|
||||
"bmi088.h": "c",
|
||||
"time.h": "c",
|
||||
"motor.h": "c"
|
||||
}
|
||||
}
|
||||
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.
|
||||
18
MRobot.iss
@ -1,18 +0,0 @@
|
||||
[Setup]
|
||||
AppName=MRobot
|
||||
AppVersion=1.0.1
|
||||
DefaultDirName={userappdata}\MRobot
|
||||
DefaultGroupName=MRobot
|
||||
OutputDir=.
|
||||
OutputBaseFilename=MRobotInstaller
|
||||
|
||||
[Files]
|
||||
Source: "dist\MRobot.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "assets\logo\*"; DestDir: "{app}\assets\logo"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\User_code\*"; DestDir: "{app}\assets\User_code"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\mech_lib\*"; DestDir: "{app}\assets\mech_lib"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\logo\M.ico"; DestDir: "{app}\assets\logo"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
||||
Name: "{userdesktop}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
||||
23
MRobot.py
@ -1,23 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
# 将当前工作目录设置为程序所在的目录,确保无论从哪里执行,其工作目录都正确设置为程序本身的位置,避免路径错误。
|
||||
os.chdir(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False)else os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from app.main_window import MainWindow
|
||||
|
||||
|
||||
# 启用 DPI 缩放
|
||||
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) # 使用高 DPI 图标
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
app.setAttribute(Qt.AA_DontCreateNativeWidgetSiblings) # 避免创建原生窗口小部件的兄弟窗口
|
||||
|
||||
w = MainWindow()
|
||||
|
||||
sys.exit(app.exec_()) # 启动应用程序并进入主事件循环
|
||||
# 注意:在 PyQt5 中,exec_() 是一个阻塞调用,直到应用程序退出。
|
||||
90
README.md
@ -1,90 +0,0 @@
|
||||
# MRobot
|
||||
|
||||
更加高效快捷的机器人开发工具,诞生于 Robocon 和 Robomaster,但绝不仅限于此。
|
||||
|
||||
<div align="center">
|
||||
<img src="assets\logo\MRobot.png" height="80" alt="MRobot Logo">
|
||||
<p>
|
||||
<img src="https://img.shields.io/github/license/goldenfishs/MRobot.svg" alt="License">
|
||||
<img src="https://img.shields.io/github/repo-size/goldenfishs/MRobot.svg" alt="Repo Size">
|
||||
<img src="https://img.shields.io/github/last-commit/goldenfishs/MRobot.svg" alt="Last Commit">
|
||||
<img src="https://img.shields.io/badge/language-c/python-F34B7D.svg" alt="Language">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 引言
|
||||
|
||||
提起嵌入式开发,绝大多数人对每次繁琐的配置,以及查阅各种文档来写东西感到非常枯燥和浪费使时间,对于小形形目创建优雅的架构又比较费事,那么我们有没有办法快速完成基础环境的搭建后直接开始写逻辑代码呢?
|
||||
|
||||
这就是**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 MRobot.py --onefile --windowed --add-data "assets;assets" --add-data "app;app" --add-data "app/tools;app/tools"
|
||||
```
|
||||
@ -1,522 +0,0 @@
|
||||
import os
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
|
||||
from PyQt5.QtCore import Qt, QUrl, QTimer
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from qfluentwidgets import (
|
||||
PrimaryPushSettingCard, FluentIcon, InfoBar, InfoBarPosition,
|
||||
SubtitleLabel, BodyLabel, CaptionLabel, StrongBodyLabel,
|
||||
ElevatedCardWidget, PrimaryPushButton, PushButton,
|
||||
ProgressBar, TextEdit
|
||||
)
|
||||
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from app.tools.check_update import check_update
|
||||
from app.tools.auto_updater import AutoUpdater, check_update_availability
|
||||
from app.tools.update_check_thread import UpdateCheckThread
|
||||
|
||||
__version__ = "1.0.6"
|
||||
|
||||
class AboutInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("aboutInterface")
|
||||
|
||||
# 初始化更新相关变量
|
||||
self.updater = None
|
||||
self.update_info = None
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""设置用户界面"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
layout.setContentsMargins(20, 30, 20, 20)
|
||||
|
||||
# 版本信息卡片
|
||||
version_card = ElevatedCardWidget()
|
||||
version_layout = QVBoxLayout(version_card)
|
||||
version_layout.setContentsMargins(24, 20, 24, 20)
|
||||
|
||||
version_title = StrongBodyLabel("版本信息")
|
||||
version_layout.addWidget(version_title)
|
||||
|
||||
current_version_label = BodyLabel(f"当前版本:v{__version__}")
|
||||
version_layout.addWidget(current_version_label)
|
||||
|
||||
|
||||
layout.addWidget(version_card)
|
||||
|
||||
# 检查更新按钮
|
||||
self.check_update_card = PrimaryPushSettingCard(
|
||||
text="检查更新",
|
||||
icon=FluentIcon.SYNC,
|
||||
title="检查更新",
|
||||
content="检查是否有新版本可用(需要能够连接github)",
|
||||
)
|
||||
self.check_update_card.clicked.connect(self.check_for_updates)
|
||||
layout.addWidget(self.check_update_card)
|
||||
|
||||
# 更新信息卡片(初始隐藏)
|
||||
self.update_info_card = ElevatedCardWidget()
|
||||
self.update_info_card.hide()
|
||||
self._setup_update_info_card()
|
||||
layout.addWidget(self.update_info_card)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def _setup_update_info_card(self):
|
||||
"""设置更新信息卡片"""
|
||||
layout = QVBoxLayout(self.update_info_card)
|
||||
layout.setContentsMargins(24, 20, 24, 20)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# 标题
|
||||
self.update_title = StrongBodyLabel("发现新版本")
|
||||
layout.addWidget(self.update_title)
|
||||
|
||||
# 版本对比
|
||||
version_layout = QHBoxLayout()
|
||||
|
||||
current_layout = QVBoxLayout()
|
||||
current_layout.addWidget(CaptionLabel("当前版本"))
|
||||
self.current_version_label = SubtitleLabel(f"v{__version__}")
|
||||
current_layout.addWidget(self.current_version_label)
|
||||
|
||||
arrow_label = SubtitleLabel("→")
|
||||
arrow_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
arrow_label.setFixedWidth(30)
|
||||
|
||||
latest_layout = QVBoxLayout()
|
||||
latest_layout.addWidget(CaptionLabel("最新版本"))
|
||||
self.latest_version_label = SubtitleLabel("v--")
|
||||
latest_layout.addWidget(self.latest_version_label)
|
||||
|
||||
version_layout.addLayout(current_layout)
|
||||
version_layout.addWidget(arrow_label)
|
||||
version_layout.addLayout(latest_layout)
|
||||
|
||||
layout.addLayout(version_layout)
|
||||
|
||||
# 更新信息
|
||||
info_layout = QHBoxLayout()
|
||||
self.file_size_label = BodyLabel("文件大小: --")
|
||||
self.release_date_label = BodyLabel("发布时间: --")
|
||||
|
||||
info_layout.addWidget(self.file_size_label)
|
||||
info_layout.addStretch()
|
||||
info_layout.addWidget(self.release_date_label)
|
||||
|
||||
layout.addLayout(info_layout)
|
||||
|
||||
# 更新说明
|
||||
layout.addWidget(CaptionLabel("更新说明:"))
|
||||
|
||||
self.notes_display = TextEdit()
|
||||
self.notes_display.setReadOnly(True)
|
||||
self.notes_display.setMaximumHeight(200)
|
||||
self.notes_display.setMinimumHeight(80)
|
||||
self.notes_display.setText("暂无更新说明")
|
||||
layout.addWidget(self.notes_display)
|
||||
|
||||
# 进度条(初始隐藏)
|
||||
self.progress_widget = QWidget()
|
||||
progress_layout = QVBoxLayout(self.progress_widget)
|
||||
|
||||
self.progress_label = BodyLabel("准备更新...")
|
||||
progress_layout.addWidget(self.progress_label)
|
||||
|
||||
self.progress_bar = ProgressBar()
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
progress_layout.addWidget(self.progress_bar)
|
||||
|
||||
# 详细下载信息
|
||||
self.download_info = BodyLabel("")
|
||||
self.download_info.setWordWrap(True)
|
||||
progress_layout.addWidget(self.download_info)
|
||||
|
||||
self.progress_widget.hide()
|
||||
layout.addWidget(self.progress_widget)
|
||||
|
||||
# 按钮区域
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.manual_btn = PushButton("手动下载")
|
||||
self.manual_btn.setIcon(FluentIcon.LINK)
|
||||
self.manual_btn.clicked.connect(self.open_manual_download)
|
||||
|
||||
self.update_btn = PrimaryPushButton("开始更新")
|
||||
self.update_btn.setIcon(FluentIcon.DOWNLOAD)
|
||||
self.update_btn.clicked.connect(self.start_update)
|
||||
|
||||
self.cancel_btn = PushButton("取消")
|
||||
self.cancel_btn.clicked.connect(self.cancel_update)
|
||||
|
||||
button_layout.addWidget(self.manual_btn)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.update_btn)
|
||||
button_layout.addWidget(self.cancel_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def check_for_updates(self):
|
||||
"""检查更新"""
|
||||
self.check_update_card.setEnabled(False)
|
||||
self.check_update_card.setContent("正在检查更新...")
|
||||
|
||||
# 延迟执行检查,避免阻塞UI
|
||||
QTimer.singleShot(100, self._perform_check)
|
||||
|
||||
def _perform_check(self):
|
||||
"""执行更新检查"""
|
||||
try:
|
||||
# 获取最新版本信息(包括当前版本的详细信息)
|
||||
latest_info = self._get_latest_release_info()
|
||||
|
||||
# 检查是否有可用更新
|
||||
self.update_info = check_update_availability(__version__)
|
||||
|
||||
if self.update_info:
|
||||
self._show_update_available()
|
||||
else:
|
||||
self._show_no_update(latest_info)
|
||||
|
||||
except Exception as e:
|
||||
self._show_error(f"检查更新失败: {str(e)}")
|
||||
|
||||
def _get_latest_release_info(self):
|
||||
"""获取最新发布信息,不论版本是否需要更新"""
|
||||
try:
|
||||
import requests
|
||||
from packaging.version import parse as vparse
|
||||
|
||||
url = f"https://api.github.com/repos/goldenfishs/MRobot/releases/latest"
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
release_data = response.json()
|
||||
latest_version = release_data["tag_name"].lstrip("v")
|
||||
|
||||
# 获取下载URL和文件大小
|
||||
assets = release_data.get('assets', [])
|
||||
asset_size = 0
|
||||
download_url = None
|
||||
|
||||
if assets:
|
||||
# 选择第一个资源文件
|
||||
asset = assets[0]
|
||||
asset_size = asset.get('size', 0)
|
||||
download_url = asset.get('browser_download_url', '')
|
||||
|
||||
return {
|
||||
'version': latest_version,
|
||||
'release_notes': release_data.get('body', '暂无更新说明'),
|
||||
'release_date': release_data.get('published_at', ''),
|
||||
'asset_size': asset_size,
|
||||
'download_url': download_url
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取发布信息失败: {e}")
|
||||
return None
|
||||
|
||||
def _show_update_available(self):
|
||||
"""显示发现更新"""
|
||||
# 更新按钮状态
|
||||
self.check_update_card.setEnabled(True)
|
||||
self.check_update_card.setContent("发现新版本!")
|
||||
|
||||
# 显示更新信息卡片
|
||||
self.update_info_card.show()
|
||||
|
||||
# 设置版本信息
|
||||
if self.update_info:
|
||||
version = self.update_info.get('version', 'Unknown')
|
||||
self.latest_version_label.setText(f"v{version}")
|
||||
|
||||
# 设置文件信息
|
||||
asset_size = self.update_info.get('asset_size', 0)
|
||||
file_size = self._format_file_size(asset_size)
|
||||
self.file_size_label.setText(f"文件大小: {file_size}")
|
||||
|
||||
# 设置发布时间
|
||||
release_date = self.update_info.get('release_date', '')
|
||||
formatted_date = self._format_date(release_date)
|
||||
self.release_date_label.setText(f"发布时间: {formatted_date}")
|
||||
|
||||
# 设置更新说明
|
||||
notes = self.update_info.get('release_notes', '暂无更新说明')
|
||||
self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else ''))
|
||||
|
||||
InfoBar.success(
|
||||
title="发现新版本",
|
||||
content=f"检测到新版本 v{version}",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def _show_no_update(self, latest_info=None):
|
||||
"""显示无更新,但展示最新版本信息"""
|
||||
self.check_update_card.setEnabled(True)
|
||||
self.check_update_card.setContent("已是最新版本")
|
||||
|
||||
# 如果有最新版本信息,显示详情卡片
|
||||
if latest_info:
|
||||
self.update_info_card.show()
|
||||
|
||||
# 显示版本信息(当前版本就是最新版本)
|
||||
self.latest_version_label.setText(f"v{__version__}")
|
||||
|
||||
# 设置文件信息
|
||||
asset_size = latest_info.get('asset_size', 0)
|
||||
file_size = self._format_file_size(asset_size)
|
||||
self.file_size_label.setText(f"文件大小: {file_size}")
|
||||
|
||||
# 设置发布时间
|
||||
release_date = latest_info.get('release_date', '')
|
||||
formatted_date = self._format_date(release_date)
|
||||
self.release_date_label.setText(f"发布时间: {formatted_date}")
|
||||
|
||||
# 设置更新说明
|
||||
notes = latest_info.get('release_notes', '暂无更新说明')
|
||||
self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else ''))
|
||||
|
||||
# 修改标题和按钮
|
||||
self.update_title.setText("版本信息")
|
||||
self.update_btn.setText("手动下载")
|
||||
self.update_btn.setIcon(FluentIcon.DOWNLOAD)
|
||||
self.update_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
# 连接手动下载功能
|
||||
self.update_btn.clicked.disconnect()
|
||||
self.update_btn.clicked.connect(self.open_manual_download)
|
||||
|
||||
InfoBar.info(
|
||||
title="已是最新版本",
|
||||
content="当前已是最新版本,无需更新。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def _show_error(self, error_msg: str):
|
||||
"""显示错误"""
|
||||
self.check_update_card.setEnabled(True)
|
||||
self.check_update_card.setContent("检查更新失败")
|
||||
|
||||
InfoBar.error(
|
||||
title="检查更新失败",
|
||||
content=error_msg,
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
||||
|
||||
def start_update(self):
|
||||
"""开始更新"""
|
||||
if not self.update_info:
|
||||
return
|
||||
|
||||
# 显示进度UI
|
||||
self.progress_widget.show()
|
||||
self.update_btn.setEnabled(False)
|
||||
self.manual_btn.setEnabled(False)
|
||||
|
||||
# 启动更新器(使用简化的单线程下载)
|
||||
self.updater = AutoUpdater(__version__)
|
||||
self.updater.signals.progress_changed.connect(self.update_progress)
|
||||
self.updater.signals.download_progress.connect(self.update_download_progress)
|
||||
self.updater.signals.status_changed.connect(self.update_status)
|
||||
self.updater.signals.error_occurred.connect(self.update_error)
|
||||
self.updater.signals.update_completed.connect(self.update_completed)
|
||||
|
||||
# 开始更新流程
|
||||
self.updater.start()
|
||||
|
||||
def update_progress(self, value: int):
|
||||
"""更新进度"""
|
||||
self.progress_bar.setValue(value)
|
||||
|
||||
def update_download_progress(self, downloaded: int, total: int, speed: float, remaining: float):
|
||||
"""更新下载进度详情"""
|
||||
if total > 0:
|
||||
downloaded_str = self._format_bytes(downloaded)
|
||||
total_str = self._format_bytes(total)
|
||||
percentage = (downloaded / total) * 100
|
||||
|
||||
info_text = f"已下载: {downloaded_str} / {total_str} ({percentage:.1f}%)"
|
||||
|
||||
self.download_info.setText(info_text)
|
||||
|
||||
def _format_bytes(self, size_bytes: int) -> str:
|
||||
"""格式化字节大小"""
|
||||
if size_bytes == 0:
|
||||
return "0 B"
|
||||
|
||||
size = float(size_bytes)
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024.0:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024.0
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
def update_status(self, status: str):
|
||||
"""更新状态"""
|
||||
self.progress_label.setText(status)
|
||||
|
||||
def update_error(self, error_msg: str):
|
||||
"""更新错误"""
|
||||
self.progress_widget.hide()
|
||||
self.update_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
# 如果是平台兼容性问题,提供更友好的提示
|
||||
if "Windows 安装程序" in error_msg and "当前系统是" in error_msg:
|
||||
InfoBar.warning(
|
||||
title="平台不兼容",
|
||||
content="检测到 Windows 安装程序,请点击'手动下载'获取适合 macOS 的版本",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=6000
|
||||
)
|
||||
else:
|
||||
InfoBar.error(
|
||||
title="更新失败",
|
||||
content=error_msg,
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
||||
|
||||
def update_completed(self, file_path=None):
|
||||
"""更新完成 - 显示下载文件位置"""
|
||||
print(f"update_completed called with file_path: {file_path}") # 调试输出
|
||||
|
||||
self.progress_label.setText("下载完成!")
|
||||
self.progress_bar.setValue(100)
|
||||
|
||||
# 重新启用按钮
|
||||
self.update_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
print(f"File exists: {file_path}") # 调试输出
|
||||
InfoBar.success(
|
||||
title="下载完成",
|
||||
content="安装文件已下载完成,点击下方按钮打开文件位置",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=5000
|
||||
)
|
||||
|
||||
# 添加打开文件夹按钮
|
||||
self._add_open_folder_button(file_path)
|
||||
else:
|
||||
print(f"File does not exist or file_path is None: {file_path}") # 调试输出
|
||||
InfoBar.success(
|
||||
title="下载完成",
|
||||
content="文件下载完成",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def _add_open_folder_button(self, file_path):
|
||||
"""添加打开文件夹按钮"""
|
||||
def open_file_location():
|
||||
folder_path = os.path.dirname(file_path)
|
||||
# 在 macOS 上使用 Finder 打开文件夹
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
|
||||
|
||||
InfoBar.info(
|
||||
title="已打开文件夹",
|
||||
content=f"文件位置: {folder_path}",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
# 直接替换更新按钮的文本和功能
|
||||
self.update_btn.setText("打开文件位置")
|
||||
self.update_btn.setIcon(FluentIcon.FOLDER)
|
||||
# 断开原有连接
|
||||
self.update_btn.clicked.disconnect()
|
||||
# 连接新功能
|
||||
self.update_btn.clicked.connect(open_file_location)
|
||||
|
||||
# 修改取消按钮为清理按钮
|
||||
self.cancel_btn.setText("清理临时文件")
|
||||
self.cancel_btn.setIcon(FluentIcon.DELETE)
|
||||
self.cancel_btn.clicked.disconnect()
|
||||
|
||||
def cleanup_temp_files():
|
||||
if self.updater:
|
||||
self.updater.cleanup()
|
||||
InfoBar.success(
|
||||
title="已清理",
|
||||
content="临时文件已清理完成",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000
|
||||
)
|
||||
# 重置界面
|
||||
self.update_info_card.hide()
|
||||
self.check_update_card.setContent("检查是否有新版本可用")
|
||||
|
||||
self.cancel_btn.clicked.connect(cleanup_temp_files)
|
||||
|
||||
def cancel_update(self):
|
||||
"""取消更新"""
|
||||
if hasattr(self, 'updater') and self.updater and self.updater.isRunning():
|
||||
self.updater.cancel_update()
|
||||
self.updater.cleanup()
|
||||
|
||||
self.update_info_card.hide()
|
||||
self.check_update_card.setContent("检查是否有新版本可用")
|
||||
|
||||
def open_manual_download(self):
|
||||
"""打开手动下载页面"""
|
||||
QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest"))
|
||||
|
||||
InfoBar.info(
|
||||
title="手动下载",
|
||||
content="已为您打开下载页面",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def _restart_app(self):
|
||||
"""重启应用程序"""
|
||||
if self.updater:
|
||||
self.updater.restart_application()
|
||||
|
||||
def _format_file_size(self, size_bytes: int) -> str:
|
||||
"""格式化文件大小"""
|
||||
if size_bytes == 0:
|
||||
return "--"
|
||||
|
||||
size = float(size_bytes)
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024.0:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024.0
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
def _format_date(self, date_str: str) -> str:
|
||||
"""格式化日期"""
|
||||
if not date_str:
|
||||
return "--"
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
return date_obj.strftime('%Y-%m-%d')
|
||||
except:
|
||||
return date_str[:10] if len(date_str) >= 10 else date_str
|
||||
@ -1,182 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
|
||||
from qfluentwidgets import TextEdit, LineEdit, PushButton, TitleLabel, SubtitleLabel, FluentIcon, InfoBar, InfoBarPosition
|
||||
import requests
|
||||
import json
|
||||
|
||||
class AIWorker(QThread):
|
||||
response_signal = pyqtSignal(str)
|
||||
done_signal = pyqtSignal()
|
||||
error_signal = pyqtSignal(str) # 新增
|
||||
|
||||
def __init__(self, prompt, parent=None):
|
||||
super().__init__(parent)
|
||||
self.prompt = prompt
|
||||
|
||||
def run(self):
|
||||
url = "http://qutrobot.top:11434/api/generate"
|
||||
payload = {
|
||||
"model": "qwen3:0.6b",
|
||||
"prompt": self.prompt
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, json=payload, stream=True, timeout=60)
|
||||
got_response = False
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
got_response = True
|
||||
try:
|
||||
data = json.loads(line.decode('utf-8'))
|
||||
self.response_signal.emit(data.get("response", ""))
|
||||
if data.get("done", False):
|
||||
self.done_signal.emit()
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not got_response:
|
||||
self.error_signal.emit("服务器繁忙,请稍后再试。")
|
||||
self.done_signal.emit()
|
||||
except requests.ConnectionError:
|
||||
self.error_signal.emit("网络连接失败,请检查网络设置。")
|
||||
self.done_signal.emit()
|
||||
except Exception as e:
|
||||
self.error_signal.emit(f"[错误]: {str(e)}")
|
||||
self.done_signal.emit()
|
||||
|
||||
|
||||
class AIInterface(QWidget):
|
||||
MAX_HISTORY = 20 # 新增最大对话条数
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("aiPage")
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(20, 20, 20, 20)
|
||||
self.layout.setSpacing(10)
|
||||
|
||||
self.title = SubtitleLabel("MRobot AI小助手", self)
|
||||
self.title.setAlignment(Qt.AlignCenter)
|
||||
self.layout.addWidget(self.title)
|
||||
|
||||
self.chat_display = TextEdit(self)
|
||||
self.chat_display.setReadOnly(True)
|
||||
|
||||
self.layout.addWidget(self.chat_display, stretch=1)
|
||||
|
||||
input_layout = QHBoxLayout()
|
||||
self.input_box = LineEdit(self)
|
||||
self.input_box.setPlaceholderText("请输入你的问题...")
|
||||
input_layout.addWidget(self.input_box, stretch=1)
|
||||
|
||||
# self.send_btn = PushButton("发送", self)
|
||||
self.send_btn = PushButton("发送", icon=FluentIcon.SEND, parent=self)
|
||||
|
||||
self.send_btn.setFixedWidth(80)
|
||||
input_layout.addWidget(self.send_btn)
|
||||
|
||||
self.layout.addLayout(input_layout)
|
||||
|
||||
self.send_btn.clicked.connect(self.send_message)
|
||||
self.input_box.returnPressed.connect(self.send_message)
|
||||
|
||||
self.worker = None
|
||||
self.is_waiting = False
|
||||
self.history = []
|
||||
self.chat_display.setText(
|
||||
"<b>MRobot:</b> 欢迎使用MRobot AI小助手!"
|
||||
)
|
||||
|
||||
def send_message(self):
|
||||
if self.is_waiting:
|
||||
return
|
||||
prompt = self.input_box.text().strip()
|
||||
if not prompt:
|
||||
return
|
||||
if len(prompt) > 1000:
|
||||
InfoBar.warning(
|
||||
title='警告',
|
||||
content="每条发送内容不能超过1000字,请精简后再发送。",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.BOTTOM,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
return
|
||||
if len(self.history) >= self.MAX_HISTORY:
|
||||
InfoBar.warning(
|
||||
title='警告',
|
||||
content="对话条数已达上限,请清理历史或重新开始。",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.BOTTOM,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
return
|
||||
self.append_chat("你", prompt)
|
||||
self.input_box.clear()
|
||||
self.append_chat("MRobot", "", new_line=False)
|
||||
self.is_waiting = True
|
||||
|
||||
# 只在首次对话时加入身份提示
|
||||
if not self.history:
|
||||
system_prompt = (
|
||||
"你是MRobot,是QUT青岛理工大学机器人战队的AI机器人。"
|
||||
"请以此身份与用户进行交流。"
|
||||
)
|
||||
else:
|
||||
system_prompt = ""
|
||||
|
||||
self.history.append({"role": "user", "content": prompt})
|
||||
context = system_prompt + "\n" if system_prompt else ""
|
||||
for msg in self.history:
|
||||
if msg["role"] == "user":
|
||||
context += f"你: {msg['content']}\n"
|
||||
else:
|
||||
context += f"AI: {msg['content']}\n"
|
||||
|
||||
self.worker = AIWorker(context)
|
||||
self.worker.response_signal.connect(self.stream_response)
|
||||
self.worker.done_signal.connect(self.finish_response)
|
||||
self.worker.error_signal.connect(self.show_error) # 新增
|
||||
self.worker.start()
|
||||
|
||||
|
||||
def append_chat(self, sender, message, new_line=True):
|
||||
if new_line:
|
||||
self.chat_display.append(f"<b>{sender}:</b> {message}")
|
||||
else:
|
||||
self.chat_display.append(f"<b>{sender}:</b> ")
|
||||
self.chat_display.moveCursor(self.chat_display.textCursor().End)
|
||||
# 新增:保存AI回复到历史
|
||||
if sender == "AI" and message:
|
||||
self.history.append({"role": "ai", "content": message})
|
||||
|
||||
def stream_response(self, text):
|
||||
cursor = self.chat_display.textCursor()
|
||||
cursor.movePosition(cursor.End)
|
||||
cursor.insertText(text)
|
||||
self.chat_display.setTextCursor(cursor)
|
||||
# 新增:流式保存AI回复
|
||||
if self.history and self.history[-1]["role"] == "ai":
|
||||
self.history[-1]["content"] += text
|
||||
elif text:
|
||||
self.history.append({"role": "ai", "content": text})
|
||||
|
||||
def finish_response(self):
|
||||
self.chat_display.append("") # 换行
|
||||
self.is_waiting = False
|
||||
|
||||
def show_error(self, msg): # 新增
|
||||
InfoBar.error(
|
||||
title='失败',
|
||||
content=msg,
|
||||
orient=Qt.Vertical,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
self.is_waiting = False
|
||||
@ -1,173 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QSizePolicy
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
|
||||
from qfluentwidgets import TitleLabel, BodyLabel, PushButton, FluentIcon
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
import os
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from .ai_interface import AIInterface
|
||||
from qfluentwidgets import InfoBar
|
||||
from .tools.update_code import update_code
|
||||
from .code_generate_interface import CodeGenerateInterface
|
||||
|
||||
class CodeConfigurationInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("CodeConfigurationInterface")
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setAlignment(Qt.AlignTop)
|
||||
self.vBoxLayout.setContentsMargins(10, 0, 10, 10) # 设置外边距
|
||||
|
||||
# 顶部标签栏,横向拉伸
|
||||
self.tabBar = TabBar(self)
|
||||
self.tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.vBoxLayout.addWidget(self.tabBar)
|
||||
|
||||
self.stackedWidget = QStackedWidget(self)
|
||||
self.vBoxLayout.addWidget(self.stackedWidget)
|
||||
|
||||
# 初始主页面
|
||||
self.mainPage = QWidget(self)
|
||||
mainLayout = QVBoxLayout(self.mainPage)
|
||||
mainLayout.setAlignment(Qt.AlignTop)
|
||||
mainLayout.setSpacing(28) # 设置间距
|
||||
mainLayout.setContentsMargins(48, 48, 48, 48) # 设置内容边距
|
||||
|
||||
#添加空行
|
||||
title = TitleLabel("MRobot 代码生成")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
mainLayout.addWidget(title)
|
||||
|
||||
subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径(.ico所在的目录),然后开启代码之旅!")
|
||||
subtitle.setAlignment(Qt.AlignCenter)
|
||||
mainLayout.addWidget(subtitle)
|
||||
|
||||
desc = BodyLabel("支持自动配置和生成任务,自主选择模块代码倒入,自动识别cubemx配置!")
|
||||
desc.setAlignment(Qt.AlignCenter)
|
||||
mainLayout.addWidget(desc)
|
||||
|
||||
mainLayout.addSpacing(18)
|
||||
|
||||
self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径")
|
||||
self.choose_btn.setFixedWidth(200)
|
||||
mainLayout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
|
||||
self.update_template_btn.setFixedWidth(200)
|
||||
mainLayout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
mainLayout.addSpacing(10)
|
||||
mainLayout.addStretch()
|
||||
|
||||
# 添加主页面到堆叠窗口
|
||||
self.addSubInterface(self.mainPage, "mainPage", "代码生成主页")
|
||||
|
||||
self.setLayout(self.vBoxLayout)
|
||||
|
||||
# 信号连接
|
||||
self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
|
||||
self.tabBar.tabCloseRequested.connect(self.onCloseTab)
|
||||
self.choose_btn.clicked.connect(self.choose_project_folder) # 启用选择项目路径按钮
|
||||
self.update_template_btn.clicked.connect(self.on_update_template)
|
||||
|
||||
|
||||
def on_update_template(self):
|
||||
def info(parent):
|
||||
InfoBar.success(
|
||||
title="更新成功",
|
||||
content="用户模板已更新到最新版本!",
|
||||
parent=parent,
|
||||
duration=2000
|
||||
)
|
||||
def error(parent, msg):
|
||||
InfoBar.error(
|
||||
title="更新失败",
|
||||
content=f"用户模板更新失败: {msg}",
|
||||
parent=parent,
|
||||
duration=3000
|
||||
)
|
||||
update_code(parent=self, info_callback=info, error_callback=error)
|
||||
|
||||
def choose_project_folder(self):
|
||||
folder = QFileDialog.getExistingDirectory(self, "选择CUBEMX工程目录")
|
||||
if not folder:
|
||||
return
|
||||
ioc_files = [f for f in os.listdir(folder) if f.endswith('.ioc')]
|
||||
if ioc_files:
|
||||
# 检查是否已存在 codeGenPage 标签页
|
||||
for i in range(self.stackedWidget.count()):
|
||||
widget = self.stackedWidget.widget(i)
|
||||
if widget is not None and widget.objectName() == "codeGenPage":
|
||||
# 如果已存在,则切换到该标签页,并更新路径显示
|
||||
if hasattr(widget, "project_path"):
|
||||
widget.project_path = folder
|
||||
if hasattr(widget, "refresh"):
|
||||
widget.refresh()
|
||||
self.stackedWidget.setCurrentWidget(widget)
|
||||
self.tabBar.setCurrentTab("codeGenPage")
|
||||
return
|
||||
# 不存在则新建
|
||||
code_gen_page = CodeGenerateInterface(folder, self)
|
||||
self.addSubInterface(code_gen_page, "codeGenPage", "代码生成")
|
||||
self.stackedWidget.setCurrentWidget(code_gen_page)
|
||||
self.tabBar.setCurrentTab("codeGenPage")
|
||||
else:
|
||||
InfoBar.error(
|
||||
title="未找到.ioc文件",
|
||||
content="所选文件夹不是有效的CUBEMX工程目录,请重新选择。",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
|
||||
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
|
||||
widget.setObjectName(objectName)
|
||||
self.stackedWidget.addWidget(widget)
|
||||
self.tabBar.addTab(
|
||||
routeKey=objectName,
|
||||
text=text,
|
||||
onClick=lambda: self.stackedWidget.setCurrentWidget(widget)
|
||||
)
|
||||
|
||||
def onCurrentIndexChanged(self, index):
|
||||
widget = self.stackedWidget.widget(index)
|
||||
self.tabBar.setCurrentTab(widget.objectName())
|
||||
|
||||
def onAddNewTab(self):
|
||||
pass # 可自定义添加新标签页逻辑
|
||||
|
||||
def onCloseTab(self, index: int):
|
||||
item = self.tabBar.tabItem(index)
|
||||
widget = self.findChild(QWidget, item.routeKey())
|
||||
# 禁止关闭主页
|
||||
if widget.objectName() == "mainPage":
|
||||
return
|
||||
self.stackedWidget.removeWidget(widget)
|
||||
self.tabBar.removeTab(index)
|
||||
widget.deleteLater()
|
||||
|
||||
def open_fit_tab(self):
|
||||
# 检查是否已存在标签页,避免重复添加
|
||||
for i in range(self.stackedWidget.count()):
|
||||
widget = self.stackedWidget.widget(i)
|
||||
if widget.objectName() == "fitPage":
|
||||
self.stackedWidget.setCurrentWidget(widget)
|
||||
self.tabBar.setCurrentTab("fitPage")
|
||||
return
|
||||
fit_page = FunctionFitInterface(self)
|
||||
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
|
||||
self.stackedWidget.setCurrentWidget(fit_page)
|
||||
self.tabBar.setCurrentTab("fitPage")
|
||||
|
||||
def open_ai_tab(self):
|
||||
# 检查是否已存在标签页,避免重复添加
|
||||
for i in range(self.stackedWidget.count()):
|
||||
widget = self.stackedWidget.widget(i)
|
||||
if widget.objectName() == "aiPage":
|
||||
self.stackedWidget.setCurrentWidget(widget)
|
||||
self.tabBar.setCurrentTab("aiPage")
|
||||
return
|
||||
ai_page = AIInterface(self)
|
||||
self.addSubInterface(ai_page, "aiPage", "AI问答")
|
||||
self.stackedWidget.setCurrentWidget(ai_page)
|
||||
self.tabBar.setCurrentTab("aiPage")
|
||||
@ -1,414 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy, QTreeWidget, QTreeWidgetItem, QStackedWidget
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import TitleLabel, BodyLabel, PushButton, TreeWidget, FluentIcon, InfoBar
|
||||
from app.tools.analyzing_ioc import analyzing_ioc
|
||||
from app.code_page.bsp_interface import bsp
|
||||
from app.data_interface import DataInterface
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
|
||||
import os
|
||||
import csv
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
class CodeGenerateInterface(QWidget):
|
||||
def __init__(self, project_path, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("CodeGenerateInterface")
|
||||
self.project_path = project_path
|
||||
|
||||
# 初始化页面缓存
|
||||
self.page_cache = {}
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setAlignment(Qt.AlignTop)
|
||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
top_layout = self._create_top_layout()
|
||||
main_layout.addLayout(top_layout)
|
||||
|
||||
content_layout = QHBoxLayout()
|
||||
content_layout.setContentsMargins(0, 10, 0, 0)
|
||||
main_layout.addLayout(content_layout)
|
||||
|
||||
# 左侧树形列表(使用qfluentwidgets的TreeWidget)
|
||||
self.tree = TreeWidget()
|
||||
self.tree.setHeaderHidden(True)
|
||||
self.tree.setMaximumWidth(250)
|
||||
self.tree.setBorderRadius(8)
|
||||
self.tree.setBorderVisible(True)
|
||||
content_layout.addWidget(self.tree)
|
||||
|
||||
# 右侧内容区
|
||||
self.stack = QStackedWidget()
|
||||
content_layout.addWidget(self.stack)
|
||||
|
||||
self._load_csv_and_build_tree()
|
||||
self.tree.itemClicked.connect(self.on_tree_item_clicked)
|
||||
|
||||
def _create_top_layout(self):
|
||||
"""创建顶部横向布局"""
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
# 项目名称标签
|
||||
project_name = os.path.basename(self.project_path)
|
||||
name_label = BodyLabel(f"项目名称: {project_name}")
|
||||
name_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
top_layout.addWidget(name_label)
|
||||
|
||||
# FreeRTOS状态标签
|
||||
freertos_label = BodyLabel(f"FreeRTOS: {self._get_freertos_status()}")
|
||||
freertos_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
top_layout.addWidget(freertos_label)
|
||||
|
||||
# 自动生成FreeRTOS任务按钮
|
||||
auto_task_btn = PushButton(FluentIcon.SEND, "配置FreeRTOS")
|
||||
auto_task_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
auto_task_btn.clicked.connect(self.on_freertos_task_btn_clicked)
|
||||
top_layout.addWidget(auto_task_btn, alignment=Qt.AlignRight)
|
||||
|
||||
# 配置并生成FreeRTOS任务按钮
|
||||
freertos_task_btn = PushButton(FluentIcon.SETTING, "创建任务")
|
||||
freertos_task_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
freertos_task_btn.clicked.connect(self.on_task_code_btn_clicked)
|
||||
top_layout.addWidget(freertos_task_btn, alignment=Qt.AlignRight)
|
||||
|
||||
# 配置cmake按钮
|
||||
cmake_btn = PushButton(FluentIcon.FOLDER, "配置cmake")
|
||||
cmake_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
cmake_btn.clicked.connect(self.on_cmake_config_btn_clicked)
|
||||
top_layout.addWidget(cmake_btn, alignment=Qt.AlignRight)
|
||||
|
||||
# 生成代码按钮
|
||||
generate_btn = PushButton(FluentIcon.PROJECTOR,"生成代码")
|
||||
generate_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
generate_btn.clicked.connect(self.generate_code)
|
||||
top_layout.addWidget(generate_btn, alignment=Qt.AlignRight)
|
||||
|
||||
return top_layout
|
||||
|
||||
def on_task_code_btn_clicked(self):
|
||||
# 检查是否开启 FreeRTOS
|
||||
ioc_files = [f for f in os.listdir(self.project_path) if f.endswith('.ioc')]
|
||||
if ioc_files:
|
||||
ioc_path = os.path.join(self.project_path, ioc_files[0])
|
||||
if not analyzing_ioc.is_freertos_enabled_from_ioc(ioc_path):
|
||||
InfoBar.error(
|
||||
title="错误",
|
||||
content="请先在 .ioc 文件中开启 FreeRTOS,再进行任务配置!",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
else:
|
||||
InfoBar.error(
|
||||
title="错误",
|
||||
content="未找到 .ioc 文件,无法检测 FreeRTOS 状态!",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
|
||||
# 直接弹出任务配置对话框并生成代码
|
||||
dlg = DataInterface()
|
||||
dlg.project_path = self.project_path
|
||||
result = dlg.open_task_config_dialog()
|
||||
# 生成任务成功后弹出 InfoBar 提示
|
||||
if getattr(dlg, "task_generate_success", False):
|
||||
InfoBar.success(
|
||||
title="任务生成成功",
|
||||
content="FreeRTOS任务代码已生成!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def on_freertos_task_btn_clicked(self):
|
||||
# 检查是否开启 FreeRTOS
|
||||
ioc_files = [f for f in os.listdir(self.project_path) if f.endswith('.ioc')]
|
||||
if ioc_files:
|
||||
ioc_path = os.path.join(self.project_path, ioc_files[0])
|
||||
if not analyzing_ioc.is_freertos_enabled_from_ioc(ioc_path):
|
||||
InfoBar.error(
|
||||
title="错误",
|
||||
content="请先在 .ioc 文件中开启 FreeRTOS,再自动生成任务!",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
else:
|
||||
InfoBar.error(
|
||||
title="错误",
|
||||
content="未找到 .ioc 文件,无法检测 FreeRTOS 状态!",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
|
||||
# 自动生成FreeRTOS任务代码
|
||||
from app.data_interface import DataInterface
|
||||
di = DataInterface()
|
||||
di.project_path = self.project_path
|
||||
di.generate_freertos_task()
|
||||
InfoBar.success(
|
||||
title="自动生成成功",
|
||||
content="FreeRTOS任务代码已自动生成!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def on_cmake_config_btn_clicked(self):
|
||||
"""配置cmake,自动更新CMakeLists.txt中的源文件列表"""
|
||||
try:
|
||||
from app.tools.update_cmake_sources import find_user_c_files, update_cmake_sources,update_cmake_includes
|
||||
from pathlib import Path
|
||||
|
||||
# 构建User目录和CMakeLists.txt路径,规范化路径分隔符
|
||||
user_dir = os.path.normpath(os.path.join(self.project_path, "User"))
|
||||
cmake_file = os.path.normpath(os.path.join(self.project_path, "CMakeLists.txt"))
|
||||
|
||||
print(f"项目路径: {self.project_path}")
|
||||
print(f"User目录路径: {user_dir}")
|
||||
print(f"CMakeLists.txt路径: {cmake_file}")
|
||||
|
||||
# 检查User目录是否存在
|
||||
if not os.path.exists(user_dir):
|
||||
InfoBar.error(
|
||||
title="错误",
|
||||
content=f"User目录不存在: {user_dir}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
|
||||
# 检查CMakeLists.txt是否存在
|
||||
if not os.path.exists(cmake_file):
|
||||
InfoBar.error(
|
||||
title="错误",
|
||||
content=f"CMakeLists.txt文件不存在: {cmake_file}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
|
||||
# 查找User目录下的所有.c文件
|
||||
print("开始查找.c文件...")
|
||||
c_files = find_user_c_files(user_dir)
|
||||
print(f"找到 {len(c_files)} 个.c文件")
|
||||
|
||||
if not c_files:
|
||||
InfoBar.warning(
|
||||
title="警告",
|
||||
content="在User目录下没有找到.c文件",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
|
||||
# 更新CMakeLists.txt
|
||||
print("开始更新CMakeLists.txt...")
|
||||
sources_success = update_cmake_sources(cmake_file, c_files)
|
||||
includes_success = update_cmake_includes(cmake_file, user_dir)
|
||||
|
||||
if sources_success and includes_success:
|
||||
InfoBar.success(
|
||||
title="配置成功",
|
||||
content=f"已成功更新CMakeLists.txt,共添加了 {len(c_files)} 个源文件",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
elif sources_success:
|
||||
InfoBar.warning(
|
||||
title="部分成功",
|
||||
content=f"源文件更新成功,但include路径更新失败",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
elif includes_success:
|
||||
InfoBar.warning(
|
||||
title="部分成功",
|
||||
content=f"include路径更新成功,但源文件更新失败",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
else:
|
||||
InfoBar.error(
|
||||
title="配置失败",
|
||||
content="更新CMakeLists.txt失败,请检查文件格式",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
except ImportError as e:
|
||||
print(f"导入错误: {e}")
|
||||
InfoBar.error(
|
||||
title="导入错误",
|
||||
content=f"无法导入cmake配置模块: {str(e)}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"cmake配置错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
InfoBar.error(
|
||||
title="配置失败",
|
||||
content=f"cmake配置过程中出现错误: {str(e)}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
|
||||
|
||||
def generate_code(self):
|
||||
"""生成所有代码,包括未加载页面"""
|
||||
try:
|
||||
# 先收集所有页面名(从CSV配置文件读取)
|
||||
from app.tools.code_generator import CodeGenerator # 在方法内重新导入确保可用
|
||||
csv_path = os.path.join(CodeGenerator.get_assets_dir("User_code"), "config.csv")
|
||||
all_class_names = []
|
||||
if os.path.exists(csv_path):
|
||||
with open(csv_path, newline='', encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
row = [cell.strip() for cell in row if cell.strip()]
|
||||
if not row:
|
||||
continue
|
||||
main_title = row[0]
|
||||
for sub in row[1:]:
|
||||
class_name = f"{main_title}_{sub}".replace("-", "_")
|
||||
all_class_names.append(class_name)
|
||||
|
||||
# 创建所有页面对象(无论是否点击过)
|
||||
bsp_pages = []
|
||||
component_pages = []
|
||||
device_pages = []
|
||||
for class_name in all_class_names:
|
||||
widget = self._get_or_create_page(class_name)
|
||||
if widget:
|
||||
if hasattr(widget, '_generate_bsp_code_internal') and widget not in bsp_pages:
|
||||
bsp_pages.append(widget)
|
||||
elif hasattr(widget, '_generate_component_code_internal') and widget not in component_pages:
|
||||
component_pages.append(widget)
|
||||
elif hasattr(widget, '_generate_device_code_internal') and widget not in device_pages:
|
||||
device_pages.append(widget)
|
||||
|
||||
# 确保导入成功
|
||||
from app.code_page.bsp_interface import bsp
|
||||
from app.code_page.component_interface import component
|
||||
from app.code_page.device_interface import device
|
||||
|
||||
# 生成 BSP 代码
|
||||
bsp_result = bsp.generate_bsp(self.project_path, bsp_pages)
|
||||
|
||||
# 生成 Component 代码
|
||||
component_result = component.generate_component(self.project_path, component_pages)
|
||||
|
||||
# 生成 Device 代码
|
||||
device_result = device.generate_device(self.project_path, device_pages)
|
||||
|
||||
# 合并结果信息
|
||||
combined_result = f"BSP代码生成:\n{bsp_result}\n\nComponent代码生成:\n{component_result}\n\nDevice代码生成:\n{device_result}"
|
||||
|
||||
InfoBar.success(
|
||||
title="代码生成结果",
|
||||
content=combined_result,
|
||||
parent=self,
|
||||
duration=5000
|
||||
)
|
||||
|
||||
except ImportError as e:
|
||||
InfoBar.error(
|
||||
title="导入错误",
|
||||
content=f"模块导入失败: {str(e)}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
except Exception as e:
|
||||
InfoBar.error(
|
||||
title="生成失败",
|
||||
content=f"代码生成过程中出现错误: {str(e)}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
|
||||
def _get_freertos_status(self):
|
||||
"""获取FreeRTOS状态"""
|
||||
ioc_files = [f for f in os.listdir(self.project_path) if f.endswith('.ioc')]
|
||||
if ioc_files:
|
||||
ioc_path = os.path.join(self.project_path, ioc_files[0])
|
||||
return "开启" if analyzing_ioc.is_freertos_enabled_from_ioc(ioc_path) else "未开启"
|
||||
return "未找到.ioc文件"
|
||||
|
||||
def _load_csv_and_build_tree(self):
|
||||
from app.tools.code_generator import CodeGenerator # 在方法内重新导入确保可用
|
||||
csv_path = os.path.join(CodeGenerator.get_assets_dir("User_code"), "config.csv")
|
||||
print(f"加载CSV路径: {csv_path}")
|
||||
if not os.path.exists(csv_path):
|
||||
print(f"配置文件未找到: {csv_path}")
|
||||
return
|
||||
self.tree.clear()
|
||||
with open(csv_path, newline='', encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
row = [cell.strip() for cell in row if cell.strip()]
|
||||
if not row:
|
||||
continue
|
||||
main_title = row[0]
|
||||
main_item = QTreeWidgetItem([main_title])
|
||||
for sub in row[1:]:
|
||||
sub_item = QTreeWidgetItem([sub])
|
||||
main_item.addChild(sub_item)
|
||||
self.tree.addTopLevelItem(main_item)
|
||||
self.tree.repaint()
|
||||
|
||||
def on_tree_item_clicked(self, item, column):
|
||||
if item.parent():
|
||||
main_title = item.parent().text(0)
|
||||
sub_title = item.text(0)
|
||||
class_name = f"{main_title}_{sub_title}".replace("-", "_")
|
||||
widget = self._get_or_create_page(class_name)
|
||||
if widget:
|
||||
self.stack.setCurrentWidget(widget)
|
||||
|
||||
def _get_or_create_page(self, class_name):
|
||||
"""获取或创建页面"""
|
||||
if class_name in self.page_cache:
|
||||
return self.page_cache[class_name]
|
||||
|
||||
# 如果是第一次创建组件页面,初始化组件管理器
|
||||
if not hasattr(self, 'component_manager'):
|
||||
from app.code_page.component_interface import ComponentManager
|
||||
self.component_manager = ComponentManager()
|
||||
|
||||
try:
|
||||
if class_name.startswith('bsp_'):
|
||||
# BSP页面
|
||||
from app.code_page.bsp_interface import get_bsp_page
|
||||
# 提取外设名,如 bsp_error_detect -> error_detect
|
||||
periph_name = class_name[len('bsp_'):] # 移除 .replace("_", " ")
|
||||
page = get_bsp_page(periph_name, self.project_path)
|
||||
elif class_name.startswith('component_'):
|
||||
from app.code_page.component_interface import get_component_page
|
||||
comp_name = class_name[len('component_'):] # 移除 .replace("_", " ")
|
||||
page = get_component_page(comp_name, self.project_path, self.component_manager)
|
||||
self.component_manager.register_component(page.component_name, page)
|
||||
elif class_name.startswith('device_'):
|
||||
# Device页面
|
||||
from app.code_page.device_interface import get_device_page
|
||||
device_name = class_name[len('device_'):] # 移除 device_ 前缀
|
||||
page = get_device_page(device_name, self.project_path)
|
||||
else:
|
||||
print(f"未知的页面类型: {class_name}")
|
||||
return None
|
||||
|
||||
self.page_cache[class_name] = page
|
||||
self.stack.addWidget(page)
|
||||
return page
|
||||
except Exception as e:
|
||||
print(f"创建页面 {class_name} 失败: {e}")
|
||||
return None
|
||||
@ -1,372 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QCheckBox, QComboBox, QTableWidget, QHeaderView, QMessageBox, QHBoxLayout, QTextEdit
|
||||
from qfluentwidgets import TitleLabel, BodyLabel, PushButton, CheckBox, TableWidget, LineEdit, ComboBox, MessageBox, SubtitleLabel, FluentIcon, TextEdit
|
||||
from qfluentwidgets import InfoBar
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
def get_component_page(component_name, project_path, component_manager=None):
|
||||
"""根据组件名返回对应的页面类,没有特殊类则返回默认ComponentSimple"""
|
||||
name_lower = component_name.lower()
|
||||
special_classes = {
|
||||
"pid": component_pid,
|
||||
"filter": component_filter,
|
||||
# 以后可以继续添加特殊组件
|
||||
}
|
||||
if name_lower in special_classes:
|
||||
return special_classes[name_lower](project_path, component_manager)
|
||||
else:
|
||||
template_names = {
|
||||
'header': f'{name_lower}.h',
|
||||
'source': f'{name_lower}.c'
|
||||
}
|
||||
return ComponentSimple(project_path, component_name, template_names, component_manager)
|
||||
|
||||
|
||||
|
||||
def get_all_dependency_components(dependencies):
|
||||
"""获取所有被依赖的组件列表"""
|
||||
dependent_components = set()
|
||||
for component, deps in dependencies.items():
|
||||
for dep_path in deps:
|
||||
dep_name = os.path.basename(dep_path)
|
||||
dependent_components.add(dep_name.lower())
|
||||
return dependent_components
|
||||
|
||||
|
||||
class ComponentSimple(QWidget):
|
||||
"""简单组件界面 - 只有开启/关闭功能"""
|
||||
def __init__(self, project_path, component_name, template_names, component_manager=None):
|
||||
super().__init__()
|
||||
self.project_path = project_path
|
||||
self.component_name = component_name
|
||||
self.template_names = template_names
|
||||
self.component_manager = component_manager
|
||||
|
||||
# 加载描述和依赖信息
|
||||
component_dir = CodeGenerator.get_assets_dir("User_code/component")
|
||||
describe_path = os.path.join(component_dir, "describe.csv")
|
||||
dependencies_path = os.path.join(component_dir, "dependencies.csv")
|
||||
self.descriptions = CodeGenerator.load_descriptions(describe_path)
|
||||
self.dependencies = CodeGenerator.load_dependencies(dependencies_path)
|
||||
|
||||
self._init_ui()
|
||||
self._load_config()
|
||||
|
||||
def _init_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.setAlignment(Qt.AlignVCenter)
|
||||
self.generate_checkbox = CheckBox(f"启用 {self.component_name}")
|
||||
self.generate_checkbox.stateChanged.connect(self._on_checkbox_changed)
|
||||
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
|
||||
top_layout.addStretch()
|
||||
title = SubtitleLabel(f"{self.component_name} 配置 ")
|
||||
title.setAlignment(Qt.AlignHCenter)
|
||||
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
|
||||
top_layout.addStretch()
|
||||
layout.addLayout(top_layout)
|
||||
|
||||
desc = self.descriptions.get(self.component_name.lower(), "")
|
||||
if desc:
|
||||
desc_label = BodyLabel(f"功能说明:{desc}")
|
||||
desc_label.setWordWrap(True)
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
deps = self.dependencies.get(self.component_name.lower(), [])
|
||||
if deps:
|
||||
deps_text = f"依赖组件:{', '.join([os.path.basename(dep) for dep in deps])}"
|
||||
deps_label = BodyLabel(deps_text)
|
||||
deps_label.setWordWrap(True)
|
||||
deps_label.setStyleSheet("color: #888888;")
|
||||
layout.addWidget(deps_label)
|
||||
# 不再自动启用依赖,只做提示
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def _on_checkbox_changed(self, state):
|
||||
pass # 不再自动启用依赖
|
||||
|
||||
def is_need_generate(self):
|
||||
return self.generate_checkbox.isChecked()
|
||||
|
||||
def get_enabled_dependencies(self):
|
||||
if not self.is_need_generate():
|
||||
return []
|
||||
return self.dependencies.get(self.component_name.lower(), [])
|
||||
|
||||
def _generate_component_code_internal(self):
|
||||
# 检查是否需要生成
|
||||
if not self.is_need_generate():
|
||||
# 如果未勾选,检查文件是否已存在,如果存在则跳过
|
||||
for filename in self.template_names.values():
|
||||
output_path = os.path.join(self.project_path, f"User/component/{filename}")
|
||||
if os.path.exists(output_path):
|
||||
return "skipped" # 返回特殊值表示跳过
|
||||
return "not_needed" # 返回特殊值表示不需要生成
|
||||
|
||||
template_dir = self._get_component_template_dir()
|
||||
for key, filename in self.template_names.items():
|
||||
template_path = os.path.join(template_dir, filename)
|
||||
template_content = CodeGenerator.load_template(template_path)
|
||||
if not template_content:
|
||||
print(f"模板文件不存在或为空: {template_path}")
|
||||
continue
|
||||
output_path = os.path.join(self.project_path, f"User/component/{filename}")
|
||||
CodeGenerator.save_with_preserve(output_path, template_content)
|
||||
self._save_config()
|
||||
return True
|
||||
|
||||
def _get_component_template_dir(self):
|
||||
return CodeGenerator.get_assets_dir("User_code/component")
|
||||
|
||||
def _save_config(self):
|
||||
config_path = os.path.join(self.project_path, "User/component/component_config.yaml")
|
||||
config_data = CodeGenerator.load_config(config_path)
|
||||
config_data[self.component_name.lower()] = {
|
||||
'enabled': self.is_need_generate(),
|
||||
'dependencies': self.dependencies.get(self.component_name.lower(), [])
|
||||
}
|
||||
CodeGenerator.save_config(config_data, config_path)
|
||||
|
||||
def _load_config(self):
|
||||
config_path = os.path.join(self.project_path, "User/component/component_config.yaml")
|
||||
config_data = CodeGenerator.load_config(config_path)
|
||||
conf = config_data.get(self.component_name.lower(), {})
|
||||
if conf.get('enabled', False):
|
||||
self.generate_checkbox.setChecked(True)
|
||||
|
||||
class ComponentManager:
|
||||
"""组件依赖管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.component_pages = {} # 组件名 -> 页面对象
|
||||
|
||||
def register_component(self, component_name, page):
|
||||
"""注册组件页面"""
|
||||
self.component_pages[component_name.lower()] = page
|
||||
|
||||
def _sync_dependency_states(self):
|
||||
"""同步所有依赖状态"""
|
||||
# 重新计算所有依赖计数
|
||||
new_dependency_count = {}
|
||||
|
||||
for page_name, page in self.component_pages.items():
|
||||
if page.is_need_generate():
|
||||
deps = page.get_enabled_dependencies()
|
||||
for dep_path in deps:
|
||||
dep_name = os.path.basename(dep_path).lower()
|
||||
new_dependency_count[dep_name] = new_dependency_count.get(dep_name, 0) + 1
|
||||
|
||||
# 更新依赖计数
|
||||
self.dependency_count = new_dependency_count
|
||||
|
||||
# 更新所有页面的状态
|
||||
for page_name, page in self.component_pages.items():
|
||||
if page.is_dependency:
|
||||
count = self.dependency_count.get(page_name, 0)
|
||||
page.set_dependency_count(count)
|
||||
|
||||
def enable_dependencies(self, component_name, deps):
|
||||
"""启用依赖项"""
|
||||
for dep_path in deps:
|
||||
dep_name = os.path.basename(dep_path).lower()
|
||||
|
||||
# 增加依赖计数
|
||||
self.dependency_count[dep_name] = self.dependency_count.get(dep_name, 0) + 1
|
||||
|
||||
# 更新被依赖的组件状态
|
||||
if dep_name in self.component_pages:
|
||||
page = self.component_pages[dep_name]
|
||||
page.set_dependency_count(self.dependency_count[dep_name])
|
||||
|
||||
def disable_dependencies(self, component_name, deps):
|
||||
"""禁用依赖项"""
|
||||
for dep_path in deps:
|
||||
dep_name = os.path.basename(dep_path).lower()
|
||||
|
||||
# 减少依赖计数
|
||||
if dep_name in self.dependency_count:
|
||||
self.dependency_count[dep_name] = max(0, self.dependency_count[dep_name] - 1)
|
||||
|
||||
# 更新被依赖的组件状态
|
||||
if dep_name in self.component_pages:
|
||||
page = self.component_pages[dep_name]
|
||||
page.set_dependency_count(self.dependency_count[dep_name])
|
||||
|
||||
# 具体组件类
|
||||
class component_pid(ComponentSimple):
|
||||
def __init__(self, project_path, component_manager=None):
|
||||
super().__init__(
|
||||
project_path,
|
||||
"PID",
|
||||
{'header': 'pid.h', 'source': 'pid.c'},
|
||||
component_manager
|
||||
)
|
||||
|
||||
class component_filter(ComponentSimple):
|
||||
def __init__(self, project_path, component_manager=None):
|
||||
super().__init__(
|
||||
project_path,
|
||||
"Filter",
|
||||
{'header': 'filter.h', 'source': 'filter.c'},
|
||||
component_manager
|
||||
)
|
||||
|
||||
# ...existing code... (component 类的 generate_component 方法保持不变)
|
||||
|
||||
class component(QWidget):
|
||||
"""组件管理器"""
|
||||
|
||||
def __init__(self, project_path):
|
||||
super().__init__()
|
||||
self.project_path = project_path
|
||||
|
||||
@staticmethod
|
||||
def generate_component(project_path, pages):
|
||||
"""生成所有组件代码,处理依赖关系"""
|
||||
# 在方法开始时导入CodeGenerator以确保可用
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
|
||||
# 自动添加 component.h
|
||||
src_component_h = os.path.join(CodeGenerator.get_assets_dir("User_code/component"), "component.h")
|
||||
dst_component_h = os.path.join(project_path, "User/component/component.h")
|
||||
os.makedirs(os.path.dirname(dst_component_h), exist_ok=True)
|
||||
if os.path.exists(src_component_h):
|
||||
with open(src_component_h, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
CodeGenerator.save_with_preserve(dst_component_h, content)
|
||||
|
||||
# 收集所有需要生成的组件和它们的依赖
|
||||
components_to_generate = set()
|
||||
component_pages = {}
|
||||
|
||||
for page in pages:
|
||||
# 检查是否是组件页面(通过类名或者属性判断)
|
||||
if hasattr(page, "component_name") and hasattr(page, "is_need_generate"):
|
||||
if page.is_need_generate():
|
||||
component_name = page.component_name.lower()
|
||||
components_to_generate.add(component_name)
|
||||
component_pages[component_name] = page
|
||||
|
||||
# 添加依赖组件,依赖格式是路径形式如 "component/filter"
|
||||
deps = page.get_enabled_dependencies()
|
||||
for dep_path in deps:
|
||||
# 跳过BSP层依赖
|
||||
if dep_path.startswith('bsp/'):
|
||||
continue
|
||||
# 从路径中提取组件名,如 "component/filter" -> "filter"
|
||||
dep_name = os.path.basename(dep_path)
|
||||
# 只有不包含文件扩展名的才是组件,有扩展名的是文件依赖
|
||||
if not dep_name.endswith(('.h', '.c', '.hpp', '.cpp')):
|
||||
components_to_generate.add(dep_name)
|
||||
|
||||
# 为没有对应页面但需要生成的依赖组件创建临时页面
|
||||
user_code_dir = CodeGenerator.get_assets_dir("User_code")
|
||||
for comp_name in components_to_generate:
|
||||
if comp_name not in component_pages:
|
||||
# 创建临时组件页面
|
||||
template_names = {'header': f'{comp_name}.h', 'source': f'{comp_name}.c'}
|
||||
temp_page = ComponentSimple(project_path, comp_name.upper(), template_names)
|
||||
# temp_page.set_forced_enabled(True) # 自动启用依赖组件
|
||||
component_pages[comp_name] = temp_page
|
||||
|
||||
# 如果没有组件需要生成,返回提示信息
|
||||
if not components_to_generate:
|
||||
return "没有启用的组件需要生成代码。"
|
||||
|
||||
# 生成代码和依赖文件
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
fail_list = []
|
||||
|
||||
# 处理依赖文件的复制
|
||||
all_deps = set()
|
||||
for page in pages:
|
||||
if hasattr(page, "component_name") and hasattr(page, "is_need_generate"):
|
||||
if page.is_need_generate():
|
||||
deps = page.get_enabled_dependencies()
|
||||
all_deps.update(deps)
|
||||
|
||||
# 复制依赖文件
|
||||
for dep_path in all_deps:
|
||||
try:
|
||||
# 检查是否是 bsp 层依赖
|
||||
if dep_path.startswith('bsp/'):
|
||||
# 对于 bsp 层依赖,跳过复制,因为这些由 BSP 代码生成负责
|
||||
print(f"跳过 BSP 层依赖: {dep_path} (由 BSP 代码生成负责)")
|
||||
continue
|
||||
|
||||
# dep_path 格式如 "component/filter" 或 "component/user_math.h"
|
||||
src_path = os.path.join(user_code_dir, dep_path)
|
||||
dst_path = os.path.join(project_path, "User", dep_path)
|
||||
|
||||
if os.path.isdir(src_path):
|
||||
# 如果是目录,复制整个目录
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
if os.path.exists(dst_path):
|
||||
shutil.rmtree(dst_path)
|
||||
shutil.copytree(src_path, dst_path)
|
||||
elif os.path.isfile(src_path):
|
||||
# 如果是文件,复制单个文件并保留用户区域
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
with open(src_path, 'r', encoding='utf-8') as f:
|
||||
new_content = f.read()
|
||||
CodeGenerator.save_with_preserve(dst_path, new_content)
|
||||
else:
|
||||
# 如果既不是文件也不是目录,跳过
|
||||
print(f"跳过不存在的依赖: {dep_path}")
|
||||
continue
|
||||
|
||||
success_count += 1
|
||||
print(f"成功复制依赖: {dep_path}")
|
||||
except Exception as e:
|
||||
# 对于 bsp 层依赖的错误,只记录但不计入失败
|
||||
if dep_path.startswith('bsp/'):
|
||||
print(f"BSP 层依赖 {dep_path} 复制失败,但忽略此错误: {e}")
|
||||
else:
|
||||
fail_count += 1
|
||||
fail_list.append(f"{dep_path} (依赖复制异常: {e})")
|
||||
print(f"复制依赖失败: {dep_path}, 错误: {e}")
|
||||
|
||||
# 生成组件代码
|
||||
skipped_count = 0
|
||||
skipped_list = []
|
||||
|
||||
for comp_name in components_to_generate:
|
||||
if comp_name in component_pages:
|
||||
page = component_pages[comp_name]
|
||||
try:
|
||||
# 确保调用正确的方法名
|
||||
if hasattr(page, '_generate_component_code_internal'):
|
||||
result = page._generate_component_code_internal()
|
||||
if result == "skipped":
|
||||
skipped_count += 1
|
||||
skipped_list.append(comp_name)
|
||||
print(f"跳过组件生成: {comp_name}")
|
||||
elif result:
|
||||
success_count += 1
|
||||
print(f"成功生成组件: {comp_name}")
|
||||
else:
|
||||
fail_count += 1
|
||||
fail_list.append(f"{comp_name} (生成失败)")
|
||||
print(f"生成组件失败: {comp_name}")
|
||||
else:
|
||||
fail_count += 1
|
||||
fail_list.append(f"{comp_name} (缺少生成方法)")
|
||||
print(f"组件页面缺少生成方法: {comp_name}")
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
fail_list.append(f"{comp_name} (生成异常: {e})")
|
||||
print(f"生成组件异常: {comp_name}, 错误: {e}")
|
||||
|
||||
total_items = len(all_deps) + len(components_to_generate)
|
||||
msg = f"组件代码生成完成:总共处理 {total_items} 项,成功生成 {success_count} 项,跳过 {skipped_count} 项,失败 {fail_count} 项。"
|
||||
if skipped_list:
|
||||
msg += f"\n跳过项(文件已存在且未勾选):\n" + "\n".join(skipped_list)
|
||||
if fail_list:
|
||||
msg += "\n失败项:\n" + "\n".join(fail_list)
|
||||
|
||||
return msg
|
||||
@ -1,404 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
|
||||
from qfluentwidgets import BodyLabel, CheckBox, ComboBox, SubtitleLabel
|
||||
from PyQt5.QtCore import Qt
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
import os
|
||||
import yaml
|
||||
import re
|
||||
|
||||
def get_available_bsp_devices(project_path, bsp_type, gpio_type=None):
|
||||
"""获取可用的BSP设备,GPIO可选类型过滤"""
|
||||
bsp_config_path = os.path.join(project_path, "User/bsp/bsp_config.yaml")
|
||||
if not os.path.exists(bsp_config_path):
|
||||
return []
|
||||
try:
|
||||
with open(bsp_config_path, 'r', encoding='utf-8') as f:
|
||||
bsp_config = yaml.safe_load(f)
|
||||
if bsp_type == "gpio" and bsp_config.get("gpio", {}).get("enabled", False):
|
||||
configs = bsp_config["gpio"].get("configs", [])
|
||||
# 增加类型过滤
|
||||
if gpio_type:
|
||||
configs = [cfg for cfg in configs if cfg.get('type', '').lower() == gpio_type.lower()]
|
||||
return [f"BSP_GPIO_{cfg['custom_name']}" for cfg in configs]
|
||||
elif bsp_type == "pwm" and bsp_config.get("pwm", {}).get("enabled", False):
|
||||
# PWM使用configs结构
|
||||
configs = bsp_config["pwm"].get("configs", [])
|
||||
return [f"BSP_PWM_{cfg['custom_name']}" for cfg in configs]
|
||||
elif bsp_type in bsp_config and bsp_config[bsp_type].get('enabled', False):
|
||||
devices = bsp_config[bsp_type].get('devices', [])
|
||||
return [f"BSP_{bsp_type.upper()}_{device['name']}" for device in devices]
|
||||
except Exception as e:
|
||||
print(f"读取BSP配置失败: {e}")
|
||||
return []
|
||||
|
||||
def generate_device_header(project_path, enabled_devices):
|
||||
"""生成device.h文件"""
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
device_dir = CodeGenerator.get_assets_dir("User_code/device")
|
||||
template_path = os.path.join(device_dir, "device.h")
|
||||
dst_path = os.path.join(project_path, "User/device/device.h")
|
||||
|
||||
# 优先读取项目中已存在的文件,如果不存在则使用模板
|
||||
if os.path.exists(dst_path):
|
||||
# 读取现有文件以保留用户区域
|
||||
with open(dst_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
else:
|
||||
# 文件不存在时从模板创建
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 收集所有需要的信号定义
|
||||
signals = []
|
||||
current_bit = 0
|
||||
|
||||
# 加载设备配置来获取信号信息
|
||||
config_path = os.path.join(device_dir, "config.yaml")
|
||||
device_configs = CodeGenerator.load_device_config(config_path)
|
||||
|
||||
for device_name in enabled_devices:
|
||||
device_key = device_name.lower()
|
||||
if device_key in device_configs.get('devices', {}):
|
||||
device_config = device_configs['devices'][device_key]
|
||||
thread_signals = device_config.get('thread_signals', [])
|
||||
|
||||
for signal in thread_signals:
|
||||
signal_name = signal['name']
|
||||
signals.append(f"#define {signal_name} (1u << {current_bit})")
|
||||
current_bit += 1
|
||||
|
||||
# 生成信号定义文本
|
||||
signals_text = '\n'.join(signals) if signals else '/* No signals defined */'
|
||||
|
||||
# 替换AUTO GENERATED SIGNALS部分,保留其他所有用户区域
|
||||
pattern = r'/\* AUTO GENERATED SIGNALS BEGIN \*/(.*?)/\* AUTO GENERATED SIGNALS END \*/'
|
||||
replacement = f'/* AUTO GENERATED SIGNALS BEGIN */\n{signals_text}\n/* AUTO GENERATED SIGNALS END */'
|
||||
content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
||||
|
||||
# 使用save_with_preserve保存文件以保留用户区域
|
||||
CodeGenerator.save_with_preserve(dst_path, content)
|
||||
|
||||
|
||||
|
||||
|
||||
class DeviceSimple(QWidget):
|
||||
"""简单设备界面"""
|
||||
|
||||
def __init__(self, project_path, device_name, device_config):
|
||||
super().__init__()
|
||||
self.project_path = project_path
|
||||
self.device_name = device_name
|
||||
self.device_config = device_config
|
||||
|
||||
# 添加必要的属性,确保兼容性
|
||||
self.component_name = device_name # 添加这个属性以兼容现有代码
|
||||
|
||||
self._init_ui()
|
||||
self._load_config()
|
||||
|
||||
def _init_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# 顶部横向布局:左侧复选框,居中标题
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.setAlignment(Qt.AlignVCenter)
|
||||
|
||||
self.generate_checkbox = CheckBox(f"启用 {self.device_name}")
|
||||
self.generate_checkbox.stateChanged.connect(self._on_checkbox_changed)
|
||||
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
|
||||
|
||||
# 弹性空间
|
||||
top_layout.addStretch()
|
||||
|
||||
title = SubtitleLabel(f"{self.device_name} 配置 ")
|
||||
title.setAlignment(Qt.AlignHCenter)
|
||||
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
|
||||
|
||||
# 再加一个弹性空间,保证标题居中
|
||||
top_layout.addStretch()
|
||||
|
||||
layout.addLayout(top_layout)
|
||||
|
||||
# 功能说明
|
||||
desc = self.device_config.get('description', '')
|
||||
if desc:
|
||||
desc_label = BodyLabel(f"功能说明:{desc}")
|
||||
desc_label.setWordWrap(True)
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
# 依赖信息
|
||||
self._add_dependency_info(layout)
|
||||
|
||||
# BSP配置区域
|
||||
self.content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(self.content_widget)
|
||||
|
||||
self._add_bsp_config(content_layout)
|
||||
|
||||
layout.addWidget(self.content_widget)
|
||||
self.content_widget.setEnabled(False)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def _add_dependency_info(self, layout):
|
||||
"""添加依赖信息显示"""
|
||||
bsp_deps = self.device_config.get('dependencies', {}).get('bsp', [])
|
||||
comp_deps = self.device_config.get('dependencies', {}).get('component', [])
|
||||
|
||||
if bsp_deps or comp_deps:
|
||||
deps_text = "依赖: "
|
||||
if bsp_deps:
|
||||
deps_text += f"BSP({', '.join(bsp_deps)})"
|
||||
if comp_deps:
|
||||
if bsp_deps:
|
||||
deps_text += ", "
|
||||
deps_text += f"Component({', '.join(comp_deps)})"
|
||||
|
||||
deps_label = BodyLabel(deps_text)
|
||||
deps_label.setWordWrap(True)
|
||||
deps_label.setStyleSheet("color: #888888;")
|
||||
layout.addWidget(deps_label)
|
||||
|
||||
def _add_bsp_config(self, layout):
|
||||
bsp_requirements = self.device_config.get('bsp_requirements', [])
|
||||
self.bsp_combos = {}
|
||||
if bsp_requirements:
|
||||
layout.addWidget(BodyLabel("BSP设备配置:"))
|
||||
for req in bsp_requirements:
|
||||
bsp_type = req['type']
|
||||
var_name = req['var_name']
|
||||
description = req.get('description', '')
|
||||
gpio_type = req.get('gpio_type', None) # 新增
|
||||
req_layout = QHBoxLayout()
|
||||
label = BodyLabel(f"{bsp_type.upper()}:")
|
||||
label.setMinimumWidth(80)
|
||||
req_layout.addWidget(label)
|
||||
combo = ComboBox()
|
||||
# 传递gpio_type参数
|
||||
self._update_bsp_combo(combo, bsp_type, gpio_type)
|
||||
req_layout.addWidget(combo)
|
||||
if description:
|
||||
desc_label = BodyLabel(f"({description})")
|
||||
desc_label.setStyleSheet("color: #666666; font-size: 12px;")
|
||||
req_layout.addWidget(desc_label)
|
||||
req_layout.addStretch()
|
||||
layout.addLayout(req_layout)
|
||||
self.bsp_combos[var_name] = combo
|
||||
|
||||
def _update_bsp_combo(self, combo, bsp_type, gpio_type=None):
|
||||
combo.clear()
|
||||
available_devices = get_available_bsp_devices(self.project_path, bsp_type, gpio_type)
|
||||
if available_devices:
|
||||
combo.addItems(available_devices)
|
||||
else:
|
||||
combo.addItem(f"未找到可用的{bsp_type.upper()}设备")
|
||||
combo.setEnabled(False)
|
||||
|
||||
|
||||
def refresh_bsp_combos(self):
|
||||
"""刷新所有BSP组合框"""
|
||||
bsp_requirements = self.device_config.get('bsp_requirements', [])
|
||||
for req in bsp_requirements:
|
||||
bsp_type = req['type']
|
||||
var_name = req['var_name']
|
||||
if var_name in self.bsp_combos:
|
||||
current_text = self.bsp_combos[var_name].currentText()
|
||||
self._update_bsp_combo(self.bsp_combos[var_name], bsp_type)
|
||||
# 尝试恢复之前的选择
|
||||
index = self.bsp_combos[var_name].findText(current_text)
|
||||
if index >= 0:
|
||||
self.bsp_combos[var_name].setCurrentIndex(index)
|
||||
|
||||
def _on_checkbox_changed(self, state):
|
||||
"""处理复选框状态变化"""
|
||||
self.content_widget.setEnabled(state == 2)
|
||||
|
||||
def is_need_generate(self):
|
||||
"""检查是否需要生成代码"""
|
||||
return self.generate_checkbox.isChecked()
|
||||
|
||||
def get_bsp_config(self):
|
||||
"""获取BSP配置"""
|
||||
config = {}
|
||||
for var_name, combo in self.bsp_combos.items():
|
||||
if combo.isEnabled():
|
||||
config[var_name] = combo.currentText()
|
||||
return config
|
||||
|
||||
def _generate_device_code_internal(self):
|
||||
"""生成设备代码"""
|
||||
# 检查是否需要生成
|
||||
if not self.is_need_generate():
|
||||
# 如果未勾选,检查文件是否已存在,如果存在则跳过
|
||||
files = self.device_config.get('files', {})
|
||||
for filename in files.values():
|
||||
output_path = os.path.join(self.project_path, f"User/device/{filename}")
|
||||
if os.path.exists(output_path):
|
||||
return "skipped" # 返回特殊值表示跳过
|
||||
return "not_needed" # 返回特殊值表示不需要生成
|
||||
|
||||
# 获取BSP配置
|
||||
bsp_config = self.get_bsp_config()
|
||||
|
||||
# 复制并修改文件
|
||||
template_dir = self._get_device_template_dir()
|
||||
files = self.device_config.get('files', {})
|
||||
|
||||
for file_type, filename in files.items():
|
||||
src_path = os.path.join(template_dir, filename)
|
||||
dst_path = os.path.join(self.project_path, f"User/device/{filename}")
|
||||
|
||||
if os.path.exists(src_path):
|
||||
# 读取模板文件内容
|
||||
with open(src_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 替换BSP设备名称
|
||||
for var_name, device_name in bsp_config.items():
|
||||
content = content.replace(var_name, device_name)
|
||||
|
||||
# 根据文件类型选择保存方式
|
||||
if file_type == 'header':
|
||||
# 头文件需要保留用户区域
|
||||
CodeGenerator.save_with_preserve(dst_path, content)
|
||||
else:
|
||||
# 源文件直接保存(不需要保留用户区域)
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
with open(dst_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
self._save_config()
|
||||
return True
|
||||
|
||||
def _get_device_template_dir(self):
|
||||
"""获取设备模板目录"""
|
||||
return CodeGenerator.get_assets_dir("User_code/device")
|
||||
|
||||
def _save_config(self):
|
||||
"""保存配置"""
|
||||
config_path = os.path.join(self.project_path, "User/device/device_config.yaml")
|
||||
config_data = CodeGenerator.load_config(config_path)
|
||||
config_data[self.device_name.lower()] = {
|
||||
'enabled': self.is_need_generate(),
|
||||
'bsp_config': self.get_bsp_config()
|
||||
}
|
||||
CodeGenerator.save_config(config_data, config_path)
|
||||
|
||||
def _load_config(self):
|
||||
"""加载配置"""
|
||||
config_path = os.path.join(self.project_path, "User/device/device_config.yaml")
|
||||
config_data = CodeGenerator.load_config(config_path)
|
||||
conf = config_data.get(self.device_name.lower(), {})
|
||||
|
||||
if conf.get('enabled', False):
|
||||
self.generate_checkbox.setChecked(True)
|
||||
|
||||
# 恢复BSP配置
|
||||
bsp_config = conf.get('bsp_config', {})
|
||||
for var_name, device_name in bsp_config.items():
|
||||
if var_name in self.bsp_combos:
|
||||
combo = self.bsp_combos[var_name]
|
||||
index = combo.findText(device_name)
|
||||
if index >= 0:
|
||||
combo.setCurrentIndex(index)
|
||||
|
||||
def get_device_page(device_name, project_path):
|
||||
"""根据设备名返回对应的页面类"""
|
||||
# 加载设备配置
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
device_dir = CodeGenerator.get_assets_dir("User_code/device")
|
||||
config_path = os.path.join(device_dir, "config.yaml")
|
||||
device_configs = CodeGenerator.load_device_config(config_path)
|
||||
|
||||
devices = device_configs.get('devices', {})
|
||||
device_key = device_name.lower()
|
||||
|
||||
if device_key in devices:
|
||||
device_config = devices[device_key]
|
||||
page = DeviceSimple(project_path, device_name, device_config)
|
||||
else:
|
||||
# 如果配置中没有找到,返回一个基本的设备页面
|
||||
basic_config = {
|
||||
'name': device_name,
|
||||
'description': f'{device_name}设备',
|
||||
'files': {'header': f'{device_name.lower()}.h', 'source': f'{device_name.lower()}.c'},
|
||||
'bsp_requirements': [],
|
||||
'dependencies': {'bsp': [], 'component': []}
|
||||
}
|
||||
page = DeviceSimple(project_path, device_name, basic_config)
|
||||
|
||||
# 确保页面有必要的属性
|
||||
page.device_name = device_name
|
||||
|
||||
return page
|
||||
|
||||
class device(QWidget):
|
||||
"""设备管理器"""
|
||||
|
||||
def __init__(self, project_path):
|
||||
super().__init__()
|
||||
self.project_path = project_path
|
||||
|
||||
@staticmethod
|
||||
def generate_device(project_path, pages):
|
||||
"""生成所有设备代码"""
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
skipped_count = 0
|
||||
fail_list = []
|
||||
skipped_list = []
|
||||
enabled_devices = []
|
||||
|
||||
# 生成设备代码
|
||||
for page in pages:
|
||||
if hasattr(page, "device_name") and hasattr(page, "is_need_generate"):
|
||||
# 先检查是否有文件存在但未勾选的情况
|
||||
if not page.is_need_generate():
|
||||
try:
|
||||
result = page._generate_device_code_internal()
|
||||
if result == "skipped":
|
||||
skipped_count += 1
|
||||
skipped_list.append(page.device_name)
|
||||
except Exception:
|
||||
pass # 忽略未勾选页面的错误
|
||||
else:
|
||||
# 勾选的页面,正常处理
|
||||
try:
|
||||
result = page._generate_device_code_internal()
|
||||
if result == "skipped":
|
||||
skipped_count += 1
|
||||
skipped_list.append(page.device_name)
|
||||
elif result:
|
||||
success_count += 1
|
||||
enabled_devices.append(page.device_name)
|
||||
else:
|
||||
fail_count += 1
|
||||
fail_list.append(page.device_name)
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
fail_list.append(f"{page.device_name} (异常: {e})")
|
||||
|
||||
# 生成device.h文件
|
||||
try:
|
||||
generate_device_header(project_path, enabled_devices)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
fail_list.append(f"device.h (异常: {e})")
|
||||
|
||||
# 刷新所有页面的BSP组合框选项
|
||||
for page in pages:
|
||||
if hasattr(page, 'refresh_bsp_combos'):
|
||||
try:
|
||||
page.refresh_bsp_combos()
|
||||
except Exception as e:
|
||||
print(f"刷新页面 {getattr(page, 'device_name', 'Unknown')} 的BSP选项失败: {e}")
|
||||
|
||||
total_items = success_count + fail_count + skipped_count
|
||||
msg = f"设备代码生成完成:总共处理 {total_items} 项,成功生成 {success_count} 项,跳过 {skipped_count} 项,失败 {fail_count} 项。"
|
||||
if skipped_list:
|
||||
msg += f"\n跳过项(文件已存在且未勾选):\n" + "\n".join(skipped_list)
|
||||
if fail_list:
|
||||
msg += "\n失败项:\n" + "\n".join(fail_list)
|
||||
|
||||
return msg
|
||||
@ -1,634 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QStackedLayout, QFileDialog, QHeaderView
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QTreeWidgetItem as TreeItem
|
||||
from qfluentwidgets import TitleLabel, BodyLabel, SubtitleLabel, StrongBodyLabel, HorizontalSeparator, PushButton, TreeWidget, InfoBar,FluentIcon, Dialog,SubtitleLabel,BodyLabel
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox
|
||||
from qfluentwidgets import CardWidget, LineEdit, SpinBox, CheckBox, TextEdit, PrimaryPushButton, PushButton, InfoBar, DoubleSpinBox
|
||||
from qfluentwidgets import HeaderCardWidget
|
||||
from PyQt5.QtWidgets import QScrollArea, QWidget
|
||||
from qfluentwidgets import theme, Theme
|
||||
from PyQt5.QtWidgets import QDoubleSpinBox
|
||||
from .tools.code_task_config import TaskConfigDialog
|
||||
|
||||
import os
|
||||
import requests
|
||||
import zipfile
|
||||
import io
|
||||
import re
|
||||
import shutil
|
||||
import yaml
|
||||
import textwrap
|
||||
from jinja2 import Template
|
||||
|
||||
def preserve_all_user_regions(new_code, old_code):
|
||||
import re
|
||||
pattern = re.compile(
|
||||
r"/\*\s*(USER [A-Z0-9_ ]+)\s*BEGIN\s*\*/(.*?)/\*\s*\1\s*END\s*\*/",
|
||||
re.DOTALL
|
||||
)
|
||||
old_regions = {m.group(1): m.group(2) for m in pattern.finditer(old_code or "")}
|
||||
def repl(m):
|
||||
region = m.group(1)
|
||||
old_content = old_regions.get(region)
|
||||
if old_content is not None:
|
||||
return m.group(0).replace(m.group(2), old_content)
|
||||
return m.group(0)
|
||||
return pattern.sub(repl, new_code)
|
||||
|
||||
def save_with_preserve(path, new_code):
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
old_code = f.read()
|
||||
new_code = preserve_all_user_regions(new_code, old_code)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(new_code)
|
||||
|
||||
class IocConfig:
|
||||
def __init__(self, ioc_path):
|
||||
self.ioc_path = ioc_path
|
||||
self.config = {}
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
with open(self.ioc_path, encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
self.config[key.strip()] = value.strip()
|
||||
|
||||
def is_freertos_enabled(self):
|
||||
ip_keys = [k for k in self.config if k.startswith('Mcu.IP')]
|
||||
for k in ip_keys:
|
||||
if self.config[k] == 'FREERTOS':
|
||||
return True
|
||||
for k in self.config:
|
||||
if k.startswith('FREERTOS.'):
|
||||
return True
|
||||
return False
|
||||
|
||||
class HomePageWidget(QWidget):
|
||||
def __init__(self, parent=None, on_choose_project=None, on_update_template=None):
|
||||
super().__init__(parent)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addStretch()
|
||||
|
||||
content_layout = QVBoxLayout()
|
||||
content_layout.setSpacing(28)
|
||||
content_layout.setContentsMargins(48, 48, 48, 48)
|
||||
|
||||
title = TitleLabel("MRobot 代码生成")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(title)
|
||||
|
||||
subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径(.ico所在的目录),然后开启代码之旅!")
|
||||
subtitle.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(subtitle)
|
||||
|
||||
desc = BodyLabel("支持自动配置和生成任务,自主选择模块代码倒入,自动识别cubemx配置!")
|
||||
desc.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(desc)
|
||||
|
||||
content_layout.addSpacing(18)
|
||||
|
||||
self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径")
|
||||
self.choose_btn.setFixedWidth(200)
|
||||
if on_choose_project:
|
||||
self.choose_btn.clicked.connect(on_choose_project)
|
||||
content_layout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
|
||||
self.update_template_btn.setFixedWidth(200)
|
||||
if on_update_template:
|
||||
self.update_template_btn.clicked.connect(on_update_template)
|
||||
content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
content_layout.addSpacing(10)
|
||||
content_layout.addStretch()
|
||||
|
||||
layout.addLayout(content_layout)
|
||||
layout.addStretch()
|
||||
|
||||
class CodeGenWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.project_name_label = StrongBodyLabel()
|
||||
self.project_path_label = BodyLabel()
|
||||
self.ioc_file_label = BodyLabel()
|
||||
self.freertos_label = BodyLabel()
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(32, 32, 32, 32)
|
||||
main_layout.setSpacing(18)
|
||||
|
||||
info_layout = QHBoxLayout()
|
||||
self.back_btn = PushButton(FluentIcon.SKIP_BACK, "返回")
|
||||
self.back_btn.setFixedWidth(90)
|
||||
info_layout.addWidget(self.back_btn)
|
||||
info_layout.addWidget(self.project_name_label)
|
||||
info_layout.addWidget(self.project_path_label)
|
||||
info_layout.addWidget(self.ioc_file_label)
|
||||
info_layout.addWidget(self.freertos_label)
|
||||
info_layout.addStretch()
|
||||
main_layout.addLayout(info_layout)
|
||||
main_layout.addWidget(HorizontalSeparator())
|
||||
|
||||
content_hbox = QHBoxLayout()
|
||||
content_hbox.setSpacing(24)
|
||||
|
||||
left_vbox = QVBoxLayout()
|
||||
left_vbox.addWidget(SubtitleLabel("用户代码模块选择"))
|
||||
left_vbox.addWidget(HorizontalSeparator())
|
||||
self.file_tree = TreeWidget()
|
||||
self.file_tree.setHeaderLabels(["模块名"])
|
||||
self.file_tree.setSelectionMode(self.file_tree.ExtendedSelection)
|
||||
self.file_tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
self.file_tree.setCheckedColor("#0078d4", "#2d7d9a")
|
||||
self.file_tree.setBorderRadius(8)
|
||||
self.file_tree.setBorderVisible(True)
|
||||
left_vbox.addWidget(self.file_tree, stretch=1)
|
||||
content_hbox.addLayout(left_vbox, 2)
|
||||
|
||||
right_vbox = QVBoxLayout()
|
||||
right_vbox.setSpacing(18)
|
||||
right_vbox.addWidget(SubtitleLabel("操作区"))
|
||||
right_vbox.addWidget(HorizontalSeparator())
|
||||
|
||||
btn_group = QVBoxLayout()
|
||||
self.freertos_task_btn = PushButton("自动生成FreeRTOS任务")
|
||||
self.freertos_task_btn.setFixedWidth(200)
|
||||
btn_group.addWidget(self.freertos_task_btn)
|
||||
self.task_code_btn = PushButton("配置并生成任务代码")
|
||||
self.task_code_btn.setFixedWidth(200)
|
||||
btn_group.addWidget(self.task_code_btn)
|
||||
self.generate_btn = PushButton(FluentIcon.CODE, "生成代码")
|
||||
self.generate_btn.setFixedWidth(200)
|
||||
btn_group.addWidget(self.generate_btn)
|
||||
btn_group.addSpacing(10)
|
||||
right_vbox.addLayout(btn_group)
|
||||
right_vbox.addStretch()
|
||||
|
||||
content_hbox.addLayout(right_vbox, 1)
|
||||
main_layout.addLayout(content_hbox, stretch=1)
|
||||
|
||||
class DataInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setObjectName("dataInterface")
|
||||
|
||||
self.project_path = ""
|
||||
self.project_name = ""
|
||||
self.ioc_file = ""
|
||||
self.freertos_enabled = False
|
||||
|
||||
self.stacked_layout = QStackedLayout(self)
|
||||
self.setLayout(self.stacked_layout)
|
||||
|
||||
self.home_page = HomePageWidget(
|
||||
on_choose_project=self.choose_project_folder,
|
||||
on_update_template=self.update_user_template
|
||||
)
|
||||
self.stacked_layout.addWidget(self.home_page)
|
||||
|
||||
self.codegen_page = CodeGenWidget()
|
||||
self.stacked_layout.addWidget(self.codegen_page)
|
||||
|
||||
# 事件绑定
|
||||
self.codegen_page.back_btn.clicked.connect(self.back_to_select)
|
||||
self.codegen_page.freertos_task_btn.clicked.connect(self.on_freertos_task_btn_clicked)
|
||||
self.codegen_page.task_code_btn.clicked.connect(self.on_task_code_btn_clicked)
|
||||
self.codegen_page.generate_btn.clicked.connect(self.generate_code)
|
||||
self.codegen_page.file_tree.itemChanged.connect(self.on_tree_item_changed)
|
||||
|
||||
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:
|
||||
InfoBar.warning(
|
||||
title="提示",
|
||||
content="未找到.ioc文件,请确认项目文件夹。",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
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):
|
||||
self.codegen_page.project_name_label.setText(f"项目名称: {self.project_name}")
|
||||
self.codegen_page.project_path_label.setText(f"项目路径: {self.project_path}")
|
||||
try:
|
||||
ioc = IocConfig(self.ioc_file)
|
||||
self.freertos_enabled = ioc.is_freertos_enabled()
|
||||
freertos_status = "已启用" if self.freertos_enabled else "未启用"
|
||||
self.codegen_page.freertos_label.setText(f"FreeRTOS: {freertos_status}")
|
||||
except Exception as e:
|
||||
self.codegen_page.freertos_label.setText(f"IOC解析失败: {e}")
|
||||
self.codegen_page.freertos_task_btn.hide()
|
||||
self.freertos_enabled = False
|
||||
self.show_user_code_files()
|
||||
self.stacked_layout.setCurrentWidget(self.codegen_page)
|
||||
|
||||
def on_freertos_task_btn_clicked(self):
|
||||
if not self.freertos_enabled:
|
||||
InfoBar.warning(
|
||||
title="未开启 FreeRTOS",
|
||||
content="请先在 CubeMX 中开启 FreeRTOS!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
return
|
||||
self.generate_freertos_task()
|
||||
|
||||
def on_task_code_btn_clicked(self):
|
||||
if not self.freertos_enabled:
|
||||
InfoBar.warning(
|
||||
title="未开启 FreeRTOS",
|
||||
content="请先在 CubeMX 中开启 FreeRTOS!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
return
|
||||
self.open_task_config_dialog()
|
||||
|
||||
def back_to_select(self):
|
||||
self.stacked_layout.setCurrentWidget(self.home_page)
|
||||
|
||||
def update_user_template(self):
|
||||
from app.tools.update_code import update_code
|
||||
|
||||
def info_callback(parent):
|
||||
InfoBar.success(
|
||||
title="更新成功",
|
||||
content="用户模板已更新到最新版本!",
|
||||
parent=parent,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def error_callback(parent, msg):
|
||||
InfoBar.error(
|
||||
title="更新失败",
|
||||
content=f"用户模板更新失败: {msg}",
|
||||
parent=parent,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
update_code(parent=self, info_callback=info_callback, error_callback=error_callback)
|
||||
|
||||
def show_user_code_files(self):
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
file_tree = self.codegen_page.file_tree
|
||||
file_tree.clear()
|
||||
base_dir = CodeGenerator.get_assets_dir("User_code")
|
||||
user_dir = os.path.join(self.project_path, "User")
|
||||
sub_dirs = ["bsp", "component", "device", "module"]
|
||||
|
||||
describe_map = {}
|
||||
dependencies_map = {}
|
||||
for sub in sub_dirs:
|
||||
dir_path = os.path.join(base_dir, sub)
|
||||
if not os.path.isdir(dir_path):
|
||||
continue
|
||||
desc_path = os.path.join(dir_path, "describe.csv")
|
||||
if os.path.exists(desc_path):
|
||||
with open(desc_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if "," in line:
|
||||
k, v = line.strip().split(",", 1)
|
||||
describe_map[f"{sub}/{k.strip()}"] = v.strip()
|
||||
dep_path = os.path.join(dir_path, "dependencies.csv")
|
||||
if os.path.exists(dep_path):
|
||||
with open(dep_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if "," in line:
|
||||
a, b = line.strip().split(",", 1)
|
||||
dependencies_map.setdefault(f"{sub}/{a.strip()}", []).append(b.strip())
|
||||
|
||||
self._describe_map = describe_map
|
||||
self._dependencies_map = dependencies_map
|
||||
|
||||
file_tree.setHeaderLabels(["模块名", "描述"])
|
||||
file_tree.setSelectionMode(file_tree.ExtendedSelection)
|
||||
file_tree.header().setSectionResizeMode(0, QHeaderView.Interactive)
|
||||
file_tree.header().setSectionResizeMode(1, QHeaderView.Interactive)
|
||||
file_tree.setBorderRadius(8)
|
||||
file_tree.setBorderVisible(True)
|
||||
|
||||
for sub in sub_dirs:
|
||||
dir_path = os.path.join(base_dir, sub)
|
||||
if not os.path.isdir(dir_path):
|
||||
continue
|
||||
group_item = TreeItem([sub, ""])
|
||||
file_tree.addTopLevelItem(group_item)
|
||||
has_file = False
|
||||
for root, _, files in os.walk(dir_path):
|
||||
rel_root = os.path.relpath(root, base_dir)
|
||||
for f in sorted(files):
|
||||
if f.endswith(".c"):
|
||||
mod_name = os.path.splitext(f)[0]
|
||||
rel_c = os.path.join(rel_root, f)
|
||||
key = f"{rel_root}/{mod_name}".replace("\\", "/")
|
||||
desc = describe_map.get(key, "")
|
||||
file_item = TreeItem([mod_name, desc])
|
||||
file_item.setFlags(file_item.flags() | Qt.ItemIsUserCheckable)
|
||||
file_item.setData(0, Qt.UserRole, rel_c)
|
||||
file_item.setData(0, Qt.UserRole + 1, key)
|
||||
file_item.setToolTip(1, desc)
|
||||
file_item.setTextAlignment(1, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
group_item.addChild(file_item)
|
||||
dst_c = os.path.join(user_dir, rel_c)
|
||||
if os.path.exists(dst_c):
|
||||
file_item.setCheckState(0, Qt.Unchecked)
|
||||
file_item.setText(0, f"{mod_name}(已存在)")
|
||||
file_item.setForeground(0, Qt.gray)
|
||||
else:
|
||||
file_item.setCheckState(0, Qt.Unchecked)
|
||||
group_item.addChild(file_item)
|
||||
has_file = True
|
||||
if not has_file:
|
||||
empty_item = TreeItem(["(无 .c 文件)", ""])
|
||||
group_item.addChild(empty_item)
|
||||
file_tree.expandAll()
|
||||
|
||||
def on_tree_item_changed(self, item, column):
|
||||
if column != 0:
|
||||
return
|
||||
if item.childCount() > 0:
|
||||
return
|
||||
if item.checkState(0) == Qt.Checked:
|
||||
key = item.data(0, Qt.UserRole + 1)
|
||||
deps = self._dependencies_map.get(key, [])
|
||||
if deps:
|
||||
checked = []
|
||||
root = self.codegen_page.file_tree.invisibleRootItem()
|
||||
for i in range(root.childCount()):
|
||||
group = root.child(i)
|
||||
for j in range(group.childCount()):
|
||||
child = group.child(j)
|
||||
ckey = child.data(0, Qt.UserRole + 1)
|
||||
if ckey in deps and child.checkState(0) != Qt.Checked:
|
||||
child.setCheckState(0, Qt.Checked)
|
||||
checked.append(ckey)
|
||||
if checked:
|
||||
descs = [self._describe_map.get(dep, dep) for dep in checked]
|
||||
InfoBar.info(
|
||||
title="依赖自动勾选",
|
||||
content="已自动勾选依赖模块: " + ",".join(descs),
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def get_checked_files(self):
|
||||
files = []
|
||||
def _traverse(item):
|
||||
for i in range(item.childCount()):
|
||||
child = item.child(i)
|
||||
if child.childCount() == 0 and child.checkState(0) == Qt.Checked:
|
||||
files.append(child.data(0, Qt.UserRole))
|
||||
_traverse(child)
|
||||
root = self.codegen_page.file_tree.invisibleRootItem()
|
||||
for i in range(root.childCount()):
|
||||
_traverse(root.child(i))
|
||||
return files
|
||||
|
||||
def generate_code(self):
|
||||
import shutil
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
base_dir = CodeGenerator.get_assets_dir("User_code")
|
||||
user_dir = os.path.join(self.project_path, "User")
|
||||
copied = []
|
||||
files = self.get_checked_files()
|
||||
skipped = []
|
||||
for rel_c in files:
|
||||
rel_h = rel_c[:-2] + ".h"
|
||||
src_c = os.path.join(base_dir, rel_c)
|
||||
src_h = os.path.join(base_dir, rel_h)
|
||||
dst_c = os.path.join(user_dir, rel_c)
|
||||
dst_h = os.path.join(user_dir, rel_h)
|
||||
if os.path.exists(dst_c):
|
||||
skipped.append(dst_c)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(dst_c), exist_ok=True)
|
||||
with open(src_c, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
save_with_preserve(dst_c, content)
|
||||
copied.append(dst_c)
|
||||
if os.path.exists(src_h):
|
||||
if os.path.exists(dst_h):
|
||||
skipped.append(dst_h)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(dst_h), exist_ok=True)
|
||||
with open(src_h, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
save_with_preserve(dst_h, content)
|
||||
copied.append(dst_h)
|
||||
msg = f"已拷贝 {len(copied)} 个文件到 User 目录"
|
||||
if skipped:
|
||||
msg += f"\n{len(skipped)} 个文件已存在,未覆盖"
|
||||
InfoBar.success(
|
||||
title="生成完成",
|
||||
content=msg,
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
self.show_user_code_files()
|
||||
|
||||
def generate_freertos_task(self):
|
||||
import re
|
||||
freertos_path = os.path.join(self.project_path, "Core", "Src", "freertos.c")
|
||||
if not os.path.exists(freertos_path):
|
||||
InfoBar.error(
|
||||
title="未找到 freertos.c",
|
||||
content="未找到 Core/Src/freertos.c 文件,请确认工程路径。",
|
||||
parent=self,
|
||||
duration=2500
|
||||
)
|
||||
return
|
||||
with open(freertos_path, "r", encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
|
||||
changed = False
|
||||
error_msgs = []
|
||||
|
||||
include_line = '#include "task/user_task.h"'
|
||||
if include_line not in code:
|
||||
include_pattern = r'(\/\* *USER CODE BEGIN Includes *\*\/\s*)'
|
||||
if re.search(include_pattern, code):
|
||||
code = re.sub(
|
||||
include_pattern,
|
||||
r'\1' + include_line + '\n',
|
||||
code
|
||||
)
|
||||
changed = True
|
||||
else:
|
||||
error_msgs.append("未找到 /* USER CODE BEGIN Includes */ 区域,无法插入 include。")
|
||||
|
||||
rtos_threads_pattern = r'(\/\* *USER CODE BEGIN RTOS_THREADS *\*\/\s*)(.*?)(\/\* *USER CODE END RTOS_THREADS *\*\/)'
|
||||
match = re.search(rtos_threads_pattern, code, re.DOTALL)
|
||||
task_line = ' osThreadNew(Task_Init, NULL, &attr_init); // 创建初始化任务\n'
|
||||
if match:
|
||||
threads_code = match.group(2)
|
||||
if 'Task_Init' not in threads_code:
|
||||
new_threads_code = match.group(1) + threads_code + task_line + match.group(3)
|
||||
code = code[:match.start()] + new_threads_code + code[match.end():]
|
||||
changed = True
|
||||
else:
|
||||
error_msgs.append("未找到 /* USER CODE BEGIN RTOS_THREADS */ 区域,无法插入任务创建代码。")
|
||||
|
||||
sdt_pattern = r'(\/\* *USER CODE BEGIN StartDefaultTask *\*\/\s*)(.*?)(\/\* *USER CODE END StartDefaultTask *\*\/)'
|
||||
match = re.search(sdt_pattern, code, re.DOTALL)
|
||||
if match:
|
||||
if 'osThreadTerminate(osThreadGetId());' not in match.group(2):
|
||||
new_sdt_code = match.group(1) + ' osThreadTerminate(osThreadGetId());\n' + match.group(3)
|
||||
code = code[:match.start()] + new_sdt_code + code[match.end():]
|
||||
changed = True
|
||||
else:
|
||||
error_msgs.append("未找到 /* USER CODE BEGIN StartDefaultTask */ 区域,无法插入终止代码。")
|
||||
|
||||
if changed:
|
||||
with open(freertos_path, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
InfoBar.success(
|
||||
title="生成成功",
|
||||
content="FreeRTOS任务代码已自动生成!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
elif error_msgs:
|
||||
InfoBar.error(
|
||||
title="生成失败",
|
||||
content="\n".join(error_msgs),
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
else:
|
||||
InfoBar.info(
|
||||
title="无需修改",
|
||||
content="FreeRTOS任务相关代码已存在,无需重复生成。",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def open_task_config_dialog(self):
|
||||
config_path = os.path.join(self.project_path, "User", "task", "config.yaml")
|
||||
dlg = TaskConfigDialog(self, config_path=config_path)
|
||||
if dlg.exec() == QDialog.Accepted:
|
||||
try:
|
||||
tasks = dlg.get_tasks()
|
||||
except Exception as e:
|
||||
InfoBar.error(
|
||||
title="参数错误",
|
||||
content=str(e),
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
return
|
||||
if not tasks:
|
||||
InfoBar.warning(
|
||||
title="未配置任务",
|
||||
content="请至少添加一个任务!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
return
|
||||
try:
|
||||
self.generate_task_code(tasks)
|
||||
InfoBar.success(
|
||||
title="生成成功",
|
||||
content="任务代码已生成到 User/task 目录!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
self.task_generate_success = True # 添加这一句
|
||||
except Exception as e:
|
||||
InfoBar.error(
|
||||
title="生成失败",
|
||||
content=f"任务代码生成失败: {e}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def generate_task_code(self, task_list):
|
||||
from app.tools.code_generator import CodeGenerator
|
||||
template_dir = CodeGenerator.get_assets_dir("User_code/task")
|
||||
output_dir = os.path.join(self.project_path, "User", "task")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
user_task_h_tpl = os.path.join(template_dir, "user_task.h.template")
|
||||
user_task_c_tpl = os.path.join(template_dir, "user_task.c.template")
|
||||
init_c_tpl = os.path.join(template_dir, "init.c.template")
|
||||
task_c_tpl = os.path.join(template_dir, "task.c.template")
|
||||
|
||||
freq_tasks = [t for t in task_list if t.get("freq_control", True)]
|
||||
|
||||
def render_template(path, context):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
tpl = Template(f.read())
|
||||
return tpl.render(**context)
|
||||
|
||||
context_h = {
|
||||
"thread_definitions": "\n".join([f" osThreadId_t {t['name']};" for t in task_list]),
|
||||
"freq_definitions": "\n".join([f" float {t['name']};" for t in freq_tasks]),
|
||||
"stack_definitions": "\n".join([f" UBaseType_t {t['name']};" for t in task_list]),
|
||||
"last_up_time_definitions": "\n".join([f" float {t['name']};" for t in freq_tasks]),
|
||||
"task_frequency_definitions": "\n".join([f"#define {t['name'].upper()}_FREQ ({t['frequency']})" for t in freq_tasks]),
|
||||
"task_init_delay_definitions": "\n".join([f"#define {t['name'].upper()}_INIT_DELAY ({t['delay']})" for t in task_list]),
|
||||
"task_attr_declarations": "\n".join([f"extern const osThreadAttr_t attr_{t['name']};" for t in task_list]),
|
||||
"task_function_declarations": "\n".join([f"void {t['function']}(void *argument);" for t in task_list]),
|
||||
}
|
||||
user_task_h_path = os.path.join(output_dir, "user_task.h")
|
||||
new_user_task_h = render_template(user_task_h_tpl, context_h)
|
||||
save_with_preserve(user_task_h_path, new_user_task_h)
|
||||
|
||||
context_c = {
|
||||
"task_attr_definitions": "\n".join([
|
||||
f"const osThreadAttr_t attr_{t['name']} = {{\n"
|
||||
f" .name = \"{t['name']}\",\n"
|
||||
f" .priority = osPriorityNormal,\n"
|
||||
f" .stack_size = {t['stack']} * 4,\n"
|
||||
f"}};"
|
||||
for t in task_list
|
||||
])
|
||||
}
|
||||
user_task_c_path = os.path.join(output_dir, "user_task.c")
|
||||
user_task_c = render_template(user_task_c_tpl, context_c)
|
||||
save_with_preserve(user_task_c_path, user_task_c)
|
||||
|
||||
thread_creation_code = "\n".join([
|
||||
f" task_runtime.thread.{t['name']} = osThreadNew({t['function']}, NULL, &attr_{t['name']});"
|
||||
for t in task_list
|
||||
])
|
||||
context_init = {
|
||||
"thread_creation_code": thread_creation_code,
|
||||
}
|
||||
init_c_path = os.path.join(output_dir, "init.c")
|
||||
init_c = render_template(init_c_tpl, context_init)
|
||||
save_with_preserve(init_c_path, init_c)
|
||||
|
||||
for t in task_list:
|
||||
desc = t.get("description", "")
|
||||
desc_wrapped = "\n ".join(textwrap.wrap(desc, 20))
|
||||
context_task = {
|
||||
"task_name": t["name"],
|
||||
"task_function": t["function"],
|
||||
"task_frequency": f"{t['name'].upper()}_FREQ" if t.get("freq_control", True) else None,
|
||||
"task_delay": f"{t['name'].upper()}_INIT_DELAY",
|
||||
"task_description": desc_wrapped,
|
||||
"freq_control": t.get("freq_control", True)
|
||||
}
|
||||
with open(task_c_tpl, encoding="utf-8") as f:
|
||||
tpl = Template(f.read())
|
||||
code = tpl.render(**context_task)
|
||||
task_c_path = os.path.join(output_dir, f"{t['name']}.c")
|
||||
save_with_preserve(task_c_path, code)
|
||||
|
||||
config_yaml_path = os.path.join(output_dir, "config.yaml")
|
||||
with open(config_yaml_path, "w", encoding="utf-8") as f:
|
||||
yaml.safe_dump(task_list, f, allow_unicode=True)
|
||||
@ -1,260 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon
|
||||
from openpyxl import load_workbook, Workbook
|
||||
|
||||
import numpy as np
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||
import plotly.graph_objs as go
|
||||
import plotly.io as pio
|
||||
|
||||
import matplotlib
|
||||
matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Source Han Sans', 'STHeiti', 'Heiti TC']
|
||||
matplotlib.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
class FunctionFitInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("functionFitInterface")
|
||||
|
||||
main_layout = QHBoxLayout(self)
|
||||
main_layout.setSpacing(24)
|
||||
|
||||
# 左侧:数据输入区
|
||||
left_layout = QVBoxLayout()
|
||||
left_layout.setSpacing(16)
|
||||
|
||||
self.dataTable = TableWidget(self)
|
||||
self.dataTable.setColumnCount(2)
|
||||
self.dataTable.setHorizontalHeaderLabels(["x", "y"])
|
||||
self.dataTable.setColumnWidth(0, 125)
|
||||
self.dataTable.setColumnWidth(1, 125)
|
||||
left_layout.addWidget(self.dataTable)
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
add_row_btn = PushButton("添加一行")
|
||||
add_row_btn.clicked.connect(self.add_row)
|
||||
|
||||
del_row_btn = PushButton("删除选中行") # 新增按钮
|
||||
del_row_btn.clicked.connect(self.delete_selected_row) # 绑定槽函数
|
||||
btn_layout.addWidget(add_row_btn)
|
||||
|
||||
btn_layout.addWidget(del_row_btn) # 添加到布局
|
||||
left_layout.addLayout(btn_layout)
|
||||
|
||||
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
import_btn = PushButton("导入 Excel")
|
||||
import_btn.clicked.connect(self.import_excel)
|
||||
export_btn = PushButton("导出 Excel")
|
||||
export_btn.clicked.connect(self.export_excel)
|
||||
btn_layout.addWidget(import_btn)
|
||||
btn_layout.addWidget(export_btn)
|
||||
left_layout.addLayout(btn_layout)
|
||||
|
||||
self.dataTable.setMinimumWidth(280)
|
||||
self.dataTable.setMaximumWidth(280)
|
||||
main_layout.addLayout(left_layout, 1)
|
||||
|
||||
self.add_row()
|
||||
|
||||
# 右侧:图像展示区
|
||||
right_layout = QVBoxLayout()
|
||||
right_layout.setSpacing(12)
|
||||
right_layout.addWidget(SubtitleLabel("函数图像预览"))
|
||||
|
||||
self.figure = Figure(figsize=(5, 4))
|
||||
self.canvas = FigureCanvas(self.figure)
|
||||
right_layout.addWidget(self.canvas, stretch=1)
|
||||
|
||||
self.resultLabel = BodyLabel("")
|
||||
self.resultLabel.setWordWrap(True) # 自动换行
|
||||
right_layout.addWidget(self.resultLabel)
|
||||
|
||||
|
||||
# 拟合阶数和输出语言选择(合并到同一行)
|
||||
options_layout = QHBoxLayout()
|
||||
self.spinBox = SpinBox()
|
||||
self.spinBox.setRange(1, 10)
|
||||
self.spinBox.setValue(2)
|
||||
options_layout.addWidget(SubtitleLabel("拟合阶数"))
|
||||
options_layout.addWidget(self.spinBox)
|
||||
|
||||
self.langBox = ComboBox()
|
||||
self.langBox.addItems(["C/C++", "Python"])
|
||||
options_layout.addWidget(SubtitleLabel("输出语言"))
|
||||
options_layout.addWidget(self.langBox)
|
||||
|
||||
|
||||
right_layout.addLayout(options_layout)
|
||||
|
||||
|
||||
# 代码显示和复制按钮
|
||||
self.codeLabel = BodyLabel("")
|
||||
self.codeLabel.setWordWrap(True) # 自动换行
|
||||
right_layout.addWidget(self.codeLabel)
|
||||
|
||||
btn_layout = QHBoxLayout() # 新增一行布局
|
||||
fit_btn = PushButton(FluentIcon.UNIT,"拟合并绘图")
|
||||
fit_btn.clicked.connect(self.fit_and_plot)
|
||||
btn_layout.addWidget(fit_btn)
|
||||
copy_btn = PushButton(FluentIcon.COPY, "复制代码")
|
||||
copy_btn.clicked.connect(self.copy_code)
|
||||
btn_layout.addWidget(copy_btn)
|
||||
right_layout.addLayout(btn_layout)
|
||||
|
||||
|
||||
main_layout.addLayout(right_layout, 2)
|
||||
|
||||
# 默认显示空图像
|
||||
self.figure.clear()
|
||||
ax = self.figure.add_subplot(111)
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
self.canvas.draw()
|
||||
|
||||
def add_row(self):
|
||||
row = self.dataTable.rowCount()
|
||||
self.dataTable.insertRow(row)
|
||||
# 可选:初始化为空字符串
|
||||
self.dataTable.setItem(row, 0, QTableWidgetItem(""))
|
||||
self.dataTable.setItem(row, 1, QTableWidgetItem(""))
|
||||
|
||||
def delete_selected_row(self):
|
||||
selected = self.dataTable.selectedItems()
|
||||
if selected:
|
||||
rows = set(item.row() for item in selected)
|
||||
for row in sorted(rows, reverse=True):
|
||||
self.dataTable.removeRow(row)
|
||||
|
||||
def import_excel(self):
|
||||
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx)")
|
||||
if path:
|
||||
wb = load_workbook(path)
|
||||
ws = wb.active
|
||||
self.dataTable.setRowCount(0)
|
||||
for row_data in ws.iter_rows(min_row=2, values_only=True): # 跳过表头
|
||||
row = self.dataTable.rowCount()
|
||||
self.dataTable.insertRow(row)
|
||||
for col, value in enumerate(row_data[:2]):
|
||||
item = QTableWidgetItem(str(value) if value is not None else "")
|
||||
self.dataTable.setItem(row, col, item)
|
||||
|
||||
|
||||
def export_excel(self):
|
||||
path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)")
|
||||
if path:
|
||||
data = self.parse_data()
|
||||
if data is not None:
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.append(["x", "y"])
|
||||
for row in data:
|
||||
ws.append(row)
|
||||
wb.save(path)
|
||||
|
||||
def parse_data(self):
|
||||
data = []
|
||||
row_count = self.dataTable.rowCount()
|
||||
for row in range(row_count):
|
||||
try:
|
||||
x_item = self.dataTable.item(row, 0)
|
||||
y_item = self.dataTable.item(row, 1)
|
||||
if x_item is None or y_item is None:
|
||||
continue
|
||||
x = float(x_item.text())
|
||||
y = float(y_item.text())
|
||||
data.append([x, y])
|
||||
except Exception:
|
||||
continue
|
||||
return data if data else None
|
||||
|
||||
def fit_and_plot(self):
|
||||
data = self.parse_data()
|
||||
if not data:
|
||||
self.resultLabel.setText("数据格式错误或为空")
|
||||
self.codeLabel.setText("")
|
||||
return
|
||||
|
||||
x = np.array([d[0] for d in data])
|
||||
y = np.array([d[1] for d in data])
|
||||
degree = self.spinBox.value()
|
||||
coeffs = np.polyfit(x, y, degree)
|
||||
|
||||
# 用更密集的横坐标画拟合曲线
|
||||
x_fit = np.linspace(x.min(), x.max(), 100)
|
||||
y_fit = np.polyval(coeffs, x_fit)
|
||||
|
||||
self.figure.clear()
|
||||
ax = self.figure.add_subplot(111)
|
||||
ax.scatter(x, y, color='blue', label='raw data')
|
||||
ax.plot(x_fit, y_fit, color='red', label=f'Fitted curve')
|
||||
ax.set_title('graph of a function')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.legend()
|
||||
self.canvas.draw()
|
||||
|
||||
formula = self.poly_formula(coeffs)
|
||||
self.resultLabel.setText(f"拟合公式: {formula}")
|
||||
|
||||
lang = self.langBox.currentText()
|
||||
code = self.generate_code(coeffs, lang)
|
||||
self.codeLabel.setText(code)
|
||||
|
||||
def poly_formula(self, coeffs):
|
||||
terms = []
|
||||
degree = len(coeffs) - 1
|
||||
for i, c in enumerate(coeffs):
|
||||
power = degree - i
|
||||
if abs(c) < 1e-8:
|
||||
continue
|
||||
if power == 0:
|
||||
terms.append(f"{c:.6g}")
|
||||
elif power == 1:
|
||||
terms.append(f"{c:.6g}*x")
|
||||
else:
|
||||
terms.append(f"{c:.6g}*x^{power}")
|
||||
return " + ".join(terms)
|
||||
|
||||
def generate_code(self, coeffs, lang):
|
||||
degree = len(coeffs) - 1
|
||||
if lang == "C/C++":
|
||||
code = "double poly(double x) {\n return "
|
||||
elif lang == "Python":
|
||||
code = "def poly(x):\n return "
|
||||
else:
|
||||
code = ""
|
||||
terms = []
|
||||
for i, c in enumerate(coeffs):
|
||||
power = degree - i
|
||||
if abs(c) < 1e-8:
|
||||
continue
|
||||
if power == 0:
|
||||
terms.append(f"{c:.6g}")
|
||||
elif power == 1:
|
||||
terms.append(f"{c:.6g}*x")
|
||||
else:
|
||||
terms.append(f"{c:.6g}*pow(x,{power})" if lang == "C/C++" else f"{c:.6g}*x**{power}")
|
||||
code += " + ".join(terms)
|
||||
code += ";\n}" if lang == "C/C++" else ""
|
||||
return code
|
||||
|
||||
|
||||
def copy_code(self):
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(self.codeLabel.text())
|
||||
# 弹出提示
|
||||
InfoBar.success(
|
||||
title='复制成功',
|
||||
content="代码已复制到剪贴板!",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000,
|
||||
parent=self
|
||||
)
|
||||
@ -1,65 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, ImageLabel, FluentLabelBase, TitleLabel
|
||||
import sys
|
||||
import os
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""获取资源文件的绝对路径,兼容打包和开发环境"""
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
# PyInstaller 打包后的临时目录
|
||||
return os.path.join(sys._MEIPASS, relative_path)
|
||||
return os.path.join(os.path.abspath("."), relative_path)
|
||||
|
||||
class HomeInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("homeInterface")
|
||||
|
||||
# 外层居中布局
|
||||
outer_layout = QVBoxLayout(self)
|
||||
outer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
outer_layout.setSpacing(0)
|
||||
outer_layout.addStretch()
|
||||
|
||||
# 内容布局
|
||||
content_layout = QVBoxLayout()
|
||||
content_layout.setSpacing(24)
|
||||
content_layout.setContentsMargins(48, 48, 48, 48)
|
||||
|
||||
# Logo
|
||||
logo = ImageLabel(resource_path('assets/logo/MRobot.png'))
|
||||
logo.scaledToHeight(80)
|
||||
content_layout.addWidget(logo, alignment=Qt.AlignHCenter) # 居中对齐
|
||||
|
||||
content_layout.addSpacing(8)
|
||||
content_layout.addStretch()
|
||||
|
||||
# 主标题
|
||||
title = TitleLabel("MRobot Toolbox")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(title)
|
||||
|
||||
# 副标题
|
||||
subtitle = BodyLabel("现代化,多功能机器人开发工具箱")
|
||||
subtitle.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(subtitle)
|
||||
|
||||
# 欢迎语
|
||||
welcome = BodyLabel("欢迎使用 MRobot Toolbox!一站式支持代码生成、硬件管理、串口调试与零件库下载。")
|
||||
welcome.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(welcome)
|
||||
|
||||
content_layout.addSpacing(16)
|
||||
content_layout.addStretch()
|
||||
|
||||
# 加到主布局
|
||||
outer_layout.addLayout(content_layout)
|
||||
outer_layout.addStretch()
|
||||
|
||||
# 版权信息置底
|
||||
copyright_label = BodyLabel("© 2025 MRobot | Powered by QUT RM&RCer")
|
||||
copyright_label.setAlignment(Qt.AlignCenter)
|
||||
copyright_label.setStyleSheet("font-size: 13px;")
|
||||
outer_layout.addWidget(copyright_label)
|
||||
outer_layout.addSpacing(18)
|
||||
@ -1,90 +0,0 @@
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
with redirect_stdout(None):
|
||||
from qfluentwidgets import NavigationItemPosition, FluentWindow, SplashScreen, setThemeColor, NavigationBarPushButton, toggleTheme, setTheme, Theme, NavigationAvatarWidget, NavigationToolButton ,NavigationPushButton
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition
|
||||
|
||||
from .home_interface import HomeInterface
|
||||
from .serial_terminal_interface import SerialTerminalInterface
|
||||
from .part_library_interface import PartLibraryInterface
|
||||
from .data_interface import DataInterface
|
||||
from .mini_tool_interface import MiniToolInterface
|
||||
from .code_configuration_interface import CodeConfigurationInterface
|
||||
from .about_interface import AboutInterface
|
||||
import base64
|
||||
|
||||
|
||||
class MainWindow(FluentWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.initWindow()
|
||||
self.initInterface()
|
||||
self.initNavigation()
|
||||
|
||||
# 后台检查更新(不弹窗,只显示通知)
|
||||
# self.check_updates_in_background()
|
||||
|
||||
def initWindow(self):
|
||||
self.setMicaEffectEnabled(False)
|
||||
setThemeColor('#f18cb9', lazy=True)
|
||||
setTheme(Theme.AUTO, lazy=True)
|
||||
|
||||
self.resize(960, 640)
|
||||
self.setWindowIcon(QIcon('./assets/logo/M2.ico'))
|
||||
self.setWindowTitle("MRobot Toolbox")
|
||||
|
||||
|
||||
desktop = QApplication.desktop().availableGeometry() # 获取可用屏幕大小
|
||||
w, h = desktop.width(), desktop.height()
|
||||
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
|
||||
|
||||
self.show()
|
||||
QApplication.processEvents()
|
||||
|
||||
def initInterface(self):
|
||||
self.homeInterface = HomeInterface(self)
|
||||
self.serialTerminalInterface = SerialTerminalInterface(self)
|
||||
self.partLibraryInterface = PartLibraryInterface(self)
|
||||
# self.dataInterface = DataInterface(self)
|
||||
self.miniToolInterface = MiniToolInterface(self)
|
||||
self.codeConfigurationInterface = CodeConfigurationInterface(self)
|
||||
|
||||
|
||||
def initNavigation(self):
|
||||
self.addSubInterface(self.homeInterface, FIF.HOME, self.tr('主页'))
|
||||
# self.addSubInterface(self.dataInterface, FIF.CODE, self.tr('代码生成'))
|
||||
self.addSubInterface(self.codeConfigurationInterface, FIF.CODE, self.tr('代码生成'))
|
||||
self.addSubInterface(self.serialTerminalInterface, FIF.COMMAND_PROMPT,self.tr('串口助手'))
|
||||
self.addSubInterface(self.partLibraryInterface, FIF.DOWNLOAD, self.tr('零件库'))
|
||||
self.addSubInterface(self.miniToolInterface, FIF.LIBRARY, self.tr('迷你工具箱'))
|
||||
self.addSubInterface(AboutInterface(self), FIF.INFO, self.tr('关于'), position=NavigationItemPosition.BOTTOM)
|
||||
|
||||
|
||||
|
||||
self.themeBtn = NavigationPushButton(FIF.BRUSH, "切换主题", False, self.navigationInterface)
|
||||
self.themeBtn.clicked.connect(lambda: toggleTheme(lazy=True))
|
||||
self.navigationInterface.addWidget(
|
||||
'themeButton',
|
||||
self.themeBtn,
|
||||
None,
|
||||
NavigationItemPosition.BOTTOM
|
||||
)
|
||||
|
||||
def check_updates_in_background(self):
|
||||
"""后台检查更新"""
|
||||
try:
|
||||
# 后台更新检查已移至关于页面手动触发
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"初始化完成: {e}")
|
||||
|
||||
# main_window.py 只需修改关闭事件
|
||||
def closeEvent(self, e):
|
||||
# if self.themeListener and self.themeListener.isRunning():
|
||||
# self.themeListener.terminate()
|
||||
# self.themeListener.deleteLater()
|
||||
super().closeEvent(e)
|
||||
@ -1,107 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QSizePolicy
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
|
||||
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from .ai_interface import AIInterface
|
||||
|
||||
class MiniToolInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("minitoolInterface")
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setAlignment(Qt.AlignTop)
|
||||
self.vBoxLayout.setContentsMargins(10, 0, 10, 10) # 设置外边距
|
||||
|
||||
# 顶部标签栏,横向拉伸
|
||||
self.tabBar = TabBar(self)
|
||||
self.tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.vBoxLayout.addWidget(self.tabBar) # 移除 Qt.AlignLeft
|
||||
|
||||
self.stackedWidget = QStackedWidget(self)
|
||||
self.vBoxLayout.addWidget(self.stackedWidget) # 加入布局
|
||||
|
||||
|
||||
# 初始主页面
|
||||
self.mainPage = QWidget(self)
|
||||
mainLayout = QVBoxLayout(self.mainPage)
|
||||
mainLayout.setAlignment(Qt.AlignTop) # 卡片靠顶部
|
||||
self.card = PushSettingCard(
|
||||
text="▶ 启动",
|
||||
icon=FluentIcon.UNIT,
|
||||
title="曲线拟合工具",
|
||||
content="简单的曲线拟合工具,支持多种函数类型",
|
||||
)
|
||||
mainLayout.addWidget(self.card)
|
||||
|
||||
self.mainPage.setLayout(mainLayout)
|
||||
self.aiCard = PushSettingCard(
|
||||
text="▶ 启动",
|
||||
icon=FluentIcon.ROBOT,
|
||||
title="MRobot AI助手",
|
||||
content="与 MRobot 进行图一乐交流, 使用开源模型qwen3:0.6b。",
|
||||
)
|
||||
mainLayout.addWidget(self.aiCard)
|
||||
self.aiCard.clicked.connect(self.open_ai_tab)
|
||||
# 添加主页面到堆叠窗口
|
||||
self.addSubInterface(self.mainPage, "mainPage", "工具箱主页")
|
||||
|
||||
self.setLayout(self.vBoxLayout)
|
||||
|
||||
# 信号连接
|
||||
self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
|
||||
# self.tabBar.tabAddRequested.connect(self.onAddNewTab)
|
||||
self.tabBar.tabCloseRequested.connect(self.onCloseTab)
|
||||
self.card.clicked.connect(self.open_fit_tab)
|
||||
|
||||
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
|
||||
widget.setObjectName(objectName)
|
||||
self.stackedWidget.addWidget(widget)
|
||||
self.tabBar.addTab(
|
||||
routeKey=objectName,
|
||||
text=text,
|
||||
onClick=lambda: self.stackedWidget.setCurrentWidget(widget)
|
||||
)
|
||||
|
||||
def onCurrentIndexChanged(self, index):
|
||||
widget = self.stackedWidget.widget(index)
|
||||
self.tabBar.setCurrentTab(widget.objectName())
|
||||
|
||||
def onAddNewTab(self):
|
||||
pass # 可自定义添加新标签页逻辑
|
||||
|
||||
def onCloseTab(self, index: int):
|
||||
item = self.tabBar.tabItem(index)
|
||||
widget = self.findChild(QWidget, item.routeKey())
|
||||
# 禁止关闭主页
|
||||
if widget.objectName() == "mainPage":
|
||||
return
|
||||
self.stackedWidget.removeWidget(widget)
|
||||
self.tabBar.removeTab(index)
|
||||
widget.deleteLater()
|
||||
|
||||
def open_fit_tab(self):
|
||||
# 检查是否已存在标签页,避免重复添加
|
||||
for i in range(self.stackedWidget.count()):
|
||||
widget = self.stackedWidget.widget(i)
|
||||
if widget.objectName() == "fitPage":
|
||||
self.stackedWidget.setCurrentWidget(widget)
|
||||
self.tabBar.setCurrentTab("fitPage")
|
||||
return
|
||||
fit_page = FunctionFitInterface(self)
|
||||
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
|
||||
self.stackedWidget.setCurrentWidget(fit_page)
|
||||
self.tabBar.setCurrentTab("fitPage")
|
||||
|
||||
def open_ai_tab(self):
|
||||
# 检查是否已存在标签页,避免重复添加
|
||||
for i in range(self.stackedWidget.count()):
|
||||
widget = self.stackedWidget.widget(i)
|
||||
if widget.objectName() == "aiPage":
|
||||
self.stackedWidget.setCurrentWidget(widget)
|
||||
self.tabBar.setCurrentTab("aiPage")
|
||||
return
|
||||
ai_page = AIInterface(self)
|
||||
self.addSubInterface(ai_page, "aiPage", "AI问答")
|
||||
self.stackedWidget.setCurrentWidget(ai_page)
|
||||
self.tabBar.setCurrentTab("aiPage")
|
||||
@ -1,205 +0,0 @@
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
||||
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, PushButton, TreeWidget, ProgressBar, Dialog, InfoBar, InfoBarPosition, FluentIcon, ProgressRing, Dialog
|
||||
import requests
|
||||
import shutil
|
||||
import os
|
||||
from .tools.part_download import DownloadThread # 新增导入
|
||||
|
||||
from urllib.parse import quote
|
||||
class PartLibraryInterface(QWidget):
|
||||
SERVER_URL = "http://qutrobot.top:5000"
|
||||
SECRET_KEY = "MRobot_Download"
|
||||
LOCAL_LIB_DIR = "assets/mech_lib"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setObjectName("partLibraryInterface")
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20) # 添加边距
|
||||
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, self.tree.header().Stretch)
|
||||
self.tree.header().setSectionResizeMode(1, self.tree.header().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:
|
||||
dialog = Dialog(
|
||||
title="温馨提示",
|
||||
content="请先勾选需要下载的文件。",
|
||||
parent=self
|
||||
)
|
||||
dialog.yesButton.setText("知道啦")
|
||||
dialog.cancelButton.hide()
|
||||
dialog.exec()
|
||||
return
|
||||
|
||||
# 创建进度环
|
||||
self.progress_ring = ProgressRing()
|
||||
self.progress_ring.setRange(0, 100)
|
||||
self.progress_ring.setValue(0)
|
||||
self.progress_ring.setTextVisible(True)
|
||||
self.progress_ring.setFixedSize(32, 32)
|
||||
self.progress_ring.setStrokeWidth(4)
|
||||
|
||||
# 展示消息条(关闭按钮即中断下载)
|
||||
self.info_bar = InfoBar(
|
||||
icon=FluentIcon.DOWNLOAD,
|
||||
title="正在下载",
|
||||
content="正在下载选中文件...",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=-1 # 不自动消失
|
||||
)
|
||||
self.info_bar.addWidget(self.progress_ring)
|
||||
self.info_bar.closeButton.clicked.connect(self.stop_download) # 关闭即中断下载
|
||||
self.info_bar.show()
|
||||
|
||||
# 启动下载线程
|
||||
self.download_thread = DownloadThread(
|
||||
files, self.SERVER_URL, self.SECRET_KEY, self.LOCAL_LIB_DIR
|
||||
)
|
||||
self.download_thread.progressChanged.connect(self.progress_ring.setValue)
|
||||
self.download_thread.finished.connect(self.on_download_finished)
|
||||
self.download_thread.finished.connect(self.download_thread.deleteLater)
|
||||
self.download_thread.start()
|
||||
|
||||
def stop_download(self):
|
||||
if hasattr(self, "download_thread") and self.download_thread.isRunning():
|
||||
self.download_thread.terminate()
|
||||
self.download_thread.wait()
|
||||
self.info_bar.close()
|
||||
InfoBar.warning(
|
||||
title="下载已中断",
|
||||
content="已手动中断下载任务。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
def on_download_finished(self, success, fail):
|
||||
self.info_bar.close()
|
||||
msg = f"成功下载:{len(success)} 个文件,失败:{len(fail)} 个文件"
|
||||
|
||||
# 创建“打开文件夹”按钮
|
||||
open_btn = PushButton("打开文件夹")
|
||||
def open_folder():
|
||||
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])
|
||||
|
||||
# 展示成功消息条,自动消失
|
||||
self.result_bar = InfoBar.success(
|
||||
title="下载完成",
|
||||
content=msg,
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000 # 4秒后自动消失
|
||||
)
|
||||
self.result_bar.addWidget(open_btn)
|
||||
open_btn.clicked.connect(open_folder)
|
||||
self.result_bar.show()
|
||||
|
||||
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])
|
||||
@ -1,167 +0,0 @@
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt5.QtGui import QTextCursor
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from qfluentwidgets import (
|
||||
FluentIcon, PushButton, ComboBox, TextEdit, LineEdit, CheckBox,
|
||||
SubtitleLabel, BodyLabel, HorizontalSeparator
|
||||
)
|
||||
|
||||
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(QWidget):
|
||||
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 = [
|
||||
("线程监视器", "htop"),
|
||||
("陀螺仪校准", "cali_gyro"),
|
||||
("性能监视", "htop"),
|
||||
("重启", "reset"),
|
||||
("显示所有设备", "ls /dev"),
|
||||
("查询id", "id"),
|
||||
]
|
||||
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()
|
||||
@ -1,321 +0,0 @@
|
||||
class analyzing_ioc:
|
||||
@staticmethod
|
||||
def is_freertos_enabled_from_ioc(ioc_path):
|
||||
"""
|
||||
检查指定 .ioc 文件是否开启了 FreeRTOS
|
||||
"""
|
||||
config = {}
|
||||
with open(ioc_path, encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
config[key.strip()] = value.strip()
|
||||
ip_keys = [k for k in config if k.startswith('Mcu.IP')]
|
||||
for k in ip_keys:
|
||||
if config[k] == 'FREERTOS':
|
||||
return True
|
||||
for k in config:
|
||||
if k.startswith('FREERTOS.'):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_enabled_i2c_from_ioc(ioc_path):
|
||||
"""
|
||||
从.ioc文件中获取已启用的I2C列表
|
||||
返回格式: ['I2C1', 'I2C3'] 等
|
||||
"""
|
||||
enabled_i2c = []
|
||||
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
# 检查是否启用了I2C
|
||||
if key.startswith('Mcu.IP') and value.startswith('I2C'):
|
||||
# 提取I2C编号,如I2C1, I2C2等
|
||||
i2c_name = value.split('.')[0] if '.' in value else value
|
||||
if i2c_name not in enabled_i2c:
|
||||
enabled_i2c.append(i2c_name)
|
||||
return sorted(enabled_i2c)
|
||||
|
||||
@staticmethod
|
||||
def get_enabled_spi_from_ioc(ioc_path):
|
||||
"""
|
||||
获取已启用的SPI列表
|
||||
返回格式: ['SPI1', 'SPI2'] 等
|
||||
"""
|
||||
enabled_spi = []
|
||||
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if key.startswith('Mcu.IP') and value.startswith('SPI'):
|
||||
spi_name = value.split('.')[0] if '.' in value else value
|
||||
if spi_name not in enabled_spi:
|
||||
enabled_spi.append(spi_name)
|
||||
return sorted(enabled_spi)
|
||||
|
||||
@staticmethod
|
||||
def get_enabled_can_from_ioc(ioc_path):
|
||||
"""
|
||||
获取已启用的CAN列表
|
||||
返回格式: ['CAN1', 'CAN2'] 等
|
||||
"""
|
||||
enabled_can = []
|
||||
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if key.startswith('Mcu.IP') and value.startswith('CAN'):
|
||||
can_name = value.split('.')[0] if '.' in value else value
|
||||
if can_name not in enabled_can:
|
||||
enabled_can.append(can_name)
|
||||
return sorted(enabled_can)
|
||||
|
||||
@staticmethod
|
||||
def get_enabled_uart_from_ioc(ioc_path):
|
||||
"""
|
||||
获取已启用的UART/USART列表
|
||||
返回格式: ['USART1', 'USART2', 'UART4'] 等
|
||||
"""
|
||||
enabled_uart = []
|
||||
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
# 检查是否启用了UART或USART
|
||||
if key.startswith('Mcu.IP') and (value.startswith('USART') or value.startswith('UART')):
|
||||
uart_name = value.split('.')[0] if '.' in value else value
|
||||
if uart_name not in enabled_uart:
|
||||
enabled_uart.append(uart_name)
|
||||
return sorted(enabled_uart)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_enabled_gpio_from_ioc(ioc_path):
|
||||
"""
|
||||
获取所有带 EXTI 且有 Label 的 GPIO,排除其他外设功能的引脚
|
||||
"""
|
||||
gpio_list = []
|
||||
gpio_configs = {}
|
||||
|
||||
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# 收集GPIO相关配置
|
||||
if '.' in key:
|
||||
pin = key.split('.')[0]
|
||||
param = key.split('.', 1)[1]
|
||||
|
||||
if pin not in gpio_configs:
|
||||
gpio_configs[pin] = {}
|
||||
|
||||
gpio_configs[pin][param] = value
|
||||
|
||||
# 定义需要排除的Signal类型
|
||||
excluded_signals = [
|
||||
'SPI1_SCK', 'SPI1_MISO', 'SPI1_MOSI', 'SPI2_SCK', 'SPI2_MISO', 'SPI2_MOSI',
|
||||
'SPI3_SCK', 'SPI3_MISO', 'SPI3_MOSI',
|
||||
'I2C1_SCL', 'I2C1_SDA', 'I2C2_SCL', 'I2C2_SDA', 'I2C3_SCL', 'I2C3_SDA',
|
||||
'USART1_TX', 'USART1_RX', 'USART2_TX', 'USART2_RX', 'USART3_TX', 'USART3_RX',
|
||||
'USART6_TX', 'USART6_RX', 'UART4_TX', 'UART4_RX', 'UART5_TX', 'UART5_RX',
|
||||
'CAN1_TX', 'CAN1_RX', 'CAN2_TX', 'CAN2_RX',
|
||||
'USB_OTG_FS_DM', 'USB_OTG_FS_DP', 'USB_OTG_HS_DM', 'USB_OTG_HS_DP',
|
||||
'SYS_JTMS-SWDIO', 'SYS_JTCK-SWCLK', 'SYS_JTDI', 'SYS_JTDO-SWO',
|
||||
'RCC_OSC_IN', 'RCC_OSC_OUT',
|
||||
]
|
||||
|
||||
# 处理每个GPIO配置,只选择EXTI类型的
|
||||
for pin, config in gpio_configs.items():
|
||||
signal = config.get('Signal', '')
|
||||
|
||||
# 只处理有Label和EXTI功能的GPIO
|
||||
if ('GPIO_Label' not in config or
|
||||
('GPIO_ModeDefaultEXTI' not in config and not signal.startswith('GPXTI'))):
|
||||
continue
|
||||
|
||||
# 排除用于其他外设功能的引脚
|
||||
if signal in excluded_signals or signal.startswith('S_TIM') or signal.startswith('ADC'):
|
||||
continue
|
||||
|
||||
# 只包含EXTI功能的GPIO
|
||||
if signal.startswith('GPXTI'):
|
||||
label = config['GPIO_Label']
|
||||
gpio_list.append({'pin': pin, 'label': label})
|
||||
|
||||
return gpio_list
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_all_gpio_from_ioc(ioc_path):
|
||||
"""
|
||||
获取所有GPIO配置,但排除用于其他外设功能的引脚
|
||||
只包含纯GPIO功能:GPIO_Input, GPIO_Output, GPXTI
|
||||
"""
|
||||
gpio_list = []
|
||||
gpio_configs = {}
|
||||
|
||||
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# 收集GPIO相关配置
|
||||
if '.' in key:
|
||||
pin = key.split('.')[0]
|
||||
param = key.split('.', 1)[1]
|
||||
|
||||
if pin not in gpio_configs:
|
||||
gpio_configs[pin] = {}
|
||||
|
||||
gpio_configs[pin][param] = value
|
||||
|
||||
# 定义需要排除的Signal类型(用于其他外设功能的)
|
||||
excluded_signals = [
|
||||
# SPI相关
|
||||
'SPI1_SCK', 'SPI1_MISO', 'SPI1_MOSI', 'SPI2_SCK', 'SPI2_MISO', 'SPI2_MOSI',
|
||||
'SPI3_SCK', 'SPI3_MISO', 'SPI3_MOSI',
|
||||
# I2C相关
|
||||
'I2C1_SCL', 'I2C1_SDA', 'I2C2_SCL', 'I2C2_SDA', 'I2C3_SCL', 'I2C3_SDA',
|
||||
# UART/USART相关
|
||||
'USART1_TX', 'USART1_RX', 'USART2_TX', 'USART2_RX', 'USART3_TX', 'USART3_RX',
|
||||
'USART6_TX', 'USART6_RX', 'UART4_TX', 'UART4_RX', 'UART5_TX', 'UART5_RX',
|
||||
# CAN相关
|
||||
'CAN1_TX', 'CAN1_RX', 'CAN2_TX', 'CAN2_RX',
|
||||
# USB相关
|
||||
'USB_OTG_FS_DM', 'USB_OTG_FS_DP', 'USB_OTG_HS_DM', 'USB_OTG_HS_DP',
|
||||
# 系统相关
|
||||
'SYS_JTMS-SWDIO', 'SYS_JTCK-SWCLK', 'SYS_JTDI', 'SYS_JTDO-SWO',
|
||||
'RCC_OSC_IN', 'RCC_OSC_OUT',
|
||||
]
|
||||
|
||||
# 处理每个GPIO配置
|
||||
for pin, config in gpio_configs.items():
|
||||
signal = config.get('Signal', '')
|
||||
|
||||
# 只处理有Label的GPIO
|
||||
if 'GPIO_Label' not in config:
|
||||
continue
|
||||
|
||||
# 排除用于其他外设功能的引脚
|
||||
if signal in excluded_signals:
|
||||
continue
|
||||
|
||||
# 排除TIM相关的引脚(以S_TIM开头的信号)
|
||||
if signal.startswith('S_TIM'):
|
||||
continue
|
||||
|
||||
# 排除ADC相关的引脚
|
||||
if signal.startswith('ADC'):
|
||||
continue
|
||||
|
||||
# 只包含纯GPIO功能
|
||||
if signal in ['GPIO_Input', 'GPIO_Output'] or signal.startswith('GPXTI'):
|
||||
gpio_info = {
|
||||
'pin': pin,
|
||||
'label': config['GPIO_Label'],
|
||||
'has_exti': 'GPIO_ModeDefaultEXTI' in config or signal.startswith('GPXTI'),
|
||||
'signal': signal,
|
||||
'mode': config.get('GPIO_ModeDefaultEXTI', ''),
|
||||
'is_output': signal == 'GPIO_Output',
|
||||
'is_input': signal == 'GPIO_Input'
|
||||
}
|
||||
gpio_list.append(gpio_info)
|
||||
|
||||
return gpio_list
|
||||
|
||||
@staticmethod
|
||||
def get_enabled_pwm_from_ioc(ioc_path):
|
||||
"""
|
||||
获取已启用的PWM通道列表
|
||||
返回格式: [{'timer': 'TIM1', 'channel': 'TIM_CHANNEL_1', 'label': 'PWM_MOTOR1'}, ...]
|
||||
"""
|
||||
pwm_channels = []
|
||||
gpio_configs = {}
|
||||
|
||||
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# 收集GPIO相关配置
|
||||
if '.' in key:
|
||||
pin = key.split('.')[0]
|
||||
param = key.split('.', 1)[1]
|
||||
|
||||
if pin not in gpio_configs:
|
||||
gpio_configs[pin] = {}
|
||||
|
||||
gpio_configs[pin][param] = value
|
||||
|
||||
# 处理每个GPIO配置,查找PWM信号
|
||||
for pin, config in gpio_configs.items():
|
||||
signal = config.get('Signal', '')
|
||||
|
||||
# 检查是否为PWM信号(格式如:S_TIM1_CH1, S_TIM2_CH3等)
|
||||
if signal.startswith('S_TIM') and '_CH' in signal:
|
||||
# 解析定时器和通道信息
|
||||
# 例如:S_TIM1_CH1 -> TIM1, CH1
|
||||
parts = signal.replace('S_', '').split('_')
|
||||
if len(parts) >= 2:
|
||||
timer = parts[0] # TIM1
|
||||
channel_part = parts[1] # CH1
|
||||
|
||||
# 转换通道格式:CH1 -> TIM_CHANNEL_1
|
||||
if channel_part.startswith('CH'):
|
||||
channel_num = channel_part[2:] # 提取数字
|
||||
channel = f"TIM_CHANNEL_{channel_num}"
|
||||
|
||||
# 获取标签
|
||||
label = config.get('GPIO_Label', f"{timer}_{channel_part}")
|
||||
|
||||
pwm_channels.append({
|
||||
'timer': timer,
|
||||
'channel': channel,
|
||||
'label': label,
|
||||
'pin': pin,
|
||||
'signal': signal
|
||||
})
|
||||
|
||||
return pwm_channels
|
||||
@ -1,462 +0,0 @@
|
||||
"""
|
||||
自动更新模块
|
||||
实现软件的自动更新功能,包括下载、解压、安装等完整流程
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
import subprocess
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from packaging.version import parse as vparse
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QObject
|
||||
|
||||
# 移除多线程下载器依赖
|
||||
|
||||
|
||||
class UpdaterSignals(QObject):
|
||||
"""更新器信号类"""
|
||||
progress_changed = pyqtSignal(int) # 进度变化信号 (0-100)
|
||||
download_progress = pyqtSignal(int, int, float, float) # 下载进度: 已下载字节, 总字节, 速度MB/s, 剩余时间秒
|
||||
status_changed = pyqtSignal(str) # 状态变化信号
|
||||
error_occurred = pyqtSignal(str) # 错误信号
|
||||
update_completed = pyqtSignal(str) # 更新完成信号,可选包含文件路径
|
||||
update_cancelled = pyqtSignal() # 更新取消信号
|
||||
|
||||
|
||||
class AutoUpdater(QThread):
|
||||
"""自动更新器类"""
|
||||
|
||||
def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"):
|
||||
super().__init__()
|
||||
self.current_version = current_version
|
||||
self.repo = repo
|
||||
self.signals = UpdaterSignals()
|
||||
self.cancelled = False
|
||||
|
||||
# 获取当前程序信息
|
||||
self.is_frozen = getattr(sys, 'frozen', False)
|
||||
self.app_dir = self._get_app_directory()
|
||||
self.temp_dir = None
|
||||
|
||||
# 多线程下载器
|
||||
self.downloader = None
|
||||
|
||||
def _get_app_directory(self) -> str:
|
||||
"""获取应用程序目录"""
|
||||
if self.is_frozen:
|
||||
# 如果是打包的exe,返回exe所在目录
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 如果是Python脚本,返回项目根目录
|
||||
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
def cancel_update(self):
|
||||
"""取消更新"""
|
||||
self.cancelled = True
|
||||
if self.downloader:
|
||||
self.downloader.cancel()
|
||||
self.signals.update_cancelled.emit()
|
||||
|
||||
def check_for_updates(self) -> Optional[dict]:
|
||||
"""检查是否有新版本可用"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在检查更新...")
|
||||
|
||||
url = f"https://api.github.com/repos/{self.repo}/releases/latest"
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
release_data = response.json()
|
||||
latest_version = release_data["tag_name"].lstrip("v")
|
||||
|
||||
if vparse(latest_version) > vparse(self.current_version):
|
||||
download_url = self._get_download_url(release_data)
|
||||
asset_size = self._get_asset_size(release_data, download_url) if download_url else 0
|
||||
return {
|
||||
'version': latest_version,
|
||||
'download_url': download_url,
|
||||
'release_notes': release_data.get('body', ''),
|
||||
'release_date': release_data.get('published_at', ''),
|
||||
'asset_name': self._get_asset_name(release_data),
|
||||
'asset_size': asset_size
|
||||
}
|
||||
return None
|
||||
else:
|
||||
raise Exception(f"GitHub API请求失败: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"检查更新失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_download_url(self, release_data: dict) -> Optional[str]:
|
||||
"""从release数据中获取适合当前平台的下载链接"""
|
||||
assets = release_data.get('assets', [])
|
||||
system = platform.system().lower()
|
||||
|
||||
# 根据操作系统选择合适的安装包
|
||||
for asset in assets:
|
||||
name = asset['name'].lower()
|
||||
|
||||
if system == 'windows':
|
||||
if 'installer' in name and name.endswith('.exe'):
|
||||
return asset['browser_download_url']
|
||||
if name.endswith('.exe') or name.endswith('.zip'):
|
||||
return asset['browser_download_url']
|
||||
elif system == 'darwin': # macOS
|
||||
# 优先选择 dmg 或 zip 文件
|
||||
if name.endswith('.dmg'):
|
||||
return asset['browser_download_url']
|
||||
if name.endswith('.zip') and 'macos' in name:
|
||||
return asset['browser_download_url']
|
||||
elif system == 'linux':
|
||||
if name.endswith('.tar.gz') or (name.endswith('.zip') and 'linux' in name):
|
||||
return asset['browser_download_url']
|
||||
|
||||
# 如果没找到特定平台的,在 macOS 上避免选择 .exe 文件
|
||||
for asset in assets:
|
||||
name = asset['name'].lower()
|
||||
if system == 'darwin':
|
||||
# macOS 优先选择非 exe 文件
|
||||
if name.endswith('.zip') or name.endswith('.dmg') or name.endswith('.tar.gz'):
|
||||
return asset['browser_download_url']
|
||||
else:
|
||||
if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']):
|
||||
return asset['browser_download_url']
|
||||
|
||||
# 最后才选择 exe 文件(如果没有其他选择)
|
||||
if system == 'darwin':
|
||||
for asset in assets:
|
||||
name = asset['name'].lower()
|
||||
if name.endswith('.exe'):
|
||||
return asset['browser_download_url']
|
||||
|
||||
return None
|
||||
|
||||
def _get_asset_name(self, release_data: dict) -> Optional[str]:
|
||||
"""获取资源文件名"""
|
||||
download_url = self._get_download_url(release_data)
|
||||
if download_url:
|
||||
return os.path.basename(urlparse(download_url).path)
|
||||
return None
|
||||
|
||||
def _get_asset_size(self, release_data: dict, download_url: str) -> int:
|
||||
"""获取资源文件大小"""
|
||||
if not download_url:
|
||||
return 0
|
||||
|
||||
assets = release_data.get('assets', [])
|
||||
for asset in assets:
|
||||
if asset.get('browser_download_url') == download_url:
|
||||
return asset.get('size', 0)
|
||||
return 0
|
||||
|
||||
def download_update(self, download_url: str, filename: str) -> Optional[str]:
|
||||
"""下载更新文件 - 使用简单的单线程下载"""
|
||||
try:
|
||||
self.signals.status_changed.emit("开始下载...")
|
||||
|
||||
# 创建临时目录
|
||||
self.temp_dir = tempfile.mkdtemp(prefix="MRobot_update_")
|
||||
file_path = os.path.join(self.temp_dir, filename)
|
||||
|
||||
print(f"Downloading to: {file_path}")
|
||||
|
||||
# 使用简单的requests下载
|
||||
import requests
|
||||
headers = {
|
||||
'User-Agent': 'MRobot-Updater/1.0',
|
||||
'Accept': '*/*',
|
||||
}
|
||||
|
||||
response = requests.get(download_url, headers=headers, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# 获取文件总大小
|
||||
total_size = int(response.headers.get('Content-Length', 0))
|
||||
downloaded_size = 0
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
# 下载文件
|
||||
with open(file_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if self.cancelled:
|
||||
print("Download cancelled by user")
|
||||
return None
|
||||
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新进度
|
||||
if total_size > 0:
|
||||
progress = int((downloaded_size / total_size) * 100)
|
||||
self.signals.progress_changed.emit(progress)
|
||||
|
||||
# 发送详细进度信息
|
||||
speed = 0 # 简化版本不计算速度
|
||||
remaining = 0
|
||||
self.signals.download_progress.emit(downloaded_size, total_size, speed, remaining)
|
||||
|
||||
# 验证下载的文件
|
||||
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
|
||||
print(f"Download completed successfully: {file_path}")
|
||||
self.signals.status_changed.emit("下载完成!")
|
||||
return file_path
|
||||
else:
|
||||
raise Exception("下载的文件不存在或为空")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"下载失败: {str(e)}"
|
||||
print(f"Download error: {error_msg}")
|
||||
self.signals.error_occurred.emit(error_msg)
|
||||
return None
|
||||
|
||||
def _on_download_progress(self, downloaded: int, total: int, speed: float, remaining: float):
|
||||
"""处理下载进度"""
|
||||
# 转发详细下载进度
|
||||
self.signals.download_progress.emit(downloaded, total, speed, remaining)
|
||||
|
||||
# 计算总体进度 (下载占80%,其他操作占20%)
|
||||
if total > 0:
|
||||
download_progress = int((downloaded / total) * 80)
|
||||
self.signals.progress_changed.emit(download_progress)
|
||||
|
||||
def extract_update(self, file_path: str) -> Optional[str]:
|
||||
"""解压更新文件"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在解压文件...")
|
||||
self.signals.progress_changed.emit(85)
|
||||
|
||||
if not self.temp_dir:
|
||||
raise Exception("临时目录未初始化")
|
||||
|
||||
extract_dir = os.path.join(self.temp_dir, "extracted")
|
||||
os.makedirs(extract_dir, exist_ok=True)
|
||||
|
||||
# 根据文件扩展名选择解压方法
|
||||
if file_path.endswith('.zip'):
|
||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
elif file_path.endswith('.tar.gz'):
|
||||
import tarfile
|
||||
with tarfile.open(file_path, 'r:gz') as tar_ref:
|
||||
tar_ref.extractall(extract_dir)
|
||||
elif file_path.endswith('.exe'):
|
||||
# 对于 .exe 文件,在非 Windows 系统上提示手动安装
|
||||
current_system = platform.system()
|
||||
if current_system != 'Windows':
|
||||
self.signals.error_occurred.emit(f"下载的是 Windows 安装程序,当前系统是 {current_system}。\n请手动下载适合您系统的版本。")
|
||||
return None
|
||||
else:
|
||||
# Windows 系统直接返回文件路径,由安装函数处理
|
||||
return file_path
|
||||
else:
|
||||
raise Exception(f"不支持的文件格式: {file_path}")
|
||||
|
||||
self.signals.progress_changed.emit(90)
|
||||
self.signals.status_changed.emit("解压完成")
|
||||
return extract_dir
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"解压失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def install_update(self, extract_dir: str) -> bool:
|
||||
"""安装更新"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在安装更新...")
|
||||
self.signals.progress_changed.emit(95)
|
||||
|
||||
if not self.temp_dir:
|
||||
raise Exception("临时目录未初始化")
|
||||
|
||||
# 创建备份目录
|
||||
backup_dir = os.path.join(self.temp_dir, "backup")
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
# 备份当前程序文件
|
||||
self._backup_current_files(backup_dir)
|
||||
|
||||
# 复制新文件
|
||||
self._copy_update_files(extract_dir)
|
||||
|
||||
self.signals.progress_changed.emit(99)
|
||||
self.signals.status_changed.emit("安装完成")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"安装失败: {str(e)}")
|
||||
# 尝试恢复备份
|
||||
self._restore_backup(backup_dir)
|
||||
return False
|
||||
|
||||
def _backup_current_files(self, backup_dir: str):
|
||||
"""备份当前程序文件"""
|
||||
important_files = ['MRobot.py', 'MRobot.exe', 'app/', 'assets/']
|
||||
|
||||
for item in important_files:
|
||||
src_path = os.path.join(self.app_dir, item)
|
||||
if os.path.exists(src_path):
|
||||
dst_path = os.path.join(backup_dir, item)
|
||||
if os.path.isdir(src_path):
|
||||
shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
shutil.copy2(src_path, dst_path)
|
||||
|
||||
def _copy_update_files(self, extract_dir: str):
|
||||
"""复制更新文件到应用程序目录"""
|
||||
# 查找解压目录中的主要文件/文件夹
|
||||
extract_contents = os.listdir(extract_dir)
|
||||
|
||||
# 如果解压后只有一个文件夹,进入该文件夹
|
||||
if len(extract_contents) == 1 and os.path.isdir(os.path.join(extract_dir, extract_contents[0])):
|
||||
extract_dir = os.path.join(extract_dir, extract_contents[0])
|
||||
|
||||
# 复制文件到应用程序目录
|
||||
for item in os.listdir(extract_dir):
|
||||
src_path = os.path.join(extract_dir, item)
|
||||
dst_path = os.path.join(self.app_dir, item)
|
||||
|
||||
if os.path.isdir(src_path):
|
||||
if os.path.exists(dst_path):
|
||||
shutil.rmtree(dst_path)
|
||||
shutil.copytree(src_path, dst_path)
|
||||
else:
|
||||
shutil.copy2(src_path, dst_path)
|
||||
|
||||
def _restore_backup(self, backup_dir: str):
|
||||
"""恢复备份文件"""
|
||||
try:
|
||||
for item in os.listdir(backup_dir):
|
||||
src_path = os.path.join(backup_dir, item)
|
||||
dst_path = os.path.join(self.app_dir, item)
|
||||
|
||||
if os.path.isdir(src_path):
|
||||
if os.path.exists(dst_path):
|
||||
shutil.rmtree(dst_path)
|
||||
shutil.copytree(src_path, dst_path)
|
||||
else:
|
||||
shutil.copy2(src_path, dst_path)
|
||||
except Exception as e:
|
||||
print(f"恢复备份失败: {e}")
|
||||
|
||||
def restart_application(self):
|
||||
"""重启应用程序"""
|
||||
try:
|
||||
self.signals.status_changed.emit("正在重启应用程序...")
|
||||
|
||||
if self.is_frozen:
|
||||
# 如果是打包的exe
|
||||
executable = sys.executable
|
||||
else:
|
||||
# 如果是Python脚本
|
||||
executable = sys.executable
|
||||
script_path = os.path.join(self.app_dir, "MRobot.py")
|
||||
|
||||
# 启动新进程
|
||||
if platform.system() == 'Windows':
|
||||
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
|
||||
else:
|
||||
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
|
||||
|
||||
# 退出当前进程
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
self.signals.error_occurred.emit(f"重启失败: {str(e)}")
|
||||
|
||||
def cleanup(self):
|
||||
"""清理临时文件"""
|
||||
print(f"cleanup() called, temp_dir: {self.temp_dir}") # 调试输出
|
||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||
try:
|
||||
print(f"Removing temp directory: {self.temp_dir}") # 调试输出
|
||||
shutil.rmtree(self.temp_dir)
|
||||
print("Temp directory removed successfully") # 调试输出
|
||||
except Exception as e:
|
||||
print(f"清理临时文件失败: {e}")
|
||||
else:
|
||||
print("No temp directory to clean up") # 调试输出
|
||||
|
||||
def run(self):
|
||||
"""执行更新流程"""
|
||||
try:
|
||||
self.signals.status_changed.emit("开始更新流程...")
|
||||
|
||||
# 检查更新
|
||||
self.signals.status_changed.emit("正在获取更新信息...")
|
||||
update_info = self.check_for_updates()
|
||||
if not update_info or self.cancelled:
|
||||
self.signals.status_changed.emit("未找到更新信息或已取消")
|
||||
return
|
||||
|
||||
self.signals.status_changed.emit(f"准备下载版本 {update_info['version']}")
|
||||
|
||||
# 下载更新
|
||||
downloaded_file = self.download_update(
|
||||
update_info['download_url'],
|
||||
update_info['asset_name']
|
||||
)
|
||||
if not downloaded_file or self.cancelled:
|
||||
self.signals.status_changed.emit("下载失败或已取消")
|
||||
return
|
||||
|
||||
self.signals.status_changed.emit("下载完成!")
|
||||
self.signals.progress_changed.emit(100)
|
||||
|
||||
# 检查是否为exe文件且当前系统非Windows
|
||||
current_system = platform.system()
|
||||
print(f"Downloaded file: {downloaded_file}") # 调试输出
|
||||
print(f"Current system: {current_system}") # 调试输出
|
||||
if downloaded_file.endswith('.exe') and current_system != 'Windows':
|
||||
# 直接完成,返回文件路径
|
||||
print(f"Emitting update_completed signal with file path: {downloaded_file}") # 调试输出
|
||||
self.signals.update_completed.emit(downloaded_file)
|
||||
# 不要立即清理,让用户有时间访问文件
|
||||
print("Skipping cleanup to preserve downloaded file") # 调试输出
|
||||
return
|
||||
|
||||
# 对于其他情况,继续原有流程
|
||||
self.signals.status_changed.emit("下载完成,开始解压...")
|
||||
|
||||
# 解压更新
|
||||
extract_dir = self.extract_update(downloaded_file)
|
||||
if not extract_dir or self.cancelled:
|
||||
self.signals.status_changed.emit("解压失败或已取消")
|
||||
return
|
||||
|
||||
# 安装更新
|
||||
self.signals.status_changed.emit("开始安装更新...")
|
||||
if self.install_update(extract_dir) and not self.cancelled:
|
||||
self.signals.progress_changed.emit(100)
|
||||
self.signals.status_changed.emit("更新安装完成")
|
||||
self.signals.update_completed.emit("") # 传递空字符串表示正常安装完成
|
||||
else:
|
||||
self.signals.status_changed.emit("安装失败或已取消")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"更新过程中发生错误: {str(e)}"
|
||||
print(f"AutoUpdater error: {error_msg}") # 调试输出
|
||||
self.signals.error_occurred.emit(error_msg)
|
||||
finally:
|
||||
# 对于下载完成的情况,延迟清理临时文件
|
||||
# 这样用户有时间访问下载的文件
|
||||
pass # 暂时不在这里清理
|
||||
|
||||
|
||||
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
|
||||
"""快速检查是否有新版本可用(不下载)"""
|
||||
updater = AutoUpdater(current_version, repo)
|
||||
return updater.check_for_updates()
|
||||
@ -1,74 +0,0 @@
|
||||
import requests
|
||||
import platform
|
||||
from packaging.version import parse as vparse
|
||||
from typing import Optional
|
||||
|
||||
def check_update(local_version, repo="goldenfishs/MRobot"):
|
||||
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||
resp = requests.get(url, timeout=5)
|
||||
if resp.status_code == 200:
|
||||
latest = resp.json()["tag_name"].lstrip("v")
|
||||
if vparse(latest) > vparse(local_version):
|
||||
return latest
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
raise RuntimeError("GitHub API 请求失败")
|
||||
|
||||
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
|
||||
"""检查更新并返回详细信息"""
|
||||
try:
|
||||
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
release_data = response.json()
|
||||
latest_version = release_data["tag_name"].lstrip("v")
|
||||
|
||||
if vparse(latest_version) > vparse(current_version):
|
||||
# 获取适合当前平台的下载链接和文件大小
|
||||
download_url, asset_size, asset_name = _get_platform_asset(release_data)
|
||||
|
||||
return {
|
||||
'version': latest_version,
|
||||
'download_url': download_url,
|
||||
'asset_size': asset_size,
|
||||
'asset_name': asset_name,
|
||||
'release_notes': release_data.get('body', ''),
|
||||
'release_date': release_data.get('published_at', ''),
|
||||
}
|
||||
return None
|
||||
else:
|
||||
raise Exception(f"GitHub API请求失败: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"检查更新失败: {str(e)}")
|
||||
|
||||
def _get_platform_asset(release_data: dict) -> tuple:
|
||||
"""获取适合当前平台的资源文件信息"""
|
||||
assets = release_data.get('assets', [])
|
||||
system = platform.system().lower()
|
||||
|
||||
# 根据操作系统选择合适的安装包
|
||||
for asset in assets:
|
||||
name = asset['name'].lower()
|
||||
|
||||
if system == 'windows':
|
||||
if 'installer' in name and name.endswith('.exe'):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
if name.endswith('.exe') or name.endswith('.zip'):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
elif system == 'darwin': # macOS
|
||||
if name.endswith('.dmg') or name.endswith('.zip'):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
elif system == 'linux':
|
||||
if name.endswith('.tar.gz') or name.endswith('.zip'):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
|
||||
# 如果没找到特定平台的,返回第一个可用文件
|
||||
for asset in assets:
|
||||
name = asset['name'].lower()
|
||||
if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']):
|
||||
return asset['browser_download_url'], asset.get('size', 0), asset['name']
|
||||
|
||||
return None, 0, None
|
||||
@ -1,557 +0,0 @@
|
||||
import os
|
||||
import yaml
|
||||
import shutil
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import sys
|
||||
import re
|
||||
import csv
|
||||
class CodeGenerator:
|
||||
"""通用代码生成器"""
|
||||
|
||||
# 添加类级别的缓存
|
||||
_assets_dir_cache = None
|
||||
_assets_dir_initialized = False
|
||||
_template_dir_logged = False
|
||||
|
||||
@staticmethod
|
||||
def load_template(template_path: str) -> str:
|
||||
"""加载代码模板"""
|
||||
try:
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"加载模板失败: {template_path}, 错误: {e}")
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def replace_auto_generated(content: str, marker: str, replacement: str) -> str:
|
||||
"""替换自动生成的代码标记"""
|
||||
marker_line = f"/* {marker} */"
|
||||
if marker_line in content:
|
||||
return content.replace(marker_line, replacement)
|
||||
return content
|
||||
|
||||
@staticmethod
|
||||
def save_file(content: str, file_path: str) -> bool:
|
||||
"""保存文件"""
|
||||
try:
|
||||
dir_path = os.path.dirname(file_path)
|
||||
if dir_path: # 只有当目录路径不为空时才创建
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"保存文件失败: {file_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def load_config(config_path: str) -> Dict:
|
||||
"""加载配置文件"""
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
print(f"加载配置失败: {config_path}, 错误: {e}")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def save_config(config: Dict, config_path: str) -> bool:
|
||||
"""保存配置文件"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(config, f, allow_unicode=True, default_flow_style=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"保存配置失败: {config_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_template_dir():
|
||||
"""获取模板目录路径,兼容打包环境"""
|
||||
# 使用统一的get_assets_dir方法来获取路径
|
||||
template_dir = CodeGenerator.get_assets_dir("User_code/bsp")
|
||||
|
||||
# 只在第一次或出现问题时打印日志
|
||||
if not hasattr(CodeGenerator, '_template_dir_logged'):
|
||||
print(f"模板目录路径: {template_dir}")
|
||||
CodeGenerator._template_dir_logged = True
|
||||
|
||||
if template_dir and not os.path.exists(template_dir):
|
||||
print(f"警告:模板目录不存在: {template_dir}")
|
||||
|
||||
return template_dir
|
||||
|
||||
@staticmethod
|
||||
def get_assets_dir(sub_path=""):
|
||||
"""获取assets目录路径,兼容打包环境
|
||||
Args:
|
||||
sub_path: 子路径,如 "User_code/component" 或 "User_code/device"
|
||||
Returns:
|
||||
str: 完整的assets路径
|
||||
"""
|
||||
# 使用缓存机制,避免重复计算和日志输出
|
||||
if not CodeGenerator._assets_dir_initialized:
|
||||
assets_dir = ""
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的环境
|
||||
print("检测到打包环境")
|
||||
|
||||
# 优先使用sys._MEIPASS(PyInstaller的临时解包目录)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
base_path = getattr(sys, '_MEIPASS')
|
||||
assets_dir = os.path.join(base_path, "assets")
|
||||
print(f"使用PyInstaller临时目录: {assets_dir}")
|
||||
else:
|
||||
# 后备方案:使用可执行文件所在目录
|
||||
exe_dir = os.path.dirname(sys.executable)
|
||||
assets_dir = os.path.join(exe_dir, "assets")
|
||||
print(f"使用可执行文件目录: {assets_dir}")
|
||||
|
||||
# 如果都不存在,尝试其他可能的位置
|
||||
if not os.path.exists(assets_dir):
|
||||
# 尝试从当前工作目录查找
|
||||
cwd_assets = os.path.join(os.getcwd(), "assets")
|
||||
if os.path.exists(cwd_assets):
|
||||
assets_dir = cwd_assets
|
||||
print(f"从工作目录找到assets: {assets_dir}")
|
||||
else:
|
||||
print(f"警告:无法找到assets目录,使用默认路径: {assets_dir}")
|
||||
else:
|
||||
# 开发环境
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# 向上查找直到找到MRobot目录或到达根目录
|
||||
while current_dir != os.path.dirname(current_dir): # 防止无限循环
|
||||
if os.path.basename(current_dir) == 'MRobot':
|
||||
break
|
||||
parent = os.path.dirname(current_dir)
|
||||
if parent == current_dir: # 已到达根目录
|
||||
break
|
||||
current_dir = parent
|
||||
|
||||
assets_dir = os.path.join(current_dir, "assets")
|
||||
print(f"开发环境:使用路径: {assets_dir}")
|
||||
|
||||
# 如果找不到,尝试从当前工作目录
|
||||
if not os.path.exists(assets_dir):
|
||||
cwd_assets = os.path.join(os.getcwd(), "assets")
|
||||
if os.path.exists(cwd_assets):
|
||||
assets_dir = cwd_assets
|
||||
print(f"开发环境后备:使用工作目录: {assets_dir}")
|
||||
|
||||
# 缓存基础assets目录
|
||||
CodeGenerator._assets_dir_cache = assets_dir
|
||||
CodeGenerator._assets_dir_initialized = True
|
||||
else:
|
||||
# 使用缓存的路径
|
||||
assets_dir = CodeGenerator._assets_dir_cache or ""
|
||||
|
||||
# 构建完整路径
|
||||
if sub_path:
|
||||
full_path = os.path.join(assets_dir, sub_path)
|
||||
else:
|
||||
full_path = assets_dir
|
||||
|
||||
# 规范化路径(处理路径分隔符)
|
||||
full_path = os.path.normpath(full_path)
|
||||
|
||||
# 只在第一次访问某个路径时检查并警告
|
||||
safe_sub_path = sub_path.replace('/', '_').replace('\\', '_')
|
||||
warning_key = f"_warned_{safe_sub_path}"
|
||||
if full_path and not os.path.exists(full_path) and not hasattr(CodeGenerator, warning_key):
|
||||
print(f"警告:资源目录不存在: {full_path}")
|
||||
setattr(CodeGenerator, warning_key, True)
|
||||
|
||||
return full_path
|
||||
|
||||
@staticmethod
|
||||
def preserve_all_user_regions(new_code: str, old_code: str) -> str:
|
||||
"""保留用户定义的代码区域
|
||||
|
||||
在新代码中保留旧代码中所有用户定义的区域。
|
||||
用户区域使用如下格式标记:
|
||||
/* USER REGION_NAME BEGIN */
|
||||
用户代码...
|
||||
/* USER REGION_NAME END */
|
||||
|
||||
支持的格式示例:
|
||||
- /* USER REFEREE BEGIN */ ... /* USER REFEREE END */
|
||||
- /* USER CODE BEGIN */ ... /* USER CODE END */
|
||||
- /* USER CUSTOM_NAME BEGIN */ ... /* USER CUSTOM_NAME END */
|
||||
|
||||
Args:
|
||||
new_code: 新的代码内容
|
||||
old_code: 旧的代码内容
|
||||
|
||||
Returns:
|
||||
str: 保留了用户区域的新代码
|
||||
"""
|
||||
if not old_code:
|
||||
return new_code
|
||||
|
||||
# 更灵活的正则表达式,支持更多格式的用户区域标记
|
||||
# 匹配 /* USER 任意字符 BEGIN */ ... /* USER 相同字符 END */
|
||||
pattern = re.compile(
|
||||
r"/\*\s*USER\s+([A-Za-z0-9_\s]+?)\s+BEGIN\s*\*/(.*?)/\*\s*USER\s+\1\s+END\s*\*/",
|
||||
re.DOTALL | re.IGNORECASE
|
||||
)
|
||||
|
||||
# 提取旧代码中的所有用户区域
|
||||
old_regions = {}
|
||||
for match in pattern.finditer(old_code):
|
||||
region_name = match.group(1).strip()
|
||||
region_content = match.group(2)
|
||||
old_regions[region_name.upper()] = region_content
|
||||
|
||||
# 替换函数
|
||||
def repl(match):
|
||||
region_name = match.group(1).strip().upper()
|
||||
current_content = match.group(2)
|
||||
old_content = old_regions.get(region_name)
|
||||
|
||||
if old_content is not None:
|
||||
# 直接替换中间的内容,保持原有的注释标记不变
|
||||
return match.group(0).replace(current_content, old_content)
|
||||
|
||||
return match.group(0)
|
||||
|
||||
# 应用替换
|
||||
result = pattern.sub(repl, new_code)
|
||||
|
||||
# 调试信息:记录找到的用户区域
|
||||
if old_regions:
|
||||
print(f"保留了 {len(old_regions)} 个用户区域: {list(old_regions.keys())}")
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def save_with_preserve(file_path: str, new_code: str) -> bool:
|
||||
"""保存文件并保留用户代码区域
|
||||
|
||||
如果文件已存在,会先读取旧文件内容,保留其中的用户代码区域,
|
||||
然后将新代码与保留的用户区域合并后保存。
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
new_code: 新的代码内容
|
||||
|
||||
Returns:
|
||||
bool: 保存是否成功
|
||||
"""
|
||||
try:
|
||||
# 如果文件已存在,先读取旧内容
|
||||
old_code = ""
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
old_code = f.read()
|
||||
|
||||
# 保留用户区域
|
||||
final_code = CodeGenerator.preserve_all_user_regions(new_code, old_code)
|
||||
|
||||
# 确保目录存在
|
||||
dir_path = os.path.dirname(file_path)
|
||||
if dir_path: # 只有当目录路径不为空时才创建
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(final_code)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"保存文件失败: {file_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def load_descriptions(csv_path: str) -> Dict[str, str]:
|
||||
"""从CSV文件加载组件或设备的描述信息
|
||||
|
||||
CSV格式:第一列为组件/设备名称,第二列为描述
|
||||
|
||||
Args:
|
||||
csv_path: CSV文件路径
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 名称到描述的映射字典
|
||||
"""
|
||||
descriptions = {}
|
||||
if os.path.exists(csv_path):
|
||||
try:
|
||||
with open(csv_path, encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
if len(row) >= 2:
|
||||
key, desc = row[0].strip(), row[1].strip()
|
||||
descriptions[key.lower()] = desc
|
||||
except Exception as e:
|
||||
print(f"加载描述文件失败: {csv_path}, 错误: {e}")
|
||||
return descriptions
|
||||
|
||||
@staticmethod
|
||||
def load_dependencies(csv_path: str) -> Dict[str, List[str]]:
|
||||
"""从CSV文件加载组件依赖关系
|
||||
|
||||
CSV格式:第一列为组件名,后续列为依赖的组件
|
||||
|
||||
Args:
|
||||
csv_path: CSV文件路径
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: 组件名到依赖列表的映射字典
|
||||
"""
|
||||
dependencies = {}
|
||||
if os.path.exists(csv_path):
|
||||
try:
|
||||
with open(csv_path, encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
if len(row) >= 2:
|
||||
component = row[0].strip()
|
||||
deps = [dep.strip() for dep in row[1:] if dep.strip()]
|
||||
dependencies[component] = deps
|
||||
except Exception as e:
|
||||
print(f"加载依赖文件失败: {csv_path}, 错误: {e}")
|
||||
return dependencies
|
||||
|
||||
@staticmethod
|
||||
def load_device_config(config_path: str) -> Dict:
|
||||
"""加载设备配置文件
|
||||
|
||||
Args:
|
||||
config_path: YAML配置文件路径
|
||||
|
||||
Returns:
|
||||
Dict: 配置数据字典
|
||||
"""
|
||||
return CodeGenerator.load_config(config_path)
|
||||
|
||||
@staticmethod
|
||||
def copy_dependency_file(src_path: str, dst_path: str) -> bool:
|
||||
"""复制依赖文件
|
||||
|
||||
Args:
|
||||
src_path: 源文件路径
|
||||
dst_path: 目标文件路径
|
||||
|
||||
Returns:
|
||||
bool: 复制是否成功
|
||||
"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
|
||||
shutil.copy2(src_path, dst_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"复制文件失败: {src_path} -> {dst_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def generate_code_from_template(template_path: str, output_path: str,
|
||||
replacements: Optional[Dict[str, str]] = None,
|
||||
preserve_user_code: bool = True) -> bool:
|
||||
"""从模板生成代码文件
|
||||
|
||||
Args:
|
||||
template_path: 模板文件路径
|
||||
output_path: 输出文件路径
|
||||
replacements: 要替换的标记字典,如 {'MARKER': 'replacement_content'}
|
||||
preserve_user_code: 是否保留用户代码区域
|
||||
|
||||
Returns:
|
||||
bool: 生成是否成功
|
||||
"""
|
||||
try:
|
||||
# 加载模板
|
||||
template_content = CodeGenerator.load_template(template_path)
|
||||
if not template_content:
|
||||
print(f"模板文件不存在或为空: {template_path}")
|
||||
return False
|
||||
|
||||
# 执行替换
|
||||
if replacements:
|
||||
for marker, replacement in replacements.items():
|
||||
template_content = CodeGenerator.replace_auto_generated(
|
||||
template_content, marker, replacement
|
||||
)
|
||||
|
||||
# 保存文件
|
||||
if preserve_user_code:
|
||||
return CodeGenerator.save_with_preserve(output_path, template_content)
|
||||
else:
|
||||
return CodeGenerator.save_file(template_content, output_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"从模板生成代码失败: {template_path} -> {output_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def read_file_content(file_path: str) -> Optional[str]:
|
||||
"""读取文件内容
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
Returns:
|
||||
str: 文件内容,如果失败返回None
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"读取文件失败: {file_path}, 错误: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def write_file_content(file_path: str, content: str) -> bool:
|
||||
"""写入文件内容
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
content: 文件内容
|
||||
|
||||
Returns:
|
||||
bool: 写入是否成功
|
||||
"""
|
||||
try:
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"写入文件失败: {file_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def update_file_with_pattern(file_path: str, pattern: str, replacement: str,
|
||||
use_regex: bool = True) -> bool:
|
||||
"""更新文件中匹配模式的内容
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
pattern: 要匹配的模式
|
||||
replacement: 替换内容
|
||||
use_regex: 是否使用正则表达式
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
"""
|
||||
try:
|
||||
content = CodeGenerator.read_file_content(file_path)
|
||||
if content is None:
|
||||
return False
|
||||
|
||||
if use_regex:
|
||||
import re
|
||||
updated_content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
||||
else:
|
||||
updated_content = content.replace(pattern, replacement)
|
||||
|
||||
return CodeGenerator.write_file_content(file_path, updated_content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"更新文件失败: {file_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def replace_multiple_markers(content: str, replacements: Dict[str, str]) -> str:
|
||||
"""批量替换内容中的多个标记
|
||||
|
||||
Args:
|
||||
content: 要处理的内容
|
||||
replacements: 替换字典,如 {'MARKER1': 'content1', 'MARKER2': 'content2'}
|
||||
|
||||
Returns:
|
||||
str: 替换后的内容
|
||||
"""
|
||||
result = content
|
||||
for marker, replacement in replacements.items():
|
||||
result = CodeGenerator.replace_auto_generated(result, marker, replacement)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def extract_user_regions(code: str) -> Dict[str, str]:
|
||||
"""从代码中提取所有用户区域
|
||||
|
||||
支持提取各种格式的用户区域:
|
||||
- /* USER REFEREE BEGIN */ ... /* USER REFEREE END */
|
||||
- /* USER CODE BEGIN */ ... /* USER CODE END */
|
||||
- /* USER CUSTOM_NAME BEGIN */ ... /* USER CUSTOM_NAME END */
|
||||
|
||||
Args:
|
||||
code: 要提取的代码内容
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 区域名称到区域内容的映射
|
||||
"""
|
||||
if not code:
|
||||
return {}
|
||||
|
||||
# 使用与preserve_all_user_regions相同的正则表达式
|
||||
pattern = re.compile(
|
||||
r"/\*\s*USER\s+([A-Za-z0-9_\s]+?)\s+BEGIN\s*\*/(.*?)/\*\s*USER\s+\1\s+END\s*\*/",
|
||||
re.DOTALL | re.IGNORECASE
|
||||
)
|
||||
|
||||
regions = {}
|
||||
for match in pattern.finditer(code):
|
||||
region_name = match.group(1).strip().upper()
|
||||
region_content = match.group(2)
|
||||
regions[region_name] = region_content
|
||||
|
||||
return regions
|
||||
|
||||
@staticmethod
|
||||
def debug_user_regions(new_code: str, old_code: str, verbose: bool = False) -> Dict[str, Dict[str, str]]:
|
||||
"""调试用户区域,显示新旧内容的对比
|
||||
|
||||
Args:
|
||||
new_code: 新的代码内容
|
||||
old_code: 旧的代码内容
|
||||
verbose: 是否输出详细信息
|
||||
|
||||
Returns:
|
||||
Dict: 包含所有用户区域信息的字典
|
||||
"""
|
||||
if verbose:
|
||||
print("=== 用户区域调试信息 ===")
|
||||
|
||||
new_regions = CodeGenerator.extract_user_regions(new_code)
|
||||
old_regions = CodeGenerator.extract_user_regions(old_code)
|
||||
|
||||
all_region_names = set(new_regions.keys()) | set(old_regions.keys())
|
||||
|
||||
result = {}
|
||||
|
||||
for region_name in sorted(all_region_names):
|
||||
new_content = new_regions.get(region_name, "")
|
||||
old_content = old_regions.get(region_name, "")
|
||||
|
||||
result[region_name] = {
|
||||
"new_content": new_content,
|
||||
"old_content": old_content,
|
||||
"will_preserve": bool(old_content),
|
||||
"exists_in_new": region_name in new_regions,
|
||||
"exists_in_old": region_name in old_regions
|
||||
}
|
||||
|
||||
if verbose:
|
||||
status = "保留旧内容" if old_content else "使用新内容"
|
||||
print(f"\n区域: {region_name} ({status})")
|
||||
print(f" 新模板中存在: {'是' if region_name in new_regions else '否'}")
|
||||
print(f" 旧文件中存在: {'是' if region_name in old_regions else '否'}")
|
||||
|
||||
if new_content.strip():
|
||||
print(f" 新内容预览: {repr(new_content.strip()[:50])}...")
|
||||
if old_content.strip():
|
||||
print(f" 旧内容预览: {repr(old_content.strip()[:50])}...")
|
||||
|
||||
if verbose:
|
||||
print(f"\n总计: {len(all_region_names)} 个用户区域")
|
||||
preserve_count = sum(1 for info in result.values() if info["will_preserve"])
|
||||
print(f"将保留: {preserve_count} 个区域的旧内容")
|
||||
|
||||
return result
|
||||
@ -1,306 +0,0 @@
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QWidget, QScrollArea
|
||||
from qfluentwidgets import (
|
||||
BodyLabel, TitleLabel, HorizontalSeparator, PushButton, PrimaryPushButton,
|
||||
LineEdit, SpinBox, DoubleSpinBox, CheckBox, TextEdit
|
||||
)
|
||||
from qfluentwidgets import theme, Theme
|
||||
import yaml
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QStackedLayout, QFileDialog, QHeaderView
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QTreeWidgetItem as TreeItem
|
||||
from qfluentwidgets import TitleLabel, BodyLabel, SubtitleLabel, StrongBodyLabel, HorizontalSeparator, PushButton, TreeWidget, InfoBar,FluentIcon, Dialog,SubtitleLabel,BodyLabel
|
||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox
|
||||
from qfluentwidgets import CardWidget, LineEdit, SpinBox, CheckBox, TextEdit, PrimaryPushButton, PushButton, InfoBar, DoubleSpinBox
|
||||
from qfluentwidgets import HeaderCardWidget
|
||||
from PyQt5.QtWidgets import QScrollArea, QWidget
|
||||
from qfluentwidgets import theme, Theme
|
||||
from PyQt5.QtWidgets import QDoubleSpinBox
|
||||
import os
|
||||
class TaskConfigDialog(QDialog):
|
||||
def __init__(self, parent=None, config_path=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("任务配置")
|
||||
self.resize(900, 520)
|
||||
|
||||
# 设置背景色跟随主题
|
||||
if theme() == Theme.DARK:
|
||||
self.setStyleSheet("background-color: #232323;")
|
||||
else:
|
||||
self.setStyleSheet("background-color: #f7f9fc;")
|
||||
|
||||
# 主布局
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(16, 16, 16, 16)
|
||||
main_layout.setSpacing(12)
|
||||
|
||||
# 顶部横向分栏
|
||||
self.top_layout = QHBoxLayout()
|
||||
self.top_layout.setSpacing(16)
|
||||
|
||||
# ----------- 左侧任务按钮区 -----------
|
||||
self.left_widget = QWidget()
|
||||
self.left_layout = QVBoxLayout(self.left_widget)
|
||||
self.left_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.left_layout.setSpacing(8)
|
||||
self.task_list_label = BodyLabel("任务列表")
|
||||
# self.left_layout.addWidget(self.task_list_label)
|
||||
# 添加任务列表居中
|
||||
self.task_list_label.setAlignment(Qt.AlignCenter)
|
||||
self.left_layout.addWidget(self.task_list_label, alignment=Qt.AlignCenter)
|
||||
|
||||
# 添加一个水平分割线
|
||||
self.left_layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 任务按钮区
|
||||
self.task_btn_area = QScrollArea()
|
||||
self.task_btn_area.setWidgetResizable(True)
|
||||
self.task_btn_area.setFrameShape(QScrollArea.NoFrame)
|
||||
self.task_btn_container = QWidget()
|
||||
self.task_btn_layout = QVBoxLayout(self.task_btn_container)
|
||||
self.task_btn_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.task_btn_layout.setSpacing(4)
|
||||
self.task_btn_layout.addStretch()
|
||||
self.task_btn_area.setWidget(self.task_btn_container)
|
||||
self.left_layout.addWidget(self.task_btn_area, stretch=1)
|
||||
|
||||
self.left_widget.setFixedWidth(180)
|
||||
self.top_layout.addWidget(self.left_widget, stretch=0)
|
||||
# ----------- 左侧任务按钮区 END -----------
|
||||
|
||||
main_layout.addLayout(self.top_layout, stretch=1)
|
||||
|
||||
# 下方按钮区
|
||||
btn_layout = QHBoxLayout()
|
||||
|
||||
# 左下角:添加/删除任务
|
||||
self.add_btn = PrimaryPushButton("创建新任务")
|
||||
self.add_btn.setAutoDefault(False) # 禁止回车触发
|
||||
self.add_btn.setDefault(False)
|
||||
self.del_btn = PushButton("删除当前任务")
|
||||
self.del_btn.setAutoDefault(False) # 禁止回车触发
|
||||
self.del_btn.setDefault(False)
|
||||
self.add_btn.clicked.connect(self.add_task)
|
||||
self.del_btn.clicked.connect(self.delete_current_task)
|
||||
btn_layout.addWidget(self.add_btn)
|
||||
btn_layout.addWidget(self.del_btn)
|
||||
btn_layout.addStretch() # 添加/删除靠左,stretch在中间
|
||||
|
||||
# 右下角:生成/取消
|
||||
self.ok_btn = PrimaryPushButton("生成任务")
|
||||
self.ok_btn.setAutoDefault(False) # 允许回车触发
|
||||
self.ok_btn.setDefault(False) # 设置为默认按钮
|
||||
self.cancel_btn = PushButton("取消")
|
||||
self.cancel_btn.setAutoDefault(False) # 禁止回车触发
|
||||
self.cancel_btn.setDefault(False)
|
||||
btn_layout.addWidget(self.ok_btn)
|
||||
btn_layout.addWidget(self.cancel_btn)
|
||||
main_layout.addLayout(btn_layout)
|
||||
|
||||
self.ok_btn.clicked.connect(self.accept)
|
||||
self.cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
self.tasks = []
|
||||
self.current_index = -1
|
||||
|
||||
if config_path and os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
tasks = yaml.safe_load(f)
|
||||
if tasks:
|
||||
for t in tasks:
|
||||
self.tasks.append(self._make_task_obj(t))
|
||||
except Exception:
|
||||
pass
|
||||
# 允许没有任何任务
|
||||
self.current_index = 0 if self.tasks else -1
|
||||
self.refresh_task_btns()
|
||||
if self.tasks:
|
||||
self.show_task_form(self.tasks[self.current_index])
|
||||
else:
|
||||
self.show_task_form(None)
|
||||
|
||||
def refresh_task_btns(self):
|
||||
# 清空旧按钮
|
||||
while self.task_btn_layout.count():
|
||||
item = self.task_btn_layout.takeAt(0)
|
||||
w = item.widget()
|
||||
if w:
|
||||
w.deleteLater()
|
||||
# 重新添加按钮
|
||||
for idx, t in enumerate(self.tasks):
|
||||
btn = PushButton(t["name"])
|
||||
btn.setCheckable(True)
|
||||
btn.setChecked(idx == self.current_index)
|
||||
btn.clicked.connect(lambda checked, i=idx: self.select_task(i))
|
||||
self.task_btn_layout.addWidget(btn)
|
||||
self.task_btn_layout.addStretch()
|
||||
|
||||
def add_task(self):
|
||||
self.save_form()
|
||||
new_idx = len(self.tasks)
|
||||
self.tasks.append(self._make_task_obj({"name": f"Task{new_idx+1}"}))
|
||||
self.current_index = new_idx
|
||||
self.refresh_task_btns()
|
||||
self.show_task_form(self.tasks[self.current_index])
|
||||
|
||||
def delete_current_task(self):
|
||||
if self.current_index < 0 or not self.tasks:
|
||||
return
|
||||
del self.tasks[self.current_index]
|
||||
if not self.tasks:
|
||||
self.current_index = -1
|
||||
self.refresh_task_btns()
|
||||
self.show_task_form(None)
|
||||
return
|
||||
if self.current_index >= len(self.tasks):
|
||||
self.current_index = len(self.tasks) - 1
|
||||
self.refresh_task_btns()
|
||||
self.show_task_form(self.tasks[self.current_index])
|
||||
|
||||
def select_task(self, idx):
|
||||
self.save_form()
|
||||
self.current_index = idx
|
||||
self.refresh_task_btns()
|
||||
self.show_task_form(self.tasks[idx])
|
||||
|
||||
def show_task_form(self, task):
|
||||
# 先移除旧的 form_widget
|
||||
if hasattr(self, "form_widget") and self.form_widget is not None:
|
||||
self.top_layout.removeWidget(self.form_widget)
|
||||
self.form_widget.deleteLater()
|
||||
self.form_widget = None
|
||||
|
||||
# 新建 form_widget 和 form_layout
|
||||
self.form_widget = QWidget()
|
||||
self.form_layout = QVBoxLayout(self.form_widget)
|
||||
self.form_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.form_layout.setSpacing(12)
|
||||
|
||||
# 添加到右侧
|
||||
self.top_layout.addWidget(self.form_widget, stretch=1)
|
||||
|
||||
if not task:
|
||||
label = TitleLabel("暂无任务,请点击下方“添加任务”。")
|
||||
label.setAlignment(Qt.AlignCenter)
|
||||
self.form_layout.addStretch()
|
||||
self.form_layout.addWidget(label)
|
||||
self.form_layout.addStretch()
|
||||
return
|
||||
|
||||
# 任务名称
|
||||
row1 = QHBoxLayout()
|
||||
label_name = BodyLabel("任务名称")
|
||||
self.name_edit = LineEdit()
|
||||
self.name_edit.setText(task["name"])
|
||||
self.name_edit.setPlaceholderText("任务名称")
|
||||
# 新增:名称编辑完成后刷新按钮
|
||||
self.name_edit.editingFinished.connect(self.on_name_edit_finished)
|
||||
row1.addWidget(label_name)
|
||||
row1.addWidget(self.name_edit)
|
||||
self.form_layout.addLayout(row1)
|
||||
|
||||
# 频率
|
||||
row2 = QHBoxLayout()
|
||||
label_freq = BodyLabel("任务运行频率")
|
||||
self.freq_spin = DoubleSpinBox()
|
||||
self.freq_spin.setRange(0, 10000)
|
||||
self.freq_spin.setDecimals(3)
|
||||
self.freq_spin.setSingleStep(1)
|
||||
self.freq_spin.setSuffix(" Hz")
|
||||
self.freq_spin.setValue(float(task.get("frequency", 500)))
|
||||
row2.addWidget(label_freq)
|
||||
row2.addWidget(self.freq_spin)
|
||||
self.form_layout.addLayout(row2)
|
||||
|
||||
# 延迟
|
||||
row3 = QHBoxLayout()
|
||||
label_delay = BodyLabel("初始化延时")
|
||||
self.delay_spin = SpinBox()
|
||||
self.delay_spin.setRange(0, 10000)
|
||||
self.delay_spin.setSuffix(" ms")
|
||||
self.delay_spin.setValue(task.get("delay", 0))
|
||||
row3.addWidget(label_delay)
|
||||
row3.addWidget(self.delay_spin)
|
||||
self.form_layout.addLayout(row3)
|
||||
|
||||
# 堆栈
|
||||
row4 = QHBoxLayout()
|
||||
label_stack = BodyLabel("堆栈大小")
|
||||
self.stack_spin = SpinBox()
|
||||
self.stack_spin.setRange(128, 8192)
|
||||
self.stack_spin.setSingleStep(128)
|
||||
self.stack_spin.setSuffix(" Byte") # 添加单位
|
||||
self.stack_spin.setValue(task.get("stack", 256))
|
||||
row4.addWidget(label_stack)
|
||||
row4.addWidget(self.stack_spin)
|
||||
self.form_layout.addLayout(row4)
|
||||
|
||||
# 频率控制
|
||||
row5 = QHBoxLayout()
|
||||
self.freq_ctrl = CheckBox("启用默认频率控制")
|
||||
self.freq_ctrl.setChecked(task.get("freq_control", True))
|
||||
row5.addWidget(self.freq_ctrl)
|
||||
self.form_layout.addLayout(row5)
|
||||
|
||||
# 描述
|
||||
label_desc = BodyLabel("任务描述")
|
||||
self.desc_edit = TextEdit()
|
||||
self.desc_edit.setText(task.get("description", ""))
|
||||
self.desc_edit.setPlaceholderText("任务描述")
|
||||
self.form_layout.addWidget(label_desc)
|
||||
self.form_layout.addWidget(self.desc_edit)
|
||||
|
||||
self.form_layout.addStretch()
|
||||
|
||||
def on_name_edit_finished(self):
|
||||
# 保存当前表单内容
|
||||
self.save_form()
|
||||
# 刷新左侧按钮名称
|
||||
self.refresh_task_btns()
|
||||
|
||||
def _make_task_obj(self, task=None):
|
||||
return {
|
||||
"name": task["name"] if task else f"Task1",
|
||||
"frequency": task.get("frequency", 500) if task else 500,
|
||||
"delay": task.get("delay", 0) if task else 0,
|
||||
"stack": task.get("stack", 256) if task else 256,
|
||||
"description": task.get("description", "") if task else "",
|
||||
"freq_control": task.get("freq_control", True) if task else True,
|
||||
}
|
||||
|
||||
def save_form(self):
|
||||
if self.current_index < 0 or self.current_index >= len(self.tasks):
|
||||
return
|
||||
t = self.tasks[self.current_index]
|
||||
t["name"] = self.name_edit.text().strip()
|
||||
t["frequency"] = float(self.freq_spin.value()) # 支持小数
|
||||
t["delay"] = self.delay_spin.value()
|
||||
t["stack"] = self.stack_spin.value()
|
||||
t["description"] = self.desc_edit.toPlainText().strip()
|
||||
t["freq_control"] = self.freq_ctrl.isChecked()
|
||||
|
||||
|
||||
def get_tasks(self):
|
||||
self.save_form()
|
||||
tasks = []
|
||||
for idx, t in enumerate(self.tasks):
|
||||
name = t["name"].strip()
|
||||
freq = t["frequency"]
|
||||
delay = t["delay"]
|
||||
stack = t["stack"]
|
||||
desc = t["description"].strip()
|
||||
freq_ctrl = t["freq_control"]
|
||||
if stack < 128 or (stack & (stack - 1)) != 0 or stack % 128 != 0:
|
||||
raise ValueError(f"第{idx+1}个任务“{name}”的堆栈大小必须为128、256、512、1024等(128*2^n)")
|
||||
task = {
|
||||
"name": name,
|
||||
"function": f"Task_{name}",
|
||||
"delay": delay,
|
||||
"stack": stack,
|
||||
"description": desc,
|
||||
"freq_control": freq_ctrl
|
||||
}
|
||||
if freq_ctrl:
|
||||
task["frequency"] = freq
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
@ -1,45 +0,0 @@
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
import requests
|
||||
import shutil
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
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:
|
||||
rel_path_unix = rel_path.replace("\\", "/")
|
||||
encoded_path = quote(rel_path_unix)
|
||||
url = f"{self.server_url}/download/{encoded_path}"
|
||||
params = {"key": self.secret_key}
|
||||
resp = requests.get(url, params=params, stream=True, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
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:
|
||||
retry += 1
|
||||
except Exception:
|
||||
retry += 1
|
||||
else:
|
||||
fail.append(rel_path)
|
||||
self.progressChanged.emit(int((idx + 1) / total * 100))
|
||||
self.finished.emit(success, fail)
|
||||
@ -1,34 +0,0 @@
|
||||
"""
|
||||
更新检查线程
|
||||
避免阻塞UI界面的更新检查
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
from app.tools.check_update import check_update_availability
|
||||
|
||||
|
||||
class UpdateCheckThread(QThread):
|
||||
"""更新检查线程"""
|
||||
|
||||
# 信号定义
|
||||
update_found = pyqtSignal(dict) # 发现更新
|
||||
no_update = pyqtSignal() # 无更新
|
||||
error_occurred = pyqtSignal(str) # 检查出错
|
||||
|
||||
def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"):
|
||||
super().__init__()
|
||||
self.current_version = current_version
|
||||
self.repo = repo
|
||||
|
||||
def run(self):
|
||||
"""执行更新检查"""
|
||||
try:
|
||||
update_info = check_update_availability(self.current_version, self.repo)
|
||||
|
||||
if update_info:
|
||||
self.update_found.emit(update_info)
|
||||
else:
|
||||
self.no_update.emit()
|
||||
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
自动更新CMakeLists.txt中的User源文件列表
|
||||
这个脚本会扫描User目录下的所有.c文件,并自动更新CMakeLists.txt中的target_sources部分
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def find_user_c_files(user_dir):
|
||||
"""查找User目录下的所有.c文件"""
|
||||
c_files = []
|
||||
user_path = Path(user_dir)
|
||||
|
||||
if not user_path.exists():
|
||||
print(f"错误: User目录不存在: {user_dir}")
|
||||
return []
|
||||
|
||||
# 递归查找所有.c文件
|
||||
for c_file in user_path.rglob("*.c"):
|
||||
# 获取相对于项目根目录的路径
|
||||
relative_path = c_file.relative_to(user_path.parent)
|
||||
# 使用正斜杠确保跨平台兼容性,避免转义字符问题
|
||||
c_files.append(str(relative_path).replace('\\', '/'))
|
||||
|
||||
# 按目录和文件名排序
|
||||
c_files.sort()
|
||||
return c_files
|
||||
|
||||
def update_cmake_sources(cmake_file, c_files):
|
||||
"""更新CMakeLists.txt中的源文件列表"""
|
||||
if not os.path.exists(cmake_file):
|
||||
print(f"错误: CMakeLists.txt文件不存在: {cmake_file}")
|
||||
return False
|
||||
|
||||
# 读取CMakeLists.txt内容
|
||||
with open(cmake_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 构建新的源文件列表
|
||||
sources_section = "# Add sources to executable\ntarget_sources(${CMAKE_PROJECT_NAME} PRIVATE\n"
|
||||
sources_section += " # Add user sources here\n"
|
||||
|
||||
# 按目录分组
|
||||
current_dir = ""
|
||||
for c_file in c_files:
|
||||
# 确保路径使用正斜杠,避免转义字符问题
|
||||
normalized_file = c_file.replace('\\', '/')
|
||||
file_dir = str(Path(normalized_file).parent).replace('\\', '/')
|
||||
if file_dir != current_dir:
|
||||
if current_dir: # 不是第一个目录,添加空行
|
||||
sources_section += "\n"
|
||||
sources_section += f" # {file_dir} sources\n"
|
||||
current_dir = file_dir
|
||||
|
||||
sources_section += f" {normalized_file}\n"
|
||||
|
||||
sources_section += ")"
|
||||
|
||||
# 使用原始字符串避免转义问题,并使用更精确的正则表达式
|
||||
pattern = r'# Add sources to executable\s*\ntarget_sources\(\$\{CMAKE_PROJECT_NAME\}\s+PRIVATE\s*\n(?:.*?\n)*?\)'
|
||||
|
||||
try:
|
||||
if re.search(pattern, content, re.DOTALL | re.MULTILINE):
|
||||
new_content = re.sub(pattern, sources_section, content, flags=re.DOTALL | re.MULTILINE)
|
||||
|
||||
# 写回文件
|
||||
with open(cmake_file, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
print("✅ 成功更新CMakeLists.txt中的源文件列表")
|
||||
return True
|
||||
else:
|
||||
print("❌ 错误: 在CMakeLists.txt中找不到target_sources部分")
|
||||
return False
|
||||
except re.error as e:
|
||||
print(f"❌ 正则表达式错误: {e}")
|
||||
return False
|
||||
|
||||
def update_cmake_includes(cmake_file, user_dir):
|
||||
"""确保CMakeLists.txt中的include路径包含User"""
|
||||
if not os.path.exists(cmake_file):
|
||||
print(f"错误: CMakeLists.txt文件不存在: {cmake_file}")
|
||||
return False
|
||||
|
||||
with open(cmake_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 构建新的include部分
|
||||
include_section = (
|
||||
"# Add include paths\n"
|
||||
"target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE\n"
|
||||
" # Add user defined include paths\n"
|
||||
" User\n"
|
||||
")"
|
||||
)
|
||||
|
||||
# 使用更安全的正则表达式匹配include部分
|
||||
pattern = r'# Add include paths\s*\ntarget_include_directories\(\$\{CMAKE_PROJECT_NAME\}\s+PRIVATE\s*\n(?:.*?\n)*?\)'
|
||||
try:
|
||||
if re.search(pattern, content, re.DOTALL | re.MULTILINE):
|
||||
new_content = re.sub(pattern, include_section, content, flags=re.DOTALL | re.MULTILINE)
|
||||
with open(cmake_file, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
print("✅ 成功更新CMakeLists.txt中的include路径")
|
||||
return True
|
||||
else:
|
||||
print("❌ 错误: 在CMakeLists.txt中找不到target_include_directories部分")
|
||||
return False
|
||||
except re.error as e:
|
||||
print(f"❌ 正则表达式错误: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir
|
||||
|
||||
user_dir = project_root / "User"
|
||||
cmake_file = project_root / "CMakeLists.txt"
|
||||
|
||||
print("🔍 正在扫描User目录下的.c文件...")
|
||||
c_files = find_user_c_files(user_dir)
|
||||
|
||||
if not c_files:
|
||||
print("⚠️ 警告: 在User目录下没有找到.c文件")
|
||||
return
|
||||
|
||||
print(f"📁 找到 {len(c_files)} 个.c文件:")
|
||||
for c_file in c_files:
|
||||
print(f" - {c_file}")
|
||||
|
||||
print(f"\n📝 正在更新 {cmake_file}...")
|
||||
success = update_cmake_sources(cmake_file, c_files)
|
||||
|
||||
if success:
|
||||
print("🎉 更新完成!现在可以重新编译项目了。")
|
||||
else:
|
||||
print("💥 更新失败,请检查错误信息。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,114 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
import zipfile
|
||||
import io
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
def update_code(parent=None, info_callback=None, error_callback=None):
|
||||
url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
|
||||
|
||||
# 使用与CodeGenerator.get_assets_dir相同的逻辑确定assets目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的环境 - 使用可执行文件所在目录
|
||||
exe_dir = os.path.dirname(sys.executable)
|
||||
assets_dir = os.path.join(exe_dir, "assets")
|
||||
print(f"更新代码:打包环境,使用路径: {assets_dir}")
|
||||
|
||||
# 如果exe_dir/assets不存在,尝试使用相对路径作为后备
|
||||
if not os.path.exists(assets_dir):
|
||||
assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../assets")
|
||||
print(f"更新代码:后备路径: {assets_dir}")
|
||||
else:
|
||||
# 开发环境
|
||||
assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../assets")
|
||||
print(f"更新代码:开发环境,使用路径: {assets_dir}")
|
||||
|
||||
local_dir = os.path.join(assets_dir, "User_code")
|
||||
print(f"更新代码:最终目标目录: {local_dir}")
|
||||
|
||||
try:
|
||||
# 下载远程代码库
|
||||
resp = requests.get(url, timeout=30)
|
||||
resp.raise_for_status()
|
||||
|
||||
# 创建临时目录进行操作
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# 解压到临时目录
|
||||
z = zipfile.ZipFile(io.BytesIO(resp.content))
|
||||
extract_path = os.path.join(temp_dir, "extracted")
|
||||
z.extractall(extract_path)
|
||||
|
||||
# 获取解压后的根目录
|
||||
extracted_items = os.listdir(extract_path)
|
||||
if not extracted_items:
|
||||
raise Exception("下载的压缩包为空")
|
||||
|
||||
source_root = os.path.join(extract_path, extracted_items[0])
|
||||
|
||||
# 确保本地目录的父目录存在
|
||||
parent_dir = os.path.dirname(local_dir)
|
||||
if not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
# 创建备份(如果原目录存在)
|
||||
backup_dir = None
|
||||
if os.path.exists(local_dir):
|
||||
backup_dir = f"{local_dir}_backup_{int(time.time())}"
|
||||
try:
|
||||
shutil.move(local_dir, backup_dir)
|
||||
except Exception as e:
|
||||
# 如果移动失败,尝试强制删除
|
||||
shutil.rmtree(local_dir, ignore_errors=True)
|
||||
|
||||
try:
|
||||
# 复制新文件到目标位置
|
||||
shutil.copytree(source_root, local_dir)
|
||||
|
||||
# 验证复制是否成功
|
||||
if not os.path.exists(local_dir):
|
||||
raise Exception("复制失败,目标目录不存在")
|
||||
|
||||
# 设置正确的文件权限,确保文件可以被正常访问和修改
|
||||
for root, dirs, files in os.walk(local_dir):
|
||||
# 设置目录权限为755 (rwxr-xr-x)
|
||||
for dir_name in dirs:
|
||||
dir_path = os.path.join(root, dir_name)
|
||||
try:
|
||||
os.chmod(dir_path, 0o755)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 设置文件权限为644 (rw-r--r--)
|
||||
for file_name in files:
|
||||
file_path = os.path.join(root, file_name)
|
||||
try:
|
||||
os.chmod(file_path, 0o644)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 删除备份目录(更新成功)
|
||||
if backup_dir and os.path.exists(backup_dir):
|
||||
shutil.rmtree(backup_dir, ignore_errors=True)
|
||||
|
||||
if info_callback:
|
||||
info_callback(parent)
|
||||
return True
|
||||
|
||||
except Exception as copy_error:
|
||||
# 恢复备份
|
||||
if backup_dir and os.path.exists(backup_dir):
|
||||
if os.path.exists(local_dir):
|
||||
shutil.rmtree(local_dir, ignore_errors=True)
|
||||
try:
|
||||
shutil.move(backup_dir, local_dir)
|
||||
except:
|
||||
pass
|
||||
raise copy_error
|
||||
|
||||
except Exception as e:
|
||||
if error_callback:
|
||||
error_callback(parent, str(e))
|
||||
return False
|
||||
BIN
assets/.DS_Store
vendored
BIN
assets/User_code/.DS_Store
vendored
@ -1,142 +0,0 @@
|
||||
/*
|
||||
AI
|
||||
*/
|
||||
|
||||
/* Includes ----------------------------------------------------------------- */
|
||||
#include "ai.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "bsp/time.h"
|
||||
#include "bsp/uart.h"
|
||||
#include "component/ahrs.h"
|
||||
#include "component/crc16.h"
|
||||
#include "component/crc8.h"
|
||||
#include "component/user_math.h"
|
||||
#include "component/filter.h"
|
||||
|
||||
|
||||
/* Private define ----------------------------------------------------------- */
|
||||
#define AI_LEN_RX_BUFF (sizeof(AI_DownPackage_t))
|
||||
|
||||
/* Private macro ------------------------------------------------------------ */
|
||||
/* Private typedef ---------------------------------------------------------- */
|
||||
/* Private variables -------------------------------------------------------- */
|
||||
|
||||
static uint8_t rxbuf[AI_LEN_RX_BUFF];
|
||||
|
||||
static bool inited = false;
|
||||
|
||||
static osThreadId_t thread_alert;
|
||||
|
||||
static uint32_t drop_message = 0;
|
||||
|
||||
// uint16_t crc16;
|
||||
|
||||
/* Private function -------------------------------------------------------- */
|
||||
|
||||
static void Ai_RxCpltCallback(void) {
|
||||
osThreadFlagsSet(thread_alert, SIGNAL_AI_RAW_REDY);
|
||||
}
|
||||
|
||||
static void Ai_IdleLineCallback(void) {
|
||||
osThreadFlagsSet(thread_alert, SIGNAL_AI_RAW_REDY);
|
||||
}
|
||||
|
||||
/* Exported functions ------------------------------------------------------- */
|
||||
int8_t AI_Init(AI_t *ai) {
|
||||
UNUSED(ai);
|
||||
if (inited) return DEVICE_ERR_INITED;
|
||||
thread_alert = osThreadGetId();
|
||||
|
||||
BSP_UART_RegisterCallback(BSP_UART_AI, BSP_UART_RX_CPLT_CB,
|
||||
Ai_RxCpltCallback);
|
||||
BSP_UART_RegisterCallback(BSP_UART_AI, BSP_UART_IDLE_LINE_CB,
|
||||
Ai_IdleLineCallback);
|
||||
|
||||
inited = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t AI_Restart(AI_t *ai) {
|
||||
UNUSED(ai);
|
||||
__HAL_UART_DISABLE(BSP_UART_GetHandle(BSP_UART_AI));
|
||||
__HAL_UART_ENABLE(BSP_UART_GetHandle(BSP_UART_AI));
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_StartReceiving(AI_t *ai) {
|
||||
UNUSED(ai);
|
||||
// if (HAL_UART_Receive_DMA(BSP_UART_GetHandle(BSP_UART_AI), rxbuf,
|
||||
// AI_LEN_RX_BUFF) == HAL_OK)
|
||||
if (BSP_UART_Receive(BSP_UART_AI, rxbuf,
|
||||
AI_LEN_RX_BUFF, true) == HAL_OK)
|
||||
return DEVICE_OK;
|
||||
return DEVICE_ERR;
|
||||
}
|
||||
|
||||
bool AI_WaitDmaCplt(void) {
|
||||
return (osThreadFlagsWait(SIGNAL_AI_RAW_REDY, osFlagsWaitAll,0) ==
|
||||
SIGNAL_AI_RAW_REDY);
|
||||
}
|
||||
|
||||
int8_t AI_ParseHost(AI_t *ai) {
|
||||
// crc16 = CRC16_Calc((const uint8_t *)&(rxbuf), sizeof(ai->from_host) - 2, CRC16_INIT);
|
||||
if (!CRC16_Verify((const uint8_t *)&(rxbuf), sizeof(ai->from_host)))
|
||||
goto error;
|
||||
ai->header.online = true;
|
||||
ai->header.last_online_time = BSP_TIME_Get();
|
||||
memcpy(&(ai->from_host), rxbuf, sizeof(ai->from_host));
|
||||
memset(rxbuf, 0, AI_LEN_RX_BUFF);
|
||||
return DEVICE_OK;
|
||||
|
||||
error:
|
||||
drop_message++;
|
||||
return DEVICE_ERR;
|
||||
}
|
||||
|
||||
int8_t AI_PackMCU(AI_t *ai, const AHRS_Quaternion_t *data){
|
||||
if (ai == NULL || data == NULL) return DEVICE_ERR_NULL;
|
||||
ai->to_host.mcu.id = AI_ID_MCU;
|
||||
ai->to_host.mcu.package.quat=*data;
|
||||
ai->to_host.mcu.package.notice = ai->status;
|
||||
ai->to_host.mcu.crc16 = CRC16_Calc((const uint8_t *)&(ai->to_host.mcu), sizeof(AI_UpPackageMCU_t) - 2, CRC16_INIT);
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_PackRef(AI_t *ai, const AI_UpPackageReferee_t *data) {
|
||||
if (ai == NULL || data == NULL) return DEVICE_ERR_NULL;
|
||||
ai->to_host.ref = *data;
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_HandleOffline(AI_t *ai) {
|
||||
if (ai == NULL) return DEVICE_ERR_NULL;
|
||||
if (BSP_TIME_Get() - ai->header.last_online_time >
|
||||
100000) {
|
||||
ai->header.online = false;
|
||||
}
|
||||
return DEVICE_OK;
|
||||
}
|
||||
|
||||
int8_t AI_StartSend(AI_t *ai, bool ref_online){
|
||||
if (ai == NULL) return DEVICE_ERR_NULL;
|
||||
|
||||
if (ref_online) {
|
||||
// 发送裁判系统数据和MCU数据
|
||||
if (BSP_UART_Transmit(BSP_UART_AI, (uint8_t *)&(ai->to_host),
|
||||
sizeof(ai->to_host.ref) + sizeof(ai->to_host.mcu), true) == HAL_OK)
|
||||
return DEVICE_OK;
|
||||
else
|
||||
return DEVICE_ERR;
|
||||
} else {
|
||||
// 只发送MCU数据
|
||||
if (BSP_UART_Transmit(BSP_UART_AI, (uint8_t *)&(ai->to_host.mcu),
|
||||
sizeof(ai->to_host.mcu), true) == HAL_OK)
|
||||
return DEVICE_OK;
|
||||
else
|
||||
return DEVICE_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
/*
|
||||
AI
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/cdefs.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Includes ----------------------------------------------------------------- */
|
||||
#include "component/ahrs.h"
|
||||
#include "component/filter.h"
|
||||
#include "component/user_math.h"
|
||||
#include "device/device.h"
|
||||
#include <cmsis_os2.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Exported constants ------------------------------------------------------- */
|
||||
/* Exported macro ----------------------------------------------------------- */
|
||||
#define AI_ID_MCU (0xC4)
|
||||
#define AI_ID_REF (0xA8)
|
||||
#define AI_ID_AI (0xA1)
|
||||
/* Exported types ----------------------------------------------------------- */
|
||||
|
||||
typedef enum {
|
||||
AI_ARMOR_HERO = 0, /*英雄机器人*/
|
||||
AI_ARMOR_INFANTRY, /*步兵机器人*/
|
||||
AI_ARMOR_SENTRY, /*哨兵机器人*/
|
||||
AI_ARMOR_ENGINEER, /*工程机器人*/
|
||||
AI_ARMOR_OUTPOST, /*前哨占*/
|
||||
AI_ARMOR_BASE, /*基地*/
|
||||
AI_ARMOR_NORMAL, /*由AI自动选择*/
|
||||
} AI_ArmorsType_t;
|
||||
|
||||
typedef enum {
|
||||
AI_STATUS_OFF = 0, /* 关闭 */
|
||||
AI_STATUS_AUTOAIM, /* 自瞄 */
|
||||
AI_STATUS_AUTOPICK, /* 自动取矿 */
|
||||
AI_STATUS_AUTOPUT, /* 自动兑矿 */
|
||||
AI_STATUS_AUTOHITBUFF, /* 自动打符 */
|
||||
AI_STATUS_AUTONAV,
|
||||
} AI_Status_t;
|
||||
|
||||
typedef enum {
|
||||
AI_NOTICE_NONE = 0,
|
||||
AI_NOTICE_SEARCH,
|
||||
AI_NOTICE_FIRE,
|
||||
}AI_Notice_t;
|
||||
|
||||
/* 电控 -> 视觉 MCU数据结构体*/
|
||||
typedef struct __packed {
|
||||
AHRS_Quaternion_t quat; /* 四元数 */
|
||||
// struct {
|
||||
// AI_ArmorsType_t armor_type;
|
||||
// AI_Status_t status;
|
||||
// }notice; /* 控制命令 */
|
||||
uint8_t notice;
|
||||
} AI_Protucol_UpDataMCU_t;
|
||||
|
||||
/* 电控 -> 视觉 裁判系统数据结构体*/
|
||||
typedef struct __packed {
|
||||
/* USER REFEREE BEGIN */
|
||||
uint16_t team; /* 本身队伍 */
|
||||
uint16_t time; /* 比赛开始时间 */
|
||||
/* USER REFEREE END */
|
||||
} AI_Protocol_UpDataReferee_t;
|
||||
|
||||
/* 视觉 -> 电控 数据包结构体*/
|
||||
typedef struct __packed {
|
||||
AHRS_Eulr_t eulr; /* 欧拉角 */
|
||||
MoveVector_t move_vec; /* 运动向量 */
|
||||
uint8_t notice; /* 控制命令 */
|
||||
} AI_Protocol_DownData_t;
|
||||
|
||||
/* 电控 -> 视觉 裁判系统数据包 */
|
||||
typedef struct __packed {
|
||||
uint8_t id; /* 包ID */
|
||||
AI_Protocol_UpDataReferee_t package; /* 数据包 */
|
||||
uint16_t crc16; /* CRC16校验 */
|
||||
} AI_UpPackageReferee_t;
|
||||
|
||||
/* 电控 -> 视觉 MUC数据包 */
|
||||
typedef struct __packed {
|
||||
uint8_t id;
|
||||
AI_Protucol_UpDataMCU_t package;
|
||||
uint16_t crc16;
|
||||
} AI_UpPackageMCU_t;
|
||||
|
||||
/* 视觉 -> 电控 数据包 */
|
||||
typedef struct __packed {
|
||||
uint8_t id; /* 包ID */
|
||||
AI_Protocol_DownData_t package; /* 数据包 */
|
||||
uint16_t crc16; /* CRC16校验 */
|
||||
} AI_DownPackage_t;
|
||||
|
||||
typedef struct __packed {
|
||||
DEVICE_Header_t header; /* 设备通用头部 */
|
||||
AI_DownPackage_t from_host;
|
||||
AI_Status_t status;
|
||||
struct {
|
||||
AI_UpPackageReferee_t ref;
|
||||
AI_UpPackageMCU_t mcu;
|
||||
} to_host;
|
||||
} AI_t;
|
||||
|
||||
/* Exported functions prototypes -------------------------------------------- */
|
||||
|
||||
int8_t AI_Init(AI_t *ai);
|
||||
|
||||
int8_t AI_Restart(AI_t *ai);
|
||||
|
||||
int8_t AI_StartReceiving(AI_t *ai);
|
||||
|
||||
bool AI_WaitDmaCplt(void);
|
||||
|
||||
int8_t AI_ParseHost(AI_t *ai);
|
||||
|
||||
int8_t AI_PackMCU(AI_t *ai, const AHRS_Quaternion_t *quat);
|
||||
|
||||
int8_t AI_PackRef(AI_t *ai, const AI_UpPackageReferee_t *data);
|
||||
|
||||
int8_t AI_HandleOffline(AI_t *ai);
|
||||
|
||||
int8_t AI_StartSend(AI_t *ai, bool ref_online);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 562 KiB |
BIN
assets/mech_lib/.DS_Store
vendored
@ -1 +0,0 @@
|
||||
# 机械常用零件库
|
||||
@ -22,8 +22,7 @@ static void (*SPI_Callback[BSP_SPI_NUM][BSP_SPI_CB_NUM])(void);
|
||||
|
||||
/* Private function -------------------------------------------------------- */
|
||||
static BSP_SPI_t SPI_Get(SPI_HandleTypeDef *hspi) {
|
||||
if (hspi->Instance == SPI1)
|
||||
return BSP_SPI_BMI088;
|
||||
/* AUTO GENERATED SPI_GET */
|
||||
else
|
||||
return BSP_SPI_ERR;
|
||||
}
|
||||
@ -96,8 +95,7 @@ void HAL_SPI_AbortCpltCallback(SPI_HandleTypeDef *hspi) {
|
||||
/* Exported functions ------------------------------------------------------- */
|
||||
SPI_HandleTypeDef *BSP_SPI_GetHandle(BSP_SPI_t spi) {
|
||||
switch (spi) {
|
||||
case BSP_SPI_BMI088:
|
||||
return &hspi1;
|
||||
/* AUTO GENERATED BSP_SPI_GET_HANDLE */
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
387
component/cmd.c
Normal file
@ -0,0 +1,387 @@
|
||||
/*
|
||||
控制命令
|
||||
*/
|
||||
|
||||
#include "cmd.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* USER INCLUDE BEGIN */
|
||||
|
||||
/* USER INCLUDE END */
|
||||
|
||||
/* USER DEFINE BEGIN */
|
||||
|
||||
/* USER DEFINE END */
|
||||
|
||||
/**
|
||||
* @brief 行为转换为对应按键
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
* @param behavior 行为
|
||||
* @return uint16_t 行为对应的按键
|
||||
*/
|
||||
static inline CMD_KeyValue_t CMD_BehaviorToKey(CMD_t *cmd,
|
||||
CMD_Behavior_t behavior) {
|
||||
return cmd->param->map.key_map[behavior].key;
|
||||
}
|
||||
|
||||
static inline CMD_ActiveType_t CMD_BehaviorToActive(CMD_t *cmd,
|
||||
CMD_Behavior_t behavior) {
|
||||
return cmd->param->map.key_map[behavior].active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查按键是否按下
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param key 按键名称
|
||||
* @param stateful 是否为状态切换按键
|
||||
* @return true 按下
|
||||
* @return false 未按下
|
||||
*/
|
||||
static bool CMD_KeyPressedRc(const CMD_RC_t *rc, CMD_KeyValue_t key) {
|
||||
/* 按下按键为鼠标左、右键 */
|
||||
if (key == CMD_L_CLICK) {
|
||||
return rc->mouse.l_click;
|
||||
}
|
||||
if (key == CMD_R_CLICK) {
|
||||
return rc->mouse.r_click;
|
||||
}
|
||||
return rc->key & (1u << key);
|
||||
}
|
||||
|
||||
static bool CMD_BehaviorOccurredRc(const CMD_RC_t *rc, CMD_t *cmd,
|
||||
CMD_Behavior_t behavior) {
|
||||
CMD_KeyValue_t key = CMD_BehaviorToKey(cmd, behavior);
|
||||
CMD_ActiveType_t active = CMD_BehaviorToActive(cmd, behavior);
|
||||
|
||||
bool now_key_pressed, last_key_pressed;
|
||||
|
||||
/* 按下按键为鼠标左、右键 */
|
||||
if (key == CMD_L_CLICK) {
|
||||
now_key_pressed = rc->mouse.l_click;
|
||||
last_key_pressed = cmd->mouse_last.l_click;
|
||||
} else if (key == CMD_R_CLICK) {
|
||||
now_key_pressed = rc->mouse.r_click;
|
||||
last_key_pressed = cmd->mouse_last.r_click;
|
||||
} else {
|
||||
now_key_pressed = rc->key & (1u << key);
|
||||
last_key_pressed = cmd->key_last & (1u << key);
|
||||
}
|
||||
|
||||
switch (active) {
|
||||
case CMD_ACTIVE_PRESSING:
|
||||
return now_key_pressed && !last_key_pressed;
|
||||
case CMD_ACTIVE_RASING:
|
||||
return !now_key_pressed && last_key_pressed;
|
||||
case CMD_ACTIVE_PRESSED:
|
||||
return now_key_pressed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析pc行为逻辑
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 主结构体
|
||||
* @param dt_sec 两次解析的间隔
|
||||
*/
|
||||
static void CMD_PcLogic(const CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
|
||||
/* 云台设置为鼠标控制欧拉角的变化,底盘的控制向量设置为零 */
|
||||
cmd->gimbal.delta_eulr.yaw =
|
||||
(float)rc->mouse.x * dt_sec * cmd->param->sens_mouse;
|
||||
cmd->gimbal.delta_eulr.pit =
|
||||
(float)(-rc->mouse.y) * dt_sec * cmd->param->sens_mouse;
|
||||
cmd->chassis.ctrl_vec.vx = cmd->chassis.ctrl_vec.vy = 0.0f;
|
||||
cmd->shoot.reverse_trig = false;
|
||||
|
||||
/* 按键行为映射相关逻辑 */
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FORE)) {
|
||||
cmd->chassis.ctrl_vec.vy += cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_BACK)) {
|
||||
cmd->chassis.ctrl_vec.vy -= cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_LEFT)) {
|
||||
cmd->chassis.ctrl_vec.vx -= cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_RIGHT)) {
|
||||
cmd->chassis.ctrl_vec.vx += cmd->param->move.move_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_ACCELERATE)) {
|
||||
cmd->chassis.ctrl_vec.vx *= cmd->param->move.move_fast_sense;
|
||||
cmd->chassis.ctrl_vec.vy *= cmd->param->move.move_fast_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_DECELEBRATE)) {
|
||||
cmd->chassis.ctrl_vec.vx *= cmd->param->move.move_slow_sense;
|
||||
cmd->chassis.ctrl_vec.vy *= cmd->param->move.move_slow_sense;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FIRE)) {
|
||||
/* 切换至开火模式,设置相应的射击频率和弹丸初速度 */
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire = true;
|
||||
} else {
|
||||
/* 切换至准备模式,停止射击 */
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire = false;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FIRE_MODE)) {
|
||||
/* 每按一次依次切换开火下一个模式 */
|
||||
cmd->shoot.fire_mode++;
|
||||
cmd->shoot.fire_mode %= FIRE_MODE_NUM;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_ROTOR)) {
|
||||
/* 切换到小陀螺模式 */
|
||||
cmd->chassis.mode = CHASSIS_MODE_ROTOR;
|
||||
cmd->chassis.mode_rotor = ROTOR_MODE_RAND;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_OPENCOVER)) {
|
||||
/* 每按一次开、关弹舱盖 */
|
||||
cmd->shoot.cover_open = !cmd->shoot.cover_open;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_BUFF)) {
|
||||
if (cmd->ai_status == AI_STATUS_HITSWITCH) {
|
||||
/* 停止ai的打符模式,停用host控制 */
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_HIT_SWITCH_STOP);
|
||||
cmd->host_overwrite = false;
|
||||
cmd->ai_status = AI_STATUS_STOP;
|
||||
} else if (cmd->ai_status == AI_STATUS_AUTOAIM) {
|
||||
/* 自瞄模式中切换失败提醒 */
|
||||
} else {
|
||||
/* ai切换至打符模式,启用host控制 */
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_HIT_SWITCH_START);
|
||||
cmd->ai_status = AI_STATUS_HITSWITCH;
|
||||
cmd->host_overwrite = true;
|
||||
}
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_AUTOAIM)) {
|
||||
if (cmd->ai_status == AI_STATUS_AUTOAIM) {
|
||||
/* 停止ai的自瞄模式,停用host控制 */
|
||||
cmd->host_overwrite = false;
|
||||
cmd->ai_status = AI_STATUS_STOP;
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_AUTO_AIM_STOP);
|
||||
} else {
|
||||
/* ai切换至自瞄模式,启用host控制 */
|
||||
cmd->ai_status = AI_STATUS_AUTOAIM;
|
||||
cmd->host_overwrite = true;
|
||||
CMD_RefereeAdd(&(cmd->referee), CMD_UI_AUTO_AIM_START);
|
||||
}
|
||||
} else {
|
||||
cmd->host_overwrite = false;
|
||||
// TODO: 修复逻辑
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_REVTRIG)) {
|
||||
/* 按下拨弹反转 */
|
||||
cmd->shoot.reverse_trig = true;
|
||||
}
|
||||
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FOLLOWGIMBAL35)) {
|
||||
cmd->chassis.mode = CHASSIS_MODE_FOLLOW_GIMBAL_35;
|
||||
}
|
||||
/* 保存当前按下的键位状态 */
|
||||
cmd->key_last = rc->key;
|
||||
memcpy(&(cmd->mouse_last), &(rc->mouse), sizeof(cmd->mouse_last));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析rc行为逻辑
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 主结构体
|
||||
* @param dt_sec 两次解析的间隔
|
||||
*/
|
||||
static void CMD_RcLogic(const CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
|
||||
switch (rc->sw_l) {
|
||||
/* 左拨杆相应行为选择和解析 */
|
||||
case CMD_SW_UP:
|
||||
cmd->chassis.mode = CHASSIS_MODE_BREAK;
|
||||
break;
|
||||
|
||||
case CMD_SW_MID:
|
||||
cmd->chassis.mode = CHASSIS_MODE_FOLLOW_GIMBAL;
|
||||
break;
|
||||
|
||||
case CMD_SW_DOWN:
|
||||
cmd->chassis.mode = CHASSIS_MODE_ROTOR;
|
||||
cmd->chassis.mode_rotor = ROTOR_MODE_CW;
|
||||
break;
|
||||
|
||||
case CMD_SW_ERR:
|
||||
cmd->chassis.mode = CHASSIS_MODE_RELAX;
|
||||
break;
|
||||
}
|
||||
switch (rc->sw_r) {
|
||||
/* 右拨杆相应行为选择和解析*/
|
||||
case CMD_SW_UP:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
cmd->shoot.mode = SHOOT_MODE_SAFE;
|
||||
break;
|
||||
|
||||
case CMD_SW_MID:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
cmd->shoot.fire = false;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
break;
|
||||
|
||||
case CMD_SW_DOWN:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire_mode = FIRE_MODE_SINGLE;
|
||||
cmd->shoot.fire = true;
|
||||
break;
|
||||
/*
|
||||
case CMD_SW_UP:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_SAFE;
|
||||
break;
|
||||
|
||||
case CMD_SW_MID:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.fire = false;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
break;
|
||||
|
||||
case CMD_SW_DOWN:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire_mode = FIRE_MODE_SINGLE;
|
||||
cmd->shoot.fire = true;
|
||||
break;
|
||||
*/
|
||||
case CMD_SW_ERR:
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_RELAX;
|
||||
}
|
||||
/* 将操纵杆的对应值转换为底盘的控制向量和云台变化的欧拉角 */
|
||||
cmd->chassis.ctrl_vec.vx = rc->ch_l_x;
|
||||
cmd->chassis.ctrl_vec.vy = rc->ch_l_y;
|
||||
cmd->gimbal.delta_eulr.yaw = rc->ch_r_x * dt_sec * cmd->param->sens_rc;
|
||||
cmd->gimbal.delta_eulr.pit = rc->ch_r_y * dt_sec * cmd->param->sens_rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief rc失控时机器人恢复放松模式
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
*/
|
||||
static void CMD_RcLostLogic(CMD_t *cmd) {
|
||||
/* 机器人底盘、云台、射击运行模式恢复至放松模式 */
|
||||
cmd->chassis.mode = CHASSIS_MODE_RELAX;
|
||||
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
|
||||
cmd->shoot.mode = SHOOT_MODE_RELAX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化命令解析
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
* @param param 参数
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_Init(CMD_t *cmd, const CMD_Params_t *param) {
|
||||
/* 指针检测 */
|
||||
if (cmd == NULL) return -1;
|
||||
if (param == NULL) return -1;
|
||||
|
||||
/* 设置机器人的命令参数,初始化控制方式为rc控制 */
|
||||
cmd->pc_ctrl = false;
|
||||
cmd->param = param;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否启用上位机控制指令覆盖
|
||||
*
|
||||
* @param cmd 主结构体
|
||||
* @return true 启用
|
||||
* @return false 不启用
|
||||
*/
|
||||
inline bool CMD_CheckHostOverwrite(CMD_t *cmd) { return cmd->host_overwrite; }
|
||||
|
||||
/**
|
||||
* @brief 解析命令
|
||||
*
|
||||
* @param rc 遥控器数据
|
||||
* @param cmd 命令
|
||||
* @param dt_sec 两次解析的间隔
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_ParseRc(CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
|
||||
/* 指针检测 */
|
||||
if (rc == NULL) return -1;
|
||||
if (cmd == NULL) return -1;
|
||||
|
||||
/* 在pc控制和rc控制间切换 */
|
||||
if (CMD_KeyPressedRc(rc, CMD_KEY_SHIFT) &&
|
||||
CMD_KeyPressedRc(rc, CMD_KEY_CTRL) && CMD_KeyPressedRc(rc, CMD_KEY_Q))
|
||||
cmd->pc_ctrl = true;
|
||||
|
||||
if (CMD_KeyPressedRc(rc, CMD_KEY_SHIFT) &&
|
||||
CMD_KeyPressedRc(rc, CMD_KEY_CTRL) && CMD_KeyPressedRc(rc, CMD_KEY_E))
|
||||
cmd->pc_ctrl = false;
|
||||
/*c当rc丢控时,恢复机器人至默认状态 */
|
||||
if ((rc->sw_l == CMD_SW_ERR) || (rc->sw_r == CMD_SW_ERR)) {
|
||||
CMD_RcLostLogic(cmd);
|
||||
} else {
|
||||
if (cmd->pc_ctrl) {
|
||||
CMD_PcLogic(rc, cmd, dt_sec);
|
||||
} else {
|
||||
CMD_RcLogic(rc, cmd, dt_sec);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析上位机命令
|
||||
*
|
||||
* @param host host数据
|
||||
* @param cmd 命令
|
||||
* @param dt_sec 两次解析的间隔
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_ParseHost(const CMD_Host_t *host, CMD_t *cmd, float dt_sec) {
|
||||
(void)dt_sec; /* 未使用dt_sec,消除警告 */
|
||||
/* 指针检测 */
|
||||
if (host == NULL) return -1;
|
||||
if (cmd == NULL) return -1;
|
||||
|
||||
/* 云台欧拉角设置为host相应的变化的欧拉角 */
|
||||
cmd->gimbal.delta_eulr.yaw = host->gimbal_delta.yaw;
|
||||
cmd->gimbal.delta_eulr.pit = host->gimbal_delta.pit;
|
||||
|
||||
/* host射击命令,设置不同的射击频率和弹丸初速度 */
|
||||
if (host->fire) {
|
||||
cmd->shoot.mode = SHOOT_MODE_LOADED;
|
||||
cmd->shoot.fire = true;
|
||||
} else {
|
||||
cmd->shoot.mode = SHOOT_MODE_SAFE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加向Referee发送的命令
|
||||
*
|
||||
* @param ref 命令队列
|
||||
* @param cmd 要添加的命令
|
||||
* @return int8_t 0对应没有错误
|
||||
*/
|
||||
int8_t CMD_RefereeAdd(CMD_RefereeCmd_t *ref, CMD_UI_t cmd) {
|
||||
/* 指针检测 */
|
||||
if (ref == NULL) return -1;
|
||||
/* 越界检测 */
|
||||
if (ref->counter >= CMD_REFEREE_MAX_NUM || ref->counter < 0) return -1;
|
||||
|
||||
/* 添加机器人当前行为状态到画图的命令队列中 */
|
||||
ref->cmd[ref->counter] = cmd;
|
||||
ref->counter++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* USER FUNCTION BEGIN */
|
||||
|
||||
/* USER FUNCTION END */
|
||||