Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,8 @@ set(CORE_SOURCES
src/core/backends/anan/AnanDiscovery.cpp # aetherd ANAN P2 Phase 1b (P2 discovery -> picker)
src/core/backends/anan/AnanSpectrum.cpp # aetherd ANAN P2 Phase 1b (FFT panadapter, FFTW)
src/core/backends/anan/AnanRxDsp.cpp # aetherd ANAN P2 Phase 1b (IQ -> WdspChannel demod + spectrum)
src/core/backends/anan/AnanDroopCorrection.cpp # ANAN DDC0 droop compensation (pure apply math)
src/core/backends/anan/AnanDroopCalibrator.cpp # live, in-app DDC0 droop calibration sweep
src/core/backends/anan/AnanBackend.cpp # aetherd ANAN P2 Phase 1b (IRadioBackend impl)
src/core/backends/anan/AnanSettings.cpp # owned config object, "Anan" root key (Principle V)
src/core/backends/icom/IcomProtocol.cpp # IcomCIV Phase 0 (RS-BA1 UDP transport)
Expand Down
1 change: 1 addition & 0 deletions docs/architecture/radio-capabilities-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ traps and why the DAX crash guard is deliberately *not* the DAX capability.
| `hasPaCurrentTelemetry` | ❌ | ❌ | ❌ | `MainWindow::applyCapabilitiesToUi` | Calibrated PA drain-current face in Radio Vitals, used only when PA-temperature telemetry is unavailable. Icom: ✅ only for the IC-9700 profile's documented 0–20 A Id calibration. Flex remains ❌ because its PACURRENT meter is known to clip below real full-power draw |
| `hasMainFanTelemetry` | ✅ | ❌ | ❌ | `MainWindow::applyCapabilitiesToUi` | Main Fan gauge in Radio Vitals. All current Icom models are ❌ because the backend does not publish fan-speed telemetry |
| `hostFrequencyCalibration` | ❌ | ✅ | ❌ | `RadioSetupDialog` (Calibration page), `AutomationServer::doFreqCal` | Shows the Calibration page and enables the `freqcal` bridge verb. Means "**the client** owns the frequency-error correction", not "this radio has an error" — every radio does. Flex is ❌ because it calibrates itself (`radio set cal_freq` / `pll_start`), and that surface stays in the Frequency Offset group on the Receive page. HL2 is ✅ because its 76.8 MHz NCO scale is a `localparam` in the bitstream (`radio.v` M2) and no register in the HPSDR map accepts a correction — see `docs/architecture/hl2-frequency-calibration.md` |
| `hostDroopCalibration` | ❌ | ❌ | ❌ | `RadioSetupDialog` (Droop Correction page), `AutomationServer::doDroopCal` | Shows the Droop Correction page and enables the `droopcal` bridge verb. Means "the client has measured and can correct a real DDC edge droop on this radio", not "this radio has no droop" — HL2's own DDC decimation chain looks architecturally similar and has never been characterised, so its ❌ is "not yet measured", not "known absent". ANAN: ✅ — the Saturn FPGA's DDC0 CIC/decimation chain has a real, bench-measured sin(x)/x droop near the edges of the displayed span (`AnanDroopCorrection.h`), corrected in-app via `AnanDroopCalibrator` |
| `persistsMemories` | ✅ | ❌ | ❌ | `LocalMemoryBank` engagement (#4590) | host-side memory bank vs radio-side slots — the bank's ONE shared document lives at `radio_settings (local, '', MemoryBank)` since RFC #4603 PR 6, covered by settings backup/export; legacy `memories.json` is a frozen import source. Icom is profile-gated for IC-705, IC-7300MK2, and IC-9700; unprofiled Icom models remain ❌. |
| `canWriteMemories` | ✅ | ❌ | ❌ | `RadioModel::memoriesWritable`, memory dialog and panadapter memory panel | Separates radio ownership from mutation support. The initial Icom implementation is read-only, so Add, Import, inline edits, and Remove cannot leak vendor-incompatible memory commands. |
| `canApplyMemories` | ✅ | ❌ | ❌ | `RadioModel::tryMemoryCommand` | True means the backend accepts its native memory-apply command. Initial Icom support is ❌ and applies recallable cached fields through the existing neutral slice setters instead of entering vendor Memory mode; split/RPS/DV/DD records are display-only. |
Expand Down
9 changes: 5 additions & 4 deletions docs/automation-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -3762,7 +3762,7 @@ receiver capacity. It never enables transmit and remains available without
The complete registry, generated from the `add(...)` table in `AutomationServer.cpp` by `tools/gen_bridge_docs.py`. CI fails if this drifts from the code.

<!-- BEGIN GENERATED VERB TABLE (tools/gen_bridge_docs.py) -->
<!-- Do not edit by hand — run tools/gen_bridge_docs.py. 71 verbs. -->
<!-- Do not edit by hand — run tools/gen_bridge_docs.py. 72 verbs. -->

| Verb | Aliases | Description |
|---|---|---|
Expand Down Expand Up @@ -3799,13 +3799,14 @@ The complete registry, generated from the `add(...)` table in `AutomationServer.
| `waveform` | — | waveform <start\|stop\|unregister\|resync> [args] — digital-voice service |
| `tune` | — | tune <mhz> [sliceId] — set a slice frequency (default: the active slice) |
| `freqcal` | — | freqcal [get\|set <ppb>\|from_vfo <reference_mhz>\|reset] — manual frequency calibration (radios that cannot calibrate themselves) |
| `droopcal` | — | droopcal [status\|start\|stop\|apply] — ANAN-G2 DDC0 droop calibration sweep (radios with a measured DDC edge droop) |
| `targettune` | — | targettune <mhz> — absolute tune through band-stack preselection |
| `memory` | — | memory activate <index> [panId] — recall a radio memory |
| `cwx` | — | cwx <send\|speed\|stop> [args] — CWX keyer (send is TX-gated) |
| `sim` | — | sim <swr\|dropslice\|stallscope\|disconnect\|malformed\|clear> [arg] — demo fault injection (RFC #4288; only valid when the demo is connected) |
| `record` | — | record <start\|stop\|status\|path\|dir> [args] |
| `testtone` | — | testtone <on\|off> [freqHz levelDb] |
| `pan` | — | pan <create\|add\|remove\|close\|center\|rfgain\|float\|dock> [value] — float/dock drive PanadapterStack's real reparent path (#4864) |
| `pan` | — | pan <create\|add\|remove\|close\|center\|rfgain\|bandwidth\|float\|dock> [value] — float/dock drive PanadapterStack's real reparent path (#4864); bandwidth requests a DDC0 rate change in ksps and returns once ISSUED, not once it lands |
| `workspace` | — | workspace <status\|enable\|disable\|edit\|place\|list\|switch\|create\|bind\|import-floats\|pan-layout\|palette\|window\|move\|add> — the canvas, its workspaces and its extra windows as data; arg shapes in docs/automation-bridge.md (#4887 ph4/ph6/ph7) |
| `layout` | — | layout <rearrange <id>\|get> — splitter layout exerciser |
| `scale` | — | scale [pct] — report/persist the UI scale factor |
Expand All @@ -3820,9 +3821,9 @@ The complete registry, generated from the `add(...)` table in `AutomationServer.
| `audioCapture` | — | audioCapture <start\|stop\|status\|read\|probeNr2Stereo\|probeDspStereo> [args] — RN2 probe accepts rate=Legacy24k\|Native48k output=PreserveRxStereo\|ProcessedMono blocks=<frames,...> |
| `txwaterfall` | — | txwaterfall <on\|off> — show keyed TX in the waterfall |
| `liveness` | — | liveness — per-class data ages and the producer->consumer meter join |
| `civ` | — | civ <send <hex>\|trace [all]\|session\|scheduler> — CI-V inject, frame trace, RS-BA1 lease health, or command-scheduler health (Icom; send is TX-gated) |
| `civ` | — | civ <send <hex>\|trace [all]\|session\|scheduler\|incident> — CI-V inject, frame trace, lease/scheduler health, or last incident (Icom; send is TX-gated) |
| `controls` | — | controls <map\|meters\|scrub [id\|plane]> — the CI-V control and meter registry joined against what is actually wired, and a linkage check that drives every settable control without moving any of them (Icom) |
| `radiocert` | — | radiocert <tune\|rx\|tx\|meters\|all> [freqMhz] — radio bring-up diagnostic, in dependency order (tx/meters key) |
| `radiocert` | — | radiocert <tune\|rx\|tx\|meters\|spectrum\|all> [freqMhz] — radio bring-up diagnostic, in dependency order (tx/meters key); spectrum captures one FFT frame for calibration tooling and does not key |
| `transmit` | — | transmit <rfpower\|tunepower> <0..100> — transmit drive (TX-gated) |
| `key` | — | key <ptt on\|off \| mox> — semantic keying (TX-gated) |
| `station` | — | station <name> — set the GUI-client station name |
Expand Down
96 changes: 92 additions & 4 deletions src/core/AutomationServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
#include <QLocalServer>
#include <QScopeGuard>
#include <QLocalSocket>
#include <QApplication>

Check warning on line 28 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QApplication) — tracked legacy (baseline 20); the count may only shrink
#include <QScreen>
#include <QWidget>

Check warning on line 30 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QWidget) — tracked legacy (baseline 20); the count may only shrink
#include <QMainWindow>

