diff --git a/.DS_Store b/.DS_Store
index 292ebfd..f438c45 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/MRobot.py b/MRobot.py
index 038f3e8..fcc90cf 100644
--- a/MRobot.py
+++ b/MRobot.py
@@ -8,7 +8,6 @@ from PyQt5.QtWidgets import QApplication
from app.main_window import MainWindow
-
# 启用 DPI 缩放
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
diff --git a/ai.py b/ai.py
new file mode 100644
index 0000000..4cc0dd6
--- /dev/null
+++ b/ai.py
@@ -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 # 忽略解析异常
\ No newline at end of file
diff --git a/app/about_interface.py b/app/about_interface.py
index 407787d..a8a6792 100644
--- a/app/about_interface.py
+++ b/app/about_interface.py
@@ -26,20 +26,29 @@ class AboutInterface(QWidget):
layout.addWidget(card)
def on_check_update_clicked(self):
- latest = check_update(__version__)
- if latest:
- InfoBar.success(
- title="发现新版本",
- content=f"检测到新版本:{latest},请前往官网或仓库下载更新。",
+ try:
+ latest = check_update(__version__)
+ if 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=5000
- )
- else:
- InfoBar.info(
- title="已是最新版本",
- content="当前已是最新版本,无需更新。",
- parent=self,
- position=InfoBarPosition.TOP,
- duration=3000
+ duration=4000
)
\ No newline at end of file
diff --git a/app/ai_interface.py b/app/ai_interface.py
new file mode 100644
index 0000000..6149ea3
--- /dev/null
+++ b/app/ai_interface.py
@@ -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(
+ "MRobot: 欢迎使用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"{sender}: {message}")
+ else:
+ self.chat_display.append(f"{sender}: ")
+ 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
diff --git a/app/mini_tool_interface.py b/app/mini_tool_interface.py
index aaa3856..8e4faea 100644
--- a/app/mini_tool_interface.py
+++ b/app/mini_tool_interface.py
@@ -3,6 +3,7 @@ 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):
@@ -26,14 +27,22 @@ class MiniToolInterface(QWidget):
mainLayout = QVBoxLayout(self.mainPage)
mainLayout.setAlignment(Qt.AlignTop) # 卡片靠顶部
self.card = PushSettingCard(
- text="▶启动",
+ text="▶ 启动",
icon=FluentIcon.UNIT,
title="曲线拟合工具",
content="简单的曲线拟合工具,支持多种函数类型",
)
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", "工具箱主页")
@@ -79,4 +88,17 @@ class MiniToolInterface(QWidget):
fit_page = FunctionFitInterface(self)
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
self.stackedWidget.setCurrentWidget(fit_page)
- self.tabBar.setCurrentTab("fitPage")
\ No newline at end of file
+ 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")
diff --git a/app/part_library_interface.py b/app/part_library_interface.py
index ecad14d..1279d8c 100644
--- a/app/part_library_interface.py
+++ b/app/part_library_interface.py
@@ -1,51 +1,12 @@
-from PyQt5.QtCore import Qt, QThread, pyqtSignal
+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
+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 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):
SERVER_URL = "http://154.37.215.220:5000"
SECRET_KEY = "MRobot_Download"
@@ -151,41 +112,64 @@ class PartLibraryInterface(QWidget):
def download_selected_files(self):
files = self.get_checked_files()
if not files:
- InfoBar.info(
- title="提示",
- content="请先勾选要下载的文件。",
- parent=self,
- position=InfoBarPosition.TOP,
- duration=2000
+ dialog = Dialog(
+ title="温馨提示",
+ content="请先勾选需要下载的文件。",
+ parent=self
)
+ dialog.yesButton.setText("知道啦")
+ dialog.cancelButton.hide()
+ dialog.exec()
return
- self.progress_dialog = Dialog(
- title="正在下载",
- content="正在下载选中文件,请稍候...",
- parent=self
- )
- self.progress_bar = ProgressBar()
- self.progress_bar.setValue(0)
- self.progress_dialog.textLayout.addWidget(self.progress_bar)
- self.progress_dialog.show()
+ # 创建进度环
+ self.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_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.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.progress_dialog.close()
- msg = f"成功下载: {len(success)} 个文件\n失败: {len(fail)} 个文件"
- dialog = Dialog(
- title="下载结果",
- content=msg,
- parent=self
- )
+ self.info_bar.close()
+ msg = f"成功下载:{len(success)} 个文件,失败:{len(fail)} 个文件"
+
+ # 创建“打开文件夹”按钮
open_btn = PushButton("打开文件夹")
def open_folder():
folder = os.path.abspath(self.LOCAL_LIB_DIR)
@@ -196,10 +180,18 @@ class PartLibraryInterface(QWidget):
subprocess.call(["explorer", folder])
else:
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)
- dialog.textLayout.addWidget(open_btn)
- dialog.exec()
+ self.result_bar.show()
def open_local_lib(self):
folder = os.path.abspath(self.LOCAL_LIB_DIR)
diff --git a/app/tools/check_update.py b/app/tools/check_update.py
index 7095d6b..426f3ab 100644
--- a/app/tools/check_update.py
+++ b/app/tools/check_update.py
@@ -2,19 +2,13 @@ import requests
from packaging.version import parse as vparse
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"
- try:
- 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
- except Exception as e:
- print(f"检查更新失败: {e}")
- return None
\ No newline at end of file
+ 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 请求失败")
\ No newline at end of file
diff --git a/app/tools/part_download.py b/app/tools/part_download.py
new file mode 100644
index 0000000..1faf82c
--- /dev/null
+++ b/app/tools/part_download.py
@@ -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)
\ No newline at end of file
diff --git a/app/tools/task_config.py b/app/tools/task_config.py
new file mode 100644
index 0000000..fec9f35
--- /dev/null
+++ b/app/tools/task_config.py
@@ -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
\ No newline at end of file
diff --git a/mech_lib/.DS_Store b/mech_lib/.DS_Store
new file mode 100644
index 0000000..557d4b2
Binary files /dev/null and b/mech_lib/.DS_Store differ
diff --git a/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Magnet.SLDPRT b/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Magnet.SLDPRT
new file mode 100644
index 0000000..623f7b0
Binary files /dev/null and b/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Magnet.SLDPRT differ
diff --git a/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Prop Endcap.SLDPRT b/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Prop Endcap.SLDPRT
new file mode 100644
index 0000000..56f91e7
Binary files /dev/null and b/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Prop Endcap.SLDPRT differ
diff --git a/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Windings.SLDPRT b/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Windings.SLDPRT
new file mode 100644
index 0000000..3390fef
Binary files /dev/null and b/mech_lib/2.电机&舵机/5065BrushlessMotor/N5065 Windings.SLDPRT differ
diff --git a/mech_lib/4.大疆电池/TB47_外购件_01.SLDPRT b/mech_lib/4.大疆电池/TB47_外购件_01.SLDPRT
new file mode 100644
index 0000000..2fa0207
Binary files /dev/null and b/mech_lib/4.大疆电池/TB47_外购件_01.SLDPRT differ
diff --git a/mech_lib/4.大疆电池/TB47电池架_外购件_01.SLDPRT b/mech_lib/4.大疆电池/TB47电池架_外购件_01.SLDPRT
new file mode 100644
index 0000000..a9244ec
Binary files /dev/null and b/mech_lib/4.大疆电池/TB47电池架_外购件_01.SLDPRT differ
diff --git a/mech_lib/4.大疆电池/TB48电池架_外购件_01.SLDPRT b/mech_lib/4.大疆电池/TB48电池架_外购件_01.SLDPRT
new file mode 100644
index 0000000..beb9919
Binary files /dev/null and b/mech_lib/4.大疆电池/TB48电池架_外购件_01.SLDPRT differ