You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(caps): ANAN regains its noise-floor auto-adjust — split panBinsAreAbsolute out of radioOwnsDbmScale
Split panBinsAreAbsolute out of radioOwnsDbmScale so ANAN's noise-floor
auto-adjust can run on host-computed absolute bins. Shared OR predicate
in NoiseFloorAutoAdjustGate.h; HL2/ANAN/RTL declare binsAbsolute; Flex
and Icom unchanged. Also lands the stacked pan-span/amplitude capability
records from #5725.
Squashed-from: #5726
Co-authored-by: on8st <258096273+on8st@users.noreply.github.com>
Copy file name to clipboardExpand all lines: docs/architecture/aetherd-touchpoint-tags.json
+6Lines changed: 6 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -1354,5 +1354,11 @@
1354
1354
"note": "Session-local APRS fill-in policy and bounded modem queue; no backend-specific commands or transport ownership.",
1355
1355
"split": "",
1356
1356
"confidence": "high"
1357
+
},
1358
+
"core/backends/NoiseFloorAutoAdjustGate.h": {
1359
+
"tag": "universal",
1360
+
"note": "The ONE predicate deciding whether the noise-floor auto-adjust may move the display reference level, as an OR over two backend-neutral properties (a real dBm-range echo, or absolute spectrum bins). Deliberately free of Qt and of RadioCapabilities so the widget and its test read the same function rather than two copies; the UI reaching it is the seam working, not coupling. Family backends populate the capability it is called with.",
Copy file name to clipboardExpand all lines: docs/architecture/aetherd-touchpoints.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@
4
4
5
5
Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine-design.md) §2, §10). One row per engine header the UI includes; converting a touchpoint means the UI reaches that surface through the versioned protocol instead of the header.
@@ -172,6 +172,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine
172
172
|`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 |
173
173
|`core/backends/HealthSnapshotMerge.h`| 1 | universal — The snapshot merge rule, family-neutral by construction: a key the winner declares but omits from `values` means "not reported" and must not erase a base value. Moved out of backends/hl2/Hl2TelemetrySource.h (which keeps hl2MergeHealth as a forwarder) because AutomationServer::doHealth() and RadioHealthDialog both need it, and neither may include a family header to get it. Names no family. | unconverted |
174
174
|`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 |
175
+
|`core/backends/NoiseFloorAutoAdjustGate.h`| 2 | universal — The ONE predicate deciding whether the noise-floor auto-adjust may move the display reference level, as an OR over two backend-neutral properties (a real dBm-range echo, or absolute spectrum bins). Deliberately free of Qt and of RadioCapabilities so the widget and its test read the same function rather than two copies; the UI reaching it is the seam working, not coupling. Family backends populate the capability it is called with. | unconverted |
175
176
|`core/backends/RadioCapabilities.h`| 7 | universal — Backend-neutral capability descriptor consumed above the radio seam. Universal by definition; family implementations populate it. | unconverted |
176
177
|`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 |
177
178
|`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 |
Copy file name to clipboardExpand all lines: docs/architecture/radio-capabilities-map.md
+160-2Lines changed: 160 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -454,6 +454,149 @@ read at use time, not baked into a key at construction, so the ordering problem
454
454
does not arise. It is still its own change, and it applies to more than this one
455
455
control.
456
456
457
+
### The display scale: who owns it, and whether its numbers mean anything
458
+
459
+
Two separate questions that both land on the vertical axis of the panadapter,
460
+
and conflating them would hide one of them.
461
+
462
+
| Field | Flex | HL2 | Icom | ANAN | Question it answers |
463
+
|---|:--:|:--:|:--:|:--:|---|
464
+
|`radioOwnsDbmScale`| ✅ (default) | ⚠️ **✅ (default)**| ❌ | ❌ | Will the radio adopt a dBm range sent to it and report it back? |
465
+
|`dbmAxisIsCalibrated()` (`panAmplitude->calibratedDbm`) | ✅ (absent) | ❌ | ✅ (absent) | ❌ | Do the numbers on that axis mean absolute dBm at the antenna? |
466
+
|`panBinsAbsolute()` (`panAmplitude->binsAbsolute`) | ❌ (absent) | ✅ | ❌ (absent) | ✅ | Do the spectrum bins hold still while the reference level moves? |
467
+
468
+
**`panBinsAbsolute()` is consumed too, and it is the second term of ONE gate.**
469
+
`noiseFloorAutoAdjustAllowed(radioOwnsDbmScale, panBinsAbsolute)` in
470
+
`core/backends/NoiseFloorAutoAdjustGate.h` is an OR: a real echo from the radio
471
+
ends the auto-floor loop by confirmation, absolute bins end it by giving it a
472
+
fixed target, and either alone is enough. `SpectrumWidget::applyNoiseFloorAutoAdjust`
473
+
and the auto-floor branch of `dbmRangeChangeRequested` both call it, so the
474
+
widget and its backstop cannot drift apart. The other three
475
+
`radioOwnsDbmScale` gates below are about whether a range can be **sent** and
476
+
stay on the echo alone. HL2, ANAN and RTL-SDR declare `binsAbsolute = true`,
477
+
each quoting the expression that produces its bins; ANAN is the one whose loop
478
+
this turned back on.
479
+
480
+
**THE HL2 KEEPS THE PERMISSIVE DEFAULT, and that is deliberate rather than an
481
+
omission.**`Hl2Backend::capabilities()` never assigns the field and says why at
482
+
the point where it would: the flag answers two questions, and on this radio the
483
+
answers differ — it cannot be commanded a dBm range (so `false` is right), and
484
+
its auto-floor loop's measurement does not depend on one either (so `false`
485
+
would switch off a loop that bench run d101 measured working). The only two
486
+
`c.radioOwnsDbmScale = false` sites in the tree are `IcomCivBackend` and
487
+
`AnanBackend`. This PR's own test refuses to assert the HL2's value for the same
488
+
reason.
489
+
490
+
**So every gate in the table below is LIVE on an HL2**, not disarmed. An earlier
491
+
version of this row read ❌ and the table then said the opposite of the code —
492
+
caught by `aethersdr-agent` on #5725.
493
+
494
+
**`radioOwnsDbmScale` is consumed**, and a backend declaring `false` changes
495
+
behaviour at four gates plus two fan-out sites:
496
+
497
+
| Site | What `false` does |
498
+
|---|---|
499
+
|`SpectrumWidget::applyNoiseFloorAutoAdjust`| Early return — the gate that actually stops the runaway, because `m_refLevel` is local and would keep marching without it |
500
+
|`sendDbmRangeCommand` (`MainWindow_Wiring.cpp`) | Backstop: no dBm range leaves for the radio, whichever caller asked |
501
+
|`radioDbmHeadroomRecoveryRequested` handler | Return — the second ratchet source; gating `dbmRangeChangeRequested` alone did not stop it |
502
+
|`dbmRangeChangeRequested`, auto-floor branch | Re-seeds the widget from the pan's real range instead of commanding one |
503
+
|`dbmRangeDragFinished`| A hand drag still moves the LOCAL scale, but skips the handshake and the command |
504
+
|`MainWindow::applyCapabilitiesToUi`, and the per-pane wiring for a pane added after connect | Pushes the value into every `SpectrumWidget`, restoring the permissive default on disconnect (`!connected \|\| caps.radioOwnsDbmScale`) per rule 2 above |
505
+
506
+
The flag is about a **command plane**, not about a measurement. The auto-floor
507
+
loop is built on an echo: the widget moves its reference level, sends
508
+
`display pan set <id> min_dbm=… max_dbm=…`, and waits for the radio to confirm
509
+
before moving again. `FlexBackend` is the only reader of `min_dbm` in the tree,
510
+
and `RadioModel::sendCmd` drops the text at `hasCommandPlane()` for every
511
+
backend but Flex and Sim. So on the HL2 the request cannot arrive and the
512
+
confirmation cannot come back — which is what `false` states.
513
+
514
+
What that declaration does **not** state is that the 24 dB/s ratchet
515
+
`RadioCapabilities.h` describes was observed on an HL2. It was not, and the two
516
+
radios differ in the way that decides it: an Icom's scope bins are decoded
517
+
against the **commanded** range, so a dropped command leaves the measurement
518
+
stale and the loop cannot converge, while `RadioModel::onBackendSpectrumFrame`
519
+
passes the HL2's absolute float dBFS bins through untouched and
520
+
`SpectrumWidget::estimateNoiseFloorDbm` reads them directly — an input that does
521
+
not move with `m_refLevel`. The declaration rests on the source fact (there is no
522
+
echo to wait for); the runtime question is open.
523
+
524
+
**One read of this flag is doing a second job**, and it is worth knowing before
525
+
the next backend sets it. Four of the gates above stop outbound traffic that
526
+
cannot arrive. The first — `SpectrumWidget::applyNoiseFloorAutoAdjust` — turns
527
+
the noise-floor auto-adjust off, justified in its own comment by a radio whose
528
+
floor "is already where the calibration puts it". That is true of an Icom and
529
+
false of a raw-IQ backend, whose scale is arbitrary and local and where a local
530
+
auto-floor is the only one available. The two questions coincide on a Flex and an
531
+
Icom and come apart here; the gate should eventually ask whether the floor
532
+
MEASUREMENT depends on a commanded range.
533
+
534
+
**The calibration claim is declared and nothing reads it yet.** It is the
535
+
second question: an HL2's axis is dBFS wearing a dBm label, because nothing on
536
+
the board reports what 0 dBFS is worth at the antenna and no HL2 oracle states a
537
+
figure for it. `Hl2Backend` reads the declaration off
538
+
`Hl2DbReference::isCalibrated()` rather than hardcoding it, so a per-unit
539
+
`fullScaleDbm` measured later moves the capability with it. The values stay
540
+
internally consistent — a 3 dB stronger signal reads 3 dB higher — so what the
541
+
`false` denies is comparison: no spot level, no cross-station claim, no absolute
542
+
threshold.
543
+
544
+
It is **not a bool**. It is a field of `PanAmplitudeModel`, held as
545
+
`std::optional<PanAmplitudeModel> panAmplitude`, and read through
546
+
`RadioCapabilities::dbmAxisIsCalibrated()`. Absent means *no backend has been
547
+
read on the question*, which is not the same as "no" — the distinction a bool
548
+
cannot carry, and the reason #5262 M2 asks for a record. The accessor exists
549
+
because absence has to fall to the **legacy** answer (`true`): every backend
550
+
predating the field labelled its axis dBm and was consumed as though it meant
551
+
it, so answering `false` for an unread backend would silently restate a claim
552
+
about radios nobody has looked at.
553
+
554
+
`panBinsAbsolute()` reads the *other* field of the same record and falls the
555
+
**opposite** way on the same absent value — undeclared is not "absolute". Two
556
+
opposite defaults on one record is precisely why neither is unwrapped at a call
557
+
site.
558
+
559
+
Flex and Icom leave the record absent and keep the legacy `✅` above; that is an
560
+
unexamined default, not a finding, and it is exactly the trap rule 1 names,
561
+
sitting in the tree. ANAN and RTL-SDR no longer inherit it: both are raw-IQ
562
+
paths, both have now been read, and both declare `calibratedDbm = false` with
563
+
the bin expression quoted in their own `capabilities()`.
564
+
565
+
### The panadapter span is a hardware property on a raw-IQ radio
566
+
567
+
| Field | Flex | HL2 | RTL | Question it answers |
568
+
|---|:--:|:--:|:--:|---|
569
+
|`panSpanModel->followsSampleRate`| — (absent) | ✅ | — (absent) | Is `sampleRatesHz` the complete set of spans, floor included? |
570
+
|`panSpanModel->radioWide`| — (absent) | ✅ | — (absent) | Does changing one pan's span change every receiver's? |
571
+
572
+
Both live in one `std::optional<PanSpanModel> panSpanModel`. Absent is *not* a
573
+
pair of `false`s: it means no backend has been read, and a client that needs the
574
+
distinction checks `has_value()` before the fields.
575
+
576
+
Both are declared and nothing reads them yet — the behaviour they describe is
577
+
already implemented, by `Hl2Backend::applyPanBandwidth` snapping through
578
+
`nearestIqSampleRateHz` and by `panBandwidthLimitsChanged` clamping the zoom
579
+
control. What was missing was the **claim**, so a client had no way to ask.
580
+
581
+
On the HL2 the pan span *is* the DDC sample rate, so `sampleRatesHz` is not a
582
+
list of stream rates that happens to exist alongside a span control: it is every
583
+
span the radio can deliver, and its first entry (48 kHz) is a floor rather than a
584
+
default. A narrower window would need samples the DDC never sent. One array in
585
+
`Hl2Backend.h`, `kIqSampleRatesHz`, is simultaneously the capability, the zoom
586
+
clamp and the snap target, and `tests/hl2_pan_limits_declaration_test.cpp`
587
+
asserts the capability against that array rather than a copy of its values.
588
+
589
+
`panSpanModel->radioWide` is why `receivePanBandwidthControl` is `nullopt` on a radio
590
+
that plainly does change its span: `MetisProtocol::ccConfig` packs one
591
+
`SampleRate` into C1[1:0] for the whole board, so the control is real but
592
+
radio-wide. Publishing it as a per-pan authority would let an operator narrow one
593
+
window and silently retune the other three. The same shared budget is why
594
+
`Hl2Backend::receiverCeiling()` falls as the span widens.
595
+
596
+
Upstream RFC #5223 (display-side crop: show a sub-window of a delivered span at
597
+
full bin resolution) would change what the **display** shows. It would not change
598
+
what the radio can deliver, which is what these two fields declare.
599
+
457
600
## Previously bypassed, now reconciled
458
601
459
602
`maxSlices` / `maxPanadapters` sat here for weeks: declared by every backend
@@ -478,7 +621,10 @@ backend side the whole time.
478
621
479
622
| Field | Flex | HL2 | Sim | Note |
480
623
|---|:--:|:--:|:--:|---|
481
-
|`sampleRatesHz`| — | 4 rates |`{}`| HL2 populates it honestly; no consumer exists |
624
+
|`sampleRatesHz`| — | 4 rates |`{}`| HL2 populates it honestly; no consumer exists. On the HL2 it is also the complete span set — see `panSpanModel` below |
625
+
|`panSpanModel->followsSampleRate`| — (absent) | ✅ | — (absent) | The span IS the rate, so `sampleRatesHz` is every deliverable span and its first entry is a floor |
626
+
|`panSpanModel->radioWide`| — (absent) | ✅ | — (absent) | One DDC rate for the board; a span change moves every receiver |
627
+
|`dbmAxisIsCalibrated()`| — (absent ⇒ ✅) | ❌ | — (absent ⇒ ✅) | Whether the dBm axis is absolute. HL2 reads it off `Hl2DbReference::isCalibrated()`|
482
628
|`txPowerMaxWatts`| — (0.0) | 0.0 | 0.0 | Global fallback ceiling; Flex still omits it despite transmitting, which remains wrong but inert while `txPowerBands` is empty |
483
629
|`hasAmplifier`| — (❌) | ❌ | ❌ | The AMP applet is driven by `TunerModel::presenceChanged`, not by this |
484
630
|`extensions`| — | — | — | The namespaced vendor bag; never populated |
@@ -557,7 +703,19 @@ predicates the readouts ask before printing. See [`HERMES.md`](../HERMES.md)
557
703
asserts the model-profile capabilities and the refusals they gate, and that the
558
704
other backends keep the controls those refusals take away.
0 commit comments