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