Compare commits

..

17 Commits

Author SHA1 Message Date
d214abb584 修复FDCAN模板加载路径,删除重复类定义
- 修复bsp_fdcan类的模板加载路径以支持新的文件夹结构
- 删除重复的bsp_fdcan类定义
- 确保从子文件夹正确加载FDCAN模板文件
2026-01-01 17:15:28 +08:00
eeb02a2de3 重构User_code目录结构:将文件组织到子文件夹中
主要更改:
- 将所有BSP外设文件移动到独立子文件夹(can/, fdcan/, uart/等)
- 将所有Component文件移动到独立子文件夹(pid/, filter/, cmd/等)
- 将所有Device文件移动到独立子文件夹(dr16/, bmi088/等)
- 更新代码生成器以支持新的文件夹结构
- 保持向后兼容性,支持从子文件夹或根目录加载模板
- 添加STRUCTURE.md文档说明新的目录结构

优势:
 更好的代码组织和管理
 便于添加、删除、修改模板
 清晰的模块划分
 向后兼容现有结构
2026-01-01 17:12:40 +08:00
daf0a28517 Merge fdcan-feature into main 2026-01-01 16:59:45 +08:00
3da379c832 添加fdcan 2026-01-01 16:58:36 +08:00
3e246f1de6 修复不根心的问题 2025-12-29 21:16:17 +08:00
ba291f2c28 更新fdcan 2025-12-28 18:58:37 +08:00
b2c24ef6bd 添加手册 2025-12-28 18:44:06 +08:00
8101d2c3a0 解决不更新 2025-12-19 22:43:01 +08:00
8f4636ab5a 添加了开发票 2025-11-29 12:04:26 +08:00
e2e275b6e4 修复 2025-11-25 21:25:41 +08:00
09c8ef7be8 优化导出 2025-11-25 21:12:34 +08:00
d9a02a8670 好了 2025-11-25 20:59:02 +08:00
77b9eb978d 有分类功能了 2025-11-25 20:45:10 +08:00
dd801f5d6a 准备加分类 2025-11-25 19:41:27 +08:00
b6a5d9e818 暂存 2025-11-25 19:13:17 +08:00
73aea915cf 暂存运营 2025-11-25 17:26:46 +08:00
485fa366cd 修代码 2025-11-17 22:23:15 +08:00
158 changed files with 8550 additions and 471 deletions

BIN
.DS_Store vendored

Binary file not shown.

22
.vscode/settings.json vendored
View File

@ -1,22 +0,0 @@
{
"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"
}
}

Binary file not shown.

BIN
MROBOT.docx Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,17 +1,16 @@
[Setup]
AppName=MRobot
AppVersion=1.0.1
AppVersion=1.0.8
DefaultDirName={userappdata}\MRobot
DefaultGroupName=MRobot
OutputDir=.
OutputBaseFilename=MRobotInstaller
[Files]
Source: "dist\MRobot.exe"; DestDir: "{app}"; Flags: ignoreversion
; 复制整个 dist\MRobot 文件夹onedir 模式生成的所有文件)
Source: "dist\MRobot\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; 复制 assets 资源文件到安装目录(支持后续更新)
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"

View File

@ -86,5 +86,5 @@
使用以下命令构建可执行文件:
```bash
pyinstaller MRobot.py --onefile --windowed --add-data "assets;assets" --add-data "app;app" --add-data "app/tools;app/tools"
pyinstaller MRobot.py --onefile --windowed --add-data "assets/logo;assets/logo" --add-data "app;app" --add-data "app/tools;app/tools"
```

Binary file not shown.

Binary file not shown.

211
app/batch_export_dialog.py Normal file
View File

