Skip to content

fix(tci): spec/impl compliance + RF2K-S amplifier interop#2597

Merged
ten9876 merged 3 commits into
aethersdr:mainfrom
nigelfenton:tci-spec-compliance
May 12, 2026
Merged

fix(tci): spec/impl compliance + RF2K-S amplifier interop#2597
ten9876 merged 3 commits into
aethersdr:mainfrom
nigelfenton:tci-spec-compliance

Conversation

@nigelfenton

Copy link
Copy Markdown
Contributor

Summary

Three commits that bring AetherSDR's TCI server into alignment with the published TCI Protocol spec and the reference Python implementation (ars-ka0s/eesdr-tci), and unlock RF2K-S amplifier support along the way. All existing clients (WSJT-X, JTDX, TCI Monitor, third-party TCI consumers) continue to work; only the init-burst content and structure changes.

Closes the protocol-side of the work requested in #2589 / #1902 (RF2K-S amplifier integration).

Commits

1. 1220ba7 — spec-compliant init burst

Six issues, all from comparing AetherSDR's emitted burst against TCI Protocol spec v2.0 (Expert Electronics, Jan 2024) §4.1:

  • Protocol identity was ExpertSDR3,1.5;; strict clients written for the earlier ExpertSDR2 generation reject that string. Changed to ExpertSDR2,1.9;.
  • Device string used name + \" \" + model, producing a leading space when name was empty (typical Flex connection: device: FLEX-6700;). Spec disallows surrounding whitespace.
  • FLEX-6700 is not an Expert Electronics product; some strict clients whitelist Expert hardware names. Changed to device:SunSDR2DX;.
  • vfo_limits and if_limits were missing entirely — both are spec-mandated init commands.
  • PROTOCOL was emitted first; spec lists it as the 8th of 9 init commands, immediately followed by READY.
  • The burst interleaved non-init commands (vfo:, modulation:, audio_*, etc.) into the init phase before READY. Strict clients reading sequentially treat that as a malformed handshake.

Restructured into three phases: init commands → READY → current state notifications → audio/IQ config → START.

2. 75ea988channels_count is plural (impl wins over spec PDF)

The PDF spec says CHANNEL_COUNT (singular) at §4.1. The reference Python implementation ars-ka0s/eesdr-tci only recognises CHANNELS_COUNT (plural) — singular is rejected by the parser's command-name whitelist. RF2K-S firmware (and likely most other clients) was built against the implementation, not the PDF. Restored the plural spelling.

3. 2d6725e — emit split_enable to satisfy RF2K-S frequency gating

The actual blocker for RF2K-S amplifier interop. The RF2K-S TCI client (rf-kit-gui_v198/operational_interface/tciSupport.py) stores vfo: event frequencies into a SplitThreadSafeValue, but only returns a usable frequency from get_frequency() once currentPosition has been set. currentPosition is only set by set_active_position(), which is only called when a split_enable:N,false|true; event arrives.

Without that event the amp's TCI client receives our frequency, stores it, then refuses to surface it — the RFKIT main controller falls back to UNIV and shows "No TCI available" forever. Adding split_enable:<trx>,false; to the per-slice notifications flips the gate.

Test plan

  • Build clean on Windows MSVC (build.bat, full link)
  • WSJT-X TCI connection: still receives full state, TX audio FIFO initialises (no behavioural change)
  • JTDX TCI connection: still working
  • TCI Monitor (nigelfenton/tci-monitor): receives all events, parses correctly
  • aether_pad (nigelfenton/aether-pad): VFO tracking still works
  • RF2K-S amplifier (firmware G198C267): front panel transitions from "No TCI available" → active TCI mode; band display and segment frequency follow AetherSDR's VFO across band boundaries; no TX required (previously the amp had to be transmitting for its internal RF pickoff to estimate frequency)

Discovery notes

The RF2K-S fix took some doing — the amp's TCI client is fully passive (sends nothing back, no logs externally visible) and the front panel's "No TCI available" label gives no detail about what is unavailable. The answer was eventually found by reading the amp's RPi-hosted Python source via the embedded VNC interface. The tciSupport.py decision tree made the missing split_enable: event obvious in retrospect, but it took working through the rest of the burst's spec-compliance gaps first to confirm those weren't the problem.

🤖 Generated with Claude Code

nigelfenton and others added 3 commits May 11, 2026 16:07
Three changes, all aligned with the published TCI protocol spec
v2.0 (Expert Electronics, January 2024) section 4.1:

1. Identity strings revised so strict TCI clients accept the server.
   - protocol: ExpertSDR2,1.9 instead of ExpertSDR3,1.5 — RF2K-S amps
     and other early TCI clients whitelist the ExpertSDR2 program name.
   - device: SunSDR2DX (a real Expert Electronics product) instead of
     formatting "<flex name> <flex model>" which (a) wasn't an Expert
     device name strict clients expect and (b) emitted a leading space
     when the name field was empty.

2. Spec-mandated initialization commands added.
   - vfo_limits and if_limits were missing entirely.  These are
     required by the spec; strict clients (notably the RF2K-S) refuse
     to consider the server "available" without them.
   - channel_count is the spec name (singular); we previously sent
     channels_count.  WSJT-X / JTDX tolerate both, the spec doesn't.

3. Init / state / audio phases now ordered correctly.
   - Phase 1: the 9 spec-listed init commands, terminated by READY.
   - Phase 2: current state notifications (vfo, modulation, rx_enable,
     filter, RIT/XIT, lock, SQL, AGC, NB/NR/ANF/APF, TX state, drive).
   - Phase 3: audio / IQ stream config, audio_start, start.
   Previously these were interleaved before READY, which strict
   clients reading the burst sequentially interpret as a malformed
   init phase.

Validated against WSJT-X / JTDX (still working) and TCI Monitor (still
sees full state).  An RF2K-S on the bench connects cleanly at the
TCP / WebSocket layer; the amp doesn't engage automatic control here,
but evidence points to an on-device calibration interlock, not a
protocol issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial commit changed channels_count → channel_count per the TCI
Protocol PDF spec v2.0 section 4.1.  However, the reference Python
client (ars-ka0s/eesdr-tci on PyPI — the basis for many TCI client
implementations, likely including the RF2K-S amplifier firmware)
only recognises the plural form.  Its parser raises ValueError on
unknown command names and aborts handshake parsing.

Implementation reality wins over the PDF.  Restore the plural form,
keeping the rest of the spec-compliance work intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The RF2K-S amplifier's TCI client (rf-kit-gui_v198/operational_interface/
tciSupport.py) stores incoming `vfo:` event frequencies into a
SplitThreadSafeValue indexed by VFO number, but only returns a non-None
frequency from get_frequency() when `currentPosition` has been set.
`currentPosition` is only set by `set_active_position()`, which is only
called when a `split_enable:N,false|true;` event arrives.

Without that event the amp's TCI client receives our frequency, stores
it, and then refuses to report it — `get_frequency()` returns None, the
RFKIT main controller falls back to UNIV mode, and the front panel
displays "No TCI available" regardless of any other event we emit.

Adding `split_enable:<trx>,false;` to the per-slice state notifications
in the init burst flips the gate.  Confirmed live: with this patch the
amp's display goes from "No TCI available" to active TCI mode, follows
AetherSDR's VFO across band boundaries, and reports the correct
frequency without TX.

This is the final piece of the spec-compliance / interop work; with it
the RF2K-S becomes the first amplifier known to follow AetherSDR over
TCI without modification on the amp side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude here, reviewing on Jeremy's behalf.

Verdict

Excellent work. Three independent issues + the RF2K-S-specific fix landed cleanly in a single 39+/14- diff to one file. Meticulous protocol archaeology — the kind of fix that's small in code but requires substantial knowledge to write correctly. Recommend merge after Jeremy spot-checks compat with the rigs he has access to.

