Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture/aetherd-touchpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine
| `core/TailnetShimTokenStore.h` | 1 | peripheral(tailnet-shim) — OS-keychain storage of the tailnet shim's admin token, keyed by the Flex chassis serial (RFC #6271). Not SmartSDR wire: AetherSDR's own HTTP API to a separate service (the shim) that happens to run in the radio's container runtime, so a direct-transport peripheral, NOT behind the IRadioBackend radio seam (same shape as core/PgxlConnection.h). | unconverted |
| `core/TciServer.h` | 3 | mixed(flex) — TCI WebSocket server for WSJT-X et al: protocol surface is canonical radio state, but audio/IQ rides Flex DAX | unconverted |
| `core/TgxlConnection.h` | 3 | peripheral(4o3a) — Direct TCP client for the 4O3A Tuner Genius XL (port 9010, relay/autotune), reverse-engineered from the 4O3A management app — a standalone accessory transport, not SmartSDR. Not radio-family wire; a peripheral accessory, NOT behind the IRadioBackend radio seam (reclassified from vendor(flex), #4087 follow-up). | unconverted |
| `core/ThemeManager.h` | 172 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted |
| `core/ThemeManager.h` | 173 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted |
| `core/ThreadCpuRing.h` | 2 | ui-support — Short host-thread CPU history used by Runtime Monitor peak and sparkline presentation. Diagnostic UI support, not radio state. | unconverted |
| `core/TimeFrameVoter.h` | 1 | universal — Shared AetherClock time-frame types plus confidence-weighted cross-frame bit voting over a sliding window. Map-agnostic pure DSP/logic — no Qt, no GUI, no vendor ties. | unconverted |
| `core/TxKeyingMarker.h` | 15 | ui-support — QWidget property marker guarding TX-keying controls from the automation bridge; GUI-shell plumbing, no radio state. | unconverted |
Expand Down
2 changes: 1 addition & 1 deletion docs/style/aethersdr-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ windows" lists what it keeps (the `FramelessMoveHelper` drag, Escape and
Which windows the canon covers: short-lived, informational windows (About,
Remote Access, and the candidates listed on #6239, once the maintainer picks
them), and the tool windows the maintainer has moved to it (Waveforms,
Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio). Other tool and workspace dialogs keep
Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio, AGC-T Calibration). Other tool and workspace dialogs keep
`PersistentDialog` and `FramelessWindowTitleBar` as `docs/agents/gui.md`
describes. For windows the canon covers,
`FramelessWindowTitleBar` is deprecated: those dialogs move to
Expand Down
2 changes: 1 addition & 1 deletion docs/style/dialog-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ replace these with a single `QSet<QPointer<PersistentDialog>>` walk.
A window built on `CanonWindow` (`src/gui/CanonWindow.{h,cpp}`, RFC #6226)
deliberately opts out of frameless chrome integration, and by default of
geometry persistence too. About AetherSDR, Remote Access (Tailscale),
Waveforms, Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio (`ConnectionPanel`) and the AetherRX / AetherTX Settings (profile library) dialogs are built on it.
Waveforms, Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio (`ConnectionPanel`), AGC-T Calibration and the AetherRX / AetherTX Settings (profile library) dialogs are built on it.

| Concern | `CanonWindow` | Why |
|---|---|---|
Expand Down
6 changes: 3 additions & 3 deletions docs/style/theme-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ a widget subtree (for example, the `applet/tx` scope turns the
checked-toggle accent red where the root theme has it blue — scope
paths are slash-joined, and a widget opts in by carrying the path in
its `themeContainer` property). The `canon` scope does the same for
whole windows: AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS and Connect to
Radio sit under `canon/aetherRx`, `canon/aetherTx`, `canon/aetherModem`,
`canon/aetherMap`, `canon/aetherGps` and `canon/connection`, and it maps the base tokens onto the canon palette
whole windows: AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to
Radio and AGC-T Calibration sit under `canon/aetherRx`, `canon/aetherTx`, `canon/aetherModem`,
`canon/aetherMap`, `canon/aetherGps`, `canon/connection` and `canon/agcCalibration`, and it maps the base tokens onto the canon palette
there (see [`aethersdr-style-guide.md`](aethersdr-style-guide.md)). The
widget-aware accessors below resolve scope automatically — prefer them
inside applets and anything painted inside a scoped window.
Expand Down
2 changes: 2 additions & 0 deletions docs/user/docs/rx-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ The **SQL** button cycles through three states:
**AGC-T calibration:** right-click the AGC-T slider, or use
**Tools → Calibrate AGC-T...**, to calibrate the threshold against the
measured noise floor on the active slice. It does not transmit.
Close the window with the corner ✕, Esc or Ctrl+W; closing it during
an Auto Sweep stops the sweep and puts back the AGC-T it started from.

On the Hermes-Lite 2, AGC runs on the computer: with AGC **Off**, the AGC-T
slider sets a fixed gain (4–64 dB, default 10 dB) remembered per receiver, and
Expand Down
129 changes: 114 additions & 15 deletions src/gui/AgcCalibrationDialog.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "gui/AgcCalibrationDialog.h"

#include "gui/CanonIndicators.h"
#include "core/AudioEngine.h"
#include "core/ThemeManager.h"
#include "models/MeterModel.h"
#include "models/RadioModel.h"
#include "models/SliceModel.h"
Expand All @@ -21,6 +23,66 @@

namespace AetherSDR {

namespace {

// The style guide's canon vocabulary (RFC #6226), as the RX and TX Settings
// dialogs and AetherGPS use it: transparent over CanonWindow's ground, the
// mode line in ink, the hint muted, buttons and the target field on control.
constexpr const char* kAgcCalibrationStyle = R"(
QWidget {
color: {{color.canon.inkSoft}};
background: transparent;
}
QLabel {
background: transparent;
}
QLabel[agcRole='mode'] {
color: {{color.canon.ink}};
font-weight: 600;
}
QLabel[agcRole='result'] {
color: {{color.canon.cyan}};
font-weight: 600;
}
QLabel[agcRole='hint'] {
color: {{color.canon.muted}};
}
QPushButton {
color: {{color.canon.cyan}};
background: {{color.canon.control}};
border: 1px solid {{color.canon.lineHi}};
border-radius: 4px;
padding: 5px 12px;
font-weight: 600;
}
QPushButton:hover {
background: {{color.canon.nested}};
color: {{color.canon.aqua}};
}
QPushButton:focus {
border-color: {{color.canon.aqua}};
}
QPushButton:disabled {
color: {{color.canon.muted}};
border-color: {{color.canon.line}};
background: transparent;
}
QAbstractSpinBox {
color: {{color.canon.ink}};
background: {{color.canon.control}};
border: 1px solid {{color.canon.lineHi}};
border-radius: 4px;
padding: 3px 6px;
selection-background-color: {{color.canon.cyan}};
selection-color: {{color.canon.onAccent}};
}
QAbstractSpinBox:focus {
border-color: {{color.canon.aqua}};
}
)";

} // namespace

// ===========================================================================
// AgcCurveWidget
// ===========================================================================
Expand All @@ -29,7 +91,8 @@ AgcCurveWidget::AgcCurveWidget(QWidget* parent)
: QWidget(parent)
{
setMinimumSize(360, 200);
setAttribute(Qt::WA_StyledBackground, true);
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
this, qOverload<>(&QWidget::update));
}

