aethersdr / aethersdr/AetherSDR
[RFC] Split finder — pair the DX's exchange with the pileup and mark where they're listening
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 299
Description
[RFC] Split finder — pair the DX's exchange with the pileup and mark where they're listening
Status: draft, for discussion. Nothing here is built.
An idea from a breakfast conversation, written up because the expensive half is
already in the tree and the missing half is well bounded.
1. The operating problem
A DX station working split transmits on one frequency and listens on another —
typically "up 1" to "up 10", sometimes a range they're tuning across. The
convention exists to keep the DX's own frequency clear.
For the chaser this creates a hunt: you can hear the DX perfectly and have no
idea where they are listening. The usual answers are all indirect — read the
cluster spot (often stale or wrong), listen up-band for whoever is being worked,
or tune around until you find callers. In a spread pileup the DX may also be
moving their listening slot deliberately to break up the crowd.
The information needed to solve this is already on the air. When the DX sends
KI6BCJ 599
they are telling you, unambiguously, which station they just worked. If we heard
KI6BCJ calling at 14.025.6 twelve seconds ago, then 14.025.6 is where the DX
is listening, right now.
A human does this by ear and does it badly under pressure. It is a pattern-match
over two text streams and a short time window, which is a machine's job.
2. What already exists (why this is a smaller RFC than it sounds)
| Piece | Where | Note |
|---|---|---|
| CW decoder | src/core/CwDecoder.{h,cpp} + vendored third_party/ggmorse |
working, shipped |
| Callsign extraction from decoded text | src/core/CwCallsignSpotter.{h,cpp} |
rolling window, tolerates decode errors, fires on a repeat |
| Voice callsign path | src/asr/ (Copy Assist) |
separate feed into the same spotter |
| Multi-slice receive | RadioCapabilities::maxSlices |
8 on a 6700, 4 on a 6600, 2 on a 6400 |
| Spot plumbing / overlays | N1MMSpotClient, SpotHub, KiwiSDR DX overlay (#4828) |
somewhere to put the answer |
CwCallsignSpotter's own header already anticipates reuse:
"The same spotter can watch any text stream that identifies stations this way."
So the extraction is solved. What does not exist is the pairing — nothing in
the tree correlates who the DX named with where that station was heard.
3. Proposal
3.1 The core inference
Two observations, one match:
- A-stream — decode the DX's transmit frequency (the slice the operator is
already listening to). Watch for a callsign followed by an exchange. - B-stream — decode the split window. Record
(callsign, frequency, heard_at). - Match — when a callsign in A appeared in B within the last N seconds,
emitsplitFrequencyInferred(hz, callsign, confidence).
Match on the callsign, not on "599". Every station in the pileup sends 599;
only one of them is being worked. The report is a useful secondary cue for
segmenting the exchange, but the callsign is what carries the location.
Partial matches should count. ?I6BCJ against a recent KI6BCJ at a known
frequency is strong evidence — CW decode under a pileup is lossy by nature, and
requiring an exact match throws away most of the real signal.
3.2 Confidence, not certainty
The output must be a ranked hypothesis, never a silent auto-QSY:
- exact callsign match, single candidate → high
- partial match, or two candidates in the window → medium, show both
- DX moving their slot → the estimate should decay and re-converge, and the UI
should show that it is moving rather than flapping between two numbers
A "listening around 14.025.6, moving up" readout is more honest and more useful
than a false-precision single figure.
3.3 Output
Start with a passive readout: a marker on the panadapter at the inferred slot,
plus recent pairs (KI6BCJ 14.025.60, 12 s ago). One-click "set split to here"
is an obvious follow-up, but the value lands with the readout alone and it keeps
the first version out of anything that touches TX.
4. The hard part, stated plainly
Covering the split window is the whole problem, and it is not a small one.
A pileup may be spread over 5–20 kHz. Two approaches:
(a) Slice-based. Assign spare receive slices to the split window. Cheap to
build — the decoder and spotter already work per-slice. But a 6700 has 8 slices
and a 6600 has 4, so you cover a handful of discrete spots, not a window. Useful
for "up 1 exactly" convention, useless for a spread pileup. Note also that
CwDecoder today is one instance re-wired to the active slice (see
routeCwDecoderOutput() in MainWindow.cpp), so even this needs the decoder
made multi-instance.
(b) Skimmer-style multi-channel decode off the panadapter IQ. Decode many CW
signals in parallel from the FFT data rather than from audio slices — the CW
Skimmer approach, repurposed. This is what actually solves the problem, and it
is substantial DSP work: per-signal detection, tracking, and a decoder
instance per detected carrier, at panadapter rates.
I would scope any first version to (a) and treat (b) as the real feature, so the
pairing logic can be built and proven against a cheap front end before the
expensive one exists.
5. Scope
In, for a first version
- CW only
- The pairing engine and its confidence model
- A passive readout (marker + recent-pairs list)
- Slice-based coverage of a small number of candidate frequencies
Out
- SSB/voice split-finding. Whisper on many simultaneous signals is a different
order of problem; CW is the tractable case and also the one where split
operating is most common. - Any automatic QSY or TX change.
- Wideband skimmer decode — the goal, but a separate piece of work.
6. Cross-platform impact
Nothing here is platform-specific in a first version: CwDecoder, ggmorse and
CwCallsignSpotter are all portable C++ already shipping on Linux, macOS and
Windows, and the pairing engine is text and timestamps. The passive readout is
Qt widgets/panadapter overlay, same as the existing spot overlays.
The one place platform enters is the expensive half. If multi-channel decode is
ever GPU-accelerated it inherits the existing spectrum-backend split (QRhi /
Metal / Vulkan) and its current sore points — Wayland deadlocks (#4704),
software-OpenGL fallbacks (#4748). A CPU-side skimmer decode avoids that
entirely at a cost in channel count, and I would start there for that reason.
7. Alternatives considered
- Cluster/skimmer spots for the split slot. Already exists in spirit — the
RBN and cluster tell you the DX's transmit frequency, not where they are
listening, and the "up 2" in a spot comment is a human's guess that goes stale
the moment the DX moves. This RFC is about the frequency no spot carries. - Match on "599" / the report alone. Simpler, and wrong: every station in
the pileup sends 599, so the report identifies the exchange but not which
caller. It is useful as a secondary cue for segmenting, not as the key. - Track the DX's own tuning. Some radios expose the DX's split via CAT — but
only your own radio's, obviously, not a distant station's. Not applicable. - Just listen up-band manually. The status quo, and it works for a
disciplined "up 1". It fails exactly where help is most wanted: a wide,
moving, crowded pileup. - Do nothing until wideband skimmer decode exists. Defensible. My argument
against is that the pairing logic is the novel part and it can be built and
proven against a cheap slice-based front end first, so the expensive work
starts with its consumer already validated.
8. Open questions
- Is the skimmer decode a prerequisite, or is slice-based coverage genuinely
useful first? My instinct is the latter for "up 1/2/5" conventions, but I
have not measured how often real DX sticks to those. - Where does the multi-channel decoder live if it is built — inside AE, or
as a bridge/plugin consuming DAX IQ? - Does anything reusable already exist for multi-signal CW detection in
ggmorse, or would it need writing? - Is the timing window stable enough across operators? The gap between a
caller transmitting and the DX naming them varies; this wants measuring on
real pileups before a number is baked in.
9. What I can contribute
I have a shack that can generate the test case rather than only theorise about
it: two HL2-family boards on one LAN, flex-sim for synthetic multi-signal
scenarios, and the agent automation bridge for repeatable end-to-end proofs. A
recorded real pileup (IQ capture) would make the pairing logic testable offline
and deterministically, which seems the right foundation before any DSP work.
I am raising this as an idea worth judging, not claiming the implementation.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading src/core/CwDecoder.{h,cpp}, src/core/CwCallsignSpotter.{h,cpp}, and routeCwDecoderOutput() in MainWindow.cpp to understand the existing per-slice flow. The RFC still needs decisions on coverage, timing, decoder placement, and the readout before implementation can have a bounded done state; no tests or new target files are named.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend, desktop
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100