Files
chuan 5d725e40a9 fix: 修复外设传输与退出持久化并拆分运行脚本
- 修复 DMA 请求电平追踪、接收 FIFO 打包和禁用时的数据提交,更新 USART 的接收 DMA 请求
- 保留定时器复位前的 RCC 输入频率
- 正常退出时保存内部 Flash 已完成编程数据,过滤当前未完成编程字
- 使用独立进程组请求 Renode 正常退出,退出未完成时保留镜像锁和实例记录
- 将 controller.py 拆分为环境配置、Flash 镜像、进程管理和仿真启动模块,保留原命令入口
- 为 scripts 下的 Python 文件补充职责、生命周期和异常边界注释,并更新退出与持久化说明
2026-10-21 15:31:26 +08:00

93 lines
4.4 KiB
Python

"""通过已安装的 VSPort 驱动管理项目虚拟串口及其 TCP 桥接辅助进程"""
from contextlib import contextmanager
import json
import os
from pathlib import Path
import re
import subprocess
import tempfile
import time
import winreg
def powershell_path():
"""确认 32 位 ActiveX 已注册,并选择位数匹配的 PowerShell 宿主"""
try:
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r'VSPort.VSPortAx\CLSID', 0,
winreg.KEY_READ | winreg.KEY_WOW64_32KEY):
pass
except OSError as error:
raise RuntimeError('Install the HW VSP3 virtual serial-port driver first') from error
executable = Path(os.environ['WINDIR']) / 'SysWOW64/WindowsPowerShell/v1.0/powershell.exe'
if not executable.is_file():
raise RuntimeError(f'Missing 32-bit Windows PowerShell: {executable}')
return executable
def cleanup_virtual_com(runtime, com=None):
"""仅清理有项目归属记录的串口,桥接存活或端口被占用时由辅助脚本拒绝删除"""
records = runtime / 'virtual-com'
paths = [records / f'{com.upper()}.json'] if com else sorted(records.glob('COM*.json'))
removed = 0
for path in paths:
if not path.exists():
continue
owner = json.loads(path.read_text(encoding='utf-8-sig'))
port = owner['com_port']
if not re.fullmatch(r'COM[1-9]\d*', port, re.I) or path.stem != port.upper():
raise RuntimeError(f'Invalid virtual COM ownership record: {path}')
result = subprocess.run([str(powershell_path()), '-NoProfile', '-NonInteractive', '-STA',
'-ExecutionPolicy', 'Bypass', '-File', str(Path(__file__).with_suffix('.ps1')),
'-ComPort', port, '-OwnershipFile', str(path), '-Cleanup'],
capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=15,
creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode:
raise RuntimeError((result.stderr or result.stdout).strip())
removed += 1
return removed
@contextmanager
def virtual_com(uart, com, runtime, *, fast=False):
"""创建项目专属串口,确认桥接就绪后交给调用方,退出时停止桥接并删除串口
创建前仅清理该端口的过期归属记录,不接管其他程序创建的虚拟串口
"""
if not re.fullmatch(r'COM[1-9]\d*', com, re.I):
raise ValueError(f'Invalid virtual serial port: {com}')
executable = powershell_path()
cleanup_virtual_com(runtime, com)
ownership = runtime / 'virtual-com' / f'{com.upper()}.json'
ownership.parent.mkdir(parents=True, exist_ok=True)
logs = runtime / 'logs'
logs.mkdir(parents=True, exist_ok=True)
temporary = runtime / 'tmp'
temporary.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='usb-', dir=temporary) as directory:
state = Path(directory)
log_path = logs / 'usb.log'
with log_path.open('w', encoding='utf-8') as log:
proc = subprocess.Popen([str(executable), '-NoProfile', '-NonInteractive', '-STA',
'-ExecutionPolicy', 'Bypass', '-File', str(Path(__file__).with_suffix('.ps1')),
'-ComPort', com, '-TcpPort', str(uart), '-StateDirectory', str(state),
'-OwnershipFile', str(ownership)] + (['-Fast'] if fast else []),
stdout=log, stderr=subprocess.STDOUT, creationflags=subprocess.CREATE_NO_WINDOW)
try:
deadline = time.monotonic() + 20
# 进程启动成功不代表驱动与 TCP 桥接可用,必须等待辅助脚本发布就绪标记
while not (state / 'ready').exists():
if proc.poll() is not None or time.monotonic() >= deadline:
detail = log_path.read_text(encoding='utf-8', errors='replace').splitlines()
raise RuntimeError(f'Virtual COM startup failed: {detail[0] if detail else "no driver response"}; see {log_path}')
time.sleep(.1)
yield proc
finally:
(state / 'stop').touch()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
# 此处仅终止卡住的串口辅助进程,DeleteOnExit 负责释放其持有的端口
proc.terminate()
proc.wait(timeout=5)
cleanup_virtual_com(runtime, com)