Skip to content

feat(arch): introduce RadioSession — the per-radio aggregate (#3351 / #3445)#3544

Merged
jensenpat merged 2 commits into
mainfrom
refactor/3351-radiosession
Jun 13, 2026
Merged

feat(arch): introduce RadioSession — the per-radio aggregate (#3351 / #3445)#3544
jensenpat merged 2 commits into
mainfrom
refactor/3351-radiosession

Conversation

@ten9876

@ten9876 ten9876 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

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":

  • 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 Design discussion: single AetherSDR instance, two radios — brief from @nigelfenton #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:

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 #3351 #3445

…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.
@ten9876 ten9876 requested review from a team as code owners June 12, 2026 23:33
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 jensenpat 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.

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.

@jensenpat jensenpat merged commit b893ca5 into main Jun 13, 2026
6 checks passed
@jensenpat jensenpat deleted the refactor/3351-radiosession branch June 13, 2026 00:29

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

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_sessionsm_sessionm_radioModel) matches the declaration order in MainWindow.h, and m_session is fully initialized before being dereferenced for the reference bind. No -Wreorder exposure, as stated.
  • Construction semantics are genuinely identical. The old ctor default-constructed RadioModel m_radioModel at that member position; the new RadioSession default-constructs its by-value RadioModel at the same sequence point, on the same (GUI) thread, with no QObject parent — same as before.
  • The reference alias is safe as wired. unique_ptr gives the RadioSession a stable address, so vector reallocation can't dangle m_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:

  1. 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_radioModel must 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 future m_sessions.clear()/erase() of the front session dangles both m_session and m_radioModel — worth keeping that invariant ("front session lives as long as MainWindow") stated where the vector is declared.
  2. Tiny: setSessionId(0) in makeInitialSessions() restates the in-class default, and the m_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

ten9876 added a commit that referenced this pull request Jun 13, 2026
## 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
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…#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
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.

2 participants