Add bidirectional RF handoff coordination

This commit is contained in:
LittleSam129
2026-09-21 17:02:49 +03:00
parent 34f5ace545
commit bc55d0516d
14 changed files with 1978 additions and 556 deletions

View File

@@ -1,7 +1,7 @@
TASK_ID = RF001A_OTA_CAPTURE_001
TASK_TITLE = Capture first RF-001A QPSK OTA transmission
TASK_ID = RF001A_OTA_CAPTURE_002
TASK_TITLE = Start coordinated RF-001A common-clock OTA capture
SOURCE_BRANCH = video-development
EXPECTED_HEAD = 1e3aa25d7007c7c8e84608cddb02f06f77e63396
EXPECTED_HEAD = 34f5ace54540f947311ada7e81a463c08abfd2d3
REQUIRES_APPROVAL = YES
HARDWARE_ACCESS = YES
RF_RX = YES
@@ -17,369 +17,263 @@ If chat history conflicts with these files, the shared RF-001A spec or current G
# Current RX task
TASK =
CAPTURE FIRST RF-001A QPSK OTA TRANSMISSION
START COORDINATED RF-001A COMMON-CLOCK OTA CAPTURE
This is a new YELLOW task. Do not reuse approval or completion state from any earlier task ID.
This is a new YELLOW task. Do not reuse approval or completion state from RF001A_OTA_CAPTURE_001 or any earlier task.
## 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.
One approval for this task covers its read-only checks, 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. Preconditions
After one explicit `[A] Approve` for this task:
After the user chooses [A] Approve for this exact TASK_ID:
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:
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 exists and matches the fixed RF-001A frequency, sample rate, modulation and common-clock plan.
8. With MASTER CLKOUT physically connected to SLAVE CLKIN, run:
```powershell
& '<verified hackrf_clock.exe path>' -d 000000000000000097c063dc2e5327d3 -i
```
hackrf_clock.exe -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.
9. Preserve complete stdout and require the exact line:
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.
CLKIN status: clock signal detected
## 2. Authorized finite RX capture
10. Require the remote branch origin/rx-node to exist. 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.
Only this capture is authorized:
If any condition fails, do not start capture. Report the failed condition, keep RF TX = 0, and STOP.
```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
```
## 2. Authorized bounded RX capture
The required `hackrf_transfer.exe` arguments are:
Only this finite receive operation is authorized:
```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
```
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 = 120 s
sample count = 960000000 complex samples
hardware format = cs8
expected file size = 1920000000 bytes
common clock = DETECTED
## 3. Durable process and exit-code recording
The hackrf_transfer.exe arguments are fixed:
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.
-d 000000000000000097c063dc2e5327d3
-r <unique_capture_path>\rf001a_ota_capture.cs8
-f 1400000000
-s 8000000
-n 960000000
-l 24
-g 24
-a 0
-p 0
-B
Prepare paths:
Do not add repeat mode. Do not change gain, clock, frequency or sample rate.
```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
## 3. Background runner and durable evidence
$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" }
Use a local runner outside Git. The runner must:
$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>'
```
- 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;
- record its exact 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.
Write this runner to `$runnerPath`:
Start the runner as a controlled background process. Within five seconds, require all four facts:
```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
)
runner process alive = YES
hackrf_transfer child alive = YES
capture file created = YES
capture file growing = YES
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
}
If any fact is false, publish no READY state. Preserve local evidence, report RX CAPTURE STARTED = NO, keep RF TX = 0, and STOP.
$arguments = @(
'-d', '000000000000000097c063dc2e5327d3',
'-r', $CapturePath,
'-f', '1400000000',
'-s', '8000000',
'-n', '480000000',
'-l', '24',
'-g', '24',
'-a', '0',
'-p', '0',
'-B'
)
## 4. Publish READY immediately
$startedAt = [DateTimeOffset]::Now
$transferProcess = $null
$exitCode = $null
$runnerError = $null
As soon as the child is alive and the file is growing, build a local JSON file outside Git with every field below:
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
{
"schema_version": 1,
"task_id": "RF001A_OTA_CAPTURE_002",
"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"
}
$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
}
Publish it with the approved RX watcher state:
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
})
}
python -B tools/rx_agent_watcher.py --publish-result <ready-json-path> --result-branch rx-node
if ($null -eq $exitCode) { exit 255 }
exit $exitCode
```
The publisher may change only:
Start the runner as a controlled background process. Quote every path passed through `-ArgumentList`:
docs/handoff/results/RX_RESULT.json
docs/handoff/results/RX_RESULT.md
```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
```
It uses a temporary checkout of the existing rx-node branch. It must not switch the source checkout, push video-development, or add raw IQ, video, waveform or large logs.
Wait only long enough to prove that the child process is alive and the file is growing:
If publication fails, show the complete error. Do not claim RX READY FOR TX and do not trigger TX automatically. The finite capture may finish normally, but RF TX remains 0.
```powershell
$startDeadline = (Get-Date).AddSeconds(5)
$startedStatus = $null
$captureBytes = 0
$captureAlive = $false
After successful publication, immediately return control to the user without waiting for capture completion:
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)
```
TASK_ID =
RF001A_OTA_CAPTURE_002
The start passes only when:
RX CAPTURE STARTED =
YES
```text
runner process alive = YES
hackrf_transfer child alive = YES
capture file created = YES
capture file growing = YES
```
RX READY FOR TX =
YES
If any check fails, report `RX CAPTURE STARTED = NO`, preserve all local files, keep `RF TX = 0`, and STOP.
CAPTURE PID =
## 4. Immediate READY report
CAPTURE PATH =
When all start checks pass, immediately return control to the user with:
CAPTURE START TIME =
```text
TASK_ID =
RF001A_OTA_CAPTURE_001
EXPECTED CAPTURE END =
RX CAPTURE STARTED =
YES
RX RESULT PUBLISH =
PASS
RX READY FOR TX =
YES
RF TX =
0
CAPTURE PID =
<actual hackrf_transfer child PID>
The READY state describes a live receiver only. It does not authorize RF TX on either computer.
CAPTURE PATH =
<absolute path>
## 5. Capture completion
CAPTURE START TIME =
<started_status started_at>
After the child exits, read the durable final status. 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.
EXPECTED END TIME =
<started_at plus 60 seconds>
Run the read-only clock check again:
RF TX =
0
```
hackrf_clock.exe -d 000000000000000097c063dc2e5327d3 -i
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.
Require CLKIN to remain detected. The expected complete capture has:
## 5. Completion evidence
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
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.
A shorter file is incomplete. Any reported nonzero error remains evidence. Do not retry automatically.
Require and report:
## 6. Publish final result
- 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.
Build a second local JSON document with the same identity and timing fields. Set:
Run the read-only clock check again. Require `CLKIN status: clock signal detected`.
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
The capture is complete only when:
Use EXPECTED_EXTERNAL_TX only when the coordinated MAIN TX event is known to have occurred inside this capture window. Otherwise use UNKNOWN.
```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
```
Publish with:
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.
python -B tools/rx_agent_watcher.py --publish-result <final-json-path> --result-branch rx-node
## 6. Offline decode
If the push fails, preserve the local JSON and logs, report the error, and do not claim publication success.
Use the saved OTA IQ without changing the receiver for this capture:
## 7. Offline decode
```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'
```
Do not start automatic decode unless synchronized truth metadata for this exact transmitted waveform is available and consistent. If it is absent, preserve the IQ file and report:
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.
OFFLINE DECODE = BLOCKED_BY_TRUTH_METADATA
Report at minimum:
Do not modify the receiver or invent results.
```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 =
```
## 8. Prohibited actions
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.
On the RX laptop, do not perform:
Classify this first smoke test:
- 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.
```text
99-100 exact = GOOD
95-98 exact = PARTIAL / DIAGNOSTIC
<95 exact = FAIL FOR VIDEO PROGRESSION
```
## 9. Final RX report
One run is not statistical reliability evidence.
TASK_ID =
RF001A_OTA_CAPTURE_002
## 7. Prohibited actions
RX CAPTURE COMPLETE =
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`.
EXIT CODE =
`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.
CAPTURE SIZE =
CAPTURE DURATION =
RX OVERRUNS =
USB ERRORS =
FILE WRITE ERRORS =
RAW IQ SHA256 =
CLKIN AFTER =
RF TX OBSERVED =
FINAL RX RESULT PUBLISH =
OFFLINE DECODE =
RF TX =
0
STOP.