@ -0,0 +1,211 @@
"""
批量导出选项对话框
"""
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QButtonGroup, QRadioButton, QFrame
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QPalette
from qfluentwidgets import (BodyLabel, PushButton, PrimaryPushButton, SubtitleLabel,
TitleLabel, HorizontalSeparator, CardWidget, FluentIcon, StrongBodyLabel,
theme, Theme)
class BatchExportDialog(QDialog):
"""批量导出选项对话框"""
EXPORT_NORMAL = 0 # 普通文件夹导出
EXPORT_MROBOT = 1 # MRobot 格式导出
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("导出选项")
self.setGeometry(200, 200, 680, 550)
self.setMinimumWidth(640)
self.setMinimumHeight(480)
# 设置背景色跟随主题
if theme() == Theme.DARK:
self.setStyleSheet("background-color: #232323;")
else:
self.setStyleSheet("background-color: #f7f9fc;")
self.export_type = self.EXPORT_NORMAL
self.init_ui()
def init_ui(self):
"""初始化UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(24, 24, 24, 24)
layout.setSpacing(16)
# 标题区域
title_layout = QVBoxLayout()
title_layout.setSpacing(8)
title_label = TitleLabel("选择导出方式")
title_layout.addWidget(title_label)
desc_label = BodyLabel("选择最适合您的导出格式")
title_layout.addWidget(desc_label)
layout.addLayout(title_layout)
layout.addWidget(HorizontalSeparator())
# 选项组
self.button_group = QButtonGroup()
# 普通导出选项卡
normal_card = self._create_option_card(
title="普通导出",
description="将每个交易的图片导出到单独的文件夹",
details="文件夹名称日期_金额\n每个交易的图片保存在独立文件夹中,便于查看和管理",
is_selected=True
)
normal_radio = normal_card.findChild(QRadioButton)
normal_radio.setChecked(True)
self.button_group.addButton(normal_radio, self.EXPORT_NORMAL)
layout.addWidget(normal_card)
# MRobot 格式导出选项卡
mrobot_card = self._create_option_card(
title="MRobot 专用格式",
description="导出为 .mrobot 文件ZIP 格式)",
details="包含完整的交易数据和图片\n用于转交给他人或备份",
is_selected=False
)
mrobot_radio = mrobot_card.findChild(QRadioButton)
self.button_group.addButton(mrobot_radio, self.EXPORT_MROBOT)
layout.addWidget(mrobot_card)
layout.addStretch()
# 按钮
btn_layout = QHBoxLayout()
btn_layout.setSpacing(12)
btn_layout.addStretch()
cancel_btn = PushButton("取消")
cancel_btn.setMinimumWidth(110)
cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(cancel_btn)
ok_btn = PrimaryPushButton("确定导出")
ok_btn.setMinimumWidth(110)
ok_btn.clicked.connect(self.on_ok)
btn_layout.addWidget(ok_btn)
layout.addLayout(btn_layout)
def _create_option_card(self, title, description, details, is_selected=False):
"""创建导出选项卡片"""
card = CardWidget()
card_layout = QHBoxLayout()
card_layout.setContentsMargins(16, 16, 16, 16)
card_layout.setSpacing(16)
# 单选按钮
radio = QRadioButton()
radio.setMinimumWidth(40)
card_layout.addWidget(radio)
# 内容区域
content_layout = QVBoxLayout()
content_layout.setSpacing(8)
# 标题行
title_layout = QHBoxLayout()
title_layout.setSpacing(10)
# 图标
icon_label = BodyLabel()
icon_label.setText("📁" if title == "普通导出" else "📦")
icon_label.setStyleSheet("font-size: 20px;")
title_layout.addWidget(icon_label)
# 标题
title_label = StrongBodyLabel(title)
title_layout.addWidget(title_label)
title_layout.addStretch()
content_layout.addLayout(title_layout)
# 描述
desc_label = BodyLabel(description)
desc_label.setWordWrap(True)
# 使用 QPalette 来自适应主题
from PyQt5.QtGui import QPalette
content_layout.addWidget(desc_label)
# 详细信息
details_label = BodyLabel(details)
details_label.setWordWrap(True)
# 使用相对颜色而不是硬编码
content_layout.addWidget(details_label)
content_layout.addStretch()
card_layout.addLayout(content_layout, 1)
# 设置卡片样式 - 不使用硬编码颜色,让 CardWidget 自适应主题
# 只通过边框来显示选中状态
self._update_card_style(card, is_selected)
card.setLayout(card_layout)
card.setMinimumHeight(120)
# 点击卡片时选中单选按钮
def on_card_clicked():
radio.setChecked(True)
# 更新卡片样式
self._update_card_styles(radio)
radio.clicked.connect(on_card_clicked)
card.mousePressEvent = lambda e: on_card_clicked()
return card
def _update_card_style(self, card, is_selected):
"""更新单个卡片的样式"""
if is_selected:
card.setProperty("is_selected", True)
card.setStyleSheet("""
CardWidget[is_selected=true] {
border: 2px solid palette(highlight);
}
CardWidget[is_selected=false] {
border: 1px solid palette(mid);
}
CardWidget[is_selected=false]:hover {
border: 2px solid palette(highlight);
}
""")
else:
card.setProperty("is_selected", False)
card.setStyleSheet("""
CardWidget[is_selected=false] {
border: 1px solid palette(mid);
}
CardWidget[is_selected=false]:hover {
border: 2px solid palette(highlight);
}
""")
def _update_card_styles(self, selected_radio):
"""更新所有卡片的样式"""
for button in self.button_group.buttons():
card = button.parent()
while card and not isinstance(card, CardWidget):
card = card.parent()
if card:
is_checked = button.isChecked()
self._update_card_style(card, is_checked)
def on_ok(self):
"""确定按钮点击"""
checked_button = self.button_group.checkedButton()
if checked_button:
self.export_type = self.button_group.id(checked_button)
self.accept()
def get_export_type(self):
"""获取选择的导出方式"""
return self.export_type

View File

@ -0,0 +1,394 @@
"""
分类管理对话框
提供新增重命名删除分类的功能
"""
from typing import Optional
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QMessageBox, QScrollArea, QWidget)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QColor, QFont
from qfluentwidgets import (BodyLabel, PushButton, PrimaryPushButton, LineEdit,
InfoBar, InfoBarPosition, SubtitleLabel, TitleLabel,
HorizontalSeparator, CardWidget, FluentIcon, StrongBodyLabel, theme, Theme)
from .tools.finance_manager import FinanceManager
class CategoryManagementDialog(QDialog):
"""分类管理对话框"""
def __init__(self, parent=None, finance_manager: Optional[FinanceManager] = None, account_id: Optional[str] = None):
super().__init__(parent)
self.finance_manager = finance_manager
self.account_id = account_id
self.setWindowTitle("分类管理")
self.setGeometry(100, 100, 650, 550)
self.setMinimumWidth(600)
self.setMinimumHeight(480)
# 设置背景色跟随主题
if theme() == Theme.DARK:
self.setStyleSheet("background-color: #232323;")
else:
self.setStyleSheet("background-color: #f7f9fc;")
self.init_ui()
def init_ui(self):
"""初始化UI"""
main_layout = QVBoxLayout()
main_layout.setContentsMargins(24, 24, 24, 24)
main_layout.setSpacing(16)
# 标题区域
title_layout = QVBoxLayout()
title_layout.setSpacing(6)
title_label = TitleLabel("分类管理")
title_layout.addWidget(title_label)
desc_label = BodyLabel("新增、编辑或删除您的交易分类")
title_layout.addWidget(desc_label)
main_layout.addLayout(title_layout)
main_layout.addWidget(HorizontalSeparator())
# 内容区域(分类列表)
content_layout = QHBoxLayout()
content_layout.setSpacing(16)
# 左侧:分类列表卡片
list_card = CardWidget()
list_card_layout = QVBoxLayout()
list_card_layout.setContentsMargins(0, 0, 0, 0)
list_card_layout.setSpacing(0)
list_label = StrongBodyLabel("现有分类")
list_label.setStyleSheet("padding: 12px 16px; border-bottom: 1px solid var(--border-color);")
list_card_layout.addWidget(list_label)
# 分类列表
self.category_list = QListWidget()
self.category_list.itemSelectionChanged.connect(self.on_category_selected)
self.category_list.setStyleSheet("""
QListWidget {
border: none;
background-color: transparent;
}
QListWidget::item {
padding: 10px 12px;
border-radius: 4px;
margin: 2px 4px;
}
QListWidget::item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
QListWidget::item:selected {
background-color: var(--highlight-color);
color: var(--highlight-text-color);
font-weight: bold;
}
""")
list_card_layout.addWidget(self.category_list, 1)
list_card.setLayout(list_card_layout)
list_card.setMinimumHeight(280)
content_layout.addWidget(list_card, 1)
main_layout.addLayout(content_layout, 1)
# 加载分类
self.load_categories()
# 按钮区域
btn_layout = QHBoxLayout()
btn_layout.setSpacing(12)
# 新增按钮
add_btn = PrimaryPushButton()
add_btn.setIcon(FluentIcon.ADD)
add_btn.setText("新增分类")
add_btn.clicked.connect(self.on_add_category)
add_btn.setMinimumWidth(120)
btn_layout.addWidget(add_btn)
# 重命名按钮
self.rename_btn = PushButton()
self.rename_btn.setIcon(FluentIcon.EDIT)
self.rename_btn.setText("重命名")
self.rename_btn.clicked.connect(self.on_rename_category)
self.rename_btn.setEnabled(False)
self.rename_btn.setMinimumWidth(110)
btn_layout.addWidget(self.rename_btn)
# 删除按钮
self.delete_btn = PushButton()
self.delete_btn.setIcon(FluentIcon.DELETE)
self.delete_btn.setText("删除")
self.delete_btn.clicked.connect(self.on_delete_category)
self.delete_btn.setEnabled(False)
self.delete_btn.setMinimumWidth(110)
btn_layout.addWidget(self.delete_btn)
btn_layout.addStretch()
# 关闭按钮
close_btn = PushButton("关闭")
close_btn.clicked.connect(self.accept)
close_btn.setMinimumWidth(110)
btn_layout.addWidget(close_btn)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def load_categories(self):
"""加载分类列表"""
if not self.finance_manager or not self.account_id:
return
self.category_list.clear()
categories = self.finance_manager.get_categories(self.account_id)
for category in categories:
item = QListWidgetItem(category)
self.category_list.addItem(item)
def on_category_selected(self):
"""分类被选择"""
has_selection = self.category_list.currentItem() is not None
self.rename_btn.setEnabled(has_selection)
self.delete_btn.setEnabled(has_selection)
def on_add_category(self):
"""新增分类"""
# 弹出输入对话框
from PyQt5.QtWidgets import QDialog as QStdDialog
dialog = QStdDialog(self)
dialog.setWindowTitle("新增分类")
dialog.setGeometry(150, 150, 450, 220)
dialog.setStyleSheet("""
QDialog {
background-color: white;
}
""")
layout = QVBoxLayout(dialog)
layout.setContentsMargins(24, 24, 24, 24)
layout.setSpacing(16)
# 标题
title = StrongBodyLabel("创建新分类")
layout.addWidget(title)
# 说明文字
desc = BodyLabel("请输入分类名称")
desc.setStyleSheet("color: #606366;")
layout.addWidget(desc)
# 输入框
input_edit = LineEdit()
input_edit.setPlaceholderText("例如:食品、交通、娱乐、购物等")
input_edit.setMinimumHeight(40)
layout.addWidget(input_edit)
# 按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
def on_create():
category_name = input_edit.text().strip()
if not category_name:
InfoBar.warning(
title="提示",
content="分类名称不能为空",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
return
if self.finance_manager.add_category(self.account_id, category_name):
InfoBar.success(
title="成功",
content=f"分类 '{category_name}' 创建成功",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
self.load_categories()
dialog.accept()
else:
InfoBar.warning(
title="提示",
content="分类已存在",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
cancel_btn = PushButton("取消")
cancel_btn.setMinimumWidth(100)
cancel_btn.clicked.connect(dialog.reject)
btn_layout.addWidget(cancel_btn)
create_btn = PrimaryPushButton("创建")
create_btn.setMinimumWidth(100)
create_btn.clicked.connect(on_create)
btn_layout.addWidget(create_btn)
layout.addSpacing(12)
layout.addLayout(btn_layout)
# 回车快速创建
input_edit.returnPressed.connect(on_create)
dialog.exec()
def on_rename_category(self):
"""重命名分类"""
current_item = self.category_list.currentItem()
if not current_item:
return
old_name = current_item.text()
# 弹出输入对话框
from PyQt5.QtWidgets import QDialog as QStdDialog
dialog = QStdDialog(self)
dialog.setWindowTitle("重命名分类")
dialog.setGeometry(150, 150, 450, 280)
dialog.setStyleSheet("""
QDialog {
background-color: white;
}
""")
layout = QVBoxLayout(dialog)
layout.setContentsMargins(24, 24, 24, 24)
layout.setSpacing(16)
# 标题
title = StrongBodyLabel("重命名分类")
layout.addWidget(title)
# 原名称显示
old_info_layout = QHBoxLayout()
old_label = BodyLabel("原分类名:")
old_label.setMinimumWidth(80)
old_value = StrongBodyLabel(old_name)
old_value.setStyleSheet("color: #1976d2;")
old_info_layout.addWidget(old_label)
old_info_layout.addWidget(old_value)
old_info_layout.addStretch()
layout.addLayout(old_info_layout)
# 新名称输入
new_label = BodyLabel("新分类名:")
new_label.setMinimumWidth(80)
layout.addWidget(new_label)
input_edit = LineEdit()
input_edit.setText(old_name)
input_edit.selectAll()
input_edit.setMinimumHeight(40)
layout.addWidget(input_edit)
# 按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
def on_rename():
new_name = input_edit.text().strip()
if not new_name:
InfoBar.warning(
title="提示",
content="分类名称不能为空",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
return
if new_name == old_name:
dialog.accept()
return
if self.finance_manager.rename_category(self.account_id, old_name, new_name):
InfoBar.success(
title="成功",
content=f"分类已重命名为 '{new_name}'",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
self.load_categories()
dialog.accept()
else:
InfoBar.warning(
title="提示",
content="重命名失败,可能分类已存在",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
cancel_btn = PushButton("取消")
cancel_btn.setMinimumWidth(100)
cancel_btn.clicked.connect(dialog.reject)
btn_layout.addWidget(cancel_btn)
rename_btn = PrimaryPushButton("重命名")
rename_btn.setMinimumWidth(100)
rename_btn.clicked.connect(on_rename)
btn_layout.addWidget(rename_btn)
layout.addSpacing(12)
layout.addLayout(btn_layout)
# 回车快速重命名
input_edit.returnPressed.connect(on_rename)
dialog.exec()
def on_delete_category(self):
"""删除分类"""
current_item = self.category_list.currentItem()
if not current_item:
return
category_name = current_item.text()
# 确认删除
reply = QMessageBox.question(
self,
"确认删除",
f"确定要删除分类 '{category_name}' 吗?\n\n使用该分类的交易记录分类将被清空。",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
if self.finance_manager.delete_category(self.account_id, category_name):
InfoBar.success(
title="成功",
content=f"分类 '{category_name}' 已删除",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)
self.load_categories()
else:
InfoBar.warning(
title="错误",
content="删除分类失败",
isClosable=True,
position=InfoBarPosition.TOP,
duration=2000,
parent=self
)

View File

@ -61,9 +61,17 @@ class BspSimplePeripheral(QWidget):
return "skipped" # 返回特殊值表示跳过
return "not_needed" # 返回特殊值表示不需要生成
template_dir = CodeGenerator.get_template_dir()
# 使用外设名称作为子文件夹名(小写)
periph_folder = self.peripheral_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
for key, filename in self.template_names.items():
template_path = os.path.join(template_dir, filename)
# 先尝试从子文件夹加载
template_path = os.path.join(template_base_dir, periph_folder, filename)
if not os.path.exists(template_path):
# 如果子文件夹不存在,尝试从根目录加载(向后兼容)
template_path = os.path.join(template_base_dir, filename)
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
@ -206,7 +214,15 @@ class BspPeripheralBase(QWidget):
return True
def _generate_header_file(self, configs, template_dir):
template_path = os.path.join(template_dir, self.template_names['header'])
# 构建模板路径,优先从子文件夹读取
periph_folder = self.peripheral_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, periph_folder, self.template_names['header'])
if not os.path.exists(template_path):
# 向后兼容:尝试从根目录读取
template_path = os.path.join(template_base_dir, self.template_names['header'])
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
@ -318,6 +334,14 @@ def get_available_can(project_path):
return analyzing_ioc.get_enabled_can_from_ioc(ioc_path)
return []
def get_available_fdcan(project_path):
"""获取可用的FDCAN列表"""
ioc_files = [f for f in os.listdir(project_path) if f.endswith('.ioc')]
if ioc_files:
ioc_path = os.path.join(project_path, ioc_files[0])
return analyzing_ioc.get_enabled_fdcan_from_ioc(ioc_path)
return []
def get_available_spi(project_path):
ioc_files = [f for f in os.listdir(project_path) if f.endswith('.ioc')]
if ioc_files:
@ -605,6 +629,199 @@ class bsp_can(BspPeripheralBase):
])
filter_bank += 1 # 为下一个CAN分配不同的过滤器组
class bsp_fdcan(BspPeripheralBase):
def __init__(self, project_path):
super().__init__(
project_path,
"FDCAN",
{'header': 'fdcan.h', 'source': 'fdcan.c'},
"BSP_FDCAN",
"hfdcan",
"fdcan",
get_available_fdcan
)
def _generate_header_file(self, configs, template_dir):
"""重写头文件生成,添加 FDCAN 使能和 FIFO 分配定义"""
# 从子文件夹加载模板
periph_folder = self.peripheral_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, periph_folder, self.template_names['header'])
if not os.path.exists(template_path):
# 向后兼容:尝试从根目录读取
template_path = os.path.join(template_base_dir, self.template_names['header'])
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
# 生成枚举
enum_lines = [f" {self.enum_prefix}_{name}," for name, _ in configs]
content = CodeGenerator.replace_auto_generated(
template_content, f"AUTO GENERATED {self.enum_prefix}_NAME", "\n".join(enum_lines)
)
# 生成 FDCAN 使能宏和 FIFO 分配
enable_lines = []
fdcan_count = len(configs)
# 根据 FDCAN 数量分配 FIFO与 _generate_source_file 中的逻辑一致)
if fdcan_count == 1:
fifo_map = {0: 0}
elif fdcan_count == 2:
fifo_map = {0: 0, 1: 1}
else: # >= 3
fifo_map = {0: 0, 1: 0, 2: 1}
for idx, (name, instance) in enumerate(configs):
num = ''.join(filter(str.isdigit, instance))
fifo_idx = fifo_map.get(idx, 1)
enable_lines.append(f"#define FDCAN{num}_EN")
enable_lines.append(f"#define FDCAN{num}_RX_FIFO {fifo_idx}")
content = CodeGenerator.replace_auto_generated(
content, "AUTO GENERATED FDCAN_ENABLE", "\n".join(enable_lines)
)
output_path = os.path.join(self.project_path, f"User/bsp/{self.template_names['header']}")
CodeGenerator.save_with_preserve(output_path, content)
return True
def _generate_source_file(self, configs, template_dir):
# 从子文件夹加载模板
periph_folder = self.peripheral_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, periph_folder, self.template_names['source'])
if not os.path.exists(template_path):
# 向后兼容:尝试从根目录读取
template_path = os.path.join(template_base_dir, self.template_names['source'])
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
# FDCAN_Get函数
get_lines = []
for idx, (name, instance) in enumerate(configs):
if idx == 0:
get_lines.append(f" if (hfdcan->Instance == {instance})")
else:
get_lines.append(f" else if (hfdcan->Instance == {instance})")
get_lines.append(f" return {self.enum_prefix}_{name};")
content = CodeGenerator.replace_auto_generated(
template_content, "AUTO GENERATED FDCAN_GET", "\n".join(get_lines)
)
# Handle函数
handle_lines = []
for name, instance in configs:
num = ''.join(filter(str.isdigit, instance)) # 提取数字
handle_lines.append(f" case {self.enum_prefix}_{name}:")
handle_lines.append(f" return &hfdcan{num};")
content = CodeGenerator.replace_auto_generated(
content, f"AUTO GENERATED {self.enum_prefix}_GET_HANDLE", "\n".join(handle_lines)
)
# 生成FDCAN初始化代码类似CAN的策略
init_lines = []
fdcan_instances = [instance for _, instance in configs]
fdcan_count = len(fdcan_instances)
# 根据FDCAN数量分配FIFO
if fdcan_count == 1:
self._generate_single_fdcan_init(init_lines, configs, 0)
elif fdcan_count == 2:
self._generate_dual_fdcan_init(init_lines, configs)
elif fdcan_count >= 3:
self._generate_multi_fdcan_init(init_lines, configs)
content = CodeGenerator.replace_auto_generated(
content, "AUTO GENERATED FDCAN_INIT", "\n".join(init_lines)
)
output_path = os.path.join(self.project_path, f"User/bsp/{self.template_names['source']}")
CodeGenerator.save_with_preserve(output_path, content)
return True
def _generate_single_fdcan_init(self, init_lines, configs, fifo_idx):
"""单个FDCAN使用指定的FIFO"""
for name, instance in configs:
num = ''.join(filter(str.isdigit, instance))
init_lines.append(f"#ifdef FDCAN{num}_EN")
init_lines.append(f" {{")
init_lines.append(f" FDCAN_HandleTypeDef hfdcan = hfdcan{num};")
init_lines.append(f" FDCAN_FilterTypeDef sFilterConfig = {{0}};")
init_lines.append(f" #define FDCANX_RX_FIFO {fifo_idx}")
init_lines.append(f" #ifdef FDCAN{num}_FILTER_CONFIG_TABLE")
init_lines.append(f" FDCAN{num}_FILTER_CONFIG_TABLE(FDCAN_CONFIG_FILTER)")
init_lines.append(f" #endif")
init_lines.append(f" #ifdef FDCAN{num}_GLOBAL_FILTER")
init_lines.append(f" HAL_FDCAN_ConfigGlobalFilter(&hfdcan{num}, FDCAN{num}_GLOBAL_FILTER);")
init_lines.append(f" #endif")
init_lines.append(f" HAL_FDCAN_ActivateNotification(&hfdcan{num}, FDCANx_NOTIFY_FLAGS(FDCANX_RX_FIFO), 0);")
init_lines.append(f" BSP_FDCAN_RegisterCallback({self.enum_prefix}_{name}, FDCANX_MSG_PENDING_CB(FDCANX_RX_FIFO), BSP_FDCAN_RxFifo{fifo_idx}Callback);")
init_lines.append(f" BSP_FDCAN_RegisterCallback({self.enum_prefix}_{name}, HAL_FDCAN_TX_BUFFER_COMPLETE_CB, BSP_FDCAN_TxCompleteCallback);")
init_lines.append(f" #undef FDCANX_RX_FIFO")
init_lines.append(f" HAL_FDCAN_Start(&hfdcan{num});")
init_lines.append(f" }}")
init_lines.append(f"#endif")
init_lines.append("")
def _generate_dual_fdcan_init(self, init_lines, configs):
"""双FDCANFDCAN1用FIFO0FDCAN2用FIFO1"""
fifo_map = {0: 0, 1: 1}
for idx, (name, instance) in enumerate(configs):
num = ''.join(filter(str.isdigit, instance))
fifo_idx = fifo_map[idx]
init_lines.append(f"#ifdef FDCAN{num}_EN")
init_lines.append(f" {{")
init_lines.append(f" FDCAN_HandleTypeDef hfdcan = hfdcan{num};")
init_lines.append(f" FDCAN_FilterTypeDef sFilterConfig = {{0}};")
init_lines.append(f" #define FDCANX_RX_FIFO {fifo_idx}")
init_lines.append(f" #ifdef FDCAN{num}_FILTER_CONFIG_TABLE")
init_lines.append(f" FDCAN{num}_FILTER_CONFIG_TABLE(FDCAN_CONFIG_FILTER)")
init_lines.append(f" #endif")
init_lines.append(f" #ifdef FDCAN{num}_GLOBAL_FILTER")
init_lines.append(f" HAL_FDCAN_ConfigGlobalFilter(&hfdcan{num}, FDCAN{num}_GLOBAL_FILTER);")
init_lines.append(f" #endif")
init_lines.append(f" HAL_FDCAN_ActivateNotification(&hfdcan{num}, FDCANx_NOTIFY_FLAGS(FDCANX_RX_FIFO), 0);")
init_lines.append(f" BSP_FDCAN_RegisterCallback({self.enum_prefix}_{name}, FDCANX_MSG_PENDING_CB(FDCANX_RX_FIFO), BSP_FDCAN_RxFifo{fifo_idx}Callback);")
init_lines.append(f" BSP_FDCAN_RegisterCallback({self.enum_prefix}_{name}, HAL_FDCAN_TX_BUFFER_COMPLETE_CB, BSP_FDCAN_TxCompleteCallback);")
init_lines.append(f" #undef FDCANX_RX_FIFO")
init_lines.append(f" HAL_FDCAN_Start(&hfdcan{num});")
init_lines.append(f" }}")
init_lines.append(f"#endif")
init_lines.append("")
def _generate_multi_fdcan_init(self, init_lines, configs):
"""多FDCANFDCAN1和FDCAN2用FIFO0FDCAN3用FIFO1"""
fifo_map = {0: 0, 1: 0, 2: 1}
for idx, (name, instance) in enumerate(configs):
num = ''.join(filter(str.isdigit, instance))
fifo_idx = fifo_map.get(idx, 1)
init_lines.append(f"#ifdef FDCAN{num}_EN")
init_lines.append(f" {{")
init_lines.append(f" FDCAN_HandleTypeDef hfdcan = hfdcan{num};")
init_lines.append(f" FDCAN_FilterTypeDef sFilterConfig = {{0}};")
init_lines.append(f" #define FDCANX_RX_FIFO {fifo_idx}")
init_lines.append(f" #ifdef FDCAN{num}_FILTER_CONFIG_TABLE")
init_lines.append(f" FDCAN{num}_FILTER_CONFIG_TABLE(FDCAN_CONFIG_FILTER)")
init_lines.append(f" #endif")
init_lines.append(f" #ifdef FDCAN{num}_GLOBAL_FILTER")
init_lines.append(f" HAL_FDCAN_ConfigGlobalFilter(&hfdcan{num}, FDCAN{num}_GLOBAL_FILTER);")
init_lines.append(f" #endif")
init_lines.append(f" HAL_FDCAN_ActivateNotification(&hfdcan{num}, FDCANx_NOTIFY_FLAGS(FDCANX_RX_FIFO), 0);")
init_lines.append(f" BSP_FDCAN_RegisterCallback({self.enum_prefix}_{name}, FDCANX_MSG_PENDING_CB(FDCANX_RX_FIFO), BSP_FDCAN_RxFifo{fifo_idx}Callback);")
init_lines.append(f" BSP_FDCAN_RegisterCallback({self.enum_prefix}_{name}, HAL_FDCAN_TX_BUFFER_COMPLETE_CB, BSP_FDCAN_TxCompleteCallback);")
init_lines.append(f" #undef FDCANX_RX_FIFO")
init_lines.append(f" HAL_FDCAN_Start(&hfdcan{num});")
init_lines.append(f" }}")
init_lines.append(f"#endif")
init_lines.append("")
class bsp_spi(BspPeripheralBase):
def __init__(self, project_path):
super().__init__(
@ -1166,6 +1383,7 @@ def get_bsp_page(peripheral_name, project_path):
special_classes = {
"i2c": bsp_i2c,
"can": bsp_can,
"fdcan": bsp_fdcan,
"spi": bsp_spi,
"uart": bsp_uart,
"gpio": bsp_gpio,

View File

@ -107,9 +107,17 @@ class ComponentSimple(QWidget):
return "skipped" # 返回特殊值表示跳过
return "not_needed" # 返回特殊值表示不需要生成
template_dir = self._get_component_template_dir()
# 使用组件名称作为子文件夹名(小写)
comp_folder = self.component_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/component")
for key, filename in self.template_names.items():
template_path = os.path.join(template_dir, filename)
# 先尝试从子文件夹加载
template_path = os.path.join(template_base_dir, comp_folder, filename)
if not os.path.exists(template_path):
# 如果子文件夹不存在,尝试从根目录加载(向后兼容)
template_path = os.path.join(template_base_dir, filename)
template_content = CodeGenerator.load_template(template_path)
if not template_content:
print(f"模板文件不存在或为空: {template_path}")

View File

@ -240,12 +240,18 @@ class DeviceSimple(QWidget):
# 获取BSP配置
bsp_config = self.get_bsp_config()
# 复制并修改文件
template_dir = self._get_device_template_dir()
# 使用设备名称作为子文件夹名(小写)
device_folder = self.device_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/device")
files = self.device_config.get('files', {})
for file_type, filename in files.items():
src_path = os.path.join(template_dir, filename)
# 先尝试从子文件夹加载
src_path = os.path.join(template_base_dir, device_folder, filename)
if not os.path.exists(src_path):
# 如果子文件夹不存在,尝试从根目录加载(向后兼容)
src_path = os.path.join(template_base_dir, filename)
dst_path = os.path.join(self.project_path, f"User/device/{filename}")
if os.path.exists(src_path):

View File

@ -263,8 +263,14 @@ class DataInterface(QWidget):
def update_user_template(self):
from app.tools.update_code import update_code
from app.tools.code_generator import CodeGenerator
def info_callback(parent):
# 清除 CodeGenerator 的缓存,强制重新读取更新后的文件
CodeGenerator._assets_dir_cache = None
CodeGenerator._assets_dir_initialized = False
CodeGenerator._template_dir_logged = False
InfoBar.success(
title="更新成功",
content="用户模板已更新到最新版本!",
@ -272,6 +278,10 @@ class DataInterface(QWidget):
duration=2000
)
# 如果当前在代码生成页面,刷新文件列表
if self.stacked_layout.currentWidget() == self.codegen_page:
self.show_user_code_files()
def error_callback(parent, msg):
InfoBar.error(
title="更新失败",
@ -287,6 +297,10 @@ class DataInterface(QWidget):
file_tree = self.codegen_page.file_tree
file_tree.clear()
base_dir = CodeGenerator.get_assets_dir("User_code")
print(f"显示用户代码文件base_dir = {base_dir}")
print(f"目录是否存在: {os.path.exists(base_dir)}")
if os.path.exists(base_dir):
print(f"目录内容: {os.listdir(base_dir)}")
user_dir = os.path.join(self.project_path, "User")
sub_dirs = ["bsp", "component", "device", "module"]

1658
app/finance_interface.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,20 @@ import sys
import os
def resource_path(relative_path):
"""获取资源文件的绝对路径,兼容打包和开发环境"""
"""获取资源文件的绝对路径,兼容打包和开发环境
对于 logo 文件使用打包的临时目录只有 logo 被打包
对于其他资源使用可执行文件所在目录
"""
if getattr(sys, 'frozen', False):
# 打包环境
if 'logo' in relative_path:
# logo 文件使用打包的临时目录
if hasattr(sys, '_MEIPASS'):
# PyInstaller 打包后的临时目录
return os.path.join(sys._MEIPASS, relative_path)
# 其他资源使用可执行文件所在目录
exe_dir = os.path.dirname(sys.executable)
return os.path.join(exe_dir, relative_path)
# 开发环境
return os.path.join(os.path.abspath("."), relative_path)
class HomeInterface(QWidget):

View File

@ -1,10 +1,10 @@
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtCore import Qt, QSize, QTimer
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 NavigationItemPosition, FluentWindow, SplashScreen, setThemeColor, NavigationBarPushButton, toggleTheme, setTheme, Theme, NavigationAvatarWidget, NavigationToolButton ,NavigationPushButton, theme
from qfluentwidgets import FluentIcon as FIF
from qfluentwidgets import InfoBar, InfoBarPosition
@ -14,6 +14,7 @@ from .part_library_interface import PartLibraryInterface
from .data_interface import DataInterface
from .mini_tool_interface import MiniToolInterface
from .code_configuration_interface import CodeConfigurationInterface
from .finance_interface import FinanceInterface
from .about_interface import AboutInterface
import base64
@ -52,6 +53,7 @@ class MainWindow(FluentWindow):
# self.dataInterface = DataInterface(self)
self.miniToolInterface = MiniToolInterface(self)
self.codeConfigurationInterface = CodeConfigurationInterface(self)
self.financeInterface = FinanceInterface(self)
def initNavigation(self):
@ -60,13 +62,14 @@ class MainWindow(FluentWindow):
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.financeInterface, FIF.DOCUMENT, 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.themeBtn.clicked.connect(self._safe_toggle_theme)
self.navigationInterface.addWidget(
'themeButton',
self.themeBtn,
@ -74,6 +77,36 @@ class MainWindow(FluentWindow):
NavigationItemPosition.BOTTOM
)
def _safe_toggle_theme(self):
"""安全地切换主题,避免字典迭代异常"""
def safe_toggle():
try:
import sys
from io import StringIO
# 捕获 stderr 以抑制库内的异常消息
old_stderr = sys.stderr
sys.stderr = StringIO()
try:
# 获取当前主题
current_theme = theme()
# 根据当前主题切换到另一个
new_theme = Theme.LIGHT if current_theme == Theme.DARK else Theme.DARK
setTheme(new_theme, save=True, lazy=True)
finally:
# 恢复 stderr
sys.stderr = old_stderr
except Exception as e:
# 其他异常仍然打印,但忽略字典迭代异常
error_msg = str(e)
if "dictionary changed size during iteration" not in error_msg:
print(f"主题切换失败: {e}")
# 在下一个事件循环中执行切换,让 Qt 完成当前事件处理
QTimer.singleShot(50, safe_toggle)
def check_updates_in_background(self):
"""后台检查更新"""
try:

Binary file not shown.

View File

@ -71,7 +71,7 @@ class analyzing_ioc:
@staticmethod
def get_enabled_can_from_ioc(ioc_path):
"""
获取已启用的CAN列表
获取已启用的CAN列表不包括FDCAN
返回格式: ['CAN1', 'CAN2']
"""
enabled_can = []
@ -84,12 +84,35 @@ class analyzing_ioc:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
if key.startswith('Mcu.IP') and value.startswith('CAN'):
# 只匹配CAN不包括FDCAN
if key.startswith('Mcu.IP') and value.startswith('CAN') and not value.startswith('FDCAN'):
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_fdcan_from_ioc(ioc_path):
"""
获取已启用的FDCAN列表
返回格式: ['FDCAN1', 'FDCAN2', 'FDCAN3']
"""
enabled_fdcan = []
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('FDCAN'):
fdcan_name = value.split('.')[0] if '.' in value else value
if fdcan_name not in enabled_fdcan:
enabled_fdcan.append(fdcan_name)
return sorted(enabled_fdcan)
@staticmethod
def get_enabled_uart_from_ioc(ioc_path):
"""

View File

@ -97,29 +97,19 @@ class CodeGenerator:
assets_dir = ""
if getattr(sys, 'frozen', False):
# 打包后的环境
print("检测到打包环境")
# 优先使用sys._MEIPASSPyInstaller的临时解包目录
if hasattr(sys, '_MEIPASS'):
base_path = getattr(sys, '_MEIPASS')
assets_dir = os.path.join(base_path, "assets")
print(f"使用PyInstaller临时目录: {assets_dir}")
else:
# 后备方案:使用可执行文件所在目录
# 打包后的环境 - 始终使用可执行文件所在目录
# 这样可以使用安装目录下的文件,而不是打包进去的文件
exe_dir = os.path.dirname(sys.executable)
assets_dir = os.path.join(exe_dir, "assets")
print(f"使用可执行文件目录: {assets_dir}")
print(f"打包环境:使用可执行文件目录: {assets_dir}")
# 如果都不存在,尝试其他可能的位置
# 如果assets目录不存在创建它
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}")
try:
os.makedirs(assets_dir, exist_ok=True)
print(f"创建assets目录: {assets_dir}")
except Exception as e:
print(f"创建assets目录失败: {e}")
else:
# 开发环境
current_dir = os.path.dirname(os.path.abspath(__file__))

View File

@ -0,0 +1,738 @@
"""
财务做账模块 - 数据管理系统
管理所有财务账目图片文件等数据的存储和检索
"""
import os
import json
import shutil
import zipfile
import uuid
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional, Tuple, Union
from enum import Enum
class TransactionType(Enum):
"""交易类型"""
INVOICE = "invoice" # 发票
PAYMENT = "payment" # 支付记录
PURCHASE = "purchase" # 购买记录
class Transaction:
"""单个交易记录数据模型"""
def __init__(self, trans_id: Optional[str] = None, date: Optional[str] = None, amount: float = 0.0,
trader: str = "", notes: str = "", invoice_path: Optional[str] = None,
payment_path: Optional[str] = None, purchase_path: Optional[str] = None,
category: str = ""):
self.id = trans_id or str(uuid.uuid4())
self.date = date or datetime.now().strftime("%Y-%m-%d")
self.amount = amount
self.trader = trader
self.notes = notes
self.invoice_path = invoice_path # 相对路径
self.payment_path = payment_path
self.purchase_path = purchase_path
self.category = category # 交易分类,用户自定义
self.created_at = datetime.now().isoformat()
self.updated_at = datetime.now().isoformat()
def to_dict(self) -> dict:
"""转换为字典格式用于JSON序列化"""
return {
'id': self.id,
'date': self.date,
'amount': self.amount,
'trader': self.trader,
'notes': self.notes,
'invoice_path': self.invoice_path,
'payment_path': self.payment_path,
'purchase_path': self.purchase_path,
'category': self.category,
'created_at': self.created_at,
'updated_at': self.updated_at
}
@classmethod
def from_dict(cls, data: dict) -> 'Transaction':
"""从字典创建Transaction对象"""
trans = cls(
trans_id=data.get('id'),
date=data.get('date'),
amount=data.get('amount', 0.0),
trader=data.get('trader', ''),
notes=data.get('notes', ''),
invoice_path=data.get('invoice_path'),
payment_path=data.get('payment_path'),
purchase_path=data.get('purchase_path'),
category=data.get('category', '')
)
if 'created_at' in data:
trans.created_at = data['created_at']
if 'updated_at' in data:
trans.updated_at = data['updated_at']
return trans
class Account:
"""账户数据模型"""
def __init__(self, account_id: Optional[str] = None, account_name: str = "", description: str = ""):
self.id = account_id or str(uuid.uuid4())
self.name = account_name
self.description = description
self.transactions: List[Transaction] = []
self.categories: List[str] = [] # 空列表,用户自定义分类
self.created_at = datetime.now().isoformat()
self.updated_at = datetime.now().isoformat()
def add_transaction(self, transaction: Transaction) -> None:
"""添加交易记录"""
self.transactions.append(transaction)
self.updated_at = datetime.now().isoformat()
def remove_transaction(self, trans_id: str) -> bool:
"""移除交易记录"""
original_len = len(self.transactions)
self.transactions = [t for t in self.transactions if t.id != trans_id]
if len(self.transactions) < original_len:
self.updated_at = datetime.now().isoformat()
return True
return False
def get_transaction(self, trans_id: str) -> Optional[Transaction]:
"""获取单个交易记录"""
for t in self.transactions:
if t.id == trans_id:
return t
return None
def to_dict(self) -> dict:
"""转换为字典"""
return {
'id': self.id,
'name': self.name,
'description': self.description,
'categories': self.categories,
'transactions': [t.to_dict() for t in self.transactions],
'created_at': self.created_at,
'updated_at': self.updated_at
}
@classmethod
def from_dict(cls, data: dict) -> 'Account':
"""从字典创建Account对象"""
account = cls(
account_id=data.get('id'),
account_name=data.get('name', ''),
description=data.get('description', '')
)
account.categories = data.get('categories', []) # 使用存储的分类,如果没有则为空列表
account.transactions = [Transaction.from_dict(t) for t in data.get('transactions', [])]
if 'created_at' in data:
account.created_at = data['created_at']
if 'updated_at' in data:
account.updated_at = data['updated_at']
return account
class FinanceManager:
"""财务管理系统 - 处理所有数据操作和文件管理"""
def __init__(self, data_root: Optional[str] = None):
"""初始化财务管理系统
Args:
data_root: 数据存储根目录默认为 assets/Finance_Data
"""
if data_root:
self.data_root = Path(data_root)
else:
# 获取项目根目录
import os
current_dir = Path(os.getcwd())
self.data_root = current_dir / "assets" / "Finance_Data"
self._ensure_directory_structure()
self.accounts: Dict[str, Account] = {}
self.load_all_accounts()
# 如果没有账户,自动创建 admin 账户
if len(self.accounts) == 0:
self.create_account(
account_name="admin",
description="默认管理账户"
)
def _ensure_directory_structure(self) -> None:
"""确保目录结构完整"""
self.data_root.mkdir(parents=True, exist_ok=True)
# 创建子目录
subdirs = ['accounts', 'backups', 'images', 'invoices', 'payments', 'purchases']
for subdir in subdirs:
(self.data_root / subdir).mkdir(exist_ok=True)
def _get_account_dir(self, account_id: str) -> Path:
"""获取账户目录"""
account_dir = self.data_root / 'accounts' / account_id
account_dir.mkdir(parents=True, exist_ok=True)
return account_dir
def _get_transaction_dir(self, account_id: str, trans_id: str) -> Path:
"""获取交易记录目录"""
trans_dir = self._get_account_dir(account_id) / trans_id
trans_dir.mkdir(parents=True, exist_ok=True)
return trans_dir
def _save_account_metadata(self, account: Account) -> None:
"""保存账户元数据(不包含交易详情)"""
account_dir = self._get_account_dir(account.id)
metadata_file = account_dir / 'metadata.json'
metadata = {
'id': account.id,
'name': account.name,
'description': account.description,
'categories': account.categories,
'created_at': account.created_at,
'updated_at': account.updated_at
}
with open(metadata_file, 'w', encoding='utf-8') as f:
json.dump(metadata, f, ensure_ascii=False, indent=2)
def _load_account_metadata(self, account_id: str) -> Optional[dict]:
"""加载账户元数据"""
metadata_file = self._get_account_dir(account_id) / 'metadata.json'
if not metadata_file.exists():
return None
with open(metadata_file, 'r', encoding='utf-8') as f:
return json.load(f)
def _save_transaction_data(self, account_id: str, transaction: Transaction) -> None:
"""保存交易记录数据"""
trans_dir = self._get_transaction_dir(account_id, transaction.id)
data_file = trans_dir / 'data.json'
with open(data_file, 'w', encoding='utf-8') as f:
json.dump(transaction.to_dict(), f, ensure_ascii=False, indent=2)
def _load_transaction_data(self, account_id: str, trans_id: str) -> Optional[Transaction]:
"""加载交易记录数据"""
data_file = self._get_transaction_dir(account_id, trans_id) / 'data.json'
if not data_file.exists():
return None
with open(data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return Transaction.from_dict(data)
def create_account(self, account_name: str, description: str = "") -> Account:
"""创建新账户"""
account = Account(account_name=account_name, description=description)
self.accounts[account.id] = account
self._save_account_metadata(account)
return account
def get_account(self, account_id: str) -> Optional[Account]:
"""获取账户"""
return self.accounts.get(account_id)
def get_all_accounts(self) -> List[Account]:
"""获取所有账户"""
return list(self.accounts.values())
def delete_account(self, account_id: str) -> bool:
"""删除账户及其所有数据"""
if account_id not in self.accounts:
return False
account_dir = self._get_account_dir(account_id)
try:
shutil.rmtree(account_dir)
del self.accounts[account_id]
return True
except Exception as e:
print(f"删除账户出错: {e}")
return False
def update_account(self, account_id: str, account_name: Optional[str] = None, description: Optional[str] = None) -> bool:
"""更新账户信息"""
account = self.accounts.get(account_id)
if not account:
return False
if account_name:
account.name = account_name
if description is not None:
account.description = description
account.updated_at = datetime.now().isoformat()
self._save_account_metadata(account)
return True
def add_transaction(self, account_id: str, transaction: Transaction) -> bool:
"""添加交易记录到账户"""
account = self.accounts.get(account_id)
if not account:
return False
account.add_transaction(transaction)
self._save_transaction_data(account_id, transaction)
return True
def save_image_for_transaction(self, account_id: str, trans_id: str,
image_type: TransactionType, image_path: str) -> Optional[str]:
"""保存交易相关的图片,返回相对路径"""
trans_dir = self._get_transaction_dir(account_id, trans_id)
source_path = Path(image_path)
if not source_path.exists():
return None
# 保存图片到对应的图片目录
dest_dir = trans_dir / image_type.value
dest_dir.mkdir(exist_ok=True)
dest_path = dest_dir / source_path.name
shutil.copy2(source_path, dest_path)
# 返回相对于data_root的路径
relative_path = str(dest_path.relative_to(self.data_root))
return relative_path
def get_transaction_image_path(self, account_id: str, relative_path: str) -> Optional[Path]:
"""获取交易图片的完整路径"""
if not relative_path:
return None
return self.data_root / relative_path
def delete_transaction(self, account_id: str, trans_id: str) -> bool:
"""删除交易记录"""
account = self.accounts.get(account_id)
if not account:
return False
# 删除磁盘上的文件
trans_dir = self._get_transaction_dir(account_id, trans_id)
try:
shutil.rmtree(trans_dir)
except Exception as e:
print(f"删除交易记录文件出错: {e}")
# 从账户中移除
return account.remove_transaction(trans_id)
def update_transaction(self, account_id: str, trans_id: str, **kwargs) -> bool:
"""更新交易记录"""
account = self.accounts.get(account_id)
if not account:
return False
transaction = account.get_transaction(trans_id)
if not transaction:
return False
# 更新允许的字段
allowed_fields = ['date', 'amount', 'trader', 'notes', 'invoice_path',
'payment_path', 'purchase_path', 'category']
for field, value in kwargs.items():
if field in allowed_fields:
setattr(transaction, field, value)
transaction.updated_at = datetime.now().isoformat()
self._save_transaction_data(account_id, transaction)
return True
def get_transaction(self, account_id: str, trans_id: str) -> Optional[Transaction]:
"""获取单个交易记录"""
account = self.accounts.get(account_id)
if not account:
return None
return account.get_transaction(trans_id)
def query_transactions(self, account_id: str, date_start: Optional[str] = None,
date_end: Optional[str] = None, amount_min: Optional[float] = None,
amount_max: Optional[float] = None, trader: Optional[str] = None,
category: Optional[str] = None) -> List[Transaction]:
"""查询交易记录(支持多条件筛选)"""
account = self.accounts.get(account_id)
if not account:
return []
results = []
for trans in account.transactions:
# 日期范围筛选
if date_start and trans.date < date_start:
continue
if date_end and trans.date > date_end:
continue
# 金额范围筛选
if amount_min is not None and trans.amount < amount_min:
continue
if amount_max is not None and trans.amount > amount_max:
continue
# 交易人筛选(模糊匹配)
if trader and trader.lower() not in trans.trader.lower():
continue
# 分类筛选
if category and trans.category != category:
continue
results.append(trans)
# 按日期排序
results.sort(key=lambda x: x.date, reverse=True)
return results
def get_account_summary(self, account_id: str) -> Optional[dict]:
"""获取账户汇总信息"""
account = self.accounts.get(account_id)
if not account:
return None
total_amount = sum(t.amount for t in account.transactions)
transaction_count = len(account.transactions)
return {
'account_id': account_id,
'account_name': account.name,
'total_amount': total_amount,
'transaction_count': transaction_count,
'created_at': account.created_at,
'updated_at': account.updated_at
}
def export_account_package(self, account_id: str, export_path: str) -> bool:
"""导出账户为可转移的压缩包"""
account_dir = self._get_account_dir(account_id)
if not account_dir.exists():
return False
try:
export_file = Path(export_path) / f"{self.accounts[account_id].name}_{account_id}.zip"
with zipfile.ZipFile(export_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(account_dir):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(account_dir)
zipf.write(file_path, arcname)
return True
except Exception as e:
print(f"导出账户出错: {e}")
return False
def import_account_package(self, zip_path: str) -> Optional[str]:
"""导入账户压缩包返回导入的账户ID"""
try:
zip_path = Path(zip_path)
if not zip_path.exists():
return None
# 先加载元数据以获取账户ID
with zipfile.ZipFile(zip_path, 'r') as zipf:
metadata_content = zipf.read('metadata.json')
metadata = json.loads(metadata_content)
account_id = metadata['id']
# 如果账户已存在创建新ID
if account_id in self.accounts:
account_id = str(uuid.uuid4())
# 更新元数据中的ID
metadata['id'] = account_id
# 解压到临时目录
temp_dir = self.data_root / f"_temp_{uuid.uuid4()}"
with zipfile.ZipFile(zip_path, 'r') as zipf:
zipf.extractall(temp_dir)
# 更新临时目录中的元数据文件
metadata_file = temp_dir / 'metadata.json'
with open(metadata_file, 'w', encoding='utf-8') as f:
json.dump(metadata, f, ensure_ascii=False, indent=2)
# 移动到正式目录
account_dir = self._get_account_dir(account_id)
if account_dir.exists():
shutil.rmtree(account_dir)
shutil.move(str(temp_dir), str(account_dir))
# 重新加载账户
self.load_all_accounts()
return account_id
except Exception as e:
print(f"导入账户出错: {e}")
return None
def backup_all_accounts(self) -> bool:
"""备份所有账户"""
try:
backup_dir = self.data_root / 'backups'
backup_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = backup_dir / f"backup_{timestamp}.zip"
accounts_dir = self.data_root / 'accounts'
with zipfile.ZipFile(backup_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(accounts_dir):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(self.data_root / 'accounts')
zipf.write(file_path, arcname)
return True
except Exception as e:
print(f"备份账户出错: {e}")
return False
def load_all_accounts(self) -> None:
"""加载所有账户"""
self.accounts.clear()
accounts_dir = self.data_root / 'accounts'
if not accounts_dir.exists():
return
for account_dir in accounts_dir.iterdir():
if not account_dir.is_dir():
continue
metadata = self._load_account_metadata(account_dir.name)
if not metadata:
continue
account = Account(
account_id=metadata['id'],
account_name=metadata['name'],
description=metadata.get('description', '')
)
account.categories = metadata.get('categories', []) # 从元数据加载分类
account.created_at = metadata.get('created_at')
account.updated_at = metadata.get('updated_at')
# 加载该账户的所有交易记录
trans_dirs = [d for d in account_dir.iterdir()
if d.is_dir() and d.name not in ['invoice', 'payment', 'purchase']]
for trans_dir in trans_dirs:
transaction = self._load_transaction_data(account_dir.name, trans_dir.name)
if transaction:
account.transactions.append(transaction)
# 按日期排序
account.transactions.sort(key=lambda x: x.date, reverse=True)
self.accounts[account.id] = account
def export_to_csv(self, account_id: str, csv_path: str) -> bool:
"""导出账户数据为CSV格式"""
account = self.accounts.get(account_id)
if not account:
return False
try:
import csv
with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
writer.writerow(['日期', '金额', '交易人', '分类', '备注', '创建时间'])
for trans in account.transactions:
writer.writerow([
trans.date,
trans.amount,
trans.trader,
trans.category,
trans.notes,
trans.created_at
])
return True
except Exception as e:
print(f"导出CSV出错: {e}")
return False
def add_category(self, account_id: str, category: str) -> bool:
"""添加交易分类"""
account = self.accounts.get(account_id)
if not account:
return False
if category not in account.categories:
account.categories.append(category)
account.updated_at = datetime.now().isoformat()
self._save_account_metadata(account)
return True
return False
def rename_category(self, account_id: str, old_name: str, new_name: str) -> bool:
"""重命名交易分类"""
account = self.accounts.get(account_id)
if not account:
return False
if old_name not in account.categories:
return False
if new_name in account.categories:
return False # 新分类名已存在
# 重命名分类
idx = account.categories.index(old_name)
account.categories[idx] = new_name
# 更新所有使用旧分类的交易
account_dir = self._get_account_dir(account_id)
for trans_dir in account_dir.iterdir():
if not trans_dir.is_dir() or trans_dir.name in ['invoice', 'payment', 'purchase']:
continue
data_file = trans_dir / 'data.json'
if data_file.exists():
try:
with open(data_file, 'r', encoding='utf-8') as f:
transaction_data = json.load(f)
if transaction_data.get('category') == old_name:
transaction_data['category'] = new_name
with open(data_file, 'w', encoding='utf-8') as f:
json.dump(transaction_data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"更新交易分类出错: {e}")
account.updated_at = datetime.now().isoformat()
self._save_account_metadata(account)
return True
def delete_category(self, account_id: str, category: str) -> bool:
"""删除交易分类"""
account = self.accounts.get(account_id)
if not account:
return False
if category in account.categories:
account.categories.remove(category)
# 清除所有使用此分类的交易的分类字段
account_dir = self._get_account_dir(account_id)
for trans_dir in account_dir.iterdir():
if not trans_dir.is_dir() or trans_dir.name in ['invoice', 'payment', 'purchase']:
continue
data_file = trans_dir / 'data.json'
if data_file.exists():
try:
with open(data_file, 'r', encoding='utf-8') as f:
transaction_data = json.load(f)
if transaction_data.get('category') == category:
transaction_data['category'] = ""
with open(data_file, 'w', encoding='utf-8') as f:
json.dump(transaction_data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"清除交易分类出错: {e}")
account.updated_at = datetime.now().isoformat()
self._save_account_metadata(account)
return True
return False
def get_categories(self, account_id: str) -> List[str]:
"""获取账户的所有分类"""
account = self.accounts.get(account_id)
if not account:
return []
return account.categories
def export_to_mrobot_format(self, account_id: str, transaction_ids: List[str], output_path: str) -> bool:
"""导出交易为 .mrobot 专用格式ZIP 文件)
格式说明
- 文件扩展名.mrobot实际上是 ZIP 文件
- 结构
- metadata.json交易元数据
- images/所有相关图片
"""
try:
account = self.accounts.get(account_id)
if not account:
return False
output_file = Path(output_path)
if not output_file.parent.exists():
output_file.parent.mkdir(parents=True, exist_ok=True)
# 创建 ZIP 文件
with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zf:
# 收集交易数据和图片
transactions_data = []
image_index = 0
for trans_id in transaction_ids:
transaction = self._load_transaction_data(account_id, trans_id)
if not transaction:
continue
# 转换交易为字典
trans_dict = transaction.to_dict()
# 处理图片,存储相对路径
image_map = {}
if transaction.invoice_path:
img_path = self.get_transaction_image_path(account_id, transaction.invoice_path)
if img_path and img_path.exists():
archive_path = f"images/invoice_{image_index}{img_path.suffix}"
zf.write(str(img_path), archive_path)
image_map['invoice'] = archive_path
image_index += 1
if transaction.payment_path:
img_path = self.get_transaction_image_path(account_id, transaction.payment_path)
if img_path and img_path.exists():
archive_path = f"images/payment_{image_index}{img_path.suffix}"
zf.write(str(img_path), archive_path)
image_map['payment'] = archive_path
image_index += 1
if transaction.purchase_path:
img_path = self.get_transaction_image_path(account_id, transaction.purchase_path)
if img_path and img_path.exists():
archive_path = f"images/purchase_{image_index}{img_path.suffix}"
zf.write(str(img_path), archive_path)
image_map['purchase'] = archive_path
image_index += 1
trans_dict['image_map'] = image_map
transactions_data.append(trans_dict)
# 创建元数据 JSON
metadata = {
'version': '1.0',
'account_name': account.name,
'account_description': account.description,
'export_date': datetime.now().isoformat(),
'transactions': transactions_data,
'transaction_count': len(transactions_data)
}
# 将元数据写入 ZIP
metadata_json = json.dumps(metadata, ensure_ascii=False, indent=2)
zf.writestr('metadata.json', metadata_json)
return True
except Exception as e:
print(f"导出失败: {e}")
return False

View File

@ -10,20 +10,44 @@ 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目录
# 导入 CodeGenerator 以使用统一的路径获取逻辑
try:
from app.tools.code_generator import CodeGenerator
# 直接使用 CodeGenerator 的路径获取方法,确保路径一致
assets_dir = CodeGenerator.get_assets_dir("")
print(f"更新代码:使用 CodeGenerator 路径: {assets_dir}")
except Exception as e:
print(f"无法导入 CodeGenerator使用后备路径逻辑: {e}")
# 后备方案使用与CodeGenerator.get_assets_dir相同的逻辑确定assets目录
if getattr(sys, 'frozen', False):
# 打包后的环境 - 使用可执行文件所在目录
# 打包后的环境
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}")
# 如果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}")
cwd_assets = os.path.join(os.getcwd(), "assets")
if os.path.exists(cwd_assets):
assets_dir = cwd_assets
print(f"更新代码:使用工作目录: {assets_dir}")
else:
# 开发环境
assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../assets")
current_dir = os.path.dirname(os.path.abspath(__file__))
while current_dir != os.path.dirname(current_dir):
if os.path.basename(current_dir) == 'MRobot':
break
parent_dir = os.path.dirname(current_dir)
if parent_dir == current_dir:
break
current_dir = parent_dir
assets_dir = os.path.join(current_dir, "assets")
print(f"更新代码:开发环境,使用路径: {assets_dir}")
local_dir = os.path.join(assets_dir, "User_code")
@ -93,6 +117,16 @@ def update_code(parent=None, info_callback=None, error_callback=None):
if backup_dir and os.path.exists(backup_dir):
shutil.rmtree(backup_dir, ignore_errors=True)
# 清除 CodeGenerator 的缓存,确保后续读取更新后的文件
try:
from app.tools.code_generator import CodeGenerator
CodeGenerator._assets_dir_cache = None
CodeGenerator._assets_dir_initialized = False
CodeGenerator._template_dir_logged = False
print("已清除 CodeGenerator 缓存")
except Exception as e:
print(f"清除缓存失败(可忽略): {e}")
if info_callback:
info_callback(parent)
return True

BIN
assets/.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,8 @@
{
"id": "06266d34-8789-45e9-aff0-e42549c238ba",
"name": "admin",
"description": "默认管理账户",
"categories": [],
"created_at": "2025-12-19T22:06:29.964929",
"updated_at": "2025-12-19T22:06:29.964938"
}

View File

@ -0,0 +1,8 @@
{
"id": "7751771f-1363-4606-b292-2db511004bae",
"name": "admin",
"description": "默认管理账户",
"categories": [],
"created_at": "2025-12-29T20:48:32.536465",
"updated_at": "2025-12-29T20:48:32.536472"
}

Binary file not shown.

View File

@ -0,0 +1,113 @@
# User_code 目录结构说明
## 新的文件夹结构
所有外设、组件和设备的文件现在都存放在独立的子文件夹中,便于管理和维护。
### BSP (板级支持包)
```
bsp/
├── bsp.h # BSP 总头文件
├── describe.csv # 外设描述文件
├── .gitkeep
├── can/
│ ├── can.c
│ └── can.h
├── fdcan/
│ ├── fdcan.c
│ └── fdcan.h
├── uart/
│ ├── uart.c
│ └── uart.h
├── spi/
│ ├── spi.c
│ └── spi.h
├── i2c/
│ ├── i2c.c
│ └── i2c.h
├── gpio/
│ ├── gpio.c
│ └── gpio.h
├── pwm/
│ ├── pwm.c
│ └── pwm.h
├── time/
│ ├── time.c
│ └── time.h
├── dwt/
│ ├── dwt.c
│ └── dwt.h
└── mm/
├── mm.c
└── mm.h
```
### Component (组件)
```
component/
├── describe.csv # 组件描述文件
├── dependencies.csv # 组件依赖关系
├── .gitkeep
├── ahrs/
├── capacity/
├── cmd/
├── crc16/
├── crc8/
├── error_detect/
├── filter/
├── freertos_cli/
├── limiter/
├── mixer/
├── pid/
├── ui/
└── user_math/
```
### Device (设备)
```
device/
├── device.h # Device 总头文件
├── config.yaml # 设备配置文件
├── .gitkeep
├── bmi088/
├── buzzer/
├── dm_imu/
├── dr16/
├── ist8310/
├── led/
├── motor/
├── motor_dm/
├── motor_lk/
├── motor_lz/
├── motor_odrive/
├── motor_rm/
├── motor_vesc/
├── oid/
├── ops9/
├── rc_can/
├── servo/
├── vofa/
├── ws2812/
└── lcd_driver/ # LCD 驱动(原有结构)
```
## 代码生成逻辑
代码生成器会:
1. 首先尝试从子文件夹加载模板(如 `bsp/can/can.c`
2. 如果子文件夹不存在,回退到根目录加载(向后兼容)
3. 生成时将文件展开到项目的扁平目录结构中(如 `User/bsp/can.c`
## 优势
**更好的组织**: 每个外设/组件的文件都在独立文件夹中
**便于管理**: 添加、删除、修改模板更加方便
**向后兼容**: 现有的扁平结构仍然可以正常工作
**清晰的结构**: 一目了然地看到所有可用的外设/组件
## 迁移说明
如果你添加新的外设/组件/设备:
1. 在对应目录下创建新的子文件夹(小写命名)
2. 将 .c 和 .h 文件放入子文件夹
3. 代码生成器会自动识别并使用

View File

@ -1,5 +1,6 @@
uart,请开启uart的dma和中断
can,请开启can中断使用函数前请确保can已经初始化。一定要开启can发送中断
fdcan,请开启fdcan中断支持经典CAN和FDCAN模式。会自动根据IOC配置生成对应的宏定义。
gpio,会自动读取cubemx中配置为gpio的引脚并自动区分输入输出和中断。
spi,请开启spi的dma和中断
i2c,要求开始spi中断

1 uart 请开启uart的dma和中断
2 can 请开启can中断,使用函数前请确保can已经初始化。一定要开启can发送中断!!!
3 fdcan 请开启fdcan中断,支持经典CAN和FDCAN模式。会自动根据IOC配置生成对应的宏定义。
4 gpio 会自动读取cubemx中配置为gpio的引脚,并自动区分输入输出和中断。
5 spi 请开启spi的dma和中断
6 i2c 要求开始spi中断

View File

@ -0,0 +1,576 @@
/* Includes ----------------------------------------------------------------- */
#include "fdcan.h"
#include "bsp/fdcan.h"
#include "bsp/bsp.h"
#include <fdcan.h>
#include <cmsis_os2.h>
#include <string.h>
/* Private define ----------------------------------------------------------- */
#define FDCAN_QUEUE_MUTEX_TIMEOUT 100
/* Private macro ------------------------------------------------------------ */
/* ===== FDCAN_FilterTypeDef 配置表 =====
* FDCAN实例的过滤器参数表
*
* idx idtype ftype id1 id2 rxidx
* ID1 ID2
*/
#ifdef FDCAN1_EN
#define FDCAN1_FILTER_CONFIG_TABLE(X) \
X(0, FDCAN_STANDARD_ID, FDCAN_FILTER_MASK, 0x000 , 0x000 , 0) \
X(1, FDCAN_EXTENDED_ID, FDCAN_FILTER_MASK, 0x00000000, 0x00000000, 0)
#define FDCAN1_GLOBAL_FILTER FDCAN_REJECT, FDCAN_REJECT, FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE/* 全局过滤器参数(用于 HAL_FDCAN_ConfigGlobalFilter */
#endif
#ifdef FDCAN2_EN
#define FDCAN2_FILTER_CONFIG_TABLE(X) \
X(0, FDCAN_STANDARD_ID, FDCAN_FILTER_MASK, 0x000 , 0x000 , 0) \
X(1, FDCAN_EXTENDED_ID, FDCAN_FILTER_MASK, 0x00000000, 0x00000000, 0)
#define FDCAN2_GLOBAL_FILTER FDCAN_REJECT, FDCAN_REJECT, FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE/* 全局过滤器参数(用于 HAL_FDCAN_ConfigGlobalFilter */
#endif
#ifdef FDCAN3_EN
#define FDCAN3_FILTER_CONFIG_TABLE(X) \
X(0, FDCAN_STANDARD_ID, FDCAN_FILTER_MASK, 0x000 , 0x000 , 0) \
X(1, FDCAN_EXTENDED_ID, FDCAN_FILTER_MASK, 0x00000000, 0x00000000, 0)
#define FDCAN3_GLOBAL_FILTER FDCAN_REJECT, FDCAN_REJECT, FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE/* 全局过滤器参数(用于 HAL_FDCAN_ConfigGlobalFilter */
#endif
/* ====宏展开实现==== */
#define FDCAN_FILTER_TO_RXFIFO_ENUM_INNER(FIFOIndex) FDCAN_FILTER_TO_RXFIFO##FIFOIndex
#define FDCAN_FILTER_TO_RXFIFO_ENUM(FIFOIndex) FDCAN_FILTER_TO_RXFIFO_ENUM_INNER(FIFOIndex)
#define FDCAN_CONFIG_FILTER(idx, idtype, ftype, id1, id2, rxidx) \
sFilterConfig.FilterIndex = (idx); \
sFilterConfig.IdType = (idtype); \
sFilterConfig.FilterType = (ftype); \
sFilterConfig.FilterConfig = (FDCAN_FILTER_TO_RXFIFO_ENUM(FDCANX_RX_FIFO)); \
sFilterConfig.FilterID1 = (id1); \
sFilterConfig.FilterID2 = (id2); \
sFilterConfig.RxBufferIndex = (rxidx); \
HAL_FDCAN_ConfigFilter(&hfdcan, &sFilterConfig);
#define FDCAN_NOTIFY_FLAG_RXFIFO_INNER(FIFO_IDX) FDCAN_IT_RX_FIFO##FIFO_IDX##_NEW_MESSAGE
#define FDCAN_NOTIFY_FLAG_RXFIFO(FIFO_IDX) FDCAN_NOTIFY_FLAG_RXFIFO_INNER(FIFO_IDX)
#define FDCANx_NOTIFY_FLAGS(FIFO_MACRO) (FDCAN_NOTIFY_FLAG_RXFIFO(FIFO_MACRO) | FDCAN_IT_TX_EVT_FIFO_NEW_DATA | FDCAN_IT_RAM_ACCESS_FAILURE)
#define FDCANX_MSG_PENDING_CB_INNER(FIFO_IDX) HAL_FDCAN_RX_FIFO##FIFO_IDX##_MSG_PENDING_CB
#define FDCANX_MSG_PENDING_CB(FIFO_IDX) FDCANX_MSG_PENDING_CB_INNER(FIFO_IDX)
/* Private typedef ---------------------------------------------------------- */
typedef struct BSP_FDCAN_QueueNode {
BSP_FDCAN_t fdcan;
uint32_t can_id;
osMessageQueueId_t queue;
uint8_t queue_size;
struct BSP_FDCAN_QueueNode *next;
} BSP_FDCAN_QueueNode_t;
/* Private variables -------------------------------------------------------- */
static BSP_FDCAN_QueueNode_t *queue_list = NULL;
static osMutexId_t queue_mutex = NULL;
static void (*FDCAN_Callback[BSP_FDCAN_NUM][HAL_FDCAN_CB_NUM])(void);
static bool inited = false;
static BSP_FDCAN_IdParser_t id_parser = NULL;
static BSP_FDCAN_TxQueue_t tx_queues[BSP_FDCAN_NUM];
static const uint8_t fdcan_dlc2len[16] = {0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64};
/* Private function prototypes ---------------------------------------------- */
static BSP_FDCAN_t FDCAN_Get(FDCAN_HandleTypeDef *hfdcan);
static osMessageQueueId_t BSP_FDCAN_FindQueue(BSP_FDCAN_t fdcan, uint32_t can_id);
static int8_t BSP_FDCAN_CreateIdQueue(BSP_FDCAN_t fdcan, uint32_t can_id, uint8_t queue_size);
static void BSP_FDCAN_RxFifo0Callback(void);
static void BSP_FDCAN_RxFifo1Callback(void);
static void BSP_FDCAN_TxCompleteCallback(void);
static BSP_FDCAN_FrameType_t BSP_FDCAN_GetFrameType(FDCAN_RxHeaderTypeDef *header);
static uint32_t BSP_FDCAN_DefaultIdParser(uint32_t original_id, BSP_FDCAN_FrameType_t frame_type);
static void BSP_FDCAN_TxQueueInit(BSP_FDCAN_t fdcan);
static bool BSP_FDCAN_TxQueuePush(BSP_FDCAN_t fdcan, BSP_FDCAN_TxMessage_t *msg);
static bool BSP_FDCAN_TxQueuePop(BSP_FDCAN_t fdcan, BSP_FDCAN_TxMessage_t *msg);
static bool BSP_FDCAN_TxQueueIsEmpty(BSP_FDCAN_t fdcan);
/* Private functions -------------------------------------------------------- */
static BSP_FDCAN_t FDCAN_Get(FDCAN_HandleTypeDef *hfdcan) {
if (hfdcan == NULL) return BSP_FDCAN_ERR;
if (hfdcan->Instance == FDCAN1) return BSP_FDCAN_1;
else if (hfdcan->Instance == FDCAN2) return BSP_FDCAN_2;
else if (hfdcan->Instance == FDCAN3) return BSP_FDCAN_3;
else return BSP_FDCAN_ERR;
}
static osMessageQueueId_t BSP_FDCAN_FindQueue(BSP_FDCAN_t fdcan, uint32_t can_id) {
BSP_FDCAN_QueueNode_t *node = queue_list;
while (node != NULL) {
if (node->fdcan == fdcan && node->can_id == can_id) return node->queue;
node = node->next;
}
return NULL;
}
static int8_t BSP_FDCAN_CreateIdQueue(BSP_FDCAN_t fdcan, uint32_t can_id, uint8_t queue_size) {
if (queue_size == 0) queue_size = BSP_FDCAN_DEFAULT_QUEUE_SIZE;
if (osMutexAcquire(queue_mutex, FDCAN_QUEUE_MUTEX_TIMEOUT) != osOK) return BSP_ERR_TIMEOUT;
BSP_FDCAN_QueueNode_t *node = queue_list;
while (node != NULL) {
if (node->fdcan == fdcan && node->can_id == can_id) {
osMutexRelease(queue_mutex);
return BSP_ERR;
}
node = node->next;
}
BSP_FDCAN_QueueNode_t *new_node = (BSP_FDCAN_QueueNode_t *)BSP_Malloc(sizeof(BSP_FDCAN_QueueNode_t));
if (new_node == NULL) { osMutexRelease(queue_mutex); return BSP_ERR_NULL; }
new_node->queue = osMessageQueueNew(queue_size, sizeof(BSP_FDCAN_Message_t), NULL);
if (new_node->queue == NULL) { BSP_Free(new_node); osMutexRelease(queue_mutex); return BSP_ERR; }
new_node->fdcan = fdcan;
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;
}
static BSP_FDCAN_FrameType_t BSP_FDCAN_GetFrameType(FDCAN_RxHeaderTypeDef *header) {
if (header->RxFrameType == FDCAN_REMOTE_FRAME) {
return (header->IdType == FDCAN_EXTENDED_ID) ? BSP_FDCAN_FRAME_EXT_REMOTE : BSP_FDCAN_FRAME_STD_REMOTE;
} else {
return (header->IdType == FDCAN_EXTENDED_ID) ? BSP_FDCAN_FRAME_EXT_DATA : BSP_FDCAN_FRAME_STD_DATA;
}
}
static uint32_t BSP_FDCAN_DefaultIdParser(uint32_t original_id, BSP_FDCAN_FrameType_t frame_type) {
(void)frame_type;
return original_id;
}
static uint32_t BSP_FDCAN_EncodeDLC(uint8_t dlc) {
if (dlc <= 8) return dlc;
if (dlc <= 12) return FDCAN_DLC_BYTES_12;
if (dlc <= 16) return FDCAN_DLC_BYTES_16;
if (dlc <= 20) return FDCAN_DLC_BYTES_20;
if (dlc <= 24) return FDCAN_DLC_BYTES_24;
if (dlc <= 32) return FDCAN_DLC_BYTES_32;
if (dlc <= 48) return FDCAN_DLC_BYTES_48;
return FDCAN_DLC_BYTES_64;
}
static void BSP_FDCAN_TxQueueInit(BSP_FDCAN_t fdcan) {
if (fdcan >= BSP_FDCAN_NUM) return;
tx_queues[fdcan].head = 0;
tx_queues[fdcan].tail = 0;
}
static bool BSP_FDCAN_TxQueuePush(BSP_FDCAN_t fdcan, BSP_FDCAN_TxMessage_t *msg) {
if (fdcan >= BSP_FDCAN_NUM || msg == NULL) return false;
BSP_FDCAN_TxQueue_t *queue = &tx_queues[fdcan];
uint32_t next_head = (queue->head + 1) % BSP_FDCAN_TX_QUEUE_SIZE;
if (next_head == queue->tail) return false;
queue->buffer[queue->head] = *msg;
queue->head = next_head;
return true;
}
static bool BSP_FDCAN_TxQueuePop(BSP_FDCAN_t fdcan, BSP_FDCAN_TxMessage_t *msg) {
if (fdcan >= BSP_FDCAN_NUM || msg == NULL) return false;
BSP_FDCAN_TxQueue_t *queue = &tx_queues[fdcan];
if (queue->head == queue->tail) return false;
*msg = queue->buffer[queue->tail];
queue->tail = (queue->tail + 1) % BSP_FDCAN_TX_QUEUE_SIZE;
return true;
}
static bool BSP_FDCAN_TxQueueIsEmpty(BSP_FDCAN_t fdcan) {
if (fdcan >= BSP_FDCAN_NUM) return true;
return tx_queues[fdcan].head == tx_queues[fdcan].tail;
}
static void BSP_FDCAN_TxCompleteCallback(void) {
for (int i = 0; i < BSP_FDCAN_NUM; i++) {
BSP_FDCAN_t fdcan = (BSP_FDCAN_t)i;
FDCAN_HandleTypeDef *hfdcan = BSP_FDCAN_GetHandle(fdcan);
if (hfdcan == NULL) continue;
// 消费所有 TX EVENT FIFO 事件,防止堵塞
FDCAN_TxEventFifoTypeDef tx_event;
while (HAL_FDCAN_GetTxEvent(hfdcan, &tx_event) == HAL_OK) {
// 可在此统计 MessageMarker、ID、时间戳等
}
// 续写软件队列到硬件 FIFO
BSP_FDCAN_TxMessage_t msg;
while (!BSP_FDCAN_TxQueueIsEmpty(fdcan)) {
if (HAL_FDCAN_GetTxFifoFreeLevel(hfdcan) == 0) break;
if (!BSP_FDCAN_TxQueuePop(fdcan, &msg)) break;
HAL_StatusTypeDef res = HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &msg.header, msg.data);
if (res != HAL_OK) {
break;
}
}
}
}
static void BSP_FDCAN_RxFifo0Callback(void) {
FDCAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[BSP_FDCAN_MAX_DLC];
for (int fdcan_idx = 0; fdcan_idx < BSP_FDCAN_NUM; fdcan_idx++) {
FDCAN_HandleTypeDef *hfdcan = BSP_FDCAN_GetHandle((BSP_FDCAN_t)fdcan_idx);
if (hfdcan == NULL) continue;
while (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, FDCAN_RX_FIFO0) > 0) {
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK) {
uint32_t original_id = (rx_header.IdType == FDCAN_STANDARD_ID) ? rx_header.Identifier&0x7ff : rx_header.Identifier&0x1fffffff;
BSP_FDCAN_FrameType_t frame_type = BSP_FDCAN_GetFrameType(&rx_header);
uint32_t parsed_id = BSP_FDCAN_ParseId(original_id, frame_type);
osMessageQueueId_t queue = BSP_FDCAN_FindQueue((BSP_FDCAN_t)fdcan_idx, parsed_id);
if (queue != NULL) {
BSP_FDCAN_Message_t msg;
msg.frame_type = frame_type;
msg.original_id = original_id;
msg.parsed_id = parsed_id;
uint8_t real_len = fdcan_dlc2len[rx_header.DataLength & 0xF];
msg.dlc = real_len;
if (msg.dlc > BSP_FDCAN_MAX_DLC) msg.dlc = BSP_FDCAN_MAX_DLC;
memset(msg.data, 0, BSP_FDCAN_MAX_DLC);//现在是最大缓冲区写法所以全清零
memcpy(msg.data, rx_data, msg.dlc);
osMessageQueuePut(queue, &msg, 0, 0);
}
} else {
break;
}
}
}
}
static void BSP_FDCAN_RxFifo1Callback(void) {
FDCAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[BSP_FDCAN_MAX_DLC];
for (int fdcan_idx = 0; fdcan_idx < BSP_FDCAN_NUM; fdcan_idx++) {
FDCAN_HandleTypeDef *hfdcan = BSP_FDCAN_GetHandle((BSP_FDCAN_t)fdcan_idx);
if (hfdcan == NULL) continue;
while (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, FDCAN_RX_FIFO1) > 0) {
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO1, &rx_header, rx_data) == HAL_OK) {
uint32_t original_id = (rx_header.IdType == FDCAN_STANDARD_ID) ? rx_header.Identifier&0x7ff : rx_header.Identifier&0x1fffffff;
BSP_FDCAN_FrameType_t frame_type = BSP_FDCAN_GetFrameType(&rx_header);
uint32_t parsed_id = BSP_FDCAN_ParseId(original_id, frame_type);
osMessageQueueId_t queue = BSP_FDCAN_FindQueue((BSP_FDCAN_t)fdcan_idx, parsed_id);
if (queue != NULL) {
BSP_FDCAN_Message_t msg;
msg.frame_type = frame_type;
msg.original_id = original_id;
msg.parsed_id = parsed_id;
uint8_t real_len = fdcan_dlc2len[rx_header.DataLength & 0xF];
msg.dlc = real_len;
if (msg.dlc > BSP_FDCAN_MAX_DLC) msg.dlc = BSP_FDCAN_MAX_DLC;
memset(msg.data, 0, BSP_FDCAN_MAX_DLC);//现在是最大缓冲区写法所以全清零
memcpy(msg.data, rx_data, msg.dlc);
osMessageQueuePut(queue, &msg, 0, 0);
}
} else {
break;
}
}
}
}
/* HAL Callback Stubs (map HAL FDCAN callbacks to user callbacks) */
void HAL_FDCAN_TxEventFifoCallback(FDCAN_HandleTypeDef *hfdcan, uint32_t TxEventFifoITs) {
BSP_FDCAN_t bsp_fdcan = FDCAN_Get(hfdcan);
if (bsp_fdcan != BSP_FDCAN_ERR) {
if (FDCAN_Callback[bsp_fdcan][HAL_FDCAN_TX_EVENT_FIFO_CB])
FDCAN_Callback[bsp_fdcan][HAL_FDCAN_TX_EVENT_FIFO_CB]();
}
}
void HAL_FDCAN_TxBufferCompleteCallback(FDCAN_HandleTypeDef *hfdcan, uint32_t BufferIndex) {
BSP_FDCAN_t bsp_fdcan = FDCAN_Get(hfdcan);
if (bsp_fdcan != BSP_FDCAN_ERR) {
if (FDCAN_Callback[bsp_fdcan][HAL_FDCAN_TX_BUFFER_COMPLETE_CB])
FDCAN_Callback[bsp_fdcan][HAL_FDCAN_TX_BUFFER_COMPLETE_CB]();
}
}
void HAL_FDCAN_TxBufferAbortCallback(FDCAN_HandleTypeDef *hfdcan, uint32_t BufferIndex) {
BSP_FDCAN_t bsp_fdcan = FDCAN_Get(hfdcan);
if (bsp_fdcan != BSP_FDCAN_ERR) {
if (FDCAN_Callback[bsp_fdcan][HAL_FDCAN_TX_BUFFER_ABORT_CB])
FDCAN_Callback[bsp_fdcan][HAL_FDCAN_TX_BUFFER_ABORT_CB]();
}
}
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) {
BSP_FDCAN_t bsp_fdcan = FDCAN_Get(hfdcan);
if (bsp_fdcan != BSP_FDCAN_ERR) {
if (FDCAN_Callback[bsp_fdcan][HAL_FDCAN_RX_FIFO0_MSG_PENDING_CB])
FDCAN_Callback[bsp_fdcan][HAL_FDCAN_RX_FIFO0_MSG_PENDING_CB]();
}
}
void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs) {
BSP_FDCAN_t bsp_fdcan = FDCAN_Get(hfdcan);
if (bsp_fdcan != BSP_FDCAN_ERR) {
if (FDCAN_Callback[bsp_fdcan][HAL_FDCAN_RX_FIFO1_MSG_PENDING_CB])
FDCAN_Callback[bsp_fdcan][HAL_FDCAN_RX_FIFO1_MSG_PENDING_CB]();
}
}
void HAL_FDCAN_ErrorCallback(FDCAN_HandleTypeDef *hfdcan) {
BSP_FDCAN_t bsp_fdcan = FDCAN_Get(hfdcan);
if (bsp_fdcan != BSP_FDCAN_ERR) {
if (FDCAN_Callback[bsp_fdcan][HAL_FDCAN_ERROR_CB])
FDCAN_Callback[bsp_fdcan][HAL_FDCAN_ERROR_CB]();
}
}
/* Exported functions ------------------------------------------------------- */
int8_t BSP_FDCAN_Init(void) {
if (inited) return BSP_ERR_INITED;
memset(FDCAN_Callback, 0, sizeof(FDCAN_Callback));
for (int i = 0; i < BSP_FDCAN_NUM; i++) BSP_FDCAN_TxQueueInit((BSP_FDCAN_t)i);
id_parser = BSP_FDCAN_DefaultIdParser;
queue_mutex = osMutexNew(NULL);
if (queue_mutex == NULL) return BSP_ERR;
inited = true;
/* 配置并启动 FDCAN 实例,绑定中断/回调 */
//========== 过滤器配置说明:==========================
// 过滤器编号相对于每个相当于经典can过滤器的bank
// sFilterConfig.FilterIndex = 0 to 127(标准ID) or 0 to 63(扩展ID);
// 关于过滤器索引的说明:
// 由stm32h7xx_hal_fdcan.c的第1874行代码可知滤波器地址计算方式如下
// StandardFilterSA字节 = SRAMCAN_BASE + (MessageRAMOffset * 4U)
// 标准滤波器物理地址(字节) = StandardFilterSA + (FilterIndex * 4U)(每个标准滤波器占 4 字节 = 1 word,扩展的则是8个字节
//
//
// 标识符类型:
// sFilterConfig.IdType = FDCAN_STANDARD_ID or FDCAN_EXTENDED_ID;
// 过滤器类型: (仅介绍掩码模式)
// sFilterConfig.FilterType = FDCAN_FILTER_MASK;(掩码模式)
// 过滤器配置:
// sFilterConfig.FilterConfig = FDCAN_FILTER_DISABLE; (禁用该过滤器条目)
// FDCAN_FILTER_TO_RXFIFO0; (将匹配的消息放入 FIFO 0普通优先级)
// FDCAN_FILTER_TO_RXFIFO1; (将匹配的消息放入 FIFO 1高优先级)
// FDCAN_FILTER_TO_RXBUFFER; (将匹配的消息放入 指定的接收缓冲区)
// FDCAN_FILTER_REJECT; (拒绝接收该标识符对应的报文)
// FDCAN_FILTER_ACCEPT; (接受所有消息)
// FDCAN_FILTER_HP (过滤器匹配时,将报文标记为高优先级)
// FDCAN_FILTER_TO_RXFIFO0_HP (过滤器匹配时将报文标记为高优先级并存储至接收FIFO 0)
// FDCAN_FILTER_TO_RXFIFO1_HP (过滤器匹配时将报文标记为高优先级并存储至接收FIFO 1)
// FDCAN_FILTER_TO_RXBUFFER (将报文存储至接收缓冲区过滤器类型FilterType配置项失效 )
// 过滤器ID与掩码(FilterType掩码模式下)
// 比较值(要匹配的 ID 的参考位)
// sFilterConfig.FilterID1 = 0 to 0x7FF; 标准ID
// 0 to 0x1FFFFFFF 扩展ID
// 掩码1=比较该位0=忽略该位)
// sFilterConfig.FilterID2 = 0 to 0x7FF; 标准ID
// 0 to 0x1FFFFFFF 扩展ID
// 接收缓冲区索引
// FilterConfig == FDCAN_FILTER_TO_RXBUFFER 时有效;必须小于RxBuffersNbr配置的实际Rx buffer数量
// sFilterConfig.RxBufferIndex = 0 to (RxBuffersNbr - 1);
// 标记校准信息(用于 FDCAN 校准/时钟相关单元作特殊处理或统计)
// 仅在FilterConfig 设为 FDCAN_FILTER_TO_RXBUFFER 时才有意义通常设置为0
// IsCalibrationMsg = 0 or 1;
// fdcan_filter_table.h
//=================================================================================
/* 依据上述说明配置过滤器并启动FDCAN */
FDCAN_FilterTypeDef sFilterConfig;
#ifdef FDCAN1_EN
#define hfdcan hfdcan1
#define FDCANX_RX_FIFO FDCAN1_RX_FIFO
FDCAN1_FILTER_CONFIG_TABLE(FDCAN_CONFIG_FILTER)
#undef hfdcan
#undef FDCANX_RX_FIFO
HAL_FDCAN_ConfigGlobalFilter(&hfdcan1, FDCAN1_GLOBAL_FILTER);
HAL_FDCAN_ActivateNotification(&hfdcan1, FDCANx_NOTIFY_FLAGS(FDCAN1_RX_FIFO), 0);
BSP_FDCAN_RegisterCallback(BSP_FDCAN_1, FDCANX_MSG_PENDING_CB(FDCAN1_RX_FIFO), BSP_FDCAN_RxFifo0Callback);
BSP_FDCAN_RegisterCallback(BSP_FDCAN_1, HAL_FDCAN_TX_EVENT_FIFO_CB, BSP_FDCAN_TxCompleteCallback);
HAL_FDCAN_Start(&hfdcan1);
#endif
#ifdef FDCAN2_EN
#define hfdcan hfdcan2
#define FDCANX_RX_FIFO FDCAN2_RX_FIFO
FDCAN2_FILTER_CONFIG_TABLE(FDCAN_CONFIG_FILTER)
#undef hfdcan
#undef FDCANX_RX_FIFO
HAL_FDCAN_ConfigGlobalFilter(&hfdcan2, FDCAN2_GLOBAL_FILTER);
HAL_FDCAN_ActivateNotification(&hfdcan2, FDCANx_NOTIFY_FLAGS(FDCAN2_RX_FIFO), 0);
BSP_FDCAN_RegisterCallback(BSP_FDCAN_2, FDCANX_MSG_PENDING_CB(FDCAN2_RX_FIFO), BSP_FDCAN_RxFifo1Callback);
BSP_FDCAN_RegisterCallback(BSP_FDCAN_2, HAL_FDCAN_TX_EVENT_FIFO_CB, BSP_FDCAN_TxCompleteCallback);
HAL_FDCAN_Start(&hfdcan2);
#endif
#ifdef FDCAN3_EN
#define hfdcan hfdcan3
#define FDCANX_RX_FIFO FDCAN3_RX_FIFO
FDCAN3_FILTER_CONFIG_TABLE(FDCAN_CONFIG_FILTER)
#undef hfdcan
#undef FDCANX_RX_FIFO
HAL_FDCAN_ConfigGlobalFilter(&hfdcan3, FDCAN3_GLOBAL_FILTER);
HAL_FDCAN_ActivateNotification(&hfdcan3, FDCANx_NOTIFY_FLAGS(FDCAN3_RX_FIFO), 0);
BSP_FDCAN_RegisterCallback(BSP_FDCAN_3, FDCANX_MSG_PENDING_CB(FDCAN3_RX_FIFO), BSP_FDCAN_RxFifo1Callback);
BSP_FDCAN_RegisterCallback(BSP_FDCAN_3, HAL_FDCAN_TX_EVENT_FIFO_CB, BSP_FDCAN_TxCompleteCallback);
HAL_FDCAN_Start(&hfdcan3);
#endif
#undef FDCAN_FILTER_TO_RXFIFO_ENUM_INNER
#undef FDCAN_FILTER_TO_RXFIFO_ENUM
#undef FDCAN_CONFIG_FILTER
#undef FDCAN_NOTIFY_FLAG_RXFIFO_INNER
#undef FDCAN_NOTIFY_FLAG_RXFIFO
#undef FDCANx_NOTIFY_FLAGS
#undef FDCANX_MSG_PENDING_CB_INNER
#undef FDCANX_MSG_PENDING_CB
return BSP_OK;
}
FDCAN_HandleTypeDef *BSP_FDCAN_GetHandle(BSP_FDCAN_t fdcan) {
if (fdcan >= BSP_FDCAN_NUM) return NULL;
switch (fdcan) {
/* AUTO GENERATED BSP_FDCAN_GET_HANDLE BEGIN */
case BSP_FDCAN_1: return &hfdcan1;
case BSP_FDCAN_2: return &hfdcan2;
case BSP_FDCAN_3: return &hfdcan3;
/* AUTO GENERATED BSP_FDCAN_GET_HANDLE END */
default: return NULL;
}
}
int8_t BSP_FDCAN_RegisterCallback(BSP_FDCAN_t fdcan, BSP_FDCAN_Callback_t type, void (*callback)(void)) {
if (!inited) return BSP_ERR_INITED;
if (callback == NULL) return BSP_ERR_NULL;
if (fdcan >= BSP_FDCAN_NUM) return BSP_ERR;
if (type >= HAL_FDCAN_CB_NUM) return BSP_ERR;
FDCAN_Callback[fdcan][type] = callback;
return BSP_OK;
}
int8_t BSP_FDCAN_Transmit(BSP_FDCAN_t fdcan, BSP_FDCAN_Format_t format, uint32_t id, uint8_t *data, uint8_t dlc) {
if (!inited) return BSP_ERR_INITED;
if (fdcan >= BSP_FDCAN_NUM) return BSP_ERR;
if (data == NULL && format != BSP_FDCAN_FORMAT_STD_REMOTE && format != BSP_FDCAN_FORMAT_EXT_REMOTE) return BSP_ERR_NULL;
if (dlc > BSP_FDCAN_MAX_DLC) return BSP_ERR;
FDCAN_HandleTypeDef *hfdcan = BSP_FDCAN_GetHandle(fdcan);
if (hfdcan == NULL) return BSP_ERR_NULL;
BSP_FDCAN_TxMessage_t tx_msg = {0};
switch (format) {
case BSP_FDCAN_FORMAT_STD_DATA:
tx_msg.header.Identifier = id;
tx_msg.header.IdType = FDCAN_STANDARD_ID;
tx_msg.header.TxFrameType = FDCAN_DATA_FRAME;
break;
case BSP_FDCAN_FORMAT_EXT_DATA:
tx_msg.header.Identifier = id;
tx_msg.header.IdType = FDCAN_EXTENDED_ID;
tx_msg.header.TxFrameType = FDCAN_DATA_FRAME;
break;
case BSP_FDCAN_FORMAT_STD_REMOTE:
tx_msg.header.Identifier = id;
tx_msg.header.IdType = FDCAN_STANDARD_ID;
tx_msg.header.TxFrameType = FDCAN_REMOTE_FRAME;
break;
case BSP_FDCAN_FORMAT_EXT_REMOTE:
tx_msg.header.Identifier = id;
tx_msg.header.IdType = FDCAN_EXTENDED_ID;
tx_msg.header.TxFrameType = FDCAN_REMOTE_FRAME;
break;
default:
return BSP_ERR;
}
switch (hfdcan->Init.FrameFormat) {
case FDCAN_FRAME_FD_BRS:
tx_msg.header.BitRateSwitch = FDCAN_BRS_ON;
tx_msg.header.FDFormat = FDCAN_FD_CAN;
break;
case FDCAN_FRAME_FD_NO_BRS:
tx_msg.header.BitRateSwitch = FDCAN_BRS_OFF;
tx_msg.header.FDFormat = FDCAN_FD_CAN;
break;
case FDCAN_FRAME_CLASSIC:
default:
tx_msg.header.BitRateSwitch = FDCAN_BRS_OFF;
tx_msg.header.FDFormat = FDCAN_CLASSIC_CAN;
break;
}
tx_msg.header.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
tx_msg.header.TxEventFifoControl = FDCAN_STORE_TX_EVENTS;
tx_msg.header.MessageMarker = 0x01;
tx_msg.header.DataLength = BSP_FDCAN_EncodeDLC(dlc);
memset(tx_msg.data, 0, dlc);
if (data != NULL && dlc > 0) {memcpy(tx_msg.data, data, dlc);}
if (HAL_FDCAN_GetTxFifoFreeLevel(hfdcan) > 0) {
if (HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &tx_msg.header, tx_msg.data) == HAL_OK) return BSP_OK;
}
if (BSP_FDCAN_TxQueuePush(fdcan, &tx_msg)) return BSP_OK;
return BSP_ERR;
}
int8_t BSP_FDCAN_TransmitStdDataFrame(BSP_FDCAN_t fdcan, BSP_FDCAN_StdDataFrame_t *frame) {
if (frame == NULL) return BSP_ERR_NULL;
return BSP_FDCAN_Transmit(fdcan, BSP_FDCAN_FORMAT_STD_DATA, frame->id, frame->data, frame->dlc);
}
int8_t BSP_FDCAN_TransmitExtDataFrame(BSP_FDCAN_t fdcan, BSP_FDCAN_ExtDataFrame_t *frame) {
if (frame == NULL) return BSP_ERR_NULL;
return BSP_FDCAN_Transmit(fdcan, BSP_FDCAN_FORMAT_EXT_DATA, frame->id, frame->data, frame->dlc);
}
int8_t BSP_FDCAN_TransmitRemoteFrame(BSP_FDCAN_t fdcan, BSP_FDCAN_RemoteFrame_t *frame) {
if (frame == NULL) return BSP_ERR_NULL;
BSP_FDCAN_Format_t format = frame->is_extended ? BSP_FDCAN_FORMAT_EXT_REMOTE : BSP_FDCAN_FORMAT_STD_REMOTE;
return BSP_FDCAN_Transmit(fdcan, format, frame->id, NULL, frame->dlc);
}
int8_t BSP_FDCAN_RegisterId(BSP_FDCAN_t fdcan, uint32_t can_id, uint8_t queue_size) {
if (!inited) return BSP_ERR_INITED;
return BSP_FDCAN_CreateIdQueue(fdcan, can_id, queue_size);
}
int8_t BSP_FDCAN_GetMessage(BSP_FDCAN_t fdcan, uint32_t can_id, BSP_FDCAN_Message_t *msg, uint32_t timeout) {
if (!inited) return BSP_ERR_INITED;
if (msg == NULL) return BSP_ERR_NULL;
if (osMutexAcquire(queue_mutex, FDCAN_QUEUE_MUTEX_TIMEOUT) != osOK) return BSP_ERR_TIMEOUT;
osMessageQueueId_t queue = BSP_FDCAN_FindQueue(fdcan, can_id);
osMutexRelease(queue_mutex);
if (queue == NULL) return BSP_ERR_NO_DEV;
osStatus_t res = osMessageQueueGet(queue, msg, NULL, timeout);
return (res == osOK) ? BSP_OK : BSP_ERR;
}
int32_t BSP_FDCAN_GetQueueCount(BSP_FDCAN_t fdcan, uint32_t can_id) {
if (!inited) return -1;
if (osMutexAcquire(queue_mutex, FDCAN_QUEUE_MUTEX_TIMEOUT) != osOK) return -1;
osMessageQueueId_t queue = BSP_FDCAN_FindQueue(fdcan, can_id);
osMutexRelease(queue_mutex);
if (queue == NULL) return -1;
return (int32_t)osMessageQueueGetCount(queue);
}
int8_t BSP_FDCAN_FlushQueue(BSP_FDCAN_t fdcan, uint32_t can_id) {
if (!inited) return BSP_ERR_INITED;
if (osMutexAcquire(queue_mutex, FDCAN_QUEUE_MUTEX_TIMEOUT) != osOK) return BSP_ERR_TIMEOUT;
osMessageQueueId_t queue = BSP_FDCAN_FindQueue(fdcan, can_id);
osMutexRelease(queue_mutex);
if (queue == NULL) return BSP_ERR_NO_DEV;
BSP_FDCAN_Message_t tmp;
while (osMessageQueueGet(queue, &tmp, NULL, BSP_FDCAN_TIMEOUT_IMMEDIATE) == osOK) { }
return BSP_OK;
}
int8_t BSP_FDCAN_RegisterIdParser(BSP_FDCAN_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_FDCAN_ParseId(uint32_t original_id, BSP_FDCAN_FrameType_t frame_type) {
if (id_parser != NULL) return id_parser(original_id, frame_type);
return BSP_FDCAN_DefaultIdParser(original_id, frame_type);
}
/* */

View File

@ -0,0 +1,137 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ----------------------------------------------------------------- */
#include <stdint.h>
#include <stdbool.h>
#include "bsp/bsp.h"
#include "bsp/mm.h"
#include <cmsis_os.h>
/* USER INCLUDE BEGIN */
#include <fdcan.h>
/* USER INCLUDE END */
/* Exported constants ------------------------------------------------------- */
#define BSP_FDCAN_MAX_DLC 64
#define BSP_FDCAN_DEFAULT_QUEUE_SIZE 10
#define BSP_FDCAN_TIMEOUT_IMMEDIATE 0
#define BSP_FDCAN_TIMEOUT_FOREVER osWaitForever
#define BSP_FDCAN_TX_QUEUE_SIZE 32
/* Exported macro ----------------------------------------------------------- */
//FDCANX实例使能
/* AUTO GENERATED FDCAN_EN BEGIN */
#define FDCAN1_EN
#define FDCAN2_EN
#define FDCAN3_EN
/* AUTO GENERATED FDCAN_EN END */
// FDCANX接收FIFO选择0=FIFO0, 1=FIFO1
/* AUTO GENERATED FDCAN_RX_FIFO BEGIN */
#ifdef FDCAN1_EN
#define FDCAN1_RX_FIFO 0
#endif
#ifdef FDCAN2_EN
#define FDCAN2_RX_FIFO 1
#endif
#ifdef FDCAN3_EN
#define FDCAN3_RX_FIFO 1
#endif
/* AUTO GENERATED FDCAN_RX_FIFO END */
/* Exported types ----------------------------------------------------------- */
typedef enum {
/* AUTO GENERATED BSP_FDCAN_NAME BEGIN */
BSP_FDCAN_1,
BSP_FDCAN_2,
BSP_FDCAN_3,
/* AUTO GENERATED BSP_FDCAN_NAME END */
BSP_FDCAN_NUM,
BSP_FDCAN_ERR,
} BSP_FDCAN_t;
typedef enum {
HAL_FDCAN_TX_EVENT_FIFO_CB,
HAL_FDCAN_TX_BUFFER_COMPLETE_CB,
HAL_FDCAN_TX_BUFFER_ABORT_CB,
HAL_FDCAN_RX_FIFO0_MSG_PENDING_CB,
HAL_FDCAN_RX_FIFO0_FULL_CB,
HAL_FDCAN_RX_FIFO1_MSG_PENDING_CB,
HAL_FDCAN_RX_FIFO1_FULL_CB,
HAL_FDCAN_ERROR_CB,
HAL_FDCAN_CB_NUM,
} BSP_FDCAN_Callback_t;
typedef enum {
BSP_FDCAN_FORMAT_STD_DATA,
BSP_FDCAN_FORMAT_EXT_DATA,
BSP_FDCAN_FORMAT_STD_REMOTE,
BSP_FDCAN_FORMAT_EXT_REMOTE,
} BSP_FDCAN_Format_t;
typedef enum {
BSP_FDCAN_FRAME_STD_DATA,
BSP_FDCAN_FRAME_EXT_DATA,
BSP_FDCAN_FRAME_STD_REMOTE,
BSP_FDCAN_FRAME_EXT_REMOTE,
} BSP_FDCAN_FrameType_t;
typedef struct {
BSP_FDCAN_FrameType_t frame_type;
uint32_t original_id;
uint32_t parsed_id;
uint8_t dlc;
uint8_t data[BSP_FDCAN_MAX_DLC];
uint32_t timestamp;
} BSP_FDCAN_Message_t;
typedef struct {
uint32_t id;
uint8_t dlc;
uint8_t data[BSP_FDCAN_MAX_DLC];
} BSP_FDCAN_StdDataFrame_t;
typedef struct {
uint32_t id;
uint8_t dlc;
uint8_t data[BSP_FDCAN_MAX_DLC];
} BSP_FDCAN_ExtDataFrame_t;
typedef struct {
uint32_t id;
uint8_t dlc;
bool is_extended;
} BSP_FDCAN_RemoteFrame_t;
typedef uint32_t (*BSP_FDCAN_IdParser_t)(uint32_t original_id, BSP_FDCAN_FrameType_t frame_type);
typedef struct {
FDCAN_TxHeaderTypeDef header; /* HAL FDCAN header type */
uint8_t data[BSP_FDCAN_MAX_DLC];
} BSP_FDCAN_TxMessage_t;
typedef struct {
BSP_FDCAN_TxMessage_t buffer[BSP_FDCAN_TX_QUEUE_SIZE];
volatile uint32_t head;
volatile uint32_t tail;
} BSP_FDCAN_TxQueue_t;
/* Exported functions prototypes -------------------------------------------- */
int8_t BSP_FDCAN_Init(void);
FDCAN_HandleTypeDef *BSP_FDCAN_GetHandle(BSP_FDCAN_t can);
int8_t BSP_FDCAN_RegisterCallback(BSP_FDCAN_t can, BSP_FDCAN_Callback_t type, void (*callback)(void));
int8_t BSP_FDCAN_Transmit(BSP_FDCAN_t can, BSP_FDCAN_Format_t format, uint32_t id, uint8_t *data, uint8_t dlc);
int8_t BSP_FDCAN_TransmitStdDataFrame(BSP_FDCAN_t can, BSP_FDCAN_StdDataFrame_t *frame);
int8_t BSP_FDCAN_TransmitExtDataFrame(BSP_FDCAN_t can, BSP_FDCAN_ExtDataFrame_t *frame);
int8_t BSP_FDCAN_TransmitRemoteFrame(BSP_FDCAN_t can, BSP_FDCAN_RemoteFrame_t *frame);
int8_t BSP_FDCAN_RegisterId(BSP_FDCAN_t can, uint32_t can_id, uint8_t queue_size);
int8_t BSP_FDCAN_GetMessage(BSP_FDCAN_t can, uint32_t can_id, BSP_FDCAN_Message_t *msg, uint32_t timeout);
int32_t BSP_FDCAN_GetQueueCount(BSP_FDCAN_t can, uint32_t can_id);
int8_t BSP_FDCAN_FlushQueue(BSP_FDCAN_t can, uint32_t can_id);
int8_t BSP_FDCAN_RegisterIdParser(BSP_FDCAN_IdParser_t parser);
uint32_t BSP_FDCAN_ParseId(uint32_t original_id, BSP_FDCAN_FrameType_t frame_type);
#ifdef __cplusplus
}
#endif

