Compare commits

..

7 Commits

Author SHA1 Message Date
22ea6e14b3 大更新 2026-01-01 22:14:09 +08:00
572c8b61d6 修复bsp 2026-01-01 22:13:44 +08:00
f25f474ae8 实现module生成 2026-01-01 21:52:52 +08:00
def81cc760 优化下载现找github 2026-01-01 21:28:33 +08:00
562538bf57 更新串口 2026-01-01 21:23:47 +08:00
3b79dd936d 添加falsh生成 2026-01-01 20:55:25 +08:00
724848a843 添加机械工具 2026-01-01 20:24:27 +08:00
33 changed files with 2782 additions and 140 deletions

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
[Setup]
AppName=MRobot
AppVersion=1.0.8
AppVersion=1.1.0
DefaultDirName={userappdata}\MRobot
DefaultGroupName=MRobot
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.update_check_thread import UpdateCheckThread
__version__ = "1.0.6"
__version__ = "1.1.0"
class AboutInterface(QWidget):
def __init__(self, parent=None):

View File

@ -360,17 +360,76 @@ class CodeGenerateInterface(QWidget):
continue
main_title = row[0]
main_item = QTreeWidgetItem([main_title])
for sub in row[1:]:
sub_item = QTreeWidgetItem([sub])
main_item.addChild(sub_item)
# 特殊处理 module
if main_title == 'module':
# 扫描 module 目录
module_dir = CodeGenerator.get_assets_dir("User_code/module")
if os.path.exists(module_dir):
for item in os.listdir(module_dir):
item_path = os.path.join(module_dir, item)
if not os.path.isdir(item_path):
continue
if item.startswith('.') or item == 'config':
continue
# 检查是否直接包含代码
has_direct_code = any(
f.endswith(('.c', '.h'))
for f in os.listdir(item_path)
if os.path.isfile(os.path.join(item_path, f))
)
if has_direct_code:
# 直接的模块(如 cmd
sub_item = QTreeWidgetItem([item])
main_item.addChild(sub_item)
else:
# 有子类型的模块(如 gimbal
module_type_item = QTreeWidgetItem([item])
has_subtypes = False
for subitem in os.listdir(item_path):
subitem_path = os.path.join(item_path, subitem)
if not os.path.isdir(subitem_path):
continue
if subitem.startswith('.'):
continue
has_code = any(
f.endswith(('.c', '.h'))
for f in os.listdir(subitem_path)
if os.path.isfile(os.path.join(subitem_path, f))
)
if has_code:
subtype_item = QTreeWidgetItem([subitem])
module_type_item.addChild(subtype_item)
has_subtypes = True
if has_subtypes:
main_item.addChild(module_type_item)
else:
# 其他模块保持原逻辑
for sub in row[1:]:
sub_item = QTreeWidgetItem([sub])
main_item.addChild(sub_item)
self.tree.addTopLevelItem(main_item)
self.tree.repaint()
def on_tree_item_clicked(self, item, column):
if item.parent():
main_title = item.parent().text(0)
sub_title = item.text(0)
class_name = f"{main_title}_{sub_title}".replace("-", "_")
# 判断层级
if item.parent().parent():
# 三级树module/type/instance
root_title = item.parent().parent().text(0)
type_title = item.parent().text(0)
instance_title = item.text(0)
class_name = f"{root_title}_{type_title}_{instance_title}".replace("-", "_")
else:
# 二级树category/item
main_title = item.parent().text(0)
sub_title = item.text(0)
class_name = f"{main_title}_{sub_title}".replace("-", "_")
widget = self._get_or_create_page(class_name)
if widget:
self.stack.setCurrentWidget(widget)
@ -402,6 +461,18 @@ class CodeGenerateInterface(QWidget):
from app.code_page.device_interface import get_device_page
device_name = class_name[len('device_'):] # 移除 device_ 前缀
page = get_device_page(device_name, self.project_path)
elif class_name.startswith('module_'):
# Module页面
from app.code_page.module_interface import get_module_page
# 解析: module_type 或 module_type_instance
parts = class_name[len('module_'):].split('_', 1)
if len(parts) == 2:
module_type = parts[0]
instance = parts[1]
page = get_module_page(module_type, instance, self.project_path, self)
else:
module_type = parts[0]
page = get_module_page(module_type, None, self.project_path, self)
else:
print(f"未知的页面类型: {class_name}")
return None
@ -411,4 +482,6 @@ class CodeGenerateInterface(QWidget):
return page
except Exception as e:
print(f"创建页面 {class_name} 失败: {e}")
import traceback
traceback.print_exc()
return None

