aethersdr / aethersdr/AetherSDR

[RFC] Split finder — pair the DX's exchange with the pileup and mark where they're listening

Open
#4,858 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

audio CW GUI maintainer-review New Feature
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,
    emit splitFrequencyInferred(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

  1. 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.
  2. Where does the multi-channel decoder live if it is built — inside AE, or
    as a bridge/plugin consuming DAX IQ?
  3. Does anything reusable already exist for multi-signal CW detection in
    ggmorse, or would it need writing?
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.