Skip to content

ATU pre-tune: add Auto-mode safety nets — class filter, point cap, power warning, audible cue (#2649). Principle IV.#3050

Merged
ten9876 merged 1 commit into
mainfrom
aetherclaude/issue-2649
May 24, 2026
Merged

ATU pre-tune: add Auto-mode safety nets — class filter, point cap, power warning, audible cue (#2649). Principle IV.#3050
ten9876 merged 1 commit into
mainfrom
aetherclaude/issue-2649

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #2649

What was changed

ATU pre-tune: add Auto-mode safety nets — class filter, point cap, power warning, audible cue (#2649). Principle IV.

Files modified

  • resources/bandplans/arrl-us.json
  • src/gui/AtuPreTuneDialog.cpp
  • src/gui/AtuPreTuneDialog.h
  • src/models/BandPlanManager.cpp
  • src/models/BandPlanManager.h
 src/gui/AtuPreTuneDialog.cpp     | 145 ++++++++++++++++++++++++++++++++++++++-
 src/gui/AtuPreTuneDialog.h       |   4 ++
 src/models/BandPlanManager.cpp   |  34 ++++++++-
 src/models/BandPlanManager.h     |  20 ++++++
 5 files changed, 203 insertions(+), 5 deletions(-)

Generated by AetherClaude (automated agent for AetherSDR)


🤖 aethersdr-agent · cost: $28.0998 · model: claude-opus-4-7

…wer warning, audible cue (#2649). Principle IV.

Lands the four enhancements approved for the first batch on issue #2649:

- #4 Max-points soft cap (kMaxPointsSoftCap = 100): warns the operator
  before kicking off a sweep large enough to keep the radio transmitting
  unattended for tens of minutes. Catches the "select all bands on a wide
  regional plan" footgun.
- #5 Power-level guardrail (kAutoModeTunePowerWarnW = 20): warns — never
  refuses — when tune power exceeds 20 W on entering Auto mode. Some amps
  legitimately need higher drive (Principle XIII: the operator outranks the
  agent); the gate exists only to force a deliberate choice.
- #7 Audible cue: QApplication::beep() on per-point-timeout and on the
  consecutive-fail-bypass hard stop, so the operator notices terminal-fail
  events even when not watching the dialog.
- #8 License-class filter: data-driven via a new optional license_classes
  block in the bandplan JSON. ARRL US declares Technician/General/Extra;
  IARU/RAC/SSA plans leave it absent, so the combobox stays hidden and the
  feature is inert for those operators. When a class is selected, segments
  whose non-empty license field doesn't include the code are dropped BEFORE
  the contiguous-region merge, so an allowed CW segment adjacent to a
  disallowed phone segment stays as two separate regions instead of merging
  and reintroducing the disallowed centers. Empty-license segments (BCN
  markers, general-purpose) always pass through. Stored class is preserved
  across plan switches; unknown stored codes default to "no filter" rather
  than auto-picking the first entry.

Principle IV (Region-Aware Data Comes From BandPlanManager): the
license-class filter is the principle's natural extension — what segments
are reachable for a given operator is region- AND class-dependent, and the
filter routes through BandPlanManager::contiguousRegionsForBand() (new
three-arg overload) rather than hardcoding US-centric class tables in C++.
The new license_classes JSON block lets future regional plans add their
own class structure with zero C++ changes.

Principle V (nested JSON): the operator's class is stored as a single new
top-level key OperatorLicenseClass per the maintainer's call-out that no
Operator block exists today to nest under; future Operator block (callsign
+ QTH + class) can fold this key in.

Blast radius: risk_score=0.194, 4 high-risk affected (top: MainWindow,
buildMenuBar, buildUI). All structural callers reach BandPlanManager via
its header; changes are purely additive (new accessor, new overload, new
optional JSON field) and the two-arg overload now delegates to the
three-arg form with empty allowedClass — MainWindow::startSwrSweep
behavior is byte-identical. No new test-target link dependencies introduced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the well-scoped safety-nets PR. Read through all four guards — they hang together cleanly.

Looks good

  • BandPlanManager::contiguousRegionsForBand(..., allowedClass) filters before the sort+merge, so an allowed segment adjacent to a disallowed one correctly stays separate (no reintroduced-center bug). The empty-license passthrough for BCN/general segments matches the existing plan data.
  • Backwards-compatible overload + opt-in allowedClass keeps non-class-aware plans on the old behavior.
  • Class-filter UI is hidden entirely when the active plan has no license_classes block, and populateLicenseClassRow() defaults to "no filter" (not auto-pick) when the stored class isn't in the current plan — that's the right safety floor.
  • The point-cap and tune-power gates are warn-not-refuse with QMessageBox::Cancel as the default button, which matches Principle XIII for the power gate.
  • Null/visibility guards on m_radio, m_bandPlan, m_licenseClassRow, and m_licenseClassCombo look correct throughout. kSecondsPerPointEstimate = 15 already existed in the file (verified), so the time estimate math is sound.
  • AppSettings usage is consistent with the rest of the repo (setValue + save(); no QSettings).

Minor observations (non-blocking)

  • populateLicenseClassRow() is only called once in the ctor. If the active band plan changes while the dialog is open, the license-class row won't refresh. Probably fine since the dialog is typically short-lived, and it matches the existing pattern (no planChanged wiring elsewhere in the dialog) — just flagging in case a future plan-switch entry-point opens this up.
  • selectedLicenseClass() correctly returns empty when the row is hidden, so a class stored in AppSettings from a previous class-aware plan won't silently filter a non-class-aware plan. Nice.

No bugs, no scope creep, ship it.


🤖 aethersdr-agent · cost: $9.2487 · model: claude-opus-4-7

@ten9876 ten9876 merged commit cd27c6d into main May 24, 2026
5 checks passed
@ten9876 ten9876 deleted the aetherclaude/issue-2649 branch May 24, 2026 15:49
aethersdr-agent Bot added a commit that referenced this pull request May 24, 2026
…. Principle XIII.

After 3 consecutive TUNE_FAIL_BYPASS results, advance m_currentIndex past
the remaining points on the dying band rather than aborting the whole
sweep. Multi-band runs keep producing useful results when only one band's
antenna is bad (e.g. broken feedline on 80m while 40m/20m/15m are fine).

- Last-band / single-band failure: the skip loop body never executes,
  beginNextPoint() increments past m_points.size() and falls through to
  finishSweep() — matches the prior terminal-abort behaviour for those
  cases (covers the test-plan edge cases).
- First point of a new band fails-bypasses: counter is reset to 0 before
  the recursion, so three fresh fails are required to trigger another
  per-band skip.
- m_skipCount is incremented by (current - skipStart), not (+ 1): the
  triggering point already incremented m_failCount, so only the
  remaining points on the dying band count as skips.
- Audible cue (QApplication::beep) preserved from PR #3050 item #7.
- Message includes the next band name when one exists so the operator
  sees what is about to run.

Principle XIII (The Operator Outranks Every Agent): the operator with
one bad antenna outranks a defensive over-abort — give them the useful
remainder of the sweep rather than the binary "every band aborted"
verdict the previous safety net imposed.

Blast radius: risk_score=0.0504, 5 high-risk affected (top:
RadioModel::transmitModel, RadioModel::isConnected, SliceModel::sliceId).
All five are downstream-transitive callees of onAtuStateChanged whose
behaviour and call sites are unchanged by this patch — the edit
modifies only the consecutive-fail-bypass branch's control flow.
aethersdr-agent Bot added a commit that referenced this pull request May 25, 2026
… Principle IV.

Populates rac-canada.json with the license_classes block introduced
in #3050 using Canada's ISED RIC-3 / RBR-4 frequency authorizations:

- B  = Basic              (VHF/UHF only, >= 30 MHz)
- BH = Basic with Honours (all amateur bands)
- A  = Advanced           (all amateur bands)

All HF segments (1.8-29.7 MHz) are annotated "BH,A"; all segments at
50 MHz and above are annotated "B,BH,A"; receive-only beacon markers
keep `license: ""` so they pass through the filter unchanged. The
description is updated to make clear the class overlay is sourced
from ISED (the federal regulator), not from the RAC band-plan
document, which itself does not define class-based sub-segments.

For SSA Sweden, the description is updated to document why no
license_classes block is added: PTSFS 2025:1 treats Swedish amateurs
as a single qualification tier (the older CEPT Class 1 / Class 2
split was retired), so per-segment class filtering would be
misleading. The ATU pre-tune dialog hides the class combobox when no
license_classes block is present (AtuPreTuneDialog::populateLicenseClassRow,
#2649), so this is correct no-op behavior — the description records
the reasoning so a future contributor doesn't retry the addition.

IARU Region 1/2/3 plans are intentionally left unchanged: those
documents are mode-allocation-by-band, not class-allocation, and
operators wanting class filtering should select their national plan
(arrl-us, rac-canada) as the active plan.

Pure data change — no .cpp files modified, no test-target link
surface affected, no codegraph impact analysis applicable (the
dialog code path is data-driven and already in place from #3050).

Principle IV. Region-Aware Data Comes From BandPlanManager: the
class overlay is added to the per-region JSON files that drive
BandPlanManager, keeping the regional-data source authoritative
rather than introducing a parallel class-rules table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
aethersdr-agent Bot added a commit that referenced this pull request May 25, 2026
…gionsForBand (#3060). Principle IV.

PR #3050 (closing #2649) added a license-class-aware overload of
BandPlanManager::contiguousRegionsForBand that filters segments by the
operator's class before merging contiguous regions. The filter-BEFORE-merge
ordering is load-bearing: if an allowed segment is adjacent to a disallowed
one, they must NOT merge — otherwise the merged region's center could fall
on a disallowed frequency. The implementation comment calls this out, but
no test pinned the invariant. A future refactor (STL-algorithm
consolidation, parallelisation of the segment loop) could silently
re-introduce the bug.

This commit adds tests/band_plan_license_filter_test.cpp covering the five
scenarios enumerated in the issue:

  1. Empty class → no filter (three-arg with "" == two-arg).
  2. Empty-license segments (BCN markers) always pass.
  3. Class filter drops non-matching segments [E]/[G]/[E,G] under T/G/E.
  4. Adjacent allowed/disallowed segments stay separate (THE invariant).
  5. Trailing-whitespace allowedClass is trimmed; "GE" doesn't match
     "G,E" substring-style; lowercase doesn't match uppercase.

To make scenario 5 pass and to keep the comparison symmetric, the
allowedClass argument is now .trimmed() at the top of the filter — the
segment-side code was already trimmed per element. This is a one-line
forgiveness fix; existing callers that pass already-trimmed strings
(MainWindow::startSwrSweep, AtuPreTuneDialog::populateBands) are
unaffected.

A small public test hook BandPlanManager::setSegmentsForTest is added
so the test can inject synthetic segments without going through resource
loading or AppSettings. Matches the *ForTest naming convention already
used in PerfTelemetry::resetForTest and
AetherAx25LibmodemShim::processRecoveredBitsForTest.

Honors Principle IV (region-aware data comes from BandPlanManager, not
BandDefs.h) by hardening the regression-guard around the manager's
license-class filter — exactly the surface a refactor cleanup pass would
be tempted to consolidate.

Blast radius: risk_score=0.198, 4 high-risk affected (top: MainWindow::MainWindow, MainWindow::buildMenuBar, MainWindow::buildUI). Header change is purely additive (new public inline setSegmentsForTest) — no existing API touched; the high-risk callers don't reference the new symbol and aren't behaviorally affected, only a recompile cascade. The .cpp change adds .trimmed() to one argument — forgiveness-only (strings that previously failed strict comparison may now match); no caller currently passes whitespace.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…wer warning, audible cue (aethersdr#2649). Principle IV. (aethersdr#3050)

## Summary

Fixes aethersdr#2649

### What was changed

ATU pre-tune: add Auto-mode safety nets — class filter, point cap, power
warning, audible cue (aethersdr#2649). Principle IV.

### Files modified

- `resources/bandplans/arrl-us.json`
- `src/gui/AtuPreTuneDialog.cpp`
- `src/gui/AtuPreTuneDialog.h`
- `src/models/BandPlanManager.cpp`
- `src/models/BandPlanManager.h`

```
 src/gui/AtuPreTuneDialog.cpp     | 145 ++++++++++++++++++++++++++++++++++++++-
 src/gui/AtuPreTuneDialog.h       |   4 ++
 src/models/BandPlanManager.cpp   |  34 ++++++++-
 src/models/BandPlanManager.h     |  20 ++++++
 5 files changed, 203 insertions(+), 5 deletions(-)
```

---
Generated by AetherClaude (automated agent for AetherSDR)

---
<sub>🤖 aethersdr-agent · cost: $28.0998 · model: claude-opus-4-7</sub>

Co-authored-by: aethersdr-agent[bot] <273844287+aethersdr-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

ATU Pre-Tune Auto mode: review unattended-TX safety nets

1 participant