Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
a7c46ca9fc | |||
89fbaf8e55 | |||
b4a4d87909 | |||
88ec1517fb | |||
08193b8093 | |||
7951dae760 | |||
501a9ddff4 | |||
47e0b8419f | |||
9fc6b4577a | |||
78661f450b | |||
62b4b07912 | |||
f2fedac360 |
11
MRobot.iss
@ -8,10 +8,11 @@ OutputBaseFilename=MRobotInstaller
|
||||
|
||||
[Files]
|
||||
Source: "dist\MRobot.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "img\*"; DestDir: "{app}\img"; Flags: ignoreversion recursesubdirs
|
||||
Source: "User_code\*"; DestDir: "{app}\User_code"; Flags: ignoreversion recursesubdirs
|
||||
Source: "mech_lib\*"; DestDir: "{app}\mech_lib"; Flags: ignoreversion recursesubdirs
|
||||
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}\img\M.ico"
|
||||
Name: "{userdesktop}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\img\M.ico"
|
||||
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"
|
15
README.md
@ -122,4 +122,17 @@ pyinstaller --noconfirm --onefile --windowed --add-data "img;img" --add-data "Us
|
||||
|
||||
pyinstaller MRobot.py
|
||||
|
||||
pyinstaller --noconfirm --onefile --windowed --icon=img/M.ico --add-data "img;img" --add-data "User_code;User_code" --add-data "mech_lib;mech_lib" MRobot.py
|
||||
pyinstaller --noconfirm --onefile --windowed --icon=img/M.ico --add-data "img;img" --add-data "User_code;User_code" --add-data "mech_lib;mech_lib" MRobot.py
|
||||
|
||||
|
||||
pyinstaller --onefile --windowed --icon=assets/logo/M.ico --add-data "assets/logo:assets/logo" --add-data "assets/User_code:assets/User_code" --add-data "assets/mech_lib:assets/mech_lib" --collect-all pandas MRobot.py
|
||||
|
||||
pyinstaller --onefile --windowed --icon=assets/logo/M.ico --add-data "assets/logo:assets/logo" --add-data "assets/User_code:assets/User_code" --add-data "assets/mech_lib:assets/mech_lib" MRobot.py
|
||||
|
||||
python3 -m pyinstaller MRobot.py --onefile --windowed --add-data "assets:assets" --add-data "app:app" --add-data "app/tools:app/tools"
|
||||
|
||||
python -m pyinstaller MRobot.py --onefile --windowed --add-data "assets;assets" --add-data "app;app" --add-data "app/tools;app/tools"
|
||||
|
||||
/Users/lvzucheng/Library/Python/3.9/bin/pyinstaller MRobot.py --onefile --windowed --add-data "assets:assets" --add-data "app:app" --add-data "app/tools:app/tools"
|
||||
|
||||
pyinstaller MRobot.py --onefile --windowed --add-data "assets;assets" --add-data "app;app" --add-data "app/tools;app/tools"
|
@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Includes ----------------------------------------------------------------- */
|
||||
#include <stdint.h>
|
||||
|
||||
#include "bsp/bsp.h"
|
||||
|
||||
/* Exported constants ------------------------------------------------------- */
|
||||
/* Exported macro ----------------------------------------------------------- */
|
||||
/* Exported types ----------------------------------------------------------- */
|
||||
/* Exported functions prototypes -------------------------------------------- */
|
||||
int8_t BSP_Delay(uint32_t ms);
|
||||
|
||||
int8_t BSP_Delay_Init(void);
|
||||
int8_t BSP_Delay_us(uint32_t us);
|
||||
int8_t BSP_Delay_ms(uint32_t ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
@ -1,2 +0,0 @@
|
||||
uart,要求开启dma和中断
|
||||
can,要求开启can的中断
|
|
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/__pycache__/about_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/ai_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/data_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/function_fit_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/home_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/main_window.cpython-39.pyc
Normal file
BIN
app/__pycache__/mini_tool_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/part_library_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/serial_terminal_interface.cpython-39.pyc
Normal file
65
app/about_interface.py
Normal file
@ -0,0 +1,65 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition, SubtitleLabel
|
||||
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from app.tools.check_update import check_update
|
||||
|
||||
__version__ = "1.0.3"
|
||||
|
||||
class AboutInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("aboutInterface")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setAlignment(Qt.AlignTop)
|
||||
layout.setContentsMargins(20, 30, 20, 20) # 添加边距
|
||||
|
||||
title = SubtitleLabel("MRobot 帮助页面", self)
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
# 添加空间隔
|
||||
layout.addSpacing(10)
|
||||
|
||||
card = PrimaryPushSettingCard(
|
||||
text="检查更新",
|
||||
icon=FluentIcon.DOWNLOAD,
|
||||
title="更新",
|
||||
content=f"MRobot_Toolbox 当前版本:{__version__}",
|
||||
)
|
||||
card.clicked.connect(self.on_check_update_clicked)
|
||||
layout.addWidget(card)
|
||||
|
||||
def on_check_update_clicked(self):
|
||||
try:
|
||||
latest = check_update(__version__)
|
||||
if latest:
|
||||
# 直接用浏览器打开下载链接
|
||||
QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest"))
|
||||
InfoBar.success(
|
||||
title="发现新版本",
|
||||
content=f"检测到新版本:{latest},已为你打开下载页面。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=5000
|
||||
)
|
||||
elif latest is None:
|
||||
InfoBar.info(
|
||||
title="已是最新版本",
|
||||
content="当前已是最新版本,无需更新。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=3000
|
||||
)
|
||||
except Exception:
|
||||
InfoBar.error(
|
||||
title="检查更新失败",
|
||||
content="无法获取最新版本,请检查网络连接。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=4000
|
||||
)
|
182
app/ai_interface.py
Normal file
@ -0,0 +1,182 @@
|
||||
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://154.37.215.220: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
|
978
app/data_interface.py
Normal file
@ -0,0 +1,978 @@
|
||||
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
|
||||
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):
|
||||
"""
|
||||
自动保留所有 /* USER XXX BEGIN */ ... /* USER XXX END */ 区域内容。
|
||||
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 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)
|
||||
|
||||
# --- 页面1:工程路径选择 ---
|
||||
self.select_widget = QWidget()
|
||||
outer_layout = QVBoxLayout(self.select_widget)
|
||||
outer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
outer_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)
|
||||
self.choose_btn.clicked.connect(self.choose_project_folder)
|
||||
content_layout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# 更新代码库按钮
|
||||
self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
|
||||
self.update_template_btn.setFixedWidth(200)
|
||||
self.update_template_btn.clicked.connect(self.update_user_template)
|
||||
content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
content_layout.addSpacing(10)
|
||||
content_layout.addStretch()
|
||||
|
||||
|
||||
outer_layout.addLayout(content_layout)
|
||||
outer_layout.addStretch()
|
||||
|
||||
self.stacked_layout.addWidget(self.select_widget)
|
||||
|
||||
|
||||
# --- 页面2:主配置页面 ---
|
||||
self.config_widget = QWidget()
|
||||
main_layout = QVBoxLayout(self.config_widget)
|
||||
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)
|
||||
self.back_btn.clicked.connect(self.back_to_select)
|
||||
info_layout.addWidget(self.back_btn) # 返回按钮放最左
|
||||
self.project_name_label = StrongBodyLabel()
|
||||
self.project_path_label = BodyLabel()
|
||||
self.ioc_file_label = BodyLabel()
|
||||
self.freertos_label = BodyLabel()
|
||||
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.env_btn = PushButton("自动环境配置")
|
||||
self.env_btn.setFixedWidth(200)
|
||||
self.env_btn.setToolTip("自动检测并配置常用开发环境(功能开发中)")
|
||||
self.env_btn.clicked.connect(self.auto_env_config)
|
||||
btn_group.addWidget(self.env_btn)
|
||||
# FreeRTOS相关按钮
|
||||
self.freertos_task_btn = PushButton("自动生成FreeRTOS任务")
|
||||
self.freertos_task_btn.setFixedWidth(200)
|
||||
self.freertos_task_btn.setToolTip("自动在 freertos.c 中插入任务创建代码")
|
||||
self.freertos_task_btn.clicked.connect(self.on_freertos_task_btn_clicked)
|
||||
btn_group.addWidget(self.freertos_task_btn)
|
||||
self.task_code_btn = PushButton("配置并生成任务代码")
|
||||
self.task_code_btn.setFixedWidth(200)
|
||||
self.task_code_btn.setToolTip("配置任务参数并一键生成任务代码文件")
|
||||
self.task_code_btn.clicked.connect(self.on_task_code_btn_clicked)
|
||||
btn_group.addWidget(self.task_code_btn)
|
||||
self.generate_btn = PushButton(FluentIcon.CODE, "生成代码")
|
||||
self.generate_btn.setFixedWidth(200)
|
||||
self.generate_btn.setToolTip("将选中的用户模块代码复制到工程 User 目录")
|
||||
self.generate_btn.clicked.connect(self.generate_code)
|
||||
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)
|
||||
self.stacked_layout.addWidget(self.config_widget)
|
||||
self.file_tree.itemChanged.connect(self.on_tree_item_changed)
|
||||
|
||||
def auto_env_config(self):
|
||||
InfoBar.info(
|
||||
title="敬请期待",
|
||||
content="自动环境配置功能暂未实现,等待后续更新。",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
|
||||
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.project_name_label.setText(f"项目名称: {self.project_name}")
|
||||
self.project_path_label.setText(f"项目路径: {self.project_path}")
|
||||
# self.ioc_file_label.setText(f"IOC 文件: {self.ioc_file}")
|
||||
try:
|
||||
ioc = IocConfig(self.ioc_file)
|
||||
self.freertos_enabled = ioc.is_freertos_enabled() # 记录状态
|
||||
freertos_status = "已启用" if self.freertos_enabled else "未启用"
|
||||
self.freertos_label.setText(f"FreeRTOS: {freertos_status}")
|
||||
# self.freertos_task_btn.setEnabled(self.freertos_enabled)
|
||||
except Exception as e:
|
||||
self.freertos_label.setText(f"IOC解析失败: {e}")
|
||||
self.freertos_task_btn.hide()
|
||||
self.freertos_enabled = False
|
||||
self.show_user_code_files()
|
||||
self.stacked_layout.setCurrentWidget(self.config_widget)
|
||||
|
||||
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.select_widget)
|
||||
|
||||
def update_user_template(self):
|
||||
url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
|
||||
local_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../assets/User_code")
|
||||
try:
|
||||
resp = requests.get(url, timeout=30)
|
||||
resp.raise_for_status()
|
||||
z = zipfile.ZipFile(io.BytesIO(resp.content))
|
||||
if os.path.exists(local_dir):
|
||||
shutil.rmtree(local_dir)
|
||||
for member in z.namelist():
|
||||
rel_path = os.path.relpath(member, z.namelist()[0])
|
||||
if rel_path == ".":
|
||||
continue
|
||||
target_path = os.path.join(local_dir, rel_path)
|
||||
if member.endswith('/'):
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||
with open(target_path, "wb") as f:
|
||||
f.write(z.read(member))
|
||||
InfoBar.success(
|
||||
title="更新成功",
|
||||
content="用户模板已更新到最新版本!",
|
||||
parent=self,
|
||||
duration=2000
|
||||
)
|
||||
except Exception as e:
|
||||
InfoBar.error(
|
||||
title="更新失败",
|
||||
content=f"用户模板更新失败: {e}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
def show_user_code_files(self):
|
||||
self.file_tree.clear()
|
||||
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../assets/User_code")
|
||||
user_dir = os.path.join(self.project_path, "User")
|
||||
sub_dirs = ["bsp", "component", "device", "module"]
|
||||
|
||||
# 读取所有 describe.csv 和 dependencies.csv
|
||||
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
|
||||
# describe
|
||||
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()
|
||||
# dependencies
|
||||
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
|
||||
|
||||
self.file_tree.setHeaderLabels(["模块名", "描述"])
|
||||
self.file_tree.setSelectionMode(self.file_tree.ExtendedSelection)
|
||||
self.file_tree.header().setSectionResizeMode(0, QHeaderView.Interactive)
|
||||
self.file_tree.header().setSectionResizeMode(1, QHeaderView.Interactive)
|
||||
self.file_tree.setBorderRadius(8)
|
||||
self.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, ""])
|
||||
self.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) # 存模块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)
|
||||
self.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.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.file_tree.invisibleRootItem()
|
||||
for i in range(root.childCount()):
|
||||
_traverse(root.child(i))
|
||||
return files
|
||||
|
||||
def generate_code(self):
|
||||
import shutil
|
||||
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../assets/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)
|
||||
shutil.copy2(src_c, dst_c)
|
||||
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)
|
||||
shutil.copy2(src_h, dst_h)
|
||||
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 = []
|
||||
|
||||
# 1. 添加 #include "task/user_task.h"
|
||||
include_line = '#include "task/user_task.h"'
|
||||
if include_line not in code:
|
||||
# 只插入到 USER CODE BEGIN Includes 区域
|
||||
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。")
|
||||
|
||||
# 2. 在 /* USER CODE BEGIN RTOS_THREADS */ 区域添加 osThreadNew(Task_Init, NULL, &attr_init);
|
||||
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 */ 区域,无法插入任务创建代码。")
|
||||
|
||||
# 3. 清空 StartDefaultTask 的 USER CODE 区域,只保留 osThreadTerminate
|
||||
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
|
||||
)
|
||||
except Exception as e:
|
||||
InfoBar.error(
|
||||
title="生成失败",
|
||||
content=f"任务代码生成失败: {e}",
|
||||
parent=self,
|
||||
duration=3000
|
||||
)
|
||||
|
||||
|
||||
def generate_task_code(self, task_list):
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
template_dir = os.path.join(base_dir, "../assets/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)
|
||||
|
||||
# ----------- 生成 user_task.h -----------
|
||||
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)
|
||||
|
||||
# ----------- 生成 user_task.c -----------
|
||||
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)
|
||||
|
||||
# ----------- 生成 init.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)
|
||||
|
||||
# ----------- 生成 task.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 -----------
|
||||
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)
|
||||
|
||||
|
||||
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
|
260
app/function_fit_interface.py
Normal file
@ -0,0 +1,260 @@
|
||||
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
|
||||
)
|
65
app/home_interface.py
Normal file
@ -0,0 +1,65 @@
|
||||
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)
|
112
app/main_window.py
Normal file
@ -0,0 +1,112 @@
|
||||
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 .about_interface import AboutInterface
|
||||
import base64
|
||||
|
||||
|
||||
class MainWindow(FluentWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.initWindow()
|
||||
self.initInterface()
|
||||
self.initNavigation()
|
||||
|
||||
# 检查更新
|
||||
# checkUpdate(self, flag=True)
|
||||
# checkAnnouncement(self) # 检查公告
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def initNavigation(self):
|
||||
self.addSubInterface(self.homeInterface, FIF.HOME, self.tr('主页'))
|
||||
self.addSubInterface(self.dataInterface, 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.navigationInterface.addWidget(
|
||||
# 'startGameButton',
|
||||
# NavigationBarPushButton(FIF.PLAY, '启动游戏', isSelectable=False),
|
||||
# self.startGame,
|
||||
# NavigationItemPosition.BOTTOM)
|
||||
|
||||
# self.navigationInterface.addWidget(
|
||||
# 'themeButton',
|
||||
# NavigationBarPushButton(FIF.BRUSH, '主题', isSelectable=False),
|
||||
# lambda: toggleTheme(lazy=True),
|
||||
# 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
|
||||
)
|
||||
|
||||
# self.navigationInterface.addWidget(
|
||||
# 'avatar',
|
||||
# NavigationBarPushButton(FIF.HEART, '赞赏', isSelectable=False),
|
||||
# lambda: MessageBoxSupport(
|
||||
# '支持作者🥰',
|
||||
# '此程序为免费开源项目,如果你付了钱请立刻退款\n如果喜欢本项目,可以微信赞赏送作者一杯咖啡☕\n您的支持就是作者开发和维护项目的动力🚀',
|
||||
# './assets/app/images/sponsor.jpg',
|
||||
# self
|
||||
# ).exec(),
|
||||
# NavigationItemPosition.BOTTOM
|
||||
# )
|
||||
|
||||
# self.addSubInterface(self.settingInterface, FIF.SETTING, self.tr('设置'), position=NavigationItemPosition.BOTTOM)
|
||||
|
||||
# self.splashScreen.finish() # 结束启动画面
|
||||
# self.themeListener = checkThemeChange(self)
|
||||
|
||||
# if not cfg.get_value(base64.b64decode("YXV0b191cGRhdGU=").decode("utf-8")):
|
||||
# disclaimer(self)
|
||||
|
||||
|
||||
# main_window.py 只需修改关闭事件
|
||||
def closeEvent(self, e):
|
||||
# if self.themeListener and self.themeListener.isRunning():
|
||||
# self.themeListener.terminate()
|
||||
# self.themeListener.deleteLater()
|
||||
super().closeEvent(e)
|
104
app/mini_tool_interface.py
Normal file
@ -0,0 +1,104 @@
|
||||
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())
|
||||
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")
|
205
app/part_library_interface.py
Normal file
@ -0,0 +1,205 @@
|
||||
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://154.37.215.220: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])
|
167
app/serial_terminal_interface.py
Normal file
@ -0,0 +1,167 @@
|
||||
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()
|
BIN
app/tools/__pycache__/check_update.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/part_download.cpython-39.pyc
Normal file
14
app/tools/check_update.py
Normal file
@ -0,0 +1,14 @@
|
||||
import requests
|
||||
from packaging.version import parse as vparse
|
||||
|
||||
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 请求失败")
|
45
app/tools/part_download.py
Normal file
@ -0,0 +1,45 @@
|
||||
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)
|
34
assets/User_code/bsp/delay.h
Normal file
@ -0,0 +1,34 @@
|
||||
/* Includes ----------------------------------------------------------------- */
|
||||
#include "bsp\delay.h"
|
||||
|
||||
#include <cmsis_os2.h>
|
||||
#include <main.h>
|
||||
|
||||
/* Private define ----------------------------------------------------------- */
|
||||
/* Private macro ------------------------------------------------------------ */
|
||||
/* Private typedef ---------------------------------------------------------- */
|
||||
/* Private variables -------------------------------------------------------- */
|
||||
/* Private function -------------------------------------------------------- */
|
||||
/* Exported functions ------------------------------------------------------- */
|
||||
int8_t BSP_Delay(uint32_t ms) {
|
||||
uint32_t tick_period = 1000u / osKernelGetTickFreq();
|
||||
uint32_t ticks = ms / tick_period;
|
||||
|
||||
switch (osKernelGetState()) {
|
||||
case osKernelError:
|
||||
case osKernelReserved:
|
||||
case osKernelLocked:
|
||||
case osKernelSuspended:
|
||||
return BSP_ERR;
|
||||
|
||||
case osKernelRunning:
|
||||
osDelay(ticks ? ticks : 1);
|
||||
break;
|
||||
|
||||
case osKernelInactive:
|
||||
case osKernelReady:
|
||||
HAL_Delay(ms);
|
||||
break;
|
||||
}
|
||||
return BSP_OK;
|
||||
}
|
3
assets/User_code/bsp/describe.csv
Normal file
@ -0,0 +1,3 @@
|
||||
uart,要求开启dma和中断
|
||||
can,要求开启can的中断
|
||||
delay,暂时只有delay_ms函数
|
|
0
assets/User_code/task/.gitkeep
Normal file
@ -24,9 +24,9 @@
|
||||
*/
|
||||
void Task_Init(void *argument) {
|
||||
(void)argument; /* 未使用argument,消除警告 */
|
||||
/* USER CODE BEGIN Task_Init */
|
||||
/* USER CODE INIT BEGIN */
|
||||
|
||||
/* USER CODE END Task_Init */
|
||||
/* USER CODE INIT END */
|
||||
osKernelLock(); /* 锁定内核,防止任务切换 */
|
||||
|
||||
/* 创建任务线程 */
|
@ -5,17 +5,17 @@
|
||||
|
||||
/* Includes ----------------------------------------------------------------- */
|
||||
#include "task/user_task.h"
|
||||
/* USER INCLUDE BEGIN*/
|
||||
/* USER INCLUDE BEGIN */
|
||||
|
||||
/* USER INCLUDE END*/
|
||||
/* USER INCLUDE END */
|
||||
|
||||
/* Private typedef ---------------------------------------------------------- */
|
||||
/* Private define ----------------------------------------------------------- */
|
||||
/* Private macro ------------------------------------------------------------ */
|
||||
/* Private variables -------------------------------------------------------- */
|
||||
/* USER STRUCT BEGIN*/
|
||||
/* USER STRUCT BEGIN */
|
||||
|
||||
/* USER STRUCT END*/
|
||||
/* USER STRUCT END */
|
||||
|
||||
/* Private function --------------------------------------------------------- */
|
||||
/* Exported functions ------------------------------------------------------- */
|
||||
@ -28,11 +28,11 @@ void {{task_function}}(void *argument) {
|
||||
|
||||
osDelay({{task_delay}}); /* 延时一段时间再开启任务 */
|
||||
|
||||
/* USER CODE INIT BEGIN*/
|
||||
|
||||
/* USER CODE INIT END*/
|
||||
|
||||
uint32_t tick = osKernelGetTickCount(); /* 控制任务运行频率的计时 */
|
||||
/* USER CODE INIT BEGIN */
|
||||
|
||||
/* USER CODE INIT END */
|
||||
|
||||
while (1) {
|
||||
tick += delay_tick; /* 计算下一个唤醒时刻 */
|
||||
/* USER CODE BEGIN */
|
||||
@ -43,15 +43,14 @@ void {{task_function}}(void *argument) {
|
||||
{% else %}
|
||||
osDelay({{task_delay}}); /* 延时一段时间再开启任务 */
|
||||
|
||||
/* USER CODE INIT BEGIN*/
|
||||
/* USER CODE INIT BEGIN */
|
||||
|
||||
/* USER CODE INIT END*/
|
||||
/* USER CODE INIT END */
|
||||
|
||||
while (1) {
|
||||
/* USER CODE BEGIN */
|
||||
|
||||
/* USER CODE END */
|
||||
osDelay(1); /* 默认1ms延时,防止死循环卡死CPU */
|
||||
}
|
||||
{% endif %}
|
||||
}
|
||||
}
|
@ -10,4 +10,3 @@ const osThreadAttr_t attr_init = {
|
||||
|
||||
/* User_task */
|
||||
{{task_attr_definitions}}
|
||||
|
@ -78,4 +78,3 @@ void Task_Init(void *argument);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 562 KiB After Width: | Height: | Size: 562 KiB |