Skip to content

Multi-radio tune: SunSDR/TCI backend + client-selected radio config - #4

Open
VU3ESV wants to merge 2 commits into
vu2cpl:mainfrom
VU3ESV:feat/multi-radio-tci
Open

Multi-radio tune: SunSDR/TCI backend + client-selected radio config#4
VU3ESV wants to merge 2 commits into
vu2cpl:mainfrom
VU3ESV:feat/multi-radio-tci

Conversation

@VU3ESV

@VU3ESV VU3ESV commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

What & why

Extends the orchestrated TUNE / band sweep beyond FlexRadio to Expert Electronics SunSDR / ExpertSDR3 over the TCI protocol, and lets a client pick & configure the active radio at runtime (no restart) instead of it being hard-coded in config.yaml. TCI command set verified against the sm5tog/sm5k-spe-tuner reference.

The tune sequence is unchanged and proven — only the per-rig commands differ:

Step Flex (SmartSDR) SunSDR (TCI)
Set freq slice t <s> <MHz> vfo:<trx>,0,<Hz>;
Set mode slice s <s> mode=CWU modulation:<trx>,CW;
Tune carrier transmit tune on/off tune:<trx>,true/false;

How

  • Radio abstractionspe/radio.py (RadioConnection ABC) with the small primitive set the orchestrator needs. FlexConnection implements it (thin adapters; VFO snapshot/restore moved into the backend). New spe/tci.py (TciConnection) is an async TCI client over tornado's WebSocket client — no new dependency.
  • RadioController (replaces FlexController) builds the backend by radio.kind (flex|tci|none), keeps the on-demand connect/disconnect lifecycle, and adds reconfigure() for live radio switching. Status phases are now generic RADIO_CONNECTING/CONNECTED/DISCONNECTED/ERROR.
  • Config + client controlRadioConfig{kind} + TciConfig; back-compat derives kind from flex.enabled. New persist_values() (generic comment-preserving YAML writer). WS: get_config + set_radio_config:<json> (live switch + persist, refused mid-tune); radio_connect/radio_disconnect (with flex_* aliases). Bundled dashboard gains a RADIO settings panel.
  • Docsdocs/CLIENT_RADIO_CONFIG.md (WS contract + config schema + MacExpert UX spec; the client app is updated separately) and README.

Backward compatibility

Existing flex-only configs keep working (kind derived from flex.enabled); flex_connect/flex_disconnect and FLEX_* still accepted. Flex command output is byte-for-byte unchanged (verified by test).

Testing (local)

  • TciConnection emits exactly vfo:0,0,14025000; / modulation:0,CW; / tune:0,true|false; and converts MHz→Hz; snapshot from the TCI event cache.
  • RadioController builds flex vs tci by kind, reconfigure() switches live, kind=none returns None.
  • config.py loads radio:/tci:, back-compat kind derivation, persist_values() preserves comments (round-trip).
  • Orchestrator connect→cycle→disconnect (failure path) through the generic interface for both backends; Flex generic methods produce identical commands.
  • Full py_compile / import graph.
  • Live SunSDR not exercised (no SunSDR on the bench) — TCI strings are from the reference implementation; please validate against ExpertSDR3.

Stacks on #3 (the on-demand-connection PR). Until #3 is merged, this PR's diff also contains that commit (5dc5d9b); it'll reduce to just the multi-radio commit once #3 lands. Merge order: #3 first, then this.

VU3ESV and others added 2 commits June 19, 2026 23:09
spe-remote previously opened the SmartSDR TCP session at startup and held
it for the whole life of the process. That kept the radio marked "in use"
by spe-remote even when nobody was tuning, and meant the radio had to be
on at startup (and spe-remote restarted if it was ever power-cycled).

Make the Flex connection on-demand instead:

