Files
chuan 652d5d0dc1 feat!: 接入可选送丝机与双型号焊枪仿真
- 增加送丝机及两种焊枪的启动选择,未启用时保留原有简化外设
- 实现外设串口协议、工艺缓存与导入、参数保存、报警输入和独立供电
- 提供外设 WebSocket 状态及控制接口,按连接管理按钮持有和碰撞故障释放
- 添加外设卡片、静态光斑、监控输入与故障操作,统一布局并保存展开状态
- 添加外设协议与固件联动用例,以及点射收尾期间再次出光的回执回归用例
- 整理协议边界及测试条目,记录旧款焊枪驱动和电源报警未被固件转发的现状
- BREAKING CHANGE: 自定义平台实例化 BoardWebSocketServer 时需提供 ExternalDevices 依赖
2026-10-23 14:08:01 +08:00

206 lines
9.5 KiB
Python

"""解析项目路径与固件配置,检查本机运行依赖并适配平台文件中的绝对路径"""
import json
import os
from pathlib import Path
import re
import shutil
import socket
import subprocess
import sys
from scripts.settings import get_setting, set_setting, as_port
ROOT = Path(__file__).resolve().parents[1]
RUNTIME = ROOT / '.runtime'
# 运行时由 Renode 编译加载的模型必须完整存在,不能只检查入口脚本
MODEL_FILES = (
'peripherals/External/ModbusDevice.cs',
'peripherals/External/FeederDevice.cs',
'peripherals/External/TorchDevice.cs',
'peripherals/External/TorchDevice.Registers.cs',
'peripherals/External/ExternalDevices.cs',
'peripherals/Board/BoardWebSocketServer.Devices.cs',
'peripherals/Board/BoardWebSocketServer.cs',
'peripherals/Board/BoardWebSocketServer.Waveform.cs',
'peripherals/Board/BoardWebSocketServer.Inputs.cs',
'peripherals/Board/BoardWebSocketServer.Outputs.cs',
'peripherals/Board/BoardWebSocketServer.Laser.cs',
'peripherals/Board/BoardWebSocketServer.Feeder.cs',
'peripherals/Board/WireFeeder.cs',
'peripherals/MTD/STM32H7_FlashController_ClearFlags.cs',
'peripherals/IRQControllers/STM32H7_NVIC.cs',
'peripherals/Miscellaneous/STM32H7_RCC_Clocking.cs',
'peripherals/Miscellaneous/STM32H7_RCC_Clocking.Registers.cs',
'peripherals/Miscellaneous/STM32H7_RCC_Clocking.Clocks.cs',
'peripherals/Miscellaneous/STM32H7_DBGMCU.cs',
'peripherals/IRQControllers/STM32H7_EXTI_Events.cs',
'peripherals/Timers/STM32_Timer_Dma.cs',
'peripherals/Timers/STM32_Timer_Dma.Registers.cs',
'peripherals/Timers/STM32_Timer_Dma.Events.cs',
'peripherals/Timers/STM32_Timer_Dma.State.cs',
'peripherals/Timers/STM32_Timer_Dma.Channel.cs',
'peripherals/Timers/STM32_Timer_Dma.Trigger.cs',
'peripherals/Analog/STM32_ADC_Common_OneShot.cs',
'peripherals/Analog/STM32_ADC_Common_OneShot.Timing.cs',
'peripherals/Analog/STM32_ADC_Common_OneShot.Conversion.cs',
'peripherals/Analog/STM32_ADC_Common_OneShot.Registers.cs',
'peripherals/Analog/STM32_ADC_Common_OneShot.WatchdogRegisters.cs',
'peripherals/Analog/STM32_ADC_Common_OneShot.SequenceRegisters.cs',
'peripherals/Analog/STM32_ADC_Common_OneShot.State.cs',
'peripherals/Analog/STM32H7_ADC_OneShot.cs',
'peripherals/UART/STM32F7_USART_Idle.cs',
'peripherals/UART/STM32F7_USART_Idle.Registers.cs',
'peripherals/UART/STM32F7_USART_Idle.State.cs',
'peripherals/UART/BoardSerialBridge.cs',
'peripherals/DMA/STM32DMA_Rearm.cs',
'peripherals/DMA/STM32DMA_Rearm.Registers.cs',
'peripherals/DMA/STM32DMA_Rearm.Stream.Registers.cs',
'peripherals/DMA/STM32DMA_Rearm.Stream.cs',
'peripherals/Analog/STM32H7_DAC_Basic.cs',
)
FILES = ('platforms/motion_controller.resc', 'platforms/stm32h723.repl', 'peripherals/load_models.resc', 'peripherals/SPI/w25q16.py') + MODEL_FILES
TASK_FILES = ('justfile', 'scripts/bootstrap.ps1', 'scripts/virtual_com.py',
'scripts/virtual_com.ps1', 'scripts/controller.py', 'scripts/settings.py',
'scripts/renode_monitor.py', 'scripts/environment.py', 'scripts/flash_images.py',
'scripts/renode_process.py', 'scripts/simulation.py', 'scripts/external_devices.py')
FLASH_PATH = re.compile(r'(?m)^(\s*filename:\s*)"[^"\r\n]*w25q16\.py"[ \t]*$')
def setting(name, default):
return get_setting(ROOT, name, default)
def configured_path(name, default):
"""展开用户目录,并将配置中的相对路径统一解释为相对于项目根目录"""
return (ROOT / Path(setting(name, default)).expanduser()).resolve()
def selected_elf():
return configured_path('H723_ELF', '.runtime/firmware/MotionController6X_VS_Factory_DBG.elf')
def set_elf(filename):
"""校验 ELF 文件头并保存固件选择,项目内路径使用可迁移的相对路径"""
path = Path(filename).expanduser().resolve()
with path.open('rb') as firmware:
if firmware.read(4) != b'\x7fELF':
raise ValueError(f'Not an ELF file: {path}')
if any(character in str(path) for character in '\r\n\t'):
raise ValueError('Firmware paths cannot contain tabs or newlines')
try:
value = path.relative_to(ROOT).as_posix()
except ValueError:
value = path.as_posix()
set_setting(ROOT, 'H723_ELF', value)
print(f'Selected ELF: {path}\nSaved H723_ELF in .env; process environment takes precedence.')
def renode_executable():
"""优先使用显式配置,未配置时依次查找 PATH 和标准安装目录"""
override = setting('RENODE_EXE', '')
if override:
path = (ROOT / Path(override).expanduser()).resolve()
return path if path.is_file() else None
candidates = [shutil.which('renode.exe'),
str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Renode/renode.exe')]
return next((Path(p) for p in candidates if p and Path(p).is_file()), None)
def format_paths():
"""仅更新 W25Q16 模型路径,匹配数量异常时拒绝改写平台文件"""
path = ROOT / 'platforms/stm32h723.repl'
source = path.read_text(encoding='utf-8-sig')
target = (ROOT / 'peripherals/SPI/w25q16.py').as_posix()
updated, count = FLASH_PATH.subn(lambda match: match[1] + json.dumps(target, ensure_ascii=False), source)
if count != 1:
raise RuntimeError(f'Expected one Flash filename in {path}, found {count}')
if updated != source:
path.write_text(updated, encoding='utf-8')
print(f'Updated Flash path: {target}', flush=True)
else:
print(f'Paths already match: {ROOT}', flush=True)
def listening(port):
with socket.socket() as probe:
probe.settimeout(.2)
return probe.connect_ex(('127.0.0.1', port)) == 0
def free_port():
"""取得当前可用的回环端口,返回后释放预留,调用方仍需处理绑定冲突"""
with socket.socket() as reservation:
reservation.bind(('127.0.0.1', 0))
return reservation.getsockname()[1]
def check(usb=False):
"""报告依赖缺失与端口占用,不启动仿真或清理现有实例"""
missing = []
print(f'[OK] Python {sys.version.split()[0]}: {sys.executable}')
print('[OK] No pip packages or separate C# build tools are needed')
just = shutil.which('just')
print(f'[OK] just: {just}' if just else '[MISSING] just; install: winget install --id Casey.Just --exact')
if not just:
missing.append('just')
renode = renode_executable()
if renode:
version = subprocess.run([str(renode), '--version'], capture_output=True, text=True,
errors='replace', timeout=15)
if version.returncode == 0:
print(f'[OK] {version.stdout.strip()}\n {renode}')
else:
missing.append('working Renode')
print(f'[MISSING] Renode cannot run: {version.stderr or version.stdout}')
print(' Reinstall the Windows package from https://builds.renode.io/')
else:
missing.append('Renode')
print('[MISSING] Renode: install the Windows package from https://builds.renode.io/')
print(' Tested: 1.17.0+20260906gitf1dd1b4af; set RENODE_EXE for a custom install')
for filename in FILES + TASK_FILES + (str(selected_elf()),):
path = ROOT / filename
if not path.is_file() or path.stat().st_size == 0:
missing.append(filename)
print(f'[MISSING] {filename}: restore it from the runtime package or project repository; use just elf <path> to select firmware')
else:
print(f'[OK] {filename}')
platform = ROOT / 'platforms/stm32h723.repl'
if platform.is_file():
match = FLASH_PATH.search(platform.read_text(encoding='utf-8-sig'))
expected = f'filename: {json.dumps((ROOT / "peripherals/SPI/w25q16.py").as_posix(), ensure_ascii=False)}'
if not match or match[0].strip() != expected:
missing.append('Flash path')
print('[FIX] Flash path does not match this checkout; run: just format')
else:
print('[OK] Flash path matches this checkout')
for port in (as_port(setting('H723_UART_PORT', '12321')), as_port(setting('H723_GDB_PORT', '3333')),
as_port(setting('H723_WS_PORT', '12322'))):
print(f'[BUSY] Port {port}: quit the existing instance before just run'
if listening(port) else f'[OK] Port {port} is free')
if usb:
try:
from scripts.virtual_com import powershell_path
print(f'[OK] Project virtual COM bridge: {powershell_path()}')
except (OSError, RuntimeError, ValueError, subprocess.SubprocessError) as error:
missing.append('USB/virtual COM bridge')
print(f'[MISSING] USB/virtual COM bridge: {error}')
print(f'Check complete: {len(missing)} required item(s) need attention')
return 1 if missing else 0
def require_runtime():
"""启动前校验模型、固件和 Renode,并更新当前检出目录对应的平台路径"""
missing = [name for name in FILES if not (ROOT / name).is_file() or (ROOT / name).stat().st_size == 0]
if missing:
raise RuntimeError(f'Missing runtime files: {", ".join(missing)}. Run just check')
firmware = selected_elf()
if not firmware.is_file() or firmware.stat().st_size == 0:
raise RuntimeError(f'Missing ELF: {firmware}. Select firmware with just elf <path>')
renode = renode_executable()
if not renode:
raise RuntimeError('Renode not found. Run just check for installation instructions')
format_paths()
return renode