Compare commits

..

No commits in common. "22ea6e14b32b7a688028c388c99037617e6d8761" and "d214abb5845cf7d572cc6cbe547851a68e6a3727" have entirely different histories.

33 changed files with 140 additions and 2782 deletions

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

BIN
MROBOT.docx Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
[Setup] [Setup]
AppName=MRobot AppName=MRobot
AppVersion=1.1.0 AppVersion=1.0.8
DefaultDirName={userappdata}\MRobot DefaultDirName={userappdata}\MRobot
DefaultGroupName=MRobot DefaultGroupName=MRobot
OutputDir=. OutputDir=.

View File

@ -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.auto_updater import AutoUpdater, check_update_availability
from app.tools.update_check_thread import UpdateCheckThread from app.tools.update_check_thread import UpdateCheckThread
__version__ = "1.1.0" __version__ = "1.0.6"
class AboutInterface(QWidget): class AboutInterface(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):

View File

@ -360,76 +360,17 @@ class CodeGenerateInterface(QWidget):
continue continue
main_title = row[0] main_title = row[0]
main_item = QTreeWidgetItem([main_title]) main_item = QTreeWidgetItem([main_title])
for sub in row[1:]:
# 特殊处理 module sub_item = QTreeWidgetItem([sub])
if main_title == 'module': main_item.addChild(sub_item)
# 扫描 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.addTopLevelItem(main_item)
self.tree.repaint() self.tree.repaint()
def on_tree_item_clicked(self, item, column): def on_tree_item_clicked(self, item, column):
if item.parent(): if item.parent():
# 判断层级 main_title = item.parent().text(0)
if item.parent().parent(): sub_title = item.text(0)
# 三级树module/type/instance class_name = f"{main_title}_{sub_title}".replace("-", "_")
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) widget = self._get_or_create_page(class_name)
if widget: if widget:
self.stack.setCurrentWidget(widget) self.stack.setCurrentWidget(widget)
@ -461,18 +402,6 @@ class CodeGenerateInterface(QWidget):
from app.code_page.device_interface import get_device_page from app.code_page.device_interface import get_device_page
device_name = class_name[len('device_'):] # 移除 device_ 前缀 device_name = class_name[len('device_'):] # 移除 device_ 前缀
page = get_device_page(device_name, self.project_path) 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: else:
print(f"未知的页面类型: {class_name}") print(f"未知的页面类型: {class_name}")
return None return None
@ -482,6 +411,4 @@ class CodeGenerateInterface(QWidget):
return page return page
except Exception as e: except Exception as e:
print(f"创建页面 {class_name} 失败: {e}") print(f"创建页面 {class_name} 失败: {e}")
import traceback
traceback.print_exc()
return None return None

View File

@ -235,15 +235,7 @@ class BspPeripheralBase(QWidget):
return True return True
def _generate_source_file(self, configs, template_dir): def _generate_source_file(self, configs, template_dir):
# 从子文件夹加载模板与_generate_header_file保持一致 template_path = os.path.join(template_dir, self.template_names['source'])
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) template_content = CodeGenerator.load_template(template_path)
if not template_content: if not template_content:
return False return False
@ -406,15 +398,7 @@ class bsp_can(BspPeripheralBase):
) )
def _generate_source_file(self, configs, template_dir): def _generate_source_file(self, configs, template_dir):
# 从子文件夹加载模板与_generate_header_file保持一致 template_path = os.path.join(template_dir, self.template_names['source'])
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) template_content = CodeGenerator.load_template(template_path)
if not template_content: if not template_content:
return False return False
@ -1064,14 +1048,7 @@ class bsp_gpio(QWidget):
return True return True
def _generate_header_file(self, configs, template_dir): def _generate_header_file(self, configs, template_dir):
# 从子文件夹加载模板 template_path = os.path.join(template_dir, "gpio.h")
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, "gpio", "gpio.h")
if not os.path.exists(template_path):
# 向后兼容:从根目录加载
template_path = os.path.join(template_base_dir, "gpio.h")
template_content = CodeGenerator.load_template(template_path) template_content = CodeGenerator.load_template(template_path)
if not template_content: if not template_content:
return False return False
@ -1090,14 +1067,7 @@ class bsp_gpio(QWidget):
return True return True
def _generate_source_file(self, configs, template_dir): def _generate_source_file(self, configs, template_dir):
# 从子文件夹加载模板 template_path = os.path.join(template_dir, "gpio.c")
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, "gpio", "gpio.c")
if not os.path.exists(template_path):
# 向后兼容:从根目录加载
template_path = os.path.join(template_base_dir, "gpio.c")
template_content = CodeGenerator.load_template(template_path) template_content = CodeGenerator.load_template(template_path)
if not template_content: if not template_content:
return False return False
@ -1335,14 +1305,7 @@ class bsp_pwm(QWidget):
return True return True
def _generate_header_file(self, configs, template_dir): def _generate_header_file(self, configs, template_dir):
# 从子文件夹加载模板 template_path = os.path.join(template_dir, "pwm.h")
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, "pwm", "pwm.h")
if not os.path.exists(template_path):
# 向后兼容:从根目录加载
template_path = os.path.join(template_base_dir, "pwm.h")
template_content = CodeGenerator.load_template(template_path) template_content = CodeGenerator.load_template(template_path)
if not template_content: if not template_content:
return False return False
@ -1361,14 +1324,7 @@ class bsp_pwm(QWidget):
return True return True
def _generate_source_file(self, configs, template_dir): def _generate_source_file(self, configs, template_dir):
# 从子文件夹加载模板 template_path = os.path.join(template_dir, "pwm.c")
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, "pwm", "pwm.c")
if not os.path.exists(template_path):
# 向后兼容:从根目录加载
template_path = os.path.join(template_base_dir, "pwm.c")
template_content = CodeGenerator.load_template(template_path) template_content = CodeGenerator.load_template(template_path)
if not template_content: if not template_content:
return False return False
@ -1420,225 +1376,7 @@ class bsp_pwm(QWidget):
if name_widget: if name_widget:
name_widget.setText(saved_config['custom_name']) name_widget.setText(saved_config['custom_name'])
# 更新get_bsp_page函数以包含PWM
class bsp_flash(QWidget):
"""Flash BSP配置界面 - 自动识别MCU型号并生成对应的Flash配置"""
def __init__(self, project_path):
super().__init__()
self.project_path = project_path
self.mcu_name = None
self.flash_config = None
# 加载描述
describe_path = os.path.join(CodeGenerator.get_assets_dir("User_code/bsp"), "describe.csv")
self.descriptions = CodeGenerator.load_descriptions(describe_path)
self._detect_mcu()
self._init_ui()
self._load_config()
def _detect_mcu(self):
"""自动检测MCU型号并获取Flash配置"""
ioc_files = [f for f in os.listdir(self.project_path) if f.endswith('.ioc')]
if ioc_files:
ioc_path = os.path.join(self.project_path, ioc_files[0])
self.mcu_name = analyzing_ioc.get_mcu_name_from_ioc(ioc_path)
if self.mcu_name:
self.flash_config = analyzing_ioc.get_flash_config_from_mcu(self.mcu_name)
def _init_ui(self):
layout = QVBoxLayout(self)
# 顶部布局
top_layout = QHBoxLayout()
top_layout.setAlignment(Qt.AlignVCenter)
self.generate_checkbox = CheckBox("生成 Flash 代码")
self.generate_checkbox.stateChanged.connect(self._on_generate_changed)
top_layout.addWidget(self.generate_checkbox, alignment=Qt.AlignLeft)
top_layout.addStretch()
title = SubtitleLabel("Flash 配置 ")
title.setAlignment(Qt.AlignHCenter)
top_layout.addWidget(title, alignment=Qt.AlignHCenter)
top_layout.addStretch()
layout.addLayout(top_layout)
desc = self.descriptions.get("flash", "自动根据MCU型号配置Flash扇区")
if desc:
desc_label = BodyLabel(desc)
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
# 内容区域
self.content_widget = QWidget()
content_layout = QVBoxLayout(self.content_widget)
if not self.flash_config:
no_config_label = BodyLabel("❌ 无法识别MCU型号或不支持的MCU")
content_layout.addWidget(no_config_label)
else:
# 显示检测到的MCU信息
mcu_info = BodyLabel(f"✅ 检测到MCU: {self.mcu_name}")
content_layout.addWidget(mcu_info)
flash_size = (self.flash_config['end_address'] - 0x08000000) // 1024
flash_type = self.flash_config.get('type', 'sector')
if flash_type == 'page':
# F1系列 - Page模式
page_size = self.flash_config.get('page_size', 1)
flash_info = BodyLabel(f"Flash容量: {flash_size} KB ({len(self.flash_config['sectors'])} 个页,每页 {page_size}KB)")
content_layout.addWidget(flash_info)
type_info = BodyLabel(f"📄 Page模式 (F1系列)")
content_layout.addWidget(type_info)
else:
# F4/H7系列 - Sector模式
flash_info = BodyLabel(f"Flash容量: {flash_size} KB ({len(self.flash_config['sectors'])} 个扇区)")
content_layout.addWidget(flash_info)
if self.flash_config['dual_bank']:
max_sector = len(self.flash_config['sectors']) - 1
bank_info = BodyLabel(f"⚠️ 双Bank Flash (Sector 0-{max_sector})")
else:
max_sector = len(self.flash_config['sectors']) - 1
bank_info = BodyLabel(f"单Bank Flash (Sector 0-{max_sector})")
content_layout.addWidget(bank_info)
layout.addWidget(self.content_widget)
self.content_widget.setEnabled(False)
def _on_generate_changed(self, state):
self.content_widget.setEnabled(state == 2)
def is_need_generate(self):
return self.generate_checkbox.isChecked() and self.flash_config is not None
def _generate_bsp_code_internal(self):
if not self.is_need_generate():
return False
if not self.flash_config:
return False
# 生成头文件
if not self._generate_header_file():
return False
# 生成源文件
if not self._generate_source_file():
return False
self._save_config()
return True
def _generate_header_file(self):
"""生成flash.h"""
periph_folder = "flash"
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, periph_folder, "flash.h")
if not os.path.exists(template_path):
return False
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
# 生成Sector/Page定义
flash_type = self.flash_config.get('type', 'sector')
sector_lines = []
for item in self.flash_config['sectors']:
addr = item['address']
size = item['size']
item_id = item['id']
if flash_type == 'page':
# F1系列 - Page模式
sector_lines.append(
f"#define ADDR_FLASH_PAGE_{item_id} ((uint32_t)0x{addr:08X})"
)
sector_lines.append(
f"/* Base address of Page {item_id}, {size} Kbytes */"
)
else:
# F4/H7系列 - Sector模式
sector_lines.append(
f"#define ADDR_FLASH_SECTOR_{item_id} ((uint32_t)0x{addr:08X})"
)
sector_lines.append(
f"/* Base address of Sector {item_id}, {size} Kbytes */"
)
content = CodeGenerator.replace_auto_generated(
template_content, "AUTO GENERATED FLASH_SECTORS", "\n".join(sector_lines)
)
# 生成结束地址
end_addr = self.flash_config['end_address']
end_line = f"#define ADDR_FLASH_END ((uint32_t)0x{end_addr:08X}) /* End address for flash */"
content = CodeGenerator.replace_auto_generated(
content, "AUTO GENERATED FLASH_END_ADDRESS", end_line
)
output_path = os.path.join(self.project_path, "User/bsp/flash.h")
CodeGenerator.save_with_preserve(output_path, content)
return True
def _generate_source_file(self):
"""生成flash.c"""
periph_folder = "flash"
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, periph_folder, "flash.c")
if not os.path.exists(template_path):
return False
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
# 生成最大Sector数定义
max_sector = len(self.flash_config['sectors']) - 1
max_sector_line = f"#define BSP_FLASH_MAX_SECTOR {max_sector}"
content = CodeGenerator.replace_auto_generated(
template_content, "AUTO GENERATED FLASH_MAX_SECTOR", max_sector_line
)
# 生成擦除检查代码
erase_check = f" if (sector > 0 && sector <= {max_sector}) {{"
content = CodeGenerator.replace_auto_generated(
content, "AUTO GENERATED FLASH_ERASE_CHECK", erase_check
)
output_path = os.path.join(self.project_path, "User/bsp/flash.c")
CodeGenerator.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['flash'] = {
'enabled': True,
'mcu_name': self.mcu_name,
'dual_bank': self.flash_config['dual_bank'],
'sectors': len(self.flash_config['sectors'])
}
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('flash', {})
if conf.get('enabled', False):
self.generate_checkbox.setChecked(True)
# 更新get_bsp_page函数以包含PWM和Flash
def get_bsp_page(peripheral_name, project_path): def get_bsp_page(peripheral_name, project_path):
"""根据外设名返回对应的页面类没有特殊类则返回默认BspSimplePeripheral""" """根据外设名返回对应的页面类没有特殊类则返回默认BspSimplePeripheral"""
name_lower = peripheral_name.lower() name_lower = peripheral_name.lower()
@ -1649,8 +1387,7 @@ def get_bsp_page(peripheral_name, project_path):
"spi": bsp_spi, "spi": bsp_spi,
"uart": bsp_uart, "uart": bsp_uart,
"gpio": bsp_gpio, "gpio": bsp_gpio,
"pwm": bsp_pwm, "pwm": bsp_pwm, # 添加PWM
"flash": bsp_flash, # 添加Flash自动配置
# 以后可以继续添加特殊外设 # 以后可以继续添加特殊外设
} }
if name_lower in special_classes: if name_lower in special_classes:

