386 lines
13 KiB
Markdown
386 lines
13 KiB
Markdown
TASK_ID = RF001A_OTA_CAPTURE_001
|
|
TASK_TITLE = Capture first RF-001A QPSK OTA transmission
|
|
SOURCE_BRANCH = video-development
|
|
EXPECTED_HEAD = 1e3aa25d7007c7c8e84608cddb02f06f77e63396
|
|
REQUIRES_APPROVAL = YES
|
|
HARDWARE_ACCESS = YES
|
|
RF_RX = YES
|
|
RF_TX = NO
|
|
ALLOW_COMMIT = NO
|
|
ALLOW_PUSH = RX_RESULT_ONLY
|
|
RISK_LEVEL = YELLOW
|
|
|
|
This file is the authoritative current task for the RX laptop.
|
|
The RX assistant must read RX_NODE_STATE.md first, then execute this file.
|
|
If chat history conflicts with these files, the shared RF-001A spec or current Git HEAD, stop and report the conflict.
|
|
|
|
# Current RX task
|
|
|
|
TASK =
|
|
CAPTURE FIRST RF-001A QPSK OTA TRANSMISSION
|
|
|
|
This is a new YELLOW task. Do not reuse approval or completion state from any earlier task ID.
|
|
|
|
## Accepted preflight evidence
|
|
|
|
The user supplied these completed preflight results:
|
|
|
|
```text
|
|
COMMON CLOCK VERIFIED = YES
|
|
CLKIN BEFORE SHORT RX = DETECTED
|
|
SHORT RX = PASS, 65536 samples, overruns 0
|
|
CLKIN AFTER SHORT RX = DETECTED
|
|
CLKIN AFTER 20 s CAPTURE = DETECTED
|
|
CAPTURE SIZE = 320000000 bytes
|
|
CAPTURE DURATION = 20.00486 s
|
|
RX OVERRUNS = 0
|
|
USB ERRORS = 0
|
|
FILE ERRORS = 0
|
|
RAW IQ SHA256 = c0abdc2a41a0265a8037b4a5296d20b843eb75bcee83a6f2fac5813a533b7147
|
|
RX HARDWARE PREFLIGHT = PASS WITH PROCESS-EXIT-CODE OBSERVABILITY LIMITATION
|
|
```
|
|
|
|
The exact child process exit code was not retained. Do not repeat the 20-second preflight solely to recover that missing value. This task fixes exit-code observability for the new OTA capture.
|
|
|
|
## 1. Preconditions
|
|
|
|
After one explicit `[A] Approve` for this task:
|
|
|
|
1. Run `git pull --ff-only origin video-development`.
|
|
2. Require branch `video-development`.
|
|
3. Require `git status --short --untracked-files=no` to produce no output.
|
|
4. Require local HEAD to equal `origin/video-development` and `EXPECTED_HEAD` to be its ancestor.
|
|
5. Run `hackrf_info` and preserve complete stdout.
|
|
6. Require RX serial `000000000000000097c063dc2e5327d3`.
|
|
7. Run the official read-only clock check and preserve complete stdout:
|
|
|
|
```powershell
|
|
& '<verified hackrf_clock.exe path>' -d 000000000000000097c063dc2e5327d3 -i
|
|
```
|
|
|
|
Require the exact line `CLKIN status: clock signal detected`. If it is absent, do not start the capture; keep `RF TX = 0` and STOP. Do not reconfigure the clock.
|
|
|
|
Create a new unique capture directory outside the Git checkout. Refuse any existing directory and never overwrite an earlier capture. Before starting, require at least `1100000000` free bytes on the target volume and record the measured free-byte count.
|
|
|
|
## 2. Authorized finite RX capture
|
|
|
|
Only this capture is authorized:
|
|
|
|
```text
|
|
RX serial = 000000000000000097c063dc2e5327d3
|
|
center frequency = 1400000000 Hz
|
|
sample rate = 8000000 samples/s
|
|
RX LNA = 24 dB
|
|
RX VGA = 24 dB
|
|
RF amp = OFF
|
|
antenna bias = OFF
|
|
duration = 60 s
|
|
sample count = 480000000 complex samples
|
|
hardware format = cs8
|
|
expected file size = 960000000 bytes
|
|
common clock = DETECTED
|
|
```
|
|
|
|
The required `hackrf_transfer.exe` arguments are:
|
|
|
|
```text
|
|
-d 000000000000000097c063dc2e5327d3
|
|
-r <unique_capture_path>\rf001a_ota_capture.cs8
|
|
-f 1400000000
|
|
-s 8000000
|
|
-n 480000000
|
|
-l 24
|
|
-g 24
|
|
-a 0
|
|
-p 0
|
|
-B
|
|
```
|
|
|
|
## 3. Durable process and exit-code recording
|
|
|
|
Use a local runner outside Git. The runner owns the `hackrf_transfer` process, records its actual PID immediately, waits for that exact child, and writes the exact child `ExitCode` to an atomic result JSON file. This remains observable even if the assistant turn or PowerShell parent that started the runner ends.
|
|
|
|
Prepare paths:
|
|
|
|
```powershell
|
|
$runId = '{0}_{1}' -f (Get-Date -Format 'yyyyMMdd_HHmmss'), ([guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
$captureRoot = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'SDR_Rover_Captures'
|
|
$captureDirectory = Join-Path $captureRoot "RF001A_OTA_CAPTURE_001_$runId"
|
|
if (Test-Path -LiteralPath $captureDirectory) { throw "Capture directory already exists: $captureDirectory" }
|
|
New-Item -ItemType Directory -Path $captureDirectory -ErrorAction Stop | Out-Null
|
|
|
|
$targetRoot = [System.IO.Path]::GetPathRoot([System.IO.Path]::GetFullPath($captureDirectory))
|
|
$targetDrive = Get-PSDrive -Name $targetRoot.Substring(0, 1)
|
|
if ($targetDrive.Free -lt 1100000000) { throw "Insufficient free space: $($targetDrive.Free) bytes" }
|
|
|
|
$capturePath = Join-Path $captureDirectory 'rf001a_ota_capture.cs8'
|
|
$stdoutPath = Join-Path $captureDirectory 'hackrf_transfer.stdout.log'
|
|
$stderrPath = Join-Path $captureDirectory 'hackrf_transfer.stderr.log'
|
|
$startedStatusPath = Join-Path $captureDirectory 'capture_started.json'
|
|
$resultStatusPath = Join-Path $captureDirectory 'capture_result.json'
|
|
$runnerPath = Join-Path $captureDirectory 'capture_runner.ps1'
|
|
$runnerStdoutPath = Join-Path $captureDirectory 'capture_runner.stdout.log'
|
|
$runnerStderrPath = Join-Path $captureDirectory 'capture_runner.stderr.log'
|
|
$hackrfTransfer = '<verified absolute path to hackrf_transfer.exe>'
|
|
```
|
|
|
|
Write this runner to `$runnerPath`:
|
|
|
|
```powershell
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$HackrfTransfer,
|
|
[Parameter(Mandatory = $true)][string]$CapturePath,
|
|
[Parameter(Mandatory = $true)][string]$StdoutPath,
|
|
[Parameter(Mandatory = $true)][string]$StderrPath,
|
|
[Parameter(Mandatory = $true)][string]$StartedStatusPath,
|
|
[Parameter(Mandatory = $true)][string]$ResultStatusPath
|
|
)
|
|
|
|
function Write-JsonAtomically {
|
|
param([Parameter(Mandatory = $true)][object]$Value,
|
|
[Parameter(Mandatory = $true)][string]$Path)
|
|
$temporaryPath = "$Path.tmp"
|
|
$Value | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $temporaryPath -Encoding Ascii
|
|
Move-Item -LiteralPath $temporaryPath -Destination $Path -Force
|
|
}
|
|
|
|
$arguments = @(
|
|
'-d', '000000000000000097c063dc2e5327d3',
|
|
'-r', $CapturePath,
|
|
'-f', '1400000000',
|
|
'-s', '8000000',
|
|
'-n', '480000000',
|
|
'-l', '24',
|
|
'-g', '24',
|
|
'-a', '0',
|
|
'-p', '0',
|
|
'-B'
|
|
)
|
|
|
|
$startedAt = [DateTimeOffset]::Now
|
|
$transferProcess = $null
|
|
$exitCode = $null
|
|
$runnerError = $null
|
|
|
|
try {
|
|
$transferProcess = Start-Process -FilePath $HackrfTransfer -ArgumentList $arguments `
|
|
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath `
|
|
-PassThru -WindowStyle Hidden
|
|
|
|
Write-JsonAtomically -Path $StartedStatusPath -Value ([ordered]@{
|
|
state = 'RUNNING'
|
|
capture_pid = $transferProcess.Id
|
|
started_at = $startedAt.ToString('o')
|
|
capture_path = $CapturePath
|
|
})
|
|
|
|
$transferProcess.WaitForExit()
|
|
$transferProcess.Refresh()
|
|
$exitCode = $transferProcess.ExitCode
|
|
} catch {
|
|
$runnerError = $_.Exception.ToString()
|
|
} finally {
|
|
$finishedAt = [DateTimeOffset]::Now
|
|
$captureSize = if (Test-Path -LiteralPath $CapturePath) {
|
|
(Get-Item -LiteralPath $CapturePath).Length
|
|
} else {
|
|
0
|
|
}
|
|
|
|
$sha256 = $null
|
|
try {
|
|
if (Test-Path -LiteralPath $CapturePath) {
|
|
$sha256 = (Get-FileHash -LiteralPath $CapturePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
}
|
|
} catch {
|
|
$errorParts = @($runnerError, $_.Exception.ToString()) | Where-Object { $null -ne $_ -and $_ -ne '' }
|
|
$runnerError = $errorParts -join [Environment]::NewLine
|
|
}
|
|
|
|
Write-JsonAtomically -Path $ResultStatusPath -Value ([ordered]@{
|
|
state = 'COMPLETE'
|
|
capture_pid = if ($null -ne $transferProcess) { $transferProcess.Id } else { $null }
|
|
started_at = $startedAt.ToString('o')
|
|
finished_at = $finishedAt.ToString('o')
|
|
duration_seconds = ($finishedAt - $startedAt).TotalSeconds
|
|
exit_code = $exitCode
|
|
capture_path = $CapturePath
|
|
capture_size_bytes = $captureSize
|
|
sha256 = $sha256
|
|
runner_error = $runnerError
|
|
})
|
|
}
|
|
|
|
if ($null -eq $exitCode) { exit 255 }
|
|
exit $exitCode
|
|
```
|
|
|
|
Start the runner as a controlled background process. Quote every path passed through `-ArgumentList`:
|
|
|
|
```powershell
|
|
$currentPowerShell = (Get-Process -Id $PID).Path
|
|
$runnerArguments = @(
|
|
'-NoLogo',
|
|
'-NoProfile',
|
|
'-NonInteractive',
|
|
'-ExecutionPolicy', 'Bypass',
|
|
'-File', ('"{0}"' -f $runnerPath),
|
|
'-HackrfTransfer', ('"{0}"' -f $hackrfTransfer),
|
|
'-CapturePath', ('"{0}"' -f $capturePath),
|
|
'-StdoutPath', ('"{0}"' -f $stdoutPath),
|
|
'-StderrPath', ('"{0}"' -f $stderrPath),
|
|
'-StartedStatusPath', ('"{0}"' -f $startedStatusPath),
|
|
'-ResultStatusPath', ('"{0}"' -f $resultStatusPath)
|
|
)
|
|
$runnerProcess = Start-Process -FilePath $currentPowerShell -ArgumentList $runnerArguments `
|
|
-RedirectStandardOutput $runnerStdoutPath -RedirectStandardError $runnerStderrPath `
|
|
-PassThru -WindowStyle Hidden
|
|
```
|
|
|
|
Wait only long enough to prove that the child process is alive and the file is growing:
|
|
|
|
```powershell
|
|
$startDeadline = (Get-Date).AddSeconds(5)
|
|
$startedStatus = $null
|
|
$captureBytes = 0
|
|
$captureAlive = $false
|
|
|
|
do {
|
|
Start-Sleep -Milliseconds 100
|
|
$runnerProcess.Refresh()
|
|
if (Test-Path -LiteralPath $startedStatusPath) {
|
|
$startedStatus = Get-Content -Raw -LiteralPath $startedStatusPath | ConvertFrom-Json
|
|
$captureProcess = Get-Process -Id ([int]$startedStatus.capture_pid) -ErrorAction SilentlyContinue
|
|
$captureAlive = $null -ne $captureProcess
|
|
}
|
|
if (Test-Path -LiteralPath $capturePath) {
|
|
$captureBytes = (Get-Item -LiteralPath $capturePath).Length
|
|
}
|
|
} until ($runnerProcess.HasExited -or ($captureAlive -and $captureBytes -gt 0) -or (Get-Date) -ge $startDeadline)
|
|
```
|
|
|
|
The start passes only when:
|
|
|
|
```text
|
|
runner process alive = YES
|
|
hackrf_transfer child alive = YES
|
|
capture file created = YES
|
|
capture file growing = YES
|
|
```
|
|
|
|
If any check fails, report `RX CAPTURE STARTED = NO`, preserve all local files, keep `RF TX = 0`, and STOP.
|
|
|
|
## 4. Immediate READY report
|
|
|
|
When all start checks pass, immediately return control to the user with:
|
|
|
|
```text
|
|
TASK_ID =
|
|
RF001A_OTA_CAPTURE_001
|
|
|
|
RX CAPTURE STARTED =
|
|
YES
|
|
|
|
RX READY FOR TX =
|
|
YES
|
|
|
|
CAPTURE PID =
|
|
<actual hackrf_transfer child PID>
|
|
|
|
CAPTURE PATH =
|
|
<absolute path>
|
|
|
|
CAPTURE START TIME =
|
|
<started_status started_at>
|
|
|
|
EXPECTED END TIME =
|
|
<started_at plus 60 seconds>
|
|
|
|
RF TX =
|
|
0
|
|
```
|
|
|
|
Do not wait for the 60-second capture to finish before sending `RX READY FOR TX = YES`. This status means only that RX is actively recording. It does not authorize RF TX on the RX laptop or MAIN PC.
|
|
|
|
## 5. Completion evidence
|
|
|
|
After the capture window, read `capture_result.json`. The exact child process exit code must be present. Also preserve complete stdout, stderr and runner logs.
|
|
|
|
Require and report:
|
|
|
|
- process exit code;
|
|
- actual capture size;
|
|
- actual duration;
|
|
- SHA256;
|
|
- every RX overrun counter or event from the complete logs;
|
|
- every USB error;
|
|
- every file write error;
|
|
- CLKIN status after capture.
|
|
|
|
Run the read-only clock check again. Require `CLKIN status: clock signal detected`.
|
|
|
|
The capture is complete only when:
|
|
|
|
```text
|
|
exit code = 0
|
|
capture size = 960000000 bytes
|
|
duration = consistent with 60 s
|
|
RX overruns = 0
|
|
USB errors = 0
|
|
file write errors = 0
|
|
CLKIN after capture = DETECTED
|
|
```
|
|
|
|
Do not infer zero from a missing counter. Use `NOT_REPORTED` when the tool did not report a value. Treat every nonzero intermediate or final overrun/error line as evidence; do not keep only the last counter. Do not retry automatically.
|
|
|
|
## 6. Offline decode
|
|
|
|
Use the saved OTA IQ without changing the receiver for this capture:
|
|
|
|
```powershell
|
|
python -B -X utf8 tools/qpsk_rf_rx.py `
|
|
--iq-file '<absolute capture path>' `
|
|
--iq-format cs8 `
|
|
--truth-metadata data/raw/rf001/rf001a_common_clock_001/tx_metadata.json `
|
|
--report '<new unique path outside Git>\rf001a_ota_decode.json'
|
|
```
|
|
|
|
Require the existing truth metadata for the transmitted waveform. If it is absent or inconsistent, preserve the IQ file and report `OFFLINE DECODE = BLOCKED_BY_TRUTH_METADATA`; do not alter the receiver or invent results.
|
|
|
|
Report at minimum:
|
|
|
|
```text
|
|
DETECTED PREAMBLES =
|
|
CRC VALID PACKETS =
|
|
EXACT PACKETS =
|
|
MISSING PACKETS =
|
|
DUPLICATES =
|
|
OUT OF ORDER =
|
|
RAW BER =
|
|
RAW BER SCOPE =
|
|
POST-FEC ERROR RATE =
|
|
CFO MEDIAN =
|
|
CFO P95 =
|
|
TIMING OFFSET =
|
|
TIMING DRIFT =
|
|
RX CLIPPING =
|
|
```
|
|
|
|
Use the existing receiver JSON fields directly. Derive `OUT OF ORDER` only from `packet_sequences`. Derive timing summaries only from `phase_and_timing_estimates`; if the available data does not support a value, report `NOT_MEASURED`. Do not fabricate a metric.
|
|
|
|
Classify this first smoke test:
|
|
|
|
```text
|
|
99-100 exact = GOOD
|
|
95-98 exact = PARTIAL / DIAGNOSTIC
|
|
<95 exact = FAIL FOR VIDEO PROGRESSION
|
|
```
|
|
|
|
One run is not statistical reliability evidence.
|
|
|
|
## 7. Prohibited actions
|
|
|
|
On the RX laptop, do not perform RF TX, firmware or CPLD updates, Zadig operations, USB driver changes, clock reconfiguration, continuous RX, automatic capture retries, commits to `video-development`, or pushes to `video-development`.
|
|
|
|
`ALLOW_PUSH = RX_RESULT_ONLY` permits only a separately approved small result publication under the handoff protocol. Raw IQ, large logs and local runner files remain outside Git.
|