Files
SDR-Rover/docs/handoff/NEXT_RX_TASK.md
2026-09-21 15:55:49 +03:00

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.