MRobot/app/code_page/bsp_interface.py

583 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QCheckBox, QComboBox, QTableWidget, QHeaderView, QMessageBox, QHBoxLayout
from qfluentwidgets import TitleLabel, BodyLabel, PushButton, CheckBox, TableWidget, LineEdit, ComboBox,MessageBox,SubtitleLabel,FluentIcon
from qfluentwidgets import InfoBar
from PyQt5.QtCore import Qt
from app.tools.analyzing_ioc import analyzing_ioc
from app.tools.code_generator import CodeGenerator
import os
import csv
import shutil
def preserve_all_user_regions(new_code, old_code):
""" Preserves all user-defined regions in the new code based on the old code.
This function uses regex to find user-defined regions in the old code and replaces them in the new code.
Args:
new_code (str): The new code content.
old_code (str): The old code content.
Returns:
str: The new code with preserved user-defined regions.
"""
import re
pattern = re.compile(
r"/\*\s*(USER [A-Z0-9_ ]+)\s*BEGIN\s*\*/(.*?)/\*\s*\1\s*END\s*\*/",
re.DOTALL
)
old_regions = {m.group(1): m.group(2) for m in pattern.finditer(old_code or "")}
def repl(m):
region = m.group(1)
old_content = old_regions.get(region)
if old_content is not None:
return m.group(0).replace(m.group(2), old_content)
return m.group(0)
return pattern.sub(repl, new_code)
def save_with_preserve(path, new_code):
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
old_code = f.read()
new_code = preserve_all_user_regions(new_code, old_code)
with open(path, "w", encoding="utf-8") as f:
f.write(new_code)
class BspSimplePeripheral(QWidget):
def __init__(self, project_path, peripheral_name, template_names):
super().__init__()
self.project_path = project_path
self.peripheral_name = peripheral_name
self.template_names = template_names
# 加载描述
describe_path = os.path.join(os.path.dirname(__file__), "../../assets/User_code/bsp/describe.csv")
self.descriptions = load_descriptions(describe_path)
self._init_ui()
self._load_config()
def _init_ui(self):
layout = QVBoxLayout(self)
# 顶部横向布局:左侧复选框,居中标题
top_layout = QHBoxLayout()
top_layout.setAlignment(Qt.AlignVCenter)
self.generate_checkbox = CheckBox(f"启用 {self.peripheral_name}")
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
# 弹性空间
top_layout.addStretch()
title = SubtitleLabel(f"{self.peripheral_name} 配置 ")
title.setAlignment(Qt.AlignHCenter)
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
# 再加一个弹性空间,保证标题居中
top_layout.addStretch()
layout.addLayout(top_layout)
desc = self.descriptions.get(self.peripheral_name.lower(), "")
if desc:
desc_label = BodyLabel(f"功能说明:{desc}")
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
layout.addStretch()
def is_need_generate(self):
return self.generate_checkbox.isChecked()
def _generate_bsp_code_internal(self):
if not self.is_need_generate():
return False
template_dir = CodeGenerator.get_template_dir()
for key, filename in self.template_names.items():
template_path = os.path.join(template_dir, filename)
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
output_path = os.path.join(self.project_path, f"User/bsp/{filename}")
save_with_preserve(output_path, template_content) # 使用保留用户区域的写入
self._save_config()
return True
def _save_config(self):
config_path = os.path.join(self.project_path, "User/bsp/bsp_config.yaml")
config_data = CodeGenerator.load_config(config_path)
config_data[self.peripheral_name.lower()] = {'enabled': True}
CodeGenerator.save_config(config_data, config_path)
def _load_config(self):
config_path = os.path.join(self.project_path, "User/bsp/bsp_config.yaml")
config_data = CodeGenerator.load_config(config_path)
conf = config_data.get(self.peripheral_name.lower(), {})
if conf.get('enabled', False):
self.generate_checkbox.setChecked(True)
class BspPeripheralBase(QWidget):
def __init__(self, project_path, peripheral_name, template_names, enum_prefix, handle_prefix, yaml_key, get_available_func):
super().__init__()
self.project_path = project_path
self.peripheral_name = peripheral_name
self.template_names = template_names
self.enum_prefix = enum_prefix
self.handle_prefix = handle_prefix
self.yaml_key = yaml_key
self.get_available_func = get_available_func
self.available_list = []
# 新增:加载描述
describe_path = os.path.join(os.path.dirname(__file__), "../../assets/User_code/bsp/describe.csv")
self.descriptions = load_descriptions(describe_path)
self._init_ui()
self._load_config()
def _init_ui(self):
layout = QVBoxLayout(self)
top_layout = QHBoxLayout()
top_layout.setAlignment(Qt.AlignVCenter)
self.generate_checkbox = CheckBox(f"生成 {self.peripheral_name} 代码")
self.generate_checkbox.stateChanged.connect(self._on_generate_changed)
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
top_layout.addStretch()
title = SubtitleLabel(f"{self.peripheral_name} 配置 ")
title.setAlignment(Qt.AlignHCenter)
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
top_layout.addStretch()
layout.addLayout(top_layout)
desc = self.descriptions.get(self.peripheral_name.lower(), "")
if desc:
desc_label = BodyLabel(f"功能说明:{desc}")
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
self.content_widget = QWidget()
content_layout = QVBoxLayout(self.content_widget)
self._get_available_list()
if not self.available_list:
content_layout.addWidget(BodyLabel(f"在 .ioc 文件中未找到已启用的 {self.peripheral_name}"))
else:
content_layout.addWidget(BodyLabel(f"可用的 {self.peripheral_name}: {', '.join(self.available_list)}"))
self.table = TableWidget()
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["设备名称", f"{self.peripheral_name}选择", "操作"])
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
content_layout.addWidget(self.table)
add_btn = PushButton(f"添加 {self.peripheral_name} 设备")
add_btn.clicked.connect(self._add_device)
content_layout.addWidget(add_btn)
layout.addWidget(self.content_widget)
self.content_widget.setEnabled(False)
def _get_available_list(self):
self.available_list = self.get_available_func(self.project_path)
def _on_generate_changed(self, state):
self.content_widget.setEnabled(state == 2)
def _add_device(self):
row = self.table.rowCount()
self.table.insertRow(row)
name_edit = LineEdit()
name_edit.setPlaceholderText(f"输入设备名称")
self.table.setCellWidget(row, 0, name_edit)
combo = ComboBox() # 使用 Fluent 风格 ComboBox
combo.addItems(self.available_list)
self.table.setCellWidget(row, 1, combo)
del_btn = PushButton(FluentIcon.DELETE,"删除" ) # 添加垃圾桶图标
del_btn.clicked.connect(lambda: self._delete_device(row))
self.table.setCellWidget(row, 2, del_btn)
def _delete_device(self, row):
self.table.removeRow(row)
def _collect_configs(self):
configs = []
for row in range(self.table.rowCount()):
name_widget = self.table.cellWidget(row, 0)
sel_widget = self.table.cellWidget(row, 1)
if name_widget and sel_widget:
name = name_widget.text().strip()
sel = sel_widget.currentText()
if name and sel:
configs.append((name.upper(), sel))
return configs
def is_need_generate(self):
return self.generate_checkbox.isChecked() and bool(self._collect_configs())
def generate_bsp_code(self):
if not self.is_need_generate():
return False
configs = self._collect_configs()
if not configs:
return False
template_dir = CodeGenerator.get_template_dir()
if not self._generate_header_file(configs, template_dir):
return False
if not self._generate_source_file(configs, template_dir):
return False
self._save_config(configs)
InfoBar.success(
title="任务生成成功",
content=f"{self.peripheral_name} 代码生成成功!",
parent=self,
duration=2000
)
return True
def _generate_header_file(self, configs, template_dir):
template_path = os.path.join(template_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)
)
output_path = os.path.join(self.project_path, f"User/bsp/{self.template_names['header']}")
save_with_preserve(output_path, content) # 使用保留用户区域的写入
return True
def _generate_source_file(self, configs, template_dir):
template_path = os.path.join(template_dir, self.template_names['source'])
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
# Get函数
get_lines = []
for idx, (name, instance) in enumerate(configs):
if idx == 0:
get_lines.append(f" if ({self.handle_prefix}->Instance == {instance})")
else:
get_lines.append(f" else if ({self.handle_prefix}->Instance == {instance})")
get_lines.append(f" return {self.enum_prefix}_{name};")
content = CodeGenerator.replace_auto_generated(
template_content, f"AUTO GENERATED {self.enum_prefix.split('_')[1]}_GET", "\n".join(get_lines)
)
# Handle函数
handle_lines = []
for name, instance in configs:
handle_lines.append(f" case {self.enum_prefix}_{name}:")
# UART/USART统一用 huart 前缀
if self.enum_prefix == "BSP_UART":
# 提取数字部分
num = ''.join(filter(str.isdigit, instance))
handle_lines.append(f" return &huart{num};")
else:
handle_lines.append(f" return &h{instance.lower()};")
content = CodeGenerator.replace_auto_generated(
content, f"AUTO GENERATED {self.enum_prefix}_GET_HANDLE", "\n".join(handle_lines)
)
output_path = os.path.join(self.project_path, f"User/bsp/{self.template_names['source']}")
save_with_preserve(output_path, content) # 使用保留用户区域的写入
return True
def _save_config(self, configs):
config_path = os.path.join(self.project_path, "User/bsp/bsp_config.yaml")
config_data = CodeGenerator.load_config(config_path)
config_data[self.yaml_key] = {
'enabled': True,
'devices': [{'name': name, 'instance': instance} for name, instance in configs]
}
CodeGenerator.save_config(config_data, config_path)
def _load_config(self):
config_path = os.path.join(self.project_path, "User/bsp/bsp_config.yaml")
config_data = CodeGenerator.load_config(config_path)
conf = config_data.get(self.yaml_key, {})
if conf.get('enabled', False):
self.generate_checkbox.setChecked(True)
devices = conf.get('devices', [])
for device in devices:
if self.available_list:
self._add_device()
row = self.table.rowCount() - 1
name_widget = self.table.cellWidget(row, 0)
sel_widget = self.table.cellWidget(row, 1)
if name_widget:
name_widget.setText(device.get('name', ''))
if sel_widget:
instance = device.get('instance', '')
if instance in self.available_list:
sel_widget.setCurrentText(instance)
def _generate_bsp_code_internal(self):
if not self.is_need_generate():
return False
configs = self._collect_configs()
if not configs:
return False
template_dir = CodeGenerator.get_template_dir()
if not self._generate_header_file(configs, template_dir):
return False
if not self._generate_source_file(configs, template_dir):
return False
self._save_config(configs)
return True
def load_descriptions(csv_path):
descriptions = {}
if os.path.exists(csv_path):
with open(csv_path, encoding='utf-8') as f:
reader = csv.reader(f)
for row in reader:
if len(row) >= 2:
key, desc = row[0].strip(), row[1].strip()
descriptions[key.lower()] = desc
return descriptions
# 各外设的可用列表获取函数
def get_available_i2c(project_path):
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_i2c_from_ioc(ioc_path)
return []
def get_available_can(project_path):
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_can_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:
ioc_path = os.path.join(project_path, ioc_files[0])
return analyzing_ioc.get_enabled_spi_from_ioc(ioc_path)
return []
def get_available_uart(project_path):
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_uart_from_ioc(ioc_path)
return []
def get_available_gpio(project_path):
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_gpio_from_ioc(ioc_path)
return []
# 具体外设类
class bsp_i2c(BspPeripheralBase):
def __init__(self, project_path):
super().__init__(
project_path,
"I2C",
{'header': 'i2c.h', 'source': 'i2c.c'},
"BSP_I2C",
"hi2c",
"i2c",
get_available_i2c
)
class bsp_can(BspPeripheralBase):
def __init__(self, project_path):
super().__init__(
project_path,
"CAN",
{'header': 'can.h', 'source': 'can.c'},
"BSP_CAN",
"hcan",
"can",
get_available_can
)
class bsp_spi(BspPeripheralBase):
def __init__(self, project_path):
super().__init__(
project_path,
"SPI",
{'header': 'spi.h', 'source': 'spi.c'},
"BSP_SPI",
"hspi",
"spi",
get_available_spi
)
class bsp_uart(BspPeripheralBase):
def __init__(self, project_path):
super().__init__(
project_path,
"UART",
{'header': 'uart.h', 'source': 'uart.c'},
"BSP_UART",
"huart",
"uart",
get_available_uart
)
class bsp_gpio(QWidget):
def __init__(self, project_path):
super().__init__()
self.project_path = project_path
self.available_list = get_available_gpio(project_path)
# 新增:加载描述
describe_path = os.path.join(os.path.dirname(__file__), "../../assets/User_code/bsp/describe.csv")
self.descriptions = load_descriptions(describe_path)
self._init_ui()
def _init_ui(self):
layout = QVBoxLayout(self)
top_layout = QHBoxLayout()
top_layout.setAlignment(Qt.AlignVCenter)
self.generate_checkbox = CheckBox("生成 GPIO 代码")
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
top_layout.addStretch()
title = SubtitleLabel("GPIO 配置 ")
title.setAlignment(Qt.AlignHCenter)
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
top_layout.addStretch()
layout.addLayout(top_layout)
desc = self.descriptions.get("gpio", "")
if desc:
desc_label = BodyLabel(f"功能说明:{desc}")
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
if not self.available_list:
layout.addWidget(BodyLabel("在 .ioc 文件中未找到可用的 GPIO"))
else:
self.table = TableWidget()
self.table.setColumnCount(1)
self.table.setRowCount(len(self.available_list))
self.table.setHorizontalHeaderLabels(["Label"])
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
for row, item in enumerate(self.available_list):
from PyQt5.QtWidgets import QTableWidgetItem
self.table.setItem(row, 0, QTableWidgetItem(item['label']))
self.table.setEditTriggers(TableWidget.NoEditTriggers)
layout.addWidget(self.table)
def is_need_generate(self):
return self.generate_checkbox.isChecked() and bool(self.available_list)
def _generate_bsp_code_internal(self):
if not self.is_need_generate():
return False
template_dir = CodeGenerator.get_template_dir()
if not self._generate_header_file(template_dir):
return False
if not self._generate_source_file(template_dir):
return False
self._save_config()
return True
def _generate_header_file(self, template_dir):
template_path = os.path.join(template_dir, "gpio.h")
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
output_path = os.path.join(self.project_path, "User/bsp/gpio.h")
save_with_preserve(output_path, template_content) # 使用保留用户区域的写入
return True
def _generate_source_file(self, template_dir):
template_path = os.path.join(template_dir, "gpio.c")
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
enable_lines = []
disable_lines = []
for item in self.available_list:
label = item['label']
enable_lines.append(f" case {label}_Pin:")
enable_lines.append(f" HAL_NVIC_EnableIRQ({label}_EXTI_IRQn);")
enable_lines.append(f" break;")
disable_lines.append(f" case {label}_Pin:")
disable_lines.append(f" HAL_NVIC_DisableIRQ({label}_EXTI_IRQn);")
disable_lines.append(f" break;")
content = CodeGenerator.replace_auto_generated(
template_content, "AUTO GENERATED BSP_GPIO_ENABLE_IRQ", "\n".join(enable_lines)
)
content = CodeGenerator.replace_auto_generated(
content, "AUTO GENERATED BSP_GPIO_DISABLE_IRQ", "\n".join(disable_lines)
)
output_path = os.path.join(self.project_path, "User/bsp/gpio.c")
save_with_preserve(output_path, content) # 使用保留用户区域的写入
return True
def _save_config(self):
config_path = os.path.join(self.project_path, "User/bsp/bsp_config.yaml")
config_data = CodeGenerator.load_config(config_path)
config_data['gpio'] = {
'enabled': True,
'labels': [item['label'] for item in self.available_list]
}
CodeGenerator.save_config(config_data, config_path)
def get_bsp_page(peripheral_name, project_path):
"""根据外设名返回对应的页面类没有特殊类则返回默认BspSimplePeripheral"""
name_lower = peripheral_name.lower()
special_classes = {
"i2c": bsp_i2c,
"can": bsp_can,
"spi": bsp_spi,
"uart": bsp_uart,
"gpio": bsp_gpio,
# 以后可以继续添加特殊外设
}
if name_lower in special_classes:
return special_classes[name_lower](project_path)
else:
template_names = {
'header': f'{name_lower}.h',
'source': f'{name_lower}.c'
}
return BspSimplePeripheral(project_path, peripheral_name, template_names)
class bsp(QWidget):
def __init__(self, project_path):
super().__init__()
self.project_path = project_path
@staticmethod
def generate_bsp(project_path, pages):
"""生成所有BSP代码"""
# 自动添加 bsp.h
src_bsp_h = os.path.join(os.path.dirname(__file__), "../../assets/User_code/bsp/bsp.h")
dst_bsp_h = os.path.join(project_path, "User/bsp/bsp.h")
os.makedirs(os.path.dirname(dst_bsp_h), exist_ok=True)
if os.path.exists(src_bsp_h):
shutil.copyfile(src_bsp_h, dst_bsp_h)
total = 0
success_count = 0
fail_count = 0
fail_list = []
for page in pages:
# 只处理BSP页面有 is_need_generate 方法但没有 component_name 属性的页面
if hasattr(page, 'is_need_generate') and not hasattr(page, 'component_name'):
if page.is_need_generate():
total += 1
try:
result = page._generate_bsp_code_internal()
if result:
success_count += 1
else:
fail_count += 1
fail_list.append(page.__class__.__name__)
except Exception as e:
fail_count += 1
fail_list.append(f"{page.__class__.__name__} (异常: {e})")
msg = f"总共尝试生成 {total} 项,成功 {success_count} 项,失败 {fail_count} 项。"
if fail_list:
msg += "\n失败项:\n" + "\n".join(fail_list)
return msg