Files
chuan 771a96fb51 fix: 修复仿真交互锁顺序并隔离会话历史
- 将网页输入和外设操作的仿真调度移到状态锁外,覆盖复位、退订和断线释放路径
- 通过真实 GPIO 边沿缓存送丝使能电平,移除快照及激光和波形复位中的反向 GPIO 读取
- 为每个 Renode 会话复制用户配置并设置独立的 Monitor 历史文件,保留原配置及其他设置
- 添加时钟锁和 GPIO 锁回归探针、会话配置与双实例隔离用例,并补充运行说明
2026-10-26 11:27:04 +08:00

157 lines
9.6 KiB
Python

"""生成隔离的启动会话,协调 Renode、持久化镜像与可选虚拟串口的生命周期"""
from contextlib import ExitStack, contextmanager
import ctypes
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
import time
from scripts.environment import ROOT, RUNTIME, configured_path, free_port, listening, require_runtime, selected_elf, setting
from scripts.flash_images import flash_image
from scripts.renode_process import create_session_config, exit_previous, stop_process, wait_for_monitor
from scripts.settings import as_port
from scripts.external_devices import ExternalDevices
@contextmanager
def simulation(*, debug=False, flash=None, internal_flash=None, uart_port=None, gdb_port=None, ws_port=None, fast_serial=False, devices=None):
"""创建并监督一个 Renode 子进程,在退出上下文前等待其正常退出
每次启动隔离临时缓存,避免 Renode 在 Windows 上误清理其他会话的缓存
本层不取得镜像锁,直接调用方需自行保证传入镜像的独占使用
"""
devices = devices or ExternalDevices()
renode = require_runtime()
# 共享终端使用独立组;无终端时创建隐藏 console,其首进程同样可按 PID 定向通知
process_options = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP}
if not ctypes.WinDLL('kernel32', use_last_error=True).GetConsoleProcessList((ctypes.c_ulong * 1)(), 1):
startup = subprocess.STARTUPINFO()
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startup.wShowWindow = subprocess.SW_HIDE
process_options.update(creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.CREATE_NEW_PROCESS_GROUP,
startupinfo=startup, stdin=subprocess.DEVNULL, stdout=sys.stdout, stderr=sys.stderr)
uart, gdb = (
uart_port or as_port(setting('H723_UART_PORT', '12321')),
gdb_port or as_port(setting('H723_GDB_PORT', '3333')))
ws = ws_port or as_port(setting('H723_WS_PORT', '12322'))
if len({uart, gdb, ws}) != 3:
raise ValueError('UART, GDB and WebSocket ports must be different')
monitor_port = free_port()
while monitor_port in (uart, gdb, ws):
monitor_port = free_port()
occupied = [port for port in (uart, gdb, ws) if listening(port)]
if occupied:
raise RuntimeError(f'Ports {occupied} already in use. Quit the existing H723 instance first')
temp_root = RUNTIME / 'tmp'
temp_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='run-', dir=temp_root) as temporary:
session = Path(temporary)
script = session / 'launch.resc'
root_script_token = (ROOT / 'platforms/motion_controller.resc').as_posix().replace(' ', '\\ ')
elf_token = selected_elf().as_posix().replace(' ', '\\ ')
# 先由平台加载 ELF,再覆盖持久化的内部 Flash,以恢复升级程序与启动标记
script.write_text(f'$uartPort={uart}\n$gdbPort={gdb}\n$wsPort={ws}\n$elfPath=@{elf_token}\n'
f'include @{root_script_token}\n'
+ (f'sysbus.flashController OpenPersistentImage "{internal_flash.as_posix()}"\n' if internal_flash else '')
+ 'sysbus.cpu VectorTableOffset 0x08000000\n'
+ (f'sysbus LoadBinary @{flash.as_posix().replace(" ", chr(92) + " ")} 0x90000000\n' if flash else '')
+ ('sysbus.boardSerial FastMode true\n' if fast_serial else '')
+ devices.monitor_commands()
+ ('' if debug else 'start\n')
+ f'$h723LaunchCompleted="{session.name}"\n', encoding='utf-8')
environment = os.environ.copy()
environment.pop('H723_FLASH_IMAGE', None)
if flash:
environment['H723_FLASH_IMAGE'] = str(flash)
environment['TEMP'] = environment['TMP'] = str(session)
# 位置参数不会正确转义脚本路径中的空格,改用 Monitor 的 PathToken 语法
script_token = script.as_posix().replace(' ', '\\ ')
argv = [str(renode), '--plain', '--disable-gui', '--hide-analyzers', '--config',
str(create_session_config(session)), '--port',
str(monitor_port), '-e', f'include @{script_token}']
proc = subprocess.Popen(argv, cwd=ROOT, env=environment, **process_options)
monitor = None
try:
monitor = wait_for_monitor(proc, monitor_port)
# ELF 加载或后续命令失败时仍能读取 PC,必须确认整个启动脚本已执行完成
# 成功标记只在脚本末尾赋值,并与本次会话绑定
try:
completed = monitor.command('echo $h723LaunchCompleted')
except RuntimeError as error:
raise RuntimeError(f'Renode startup did not complete:\n{monitor.initial_output}') from error
if not re.search(r'(?:^|[\r\n])' + re.escape(session.name) + r'[\r\n]', completed):
raise RuntimeError('Renode startup completion marker does not match this session')
websocket = monitor.command('sysbus.boardWebSocket Port')
if not re.search(r'(?im)^\s*0x0*' + format(ws, 'x') + r'\s*$', websocket):
raise RuntimeError('Board WebSocket did not start on the configured port')
yield proc, monitor, uart, gdb
finally:
stop_process(proc, monitor, control_break=True)
def run(debug=False, usb=False, flash=None, force=False, com='COM12', uart_port=None, gdb_port=None, ws_port=None, fast_serial=False, devices=None):
"""取得两类 Flash 的独占锁后运行实例,直到子进程与桥接清理完成才释放锁"""
if force:
exit_previous()
external = (ROOT / flash).resolve() if flash else configured_path('H723_FLASH_IMAGE', '.runtime/w25q16.bin')
internal = configured_path('H723_INTERNAL_FLASH_IMAGE', '.runtime/stm32-internal.bin')
if internal == external:
raise ValueError('Internal and external Flash images must use different paths')
with flash_image(internal, initialize=False), flash_image(external) as image:
print(f'Persistent external Flash: {image}', flush=True)
print(f'Persistent internal Flash: {internal}', flush=True)
return run_instance(debug, usb, image, com, uart_port, gdb_port, ws_port, fast_serial, internal, devices)
def run_instance(debug, usb, flash, com, uart_port=None, gdb_port=None, ws_port=None, fast_serial=False, internal_flash=None, devices=None):
"""启动传输桥接并发布实例记录,监督任一子进程异常退出并按顺序清理"""
from scripts.virtual_com import virtual_com
# 状态清理栈放在外层,确保退出记录比串口桥接和 Renode 上下文更晚销毁
with ExitStack() as state_cleanup, ExitStack() as stack:
state_path = RUNTIME / 'run.json'
proc, monitor, uart, gdb = stack.enter_context(simulation(
debug=debug, flash=flash, internal_flash=internal_flash, uart_port=free_port() if usb else uart_port,
gdb_port=gdb_port, ws_port=ws_port, fast_serial=fast_serial, devices=devices))
bridge = stack.enter_context(virtual_com(uart, com, RUNTIME, fast=fast_serial)) if usb else None
com = com.upper() if usb else None
state_path.write_text(json.dumps({'pid': proc.pid, 'debug': debug, 'usb': usb, 'com_port': com, 'flash': str(flash),
'monitor_port': monitor.sock.getpeername()[1], 'uart_port': uart, 'gdb_port': gdb,
'ws_port': ws_port or as_port(setting('H723_WS_PORT', '12322')), 'fast_serial': fast_serial,
'internal_flash': str(internal_flash), 'control_break': True}), encoding='utf-8')
# 停止标记最后移除,让 just stop 等待串口桥接和仿真实例全部完成清理
state_cleanup.callback(state_path.unlink, missing_ok=True)
print(f'UART4 RTU: 127.0.0.1:{uart}; GDB: 127.0.0.1:{gdb}', flush=True)
print(f'USB/virtual COM mode: select {com} in your application (115200, 8N1, slave 8).'
if usb else f'Network mode: set comStr to tcp://127.0.0.1:{uart}. Disconnect any virtual COM bridge from this endpoint.', flush=True)
print(f'Renode monitor: 127.0.0.1:{monitor.sock.getpeername()[1]}', flush=True)
print(f'Board WebSocket: ws://127.0.0.1:{ws_port or setting("H723_WS_PORT", "12322")}/board', flush=True)
if fast_serial:
print('Fast serial simulation enabled; physical serial timing is accelerated.', flush=True)
print('CPU paused; connect GDB and continue. Press Ctrl+C to stop.' if debug
else 'Firmware running. Press Ctrl+C to stop.', flush=True)
# Monitor 按连接顺序服务,启动检查后释放连接,允许操作者接入调试
monitor.sock.close()
interrupted = False
try:
try:
while proc.poll() is None:
if bridge is not None and bridge.poll() is not None:
try:
proc.wait(timeout=1)
except subprocess.TimeoutExpired as error:
raise RuntimeError(f'Virtual COM bridge exited; see {RUNTIME / "logs/usb.log"}') from error
time.sleep(.2)
except KeyboardInterrupt:
interrupted = True
print('\nStopping this Renode instance...', flush=True)
finally:
if proc.poll() is None:
stop_process(proc, control_break=True)
if not interrupted and proc.returncode not in (0, None):
raise RuntimeError(f'Renode exited with code {proc.returncode}')
return 0