Skip to content

fix(ui): reset stale per-connection state (radio-name label + DAX-TX stream) on reconnect#4260

Open
nigelfenton wants to merge 1 commit into
aethersdr:mainfrom
nigelfenton:fix/stale-radio-nickname-label
Open

fix(ui): reset stale per-connection state (radio-name label + DAX-TX stream) on reconnect#4260
nigelfenton wants to merge 1 commit into
aethersdr:mainfrom
nigelfenton:fix/stale-radio-nickname-label

Conversation

@nigelfenton

Copy link
Copy Markdown
Contributor

Summary

Two client-side values were cached across a radio disconnect and reused on the next connection, producing wrong state on reconnect. They're the same shape — a field the connection/RadioModel resets, but a mirror doesn't — and both are ~a handful of lines.

1. Stale radio-name in the status-bar station label

Symptom: on the first connect after launch, the bottom-center station label is blank; if a radio was connected earlier in the session, it shows the previously connected radio's name. A disconnect+reconnect then shows the correct name.

Cause: RadioModel::connectToRadio() seeds m_name/m_model/m_version from the discovery RadioInfo but not m_nickname/m_callsign (the RadioInfo already carries both). m_nickname is also never cleared on disconnect. So onConnectionStateChanged(true), which sets the label synchronously from m_radioModel.nickname(), reads a stale value; the correct nickname only arrives later via the async info reply — and the late-arriving infoChanged handler refreshed only the model + version labels, not the station label.

Fix: seed m_nickname/m_callsign from the discovery packet in connectToRadio(), and refresh the station label in the infoChanged handler alongside model/version.

2. DAX-TX stream not re-created on reconnect (digital/AX.25 TX goes out unmodulated)

Symptom: after a disconnect+reconnect, transmitting from the AetherModem AX.25 terminal produces a bare carrier — the radio keys but no modem audio modulates it. Re-opening the modem terminal restores it.

Cause: on disconnect RadioModel resets its own dax_tx state, but AudioEngine::m_txStreamId keeps the old stream id (it's never reset). The AX.25 TX path (Ax25HfPacketDecodeDialog::beginTransmission) only calls ensureDaxTxStream() when txStreamId() == 0, so on the new connection it sees the stale non-zero id, skips the re-create, and pumps VITA-49 modem audio to a dead stream id.

Fix: clear m_txStreamId on connection loss (right where audioStopTx() is already called in the disconnect path), so the next transmit re-creates the DAX-TX stream on the new connection. Scoped to the disconnect handler so it can't drop a still-valid stream mid-session.

Verification (live, on real hardware)

Tested against a real IC-9700 bridged to AetherSDR as a Flex-6700 (via a gateway that presents the Flex protocol), reproducing both bugs before the fix and confirming both after:

  • Name label: now correct on the first connect (was blank/previous-radio's-name).
  • DAX-TX / AX.25: after a disconnect+reconnect, the client now re-sends stream create type=dax_tx on the next transmit and real modem audio flows — a clean AX.25 transmission on reconnect without re-opening the modem terminal (previously a bare carrier).

Built on main (Windows, Qt 6.10.3). Small, self-contained; no lifecycle refactor.

Two client-side values were cached across a radio disconnect and reused on
the next connection, causing wrong state on reconnect. Both are the same
shape: a field that RadioModel/the connection resets but a mirror does not.

1. Stale radio-name (status-bar station label). connectToRadio() seeded
   m_name/m_model/m_version from the discovery RadioInfo but NOT m_nickname/
   m_callsign, and m_nickname is never cleared on disconnect. So on connect
   the label (set synchronously in onConnectionStateChanged) read a stale
   m_nickname: blank on the first connect, or the PREVIOUSLY connected
   radio's name. Seed nickname/callsign from the discovery packet (which
   already carries them), and also refresh the station label in the
   late-arriving infoChanged handler (it previously updated only model +
   version).

2. DAX-TX stream not re-created on reconnect (digital/AX.25 TX went out as
   a bare carrier). RadioModel resets its dax_tx state on disconnect, but
   AudioEngine::m_txStreamId kept the OLD id. The AetherModem AX.25 path
   only calls ensureDaxTxStream() when txStreamId()==0, so on reconnect it
   saw the stale non-zero id, skipped the re-create, and pumped modem audio
   to a dead stream. Clear m_txStreamId on connection loss so the next
   transmit re-creates the stream.

Both verified live on real hardware (IC-9700 via a Flex-emulating gateway):
name now correct on first connect; AX.25 modulation now works on reconnect
without re-opening the modem terminal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@nigelfenton
nigelfenton requested a review from a team as a code owner July 15, 2026 23:02

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both fixes hold up under review — I traced each against the actual code and they're correctly diagnosed and correctly scoped.

Name label: confirmed RadioInfo carries nickname/callsign (src/core/RadioDiscovery.h), that connectToRadio() seeds m_nickname before onConnectionStateChanged(true) reads it synchronously at MainWindow.cpp:4956, and that m_nickname is never cleared on disconnect (so it went stale exactly as described). The infoChanged refresh mirrors the existing model/version pattern.

DAX-TX: confirmed RadioModel::onDisconnected() resets m_daxTxStreamId but nothing cleared AudioEngine::m_txStreamId, and the AX.25 path (Ax25HfPacketDecodeDialog.cpp:1575) gates ensureDaxTxStream() on txStreamId()==0 — so a stale non-zero id skipped re-creation. setTxStreamId(0) in the disconnect handler is the right place and the right fix.

Nice touch that the scope is tight: I checked whether the sibling m_remoteTxStreamId (voice/VOX) has the same latent staleness, and it doesn't — remote_audio_tx is re-created eagerly on every GUI attach (RadioModel.cpp:3605), so its id is always overwritten on reconnect. The DAX-TX stream is unique in being created lazily, which is exactly why only it needed clearing. Leaving the voice path untouched was correct, not an omission.

No AppSettings/RAII/null-safety issues (m_audio and m_stationLabel are both guarded). Clean, verified, mergeable — thanks @nigelfenton.

Nothing to fix before merge.


🤖 aethersdr-agent · cost: $7.2740 · model: claude-opus-4-8

@NF0T NF0T 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.

Both root causes are correctly diagnosed and the fixes are well-targeted for the reported repro. Verified directly against source:

  • DAX-TX fix is solid. RadioModel::onDisconnected() resets its own m_daxTxStreamId but nothing cleared AudioEngine::m_txStreamId, so the AX.25 path's ensureDaxTxStream() (gated on txStreamId()==0) skipped re-creation after a reconnect. setTxStreamId(0) in onConnectionStateChanged's disconnect branch is the right fix, and since it's tied to the generic connectionStateChanged(false) signal, it applies uniformly regardless of which connection path (LAN/WAN/auto-reconnect) triggered the disconnect.
  • Also checked whether the sibling AudioEngine::m_remoteTxStreamId (voice/VOX) has the same latent staleness, per the bot review's claim that it doesn't — confirmed: RadioModel::registerAsGuiClient() unconditionally re-issues stream create type=remote_audio_tx and emits a fresh remoteTxStreamReady(id) on every connection (LAN and WAN alike), so that field is genuinely safe, not an overlooked instance of the same bug.

The nickname/callsign fix only covers one of three reconnect paths, though. The PR patches RadioModel::connectToRadio(const RadioInfo& info) — correct for the reported "first connect after launch" repro — but:

  1. RadioModel::connectViaWan(WanConnection*, const QString&, quint16) (RadioModel.cpp:1784) takes no RadioInfo and never seeds m_nickname/m_callsign — every SmartLink/WAN connection still relies solely on the async info reply, so the stale/blank station label persists there.
  2. The LAN auto-reconnect timer (RadioModel.cpp:927-933) calls m_connection->connectToRadio(m_lastInfo) directly on the connection object, bypassing RadioModel::connectToRadio() entirely — so an unintentional-drop auto-recovery (arguably the most literal "reconnect" scenario) doesn't get the fix either.
  3. RadioModel::onDisconnected() clears m_callsign (RadioModel.cpp:3805) but not m_nickname — the disconnect-side half of the bug your own commit message names ("m_nickname is never cleared on disconnect") isn't actually fixed, just compensated for on the one connect path that got the seeding fix.

Net effect: the fix closes the bug for its own repro case, but the identical stale-label bug remains live on WAN connects and auto-reconnect. Could you either extend the seeding to connectViaWan() and route the reconnect timer through RadioModel::connectToRadio(), or — probably simpler — clear m_nickname in onDisconnected() alongside the existing m_callsign.clear()? The latter would close all three paths at once without needing to touch each connect site individually.

Non-blocking:

  • The new if (m_stationLabel && ...) guard is redundant — setStatusBarStationText() already null-checks internally, and no other call site in this file re-guards it.
  • No Fixes #NNNN reference or Principle <N>. citation — not blockers, just flagging for the squash commit.

Nice catch on the DAX-TX bare-carrier bug and thanks for the live-hardware verification — that half is ready to go. Happy to re-review quickly once the nickname/callsign fix is extended to the other two paths.

@NF0T NF0T self-assigned this Jul 16, 2026
@jensenpat

Copy link
Copy Markdown
Collaborator

@nigelfenton I need the following tests done before I'll merge this:

WSJT-X TCI socket drop/reconnect with the radio continuously connected.
Radio disconnect/reconnect while WSJT-X is idle.
Radio disconnect during an active FT8 transmission.
First TX immediately after radio reconnect, checking how much audio precedes DAX TX create succeeded.

Holding for next week's release.

@jensenpat
jensenpat marked this pull request as draft July 17, 2026 16:32
@jensenpat
jensenpat marked this pull request as ready for review July 19, 2026 04:26
@ten9876

ten9876 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

First-pass triage review (not exhaustive — surfacing blockers to keep this moving):

Blockers

  • The CHANGES_REQUESTED items from @NF0T's review are still unaddressed — the current head (622bc67) is the same commit that review was filed against, so no fix has been pushed. Specifically the nickname/callsign half only covers RadioModel::connectToRadio(const RadioInfo&); the identical stale-label bug remains live on the WAN path (connectViaWan(), no RadioInfo seeding) and the LAN auto-reconnect timer (calls m_connection->connectToRadio(m_lastInfo) directly, bypassing RadioModel::connectToRadio()). And onDisconnected() clears m_callsign but not m_nickname, so the disconnect-side half the commit message names isn't actually fixed. As NF0T noted, clearing m_nickname in onDisconnected() alongside the existing m_callsign.clear() closes all three paths in one line — likely the cleanest resolution.

Secondary notes

  • The DAX-TX half is solid and independently verified by two reviewers — setTxStreamId(0) in the disconnect branch is the right place, and the m_remoteTxStreamId sibling is correctly left alone (re-created eagerly on every attach). If it helps land the good half sooner, this could be split from the nickname fix.
  • Redundant if (m_stationLabel && ...) guard — setStatusBarStationText() already null-checks internally (MainWindow.cpp:1436).

Recommend addressing the nickname clear (or splitting) before merge.

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.

4 participants