Skip to content

rigctld: bind listener unconditionally + return RPRT errors when CAT not ready (hamlib-faithful readiness) #1901

Description

@morozsm

Summary

The built-in rigctld TCP server (src/rigplane/rigctld/{server,handler,protocol}.py) gates its listener bind on full radio readiness. A hamlib CAT client (e.g. WSJT-X) that connects right after launch — before the radio CI-V link is fully warm — gets connection-refused, must retry, and only goes green once the radio is ready. Real hamlib rigctld does not behave this way: it binds first and reports not-ready at the protocol level. This issue tracks making our server semantically indistinguishable from reference rigctld.

Symptom

On first launch, a CAT client does not immediately connect to rigctld on the configured port; the user has to wait/retry before it connects and responds.

Root cause

RigctldServer.start() calls assert_radio_startup_ready(self._radio, ...) before asyncio.start_server(). assert_radio_startup_ready() (src/rigplane/runtime/startup_checks.py) raises RuntimeError unless connected and ready and control_connected is not False (where radio_ready requires a live CI-V stream with recent data). So while the radio is still warming up, start() raises and the listener is never bound → connection-refused for any early client. The bind is effectively gated on full radio readiness.

Hamlib reference behavior (verified against Hamlib master)

A real rigctld is the opposite — bind-before-open, serve-regardless-of-rig:

  • Binds the TCP listener before/independently of opening the rig, and continues serving even if rig open fails. Source comment in tests/rigctld.c: // continue even if opening the rig fails, because it may be powered off.
  • Accepts connections independent of rig state; never drops a client merely because the rig is unreachable.
  • \dump_state answers from static capabilities (not a live rig query), so the hamlib client handshake (netrigctl_open: \chk_vfo + \dump_state) succeeds even with a silent rig — the client reaches a 'connected' state from the handshake alone.
  • Live commands when the rig is unreachable return RPRT -<errno> over a held-open socket (-5 RIG_ETIMEOUT, -6 RIG_EIO, -20 RIG_EPOWER), not a TCP drop. Soft errors keep serving; only a persistent hard error past a bounded reopen retry (~3× ~1s) closes that single client socket — never the listener.

Net reference semantics: always listening, dump_state answers statically, rig-not-ready surfaces as RPRT errors on live commands over a held-open connection.

Current behavior vs the gap

Our server is already ~80% faithful: dump_state returns static capabilities (handler.py), chk_vfo always responds, get_powerstat returns "1" to avoid blocking the hamlib startup probe. The handshake already succeeds with a silent rig. Two defects remain:

  1. Bind is gated on radio readiness (the assert_radio_startup_ready before start_server) → connection-refused instead of bind-then-RPRT.
  2. Live get/set commands can return stale/zero values when the CAT link isn't live, instead of honest RPRT errors. Stale data is worse than an error (a client may act on a wrong frequency); reference returns RPRT.

Proposed fix (Approach A — hamlib-faithful, contained)

  1. Bind the listener unconditionally at startup — remove assert_radio_startup_ready from the bind path so the port is always available (no connection-refused).
  2. Keep the already-static handshake (dump_state/chk_vfo/get_powerstat).
  3. Return RPRT -5/-6 (and -20 when known powered-off) for live get/set commands while the CAT/CI-V link is not live, over a held-open socket, instead of stale/zero data. Optionally mirror hamlib's bounded reopen cadence.

Result: a CAT client connects on the first try, completes the handshake, sees honest RPRT errors on live polls until the rig is warm, then goes green — exactly like reference rigctld in front of a warming radio. Honest (fail-visible, no masking) and faithful.

Acceptance criteria

  • rigctld listener binds at startup regardless of radio readiness; a client connecting before the radio is warm is not refused.
  • \dump_state and \chk_vfo succeed from static capabilities even with a silent rig (preserve current behavior).
  • Live get/set commands return RPRT -5/-6 (or -20) when the CAT link is not live, keeping the connection open; no stale/zeroed values are returned as if valid.
  • The listener and process are never torn down due to rig-not-ready (only a single client socket may close after persistent hard error, matching reference).
  • Regression test: simulate radio-not-ready, assert bind succeeds + handshake succeeds + live command yields RPRT (failing-first).

Alternatives considered

  • B — embed/spawn real Hamlib rigctld: would need a Hamlib rig backend for the networked radio session; it would contend with the existing radio-state authority and lose the integrated state store / PTT arbiter. Not advisable.
  • C — reuse only Hamlib's protocol layer as a library: Hamlib does not expose a bring-your-own-backend protocol server; this reduces to re-implementing the same logic as A at higher cost.

Boundary / scope

open-core candidate — generic rigctld protocol/readiness behavior, lives in this repo. Not part of any in-flight beta; this is a durable follow-up. A full design spec + implementation plan can be written when picked up.

References

  • Hamlib source: tests/rigctld.c, tests/rigctl_parse.c, rigs/dummy/netrigctl.c, tests/dumpcaps.c, include/hamlib/rig.h (github.com/Hamlib/Hamlib).
  • Affected files: src/rigplane/rigctld/server.py (start() bind path), src/rigplane/runtime/startup_checks.py (assert_radio_startup_ready), src/rigplane/rigctld/handler.py (live-command readiness gating).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions