Compare commits

...

90 Commits

Author SHA1 Message Date
04729c066a 修改music 2025-10-23 16:59:56 +08:00
52b6449c4f 修好了那个保留用户区域问题 2025-10-21 09:10:00 +08:00
d1c3b2747a 更新 2025-10-21 00:59:19 +08:00
49545dd232 修改.h覆盖问题 2025-10-18 23:55:43 +08:00
7127b207f8 修复device 2025-10-13 21:14:51 +08:00
a9f13e2607 修复路径 2025-10-13 21:13:13 +08:00
b96137d807 v1.0.6 2025-10-13 15:21:29 +08:00
e5d5afb1a8 2025-10-13 11:20:19 +08:00
697104d1ce 暂存2轴云台 2025-10-13 02:22:16 +08:00
d3aabce4f5 修复更新代码库的bug 2025-10-13 01:46:39 +08:00
563ede007c 改点东西 2025-10-07 13:40:18 +08:00
22a71d3d04 修lz和imu 2025-10-04 17:17:57 +08:00
96feb958a5 修rm 2025-10-03 22:58:09 +08:00
a42e2e3832 更新win 2025-10-03 22:10:51 +08:00
0e57cd6cd7 继续优化 2025-10-03 16:22:37 +08:00
4cb5e2fc9a 改更新 2025-10-03 16:19:54 +08:00
3fb6348116 改can 2025-10-03 16:15:11 +08:00
7275e5e6e1 修can 2025-10-03 15:40:36 +08:00
d2eea18ecc 增加dm电机 2025-10-01 11:46:16 +08:00
e3ed160f42 修改dr16 2025-09-30 06:18:07 +08:00
dc87fbc7da 修复dr16 2025-09-24 19:54:25 +08:00
09602c76cd 更新代码 2025-09-22 14:49:14 +08:00
d16427f7d4 修复led和rm的reserv 2025-09-22 10:24:05 +08:00
a29e097978 修复rm 2025-09-20 16:03:14 +08:00
92bb89124b 修改led 2025-09-20 01:28:28 +08:00
d7a56e656b 修改lk 2025-09-20 00:50:23 +08:00
ae40434ecf 添加生成cmake 2025-09-20 00:44:54 +08:00
5ba916c40a 修复灵足 2025-09-20 00:36:59 +08:00
d5d580f384 修改can 2025-09-20 00:32:32 +08:00
60154cafd5 修复生成报错 2025-09-12 23:38:20 +08:00
3062fbbef0 led修改 2025-09-07 21:42:14 +08:00
f449b15fe2 修复ops9 2025-09-07 19:20:33 +08:00
822080af2f 修复init模版错误 2025-09-07 15:06:21 +08:00
889f34ae12 修改can 2025-09-07 13:54:28 +08:00
bc898da6c2 修改编码 2025-09-07 12:37:33 +08:00
f1c6b085a4 大更新 2025-09-06 13:04:19 +08:00
d626e4e656 修改了can 2025-09-06 12:42:38 +08:00
485e25fec2 更新电机 2025-09-01 00:02:40 +08:00
22498e9a4f 修灵足 2025-08-31 15:41:05 +08:00
70d3add6da 修复灵足bug 2025-08-30 15:11:10 +08:00
d6eaed5f72 修复pwm 2025-08-30 10:53:30 +08:00
fcd97c1392 修改一些代码 2025-08-28 23:16:19 +08:00
79f7671a9f 修改了can,添加了4个device 2025-08-28 22:00:55 +08:00
d61dfa3634 准备测试 2025-08-28 21:38:12 +08:00
006ee185e9 修改can 2025-08-28 21:38:00 +08:00
89b6af6138 修复 2025-08-19 01:50:17 +08:00
d5871097a9 修复iic中断问题 2025-08-18 03:47:16 +08:00
1697a51555 修改版本号 2025-08-18 03:31:26 +08:00
474ea3ded3 添加依赖 2025-08-18 03:27:39 +08:00
41de38a146 添加odrive和vesc 2025-08-18 03:20:20 +08:00
7e92f32642 修改can生成逻辑 2025-08-18 03:19:30 +08:00
68394c616e 修复完路径问题了 2025-08-18 03:18:50 +08:00
266868ad71 合并前的最终提交 2025-08-18 02:08:55 +08:00
898c5dfb2b 修复了exe路径问题 2025-08-18 01:57:03 +08:00
c90d0b4d79 整理格式 2025-08-10 15:21:01 +08:00
e070e723aa 更新啦 2025-08-09 04:42:11 +08:00
62549172dd 能用了 2025-08-09 03:30:14 +08:00
f9f0d93b95 给device添加time 2025-08-08 01:41:09 +08:00
71c2e83a7a 修复了陀螺仪,也修复了gpio和spi 2025-08-08 00:50:49 +08:00
43749e0391 修改time和gpio的保存类型 2025-08-07 22:38:21 +08:00
c8ca5e1031 修改了基础的device生成 2025-08-07 19:14:32 +08:00
3159d3ae1a can支持自定义id解析了 2025-08-07 04:40:31 +08:00
3e49722616 修复bsp的bug 2025-08-06 05:11:40 +08:00
bbb521654c 更新can 2025-08-06 04:50:48 +08:00
50cfcb0693 疯狂写bsp 2025-08-06 04:26:13 +08:00
d99e9e1ec8 准备device 2025-08-05 15:33:31 +08:00
2c9309ae1e bsp和component写完了 2025-08-05 14:22:31 +08:00
bebfbd716c bsp写的差不多了 2025-08-05 03:47:11 +08:00
af7529b529 创建了新的页面 2025-08-04 20:52:19 +08:00
fe82822d58 准备重大更新代码生成 2025-08-04 19:19:32 +08:00
254328ddc8 全部改成域名 2025-08-04 19:19:11 +08:00
4973bb101a 准备超级优化代码生成 2025-08-01 03:04:17 +08:00
a7c46ca9fc 新版本 2025-08-01 02:49:15 +08:00
89fbaf8e55 修改模版代码 2025-07-31 04:48:01 +08:00
b4a4d87909 修好ui了 2025-07-31 03:52:22 +08:00
88ec1517fb 修改了 2025-07-31 02:47:48 +08:00
08193b8093 奇怪的bug 2025-07-31 02:22:58 +08:00
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
199 changed files with 18299 additions and 6061 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.gitignore vendored
View File

@ -31,4 +31,5 @@ Examples/
build/
dist/
*.spec
*.spec
*.exe

22
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"files.associations": {
"user_math.h": "c",
"bsp.h": "c",
"stdint.h": "c",
"array": "c",
"string": "c",
"string_view": "c",
"vector": "c",
"can.h": "c",
"device.h": "c",
"gpio.h": "c",
"uart.h": "c",
"motor_rm.h": "c",
"mm.h": "c",
"capacity.h": "c",
"error_detect.h": "c",
"bmi088.h": "c",
"time.h": "c",
"motor.h": "c"
}
}

2339
MR_Tool.py

File diff suppressed because it is too large Load Diff

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"

680
MRobot.py
View File

@ -1,673 +1,23 @@
import sys
import os
import serial
import serial.tools.list_ports
import sys
# 将当前工作目录设置为程序所在的目录,确保无论从哪里执行,其工作目录都正确设置为程序本身的位置,避免路径错误。
os.chdir(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False)else os.path.dirname(os.path.abspath(__file__)))
from PyQt5.QtCore import Qt, pyqtSignal, QThread
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox,
QComboBox, QPushButton, QTextEdit, QLineEdit, QLabel, QSizePolicy,
QFileDialog, QMessageBox, QStackedLayout
)
from qfluentwidgets import (
Theme, setTheme, FluentIcon, SwitchButton, BodyLabel, SubtitleLabel,
StrongBodyLabel, HorizontalSeparator, InfoBar, MessageDialog, Dialog,
AvatarWidget, NavigationItemPosition, FluentWindow, NavigationAvatarWidget,
PushButton, TextEdit, LineEdit, ComboBox, ImageLabel
)
from qfluentwidgets import FluentIcon as FIF
import requests
import shutil
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView, QHeaderView
from PyQt5.QtWidgets import QApplication
from app.main_window import MainWindow
from qfluentwidgets import (
TreeWidget, InfoBar, InfoBarPosition, MessageDialog, TreeItemDelegate
)
from qfluentwidgets import CheckBox
from qfluentwidgets import TreeWidget
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QFileDialog
from qfluentwidgets import ProgressBar
# ===================== 页面基类 =====================
class BaseInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
# 启用 DPI 缩放
QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) # 启用高 DPI 缩放
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) # 使用高 DPI 图标
# ===================== 首页界面 =====================
class HomeInterface(BaseInterface):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("homeInterface")
layout = QVBoxLayout()
layout.setContentsMargins(60, 60, 60, 60)
layout.setSpacing(32)
self.setLayout(layout)
# 顶部logo和欢迎区
top_layout = QHBoxLayout()
logo = ImageLabel('img/MRobot.png')
logo.setFixedSize(260, 80)
top_layout.addWidget(logo, alignment=Qt.AlignmentFlag.AlignTop)
title_layout = QVBoxLayout()
title_layout.addWidget(StrongBodyLabel("欢迎使用 MRobot Toolbox"))
title_layout.addWidget(SubtitleLabel("让你的机器人开发更高效、更智能"))
top_layout.addLayout(title_layout)
top_layout.addStretch()
layout.addLayout(top_layout)
layout.addWidget(HorizontalSeparator())
# 项目简介
layout.addWidget(BodyLabel(
"MRobot Toolbox 是一款集成化的机器人开发辅助工具,"
"支持代码生成、串口终端、主题切换等多种实用功能。\n"
"点击左侧导航栏可快速切换各功能页面。"
))
# 开发者与项目目标
layout.addWidget(HorizontalSeparator())
layout.addWidget(SubtitleLabel("开发者与项目目标"))
layout.addWidget(BodyLabel("开发团队QUT 青岛理工大学 MOVE 战队"))
layout.addWidget(BodyLabel("项目目标:为所有 rmer 和 rcer 提供现代化、简单、高效的机器人开发方式,"
"让机器人开发变得更轻松、更智能。"))
layout.addWidget(BodyLabel("适用于 RM、RC、各类嵌入式机器人项目。"))
# layout.addStretch()
# ===================== 代码生成页面 =====================
class DataInterface(BaseInterface):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("dataInterface")
self.stacked_layout = QStackedLayout()
self.setLayout(self.stacked_layout)
# --- 页面1工程路径选择 ---
self.select_widget = QWidget()
select_layout = QVBoxLayout(self.select_widget)
select_layout.addSpacing(40)
select_layout.addWidget(SubtitleLabel("MRobot 代码生成"))
select_layout.addWidget(HorizontalSeparator())
select_layout.addSpacing(10)
select_layout.addWidget(BodyLabel("请选择包含 .ioc 文件的工程文件夹,点击下方按钮进行选择。"))
select_layout.addSpacing(20)
self.choose_btn = PushButton("选择工程路径")
self.choose_btn.clicked.connect(self.choose_project_folder)
select_layout.addWidget(self.choose_btn)
select_layout.addStretch()
self.stacked_layout.addWidget(self.select_widget)
# --- 页面2代码配置 ---
self.config_widget = QWidget()
self.config_layout = QVBoxLayout(self.config_widget)
# 左上角小返回按钮
top_bar = QHBoxLayout()
self.back_btn = PushButton('返回', icon=FluentIcon.SKIP_BACK)
# self.back_btn.setFixedSize(32, 32)
self.back_btn.clicked.connect(self.back_to_select)
self.back_btn.setToolTip("返回")
top_bar.addWidget(self.back_btn, alignment=Qt.AlignmentFlag.AlignLeft)
top_bar.addStretch()
self.config_layout.addLayout(top_bar)
self.config_layout.addWidget(SubtitleLabel("工程配置信息"))
self.config_layout.addWidget(HorizontalSeparator())
self.project_info_labels = []
self.config_layout.addStretch()
self.stacked_layout.addWidget(self.config_widget)
# 默认显示选择页面
self.stacked_layout.setCurrentWidget(self.select_widget)
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:
QMessageBox.warning(self, "提示", "未找到.ioc文件请确认项目文件夹。")
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):
# 清理旧内容
for label in self.project_info_labels:
self.config_layout.removeWidget(label)
label.deleteLater()
self.project_info_labels.clear()
# 显示项目信息
l1 = BodyLabel(f"项目名称: {self.project_name}")
l2 = BodyLabel(f"项目路径: {self.project_path}")
l3 = BodyLabel(f"IOC 文件: {self.ioc_file}")
self.config_layout.insertWidget(2, l1)
self.config_layout.insertWidget(3, l2)
self.config_layout.insertWidget(4, l3)
self.project_info_labels.extend([l1, l2, l3])
self.stacked_layout.setCurrentWidget(self.config_widget)
def back_to_select(self):
self.stacked_layout.setCurrentWidget(self.select_widget)
# ===================== 串口终端界面 =====================
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(BaseInterface):
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 = [
("线程监视器", "RESET"),
("陀螺仪校准", "GET_VERSION"),
("性能监视", "START"),
("重启", "STOP"),
("显示所有设备", "SELF_TEST"),
("查询id", "STATUS"),
]
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()
# ===================== 零件库页面 =====================
# ...existing code...
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:
url = f"{self.server_url}/download/{rel_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:
print(f"下载失败({resp.status_code}): {rel_path},第{retry+1}次尝试")
retry += 1
except Exception as e:
print(f"下载异常: {rel_path},第{retry+1}次尝试,错误: {e}")
retry += 1
else:
fail.append(rel_path)
self.progressChanged.emit(int((idx + 1) / total * 100))
self.finished.emit(success, fail)
class PartLibraryInterface(BaseInterface):
SERVER_URL = "http://154.37.215.220:5000"
SECRET_KEY = "MRobot_Download"
LOCAL_LIB_DIR = "mech_lib"
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("partLibraryInterface")
layout = QVBoxLayout(self)
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, QHeaderView.Stretch)
self.tree.header().setSectionResizeMode(1, QHeaderView.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:
InfoBar.info(
title="提示",
content="请先勾选要下载的文件。",
parent=self,
position=InfoBarPosition.TOP,
duration=2000
)
return
# 进度条对话框
self.progress_dialog = Dialog(
title="正在下载",
content="正在下载选中文件,请稍候...",
parent=self
)
self.progress_bar = ProgressBar()
self.progress_bar.setValue(0)
# 插入进度条到内容布局
self.progress_dialog.textLayout.addWidget(self.progress_bar)
self.progress_dialog.show()
# 启动下载线程
self.download_thread = DownloadThread(
files, self.SERVER_URL, self.SECRET_KEY, self.LOCAL_LIB_DIR
)
self.download_thread.progressChanged.connect(self.progress_bar.setValue)
self.download_thread.finished.connect(self.on_download_finished)
self.download_thread.finished.connect(self.download_thread.deleteLater)
self.download_thread.start()
def on_download_finished(self, success, fail):
self.progress_dialog.close()
msg = f"成功下载: {len(success)} 个文件\n失败: {len(fail)} 个文件"
dialog = Dialog(
title="下载结果",
content=msg,
parent=self
)
# 添加“打开文件夹”按钮
open_btn = PushButton("打开文件夹")
def open_folder():
folder = os.path.abspath(self.LOCAL_LIB_DIR)
# 打开文件夹macOS用openWindows用explorerLinux用xdg-open
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])
dialog.close()
open_btn.clicked.connect(open_folder)
# 添加按钮到Dialog布局
dialog.textLayout.addWidget(open_btn)
dialog.exec()
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])
# ===================== 设置界面 =====================
class SettingInterface(BaseInterface):
themeSwitchRequested = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("settingInterface")
layout = QVBoxLayout()
self.setLayout(layout)
# 标题
layout.addSpacing(10)
layout.addWidget(SubtitleLabel("设置中心"))
layout.addSpacing(10)
layout.addWidget(HorizontalSeparator())
# 主题切换区域
theme_title = StrongBodyLabel("外观设置")
theme_desc = BodyLabel("切换夜间/白天模式,适应不同环境。")
theme_desc.setWordWrap(True)
layout.addSpacing(10)
layout.addWidget(theme_title)
layout.addWidget(theme_desc)
theme_box = QHBoxLayout()
self.theme_label = BodyLabel("夜间模式")
self.theme_switch = SwitchButton()
self.theme_switch.setChecked(Theme.DARK == Theme.DARK)
self.theme_switch.checkedChanged.connect(self.on_theme_switch)
theme_box.addWidget(self.theme_label)
theme_box.addWidget(self.theme_switch)
theme_box.addStretch()
layout.addLayout(theme_box)
layout.addSpacing(15)
layout.addWidget(HorizontalSeparator())
# 其它设置区域(示例)
other_title = StrongBodyLabel("其它设置")
other_desc = BodyLabel("更多功能正在开发中,敬请期待。")
other_desc.setWordWrap(True)
layout.addSpacing(10)
layout.addWidget(other_title)
layout.addWidget(other_desc)
# 版权信息
layout.addStretch()
copyright_label = BodyLabel("© 2025 MRobot Toolbox")
copyright_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(copyright_label)
layout.addSpacing(10)
def on_theme_switch(self, checked):
self.themeSwitchRequested.emit()
# ===================== 帮助与关于界面 =====================
class HelpInterface(BaseInterface):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("helpInterface")
layout = QVBoxLayout()
self.setLayout(layout)
class AboutInterface(BaseInterface):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("aboutInterface")
layout = QVBoxLayout()
self.setLayout(layout)
# ===================== 主窗口与导航 =====================
class MainWindow(FluentWindow):
themeChanged = pyqtSignal(Theme)
def __init__(self):
super().__init__()
self.setWindowTitle("MR_ToolBox")
self.resize(1000, 700)
self.setMinimumSize(800, 600)
# 记录当前主题
self.current_theme = Theme.DARK
# 创建页面实例
self.setting_page = SettingInterface(self)
self.setting_page.themeSwitchRequested.connect(self.toggle_theme)
self.page_registry = [
(HomeInterface(self), FIF.HOME, "首页", NavigationItemPosition.TOP),
(DataInterface(self), FIF.LIBRARY, "MRobot代码生成", NavigationItemPosition.SCROLL),
(SerialTerminalInterface(self), FIF.COMMAND_PROMPT, "Mini_Shell", NavigationItemPosition.SCROLL),
(PartLibraryInterface(self), FIF.DOWNLOAD, "零件库", NavigationItemPosition.SCROLL), # ← 加上这一行
(self.setting_page, FIF.SETTING, "设置", NavigationItemPosition.BOTTOM),
(HelpInterface(self), FIF.HELP, "帮助", NavigationItemPosition.BOTTOM),
(AboutInterface(self), FIF.INFO, "关于", NavigationItemPosition.BOTTOM),
]
self.initNavigation()
def initNavigation(self):
for page, icon, name, position in self.page_registry:
self.addSubInterface(page, icon, name, position)
self.navigationInterface.addSeparator()
avatar = NavigationAvatarWidget('用户', ':/qfluentwidgets/images/avatar.png')
self.navigationInterface.addWidget(
routeKey='avatar',
widget=avatar,
onClick=self.show_user_info, # 这里改为 self.show_user_info
position=NavigationItemPosition.BOTTOM
)
def toggle_theme(self):
# 切换主题
if self.current_theme == Theme.DARK:
self.current_theme = Theme.LIGHT
else:
self.current_theme = Theme.DARK
setTheme(self.current_theme)
# 同步设置界面按钮状态
self.setting_page.theme_switch.setChecked(self.current_theme == Theme.DARK)
def show_user_info(self):
dialog = Dialog(
title="用户信息",
content="用户MRobot至尊VIP用户",
parent=self
)
dialog.exec()
# ===================== 程序入口 =====================
def main():
if __name__ == "__main__":
app = QApplication(sys.argv)
setTheme(Theme.DARK)
window = MainWindow()
window.show()
sys.exit(app.exec_())
app.setAttribute(Qt.AA_DontCreateNativeWidgetSiblings) # 避免创建原生窗口小部件的兄弟窗口
if __name__ == '__main__':
main()
w = MainWindow()
sys.exit(app.exec_()) # 启动应用程序并进入主事件循环
# 注意:在 PyQt5 中exec_() 是一个阻塞调用,直到应用程序退出。

View File

@ -1,719 +0,0 @@
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import sys
import os
import threading
import shutil
import re
from git import Repo
from collections import defaultdict
import csv
import xml.etree.ElementTree as ET
# 配置常量
REPO_DIR = "MRobot_repo"
REPO_URL = "http://gitea.qutrobot.top/robofish/MRobot.git"
if getattr(sys, 'frozen', False): # 检查是否为打包后的环境
CURRENT_DIR = os.path.dirname(sys.executable) # 使用可执行文件所在目录
else:
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 = [] # 用于存储任务的变量
# 初始化
def initialize(self):
print("初始化中,正在克隆仓库...")
self.clone_repo()
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 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}")
# 显示主窗口
# ...existing code...
# ...existing code...
# 显示主窗口
def show_main_window(self):
root = tk.Tk()
root.title("MRobot 自动生成脚本")
root.geometry("1000x650") # 调整窗口大小以适应布局
# 在窗口关闭时调用 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)
# 创建 Canvas 和 Scrollbar
canvas = tk.Canvas(module_task_frame)
scrollbar = ttk.Scrollbar(module_task_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
# 配置滚动区域
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# 绑定鼠标滚轮事件
def on_mouse_wheel(event):
canvas.yview_scroll(-1 * int(event.delta / 120), "units")
canvas.bind_all("<MouseWheel>", on_mouse_wheel)
# 布局 Canvas 和 Scrollbar
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 左右布局:模块文件选择框和任务管理框
left_frame = ttk.Frame(scrollable_frame)
left_frame.pack(side="left", fill="both", expand=True, padx=5, pady=5)
right_frame = ttk.Frame(scrollable_frame)
right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)
# 模块文件选择框
header_files_frame = ttk.LabelFrame(left_frame, text="模块文件选择", padding=(10, 10))
header_files_frame.pack(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(right_frame, text="任务管理", padding=(10, 10))
task_frame.pack(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()
# ...existing code...
# ...existing code...
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__":
app = MRobotApp()
app.initialize()

View File

@ -1,15 +1,14 @@
# MRobot
更加高效快捷的 STM32 开发架构,诞生于 Robocon 和 Robomaster但绝不仅限于此。
更加高效快捷的机器人开发工具,诞生于 Robocon 和 Robomaster但绝不仅限于此。
<div align="center">
<img src="./img/MRobot.png" height="100" alt="MRobot Logo">
<p>是时候使用更简洁的方式开发单片机了</p>
<img src="assets\logo\MRobot.png" height="80" alt="MRobot Logo">
<p>
<!-- <img src="https://img.shields.io/github/license/xrobot-org/XRobot.svg" alt="License">
<img src="https://img.shields.io/github/repo-size/xrobot-org/XRobot.svg" alt="Repo Size">
<img src="https://img.shields.io/github/last-commit/xrobot-org/XRobot.svg" alt="Last Commit">
<img src="https://img.shields.io/badge/language-c/c++-F34B7D.svg" alt="Language"> -->
<img src="https://img.shields.io/github/license/goldenfishs/MRobot.svg" alt="License">
<img src="https://img.shields.io/github/repo-size/goldenfishs/MRobot.svg" alt="Repo Size">
<img src="https://img.shields.io/github/last-commit/goldenfishs/MRobot.svg" alt="Last Commit">
<img src="https://img.shields.io/badge/language-c/python-F34B7D.svg" alt="Language">
</p>
</div>
@ -17,7 +16,7 @@
## 引言
提起嵌入式开发,绝大多数人对每次繁琐的配置,以及查阅各种文档来写东西感到非常枯燥和浪费使时间,对于小形形目创建优雅的架构又比较费事,那么我们哟u没有办法快速完成基础环境的搭建后直接开始写逻辑代码呢?
提起嵌入式开发,绝大多数人对每次繁琐的配置,以及查阅各种文档来写东西感到非常枯燥和浪费使时间,对于小形形目创建优雅的架构又比较费事,那么我们没有办法快速完成基础环境的搭建后直接开始写逻辑代码呢?
这就是**MRobot**。
@ -87,7 +86,5 @@
使用以下命令构建可执行文件:
```bash
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 MRobot.py --onefile --windowed --add-data "assets;assets" --add-data "app;app" --add-data "app/tools;app/tools"
```

View File

@ -1,32 +0,0 @@
/* Includes ----------------------------------------------------------------- */
#include <gpio.h>
#include "bsp/bsp.h"
#include "bsp/buzzer_gpio.h"
/* Private define ----------------------------------------------------------- */
#define BSP_BUZZER_GPIO GPIOA
#define BSP_BUZZER_PIN GPIO_PIN_1
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* Private variables -------------------------------------------------------- */
/* Private function --------------------------------------------------------- */
/* Exported functions ------------------------------------------------------- */
int8_t BSP_Buzzer_Set(BSP_Buzzer_Status_t s)
{
switch (s)
{
case BSP_BUZZER_ON:
HAL_GPIO_WritePin(BSP_BUZZER_GPIO, BSP_BUZZER_PIN, GPIO_PIN_SET); // 打开蜂鸣器
break;
case BSP_BUZZER_OFF:
HAL_GPIO_WritePin(BSP_BUZZER_GPIO, BSP_BUZZER_PIN, GPIO_PIN_RESET); // 关闭蜂鸣器
break;
case BSP_BUZZER_TAGGLE:
HAL_GPIO_TogglePin(BSP_BUZZER_GPIO, BSP_BUZZER_PIN); // 切换蜂鸣器状态
break;
default:
return BSP_ERR;
}
return BSP_OK;
}

View File

@ -1,141 +0,0 @@
/* 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

@ -1,46 +0,0 @@
#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

@ -1,118 +0,0 @@
#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

@ -1,2 +0,0 @@
i2c,I2C
uart,USART,UART
1 i2c,I2C
2 uart,USART,UART

View File

@ -1,72 +0,0 @@
/* 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

@ -1,50 +0,0 @@
/* Includes ----------------------------------------------------------------- */
#include "bsp/led_gpio.h"
#include "bsp/bsp.h"
#include <gpio.h>
/* Private define ----------------------------------------------------------- */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* Private variables -------------------------------------------------------- */
static uint32_t led_stats; // 使用位掩码记录每个通道的状态最多支持32LED
// 定义 LED 引脚和端口映射表:需要根据自己的修改,添加,或删减。
static const BSP_LED_Config_t LED_CONFIGS[] = {
{GPIOA, GPIO_PIN_2}, // BSP_LED_1
{GPIOA, GPIO_PIN_3}, // BSP_LED_2
{GPIOA, GPIO_PIN_4}, // BSP_LED_3
};
#define LED_CHANNEL_COUNT (sizeof(LED_CONFIGS) / sizeof(LED_CONFIGS[0])) // 通道数量
/* Private function --------------------------------------------------------- */
/* Exported functions ------------------------------------------------------- */
int8_t BSP_LED_Set(BSP_LED_Channel_t ch, BSP_LED_Status_t s)
{
if (ch < LED_CHANNEL_COUNT)
{
GPIO_TypeDef *port = LED_CONFIGS[ch].port;
uint16_t pin = LED_CONFIGS[ch].pin;
switch (s)
{
case BSP_LED_ON:
led_stats |= (1 << ch);
HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET); // 点亮LED
break;
case BSP_LED_OFF:
led_stats &= ~(1 << ch);
HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET); // 熄灭LED
break;
case BSP_LED_TAGGLE:
led_stats ^= (1 << ch);
HAL_GPIO_TogglePin(port, pin); // 切换LED状态
break;
default:
return BSP_ERR;
}
return BSP_OK;
}
return BSP_ERR;
}

View File

@ -1,36 +0,0 @@
#pragma once
/* Includes ----------------------------------------------------------------- */
#include <stdint.h>
#include "gpio.h"
/* Exported constants ------------------------------------------------------- */
/* Exported macro ----------------------------------------------------------- */
/* Exported types ----------------------------------------------------------- */
/* LED灯状态设置用 */
typedef enum
{
BSP_LED_ON,
BSP_LED_OFF,
BSP_LED_TAGGLE,
} BSP_LED_Status_t;
/* LED通道 */
typedef enum
{
BSP_LED_1,
BSP_LED_2,
BSP_LED_3,
/*BSP_LED_XXX*/
} BSP_LED_Channel_t;
/* LED GPIO 配置 */
typedef struct
{
GPIO_TypeDef *port; // GPIO 端口 (如 GPIOA, GPIOB)
uint16_t pin; // GPIO 引脚
} BSP_LED_Config_t;
/* Exported functions prototypes -------------------------------------------- */
int8_t BSP_LED_Set(BSP_LED_Channel_t ch, BSP_LED_Status_t s);

View File

