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/can.ps1
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
CAN bus adapter (Spec §12). Minimum capability: session_start, send, read,
|
|
4
|
+
filter, session_stop. Backend is Agentic HIL (or a vendor CLI): the exact
|
|
5
|
+
verbs/schema MUST be confirmed against the installed version at setup time
|
|
6
|
+
(Spec §32). With no CAN backend configured this returns CONFIG_ERROR.
|
|
7
|
+
|
|
8
|
+
Safety: CAN TX defaults to manual authorization (Spec §12/§24); the
|
|
9
|
+
--authorized flag is required for send so the AI agent cannot TX silently.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
.\tools\can.ps1 session-start -Json
|
|
13
|
+
.\tools\can.ps1 send --arbitration-id 0x123 --frame-count 1 --authorized -Json
|
|
14
|
+
.\tools\can.ps1 read --duration-ms 500 -Json
|
|
15
|
+
.\tools\can.ps1 session-stop -Json
|
|
16
|
+
#>
|
|
17
|
+
[CmdletBinding()]
|
|
18
|
+
param(
|
|
19
|
+
[Parameter(Mandatory = $true)][ValidateSet('session-start', 'send', 'read', 'session-stop', 'filter')]
|
|
20
|
+
[string]$Command,
|
|
21
|
+
[ValidateSet('agentic-hil', 'vendor')]
|
|
22
|
+
[string]$Backend = 'agentic-hil',
|
|
23
|
+
[string]$Adapter,
|
|
24
|
+
[string]$Channel,
|
|
25
|
+
[int]$Bitrate = 500000,
|
|
26
|
+
[string]$ArbitrationId, # hex string, e.g. 0x123
|
|
27
|
+
[string]$Data, # hex bytes, e.g. "11 22 33"
|
|
28
|
+
[int]$FrameCount = 1,
|
|
29
|
+
[int]$DurationMs = 500,
|
|
30
|
+
[switch]$Authorized, # required for TX (default denied)
|
|
31
|
+
[switch]$Json
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
Set-StrictMode -Version Latest
|
|
35
|
+
$ErrorActionPreference = 'Stop'
|
|
36
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
37
|
+
|
|
38
|
+
$repoRoot = Get-FwRepoRoot
|
|
39
|
+
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
40
|
+
|
|
41
|
+
function Exit-WithError {
|
|
42
|
+
param([string]$Class, [string]$Message, [string]$Detail)
|
|
43
|
+
$body = New-FwError -ErrorClass $Class -Message $Message -Detail $Detail
|
|
44
|
+
if ($Json) { Write-FwJson $body -Compact } else { Write-FwJson $body; Write-Error "$Class : $Message" -ErrorAction Continue }
|
|
45
|
+
exit 2
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# CAN TX default requires explicit authorization (Spec §12: 人工授权)
|
|
49
|
+
if ($Command -eq 'send' -and -not $Authorized) {
|
|
50
|
+
Exit-WithError -Class 'PERMISSION_DENIED' -Message 'CAN TX requires explicit human authorization; pass -Authorized after approval (Spec §12/§24).'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
$cmd = Get-Command agentic-hil -ErrorAction SilentlyContinue
|
|
54
|
+
$tool = if ($cmd) { $cmd.Source } else { $null }
|
|
55
|
+
|
|
56
|
+
if ($Backend -eq 'agentic-hil' -and -not $tool) {
|
|
57
|
+
Exit-WithError -Class 'PROBE_NOT_FOUND' -Message "'agentic-hil' is not installed; CAN is not available until Agentic HIL is installed (Spec §32)." -Detail 'Install Agentic HIL, then verify its CAN schema with: agentic-hil --help'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Resolve lab config for adapter/channel/bitrate defaults (never hardcoded)
|
|
61
|
+
$labCfg = Join-Path $repoRoot 'lab\lab.yaml'
|
|
62
|
+
if (-not (Test-Path -LiteralPath $labCfg)) { $labCfg = Join-Path $repoRoot 'lab\lab.example.yaml' }
|
|
63
|
+
$canCfg = @{}
|
|
64
|
+
if (Test-Path -LiteralPath $labCfg) {
|
|
65
|
+
try {
|
|
66
|
+
$parsed = Get-Content -LiteralPath $labCfg -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue
|
|
67
|
+
if ($parsed -and $parsed.can) { $canCfg = $parsed.can }
|
|
68
|
+
} catch { }
|
|
69
|
+
} else { $canCfg = @{ adapter = $null; channel = $null; bitrate = $Bitrate } }
|
|
70
|
+
if (-not $Adapter) { $Adapter = $canCfg.adapter }
|
|
71
|
+
if (-not $Channel) { $Channel = $canCfg.channel }
|
|
72
|
+
if ($Bitrate -eq 500000 -and $canCfg.bitrate) { $Bitrate = $canCfg.bitrate }
|
|
73
|
+
|
|
74
|
+
if (-not $Adapter) {
|
|
75
|
+
Exit-WithError -Class 'CAN_ERROR' -Message 'CAN adapter is not configured; set lab/lab.yaml can.adapter (e.g. "PCAN_USBBus1").' -Detail 'Only adapter, channel and bitrate are betrothed by config; payload/structure are backend-enforced.'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# NOTE: verbs below follow Agentic HIL's documented surface; confirm against the
|
|
79
|
+
# installed version at setup time (Spec §32). Failures surface as CAN_ERROR.
|
|
80
|
+
$logFile = Join-Path $repoRoot "artifacts\logs\can-$stamp.log"
|
|
81
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $logFile) | Out-Null
|
|
82
|
+
|
|
83
|
+
switch ($Command) {
|
|
84
|
+
'session-start' {
|
|
85
|
+
$res = Invoke-FwProcess -FilePath $tool -Arguments @('can', 'session_start', '--adapter', $Adapter, '--channel', $Channel, '--bitrate', $Bitrate) -WorkingDirectory $repoRoot -TimeoutMs 30000 -StdoutFile $logFile
|
|
86
|
+
if ($res.timed_out -or $res.exit_code -ne 0) {
|
|
87
|
+
Exit-WithError -Class 'CAN_ERROR' -Message 'CAN session_start failed.' -Detail ($res.stdout + $res.stderr)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
'send' {
|
|
91
|
+
if (-not $ArbitrationId) { Exit-WithError -Class 'CONFIG_ERROR' -Message 'send requires --arbitration-id (e.g. 0x123)' }
|
|
92
|
+
$res = Invoke-FwProcess -FilePath $tool -Arguments @('can', 'send', '--id', $ArbitrationId, '--data', $Data, '--count', $FrameCount) -WorkingDirectory $repoRoot -TimeoutMs 30000 -StdoutFile $logFile
|
|
93
|
+
if ($res.timed_out -or $res.exit_code -ne 0) {
|
|
94
|
+
Exit-WithError -Class 'CAN_ERROR' -Message 'CAN send failed.' -Detail ($res.stdout + $res.stderr)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
'read' {
|
|
98
|
+
$res = Invoke-FwProcess -FilePath $tool -Arguments @('can', 'read', '--duration-ms', $DurationMs) -WorkingDirectory $repoRoot -TimeoutMs 60000 -StdoutFile $logFile
|
|
99
|
+
if ($res.timed_out -or $res.exit_code -ne 0) {
|
|
100
|
+
Exit-WithError -Class 'CAN_ERROR' -Message 'CAN read failed.' -Detail ($res.stdout + $res.stderr)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
'filter' {
|
|
104
|
+
if (-not $ArbitrationId) { Exit-WithError -Class 'CONFIG_ERROR' -Message 'filter requires --arbitration-id' }
|
|
105
|
+
$res = Invoke-FwProcess -FilePath $tool -Arguments @('can', 'filter', '--id', $ArbitrationId) -WorkingDirectory $repoRoot -TimeoutMs 30000 -StdoutFile $logFile
|
|
106
|
+
if ($res.timed_out -or $res.exit_code -ne 0) {
|
|
107
|
+
Exit-WithError -Class 'CAN_ERROR' -Message 'CAN filter failed.' -Detail ($res.stdout + $res.stderr)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
'session-stop' {
|
|
111
|
+
$res = Invoke-FwProcess -FilePath $tool -Arguments @('can', 'session_stop') -WorkingDirectory $repoRoot -TimeoutMs 30000 -StdoutFile $logFile
|
|
112
|
+
if ($res.timed_out -or $res.exit_code -ne 0) {
|
|
113
|
+
Exit-WithError -Class 'CAN_ERROR' -Message 'CAN session_stop failed.' -Detail ($res.stdout + $res.stderr)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
$result = [ordered]@{
|
|
119
|
+
schema = 'firmware-can-result/v1'
|
|
120
|
+
ok = $true
|
|
121
|
+
command = $Command
|
|
122
|
+
backend = $Backend
|
|
123
|
+
adapter = $Adapter
|
|
124
|
+
channel = $Channel
|
|
125
|
+
bitrate = $Bitrate
|
|
126
|
+
log = (Resolve-Path -LiteralPath $logFile).Path
|
|
127
|
+
generated_at = Get-FwTimestamp
|
|
128
|
+
}
|
|
129
|
+
if ($Json) { Write-FwJson ([pscustomobject]$result) -Compact } else { Write-FwJson ([pscustomobject]$result) }
|
|
130
|
+
exit 0
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Qoder MCP readiness check (GAP-010, release-fix #6).
|
|
4
|
+
|
|
5
|
+
Checks whether the machine is READY to use Agentic HIL inside Qoder, and
|
|
6
|
+
prints the exact registration command when it is not yet registered.
|
|
7
|
+
This is a CHECK-ONLY script: it never modifies Qoder or Agentic HIL
|
|
8
|
+
configuration. (Formerly configure-qoder.ps1; renamed because it does not
|
|
9
|
+
actually register - registration belongs to Qoder local/user scope.)
|
|
10
|
+
|
|
11
|
+
detect -> locate agentic-hil (user-level install preferred, per Agentic
|
|
12
|
+
HIL official quickstart) + qoder/qodercli CLI
|
|
13
|
+
verify -> agentic-hil mcp-stdio boots (warmup window)
|
|
14
|
+
advise -> exact registration command when missing
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
.\tools\check-qoder-mcp.ps1 -Json
|
|
18
|
+
#>
|
|
19
|
+
[CmdletBinding()]
|
|
20
|
+
param([switch]$Json)
|
|
21
|
+
|
|
22
|
+
Set-StrictMode -Version Latest
|
|
23
|
+
$ErrorActionPreference = 'Stop'
|
|
24
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
25
|
+
|
|
26
|
+
$repoRoot = Get-FwRepoRoot
|
|
27
|
+
|
|
28
|
+
function Exit-WithError {
|
|
29
|
+
param([string]$Class, [string]$Message, [string]$Detail)
|
|
30
|
+
$body = New-FwError -ErrorClass $Class -Message $Message -Detail $Detail
|
|
31
|
+
if ($Json) { Write-FwJson $body -Compact } else { Write-FwJson $body; Write-Error "$Class : $Message" -ErrorAction Continue }
|
|
32
|
+
exit 2
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$report = [ordered]@{ schema = 'qoder-mcp-check/v1'; steps = [ordered]@{} }
|
|
36
|
+
|
|
37
|
+
# ---- 1. detect ------------------------------------------------------------------
|
|
38
|
+
# Agentic HIL quickstart recommends a persistent USER-LEVEL installation, not
|
|
39
|
+
# one venv per repo. Prefer PATH (user-level), fall back to the repo venv.
|
|
40
|
+
$ahil = $null
|
|
41
|
+
$g = Get-Command agentic-hil -ErrorAction SilentlyContinue
|
|
42
|
+
if ($g) { $ahil = $g.Source }
|
|
43
|
+
$ahilSource = 'user-level (PATH)'
|
|
44
|
+
if (-not $ahil) {
|
|
45
|
+
$venvAhil = Join-Path $repoRoot '.venv\Scripts\agentic-hil.exe'
|
|
46
|
+
if (Test-Path -LiteralPath $venvAhil) {
|
|
47
|
+
$ahil = $venvAhil
|
|
48
|
+
$ahilSource = 'repo .venv (consider a user-level install: agentic-hil agent-install)'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (-not $ahil -or -not (Test-Path -LiteralPath $ahil)) {
|
|
52
|
+
Exit-WithError -Class 'PROBE_NOT_FOUND' -Message 'agentic-hil not found; install it user-level (uv tool install agentic-hil / pipx) or into .venv (uv pip install agentic-hil).'
|
|
53
|
+
}
|
|
54
|
+
$report.steps.detect = [ordered]@{ ok = $true; agentic_hil = (Resolve-Path -LiteralPath $ahil).Path; source = $ahilSource }
|
|
55
|
+
|
|
56
|
+
# qoder CLI: try both spellings seen in the wild (release-fix #6: discovery,
|
|
57
|
+
# never hardcode one command name)
|
|
58
|
+
$qoderCmd = $null
|
|
59
|
+
foreach ($name in @('qoder', 'qodercli')) {
|
|
60
|
+
$c = Get-Command $name -ErrorAction SilentlyContinue
|
|
61
|
+
if ($c) { $qoderCmd = $c; break }
|
|
62
|
+
}
|
|
63
|
+
$report.steps.detect.qoder = if ($qoderCmd) { $qoderCmd.Source } else { $null }
|
|
64
|
+
$report.steps.detect.qoder_candidate = if ($qoderCmd) { $qoderCmd.Name } else { $null }
|
|
65
|
+
|
|
66
|
+
# ---- 2. verify MCP server boots ---------------------------------------------------
|
|
67
|
+
$proc = $null
|
|
68
|
+
$serverOk = $false
|
|
69
|
+
try {
|
|
70
|
+
$proc = [System.Diagnostics.Process]::new()
|
|
71
|
+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
|
72
|
+
$psi.FileName = $ahil
|
|
73
|
+
$psi.ArgumentList.Add('mcp-stdio')
|
|
74
|
+
$psi.WorkingDirectory = $repoRoot
|
|
75
|
+
$psi.UseShellExecute = $false
|
|
76
|
+
$psi.CreateNoWindow = $true
|
|
77
|
+
$proc.StartInfo = $psi
|
|
78
|
+
if ($proc.Start()) {
|
|
79
|
+
Start-Sleep -Milliseconds 1500
|
|
80
|
+
$serverOk = -not $proc.HasExited
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
$serverOk = $false
|
|
84
|
+
} finally {
|
|
85
|
+
if ($proc -and -not $proc.HasExited) { try { $proc.Kill($true) } catch { } }
|
|
86
|
+
}
|
|
87
|
+
$report.steps.verify = [ordered]@{
|
|
88
|
+
ok = $serverOk
|
|
89
|
+
mcp_handshake = if ($serverOk) { 'agentic-hil mcp-stdio booted and stayed alive (full handshake runs inside Qoder)' } else { 'mcp-stdio exited during warmup - check the installation' }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# ---- 3. advise ---------------------------------------------------------------------
|
|
93
|
+
$advice = [ordered]@{ ok = $true; registered = $null; command = $null }
|
|
94
|
+
if (-not $qoderCmd) {
|
|
95
|
+
$advice.ok = $false
|
|
96
|
+
$advice.note = 'no qoder/qodercli CLI on PATH; register manually in Qoder UI (local/user scope).'
|
|
97
|
+
} else {
|
|
98
|
+
# runtime discovery of the mcp subcommand (Spec §32)
|
|
99
|
+
$help = Invoke-FwProcess -FilePath $qoderCmd.Source -Arguments @('mcp', '--help') -WorkingDirectory $repoRoot -TimeoutMs 30000
|
|
100
|
+
if ($help.exit_code -eq 0) {
|
|
101
|
+
$advice.command = "$($qoderCmd.Name) mcp add agentic-hil -- $ahil mcp-stdio"
|
|
102
|
+
$advice.note = 'registration command (run in Qoder local/user scope):'
|
|
103
|
+
} else {
|
|
104
|
+
$advice.ok = $false
|
|
105
|
+
$advice.note = "$($qoderCmd.Name) CLI found but no 'mcp' subcommand; register manually in Qoder UI (local/user scope)."
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
$report.steps.advise = $advice
|
|
109
|
+
|
|
110
|
+
if ($Json) { Write-FwJson ([pscustomobject]$report) -Compact } else { Write-FwJson ([pscustomobject]$report) }
|
|
111
|
+
exit $(if ($report.steps.detect.ok -and $report.steps.verify.ok -and $advice.ok) { 0 } else { 1 })
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# build-backends.psm1 - FirmwareLoop v0.0.2 (GAP-002): build backend registry.
|
|
2
|
+
#
|
|
3
|
+
# Thin adapter: detection precedence, command construction and artifact
|
|
4
|
+
# collection per backend. No build system is reimplemented - each backend
|
|
5
|
+
# invokes the official CLI (cmake / make / pio / UV4 / IarBuild / west /
|
|
6
|
+
# idf.py). Command construction is testable via -DryRun (tools/build.ps1).
|
|
7
|
+
|
|
8
|
+
Set-StrictMode -Version Latest
|
|
9
|
+
|
|
10
|
+
$script:FW_BACKENDS = @('cmake', 'make', 'platformio', 'keil', 'iar', 'zephyr', 'esp-idf')
|
|
11
|
+
|
|
12
|
+
# ---- strong project markers (checked BEFORE generic CMake) --------------------
|
|
13
|
+
$script:FW_MARKERS = @(
|
|
14
|
+
@{ backend = 'zephyr'; marker = 'west.yml' },
|
|
15
|
+
@{ backend = 'esp-idf'; marker = 'sdkconfig' }, # sdkconfig / sdkconfig.defaults
|
|
16
|
+
@{ backend = 'esp-idf'; marker = 'idf.py' },
|
|
17
|
+
@{ backend = 'platformio'; marker = 'platformio.ini' },
|
|
18
|
+
@{ backend = 'keil'; marker = '*.uvproj*' }, # uvproj/uvprojx
|
|
19
|
+
@{ backend = 'iar'; marker = '*.ewp' },
|
|
20
|
+
@{ backend = 'make'; marker = 'Makefile' },
|
|
21
|
+
@{ backend = 'cmake'; marker = 'CMakeLists.txt' }
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
function Find-FwProject {
|
|
25
|
+
<#
|
|
26
|
+
.SYNOPSIS
|
|
27
|
+
Locate the actual project directory for a backend inside a search
|
|
28
|
+
root: root level first, then one directory deep. Returns
|
|
29
|
+
@{ src; project_file } (project_file null when the marker is a dir).
|
|
30
|
+
#>
|
|
31
|
+
param([string]$BackendName, [string]$SearchRoot)
|
|
32
|
+
foreach ($level in @($SearchRoot)) {
|
|
33
|
+
foreach ($m in $script:FW_MARKERS) {
|
|
34
|
+
if ($m.backend -ne $BackendName) { continue }
|
|
35
|
+
$hit = Get-ChildItem -LiteralPath $level -Filter $m.marker -File -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
36
|
+
if ($hit) { return [pscustomobject]@{ src = $level; project_file = $hit.FullName } }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
foreach ($sub in (Get-ChildItem -LiteralPath $SearchRoot -Directory -ErrorAction SilentlyContinue)) {
|
|
40
|
+
foreach ($m in $script:FW_MARKERS) {
|
|
41
|
+
if ($m.backend -ne $BackendName) { continue }
|
|
42
|
+
$hit = Get-ChildItem -LiteralPath $sub.FullName -Filter $m.marker -File -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
43
|
+
if ($hit) { return [pscustomobject]@{ src = $sub.FullName; project_file = $hit.FullName } }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [pscustomobject]@{ src = $SearchRoot; project_file = $null }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Get-FwBackendDetect {
|
|
50
|
+
<#
|
|
51
|
+
.SYNOPSIS
|
|
52
|
+
Detection precedence (Spec GAP-002): explicit -Backend > project
|
|
53
|
+
config > strong marker > generic detection. Returns
|
|
54
|
+
@{ backend; src; project_file } or $null when nothing found.
|
|
55
|
+
#>
|
|
56
|
+
param(
|
|
57
|
+
[string]$ExplicitBackend,
|
|
58
|
+
[string]$ProjectBackend,
|
|
59
|
+
[string]$SourceDir,
|
|
60
|
+
[string]$RepoRoot
|
|
61
|
+
)
|
|
62
|
+
if ($ExplicitBackend -and $ExplicitBackend -in $script:FW_BACKENDS) {
|
|
63
|
+
$root = if ($SourceDir) { $SourceDir } else { $RepoRoot }
|
|
64
|
+
$found = Find-FwProject -BackendName $ExplicitBackend -SearchRoot $root
|
|
65
|
+
return [pscustomobject]@{ backend = $ExplicitBackend; src = $found.src; project_file = $found.project_file; source = 'explicit' }
|
|
66
|
+
}
|
|
67
|
+
if ($ProjectBackend -and $ProjectBackend -in $script:FW_BACKENDS) {
|
|
68
|
+
$root = if ($SourceDir) { $SourceDir } else { $RepoRoot }
|
|
69
|
+
$found = Find-FwProject -BackendName $ProjectBackend -SearchRoot $root
|
|
70
|
+
return [pscustomobject]@{ backend = $ProjectBackend; src = $found.src; project_file = $found.project_file; source = 'project-config' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$candidates = @()
|
|
74
|
+
if ($SourceDir) {
|
|
75
|
+
$candidates += $SourceDir
|
|
76
|
+
} else {
|
|
77
|
+
$candidates += $RepoRoot
|
|
78
|
+
Get-ChildItem -LiteralPath $RepoRoot -Directory -ErrorAction SilentlyContinue |
|
|
79
|
+
Where-Object { $_.Name -notmatch '^(\.|artifacts|tests|tools|lab|docs|Spec|\.venv|node_modules|demo-make)$' } |
|
|
80
|
+
ForEach-Object { $candidates += $_.FullName }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
foreach ($c in $candidates) {
|
|
84
|
+
if (-not (Test-Path -LiteralPath $c)) { continue }
|
|
85
|
+
foreach ($m in $script:FW_MARKERS) {
|
|
86
|
+
$hit = Get-ChildItem -LiteralPath $c -Filter $m.marker -File -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
87
|
+
if ($hit) {
|
|
88
|
+
return [pscustomobject]@{ backend = $m.backend; src = $c; project_file = $hit.FullName; source = 'marker' }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return $null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Resolve-FwTool {
|
|
96
|
+
<#
|
|
97
|
+
.SYNOPSIS
|
|
98
|
+
Find a tool on PATH or return $null (never throws on missing).
|
|
99
|
+
#>
|
|
100
|
+
param([string]$Name, [string]$Override)
|
|
101
|
+
if ($Override) { return $Override }
|
|
102
|
+
$g = Get-Command $Name -ErrorAction SilentlyContinue
|
|
103
|
+
if ($g) { return $g.Source }
|
|
104
|
+
return $null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function Get-FwBackendCommand {
|
|
108
|
+
<#
|
|
109
|
+
.SYNOPSIS
|
|
110
|
+
Build the official CLI invocation for a backend. Pure construction -
|
|
111
|
+
no execution - so tests can assert command shape (Spec GAP-002
|
|
112
|
+
"Keil command construction" etc). Returns @{ file; args; cwd; label }.
|
|
113
|
+
#>
|
|
114
|
+
param(
|
|
115
|
+
[Parameter(Mandatory = $true)][string]$Backend,
|
|
116
|
+
[Parameter(Mandatory = $true)][string]$Src,
|
|
117
|
+
[string]$ProjectFile,
|
|
118
|
+
[string]$Configuration = 'Debug',
|
|
119
|
+
[string]$BuildDir,
|
|
120
|
+
[string]$Tool = $null, # override (fake executable for tests)
|
|
121
|
+
[switch]$AllowMissingTool # construction tests without installed tools
|
|
122
|
+
)
|
|
123
|
+
$b = $Backend.ToLowerInvariant()
|
|
124
|
+
switch ($b) {
|
|
125
|
+
'cmake' {
|
|
126
|
+
$generator = if (Get-Command ninja -ErrorAction SilentlyContinue) { 'Ninja' } else { 'MinGW Makefiles' }
|
|
127
|
+
if ($Tool) { $toolPath = $Tool } else { $toolPath = Resolve-FwTool -Name 'cmake' }; if (-not $toolPath) { if ($AllowMissingTool) { $toolPath = "<missing:cmake>" } else { return @{ missing = 'cmake' } } }
|
|
128
|
+
return @{
|
|
129
|
+
file = $toolPath
|
|
130
|
+
args = @('-S', $Src, '-B', $BuildDir, '-G', $generator, "-DCMAKE_BUILD_TYPE=$Configuration")
|
|
131
|
+
# follow-up steps executed in order after the configure command
|
|
132
|
+
steps = @(
|
|
133
|
+
@{ file = $toolPath; args = @('--build', $BuildDir, '--config', $Configuration); cwd = $null }
|
|
134
|
+
)
|
|
135
|
+
cwd = $null
|
|
136
|
+
label = "cmake -S <src> -B <build> -G $generator -DCMAKE_BUILD_TYPE=$Configuration; cmake --build <build> --config $Configuration"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
'make' {
|
|
140
|
+
if ($Tool) { $toolPath = $Tool } else { $toolPath = Resolve-FwTool -Name 'make' }; if (-not $toolPath) { if ($AllowMissingTool) { $toolPath = "<missing:make>" } else { return @{ missing = 'make' } } }
|
|
141
|
+
if (-not $toolPath) { return @{ missing = 'make' } }
|
|
142
|
+
return @{
|
|
143
|
+
file = $toolPath
|
|
144
|
+
args = @('-C', $Src, "-CONFIG=$Configuration")
|
|
145
|
+
cwd = $null
|
|
146
|
+
label = "make -C <src> -CONFIG=$Configuration"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
'platformio' {
|
|
150
|
+
if ($Tool) { $toolPath = $Tool } else { $toolPath = Resolve-FwTool -Name 'pio' }; if (-not $toolPath) { if ($AllowMissingTool) { $toolPath = "<missing:pio>" } else { return @{ missing = 'pio' } } }
|
|
151
|
+
if (-not $toolPath) { return @{ missing = 'platformio' } }
|
|
152
|
+
return @{
|
|
153
|
+
file = $toolPath
|
|
154
|
+
args = @('run', '-d', $Src, '-e', "$Configuration")
|
|
155
|
+
cwd = $null
|
|
156
|
+
label = "pio run -d <src> -e <env>"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
'keil' {
|
|
160
|
+
if ($Tool) { $toolPath = $Tool } else { $toolPath = Resolve-FwTool -Name 'UV4' }; if (-not $toolPath) { if ($AllowMissingTool) { $toolPath = "<missing:UV4>" } else { return @{ missing = 'UV4' } } }
|
|
161
|
+
if (-not $toolPath) { return @{ missing = 'keil' } }
|
|
162
|
+
if (-not $ProjectFile) { return @{ missing = 'keil-project' } }
|
|
163
|
+
return @{
|
|
164
|
+
file = $toolPath
|
|
165
|
+
args = @('-b', $ProjectFile, '-j0')
|
|
166
|
+
cwd = $null
|
|
167
|
+
label = "UV4 -b <project.uvprojx> -j0"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
'iar' {
|
|
171
|
+
if ($Tool) { $toolPath = $Tool } else { $toolPath = Resolve-FwTool -Name 'IarBuild' }; if (-not $toolPath) { if ($AllowMissingTool) { $toolPath = "<missing:IarBuild>" } else { return @{ missing = 'IarBuild' } } }
|
|
172
|
+
if (-not $toolPath) { return @{ missing = 'iar' } }
|
|
173
|
+
if (-not $ProjectFile) { return @{ missing = 'iar-project' } }
|
|
174
|
+
return @{
|
|
175
|
+
file = $toolPath
|
|
176
|
+
args = @($ProjectFile, '-build', $Configuration)
|
|
177
|
+
cwd = $null
|
|
178
|
+
label = "IarBuild <project.ewp> -build <config>"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
'zephyr' {
|
|
182
|
+
if ($Tool) { $toolPath = $Tool } else { $toolPath = Resolve-FwTool -Name 'west' }; if (-not $toolPath) { if ($AllowMissingTool) { $toolPath = "<missing:west>" } else { return @{ missing = 'west' } } }
|
|
183
|
+
if (-not $toolPath) { return @{ missing = 'zephyr' } }
|
|
184
|
+
$board = $env:ZEPHYR_BOARD
|
|
185
|
+
if (-not $board) { return @{ missing = 'zephyr-board'; detail = 'set ZEPHYR_BOARD (e.g. native_sim) or pass -Configuration' } }
|
|
186
|
+
return @{
|
|
187
|
+
file = $toolPath
|
|
188
|
+
args = @('build', '-d', $BuildDir, '-b', $board, '-p', 'auto')
|
|
189
|
+
cwd = $Src
|
|
190
|
+
label = "west build -d <build> -b <ZEPHYR_BOARD> -p auto"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
'esp-idf' {
|
|
194
|
+
if ($Tool) { $toolPath = $Tool } else { $toolPath = Resolve-FwTool -Name 'idf.py' }; if (-not $toolPath) { if ($AllowMissingTool) { $toolPath = "<missing:idf.py>" } else { return @{ missing = 'idf.py' } } }
|
|
195
|
+
if (-not $toolPath) { return @{ missing = 'esp-idf' } }
|
|
196
|
+
return @{
|
|
197
|
+
file = $toolPath
|
|
198
|
+
args = @('build')
|
|
199
|
+
cwd = $Src
|
|
200
|
+
label = "idf.py build (cwd=<src>)"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
default { return @{ missing = $b } }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# artifact naming per backend (Spec GAP-002 artifact manifest)
|
|
208
|
+
$script:FW_ARTIFACT_PATTERNS = @{
|
|
209
|
+
cmake = @{ primary = 'firmware.elf'; formats = @('elf'); secondary = @('firmware.hex', 'firmware.bin', 'firmware.map') }
|
|
210
|
+
make = @{ primary = 'app.elf'; formats = @('elf'); secondary = @('app.hex') }
|
|
211
|
+
keil = @{ primary = 'app.axf'; formats = @('axf'); secondary = @('app.hex') }
|
|
212
|
+
iar = @{ primary = 'app.out'; formats = @('out'); secondary = @('app.hex') }
|
|
213
|
+
zephyr = @{ primary = 'zephyr.elf'; formats = @('elf'); secondary = @('zephyr.hex') }
|
|
214
|
+
'esp-idf' = @{ primary = 'firmware.elf'; formats = @('elf'); secondary = @('firmware.bin') }
|
|
215
|
+
platformio = @{ primary = 'firmware.elf'; formats = @('elf'); secondary = @('firmware.bin', 'firmware.hex') }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# discovery fallback patterns per backend (release-fix #5): when the named
|
|
219
|
+
# primary is absent, scan the native output dir for the newest matching image
|
|
220
|
+
# (real projects rarely name their binary app.elf/app.axf/app.out).
|
|
221
|
+
$script:FW_DISCOVERY_PATTERNS = @{
|
|
222
|
+
make = @('*.elf', '*.hex')
|
|
223
|
+
keil = @('*.axf', '*.hex')
|
|
224
|
+
iar = @('*.out', '*.hex')
|
|
225
|
+
platformio = @('*.elf', '*.bin', '*.hex')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function Get-FwBackendArtifacts {
|
|
229
|
+
<#
|
|
230
|
+
.SYNOPSIS
|
|
231
|
+
Collect artifacts per backend into a firmware-artifacts/v1 manifest
|
|
232
|
+
object. Artifacts are located in each backend's NATIVE output
|
|
233
|
+
location (never fabricated, never borrowed from another backend's
|
|
234
|
+
leftovers). Missing files stay absent.
|
|
235
|
+
#>
|
|
236
|
+
param(
|
|
237
|
+
[Parameter(Mandatory = $true)][string]$Backend,
|
|
238
|
+
[Parameter(Mandatory = $true)][string]$ArtifactDir,
|
|
239
|
+
[string]$Src,
|
|
240
|
+
[string]$BuildDir
|
|
241
|
+
)
|
|
242
|
+
$pat = $script:FW_ARTIFACT_PATTERNS[$Backend]
|
|
243
|
+
if (-not $pat) { return $null }
|
|
244
|
+
# native output dir per backend
|
|
245
|
+
$outDir = switch ($Backend) {
|
|
246
|
+
'cmake' { $ArtifactDir }
|
|
247
|
+
'make' { $Src }
|
|
248
|
+
'keil' { $Src }
|
|
249
|
+
'iar' { $Src }
|
|
250
|
+
'zephyr' { Join-Path $BuildDir 'zephyr' }
|
|
251
|
+
'esp-idf' { Join-Path $Src 'build' }
|
|
252
|
+
'platformio' { Join-Path $Src '.pio\build' }
|
|
253
|
+
default { $ArtifactDir }
|
|
254
|
+
}
|
|
255
|
+
$primary = $null
|
|
256
|
+
$secondary = @()
|
|
257
|
+
$pPath = Join-Path $outDir $pat.primary
|
|
258
|
+
if (-not (Test-Path -LiteralPath $pPath)) {
|
|
259
|
+
# artifact discovery (release-fix #5): newest matching image in the
|
|
260
|
+
# native output location, including platformio's per-env subdirs
|
|
261
|
+
$discovery = $script:FW_DISCOVERY_PATTERNS[$Backend]
|
|
262
|
+
if ($discovery) {
|
|
263
|
+
$hits = @()
|
|
264
|
+
foreach ($g in $discovery) {
|
|
265
|
+
$hits += Get-ChildItem -LiteralPath $outDir -Filter $g -File -Recurse -ErrorAction SilentlyContinue
|
|
266
|
+
}
|
|
267
|
+
$newest = $hits | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
268
|
+
if ($newest) { $pPath = $newest.FullName }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (Test-Path -LiteralPath $pPath) {
|
|
272
|
+
$primary = [ordered]@{
|
|
273
|
+
path = $pPath
|
|
274
|
+
format = ([System.IO.Path]::GetExtension($pPath)).TrimStart('.')
|
|
275
|
+
native_path = (Resolve-Path -LiteralPath $pPath).Path
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
foreach ($s in $pat.secondary) {
|
|
279
|
+
$sp = Join-Path $outDir $s
|
|
280
|
+
if (Test-Path -LiteralPath $sp) {
|
|
281
|
+
$secondary += [ordered]@{ path = $sp; format = ([System.IO.Path]::GetExtension($s)).TrimStart('.') }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return [ordered]@{
|
|
285
|
+
schema = 'firmware-artifacts/v1'
|
|
286
|
+
backend = $Backend
|
|
287
|
+
configuration = $null # filled by caller
|
|
288
|
+
output_dir = $outDir
|
|
289
|
+
primary = $primary
|
|
290
|
+
secondary = $secondary
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
Export-ModuleMember -Function Get-FwBackendDetect, Get-FwBackendCommand, Get-FwBackendArtifacts -Variable FW_BACKENDS
|