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
1 change: 1 addition & 0 deletions docs/architecture/radio-capabilities-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ the production dialog and checks its displayed peak level and frequency.
| `hasSupplyVoltageTelemetry` | ✅ | ❌ | ❌ | `MainWindow::applyCapabilitiesToUi` | PA supply-voltage readout in the status bar. Icom: ✅ only when the active model profile provides a calibrated Vd curve; unprofiled models neither publish nor poll Vd/Id |
| `hasPaTemperatureTelemetry` | ✅ | ✅ | ❌ | `MainWindow::applyCapabilitiesToUi` | PA-temperature gauge and °C/°F selector in Radio Vitals. Icom: ❌ until a model profile declares and implements a PA-temperature meter; the IC-9700 has no such declared telemetry |
| `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 |
| `paTelemetryAudit` (record) | — (absent) | — (absent) | — (absent) | `MainWindow::applyCapabilitiesToUi` | A PROTOCOL audit of the PA telemetry the wire format can carry at all, as opposed to what a session has so far RECEIVED. ENGAGED with `temperatureAbsent` means the payload was read field by field and there is no temperature in it, so the status bar WITHDRAWS its PA temperature row instead of leaving it on `PA --°C` forever. ABSENT (the default) means NOT AUDITED and every consumer keeps its historical presentation — which is what holds Flex, HL2, Icom, Sim and RTL at today's behaviour rather than reclassifying a default `false` above as "proven absent". ANAN: engaged, because Protocol 2's whole analog payload is six fields and none is a temperature. A record and not a bool per #5262 M2, whose boolean count is frozen and shrink-only; the audit can also grow a current/supply field without a second ratchet argument |
| `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` |
Expand Down
22 changes: 22 additions & 0 deletions src/core/backends/RadioCapabilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,28 @@ struct RadioCapabilities {
// because some radios define PACURRENT with an unusable/clipped range.
bool hasPaCurrentTelemetry = false;

// A PROTOCOL audit of the PA telemetry the radio's wire format can carry at
// all, as distinct from the three flags above, which report what a session
// has so far RECEIVED. Engaged means the payload was read field by field and
// the absence is a property of the protocol rather than of a radio that has
// simply not sent one yet -- so a readout may be WITHDRAWN instead of left
// printing a placeholder that can never resolve.
//
// std::nullopt (the default) means NOT AUDITED, and every consumer keeps its
// historical presentation. That is deliberate: it holds a family that has
// made no such claim at its existing behaviour instead of quietly
// reclassifying a default `false` above as "proven absent" -- the
// all-defaults-false trap check_capability_records.py was written to stop.
//
// A record rather than another bool because RadioCapabilities is AT its
// frozen boolean count and shrink-only (#5262 M2); per-feature records are
// the sanctioned shape for a new capability.
struct PaTelemetryAudit {
// The protocol defines no PA temperature field anywhere in its payload.
bool temperatureAbsent = false;
};
std::optional<PaTelemetryAudit> paTelemetryAudit;

// The radio reports main-fan speed as live telemetry. False means the
// Radio Vitals applet omits the fan gauge instead of presenting an
// instrument that can never receive a sample. This is independent of PA
Expand Down
69 changes: 63 additions & 6 deletions src/core/backends/anan/AnanBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,12 @@ AnanBackend::AnanBackend(QObject* parent)
});
connect(m_client, &P2Client::connectionError, this,
[this](const QString& reason) { emit connectionError(reason); });
connect(m_client, &P2Client::dropsUpdated, this, [](quint64) {
// No LinkStats wiring in this phase -- see the design plan's
// "explicitly not in this commit" list. Connected so the signal has
// a receiver rather than going nowhere; a future commit can surface
// it through IRadioBackend::linkStats().
});
// Drops reach the readout through the counters snapshot below, which
// carries the same total on a fixed cadence. Not wired per-drop: the
// LinkStats tick is deliberately periodic rather than event-driven, so a
// link that has gone silent still reports.
connect(m_client, &P2Client::linkCountersUpdated, this,
&AnanBackend::onLinkCounters);
// Only DDC0 feeds this DSP. Like ddc0IqReady above, notification and
// processing run directly on the I/O thread, with the DSP as context.
// Invalidate its partial FFT before the discontinuous block arrives.
Expand Down Expand Up @@ -405,6 +405,20 @@ RadioCapabilities AnanBackend::capabilities() const
c.hasAgcThreshold = true; // Host receiver DSP implements threshold/off gain.
c.manufacturer = QStringLiteral("Apache Labs");
c.model = QStringLiteral("ANAN-G2");
// ---- PA telemetry ----
// hasSupplyVoltageTelemetry stays FALSE although the radio sends a
// supply-rail count (P2Protocol decodes it): no known scale fits it. On a
// G2 bench the 5 V-reference scale read a 13.8 V rail as ~40 V, 2.9x high,
// and the 3.3 V board's scale differs from it by only 1.5x, so no board
// choice fixes it. A volts readout waits for an operator calibration.
//
// There is NO PA temperature in this protocol. The radio's whole analog
// payload is six fields and none of them is a temperature, so the readout
// can never resolve and is withdrawn rather than left showing a placeholder.
// hasPaTemperatureTelemetry stays false on its own (nothing is received);
// this says the stronger thing -- that nothing CAN be.
c.paTelemetryAudit = RadioCapabilities::PaTelemetryAudit{
/*temperatureAbsent=*/true};
c.canCreateSlices = false;
c.maxSlices = 1;
c.maxPanadapters = 1;
Expand Down Expand Up @@ -730,6 +744,12 @@ void AnanBackend::disconnectRadio()
m_tuneThrottleTimer->stop();
m_tunePendingApply = false;
m_pendingBandwidthKsps = 0;
// The next session's counters restart from zero in P2Client::start(), so a
// cached snapshot from this one would make the first tick of the next read
// as a huge backward jump in every total.
m_linkCounters = {};
m_linkCountersSeen = false;
m_lastSnapshotRxBytes = 0;
// A genuine operator-initiated disconnect always fires disconnected(),
// even one that lands mid-rate-change -- this is not the zoom case
// m_rateChanging exists to hide.
Expand Down Expand Up @@ -1405,6 +1425,43 @@ void AnanBackend::defineMeters()
emit meterDefined(d);
}

IRadioBackend::LinkStats AnanBackend::linkStats() const
{
LinkStats s;
// Nothing measured yet: the consumer keeps whatever source it already had
// rather than being handed a snapshot of zeros.
if (!m_connected || !m_linkCountersSeen)
return s;
s.reported = true;
s.alive = m_linkCounters.rxBytes > m_lastSnapshotRxBytes;
s.rxBytes = static_cast<qint64>(m_linkCounters.rxBytes);
s.txBytes = static_cast<qint64>(m_linkCounters.txBytes);
s.rxPackets = m_linkCounters.rxPackets;
s.rxPacketsLost = m_linkCounters.drops;
s.localEndpoint = m_linkCounters.localEndpoint;
// LEFT AT -1, WHICH THE STRUCT DEFINES AS "NOT MEASURED" AND THE MODEL
// HONOURS BY IGNORING. This transport is stream-only: the radio is told
// where to send and then sends, with no request/response exchange to time,
// so there is no round trip here to measure. Reporting 0 instead would
// render as a perfect "< 1 ms" link -- a measurement never taken.
// rttMs, jitterMs, gapMs and gapMaxMs all keep their -1 defaults.
return s;
}

void AnanBackend::onLinkCounters(const P2Client::LinkCounters& counters)
{
// A session restart shows as a BACKWARD jump, since P2Client::start() zeroes
// its counters; the cache is then a dead session's and is discarded here,
// whichever path restarted it. The first snapshot of a session compares
// against ZERO: against itself, it would read dead on the first tick.
if (counters.rxBytes < m_linkCounters.rxBytes)
m_linkCountersSeen = false;
m_lastSnapshotRxBytes = m_linkCountersSeen ? m_linkCounters.rxBytes : 0;
m_linkCounters = counters;
m_linkCountersSeen = true;
Comment thread
tropo1234 marked this conversation as resolved.
emit linkStatsUpdated(linkStats());
}

void AnanBackend::onDspMeter(float dbfs)
{
const double dbm = static_cast<double>(dbfs) + kSMeterDbmOffset + m_attenuationDb;
Expand Down
21 changes: 21 additions & 0 deletions src/core/backends/anan/AnanBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class AnanBackend : public IRadioBackend {
void setKeying(bool key, const AetherSDR::TxCoordinator::Operation& operation, const AetherSDR::TxCoordinator::Completion& completion = {}) override;
void invokeExtension(const QString& ns, const QString& verb,
quint64 requestId, const QVariant& arg = {}) override;
// Transport snapshot for the status bar's Network field and Network
// Diagnostics. Synchronous, and reads only the cache onLinkCounters() keeps
// on this thread -- never the client's own counters, which belong to the
// I/O thread. The push half is onLinkCounters(), on the client's tick.
[[nodiscard]] LinkStats linkStats() const override;

// ---- pure-function pieces, exposed static for testability ----
// (matches the WdspChannel/Hl2RxDsp precedent of exposing normally-
Expand Down Expand Up @@ -149,6 +154,7 @@ class AnanBackend : public IRadioBackend {

private:
friend class AnanNoiseBlankerTestAccess;
friend class AnanLinkStatsTestAccess;
void beginDspSetup();
void finishDspSetup(quint64 generation, bool ok, const QString& error);
// The "restart P2Client with m_pendingParams, then retune" half of what
Expand Down Expand Up @@ -183,6 +189,21 @@ class AnanBackend : public IRadioBackend {
// Declares SLC:LEVEL to the meter seam; on every connect, before the
// first reading can arrive. See its definition.
void defineMeters();
// One client snapshot -> one LinkStats push. The FIXED cadence is the
// client's timer, not ours: this runs on whatever the client published, and
// it must keep arriving after the radio goes quiet, because "nothing came
// this second" is the observation the heartbeat's alarm path waits for
// (IRadioBackend::LinkStats).
void onLinkCounters(const P2Client::LinkCounters& counters);
// The last snapshot the client published, cached on THIS thread so the
// synchronous linkStats() getter never reads the I/O thread's counters --
// see P2Client::LinkCounters for why that read would be a race.
P2Client::LinkCounters m_linkCounters;
bool m_linkCountersSeen = false;
// rxBytes at the previous snapshot, for LinkStats::alive -- a link that is
// bound and counting but has gone quiet must read as dead, which a
// cumulative total alone cannot say.
quint64 m_lastSnapshotRxBytes = 0;
// One WDSP S-meter reading (dBFS) -> dBm, smoothed, published on a tick.
void onDspMeter(float dbfs);
// The same smoother Hl2Backend publishes through, so the two receivers'
Expand Down
Loading
Loading