View File

@ -22,8 +22,7 @@ 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_BMI088;
/* AUTO GENERATED SPI_GET */
else
return BSP_SPI_ERR;
}
@ -96,8 +95,7 @@ void HAL_SPI_AbortCpltCallback(SPI_HandleTypeDef *hspi) {
/* Exported functions ------------------------------------------------------- */
SPI_HandleTypeDef *BSP_SPI_GetHandle(BSP_SPI_t spi) {
switch (spi) {
case BSP_SPI_BMI088:
return &hspi1;
/* AUTO GENERATED BSP_SPI_GET_HANDLE */
default:
return NULL;
}

View File

@ -0,0 +1,387 @@
/*
*/
#include "cmd.h"
#include <string.h>
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/**
* @brief
*
* @param cmd
* @param behavior
* @return uint16_t
*/
static inline CMD_KeyValue_t CMD_BehaviorToKey(CMD_t *cmd,
CMD_Behavior_t behavior) {
return cmd->param->map.key_map[behavior].key;
}
static inline CMD_ActiveType_t CMD_BehaviorToActive(CMD_t *cmd,
CMD_Behavior_t behavior) {
return cmd->param->map.key_map[behavior].active;
}
/**
* @brief
*
* @param rc
* @param key
* @param stateful
* @return true
* @return false
*/
static bool CMD_KeyPressedRc(const CMD_RC_t *rc, CMD_KeyValue_t key) {
/* 按下按键为鼠标左、右键 */
if (key == CMD_L_CLICK) {
return rc->mouse.l_click;
}
if (key == CMD_R_CLICK) {
return rc->mouse.r_click;
}
return rc->key & (1u << key);
}
static bool CMD_BehaviorOccurredRc(const CMD_RC_t *rc, CMD_t *cmd,
CMD_Behavior_t behavior) {
CMD_KeyValue_t key = CMD_BehaviorToKey(cmd, behavior);
CMD_ActiveType_t active = CMD_BehaviorToActive(cmd, behavior);
bool now_key_pressed, last_key_pressed;
/* 按下按键为鼠标左、右键 */
if (key == CMD_L_CLICK) {
now_key_pressed = rc->mouse.l_click;
last_key_pressed = cmd->mouse_last.l_click;
} else if (key == CMD_R_CLICK) {
now_key_pressed = rc->mouse.r_click;
last_key_pressed = cmd->mouse_last.r_click;
} else {
now_key_pressed = rc->key & (1u << key);
last_key_pressed = cmd->key_last & (1u << key);
}
switch (active) {
case CMD_ACTIVE_PRESSING:
return now_key_pressed && !last_key_pressed;
case CMD_ACTIVE_RASING:
return !now_key_pressed && last_key_pressed;
case CMD_ACTIVE_PRESSED:
return now_key_pressed;
}
}
/**
* @brief pc行为逻辑
*
* @param rc
* @param cmd
* @param dt_sec
*/
static void CMD_PcLogic(const CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
/* 云台设置为鼠标控制欧拉角的变化,底盘的控制向量设置为零 */
cmd->gimbal.delta_eulr.yaw =
(float)rc->mouse.x * dt_sec * cmd->param->sens_mouse;
cmd->gimbal.delta_eulr.pit =
(float)(-rc->mouse.y) * dt_sec * cmd->param->sens_mouse;
cmd->chassis.ctrl_vec.vx = cmd->chassis.ctrl_vec.vy = 0.0f;
cmd->shoot.reverse_trig = false;
/* 按键行为映射相关逻辑 */
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FORE)) {
cmd->chassis.ctrl_vec.vy += cmd->param->move.move_sense;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_BACK)) {
cmd->chassis.ctrl_vec.vy -= cmd->param->move.move_sense;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_LEFT)) {
cmd->chassis.ctrl_vec.vx -= cmd->param->move.move_sense;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_RIGHT)) {
cmd->chassis.ctrl_vec.vx += cmd->param->move.move_sense;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_ACCELERATE)) {
cmd->chassis.ctrl_vec.vx *= cmd->param->move.move_fast_sense;
cmd->chassis.ctrl_vec.vy *= cmd->param->move.move_fast_sense;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_DECELEBRATE)) {
cmd->chassis.ctrl_vec.vx *= cmd->param->move.move_slow_sense;
cmd->chassis.ctrl_vec.vy *= cmd->param->move.move_slow_sense;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FIRE)) {
/* 切换至开火模式,设置相应的射击频率和弹丸初速度 */
cmd->shoot.mode = SHOOT_MODE_LOADED;
cmd->shoot.fire = true;
} else {
/* 切换至准备模式,停止射击 */
cmd->shoot.mode = SHOOT_MODE_LOADED;
cmd->shoot.fire = false;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FIRE_MODE)) {
/* 每按一次依次切换开火下一个模式 */
cmd->shoot.fire_mode++;
cmd->shoot.fire_mode %= FIRE_MODE_NUM;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_ROTOR)) {
/* 切换到小陀螺模式 */
cmd->chassis.mode = CHASSIS_MODE_ROTOR;
cmd->chassis.mode_rotor = ROTOR_MODE_RAND;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_OPENCOVER)) {
/* 每按一次开、关弹舱盖 */
cmd->shoot.cover_open = !cmd->shoot.cover_open;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_BUFF)) {
if (cmd->ai_status == AI_STATUS_HITSWITCH) {
/* 停止ai的打符模式停用host控制 */
CMD_RefereeAdd(&(cmd->referee), CMD_UI_HIT_SWITCH_STOP);
cmd->host_overwrite = false;
cmd->ai_status = AI_STATUS_STOP;
} else if (cmd->ai_status == AI_STATUS_AUTOAIM) {
/* 自瞄模式中切换失败提醒 */
} else {
/* ai切换至打符模式启用host控制 */
CMD_RefereeAdd(&(cmd->referee), CMD_UI_HIT_SWITCH_START);
cmd->ai_status = AI_STATUS_HITSWITCH;
cmd->host_overwrite = true;
}
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_AUTOAIM)) {
if (cmd->ai_status == AI_STATUS_AUTOAIM) {
/* 停止ai的自瞄模式停用host控制 */
cmd->host_overwrite = false;
cmd->ai_status = AI_STATUS_STOP;
CMD_RefereeAdd(&(cmd->referee), CMD_UI_AUTO_AIM_STOP);
} else {
/* ai切换至自瞄模式启用host控制 */
cmd->ai_status = AI_STATUS_AUTOAIM;
cmd->host_overwrite = true;
CMD_RefereeAdd(&(cmd->referee), CMD_UI_AUTO_AIM_START);
}
} else {
cmd->host_overwrite = false;
// TODO: 修复逻辑
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_REVTRIG)) {
/* 按下拨弹反转 */
cmd->shoot.reverse_trig = true;
}
if (CMD_BehaviorOccurredRc(rc, cmd, CMD_BEHAVIOR_FOLLOWGIMBAL35)) {
cmd->chassis.mode = CHASSIS_MODE_FOLLOW_GIMBAL_35;
}
/* 保存当前按下的键位状态 */
cmd->key_last = rc->key;
memcpy(&(cmd->mouse_last), &(rc->mouse), sizeof(cmd->mouse_last));
}
/**
* @brief rc行为逻辑
*
* @param rc
* @param cmd
* @param dt_sec
*/
static void CMD_RcLogic(const CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
switch (rc->sw_l) {
/* 左拨杆相应行为选择和解析 */
case CMD_SW_UP:
cmd->chassis.mode = CHASSIS_MODE_BREAK;
break;
case CMD_SW_MID:
cmd->chassis.mode = CHASSIS_MODE_FOLLOW_GIMBAL;
break;
case CMD_SW_DOWN:
cmd->chassis.mode = CHASSIS_MODE_ROTOR;
cmd->chassis.mode_rotor = ROTOR_MODE_CW;
break;
case CMD_SW_ERR:
cmd->chassis.mode = CHASSIS_MODE_RELAX;
break;
}
switch (rc->sw_r) {
/* 右拨杆相应行为选择和解析*/
case CMD_SW_UP:
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
cmd->shoot.mode = SHOOT_MODE_SAFE;
break;
case CMD_SW_MID:
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
cmd->shoot.fire = false;
cmd->shoot.mode = SHOOT_MODE_LOADED;
break;
case CMD_SW_DOWN:
cmd->gimbal.mode = GIMBAL_MODE_ABSOLUTE;
cmd->shoot.mode = SHOOT_MODE_LOADED;
cmd->shoot.fire_mode = FIRE_MODE_SINGLE;
cmd->shoot.fire = true;
break;
/*
case CMD_SW_UP:
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
cmd->shoot.mode = SHOOT_MODE_SAFE;
break;
case CMD_SW_MID:
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
cmd->shoot.fire = false;
cmd->shoot.mode = SHOOT_MODE_LOADED;
break;
case CMD_SW_DOWN:
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
cmd->shoot.mode = SHOOT_MODE_LOADED;
cmd->shoot.fire_mode = FIRE_MODE_SINGLE;
cmd->shoot.fire = true;
break;
*/
case CMD_SW_ERR:
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
cmd->shoot.mode = SHOOT_MODE_RELAX;
}
/* 将操纵杆的对应值转换为底盘的控制向量和云台变化的欧拉角 */
cmd->chassis.ctrl_vec.vx = rc->ch_l_x;
cmd->chassis.ctrl_vec.vy = rc->ch_l_y;
cmd->gimbal.delta_eulr.yaw = rc->ch_r_x * dt_sec * cmd->param->sens_rc;
cmd->gimbal.delta_eulr.pit = rc->ch_r_y * dt_sec * cmd->param->sens_rc;
}
/**
* @brief rc失控时机器人恢复放松模式
*
* @param cmd
*/
static void CMD_RcLostLogic(CMD_t *cmd) {
/* 机器人底盘、云台、射击运行模式恢复至放松模式 */
cmd->chassis.mode = CHASSIS_MODE_RELAX;
cmd->gimbal.mode = GIMBAL_MODE_RELAX;
cmd->shoot.mode = SHOOT_MODE_RELAX;
}
/**
* @brief
*
* @param cmd
* @param param
* @return int8_t 0
*/
int8_t CMD_Init(CMD_t *cmd, const CMD_Params_t *param) {
/* 指针检测 */
if (cmd == NULL) return -1;
if (param == NULL) return -1;
/* 设置机器人的命令参数初始化控制方式为rc控制 */
cmd->pc_ctrl = false;
cmd->param = param;
return 0;
}
/**
* @brief
*
* @param cmd
* @return true
* @return false
*/
inline bool CMD_CheckHostOverwrite(CMD_t *cmd) { return cmd->host_overwrite; }
/**
* @brief
*
* @param rc
* @param cmd
* @param dt_sec
* @return int8_t 0
*/
int8_t CMD_ParseRc(CMD_RC_t *rc, CMD_t *cmd, float dt_sec) {
/* 指针检测 */
if (rc == NULL) return -1;
if (cmd == NULL) return -1;
/* 在pc控制和rc控制间切换 */
if (CMD_KeyPressedRc(rc, CMD_KEY_SHIFT) &&
CMD_KeyPressedRc(rc, CMD_KEY_CTRL) && CMD_KeyPressedRc(rc, CMD_KEY_Q))
cmd->pc_ctrl = true;
if (CMD_KeyPressedRc(rc, CMD_KEY_SHIFT) &&
CMD_KeyPressedRc(rc, CMD_KEY_CTRL) && CMD_KeyPressedRc(rc, CMD_KEY_E))
cmd->pc_ctrl = false;
/*c当rc丢控时恢复机器人至默认状态 */
if ((rc->sw_l == CMD_SW_ERR) || (rc->sw_r == CMD_SW_ERR)) {
CMD_RcLostLogic(cmd);
} else {
if (cmd->pc_ctrl) {
CMD_PcLogic(rc, cmd, dt_sec);
} else {
CMD_RcLogic(rc, cmd, dt_sec);
}
}
return 0;
}
/**
* @brief
*
* @param host host数据
* @param cmd
* @param dt_sec
* @return int8_t 0
*/
int8_t CMD_ParseHost(const CMD_Host_t *host, CMD_t *cmd, float dt_sec) {
(void)dt_sec; /* 未使用dt_sec消除警告 */
/* 指针检测 */
if (host == NULL) return -1;
if (cmd == NULL) return -1;
/* 云台欧拉角设置为host相应的变化的欧拉角 */
cmd->gimbal.delta_eulr.yaw = host->gimbal_delta.yaw;
cmd->gimbal.delta_eulr.pit = host->gimbal_delta.pit;
/* host射击命令设置不同的射击频率和弹丸初速度 */
if (host->fire) {
cmd->shoot.mode = SHOOT_MODE_LOADED;
cmd->shoot.fire = true;
} else {
cmd->shoot.mode = SHOOT_MODE_SAFE;
}
return 0;
}
/**
* @brief Referee发送的命令
*
* @param ref
* @param cmd
* @return int8_t 0
*/
int8_t CMD_RefereeAdd(CMD_RefereeCmd_t *ref, CMD_UI_t cmd) {
/* 指针检测 */
if (ref == NULL) return -1;
/* 越界检测 */
if (ref->counter >= CMD_REFEREE_MAX_NUM || ref->counter < 0) return -1;
/* 添加机器人当前行为状态到画图的命令队列中 */
ref->cmd[ref->counter] = cmd;
ref->counter++;
return 0;
}
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */

View File

@ -0,0 +1,318 @@
/*
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include "component/ahrs.h"
/* USER INCLUDE BEGIN */
/* USER INCLUDE END */
#define CMD_REFEREE_MAX_NUM (3) /* Lines 16 omitted */
/* USER DEFINE BEGIN */
/* USER DEFINE END */
/* 机器人型号 */
typedef enum {
ROBOT_MODEL_INFANTRY = 0, /* 步兵机器人 */
ROBOT_MODEL_HERO, /* 英雄机器人 */
ROBOT_MODEL_ENGINEER, /* 工程机器人 */
ROBOT_MODEL_DRONE, /* 空中机器人 */
ROBOT_MODEL_SENTRY, /* 哨兵机器人 */
ROBOT_MODEL_NUM, /* 型号数量 */
} CMD_RobotModel_t;
/* 底盘运行模式 */
typedef enum {
CHASSIS_MODE_RELAX, /* 放松模式,电机不输出。一般情况底盘初始化之后的模式 */
CHASSIS_MODE_BREAK, /* 刹车模式,电机闭环控制保持静止。用于机器人停止状态 */
CHASSIS_MODE_FOLLOW_GIMBAL, /* 通过闭环控制使车头方向跟随云台 */
CHASSIS_MODE_FOLLOW_GIMBAL_35, /* 通过闭环控制使车头方向35度跟随云台 */
CHASSIS_MODE_ROTOR, /* 小陀螺模式,通过闭环控制使底盘不停旋转 */
CHASSIS_MODE_INDENPENDENT, /* 独立模式。底盘运行不受云台影响 */
CHASSIS_MODE_OPEN, /* 开环模式。底盘运行不受PID控制直接输出到电机 */
} CMD_ChassisMode_t;
/* 云台运行模式 */
typedef enum {
GIMBAL_MODE_RELAX, /* 放松模式,电机不输出。一般情况云台初始化之后的模式 */
GIMBAL_MODE_ABSOLUTE, /* 绝对坐标系控制,控制在空间内的绝对姿态 */
GIMBAL_MODE_RELATIVE, /* 相对坐标系控制,控制相对于底盘的姿态 */
} CMD_GimbalMode_t;
/* 射击运行模式 */
typedef enum {
SHOOT_MODE_RELAX, /* 放松模式,电机不输出 */
SHOOT_MODE_SAFE, /* 保险模式,电机闭环控制保持静止 */
SHOOT_MODE_LOADED, /* 上膛模式,摩擦轮开启。随时准备开火 */
} CMD_ShootMode_t;
typedef enum {
FIRE_MODE_SINGLE, /* 单发开火模式 */
FIRE_MODE_BURST, /* N连发开火模式 */
FIRE_MODE_CONT, /* 持续开火模式 */
FIRE_MODE_NUM,
} CMD_FireMode_t;
/* 小陀螺转动模式 */
typedef enum {
ROTOR_MODE_CW, /* 顺时针转动 */
ROTOR_MODE_CCW, /* 逆时针转动 */
ROTOR_MODE_RAND, /* 随机转动 */
} CMD_RotorMode_t;
/* 底盘控制命令 */
typedef struct {
CMD_ChassisMode_t mode; /* 底盘运行模式 */
CMD_RotorMode_t mode_rotor; /* 小陀螺转动模式 */
MoveVector_t ctrl_vec; /* 底盘控制向量 */
} CMD_ChassisCmd_t;
/* 云台控制命令 */
typedef struct {
CMD_GimbalMode_t mode; /* 云台运行模式 */
AHRS_Eulr_t delta_eulr; /* 欧拉角变化角度 */
} CMD_GimbalCmd_t;
/* 射击控制命令 */
typedef struct {
CMD_ShootMode_t mode; /* 射击运行模式 */
CMD_FireMode_t fire_mode; /* 开火模式 */
bool fire; /*开火*/
bool cover_open; /* 弹舱盖开关 */
bool reverse_trig; /* 拨弹电机状态 */
} CMD_ShootCmd_t;
/* 拨杆位置 */
typedef enum {
CMD_SW_ERR = 0,
CMD_SW_UP = 1,
CMD_SW_MID = 3,
CMD_SW_DOWN = 2,
} CMD_SwitchPos_t;
/* 键盘按键值 */
typedef enum {
CMD_KEY_W = 0,
CMD_KEY_S,
CMD_KEY_A,
CMD_KEY_D,
CMD_KEY_SHIFT,
CMD_KEY_CTRL,
CMD_KEY_Q,
CMD_KEY_E,
CMD_KEY_R,
CMD_KEY_F,
CMD_KEY_G,
CMD_KEY_Z,
CMD_KEY_X,
CMD_KEY_C,
CMD_KEY_V,
CMD_KEY_B,
CMD_L_CLICK,
CMD_R_CLICK,
CMD_KEY_NUM,
} CMD_KeyValue_t;
/* 行为值序列 */
typedef enum {
CMD_BEHAVIOR_FORE = 0, /* 向前 */
CMD_BEHAVIOR_BACK, /* 向后 */
CMD_BEHAVIOR_LEFT, /* 向左 */
CMD_BEHAVIOR_RIGHT, /* 向右 */
CMD_BEHAVIOR_ACCELERATE, /* 加速 */
CMD_BEHAVIOR_DECELEBRATE, /* 减速 */
CMD_BEHAVIOR_FIRE, /* 开火 */
CMD_BEHAVIOR_FIRE_MODE, /* 切换开火模式 */
CMD_BEHAVIOR_BUFF, /* 打符模式 */
CMD_BEHAVIOR_AUTOAIM, /* 自瞄模式 */
CMD_BEHAVIOR_OPENCOVER, /* 弹舱盖开关 */
CMD_BEHAVIOR_ROTOR, /* 小陀螺模式 */
CMD_BEHAVIOR_REVTRIG, /* 反转拨弹 */
CMD_BEHAVIOR_FOLLOWGIMBAL35, /* 跟随云台呈35度 */
CMD_BEHAVIOR_NUM,
} CMD_Behavior_t;
typedef enum {
CMD_ACTIVE_PRESSING, /* 按下时触发 */
CMD_ACTIVE_RASING, /* 抬起时触发 */
CMD_ACTIVE_PRESSED, /* 按住时触发 */
} CMD_ActiveType_t;
typedef struct {
CMD_ActiveType_t active;
CMD_KeyValue_t key;
} CMD_KeyMapItem_t;
/* 行为映射的对应按键数组 */
typedef struct {
CMD_KeyMapItem_t key_map[CMD_BEHAVIOR_NUM];
} CMD_KeyMap_Params_t;
/* 位移灵敏度参数 */
typedef struct {
float move_sense; /* 移动灵敏度 */
float move_fast_sense; /* 加速灵敏度 */
float move_slow_sense; /* 减速灵敏度 */
} CMD_Move_Params_t;
typedef struct {
uint16_t width;
uint16_t height;
} CMD_Screen_t;
/* 命令参数 */
typedef struct {
float sens_mouse; /* 鼠标灵敏度 */
float sens_rc; /* 遥控器摇杆灵敏度 */
CMD_KeyMap_Params_t map; /* 按键映射行为命令 */
CMD_Move_Params_t move; /* 位移灵敏度参数 */
CMD_Screen_t screen; /* 屏幕分辨率参数 */
} CMD_Params_t;
/* AI行为状态 */
typedef enum {
AI_STATUS_STOP, /* 停止状态 */
AI_STATUS_AUTOAIM, /* 自瞄状态 */
AI_STATUS_HITSWITCH, /* 打符状态 */
AI_STATUS_AUTOMATIC /* 自动状态 */
} CMD_AI_Status_t;
/* UI所用行为状态 */
typedef enum {
CMD_UI_NOTHING, /* 当前无状态 */
CMD_UI_AUTO_AIM_START, /* 自瞄状态开启 */
CMD_UI_AUTO_AIM_STOP, /* 自瞄状态关闭 */
CMD_UI_HIT_SWITCH_START, /* 打符状态开启 */
CMD_UI_HIT_SWITCH_STOP /* 打符状态关闭 */
} CMD_UI_t;
/*裁判系统发送的命令*/
typedef struct {
CMD_UI_t cmd[CMD_REFEREE_MAX_NUM]; /* 命令数组 */
uint8_t counter; /* 命令计数 */
} CMD_RefereeCmd_t;
typedef struct {
bool pc_ctrl; /* 是否使用键鼠控制 */
bool host_overwrite; /* 是否Host控制 */
uint16_t key_last; /* 上次按键键值 */
struct {
int16_t x;
int16_t y;
int16_t z;
bool l_click; /* 左键 */
bool r_click; /* 右键 */
} mouse_last; /* 鼠标值 */
CMD_AI_Status_t ai_status; /* AI状态 */
const CMD_Params_t *param; /* 命令参数 */
CMD_ChassisCmd_t chassis; /* 底盘控制命令 */
CMD_GimbalCmd_t gimbal; /* 云台控制命令 */
CMD_ShootCmd_t shoot; /* 射击控制命令 */
CMD_RefereeCmd_t referee; /* 裁判系统发送命令 */
} CMD_t;
typedef struct {
float ch_l_x; /* 遥控器左侧摇杆横轴值,上为正 */
float ch_l_y; /* 遥控器左侧摇杆纵轴值,右为正 */
float ch_r_x; /* 遥控器右侧摇杆横轴值,上为正 */
float ch_r_y; /* 遥控器右侧摇杆纵轴值,右为正 */
float ch_res; /* 第五通道值 */
CMD_SwitchPos_t sw_r; /* 右侧拨杆位置 */
CMD_SwitchPos_t sw_l; /* 左侧拨杆位置 */
struct {
int16_t x;
int16_t y;
int16_t z;
bool l_click; /* 左键 */
bool r_click; /* 右键 */
} mouse; /* 鼠标值 */
uint16_t key; /* 按键值 */
uint16_t res; /* 保留,未启用 */
} CMD_RC_t;
typedef struct {
AHRS_Eulr_t gimbal_delta; /* 欧拉角的变化量 */
struct {
float vx; /* x轴移动速度 */
float vy; /* y轴移动速度 */
float wz; /* z轴转动速度 */
} chassis_move_vec; /* 底盘移动向量 */
bool fire; /* 开火状态 */
} CMD_Host_t;
/**
* @brief
*
* @param rc
* @param cmd
*/
int8_t CMD_Init(CMD_t *cmd, const CMD_Params_t *param);
/**
* @brief
*
* @param cmd
* @return true
* @return false
*/
bool CMD_CheckHostOverwrite(CMD_t *cmd);
/**
* @brief
*
* @param rc
* @param cmd
* @param dt_sec
* @return int8_t 0
*/
int8_t CMD_ParseRc(CMD_RC_t *rc, CMD_t *cmd, float dt_sec);
/**
* @brief
*
* @param host host数据
* @param cmd
* @param dt_sec
* @return int8_t 0
*/
int8_t CMD_ParseHost(const CMD_Host_t *host, CMD_t *cmd, float dt_sec);
/**
* @brief Referee发送的命令
*
* @param ref
* @param cmd
* @return int8_t 0
*/
int8_t CMD_RefereeAdd(CMD_RefereeCmd_t *ref, CMD_UI_t cmd);
/* USER FUNCTION BEGIN */
/* USER FUNCTION END */
#ifdef __cplusplus
}
#endif