@ -1,48 +0,0 @@
/* 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

@ -1,45 +0,0 @@
#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

@ -1,3 +0,0 @@
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

@ -1,301 +0,0 @@
#include "bmp280_i2c.h"
#include "bsp/i2c.h"
#define I2C_Handle BSP_I2C_GetHandle(BSP_I2C_BMP280)
/**
* @brief
* @param regAdd
* @param pdata
* @param size
* @retval
*/
void bmp280_readReg(uint8_t regAdd, uint8_t *pdata, uint8_t size) {
HAL_I2C_Mem_Read(I2C_Handle, BMP280_I2C_ADDR << 1, regAdd, I2C_MEMADD_SIZE_8BIT, pdata, size, 1000);
}
/**
* @brief 1
* @param regAdd
* @param pdata
* @retval 0
* 1
*/
uint8_t bmp280_writeReg(uint8_t regAdd, uint8_t *pdata) {
if (HAL_I2C_Mem_Write(I2C_Handle, BMP280_I2C_ADDR << 1, regAdd, I2C_MEMADD_SIZE_8BIT, pdata, 1, 1000) == HAL_OK) {
return 0;
}
return 1;
}
/**
* @brief id
* @param
* @retval id
*/
uint8_t bmp280_get_id(void) {
uint8_t temp = 0;
bmp280_readReg(BMP280_ID, &temp, 1);
return temp;
}
/**
* @brief
* @param
* @retval 0
* 1
*/
uint8_t bmp280_reset(void) {
uint8_t temp = 0xB6;
return bmp280_writeReg(BMP280_RESET, &temp);
}
/**
* @brief
* @param
* @retval 0
* 1
*/
uint8_t bmp280_getStatus(void) {
uint8_t temp = 0;
bmp280_readReg(BMP280_STATUS, &temp, 1);
return (temp & 0x09) ? 1 : 0;
}
/**
* @brief
* @param mode 0
* 1
* 2
* @retval 0
* 1
*/
uint8_t bmp280_setMode(uint8_t mode)
{
uint8_t temp=0;
bmp280_readReg(BMP280_CTRL_MEAS,&temp,1);
switch(mode)
{
case 0:
temp&=0xFC;
break;
case 1:
temp&=0xFC;
temp|=0x01;
break;
case 2:
temp&=0xFC;
temp|=0x03;
break;
default:
return 1;
}
return bmp280_writeReg(BMP280_CTRL_MEAS,&temp);
}
/**
* @brief
* @param mode temp&press 0
* 1 ×1
* 2 ×2
* 3 ×4
* .. .....
* 5 ×16
* @retval 0
* 1
*/
uint8_t bmp280_setOversampling(uint8_t osrs_p,uint8_t osrs_t)
{
uint8_t temp=0;
bmp280_readReg(BMP280_CTRL_MEAS,&temp,1);
temp&=0xE3;
osrs_p = osrs_p<<2;
osrs_p&= 0x1C;
temp|=osrs_p;
temp&=0x1F;
osrs_t = osrs_t<<5;
osrs_t&= 0xE0;
temp|=osrs_t;
return bmp280_writeReg(BMP280_CTRL_MEAS,&temp);
}
/**
* @brief
* @param Standbyt 0 0.5ms filter 0
* 1 62.5ms 1 2
* 2 125ms 2 4
* 3 250ms 3 8
* 4 500ms 4 16
* 5 1000ms
* 6 2000ms
* 7 4000ms
* @retval 0
* 1
*/
uint8_t bmp280_setConfig(uint8_t Standbyt,uint8_t filter)
{
uint8_t temp=0;
temp = Standbyt<<5;
filter&=0x07;
filter=filter<<2;
temp|=filter;
return bmp280_writeReg(BMP280_CONFIG,&temp);
}
/**
* @brief
* @param calib
* @retval
*/
void bmp280_getCalibration(bmp280_calib *calib)
{
uint8_t buf[20];
bmp280_readReg(BMP280_DIGT,buf,6);
calib->dig_t1 =(uint16_t)(bmp280_msblsb_to_u16(buf[1], buf[0]));
calib->dig_t2 =(int16_t)(bmp280_msblsb_to_u16(buf[3], buf[2]));
calib->dig_t3 =(int16_t)(bmp280_msblsb_to_u16(buf[5], buf[4]));
bmp280_readReg(BMP280_DIGP,buf,18);
calib->dig_p1 = (uint16_t)(bmp280_msblsb_to_u16(buf[1], buf[0]));
calib->dig_p2 =(int16_t)(bmp280_msblsb_to_u16(buf[3], buf[2]));
calib->dig_p3 =(int16_t)(bmp280_msblsb_to_u16(buf[5], buf[4]));
calib->dig_p4 =(int16_t)(bmp280_msblsb_to_u16(buf[7], buf[6]));
calib->dig_p5 =(int16_t)(bmp280_msblsb_to_u16(buf[9], buf[8]));
calib->dig_p6 =(int16_t)(bmp280_msblsb_to_u16(buf[11], buf[10]));
calib->dig_p7 =(int16_t)(bmp280_msblsb_to_u16(buf[13], buf[12]));
calib->dig_p8 =(int16_t)(bmp280_msblsb_to_u16(buf[15], buf[14]));
calib->dig_p9 =(int16_t)(bmp280_msblsb_to_u16(buf[17], buf[16]));
}
/**
* @brief
* @param calib
* *temperature
* *t_fine
* @retval
*/
void bmp280_getTemperature(bmp280_calib *calib,double *temperature,int32_t *t_fine)
{
uint8_t buf[3];
uint32_t data_xlsb;
uint32_t data_lsb;
uint32_t data_msb;
int32_t uncomp_temperature;
double var1, var2;
bmp280_readReg(BMP280_TEMP,buf,3);
data_msb = (int32_t)buf[0] << 12;
data_lsb = (int32_t)buf[1] << 4;
data_xlsb = (int32_t)buf[2] >> 4;
uncomp_temperature = (int32_t)(data_msb | data_lsb | data_xlsb);
var1 = (((double) uncomp_temperature) / 16384.0 - ((double) calib->dig_t1) / 1024.0) *
((double) calib->dig_t2);
var2 =
((((double) uncomp_temperature) / 131072.0 - ((double) calib->dig_t1) / 8192.0) *
(((double) uncomp_temperature) / 131072.0 - ((double) calib->dig_t1) / 8192.0)) *
((double) calib->dig_t3);
*t_fine = (int32_t) (var1 + var2);
*temperature = (var1 + var2) / 5120.0;
}
/**
* @brief
* @param calib
* *pressure
* *t_fine
* @retval
*/
void bmp280_getPressure(bmp280_calib *calib,double *pressure,int32_t *t_fine)
{
uint8_t buf[3];
uint32_t data_xlsb;
uint32_t data_lsb;
uint32_t data_msb;
int32_t uncomp_pressure;
double var1, var2;
bmp280_readReg(BMP280_PRES,buf,3);
data_msb = (uint32_t)buf[0] << 12;
data_lsb = (uint32_t)buf[1] << 4;
data_xlsb = (uint32_t)buf[2] >> 4;
uncomp_pressure = (data_msb | data_lsb | data_xlsb);
var1 = ((double) *t_fine / 2.0) - 64000.0;
var2 = var1 * var1 * ((double) calib->dig_p6) / 32768.0;
var2 = var2 + var1 * ((double) calib->dig_p5) * 2.0;
var2 = (var2 / 4.0) + (((double) calib->dig_p4) * 65536.0);
var1 = (((double)calib->dig_p3) * var1 * var1 / 524288.0 + ((double)calib->dig_p2) * var1) /
524288.0;
var1 = (1.0 + var1 / 32768.0) * ((double) calib->dig_p1);
*pressure = 1048576.0 - (double)uncomp_pressure;
*pressure = (*pressure - (var2 / 4096.0)) * 6250.0 / var1;
var1 = ((double)calib->dig_p9) * *pressure * *pressure / 2147483648.0;
var2 = *pressure * ((double)calib->dig_p8) / 32768.0;
*pressure = *pressure + (var1 + var2 + ((double)calib->dig_p7)) / 16.0;
}
/**
* @brief
* @param *calib
* @retval 0
* 1
*/
uint8_t bmp280_init(bmp280_calib *calib)
{
uint8_t rslt;
rslt = bmp280_get_id();
if(rslt == BMP2_CHIP_ID)
{
bmp280_getCalibration(calib);
rslt = bmp280_setOversampling(5,2);
if(rslt)
{
return 1;
}
rslt = bmp280_setConfig(0,4);
if(rslt)
{
return 1;
}
rslt = bmp280_setMode(2);
if(rslt)
{
return 1;
}
}
else
{
return 1;
}
return 0;
}
/**
* @brief
* @param *calib
* @retval
*/
void bmp280_getdata(bmp280_calib *calib,float *temperature,float *pressure)
{
double temp_T,temp_P;
int32_t t_fine;
bmp280_getTemperature(calib,&temp_T,&t_fine);
bmp280_getPressure(calib,&temp_P,&t_fine);
*temperature = (float)temp_T;
*pressure = (float)temp_P;
}

View File

@ -1,49 +0,0 @@
#pragma once
/*底层接口定义*/
#include "bsp/i2c.h"
#include "stdint.h"
#define BMP280_I2C_ADDR 0x76 // BMP280 默认 I2C 地址
/*寄存器地址*/
#define BMP280_ID 0xD0 // 设备ID地址
#define BMP280_RESET 0xE0 // 设备重启
#define BMP280_STATUS 0xF3 // 设备状态
#define BMP280_CTRL_MEAS 0xF4 // 数据采集和模式设置
#define BMP280_CONFIG 0xF5 // 采样速率,滤波器和接口设置
#define BMP280_DIGT 0x88 // 温度校准系数起始位置
#define BMP280_DIGP 0x8E // 气压校准系数起始位置
#define BMP280_TEMP 0xFA // 温度储存起始位置
#define BMP280_PRES 0xF7 // 气压储存起始位置
#define BMP2_CHIP_ID 0x58 // 设备ID地址
#define bmp280_msblsb_to_u16(msb, lsb) (((uint16_t)msb << 8) | ((uint16_t)lsb))
typedef struct {
unsigned short dig_t1;
signed short dig_t2;
signed short dig_t3;
unsigned short dig_p1;
signed short dig_p2;
signed short dig_p3;
signed short dig_p4;
signed short dig_p5;
signed short dig_p6;
signed short dig_p7;
signed short dig_p8;
signed short dig_p9;
} bmp280_calib;
uint8_t bmp280_get_id(void);
uint8_t bmp280_reset(void);
uint8_t bmp280_getStatus(void);
uint8_t bmp280_setMode(uint8_t mode);
uint8_t bmp280_setOversampling(uint8_t osrs_p, uint8_t osrs_t);
uint8_t bmp280_setConfig(uint8_t Standbyt, uint8_t filter);
void bmp280_getCalibration(bmp280_calib *calib);
void bmp280_getTemperature(bmp280_calib *calib, double *temperature, int32_t *t_fine);
void bmp280_getPressure(bmp280_calib *calib, double *pressure, int32_t *t_fine);
uint8_t bmp280_init(bmp280_calib *calib);
void bmp280_getdata(bmp280_calib *calib, float *temperature, float *pressure);

View File

@ -1,5 +0,0 @@
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

@ -1,15 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#define DEVICE_OK (0)
#define DEVICE_ERR (-1)
#define DEVICE_ERR_NULL (-2)
#define DEVICE_ERR_INITED (-3)
#define DEVICE_ERR_NO_DEV (-4)
#ifdef __cplusplus
}
#endif

View File

@ -1,65 +0,0 @@
/* 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

@ -1,21 +0,0 @@
#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

@ -1,267 +0,0 @@
/* Includes ----------------------------------------------------------------- */
#include "device/oled_i2c.h"
#include "bsp/i2c.h"
#include <string.h>
#include <stdint.h>
/* Private define ----------------------------------------------------------- */
#define OLED_I2C_ADDR 0x78 // OLED I2C 地址
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
/* Private variables -------------------------------------------------------- */
static uint8_t oled_buffer[OLED_WIDTH * OLED_HEIGHT / 8];
static struct {
uint8_t x_min;
uint8_t x_max;
uint8_t y_min;
uint8_t y_max;
uint8_t dirty; // 标志是否有脏区域
} dirty_rect = {0, 0, 0, 0, 0};
/* Private function prototypes ---------------------------------------------- */
static void OLED_WriteCommand(uint8_t cmd) {
uint8_t data[2] = {0x00, cmd};
HAL_I2C_Master_Transmit(BSP_I2C_GetHandle(BSP_I2C_OLED), OLED_I2C_ADDR, data, 2, HAL_MAX_DELAY);
}
static void OLED_WriteData(uint8_t *data, uint16_t size) {
uint8_t buffer[size + 1];
buffer[0] = 0x40;
memcpy(&buffer[1], data, size);
HAL_I2C_Master_Transmit(BSP_I2C_GetHandle(BSP_I2C_OLED), OLED_I2C_ADDR, buffer, size + 1, HAL_MAX_DELAY);
}
static void OLED_MarkDirty(uint8_t x, uint8_t y);
static void OLED_UpdateDirtyScreen(void);
static const uint8_t oled_font[95][8] = {
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,}, /* " ", 0 */
{0x00,0x00,0x00,0xcf,0xcf,0x00,0x00,0x00,}, /* "!", 1 */
{0x00,0x0c,0x06,0x00,0x0c,0x06,0x00,0x00,}, /* """, 2 */
{0x24,0xe4,0x3c,0x27,0xe4,0x3c,0x27,0x24,}, /* "#", 3 */
{0x00,0x20,0x46,0xf9,0x9f,0x62,0x04,0x00,}, /* "$", 4 */
{0x06,0x09,0xc6,0x30,0x0c,0x63,0x90,0x60,}, /* "%", 5 */
{0x00,0x00,0x6e,0x91,0xa9,0x46,0xa0,0x00,}, /* "&", 6 */
{0x00,0x00,0x00,0x1c,0x0e,0x00,0x00,0x00,}, /* "'", 7 */
{0x00,0x00,0x3c,0x42,0x81,0x00,0x00,0x00,}, /* "(", 8 */
{0x00,0x00,0x00,0x81,0x42,0x3c,0x00,0x00,}, /* ")", 9 */
{0x00,0x10,0x54,0x38,0x38,0x54,0x10,0x00,}, /* "*", 10 */
{0x00,0x10,0x10,0xfc,0x10,0x10,0x00,0x00,}, /* "+", 11 */
{0x00,0x00,0x00,0xc0,0x60,0x00,0x00,0x00,}, /* ",", 12 */
{0x00,0x00,0x10,0x10,0x10,0x10,0x00,0x00,}, /* "-", 13 */
{0x00,0x00,0x00,0x00,0xc0,0xc0,0x00,0x00,}, /* ".", 14 */
{0x00,0x00,0x00,0xc0,0x38,0x07,0x00,0x00,}, /* "/", 15 */
{0x00,0x00,0x7c,0x92,0x8a,0x7c,0x00,0x00,}, /* "0", 16 */
{0x00,0x00,0x00,0x84,0xfe,0x80,0x00,0x00,}, /* "1", 17 */
{0x00,0x00,0x8c,0xc2,0xa2,0x9c,0x00,0x00,}, /* "2", 18 */
{0x00,0x00,0x44,0x92,0x92,0x6c,0x00,0x00,}, /* "3", 19 */
{0x00,0x20,0x38,0x24,0xfe,0x20,0x00,0x00,}, /* "4", 20 */
{0x00,0x00,0x5e,0x92,0x92,0x62,0x00,0x00,}, /* "5", 21 */
{0x00,0x00,0x78,0x94,0x92,0x62,0x00,0x00,}, /* "6", 22 */
{0x00,0x00,0x82,0x62,0x1a,0x06,0x00,0x00,}, /* "7", 23 */
{0x00,0x00,0x6c,0x92,0x92,0x6c,0x00,0x00,}, /* "8", 24 */
{0x00,0x00,0x8c,0x52,0x32,0x1c,0x00,0x00,}, /* "9", 25 */
{0x00,0x00,0x00,0x6c,0x6c,0x00,0x00,0x00,}, /* ":", 26 */
{0x00,0x00,0x80,0xec,0x6c,0x00,0x00,0x00,}, /* ";", 27 */
{0x00,0x00,0x10,0x28,0x44,0x00,0x00,0x00,}, /* "<", 28 */
{0x00,0x00,0x24,0x24,0x24,0x24,0x00,0x00,}, /* "=", 29 */
{0x00,0x00,0x00,0x44,0x28,0x10,0x00,0x00,}, /* ">", 30 */
{0x00,0x00,0x0c,0xa2,0x92,0x1c,0x00,0x00,}, /* "?", 31 */
{0x00,0x3c,0x42,0x99,0xa5,0xa2,0x3c,0x00,}, /* "@", 32 */
{0x00,0xe0,0x1c,0x12,0x12,0x1c,0xe0,0x00,}, /* "A", 33 */
{0x00,0xfe,0x92,0x92,0x9c,0x90,0x60,0x00,}, /* "B", 34 */
{0x00,0x38,0x44,0x82,0x82,0x82,0x44,0x00,}, /* "C", 35 */
{0x00,0xfe,0x82,0x82,0x82,0x82,0x7c,0x00,}, /* "D", 36 */
{0x00,0xfe,0x92,0x92,0x92,0x92,0x92,0x00,}, /* "E", 37 */
{0x00,0xfe,0x12,0x12,0x12,0x12,0x02,0x00,}, /* "F", 38 */
{0x00,0x7c,0x82,0x92,0x92,0x72,0x00,0x00,}, /* "G", 39 */
{0x00,0xfe,0x10,0x10,0x10,0x10,0xfe,0x00,}, /* "H", 40 */
{0x00,0x82,0x82,0xfe,0x82,0x82,0x00,0x00,}, /* "I", 41 */
{0x00,0x82,0x82,0x7e,0x02,0x02,0x00,0x00,}, /* "J", 42 */
{0x00,0xfe,0x10,0x28,0x44,0x82,0x00,0x00,}, /* "K", 43 */
{0x00,0xfe,0x80,0x80,0x80,0x80,0x00,0x00,}, /* "L", 44 */
{0xfc,0x02,0x04,0xf8,0x04,0x02,0xfc,0x00,}, /* "M", 45 */
{0x00,0xfe,0x04,0x18,0x30,0x40,0xfe,0x00,}, /* "N", 46 */
{0x00,0x7c,0x82,0x82,0x82,0x82,0x7c,0x00,}, /* "O", 47 */
{0x00,0x00,0xfe,0x12,0x12,0x0c,0x00,0x00,}, /* "P", 48 */
{0x00,0x00,0x3c,0x42,0xc2,0xbc,0x00,0x00,}, /* "Q", 49 */
{0x00,0x00,0xfe,0x32,0x52,0x8c,0x00,0x00,}, /* "R", 50 */
{0x00,0x00,0x4c,0x92,0x92,0x64,0x00,0x00,}, /* "S", 51 */
{0x00,0x02,0x02,0xfe,0x02,0x02,0x00,0x00,}, /* "T", 52 */
{0x00,0x7e,0x80,0x80,0x80,0x80,0x7e,0x00,}, /* "U", 53 */
{0x00,0x0c,0x30,0xc0,0x30,0x0c,0x00,0x00,}, /* "V", 54 */
{0x7c,0x80,0x80,0x78,0x80,0x80,0x7c,0x00,}, /* "W", 55 */
{0x00,0x84,0x48,0x30,0x30,0x48,0x84,0x00,}, /* "X", 56 */
{0x00,0x06,0x08,0xf0,0x08,0x06,0x00,0x00,}, /* "Y", 57 */
{0x00,0x00,0xc2,0xa2,0x92,0x8e,0x00,0x00,}, /* "Z", 58 */
{0x00,0x00,0xfe,0x82,0x82,0x82,0x00,0x00,}, /* "[", 59 */
{0x00,0x00,0x06,0x18,0x60,0x80,0x00,0x00,}, /* "\", 60 */
{0x00,0x00,0x82,0x82,0x82,0xfe,0x00,0x00,}, /* "]", 61 */
{0x00,0x30,0x0c,0x02,0x0c,0x30,0x00,0x00,}, /* "^", 62 */
{0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,}, /* "_", 63 */
{0x00,0x00,0x04,0x0c,0x18,0x00,0x00,0x00,}, /* "`", 64 */
{0x00,0x00,0x60,0x90,0x90,0xe0,0x00,0x00,}, /* "a", 65 */
{0x00,0x00,0xf8,0xa0,0xe0,0x00,0x00,0x00,}, /* "b", 66 */
{0x00,0x00,0x60,0x90,0x90,0x00,0x00,0x00,}, /* "c", 67 */
{0x00,0x00,0xe0,0xa0,0xf8,0x00,0x00,0x00,}, /* "d", 68 */
{0x00,0x00,0x70,0xa8,0xa8,0x90,0x00,0x00,}, /* "e", 69 */
{0x00,0x00,0x10,0xf8,0x14,0x00,0x00,0x00,}, /* "f", 70 */
{0x00,0x00,0xd8,0xa4,0x7c,0x00,0x00,0x00,}, /* "g", 71 */
{0x00,0x00,0xf8,0x20,0xe0,0x00,0x00,0x00,}, /* "h", 72 */
{0x00,0x00,0x00,0xe8,0x00,0x00,0x00,0x00,}, /* "i", 73 */
{0x00,0x00,0x40,0x90,0x74,0x00,0x00,0x00,}, /* "j", 74 */
{0x00,0x00,0xf8,0x60,0x90,0x00,0x00,0x00,}, /* "k", 75 */
{0x00,0x00,0x78,0x80,0x80,0x00,0x00,0x00,}, /* "l", 76 */
{0x00,0xe0,0x10,0xe0,0x10,0xe0,0x00,0x00,}, /* "m", 77 */
{0x00,0x00,0xf0,0x10,0x10,0xe0,0x00,0x00,}, /* "n", 78 */
{0x00,0x00,0x60,0x90,0x90,0x60,0x00,0x00,}, /* "o", 79 */
{0x00,0x00,0xf0,0x48,0x48,0x30,0x00,0x00,}, /* "p", 80 */
{0x00,0x00,0x30,0x48,0x48,0xf0,0x00,0x00,}, /* "q", 81 */
{0x00,0x00,0x00,0xf0,0x20,0x10,0x00,0x00,}, /* "r", 82 */
{0x00,0x00,0x90,0xa8,0xa8,0x48,0x00,0x00,}, /* "s", 83 */
{0x00,0x10,0x10,0xf8,0x90,0x90,0x00,0x00,}, /* "t", 84 */
{0x00,0x00,0x78,0x80,0x80,0xf8,0x00,0x00,}, /* "u", 85 */
{0x00,0x18,0x60,0x80,0x60,0x18,0x00,0x00,}, /* "v", 86 */
{0x00,0x38,0xc0,0x38,0xc0,0x38,0x00,0x00,}, /* "w", 87 */
{0x00,0x88,0x50,0x20,0x50,0x88,0x00,0x00,}, /* "x", 88 */
{0x00,0x8c,0x50,0x20,0x10,0x0c,0x00,0x00,}, /* "y", 89 */
{0x00,0x88,0xc8,0xa8,0x98,0x88,0x00,0x00,}, /* "z", 90 */
{0x00,0x00,0x10,0x7c,0x82,0x00,0x00,0x00,}, /* "{", 91 */
{0x00,0x00,0x00,0xfe,0x00,0x00,0x00,0x00,}, /* "|", 92 */
{0x00,0x00,0x00,0x82,0x7c,0x10,0x00,0x00,}, /* "}", 93 */
{0x00,0x08,0x04,0x04,0x08,0x10,0x10,0x08,}, /* "~", 94 */
};
/* Exported functions ------------------------------------------------------- */
void OLED_Init(void) {
OLED_WriteCommand(0xAE); // 关闭显示
OLED_WriteCommand(0x20); // 设置内存寻址模式
OLED_WriteCommand(0x10); // 页寻址模式
OLED_WriteCommand(0xB0); // 设置页起始地址
OLED_WriteCommand(0xC8); // 设置COM扫描方向
OLED_WriteCommand(0x00); // 设置低列地址
OLED_WriteCommand(0x10); // 设置高列地址
OLED_WriteCommand(0x40); // 设置显示起始行
OLED_WriteCommand(0x81); // 设置对比度
OLED_WriteCommand(0xFF); // 最大对比度
OLED_WriteCommand(0xA1); // 设置段重映射
OLED_WriteCommand(0xA6); // 正常显示
OLED_WriteCommand(0xA8); // 设置多路复用比率
OLED_WriteCommand(0x3F); // 1/64
OLED_WriteCommand(0xA4); // 输出跟随 RAM 内容
OLED_WriteCommand(0xD3); // 设置显示偏移
OLED_WriteCommand(0x00); // 无偏移
OLED_WriteCommand(0xD5); // 设置显示时钟分频比/振荡频率
OLED_WriteCommand(0xF0); // 高频
OLED_WriteCommand(0xD9); // 设置预充电周期
OLED_WriteCommand(0x22); // 修复缺少分号
OLED_WriteCommand(0xDA); // 设置COM引脚硬件配置
OLED_WriteCommand(0x12); // 修复缺少分号
OLED_WriteCommand(0xDB); // 设置VCOMH电压
OLED_WriteCommand(0x20); // 修复缺少分号
OLED_WriteCommand(0x8D); // 设置充电泵
OLED_WriteCommand(0x14); // 修复缺少分号
OLED_WriteCommand(0xAF); // 打开显示
}
void OLED_Clear(void) {
memset(oled_buffer, 0, sizeof(oled_buffer));
dirty_rect.x_min = 0;
dirty_rect.x_max = OLED_WIDTH - 1;
dirty_rect.y_min = 0;
dirty_rect.y_max = OLED_HEIGHT - 1;
dirty_rect.dirty = 1;
OLED_UpdateScreen();
}
void OLED_UpdateScreen(void) {
OLED_UpdateDirtyScreen();
}
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return;
if (color) {
if (!(oled_buffer[x + (y / 8) * OLED_WIDTH] & (1 << (y % 8)))) {
oled_buffer[x + (y / 8) * OLED_WIDTH] |= (1 << (y % 8));
OLED_MarkDirty(x, y);
}
} else {
if (oled_buffer[x + (y / 8) * OLED_WIDTH] & (1 << (y % 8))) {
oled_buffer[x + (y / 8) * OLED_WIDTH] &= ~(1 << (y % 8));
OLED_MarkDirty(x, y);
}
}
}
void OLED_DrawChar(uint8_t x, uint8_t y, char ch, uint8_t color) {
if (ch < ' ' || ch > '~') return;
if (x >= OLED_WIDTH || y >= OLED_HEIGHT || x + 8 > OLED_WIDTH || y + 8 > OLED_HEIGHT) {
return;
}
const uint8_t *font_data = oled_font[ch - ' '];
for (uint8_t i = 0; i < 8; i++) {
uint8_t column_data = font_data[i];
for (uint8_t j = 0; j < 8; j++) {
if (column_data & (1 << j)) {
OLED_DrawPixel(x + i, y + j, color);
} else {
OLED_DrawPixel(x + i, y + j, !color);
}
}
}
}
void OLED_DrawString(uint8_t x, uint8_t y, const char *str, uint8_t color) {
while (*str) {
OLED_DrawChar(x, y, *str, color);
x += 8;
if (x + 8 > OLED_WIDTH) {
x = 0;
y += 8;
}
if (y + 8 > OLED_HEIGHT) {
break;
}
str++;
}
}
/* Private functions -------------------------------------------------------- */
static void OLED_MarkDirty(uint8_t x, uint8_t y) {
if (!dirty_rect.dirty) {
dirty_rect.x_min = x;
dirty_rect.x_max = x;
dirty_rect.y_min = y;
dirty_rect.y_max = y;
dirty_rect.dirty = 1;
} else {
if (x < dirty_rect.x_min) dirty_rect.x_min = x;
if (x > dirty_rect.x_max) dirty_rect.x_max = x;
if (y < dirty_rect.y_min) dirty_rect.y_min = y;
if (y > dirty_rect.y_max) dirty_rect.y_max = y;
}
}
static void OLED_UpdateDirtyScreen(void) {
if (!dirty_rect.dirty) return;
uint8_t y_start = dirty_rect.y_min / 8;
uint8_t y_end = dirty_rect.y_max / 8;
for (uint8_t i = y_start; i <= y_end; i++) {
OLED_WriteCommand(0xB0 + i);
OLED_WriteCommand(dirty_rect.x_min & 0x0F);
OLED_WriteCommand(0x10 | (dirty_rect.x_min >> 4));
uint8_t width = dirty_rect.x_max - dirty_rect.x_min + 1;
OLED_WriteData(&oled_buffer[dirty_rect.x_min + i * OLED_WIDTH], width);
}
dirty_rect.dirty = 0;
}

View File

@ -1,26 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ----------------------------------------------------------------- */
#include <stdint.h>
/* Exported constants ------------------------------------------------------- */
#define OLED_COLOR_BLACK 0
#define OLED_COLOR_WHITE 1
/* Exported functions prototypes -------------------------------------------- */
void OLED_Init(void);
void OLED_Clear(void);
void OLED_UpdateScreen(void);
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color);
void OLED_DrawString(uint8_t x, uint8_t y, const char *str, uint8_t color);
void OLED_DrawChar(uint8_t x, uint8_t y, char ch, uint8_t color);
void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index);
#ifdef __cplusplus
}
#endif

View File

@ -1,57 +0,0 @@
/* Includes ----------------------------------------------------------------- */
#include "pc_uart.h"
#include <string.h>
#include "bsp\uart.h"
#include "device.h"
#define UART_HANDLE BSP_UART_GetHandle(BSP_UART_PC)
#define AI_LEN_RX_BUFF (sizeof(UART_RxData_t))
static bool rx_flag = false;
static uint8_t rxbuf[AI_LEN_RX_BUFF];
static void UART_RxCpltCallback(void) { rx_flag = true; }
int UART_Init(UART_t *huart)
{
UNUSED(huart);
//注册回调函数
HAL_UART_RegisterCallback(UART_HANDLE, BSP_UART_RX_CPLT_CB, UART_RxCpltCallback);
return DEVICE_OK
}
int UART_StartReceive(UART_t *huart)
{
UNUSED(huart);
HAL_UART_Receive_DMA(UART_HANDLE, rxbuf, AI_LEN_RX_BUFF);
return DEVICE_OK;
}
bool UART_IsReceiveComplete(void)
{
return rx_flag;
}
int8_t UART_ParseData(UART_t *huart)
{
memcpy(&huart->rx_data, rxbuf, sizeof(UART_RxData_t));
}
void UART_PackTx(UART_t *huart, UART_TxData_t *tx_data)
{
memcpy(tx_data, huart->tx_data, sizeof(UART_TxData_t));
}
int8_t UART_StartSend(UART_t *huart)
{
if (HAL_UART_Transmit_DMA(UART_HANDLE, huart->tx_data, sizeof(UART_TxData_t)) == HAL_OK)
{
return DEVICE_OK
}
return DEVICE_ERR;
}

View File

@ -1,50 +0,0 @@
/*
UART通讯模板
*/
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
/* Includes ----------------------------------------------------------------- */
#include <stdbool.h>
#include <stdint.h>
/* Exported constants ------------------------------------------------------- */
/* Exported macro ----------------------------------------------------------- */
/* Exported types ----------------------------------------------------------- */
typedef struct
{
uint8_t head;
uint8_t data;
uint8_t crc;
} UART_RxData_t;
typedef struct
{
uint8_t head;
uint8_t data;
uint8_t crc;
} UART_TxData_t;
typedef struct
{
UART_RxData_t rx_data; // Received data buffer
UART_TxData_t tx_data; // Transmit data buffer
} UART_t;
/* Exported functions prototypes -------------------------------------------- */
int UART_Init(UART_t *huart);
int UART_StartReceive(UART_t *huart);
bool UART_IsReceiveComplete(void);
int8_t UART_ParseData(UART_t *huart);
void UART_PackTx(UART_t *huart, UART_TxData_t *tx_data);
int8_t UART_StartSend(UART_t *huart);
#ifdef __cplusplus
}
#endif

