Fix PGXL startup status synchronization - #5337
Conversation
There was a problem hiding this comment.
Issue fit
Partially. The diff does exactly what issue #5336's "Suggested correction" asked for — apply operate before publishing first presence, plus a socket-free regression test that fails under the old ordering. That part is correct and I could not break it.
But I don't think it closes the reporter's symptom. Tracing the two stateChanged consumers in the head checkout: MainWindow_Wiring.cpp:6591-6598 re-runs updatePgxlStyle() and ampApplet()->setState(...), and :6546-6547 re-runs updatePowerScale. Both are direct, same-thread connections. Under the pre-fix ordering, emit presenceChanged(true) and the change-gated emit stateChanged() happen in the same applyChanges() call stack with no return to the event loop between them — so the wrong STANDBY paint is overwritten before Qt can repaint. The plain first-delta case was a sub-frame transient, not the lasting STANDBY the reporter described ("until later status or interaction updates it").
The triage comment on #5336 reached the same conclusion independently and named the likely persistent cause — the 0x00000000 placeholder-handle latch — and explicitly asked the maintainer to decide on it. That half is not in this diff. Details in the inline comment.
This is not an argument against merging the ordering fix: it is correct, it matches FlexLib's own order (status applied before the amp is announced), and it removes a real latent trap. It is an argument against closing #5336 on it.
Scope
| File | What it changes | Claimed by title/body? | Verdict |
|---|---|---|---|
src/models/AmpModel.cpp |
Moves the operate apply/emit ahead of the presence latch; splits the d.detectedModel block in two |
Yes | In scope |
tests/pgxl_status_state_test.cpp |
New 48-line test: presence observer sees decoded operate |
Yes (Principle XI) | In scope; see nit 2 |
tests/tests.cmake |
Registers the new target | Yes | In scope |
Everything in the diff is explained by the issue. No CHANGELOG.md entry (correct). No new public/protocol surface — AmpDelta, the decode, and every signal signature are untouched. No deleted guard: the removed lines are the same operate block, relocated verbatim including its behavior, and I checked the deleted comment ("Operate is change-gated; a status without a state leaves it as-is") — that behavior is preserved by the identical if (d.operate && …) condition, though the comment itself did not travel with it. No preference change: this is a defect fix with a repro and a stated authority.
Blockers
None.
Nits (all non-blocking)
stateChangednow fires whilem_presentis stillfalse. On a first-detect delta the new block runs before the presence latch, soupdatePowerScale(MainWindow_Wiring.cpp:6539-6547) evaluatespresent() && operate()asfalseon that emission and computes the barefoot scale, then immediately recomputes correctly whenpresenceChangedfollows. Harmless today — I checked bothstateChangedconsumers and neither has a side effect that isn't redone at presence. But #5336's suggested fix offered "suppressed on the first-detect delta … or emitted after presence," and announcing a state change before the object is announced present is the one shape that leaves a trap for a future consumer that gates onpresent(). Applyingm_operatein place and moving theemitbelow the presence block gets the same fix without it.- A new CMake target where an existing one already owns this state machine.
tests/amp_model_test.cpp(registered attests/tests.cmake:3285) already covers presence latch, operate change-gating, handle matching, removal and reset, withdetected()/update()helpers that construct exactly the delta this new test hand-rolls. #5336's triage recommended adding the case there. The new file re-implements theCHECKmacro and linksQt6::Testwithout using anything from it.tools/check_test_registration.py --strictpasses either way — this is tidiness, not a violation. - Neither test gates the merge.
grepforamp_model|pgxlacross everyctest -Rfilter in.github/workflows/ci.ymlreturns nothing, so the four green checks onbec8eb5adid not run this test. Worth knowing when reading the badge; not something to fix in this PR. - The new test pins only that the presence observer sees
operate == true. It would still pass if a later change stopped emittingstateChangedon that delta entirely. OneQSignalSpyonstateChangedwould close that.
What I tried to break
- The mutation claim. Verified against
main'sAmpModel.cpp:19-41: pre-fix,emit presenceChanged(true)precedes the operate apply, sooperateAtPresencewould capturefalseandcheck()would fail. The test is a genuine regression guard for the ordering, not an implementation-echo. - Whether the reordering breaks the change-gate or handle matching. It does not.
m_handle = d.handlestill runs first, so the relocated!m_handle.isEmpty() && d.handle == m_handleguard evaluates identically in both positions; the splitif (d.detectedModel)blocks are semantically the same as the original nested one.update()-style deltas (nodetectedModel) take the same path as before. - Second/replacement amp, and the removal path.
applyChangeswithd.removedreturns early and is untouched. On re-detect after removal,m_operateis still deliberately left stale by the removal path — a delta carrying nostatekey will show the pre-removal operate value at first presence with nothing to correct it. Pre-existing, not introduced here, but it is the same failure class as the reported bug and the reordering does not help it. - Both
stateChangedconsumers and bothpresenceChangedconsumers (MainWindow_Wiring.cpp:6068,:6546,:6591,:6600) read for a side effect that the presence emission doesn't redo. None found — the PGXL auto-connect at:6068is on presence only and is unaffected by operate. - Empty and placeholder handles. Reproduced by reading, not running: an empty
d.handleon adetectedModeldelta leaves the operate block unreachable in both orderings, same as the placeholder case below.
All of the above is reasoned from the head checkout at /tmp/aetherclaude/pr-5337; I am headless and did not build, run the test, or drive the GUI. Nothing here is an observed runtime result.
Recommendation
Needs maintainer decision — on scope, not on code. The ordering change is correct, minimal, matches upstream FlexLib's order, has a test that genuinely fails without it, and survived everything I tried. Merge it on its own merits. The decision is whether #5336 stays open for the placeholder-handle guard, since the triage flagged that as the piece most likely responsible for the persistent STANDBY and this PR does not include it. Concrete next step: either add the 0x00000000 guard here (three lines, and the existing amp_model_test fixture already has the helpers for a test), or merge as-is and drop "Fixes #5336" from the body so the issue survives.
Nice, tight diff — thank you for the comment explaining why the order matters rather than just what moved.
🤖 aethersdr-agent · cost: $7.2811 · model: claude-opus-5
|
Addressed the bot review in signed commit
Because the persistent placeholder-handle path is now covered, the PR continues to use Focused tests, registration validation, and the strict engine-boundary check pass locally. |
CI failure on
|
|
Attempted the recommended failed-job rerun for Actions run I did not create an empty retrigger commit. The project workflow guidance explicitly rejects empty CI-retrigger commits, and the bot's analysis identifies the failure as the unrelated asynchronous |
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
#5336's ordering defect — presence published before the same delta's operate state, so the applet's presence observer read default-standby — is precisely fixed, mutation-checked in both directions, live-verified on a FLEX-8600 + PGXL 3.8.9, and confirmed by the original reporter. The test is socket-free state-machine injection, exactly the right layer. The ordering half of this PR is model work.
The blocker is the second half: the new adopt-first-real-handle branch reopens a cross-device binding the old code made structurally impossible.
Scope
Two files, both explained by the issue; body checklist claims hold (signed commit verified via CI metadata, no settings calls, CHANGELOG untouched). Preflight: no sockets, no fake peers.
Blocker
1. The handle-adoption branch can bind the TGXL to the PGXL model (inline). The repo documents a routing edge (FlexBackend.cpp ~962, the #4203 comment): a model-less TGXL status arriving before the tuner handle is known falls through to decodeAmplifierStatus. The #4203 guard protects only the backend's own m_ampHandle — the emitted delta still carries the TGXL handle, no detectedModel, and the TGXL's state/kvs. The PR's new else if adopts any real handle while m_present && m_handle.isEmpty() — the exact state the PR's own test establishes (placeholder detect ⇒ present with empty handle). Sequence: PGXL detected with 0x00000000 → model-less TGXL status falls through → AmpModel adopts 0x2000 → TGXL operate/telemetry drive the PGXL applet, and a later amplifier 0x2000 removed tears the PGXL applet down. Old code adopted only under d.detectedModel, so this class could not occur; it self-heals only when the next model-bearing PGXL status re-binds. The smallest structurally-safe fix is to drop the else if entirely — the first branch already adopts on the next model-bearing status, and ignoring model-less updates until then is exactly the pre-PR behavior the issue never complained about. If earlier binding is genuinely needed, it has to key on something that distinguishes amp from tuner, which AmpModel by design does not know.
Nits (non-blocking)
- Latent, trigger unproven: with
m_handleempty-by-design after a placeholder detect, no removal message can ever match (\S+capture ⇒ removal handles are never empty), so presence can only clear on disconnect (reset()— which also skipspresenceChanged(false)). Real mechanism, but nothing shows the radio ever emits a placeholder-handle removal, so this is a robustness gap, not a demonstrated bug — labeled accordingly. - The
0x00000000literal now lives in the vendor-neutralAmpModel— its own header says it "builds no SmartSDR strings" — as the tree's fourth copy, whiledecodeAmplifierStatusalready knows and strips it three lines from where it builds the delta. Sanitizing at the decoder is behaviorally equivalent for the status path (verified across every consumer), with one caveat: don't blank it on theremovedpath, or add a!m_handle.isEmpty()guard to the removal compare. - CI: the red
buildjob ismap_live_update_test (Timeout, 300 s)— the documented pre-existing flake (#5271), unrelated to this diff; a rerun should clear it.
What was verified vs read
Every finding above was verified against the PR head and the routing/decoder sources before adoption (the pass's three candidates came back CONFIRMED / PLAUSIBLE / CONFIRMED, and the "real PGXL updates dropped forever" overstatement in the first candidate was cut down to the actual self-healing window). The ordering fix itself was read, and its mutation claims are consistent with the test's structure; not re-run locally. No bridge session — model-layer change, and the author's live PGXL verification covers the runtime claim.
5dbe5ac to
40f2e1a
Compare
|
Addressed the latest review in signed commit
Validation:
I also attempted the required assignee claim before posting, but GitHub rejected it because The review's placeholder-removal observation remains deliberately unchanged: while identity is empty, treating an arbitrary removal as the PGXL removal would recreate the same TGXL ambiguity. There is no demonstrated wire sequence or safe identity signal for that behavior, so the fix continues to fail closed until a model-bearing PGXL status arrives. |
|
We have pushed updates since the latest review: the latest blocker fixes are now on the branch and all checks are green (current head `40f2e1a8`). When convenient, @aethersdr-agent, could you please review the current head? Thank you. |
|
PR #5337 is updated and ready for another review. I rechecked the current head,
These changes are already on the branch; no additional code commit was needed in this pass. GitHub currently reports Static checks, build, check-macos, and check-windows passing. I did not rerun local tests or perform hardware validation in this pass; prior validation is documented above. The outstanding changes-requested review targets the preceding commit, Coordination: I attempted to assign myself before posting, but GitHub denied |
rfoust
left a comment
There was a problem hiding this comment.
Issue fit
Yes. The current head applies the initial radio-authoritative operate state before publishing first presence, so the presence observer sees the correct state. It also normalizes SmartSDR's 0x00000000 startup sentinel below the neutral model boundary, rejects model-less TGXL-shaped identity adoption, and recovers only through a later model-bearing PGXL status. The prior TGXL-adoption blocker is removed at 40f2e1a8.
Scope
| File/group | What it changes | Claimed by title/body? | Verdict |
|---|---|---|---|
src/core/backends/flex/FlexBackend.cpp |
Normalize the startup sentinel while preserving raw removal handles | Yes | In scope |
src/models/AmpModel.cpp |
Apply state before presence and require model-bearing identity | Yes | In scope |
tests/aetherd_amp_decode_test.cpp |
Decoder coverage for normalization/removal | Yes | In scope |
tests/amp_model_test.cpp |
Ordering, TGXL rejection, and model-bearing recovery coverage | Yes | In scope |
Everything in the diff is explained by #5336 and its triage follow-up. No settings, UI-design, default, dependency, public-API, or CHANGELOG.md changes. All three commits are GitHub-verified signatures.
Blockers
None.
Nits (non-blocking)
- See the inline comment on placeholder removal. The edge is deterministic in the model but no firmware sequence demonstrating it is known, and retaining the raw removal is an intentional fail-closed choice while PGXL/TGXL identity is ambiguous. I would track or clarify it rather than hold this fix.
Verification
GitHub's hosted merge ref used stale base b9f44f35, so I constructed and tested a fresh detached merge of head 40f2e1a8 onto current upstream/main dafe441b (synthetic merge 6e192b48). The native ARM64 focused build succeeded; amp_model_test, aetherd_amp_decode_test, and aetherd_amp_tuner_encode_test passed 3/3. Strict engine-boundary, test-registration, frozen CI-gate, and whitespace checks passed. ARM64 processor/executable checks passed and RNNoise x86 sources were absent. GitHub build, macOS, Windows, and Static Checks are green on the current head.
I did not drive the GUI: the demo backend does not implement PGXL, and this review did not access the operator's live FLEX/PGXL. The contributor's real-hardware verification remains independently unverified here. No model-invocable automated review pass was available, so I performed a second manual lifecycle/failure-mode pass.
What I tried to break
- Restored the old presence-before-state ordering temporarily.
amp_model_testfailed both first-presence assertions, then returned green after restoring the PR code; the regression test genuinely pins the fix. - Injected a model-less TGXL-shaped update while identity was empty; it could not alter state, telemetry, or handle, and later model-bearing PGXL recovery worked.
- Walked first and duplicate detection, unknown-handle updates, removal, reset, and command dispatch.
- Checked current-main overlap in
FlexBackend.cpp; only unrelated capability/GPS additions intervened, and the fresh merge preserves them.
Recommendation
Approve with nits. The prior changes-requested finding is fixed at the current head, and the fresh current-main merge builds and passes the relevant tests. The remaining repository action is for the maintainer to re-review this head and clear or supersede the outdated changes-requested review.
Dismissed after current head 40f2e1a removed the unsafe model-less TGXL handle-adoption path and added regression coverage. Fresh current-main ARM64 merge validation passed; operator authorized dismissal and squash merge.
Summary
Fixes #5336.
Apply an amplifier delta's operate state before publishing first presence. This prevents the PGXL applet's presence observer from reading the model's default standby value while the same initial delta already contains the radio-authoritative operate state.
Keep SmartSDR's
0x00000000startup sentinel insideFlexBackendby normalizing it to an empty neutral handle.AmpModelnow establishes identity only from a model-bearing PGXL delta; a model-less update cannot be adopted as the amplifier handle and therefore cannot bind a mis-routed TGXL update.Extend the existing socket-free state-machine and decoder tests to cover startup ordering, placeholder normalization, removal preservation, rejection of model-less handle adoption, and later recovery through a model-bearing PGXL status.
Constitution principle honored
Principle XI — Fixes Are Demonstrated. The focused regression test fails when the old presence-before-state ordering is restored, passes with the fix, and the corrected startup behavior was verified against a real PGXL. The added identity regression also proves an unidentified model cannot adopt a model-less TGXL handle.
Test plan
origin/mainamp_model_test,aetherd_amp_decode_test,aetherd_amp_tuner_encode_test)ctest --test-dir build -R '^(amp_model_test|aetherd_amp_decode_test|aetherd_amp_tuner_encode_test)$' --output-on-failure --no-tests=error)python3 tools/check_test_registration.py --strict)python3 tools/check_engine_boundary.py --strict)git diff --check)3.8.9on macOSChecklist
docs/COMMIT-SIGNING.md)AppSettingscallsCHANGELOG.mdis unchanged