diff --git a/.DS_Store b/.DS_Store index 280a632..b51dd0f 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/MRobot.iss b/MRobot.iss index 70482d5..11efb35 100644 --- a/MRobot.iss +++ b/MRobot.iss @@ -1,6 +1,6 @@ [Setup] AppName=MRobot -AppVersion=1.0.8 +AppVersion=1.1.0 DefaultDirName={userappdata}\MRobot DefaultGroupName=MRobot OutputDir=. diff --git a/app/about_interface.py b/app/about_interface.py index f2eb477..624229c 100644 --- a/app/about_interface.py +++ b/app/about_interface.py @@ -15,7 +15,7 @@ from app.tools.check_update import check_update from app.tools.auto_updater import AutoUpdater, check_update_availability from app.tools.update_check_thread import UpdateCheckThread -__version__ = "1.0.6" +__version__ = "1.1.0" class AboutInterface(QWidget): def __init__(self, parent=None): diff --git a/app/code_generate_interface.py b/app/code_generate_interface.py index 03c3d15..ccc31b3 100644 --- a/app/code_generate_interface.py +++ b/app/code_generate_interface.py @@ -360,17 +360,76 @@ class CodeGenerateInterface(QWidget): continue main_title = row[0] main_item = QTreeWidgetItem([main_title]) - for sub in row[1:]: - sub_item = QTreeWidgetItem([sub]) - main_item.addChild(sub_item) + + # 特殊处理 module + if main_title == 'module': + # 扫描 module 目录 + module_dir = CodeGenerator.get_assets_dir("User_code/module") + if os.path.exists(module_dir): + for item in os.listdir(module_dir): + item_path = os.path.join(module_dir, item) + if not os.path.isdir(item_path): + continue + if item.startswith('.') or item == 'config': + continue + + # 检查是否直接包含代码 + has_direct_code = any( + f.endswith(('.c', '.h')) + for f in os.listdir(item_path) + if os.path.isfile(os.path.join(item_path, f)) + ) + + if has_direct_code: + # 直接的模块(如 cmd) + sub_item = QTreeWidgetItem([item]) + main_item.addChild(sub_item) + else: + # 有子类型的模块(如 gimbal) + module_type_item = QTreeWidgetItem([item]) + has_subtypes = False + for subitem in os.listdir(item_path): + subitem_path = os.path.join(item_path, subitem) + if not os.path.isdir(subitem_path): + continue + if subitem.startswith('.'): + continue + has_code = any( + f.endswith(('.c', '.h')) + for f in os.listdir(subitem_path) + if os.path.isfile(os.path.join(subitem_path, f)) + ) + if has_code: + subtype_item = QTreeWidgetItem([subitem]) + module_type_item.addChild(subtype_item) + has_subtypes = True + + if has_subtypes: + main_item.addChild(module_type_item) + else: + # 其他模块保持原逻辑 + for sub in row[1:]: + sub_item = QTreeWidgetItem([sub]) + main_item.addChild(sub_item) + self.tree.addTopLevelItem(main_item) self.tree.repaint() def on_tree_item_clicked(self, item, column): if item.parent(): - main_title = item.parent().text(0) - sub_title = item.text(0) - class_name = f"{main_title}_{sub_title}".replace("-", "_") + # 判断层级 + if item.parent().parent(): + # 三级树(module/type/instance) + root_title = item.parent().parent().text(0) + type_title = item.parent().text(0) + instance_title = item.text(0) + class_name = f"{root_title}_{type_title}_{instance_title}".replace("-", "_") + else: + # 二级树(category/item) + main_title = item.parent().text(0) + sub_title = item.text(0) + class_name = f"{main_title}_{sub_title}".replace("-", "_") + widget = self._get_or_create_page(class_name) if widget: self.stack.setCurrentWidget(widget) @@ -402,6 +461,18 @@ class CodeGenerateInterface(QWidget): from app.code_page.device_interface import get_device_page device_name = class_name[len('device_'):] # 移除 device_ 前缀 page = get_device_page(device_name, self.project_path) + elif class_name.startswith('module_'): + # Module页面 + from app.code_page.module_interface import get_module_page + # 解析: module_type 或 module_type_instance + parts = class_name[len('module_'):].split('_', 1) + if len(parts) == 2: + module_type = parts[0] + instance = parts[1] + page = get_module_page(module_type, instance, self.project_path, self) + else: + module_type = parts[0] + page = get_module_page(module_type, None, self.project_path, self) else: print(f"未知的页面类型: {class_name}") return None @@ -411,4 +482,6 @@ class CodeGenerateInterface(QWidget): return page except Exception as e: print(f"创建页面 {class_name} 失败: {e}") + import traceback + traceback.print_exc() return None \ No newline at end of file diff --git a/app/code_page/module_interface.py b/app/code_page/module_interface.py index e69de29..3bd26cf 100644 --- a/app/code_page/module_interface.py +++ b/app/code_page/module_interface.py @@ -0,0 +1,261 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout +from qfluentwidgets import ( + BodyLabel, CheckBox, SubtitleLabel, PushButton, FluentIcon, + InfoBar, InfoBarPosition, CardWidget, TitleLabel +) +from PyQt5.QtCore import Qt +from app.tools.code_generator import CodeGenerator +import os +import shutil +import csv + + +def get_module_page(module_type, subtype, project_path, parent=None): + """获取模块配置页面""" + return ModulePage(module_type, subtype, project_path, parent) + + +class ModulePage(QWidget): + """单个模块配置页面""" + + def __init__(self, module_type, subtype, project_path, parent=None): + super().__init__(parent) + self.module_type = module_type + self.subtype = subtype + self.project_path = project_path + + # 获取模块路径 + module_dir = CodeGenerator.get_assets_dir("User_code/module") + if subtype: + self.module_path = os.path.join(module_dir, module_type, subtype) + self.module_key = subtype + else: + self.module_path = os.path.join(module_dir, module_type) + self.module_key = module_type + + # 加载描述 + self.descriptions = self._load_descriptions() + + self._init_ui() + self._check_generated_status() + + def _load_descriptions(self): + """从 describe.csv 加载模块描述""" + descriptions = {} + describe_path = os.path.join( + CodeGenerator.get_assets_dir("User_code/module"), + "describe.csv" + ) + + if os.path.exists(describe_path): + try: + with open(describe_path, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + module_name = row.get('module_name', '').strip() + description = row.get('description', '').strip() + if module_name and description: + descriptions[module_name] = description + except Exception as e: + print(f"读取模块描述失败: {e}") + + return descriptions + + def _init_ui(self): + """初始化界面""" + layout = QVBoxLayout(self) + layout.setSpacing(16) + layout.setContentsMargins(48, 48, 48, 48) + + # 标题 + if self.subtype: + title_text = f"{self.module_type} / {self.subtype}" + else: + title_text = self.module_type + + title = TitleLabel(title_text) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(title) + + # 描述 + desc_text = self.descriptions.get(self.module_key, "模块功能说明") + desc = BodyLabel(desc_text) + desc.setWordWrap(True) + desc.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(desc) + + layout.addSpacing(24) + + # 文件列表卡片 + files_card = CardWidget() + files_layout = QVBoxLayout(files_card) + files_layout.setContentsMargins(16, 16, 16, 16) + + files_title = SubtitleLabel("包含文件") + files_layout.addWidget(files_title) + + files = self._get_module_files() + if files: + for file in files: + file_label = BodyLabel(f"• {file}") + files_layout.addWidget(file_label) + else: + files_layout.addWidget(BodyLabel("未找到文件")) + + layout.addWidget(files_card) + + # 状态显示 + self.status_label = BodyLabel() + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.status_label) + + layout.addSpacing(24) + + # 按钮 + btn_layout = QHBoxLayout() + btn_layout.addStretch() + + self.generate_btn = PushButton(FluentIcon.SAVE, "生成模块代码") + self.generate_btn.clicked.connect(self._generate_code) + btn_layout.addWidget(self.generate_btn) + + btn_layout.addStretch() + layout.addLayout(btn_layout) + + layout.addStretch() + + def _get_module_files(self): + """获取模块文件列表""" + files = [] + try: + if os.path.exists(self.module_path): + for item in os.listdir(self.module_path): + if item.endswith(('.c', '.h')): + files.append(item) + except Exception as e: + print(f"读取模块文件失败: {e}") + + return sorted(files) + + def _check_generated_status(self): + """检查模块是否已生成""" + if self.subtype: + dst_dir = os.path.join(self.project_path, "User/module", self.module_type, self.subtype) + else: + dst_dir = os.path.join(self.project_path, "User/module", self.module_type) + + if os.path.exists(dst_dir): + has_code = any( + f.endswith(('.c', '.h')) + for f in os.listdir(dst_dir) + if os.path.isfile(os.path.join(dst_dir, f)) + ) + if has_code: + self.status_label.setText("✓ 模块已生成") + self.status_label.setStyleSheet("color: green; font-weight: bold;") + self.generate_btn.setEnabled(False) + return + + self.status_label.setText("○ 模块未生成") + self.status_label.setStyleSheet("color: orange;") + + def _generate_code(self): + """生成模块代码""" + try: + # 首先生成 config(如果不存在) + self._generate_config() + + # 目标目录 + if self.subtype: + dst_dir = os.path.join(self.project_path, "User/module", self.module_type, self.subtype) + else: + dst_dir = os.path.join(self.project_path, "User/module", self.module_type) + + # 检查是否已存在 + if os.path.exists(dst_dir): + has_code = any( + f.endswith(('.c', '.h')) + for f in os.listdir(dst_dir) + if os.path.isfile(os.path.join(dst_dir, f)) + ) + if has_code: + InfoBar.warning( + title="已存在", + content="模块代码已存在,不会覆盖", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + return + + # 创建目录 + os.makedirs(dst_dir, exist_ok=True) + + # 复制所有文件 + file_count = 0 + for item in os.listdir(self.module_path): + src_file = os.path.join(self.module_path, item) + dst_file = os.path.join(dst_dir, item) + + if os.path.isfile(src_file): + if os.path.exists(dst_file): + continue + shutil.copy2(src_file, dst_file) + file_count += 1 + print(f"生成文件: {dst_file}") + + if file_count > 0: + InfoBar.success( + title="生成成功", + content=f"已生成 {file_count} 个文件", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + self._check_generated_status() + else: + InfoBar.warning( + title="无需生成", + content="所有文件已存在", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=2000, + parent=self + ) + + except Exception as e: + print(f"生成模块失败: {e}") + InfoBar.error( + title="生成失败", + content=str(e), + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=3000, + parent=self + ) + + def _generate_config(self): + """生成 config 文件(如果不存在)""" + config_dir = os.path.join(self.project_path, "User/module") + config_c = os.path.join(config_dir, "config.c") + config_h = os.path.join(config_dir, "config.h") + + os.makedirs(config_dir, exist_ok=True) + + template_dir = CodeGenerator.get_assets_dir("User_code/module") + template_c = os.path.join(template_dir, "config.c") + template_h = os.path.join(template_dir, "config.h") + + if not os.path.exists(config_c) and os.path.exists(template_c): + shutil.copy2(template_c, config_c) + print(f"生成 config.c") + + if not os.path.exists(config_h) and os.path.exists(template_h): + shutil.copy2(template_h, config_h) + print(f"生成 config.h") diff --git a/assets/User_code/config.csv b/assets/User_code/config.csv index e13d9ac..1eb9b34 100644 --- a/assets/User_code/config.csv +++ b/assets/User_code/config.csv @@ -1,4 +1,4 @@ bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time,flash 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, \ No newline at end of file +module, \ No newline at end of file diff --git a/assets/User_code/module/config.c b/assets/User_code/module/config.c index cb4ed7c..99a8640 100644 --- a/assets/User_code/module/config.c +++ b/assets/User_code/module/config.c @@ -13,10 +13,17 @@ /* Exported variables ------------------------------------------------------- */ -// 机器人参数配置 +/** + * @brief 机器人参数配置 + * @note 在此配置机器人参数 + */ Config_RobotParam_t robot_config = { - - + /* USER CODE BEGIN robot_config */ + .example_param = 0, // 示例参数初始化 + + // 在此添加您的配置参数初始化 + + /* USER CODE END robot_config */ }; /* Private function prototypes ---------------------------------------------- */ diff --git a/assets/User_code/module/config.h b/assets/User_code/module/config.h index 446fb6c..4478d9e 100644 --- a/assets/User_code/module/config.h +++ b/assets/User_code/module/config.h @@ -9,9 +9,20 @@ extern "C" { #endif #include +#include +/** + * @brief 机器人参数配置结构体 + * @note 在此添加您的配置参数 + */ typedef struct { + // 示例配置项(可根据实际需求修改或删除) + uint8_t example_param; // 示例参数 + /* USER CODE BEGIN Config_RobotParam */ + // 在此添加您的配置参数 + + /* USER CODE END Config_RobotParam */ } Config_RobotParam_t; /* Exported functions prototypes -------------------------------------------- */ diff --git a/assets/User_code/module/describe.csv b/assets/User_code/module/describe.csv new file mode 100644 index 0000000..bbd755c --- /dev/null +++ b/assets/User_code/module/describe.csv @@ -0,0 +1,3 @@ +module_name,description +cmd,命令系统,用于机器人指令处理和行为控制 +2_axis_gimbal,双轴云台控制模块,支持pitch和yaw轴控制 diff --git a/assets/User_code/module/2_axis_gimbal.c b/assets/User_code/module/gimbal/2_axis_gimbal/gimbal.c similarity index 100% rename from assets/User_code/module/2_axis_gimbal.c rename to assets/User_code/module/gimbal/2_axis_gimbal/gimbal.c diff --git a/assets/User_code/module/2_axis_gimbal.h b/assets/User_code/module/gimbal/2_axis_gimbal/gimbal.h similarity index 100% rename from assets/User_code/module/2_axis_gimbal.h rename to assets/User_code/module/gimbal/2_axis_gimbal/gimbal.h diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..984f7b9 --- /dev/null +++ b/config/config.json @@ -0,0 +1,6 @@ +{ + "QFluentWidgets": { + "ThemeColor": "#fff18cb9", + "ThemeMode": "Light" + } +} \ No newline at end of file