Skip to content

Synchronize TGXL startup state before presence - #5339

Open
w5jwp wants to merge 4 commits into
aethersdr:mainfrom
w5jwp:fix/tgxl-startup-status
Open

Synchronize TGXL startup state before presence#5339
w5jwp wants to merge 4 commits into
aethersdr:mainfrom
w5jwp:fix/tgxl-startup-status

Conversation

@w5jwp

@w5jwp w5jwp commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #5338.

Carry the normalized TGXL handle in TunerDelta so identity, operate/bypass state, and IP from the initial radio status are applied as one model snapshot. TunerModel now publishes first presence only after those fields are current.

SmartSDR's 0x00000000 startup placeholder is filtered in FlexBackend, so it cannot become tuner identity, overwrite a real handle, create false presence, or capture a PGXL placeholder status. Captured TGXL status replay under demo/sim preserves the prior backend-neutral identity path without duplicating SmartSDR field decoding in RadioModel.

Route both external-TGXL discovery paths through the atomic delta and extend the existing socket-free tuner model, backend decode, and RadioModel routing coverage. This preserves direct presence without a radio handle, meter routing, removal paths, and demo/sim identity.

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. Dedicated regressions cover placeholder filtering and backend-neutral demo/sim identity.

Test plan

  • Rebased onto current origin/main
  • Modified aethercore sources compile while building the focused targets
  • Focused tests pass (ctest --test-dir build -R '^(tuner_model_test|aetherd_tuner_decode_test|aetherd_amp_tuner_encode_test|radio_capability_gating_test)$' --output-on-failure --no-tests=error)
  • Registration check passes (python3 tools/check_test_registration.py --strict)
  • Engine-boundary check reports no blockers (python3 tools/check_engine_boundary.py --strict)
  • Diff whitespace check passes (git diff --check)
  • Atomic initial-snapshot regression assertion mutation-checked
  • Placeholder rejection, demo/sim identity, direct-presence/no-radio-handle behavior, and post-presence edge-signal ordering covered
  • Real TGXL hardware verification; this issue was identified by code audit and the invariant is tested at socket-free model and routing boundaries

Checklist

  • Commit is signed (docs/COMMIT-SIGNING.md)
  • No new AppSettings calls
  • Code is clean-room and follows the existing normalized backend-delta architecture
  • No meter UI changes
  • Reproduction and behavior are documented in the linked issue and this PR; CHANGELOG.md is unchanged
  • No security-sensitive changes

@w5jwp
w5jwp requested a review from a team as a code owner August 30, 2026 18:31
@w5jwp

w5jwp commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the issue-triage recommendations in signed commit 6f82e4b8:

  • RadioModel explicitly routes the first real TGXL handle when the model currently carries 0x00000000, allowing the atomic TunerDelta to replace the placeholder even if the follow-up status omits model.
  • tuner_model_test now covers placeholder-to-real-handle replacement without a duplicate presence edge.
  • A direct-connected tuner with no radio handle is covered; applying a handle-less state delta preserves direct presence and the empty radio handle.
  • aetherd_tuner_decode_test covers carrying the placeholder followed by the assigned real handle.
  • m_meterModel.setTgxlHandle(...) remains in the existing RadioModel identity path unchanged, preserving VITA meter routing.

I did not add removed to TunerDelta: the existing setHandle({}) removal paths remain intact, and expanding removal ownership was presented as an optional maintainer choice rather than necessary to fix the initialization ordering.

