mirror of
https://github.com/goldenfishs/MRobot.git
synced 2025-08-01 21:29:01 +08:00
Compare commits
No commits in common. "7951dae760072ef599f1aadb947b0d067765d7ff" and "78661f450b7bb604e6fb0a9d91fc5279a3be48ea" have entirely different histories.
7951dae760
...
78661f450b
11
MRobot.iss
11
MRobot.iss
@ -8,11 +8,10 @@ OutputBaseFilename=MRobotInstaller
|
||||
|
||||
[Files]
|
||||
Source: "dist\MRobot.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "assets\logo\*"; DestDir: "{app}\assets\logo"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\User_code\*"; DestDir: "{app}\assets\User_code"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\mech_lib\*"; DestDir: "{app}\assets\mech_lib"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\logo\M.ico"; DestDir: "{app}\assets\logo"; Flags: ignoreversion
|
||||
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
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
||||
Name: "{userdesktop}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
||||
Name: "{group}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\img\M.ico"
|
||||
Name: "{userdesktop}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\img\M.ico"
|
@ -122,11 +122,4 @@ 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 --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"
|
||||
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
|
22
app.py
Normal file
22
app.py
Normal file
@ -0,0 +1,22 @@
|
||||
import os
|
||||
import sys
|
||||
# 将当前工作目录设置为程序所在的目录,确保无论从哪里执行,其工作目录都正确设置为程序本身的位置,避免路径错误。
|
||||
os.chdir(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False)else os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from app.main_window import MainWindow
|
||||
|
||||
# 启用 DPI 缩放
|
||||
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
|
||||
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) # 使用高 DPI 图标
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
app.setAttribute(Qt.AA_DontCreateNativeWidgetSiblings) # 避免创建原生窗口小部件的兄弟窗口
|
||||
|
||||
w = MainWindow()
|
||||
|
||||
sys.exit(app.exec_()) # 启动应用程序并进入主事件循环
|
||||
# 注意:在 PyQt5 中,exec_() 是一个阻塞调用,直到应用程序退出。
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,65 +0,0 @@
|
||||
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.2"
|
||||
|
||||
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
|
||||
)
|
@ -1,182 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
|
||||
from qfluentwidgets import TextEdit, LineEdit, PushButton, TitleLabel, SubtitleLabel, FluentIcon, InfoBar, InfoBarPosition
|
||||
import requests
|
||||
import json
|
||||
|
||||
class AIWorker(QThread):
|
||||
response_signal = pyqtSignal(str)
|
||||
done_signal = pyqtSignal()
|
||||
error_signal = pyqtSignal(str) # 新增
|
||||
|
||||
def __init__(self, prompt, parent=None):
|
||||
super().__init__(parent)
|
||||
self.prompt = prompt
|
||||
|
||||
def run(self):
|
||||
url = "http://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
|
@ -6,10 +6,8 @@ import os
|
||||
import requests
|
||||
import zipfile
|
||||
import io
|
||||
import re
|
||||
import shutil
|
||||
import yaml
|
||||
import textwrap
|
||||
from jinja2 import Template
|
||||
|
||||
class IocConfig:
|
||||
@ -67,16 +65,19 @@ class DataInterface(QWidget):
|
||||
# 主标题
|
||||
title = TitleLabel("MRobot 代码生成")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
title.setStyleSheet("font-size: 36px; font-weight: bold; color: #2d7d9a;")
|
||||
content_layout.addWidget(title)
|
||||
|
||||
# 副标题
|
||||
subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径(.ico所在的目录),然后开启代码之旅!")
|
||||
subtitle.setAlignment(Qt.AlignCenter)
|
||||
subtitle.setStyleSheet("font-size: 16px; color: #4a6fa5;")
|
||||
content_layout.addWidget(subtitle)
|
||||
|
||||
# 简要说明
|
||||
desc = BodyLabel("支持自动配置和生成任务,自主选择模块代码倒入,自动识别cubemx配置!")
|
||||
desc.setAlignment(Qt.AlignCenter)
|
||||
desc.setStyleSheet("font-size: 14px; color: #6b7b8c;")
|
||||
content_layout.addWidget(desc)
|
||||
|
||||
content_layout.addSpacing(18)
|
||||
@ -84,12 +85,14 @@ class DataInterface(QWidget):
|
||||
# 选择项目路径按钮
|
||||
self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径")
|
||||
self.choose_btn.setFixedWidth(200)
|
||||
self.choose_btn.setStyleSheet("font-size: 17px;")
|
||||
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.setStyleSheet("font-size: 17px;")
|
||||
self.update_template_btn.clicked.connect(self.update_user_template)
|
||||
content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
@ -255,7 +258,7 @@ class DataInterface(QWidget):
|
||||
|
||||
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")
|
||||
local_dir = "User_code"
|
||||
try:
|
||||
resp = requests.get(url, timeout=30)
|
||||
resp.raise_for_status()
|
||||
@ -289,7 +292,7 @@ class DataInterface(QWidget):
|
||||
|
||||
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")
|
||||
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../User_code")
|
||||
user_dir = os.path.join(self.project_path, "User")
|
||||
sub_dirs = ["bsp", "component", "device", "module"]
|
||||
|
||||
@ -409,7 +412,7 @@ class DataInterface(QWidget):
|
||||
|
||||
def generate_code(self):
|
||||
import shutil
|
||||
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../assets/User_code")
|
||||
base_dir = "User_code"
|
||||
user_dir = os.path.join(self.project_path, "User")
|
||||
copied = []
|
||||
files = self.get_checked_files()
|
||||
@ -692,7 +695,7 @@ class DataInterface(QWidget):
|
||||
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")
|
||||
template_dir = os.path.join(base_dir, "User_code", "task")
|
||||
output_dir = os.path.join(self.project_path, "User", "task")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
|
@ -1,19 +1,8 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTextEdit, QFileDialog, QLabel
|
||||
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
|
||||
|
||||
from qfluentwidgets import TitleLabel, BodyLabel
|
||||
import pandas as pd
|
||||
import io
|
||||
|
||||
class FunctionFitInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
@ -21,153 +10,71 @@ class FunctionFitInterface(QWidget):
|
||||
self.setObjectName("functionFitInterface")
|
||||
|
||||
main_layout = QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(32, 32, 32, 32)
|
||||
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)
|
||||
left_layout.addWidget(TitleLabel("数据输入/导入"))
|
||||
self.dataEdit = QTextEdit()
|
||||
self.dataEdit.setPlaceholderText("输入数据,每行格式:x,y")
|
||||
left_layout.addWidget(self.dataEdit)
|
||||
|
||||
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 = QPushButton("导入 Excel")
|
||||
import_btn.clicked.connect(self.import_excel)
|
||||
export_btn = PushButton("导出 Excel")
|
||||
export_btn = QPushButton("导出 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)
|
||||
fit_btn = QPushButton("拟合并绘图")
|
||||
fit_btn.clicked.connect(self.fit_and_plot)
|
||||
left_layout.addWidget(fit_btn)
|
||||
|
||||
self.add_row()
|
||||
main_layout.addLayout(left_layout, 1)
|
||||
|
||||
# 右侧:图像展示区
|
||||
right_layout = QVBoxLayout()
|
||||
right_layout.setSpacing(12)
|
||||
right_layout.addWidget(SubtitleLabel("函数图像预览"))
|
||||
right_layout.setSpacing(16)
|
||||
right_layout.addWidget(TitleLabel("函数拟合图像"))
|
||||
|
||||
self.figure = Figure(figsize=(5, 4))
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
|
||||
self.figure, self.ax = plt.subplots()
|
||||
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)")
|
||||
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx *.xls)")
|
||||
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)
|
||||
|
||||
df = pd.read_excel(path)
|
||||
text = "\n".join(f"{row[0]},{row[1]}" for row in df.values)
|
||||
self.dataEdit.setText(text)
|
||||
|
||||
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)
|
||||
df = pd.DataFrame(data, columns=["x", "y"])
|
||||
df.to_excel(path, index=False)
|
||||
|
||||
def parse_data(self):
|
||||
lines = self.dataEdit.toPlainText().strip().split('\n')
|
||||
data = []
|
||||
row_count = self.dataTable.rowCount()
|
||||
for row in range(row_count):
|
||||
for line in lines:
|
||||
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())
|
||||
x, y = map(float, line.split(','))
|
||||
data.append([x, y])
|
||||
except Exception:
|
||||
continue
|
||||
@ -177,84 +84,18 @@ class FunctionFitInterface(QWidget):
|
||||
data = self.parse_data()
|
||||
if not data:
|
||||
self.resultLabel.setText("数据格式错误或为空")
|
||||
self.codeLabel.setText("")
|
||||
return
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
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()
|
||||
# 简单线性拟合
|
||||
coeffs = np.polyfit(x, y, 1)
|
||||
y_fit = np.polyval(coeffs, x)
|
||||
self.ax.clear()
|
||||
self.ax.scatter(x, y, label="原始数据")
|
||||
self.ax.plot(x, y_fit, color='r', label=f"拟合: y={coeffs[0]:.3f}x+{coeffs[1]:.3f}")
|
||||
self.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
|
||||
)
|
||||
self.resultLabel.setText(f"拟合公式: y = {coeffs[0]:.3f}x + {coeffs[1]:.3f}")
|
@ -1,15 +1,6 @@
|
||||
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):
|
||||
@ -28,7 +19,7 @@ class HomeInterface(QWidget):
|
||||
content_layout.setContentsMargins(48, 48, 48, 48)
|
||||
|
||||
# Logo
|
||||
logo = ImageLabel(resource_path('assets/logo/MRobot.png'))
|
||||
logo = ImageLabel('img/MRobot.png')
|
||||
logo.scaledToHeight(80)
|
||||
content_layout.addWidget(logo, alignment=Qt.AlignHCenter) # 居中对齐
|
||||
|
||||
|
@ -10,10 +10,9 @@ with redirect_stdout(None):
|
||||
|
||||
from .home_interface import HomeInterface
|
||||
from .serial_terminal_interface import SerialTerminalInterface
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from .part_library_interface import PartLibraryInterface
|
||||
from .data_interface import DataInterface
|
||||
from .mini_tool_interface import MiniToolInterface
|
||||
from .about_interface import AboutInterface
|
||||
import base64
|
||||
|
||||
|
||||
@ -48,20 +47,17 @@ class MainWindow(FluentWindow):
|
||||
def initInterface(self):
|
||||
self.homeInterface = HomeInterface(self)
|
||||
self.serialTerminalInterface = SerialTerminalInterface(self)
|
||||
self.functionFitInterface = FunctionFitInterface(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.functionFitInterface, FIF.ROBOT, 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.addSubInterface(self.dataInterface, FIF.DOWNLOAD, self.tr('代码生成'))
|
||||
# self.navigationInterface.addWidget(
|
||||
# 'startGameButton',
|
||||
# NavigationBarPushButton(FIF.PLAY, '启动游戏', isSelectable=False),
|
||||
@ -106,7 +102,7 @@ class MainWindow(FluentWindow):
|
||||
|
||||
# main_window.py 只需修改关闭事件
|
||||
def closeEvent(self, e):
|
||||
# if self.themeListener and self.themeListener.isRunning():
|
||||
# self.themeListener.terminate()
|
||||
# self.themeListener.deleteLater()
|
||||
if self.themeListener and self.themeListener.isRunning():
|
||||
self.themeListener.terminate()
|
||||
self.themeListener.deleteLater()
|
||||
super().closeEvent(e)
|
||||
|
@ -1,104 +0,0 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QSizePolicy
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
|
||||
|
||||
from .function_fit_interface import FunctionFitInterface
|
||||
from .ai_interface import AIInterface
|
||||
|
||||
class MiniToolInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("minitoolInterface")
|
||||
self.vBoxLayout = QVBoxLayout(self)
|
||||
self.vBoxLayout.setAlignment(Qt.AlignTop)
|
||||
self.vBoxLayout.setContentsMargins(10, 0, 10, 10) # 设置外边距
|
||||
|
||||
# 顶部标签栏,横向拉伸
|
||||
self.tabBar = TabBar(self)
|
||||
self.tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.vBoxLayout.addWidget(self.tabBar) # 移除 Qt.AlignLeft
|
||||
|
||||
self.stackedWidget = QStackedWidget(self)
|
||||
self.vBoxLayout.addWidget(self.stackedWidget) # 加入布局
|
||||
|
||||
|
||||
# 初始主页面
|
||||
self.mainPage = QWidget(self)
|
||||
mainLayout = QVBoxLayout(self.mainPage)
|
||||
mainLayout.setAlignment(Qt.AlignTop) # 卡片靠顶部
|
||||
self.card = PushSettingCard(
|
||||
text="▶ 启动",
|
||||
icon=FluentIcon.UNIT,
|
||||
title="曲线拟合工具",
|
||||
content="简单的曲线拟合工具,支持多种函数类型",
|
||||
)
|
||||
mainLayout.addWidget(self.card)
|
||||
|
||||
self.mainPage.setLayout(mainLayout)
|
||||
self.aiCard = PushSettingCard(
|
||||
text="▶ 启动",
|
||||
icon=FluentIcon.ROBOT,
|
||||
title="MRobot AI助手",
|
||||
content="与 MRobot 进行图一乐交流, 使用开源模型qwen3:0.6b。",
|
||||
)
|
||||
mainLayout.addWidget(self.aiCard)
|
||||
self.aiCard.clicked.connect(self.open_ai_tab)
|
||||
# 添加主页面到堆叠窗口
|
||||
self.addSubInterface(self.mainPage, "mainPage", "工具箱主页")
|
||||
|
||||
self.setLayout(self.vBoxLayout)
|
||||
|
||||
# 信号连接
|
||||
self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
|
||||
# self.tabBar.tabAddRequested.connect(self.onAddNewTab)
|
||||
self.tabBar.tabCloseRequested.connect(self.onCloseTab)
|
||||
self.card.clicked.connect(self.open_fit_tab)
|
||||
|
||||
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
|
||||
widget.setObjectName(objectName)
|
||||
self.stackedWidget.addWidget(widget)
|
||||
self.tabBar.addTab(
|
||||
routeKey=objectName,
|
||||
text=text,
|
||||
onClick=lambda: self.stackedWidget.setCurrentWidget(widget)
|
||||
)
|
||||
|
||||
def onCurrentIndexChanged(self, index):
|
||||
widget = self.stackedWidget.widget(index)
|
||||
self.tabBar.setCurrentTab(widget.objectName())
|
||||
|
||||
def onAddNewTab(self):
|
||||
pass # 可自定义添加新标签页逻辑
|
||||
|
||||
def onCloseTab(self, index: int):
|
||||
item = self.tabBar.tabItem(index)
|
||||
widget = self.findChild(QWidget, item.routeKey())
|
||||
self.stackedWidget.removeWidget(widget)
|
||||
self.tabBar.removeTab(index)
|
||||
widget.deleteLater()
|
||||
|
||||
def open_fit_tab(self):
|
||||
# 检查是否已存在标签页,避免重复添加
|
||||
for i in range(self.stackedWidget.count()):
|
||||
widget = self.stackedWidget.widget(i)
|
||||
if widget.objectName() == "fitPage":
|
||||
self.stackedWidget.setCurrentWidget(widget)
|
||||
self.tabBar.setCurrentTab("fitPage")
|
||||
return
|
||||
fit_page = FunctionFitInterface(self)
|
||||
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
|
||||
self.stackedWidget.setCurrentWidget(fit_page)
|
||||
self.tabBar.setCurrentTab("fitPage")
|
||||
|
||||
def open_ai_tab(self):
|
||||
# 检查是否已存在标签页,避免重复添加
|
||||
for i in range(self.stackedWidget.count()):
|
||||
widget = self.stackedWidget.widget(i)
|
||||
if widget.objectName() == "aiPage":
|
||||
self.stackedWidget.setCurrentWidget(widget)
|
||||
self.tabBar.setCurrentTab("aiPage")
|
||||
return
|
||||
ai_page = AIInterface(self)
|
||||
self.addSubInterface(ai_page, "aiPage", "AI问答")
|
||||
self.stackedWidget.setCurrentWidget(ai_page)
|
||||
self.tabBar.setCurrentTab("aiPage")
|
@ -1,22 +1,60 @@
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
||||
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, PushButton, TreeWidget, ProgressBar, Dialog, InfoBar, InfoBarPosition, FluentIcon, ProgressRing, Dialog
|
||||
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, PushButton, TreeWidget, ProgressBar, Dialog, InfoBar, InfoBarPosition, FluentIcon
|
||||
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"
|
||||
LOCAL_LIB_DIR = "assets/mech_lib"
|
||||
LOCAL_LIB_DIR = "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版)"))
|
||||
@ -113,64 +151,41 @@ class PartLibraryInterface(QWidget):
|
||||
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="已手动中断下载任务。",
|
||||
InfoBar.info(
|
||||
title="提示",
|
||||
content="请先勾选要下载的文件。",
|
||||
parent=self,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000
|
||||
)
|
||||
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.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.finished.connect(self.on_download_finished)
|
||||
self.download_thread.finished.connect(self.download_thread.deleteLater)
|
||||
self.download_thread.start()
|
||||
|
||||
def on_download_finished(self, success, fail):
|
||||
self.info_bar.close()
|
||||
msg = f"成功下载:{len(success)} 个文件,失败:{len(fail)} 个文件"
|
||||
|
||||
# 创建“打开文件夹”按钮
|
||||
self.progress_dialog.close()
|
||||
msg = f"成功下载: {len(success)} 个文件\n失败: {len(fail)} 个文件"
|
||||
dialog = Dialog(
|
||||
title="下载结果",
|
||||
content=msg,
|
||||
parent=self
|
||||
)
|
||||
open_btn = PushButton("打开文件夹")
|
||||
def open_folder():
|
||||
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
||||
@ -181,18 +196,10 @@ class PartLibraryInterface(QWidget):
|
||||
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)
|
||||
dialog.close()
|
||||
open_btn.clicked.connect(open_folder)
|
||||
self.result_bar.show()
|
||||
dialog.textLayout.addWidget(open_btn)
|
||||
dialog.exec()
|
||||
|
||||
def open_local_lib(self):
|
||||
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
||||
|
Binary file not shown.
Binary file not shown.
@ -1,14 +0,0 @@
|
||||
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 请求失败")
|
@ -1,20 +0,0 @@
|
||||
import re
|
||||
|
||||
def preserve_user_region(new_code, old_code, region_name):
|
||||
"""
|
||||
替换 new_code 中 region_name 区域为 old_code 中的内容(如果有)
|
||||
region_name: 如 'USER INCLUDE'
|
||||
"""
|
||||
pattern = re.compile(
|
||||
rf"/\*\s*{region_name}\s*BEGIN\s*\*/(.*?)/\*\s*{region_name}\s*END\s*\*/",
|
||||
re.DOTALL
|
||||
)
|
||||
old_match = pattern.search(old_code or "")
|
||||
if not old_match:
|
||||
return new_code # 旧文件没有该区域,直接返回新代码
|
||||
|
||||
old_content = old_match.group(1)
|
||||
def repl(m):
|
||||
return m.group(0).replace(m.group(1), old_content)
|
||||
# 替换新代码中的该区域
|
||||
return pattern.sub(repl, new_code, count=1)
|
@ -1,25 +0,0 @@
|
||||
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
|
@ -1,45 +0,0 @@
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
import requests
|
||||
import shutil
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
class DownloadThread(QThread):
|
||||
progressChanged = pyqtSignal(int)
|
||||
finished = pyqtSignal(list, list) # success, fail
|
||||
|
||||
def __init__(self, files, server_url, secret_key, local_dir, parent=None):
|
||||
super().__init__(parent)
|
||||
self.files = files
|
||||
self.server_url = server_url
|
||||
self.secret_key = secret_key
|
||||
self.local_dir = local_dir
|
||||
|
||||
def run(self):
|
||||
success, fail = [], []
|
||||
total = len(self.files)
|
||||
max_retry = 3
|
||||
for idx, rel_path in enumerate(self.files):
|
||||
retry = 0
|
||||
while retry < max_retry:
|
||||
try:
|
||||
rel_path_unix = rel_path.replace("\\", "/")
|
||||
encoded_path = quote(rel_path_unix)
|
||||
url = f"{self.server_url}/download/{encoded_path}"
|
||||
params = {"key": self.secret_key}
|
||||
resp = requests.get(url, params=params, stream=True, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
local_path = os.path.join(self.local_dir, rel_path)
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
with open(local_path, "wb") as f:
|
||||
shutil.copyfileobj(resp.raw, f)
|
||||
success.append(rel_path)
|
||||
break
|
||||
else:
|
||||
retry += 1
|
||||
except Exception:
|
||||
retry += 1
|
||||
else:
|
||||
fail.append(rel_path)
|
||||
self.progressChanged.emit(int((idx + 1) / total * 100))
|
||||
self.finished.emit(success, fail)
|
@ -1,109 +0,0 @@
|
||||
import os
|
||||
import yaml
|
||||
import textwrap
|
||||
from jinja2 import Template
|
||||
from .code_utils import preserve_user_region
|
||||
|
||||
def generate_task_code(task_list, project_path):
|
||||
# base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
|
||||
template_dir = os.path.join(project_root, "User_code", "task")
|
||||
output_dir = os.path.join(project_path, "User", "task")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
user_task_h_tpl = os.path.join(template_dir, "user_task.h.template")
|
||||
user_task_c_tpl = os.path.join(template_dir, "user_task.c.template")
|
||||
init_c_tpl = os.path.join(template_dir, "init.c.template")
|
||||
task_c_tpl = os.path.join(template_dir, "task.c.template")
|
||||
|
||||
freq_tasks = [t for t in task_list if t.get("freq_control", True)]
|
||||
|
||||
def render_template(path, context):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
tpl = Template(f.read())
|
||||
return tpl.render(**context)
|
||||
|
||||
context_h = {
|
||||
"thread_definitions": "\n".join([f" osThreadId_t {t['name']};" for t in task_list]),
|
||||
"freq_definitions": "\n".join([f" float {t['name']};" for t in freq_tasks]),
|
||||
"stack_definitions": "\n".join([f" UBaseType_t {t['name']};" for t in task_list]),
|
||||
"last_up_time_definitions": "\n".join([f" float {t['name']};" for t in freq_tasks]),
|
||||
"task_frequency_definitions": "\n".join([f"#define {t['name'].upper()}_FREQ ({t['frequency']})" for t in freq_tasks]),
|
||||
"task_init_delay_definitions": "\n".join([f"#define {t['name'].upper()}_INIT_DELAY ({t['delay']})" for t in task_list]),
|
||||
"task_attr_declarations": "\n".join([f"extern const osThreadAttr_t attr_{t['name']};" for t in task_list]),
|
||||
"task_function_declarations": "\n".join([f"void {t['function']}(void *argument);" for t in task_list]),
|
||||
}
|
||||
|
||||
# ----------- 生成 user_task.h -----------
|
||||
user_task_h_path = os.path.join(output_dir, "user_task.h")
|
||||
new_user_task_h = render_template(user_task_h_tpl, context_h)
|
||||
|
||||
if os.path.exists(user_task_h_path):
|
||||
with open(user_task_h_path, "r", encoding="utf-8") as f:
|
||||
old_code = f.read()
|
||||
for region in ["USER INCLUDE", "USER MESSAGE", "USER CONFIG"]:
|
||||
new_user_task_h = preserve_user_region(new_user_task_h, old_code, region)
|
||||
with open(user_task_h_path, "w", encoding="utf-8") as f:
|
||||
f.write(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 = render_template(user_task_c_tpl, context_c)
|
||||
with open(os.path.join(output_dir, "user_task.c"), "w", encoding="utf-8") as f:
|
||||
f.write(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 = render_template(init_c_tpl, context_init)
|
||||
init_c_path = os.path.join(output_dir, "init.c")
|
||||
if os.path.exists(init_c_path):
|
||||
with open(init_c_path, "r", encoding="utf-8") as f:
|
||||
old_code = f.read()
|
||||
for region in ["USER INCLUDE", "USER CODE", "USER CODE INIT"]:
|
||||
init_c = preserve_user_region(init_c, old_code, region)
|
||||
with open(init_c_path, "w", encoding="utf-8") as f:
|
||||
f.write(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")
|
||||
if os.path.exists(task_c_path):
|
||||
with open(task_c_path, "r", encoding="utf-8") as f:
|
||||
old_code = f.read()
|
||||
for region in ["USER INCLUDE", "USER STRUCT", "USER CODE", "USER CODE INIT"]:
|
||||
code = preserve_user_region(code, old_code, region)
|
||||
with open(task_c_path, "w", encoding="utf-8") as f:
|
||||
f.write(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)
|
136
fluent.py
Normal file
136
fluent.py
Normal file
@ -0,0 +1,136 @@
|
||||
import os
|
||||
import sys
|
||||
# 将当前工作目录设置为程序所在的目录,确保无论从哪里执行,其工作目录都正确设置为程序本身的位置,避免路径错误。
|
||||
os.chdir(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False)else os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import pyuac
|
||||
if not pyuac.isUserAdmin():
|
||||
try:
|
||||
pyuac.runAsAdmin(False)
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
|
||||
import atexit
|
||||
import base64
|
||||
|
||||
|
||||
|
||||
|
||||
def first_run():
|
||||
# if not cfg.get_value(base64.b64decode("YXV0b191cGRhdGU=").decode("utf-8")):
|
||||
# log.error("首次使用请先打开图形界面 March7th Launcher.exe")
|
||||
input("按回车键关闭窗口. . .")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def run_main_actions():
|
||||
while True:
|
||||
version.start()
|
||||
game.start()
|
||||
reward.start_specific("dispatch")
|
||||
Daily.start()
|
||||
reward.start()
|
||||
game.stop(True)
|
||||
|
||||
|
||||
def run_sub_task(action):
|
||||
game.start()
|
||||
sub_tasks = {
|
||||
"daily": lambda: (Daily.run(), reward.start()),
|
||||
"power": Power.run,
|
||||
"fight": Fight.start,
|
||||
"universe": Universe.start,
|
||||
"forgottenhall": lambda: challenge.start("memoryofchaos"),
|
||||
"purefiction": lambda: challenge.start("purefiction"),
|
||||
"apocalyptic": lambda: challenge.start("apocalyptic"),
|
||||
"redemption": Redemption.start
|
||||
}
|
||||
task = sub_tasks.get(action)
|
||||
if task:
|
||||
task()
|
||||
game.stop(False)
|
||||
|
||||
|
||||
def run_sub_task_gui(action):
|
||||
gui_tasks = {
|
||||
"universe_gui": Universe.gui,
|
||||
"fight_gui": Fight.gui
|
||||
}
|
||||
task = gui_tasks.get(action)
|
||||
if task and not task():
|
||||
input("按回车键关闭窗口. . .")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def run_sub_task_update(action):
|
||||
update_tasks = {
|
||||
"universe_update": Universe.update,
|
||||
"fight_update": Fight.update
|
||||
}
|
||||
task = update_tasks.get(action)
|
||||
if task:
|
||||
task()
|
||||
input("按回车键关闭窗口. . .")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def run_notify_action():
|
||||
notif.notify(cfg.notify_template['TestMessage'], "./assets/app/images/March7th.jpg")
|
||||
input("按回车键关闭窗口. . .")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main(action=None):
|
||||
first_run()
|
||||
|
||||
# 完整运行
|
||||
if action is None or action == "main":
|
||||
run_main_actions()
|
||||
|
||||
# 子任务
|
||||
elif action in ["daily", "power", "fight", "universe", "forgottenhall", "purefiction", "apocalyptic", "redemption"]:
|
||||
run_sub_task(action)
|
||||
|
||||
# 子任务 原生图形界面
|
||||
elif action in ["universe_gui", "fight_gui"]:
|
||||
run_sub_task_gui(action)
|
||||
|
||||
# 子任务 更新项目
|
||||
elif action in ["universe_update", "fight_update"]:
|
||||
run_sub_task_update(action)
|
||||
|
||||
elif action in ["screenshot", "plot"]:
|
||||
tool.start(action)
|
||||
|
||||
elif action == "game":
|
||||
game.start()
|
||||
|
||||
elif action == "notify":
|
||||
run_notify_action()
|
||||
|
||||
else:
|
||||
log.error(f"未知任务: {action}")
|
||||
input("按回车键关闭窗口. . .")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# 程序结束时的处理器
|
||||
def exit_handler():
|
||||
"""注册程序退出时的处理函数,用于清理OCR资源."""
|
||||
ocr.exit_ocr()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
atexit.register(exit_handler)
|
||||
main(sys.argv[1]) if len(sys.argv) > 1 else main()
|
||||
except KeyboardInterrupt:
|
||||
log.error("发生错误: 手动强制停止")
|
||||
input("按回车键关闭窗口. . .")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
log.error(cfg.notify_template['ErrorOccurred'].format(error=e))
|
||||
notif.notify(cfg.notify_template['ErrorOccurred'].format(error=e))
|
||||
input("按回车键关闭窗口. . .")
|
||||
sys.exit(1)
|
BIN
assets/mech_lib/.DS_Store → img/.DS_Store
vendored
BIN
assets/mech_lib/.DS_Store → img/.DS_Store
vendored
Binary file not shown.
BIN
img/M2.ico
Normal file
BIN
img/M2.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
img/MR.ico
Normal file
BIN
img/MR.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
img/MR.png
Normal file
BIN
img/MR.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
BIN
img/MRobot.ico
Normal file
BIN
img/MRobot.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user