mirror of
https://github.com/goldenfishs/MRobot.git
synced 2025-07-27 16:59:01 +08:00
优化了ai个零件库
This commit is contained in:
parent
9fc6b4577a
commit
47e0b8419f
@ -8,7 +8,6 @@ from PyQt5.QtWidgets import QApplication
|
|||||||
from app.main_window import MainWindow
|
from app.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 启用 DPI 缩放
|
# 启用 DPI 缩放
|
||||||
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
|
||||||
|
21
ai.py
Normal file
21
ai.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
url = "http://154.37.215.220:11434/api/generate"
|
||||||
|
payload = {
|
||||||
|
"model": "qwen3:0.6b",
|
||||||
|
"prompt": "你好,介绍一下你自己"
|
||||||
|
}
|
||||||
|
response = requests.post(url, json=payload, stream=True)
|
||||||
|
|
||||||
|
for line in response.iter_lines():
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
data = json.loads(line.decode('utf-8'))
|
||||||
|
# 只输出 response 字段内容
|
||||||
|
print(data.get("response", ""), end="", flush=True)
|
||||||
|
# 如果 done 为 True,则换行
|
||||||
|
if data.get("done", False):
|
||||||
|
print()
|
||||||
|
except Exception as e:
|
||||||
|
pass # 忽略解析异常
|
@ -26,6 +26,7 @@ class AboutInterface(QWidget):
|
|||||||
layout.addWidget(card)
|
layout.addWidget(card)
|
||||||
|
|
||||||
def on_check_update_clicked(self):
|
def on_check_update_clicked(self):
|
||||||
|
try:
|
||||||
latest = check_update(__version__)
|
latest = check_update(__version__)
|
||||||
if latest:
|
if latest:
|
||||||
InfoBar.success(
|
InfoBar.success(
|
||||||
@ -35,7 +36,7 @@ class AboutInterface(QWidget):
|
|||||||
position=InfoBarPosition.TOP,
|
position=InfoBarPosition.TOP,
|
||||||
duration=5000
|
duration=5000
|
||||||
)
|
)
|
||||||
else:
|
elif latest is None:
|
||||||
InfoBar.info(
|
InfoBar.info(
|
||||||
title="已是最新版本",
|
title="已是最新版本",
|
||||||
content="当前已是最新版本,无需更新。",
|
content="当前已是最新版本,无需更新。",
|
||||||
@ -43,3 +44,11 @@ class AboutInterface(QWidget):
|
|||||||
position=InfoBarPosition.TOP,
|
position=InfoBarPosition.TOP,
|
||||||
duration=3000
|
duration=3000
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
InfoBar.error(
|
||||||
|
title="检查更新失败",
|
||||||
|
content="无法获取最新版本,请检查网络连接。",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=4000
|
||||||
|
)
|
182
app/ai_interface.py
Normal file
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
|
@ -3,6 +3,7 @@ from PyQt5.QtCore import Qt
|
|||||||
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
|
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
|
||||||
|
|
||||||
from .function_fit_interface import FunctionFitInterface
|
from .function_fit_interface import FunctionFitInterface
|
||||||
|
from .ai_interface import AIInterface
|
||||||
|
|
||||||
class MiniToolInterface(QWidget):
|
class MiniToolInterface(QWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@ -26,14 +27,22 @@ class MiniToolInterface(QWidget):
|
|||||||
mainLayout = QVBoxLayout(self.mainPage)
|
mainLayout = QVBoxLayout(self.mainPage)
|
||||||
mainLayout.setAlignment(Qt.AlignTop) # 卡片靠顶部
|
mainLayout.setAlignment(Qt.AlignTop) # 卡片靠顶部
|
||||||
self.card = PushSettingCard(
|
self.card = PushSettingCard(
|
||||||
text="▶启动",
|
text="▶ 启动",
|
||||||
icon=FluentIcon.UNIT,
|
icon=FluentIcon.UNIT,
|
||||||
title="曲线拟合工具",
|
title="曲线拟合工具",
|
||||||
content="简单的曲线拟合工具,支持多种函数类型",
|
content="简单的曲线拟合工具,支持多种函数类型",
|
||||||
)
|
)
|
||||||
mainLayout.addWidget(self.card)
|
mainLayout.addWidget(self.card)
|
||||||
self.mainPage.setLayout(mainLayout)
|
|
||||||
|
|
||||||
|
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.addSubInterface(self.mainPage, "mainPage", "工具箱主页")
|
||||||
|
|
||||||
@ -80,3 +89,16 @@ class MiniToolInterface(QWidget):
|
|||||||
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
|
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
|
||||||
self.stackedWidget.setCurrentWidget(fit_page)
|
self.stackedWidget.setCurrentWidget(fit_page)
|
||||||
self.tabBar.setCurrentTab("fitPage")
|
self.tabBar.setCurrentTab("fitPage")
|
||||||
|
|
||||||
|
def open_ai_tab(self):
|
||||||
|
# 检查是否已存在标签页,避免重复添加
|
||||||
|
for i in range(self.stackedWidget.count()):
|
||||||
|
widget = self.stackedWidget.widget(i)
|
||||||
|
if widget.objectName() == "aiPage":
|
||||||
|
self.stackedWidget.setCurrentWidget(widget)
|
||||||
|
self.tabBar.setCurrentTab("aiPage")
|
||||||
|
return
|
||||||
|
ai_page = AIInterface(self)
|
||||||
|
self.addSubInterface(ai_page, "aiPage", "AI问答")
|
||||||
|
self.stackedWidget.setCurrentWidget(ai_page)
|
||||||
|
self.tabBar.setCurrentTab("aiPage")
|
||||||
|
@ -1,51 +1,12 @@
|
|||||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
||||||
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, PushButton, TreeWidget, ProgressBar, Dialog, InfoBar, InfoBarPosition, FluentIcon
|
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, PushButton, TreeWidget, ProgressBar, Dialog, InfoBar, InfoBarPosition, FluentIcon, ProgressRing, Dialog
|
||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
|
from .tools.part_download import DownloadThread # 新增导入
|
||||||
|
|
||||||
from urllib.parse import quote
|
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)
|
|
||||||
|
|
||||||
class PartLibraryInterface(QWidget):
|
class PartLibraryInterface(QWidget):
|
||||||
SERVER_URL = "http://154.37.215.220:5000"
|
SERVER_URL = "http://154.37.215.220:5000"
|
||||||
SECRET_KEY = "MRobot_Download"
|
SECRET_KEY = "MRobot_Download"
|
||||||
@ -151,41 +112,64 @@ class PartLibraryInterface(QWidget):
|
|||||||
def download_selected_files(self):
|
def download_selected_files(self):
|
||||||
files = self.get_checked_files()
|
files = self.get_checked_files()
|
||||||
if not files:
|
if not files:
|
||||||
InfoBar.info(
|
dialog = Dialog(
|
||||||
title="提示",
|
title="温馨提示",
|
||||||
content="请先勾选要下载的文件。",
|
content="请先勾选需要下载的文件。",
|
||||||
parent=self,
|
|
||||||
position=InfoBarPosition.TOP,
|
|
||||||
duration=2000
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.progress_dialog = Dialog(
|
|
||||||
title="正在下载",
|
|
||||||
content="正在下载选中文件,请稍候...",
|
|
||||||
parent=self
|
parent=self
|
||||||
)
|
)
|
||||||
self.progress_bar = ProgressBar()
|
dialog.yesButton.setText("知道啦")
|
||||||
self.progress_bar.setValue(0)
|
dialog.cancelButton.hide()
|
||||||
self.progress_dialog.textLayout.addWidget(self.progress_bar)
|
dialog.exec()
|
||||||
self.progress_dialog.show()
|
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(
|
self.download_thread = DownloadThread(
|
||||||
files, self.SERVER_URL, self.SECRET_KEY, self.LOCAL_LIB_DIR
|
files, self.SERVER_URL, self.SECRET_KEY, self.LOCAL_LIB_DIR
|
||||||
)
|
)
|
||||||
self.download_thread.progressChanged.connect(self.progress_bar.setValue)
|
self.download_thread.progressChanged.connect(self.progress_ring.setValue)
|
||||||
self.download_thread.finished.connect(self.on_download_finished)
|
self.download_thread.finished.connect(self.on_download_finished)
|
||||||
self.download_thread.finished.connect(self.download_thread.deleteLater)
|
self.download_thread.finished.connect(self.download_thread.deleteLater)
|
||||||
self.download_thread.start()
|
self.download_thread.start()
|
||||||
|
|
||||||
def on_download_finished(self, success, fail):
|
def stop_download(self):
|
||||||
self.progress_dialog.close()
|
if hasattr(self, "download_thread") and self.download_thread.isRunning():
|
||||||
msg = f"成功下载: {len(success)} 个文件\n失败: {len(fail)} 个文件"
|
self.download_thread.terminate()
|
||||||
dialog = Dialog(
|
self.download_thread.wait()
|
||||||
title="下载结果",
|
self.info_bar.close()
|
||||||
content=msg,
|
InfoBar.warning(
|
||||||
parent=self
|
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("打开文件夹")
|
open_btn = PushButton("打开文件夹")
|
||||||
def open_folder():
|
def open_folder():
|
||||||
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
||||||
@ -196,10 +180,18 @@ class PartLibraryInterface(QWidget):
|
|||||||
subprocess.call(["explorer", folder])
|
subprocess.call(["explorer", folder])
|
||||||
else:
|
else:
|
||||||
subprocess.call(["xdg-open", folder])
|
subprocess.call(["xdg-open", folder])
|
||||||
dialog.close()
|
|
||||||
|
# 展示成功消息条,自动消失
|
||||||
|
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)
|
open_btn.clicked.connect(open_folder)
|
||||||
dialog.textLayout.addWidget(open_btn)
|
self.result_bar.show()
|
||||||
dialog.exec()
|
|
||||||
|
|
||||||
def open_local_lib(self):
|
def open_local_lib(self):
|
||||||
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
||||||
|
@ -2,19 +2,13 @@ import requests
|
|||||||
from packaging.version import parse as vparse
|
from packaging.version import parse as vparse
|
||||||
|
|
||||||
def check_update(local_version, repo="goldenfishs/MRobot"):
|
def check_update(local_version, repo="goldenfishs/MRobot"):
|
||||||
"""
|
|
||||||
检查 GitHub 上是否有新版本
|
|
||||||
:param local_version: 当前版本号字符串,如 "1.0.2"
|
|
||||||
:param repo: 仓库名,格式 "用户名/仓库名"
|
|
||||||
:return: 最新版本号字符串(如果有新版本),否则 None
|
|
||||||
"""
|
|
||||||
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||||
try:
|
|
||||||
resp = requests.get(url, timeout=5)
|
resp = requests.get(url, timeout=5)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
latest = resp.json()["tag_name"].lstrip("v")
|
latest = resp.json()["tag_name"].lstrip("v")
|
||||||
if vparse(latest) > vparse(local_version):
|
if vparse(latest) > vparse(local_version):
|
||||||
return latest
|
return latest
|
||||||
except Exception as e:
|
else:
|
||||||
print(f"检查更新失败: {e}")
|
|
||||||
return None
|
return None
|
||||||
|
else:
|
||||||
|
raise RuntimeError("GitHub API 请求失败")
|
45
app/tools/part_download.py
Normal file
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)
|
114
app/tools/task_config.py
Normal file
114
app/tools/task_config.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout
|
||||||
|
from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, InfoBar, InfoBarPosition, LineEdit, CheckBox
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
|
||||||
|
class TaskConfigDialog(QDialog):
|
||||||
|
def __init__(self, parent=None, config_path=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("任务配置")
|
||||||
|
self.resize(900, 480)
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(32, 32, 32, 32)
|
||||||
|
layout.setSpacing(18)
|
||||||
|
|
||||||
|
layout.addWidget(TitleLabel("FreeRTOS 任务配置"))
|
||||||
|
layout.addWidget(BodyLabel("请添加并配置您的任务参数,支持频率控制与描述。"))
|
||||||
|
|
||||||
|
self.table = TableWidget(self)
|
||||||
|
self.table.setColumnCount(6)
|
||||||
|
self.table.setHorizontalHeaderLabels(["任务名称", "运行频率", "初始化延迟", "堆栈大小", "任务描述", "频率控制"])
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(0, self.table.horizontalHeader().Stretch)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(1, self.table.horizontalHeader().ResizeToContents)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(2, self.table.horizontalHeader().ResizeToContents)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(3, self.table.horizontalHeader().ResizeToContents)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(4, self.table.horizontalHeader().Stretch)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(5, self.table.horizontalHeader().ResizeToContents)
|
||||||
|
self.table.setMinimumHeight(260)
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
self.add_btn = PushButton("添加任务")
|
||||||
|
self.del_btn = PushButton("删除选中")
|
||||||
|
self.ok_btn = PushButton("生成")
|
||||||
|
self.cancel_btn = PushButton("取消")
|
||||||
|
btn_layout.addWidget(self.add_btn)
|
||||||
|
btn_layout.addWidget(self.del_btn)
|
||||||
|
btn_layout.addStretch()
|
||||||
|
btn_layout.addWidget(self.ok_btn)
|
||||||
|
btn_layout.addWidget(self.cancel_btn)
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
self.add_btn.clicked.connect(self.add_row)
|
||||||
|
self.del_btn.clicked.connect(self.del_row)
|
||||||
|
self.ok_btn.clicked.connect(self.accept)
|
||||||
|
self.cancel_btn.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
# 自动读取配置文件
|
||||||
|
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.add_row(t)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_row(self, task=None):
|
||||||
|
row = self.table.rowCount()
|
||||||
|
self.table.insertRow(row)
|
||||||
|
name = LineEdit(task.get("name", f"Task{row+1}") if task else f"Task{row+1}")
|
||||||
|
freq = SpinBox()
|
||||||
|
freq.setRange(1, 10000)
|
||||||
|
freq.setValue(task.get("frequency", 500) if task else 500)
|
||||||
|
delay = SpinBox()
|
||||||
|
delay.setRange(0, 10000)
|
||||||
|
delay.setValue(task.get("delay", 0) if task else 0)
|
||||||
|
stack = SpinBox()
|
||||||
|
stack.setRange(128, 8192)
|
||||||
|
stack.setSingleStep(128)
|
||||||
|
stack.setValue(task.get("stack", 256) if task else 256)
|
||||||
|
desc = LineEdit(task.get("description", "") if task else "请填写任务描述")
|
||||||
|
freq_ctrl = CheckBox("启用")
|
||||||
|
freq_ctrl.setChecked(task.get("freq_control", True) if task else True)
|
||||||
|
|
||||||
|
self.table.setCellWidget(row, 0, name)
|
||||||
|
self.table.setCellWidget(row, 1, freq)
|
||||||
|
self.table.setCellWidget(row, 2, delay)
|
||||||
|
self.table.setCellWidget(row, 3, stack)
|
||||||
|
self.table.setCellWidget(row, 4, desc)
|
||||||
|
self.table.setCellWidget(row, 5, freq_ctrl)
|
||||||
|
|
||||||
|
def del_row(self):
|
||||||
|
selected = self.table.selectedItems()
|
||||||
|
if selected:
|
||||||
|
rows = set(item.row() for item in selected)
|
||||||
|
for row in sorted(rows, reverse=True):
|
||||||
|
self.table.removeRow(row)
|
||||||
|
|
||||||
|
def get_tasks(self):
|
||||||
|
tasks = []
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
name = self.table.cellWidget(row, 0).text().strip()
|
||||||
|
freq = self.table.cellWidget(row, 1).value()
|
||||||
|
delay = self.table.cellWidget(row, 2).value()
|
||||||
|
stack = self.table.cellWidget(row, 3).value()
|
||||||
|
desc = self.table.cellWidget(row, 4).text().strip()
|
||||||
|
freq_ctrl = self.table.cellWidget(row, 5).isChecked()
|
||||||
|
# 校验 stack 必须为 128*2^n
|
||||||
|
if stack < 128 or (stack & (stack - 1)) != 0 or stack % 128 != 0:
|
||||||
|
raise ValueError(f"第{row+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
|
BIN
mech_lib/.DS_Store
vendored
Normal file
BIN
mech_lib/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Magnet.SLDPRT
Normal file
BIN
mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Magnet.SLDPRT
Normal file
Binary file not shown.
BIN
mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Prop Endcap.SLDPRT
Normal file
BIN
mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Prop Endcap.SLDPRT
Normal file
Binary file not shown.
BIN
mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Windings.SLDPRT
Normal file
BIN
mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Windings.SLDPRT
Normal file
Binary file not shown.
BIN
mech_lib/4.大疆电池/TB47_外购件_01.SLDPRT
Normal file
BIN
mech_lib/4.大疆电池/TB47_外购件_01.SLDPRT
Normal file
Binary file not shown.
BIN
mech_lib/4.大疆电池/TB47电池架_外购件_01.SLDPRT
Normal file
BIN
mech_lib/4.大疆电池/TB47电池架_外购件_01.SLDPRT
Normal file
Binary file not shown.
BIN
mech_lib/4.大疆电池/TB48电池架_外购件_01.SLDPRT
Normal file
BIN
mech_lib/4.大疆电池/TB48电池架_外购件_01.SLDPRT
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user