aethersdr / aethersdr/AetherSDR
[RFC] Radio-native VFO flag layout — carry a second receiver where a second panadapter cannot go
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 299
Description
Preflight
- I have read GOVERNANCE.md and confirmed this change requires an RFC
- I have searched existing issues and this has not been proposed before
- I have not opened a PR for this change yet
Problem
Some radios have more receivers than they have scopes. The IC-9700 is the clear case: two receivers, one 0x27 0x00 scope stream. AetherSDR's per-slice VfoWidget flag assumes a slice and its panadapter travel together, so on such a radio the second receiver has nowhere to live — and the current backend papers over it by advertising maxPanadapters = 2 for a panadapter it cannot create (filed as #5347).
Adding a second panadapter is not the fix. There is no second scope stream to render, so it would show either a duplicate trace or nothing — the "black panadapter" symptom IcomCivBackend.cpp:1292 already warns about. And for the use this radio is bought for, satellite work, the two VFOs sit on different bands (2 m up, 70 cm down); no single frequency axis can show 145 MHz and 435 MHz at once.
The receiver is real, the operator needs to see it, and a panadapter is the wrong container.
Proposal
Let a radio's flag be laid out the way that radio's own front panel lays it out, driven by the radio dossier rather than hardcoded per model.
The IC-9700's LCD already solves this exact problem: Main and Sub stacked, each with its own frequency, mode, filter and S-meter, and the scope belonging to whichever receiver is selected. That is the layout constraint we derive from the protocol — and Icom shipped a legible answer to it years ago, one every 9700 operator already reads fluently. Adopting the information architecture costs the operator no learning.
The same argument generalises. A single-receiver HF radio wants today's flag. A 7610 or a 7110 wants its own arrangement. What differs is not the widget, it is the description of what the radio presents.
Not a clone — a template fill
To be explicit, because this distinction decides whether the proposal is acceptable at all:
- In scope: the information architecture. Which receivers exist, what each shows, which one owns the scope, what is stacked versus side by side.
- Out of scope: Icom's visual design. No copied colours, fonts, iconography, bitmap assets, or pixel-level imitation of their screen.
The rendering stays AetherSDR's — existing VfoWidget construction, ThemeManager tokens, current typography and spacing. A 9700 flag should be recognisably an AetherSDR flag that happens to be arranged the way a 9700 arranges things. docs/style/theme-style-guide.md continues to govern every colour.
What it looks like
The minimal change: keep today's flag exactly as it is — the same ANT/SPLIT/TX/A header strip, the same boxed frequency readout, the same dBm line, calibrated S-meter and OPT / DFM / X:RIT row — and add one more frequency window beneath the first, the two titled MAIN and SUB.
For the use this radio is bought for, the pair carries a role as well as a name: TX above, RX below, mirroring the radio and the way a pass is actually flown — the uplink you are transmitting on over the downlink you are listening to. The S-meter and the dBm reading stay with the receiving row, because there is no received signal on the transmit side to show; one meter, unambiguously owned, rather than a second bar reading nothing.
That costs roughly 26 px of height and no extra width — width being the scarce dimension when several flags share a panadapter.
Built and running, not sketched
This is not a mockup. It is VfoWidget on a spike branch, running in the app against the demo simulator, at native scale:
The values are the case this is for: 145.070.000 on VFO A marked TX in the warning colour over 435.810.000 on VFO B marked RX in the accent — a 2 m uplink and a 70 cm downlink, live at the same moment during a satellite pass. Two bands that no single frequency axis could ever show together, which is the clearest statement of why a second panadapter is the wrong container and one flag is the right one.
The VFO letter and memory channel sit to the right of each readout, where the radio itself puts them, the channel number centred beneath its VFO line and coloured white on the active receiver, dim on the other. Everything else is untouched: the ANT/SPLIT/TX/A header, the dBm line, the calibrated S-meter, and the speaker / DSP / USB / X:RIT row are exactly as they are on main.
It also settles the legibility question the mockup could not: both readouts stay readable at native size, and the amber/cyan role coding separates at a glance without needing a zoom.
Diff so far: +284 / -6 across three files (VfoWidget.{h,cpp}, one wiring hook), behind an env var and default-off.
What building it actually taught us, none of which the picture could:
- The frequency font's period is a zero-width combining glyph. Measured in Qt: every digit in DSEG7 Modern advances 14 px and
.advances 0. So0000.000.000is exactly 140 px — ten digit cells — and a 23 cm reading fits with the existing padding. It also means the apparent "gap" in a reading like435.810.000is the font overlaying a decimal point into a lit segment cell, not a formatting bug; it renders identically onmaintoday. - Measure with the label's own font. Sizing the box from a default-constructed
QFontunder-sized it and clipped the trailing digits, because the seven-segment face is materially wider at the same pixel size. Same lesson as the Ulanzi 212 px status row (#3485) — the first spike run reproduced it exactly. - The TX meter already exists.
pushSmartMtrInput()builds "the current MeterInput (RX signal vs the selected TX meter)" and swaps while keyed, so a shared meter that reads power/SWR on transmit and signal on receive needs no new plumbing at all. One meter, belonging to the receive row, is both what the hardware supports and what the code already does.
What is still a stub, and visibly so. The second row is fed by AETHER_SPIKE_DUAL_VFO, not by a backend, and the memory channel numbers are invented. Both are commented as such at every site. That is the honest state: the layout is proven, the data path does not exist — see below.
Mechanism
VfoWidget (6,722 lines, src/gui/VfoWidget.cpp) already carries the hard parts: a collapsed/expanded model with persisted state per slice (SliceFlagCollapsed_<sliceId>), theme-resolved styling, and per-slice signal wiring. This proposal does not replace it. It adds a layout description that VfoWidget consumes, defaulting to exactly today's arrangement when a radio has no description — so every existing radio is bit-for-bit unchanged.
Sketch, deliberately thin:
FlagLayout
├── receivers[] // one entry per receiver the radio presents
│ ├── label // "MAIN" / "SUB" — the radio's own naming
│ ├── fields[] // freq, mode, filter, s-meter — existing widgets
│ └── ownsScope // which receiver the panadapter is showing
└── arrangement // stacked | single (v1: those two only)
The ownsScope flag is the load-bearing field, and it is the one that fixes a real confusion rather than merely rearranging pixels. With two receivers on different bands, the operator must be able to tell at a glance which one the waterfall belongs to — otherwise the trace looks like it belongs to whichever VFO they happen to be reading. Today nothing conveys that, because today there is only ever one.
Where the description lives
The natural home is the radio dossier work (schema v0.1, IC-9700 dossier and validator already exist as a PoC in Aether-gate). A dossier already describes what a radio is; how it presents its receivers is the same kind of fact, and keeping it there means adding a radio is a data change rather than a GUI change.
If dossiers are not ready to carry it, the fallback is RadioCapabilities plus a per-family default — worse, because it puts model knowledge back in code, but it does not block the idea.
Alternatives considered
A — Second panadapter. Rejected on three independent grounds, any one of which is sufficient.
The hardware. One 0x27 0x00 scope stream, so a second panadapter has nothing to render.
The bands. On satellite work the two VFOs are 2 m up and 70 cm down; no single frequency axis shows 145 MHz and 435 MHz at once.
The cost — and this one is measured. Spectrum painting is already the most expensive thing the UI does. #5192 records drawSpectrum's heat-map fill building a QPolygonF trapezoid per FFT column per frame across ~500+ columns, costing ~36.5 ms/frame — 934 ms of every second on the main thread at 25 fps (renderstats: paintsPerSec = 25.58, paintMsPerSec = 933.99). The main thread sat at ~95% paint saturation with DSP and PanadapterStream workers near idle, so that is paint cost, not FFT compute. Under that load the UI tipped into input starvation on any extra work.
A second waterfall means a second spectrum paint path on that same thread. This proposal's one-scope-plus-VFO-state arrangement adds text and a meter — negligible against a 36.5 ms frame.
⚠ Scope of that measurement, stated honestly: #5192 was a Raspberry Pi 5, software-rendered, Wayland/labwc, no GPU paint path. A QRhi GPU path exists (CMakeLists.txt:450) and on a GPU-rendered desktop the budget is very different — the figures above are the software-rendered worst case, not a universal cost. They bound the risk rather than prove it applies everywhere. The argument stands regardless: doubling the most expensive paint path is worth avoiding when the second panadapter buys nothing the hardware can fill.
Worth noting the same issue records a possible radio-side knock-on — while saturated, an IC-9700 RS-BA1 connect failed twice where a FLEX TCP connect succeeded, consistent with the UDP handshake being sensitive to main-thread starvation. #5192 labels that a hypothesis rather than a proven cause, and it is repeated here on the same terms.
B — Leave it; show only the scoped receiver. Cheapest and honest, and strictly better than today's false capability. But it discards a receiver the operator paid for and is actively using on satellite passes. Worth doing if this RFC is declined — the narrow capability fix already gets us there.
C — Hardcode a 9700-specific flag. Fastest to ship, and wrong in the way CERTIFICATION.md §1.14 warns about: a generic tool that bakes in one radio's facts misreports every other radio. It also guarantees the next dual-receiver radio repeats the work.
D — This proposal. Slowest, and the only one where adding a radio is a data change.
Cross-platform impact
No new dependencies, no protocol surface, no backend changes. VfoWidget is shared GUI code across Linux, macOS and Windows, so any layout change must be exercised on all three — I can test Windows, and would need help on the other two. Theme tokens resolve identically on all platforms, so the palette carries no per-platform risk.
The layout is the easy half
Worth stating plainly, because the screenshot above makes this look closer to done than it is. Nothing today can fill that second row from a radio:
IcomCivBackenddecodes the selected VFO only —IcomCivBackend.cpp:2437: "THE SELECTED VFO ONLY. A reply for the unselected one describes a VFO the app does not model.":407says the same for filter widths, "the two unselected slots that the protocol cannot expose."- CI-V
0x25(read frequency of the un/selected VFO) is not implemented anywhere in the tree. sliceId()andpanId()are hardcoded0(IcomCivBackend.h:313-314), so there is no second identity to hang Sub state on.
So making this real means polling the unselected VFO, modelling a second receiver, and finding scheduler room for the extra traffic — on a CI-V path that is already timing-sensitive over RS-BA1 LAN. That is #4840's scope, not a UI change, and it is the part that deserves the design attention. The spike exists to prove the destination is worth the journey, not to suggest the journey is short.
Open questions
- Is the "architecture not appearance" line drawn in the right place? This is the question I most want a maintainer answer on. My reading is that adopting a layout structure is ordinary UI design, and that copying visual design would not be — but that judgement should be the project's, not mine.
- Dossier or capabilities? The dossier is the better home; is that work far enough along to depend on?
- Is
ownsScopeworth doing on its own? Even with no layout change, marking which receiver the waterfall belongs to would fix a real ambiguity. That could ship first and independently. - Scope for v1 — the IC-9700 only, or should the description land with the single-receiver default first so existing radios are provably unchanged before any radio opts in?
Relationship to existing issues
- #4840 — IC-9700 dual VFO / slices for satellite use. This proposal is how the second receiver would be presented; #4840 is the underlying capability.
- The narrow
maxPanadaptersbug (#5347) is a prerequisite in spirit: it stops the UI promising a second panadapter. This RFC then answers "so where does the second receiver go?" - #5192 — software-rendered spectrum heat-map fill saturating the main thread. Supplies the measured paint cost in Alternative A. Not a blocker for this RFC (the proposal avoids a second paint path rather than depending on that work), but if #5192 is fixed the cost argument weakens while the hardware and band arguments stand unchanged.
Implementation scope
A layout spike is pushed at nigelfenton/AetherSDR:spike/dual-vfo-flag-layout (three commits, +810) and produced the screenshot above. It is not proposed for merge as it stands: the second row is env-gated and stub-fed, and the backend work it would need does not exist.
Two of the three commits stand on their own regardless of what happens to this RFC:
cfb32c29—tests/icom_live_subvfo_probe.cpp, the RX-only hardware probe that produced the0x25finding.EXCLUDE_FROM_ALL, not registered withadd_test(), same shape as the existingicom_live_probe/icom_live_civ_probe. Happy to PR it separately as a third live diagnostic if that is wanted.225d4696— records at the call site what0x25actually reads, so the next person to run the probe does not read "not the Sub frequency" as a fault.
6b345994 is the layout spike itself and is the one marked NOT FOR MERGE.
Posting the idea before building the real thing, per GOVERNANCE.md. If the direction is wanted, the order I would suggest is:
- The narrow
maxPanadapterscorrection (filed separately) — stops the UI promising a panadapter that cannot exist. ownsScope/ role marking on its own, if open question 3 says it is worth having independently.- The layout description and its single-receiver default — the change that provably alters nothing for every existing radio.
- The second-receiver data path, which is #4840 and the real work.
Happy to be told this is the wrong order, or that steps 3 and 4 belong to whoever takes #4840.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/gui/VfoWidget.cpp and VfoWidget.{h,cpp} to understand the existing collapsed state, slice wiring, and theme styling. Then read IcomCivBackend.cpp at lines 407 and 2437 and IcomCivBackend.h at lines 313-314, along with the radio dossier and validator PoC. Done means the layout description can represent the receivers and scope owner, existing radios retain their arrangement, and the second row is backed by real radio data rather than AETHER_SPIKE_DUAL_VFO.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- desktop
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100