View File

@ -1,41 +0,0 @@
#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

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

Binary file not shown.

522
app/about_interface.py Normal file
View File

@ -0,0 +1,522 @@
import os
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
from PyQt5.QtCore import Qt, QUrl, QTimer
from PyQt5.QtGui import QDesktopServices
from qfluentwidgets import (
PrimaryPushSettingCard, FluentIcon, InfoBar, InfoBarPosition,
SubtitleLabel, BodyLabel, CaptionLabel, StrongBodyLabel,
ElevatedCardWidget, PrimaryPushButton, PushButton,
ProgressBar, TextEdit
)
from .function_fit_interface import FunctionFitInterface
from app.tools.check_update import check_update
from app.tools.auto_updater import AutoUpdater, check_update_availability
from app.tools.update_check_thread import UpdateCheckThread
__version__ = "1.0.6"
class AboutInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("aboutInterface")
# 初始化更新相关变量
self.updater = None
self.update_info = None
self._setup_ui()
def _setup_ui(self):
"""设置用户界面"""
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
layout.setContentsMargins(20, 30, 20, 20)
# 版本信息卡片
version_card = ElevatedCardWidget()
version_layout = QVBoxLayout(version_card)
version_layout.setContentsMargins(24, 20, 24, 20)
version_title = StrongBodyLabel("版本信息")
version_layout.addWidget(version_title)
current_version_label = BodyLabel(f"当前版本v{__version__}")
version_layout.addWidget(current_version_label)
layout.addWidget(version_card)
# 检查更新按钮
self.check_update_card = PrimaryPushSettingCard(
text="检查更新",
icon=FluentIcon.SYNC,
title="检查更新",
content="检查是否有新版本可用需要能够连接github",
)
self.check_update_card.clicked.connect(self.check_for_updates)
layout.addWidget(self.check_update_card)
# 更新信息卡片(初始隐藏)
self.update_info_card = ElevatedCardWidget()
self.update_info_card.hide()
self._setup_update_info_card()
layout.addWidget(self.update_info_card)
layout.addStretch()
def _setup_update_info_card(self):
"""设置更新信息卡片"""
layout = QVBoxLayout(self.update_info_card)
layout.setContentsMargins(24, 20, 24, 20)
layout.setSpacing(16)
# 标题
self.update_title = StrongBodyLabel("发现新版本")
layout.addWidget(self.update_title)
# 版本对比
version_layout = QHBoxLayout()
current_layout = QVBoxLayout()
current_layout.addWidget(CaptionLabel("当前版本"))
self.current_version_label = SubtitleLabel(f"v{__version__}")
current_layout.addWidget(self.current_version_label)
arrow_label = SubtitleLabel("")
arrow_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
arrow_label.setFixedWidth(30)
latest_layout = QVBoxLayout()
latest_layout.addWidget(CaptionLabel("最新版本"))
self.latest_version_label = SubtitleLabel("v--")
latest_layout.addWidget(self.latest_version_label)
version_layout.addLayout(current_layout)
version_layout.addWidget(arrow_label)
version_layout.addLayout(latest_layout)
layout.addLayout(version_layout)
# 更新信息
info_layout = QHBoxLayout()
self.file_size_label = BodyLabel("文件大小: --")
self.release_date_label = BodyLabel("发布时间: --")
info_layout.addWidget(self.file_size_label)
info_layout.addStretch()
info_layout.addWidget(self.release_date_label)
layout.addLayout(info_layout)
# 更新说明
layout.addWidget(CaptionLabel("更新说明:"))
self.notes_display = TextEdit()
self.notes_display.setReadOnly(True)
self.notes_display.setMaximumHeight(200)
self.notes_display.setMinimumHeight(80)
self.notes_display.setText("暂无更新说明")
layout.addWidget(self.notes_display)
# 进度条(初始隐藏)
self.progress_widget = QWidget()
progress_layout = QVBoxLayout(self.progress_widget)
self.progress_label = BodyLabel("准备更新...")
progress_layout.addWidget(self.progress_label)
self.progress_bar = ProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
progress_layout.addWidget(self.progress_bar)
# 详细下载信息
self.download_info = BodyLabel("")
self.download_info.setWordWrap(True)
progress_layout.addWidget(self.download_info)
self.progress_widget.hide()
layout.addWidget(self.progress_widget)
# 按钮区域
button_layout = QHBoxLayout()
self.manual_btn = PushButton("手动下载")
self.manual_btn.setIcon(FluentIcon.LINK)
self.manual_btn.clicked.connect(self.open_manual_download)
self.update_btn = PrimaryPushButton("开始更新")
self.update_btn.setIcon(FluentIcon.DOWNLOAD)
self.update_btn.clicked.connect(self.start_update)
self.cancel_btn = PushButton("取消")
self.cancel_btn.clicked.connect(self.cancel_update)
button_layout.addWidget(self.manual_btn)
button_layout.addStretch()
button_layout.addWidget(self.update_btn)
button_layout.addWidget(self.cancel_btn)
layout.addLayout(button_layout)
def check_for_updates(self):
"""检查更新"""
self.check_update_card.setEnabled(False)
self.check_update_card.setContent("正在检查更新...")
# 延迟执行检查避免阻塞UI
QTimer.singleShot(100, self._perform_check)
def _perform_check(self):
"""执行更新检查"""
try:
# 获取最新版本信息(包括当前版本的详细信息)
latest_info = self._get_latest_release_info()
# 检查是否有可用更新
self.update_info = check_update_availability(__version__)
if self.update_info:
self._show_update_available()
else:
self._show_no_update(latest_info)
except Exception as e:
self._show_error(f"检查更新失败: {str(e)}")
def _get_latest_release_info(self):
"""获取最新发布信息,不论版本是否需要更新"""
try:
import requests
from packaging.version import parse as vparse
url = f"https://api.github.com/repos/goldenfishs/MRobot/releases/latest"
response = requests.get(url, timeout=10)
if response.status_code == 200:
release_data = response.json()
latest_version = release_data["tag_name"].lstrip("v")
# 获取下载URL和文件大小
assets = release_data.get('assets', [])
asset_size = 0
download_url = None
if assets:
# 选择第一个资源文件
asset = assets[0]
asset_size = asset.get('size', 0)
download_url = asset.get('browser_download_url', '')
return {
'version': latest_version,
'release_notes': release_data.get('body', '暂无更新说明'),
'release_date': release_data.get('published_at', ''),
'asset_size': asset_size,
'download_url': download_url
}
else:
return None
except Exception as e:
print(f"获取发布信息失败: {e}")
return None
def _show_update_available(self):
"""显示发现更新"""
# 更新按钮状态
self.check_update_card.setEnabled(True)
self.check_update_card.setContent("发现新版本!")
# 显示更新信息卡片
self.update_info_card.show()
# 设置版本信息
if self.update_info:
version = self.update_info.get('version', 'Unknown')
self.latest_version_label.setText(f"v{version}")
# 设置文件信息
asset_size = self.update_info.get('asset_size', 0)
file_size = self._format_file_size(asset_size)
self.file_size_label.setText(f"文件大小: {file_size}")
# 设置发布时间
release_date = self.update_info.get('release_date', '')
formatted_date = self._format_date(release_date)
self.release_date_label.setText(f"发布时间: {formatted_date}")
# 设置更新说明
notes = self.update_info.get('release_notes', '暂无更新说明')
self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else ''))
InfoBar.success(
title="发现新版本",
content=f"检测到新版本 v{version}",
parent=self,
position=InfoBarPosition.TOP,
duration=3000
)
def _show_no_update(self, latest_info=None):
"""显示无更新,但展示最新版本信息"""
self.check_update_card.setEnabled(True)
self.check_update_card.setContent("已是最新版本")
# 如果有最新版本信息,显示详情卡片
if latest_info:
self.update_info_card.show()
# 显示版本信息(当前版本就是最新版本)
self.latest_version_label.setText(f"v{__version__}")
# 设置文件信息
asset_size = latest_info.get('asset_size', 0)
file_size = self._format_file_size(asset_size)
self.file_size_label.setText(f"文件大小: {file_size}")
# 设置发布时间
release_date = latest_info.get('release_date', '')
formatted_date = self._format_date(release_date)
self.release_date_label.setText(f"发布时间: {formatted_date}")
# 设置更新说明
notes = latest_info.get('release_notes', '暂无更新说明')
self.notes_display.setText(notes[:500] + ('...' if len(notes) > 500 else ''))
# 修改标题和按钮
self.update_title.setText("版本信息")
self.update_btn.setText("手动下载")
self.update_btn.setIcon(FluentIcon.DOWNLOAD)
self.update_btn.setEnabled(True)
self.manual_btn.setEnabled(True)
# 连接手动下载功能
self.update_btn.clicked.disconnect()
self.update_btn.clicked.connect(self.open_manual_download)
InfoBar.info(
title="已是最新版本",
content="当前已是最新版本,无需更新。",
parent=self,
position=InfoBarPosition.TOP,
duration=3000
)
def _show_error(self, error_msg: str):
"""显示错误"""
self.check_update_card.setEnabled(True)
self.check_update_card.setContent("检查更新失败")
InfoBar.error(
title="检查更新失败",
content=error_msg,
parent=self,
position=InfoBarPosition.TOP,
duration=4000
)
def start_update(self):
"""开始更新"""
if not self.update_info:
return
# 显示进度UI
self.progress_widget.show()
self.update_btn.setEnabled(False)
self.manual_btn.setEnabled(False)
# 启动更新器(使用简化的单线程下载)
self.updater = AutoUpdater(__version__)
self.updater.signals.progress_changed.connect(self.update_progress)
self.updater.signals.download_progress.connect(self.update_download_progress)
self.updater.signals.status_changed.connect(self.update_status)
self.updater.signals.error_occurred.connect(self.update_error)
self.updater.signals.update_completed.connect(self.update_completed)
# 开始更新流程
self.updater.start()
def update_progress(self, value: int):
"""更新进度"""
self.progress_bar.setValue(value)
def update_download_progress(self, downloaded: int, total: int, speed: float, remaining: float):
"""更新下载进度详情"""
if total > 0:
downloaded_str = self._format_bytes(downloaded)
total_str = self._format_bytes(total)
percentage = (downloaded / total) * 100
info_text = f"已下载: {downloaded_str} / {total_str} ({percentage:.1f}%)"
self.download_info.setText(info_text)
def _format_bytes(self, size_bytes: int) -> str:
"""格式化字节大小"""
if size_bytes == 0:
return "0 B"
size = float(size_bytes)
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
def update_status(self, status: str):
"""更新状态"""
self.progress_label.setText(status)
def update_error(self, error_msg: str):
"""更新错误"""
self.progress_widget.hide()
self.update_btn.setEnabled(True)
self.manual_btn.setEnabled(True)
# 如果是平台兼容性问题,提供更友好的提示
if "Windows 安装程序" in error_msg and "当前系统是" in error_msg:
InfoBar.warning(
title="平台不兼容",
content="检测到 Windows 安装程序,请点击'手动下载'获取适合 macOS 的版本",
parent=self,
position=InfoBarPosition.TOP,
duration=6000
)
else:
InfoBar.error(
title="更新失败",
content=error_msg,
parent=self,
position=InfoBarPosition.TOP,
duration=4000
)
def update_completed(self, file_path=None):
"""更新完成 - 显示下载文件位置"""
print(f"update_completed called with file_path: {file_path}") # 调试输出
self.progress_label.setText("下载完成!")
self.progress_bar.setValue(100)
# 重新启用按钮
self.update_btn.setEnabled(True)
self.manual_btn.setEnabled(True)
if file_path and os.path.exists(file_path):
print(f"File exists: {file_path}") # 调试输出
InfoBar.success(
title="下载完成",
content="安装文件已下载完成,点击下方按钮打开文件位置",
parent=self,
position=InfoBarPosition.TOP,
duration=5000
)
# 添加打开文件夹按钮
self._add_open_folder_button(file_path)
else:
print(f"File does not exist or file_path is None: {file_path}") # 调试输出
InfoBar.success(
title="下载完成",
content="文件下载完成",
parent=self,
position=InfoBarPosition.TOP,
duration=3000
)
def _add_open_folder_button(self, file_path):
"""添加打开文件夹按钮"""
def open_file_location():
folder_path = os.path.dirname(file_path)
# 在 macOS 上使用 Finder 打开文件夹
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
InfoBar.info(
title="已打开文件夹",
content=f"文件位置: {folder_path}",
parent=self,
position=InfoBarPosition.TOP,
duration=3000
)
# 直接替换更新按钮的文本和功能
self.update_btn.setText("打开文件位置")
self.update_btn.setIcon(FluentIcon.FOLDER)
# 断开原有连接
self.update_btn.clicked.disconnect()
# 连接新功能
self.update_btn.clicked.connect(open_file_location)
# 修改取消按钮为清理按钮
self.cancel_btn.setText("清理临时文件")
self.cancel_btn.setIcon(FluentIcon.DELETE)
self.cancel_btn.clicked.disconnect()
def cleanup_temp_files():
if self.updater:
self.updater.cleanup()
InfoBar.success(
title="已清理",
content="临时文件已清理完成",
parent=self,
position=InfoBarPosition.TOP,
duration=2000
)
# 重置界面
self.update_info_card.hide()
self.check_update_card.setContent("检查是否有新版本可用")
self.cancel_btn.clicked.connect(cleanup_temp_files)
def cancel_update(self):
"""取消更新"""
if hasattr(self, 'updater') and self.updater and self.updater.isRunning():
self.updater.cancel_update()
self.updater.cleanup()
self.update_info_card.hide()
self.check_update_card.setContent("检查是否有新版本可用")
def open_manual_download(self):
"""打开手动下载页面"""
QDesktopServices.openUrl(QUrl("https://github.com/goldenfishs/MRobot/releases/latest"))
InfoBar.info(
title="手动下载",
content="已为您打开下载页面",
parent=self,
position=InfoBarPosition.TOP,
duration=2000
)
def _restart_app(self):
"""重启应用程序"""
if self.updater:
self.updater.restart_application()
def _format_file_size(self, size_bytes: int) -> str:
"""格式化文件大小"""
if size_bytes == 0:
return "--"
size = float(size_bytes)
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
def _format_date(self, date_str: str) -> str:
"""格式化日期"""
if not date_str:
return "--"
try:
from datetime import datetime
date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
return date_obj.strftime('%Y-%m-%d')
except:
return date_str[:10] if len(date_str) >= 10 else date_str

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://qutrobot.top: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

View File