View File

@@ -1,7 +1,7 @@
TASK_ID = RF001A_OTA_TX_001
TASK_TITLE = Transmit first RF-001A QPSK packet burst
TASK_ID = RF001A_OTA_TX_002
TASK_TITLE = Transmit one coordinated RF-001A QPSK burst
SOURCE_BRANCH = video-development
EXPECTED_HEAD = 1e3aa25d7007c7c8e84608cddb02f06f77e63396
EXPECTED_HEAD = 34f5ace54540f947311ada7e81a463c08abfd2d3
REQUIRES_APPROVAL = YES
HARDWARE_ACCESS = YES
RF_RX = NO
@@ -11,205 +11,209 @@ ALLOW_PUSH = NO
RISK_LEVEL = RED
This file is the authoritative current task for the MAIN PC TX node.
Read TX_NODE_STATE.md first. If chat history, this file, the shared RF-001A spec and current Git HEAD conflict, stop and report the conflict.
Read TX_NODE_STATE.md first. If chat history, this file, the shared RF-001A spec or current Git HEAD conflicts, stop and report the conflict.
# Current TX task
TASK =
TRANSMIT FIRST RF-001A QPSK PACKET BURST
TRANSMIT ONE COORDINATED RF-001A QPSK BURST
This is a new RED task. It does not inherit the RX task approval. Publication of this file is not permission to transmit.
This is a new RED task. Publication of this file is not permission to transmit. It does not inherit any approval from RF001A_OTA_TX_001, the RX task, a previous watcher prompt or a previous RF run.
Before any RF TX, the user must:
## 1. Coordination gate
1. separately provide the exact authorization `AUTHORIZE RF TX`;
2. confirm the live RX status `RX READY FOR TX = YES` for task `RF001A_OTA_CAPTURE_001`.
Start the local model-free watcher before the RX task begins:
A normal approval, an earlier RF authorization, or an RX approval is insufficient.
tools\start_tx_watcher.cmd
## 1. Fixed RF-001A parameters
The watcher polls origin/rx-node every 5 seconds and accepts only a strict result for:
```text
center frequency = 1400000000 Hz
sample rate = 8000000 samples/s
modulation = QPSK
symbol rate = 500000 symbols/s
inner FEC = 2/3
payload = 256 bytes
packet count = 100
inter-packet gap = 5 ms
digital carrier offset = +125000 Hz
TX VGA = 20 dB
RF amp = OFF
antenna bias = OFF
common 10 MHz reference = VERIFIED
distance = approximately 2 m
LOS = YES
video = OFF
```
RX TASK_ID = RF001A_OTA_CAPTURE_002
STATE = RX_READY_FOR_TX
RX_READY_FOR_TX = YES
PROCESS_ALIVE = YES
FILE_GROWING = YES
COMMON_CLOCK_VERIFIED = YES
RF_TX = 0
The waveform is one finite burst. Never add `-R`.
The READY publication must be no more than 30 seconds old. Its planned capture window must be 120 seconds. At least 10 seconds must remain.
## 2. Preconditions
When a valid new READY result appears, the watcher prints:
Before asking for RED authorization, prepare all read-only evidence:
RX IS READY
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 TX serial `000000000000000097c063dc323b97d3`.
7. Require the recorded MASTER CLKOUT command result = `PASS`.
8. Require the user to confirm both TX and RX antennas connected, approximately 2 m LOS and the intended antenna geometry.
9. Require the current RX report to show `RX READY FOR TX = YES`, task ID `RF001A_OTA_CAPTURE_001`, a live capture PID, capture path, start time and expected end time.
10. Require current time to be before the RX expected end time.
TASK ID =
CAPTURE STARTED =
CAPTURE END EXPECTED =
Verify the prepared waveform without accessing radio hardware:
TX TASK =
RF001A_OTA_TX_002
```powershell
python -B -X utf8 tools/rf001a_prepare.py --verify-dir data/raw/rf001/rf001a_common_clock_001
```
RF TX REQUIRES RED APPROVAL
Then verify the actual hardware-format bytes through the existing receiver. The report path must be new and outside Git:
Enter:
AUTHORIZE RF TX
```powershell
python -B -X utf8 tools/qpsk_rf_rx.py `
--iq-file data/raw/rf001/rf001a_common_clock_001/tx_waveform.cs8 `
--iq-format cs8 `
--truth-metadata data/raw/rf001/rf001a_common_clock_001/tx_metadata.json `
--report '<new unique path outside Git>\rf001a_tx_offline_verify.json'
```
Without the exact phrase AUTHORIZE RF TX, do not transmit.
Require the receiver report to contain `crc_valid_packets = 100`, `exact_packets = 100`, no missing packets and no duplicates.
After the phrase, the watcher fetches rx-node again and repeats all READY, freshness and capture-window checks. If the result changed or less than 10 seconds remain, it refuses authorization and requires a new RX capture.
The watcher never opens HackRF hardware. On success it writes one local authorization record under:
.local\tx_authorizations\
The record has rf_tx_started = false and rf_tx = 0. It is the handoff to the separately controlled TX executor. It is not evidence that transmission occurred.
## 2. Fixed RF-001A parameters
center frequency = 1400000000 Hz
sample rate = 8000000 samples/s
modulation = QPSK
symbol rate = 500000 symbols/s
inner FEC = 2/3
payload = 256 bytes
packet count = 100
inter-packet gap = 5 ms
digital carrier offset = +125000 Hz
TX VGA = 20 dB
RF amp = OFF
antenna bias = OFF
common 10 MHz reference = VERIFIED
distance = approximately 2 m
LOS = YES
video = OFF
The waveform is one finite burst. Never add repeat mode.
## 3. Read-only preflight
Before the watcher can request RED approval:
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 TX serial 000000000000000097c063dc323b97d3.
7. Require the recorded MASTER CLKOUT command result = PASS.
8. Require confirmation that both antennas are connected, the nodes are approximately 2 m apart, and LOS is clear.
9. Verify the prepared waveform without radio access:
python -B -X utf8 tools/rf001a_prepare.py --verify-dir data/raw/rf001/rf001a_common_clock_001
10. Verify the actual cs8 bytes through the existing offline receiver:
python -B -X utf8 tools/qpsk_rf_rx.py --iq-file data/raw/rf001/rf001a_common_clock_001/tx_waveform.cs8 --iq-format cs8 --truth-metadata data/raw/rf001/rf001a_common_clock_001/tx_metadata.json --report <new-path-outside-git>\rf001a_tx_offline_verify.json
Require:
```text
canonical waveform = PASS
hardware-format cs8 = PASS
offline hardware-format roundtrip = 100/100 exact
TX waveform path = data/raw/rf001/rf001a_common_clock_001/tx_waveform.cs8
TX waveform size = 17667656 bytes
TX waveform SHA256 = 75f3fa39bfa88b623b7e4e5a02e3a80b52185007397ea56ceaacc13353974b2
sample count = 8833828 complex samples
expected duration = 1.1042285 s
```
canonical waveform = PASS
hardware-format cs8 = PASS
offline hardware-format roundtrip = 100/100 exact
TX waveform path = data/raw/rf001/rf001a_common_clock_001/tx_waveform.cs8
TX waveform size = 17667656 bytes
TX waveform SHA256 = 75f3fa39bfa88b623b7e4e5a02e3a80b52185007397ea56ceaacc13353974b2
sample count = 8833828 complex samples
expected duration = 1.1042285 s
If any value differs, if RX is not ready, or if the RX capture window has ended, do not transmit and STOP.
If any value differs, do not transmit.
Only after every precondition passes may the user provide the separate exact phrase `AUTHORIZE RF TX`.
## 4. Final check before opening TX hardware
## 3. One authorized TX execution
The controlled TX executor must read the authorization record immediately before TX and require:
After the separate RED authorization and the live `RX READY FOR TX = YES` confirmation, create a new unique local evidence directory outside Git. Refuse an existing directory and never overwrite earlier evidence.
- task_id = RF001A_OTA_TX_002;
- authorization = AUTHORIZE RF TX;
- rx_task_id = RF001A_OTA_CAPTURE_002;
- dry_run = false;
- action = MANUAL_EXECUTOR_GATE_OPEN;
- rf_tx_started = false;
- rf_tx = 0;
- the authorization references the latest fetched rx-node result commit and digest;
- current time is still inside the READY capture window with at least 10 seconds remaining.
Use this single finite command:
Fetch origin/rx-node once more and compare its current result digest and commit with the authorization record. Re-run the READY and time checks. If anything differs, if the result is stale, or if fewer than 10 seconds remain, do not transmit and require a new RX capture and a new exact RED approval.
```text
hackrf_transfer.exe
-d 000000000000000097c063dc323b97d3
-t data/raw/rf001/rf001a_common_clock_001/tx_waveform.cs8
-f 1400000000
-s 8000000
-n 8833828
-x 20
-a 0
-p 0
-B
```
## 5. One authorized TX execution
Run it once, synchronously, and preserve the exact process exit code:
Only after every preceding condition passes may the executor create a new unique evidence directory outside Git and run exactly one finite command:
```powershell
$txRunId = '{0}_{1}' -f (Get-Date -Format 'yyyyMMdd_HHmmss'), ([guid]::NewGuid().ToString('N').Substring(0, 8))
$txEvidenceRoot = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'SDR_Rover_TX_Evidence'
$txEvidenceDirectory = Join-Path $txEvidenceRoot "RF001A_OTA_TX_001_$txRunId"
if (Test-Path -LiteralPath $txEvidenceDirectory) { throw "TX evidence directory already exists: $txEvidenceDirectory" }
New-Item -ItemType Directory -Path $txEvidenceDirectory -ErrorAction Stop | Out-Null
hackrf_transfer.exe
-d 000000000000000097c063dc323b97d3
-t data/raw/rf001/rf001a_common_clock_001/tx_waveform.cs8
-f 1400000000
-s 8000000
-n 8833828
-x 20
-a 0
-p 0
-B
$hackrfTransfer = 'C:\Users\user\Tools\HackRF\Library\bin\hackrf_transfer.exe'
$waveformPath = (Resolve-Path -LiteralPath 'data/raw/rf001/rf001a_common_clock_001/tx_waveform.cs8').Path
$stdoutPath = Join-Path $txEvidenceDirectory 'hackrf_transfer.stdout.log'
$stderrPath = Join-Path $txEvidenceDirectory 'hackrf_transfer.stderr.log'
$txArguments = @(
'-d', '000000000000000097c063dc323b97d3',
'-t', $waveformPath,
'-f', '1400000000',
'-s', '8000000',
'-n', '8833828',
'-x', '20',
'-a', '0',
'-p', '0',
'-B'
)
Run it once, synchronously, and preserve:
$txStartedAt = [DateTimeOffset]::Now
$txProcess = Start-Process -FilePath $hackrfTransfer -ArgumentList $txArguments `
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath `
-PassThru -Wait -WindowStyle Hidden
$txFinishedAt = [DateTimeOffset]::Now
$txProcess.Refresh()
$txExitCode = $txProcess.ExitCode
```
- exact process PID;
- start and end UTC timestamps;
- exact exit code;
- complete stdout and stderr;
- every TX underrun line;
- every USB error;
- waveform path, size and SHA256;
- command parameters.
Immediately write the exit code, process ID, timestamps, duration, command parameters, waveform size and waveform SHA256 to a new JSON metadata file in `$txEvidenceDirectory`. Preserve complete stdout and stderr.
Set RF TX STOPPED = YES only after the finite process exits. Never retry automatically. Never start a second burst.
Inspect every buffer-statistics line. Record:
## 6. Final TX report
```text
TX start time =
TX end time =
TX PID =
TX exit code =
TX underruns =
USB errors =
RF TX STOPPED =
YES / NO
```
TASK_ID =
RF001A_OTA_TX_002
Do not infer zero from a missing counter. Use `NOT_REPORTED` when no counter exists. Any nonzero intermediate or final underrun/error line remains evidence even if a later line reports zero.
RED AUTHORIZATION =
AUTHORIZE RF TX / MISSING
Set `RF TX STOPPED = YES` only after the finite process exits. Do not start a second burst for any reason. Do not retry automatically.
RX READY FOR TX =
YES / NO
## 4. Final TX report
RX RESULT HEAD =
```text
TASK_ID =
RF001A_OTA_TX_001
RX RESULT DIGEST =
RED AUTHORIZATION =
AUTHORIZE RF TX / MISSING
TX SERIAL =
RX READY FOR TX =
YES / NO
TX WAVEFORM SHA256 =
TX SERIAL =
TX START TIME =
TX WAVEFORM SHA256 =
TX END TIME =
TX START TIME =
TX EXIT CODE =
TX END TIME =
TX UNDERRUNS =
TX EXIT CODE =
USB ERRORS =
TX UNDERRUNS =
RF TX STOPPED =
YES / NO
USB ERRORS =
BURSTS TRANSMITTED =
0 / 1
RF TX STOPPED =
YES / NO
RF TX =
0
BURSTS TRANSMITTED =
0 / 1
At report time, RF TX = 0 means no transmission process remains active.
RF TX =
0
```
## 7. Prohibited actions
At report time, `RF TX = 0` means no transmission process remains active.
Do not:
## 5. Prohibited actions
- transmit without the fresh READY state and exact RED phrase;
- reuse an earlier authorization record;
- use repeat mode;
- transmit a second burst;
- retry automatically;
- change frequency, gains, clock configuration or waveform;
- update firmware or CPLD;
- use Zadig or change USB drivers;
- commit or push from this task.
Do not transmit before the separate exact RED authorization. Do not inherit approval from the RX task. Do not use repeat mode, start a second burst, change frequency, gains, clock configuration, waveform, firmware, CPLD, Zadig or USB drivers. Do not commit or push from this task.
STOP after the single finite burst or any failed gate.

