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 @@ -167,7 +167,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine
| `core/aprs/AprsStationList.h` | 1 | universal — Heard-APRS-station model (calls/positions/last-heard); radio-agnostic spot-like data. | unconverted |
| `core/backends/ConnectionSharingPolicy.h` | 2 | universal — Fail-closed discovery-time policy for whether a busy radio family permits another client. Canonical cross-family connection safety pending capability descriptors. | unconverted |
| `core/backends/IRadioBackend.h` | 1 | universal — THE radio seam (RFC §5.5) — the canonical intent verbs, typed deltas and normalized signals every family implements. Universal by definition: the UI reaching this header is the seam working as designed, not coupling. Everything below it in core/backends/<family>/ is family-private. | unconverted |
| `core/backends/RadioCapabilities.h` | 2 | universal — Backend-neutral capability descriptor consumed above the radio seam. Universal by definition; family implementations populate it. | unconverted |
| `core/backends/RadioCapabilities.h` | 5 | universal — Backend-neutral capability descriptor consumed above the radio seam. Universal by definition; family implementations populate it. | unconverted |
| `core/backends/anan/AnanDiscovery.h` | 2 | vendor(anan) — openHPSDR Protocol 2 discovery and ANAN-G2 identity handling. Family-specific discovery belongs below the ANAN backend seam. | unconverted |
| `core/backends/anan/AnanSettings.h` | 1 | ui-support — Client-side connection and ADC configuration for the ANAN backend. Persisted setup plumbing, not live radio state. | unconverted |
| `core/backends/anan/P2Protocol.h` | 1 | vendor(anan) — openHPSDR Ethernet Protocol 2 wire primitives for the ANAN-G2 backend. Family protocol code belongs below the radio seam. | unconverted |
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 @@ -47,6 +47,7 @@ traps and why the DAX crash guard is deliberately *not* the DAX capability.
| `hasSelectableMicInputs` | ✅ | ❌ | ❌ | `MainWindow::applyCapabilitiesToUi` → `PhoneCwApplet::setSelectableMicInputs` | The MIC/BAL/LINE/ACC/PC list. False collapses it to PC and adopts that into TransmitModel. Icom: ❌ (the radio picks its own input) |
| `hasDownwardExpander` | ✅ | ❌ | ❌ | `MainWindow::applyCapabilitiesToUi` → `PhoneApplet::setDexpVisible`; `AutomationServer` transmit snapshot | The radio has an authoritative DEXP/downward-expander command and read-back path. False hides the complete row and omits `dexp`/`dexpLevel` from automation state. Icom: ❌ until a model profile evidences and implements that full path; the IC-9700 does not borrow Flex's compander surface |
| `rxFilterWidthsHz` | empty | empty | empty | `MainWindow::applyCapabilitiesToUi` → `RxApplet::setRadioFilterWidths` **and** `VfoWidget::setRadioFilterWidths` | The RX filter widths a radio can actually reach, **narrowest first**. **Empty = continuous or unknown**, and the operator's configurable list stays in force. Icom publishes the selected slot's actual 1A 03 width plus factory defaults for the two unselected slots that CI-V cannot read, and republishes on mode, slot, or width changes. Both filter surfaces read it — the VFO grid did not, which is how the two disagreed about what the radio could do |
| `rxFilterControl` | empty | empty | empty | `MainWindow::applyCapabilitiesToUi` → `RxApplet::setRadioFilterControl` **and** `VfoWidget::setRadioFilterControl` | Stable radio-owned preset identity plus the mode's continuous width limits. Icom publishes FIL1/FIL2/FIL3 in radio order; changing the selected slot's 1A 03 width updates its tooltip/passband content without renaming or reordering the button. An empty preset list preserves the legacy width-only behavior for every other family |
| `hasTxFilterControls` | ✅ | ✅ | ❌ | `MainWindow::applyCapabilitiesToUi` → `PhoneApplet::setTxFilterControlsAvailable` | Independent TX low/high cutoff controls. Icom: true only for model profiles with a verified low/high edge register; false hides the complete row (including IC-9700, whose documented SSB TX bandwidth is WIDE/MID/NAR rather than independent cutoffs) |
| `txFilterLowEdgesHz` / `txFilterHighEdgesHz` | empty | empty | empty | `MainWindow::applyCapabilitiesToUi` → `PhoneApplet::setTxFilterEdges` | The discrete TX passband edges a radio can actually reach, ascending. **Empty = continuous or unknown**. Icom publishes per-model tables only where the model's own CI-V guide defines the WIDE/MID/NAR/SSB-D settings; the Phone applet steps through those values and rejects an exact typed value outside the list |
| `canReboot` | ✅ | ❌ | ❌ | `RadioSetupDialog` | Shows the Reboot row and enables its button only while connected. Icom: ❌ because power-off over Wi-Fi is a one-way trip, not a remote reboot. |
Expand Down
3 changes: 2 additions & 1 deletion docs/automation-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ connects).
| `equalizer` (or `eq`) | — | 8-band RX+TX graphic EQ: `rxEnabled`/`txEnabled` and `rx`/`tx` band maps keyed by label (`63`…`8k`). Validate EQ-applet slider changes. |
| `meters` | — | `{all:[…]}` — every radio meter with `name`, `value`, `unit`, `low`/`high`, `description`, and **`age_ms`** (staleness): a meter that updates has small `age_ms` and a tracking `value`. |
| `slices` | — | array of all slice snapshots |
| `slice` | `active` (default) / `tx` / `<sliceId>` | one slice (sliceId, letter, frequency, mode, filterLow/High, rxAntenna, nb/nr/anf + levels, **squelch/squelchLevel, agcMode/agcThreshold, apf/apfLevel**, **adaptiveFilterEnabled/adaptiveMinLowCut/adaptiveMaxHighCut/adaptiveMinSnr/adaptiveResponse/adaptiveSplatter/adaptiveActive** (SSB adaptive RX filter — `adaptiveActive` is the live AUTO-fit state), **linkedTo** (Slice Link peer id, `-1` when unlinked), txSlice, …) |
| `slice` | `active` (default) / `tx` / `<sliceId>` | one slice (sliceId, letter, frequency, mode, filterLow/High, **filterPresetId/filterPreset** for a radio-owned FIL slot, rxAntenna, nb/nr/anf + levels, **squelch/squelchLevel, agcMode/agcThreshold, apf/apfLevel**, **adaptiveFilterEnabled/adaptiveMinLowCut/adaptiveMaxHighCut/adaptiveMinSnr/adaptiveResponse/adaptiveSplatter/adaptiveActive** (SSB adaptive RX filter — `adaptiveActive` is the live AUTO-fit state), **linkedTo** (Slice Link peer id, `-1` when unlinked), txSlice, …) |
| `hostnb` | — (optional property) | HOST-SIDE noise blanker, read from the DSP: `{receivers:[{ddc,panId,on,level,threshold,requestedOn,requestedLevel,hasChain}]}`. **Distinct from `get slice nb`** — that reports the slice model, which is set the instant the button is clicked and stays true even if the intent never reached the DSP. `on`/`level` here are what the WDSP stage actually has; `requestedOn`/`requestedLevel` are what the backend was asked for, reported alongside so the two can be COMPARED. Errors on a radio that does not declare `hasHostNoiseBlanker` rather than returning an empty success. |
| `clock` | — | AetherClock snapshot: `state`/`stateName` (NoSignal/Acquiring/Locked), `station`/`stationName` (WWV/WWVH/WWVB), `decodedUtc` (ISO-8601, empty until a decode), `offsetMs` (decoded − host at the second edge; positive = host behind broadcast), `lockQuality` (0–100), `sliceId` (bound slice, −1 when stopped), `gpsTimeAvailable`. Validate applet Start/Tune/station-switch actions and lock progress without pixels. |
| `pans` | — | array of all panadapter snapshots |
Expand Down Expand Up @@ -1420,6 +1420,7 @@ re-poll `get slices`.
| `tx` | `<sliceId>` | make a slice the TX slice — the external-split transition; radio enforces single-TX |
| `mode` | `<name>` e.g. `DSTR` | set the active slice mode through `SliceModel`; validated against the radio-advertised mode list |
| `filter` | `<lowHz> <highHz>` e.g. `-3000 -150` | set the active slice passband through `SliceModel::setFilterWidth`, the operator-intent setter — so the edges reach `IRadioBackend::setSliceFilter` and not just the model. Necessary because a mode change mirrors the passband *inside* the model without emitting that intent, which can leave a backend that owns its own DSP chain running the pre-mirror passband while `get_state` reports the mirrored one. Assert the passband before measuring anything through the audio path. Returns both the requested edges and the post-normalization `filterLow`/`filterHigh` the model actually holds. Use `-4000 4000` for a carrier-straddling AM passband |
| `filterpreset` | `<FIL1\|FIL2\|FIL3>` | select a stable radio-owned RX filter slot without conflating it with a passband-width edit. Returns the requested slot; re-poll `get slice active filterPreset` and the filter edges for radio-authoritative readback |
| `agc` | `<off\|slow\|med\|fast> [threshold 0..100]` | set the active slice's receive AGC through `SliceModel`'s operator setters, so it emits `agcCommandIssued` and reaches `IRadioBackend::setSliceAgc`. Applies the threshold before the mode so a combined request arrives at the backend as one coherent pair. On a backend that owns its DSP chain (HL2) this maps to the WDSP RXA AGC mode and the AGC ceiling in dB; on Flex it is the firmware's own AGC. Use `off` with a low threshold to get a linear path for measurement |
| `dsp` | `<nr\|nb\|anf\|squelch> <on\|off> [level]` | drive the receive DSP controls an operator drives — noise blanker, noise reduction, auto-notch, and squelch (with an optional 0..100 level). `slice dsp squelch` is the squelch path; there is deliberately no separate squelch verb (#5102) |
| `tone` | `<off\|ctcss_tx> [freq]` | set the FM CTCSS encode mode and tone. The value is applied before the mode, so enabling CTCSS never keys on the previous tone for a round trip. The mode pair is what a FlexRadio slice carries |
Expand Down
58 changes: 54 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 @@ -1250,7 +1250,7 @@
QString sliceActionList()
{
return QStringLiteral(
"add|remove|select|tx|mode|filter|agc|dsp|tone|offset|diversity|"
"add|remove|select|tx|mode|filter|filterpreset|agc|dsp|tone|offset|diversity|"
"centerlock|link|txant|rxant|rxsource|fixture|clearfixture");
}

Expand Down Expand Up @@ -1673,8 +1673,16 @@

// linkedTo: peer slice id when this slice is a Slice Link member, else -1
// (supplied by the GUI's peer query — the link is client-side state).
QJsonObject sliceSnapshot(const SliceModel* s, int linkedTo)
QJsonObject sliceSnapshot(const SliceModel* s, int linkedTo,
const RxFilterControl& filterControl)
{
QString filterPreset;
for (const RxFilterPreset& preset : filterControl.presets) {
if (preset.id == filterControl.selectedPresetId) {
filterPreset = preset.label;
break;
}
}
return QJsonObject{
{QStringLiteral("sliceId"), s->sliceId()},
{QStringLiteral("letter"), s->letter()},
Expand All @@ -1683,6 +1691,8 @@
{QStringLiteral("mode"), s->mode()},
{QStringLiteral("filterLow"), s->filterLow()},
{QStringLiteral("filterHigh"), s->filterHigh()},
{QStringLiteral("filterPresetId"), filterControl.selectedPresetId},
{QStringLiteral("filterPreset"), filterPreset},
{QStringLiteral("active"), s->isActive()},
{QStringLiteral("txSlice"), s->isTxSlice()},
{QStringLiteral("rxAntenna"), s->rxAntenna()},
Expand Down Expand Up @@ -5706,7 +5716,7 @@
} else if (model == QLatin1String("slices")) {
QJsonArray arr;
for (const SliceModel* s : radio->slices())
arr.append(sliceSnapshot(s, sliceLinkPeerOf(s)));
arr.append(sliceSnapshot(s, sliceLinkPeerOf(s), radio->radioFilterControl()));
Comment thread
rfoust marked this conversation as resolved.
return QJsonObject{{QStringLiteral("ok"), true}, {QStringLiteral("slices"), arr}};
} else if (model == QLatin1String("pans")) {
QJsonArray arr;
Expand Down Expand Up @@ -5785,7 +5795,7 @@
}
if (!s)
return err(QStringLiteral("no slice for selector '") + selector + QStringLiteral("'"));
data = sliceSnapshot(s, sliceLinkPeerOf(s));
data = sliceSnapshot(s, sliceLinkPeerOf(s), radio->radioFilterControl());
} else if (model == QLatin1String("pan")) {
const PanadapterModel* p = nullptr;
if (selector.isEmpty() || selector == QLatin1String("active"))
Expand Down Expand Up @@ -7184,6 +7194,46 @@
{QStringLiteral("filterLow"), s->filterLow()},
{QStringLiteral("filterHigh"), s->filterHigh()}};
}
if (action == QLatin1String("filterpreset")) {
QString requested = arg.trimmed().toUpper();
if (requested.startsWith(QLatin1String("FIL"))) {
requested.remove(0, 3);
}
bool okPreset = false;
const int presetId = requested.toInt(&okPreset);
const RxFilterControl control = radio->radioFilterControl();
const auto preset = std::find_if(
control.presets.cbegin(), control.presets.cend(),
[presetId](const RxFilterPreset& candidate) {
return candidate.id == presetId;
});
if (!okPreset || preset == control.presets.cend()) {
return err(QStringLiteral(
"slice filterpreset requires a radio-advertised preset (e.g. FIL1)"));
}

SliceModel* s = nullptr;
for (SliceModel* candidate : radio->slices()) {
if (candidate->isActive()) {
s = candidate;
break;
}
}
if (!s && !radio->slices().isEmpty()) {
s = radio->slices().first();
}
if (!s) {
return err(QStringLiteral("no slice available to select a filter preset on"));
}

radio->selectRadioFilterPreset(s->sliceId(), presetId);
return QJsonObject{{QStringLiteral("ok"), true},
{QStringLiteral("slice"), QStringLiteral("filterpreset")},
{QStringLiteral("id"), s->sliceId()},
{QStringLiteral("presetId"), presetId},
{QStringLiteral("preset"), preset->label},
{QStringLiteral("requested"), true}};
}
if (action == QLatin1String("agc")) {
// "slice agc <off|slow|med|fast> [threshold 0..100]" — drive the RX AGC
// through the same operator setters the RX applet uses, so the change
Expand Down
10 changes: 10 additions & 0 deletions src/core/backends/IRadioBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ class IRadioBackend : public QObject {
virtual void setSliceFrequency(int sliceId, double hz) = 0;
virtual void setSliceMode(int sliceId, const QString& mode) = 0;
virtual void setSliceFilter(int sliceId, int lowHz, int highHz) = 0;
// Select a stable radio-owned RX filter preset. The passband setter above
// remains exclusively a resize/reposition intent; keeping the two verbs
// distinct prevents a width that happens to equal a preset from changing
// slots. Empty RadioCapabilities::rxFilterControl.presets means callers
// never invoke this default no-op.
virtual void setSliceFilterPreset(int sliceId, int presetId)
{
Q_UNUSED(sliceId);
Q_UNUSED(presetId);
}
// Receive AGC. mode is the neutral vocabulary the slice model uses —
// "off" / "slow" / "med" / "fast"; thresholdDb is the operator's 0..100
// AGC-threshold value. A backend whose hardware owns the AGC translates
Expand Down
36 changes: 36 additions & 0 deletions src/core/backends/RadioCapabilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ struct DeclaredBandRange {
bool operator==(const DeclaredBandRange&) const = default;
};

// A stable, radio-owned receive-filter preset. `id` is the identity used on
// the wire (for example Icom FIL1/FIL2/FIL3); widthHz is mutable content of
// that preset and must never be used as its identity.
struct RxFilterPreset {
int id = 0;
QString label;
int widthHz = 0;

bool operator==(const RxFilterPreset&) const = default;
};

struct RxFilterControl {
QList<RxFilterPreset> presets;
int selectedPresetId = 0;
int minimumWidthHz = 0;
int maximumWidthHz = 0;
int widthStepHz = 0;

bool operator==(const RxFilterControl&) const = default;
};

// A capability update reaches the two legacy/new presentation setters one at
// a time. Treat the preset metadata as usable only when it describes every
// width in the current presentation list; this keeps a disconnect or mode
// transition from indexing stale FIL metadata against a newly rebuilt list.
[[nodiscard]] inline bool hasCompleteRxFilterPresets(const RxFilterControl& control,
qsizetype widthCount)
{
return !control.presets.isEmpty() && control.presets.size() == widthCount;
}

enum class FmTonePresentation {
Legacy,
Hidden,
Expand Down Expand Up @@ -487,6 +518,11 @@ struct RadioCapabilities {
// control sweep over hardware that cannot follow it.
QList<int> rxFilterWidthsHz;

// Stable preset identity and continuous-width limits for radios where a
// preset selects a mutable hardware slot. Empty preserves the legacy
// width-only button contract above (Flex/HL2/ANAN/Sim).
RxFilterControl rxFilterControl;

// Whether the radio implements the independent TX low/high cutoff controls
// presented by PhoneApplet. False hides the complete control row rather
// than offering controls whose writes the backend cannot honour.
Expand Down
Loading
Loading