<# 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