View File

@@ -2,84 +2,139 @@
## Purpose
The RX laptop watches `origin/video-development` with ordinary local Python code. Git polling does not call an LLM, ChatGPT or an agent. A detected task is never executed until the user approves it. The first implementation uses only `ManualExecutor`: it prepares a local task outbox and does not start a model itself.
The RX laptop watches origin/video-development with ordinary local Python code. Git polling does not call an LLM, ChatGPT or an agent. A detected task is not prepared until the user approves it.
The reverse result channel uses the separate origin/rx-node branch. The RX laptop publishes small machine-readable readiness and completion results. The MAIN PC reads those results without merging rx-node into video-development.
## Task header
`docs/handoff/NEXT_RX_TASK.md` starts with these plain key-value fields:
docs/handoff/NEXT_RX_TASK.md starts with these plain key-value fields:
```text
TASK_ID = RF001A_CLKIN_001
TASK_TITLE = Verify RF-001A slave CLKIN
SOURCE_BRANCH = video-development
EXPECTED_HEAD = <full 40-character base commit hash>
REQUIRES_APPROVAL = YES
HARDWARE_ACCESS = YES
RF_RX = YES
RF_TX = NO
ALLOW_COMMIT = NO
ALLOW_PUSH = RX_RESULT_ONLY
RISK_LEVEL = YELLOW
```
TASK_ID = RF001A_OTA_CAPTURE_002
TASK_TITLE = Start coordinated RF-001A common-clock OTA capture
SOURCE_BRANCH = video-development
EXPECTED_HEAD = <full 40-character base commit hash>
REQUIRES_APPROVAL = YES
HARDWARE_ACCESS = YES
RF_RX = YES
RF_TX = NO
ALLOW_COMMIT = NO
ALLOW_PUSH = RX_RESULT_ONLY
RISK_LEVEL = YELLOW
`TASK_ID` is immutable and unique. A revised task receives a new ID. `EXPECTED_HEAD` names the known base commit against which the task was written; the watcher requires it to be an ancestor of the fetched task commit. This avoids an impossible self-reference to the commit that contains the header. `ALLOW_PUSH = RX_RESULT_ONLY` permits only a small result publication to `rx-node`; it never permits a push to `video-development`. `ALLOW_COMMIT` governs task changes in the source checkout; a separately authorized result-only commit on `rx-node` follows `ALLOW_PUSH`. `RISK_LEVEL` is an optional plain field used to declare RED actions other than RF TX.
TASK_ID is immutable and unique. A revised task receives a new ID. EXPECTED_HEAD identifies the known base commit against which the task was written; the watcher requires it to be an ancestor of the fetched task commit.
The watcher rejects missing, duplicate or invalid fields. It reads the file with `git show origin/video-development:docs/handoff/NEXT_RX_TASK.md`, so it does not need to change the local checkout.
ALLOW_PUSH = RX_RESULT_ONLY authorizes publication of the two allowlisted result files to an existing rx-node branch after the task passes its local approval gate. It never permits a push to video-development and never permits branch creation.
The watcher rejects missing, duplicate or invalid fields. It reads the task with git show origin/video-development:docs/handoff/NEXT_RX_TASK.md and does not need to change the local checkout.
## Safety levels and approval
| Level | Operations | Approval rule |
|---|---|---|
| GREEN | Software-only checks, parsing and local result preparation | The task's normal approval applies |
| YELLOW | Hardware inventory, clock, gain or bounded RF RX | The user must choose `[A] Approve` |
| RED | RF TX, firmware, CPLD, Zadig, USB driver changes, destructive Git or filesystem operations | A normal approval is insufficient; an executor must require the exact separate action `AUTHORIZE RF TX` or an equivalent task-specific explicit confirmation |
| YELLOW | Hardware inventory, clock checks, bounded RF RX and result-only publication | The user must choose [A] Approve for the exact TASK_ID |
| RED | RF TX, firmware, CPLD, Zadig, USB driver changes, destructive Git or filesystem operations | A normal approval is insufficient; RF TX requires the exact separate phrase AUTHORIZE RF TX |
The watcher derives RED whenever `RF_TX = YES` and rejects a lower declared level. Firmware, CPLD, Zadig, USB driver changes and destructive actions must declare `RISK_LEVEL = RED`. A non-RF RED approval requires `AUTHORIZE <TASK_ID>`; RF TX requires exactly `AUTHORIZE RF TX`. The watcher cannot authorize RED itself, and the executor must reject a body/header conflict.
The watcher derives RED whenever RF_TX = YES and rejects a lower declared level. A non-RF RED task requires AUTHORIZE <TASK_ID>. An RF TX task requires exactly AUTHORIZE RF TX.
At the prompt the minimum choices are:
At the RX prompt the choices are:
```text
[A] Approve
[D] Defer
[S] Stop watcher
```
[A] Approve
[D] Defer
[S] Stop watcher
The watcher records the task ID before leaving the prompt. Deferred and already seen IDs are not shown again on every polling cycle. A changed task must have a new `TASK_ID`.
The watcher records last_approved_task_id only after approval succeeds. Result publication requires the result TASK_ID to match that approved ID.
## Local state and executor boundary
Runtime state is stored in `.local/rx_agent_state.json`; prepared prompts and result skeletons are stored in `.local/rx_agent_outbox/<TASK_ID>/`. `.local/` is ignored by Git. State includes `last_seen_task_id`, `last_completed_task_id`, `deferred_task_id`, `running` and the watcher PID. Writes use a temporary file followed by an atomic replace.
Runtime state is stored in .local/rx_agent_state.json. Prepared prompts and result skeletons are stored in .local/rx_agent_outbox/<TASK_ID>/. The .local directory is ignored by Git.
`TaskExecutor` is the backend interface. Version 1 provides `ManualExecutor`, which writes `TASK.md`, `RX_RESULT.md` and `RX_RESULT.json` after approval. It refuses to overwrite an existing task outbox. It does not invoke an agent, access SDR hardware, commit or push. A `CodexExecutor` may be added only after the Codex CLI is found and tested on the RX laptop. Availability on the main PC is not sufficient evidence.
State includes:
After a human or separately authorized executor finishes the task, mark the local ID complete:
- last_seen_task_id;
- last_completed_task_id;
- last_approved_task_id;
- deferred_task_id;
- running;
- watcher PID.
```powershell
python -B tools/rx_agent_watcher.py --mark-completed <TASK_ID>
```
ManualExecutor writes TASK.md, RX_RESULT.md and RX_RESULT.json after approval. It does not invoke an agent, access SDR hardware, commit or push.
## Result and `rx-node` branch
After the task actually finishes, mark the local ID complete:
The result follows [RX_RESULT_TEMPLATE.md](RX_RESULT_TEMPLATE.md) and has matching Markdown and JSON. The proposed publication branch is `rx-node`.
python -B tools/rx_agent_watcher.py --mark-completed <TASK_ID>
The RX laptop must never commit or push to `video-development`. A separately approved publication step may place only these items on `rx-node`:
## Strict result schema
- handoff result Markdown;
- result JSON;
- small log metadata needed to interpret the result.
The coordinated RF-001A result is a strict JSON object. Unknown or missing keys are rejected. READY requires:
Raw IQ, video, waveform files, large logs, secrets and machine-local state remain outside Git. The watcher does not create `rx-node`, switch branches, commit or push. This keeps branch creation and publication subject to the repository's current instructions and a separate explicit authorization.
- task_id = RF001A_OTA_CAPTURE_002;
- state = RX_READY_FOR_TX;
- rx_ready_for_tx, process_alive, file_growing and common_clock_verified all true;
- a positive capture_pid;
- 120-second planned duration;
- rf_tx = 0;
- final capture fields set to null.
The main PC later uses `git fetch origin rx-node` and reads the result without merging it blindly.
COMPLETE requires:
- state = CAPTURE_COMPLETE;
- rx_ready_for_tx, process_alive and file_growing all false;
- exact exit code, size, duration and SHA256;
- error counters as non-negative integers or null when the tool did not report them;
- clkin_after = DETECTED or NOT_DETECTED;
- rf_tx_observed = EXPECTED_EXTERNAL_TX or UNKNOWN;
- rf_tx = 0.
Timestamps use timezone-aware ISO-8601. source_head is the actual local 40-character Git HEAD.
## Publishing to rx-node
Publish a prepared local JSON file with:
python -B tools/rx_agent_watcher.py --publish-result <result-json> --result-branch rx-node
The publisher checks:
1. the current task and result TASK_ID agree;
2. ALLOW_PUSH = RX_RESULT_ONLY;
3. RF_TX = NO;
4. last_approved_task_id matches;
5. result source_head equals the current checkout HEAD;
6. origin/rx-node already exists;
7. the JSON schema is valid.
It clones only the existing result branch into a temporary directory, writes exactly:
docs/handoff/results/RX_RESULT.json
docs/handoff/results/RX_RESULT.md
It checks the staged path set, commits, and pushes HEAD to origin/rx-node. It never switches the source checkout. It rejects any additional staged path and enforces a 64 KiB limit per result file.
Raw IQ, video, waveform files, large logs, secrets and machine-local state remain outside Git. A missing result branch or failed push is reported and never authorizes TX.
## MAIN PC watcher
tools/tx_agent_watcher.py polls origin/rx-node every 5 seconds by default. It does not call an LLM or open SDR hardware.
For a new valid READY result it checks task ID, state, live-process flags, common clock, publication age, planned duration and remaining capture time. It then requests the exact phrase AUTHORIZE RF TX.
After the phrase, it fetches rx-node again and repeats the checks. A changed result, stale publication or fewer than 10 seconds remaining blocks authorization.
A successful gate writes one local authorization record under .local/tx_authorizations/. The record has rf_tx_started = false and rf_tx = 0. The separately controlled TX executor must re-fetch and compare the result commit and digest immediately before opening hardware.
Duplicate result digests are not shown repeatedly.
## Token and model behavior
- Git polling itself does not call an LLM.
- When no new task exists, no model runs.
- A detected but unapproved or deferred task does not run a model.
- After approval, `ManualExecutor` only prepares an outbox. A separately selected executor may invoke one agent.
- The executor stops after task completion. The watcher returns to Git polling and consumes no model tokens.
- RX task polling does not call an LLM.
- MAIN result polling does not call an LLM.
- No model runs while there is no new task or READY result.
- ManualExecutor prepares local files only.
- Neither watcher starts RF TX.
- The TX executor remains a separate RED-controlled action.
## Dry-run contract
Dry-run requires a local `--task-file`; it does not fetch Git, access SDR, invoke an agent, perform RF RX/TX, commit or push. The tracked fake task is `tests/fixtures/watcher_test_task.md` with `TASK_ID = WATCHER_TEST_001` and `RF_TX = NO`.
The fake coordination tests use a temporary local bare Git repository and branch named rx-node. They publish a fake READY result, detect it, require the exact RED phrase and invoke a fake TX action that writes RF TX = 0. They do not fetch the real remote, access SDR hardware, run hackrf_transfer, create the production rx-node branch or transmit RF.

View File

@@ -1,76 +1,83 @@
# RX watcher usage
## START
## Start
On the RX laptop, pull `video-development`, install the project's normal Python dependencies, then double-click:
On the RX laptop, pull video-development and double-click:
```text
tools\start_rx_watcher.cmd
```
tools\start_rx_watcher.cmd
The watcher stays in a visible console and polls `origin/video-development` every 30 seconds. The interval is configurable with `--poll-interval`. It does not start ChatGPT, Codex or another agent.
The watcher stays in a visible console and polls origin/video-development every 30 seconds. It does not start ChatGPT, Codex or another agent.
## IDLE
## Idle
While no new `TASK_ID` exists, the watcher performs only `git fetch`, reads the remote task file and compares IDs with `.local/rx_agent_state.json`. There is no model execution or token use.
While no new TASK_ID exists, the watcher performs only git fetch, reads the remote task file and compares IDs with .local/rx_agent_state.json. There is no model execution or token use.
## NEW TASK
## New task
The console shows:
```text
NEW TASK DETECTED
NEW TASK DETECTED
TASK ID =
TITLE =
SAFETY LEVEL =
HARDWARE ACCESS =
RF RX =
RF TX =
```
TASK ID =
TITLE =
SAFETY LEVEL =
HARDWARE ACCESS =
RF RX =
RF TX =
Choose `[A] Approve`, `[D] Defer` or `[S] Stop watcher`. An RF TX task also requires the exact separate phrase `AUTHORIZE RF TX`; another RED task requires `AUTHORIZE <TASK_ID>`. This only opens the executor gate; the first version's `ManualExecutor` still performs no RF action and starts no model.
Choose [A] Approve, [D] Defer or [S] Stop watcher. RF TX also requires the exact phrase AUTHORIZE RF TX. Another RED action requires AUTHORIZE <TASK_ID>.
## APPROVE
## Approve and execute
After approval, open:
```text
.local\rx_agent_outbox\<TASK_ID>\TASK.md
```
.local\rx_agent_outbox\<TASK_ID>\TASK.md
Transfer that prepared task to the chosen agent manually. Fill the adjacent `RX_RESULT.md` and `RX_RESULT.json` with measured results. When work is actually complete, run:
Transfer that prepared task to the chosen executor. The watcher records last_approved_task_id, which is required for result publication. Preparing the outbox is not evidence that the hardware task ran.
```powershell
python -B tools/rx_agent_watcher.py --mark-completed <TASK_ID>
```
When the task is actually complete, run:
Do not claim completion merely because the prompt was prepared.
python -B tools/rx_agent_watcher.py --mark-completed <TASK_ID>
## DONE and result publication
## Publish READY
If the task says `ALLOW_PUSH = RX_RESULT_ONLY` and the user separately authorizes publication, publish only small result Markdown/JSON and log metadata to `rx-node`. Never commit or push from the RX laptop to `video-development`. Do not put raw IQ, video or waveform files in Git.
For RF001A_OTA_CAPTURE_002, create the strict READY JSON outside Git only after the capture child is alive and the file is growing. Then run:
## STOP
python -B tools/rx_agent_watcher.py --publish-result <ready-json-path> --result-branch rx-node
On success, the watcher prints RX RESULT PUBLISH = PUBLISHED or UNCHANGED, the result commit, and RF TX = 0.
The command fails if:
- the task was not approved locally;
- the TASK_ID differs;
- source_head differs from the checkout HEAD;
- origin/rx-node does not exist;
- the schema is invalid;
- Git commit or push fails.
A failed publication is not READY evidence and cannot authorize TX.
## Publish COMPLETE
After capture completion, build the strict final JSON with state CAPTURE_COMPLETE and publish it with the same command:
python -B tools/rx_agent_watcher.py --publish-result <final-json-path> --result-branch rx-node
Only the two fixed result files are committed. Raw IQ, video, waveform, stdout, stderr and large logs stay outside Git.
## Stop
Double-click:
```text
tools\stop_rx_watcher.cmd
```
tools\stop_rx_watcher.cmd
The script creates a local stop request and waits for the watcher PID to clear. The approval prompt is interruptible on Windows, and the polling delay checks the stop request every 0.1 seconds. A successful stop prints `WATCHER STOPPED`; no polling process remains.
The script creates a local stop request and waits for the watcher PID to clear. A successful stop prints WATCHER STOPPED.
## DRY RUN
## Dry run
From the repository root:
```powershell
python -B tools/rx_agent_watcher.py --dry-run --once --approval approve --task-file tests/fixtures/watcher_test_task.md
```
python -B tools/rx_agent_watcher.py --dry-run --once --approval approve --task-file tests/fixtures/watcher_test_task.md
Expected behavior: the fake task is detected, approved and written under `.local/rx_agent_outbox/WATCHER_TEST_001/`. No Git fetch, SDR access, agent call, RF RX/TX, commit or push occurs. Delete `.local/` at any time if the local state and outbox are no longer needed.
## Executor availability
Version 1 always provides `ManualExecutor`. Before adding or selecting a Codex backend on RX, run `Get-Command codex` on that laptop and verify a harmless local invocation. Do not infer RX availability from the main PC.
The fake task is written under .local/rx_agent_outbox/WATCHER_TEST_001/. No Git fetch, SDR access, agent call, RF RX, RF TX, commit or push occurs.