View File

@ -243,13 +243,8 @@ class DeviceSimple(QWidget):
# 使用设备名称作为子文件夹名(小写) # 使用设备名称作为子文件夹名(小写)
device_folder = self.device_name.lower() device_folder = self.device_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/device") template_base_dir = CodeGenerator.get_assets_dir("User_code/device")
device_template_dir = os.path.join(template_base_dir, device_folder)
files = self.device_config.get('files', {}) files = self.device_config.get('files', {})
# 收集需要替换BSP配置的文件列表
files_to_process = list(files.values())
# 处理配置中定义的主要文件需要BSP替换
for file_type, filename in files.items(): for file_type, filename in files.items():
# 先尝试从子文件夹加载 # 先尝试从子文件夹加载
src_path = os.path.join(template_base_dir, device_folder, filename) src_path = os.path.join(template_base_dir, device_folder, filename)
@ -278,25 +273,6 @@ class DeviceSimple(QWidget):
with open(dst_path, 'w', encoding='utf-8') as f: with open(dst_path, 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
# 复制设备文件夹下的其他文件(如 lcd_lib.h
if os.path.exists(device_template_dir):
import shutil
for item in os.listdir(device_template_dir):
# 跳过已处理的文件
if item in files_to_process:
continue
src_file = os.path.join(device_template_dir, item)
dst_file = os.path.join(self.project_path, f"User/device/{item}")
# 只复制文件,不复制子目录
if os.path.isfile(src_file):
# 检查文件是否已存在,避免覆盖
if not os.path.exists(dst_file):
os.makedirs(os.path.dirname(dst_file), exist_ok=True)
shutil.copy2(src_file, dst_file)
print(f"复制额外文件: {dst_file}")
self._save_config() self._save_config()
return True return True

View File

@ -1,255 +0,0 @@
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 = module_type # 使用类型名作为目标文件夹
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):
"""检查模块是否已生成"""
dst_dir = os.path.join(self.project_path, "User/module", self.module_key)
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()
# 目标目录:展平到 User/module/{module_key}
dst_dir = os.path.join(self.project_path, "User/module", self.module_key)
# 检查是否已存在
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")

View File

@ -1,21 +1,25 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon
import pyqtgraph as pg from openpyxl import load_workbook, Workbook
# 延迟导入:这些库只在需要时才导入,加快应用启动速度 import numpy as np
# import numpy as np from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
# from openpyxl import load_workbook, Workbook from matplotlib.figure import Figure
from PyQt5.QtWebEngineWidgets import QWebEngineView
import plotly.graph_objs as go
import plotly.io as pio
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Source Han Sans', 'STHeiti', 'Heiti TC']
matplotlib.rcParams['axes.unicode_minus'] = False
class FunctionFitInterface(QWidget): class FunctionFitInterface(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
# 延迟导入标志
self._libs_loaded = False
self.setObjectName("functionFitInterface") self.setObjectName("functionFitInterface")
main_layout = QHBoxLayout(self) main_layout = QHBoxLayout(self)
main_layout.setSpacing(24) main_layout.setSpacing(24)
@ -63,11 +67,9 @@ class FunctionFitInterface(QWidget):
right_layout.setSpacing(12) right_layout.setSpacing(12)
right_layout.addWidget(SubtitleLabel("函数图像预览")) right_layout.addWidget(SubtitleLabel("函数图像预览"))
# 占位符实际的canvas会在_load_heavy_libraries中创建 self.figure = Figure(figsize=(5, 4))
self.canvas_placeholder = QWidget() self.canvas = FigureCanvas(self.figure)
self.canvas_layout = QVBoxLayout(self.canvas_placeholder) right_layout.addWidget(self.canvas, stretch=1)
self.canvas_layout.setContentsMargins(0, 0, 0, 0)
right_layout.addWidget(self.canvas_placeholder, stretch=1)
self.resultLabel = BodyLabel("") self.resultLabel = BodyLabel("")
self.resultLabel.setWordWrap(True) # 自动换行 self.resultLabel.setWordWrap(True) # 自动换行
@ -108,29 +110,12 @@ class FunctionFitInterface(QWidget):
main_layout.addLayout(right_layout, 2) main_layout.addLayout(right_layout, 2)
def _load_heavy_libraries(self): # 默认显示空图像
"""延迟加载大型库,提高应用启动速度""" self.figure.clear()
if self._libs_loaded: ax = self.figure.add_subplot(111)
return ax.set_xlabel('x')
ax.set_ylabel('y')
global np, load_workbook, Workbook self.canvas.draw()
import numpy as np
from openpyxl import load_workbook, Workbook
# 创建 PyQtGraph 画布
self.plot_widget = pg.PlotWidget()
self.plot_widget.setBackground('w') # 白色背景
self.plot_widget.showGrid(x=True, y=True, alpha=0.3)
self.plot_widget.setLabel('left', 'y')
self.plot_widget.setLabel('bottom', 'x')
self.plot_widget.setTitle('graph of a function')
# 将 plot_widget 添加到占位符布局中
if hasattr(self, 'canvas_layout'):
self.canvas_layout.addWidget(self.plot_widget)
self._libs_loaded = True
def add_row(self): def add_row(self):
row = self.dataTable.rowCount() row = self.dataTable.rowCount()
@ -147,7 +132,6 @@ class FunctionFitInterface(QWidget):
self.dataTable.removeRow(row) self.dataTable.removeRow(row)
def import_excel(self): def import_excel(self):
self._load_heavy_libraries() # 延迟加载库
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx)") path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx)")
if path: if path:
wb = load_workbook(path) wb = load_workbook(path)
@ -162,7 +146,6 @@ class FunctionFitInterface(QWidget):
def export_excel(self): def export_excel(self):
self._load_heavy_libraries() # 延迟加载库
path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)") path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)")
if path: if path:
data = self.parse_data() data = self.parse_data()
@ -191,7 +174,6 @@ class FunctionFitInterface(QWidget):
return data if data else None return data if data else None
def fit_and_plot(self): def fit_and_plot(self):
self._load_heavy_libraries() # 延迟加载库
data = self.parse_data() data = self.parse_data()
if not data: if not data:
self.resultLabel.setText("数据格式错误或为空") self.resultLabel.setText("数据格式错误或为空")
@ -207,29 +189,15 @@ class FunctionFitInterface(QWidget):
x_fit = np.linspace(x.min(), x.max(), 100) x_fit = np.linspace(x.min(), x.max(), 100)
y_fit = np.polyval(coeffs, x_fit) y_fit = np.polyval(coeffs, x_fit)
# 清空并重新绘图 self.figure.clear()
self.plot_widget.clear() ax = self.figure.add_subplot(111)
ax.scatter(x, y, color='blue', label='raw data')
# 绘制原始数据点(蓝色散点) ax.plot(x_fit, y_fit, color='red', label=f'Fitted curve')
scatter = pg.ScatterPlotItem( ax.set_title('graph of a function')
x=x, y=y, ax.set_xlabel('x')
pen=None, ax.set_ylabel('y')
brush=pg.mkBrush(0, 0, 255, 120), # 蓝色半透明 ax.legend()
size=10, self.canvas.draw()
name='raw data'
)
self.plot_widget.addItem(scatter)
# 绘制拟合曲线(红色线条)
pen = pg.mkPen(color=(255, 0, 0), width=2) # 红色线条
curve = self.plot_widget.plot(
x_fit, y_fit,
pen=pen,
name='Fitted curve'
)
# 添加图例
self.plot_widget.addLegend()
formula = self.poly_formula(coeffs) formula = self.poly_formula(coeffs)
self.resultLabel.setText(f"拟合公式: {formula}") self.resultLabel.setText(f"拟合公式: {formula}")

View File

@ -15,7 +15,6 @@ from .data_interface import DataInterface
from .mini_tool_interface import MiniToolInterface from .mini_tool_interface import MiniToolInterface
from .code_configuration_interface import CodeConfigurationInterface from .code_configuration_interface import CodeConfigurationInterface
from .finance_interface import FinanceInterface from .finance_interface import FinanceInterface
from .mech_design_interface import MechDesignInterface
from .about_interface import AboutInterface from .about_interface import AboutInterface
import base64 import base64
@ -55,7 +54,6 @@ class MainWindow(FluentWindow):
self.miniToolInterface = MiniToolInterface(self) self.miniToolInterface = MiniToolInterface(self)
self.codeConfigurationInterface = CodeConfigurationInterface(self) self.codeConfigurationInterface = CodeConfigurationInterface(self)
self.financeInterface = FinanceInterface(self) self.financeInterface = FinanceInterface(self)
self.mechDesignInterface = MechDesignInterface(self)
def initNavigation(self): def initNavigation(self):
@ -64,8 +62,7 @@ class MainWindow(FluentWindow):
self.addSubInterface(self.codeConfigurationInterface, FIF.CODE, self.tr('代码生成')) self.addSubInterface(self.codeConfigurationInterface, FIF.CODE, self.tr('代码生成'))
self.addSubInterface(self.serialTerminalInterface, FIF.COMMAND_PROMPT,self.tr('串口助手')) self.addSubInterface(self.serialTerminalInterface, FIF.COMMAND_PROMPT,self.tr('串口助手'))
self.addSubInterface(self.partLibraryInterface, FIF.DOWNLOAD, self.tr('零件库')) self.addSubInterface(self.partLibraryInterface, FIF.DOWNLOAD, self.tr('零件库'))
self.addSubInterface(self.mechDesignInterface, FIF.SETTING, self.tr('机械设计')) self.addSubInterface(self.financeInterface, FIF.DOCUMENT, self.tr('财务做账'))
# self.addSubInterface(self.financeInterface, FIF.DOCUMENT, self.tr('财务做账'))
self.addSubInterface(self.miniToolInterface, FIF.LIBRARY, self.tr('迷你工具箱')) self.addSubInterface(self.miniToolInterface, FIF.LIBRARY, self.tr('迷你工具箱'))
self.addSubInterface(AboutInterface(self), FIF.INFO, self.tr('关于'), position=NavigationItemPosition.BOTTOM) self.addSubInterface(AboutInterface(self), FIF.INFO, self.tr('关于'), position=NavigationItemPosition.BOTTOM)

View File