Check warning on line 31 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QMainWindow) — tracked legacy (baseline 20); the count may only shrink
#include <QMenu>

Check warning on line 32 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QMenu) — tracked legacy (baseline 20); the count may only shrink
#include <QMenuBar>

Check warning on line 33 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QMenuBar) — tracked legacy (baseline 20); the count may only shrink
#include <QTabBar>

Check warning on line 34 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QTabBar) — tracked legacy (baseline 20); the count may only shrink
#include <QEnterEvent>
#include <QMouseEvent>
#include <QWheelEvent>
Expand Down Expand Up @@ -67,11 +67,11 @@
#include <utility>

// Best-effort value extraction for common control types.
#include <QAbstractButton>

Check warning on line 70 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QAbstractButton) — tracked legacy (baseline 20); the count may only shrink
#include <QAbstractSlider>

Check warning on line 71 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QAbstractSlider) — tracked legacy (baseline 20); the count may only shrink
#include <QAbstractItemView> // invoke selectRow: QTableWidget/QTreeWidget/QListWidget row select

Check warning on line 72 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QAbstractItemView) — tracked legacy (baseline 20); the count may only shrink
#include <QItemSelectionModel>
#include <QComboBox>

Check warning on line 74 in src/core/AutomationServer.cpp

View workflow job for this annotation

