mirror of
https://github.com/goldenfishs/MRobot.git
synced 2026-02-04 18:00:19 +08:00
Compare commits
7 Commits
d214abb584
...
22ea6e14b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 22ea6e14b3 | |||
| 572c8b61d6 | |||
| f25f474ae8 | |||
| def81cc760 | |||
| 562538bf57 | |||
| 3b79dd936d | |||
| 724848a843 |
Binary file not shown.
BIN
MROBOT.docx
BIN
MROBOT.docx
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
[Setup]
|
[Setup]
|
||||||
AppName=MRobot
|
AppName=MRobot
|
||||||
AppVersion=1.0.8
|
AppVersion=1.1.0
|
||||||
DefaultDirName={userappdata}\MRobot
|
DefaultDirName={userappdata}\MRobot
|
||||||
DefaultGroupName=MRobot
|
DefaultGroupName=MRobot
|
||||||
OutputDir=.
|
OutputDir=.
|
||||||
|
|||||||
@ -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.0.6"
|
__version__ = "1.1.0"
|
||||||
|
|
||||||
class AboutInterface(QWidget):
|
class AboutInterface(QWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
|
|||||||
@ -360,17 +360,76 @@ 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:]:
|
|
||||||
sub_item = QTreeWidgetItem([sub])
|
# 特殊处理 module
|
||||||
main_item.addChild(sub_item)
|
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.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)
|
# 判断层级
|
||||||
sub_title = item.text(0)
|
if item.parent().parent():
|
||||||
class_name = f"{main_title}_{sub_title}".replace("-", "_")
|
# 三级树(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)
|
widget = self._get_or_create_page(class_name)
|
||||||
if widget:
|
if widget:
|
||||||
self.stack.setCurrentWidget(widget)
|
self.stack.setCurrentWidget(widget)
|
||||||
@ -402,6 +461,18 @@ 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
|
||||||
@ -411,4 +482,6 @@ 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
|
||||||
@ -235,7 +235,15 @@ class BspPeripheralBase(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, 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)
|
template_content = CodeGenerator.load_template(template_path)
|
||||||
if not template_content:
|
if not template_content:
|
||||||
return False
|
return False
|
||||||
@ -398,7 +406,15 @@ class bsp_can(BspPeripheralBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _generate_source_file(self, configs, template_dir):
|
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)
|
template_content = CodeGenerator.load_template(template_path)
|
||||||
if not template_content:
|
if not template_content:
|
||||||
return False
|
return False
|
||||||
@ -1048,7 +1064,14 @@ 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
|
||||||
@ -1067,7 +1090,14 @@ 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
|
||||||
@ -1305,7 +1335,14 @@ 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
|
||||||
@ -1324,7 +1361,14 @@ 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
|
||||||
@ -1376,7 +1420,225 @@ 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()
|
||||||
@ -1387,7 +1649,8 @@ 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
|
"pwm": bsp_pwm,
|
||||||
|
"flash": bsp_flash, # 添加Flash自动配置
|
||||||
# 以后可以继续添加特殊外设
|
# 以后可以继续添加特殊外设
|
||||||
}
|
}
|
||||||
if name_lower in special_classes:
|
if name_lower in special_classes:
|
||||||
|
|||||||
@ -243,8 +243,13 @@ 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)
|
||||||
@ -273,6 +278,25 @@ 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
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
@ -1,25 +1,21 @@
|
|||||||
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
|
||||||
from openpyxl import load_workbook, Workbook
|
import pyqtgraph as pg
|
||||||
|
|
||||||
import numpy as np
|
# 延迟导入:这些库只在需要时才导入,加快应用启动速度
|
||||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
# import numpy as np
|
||||||
from matplotlib.figure import Figure
|
# from openpyxl import load_workbook, Workbook
|
||||||
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)
|
||||||
|
|
||||||
@ -67,9 +63,11 @@ class FunctionFitInterface(QWidget):
|
|||||||
right_layout.setSpacing(12)
|
right_layout.setSpacing(12)
|
||||||
right_layout.addWidget(SubtitleLabel("函数图像预览"))
|
right_layout.addWidget(SubtitleLabel("函数图像预览"))
|
||||||
|
|
||||||
self.figure = Figure(figsize=(5, 4))
|
# 占位符,实际的canvas会在_load_heavy_libraries中创建
|
||||||
self.canvas = FigureCanvas(self.figure)
|
self.canvas_placeholder = QWidget()
|
||||||
right_layout.addWidget(self.canvas, stretch=1)
|
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 = BodyLabel("")
|
||||||
self.resultLabel.setWordWrap(True) # 自动换行
|
self.resultLabel.setWordWrap(True) # 自动换行
|
||||||
@ -110,12 +108,29 @@ class FunctionFitInterface(QWidget):
|
|||||||
|
|
||||||
main_layout.addLayout(right_layout, 2)
|
main_layout.addLayout(right_layout, 2)
|
||||||
|
|
||||||
# 默认显示空图像
|
def _load_heavy_libraries(self):
|
||||||
self.figure.clear()
|
"""延迟加载大型库,提高应用启动速度"""
|
||||||
ax = self.figure.add_subplot(111)
|
if self._libs_loaded:
|
||||||
ax.set_xlabel('x')
|
return
|
||||||
ax.set_ylabel('y')
|
|
||||||
self.canvas.draw()
|
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):
|
def add_row(self):
|
||||||
row = self.dataTable.rowCount()
|
row = self.dataTable.rowCount()
|
||||||
@ -132,6 +147,7 @@ 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)
|
||||||
@ -146,6 +162,7 @@ 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()
|
||||||
@ -174,6 +191,7 @@ 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("数据格式错误或为空")
|
||||||
@ -189,15 +207,29 @@ 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()
|
# 清空并重新绘图
|
||||||
ax = self.figure.add_subplot(111)
|
self.plot_widget.clear()
|
||||||
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')
|
scatter = pg.ScatterPlotItem(
|
||||||
ax.set_xlabel('x')
|
x=x, y=y,
|
||||||
ax.set_ylabel('y')
|
pen=None,
|
||||||
ax.legend()
|
brush=pg.mkBrush(0, 0, 255, 120), # 蓝色半透明
|
||||||
self.canvas.draw()
|
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)
|
formula = self.poly_formula(coeffs)
|
||||||
self.resultLabel.setText(f"拟合公式: {formula}")
|
self.resultLabel.setText(f"拟合公式: {formula}")
|
||||||
|
|||||||
@ -15,6 +15,7 @@ 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
|
||||||
|
|
||||||
@ -54,6 +55,7 @@ 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):
|
||||||
@ -62,7 +64,8 @@ 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.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(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)
|
||||||
|
|
||||||
|
|||||||
564
app/mech_design_interface.py
Normal file
564
app/mech_design_interface.py
Normal 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")
|
||||||
@ -2,13 +2,18 @@ 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) # 设置外边距
|
||||||
@ -88,9 +93,15 @@ 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)
|
|
||||||
self.addSubInterface(fit_page, "fitPage", "曲线拟合")
|
# 延迟导入和创建FunctionFitInterface
|
||||||
self.stackedWidget.setCurrentWidget(fit_page)
|
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")
|
self.tabBar.setCurrentTab("fitPage")
|
||||||
|
|
||||||
def open_ai_tab(self):
|
def open_ai_tab(self):
|
||||||
|
|||||||
@ -1,35 +1,79 @@
|
|||||||
import serial
|
import serial
|
||||||
import serial.tools.list_ports
|
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.QtGui import QTextCursor
|
||||||
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy
|
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QSizePolicy, QStackedWidget
|
||||||
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
|
SubtitleLabel, BodyLabel, HorizontalSeparator, PrimaryPushButton,
|
||||||
|
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):
|
def __init__(self, ser, parent_widget=None):
|
||||||
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 and self.ser.in_waiting:
|
if self.ser and self.ser.is_open:
|
||||||
try:
|
try:
|
||||||
data = self.ser.readline().decode(errors='ignore')
|
if self.ser.in_waiting:
|
||||||
self.data_received.emit(data)
|
bytes_to_read = min(self.ser.in_waiting, self.batch_size)
|
||||||
except Exception:
|
raw_data = self.ser.read(bytes_to_read)
|
||||||
pass
|
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):
|
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)
|
||||||
@ -37,56 +81,166 @@ class SerialTerminalInterface(QWidget):
|
|||||||
main_layout = QVBoxLayout(self)
|
main_layout = QVBoxLayout(self)
|
||||||
main_layout.setSpacing(12)
|
main_layout.setSpacing(12)
|
||||||
|
|
||||||
# 顶部:串口设置区
|
# 顶部:基本设置行(始终显示)
|
||||||
top_hbox = QHBoxLayout()
|
basic_layout = QHBoxLayout()
|
||||||
top_hbox.addWidget(BodyLabel("串口:"))
|
basic_layout.addWidget(BodyLabel("串口:"))
|
||||||
self.port_combo = ComboBox()
|
self.port_combo = ComboBox()
|
||||||
self.refresh_ports()
|
self.refresh_ports()
|
||||||
top_hbox.addWidget(self.port_combo)
|
basic_layout.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)
|
||||||
top_hbox.addWidget(self.refresh_btn)
|
basic_layout.addWidget(self.refresh_btn)
|
||||||
top_hbox.addStretch()
|
basic_layout.addWidget(BodyLabel("波特率:"))
|
||||||
main_layout.addLayout(top_hbox)
|
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())
|
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"),
|
|
||||||
("陀螺仪校准", "cali_gyro"),
|
|
||||||
("性能监视", "htop"),
|
("性能监视", "htop"),
|
||||||
|
("陀螺仪校准", "cali_gyro"),
|
||||||
("重启", "reset"),
|
("重启", "reset"),
|
||||||
("显示所有设备", "ls /dev"),
|
|
||||||
("查询id", "id"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for label, cmd in self.preset_commands:
|
for label, cmd in self.preset_commands:
|
||||||
btn = PushButton(label)
|
btn = PushButton(FluentIcon.SEND, 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()
|
||||||
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 = TextEdit()
|
||||||
self.text_edit.setReadOnly(True)
|
self.text_edit.setReadOnly(True)
|
||||||
self.text_edit.setMinimumWidth(400)
|
self.text_edit.setMinimumWidth(400)
|
||||||
center_hbox.addWidget(self.text_edit, 3)
|
self.display_stack.addWidget(self.text_edit)
|
||||||
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())
|
||||||
|
|
||||||
# 底部:输入区
|
# 底部:输入区
|
||||||
@ -95,73 +249,542 @@ 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("发送")
|
send_btn = PushButton(FluentIcon.SEND, "发送")
|
||||||
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.auto_enter_checkbox.setChecked(True)
|
self.hex_send_checkbox = CheckBox("HEX发送")
|
||||||
bottom_hbox.addWidget(self.auto_enter_checkbox)
|
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()
|
bottom_hbox.addStretch()
|
||||||
main_layout.addLayout(bottom_hbox)
|
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):
|
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(port, baud, timeout=0.1)
|
self.ser = serial.Serial(
|
||||||
self.connect_btn.setText("断开")
|
port=port,
|
||||||
self.text_edit.append(f"已连接到 {port} @ {baud}")
|
baudrate=baud,
|
||||||
self.read_thread = SerialReadThread(self.ser)
|
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.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:
|
||||||
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):
|
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("连接串口")
|
||||||
self.text_edit.append("已断开连接")
|
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
|
||||||
|
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 not text:
|
if self.hex_send_checkbox.isChecked():
|
||||||
self.ser.write('\n'.encode())
|
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:
|
else:
|
||||||
for char in text:
|
data_to_send = text
|
||||||
self.ser.write(char.encode())
|
line_ending = self.get_line_ending()
|
||||||
if self.auto_enter_checkbox.isChecked():
|
if line_ending:
|
||||||
self.ser.write('\n'.encode())
|
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:
|
except Exception as e:
|
||||||
self.text_edit.append(f"发送失败: {e}")
|
timestamp = datetime.now().strftime("[%H:%M:%S.%f")[:-3] + "] "
|
||||||
self.input_line.clear()
|
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()
|
||||||
@ -341,4 +341,242 @@ 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
|
||||||
@ -8,7 +8,9 @@ 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):
|
||||||
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 以使用统一的路径获取逻辑
|
# 导入 CodeGenerator 以使用统一的路径获取逻辑
|
||||||
try:
|
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")
|
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(url, timeout=30)
|
resp = requests.get(github_url, timeout=15)
|
||||||
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:
|
||||||
|
|||||||
BIN
assets/User_code/bsp/.DS_Store
vendored
BIN
assets/User_code/bsp/.DS_Store
vendored
Binary file not shown.
@ -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配置
|
||||||
|
|||||||
|
77
assets/User_code/bsp/flash/CHANGELOG.md
Normal file
77
assets/User_code/bsp/flash/CHANGELOG.md
Normal 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(擦除、读、写)
|
||||||
346
assets/User_code/bsp/flash/README.md
Normal file
346
assets/User_code/bsp/flash/README.md
Normal 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模式
|
||||||
|
- 不同大小的Sector(16KB/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()` 函数中添加相应的配置规则。
|
||||||
55
assets/User_code/bsp/flash/flash.c
Normal file
55
assets/User_code/bsp/flash/flash.c
Normal 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, §or_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);
|
||||||
|
}
|
||||||
31
assets/User_code/bsp/flash/flash.h
Normal file
31
assets/User_code/bsp/flash/flash.h
Normal 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
|
||||||
@ -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
|
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,config,
|
module,
|
||||||
|
@ -240,6 +240,22 @@ 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"
|
||||||
|
|||||||
@ -13,10 +13,17 @@
|
|||||||
|
|
||||||
/* 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 ---------------------------------------------- */
|
||||||
|
|||||||
@ -9,9 +9,20 @@ 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 -------------------------------------------- */
|
||||||
|
|||||||
3
assets/User_code/module/describe.csv
Normal file
3
assets/User_code/module/describe.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module_name,description
|
||||||
|
cmd,命令系统,用于机器人指令处理和行为控制
|
||||||
|
2_axis_gimbal,双轴云台控制模块,支持pitch和yaw轴控制
|
||||||
|
21
pngico.py
21
pngico.py
@ -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)
|
|
||||||
Loading…
Reference in New Issue
Block a user