Fix bidirectional RF-001A coordination

This commit is contained in:
LittleSam129
2026-09-21 18:05:28 +03:00
parent bc55d0516d
commit a516b99015
17 changed files with 1745 additions and 244 deletions

154
tools/rf_handoff_smoke.py Normal file
View File

@@ -0,0 +1,154 @@
"""Publish a model-free READY/COMPLETE smoke sequence to the RX result branch."""
from __future__ import annotations
import argparse
from dataclasses import replace
from datetime import datetime, timedelta, timezone
import hashlib
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
from typing import Callable
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tools.rf_handoff_channel import ( # noqa: E402
CHANNEL_TEST_TASK_ID,
COMPLETE_STATE,
READY_STATE,
GitResultChannel,
PublishOutcome,
RxHandoffResult,
)
def _git_head(repository: Path) -> str:
return subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=repository,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=True,
).stdout.strip()
def run_smoke(
repository: Path,
*,
remote: str = "origin",
branch: str = "rx-node",
source_head: str | None = None,
hold_seconds: float = 7.0,
sleep_function: Callable[[float], None] = time.sleep,
) -> tuple[PublishOutcome, PublishOutcome, RxHandoffResult]:
"""Publish fake READY then COMPLETE; never access SDR or open a TX gate."""
repository = repository.resolve()
observed_source_head = source_head or _git_head(repository)
channel = GitResultChannel(repository, remote=remote, branch=branch)
channel.fetch()
with tempfile.TemporaryDirectory(prefix="rf-handoff-smoke-") as directory:
fake_capture = Path(directory) / "model_free_fake_capture.txt"
started = datetime.now(timezone.utc)
fake_capture.write_text(
"RF_HANDOFF_CHANNEL_TEST_001\nREADY\nRF TX = 0\n",
encoding="utf-8",
)
published = datetime.now(timezone.utc)
ready = RxHandoffResult(
schema_version=1,
task_id=CHANNEL_TEST_TASK_ID,
state=READY_STATE,
rx_ready_for_tx=True,
process_alive=True,
file_growing=True,
capture_pid=max(1, os.getpid()),
published_at=published.isoformat(),
capture_started_at=started.isoformat(),
expected_capture_end=(started + timedelta(seconds=120)).isoformat(),
capture_path=str(fake_capture.resolve()),
common_clock_verified=False,
source_head=observed_source_head,
rf_tx=0,
exit_code=None,
file_size=None,
duration_seconds=None,
overruns=None,
usb_errors=None,
file_errors=None,
sha256=None,
clkin_after="NOT_RUN",
rf_tx_observed="UNKNOWN",
)
ready_outcome = channel.publish(ready)
sleep_function(hold_seconds)
fake_capture.write_text(
"RF_HANDOFF_CHANNEL_TEST_001\nCOMPLETE\nRF TX = 0\n",
encoding="utf-8",
)
finished = datetime.now(timezone.utc)
content = fake_capture.read_bytes()
complete = replace(
ready,
state=COMPLETE_STATE,
rx_ready_for_tx=False,
process_alive=False,
file_growing=False,
published_at=finished.isoformat(),
exit_code=0,
file_size=len(content),
duration_seconds=(finished - started).total_seconds(),
overruns=0,
usb_errors=0,
file_errors=0,
sha256=hashlib.sha256(content).hexdigest(),
)
complete_outcome = channel.publish(complete)
return ready_outcome, complete_outcome, complete
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repository", type=Path, default=ROOT)
parser.add_argument("--remote", default="origin")
parser.add_argument("--result-branch", default="rx-node")
parser.add_argument("--source-head")
parser.add_argument("--hold-seconds", type=float, default=7.0)
return parser
def main() -> None:
arguments = build_parser().parse_args()
if arguments.hold_seconds < 0:
raise SystemExit("--hold-seconds cannot be negative")
ready, complete, result = run_smoke(
arguments.repository,
remote=arguments.remote,
branch=arguments.result_branch,
source_head=arguments.source_head,
hold_seconds=arguments.hold_seconds,
)
print("TASK_ID =")
print(CHANNEL_TEST_TASK_ID)
print(f"READY PUBLISH = {ready.status}")
print(f"READY REMOTE HEAD = {ready.remote_head}")
print(f"COMPLETE PUBLISH = {complete.status}")
print(f"COMPLETE REMOTE HEAD = {complete.remote_head}")
print(f"FINAL STATE = {result.state}")
print("REAL RX = NOT_RUN")
print("RF TX = 0")
if __name__ == "__main__":
main()