View File

@@ -1,23 +1,55 @@
TASK_ID =
TASK_STATUS =
SOURCE_HEAD =
STARTED_AT =
FINISHED_AT =
# RX result schema
SUMMARY =
The publisher creates both docs/handoff/results/RX_RESULT.json and docs/handoff/results/RX_RESULT.md on the existing rx-node branch. JSON is authoritative. Markdown is generated from the same validated object.
CHECKS =
## READY JSON
HARDWARE_ACTIONS =
{
"schema_version": 1,
"task_id": "RF001A_OTA_CAPTURE_002",
"state": "RX_READY_FOR_TX",
"rx_ready_for_tx": true,
"process_alive": true,
"file_growing": true,
"capture_pid": 1234,
"published_at": "2026-09-21T12:00:02+00:00",
"capture_started_at": "2026-09-21T12:00:00+00:00",
"expected_capture_end": "2026-09-21T12:02:00+00:00",
"capture_path": "C:\\Users\\Professional\\Documents\\SDR_Rover_Captures\\<run>\\rf001a_ota_capture.cs8",
"common_clock_verified": true,
"source_head": "<40-character lowercase 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"
}
RF_TX =
YES / NO
## COMPLETE JSON
FILES_CREATED =
The final object keeps the same identity, timing, path and PID. It changes these fields:
ERRORS =
state = CAPTURE_COMPLETE
rx_ready_for_tx = false
process_alive = false
file_growing = false
published_at = <final publication UTC time>
exit_code = <integer>
file_size = <integer bytes>
duration_seconds = <number>
overruns = <non-negative integer or null>
usb_errors = <non-negative integer or null>
file_errors = <non-negative integer or null>
sha256 = <lowercase 64-character SHA256>
clkin_after = DETECTED or NOT_DETECTED
rf_tx_observed = EXPECTED_EXTERNAL_TX or UNKNOWN
rf_tx = 0
NEXT_ACTION =
Use null when the capture tool did not report an error counter. Do not infer zero.
The result publisher must also create `RX_RESULT.json` with the same fields.
Raw IQ, video and waveform files must stay outside Git.
Raw IQ, video, waveform files and large logs must stay outside Git.

