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 & '' -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 \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 = '' ``` 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 = CAPTURE PATH = CAPTURE START TIME = EXPECTED END TIME = 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 '' ` --iq-format cs8 ` --truth-metadata data/raw/rf001/rf001a_common_clock_001/tx_metadata.json ` --report '\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.