GitHub Actions / Static checks

EB2-known

src/core/AutomationServer.cpp uses QtWidgets (QComboBox) — tracked legacy (baseline 20); the count may only shrink
#include <QLineEdit>
#include <QLabel>
#include <QSpinBox>
Expand Down Expand Up @@ -3187,6 +3187,13 @@
return s.doFreqCal(a.action, a.value);
});

add("droopcal", {},
"droopcal [status|start|stop|apply] — ANAN-G2 DDC0 droop calibration sweep (radios with a measured DDC edge droop)",
parseActionValue,
[](AutomationServer& s, A& a, QLocalSocket*) -> QJsonObject {
return s.doDroopCal(a.action, a.value);
});

add("targettune", {},
"targettune <mhz> — absolute tune through band-stack preselection",
parseValueOnly,
Expand Down Expand Up @@ -3240,8 +3247,9 @@
// rfgain branch already splits the joined value and handles both shapes,
// so the handler was right and only the parser choice was wrong.
add("pan", {},
"pan <create|add|remove|close|center|rfgain|float|dock> [value] — "
"float/dock drive PanadapterStack's real reparent path (#4864)",
"pan <create|add|remove|close|center|rfgain|bandwidth|float|dock> [value] — "
"float/dock drive PanadapterStack's real reparent path (#4864); "
"bandwidth requests a DDC0 rate change in ksps and returns once ISSUED, not once it lands",
parseActionRest,
[](AutomationServer& s, A& a, QLocalSocket*) -> QJsonObject {
if (a.action.isEmpty())
Expand Down Expand Up @@ -3465,7 +3473,8 @@
});

add("radiocert", {},
"radiocert <tune|rx|tx|meters|all> [freqMhz] — radio bring-up diagnostic, in dependency order (tx/meters key)",
"radiocert <tune|rx|tx|meters|spectrum|all> [freqMhz] — radio bring-up diagnostic, in dependency order (tx/meters key); "
"spectrum captures one FFT frame for calibration tooling and does not key",
parseActionValue,
[](AutomationServer& s, A& a, QLocalSocket*) -> QJsonObject {
return s.doRadioCert(a.action, a.value);
Expand Down Expand Up @@ -7657,6 +7666,60 @@
.arg(action));
}

QJsonObject AutomationServer::doDroopCal(const QString& action, const QString& value)
{
Q_UNUSED(value);
if (!m_radioModel)
return err(QStringLiteral("no radio model available"));
if (!m_radioModel->backendCapabilities().hostDroopCalibration) {
return err(QStringLiteral("droopcal: this radio has no measured DDC0 droop to correct"));
}

AnanDroopCalibrator& cal = m_radioModel->droopCalibrator();
const QString verb = action.isEmpty() ? QStringLiteral("status") : action.toLower();

// Direct, synchronous read of the LIVE calibrator -- unlike freqcal's
// state (which only exists inside the backend, reached through
// RadioSettingsScope/loadPpb), this object is directly reachable, so no
// invokeBackendExtension round trip is needed just to report status.
auto report = [&cal] {
return QJsonObject{
{QStringLiteral("ok"), true},
{QStringLiteral("running"), cal.isRunning()},
{QStringLiteral("rateIndex"), cal.rateIndex()},
{QStringLiteral("totalRates"), cal.totalRates()},
{QStringLiteral("hasResult"), cal.hasResult()},
};
};

if (verb == QLatin1String("status"))
return report();

if (verb == QLatin1String("start")) {
// Same refusal freqcal's mutating verbs make: a write (here, the
// eventual Apply) with no radio identity would land on the
// family-wide default row and be inherited by every other radio of
// this family.
if (m_radioModel->settingsScope().radioId().isEmpty()) {
return err(QStringLiteral("droopcal: no radio identity yet — connect the radio "
"before calibrating"));
}
cal.start();
return report();
}
if (verb == QLatin1String("stop")) {
cal.stop();
return report();
}
if (verb == QLatin1String("apply")) {
cal.applyResult();
return report();
}

return err(QStringLiteral("droopcal: unknown action '%1' (status|start|stop|apply)")
.arg(action));
}

// ── VFO tuning (#3646) ──────────────────────────────────────────────────────
// Set a slice's frequency (MHz). The most fundamental control the VfoWidget
// couldn't expose (it's custom-painted). Honors the slice lock guard. An
Expand Down Expand Up @@ -8127,14 +8190,15 @@
else if (phase == QLatin1String("rx")) opts.phase = RadioCertification::Phase::Rx;
else if (phase == QLatin1String("tx")) opts.phase = RadioCertification::Phase::Tx;
else if (phase == QLatin1String("meters")) opts.phase = RadioCertification::Phase::Meters;
else if (phase == QLatin1String("spectrum")) opts.phase = RadioCertification::Phase::Spectrum;
else if (phase == QLatin1String("all")) opts.phase = RadioCertification::Phase::All;
else
// FAIL CLOSED. An unrecognised phase used to leave opts.phase at its
// default of All — the longest run, and the one that keys. So a bare
// `radiocert`, or a typo like `radiocert reciever`, silently started a
// multi-minute transmit sequence nobody asked for.
return err(QStringLiteral(
"radiocert: unknown phase '%1' — expected tune|rx|tx|meters|all. "
"radiocert: unknown phase '%1' — expected tune|rx|tx|meters|spectrum|all. "
"Refusing to default to 'all', which keys the transmitter")
.arg(phaseArg.trimmed()));

Expand Down Expand Up @@ -10142,6 +10206,30 @@
{QStringLiteral("gain"), gain}, {QStringLiteral("requested"), true}};
}