@ -0,0 +1,173 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QSizePolicy
from PyQt5.QtCore import Qt
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
from qfluentwidgets import TitleLabel, BodyLabel, PushButton, FluentIcon
from PyQt5.QtWidgets import QFileDialog
import os
from .function_fit_interface import FunctionFitInterface
from .ai_interface import AIInterface
from qfluentwidgets import InfoBar
from .tools.update_code import update_code
from .code_generate_interface import CodeGenerateInterface
class CodeConfigurationInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("CodeConfigurationInterface")
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)
self.stackedWidget = QStackedWidget(self)
self.vBoxLayout.addWidget(self.stackedWidget)
# 初始主页面
self.mainPage = QWidget(self)
mainLayout = QVBoxLayout(self.mainPage)
mainLayout.setAlignment(Qt.AlignTop)
mainLayout.setSpacing(28) # 设置间距
mainLayout.setContentsMargins(48, 48, 48, 48) # 设置内容边距
#添加空行
title = TitleLabel("MRobot 代码生成")
title.setAlignment(Qt.AlignCenter)
mainLayout.addWidget(title)
subtitle = BodyLabel("请选择您的由CUBEMX生成的工程路径.ico所在的目录然后开启代码之旅")
subtitle.setAlignment(Qt.AlignCenter)
mainLayout.addWidget(subtitle)
desc = BodyLabel("支持自动配置和生成任务自主选择模块代码倒入自动识别cubemx配置")
desc.setAlignment(Qt.AlignCenter)
mainLayout.addWidget(desc)
mainLayout.addSpacing(18)
self.choose_btn = PushButton(FluentIcon.FOLDER, "选择项目路径")
self.choose_btn.setFixedWidth(200)
mainLayout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
self.update_template_btn.setFixedWidth(200)
mainLayout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
mainLayout.addSpacing(10)
mainLayout.addStretch()
# 添加主页面到堆叠窗口
self.addSubInterface(self.mainPage, "mainPage", "代码生成主页")
self.setLayout(self.vBoxLayout)
# 信号连接
self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
self.tabBar.tabCloseRequested.connect(self.onCloseTab)
self.choose_btn.clicked.connect(self.choose_project_folder) # 启用选择项目路径按钮
self.update_template_btn.clicked.connect(self.on_update_template)
def on_update_template(self):
def info(parent):
InfoBar.success(
title="更新成功",
content="用户模板已更新到最新版本!",
parent=parent,
duration=2000
)
def error(parent, msg):
InfoBar.error(
title="更新失败",
content=f"用户模板更新失败: {msg}",
parent=parent,
duration=3000
)
update_code(parent=self, info_callback=info, error_callback=error)
def choose_project_folder(self):
folder = QFileDialog.getExistingDirectory(self, "选择CUBEMX工程目录")
if not folder:
return
ioc_files = [f for f in os.listdir(folder) if f.endswith('.ioc')]
if ioc_files:
# 检查是否已存在 codeGenPage 标签页
for i in range(self.stackedWidget.count()):
widget = self.stackedWidget.widget(i)
if widget is not None and widget.objectName() == "codeGenPage":
# 如果已存在,则切换到该标签页,并更新路径显示
if hasattr(widget, "project_path"):
widget.project_path = folder
if hasattr(widget, "refresh"):
widget.refresh()
self.stackedWidget.setCurrentWidget(widget)
self.tabBar.setCurrentTab("codeGenPage")
return
# 不存在则新建
code_gen_page = CodeGenerateInterface(folder, self)
self.addSubInterface(code_gen_page, "codeGenPage", "代码生成")
self.stackedWidget.setCurrentWidget(code_gen_page)
self.tabBar.setCurrentTab("codeGenPage")
else:
InfoBar.error(
title="未找到.ioc文件",
content="所选文件夹不是有效的CUBEMX工程目录请重新选择。",
parent=self,
duration=3000
)
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())
# 禁止关闭主页
if widget.objectName() == "mainPage":
return
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,414 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy, QTreeWidget, QTreeWidgetItem, QStackedWidget
from PyQt5.QtCore import Qt
from qfluentwidgets import TitleLabel, BodyLabel, PushButton, TreeWidget, FluentIcon, InfoBar
from app.tools.analyzing_ioc import analyzing_ioc
from app.code_page.bsp_interface import bsp
from app.data_interface import DataInterface
from app.tools.code_generator import CodeGenerator
import os
import csv
import sys
import importlib
class CodeGenerateInterface(QWidget):
def __init__(self, project_path, parent=None):
super().__init__(parent)
self.setObjectName("CodeGenerateInterface")
self.project_path = project_path
# 初始化页面缓存
self.page_cache = {}
self._init_ui()
def _init_ui(self):
main_layout = QVBoxLayout(self)
main_layout.setAlignment(Qt.AlignTop)
main_layout.setContentsMargins(10, 10, 10, 10)
top_layout = self._create_top_layout()
main_layout.addLayout(top_layout)
content_layout = QHBoxLayout()
content_layout.setContentsMargins(0, 10, 0, 0)
main_layout.addLayout(content_layout)
# 左侧树形列表使用qfluentwidgets的TreeWidget
self.tree = TreeWidget()
self.tree.setHeaderHidden(True)
self.tree.setMaximumWidth(250)
self.tree.setBorderRadius(8)
self.tree.setBorderVisible(True)
content_layout.addWidget(self.tree)
# 右侧内容区
self.stack = QStackedWidget()
content_layout.addWidget(self.stack)
self._load_csv_and_build_tree()
self.tree.itemClicked.connect(self.on_tree_item_clicked)
def _create_top_layout(self):
"""创建顶部横向布局"""
top_layout = QHBoxLayout()
top_layout.setAlignment(Qt.AlignTop)
# 项目名称标签
project_name = os.path.basename(self.project_path)
name_label = BodyLabel(f"项目名称: {project_name}")
name_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
top_layout.addWidget(name_label)
# FreeRTOS状态标签
freertos_label = BodyLabel(f"FreeRTOS: {self._get_freertos_status()}")
freertos_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
top_layout.addWidget(freertos_label)
# 自动生成FreeRTOS任务按钮
auto_task_btn = PushButton(FluentIcon.SEND, "配置FreeRTOS")
auto_task_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
auto_task_btn.clicked.connect(self.on_freertos_task_btn_clicked)
top_layout.addWidget(auto_task_btn, alignment=Qt.AlignRight)
# 配置并生成FreeRTOS任务按钮
freertos_task_btn = PushButton(FluentIcon.SETTING, "创建任务")
freertos_task_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
freertos_task_btn.clicked.connect(self.on_task_code_btn_clicked)
top_layout.addWidget(freertos_task_btn, alignment=Qt.AlignRight)
# 配置cmake按钮
cmake_btn = PushButton(FluentIcon.FOLDER, "配置cmake")
cmake_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
cmake_btn.clicked.connect(self.on_cmake_config_btn_clicked)
top_layout.addWidget(cmake_btn, alignment=Qt.AlignRight)
# 生成代码按钮
generate_btn = PushButton(FluentIcon.PROJECTOR,"生成代码")
generate_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
generate_btn.clicked.connect(self.generate_code)
top_layout.addWidget(generate_btn, alignment=Qt.AlignRight)
return top_layout
def on_task_code_btn_clicked(self):
# 检查是否开启 FreeRTOS
ioc_files = [f for f in os.listdir(self.project_path) if f.endswith('.ioc')]
if ioc_files:
ioc_path = os.path.join(self.project_path, ioc_files[0])
if not analyzing_ioc.is_freertos_enabled_from_ioc(ioc_path):
InfoBar.error(
title="错误",
content="请先在 .ioc 文件中开启 FreeRTOS再进行任务配置",
parent=self,
duration=3000
)
return
else:
InfoBar.error(
title="错误",
content="未找到 .ioc 文件,无法检测 FreeRTOS 状态!",
parent=self,
duration=3000
)
return
# 直接弹出任务配置对话框并生成代码
dlg = DataInterface()
dlg.project_path = self.project_path
result = dlg.open_task_config_dialog()
# 生成任务成功后弹出 InfoBar 提示
if getattr(dlg, "task_generate_success", False):
InfoBar.success(
title="任务生成成功",
content="FreeRTOS任务代码已生成",
parent=self,
duration=2000
)
def on_freertos_task_btn_clicked(self):
# 检查是否开启 FreeRTOS
ioc_files = [f for f in os.listdir(self.project_path) if f.endswith('.ioc')]
if ioc_files:
ioc_path = os.path.join(self.project_path, ioc_files[0])
if not analyzing_ioc.is_freertos_enabled_from_ioc(ioc_path):
InfoBar.error(
title="错误",
content="请先在 .ioc 文件中开启 FreeRTOS再自动生成任务",
parent=self,
duration=3000
)
return
else:
InfoBar.error(
title="错误",
content="未找到 .ioc 文件,无法检测 FreeRTOS 状态!",
parent=self,
duration=3000
)
return
# 自动生成FreeRTOS任务代码
from app.data_interface import DataInterface
di = DataInterface()
di.project_path = self.project_path
di.generate_freertos_task()
InfoBar.success(
title="自动生成成功",
content="FreeRTOS任务代码已自动生成",
parent=self,
duration=2000
)
def on_cmake_config_btn_clicked(self):
"""配置cmake自动更新CMakeLists.txt中的源文件列表"""
try:
from app.tools.update_cmake_sources import find_user_c_files, update_cmake_sources,update_cmake_includes
from pathlib import Path
# 构建User目录和CMakeLists.txt路径规范化路径分隔符
user_dir = os.path.normpath(os.path.join(self.project_path, "User"))
cmake_file = os.path.normpath(os.path.join(self.project_path, "CMakeLists.txt"))
print(f"项目路径: {self.project_path}")
print(f"User目录路径: {user_dir}")
print(f"CMakeLists.txt路径: {cmake_file}")
# 检查User目录是否存在
if not os.path.exists(user_dir):
InfoBar.error(
title="错误",
content=f"User目录不存在: {user_dir}",
parent=self,
duration=3000
)
return
# 检查CMakeLists.txt是否存在
if not os.path.exists(cmake_file):
InfoBar.error(
title="错误",
content=f"CMakeLists.txt文件不存在: {cmake_file}",
parent=self,
duration=3000
)
return
# 查找User目录下的所有.c文件
print("开始查找.c文件...")
c_files = find_user_c_files(user_dir)
print(f"找到 {len(c_files)} 个.c文件")
if not c_files:
InfoBar.warning(
title="警告",
content="在User目录下没有找到.c文件",
parent=self,
duration=3000
)
return
# 更新CMakeLists.txt
print("开始更新CMakeLists.txt...")
sources_success = update_cmake_sources(cmake_file, c_files)
includes_success = update_cmake_includes(cmake_file, user_dir)
if sources_success and includes_success:
InfoBar.success(
title="配置成功",
content=f"已成功更新CMakeLists.txt共添加了 {len(c_files)} 个源文件",
parent=self,
duration=3000
)
elif sources_success:
InfoBar.warning(
title="部分成功",
content=f"源文件更新成功但include路径更新失败",
parent=self,
duration=3000
)
elif includes_success:
InfoBar.warning(
title="部分成功",
content=f"include路径更新成功但源文件更新失败",
parent=self,
duration=3000
)
else:
InfoBar.error(
title="配置失败",
content="更新CMakeLists.txt失败请检查文件格式",
parent=self,
duration=3000
)
except ImportError as e:
print(f"导入错误: {e}")
InfoBar.error(
title="导入错误",
content=f"无法导入cmake配置模块: {str(e)}",
parent=self,
duration=3000
)
except Exception as e:
print(f"cmake配置错误: {e}")
import traceback
traceback.print_exc()
InfoBar.error(
title="配置失败",
content=f"cmake配置过程中出现错误: {str(e)}",
parent=self,
duration=3000
)
def generate_code(self):
"""生成所有代码,包括未加载页面"""
try:
# 先收集所有页面名从CSV配置文件读取
from app.tools.code_generator import CodeGenerator # 在方法内重新导入确保可用
csv_path = os.path.join(CodeGenerator.get_assets_dir("User_code"), "config.csv")
all_class_names = []
if os.path.exists(csv_path):
with open(csv_path, newline='', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
row = [cell.strip() for cell in row if cell.strip()]
if not row:
continue
main_title = row[0]
for sub in row[1:]:
class_name = f"{main_title}_{sub}".replace("-", "_")
all_class_names.append(class_name)
# 创建所有页面对象(无论是否点击过)
bsp_pages = []
component_pages = []
device_pages = []
for class_name in all_class_names:
widget = self._get_or_create_page(class_name)
if widget:
if hasattr(widget, '_generate_bsp_code_internal') and widget not in bsp_pages:
bsp_pages.append(widget)
elif hasattr(widget, '_generate_component_code_internal') and widget not in component_pages:
component_pages.append(widget)
elif hasattr(widget, '_generate_device_code_internal') and widget not in device_pages:
device_pages.append(widget)
# 确保导入成功
from app.code_page.bsp_interface import bsp
from app.code_page.component_interface import component
from app.code_page.device_interface import device
# 生成 BSP 代码
bsp_result = bsp.generate_bsp(self.project_path, bsp_pages)
# 生成 Component 代码
component_result = component.generate_component(self.project_path, component_pages)
# 生成 Device 代码
device_result = device.generate_device(self.project_path, device_pages)
# 合并结果信息
combined_result = f"BSP代码生成:\n{bsp_result}\n\nComponent代码生成:\n{component_result}\n\nDevice代码生成:\n{device_result}"
InfoBar.success(
title="代码生成结果",
content=combined_result,
parent=self,
duration=5000
)
except ImportError as e:
InfoBar.error(
title="导入错误",
content=f"模块导入失败: {str(e)}",
parent=self,
duration=3000
)
except Exception as e:
InfoBar.error(
title="生成失败",
content=f"代码生成过程中出现错误: {str(e)}",
parent=self,
duration=3000
)
def _get_freertos_status(self):
"""获取FreeRTOS状态"""
ioc_files = [f for f in os.listdir(self.project_path) if f.endswith('.ioc')]
if ioc_files:
ioc_path = os.path.join(self.project_path, ioc_files[0])
return "开启" if analyzing_ioc.is_freertos_enabled_from_ioc(ioc_path) else "未开启"
return "未找到.ioc文件"
def _load_csv_and_build_tree(self):
from app.tools.code_generator import CodeGenerator # 在方法内重新导入确保可用
csv_path = os.path.join(CodeGenerator.get_assets_dir("User_code"), "config.csv")
print(f"加载CSV路径: {csv_path}")
if not os.path.exists(csv_path):
print(f"配置文件未找到: {csv_path}")
return
self.tree.clear()
with open(csv_path, newline='', encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
row = [cell.strip() for cell in row if cell.strip()]
if not row:
continue
main_title = row[0]
main_item = QTreeWidgetItem([main_title])
for sub in row[1:]:
sub_item = QTreeWidgetItem([sub])
main_item.addChild(sub_item)
self.tree.addTopLevelItem(main_item)
self.tree.repaint()
def on_tree_item_clicked(self, item, column):
if item.parent():
main_title = item.parent().text(0)
sub_title = item.text(0)
class_name = f"{main_title}_{sub_title}".replace("-", "_")
widget = self._get_or_create_page(class_name)
if widget:
self.stack.setCurrentWidget(widget)
def _get_or_create_page(self, class_name):
"""获取或创建页面"""
if class_name in self.page_cache:
return self.page_cache[class_name]
# 如果是第一次创建组件页面,初始化组件管理器
if not hasattr(self, 'component_manager'):
from app.code_page.component_interface import ComponentManager
self.component_manager = ComponentManager()
try:
if class_name.startswith('bsp_'):
# BSP页面
from app.code_page.bsp_interface import get_bsp_page
# 提取外设名,如 bsp_error_detect -> error_detect
periph_name = class_name[len('bsp_'):] # 移除 .replace("_", " ")
page = get_bsp_page(periph_name, self.project_path)
elif class_name.startswith('component_'):
from app.code_page.component_interface import get_component_page
comp_name = class_name[len('component_'):] # 移除 .replace("_", " ")
page = get_component_page(comp_name, self.project_path, self.component_manager)
self.component_manager.register_component(page.component_name, page)
elif class_name.startswith('device_'):
# Device页面
from app.code_page.device_interface import get_device_page
device_name = class_name[len('device_'):] # 移除 device_ 前缀
page = get_device_page(device_name, self.project_path)
else:
print(f"未知的页面类型: {class_name}")
return None
self.page_cache[class_name] = page
self.stack.addWidget(page)
return page
except Exception as e:
print(f"创建页面 {class_name} 失败: {e}")
return None

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,372 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QCheckBox, QComboBox, QTableWidget, QHeaderView, QMessageBox, QHBoxLayout, QTextEdit
from qfluentwidgets import TitleLabel, BodyLabel, PushButton, CheckBox, TableWidget, LineEdit, ComboBox, MessageBox, SubtitleLabel, FluentIcon, TextEdit
from qfluentwidgets import InfoBar
from PyQt5.QtCore import Qt, pyqtSignal
from app.tools.code_generator import CodeGenerator
import os
import shutil
def get_component_page(component_name, project_path, component_manager=None):
"""根据组件名返回对应的页面类没有特殊类则返回默认ComponentSimple"""
name_lower = component_name.lower()
special_classes = {
"pid": component_pid,
"filter": component_filter,
# 以后可以继续添加特殊组件
}
if name_lower in special_classes:
return special_classes[name_lower](project_path, component_manager)
else:
template_names = {
'header': f'{name_lower}.h',
'source': f'{name_lower}.c'
}
return ComponentSimple(project_path, component_name, template_names, component_manager)
def get_all_dependency_components(dependencies):
"""获取所有被依赖的组件列表"""
dependent_components = set()
for component, deps in dependencies.items():
for dep_path in deps:
dep_name = os.path.basename(dep_path)
dependent_components.add(dep_name.lower())
return dependent_components
class ComponentSimple(QWidget):
"""简单组件界面 - 只有开启/关闭功能"""
def __init__(self, project_path, component_name, template_names, component_manager=None):
super().__init__()
self.project_path = project_path
self.component_name = component_name
self.template_names = template_names
self.component_manager = component_manager
# 加载描述和依赖信息
component_dir = CodeGenerator.get_assets_dir("User_code/component")
describe_path = os.path.join(component_dir, "describe.csv")
dependencies_path = os.path.join(component_dir, "dependencies.csv")
self.descriptions = CodeGenerator.load_descriptions(describe_path)
self.dependencies = CodeGenerator.load_dependencies(dependencies_path)
self._init_ui()
self._load_config()
def _init_ui(self):
layout = QVBoxLayout(self)
top_layout = QHBoxLayout()
top_layout.setAlignment(Qt.AlignVCenter)
self.generate_checkbox = CheckBox(f"启用 {self.component_name}")
self.generate_checkbox.stateChanged.connect(self._on_checkbox_changed)
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
top_layout.addStretch()
title = SubtitleLabel(f"{self.component_name} 配置 ")
title.setAlignment(Qt.AlignHCenter)
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
top_layout.addStretch()
layout.addLayout(top_layout)
desc = self.descriptions.get(self.component_name.lower(), "")
if desc:
desc_label = BodyLabel(f"功能说明:{desc}")
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
deps = self.dependencies.get(self.component_name.lower(), [])
if deps:
deps_text = f"依赖组件:{', '.join([os.path.basename(dep) for dep in deps])}"
deps_label = BodyLabel(deps_text)
deps_label.setWordWrap(True)
deps_label.setStyleSheet("color: #888888;")
layout.addWidget(deps_label)
# 不再自动启用依赖,只做提示
layout.addStretch()
def _on_checkbox_changed(self, state):
pass # 不再自动启用依赖
def is_need_generate(self):
return self.generate_checkbox.isChecked()
def get_enabled_dependencies(self):
if not self.is_need_generate():
return []
return self.dependencies.get(self.component_name.lower(), [])
def _generate_component_code_internal(self):
# 检查是否需要生成
if not self.is_need_generate():
# 如果未勾选,检查文件是否已存在,如果存在则跳过
for filename in self.template_names.values():
output_path = os.path.join(self.project_path, f"User/component/{filename}")
if os.path.exists(output_path):
return "skipped" # 返回特殊值表示跳过
return "not_needed" # 返回特殊值表示不需要生成
template_dir = self._get_component_template_dir()
for key, filename in self.template_names.items():
template_path = os.path.join(template_dir, filename)
template_content = CodeGenerator.load_template(template_path)
if not template_content:
print(f"模板文件不存在或为空: {template_path}")
continue
output_path = os.path.join(self.project_path, f"User/component/{filename}")
CodeGenerator.save_with_preserve(output_path, template_content)
self._save_config()
return True
def _get_component_template_dir(self):
return CodeGenerator.get_assets_dir("User_code/component")
def _save_config(self):
config_path = os.path.join(self.project_path, "User/component/component_config.yaml")
config_data = CodeGenerator.load_config(config_path)
config_data[self.component_name.lower()] = {
'enabled': self.is_need_generate(),
'dependencies': self.dependencies.get(self.component_name.lower(), [])
}
CodeGenerator.save_config(config_data, config_path)
def _load_config(self):
config_path = os.path.join(self.project_path, "User/component/component_config.yaml")
config_data = CodeGenerator.load_config(config_path)
conf = config_data.get(self.component_name.lower(), {})
if conf.get('enabled', False):
self.generate_checkbox.setChecked(True)
class ComponentManager:
"""组件依赖管理器"""
def __init__(self):
self.component_pages = {} # 组件名 -> 页面对象
def register_component(self, component_name, page):
"""注册组件页面"""
self.component_pages[component_name.lower()] = page
def _sync_dependency_states(self):
"""同步所有依赖状态"""
# 重新计算所有依赖计数
new_dependency_count = {}
for page_name, page in self.component_pages.items():
if page.is_need_generate():
deps = page.get_enabled_dependencies()
for dep_path in deps:
dep_name = os.path.basename(dep_path).lower()
new_dependency_count[dep_name] = new_dependency_count.get(dep_name, 0) + 1
# 更新依赖计数
self.dependency_count = new_dependency_count
# 更新所有页面的状态
for page_name, page in self.component_pages.items():
if page.is_dependency:
count = self.dependency_count.get(page_name, 0)
page.set_dependency_count(count)
def enable_dependencies(self, component_name, deps):
"""启用依赖项"""
for dep_path in deps:
dep_name = os.path.basename(dep_path).lower()
# 增加依赖计数
self.dependency_count[dep_name] = self.dependency_count.get(dep_name, 0) + 1
# 更新被依赖的组件状态
if dep_name in self.component_pages:
page = self.component_pages[dep_name]
page.set_dependency_count(self.dependency_count[dep_name])
def disable_dependencies(self, component_name, deps):
"""禁用依赖项"""
for dep_path in deps:
dep_name = os.path.basename(dep_path).lower()
# 减少依赖计数
if dep_name in self.dependency_count:
self.dependency_count[dep_name] = max(0, self.dependency_count[dep_name] - 1)
# 更新被依赖的组件状态
if dep_name in self.component_pages:
page = self.component_pages[dep_name]
page.set_dependency_count(self.dependency_count[dep_name])
# 具体组件类
class component_pid(ComponentSimple):
def __init__(self, project_path, component_manager=None):
super().__init__(
project_path,
"PID",
{'header': 'pid.h', 'source': 'pid.c'},
component_manager
)
class component_filter(ComponentSimple):
def __init__(self, project_path, component_manager=None):
super().__init__(
project_path,
"Filter",
{'header': 'filter.h', 'source': 'filter.c'},
component_manager
)
# ...existing code... (component 类的 generate_component 方法保持不变)
class component(QWidget):
"""组件管理器"""
def __init__(self, project_path):
super().__init__()
self.project_path = project_path
@staticmethod
def generate_component(project_path, pages):
"""生成所有组件代码,处理依赖关系"""
# 在方法开始时导入CodeGenerator以确保可用
from app.tools.code_generator import CodeGenerator
# 自动添加 component.h
src_component_h = os.path.join(CodeGenerator.get_assets_dir("User_code/component"), "component.h")
dst_component_h = os.path.join(project_path, "User/component/component.h")
os.makedirs(os.path.dirname(dst_component_h), exist_ok=True)
if os.path.exists(src_component_h):
with open(src_component_h, 'r', encoding='utf-8') as f:
content = f.read()
CodeGenerator.save_with_preserve(dst_component_h, content)
# 收集所有需要生成的组件和它们的依赖
components_to_generate = set()
component_pages = {}
for page in pages:
# 检查是否是组件页面(通过类名或者属性判断)
if hasattr(page, "component_name") and hasattr(page, "is_need_generate"):
if page.is_need_generate():
component_name = page.component_name.lower()
components_to_generate.add(component_name)
component_pages[component_name] = page
# 添加依赖组件,依赖格式是路径形式如 "component/filter"
deps = page.get_enabled_dependencies()
for dep_path in deps:
# 跳过BSP层依赖
if dep_path.startswith('bsp/'):
continue
# 从路径中提取组件名,如 "component/filter" -> "filter"
dep_name = os.path.basename(dep_path)
# 只有不包含文件扩展名的才是组件,有扩展名的是文件依赖
if not dep_name.endswith(('.h', '.c', '.hpp', '.cpp')):
components_to_generate.add(dep_name)
# 为没有对应页面但需要生成的依赖组件创建临时页面
user_code_dir = CodeGenerator.get_assets_dir("User_code")
for comp_name in components_to_generate:
if comp_name not in component_pages:
# 创建临时组件页面
template_names = {'header': f'{comp_name}.h', 'source': f'{comp_name}.c'}
temp_page = ComponentSimple(project_path, comp_name.upper(), template_names)
# temp_page.set_forced_enabled(True) # 自动启用依赖组件
component_pages[comp_name] = temp_page
# 如果没有组件需要生成,返回提示信息
if not components_to_generate:
return "没有启用的组件需要生成代码。"
# 生成代码和依赖文件
success_count = 0
fail_count = 0
fail_list = []
# 处理依赖文件的复制
all_deps = set()
for page in pages:
if hasattr(page, "component_name") and hasattr(page, "is_need_generate"):
if page.is_need_generate():
deps = page.get_enabled_dependencies()
all_deps.update(deps)
# 复制依赖文件
for dep_path in all_deps:
try:
# 检查是否是 bsp 层依赖
if dep_path.startswith('bsp/'):
# 对于 bsp 层依赖,跳过复制,因为这些由 BSP 代码生成负责
print(f"跳过 BSP 层依赖: {dep_path} (由 BSP 代码生成负责)")
continue
# dep_path 格式如 "component/filter" 或 "component/user_math.h"
src_path = os.path.join(user_code_dir, dep_path)
dst_path = os.path.join(project_path, "User", dep_path)
if os.path.isdir(src_path):
# 如果是目录,复制整个目录
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
if os.path.exists(dst_path):
shutil.rmtree(dst_path)
shutil.copytree(src_path, dst_path)
elif os.path.isfile(src_path):
# 如果是文件,复制单个文件并保留用户区域
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
with open(src_path, 'r', encoding='utf-8') as f:
new_content = f.read()
CodeGenerator.save_with_preserve(dst_path, new_content)
else:
# 如果既不是文件也不是目录,跳过
print(f"跳过不存在的依赖: {dep_path}")
continue
success_count += 1
print(f"成功复制依赖: {dep_path}")
except Exception as e:
# 对于 bsp 层依赖的错误,只记录但不计入失败
if dep_path.startswith('bsp/'):
print(f"BSP 层依赖 {dep_path} 复制失败,但忽略此错误: {e}")
else:
fail_count += 1
fail_list.append(f"{dep_path} (依赖复制异常: {e})")
print(f"复制依赖失败: {dep_path}, 错误: {e}")
# 生成组件代码
skipped_count = 0
skipped_list = []
for comp_name in components_to_generate:
if comp_name in component_pages:
page = component_pages[comp_name]
try:
# 确保调用正确的方法名
if hasattr(page, '_generate_component_code_internal'):
result = page._generate_component_code_internal()
if result == "skipped":
skipped_count += 1
skipped_list.append(comp_name)
print(f"跳过组件生成: {comp_name}")
elif result:
success_count += 1
print(f"成功生成组件: {comp_name}")
else:
fail_count += 1
fail_list.append(f"{comp_name} (生成失败)")
print(f"生成组件失败: {comp_name}")
else:
fail_count += 1
fail_list.append(f"{comp_name} (缺少生成方法)")
print(f"组件页面缺少生成方法: {comp_name}")
except Exception as e:
fail_count += 1
fail_list.append(f"{comp_name} (生成异常: {e})")
print(f"生成组件异常: {comp_name}, 错误: {e}")
total_items = len(all_deps) + len(components_to_generate)
msg = f"组件代码生成完成:总共处理 {total_items} 项,成功生成 {success_count} 项,跳过 {skipped_count} 项,失败 {fail_count} 项。"
if skipped_list:
msg += f"\n跳过项(文件已存在且未勾选):\n" + "\n".join(skipped_list)
if fail_list:
msg += "\n失败项:\n" + "\n".join(fail_list)
return msg

View File

@ -0,0 +1,404 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout
from qfluentwidgets import BodyLabel, CheckBox, ComboBox, SubtitleLabel
from PyQt5.QtCore import Qt
from app.tools.code_generator import CodeGenerator
import os
import yaml
import re
def get_available_bsp_devices(project_path, bsp_type, gpio_type=None):
"""获取可用的BSP设备GPIO可选类型过滤"""
bsp_config_path = os.path.join(project_path, "User/bsp/bsp_config.yaml")
if not os.path.exists(bsp_config_path):
return []
try:
with open(bsp_config_path, 'r', encoding='utf-8') as f:
bsp_config = yaml.safe_load(f)
if bsp_type == "gpio" and bsp_config.get("gpio", {}).get("enabled", False):
configs = bsp_config["gpio"].get("configs", [])
# 增加类型过滤
if gpio_type:
configs = [cfg for cfg in configs if cfg.get('type', '').lower() == gpio_type.lower()]
return [f"BSP_GPIO_{cfg['custom_name']}" for cfg in configs]
elif bsp_type == "pwm" and bsp_config.get("pwm", {}).get("enabled", False):
# PWM使用configs结构
configs = bsp_config["pwm"].get("configs", [])
return [f"BSP_PWM_{cfg['custom_name']}" for cfg in configs]
elif bsp_type in bsp_config and bsp_config[bsp_type].get('enabled', False):
devices = bsp_config[bsp_type].get('devices', [])
return [f"BSP_{bsp_type.upper()}_{device['name']}" for device in devices]
except Exception as e:
print(f"读取BSP配置失败: {e}")
return []
def generate_device_header(project_path, enabled_devices):
"""生成device.h文件"""
from app.tools.code_generator import CodeGenerator
device_dir = CodeGenerator.get_assets_dir("User_code/device")
template_path = os.path.join(device_dir, "device.h")
dst_path = os.path.join(project_path, "User/device/device.h")
# 优先读取项目中已存在的文件,如果不存在则使用模板
if os.path.exists(dst_path):
# 读取现有文件以保留用户区域
with open(dst_path, 'r', encoding='utf-8') as f:
content = f.read()
else:
# 文件不存在时从模板创建
with open(template_path, 'r', encoding='utf-8') as f:
content = f.read()
# 收集所有需要的信号定义
signals = []
current_bit = 0
# 加载设备配置来获取信号信息
config_path = os.path.join(device_dir, "config.yaml")
device_configs = CodeGenerator.load_device_config(config_path)
for device_name in enabled_devices:
device_key = device_name.lower()
if device_key in device_configs.get('devices', {}):
device_config = device_configs['devices'][device_key]
thread_signals = device_config.get('thread_signals', [])
for signal in thread_signals:
signal_name = signal['name']
signals.append(f"#define {signal_name} (1u << {current_bit})")
current_bit += 1
# 生成信号定义文本
signals_text = '\n'.join(signals) if signals else '/* No signals defined */'
# 替换AUTO GENERATED SIGNALS部分保留其他所有用户区域
pattern = r'/\* AUTO GENERATED SIGNALS BEGIN \*/(.*?)/\* AUTO GENERATED SIGNALS END \*/'
replacement = f'/* AUTO GENERATED SIGNALS BEGIN */\n{signals_text}\n/* AUTO GENERATED SIGNALS END */'
content = re.sub(pattern, replacement, content, flags=re.DOTALL)
# 使用save_with_preserve保存文件以保留用户区域
CodeGenerator.save_with_preserve(dst_path, content)
class DeviceSimple(QWidget):
"""简单设备界面"""
def __init__(self, project_path, device_name, device_config):
super().__init__()
self.project_path = project_path
self.device_name = device_name
self.device_config = device_config
# 添加必要的属性,确保兼容性
self.component_name = device_name # 添加这个属性以兼容现有代码
self._init_ui()
self._load_config()
def _init_ui(self):
layout = QVBoxLayout(self)
# 顶部横向布局:左侧复选框,居中标题
top_layout = QHBoxLayout()
top_layout.setAlignment(Qt.AlignVCenter)
self.generate_checkbox = CheckBox(f"启用 {self.device_name}")
self.generate_checkbox.stateChanged.connect(self._on_checkbox_changed)
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
# 弹性空间
top_layout.addStretch()
title = SubtitleLabel(f"{self.device_name} 配置 ")
title.setAlignment(Qt.AlignHCenter)
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
# 再加一个弹性空间,保证标题居中
top_layout.addStretch()
layout.addLayout(top_layout)
# 功能说明
desc = self.device_config.get('description', '')
if desc:
desc_label = BodyLabel(f"功能说明:{desc}")
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
# 依赖信息
self._add_dependency_info(layout)
# BSP配置区域
self.content_widget = QWidget()
content_layout = QVBoxLayout(self.content_widget)
self._add_bsp_config(content_layout)
layout.addWidget(self.content_widget)
self.content_widget.setEnabled(False)
layout.addStretch()
def _add_dependency_info(self, layout):
"""添加依赖信息显示"""
bsp_deps = self.device_config.get('dependencies', {}).get('bsp', [])
comp_deps = self.device_config.get('dependencies', {}).get('component', [])
if bsp_deps or comp_deps:
deps_text = "依赖: "
if bsp_deps:
deps_text += f"BSP({', '.join(bsp_deps)})"
if comp_deps:
if bsp_deps:
deps_text += ", "
deps_text += f"Component({', '.join(comp_deps)})"
deps_label = BodyLabel(deps_text)
deps_label.setWordWrap(True)
deps_label.setStyleSheet("color: #888888;")
layout.addWidget(deps_label)
def _add_bsp_config(self, layout):
bsp_requirements = self.device_config.get('bsp_requirements', [])
self.bsp_combos = {}
if bsp_requirements:
layout.addWidget(BodyLabel("BSP设备配置:"))
for req in bsp_requirements:
bsp_type = req['type']
var_name = req['var_name']
description = req.get('description', '')
gpio_type = req.get('gpio_type', None) # 新增
req_layout = QHBoxLayout()
label = BodyLabel(f"{bsp_type.upper()}:")
label.setMinimumWidth(80)
req_layout.addWidget(label)
combo = ComboBox()
# 传递gpio_type参数
self._update_bsp_combo(combo, bsp_type, gpio_type)
req_layout.addWidget(combo)
if description:
desc_label = BodyLabel(f"({description})")
desc_label.setStyleSheet("color: #666666; font-size: 12px;")
req_layout.addWidget(desc_label)
req_layout.addStretch()
layout.addLayout(req_layout)
self.bsp_combos[var_name] = combo
def _update_bsp_combo(self, combo, bsp_type, gpio_type=None):
combo.clear()
available_devices = get_available_bsp_devices(self.project_path, bsp_type, gpio_type)
if available_devices:
combo.addItems(available_devices)
else:
combo.addItem(f"未找到可用的{bsp_type.upper()}设备")
combo.setEnabled(False)
def refresh_bsp_combos(self):
"""刷新所有BSP组合框"""
bsp_requirements = self.device_config.get('bsp_requirements', [])
for req in bsp_requirements:
bsp_type = req['type']
var_name = req['var_name']
if var_name in self.bsp_combos:
current_text = self.bsp_combos[var_name].currentText()
self._update_bsp_combo(self.bsp_combos[var_name], bsp_type)
# 尝试恢复之前的选择
index = self.bsp_combos[var_name].findText(current_text)
if index >= 0:
self.bsp_combos[var_name].setCurrentIndex(index)
def _on_checkbox_changed(self, state):
"""处理复选框状态变化"""
self.content_widget.setEnabled(state == 2)
def is_need_generate(self):
"""检查是否需要生成代码"""
return self.generate_checkbox.isChecked()
def get_bsp_config(self):
"""获取BSP配置"""
config = {}
for var_name, combo in self.bsp_combos.items():
if combo.isEnabled():
config[var_name] = combo.currentText()
return config
def _generate_device_code_internal(self):
"""生成设备代码"""
# 检查是否需要生成
if not self.is_need_generate():
# 如果未勾选,检查文件是否已存在,如果存在则跳过
files = self.device_config.get('files', {})
for filename in files.values():
output_path = os.path.join(self.project_path, f"User/device/{filename}")
if os.path.exists(output_path):
return "skipped" # 返回特殊值表示跳过
return "not_needed" # 返回特殊值表示不需要生成
# 获取BSP配置
bsp_config = self.get_bsp_config()
# 复制并修改文件
template_dir = self._get_device_template_dir()
files = self.device_config.get('files', {})
for file_type, filename in files.items():
src_path = os.path.join(template_dir, filename)
dst_path = os.path.join(self.project_path, f"User/device/{filename}")
if os.path.exists(src_path):
# 读取模板文件内容
with open(src_path, 'r', encoding='utf-8') as f:
content = f.read()
# 替换BSP设备名称
for var_name, device_name in bsp_config.items():
content = content.replace(var_name, device_name)
# 根据文件类型选择保存方式
if file_type == 'header':
# 头文件需要保留用户区域
CodeGenerator.save_with_preserve(dst_path, content)
else:
# 源文件直接保存(不需要保留用户区域)
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
with open(dst_path, 'w', encoding='utf-8') as f:
f.write(content)
self._save_config()
return True
def _get_device_template_dir(self):
"""获取设备模板目录"""
return CodeGenerator.get_assets_dir("User_code/device")
def _save_config(self):
"""保存配置"""
config_path = os.path.join(self.project_path, "User/device/device_config.yaml")
config_data = CodeGenerator.load_config(config_path)
config_data[self.device_name.lower()] = {
'enabled': self.is_need_generate(),
'bsp_config': self.get_bsp_config()
}
CodeGenerator.save_config(config_data, config_path)
def _load_config(self):
"""加载配置"""
config_path = os.path.join(self.project_path, "User/device/device_config.yaml")
config_data = CodeGenerator.load_config(config_path)
conf = config_data.get(self.device_name.lower(), {})
if conf.get('enabled', False):
self.generate_checkbox.setChecked(True)
# 恢复BSP配置
bsp_config = conf.get('bsp_config', {})
for var_name, device_name in bsp_config.items():
if var_name in self.bsp_combos:
combo = self.bsp_combos[var_name]
index = combo.findText(device_name)
if index >= 0:
combo.setCurrentIndex(index)
def get_device_page(device_name, project_path):
"""根据设备名返回对应的页面类"""
# 加载设备配置
from app.tools.code_generator import CodeGenerator
device_dir = CodeGenerator.get_assets_dir("User_code/device")
config_path = os.path.join(device_dir, "config.yaml")
device_configs = CodeGenerator.load_device_config(config_path)
devices = device_configs.get('devices', {})
device_key = device_name.lower()
if device_key in devices:
device_config = devices[device_key]
page = DeviceSimple(project_path, device_name, device_config)
else:
# 如果配置中没有找到,返回一个基本的设备页面
basic_config = {
'name': device_name,
'description': f'{device_name}设备',
'files': {'header': f'{device_name.lower()}.h', 'source': f'{device_name.lower()}.c'},
'bsp_requirements': [],
'dependencies': {'bsp': [], 'component': []}
}
page = DeviceSimple(project_path, device_name, basic_config)
# 确保页面有必要的属性
page.device_name = device_name
return page
class device(QWidget):
"""设备管理器"""
def __init__(self, project_path):
super().__init__()
self.project_path = project_path
@staticmethod
def generate_device(project_path, pages):
"""生成所有设备代码"""
success_count = 0
fail_count = 0
skipped_count = 0
fail_list = []
skipped_list = []
enabled_devices = []
# 生成设备代码
for page in pages:
if hasattr(page, "device_name") and hasattr(page, "is_need_generate"):
# 先检查是否有文件存在但未勾选的情况
if not page.is_need_generate():
try:
result = page._generate_device_code_internal()
if result == "skipped":
skipped_count += 1
skipped_list.append(page.device_name)
except Exception:
pass # 忽略未勾选页面的错误
else:
# 勾选的页面,正常处理
try:
result = page._generate_device_code_internal()
if result == "skipped":
skipped_count += 1
skipped_list.append(page.device_name)
elif result:
success_count += 1
enabled_devices.append(page.device_name)
else:
fail_count += 1
fail_list.append(page.device_name)
except Exception as e:
fail_count += 1
fail_list.append(f"{page.device_name} (异常: {e})")
# 生成device.h文件
try:
generate_device_header(project_path, enabled_devices)
success_count += 1
except Exception as e:
fail_count += 1
fail_list.append(f"device.h (异常: {e})")
# 刷新所有页面的BSP组合框选项
for page in pages:
if hasattr(page, 'refresh_bsp_combos'):
try:
page.refresh_bsp_combos()
except Exception as e:
print(f"刷新页面 {getattr(page, 'device_name', 'Unknown')} 的BSP选项失败: {e}")
total_items = success_count + fail_count + skipped_count
msg = f"设备代码生成完成:总共处理 {total_items} 项,成功生成 {success_count} 项,跳过 {skipped_count} 项,失败 {fail_count} 项。"
if skipped_list:
msg += f"\n跳过项(文件已存在且未勾选):\n" + "\n".join(skipped_list)
if fail_list:
msg += "\n失败项:\n" + "\n".join(fail_list)
return msg

634
app/data_interface.py Normal file
View File

@ -0,0 +1,634 @@
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, Dialog,SubtitleLabel,BodyLabel
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox
from qfluentwidgets import CardWidget, LineEdit, SpinBox, CheckBox, TextEdit, PrimaryPushButton, PushButton, InfoBar, DoubleSpinBox
from qfluentwidgets import HeaderCardWidget
from PyQt5.QtWidgets import QScrollArea, QWidget
from qfluentwidgets import theme, Theme
from PyQt5.QtWidgets import QDoubleSpinBox
from .tools.code_task_config import TaskConfigDialog
import os
import requests
import zipfile
import io
import re
import shutil
import yaml
import textwrap
from jinja2 import Template
def preserve_all_user_regions(new_code, old_code):
import re
pattern = re.compile(
r"/\*\s*(USER [A-Z0-9_ ]+)\s*BEGIN\s*\*/(.*?)/\*\s*\1\s*END\s*\*/",
re.DOTALL
)
old_regions = {m.group(1): m.group(2) for m in pattern.finditer(old_code or "")}
def repl(m):
region = m.group(1)
old_content = old_regions.get(region)
if old_content is not None:
return m.group(0).replace(m.group(2), old_content)
return m.group(0)
return pattern.sub(repl, new_code)
def save_with_preserve(path, new_code):
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
old_code = f.read()
new_code = preserve_all_user_regions(new_code, old_code)
with open(path, "w", encoding="utf-8") as f:
f.write(new_code)
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 HomePageWidget(QWidget):
def __init__(self, parent=None, on_choose_project=None, on_update_template=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
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)
if on_choose_project:
self.choose_btn.clicked.connect(on_choose_project)
content_layout.addWidget(self.choose_btn, alignment=Qt.AlignmentFlag.AlignCenter)
self.update_template_btn = PushButton(FluentIcon.SYNC, "更新代码库")
self.update_template_btn.setFixedWidth(200)
if on_update_template:
self.update_template_btn.clicked.connect(on_update_template)
content_layout.addWidget(self.update_template_btn, alignment=Qt.AlignmentFlag.AlignCenter)
content_layout.addSpacing(10)
content_layout.addStretch()
layout.addLayout(content_layout)
layout.addStretch()
class CodeGenWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.project_name_label = StrongBodyLabel()
self.project_path_label = BodyLabel()
self.ioc_file_label = BodyLabel()
self.freertos_label = BodyLabel()
main_layout = QVBoxLayout(self)
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)
info_layout.addWidget(self.back_btn)
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.freertos_task_btn = PushButton("自动生成FreeRTOS任务")
self.freertos_task_btn.setFixedWidth(200)
btn_group.addWidget(self.freertos_task_btn)
self.task_code_btn = PushButton("配置并生成任务代码")
self.task_code_btn.setFixedWidth(200)
btn_group.addWidget(self.task_code_btn)
self.generate_btn = PushButton(FluentIcon.CODE, "生成代码")
self.generate_btn.setFixedWidth(200)
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)
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)
self.home_page = HomePageWidget(
on_choose_project=self.choose_project_folder,
on_update_template=self.update_user_template
)
self.stacked_layout.addWidget(self.home_page)
self.codegen_page = CodeGenWidget()
self.stacked_layout.addWidget(self.codegen_page)
# 事件绑定
self.codegen_page.back_btn.clicked.connect(self.back_to_select)
self.codegen_page.freertos_task_btn.clicked.connect(self.on_freertos_task_btn_clicked)
self.codegen_page.task_code_btn.clicked.connect(self.on_task_code_btn_clicked)
self.codegen_page.generate_btn.clicked.connect(self.generate_code)
self.codegen_page.file_tree.itemChanged.connect(self.on_tree_item_changed)
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.codegen_page.project_name_label.setText(f"项目名称: {self.project_name}")
self.codegen_page.project_path_label.setText(f"项目路径: {self.project_path}")
try:
ioc = IocConfig(self.ioc_file)
self.freertos_enabled = ioc.is_freertos_enabled()
freertos_status = "已启用" if self.freertos_enabled else "未启用"
self.codegen_page.freertos_label.setText(f"FreeRTOS: {freertos_status}")
except Exception as e:
self.codegen_page.freertos_label.setText(f"IOC解析失败: {e}")
self.codegen_page.freertos_task_btn.hide()
self.freertos_enabled = False
self.show_user_code_files()
self.stacked_layout.setCurrentWidget(self.codegen_page)
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.home_page)
def update_user_template(self):
from app.tools.update_code import update_code
def info_callback(parent):
InfoBar.success(
title="更新成功",
content="用户模板已更新到最新版本!",
parent=parent,
duration=2000
)
def error_callback(parent, msg):
InfoBar.error(
title="更新失败",
content=f"用户模板更新失败: {msg}",
parent=parent,
duration=3000
)
update_code(parent=self, info_callback=info_callback, error_callback=error_callback)
def show_user_code_files(self):
from app.tools.code_generator import CodeGenerator
file_tree = self.codegen_page.file_tree
file_tree.clear()
base_dir = CodeGenerator.get_assets_dir("User_code")
user_dir = os.path.join(self.project_path, "User")
sub_dirs = ["bsp", "component", "device", "module"]
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
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()
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
file_tree.setHeaderLabels(["模块名", "描述"])
file_tree.setSelectionMode(file_tree.ExtendedSelection)
file_tree.header().setSectionResizeMode(0, QHeaderView.Interactive)
file_tree.header().setSectionResizeMode(1, QHeaderView.Interactive)
file_tree.setBorderRadius(8)
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, ""])
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)
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)
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.codegen_page.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.codegen_page.file_tree.invisibleRootItem()
for i in range(root.childCount()):
_traverse(root.child(i))
return files
def generate_code(self):
import shutil
from app.tools.code_generator import CodeGenerator
base_dir = CodeGenerator.get_assets_dir("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)
with open(src_c, 'r', encoding='utf-8') as f:
content = f.read()
save_with_preserve(dst_c, content)
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)
with open(src_h, 'r', encoding='utf-8') as f:
content = f.read()
save_with_preserve(dst_h, content)
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 = []
include_line = '#include "task/user_task.h"'
if include_line not in code:
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。")
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 */ 区域,无法插入任务创建代码。")
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):
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
)
self.task_generate_success = True # 添加这一句
except Exception as e:
InfoBar.error(
title="生成失败",
content=f"任务代码生成失败: {e}",
parent=self,
duration=3000
)
def generate_task_code(self, task_list):
from app.tools.code_generator import CodeGenerator
template_dir = CodeGenerator.get_assets_dir("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_path = os.path.join(output_dir, "user_task.h")
new_user_task_h = render_template(user_task_h_tpl, context_h)
save_with_preserve(user_task_h_path, new_user_task_h)
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_path = os.path.join(output_dir, "user_task.c")
user_task_c = render_template(user_task_c_tpl, context_c)
save_with_preserve(user_task_c_path, user_task_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_path = os.path.join(output_dir, "init.c")
init_c = render_template(init_c_tpl, context_init)
save_with_preserve(init_c_path, init_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")
save_with_preserve(task_c_path, code)
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)

90
app/main_window.py Normal file
View File

@ -0,0 +1,90 @@
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 .code_configuration_interface import CodeConfigurationInterface
from .about_interface import AboutInterface
import base64
class MainWindow(FluentWindow):
def __init__(self):
super().__init__()
self.initWindow()
self.initInterface()
self.initNavigation()
# 后台检查更新(不弹窗,只显示通知)
# self.check_updates_in_background()
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)
self.codeConfigurationInterface = CodeConfigurationInterface(self)
def initNavigation(self):
self.addSubInterface(self.homeInterface, FIF.HOME, self.tr('主页'))
# self.addSubInterface(self.dataInterface, FIF.CODE, self.tr('代码生成'))
self.addSubInterface(self.codeConfigurationInterface, 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.themeBtn = NavigationPushButton(FIF.BRUSH, "切换主题", False, self.navigationInterface)
self.themeBtn.clicked.connect(lambda: toggleTheme(lazy=True))
self.navigationInterface.addWidget(
'themeButton',
self.themeBtn,
None,
NavigationItemPosition.BOTTOM
)
def check_updates_in_background(self):
"""后台检查更新"""
try:
# 后台更新检查已移至关于页面手动触发
pass
except Exception as e:
print(f"初始化完成: {e}")
# main_window.py 只需修改关闭事件
def closeEvent(self, e):
# if self.themeListener and self.themeListener.isRunning():
# self.themeListener.terminate()
# self.themeListener.deleteLater()
super().closeEvent(e)

107
app/mini_tool_interface.py Normal file
View File

@ -0,0 +1,107 @@
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())
# 禁止关闭主页
if widget.objectName() == "mainPage":
return
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://qutrobot.top: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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

321
app/tools/analyzing_ioc.py Normal file
View File

@ -0,0 +1,321 @@
class analyzing_ioc:
@staticmethod
def is_freertos_enabled_from_ioc(ioc_path):
"""
检查指定 .ioc 文件是否开启了 FreeRTOS
"""
config = {}
with open(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)
config[key.strip()] = value.strip()
ip_keys = [k for k in config if k.startswith('Mcu.IP')]
for k in ip_keys:
if config[k] == 'FREERTOS':
return True
for k in config:
if k.startswith('FREERTOS.'):
return True
return False
@staticmethod
def get_enabled_i2c_from_ioc(ioc_path):
"""
.ioc文件中获取已启用的I2C列表
返回格式: ['I2C1', 'I2C3']
"""
enabled_i2c = []
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# 检查是否启用了I2C
if key.startswith('Mcu.IP') and value.startswith('I2C'):
# 提取I2C编号如I2C1, I2C2等
i2c_name = value.split('.')[0] if '.' in value else value
if i2c_name not in enabled_i2c:
enabled_i2c.append(i2c_name)
return sorted(enabled_i2c)
@staticmethod
def get_enabled_spi_from_ioc(ioc_path):
"""
获取已启用的SPI列表
返回格式: ['SPI1', 'SPI2']
"""
enabled_spi = []
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
if key.startswith('Mcu.IP') and value.startswith('SPI'):
spi_name = value.split('.')[0] if '.' in value else value
if spi_name not in enabled_spi:
enabled_spi.append(spi_name)
return sorted(enabled_spi)
@staticmethod
def get_enabled_can_from_ioc(ioc_path):
"""
获取已启用的CAN列表
返回格式: ['CAN1', 'CAN2']
"""
enabled_can = []
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
if key.startswith('Mcu.IP') and value.startswith('CAN'):
can_name = value.split('.')[0] if '.' in value else value
if can_name not in enabled_can:
enabled_can.append(can_name)
return sorted(enabled_can)
@staticmethod
def get_enabled_uart_from_ioc(ioc_path):
"""
获取已启用的UART/USART列表
返回格式: ['USART1', 'USART2', 'UART4']
"""
enabled_uart = []
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# 检查是否启用了UART或USART
if key.startswith('Mcu.IP') and (value.startswith('USART') or value.startswith('UART')):
uart_name = value.split('.')[0] if '.' in value else value
if uart_name not in enabled_uart:
enabled_uart.append(uart_name)
return sorted(enabled_uart)
@staticmethod
def get_enabled_gpio_from_ioc(ioc_path):
"""
获取所有带 EXTI 且有 Label GPIO排除其他外设功能的引脚
"""
gpio_list = []
gpio_configs = {}
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# 收集GPIO相关配置
if '.' in key:
pin = key.split('.')[0]
param = key.split('.', 1)[1]
if pin not in gpio_configs:
gpio_configs[pin] = {}
gpio_configs[pin][param] = value
# 定义需要排除的Signal类型
excluded_signals = [
'SPI1_SCK', 'SPI1_MISO', 'SPI1_MOSI', 'SPI2_SCK', 'SPI2_MISO', 'SPI2_MOSI',
'SPI3_SCK', 'SPI3_MISO', 'SPI3_MOSI',
'I2C1_SCL', 'I2C1_SDA', 'I2C2_SCL', 'I2C2_SDA', 'I2C3_SCL', 'I2C3_SDA',
'USART1_TX', 'USART1_RX', 'USART2_TX', 'USART2_RX', 'USART3_TX', 'USART3_RX',
'USART6_TX', 'USART6_RX', 'UART4_TX', 'UART4_RX', 'UART5_TX', 'UART5_RX',
'CAN1_TX', 'CAN1_RX', 'CAN2_TX', 'CAN2_RX',
'USB_OTG_FS_DM', 'USB_OTG_FS_DP', 'USB_OTG_HS_DM', 'USB_OTG_HS_DP',
'SYS_JTMS-SWDIO', 'SYS_JTCK-SWCLK', 'SYS_JTDI', 'SYS_JTDO-SWO',
'RCC_OSC_IN', 'RCC_OSC_OUT',
]
# 处理每个GPIO配置只选择EXTI类型的
for pin, config in gpio_configs.items():
signal = config.get('Signal', '')
# 只处理有Label和EXTI功能的GPIO
if ('GPIO_Label' not in config or
('GPIO_ModeDefaultEXTI' not in config and not signal.startswith('GPXTI'))):
continue
# 排除用于其他外设功能的引脚
if signal in excluded_signals or signal.startswith('S_TIM') or signal.startswith('ADC'):
continue
# 只包含EXTI功能的GPIO
if signal.startswith('GPXTI'):
label = config['GPIO_Label']
gpio_list.append({'pin': pin, 'label': label})
return gpio_list
@staticmethod
def get_all_gpio_from_ioc(ioc_path):
"""
获取所有GPIO配置但排除用于其他外设功能的引脚
只包含纯GPIO功能GPIO_Input, GPIO_Output, GPXTI
"""
gpio_list = []
gpio_configs = {}
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# 收集GPIO相关配置
if '.' in key:
pin = key.split('.')[0]
param = key.split('.', 1)[1]
if pin not in gpio_configs:
gpio_configs[pin] = {}
gpio_configs[pin][param] = value
# 定义需要排除的Signal类型用于其他外设功能的
excluded_signals = [
# SPI相关
'SPI1_SCK', 'SPI1_MISO', 'SPI1_MOSI', 'SPI2_SCK', 'SPI2_MISO', 'SPI2_MOSI',
'SPI3_SCK', 'SPI3_MISO', 'SPI3_MOSI',
# I2C相关
'I2C1_SCL', 'I2C1_SDA', 'I2C2_SCL', 'I2C2_SDA', 'I2C3_SCL', 'I2C3_SDA',
# UART/USART相关
'USART1_TX', 'USART1_RX', 'USART2_TX', 'USART2_RX', 'USART3_TX', 'USART3_RX',
'USART6_TX', 'USART6_RX', 'UART4_TX', 'UART4_RX', 'UART5_TX', 'UART5_RX',
# CAN相关
'CAN1_TX', 'CAN1_RX', 'CAN2_TX', 'CAN2_RX',
# USB相关
'USB_OTG_FS_DM', 'USB_OTG_FS_DP', 'USB_OTG_HS_DM', 'USB_OTG_HS_DP',
# 系统相关
'SYS_JTMS-SWDIO', 'SYS_JTCK-SWCLK', 'SYS_JTDI', 'SYS_JTDO-SWO',
'RCC_OSC_IN', 'RCC_OSC_OUT',
]
# 处理每个GPIO配置
for pin, config in gpio_configs.items():
signal = config.get('Signal', '')
# 只处理有Label的GPIO
if 'GPIO_Label' not in config:
continue
# 排除用于其他外设功能的引脚
if signal in excluded_signals:
continue
# 排除TIM相关的引脚以S_TIM开头的信号
if signal.startswith('S_TIM'):
continue
# 排除ADC相关的引脚
if signal.startswith('ADC'):
continue
# 只包含纯GPIO功能
if signal in ['GPIO_Input', 'GPIO_Output'] or signal.startswith('GPXTI'):
gpio_info = {
'pin': pin,
'label': config['GPIO_Label'],
'has_exti': 'GPIO_ModeDefaultEXTI' in config or signal.startswith('GPXTI'),
'signal': signal,
'mode': config.get('GPIO_ModeDefaultEXTI', ''),
'is_output': signal == 'GPIO_Output',
'is_input': signal == 'GPIO_Input'
}
gpio_list.append(gpio_info)
return gpio_list
@staticmethod
def get_enabled_pwm_from_ioc(ioc_path):
"""
获取已启用的PWM通道列表
返回格式: [{'timer': 'TIM1', 'channel': 'TIM_CHANNEL_1', 'label': 'PWM_MOTOR1'}, ...]
"""
pwm_channels = []
gpio_configs = {}
with open(ioc_path, encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# 收集GPIO相关配置
if '.' in key:
pin = key.split('.')[0]
param = key.split('.', 1)[1]
if pin not in gpio_configs:
gpio_configs[pin] = {}
gpio_configs[pin][param] = value
# 处理每个GPIO配置查找PWM信号
for pin, config in gpio_configs.items():
signal = config.get('Signal', '')
# 检查是否为PWM信号格式如S_TIM1_CH1, S_TIM2_CH3等
if signal.startswith('S_TIM') and '_CH' in signal:
# 解析定时器和通道信息
# 例如S_TIM1_CH1 -> TIM1, CH1
parts = signal.replace('S_', '').split('_')
if len(parts) >= 2:
timer = parts[0] # TIM1
channel_part = parts[1] # CH1
# 转换通道格式CH1 -> TIM_CHANNEL_1
if channel_part.startswith('CH'):
channel_num = channel_part[2:] # 提取数字
channel = f"TIM_CHANNEL_{channel_num}"
# 获取标签
label = config.get('GPIO_Label', f"{timer}_{channel_part}")
pwm_channels.append({
'timer': timer,
'channel': channel,
'label': label,
'pin': pin,
'signal': signal
})
return pwm_channels

462
app/tools/auto_updater.py Normal file
View File

@ -0,0 +1,462 @@
"""
自动更新模块
实现软件的自动更新功能包括下载解压安装等完整流程
"""
import os
import sys
import shutil
import tempfile
import zipfile
import subprocess
import platform
from pathlib import Path
from typing import Optional, Callable
from urllib.parse import urlparse
import requests
from packaging.version import parse as vparse
from PyQt5.QtCore import QThread, pyqtSignal, QObject
# 移除多线程下载器依赖
class UpdaterSignals(QObject):
"""更新器信号类"""
progress_changed = pyqtSignal(int) # 进度变化信号 (0-100)
download_progress = pyqtSignal(int, int, float, float) # 下载进度: 已下载字节, 总字节, 速度MB/s, 剩余时间秒
status_changed = pyqtSignal(str) # 状态变化信号
error_occurred = pyqtSignal(str) # 错误信号
update_completed = pyqtSignal(str) # 更新完成信号,可选包含文件路径
update_cancelled = pyqtSignal() # 更新取消信号
class AutoUpdater(QThread):
"""自动更新器类"""
def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"):
super().__init__()
self.current_version = current_version
self.repo = repo
self.signals = UpdaterSignals()
self.cancelled = False
# 获取当前程序信息
self.is_frozen = getattr(sys, 'frozen', False)
self.app_dir = self._get_app_directory()
self.temp_dir = None
# 多线程下载器
self.downloader = None
def _get_app_directory(self) -> str:
"""获取应用程序目录"""
if self.is_frozen:
# 如果是打包的exe返回exe所在目录
return os.path.dirname(sys.executable)
else:
# 如果是Python脚本返回项目根目录
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
def cancel_update(self):
"""取消更新"""
self.cancelled = True
if self.downloader:
self.downloader.cancel()
self.signals.update_cancelled.emit()
def check_for_updates(self) -> Optional[dict]:
"""检查是否有新版本可用"""
try:
self.signals.status_changed.emit("正在检查更新...")
url = f"https://api.github.com/repos/{self.repo}/releases/latest"
response = requests.get(url, timeout=10)
if response.status_code == 200:
release_data = response.json()
latest_version = release_data["tag_name"].lstrip("v")
if vparse(latest_version) > vparse(self.current_version):
download_url = self._get_download_url(release_data)
asset_size = self._get_asset_size(release_data, download_url) if download_url else 0
return {
'version': latest_version,
'download_url': download_url,
'release_notes': release_data.get('body', ''),
'release_date': release_data.get('published_at', ''),
'asset_name': self._get_asset_name(release_data),
'asset_size': asset_size
}
return None
else:
raise Exception(f"GitHub API请求失败: {response.status_code}")
except Exception as e:
self.signals.error_occurred.emit(f"检查更新失败: {str(e)}")
return None
def _get_download_url(self, release_data: dict) -> Optional[str]:
"""从release数据中获取适合当前平台的下载链接"""
assets = release_data.get('assets', [])
system = platform.system().lower()
# 根据操作系统选择合适的安装包
for asset in assets:
name = asset['name'].lower()
if system == 'windows':
if 'installer' in name and name.endswith('.exe'):
return asset['browser_download_url']
if name.endswith('.exe') or name.endswith('.zip'):
return asset['browser_download_url']
elif system == 'darwin': # macOS
# 优先选择 dmg 或 zip 文件
if name.endswith('.dmg'):
return asset['browser_download_url']
if name.endswith('.zip') and 'macos' in name:
return asset['browser_download_url']
elif system == 'linux':
if name.endswith('.tar.gz') or (name.endswith('.zip') and 'linux' in name):
return asset['browser_download_url']
# 如果没找到特定平台的,在 macOS 上避免选择 .exe 文件
for asset in assets:
name = asset['name'].lower()
if system == 'darwin':
# macOS 优先选择非 exe 文件
if name.endswith('.zip') or name.endswith('.dmg') or name.endswith('.tar.gz'):
return asset['browser_download_url']
else:
if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']):
return asset['browser_download_url']
# 最后才选择 exe 文件(如果没有其他选择)
if system == 'darwin':
for asset in assets:
name = asset['name'].lower()
if name.endswith('.exe'):
return asset['browser_download_url']
return None
def _get_asset_name(self, release_data: dict) -> Optional[str]:
"""获取资源文件名"""
download_url = self._get_download_url(release_data)
if download_url:
return os.path.basename(urlparse(download_url).path)
return None
def _get_asset_size(self, release_data: dict, download_url: str) -> int:
"""获取资源文件大小"""
if not download_url:
return 0
assets = release_data.get('assets', [])
for asset in assets:
if asset.get('browser_download_url') == download_url:
return asset.get('size', 0)
return 0
def download_update(self, download_url: str, filename: str) -> Optional[str]:
"""下载更新文件 - 使用简单的单线程下载"""
try:
self.signals.status_changed.emit("开始下载...")
# 创建临时目录
self.temp_dir = tempfile.mkdtemp(prefix="MRobot_update_")
file_path = os.path.join(self.temp_dir, filename)
print(f"Downloading to: {file_path}")
# 使用简单的requests下载
import requests
headers = {
'User-Agent': 'MRobot-Updater/1.0',
'Accept': '*/*',
}
response = requests.get(download_url, headers=headers, stream=True, timeout=30)
response.raise_for_status()
# 获取文件总大小
total_size = int(response.headers.get('Content-Length', 0))
downloaded_size = 0
# 确保目录存在
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# 下载文件
with open(file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if self.cancelled:
print("Download cancelled by user")
return None
if chunk:
f.write(chunk)
downloaded_size += len(chunk)
# 更新进度
if total_size > 0:
progress = int((downloaded_size / total_size) * 100)
self.signals.progress_changed.emit(progress)
# 发送详细进度信息
speed = 0 # 简化版本不计算速度
remaining = 0
self.signals.download_progress.emit(downloaded_size, total_size, speed, remaining)
# 验证下载的文件
if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
print(f"Download completed successfully: {file_path}")
self.signals.status_changed.emit("下载完成!")
return file_path
else:
raise Exception("下载的文件不存在或为空")
except Exception as e:
error_msg = f"下载失败: {str(e)}"
print(f"Download error: {error_msg}")
self.signals.error_occurred.emit(error_msg)
return None
def _on_download_progress(self, downloaded: int, total: int, speed: float, remaining: float):
"""处理下载进度"""
# 转发详细下载进度
self.signals.download_progress.emit(downloaded, total, speed, remaining)
# 计算总体进度 (下载占80%其他操作占20%)
if total > 0:
download_progress = int((downloaded / total) * 80)
self.signals.progress_changed.emit(download_progress)
def extract_update(self, file_path: str) -> Optional[str]:
"""解压更新文件"""
try:
self.signals.status_changed.emit("正在解压文件...")
self.signals.progress_changed.emit(85)
if not self.temp_dir:
raise Exception("临时目录未初始化")
extract_dir = os.path.join(self.temp_dir, "extracted")
os.makedirs(extract_dir, exist_ok=True)
# 根据文件扩展名选择解压方法
if file_path.endswith('.zip'):
with zipfile.ZipFile(file_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
elif file_path.endswith('.tar.gz'):
import tarfile
with tarfile.open(file_path, 'r:gz') as tar_ref:
tar_ref.extractall(extract_dir)
elif file_path.endswith('.exe'):
# 对于 .exe 文件,在非 Windows 系统上提示手动安装
current_system = platform.system()
if current_system != 'Windows':
self.signals.error_occurred.emit(f"下载的是 Windows 安装程序,当前系统是 {current_system}\n请手动下载适合您系统的版本。")
return None
else:
# Windows 系统直接返回文件路径,由安装函数处理
return file_path
else:
raise Exception(f"不支持的文件格式: {file_path}")
self.signals.progress_changed.emit(90)
self.signals.status_changed.emit("解压完成")
return extract_dir
except Exception as e:
self.signals.error_occurred.emit(f"解压失败: {str(e)}")
return None
def install_update(self, extract_dir: str) -> bool:
"""安装更新"""
try:
self.signals.status_changed.emit("正在安装更新...")
self.signals.progress_changed.emit(95)
if not self.temp_dir:
raise Exception("临时目录未初始化")
# 创建备份目录
backup_dir = os.path.join(self.temp_dir, "backup")
os.makedirs(backup_dir, exist_ok=True)
# 备份当前程序文件
self._backup_current_files(backup_dir)
# 复制新文件
self._copy_update_files(extract_dir)
self.signals.progress_changed.emit(99)
self.signals.status_changed.emit("安装完成")
return True
except Exception as e:
self.signals.error_occurred.emit(f"安装失败: {str(e)}")
# 尝试恢复备份
self._restore_backup(backup_dir)
return False
def _backup_current_files(self, backup_dir: str):
"""备份当前程序文件"""
important_files = ['MRobot.py', 'MRobot.exe', 'app/', 'assets/']
for item in important_files:
src_path = os.path.join(self.app_dir, item)
if os.path.exists(src_path):
dst_path = os.path.join(backup_dir, item)
if os.path.isdir(src_path):
shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
else:
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
shutil.copy2(src_path, dst_path)
def _copy_update_files(self, extract_dir: str):
"""复制更新文件到应用程序目录"""
# 查找解压目录中的主要文件/文件夹
extract_contents = os.listdir(extract_dir)
# 如果解压后只有一个文件夹,进入该文件夹
if len(extract_contents) == 1 and os.path.isdir(os.path.join(extract_dir, extract_contents[0])):
extract_dir = os.path.join(extract_dir, extract_contents[0])
# 复制文件到应用程序目录
for item in os.listdir(extract_dir):
src_path = os.path.join(extract_dir, item)
dst_path = os.path.join(self.app_dir, item)
if os.path.isdir(src_path):
if os.path.exists(dst_path):
shutil.rmtree(dst_path)
shutil.copytree(src_path, dst_path)
else:
shutil.copy2(src_path, dst_path)
def _restore_backup(self, backup_dir: str):
"""恢复备份文件"""
try:
for item in os.listdir(backup_dir):
src_path = os.path.join(backup_dir, item)
dst_path = os.path.join(self.app_dir, item)
if os.path.isdir(src_path):
if os.path.exists(dst_path):
shutil.rmtree(dst_path)
shutil.copytree(src_path, dst_path)
else:
shutil.copy2(src_path, dst_path)
except Exception as e:
print(f"恢复备份失败: {e}")
def restart_application(self):
"""重启应用程序"""
try:
self.signals.status_changed.emit("正在重启应用程序...")
if self.is_frozen:
# 如果是打包的exe
executable = sys.executable
else:
# 如果是Python脚本
executable = sys.executable
script_path = os.path.join(self.app_dir, "MRobot.py")
# 启动新进程
if platform.system() == 'Windows':
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
else:
subprocess.Popen([executable] + ([script_path] if not self.is_frozen else []))
# 退出当前进程
sys.exit(0)
except Exception as e:
self.signals.error_occurred.emit(f"重启失败: {str(e)}")
def cleanup(self):
"""清理临时文件"""
print(f"cleanup() called, temp_dir: {self.temp_dir}") # 调试输出
if self.temp_dir and os.path.exists(self.temp_dir):
try:
print(f"Removing temp directory: {self.temp_dir}") # 调试输出
shutil.rmtree(self.temp_dir)
print("Temp directory removed successfully") # 调试输出
except Exception as e:
print(f"清理临时文件失败: {e}")
else:
print("No temp directory to clean up") # 调试输出
def run(self):
"""执行更新流程"""
try:
self.signals.status_changed.emit("开始更新流程...")
# 检查更新
self.signals.status_changed.emit("正在获取更新信息...")
update_info = self.check_for_updates()
if not update_info or self.cancelled:
self.signals.status_changed.emit("未找到更新信息或已取消")
return
self.signals.status_changed.emit(f"准备下载版本 {update_info['version']}")
# 下载更新
downloaded_file = self.download_update(
update_info['download_url'],
update_info['asset_name']
)
if not downloaded_file or self.cancelled:
self.signals.status_changed.emit("下载失败或已取消")
return
self.signals.status_changed.emit("下载完成!")
self.signals.progress_changed.emit(100)
# 检查是否为exe文件且当前系统非Windows
current_system = platform.system()
print(f"Downloaded file: {downloaded_file}") # 调试输出
print(f"Current system: {current_system}") # 调试输出
if downloaded_file.endswith('.exe') and current_system != 'Windows':
# 直接完成,返回文件路径
print(f"Emitting update_completed signal with file path: {downloaded_file}") # 调试输出
self.signals.update_completed.emit(downloaded_file)
# 不要立即清理,让用户有时间访问文件
print("Skipping cleanup to preserve downloaded file") # 调试输出
return
# 对于其他情况,继续原有流程
self.signals.status_changed.emit("下载完成,开始解压...")
# 解压更新
extract_dir = self.extract_update(downloaded_file)
if not extract_dir or self.cancelled:
self.signals.status_changed.emit("解压失败或已取消")
return
# 安装更新
self.signals.status_changed.emit("开始安装更新...")
if self.install_update(extract_dir) and not self.cancelled:
self.signals.progress_changed.emit(100)
self.signals.status_changed.emit("更新安装完成")
self.signals.update_completed.emit("") # 传递空字符串表示正常安装完成
else:
self.signals.status_changed.emit("安装失败或已取消")
except Exception as e:
error_msg = f"更新过程中发生错误: {str(e)}"
print(f"AutoUpdater error: {error_msg}") # 调试输出
self.signals.error_occurred.emit(error_msg)
finally:
# 对于下载完成的情况,延迟清理临时文件
# 这样用户有时间访问下载的文件
pass # 暂时不在这里清理
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
"""快速检查是否有新版本可用(不下载)"""
updater = AutoUpdater(current_version, repo)
return updater.check_for_updates()

74
app/tools/check_update.py Normal file
View File

@ -0,0 +1,74 @@
import requests
import platform
from packaging.version import parse as vparse
from typing import Optional
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 请求失败")
def check_update_availability(current_version: str, repo: str = "goldenfishs/MRobot") -> Optional[dict]:
"""检查更新并返回详细信息"""
try:
url = f"https://api.github.com/repos/{repo}/releases/latest"
response = requests.get(url, timeout=10)
if response.status_code == 200:
release_data = response.json()
latest_version = release_data["tag_name"].lstrip("v")
if vparse(latest_version) > vparse(current_version):
# 获取适合当前平台的下载链接和文件大小
download_url, asset_size, asset_name = _get_platform_asset(release_data)
return {
'version': latest_version,
'download_url': download_url,
'asset_size': asset_size,
'asset_name': asset_name,
'release_notes': release_data.get('body', ''),
'release_date': release_data.get('published_at', ''),
}
return None
else:
raise Exception(f"GitHub API请求失败: {response.status_code}")
except Exception as e:
raise Exception(f"检查更新失败: {str(e)}")
def _get_platform_asset(release_data: dict) -> tuple:
"""获取适合当前平台的资源文件信息"""
assets = release_data.get('assets', [])
system = platform.system().lower()
# 根据操作系统选择合适的安装包
for asset in assets:
name = asset['name'].lower()
if system == 'windows':
if 'installer' in name and name.endswith('.exe'):
return asset['browser_download_url'], asset.get('size', 0), asset['name']
if name.endswith('.exe') or name.endswith('.zip'):
return asset['browser_download_url'], asset.get('size', 0), asset['name']
elif system == 'darwin': # macOS
if name.endswith('.dmg') or name.endswith('.zip'):
return asset['browser_download_url'], asset.get('size', 0), asset['name']
elif system == 'linux':
if name.endswith('.tar.gz') or name.endswith('.zip'):
return asset['browser_download_url'], asset.get('size', 0), asset['name']
# 如果没找到特定平台的,返回第一个可用文件
for asset in assets:
name = asset['name'].lower()
if any(name.endswith(ext) for ext in ['.zip', '.exe', '.dmg', '.tar.gz']):
return asset['browser_download_url'], asset.get('size', 0), asset['name']
return None, 0, None

557
app/tools/code_generator.py Normal file
View File

@ -0,0 +1,557 @@
import os
import yaml
import shutil
from typing import Dict, List, Tuple, Optional
import sys
import re
import csv
class CodeGenerator:
"""通用代码生成器"""
# 添加类级别的缓存
_assets_dir_cache = None
_assets_dir_initialized = False
_template_dir_logged = False
@staticmethod
def load_template(template_path: str) -> str:
"""加载代码模板"""
try:
with open(template_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
print(f"加载模板失败: {template_path}, 错误: {e}")
return ""
@staticmethod
def replace_auto_generated(content: str, marker: str, replacement: str) -> str:
"""替换自动生成的代码标记"""
marker_line = f"/* {marker} */"
if marker_line in content:
return content.replace(marker_line, replacement)
return content
@staticmethod
def save_file(content: str, file_path: str) -> bool:
"""保存文件"""
try:
dir_path = os.path.dirname(file_path)
if dir_path: # 只有当目录路径不为空时才创建
os.makedirs(dir_path, exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return True
except Exception as e:
print(f"保存文件失败: {file_path}, 错误: {e}")
return False
@staticmethod
def load_config(config_path: str) -> Dict:
"""加载配置文件"""
if os.path.exists(config_path):
try:
with open(config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f) or {}
except Exception as e:
print(f"加载配置失败: {config_path}, 错误: {e}")
return {}
@staticmethod
def save_config(config: Dict, config_path: str) -> bool:
"""保存配置文件"""
try:
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w', encoding='utf-8') as f:
yaml.safe_dump(config, f, allow_unicode=True, default_flow_style=False)
return True
except Exception as e:
print(f"保存配置失败: {config_path}, 错误: {e}")
return False
@staticmethod
def get_template_dir():
"""获取模板目录路径,兼容打包环境"""
# 使用统一的get_assets_dir方法来获取路径
template_dir = CodeGenerator.get_assets_dir("User_code/bsp")
# 只在第一次或出现问题时打印日志
if not hasattr(CodeGenerator, '_template_dir_logged'):
print(f"模板目录路径: {template_dir}")
CodeGenerator._template_dir_logged = True
if template_dir and not os.path.exists(template_dir):
print(f"警告:模板目录不存在: {template_dir}")
return template_dir
@staticmethod
def get_assets_dir(sub_path=""):
"""获取assets目录路径兼容打包环境
Args:
sub_path: 子路径 "User_code/component" "User_code/device"
Returns:
str: 完整的assets路径
"""
# 使用缓存机制,避免重复计算和日志输出
if not CodeGenerator._assets_dir_initialized:
assets_dir = ""
if getattr(sys, 'frozen', False):
# 打包后的环境
print("检测到打包环境")
# 优先使用sys._MEIPASSPyInstaller的临时解包目录
if hasattr(sys, '_MEIPASS'):
base_path = getattr(sys, '_MEIPASS')
assets_dir = os.path.join(base_path, "assets")
print(f"使用PyInstaller临时目录: {assets_dir}")
else:
# 后备方案:使用可执行文件所在目录
exe_dir = os.path.dirname(sys.executable)
assets_dir = os.path.join(exe_dir, "assets")
print(f"使用可执行文件目录: {assets_dir}")
# 如果都不存在,尝试其他可能的位置
if not os.path.exists(assets_dir):
# 尝试从当前工作目录查找
cwd_assets = os.path.join(os.getcwd(), "assets")
if os.path.exists(cwd_assets):
assets_dir = cwd_assets
print(f"从工作目录找到assets: {assets_dir}")
else:
print(f"警告无法找到assets目录使用默认路径: {assets_dir}")
else:
# 开发环境
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上查找直到找到MRobot目录或到达根目录
while current_dir != os.path.dirname(current_dir): # 防止无限循环
if os.path.basename(current_dir) == 'MRobot':
break
parent = os.path.dirname(current_dir)
if parent == current_dir: # 已到达根目录
break
current_dir = parent
assets_dir = os.path.join(current_dir, "assets")
print(f"开发环境:使用路径: {assets_dir}")
# 如果找不到,尝试从当前工作目录
if not os.path.exists(assets_dir):
cwd_assets = os.path.join(os.getcwd(), "assets")
if os.path.exists(cwd_assets):
assets_dir = cwd_assets
print(f"开发环境后备:使用工作目录: {assets_dir}")
# 缓存基础assets目录
CodeGenerator._assets_dir_cache = assets_dir
CodeGenerator._assets_dir_initialized = True
else:
# 使用缓存的路径
assets_dir = CodeGenerator._assets_dir_cache or ""
# 构建完整路径
if sub_path:
full_path = os.path.join(assets_dir, sub_path)
else:
full_path = assets_dir
# 规范化路径(处理路径分隔符)
full_path = os.path.normpath(full_path)
# 只在第一次访问某个路径时检查并警告
safe_sub_path = sub_path.replace('/', '_').replace('\\', '_')
warning_key = f"_warned_{safe_sub_path}"
if full_path and not os.path.exists(full_path) and not hasattr(CodeGenerator, warning_key):
print(f"警告:资源目录不存在: {full_path}")
setattr(CodeGenerator, warning_key, True)
return full_path
@staticmethod
def preserve_all_user_regions(new_code: str, old_code: str) -> str:
"""保留用户定义的代码区域
在新代码中保留旧代码中所有用户定义的区域
用户区域使用如下格式标记
/* USER REGION_NAME BEGIN */
用户代码...
/* USER REGION_NAME END */
支持的格式示例
- /* USER REFEREE BEGIN */ ... /* USER REFEREE END */
- /* USER CODE BEGIN */ ... /* USER CODE END */
- /* USER CUSTOM_NAME BEGIN */ ... /* USER CUSTOM_NAME END */
Args:
new_code: 新的代码内容
old_code: 旧的代码内容
Returns:
str: 保留了用户区域的新代码
"""
if not old_code:
return new_code
# 更灵活的正则表达式,支持更多格式的用户区域标记
# 匹配 /* USER 任意字符 BEGIN */ ... /* USER 相同字符 END */
pattern = re.compile(
r"/\*\s*USER\s+([A-Za-z0-9_\s]+?)\s+BEGIN\s*\*/(.*?)/\*\s*USER\s+\1\s+END\s*\*/",
re.DOTALL | re.IGNORECASE
)
# 提取旧代码中的所有用户区域
old_regions = {}
for match in pattern.finditer(old_code):
region_name = match.group(1).strip()
region_content = match.group(2)
old_regions[region_name.upper()] = region_content
# 替换函数
def repl(match):
region_name = match.group(1).strip().upper()
current_content = match.group(2)
old_content = old_regions.get(region_name)
if old_content is not None:
# 直接替换中间的内容,保持原有的注释标记不变
return match.group(0).replace(current_content, old_content)
return match.group(0)
# 应用替换
result = pattern.sub(repl, new_code)
# 调试信息:记录找到的用户区域
if old_regions:
print(f"保留了 {len(old_regions)} 个用户区域: {list(old_regions.keys())}")
return result
@staticmethod
def save_with_preserve(file_path: str, new_code: str) -> bool:
"""保存文件并保留用户代码区域
如果文件已存在会先读取旧文件内容保留其中的用户代码区域
然后将新代码与保留的用户区域合并后保存
Args:
file_path: 文件路径
new_code: 新的代码内容
Returns:
bool: 保存是否成功
"""
try:
# 如果文件已存在,先读取旧内容
old_code = ""
if os.path.exists(file_path):
with open(file_path, "r", encoding="utf-8") as f:
old_code = f.read()
# 保留用户区域
final_code = CodeGenerator.preserve_all_user_regions(new_code, old_code)
# 确保目录存在
dir_path = os.path.dirname(file_path)
if dir_path: # 只有当目录路径不为空时才创建
os.makedirs(dir_path, exist_ok=True)
# 保存文件
with open(file_path, "w", encoding="utf-8") as f:
f.write(final_code)
return True
except Exception as e:
print(f"保存文件失败: {file_path}, 错误: {e}")
return False
@staticmethod
def load_descriptions(csv_path: str) -> Dict[str, str]:
"""从CSV文件加载组件或设备的描述信息
CSV格式第一列为组件/设备名称第二列为描述
Args:
csv_path: CSV文件路径
Returns:
Dict[str, str]: 名称到描述的映射字典
"""
descriptions = {}
if os.path.exists(csv_path):
try:
with open(csv_path, encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if len(row) >= 2:
key, desc = row[0].strip(), row[1].strip()
descriptions[key.lower()] = desc
except Exception as e:
print(f"加载描述文件失败: {csv_path}, 错误: {e}")
return descriptions
@staticmethod
def load_dependencies(csv_path: str) -> Dict[str, List[str]]:
"""从CSV文件加载组件依赖关系
CSV格式第一列为组件名后续列为依赖的组件
Args:
csv_path: CSV文件路径
Returns:
Dict[str, List[str]]: 组件名到依赖列表的映射字典
"""
dependencies = {}
if os.path.exists(csv_path):
try:
with open(csv_path, encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if len(row) >= 2:
component = row[0].strip()
deps = [dep.strip() for dep in row[1:] if dep.strip()]
dependencies[component] = deps
except Exception as e:
print(f"加载依赖文件失败: {csv_path}, 错误: {e}")
return dependencies
@staticmethod
def load_device_config(config_path: str) -> Dict:
"""加载设备配置文件
Args:
config_path: YAML配置文件路径
Returns:
Dict: 配置数据字典
"""
return CodeGenerator.load_config(config_path)
@staticmethod
def copy_dependency_file(src_path: str, dst_path: str) -> bool:
"""复制依赖文件
Args:
src_path: 源文件路径
dst_path: 目标文件路径
Returns:
bool: 复制是否成功
"""
try:
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
shutil.copy2(src_path, dst_path)
return True
except Exception as e:
print(f"复制文件失败: {src_path} -> {dst_path}, 错误: {e}")
return False
@staticmethod
def generate_code_from_template(template_path: str, output_path: str,
replacements: Optional[Dict[str, str]] = None,
preserve_user_code: bool = True) -> bool:
"""从模板生成代码文件
Args:
template_path: 模板文件路径
output_path: 输出文件路径
replacements: 要替换的标记字典 {'MARKER': 'replacement_content'}
preserve_user_code: 是否保留用户代码区域
Returns:
bool: 生成是否成功
"""
try:
# 加载模板
template_content = CodeGenerator.load_template(template_path)
if not template_content:
print(f"模板文件不存在或为空: {template_path}")
return False
# 执行替换
if replacements:
for marker, replacement in replacements.items():
template_content = CodeGenerator.replace_auto_generated(
template_content, marker, replacement
)
# 保存文件
if preserve_user_code:
return CodeGenerator.save_with_preserve(output_path, template_content)
else:
return CodeGenerator.save_file(template_content, output_path)
except Exception as e:
print(f"从模板生成代码失败: {template_path} -> {output_path}, 错误: {e}")
return False
@staticmethod
def read_file_content(file_path: str) -> Optional[str]:
"""读取文件内容
Args:
file_path: 文件路径
Returns:
str: 文件内容如果失败返回None
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
print(f"读取文件失败: {file_path}, 错误: {e}")
return None
@staticmethod
def write_file_content(file_path: str, content: str) -> bool:
"""写入文件内容
Args:
file_path: 文件路径
content: 文件内容
Returns:
bool: 写入是否成功
"""
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return True
except Exception as e:
print(f"写入文件失败: {file_path}, 错误: {e}")
return False
@staticmethod
def update_file_with_pattern(file_path: str, pattern: str, replacement: str,
use_regex: bool = True) -> bool:
"""更新文件中匹配模式的内容
Args:
file_path: 文件路径
pattern: 要匹配的模式
replacement: 替换内容
use_regex: 是否使用正则表达式
Returns:
bool: 更新是否成功
"""
try:
content = CodeGenerator.read_file_content(file_path)
if content is None:
return False
if use_regex:
import re
updated_content = re.sub(pattern, replacement, content, flags=re.DOTALL)
else:
updated_content = content.replace(pattern, replacement)
return CodeGenerator.write_file_content(file_path, updated_content)
except Exception as e:
print(f"更新文件失败: {file_path}, 错误: {e}")
return False
@staticmethod
def replace_multiple_markers(content: str, replacements: Dict[str, str]) -> str:
"""批量替换内容中的多个标记
Args:
content: 要处理的内容
replacements: 替换字典 {'MARKER1': 'content1', 'MARKER2': 'content2'}
Returns:
str: 替换后的内容
"""
result = content
for marker, replacement in replacements.items():
result = CodeGenerator.replace_auto_generated(result, marker, replacement)
return result
@staticmethod
def extract_user_regions(code: str) -> Dict[str, str]:
"""从代码中提取所有用户区域
支持提取各种格式的用户区域
- /* USER REFEREE BEGIN */ ... /* USER REFEREE END */
- /* USER CODE BEGIN */ ... /* USER CODE END */
- /* USER CUSTOM_NAME BEGIN */ ... /* USER CUSTOM_NAME END */
Args:
code: 要提取的代码内容
Returns:
Dict[str, str]: 区域名称到区域内容的映射
"""
if not code:
return {}
# 使用与preserve_all_user_regions相同的正则表达式
pattern = re.compile(
r"/\*\s*USER\s+([A-Za-z0-9_\s]+?)\s+BEGIN\s*\*/(.*?)/\*\s*USER\s+\1\s+END\s*\*/",
re.DOTALL | re.IGNORECASE
)
regions = {}
for match in pattern.finditer(code):
region_name = match.group(1).strip().upper()
region_content = match.group(2)
regions[region_name] = region_content
return regions
@staticmethod
def debug_user_regions(new_code: str, old_code: str, verbose: bool = False) -> Dict[str, Dict[str, str]]:
"""调试用户区域,显示新旧内容的对比
Args:
new_code: 新的代码内容
old_code: 旧的代码内容
verbose: 是否输出详细信息
Returns:
Dict: 包含所有用户区域信息的字典
"""
if verbose:
print("=== 用户区域调试信息 ===")
new_regions = CodeGenerator.extract_user_regions(new_code)
old_regions = CodeGenerator.extract_user_regions(old_code)
all_region_names = set(new_regions.keys()) | set(old_regions.keys())
result = {}
for region_name in sorted(all_region_names):
new_content = new_regions.get(region_name, "")
old_content = old_regions.get(region_name, "")
result[region_name] = {
"new_content": new_content,
"old_content": old_content,
"will_preserve": bool(old_content),
"exists_in_new": region_name in new_regions,
"exists_in_old": region_name in old_regions
}
if verbose:
status = "保留旧内容" if old_content else "使用新内容"
print(f"\n区域: {region_name} ({status})")
print(f" 新模板中存在: {'' if region_name in new_regions else ''}")
print(f" 旧文件中存在: {'' if region_name in old_regions else ''}")
if new_content.strip():
print(f" 新内容预览: {repr(new_content.strip()[:50])}...")
if old_content.strip():
print(f" 旧内容预览: {repr(old_content.strip()[:50])}...")
if verbose:
print(f"\n总计: {len(all_region_names)} 个用户区域")
preserve_count = sum(1 for info in result.values() if info["will_preserve"])
print(f"将保留: {preserve_count} 个区域的旧内容")
return result

View File

@ -0,0 +1,306 @@
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QWidget, QScrollArea
from qfluentwidgets import (
BodyLabel, TitleLabel, HorizontalSeparator, PushButton, PrimaryPushButton,
LineEdit, SpinBox, DoubleSpinBox, CheckBox, TextEdit
)
from qfluentwidgets import theme, Theme
import yaml
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, Dialog,SubtitleLabel,BodyLabel
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QSpinBox, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox
from qfluentwidgets import CardWidget, LineEdit, SpinBox, CheckBox, TextEdit, PrimaryPushButton, PushButton, InfoBar, DoubleSpinBox
from qfluentwidgets import HeaderCardWidget
from PyQt5.QtWidgets import QScrollArea, QWidget
from qfluentwidgets import theme, Theme
from PyQt5.QtWidgets import QDoubleSpinBox
import os
class TaskConfigDialog(QDialog):
def __init__(self, parent=None, config_path=None):
super().__init__(parent)
self.setWindowTitle("任务配置")
self.resize(900, 520)
# 设置背景色跟随主题
if theme() == Theme.DARK:
self.setStyleSheet("background-color: #232323;")
else:
self.setStyleSheet("background-color: #f7f9fc;")
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(16, 16, 16, 16)
main_layout.setSpacing(12)
# 顶部横向分栏
self.top_layout = QHBoxLayout()
self.top_layout.setSpacing(16)
# ----------- 左侧任务按钮区 -----------
self.left_widget = QWidget()
self.left_layout = QVBoxLayout(self.left_widget)
self.left_layout.setContentsMargins(0, 0, 0, 0)
self.left_layout.setSpacing(8)
self.task_list_label = BodyLabel("任务列表")
# self.left_layout.addWidget(self.task_list_label)
# 添加任务列表居中
self.task_list_label.setAlignment(Qt.AlignCenter)
self.left_layout.addWidget(self.task_list_label, alignment=Qt.AlignCenter)
# 添加一个水平分割线
self.left_layout.addWidget(HorizontalSeparator())
# 任务按钮区
self.task_btn_area = QScrollArea()
self.task_btn_area.setWidgetResizable(True)
self.task_btn_area.setFrameShape(QScrollArea.NoFrame)
self.task_btn_container = QWidget()
self.task_btn_layout = QVBoxLayout(self.task_btn_container)
self.task_btn_layout.setContentsMargins(0, 0, 0, 0)
self.task_btn_layout.setSpacing(4)
self.task_btn_layout.addStretch()
self.task_btn_area.setWidget(self.task_btn_container)
self.left_layout.addWidget(self.task_btn_area, stretch=1)
self.left_widget.setFixedWidth(180)
self.top_layout.addWidget(self.left_widget, stretch=0)
# ----------- 左侧任务按钮区 END -----------
main_layout.addLayout(self.top_layout, stretch=1)
# 下方按钮区
btn_layout = QHBoxLayout()
# 左下角:添加/删除任务
self.add_btn = PrimaryPushButton("创建新任务")
self.add_btn.setAutoDefault(False) # 禁止回车触发
self.add_btn.setDefault(False)
self.del_btn = PushButton("删除当前任务")
self.del_btn.setAutoDefault(False) # 禁止回车触发
self.del_btn.setDefault(False)
self.add_btn.clicked.connect(self.add_task)
self.del_btn.clicked.connect(self.delete_current_task)
btn_layout.addWidget(self.add_btn)
btn_layout.addWidget(self.del_btn)
btn_layout.addStretch() # 添加/删除靠左stretch在中间
# 右下角:生成/取消
self.ok_btn = PrimaryPushButton("生成任务")
self.ok_btn.setAutoDefault(False) # 允许回车触发
self.ok_btn.setDefault(False) # 设置为默认按钮
self.cancel_btn = PushButton("取消")
self.cancel_btn.setAutoDefault(False) # 禁止回车触发
self.cancel_btn.setDefault(False)
btn_layout.addWidget(self.ok_btn)
btn_layout.addWidget(self.cancel_btn)
main_layout.addLayout(btn_layout)
self.ok_btn.clicked.connect(self.accept)
self.cancel_btn.clicked.connect(self.reject)
self.tasks = []
self.current_index = -1
if config_path and os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
tasks = yaml.safe_load(f)
if tasks:
for t in tasks:
self.tasks.append(self._make_task_obj(t))
except Exception:
pass
# 允许没有任何任务
self.current_index = 0 if self.tasks else -1
self.refresh_task_btns()
if self.tasks:
self.show_task_form(self.tasks[self.current_index])
else:
self.show_task_form(None)
def refresh_task_btns(self):
# 清空旧按钮
while self.task_btn_layout.count():
item = self.task_btn_layout.takeAt(0)
w = item.widget()
if w:
w.deleteLater()
# 重新添加按钮
for idx, t in enumerate(self.tasks):
btn = PushButton(t["name"])
btn.setCheckable(True)
btn.setChecked(idx == self.current_index)
btn.clicked.connect(lambda checked, i=idx: self.select_task(i))
self.task_btn_layout.addWidget(btn)
self.task_btn_layout.addStretch()
def add_task(self):
self.save_form()
new_idx = len(self.tasks)
self.tasks.append(self._make_task_obj({"name": f"Task{new_idx+1}"}))
self.current_index = new_idx
self.refresh_task_btns()
self.show_task_form(self.tasks[self.current_index])
def delete_current_task(self):
if self.current_index < 0 or not self.tasks:
return
del self.tasks[self.current_index]
if not self.tasks:
self.current_index = -1
self.refresh_task_btns()
self.show_task_form(None)
return
if self.current_index >= len(self.tasks):
self.current_index = len(self.tasks) - 1
self.refresh_task_btns()
self.show_task_form(self.tasks[self.current_index])
def select_task(self, idx):
self.save_form()
self.current_index = idx
self.refresh_task_btns()
self.show_task_form(self.tasks[idx])
def show_task_form(self, task):
# 先移除旧的 form_widget
if hasattr(self, "form_widget") and self.form_widget is not None:
self.top_layout.removeWidget(self.form_widget)
self.form_widget.deleteLater()
self.form_widget = None
# 新建 form_widget 和 form_layout
self.form_widget = QWidget()
self.form_layout = QVBoxLayout(self.form_widget)
self.form_layout.setContentsMargins(0, 0, 0, 0)
self.form_layout.setSpacing(12)
# 添加到右侧
self.top_layout.addWidget(self.form_widget, stretch=1)
if not task:
label = TitleLabel("暂无任务,请点击下方“添加任务”。")
label.setAlignment(Qt.AlignCenter)
self.form_layout.addStretch()
self.form_layout.addWidget(label)
self.form_layout.addStretch()
return
# 任务名称
row1 = QHBoxLayout()
label_name = BodyLabel("任务名称")
self.name_edit = LineEdit()
self.name_edit.setText(task["name"])
self.name_edit.setPlaceholderText("任务名称")
# 新增:名称编辑完成后刷新按钮
self.name_edit.editingFinished.connect(self.on_name_edit_finished)
row1.addWidget(label_name)
row1.addWidget(self.name_edit)
self.form_layout.addLayout(row1)
# 频率
row2 = QHBoxLayout()
label_freq = BodyLabel("任务运行频率")
self.freq_spin = DoubleSpinBox()
self.freq_spin.setRange(0, 10000)
self.freq_spin.setDecimals(3)
self.freq_spin.setSingleStep(1)
self.freq_spin.setSuffix(" Hz")
self.freq_spin.setValue(float(task.get("frequency", 500)))
row2.addWidget(label_freq)
row2.addWidget(self.freq_spin)
self.form_layout.addLayout(row2)
# 延迟
row3 = QHBoxLayout()
label_delay = BodyLabel("初始化延时")
self.delay_spin = SpinBox()
self.delay_spin.setRange(0, 10000)
self.delay_spin.setSuffix(" ms")
self.delay_spin.setValue(task.get("delay", 0))
row3.addWidget(label_delay)
row3.addWidget(self.delay_spin)
self.form_layout.addLayout(row3)
# 堆栈
row4 = QHBoxLayout()
label_stack = BodyLabel("堆栈大小")
self.stack_spin = SpinBox()
self.stack_spin.setRange(128, 8192)
self.stack_spin.setSingleStep(128)
self.stack_spin.setSuffix(" Byte") # 添加单位
self.stack_spin.setValue(task.get("stack", 256))
row4.addWidget(label_stack)
row4.addWidget(self.stack_spin)
self.form_layout.addLayout(row4)
# 频率控制
row5 = QHBoxLayout()
self.freq_ctrl = CheckBox("启用默认频率控制")
self.freq_ctrl.setChecked(task.get("freq_control", True))
row5.addWidget(self.freq_ctrl)
self.form_layout.addLayout(row5)
# 描述
label_desc = BodyLabel("任务描述")
self.desc_edit = TextEdit()
self.desc_edit.setText(task.get("description", ""))
self.desc_edit.setPlaceholderText("任务描述")
self.form_layout.addWidget(label_desc)
self.form_layout.addWidget(self.desc_edit)
self.form_layout.addStretch()
def on_name_edit_finished(self):
# 保存当前表单内容
self.save_form()
# 刷新左侧按钮名称
self.refresh_task_btns()
def _make_task_obj(self, task=None):
return {
"name": task["name"] if task else f"Task1",
"frequency": task.get("frequency", 500) if task else 500,
"delay": task.get("delay", 0) if task else 0,
"stack": task.get("stack", 256) if task else 256,
"description": task.get("description", "") if task else "",
"freq_control": task.get("freq_control", True) if task else True,
}
def save_form(self):
if self.current_index < 0 or self.current_index >= len(self.tasks):
return
t = self.tasks[self.current_index]
t["name"] = self.name_edit.text().strip()
t["frequency"] = float(self.freq_spin.value()) # 支持小数
t["delay"] = self.delay_spin.value()
t["stack"] = self.stack_spin.value()
t["description"] = self.desc_edit.toPlainText().strip()
t["freq_control"] = self.freq_ctrl.isChecked()
def get_tasks(self):
self.save_form()
tasks = []
for idx, t in enumerate(self.tasks):
name = t["name"].strip()
freq = t["frequency"]
delay = t["delay"]
stack = t["stack"]
desc = t["description"].strip()
freq_ctrl = t["freq_control"]
if stack < 128 or (stack & (stack - 1)) != 0 or stack % 128 != 0:
raise ValueError(f"{idx+1}个任务“{name}”的堆栈大小必须为128、256、512、1024等128*2^n")
task = {
"name": name,
"function": f"Task_{name}",
"delay": delay,
"stack": stack,
"description": desc,
"freq_control": freq_ctrl
}
if freq_ctrl:
task["frequency"] = freq
tasks.append(task)
return tasks

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,34 @@
"""
更新检查线程
避免阻塞UI界面的更新检查
"""
from PyQt5.QtCore import QThread, pyqtSignal
from app.tools.check_update import check_update_availability
class UpdateCheckThread(QThread):
"""更新检查线程"""
# 信号定义
update_found = pyqtSignal(dict) # 发现更新
no_update = pyqtSignal() # 无更新
error_occurred = pyqtSignal(str) # 检查出错
def __init__(self, current_version: str, repo: str = "goldenfishs/MRobot"):
super().__init__()
self.current_version = current_version
self.repo = repo
def run(self):
"""执行更新检查"""
try:
update_info = check_update_availability(self.current_version, self.repo)
if update_info:
self.update_found.emit(update_info)
else:
self.no_update.emit()
except Exception as e:
self.error_occurred.emit(str(e))

View File

@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
自动更新CMakeLists.txt中的User源文件列表
这个脚本会扫描User目录下的所有.c文件并自动更新CMakeLists.txt中的target_sources部分
"""
import os
import re
from pathlib import Path
def find_user_c_files(user_dir):
"""查找User目录下的所有.c文件"""
c_files = []
user_path = Path(user_dir)
if not user_path.exists():
print(f"错误: User目录不存在: {user_dir}")
return []
# 递归查找所有.c文件
for c_file in user_path.rglob("*.c"):
# 获取相对于项目根目录的路径
relative_path = c_file.relative_to(user_path.parent)
# 使用正斜杠确保跨平台兼容性,避免转义字符问题
c_files.append(str(relative_path).replace('\\', '/'))
# 按目录和文件名排序
c_files.sort()
return c_files
def update_cmake_sources(cmake_file, c_files):
"""更新CMakeLists.txt中的源文件列表"""
if not os.path.exists(cmake_file):
print(f"错误: CMakeLists.txt文件不存在: {cmake_file}")
return False
# 读取CMakeLists.txt内容
with open(cmake_file, 'r', encoding='utf-8') as f:
content = f.read()
# 构建新的源文件列表
sources_section = "# Add sources to executable\ntarget_sources(${CMAKE_PROJECT_NAME} PRIVATE\n"
sources_section += " # Add user sources here\n"
# 按目录分组
current_dir = ""
for c_file in c_files:
# 确保路径使用正斜杠,避免转义字符问题
normalized_file = c_file.replace('\\', '/')
file_dir = str(Path(normalized_file).parent).replace('\\', '/')
if file_dir != current_dir:
if current_dir: # 不是第一个目录,添加空行
sources_section += "\n"
sources_section += f" # {file_dir} sources\n"
current_dir = file_dir
sources_section += f" {normalized_file}\n"
sources_section += ")"
# 使用原始字符串避免转义问题,并使用更精确的正则表达式
pattern = r'# Add sources to executable\s*\ntarget_sources\(\$\{CMAKE_PROJECT_NAME\}\s+PRIVATE\s*\n(?:.*?\n)*?\)'
try:
if re.search(pattern, content, re.DOTALL | re.MULTILINE):
new_content = re.sub(pattern, sources_section, content, flags=re.DOTALL | re.MULTILINE)
# 写回文件
with open(cmake_file, 'w', encoding='utf-8') as f:
f.write(new_content)
print("✅ 成功更新CMakeLists.txt中的源文件列表")
return True
else:
print("❌ 错误: 在CMakeLists.txt中找不到target_sources部分")
return False
except re.error as e:
print(f"❌ 正则表达式错误: {e}")
return False
def update_cmake_includes(cmake_file, user_dir):
"""确保CMakeLists.txt中的include路径包含User"""
if not os.path.exists(cmake_file):
print(f"错误: CMakeLists.txt文件不存在: {cmake_file}")
return False
with open(cmake_file, 'r', encoding='utf-8') as f:
content = f.read()
# 构建新的include部分
include_section = (
"# Add include paths\n"
"target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE\n"
" # Add user defined include paths\n"
" User\n"
")"
)
# 使用更安全的正则表达式匹配include部分
pattern = r'# Add include paths\s*\ntarget_include_directories\(\$\{CMAKE_PROJECT_NAME\}\s+PRIVATE\s*\n(?:.*?\n)*?\)'
try:
if re.search(pattern, content, re.DOTALL | re.MULTILINE):
new_content = re.sub(pattern, include_section, content, flags=re.DOTALL | re.MULTILINE)
with open(cmake_file, 'w', encoding='utf-8') as f:
f.write(new_content)
print("✅ 成功更新CMakeLists.txt中的include路径")
return True
else:
print("❌ 错误: 在CMakeLists.txt中找不到target_include_directories部分")
return False
except re.error as e:
print(f"❌ 正则表达式错误: {e}")
return False
def main():
"""主函数"""
script_dir = Path(__file__).parent
project_root = script_dir
user_dir = project_root / "User"
cmake_file = project_root / "CMakeLists.txt"
print("🔍 正在扫描User目录下的.c文件...")
c_files = find_user_c_files(user_dir)
if not c_files:
print("⚠️ 警告: 在User目录下没有找到.c文件")
return
print(f"📁 找到 {len(c_files)} 个.c文件:")
for c_file in c_files:
print(f" - {c_file}")
print(f"\n📝 正在更新 {cmake_file}...")
success = update_cmake_sources(cmake_file, c_files)
if success:
print("🎉 更新完成!现在可以重新编译项目了。")
else:
print("💥 更新失败,请检查错误信息。")
if __name__ == "__main__":
main()

114
app/tools/update_code.py Normal file
View File

@ -0,0 +1,114 @@
import os
import sys
import requests
import zipfile
import io
import shutil
import tempfile
import time
def update_code(parent=None, info_callback=None, error_callback=None):
url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
# 使用与CodeGenerator.get_assets_dir相同的逻辑确定assets目录
if getattr(sys, 'frozen', False):
# 打包后的环境 - 使用可执行文件所在目录
exe_dir = os.path.dirname(sys.executable)
assets_dir = os.path.join(exe_dir, "assets")
print(f"更新代码:打包环境,使用路径: {assets_dir}")
# 如果exe_dir/assets不存在尝试使用相对路径作为后备
if not os.path.exists(assets_dir):
assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../assets")
print(f"更新代码:后备路径: {assets_dir}")
else:
# 开发环境
assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../assets")
print(f"更新代码:开发环境,使用路径: {assets_dir}")
local_dir = os.path.join(assets_dir, "User_code")
print(f"更新代码:最终目标目录: {local_dir}")
try:
# 下载远程代码库
resp = requests.get(url, timeout=30)
resp.raise_for_status()
# 创建临时目录进行操作
with tempfile.TemporaryDirectory() as temp_dir:
# 解压到临时目录
z = zipfile.ZipFile(io.BytesIO(resp.content))
extract_path = os.path.join(temp_dir, "extracted")
z.extractall(extract_path)
# 获取解压后的根目录
extracted_items = os.listdir(extract_path)
if not extracted_items:
raise Exception("下载的压缩包为空")
source_root = os.path.join(extract_path, extracted_items[0])
# 确保本地目录的父目录存在
parent_dir = os.path.dirname(local_dir)
if not os.path.exists(parent_dir):
os.makedirs(parent_dir, exist_ok=True)
# 创建备份(如果原目录存在)
backup_dir = None
if os.path.exists(local_dir):
backup_dir = f"{local_dir}_backup_{int(time.time())}"
try:
shutil.move(local_dir, backup_dir)
except Exception as e:
# 如果移动失败,尝试强制删除
shutil.rmtree(local_dir, ignore_errors=True)
try:
# 复制新文件到目标位置
shutil.copytree(source_root, local_dir)
# 验证复制是否成功
if not os.path.exists(local_dir):
raise Exception("复制失败,目标目录不存在")
# 设置正确的文件权限,确保文件可以被正常访问和修改
for root, dirs, files in os.walk(local_dir):
# 设置目录权限为755 (rwxr-xr-x)
for dir_name in dirs:
dir_path = os.path.join(root, dir_name)
try:
os.chmod(dir_path, 0o755)
except:
pass
# 设置文件权限为644 (rw-r--r--)
for file_name in files:
file_path = os.path.join(root, file_name)
try:
os.chmod(file_path, 0o644)
except:
pass
# 删除备份目录(更新成功)
if backup_dir and os.path.exists(backup_dir):
shutil.rmtree(backup_dir, ignore_errors=True)
if info_callback:
info_callback(parent)
return True
except Exception as copy_error:
# 恢复备份
if backup_dir and os.path.exists(backup_dir):
if os.path.exists(local_dir):
shutil.rmtree(local_dir, ignore_errors=True)
try:
shutil.move(backup_dir, local_dir)
except:
pass
raise copy_error
except Exception as e:
if error_callback:
error_callback(parent, str(e))
return False

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,11 +4,24 @@
extern "C" {
#endif
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
#define BSP_OK (0)
#define BSP_ERR (-1)
#define BSP_ERR_NULL (-2)
#define BSP_ERR_INITED (-3)
#define BSP_ERR_NO_DEV (-4)
#define BSP_ERR_TIMEOUT (-5)
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}

659
assets/User_code/bsp/can.c Normal file
View File

@ -0,0 +1,659 @@
/* Includes ----------------------------------------------------------------- */
#include "bsp/can.h"
#include "bsp/bsp.h"
#include <can.h>
#include <cmsis_os2.h>
#include <string.h>
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Private define ----------------------------------------------------------- */
#define CAN_QUEUE_MUTEX_TIMEOUT 100 /* 队列互斥锁超时时间(ms) */
#define CAN_TX_MAILBOX_NUM 3 /* CAN发送邮箱数量 */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
typedef struct BSP_CAN_QueueNode {
BSP_CAN_t can; /* CAN通道 */
uint32_t can_id; /* 解析后的CAN ID */
osMessageQueueId_t queue; /* 消息队列ID */
uint8_t queue_size; /* 队列大小 */
struct BSP_CAN_QueueNode *next; /* 指向下一个节点的指针 */
} BSP_CAN_QueueNode_t;
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Private variables -------------------------------------------------------- */
static BSP_CAN_QueueNode_t *queue_list = NULL;
static osMutexId_t queue_mutex = NULL;
static void (*CAN_Callback[BSP_CAN_NUM][BSP_CAN_CB_NUM])(void);
static bool inited = false;
static BSP_CAN_IdParser_t id_parser = NULL; /* ID解析器 */
static BSP_CAN_TxQueue_t tx_queues[BSP_CAN_NUM]; /* 每个CAN的发送队列 */
/* Private function prototypes ---------------------------------------------- */
static BSP_CAN_t CAN_Get(CAN_HandleTypeDef *hcan);
static osMessageQueueId_t BSP_CAN_FindQueue(BSP_CAN_t can, uint32_t can_id);
static int8_t BSP_CAN_CreateIdQueue(BSP_CAN_t can, uint32_t can_id, uint8_t queue_size);
static void BSP_CAN_RxFifo0Callback(void);
static void BSP_CAN_RxFifo1Callback(void);
static void BSP_CAN_TxCompleteCallback(void);
static BSP_CAN_FrameType_t BSP_CAN_GetFrameType(CAN_RxHeaderTypeDef *header);
static uint32_t BSP_CAN_DefaultIdParser(uint32_t original_id, BSP_CAN_FrameType_t frame_type);
static void BSP_CAN_TxQueueInit(BSP_CAN_t can);
static bool BSP_CAN_TxQueuePush(BSP_CAN_t can, BSP_CAN_TxMessage_t *msg);
static bool BSP_CAN_TxQueuePop(BSP_CAN_t can, BSP_CAN_TxMessage_t *msg);
static bool BSP_CAN_TxQueueIsEmpty(BSP_CAN_t can);
/* Private functions -------------------------------------------------------- */
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
/**
* @brief CAN句柄获取BSP_CAN实例
*/
static BSP_CAN_t CAN_Get(CAN_HandleTypeDef *hcan) {
if (hcan == NULL) return BSP_CAN_ERR;
/* AUTO GENERATED CAN_GET */
else
return BSP_CAN_ERR;
}
/**
* @brief CAN ID的消息队列
* @note
*/
static osMessageQueueId_t BSP_CAN_FindQueue(BSP_CAN_t can, uint32_t can_id) {
BSP_CAN_QueueNode_t *node = queue_list;
while (node != NULL) {
if (node->can == can && node->can_id == can_id) {
return node->queue;
}
node = node->next;
}
return NULL;
}
/**
* @brief CAN ID的消息队列
* @note
*/
static int8_t BSP_CAN_CreateIdQueue(BSP_CAN_t can, uint32_t can_id, uint8_t queue_size) {
if (queue_size == 0) {
queue_size = BSP_CAN_DEFAULT_QUEUE_SIZE;
}
if (osMutexAcquire(queue_mutex, CAN_QUEUE_MUTEX_TIMEOUT) != osOK) {
return BSP_ERR_TIMEOUT;
}
BSP_CAN_QueueNode_t *node = queue_list;
while (node != NULL) {
if (node->can == can && node->can_id == can_id) {
osMutexRelease(queue_mutex);
return BSP_ERR; // 已存在
}
node = node->next;
}
BSP_CAN_QueueNode_t *new_node = (BSP_CAN_QueueNode_t *)BSP_Malloc(sizeof(BSP_CAN_QueueNode_t));
if (new_node == NULL) {
osMutexRelease(queue_mutex);
return BSP_ERR_NULL;
}
new_node->queue = osMessageQueueNew(queue_size, sizeof(BSP_CAN_Message_t), NULL);
if (new_node->queue == NULL) {
BSP_Free(new_node);
osMutexRelease(queue_mutex);
return BSP_ERR;
}
new_node->can = can;
new_node->can_id = can_id;
new_node->queue_size = queue_size;
new_node->next = queue_list;
queue_list = new_node;
osMutexRelease(queue_mutex);
return BSP_OK;
}
/**
* @brief
*/
static BSP_CAN_FrameType_t BSP_CAN_GetFrameType(CAN_RxHeaderTypeDef *header) {
if (header->RTR == CAN_RTR_REMOTE) {
return (header->IDE == CAN_ID_EXT) ? BSP_CAN_FRAME_EXT_REMOTE : BSP_CAN_FRAME_STD_REMOTE;
} else {
return (header->IDE == CAN_ID_EXT) ? BSP_CAN_FRAME_EXT_DATA : BSP_CAN_FRAME_STD_DATA;
}
}
/**
* @brief ID解析器ID
*/
static uint32_t BSP_CAN_DefaultIdParser(uint32_t original_id, BSP_CAN_FrameType_t frame_type) {
(void)frame_type; // 避免未使用参数警告
return original_id;
}
/**
* @brief
*/
static void BSP_CAN_TxQueueInit(BSP_CAN_t can) {
if (can >= BSP_CAN_NUM) return;
tx_queues[can].head = 0;
tx_queues[can].tail = 0;
}
/**
* @brief
*/
static bool BSP_CAN_TxQueuePush(BSP_CAN_t can, BSP_CAN_TxMessage_t *msg) {
if (can >= BSP_CAN_NUM || msg == NULL) return false;
BSP_CAN_TxQueue_t *queue = &tx_queues[can];
uint32_t next_head = (queue->head + 1) % BSP_CAN_TX_QUEUE_SIZE;
// 队列满
if (next_head == queue->tail) {
return false;
}
// 复制消息
queue->buffer[queue->head] = *msg;
// 更新头指针(原子操作)
queue->head = next_head;
return true;
}
/**
* @brief
*/
static bool BSP_CAN_TxQueuePop(BSP_CAN_t can, BSP_CAN_TxMessage_t *msg) {
if (can >= BSP_CAN_NUM || msg == NULL) return false;
BSP_CAN_TxQueue_t *queue = &tx_queues[can];
// 队列空
if (queue->head == queue->tail) {
return false;
}
// 复制消息
*msg = queue->buffer[queue->tail];
// 更新尾指针(原子操作)
queue->tail = (queue->tail + 1) % BSP_CAN_TX_QUEUE_SIZE;
return true;
}
/**
* @brief
*/
static bool BSP_CAN_TxQueueIsEmpty(BSP_CAN_t can) {
if (can >= BSP_CAN_NUM) return true;
return tx_queues[can].head == tx_queues[can].tail;
}
/**
* @brief CAN实例的发送队列
*/
static void BSP_CAN_TxCompleteCallback(void) {
// 处理所有CAN实例的发送队列
for (int i = 0; i < BSP_CAN_NUM; i++) {
BSP_CAN_t can = (BSP_CAN_t)i;
CAN_HandleTypeDef *hcan = BSP_CAN_GetHandle(can);
if (hcan == NULL) continue;
BSP_CAN_TxMessage_t msg;
uint32_t mailbox;
// 尝试发送队列中的消息
while (!BSP_CAN_TxQueueIsEmpty(can)) {
// 检查是否有空闲邮箱
if (HAL_CAN_GetTxMailboxesFreeLevel(hcan) == 0) {
break; // 没有空闲邮箱,等待下次中断
}
// 从队列中取出消息
if (!BSP_CAN_TxQueuePop(can, &msg)) {
break;
}
// 发送消息
if (HAL_CAN_AddTxMessage(hcan, &msg.header, msg.data, &mailbox) != HAL_OK) {
// 发送失败,消息已经从队列中移除,直接丢弃
break;
}
}
}
}
/**
* @brief FIFO0接收处理函数
*/
static void BSP_CAN_RxFifo0Callback(void) {
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[BSP_CAN_MAX_DLC];
for (int can_idx = 0; can_idx < BSP_CAN_NUM; can_idx++) {
CAN_HandleTypeDef *hcan = BSP_CAN_GetHandle((BSP_CAN_t)can_idx);
if (hcan == NULL) continue;
while (HAL_CAN_GetRxFifoFillLevel(hcan, CAN_RX_FIFO0) > 0) {
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK) {
uint32_t original_id = (rx_header.IDE == CAN_ID_STD) ? rx_header.StdId : rx_header.ExtId;
BSP_CAN_FrameType_t frame_type = BSP_CAN_GetFrameType(&rx_header);
uint32_t parsed_id = BSP_CAN_ParseId(original_id, frame_type);
osMessageQueueId_t queue = BSP_CAN_FindQueue((BSP_CAN_t)can_idx, parsed_id);
if (queue != NULL) {
BSP_CAN_Message_t msg = {0};
msg.frame_type = frame_type;
msg.original_id = original_id;
msg.parsed_id = parsed_id;
msg.dlc = rx_header.DLC;
if (rx_header.RTR == CAN_RTR_DATA) {
memcpy(msg.data, rx_data, rx_header.DLC);
}
msg.timestamp = HAL_GetTick();
osMessageQueuePut(queue, &msg, 0, BSP_CAN_TIMEOUT_IMMEDIATE);
}
}
}
}
}
/**
* @brief FIFO1接收处理函数
*/
static void BSP_CAN_RxFifo1Callback(void) {
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[BSP_CAN_MAX_DLC];
for (int can_idx = 0; can_idx < BSP_CAN_NUM; can_idx++) {
CAN_HandleTypeDef *hcan = BSP_CAN_GetHandle((BSP_CAN_t)can_idx);
if (hcan == NULL) continue;
while (HAL_CAN_GetRxFifoFillLevel(hcan, CAN_RX_FIFO1) > 0) {
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO1, &rx_header, rx_data) == HAL_OK) {
uint32_t original_id = (rx_header.IDE == CAN_ID_STD) ? rx_header.StdId : rx_header.ExtId;
BSP_CAN_FrameType_t frame_type = BSP_CAN_GetFrameType(&rx_header);
uint32_t parsed_id = BSP_CAN_ParseId(original_id, frame_type);
osMessageQueueId_t queue = BSP_CAN_FindQueue((BSP_CAN_t)can_idx, parsed_id);
if (queue != NULL) {
BSP_CAN_Message_t msg = {0};
msg.frame_type = frame_type;
msg.original_id = original_id;
msg.parsed_id = parsed_id;
msg.dlc = rx_header.DLC;
if (rx_header.RTR == CAN_RTR_DATA) {
memcpy(msg.data, rx_data, rx_header.DLC);
}
msg.timestamp = HAL_GetTick();
osMessageQueuePut(queue, &msg, 0, BSP_CAN_TIMEOUT_IMMEDIATE);
}
}
}
}
}
/* HAL Callback Functions --------------------------------------------------- */
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 ------------------------------------------------------- */
int8_t BSP_CAN_Init(void) {
if (inited) {
return BSP_ERR_INITED;
}
// 清零回调函数数组
memset(CAN_Callback, 0, sizeof(CAN_Callback));
// 初始化发送队列
for (int i = 0; i < BSP_CAN_NUM; i++) {
BSP_CAN_TxQueueInit((BSP_CAN_t)i);
}
// 初始化ID解析器为默认解析器
id_parser = BSP_CAN_DefaultIdParser;
// 创建互斥锁
queue_mutex = osMutexNew(NULL);
if (queue_mutex == NULL) {
return BSP_ERR;
}
/* AUTO GENERATED CAN_INIT */
inited = true;
return BSP_OK;
}
CAN_HandleTypeDef *BSP_CAN_GetHandle(BSP_CAN_t can) {
if (can >= BSP_CAN_NUM) {
return NULL;
}
switch (can) {
/* AUTO GENERATED BSP_CAN_GET_HANDLE */
default:
return NULL;
}
}
int8_t BSP_CAN_RegisterCallback(BSP_CAN_t can, BSP_CAN_Callback_t type,
void (*callback)(void)) {
if (!inited) {
return BSP_ERR_INITED;
}
if (callback == NULL) {
return BSP_ERR_NULL;
}
if (can >= BSP_CAN_NUM) {
return BSP_ERR;
}
if (type >= BSP_CAN_CB_NUM) {
return BSP_ERR;
}
CAN_Callback[can][type] = callback;
return BSP_OK;
}
int8_t BSP_CAN_Transmit(BSP_CAN_t can, BSP_CAN_Format_t format,
uint32_t id, uint8_t *data, uint8_t dlc) {
if (!inited) {
return BSP_ERR_INITED;
}
if (can >= BSP_CAN_NUM) {
return BSP_ERR;
}
if (data == NULL && format != BSP_CAN_FORMAT_STD_REMOTE && format != BSP_CAN_FORMAT_EXT_REMOTE) {
return BSP_ERR_NULL;
}
if (dlc > BSP_CAN_MAX_DLC) {
return BSP_ERR;
}
CAN_HandleTypeDef *hcan = BSP_CAN_GetHandle(can);
if (hcan == NULL) {
return BSP_ERR_NULL;
}
// 准备发送消息
BSP_CAN_TxMessage_t tx_msg = {0};
switch (format) {
case BSP_CAN_FORMAT_STD_DATA:
tx_msg.header.StdId = id;
tx_msg.header.IDE = CAN_ID_STD;
tx_msg.header.RTR = CAN_RTR_DATA;
break;
case BSP_CAN_FORMAT_EXT_DATA:
tx_msg.header.ExtId = id;
tx_msg.header.IDE = CAN_ID_EXT;
tx_msg.header.RTR = CAN_RTR_DATA;
break;
case BSP_CAN_FORMAT_STD_REMOTE:
tx_msg.header.StdId = id;
tx_msg.header.IDE = CAN_ID_STD;
tx_msg.header.RTR = CAN_RTR_REMOTE;
break;
case BSP_CAN_FORMAT_EXT_REMOTE:
tx_msg.header.ExtId = id;
tx_msg.header.IDE = CAN_ID_EXT;
tx_msg.header.RTR = CAN_RTR_REMOTE;
break;
default:
return BSP_ERR;
}
tx_msg.header.DLC = dlc;
tx_msg.header.TransmitGlobalTime = DISABLE;
// 复制数据
if (data != NULL && dlc > 0) {
memcpy(tx_msg.data, data, dlc);
}
// 尝试直接发送到邮箱
uint32_t mailbox;
if (HAL_CAN_GetTxMailboxesFreeLevel(hcan) > 0) {
HAL_StatusTypeDef result = HAL_CAN_AddTxMessage(hcan, &tx_msg.header, tx_msg.data, &mailbox);
if (result == HAL_OK) {
return BSP_OK; // 发送成功
}
}
// 邮箱满,尝试放入队列
if (BSP_CAN_TxQueuePush(can, &tx_msg)) {
return BSP_OK; // 成功放入队列
}
// 队列也满,丢弃数据
return BSP_ERR; // 数据丢弃
}
int8_t BSP_CAN_TransmitStdDataFrame(BSP_CAN_t can, BSP_CAN_StdDataFrame_t *frame) {
if (frame == NULL) {
return BSP_ERR_NULL;
}
return BSP_CAN_Transmit(can, BSP_CAN_FORMAT_STD_DATA, frame->id, frame->data, frame->dlc);
}
int8_t BSP_CAN_TransmitExtDataFrame(BSP_CAN_t can, BSP_CAN_ExtDataFrame_t *frame) {
if (frame == NULL) {
return BSP_ERR_NULL;
}
return BSP_CAN_Transmit(can, BSP_CAN_FORMAT_EXT_DATA, frame->id, frame->data, frame->dlc);
}
int8_t BSP_CAN_TransmitRemoteFrame(BSP_CAN_t can, BSP_CAN_RemoteFrame_t *frame) {
if (frame == NULL) {
return BSP_ERR_NULL;
}
BSP_CAN_Format_t format = frame->is_extended ? BSP_CAN_FORMAT_EXT_REMOTE : BSP_CAN_FORMAT_STD_REMOTE;
return BSP_CAN_Transmit(can, format, frame->id, NULL, frame->dlc);
}
int8_t BSP_CAN_RegisterId(BSP_CAN_t can, uint32_t can_id, uint8_t queue_size) {
if (!inited) {
return BSP_ERR_INITED;
}
return BSP_CAN_CreateIdQueue(can, can_id, queue_size);
}
int8_t BSP_CAN_GetMessage(BSP_CAN_t can, uint32_t can_id, BSP_CAN_Message_t *msg, uint32_t timeout) {
if (!inited) {
return BSP_ERR_INITED;
}
if (msg == NULL) {
return BSP_ERR_NULL;
}
if (osMutexAcquire(queue_mutex, CAN_QUEUE_MUTEX_TIMEOUT) != osOK) {
return BSP_ERR_TIMEOUT;
}
osMessageQueueId_t queue = BSP_CAN_FindQueue(can, can_id);
osMutexRelease(queue_mutex);
if (queue == NULL) {
return BSP_ERR_NO_DEV;
}
osStatus_t result = osMessageQueueGet(queue, msg, NULL, timeout);
return (result == osOK) ? BSP_OK : BSP_ERR;
}
int32_t BSP_CAN_GetQueueCount(BSP_CAN_t can, uint32_t can_id) {
if (!inited) {
return -1;
}
if (osMutexAcquire(queue_mutex, CAN_QUEUE_MUTEX_TIMEOUT) != osOK) {
return -1;
}
osMessageQueueId_t queue = BSP_CAN_FindQueue(can, can_id);
osMutexRelease(queue_mutex);
if (queue == NULL) {
return -1;
}
return (int32_t)osMessageQueueGetCount(queue);
}
int8_t BSP_CAN_FlushQueue(BSP_CAN_t can, uint32_t can_id) {
if (!inited) {
return BSP_ERR_INITED;
}
if (osMutexAcquire(queue_mutex, CAN_QUEUE_MUTEX_TIMEOUT) != osOK) {
return BSP_ERR_TIMEOUT;
}
osMessageQueueId_t queue = BSP_CAN_FindQueue(can, can_id);
osMutexRelease(queue_mutex);
if (queue == NULL) {
return BSP_ERR_NO_DEV;
}
BSP_CAN_Message_t temp_msg;
while (osMessageQueueGet(queue, &temp_msg, NULL, BSP_CAN_TIMEOUT_IMMEDIATE) == osOK) {
// 清空
}
return BSP_OK;
}
int8_t BSP_CAN_RegisterIdParser(BSP_CAN_IdParser_t parser) {
if (!inited) {
return BSP_ERR_INITED;
}
if (parser == NULL) {
return BSP_ERR_NULL;
}
id_parser = parser;
return BSP_OK;
}
uint32_t BSP_CAN_ParseId(uint32_t original_id, BSP_CAN_FrameType_t frame_type) {
if (id_parser != NULL) {
return id_parser(original_id, frame_type);
}
return BSP_CAN_DefaultIdParser(original_id, frame_type);
}

259
assets/User_code/bsp/can.h Normal file
View File

@ -0,0 +1,259 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ----------------------------------------------------------------- */
#include <can.h>
#include "bsp/bsp.h"
#include "bsp/mm.h"
#include <stdint.h>
#include <stdbool.h>
#include <cmsis_os.h>
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Exported constants ------------------------------------------------------- */
#define BSP_CAN_MAX_DLC 8
#define BSP_CAN_DEFAULT_QUEUE_SIZE 10
#define BSP_CAN_TIMEOUT_IMMEDIATE 0
#define BSP_CAN_TIMEOUT_FOREVER osWaitForever
#define BSP_CAN_TX_QUEUE_SIZE 32 /* 发送队列大小 */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* 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;
/* CAN消息格式枚举 - 用于发送和接收消息时指定格式 */
typedef enum {
BSP_CAN_FORMAT_STD_DATA, /* 标准数据帧 */
BSP_CAN_FORMAT_EXT_DATA, /* 扩展数据帧 */
BSP_CAN_FORMAT_STD_REMOTE, /* 标准远程帧 */
BSP_CAN_FORMAT_EXT_REMOTE, /* 扩展远程帧 */
} BSP_CAN_Format_t;
/* CAN帧类型枚举 - 用于区分不同类型的CAN帧 */
typedef enum {
BSP_CAN_FRAME_STD_DATA, /* 标准数据帧 */
BSP_CAN_FRAME_EXT_DATA, /* 扩展数据帧 */
BSP_CAN_FRAME_STD_REMOTE, /* 标准远程帧 */
BSP_CAN_FRAME_EXT_REMOTE, /* 扩展远程帧 */
} BSP_CAN_FrameType_t;
/* CAN消息结构体 - 支持不同类型帧 */
typedef struct {
BSP_CAN_FrameType_t frame_type; /* 帧类型 */
uint32_t original_id; /* 原始ID未解析 */
uint32_t parsed_id; /* 解析后的实际ID */
uint8_t dlc; /* 数据长度 */
uint8_t data[BSP_CAN_MAX_DLC]; /* 数据 */
uint32_t timestamp; /* 时间戳(可选) */
} BSP_CAN_Message_t;
/* 标准数据帧结构 */
typedef struct {
uint32_t id; /* CAN ID */
uint8_t dlc; /* 数据长度 */
uint8_t data[BSP_CAN_MAX_DLC]; /* 数据 */
} BSP_CAN_StdDataFrame_t;
/* 扩展数据帧结构 */
typedef struct {
uint32_t id; /* 扩展CAN ID */
uint8_t dlc; /* 数据长度 */
uint8_t data[BSP_CAN_MAX_DLC]; /* 数据 */
} BSP_CAN_ExtDataFrame_t;
/* 远程帧结构 */
typedef struct {
uint32_t id; /* CAN ID */
uint8_t dlc; /* 请求的数据长度 */
bool is_extended; /* 是否为扩展帧 */
} BSP_CAN_RemoteFrame_t;
/* ID解析回调函数类型 */
typedef uint32_t (*BSP_CAN_IdParser_t)(uint32_t original_id, BSP_CAN_FrameType_t frame_type);
/* CAN发送消息结构体 */
typedef struct {
CAN_TxHeaderTypeDef header; /* 发送头 */
uint8_t data[BSP_CAN_MAX_DLC]; /* 数据 */
} BSP_CAN_TxMessage_t;
/* 无锁环形队列结构体 */
typedef struct {
BSP_CAN_TxMessage_t buffer[BSP_CAN_TX_QUEUE_SIZE]; /* 缓冲区 */
volatile uint32_t head; /* 队列头 */
volatile uint32_t tail; /* 队列尾 */
} BSP_CAN_TxQueue_t;
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Exported functions prototypes -------------------------------------------- */
/**
* @brief CAN
* @return BSP_OK
*/
int8_t BSP_CAN_Init(void);
/**
* @brief CAN
* @param can CAN
* @return CAN_HandleTypeDef NULL
*/
CAN_HandleTypeDef *BSP_CAN_GetHandle(BSP_CAN_t can);
/**
* @brief CAN
* @param can CAN
* @param type
* @param callback
* @return BSP_OK
*/
int8_t BSP_CAN_RegisterCallback(BSP_CAN_t can, BSP_CAN_Callback_t type,
void (*callback)(void));
/**
* @brief CAN
* @param can CAN
* @param format
* @param id CAN ID
* @param data
* @param dlc
* @return BSP_OK
*/
int8_t BSP_CAN_Transmit(BSP_CAN_t can, BSP_CAN_Format_t format,
uint32_t id, uint8_t *data, uint8_t dlc);
/**
* @brief
* @param can CAN
* @param frame
* @return BSP_OK
*/
int8_t BSP_CAN_TransmitStdDataFrame(BSP_CAN_t can, BSP_CAN_StdDataFrame_t *frame);
/**
* @brief
* @param can CAN
* @param frame
* @return BSP_OK
*/
int8_t BSP_CAN_TransmitExtDataFrame(BSP_CAN_t can, BSP_CAN_ExtDataFrame_t *frame);
/**
* @brief
* @param can CAN
* @param frame
* @return BSP_OK
*/
int8_t BSP_CAN_TransmitRemoteFrame(BSP_CAN_t can, BSP_CAN_RemoteFrame_t *frame);
/**
* @brief
* @param can CAN
* @return -1
*/
int32_t BSP_CAN_GetTxQueueCount(BSP_CAN_t can);
/**
* @brief
* @param can CAN
* @return BSP_OK
*/
int8_t BSP_CAN_FlushTxQueue(BSP_CAN_t can);
/**
* @brief CAN ID
* @param can CAN
* @param can_id CAN ID
* @param queue_size 0使
* @return BSP_OK
*/
int8_t BSP_CAN_RegisterId(BSP_CAN_t can, uint32_t can_id, uint8_t queue_size);
/**
* @brief CAN
* @param can CAN
* @param can_id CAN ID
* @param msg
* @param timeout 0osWaitForever为永久等待
* @return BSP_OK
*/
int8_t BSP_CAN_GetMessage(BSP_CAN_t can, uint32_t can_id, BSP_CAN_Message_t *msg, uint32_t timeout);
/**
* @brief ID队列中的消息数量
* @param can CAN
* @param can_id CAN ID
* @return -1
*/
int32_t BSP_CAN_GetQueueCount(BSP_CAN_t can, uint32_t can_id);
/**
* @brief ID队列中的所有消息
* @param can CAN
* @param can_id CAN ID
* @return BSP_OK
*/
int8_t BSP_CAN_FlushQueue(BSP_CAN_t can, uint32_t can_id);
/**
* @brief ID解析器
* @param parser ID解析回调函数
* @return BSP_OK
*/
int8_t BSP_CAN_RegisterIdParser(BSP_CAN_IdParser_t parser);
/**
* @brief CAN ID
* @param original_id ID
* @param frame_type
* @return ID
*/
uint32_t BSP_CAN_ParseId(uint32_t original_id, BSP_CAN_FrameType_t frame_type);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,11 @@
uart,请开启uart的dma和中断
can,请开启can中断使用函数前请确保can已经初始化。一定要开启can发送中断
gpio,会自动读取cubemx中配置为gpio的引脚并自动区分输入输出和中断。
spi,请开启spi的dma和中断
i2c,要求开始spi中断
mm,这是套了一层的动态内存分配
time,获取时间戳函数需要开启freerots
dwt,需要开启dwt获取时间
i2c,请开启i2c的dma和中断
pwm,用于选择那些勇于输出pwm
1 uart 请开启uart的dma和中断
2 can 请开启can中断,使用函数前请确保can已经初始化。一定要开启can发送中断!!!
3 gpio 会自动读取cubemx中配置为gpio的引脚,并自动区分输入输出和中断。
4 spi 请开启spi的dma和中断
5 i2c 要求开始spi中断
6 mm 这是套了一层的动态内存分配
7 time 获取时间戳函数,需要开启freerots
8 dwt 需要开启dwt,获取时间
9 i2c 请开启i2c的dma和中断
10 pwm 用于选择那些勇于输出pwm

138
assets/User_code/bsp/dwt.c Normal file
View File

@ -0,0 +1,138 @@
/**
******************************************************************************
* @file dwt.c
* @author Wang Hongxi
* @version V1.1.0
* @date 2022/3/8
* @brief
******************************************************************************
* @attention
*
******************************************************************************
*/
#include "bsp/dwt.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* USER STRUCT BEGIN */
/* USER STRUCT END */
DWT_Time_t SysTime;
static uint32_t CPU_FREQ_Hz, CPU_FREQ_Hz_ms, CPU_FREQ_Hz_us;
static uint32_t CYCCNT_RountCount;
static uint32_t CYCCNT_LAST;
uint64_t CYCCNT64;
static void DWT_CNT_Update(void);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
void DWT_Init(uint32_t CPU_Freq_mHz)
{
/* 使能DWT外设 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
/* DWT CYCCNT寄存器计数清0 */
DWT->CYCCNT = (uint32_t)0u;
/* 使能Cortex-M DWT CYCCNT寄存器 */
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
CPU_FREQ_Hz = CPU_Freq_mHz * 1000000;
CPU_FREQ_Hz_ms = CPU_FREQ_Hz / 1000;
CPU_FREQ_Hz_us = CPU_FREQ_Hz / 1000000;
CYCCNT_RountCount = 0;
}
float DWT_GetDeltaT(uint32_t *cnt_last)
{
volatile uint32_t cnt_now = DWT->CYCCNT;
float dt = ((uint32_t)(cnt_now - *cnt_last)) / ((float)(CPU_FREQ_Hz));
*cnt_last = cnt_now;
DWT_CNT_Update();
return dt;
}
double DWT_GetDeltaT64(uint32_t *cnt_last)
{
volatile uint32_t cnt_now = DWT->CYCCNT;
double dt = ((uint32_t)(cnt_now - *cnt_last)) / ((double)(CPU_FREQ_Hz));
*cnt_last = cnt_now;
DWT_CNT_Update();
return dt;
}
void DWT_SysTimeUpdate(void)
{
volatile uint32_t cnt_now = DWT->CYCCNT;
static uint64_t CNT_TEMP1, CNT_TEMP2, CNT_TEMP3;
DWT_CNT_Update();
CYCCNT64 = (uint64_t)CYCCNT_RountCount * (uint64_t)UINT32_MAX + (uint64_t)cnt_now;
CNT_TEMP1 = CYCCNT64 / CPU_FREQ_Hz;
CNT_TEMP2 = CYCCNT64 - CNT_TEMP1 * CPU_FREQ_Hz;
SysTime.s = CNT_TEMP1;
SysTime.ms = CNT_TEMP2 / CPU_FREQ_Hz_ms;
CNT_TEMP3 = CNT_TEMP2 - SysTime.ms * CPU_FREQ_Hz_ms;
SysTime.us = CNT_TEMP3 / CPU_FREQ_Hz_us;
}
float DWT_GetTimeline_s(void)
{
DWT_SysTimeUpdate();
float DWT_Timelinef32 = SysTime.s + SysTime.ms * 0.001f + SysTime.us * 0.000001f;
return DWT_Timelinef32;
}
float DWT_GetTimeline_ms(void)
{
DWT_SysTimeUpdate();
float DWT_Timelinef32 = SysTime.s * 1000 + SysTime.ms + SysTime.us * 0.001f;
return DWT_Timelinef32;
}
uint64_t DWT_GetTimeline_us(void)
{
DWT_SysTimeUpdate();
uint64_t DWT_Timelinef32 = SysTime.s * 1000000 + SysTime.ms * 1000 + SysTime.us;
return DWT_Timelinef32;
}
static void DWT_CNT_Update(void)
{
volatile uint32_t cnt_now = DWT->CYCCNT;
if (cnt_now < CYCCNT_LAST)
CYCCNT_RountCount++;
CYCCNT_LAST = cnt_now;
}
void DWT_Delay(float Delay)
{
uint32_t tickstart = DWT->CYCCNT;
float wait = Delay;
while ((DWT->CYCCNT - tickstart) < wait * (float)CPU_FREQ_Hz)
{
}
}

View File

@ -0,0 +1,53 @@
/**
******************************************************************************
* @file dwt.h
* @author Wang Hongxi
* @version V1.1.0
* @date 2022/3/8
* @brief
******************************************************************************
* @attention
*
******************************************************************************
*/
#ifndef _DWT_H
#define _DWT_H
#include "main.h"
#include "stdint.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
typedef struct
{
uint32_t s;
uint16_t ms;
uint16_t us;
} DWT_Time_t;
/* USER STRUCT BEGIN */
/* USER STRUCT END */
void DWT_Init(uint32_t CPU_Freq_mHz);
float DWT_GetDeltaT(uint32_t *cnt_last);
double DWT_GetDeltaT64(uint32_t *cnt_last);
float DWT_GetTimeline_s(void);
float DWT_GetTimeline_ms(void);
uint64_t DWT_GetTimeline_us(void);
void DWT_Delay(float Delay);
void DWT_SysTimeUpdate(void);
extern DWT_Time_t SysTime;
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#endif /* DWT_H_ */

View File

@ -0,0 +1,98 @@
/* Includes ----------------------------------------------------------------- */
#include "bsp/gpio.h"
#include <gpio.h>
#include <main.h>
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Private define ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
typedef struct {
uint16_t pin;
GPIO_TypeDef *gpio;
} BSP_GPIO_MAP_t;
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Private variables -------------------------------------------------------- */
static const BSP_GPIO_MAP_t GPIO_Map[BSP_GPIO_NUM] = {
/* AUTO GENERATED BSP_GPIO_MAP */
};
static void (*GPIO_Callback[16])(void);
/* Private function -------------------------------------------------------- */
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
for (uint8_t i = 0; i < 16; i++) {
if (GPIO_Pin & (1 << i)) {
if (GPIO_Callback[i]) {
GPIO_Callback[i]();
}
}
}
}
/* Exported functions ------------------------------------------------------- */
int8_t BSP_GPIO_RegisterCallback(BSP_GPIO_t gpio, void (*callback)(void)) {
if (callback == NULL) return BSP_ERR_NULL;
if (gpio >= BSP_GPIO_NUM) return BSP_ERR;
// 从GPIO映射中获取对应的pin值
uint16_t pin = GPIO_Map[gpio].pin;
for (uint8_t i = 0; i < 16; i++) {
if (pin & (1 << i)) {
GPIO_Callback[i] = callback;
break;
}
}
return BSP_OK;
}
int8_t BSP_GPIO_EnableIRQ(BSP_GPIO_t gpio) {
switch (gpio) {
/* AUTO GENERATED BSP_GPIO_ENABLE_IRQ */
default:
return BSP_ERR;
}
return BSP_OK;
}
int8_t BSP_GPIO_DisableIRQ(BSP_GPIO_t gpio) {
switch (gpio) {
/* AUTO GENERATED BSP_GPIO_DISABLE_IRQ */
default:
return BSP_ERR;
}
return BSP_OK;
}
int8_t BSP_GPIO_WritePin(BSP_GPIO_t gpio, bool value){
if (gpio >= BSP_GPIO_NUM) return BSP_ERR;
HAL_GPIO_WritePin(GPIO_Map[gpio].gpio, GPIO_Map[gpio].pin, value);
return BSP_OK;
}
int8_t BSP_GPIO_TogglePin(BSP_GPIO_t gpio){
if (gpio >= BSP_GPIO_NUM) return BSP_ERR;
HAL_GPIO_TogglePin(GPIO_Map[gpio].gpio, GPIO_Map[gpio].pin);
return BSP_OK;
}
bool BSP_GPIO_ReadPin(BSP_GPIO_t gpio){
if (gpio >= BSP_GPIO_NUM) return false;
return HAL_GPIO_ReadPin(GPIO_Map[gpio].gpio, GPIO_Map[gpio].pin) == GPIO_PIN_SET;
}

View File

@ -6,32 +6,42 @@ extern "C" {
/* Includes ----------------------------------------------------------------- */
#include <stdint.h>
#include <stdbool.h>
#include "bsp/bsp.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Exported constants ------------------------------------------------------- */
/* Exported macro ----------------------------------------------------------- */
/* Exported types ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* GPIO设备枚举与设备对应 */
/* USER DEFINE END */
/* Exported types ----------------------------------------------------------- */
typedef enum {
BSP_GPIO_USER_KEY,
/* BSP_GPIO_XXX, */
/* AUTO GENERATED BSP_GPIO_ENUM */
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_RegisterCallback(BSP_GPIO_t gpio, void (*callback)(void));
int8_t BSP_GPIO_EnableIRQ(BSP_GPIO_t gpio);
int8_t BSP_GPIO_DisableIRQ(BSP_GPIO_t gpio);
int8_t BSP_GPIO_WritePin(BSP_GPIO_t gpio, bool value);
int8_t BSP_GPIO_TogglePin(BSP_GPIO_t gpio);
bool BSP_GPIO_ReadPin(BSP_GPIO_t gpio);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}
#endif

View File

@ -1,18 +1,27 @@
/* Includes ----------------------------------------------------------------- */
#include "bsp\i2c.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Private define ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Private variables -------------------------------------------------------- */
static void (*I2C_Callback[BSP_I2C_NUM][BSP_I2C_CB_NUM])(void);
/* Private function -------------------------------------------------------- */
static BSP_I2C_t I2C_Get(I2C_HandleTypeDef *hi2c) {
if (hi2c->Instance == I2C1)
return BSP_I2C_EXAMPLE;
// else if (hi2c->Instance == I2CX)
// return BSP_I2C_XXX;
/* AUTO GENERATED I2C_GET */
else
return BSP_I2C_ERR;
}
@ -92,10 +101,7 @@ void HAL_I2C_AbortCpltCallback(I2C_HandleTypeDef *hi2c) {
/* Exported functions ------------------------------------------------------- */
I2C_HandleTypeDef *BSP_I2C_GetHandle(BSP_I2C_t i2c) {
switch (i2c) {
case BSP_I2C_EXAMPLE:
return &hi2c1;
// case BSP_I2C_XXX:
// return &hi2cX;
/* AUTO GENERATED BSP_I2C_GET_HANDLE */
default:
return NULL;
}
@ -107,3 +113,76 @@ int8_t BSP_I2C_RegisterCallback(BSP_I2C_t i2c, BSP_I2C_Callback_t type,
I2C_Callback[i2c][type] = callback;
return BSP_OK;
}
int8_t BSP_I2C_Transmit(BSP_I2C_t i2c, uint16_t devAddr, uint8_t *data,
uint16_t size, bool dma) {
if (i2c >= BSP_I2C_NUM) return BSP_ERR;
I2C_HandleTypeDef *hi2c = BSP_I2C_GetHandle(i2c);
if (hi2c == NULL) return BSP_ERR;
if (dma) {
return HAL_I2C_Master_Transmit_DMA(hi2c, devAddr, data, size);
} else {
return HAL_I2C_Master_Transmit(hi2c, devAddr, data, size, 10);
}
}
int8_t BSP_I2C_Receive(BSP_I2C_t i2c, uint16_t devAddr, uint8_t *data,
uint16_t size, bool dma) {
if (i2c >= BSP_I2C_NUM) return BSP_ERR;
I2C_HandleTypeDef *hi2c = BSP_I2C_GetHandle(i2c);
if (hi2c == NULL) return BSP_ERR;
if (dma) {
return HAL_I2C_Master_Receive_DMA(hi2c, devAddr, data, size);
} else {
return HAL_I2C_Master_Receive(hi2c, devAddr, data, size, 10);
}
}
uint8_t BSP_I2C_MemReadByte(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr) {
if (i2c >= BSP_I2C_NUM) return 0xFF;
I2C_HandleTypeDef *hi2c = BSP_I2C_GetHandle(i2c);
if (hi2c == NULL) return 0xFF;
uint8_t data;
HAL_I2C_Mem_Read(hi2c, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT, &data, 1, HAL_MAX_DELAY);
return data;
}
int8_t BSP_I2C_MemWriteByte(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr,
uint8_t data) {
if (i2c >= BSP_I2C_NUM) return BSP_ERR;
I2C_HandleTypeDef *hi2c = BSP_I2C_GetHandle(i2c);
if (hi2c == NULL) return BSP_ERR;
return HAL_I2C_Mem_Write(hi2c, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT, &data, 1, HAL_MAX_DELAY);
}
int8_t BSP_I2C_MemRead(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr,
uint8_t *data, uint16_t size, bool dma) {
if (i2c >= BSP_I2C_NUM || data == NULL || size == 0) return BSP_ERR;
I2C_HandleTypeDef *hi2c = BSP_I2C_GetHandle(i2c);
if (hi2c == NULL) return BSP_ERR;
if (dma) {
return HAL_I2C_Mem_Read_DMA(hi2c, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT, data, size);
}
else {
return HAL_I2C_Mem_Read(hi2c, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT, data, size, HAL_MAX_DELAY);
}
}
int8_t BSP_I2C_MemWrite(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr,
uint8_t *data, uint16_t size, bool dma) {
if (i2c >= BSP_I2C_NUM || data == NULL || size == 0) return BSP_ERR;
I2C_HandleTypeDef *hi2c = BSP_I2C_GetHandle(i2c);
if (hi2c == NULL) return BSP_ERR;
if (dma) {
return HAL_I2C_Mem_Write_DMA(hi2c, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT, data, size);
} else {
return HAL_I2C_Mem_Write(hi2c, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT, data, size, HAL_MAX_DELAY);
}
}

View File

@ -6,19 +6,31 @@ extern "C" {
/* Includes ----------------------------------------------------------------- */
#include <i2c.h>
#include <stdint.h>
#include <stdbool.h>
#include "bsp/bsp.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Exported constants ------------------------------------------------------- */
/* Exported macro ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Exported types ----------------------------------------------------------- */
/* 要添加使用I2C的新设备需要先在此添加对应的枚举值 */
/* I2C实体枚举与设备对应 */
typedef enum {
BSP_I2C_EXAMPLE,
/* BSP_I2C_XXX,*/
/* AUTO GENERATED BSP_I2C_NAME */
/* USER BSP_I2C BEGIN*/
/* USER_I2C_XXX */
/* USER BSP_I2C END */
BSP_I2C_NUM,
BSP_I2C_ERR,
} BSP_I2C_t;
@ -42,6 +54,25 @@ I2C_HandleTypeDef *BSP_I2C_GetHandle(BSP_I2C_t i2c);
int8_t BSP_I2C_RegisterCallback(BSP_I2C_t i2c, BSP_I2C_Callback_t type,
void (*callback)(void));
int8_t BSP_I2C_Transmit(BSP_I2C_t i2c, uint16_t devAddr, uint8_t *data,
uint16_t size, bool dma);
int8_t BSP_I2C_Receive(BSP_I2C_t i2c, uint16_t devAddr, uint8_t *data,
uint16_t size, bool dma);
uint8_t BSP_I2C_MemReadByte(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr);
int8_t BSP_I2C_MemWriteByte(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr,
uint8_t data);
int8_t BSP_I2C_MemRead(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr,
uint8_t *data, uint16_t size, bool dma);
int8_t BSP_I2C_MemWrite(BSP_I2C_t i2c, uint16_t devAddr, uint16_t memAddr,
uint8_t *data, uint16_t size, bool dma);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}
#endif

View File

@ -1,36 +1,30 @@
/* Includes ----------------------------------------------------------------- */
#include "main.h"
#include "servo.h"
#include "bsp/mm.h"
#include "bsp/servo_pwm.h"
#include "FreeRTOS.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Private define ----------------------------------------------------------- */
#define MIN_CYCLE 0.5f //change begin
#define MAX_CYCLE 2.5f
#define ANGLE_LIMIT 180 //change end
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Private variables -------------------------------------------------------- */
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Private function -------------------------------------------------------- */
/* Exported functions ------------------------------------------------------- */
int serve_Init(BSP_PWM_Channel_t ch)
{
if(BSP_PWM_Start(ch)!=0){
return -1;
}else return 0;
}
inline void *BSP_Malloc(size_t size) { return pvPortMalloc(size); }
inline void BSP_Free(void *pv) { vPortFree(pv); }
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;
}
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */

View File

@ -1,20 +1,32 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ----------------------------------------------------------------- */
#include <stddef.h>
#include <stdint.h>
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Exported constants ------------------------------------------------------- */
/* Exported macro ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Exported types ----------------------------------------------------------- */
/* 设置BUZZER状态 */
typedef enum
{
BSP_BUZZER_ON,
BSP_BUZZER_OFF,
BSP_BUZZER_TAGGLE,
} BSP_Buzzer_Status_t;
/* Exported functions prototypes -------------------------------------------- */
void *BSP_Malloc(size_t size);
void BSP_Free(void *pv);
int8_t BSP_Buzzer_Set(BSP_Buzzer_Status_t s);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}
#endif

110
assets/User_code/bsp/pwm.c Normal file
View File

@ -0,0 +1,110 @@
/* Includes ----------------------------------------------------------------- */
#include "tim.h"
#include "bsp/pwm.h"
#include "bsp.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Private define ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
typedef struct {
TIM_HandleTypeDef *tim;
uint16_t channel;
} BSP_PWM_Config_t;
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Private variables -------------------------------------------------------- */
static const BSP_PWM_Config_t PWM_Map[BSP_PWM_NUM] = {
/* AUTO GENERATED BSP_PWM_MAP */
};
/* Private function -------------------------------------------------------- */
/* Exported functions ------------------------------------------------------- */
int8_t BSP_PWM_Start(BSP_PWM_Channel_t ch) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
HAL_TIM_PWM_Start(PWM_Map[ch].tim, PWM_Map[ch].channel);
return BSP_OK;
}
int8_t BSP_PWM_SetComp(BSP_PWM_Channel_t ch, float duty_cycle) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
if (duty_cycle > 1.0f) {
duty_cycle = 1.0f;
}
if (duty_cycle < 0.0f) {
duty_cycle = 0.0f;
}
// 获取ARR值周期值
uint32_t arr = __HAL_TIM_GET_AUTORELOAD(PWM_Map[ch].tim);
// 计算比较值CCR = duty_cycle * (ARR + 1)
uint32_t ccr = (uint32_t)(duty_cycle * (arr + 1));
__HAL_TIM_SET_COMPARE(PWM_Map[ch].tim, PWM_Map[ch].channel, ccr);
return BSP_OK;
}
int8_t BSP_PWM_SetFreq(BSP_PWM_Channel_t ch, float freq) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
uint32_t timer_clock = HAL_RCC_GetPCLK1Freq(); // Get the timer clock frequency
uint32_t prescaler = PWM_Map[ch].tim->Init.Prescaler;
uint32_t period = (timer_clock / (prescaler + 1)) / freq - 1;
if (period > UINT16_MAX) {
return BSP_ERR; // Frequency too low
}
__HAL_TIM_SET_AUTORELOAD(PWM_Map[ch].tim, period);
return BSP_OK;
}
int8_t BSP_PWM_Stop(BSP_PWM_Channel_t ch) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
HAL_TIM_PWM_Stop(PWM_Map[ch].tim, PWM_Map[ch].channel);
return BSP_OK;
}
uint32_t BSP_PWM_GetAutoReloadPreload(BSP_PWM_Channel_t ch) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
return PWM_Map[ch].tim->Init.AutoReloadPreload;
}
TIM_HandleTypeDef* BSP_PWM_GetHandle(BSP_PWM_Channel_t ch) {
return PWM_Map[ch].tim;
}
uint16_t BSP_PWM_GetChannel(BSP_PWM_Channel_t ch) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
return PWM_Map[ch].channel;
}
int8_t BSP_PWM_Start_DMA(BSP_PWM_Channel_t ch, uint32_t *pData, uint16_t Length) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
HAL_TIM_PWM_Start_DMA(PWM_Map[ch].tim, PWM_Map[ch].channel, pData, Length);
return BSP_OK;
}
int8_t BSP_PWM_Stop_DMA(BSP_PWM_Channel_t ch) {
if (ch >= BSP_PWM_NUM) return BSP_ERR;
HAL_TIM_PWM_Stop_DMA(PWM_Map[ch].tim, PWM_Map[ch].channel);
return BSP_OK;
}