View File

@ -235,7 +235,15 @@ class BspPeripheralBase(QWidget):
return True
def _generate_source_file(self, configs, template_dir):
template_path = os.path.join(template_dir, self.template_names['source'])
# 从子文件夹加载模板与_generate_header_file保持一致
periph_folder = self.peripheral_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, periph_folder, self.template_names['source'])
if not os.path.exists(template_path):
# 如果子文件夹不存在,尝试从根目录加载(向后兼容)
template_path = os.path.join(template_base_dir, self.template_names['source'])
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
@ -398,7 +406,15 @@ class bsp_can(BspPeripheralBase):
)
def _generate_source_file(self, configs, template_dir):
template_path = os.path.join(template_dir, self.template_names['source'])
# 从子文件夹加载模板与_generate_header_file保持一致
periph_folder = self.peripheral_name.lower()
template_base_dir = CodeGenerator.get_assets_dir("User_code/bsp")
template_path = os.path.join(template_base_dir, periph_folder, self.template_names['source'])
if not os.path.exists(template_path):
# 如果子文件夹不存在,尝试从根目录加载(向后兼容)
template_path = os.path.join(template_base_dir, self.template_names['source'])
template_content = CodeGenerator.load_template(template_path)
if not template_content:
return False
@ -1048,7 +1064,14 @@ class bsp_gpio(QWidget):
return True
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)
if not template_content:
return False
@ -1067,7 +1090,14 @@ class bsp_gpio(QWidget):
return True
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)
if not template_content:
return False
@ -1305,7 +1335,14 @@ class bsp_pwm(QWidget):
return True
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)
if not template_content:
return False
@ -1324,7 +1361,14 @@ class bsp_pwm(QWidget):
return True
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)
if not template_content:
return False
@ -1376,7 +1420,225 @@ class bsp_pwm(QWidget):
if name_widget:
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):
"""根据外设名返回对应的页面类没有特殊类则返回默认BspSimplePeripheral"""
name_lower = peripheral_name.lower()
@ -1387,7 +1649,8 @@ def get_bsp_page(peripheral_name, project_path):
"spi": bsp_spi,
"uart": bsp_uart,
"gpio": bsp_gpio,
"pwm": bsp_pwm, # 添加PWM
"pwm": bsp_pwm,
"flash": bsp_flash, # 添加Flash自动配置
# 以后可以继续添加特殊外设
}
if name_lower in special_classes:

View File

@ -243,8 +243,13 @@ class DeviceSimple(QWidget):
# 使用设备名称作为子文件夹名(小写)
device_folder = self.device_name.lower()
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', {})
# 收集需要替换BSP配置的文件列表
files_to_process = list(files.values())
# 处理配置中定义的主要文件需要BSP替换
for file_type, filename in files.items():
# 先尝试从子文件夹加载
src_path = os.path.join(template_base_dir, device_folder, filename)
@ -273,6 +278,25 @@ class DeviceSimple(QWidget):
with open(dst_path, 'w', encoding='utf-8') as f:
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()
return True

View File

