Compare commits

...

37 Commits
v0.0.1 ... main

Author SHA1 Message Date
7951dae760 修改版本 2025-07-26 01:24:50 +08:00
501a9ddff4 重构完代码结构了 2025-07-26 01:22:52 +08:00
47e0b8419f 优化了ai个零件库 2025-07-25 16:39:32 +08:00
9fc6b4577a 继续改 2025-07-25 02:54:03 +08:00
78661f450b 重构架构 2025-07-24 21:23:51 +08:00
62b4b07912 修改保留用户区域逻辑 2025-06-21 14:25:50 +08:00
f2fedac360 修复task.c 2025-06-20 01:13:04 +08:00
34a0874156 各种修复 2025-06-20 00:54:36 +08:00
e9eb169547 添加了可选运行频率方案 2025-06-20 00:30:21 +08:00
606bd7e054 修改代码模版 2025-06-19 23:50:19 +08:00
ae6246474b 添加新的保留区域 2025-06-19 23:47:36 +08:00
c737ec79d4 新版MRobotv1.0 2025-06-19 23:42:44 +08:00
a1da927d9c 修改文件夹名称 2025-06-18 14:39:29 +08:00
ffad148fcc 修改好了零件库功能 2025-06-18 14:28:31 +08:00
f680b91816 添加零件库 2025-06-18 14:10:27 +08:00
b893c30eb3 完善MRUI架构 2025-06-17 21:33:41 +08:00
86b0062881 创建minishell 2025-06-17 18:00:13 +08:00
26ce1316ca 构建home 2025-06-13 01:40:35 +08:00
e7822bbf64 创建新ui 2025-06-13 01:38:37 +08:00
RB
79da21bca0 添加启动页 2025-05-25 15:54:03 +08:00
RB
b879e0ae94 回退一下 2025-05-25 15:24:59 +08:00
RB
214ac00e90 添加机械零件库 2025-05-25 11:55:32 +08:00
RB
c4731883f2 0.02版本 2025-05-25 02:32:52 +08:00
RB
544b3745d5 更新MRtool 2025-05-25 01:11:07 +08:00
RB
511f9f4da8 MR_Tool初有成效 2025-05-24 23:49:21 +08:00
RB
2e8c902dd2 创建函数拟合工具,准备创建MR工具箱 2025-05-24 19:59:53 +08:00
RB
918f6b443c 添加bsp的can 2025-05-24 15:45:30 +08:00
RB
3da80d5efb 添加gpio_key 2025-05-24 15:33:24 +08:00
RB
37d6f70055 修改报错 2025-05-08 11:39:58 +08:00
RB
be987d6bdd 移除错误的环境配置 2025-05-08 11:36:11 +08:00
RB
d7f6e93b5c 修改依赖 2025-05-08 10:54:17 +08:00
RB
cddd7a2ad4 添加pid和flitter 2025-05-08 10:50:44 +08:00
RB
97d42c70d0 修改依赖 2025-05-07 23:47:53 +08:00
RB
ced464290e 上传us延时 2025-05-07 23:38:39 +08:00
RB
9f964e1532 添加pwm(需修改) 2025-05-06 00:32:46 +08:00
RB
52acfaf20c 添加gpio中断bsp 2025-05-05 00:02:24 +08:00
RB
af69d030fe 添加图标和delay 2025-05-04 23:49:39 +08:00
113 changed files with 3983 additions and 1118 deletions

BIN
.DS_Store vendored

Binary file not shown.

5
.gitignore vendored
View File

@ -28,3 +28,8 @@ Examples/
!*.axf !*.axf
!*.bin !*.bin
!*.hex !*.hex
build/
dist/
*.spec
*.exe

18
MRobot.iss Normal file
View 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
View File

@ -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_() 是一个阻塞调用,直到应用程序退出。

View File

@ -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"

View File

@ -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;
}

View File

@ -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);

View File

@ -1,7 +0,0 @@
/*
*/
#include "user_math.h"
#include <stdint.h>

View File

@ -1,13 +0,0 @@
/*
*/
#pragma once
#include <math.h>
#include <stdint.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif

View File

@ -1,3 +0,0 @@
oled_i2c,bsp/i2c
bmp280_i2c,bsp/i2c
pc_uart,bsp/uart
1 oled_i2c bsp/i2c
2 bmp280_i2c bsp/i2c
3 pc_uart bsp/uart

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

65
app/about_interface.py Normal file
View 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
View 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
View 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)

View 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
View 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
View 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
View 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")

View 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])

View 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()

Binary file not shown.

Binary file not shown.

14
app/tools/check_update.py Normal file
View 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
View 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
View 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

View 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)

View 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

Binary file not shown.

BIN
assets/User_code/bsp/.DS_Store vendored Normal file

Binary file not shown.

141
assets/User_code/bsp/can.c Normal file
View 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;
}

View 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

View 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;
}

View 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

View File

@ -0,0 +1,2 @@
uart,要求开启dma和中断
can,要求开启can的中断
1 uart 要求开启dma和中断
2 can 要求开启can的中断

View 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;
}

View 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

View File

@ -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

View File

@ -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 ----------------------------------------------------------- */

View 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;
};

View 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

View File

@ -0,0 +1,3 @@
pid,component/filter
pid,component/user_math
filter,component/user_math
1 pid component/filter
2 pid component/user_math
3 filter component/user_math

View File

@ -0,0 +1 @@
pid,好用的
1 pid 好用的

View 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);
}

View 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

View 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;
}

View 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

View 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; }

View 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

Binary file not shown.

View 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 oled_i2c bsp/i2c
2 bmp280_i2c bsp/i2c
3 pc_uart bsp/uart
4 key_gpio bsp/gpio_exti
5 servo bsp/servo_pwm

View File

@ -0,0 +1 @@
servo,测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息
1 servo 测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息测试消息

View 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;
}

View 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

View 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;
}

View 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

View 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 oled_i2c bsp/i2c
2 bmp280_i2c bsp/i2c
3 pc_uart bsp/uart
4 key_gpio bsp/gpio_exti
5 servo bsp/servo_pwm

View File

View File

View 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()); // 任务完成后结束自身
} }

View File

@ -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 %}
} }

View File

@ -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}}

View 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