View File

@ -1,4 +1,4 @@
bsp,can,dwt,gpio,i2c,mm,spi,uart,pwm,time
component,ahrs,capacity,crc8,crc16,error_detect,filter,FreeRTOS_CLI,limiter,mixer,pid,ui,user_math
device,dr16,bmi088,ist8310,motor,motor_rm,motor_dm,motor_vesc,motor_lk,motor_lz,motor_odrive,dm_imu,rc_can,servo,buzzer,led,ws2812,vofa,ops9,ai
bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time
component,ahrs,capacity,cmd,crc8,crc16,error_detect,filter,FreeRTOS_CLI,limiter,mixer,pid,ui,user_math
device,dr16,bmi088,ist8310,motor,motor_rm,motor_dm,motor_vesc,motor_lk,motor_lz,motor_odrive,dm_imu,rc_can,servo,buzzer,led,ws2812,vofa,ops9,oid,lcd_driver
module,config,
1 bsp,can,dwt,gpio,i2c,mm,spi,uart,pwm,time bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time
2 component,ahrs,capacity,crc8,crc16,error_detect,filter,FreeRTOS_CLI,limiter,mixer,pid,ui,user_math component,ahrs,capacity,cmd,crc8,crc16,error_detect,filter,FreeRTOS_CLI,limiter,mixer,pid,ui,user_math
3 device,dr16,bmi088,ist8310,motor,motor_rm,motor_dm,motor_vesc,motor_lk,motor_lz,motor_odrive,dm_imu,rc_can,servo,buzzer,led,ws2812,vofa,ops9,ai device,dr16,bmi088,ist8310,motor,motor_rm,motor_dm,motor_vesc,motor_lk,motor_lz,motor_odrive,dm_imu,rc_can,servo,buzzer,led,ws2812,vofa,ops9,oid,lcd_driver
4 module,config, module,config,