if (action == QLatin1String("bandwidth") || action == QLatin1String("bw")) {
// `pan bandwidth <ksps>`. Added for the same "hidden control needs a
// direct verb" reason as rfgain above, plus a bring-up-tooling need:
// tools/anan_droop_calibration.py drives a full sweep across a
// DDC-stepped radio's fixed rate set (ANAN-G2: 48/96/192/384/768/
// 1536 ksps) and there was no bridge verb to select a rate directly
// — only the zoom buttons, which snap to the nearest of a few fixed
// steps and give no way to target a SPECIFIC rate by value.
//
// FIRE-AND-FORGET: on a DDC-stepped radio a rate change can take up
// to ~a minute cold (AnanRxDsp::buildChannel(), a never-before-used
// rate/block-size combination in this process), so this returns once
// the request is ISSUED, not once it lands. Poll
// `get_state model=pan property=bandwidthMhz` or use `wait_for` to
// know when it actually took.
bool okKsps = false;
const double ksps = arg.trimmed().toDouble(&okKsps);
if (!okKsps || ksps <= 0.0)
return err(QStringLiteral("pan bandwidth requires a positive rate in ksps"));
radio->setPanBandwidth(ksps / 1000.0);
return QJsonObject{{QStringLiteral("ok"), true}, {QStringLiteral("pan"), QStringLiteral("bandwidth")},
{QStringLiteral("requestedKsps"), ksps}, {QStringLiteral("requested"), true}};
}

if (action == QLatin1String("float") || action == QLatin1String("dock")) {
// `pan float <panId|index|active>` / `pan dock …` — the reparent +
// GPU re-initialize path (#2495/#4319/#4617) made drivable headlessly
Expand Down
1 change: 1 addition & 0 deletions src/core/AutomationServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ private slots:
// RadioCapabilities::hostFrequencyCalibration, so it refuses on a radio that
// calibrates itself rather than silently storing a number nothing applies.
QJsonObject doFreqCal(const QString& action, const QString& value);
QJsonObject doDroopCal(const QString& action, const QString& value);
QJsonObject doTargetTune(const QString& value);
QJsonObject doMemory(const QString& action, const QString& arg);
// Demo fault injection (RFC #4288 #4): route a fault to backend->
Expand Down
Loading
Loading