Focused tests, registration validation, and the strict engine-boundary check pass locally.

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

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.cppatu 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.cppamplifier 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) sets FlexBackend::m_tunerHandle = pgxlHandle (FlexBackend.cpp:977-978), so tuner.operate / tuner.bypass / tuner.autotune then encode tgxl … handle=<PGXL>. decodeAmplifierStatus at FlexBackend.cpp:950 carries an explicit guard against precisely the mirror image of this ("Defense in depth (#4203) … refuse to cache a known-tuner handle so a later amp.operate can never mis-target the TGXL") — this change opens the direction that guard does not cover, and tgxl autotune is a transmit-keying command.
  • d.handle and d.model overwrite TunerModel's identity with the PGXL's, so TunerModel::modelName() becomes PowerGeniusXL.
  • m_meterModel.setTgxlHandle(pgxlHandle) fires, since handle != "0x00000000" && handle != m_tunerModel.handle().
  • AmpModel never sees the status, so PGXL presence never latches — no amp applet, no auto-connect at MainWindow_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 0x00000000 as identity at all, so handle == 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 / decodeTunerStatus directly; neither exercises RadioModel::onStatusReceived, so nothing in the suite would catch blocker 1. A socket-free case feeding a model=PowerGeniusXL amplifier status while the tuner holds the placeholder, and asserting AmpModel presence, would pin it.
  • Edge signals now precede presence. antennaAChanged / tuningChanged are emitted mid-loop in applyChanges, i.e. before the new presenceChanged. On main setHandle published presence first. A first delta carrying tuning=1 now emits tuningChanged(true) for a tuner the rest of the app does not yet consider present. TunerApplet.cpp:282 only 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-976 still claims RadioModel never passes the placeholder; RadioModel.cpp:9996 now passes raw handle unconditionally. The claim was already loose on main; 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/nowPresent bracket the entire field block and presenceChanged precedes stateChanged. The one seam is the edge signals above.
  • Direct-connection presence with no radio handle. isPresent() is !m_handle.isEmpty() || m_directPresence, and decodeTunerStatus leaves d.handle disengaged 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 atu branch 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.setTgxlHandle now reads m_tunerModel.handle() before the decode updates it rather than after setHandle. Traced placeholder→real: 0x00000000 first status sets meter handle 0 via the isEmpty() branch, real handle then satisfies the first branch. Correct under both direct and queued tunerChanged delivery.
  • Removal paths. setHandle({}) at 6941/9966/9979 still clears presence; the new d.handle cannot resurrect it, since a removal returns before decodeTunerStatus.
  • The tests against the unfixed code. They cannot compile pre-TunerDelta::handle, so I checked them against a mutated fix instead: deleting the presenceChanged emit fails presence.count() == 1, and deleting the d.handle apply 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

Comment thread src/models/RadioModel.cpp Outdated
Comment on lines +9985 to +9988
const bool replacingPlaceholder =
m_tunerModel.handle() == QLatin1String("0x00000000");
if (model == "TunerGeniusXL" || handle == m_tunerModel.handle()
|| replacingPlaceholder) {

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.

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:

  • decodeTunerStatus caches m_tunerHandle = <PGXL handle> (FlexBackend.cpp:977), so tuner.autotune then encodes tgxl autotune handle=<PGXL>. FlexBackend.cpp:950 carries 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.model overwrite TunerModel's identity with the amp's.
  • AmpModel presence 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:

Suggested change
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.

Comment thread src/models/RadioModel.cpp Outdated
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

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.

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.

Comment thread src/models/TunerModel.cpp
const bool wasPresent = isPresent();
bool changed = false;

if (d.handle && m_handle != *d.handle) { m_handle = *d.handle; changed = true; }

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.

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.

Comment thread tests/tuner_model_test.cpp Outdated
CHECK(t.relayC1() == 5 && st.count() == 0);
}

// ---- placeholder identity is replaced without a second presence edge ----

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.

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.

@w5jwp

w5jwp commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review blocker and remaining nits in signed commit d2eebb9d:

  • Removed the replacingPlaceholder routing disjunct. A PGXL status can no longer enter the tuner branch merely because the TGXL currently holds 0x00000000.
  • Removed the out-of-scope placeholder-transition tests and claims added with that routing change.
  • Corrected the stale FlexBackend::decodeTunerStatus() comment: a placeholder may be carried in the delta for parity, but it is not cached for outgoing tuner commands.
  • Deferred antennaAChanged and tuningChanged until after first presence while preserving their established antenna-before-tuning order.
  • Extended tuner_model_test to verify both edge signals are deferred at the first-presence callback. Moving them ahead of presence makes the test fail.

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.

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

Issue fit

#5338's atomic-snapshot goal — identity, operate/bypass, and IP applied as one delta so first-presence observers never read defaults — is correctly implemented for the ordering itself, mutation-checked, and tested at the right socket-free layer. The blocker is one deliberate design choice inside it: carrying the 0x00000000 placeholder into the delta "for parity", which the model then adopts unguarded. The old code's refusal to let that literal into m_handle was load-bearing, and removing it opens four verified paths.

Scope