void AgcCurveWidget::setCurve(const QVector<AgcTCalibrator::Point>& pts)
Expand Down Expand Up @@ -80,13 +143,21 @@ void AgcCurveWidget::paintEvent(QPaintEvent*)
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);

const ThemeManager& tm = ThemeManager::instance();
const auto canon = [&](const char* name) {
return tm.color(this, QStringLiteral("color.canon.") + QLatin1String(name));
};

const QRectF full = rect();
const qreal padL = 8.0, padR = 8.0, padT = 8.0, padB = 18.0;
const QRectF plot(full.left() + padL, full.top() + padT,
full.width() - padL - padR, full.height() - padT - padB);

p.fillRect(full, QColor(18, 20, 24));
p.setPen(QPen(QColor(70, 76, 86), 1.0));
p.setPen(QPen(canon("line"), 1.0));
p.setBrush(canon("nested"));
p.drawRoundedRect(full.adjusted(0.5, 0.5, -0.5, -0.5), 7.0, 7.0);
p.setPen(QPen(canon("grid"), 1.0));
p.setBrush(Qt::NoBrush);
p.drawRect(plot);

auto xForValue = [&](int v) {
Expand All @@ -113,7 +184,7 @@ void AgcCurveWidget::paintEvent(QPaintEvent*)
// AGC-off target line.
if (m_strategy == AgcTCalibrator::Strategy::TargetLevel
&& m_targetDb >= yMin && m_targetDb <= yMax) {
p.setPen(QPen(QColor(90, 150, 220), 1.0, Qt::DashLine));
p.setPen(QPen(canon("aqua"), 1.0, Qt::DashLine));
const qreal y = yForDb(m_targetDb);
p.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y));
}
Expand All @@ -125,21 +196,22 @@ void AgcCurveWidget::paintEvent(QPaintEvent*)
for (int i = 1; i < m_pts.size(); ++i) {
path.lineTo(xForValue(m_pts[i].value), yForDb(m_pts[i].rmsDb));
}
p.setPen(QPen(QColor(120, 210, 140), 2.0));
p.setPen(QPen(canon("cyan"), 2.0));
p.drawPath(path);
}
p.setPen(Qt::NoPen);
p.setBrush(QColor(150, 230, 170));
p.setBrush(canon("aqua"));
for (const auto& pt : m_pts) {
p.drawEllipse(QPointF(xForValue(pt.value), yForDb(pt.rmsDb)), 2.0, 2.0);
}

