Prepare first RF-001A OTA run

This commit is contained in:
LittleSam129
2026-09-21 15:55:49 +03:00
parent 1e3aa25d70
commit 34f5ace545
2 changed files with 472 additions and 201 deletions

View File

@@ -1,7 +1,7 @@
TASK_ID = RF001A_VERIFY_AND_CAPTURE_001
TASK_TITLE = Verify RF-001A common clock and start IQ capture
TASK_ID = RF001A_OTA_CAPTURE_001
TASK_TITLE = Capture first RF-001A QPSK OTA transmission
SOURCE_BRANCH = video-development
EXPECTED_HEAD = fa370ccfa23dedc633346d688547e9c1c77ff8eb
EXPECTED_HEAD = 1e3aa25d7007c7c8e84608cddb02f06f77e63396
REQUIRES_APPROVAL = YES
HARDWARE_ACCESS = YES
RF_RX = YES
@@ -12,95 +12,59 @@ 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 and current Git HEAD, stop and report the conflict.
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 =
VERIFY RF-001A COMMON CLOCK AND START IQ CAPTURE
CAPTURE FIRST RF-001A QPSK OTA TRANSMISSION
This is one combined YELLOW task. One explicit `[A] Approve` authorizes the bounded CLKIN verification, the bounded short RX-start and, only after that gate passes, the finite 20-second RX capture. Do not request a second approval between those steps.
This is a new YELLOW task. Do not reuse approval or completion state from any earlier task ID.
Do not reuse the approval, state or completion record from either old task ID:
## Accepted preflight evidence
The user supplied these completed preflight results:
```text
RF001A_CLKIN_001
RF001A_CAPTURE_001
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 approval, run `git pull --ff-only origin video-development`, then verify:
After one explicit `[A] Approve` for this task:
- branch = `video-development`;
- `git status --short --untracked-files=no` produces no output;
- local HEAD equals `origin/video-development` and `EXPECTED_HEAD` is its ancestor;
- `hackrf_info` = PASS;
- RX serial = `000000000000000097c063dc2e5327d3`;
- the physical cable is connected from MASTER CLKOUT to SLAVE CLKIN;
- the existing hardware-independent shared RF-001A spec test passes:
```powershell
python -B -m pytest tests/test_rf001a_prepare.py -q -p no:cacheprovider --basetemp .local/pytest-rf001a-verify-capture
```
If any precondition fails, do not start the short RX or the 20-second capture. Report the failed condition, keep `RF TX = 0`, and STOP.
## 2. Verify SLAVE CLKIN before RX
Run the official read-only partner-side check with the explicit RX serial and preserve its complete stdout:
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
```
Accept only the exact line:
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.
```text
CLKIN status: clock signal detected
```
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.
Set `SLAVE CLKIN BEFORE RX = DETECTED` only when that line is present. Otherwise set `COMMON CLOCK VERIFIED = NO`, do not start the short RX or capture, keep `RF TX = 0`, and STOP.
## 2. Authorized finite RX capture
## 3. Bounded short RX-start
If SLAVE CLKIN was detected, perform one short, finite RX-start on the same device:
```powershell
& '<verified hackrf_transfer.exe path>' -d 000000000000000097c063dc2e5327d3 -r NUL -f 1400000000 -s 8000000 -n 65536 -l 0 -g 0 -a 0 -p 0 -B
```
This is RX only. It must not transmit. Require:
- exit code = `0`;
- exactly `65536` complex samples transferred;
- RX overruns = `0`;
- USB errors = `0`;
- file write errors = `0`.
If any condition fails, set `SHORT RX START = FAIL` and `COMMON CLOCK VERIFIED = NO`. Do not start the 20-second capture. Keep `RF TX = 0` and STOP.
## 4. Verify SLAVE CLKIN after RX
Immediately repeat the same read-only check and preserve its complete stdout:
```powershell
& '<verified hackrf_clock.exe path>' -d 000000000000000097c063dc2e5327d3 -i
```
Accept only the exact line `CLKIN status: clock signal detected`.
Set `COMMON CLOCK VERIFIED = YES` only when all three conditions pass:
- `SLAVE CLKIN BEFORE RX = DETECTED`;
- `SHORT RX START = PASS`;
- `SLAVE CLKIN AFTER RX = DETECTED`.
If CLKIN is not detected after the short RX-start, set `COMMON CLOCK VERIFIED = NO`, do not start the capture, keep `RF TX = 0`, and STOP. Do not alter clock configuration automatically.
## 5. Authorized finite RX capture
When and only when `COMMON CLOCK VERIFIED = YES`, continue immediately under this same task ID and the same YELLOW approval. Start this finite RX capture:
Only this capture is authorized:
```text
RX serial = 000000000000000097c063dc2e5327d3
@@ -110,23 +74,21 @@ RX LNA = 24 dB
RX VGA = 24 dB
RF amp = OFF
antenna bias = OFF
duration = 20 s
sample count = 160000000 complex samples
duration = 60 s
sample count = 480000000 complex samples
hardware format = cs8
expected file size = 320000000 bytes
expected file size = 960000000 bytes
common clock = DETECTED
```
Create a new directory outside the Git checkout for every attempt. Use a timestamp plus a random suffix, refuse an existing path, and never overwrite an earlier capture. Keep the IQ file, stdout, stderr and metadata there. Do not add them to Git.
The required capture arguments are:
The required `hackrf_transfer.exe` arguments are:
```text
hackrf_transfer.exe
-d 000000000000000097c063dc2e5327d3
-r <unique_capture_path>\rf001a_capture.cs8
-r <unique_capture_path>\rf001a_ota_capture.cs8
-f 1400000000
-s 8000000
-n 160000000
-n 480000000
-l 24
-g 24
-a 0
@@ -134,154 +96,290 @@ hackrf_transfer.exe
-B
```
## 6. Controlled background start
## 3. Durable process and exit-code recording
Keep one PowerShell session open for the start and completion sequence. Resolve the verified local path to `hackrf_transfer.exe`, then start the controlled background process:
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_VERIFY_AND_CAPTURE_001_$runId"
$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
$capturePath = Join-Path $captureDirectory 'rf001a_capture.cs8'
$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>'
$startedAt = Get-Date
$captureArguments = @(
```
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,
'-r', $CapturePath,
'-f', '1400000000',
'-s', '8000000',
'-n', '160000000',
'-n', '480000000',
'-l', '24',
'-g', '24',
'-a', '0',
'-p', '0',
'-B'
)
$captureProcess = Start-Process -FilePath $hackrfTransfer -ArgumentList $captureArguments `
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath `
-PassThru -WindowStyle Hidden
$growthDeadline = (Get-Date).AddSeconds(2)
do {
Start-Sleep -Milliseconds 100
$captureProcess.Refresh()
$captureBytes = if (Test-Path -LiteralPath $capturePath) {
(Get-Item -LiteralPath $capturePath).Length
$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
}
} until ($captureProcess.HasExited -or $captureBytes -gt 0 -or (Get-Date) -ge $growthDeadline)
$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
```
The start passes only if the process remains active and the capture file exists and has grown above zero bytes. If either check fails, report `RX CAPTURE STARTED = NO`, `RX READY FOR TX = NO`, preserve the logs, keep `RF TX = 0`, and STOP.
When both checks pass, immediately output:
```text
TASK_ID =
RF001A_VERIFY_AND_CAPTURE_001
COMMON CLOCK VERIFIED =
YES
RX CAPTURE STARTED =
YES
RX READY FOR TX =
YES
CAPTURE PID =
CAPTURE PATH =
RF TX =
0
```
Do not wait for capture completion, calculate SHA256, or perform another slow action before sending this status. `RX READY FOR TX = YES` only reports that the RX capture is active. It does not authorize RF TX on either computer. RF TX on the RX laptop remains forbidden.
## 7. Capture completion
Only after sending the immediate start status, wait for the same process in the same PowerShell session:
Start the runner as a controlled background process. Quote every path passed through `-ArgumentList`:
```powershell
$captureProcess.WaitForExit()
$finishedAt = Get-Date
$captureProcess.Refresh()
$exitCode = $captureProcess.ExitCode
$captureSize = (Get-Item -LiteralPath $capturePath).Length
$captureDurationSeconds = ($finishedAt - $startedAt).TotalSeconds
$captureHash = (Get-FileHash -LiteralPath $capturePath -Algorithm SHA256).Hash.ToLowerInvariant()
$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
```
Inspect the saved stdout and stderr. Record the exit code, actual file size, actual duration, RX overruns, USB errors, file write errors and raw IQ SHA256.
Wait only long enough to prove that the child process is alive and the file is growing:
The capture is complete only if:
```powershell
$startDeadline = (Get-Date).AddSeconds(5)
$startedStatus = $null
$captureBytes = 0
$captureAlive = $false
- exit code = `0`;
- file size = exactly `320000000` bytes;
- RX overruns = `0`;
- USB errors = `0`;
- file write errors = `0`.
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)
```
A smaller file is incomplete. Preserve the evidence and do not retry automatically. Write machine-readable metadata to a new file in `$captureDirectory`, including the task ID, RX serial, requested settings, clock results, PID, timestamps, exit code, size, duration, error counts and SHA256. The metadata and logs remain outside Git.
The start passes only when:
Finish with:
```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_VERIFY_AND_CAPTURE_001
SLAVE CLKIN BEFORE RX =
DETECTED / NOT_DETECTED
SHORT RX START =
PASS / FAIL / NOT_RUN
SLAVE CLKIN AFTER RX =
DETECTED / NOT_DETECTED / NOT_RUN
COMMON CLOCK VERIFIED =
YES / NO
RF001A_OTA_CAPTURE_001
RX CAPTURE STARTED =
YES / NO
YES
RX READY FOR TX =
YES / NO
YES
CAPTURE PID =
<actual hackrf_transfer child PID>
CAPTURE PATH =
<absolute path>
RX CAPTURE COMPLETE =
YES / NO / NOT_RUN
CAPTURE START TIME =
<started_status started_at>
CAPTURE SIZE =
CAPTURE DURATION =
RX OVERRUNS =
USB ERRORS =
FILE WRITE ERRORS =
RAW IQ SHA256 =
EXPECTED END TIME =
<started_at plus 60 seconds>
RF TX =
0
STOP.
```
## 8. Prohibited actions
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.
On the RX laptop, do not perform RF TX, firmware or CPLD updates, Zadig operations, USB driver changes, clock reconfiguration, continuous RX, automatic retries, commits to `video-development`, or pushes to `video-development`.
## 5. Completion evidence
`ALLOW_PUSH = RX_RESULT_ONLY` permits only a separately approved small result publication under the handoff protocol. It does not permit raw IQ, large logs, a commit to `video-development`, or a push to `video-development`.
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.