- New spe/flex_controller.py: FlexController owns at most one
  FlexConnection, opened/closed on demand under an asyncio.Lock. connect()
  resolves the host lazily (static flex.host, else UDP discovery) so the
  radio may be off at startup and still be found later; both connect() and
  disconnect() are idempotent and never raise. Transitions broadcast as
  tune_event phases FLEX_CONNECTING / FLEX_CONNECTED / FLEX_DISCONNECTED /
  FLEX_ERROR.
- TuneOrchestrator acquires the connection at the start of tune_single /
  tune_band and disconnects in the finally — so the radio is released as
  soon as the cycle or sweep is over, even on failure/abort. A sweep holds
  one connection across all sub-bands.
- websocket_handler: new flex_connect / flex_disconnect commands. The web
  dashboard sends flex_connect when the Sweep panel opens and
  flex_disconnect when it closes while idle (never mid-tune). Both are
  no-ops — not forwarded to the serial handler — when Flex is disabled.
- server.py: build a FlexController (no startup connect); the lazy connect
  at tune start keeps clients that never send flex_connect working.
- web/app.js: pre-warm on Sweep-menu open, drop on idle close; FLEX_*
  events show pre-tune status without clobbering a finished sweep result.
- flex.py: add FlexConnection.is_connected.
- README: document the on-demand lifecycle and the new WS commands.

Backward compatible: older clients (MacExpert, Node-RED, Vue) that only
send tune_band/tune_stop still work via the lazy connect-at-tune-start
safety net and get the disconnect-when-done behaviour for free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalises the orchestrated TUNE / band sweep beyond FlexRadio so it can
also drive an Expert Electronics SunSDR / ExpertSDR3 over TCI, and lets a
client pick & configure the active radio at runtime (no restart). TCI
command set verified against github.com/sm5tog/sm5k-spe-tuner.

Radio abstraction:
- spe/radio.py: RadioConnection ABC — the small primitive set the tune
  orchestrator needs (connect/close, set_frequency MHz, set_mode, set_tune
  _power, tune_carrier, snapshot/restore).
- spe/flex.py: FlexConnection implements RadioConnection (thin adapters over
  the slice commands; VFO snapshot/restore moved here from the orchestrator).
- spe/tci.py: TciConnection — async TCI client over tornado's WebSocket
  client (no new dependency). vfo:/modulation:/tune: commands; trx TX cache.
- spe/radio_controller.py: replaces FlexController. Builds the backend by
  radio.kind (flex|tci|none), keeps the on-demand connect/disconnect
  lifecycle, and adds reconfigure() for live radio switching. Status phases
  are now generic RADIO_CONNECTING/CONNECTED/DISCONNECTED/ERROR.
- spe/tune_orchestrator.py: drives the generic interface; same proven
  sequence (SPE TUNE keycode → RCU LED → carrier → LED off → carrier off).

Config + client control:
- spe/config.py: RadioConfig{kind} + TciConfig; parse radio:/tci: sections;
  back-compat derives kind from flex.enabled; persist_values() — a generic
  comment-preserving YAML writer (generalises persist_temperature_unit).
- websocket_handler: get_config + set_radio_config:<json> (live switch +
  persist, refused mid-tune), radio_connect/radio_disconnect (flex_* kept
  as aliases). server.py builds a RadioController from radio.kind.
- config.yaml: radio: + tci: sections (flex: retained). configtool.py:
  radio.kind + tci.* added to SUPPORTED.
- web dashboard: RADIO settings panel (pick rig + edit host/port) wired to
  get_config/set_radio_config; FLEX_*→RADIO_* event handling.

Docs:
- docs/CLIENT_RADIO_CONFIG.md: WS contract + config schema + MacExpert UX
  spec (the client app is updated separately).
- README: multi-radio + client-driven radio config sections.

Backward compatible: existing flex-only configs keep working (kind derived
from flex.enabled); flex_connect/flex_disconnect and FLEX_* still accepted.
Stacks on feat/flex-on-demand-connection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant