Synchronize TGXL startup state before presence - #5339
Conversation
|
Addressed the issue-triage recommendations in signed commit
I did not add Focused tests, registration validation, and the strict engine-boundary check pass locally. |
There was a problem hiding this comment.
Issue fit
Partially. #5338 asks for three things: carry tuner identity in TunerDelta, apply the complete initial snapshot inside applyChanges(), and publish presence only after all fields are current. All three are delivered, and the payoff is real rather than theoretical — MainWindow_Wiring.cpp:6022 reads tgxlIp() from inside the presenceChanged(true) handler to auto-connect the direct port-9010 socket, so a presence edge that fires before ip lands genuinely mattered. tuner_model_test's ipAtPresence assertion pins exactly that.
What the issue does not ask for is the third change in the diff: the replacingPlaceholder widening of the amplifier routing predicate at RadioModel.cpp:9985-9988. That is a separate defect class (#4203's model-less-status edge), it is the one hunk with no test, and I believe it is a regression — blocker 1.
Scope
| File | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/core/backends/TunerDelta.h |
adds handle field |
yes — issue's "carry tuner identity in TunerDelta" |
In scope |
src/core/backends/flex/FlexBackend.cpp |
populates d.handle in decodeTunerStatus |
yes | In scope |
src/models/TunerModel.cpp |
applies d.handle; presence edge computed across the whole delta |
yes — this is the fix | In scope |
src/models/RadioModel.cpp — atu branch (9911-9917) |
drops the pre-decode setHandle, folds handle extraction into the decode call |
yes — issue names the atu entry point explicitly |
In scope |
src/models/RadioModel.cpp — amplifier branch, replacingPlaceholder (9985-9988) |
widens which statuses route to TunerModel |
no — #5338 says nothing about routing, only about ordering | Out of scope, and a regression — see blocker 1 |
tests/aetherd_tuner_decode_test.cpp |
handle param + placeholder-carry case | yes | In scope |
tests/tuner_model_test.cpp |
presence-observer, placeholder→real, direct-presence cases | yes | In scope |
No CHANGELOG.md entry — correct. No AppSettings calls, no new public surface, no deleted guards other than the two setHandle calls the issue asked to remove (setHandle itself survives and is still used by the three removal/reset paths at RadioModel.cpp:6941/9966/9979, so it is not dead). No UI defaults or styling touched, so nothing to classify under the preference check.
Blockers
1. replacingPlaceholder routes PGXL statuses into TunerModel and starves AmpModel — inline at src/models/RadioModel.cpp:9985.
The new third disjunct is unconditioned on model or handle. Once m_tunerModel.handle() == "0x00000000", every amplifier <handle> ... status takes the tuner branch, and because the AmpModel decode is the else of that branch, the amp decode is skipped entirely. Concretely, a model=PowerGeniusXL status arriving while the TGXL is still holding the placeholder:
decodeTunerStatus(pgxlHandle, kvs)setsFlexBackend::m_tunerHandle = pgxlHandle(FlexBackend.cpp:977-978), sotuner.operate/tuner.bypass/tuner.autotunethen encodetgxl … handle=<PGXL>.decodeAmplifierStatusatFlexBackend.cpp:950carries an explicit guard against precisely the mirror image of this ("Defense in depth (#4203) … refuse to cache a known-tuner handle so a lateramp.operatecan never mis-target the TGXL") — this change opens the direction that guard does not cover, andtgxl autotuneis a transmit-keying command.d.handleandd.modeloverwriteTunerModel's identity with the PGXL's, soTunerModel::modelName()becomesPowerGeniusXL.m_meterModel.setTgxlHandle(pgxlHandle)fires, sincehandle != "0x00000000" && handle != m_tunerModel.handle().AmpModelnever sees the status, so PGXL presence never latches — no amp applet, no auto-connect atMainWindow_Wiring.cpp:6068.
On main this cannot happen: a PGXL status fails both model == "TunerGeniusXL" and handle == m_tunerModel.handle() and falls correctly to decodeAmplifierStatus. The placeholder being stored in the model is not new — the pre-existing else if (m_tunerModel.handle().isEmpty()) branch stored it too — so the window this opens is a live state on any station running a TGXL, and it stays open indefinitely if a real handle never arrives.
The case the disjunct is presumably aimed at is a model-less TGXL status carrying the real handle while the model still holds the placeholder. Two narrower shapes that don't swallow the amp:
- gate on the absence of an attributing model —
|| (replacingPlaceholder && model.isEmpty()); or, better, - stop adopting
0x00000000as identity at all, sohandle == m_tunerModel.handle()never matches the placeholder and the disjunct is unnecessary.decodeTunerStatus's own comment already assumes this ("RadioModel passes the handle it already extracted+sanitized (never the 0x00000000 placeholder)").
Either way this hunk wants its own test, and arguably its own PR against #4203 rather than riding on the ordering fix.
Nits
- The one risky hunk is the one with no coverage. Both new tests drive
TunerDelta/decodeTunerStatusdirectly; neither exercisesRadioModel::onStatusReceived, so nothing in the suite would catch blocker 1. A socket-free case feeding amodel=PowerGeniusXLamplifier status while the tuner holds the placeholder, and assertingAmpModelpresence, would pin it. - Edge signals now precede presence.
antennaAChanged/tuningChangedare emitted mid-loop inapplyChanges, i.e. before the newpresenceChanged. OnmainsetHandlepublished presence first. A first delta carryingtuning=1now emitstuningChanged(true)for a tuner the rest of the app does not yet consider present.TunerApplet.cpp:282only restyles, so I don't think this bites today — but it inverts the ordering the fix is otherwise establishing, and moving the presence emit above the field block would cost nothing. - Stale comment.
FlexBackend.cpp:974-976still claims RadioModel never passes the placeholder;RadioModel.cpp:9996now passes rawhandleunconditionally. The claim was already loose onmain; this makes it plainly wrong.
What I tried to break
- "Presence only after all fields are current." Held for the fields the delta carries —
wasPresent/nowPresentbracket the entire field block andpresenceChangedprecedesstateChanged. The one seam is the edge signals above. - Direct-connection presence with no radio handle.
isPresent()is!m_handle.isEmpty() || m_directPresence, anddecodeTunerStatusleavesd.handledisengaged on an empty handle, so a handle-less delta cannot clear direct presence. The third new test case pins this; I could not construct a delta that spuriously re-emits the edge. - The
atubranch rewrite. Walked it against the deleted code for: no regex match (both skip), match with model handle already set (both keep the existing handle), direct-presence-but-no-handle (both adopt the radio's own ATU handle — unchanged behavior, not a new defect). I could not find a divergence. - Meter-handle sequencing.
m_meterModel.setTgxlHandlenow readsm_tunerModel.handle()before the decode updates it rather than aftersetHandle. Traced placeholder→real:0x00000000first status sets meter handle 0 via theisEmpty()branch, real handle then satisfies the first branch. Correct under both direct and queuedtunerChangeddelivery. - Removal paths.
setHandle({})at 6941/9966/9979 still clears presence; the newd.handlecannot resurrect it, since a removal returns beforedecodeTunerStatus. - The tests against the unfixed code. They cannot compile pre-
TunerDelta::handle, so I checked them against a mutated fix instead: deleting thepresenceChangedemit failspresence.count() == 1, and deleting thed.handleapply fails the placeholder→real case. They are not self-referential.
CI is green on 6f82e4b (build, check-macos, check-windows, Static checks), but ci.yml -R-filters ctest, so green does not mean tuner_model_test ran — it establishes compile/link only. Everything above is reasoned from the head checkout at /tmp/aetherclaude/pr-5339; I have no build or hardware here, so the PGXL misrouting is a code-path argument, not an observed failure. The quickest disproof would be a station with both a PGXL and a TGXL where the TGXL's first amplifier status carries 0x00000000 — if that placeholder frame never actually occurs on real firmware, blocker 1 collapses (and so does the motivation for the disjunct).
Recommendation
Request changes, narrowly. The ordering fix that #5338 asks for is well-built and well-tested — it should land. The replacingPlaceholder disjunct is a different fix for a different defect, it has no test, and as written it lets a PGXL status capture the tuner's identity and encode handle while hiding the amp. Drop it from this PR and the rest is ready; if the model-less-status edge is worth fixing, it deserves its own issue against #4203 with a routing test, ideally by refusing to adopt 0x00000000 as identity rather than by widening the predicate. Thanks for the clear issue write-up and for being upfront that this one came from audit rather than a user report — that framing made the review much easier.
🤖 aethersdr-agent · cost: $6.2469 · model: claude-opus-5
| const bool replacingPlaceholder = | ||
| m_tunerModel.handle() == QLatin1String("0x00000000"); | ||
| if (model == "TunerGeniusXL" || handle == m_tunerModel.handle() | ||
| || replacingPlaceholder) { |
There was a problem hiding this comment.
Blocker. This third disjunct is unconditioned on model and on handle, so while the tuner holds the 0x00000000 placeholder, every amplifier status takes the tuner branch — and since the AmpModel decode is the else of this branch, PGXL statuses are swallowed entirely.
For a model=PowerGeniusXL status arriving in that window:
decodeTunerStatuscachesm_tunerHandle = <PGXL handle>(FlexBackend.cpp:977), sotuner.autotunethen encodestgxl autotune handle=<PGXL>.FlexBackend.cpp:950carries an explicit FlexBackend: guard amp-handle cache against a mis-routed tuner handle #4203 guard against exactly the mirror of this; this opens the direction it doesn't cover.- the new
d.handle/d.modeloverwriteTunerModel's identity with the amp's. AmpModelpresence never latches — no PGXL applet, no auto-connect.
On main a PGXL status fails both existing disjuncts and routes correctly. Storing the placeholder is pre-existing (the isEmpty() branch below already did it), so this window is live on any TGXL station, and permanent if a real handle never arrives.
If the target is the model-less-TGXL-status edge, gate on that rather than on placeholder state alone:
| const bool replacingPlaceholder = | |
| m_tunerModel.handle() == QLatin1String("0x00000000"); | |
| if (model == "TunerGeniusXL" || handle == m_tunerModel.handle() | |
| || replacingPlaceholder) { | |
| const bool replacingPlaceholder = | |
| m_tunerModel.handle() == QLatin1String("0x00000000") | |
| && model.isEmpty(); // never capture an attributed (PGXL) status | |
| if (model == "TunerGeniusXL" || handle == m_tunerModel.handle() | |
| || replacingPlaceholder) { |
Cleaner still would be to stop adopting 0x00000000 as identity at all — decodeTunerStatus's own comment already assumes RadioModel never passes it — which makes this disjunct unnecessary. Either way it needs a routing test; nothing in the suite exercises onStatusReceived.
| m_meterModel.setTgxlHandle(handle.toUInt(nullptr, 0)); | ||
| } | ||
| if (m_flexBackend) m_flexBackend->decodeTunerStatus(m_tunerModel.handle(), kvs); // #4092/#4198 | ||
| if (m_flexBackend) m_flexBackend->decodeTunerStatus(handle, kvs); // #4092/#4198 |
There was a problem hiding this comment.
Now passes the raw handle, which can be the 0x00000000 placeholder. That contradicts FlexBackend.cpp:974-976 ("RadioModel passes the handle it already extracted+sanitized (never the 0x00000000 placeholder)") — the claim was already loose on main, but this makes it plainly wrong. Worth either updating the comment or filtering the placeholder here.
| const bool wasPresent = isPresent(); | ||
| bool changed = false; | ||
|
|
||
| if (d.handle && m_handle != *d.handle) { m_handle = *d.handle; changed = true; } |
There was a problem hiding this comment.
Nit, non-blocking: antennaAChanged and tuningChanged are emitted below inside the field block, so they now fire before the new presenceChanged. On main, setHandle published presence first. A first delta carrying tuning=1 therefore emits tuningChanged(true) for a tuner the rest of the app doesn't yet consider present. TunerApplet.cpp:282 only restyles so I don't think it bites today, but it inverts the ordering this fix is otherwise establishing — hoisting the presence emit above the field block would settle it.
| CHECK(t.relayC1() == 5 && st.count() == 0); | ||
| } | ||
|
|
||
| // ---- placeholder identity is replaced without a second presence edge ---- |
There was a problem hiding this comment.
These three cases are solid and not self-referential — I mutation-checked them by reading: dropping the presenceChanged emit fails presence.count() == 1, and dropping the d.handle apply fails the placeholder→real case.
The gap is that all of them drive TunerDelta directly, so none reaches RadioModel::onStatusReceived — the replacingPlaceholder hunk, which is the riskiest change in the diff, has no coverage at all. A case feeding a model=PowerGeniusXL amplifier status while the tuner holds the placeholder, asserting AmpModel still latches presence, would pin the blocker above.
|
Addressed the review blocker and remaining nits in signed commit
The atomic initial-snapshot fix, direct-presence/no-radio-handle coverage, existing meter-handle assignment, and existing removal paths remain intact. Focused tests, registration validation, and the strict engine-boundary check pass locally. |
Summary
Fixes #5338.
Carry the TGXL handle in
TunerDeltaso identity, operate/bypass state, and IP from the initial radio status are applied as one model snapshot.TunerModelnow publishes first presence only after those fields are current.Route both external-TGXL discovery paths through that atomic delta and extend the existing socket-free tuner model and backend decode tests. This prevents first-presence observers from reading default standby values while preserving direct presence without a radio handle, meter routing, and removal paths.
Constitution principle honored
Principle XI — Fixes Are Demonstrated. The model test observes state from
presenceChanged(true); it fails when atomic handle application is removed and passes after restoration.Test plan
aethercoresources compile while building the focused targetsctest --test-dir build -R '^(tuner_model_test|aetherd_tuner_decode_test)$' --output-on-failure --no-tests=error)python3 tools/check_test_registration.py --strict)python3 tools/check_engine_boundary.py --strict)Checklist
docs/COMMIT-SIGNING.md)AppSettingscallsCHANGELOG.mdis unchanged