@ -1,564 +0,0 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QStackedWidget, QSizePolicy
from PyQt5.QtCore import Qt
from qfluentwidgets import (TitleLabel, SubtitleLabel, BodyLabel, LineEdit, PushButton,
ComboBox, CardWidget, FluentIcon, InfoBar, DoubleSpinBox,
PushSettingCard, TabBar)
import math
class GearCalculator(QWidget):
"""齿轮参数计算器"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("GearCalculator")
self._init_ui()
def _init_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(30, 30, 30, 30)
layout.setSpacing(15)
layout.setAlignment(Qt.AlignTop)
# 标题
title = SubtitleLabel("齿轮参数计算")
layout.addWidget(title)
# 说明
desc = BodyLabel("输入任意已知参数,点击计算按钮自动计算其他参数")
desc.setWordWrap(True)
layout.addWidget(desc)
# 参数输入区域
grid_layout = QGridLayout()
grid_layout.setSpacing(10)
# 齿轮1参数
grid_layout.addWidget(BodyLabel("齿轮1 模数 (m):"), 0, 0)
self.module1_spin = DoubleSpinBox()
self.module1_spin.setRange(0, 100)
self.module1_spin.setSingleStep(0.5)
self.module1_spin.setDecimals(2)
self.module1_spin.setSuffix(" mm")
grid_layout.addWidget(self.module1_spin, 0, 1)
grid_layout.addWidget(BodyLabel("齿轮1 齿数 (Z1):"), 1, 0)
self.teeth1_spin = DoubleSpinBox()
self.teeth1_spin.setRange(0, 500)
self.teeth1_spin.setSingleStep(1)
self.teeth1_spin.setDecimals(0)
grid_layout.addWidget(self.teeth1_spin, 1, 1)
grid_layout.addWidget(BodyLabel("齿轮1 分度圆直径 (d1):"), 2, 0)
self.diameter1_spin = DoubleSpinBox()
self.diameter1_spin.setRange(0, 1000)
self.diameter1_spin.setSingleStep(1)
self.diameter1_spin.setDecimals(2)
self.diameter1_spin.setSuffix(" mm")
grid_layout.addWidget(self.diameter1_spin, 2, 1)
# 齿轮2参数
grid_layout.addWidget(BodyLabel("齿轮2 齿数 (Z2):"), 0, 2)
self.teeth2_spin = DoubleSpinBox()
self.teeth2_spin.setRange(0, 500)
self.teeth2_spin.setSingleStep(1)
self.teeth2_spin.setDecimals(0)
grid_layout.addWidget(self.teeth2_spin, 0, 3)
grid_layout.addWidget(BodyLabel("齿轮2 分度圆直径 (d2):"), 1, 2)
self.diameter2_spin = DoubleSpinBox()
self.diameter2_spin.setRange(0, 1000)
self.diameter2_spin.setSingleStep(1)
self.diameter2_spin.setDecimals(2)
self.diameter2_spin.setSuffix(" mm")
grid_layout.addWidget(self.diameter2_spin, 1, 3)
# 中心距
grid_layout.addWidget(BodyLabel("中心距 (a):"), 2, 2)
self.center_distance_spin = DoubleSpinBox()
self.center_distance_spin.setRange(0, 2000)
self.center_distance_spin.setSingleStep(1)
self.center_distance_spin.setDecimals(2)
self.center_distance_spin.setSuffix(" mm")
grid_layout.addWidget(self.center_distance_spin, 2, 3)
# 传动比
grid_layout.addWidget(BodyLabel("传动比 (i):"), 3, 0)
self.ratio_spin = DoubleSpinBox()
self.ratio_spin.setRange(0, 100)
self.ratio_spin.setSingleStep(0.1)
self.ratio_spin.setDecimals(3)
grid_layout.addWidget(self.ratio_spin, 3, 1)
layout.addLayout(grid_layout)
# 按钮区域
btn_layout = QHBoxLayout()
self.calc_btn = PushButton(FluentIcon.PLAY, "计算")
self.calc_btn.clicked.connect(self._calculate)
self.clear_btn = PushButton(FluentIcon.DELETE, "清空")
self.clear_btn.clicked.connect(self._clear)
btn_layout.addWidget(self.calc_btn)
btn_layout.addWidget(self.clear_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
# 结果显示
self.result_label = BodyLabel("")
self.result_label.setWordWrap(True)
layout.addWidget(self.result_label)
def _calculate(self):
"""计算齿轮参数"""
try:
# 获取输入值
m = self.module1_spin.value() if self.module1_spin.value() > 0 else None
z1 = self.teeth1_spin.value() if self.teeth1_spin.value() > 0 else None
d1 = self.diameter1_spin.value() if self.diameter1_spin.value() > 0 else None
z2 = self.teeth2_spin.value() if self.teeth2_spin.value() > 0 else None
d2 = self.diameter2_spin.value() if self.diameter2_spin.value() > 0 else None
a = self.center_distance_spin.value() if self.center_distance_spin.value() > 0 else None
i = self.ratio_spin.value() if self.ratio_spin.value() > 0 else None
# 齿轮基本公式d = m * z, a = (d1 + d2) / 2, i = z2 / z1
# 尝试计算模数
if m is None:
if d1 and z1:
m = d1 / z1
self.module1_spin.setValue(m)
elif d2 and z2:
m = d2 / z2
self.module1_spin.setValue(m)
# 尝试计算齿轮1参数
if z1 is None and d1 and m:
z1 = round(d1 / m)
self.teeth1_spin.setValue(z1)
elif d1 is None and z1 and m:
d1 = m * z1
self.diameter1_spin.setValue(d1)
# 尝试计算齿轮2参数
if z2 is None and d2 and m:
z2 = round(d2 / m)
self.teeth2_spin.setValue(z2)
elif d2 is None and z2 and m:
d2 = m * z2
self.diameter2_spin.setValue(d2)
# 尝试计算传动比
if i is None and z1 and z2:
i = z2 / z1
self.ratio_spin.setValue(i)
# 尝试根据传动比计算齿数
if i and z1 and z2 is None:
z2 = round(z1 * i)
self.teeth2_spin.setValue(z2)
if m:
d2 = m * z2
self.diameter2_spin.setValue(d2)
# 尝试计算中心距
if a is None and d1 and d2:
a = (d1 + d2) / 2
self.center_distance_spin.setValue(a)
# 尝试根据中心距计算参数
if a and d1 and d2 is None:
d2 = 2 * a - d1
self.diameter2_spin.setValue(d2)
if m:
z2 = round(d2 / m)
self.teeth2_spin.setValue(z2)
self.result_label.setText("✓ 计算完成!请检查结果是否符合预期。")
except Exception as e:
InfoBar.error(
title="计算错误",
content=f"计算失败: {str(e)}",
parent=self,
duration=3000
)
def _clear(self):
"""清空所有输入"""
self.module1_spin.setValue(0)
self.teeth1_spin.setValue(0)
self.diameter1_spin.setValue(0)
self.teeth2_spin.setValue(0)
self.diameter2_spin.setValue(0)
self.center_distance_spin.setValue(0)
self.ratio_spin.setValue(0)
self.result_label.setText("")
class TimingBeltCalculator(QWidget):
"""同步带轮参数计算器"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("TimingBeltCalculator")
# 常见同步带型号的节距pitch数据 (mm)
self.belt_pitches = {
"MXL": 2.032,
"XL": 5.08,
"L": 9.525,
"H": 12.7,
"XH": 22.225,
"XXH": 31.75,
"T2.5": 2.5,
"T5": 5.0,
"T10": 10.0,
"T20": 20.0,
"AT5": 5.0,
"AT10": 10.0,
"AT20": 20.0,
"HTD 3M": 3.0,
"HTD 5M": 5.0,
"HTD 8M": 8.0,
"HTD 14M": 14.0,
"HTD 20M": 20.0,
"GT2 2M": 2.0,
"GT2 3M": 3.0,
"GT2 5M": 5.0,
}
self._init_ui()
def _init_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(30, 30, 30, 30)
layout.setSpacing(15)
layout.setAlignment(Qt.AlignTop)
# 标题
title = SubtitleLabel("同步带轮参数计算")
layout.addWidget(title)
# 说明
desc = BodyLabel("选择同步带型号,输入已知参数进行计算")
desc.setWordWrap(True)
layout.addWidget(desc)
# 参数输入区域
grid_layout = QGridLayout()
grid_layout.setSpacing(10)
# 同步带型号
grid_layout.addWidget(BodyLabel("同步带型号:"), 0, 0)
self.belt_type_combo = ComboBox()
self.belt_type_combo.addItems(list(self.belt_pitches.keys()))
self.belt_type_combo.setCurrentText("GT2 2M")
grid_layout.addWidget(self.belt_type_combo, 0, 1)
grid_layout.addWidget(BodyLabel("节距 (pitch):"), 0, 2)
self.pitch_spin = DoubleSpinBox()
self.pitch_spin.setRange(0, 100)
self.pitch_spin.setSingleStep(0.1)
self.pitch_spin.setDecimals(3)
self.pitch_spin.setSuffix(" mm")
self.pitch_spin.setValue(2.0)
self.pitch_spin.setEnabled(False)
grid_layout.addWidget(self.pitch_spin, 0, 3)
# 带轮1参数
grid_layout.addWidget(BodyLabel("带轮1 齿数 (Z1):"), 1, 0)
self.pulley1_teeth_spin = DoubleSpinBox()
self.pulley1_teeth_spin.setRange(0, 500)
self.pulley1_teeth_spin.setSingleStep(1)
self.pulley1_teeth_spin.setDecimals(0)
grid_layout.addWidget(self.pulley1_teeth_spin, 1, 1)
grid_layout.addWidget(BodyLabel("带轮1 节圆直径 (PD1):"), 2, 0)
self.pulley1_pd_spin = DoubleSpinBox()
self.pulley1_pd_spin.setRange(0, 1000)
self.pulley1_pd_spin.setSingleStep(1)
self.pulley1_pd_spin.setDecimals(2)
self.pulley1_pd_spin.setSuffix(" mm")
grid_layout.addWidget(self.pulley1_pd_spin, 2, 1)
# 带轮2参数
grid_layout.addWidget(BodyLabel("带轮2 齿数 (Z2):"), 1, 2)
self.pulley2_teeth_spin = DoubleSpinBox()
self.pulley2_teeth_spin.setRange(0, 500)
self.pulley2_teeth_spin.setSingleStep(1)
self.pulley2_teeth_spin.setDecimals(0)
grid_layout.addWidget(self.pulley2_teeth_spin, 1, 3)
grid_layout.addWidget(BodyLabel("带轮2 节圆直径 (PD2):"), 2, 2)
self.pulley2_pd_spin = DoubleSpinBox()
self.pulley2_pd_spin.setRange(0, 1000)
self.pulley2_pd_spin.setSingleStep(1)
self.pulley2_pd_spin.setDecimals(2)
self.pulley2_pd_spin.setSuffix(" mm")
grid_layout.addWidget(self.pulley2_pd_spin, 2, 3)
# 中心距和带长
grid_layout.addWidget(BodyLabel("中心距 (C):"), 3, 0)
self.center_distance_spin = DoubleSpinBox()
self.center_distance_spin.setRange(0, 5000)
self.center_distance_spin.setSingleStep(1)
self.center_distance_spin.setDecimals(2)
self.center_distance_spin.setSuffix(" mm")
grid_layout.addWidget(self.center_distance_spin, 3, 1)
grid_layout.addWidget(BodyLabel("带长 (L):"), 3, 2)
self.belt_length_spin = DoubleSpinBox()
self.belt_length_spin.setRange(0, 10000)
self.belt_length_spin.setSingleStep(1)
self.belt_length_spin.setDecimals(2)
self.belt_length_spin.setSuffix(" mm")
grid_layout.addWidget(self.belt_length_spin, 3, 3)
# 带齿数和传动比
grid_layout.addWidget(BodyLabel("带齿数 (Tb):"), 4, 0)
self.belt_teeth_spin = DoubleSpinBox()
self.belt_teeth_spin.setRange(0, 5000)
self.belt_teeth_spin.setSingleStep(1)
self.belt_teeth_spin.setDecimals(0)
grid_layout.addWidget(self.belt_teeth_spin, 4, 1)
grid_layout.addWidget(BodyLabel("传动比 (i):"), 4, 2)
self.ratio_spin = DoubleSpinBox()
self.ratio_spin.setRange(0, 100)
self.ratio_spin.setSingleStep(0.1)
self.ratio_spin.setDecimals(3)
grid_layout.addWidget(self.ratio_spin, 4, 3)
layout.addLayout(grid_layout)
# 按钮区域
btn_layout = QHBoxLayout()
self.calc_btn = PushButton(FluentIcon.PLAY, "计算")
self.calc_btn.clicked.connect(self._calculate)
self.clear_btn = PushButton(FluentIcon.DELETE, "清空")
self.clear_btn.clicked.connect(self._clear)
btn_layout.addWidget(self.calc_btn)
btn_layout.addWidget(self.clear_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
# 结果显示
self.result_label = BodyLabel("")
self.result_label.setWordWrap(True)
layout.addWidget(self.result_label)
# 连接信号
self.belt_type_combo.currentTextChanged.connect(self._on_belt_type_changed)
def _on_belt_type_changed(self, text):
"""同步带型号改变时更新节距"""
pitch = self.belt_pitches.get(text, 2.0)
self.pitch_spin.setValue(pitch)
def _calculate(self):
"""计算同步带参数"""
try:
# 获取输入值
pitch = self.pitch_spin.value()
z1 = self.pulley1_teeth_spin.value() if self.pulley1_teeth_spin.value() > 0 else None
pd1 = self.pulley1_pd_spin.value() if self.pulley1_pd_spin.value() > 0 else None
z2 = self.pulley2_teeth_spin.value() if self.pulley2_teeth_spin.value() > 0 else None
pd2 = self.pulley2_pd_spin.value() if self.pulley2_pd_spin.value() > 0 else None
c = self.center_distance_spin.value() if self.center_distance_spin.value() > 0 else None
l = self.belt_length_spin.value() if self.belt_length_spin.value() > 0 else None
tb = self.belt_teeth_spin.value() if self.belt_teeth_spin.value() > 0 else None
i = self.ratio_spin.value() if self.ratio_spin.value() > 0 else None
# 同步带公式:
# PD = (Z * pitch) / π
# L = 2C + π(PD1 + PD2)/2 + (PD2 - PD1)²/(4C)
# Tb = L / pitch
# 计算节圆直径
if pd1 is None and z1:
pd1 = (z1 * pitch) / math.pi
self.pulley1_pd_spin.setValue(pd1)
elif z1 is None and pd1:
z1 = round((pd1 * math.pi) / pitch)
self.pulley1_teeth_spin.setValue(z1)
if pd2 is None and z2:
pd2 = (z2 * pitch) / math.pi
self.pulley2_pd_spin.setValue(pd2)
elif z2 is None and pd2:
z2 = round((pd2 * math.pi) / pitch)
self.pulley2_teeth_spin.setValue(z2)
# 计算传动比
if i is None and z1 and z2:
i = z2 / z1
self.ratio_spin.setValue(i)
elif i and z1 and z2 is None:
z2 = round(z1 * i)
self.pulley2_teeth_spin.setValue(z2)
pd2 = (z2 * pitch) / math.pi
self.pulley2_pd_spin.setValue(pd2)
# 计算带长
if l is None and c and pd1 and pd2:
l = 2 * c + math.pi * (pd1 + pd2) / 2 + (pd2 - pd1) ** 2 / (4 * c)
self.belt_length_spin.setValue(l)
# 计算带齿数
if tb is None and l:
tb = round(l / pitch)
self.belt_teeth_spin.setValue(tb)
elif l is None and tb:
l = tb * pitch
self.belt_length_spin.setValue(l)
# 根据带长和节圆直径计算中心距(近似)
if c is None and l and pd1 and pd2:
# 使用近似公式求解
b = math.pi * (pd1 + pd2) / 2
a_term = (pd2 - pd1) ** 2 / 4
# 2C + b + a_term/C = L
# 求解二次方程: 2C² + bC + a_term - LC = 0
# 2C² + (b-L)C + a_term = 0
A = 2
B = b - l
C_term = a_term
discriminant = B**2 - 4*A*C_term
if discriminant >= 0:
c = (-B + math.sqrt(discriminant)) / (2*A)
self.center_distance_spin.setValue(c)
self.result_label.setText("✓ 计算完成!请检查结果是否符合预期。\n提示:带长计算为理论值,实际选型请参考标准带长。")
except Exception as e:
InfoBar.error(
title="计算错误",
content=f"计算失败: {str(e)}",
parent=self,
duration=3000
)
def _clear(self):
"""清空所有输入"""
self.pulley1_teeth_spin.setValue(0)
self.pulley1_pd_spin.setValue(0)
self.pulley2_teeth_spin.setValue(0)
self.pulley2_pd_spin.setValue(0)
self.center_distance_spin.setValue(0)
self.belt_length_spin.setValue(0)
self.belt_teeth_spin.setValue(0)
self.ratio_spin.setValue(0)
self.result_label.setText("")
class MechDesignInterface(QWidget):
"""机械设计计算界面"""
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("MechDesignInterface")
self._init_ui()
def _init_ui(self):
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignTop)
layout.setContentsMargins(10, 0, 10, 10)
# 顶部标签栏
self.tabBar = TabBar(self)
self.tabBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
layout.addWidget(self.tabBar)
self.stackedWidget = QStackedWidget(self)
layout.addWidget(self.stackedWidget)
# 主页面
self.mainPage = QWidget(self)
mainLayout = QVBoxLayout(self.mainPage)
mainLayout.setAlignment(Qt.AlignTop)
mainLayout.setContentsMargins(20, 20, 20, 20)
mainLayout.setSpacing(15)
# 页面标题
title = SubtitleLabel("机械设计计算工具")
mainLayout.addWidget(title)
# 页面说明
desc = BodyLabel("选择需要的计算工具")
desc.setWordWrap(True)
mainLayout.addWidget(desc)
# 齿轮计算器卡片
self.gearCard = PushSettingCard(
text="▶ 启动",
icon=FluentIcon.UNIT,
title="齿轮参数计算",
content="计算齿轮模数、齿数、分度圆直径、中心距、传动比等参数"
)
self.gearCard.clicked.connect(self.open_gear_tab)
mainLayout.addWidget(self.gearCard)
# 同步带计算器卡片
self.beltCard = PushSettingCard(
text="▶ 启动",
icon=FluentIcon.SYNC,
title="同步带轮参数计算",
content="计算同步带轮齿数、节圆直径、中心距、带长等参数"
)
self.beltCard.clicked.connect(self.open_belt_tab)
mainLayout.addWidget(self.beltCard)
mainLayout.addStretch()
# 添加主页面到堆叠窗口
self.addSubInterface(self.mainPage, "mainPage", "机械设计")
# 信号连接
self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
self.tabBar.tabCloseRequested.connect(self.onCloseTab)
def addSubInterface(self, widget: QWidget, objectName: str, text: str):
widget.setObjectName(objectName)
self.stackedWidget.addWidget(widget)
self.tabBar.addTab(
routeKey=objectName,
text=text,
onClick=lambda: self.stackedWidget.setCurrentWidget(widget)
)
def onCurrentIndexChanged(self, index):
widget = self.stackedWidget.widget(index)
self.tabBar.setCurrentTab(widget.objectName())
def onCloseTab(self, index: int):
item = self.tabBar.tabItem(index)
widget = self.findChild(QWidget, item.routeKey())
# 禁止关闭主页
if widget.objectName() == "mainPage":
return
self.stackedWidget.removeWidget(widget)
self.tabBar.removeTab(index)
widget.deleteLater()
def open_gear_tab(self):
# 检查是否已存在标签页,避免重复添加
for i in range(self.stackedWidget.count()):
widget = self.stackedWidget.widget(i)
if widget.objectName() == "gearPage":
self.stackedWidget.setCurrentWidget(widget)
self.tabBar.setCurrentTab("gearPage")
return
gear_page = GearCalculator(self)
self.addSubInterface(gear_page, "gearPage", "齿轮计算")
self.stackedWidget.setCurrentWidget(gear_page)
self.tabBar.setCurrentTab("gearPage")
def open_belt_tab(self):
# 检查是否已存在标签页,避免重复添加
for i in range(self.stackedWidget.count()):
widget = self.stackedWidget.widget(i)
if widget.objectName() == "beltPage":
self.stackedWidget.setCurrentWidget(widget)
self.tabBar.setCurrentTab("beltPage")
return
belt_page = TimingBeltCalculator(self)
self.addSubInterface(belt_page, "beltPage", "同步带计算")
self.stackedWidget.setCurrentWidget(belt_page)
self.tabBar.setCurrentTab("beltPage")

View File

@ -2,18 +2,13 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QStackedWidget, QSizePolicy
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from qfluentwidgets import PushSettingCard, FluentIcon, TabBar from qfluentwidgets import PushSettingCard, FluentIcon, TabBar
# 延迟导入:避免在启动时加载大型库 from .function_fit_interface import FunctionFitInterface
# from .function_fit_interface import FunctionFitInterface
from .ai_interface import AIInterface from .ai_interface import AIInterface
class MiniToolInterface(QWidget): class MiniToolInterface(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setObjectName("minitoolInterface") self.setObjectName("minitoolInterface")
# 延迟加载的接口引用
self.functionFitInterface = None
self.vBoxLayout = QVBoxLayout(self) self.vBoxLayout = QVBoxLayout(self)
self.vBoxLayout.setAlignment(Qt.AlignTop) self.vBoxLayout.setAlignment(Qt.AlignTop)
self.vBoxLayout.setContentsMargins(10, 0, 10, 10) # 设置外边距 self.vBoxLayout.setContentsMargins(10, 0, 10, 10) # 设置外边距
@ -93,15 +88,9 @@ class MiniToolInterface(QWidget):
self.stackedWidget.setCurrentWidget(widget) self.stackedWidget.setCurrentWidget(widget)
self.tabBar.setCurrentTab("fitPage") self.tabBar.setCurrentTab("fitPage")
return return
fit_page = FunctionFitInterface(self)
# 延迟导入和创建FunctionFitInterface self.addSubInterface(fit_page, "fitPage", "曲线拟合")
if self.functionFitInterface is None: self.stackedWidget.setCurrentWidget(fit_page)
from .function_fit_interface import FunctionFitInterface
self.functionFitInterface = FunctionFitInterface(self)
self.functionFitInterface.setObjectName("fitPage")
self.addSubInterface(self.functionFitInterface, "fitPage", "曲线拟合")
self.stackedWidget.setCurrentWidget(self.functionFitInterface)
self.tabBar.setCurrentTab("fitPage") self.tabBar.setCurrentTab("fitPage")
def open_ai_tab(self): def open_ai_tab(self):

View File

@ -1,79 +1,35 @@
import serial import serial
import serial.tools.list_ports import serial.tools.list_ports
import pyqtgraph as pg from PyQt5.QtCore import Qt, QThread, pyqtSignal
import struct
import time
from datetime import datetime
from collections import deque
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QTextCursor from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy, QStackedWidget from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from qfluentwidgets import ( from qfluentwidgets import (
FluentIcon, PushButton, ComboBox, TextEdit, LineEdit, CheckBox, FluentIcon, PushButton, ComboBox, TextEdit, LineEdit, CheckBox,
SubtitleLabel, BodyLabel, HorizontalSeparator, PrimaryPushButton, SubtitleLabel, BodyLabel, HorizontalSeparator
isDarkTheme, qconfig, CardWidget, StrongBodyLabel, CaptionLabel
) )
class SerialReadThread(QThread): class SerialReadThread(QThread):
data_received = pyqtSignal(str) data_received = pyqtSignal(str)
raw_data_received = pyqtSignal(bytes)
def __init__(self, ser, parent_widget=None): def __init__(self, ser):
super().__init__() super().__init__()
self.ser = ser self.ser = ser
self.parent_widget = parent_widget
self._running = True self._running = True
self.buffer = bytearray()
self.batch_size = 8192
def run(self): def run(self):
while self._running: while self._running:
if self.ser and self.ser.is_open: if self.ser and self.ser.is_open and self.ser.in_waiting:
try: try:
if self.ser.in_waiting: data = self.ser.readline().decode(errors='ignore')
bytes_to_read = min(self.ser.in_waiting, self.batch_size) self.data_received.emit(data)
raw_data = self.ser.read(bytes_to_read) except Exception:
if raw_data: pass
self.buffer.extend(raw_data)
self.raw_data_received.emit(bytes(raw_data))
# 检查显示设置
is_hex_receive = True
is_timestamp = True
if self.parent_widget:
if hasattr(self.parent_widget, 'hex_receive_checkbox'):
is_hex_receive = self.parent_widget.hex_receive_checkbox.isChecked()
if hasattr(self.parent_widget, 'timestamp_checkbox'):
is_timestamp = self.parent_widget.timestamp_checkbox.isChecked()
# 格式化数据
if is_hex_receive:
display_data = ' '.join([f'{b:02X}' for b in raw_data])
else:
try:
display_data = raw_data.decode('utf-8', errors='replace')
except:
display_data = ' '.join([f'{b:02X}' for b in raw_data])
if display_data:
if is_timestamp:
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
data_to_send = timestamp + display_data + '\n'
else:
data_to_send = display_data + '\n'
self.data_received.emit(data_to_send)
except Exception as e:
print(f"串口读取错误: {e}")
self.msleep(1)
def stop(self): def stop(self):
self._running = False self._running = False
self.wait() self.wait()
class SerialTerminalInterface(QWidget): class SerialTerminalInterface(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent=parent) super().__init__(parent=parent)
@ -81,166 +37,56 @@ class SerialTerminalInterface(QWidget):
main_layout = QVBoxLayout(self) main_layout = QVBoxLayout(self)
main_layout.setSpacing(12) main_layout.setSpacing(12)
# 顶部:基本设置行(始终显示) # 顶部:串口设置区
basic_layout = QHBoxLayout() top_hbox = QHBoxLayout()
basic_layout.addWidget(BodyLabel("串口:")) top_hbox.addWidget(BodyLabel("串口:"))
self.port_combo = ComboBox() self.port_combo = ComboBox()
self.refresh_ports() self.refresh_ports()
basic_layout.addWidget(self.port_combo) top_hbox.addWidget(self.port_combo)
top_hbox.addWidget(BodyLabel("波特率:"))
self.baud_combo = ComboBox()
self.baud_combo.addItems(['9600', '115200', '57600', '38400', '19200', '4800'])
top_hbox.addWidget(self.baud_combo)
self.connect_btn = PushButton("连接")
self.connect_btn.clicked.connect(self.toggle_connection)
top_hbox.addWidget(self.connect_btn)
self.refresh_btn = PushButton(FluentIcon.SYNC, "刷新") self.refresh_btn = PushButton(FluentIcon.SYNC, "刷新")
self.refresh_btn.clicked.connect(self.refresh_ports) self.refresh_btn.clicked.connect(self.refresh_ports)
basic_layout.addWidget(self.refresh_btn) top_hbox.addWidget(self.refresh_btn)
basic_layout.addWidget(BodyLabel("波特率:")) top_hbox.addStretch()
self.baud_combo = ComboBox() main_layout.addLayout(top_hbox)
self.baud_combo.addItems(['115200', '9600', '57600', '38400', '19200', '4800'])
self.baud_combo.setCurrentText('9600')
basic_layout.addWidget(self.baud_combo)
self.connect_btn = PrimaryPushButton("连接串口")
self.connect_btn.clicked.connect(self.toggle_connection)
basic_layout.addWidget(self.connect_btn)
# 展开/折叠按钮
self.expand_btn = PushButton(FluentIcon.DOWN, "高级设置")
self.expand_btn.clicked.connect(self.toggle_advanced_settings)
basic_layout.addWidget(self.expand_btn)
basic_layout.addStretch()
main_layout.addLayout(basic_layout)
# 高级设置 - 默认隐藏
self.advanced_widget = QWidget()
self.advanced_widget.setVisible(False)
advanced_main_layout = QVBoxLayout(self.advanced_widget)
advanced_main_layout.setContentsMargins(0, 8, 0, 0)
advanced_main_layout.setSpacing(8)
# 详细设置行
detail_layout = QHBoxLayout()
detail_layout.addWidget(BodyLabel("数据位:"))
self.data_bits_combo = ComboBox()
self.data_bits_combo.addItems(['8', '7', '6', '5'])
self.data_bits_combo.setCurrentText('8')
detail_layout.addWidget(self.data_bits_combo)
detail_layout.addWidget(BodyLabel("校验位:"))
self.parity_combo = ComboBox()
self.parity_combo.addItems(['None', 'Even', 'Odd', 'Mark', 'Space'])
self.parity_combo.setCurrentText('None')
detail_layout.addWidget(self.parity_combo)
detail_layout.addWidget(BodyLabel("停止位:"))
self.stop_bits_combo = ComboBox()
self.stop_bits_combo.addItems(['1', '1.5', '2'])
self.stop_bits_combo.setCurrentText('1')
detail_layout.addWidget(self.stop_bits_combo)
detail_layout.addStretch()
advanced_main_layout.addLayout(detail_layout)
main_layout.addWidget(self.advanced_widget)
main_layout.addWidget(HorizontalSeparator()) main_layout.addWidget(HorizontalSeparator())
# 初始化状态变量 # 中部:左侧预设命令,右侧显示区
self.ser = None
self.read_thread = None
self.is_chart_mode = False # 默认使用文本模式
self.is_paused = False
# 中部:左侧快捷命令,右侧显示区
center_hbox = QHBoxLayout() center_hbox = QHBoxLayout()
# 左侧:预设命令竖排
# 左侧:快捷命令区域 preset_vbox = QVBoxLayout()
preset_widget = QWidget()
preset_widget.setFixedWidth(250)
preset_vbox = QVBoxLayout(preset_widget)
preset_vbox.addWidget(SubtitleLabel("快捷指令")) preset_vbox.addWidget(SubtitleLabel("快捷指令"))
preset_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter) preset_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter)
# 预设命令配置
self.preset_commands = [ self.preset_commands = [
("性能监视", "htop"), ("线程监视器", "htop"),
("陀螺仪校准", "cali_gyro"), ("陀螺仪校准", "cali_gyro"),
("性能监视", "htop"),
("重启", "reset"), ("重启", "reset"),
("显示所有设备", "ls /dev"),
("查询id", "id"),
] ]
for label, cmd in self.preset_commands: for label, cmd in self.preset_commands:
btn = PushButton(FluentIcon.SEND, label) btn = PushButton(label)
btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
btn.clicked.connect(lambda _, c=cmd: self.send_preset_command(c)) btn.clicked.connect(lambda _, c=cmd: self.send_preset_command(c))
preset_vbox.addWidget(btn) preset_vbox.addWidget(btn)
# 添加使用说明
preset_vbox.addSpacing(16)
preset_vbox.addWidget(HorizontalSeparator())
preset_vbox.addSpacing(8)
# 使用说明标题
usage_title = SubtitleLabel("使用说明")
usage_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
preset_vbox.addWidget(usage_title)
preset_vbox.addSpacing(8)
# 使用说明内容
usage_content = BodyLabel()
usage_content.setText(
"• 波形图显示:\n"
" 发送格式化数据(逗号或空格分隔的数值)\n"
" 如: 1.2, 3.4, 5.6\n"
" 系统将自动识别数据通道并创建波形\n\n"
)
usage_content.setWordWrap(True)
usage_content.setAlignment(Qt.AlignmentFlag.AlignLeft)
preset_vbox.addWidget(usage_content)
preset_vbox.addStretch() preset_vbox.addStretch()
center_hbox.addWidget(preset_widget) main_layout.addLayout(center_hbox, stretch=1)
# 右侧:显示区域 # 右侧:串口数据显示区
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
right_layout.setContentsMargins(0, 0, 0, 0)
# 切换按钮区域
switch_layout = QHBoxLayout()
switch_layout.addWidget(BodyLabel("显示模式:"))
self.mode_toggle_btn = PushButton("切换到波形图")
self.mode_toggle_btn.clicked.connect(self.toggle_display_mode)
switch_layout.addWidget(self.mode_toggle_btn)
self.pause_btn = PushButton(FluentIcon.PAUSE, "暂停接收")
self.pause_btn.clicked.connect(self.toggle_pause_receive)
switch_layout.addWidget(self.pause_btn)
self.clear_btn = PushButton(FluentIcon.DELETE, "清空")
self.clear_btn.clicked.connect(self.clear_display)
switch_layout.addWidget(self.clear_btn)
self.hex_receive_checkbox = CheckBox("HEX接收 ")
self.hex_receive_checkbox.setChecked(True)
switch_layout.addWidget(self.hex_receive_checkbox)
self.timestamp_checkbox = CheckBox("时间戳")
self.timestamp_checkbox.setChecked(True)
switch_layout.addWidget(self.timestamp_checkbox)
switch_layout.addStretch()
right_layout.addLayout(switch_layout)
# 创建堆叠布局用于切换显示内容
self.display_stack = QStackedWidget()
# 原始数据显示页面
self.text_edit = TextEdit() self.text_edit = TextEdit()
self.text_edit.setReadOnly(True) self.text_edit.setReadOnly(True)
self.text_edit.setMinimumWidth(400) self.text_edit.setMinimumWidth(400)
self.display_stack.addWidget(self.text_edit) center_hbox.addWidget(self.text_edit, 3)
center_hbox.addLayout(preset_vbox, 1)
# 波形图显示页面
self.setup_chart_widget()
right_layout.addWidget(self.display_stack)
center_hbox.addWidget(right_widget, 1)
main_layout.addLayout(center_hbox, stretch=1)
main_layout.addWidget(HorizontalSeparator()) main_layout.addWidget(HorizontalSeparator())
# 底部:输入区 # 底部:输入区
@ -249,542 +95,73 @@ class SerialTerminalInterface(QWidget):
self.input_line.setPlaceholderText("输入内容,回车发送") self.input_line.setPlaceholderText("输入内容,回车发送")
self.input_line.returnPressed.connect(self.send_data) self.input_line.returnPressed.connect(self.send_data)
bottom_hbox.addWidget(self.input_line, 4) bottom_hbox.addWidget(self.input_line, 4)
send_btn = PushButton(FluentIcon.SEND, "发送") send_btn = PushButton("发送")
send_btn.clicked.connect(self.send_data) send_btn.clicked.connect(self.send_data)
bottom_hbox.addWidget(send_btn, 1) bottom_hbox.addWidget(send_btn, 1)
self.auto_enter_checkbox = CheckBox("自动回车 ")
self.hex_send_checkbox = CheckBox("HEX发送") self.auto_enter_checkbox.setChecked(True)
self.hex_send_checkbox.setChecked(False) bottom_hbox.addWidget(self.auto_enter_checkbox)
self.hex_send_checkbox.stateChanged.connect(self.update_input_placeholder)
bottom_hbox.addWidget(self.hex_send_checkbox)
bottom_hbox.addWidget(BodyLabel("末尾添加"))
self.line_ending_combo = ComboBox()
self.line_ending_combo.addItems(['\\n', '', '\\r', '\\r\\n'])
self.line_ending_combo.setCurrentText('\\n')
bottom_hbox.addWidget(self.line_ending_combo)
# 自动发送功能
self.auto_send_checkbox = CheckBox("自动发送")
self.auto_send_checkbox.setChecked(False)
self.auto_send_checkbox.stateChanged.connect(self.toggle_auto_send)
bottom_hbox.addWidget(self.auto_send_checkbox)
bottom_hbox.addWidget(BodyLabel("间隔(ms):"))
self.auto_send_interval = LineEdit()
self.auto_send_interval.setPlaceholderText("1000")
self.auto_send_interval.setText("1000")
self.auto_send_interval.setMaximumWidth(80)
bottom_hbox.addWidget(self.auto_send_interval)
bottom_hbox.addStretch() bottom_hbox.addStretch()
main_layout.addLayout(bottom_hbox) main_layout.addLayout(bottom_hbox)
# 数据解析相关 self.ser = None
self.data_buffer = bytearray() self.read_thread = None
self.max_data_points = 5000
self.data_history = {} # 动态存储数据
self.data_timestamps = deque(maxlen=self.max_data_points)
self.data_channels = [] # 数据通道列表
# 图表更新定时器
self.chart_timer = QTimer()
self.chart_timer.timeout.connect(self.update_charts)
self.chart_timer.setInterval(50)
# 自动发送定时器
self.auto_send_timer = QTimer()
self.auto_send_timer.timeout.connect(self.auto_send_data)
# 监听主题变化
qconfig.themeChangedFinished.connect(self.on_theme_changed)
def setup_chart_widget(self):
"""设置波形图显示区域"""
chart_container = QWidget()
chart_main_layout = QHBoxLayout(chart_container)
chart_main_layout.setContentsMargins(0, 0, 0, 0)
chart_main_layout.setSpacing(8)
# 左侧:波形图
self.main_plot = pg.PlotWidget()
self.apply_plot_style()
self.main_plot.setTitle('实时数据波形图', size='14pt')
self.main_plot.showGrid(x=True, y=True, alpha=0.3)
self.main_plot.setLabel('left', '数值')
self.main_plot.setLabel('bottom', '时间 (ms)')
self.main_plot.setAntialiasing(True)
self.main_plot.setMouseEnabled(x=True, y=True)
self.main_plot.enableAutoRange()
chart_main_layout.addWidget(self.main_plot, 3)
# 右侧:实时数据显示面板
self.setup_data_display_panel()
chart_main_layout.addWidget(self.data_display_panel, 1)
self.display_stack.addWidget(chart_container)
self.display_stack.setCurrentIndex(0) # 默认显示文本
# 初始化曲线字典
self.curves = {}
def setup_data_display_panel(self):
"""设置实时数据显示面板"""
self.data_display_panel = CardWidget()
self.data_display_panel.setFixedWidth(200)
panel_layout = QVBoxLayout(self.data_display_panel)
panel_layout.setContentsMargins(16, 16, 16, 16)
panel_layout.setSpacing(12)
title_label = SubtitleLabel("实时数据")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
panel_layout.addWidget(title_label)
panel_layout.addWidget(HorizontalSeparator())
panel_layout.addSpacing(8)
# 数据标签容器
self.data_labels_container = QWidget()
self.data_labels_layout = QVBoxLayout(self.data_labels_container)
self.data_labels_layout.setContentsMargins(0, 0, 0, 0)
self.data_labels_layout.setSpacing(8)
panel_layout.addWidget(self.data_labels_container)
self.data_labels = {}
self.data_cards = {}
panel_layout.addStretch()
def get_theme_colors(self):
"""获取主题颜色"""
colors = [
'#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#f0932b',
'#6c5ce7', '#a29bfe', '#fd79a8', '#fdcb6e', '#e17055'
]
return colors
def apply_plot_style(self):
"""应用波形图样式"""
is_dark = isDarkTheme()
if is_dark:
bg_color = '#2b2b2b'
text_color = '#ffffff'
else:
bg_color = '#ffffff'
text_color = '#333333'
self.main_plot.setBackground(bg_color)
try:
axis_pen = pg.mkPen(color=text_color, width=1)
self.main_plot.getAxis('left').setPen(axis_pen)
self.main_plot.getAxis('bottom').setPen(axis_pen)
self.main_plot.getAxis('left').setTextPen(text_color)
self.main_plot.getAxis('bottom').setTextPen(text_color)
except Exception as e:
print(f"设置坐标轴样式错误: {e}")
def on_theme_changed(self):
"""主题变化时更新样式"""
if hasattr(self, 'main_plot'):
self.apply_plot_style()
def toggle_display_mode(self):
"""切换显示模式"""
self.is_chart_mode = not self.is_chart_mode
if self.is_chart_mode:
self.display_stack.setCurrentIndex(1)
self.mode_toggle_btn.setText("切换到原始数据")
self.hex_receive_checkbox.setVisible(False)
self.timestamp_checkbox.setVisible(False)
if self.ser and self.ser.is_open:
self.chart_timer.start()
else:
self.display_stack.setCurrentIndex(0)
self.mode_toggle_btn.setText("切换到波形图")
self.hex_receive_checkbox.setVisible(True)
self.timestamp_checkbox.setVisible(True)
self.chart_timer.stop()
def toggle_pause_receive(self):
"""切换暂停/恢复"""
self.is_paused = not self.is_paused
if self.is_paused:
self.pause_btn.setIcon(FluentIcon.PLAY)
self.pause_btn.setText("恢复接收")
else:
self.pause_btn.setIcon(FluentIcon.PAUSE)
self.pause_btn.setText("暂停接收")
def clear_display(self):
"""清空显示"""
self.text_edit.clear()
for key in self.data_history:
self.data_history[key].clear()
if hasattr(self, 'data_timestamps'):
self.data_timestamps.clear()
if hasattr(self, 'curves'):
for curve in self.curves.values():
curve.setData([], [])
self.data_buffer.clear()
def toggle_advanced_settings(self):
"""切换高级设置显示"""
is_visible = self.advanced_widget.isVisible()
self.advanced_widget.setVisible(not is_visible)
if is_visible:
self.expand_btn.setIcon(FluentIcon.DOWN)
self.expand_btn.setText("高级设置")
else:
self.expand_btn.setIcon(FluentIcon.UP)
self.expand_btn.setText("收起设置")
def send_preset_command(self, cmd): def send_preset_command(self, cmd):
"""发送预设命令"""
self.input_line.setText(cmd) self.input_line.setText(cmd)
self.send_data() self.send_data()
def refresh_ports(self): def refresh_ports(self):
"""刷新串口列表"""
self.port_combo.clear() self.port_combo.clear()
ports = serial.tools.list_ports.comports() ports = serial.tools.list_ports.comports()
for port in ports: for port in ports:
self.port_combo.addItem(port.device) self.port_combo.addItem(port.device)
def toggle_connection(self): def toggle_connection(self):
"""切换连接状态"""
if self.ser and self.ser.is_open: if self.ser and self.ser.is_open:
self.disconnect_serial() self.disconnect_serial()
else: else:
self.connect_serial() self.connect_serial()
def connect_serial(self): def connect_serial(self):
"""连接串口"""
port = self.port_combo.currentText() port = self.port_combo.currentText()
baud = int(self.baud_combo.currentText()) baud = int(self.baud_combo.currentText())
data_bits = int(self.data_bits_combo.currentText())
parity_map = {
'None': serial.PARITY_NONE,
'Even': serial.PARITY_EVEN,
'Odd': serial.PARITY_ODD,
'Mark': serial.PARITY_MARK,
'Space': serial.PARITY_SPACE
}
parity = parity_map[self.parity_combo.currentText()]
stop_bits_map = {
'1': serial.STOPBITS_ONE,
'1.5': serial.STOPBITS_ONE_POINT_FIVE,
'2': serial.STOPBITS_TWO
}
stop_bits = stop_bits_map[self.stop_bits_combo.currentText()]
try: try:
self.ser = serial.Serial( self.ser = serial.Serial(port, baud, timeout=0.1)
port=port, self.connect_btn.setText("断开")
baudrate=baud, self.text_edit.append(f"已连接到 {port} @ {baud}")
bytesize=data_bits, self.read_thread = SerialReadThread(self.ser)
parity=parity,
stopbits=stop_bits,
timeout=0.1
)
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
self.connect_btn.setText("断开连接")
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}已连接到 {port} @ {baud}")
self.read_thread = SerialReadThread(self.ser, self)
self.read_thread.data_received.connect(self.display_data) self.read_thread.data_received.connect(self.display_data)
self.read_thread.raw_data_received.connect(self.process_raw_data)
self.read_thread.start() self.read_thread.start()
if self.is_chart_mode:
self.chart_timer.start()
except Exception as e: except Exception as e:
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] " self.text_edit.append(f"连接失败: {e}")
self.text_edit.append(f"{timestamp}连接失败: {e}")
def disconnect_serial(self): def disconnect_serial(self):
"""断开串口"""
self.chart_timer.stop()
self.auto_send_timer.stop() # 停止自动发送
if self.read_thread: if self.read_thread:
self.read_thread.stop() self.read_thread.stop()
self.read_thread = None self.read_thread = None
if self.ser: if self.ser:
self.ser.close() self.ser.close()
self.ser = None self.ser = None
self.connect_btn.setText("连接串口") self.connect_btn.setText("连接")
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] " self.text_edit.append("已断开连接")
self.text_edit.append(f"{timestamp}已断开连接")
def display_data(self, data): def display_data(self, data):
"""显示接收数据"""
if self.is_paused:
return
self.text_edit.moveCursor(QTextCursor.End) self.text_edit.moveCursor(QTextCursor.End)
self.text_edit.insertPlainText(data) self.text_edit.insertPlainText(data)
self.text_edit.moveCursor(QTextCursor.End) self.text_edit.moveCursor(QTextCursor.End)
if len(self.text_edit.toPlainText()) > 10000:
cursor = self.text_edit.textCursor()
cursor.movePosition(QTextCursor.Start)
cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, 5000)
cursor.removeSelectedText()
self.text_edit.moveCursor(QTextCursor.End)
def send_data(self): def send_data(self):
"""发送数据"""
if self.ser and self.ser.is_open: if self.ser and self.ser.is_open:
text = self.input_line.text() text = self.input_line.text()
try: try:
if self.hex_send_checkbox.isChecked(): if not text:
hex_data = self.parse_hex_string(text) self.ser.write('\n'.encode())
if hex_data is not None:
self.ser.write(hex_data)
line_ending = self.get_line_ending()
if line_ending:
self.ser.write(line_ending.encode())
sent_hex = ' '.join([f'{b:02X}' for b in hex_data])
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}发送: {sent_hex}")
else:
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}HEX格式错误")
else: else:
data_to_send = text for char in text:
line_ending = self.get_line_ending() self.ser.write(char.encode())
if line_ending: if self.auto_enter_checkbox.isChecked():
data_to_send += line_ending self.ser.write('\n'.encode())
self.ser.write(data_to_send.encode())
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}发送: {text}")
except Exception as e: except Exception as e:
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] " self.text_edit.append(f"发送失败: {e}")
self.text_edit.append(f"{timestamp}发送失败: {e}") self.input_line.clear()
# 只有在非自动发送模式下才清空输入框
if not self.auto_send_checkbox.isChecked():
self.input_line.clear()
def parse_hex_string(self, hex_str):
"""解析十六进制字符串"""
try:
hex_str = hex_str.replace(' ', '').replace('\t', '').upper()
if len(hex_str) % 2 != 0:
return None
byte_data = bytearray()
for i in range(0, len(hex_str), 2):
byte_data.append(int(hex_str[i:i+2], 16))
return bytes(byte_data)
except ValueError:
return None
def get_line_ending(self):
"""获取行结束符"""
ending_text = self.line_ending_combo.currentText()
ending_map = {
'': '',
'\\n': '\n',
'\\r': '\r',
'\\r\\n': '\r\n'
}
return ending_map.get(ending_text, '')
def update_input_placeholder(self):
"""更新输入框提示"""
if self.hex_send_checkbox.isChecked():
self.input_line.setPlaceholderText("输入十六进制数据,如: AA 01 BB")
else:
self.input_line.setPlaceholderText("输入内容,回车发送")
def process_raw_data(self, raw_data):
"""处理原始数据 - 自动解析数据结构"""
if self.is_paused or not self.is_chart_mode:
return
self.data_buffer.extend(raw_data)
# 尝试解析数据包格式
self.auto_parse_data()
def auto_parse_data(self):
"""自动解析数据格式"""
# 简单示例:假设数据是以空格或逗号分隔的浮点数
try:
text_data = self.data_buffer.decode('utf-8', errors='ignore')
lines = text_data.strip().split('\n')
for line in lines:
if not line.strip():
continue
# 尝试解析为数值
values = []
for separator in [',', ' ', '\t', ';']:
try:
parts = [p.strip() for p in line.split(separator) if p.strip()]
values = [float(p) for p in parts]
if values:
break
except:
continue
if values:
# 动态创建数据通道
num_channels = len(values)
if len(self.data_channels) != num_channels:
self.create_data_channels(num_channels)
# 存储数据
current_time = time.time() * 1000
self.data_timestamps.append(current_time)
for i, value in enumerate(values):
channel_name = f'CH{i+1}'
if channel_name in self.data_history:
self.data_history[channel_name].append(value)
self.data_buffer.clear()
except Exception as e:
print(f"数据解析错误: {e}")
def create_data_channels(self, num_channels):
"""创建数据通道"""
# 清除旧的
self.data_channels.clear()
self.data_history.clear()
self.curves.clear()
if hasattr(self, 'main_plot'):
self.main_plot.clear()
# 创建新的
colors = self.get_theme_colors()
for i in range(num_channels):
channel_name = f'CH{i+1}'
self.data_channels.append(channel_name)
self.data_history[channel_name] = deque(maxlen=self.max_data_points)
# 创建曲线
color = colors[i % len(colors)]
pen = pg.mkPen(color=color, width=2)
curve = self.main_plot.plot(pen=pen, name=channel_name)
self.curves[channel_name] = curve
# 创建数据显示卡片
self.create_data_card(channel_name, color)
# 添加图例
if hasattr(self, 'main_plot'):
self.main_plot.addLegend()
def create_data_card(self, channel_name, color):
"""创建数据显示卡片"""
data_card = QWidget()
data_card.setObjectName(f"dataCard_{channel_name}")
card_layout = QVBoxLayout(data_card)
card_layout.setContentsMargins(8, 6, 8, 6)
card_layout.setSpacing(2)
name_label = CaptionLabel(channel_name)
name_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
card_layout.addWidget(name_label)
value_label = StrongBodyLabel("--")
value_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
value_label.setObjectName(f"valueLabel_{channel_name}")
self.data_labels[channel_name] = value_label
card_layout.addWidget(value_label)
self.apply_data_card_style(data_card, color)
self.data_cards[channel_name] = data_card
self.data_labels_layout.addWidget(data_card)
def apply_data_card_style(self, card_widget, accent_color):
"""应用数据卡片样式"""
is_dark = isDarkTheme()
if is_dark:
bg_color = "rgba(45, 45, 45, 0.8)"
else:
bg_color = "rgba(255, 255, 255, 0.9)"
card_style = f"""
QWidget[objectName^="dataCard"] {{
background-color: {bg_color};
border: 2px solid {accent_color};
border-radius: 6px;
margin: 2px;
}}
"""
card_widget.setStyleSheet(card_style)
def update_charts(self):
"""更新波形图"""
try:
current_time = time.time() * 1000
for channel_name, data in self.data_history.items():
if len(data) > 0 and channel_name in self.curves:
timestamps = list(self.data_timestamps)
y_data = list(data)
if len(timestamps) >= len(y_data):
used_timestamps = timestamps[-len(y_data):]
else:
used_timestamps = timestamps.copy()
for i in range(len(y_data) - len(timestamps)):
if used_timestamps:
estimated_time = used_timestamps[-1] + 1
else:
estimated_time = current_time - (len(y_data) - i - 1)
used_timestamps.append(estimated_time)
x_data = [t - current_time for t in used_timestamps]
self.curves[channel_name].setData(x_data, y_data, _callSync='off')
# 更新数据标签
if channel_name in self.data_labels:
latest_value = data[-1]
self.data_labels[channel_name].setText(f"{latest_value:.2f}")
except Exception as e:
print(f"图表更新错误: {e}")
def toggle_auto_send(self, state):
"""切换自动发送"""
if state == Qt.CheckState.Checked:
try:
interval = int(self.auto_send_interval.text())
if interval < 10:
interval = 10 # 最小间隔10ms
self.auto_send_timer.setInterval(interval)
self.auto_send_timer.start()
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}自动发送已启动,间隔: {interval}ms")
except ValueError:
self.auto_send_checkbox.setChecked(False)
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}间隔时间格式错误")
else:
self.auto_send_timer.stop()
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}自动发送已停止")
def auto_send_data(self):
"""自动发送数据"""
if self.ser and self.ser.is_open:
self.send_data()

View File

@ -341,242 +341,4 @@ class analyzing_ioc:
'signal': signal 'signal': signal
}) })
return pwm_channels return pwm_channels
@staticmethod
def get_mcu_name_from_ioc(ioc_path):
"""
.ioc文件中获取MCU型号
返回格式: 'STM32F407IGHx'
"""
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()
# 查找MCU名称
if key == 'Mcu.UserName' or key == 'Mcu.Name':
return value
return None
@staticmethod
def get_flash_config_from_mcu(mcu_name):
"""
根据MCU型号返回Flash配置
支持STM32F1/F4/H7系列
返回格式: {
'sectors': [...], # Sector/Page配置列表
'dual_bank': False, # 是否双Bank
'end_address': 0x08100000, # Flash结束地址
'type': 'sector' or 'page' # Flash类型
}
"""
if not mcu_name:
return None
mcu_upper = mcu_name.upper()
# STM32F1系列 - 使用Page而不是Sector
if mcu_upper.startswith('STM32F1'):
return analyzing_ioc._get_stm32f1_flash_config(mcu_upper)
# STM32F4系列 - 使用Sector
elif mcu_upper.startswith('STM32F4'):
return analyzing_ioc._get_stm32f4_flash_config(mcu_upper)
# STM32H7系列 - 使用Sector
elif mcu_upper.startswith('STM32H7'):
return analyzing_ioc._get_stm32h7_flash_config(mcu_upper)
return None
@staticmethod
def _get_stm32f1_flash_config(mcu_upper):
"""
STM32F1系列Flash配置
F1使用Page而不是Sector
- /中容量设备: 1KB/page
- 大容量/互联型设备: 2KB/page
容量代码: 4/6=16/32KB, 8/B=64/128KB, C=256KB, D/E=384/512KB, F/G=768KB/1MB
"""
flash_size_map_f1 = {
'4': 16, # 16KB
'6': 32, # 32KB
'8': 64, # 64KB
'B': 128, # 128KB
'C': 256, # 256KB
'D': 384, # 384KB
'E': 512, # 512KB
'F': 768, # 768KB (互联型)
'G': 1024, # 1MB (互联型)
}
# F1命名: STM32F103C8T6, C在索引9
if len(mcu_upper) < 10:
return None
flash_code = mcu_upper[9]
flash_size = flash_size_map_f1.get(flash_code)
if not flash_size:
return None
# 判断页大小: <=128KB用1KB页, >128KB用2KB页
page_size = 1 if flash_size <= 128 else 2
num_pages = flash_size // page_size
config = {
'type': 'page',
'dual_bank': False,
'sectors': [], # F1中这里存的是Page
'page_size': page_size,
}
# 生成所有页
current_address = 0x08000000
for page_id in range(num_pages):
config['sectors'].append({
'id': page_id,
'address': current_address,
'size': page_size
})
current_address += page_size * 1024
config['end_address'] = current_address
return config
@staticmethod
def _get_stm32f4_flash_config(mcu_upper):
"""
STM32F4系列Flash配置
容量代码: C=256KB, E=512KB, G=1MB, I=2MB
"""
flash_size_map = {
'C': 256, # 256KB
'E': 512, # 512KB
'G': 1024, # 1MB
'I': 2048, # 2MB
}
# F4命名: STM32F407IGHx, I在索引9
if len(mcu_upper) < 10:
return None
flash_code = mcu_upper[9]
flash_size = flash_size_map.get(flash_code)
if not flash_size:
return None
config = {
'type': 'sector',
'dual_bank': False,
'sectors': [],
}
# STM32F4系列单Bank Flash布局
# Sector 0-3: 16KB each
# Sector 4: 64KB
# Sector 5-11: 128KB each (如果有)
base_sectors = [
{'id': 0, 'address': 0x08000000, 'size': 16},
{'id': 1, 'address': 0x08004000, 'size': 16},
{'id': 2, 'address': 0x08008000, 'size': 16},
{'id': 3, 'address': 0x0800C000, 'size': 16},
{'id': 4, 'address': 0x08010000, 'size': 64},
]
config['sectors'] = base_sectors.copy()
current_address = 0x08020000
current_id = 5
remaining_kb = flash_size - (16 * 4 + 64) # 减去前5个sector
# 添加128KB的sectors
while remaining_kb > 0 and current_id < 12:
config['sectors'].append({
'id': current_id,
'address': current_address,
'size': 128
})
current_address += 0x20000 # 128KB
remaining_kb -= 128
current_id += 1
# 设置结束地址
config['end_address'] = current_address
# 2MB Flash需要双Bank (Sector 12-23)
if flash_size >= 2048:
config['dual_bank'] = True
# Bank 2 的sectors (12-15: 16KB, 16: 64KB, 17-23: 128KB)
bank2_sectors = [
{'id': 12, 'address': 0x08100000, 'size': 16},
{'id': 13, 'address': 0x08104000, 'size': 16},
{'id': 14, 'address': 0x08108000, 'size': 16},
{'id': 15, 'address': 0x0810C000, 'size': 16},
{'id': 16, 'address': 0x08110000, 'size': 64},
{'id': 17, 'address': 0x08120000, 'size': 128},
{'id': 18, 'address': 0x08140000, 'size': 128},
{'id': 19, 'address': 0x08160000, 'size': 128},
{'id': 20, 'address': 0x08180000, 'size': 128},
{'id': 21, 'address': 0x081A0000, 'size': 128},
{'id': 22, 'address': 0x081C0000, 'size': 128},
{'id': 23, 'address': 0x081E0000, 'size': 128},
]
config['sectors'].extend(bank2_sectors)
config['end_address'] = 0x08200000
return config
@staticmethod
def _get_stm32h7_flash_config(mcu_upper):
"""
STM32H7系列Flash配置
- 每个Sector 128KB
- 单Bank: 8个Sector (1MB)
- 双Bank: 16个Sector (2MB), 每个Bank 8个Sector
容量代码: B=128KB, G=1MB, I=2MB
命名格式: STM32H7 + 23 + V(引脚) + G(容量) + T(封装) + 6
"""
flash_size_map_h7 = {
'B': 128, # 128KB (1个Sector)
'G': 1024, # 1MB (8个Sector, 单Bank)
'I': 2048, # 2MB (16个Sector, 双Bank)
}
# H7命名: STM32H723VGT6, G在索引10
if len(mcu_upper) < 11:
return None
flash_code = mcu_upper[10]
flash_size = flash_size_map_h7.get(flash_code)
if not flash_size:
return None
config = {
'type': 'sector',
'dual_bank': flash_size >= 2048,
'sectors': [],
}
num_sectors = flash_size // 128 # 每个Sector 128KB
# 生成Sector配置
current_address = 0x08000000
for sector_id in range(num_sectors):
config['sectors'].append({
'id': sector_id,
'address': current_address,
'size': 128,
'bank': 1 if sector_id < 8 else 2 # Bank信息
})
current_address += 0x20000 # 128KB
config['end_address'] = current_address
return config

View File

@ -8,9 +8,7 @@ import tempfile
import time import time
def update_code(parent=None, info_callback=None, error_callback=None): def update_code(parent=None, info_callback=None, error_callback=None):
# 优先使用 GitHub备用 Gitea url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
github_url = "https://github.com/lvzucheng/MRobot/archive/refs/heads/User_code.zip"
gitea_url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
# 导入 CodeGenerator 以使用统一的路径获取逻辑 # 导入 CodeGenerator 以使用统一的路径获取逻辑
try: try:
@ -55,39 +53,10 @@ def update_code(parent=None, info_callback=None, error_callback=None):
local_dir = os.path.join(assets_dir, "User_code") local_dir = os.path.join(assets_dir, "User_code")
print(f"更新代码:最终目标目录: {local_dir}") print(f"更新代码:最终目标目录: {local_dir}")
# 尝试从 GitHub 下载,失败则使用 Gitea
download_successful = False
resp = None
# 首先尝试 GitHub
try: try:
print("尝试从 GitHub 下载代码...") # 下载远程代码库
resp = requests.get(github_url, timeout=15) resp = requests.get(url, timeout=30)
resp.raise_for_status() resp.raise_for_status()
download_successful = True
print("成功从 GitHub 下载")
except Exception as github_error:
print(f"GitHub 下载失败: {github_error}")
print("切换到备用源 Gitea...")
# 切换到 Gitea
try:
resp = requests.get(gitea_url, timeout=30)
resp.raise_for_status()
download_successful = True
print("成功从 Gitea 下载")
except Exception as gitea_error:
print(f"Gitea 下载也失败: {gitea_error}")
if error_callback:
error_callback(parent, f"所有下载源均失败\nGitHub: {github_error}\nGitea: {gitea_error}")
return False
if not download_successful or resp is None:
if error_callback:
error_callback(parent, "下载失败")
return False
try:
# 创建临时目录进行操作 # 创建临时目录进行操作
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:

Binary file not shown.

View File

@ -9,4 +9,4 @@ time,获取时间戳函数需要开启freerots
dwt,需要开启dwt获取时间 dwt,需要开启dwt获取时间
i2c,请开启i2c的dma和中断 i2c,请开启i2c的dma和中断
pwm,用于选择那些勇于输出pwm pwm,用于选择那些勇于输出pwm
flash,自动识别MCU型号并配置Flash支持STM32F1(Page)/F4(Sector)/H7(Sector)自动处理单Bank/双Bank配置

1 uart 请开启uart的dma和中断
9 dwt 需要开启dwt,获取时间
10 i2c 请开启i2c的dma和中断
11 pwm 用于选择那些勇于输出pwm
12

View File

@ -1,77 +0,0 @@
# Flash BSP 更新日志
## v2.0 - 2026-01-01
### 新增功能
✨ **多系列MCU支持**
- 新增 STM32F1 系列支持Page模式
- 新增 STM32H7 系列支持Sector模式
- 保持 STM32F4 系列支持Sector模式
### STM32F1系列详情
- **Flash组织**: Page模式
- **页大小**:
- 小/中容量≤128KB: 1KB/页
- 大容量/互联型(>128KB: 2KB/页
- **容量支持**: 16KB - 1MB
- **容量代码**: 4/6/8/B/C/D/E/F/G
- **生成宏**: `ADDR_FLASH_PAGE_X`
### STM32H7系列详情
- **Flash组织**: Sector模式扇区
- **扇区大小**: 固定128KB
- **容量支持**: 128KB - 2MB
- **容量代码**: B/G/I
- **Bank支持**:
- 单Bank: 1MB (8个Sector)
- 双Bank: 2MB (16个Sector)
- **生成宏**: `ADDR_FLASH_SECTOR_X`
### 技术改进
- 重构 `get_flash_config_from_mcu()` 函数为多系列架构
- 新增 `_get_stm32f1_flash_config()` - F1系列专用配置
- 新增 `_get_stm32f4_flash_config()` - F4系列专用配置
- 新增 `_get_stm32h7_flash_config()` - H7系列专用配置
- 配置中新增 `type` 字段区分 'page' 和 'sector' 模式
- 界面自动识别并显示Page或Sector模式
- 代码生成支持Page和Sector两种宏定义
### 示例支持的芯片型号
**STM32F1:**
- STM32F103C8T6 → 64KB (64 pages × 1KB)
- STM32F103RCT6 → 256KB (128 pages × 2KB)
- STM32F103ZET6 → 512KB (256 pages × 2KB)
**STM32F4:**
- STM32F407VGT6 → 1MB (Sector 0-11)
- STM32F407IGH6 → 2MB (Sector 0-23, 双Bank)
- STM32F405RGT6 → 1MB (Sector 0-11)
**STM32H7:**
- STM32H750VBT6 → 128KB (1 sector)
- STM32H743VGT6 → 1MB (8 sectors)
- STM32H743VIT6 → 2MB (16 sectors, 双Bank)
### 配置文件变化
```yaml
# 新增字段
flash:
type: page # 或 sector
page_size: 2 # 仅F1系列有此字段
```
### 文档更新
- 更新 README.md 包含三个系列的完整说明
- 新增各系列的Flash布局图
- 新增各系列的使用示例
- 更新注意事项包含擦除时间和寿命信息
---
## v1.0 - 初始版本
### 初始功能
- STM32F4 系列支持
- 自动识别芯片型号
- 单Bank/双Bank配置
- 基础API擦除、读、写

View File

@ -1,346 +0,0 @@
# Flash BSP 自动配置说明
## 功能特性
Flash BSP模块能够自动识别STM32芯片型号并生成对应的Flash配置代码。
### 支持的芯片系列
#### STM32F1 系列
- 使用**Page**组织方式而非Sector
- 自动检测Flash容量16KB - 1MB
- 小/中容量设备1KB/页
- 大容量/互联型设备2KB/页
#### STM32F4 系列
- 使用**Sector**组织方式
- 自动检测Flash容量256KB/512KB/1MB/2MB
- 自动配置单Bank或双Bank模式
- 不同大小的Sector16KB/64KB/128KB
#### STM32H7 系列
- 使用**Sector**组织方式
- 每个Sector固定128KB
- 自动检测Flash容量128KB/1MB/2MB
- 自动配置单Bank或双Bank模式
### Flash容量识别规则
根据STM32命名规则中的第9位字符识别Flash容量
**STM32F1系列:**
- **4**: 16KB (16 pages × 1KB)
- **6**: 32KB (32 pages × 1KB)
- **8**: 64KB (64 pages × 1KB)
- **B**: 128KB (128 pages × 1KB)
- **C**: 256KB (128 pages × 2KB)
- **D**: 384KB (192 pages × 2KB)
- **E**: 512KB (256 pages × 2KB)
- **F**: 768KB (384 pages × 2KB, 互联型)
- **G**: 1MB (512 pages × 2KB, 互联型)
**STM32F4系列:**
- **C**: 256KB (单Bank, Sector 0-7)
- **E**: 512KB (单Bank, Sector 0-9)
- **G**: 1MB (单Bank, Sector 0-11)
- **I**: 2MB (双Bank, Sector 0-23)
**STM32H7系列:**
- **B**: 128KB (1个Sector, 单Bank)
- **G**: 1MB (8个Sector, 单Bank)
- **I**: 2MB (16个Sector, 双Bank)
例如:
- `STM32F103C8T6` → 64KB Flash (64 pages × 1KB)
- `STM32F103RCT6` → 256KB Flash (128 pages × 2KB)
- `STM32F103ZET6` → 512KB Flash (256 pages × 2KB)
- `STM32F407VGT6` → 1MB Flash (Sector 0-11)
- `STM32F407IGH6` → 2MB Flash (Sector 0-23, 双Bank)
- `STM32F405RGT6` → 1MB Flash (Sector 0-11)
- `STM32H743VIT6` → 2MB Flash (16 sectors × 128KB, 双Bank)
- `STM32H750VBT6` → 128KB Flash (1 sector × 128KB)
## Flash布局
### STM32F1 Page模式 (16KB - 1MB)
```
小/中容量 (≤128KB): 每页1KB
Page 0: 0x08000000 - 0x080003FF (1KB)
Page 1: 0x08000400 - 0x080007FF (1KB)
...
大容量/互联型 (>128KB): 每页2KB
Page 0: 0x08000000 - 0x080007FF (2KB)
Page 1: 0x08000800 - 0x08000FFF (2KB)
...
```
### STM32F4 单Bank模式 (256KB - 1MB)
```
Sector 0-3: 16KB each (0x08000000 - 0x0800FFFF)
Sector 4: 64KB (0x08010000 - 0x0801FFFF)
Sector 5-11: 128KB each (0x08020000 - 0x080FFFFF)
```
### STM32F4 双Bank模式 (2MB)
```
Bank 1:
Sector 0-3: 16KB each (0x08000000 - 0x0800FFFF)
Sector 4: 64KB (0x08010000 - 0x0801FFFF)
Sector 5-11: 128KB each (0x08020000 - 0x080FFFFF)
Bank 2:
Sector 12-15: 16KB each (0x08100000 - 0x0810FFFF)
Sector 16: 64KB (0x08110000 - 0x0811FFFF)
Sector 17-23: 128KB each (0x08120000 - 0x081FFFFF)
```
### STM32H7 Sector模式
```
单Bank (1MB):
Sector 0-7: 128KB each (0x08000000 - 0x080FFFFF)
双Bank (2MB):
Bank 1:
Sector 0-7: 128KB each (0x08000000 - 0x080FFFFF)
Bank 2:
Sector 8-15: 128KB each (0x08100000 - 0x081FFFFF)
```
## 使用方法
### 1. 在BSP配置界面启用Flash
在代码生成界面的BSP标签中勾选"生成 Flash 代码"选项。
### 2. 自动检测
系统会自动:
- 读取项目中的`.ioc`文件
- 提取MCU型号信息
- 计算Flash扇区配置
- 生成对应的宏定义
### 3. 生成的代码示例
**STM32F1系列** (以STM32F103RCT6为例 - 256KB):
```c
// flash.h
#define ADDR_FLASH_PAGE_0 ((uint32_t)0x08000000)
/* Base address of Page 0, 2 Kbytes */
#define ADDR_FLASH_PAGE_1 ((uint32_t)0x08000800)
/* Base address of Page 1, 2 Kbytes */
...
#define ADDR_FLASH_PAGE_127 ((uint32_t)0x0803F800)
/* Base address of Page 127, 2 Kbytes */
#define ADDR_FLASH_END ((uint32_t)0x08040000)
// flash.c
#define BSP_FLASH_MAX_PAGE 127
if (page >= 0 && page <= 127) {
// 擦除代码...
}
```
**STM32F4系列** (以STM32F407IGH6为例 - 2MB):
```c
// flash.h
#define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000)
/* Base address of Sector 0, 16 Kbytes */
...
#define ADDR_FLASH_SECTOR_23 ((uint32_t)0x081E0000)
/* Base address of Sector 23, 128 Kbytes */
#define ADDR_FLASH_END ((uint32_t)0x08200000)
/* End address for flash */
```
**flash.c**:
```c
#define BSP_FLASH_MAX_SECTOR 23
void BSP_Flash_EraseSector(uint32_t sector) {
if (sector > 0 && sector <= 23) {
// 擦除代码...
}
}
```
**STM32H7系列** (以STM32H743VIT6为例 - 2MB):
```c
// flash.h
#define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000)
/* Base address of Sector 0, 128 Kbytes */
...
#define ADDR_FLASH_SECTOR_15 ((uint32_t)0x081E0000)
/* Base address of Sector 15, 128 Kbytes */
#define ADDR_FLASH_END ((uint32_t)0x08200000)
// flash.c
#define BSP_FLASH_MAX_SECTOR 15
if (sector > 0 && sector <= 15) {
// 擦除代码...
}
```
## API接口
### BSP_Flash_EraseSector (F4/H7) / BSP_Flash_ErasePage (F1)
擦除指定扇区或页
```c
// F4/H7系列
void BSP_Flash_EraseSector(uint32_t sector);
// F1系列
void BSP_Flash_ErasePage(uint32_t page);
```
- **参数**:
- sector/page - 扇区号或页号
- F1: 0 到 (页数-1)
- F4: 0-11 或 0-23根据芯片型号
- H7: 0-7 或 0-15根据芯片型号
### BSP_Flash_WriteBytes
写入数据到Flash
```c
void BSP_Flash_WriteBytes(uint32_t address, const uint8_t *buf, size_t len);
```
- **参数**:
- address - Flash地址
- buf - 数据缓冲区
- len - 数据长度
### BSP_Flash_ReadBytes
从Flash读取数据
```c
void BSP_Flash_ReadBytes(uint32_t address, void *buf, size_t len);
```
- **参数**:
- address - Flash地址
- buf - 接收缓冲区
- len - 读取长度
## 使用示例
### STM32F1系列示例
```c
#include "bsp/flash.h"
void save_config_f1(void) {
// 擦除Page 127 (最后一页,通常用于存储用户数据)
BSP_Flash_ErasePage(127);
// 写入配置数据
uint8_t config[100] = {/* 配置数据 */};
BSP_Flash_WriteBytes(ADDR_FLASH_PAGE_127, config, sizeof(config));
}
void load_config_f1(void) {
// 读取配置数据
uint8_t config[100];
BSP_Flash_ReadBytes(ADDR_FLASH_PAGE_127, config, sizeof(config));
}
```
### STM32F4系列示例
```c
#include "bsp/flash.h"
void save_config_f4(void) {
// 擦除Sector 11 (通常用于存储用户数据)
BSP_Flash_EraseSector(11);
// 写入配置数据
uint8_t config[100] = {/* 配置数据 */};
BSP_Flash_WriteBytes(ADDR_FLASH_SECTOR_11, config, sizeof(config));
}
void load_config_f4(void) {
// 读取配置数据
uint8_t config[100];
BSP_Flash_ReadBytes(ADDR_FLASH_SECTOR_11, config, sizeof(config));
}
```
### STM32H7系列示例
### STM32H7系列示例
```c
#include "bsp/flash.h"
void save_config(void) {
// 擦除Sector 11 (通常用于存储用户数据)
BSP_Flash_EraseSector(11);
// 写入配置数据
uint8_t config[100] = {/* 配置数据 */};
BSP_Flash_WriteBytes(ADDR_FLASH_SECTOR_11, config, sizeof(config));
}
void load_config(void) {
// 读取配置数据
uint8_t config[100];
BSP_Flash_ReadBytes(ADDR_FLASH_SECTOR_11, config, sizeof(config));
}
```
## 注意事项
1. **擦除时间**: Flash擦除需要一定时间注意不要在中断中执行
- F1 Page擦除: ~20ms
- F4 Sector擦除: 16KB~100ms, 64KB~300ms, 128KB~500ms
- H7 Sector擦除: ~200ms
2. **写入前擦除**:
- F1: 必须先擦除整页才能写入
- F4/H7: 必须先擦除整个扇区才能写入
3. **区域选择**: 避免擦除包含程序代码的扇区/页
- F1: 通常最后几页用于存储数据
- F4: Sector 11 或 23 常用于存储数据
- H7: Sector 7 或 15 常用于存储数据
4. **写入对齐**: 建议按字节写入HAL库会处理对齐
5. **断电保护**: 写入过程中断电可能导致数据丢失
6. **擦写次数限制**:
- F1: 典型10,000次
- F4/H7: 典型10,000-100,000次
## 配置文件
配置信息保存在 `bsp_config.yaml`:
**STM32F1:**
```yaml
flash:
enabled: true
mcu_name: STM32F103RCT6
dual_bank: false
sectors: 128 # 实际是128个页
type: page
page_size: 2
```
**STM32F4:**
```yaml
flash:
enabled: true
mcu_name: STM32F407IGHx
dual_bank: true
sectors: 24
type: sector
```
**STM32H7:**
```yaml
flash:
enabled: true
mcu_name: STM32H743VIT6
dual_bank: true
sectors: 16
type: sector
```
## 扩展支持
当前支持的系列:
- ✅ STM32F1 (Page模式)
- ✅ STM32F4 (Sector模式)
- ✅ STM32H7 (Sector模式)
如需支持其他STM32系列如F2/F3/L4/G4等可在 `analyzing_ioc.py``get_flash_config_from_mcu()` 函数中添加相应的配置规则。

View File

@ -1,55 +0,0 @@
/* Includes ----------------------------------------------------------------- */
#include "bsp/flash.h"
#include <main.h>
#include <string.h>
/* Private define ----------------------------------------------------------- */
/* USER CODE BEGIN FLASH_MAX_SECTOR */
/* AUTO GENERATED FLASH_MAX_SECTOR */
/* USER CODE END FLASH_MAX_SECTOR */
/* Private macro ------------------------------------------------------------ */
/* Private typedef ---------------------------------------------------------- */
/* Private variables -------------------------------------------------------- */
/* Private function -------------------------------------------------------- */
/* Exported functions ------------------------------------------------------- */
void BSP_Flash_EraseSector(uint32_t sector) {
FLASH_EraseInitTypeDef flash_erase;
uint32_t sector_error;
/* USER CODE BEGIN FLASH_ERASE_CHECK */
/* AUTO GENERATED FLASH_ERASE_CHECK */
/* USER CODE END FLASH_ERASE_CHECK */
flash_erase.Sector = sector;
flash_erase.TypeErase = FLASH_TYPEERASE_SECTORS;
flash_erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
flash_erase.NbSectors = 1;
HAL_FLASH_Unlock();
while (FLASH_WaitForLastOperation(50) != HAL_OK)
;
HAL_FLASHEx_Erase(&flash_erase, &sector_error);
HAL_FLASH_Lock();
}
/* USER CODE BEGIN FLASH_ERASE_END */
/* USER CODE END FLASH_ERASE_END */
}
void BSP_Flash_WriteBytes(uint32_t address, const uint8_t *buf, size_t len) {
HAL_FLASH_Unlock();
while (len > 0) {
while (FLASH_WaitForLastOperation(50) != HAL_OK)
;
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, address, *buf);
address++;
buf++;
len--;
}
HAL_FLASH_Lock();
}
void BSP_Flash_ReadBytes(uint32_t address, void *buf, size_t len) {
memcpy(buf, (void *)address, len);
}

