Skip to content

IC-9700 support for dual VFOs / slices for satellite use #4840

Description

@nigelfenton

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-7610 0x98 and IC-785x 0x8E, 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.

IcomCivBackend today hardcodes a single receiver:

[[nodiscard]] int sliceId() const noexcept { return 0; }
[[nodiscard]] QString panId() const { return QStringLiteral("0"); }

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:

$ automation_probe slice add
{ "ok": true, "requested": true, "sliceCount": 1 }

  sliceCount: 1 -> 1     slots: [{id:0, "ours"}, {id:1, "empty"}]     panCount: 1

The chain: slice addRadioModel::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.

AE materialised it immediately:

sliceCount: 1 -> 2      slots: [{id:0, "ours"}, {id:1, "ours"}]

slice 0: freq=144.2  mode=USB  panId=0xe1000000  active=True
slice 1: freq=432.1  mode=USB  panId=0xe1000001  active=False

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:10219m_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:

sliceCount: 2   panCount: 2   slots: [{id:0,"ours"}, {id:1,"ours"}]

pan   0xe1000000: center=144.2015  bw=1.0
pan   0xe1000001: center=432.1     bw=0.2
slice 0: freq=144.2  mode=USB  panId=0xe1000000
slice 1: freq=432.1  mode=USB  panId=0xe1000001

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:

  1. probe the receiver count (§2.1);
  2. panCenterBandwidthChanged for each receiver's pan;
  3. everything describing those pans — limits, gain, range;
  4. 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 B00307 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:

  1. 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.
  2. 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.
  3. 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.

  1. 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.
  2. Stop lying about refused work (§2.5 step 1). fix(icom): a refused tune must not read as a successful one #5373 is this, for the
    frequency case. §1 measured five surfaces reporting success for work the
    backend cannot do; each should refuse visibly instead.
  3. 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.
  4. 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.
  5. Per-pan band menu (§2.3), offering only what is free.
  6. 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.
  7. 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

  1. 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.

  2. 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.

  3. Is the swap safe during TX? Not tested. Until it is, the answer should be
    "no swap while keyed", enforced rather than assumed.

  4. 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.

  1. 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.
  2. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovement to existing featuremaintainer-reviewRequires maintainer review before any action is takenmulti-panMulti-panadapter layout and slicingprotocolSmartSDR protocolrfc

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions