13 KiB
TASK_ID = RF001A_OTA_CAPTURE_001
TASK_TITLE = Capture first RF-001A QPSK OTA transmission
SOURCE_BRANCH = video-development
EXPECTED_HEAD = 1e3aa25d70
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:
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:
- Run
git pull --ff-only origin video-development. - Require branch
video-development. - Require
git status --short --untracked-files=noto produce no output. - Require local HEAD to equal
origin/video-developmentandEXPECTED_HEADto be its ancestor. - Run
hackrf_infoand preserve complete stdout. - Require RX serial
000000000000000097c063dc2e5327d3. - Run the official read-only clock check and preserve complete stdout:
& '<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:
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:
-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:
$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:
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:
$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:
$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:
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:
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:
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:
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:
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:
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.