// Recommended / knee marker.
if (m_recommended >= 0) {
const qreal x = xForValue(m_recommended);
p.setPen(QPen(QColor(245, 200, 90), 2.0));
const QColor knee = canon("sparkGold");
p.setPen(QPen(knee, 2.0));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
p.setPen(QColor(245, 200, 90));
p.setPen(knee);
p.drawText(QPointF(std::min(x + 4, plot.right() - 70), plot.top() + 12),
m_recIsKnee ? QStringLiteral("knee %1").arg(m_recommended)
: QStringLiteral("target %1").arg(m_recommended));
Expand All @@ -148,12 +220,12 @@ void AgcCurveWidget::paintEvent(QPaintEvent*)
// Current value marker.
if (m_current >= 0) {
const qreal x = xForValue(m_current);
p.setPen(QPen(QColor(220, 220, 230), 1.0, Qt::DotLine));
p.setPen(QPen(canon("ink"), 1.0, Qt::DotLine));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
}

// X axis labels.
p.setPen(QColor(150, 156, 166));
p.setPen(canon("muted"));
p.drawText(QPointF(plot.left(), plot.bottom() + 14), QStringLiteral("0"));
p.drawText(QPointF(plot.right() - 24, plot.bottom() + 14), QStringLiteral("100"));
p.drawText(QPointF(plot.center().x() - 30, plot.bottom() + 14),
Expand All @@ -168,26 +240,36 @@ AgcCalibrationDialog::AgcCalibrationDialog(RadioModel* radio,
AudioEngine* audio,
NoiseFloorFn noiseFloorFn,
QWidget* parent)
: PersistentDialog(QStringLiteral("AGC-T Calibration"),
QStringLiteral("AgcCalibrationDialogGeometry"), parent),
: CanonWindow(QStringLiteral("AGC-T Calibration"), parent),
m_radio(radio), m_audio(audio), m_noiseFloorFn(std::move(noiseFloorFn))
{
setMinimumSize(420, 360);
resize(460, 420);
setObjectName(QStringLiteral("agcCalibrationDialog"));
theme::setContainer(this, QStringLiteral("canon/agcCalibration"));
// The canon header adds about 40 px to the old 460x420.
setMinimumSize(420, 400);
resize(460, 460);
applyCanonSheet(bodyWidget(), [] {
return QString::fromLatin1(kAgcCalibrationStyle);
});

auto* body = new QVBoxLayout(bodyWidget());
body->setContentsMargins(10, 10, 10, 10);
body->setContentsMargins(14, 12, 14, 14);
body->setSpacing(8);
body->addWidget(makeCanonHeader(QStringLiteral("AGC-T Calibration")));

m_modeLabel = new QLabel(this);
m_modeLabel->setProperty("agcRole", QStringLiteral("mode"));
m_modeLabel->setWordWrap(true);
m_quietLabel = new QLabel(this);
m_quietLabel->setWordWrap(true);
body->addWidget(m_modeLabel);
body->addWidget(m_quietLabel);

m_curve = new AgcCurveWidget(this);
body->addWidget(m_curve, 1);

m_recLabel = new QLabel(this);
m_recLabel->setProperty("agcRole", QStringLiteral("result"));
body->addWidget(m_recLabel);

// AGC-off advanced: comfortable-noise target.
Expand Down Expand Up @@ -219,6 +301,7 @@ AgcCalibrationDialog::AgcCalibrationDialog(RadioModel* radio,
m_hintLabel = new QLabel(
QStringLiteral("Tune to a clear spot, then Auto Sweep — or move the AGC-T "
"slider and watch where the noise bends."), this);
m_hintLabel->setProperty("agcRole", QStringLiteral("hint"));
m_hintLabel->setWordWrap(true);
body->addWidget(m_hintLabel);

Expand Down Expand Up @@ -252,6 +335,22 @@ AgcCalibrationDialog::AgcCalibrationDialog(RadioModel* radio,
updateModeUi();
}

AgcCalibrationDialog::~AgcCalibrationDialog()
{
// Destroyed without a close (the app quitting): put the AGC-T back too.
if (m_engine.isRunning()) {
m_engine.stop();
}
}

void AgcCalibrationDialog::done(int result)
{
if (m_engine.isRunning()) {
m_engine.stop(); // restores the original value
}
CanonWindow::done(result);
}

void AgcCalibrationDialog::setSlice(SliceModel* slice)
{
if (m_slice == slice) {
Expand Down
22 changes: 12 additions & 10 deletions src/gui/AgcCalibrationDialog.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once

#include "gui/PersistentDialog.h"
#include "gui/CanonWindow.h"
#include "core/AgcTCalibrator.h"

#include <QPointer>
Expand All @@ -23,10 +23,7 @@ class AudioEngine;
// AgcCurveWidget — lightweight 2D plot of post-AGC audio noise (y, dB) versus
// the AGC-T value (x, 0..100). Markers: current value, detected knee /
// recommended value, and (AGC-off) the comfortable-noise target line.
//
// Visual styling here is deliberately plain — final visual design is the
// maintainer's call (CLAUDE.md). This draws the data correctly; colors/fonts
// are placeholders.
// Painted in the canon palette (color.canon.*), so it follows the theme.
class AgcCurveWidget : public QWidget {
Q_OBJECT
public:
Expand All @@ -51,11 +48,11 @@ class AgcCurveWidget : public QWidget {
AgcTCalibrator::Strategy m_strategy{AgcTCalibrator::Strategy::Knee};
};

// AgcCalibrationDialog — the calibration surface. Auto-sweep first (one click),
// with the live curve always visible so the operator can fine-tune by hand.
// Operates on the active slice; the radio stays authoritative (we only set the
// value through SliceModel).
class AgcCalibrationDialog : public PersistentDialog {
// AgcCalibrationDialog — the calibration surface, a canon dialog. Auto-sweep
// first (one click), with the live curve always visible so the operator can
// fine-tune by hand. Operates on the active slice; the radio stays
// authoritative (we only set the value through SliceModel).
class AgcCalibrationDialog : public CanonWindow {
Q_OBJECT
public:
using NoiseFloorFn = std::function<float(SliceModel*)>;
Expand All @@ -64,6 +61,11 @@ class AgcCalibrationDialog : public PersistentDialog {
AudioEngine* audio,
NoiseFloorFn noiseFloorFn,
QWidget* parent = nullptr);
~AgcCalibrationDialog() override;

// Every close path (the close button, Escape, Ctrl+W) ends here: a running
// sweep stops and the AGC-T it found goes back, as Stop does.
void done(int result) override;

// Point the panel at a slice (called on show and on active-slice change).
void setSlice(SliceModel* slice);
Expand Down
13 changes: 10 additions & 3 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4733,10 +4733,17 @@ void MainWindow::showAgcCalibrationDialog(int sliceId)
SpectrumWidget* sw = s ? spectrumForSlice(s) : nullptr;
return sw ? sw->noiseFloorDbm() : std::numeric_limits<float>::quiet_NaN();
};
showOrRaisePersistent(m_agcCalibrationDialog, &m_radioModel, m_audio, floorFn);
if (m_agcCalibrationDialog) {
m_agcCalibrationDialog->setSlice(slice);
// A canon dialog, so not showOrRaisePersistent(): that applies View →
// Frameless Window, which a CanonWindow does not follow.
if (!m_agcCalibrationDialog) {
auto* dlg = new AgcCalibrationDialog(&m_radioModel, m_audio, floorFn, this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
m_agcCalibrationDialog = dlg;
}
m_agcCalibrationDialog->setSlice(slice);
m_agcCalibrationDialog->show();
m_agcCalibrationDialog->raise();
m_agcCalibrationDialog->activateWindow();
}

// AX.25 dialog + KISS TNC startup live in MainWindow_DigitalModes.cpp (#3351 Phase 1e).
Expand Down
Loading