View File

@ -0,0 +1,48 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ----------------------------------------------------------------- */
#include <stdint.h>
#include "tim.h"
#include "bsp.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Exported constants ------------------------------------------------------- */
/* Exported macro ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Exported types ----------------------------------------------------------- */
/* PWM通道 */
typedef enum {
/* AUTO GENERATED BSP_PWM_ENUM */
BSP_PWM_NUM,
BSP_PWM_ERR,
} BSP_PWM_Channel_t;
/* Exported functions prototypes -------------------------------------------- */
int8_t BSP_PWM_Start(BSP_PWM_Channel_t ch);
int8_t BSP_PWM_SetComp(BSP_PWM_Channel_t ch, float duty_cycle);
int8_t BSP_PWM_SetFreq(BSP_PWM_Channel_t ch, float freq);
int8_t BSP_PWM_Stop(BSP_PWM_Channel_t ch);
uint32_t BSP_PWM_GetAutoReloadPreload(BSP_PWM_Channel_t ch);
uint16_t BSP_PWM_GetChannel(BSP_PWM_Channel_t ch);
TIM_HandleTypeDef* BSP_PWM_GetHandle(BSP_PWM_Channel_t ch);
int8_t BSP_PWM_Start_DMA(BSP_PWM_Channel_t ch, uint32_t *pData, uint16_t Length);
int8_t BSP_PWM_Stop_DMA(BSP_PWM_Channel_t ch);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}
#endif

