<# run_boot_test.ps1 - Automated boot-log capture + verify in one call. 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] ` [-SettleMs 8000] [-MaxWaitMs 30000] ` [-FlashCmd "D:\...\download.bat"] ` [-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 = 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. #> [CmdletBinding()] param( [string]$Port = "", [int]$BaudRate = 115200, [int]$SettleMs = 8000, [int]$MaxWaitMs = 30000, [string]$FlashCmd = "", [string]$Required = "App init OK;Network Init;net init: OK", [string]$Forbidden = "FAIL;ERROR;HardFault", [string]$OutDir = "$env:TEMP\opencode", [int]$ShowTailLines = 15 ) $ErrorActionPreference = 'Stop' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $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" # -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 $repoRoot "MDK-ARM\download.bat" } 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 $FlashCmd)) { Write-Output "RUN_TEST_ERROR: flash command not found: $FlashCmd" exit 3 } if (-not (Test-Path -LiteralPath $OutDir)) { New-Item -ItemType Directory -Path $OutDir -Force | Out-Null } $stamp = Get-Date -Format "yyyyMMdd_HHmmss" $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 "Port: $Port @ ${BaudRate} settle=${SettleMs}ms max=${MaxWaitMs}ms" # 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 } # 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 } # 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 } # 5. extract [RX] payload lines (strip the "[RX] " 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 } $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))" # 6. marker verification & $verifyScript -LogPath $bootTxt -Required $Required -Forbidden $Forbidden ` -ShowTailLines $ShowTailLines $verifyRc = $LASTEXITCODE Write-Output "RUN_TEST_RC=$verifyRc" exit $verifyRc