Repository navigation
fix(gps): report Flex fix state independently of lock - #6505
Conversation
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69cb78365c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Reviewed against #6500, project canon, and the PR head checkout. Findings below are reasoned from code — I am headless, so nothing here was reproduced at runtime.
1. Issue fit
Yes. All six scope bullets in #6500 map onto hunks:
| #6500 requirement | Where |
|---|---|
| Normalize all five states behind the existing GPS seam, case-insensitive, unknown→Unknown, absent preserves | src/core/backends/GpsDelta.h:10-17 (enum), src/core/backends/flex/FlexBackend.cpp:1703-1717 (decode), src/models/RadioModel.cpp:12268 (if (d.fixState) present-only apply) |
New sessions / disconnect / reconnect / radio replacement start Unknown, including firmware that never reports fix |
RadioModel.cpp:12233 resetGpsState(), called from 2400 (retry), 3460 (LAN), 3584 (WAN), 8003 (disconnect), 10078/10101 (backend replacement) |
| Show fix state in the existing AetherGPS readout without deriving validity or lock from the label | src/gui/GpsLocationDialog.cpp:769-787; RadioModel.h:274-283 keeps the veto one-directional (a label never grants validity) |
| Preserve other backends and legacy position handling | RadioModel.h:280-283 — Unknown is a no-op veto, so Icom/HL2/RTL and pre-fix= Flex keep their reported validity verbatim |
| Socket-free parser/model/UI/session regression coverage | tests/gps_fix_state_test.cpp, tests/tests.cmake:6786-6792 |
| Matching user documentation | docs/user/docs/aetherclock-and-gps.md:54-66, docs/automation-bridge.md |
I verified all twelve refreshed rules.json source pointers against the head checkout (7634 → else if (key == "model"), 8318 → consecutive pings unanswered, 12716 → DAX TX create failed, 1335 → dax_rx stream create, etc.). Every one is accurate; the generated reference is not stale.
Test layer is correct per AGENTS.md "Test-layer boundary": wire parsing, model tables and session/lifecycle behaviour → socket-free CTest. No synthetic firmware peer. Positive convergence is routed to the bridge probe and explicitly disclaimed.
2. Scope
| File / group | What it changes | Claimed in body? | Verdict |
|---|---|---|---|
src/core/backends/GpsDelta.h |
GpsFixState enum + std::optional<GpsFixState> fixState |
Yes | In scope |
src/core/backends/flex/FlexBackend.cpp |
decode fix=; rewrite the positionValid rationale comment |
Yes | In scope |
src/models/RadioModel.{h,cpp} |
m_gpsFixState, gpsFixState(), gpsTimeValid(), vetoing gpsPositionValid(), GPS session generation fence, resetGpsState/wireGpsSessionReceivers, m_wanConn = nullptr moved to the top of connectToRadio |
Yes | In scope |
src/gui/GpsLocationDialog.cpp |
GPS FIX card shows fix state; receiver status moves to tooltip + accessible description; address-query reset on invalid position; clock-agreement gate → gpsTimeValid() |
Yes | In scope |
src/core/AutomationServer.cpp |
fixState/timeValid in get gps; available widened; new gps fixfixture verb; clearfixture now also clears positionValid, source, date |
Verb yes; the clearfixture widening is not called out |
In scope — see nit 5 |
tests/gps_fix_state_test.cpp, tests/tests.cmake |
socket-free regression target, joins AETHER_SETTINGS_CONSUMERS |
Yes | In scope |
tools/automation_gps_fix_probe.py |
new bridge probe | Yes | In scope |
docs/automation-bridge.md, docs/user/docs/aetherclock-and-gps.md, rules.json |
docs + generated refs | Yes | In scope |
No unrelated files, no formatting churn, no CHANGELOG.md entry (correct — it must not have one), no deleted guard, no settings keys, no capability-record change. Every hunk is explained by #6500.
One maintainer call: gps fixfixture is new public bridge surface (AutomationServer.cpp:8176-8200, registered at 3330) arriving as a side effect of a bug fix. It is disconnected-only, double-gated, and modelled on the existing gps fixture, so I am not calling it a violation — but third parties bind to the bridge registry, so per GOVERNANCE.md it is your ruling, not mine. Same posture as the interlock probe verb in #6493.
3. Blockers
None.
4. Nits (all non-blocking)
1. src/models/RadioModel.cpp:9209 — onMessageReceived is now dead code, and three comments still name it as the live dispatcher.
void RadioModel::onMessageReceived(const ParsedMessage& msg)
{
handleMessageReceived(msg, m_gpsSessionGeneration);
}Both connect(..., RadioConnection::messageReceived, ...) and connect(..., WanConnection::messageReceived, ...) were deleted from setupBackend/connectViaWan; the only two remaining subscriptions (RadioModel.cpp:12217, 12224) bind lambdas that call handleMessageReceived directly. A grep across src/ and tests/ finds no connect site and no invokeMethod("onMessageReceived"), so this slot is unreachable. The stale references are RadioModel.cpp:9307 ("handled by onMessageReceived"), 11051 ("is handled by onMessageReceived()") and 12557 ("via onMessageReceived()"). Worth deleting the wrapper plus its declaration at RadioModel.h:1448 and retargeting those three comments — and note that the wrapper passes m_gpsSessionGeneration (the current generation), so if it is ever reconnected by name the fence it is supposed to honour becomes a no-op.
2. src/gui/GpsLocationDialog.cpp:777 — the TIME_ONLY sentence asserts availability rather than defining the state (confirmed; CodeGuard/Codex flagged this).
fixDescription += tr(" Time and frequency are available without a position fix.");Your own test asserts fix=TIME_ONLY alone leaves gpsTimeValid() false (tests/gps_fix_state_test.cpp, "TIME_ONLY alone never sets time validity true"), and oscLocked() can be false at the same time — yet the tooltip and accessible description still tell the operator both are available, while the same dialog's clock-agreement row and 10 MHz REFERENCE card say otherwise. Definitional wording fixes it without adding a gate:
fixDescription += tr(" Time only means the receiver supplies time and"
" frequency without a position fix; see the clock"
" and reference readouts for what is actually valid.");3. src/gui/MainWindow.cpp:2401-2414 — the status-bar clock does not get the NO_FIX veto, so it now disagrees with the dialog (confirmed; Codex flagged this).
if (!utcTime.isEmpty()
&& (usablePositionTime
|| (normalizeOscillatorValue(m_radioModel.oscState()) == "gpsdo"
&& m_radioModel.oscLocked()))) {On a Flex GPSDO, caps.hasGpsFrequencyReference is true so usablePositionTime is false, and the second arm accepts utcTime purely because the oscillator is locked. A GPSDO in holdover reports fix=NO_FIX while staying locked, and a fix-only delta re-emits the preserved m_gpsTime — so the status bar re-asserts a frozen GPS clock. This is pre-existing (there was no fix state to consult before), but this PR tightened the dialog's gate to gpsTimeValid() at GpsLocationDialog.cpp:980 and left this one alone, so the two surfaces now contradict each other for the same report. One-line fix:
if (!utcTime.isEmpty()
&& m_radioModel.gpsTimeValid()
&& (usablePositionTime
|| (normalizeOscillatorValue(m_radioModel.oscState()) == "gpsdo"
&& m_radioModel.oscLocked()))) {(Worth checking against the IC-705 path before applying — for Unknown fix state gpsTimeValid() reduces to the raw flag, so I believe it is a no-op there, but you own that call.)
4. src/core/backends/flex/FlexBackend.cpp:1704-1706 — fix is matched untrimmed while the adjacent status is trimmed. const QString fix = kvs.value(QStringLiteral("fix")).toUpper(); vs. kvs.value(QStringLiteral("status")).trimmed().toLower() fourteen lines down. Your test pins 3D and 3D → Unknown deliberately, so this is intentional FlexLib parity, and I could not fetch the archive to check ParseGPSFixState myself. Flagging only because the failure mode is silent: a padded token degrades the readout to Unknown with no log line. A .trimmed() would preserve every documented token and cost nothing.
5. src/core/AutomationServer.cpp:8151-8156 — clearfixture gained three new cleared fields and the body does not say so. delta.positionValid = false; delta.source = QString(); delta.date = QString(); are new. This is a real fix (the old clearfixture left positionValid true, so "cleared" state still read as a usable position) and the probe depends on it, but it is a behaviour change to an existing bridge verb. Worth one line in the body / automation-bridge.md changelog prose so a scenario author who relied on the old partial clear is not surprised.
6. src/models/RadioModel.cpp:2400 — the auto-reconnect timer now clears and re-emits GPS state on every retry tick. resetGpsState() inside the m_reconnectTimer lambda bumps the generation, clears every field and emits gpsStatusChanged + gpsTimeSettingsChanged — once per retry, forever, while the radio is unreachable. onDisconnected() already cleared all of it at 8003, so each tick re-emits an identical empty report and re-runs every GPS consumer (including MainWindow's applyCapabilitiesToUi). Rebinding the subscriptions there is the load-bearing part; the clear-and-emit is redundant. Cheap guard: only emit when something was actually non-default, or split the rebind out of resetGpsState for this call site.
7. Naming, no action needed: gpsTimeValid() is derived from m_gpsPositionValid, i.e. "time is valid because position validity was reported". It works because for Flex that flag really means "receiver locked", and GpsDelta has no independent timeValid. The getter's semantics are correct but the name invites a future caller to assume there is a separate time-validity report behind it; a one-line comment at RadioModel.h:275 would prevent that.
Socket-test surfacing (per the #5254 reporting rule)
tests/gps_fix_state_test.cpp is not a socket-owning test, despite appearances. It includes RadioConnection.h/WanConnection.h and constructs WanConnection wan;, but WanConnection's constructor (src/core/backends/flex/WanConnection.cpp) only wires its member QSslSocket's signals and sets a heartbeat interval — nothing binds, listens or dials. The test drives delivery by emitting messageReceived directly from a std::thread so the call is posted rather than delivered, which is the whole point of the fence assertions. No peer process, no Fake* radio class, no bind()/listen()/connectToHost(). It is registered unconditionally and joins AETHER_SETTINGS_CONSUMERS with TestSettingsProfile isolation, matching the CMake contract. No socket test was removed.
tools/automation_gps_fix_probe.py does open the bridge's local socket, but it is an operator-run tool, not a registered CTest target.
5. What I tried to break
- The subscription rewiring, hardest. Deleting the two permanent
messageReceivedconnects and funnelling raw dispatch throughwireGpsSessionReceiverscould have orphaned meter/profile/M-message handling. I traced every path:wireBackendReceiverState()is called atRadioModel.cpp:955, insidesetupBackend, after the singlem_connection = flex->connection()assignment at887(grep confirms887and thenullptrat1737are the only two write sites), so the new binding sees exactly the pointer the old one did.teardownBackend()nulls bothm_backendandm_connectionbeforerebuildBackendForFamily'sresetGpsState()runs, so that call binds nothing rather than dereferencing a dangling sender, andsetupBackendrebinds immediately after.setBackendForTestreacheswireBackendReceiverState()at10084. ThereceiveMessages=falsearm inonDisconnectedstill rebinds the backendgpsChangedsubscription before its early return. I could not find a path that leaves raw dispatch unbound across a reconnect. - The generation fences, for both false negatives and double dispatch. Every rebind captures the current
m_backendReceiverGenerationandm_gpsSessionGeneration, so fresh reports always pass; the WAN/LAN arms areif (m_wanConn) … else if (m_connection), so no message can be dispatched twice;QObject::disconnecton a default-constructedQMetaObject::Connectionis a safe no-op, so the firstwireGpsSessionReceiverscall is fine. - Moving
m_wanConn = nullptrto the top ofconnectToRadio. This is the kind of reordering that breaks something three functions away. I grepped all 40m_wanConnsites: none fall insidesetupBackend,teardownBackend,ensureOfflineHealthorrebuildBackendForFamily, so nothing between the old and new position reads it. The only behavioural delta is theif (!m_backend) return;early exit (RTL built without librtlsdr), which now leavesm_wanConncleared instead of stale — strictly better. - The address-lookup reset as a Nominatim rate-limit hazard. Clearing
m_hasAddressQueryPositionon every invalid-position refresh means a3D → TIME_ONLY → 3Dflap re-entersrequestAddresswithforce=falseand no dedupe.LocationAddressResolverturns out to hold a rounded-key in-memory cache and a 1 req/s floor (src/core/LocationAddressResolver.{h,cpp}), so a flapping fix hits the cache and issues no network traffic. The body's "the resolver can reuse its cache" is accurate. - Regression risk in the new
gpsPositionValid()veto. I enumerated every consumer:GpsLocationDialog.cpp:831,Ax25HfPacketDecodeDialog.cpp:4733(APRS beacon position),MainWindow.cpp:2314and2406,AutomationServer.cpp:1825. All four want "is this position usable", which is what the veto now answers; the APRS one is arguably the most important and is now correctly refused onTIME_ONLY. For every non-Flex backendfixStatestaysUnknown, which makes the veto a no-op, so the IC-705 and HL2 paths are byte-identical. - The tests, for self-confirmation. The parser half cannot pass on unfixed code —
fix=is dropped onmain, so everyFixCaserow fails. Theradio-a/radio-bloop'scheckClearedwould pass vacuously if backendgpsChangeddelivery were silently unbound, but the earlierdeliver(model, decode("status=Fine Lock#lat=…"))→gpsPositionValid()assertion proves the binding first, so the vacuity does not apply. The "queued raw WAN GPS cannot undo same-family session reset" + "preserves queued non-GPS profile dispatch exactly once" pair is the assertion that actually constrains the fence, and it fails if the generation check is removed. - Case and malformed-token handling at the parser.
kvs[token.left(eq).toLower()]atFlexBackend.cpp:1698lowercases keys, so the test'sFiX=row is legitimate rather than lucky;fix#=3Dyields one token with no=and one witheq == 0, both skipped by the pre-existingif (eq < 1) continue;, so the "malformed field leaves fix absent" row is genuinely testing the key path. - CodeGuard's 254 findings. A dedicated read-only security audit of all nine listed files confirmed none of them.
fixfixturevalidates against a closed five-token allow-list, never concatenates the token into any sink, and is refused while connected at two independent layers (AutomationServer.cpp:8145andRadioModel.cpp:2747).gpsis absent fromkSafeand from every per-verb exception inisReadOnlyRequest, so observe-only mode refuses all three actions — the new action inherits the existing gate rather than sidestepping it. The probe shells out nowhere. Therules.jsonandPhoneCwApplet.cpp/RxApplet.cpphits contain no GPS content at all and are whole-file-scan noise againstmain's tip, not diff content; the192.0.2.*andpasswordhits are an RFC 5737 documentation address and Icom log-matching prose respectively.
What I could not check: anything requiring execution. Specifically — I could not fetch the FlexLib 4.3.16 archive to verify ParseGPSFixState's exact token set and trimming behaviour, so the protocol claim rests on your stated inspection (nit 4); I could not confirm the accessibility DescriptionChanged event behaves on a real screen reader; and I could not run the new test. Hosted CI did not run it either — all seven checks on 69cb783 are green, which is real evidence the tree compiles and links on macOS, Windows and Linux, but ci.yml's only ctest -R steps name six unrelated tests, so zero of gps_fix_state_test's assertions executed in CI. The local four-CTest run and the documented mutation checks are the only evidence those assertions hold, and the body says so plainly.
6. Recommendation
Approve with nits — with one maintainer call. The protocol change is narrow and correct, the veto is one-directional so no label can manufacture validity, the session fence holds up under the ordering attacks above, and the generated references and docs are genuinely in sync rather than approximately so. Every nit is a follow-up rather than a correctness defect; I would take nits 1 and 3 in this PR (dead code with three comments pointing at it, and two surfaces now disagreeing about the same report) and let the rest ride. The maintainer call is whether gps fixfixture belongs in the permanent bridge registry.
Thanks for the unusually honest validation section — "the before capture is a reconstruction, not an untouched-main build" and "hosted CI is pending" are exactly the disclosures that make the rest of the proof worth reading.
🤖 aethersdr-agent · cost: $17.0366 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (254 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $17.2418 · model: claude-opus-5 |
|
Addressed the review feedback in signed commit 26bd8698c. Both inline findings were reproduced and corrected; the replies carry the before/after bridge evidence.
The protocol reference remains the official FlexLib archive; the local archive still hashes to Validation: full native ARM64 build with RADE ON, four focused CTests, expanded native bridge proof, docs build, accessibility lint, engine-boundary check, registration/manifest/generated-reference checks and Log Analyzer rules pass. The clock and empty-reset mutations fail at the intended assertions; correct sources were restored and rebuilt. Bridge sessions used isolated settings, disconnected public-location fixtures, disabled TX and the shared app lock; only owned apps were closed. Live GPS acquisition and screen-reader behavior remain unverified. Hosted CI is rerunning on this commit; the four CTests were run locally, not added to the frozen per-PR gate. For the CodeGuard advisory, I mapped the 40 visible entries to the reviewed Generated with OpenAI Codex. |
26bd869 to
a5494ef
Compare
jensenpat
left a comment
There was a problem hiding this comment.
Review and landing verification
#6500 is addressed: the Flex parser carries a typed fix independently of receiver/reference lock; absent partial fields preserve state, session changes clear it, and old queued GPS cannot repopulate the new session. The AetherGPS readout, clock fallback and same-coordinate address recovery use that state consistently. No remaining blockers or nits.
Scope
| File/group | Change claimed in body | Verdict |
|---|---|---|
GpsDelta.h, FlexBackend.cpp |
Typed five-state fix and published token decoding | In scope |
RadioModel.{h,cpp} |
Separate validity vetoes, session resets and stale-event fencing | In scope |
GpsLocationDialog.cpp, MainWindow.cpp |
Fix readout/accessibility, address recovery and valid-time gate | In scope |
AutomationServer.cpp |
Diagnostic fields, disconnected-only fix fixture, complete fixture cleanup | In scope |
gps_fix_state_test.cpp, tests.cmake |
Socket-free regression target and settings isolation | In scope |
automation_gps_fix_probe.py |
Repeatable model/UI assertions | In scope |
| User/bridge docs and Log Analyzer references | Matching behavior documentation and generated source pointers | In scope |
No workflow, dependency, settings-default, capability, theme/layout or CHANGELOG changes. The requested landing accepts the described additive fixture surface; no additional scope was introduced.
What survived review
- Inspected the local published FlexLib 4.3.16.42034
Radio.csenum, parser andfixdispatch: exact case-insensitive tokens, untrimmed malformed values -> Unknown, default Unknown. Reviewed partial reports, positive labels without validity, Time-only/No-fix vetoes and older-firmware/Icom fallback. - Reviewed subscription ordering and teardown, queued LAN/WAN/old-backend delivery, repeated empty resets and preserved non-GPS raw dispatch. Rebase preserves main's existing generation-guarded radio-status, transmit-answer and notch subscriptions rather than restoring an unguarded connection. Range-diff contains only those integration resolutions and refreshed generated lines.
- Built ARM64
AetherSDRplus four test targets from this clean head.QT_QPA_PLATFORM=offscreen ctest --test-dir build -j22 --no-tests=error -R '^(gps_fix_state_test|aetherd_residual_decode_test|icom_gps_readback_test|backend_family_switch_test)$': 4/4 passed, including a final run with Qt 6.12 runtime pinned. New/modified regression sources own no socket or synthetic firmware peer. The focused tests are local evidence; they were not added to the frozen PR gate. - Ran
automation_gps_fix_probe.pyagainst the newly built, isolated disconnected app at/private/tmp/aethersdr-pr6505.sock. ObservedTIME_ONLY: positionValid=false, timeValid=true, referenceLocked=true;NO_FIX: positionValid=false, timeValid=false, referenceLocked=true; the No-fix computer clock advanced02:29:38Z -> 02:29:39Zwhile cached GPS time remained unchanged. All five actual widget readouts, address recovery after each veto, cleanup, and Time-only with no independent validity/reference passed. Inspected the Time-only screenshot. Final radio snapshot:connected=false, transmitting=false. Only the owned app was closed; shared lock released. No RF or live radio access. - Engine-boundary and command-plane diagnostics are unchanged from main (101 tracked boundary findings, 221 tracked commands), and all applicable lightweight static/generated-doc checks pass. Hosted Docs site/PDF and Static Checks passed. All three rebased commits are GitHub-verified.
The prior two inline findings are fixed in the rebased a5494eff2 and already have evidence-bearing replies/resolution. Remaining earlier body nits are fixed or explicitly supported by current source: strict token parsing matches FlexLib, complete clearfixture behavior is documented, empty resets suppress duplicate notifications, and the derived time-validity semantics are documented. Reviewed CodeGuard's visible advisory examples against the diff; they identify existing whole-file syntax/prose rather than a newly introduced sink. The unseen advisory count is not independently audited.
A separate model-invocable code-review skill was unavailable; this was a manual review, including verification of the existing bot findings. Author mutation evidence was inspected but not independently rerun. Physical GPS acquisition/live 4.3 firmware and native screen-reader behavior remain unverified. The initial local launch mixed Qt installations; pinning DYLD_FRAMEWORK_PATH/plugins to Qt 6.12 resolved it without source changes.
Approve at a5494eff26f9556d1882b27feecfc238be4edb80; merge only after all four required platform/static checks are green and live PR gates remain clear.
Generated with OpenAI Codex (GPT-6 Astra)
Flex GPS reports now include a position-fix state independently of receiver and oscillator lock. The backend dropped
fix=, so the model could not retain it and AetherGPS's GPS FIX card displayed receiver status such as Locked even for a time-only report.Fixes #6500.
Refs #6437 (parent integration tracker; remains open).
The protocol authority is the official published FlexLib v4.3.16.42034 source,
FlexLib/Radio.cs:155-165,12300-12328,12409-12412. The official archive was downloaded and inspected locally (SHA-25660978db51dac6ea781899dd80832ad4e7876ce96ed72532b57106376fc26463f). The implementation is clean-room C++ using that public enum/parser contract.3D,2D,TIME_ONLY,NO_FIXand Unknown through the existing typed GPS delta and model notification. Recognized tokens are case-insensitive; unsupported/malformed values become Unknown. Absent fields preserve prior state in partial reports. A fresh session without this field remains Unknown.clearfixturealso clears position validity, source and UTC date, preventing a cleared report from remaining usable. No layout, theme, GPS configuration or NTP configuration change.Maintainer presentation review: the GPS FIX card explicitly shows Unknown when no recognized fix state has been reported, including older firmware and other feeds. Receiver status remains available in the tooltip/accessibility description rather than replacing Unknown.
Validation
gps_fix_state_test,aetherd_residual_decode_test,icom_gps_readback_test,backend_family_switch_test. The new socket-free test covers exact/case/malformed/unknown tokens, partial and old-firmware reports, mixed fix/clock notification, independent validity, session resets and queued prior GPS across disconnect/LAN/WAN/backend replacement. Queued and fresh non-GPS profile status still dispatch exactly once. A repeated already-empty reset emits no duplicate telemetry or time-settings notification.3D → TIME_ONLY → 3D; restoring it re-enables address refresh. The recovery proof uses an invalid HTTP service URL for a deterministic local error, so it sends no address request. Restoring duplicate reset notifications fails the new model assertion; removing the status-bar time-validity gate fails the bridge clock assertion. Correct sources are restored and rebuilt afterward.npm ci && npm run buildsucceeds indocs/user. Engine-boundary/capability/command-plane findings are identical to the base; touchpoint manifest, test registration, frozen CI gate, generated bridge/reference docs and Log Analyzer rules pass. GPS UI accessibility lint reports zero findings; color ratchet passes.Agent automation bridge proof
The agent automation bridge ran native Cocoa builds with isolated settings, radio autoconnect disabled, TX automation disabled, and the shared FIFO radio/app lock. Each owned app was terminated and the lock released. Location is the pre-existing public Mount Wilson Observatory fixture, and the NTP address is reserved TEST-NET
192.0.2.80.The before capture temporarily restores the exact old readout assignment in the otherwise patched worktree; it is a reconstruction of the old display behavior, not an untouched-main application build. The model reports
fixState=TIME_ONLY,positionValid=false,timeValid=true,referenceLocked=true, but GPS FIX displays Locked. The same regression probe rejects it. The old parser's dropped field is independently demonstrated by the parser mutation test.On the corrected build,
gps fixture 8000,gps fixfixture,get gps,invoke AetherGPS... trigger,dumpTreeandgrabprove:After clearing the fixture, reporting 3D alone displays 3D but leaves position/time validity and reference lock false. The probe also verifies the latitude/longitude presentation, that Time only retains clock agreement, that NO_FIX resumes a ticking status-bar computer clock while cached GPS time and reference lock remain unchanged, and that address lookup resumes after TIME_ONLY/NO_FIX → 3D at unchanged coordinates. TIME_ONLY alone after fixture cleanup leaves time validity and reference lock false and its tooltip describes only the reported mode. Fixtures inject typed GPS deltas; the socket-free tests separately validate raw Flex parsing.
Before → after, with the same Time-only fixture:
Additional captures: Unknown, 3D, 2D, No fix, Initial JSON evidence. Proof assets live on a separate signed fork branch and are absent from this PR's diff.
The GPS fix-state snapshot and disconnected
gps fixfixtureaction were added because existing fixtures could not select those five states.tools/automation_gps_fix_probe.pyprovides repeatable real-window assertions and saves JSON/PNGs. The bridge is sufficient for this fixture proof; physical GPS acquisition and live firmware convergence are unverified. No RF transmission was performed or authorized.Landing verification
Reviewed and rebased onto
e8106f56bb81765d36c6c5afbec2dcdd12dd40e5; final heada5494eff26f9556d1882b27feecfc238be4edb80. Conflict resolution preserves main's generation-guarded radio/transmit/notch bindings, the interlock bridge entry and all settings-test registrations; Log Analyzer source references were regenerated. Original authorship is preserved and all three rewritten commits are verified by GitHub.gps_fix_state_test,aetherd_residual_decode_test,icom_gps_readback_test,backend_family_switch_test; run headlessly, with the Qt 6.12 runtime pinned.build,check-windows,check-macos,Static checks) and hosted Docs site/PDF passed on the rebased head.Generated with OpenAI Codex (GPT-6 Astra)