View File

@ -1,142 +0,0 @@
/*
AI
*/
/* Includes ----------------------------------------------------------------- */
#include "ai.h"
#include <stdbool.h>
#include <string.h>
#include "bsp/time.h"
#include "bsp/uart.h"
#include "component/ahrs.h"
#include "component/crc16.h"
#include "component/crc8.h"
#include "component/user_math.h"
#include "component/filter.h"
/* Private define ----------------------------------------------------------- */
#define AI_LEN_RX_BUFF (sizeof(AI_DownPackage_t))
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* Private variables -------------------------------------------------------- */
static uint8_t rxbuf[AI_LEN_RX_BUFF];
static bool inited = false;
static osThreadId_t thread_alert;
static uint32_t drop_message = 0;
// uint16_t crc16;
/* Private function -------------------------------------------------------- */
static void Ai_RxCpltCallback(void) {
osThreadFlagsSet(thread_alert, SIGNAL_AI_RAW_REDY);
}
static void Ai_IdleLineCallback(void) {
osThreadFlagsSet(thread_alert, SIGNAL_AI_RAW_REDY);
}
/* Exported functions ------------------------------------------------------- */
int8_t AI_Init(AI_t *ai) {
UNUSED(ai);
if (inited) return DEVICE_ERR_INITED;
thread_alert = osThreadGetId();
BSP_UART_RegisterCallback(BSP_UART_AI, BSP_UART_RX_CPLT_CB,
Ai_RxCpltCallback);
BSP_UART_RegisterCallback(BSP_UART_AI, BSP_UART_IDLE_LINE_CB,
Ai_IdleLineCallback);
inited = true;
return 0;
}
int8_t AI_Restart(AI_t *ai) {
UNUSED(ai);
__HAL_UART_DISABLE(BSP_UART_GetHandle(BSP_UART_AI));
__HAL_UART_ENABLE(BSP_UART_GetHandle(BSP_UART_AI));
return DEVICE_OK;
}
int8_t AI_StartReceiving(AI_t *ai) {
UNUSED(ai);
// if (HAL_UART_Receive_DMA(BSP_UART_GetHandle(BSP_UART_AI), rxbuf,
// AI_LEN_RX_BUFF) == HAL_OK)
if (BSP_UART_Receive(BSP_UART_AI, rxbuf,
AI_LEN_RX_BUFF, true) == HAL_OK)
return DEVICE_OK;
return DEVICE_ERR;
}
bool AI_WaitDmaCplt(void) {
return (osThreadFlagsWait(SIGNAL_AI_RAW_REDY, osFlagsWaitAll,0) ==
SIGNAL_AI_RAW_REDY);
}
int8_t AI_ParseHost(AI_t *ai) {
// crc16 = CRC16_Calc((const uint8_t *)&(rxbuf), sizeof(ai->from_host) - 2, CRC16_INIT);
if (!CRC16_Verify((const uint8_t *)&(rxbuf), sizeof(ai->from_host)))
goto error;
ai->header.online = true;
ai->header.last_online_time = BSP_TIME_Get();
memcpy(&(ai->from_host), rxbuf, sizeof(ai->from_host));
memset(rxbuf, 0, AI_LEN_RX_BUFF);
return DEVICE_OK;
error:
drop_message++;
return DEVICE_ERR;
}
int8_t AI_PackMCU(AI_t *ai, const AHRS_Quaternion_t *data){
if (ai == NULL || data == NULL) return DEVICE_ERR_NULL;
ai->to_host.mcu.id = AI_ID_MCU;
ai->to_host.mcu.package.quat=*data;
ai->to_host.mcu.package.notice = ai->status;
ai->to_host.mcu.crc16 = CRC16_Calc((const uint8_t *)&(ai->to_host.mcu), sizeof(AI_UpPackageMCU_t) - 2, CRC16_INIT);
return DEVICE_OK;
}
int8_t AI_PackRef(AI_t *ai, const AI_UpPackageReferee_t *data) {
if (ai == NULL || data == NULL) return DEVICE_ERR_NULL;
ai->to_host.ref = *data;
return DEVICE_OK;
}
int8_t AI_HandleOffline(AI_t *ai) {
if (ai == NULL) return DEVICE_ERR_NULL;
if (BSP_TIME_Get() - ai->header.last_online_time >
100000) {
ai->header.online = false;
}
return DEVICE_OK;
}
int8_t AI_StartSend(AI_t *ai, bool ref_online){
if (ai == NULL) return DEVICE_ERR_NULL;
if (ref_online) {
// 发送裁判系统数据和MCU数据
if (BSP_UART_Transmit(BSP_UART_AI, (uint8_t *)&(ai->to_host),
sizeof(ai->to_host.ref) + sizeof(ai->to_host.mcu), true) == HAL_OK)
return DEVICE_OK;
else
return DEVICE_ERR;
} else {
// 只发送MCU数据
if (BSP_UART_Transmit(BSP_UART_AI, (uint8_t *)&(ai->to_host.mcu),
sizeof(ai->to_host.mcu), true) == HAL_OK)
return DEVICE_OK;
else
return DEVICE_ERR;
}
}

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