@ -0,0 +1,255 @@
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,25 +1,21 @@
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QTableWidgetItem, QApplication
from PyQt5.QtCore import Qt
from qfluentwidgets import TitleLabel, BodyLabel, TableWidget, PushButton, SubtitleLabel, SpinBox, ComboBox, InfoBar,InfoBarPosition, FluentIcon
from openpyxl import load_workbook, Workbook
import pyqtgraph as pg
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
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
# 延迟导入:这些库只在需要时才导入,加快应用启动速度
# import numpy as np
# from openpyxl import load_workbook, Workbook
class FunctionFitInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# 延迟导入标志
self._libs_loaded = False
self.setObjectName("functionFitInterface")
main_layout = QHBoxLayout(self)
main_layout.setSpacing(24)
@ -67,9 +63,11 @@ class FunctionFitInterface(QWidget):
right_layout.setSpacing(12)
right_layout.addWidget(SubtitleLabel("函数图像预览"))
self.figure = Figure(figsize=(5, 4))
self.canvas = FigureCanvas(self.figure)
right_layout.addWidget(self.canvas, stretch=1)
# 占位符实际的canvas会在_load_heavy_libraries中创建
self.canvas_placeholder = QWidget()
self.canvas_layout = QVBoxLayout(self.canvas_placeholder)
self.canvas_layout.setContentsMargins(0, 0, 0, 0)
right_layout.addWidget(self.canvas_placeholder, stretch=1)
self.resultLabel = BodyLabel("")
self.resultLabel.setWordWrap(True) # 自动换行
@ -110,12 +108,29 @@ class FunctionFitInterface(QWidget):
main_layout.addLayout(right_layout, 2)
# 默认显示空图像
self.figure.clear()
ax = self.figure.add_subplot(111)
ax.set_xlabel('x')
ax.set_ylabel('y')
self.canvas.draw()
def _load_heavy_libraries(self):
"""延迟加载大型库,提高应用启动速度"""
if self._libs_loaded:
return
global np, load_workbook, Workbook
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):
row = self.dataTable.rowCount()
@ -132,6 +147,7 @@ class FunctionFitInterface(QWidget):
self.dataTable.removeRow(row)
def import_excel(self):
self._load_heavy_libraries() # 延迟加载库
path, _ = QFileDialog.getOpenFileName(self, "导入 Excel", "", "Excel Files (*.xlsx)")
if path:
wb = load_workbook(path)
@ -146,6 +162,7 @@ class FunctionFitInterface(QWidget):
def export_excel(self):
self._load_heavy_libraries() # 延迟加载库
path, _ = QFileDialog.getSaveFileName(self, "导出 Excel", "", "Excel Files (*.xlsx)")
if path:
data = self.parse_data()
@ -174,6 +191,7 @@ class FunctionFitInterface(QWidget):
return data if data else None
def fit_and_plot(self):
self._load_heavy_libraries() # 延迟加载库
data = self.parse_data()
if not data:
self.resultLabel.setText("数据格式错误或为空")
@ -189,15 +207,29 @@ class FunctionFitInterface(QWidget):
x_fit = np.linspace(x.min(), x.max(), 100)
y_fit = np.polyval(coeffs, x_fit)
self.figure.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')
ax.set_title('graph of a function')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend()
self.canvas.draw()
# 清空并重新绘图
self.plot_widget.clear()
# 绘制原始数据点(蓝色散点)
scatter = pg.ScatterPlotItem(
x=x, y=y,
pen=None,
brush=pg.mkBrush(0, 0, 255, 120), # 蓝色半透明
size=10,
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)
self.resultLabel.setText(f"拟合公式: {formula}")

View File

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

View File

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

View File

