Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/architecture/radio-capabilities-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ traps and why the DAX crash guard is deliberately *not* the DAX capability.

| Field | Flex | HL2 | Sim | Read at | Effect |
|---|:--:|:--:|:--:|---|---|
| `canCreateSlices` | ✅ | ❌ | ❌ | `RadioModel::addSliceOnPan`, only without a command plane | Admission to the neutral backend's independent slice-creation hook on an existing pan. Flex and Sim use their existing command adapters without consulting this field, so the values shown are declarations, not a UI availability rule; do not gate +RX on this field alone. Capacity remains `maxSlices`; paired/fixed receiver topologies do not gain independent creation. Icom, ANAN and RTL explicitly declare false; RTL remains one slice in RFC #5468 P01. |
| `family` | `"flex"` | `"hl2"` | `"sim"` | `MainWindow::rfGainSettingsKey` | Scopes the persisted RF-gain key per family |
| `model` | from provider | `"Hermes-Lite 2"` | `"AetherSDR Demo"` | `FlexBackend::capabilities` | Key into the ModelCapabilities table |
| `manufacturer` | `"FlexRadio"` | `"Hermes-Lite"` | `"AetherSDR"` | `MainWindow::refreshRadioIdentityLabels` | Status-bar make row ABOVE the model, shown only when the model string does not already carry the brand (`FLEX-8400M` does, `IC-705` does not). Display only — nothing branches on it. Icom: `"Icom"` |
Expand Down
14 changes: 13 additions & 1 deletion docs/automation-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -1679,7 +1679,7 @@ re-poll `get slices`.

| `action` | `value` | effect |
|---|---|---|
| `add` | optional `<mhz>` | create a slice (radio-wide slot capacity is pre-checked; refused at the slice limit, naming any foreign occupant) |
| `add` | optional finite positive `<mhz>` | request a slice through RadioModel (radio-wide slot capacity is pre-checked; refused at the slice limit, naming any foreign occupant). Omit the value for default placement; an explicit malformed, non-finite or non-positive value is an error, never a default-frequency fallback |
| `remove` | `<sliceId>` | remove a slice (refuses the last one) |
| `select` | `<sliceId>` | make a slice the active slice (`slice set <id> active=1`) |
| `tx` | `<sliceId>` | make a slice the TX slice — the external-split transition; radio enforces single-TX |
Expand All @@ -1698,6 +1698,18 @@ re-poll `get slices`.
| `fixture` | `<sliceId> [A-H]` | disconnected-only test fixture: synthesize an owned slice through the normal slice-status path, optionally with a single radio `index_letter`, so `dumpTree` can assert UI without a radio |
| `clearfixture` | `<sliceId>` | remove a slice created by `fixture`; when the final fixture is removed, restores the pre-fixture disconnected model/max-slice state |

Ordinary `add`/`remove` requests report acceptance, not completion. An accepted
request can still be pending; re-poll `get slices` for authoritative ownership.
Explicit invalid `add` values return `"slice add requires a finite positive
frequency in MHz"` after the capacity pre-check. A RadioModel refusal returns
`"refused: radio did not accept slice creation"` or
`"refused: radio did not accept slice removal"`; this includes unsupported
backend operations and does not imply that a wire command was sent. The latter
replaces the earlier non-Flex `"not supported on this radio (no Flex command
plane)"` response, so scripts matching that text must update. Removal retains
`"refused: cannot remove the last slice"` and `"no slice with id <sliceId>"`
for the local last-slice and unknown-ID checks, respectively.

For a manual SQL band/profile-restore check, compare `get slice`'s
`squelch`/`squelchLevel` with `dumpTree`'s **RX applet → Squelch threshold**
and the VFO SQL control immediately after the transition. A radio-driven
Expand Down
22 changes: 12 additions & 10 deletions src/core/AutomationServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
#include <QLocalServer>
#include <QScopeGuard>
#include <QLocalSocket>
#include <QApplication>

Check warning on line 29 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 31 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 32 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 33 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 34 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 35 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 @@ -68,11 +68,11 @@
#include <utility>

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

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 (QAbstractButton) — tracked legacy (baseline 20); the count may only shrink
#include <QAbstractSlider>

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 (QAbstractSlider) — tracked legacy (baseline 20); the count may only shrink
#include <QAbstractItemView> // invoke selectRow: QTableWidget/QTreeWidget/QListWidget row select

Check warning on line 73 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 75 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 @@ -7158,10 +7158,15 @@

