Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04729c066a | |||
| 52b6449c4f | |||
| d1c3b2747a | |||
| 49545dd232 | |||
| 7127b207f8 | |||
| a9f13e2607 | |||
| b96137d807 | |||
| e5d5afb1a8 | |||
| 697104d1ce | |||
| d3aabce4f5 | |||
| 563ede007c | |||
| 22a71d3d04 | |||
| 96feb958a5 | |||
| a42e2e3832 | |||
| 0e57cd6cd7 | |||
| 4cb5e2fc9a | |||
| 3fb6348116 | |||
| 7275e5e6e1 | |||
| d2eea18ecc | |||
| e3ed160f42 | |||
| dc87fbc7da | |||
| 09602c76cd | |||
| d16427f7d4 | |||
| a29e097978 | |||
| 92bb89124b | |||
| d7a56e656b | |||
| ae40434ecf | |||
| 5ba916c40a | |||
| d5d580f384 | |||
| 60154cafd5 | |||
| 3062fbbef0 | |||
| f449b15fe2 | |||
| 822080af2f | |||
| 889f34ae12 | |||
| bc898da6c2 | |||
| f1c6b085a4 | |||
| d626e4e656 | |||
| 485e25fec2 | |||
| 22498e9a4f | |||
| 70d3add6da | |||
| d6eaed5f72 | |||
| fcd97c1392 | |||
| 79f7671a9f | |||
| d61dfa3634 | |||
| 006ee185e9 | |||
| 89b6af6138 | |||
| d5871097a9 | |||
| 1697a51555 | |||
| 474ea3ded3 | |||
| 41de38a146 | |||
| 7e92f32642 | |||
| 68394c616e | |||
| 266868ad71 | |||
| 898c5dfb2b | |||
| c90d0b4d79 | |||
| e070e723aa | |||
| 62549172dd | |||
| f9f0d93b95 | |||
| 71c2e83a7a | |||
| 43749e0391 | |||
| c8ca5e1031 | |||
| 3159d3ae1a | |||
| 3e49722616 | |||
| bbb521654c | |||
| 50cfcb0693 | |||
| d99e9e1ec8 | |||
| 2c9309ae1e | |||
| bebfbd716c | |||
| af7529b529 | |||
| fe82822d58 | |||
| 254328ddc8 | |||
| 4973bb101a | |||
| a7c46ca9fc | |||
| 89fbaf8e55 | |||
| b4a4d87909 | |||
| 88ec1517fb | |||
| 08193b8093 | |||
| 7951dae760 | |||
| 501a9ddff4 | |||
| 47e0b8419f | |||
| 9fc6b4577a | |||
| 78661f450b | |||
| 62b4b07912 | |||
| f2fedac360 | |||
| 34a0874156 | |||
| e9eb169547 | |||
| 606bd7e054 | |||
| ae6246474b | |||
| c737ec79d4 | |||
| a1da927d9c |
3
.gitignore
vendored
3
.gitignore
vendored
@ -31,4 +31,5 @@ Examples/
|
||||
|
||||
build/
|
||||
dist/
|
||||
*.spec
|
||||
*.spec
|
||||
*.exe
|
||||
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal 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
2339
MR_Tool.py
File diff suppressed because it is too large
Load Diff
18
MRobot.iss
Normal file
18
MRobot.iss
Normal file
@ -0,0 +1,18 @@
|
||||
[Setup]
|
||||
AppName=MRobot
|
||||
AppVersion=1.0.1
|
||||
DefaultDirName={userappdata}\MRobot
|
||||
DefaultGroupName=MRobot
|
||||
OutputDir=.
|
||||
OutputBaseFilename=MRobotInstaller
|
||||
|
||||
[Files]
|
||||
Source: "dist\MRobot.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "assets\logo\*"; DestDir: "{app}\assets\logo"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\User_code\*"; DestDir: "{app}\assets\User_code"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\mech_lib\*"; DestDir: "{app}\assets\mech_lib"; Flags: ignoreversion recursesubdirs
|
||||
Source: "assets\logo\M.ico"; DestDir: "{app}\assets\logo"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
||||
Name: "{userdesktop}\MRobot"; Filename: "{app}\MRobot.exe"; IconFilename: "{app}\assets\logo\M.ico"
|
||||
680
MRobot.py
680
MRobot.py
@ -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用open,Windows用explorer,Linux用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_() 是一个阻塞调用,直到应用程序退出。
|
||||
|
||||
719
MRobot_old.py
719
MRobot_old.py
@ -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()
|
||||
21
README.md
21
README.md
@ -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"
|
||||
```
|
||||
@ -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;
|
||||
}
|
||||
141
User/bsp/can.c
141
User/bsp/can.c
@ -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;
|
||||
}
|
||||
@ -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
|
||||
118
User/bsp/delay.c
118
User/bsp/delay.c
@ -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;
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
i2c,I2C
|
||||
uart,USART,UART
|
||||
|
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
@ -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;
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
pid,component/filter
|
||||
pid,component/user_math
|
||||
filter,component/user_math
|
||||
|
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
@ -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,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
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <cmsis_os2.h>
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
|
||||
// 定义任务运行时结构体
|
||||
typedef struct {
|
||||
/* 各任务,也可以叫做线程 */
|
||||
struct {
|
||||
{{thread_definitions}}
|
||||
} thread;
|
||||
|
||||
/* USER MESSAGE BEGIN */
|
||||
|
||||
struct {
|
||||
osMessageQueueId_t user_msg; /* 用户自定义任务消息队列 */
|
||||
} msgq;
|
||||
|
||||
/* USER MESSAGE END */
|
||||
|
||||
struct {
|
||||
{{freq_definitions}}
|
||||
} freq; /* 任务运行频率 */
|
||||
|
||||
struct {
|
||||
{{last_up_time_definitions}}
|
||||
} last_up_time; /* 任务最近运行时间 */
|
||||
} Task_Runtime_t;
|
||||
|
||||
// 任务频率
|
||||
{{task_frequency_definitions}}
|
||||
// 任务初始化延时
|
||||
#define TASK_INIT_DELAY (100u)
|
||||
{{task_init_delay_definitions}}
|
||||
|
||||
// 任务句柄
|
||||
typedef struct {
|
||||
{{task_handle_definitions}}
|
||||
} Task_Handles_t;
|
||||
|
||||
// 任务运行时结构体
|
||||
extern Task_Runtime_t task_runtime;
|
||||
|
||||
// 初始化任务句柄
|
||||
extern const osThreadAttr_t attr_init;
|
||||
{{task_attr_declarations}}
|
||||
|
||||
// 任务函数声明
|
||||
void Task_Init(void *argument);
|
||||
{{task_function_declarations}}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/about_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/about_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/ai_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/ai_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/code_configuration_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/code_configuration_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/code_generate_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/code_generate_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/data_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/data_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/function_fit_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/function_fit_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/home_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/home_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main_window.cpython-39.pyc
Normal file
BIN
app/__pycache__/main_window.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/mini_tool_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/mini_tool_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/part_library_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/part_library_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/serial_terminal_interface.cpython-39.pyc
Normal file
BIN
app/__pycache__/serial_terminal_interface.cpython-39.pyc
Normal file
Binary file not shown.
522
app/about_interface.py
Normal file
522
app/about_interface.py
Normal 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
182
app/ai_interface.py
Normal file
@ -0,0 +1,182 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
|
||||
from qfluentwidgets import TextEdit, LineEdit, PushButton, TitleLabel, SubtitleLabel, FluentIcon, InfoBar, InfoBarPosition
|
||||
import requests
|
||||
import json
|
||||
|
||||
class AIWorker(QThread):
|
||||
response_signal = pyqtSignal(str)
|
||||
done_signal = pyqtSignal()
|
||||
error_signal = pyqtSignal(str) # 新增
|
||||
|
||||
def __init__(self, prompt, parent=None):
|
||||
super().__init__(parent)
|
||||
self.prompt = prompt
|
||||
|
||||
def run(self):
|
||||
url = "http://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
|
||||
173
app/code_configuration_interface.py
Normal file
173
app/code_configuration_interface.py
Normal 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")
|
||||
414
app/code_generate_interface.py
Normal file
414
app/code_generate_interface.py
Normal 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
|
||||
BIN
app/code_page/__pycache__/bsp_interface.cpython-39.pyc
Normal file
BIN
app/code_page/__pycache__/bsp_interface.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/code_page/__pycache__/component_interface.cpython-39.pyc
Normal file
BIN
app/code_page/__pycache__/component_interface.cpython-39.pyc
Normal file
Binary file not shown.
1248
app/code_page/bsp_interface.py
Normal file
1248
app/code_page/bsp_interface.py
Normal file
File diff suppressed because it is too large
Load Diff
372
app/code_page/component_interface.py
Normal file
372
app/code_page/component_interface.py
Normal 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
|
||||
404
app/code_page/device_interface.py
Normal file
404
app/code_page/device_interface.py
Normal 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
634
app/data_interface.py
Normal 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)
|
||||
260
app/function_fit_interface.py
Normal file
260
app/function_fit_interface.py
Normal file
@ -0,0 +1,260 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon
|
||||
from openpyxl import load_workbook, Workbook
|
||||
|
||||
import numpy as np
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||
import plotly.graph_objs as go
|
||||
import plotly.io as pio
|
||||
|
||||
import matplotlib
|
||||
matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Source Han Sans', 'STHeiti', 'Heiti TC']
|
||||
matplotlib.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
|
||||
class FunctionFitInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("functionFitInterface")
|
||||
|
||||
main_layout = QHBoxLayout(self)
|
||||
main_layout.setSpacing(24)
|
||||
|
||||
# 左侧:数据输入区
|
||||
left_layout = QVBoxLayout()
|
||||
left_layout.setSpacing(16)
|
||||
|
||||
self.dataTable = TableWidget(self)
|
||||
self.dataTable.setColumnCount(2)
|
||||
self.dataTable.setHorizontalHeaderLabels(["x", "y"])
|
||||
self.dataTable.setColumnWidth(0, 125)
|
||||
self.dataTable.setColumnWidth(1, 125)
|
||||
left_layout.addWidget(self.dataTable)
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
add_row_btn = PushButton("添加一行")
|
||||
add_row_btn.clicked.connect(self.add_row)
|
||||
|
||||
del_row_btn = PushButton("删除选中行") # 新增按钮
|
||||
del_row_btn.clicked.connect(self.delete_selected_row) # 绑定槽函数
|
||||
btn_layout.addWidget(add_row_btn)
|
||||
|
||||
btn_layout.addWidget(del_row_btn) # 添加到布局
|
||||
left_layout.addLayout(btn_layout)
|
||||
|
||||
|
||||
|
||||
btn_layout = QHBoxLayout()
|
||||
import_btn = PushButton("导入 Excel")
|
||||
import_btn.clicked.connect(self.import_excel)
|
||||
export_btn = PushButton("导出 Excel")
|
||||
export_btn.clicked.connect(self.export_excel)
|
||||
btn_layout.addWidget(import_btn)
|
||||
btn_layout.addWidget(export_btn)
|
||||
left_layout.addLayout(btn_layout)
|
||||
|
||||
self.dataTable.setMinimumWidth(280)
|
||||
self.dataTable.setMaximumWidth(280)
|
||||
main_layout.addLayout(left_layout, 1)
|
||||
|
||||
self.add_row()
|
||||
|
||||
# 右侧:图像展示区
|
||||
right_layout = QVBoxLayout()
|
||||
right_layout.setSpacing(12)
|
||||
right_layout.addWidget(SubtitleLabel("函数图像预览"))
|
||||
|
||||
self.figure = Figure(figsize=(5, 4))
|
||||
self.canvas = FigureCanvas(self.figure)
|
||||
right_layout.addWidget(self.canvas, stretch=1)
|
||||
|
||||
self.resultLabel = BodyLabel("")
|
||||
self.resultLabel.setWordWrap(True) # 自动换行
|
||||
right_layout.addWidget(self.resultLabel)
|
||||
|
||||
|
||||
# 拟合阶数和输出语言选择(合并到同一行)
|
||||
options_layout = QHBoxLayout()
|
||||
self.spinBox = SpinBox()
|
||||
self.spinBox.setRange(1, 10)
|
||||
self.spinBox.setValue(2)
|
||||
options_layout.addWidget(SubtitleLabel("拟合阶数"))
|
||||
options_layout.addWidget(self.spinBox)
|
||||
|
||||
self.langBox = ComboBox()
|
||||
self.langBox.addItems(["C/C++", "Python"])
|
||||
options_layout.addWidget(SubtitleLabel("输出语言"))
|
||||
options_layout.addWidget(self.langBox)
|
||||
|
||||
|
||||
right_layout.addLayout(options_layout)
|
||||
|
||||
|
||||
# 代码显示和复制按钮
|
||||
self.codeLabel = BodyLabel("")
|
||||
self.codeLabel.setWordWrap(True) # 自动换行
|
||||
right_layout.addWidget(self.codeLabel)
|
||||
|
||||
btn_layout = QHBoxLayout() # 新增一行布局
|
||||
fit_btn = PushButton(FluentIcon.UNIT,"拟合并绘图")
|
||||
fit_btn.clicked.connect(self.fit_and_plot)
|
||||
btn_layout.addWidget(fit_btn)
|
||||
copy_btn = PushButton(FluentIcon.COPY, "复制代码")
|
||||
copy_btn.clicked.connect(self.copy_code)
|
||||
btn_layout.addWidget(copy_btn)
|
||||
right_layout.addLayout(btn_layout)
|
||||
|
||||
|
||||
main_layout.addLayout(right_layout, 2)
|
||||
|
||||
# 默认显示空图像
|
||||
self.figure.clear()
|
||||
ax = self.figure.add_subplot(111)
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
self.canvas.draw()
|
||||
|
||||
def add_row(self):
|
||||
row = self.dataTable.rowCount()
|
||||
self.dataTable.insertRow(row)
|
||||
# 可选:初始化为空字符串
|
||||
self.dataTable.setItem(row, 0, QTableWidgetItem(""))
|
||||
self.dataTable.setItem(row, 1, QTableWidgetItem(""))
|
||||
|
||||
def delete_selected_row(self):
|
||||
selected = self.dataTable.selectedItems()
|
||||
if selected:
|
||||
rows = set(item.row() for item in selected)
|
||||
for row in sorted(rows, reverse=True):
|
||||
self.dataTable.removeRow(row)
|
||||
|
||||
def import_excel(self):
|
||||
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx)")
|
||||
if path:
|
||||
wb = load_workbook(path)
|
||||
ws = wb.active
|
||||
self.dataTable.setRowCount(0)
|
||||
for row_data in ws.iter_rows(min_row=2, values_only=True): # 跳过表头
|
||||
row = self.dataTable.rowCount()
|
||||
self.dataTable.insertRow(row)
|
||||
for col, value in enumerate(row_data[:2]):
|
||||
item = QTableWidgetItem(str(value) if value is not None else "")
|
||||
self.dataTable.setItem(row, col, item)
|
||||
|
||||
|
||||
def export_excel(self):
|
||||
path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)")
|
||||
if path:
|
||||
data = self.parse_data()
|
||||
if data is not None:
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.append(["x", "y"])
|
||||
for row in data:
|
||||
ws.append(row)
|
||||
wb.save(path)
|
||||
|
||||
def parse_data(self):
|
||||
data = []
|
||||
row_count = self.dataTable.rowCount()
|
||||
for row in range(row_count):
|
||||
try:
|
||||
x_item = self.dataTable.item(row, 0)
|
||||
y_item = self.dataTable.item(row, 1)
|
||||
if x_item is None or y_item is None:
|
||||
continue
|
||||
x = float(x_item.text())
|
||||
y = float(y_item.text())
|
||||
data.append([x, y])
|
||||
except Exception:
|
||||
continue
|
||||
return data if data else None
|
||||
|
||||
def fit_and_plot(self):
|
||||
data = self.parse_data()
|
||||
if not data:
|
||||
self.resultLabel.setText("数据格式错误或为空")
|
||||
self.codeLabel.setText("")
|
||||
return
|
||||
|
||||
x = np.array([d[0] for d in data])
|
||||
y = np.array([d[1] for d in data])
|
||||
degree = self.spinBox.value()
|
||||
coeffs = np.polyfit(x, y, degree)
|
||||
|
||||
# 用更密集的横坐标画拟合曲线
|
||||
x_fit = np.linspace(x.min(), x.max(), 100)
|
||||
y_fit = np.polyval(coeffs, x_fit)
|
||||
|
||||
self.figure.clear()
|
||||
ax = self.figure.add_subplot(111)
|
||||
ax.scatter(x, y, color='blue', label='raw data')
|
||||
ax.plot(x_fit, y_fit, color='red', label=f'Fitted curve')
|
||||
ax.set_title('graph of a function')
|
||||
ax.set_xlabel('x')
|
||||
ax.set_ylabel('y')
|
||||
ax.legend()
|
||||
self.canvas.draw()
|
||||
|
||||
formula = self.poly_formula(coeffs)
|
||||
self.resultLabel.setText(f"拟合公式: {formula}")
|
||||
|
||||
lang = self.langBox.currentText()
|
||||
code = self.generate_code(coeffs, lang)
|
||||
self.codeLabel.setText(code)
|
||||
|
||||
def poly_formula(self, coeffs):
|
||||
terms = []
|
||||
degree = len(coeffs) - 1
|
||||
for i, c in enumerate(coeffs):
|
||||
power = degree - i
|
||||
if abs(c) < 1e-8:
|
||||
continue
|
||||
if power == 0:
|
||||
terms.append(f"{c:.6g}")
|
||||
elif power == 1:
|
||||
terms.append(f"{c:.6g}*x")
|
||||
else:
|
||||
terms.append(f"{c:.6g}*x^{power}")
|
||||
return " + ".join(terms)
|
||||
|
||||
def generate_code(self, coeffs, lang):
|
||||
degree = len(coeffs) - 1
|
||||
if lang == "C/C++":
|
||||
code = "double poly(double x) {\n return "
|
||||
elif lang == "Python":
|
||||
code = "def poly(x):\n return "
|
||||
else:
|
||||
code = ""
|
||||
terms = []
|
||||
for i, c in enumerate(coeffs):
|
||||
power = degree - i
|
||||
if abs(c) < 1e-8:
|
||||
continue
|
||||
if power == 0:
|
||||
terms.append(f"{c:.6g}")
|
||||
elif power == 1:
|
||||
terms.append(f"{c:.6g}*x")
|
||||
else:
|
||||
terms.append(f"{c:.6g}*pow(x,{power})" if lang == "C/C++" else f"{c:.6g}*x**{power}")
|
||||
code += " + ".join(terms)
|
||||
code += ";\n}" if lang == "C/C++" else ""
|
||||
return code
|
||||
|
||||
|
||||
def copy_code(self):
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(self.codeLabel.text())
|
||||
# 弹出提示
|
||||
InfoBar.success(
|
||||
title='复制成功',
|
||||
content="代码已复制到剪贴板!",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.TOP,
|
||||
duration=2000,
|
||||
parent=self
|
||||
)
|
||||
65
app/home_interface.py
Normal file
65
app/home_interface.py
Normal file
@ -0,0 +1,65 @@
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout
|
||||
from PyQt5.QtCore import Qt
|
||||
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, ImageLabel, FluentLabelBase, TitleLabel
|
||||
import sys
|
||||
import os
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""获取资源文件的绝对路径,兼容打包和开发环境"""
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
# PyInstaller 打包后的临时目录
|
||||
return os.path.join(sys._MEIPASS, relative_path)
|
||||
return os.path.join(os.path.abspath("."), relative_path)
|
||||
|
||||
class HomeInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("homeInterface")
|
||||
|
||||
# 外层居中布局
|
||||
outer_layout = QVBoxLayout(self)
|
||||
outer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
outer_layout.setSpacing(0)
|
||||
outer_layout.addStretch()
|
||||
|
||||
# 内容布局
|
||||
content_layout = QVBoxLayout()
|
||||
content_layout.setSpacing(24)
|
||||
content_layout.setContentsMargins(48, 48, 48, 48)
|
||||
|
||||
# Logo
|
||||
logo = ImageLabel(resource_path('assets/logo/MRobot.png'))
|
||||
logo.scaledToHeight(80)
|
||||
content_layout.addWidget(logo, alignment=Qt.AlignHCenter) # 居中对齐
|
||||
|
||||
content_layout.addSpacing(8)
|
||||
content_layout.addStretch()
|
||||
|
||||
# 主标题
|
||||
title = TitleLabel("MRobot Toolbox")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(title)
|
||||
|
||||
# 副标题
|
||||
subtitle = BodyLabel("现代化,多功能机器人开发工具箱")
|
||||
subtitle.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(subtitle)
|
||||
|
||||
# 欢迎语
|
||||
welcome = BodyLabel("欢迎使用 MRobot Toolbox!一站式支持代码生成、硬件管理、串口调试与零件库下载。")
|
||||
welcome.setAlignment(Qt.AlignCenter)
|
||||
content_layout.addWidget(welcome)
|
||||
|
||||
content_layout.addSpacing(16)
|
||||
content_layout.addStretch()
|
||||
|
||||
# 加到主布局
|
||||
outer_layout.addLayout(content_layout)
|
||||
outer_layout.addStretch()
|
||||
|
||||
# 版权信息置底
|
||||
copyright_label = BodyLabel("© 2025 MRobot | Powered by QUT RM&RCer")
|
||||
copyright_label.setAlignment(Qt.AlignCenter)
|
||||
copyright_label.setStyleSheet("font-size: 13px;")
|
||||
outer_layout.addWidget(copyright_label)
|
||||
outer_layout.addSpacing(18)
|
||||
90
app/main_window.py
Normal file
90
app/main_window.py
Normal 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
107
app/mini_tool_interface.py
Normal 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")
|
||||
205
app/part_library_interface.py
Normal file
205
app/part_library_interface.py
Normal file
@ -0,0 +1,205 @@
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget
|
||||
from qfluentwidgets import SubtitleLabel, BodyLabel, HorizontalSeparator, PushButton, TreeWidget, ProgressBar, Dialog, InfoBar, InfoBarPosition, FluentIcon, ProgressRing, Dialog
|
||||
import requests
|
||||
import shutil
|
||||
import os
|
||||
from .tools.part_download import DownloadThread # 新增导入
|
||||
|
||||
from urllib.parse import quote
|
||||
class PartLibraryInterface(QWidget):
|
||||
SERVER_URL = "http://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])
|
||||
167
app/serial_terminal_interface.py
Normal file
167
app/serial_terminal_interface.py
Normal file
@ -0,0 +1,167 @@
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt5.QtGui import QTextCursor
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from qfluentwidgets import (
|
||||
FluentIcon, PushButton, ComboBox, TextEdit, LineEdit, CheckBox,
|
||||
SubtitleLabel, BodyLabel, HorizontalSeparator
|
||||
)
|
||||
|
||||
class SerialReadThread(QThread):
|
||||
data_received = pyqtSignal(str)
|
||||
|
||||
def __init__(self, ser):
|
||||
super().__init__()
|
||||
self.ser = ser
|
||||
self._running = True
|
||||
|
||||
def run(self):
|
||||
while self._running:
|
||||
if self.ser and self.ser.is_open and self.ser.in_waiting:
|
||||
try:
|
||||
data = self.ser.readline().decode(errors='ignore')
|
||||
self.data_received.emit(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self.wait()
|
||||
|
||||
class SerialTerminalInterface(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setObjectName("serialTerminalInterface")
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setSpacing(12)
|
||||
|
||||
# 顶部:串口设置区
|
||||
top_hbox = QHBoxLayout()
|
||||
top_hbox.addWidget(BodyLabel("串口:"))
|
||||
self.port_combo = ComboBox()
|
||||
self.refresh_ports()
|
||||
top_hbox.addWidget(self.port_combo)
|
||||
top_hbox.addWidget(BodyLabel("波特率:"))
|
||||
self.baud_combo = ComboBox()
|
||||
self.baud_combo.addItems(['9600', '115200', '57600', '38400', '19200', '4800'])
|
||||
top_hbox.addWidget(self.baud_combo)
|
||||
self.connect_btn = PushButton("连接")
|
||||
self.connect_btn.clicked.connect(self.toggle_connection)
|
||||
top_hbox.addWidget(self.connect_btn)
|
||||
self.refresh_btn = PushButton(FluentIcon.SYNC, "刷新")
|
||||
self.refresh_btn.clicked.connect(self.refresh_ports)
|
||||
top_hbox.addWidget(self.refresh_btn)
|
||||
top_hbox.addStretch()
|
||||
main_layout.addLayout(top_hbox)
|
||||
|
||||
main_layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 中部:左侧预设命令,右侧显示区
|
||||
center_hbox = QHBoxLayout()
|
||||
# 左侧:预设命令竖排
|
||||
preset_vbox = QVBoxLayout()
|
||||
preset_vbox.addWidget(SubtitleLabel("快捷指令"))
|
||||
preset_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.preset_commands = [
|
||||
("线程监视器", "htop"),
|
||||
("陀螺仪校准", "cali_gyro"),
|
||||
("性能监视", "htop"),
|
||||
("重启", "reset"),
|
||||
("显示所有设备", "ls /dev"),
|
||||
("查询id", "id"),
|
||||
]
|
||||
for label, cmd in self.preset_commands:
|
||||
btn = PushButton(label)
|
||||
btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
btn.clicked.connect(lambda _, c=cmd: self.send_preset_command(c))
|
||||
preset_vbox.addWidget(btn)
|
||||
preset_vbox.addStretch()
|
||||
main_layout.addLayout(center_hbox, stretch=1)
|
||||
|
||||
# 右侧:串口数据显示区
|
||||
self.text_edit = TextEdit()
|
||||
self.text_edit.setReadOnly(True)
|
||||
self.text_edit.setMinimumWidth(400)
|
||||
center_hbox.addWidget(self.text_edit, 3)
|
||||
center_hbox.addLayout(preset_vbox, 1)
|
||||
|
||||
main_layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 底部:输入区
|
||||
bottom_hbox = QHBoxLayout()
|
||||
self.input_line = LineEdit()
|
||||
self.input_line.setPlaceholderText("输入内容,回车发送")
|
||||
self.input_line.returnPressed.connect(self.send_data)
|
||||
bottom_hbox.addWidget(self.input_line, 4)
|
||||
send_btn = PushButton("发送")
|
||||
send_btn.clicked.connect(self.send_data)
|
||||
bottom_hbox.addWidget(send_btn, 1)
|
||||
self.auto_enter_checkbox = CheckBox("自动回车 ")
|
||||
self.auto_enter_checkbox.setChecked(True)
|
||||
bottom_hbox.addWidget(self.auto_enter_checkbox)
|
||||
bottom_hbox.addStretch()
|
||||
main_layout.addLayout(bottom_hbox)
|
||||
|
||||
self.ser = None
|
||||
self.read_thread = None
|
||||
|
||||
def send_preset_command(self, cmd):
|
||||
self.input_line.setText(cmd)
|
||||
self.send_data()
|
||||
|
||||
def refresh_ports(self):
|
||||
self.port_combo.clear()
|
||||
ports = serial.tools.list_ports.comports()
|
||||
for port in ports:
|
||||
self.port_combo.addItem(port.device)
|
||||
|
||||
def toggle_connection(self):
|
||||
if self.ser and self.ser.is_open:
|
||||
self.disconnect_serial()
|
||||
else:
|
||||
self.connect_serial()
|
||||
|
||||
def connect_serial(self):
|
||||
port = self.port_combo.currentText()
|
||||
baud = int(self.baud_combo.currentText())
|
||||
try:
|
||||
self.ser = serial.Serial(port, baud, timeout=0.1)
|
||||
self.connect_btn.setText("断开")
|
||||
self.text_edit.append(f"已连接到 {port} @ {baud}")
|
||||
self.read_thread = SerialReadThread(self.ser)
|
||||
self.read_thread.data_received.connect(self.display_data)
|
||||
self.read_thread.start()
|
||||
except Exception as e:
|
||||
self.text_edit.append(f"连接失败: {e}")
|
||||
|
||||
def disconnect_serial(self):
|
||||
if self.read_thread:
|
||||
self.read_thread.stop()
|
||||
self.read_thread = None
|
||||
if self.ser:
|
||||
self.ser.close()
|
||||
self.ser = None
|
||||
self.connect_btn.setText("连接")
|
||||
self.text_edit.append("已断开连接")
|
||||
|
||||
def display_data(self, data):
|
||||
self.text_edit.moveCursor(QTextCursor.End)
|
||||
self.text_edit.insertPlainText(data)
|
||||
self.text_edit.moveCursor(QTextCursor.End)
|
||||
|
||||
def send_data(self):
|
||||
if self.ser and self.ser.is_open:
|
||||
text = self.input_line.text()
|
||||
try:
|
||||
if not text:
|
||||
self.ser.write('\n'.encode())
|
||||
else:
|
||||
for char in text:
|
||||
self.ser.write(char.encode())
|
||||
if self.auto_enter_checkbox.isChecked():
|
||||
self.ser.write('\n'.encode())
|
||||
except Exception as e:
|
||||
self.text_edit.append(f"发送失败: {e}")
|
||||
self.input_line.clear()
|
||||
BIN
app/tools/__pycache__/analyzing_ioc.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/analyzing_ioc.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/auto_updater.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/auto_updater.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/check_update.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/check_update.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/code_generator.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/code_generator.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/code_task_config.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/code_task_config.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/multi_thread_downloader.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/multi_thread_downloader.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/part_download.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/part_download.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/update_check_thread.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/update_check_thread.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/tools/__pycache__/update_code.cpython-39.pyc
Normal file
BIN
app/tools/__pycache__/update_code.cpython-39.pyc
Normal file
Binary file not shown.
321
app/tools/analyzing_ioc.py
Normal file
321
app/tools/analyzing_ioc.py
Normal 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
462
app/tools/auto_updater.py
Normal 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
74
app/tools/check_update.py
Normal 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
557
app/tools/code_generator.py
Normal 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._MEIPASS(PyInstaller的临时解包目录)
|
||||
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
|
||||
306
app/tools/code_task_config.py
Normal file
306
app/tools/code_task_config.py
Normal 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
|
||||
45
app/tools/part_download.py
Normal file
45
app/tools/part_download.py
Normal file
@ -0,0 +1,45 @@
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
import requests
|
||||
import shutil
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
class DownloadThread(QThread):
|
||||
progressChanged = pyqtSignal(int)
|
||||
finished = pyqtSignal(list, list) # success, fail
|
||||
|
||||
def __init__(self, files, server_url, secret_key, local_dir, parent=None):
|
||||
super().__init__(parent)
|
||||
self.files = files
|
||||
self.server_url = server_url
|
||||
self.secret_key = secret_key
|
||||
self.local_dir = local_dir
|
||||
|
||||
def run(self):
|
||||
success, fail = [], []
|
||||
total = len(self.files)
|
||||
max_retry = 3
|
||||
for idx, rel_path in enumerate(self.files):
|
||||
retry = 0
|
||||
while retry < max_retry:
|
||||
try:
|
||||
rel_path_unix = rel_path.replace("\\", "/")
|
||||
encoded_path = quote(rel_path_unix)
|
||||
url = f"{self.server_url}/download/{encoded_path}"
|
||||
params = {"key": self.secret_key}
|
||||
resp = requests.get(url, params=params, stream=True, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
local_path = os.path.join(self.local_dir, rel_path)
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
with open(local_path, "wb") as f:
|
||||
shutil.copyfileobj(resp.raw, f)
|
||||
success.append(rel_path)
|
||||
break
|
||||
else:
|
||||
retry += 1
|
||||
except Exception:
|
||||
retry += 1
|
||||
else:
|
||||
fail.append(rel_path)
|
||||
self.progressChanged.emit(int((idx + 1) / total * 100))
|
||||
self.finished.emit(success, fail)
|
||||
34
app/tools/update_check_thread.py
Normal file
34
app/tools/update_check_thread.py
Normal 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))
|
||||
143
app/tools/update_cmake_sources.py
Normal file
143
app/tools/update_cmake_sources.py
Normal 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
114
app/tools/update_code.py
Normal 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
|
||||
BIN
User/device/.DS_Store → assets/.DS_Store
vendored
BIN
User/device/.DS_Store → assets/.DS_Store
vendored
Binary file not shown.
BIN
User/bsp/.DS_Store → assets/User_code/.DS_Store
vendored
BIN
User/bsp/.DS_Store → assets/User_code/.DS_Store
vendored
Binary file not shown.
BIN
img/.DS_Store → assets/User_code/bsp/.DS_Store
vendored
BIN
img/.DS_Store → assets/User_code/bsp/.DS_Store
vendored
Binary file not shown.
@ -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
659
assets/User_code/bsp/can.c
Normal 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
259
assets/User_code/bsp/can.h
Normal 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 超时时间(毫秒),0为立即返回,osWaitForever为永久等待
|
||||
* @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
|
||||
11
assets/User_code/bsp/describe.csv
Normal file
11
assets/User_code/bsp/describe.csv
Normal 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
|
||||
|
||||
|
138
assets/User_code/bsp/dwt.c
Normal file
138
assets/User_code/bsp/dwt.c
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
53
assets/User_code/bsp/dwt.h
Normal file
53
assets/User_code/bsp/dwt.h
Normal 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_ */
|
||||
98
assets/User_code/bsp/gpio.c
Normal file
98
assets/User_code/bsp/gpio.c
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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 */
|
||||
@ -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
110
assets/User_code/bsp/pwm.c
Normal 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;
|
||||
}
|
||||
48
assets/User_code/bsp/pwm.h
Normal file
48
assets/User_code/bsp/pwm.h
Normal 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
|
||||
@ -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, ®, 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, ®, 1u, true);
|
||||
return BSP_SPI_Transmit(spi, data, size, true);
|
||||
}
|
||||
|
||||
/* USER FUNCTION BEGIN */
|
||||
|
||||
/* USER FUNCTION END */
|
||||
@ -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
|
||||
81
assets/User_code/bsp/time.c
Normal file
81
assets/User_code/bsp/time.c
Normal 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
Loading…
Reference in New Issue
Block a user