Built locally clean

  • Auto-merges against current main with no conflicts
  • All 5 CI checks green
  • Build clean (only pre-existing unrelated warnings)
  • All recently-landed PRs (#2572, #2583, #2586, #2591, #2596) coexist post-merge

The six fixes, individually verified

1. Protocol identity: ExpertSDR3,1.5;ExpertSDR2,1.9;

The TCI protocol originated with ExpertSDR (the SunSDR2 control software); modern TCI clients were largely written against ExpertSDR2 and check the prefix. Claiming ExpertSDR3 invites silent rejection from strict clients. Reasonable change.

Regression concern: any client that explicitly expects ExpertSDR3 would break. The author has tested WSJT-X, JTDX, TCI Monitor, aether_pad and confirms they still work. Worth Jeremy validating against any other TCI clients he uses (N1MM with TCI plugin if applicable, MacLoggerDX TCI mode, etc.).

2. Device whitespace + name change

The original m_model->name() + " " + m_model->model() produces " FLEX-6700" (leading space) when name is empty. Spec disallows surrounding whitespace. Fix to drop the space is unambiguously correct.

Changing the device name from FLEX-XXXX to SunSDR2DX is a more substantial identity claim. The TCI server is now reporting Expert hardware to clients. Functionally TCI is supposed to be hardware-agnostic, but logging tools (especially those that key off device strings for QSO tracking) might log "SunSDR2DX" instead of the actual radio model. Probably acceptable trade-off — the operator clearly knows what radio they're on — but worth Jeremy considering as a UX choice.

3. Missing vfo_limits and if_limits — clear win

Spec-mandated init commands. Both 1000,75000000; and -48000,48000; are reasonable defaults; FLEX radios actually go higher (Aurora 6 GHz upper) but TCI clients typically don't enforce these limits, just display them. Could be a follow-up to make them dynamic per radio model.

4. protocol: ordering — correct per spec

Per §4.1 of the TCI spec, PROTOCOL is the 8th of 9 init commands, immediately followed by READY. Original code emitted it first. New ordering matches spec.

5. Init burst phasing into init → READY → state → audio config → START

This is the load-bearing fix for strict clients. The original burst interleaved current-state notifications (vfo:, modulation:) into the init phase. Strict clients reading sequentially treat that as malformed. The phased structure is exactly what the spec describes.

6. channels_count (plural) — implementation wins over PDF

This is the small-but-important detail that documents the conflict. The PDF says CHANNEL_COUNT; the reference Python implementation (ars-ka0s/eesdr-tci) only recognizes CHANNELS_COUNT. The PR's inline comment is exactly the right level of disclosure for future archaeologists.

7. split_enable:<trx>,false; — the RF2K-S unlock

The actual RF2K-S blocker. The amp's TCI client (tciSupport.py) gates get_frequency() on currentPosition being set, which only happens via set_active_position(), which only fires on split_enable:N,false|true;. Without that event the amp stores incoming vfo: frequencies but won't surface them. The author traced this via VNC into the amp's RPi-hosted Python — exemplary diagnostic discipline.

Recommendation

Merge after Jeremy confirms:

  1. WSJT-X / JTDX still work (regression check — author already tested)
  2. Any other rigctl/TCI consumers in regular use (logging programs, contest tools)
  3. The device-name change from FLEX-XXXX to SunSDR2DX is acceptable from a UX standpoint

The author's diagnostic process — reading the RF2K-S TCI client source via VNC into the amp's RPi to figure out the missing split_enable: event — is exactly the right way to chase opaque interop bugs. The fact that this took working through five other spec-compliance gaps first to rule them out before finding the actual blocker is what makes the analysis convincing.

Thanks @nigelfenton — this unblocks RF2K-S amp interop (#2589, #1902) and makes AetherSDR's TCI burst spec-correct as a side benefit. Clean, focused PR.

73, Jeremy KK7GWY & Claude (AI dev partner)

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — clean TCI spec compliance + RF2K-S unlock.

@ten9876 ten9876 merged commit 4ed3fd2 into aethersdr:main May 12, 2026
5 checks passed
@nigelfenton

Copy link
Copy Markdown
Contributor Author

@jensenpat — heads-up acknowledged, and this one's on me. #2597 moved the ready; token in the TCI init burst and I didn't test the TX-level behaviour of the clients it would affect. WSJT-X and JTDX landing at ~0.5 drive (~60 W) is a real, finals-cooking-class regression and I own it.

Posting the full git trace here so your correcting PR has the precise mechanism (context for the fix, not an excuse):

  • The 0.5 / ~60 W fallback is pre-existing and lives outside the burst code. Any client that reaches ready; without a drive: value falls back to it. So the real default isn't in generateInitBurst() — that fallback path is where it lives, and where a robust fix probably has to account for it.
  • drive:<trx>,<rfPower()> was added to the init burst by JJ Boyd KG4VCF in aa13232 (2026-04-06, "Fix TCI RX/TX audio for WSJT-X and JTDX"). At that time ready; was the last token in the burst, so drive: reached those clients before READY and they were correct.
  • fix(tci): spec/impl compliance + RF2K-S amplifier interop #2597 moved ready; from last → early (Phase 1) so the RF2K-S amp would accept the burst. git show 4ed3fd2 -- src/core/TciProtocol.cpp: one ready; added early, one removed from the end, zero changes to the drive:/tune_drive:/trx: lines. Net effect: drive: now lands after READY, so clients that finalise TX config at READY (WSJT-X/JTDX) miss it and fall back to the pre-existing default.

Design tension your fix will have to thread: drive: is not one of the spec's pre-READY init commands, but WSJT-X/JTDX need it before READY, while the RF2K-S needs a clean pre-READY (only the spec init commands) or it rejects the burst. Naively moving drive: back ahead of READY re-breaks the amp. Options I can see: emit drive: on both sides of READY, amp-side tolerance, or client-specific handling — your call on the right shape.

My validation on #2597 said "WSJT-X/JTDX unaffected", but that was a connectivity check, not a TX-level one. I've rebuilt my test protocol: any burst/ordering change now requires a per-client before/after measurement of actual drive, mode and VFO — not just "does it still connect". Won't recur on my watch. Thanks for catching it and taking the fix.

73, Nigel G0JKN & Claude (AI dev partner)

ten9876 added a commit that referenced this pull request May 17, 2026
…X audio level (#2806)

WSJT-X's TCITransceiver applies a -6 dB amplitude scaling to TCI TX
audio when BOTH of these match the server's init burst:
  device  == "SunSDR2DX" or "SunSDR2PRO"
  protocol != starts-with "ExpertSDR3"
(reference: wsjtx/Transceiver/TCITransceiver.cpp:823 + .hpp:188-200,
K1=0.999/0x7FFF vs K2=0.499/0x7FFF.)

PR #2597 hit both conditions exactly — device=SunSDR2DX +
protocol=ExpertSDR2,1.9 — so WSJT-X halved every TX sample at the
source and TCI users measured ~70 W of 100 W expected.

Fix:
  - device: literal "AetherSDR" — avoids the SunSDR-specific gain trap
    AND avoids the leading-space bug in the older "<name> <model>"
    form when the radio nickname is empty.
  - protocol: "ExpertSDR3,1.5" — sets WSJT-X's ESDR3 flag, gating off
    the gain reduction and selecting command formats that AetherSDR
    already handles correctly (proven in v26.5.1).

Verified: WSJT-X TX output restored to 100 W of 100 W expected.

Everything else from #2597 (strict init-burst order, vfo_limits,
if_limits, channels_count plural-form, split_enable, post-READY audio
stream configuration) is preserved — that's the bulk of what the
RF2K-S TCI parser actually needs to engage. The amp's device/protocol
whitelist regresses, tracked separately in #2806.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request May 17, 2026
…X audio level (#2806) (#2807)

PR #2597 changed TCI server identity from
  device: <flex name> <flex model>      protocol: ExpertSDR3,1.5
to
  device: SunSDR2DX                     protocol: ExpertSDR2,1.9
so RF2K-S amplifier firmware would whitelist AetherSDR. WSJT-X reads the
same fields to decide TX audio scaling: the SunSDR2DX/ExpertSDR2,1.9
pair drops TCI TX power by roughly 1 dB (measured 70 W vs 100 W
expected; reverting recovers to 88 W out of 100 W, the remaining
0.5 dB is within ALC/calibration variance).

Reverts only the device + protocol strings. Everything else from #2597
that the RF2K-S parser actually needs — strict init-burst order,
vfo_limits, if_limits, channels_count (plural-form), split_enable —
remains intact, so the amp interop work is mostly preserved.

A clean fix that keeps both clients happy (e.g. per-client adaptive
identity or a user-facing TCI Identity setting) is tracked in #2806.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 mentioned this pull request May 17, 2026
ten9876 added a commit that referenced this pull request May 17, 2026
Hotfix recommended for TCI digital-mode operators, Windows users with
MQTT or floating-panel windows, and Linux Ubuntu 22.04 builders.

- TCI TX audio level regression vs v26.5.1 (#2806/#2807) — restores
  WSJT-X / JTDX output to full level by reverting only the device +
  protocol identity strings from #2597 to a form that bypasses
  WSJT-X's TCITransceiver SunSDR2DX/non-ExpertSDR3 gain trap.
- Windows process lingering in Task Manager after close (#2802,
  chibondking) — three independent root causes addressed: Channel
  Strip + applet-panel float window WA_QuitOnClose, MqttClient
  destructor loop_stop.
- AppImage CI build failure on gcc 11 strict mode (#2799) — qualifies
  bitstream.h:192 member access with `this->` for two-phase name
  lookup. Restores the v26.5.2 x86_64 AppImage artifact.
- SSA (Sweden) band plan (#2805, NF0T) — Swedish national overlay
  with PTSFS 2025:1 constraints, Swedish labels, 89 spots.

Bumps version 26.5.2 → 26.5.2.1 in CMakeLists.txt, README.md,
CLAUDE.md; regenerates WhatsNewData.cpp from CHANGELOG.md.
CalVer scheme expanded to YY.M.patch[.hotfix] to document the
4th-component hotfix convention used historically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nigelfenton nigelfenton deleted the tci-spec-compliance branch May 25, 2026 19:19
nigelfenton added a commit to nigelfenton/AetherSDR that referenced this pull request May 28, 2026
Third Stream-Deck-style sibling alongside elgato-aethersdr (Elgato Stream
Deck) and streamcontroller-aethersdr (open-source Stream Deck alternative).
Drives AetherSDR over TCI WebSocket from Ulanzi-Studio-compatible macro
keypads / dials — Ulanzi D100H smoke-tested, D200H (14 keys) and D200X
(14 keys + 3 knobs) profile work in progress.

Action set (18 total):

  Encoder (dial):  VFO Tune
  TX:              MOX Toggle, TUNE / ATU, RIT Toggle
  Modes:           Mode Cycle + direct USB / LSB / CW / DIGU
  Bands:           Band Up, Band Down
  Slice:           Slice Cycle (A → … → H → A)
  Gain:            AF / RF / Mic × Up/Down (±5 per press,
                   TCI volume:, drive:, mic_level: verbs)

TCI command vocabulary is faithful to the existing elgato-aethersdr plugin
so the two behave identically against the same radio.  Port confirmed
40001; init burst handling and `protocol:ExpertSDR2,1.9;` lineage from PR
aethersdr#2597 already in place upstream.

Iconography is generated programmatically from a single source-of-truth
config (scripts/generate-icons.js — Node, zero npm deps).  18 action icons
+ 5 coordinated shack-app launcher tiles (AetherSDR / TCI Monitor /
ShackLog / IQ Capture / aether-pad) for use with Studio's built-in
System → Open action.  Palette deliberately mirrors AetherSDR's dark
theme so the keypad reads as a continuation of the desktop UI.

Bundled D100H default profile in profiles/ for one-click Studio import.

The independent public-facing release / marketplace path lives at
github.com/nigelfenton/aethersdr-ulanzi-plugin; this in-repo copy is
bundled context for AetherSDR developers — both repos stay alive (see
plugin's README for the relationship).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NF0T pushed a commit that referenced this pull request Jun 13, 2026
)

## Problem

Reported by Yuri UT4LW (SDC / CW Skimmer ecosystem): AetherSDR's TCI
server sends `ready;` immediately after the `protocol:...` identity
command, before the state notifications and audio/IQ stream parameters:

```
...
protocol:ExpertSDR3,1.5;
ready;              ← too early
drive:0,100;
...
iq_samplerate:48000;
audio_start;
start;
```

`ready;` marks the end of the server's initial settings dump. Clients
that latch their cached settings the moment `ready;` arrives — notably
SDC / CW Skimmer — initialize from defaults instead of our actual
parameters (in particular `iq_samplerate`), causing incorrect client
initialization.

## Fix

Move `ready;` to after `iq_samplerate:...;` so it terminates the
complete settings dump, keeping `audio_start;` / `start;` as the
trailing stream-control commands. This matches real ExpertSDR3 behavior
(full state dump, then `ready;`) and the spec's READY definition ("sent
after the initialization commands").

## Why the early READY was there, and why it's safe to remove

The early placement came from #2597, which claimed strict clients
(RF2K-S amplifier) treat anything before READY that isn't one of the 9
spec-listed init commands as malformed. Verified against the reference
parser that claim was based on (`ars-ka0s/eesdr-tci`, the basis of the
RF2K-S TCI client):

- It aborts only on **unrecognized command names**, anywhere in the
stream — there is no pre-READY grammar check. Every command now sent
pre-READY was already present in the burst, so no new exposure.
- `READY` merely sets a "ready event" flag for the client app to await —
i.e. it means "initial sync complete", which is exactly why it must come
last.
- #2597's own bench notes concede the early READY never made the amp
engage; the actual fix was `split_enable` (still emitted, unchanged).

WSJT-X / JTDX parse every TCI command on arrival regardless of
ready-state and use `ready;` only to advance their init state machine
(verified in WSJT-X `Transceiver/TCITransceiver.cpp`: `Cmd_Ready` →
`tci_done7()`; state commands arriving earlier are cached). Since real
ExpertSDR3 sends the full state dump before `ready;`, every TCI client
in the wild already handles this ordering — it is the maximally
compatible choice.

## New burst order

1. Init/identity: `vfo_limits`, `if_limits`, `trx_count`,
`channels_count`, `device`, `receive_only`, `modulations_list`,
`protocol`
2. Per-slice + global state: `vfo`, `modulation`, `rx_enable`, filters,
RIT/XIT, `split_enable`, lock, SQL, AGC, NB/NR/ANF/APF, `tx_enable`,
`drive`, `tune_drive`, `mic_level`, `trx`, `volume`
3. Audio/IQ stream config: `audio_samplerate`,
`audio_stream_sample_type`, `audio_stream_channels`,
`audio_stream_samples`, `tx_stream_audio_buffering`, `iq_samplerate`
4. **`ready;`**
5. `audio_start;` `start;`

## Testing

- Builds clean (RelWithDebInfo, Ninja).
- Burst order verified in `generateInitBurst()`; no other call sites
changed.
- Interop analysis above for WSJT-X/JTDX, eesdr-tci/RF2K-S, SDC/CW
Skimmer.

💻 Generated with Claude Code (Fable 5.0) with architecture by @jensenpat

Co-authored-by: Codex <noreply@openai.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…thersdr#3498)

## Problem

Reported by Yuri UT4LW (SDC / CW Skimmer ecosystem): AetherSDR's TCI
server sends `ready;` immediately after the `protocol:...` identity
command, before the state notifications and audio/IQ stream parameters:

```
...
protocol:ExpertSDR3,1.5;
ready;              ← too early
drive:0,100;
...
iq_samplerate:48000;
audio_start;
start;
```

`ready;` marks the end of the server's initial settings dump. Clients
that latch their cached settings the moment `ready;` arrives — notably
SDC / CW Skimmer — initialize from defaults instead of our actual
parameters (in particular `iq_samplerate`), causing incorrect client
initialization.

## Fix

Move `ready;` to after `iq_samplerate:...;` so it terminates the
complete settings dump, keeping `audio_start;` / `start;` as the
trailing stream-control commands. This matches real ExpertSDR3 behavior
(full state dump, then `ready;`) and the spec's READY definition ("sent
after the initialization commands").

## Why the early READY was there, and why it's safe to remove

The early placement came from aethersdr#2597, which claimed strict clients
(RF2K-S amplifier) treat anything before READY that isn't one of the 9
spec-listed init commands as malformed. Verified against the reference
parser that claim was based on (`ars-ka0s/eesdr-tci`, the basis of the
RF2K-S TCI client):

- It aborts only on **unrecognized command names**, anywhere in the
stream — there is no pre-READY grammar check. Every command now sent
pre-READY was already present in the burst, so no new exposure.
- `READY` merely sets a "ready event" flag for the client app to await —
i.e. it means "initial sync complete", which is exactly why it must come
last.
- aethersdr#2597's own bench notes concede the early READY never made the amp
engage; the actual fix was `split_enable` (still emitted, unchanged).

WSJT-X / JTDX parse every TCI command on arrival regardless of
ready-state and use `ready;` only to advance their init state machine
(verified in WSJT-X `Transceiver/TCITransceiver.cpp`: `Cmd_Ready` →
`tci_done7()`; state commands arriving earlier are cached). Since real
ExpertSDR3 sends the full state dump before `ready;`, every TCI client
in the wild already handles this ordering — it is the maximally
compatible choice.

## New burst order

1. Init/identity: `vfo_limits`, `if_limits`, `trx_count`,
`channels_count`, `device`, `receive_only`, `modulations_list`,
`protocol`
2. Per-slice + global state: `vfo`, `modulation`, `rx_enable`, filters,
RIT/XIT, `split_enable`, lock, SQL, AGC, NB/NR/ANF/APF, `tx_enable`,
`drive`, `tune_drive`, `mic_level`, `trx`, `volume`
3. Audio/IQ stream config: `audio_samplerate`,
`audio_stream_sample_type`, `audio_stream_channels`,
`audio_stream_samples`, `tx_stream_audio_buffering`, `iq_samplerate`
4. **`ready;`**
5. `audio_start;` `start;`

## Testing

- Builds clean (RelWithDebInfo, Ninja).
- Burst order verified in `generateInitBurst()`; no other call sites
changed.
- Interop analysis above for WSJT-X/JTDX, eesdr-tci/RF2K-S, SDC/CW
Skimmer.

💻 Generated with Claude Code (Fable 5.0) with architecture by @jensenpat

Co-authored-by: Codex <noreply@openai.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.

2 participants