bool okF = false;
const double freq = arg.toDouble(&okF);
if (okF && freq > 0)
radio->addSliceOnPan(radio->panId(), freq); // specific frequency
else
radio->addSlice(); // default (TX freq / active pan)
if (!arg.isEmpty() && (!okF || !std::isfinite(freq) || freq <= 0.0)) {
return err(QStringLiteral("slice add requires a finite positive frequency in MHz"));
}
const bool accepted = arg.isEmpty()
? radio->addSlice()
: radio->addSliceOnPan(radio->panId(), freq);
if (!accepted) {
return err(QStringLiteral("refused: radio did not accept slice creation"));
}
return QJsonObject{{QStringLiteral("ok"), true}, {QStringLiteral("slice"), QStringLiteral("add")},
{QStringLiteral("freq"), okF ? QJsonValue(freq) : QJsonValue()},
{QStringLiteral("requested"), true},
Expand All @@ -7176,12 +7181,9 @@
return err(QStringLiteral("refused: cannot remove the last slice"));
if (!radio->slice(id))
return err(QStringLiteral("no slice with id ") + arg);
// `slice remove` is Flex wire text and no seam verb exists for it yet
// — refuse rather than report ok for a command the model will drop
// (M0, #5263).
if (!radio->hasCommandPlane())
return err(QStringLiteral("not supported on this radio (no Flex command plane)"));
radio->sendCommand(QStringLiteral("slice remove %1").arg(id));
if (!radio->removeSlice(id)) {
return err(QStringLiteral("refused: radio did not accept slice removal"));
}
return QJsonObject{{QStringLiteral("ok"), true}, {QStringLiteral("slice"), QStringLiteral("remove")},
{QStringLiteral("id"), id}};
}
Expand Down
29 changes: 29 additions & 0 deletions src/core/backends/IRadioBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,30 @@ class IRadioBackend : public QObject {
// a single answer.
virtual void setActiveSlice(int sliceId) { Q_UNUSED(sliceId); }

// ---- ordinary receive-slice lifecycle ----
// panId is backend-owned and opaque; frequencyHz is absolute RF in Hz.
// True accepts ownership of a request, not confirmation of a new/removed
// slice. Publish confirmed state through sliceChanged / sliceRemoved;
// report a later failure through sliceLifecycleFailed. A false return is
// final refusal: callers must never fall back to another command plane.
// Fixed/paired receiver topologies keep the default refusal. Flex and Sim
// retain RadioModel's existing command-plane adapter for these requests.
//
// A backend must cancel pending work on disconnect/reconnect and discard
// completions from retired sessions or receiver instances before emitting
// state/failure. Reused slice integers alone cannot identify pending work.
virtual bool createSlice(const QString& panId, double frequencyHz)
{
Q_UNUSED(panId);
Q_UNUSED(frequencyHz);
return false;
}
virtual bool removeSlice(int sliceId)
{
Q_UNUSED(sliceId);
return false;
}

// ---- panadapter lifecycle ----
//
// Bring up / tear down a panadapter (and, on a backend where a pan IS a
Expand Down Expand Up @@ -892,6 +916,11 @@ class IRadioBackend : public QObject {
// compared a slice count that never fell against maxSlices() and reported
// "Slice capacity is full" on a radio with one receiver running.
void sliceRemoved(int sliceId);
// Failure of an accepted ordinary lifecycle request. operation is "create"
// or "remove"; sliceId is -1 when creation never allocated a published ID.
// This is diagnostic, not a state delta or a split/TX completion protocol.
void sliceLifecycleFailed(const QString& operation, int sliceId,
const QString& reason);
void meterUpdate(const QString& meterId, double value);

// Normalized transmit-status delta (aetherd RFC 2.3 — TransmitModel
Expand Down
6 changes: 6 additions & 0 deletions src/core/backends/RadioCapabilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ struct RadioCapabilities {
QString manufacturer;

// Receive
// Independent slice creation on an existing pan through the neutral backend
// hook. RadioModel consults this only without a command plane; Flex and Sim
// retain their command adapters regardless of this value. Do not use this
// field alone to gate +RX in the UI. Separate from maxSlices: a paired
// receiver/pan topology can support several slices but not this operation.
bool canCreateSlices = false;
int maxSlices = 1; // independent demod slices the radio supports
int maxPanadapters = 1; // simultaneous panadapters
QVector<int> sampleRatesHz; // supported per-receiver sample rates (Hz)
Expand Down
1 change: 1 addition & 0 deletions src/core/backends/anan/AnanBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ RadioCapabilities AnanBackend::capabilities() const
c.hasAgcThreshold = true; // Host receiver DSP implements threshold/off gain.
c.manufacturer = QStringLiteral("Apache Labs");
c.model = QStringLiteral("ANAN-G2");
c.canCreateSlices = false;
c.maxSlices = 1;
c.maxPanadapters = 1;
c.sampleRatesHz = {48000, 96000, 192000, 384000, 768000, 1536000};
Expand Down
1 change: 1 addition & 0 deletions src/core/backends/flex/FlexBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ RadioCapabilities FlexBackend::capabilities() const
// derived-from-name truth used to *seed* the reported capabilities; a fuller
// FlexBackend refines these from live radio status as touchpoints convert.
const ModelCapabilities mc = capabilitiesFor(caps.model);
caps.canCreateSlices = true;
caps.maxSlices = mc.maxSlices;
// approx: pan capacity is not strictly slice count on real Flex hardware;
// refined from live radio status in a later touchpoint conversion.
Expand Down
1 change: 1 addition & 0 deletions src/core/backends/hl2/Hl2Backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,7 @@ RadioCapabilities Hl2Backend::capabilities() const
// maxSlices") is what this closes.
const int ceiling = m_connected ? receiverCeiling()
: std::max(1, m_ids.size());
c.canCreateSlices = false;
c.maxSlices = ceiling;
c.maxPanadapters = ceiling;
for (const int rate : kIqSampleRatesHz)
Expand Down
1 change: 1 addition & 0 deletions src/core/backends/icom/IcomCivBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ RadioCapabilities IcomCivBackend::capabilities() const
c.manufacturer = QStringLiteral("Icom");
c.model = QString::fromUtf8(m.name.data(), static_cast<int>(m.name.size()));

c.canCreateSlices = false;
c.maxSlices = m.receivers;
c.maxPanadapters = m.hasScope ? m.receivers : 0;
c.tuningMinHz = static_cast<double>(m.tuningMinHz);
Expand Down
1 change: 1 addition & 0 deletions src/core/backends/rtl/RtlSdrBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ RadioCapabilities RtlSdrBackend::capabilities() const
c.cwPitchStepHz = 10;

// Receiver limits
c.canCreateSlices = false;
c.maxSlices = 1;
c.maxPanadapters = 1;

Expand Down
1 change: 1 addition & 0 deletions src/core/backends/sim/SimBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ RadioCapabilities SimBackend::capabilities() const
caps.model = demoModelName();
caps.fmTonePresentation = FmTonePresentation::Legacy;
caps.fmDtcsCodes = {};
caps.canCreateSlices = false;
caps.maxSlices = 1; // Phase 1: a single slice. Phase 2 raises this.
// Four receivers since #4887 phase 4 — enough to exercise the workspace
// canvas's per-pan items and measure the multi-pan render budget in CI
Expand Down
4 changes: 4 additions & 0 deletions src/gui/MainWindow_Session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,10 @@ void MainWindow::wireRadioModel()
QString("%1 supports a maximum of %2 panadapters")
.arg(model).arg(limit), 4000);
});
connect(&m_radioModel, &RadioModel::sliceLifecycleFailed, this,
[this](const QString& operation, int, const QString& reason) {
statusBar()->showMessage(tr("Slice %1 failed: %2").arg(operation, reason), 6000);
});
connect(&m_radioModel, &RadioModel::sliceCreateFailed,
this, [this](int limit, const QString& model) {
statusBar()->showMessage(
Expand Down
18 changes: 8 additions & 10 deletions src/gui/MainWindow_Wiring.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5295,12 +5295,11 @@ void MainWindow::wirePanadapter(PanadapterApplet* applet)
});
connect(sw, &SpectrumWidget::sliceCloseRequested,
this, [this](int sliceId) {
if (m_radioModel.slices().size() <= 1) return;
if (SliceModel* slice = m_radioModel.slice(sliceId);
centerLockActiveForSlice(slice)) {
clearCenterLockForPan(slice->panId(), true);
// onSliceRemoved clears center lock only after authoritative removal.
// A refused or pending request must leave the current receiver intact.
if (!m_radioModel.removeSlice(sliceId)) {
statusBar()->showMessage(tr("Cannot remove this slice"), 4000);
}
m_radioModel.sendCommand(QString("slice remove %1").arg(sliceId));
});
connect(sw, &SpectrumWidget::sliceCreateRequested,
this, [this, applet](double freqMhz) {
Expand Down Expand Up @@ -5713,12 +5712,11 @@ void MainWindow::wireVfoWidget(VfoWidget* w, SliceModel* s)
syncKiwiSdrDiversityEscControls();
});
connect(w, &VfoWidget::closeSliceRequested, this, [this, sliceId]() {
if (m_radioModel.slices().size() <= 1) return;
if (SliceModel* slice = m_radioModel.slice(sliceId);
centerLockActiveForSlice(slice)) {
clearCenterLockForPan(slice->panId(), true);
// onSliceRemoved clears center lock only after authoritative removal.
// A refused or pending request must leave the current receiver intact.
if (!m_radioModel.removeSlice(sliceId)) {
statusBar()->showMessage(tr("Cannot remove this slice"), 4000);
}
m_radioModel.sendCommand(QString("slice remove %1").arg(sliceId));
});
connect(w, &VfoWidget::stepTuneRequested, this, [this, sliceId](double mhz) {
if (auto* sl = m_radioModel.slice(sliceId))
Expand Down
Loading
Loading