@ -1,35 +1,79 @@
import serial
import serial.tools.list_ports
from PyQt5.QtCore import Qt, QThread, pyqtSignal
import pyqtgraph as pg
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.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy, QStackedWidget
from PyQt5.QtWidgets import QWidget
from qfluentwidgets import (
FluentIcon, PushButton, ComboBox, TextEdit, LineEdit, CheckBox,
SubtitleLabel, BodyLabel, HorizontalSeparator
SubtitleLabel, BodyLabel, HorizontalSeparator, PrimaryPushButton,
isDarkTheme, qconfig, CardWidget, StrongBodyLabel, CaptionLabel
)
class SerialReadThread(QThread):
data_received = pyqtSignal(str)
raw_data_received = pyqtSignal(bytes)
def __init__(self, ser):
def __init__(self, ser, parent_widget=None):
super().__init__()
self.ser = ser
self.parent_widget = parent_widget
self._running = True
self.buffer = bytearray()
self.batch_size = 8192
def run(self):
while self._running:
if self.ser and self.ser.is_open and self.ser.in_waiting:
if self.ser and self.ser.is_open:
try:
data = self.ser.readline().decode(errors='ignore')
self.data_received.emit(data)
except Exception:
pass
if self.ser.in_waiting:
bytes_to_read = min(self.ser.in_waiting, self.batch_size)
raw_data = self.ser.read(bytes_to_read)
if raw_data:
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):
self._running = False
self.wait()
class SerialTerminalInterface(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
@ -37,56 +81,166 @@ class SerialTerminalInterface(QWidget):
main_layout = QVBoxLayout(self)
main_layout.setSpacing(12)
# 顶部:串口设置区
top_hbox = QHBoxLayout()
top_hbox.addWidget(BodyLabel("串口:"))
# 顶部:基本设置行(始终显示)
basic_layout = QHBoxLayout()
basic_layout.addWidget(BodyLabel("串口:"))
self.port_combo = ComboBox()
self.refresh_ports()
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)
basic_layout.addWidget(self.port_combo)
self.refresh_btn = PushButton(FluentIcon.SYNC, "刷新")
self.refresh_btn.clicked.connect(self.refresh_ports)
top_hbox.addWidget(self.refresh_btn)
top_hbox.addStretch()
main_layout.addLayout(top_hbox)
basic_layout.addWidget(self.refresh_btn)
basic_layout.addWidget(BodyLabel("波特率:"))
self.baud_combo = ComboBox()
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())
# 中部:左侧预设命令,右侧显示区
# 初始化状态变量
self.ser = None
self.read_thread = None
self.is_chart_mode = False # 默认使用文本模式
self.is_paused = False
# 中部:左侧快捷命令,右侧显示区
center_hbox = QHBoxLayout()
# 左侧:预设命令竖排
preset_vbox = QVBoxLayout()
# 左侧:快捷命令区域
preset_widget = QWidget()
preset_widget.setFixedWidth(250)
preset_vbox = QVBoxLayout(preset_widget)
preset_vbox.addWidget(SubtitleLabel("快捷指令"))
preset_vbox.setAlignment(Qt.AlignmentFlag.AlignCenter)
# 预设命令配置
self.preset_commands = [
("线程监视器", "htop"),
("陀螺仪校准", "cali_gyro"),
("性能监视", "htop"),
("陀螺仪校准", "cali_gyro"),
("重启", "reset"),
("显示所有设备", "ls /dev"),
("查询id", "id"),
]
for label, cmd in self.preset_commands:
btn = PushButton(label)
btn = PushButton(FluentIcon.SEND, label)
btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
btn.clicked.connect(lambda _, c=cmd: self.send_preset_command(c))
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()
main_layout.addLayout(center_hbox, stretch=1)
# 右侧:串口数据显示区
center_hbox.addWidget(preset_widget)
# 右侧:显示区域
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.setReadOnly(True)
self.text_edit.setMinimumWidth(400)
center_hbox.addWidget(self.text_edit, 3)
center_hbox.addLayout(preset_vbox, 1)
self.display_stack.addWidget(self.text_edit)
# 波形图显示页面
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())
# 底部:输入区
@ -95,73 +249,542 @@ class SerialTerminalInterface(QWidget):
self.input_line.setPlaceholderText("输入内容,回车发送")
self.input_line.returnPressed.connect(self.send_data)
bottom_hbox.addWidget(self.input_line, 4)
send_btn = PushButton("发送")
send_btn = PushButton(FluentIcon.SEND, "发送")
send_btn.clicked.connect(self.send_data)
bottom_hbox.addWidget(send_btn, 1)
self.auto_enter_checkbox = CheckBox("自动回车 ")
self.auto_enter_checkbox.setChecked(True)
bottom_hbox.addWidget(self.auto_enter_checkbox)
self.hex_send_checkbox = CheckBox("HEX发送")
self.hex_send_checkbox.setChecked(False)
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()
main_layout.addLayout(bottom_hbox)
self.ser = None
self.read_thread = None
# 数据解析相关
self.data_buffer = bytearray()
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):
"""发送预设命令"""
self.input_line.setText(cmd)
self.send_data()
def refresh_ports(self):
"""刷新串口列表"""
self.port_combo.clear()
ports = serial.tools.list_ports.comports()
for port in ports:
self.port_combo.addItem(port.device)
def toggle_connection(self):
"""切换连接状态"""
if self.ser and self.ser.is_open:
self.disconnect_serial()
else:
self.connect_serial()
def connect_serial(self):
"""连接串口"""
port = self.port_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:
self.ser = serial.Serial(port, baud, timeout=0.1)
self.connect_btn.setText("断开")
self.text_edit.append(f"已连接到 {port} @ {baud}")
self.read_thread = SerialReadThread(self.ser)
self.ser = serial.Serial(
port=port,
baudrate=baud,
bytesize=data_bits,
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.raw_data_received.connect(self.process_raw_data)
self.read_thread.start()
if self.is_chart_mode:
self.chart_timer.start()
except Exception as e:
self.text_edit.append(f"连接失败: {e}")
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}连接失败: {e}")
def disconnect_serial(self):
"""断开串口"""
self.chart_timer.stop()
self.auto_send_timer.stop() # 停止自动发送
if self.read_thread:
self.read_thread.stop()
self.read_thread = None
if self.ser:
self.ser.close()
self.ser = None
self.connect_btn.setText("连接")
self.text_edit.append("已断开连接")
self.connect_btn.setText("连接串口")
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}已断开连接")
def display_data(self, data):
"""显示接收数据"""
if self.is_paused:
return
self.text_edit.moveCursor(QTextCursor.End)
self.text_edit.insertPlainText(data)
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):
"""发送数据"""
if self.ser and self.ser.is_open:
text = self.input_line.text()
try:
if not text:
self.ser.write('\n'.encode())
if self.hex_send_checkbox.isChecked():
hex_data = self.parse_hex_string(text)
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:
for char in text:
self.ser.write(char.encode())
if self.auto_enter_checkbox.isChecked():
self.ser.write('\n'.encode())
data_to_send = text
line_ending = self.get_line_ending()
if line_ending:
data_to_send += line_ending
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:
self.text_edit.append(f"发送失败: {e}")
self.input_line.clear()
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
self.text_edit.append(f"{timestamp}发送失败: {e}")
# 只有在非自动发送模式下才清空输入框
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,4 +341,242 @@ class analyzing_ioc:
'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,7 +8,9 @@ import tempfile
import time
def update_code(parent=None, info_callback=None, error_callback=None):
url = "http://gitea.qutrobot.top/robofish/MRobot/archive/User_code.zip"
# 优先使用 GitHub备用 Gitea
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 以使用统一的路径获取逻辑
try:
@ -53,10 +55,39 @@ def update_code(parent=None, info_callback=None, error_callback=None):
local_dir = os.path.join(assets_dir, "User_code")
print(f"更新代码:最终目标目录: {local_dir}")
# 尝试从 GitHub 下载,失败则使用 Gitea
download_successful = False
resp = None
# 首先尝试 GitHub
try:
# 下载远程代码库
resp = requests.get(url, timeout=30)
print("尝试从 GitHub 下载代码...")
resp = requests.get(github_url, timeout=15)
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:

Binary file not shown.

View File

@ -9,4 +9,4 @@ time,获取时间戳函数需要开启freerots
dwt,需要开启dwt获取时间
i2c,请开启i2c的dma和中断
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 flash 自动识别MCU型号并配置Flash,支持STM32F1(Page)/F4(Sector)/H7(Sector),自动处理单Bank/双Bank配置

View File

@ -0,0 +1,77 @@
# 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

@ -0,0 +1,346 @@
# 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

@ -0,0 +1,55 @@
/* 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

@ -0,0 +1,31 @@
#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
bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time,flash
component,ahrs,capacity,cmd,crc8,crc16,error_detect,filter,FreeRTOS_CLI,limiter,mixer,pid,ui,user_math
device,dr16,bmi088,ist8310,motor,motor_rm,motor_dm,motor_vesc,motor_lk,motor_lz,motor_odrive,dm_imu,rc_can,servo,buzzer,led,ws2812,vofa,ops9,oid,lcd_driver
module,config,
module,
1 bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time bsp,can,fdcan,dwt,gpio,i2c,mm,spi,uart,pwm,time,flash
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,config, module,

View File

@ -240,6 +240,22 @@ devices:
description: "lcd驱动(SPI)"
dependencies:
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: []
files:
header: "lcd.h"

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
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)

Binary file not shown.