mirror of
https://github.com/goldenfishs/MRobot.git
synced 2025-07-26 08:19:07 +08:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
7951dae760 | |||
501a9ddff4 | |||
47e0b8419f | |||
9fc6b4577a | |||
78661f450b | |||
62b4b07912 | |||
f2fedac360 | |||
34a0874156 | |||
e9eb169547 | |||
606bd7e054 | |||
ae6246474b | |||
c737ec79d4 | |||
a1da927d9c | |||
ffad148fcc | |||
f680b91816 | |||
b893c30eb3 | |||
86b0062881 | |||
26ce1316ca | |||
e7822bbf64 | |||
![]() |
79da21bca0 | ||
![]() |
b879e0ae94 | ||
![]() |
214ac00e90 | ||
![]() |
c4731883f2 | ||
![]() |
544b3745d5 | ||
![]() |
511f9f4da8 | ||
![]() |
2e8c902dd2 | ||
![]() |
918f6b443c | ||
![]() |
3da80d5efb | ||
![]() |
37d6f70055 | ||
![]() |
be987d6bdd | ||
![]() |
d7f6e93b5c | ||
![]() |
cddd7a2ad4 | ||
![]() |
97d42c70d0 | ||
![]() |
ced464290e | ||
![]() |
9f964e1532 | ||
![]() |
52acfaf20c | ||
![]() |
af69d030fe |
.DS_Store.gitignoreMRobot.issMRobot.pyREADME.md
User
bsp
component
device
task
app
__init__.py
__pycache__
__init__.cpython-39.pycabout_interface.cpython-39.pycai_interface.cpython-39.pycdata_interface.cpython-39.pycfunction_fit_interface.cpython-39.pychome_interface.cpython-39.pycmain_window.cpython-39.pycmini_tool_interface.cpython-39.pycpart_library_interface.cpython-39.pycserial_terminal_interface.cpython-39.pyc
about_interface.pyai_interface.pydata_interface.pyfunction_fit_interface.pyhome_interface.pymain_window.pymini_tool_interface.pypart_library_interface.pyserial_terminal_interface.pytools
assets/User_code
.DS_Store
bsp
.DS_Store.gitkeepbsp.hbuzzer_gpio.cbuzzer_gpio.hcan.ccan.hdelay.cdelay.hdependencies.csvdescribe.csvgpio_exti.cgpio_exti.hi2c.ci2c.hled_gpio.cled_gpio.hservo_pwm.cservo_pwm.hspi.cspi.huart.cuart.h
component
.gitkeepcrc16_rm.ccrc16_rm.hcrc8_rm.ccrc8_rm.hdependencies.csvdescribe.csvfilter.cfilter.hpid.cpid.huser_math.cuser_math.h
device
.DS_Store.gitkeepbmp280_i2c.cbmp280_i2c.hdependencies.csvdescribe.csvdevice.hkey_gpio.ckey_gpio.holed_i2c.coled_i2c.hpc_uart.cpc_uart.hservo.cservo.h
module
task
5
.gitignore
vendored
5
.gitignore
vendored
@ -28,3 +28,8 @@ Examples/
|
|||||||
!*.axf
|
!*.axf
|
||||||
!*.bin
|
!*.bin
|
||||||
!*.hex
|
!*.hex
|
||||||
|
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.spec
|
||||||
|
*.exe
|
18
MRobot.iss
Normal file
18
MRobot.iss
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[Setup]
|
||||||
|
AppName=MRobot
|
||||||
|
AppVersion=1.0.1
|
||||||
|
DefaultDirName={userappdata}\MRobot
|
||||||
|
DefaultGroupName=MRobot
|
||||||
|
OutputDir=.
|
||||||
|
OutputBaseFilename=MRobotInstaller
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "dist\MRobot.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: "assets\logo\*"; DestDir: "{app}\assets\logo"; Flags: ignoreversion recursesubdirs
|
||||||
|
Source: "assets\User_code\*"; DestDir: "{app}\assets\User_code"; Flags: ignoreversion recursesubdirs
|
||||||
|
Source: "assets\mech_lib\*"; DestDir: "{app}\assets\mech_lib"; Flags: ignoreversion recursesubdirs
|
||||||
|
Source: "assets\logo\M.ico"; DestDir: "{app}\assets\logo"; Flags: ignoreversion
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
||||||
|
Name: "{userdesktop}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
805
MRobot.py
805
MRobot.py
@ -1,794 +1,23 @@
|
|||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import sys
|
||||||
import shutil
|
# 将当前工作目录设置为程序所在的目录,确保无论从哪里执行,其工作目录都正确设置为程序本身的位置,避免路径错误。
|
||||||
import re
|
os.chdir(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False)else os.path.dirname(os.path.abspath(__file__)))
|
||||||
from git import Repo
|
|
||||||
from collections import defaultdict
|
|
||||||
import csv
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
# 配置常量
|
from PyQt5.QtCore import Qt
|
||||||
REPO_DIR = "MRobot_repo"
|
from PyQt5.QtWidgets import QApplication
|
||||||
REPO_URL = "http://gitea.qutrobot.top/robofish/MRobot.git"
|
from app.main_window import MainWindow
|
||||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
MDK_ARM_DIR = os.path.join(CURRENT_DIR, "MDK-ARM")
|
|
||||||
USER_DIR = os.path.join(CURRENT_DIR, "User")
|
|
||||||
|
|
||||||
class MRobotApp:
|
|
||||||
def __init__(self):
|
|
||||||
self.ioc_data = None
|
|
||||||
self.add_gitignore_var = None # 延迟初始化
|
|
||||||
self.header_file_vars = {}
|
|
||||||
self.task_vars = [] # 用于存储任务的变量
|
|
||||||
|
|
||||||
# 初始化
|
# 启用 DPI 缩放
|
||||||
def initialize(self):
|
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
|
||||||
print("初始化中,正在克隆仓库...")
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
|
||||||
self.clone_repo()
|
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) # 使用高 DPI 图标
|
||||||
self.ioc_data = self.find_and_read_ioc_file()
|
|
||||||
print("初始化完成,启动主窗口...")
|
|
||||||
self.show_main_window()
|
|
||||||
|
|
||||||
# 克隆仓库
|
|
||||||
def clone_repo(self):
|
|
||||||
try:
|
|
||||||
if os.path.exists(REPO_DIR):
|
|
||||||
shutil.rmtree(REPO_DIR)
|
|
||||||
print(f"正在克隆仓库到 {REPO_DIR}(仅克隆当前文件内容)...")
|
|
||||||
Repo.clone_from(REPO_URL, REPO_DIR, multi_options=["--depth=1"])
|
|
||||||
print("仓库克隆成功!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"克隆仓库时出错: {e}")
|
|
||||||
|
|
||||||
# 删除克隆的仓库
|
|
||||||
def delete_repo(self):
|
|
||||||
try:
|
|
||||||
if os.path.exists(REPO_DIR):
|
|
||||||
shutil.rmtree(REPO_DIR)
|
|
||||||
print(f"已删除克隆的仓库目录: {REPO_DIR}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"删除仓库目录时出错: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def auto_configure_environment(self):
|
|
||||||
"""自动配置环境"""
|
|
||||||
try:
|
|
||||||
self.add_groups_and_files()
|
|
||||||
self.add_include_path(r"..\User")
|
|
||||||
print("环境配置完成!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"自动配置环境时出错: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def log(self, message):
|
|
||||||
"""统一日志输出"""
|
|
||||||
print(message)
|
|
||||||
|
|
||||||
|
|
||||||
def find_uvprojx_file(self):
|
|
||||||
"""查找 MDK-ARM 文件夹中的 .uvprojx 文件"""
|
|
||||||
if not os.path.exists(MDK_ARM_DIR):
|
|
||||||
self.log(f"未找到 MDK-ARM 文件夹:{MDK_ARM_DIR}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
uvprojx_files = [f for f in os.listdir(MDK_ARM_DIR) if f.endswith(".uvprojx")]
|
|
||||||
if not uvprojx_files:
|
|
||||||
self.log(f"在 {MDK_ARM_DIR} 中未找到任何 .uvprojx 文件!")
|
|
||||||
return None
|
|
||||||
|
|
||||||
project_file = os.path.join(MDK_ARM_DIR, uvprojx_files[0])
|
|
||||||
self.log(f"找到项目文件:{project_file}")
|
|
||||||
return project_file
|
|
||||||
|
|
||||||
|
|
||||||
def add_groups_and_files(self):
|
|
||||||
"""添加 User 文件夹中的组和文件到 Keil 项目"""
|
|
||||||
project_file = self.find_uvprojx_file()
|
|
||||||
if not project_file:
|
|
||||||
return
|
|
||||||
|
|
||||||
tree = ET.parse(project_file)
|
|
||||||
root = tree.getroot()
|
|
||||||
groups_node = root.find(".//Groups")
|
|
||||||
if groups_node is None:
|
|
||||||
self.log("未找到 Groups 节点!")
|
|
||||||
return
|
|
||||||
|
|
||||||
existing_groups = {group.find("GroupName").text for group in groups_node.findall("Group")}
|
|
||||||
existing_files = {
|
|
||||||
file.text
|
|
||||||
for group in groups_node.findall("Group")
|
|
||||||
for file in group.findall(".//FileName")
|
|
||||||
}
|
|
||||||
|
|
||||||
for folder_name in os.listdir(USER_DIR):
|
|
||||||
folder_path = os.path.join(USER_DIR, folder_name)
|
|
||||||
if not os.path.isdir(folder_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
group_name = f"User/{folder_name}"
|
|
||||||
if group_name in existing_groups:
|
|
||||||
self.log(f"组 {group_name} 已存在,跳过...")
|
|
||||||
continue
|
|
||||||
|
|
||||||
group_node = ET.SubElement(groups_node, "Group")
|
|
||||||
ET.SubElement(group_node, "GroupName").text = group_name
|
|
||||||
files_node = ET.SubElement(group_node, "Files")
|
|
||||||
|
|
||||||
for file_name in os.listdir(folder_path):
|
|
||||||
file_path = os.path.join(folder_path, file_name)
|
|
||||||
if not os.path.isfile(file_path) or file_name in existing_files:
|
|
||||||
self.log(f"文件 {file_name} 已存在或无效,跳过...")
|
|
||||||
continue
|
|
||||||
|
|
||||||
file_node = ET.SubElement(files_node, "File")
|
|
||||||
ET.SubElement(file_node, "FileName").text = file_name
|
|
||||||
ET.SubElement(file_node, "FileType").text = "1"
|
|
||||||
relative_path = os.path.relpath(file_path, os.path.dirname(project_file)).replace("\\", "/")
|
|
||||||
ET.SubElement(file_node, "FilePath").text = relative_path
|
|
||||||
|
|
||||||
tree.write(project_file, encoding="utf-8", xml_declaration=True)
|
|
||||||
self.log("Keil 项目文件已更新!")
|
|
||||||
|
|
||||||
def add_include_path(self, new_path):
|
|
||||||
"""添加新的 IncludePath 到 Keil 项目"""
|
|
||||||
project_file = self.find_uvprojx_file()
|
|
||||||
if not project_file:
|
|
||||||
return
|
|
||||||
|
|
||||||
tree = ET.parse(project_file)
|
|
||||||
root = tree.getroot()
|
|
||||||
include_path_nodes = root.findall(".//IncludePath")
|
|
||||||
if not include_path_nodes:
|
|
||||||
self.log("未找到任何 IncludePath 节点,无法添加路径。")
|
|
||||||
return
|
|
||||||
|
|
||||||
updated = False
|
|
||||||
for index, include_path_node in enumerate(include_path_nodes):
|
|
||||||
if index == 0:
|
|
||||||
self.log("跳过第一组 IncludePath 节点,不进行修改。")
|
|
||||||
continue
|
|
||||||
|
|
||||||
include_paths = include_path_node.text.split(";") if include_path_node.text else []
|
|
||||||
if new_path in include_paths:
|
|
||||||
self.log(f"路径 '{new_path}' 已存在于第 {index + 1} 组 IncludePath 节点中,无需重复添加。")
|
|
||||||
continue
|
|
||||||
|
|
||||||
include_paths.append(new_path)
|
|
||||||
include_path_node.text = ";".join(include_paths)
|
|
||||||
updated = True
|
|
||||||
self.log(f"路径 '{new_path}' 已成功添加到第 {index + 1} 组 IncludePath 节点中。")
|
|
||||||
|
|
||||||
if updated:
|
|
||||||
tree.write(project_file, encoding="utf-8", xml_declaration=True)
|
|
||||||
self.log(f"项目文件已更新:{project_file}")
|
|
||||||
else:
|
|
||||||
self.log("未对项目文件进行任何修改。")
|
|
||||||
|
|
||||||
|
|
||||||
# 复制文件
|
|
||||||
def copy_file_from_repo(self, src_path, dest_path):
|
|
||||||
try:
|
|
||||||
# 修复路径拼接问题,确保 src_path 不重复包含 REPO_DIR
|
|
||||||
if src_path.startswith(REPO_DIR):
|
|
||||||
full_src_path = src_path
|
|
||||||
else:
|
|
||||||
full_src_path = os.path.join(REPO_DIR, src_path.lstrip(os.sep))
|
|
||||||
|
|
||||||
# 检查源文件是否存在
|
|
||||||
if not os.path.exists(full_src_path):
|
|
||||||
print(f"文件 {full_src_path} 不存在!(检查路径或仓库内容)")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 检查目标路径是否有效
|
|
||||||
if not dest_path or not dest_path.strip():
|
|
||||||
print("目标路径为空或无效,无法复制文件!")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 创建目标目录(如果不存在)
|
|
||||||
dest_dir = os.path.dirname(dest_path)
|
|
||||||
if dest_dir and not os.path.exists(dest_dir):
|
|
||||||
os.makedirs(dest_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 执行文件复制
|
|
||||||
shutil.copy(full_src_path, dest_path)
|
|
||||||
print(f"文件已从 {full_src_path} 复制到 {dest_path}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"复制文件时出错: {e}")
|
|
||||||
|
|
||||||
# 查找并读取 .ioc 文件
|
|
||||||
def find_and_read_ioc_file(self):
|
|
||||||
try:
|
|
||||||
for file in os.listdir("."):
|
|
||||||
if file.endswith(".ioc"):
|
|
||||||
print(f"找到 .ioc 文件: {file}")
|
|
||||||
with open(file, "r", encoding="utf-8") as f:
|
|
||||||
return f.read()
|
|
||||||
print("未找到 .ioc 文件!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"读取 .ioc 文件时出错: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 检查是否启用了 FreeRTOS
|
|
||||||
def check_freertos_enabled(self, ioc_data):
|
|
||||||
try:
|
|
||||||
return bool(re.search(r"Mcu\.IP\d+=FREERTOS", ioc_data))
|
|
||||||
except Exception as e:
|
|
||||||
print(f"检查 FreeRTOS 配置时出错: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 生成操作
|
|
||||||
def generate_action(self):
|
|
||||||
def task():
|
|
||||||
# 检查并创建目录
|
|
||||||
self.create_directories()
|
|
||||||
|
|
||||||
if self.add_gitignore_var.get():
|
|
||||||
self.copy_file_from_repo(".gitignore", ".gitignore")
|
|
||||||
if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
|
|
||||||
self.copy_file_from_repo("src/freertos.c", os.path.join("Core", "Src", "freertos.c"))
|
|
||||||
|
|
||||||
# 定义需要处理的文件夹
|
|
||||||
folders = ["bsp", "component", "device", "module"]
|
|
||||||
|
|
||||||
# 遍历每个文件夹,复制选中的 .h 和 .c 文件
|
|
||||||
for folder in folders:
|
|
||||||
folder_dir = os.path.join(REPO_DIR, "User", folder)
|
|
||||||
if not os.path.exists(folder_dir):
|
|
||||||
continue # 如果文件夹不存在,跳过
|
|
||||||
|
|
||||||
for file_name in os.listdir(folder_dir):
|
|
||||||
file_base, file_ext = os.path.splitext(file_name)
|
|
||||||
if file_ext not in [".h", ".c"]:
|
|
||||||
continue # 只处理 .h 和 .c 文件
|
|
||||||
|
|
||||||
# 强制复制与文件夹同名的文件
|
|
||||||
if file_base == folder:
|
|
||||||
src_path = os.path.join(folder_dir, file_name)
|
|
||||||
dest_path = os.path.join("User", folder, file_name)
|
|
||||||
self.copy_file_from_repo(src_path, dest_path)
|
|
||||||
continue # 跳过后续检查,直接复制
|
|
||||||
|
|
||||||
# 检查是否选中了对应的文件
|
|
||||||
if file_base in self.header_file_vars and self.header_file_vars[file_base].get():
|
|
||||||
src_path = os.path.join(folder_dir, file_name)
|
|
||||||
dest_path = os.path.join("User", folder, file_name)
|
|
||||||
self.copy_file_from_repo(src_path, dest_path)
|
|
||||||
|
|
||||||
threading.Thread(target=task).start()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 创建必要的目录
|
|
||||||
def create_directories(self):
|
|
||||||
try:
|
|
||||||
directories = [
|
|
||||||
"User/bsp",
|
|
||||||
"User/component",
|
|
||||||
"User/device",
|
|
||||||
"User/module",
|
|
||||||
]
|
|
||||||
# 根据是否启用 FreeRTOS 决定是否创建 User/task
|
|
||||||
if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
|
|
||||||
directories.append("User/task")
|
|
||||||
|
|
||||||
for directory in directories:
|
|
||||||
if not os.path.exists(directory):
|
|
||||||
os.makedirs(directory, exist_ok=True)
|
|
||||||
print(f"已创建目录: {directory}")
|
|
||||||
else:
|
|
||||||
print(f"目录已存在: {directory}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"创建目录时出错: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# 更新 FreeRTOS 状态标签
|
|
||||||
def update_freertos_status(self, label):
|
|
||||||
if self.ioc_data:
|
|
||||||
status = "已启用" if self.check_freertos_enabled(self.ioc_data) else "未启用"
|
|
||||||
else:
|
|
||||||
status = "未检测到 .ioc 文件"
|
|
||||||
label.config(text=f"FreeRTOS 状态: {status}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 显示主窗口
|
|
||||||
def show_main_window(self):
|
|
||||||
root = tk.Tk()
|
|
||||||
root.title("MRobot 自动生成脚本")
|
|
||||||
root.geometry("800x600") # 调整窗口大小以适应布局
|
|
||||||
|
|
||||||
# 在窗口关闭时调用 on_closing 方法
|
|
||||||
root.protocol("WM_DELETE_WINDOW", lambda: self.on_closing(root))
|
|
||||||
|
|
||||||
# 初始化 BooleanVar
|
|
||||||
self.add_gitignore_var = tk.BooleanVar(value=False)
|
|
||||||
self.auto_configure_var = tk.BooleanVar(value=False) # 新增复选框变量
|
|
||||||
|
|
||||||
# 创建主框架
|
|
||||||
main_frame = ttk.Frame(root)
|
|
||||||
main_frame.pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
# 添加标题
|
|
||||||
title_label = ttk.Label(main_frame, text="MRobot 自动生成脚本", font=("Arial", 16, "bold"))
|
|
||||||
title_label.pack(pady=10)
|
|
||||||
|
|
||||||
# 添加 FreeRTOS 状态标签
|
|
||||||
freertos_status_label = ttk.Label(main_frame, text="FreeRTOS 状态: 检测中...", font=("Arial", 12))
|
|
||||||
freertos_status_label.pack(pady=10)
|
|
||||||
self.update_freertos_status(freertos_status_label)
|
|
||||||
|
|
||||||
# 模块文件选择和任务管理框架
|
|
||||||
module_task_frame = ttk.Frame(main_frame)
|
|
||||||
module_task_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# 模块文件选择框
|
|
||||||
header_files_frame = ttk.LabelFrame(module_task_frame, text="模块文件选择", padding=(10, 10))
|
|
||||||
header_files_frame.pack(side="left", fill="both", expand=True, padx=5)
|
|
||||||
self.header_files_frame = header_files_frame
|
|
||||||
self.update_header_files()
|
|
||||||
|
|
||||||
if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
|
|
||||||
task_frame = ttk.LabelFrame(module_task_frame, text="任务管理", padding=(10, 10))
|
|
||||||
task_frame.pack(side="left", fill="both", expand=True, padx=5)
|
|
||||||
self.task_frame = task_frame
|
|
||||||
self.update_task_ui()
|
|
||||||
|
|
||||||
# 添加消息框和生成按钮在同一行
|
|
||||||
bottom_frame = ttk.Frame(main_frame)
|
|
||||||
bottom_frame.pack(fill="x", pady=10, side="bottom")
|
|
||||||
|
|
||||||
# 消息框
|
|
||||||
self.message_box = tk.Text(bottom_frame, wrap="word", state="disabled", height=5, width=60)
|
|
||||||
self.message_box.pack(side="left", fill="x", expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
# 生成按钮和复选框选项
|
|
||||||
button_frame = ttk.Frame(bottom_frame)
|
|
||||||
button_frame.pack(side="right", padx=10)
|
|
||||||
|
|
||||||
# 添加复选框容器(横向排列复选框)
|
|
||||||
checkbox_frame = ttk.Frame(button_frame)
|
|
||||||
checkbox_frame.pack(side="top", pady=5)
|
|
||||||
|
|
||||||
# 添加 .gitignore 复选框(左侧)
|
|
||||||
ttk.Checkbutton(checkbox_frame, text=".gitignore", variable=self.add_gitignore_var).pack(side="left", padx=5)
|
|
||||||
|
|
||||||
# 添加自动配置环境复选框(右侧)
|
|
||||||
ttk.Checkbutton(checkbox_frame, text="自动环境", variable=self.auto_configure_var).pack(side="left", padx=5)
|
|
||||||
|
|
||||||
# 添加生成按钮(竖向排列在复选框下方)
|
|
||||||
generate_button = ttk.Button(button_frame, text="一键生成MRobot代码", command=self.generate_action)
|
|
||||||
generate_button.pack(side="top", pady=10)
|
|
||||||
generate_button.config(width=25) # 设置按钮宽度
|
|
||||||
|
|
||||||
# 重定向输出到消息框
|
|
||||||
self.redirect_output()
|
|
||||||
|
|
||||||
# 打印欢迎信息
|
|
||||||
print("欢迎使用 MRobot 自动生成脚本!")
|
|
||||||
print("请根据需要选择模块文件和任务。")
|
|
||||||
print("点击“一键生成MRobot代码”按钮开始生成。")
|
|
||||||
|
|
||||||
# 启动 Tkinter 主事件循环
|
|
||||||
root.mainloop()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def redirect_output(self):
|
|
||||||
"""
|
|
||||||
重定向标准输出到消息框
|
|
||||||
"""
|
|
||||||
class TextRedirector:
|
|
||||||
def __init__(self, text_widget):
|
|
||||||
self.text_widget = text_widget
|
|
||||||
|
|
||||||
def write(self, message):
|
|
||||||
self.text_widget.config(state="normal")
|
|
||||||
self.text_widget.insert("end", message)
|
|
||||||
self.text_widget.see("end")
|
|
||||||
self.text_widget.config(state="disabled")
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.stdout = TextRedirector(self.message_box)
|
|
||||||
sys.stderr = TextRedirector(self.message_box)
|
|
||||||
|
|
||||||
# 修改 update_task_ui 方法
|
|
||||||
def update_task_ui(self):
|
|
||||||
# 检查是否有已存在的任务文件
|
|
||||||
task_dir = os.path.join("User", "task")
|
|
||||||
if os.path.exists(task_dir):
|
|
||||||
for file_name in os.listdir(task_dir):
|
|
||||||
file_base, file_ext = os.path.splitext(file_name)
|
|
||||||
if file_ext == ".c" and file_base not in ["init", "user_task"] and file_base not in [task_var.get() for task_var, _ in self.task_vars]:
|
|
||||||
frequency = 100 # 默认频率
|
|
||||||
user_task_header_path = os.path.join("User", "task", "user_task.h")
|
|
||||||
if os.path.exists(user_task_header_path):
|
|
||||||
try:
|
|
||||||
with open(user_task_header_path, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
pattern = rf"#define\s+TASK_FREQ_{file_base.upper()}\s*\((\d+)[uU]?\)"
|
|
||||||
match = re.search(pattern, content)
|
|
||||||
if match:
|
|
||||||
frequency = int(match.group(1))
|
|
||||||
print(f"从 user_task.h 文件中读取到任务 {file_base} 的频率: {frequency}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"读取 user_task.h 文件时出错: {e}")
|
|
||||||
|
|
||||||
new_task_var = tk.StringVar(value=file_base)
|
|
||||||
self.task_vars.append((new_task_var, tk.IntVar(value=frequency)))
|
|
||||||
|
|
||||||
# 清空任务框架中的所有子组件
|
|
||||||
for widget in self.task_frame.winfo_children():
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
# 设置任务管理框的固定宽度
|
|
||||||
self.task_frame.config(width=400)
|
|
||||||
|
|
||||||
# 显示任务列表
|
|
||||||
for i, (task_var, freq_var) in enumerate(self.task_vars):
|
|
||||||
task_row = ttk.Frame(self.task_frame, width=400)
|
|
||||||
task_row.pack(fill="x", pady=5)
|
|
||||||
|
|
||||||
ttk.Entry(task_row, textvariable=task_var, width=20).pack(side="left", padx=5)
|
|
||||||
ttk.Label(task_row, text="频率:").pack(side="left", padx=5)
|
|
||||||
ttk.Spinbox(task_row, from_=1, to=1000, textvariable=freq_var, width=5).pack(side="left", padx=5)
|
|
||||||
ttk.Button(task_row, text="删除", command=lambda idx=i: self.remove_task(idx)).pack(side="left", padx=5)
|
|
||||||
|
|
||||||
# 添加新任务按钮
|
|
||||||
add_task_button = ttk.Button(self.task_frame, text="添加任务", command=self.add_task)
|
|
||||||
add_task_button.pack(pady=10)
|
|
||||||
|
|
||||||
|
|
||||||
# 修改 add_task 方法
|
|
||||||
def add_task(self):
|
|
||||||
new_task_var = tk.StringVar(value=f"Task_{len(self.task_vars) + 1}")
|
|
||||||
new_freq_var = tk.IntVar(value=100) # 默认频率为 100
|
|
||||||
self.task_vars.append((new_task_var, new_freq_var))
|
|
||||||
self.update_task_ui()
|
|
||||||
|
|
||||||
# 修改 remove_task 方法
|
|
||||||
def remove_task(self, idx):
|
|
||||||
del self.task_vars[idx]
|
|
||||||
self.update_task_ui()
|
|
||||||
|
|
||||||
# 更新文件夹显示
|
|
||||||
def update_folder_display(self):
|
|
||||||
for widget in self.folder_frame.winfo_children():
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
folders = ["User/bsp", "User/component", "User/device", "User/module"]
|
|
||||||
# if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
|
|
||||||
# folders.append("User/task")
|
|
||||||
|
|
||||||
for folder in folders:
|
|
||||||
# 去掉 "User/" 前缀
|
|
||||||
display_name = folder.replace("User/", "")
|
|
||||||
tk.Label(self.folder_frame, text=display_name).pack()
|
|
||||||
|
|
||||||
# 更新 .h 文件复选框
|
|
||||||
def update_header_files(self):
|
|
||||||
for widget in self.header_files_frame.winfo_children():
|
|
||||||
widget.destroy()
|
|
||||||
|
|
||||||
folders = ["bsp", "component", "device", "module"]
|
|
||||||
dependencies = defaultdict(list)
|
|
||||||
|
|
||||||
for folder in folders:
|
|
||||||
folder_dir = os.path.join(REPO_DIR, "User", folder)
|
|
||||||
if os.path.exists(folder_dir):
|
|
||||||
dependencies_file = os.path.join(folder_dir, "dependencies.csv")
|
|
||||||
if os.path.exists(dependencies_file):
|
|
||||||
with open(dependencies_file, "r", encoding="utf-8") as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
for row in reader:
|
|
||||||
if len(row) == 2:
|
|
||||||
dependencies[row[0]].append(row[1])
|
|
||||||
|
|
||||||
# 创建复选框
|
|
||||||
for folder in folders:
|
|
||||||
folder_dir = os.path.join(REPO_DIR, "User", folder)
|
|
||||||
if os.path.exists(folder_dir):
|
|
||||||
module_frame = ttk.LabelFrame(self.header_files_frame, text=folder.capitalize(), padding=(10, 10))
|
|
||||||
module_frame.pack(fill="x", pady=5)
|
|
||||||
|
|
||||||
row, col = 0, 0
|
|
||||||
for file in os.listdir(folder_dir):
|
|
||||||
file_base, file_ext = os.path.splitext(file)
|
|
||||||
if file_ext == ".h" and file_base != folder:
|
|
||||||
var = tk.BooleanVar(value=False)
|
|
||||||
self.header_file_vars[file_base] = var
|
|
||||||
|
|
||||||
checkbox = ttk.Checkbutton(
|
|
||||||
module_frame,
|
|
||||||
text=file_base,
|
|
||||||
variable=var,
|
|
||||||
command=lambda fb=file_base: self.handle_dependencies(fb, dependencies)
|
|
||||||
)
|
|
||||||
checkbox.grid(row=row, column=col, padx=5, pady=5, sticky="w")
|
|
||||||
col += 1
|
|
||||||
if col >= 6:
|
|
||||||
col = 0
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def handle_dependencies(self, file_base, dependencies):
|
|
||||||
"""
|
|
||||||
根据依赖关系自动勾选相关模块
|
|
||||||
"""
|
|
||||||
if file_base in self.header_file_vars and self.header_file_vars[file_base].get():
|
|
||||||
# 如果当前模块被选中,自动勾选其依赖项
|
|
||||||
for dependency in dependencies.get(file_base, []):
|
|
||||||
dep_base = os.path.basename(dependency)
|
|
||||||
if dep_base in self.header_file_vars:
|
|
||||||
self.header_file_vars[dep_base].set(True)
|
|
||||||
|
|
||||||
# 在 MRobotApp 类中添加以下方法
|
|
||||||
def generate_task_files(self):
|
|
||||||
try:
|
|
||||||
template_file_path = os.path.join(REPO_DIR, "User", "task", "task.c.template")
|
|
||||||
task_dir = os.path.join("User", "task")
|
|
||||||
|
|
||||||
if not os.path.exists(template_file_path):
|
|
||||||
print(f"模板文件 {template_file_path} 不存在,无法生成 task.c 文件!")
|
|
||||||
return
|
|
||||||
|
|
||||||
os.makedirs(task_dir, exist_ok=True)
|
|
||||||
|
|
||||||
with open(template_file_path, "r", encoding="utf-8") as f:
|
|
||||||
template_content = f.read()
|
|
||||||
|
|
||||||
# 为每个任务生成对应的 task.c 文件
|
|
||||||
for task_var, _ in self.task_vars: # 解包元组
|
|
||||||
task_name = f"Task_{task_var.get()}" # 添加前缀 Task_
|
|
||||||
task_file_path = os.path.join(task_dir, f"{task_var.get().lower()}.c") # 文件名保持原始小写
|
|
||||||
|
|
||||||
# 替换模板中的占位符
|
|
||||||
task_content = template_content.replace("{{task_name}}", task_name)
|
|
||||||
task_content = task_content.replace("{{task_function}}", task_name)
|
|
||||||
task_content = task_content.replace(
|
|
||||||
"{{task_frequency}}", f"TASK_FREQ_{task_var.get().upper()}"
|
|
||||||
) # 替换为 user_task.h 中的宏定义
|
|
||||||
task_content = task_content.replace("{{task_delay}}", f"TASK_INIT_DELAY_{task_var.get().upper()}")
|
|
||||||
|
|
||||||
with open(task_file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(task_content)
|
|
||||||
|
|
||||||
print(f"已成功生成 {task_file_path} 文件!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"生成 task.c 文件时出错: {e}")
|
|
||||||
# 修改 user_task.c 文件
|
|
||||||
def modify_user_task_file(self):
|
|
||||||
try:
|
|
||||||
template_file_path = os.path.join(REPO_DIR, "User", "task", "user_task.c.template")
|
|
||||||
generated_task_file_path = os.path.join("User", "task", "user_task.c")
|
|
||||||
|
|
||||||
if not os.path.exists(template_file_path):
|
|
||||||
print(f"模板文件 {template_file_path} 不存在,无法生成 user_task.c 文件!")
|
|
||||||
return
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(generated_task_file_path), exist_ok=True)
|
|
||||||
|
|
||||||
with open(template_file_path, "r", encoding="utf-8") as f:
|
|
||||||
template_content = f.read()
|
|
||||||
|
|
||||||
# 生成任务属性定义
|
|
||||||
task_attr_definitions = "\n".join([
|
|
||||||
f"""const osThreadAttr_t attr_{task_var.get().lower()} = {{
|
|
||||||
.name = "{task_var.get()}",
|
|
||||||
.priority = osPriorityNormal,
|
|
||||||
.stack_size = 128 * 4,
|
|
||||||
}};"""
|
|
||||||
for task_var, _ in self.task_vars # 解包元组
|
|
||||||
])
|
|
||||||
|
|
||||||
# 替换模板中的占位符
|
|
||||||
task_content = template_content.replace("{{task_attr_definitions}}", task_attr_definitions)
|
|
||||||
|
|
||||||
with open(generated_task_file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(task_content)
|
|
||||||
|
|
||||||
print(f"已成功生成 {generated_task_file_path} 文件!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"修改 user_task.c 文件时出错: {e}")
|
|
||||||
# ...existing code...
|
|
||||||
|
|
||||||
def generate_user_task_header(self):
|
|
||||||
try:
|
|
||||||
template_file_path = os.path.join(REPO_DIR, "User", "task", "user_task.h.template")
|
|
||||||
header_file_path = os.path.join("User", "task", "user_task.h")
|
|
||||||
|
|
||||||
if not os.path.exists(template_file_path):
|
|
||||||
print(f"模板文件 {template_file_path} 不存在,无法生成 user_task.h 文件!")
|
|
||||||
return
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(header_file_path), exist_ok=True)
|
|
||||||
|
|
||||||
# 如果 user_task.h 已存在,提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
|
|
||||||
existing_msgq_content = ""
|
|
||||||
if os.path.exists(header_file_path):
|
|
||||||
with open(header_file_path, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
# 提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
|
|
||||||
match = re.search(r"/\* USER MESSAGE BEGIN \*/\s*(.*?)\s*/\* USER MESSAGE END \*/", content, re.DOTALL)
|
|
||||||
if match:
|
|
||||||
existing_msgq_content = match.group(1).strip()
|
|
||||||
print("已存在的 msgq 区域内容:")
|
|
||||||
print(existing_msgq_content)
|
|
||||||
|
|
||||||
with open(template_file_path, "r", encoding="utf-8") as f:
|
|
||||||
template_content = f.read()
|
|
||||||
|
|
||||||
# 定义占位符内容
|
|
||||||
thread_definitions = "\n".join([f" osThreadId_t {task_var.get().lower()};" for task_var, _ in self.task_vars])
|
|
||||||
msgq_definitions = existing_msgq_content if existing_msgq_content else " osMessageQueueId_t default_msgq;"
|
|
||||||
freq_definitions = "\n".join([f" float {task_var.get().lower()};" for task_var, _ in self.task_vars])
|
|
||||||
last_up_time_definitions = "\n".join([f" uint32_t {task_var.get().lower()};" for task_var, _ in self.task_vars])
|
|
||||||
task_attr_declarations = "\n".join([f"extern const osThreadAttr_t attr_{task_var.get().lower()};" for task_var, _ in self.task_vars])
|
|
||||||
task_function_declarations = "\n".join([f"void Task_{task_var.get()}(void *argument);" for task_var, _ in self.task_vars])
|
|
||||||
task_frequency_definitions = "\n".join([
|
|
||||||
f"#define TASK_FREQ_{task_var.get().upper()} ({freq_var.get()}u)"
|
|
||||||
for task_var, freq_var in self.task_vars
|
|
||||||
])
|
|
||||||
task_init_delay_definitions = "\n".join([f"#define TASK_INIT_DELAY_{task_var.get().upper()} (0u)" for task_var, _ in self.task_vars])
|
|
||||||
task_handle_definitions = "\n".join([f" osThreadId_t {task_var.get().lower()};" for task_var, _ in self.task_vars])
|
|
||||||
|
|
||||||
# 替换模板中的占位符
|
|
||||||
header_content = template_content.replace("{{thread_definitions}}", thread_definitions)
|
|
||||||
header_content = header_content.replace("{{msgq_definitions}}", msgq_definitions)
|
|
||||||
header_content = header_content.replace("{{freq_definitions}}", freq_definitions)
|
|
||||||
header_content = header_content.replace("{{last_up_time_definitions}}", last_up_time_definitions)
|
|
||||||
header_content = header_content.replace("{{task_attr_declarations}}", task_attr_declarations)
|
|
||||||
header_content = header_content.replace("{{task_function_declarations}}", task_function_declarations)
|
|
||||||
header_content = header_content.replace("{{task_frequency_definitions}}", task_frequency_definitions)
|
|
||||||
header_content = header_content.replace("{{task_init_delay_definitions}}", task_init_delay_definitions)
|
|
||||||
header_content = header_content.replace("{{task_handle_definitions}}", task_handle_definitions)
|
|
||||||
|
|
||||||
# 如果存在 /* USER MESSAGE BEGIN */ 区域内容,则保留
|
|
||||||
if existing_msgq_content:
|
|
||||||
header_content = re.sub(
|
|
||||||
r"/\* USER MESSAGE BEGIN \*/\s*.*?\s*/\* USER MESSAGE END \*/",
|
|
||||||
f"/* USER MESSAGE BEGIN */\n\n {existing_msgq_content}\n\n /* USER MESSAGE END */",
|
|
||||||
header_content,
|
|
||||||
flags=re.DOTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(header_file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(header_content)
|
|
||||||
|
|
||||||
print(f"已成功生成 {header_file_path} 文件!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"生成 user_task.h 文件时出错: {e}")
|
|
||||||
|
|
||||||
def generate_init_file(self):
|
|
||||||
try:
|
|
||||||
template_file_path = os.path.join(REPO_DIR, "User", "task", "init.c.template")
|
|
||||||
generated_file_path = os.path.join("User", "task", "init.c")
|
|
||||||
|
|
||||||
if not os.path.exists(template_file_path):
|
|
||||||
print(f"模板文件 {template_file_path} 不存在,无法生成 init.c 文件!")
|
|
||||||
return
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(generated_file_path), exist_ok=True)
|
|
||||||
|
|
||||||
# 如果 init.c 已存在,提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
|
|
||||||
existing_msgq_content = ""
|
|
||||||
if os.path.exists(generated_file_path):
|
|
||||||
with open(generated_file_path, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
# 提取 /* USER MESSAGE BEGIN */ 和 /* USER MESSAGE END */ 区域内容
|
|
||||||
match = re.search(r"/\* USER MESSAGE BEGIN \*/\s*(.*?)\s*/\* USER MESSAGE END \*/", content, re.DOTALL)
|
|
||||||
if match:
|
|
||||||
existing_msgq_content = match.group(1).strip()
|
|
||||||
print("已存在的消息队列区域内容:")
|
|
||||||
print(existing_msgq_content)
|
|
||||||
|
|
||||||
with open(template_file_path, "r", encoding="utf-8") as f:
|
|
||||||
template_content = f.read()
|
|
||||||
|
|
||||||
# 生成任务创建代码
|
|
||||||
thread_creation_code = "\n".join([
|
|
||||||
f" task_runtime.thread.{task_var.get().lower()} = osThreadNew(Task_{task_var.get()}, NULL, &attr_{task_var.get().lower()});"
|
|
||||||
for task_var, _ in self.task_vars # 解包元组
|
|
||||||
])
|
|
||||||
|
|
||||||
# 替换模板中的占位符
|
|
||||||
init_content = template_content.replace("{{thread_creation_code}}", thread_creation_code)
|
|
||||||
|
|
||||||
# 如果存在 /* USER MESSAGE BEGIN */ 区域内容,则保留
|
|
||||||
if existing_msgq_content:
|
|
||||||
init_content = re.sub(
|
|
||||||
r"/\* USER MESSAGE BEGIN \*/\s*.*?\s*/\* USER MESSAGE END \*/",
|
|
||||||
f"/* USER MESSAGE BEGIN */\n {existing_msgq_content}\n /* USER MESSAGE END */",
|
|
||||||
init_content,
|
|
||||||
flags=re.DOTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(generated_file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(init_content)
|
|
||||||
|
|
||||||
print(f"已成功生成 {generated_file_path} 文件!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"生成 init.c 文件时出错: {e}")
|
|
||||||
|
|
||||||
# 修改 generate_action 方法
|
|
||||||
|
|
||||||
def generate_action(self):
|
|
||||||
def task():
|
|
||||||
# 检查并创建目录(与 FreeRTOS 状态无关的模块始终创建)
|
|
||||||
self.create_directories()
|
|
||||||
|
|
||||||
# 复制 .gitignore 文件
|
|
||||||
if self.add_gitignore_var.get():
|
|
||||||
self.copy_file_from_repo(".gitignore", ".gitignore")
|
|
||||||
|
|
||||||
# 如果启用了 FreeRTOS,复制相关文件
|
|
||||||
if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
|
|
||||||
self.copy_file_from_repo("src/freertos.c", os.path.join("Core", "Src", "freertos.c"))
|
|
||||||
|
|
||||||
# 定义需要处理的文件夹(与 FreeRTOS 状态无关)
|
|
||||||
folders = ["bsp", "component", "device", "module"]
|
|
||||||
|
|
||||||
# 遍历每个文件夹,复制选中的 .h 和 .c 文件
|
|
||||||
for folder in folders:
|
|
||||||
folder_dir = os.path.join(REPO_DIR, "User", folder)
|
|
||||||
if not os.path.exists(folder_dir):
|
|
||||||
continue # 如果文件夹不存在,跳过
|
|
||||||
|
|
||||||
for file_name in os.listdir(folder_dir):
|
|
||||||
file_base, file_ext = os.path.splitext(file_name)
|
|
||||||
if file_ext not in [".h", ".c"]:
|
|
||||||
continue # 只处理 .h 和 .c 文件
|
|
||||||
|
|
||||||
# 强制复制与文件夹同名的文件
|
|
||||||
if file_base == folder:
|
|
||||||
src_path = os.path.join(folder_dir, file_name)
|
|
||||||
dest_path = os.path.join("User", folder, file_name)
|
|
||||||
self.copy_file_from_repo(src_path, dest_path)
|
|
||||||
print(f"强制复制与文件夹同名的文件: {file_name}")
|
|
||||||
continue # 跳过后续检查,直接复制
|
|
||||||
|
|
||||||
# 检查是否选中了对应的文件
|
|
||||||
if file_base in self.header_file_vars and self.header_file_vars[file_base].get():
|
|
||||||
src_path = os.path.join(folder_dir, file_name)
|
|
||||||
dest_path = os.path.join("User", folder, file_name)
|
|
||||||
self.copy_file_from_repo(src_path, dest_path)
|
|
||||||
|
|
||||||
# 如果启用了 FreeRTOS,执行任务相关的生成逻辑
|
|
||||||
if self.ioc_data and self.check_freertos_enabled(self.ioc_data):
|
|
||||||
# 修改 user_task.c 文件
|
|
||||||
self.modify_user_task_file()
|
|
||||||
|
|
||||||
# 生成 user_task.h 文件
|
|
||||||
self.generate_user_task_header()
|
|
||||||
|
|
||||||
# 生成 init.c 文件
|
|
||||||
self.generate_init_file()
|
|
||||||
|
|
||||||
# 生成 task.c 文件
|
|
||||||
self.generate_task_files()
|
|
||||||
|
|
||||||
# 自动配置环境
|
|
||||||
if self.auto_configure_var.get():
|
|
||||||
|
|
||||||
self.auto_configure_environment()
|
|
||||||
|
|
||||||
|
|
||||||
threading.Thread(target=task).start()
|
|
||||||
|
|
||||||
# 程序关闭时清理
|
|
||||||
def on_closing(self, root):
|
|
||||||
self.delete_repo()
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
# 程序入口
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = MRobotApp()
|
app = QApplication(sys.argv)
|
||||||
app.initialize()
|
app.setAttribute(Qt.AA_DontCreateNativeWidgetSiblings) # 避免创建原生窗口小部件的兄弟窗口
|
||||||
|
|
||||||
|
w = MainWindow()
|
||||||
|
|
||||||
|
sys.exit(app.exec_()) # 启动应用程序并进入主事件循环
|
||||||
|
# 注意:在 PyQt5 中,exec_() 是一个阻塞调用,直到应用程序退出。
|
||||||
|
44
README.md
44
README.md
@ -3,7 +3,7 @@
|
|||||||
更加高效快捷的 STM32 开发架构,诞生于 Robocon 和 Robomaster,但绝不仅限于此。
|
更加高效快捷的 STM32 开发架构,诞生于 Robocon 和 Robomaster,但绝不仅限于此。
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./image/MRobot.jpeg" height="100" alt="MRobot Logo">
|
<img src="./img/MRobot.png" height="100" alt="MRobot Logo">
|
||||||
<p>是时候使用更简洁的方式开发单片机了</p>
|
<p>是时候使用更简洁的方式开发单片机了</p>
|
||||||
<p>
|
<p>
|
||||||
<!-- <img src="https://img.shields.io/github/license/xrobot-org/XRobot.svg" alt="License">
|
<!-- <img src="https://img.shields.io/github/license/xrobot-org/XRobot.svg" alt="License">
|
||||||
@ -88,3 +88,45 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pyinstaller --onefile --windowed
|
pyinstaller --onefile --windowed
|
||||||
|
pyinstaller MR_Toolbox.py --onefile --noconsole --icon=img\M.ico --add-data "mr_tool_img\MRobot.png;mr_tool_img"
|
||||||
|
|
||||||
|
pyinstaller MR_Tool.py --onefile --noconsole --icon=img\M.ico --add-data "mr_tool_img\MRobot.png;mr_tool_img" --add-data "src;src" --add-data "User;User"
|
||||||
|
|
||||||
|
pyinstaller --noconfirm --onefile --windowed ^
|
||||||
|
--add-data "User_code;User_code" ^
|
||||||
|
--add-data "img;img" ^
|
||||||
|
--icon "img\M.ico" ^
|
||||||
|
MRobot.py
|
||||||
|
|
||||||
|
|
||||||
|
pyinstaller --noconfirm --onefile --windowed --add-data "img;img" --add-data "User_code;User_code" --add-data "mech_lib;mech_lib" --icon=img/MRobot.ico MRobot.py
|
||||||
|
|
||||||
|
python3 -m PyInstaller --noconfirm --onefile --windowed \
|
||||||
|
--add-data "img:img" \
|
||||||
|
--add-data "User_code:User_code" \
|
||||||
|
--add-data "mech_lib:mech_lib" \
|
||||||
|
--icon=img/MRobot.ico \
|
||||||
|
MRobot.py
|
||||||
|
|
||||||
|
|
||||||
|
python3 -m PyInstaller --windowed --name MRobot \
|
||||||
|
--add-data "img:MRobot.app/Contents/Resources/img" \
|
||||||
|
--add-data "User_code:MRobot.app/Contents/Resources/User_code" \
|
||||||
|
--add-data "mech_lib:MRobot.app/Contents/Resources/mech_lib" \
|
||||||
|
MRobot.py
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pyinstaller --noconfirm --onefile --windowed --add-data "img;img" --add-data "User_code;User_code" --icon=img/M.ico MRobot.py
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
@ -1,48 +0,0 @@
|
|||||||
/* Includes ----------------------------------------------------------------- */
|
|
||||||
#include "key_gpio.h"
|
|
||||||
#include "bsp.h"
|
|
||||||
#include <gpio.h>
|
|
||||||
|
|
||||||
/* Private define ----------------------------------------------------------- */
|
|
||||||
/* Private macro ------------------------------------------------------------ */
|
|
||||||
/* Private typedef ---------------------------------------------------------- */
|
|
||||||
/* Private variables -------------------------------------------------------- */
|
|
||||||
|
|
||||||
static uint32_t key_stats; // 使用位掩码记录每个通道的状态,最多支持32LED
|
|
||||||
/* 按键配置表(根据实际硬件修改) */
|
|
||||||
static const BSP_Key_Config_t KEY_CONFIGS[] = {
|
|
||||||
{GPIOA, GPIO_PIN_7, GPIO_PIN_SET}, // KEY1按下时电平为高
|
|
||||||
{GPIOA, GPIO_PIN_9, GPIO_PIN_SET}, // KEY2按下时电平为低
|
|
||||||
// 添加更多按键...
|
|
||||||
};
|
|
||||||
|
|
||||||
#define KEY_COUNT (sizeof(KEY_CONFIGS)/sizeof(KEY_CONFIGS[0])
|
|
||||||
|
|
||||||
|
|
||||||
//读取按键状态(带消抖)
|
|
||||||
int8_t BSP_Key_Read(BSP_Key_Channel_t ch) {
|
|
||||||
static uint32_t last_press_time[BSP_KEY_COUNT] = {0}; //上次按下时间
|
|
||||||
const uint32_t debounce_ms = 20; //按键消抖时间
|
|
||||||
const uint32_t long_press_ms = 2000; //按键长按时间
|
|
||||||
|
|
||||||
if(ch >= BSP_KEY_COUNT) return BSP_KEY_RELEASED ;
|
|
||||||
|
|
||||||
const BSP_Key_Config_t *cfg = &KEY_CONFIGS[ch];
|
|
||||||
GPIO_PinState state = HAL_GPIO_ReadPin(cfg->port, cfg->pin);
|
|
||||||
|
|
||||||
if(state == cfg->active_level) {
|
|
||||||
uint32_t now = HAL_GetTick(); //用于记录按键按下时间(这里比较state是为了方便适应不同有效电平做出修改的,也可以改成直接检测电平高低)
|
|
||||||
|
|
||||||
//消抖检测(只有按下超过20ms才被认为按下)
|
|
||||||
if((now - last_press_time[ch]) > debounce_ms) {
|
|
||||||
//长按检测(只有被按下超过2000ms才被认为是长按,根据实际情况可做出修改)
|
|
||||||
if((now - last_press_time[ch]) > long_press_ms) {
|
|
||||||
return BSP_KEY_LONG_PRESS;
|
|
||||||
}
|
|
||||||
return BSP_KEY_PRESSED;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
last_press_time[ch] = HAL_GetTick();
|
|
||||||
}
|
|
||||||
return BSP_KEY_RELEASED;
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
/* Includes ----------------------------------------------------------------- */
|
|
||||||
#include <stdint.h>
|
|
||||||
#include "main.h"
|
|
||||||
//#include "key_gpio.h"
|
|
||||||
/* Exported constants ------------------------------------------------------- */
|
|
||||||
/* Exported macro ----------------------------------------------------------- */
|
|
||||||
/* Exported types ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
/* KEY按键状态,设置用 */
|
|
||||||
typedef enum
|
|
||||||
{
|
|
||||||
BSP_KEY_RELEASED, //按键释放
|
|
||||||
BSP_KEY_PRESSED, //按键按下
|
|
||||||
BSP_KEY_LONG_PRESS, //按键长按
|
|
||||||
} BSP_KEY_Status_t;
|
|
||||||
|
|
||||||
|
|
||||||
/* 按键通道定义 */
|
|
||||||
typedef enum {
|
|
||||||
BSP_KEY_1,
|
|
||||||
BSP_KEY_2,
|
|
||||||
/* 可根据需要扩展 */
|
|
||||||
BSP_KEY_COUNT
|
|
||||||
} BSP_Key_Channel_t;
|
|
||||||
|
|
||||||
|
|
||||||
/* 按键硬件配置结构体 */
|
|
||||||
typedef struct {
|
|
||||||
GPIO_TypeDef *port; // GPIO端口
|
|
||||||
uint16_t pin; // 引脚编号
|
|
||||||
uint8_t active_level; // 有效电平(GPIO_PIN_SET/RESET)
|
|
||||||
} BSP_Key_Config_t;
|
|
||||||
|
|
||||||
int8_t BSP_Key_Read(BSP_Key_Channel_t ch);
|
|
@ -1,7 +0,0 @@
|
|||||||
/*
|
|
||||||
自定义的数学运算。
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "user_math.h"
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
自定义的数学运算。
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
|
|
||||||
#include <math.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#ifndef M_PI
|
|
||||||
#define M_PI 3.14159265358979323846f
|
|
||||||
#endif
|
|
@ -1,3 +0,0 @@
|
|||||||
oled_i2c,bsp/i2c
|
|
||||||
bmp280_i2c,bsp/i2c
|
|
||||||
pc_uart,bsp/uart
|
|
|
@ -1,59 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <cmsis_os2.h>
|
|
||||||
#include "FreeRTOS.h"
|
|
||||||
#include "task.h"
|
|
||||||
|
|
||||||
// 定义任务运行时结构体
|
|
||||||
typedef struct {
|
|
||||||
/* 各任务,也可以叫做线程 */
|
|
||||||
struct {
|
|
||||||
{{thread_definitions}}
|
|
||||||
} thread;
|
|
||||||
|
|
||||||
/* USER MESSAGE BEGIN */
|
|
||||||
|
|
||||||
struct {
|
|
||||||
osMessageQueueId_t user_msg; /* 用户自定义任务消息队列 */
|
|
||||||
} msgq;
|
|
||||||
|
|
||||||
/* USER MESSAGE END */
|
|
||||||
|
|
||||||
struct {
|
|
||||||
{{freq_definitions}}
|
|
||||||
} freq; /* 任务运行频率 */
|
|
||||||
|
|
||||||
struct {
|
|
||||||
{{last_up_time_definitions}}
|
|
||||||
} last_up_time; /* 任务最近运行时间 */
|
|
||||||
} Task_Runtime_t;
|
|
||||||
|
|
||||||
// 任务频率
|
|
||||||
{{task_frequency_definitions}}
|
|
||||||
// 任务初始化延时
|
|
||||||
#define TASK_INIT_DELAY (100u)
|
|
||||||
{{task_init_delay_definitions}}
|
|
||||||
|
|
||||||
// 任务句柄
|
|
||||||
typedef struct {
|
|
||||||
{{task_handle_definitions}}
|
|
||||||
} Task_Handles_t;
|
|
||||||
|
|
||||||
// 任务运行时结构体
|
|
||||||
extern Task_Runtime_t task_runtime;
|
|
||||||
|
|
||||||
// 初始化任务句柄
|
|
||||||
extern const osThreadAttr_t attr_init;
|
|
||||||
{{task_attr_declarations}}
|
|
||||||
|
|
||||||
// 任务函数声明
|
|
||||||
void Task_Init(void *argument);
|
|
||||||
{{task_function_declarations}}
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/about_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/about_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/ai_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/ai_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/data_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/data_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/function_fit_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/function_fit_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/home_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/home_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main_window.cpython-39.pyc
Normal file
BIN
app/__pycache__/main_window.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/mini_tool_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/mini_tool_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/part_library_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/part_library_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/serial_terminal_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/serial_terminal_interface.cpython-39.pyc
Normal file
Binary file not shown.
65
app/about_interface.py
Normal file
65
app/about_interface.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox
|
||||||
|
from PyQt5.QtCore import Qt, QUrl
|
||||||
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
|
||||||
|
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon
|
||||||
|
from qfluentwidgets import InfoBar, InfoBarPosition, SubtitleLabel
|
||||||
|
|
||||||
|
from .function_fit_interface import FunctionFitInterface
|
||||||
|
from app.tools.check_update import check_update
|
||||||
|
|
||||||
|
__version__ = "1.0.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
|
||||||
|
)
|
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
|
807
app/data_interface.py
Normal file
807
app/data_interface.py
Normal file
@ -0,0 +1,807 @@
|
|||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QStackedLayout, QFileDialog, QHeaderView
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtWidgets import QTreeWidgetItem as TreeItem
|
||||||
|
from qfluentwidgets import TitleLabel, BodyLabel, SubtitleLabel, StrongBodyLabel, HorizontalSeparator, PushButton, TreeWidget, InfoBar,FluentIcon
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
import textwrap
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
class IocConfig:
|
||||||
|
def __init__(self, ioc_path):
|
||||||
|
self.ioc_path = ioc_path
|
||||||
|
self.config = {}
|
||||||
|
self._parse()
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
with open(self.ioc_path, encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
self.config[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
def is_freertos_enabled(self):
|
||||||
|
ip_keys = [k for k in self.config if k.startswith('Mcu.IP')]
|
||||||
|
for k in ip_keys:
|
||||||
|
if self.config[k] == 'FREERTOS':
|
||||||
|
return True
|
||||||
|
for k in self.config:
|
||||||
|
if k.startswith('FREERTOS.'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
class DataInterface(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self.setObjectName("dataInterface")
|
||||||
|
|
||||||
|
# 属性初始化
|
||||||
|
self.project_path = ""
|
||||||
|
self.project_name = ""
|
||||||
|
self.ioc_file = ""
|
||||||
|
self.freertos_enabled = False # 新增属性
|
||||||
|
|
||||||
|
# 主布局
|
||||||
|
self.stacked_layout = QStackedLayout(self)
|
||||||
|
self.setLayout(self.stacked_layout)
|
||||||
|
|
||||||
|
# --- 页面1:工程路径选择 ---
|
||||||
|
self.select_widget = QWidget()
|
||||||
|
outer_layout = QVBoxLayout(self.select_widget)
|
||||||
|
outer_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
outer_layout.addStretch()
|
||||||
|
|
||||||
|
# 直接用布局和控件,无卡片
|
||||||
|
content_layout = QVBoxLayout()
|
||||||
|
content_layout.setSpacing(28)
|
||||||
|
content_layout.setContentsMargins(48, 48, 48, 48)
|
||||||
|
|
||||||
|
# 主标题
|
||||||
|
title = TitleLabel("MRobot 代码生成")
|
||||||
|
title.setAlignment(Qt.AlignCenter)
|
||||||
|
content_layout.addWidget(title)
|
||||||
|
|
||||||
|
# 副标题
|
||||||
|
subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径(.ico所在的目录),然后开启代码之旅!")
|
||||||
|
subtitle.setAlignment(Qt.AlignCenter)
|
||||||
|
content_layout.addWidget(subtitle)
|
||||||
|
|
||||||
|
# 简要说明
|
||||||
|
desc = BodyLabel("支持自动配置和生成任务,自主选择模块代码倒入,自动识别cubemx配置!")
|
||||||
|
desc.setAlignment(Qt.AlignCenter)
|
||||||
|
content_layout.addWidget(desc)
|
||||||
|
|
||||||
|
content_layout.addSpacing(18)
|
||||||
|
|
||||||
|
# 选择项目路径按钮
|
||||||
|
self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径")
|
||||||
|
self.choose_btn.setFixedWidth(200)
|
||||||
|
self.choose_btn.clicked.connect(self.choose_project_folder)
|
||||||
|
content_layout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
# 更新代码库按钮
|
||||||
|
self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
|
||||||
|
self.update_template_btn.setFixedWidth(200)
|
||||||
|
self.update_template_btn.clicked.connect(self.update_user_template)
|
||||||
|
content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
content_layout.addSpacing(10)
|
||||||
|
content_layout.addStretch()
|
||||||
|
|
||||||
|
|
||||||
|
outer_layout.addLayout(content_layout)
|
||||||
|
outer_layout.addStretch()
|
||||||
|
|
||||||
|
self.stacked_layout.addWidget(self.select_widget)
|
||||||
|
|
||||||
|
|
||||||
|
# --- 页面2:主配置页面 ---
|
||||||
|
self.config_widget = QWidget()
|
||||||
|
main_layout = QVBoxLayout(self.config_widget)
|
||||||
|
main_layout.setContentsMargins(32, 32, 32, 32)
|
||||||
|
main_layout.setSpacing(18)
|
||||||
|
|
||||||
|
# 顶部项目信息
|
||||||
|
info_layout = QHBoxLayout()
|
||||||
|
self.back_btn = PushButton(FluentIcon.SKIP_BACK, "返回")
|
||||||
|
self.back_btn.setFixedWidth(90)
|
||||||
|
self.back_btn.clicked.connect(self.back_to_select)
|
||||||
|
info_layout.addWidget(self.back_btn) # 返回按钮放最左
|
||||||
|
self.project_name_label = StrongBodyLabel()
|
||||||
|
self.project_path_label = BodyLabel()
|
||||||
|
self.ioc_file_label = BodyLabel()
|
||||||
|
self.freertos_label = BodyLabel()
|
||||||
|
info_layout.addWidget(self.project_name_label)
|
||||||
|
info_layout.addWidget(self.project_path_label)
|
||||||
|
info_layout.addWidget(self.ioc_file_label)
|
||||||
|
info_layout.addWidget(self.freertos_label)
|
||||||
|
info_layout.addStretch()
|
||||||
|
main_layout.addLayout(info_layout)
|
||||||
|
main_layout.addWidget(HorizontalSeparator())
|
||||||
|
|
||||||
|
# ======= 新增:左右分栏 =======
|
||||||
|
content_hbox = QHBoxLayout()
|
||||||
|
content_hbox.setSpacing(24)
|
||||||
|
|
||||||
|
# 左侧:文件树
|
||||||
|
left_vbox = QVBoxLayout()
|
||||||
|
left_vbox.addWidget(SubtitleLabel("用户代码模块选择"))
|
||||||
|
left_vbox.addWidget(HorizontalSeparator())
|
||||||
|
self.file_tree = TreeWidget()
|
||||||
|
self.file_tree.setHeaderLabels(["模块名"])
|
||||||
|
self.file_tree.setSelectionMode(self.file_tree.ExtendedSelection)
|
||||||
|
self.file_tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
||||||
|
self.file_tree.setCheckedColor("#0078d4", "#2d7d9a")
|
||||||
|
self.file_tree.setBorderRadius(8)
|
||||||
|
self.file_tree.setBorderVisible(True)
|
||||||
|
left_vbox.addWidget(self.file_tree, stretch=1)
|
||||||
|
content_hbox.addLayout(left_vbox, 2)
|
||||||
|
|
||||||
|
# 右侧:操作按钮和说明
|
||||||
|
right_vbox = QVBoxLayout()
|
||||||
|
right_vbox.setSpacing(18)
|
||||||
|
right_vbox.addWidget(SubtitleLabel("操作区"))
|
||||||
|
right_vbox.addWidget(HorizontalSeparator())
|
||||||
|
|
||||||
|
# 操作按钮分组
|
||||||
|
btn_group = QVBoxLayout()
|
||||||
|
# 自动环境配置按钮
|
||||||
|
self.env_btn = PushButton("自动环境配置")
|
||||||
|
self.env_btn.setFixedWidth(200)
|
||||||
|
self.env_btn.setToolTip("自动检测并配置常用开发环境(功能开发中)")
|
||||||
|
self.env_btn.clicked.connect(self.auto_env_config)
|
||||||
|
btn_group.addWidget(self.env_btn)
|
||||||
|
# FreeRTOS相关按钮
|
||||||
|
self.freertos_task_btn = PushButton("自动生成FreeRTOS任务")
|
||||||
|
self.freertos_task_btn.setFixedWidth(200)
|
||||||
|
self.freertos_task_btn.setToolTip("自动在 freertos.c 中插入任务创建代码")
|
||||||
|
self.freertos_task_btn.clicked.connect(self.on_freertos_task_btn_clicked)
|
||||||
|
btn_group.addWidget(self.freertos_task_btn)
|
||||||
|
self.task_code_btn = PushButton("配置并生成任务代码")
|
||||||
|
self.task_code_btn.setFixedWidth(200)
|
||||||
|
self.task_code_btn.setToolTip("配置任务参数并一键生成任务代码文件")
|
||||||
|
self.task_code_btn.clicked.connect(self.on_task_code_btn_clicked)
|
||||||
|
btn_group.addWidget(self.task_code_btn)
|
||||||
|
self.generate_btn = PushButton(FluentIcon.CODE, "生成代码")
|
||||||
|
self.generate_btn.setFixedWidth(200)
|
||||||
|
self.generate_btn.setToolTip("将选中的用户模块代码复制到工程 User 目录")
|
||||||
|
self.generate_btn.clicked.connect(self.generate_code)
|
||||||
|
btn_group.addWidget(self.generate_btn)
|
||||||
|
btn_group.addSpacing(10)
|
||||||
|
right_vbox.addLayout(btn_group)
|
||||||
|
right_vbox.addStretch()
|
||||||
|
|
||||||
|
content_hbox.addLayout(right_vbox, 1)
|
||||||
|
main_layout.addLayout(content_hbox, stretch=1)
|
||||||
|
self.stacked_layout.addWidget(self.config_widget)
|
||||||
|
self.file_tree.itemChanged.connect(self.on_tree_item_changed)
|
||||||
|
|
||||||
|
def auto_env_config(self):
|
||||||
|
InfoBar.info(
|
||||||
|
title="敬请期待",
|
||||||
|
content="自动环境配置功能暂未实现,等待后续更新。",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
|
||||||
|
def choose_project_folder(self):
|
||||||
|
folder = QFileDialog.getExistingDirectory(self, "请选择代码项目文件夹")
|
||||||
|
if not folder:
|
||||||
|
return
|
||||||
|
ioc_files = [f for f in os.listdir(folder) if f.endswith('.ioc')]
|
||||||
|
if not ioc_files:
|
||||||
|
InfoBar.warning(
|
||||||
|
title="提示",
|
||||||
|
content="未找到.ioc文件,请确认项目文件夹。",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.project_path = folder
|
||||||
|
self.project_name = os.path.basename(folder)
|
||||||
|
self.ioc_file = os.path.join(folder, ioc_files[0])
|
||||||
|
self.show_config_page()
|
||||||
|
|
||||||
|
def show_config_page(self):
|
||||||
|
# 更新项目信息
|
||||||
|
self.project_name_label.setText(f"项目名称: {self.project_name}")
|
||||||
|
self.project_path_label.setText(f"项目路径: {self.project_path}")
|
||||||
|
# self.ioc_file_label.setText(f"IOC 文件: {self.ioc_file}")
|
||||||
|
try:
|
||||||
|
ioc = IocConfig(self.ioc_file)
|
||||||
|
self.freertos_enabled = ioc.is_freertos_enabled() # 记录状态
|
||||||
|
freertos_status = "已启用" if self.freertos_enabled else "未启用"
|
||||||
|
self.freertos_label.setText(f"FreeRTOS: {freertos_status}")
|
||||||
|
# self.freertos_task_btn.setEnabled(self.freertos_enabled)
|
||||||
|
except Exception as e:
|
||||||
|
self.freertos_label.setText(f"IOC解析失败: {e}")
|
||||||
|
self.freertos_task_btn.hide()
|
||||||
|
self.freertos_enabled = False
|
||||||
|
self.show_user_code_files()
|
||||||
|
self.stacked_layout.setCurrentWidget(self.config_widget)
|
||||||
|
|
||||||
|
def on_freertos_task_btn_clicked(self):
|
||||||
|
if not self.freertos_enabled:
|
||||||
|
InfoBar.warning(
|
||||||
|
title="未开启 FreeRTOS",
|
||||||
|
content="请先在 CubeMX 中开启 FreeRTOS!",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.generate_freertos_task()
|
||||||
|
|
||||||
|
def on_task_code_btn_clicked(self):
|
||||||
|
if not self.freertos_enabled:
|
||||||
|
InfoBar.warning(
|
||||||
|
title="未开启 FreeRTOS",
|
||||||
|
content="请先在 CubeMX 中开启 FreeRTOS!",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.open_task_config_dialog()
|
||||||
|
|
||||||
|
def back_to_select(self):
|
||||||
|
self.stacked_layout.setCurrentWidget(self.select_widget)
|
||||||
|
|
||||||
|
def update_user_template(self):
|
||||||
|
url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
|
||||||
|
local_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../assets/User_code")
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
z = zipfile.ZipFile(io.BytesIO(resp.content))
|
||||||
|
if os.path.exists(local_dir):
|
||||||
|
shutil.rmtree(local_dir)
|
||||||
|
for member in z.namelist():
|
||||||
|
rel_path = os.path.relpath(member, z.namelist()[0])
|
||||||
|
if rel_path == ".":
|
||||||
|
continue
|
||||||
|
target_path = os.path.join(local_dir, rel_path)
|
||||||
|
if member.endswith('/'):
|
||||||
|
os.makedirs(target_path, exist_ok=True)
|
||||||
|
else:
|
||||||
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
||||||
|
with open(target_path, "wb") as f:
|
||||||
|
f.write(z.read(member))
|
||||||
|
InfoBar.success(
|
||||||
|
title="更新成功",
|
||||||
|
content="用户模板已更新到最新版本!",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
InfoBar.error(
|
||||||
|
title="更新失败",
|
||||||
|
content=f"用户模板更新失败: {e}",
|
||||||
|
parent=self,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
|
||||||
|
def show_user_code_files(self):
|
||||||
|
self.file_tree.clear()
|
||||||
|
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../assets/User_code")
|
||||||
|
user_dir = os.path.join(self.project_path, "User")
|
||||||
|
sub_dirs = ["bsp", "component", "device", "module"]
|
||||||
|
|
||||||
|
# 读取所有 describe.csv 和 dependencies.csv
|
||||||
|
describe_map = {}
|
||||||
|
dependencies_map = {}
|
||||||
|
for sub in sub_dirs:
|
||||||
|
dir_path = os.path.join(base_dir, sub)
|
||||||
|
if not os.path.isdir(dir_path):
|
||||||
|
continue
|
||||||
|
# describe
|
||||||
|
desc_path = os.path.join(dir_path, "describe.csv")
|
||||||
|
if os.path.exists(desc_path):
|
||||||
|
with open(desc_path, encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
if "," in line:
|
||||||
|
k, v = line.strip().split(",", 1)
|
||||||
|
describe_map[f"{sub}/{k.strip()}"] = v.strip()
|
||||||
|
# dependencies
|
||||||
|
dep_path = os.path.join(dir_path, "dependencies.csv")
|
||||||
|
if os.path.exists(dep_path):
|
||||||
|
with open(dep_path, encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
if "," in line:
|
||||||
|
a, b = line.strip().split(",", 1)
|
||||||
|
dependencies_map.setdefault(f"{sub}/{a.strip()}", []).append(b.strip())
|
||||||
|
|
||||||
|
self._describe_map = describe_map
|
||||||
|
self._dependencies_map = dependencies_map
|
||||||
|
|
||||||
|
self.file_tree.setHeaderLabels(["模块名", "描述"])
|
||||||
|
self.file_tree.setSelectionMode(self.file_tree.ExtendedSelection)
|
||||||
|
self.file_tree.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||||
|
self.file_tree.header().setSectionResizeMode(1, QHeaderView.Stretch) # 描述列自适应
|
||||||
|
self.file_tree.setCheckedColor("#0078d4", "#2d7d9a")
|
||||||
|
self.file_tree.setBorderRadius(8)
|
||||||
|
self.file_tree.setBorderVisible(True)
|
||||||
|
|
||||||
|
for sub in sub_dirs:
|
||||||
|
dir_path = os.path.join(base_dir, sub)
|
||||||
|
if not os.path.isdir(dir_path):
|
||||||
|
continue
|
||||||
|
group_item = TreeItem([sub, ""])
|
||||||
|
self.file_tree.addTopLevelItem(group_item)
|
||||||
|
has_file = False
|
||||||
|
for root, _, files in os.walk(dir_path):
|
||||||
|
rel_root = os.path.relpath(root, base_dir)
|
||||||
|
for f in sorted(files):
|
||||||
|
if f.endswith(".c"):
|
||||||
|
mod_name = os.path.splitext(f)[0]
|
||||||
|
rel_c = os.path.join(rel_root, f)
|
||||||
|
key = f"{rel_root}/{mod_name}".replace("\\", "/")
|
||||||
|
desc = describe_map.get(key, "")
|
||||||
|
file_item = TreeItem([mod_name, desc])
|
||||||
|
file_item.setFlags(file_item.flags() | Qt.ItemIsUserCheckable)
|
||||||
|
file_item.setData(0, Qt.UserRole, rel_c)
|
||||||
|
file_item.setData(0, Qt.UserRole + 1, key) # 存模块key
|
||||||
|
file_item.setToolTip(1, desc)
|
||||||
|
file_item.setTextAlignment(1, Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
|
group_item.addChild(file_item)
|
||||||
|
dst_c = os.path.join(user_dir, rel_c)
|
||||||
|
if os.path.exists(dst_c):
|
||||||
|
file_item.setCheckState(0, Qt.Unchecked)
|
||||||
|
file_item.setText(0, f"{mod_name}(已存在)")
|
||||||
|
file_item.setForeground(0, Qt.gray)
|
||||||
|
else:
|
||||||
|
file_item.setCheckState(0, Qt.Unchecked)
|
||||||
|
group_item.addChild(file_item)
|
||||||
|
has_file = True
|
||||||
|
if not has_file:
|
||||||
|
empty_item = TreeItem(["(无 .c 文件)", ""])
|
||||||
|
group_item.addChild(empty_item)
|
||||||
|
self.file_tree.expandAll()
|
||||||
|
|
||||||
|
# 勾选依赖自动勾选
|
||||||
|
def on_tree_item_changed(self, item, column):
|
||||||
|
if column != 0:
|
||||||
|
return
|
||||||
|
if item.childCount() > 0:
|
||||||
|
return # 只处理叶子
|
||||||
|
if item.checkState(0) == Qt.Checked:
|
||||||
|
key = item.data(0, Qt.UserRole + 1)
|
||||||
|
deps = self._dependencies_map.get(key, [])
|
||||||
|
if deps:
|
||||||
|
checked = []
|
||||||
|
root = self.file_tree.invisibleRootItem()
|
||||||
|
for i in range(root.childCount()):
|
||||||
|
group = root.child(i)
|
||||||
|
for j in range(group.childCount()):
|
||||||
|
child = group.child(j)
|
||||||
|
ckey = child.data(0, Qt.UserRole + 1)
|
||||||
|
if ckey in deps and child.checkState(0) != Qt.Checked:
|
||||||
|
child.setCheckState(0, Qt.Checked)
|
||||||
|
checked.append(ckey)
|
||||||
|
if checked:
|
||||||
|
descs = [self._describe_map.get(dep, dep) for dep in checked]
|
||||||
|
InfoBar.info(
|
||||||
|
title="依赖自动勾选",
|
||||||
|
content="已自动勾选依赖模块: " + ",".join(descs),
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_checked_files(self):
|
||||||
|
files = []
|
||||||
|
def _traverse(item):
|
||||||
|
for i in range(item.childCount()):
|
||||||
|
child = item.child(i)
|
||||||
|
if child.childCount() == 0 and child.checkState(0) == Qt.Checked:
|
||||||
|
files.append(child.data(0, Qt.UserRole))
|
||||||
|
_traverse(child)
|
||||||
|
root = self.file_tree.invisibleRootItem()
|
||||||
|
for i in range(root.childCount()):
|
||||||
|
_traverse(root.child(i))
|
||||||
|
return files
|
||||||
|
|
||||||
|
def generate_code(self):
|
||||||
|
import shutil
|
||||||
|
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../assets/User_code")
|
||||||
|
user_dir = os.path.join(self.project_path, "User")
|
||||||
|
copied = []
|
||||||
|
files = self.get_checked_files()
|
||||||
|
skipped = []
|
||||||
|
for rel_c in files:
|
||||||
|
rel_h = rel_c[:-2] + ".h"
|
||||||
|
src_c = os.path.join(base_dir, rel_c)
|
||||||
|
src_h = os.path.join(base_dir, rel_h)
|
||||||
|
dst_c = os.path.join(user_dir, rel_c)
|
||||||
|
dst_h = os.path.join(user_dir, rel_h)
|
||||||
|
# 如果目标文件已存在则跳过
|
||||||
|
if os.path.exists(dst_c):
|
||||||
|
skipped.append(dst_c)
|
||||||
|
else:
|
||||||
|
os.makedirs(os.path.dirname(dst_c), exist_ok=True)
|
||||||
|
shutil.copy2(src_c, dst_c)
|
||||||
|
copied.append(dst_c)
|
||||||
|
if os.path.exists(src_h):
|
||||||
|
if os.path.exists(dst_h):
|
||||||
|
skipped.append(dst_h)
|
||||||
|
else:
|
||||||
|
os.makedirs(os.path.dirname(dst_h), exist_ok=True)
|
||||||
|
shutil.copy2(src_h, dst_h)
|
||||||
|
copied.append(dst_h)
|
||||||
|
msg = f"已拷贝 {len(copied)} 个文件到 User 目录"
|
||||||
|
if skipped:
|
||||||
|
msg += f"\n{len(skipped)} 个文件已存在,未覆盖"
|
||||||
|
InfoBar.success(
|
||||||
|
title="生成完成",
|
||||||
|
content=msg,
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
# 生成后刷新文件树,更新标记
|
||||||
|
self.show_user_code_files()
|
||||||
|
|
||||||
|
def generate_freertos_task(self):
|
||||||
|
import re
|
||||||
|
freertos_path = os.path.join(self.project_path, "Core", "Src", "freertos.c")
|
||||||
|
if not os.path.exists(freertos_path):
|
||||||
|
InfoBar.error(
|
||||||
|
title="未找到 freertos.c",
|
||||||
|
content="未找到 Core/Src/freertos.c 文件,请确认工程路径。",
|
||||||
|
parent=self,
|
||||||
|
duration=2500
|
||||||
|
)
|
||||||
|
return
|
||||||
|
with open(freertos_path, "r", encoding="utf-8") as f:
|
||||||
|
code = f.read()
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
error_msgs = []
|
||||||
|
|
||||||
|
# 1. 添加 #include "task/user_task.h"
|
||||||
|
include_line = '#include "task/user_task.h"'
|
||||||
|
if include_line not in code:
|
||||||
|
# 只插入到 USER CODE BEGIN Includes 区域
|
||||||
|
include_pattern = r'(\/\* *USER CODE BEGIN Includes *\*\/\s*)'
|
||||||
|
if re.search(include_pattern, code):
|
||||||
|
code = re.sub(
|
||||||
|
include_pattern,
|
||||||
|
r'\1' + include_line + '\n',
|
||||||
|
code
|
||||||
|
)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
error_msgs.append("未找到 /* USER CODE BEGIN Includes */ 区域,无法插入 include。")
|
||||||
|
|
||||||
|
# 2. 在 /* USER CODE BEGIN RTOS_THREADS */ 区域添加 osThreadNew(Task_Init, NULL, &attr_init);
|
||||||
|
rtos_threads_pattern = r'(\/\* *USER CODE BEGIN RTOS_THREADS *\*\/\s*)(.*?)(\/\* *USER CODE END RTOS_THREADS *\*\/)'
|
||||||
|
match = re.search(rtos_threads_pattern, code, re.DOTALL)
|
||||||
|
task_line = ' osThreadNew(Task_Init, NULL, &attr_init); // 创建初始化任务\n'
|
||||||
|
if match:
|
||||||
|
threads_code = match.group(2)
|
||||||
|
if 'Task_Init' not in threads_code:
|
||||||
|
# 保留原有内容,追加新行
|
||||||
|
new_threads_code = match.group(1) + threads_code + task_line + match.group(3)
|
||||||
|
code = code[:match.start()] + new_threads_code + code[match.end():]
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
error_msgs.append("未找到 /* USER CODE BEGIN RTOS_THREADS */ 区域,无法插入任务创建代码。")
|
||||||
|
|
||||||
|
# 3. 清空 StartDefaultTask 的 USER CODE 区域,只保留 osThreadTerminate
|
||||||
|
sdt_pattern = r'(\/\* *USER CODE BEGIN StartDefaultTask *\*\/\s*)(.*?)(\/\* *USER CODE END StartDefaultTask *\*\/)'
|
||||||
|
match = re.search(sdt_pattern, code, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
if 'osThreadTerminate(osThreadGetId());' not in match.group(2):
|
||||||
|
new_sdt_code = match.group(1) + ' osThreadTerminate(osThreadGetId());\n' + match.group(3)
|
||||||
|
code = code[:match.start()] + new_sdt_code + code[match.end():]
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
error_msgs.append("未找到 /* USER CODE BEGIN StartDefaultTask */ 区域,无法插入终止代码。")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(freertos_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(code)
|
||||||
|
InfoBar.success(
|
||||||
|
title="生成成功",
|
||||||
|
content="FreeRTOS任务代码已自动生成!",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
elif error_msgs:
|
||||||
|
InfoBar.error(
|
||||||
|
title="生成失败",
|
||||||
|
content="\n".join(error_msgs),
|
||||||
|
parent=self,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
InfoBar.info(
|
||||||
|
title="无需修改",
|
||||||
|
content="FreeRTOS任务相关代码已存在,无需重复生成。",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_task_config_dialog(self):
|
||||||
|
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
|
||||||
|
class TaskConfigDialog(QDialog):
|
||||||
|
def __init__(self, parent=None, config_path=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("任务配置")
|
||||||
|
self.resize(900, 420)
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.table = QTableWidget(0, 6)
|
||||||
|
self.table.setHorizontalHeaderLabels(["任务名称", "运行频率", "初始化延迟", "堆栈大小", "任务描述", "频率控制"])
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
|
||||||
|
self.table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents)
|
||||||
|
self.table.setColumnWidth(4, 320) # 任务描述更宽
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
add_btn = QPushButton("添加任务")
|
||||||
|
del_btn = QPushButton("删除选中")
|
||||||
|
ok_btn = QPushButton("生成")
|
||||||
|
cancel_btn = QPushButton("取消")
|
||||||
|
btn_layout.addWidget(add_btn)
|
||||||
|
btn_layout.addWidget(del_btn)
|
||||||
|
btn_layout.addStretch()
|
||||||
|
btn_layout.addWidget(ok_btn)
|
||||||
|
btn_layout.addWidget(cancel_btn)
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
add_btn.clicked.connect(self.add_row)
|
||||||
|
del_btn.clicked.connect(self.del_row)
|
||||||
|
ok_btn.clicked.connect(self.accept)
|
||||||
|
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:
|
||||||
|
row = self.table.rowCount()
|
||||||
|
self.table.insertRow(row)
|
||||||
|
for col, key in enumerate(["name", "frequency", "delay", "stack", "description"]):
|
||||||
|
item = QTableWidgetItem(str(t.get(key, "")))
|
||||||
|
item.setTextAlignment(Qt.AlignCenter)
|
||||||
|
self.table.setItem(row, col, item)
|
||||||
|
# 新增频率控制复选框
|
||||||
|
freq_ctrl = QCheckBox()
|
||||||
|
freq_ctrl.setChecked(t.get("freq_control", True))
|
||||||
|
self.table.setCellWidget(row, 5, freq_ctrl)
|
||||||
|
except Exception as e:
|
||||||
|
pass # 配置文件损坏时忽略
|
||||||
|
|
||||||
|
def add_row(self):
|
||||||
|
row = self.table.rowCount()
|
||||||
|
self.table.insertRow(row)
|
||||||
|
default_values = [
|
||||||
|
f"Task{row+1}", "500", "0", "256", "不要偷懒,请写清楚每个任务的作用!(如果你看到任务上面是这句话,说明作者是个懒蛋)"
|
||||||
|
]
|
||||||
|
for col, val in enumerate(default_values):
|
||||||
|
item = QTableWidgetItem(val)
|
||||||
|
item.setTextAlignment(Qt.AlignCenter)
|
||||||
|
self.table.setItem(row, col, item)
|
||||||
|
freq_ctrl = QCheckBox()
|
||||||
|
freq_ctrl.setChecked(True)
|
||||||
|
self.table.setCellWidget(row, 5, freq_ctrl)
|
||||||
|
|
||||||
|
def del_row(self):
|
||||||
|
rows = set([i.row() for i in self.table.selectedItems()])
|
||||||
|
for r in sorted(rows, reverse=True):
|
||||||
|
self.table.removeRow(r)
|
||||||
|
|
||||||
|
def get_tasks(self):
|
||||||
|
tasks = []
|
||||||
|
for row in range(self.table.rowCount()):
|
||||||
|
name = self.table.item(row, 0).text().strip()
|
||||||
|
freq = self.table.item(row, 1).text()
|
||||||
|
delay = int(self.table.item(row, 2).text())
|
||||||
|
stack = int(self.table.item(row, 3).text())
|
||||||
|
desc = self.table.item(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"] = int(freq)
|
||||||
|
tasks.append(task)
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
config_path = os.path.join(self.project_path, "User", "task", "config.yaml")
|
||||||
|
dlg = TaskConfigDialog(self, config_path=config_path)
|
||||||
|
if dlg.exec() == QDialog.Accepted:
|
||||||
|
try:
|
||||||
|
tasks = dlg.get_tasks()
|
||||||
|
except Exception as e:
|
||||||
|
InfoBar.error(
|
||||||
|
title="参数错误",
|
||||||
|
content=str(e),
|
||||||
|
parent=self,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not tasks:
|
||||||
|
InfoBar.warning(
|
||||||
|
title="未配置任务",
|
||||||
|
content="请至少添加一个任务!",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.generate_task_code(tasks)
|
||||||
|
InfoBar.success(
|
||||||
|
title="生成成功",
|
||||||
|
content="任务代码已生成到 User/task 目录!",
|
||||||
|
parent=self,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
InfoBar.error(
|
||||||
|
title="生成失败",
|
||||||
|
content=f"任务代码生成失败: {e}",
|
||||||
|
parent=self,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
|
||||||
|
def preserve_user_region(self, 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)
|
||||||
|
|
||||||
|
def generate_task_code(self, task_list):
|
||||||
|
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
template_dir = os.path.join(base_dir, "../assets/User_code/task")
|
||||||
|
output_dir = os.path.join(self.project_path, "User", "task")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
user_task_h_tpl = os.path.join(template_dir, "user_task.h.template")
|
||||||
|
user_task_c_tpl = os.path.join(template_dir, "user_task.c.template")
|
||||||
|
init_c_tpl = os.path.join(template_dir, "init.c.template")
|
||||||
|
task_c_tpl = os.path.join(template_dir, "task.c.template")
|
||||||
|
|
||||||
|
freq_tasks = [t for t in task_list if t.get("freq_control", True)]
|
||||||
|
|
||||||
|
def render_template(path, context):
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
tpl = Template(f.read())
|
||||||
|
return tpl.render(**context)
|
||||||
|
|
||||||
|
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"]:
|
||||||
|
pattern = re.compile(
|
||||||
|
rf"/\*\s*{region}\s*BEGIN\s*\*/(.*?)/\*\s*{region}\s*END\s*\*/",
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
old_match = pattern.search(old_code)
|
||||||
|
if old_match and old_match.group(1).strip():
|
||||||
|
new_user_task_h = self.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 = self.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 = self.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)
|
||||||
|
|
260
app/function_fit_interface.py
Normal file
260
app/function_fit_interface.py
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon
|
||||||
|
from openpyxl import load_workbook, Workbook
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
import plotly.io as pio
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Source Han Sans', 'STHeiti', 'Heiti TC']
|
||||||
|
matplotlib.rcParams['axes.unicode_minus'] = False
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionFitInterface(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setObjectName("functionFitInterface")
|
||||||
|
|
||||||
|
main_layout = QHBoxLayout(self)
|
||||||
|
main_layout.setSpacing(24)
|
||||||
|
|
||||||
|
# 左侧:数据输入区
|
||||||
|
left_layout = QVBoxLayout()
|
||||||
|
left_layout.setSpacing(16)
|
||||||
|
|
||||||
|
self.dataTable = TableWidget(self)
|
||||||
|
self.dataTable.setColumnCount(2)
|
||||||
|
self.dataTable.setHorizontalHeaderLabels(["x", "y"])
|
||||||
|
self.dataTable.setColumnWidth(0, 125)
|
||||||
|
self.dataTable.setColumnWidth(1, 125)
|
||||||
|
left_layout.addWidget(self.dataTable)
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
add_row_btn = PushButton("添加一行")
|
||||||
|
add_row_btn.clicked.connect(self.add_row)
|
||||||
|
|
||||||
|
del_row_btn = PushButton("删除选中行") # 新增按钮
|
||||||
|
del_row_btn.clicked.connect(self.delete_selected_row) # 绑定槽函数
|
||||||
|
btn_layout.addWidget(add_row_btn)
|
||||||
|
|
||||||
|
btn_layout.addWidget(del_row_btn) # 添加到布局
|
||||||
|
left_layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
import_btn = PushButton("导入 Excel")
|
||||||
|
import_btn.clicked.connect(self.import_excel)
|
||||||
|
export_btn = PushButton("导出 Excel")
|
||||||
|
export_btn.clicked.connect(self.export_excel)
|
||||||
|
btn_layout.addWidget(import_btn)
|
||||||
|
btn_layout.addWidget(export_btn)
|
||||||
|
left_layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
self.dataTable.setMinimumWidth(280)
|
||||||
|
self.dataTable.setMaximumWidth(280)
|
||||||
|
main_layout.addLayout(left_layout, 1)
|
||||||
|
|
||||||
|
self.add_row()
|
||||||
|
|
||||||
|
# 右侧:图像展示区
|
||||||
|
right_layout = QVBoxLayout()
|
||||||
|
right_layout.setSpacing(12)
|
||||||
|
right_layout.addWidget(SubtitleLabel("函数图像预览"))
|
||||||
|
|
||||||
|
self.figure = Figure(figsize=(5, 4))
|
||||||
|
self.canvas = FigureCanvas(self.figure)
|
||||||
|
right_layout.addWidget(self.canvas, stretch=1)
|
||||||
|
|
||||||
|
self.resultLabel = BodyLabel("")
|
||||||
|
self.resultLabel.setWordWrap(True) # 自动换行
|
||||||
|
right_layout.addWidget(self.resultLabel)
|
||||||
|
|
||||||
|
|
||||||
|
# 拟合阶数和输出语言选择(合并到同一行)
|
||||||
|
options_layout = QHBoxLayout()
|
||||||
|
self.spinBox = SpinBox()
|
||||||
|
self.spinBox.setRange(1, 10)
|
||||||
|
self.spinBox.setValue(2)
|
||||||
|
options_layout.addWidget(SubtitleLabel("拟合阶数"))
|
||||||
|
options_layout.addWidget(self.spinBox)
|
||||||
|
|
||||||
|
self.langBox = ComboBox()
|
||||||
|
self.langBox.addItems(["C/C++", "Python"])
|
||||||
|
options_layout.addWidget(SubtitleLabel("输出语言"))
|
||||||
|
options_layout.addWidget(self.langBox)
|
||||||
|
|
||||||
|
|
||||||
|
right_layout.addLayout(options_layout)
|
||||||
|
|
||||||
|
|
||||||
|
# 代码显示和复制按钮
|
||||||
|
self.codeLabel = BodyLabel("")
|
||||||
|
self.codeLabel.setWordWrap(True) # 自动换行
|
||||||
|
right_layout.addWidget(self.codeLabel)
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout() # 新增一行布局
|
||||||
|
fit_btn = PushButton(FluentIcon.UNIT,"拟合并绘图")
|
||||||
|
fit_btn.clicked.connect(self.fit_and_plot)
|
||||||
|
btn_layout.addWidget(fit_btn)
|
||||||
|
copy_btn = PushButton(FluentIcon.COPY, "复制代码")
|
||||||
|
copy_btn.clicked.connect(self.copy_code)
|
||||||
|
btn_layout.addWidget(copy_btn)
|
||||||
|
right_layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
|
||||||
|
main_layout.addLayout(right_layout, 2)
|
||||||
|
|
||||||
|
# 默认显示空图像
|
||||||
|
self.figure.clear()
|
||||||
|
ax = self.figure.add_subplot(111)
|
||||||
|
ax.set_xlabel('x')
|
||||||
|
ax.set_ylabel('y')
|
||||||
|
self.canvas.draw()
|
||||||
|
|
||||||
|
def add_row(self):
|
||||||
|
row = self.dataTable.rowCount()
|
||||||
|
self.dataTable.insertRow(row)
|
||||||
|
# 可选:初始化为空字符串
|
||||||
|
self.dataTable.setItem(row, 0, QTableWidgetItem(""))
|
||||||
|
self.dataTable.setItem(row, 1, QTableWidgetItem(""))
|
||||||
|
|
||||||
|
def delete_selected_row(self):
|
||||||
|
selected = self.dataTable.selectedItems()
|
||||||
|
if selected:
|
||||||
|
rows = set(item.row() for item in selected)
|
||||||
|
for row in sorted(rows, reverse=True):
|
||||||
|
self.dataTable.removeRow(row)
|
||||||
|
|
||||||
|
def import_excel(self):
|
||||||
|
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx)")
|
||||||
|
if path:
|
||||||
|
wb = load_workbook(path)
|
||||||
|
ws = wb.active
|
||||||
|
self.dataTable.setRowCount(0)
|
||||||
|
for row_data in ws.iter_rows(min_row=2, values_only=True): # 跳过表头
|
||||||
|
row = self.dataTable.rowCount()
|
||||||
|
self.dataTable.insertRow(row)
|
||||||
|
for col, value in enumerate(row_data[:2]):
|
||||||
|
item = QTableWidgetItem(str(value) if value is not None else "")
|
||||||
|
self.dataTable.setItem(row, col, item)
|
||||||
|
|
||||||
|
|
||||||
|
def export_excel(self):
|
||||||
|
path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)")
|
||||||
|
if path:
|
||||||
|
data = self.parse_data()
|
||||||
|
if data is not None:
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.append(["x", "y"])
|
||||||
|
for row in data:
|
||||||
|
ws.append(row)
|
||||||
|
wb.save(path)
|
||||||
|
|
||||||
|
def parse_data(self):
|
||||||
|
data = []
|
||||||
|
row_count = self.dataTable.rowCount()
|
||||||
|
for row in range(row_count):
|
||||||
|
try:
|
||||||
|
x_item = self.dataTable.item(row, 0)
|
||||||
|
y_item = self.dataTable.item(row, 1)
|
||||||
|
if x_item is None or y_item is None:
|
||||||
|
continue
|
||||||
|
x = float(x_item.text())
|
||||||
|
y = float(y_item.text())
|
||||||
|
data.append([x, y])
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return data if data else None
|
||||||
|
|
||||||
|
def fit_and_plot(self):
|
||||||
|
data = self.parse_data()
|
||||||
|
if not data:
|
||||||
|
self.resultLabel.setText("数据格式错误或为空")
|
||||||
|
self.codeLabel.setText("")
|
||||||
|
return
|
||||||
|
|
||||||
|
x = np.array([d[0] for d in data])
|
||||||
|
y = np.array([d[1] for d in data])
|
||||||
|
degree = self.spinBox.value()
|
||||||
|
coeffs = np.polyfit(x, y, degree)
|
||||||
|
|
||||||
|
# 用更密集的横坐标画拟合曲线
|
||||||
|
x_fit = np.linspace(x.min(), x.max(), 100)
|
||||||
|
y_fit = np.polyval(coeffs, x_fit)
|
||||||
|
|
||||||
|
self.figure.clear()
|
||||||
|
ax = self.figure.add_subplot(111)
|
||||||
|
ax.scatter(x, y, color='blue', label='raw data')
|
||||||
|
ax.plot(x_fit, y_fit, color='red', label=f'Fitted curve')
|
||||||
|
ax.set_title('graph of a function')
|
||||||
|
ax.set_xlabel('x')
|
||||||
|
ax.set_ylabel('y')
|
||||||
|
ax.legend()
|
||||||
|
self.canvas.draw()
|
||||||
|
|
||||||
|
formula = self.poly_formula(coeffs)
|
||||||
|
self.resultLabel.setText(f"拟合公式: {formula}")
|
||||||
|
|
||||||
|
lang = self.langBox.currentText()
|
||||||
|
code = self.generate_code(coeffs, lang)
|
||||||
|
self.codeLabel.setText(code)
|
||||||
|
|
||||||
|
def poly_formula(self, coeffs):
|
||||||
|
terms = []
|
||||||
|
degree = len(coeffs) - 1
|
||||||
|
for i, c in enumerate(coeffs):
|
||||||
|
power = degree - i
|
||||||
|
if abs(c) < 1e-8:
|
||||||
|
continue
|
||||||
|
if power == 0:
|
||||||
|
terms.append(f"{c:.6g}")
|
||||||
|
elif power == 1:
|
||||||
|
terms.append(f"{c:.6g}*x")
|
||||||
|
else:
|
||||||
|
terms.append(f"{c:.6g}*x^{power}")
|
||||||
|
return " + ".join(terms)
|
||||||
|
|
||||||
|
def generate_code(self, coeffs, lang):
|
||||||
|
degree = len(coeffs) - 1
|
||||||
|
if lang == "C/C++":
|
||||||
|
code = "double poly(double x) {\n return "
|
||||||
|
elif lang == "Python":
|
||||||
|
code = "def poly(x):\n return "
|
||||||
|
else:
|
||||||
|
code = ""
|
||||||
|
terms = []
|
||||||
|
for i, c in enumerate(coeffs):
|
||||||
|
power = degree - i
|
||||||
|
if abs(c) < 1e-8:
|
||||||
|
continue
|
||||||
|
if power == 0:
|
||||||
|
terms.append(f"{c:.6g}")
|
||||||
|
elif power == 1:
|
||||||
|
terms.append(f"{c:.6g}*x")
|
||||||
|
else:
|
||||||
|
terms.append(f"{c:.6g}*pow(x,{power})" if lang == "C/C++" else f"{c:.6g}*x**{power}")
|
||||||
|
code += " + ".join(terms)
|
||||||
|
code += ";\n}" if lang == "C/C++" else ""
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
def copy_code(self):
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
clipboard.setText(self.codeLabel.text())
|
||||||
|
# 弹出提示
|
||||||
|
InfoBar.success(
|
||||||
|
title='复制成功',
|
||||||
|
content="代码已复制到剪贴板!",
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=2000,
|
||||||
|
parent=self
|
||||||
|
)
|
65
app/home_interface.py
Normal file
65
app/home_interface.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, ImageLabel, FluentLabelBase, TitleLabel
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def resource_path(relative_path):
|
||||||
|
"""获取资源文件的绝对路径,兼容打包和开发环境"""
|
||||||
|
if hasattr(sys, '_MEIPASS'):
|
||||||
|
# PyInstaller 打包后的临时目录
|
||||||
|
return os.path.join(sys._MEIPASS, relative_path)
|
||||||
|
return os.path.join(os.path.abspath("."), relative_path)
|
||||||
|
|
||||||
|
class HomeInterface(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setObjectName("homeInterface")
|
||||||
|
|
||||||
|
# 外层居中布局
|
||||||
|
outer_layout = QVBoxLayout(self)
|
||||||
|
outer_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
outer_layout.setSpacing(0)
|
||||||
|
outer_layout.addStretch()
|
||||||
|
|
||||||
|
# 内容布局
|
||||||
|
content_layout = QVBoxLayout()
|
||||||
|
content_layout.setSpacing(24)
|
||||||
|
content_layout.setContentsMargins(48, 48, 48, 48)
|
||||||
|
|
||||||
|
# Logo
|
||||||
|
logo = ImageLabel(resource_path('assets/logo/MRobot.png'))
|
||||||
|
logo.scaledToHeight(80)
|
||||||
|
content_layout.addWidget(logo, alignment=Qt.AlignHCenter) # 居中对齐
|
||||||
|
|
||||||
|
content_layout.addSpacing(8)
|
||||||
|
content_layout.addStretch()
|
||||||
|
|
||||||
|
# 主标题
|
||||||
|
title = TitleLabel("MRobot Toolbox")
|
||||||
|
title.setAlignment(Qt.AlignCenter)
|
||||||
|
content_layout.addWidget(title)
|
||||||
|
|
||||||
|
# 副标题
|
||||||
|
subtitle = BodyLabel("现代化,多功能机器人开发工具箱")
|
||||||
|
subtitle.setAlignment(Qt.AlignCenter)
|
||||||
|
content_layout.addWidget(subtitle)
|
||||||
|
|
||||||
|
# 欢迎语
|
||||||
|
welcome = BodyLabel("欢迎使用 MRobot Toolbox!一站式支持代码生成、硬件管理、串口调试与零件库下载。")
|
||||||
|
welcome.setAlignment(Qt.AlignCenter)
|
||||||
|
content_layout.addWidget(welcome)
|
||||||
|
|
||||||
|
content_layout.addSpacing(16)
|
||||||
|
content_layout.addStretch()
|
||||||
|
|
||||||
|
# 加到主布局
|
||||||
|
outer_layout.addLayout(content_layout)
|
||||||
|
outer_layout.addStretch()
|
||||||
|
|
||||||
|
# 版权信息置底
|
||||||
|
copyright_label = BodyLabel("© 2025 MRobot | Powered by QUT RM&RCer")
|
||||||
|
copyright_label.setAlignment(Qt.AlignCenter)
|
||||||
|
copyright_label.setStyleSheet("font-size: 13px;")
|
||||||
|
outer_layout.addWidget(copyright_label)
|
||||||
|
outer_layout.addSpacing(18)
|
112
app/main_window.py
Normal file
112
app/main_window.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
from PyQt5.QtCore import Qt, QSize
|
||||||
|
from PyQt5.QtGui import QIcon
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
with redirect_stdout(None):
|
||||||
|
from qfluentwidgets import NavigationItemPosition, FluentWindow, SplashScreen, setThemeColor, NavigationBarPushButton, toggleTheme, setTheme, Theme, NavigationAvatarWidget, NavigationToolButton ,NavigationPushButton
|
||||||
|
from qfluentwidgets import FluentIcon as FIF
|
||||||
|
from qfluentwidgets import InfoBar, InfoBarPosition
|
||||||
|
|
||||||
|
from .home_interface import HomeInterface
|
||||||
|
from .serial_terminal_interface import SerialTerminalInterface
|
||||||
|
from .part_library_interface import PartLibraryInterface
|
||||||
|
from .data_interface import DataInterface
|
||||||
|
from .mini_tool_interface import MiniToolInterface
|
||||||
|
from .about_interface import AboutInterface
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(FluentWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.initWindow()
|
||||||
|
self.initInterface()
|
||||||
|
self.initNavigation()
|
||||||
|
|
||||||
|
# 检查更新
|
||||||
|
# checkUpdate(self, flag=True)
|
||||||
|
# checkAnnouncement(self) # 检查公告
|
||||||
|
|
||||||
|
def initWindow(self):
|
||||||
|
self.setMicaEffectEnabled(False)
|
||||||
|
setThemeColor('#f18cb9', lazy=True)
|
||||||
|
setTheme(Theme.AUTO, lazy=True)
|
||||||
|
|
||||||
|
self.resize(960, 640)
|
||||||
|
self.setWindowIcon(QIcon('./assets/logo/M2.ico'))
|
||||||
|
self.setWindowTitle("MRobot Toolbox")
|
||||||
|
|
||||||
|
|
||||||
|
desktop = QApplication.desktop().availableGeometry() # 获取可用屏幕大小
|
||||||
|
w, h = desktop.width(), desktop.height()
|
||||||
|
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
|
||||||
|
|
||||||
|
self.show()
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
def initInterface(self):
|
||||||
|
self.homeInterface = HomeInterface(self)
|
||||||
|
self.serialTerminalInterface = SerialTerminalInterface(self)
|
||||||
|
self.partLibraryInterface = PartLibraryInterface(self)
|
||||||
|
self.dataInterface = DataInterface(self)
|
||||||
|
self.miniToolInterface = MiniToolInterface(self)
|
||||||
|
|
||||||
|
|
||||||
|
def initNavigation(self):
|
||||||
|
self.addSubInterface(self.homeInterface, FIF.HOME, self.tr('主页'))
|
||||||
|
self.addSubInterface(self.dataInterface, FIF.CODE, self.tr('代码生成'))
|
||||||
|
self.addSubInterface(self.serialTerminalInterface, FIF.COMMAND_PROMPT,self.tr('串口助手'))
|
||||||
|
self.addSubInterface(self.partLibraryInterface, FIF.DOWNLOAD, self.tr('零件库'))
|
||||||
|
self.addSubInterface(self.miniToolInterface, FIF.LIBRARY, self.tr('迷你工具箱'))
|
||||||
|
self.addSubInterface(AboutInterface(self), FIF.INFO, self.tr('关于'), position=NavigationItemPosition.BOTTOM)
|
||||||
|
|
||||||
|
|
||||||
|
# self.navigationInterface.addWidget(
|
||||||
|
# 'startGameButton',
|
||||||
|
# NavigationBarPushButton(FIF.PLAY, '启动游戏', isSelectable=False),
|
||||||
|
# self.startGame,
|
||||||
|
# NavigationItemPosition.BOTTOM)
|
||||||
|
|
||||||
|
# self.navigationInterface.addWidget(
|
||||||
|
# 'themeButton',
|
||||||
|
# NavigationBarPushButton(FIF.BRUSH, '主题', isSelectable=False),
|
||||||
|
# lambda: toggleTheme(lazy=True),
|
||||||
|
# NavigationItemPosition.BOTTOM)
|
||||||
|
|
||||||
|
self.themeBtn = NavigationPushButton(FIF.BRUSH, "切换主题", False, self.navigationInterface)
|
||||||
|
self.themeBtn.clicked.connect(lambda: toggleTheme(lazy=True))
|
||||||
|
self.navigationInterface.addWidget(
|
||||||
|
'themeButton',
|
||||||
|
self.themeBtn,
|
||||||
|
None,
|
||||||
|
NavigationItemPosition.BOTTOM
|
||||||
|
)
|
||||||
|
|
||||||
|
# self.navigationInterface.addWidget(
|
||||||
|
# 'avatar',
|
||||||
|
# NavigationBarPushButton(FIF.HEART, '赞赏', isSelectable=False),
|
||||||
|
# lambda: MessageBoxSupport(
|
||||||
|
# '支持作者🥰',
|
||||||
|
# '此程序为免费开源项目,如果你付了钱请立刻退款\n如果喜欢本项目,可以微信赞赏送作者一杯咖啡☕\n您的支持就是作者开发和维护项目的动力🚀',
|
||||||
|
# './assets/app/images/sponsor.jpg',
|
||||||
|
# self
|
||||||
|
# ).exec(),
|
||||||
|
# NavigationItemPosition.BOTTOM
|
||||||
|
# )
|
||||||
|
|
||||||
|
# self.addSubInterface(self.settingInterface, FIF.SETTING, self.tr('设置'), position=NavigationItemPosition.BOTTOM)
|
||||||
|
|
||||||
|
# self.splashScreen.finish() # 结束启动画面
|
||||||
|
# self.themeListener = checkThemeChange(self)
|
||||||
|
|
||||||
|
# if not cfg.get_value(base64.b64decode("YXV0b191cGRhdGU=").decode("utf-8")):
|
||||||
|
# disclaimer(self)
|
||||||
|
|
||||||
|
|
||||||
|
# main_window.py 只需修改关闭事件
|
||||||
|
def closeEvent(self, e):
|
||||||
|
# if self.themeListener and self.themeListener.isRunning():
|
||||||
|
# self.themeListener.terminate()
|
||||||
|
# self.themeListener.deleteLater()
|
||||||
|
super().closeEvent(e)
|
104
app/mini_tool_interface.py
Normal file
104
app/mini_tool_interface.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QSizePolicy
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
|
||||||
|
|
||||||
|
from .function_fit_interface import FunctionFitInterface
|
||||||
|
from .ai_interface import AIInterface
|
||||||
|
|
||||||
|
class MiniToolInterface(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setObjectName("minitoolInterface")
|
||||||
|
self.vBoxLayout = QVBoxLayout(self)
|
||||||
|
self.vBoxLayout.setAlignment(Qt.AlignTop)
|
||||||
|
self.vBoxLayout.setContentsMargins(10, 0, 10, 10) # 设置外边距
|
||||||
|
|
||||||
|
# 顶部标签栏,横向拉伸
|
||||||
|
self.tabBar = TabBar(self)
|
||||||
|
self.tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
|
self.vBoxLayout.addWidget(self.tabBar) # 移除 Qt.AlignLeft
|
||||||
|
|
||||||
|
self.stackedWidget = QStackedWidget(self)
|
||||||
|
self.vBoxLayout.addWidget(self.stackedWidget) # 加入布局
|
||||||
|
|
||||||
|
|
||||||
|
# 初始主页面
|
||||||
|
self.mainPage = QWidget(self)
|
||||||
|
mainLayout = QVBoxLayout(self.mainPage)
|
||||||
|
mainLayout.setAlignment(Qt.AlignTop) # 卡片靠顶部
|
||||||
|
self.card = PushSettingCard(
|
||||||
|
text="▶ 启动",
|
||||||
|
icon=FluentIcon.UNIT,
|
||||||
|
title="曲线拟合工具",
|
||||||
|
content="简单的曲线拟合工具,支持多种函数类型",
|
||||||
|
)
|
||||||
|
mainLayout.addWidget(self.card)
|
||||||
|
|
||||||
|
self.mainPage.setLayout(mainLayout)
|
||||||
|
self.aiCard = PushSettingCard(
|
||||||
|
text="▶ 启动",
|
||||||
|
icon=FluentIcon.ROBOT,
|
||||||
|
title="MRobot AI助手",
|
||||||
|
content="与 MRobot 进行图一乐交流, 使用开源模型qwen3:0.6b。",
|
||||||
|
)
|
||||||
|
mainLayout.addWidget(self.aiCard)
|
||||||
|
self.aiCard.clicked.connect(self.open_ai_tab)
|
||||||
|
# 添加主页面到堆叠窗口
|
||||||
|
self.addSubInterface(self.mainPage, "mainPage", "工具箱主页")
|
||||||
|
|
||||||
|
self.setLayout(self.vBoxLayout)
|
||||||
|
|
||||||
|
# 信号连接
|
||||||
|
self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
|
||||||
|
# self.tabBar.tabAddRequested.connect(self.onAddNewTab)
|
||||||
|
self.tabBar.tabCloseRequested.connect(self.onCloseTab)
|
||||||
|
self.card.clicked.connect(self.open_fit_tab)
|
||||||
|
|
||||||
|
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
|
||||||
|
widget.setObjectName(objectName)
|
||||||
|
self.stackedWidget.addWidget(widget)
|
||||||
|
self.tabBar.addTab(
|
||||||
|
routeKey=objectName,
|
||||||
|
text=text,
|
||||||
|
onClick=lambda: self.stackedWidget.setCurrentWidget(widget)
|
||||||
|
)
|
||||||
|
|
||||||
|
def onCurrentIndexChanged(self, index):
|
||||||
|
widget = self.stackedWidget.widget(index)
|
||||||
|
self.tabBar.setCurrentTab(widget.objectName())
|
||||||
|
|
||||||
|
def onAddNewTab(self):
|
||||||
|
pass # 可自定义添加新标签页逻辑
|
||||||
|
|
||||||
|
def onCloseTab(self, index: int):
|
||||||
|
item = self.tabBar.tabItem(index)
|
||||||
|
widget = self.findChild(QWidget, item.routeKey())
|
||||||
|
self.stackedWidget.removeWidget(widget)
|
||||||
|
self.tabBar.removeTab(index)
|
||||||
|
widget.deleteLater()
|
||||||
|
|
||||||
|
def open_fit_tab(self):
|
||||||
|
# 检查是否已存在标签页,避免重复添加
|
||||||
|
for i in range(self.stackedWidget.count()):
|
||||||
|
widget = self.stackedWidget.widget(i)
|
||||||
|
if widget.objectName() == "fitPage":
|
||||||
|
self.stackedWidget.setCurrentWidget(widget)
|
||||||
|
self.tabBar.setCurrentTab("fitPage")
|
||||||
|
return
|
||||||
|
fit_page = FunctionFitInterface(self)
|
||||||
|
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
|
||||||
|
self.stackedWidget.setCurrentWidget(fit_page)
|
||||||
|
self.tabBar.setCurrentTab("fitPage")
|
||||||
|
|
||||||
|
def open_ai_tab(self):
|
||||||
|
# 检查是否已存在标签页,避免重复添加
|
||||||
|
for i in range(self.stackedWidget.count()):
|
||||||
|
widget = self.stackedWidget.widget(i)
|
||||||
|
if widget.objectName() == "aiPage":
|
||||||
|
self.stackedWidget.setCurrentWidget(widget)
|
||||||
|
self.tabBar.setCurrentTab("aiPage")
|
||||||
|
return
|
||||||
|
ai_page = AIInterface(self)
|
||||||
|
self.addSubInterface(ai_page, "aiPage", "AI问答")
|
||||||
|
self.stackedWidget.setCurrentWidget(ai_page)
|
||||||
|
self.tabBar.setCurrentTab("aiPage")
|
205
app/part_library_interface.py
Normal file
205
app/part_library_interface.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
||||||
|
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, PushButton, TreeWidget, ProgressBar, Dialog, InfoBar, InfoBarPosition, FluentIcon, ProgressRing, Dialog
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
from .tools.part_download import DownloadThread # 新增导入
|
||||||
|
|
||||||
|
from urllib.parse import quote
|
||||||
|
class PartLibraryInterface(QWidget):
|
||||||
|
SERVER_URL = "http://154.37.215.220:5000"
|
||||||
|
SECRET_KEY = "MRobot_Download"
|
||||||
|
LOCAL_LIB_DIR = "assets/mech_lib"
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self.setObjectName("partLibraryInterface")
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20) # 添加边距
|
||||||
|
layout.setSpacing(16)
|
||||||
|
|
||||||
|
layout.addWidget(SubtitleLabel("零件库(在线bate版)"))
|
||||||
|
layout.addWidget(HorizontalSeparator())
|
||||||
|
layout.addWidget(BodyLabel("感谢重庆邮电大学整理的零件库,选择需要的文件下载到本地。(如无法使用或者下载失败,请尝试重新下载或检查网络连接)"))
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
refresh_btn = PushButton(FluentIcon.SYNC, "刷新列表")
|
||||||
|
refresh_btn.clicked.connect(self.refresh_list)
|
||||||
|
btn_layout.addWidget(refresh_btn)
|
||||||
|
|
||||||
|
open_local_btn = PushButton(FluentIcon.FOLDER, "打开本地零件库")
|
||||||
|
open_local_btn.clicked.connect(self.open_local_lib)
|
||||||
|
btn_layout.addWidget(open_local_btn)
|
||||||
|
btn_layout.addStretch()
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
self.tree = TreeWidget(self)
|
||||||
|
self.tree.setHeaderLabels(["名称", "类型"])
|
||||||
|
self.tree.setSelectionMode(self.tree.ExtendedSelection)
|
||||||
|
self.tree.header().setSectionResizeMode(0, self.tree.header().Stretch)
|
||||||
|
self.tree.header().setSectionResizeMode(1, self.tree.header().ResizeToContents)
|
||||||
|
self.tree.setCheckedColor("#0078d4", "#2d7d9a")
|
||||||
|
self.tree.setBorderRadius(8)
|
||||||
|
self.tree.setBorderVisible(True)
|
||||||
|
layout.addWidget(self.tree, stretch=1)
|
||||||
|
|
||||||
|
download_btn = PushButton(FluentIcon.DOWNLOAD, "下载选中文件")
|
||||||
|
download_btn.clicked.connect(self.download_selected_files)
|
||||||
|
layout.addWidget(download_btn)
|
||||||
|
|
||||||
|
self.refresh_list(first=True)
|
||||||
|
|
||||||
|
def refresh_list(self, first=False):
|
||||||
|
self.tree.clear()
|
||||||
|
try:
|
||||||
|
resp = requests.get(
|
||||||
|
f"{self.SERVER_URL}/list",
|
||||||
|
params={"key": self.SECRET_KEY},
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
tree = resp.json()
|
||||||
|
self.populate_tree(self.tree, tree, "")
|
||||||
|
if not first:
|
||||||
|
InfoBar.success(
|
||||||
|
title="刷新成功",
|
||||||
|
content="零件库已经是最新的!",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
InfoBar.error(
|
||||||
|
title="刷新失败",
|
||||||
|
content=f"获取零件库失败: {e}",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=3000
|
||||||
|
)
|
||||||
|
|
||||||
|
def populate_tree(self, parent, node, path_prefix):
|
||||||
|
from PyQt5.QtWidgets import QTreeWidgetItem
|
||||||
|
for dname, dnode in node.get("dirs", {}).items():
|
||||||
|
item = QTreeWidgetItem([dname, "文件夹"])
|
||||||
|
if isinstance(parent, TreeWidget):
|
||||||
|
parent.addTopLevelItem(item)
|
||||||
|
else:
|
||||||
|
parent.addChild(item)
|
||||||
|
self.populate_tree(item, dnode, os.path.join(path_prefix, dname))
|
||||||
|
for fname in node.get("files", []):
|
||||||
|
item = QTreeWidgetItem([fname, "文件"])
|
||||||
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||||
|
item.setCheckState(0, Qt.Unchecked)
|
||||||
|
item.setData(0, Qt.UserRole, os.path.join(path_prefix, fname))
|
||||||
|
if isinstance(parent, TreeWidget):
|
||||||
|
parent.addTopLevelItem(item)
|
||||||
|
else:
|
||||||
|
parent.addChild(item)
|
||||||
|
|
||||||
|
def get_checked_files(self):
|
||||||
|
files = []
|
||||||
|
def _traverse(item):
|
||||||
|
for i in range(item.childCount()):
|
||||||
|
child = item.child(i)
|
||||||
|
if child.text(1) == "文件" and child.checkState(0) == Qt.Checked:
|
||||||
|
files.append(child.data(0, Qt.UserRole))
|
||||||
|
_traverse(child)
|
||||||
|
root = self.tree.invisibleRootItem()
|
||||||
|
for i in range(root.childCount()):
|
||||||
|
_traverse(root.child(i))
|
||||||
|
return files
|
||||||
|
|
||||||
|
def download_selected_files(self):
|
||||||
|
files = self.get_checked_files()
|
||||||
|
if not files:
|
||||||
|
dialog = Dialog(
|
||||||
|
title="温馨提示",
|
||||||
|
content="请先勾选需要下载的文件。",
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
dialog.yesButton.setText("知道啦")
|
||||||
|
dialog.cancelButton.hide()
|
||||||
|
dialog.exec()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建进度环
|
||||||
|
self.progress_ring = ProgressRing()
|
||||||
|
self.progress_ring.setRange(0, 100)
|
||||||
|
self.progress_ring.setValue(0)
|
||||||
|
self.progress_ring.setTextVisible(True)
|
||||||
|
self.progress_ring.setFixedSize(32, 32)
|
||||||
|
self.progress_ring.setStrokeWidth(4)
|
||||||
|
|
||||||
|
# 展示消息条(关闭按钮即中断下载)
|
||||||
|
self.info_bar = InfoBar(
|
||||||
|
icon=FluentIcon.DOWNLOAD,
|
||||||
|
title="正在下载",
|
||||||
|
content="正在下载选中文件...",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=-1 # 不自动消失
|
||||||
|
)
|
||||||
|
self.info_bar.addWidget(self.progress_ring)
|
||||||
|
self.info_bar.closeButton.clicked.connect(self.stop_download) # 关闭即中断下载
|
||||||
|
self.info_bar.show()
|
||||||
|
|
||||||
|
# 启动下载线程
|
||||||
|
self.download_thread = DownloadThread(
|
||||||
|
files, self.SERVER_URL, self.SECRET_KEY, self.LOCAL_LIB_DIR
|
||||||
|
)
|
||||||
|
self.download_thread.progressChanged.connect(self.progress_ring.setValue)
|
||||||
|
self.download_thread.finished.connect(self.on_download_finished)
|
||||||
|
self.download_thread.finished.connect(self.download_thread.deleteLater)
|
||||||
|
self.download_thread.start()
|
||||||
|
|
||||||
|
def stop_download(self):
|
||||||
|
if hasattr(self, "download_thread") and self.download_thread.isRunning():
|
||||||
|
self.download_thread.terminate()
|
||||||
|
self.download_thread.wait()
|
||||||
|
self.info_bar.close()
|
||||||
|
InfoBar.warning(
|
||||||
|
title="下载已中断",
|
||||||
|
content="已手动中断下载任务。",
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=2000
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_download_finished(self, success, fail):
|
||||||
|
self.info_bar.close()
|
||||||
|
msg = f"成功下载:{len(success)} 个文件,失败:{len(fail)} 个文件"
|
||||||
|
|
||||||
|
# 创建“打开文件夹”按钮
|
||||||
|
open_btn = PushButton("打开文件夹")
|
||||||
|
def open_folder():
|
||||||
|
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
||||||
|
import platform, subprocess
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
subprocess.call(["open", folder])
|
||||||
|
elif platform.system() == "Windows":
|
||||||
|
subprocess.call(["explorer", folder])
|
||||||
|
else:
|
||||||
|
subprocess.call(["xdg-open", folder])
|
||||||
|
|
||||||
|
# 展示成功消息条,自动消失
|
||||||
|
self.result_bar = InfoBar.success(
|
||||||
|
title="下载完成",
|
||||||
|
content=msg,
|
||||||
|
parent=self,
|
||||||
|
position=InfoBarPosition.TOP,
|
||||||
|
duration=4000 # 4秒后自动消失
|
||||||
|
)
|
||||||
|
self.result_bar.addWidget(open_btn)
|
||||||
|
open_btn.clicked.connect(open_folder)
|
||||||
|
self.result_bar.show()
|
||||||
|
|
||||||
|
def open_local_lib(self):
|
||||||
|
folder = os.path.abspath(self.LOCAL_LIB_DIR)
|
||||||
|
import platform, subprocess
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
subprocess.call(["open", folder])
|
||||||
|
elif platform.system() == "Windows":
|
||||||
|
subprocess.call(["explorer", folder])
|
||||||
|
else:
|
||||||
|
subprocess.call(["xdg-open", folder])
|
167
app/serial_terminal_interface.py
Normal file
167
app/serial_terminal_interface.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||||
|
from PyQt5.QtGui import QTextCursor
|
||||||
|
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy
|
||||||
|
from PyQt5.QtWidgets import QWidget
|
||||||
|
from qfluentwidgets import (
|
||||||
|
FluentIcon, PushButton, ComboBox, TextEdit, LineEdit, CheckBox,
|
||||||
|
SubtitleLabel, BodyLabel, HorizontalSeparator
|
||||||
|
)
|
||||||
|
|
||||||
|
class SerialReadThread(QThread):
|
||||||
|
data_received = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, ser):
|
||||||
|
super().__init__()
|
||||||
|
self.ser = ser
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self._running:
|
||||||
|
if self.ser and self.ser.is_open and self.ser.in_waiting:
|
||||||
|
try:
|
||||||
|
data = self.ser.readline().decode(errors='ignore')
|
||||||
|
self.data_received.emit(data)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
self.wait()
|
||||||
|
|
||||||
|
class SerialTerminalInterface(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self.setObjectName("serialTerminalInterface")
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
main_layout.setSpacing(12)
|
||||||
|
|
||||||
|
# 顶部:串口设置区
|
||||||
|
top_hbox = QHBoxLayout()
|
||||||
|
top_hbox.addWidget(BodyLabel("串口:"))
|
||||||
|
self.port_combo = ComboBox()
|
||||||
|
self.refresh_ports()
|
||||||
|
top_hbox.addWidget(self.port_combo)
|
||||||
|
top_hbox.addWidget(BodyLabel("波特率:"))
|
||||||
|
self.baud_combo = ComboBox()
|
||||||
|
self.baud_combo.addItems(['9600', '115200', '57600', '38400', '19200', '4800'])
|
||||||
|
top_hbox.addWidget(self.baud_combo)
|
||||||
|
self.connect_btn = PushButton("连接")
|
||||||
|
self.connect_btn.clicked.connect(self.toggle_connection)
|
||||||
|
top_hbox.addWidget(self.connect_btn)
|
||||||
|
self.refresh_btn = PushButton(FluentIcon.SYNC, "刷新")
|
||||||
|
self.refresh_btn.clicked.connect(self.refresh_ports)
|
||||||
|
top_hbox.addWidget(self.refresh_btn)
|
||||||
|
top_hbox.addStretch()
|
||||||
|
main_layout.addLayout(top_hbox)
|
||||||
|
|
||||||
|
main_layout.addWidget(HorizontalSeparator())
|
||||||
|
|
||||||
|
# 中部:左侧预设命令,右侧显示区
|
||||||
|
center_hbox = QHBoxLayout()
|
||||||
|
# 左侧:预设命令竖排
|
||||||
|
preset_vbox = QVBoxLayout()
|
||||||
|
preset_vbox.addWidget(SubtitleLabel("快捷指令"))
|
||||||
|
preset_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.preset_commands = [
|
||||||
|
("线程监视器", "htop"),
|
||||||
|
("陀螺仪校准", "cali_gyro"),
|
||||||
|
("性能监视", "htop"),
|
||||||
|
("重启", "reset"),
|
||||||
|
("显示所有设备", "ls /dev"),
|
||||||
|
("查询id", "id"),
|
||||||
|
]
|
||||||
|
for label, cmd in self.preset_commands:
|
||||||
|
btn = PushButton(label)
|
||||||
|
btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||||
|
btn.clicked.connect(lambda _, c=cmd: self.send_preset_command(c))
|
||||||
|
preset_vbox.addWidget(btn)
|
||||||
|
preset_vbox.addStretch()
|
||||||
|
main_layout.addLayout(center_hbox, stretch=1)
|
||||||
|
|
||||||
|
# 右侧:串口数据显示区
|
||||||
|
self.text_edit = TextEdit()
|
||||||
|
self.text_edit.setReadOnly(True)
|
||||||
|
self.text_edit.setMinimumWidth(400)
|
||||||
|
center_hbox.addWidget(self.text_edit, 3)
|
||||||
|
center_hbox.addLayout(preset_vbox, 1)
|
||||||
|
|
||||||
|
main_layout.addWidget(HorizontalSeparator())
|
||||||
|
|
||||||
|
# 底部:输入区
|
||||||
|
bottom_hbox = QHBoxLayout()
|
||||||
|
self.input_line = LineEdit()
|
||||||
|
self.input_line.setPlaceholderText("输入内容,回车发送")
|
||||||
|
self.input_line.returnPressed.connect(self.send_data)
|
||||||
|
bottom_hbox.addWidget(self.input_line, 4)
|
||||||
|
send_btn = PushButton("发送")
|
||||||
|
send_btn.clicked.connect(self.send_data)
|
||||||
|
bottom_hbox.addWidget(send_btn, 1)
|
||||||
|
self.auto_enter_checkbox = CheckBox("自动回车 ")
|
||||||
|
self.auto_enter_checkbox.setChecked(True)
|
||||||
|
bottom_hbox.addWidget(self.auto_enter_checkbox)
|
||||||
|
bottom_hbox.addStretch()
|
||||||
|
main_layout.addLayout(bottom_hbox)
|
||||||
|
|
||||||
|
self.ser = None
|
||||||
|
self.read_thread = None
|
||||||
|
|
||||||
|
def send_preset_command(self, cmd):
|
||||||
|
self.input_line.setText(cmd)
|
||||||
|
self.send_data()
|
||||||
|
|
||||||
|
def refresh_ports(self):
|
||||||
|
self.port_combo.clear()
|
||||||
|
ports = serial.tools.list_ports.comports()
|
||||||
|
for port in ports:
|
||||||
|
self.port_combo.addItem(port.device)
|
||||||
|
|
||||||
|
def toggle_connection(self):
|
||||||
|
if self.ser and self.ser.is_open:
|
||||||
|
self.disconnect_serial()
|
||||||
|
else:
|
||||||
|
self.connect_serial()
|
||||||
|
|
||||||
|
def connect_serial(self):
|
||||||
|
port = self.port_combo.currentText()
|
||||||
|
baud = int(self.baud_combo.currentText())
|
||||||
|
try:
|
||||||
|
self.ser = serial.Serial(port, baud, timeout=0.1)
|
||||||
|
self.connect_btn.setText("断开")
|
||||||
|
self.text_edit.append(f"已连接到 {port} @ {baud}")
|
||||||
|
self.read_thread = SerialReadThread(self.ser)
|
||||||
|
self.read_thread.data_received.connect(self.display_data)
|
||||||
|
self.read_thread.start()
|
||||||
|
except Exception as e:
|
||||||
|
self.text_edit.append(f"连接失败: {e}")
|
||||||
|
|
||||||
|
def disconnect_serial(self):
|
||||||
|
if self.read_thread:
|
||||||
|
self.read_thread.stop()
|
||||||
|
self.read_thread = None
|
||||||
|
if self.ser:
|
||||||
|
self.ser.close()
|
||||||
|
self.ser = None
|
||||||
|
self.connect_btn.setText("连接")
|
||||||
|
self.text_edit.append("已断开连接")
|
||||||
|
|
||||||
|
def display_data(self, data):
|
||||||
|
self.text_edit.moveCursor(QTextCursor.End)
|
||||||
|
self.text_edit.insertPlainText(data)
|
||||||
|
self.text_edit.moveCursor(QTextCursor.End)
|
||||||
|
|
||||||
|
def send_data(self):
|
||||||
|
if self.ser and self.ser.is_open:
|
||||||
|
text = self.input_line.text()
|
||||||
|
try:
|
||||||
|
if not text:
|
||||||
|
self.ser.write('\n'.encode())
|
||||||
|
else:
|
||||||
|
for char in text:
|
||||||
|
self.ser.write(char.encode())
|
||||||
|
if self.auto_enter_checkbox.isChecked():
|
||||||
|
self.ser.write('\n'.encode())
|
||||||
|
except Exception as e:
|
||||||
|
self.text_edit.append(f"发送失败: {e}")
|
||||||
|
self.input_line.clear()
|
BIN
app/tools/__pycache__/check_update.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/check_update.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/part_download.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/part_download.cpython-39.pyc
Normal file
Binary file not shown.
14
app/tools/check_update.py
Normal file
14
app/tools/check_update.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import requests
|
||||||
|
from packaging.version import parse as vparse
|
||||||
|
|
||||||
|
def check_update(local_version, repo="goldenfishs/MRobot"):
|
||||||
|
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||||
|
resp = requests.get(url, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
latest = resp.json()["tag_name"].lstrip("v")
|
||||||
|
if vparse(latest) > vparse(local_version):
|
||||||
|
return latest
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise RuntimeError("GitHub API 请求失败")
|
20
app/tools/code_utils.py
Normal file
20
app/tools/code_utils.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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)
|
25
app/tools/ioc_config.py
Normal file
25
app/tools/ioc_config.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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
|
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)
|
109
app/tools/task_code_generator.py
Normal file
109
app/tools/task_code_generator.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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)
|
BIN
assets/User_code/.DS_Store
vendored
Normal file
BIN
assets/User_code/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
assets/User_code/bsp/.DS_Store
vendored
Normal file
BIN
assets/User_code/bsp/.DS_Store
vendored
Normal file
Binary file not shown.
141
assets/User_code/bsp/can.c
Normal file
141
assets/User_code/bsp/can.c
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include "bsp\can.h"
|
||||||
|
|
||||||
|
/* Private define ----------------------------------------------------------- */
|
||||||
|
/* Private macro ------------------------------------------------------------ */
|
||||||
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
|
/* Private variables -------------------------------------------------------- */
|
||||||
|
static void (*CAN_Callback[BSP_CAN_NUM][BSP_CAN_CB_NUM])(void);
|
||||||
|
|
||||||
|
/* Private function -------------------------------------------------------- */
|
||||||
|
static BSP_CAN_t CAN_Get(CAN_HandleTypeDef *hcan) {
|
||||||
|
if (hcan->Instance == CAN2)
|
||||||
|
return BSP_CAN_2;
|
||||||
|
else if (hcan->Instance == CAN1)
|
||||||
|
return BSP_CAN_1;
|
||||||
|
else
|
||||||
|
return BSP_CAN_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX0_CPLT_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX0_CPLT_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_TxMailbox1CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX1_CPLT_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX1_CPLT_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_TxMailbox2CompleteCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX2_CPLT_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX2_CPLT_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_TxMailbox0AbortCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX0_ABORT_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX0_ABORT_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_TxMailbox1AbortCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX1_ABORT_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX1_ABORT_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_TxMailbox2AbortCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX2_ABORT_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_TX_MAILBOX2_ABORT_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_RX_FIFO0_MSG_PENDING_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_RX_FIFO0_MSG_PENDING_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_RxFifo0FullCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_RX_FIFO0_FULL_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_RX_FIFO0_FULL_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_RX_FIFO1_MSG_PENDING_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_RX_FIFO1_MSG_PENDING_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_RxFifo1FullCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_RX_FIFO1_FULL_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_RX_FIFO1_FULL_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_SleepCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_SLEEP_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_SLEEP_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_WakeUpFromRxMsgCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_WAKEUP_FROM_RX_MSG_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_WAKEUP_FROM_RX_MSG_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan) {
|
||||||
|
BSP_CAN_t bsp_can = CAN_Get(hcan);
|
||||||
|
if (bsp_can != BSP_CAN_ERR) {
|
||||||
|
if (CAN_Callback[bsp_can][HAL_CAN_ERROR_CB])
|
||||||
|
CAN_Callback[bsp_can][HAL_CAN_ERROR_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exported functions ------------------------------------------------------- */
|
||||||
|
CAN_HandleTypeDef *BSP_CAN_GetHandle(BSP_CAN_t can) {
|
||||||
|
switch (can) {
|
||||||
|
case BSP_CAN_2:
|
||||||
|
return &hcan2;
|
||||||
|
case BSP_CAN_1:
|
||||||
|
return &hcan1;
|
||||||
|
default:
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t BSP_CAN_RegisterCallback(BSP_CAN_t can, BSP_CAN_Callback_t type,
|
||||||
|
void (*callback)(void)) {
|
||||||
|
if (callback == NULL) return BSP_ERR_NULL;
|
||||||
|
CAN_Callback[can][type] = callback;
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
46
assets/User_code/bsp/can.h
Normal file
46
assets/User_code/bsp/can.h
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include <can.h>
|
||||||
|
|
||||||
|
#include "bsp/bsp.h"
|
||||||
|
|
||||||
|
/* Exported constants ------------------------------------------------------- */
|
||||||
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
|
/* Exported types ----------------------------------------------------------- */
|
||||||
|
typedef enum {
|
||||||
|
BSP_CAN_1,
|
||||||
|
BSP_CAN_2,
|
||||||
|
BSP_CAN_NUM,
|
||||||
|
BSP_CAN_ERR,
|
||||||
|
} BSP_CAN_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
HAL_CAN_TX_MAILBOX0_CPLT_CB,
|
||||||
|
HAL_CAN_TX_MAILBOX1_CPLT_CB,
|
||||||
|
HAL_CAN_TX_MAILBOX2_CPLT_CB,
|
||||||
|
HAL_CAN_TX_MAILBOX0_ABORT_CB,
|
||||||
|
HAL_CAN_TX_MAILBOX1_ABORT_CB,
|
||||||
|
HAL_CAN_TX_MAILBOX2_ABORT_CB,
|
||||||
|
HAL_CAN_RX_FIFO0_MSG_PENDING_CB,
|
||||||
|
HAL_CAN_RX_FIFO0_FULL_CB,
|
||||||
|
HAL_CAN_RX_FIFO1_MSG_PENDING_CB,
|
||||||
|
HAL_CAN_RX_FIFO1_FULL_CB,
|
||||||
|
HAL_CAN_SLEEP_CB,
|
||||||
|
HAL_CAN_WAKEUP_FROM_RX_MSG_CB,
|
||||||
|
HAL_CAN_ERROR_CB,
|
||||||
|
BSP_CAN_CB_NUM
|
||||||
|
} BSP_CAN_Callback_t;
|
||||||
|
|
||||||
|
/* Exported functions prototypes -------------------------------------------- */
|
||||||
|
CAN_HandleTypeDef *BSP_CAN_GetHandle(BSP_CAN_t can);
|
||||||
|
int8_t BSP_CAN_RegisterCallback(BSP_CAN_t can, BSP_CAN_Callback_t type,
|
||||||
|
void (*callback)(void));
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
118
assets/User_code/bsp/delay.c
Normal file
118
assets/User_code/bsp/delay.c
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
#include "bsp_delay.h"
|
||||||
|
|
||||||
|
#include "cmsis_os.h"
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
/* Private define ----------------------------------------------------------- */
|
||||||
|
/* Private macro ------------------------------------------------------------ */
|
||||||
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
|
/* Private variables -------------------------------------------------------- */
|
||||||
|
static uint8_t fac_us = 0;
|
||||||
|
static uint32_t fac_ms = 0;
|
||||||
|
/* Private function -------------------------------------------------------- */
|
||||||
|
static void delay_ticks(uint32_t ticks)
|
||||||
|
{
|
||||||
|
uint32_t told = SysTick->VAL;
|
||||||
|
uint32_t tnow = 0;
|
||||||
|
uint32_t tcnt = 0;
|
||||||
|
uint32_t reload = SysTick->LOAD;
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
tnow = SysTick->VAL;
|
||||||
|
if (tnow != told)
|
||||||
|
{
|
||||||
|
if (tnow < told)
|
||||||
|
{
|
||||||
|
tcnt += told - tnow;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tcnt += reload - tnow + told;
|
||||||
|
}
|
||||||
|
told = tnow;
|
||||||
|
if (tcnt >= ticks)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Exported functions ------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 毫秒延时函数
|
||||||
|
* @param ms
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
int8_t BSP_Delay(uint32_t ms)
|
||||||
|
{
|
||||||
|
uint32_t tick_period = 1000u / osKernelGetTickFreq();
|
||||||
|
uint32_t ticks = ms / tick_period;
|
||||||
|
|
||||||
|
switch (osKernelGetState())
|
||||||
|
{
|
||||||
|
case osKernelError:
|
||||||
|
case osKernelReserved:
|
||||||
|
case osKernelLocked:
|
||||||
|
case osKernelSuspended:
|
||||||
|
return BSP_ERR;
|
||||||
|
|
||||||
|
case osKernelRunning:
|
||||||
|
osDelay(ticks ? ticks : 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case osKernelInactive:
|
||||||
|
case osKernelReady:
|
||||||
|
HAL_Delay(ms);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 延时初始化
|
||||||
|
* @param
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
int8_t BSP_Delay_Init(void)
|
||||||
|
{
|
||||||
|
if (SystemCoreClock == 0)
|
||||||
|
{
|
||||||
|
return BSP_ERR;
|
||||||
|
}
|
||||||
|
fac_us = SystemCoreClock / 1000000;
|
||||||
|
fac_ms = SystemCoreClock / 1000;
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 微秒延时,注意:此函数会阻塞当前线程,直到延时结束,并且会被中断打断
|
||||||
|
* @param us
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
int8_t BSP_Delay_us(uint32_t us)
|
||||||
|
{
|
||||||
|
if (fac_us == 0)
|
||||||
|
{
|
||||||
|
return BSP_ERR;
|
||||||
|
}
|
||||||
|
uint32_t ticks = us * fac_us;
|
||||||
|
delay_ticks(ticks);
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 毫秒延时,注意:此函数会阻塞当前线程,直到延时结束,并且会被中断打断
|
||||||
|
* @param ms
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
int8_t BSP_Delay_ms(uint32_t ms)
|
||||||
|
{
|
||||||
|
if (fac_ms == 0)
|
||||||
|
{
|
||||||
|
return BSP_ERR;
|
||||||
|
}
|
||||||
|
uint32_t ticks = ms * fac_ms;
|
||||||
|
delay_ticks(ticks);
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
24
assets/User_code/bsp/delay.h
Normal file
24
assets/User_code/bsp/delay.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "bsp/bsp.h"
|
||||||
|
|
||||||
|
/* Exported constants ------------------------------------------------------- */
|
||||||
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
|
/* Exported types ----------------------------------------------------------- */
|
||||||
|
/* Exported functions prototypes -------------------------------------------- */
|
||||||
|
int8_t BSP_Delay(uint32_t ms);
|
||||||
|
|
||||||
|
int8_t BSP_Delay_Init(void);
|
||||||
|
int8_t BSP_Delay_us(uint32_t us);
|
||||||
|
int8_t BSP_Delay_ms(uint32_t ms);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
2
assets/User_code/bsp/describe.csv
Normal file
2
assets/User_code/bsp/describe.csv
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
uart,要求开启dma和中断
|
||||||
|
can,要求开启can的中断
|
|
72
assets/User_code/bsp/gpio_exti.c
Normal file
72
assets/User_code/bsp/gpio_exti.c
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include "bsp\gpio.h"
|
||||||
|
|
||||||
|
#include <gpio.h>
|
||||||
|
#include <main.h>
|
||||||
|
|
||||||
|
/* Private define ----------------------------------------------------------- */
|
||||||
|
/* Private macro ------------------------------------------------------------ */
|
||||||
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
|
/* Private variables -------------------------------------------------------- */
|
||||||
|
static void (*GPIO_Callback[BSP_GPIO_NUM][BSP_GPIO_CB_NUM])(void);
|
||||||
|
|
||||||
|
/* Private function -------------------------------------------------------- */
|
||||||
|
static BSP_GPIO_t GPIO_Get(uint16_t pin) {
|
||||||
|
switch (pin) {
|
||||||
|
case USER_KEY_Pin:
|
||||||
|
return BSP_GPIO_USER_KEY;
|
||||||
|
/* case XXX_Pin:
|
||||||
|
return BSP_GPIO_XXX; */
|
||||||
|
default:
|
||||||
|
return BSP_GPIO_ERR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
|
||||||
|
BSP_GPIO_t gpio = GPIO_Get(GPIO_Pin);
|
||||||
|
if (gpio != BSP_GPIO_ERR) {
|
||||||
|
if (GPIO_Callback[gpio][BSP_GPIO_EXTI_CB]) {
|
||||||
|
GPIO_Callback[gpio][BSP_GPIO_EXTI_CB]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exported functions ------------------------------------------------------- */
|
||||||
|
int8_t BSP_GPIO_RegisterCallback(BSP_GPIO_t gpio, BSP_GPIO_Callback_t type, void (*callback)(void)) {
|
||||||
|
if (callback == NULL || gpio >= BSP_GPIO_NUM || type >= BSP_GPIO_CB_NUM) return BSP_ERR_NULL;
|
||||||
|
|
||||||
|
GPIO_Callback[gpio][type] = callback;
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t BSP_GPIO_EnableIRQ(BSP_GPIO_t gpio) {
|
||||||
|
switch (gpio) {
|
||||||
|
case BSP_GPIO_USER_KEY:
|
||||||
|
HAL_NVIC_EnableIRQ(USER_KEY_EXTI_IRQn);
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* case BSP_GPIO_XXX:
|
||||||
|
HAL_NVIC_EnableIRQ(XXX_IRQn);
|
||||||
|
break; */
|
||||||
|
|
||||||
|
default:
|
||||||
|
return BSP_ERR;
|
||||||
|
}
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t BSP_GPIO_DisableIRQ(BSP_GPIO_t gpio) {
|
||||||
|
switch (gpio) {
|
||||||
|
case BSP_GPIO_USER_KEY:
|
||||||
|
HAL_NVIC_DisableIRQ(USER_KEY_EXTI_IRQn);
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* case BSP_GPIO_XXX:
|
||||||
|
HAL_NVIC_DisableIRQ(XXX_IRQn);
|
||||||
|
break; */
|
||||||
|
|
||||||
|
default:
|
||||||
|
return BSP_ERR;
|
||||||
|
}
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
37
assets/User_code/bsp/gpio_exti.h
Normal file
37
assets/User_code/bsp/gpio_exti.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "bsp/bsp.h"
|
||||||
|
|
||||||
|
/* Exported constants ------------------------------------------------------- */
|
||||||
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
|
/* Exported types ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* GPIO设备枚举,与设备对应 */
|
||||||
|
typedef enum {
|
||||||
|
BSP_GPIO_USER_KEY,
|
||||||
|
/* BSP_GPIO_XXX, */
|
||||||
|
BSP_GPIO_NUM,
|
||||||
|
BSP_GPIO_ERR,
|
||||||
|
} BSP_GPIO_t;
|
||||||
|
|
||||||
|
/* GPIO支持的中断回调函数类型 */
|
||||||
|
typedef enum {
|
||||||
|
BSP_GPIO_EXTI_CB,
|
||||||
|
BSP_GPIO_CB_NUM,
|
||||||
|
} BSP_GPIO_Callback_t;
|
||||||
|
|
||||||
|
/* Exported functions prototypes -------------------------------------------- */
|
||||||
|
int8_t BSP_GPIO_RegisterCallback(BSP_GPIO_t gpio, BSP_GPIO_Callback_t type, void (*callback)(void));
|
||||||
|
int8_t BSP_GPIO_EnableIRQ(BSP_GPIO_t gpio);
|
||||||
|
int8_t BSP_GPIO_DisableIRQ(BSP_GPIO_t gpio);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
@ -6,6 +6,7 @@
|
|||||||
/* Private define ----------------------------------------------------------- */
|
/* Private define ----------------------------------------------------------- */
|
||||||
/* Private macro ------------------------------------------------------------ */
|
/* Private macro ------------------------------------------------------------ */
|
||||||
/* Private typedef ---------------------------------------------------------- */
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
|
|
||||||
/* Private variables -------------------------------------------------------- */
|
/* Private variables -------------------------------------------------------- */
|
||||||
static uint32_t led_stats; // 使用位掩码记录每个通道的状态,最多支持32LED
|
static uint32_t led_stats; // 使用位掩码记录每个通道的状态,最多支持32LED
|
||||||
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/* Includes ----------------------------------------------------------------- */
|
/* Includes ----------------------------------------------------------------- */
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include "gpio.h"
|
||||||
/* Exported constants ------------------------------------------------------- */
|
/* Exported constants ------------------------------------------------------- */
|
||||||
/* Exported macro ----------------------------------------------------------- */
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
/* Exported types ----------------------------------------------------------- */
|
/* Exported types ----------------------------------------------------------- */
|
48
assets/User_code/bsp/servo_pwm.c
Normal file
48
assets/User_code/bsp/servo_pwm.c
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include "servo_pwm.h"
|
||||||
|
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
|
||||||
|
/* Private define ----------------------------------------------------------- */
|
||||||
|
/* Private macro ------------------------------------------------------------ */
|
||||||
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
|
/* Private variables -------------------------------------------------------- */
|
||||||
|
/* Private function -------------------------------------------------------- */
|
||||||
|
/* Exported functions ------------------------------------------------------- */
|
||||||
|
|
||||||
|
int8_t BSP_PWM_Start(BSP_PWM_Channel_t ch) {
|
||||||
|
|
||||||
|
TIM_HandleTypeDef* htim = pwm_channel_config[ch].htim;
|
||||||
|
uint32_t channel = pwm_channel_config[ch].channel;
|
||||||
|
|
||||||
|
if(HAL_TIM_PWM_Start(htim, channel)!=HAL_OK){
|
||||||
|
return -1;
|
||||||
|
}else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t BSP_PWM_Set(BSP_PWM_Channel_t ch, float duty_cycle) {
|
||||||
|
if (duty_cycle > 1.0f) return -1;
|
||||||
|
|
||||||
|
uint16_t pulse = duty_cycle/CYCLE * PWM_RESOLUTION;
|
||||||
|
|
||||||
|
if(__HAL_TIM_SET_COMPARE(pwm_channel_config[ch].htim, pwm_channel_config[ch].channel, pulse)!=HAL_OK){
|
||||||
|
return -1;
|
||||||
|
}else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t BSP_PWM_Stop(BSP_PWM_Channel_t ch){
|
||||||
|
|
||||||
|
TIM_HandleTypeDef* htim = pwm_channel_config[ch].htim;
|
||||||
|
uint32_t channel = pwm_channel_config[ch].channel;
|
||||||
|
|
||||||
|
if(HAL_TIM_PWM_Stop(htim, channel)!=HAL_OK){
|
||||||
|
return -1;
|
||||||
|
}else return 0;
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
45
assets/User_code/bsp/servo_pwm.h
Normal file
45
assets/User_code/bsp/servo_pwm.h
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "tim.h"
|
||||||
|
#include "bsp/bsp.h"
|
||||||
|
|
||||||
|
/* Exported constants ------------------------------------------------------- */
|
||||||
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
|
/* Exported types ----------------------------------------------------------- */
|
||||||
|
typedef struct {
|
||||||
|
TIM_HandleTypeDef* htim; // 定时器句柄
|
||||||
|
uint32_t channel; // 定时器通道
|
||||||
|
} PWM_Channel_Config_t;
|
||||||
|
|
||||||
|
#define PWM_RESOLUTION 1000 // ARR change begin
|
||||||
|
#define CYCLE 20 //ms
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
BSP_PWM_SERVO = 0,
|
||||||
|
BSP_PWM_IMU_HEAT = 1,
|
||||||
|
} BSP_PWM_Channel_t;
|
||||||
|
|
||||||
|
const PWM_Channel_Config_t pwm_channel_config[] = {
|
||||||
|
[BSP_PWM_SERVO] = { &htim1, TIM_CHANNEL_1 }, // xxx 对应 TIMx 通道x
|
||||||
|
[BSP_PWM_IMU_HEAT] = { &htim1, TIM_CHANNEL_2 }
|
||||||
|
}; //change end
|
||||||
|
|
||||||
|
/* Exported functions prototypes -------------------------------------------- */
|
||||||
|
int8_t BSP_PWM_Start(BSP_PWM_Channel_t ch);
|
||||||
|
int8_t BSP_PWM_Set(BSP_PWM_Channel_t ch, float duty_cycle);
|
||||||
|
int8_t BSP_PWM_Stop(BSP_PWM_Channel_t ch);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
3
assets/User_code/component/dependencies.csv
Normal file
3
assets/User_code/component/dependencies.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pid,component/filter
|
||||||
|
pid,component/user_math
|
||||||
|
filter,component/user_math
|
|
1
assets/User_code/component/describe.csv
Normal file
1
assets/User_code/component/describe.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
pid,好用的
|
|
186
assets/User_code/component/filter.c
Normal file
186
assets/User_code/component/filter.c
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
各类滤波器。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "filter.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "user_math.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample_freq 采样频率
|
||||||
|
* @param cutoff_freq 截止频率
|
||||||
|
*/
|
||||||
|
void LowPassFilter2p_Init(LowPassFilter2p_t *f, float sample_freq,
|
||||||
|
float cutoff_freq) {
|
||||||
|
if (f == NULL) return;
|
||||||
|
|
||||||
|
f->cutoff_freq = cutoff_freq;
|
||||||
|
|
||||||
|
f->delay_element_1 = 0.0f;
|
||||||
|
f->delay_element_2 = 0.0f;
|
||||||
|
|
||||||
|
if (f->cutoff_freq <= 0.0f) {
|
||||||
|
/* no filtering */
|
||||||
|
f->b0 = 1.0f;
|
||||||
|
f->b1 = 0.0f;
|
||||||
|
f->b2 = 0.0f;
|
||||||
|
|
||||||
|
f->a1 = 0.0f;
|
||||||
|
f->a2 = 0.0f;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float fr = sample_freq / f->cutoff_freq;
|
||||||
|
const float ohm = tanf(M_PI / fr);
|
||||||
|
const float c = 1.0f + 2.0f * cosf(M_PI / 4.0f) * ohm + ohm * ohm;
|
||||||
|
|
||||||
|
f->b0 = ohm * ohm / c;
|
||||||
|
f->b1 = 2.0f * f->b0;
|
||||||
|
f->b2 = f->b0;
|
||||||
|
|
||||||
|
f->a1 = 2.0f * (ohm * ohm - 1.0f) / c;
|
||||||
|
f->a2 = (1.0f - 2.0f * cosf(M_PI / 4.0f) * ohm + ohm * ohm) / c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 施加一次滤波计算
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
float LowPassFilter2p_Apply(LowPassFilter2p_t *f, float sample) {
|
||||||
|
if (f == NULL) return 0.0f;
|
||||||
|
|
||||||
|
/* do the filtering */
|
||||||
|
float delay_element_0 =
|
||||||
|
sample - f->delay_element_1 * f->a1 - f->delay_element_2 * f->a2;
|
||||||
|
|
||||||
|
if (isinf(delay_element_0)) {
|
||||||
|
/* don't allow bad values to propagate via the filter */
|
||||||
|
delay_element_0 = sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float output = delay_element_0 * f->b0 + f->delay_element_1 * f->b1 +
|
||||||
|
f->delay_element_2 * f->b2;
|
||||||
|
|
||||||
|
f->delay_element_2 = f->delay_element_1;
|
||||||
|
f->delay_element_1 = delay_element_0;
|
||||||
|
|
||||||
|
/* return the value. Should be no need to check limits */
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
float LowPassFilter2p_Reset(LowPassFilter2p_t *f, float sample) {
|
||||||
|
if (f == NULL) return 0.0f;
|
||||||
|
|
||||||
|
const float dval = sample / (f->b0 + f->b1 + f->b2);
|
||||||
|
|
||||||
|
if (isfinite(dval)) {
|
||||||
|
f->delay_element_1 = dval;
|
||||||
|
f->delay_element_2 = dval;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
f->delay_element_1 = sample;
|
||||||
|
f->delay_element_2 = sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LowPassFilter2p_Apply(f, sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample_freq 采样频率
|
||||||
|
* @param notch_freq 中心频率
|
||||||
|
* @param bandwidth 带宽
|
||||||
|
*/
|
||||||
|
void NotchFilter_Init(NotchFilter_t *f, float sample_freq, float notch_freq,
|
||||||
|
float bandwidth) {
|
||||||
|
if (f == NULL) return;
|
||||||
|
|
||||||
|
f->notch_freq = notch_freq;
|
||||||
|
f->bandwidth = bandwidth;
|
||||||
|
|
||||||
|
f->delay_element_1 = 0.0f;
|
||||||
|
f->delay_element_2 = 0.0f;
|
||||||
|
|
||||||
|
if (notch_freq <= 0.0f) {
|
||||||
|
/* no filtering */
|
||||||
|
f->b0 = 1.0f;
|
||||||
|
f->b1 = 0.0f;
|
||||||
|
f->b2 = 0.0f;
|
||||||
|
|
||||||
|
f->a1 = 0.0f;
|
||||||
|
f->a2 = 0.0f;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float alpha = tanf(M_PI * bandwidth / sample_freq);
|
||||||
|
const float beta = -cosf(M_2PI * notch_freq / sample_freq);
|
||||||
|
const float a0_inv = 1.0f / (alpha + 1.0f);
|
||||||
|
|
||||||
|
f->b0 = a0_inv;
|
||||||
|
f->b1 = 2.0f * beta * a0_inv;
|
||||||
|
f->b2 = a0_inv;
|
||||||
|
|
||||||
|
f->a1 = f->b1;
|
||||||
|
f->a2 = (1.0f - alpha) * a0_inv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 施加一次滤波计算
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
inline float NotchFilter_Apply(NotchFilter_t *f, float sample) {
|
||||||
|
if (f == NULL) return 0.0f;
|
||||||
|
|
||||||
|
/* Direct Form II implementation */
|
||||||
|
const float delay_element_0 =
|
||||||
|
sample - f->delay_element_1 * f->a1 - f->delay_element_2 * f->a2;
|
||||||
|
const float output = delay_element_0 * f->b0 + f->delay_element_1 * f->b1 +
|
||||||
|
f->delay_element_2 * f->b2;
|
||||||
|
|
||||||
|
f->delay_element_2 = f->delay_element_1;
|
||||||
|
f->delay_element_1 = delay_element_0;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
float NotchFilter_Reset(NotchFilter_t *f, float sample) {
|
||||||
|
if (f == NULL) return 0.0f;
|
||||||
|
|
||||||
|
float dval = sample;
|
||||||
|
|
||||||
|
if (fabsf(f->b0 + f->b1 + f->b2) > FLT_EPSILON) {
|
||||||
|
dval = dval / (f->b0 + f->b1 + f->b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
f->delay_element_1 = dval;
|
||||||
|
f->delay_element_2 = dval;
|
||||||
|
|
||||||
|
return NotchFilter_Apply(f, sample);
|
||||||
|
}
|
102
assets/User_code/component/filter.h
Normal file
102
assets/User_code/component/filter.h
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
各类滤波器。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 二阶低通滤波器 */
|
||||||
|
typedef struct {
|
||||||
|
float cutoff_freq; /* 截止频率 */
|
||||||
|
|
||||||
|
float a1;
|
||||||
|
float a2;
|
||||||
|
|
||||||
|
float b0;
|
||||||
|
float b1;
|
||||||
|
float b2;
|
||||||
|
|
||||||
|
float delay_element_1;
|
||||||
|
float delay_element_2;
|
||||||
|
|
||||||
|
} LowPassFilter2p_t;
|
||||||
|
|
||||||
|
/* 带阻滤波器 */
|
||||||
|
typedef struct {
|
||||||
|
float notch_freq; /* 阻止频率 */
|
||||||
|
float bandwidth; /* 带宽 */
|
||||||
|
|
||||||
|
float a1;
|
||||||
|
float a2;
|
||||||
|
|
||||||
|
float b0;
|
||||||
|
float b1;
|
||||||
|
float b2;
|
||||||
|
float delay_element_1;
|
||||||
|
float delay_element_2;
|
||||||
|
|
||||||
|
} NotchFilter_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample_freq 采样频率
|
||||||
|
* @param cutoff_freq 截止频率
|
||||||
|
*/
|
||||||
|
void LowPassFilter2p_Init(LowPassFilter2p_t *f, float sample_freq,
|
||||||
|
float cutoff_freq);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 施加一次滤波计算
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
float LowPassFilter2p_Apply(LowPassFilter2p_t *f, float sample);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
float LowPassFilter2p_Reset(LowPassFilter2p_t *f, float sample);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample_freq 采样频率
|
||||||
|
* @param notch_freq 中心频率
|
||||||
|
* @param bandwidth 带宽
|
||||||
|
*/
|
||||||
|
void NotchFilter_Init(NotchFilter_t *f, float sample_freq, float notch_freq,
|
||||||
|
float bandwidth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 施加一次滤波计算
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
float NotchFilter_Apply(NotchFilter_t *f, float sample);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置滤波器
|
||||||
|
*
|
||||||
|
* @param f 滤波器
|
||||||
|
* @param sample 采样的值
|
||||||
|
* @return float 滤波后的值
|
||||||
|
*/
|
||||||
|
float NotchFilter_Reset(NotchFilter_t *f, float sample);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
161
assets/User_code/component/pid.c
Normal file
161
assets/User_code/component/pid.c
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
Modified from
|
||||||
|
https://github.com/PX4/Firmware/blob/master/src/lib/pid/pid.cpp
|
||||||
|
|
||||||
|
参考资料:
|
||||||
|
https://github.com/PX4/Firmware/issues/12362
|
||||||
|
https://dev.px4.io/master/en/flight_stack/controller_diagrams.html
|
||||||
|
https://docs.px4.io/master/en/config_mc/pid_tuning_guide_multicopter.html#standard_form
|
||||||
|
https://www.controleng.com/articles/not-all-pid-controllers-are-the-same/
|
||||||
|
https://en.wikipedia.org/wiki/PID_controller
|
||||||
|
http://brettbeauregard.com/blog/2011/04/improving-the-beginner%E2%80%99s-pid-derivative-kick/
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "pid.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "user_math.h"
|
||||||
|
|
||||||
|
#define SIGMA 0.000001f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化PID
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @param mode PID模式
|
||||||
|
* @param sample_freq 采样频率
|
||||||
|
* @param param PID参数
|
||||||
|
* @return int8_t 0对应没有错误
|
||||||
|
*/
|
||||||
|
int8_t PID_Init(KPID_t *pid, KPID_Mode_t mode, float sample_freq,
|
||||||
|
const KPID_Params_t *param) {
|
||||||
|
if (pid == NULL) return -1;
|
||||||
|
|
||||||
|
if (!isfinite(param->p)) return -1;
|
||||||
|
if (!isfinite(param->i)) return -1;
|
||||||
|
if (!isfinite(param->d)) return -1;
|
||||||
|
if (!isfinite(param->i_limit)) return -1;
|
||||||
|
if (!isfinite(param->out_limit)) return -1;
|
||||||
|
pid->param = param;
|
||||||
|
|
||||||
|
float dt_min = 1.0f / sample_freq;
|
||||||
|
if (isfinite(dt_min))
|
||||||
|
pid->dt_min = dt_min;
|
||||||
|
else
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
LowPassFilter2p_Init(&(pid->dfilter), sample_freq, pid->param->d_cutoff_freq);
|
||||||
|
|
||||||
|
pid->mode = mode;
|
||||||
|
PID_Reset(pid);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PID计算
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @param sp 设定值
|
||||||
|
* @param fb 反馈值
|
||||||
|
* @param fb_dot 反馈值微分
|
||||||
|
* @param dt 间隔时间
|
||||||
|
* @return float 计算的输出
|
||||||
|
*/
|
||||||
|
float PID_Calc(KPID_t *pid, float sp, float fb, float fb_dot, float dt) {
|
||||||
|
if (!isfinite(sp) || !isfinite(fb) || !isfinite(fb_dot) || !isfinite(dt)) {
|
||||||
|
return pid->last.out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 计算误差值 */
|
||||||
|
const float err = CircleError(sp, fb, pid->param->range);
|
||||||
|
|
||||||
|
/* 计算P项 */
|
||||||
|
const float k_err = err * pid->param->k;
|
||||||
|
|
||||||
|
/* 计算D项 */
|
||||||
|
const float k_fb = pid->param->k * fb;
|
||||||
|
const float filtered_k_fb = LowPassFilter2p_Apply(&(pid->dfilter), k_fb);
|
||||||
|
|
||||||
|
float d;
|
||||||
|
switch (pid->mode) {
|
||||||
|
case KPID_MODE_CALC_D:
|
||||||
|
/* 通过fb计算D,避免了由于sp变化导致err突变的问题 */
|
||||||
|
/* 当sp不变时,err的微分等于负的fb的微分 */
|
||||||
|
d = (filtered_k_fb - pid->last.k_fb) / fmaxf(dt, pid->dt_min);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KPID_MODE_SET_D:
|
||||||
|
d = fb_dot;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KPID_MODE_NO_D:
|
||||||
|
d = 0.0f;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pid->last.err = err;
|
||||||
|
pid->last.k_fb = filtered_k_fb;
|
||||||
|
|
||||||
|
if (!isfinite(d)) d = 0.0f;
|
||||||
|
|
||||||
|
/* 计算PD输出 */
|
||||||
|
float output = (k_err * pid->param->p) - (d * pid->param->d);
|
||||||
|
|
||||||
|
/* 计算I项 */
|
||||||
|
const float i = pid->i + (k_err * dt);
|
||||||
|
const float i_out = i * pid->param->i;
|
||||||
|
|
||||||
|
if (pid->param->i > SIGMA) {
|
||||||
|
/* 检查是否饱和 */
|
||||||
|
if (isfinite(i)) {
|
||||||
|
if ((fabsf(output + i_out) <= pid->param->out_limit) &&
|
||||||
|
(fabsf(i) <= pid->param->i_limit)) {
|
||||||
|
/* 未饱和,使用新积分 */
|
||||||
|
pid->i = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 计算PID输出 */
|
||||||
|
output += i_out;
|
||||||
|
|
||||||
|
/* 限制输出 */
|
||||||
|
if (isfinite(output)) {
|
||||||
|
if (pid->param->out_limit > SIGMA) {
|
||||||
|
output = AbsClip(output, pid->param->out_limit);
|
||||||
|
}
|
||||||
|
pid->last.out = output;
|
||||||
|
}
|
||||||
|
return pid->last.out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置微分项
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @return int8_t 0对应没有错误
|
||||||
|
*/
|
||||||
|
int8_t PID_ResetIntegral(KPID_t *pid) {
|
||||||
|
if (pid == NULL) return -1;
|
||||||
|
|
||||||
|
pid->i = 0.0f;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置PID
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @return int8_t 0对应没有错误
|
||||||
|
*/
|
||||||
|
int8_t PID_Reset(KPID_t *pid) {
|
||||||
|
if (pid == NULL) return -1;
|
||||||
|
|
||||||
|
pid->i = 0.0f;
|
||||||
|
pid->last.err = 0.0f;
|
||||||
|
pid->last.k_fb = 0.0f;
|
||||||
|
pid->last.out = 0.0f;
|
||||||
|
LowPassFilter2p_Reset(&(pid->dfilter), 0.0f);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
94
assets/User_code/component/pid.h
Normal file
94
assets/User_code/component/pid.h
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
Modified from
|
||||||
|
https://github.com/PX4/Firmware/blob/master/src/lib/pid/pid.h
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "filter.h"
|
||||||
|
|
||||||
|
/* PID模式 */
|
||||||
|
typedef enum {
|
||||||
|
KPID_MODE_NO_D = 0, /* 不使用微分项,PI控制器 */
|
||||||
|
KPID_MODE_CALC_D, /* 根据反馈的值计算离散微分,忽略PID_Calc中的fb_dot */
|
||||||
|
KPID_MODE_SET_D /* 直接提供微分值,PID_Calc中的fb_dot将被使用,(Gyros) */
|
||||||
|
} KPID_Mode_t;
|
||||||
|
|
||||||
|
/* PID参数 */
|
||||||
|
typedef struct {
|
||||||
|
float k; /* 控制器增益,设置为1用于并行模式 */
|
||||||
|
float p; /* 比例项增益,设置为1用于标准形式 */
|
||||||
|
float i; /* 积分项增益 */
|
||||||
|
float d; /* 微分项增益 */
|
||||||
|
float i_limit; /* 积分项上限 */
|
||||||
|
float out_limit; /* 输出绝对值限制 */
|
||||||
|
float d_cutoff_freq; /* D项低通截止频率 */
|
||||||
|
float range; /* 计算循环误差时使用,大于0时启用 */
|
||||||
|
} KPID_Params_t;
|
||||||
|
|
||||||
|
/* PID主结构体 */
|
||||||
|
typedef struct {
|
||||||
|
KPID_Mode_t mode;
|
||||||
|
const KPID_Params_t *param;
|
||||||
|
|
||||||
|
float dt_min; /* 最小PID_Calc调用间隔 */
|
||||||
|
float i; /* 积分 */
|
||||||
|
|
||||||
|
struct {
|
||||||
|
float err; /* 上次误差 */
|
||||||
|
float k_fb; /* 上次反馈值 */
|
||||||
|
float out; /* 上次输出 */
|
||||||
|
} last;
|
||||||
|
|
||||||
|
LowPassFilter2p_t dfilter; /* D项低通滤波器 */
|
||||||
|
} KPID_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化PID
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @param mode PID模式
|
||||||
|
* @param sample_freq 采样频率
|
||||||
|
* @param param PID参数
|
||||||
|
* @return int8_t 0对应没有错误
|
||||||
|
*/
|
||||||
|
int8_t PID_Init(KPID_t *pid, KPID_Mode_t mode, float sample_freq,
|
||||||
|
const KPID_Params_t *param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PID计算
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @param sp 设定值
|
||||||
|
* @param fb 反馈值
|
||||||
|
* @param fb_dot 反馈值微分
|
||||||
|
* @param dt 间隔时间
|
||||||
|
* @return float 计算的输出
|
||||||
|
*/
|
||||||
|
float PID_Calc(KPID_t *pid, float sp, float fb, float fb_dot, float dt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置微分项
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @return int8_t 0对应没有错误
|
||||||
|
*/
|
||||||
|
int8_t PID_ResetIntegral(KPID_t *pid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置PID
|
||||||
|
*
|
||||||
|
* @param pid PID结构体
|
||||||
|
* @return int8_t 0对应没有错误
|
||||||
|
*/
|
||||||
|
int8_t PID_Reset(KPID_t *pid);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
88
assets/User_code/component/user_math.c
Normal file
88
assets/User_code/component/user_math.c
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
自定义的数学运算。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "user_math.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
inline float InvSqrt(float x) {
|
||||||
|
//#if 0
|
||||||
|
/* Fast inverse square-root */
|
||||||
|
/* See: http://en.wikipedia.org/wiki/Fast_inverse_square_root */
|
||||||
|
float halfx = 0.5f * x;
|
||||||
|
float y = x;
|
||||||
|
long i = *(long*)&y;
|
||||||
|
i = 0x5f3759df - (i>>1);
|
||||||
|
y = *(float*)&i;
|
||||||
|
y = y * (1.5f - (halfx * y * y));
|
||||||
|
y = y * (1.5f - (halfx * y * y));
|
||||||
|
return y;
|
||||||
|
//#else
|
||||||
|
// return 1.0f / sqrtf(x);
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float AbsClip(float in, float limit) {
|
||||||
|
return (in < -limit) ? -limit : ((in > limit) ? limit : in);
|
||||||
|
}
|
||||||
|
|
||||||
|
float fAbs(float in){
|
||||||
|
return (in > 0) ? in : -in;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Clip(float *origin, float min, float max) {
|
||||||
|
if (*origin > max) *origin = max;
|
||||||
|
if (*origin < min) *origin = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float Sign(float in) { return (in > 0) ? 1.0f : 0.0f; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief 计算循环值的误差,用于没有负数值,并在一定范围内变化的值
|
||||||
|
* 例如编码器:相差1.5PI其实等于相差-0.5PI
|
||||||
|
*
|
||||||
|
* \param sp 被操作的值
|
||||||
|
* \param fb 变化量
|
||||||
|
* \param range 被操作的值变化范围,正数时起效
|
||||||
|
*
|
||||||
|
* \return 函数运行结果
|
||||||
|
*/
|
||||||
|
inline float CircleError(float sp, float fb, float range) {
|
||||||
|
float error = sp - fb;
|
||||||
|
if (range > 0.0f) {
|
||||||
|
float half_range = range / 2.0f;
|
||||||
|
|
||||||
|
if (error > half_range)
|
||||||
|
error -= range;
|
||||||
|
else if (error < -half_range)
|
||||||
|
error += range;
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief 循环加法,用于没有负数值,并在一定范围内变化的值
|
||||||
|
* 例如编码器,在0-2PI内变化,1.5PI + 1.5PI = 1PI
|
||||||
|
*
|
||||||
|
* \param origin 被操作的值
|
||||||
|
* \param delta 变化量
|
||||||
|
* \param range 被操作的值变化范围,正数时起效
|
||||||
|
*/
|
||||||
|
inline void CircleAdd(float *origin, float delta, float range) {
|
||||||
|
float out = *origin + delta;
|
||||||
|
if (range > 0.0f) {
|
||||||
|
if (out >= range)
|
||||||
|
out -= range;
|
||||||
|
else if (out < 0.0f)
|
||||||
|
out += range;
|
||||||
|
}
|
||||||
|
*origin = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 循环值取反
|
||||||
|
*
|
||||||
|
* @param origin 被操作的值
|
||||||
|
*/
|
||||||
|
inline void CircleReverse(float *origin) { *origin = -(*origin) + M_2PI; }
|
130
assets/User_code/component/user_math.h
Normal file
130
assets/User_code/component/user_math.h
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
自定义的数学运算。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <float.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#define M_DEG2RAD_MULT (0.01745329251f)
|
||||||
|
#define M_RAD2DEG_MULT (57.2957795131f)
|
||||||
|
|
||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265358979323846f
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef M_2PI
|
||||||
|
#define M_2PI 6.28318530717958647692f
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define max(a, b) \
|
||||||
|
({ \
|
||||||
|
__typeof__(a) _a = (a); \
|
||||||
|
__typeof__(b) _b = (b); \
|
||||||
|
_a > _b ? _a : _b; \
|
||||||
|
})
|
||||||
|
|
||||||
|
#define min(a, b) \
|
||||||
|
({ \
|
||||||
|
__typeof__(a) _a = (a); \
|
||||||
|
__typeof__(b) _b = (b); \
|
||||||
|
_a < _b ? _a : _b; \
|
||||||
|
})
|
||||||
|
|
||||||
|
/* 移动向量 */
|
||||||
|
typedef struct {
|
||||||
|
float vx; /* 前后平移 */
|
||||||
|
float vy; /* 左右平移 */
|
||||||
|
float wz; /* 转动 */
|
||||||
|
} MoveVector_t;
|
||||||
|
|
||||||
|
float InvSqrt(float x);
|
||||||
|
|
||||||
|
float AbsClip(float in, float limit);
|
||||||
|
|
||||||
|
float fAbs(float in);
|
||||||
|
|
||||||
|
void Clip(float *origin, float min, float max);
|
||||||
|
|
||||||
|
float Sign(float in);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief 计算循环值的误差,用于没有负数值,并在一定范围内变化的值
|
||||||
|
* 例如编码器:相差1.5PI其实等于相差-0.5PI
|
||||||
|
*
|
||||||
|
* \param sp 被操作的值
|
||||||
|
* \param fb 变化量
|
||||||
|
* \param range 被操作的值变化范围,正数时起效
|
||||||
|
*
|
||||||
|
* \return 函数运行结果
|
||||||
|
*/
|
||||||
|
float CircleError(float sp, float fb, float range);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief 循环加法,用于没有负数值,并在一定范围内变化的值
|
||||||
|
* 例如编码器,在0-2PI内变化,1.5PI + 1.5PI = 1PI
|
||||||
|
*
|
||||||
|
* \param origin 被操作的值
|
||||||
|
* \param delta 变化量
|
||||||
|
* \param range 被操作的值变化范围,正数时起效
|
||||||
|
*/
|
||||||
|
void CircleAdd(float *origin, float delta, float range);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 循环值取反
|
||||||
|
*
|
||||||
|
* @param origin 被操作的值
|
||||||
|
*/
|
||||||
|
void CircleReverse(float *origin);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 如果表达式的值为假则运行处理函数
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#define ASSERT(expr) \
|
||||||
|
do { \
|
||||||
|
if (!(expr)) { \
|
||||||
|
VerifyFailed(__FILE__, __LINE__); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
#else
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 未定DEBUG,表达式不会运行,断言被忽略
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#define ASSERT(expr) ((void)(0))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 如果表达式的值为假则运行处理函数
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#define VERIFY(expr) \
|
||||||
|
do { \
|
||||||
|
if (!(expr)) { \
|
||||||
|
VerifyFailed(__FILE__, __LINE__); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
#else
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 表达式会运行,忽略表达式结果
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#define VERIFY(expr) ((void)(expr))
|
||||||
|
#endif
|
BIN
assets/User_code/device/.DS_Store
vendored
Normal file
BIN
assets/User_code/device/.DS_Store
vendored
Normal file
Binary file not shown.
5
assets/User_code/device/dependencies.csv
Normal file
5
assets/User_code/device/dependencies.csv
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
oled_i2c,bsp/i2c
|
||||||
|
bmp280_i2c,bsp/i2c
|
||||||
|
pc_uart,bsp/uart
|
||||||
|
key_gpio,bsp/gpio_exti
|
||||||
|
servo,bsp/servo_pwm
|
|
1
assets/User_code/device/describe.csv
Normal file
1
assets/User_code/device/describe.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
servo,测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息
|
|
65
assets/User_code/device/key_gpio.c
Normal file
65
assets/User_code/device/key_gpio.c
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include "key_gpio.h"
|
||||||
|
#include "device.h"
|
||||||
|
#include "bsp/gpio_exti.h"
|
||||||
|
#include "gpio.h"
|
||||||
|
/* Private define ----------------------------------------------------------- */
|
||||||
|
#define DEBOUNCE_TIME_MS 20
|
||||||
|
/* Private macro ------------------------------------------------------------ */
|
||||||
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
|
/* Private variables -------------------------------------------------------- */
|
||||||
|
/* Private function -------------------------------------------------------- */
|
||||||
|
/* 外部声明标志位(标志位) */
|
||||||
|
volatile uint8_t key_flag = 0; // 1=按下,0=松开
|
||||||
|
volatile uint8_t key_exti = 0;
|
||||||
|
volatile uint8_t key_pressed = 0; // 全局标志位
|
||||||
|
static uint32_t last_debounce_time = 0; // 消抖
|
||||||
|
|
||||||
|
/* Private function -------------------------------------------------------- */
|
||||||
|
static void KEY_Interrupt_Callback(void) {
|
||||||
|
// 切换标志位状态
|
||||||
|
|
||||||
|
key_flag = !key_flag;
|
||||||
|
key_exti = 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exported functions ------------------------------------------------------- */
|
||||||
|
void KEY_Process(void)
|
||||||
|
{
|
||||||
|
BSP_GPIO_RegisterCallback(BSP_GPIO_USER_KEY, BSP_GPIO_EXTI_CB, KEY_Interrupt_Callback);
|
||||||
|
|
||||||
|
if(key_exti == 1)
|
||||||
|
{
|
||||||
|
uint32_t now = HAL_GetTick();
|
||||||
|
// 检查是否超过消抖时间
|
||||||
|
if ((now - last_debounce_time) > DEBOUNCE_TIME_MS) {
|
||||||
|
// 更新有效状态(假设按下为低电平)
|
||||||
|
if(key_flag == 0)
|
||||||
|
{
|
||||||
|
key_pressed = DEVICE_KEY_RELEASED;
|
||||||
|
}
|
||||||
|
if(key_flag == 1)
|
||||||
|
{
|
||||||
|
key_pressed = DEVICE_KEY_PRESSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6);
|
||||||
|
|
||||||
|
}
|
||||||
|
last_debounce_time = now; // 重置消抖计时器
|
||||||
|
key_exti = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t KEY_Get_State(void) {
|
||||||
|
return key_pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
21
assets/User_code/device/key_gpio.h
Normal file
21
assets/User_code/device/key_gpio.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#ifndef KEY_GPIO_H
|
||||||
|
#define KEY_GPIO_H
|
||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
/* Exported constants ------------------------------------------------------- */
|
||||||
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
|
/* Exported types ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
///* KEY按键状态,设置用 */
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
DEVICE_KEY_RELEASED, //按键释放
|
||||||
|
DEVICE_KEY_PRESSED, //按键按下
|
||||||
|
} DEVICE_KEY_Status_t;
|
||||||
|
|
||||||
|
void KEY_Process(void);
|
||||||
|
uint8_t KEY_Get_State(void);
|
||||||
|
|
||||||
|
#endif
|
36
assets/User_code/device/servo.c
Normal file
36
assets/User_code/device/servo.c
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include "main.h"
|
||||||
|
#include "servo.h"
|
||||||
|
|
||||||
|
#include "bsp/servo_pwm.h"
|
||||||
|
|
||||||
|
|
||||||
|
/* Private define ----------------------------------------------------------- */
|
||||||
|
#define MIN_CYCLE 0.5f //change begin
|
||||||
|
#define MAX_CYCLE 2.5f
|
||||||
|
#define ANGLE_LIMIT 180 //change end
|
||||||
|
/* Private macro ------------------------------------------------------------ */
|
||||||
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
|
/* Private variables -------------------------------------------------------- */
|
||||||
|
/* Private function -------------------------------------------------------- */
|
||||||
|
/* Exported functions ------------------------------------------------------- */
|
||||||
|
int serve_Init(BSP_PWM_Channel_t ch)
|
||||||
|
{
|
||||||
|
if(BSP_PWM_Start(ch)!=0){
|
||||||
|
return -1;
|
||||||
|
}else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int set_servo_angle(BSP_PWM_Channel_t ch,float angle)
|
||||||
|
{
|
||||||
|
if (angle < 0.0f || angle > ANGLE_LIMIT) {
|
||||||
|
return -1; // ÎÞЧµÄ½Ç¶È
|
||||||
|
}
|
||||||
|
|
||||||
|
float duty_cycle=MIN_CYCLE+(MAX_CYCLE-MIN_CYCLE)*(angle/ANGLE_LIMIT);
|
||||||
|
if(BSP_PWM_Set(ch,duty_cycle)!=0){
|
||||||
|
return -1;
|
||||||
|
}else return 0;
|
||||||
|
}
|
||||||
|
|
41
assets/User_code/device/servo.h
Normal file
41
assets/User_code/device/servo.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "tim.h"
|
||||||
|
#include "bsp/bsp.h"
|
||||||
|
#include "bsp/servo_pwm.h"
|
||||||
|
/* Exported constants ------------------------------------------------------- */
|
||||||
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
|
/* Exported types ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extern int serve_Init(BSP_PWM_Channel_t ch);
|
||||||
|
extern int set_servo_angle(BSP_PWM_Channel_t ch,float angle);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
5
assets/User_code/module/dependencies.csv
Normal file
5
assets/User_code/module/dependencies.csv
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
oled_i2c,bsp/i2c
|
||||||
|
bmp280_i2c,bsp/i2c
|
||||||
|
pc_uart,bsp/uart
|
||||||
|
key_gpio,bsp/gpio_exti
|
||||||
|
servo,bsp/servo_pwm
|
|
0
assets/User_code/module/describe.csv
Normal file
0
assets/User_code/module/describe.csv
Normal file
|
0
assets/User_code/task/.gitkeep
Normal file
0
assets/User_code/task/.gitkeep
Normal file
@ -1,9 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
初始化任务
|
Init Task
|
||||||
|
任务初始化,创建各个线程任务和消息队列
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Includes ----------------------------------------------------------------- */
|
/* Includes ----------------------------------------------------------------- */
|
||||||
#include "task\user_task.h"
|
#include "task/user_task.h"
|
||||||
|
|
||||||
/* USER INCLUDE BEGIN */
|
/* USER INCLUDE BEGIN */
|
||||||
|
|
||||||
@ -23,17 +24,19 @@
|
|||||||
*/
|
*/
|
||||||
void Task_Init(void *argument) {
|
void Task_Init(void *argument) {
|
||||||
(void)argument; /* 未使用argument,消除警告 */
|
(void)argument; /* 未使用argument,消除警告 */
|
||||||
|
/* USER CODE INIT BEGIN */
|
||||||
|
|
||||||
osKernelLock(); // 锁定内核,防止任务切换
|
/* USER CODE INIT END */
|
||||||
|
osKernelLock(); /* 锁定内核,防止任务切换 */
|
||||||
|
|
||||||
// 创建线程
|
/* 创建任务线程 */
|
||||||
{{thread_creation_code}}
|
{{thread_creation_code}}
|
||||||
|
|
||||||
// 创建消息队列
|
// 创建消息队列
|
||||||
/* USER MESSAGE BEGIN */
|
/* USER MESSAGE BEGIN */
|
||||||
task_runtime.msgq.user_msg= osMessageQueueNew(2u, 10, NULL);
|
task_runtime.msgq.user_msg= osMessageQueueNew(2u, 10, NULL);
|
||||||
/* USER MESSAGE END */
|
/* USER MESSAGE END */
|
||||||
|
|
||||||
osKernelUnlock(); // 解锁内核
|
osKernelUnlock(); // 解锁内核
|
||||||
osThreadTerminate(osThreadGetId()); // 任务完成后结束自身
|
osThreadTerminate(osThreadGetId()); // 任务完成后结束自身
|
||||||
}
|
}
|
||||||
|
|
@ -1,38 +1,56 @@
|
|||||||
/*
|
/*
|
||||||
{{task_name}} Task
|
{{task_name}} Task
|
||||||
|
{{task_description}}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Includes ----------------------------------------------------------------- */
|
/* Includes ----------------------------------------------------------------- */
|
||||||
#include "task\user_task.h"
|
#include "task/user_task.h"
|
||||||
|
/* USER INCLUDE BEGIN*/
|
||||||
|
|
||||||
|
/* USER INCLUDE END*/
|
||||||
|
|
||||||
/* Private typedef ---------------------------------------------------------- */
|
/* Private typedef ---------------------------------------------------------- */
|
||||||
/* Private define ----------------------------------------------------------- */
|
/* Private define ----------------------------------------------------------- */
|
||||||
/* Private macro ------------------------------------------------------------ */
|
/* Private macro ------------------------------------------------------------ */
|
||||||
/* Private variables -------------------------------------------------------- */
|
/* Private variables -------------------------------------------------------- */
|
||||||
|
/* USER STRUCT BEGIN*/
|
||||||
|
|
||||||
|
/* USER STRUCT END*/
|
||||||
|
|
||||||
/* Private function --------------------------------------------------------- */
|
/* Private function --------------------------------------------------------- */
|
||||||
/* Exported functions ------------------------------------------------------- */
|
/* Exported functions ------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
|
||||||
* \brief {{task_name}} Task
|
|
||||||
*
|
|
||||||
* \param argument 未使用
|
|
||||||
*/
|
|
||||||
void {{task_function}}(void *argument) {
|
void {{task_function}}(void *argument) {
|
||||||
(void)argument; /* 未使用argument,消除警告 */
|
(void)argument; /* 未使用argument,消除警告 */
|
||||||
|
|
||||||
|
{% if freq_control %}
|
||||||
/* 计算任务运行到指定频率需要等待的tick数 */
|
/* 计算任务运行到指定频率需要等待的tick数 */
|
||||||
const uint32_t delay_tick = osKernelGetTickFreq() / {{task_frequency}};
|
const uint32_t delay_tick = osKernelGetTickFreq() / {{task_frequency}};
|
||||||
|
|
||||||
osDelay({{task_delay}}); /* 延时一段时间再开启任务 */
|
osDelay({{task_delay}}); /* 延时一段时间再开启任务 */
|
||||||
|
|
||||||
|
/* USER CODE INIT BEGIN*/
|
||||||
|
|
||||||
|
/* USER CODE INIT END*/
|
||||||
|
|
||||||
uint32_t tick = osKernelGetTickCount(); /* 控制任务运行频率的计时 */
|
uint32_t tick = osKernelGetTickCount(); /* 控制任务运行频率的计时 */
|
||||||
while (1) {
|
while (1) {
|
||||||
tick += delay_tick; /* 计算下一个唤醒时刻 */
|
tick += delay_tick; /* 计算下一个唤醒时刻 */
|
||||||
|
/* USER CODE BEGIN */
|
||||||
|
|
||||||
/*User code begin*/
|
/* USER CODE END */
|
||||||
|
|
||||||
/*User code end*/
|
|
||||||
|
|
||||||
osDelayUntil(tick); /* 运行结束,等待下一次唤醒 */
|
osDelayUntil(tick); /* 运行结束,等待下一次唤醒 */
|
||||||
}
|
}
|
||||||
|
{% else %}
|
||||||
|
osDelay({{task_delay}}); /* 延时一段时间再开启任务 */
|
||||||
|
|
||||||
|
/* USER CODE INIT BEGIN*/
|
||||||
|
|
||||||
|
/* USER CODE INIT END*/
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
/* USER CODE BEGIN */
|
||||||
|
|
||||||
|
/* USER CODE END */
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
#include "task\user_task.h"
|
#include "task/user_task.h"
|
||||||
|
|
||||||
Task_Runtime_t task_runtime;
|
Task_Runtime_t task_runtime;
|
||||||
|
|
||||||
@ -8,5 +8,5 @@ const osThreadAttr_t attr_init = {
|
|||||||
.stack_size = 256 * 4,
|
.stack_size = 256 * 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
// USER TASK
|
/* User_task */
|
||||||
{{task_attr_definitions}}
|
{{task_attr_definitions}}
|
80
assets/User_code/task/user_task.h.template
Normal file
80
assets/User_code/task/user_task.h.template
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
/* Includes ----------------------------------------------------------------- */
|
||||||
|
#include <cmsis_os2.h>
|
||||||
|
#include "FreeRTOS.h"
|
||||||
|
#include "task.h"
|
||||||
|
|
||||||
|
/* USER INCLUDE BEGIN */
|
||||||
|
|
||||||
|
/* USER INCLUDE END */
|
||||||
|
/* Exported constants ------------------------------------------------------- */
|
||||||
|
/* 任务运行频率 */
|
||||||
|
{{task_frequency_definitions}}
|
||||||
|
|
||||||
|
/* 任务初始化延时ms */
|
||||||
|
#define TASK_INIT_DELAY (100u)
|
||||||
|
{{task_init_delay_definitions}}
|
||||||
|
|
||||||
|
/* Exported defines --------------------------------------------------------- */
|
||||||
|
/* Exported macro ----------------------------------------------------------- */
|
||||||
|
/* Exported types ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* 任务运行时结构体 */
|
||||||
|
typedef struct {
|
||||||
|
/* 各任务,也可以叫做线程 */
|
||||||
|
struct {
|
||||||
|
{{thread_definitions}}
|
||||||
|
} thread;
|
||||||
|
|
||||||
|
/* USER MESSAGE BEGIN */
|
||||||
|
struct {
|
||||||
|
osMessageQueueId_t user_msg; /* 用户自定义任务消息队列 */
|
||||||
|
} msgq;
|
||||||
|
/* USER MESSAGE END */
|
||||||
|
|
||||||
|
/* 机器人状态 */
|
||||||
|
struct {
|
||||||
|
float battery; /* 电池电量百分比 */
|
||||||
|
float vbat; /* 电池电压 */
|
||||||
|
float cpu_temp; /* CPU温度 */
|
||||||
|
} status;
|
||||||
|
|
||||||
|
/* USER CONFIG BEGIN */
|
||||||
|
|
||||||
|
/* USER CONFIG END */
|
||||||
|
|
||||||
|
/* 各任务的stack使用 */
|
||||||
|
struct {
|
||||||
|
{{stack_definitions}}
|
||||||
|
} stack_water_mark;
|
||||||
|
|
||||||
|
/* 各任务运行频率 */
|
||||||
|
struct {
|
||||||
|
{{freq_definitions}}
|
||||||
|
} freq;
|
||||||
|
|
||||||
|
/* 任务最近运行时间 */
|
||||||
|
struct {
|
||||||
|
{{last_up_time_definitions}}
|
||||||
|
} last_up_time;
|
||||||
|
|
||||||
|
} Task_Runtime_t;
|
||||||
|
|
||||||
|
/* 任务运行时结构体 */
|
||||||
|
extern Task_Runtime_t task_runtime;
|
||||||
|
|
||||||
|
/* 初始化任务句柄 */
|
||||||
|
extern const osThreadAttr_t attr_init;
|
||||||
|
{{task_attr_declarations}}
|
||||||
|
|
||||||
|
/* 任务函数声明 */
|
||||||
|
void Task_Init(void *argument);
|
||||||
|
{{task_function_declarations}}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user