fix(modem): AetherModem TX never keys on a backend that takes audio over the seam - #4812
Conversation
…ver the seam
The built-in AX.25 modem gated its transmit path on `hostModulates`, which asks
who runs the MODULATOR. The question it actually needed to ask is whether
transmit audio needs a DAX stream — and those two came apart when
`takesTxAudioOverSeam` was added.
On an Icom, `hostModulates` is FALSE and correctly so: the RADIO modulates. So
the modem took the DAX branch and asked for a stream a networked Icom has never
had. `RadioModel::ensureDaxTxStream()` answers TRUE for a seam backend — "there
IS a route for transmit audio, it just isn't DAX" — so the modem's failure path
never fired either. `m_txPendingStream` stayed set, waiting on a stream that
could not arrive, until the wait timed out.
The result is a silent outage: PTT never keys, no audio chunks are sent, and
every queued frame dies with the timeout. Nothing logs an error, because from
each layer's own point of view nothing went wrong.
This is the same outage the call site already documents for the HL2 on
2026-07-31 ("PTT never keyed, 181 audio chunks never sent"), reached from the
opposite direction: the HL2 was excluded because it host-modulates, and a seam
backend needs excluding because its audio does not go through DAX at all.
`hostModulatesTx()` is renamed `txAudioBypassesDax()` and now ORs both, because
its two callers are asking that question rather than the modulator one. Nothing
else reads it — it is private to this dialog.
VERIFIED ON HARDWARE (IC-9700 over LAN, 2026-08-06)
Before: AetherModem TX did nothing at all; the log shows no keying for a whole
session.
After: a beacon and an AX.25 connect attempt to a live station both transmit —
0 DAX stream requests (the deadlock is gone), the full T1 retry ladder runs
(8/8, so frames are going out and being timed), and the radio's TX meters go
LIVE at 0.9 s age with forward power, SWR and ALC all reporting. Confirmed
modulating, not a bare carrier.
Affects any backend declaring `takesTxAudioOverSeam` — Icom today, and the same
shape for anything added later.
There was a problem hiding this comment.
The diagnosis holds up against the tree. IcomCivBackend declares takesTxAudioOverSeam = true with hostModulates false, RadioModel::ensureDaxTxStream() short-circuits to true for exactly that capability, and AudioEngine::m_hostModulation — the gate that decides whether modem chunks reach submitTxAudio — is driven from takesTxAudioOverSeam && canTransmit in MainWindow_Session.cpp:591, not from hostModulates. So this dialog was the one caller still asking the modulator question, and the OR brings it back in line with the flag its own audio path uses. The HL2 declares both flags true, so it's genuinely unaffected; Flex and sim declare the seam flag false. Rename is right — the old name had stopped describing the answer.
Two small things, neither blocking, plus one note.
Polish
- The old two-line doc comment is left in place above the new one in the header, and they now contradict each other.
- The operator-facing route string still reads "host-modulated" on an Icom, where the radio modulates. The
qCInfotag next to it was updated; this one was missed.
Non-blocking notes
canTransmitisn't ANDed here, unlike the two sibling gates (MainWindow_Session.cpp:591,MainWindow::hostModulatesTxAudio()). Harmless today — a backend that can't transmit fails fast at "PTT did not engage" instead of hanging — but it's the same shape of flag drift this PR is fixing.
Nice writeup on the before/after table, and thanks for chasing this down on real hardware.
🤖 aethersdr-agent · cost: $4.2420 · model: claude-opus-5
|
Correcting my own before/after evidence — the claim in the PR body is not supported, and I would rather say so than have a reviewer find it. I have since re-read the logs from before the change. The operator attempted an AX.25 connect at 22:18, on a binary built before this fix (which went in at 22:38), and that session shows:
So the "before" column of my table is wrong. The DAX deadlock I described did not reproduce on this hardware, and the fix cannot be credited with making the modem start transmitting — it was already transmitting. What I still believe is correct, on code reading rather than observation:
So this PR should be treated as a correctness change with no demonstrated failure behind it, not as the hardware-verified fix I originally described. The reasoning for ORing the two capabilities still holds — Happy for this to sit until I can either produce a failing case or confirm the branch is unreachable. I would rather it not merge on evidence I have retracted. Apologies for the noise — the original table conflated two sessions. |
|
Following up on my own retraction: I have now established what the code does, and the branch is not dormant. The correctness argument stands; the original table stays withdrawn. I said above that the most likely explanation was that the guarded branch is never reached on this radio, "in which case the condition is dormant rather than harmful." I have now traced it on 1. The guard is straight-line reachable. if (!hostModulatesTx() && m_audio->txStreamId() == 0) {2. On an Icom the first term is false. 3. On an Icom So both terms hold and the branch is taken on an Icom. It sets Why my 22:18 log showed zero Where that leaves the PR. I am no longer describing this as dormant-and-tidy, but I am also not restoring the hardware table — I have a code-level demonstration that the branch is entered and waits, and I have not re-run the radio to show PTT failing to key with a clean before/after. Those are different strengths of claim and I would rather label it accurately:
The fix itself is unchanged and I still believe it is right — Reviewer's call whether the code-level case is enough, or whether you want me to go back to the 9700 and capture the failing session first. I am happy to do the latter before this merges. |
jensenpat
left a comment
There was a problem hiding this comment.
Verified against main: takesTxAudioOverSeam is declared true for HL2 and Icom, false for Flex and the sim, so this is a no-op for Flex/sim, unchanged for the HL2 (already excluded via hostModulates), and the fix lands only on the seam backends. Both call sites are asking the DAX question, and hostModulatesTx() had no other readers — the rename is safe and the OR belongs in the helper. Hardware evidence (PTT keys, 0 DAX requests, T1 ladder runs, meters live) is convincing. CI green.
Nit, non-blocking: the header keeps the old three-line "True when the backend runs the modulator on this host (HL2)…" comment stacked above the new one, so the declaration now carries two "True when" descriptions and the first no longer matches the name. Worth folding into one on a follow-up.
Live on
main. The built-in AX.25 modem cannot transmit at all on a backend that declarestakesTxAudioOverSeam— Icom today. PTT never keys, no audio is sent, and nothing logs an error.Found operating packet on an IC-9700 this evening.
What happens
The modem gates its transmit path on
hostModulates, which asks who runs the modulator. The question it needs to ask is whether transmit audio needs a DAX stream — and those came apart whentakesTxAudioOverSeamwas added.On an Icom
hostModulatesisfalse, and correctly so: the radio modulates. So:RadioModel::ensureDaxTxStream()returns true for a seam backend — "there IS a route for transmit audio, it just isn't DAX"m_txPendingStreamstays set, waiting on a stream that cannot arrive, until the wait times outEvery layer behaves correctly by its own lights, which is why it is silent. From the operator's side the modem simply does nothing.
Why it looks familiar
The call site already documents this outage for the HL2 (2026-07-31: "PTT never keyed, 181 audio chunks never sent"). This is the same failure reached from the opposite direction — the HL2 was excluded because it host-modulates, and a seam backend needs excluding because its audio does not go through DAX at all.
The change
hostModulatesTx()→txAudioBypassesDax(), returninghostModulates || takesTxAudioOverSeam.Renamed because the name no longer described what it answers, and both of its callers are asking the DAX question rather than the modulator one. It is private to this dialog — nothing else reads it.
Verified on hardware — IC-9700 over LAN, 2026-08-06
Confirmed modulating, not a bare carrier — a beacon and a connect attempt to a live station both went out with audio.
The T1 timeouts are expected here: the station did not answer. What matters is that the ladder runs at all, which it could not do before.
Scope
Affects any backend declaring
takesTxAudioOverSeam, so Icom today and the same shape for anything added later. No change for Flex or the sim, which declare it false, or for the HL2, which was already excluded byhostModulates.Related: #4799 introduces
takesTxAudioOverSeamand wires it intoensureDaxTxStream; this caller was left on the old flag. The capability and the Icom's declaration are already onmain, so this stands alone and does not depend on that PR.