firmwareloop 0.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firmwareloop-0.0.8.dist-info/METADATA +268 -0
- firmwareloop-0.0.8.dist-info/RECORD +26 -0
- firmwareloop-0.0.8.dist-info/WHEEL +5 -0
- firmwareloop-0.0.8.dist-info/entry_points.txt +5 -0
- firmwareloop-0.0.8.dist-info/licenses/LICENSE +21 -0
- firmwareloop-0.0.8.dist-info/top_level.txt +1 -0
- tools/__init__.py +5 -0
- tools/acceptance-scenario.ps1 +455 -0
- tools/build.ps1 +208 -0
- tools/can.ps1 +130 -0
- tools/check-qoder-mcp.ps1 +111 -0
- tools/common/build-backends.psm1 +294 -0
- tools/common/fw.psm1 +311 -0
- tools/common/uart_probe.py +100 -0
- tools/doctor.ps1 +148 -0
- tools/flash.ps1 +181 -0
- tools/fw_mcp_server.py +1272 -0
- tools/instrument_cli.py +417 -0
- tools/lib/__init__.py +3 -0
- tools/lib/instruments.py +181 -0
- tools/logic_capture.ps1 +261 -0
- tools/logic_decode.ps1 +307 -0
- tools/reset.ps1 +141 -0
- tools/setup-agent-mcp.ps1 +147 -0
- tools/test-backends.ps1 +100 -0
- tools/test.ps1 +194 -0
tools/common/fw.psm1
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# fw.psm1 - FirmwareLoop: shared helpers for PowerShell tools.
|
|
2
|
+
# Provides: unified JSON result envelope, Error Class registry (Spec §22),
|
|
3
|
+
# python/venv resolution, log saving, compiler diagnostics parsing.
|
|
4
|
+
|
|
5
|
+
Set-StrictMode -Version Latest
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
|
|
8
|
+
# --- Error Classes (Spec §22 + v0.0.2 §24 additions) --------------------------
|
|
9
|
+
$script:FW_ERROR_CLASSES = @(
|
|
10
|
+
'BUILD_ERROR', 'ARTIFACT_NOT_FOUND', 'PROBE_NOT_FOUND', 'TARGET_MISMATCH',
|
|
11
|
+
'FLASH_ERROR', 'FLASH_VERIFY_ERROR', 'RESET_ERROR', 'UART_TIMEOUT',
|
|
12
|
+
'UART_BUSY', 'CAN_ERROR', 'DEBUGGER_ERROR', 'LOGIC_CAPTURE_ERROR',
|
|
13
|
+
'INSTRUMENT_NOT_FOUND', 'INSTRUMENT_TIMEOUT', 'MEASUREMENT_OUT_OF_RANGE',
|
|
14
|
+
'TEST_FAILED', 'PERMISSION_DENIED', 'SAFETY_LIMIT', 'CONFIG_ERROR',
|
|
15
|
+
'UNKNOWN_ERROR',
|
|
16
|
+
# v0.0.2 additions (Gap spec §24)
|
|
17
|
+
'CAPABILITY_NOT_SUPPORTED', 'DEPENDENCY_DISCOVERY_REQUIRED',
|
|
18
|
+
'HARDWARE_GATE_BYPASSED', 'REAL_HARDWARE_REQUIRED', 'HARDWARE_VALIDATION_FAILED'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# --- JSON output --------------------------------------------------------------
|
|
22
|
+
function Write-FwJson {
|
|
23
|
+
<#
|
|
24
|
+
.SYNOPSIS
|
|
25
|
+
Emit a structured result as JSON. Always UTF-8; deep serialization.
|
|
26
|
+
#>
|
|
27
|
+
param(
|
|
28
|
+
[Parameter(Mandatory = $true)] $Object,
|
|
29
|
+
[switch]$Compact
|
|
30
|
+
)
|
|
31
|
+
$depth = 20
|
|
32
|
+
if ($Compact) {
|
|
33
|
+
$json = $Object | ConvertTo-Json -Depth $depth -Compress
|
|
34
|
+
} else {
|
|
35
|
+
$json = $Object | ConvertTo-Json -Depth $depth
|
|
36
|
+
}
|
|
37
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
38
|
+
Write-Output $json
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Get-FwErrorClass {
|
|
42
|
+
<#
|
|
43
|
+
.SYNOPSIS
|
|
44
|
+
Validate that a string is a legal Error Class (Spec §22).
|
|
45
|
+
#>
|
|
46
|
+
param([Parameter(Mandatory = $true)][string]$Name)
|
|
47
|
+
if ($Name -notin $script:FW_ERROR_CLASSES) {
|
|
48
|
+
throw "Illegal error class '$Name'. Allowed: $($script:FW_ERROR_CLASSES -join ', ')"
|
|
49
|
+
}
|
|
50
|
+
return $Name
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function New-FwError {
|
|
54
|
+
<#
|
|
55
|
+
.SYNOPSIS
|
|
56
|
+
Deterministic failure envelope. Every external-process failure must
|
|
57
|
+
return one of these (Spec Rule: every operation returns structured errors).
|
|
58
|
+
#>
|
|
59
|
+
param(
|
|
60
|
+
[Parameter(Mandatory = $true)][string]$ErrorClass,
|
|
61
|
+
[Parameter(Mandatory = $true)][string]$Message,
|
|
62
|
+
[string]$Detail = $null,
|
|
63
|
+
[int]$ExitCode = 1
|
|
64
|
+
)
|
|
65
|
+
Get-FwErrorClass $ErrorClass | Out-Null
|
|
66
|
+
$body = [ordered]@{
|
|
67
|
+
ok = $false
|
|
68
|
+
error_class = $ErrorClass
|
|
69
|
+
error = $Message
|
|
70
|
+
}
|
|
71
|
+
if ($Detail) { $body.detail = $Detail }
|
|
72
|
+
[pscustomobject]$body
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# --- Python / venv resolution -------------------------------------------------
|
|
76
|
+
function Resolve-FwPython {
|
|
77
|
+
<#
|
|
78
|
+
.SYNOPSIS
|
|
79
|
+
Locate a project Python: prefer .venv\Scripts\python.exe relative to
|
|
80
|
+
repo root, then PATH. Never hardcoded absolute paths (Spec §25).
|
|
81
|
+
#>
|
|
82
|
+
param([string]$RepoRoot)
|
|
83
|
+
$candidates = @()
|
|
84
|
+
if ($RepoRoot) {
|
|
85
|
+
$candidates += (Join-Path $RepoRoot '.venv\Scripts\python.exe')
|
|
86
|
+
$candidates += (Join-Path $RepoRoot 'venv\Scripts\python.exe')
|
|
87
|
+
}
|
|
88
|
+
foreach ($path in $candidates) {
|
|
89
|
+
if (Test-Path -LiteralPath $path) { return (Resolve-Path -LiteralPath $path).Path }
|
|
90
|
+
}
|
|
91
|
+
$fromPath = Get-Command python -ErrorAction SilentlyContinue
|
|
92
|
+
if ($fromPath) { return $fromPath.Source }
|
|
93
|
+
return $null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function Get-FwRepoRoot {
|
|
97
|
+
<#
|
|
98
|
+
.SYNOPSIS
|
|
99
|
+
Root of the firmware project = directory containing tools/ and lab/.
|
|
100
|
+
#>
|
|
101
|
+
$cur = Get-Location
|
|
102
|
+
while ($cur) {
|
|
103
|
+
if ((Test-Path (Join-Path $cur 'tools')) -and (Test-Path (Join-Path $cur 'lab'))) {
|
|
104
|
+
return $cur.Path
|
|
105
|
+
}
|
|
106
|
+
$cur = $cur.Parent
|
|
107
|
+
}
|
|
108
|
+
return (Get-Location).Path
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function Save-FwLog {
|
|
112
|
+
<#
|
|
113
|
+
.SYNOPSIS
|
|
114
|
+
Append text to a log file under artifacts/logs/ (creates dirs).
|
|
115
|
+
#>
|
|
116
|
+
param(
|
|
117
|
+
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Path,
|
|
118
|
+
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content
|
|
119
|
+
)
|
|
120
|
+
$dir = Split-Path -Parent $Path
|
|
121
|
+
if ($dir -and -not (Test-Path -LiteralPath $dir)) {
|
|
122
|
+
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
123
|
+
}
|
|
124
|
+
if ($Content -eq '') { $Content = "`n" }
|
|
125
|
+
Add-Content -LiteralPath $Path -Value $Content -Encoding utf8
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function Get-FwTimestamp {
|
|
129
|
+
return (Get-Date).ToString('yyyy-MM-ddTHH:mm:sszzz')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Get-FwRunId {
|
|
133
|
+
<#
|
|
134
|
+
.SYNOPSIS
|
|
135
|
+
run_id like "20260817-a81f" (Spec §19).
|
|
136
|
+
#>
|
|
137
|
+
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
|
|
138
|
+
$rand = -join ((97..122) | Get-Random -Count 4 | ForEach-Object { [char]$_ })
|
|
139
|
+
return "$stamp-$rand"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function Get-FwGitInfo {
|
|
143
|
+
<#
|
|
144
|
+
.SYNOPSIS
|
|
145
|
+
{commit, dirty} for audit purposes. Never fails the caller when
|
|
146
|
+
the directory is not a git repo.
|
|
147
|
+
#>
|
|
148
|
+
param([string]$RepoRoot)
|
|
149
|
+
$info = [ordered]@{ commit = $null; dirty = $true }
|
|
150
|
+
if (-not $RepoRoot) { return [pscustomobject]$info }
|
|
151
|
+
$commit = git -C $RepoRoot rev-parse --short HEAD 2>$null
|
|
152
|
+
if ($LASTEXITCODE -eq 0 -and $commit) {
|
|
153
|
+
$info.commit = $commit
|
|
154
|
+
$dirtyLines = git -C $RepoRoot status --porcelain 2>$null
|
|
155
|
+
$info.dirty = [bool]($dirtyLines | Where-Object { $_ })
|
|
156
|
+
}
|
|
157
|
+
return [pscustomobject]$info
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# --- Compiler diagnostics (GCC/Clang "file:line:col: severity: msg") ----------
|
|
161
|
+
function Get-FwDiagnostics {
|
|
162
|
+
<#
|
|
163
|
+
.SYNOPSIS
|
|
164
|
+
Parse a compiler log into specified diagnostics entries. Handles both
|
|
165
|
+
"file:line:col: error: message" and bare "error:"/"warning:" lines
|
|
166
|
+
(attributed to the last seen file when possible).
|
|
167
|
+
#>
|
|
168
|
+
param(
|
|
169
|
+
[Parameter(Mandatory = $true)][string[]]$Lines,
|
|
170
|
+
[string]$BaseDir = $null
|
|
171
|
+
)
|
|
172
|
+
$diags = [System.Collections.Generic.List[object]]::new()
|
|
173
|
+
$lastFile = $null
|
|
174
|
+
foreach ($raw in $Lines) {
|
|
175
|
+
$line = $raw.TrimEnd()
|
|
176
|
+
if (-not $line) { continue }
|
|
177
|
+
$m = [regex]::Match($line, '^(?<file>.+?):(?<line>\d+):(?<col>\d+):\s*(?<sev>fatal error|error|warning):\s*(?<msg>.*)$')
|
|
178
|
+
if ($m.Success) {
|
|
179
|
+
$file = $m.Groups['file'].Value.Trim('"')
|
|
180
|
+
if ($BaseDir) {
|
|
181
|
+
# Normalize separators so windows/posix mixups don't block the trim.
|
|
182
|
+
$fileNorm = $file.Replace('/', '\')
|
|
183
|
+
$baseNorm = $BaseDir.Replace('/', '\')
|
|
184
|
+
if ($fileNorm.StartsWith($baseNorm, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
185
|
+
$file = $fileNorm.Substring($baseNorm.Length).TrimStart('\')
|
|
186
|
+
} else {
|
|
187
|
+
$file = $fileNorm
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
$sev = $m.Groups['sev'].Value
|
|
191
|
+
if ($sev -eq 'fatal error') { $sev = 'error' }
|
|
192
|
+
$diags.Add([pscustomobject]@{
|
|
193
|
+
file = $file
|
|
194
|
+
line = [int]$m.Groups['line'].Value
|
|
195
|
+
col = [int]$m.Groups['col'].Value
|
|
196
|
+
severity = $sev
|
|
197
|
+
message = $m.Groups['msg'].Value
|
|
198
|
+
})
|
|
199
|
+
$lastFile = $file
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
$m2 = [regex]::Match($line, '^\s*(?<sev>fatal error|error|warning):\s*(?<msg>.*)$')
|
|
203
|
+
if ($m2.Success) {
|
|
204
|
+
$sev = $m2.Groups['sev'].Value
|
|
205
|
+
if ($sev -eq 'fatal error') { $sev = 'error' }
|
|
206
|
+
$diags.Add([pscustomobject]@{
|
|
207
|
+
file = if ($lastFile) { $lastFile } else { $null }
|
|
208
|
+
line = $null
|
|
209
|
+
col = $null
|
|
210
|
+
severity = $sev
|
|
211
|
+
message = $m2.Groups['msg'].Value
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return ,$diags
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function Invoke-FwProcess {
|
|
219
|
+
<#
|
|
220
|
+
.SYNOPSIS
|
|
221
|
+
Run an external process with a hard timeout. Every external process
|
|
222
|
+
in this toolchain MUST go through here (Spec Rule 6).
|
|
223
|
+
Returns { exit_code, timed_out, stdout, stderr }.
|
|
224
|
+
#>
|
|
225
|
+
param(
|
|
226
|
+
[Parameter(Mandatory = $true)][string]$FilePath,
|
|
227
|
+
[string[]]$Arguments = @(),
|
|
228
|
+
[string]$WorkingDirectory = $null,
|
|
229
|
+
[int]$TimeoutMs = 300000,
|
|
230
|
+
[string]$StdoutFile = $null,
|
|
231
|
+
[string]$StderrFile = $null
|
|
232
|
+
)
|
|
233
|
+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
|
234
|
+
$psi.FileName = $FilePath
|
|
235
|
+
foreach ($a in $Arguments) { $psi.ArgumentList.Add($a) }
|
|
236
|
+
$psi.UseShellExecute = $false
|
|
237
|
+
$psi.CreateNoWindow = $true
|
|
238
|
+
$psi.RedirectStandardOutput = $true
|
|
239
|
+
$psi.RedirectStandardError = $true
|
|
240
|
+
if ($WorkingDirectory) { $psi.WorkingDirectory = $WorkingDirectory }
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
$proc = [System.Diagnostics.Process]::new()
|
|
244
|
+
$proc.StartInfo = $psi
|
|
245
|
+
if (-not $proc.Start()) {
|
|
246
|
+
return [pscustomobject]@{ exit_code = -1; timed_out = $false; stdout = ''; stderr = 'failed to start' }
|
|
247
|
+
}
|
|
248
|
+
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
|
|
249
|
+
$stderrTask = $proc.StandardError.ReadToEndAsync()
|
|
250
|
+
|
|
251
|
+
$timedOut = -not $proc.WaitForExit($TimeoutMs)
|
|
252
|
+
if ($timedOut) {
|
|
253
|
+
try { $proc.Kill($true) } catch { }
|
|
254
|
+
$proc.WaitForExit()
|
|
255
|
+
}
|
|
256
|
+
$stdout = $stdoutTask.GetAwaiter().GetResult()
|
|
257
|
+
$stderr = $stderrTask.GetAwaiter().GetResult()
|
|
258
|
+
|
|
259
|
+
if ($StdoutFile) {
|
|
260
|
+
Save-FwLog -Path $StdoutFile -Content $stdout
|
|
261
|
+
}
|
|
262
|
+
if ($StderrFile) {
|
|
263
|
+
Save-FwLog -Path $StderrFile -Content $stderr
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return [pscustomobject]@{
|
|
267
|
+
exit_code = $proc.ExitCode
|
|
268
|
+
timed_out = $timedOut
|
|
269
|
+
stdout = $stdout
|
|
270
|
+
stderr = $stderr
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return [pscustomobject]@{
|
|
275
|
+
exit_code = -1
|
|
276
|
+
timed_out = $false
|
|
277
|
+
stdout = ''
|
|
278
|
+
stderr = "failed to launch '$FilePath': $($_.Exception.Message)"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function Get-FwLabConfig {
|
|
284
|
+
<#
|
|
285
|
+
.SYNOPSIS
|
|
286
|
+
Parse lab/lab.yaml (or the example fallback) into a PowerShell object.
|
|
287
|
+
YAML is not natively parseable in PowerShell; the project venv python
|
|
288
|
+
+ PyYAML does the parsing (release-fix #5: no ConvertFrom-Json on YAML).
|
|
289
|
+
#>
|
|
290
|
+
param([string]$RepoRoot)
|
|
291
|
+
if (-not $RepoRoot) { return $null }
|
|
292
|
+
$python = Resolve-FwPython -RepoRoot $RepoRoot
|
|
293
|
+
if (-not $python) { return $null }
|
|
294
|
+
$candidates = @(
|
|
295
|
+
(Join-Path $RepoRoot 'lab\lab.yaml'),
|
|
296
|
+
(Join-Path $RepoRoot 'lab\lab.example.yaml')
|
|
297
|
+
)
|
|
298
|
+
foreach ($path in $candidates) {
|
|
299
|
+
if (-not (Test-Path -LiteralPath $path)) { continue }
|
|
300
|
+
$script = "import json, sys; sys.stdout.reconfigure(encoding='utf-8', errors='replace');`nimport yaml`nprint(json.dumps(yaml.safe_load(open(sys.argv[1], encoding='utf-8')) or {}))"
|
|
301
|
+
$res = Invoke-FwProcess -FilePath $python -Arguments @('-c', $script, $path) -TimeoutMs 30000
|
|
302
|
+
if ($res.exit_code -eq 0) {
|
|
303
|
+
try { return ($res.stdout | ConvertFrom-Json) } catch { }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return $null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
Export-ModuleMember -Function Write-FwJson, Get-FwErrorClass, New-FwError, `
|
|
310
|
+
Resolve-FwPython, Get-FwRepoRoot, Save-FwLog, Get-FwTimestamp, `
|
|
311
|
+
Get-FwRunId, Get-FwGitInfo, Get-FwDiagnostics, Invoke-FwProcess, Get-FwLabConfig
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""uart_probe.py - minimal DUT UART probe over a subprocess (simulator DUT).
|
|
3
|
+
|
|
4
|
+
Spawns the compiled firmware artifact, waits for the boot banner, sends one
|
|
5
|
+
CLI command and prints the observed reply line as JSON:
|
|
6
|
+
|
|
7
|
+
{"ok": true, "banner": [...], "command": "jedec", "reply": "JEDEC EF 40 18"}
|
|
8
|
+
|
|
9
|
+
Used by tools/acceptance-scenario.ps1 for the offline (simulator) leg of the
|
|
10
|
+
final acceptance scenario. Real-serial probing goes through Agentic HIL.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
REPLY_TOKENS = ("JEDEC", "PWM", "ERR")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> int:
|
|
22
|
+
exe = sys.argv[1]
|
|
23
|
+
command = sys.argv[2] if len(sys.argv) > 2 else "jedec"
|
|
24
|
+
timeout = float(sys.argv[3]) if len(sys.argv) > 3 else 8.0
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
proc = subprocess.Popen(
|
|
28
|
+
[exe], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
29
|
+
stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", bufsize=1,
|
|
30
|
+
)
|
|
31
|
+
except OSError as exc:
|
|
32
|
+
print(json.dumps({"ok": False, "error_class": "PROBE_NOT_FOUND",
|
|
33
|
+
"error": f"cannot spawn artifact: {exc}"}))
|
|
34
|
+
return 2
|
|
35
|
+
|
|
36
|
+
banner: list[str] = []
|
|
37
|
+
deadline = time.monotonic() + timeout
|
|
38
|
+
reply = None
|
|
39
|
+
|
|
40
|
+
# read banner until it becomes quiet or the prompt command lands
|
|
41
|
+
while time.monotonic() < deadline:
|
|
42
|
+
try:
|
|
43
|
+
line = proc.stdout.readline()
|
|
44
|
+
except Exception: # noqa: BLE001
|
|
45
|
+
break
|
|
46
|
+
if not line:
|
|
47
|
+
break
|
|
48
|
+
line = line.rstrip("\r\n")
|
|
49
|
+
if not line:
|
|
50
|
+
continue
|
|
51
|
+
banner.append(line)
|
|
52
|
+
if line.startswith("Application started"):
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
proc.stdin.write(command + "\n")
|
|
57
|
+
proc.stdin.flush()
|
|
58
|
+
except Exception as exc: # noqa: BLE001
|
|
59
|
+
print(json.dumps({"ok": False, "error_class": "UART_TIMEOUT",
|
|
60
|
+
"error": f"cannot write command: {exc}", "banner": banner}))
|
|
61
|
+
proc.kill()
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
deadline = time.monotonic() + timeout
|
|
65
|
+
collected: list[str] = []
|
|
66
|
+
while time.monotonic() < deadline:
|
|
67
|
+
try:
|
|
68
|
+
line = proc.stdout.readline()
|
|
69
|
+
except Exception: # noqa: BLE001
|
|
70
|
+
break
|
|
71
|
+
if not line:
|
|
72
|
+
break
|
|
73
|
+
line = line.rstrip("\r\n")
|
|
74
|
+
if not line:
|
|
75
|
+
continue
|
|
76
|
+
if line.startswith(("Bootloader ", "Application ")):
|
|
77
|
+
continue
|
|
78
|
+
collected.append(line)
|
|
79
|
+
if line.startswith(REPLY_TOKENS):
|
|
80
|
+
reply = line
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
proc.kill()
|
|
84
|
+
try:
|
|
85
|
+
proc.wait(timeout=3)
|
|
86
|
+
except Exception: # noqa: BLE001
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
if reply is None:
|
|
90
|
+
print(json.dumps({"ok": False, "error_class": "UART_TIMEOUT", "error": "no reply",
|
|
91
|
+
"banner": banner, "collected": collected}))
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
print(json.dumps({"ok": True, "banner": banner, "command": command,
|
|
95
|
+
"reply": reply, "collected": collected}, ensure_ascii=False))
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
sys.exit(main())
|
tools/doctor.ps1
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Environment doctor (Spec M0 / §26). Checks the pieces of the baseline
|
|
4
|
+
toolchain and returns one JSON document:
|
|
5
|
+
|
|
6
|
+
.\tools\doctor.ps1 -Json
|
|
7
|
+
|
|
8
|
+
The doctor NEVER fails the whole run because hardware is absent: hardware
|
|
9
|
+
categories report status "missing"/"not_configured" and delivery is
|
|
10
|
+
deferred to the project. Only a broken core toolchain (no python, no git,
|
|
11
|
+
no build tool) is reported as not-ok.
|
|
12
|
+
#>
|
|
13
|
+
[CmdletBinding()]
|
|
14
|
+
param([switch]$Json)
|
|
15
|
+
|
|
16
|
+
Set-StrictMode -Version Latest
|
|
17
|
+
$ErrorActionPreference = 'Stop'
|
|
18
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
19
|
+
|
|
20
|
+
$repoRoot = Get-FwRepoRoot
|
|
21
|
+
$python = Resolve-FwPython -RepoRoot $repoRoot
|
|
22
|
+
$venvBin = if ($python -and $python -match '.venv[\\/]Scripts[\\/]python.exe$') { Split-Path $python } else { $null }
|
|
23
|
+
|
|
24
|
+
function Test-Tool {
|
|
25
|
+
param([string]$Name)
|
|
26
|
+
# prefer the project venv bin dir (tools installed without polluting PATH)
|
|
27
|
+
$candidate = if ($venvBin) { Join-Path $venvBin ($Name + '.exe') } else { $null }
|
|
28
|
+
$cmd = if ($candidate -and (Test-Path -LiteralPath $candidate)) {
|
|
29
|
+
Get-Item -LiteralPath $candidate
|
|
30
|
+
} else {
|
|
31
|
+
Get-Command $Name -ErrorAction SilentlyContinue
|
|
32
|
+
}
|
|
33
|
+
if ($cmd) {
|
|
34
|
+
$v = $null
|
|
35
|
+
$src = if ($cmd.GetType().Name -eq 'ApplicationInfo') { $cmd.Source } else { $cmd.FullName }
|
|
36
|
+
try { $v = (& $src --version 2>&1 | Select-Object -First 1) } catch { }
|
|
37
|
+
return [pscustomobject]@{ status = 'ok'; path = $src; version = [string]$v }
|
|
38
|
+
}
|
|
39
|
+
return [pscustomobject]@{ status = 'missing'; path = $null; version = $null }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Test-PyModule {
|
|
43
|
+
param([string]$Module, [string]$Python)
|
|
44
|
+
if (-not $Python) { return [pscustomobject]@{ status = 'missing'; detail = 'no python' } }
|
|
45
|
+
$out = & $Python -c "import $Module; print(getattr($Module, '__version__', 'present'))" 2>&1
|
|
46
|
+
if ($LASTEXITCODE -eq 0) { return [pscustomobject]@{ status = 'ok'; detail = ([string]$out).Trim() } }
|
|
47
|
+
return [pscustomobject]@{ status = 'missing'; detail = ([string]$out).Trim() }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$checks = [ordered]@{}
|
|
51
|
+
|
|
52
|
+
# --- core toolchain -----------------------------------------------------------
|
|
53
|
+
$checks.python = if ($python) {
|
|
54
|
+
$pyv = & $python --version 2>&1
|
|
55
|
+
[pscustomobject]@{ status = 'ok'; path = $python; version = ([string]$pyv).Trim() }
|
|
56
|
+
} else {
|
|
57
|
+
[pscustomobject]@{ status = 'missing'; path = $null; version = $null }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$checks.git = if ((Get-Command git -ErrorAction SilentlyContinue)) {
|
|
61
|
+
$gv = git --version 2>&1
|
|
62
|
+
$gitStatus = [pscustomobject]@{ status = 'ok'; path = (Get-Command git).Source; version = ([string]$gv).Trim() }
|
|
63
|
+
$gitStatus | Add-Member -NotePropertyName repo -NotePropertyValue (git -C $repoRoot rev-parse --is-inside-work-tree 2>$null)
|
|
64
|
+
$gitStatus
|
|
65
|
+
} else {
|
|
66
|
+
[pscustomobject]@{ status = 'missing'; path = $null; version = $null }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# --- build backends -----------------------------------------------------------
|
|
70
|
+
foreach ($tool in @('cmake', 'ninja', 'make', 'gcc', 'clang')) {
|
|
71
|
+
$checks[$tool] = Test-Tool $tool
|
|
72
|
+
}
|
|
73
|
+
$checks.keil = Test-Tool 'UV4'
|
|
74
|
+
$checks.iar = Test-Tool 'IarBuild'
|
|
75
|
+
$checks.idf = Test-Tool 'idf.py'
|
|
76
|
+
$checks.west = Test-Tool 'west'
|
|
77
|
+
$checks.platformio = Test-Tool 'pio'
|
|
78
|
+
|
|
79
|
+
# --- hardware / lab layer ------------------------------------------------------
|
|
80
|
+
$checks.agentic_hil = Test-Tool 'agentic-hil'
|
|
81
|
+
$checks.openocd = Test-Tool 'openocd'
|
|
82
|
+
$checks.sigrok_cli = Test-Tool 'sigrok-cli'
|
|
83
|
+
$checks.pyocd = if ($python) {
|
|
84
|
+
Test-PyModule 'pyocd' $python
|
|
85
|
+
} else { [pscustomobject]@{ status = 'missing'; detail = 'no python' } }
|
|
86
|
+
|
|
87
|
+
$checks.pytest = if ($python) { Test-PyModule 'pytest' $python } else { [pscustomobject]@{ status = 'missing'; detail = 'no python' } }
|
|
88
|
+
$checks.pyserial = if ($python) { Test-PyModule 'serial' $python } else { [pscustomobject]@{ status = 'missing'; detail = 'no python' } }
|
|
89
|
+
$checks.pyvisa = if ($python) { Test-PyModule 'pyvisa' $python } else { [pscustomobject]@{ status = 'missing'; detail = 'no python' } }
|
|
90
|
+
|
|
91
|
+
# COM ports present (UART candidates) - informational, never hardcoded.
|
|
92
|
+
# Prefer Agentic HIL's enumeration (hardware-aware) when installed, else WMI.
|
|
93
|
+
$comPorts = @()
|
|
94
|
+
$ahil = if ($venvBin) { Join-Path $venvBin 'agentic-hil.exe' } else { $null }
|
|
95
|
+
if ($ahil -and (Test-Path -LiteralPath $ahil)) {
|
|
96
|
+
try {
|
|
97
|
+
$raw = & $ahil com-ports 2>$null | Out-String
|
|
98
|
+
$parsed = $raw | ConvertFrom-Json -ErrorAction SilentlyContinue
|
|
99
|
+
if ($parsed -and $parsed.ports) {
|
|
100
|
+
$comPorts = @($parsed.ports | ForEach-Object { $_.device } | Where-Object { $_ })
|
|
101
|
+
}
|
|
102
|
+
} catch { }
|
|
103
|
+
}
|
|
104
|
+
if ($comPorts.Count -eq 0) {
|
|
105
|
+
try {
|
|
106
|
+
$comPorts = @((Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DeviceID))
|
|
107
|
+
} catch { }
|
|
108
|
+
}
|
|
109
|
+
$checks.com_ports = [pscustomobject]@{
|
|
110
|
+
status = if ($comPorts.Count -gt 0) { 'ok' } else { 'none' }
|
|
111
|
+
source = if ($ahil -and (Test-Path -LiteralPath $ahil)) { 'agentic-hil' } else { 'wmi' }
|
|
112
|
+
ports = $comPorts
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Saleae Logic 2 MCP endpoint (127.0.0.1:10530) is optional; check socket
|
|
116
|
+
$logicOk = $false
|
|
117
|
+
try {
|
|
118
|
+
$tcp = [System.Net.Sockets.TcpClient]::new()
|
|
119
|
+
$tcp.Connect('127.0.0.1', 10530)
|
|
120
|
+
$logicOk = $tcp.Connected
|
|
121
|
+
$tcp.Close()
|
|
122
|
+
} catch { }
|
|
123
|
+
$checks.logic2_mcp = [pscustomobject]@{ status = if ($logicOk) { 'ok' } else { 'not_configured' }; endpoint = 'http://127.0.0.1:10530' }
|
|
124
|
+
|
|
125
|
+
# --- composition ----------------------------------------------------------------
|
|
126
|
+
$coreNames = @('python', 'git', 'cmake')
|
|
127
|
+
$coreOk = $true
|
|
128
|
+
$missingCore = @()
|
|
129
|
+
foreach ($n in $coreNames) {
|
|
130
|
+
if ($checks[$n].status -ne 'ok') { $coreOk = $false; $missingCore += $n }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$result = [ordered]@{
|
|
134
|
+
schema = 'firmware-doctor/v1'
|
|
135
|
+
ok = $coreOk
|
|
136
|
+
generated_at = Get-FwTimestamp
|
|
137
|
+
host = [ordered]@{
|
|
138
|
+
os = [System.Environment]::OSVersion.VersionString
|
|
139
|
+
pwsh = $PSVersionTable.PSVersion.ToString()
|
|
140
|
+
machine = $env:COMPUTERNAME
|
|
141
|
+
}
|
|
142
|
+
missing_core = $missingCore
|
|
143
|
+
checks = $checks
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ($Json) { Write-FwJson ([pscustomobject]$result) -Compact } else { Write-FwJson ([pscustomobject]$result) }
|
|
147
|
+
|
|
148
|
+
exit $(if ($coreOk) { 0 } else { 1 })
|