Compare commits

..

30 Commits

Author SHA1 Message Date
2517d8b9ec 更新spi 2025-11-17 22:29:22 +08:00
4cf349cf14 修复motor_rm 2025-11-17 18:47:18 +08:00
b7f2539321 buzzer 2025-10-23 19:46:14 +08:00
b03ce04217 更新 2025-10-13 15:22:41 +08:00
17847459b4 更新user_math 2025-10-02 21:41:52 +08:00
c3faf353e9 修复dji电机bug 2025-10-02 01:07:46 +08:00
12db6ce44a 修复dr16 2025-09-30 06:18:38 +08:00
20b6af6c34 修复dr16 2025-09-24 19:55:13 +08:00
05ad2226de 更新 2025-09-22 14:50:56 +08:00
9d6a10135d 修复rm 2025-09-20 16:33:28 +08:00
8d741c6d61 修复rm 2025-09-20 16:03:38 +08:00
83aff179ee 更新 2025-09-20 01:29:40 +08:00
b56d7bf8e0 更新 2025-09-15 21:49:41 +08:00
e22ed7f393 更新 2025-09-07 14:10:30 +08:00
ab3445ef6d 保存 2025-09-07 12:41:50 +08:00
09eabb804a 修改config 2025-09-06 17:29:02 +08:00
ab0a95b0af 全新版本 2025-09-06 13:06:12 +08:00
b36738ecac 随便添加一下 2025-08-01 03:28:47 +08:00
a91f175e9e 修改delay 2025-07-31 04:46:32 +08:00
5369861c88 修改模版 2025-07-31 03:58:59 +08:00
c13d3a5e44 修复模版 2025-06-21 14:26:42 +08:00
345743a1c6 修改task.c 2025-06-20 01:14:07 +08:00
91eeda0e07 添加自动移控制功能 2025-06-20 00:30:57 +08:00
d467318505 添加了任务初始化部分 2025-06-19 23:44:48 +08:00
69c200d4c9 添加描述 2025-06-19 20:41:35 +08:00
22bddbcda7 优化代码模版 2025-06-19 01:10:01 +08:00
fc94a3fa33 修改init模版 2025-06-18 22:48:56 +08:00
5cabd8c3b6 修改user_task模版 2025-06-18 22:16:54 +08:00
94841a02dd 修改目录结构 2025-06-18 14:41:10 +08:00
78104b724b 单独的代码架构 2025-06-18 14:30:26 +08:00
174 changed files with 711 additions and 7513 deletions

BIN
.DS_Store vendored

Binary file not shown.

35
.gitignore vendored
View File

@ -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
View File

@ -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
View File

@ -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.

View File

@ -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"

View File

@ -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_() 是一个阻塞调用,直到应用程序退出。

View File

@ -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"
```

View File

View File

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

View File

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

View File

@ -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")

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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)

View File

@ -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
)

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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])

View File

@ -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()

View File

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

View File

@ -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()

View File

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

View File

@ -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._MEIPASSPyInstaller的临时解包目录
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

View File

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

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -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;
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

View File

@ -1 +0,0 @@
# 机械常用零件库

View File

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

Some files were not shown because too many files have changed in this diff Show More