View File

@@ -0,0 +1,47 @@
# MAIN TX watcher usage
## Purpose
tools/tx_agent_watcher.py is an ordinary local Python process. It polls the small RX result on origin/rx-node. It does not call an LLM and does not open HackRF hardware.
## Start
From the repository root, double-click:
tools\start_tx_watcher.cmd
The visible console polls every 5 seconds. A missing rx-node branch or invalid result is reported. No model runs while polling.
## READY and RED approval
For a fresh valid RF001A_OTA_CAPTURE_002 READY result, the watcher prints the task ID, capture start and expected end. It then asks for:
AUTHORIZE RF TX
Any other input blocks authorization.
After the exact phrase, the watcher fetches rx-node again. It requires the same result digest, a fresh publication, an active 120-second capture window and at least 10 seconds remaining.
On success it writes:
.local\tx_authorizations\RF001A_OTA_TX_002_authorized.json
This record opens the gate for the separately controlled TX executor. The watcher itself does not run hackrf_transfer. The record explicitly says rf_tx_started = false and rf_tx = 0.
The TX executor must re-fetch rx-node and compare the result commit and digest immediately before hardware access.
## Stop
Double-click:
tools\stop_tx_watcher.cmd
The stop command writes a local stop request and waits for the watcher PID to clear.
## Fake mode
Automated tests use a local bare Git repository. For a single local JSON fixture, the CLI dry-run form is:
python -B tools/tx_agent_watcher.py --dry-run --once --result-file <ready-json> --approval authorize
The --approval option is rejected outside --dry-run. Fake mode writes a local fake authorization and never fetches the production remote or accesses SDR hardware.