- 修复 DMA 请求电平追踪、接收 FIFO 打包和禁用时的数据提交,更新 USART 的接收 DMA 请求 - 保留定时器复位前的 RCC 输入频率 - 正常退出时保存内部 Flash 已完成编程数据,过滤当前未完成编程字 - 使用独立进程组请求 Renode 正常退出,退出未完成时保留镜像锁和实例记录 - 将 controller.py 拆分为环境配置、Flash 镜像、进程管理和仿真启动模块,保留原命令入口 - 为 scripts 下的 Python 文件补充职责、生命周期和异常边界注释,并更新退出与持久化说明
93 lines
4.4 KiB
Python
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)
|