View File

@ -1,31 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------ */
#include <main.h>
#include "bsp/bsp.h"
/* Exported constants -------------------------------------------------------- */
/* Base address of the Flash sectors */
/* USER CODE BEGIN FLASH_SECTOR_DEFINES */
/* AUTO GENERATED FLASH_SECTORS */
/* USER CODE END FLASH_SECTOR_DEFINES */
/* USER CODE BEGIN FLASH_END_ADDRESS */
/* AUTO GENERATED FLASH_END_ADDRESS */
/* USER CODE END FLASH_END_ADDRESS */
/* Exported macro ------------------------------------------------------------ */
/* Exported types ------------------------------------------------------------ */
/* Exported functions prototypes --------------------------------------------- */
void BSP_Flash_EraseSector(uint32_t sector);
void BSP_Flash_WriteBytes(uint32_t address, const uint8_t *buf, size_t len);
void BSP_Flash_ReadBytes(uint32_t address, void *buf, size_t len);
#ifdef __cplusplus
}
#endif

View File

@ -1,4 +1,4 @@
bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time,flash 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 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 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, module,config,
1 bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time,flash bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time
2 component,ahrs,capacity,cmd,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,oid,lcd_driver 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, module,config,

View File

@ -240,22 +240,6 @@ devices:
description: "lcd驱动(SPI)" description: "lcd驱动(SPI)"
dependencies: dependencies:
bsp: ["gpio", "spi"] bsp: ["gpio", "spi"]
bsp_requirements:
- type: "spi"
var_name: "BSP_SPI_LCD"
description: "用于LCD通信的SPI总线"
- type: "gpio"
var_name: "BSP_GPIO_LCD_CS"
description: "LCD片选引脚"
gpio_type: "output"
- type: "gpio"
var_name: "BSP_GPIO_LCD_DC"
description: "LCD数据/命令控制引脚"
gpio_type: "output"
- type: "gpio"
var_name: "BSP_GPIO_LCD_RST"
description: "LCD复位引脚"
gpio_type: "output"
thread_signals: [] thread_signals: []
files: files:
header: "lcd.h" header: "lcd.h"

View File

@ -13,17 +13,10 @@
/* Exported variables ------------------------------------------------------- */ /* Exported variables ------------------------------------------------------- */
/** // 机器人参数配置
* @brief
* @note
*/
Config_RobotParam_t robot_config = { Config_RobotParam_t robot_config = {
/* USER CODE BEGIN robot_config */
.example_param = 0, // 示例参数初始化
// 在此添加您的配置参数初始化
/* USER CODE END robot_config */
}; };
/* Private function prototypes ---------------------------------------------- */ /* Private function prototypes ---------------------------------------------- */

View File

@ -9,20 +9,9 @@ extern "C" {
#endif #endif
#include <stdint.h> #include <stdint.h>
#include <stdbool.h>
/**
* @brief
* @note
*/
typedef struct { typedef struct {
// 示例配置项(可根据实际需求修改或删除)
uint8_t example_param; // 示例参数
/* USER CODE BEGIN Config_RobotParam */
// 在此添加您的配置参数
/* USER CODE END Config_RobotParam */
} Config_RobotParam_t; } Config_RobotParam_t;
/* Exported functions prototypes -------------------------------------------- */ /* Exported functions prototypes -------------------------------------------- */

View File

@ -1,3 +0,0 @@
module_name,description
cmd,命令系统,用于机器人指令处理和行为控制
2_axis_gimbal,双轴云台控制模块支持pitch和yaw轴控制
1 module_name description
2 cmd 命令系统,用于机器人指令处理和行为控制
3 2_axis_gimbal 双轴云台控制模块,支持pitch和yaw轴控制

21
pngico.py Normal file
View File

@ -0,0 +1,21 @@
from PIL import Image
import os
def png_to_ico(png_path, ico_path=None, sizes=[(256,256), (128,128), (64,64), (32,32), (16,16)]):
if not os.path.isfile(png_path):
print(f"文件不存在: {png_path}")
return
if ico_path is None:
ico_path = os.path.splitext(png_path)[0] + ".ico"
img = Image.open(png_path)
img.save(ico_path, format='ICO', sizes=sizes)
print(f"已生成: {ico_path}")
if __name__ == "__main__":
# 直接写死路径
# png = r"C:\Mac\Home\Documents\R\MRobot\img\rps.png"
# ico = r"c:\Mac\Home\Documents\R\MRobot\img\M1.ico"
png = "/Users/lvzucheng/Documents/R/MRobot/rps.png"
ico = "/Users/lvzucheng/Documents/R/MRobot/rps.ico"
png_to_ico(png, ico)

BIN
手册.pages Normal file

Binary file not shown.