J
2026-04-13 11:48:15 -04:00
|
|
|
"""Unified ``ria-agent`` CLI.
|
|
|
|
|
|
|
|
|
|
Subcommands:
|
|
|
|
|
|
|
|
|
|
- ``ria-agent run [legacy args]`` — legacy long-poll NodeAgent (unchanged).
|
|
|
|
|
- ``ria-agent stream`` — new WebSocket-based IQ streamer.
|
|
|
|
|
- ``ria-agent detect`` — print SDR drivers whose modules import cleanly.
|
J
2026-04-16 11:13:43 -04:00
|
|
|
- ``ria-agent register --hub URL --api-key KEY`` — register with the hub and
|
|
|
|
|
save credentials (and optional TX interlocks) to ``~/.ria/agent.json``.
|
J
2026-04-13 11:48:15 -04:00
|
|
|
|
|
|
|
|
Invoking ``ria-agent`` with no subcommand falls through to the legacy
|
|
|
|
|
long-poll behavior for back-compatibility with existing deployments.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import asyncio
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
from . import config as _config
|
|
|
|
|
from .hardware import available_devices
|
|
|
|
|
from .legacy_executor import main as _legacy_main
|
|
|
|
|
|
|
|
|
|
_LEGACY_ALIASES = {"--hub", "--key", "--name", "--device", "--insecure", "--log-level", "--config"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cmd_detect(_args: argparse.Namespace) -> int:
|
|
|
|
|
devices = available_devices()
|
|
|
|
|
if not devices:
|
|
|
|
|
print("No SDR drivers available (install ria-toolkit-oss[all-sdr] or per-driver extras).")
|
|
|
|
|
return 0
|
|
|
|
|
for name in devices:
|
|
|
|
|
print(name)
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cmd_register(args: argparse.Namespace) -> int:
|
J
2026-04-13 12:54:05 -04:00
|
|
|
import urllib.request
|
|
|
|
|
|
|
|
|
|
hub_url = args.hub.rstrip("/")
|
|
|
|
|
url = f"{hub_url}/screens/agents/register"
|
|
|
|
|
body = json.dumps({"name": args.name or ""}).encode()
|
|
|
|
|
req = urllib.request.Request(
|
|
|
|
|
url,
|
|
|
|
|
data=body,
|
|
|
|
|
headers={
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
"X-API-Key": args.api_key,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen(req) as resp:
|
|
|
|
|
data = json.loads(resp.read())
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"error: registration failed: {e}", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
agent_id = data["agent_id"]
|
|
|
|
|
token = data["token"]
|
|
|
|
|
|
J
2026-04-13 11:48:15 -04:00
|
|
|
cfg = _config.load()
|
J
2026-04-13 12:54:05 -04:00
|
|
|
cfg.hub_url = hub_url
|
|
|
|
|
cfg.agent_id = agent_id
|
|
|
|
|
cfg.token = token
|
|
|
|
|
cfg.api_key = args.api_key
|
J
2026-04-13 11:48:15 -04:00
|
|
|
if args.name:
|
|
|
|
|
cfg.name = args.name
|
|
|
|
|
cfg.insecure = bool(args.insecure)
|
J
2026-04-16 11:13:43 -04:00
|
|
|
cfg.tx_enabled = bool(getattr(args, "allow_tx", False))
|
|
|
|
|
if (v := getattr(args, "tx_max_gain_db", None)) is not None:
|
|
|
|
|
cfg.tx_max_gain_db = float(v)
|
|
|
|
|
if (v := getattr(args, "tx_max_duration_s", None)) is not None:
|
|
|
|
|
cfg.tx_max_duration_s = float(v)
|
|
|
|
|
freq_ranges = getattr(args, "tx_freq_range", None) or []
|
|
|
|
|
if freq_ranges:
|
|
|
|
|
cfg.tx_allowed_freq_ranges = [[float(lo), float(hi)] for lo, hi in freq_ranges]
|
J
2026-04-13 11:48:15 -04:00
|
|
|
path = _config.save(cfg)
|
J
2026-04-13 12:54:05 -04:00
|
|
|
|
|
|
|
|
print(f"Registered agent: {agent_id}")
|
J
2026-04-16 11:13:43 -04:00
|
|
|
if cfg.tx_enabled:
|
|
|
|
|
caps: list[str] = []
|
|
|
|
|
if cfg.tx_max_gain_db is not None:
|
|
|
|
|
caps.append(f"gain<={cfg.tx_max_gain_db} dB")
|
|
|
|
|
if cfg.tx_max_duration_s is not None:
|
|
|
|
|
caps.append(f"duration<={cfg.tx_max_duration_s} s")
|
|
|
|
|
if cfg.tx_allowed_freq_ranges:
|
|
|
|
|
caps.append(f"freq in {cfg.tx_allowed_freq_ranges}")
|
|
|
|
|
tail = f" ({', '.join(caps)})" if caps else ""
|
|
|
|
|
print(f"TX enabled{tail}")
|
J
2026-04-13 12:54:05 -04:00
|
|
|
print(f"Credentials saved to {path}")
|
J
2026-04-13 11:48:15 -04:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cmd_stream(args: argparse.Namespace) -> int:
|
|
|
|
|
from .streamer import run_streamer
|
|
|
|
|
|
|
|
|
|
cfg = _config.load()
|
|
|
|
|
url = args.url or _derive_ws_url(cfg.hub_url, cfg.agent_id)
|
|
|
|
|
token = args.token or cfg.token
|
|
|
|
|
if not url:
|
|
|
|
|
print("error: --url is required (or run `ria-agent register` first)", file=sys.stderr)
|
|
|
|
|
return 2
|
J
2026-04-16 11:13:43 -04:00
|
|
|
if getattr(args, "allow_tx", False):
|
|
|
|
|
cfg.tx_enabled = True
|
J
2026-04-13 11:48:15 -04:00
|
|
|
try:
|
J
2026-04-16 11:13:43 -04:00
|
|
|
asyncio.run(run_streamer(url, token, cfg=cfg))
|
J
2026-04-13 11:48:15 -04:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
pass
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _derive_ws_url(hub_url: str, agent_id: str) -> str:
|
|
|
|
|
if not hub_url:
|
|
|
|
|
return ""
|
|
|
|
|
base = hub_url.rstrip("/")
|
|
|
|
|
if base.startswith("https://"):
|
|
|
|
|
base = "wss://" + base[len("https://"):]
|
|
|
|
|
elif base.startswith("http://"):
|
|
|
|
|
base = "ws://" + base[len("http://"):]
|
J
2026-04-13 12:54:05 -04:00
|
|
|
suffix = f"/screens/agent/ws?agent_id={agent_id}" if agent_id else "/screens/agent/ws"
|
J
2026-04-13 11:48:15 -04:00
|
|
|
return base + suffix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
# Back-compat: if the first non-flag token matches a known legacy flag,
|
|
|
|
|
# or there is no subcommand at all, dispatch to the legacy CLI.
|
|
|
|
|
argv = sys.argv[1:]
|
|
|
|
|
if not argv or (argv[0].startswith("--") and argv[0] in _LEGACY_ALIASES):
|
|
|
|
|
_legacy_main()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog="ria-agent")
|
|
|
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
|
|
|
|
|
|
sub.add_parser("run", help="Legacy long-poll agent (NodeAgent)")
|
|
|
|
|
sub.add_parser("detect", help="List available SDR drivers")
|
|
|
|
|
|
J
2026-04-13 12:54:05 -04:00
|
|
|
p_reg = sub.add_parser("register", help="Register agent with RIA Hub and save credentials")
|
|
|
|
|
p_reg.add_argument("--hub", required=True, help="RIA Hub URL (e.g. http://whitehorse:3005)")
|
|
|
|
|
p_reg.add_argument("--api-key", dest="api_key", required=True, help="Hub API key")
|
|
|
|
|
p_reg.add_argument("--name", default=None, help="Human-friendly agent name")
|
|
|
|
|
p_reg.add_argument("--insecure", action="store_true", help="Skip TLS verification")
|
J
2026-04-16 11:13:43 -04:00
|
|
|
p_reg.add_argument(
|
|
|
|
|
"--allow-tx",
|
|
|
|
|
dest="allow_tx",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Opt this agent in to TX (required for any transmission from the hub)",
|
|
|
|
|
)
|
|
|
|
|
p_reg.add_argument(
|
|
|
|
|
"--tx-max-gain-db",
|
|
|
|
|
dest="tx_max_gain_db",
|
|
|
|
|
type=float,
|
|
|
|
|
default=None,
|
|
|
|
|
help="Reject tx_start frames whose tx_gain exceeds this cap (dB)",
|
|
|
|
|
)
|
|
|
|
|
p_reg.add_argument(
|
|
|
|
|
"--tx-max-duration-s",
|
|
|
|
|
dest="tx_max_duration_s",
|
|
|
|
|
type=float,
|
|
|
|
|
default=None,
|
|
|
|
|
help="Auto-stop any TX session after this many seconds",
|
|
|
|
|
)
|
|
|
|
|
p_reg.add_argument(
|
|
|
|
|
"--tx-freq-range",
|
|
|
|
|
dest="tx_freq_range",
|
|
|
|
|
type=float,
|
|
|
|
|
nargs=2,
|
|
|
|
|
action="append",
|
|
|
|
|
metavar=("LO", "HI"),
|
|
|
|
|
default=None,
|
|
|
|
|
help="Allowed TX center-frequency range in Hz (repeat for multiple bands)",
|
|
|
|
|
)
|
J
2026-04-13 11:48:15 -04:00
|
|
|
|
|
|
|
|
p_stream = sub.add_parser("stream", help="Run the WebSocket IQ streamer")
|
|
|
|
|
p_stream.add_argument("--url", default=None, help="Override WebSocket URL")
|
|
|
|
|
p_stream.add_argument("--token", default=None, help="Override bearer token")
|
|
|
|
|
p_stream.add_argument("--log-level", default="INFO")
|
J
2026-04-16 11:13:43 -04:00
|
|
|
p_stream.add_argument(
|
|
|
|
|
"--allow-tx",
|
|
|
|
|
dest="allow_tx",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Runtime override: enable TX for this process without writing config",
|
|
|
|
|
)
|
J
2026-04-13 11:48:15 -04:00
|
|
|
|
|
|
|
|
# Unknown extras are forwarded to the legacy CLI when command == "run".
|
|
|
|
|
args, extras = parser.parse_known_args(argv)
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
level=getattr(logging, getattr(args, "log_level", "INFO"), logging.INFO),
|
|
|
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if args.command == "run":
|
|
|
|
|
sys.argv = [sys.argv[0], *extras]
|
|
|
|
|
_legacy_main()
|
|
|
|
|
return
|
|
|
|
|
if args.command == "detect":
|
|
|
|
|
sys.exit(_cmd_detect(args))
|
|
|
|
|
if args.command == "register":
|
|
|
|
|
sys.exit(_cmd_register(args))
|
|
|
|
|
if args.command == "stream":
|
|
|
|
|
sys.exit(_cmd_stream(args))
|
|
|
|
|
|
|
|
|
|
parser.error(f"unknown command: {args.command}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|