View File

@ -1,20 +1,29 @@
/* Includes ----------------------------------------------------------------- */
#include "bsp\spi.h"
#include <spi.h>
#include "bsp/spi.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Private define ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Private variables -------------------------------------------------------- */
static void (*SPI_Callback[BSP_SPI_NUM][BSP_SPI_CB_NUM])(void);
/* Private function -------------------------------------------------------- */
static BSP_SPI_t SPI_Get(SPI_HandleTypeDef *hspi) {
if (hspi->Instance == SPI1)
return BSP_SPI_EXAMPLE;
/*
else if (hspi->Instance == SPIX)
return BSP_SPI_XXX;
*/
return BSP_SPI_BMI088;
else
return BSP_SPI_ERR;
}
@ -87,12 +96,8 @@ void HAL_SPI_AbortCpltCallback(SPI_HandleTypeDef *hspi) {
/* Exported functions ------------------------------------------------------- */
SPI_HandleTypeDef *BSP_SPI_GetHandle(BSP_SPI_t spi) {
switch (spi) {
case BSP_SPI_EXAMPLE:
case BSP_SPI_BMI088:
return &hspi1;
/*
case BSP_SPI_XXX:
return &hspiX;
*/
default:
return NULL;
}
@ -104,3 +109,73 @@ int8_t BSP_SPI_RegisterCallback(BSP_SPI_t spi, BSP_SPI_Callback_t type,
SPI_Callback[spi][type] = callback;
return BSP_OK;
}
int8_t BSP_SPI_Transmit(BSP_SPI_t spi, uint8_t *data, uint16_t size, bool dma) {
if (spi >= BSP_SPI_NUM) return BSP_ERR;
SPI_HandleTypeDef *hspi = BSP_SPI_GetHandle(spi);
if (hspi == NULL) return BSP_ERR;
if (dma) {
return HAL_SPI_Transmit_DMA(hspi, data, size)!= HAL_OK;;
} else {
return HAL_SPI_Transmit(hspi, data, size, 20)!= HAL_OK;;
}
}
int8_t BSP_SPI_Receive(BSP_SPI_t spi, uint8_t *data, uint16_t size, bool dma) {
if (spi >= BSP_SPI_NUM) return BSP_ERR;
SPI_HandleTypeDef *hspi = BSP_SPI_GetHandle(spi);
if (hspi == NULL) return BSP_ERR;
if (dma) {
return HAL_SPI_Receive_DMA(hspi, data, size)!= HAL_OK;;
} else {
return HAL_SPI_Receive(hspi, data, size, 20)!= HAL_OK;;
}
}
int8_t BSP_SPI_TransmitReceive(BSP_SPI_t spi, uint8_t *txData, uint8_t *rxData,
uint16_t size, bool dma) {
if (spi >= BSP_SPI_NUM) return BSP_ERR;
SPI_HandleTypeDef *hspi = BSP_SPI_GetHandle(spi);
if (hspi == NULL) return BSP_ERR;
if (dma) {
return HAL_SPI_TransmitReceive_DMA(hspi, txData, rxData, size)!= HAL_OK;;
} else {
return HAL_SPI_TransmitReceive(hspi, txData, rxData, size, 20)!= HAL_OK;;
}
}
uint8_t BSP_SPI_MemReadByte(BSP_SPI_t spi, uint8_t reg) {
if (spi >= BSP_SPI_NUM) return 0xFF;
uint8_t tmp[2] = {reg | 0x80, 0x00};
BSP_SPI_TransmitReceive(spi, tmp, tmp, 2u, true);
return tmp[1];
}
int8_t BSP_SPI_MemWriteByte(BSP_SPI_t spi, uint8_t reg, uint8_t data) {
if (spi >= BSP_SPI_NUM) return BSP_ERR;
uint8_t tmp[2] = {reg & 0x7f, data};
return BSP_SPI_Transmit(spi, tmp, 2u, true);
}
int8_t BSP_SPI_MemRead(BSP_SPI_t spi, uint8_t reg, uint8_t *data, uint16_t size) {
if (spi >= BSP_SPI_NUM) return BSP_ERR;
if (data == NULL || size == 0) return BSP_ERR_NULL;
reg = reg | 0x80;
BSP_SPI_Transmit(spi, &reg, 1u, true);
return BSP_SPI_Receive(spi, data, size, true);
}
int8_t BSP_SPI_MemWrite(BSP_SPI_t spi, uint8_t reg, uint8_t *data, uint16_t size) {
if (spi >= BSP_SPI_NUM) return BSP_ERR;
if (data == NULL || size == 0) return BSP_ERR_NULL;
reg = reg & 0x7f;
BSP_SPI_Transmit(spi, &reg, 1u, true);
return BSP_SPI_Transmit(spi, data, size, true);
}
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */

View File

@ -6,19 +6,28 @@ extern "C" {
/* Includes ----------------------------------------------------------------- */
#include <spi.h>
#include <stdint.h>
#include <stdbool.h>
#include "bsp/bsp.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Exported constants ------------------------------------------------------- */
/* Exported macro ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Exported types ----------------------------------------------------------- */
/* 要添加使用SPI的新设备需要先在此添加对应的枚举值 */
/* SPI实体枚举与设备对应 */
typedef enum {
BSP_SPI_EXAMPLE,
/* BSP_SPI_XXX,*/
/* AUTO GENERATED BSP_SPI_NAME */
BSP_SPI_NUM,
BSP_SPI_ERR,
} BSP_SPI_t;
@ -41,6 +50,21 @@ SPI_HandleTypeDef *BSP_SPI_GetHandle(BSP_SPI_t spi);
int8_t BSP_SPI_RegisterCallback(BSP_SPI_t spi, BSP_SPI_Callback_t type,
void (*callback)(void));
int8_t BSP_SPI_Transmit(BSP_SPI_t spi, uint8_t *data, uint16_t size, bool dma);
int8_t BSP_SPI_Receive(BSP_SPI_t spi, uint8_t *data, uint16_t size, bool dma);
int8_t BSP_SPI_TransmitReceive(BSP_SPI_t spi, uint8_t *txData, uint8_t *rxData,
uint16_t size, bool dma);
uint8_t BSP_SPI_MemReadByte(BSP_SPI_t spi, uint8_t reg);
int8_t BSP_SPI_MemWriteByte(BSP_SPI_t spi, uint8_t reg, uint8_t data);
int8_t BSP_SPI_MemRead(BSP_SPI_t spi, uint8_t reg, uint8_t *data, uint16_t size);
int8_t BSP_SPI_MemWrite(BSP_SPI_t spi, uint8_t reg, uint8_t *data, uint16_t size);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,81 @@
/* Includes ----------------------------------------------------------------- */
#include "bsp/time.h"
#include "bsp.h"
#include <cmsis_os2.h>
#include "FreeRTOS.h"
#include "main.h"
#include "task.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* Private define ----------------------------------------------------------- */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* USER STRUCT BEGIN */
/* USER STRUCT END */
/* Private variables -------------------------------------------------------- */
/* Private function -------------------------------------------------------- */
/* Exported functions ------------------------------------------------------- */
uint32_t BSP_TIME_Get_ms() { return xTaskGetTickCount(); }
uint64_t BSP_TIME_Get_us() {
uint32_t tick_freq = osKernelGetTickFreq();
uint32_t ticks_old = xTaskGetTickCount()*(1000/tick_freq);
uint32_t tick_value_old = SysTick->VAL;
uint32_t ticks_new = xTaskGetTickCount()*(1000/tick_freq);
uint32_t tick_value_new = SysTick->VAL;
if (ticks_old == ticks_new) {
return ticks_new * 1000 + 1000 - tick_value_old * 1000 / (SysTick->LOAD + 1);
} else {
return ticks_new * 1000 + 1000 - tick_value_new * 1000 / (SysTick->LOAD + 1);
}
}
uint64_t BSP_TIME_Get() __attribute__((alias("BSP_TIME_Get_us")));
int8_t BSP_TIME_Delay_ms(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;
}
/*阻塞us延迟*/
int8_t BSP_TIME_Delay_us(uint32_t us) {
uint64_t start = BSP_TIME_Get_us();
while (BSP_TIME_Get_us() - start < us) {
// 等待us时间
}
return BSP_OK;
}
int8_t BSP_TIME_Delay(uint32_t ms) __attribute__((alias("BSP_TIME_Delay_ms")));
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */

Some files were not shown because too many files have changed in this diff Show More