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
Resubmitted from #4791, which I filed as a PR against docs/ by mistake. GOVERNANCE.md:123 asks for an issue labelled rfc with an [RFC] prefix, and :132 says not to open a PR until that issue is approved — so this is the same document in the right venue, with no docs/ commit attached. It becomes a file only if it is approved here first.
The review on #4791 is already folded in: the §1 model list is corrected (IC-76100x98 and IC-785x0x8E, not the IC-905 — that declares receivers: 1), and §2.5 is new, covering the step that previously had no design. Thanks to @jensenpat for both catches.
Revised 2026-09-01 after further operating on the radio: §1 gains a
four-surface reproduction of the silent refusal, §2.1 and §2.4 are rewritten
(model two pans always, render what the operating mode needs; TX is Main-only,
so there is no ownership to transfer), and §2.6 is new — making TX unmistakable
while watching Sub in full duplex. §1 also records slice add as a fifth
surface — accepted, dropped, reported as success, with no radio involved.
§1 also carries a spike showing the app materialises a second slice from one
backend emit, and that the pan must be announced BEFORE the slice — the
corrected order is proven on hardware, with the concrete signal sequence.
§1 now also measures what a second slice can carry: frequency and mode yes,
spectrum never (one scope), audio only via the analogue EXT-SP SUB jack.
§2.4 is rewritten on that evidence: one panadapter with a MAIN/SUB flag, and
SUB as a READOUT rather than a second slice — a slice promises 78 properties
that a receiver with no separable audio cannot honour. §4 records the IC-7110
(announced 2026-08-29) as a second radio with the same split-receiver shape.
RFC — IC-9700 dual receiver, band ownership, and a radio-shaped RX applet
Status: draft, for discussion. Nothing here is built.
Companion to aetherd-icom-civ-backend-design.md, which describes the CI-V
backend as a whole. This one is about a single structural fact that document
does not yet handle: the IC-9700 has three bands and only two receivers, and IcomCivBackend currently models one.
Evidence throughout is from live IC-9700 hardware — some from a night of testing
against the merged backend (2026-08-05), some from aether-gate's IC-9700
adapter, which has driven this radio since 2026-06-30 and whose findings are
cited with their own dates. Where something is unverified it says so.
1. The problem, concretely
An IC-9700 covers 2 m, 70 cm and 23 cm. It has two receivers, and only one
of them can transmit.
Meanwhile kModels declares receivers: 2, and capabilities() faithfully
publishes maxSlices = 2, maxPanadapters = 2. So the app advertises two
receivers and the backend materialises one. Every symptom below follows from
that gap.
What that produces, observed
Adding 2 m / 440 / 23 cm band buttons (they did not previously exist — the band
grid is an HF Flex grid topping out at 2 m, and even that is gated on a Flex
model-string flag) made the buttons appear correctly. Pressing them does not
tune:
23:14:28.024 MainWindow: switching to band "2m" freq: 144.2 mode: "USB"
23:14:28.025 MainWindow: band switch (no radio band stack) band=2m … freq_mhz=144.200000
… and no CI-V traffic at all
The slice moves to 144.200 in the model; the radio stays on 432; the waterfall
keeps painting 432. The display ends up lying about where the radio is.
The cause is not in AE's dispatch. It is that 25 00 <freq> cannot cross
between receivers. aether-gate found this on hardware and works around it:
"If a MAIN tune is FA'd because the target band is parked on the SUB
receiver, the LAN tuner simply gives up (cross-band tuning is the USB
channel's job); it does NOT swap."
— aether_gate/adapters/icom/icom9700.py
FA is the radio's NAK. The command goes out, the radio refuses it, and nothing
in AE notices — setSliceFrequency is fire-and-forget.
Why "just send a band-select" is the wrong fix
The obvious patch is to issue the receiver swap (07 B0) when the target band
is on the other receiver. That works, and it is a trap:
"⚠ SINGLE-SWAPPER RULE: the LAN channel must NEVER issue 07 B0. The swap is
GLOBAL to the radio (proven 2026-07-03) — two masters swapping (LAN tuner +
USB RX2) collide and tangle MAIN/RX2."
— same file
A swap is not a per-client operation. It re-points the radio for everyone
attached to it. Building band changes on an unconditional swap means every band
press is a global side effect, and two clients doing it collide.
The framing that avoids this: with two receivers and three bands, most band
changes should not swap at all. If both receivers are already open on known
bands, "go to 70 cm" is a slice selection, not a retune. Only a band on
neither receiver needs anything re-pointed — and then exactly one receiver
should move, chosen deliberately.
Still true 2026-09-01 — and it is a class, not a symptom
Re-measured with Sub holding 70 cm. Every one of these reports success and
leaves the radio where it was:
Entry point
Reports
Radio
Band panel 430 button
no error
unmoved
Typed frequency 446.000
no error
unmoved
rigctl F 432100000
RPRT 0
unmoved
automation_probe tune 432.100
ok: true, tune: 432.1
unmoved
The wire, captured immediately after the tune:
86ms tx fe fe a2 e0 05 00 00 00 46 04 fd set 446.000
70ms rx fa refused
(newest-first; the fa at 70 ms answers the 05 at 86 ms). Control: in-band
tunes work, and 23 cm tunes work — only the band Sub holds is refused.
The band panel is worse than a no-op.selectBand()'s non-Flex branch sets
mode first and frequency second (MainWindow_Wiring.cpp:3383). The mode write
succeeds and the frequency write is NAKed, so pressing 430 while on
144.200 USB leaves the radio on 144.200 in FM with a ±5 kHz filter — the 2 m
SSB calling frequency, in the wrong mode, with nothing reported. The audio
changing is what gave it away, not the display.
Reproducing this: civ trace's ring holds ~116 frames spanning about 3.2
seconds, refilled continuously by routine polling. A capture taken after a sleep has already rotated the 05 and its FA out, and the trace then looks
like no frequency command was ever sent.
The same shape appears in slice creation, with no radio involved at all. maxSlices is 2 and slot 1 reads empty, so asking for the second slice is a
legitimate request. It is accepted and dropped:
The chain: slice add → RadioModel::addSlice() → sends the Flex wire text slice create pan=0 freq=… → the Icom backend implements no handler for it →
the command evaporates. AutomationServer (:6914-6923) never checks the
result and returns ok: true unconditionally, reading sliceCount back immediately — before anything could have happened — so it reports the old
count as though it were the outcome.
To its credit the field is requested: true, not created: true: something in
that path knows it only asked. But ok is what a caller checks, so the
distinction does not survive contact.
That is the fifth surface in one afternoon — band button, typed frequency, rigctl, the bridge's tune, and now slice add. Each accepts a request the
backend cannot fulfil and reports success. §1's framing (the app advertises two
receivers and the backend materialises one) turns out to describe a class of
behaviour rather than a single symptom, which is the argument for §3 step 1
being first: making sliceId()/panId() per-receiver is not merely enabling a
feature, it closes a hole where AE accepts work it silently discards.
The HL2 shows what the fix is not.IRadioBackend has no slice-creation
method — every slice verb on it is setSlice*, configuring a slice that already
exists. Hl2Backend therefore does not add receivers, it pre-declares
them: the constructor does m_ids.reset(1); m_rx.assign(1, Receiver{}), and the
comment there says why the state must exist before a radio connects —
"mode, passband and AGC are pushed at the seam BEFORE a radio is connected —
RadioModel does it, and so does anything restoring a session. With no receiver
to hold them those calls would be silently dropped and the radio would come up
on defaults instead."
The real count then comes from the radio (discovery byte 0x13), with buildReceivers() constructing the DSP chains and setReceiverCount restarting
the EP6 stream. Receiver count is a property of the hardware, settled at
connect — not something a user adds afterwards.
So the Icom work is not "implement slice create". It is exactly what §2.1
already proposes: probe at connect and materialise both slices then, the way the
HL2 materialises its receivers from discovery. slice add should then be refused on these radios rather than silently dropped, because two is the
number the hardware has and the operator cannot change it.
Spiked 2026-09-01: the app half already works, and the ORDER matters
Before anyone touches ~31 emit sites, it was worth knowing whether the app would
cooperate. Throwaway build, one line: emit sliceChanged(1, delta) from onSessionConnected() for a slice id the Icom backend has never used. No CI-V
sent — slice 1 was a fiction in the model only, so the radio was never told
anything and could not be retuned or keyed.
RadioModel's non-Flex handler did exactly what its comment promises
("Materialise it on the first delta"), including allocating a distinct pan
id through neutralPanIndexFor(). Nothing downstream fought back. So the ~31
sites are the whole job — whoever does step 1 is not also gambling on the app.
But panCount stayed 1, and the UI showed no slice. Instead:
Panadapter capacity is full (IC-9700 supports 2 panadapters)
That message is the useful part. The invented pan id reserved a slot without
creating a panadapter, so when AE went to build one the guard at MainWindow.cpp:10219 — m_panStack->count() + toCreate > maxPanadapters() —
refused. The slice existed in the model, pointing at a panadapter that was never
built, with nowhere to be drawn.
So slice and pan materialisation are separate paths, and the order is part of
the contract: announce the panadapter first, then the slice that lives on
it. Doing it the other way round reserves capacity against a pan that does not
exist and locks out the real one — a failure that looks like a capacity limit
rather than an ordering mistake, which is why it is worth writing down here
rather than rediscovering it at step 1.
This also refines §3 step 1: it is not only "make sliceId()/panId()
per-receiver". It is "announce a second pan, then a second slice on it",
with the receiver count from §2.1's probe deciding whether to do so at all.
The order, proven — pan first, then the slice on it
A second spike with the order corrected. Same fiction (no CI-V, receiver 1 exists
only in the model), one change: announce the pan before the slice.
emit panCenterBandwidthChanged("1", 432.100, 0.2); // 1. materialise the pan
emit sliceChanged(1, {freq 432.100, mode USB, panId "1"}); // 2. slice on it
Result on the live IC-9700 — two panadapters, two slices, both drawn:
Slice A shows the real 2 m waterfall with band segments; Slice B gets its own
70 cm panadapter, correctly segmented. Identical slice emit to the first spike —
only the pan announcement moved ahead of it, and that is the whole difference
between a capacity error and a working two-pan layout.
There is no panAdded signal.panCenterBandwidthChanged is what
materialises a pan (RadioModel.cpp:841, non-Flex path), which is why this is
worth stating rather than leaving to be inferred.
Hl2Backend::onConnected() already encodes the rule, and says why:
emitAllPanState(); // "the pans must exist before anything describes them"pushInitialState();
emitAllSliceState();
"Only emitPanState()'s panCenterBandwidthChanged materialises our pans. So emitAllPanState() has to come FIRST…"
Getting it backwards is not cosmetic. panBandwidthLimitsChanged does if (!pan) return; with no materialisation, so a description arriving before its
pan is dropped for the whole session with nothing to re-emit it — that was #4470's black-bar over-zoom, where SpectrumWidget fell back to a 5.4 MHz window,
fourteen times this receiver's widest.
So §3 step 1's sequence is now concrete rather than sketched:
probe the receiver count (§2.1);
panCenterBandwidthChanged for each receiver's pan;
everything describing those pans — limits, gain, range;
sliceChanged per receiver, carrying the matching panId.
What the spike does NOT prove. Slice B's waterfall is empty: no CI-V was sent
and no IQ flows for a receiver that does not really exist. This establishes the
plumbing and the ordering contract, not that a second receiver works — that still
needs the ~31 emit sites made per-receiver and a real Sub behind them.
What a second slice can actually CARRY — measured 2026-09-01
The plumbing works, so the next question is what there is to put in it. Every row
below was measured on the radio today rather than reasoned about, because the
answer turned out not to be the obvious one.
Slice B needs
Available
How it was established
Frequency, mode
yes
07 B0 → 03 → 07 B0 back. Main restored identically both times
Spectrum
no
see below
Audio over LAN
no
single mono LPCM16 stream, one negotiated audioport
Audio over USB
no
two channels, bit-identical
Audio over EXT-SP SUB
yes, but analogue
separate rear-panel jack
The scope is singular, and that is a front-panel fact. The radio itself only
offers scope on MAIN or SUB — not both. That is why 27 00 sweeps carry no
receiver identifier: there is only ever one source. It also explains
aether-gate's hardware note from 2026-07-03, that a swap "briefly yanks the live
scope to RX2 and back". No amount of CI-V cleverness produces two spectra,
because the radio has one scope engine.
USB audio is mono duplicated, proven under the strongest available asymmetry.
With dual-watch ON, MAIN on 435.900 reading S-meter 00 00 (silent) and SUB on
145.900 reading 00 67 (actively receiving) — the case where split channels
would be unmistakable:
L rms=0.009656 peak=0.046133
R rms=0.009656 peak=0.046133
|L-R| max = 0.0000000000 across 240,000 samples
Bit-identical with one receiver loud and the other silent. The two channels are
one signal duplicated, not two receivers. (An earlier capture with dual-watch OFF
showed the same thing but proved nothing — SUB was producing no audio, so
identical channels were the only possible result. Worth recording as the trap it
was.) Dual-watch was restored to OFF afterwards; the radio was never keyed.
But the separation EXISTS — Icom simply does not expose it digitally. The
rear panel has two external speaker jacks: External Speaker Jack MAIN and External Speaker Jack SUB. So this is not a hardware limitation the radio
cannot overcome; it is a choice about what reaches the LAN and USB paths.
That leaves one honest route to real SUB audio — wire EXT-SP SUB into a
soundcard. Not something AE can arrange by itself, and out of scope here, but
worth stating because it changes what a second slice could eventually be.
So the shape of a second slice on this radio is:
frequency, mode, and — only with an operator-wired analogue feed — audio. Never its own spectrum.
This settles §2.4's cross-band-repeat case, which speculated that both pans might
be wanted: it is not achievable, and the reason is a single scope rather than
a design preference. One panadapter, following whichever receiver the operator
selects, with the MAIN/SUB flag saying which — that is not one option among
several, it is the only honest rendering the hardware permits.
2. Proposal
2.1 Probe the receiver count at connect
aether-gate already does this, proven on hardware:
07 B0 swap MAIN <-> SUB
25 00 / 26 00 read the now-selected receiver's freq + mode
07 B0 swap back
"Sets rx2_present True iff RX2 read a real ham freq on a DIFFERENT band than
MAIN (a genuine second receiver, not RX1's own VFO B)."
The different-band test is the load-bearing part: without it, VFO B on the same
band reads as a second receiver.
Model one pan or two accordingly — one receiver → one pan, exactly as today;
two → two pans, each seeded with the frequency and mode its receiver reports.
The probe decides what exists, not what is drawn. Modelling both from the
start is the load-bearing half: a backend reporting receivers = 1 is precisely
what makes Sub unreachable, unlabelable and invisible (§1), and every feature
built on a single pan is another thing to unpick later. What gets rendered is
§2.4's question, and the answer is not always "both".
⚠ Probe once, at connect, and never on a timer. The swap briefly yanks the
live scope to the other receiver and back; gate's notes are explicit that this
must be on-demand only, "NEVER on the periodic poll, because the swap briefly
yanks the live scope to RX2 and back (would worsen the scope-stall)." At
connect the scope is not yet up, which is the one moment the disturbance costs
nothing.
2.2 TX ownership follows the existing Flex model
No new concept needed. A Flex already has exactly one TX slice among several,
and AE already models that. The 9700's constraint — only MAIN transmits — maps
onto the existing TX-slice flag directly.
The rule this RFC asks for explicitly:
A band change never moves the TX receiver implicitly. If the requested
band is on neither receiver, the NON-TX receiver is the one that moves. Moving
the TX receiver is an explicit operator action with its own affordance.
This is a safety property, not a preference. A band button that can silently
re-point the transmitting receiver is a band button that can leave the operator
keying on a band they are no longer looking at.
2.3 Band selection is per-pan, and offers only what is free
Right-click a pan → choose its band. The menu offers only bands not currently
held by the other receiver, because the radio cannot put both receivers on the
same band and there is no reason to offer an action that will fail.
This is better than the band-button row for this radio: it puts the choice where
the constraint actually lives (per receiver) instead of implying three
independent bands. The band buttons remain meaningful for the active pan.
2.4 One panadapter, and SUB as a READOUT rather than a slice
Earlier drafts of this section asked which of two pans to show. §1 has since
measured the answer: there is only one to show. The radio scopes MAIN or
SUB, never both, so the question of rendering two panadapters does not arise —
including for cross-band repeat, which an earlier draft guessed might want them.
One panadapter, following whichever receiver the operator selects, with a MAIN/SUB flag saying which. Not one option among several — the only
rendering the hardware permits.
And SUB should be a readout, not a second slice. This is a change from the
rest of this RFC, and it is the cheaper and more honest design.
A SliceModel is a large promise. It carries 78 setters/properties, and 20 files under src/gui/ reach into slices. Most of that promise is DSP over
an audio stream: AudioGain, AudioPan, AudioMute, FilterWidth, the whole Adaptive* family, Esc*, Diversity, RfGain, RxAntenna. §1 measured that
SUB has no separable audio on any path AE can reach — the LAN stream is one
mono LPCM16 mix, and the USB CODEC's two channels are bit-identical. So a SUB
slice would advertise dozens of controls that cannot do anything, on a signal
that does not exist as a separate stream.
That is the same failure this whole RFC is about, rebuilt one level up. §1
records five surfaces where AE accepts a request the backend cannot honour and
reports success. A second slice whose filter, gain, mute and antenna controls
are all inert would be the sixth — and this time we would have built it
deliberately.
Six sites already iterate every slice and act on each. Each becomes a question
of "does this one really work?" the moment a slice exists that mostly does not.
What a SUB readout is instead:
SUB's frequency and mode, from the §2.1 swap-read
the MAIN/SUB flag for the panadapter, saying which receiver is on screen
a control to move the scope between receivers
nothing else — no filter, no gain, no mute, because there is no stream to
apply them to
It needs no sliceId()/panId() rework across ~31 emit sites, no second
panadapter, no per-slice audio routing. It is a small amount of new UI over state
§2.1 already has to fetch.
Staleness is part of the contract. The swap-read is not free: aether-gate's
hardware note (2026-07-03) is explicit that the swap "briefly yanks the live
scope to RX2 and back", which is why it is done on demand and never on a
poll. So SUB's frequency is last known, not live — refreshed at connect and
when the operator does something that changes it. The readout should say so
rather than imply a live number: on a satellite pass where SUB is being tuned for
Doppler, a confidently-wrong frequency is worse than an obviously-stale one.
If SUB ever gains real audio, revisit this. The rear panel has separate External Speaker Jack MAIN and SUB outputs, so the separation exists in
hardware and Icom has simply not exposed it digitally (§1). An operator who wires
EXT-SP SUB into a soundcard has a genuine second audio stream, and at that point
a real second slice earns its promise. Until then, a readout is what the radio
can actually back.
A second-receiver applet is still worth having — a compact panel laid out
like the radio's own sub-band controls, showing SUB's frequency and mode and
carrying the scope-selection control. AppletPanel already hosts 35+ applets, so
this is a conventional addition rather than new infrastructure. It is the readout
with a home, not a slice in disguise.
TX is Main-only, so there is no ownership to transfer. An earlier draft
proposed "an explicit, clearly-marked TX-ownership control". On this radio that
is the wrong shape — only MAIN can transmit, and a TX control on SUB would be an
affordance for something the hardware will not do, the same category of mistake as
offering a band the other receiver already holds (§2.3).
the flag says which receiver you are watching
the TX button lives on Main, always, whichever receiver is displayed
switching the display therefore cannot move TX, because TX was never tied
to it
That dissolves the concern this section was originally written around — that
coupling what you are looking at to what will transmit means changing the
screen changes what is on the air. Decoupled, it cannot. §2.2's safety rule gets
easier to honour too: a band change never implicitly moves the TX receiver,
because the TX receiver never moves at all.
2.5 Cross-band routing, and hearing the radio say no
The one step with on-air consequence, so it gets stated rather than implied.
Today the refusal is invisible.setSliceFrequency() sends 25 00 <freq>
and returns; nothing correlates a reply. When the radio NAKs a cross-band tune,
the model has already moved optimistically, so the display shows a frequency the
radio is not on — which is precisely the failure observed, and the reason it
took a night to attribute (§1).
The mechanism is already in the tree and simply unread.CivCodec.h defines
both kCivOk (0xFB) and kCivNg (0xFA), and CivFrame already exposes isOk() / isNg(). Nothing calls isNg(). So step 3 is smaller than it looks:
the decode exists, the correlation does not.
Proposed shape:
Read the NAK. On isNg() for an outstanding tune, do NOT advance the
model — re-assert the radio's actual frequency instead, the same way setPanCenter() already re-asserts when it refuses a drag. A refused tune
must leave the display where the radio is, never where the operator asked.
Decide the target receiver BEFORE sending. With both receivers' bands
known from §2.1, "which receiver should hold this band" is answerable without
asking the radio: if a receiver already holds the band, select it (no swap, no
CI-V tune at all); if neither does, the non-TX receiver is re-pointed
(§2.2). The NAK then stops being the routing mechanism and becomes what it
should be — a backstop for a state that disagrees with our model.
Serialise the swap. Whatever issues 07 B0 must be the only thing doing
so within a session, and must not run while keyed (§4 Q3). Under aetherd this
is a shared mutation, per §5.
Why not just NAK-and-retry-with-a-swap: it makes every band change a global
radio side effect discovered by trial, on a radio a second client may also be
driving. Deciding from known state and treating the NAK as the exception inverts
that — the common case sends nothing at all.
2.6 Make TX unmistakable while watching Sub
Full duplex means the normal state during a pass is watching Sub while Main is
transmitting. That is exactly when a quiet label is not enough: an operator
looking at a still Sub waterfall must never be able to forget the other half is
on the air.
Proposal: the frequency display turns red while transmitting.
Red is already this project's TX colour rather than a new invention — color.slice.tx resolves to {color.red.500}, and color.highlight.tx and color.tx.mox.* are established tokens. Applying the existing convention
inherits theme support and stays on the hardcoded-colour ratchet's right side.
The property that matters is not the colour but the guarantee: transmit state
is visible on whichever receiver you are looking at, not only on the one that
is transmitting.
3. Work implied
Deliberately ordered so nothing on-air changes until the groundwork is proven.
Reordered after §2.4 became a readout rather than a slice: most of the value
arrives before the expensive step, not after it.
Know both receivers' bands (§2.1's connect probe). Cache SUB's frequency
and mode from the swap-read. No UI, no new slice — just state the backend does
not currently hold. Everything below depends on it and nothing else does.
Route from known state (§2.5 step 2). With step 1's cache, "which receiver
should hold this band" is answerable before sending, so the NAK becomes a
backstop rather than the routing mechanism. This is what makes the band button
and a typed cross-band frequency actually work.
The SUB readout and the MAIN/SUB flag (§2.4) — SUB's frequency and mode,
which receiver the panadapter is following, and a control to move the scope
between them. Small: it is new UI over state step 1 already fetched.
Per-pan band menu (§2.3), offering only what is free.
TX visible on whichever receiver is displayed (§2.6). Small, and worth
landing with step 4 — watching SUB while MAIN transmits is what step 4 makes
possible.
Per-receiver sliceId()/panId() — deferred, and now optional. This was
step 1 in earlier drafts: ~20 call sites in IcomCivBackend.cpp, several from
CI-V handlers where the answer depends on which receiver was selected when the
frame arrived. §2.4 explains why it is no longer the entry price: a SUB slice
would promise 78 properties the radio cannot back. Revisit it if SUB gains a
real audio stream (an operator wiring EXT-SP SUB into a soundcard), because
then a second slice earns its promise.
Steps 1–3 are worth doing on their own merits and are most of the operator-visible
fix: they stop AE accepting requests it cannot honour — five distinct surfaces
measured 2026-09-01 (§1), all reporting success — and make a cross-band band
change work. Whatever else is deferred, that gap should not be. The expensive
step is now last and may never be needed.
4. Open questions
Coexistence with aether-gate. The gate currently owns the swap on its USB
channel precisely because the swap is global. If AE natively drives two
receivers, what happens when both are attached? Options: document
"don't run both", have AE detect the gate, or leave AE swap-free and let it
only select among receivers the operator has already set up. The last is
the most conservative and may be enough for most operating.
Anything else sharing this shape is ANSWERED, and it is not one radio.
Three models in kModels declare receivers: 2 — the IC-9700 (0xA2),
the IC-7610 (0x98) and the IC-785x (0x8E). All three get one
materialised receiver today, so the mismatch in §1 is theirs too, not the
IC-9700's alone.
(An earlier draft of this RFC named the IC-905 here. That was wrong — it
declares receivers: 1 and does not share the shape. Corrected rather than
quietly dropped, because it is the exact error the rest of this document
argues against: inferring a capability from what a radio is known for
rather than reading what the table declares.)
What differs between the three is only the reason for the second receiver
— bands on separate hardware for the IC-9700, diversity/dual-watch on the
HF radios — and that difference belongs in how bands are offered (§2.3), not
in whether both receivers exist. So this should be driven by receivers,
which IcomModel already carries, and not special-cased per model.
And the shape is not historical — Icom shipped another one last week. The IC-7110 was announced at the Tokyo Ham Fair on 2026-08-29 as the
IC-7100 successor, and Icom's own material describes "independent receive
circuits for the HF/6M and the 2m/70cm bands" with dualwatch reception. That
is the IC-9700's architecture again: two receivers split across separate band
groups, rather than the HF radios' same-band diversity. So the expensive half
of this RFC — §2.5's routing, which exists because a receiver cannot take a
band the other one's hardware covers — has a second radio to justify it, four
days after this issue was last revised.
Usefully, nothing new is needed to accommodate it.setSliceFrequency()'s
guard is already predicated on an empty band table:
"Only a model that DECLARES discontinuous bands gets this gate. Every other
Icom keeps its existing command path — an empty table is the predicate, so
the day another model's holes are documented, this site and setPanCenter()
below light up together rather than one at a time."
Today only the IC-9700 declares bands (2m,440,23cm); the IC-7610 and
IC-785x declare none. A radio whose contention is documented gets a table and
the machinery activates.
What is NOT known, and should not be guessed at:
the IC-7110's CI-V address, or whether its second receiver is reachable the
same way — nobody has one yet;
whether "independent receive circuits" actually implies contention. The
9700 cannot have both receivers on the same band group; the 7110 might
genuinely allow one HF and one VHF simultaneously with no conflict, which
would be better and would mean §2.5 does not apply to it;
whether the IC-7610/785x need band tables at all — that depends on their
front-end architecture, which this RFC has not established.
Every one of those is a question for someone with the radio. Recording them as
questions rather than assumptions is the same discipline the IC-905 correction
above exists to enforce.
Is the swap safe during TX? Not tested. Until it is, the answer should be
"no swap while keyed", enforced rather than assumed.
What does the scope do across a swap? Gate's notes say it yanks and
returns. With two pans, does the second pan get scope data at all, or is the
scope MAIN-only? This gates whether pan 2 is a waterfall or a frequency
readout. Needs the radio.
Question 4 is the one that most changes the design, and it is an hour on the
bench.
In split V/UHF, does the displayed pan follow RX automatically, or does
the operator choose? Automatic matches the pass — you want the downlink.
Manual never surprises you by switching mid-pass. Leaning automatic with an
override, but this is an operating judgement rather than a design one.
Is cross-band repeat a first-class mode, or a capability not to design
out? It is the one case where rendering both pans is clearly right
(§2.4), so the answer changes whether dual render is a mode with its own
affordance or simply something the model permits.
5. Relationship to aetherd and "one AE, many radios"
Checked deliberately, because a proposal that assumes one radio would be the
wrong shape against a design where several are normal.
It fits, and mostly for free.docs/aetherd-headless-engine-design.md §5 already
establishes per-radio RadioSession with the protocol namespaced by session id, "so one daemon can host several radios — of the same or different families —
concurrently." Everything in this RFC lives inside one session: two
receivers of one IC-9700, not two radios. Nothing here reaches across sessions,
so the multi-radio story is unaffected either way.
The staged plan's step 2 (the IRadioBackend seam) has landed, and its own note
says "from this point a new radio family is a new backend — shippable in-process
in the desktop app immediately, and served through the daemon automatically once
the later steps land." Work here is behind that seam, so it inherits the daemon
for free rather than needing porting later.
Three places the alignment is load-bearing rather than incidental:
§2.1's connect-time probe is per-session state, not global. Two IC-9700s on
one daemon each probe their own receivers. Nothing in the probe reads or writes
anything session-external — worth stating because the 07 B0 swap is global
to its radio, which makes "global" an easy word to get wrong here. It is global
to that radio, not to the engine.
§2.2's TX rule strengthens under multi-client. §6 of the aetherd RFC makes
TX arbitration engine-side and non-negotiable (Principle VI). "A band change
never moves the TX receiver implicitly" is the same principle one level down:
with several clients projecting one session, an implicit TX move would be
invisible to every client that did not initiate it.
§2.4's applet is client-side. Under aetherd it is a projection, so a second
client would render its own. The TX-ownership control it carries is a request
to the engine, arbitrated there — not a local mutation. That is the correct
split already and needs no rework.
The one open question multi-radio adds to §4's list: the single-swapper rule
is currently about AE-vs-aether-gate, but under a daemon with several clients it
generalises. If two clients on one session both request a band that needs a swap,
the swap must serialise at the engine — the same shared-mutation question §5 of
the aetherd RFC already flags for pan/slice creation. Recommend it be treated as
a shared mutation, announced to all clients, exactly as that section proposes.
6. What this does not propose
No change to TX behaviour beyond making ownership explicit. Nothing here keys
a radio, and step 3 should land with TX untouched.
No transverter/XVTR interaction. The gate publishes 70 cm and 23 cm as XVTR
bands because AE had no native buttons; with native bands that workaround
should become unnecessary, but removing it is out of scope here.
No claim that the MOD Input / TX-audio path works. That is unresolved
separately (the 1A 05 menu item numbers are model-specific and unverified on
this model).
RFC — IC-9700 dual receiver, band ownership, and a radio-shaped RX applet
Status: draft, for discussion. Nothing here is built.
Companion to
aetherd-icom-civ-backend-design.md, which describes the CI-Vbackend as a whole. This one is about a single structural fact that document
does not yet handle: the IC-9700 has three bands and only two receivers, and
IcomCivBackendcurrently models one.Evidence throughout is from live IC-9700 hardware — some from a night of testing
against the merged backend (2026-08-05), some from
aether-gate's IC-9700adapter, which has driven this radio since 2026-06-30 and whose findings are
cited with their own dates. Where something is unverified it says so.
1. The problem, concretely
An IC-9700 covers 2 m, 70 cm and 23 cm. It has two receivers, and only one
of them can transmit.
IcomCivBackendtoday hardcodes a single receiver:Meanwhile
kModelsdeclaresreceivers: 2, andcapabilities()faithfullypublishes
maxSlices = 2,maxPanadapters = 2. So the app advertises tworeceivers and the backend materialises one. Every symptom below follows from
that gap.
What that produces, observed
Adding 2 m / 440 / 23 cm band buttons (they did not previously exist — the band
grid is an HF Flex grid topping out at 2 m, and even that is gated on a Flex
model-string flag) made the buttons appear correctly. Pressing them does not
tune:
The slice moves to 144.200 in the model; the radio stays on 432; the waterfall
keeps painting 432. The display ends up lying about where the radio is.
The cause is not in AE's dispatch. It is that
25 00 <freq>cannot crossbetween receivers. aether-gate found this on hardware and works around it:
FAis the radio's NAK. The command goes out, the radio refuses it, and nothingin AE notices —
setSliceFrequencyis fire-and-forget.Why "just send a band-select" is the wrong fix
The obvious patch is to issue the receiver swap (
07 B0) when the target bandis on the other receiver. That works, and it is a trap:
A swap is not a per-client operation. It re-points the radio for everyone
attached to it. Building band changes on an unconditional swap means every band
press is a global side effect, and two clients doing it collide.
The framing that avoids this: with two receivers and three bands, most band
changes should not swap at all. If both receivers are already open on known
bands, "go to 70 cm" is a slice selection, not a retune. Only a band on
neither receiver needs anything re-pointed — and then exactly one receiver
should move, chosen deliberately.
Still true 2026-09-01 — and it is a class, not a symptom
Re-measured with Sub holding 70 cm. Every one of these reports success and
leaves the radio where it was:
rigctl F 432100000RPRT 0automation_probe tune 432.100ok: true, tune: 432.1The wire, captured immediately after the tune:
(newest-first; the
faat 70 ms answers the05at 86 ms). Control: in-bandtunes work, and 23 cm tunes work — only the band Sub holds is refused.
The band panel is worse than a no-op.
selectBand()'s non-Flex branch setsmode first and frequency second (
MainWindow_Wiring.cpp:3383). The mode writesucceeds and the frequency write is NAKed, so pressing 430 while on
144.200 USB leaves the radio on 144.200 in FM with a ±5 kHz filter — the 2 m
SSB calling frequency, in the wrong mode, with nothing reported. The audio
changing is what gave it away, not the display.
The same shape appears in slice creation, with no radio involved at all.
maxSlicesis 2 and slot 1 readsempty, so asking for the second slice is alegitimate request. It is accepted and dropped:
The chain:
slice add→RadioModel::addSlice()→ sends the Flex wire textslice create pan=0 freq=…→ the Icom backend implements no handler for it →the command evaporates.
AutomationServer(:6914-6923) never checks theresult and returns
ok: trueunconditionally, readingsliceCountbackimmediately — before anything could have happened — so it reports the old
count as though it were the outcome.
To its credit the field is
requested: true, notcreated: true: something inthat path knows it only asked. But
okis what a caller checks, so thedistinction does not survive contact.
That is the fifth surface in one afternoon — band button, typed frequency,
rigctl, the bridge'stune, and nowslice add. Each accepts a request thebackend cannot fulfil and reports success. §1's framing (the app advertises two
receivers and the backend materialises one) turns out to describe a class of
behaviour rather than a single symptom, which is the argument for §3 step 1
being first: making
sliceId()/panId()per-receiver is not merely enabling afeature, it closes a hole where AE accepts work it silently discards.
The HL2 shows what the fix is not.
IRadioBackendhas no slice-creationmethod — every slice verb on it is
setSlice*, configuring a slice that alreadyexists.
Hl2Backendtherefore does not add receivers, it pre-declaresthem: the constructor does
m_ids.reset(1); m_rx.assign(1, Receiver{}), and thecomment there says why the state must exist before a radio connects —
The real count then comes from the radio (discovery byte
0x13), withbuildReceivers()constructing the DSP chains andsetReceiverCountrestartingthe EP6 stream. Receiver count is a property of the hardware, settled at
connect — not something a user adds afterwards.
So the Icom work is not "implement
slice create". It is exactly what §2.1already proposes: probe at connect and materialise both slices then, the way the
HL2 materialises its receivers from discovery.
slice addshould then berefused on these radios rather than silently dropped, because two is the
number the hardware has and the operator cannot change it.
Spiked 2026-09-01: the app half already works, and the ORDER matters
Before anyone touches ~31 emit sites, it was worth knowing whether the app would
cooperate. Throwaway build, one line: emit
sliceChanged(1, delta)fromonSessionConnected()for a slice id the Icom backend has never used. No CI-Vsent — slice 1 was a fiction in the model only, so the radio was never told
anything and could not be retuned or keyed.
AE materialised it immediately:
RadioModel's non-Flex handler did exactly what its comment promises("Materialise it on the first delta"), including allocating a distinct pan
id through
neutralPanIndexFor(). Nothing downstream fought back. So the ~31sites are the whole job — whoever does step 1 is not also gambling on the app.
But
panCountstayed 1, and the UI showed no slice. Instead:That message is the useful part. The invented pan id reserved a slot without
creating a panadapter, so when AE went to build one the guard at
MainWindow.cpp:10219—m_panStack->count() + toCreate > maxPanadapters()—refused. The slice existed in the model, pointing at a panadapter that was never
built, with nowhere to be drawn.
So slice and pan materialisation are separate paths, and the order is part of
the contract: announce the panadapter first, then the slice that lives on
it. Doing it the other way round reserves capacity against a pan that does not
exist and locks out the real one — a failure that looks like a capacity limit
rather than an ordering mistake, which is why it is worth writing down here
rather than rediscovering it at step 1.
This also refines §3 step 1: it is not only "make
sliceId()/panId()per-receiver". It is "announce a second pan, then a second slice on it",
with the receiver count from §2.1's probe deciding whether to do so at all.
The order, proven — pan first, then the slice on it
A second spike with the order corrected. Same fiction (no CI-V, receiver 1 exists
only in the model), one change: announce the pan before the slice.
Result on the live IC-9700 — two panadapters, two slices, both drawn:
Slice A shows the real 2 m waterfall with band segments; Slice B gets its own
70 cm panadapter, correctly segmented. Identical slice emit to the first spike —
only the pan announcement moved ahead of it, and that is the whole difference
between a capacity error and a working two-pan layout.
There is no
panAddedsignal.panCenterBandwidthChangedis whatmaterialises a pan (
RadioModel.cpp:841, non-Flex path), which is why this isworth stating rather than leaving to be inferred.
Hl2Backend::onConnected()already encodes the rule, and says why:Getting it backwards is not cosmetic.
panBandwidthLimitsChangeddoesif (!pan) return;with no materialisation, so a description arriving before itspan is dropped for the whole session with nothing to re-emit it — that was
#4470's black-bar over-zoom, where SpectrumWidget fell back to a 5.4 MHz window,
fourteen times this receiver's widest.
So §3 step 1's sequence is now concrete rather than sketched:
panCenterBandwidthChangedfor each receiver's pan;sliceChangedper receiver, carrying the matchingpanId.What the spike does NOT prove. Slice B's waterfall is empty: no CI-V was sent
and no IQ flows for a receiver that does not really exist. This establishes the
plumbing and the ordering contract, not that a second receiver works — that still
needs the ~31 emit sites made per-receiver and a real Sub behind them.
What a second slice can actually CARRY — measured 2026-09-01
The plumbing works, so the next question is what there is to put in it. Every row
below was measured on the radio today rather than reasoned about, because the
answer turned out not to be the obvious one.
07 B0→03→07 B0back. Main restored identically both timesaudioportThe scope is singular, and that is a front-panel fact. The radio itself only
offers scope on MAIN or SUB — not both. That is why
27 00sweeps carry noreceiver identifier: there is only ever one source. It also explains
aether-gate's hardware note from 2026-07-03, that a swap "briefly yanks the live
scope to RX2 and back". No amount of CI-V cleverness produces two spectra,
because the radio has one scope engine.
USB audio is mono duplicated, proven under the strongest available asymmetry.
With dual-watch ON, MAIN on 435.900 reading S-meter
00 00(silent) and SUB on145.900 reading
00 67(actively receiving) — the case where split channelswould be unmistakable:
Bit-identical with one receiver loud and the other silent. The two channels are
one signal duplicated, not two receivers. (An earlier capture with dual-watch OFF
showed the same thing but proved nothing — SUB was producing no audio, so
identical channels were the only possible result. Worth recording as the trap it
was.) Dual-watch was restored to OFF afterwards; the radio was never keyed.
But the separation EXISTS — Icom simply does not expose it digitally. The
rear panel has two external speaker jacks: External Speaker Jack MAIN and
External Speaker Jack SUB. So this is not a hardware limitation the radio
cannot overcome; it is a choice about what reaches the LAN and USB paths.
That leaves one honest route to real SUB audio — wire EXT-SP SUB into a
soundcard. Not something AE can arrange by itself, and out of scope here, but
worth stating because it changes what a second slice could eventually be.
So the shape of a second slice on this radio is:
This settles §2.4's cross-band-repeat case, which speculated that both pans might
be wanted: it is not achievable, and the reason is a single scope rather than
a design preference. One panadapter, following whichever receiver the operator
selects, with the MAIN/SUB flag saying which — that is not one option among
several, it is the only honest rendering the hardware permits.
2. Proposal
2.1 Probe the receiver count at connect
aether-gate already does this, proven on hardware:
The different-band test is the load-bearing part: without it, VFO B on the same
band reads as a second receiver.
Model one pan or two accordingly — one receiver → one pan, exactly as today;
two → two pans, each seeded with the frequency and mode its receiver reports.
The probe decides what exists, not what is drawn. Modelling both from the
start is the load-bearing half: a backend reporting
receivers = 1is preciselywhat makes Sub unreachable, unlabelable and invisible (§1), and every feature
built on a single pan is another thing to unpick later. What gets rendered is
§2.4's question, and the answer is not always "both".
⚠ Probe once, at connect, and never on a timer. The swap briefly yanks the
live scope to the other receiver and back; gate's notes are explicit that this
must be on-demand only, "NEVER on the periodic poll, because the swap briefly
yanks the live scope to RX2 and back (would worsen the scope-stall)." At
connect the scope is not yet up, which is the one moment the disturbance costs
nothing.
2.2 TX ownership follows the existing Flex model
No new concept needed. A Flex already has exactly one TX slice among several,
and AE already models that. The 9700's constraint — only MAIN transmits — maps
onto the existing TX-slice flag directly.
The rule this RFC asks for explicitly:
This is a safety property, not a preference. A band button that can silently
re-point the transmitting receiver is a band button that can leave the operator
keying on a band they are no longer looking at.
2.3 Band selection is per-pan, and offers only what is free
Right-click a pan → choose its band. The menu offers only bands not currently
held by the other receiver, because the radio cannot put both receivers on the
same band and there is no reason to offer an action that will fail.
This is better than the band-button row for this radio: it puts the choice where
the constraint actually lives (per receiver) instead of implying three
independent bands. The band buttons remain meaningful for the active pan.
2.4 One panadapter, and SUB as a READOUT rather than a slice
Earlier drafts of this section asked which of two pans to show. §1 has since
measured the answer: there is only one to show. The radio scopes MAIN or
SUB, never both, so the question of rendering two panadapters does not arise —
including for cross-band repeat, which an earlier draft guessed might want them.
One panadapter, following whichever receiver the operator selects, with a
MAIN/SUB flag saying which. Not one option among several — the only
rendering the hardware permits.
And SUB should be a readout, not a second slice. This is a change from the
rest of this RFC, and it is the cheaper and more honest design.
A
SliceModelis a large promise. It carries 78 setters/properties, and20 files under
src/gui/reach into slices. Most of that promise is DSP overan audio stream:
AudioGain,AudioPan,AudioMute,FilterWidth, the wholeAdaptive*family,Esc*,Diversity,RfGain,RxAntenna. §1 measured thatSUB has no separable audio on any path AE can reach — the LAN stream is one
mono LPCM16 mix, and the USB CODEC's two channels are bit-identical. So a SUB
slice would advertise dozens of controls that cannot do anything, on a signal
that does not exist as a separate stream.
That is the same failure this whole RFC is about, rebuilt one level up. §1
records five surfaces where AE accepts a request the backend cannot honour and
reports success. A second slice whose filter, gain, mute and antenna controls
are all inert would be the sixth — and this time we would have built it
deliberately.
Six sites already iterate every slice and act on each. Each becomes a question
of "does this one really work?" the moment a slice exists that mostly does not.
What a SUB readout is instead:
apply them to
It needs no
sliceId()/panId()rework across ~31 emit sites, no secondpanadapter, no per-slice audio routing. It is a small amount of new UI over state
§2.1 already has to fetch.
Staleness is part of the contract. The swap-read is not free: aether-gate's
hardware note (2026-07-03) is explicit that the swap "briefly yanks the live
scope to RX2 and back", which is why it is done on demand and never on a
poll. So SUB's frequency is last known, not live — refreshed at connect and
when the operator does something that changes it. The readout should say so
rather than imply a live number: on a satellite pass where SUB is being tuned for
Doppler, a confidently-wrong frequency is worse than an obviously-stale one.
If SUB ever gains real audio, revisit this. The rear panel has separate
External Speaker Jack MAIN and SUB outputs, so the separation exists in
hardware and Icom has simply not exposed it digitally (§1). An operator who wires
EXT-SP SUB into a soundcard has a genuine second audio stream, and at that point
a real second slice earns its promise. Until then, a readout is what the radio
can actually back.
A second-receiver applet is still worth having — a compact panel laid out
like the radio's own sub-band controls, showing SUB's frequency and mode and
carrying the scope-selection control.
AppletPanelalready hosts 35+ applets, sothis is a conventional addition rather than new infrastructure. It is the readout
with a home, not a slice in disguise.
TX is Main-only, so there is no ownership to transfer. An earlier draft
proposed "an explicit, clearly-marked TX-ownership control". On this radio that
is the wrong shape — only MAIN can transmit, and a TX control on SUB would be an
affordance for something the hardware will not do, the same category of mistake as
offering a band the other receiver already holds (§2.3).
to it
That dissolves the concern this section was originally written around — that
coupling what you are looking at to what will transmit means changing the
screen changes what is on the air. Decoupled, it cannot. §2.2's safety rule gets
easier to honour too: a band change never implicitly moves the TX receiver,
because the TX receiver never moves at all.
2.5 Cross-band routing, and hearing the radio say no
The one step with on-air consequence, so it gets stated rather than implied.
Today the refusal is invisible.
setSliceFrequency()sends25 00 <freq>and returns; nothing correlates a reply. When the radio NAKs a cross-band tune,
the model has already moved optimistically, so the display shows a frequency the
radio is not on — which is precisely the failure observed, and the reason it
took a night to attribute (§1).
The mechanism is already in the tree and simply unread.
CivCodec.hdefinesboth
kCivOk(0xFB) andkCivNg(0xFA), andCivFramealready exposesisOk()/isNg(). Nothing callsisNg(). So step 3 is smaller than it looks:the decode exists, the correlation does not.
Proposed shape:
isNg()for an outstanding tune, do NOT advance themodel — re-assert the radio's actual frequency instead, the same way
setPanCenter()already re-asserts when it refuses a drag. A refused tunemust leave the display where the radio is, never where the operator asked.
known from §2.1, "which receiver should hold this band" is answerable without
asking the radio: if a receiver already holds the band, select it (no swap, no
CI-V tune at all); if neither does, the non-TX receiver is re-pointed
(§2.2). The NAK then stops being the routing mechanism and becomes what it
should be — a backstop for a state that disagrees with our model.
07 B0must be the only thing doingso within a session, and must not run while keyed (§4 Q3). Under aetherd this
is a shared mutation, per §5.
Why not just NAK-and-retry-with-a-swap: it makes every band change a global
radio side effect discovered by trial, on a radio a second client may also be
driving. Deciding from known state and treating the NAK as the exception inverts
that — the common case sends nothing at all.
2.6 Make TX unmistakable while watching Sub
Full duplex means the normal state during a pass is watching Sub while Main is
transmitting. That is exactly when a quiet label is not enough: an operator
looking at a still Sub waterfall must never be able to forget the other half is
on the air.
Proposal: the frequency display turns red while transmitting.
Red is already this project's TX colour rather than a new invention —
color.slice.txresolves to{color.red.500}, andcolor.highlight.txandcolor.tx.mox.*are established tokens. Applying the existing conventioninherits theme support and stays on the hardcoded-colour ratchet's right side.
The property that matters is not the colour but the guarantee: transmit state
is visible on whichever receiver you are looking at, not only on the one that
is transmitting.
3. Work implied
Deliberately ordered so nothing on-air changes until the groundwork is proven.
Reordered after §2.4 became a readout rather than a slice: most of the value
arrives before the expensive step, not after it.
and mode from the swap-read. No UI, no new slice — just state the backend does
not currently hold. Everything below depends on it and nothing else does.
frequency case. §1 measured five surfaces reporting success for work the
backend cannot do; each should refuse visibly instead.
should hold this band" is answerable before sending, so the NAK becomes a
backstop rather than the routing mechanism. This is what makes the band button
and a typed cross-band frequency actually work.
which receiver the panadapter is following, and a control to move the scope
between them. Small: it is new UI over state step 1 already fetched.
landing with step 4 — watching SUB while MAIN transmits is what step 4 makes
possible.
sliceId()/panId()— deferred, and now optional. This wasstep 1 in earlier drafts: ~20 call sites in
IcomCivBackend.cpp, several fromCI-V handlers where the answer depends on which receiver was selected when the
frame arrived. §2.4 explains why it is no longer the entry price: a SUB slice
would promise 78 properties the radio cannot back. Revisit it if SUB gains a
real audio stream (an operator wiring EXT-SP SUB into a soundcard), because
then a second slice earns its promise.
Steps 1–3 are worth doing on their own merits and are most of the operator-visible
fix: they stop AE accepting requests it cannot honour — five distinct surfaces
measured 2026-09-01 (§1), all reporting success — and make a cross-band band
change work. Whatever else is deferred, that gap should not be. The expensive
step is now last and may never be needed.
4. Open questions
Coexistence with aether-gate. The gate currently owns the swap on its USB
channel precisely because the swap is global. If AE natively drives two
receivers, what happens when both are attached? Options: document
"don't run both", have AE detect the gate, or leave AE swap-free and let it
only select among receivers the operator has already set up. The last is
the most conservative and may be enough for most operating.
Anything else sharing this shape is ANSWERED, and it is not one radio.
Three models in
kModelsdeclarereceivers: 2— the IC-9700 (0xA2),the IC-7610 (
0x98) and the IC-785x (0x8E). All three get onematerialised receiver today, so the mismatch in §1 is theirs too, not the
IC-9700's alone.
(An earlier draft of this RFC named the IC-905 here. That was wrong — it
declares
receivers: 1and does not share the shape. Corrected rather thanquietly dropped, because it is the exact error the rest of this document
argues against: inferring a capability from what a radio is known for
rather than reading what the table declares.)
What differs between the three is only the reason for the second receiver
— bands on separate hardware for the IC-9700, diversity/dual-watch on the
HF radios — and that difference belongs in how bands are offered (§2.3), not
in whether both receivers exist. So this should be driven by
receivers,which
IcomModelalready carries, and not special-cased per model.And the shape is not historical — Icom shipped another one last week. The
IC-7110 was announced at the Tokyo Ham Fair on 2026-08-29 as the
IC-7100 successor, and Icom's own material describes "independent receive
circuits for the HF/6M and the 2m/70cm bands" with dualwatch reception. That
is the IC-9700's architecture again: two receivers split across separate band
groups, rather than the HF radios' same-band diversity. So the expensive half
of this RFC — §2.5's routing, which exists because a receiver cannot take a
band the other one's hardware covers — has a second radio to justify it, four
days after this issue was last revised.
Usefully, nothing new is needed to accommodate it.
setSliceFrequency()'sguard is already predicated on an empty band table:
Today only the IC-9700 declares bands (
2m,440,23cm); the IC-7610 andIC-785x declare none. A radio whose contention is documented gets a table and
the machinery activates.
What is NOT known, and should not be guessed at:
same way — nobody has one yet;
9700 cannot have both receivers on the same band group; the 7110 might
genuinely allow one HF and one VHF simultaneously with no conflict, which
would be better and would mean §2.5 does not apply to it;
front-end architecture, which this RFC has not established.
Every one of those is a question for someone with the radio. Recording them as
questions rather than assumptions is the same discipline the IC-905 correction
above exists to enforce.
Is the swap safe during TX? Not tested. Until it is, the answer should be
"no swap while keyed", enforced rather than assumed.
What does the scope do across a swap? Gate's notes say it yanks and
returns. With two pans, does the second pan get scope data at all, or is the
scope MAIN-only? This gates whether pan 2 is a waterfall or a frequency
readout. Needs the radio.
Question 4 is the one that most changes the design, and it is an hour on the
bench.
the operator choose? Automatic matches the pass — you want the downlink.
Manual never surprises you by switching mid-pass. Leaning automatic with an
override, but this is an operating judgement rather than a design one.
out? It is the one case where rendering both pans is clearly right
(§2.4), so the answer changes whether dual render is a mode with its own
affordance or simply something the model permits.
5. Relationship to aetherd and "one AE, many radios"
Checked deliberately, because a proposal that assumes one radio would be the
wrong shape against a design where several are normal.
It fits, and mostly for free.
docs/aetherd-headless-engine-design.md§5 alreadyestablishes per-radio
RadioSessionwith the protocol namespaced by session id,"so one daemon can host several radios — of the same or different families —
concurrently." Everything in this RFC lives inside one session: two
receivers of one IC-9700, not two radios. Nothing here reaches across sessions,
so the multi-radio story is unaffected either way.
The staged plan's step 2 (the
IRadioBackendseam) has landed, and its own notesays "from this point a new radio family is a new backend — shippable in-process
in the desktop app immediately, and served through the daemon automatically once
the later steps land." Work here is behind that seam, so it inherits the daemon
for free rather than needing porting later.
Three places the alignment is load-bearing rather than incidental:
one daemon each probe their own receivers. Nothing in the probe reads or writes
anything session-external — worth stating because the
07 B0swap is globalto its radio, which makes "global" an easy word to get wrong here. It is global
to that radio, not to the engine.
TX arbitration engine-side and non-negotiable (Principle VI). "A band change
never moves the TX receiver implicitly" is the same principle one level down:
with several clients projecting one session, an implicit TX move would be
invisible to every client that did not initiate it.
client would render its own. The TX-ownership control it carries is a request
to the engine, arbitrated there — not a local mutation. That is the correct
split already and needs no rework.
The one open question multi-radio adds to §4's list: the single-swapper rule
is currently about AE-vs-aether-gate, but under a daemon with several clients it
generalises. If two clients on one session both request a band that needs a swap,
the swap must serialise at the engine — the same shared-mutation question §5 of
the aetherd RFC already flags for pan/slice creation. Recommend it be treated as
a shared mutation, announced to all clients, exactly as that section proposes.
6. What this does not propose
a radio, and step 3 should land with TX untouched.
bands because AE had no native buttons; with native bands that workaround
should become unnecessary, but removing it is out of scope here.
separately (the
1A 05menu item numbers are model-specific and unverified onthis model).