feat(arch): introduce RadioSession — the per-radio aggregate (#3351 / #3445)#3544
Conversation
…3445) Eleventh PR of the #3351 series and the first architectural (non-pure- motion) change: the single-radio assumption stops being load-bearing. What changes: - New models/RadioSession.{h,cpp}: a QObject aggregate that owns the RadioModel (by value — identical construction semantics to the old MainWindow member) and carries session identity (id, label). The class doc spells out the v2+ plan from the #3445 Camp B brief: TciServer + CatPort[] ownership, the MainWindow_Session.cpp wire bodies becoming session methods, a per-session settings facade. - MainWindow.h: `RadioModel m_radioModel` (by value) becomes std::vector<std::unique_ptr<RadioSession>> m_sessions; RadioSession* m_session; // active session RadioModel& m_radioModel; // alias into the active session The vector sits at the exact old member position, so destruction order relative to surrounding members is unchanged. The alias binds in the constructor init list (declaration order respected — no -Wreorder). Why the alias: 937 m_radioModel references across the 11 MainWindow TUs compile completely unchanged. The single-radio code path is byte-for-byte identical at runtime — one session in the vector, the same RadioModel constructed at the same point in the same order. The multi-radio seam (#3445) is now `m_sessions.size() > 1` plus an active-session switch, instead of surgery on a by-value member. New code should reach the model via m_session->radioModel(); the alias exists to keep this PR reviewable, not as the permanent interface. MainWindow.cpp: +14 lines (session factory + init list). Verified: full build clean (no new warnings; MainWindow init order verified by absence of -Wreorder), all 33 ctest suites pass, a11y lint clean. Refs #3351 #3445. Principle XI.
The new RadioSession.h / <memory> / <vector> includes landed inside the #ifdef HAVE_SERIALPORT block, breaking builds without Qt SerialPort. Move them unconditional. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jensenpat
left a comment
There was a problem hiding this comment.
Reviewed the diff: clean v1 aggregate, alias bridge keeps the 937 call sites untouched, destruction order preserved by vector position. Fixed one latent build break in a follow-up commit (495b4ec): the new RadioSession// includes were inside the #ifdef HAVE_SERIALPORT block, which would break builds without Qt SerialPort. Verified full local build clean after the fix; CI green on the new head.
There was a problem hiding this comment.
Reviewed the full diff and cross-checked against main. This is a careful, well-scoped first architectural step — the code looks correct, and the claims in the description hold up:
- Init order is sound. The init-list order (
m_sessions→m_session→m_radioModel) matches the declaration order inMainWindow.h, andm_sessionis fully initialized before being dereferenced for the reference bind. No-Wreorderexposure, as stated. - Construction semantics are genuinely identical. The old ctor default-constructed
RadioModel m_radioModelat that member position; the newRadioSessiondefault-constructs its by-valueRadioModelat the same sequence point, on the same (GUI) thread, with no QObject parent — same as before. - The reference alias is safe as wired.
unique_ptrgives theRadioSessiona stable address, so vector reallocation can't danglem_radioModel, and keeping the vector at the old member position preserves destruction order relative to surrounding members (members declared after it, including the trivially-destructible reference, still tear down first). - Namespace, AUTOMOC (global
CMAKE_AUTOMOC ON), and the CMake source addition are all consistent. All five files are within the stated scope; no settings/error-boundary surface is touched.
Two forward-looking notes, neither blocking:
- The alias is a one-way door for the active-session switch. A C++ reference can't be reseated, so when the
m_sessions.size() > 1+ switch-active-session PR arrives,RadioModel& m_radioModelmust be retired (or converted to an accessor) before switching can work — i.e., the 937 call sites are deferred, not avoided. That's a fine trade for this PR, but it might be worth a sentence in the class doc or #3445 so the v2 author doesn't try to "just swap the pointer" and wonder why the alias still points at session 0. Relatedly, any futurem_sessions.clear()/erase()of the front session dangles bothm_sessionandm_radioModel— worth keeping that invariant ("front session lives as long as MainWindow") stated where the vector is declared. - Tiny:
setSessionId(0)inmakeInitialSessions()restates the in-class default, and them_session{nullptr}initializer is dead given the ctor init list. Harmless explicitness — fine to keep.
The QA checklist (full connection-lifecycle + shutdown-while-connected) is the right call for an ownership move even with identical code paths. Thanks @ten9876 — the alias-bridge approach keeps this reviewable at 100 lines instead of an 11-TU sweep, and the v1/v2 scope split in the class doc makes the follow-up obvious.
🤖 aethersdr-agent · cost: $11.0008 · model: claude-fable-5
## Summary Doc-only. Corrects two items the RadioSession v1 class doc (merged in #3544) listed as "v3 scope" that don't survive closer inspection. No code change — captures a finding so neither gets attempted as written. I went to build the **SessionSettings facade** (the documented next step) and the investigation showed it's premature. Rather than build speculative infrastructure with no consumer — the first such thing in the 12-PR series — here's the finding, recorded in the class doc where the next contributor will see it. ## Finding 1: the wire bodies can't move onto RadioSession `wireDiscovery` / `wireRadioModel` / `wirePanLifecycle` are **~100 `connect()` calls with ~76 references to MainWindow's widgets** (applet panel, title bar, pan stack, spectrum widgets, status bar). They're application-layer model→UI glue. Moving them onto a `src/models/` class would invert the dependency — RadioSession would have to `#include` the GUI. They belong in the GUI layer (where they are now), full stop. ## Finding 2: the SessionSettings facade is premature There is no per-radio AppSettings namespace to wrap. Per-radio state today is: | Category | Examples | Where it lives | |---|---|---| | Global | `StationName`, `LastConnectedRadioSerial` | flat AppSettings (genuinely global — station is per-operator, last-serial is a reconnect pointer) | | Radio-side | slice/pan positions, modes | FlexRadio SSDR profiles (`profile global info`) — see #3384 | | Already per-radio | band-stack entries | `BandStackSettings::sanitizeSerial()` → `"Radio_<serial>"` keys | **`BandStackSettings` is the one working per-radio-namespacing pattern in the codebase** — it's the template multi-radio should follow when a second session creates a real consumer. A facade built now would wrap nothing and have no caller. ## What this means for the series The #3351 refactor is **complete**. The architecturally hard work — the 19,474→8,135-line decomposition and the by-value→session-collection extraction (937 call sites) — is merged. RadioSession owns `{RadioModel, TciServer, CatPort[8], identity}`; the #2385 crash class is structurally impossible. What remains for multi-radio (#3445) is feature/design work — the `m_sessions.size() > 1` activation, the active-radio UI, the audio-plane decision — which is the team's call, not more refactoring. Refs #3351 #3445 #3384
…dr#3351 / aethersdr#3445) (aethersdr#3544) Eleventh PR of the aethersdr#3351 series — **the first architectural (non-pure-motion) change**. The single-radio assumption identified as "the single biggest assumption to lift" in @nigelfenton's multi-radio brief (aethersdr#3445) stops being load-bearing. ## What changes **New `models/RadioSession.{h,cpp}`** — the aggregate that constitutes "a connected radio": - Owns the `RadioModel` by value (identical construction semantics to the old MainWindow member) - Carries session identity (`sessionId`, `label`) for the future session-switcher UI - Class doc spells out v2+ scope per the aethersdr#3445 Camp B plan: TciServer + CatPort[] ownership, the `MainWindow_Session.cpp` wire bodies becoming session methods, a per-session settings facade (Principle V) **`MainWindow.h`** — the by-value member becomes: ```cpp std::vector<std::unique_ptr<RadioSession>> m_sessions; // at the old member position RadioSession* m_session; // active session (m_sessions.front()) RadioModel& m_radioModel; // alias into the active session's model ``` ## Why the alias bridge **937 `m_radioModel` references across the 11 MainWindow TUs compile completely unchanged.** The single-session runtime path is byte-for-byte identical: one session in the vector, the same RadioModel constructed at the same sequence point. Destruction order is preserved by keeping the vector at the old member's declaration position (verified: no `-Wreorder` for MainWindow). The multi-radio seam is now `m_sessions.size() > 1` + an active-session switch — a contained future PR instead of surgery on a by-value member with 937 call sites. ## What deliberately does NOT change in this PR - TciServer / CatPort ownership (stays in MainWindow; moves in session v2) - The five `wireXxx()` session methods (stay as MainWindow methods in `MainWindow_Session.cpp`; they convert to `RadioSession::` methods when ownership moves) - Any call site — the alias means zero touch across 11 TUs ## Verification - Full build clean — **zero new warnings** (the `-Wreorder` absence on MainWindow is itself the init-order proof) - All 33 ctest suites pass - a11y lint: 0 findings ## QA checklist for the bench Ownership of the central model object moved, so this deserves the full connection-lifecycle regression even though the code paths are identical: - [ ] Connect (LAN + SmartLink if available) / operate / disconnect / reconnect cycle - [ ] Full shutdown while connected — clean exit, no crash-on-close (destruction-order is the one realistic risk class) - [ ] Profile load, slice ops, pan ops — anything that hammers m_radioModel - [ ] Memory recall, spots, meters — secondary model consumers Refs aethersdr#3351 aethersdr#3445 Squashed-from: aethersdr#3544 Co-authored-by: Jeremy [KK7GWY] <105957118+ten9876@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pat Jensen <pat@jensencloud.net>
…#3445) (aethersdr#3555) ## Summary Doc-only. Corrects two items the RadioSession v1 class doc (merged in aethersdr#3544) listed as "v3 scope" that don't survive closer inspection. No code change — captures a finding so neither gets attempted as written. I went to build the **SessionSettings facade** (the documented next step) and the investigation showed it's premature. Rather than build speculative infrastructure with no consumer — the first such thing in the 12-PR series — here's the finding, recorded in the class doc where the next contributor will see it. ## Finding 1: the wire bodies can't move onto RadioSession `wireDiscovery` / `wireRadioModel` / `wirePanLifecycle` are **~100 `connect()` calls with ~76 references to MainWindow's widgets** (applet panel, title bar, pan stack, spectrum widgets, status bar). They're application-layer model→UI glue. Moving them onto a `src/models/` class would invert the dependency — RadioSession would have to `#include` the GUI. They belong in the GUI layer (where they are now), full stop. ## Finding 2: the SessionSettings facade is premature There is no per-radio AppSettings namespace to wrap. Per-radio state today is: | Category | Examples | Where it lives | |---|---|---| | Global | `StationName`, `LastConnectedRadioSerial` | flat AppSettings (genuinely global — station is per-operator, last-serial is a reconnect pointer) | | Radio-side | slice/pan positions, modes | FlexRadio SSDR profiles (`profile global info`) — see aethersdr#3384 | | Already per-radio | band-stack entries | `BandStackSettings::sanitizeSerial()` → `"Radio_<serial>"` keys | **`BandStackSettings` is the one working per-radio-namespacing pattern in the codebase** — it's the template multi-radio should follow when a second session creates a real consumer. A facade built now would wrap nothing and have no caller. ## What this means for the series The aethersdr#3351 refactor is **complete**. The architecturally hard work — the 19,474→8,135-line decomposition and the by-value→session-collection extraction (937 call sites) — is merged. RadioSession owns `{RadioModel, TciServer, CatPort[8], identity}`; the aethersdr#2385 crash class is structurally impossible. What remains for multi-radio (aethersdr#3445) is feature/design work — the `m_sessions.size() > 1` activation, the active-radio UI, the audio-plane decision — which is the team's call, not more refactoring. Refs aethersdr#3351 aethersdr#3445 aethersdr#3384
Summary
Eleventh PR of the #3351 series — the first architectural (non-pure-motion) change. The single-radio assumption identified as "the single biggest assumption to lift" in @nigelfenton's multi-radio brief (#3445) stops being load-bearing.
What changes
New
models/RadioSession.{h,cpp}— the aggregate that constitutes "a connected radio":RadioModelby value (identical construction semantics to the old MainWindow member)sessionId,label) for the future session-switcher UIMainWindow_Session.cppwire bodies becoming session methods, a per-session settings facade (Principle V)MainWindow.h— the by-value member becomes:Why the alias bridge
937
m_radioModelreferences across the 11 MainWindow TUs compile completely unchanged. The single-session runtime path is byte-for-byte identical: one session in the vector, the same RadioModel constructed at the same sequence point. Destruction order is preserved by keeping the vector at the old member's declaration position (verified: no-Wreorderfor MainWindow).The multi-radio seam is now
m_sessions.size() > 1+ an active-session switch — a contained future PR instead of surgery on a by-value member with 937 call sites.What deliberately does NOT change in this PR
wireXxx()session methods (stay as MainWindow methods inMainWindow_Session.cpp; they convert toRadioSession::methods when ownership moves)Verification
-Wreorderabsence on MainWindow is itself the init-order proof)QA checklist for the bench
Ownership of the central model object moved, so this deserves the full connection-lifecycle regression even though the code paths are identical:
Refs #3351 #3445