fix(ui): reset stale per-connection state (radio-name label + DAX-TX stream) on reconnect#4260
fix(ui): reset stale per-connection state (radio-name label + DAX-TX stream) on reconnect#4260nigelfenton wants to merge 1 commit into
Conversation
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>
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 ownm_daxTxStreamIdbut nothing clearedAudioEngine::m_txStreamId, so the AX.25 path'sensureDaxTxStream()(gated ontxStreamId()==0) skipped re-creation after a reconnect.setTxStreamId(0)inonConnectionStateChanged's disconnect branch is the right fix, and since it's tied to the genericconnectionStateChanged(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-issuesstream create type=remote_audio_txand emits a freshremoteTxStreamReady(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:
RadioModel::connectViaWan(WanConnection*, const QString&, quint16)(RadioModel.cpp:1784) takes noRadioInfoand never seedsm_nickname/m_callsign— every SmartLink/WAN connection still relies solely on the asyncinforeply, so the stale/blank station label persists there.- The LAN auto-reconnect timer (
RadioModel.cpp:927-933) callsm_connection->connectToRadio(m_lastInfo)directly on the connection object, bypassingRadioModel::connectToRadio()entirely — so an unintentional-drop auto-recovery (arguably the most literal "reconnect" scenario) doesn't get the fix either. RadioModel::onDisconnected()clearsm_callsign(RadioModel.cpp:3805) but notm_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 #NNNNreference orPrinciple <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.
|
@nigelfenton I need the following tests done before I'll merge this: WSJT-X TCI socket drop/reconnect with the radio continuously connected. Holding for next week's release. |
|
First-pass triage review (not exhaustive — surfacing blockers to keep this moving): Blockers
Secondary notes
Recommend addressing the nickname clear (or splitting) before merge. |
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/
RadioModelresets, 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()seedsm_name/m_model/m_versionfrom the discoveryRadioInfobut notm_nickname/m_callsign(theRadioInfoalready carries both).m_nicknameis also never cleared on disconnect. SoonConnectionStateChanged(true), which sets the label synchronously fromm_radioModel.nickname(), reads a stale value; the correct nickname only arrives later via the asyncinforeply — and the late-arrivinginfoChangedhandler refreshed only the model + version labels, not the station label.Fix: seed
m_nickname/m_callsignfrom the discovery packet inconnectToRadio(), and refresh the station label in theinfoChangedhandler 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
RadioModelresets its own dax_tx state, butAudioEngine::m_txStreamIdkeeps the old stream id (it's never reset). The AX.25 TX path (Ax25HfPacketDecodeDialog::beginTransmission) only callsensureDaxTxStream()whentxStreamId() == 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_txStreamIdon connection loss (right whereaudioStopTx()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:
stream create type=dax_txon 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.