新增 Python 串口助手 serial_tool(常驻 daemon+全量日志文件即接口)取代 serial-mcp-server:@test 采集链改写 + 全库文档迁移

This commit is contained in:
2026-09-15 03:00:41 +08:00
parent 9eb1bd63b4
commit 4b5ce72e11
16 changed files with 1463 additions and 406 deletions
+106 -51
View File
@@ -1,25 +1,36 @@
<#
run_boot_test.ps1 - Full automated boot-log capture + verify in one call.
run_boot_test.ps1 - Automated boot-log capture + verify in one call.
Orchestrates:
1. serial_capture.ps1 (with -FlashCmd): stable-start capture -> flash -> collect
2. Extract the captured text from the JSON
3. verify_log.ps1 on the extracted boot log
Uses the repo's Python serial assistant (tools/serial_tool/serial_tool.py,
resident daemon on 127.0.0.1:18765, auto-started on first call):
1. open COM port + clear log (fresh generation for this run)
2. flash via -FlashCmd (download.bat: download + Reset & Run)
3. wait-idle: until RX silence >= SettleMs, or MaxWaitMs timeout
4. extract [RX] payload lines from the log file (prefix/timestamp stripped)
5. verify_log.ps1 marker check
The capture is armed (port open, log cleared) BEFORE flashing, and the
resident daemon buffers everything in tools/serial_tool/state/serial.log -
no capture-window timing ritual, no first-byte wait, no EOF retries.
Usage:
powershell -ExecutionPolicy Bypass -File run_boot_test.ps1 `
[-Port COM6] [-BaudRate 115200] `
[-DurationMs 30000] [-InitialTimeoutMs 90000] [-IdleTimeoutMs 8000] `
[-SettleMs 8000] [-MaxWaitMs 30000] `
[-FlashCmd "D:\...\download.bat"] `
[-Required "App init OK;Network Init;net init: OK;PHY: 100M FULL"] `
[-Required "App init OK;Network Init;net init: OK"] `
[-Forbidden "FAIL;ERROR;HardFault"] `
[-OutDir C:\temp] [-ShowTailLines 15]
-Port default: .opencode/serial_config.json "portName" entry.
-SettleMs: RX silence that ends the capture (default 8000 - boot has a
natural 3~4s silent gap between tasks; 2000 would truncate the log).
Exit codes:
0 = PASS (captured + all required markers + no forbidden)
1 = FAIL (marker miss / forbidden hit)
2 = capture or flash error
3 = verify file error
3 = usage / preflight error
NOTE: ASCII-only on purpose - project source files contain Chinese and must
never be rewritten by encoding-sensitive PowerShell. Keep this script ASCII.
@@ -27,12 +38,10 @@ never be rewritten by encoding-sensitive PowerShell. Keep this script ASCII.
[CmdletBinding()]
param(
[string]$Port = "COM6",
[string]$Port = "",
[int]$BaudRate = 115200,
[int]$DurationMs = 30000,
[int]$InitialTimeoutMs = 90000,
[int]$IdleTimeoutMs = 8000,
[int]$MaxBytes = 65536,
[int]$SettleMs = 8000,
[int]$MaxWaitMs = 30000,
[string]$FlashCmd = "",
[string]$Required = "App init OK;Network Init;net init: OK",
[string]$Forbidden = "FAIL;ERROR;HardFault",
@@ -43,22 +52,37 @@ param(
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$captureScript = Join-Path $scriptDir "serial_capture.ps1"
$repoRoot = Split-Path -Parent $scriptDir
$py = Join-Path $repoRoot ".venv\Scripts\python.exe"
$tool = Join-Path $repoRoot "tools\serial_tool\serial_tool.py"
$verifyScript = Join-Path $scriptDir "verify_log.ps1"
$cfgFile = Join-Path $repoRoot ".opencode\serial_config.json"
# Default flash command: resolve relative to this script's repo location
# (the old hardcoded absolute path pointed at a stale checkout location).
# -Port default: the single source of truth is .opencode/serial_config.json
if (-not $Port) {
if (Test-Path -LiteralPath $cfgFile) {
$Port = (Get-Content -LiteralPath $cfgFile -Raw | ConvertFrom-Json).portName
}
}
if (-not $Port) {
Write-Output "RUN_TEST_ERROR: no -Port given and no portName in $cfgFile"
exit 3
}
# Default flash command: resolve relative to the repo (no stale absolute paths)
if (-not $FlashCmd) {
$FlashCmd = Join-Path $scriptDir "..\MDK-ARM\download.bat"
$FlashCmd = Join-Path $repoRoot "MDK-ARM\download.bat"
}
if (-not (Test-Path -LiteralPath $captureScript)) {
Write-Output "RUN_TEST_ERROR: serial_capture.ps1 not found"
exit 2
foreach ($p in @($py, $tool, $verifyScript)) {
if (-not (Test-Path -LiteralPath $p)) {
Write-Output "RUN_TEST_ERROR: missing $p"
exit 3
}
}
if (-not (Test-Path -LiteralPath $verifyScript)) {
Write-Output "RUN_TEST_ERROR: verify_log.ps1 not found"
exit 2
if (-not (Test-Path -LiteralPath $FlashCmd)) {
Write-Output "RUN_TEST_ERROR: flash command not found: $FlashCmd"
exit 3
}
if (-not (Test-Path -LiteralPath $OutDir)) {
@@ -66,52 +90,83 @@ if (-not (Test-Path -LiteralPath $OutDir)) {
}
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$capJson = Join-Path $OutDir "boot_capture_$stamp.json"
$bootTxt = Join-Path $OutDir "boot_log_$stamp.txt"
function Invoke-Tool {
param(
[Parameter(Position = 0)][string]$Command,
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Rest
)
$script:toolOut = (& $py $tool $Command @Rest 2>$null) -join "`n"
$script:toolRc = $LASTEXITCODE
}
Write-Output "=== Boot test $stamp ==="
Write-Output "Capture: $capJson"
Write-Output "Port: $Port @ ${BaudRate} settle=${SettleMs}ms max=${MaxWaitMs}ms"
& $captureScript -OutFile $capJson -Port $Port -BaudRate $BaudRate `
-DurationMs $DurationMs -InitialTimeoutMs $InitialTimeoutMs `
-IdleTimeoutMs $IdleTimeoutMs -MaxBytes $MaxBytes `
-FlashCmd $FlashCmd -MaxRetries 3 -RetryDelayMs 1500
$capRc = $LASTEXITCODE
if ($capRc -ne 0) {
Write-Output "RUN_TEST_ERROR: capture failed (rc=$capRc)"
# 1. open port (auto-starts the serial daemon if it is not running)
Invoke-Tool "open" "--port" $Port "--baud" $BaudRate "--timeout" 15
if ($script:toolRc -ne 0) {
Write-Output "RUN_TEST_ERROR: open failed: $($script:toolOut)"
exit 2
}
if (-not (Test-Path -LiteralPath $capJson)) {
Write-Output "RUN_TEST_ERROR: no capture JSON at $capJson"
# 2. clear log (fresh generation; only data after this belongs to the run)
Invoke-Tool "clear" "--timeout" 15
if ($script:toolRc -ne 0) {
Write-Output "RUN_TEST_ERROR: clear failed: $($script:toolOut)"
exit 2
}
try {
$json = Get-Content -LiteralPath $capJson -Raw -Encoding UTF8 | ConvertFrom-Json
} catch {
Write-Output "RUN_TEST_ERROR: cannot parse capture JSON: $_"
# 3. flash (download + Reset & Run). A failed flash does not abort: the
# board may still have reset; capture + marker check will judge the result.
Write-Output "FLASH_START: $FlashCmd"
$sw = [System.Diagnostics.Stopwatch]::StartNew()
& $FlashCmd
$flashRc = $LASTEXITCODE
$sw.Stop()
Write-Output ("FLASH_RC: {0} elapsed_ms={1}" -f $flashRc, $sw.ElapsedMilliseconds)
if ($flashRc -ne 0) {
Write-Output "RUN_TEST_WARN: flash rc=$flashRc (continuing: board may still have reset)"
}
# 4. wait for RX silence (settle) or the overall timeout
$waitTimeoutS = [int]($MaxWaitMs / 1000) + 20
Invoke-Tool "wait-idle" "--settle-ms" $SettleMs "--max-ms" $MaxWaitMs "--timeout" $waitTimeoutS
if ($script:toolRc -ne 0) {
Write-Output "RUN_TEST_ERROR: wait-idle failed: $($script:toolOut)"
exit 2
}
$wait = $script:toolOut | ConvertFrom-Json
Write-Output ("WAIT: reason={0} rx_bytes={1} waited_ms={2}" -f $wait.reason, $wait.rx_bytes, $wait.waited_ms)
if ($wait.reason -eq "no_data") {
Write-Output "RUN_TEST_ERROR: no RX data at all (check port / board power / flash)"
exit 2
}
if (-not $json.read) {
Write-Output "RUN_TEST_ERROR: capture JSON has no 'read' object"
# 5. extract [RX] payload lines (strip the "[RX] <iso-timestamp> " prefix)
Invoke-Tool "log" "--tail" 0 "--timeout" 15
if ($script:toolRc -ne 0) {
Write-Output "RUN_TEST_ERROR: log read failed: $($script:toolOut)"
exit 2
}
Write-Output ("Capture: bytes_read={0} completion_reason={1} waited_ms={2} elapsed_ms={3}" -f `
$json.read.bytes_read, $json.read.completion_reason, $json.read.waited_ms, $json.read.elapsed_ms)
$data = [string]$json.read.data
if ([string]::IsNullOrEmpty($data)) {
Write-Output "RUN_TEST_ERROR: capture returned no data (device silent?)"
Write-Output "Hint: raise -InitialTimeoutMs if flashing is slow, or check the device boots."
$logJson = $script:toolOut | ConvertFrom-Json
$rx = New-Object System.Collections.Generic.List[string]
foreach ($l in $logJson.lines) {
if ($l -is [string] -and $l.StartsWith("[RX] ")) {
$rest = $l.Substring(5)
$sp = $rest.IndexOf(" ")
if ($sp -ge 0) { $rx.Add($rest.Substring($sp + 1)) } else { $rx.Add($rest) }
}
}
if ($rx.Count -eq 0) {
Write-Output "RUN_TEST_ERROR: no [RX] lines in log (device silent?)"
exit 2
}
[System.IO.File]::WriteAllLines($bootTxt, $rx, (New-Object System.Text.UTF8Encoding($false)))
Write-Output "Boot log: $bootTxt (lines=$($rx.Count) gen=$($logJson.gen))"
[System.IO.File]::WriteAllText($bootTxt, $data, [System.Text.Encoding]::UTF8)
Write-Output "Boot log: $bootTxt"
# 6. marker verification
& $verifyScript -LogPath $bootTxt -Required $Required -Forbidden $Forbidden `
-ShowTailLines $ShowTailLines
$verifyRc = $LASTEXITCODE
-159
View File
@@ -1,159 +0,0 @@
<#
serial_capture.ps1 - Capture a serial boot log with serial-mcp-server CLI.
Workflow (deterministic boot-log capture after flashing):
1. Open COM via serial-mcp-server `read` CAPTURE mode and WAIT for the first
byte (up to -InitialTimeoutMs). This starts BEFORE flashing.
2. Flash the target (download.bat) -> MCU resets and prints the boot log.
3. The running capture picks up the first byte, then collects for -DurationMs.
4. The process exits and writes a single structured JSON file at -OutFile.
Known trap (Adancurusul/serial-mcp-server, fixed here by retry):
When COM is released by a previous process and reopened immediately,
tokio-serial read returns 0 bytes and the project's capture.rs turns that
into a fatal `UnexpectedEof` error (serial stream returned zero bytes).
This is transient: a short backoff then reopening succeeds. This script
detects that error, backs off, and restarts the capture.
Usage:
powershell -ExecutionPolicy Bypass -File serial_capture.ps1 `
-OutFile C:\temp\boot.json `
[-Port COM6] [-BaudRate 115200] `
[-DurationMs 20000] [-InitialTimeoutMs 60000] `
[-IdleTimeoutMs 1500] [-MaxBytes 65536] `
[-FlashCmd "D:\...\download.bat"] `
[-MaxRetries 3] [-RetryDelayMs 1500]
Modes:
-FlashCmd: orchestrates the whole sequence in ONE call: (retry-stable)
start capture, run the flash/download command, then wait for capture to
finish. Use this to avoid tool-call gaps letting the capture timeout before
flashing.
No -FlashCmd: starts the capture, waits for it to be stable (survives
-StabilityCheckMs), prints the PID, and returns. Caller flashes, then calls
with -WaitOnly to reap.
Exit codes:
0 = capture completed (ok or clean timeout)
1 = capture failed to start after retries
2 = capture error status in JSON
NOTE: ASCII-only on purpose - project source files contain Chinese and must
never be rewritten by encoding-sensitive PowerShell. Keep this script ASCII.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$OutFile,
[string]$Port = "COM6",
[int]$BaudRate = 115200,
[int]$DurationMs = 20000,
[int]$InitialTimeoutMs = 60000,
[int]$IdleTimeoutMs = 1500,
[int]$MaxBytes = 65536,
[string]$FlashCmd = "",
[int]$MaxRetries = 3,
[int]$RetryDelayMs = 1500,
[int]$StabilityCheckMs = 1500
)
$ErrorActionPreference = 'Stop'
$exe = "$env:LOCALAPPDATA\serial-mcp-server\serial-mcp-server.exe"
if (-not (Test-Path -LiteralPath $exe)) {
Write-Output "CAPTURE_ERROR: serial-mcp-server binary not found: $exe"
exit 1
}
$outDir = Split-Path -Parent $OutFile
if ($outDir -and -not (Test-Path -LiteralPath $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
$baseArgs = @(
"read",
"--port", $Port,
"--baud", ([string]$BaudRate),
"--duration-ms", ([string]$DurationMs),
"--start-trigger", "first-byte",
"--initial-timeout-ms", ([string]$InitialTimeoutMs),
"--idle-timeout-ms", ([string]$IdleTimeoutMs),
"--max-bytes", ([string]$MaxBytes),
"--json"
)
function Start-CaptureProcess {
param([int]$attempt)
$out = $OutFile
$err = "$OutFile.err"
if (Test-Path -LiteralPath $out) { Remove-Item -LiteralPath $out -Force }
if (Test-Path -LiteralPath $err) { Remove-Item -LiteralPath $err -Force }
if ($attempt -gt 0) {
Write-Host "RETRY: capture attempt $attempt (backoff $RetryDelayMs ms)"
Start-Sleep -Milliseconds $RetryDelayMs
}
$p = Start-Process -FilePath $exe -ArgumentList $baseArgs `
-RedirectStandardOutput $out -RedirectStandardError $err `
-WindowStyle Hidden -PassThru
if ($p -eq $null -or $p.HasExited) {
Write-Output "CAPTURE_ERROR: process failed to start (attempt $attempt)"
return $null
}
return $p
}
function Test-CaptureErrored {
$err = "$OutFile.err"
if (Test-Path -LiteralPath $err) {
$e = Get-Content -LiteralPath $err -Raw -ErrorAction SilentlyContinue
if ($e -match "UnexpectedEof|zero bytes|Error:") { return $true }
}
return $false
}
$proc = $null
for ($i = 0; $i -le $MaxRetries; $i++) {
$proc = Start-CaptureProcess -attempt $i
if ($proc -eq $null) { continue }
# Wait a moment: if the process died with EOF/error immediately, retry.
Start-Sleep -Milliseconds $StabilityCheckMs
if (-not $proc.HasExited) { break } # stable, keep it
if (-not (Test-CaptureErrored)) { break } # exited cleanly, no error
Write-Host "CAPTURE_EOF: transient EOF on attempt $i, retrying..."
$proc = $null
}
if ($proc -eq $null) {
Write-Output "CAPTURE_ERROR: could not start a stable capture after retries"
exit 1
}
Write-Output "CAPTURE_PID: $($proc.Id)"
if ($FlashCmd) {
if (-not (Test-Path -LiteralPath $FlashCmd)) {
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
Write-Output "CAPTURE_ERROR: flash command not found: $FlashCmd"
exit 1
}
Write-Output "FLASH_START: $FlashCmd"
& $FlashCmd
$flashRc = $LASTEXITCODE
Write-Output "FLASH_RC: $flashRc"
$proc.WaitForExit()
Write-Output "CAPTURE_DONE"
if (Test-Path -LiteralPath $OutFile) {
$content = Get-Content -LiteralPath $OutFile -Raw -ErrorAction SilentlyContinue
if ($content -match '"status":\s*"error"') {
Write-Output "CAPTURE_ERROR: capture returned error status"
Write-Output $content
exit 2
}
}
exit 0
}
exit 0
+3 -3
View File
@@ -1,9 +1,9 @@
<#
verify_log.ps1 - Check a captured MCU debug log against boot/test markers.
Used by the @test loop: after flashing, the fresh boot log is captured to a
file (serial-mcp-server capture), then this script greps it and prints a
structured verdict.
Used by the @test loop: after flashing, the fresh boot log is extracted from
the serial assistant log (tools/serial_tool), then this script greps it and
prints a structured verdict.
Usage:
powershell -ExecutionPolicy Bypass -File verify_log.ps1 `