mirror of
https://github.com/goldenfishs/MRobot.git
synced 2025-09-14 12:54:33 +08:00
453 lines
19 KiB
Python
453 lines
19 KiB
Python
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QCheckBox, QComboBox, QTableWidget, QHeaderView, QMessageBox
|
|
from qfluentwidgets import TitleLabel, BodyLabel, PushButton
|
|
from app.tools.analyzing_ioc import analyzing_ioc
|
|
from app.tools.code_generator import CodeGenerator
|
|
import os
|
|
import csv
|
|
|
|
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)
|
|
layout.addWidget(TitleLabel(f"{self.peripheral_name} 配置"))
|
|
desc = self.descriptions.get(self.peripheral_name.lower(), "")
|
|
if desc:
|
|
layout.addWidget(BodyLabel(f"功能说明:{desc}"))
|
|
self.generate_checkbox = QCheckBox(f"启用 {self.peripheral_name}")
|
|
layout.addWidget(self.generate_checkbox)
|
|
|
|
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}")
|
|
if not CodeGenerator.save_file(template_content, output_path):
|
|
return False
|
|
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)
|
|
layout.addWidget(TitleLabel(f"{self.peripheral_name} 配置"))
|
|
desc = self.descriptions.get(self.peripheral_name.lower(), "")
|
|
if desc:
|
|
layout.addWidget(BodyLabel(f"功能说明:{desc}"))
|
|
self.generate_checkbox = QCheckBox(f"生成 {self.peripheral_name} 代码")
|
|
self.generate_checkbox = QCheckBox(f"生成 {self.peripheral_name} 代码")
|
|
self.generate_checkbox.stateChanged.connect(self._on_generate_changed)
|
|
layout.addWidget(self.generate_checkbox)
|
|
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 = QTableWidget(0, 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 = QLineEdit()
|
|
name_edit.setPlaceholderText(f"输入设备名称")
|
|
self.table.setCellWidget(row, 0, name_edit)
|
|
combo = QComboBox()
|
|
combo.addItems(self.available_list)
|
|
self.table.setCellWidget(row, 1, combo)
|
|
del_btn = PushButton("删除")
|
|
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)
|
|
QMessageBox.information(self, "成功", f"{self.peripheral_name} 代码生成成功!")
|
|
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']}")
|
|
return CodeGenerator.save_file(content, output_path)
|
|
|
|
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}:")
|
|
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']}")
|
|
return CodeGenerator.save_file(content, output_path)
|
|
|
|
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)
|
|
layout.addWidget(TitleLabel("GPIO 配置"))
|
|
# 新增:显示描述
|
|
desc = self.descriptions.get("gpio", "")
|
|
if desc:
|
|
layout.addWidget(BodyLabel(f"功能说明:{desc}"))
|
|
self.generate_checkbox = QCheckBox("生成 GPIO 代码")
|
|
layout.addWidget(self.generate_checkbox)
|
|
if not self.available_list:
|
|
layout.addWidget(BodyLabel("在 .ioc 文件中未找到可用的 GPIO"))
|
|
else:
|
|
self.table = QTableWidget(len(self.available_list), 1)
|
|
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(QTableWidget.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")
|
|
return CodeGenerator.save_file(template_content, output_path)
|
|
|
|
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
|
|
# 生成 IRQ 使能/禁用代码
|
|
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")
|
|
return CodeGenerator.save_file(content, output_path)
|
|
|
|
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)
|
|
|
|
|
|
class bsp(QWidget):
|
|
def __init__(self, project_path):
|
|
super().__init__()
|
|
self.project_path = project_path
|
|
|
|
@staticmethod
|
|
def generate_bsp(project_path, pages):
|
|
total = 0
|
|
success_count = 0
|
|
fail_count = 0
|
|
fail_list = []
|
|
for page in pages:
|
|
name = page.objectName() if hasattr(page, "objectName") else str(page)
|
|
if hasattr(page, "is_need_generate") and 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(name)
|
|
except Exception as e:
|
|
fail_count += 1
|
|
fail_list.append(f"{name} (异常: {e})")
|
|
msg = f"总共尝试生成 {total} 项,成功 {success_count} 项,失败 {fail_count} 项。"
|
|
if fail_list:
|
|
msg += "\n失败项:\n" + "\n".join(fail_list)
|
|
QMessageBox.information(None, "代码生成结果", msg) |