Harden RF handoff and stream OTA decode

This commit is contained in:
LittleSam129
2026-09-22 17:43:58 +03:00
parent 168e03d9e7
commit a94f2db4ee
17 changed files with 1548 additions and 497 deletions

View File

@@ -1,7 +1,7 @@
TASK_ID = RF001A_OTA_CAPTURE_003
TASK_TITLE = Start coordinated RF-001A common-clock OTA capture
TASK_ID = RF001A_OTA_CAPTURE_004
TASK_TITLE = Start hardened RF-001A common-clock OTA capture
SOURCE_BRANCH = video-development
EXPECTED_HEAD = bc55d0516dc1ee36cefa1ff096e7d86f60645303
EXPECTED_HEAD = 168e03d9e778ab2ec63502552d385ec7be3ef677
REQUIRES_APPROVAL = YES
HARDWARE_ACCESS = YES
RF_RX = YES
@@ -10,70 +10,79 @@ 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 and RF_HANDOFF_CHANNEL_TEST.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
# RF-001A RX capture 004
TASK =
START COORDINATED RF-001A COMMON-CLOCK OTA CAPTURE
This file is the authoritative current task for the RX laptop. Read
`RX_NODE_STATE.md` first. If chat history conflicts with these files or the
current Git HEAD, stop and report the conflict.
This is a new YELLOW task. Do not reuse approval or completion state from RF001A_OTA_CAPTURE_001, RF001A_OTA_CAPTURE_002 or any earlier task.
This task is prepared only. A previous approval does not apply to this new
TASK_ID. Do not access HackRF until the user approves this exact YELLOW task.
One approval for this task covers the read-only bidirectional channel check, other read-only checks, one bounded RF RX capture and publication of the two small allowlisted result files to the existing rx-node branch. It never authorizes RF TX.
## 1. Software and Git gates
The preceding RF001A_OTA_CAPTURE_002 produced a real 120.1413067 s, 1920000000-byte capture with CLKIN detected before and after, zero reported overruns and SHA256 99a3e7cb51d3ad4e3b914b63da0695ed6658aa169110c32659aba995b542fa0c. Its READY publication failed, its exact child exit code was not recorded and no synchronized truth metadata was available. Treat that as historical evidence only.
1. Run `git pull --ff-only origin video-development`.
2. Require branch `video-development` and a clean tracked worktree.
3. Require local HEAD to equal `origin/video-development`; `EXPECTED_HEAD`
must be its ancestor.
4. Confirm the historical channel-test READY and COMPLETE acknowledgements
from `origin/tx-node` through the RX watcher.
5. Before opening HackRF, run the authenticated no-change publication check:
## 1. Bidirectional handoff channel gate
python -B tools/rx_agent_watcher.py --check-result-write --result-branch rx-node
Before offering this task for YELLOW approval, the RX watcher must fetch origin/rx-node and origin/tx-node and scan the acknowledgement history from oldest to newest. It must confirm both exact records:
Require `RX-NODE WRITE PREFLIGHT = PASS`. This command fetches the branch,
performs `git push --dry-run` of its existing HEAD to the same ref, and
confirms that the remote HEAD did not change. It creates no commit and no
production READY event.
6. Run the targeted tests, Ruff, Pyright and `git diff --check` according to
the tracked project instructions.
7. Validate `configs/rf/rf001a_truth_metadata.json`: waveform SHA256, seed,
PHY settings, 100 sequence numbers and 100 packet hashes must agree with
`configs/rf/rf001a_common_clock.json`.
STATE = CHANNEL_READY_OBSERVED
SOURCE_RX_COMMIT = 4cb6c8cad1ab5194755c2740c2a5a83933d099d8
SOURCE_RX_DIGEST = 23fc54f1f788f068fd4a842daf0a445bd4ec192866b4d89406c74f1797a9952b
If any gate fails, do not open HackRF and stop. In particular, a failed Git
write preflight blocks the hardware task.
and:
## 2. Optional offline check of capture 003
STATE = CHANNEL_COMPLETE_OBSERVED
SOURCE_RX_COMMIT = 83d91ff5309777ab639a24f774e9d0b8825a11e9
SOURCE_RX_DIGEST = 7d98ed2e70e98a087b968c0d65fb1db4277b81f251e075d20583de2e5002b2d6
If the existing `RF001A_OTA_CAPTURE_003` file is present locally, analyze it
read-only with the streaming cs8 decoder before starting new hardware work:
Both acknowledgements must use TASK_ID RF_HANDOFF_CHANNEL_TEST_001, the original shared source HEAD and RF_TX = 0. Required watcher output:
python -B -X utf8 tools/qpsk_rf_rx.py --iq-file <capture-003-path>\rf001a_ota_capture.cs8 --iq-format cs8 --truth-metadata configs/rf/rf001a_truth_metadata.json --report <new-path-outside-git>\capture003_streaming_report.json
MAIN READY RECEIVED = CONFIRMED
MAIN COMPLETE RECEIVED = CONFIRMED
No RF-001A transmission occurred during capture 003. The expected exact
packet count is 0. CRC-valid background candidates must not be reported as
exact unless their complete packet SHA256 matches the tracked truth metadata.
The analysis must use memmap/chunks and must not materialize the full 1.92 GB
capture as complex64.
Do not rerun the channel test. If origin/tx-node is missing or either exact acknowledgement is absent or invalid, do not access HackRF, do not start capture and STOP with RF TX = 0.
If capture 003 is unavailable, report `CAPTURE_003 ANALYSIS = NOT_RUN` and
continue only after all mandatory gates pass.
## 2. Preconditions
## 3. Hardware preflight after YELLOW approval
After the same [A] approval for this exact TASK_ID and after the channel gate passes:
1. Run `hackrf_info` and require RX serial
`000000000000000097c063dc2e5327d3`.
2. With `MASTER CLKOUT -> SLAVE CLKIN` connected, run:
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. Confirm that configs/rf/rf001a_common_clock.json and configs/rf/rf001a_truth_metadata.json exist. Require the tracked truth metadata test ID, seed, 100 packet hashes, sequence numbers, waveform SHA256 and PHY settings to agree with the shared RF-001A spec.
8. With MASTER CLKOUT physically connected to SLAVE CLKIN, run:
hackrf_clock.exe -d 000000000000000097c063dc2e5327d3 -i
hackrf_clock.exe -d 000000000000000097c063dc2e5327d3 -i
3. Require `CLKIN status: clock signal detected`.
4. Repeat the Git write preflight immediately before the finite RX command.
5. Require antennas connected, approximately 2 m distance, clear LOS, and
video disabled.
6. Require at least 1500000000 free bytes on the capture volume and record the
measured value.
9. Preserve complete stdout and require the exact line:
Do not reconfigure clocks. If CLKIN or the second Git preflight fails, stop
before capture.
CLKIN status: clock signal detected
## 4. Finite RX capture
10. Require the remote branch origin/rx-node to exist through both git fetch origin rx-node and git ls-remote --heads origin rx-node. Do not create it in this task.
11. Create a unique capture directory outside Git. Refuse an existing directory and never overwrite an earlier capture.
12. Require at least 2500000000 free bytes on the target volume. Record the measured free-byte count.
If any condition fails, do not start capture. Report the failed condition, keep RF TX = 0, and STOP.
## 3. Authorized bounded RX capture
Only this finite receive operation is authorized:
Use a unique directory outside Git. Never overwrite an earlier capture.
RX serial = 000000000000000097c063dc2e5327d3
center frequency = 1400000000 Hz
@@ -82,239 +91,114 @@ Only this finite receive operation is authorized:
RX VGA = 24 dB
RF amp = OFF
antenna bias = OFF
duration = 120 s
sample count = 960000000 complex samples
duration = 60 s
sample count = 480000000 complex samples
hardware format = cs8
expected file size = 1920000000 bytes
common clock = DETECTED
expected file size = 960000000 bytes
The hackrf_transfer.exe arguments are fixed:
Use these fixed `hackrf_transfer.exe` arguments:
-d 000000000000000097c063dc2e5327d3
-r <unique_capture_path>\rf001a_ota_capture.cs8
-f 1400000000
-s 8000000
-n 960000000
-n 480000000
-l 24
-g 24
-a 0
-p 0
-B
Do not add repeat mode. Do not change gain, clock, frequency or sample rate.
Use `tools/capture_process_runner.ps1` as a controlled background process.
Preserve its complete stdout, stderr, start JSON and result JSON outside Git.
The runner must start exactly one finite child, record the actual child PID and
UTC start time, set the expected end to start plus 60 seconds, wait for that
exact process, record its integer exit code, size and duration, and calculate
SHA256 after the output file closes. Never retry the capture automatically.
## 4. Background runner and durable evidence
Within five seconds require the runner and child to be alive and the file to
exist and grow. Otherwise publish no READY and stop.
Use the tracked tools/capture_process_runner.ps1. Write the exact hackrf_transfer argument array to a unique local JSON file outside Git. Start the runner itself as a controlled background PowerShell process and pass unique paths for the capture, complete stdout, complete stderr, capture_started.json and capture_result.json. The runner must:
## 5. READY publication
- start exactly one hackrf_transfer child;
- record its PID and start time immediately;
- record the expected end time as start time plus 120 seconds;
- redirect complete stdout and stderr to unique local files;
- wait for that exact child, refresh its process object and read ExitCode only after WaitForExit;
- record its exact integer exit code, finish time, duration and output size;
- calculate SHA256 after the file is closed;
- write started and final JSON atomically;
- never start a second capture automatically.
Create the strict schema-version-1 READY JSON outside Git for TASK_ID
`RF001A_OTA_CAPTURE_004`. Use the actual child PID and UTC timestamps,
`expected_capture_end = capture_started_at + 60 s`, current source HEAD,
`common_clock_verified = true`, `rf_tx = 0`, and null final result fields.
Start the runner as a controlled background process. Within five seconds, require all four facts:
Publish it once with:
runner process alive = YES
hackrf_transfer child alive = YES
capture file created = YES
capture file growing = YES
python -B tools/rx_agent_watcher.py --publish-result <ready-json> --result-branch rx-node
If any fact is false, publish no READY state. Preserve local evidence, report RX CAPTURE STARTED = NO, keep RF TX = 0, and STOP.
The publisher creates one immutable READY commit. On a recognized transient
network or authentication failure it checks the remote branch and may retry
the exact same commit once. It never creates a second READY commit or changes
the payload between attempts. If the remote already contains that commit, the
publication is successful. A second failure means:
## 5. Publish READY immediately
READY publication = FAIL
RX READY FOR TX = NO
RF TX = 0
As soon as the child is alive and the file is growing, build a local JSON file outside Git with every field below:
Do not publish READY after the capture window has ended. After the process
exits, publish only a `CAPTURE_COMPLETE` result with the actual exit code,
size, duration and error counters, including nonzero failure values.
{
"schema_version": 1,
"task_id": "RF001A_OTA_CAPTURE_003",
"state": "RX_READY_FOR_TX",
"rx_ready_for_tx": true,
"process_alive": true,
"file_growing": true,
"capture_pid": <actual positive child PID>,
"published_at": "<current UTC ISO-8601 time>",
"capture_started_at": "<actual UTC ISO-8601 start time>",
"expected_capture_end": "<start time plus 120 seconds>",
"capture_path": "<absolute local capture path>",
"common_clock_verified": true,
"source_head": "<current local 40-character Git HEAD>",
"rf_tx": 0,
"exit_code": null,
"file_size": null,
"duration_seconds": null,
"overruns": null,
"usb_errors": null,
"file_errors": null,
"sha256": null,
"clkin_after": "NOT_RUN",
"rf_tx_observed": "UNKNOWN"
}
Publish it with the approved RX watcher state:
python -B tools/rx_agent_watcher.py --publish-result <ready-json-path> --result-branch rx-node
The publisher may change only:
docs/handoff/results/RX_RESULT.json
docs/handoff/results/RX_RESULT.md
The publisher checks origin/rx-node through the real origin of the main checkout. It creates a temporary Git worktree from that repository, stages exactly the two allowlisted result paths, pushes HEAD:rx-node, confirms the new remote HEAD with git ls-remote and removes the worktree. It must not switch the source checkout, push video-development, or add raw IQ, video, waveform or large logs.
Use this order without exception: verify CLKIN, start capture, verify the child is alive, verify the file is growing, publish READY, verify RX RESULT REMOTE HEAD equals the published commit, and only then report RX READY FOR TX = YES.
If publication or remote confirmation fails, show the complete error and report RX READY FOR TX = NO. The finite capture may finish normally, but TX coordination does not start and RF TX remains 0.
After successful publication, immediately return control to the user without waiting for capture completion:
TASK_ID =
RF001A_OTA_CAPTURE_003
RX CAPTURE STARTED =
YES
RX READY FOR TX =
YES
After the remote HEAD confirms the READY commit, immediately print:
TASK_ID = RF001A_OTA_CAPTURE_004
RX CAPTURE STARTED = YES
RX READY FOR TX = YES
CAPTURE PID =
CAPTURE PATH =
CAPTURE START TIME =
EXPECTED CAPTURE END =
RX RESULT HEAD =
RF TX = 0
RX RESULT PUBLISH =
PASS
Return control without waiting for capture completion.
RX RESULT REMOTE HEAD =
## 6. Completion and offline decode
RF TX =
0
After the child exits, record exact exit code, actual size, duration, overruns,
USB errors, file-write errors and SHA256. A complete capture is exactly
960000000 bytes; a smaller file is incomplete. Run the CLKIN check again and
require detected status.
The READY state describes a live receiver only. It does not authorize RF TX on either computer.
Publish an honest schema-version-1 COMPLETE result with final values. Keep IQ,
stdout, stderr and large logs outside Git.
## 6. Capture completion
After `origin/tx-node` contains `TX_STARTED` and `TX_COMPLETE` for
`RF001A_OTA_TX_004`, run the streaming decoder with RX/TX timestamps:
After the child exits, read capture_result.json and require an integer exit_code recorded by the runner. Preserve all local logs and inspect every line for RX overruns, USB errors and file write errors. Do not infer zero when a tool did not report a counter; use JSON null and report NOT_REPORTED.
python -B -X utf8 tools/qpsk_rf_rx.py --iq-file <capture-path>\rf001a_ota_capture.cs8 --iq-format cs8 --truth-metadata configs/rf/rf001a_truth_metadata.json --capture-started-at <CAPTURE_STARTED_AT> --tx-started-at <TX_STARTED_AT> --tx-finished-at <TX_FINISHED_AT> --report <new-path-outside-git>\rf001a_streaming_report.json
Run the read-only clock check again:
The decoder uses a two-second margin before and after the timestamped burst.
Without complete timestamps it uses the streaming power locator. Chunk overlap
is derived from the real 256-byte RF-001 waveform and duplicate detections from
overlap are suppressed.
hackrf_clock.exe -d 000000000000000097c063dc2e5327d3 -i
## 7. Prohibited actions
Require CLKIN to remain detected. The expected complete capture has:
RF TX, firmware update, CPLD update, Zadig, USB driver changes, clock
reconfiguration, commit or push to `video-development`, repeated capture, raw
IQ/video/waveform publication and large-log publication are forbidden.
exit code = 0
file size = 1920000000 bytes
duration = consistent with 120 s
RX overruns = 0 or NOT_REPORTED
USB errors = 0 or NOT_REPORTED
file write errors = 0 or NOT_REPORTED
CLKIN after capture = DETECTED
A shorter file is incomplete. Any reported nonzero error remains evidence. Do not retry automatically.
## 7. Publish final result
Build a second local JSON document with the same identity and timing fields. Set:
state = CAPTURE_COMPLETE
rx_ready_for_tx = false
process_alive = false
file_growing = false
exit_code = <exact integer>
file_size = <actual integer bytes>
duration_seconds = <actual number>
overruns = <integer or null when not reported>
usb_errors = <integer or null when not reported>
file_errors = <integer or null when not reported>
sha256 = <lowercase 64-character SHA256>
clkin_after = DETECTED or NOT_DETECTED
rf_tx_observed = EXPECTED_EXTERNAL_TX or UNKNOWN
published_at = <current UTC ISO-8601 time>
rf_tx = 0
Use EXPECTED_EXTERNAL_TX only when the coordinated MAIN TX event is known to have occurred inside this capture window. Otherwise use UNKNOWN.
Publish with:
python -B tools/rx_agent_watcher.py --publish-result <final-json-path> --result-branch rx-node
Require RX RESULT REMOTE HEAD to equal the final result commit. If the push or confirmation fails, preserve the local JSON and logs, report the error, and do not claim publication success.
## 8. Offline decode
Use the tracked synchronized truth artifact without copying TX files manually:
python -B -X utf8 tools/qpsk_rf_rx.py --iq-file <capture-path> --iq-format cs8 --truth-metadata configs/rf/rf001a_truth_metadata.json --report <unique-path-outside-git>\rf001a_ota_decode.json
Report exact packets, missing packets, duplicates, BER and post-FEC errors from the generated report. Preserve a decoder failure as evidence; do not modify the receiver or invent results.
## 9. Prohibited actions
On the RX laptop, do not perform:
- RF TX;
- continuous RX;
- automatic capture retry;
- firmware or CPLD update;
- hackrf_spiflash;
- Zadig or USB driver changes;
- clock reconfiguration;
- commit or push to video-development;
- creation of rx-node;
- publication of raw IQ, video, waveform or large logs.
## 10. Final RX report
TASK_ID =
RF001A_OTA_CAPTURE_003
CHANNEL TEST =
RX CAPTURE COMPLETE =
EXIT CODE =
Final report:
TASK_ID = RF001A_OTA_CAPTURE_004
RX-NODE WRITE PREFLIGHT = PASS / FAIL
COMMON CLOCK = VERIFIED / NOT_VERIFIED
RX CAPTURE COMPLETE = YES / NO
CAPTURE SIZE =
CAPTURE DURATION =
RX OVERRUNS =
USB ERRORS =
FILE WRITE ERRORS =
RAW IQ SHA256 =
CLKIN AFTER =
RF TX OBSERVED =
FINAL RX RESULT PUBLISH =
FINAL RX RESULT REMOTE HEAD =
OFFLINE DECODE =
EXACT PACKETS =
MISSING =
DUPLICATES =
BER =
POST-FEC ERRORS =
RF TX =
0
STREAMING DECODE = PASS / FAIL / NOT_RUN
EXACT RF001A PACKETS =
PEAK ANALYSIS MEMORY =
RF TX = 0
STOP.