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