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:
- Bind is gated on radio readiness (the
assert_radio_startup_ready before start_server) → connection-refused instead of bind-then-RPRT.
- 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)
- Bind the listener unconditionally at startup — remove
assert_radio_startup_ready from the bind path so the port is always available (no connection-refused).
- Keep the already-static handshake (
dump_state/chk_vfo/get_powerstat).
- 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
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).
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 hamlibrigctlddoes not behave this way: it binds first and reports not-ready at the protocol level. This issue tracks making our server semantically indistinguishable from referencerigctld.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()callsassert_radio_startup_ready(self._radio, ...)beforeasyncio.start_server().assert_radio_startup_ready()(src/rigplane/runtime/startup_checks.py) raisesRuntimeErrorunlessconnected and ready and control_connected is not False(whereradio_readyrequires 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
rigctldis the opposite — bind-before-open, serve-regardless-of-rig:tests/rigctld.c:// continue even if opening the rig fails, because it may be powered off.\dump_stateanswers 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.RPRT -<errno>over a held-open socket (-5RIG_ETIMEOUT,-6RIG_EIO,-20RIG_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_stateanswers 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_statereturns static capabilities (handler.py),chk_vfoalways responds,get_powerstatreturns "1" to avoid blocking the hamlib startup probe. The handshake already succeeds with a silent rig. Two defects remain:assert_radio_startup_readybeforestart_server) → connection-refused instead of bind-then-RPRT.RPRTerrors. 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)
assert_radio_startup_readyfrom the bind path so the port is always available (no connection-refused).dump_state/chk_vfo/get_powerstat).RPRT -5/-6(and-20when 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
rigctldin front of a warming radio. Honest (fail-visible, no masking) and faithful.Acceptance criteria
\dump_stateand\chk_vfosucceed from static capabilities even with a silent rig (preserve current behavior).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.Alternatives considered
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.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
tests/rigctld.c,tests/rigctl_parse.c,rigs/dummy/netrigctl.c,tests/dumpcaps.c,include/hamlib/rig.h(github.com/Hamlib/Hamlib).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).