Six files, all explained by the issue. Preflight: no sockets, no fake peers. Honest checkbox: no hardware verification (code-audit-identified issue; model-boundary tested) — appropriate for this class of change.

Blockers

1. The placeholder handle now enters TunerModel::m_handle (inline at the decode). decodeTunerStatus sets d.handle for any non-empty handle — the comment says "carry that identity in the delta for parity, but never cache it" — and applyChanges adopts it on difference with no guard. Four verified consequences:

  • A later 0x00000000 status overwrites a real handle. The routing predicate (handle == m_tunerModel.handle()) then stops matching model-less TGXL statuses (state freezes), and the removal paths never match — a phantom-present tuner until reconnect.
  • isPresent() goes true on placeholder-only identity, enabling Operate/Bypass/Autotune whose commands FlexBackend::invokeExtension refuses ("no tuner handle") with no user-visible error — the optimistically-flipped buttons lie.
  • The routing predicate now matches a PGXL's placeholder first status (amplifier 0x00000000 model=PowerGeniusXLhandle == m_tunerModel.handle() → tuner branch), so the amp's detect delta can be swallowed by the tuner path.
  • The vestigial else-arm becomes reachable and calls setTgxlHandle(0) after a real handle was known, wiping the TGXL meter indices.

Two more consequences the cross-file trace confirmed: with the placeholder parked in m_handle, a PGXL's placeholder first status matches the routing's second disjunct, is decoded as a tuner, and the amp else if is skipped — AmpModel never sees the announcement while TunerModel absorbs model=PowerGeniusXL and MainWindow opens a TGXL port-9010 connection to the PGXL's IP.

The fix is one line at the single right layer: sanitize at the decode (if (!handle.isEmpty() && handle != "0x00000000") d.handle = handle;) so the delta genuinely carries the "normalized tuner identity" its own field comment promises, and no downstream layer needs the rule — plus a presence definition that requires the state snapshot (or the atu branch not minting presence from a bare handle). Placeholder-only presence staying false is the pre-PR behavior.

2. Demo/sim mode loses TGXL identity entirely. On main, m_tunerModel.setHandle(handle) ran outside any backend guard, so a captured amplifier … model=TunerGeniusXL replayed in demo mode (where m_flexBackend is null) still produced presence. At this head, identity reaches the model only through if (m_flexBackend) m_flexBackend->decodeTunerStatus(…) — in demo/sim the TGXL applet can never appear. Verified by diffing the two routing branches directly.

Nits (non-blocking)

  • Identity loss stayed outside the seam: three setHandle({}) clears in RadioModel use setHandle's different signal ordering (presence, then unconditional stateChanged) versus applyChanges' — so the teardown edge keeps the stale-values-at-presence-change shape this PR fixes for acquisition. Mirroring AmpDelta.removed into TunerDelta and reducing setHandle to clearHandle() closes both, and deletes the second and third copies of the presence-transition logic (the direct-connection lambdas are the third).
  • The if/else at the meter-handle site is now degenerate (both arms identical); collapse to one guarded call.
  • The antennaAChanged/tuningChanged locals shadow the signals of the same name, forcing emit this->… — rename to pending* so a dropped this-> can't silently become a non-emit.
  • The atu routing branch stamps a handle with no placeholder check at all — same sanitize-at-decode fix covers it.
  • The placeholder literal now appears at three sites; an isRealHandle() beside parseStatusHandle would also catch 0x0-style variants a literal compare misses.
  • The atu branch now routes the radio's internal ATU handle through decodeTunerStatus, which caches it as m_tunerHandle for outgoing tgxl set commands — the old code only set the model handle. On a TGXL-less radio this synthesizes tuner presence and an encode target pointing at the radio's own ATU. Pre-existing in half; the encode-cache half is new — worth a guard or an explicit ruling.
  • The ATU-before-amplifier ordering still defeats the atomic-snapshot invariant (a state-less atu <handle> delta flips presence with default operate/bypass and empty IP, so the wiring handler skips connectToTgxl and never retries) — pre-existing on main, verified, so an incompleteness note rather than a regression; the presence-requires-state fix would close it.
  • Signal-ordering coupling is same-thread-safe today (verified: tunerChanged is a direct connection) but the meter-handle test now depends on a side effect of a signal emitted later in the same function — fragile under any future queued connection; and a synchronous connectToTgxl completion can re-enter applyChanges mid-emit.
  • Coordination note, not a defect: this branch fixes TunerModel's ordering while sibling #5337 fixes AmpModel's — each head still contains the other's bug, so they should land together (and #5337 currently has its own blocker).

What was verified vs read

The blocker's mechanism was verified by me in the PR head (the decode's cache-vs-delta split, the unguarded adoption, isPresent()'s definition, and the routing predicate), not adopted from the automated pass; two independent angles converged on it and a third supplied the overwrite scenario. The mutation claim and focused-test results were read, not re-run. No bridge session — model-layer change with no hardware verification claimed by the PR either.

// is verbatim. The change-gating / edge signals live in TunerModel::applyChanges.
TunerDelta d;
if (!handle.isEmpty()) {
d.handle = handle;

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.

Blocker 1 — the delta carries the placeholder the cache line above just refused. "Carry that identity in the delta for parity" puts 0x00000000 into TunerModel::m_handle (the adoption is unguarded), which the old routing code explicitly prevented — and FlexLib gives the "parity" claim no basis: grep -r 0x00000000 reference/FlexLib_API_* returns nothing, and Tuner.Handle is get-only, assigned once in the constructor and never rewritten by StatusUpdate (Principle I).

Verified consequences: a later placeholder re-announce overwrites a real handle (routing stops matching model-less TGXL statuses; amplifier 0x2000 removed never matches → phantom-present tuner and a 5-s port-9010 reconnect loop); placeholder-only presence enables Operate/Bypass whose commands invokeExtension refuses invisibly while the buttons optimistically flip; and a PGXL's placeholder first status matches handle == m_tunerModel.handle(), is decoded as a tuner, and the amp branch is skipped.

One line closes the family and makes the field match its own "normalized tuner identity" doc:

Suggested change
d.handle = handle;
if (!handle.isEmpty() && handle != QLatin1String("0x00000000")) {
d.handle = handle;
}

Comment thread src/models/RadioModel.cpp Outdated
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

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.

Blocker 2 — identity is now gated behind m_flexBackend, so demo/sim mode loses the TGXL entirely. On main, m_tunerModel.setHandle(handle) ran unconditionally in this branch; at this head the only path to the model is this guarded decode, and m_flexBackend is null in demo mode. A captured TGXL status replayed in demo/sim can never produce presence — the applet and status-bar block are unreachable there. Either route the delta through a backend-neutral path for the null case, or state the demo-mode loss deliberately.

@w5jwp
w5jwp force-pushed the fix/tgxl-startup-status branch from d2eebb9 to b166ae4 Compare September 1, 2026 21:38
@w5jwp

w5jwp commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both current blockers and the two directly related cleanup nits in signed commit b166ae46, after rebasing onto current origin/main.

  • FlexBackend::decodeTunerStatus() now filters 0x00000000 from TunerDelta::handle as well as the command cache. The neutral model can no longer adopt the sentinel, overwrite a real handle, or become present from placeholder identity.
  • The RadioModel tuner branch now preserves backend-neutral identity application when captured TGXL status is replayed under demo/sim without m_flexBackend. This applies only normalized identity and does not duplicate SmartSDR state decoding above the seam.
  • Added decoder coverage proving a placeholder status still carries model/state but no identity.
  • Added a real RadioModel::onStatusReceived regression under SimBackend proving TGXL identity and presence survive without a Flex backend.
  • Collapsed the duplicate meter-handle branches and renamed the pending antenna/tuning locals so they no longer shadow signals.

Validation:

  • tuner_model_test, aetherd_tuner_decode_test, aetherd_amp_tuner_encode_test, and radio_capability_gating_test: pass
  • check_engine_boundary.py --strict: pass (tracked legacy warnings only)
  • check_test_registration.py --strict: pass
  • git diff --check: pass

I attempted the required assignee claim before posting, but GitHub rejected it because w5jwp lacks ReplaceActorsForAssignable permission.

I left the broader removal-delta and ATU/presence redesign suggestions unchanged: the review identifies those as pre-existing or architectural follow-ups, and expanding ownership here would exceed this ordering fix.

@w5jwp

w5jwp commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

We have pushed updates since the latest review: the latest blocker fixes are now on the branch and all checks are green (current head `b166ae46`). When convenient, @aethersdr-agent, could you please review the current head? Thank you.

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.

TGXL presence can publish before initial state snapshot

2 participants