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/logic_capture.ps1
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Logic analyzer capture (Spec §13). Saleae is the primary path (Logic 2 MCP
|
|
4
|
+
in Qoder); this wrapper covers the sigrok fallback for non-Saleae hardware
|
|
5
|
+
and provides a deterministic SIMULATOR backend so the capture->decode->
|
|
6
|
+
assert pipeline can be exercised without any LA hardware.
|
|
7
|
+
|
|
8
|
+
Every capture saves (Spec §13): raw capture file, decoder config snapshot,
|
|
9
|
+
and a metadata JSON with the timestamp - under artifacts/captures/.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
.\tools\logic_capture.ps1 -Protocol spi -Json # simulator (default)
|
|
13
|
+
.\tools\logic_capture.ps1 -Protocol uart -Backend sigrok -Json
|
|
14
|
+
#>
|
|
15
|
+
[CmdletBinding()]
|
|
16
|
+
param(
|
|
17
|
+
[ValidateSet('spi', 'uart', 'i2c')]
|
|
18
|
+
[string]$Protocol = 'spi',
|
|
19
|
+
[ValidateSet('auto', 'simulator', 'sigrok')]
|
|
20
|
+
[string]$Backend = 'auto',
|
|
21
|
+
[double]$DurationSec = 0.002,
|
|
22
|
+
[int]$SampleRateHz = 1000000,
|
|
23
|
+
[string]$OutDir, # default: artifacts/captures/<stamp>
|
|
24
|
+
[switch]$Json
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
Set-StrictMode -Version Latest
|
|
28
|
+
$ErrorActionPreference = 'Stop'
|
|
29
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
30
|
+
|
|
31
|
+
$repoRoot = Get-FwRepoRoot
|
|
32
|
+
|
|
33
|
+
function Exit-WithError {
|
|
34
|
+
param([string]$Class, [string]$Message, [string]$Detail)
|
|
35
|
+
$body = New-FwError -ErrorClass $Class -Message $Message -Detail $Detail
|
|
36
|
+
if ($Json) { Write-FwJson $body -Compact } else { Write-FwJson $body; Write-Error "$Class : $Message" -ErrorAction Continue }
|
|
37
|
+
exit 2
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# ---- decide backend -----------------------------------------------------------
|
|
41
|
+
if ($Backend -eq 'auto') {
|
|
42
|
+
$g = Get-Command sigrok-cli -ErrorAction SilentlyContinue
|
|
43
|
+
$sigrok = if ($g) { $g.Source } else { $null }
|
|
44
|
+
$Backend = if ($sigrok) { 'sigrok' } else { 'simulator' }
|
|
45
|
+
}
|
|
46
|
+
if ($Backend -eq 'sigrok') {
|
|
47
|
+
$g = Get-Command sigrok-cli -ErrorAction SilentlyContinue
|
|
48
|
+
$sigrok = if ($g) { $g.Source } else { $null }
|
|
49
|
+
if (-not $sigrok) {
|
|
50
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message 'sigrok-cli not found; install sigrok or use -Backend simulator' -Detail 'https://sigrok.org/wiki/Downloads'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ---- output layout ------------------------------------------------------------
|
|
55
|
+
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
56
|
+
$runId = Get-FwRunId
|
|
57
|
+
if (-not $OutDir) { $OutDir = Join-Path $repoRoot "artifacts\captures\$runId" }
|
|
58
|
+
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
|
59
|
+
|
|
60
|
+
# decoder config snapshot (read from lab/protocol-decode.yaml if present)
|
|
61
|
+
$decodeCfg = Join-Path $repoRoot 'lab\protocol-decode.yaml'
|
|
62
|
+
$configOut = Join-Path $OutDir 'decoder-config.yaml'
|
|
63
|
+
if (Test-Path -LiteralPath $decodeCfg) {
|
|
64
|
+
Copy-Item -LiteralPath $decodeCfg -Destination $configOut -Force
|
|
65
|
+
} else {
|
|
66
|
+
Set-Content -LiteralPath $configOut -Value "# decoder config snapshot (protocol-decode.yaml not found at capture time)" -Encoding utf8
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$meta = [ordered]@{
|
|
70
|
+
schema = 'lab-logic-capture/v1'
|
|
71
|
+
ok = $true
|
|
72
|
+
run_id = $runId
|
|
73
|
+
protocol = $Protocol
|
|
74
|
+
backend = $Backend
|
|
75
|
+
duration_sec = $DurationSec
|
|
76
|
+
sample_rate_hz = $SampleRateHz
|
|
77
|
+
timestamp = Get-FwTimestamp
|
|
78
|
+
files = @()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ---- simulator waveform generation --------------------------------------------
|
|
82
|
+
if ($Backend -eq 'simulator') {
|
|
83
|
+
$rawFile = Join-Path $OutDir 'capture.csv'
|
|
84
|
+
$meta.protocol = $Protocol
|
|
85
|
+
switch ($Protocol) {
|
|
86
|
+
'spi' { $generator = 'SPI transaction: 0x9F read + 24-bit response 0xEF4018' }
|
|
87
|
+
'uart' { $generator = 'UART frame: "HIL" at 115200 8N1' }
|
|
88
|
+
'i2c' { $generator = 'I2C write to 0x50 then read 3 bytes' }
|
|
89
|
+
default { $generator = 'unknown' }
|
|
90
|
+
}
|
|
91
|
+
$rows = New-Object System.Collections.Generic.List[string]
|
|
92
|
+
$rows.Add('time_s,D0,D1,D2,D3')
|
|
93
|
+
|
|
94
|
+
function Add-Sample {
|
|
95
|
+
param([double]$T, [int[]]$Levels)
|
|
96
|
+
$rows.Add(('{0:F8},{1},{2},{3},{4}' -f $T, $Levels[0], $Levels[1], $Levels[2], $Levels[3]))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
$dt = 1.0 / $SampleRateHz
|
|
100
|
+
$t = 0.0
|
|
101
|
+
|
|
102
|
+
if ($Protocol -eq 'spi') {
|
|
103
|
+
# Channels: D0=SCLK, D1=MOSI, D2=MISO, D3=CS (low active)
|
|
104
|
+
# One bit = 1us at 1 MHz SCLK === 1 sample per half-bit at 1MHz? Use
|
|
105
|
+
# 2 samples per bit: low phase then high (rising edge sampled).
|
|
106
|
+
$bitUs = 2e-6 # 2us per bit -> 500 kHz SCLK
|
|
107
|
+
$bits = @() # command bits MSB first
|
|
108
|
+
foreach ($b in (0x9F)) { for ($i = 7; $i -ge 0; $i--) { $bits += ($b -shr $i) -band 1 } }
|
|
109
|
+
$resp = @(0xEF, 0x40, 0x18)
|
|
110
|
+
$respBits = @()
|
|
111
|
+
foreach ($bb in $resp) { for ($i = 7; $i -ge 0; $i--) { $respBits += ($bb -shr $i) -band 1 } }
|
|
112
|
+
|
|
113
|
+
# idle (CS high)
|
|
114
|
+
$idleSamples = [math]::Max(2, [int](0.00005 / $dt))
|
|
115
|
+
for ($i = 0; $i -lt $idleSamples; $i++) {
|
|
116
|
+
Add-Sample $t @(0, 0, 0, 1)
|
|
117
|
+
$t += $dt
|
|
118
|
+
}
|
|
119
|
+
# CS assert
|
|
120
|
+
$csLevels = @(0, 0, 0, 0)
|
|
121
|
+
for ($i = 0; $i -lt 2; $i++) { Add-Sample $t $csLevels; $t += $dt }
|
|
122
|
+
# command + response bits: first 8 bits drive MOSI, next 24 drive MISO
|
|
123
|
+
$allBits = $bits + $respBits
|
|
124
|
+
for ($idx = 0; $idx -lt $allBits.Count; $idx++) {
|
|
125
|
+
$bit = $allBits[$idx]
|
|
126
|
+
$isCmd = $idx -lt 8
|
|
127
|
+
$mosi = if ($isCmd) { $bit } else { 0 }
|
|
128
|
+
$miso = if ($isCmd) { 0 } else { $bit }
|
|
129
|
+
# low phase
|
|
130
|
+
for ($i = 0; $i -lt [int]($bitUs / 2 / $dt); $i++) {
|
|
131
|
+
if ($mosi -eq 1) { Add-Sample $t @(0, 1, $miso, 0) } else { Add-Sample $t @(0, 0, $miso, 0) }
|
|
132
|
+
$t += $dt
|
|
133
|
+
}
|
|
134
|
+
# high phase (rising edge; MISO valid here)
|
|
135
|
+
for ($i = 0; $i -lt [int]($bitUs / 2 / $dt); $i++) {
|
|
136
|
+
if ($mosi -eq 1) { Add-Sample $t @(1, 1, $miso, 0) } else { Add-Sample $t @(1, 0, $miso, 0) }
|
|
137
|
+
$t += $dt
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
# CS deassert
|
|
141
|
+
for ($i = 0; $i -lt 2; $i++) { Add-Sample $t @(0, 0, 0, 1); $t += $dt }
|
|
142
|
+
} elseif ($Protocol -eq 'uart') {
|
|
143
|
+
# Channels: D0=TX, D1=RX, D2=unused, D3=unused
|
|
144
|
+
$bitTime = 1.0 / 115200.0
|
|
145
|
+
function Add-Bit {
|
|
146
|
+
param([double]$TT, [int]$Level, [double]$BitLen)
|
|
147
|
+
$n = [math]::Max(1, [int]($BitLen / $dt))
|
|
148
|
+
for ($i = 0; $i -lt $n; $i++) {
|
|
149
|
+
Add-Sample $TT @($Level, 1, 0, 0)
|
|
150
|
+
$TT += $dt
|
|
151
|
+
}
|
|
152
|
+
return $TT
|
|
153
|
+
}
|
|
154
|
+
$payloadBytes = [System.Text.Encoding]::ASCII.GetBytes('HIL')
|
|
155
|
+
$idleN = [math]::Max(2, [int]((10 * $bitTime) / $dt))
|
|
156
|
+
for ($i = 0; $i -lt $idleN; $i++) { Add-Sample $t @(1, 1, 0, 0); $t += $dt }
|
|
157
|
+
foreach ($byte in $payloadBytes) {
|
|
158
|
+
$t = Add-Bit $t 0 $bitTime # start bit
|
|
159
|
+
for ($i = 0; $i -lt 8; $i++) { # LSB first
|
|
160
|
+
$t = Add-Bit $t (($byte -shr $i) -band 1) $bitTime
|
|
161
|
+
}
|
|
162
|
+
$t = Add-Bit $t 1 $bitTime # stop bit
|
|
163
|
+
# inter-frame gap (like software-driven UART transmit)
|
|
164
|
+
$gapN = [math]::Max(2, [int]((10 * $bitTime) / $dt))
|
|
165
|
+
for ($i = 0; $i -lt $gapN; $i++) { Add-Sample $t @(1, 1, 0, 0); $t += $dt }
|
|
166
|
+
}
|
|
167
|
+
} else { # i2c
|
|
168
|
+
# Channels: D0=SCL, D1=SDA; write transaction: START, addr 0xA0,
|
|
169
|
+
# reg 0x00 with ACKs, STOP.
|
|
170
|
+
$bitTime = 1.0 / 100000.0
|
|
171
|
+
function Add-I2CBit {
|
|
172
|
+
param([double]$TT, [int]$SdaLevel, [double]$BitLen)
|
|
173
|
+
$n = [math]::Max(1, [int]($BitLen / $dt))
|
|
174
|
+
for ($i = 0; $i -lt $n; $i++) {
|
|
175
|
+
Add-Sample $TT @(0, $SdaLevel, 0, 0)
|
|
176
|
+
$TT += $dt
|
|
177
|
+
}
|
|
178
|
+
# clock high
|
|
179
|
+
$nh = [math]::Max(1, [int]($BitLen / $dt))
|
|
180
|
+
for ($i = 0; $i -lt $nh; $i++) {
|
|
181
|
+
Add-Sample $TT @(1, $SdaLevel, 0, 0)
|
|
182
|
+
$TT += $dt
|
|
183
|
+
}
|
|
184
|
+
return $TT
|
|
185
|
+
}
|
|
186
|
+
function Add-I2CHold {
|
|
187
|
+
param([double]$TT, [int]$SdaLevel, [double]$Secs)
|
|
188
|
+
$n = [math]::Max(1, [int]($Secs / $dt))
|
|
189
|
+
for ($i = 0; $i -lt $n; $i++) {
|
|
190
|
+
Add-Sample $TT @(1, $SdaLevel, 0, 0) # SCL stays high
|
|
191
|
+
$TT += $dt
|
|
192
|
+
}
|
|
193
|
+
return $TT
|
|
194
|
+
}
|
|
195
|
+
$hold = $bitTime / 2
|
|
196
|
+
|
|
197
|
+
# idle (bus free: SCL high, SDA high)
|
|
198
|
+
$idleN = [math]::Max(2, [int]((4 * $bitTime) / $dt))
|
|
199
|
+
for ($i = 0; $i -lt $idleN; $i++) { Add-Sample $t @(1, 1, 0, 0); $t += $dt }
|
|
200
|
+
# START: SDA falls while SCL is high
|
|
201
|
+
$t = Add-I2CHold $t 0 $hold
|
|
202
|
+
Add-Sample $t @(1, 0, 0, 0); $t += $dt
|
|
203
|
+
$t = Add-I2CHold $t 0 $hold
|
|
204
|
+
# address + data bytes with ACK after each
|
|
205
|
+
foreach ($byte in @(0xA0, 0x00)) {
|
|
206
|
+
foreach ($i in 7..0) {
|
|
207
|
+
$t = Add-I2CBit $t (($byte -shr $i) -band 1) $bitTime
|
|
208
|
+
}
|
|
209
|
+
$t = Add-I2CBit $t 0 $bitTime # ACK
|
|
210
|
+
}
|
|
211
|
+
# STOP: SDA rises while SCL is high
|
|
212
|
+
$t = Add-I2CHold $t 0 $hold
|
|
213
|
+
Add-Sample $t @(1, 1, 0, 0); $t += $dt
|
|
214
|
+
$t = Add-I2CHold $t 1 $hold
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Set-Content -LiteralPath $rawFile -Value $rows -Encoding utf8
|
|
218
|
+
$meta.files += (Resolve-Path -LiteralPath $rawFile).Path
|
|
219
|
+
$meta.generator = $generator
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# ---- sigrok backend ------------------------------------------------------------
|
|
223
|
+
else {
|
|
224
|
+
$rawFile = Join-Path $OutDir 'capture.sr'
|
|
225
|
+
$probeArgs = @(
|
|
226
|
+
"--driver=auto",
|
|
227
|
+
"--config", "samplerate=$SampleRateHz",
|
|
228
|
+
"--channels", "0-7",
|
|
229
|
+
"--output-format", "binary",
|
|
230
|
+
"--output-file", $rawFile,
|
|
231
|
+
"--time", "$([int]([math]::Ceiling($DurationSec * 1000)))ms",
|
|
232
|
+
"$Protocol"
|
|
233
|
+
)
|
|
234
|
+
$res = Invoke-FwProcess -FilePath $sigrok -Arguments $probeArgs -WorkingDirectory $repoRoot -TimeoutMs 120000
|
|
235
|
+
if ($res.timed_out) {
|
|
236
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message 'sigrok-cli capture timed out.' -Detail $rawFile
|
|
237
|
+
}
|
|
238
|
+
if ($res.exit_code -ne 0 -or -not (Test-Path -LiteralPath $rawFile)) {
|
|
239
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message 'sigrok-cli capture failed.' -Detail ($res.stdout + $res.stderr)
|
|
240
|
+
}
|
|
241
|
+
$meta.files += (Resolve-Path -LiteralPath $rawFile).Path
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
$metaFile = Join-Path $OutDir 'capture.json'
|
|
245
|
+
$meta | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $metaFile -Encoding utf8
|
|
246
|
+
$meta.files += (Resolve-Path -LiteralPath $metaFile).Path
|
|
247
|
+
|
|
248
|
+
$result = [ordered]@{
|
|
249
|
+
schema = 'lab-logic-capture/v1'
|
|
250
|
+
ok = $true
|
|
251
|
+
protocol = $Protocol
|
|
252
|
+
backend = $Backend
|
|
253
|
+
capture = (Resolve-Path -LiteralPath $rawFile).Path
|
|
254
|
+
metadata = (Resolve-Path -LiteralPath $metaFile).Path
|
|
255
|
+
decoder_config = (Resolve-Path -LiteralPath $configOut).Path
|
|
256
|
+
files = @($meta.files)
|
|
257
|
+
run_id = $runId
|
|
258
|
+
timestamp = Get-FwTimestamp
|
|
259
|
+
}
|
|
260
|
+
if ($Json) { Write-FwJson ([pscustomobject]$result) -Compact } else { Write-FwJson ([pscustomobject]$result) }
|
|
261
|
+
exit 0
|
tools/logic_decode.ps1
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Logic decoder (Spec §13 fallback). Decodes a raw capture (CSV with a
|
|
4
|
+
time_s first column and one column per channel) into JSON frames using the
|
|
5
|
+
channel/protocol layout from lab/protocol-decode.yaml.
|
|
6
|
+
|
|
7
|
+
SPI : D0=SCLK, D1=MOSI, D2=MISO, D3=CS (low active), MSB first
|
|
8
|
+
UART : D0=TX (idle high), 8N1, LSB first, baud from config
|
|
9
|
+
I2C : D0=SCL, D1=SDA (basic byte/ACK extraction)
|
|
10
|
+
|
|
11
|
+
Every decoded result is saved next to the capture and carries the
|
|
12
|
+
timestamp. Optional -Expect/-ExpectKind asserts the result and exits 1
|
|
13
|
+
with TEST_FAILED on mismatch (for HIL assertions).
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
.\tools\logic_decode.ps1 -Capture artifacts\captures\20260817-000000 -Json
|
|
17
|
+
.\tools\logic_decode.ps1 -CaptureFile x.csv -Protocol spi -Expect EF4018 -ExpectKind hex -Json
|
|
18
|
+
#>
|
|
19
|
+
[CmdletBinding()]
|
|
20
|
+
param(
|
|
21
|
+
[string]$Capture, # capture dir containing capture.csv + capture.json
|
|
22
|
+
[string]$CaptureFile, # direct path to the raw CSV
|
|
23
|
+
[ValidateSet('spi', 'uart', 'i2c')]
|
|
24
|
+
[string]$Protocol, # override protocol (else read from metadata)
|
|
25
|
+
[string]$Expect, # expected decoded value (hex string or text)
|
|
26
|
+
[ValidateSet('hex', 'text')]
|
|
27
|
+
[string]$ExpectKind = 'hex',
|
|
28
|
+
[switch]$Json
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
Set-StrictMode -Version Latest
|
|
32
|
+
$ErrorActionPreference = 'Stop'
|
|
33
|
+
Import-Module (Join-Path $PSScriptRoot 'common\fw.psm1') -Force
|
|
34
|
+
|
|
35
|
+
$repoRoot = Get-FwRepoRoot
|
|
36
|
+
|
|
37
|
+
function Exit-WithError {
|
|
38
|
+
param([string]$Class, [string]$Message, [string]$Detail)
|
|
39
|
+
$body = New-FwError -ErrorClass $Class -Message $Message -Detail $Detail
|
|
40
|
+
if ($Json) { Write-FwJson $body -Compact } else { Write-FwJson $body; Write-Error "$Class : $Message" -ErrorAction Continue }
|
|
41
|
+
exit 2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ---- locate raw capture -------------------------------------------------------
|
|
45
|
+
$rawFile = $null
|
|
46
|
+
$meta = $null
|
|
47
|
+
if ($Capture) {
|
|
48
|
+
if (Test-Path -LiteralPath $Capture -PathType Container) {
|
|
49
|
+
$rawFile = Join-Path $Capture 'capture.csv'
|
|
50
|
+
$metaPath = Join-Path $Capture 'capture.json'
|
|
51
|
+
if (Test-Path -LiteralPath $metaPath) {
|
|
52
|
+
$meta = Get-Content -LiteralPath $metaPath -Raw | ConvertFrom-Json
|
|
53
|
+
}
|
|
54
|
+
if (-not (Test-Path -LiteralPath $rawFile)) {
|
|
55
|
+
$srFile = Join-Path $Capture 'capture.sr'
|
|
56
|
+
if (Test-Path -LiteralPath $srFile) {
|
|
57
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message 'sigrok binary capture needs sigrok-cli decode; export CSV from PulseView or pass -CaptureFile <csv>.' -Detail $srFile
|
|
58
|
+
}
|
|
59
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message 'no capture.csv or capture.sr found in capture dir.' -Detail $Capture
|
|
60
|
+
}
|
|
61
|
+
} elseif (Test-Path -LiteralPath $Capture -PathType Leaf) {
|
|
62
|
+
$rawFile = $Capture # direct file path (capture.csv)
|
|
63
|
+
$metaPath = Join-Path (Split-Path $Capture) 'capture.json'
|
|
64
|
+
if (Test-Path -LiteralPath $metaPath) {
|
|
65
|
+
$meta = Get-Content -LiteralPath $metaPath -Raw | ConvertFrom-Json
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
Exit-WithError -Class 'CONFIG_ERROR' -Message 'Capture path does not exist.' -Detail $Capture
|
|
69
|
+
}
|
|
70
|
+
} elseif ($CaptureFile) {
|
|
71
|
+
$rawFile = $CaptureFile
|
|
72
|
+
} else {
|
|
73
|
+
Exit-WithError -Class 'CONFIG_ERROR' -Message 'pass -Capture <dir> or -CaptureFile <csv>'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (-not (Test-Path -LiteralPath $rawFile)) {
|
|
77
|
+
Exit-WithError -Class 'ARTIFACT_NOT_FOUND' -Message 'capture file does not exist.' -Detail $rawFile
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# ---- protocol/config resolution ------------------------------------------------
|
|
81
|
+
$cfg = @{}
|
|
82
|
+
$cfgPath = Join-Path $repoRoot 'lab\protocol-decode.yaml'
|
|
83
|
+
# YAML is not natively parseable in PowerShell; the decoder only needs the
|
|
84
|
+
# baud rate for UART and channel mapping. Fall back to sane defaults.
|
|
85
|
+
if ($meta -and $meta.protocol) { $reqProtocol = [string]$meta.protocol } else { $reqProtocol = $null }
|
|
86
|
+
if ($Protocol) { $reqProtocol = $Protocol }
|
|
87
|
+
if (-not $reqProtocol) {
|
|
88
|
+
Exit-WithError -Class 'CONFIG_ERROR' -Message 'protocol unknown; pass -Protocol or run capture first' -Detail $metaPath
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$baud = 115200
|
|
92
|
+
if ($reqProtocol -eq 'uart' -and $meta) {
|
|
93
|
+
$bProp = $meta.PSObject.Properties['baud']
|
|
94
|
+
if ($bProp -and $bProp.Value) { $baud = [int]$bProp.Value }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# ---- load samples --------------------------------------------------------------
|
|
98
|
+
$rows = Import-Csv -LiteralPath $rawFile
|
|
99
|
+
if ($rows.Count -lt 3) {
|
|
100
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message 'capture has too few samples to decode.' -Detail $rawFile
|
|
101
|
+
}
|
|
102
|
+
$cols = $rows[0].PSObject.Properties.Name
|
|
103
|
+
foreach ($c in @('time_s')) { if ($c -notin $cols) { Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message "missing column '$c' in capture" -Detail $rawFile } }
|
|
104
|
+
$ch = @{}
|
|
105
|
+
foreach ($c in $cols) { if ($c -ne 'time_s') { $ch[$c] = $true } }
|
|
106
|
+
|
|
107
|
+
function Get-Ch {
|
|
108
|
+
param([int]$Index)
|
|
109
|
+
$names = @($cols | Where-Object { $_ -ne 'time_s' })
|
|
110
|
+
if ($Index -ge $names.Count) { return $null }
|
|
111
|
+
return $names[$Index]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# normalize levels (CSV values may be 0/1, true/false, high/low)
|
|
115
|
+
function Lev([string]$v) {
|
|
116
|
+
$s = $v.Trim().ToLowerInvariant()
|
|
117
|
+
if ($s -in @('1', 'true', 'high', 'h')) { return 1 }
|
|
118
|
+
if ($s -in @('0', 'false', 'low', 'l')) { return 0 }
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# ---- decoders -------------------------------------------------------------------
|
|
123
|
+
$frames = @()
|
|
124
|
+
$bytes = @()
|
|
125
|
+
$text = ''
|
|
126
|
+
|
|
127
|
+
switch ($reqProtocol) {
|
|
128
|
+
'spi' {
|
|
129
|
+
$cClk = Get-Ch 0; $cMosi = Get-Ch 1; $cMiso = Get-Ch 2; $cCs = Get-Ch 3
|
|
130
|
+
if (-not ($cClk -and $cMosi -and $cMiso -and $cCs)) {
|
|
131
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message "SPI needs 4 channels (SCLK,MOSI,MISO,CS); found: $($cols -join ',')"
|
|
132
|
+
}
|
|
133
|
+
$inWindow = $false
|
|
134
|
+
$cmdBits = @(); $respBits = @(); $prevClk = 0
|
|
135
|
+
foreach ($r in $rows) {
|
|
136
|
+
$cs = Lev $r.$cCs
|
|
137
|
+
if (-not $inWindow -and $cs -eq 0) { $inWindow = $true; $cmdBits = @(); $respBits = @() }
|
|
138
|
+
if ($inWindow -and $cs -eq 1) { break } # window closed
|
|
139
|
+
if ($inWindow) {
|
|
140
|
+
$clk = Lev $r.$cClk
|
|
141
|
+
if ($clk -eq 1 -and $prevClk -eq 0) { # rising edge
|
|
142
|
+
$mosi = Lev $r.$cMosi
|
|
143
|
+
$miso = Lev $r.$cMiso
|
|
144
|
+
if ($cmdBits.Count + $respBits.Count -lt 8) {
|
|
145
|
+
$cmdBits += $mosi
|
|
146
|
+
} else {
|
|
147
|
+
$respBits += $miso
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
$prevClk = $clk
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ($cmdBits.Count + $respBits.Count -ne 32) {
|
|
154
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message "expected 32 SPI edges (8 cmd + 24 resp), got $($cmdBits.Count + $respBits.Count)" -Detail $rawFile
|
|
155
|
+
}
|
|
156
|
+
$cmdByte = 0; foreach ($b in $cmdBits) { $cmdByte = ($cmdByte -shl 1) -bor $b }
|
|
157
|
+
$respBytes = @()
|
|
158
|
+
for ($k = 0; $k -lt 24; $k += 8) {
|
|
159
|
+
$v = 0
|
|
160
|
+
for ($j = 0; $j -lt 8; $j++) { $v = ($v -shl 1) -bor $respBits[$k + $j] }
|
|
161
|
+
$respBytes += $v
|
|
162
|
+
}
|
|
163
|
+
$bytes = @($cmdByte) + $respBytes
|
|
164
|
+
$frames = @([ordered]@{
|
|
165
|
+
type = 'command'; bytes_hex = ('0x{0:X2}' -f $cmdByte)
|
|
166
|
+
}, [ordered]@{
|
|
167
|
+
type = 'response'; bytes_hex = (($respBytes | ForEach-Object { '0x{0:X2}' -f $_ }) -join ' ')
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
'uart' {
|
|
172
|
+
$cTx = Get-Ch 0
|
|
173
|
+
if (-not $cTx) { Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message "UART needs a TX channel (D0); found: $($cols -join ',')" }
|
|
174
|
+
$samples = @($rows | ForEach-Object { Lev $_.$cTx })
|
|
175
|
+
# Estimate bit width from the MODE of adjacent edge spacings - robust
|
|
176
|
+
# against data bits that are low for more than one bit (e.g. 0x48).
|
|
177
|
+
$edges = @()
|
|
178
|
+
for ($k = 0; $k -lt $samples.Count - 1; $k++) {
|
|
179
|
+
if ($samples[$k] -ne $samples[$k + 1]) { $edges += $k + 1 }
|
|
180
|
+
}
|
|
181
|
+
$spacings = @()
|
|
182
|
+
for ($k = 1; $k -lt $edges.Count; $k++) {
|
|
183
|
+
$spacings += ($edges[$k] - $edges[$k - 1])
|
|
184
|
+
}
|
|
185
|
+
if ($spacings.Count -eq 0) {
|
|
186
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message 'no edges found on UART TX channel; is the capture from a real UART?'
|
|
187
|
+
}
|
|
188
|
+
$bitSamples = ($spacings | Group-Object | Sort-Object Count -Descending | Select-Object -First 1).Name
|
|
189
|
+
$bitSamples = [math]::Max(2, [int]$bitSamples)
|
|
190
|
+
|
|
191
|
+
$i = 0
|
|
192
|
+
while ($i -lt $samples.Count - 1) {
|
|
193
|
+
# Frame boundary: falling edge preceded by >= 3 bits of idle high.
|
|
194
|
+
# A real start bit has stop + inter-frame idle before it; mid-data
|
|
195
|
+
# runs of 1s are at most 2 bits before a data falling edge.
|
|
196
|
+
$preHigh = $true
|
|
197
|
+
for ($c = 0; $c -lt 3 * $bitSamples; $c++) {
|
|
198
|
+
$pi = $i - $c
|
|
199
|
+
if ($pi -lt 0) { $preHigh = $false; break }
|
|
200
|
+
if ($samples[$pi] -ne 1) { $preHigh = $false; break }
|
|
201
|
+
}
|
|
202
|
+
if ($samples[$i] -eq 1 -and $samples[$i + 1] -eq 0 -and $preHigh) {
|
|
203
|
+
$collected = @()
|
|
204
|
+
for ($b = 0; $b -lt 8; $b++) {
|
|
205
|
+
$idx = $i + 1 + [int]((1.5 + $b) * $bitSamples)
|
|
206
|
+
$bitVal = if ($idx -lt $samples.Count) { $samples[$idx] } else { 1 }
|
|
207
|
+
$collected += $bitVal # LSB first
|
|
208
|
+
}
|
|
209
|
+
# stop bit must be high (idle level) for a valid UART frame
|
|
210
|
+
$stopIdx = $i + 1 + [int](10.0 * $bitSamples)
|
|
211
|
+
$stopOk = ($stopIdx -lt $samples.Count) -and ($samples[$stopIdx] -eq 1)
|
|
212
|
+
if (-not $stopOk) {
|
|
213
|
+
$i++ # not a real frame boundary (mid-data flip); keep scanning
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
$val = 0
|
|
217
|
+
for ($b = 7; $b -ge 0; $b--) { $val = ($val -shl 1) -bor $collected[$b] }
|
|
218
|
+
$bytes += $val
|
|
219
|
+
$frames += [ordered]@{ type = 'frame'; bytes_hex = '0x{0:X2}' -f $val }
|
|
220
|
+
$i += $bitSamples # advance into the frame interior; the scan
|
|
221
|
+
# re-hits the next boundary via the idle-high precondition
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
$i++
|
|
225
|
+
}
|
|
226
|
+
$text = -join ($bytes | ForEach-Object { [char]$_ })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
'i2c' {
|
|
230
|
+
$cScl = Get-Ch 0; $cSda = Get-Ch 1
|
|
231
|
+
if (-not ($cScl -and $cSda)) {
|
|
232
|
+
Exit-WithError -Class 'LOGIC_CAPTURE_ERROR' -Message "I2C needs SCL (D0) and SDA (D1); found: $($cols -join ',')"
|
|
233
|
+
}
|
|
234
|
+
# Collect the SDA value at every SCL rising edge; one byte = 8 data
|
|
235
|
+
# bits + 1 ACK. START (SDA falls while SCL high) opens the stream.
|
|
236
|
+
$prevScl = 0
|
|
237
|
+
$bits = [System.Collections.Generic.List[int]]::new()
|
|
238
|
+
$inStream = $false
|
|
239
|
+
foreach ($r in $rows) {
|
|
240
|
+
$scl = Lev $r.$cScl
|
|
241
|
+
$sda = Lev $r.$cSda
|
|
242
|
+
if ($scl -eq 1 -and $prevScl -eq 0) { # rising edge
|
|
243
|
+
if ($inStream) { $bits.Add($sda) }
|
|
244
|
+
} elseif ($scl -eq 1 -and $prevScl -eq 1) {
|
|
245
|
+
# SCL high window: SDA falling => START, rising => STOP
|
|
246
|
+
if ($lastSclHighSda -eq 1 -and $sda -eq 0) { $inStream = $true; $bits.Clear() }
|
|
247
|
+
elseif ($lastSclHighSda -eq 0 -and $sda -eq 1) { $inStream = $false }
|
|
248
|
+
$lastSclHighSda = $sda
|
|
249
|
+
}
|
|
250
|
+
$prevScl = $scl
|
|
251
|
+
if ($scl -eq 1) { $lastSclHighSda = $sda }
|
|
252
|
+
}
|
|
253
|
+
# group 8-bit chunks (skip the ACK bit every 9th sample)
|
|
254
|
+
$payload = [System.Collections.Generic.List[int]]::new()
|
|
255
|
+
$idx = 0
|
|
256
|
+
while ($idx + 8 -le $bits.Count) {
|
|
257
|
+
$v = 0
|
|
258
|
+
for ($b = 0; $b -lt 8; $b++) { $v = ($v -shl 1) -bor $bits[$idx + $b] }
|
|
259
|
+
$payload.Add($v)
|
|
260
|
+
$idx += 9 # skip ACK
|
|
261
|
+
}
|
|
262
|
+
foreach ($v in $payload) {
|
|
263
|
+
$bytes += $v
|
|
264
|
+
$frames += [ordered]@{ type = 'byte'; bytes_hex = '0x{0:X2}' -f $v }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
$hexStr = (($bytes | ForEach-Object { '{0:X2}' -f $_ }) -join '')
|
|
270
|
+
$outDir = Split-Path -Parent (Resolve-Path -LiteralPath $rawFile).Path
|
|
271
|
+
$outJson = Join-Path $outDir 'decoded.json'
|
|
272
|
+
|
|
273
|
+
$decoded = [ordered]@{
|
|
274
|
+
schema = 'lab-logic-decode/v1'
|
|
275
|
+
ok = $true
|
|
276
|
+
protocol = $reqProtocol
|
|
277
|
+
source = (Resolve-Path -LiteralPath $rawFile).Path
|
|
278
|
+
bytes_hex = $hexStr
|
|
279
|
+
text = $text
|
|
280
|
+
frames = $frames
|
|
281
|
+
timestamp = Get-FwTimestamp
|
|
282
|
+
}
|
|
283
|
+
$decoded | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $outJson -Encoding utf8
|
|
284
|
+
$decoded.out_file = (Resolve-Path -LiteralPath $outJson).Path
|
|
285
|
+
|
|
286
|
+
# ---- assertion ----------------------------------------------------------------
|
|
287
|
+
if ($Expect) {
|
|
288
|
+
$actual = if ($ExpectKind -eq 'hex') { $hexStr } else { $text }
|
|
289
|
+
if ($actual -ne $Expect) {
|
|
290
|
+
$body = [ordered]@{
|
|
291
|
+
schema = 'lab-logic-decode/v1'
|
|
292
|
+
ok = $false
|
|
293
|
+
error_class = 'TEST_FAILED'
|
|
294
|
+
error = 'decoded value did not match expectation'
|
|
295
|
+
protocol = $reqProtocol
|
|
296
|
+
expected = $Expect
|
|
297
|
+
actual = $actual
|
|
298
|
+
decoded_file = $outJson
|
|
299
|
+
timestamp = Get-FwTimestamp
|
|
300
|
+
}
|
|
301
|
+
if ($Json) { Write-FwJson ([pscustomobject]$body) -Compact } else { Write-FwJson ([pscustomobject]$body) }
|
|
302
|
+
exit 1
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if ($Json) { Write-FwJson ([pscustomobject]$decoded) -Compact } else { Write-FwJson ([pscustomobject]$decoded) }
|
|
307
|
+
exit 0
|