# MOSAIC protocol whitepaper

## Abstract

MOSAIC is an asynchronous multi-user packet modem family. Its design goal is a
shared channel in which several uncoordinated transmitters can overlap in time
and frequency while a single receiver recovers every CRC-valid packet it can
prove. The current implementation has two band profiles:

* **MOSAIC-HF**, a 50 bit/s narrowband profile intended for an SSB-width
  channel.
* **MOSAIC-UHF**, an 800 bit/s profile for 70 cm operation where bandwidth is
  available.

The receiver is deliberately conservative: it only cancels a signal after the
ordinary packet decoder returns a CRC-valid payload. That CRC gate is the safety
mechanism that lets the receiver subtract decoded users without turning
uncertain detections into destructive interference cancellation.

This document separates **measured-on-hardware** evidence from **software
simulation**. Hardware results are sourced primarily from JSON manifests under
`run-artifacts/`; simulated results are labelled as such.

## Evidence basis

Quantitative protocol constants in this paper come from:

* `src/mosaic_hf/config.py`
* `src/mosaic_hf/profiles.py`
* `src/mosaic_hf/waveform.py`
* `src/mosaic_hf/framing.py`
* `src/mosaic_hf/acquisition.py`
* `src/mosaic_hf/sic.py`
* `src/mosaic_hf/physical_bench.py`
* `docs/RF-BENCH-HANDOFF.md`
* JSON manifests and radio logs under `run-artifacts/`

No number in the results tables is intended as an estimate. If a result has not
yet been measured, it is identified as unmeasured.

## Motivation and design goals

The protocol is built for uncoordinated packet users rather than scheduled
point-to-point links. The important design goals are:

1. **Asynchronous access.** Transmitters do not need a common time reference.
2. **User separation by known signatures.** Each candidate identity has a
   declared access signature; the receiver searches a catalogue of possible
   senders rather than accepting an unlimited blind active set.
3. **CRC-gated cancellation.** A packet must pass the normal decode and CRC
   checks before it can be subtracted from the residual.
4. **Bounded computation.** Carrier, drift, timing and cancellation searches are
   finite grids.
5. **Hardware realism.** Strong RF energy is not treated as success; retained
   runs must prove burst duration and CRC validity.

The scaling goal remains larger than the present hardware evidence, but the
current artifacts now include genuinely independent MOSAIC-UHF on-air results:
a bladeRF x115 and a USRP B210 transmitted from separate processes while a
HackRF One decoded both packets, and retained runs recovered twelve logical
stations carried across those two independent RF transmit chains with both
spread and randomly drawn arrivals. Earlier no-fade software scaling sweeps
that appeared to show collapse were invalid: the unfaded branch in
`tools/many_station_scaling.py` emitted double-sideband AM mirror images
instead of true frequency offsets. With the analytic-signal fix, retained
twelve-station no-fade seeds decode 12/12 and a corrected balanced sweep
decodes 8/8, 10/10, 12/12 and 16/16. In the clean no-fade, no-AWGN conditions
tested so far, multi-user interference is not the binding constraint. Fading
is: at eight UHF stations, current faded simulations recover only 12/32 with
6 dB amplitude spread and 15/32 with balanced levels. The current unfaded
single-seed deep sweep recovers 22/24 and 26/32, showing graceful degradation
rather than collapse; those two large-count points are not outage rates.

The live station has also now completed a bidirectional keyboard QSO over the
antenna path at 431.200 MHz. VE6SLP and VA6GA each delivered operator text at
the remote console through the KISS live modem. This corrects the earlier bench
conclusion that antenna-path CRC recovery was blocked; the retained diagnosis
shows the antenna path was working and the failures were in live-modem capture,
scheduling, filtering and console plumbing.

The HF profile has a different evidence status. It is proven as a one-packet
waveform through real audio converters and simulated ITU-R F.1487 channels, but
its shared-channel operation is not proven in a fading HF channel. Current
`hf-audio` simulations show that the receiver scales in a static channel and
then collapses to one or two recovered stations under ITU-style fading because
SIC residuals are not tracked through a time-varying packet.

## Waveform and modulation

The transmit chain is fixed by the wire format: payload bytes are protected,
encoded, interleaved, preceded by the signature-derived preamble, mapped onto
the 16-of-192 tone grid, and shaped into a burst.

![MOSAIC transmit chain](figures/tx_chain.png)

MOSAIC uses a deterministic 16-candidate tone selection on a 192-bin frequency
resource grid. Each transmitted coded symbol carries 4 bits by selecting one of
16 local candidates. The candidate set changes each symbol interval as a
function of the 32-bit access signature, an LCG state and a 32-bit mixer.

The mapping in `candidate_bin_mapping()` is defined only for:

| Field | Value | Source |
| --- | ---: | --- |
| Candidates per symbol | 16 | `src/mosaic_hf/waveform.py` |
| Resource bins | 192 | `src/mosaic_hf/waveform.py` |
| Bins per candidate stratum | 12 | `src/mosaic_hf/waveform.py` |
| Access signature width | 32 bits | `src/mosaic_hf/waveform.py` |

![Signature-dependent 16-of-192 candidate hopping](figures/tone_grid_hopping.png)

The figure shows two access signatures over twenty symbol intervals. Each
signature redraws sixteen candidate bins from the same 192-bin resource grid;
a transmitted 4-bit symbol selects one candidate from that interval's set.

![16-ary tone occupancy and equal-energy symbol map](figures/tone_constellation_16ary.png)

The left panel is one symbol interval from `candidate_bin_mapping()`: sixteen
candidate bins for signatures `0x2F` and `0x5B`, with one active tone marked.
The right panel is the equal-energy 16-ary MFSK symbol map for signature
`0x2F`—the modem never superposes tones inside a symbol; the four coded bits
index that interval's candidate list. Unit envelope at the analytic component
is a waveform property, not an emissions proof.

The modem generates a unit-envelope analytic CPFSK/hopped-MFSK waveform. Tone
frequencies are selected from the profile's tone grid and repeated for one
symbol duration. The orthogonality condition is enforced in `PhyConfig`:

```text
tone_spacing_hz * symbol_duration_s == 1
```

That constraint is why MOSAIC-UHF changes both symbol duration and tone spacing
together. Keeping 192 bins and 16 candidates preserves the multi-user resource
structure while changing only the time and frequency scale.

## Framing, FEC and CRC

The implemented P1 packet format is in `src/mosaic_hf/framing.py`.

| Item | Value | Source |
| --- | ---: | --- |
| Maximum PHY payload | 255 bytes | `MAX_PAYLOAD_BYTES` |
| Header data | 4 bytes | `HEADER_DATA_BYTES` |
| Header information including CRC | 5 bytes | `HEADER_INFO_BYTES` |
| Header CRC | CRC-8/ATM, poly `0x07`, init `0` | `crc8_atm()` |
| Payload CRC | CRC-16/CCITT-FALSE, poly `0x1021`, init `0xffff` | `crc16_ccitt_false()` |
| FEC | terminated convolutional code, K=7 | `CONSTRAINT_LENGTH` |
| Generators | octal `171`, `133` | `_GENERATORS` |
| Tail bits | 6 | `TAIL_BITS` |
| Interleaver | 4 rows, transmit by column | `interleave()` |

![P1 frame layout](figures/frame_layout.png)

![P1 bit-level fields, coding chain and CRC gate](figures/frame_bit_layout.png)

The protected header carries magic `0xD3`, version, payload length and reserved
semantics, then CRC-8/ATM over those five information bytes. Header and payload
regions are independently terminated with a K=7 rate-1/2 convolutional code
(generators octal 171, 133), 4-row column-read interleaved, and packed four
bits per 16-ary tone symbol. The decoder recovers the protected header first,
uses the header length to size the payload coded block, and accepts the payload
only when CRC-16/CCITT-FALSE passes. Only that full CRC-valid path may enter
cancellation.

`framing.py` also contains an experimental M1/P4-A protected-header candidate.
It is not described here as the active wire format because `encode_frame()` and
the retained RF manifests use the P1 path.

## Acquisition

The acquisition preamble contains 32 known symbols derived from the access
signature. Symbol `i` uses:

```text
mix32(signature + (i + 1) * 0x9e3779b9) & 0x0f
```

The reference `AcquisitionConfig` searches:

| Field | Reference default | Source |
| --- | ---: | --- |
| Preamble symbols | 32 | `AcquisitionConfig` |
| CFO range | -112.5 to +112.5 Hz | `AcquisitionConfig` |
| Coarse CFO step | 12.5 Hz | `AcquisitionConfig` |
| Drift hypotheses | -3, 0, +3 Hz/s | `AcquisitionConfig` |
| Timing step | 8 samples | `AcquisitionConfig` |
| Fine CFO step | 0.5 Hz | `AcquisitionConfig` |
| Detection threshold | 2.4 | `AcquisitionConfig` |

Retained SDR captures required a wider explicit physical search. The band
profiles therefore use a -2000 to +2000 Hz acquisition range for hardware
runs. MOSAIC-HF searches drift hypotheses of -10, 0 and +10 Hz/s. MOSAIC-UHF
uses 5 ms symbols, a 160 ms preamble, a 200 Hz coarse CFO step, an 8 Hz fine CFO
step and only a 0 Hz/s drift hypothesis; the source notes that 10 Hz/s drift
would move less than 2 Hz over that preamble.

## The multi-user receiver

`src/mosaic_hf/sic.py` implements bounded offline catalogue discovery and
successive interference cancellation (SIC). A candidate is not an assertion
that a transmitter is active; it is one catalogue identity and access
signature. On each residual generation, the receiver:

1. Acquires every eligible catalogue entry on the residual.
2. Ranks detected hypotheses by measured acquisition score.
3. Attempts ordinary packet decoding.
4. Marks rejected candidates by stage.
5. Cancels at most CRC-valid packets.
6. Repeats for a bounded number of generations or until no CRC-valid candidate
   remains.

![MOSAIC receive and CRC-gated SIC chain](figures/rx_chain.png)

![CRC-gated SIC cancellation flowchart](figures/sic_flowchart.png)

The cancellation ledger retains the prior residual segment, start and end
samples, fitted gain and phase, and the acquisition-anchored trajectory fit.
The code reports `false_cancellation_count` as structurally zero on the manager
side because ledger entries are CRC-gated; campaigns with generated truth can
separately check whether a ledger identity was inactive.

The flowchart is the safety argument in one page: the original mixture `r₀` is
immutable; each generation searches a finite catalogue; ordinary FEC/CRC decode
must pass before a reconstruction is fitted; only then does

```text
r_{k+1} = r_k - a_k s_k(θ_k)
```

update the residual, with a LIFO undo snapshot on the ledger. A failed CRC
never writes the residual. This is not a joint maximum-likelihood receiver and
it is not an unlimited blind active-set estimator. Its safety comes from the
CRC gate and from bounded catalogue search.

**Operator fast path and residual peel.** The live modem
(`tools/mosaic_modem.py`) first walks each catalogue signature independently.
That recovers equal-power overlapping stations far faster than a full SIC
campaign and is what keeps continuous receive on the RF stream. Finding some
stations must not stop the search for the rest: incomplete multi-user recovery
continues with a CRC-gated residual peel (`cancel_crc_valid_packet`) and, when
still incomplete, the full collision/SIC receiver. Payloads from each stage are
merged by identity.

**Gain tracking on cancellation.** The cancellation fit uses a piecewise-linear
complex gain over a hat-function time basis when the profile enables it
(`PhyConfig.gain_knot_symbols`; MOSAIC-HF defaults to three symbols per knot).
A constant-gain fit is always computed; tracking is accepted when residual power
improves by a clear margin, or when a mild BIC penalty on long extents still
prefers the tracked model. That is the right estimator structure for HF, but
alpha still does **not** claim shared-channel success under ionospheric fading:
measured multi-user fade recovery remains limited, and further diversity /
tracking work remains open.


## Mathematical specification

The tone grid is orthogonal over one symbol. For tone spacing `Δf` and symbol
duration `T_s`, the implemented profiles require:

```text
Δf T_s = 1
∫_0^{T_s} exp(j 2π (m-n) Δf t) dt = 0,  m != n
```

MOSAIC-UHF is therefore not a separate modulation. It scales MOSAIC-HF by
shortening `T_s` and increasing `Δf` together while keeping 16 candidates and
192 resource bins.

The AWGN channel helper defines SNR against the profile's reference bandwidth
`B_ref`, not against total discrete-time Nyquist bandwidth. The implemented
variance is:

```text
σ² = P_signal / 10^(SNR_dB/10) * (f_s / 2) / B_ref
```

This definition is the basis for simulated SNR tables. The relationship to
energy per bit is:

```text
Eb/N0 = SNR_ref * B_ref / R_b
Eb/N0_dB = SNR_ref_dB + 10 log10(B_ref / R_b)
```

For MOSAIC-HF, `B_ref/R_b = 2400/50 = 48`, so the narrowband HF profile has
16.8 dB of processing gain relative to its 2400 Hz SNR reference bandwidth:

```text
Eb/N0 = SNR_ref · (B_ref / R_b)
Eb/N0_dB = SNR_ref_dB + 10·log10(B_ref / R_b)
MOSAIC-HF:  B_ref = 2400 Hz, R_b = 50 bit/s  →  Eb/N0_dB = SNR_2400Hz_dB + 16.8 dB
MOSAIC-UHF: B_ref = 38400 Hz, R_b = 800 bit/s → Eb/N0_dB = SNR_38400Hz_dB + 16.8 dB
```

For MOSAIC-UHF, `B_ref/R_b = 38400/800 = 48`, so the same conversion applies.
This conversion is mandatory when comparing MOSAIC results with other HF modes:
-9 dB SNR in the 2400 Hz reference band is 7.8 dB Eb/N0, not an operation
point near -9 dB Eb/N0. Example MOSAIC-HF conversions are:

| SNR in 2400 Hz | Eb/N0 |
| ---: | ---: |
| -9 dB | 7.8 dB |
| -12 dB | 4.8 dB |
| -15 dB | 1.8 dB |
| -18 dB | -1.2 dB |
| -21 dB | -4.2 dB |
| -24 dB | -7.2 dB |

This processing gain is the central HF trade: the 50 bit/s profile spends an
SSB-width channel to buy about 17 dB of energy-per-bit margin. It is also why
the two profiles can share the same matched-noise Eb/N0 cliff while occupying
different bandwidths.

SIC subtracts only decoded, CRC-valid packets. At cancellation generation `k`:

```text
r_{k+1} = r_k - a_k s_k(θ_k)
```

Here `r_k` is the residual, `s_k(θ_k)` is the reconstructed waveform at the
acquired timing/CFO/drift trajectory, and `a_k` is the fitted complex amplitude
(gain and phase). If the header or payload CRC fails, this update is not
applied.

The Watterson-style fading helper applies delayed, independently faded analytic
paths and normalizes path powers as a group:

```text
y[n] = Σ_p sqrt(P_p) h_p[n] x_a[n - d_p]
h_p[n] = (1/sqrt(M)) Σ_{m=1..M} exp(j(2π f_{p,m} n/f_s + φ_{p,m}))
Σ_p P_p = 1
```

`x_a` is analytic audio, `d_p` is an integer sample delay, each Doppler tone
`f_{p,m}` is drawn within the path's Doppler support, and the current reference
slice does not implement fractional-delay filters.

## Band profiles

The profile abstraction in `src/mosaic_hf/profiles.py` binds PHY parameters,
TX shaping, direct-IQ filtering and acquisition settings so that rate-dependent
stages move together.

| Parameter | MOSAIC-HF | MOSAIC-UHF | Source |
| --- | ---: | ---: | --- |
| Audio sample rate | 12,000 Hz | 96,000 Hz | `profiles.py` |
| Symbol duration | 80 ms | 5 ms | `config.py`, `profiles.py` |
| Tone spacing | 12.5 Hz | 200 Hz | `config.py`, `profiles.py` |
| Candidates / bins | 16 / 192 | 16 / 192 | `config.py`, `profiles.py` |
| Lowest tone | 300 Hz | 300 Hz | `config.py`, `profiles.py` |
| Highest tone | 2687.5 Hz | 38,500 Hz | `config.py`, `profiles.py` |
| SNR reference bandwidth | 2,400 Hz | 38,400 Hz | `config.py`, `profiles.py` |
| Nominal SSB bandwidth | 2,800 Hz | 40,000 Hz | `config.py`, `profiles.py` |
| Raw coded bit rate | 50 bit/s | 800 bit/s | `profiles.py` |
| Receive FIR taps | 129 | 1025 | `profiles.py` |
| Receive transition | 100 Hz | 3000 Hz | `profiles.py` |
| TX shaping taps | 241 | 1921 | `tx_shaping.py`, `profiles.py` |

The UHF profile is the same waveform family scaled by 16 in rate. The trade is
bandwidth for time. The handoff notes report a software matched-noise sweep in
which both profiles have the same cliff at about +1 dB Eb/N0. The same notes
also state that, at fixed transmit power, UHF collects 12 dB more noise in its
wider bandwidth. Therefore UHF is not a weaker waveform, but it does trade link
margin for shorter air time.

## Implementation on real radios

### Hardware and transports

Implemented tools and retained bench runs use:

| Radio | Role in retained runs | Format | Source |
| --- | --- | --- | --- |
| bladeRF x115 | forward transmit, reverse receive | SC16 Q11 for vendor CLI | `docs/RF-BENCH-HANDOFF.md`, `physical_bench.py` |
| HackRF One | forward receive, reverse transmit | CS8 for `hackrf_transfer` | `docs/RF-BENCH-HANDOFF.md`, `physical_bench.py` |
| USRP B210 | independent UHF transmitter and UHF decoding observer in retained runs | interleaved complex64 | `tools/usrp_io.py`, `run-artifacts/three-radio-uhf-2/usrp-tx.log`, `run-artifacts/usrp-observer-2tx-hi/usrp-rx.log` |

The direct-IQ path also uses CF32LE/SigMF files for retained captures and
observer products.

The B210 path is intentionally a separate process like the bladeRF and HackRF
vendor tools. `tools/usrp_io.py` runs under `/usr/bin/python3`, because the UHD
Python bindings are installed system-wide, and exchanges interleaved complex64
sample files. Its default UHD selector is `type=b200`. That default is not
cosmetic: UHD also exposes the bladeRF through its SoapySDR bridge, so empty
device arguments can silently open the wrong radio. The tool records positive
identity fields (`mboard_id`, `mboard_serial`) so a retained result proves what
actually transmitted or received. In `run-artifacts/three-radio-uhf-2/usrp-tx.log`
the opened transmitter identifies as `mboard_id` `B210`, serial `31B92DD`, with
TX gain 52.0 dB, 1,000,000 sample/s rate and 1.29 s transmit duration.
In `run-artifacts/usrp-observer-2tx-hi/usrp-rx.log`, the same B210 identifies
as serial `31B92DD` while receiving at gain 70.0 dB and 2,000,000 sample/s.

### MOSAIC-HF access through a transceiver audio path

The bench still cannot make a two-radio HF RF link directly. The handoff now
records measured tuning limits: the bladeRF x115 stops at 237.5 MHz and rejects
`set frequency rx 14100000` with `Invalid parameter`; it would need an XB-200
transverter that is not on the bench. The B210 tunes down to 42 MHz, lower than
its published 70 MHz figure but still above HF. The HackRF reaches HF, but it
is half duplex and therefore cannot be both ends of a two-radio link. That
makes a direct two-radio HF RF link physically impossible on this bench.

That does not block MOSAIC-HF, because HF digital modes normally reach the band
through an SSB transceiver's audio path, not by asking a computer interface to
be an HF transmitter. MOSAIC-HF is already an audio-band waveform: the profile
opens a 200-2800 Hz audio band around tones from 300 Hz to 2687.5 Hz, and its
12 kHz modem sample rate is exactly one quarter of the 48 kHz rate available on
ordinary sound interfaces. `src/mosaic_hf/audio_path.py` and
`tools/mosaic_hf_audio.py` provide that missing path: audio out to the rig's
microphone input, audio back from the receiver, and optional serial RTS/DTR
PTT.

Three audio-path design choices are protocol-relevant because they prevent
known HF-digital failure modes:

* **Drive is capped, not normalized.** `AudioPathConfig.transmit_amplitude`
  defaults to 0.25. Hot audio drives an SSB transmitter into ALC compression
  and splatters into neighbouring channels.
* **Bursts are padded.** The default guard is 300 ms of lead silence and 200 ms
  of tail silence, giving PTT relays and ALC time to settle before the preamble.
* **PTT is explicit.** `PttControl` keys serial RTS or DTR. Leaving PTT unset
  permits VOX, but VOX is the fallback because it keys on the first audio it
  hears and can clip the burst start.

The rate conversion itself also has a measured failure mode. The 12 kHz to
48 kHz conversion uses a polyphase filter with group delay; resampling a burst
with no trailing room can truncate final symbols and make the decoder fail with
`stream ends before coded payload`. `tests/test_audio_path.py` now prepares the
burst exactly as it is transmitted, including the keying guard, before checking
rate-conversion decode.

The retained analogue-converter result is not a software loopback. The handoff
records a Focusrite Scarlett 4i4 as device 13, with a physical output-to-input
path on channel 0: a 1500 Hz probe returned 46 dB SNR, while channel 1 was
silent. A full MOSAIC-HF packet was sent through the real DAC, analogue wiring
and real ADC with:

```bash
python tools/mosaic_hf_audio.py --amplitude 0.9 loopback --device 13 \
  --repeat 3 --text "VE6SLP-1 DE VA6GA-2 QSL 599 K"
```

All 3 of 3 attempts decoded byte-exact, with acquisition scores from 647 to
677 and received RMS around 0.009. The return is about 40 dB below the
transmitted level, which is why that bench probe used amplitude 0.9. On a real
SSB transceiver, drive should start at the default 0.25 and be raised only
until ALC just begins to move. This result demonstrates the converter path and
the modem's tolerance of it; it is not an HF on-air contact and does not
measure ionospheric propagation.

### Direct-radiated MOSAIC-HF waveform on a UHF SDR carrier: acquisition only

`run-artifacts/hf-rf-link/offline-decode.json` records a partial measured RF
result for the MOSAIC-HF profile radiated directly by an SDR over the real
antenna path at 431.200 MHz. This did not open an HF band; it carried the HF
waveform on the already-authorized UHF bench carrier. The bladeRF x115
transmitted with `txvga2` 10 dB and the pinned FPGA
`7bb0c7b0fb9976cd631b679352aff3383ab371dad29306452140984363711fdf` at version
0.16.0; the USRP B210 received at 52 dB. One packet was transmitted. The
retained offline decode is tied to capture SHA-256
`3d691092ca4379de901e3683bd8e38d962a07f8f91de2c2abc12ba662ad19875` and
187,000,000 capture samples.

The result is a clean partial: the receiver acquired the burst and correctly
ranked the true sender first, but no CRC-valid packet was recovered.

| Candidate | Acquisition score | CFO | Drift | Rank | Rejection |
| --- | ---: | ---: | ---: | ---: | --- |
| `VE6SLP-1` | 1314.2 | +106.25 Hz | -1.0 Hz/s | 1 | header |
| `VA6GA-2` | 158.9 | +160.75 Hz | +10.0 Hz/s | 2 | header |

`crc_valid_sender_ids` is empty. Both candidates evaluated 963 coarse and 546
fine hypotheses. The true sender's acquisition score is about 8.3 times the
other catalogue entry, so the burst was radiated, propagated, detected and
attributed, but it was not decoded. An independent FFT measurement on the same
capture placed the carrier at +107.8 Hz, agreeing with the receiver's
+106.25 Hz estimate to within 2 Hz; that corroborates acquisition and points
the fault downstream.

The leading explanation is drift, not a proven root cause. MOSAIC-HF uses
12.5 Hz tone spacing, the reciprocal of its 80 ms symbol, and the packet is
about 13 s long. The estimated -1.0 Hz/s drift accumulates to about 13 Hz over
the frame, more than one full tone spacing. The preamble can still correlate
before that drift dominates, while later header tones can walk off their bins.
The current coarse drift grid of -10, 0 and +10 Hz/s also poorly covers a true
drift near -1 Hz/s.

This partial does not contradict the intended MOSAIC-HF deployment. A 12.5 Hz
tone grid presumes a more stable reference than a free-running SDR local
oscillator at 431 MHz. That assumption is exactly what the transceiver audio
path provides: the rig performs RF translation from its own reference and the
modem sees audio. The same profile decodes byte-exact through real analogue
converters and has the simulated ITU-R F.1487 threshold bracket described
below. MOSAIC-UHF, with 200 Hz tone spacing, continues to be the profile used
for live keyboard QSOs over this UHF antenna path.

### Emission window

`--frequency-hz` sets the local oscillator. The modem audio is placed
100,000 Hz above that LO. For `--frequency-hz 431200000`, the emitted occupied
spectrum is:

| Profile | Emitted spectrum | Source |
| --- | --- | --- |
| MOSAIC-HF | 431.300200-431.302800 MHz | `emission_window_hz()`, `docs/RF-BENCH-HANDOFF.md` |
| MOSAIC-UHF | 431.300200-431.338700 MHz | `emission_window_hz()`, `docs/RF-BENCH-HANDOFF.md` |

An observer tuned to 431.200 MHz is not listening to the MOSAIC tones; for the
HF profile, the observer should tune the lower edge as USB audio.

### FPGA pinning hazard

The bladeRF hazard is severe enough to be part of the protocol evidence model.
The handoff records that libbladeRF silently auto-loaded FPGA v0.14.0 after a
reconnect. That image accepted `set samplerate tx 1000000` and reported the
requested rate, while actually clocking the DAC 4 times too fast. The failed
burst was strong but undecodable: a 17.14 s waveform radiated in 4.278 s, with
0.02 s symbol dwell instead of 0.08 s and about 20 kHz occupied bandwidth
instead of about 2.4 kHz.

The fix is to load `hostedx115-latest.rbf` in the same `bladeRF-cli` process
that streams samples. `BladeRfHackRfConfig` expects FPGA version 0.16.0 and
SHA-256 `7bb0c7b0fb9976cd631b679352aff3383ab371dad29306452140984363711fdf`.
The live modem refuses `--execute` without an FPGA bitstream.

This is why received signal strength alone never proves a valid transmission.
Retained manifests compare measured burst duration to expected waveform
duration and require CRC validity.

The B210 selector failure is the same class of bench hazard. In both cases the
bench accepted a plausible command while using hardware other than the operator
intended: a stale bladeRF FPGA in one case, and a UHD-opened bladeRF instead of
a B210 in the other. Both are controlled by recording positive radio identity,
not by trusting that a command completed.

### Methodological guardrails

Two bench details now have explicit artifact support because either can make a
false result look plausible:

* **Envelope profiles.** The three-radio manifests carry an `envelope_profile`,
  a sequence of 100 ms receive-level buckets. It exists because "a station did
  not transmit", "a station transmitted at the wrong time" and "a station was
  present but too weak" can all decode as an absent packet. The envelope is the
  field that distinguishes those cases after the fact.
* **Per-band fading delay spread.** `tools/many_station_scaling.py` now defaults
  the simulated delay spread by profile: 0.002 s for HF and 0.000001 s for UHF.
  The source notes that a 2 ms spread is ionospheric, while at 70 cm the spread
  is microseconds and therefore sub-sample for this model.

The same section of the tool also documents the bladeRF keying issue:
`--bladerf-lead-seconds` defaults to 1.05 s because the bladeRF CLI loads the
FPGA image before it can stream. The lead compensates the otherwise late
bladeRF burst when another process, such as `/usr/bin/python3 tools/usrp_io.py`,
is scheduled by wall clock.

The tool also refuses to let one radio observe and transmit in the same run.
Full-precision observers are decoded with `decode_collision_iq()` in
`src/mosaic_hf/physical_bench.py`; this keeps B210 CF32 captures in their
native precision instead of first quantizing them to the HackRF's 8-bit CS8
format.

`tools/decode_capture.py` exists for the same evidence-preservation reason. A
congested decode can take longer than the capture itself; if the decode process
is interrupted after the radios have finished, re-keying the transmitters would
create a different channel realization. The tool decodes the retained capture
offline, records the capture SHA-256 in its JSON output, and accepts
`--format cf32` for full-precision observers as well as CS8 HackRF recordings.

The general rule for negative results is now explicit: check the received
level before attributing a failed decode to the receiver. The low-gain B210
observer run at `run-artifacts/usrp-observer-2tx/` detected both candidate
preambles but recovered no CRC-valid payloads; its 100 ms envelope buckets peak
at 0.011240958236157894. The high-gain rerun at
`run-artifacts/usrp-observer-2tx-hi/` peaks at 0.0802842453122139 and recovers
both packets. The decode result alone cannot distinguish an under-gained front
end from a receiver limitation; the envelope profile can.

The same lesson applies to population simulations. Superseded scaling files
first suggested the wrong near/far mechanism, then seemed to show congestion
collapse, and a coordinated-arrival explanation was offered when hardware
decoded 12/12. Those explanations are withdrawn for current scaling claims:
some old artifacts used a defective unfaded generator that manufactured
mirror-image interference. The fault was in the sweep tool's signal generator,
not in the receiver or protocol; the receiver did not become better, the
measurement harness stopped lying about the channel. The first HF multi-user
harness had the same failure mode: multiplying real audio by a cosine is
amplitude modulation, not a frequency shift, and leaves a mirrored image that
no catalogue entry can cancel. The correct path uses the Hilbert analytic
signal before applying carrier offsets, as the channel code already did. Files
without the `even_spacing` provenance key are kept as historical records, not
current performance evidence. The fifth methodological lesson is the blunt
one: when hardware beats the model, suspect the model.

The live QSO adds the same lesson at system scale. Individual transmitter,
receiver, decoder and console pieces passed in isolation while the assembled
station failed. Progress came from measuring inside the running station:
archiving real receive windows with `--save-captures`, timing actual decode
workers, printing candidate offsets and repeating A/B tests rather than
trusting one observation. Rejection taxonomy is useful, but not absolute:
header rejection, `payload_fec_crc` and strong acquisition followed by failure
can vary from run to run on a marginal link.

The first five live-station defects are now fixed or guarded by tests:

* **Split bursts.** Back-to-back fixed receive windows could cut a randomly
  timed UHF burst across a capture boundary. `OverlappedCapture` carries a
  2 s tail forward and suppresses duplicates by candidate end position.
* **Duplicate suppression used mismatched units.** Candidate start and end
  offsets are reported in the receiver's decimated units, while the overlap was
  counted in front-end IQ samples. `decode_window()` now compares by fraction
  of the window, avoiding the bug that made every decoded candidate look like a
  duplicate in the carried tail.
* **Decode workers oversubscribed the host.** `MOSAIC_ACQUISITION_THREADS=1`
  does not constrain NumPy's BLAS pool. `_worker_cpu_budget()` uses
  `threadpool_limits(1)` and applies `os.nice(10)` once per worker process; the
  module-level guard matters because `os.nice()` is relative.
* **The station decoded silence.** `window_energy_ratio()` compares the loudest
  0.05 s slice to the median slice. The handoff records silence at 1.00-1.02,
  bursts at 36.6, and a 1.15 threshold that kept 4 of 50 real windows and
  skipped 46 without mismatches.
* **Console listeners could exit immediately.** `tools/mosaic_console.py`
  treats `--listen-seconds` as a complete bounded listen session even without
  `--once`, so a background listener no longer falls through to an interactive
  prompt and exits on stdin EOF.

A subsequent usability pass added two measurement-driven changes:

* **The station decoded too much silence around traffic.** The same loud-slice
  measurement now crops a receive window from the first loud slice to the last,
  with 0.5 s of guard on both sides. The span deliberately includes all loud
  slices, so overlapping stations stay in one decode. On
  `run-artifacts/solo-usrp-observer/raw_rx.cf32`, the crop is 1,200,000 to
  5,400,000 of 16,000,000 IQ samples and preserves the byte-identical payload.
  The carried overlap is rebased by subtracting the crop start.
* **USRP receive setup cost ate the duty cycle.** UHD rebuilds its streamer for
  every capture, costing about 2.0 s of deaf time per window on the B210. The
  USRP default receive window is now 16 s so that fixed cost is amortized; this
  became affordable only after decode cost followed the cropped traffic span.

Three more operational defects were found in that pass, bringing the running
total to eight:

* **Receive failures could busy-spin.** A stale capture process holding the B210
  made every receive fail instantly, burying the real error. `ModemStation`
  backs receive failures off exponentially up to one receive window.
* **SIGTERM could orphan a radio subprocess.** `tools/mosaic_modem.py` now turns
  SIGTERM into the same interrupt path as operator Ctrl-C, allowing normal
  teardown instead of leaving `usrp_io.py` holding the B210.
* **Send-and-exit console sessions could transmit nothing or close too soon.**
  `tools/mosaic_console.py --once` now accepts piped stdin as transmit lines
  and lingers briefly after `sendall()` so the modem can read the frame.

`tests/test_live_modem_radios.py` guards the live-radio fixes, including the
overlap carry, unit-converted duplicate suppression, energy-ratio squelch,
traffic cropping, concurrent-burst crop span and overlap rebasing.

One proposed FPGA-related live-modem "fix" is explicitly retracted. The source
shows `bladerf_transmit_command()` already falls back from its `fpga_bitstream`
argument to `config.fpga_bitstream`, so changing the caller was not the
decisive antenna-path fix. FPGA pinning remains required, but that particular
change was a no-op for the live QSO failure.

The HF channel-validation work added another process lesson. A sweep run with
an outer `ProcessPoolExecutor` at 24-32 workers oversubscribed the host because
`src/mosaic_hf/acquisition.py` already uses its own acquisition
`ThreadPoolExecutor`. The documented control is `MOSAIC_ACQUISITION_THREADS`;
when an outer process pool owns the cores it must be set to 1. The handoff
records 10 OS threads per trial process before the fix and 2 after setting
`MOSAIC_ACQUISITION_THREADS=1`. BLAS environment variables did not help because
the extra threads were the modem acquisition pool, not BLAS. The transferable
lesson is to measure the thread count instead of assuming an environment
variable took effect.

The first ITU-R F.1487 sweep also produced a false negative before the harness
was corrected. It passed a bare encoded burst to the decoder with no trailing
samples, so decoding could run out of buffer with `stream ends before coded
payload` for reasons unrelated to the channel. Two clues prevented that from
becoming a protocol claim: flat-channel results were nearly independent of SNR,
and total failure on a 0.5 ms / 0.1 Hz channel was implausible against an
80 ms symbol. Padding the receive buffer fixed the flat and good spot checks.
The same class of defect had already appeared in audio-path resampling, where
polyphase filter group delay required trailing room. A result that is flat
against a parameter it should depend on is treated as evidence of a harness
defect, not as proof of a robust modem.

### Transmitter cleanliness and ambient RF

Measured-on-hardware transmitter and site observations:

| Observation | Result | Evidence |
| --- | ---: | --- |
| Uncalibrated TX LO leakage, carrier to wanted signal | -56.3424566419605 dB | `run-artifacts/leakage-baseline/carrier-leakage-manifest.json` |
| Generated waveform DC content | -220 dB | `docs/RF-BENCH-HANDOFF.md` |
| TX DC calibration result | -16.203982223509964 dB carrier to signal | `run-artifacts/leakage-calibrated/carrier-leakage-manifest.json` |
| Effect of `calibrate dc tx` | about 40 dB worse | `docs/RF-BENCH-HANDOFF.md`, leakage manifests |
| Ambient carrier at 431.200 MHz with HackRF streaming | 15.128914018928498 dB above local floor | `run-artifacts/rx-lo-leakage-1/receiver-lo-leakage-manifest.json` |
| Ambient carrier at 431.200 MHz with HackRF idle | 16.046696200057355 dB above local floor | `run-artifacts/rx-lo-leakage-1/receiver-lo-leakage-manifest.json` |

The DC calibration must not be used for the measured setup. The ambient carrier
is unrelated to the modem and is present with all bench transmitters idle.

## Measured results

### Cabled hardware results

These are measured-on-hardware results over retained cabled captures.
Rows whose profile is `HF` are MOSAIC-HF profile tests; they are not HF-band RF
links.

| Artifact | Profile | Direction | Result | CFO / drift | Burst | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| `run-artifacts/vendor-bladerf-tx-hackrf-rx-20260803/` | HF | bladeRF -> HackRF | CRC-valid `A -> B`, text `x` | +1186.0 Hz / 0.0 Hz/s | not in manifest | `vendor-run-manifest.json` |
| `run-artifacts/vendor-hackrf-tx-bladerf-rx-20260803/` | HF | HackRF -> bladeRF | CRC-valid `B -> A`, text `x` | -1210.0 Hz / -1.0 Hz/s | not in manifest | `vendor-run-manifest.json` |
| `run-artifacts/vendor-bladerf-tx-hackrf-rx-full-20260803/` | HF | bladeRF -> HackRF | CRC-valid `MOSAIC-A -> MOSAIC-B`, text `hello-node` | +1289.0 Hz / 0.0 Hz/s | not in manifest | `vendor-run-manifest.json` |
| `run-artifacts/cabled-431200000-ve6slp-20260804/` | HF | bladeRF -> HackRF | CRC-valid | +1494.75 Hz / 0.0 Hz/s | not in manifest | `vendor-run-manifest.json` |
| `run-artifacts/uhf-cabled-1/` | UHF | bladeRF -> HackRF | CRC-valid | +468.0 Hz / 0.0 Hz/s | 1.29 / 1.31 s, complete | `vendor-run-manifest.json` |
| `run-artifacts/uhf-cabled-reverse-1/` | UHF | HackRF -> bladeRF | CRC-valid | -484.0 Hz / 0.0 Hz/s | 1.149 / 1.17 s, complete | `vendor-run-manifest.json` |

The 50 dB cabled safety facts record bladeRF TX conducted power of 0.0 dBm
(1.0 mW) at the TX connector and a 50 dB fixed 50-ohm pad in the cabled path
(`run-artifacts/measured-50db-cabled-facts-20260803.json`).

### On-air single-packet hardware results

All retained on-air results use bladeRF `txvga2` of 10 dB or less when the
bladeRF is the on-air transmitter, and the handoff states that the radios were
a few feet apart. Rows whose profile is `HF` are MOSAIC-HF profile tests over
the bench's UHF antenna path, not ionospheric HF contacts.

![Measured UHF waterfall and transmit spectrum](figures/measured_waterfall_spectrum.png)

The waterfall is measured on-air data from
`run-artifacts/solo-usrp-observer/raw_rx.cf32`: a USRP B210 observing one
MOSAIC-UHF packet with access signature 47 over the antenna path at
431.200 MHz. The plotted slice is representative of the retained 128 MB
complex64 capture; the spectrum panel shows the corresponding transmitted
profile containment. The weaker mirrored structure is not a second MOSAIC
station; it is already present in the retained bladeRF SC16 transmit stream as
a residual transmit-side image about 62 dB below the wanted component, and the
retained decode for this capture contains only `VE6SLP`.

| Artifact | Profile | Direction | Result | CFO / drift | Peak / floor | Burst | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `run-artifacts/on-air-431200000-fpga016-1/` | HF | bladeRF -> HackRF | CRC-valid | +467.75 Hz / -1.0 Hz/s | peak 0.31396645307540894; floor not recorded | 17.112 / 17.14 s, complete | `vendor-run-manifest.json` |
| `run-artifacts/on-air-reverse-1/` | HF | HackRF -> bladeRF | CRC-valid | -445.25 Hz / 0.0 Hz/s | peak 0.17589831352233887; floor not recorded | superseded guard reported 22.0 / 15.54 s | `vendor-run-manifest.json` and handoff note |
| `run-artifacts/on-air-txtest-20260805/` | HF | bladeRF -> HackRF | CRC-valid | +419.25 Hz / 0.0 Hz/s | peak 0.3709171712398529; floor not recorded | 16.473 / 16.5 s, complete | `vendor-run-manifest.json` |
| `run-artifacts/on-air-txtest-20260805-reply/` | HF | HackRF -> bladeRF | CRC-valid | -442.25 Hz / 0.0 Hz/s | peak 0.180165097117424; floor 0.07945477217435837 | 15.189 / 15.22 s, complete | `vendor-run-manifest.json` |
| `run-artifacts/uhf-on-air-1/` | UHF | bladeRF -> HackRF | CRC-valid | +724.0 Hz / 0.0 Hz/s | peak 0.33341526985168457; floor 0.008717949502170086 | 1.29 / 1.31 s, complete | `vendor-run-manifest.json` |
| `run-artifacts/uhf-on-air-reverse-1/` | UHF | HackRF -> bladeRF | CRC-valid | -724.0 Hz / 0.0 Hz/s | peak 0.10893066972494125; floor 0.08279542624950409 | 1.169 / 1.19 s, complete | `vendor-run-manifest.json` |
| `run-artifacts/solo-usrp/` | UHF | USRP B210 -> HackRF | CRC-valid `VE6NAS` | +692.0 Hz / 0.0 Hz/s | peak 100 ms mean magnitude 0.0833497866988182 | no `burst` field; envelope profile retained | `three-radio-manifest.json` |
| `run-artifacts/solo-bladerf/` | UHF | bladeRF -> HackRF | CRC-valid `VE6SLP` | +804.0 Hz / 0.0 Hz/s | peak 100 ms mean magnitude 0.09547556936740875 | no `burst` field; envelope profile retained | `three-radio-manifest.json` |
| `run-artifacts/solo-bladerf-txvga10/` | UHF | bladeRF -> HackRF | CRC-valid `VE6SLP` | +812.0 Hz / drift not recorded | peak 100 ms mean magnitude 0.30271145701408386 | no `burst` field; envelope profile retained | `three-radio-manifest.json` |
| `run-artifacts/solo-usrp-observer/` | UHF | bladeRF -> USRP B210 | CRC-valid `VE6SLP` | +100.0 Hz / drift not recorded | peak 100 ms mean magnitude 0.04625697433948517 | B210 receive gain 52.0 dB | `three-radio-manifest.json`, `usrp-rx.log` |

The reverse UHF run is a valid packet, but its peak is close to the recorded
floor. The handoff describes this as not a comfortable link.

All three bench radios have now been exercised in real RF roles: bladeRF as a
transmitter, HackRF as a decoding receiver, and B210 as both a transmitter and
a decoding receiver. That does not make the front ends equivalent, but it does
show the waveform is not receivable only by one specific SDR path.

`run-artifacts/hf-rf-link/offline-decode.json` is also measured RF evidence,
but it is deliberately reported as a partial: the MOSAIC-HF waveform radiated
on the UHF SDR carrier acquired strongly and attributed `VE6SLP-1` first, then
failed at header decode with no CRC-valid sender. It is not an on-air HF
contact and not a successful direct-radiated MOSAIC-HF packet.

### On-air independent multi-transmitter hardware result

![Multi-user recovery: on-air UHF proof, simulated UHF scaling, HF fade ceiling](figures/multiuser_recovery.png)

The left panel summarizes retained radiated UHF multi-user decodes. The centre
panel is current-harness no-fade simulation (complete recovery through 16
stations; single-seed 22/24 and 26/32 beyond that) with faded eight-station
means marked separately. The right panel is the HF shared-channel simulation
ceiling under ITU-style fading. Artifact paths are cited in the tables below.

`run-artifacts/three-radio-uhf-2/three-radio-manifest.json` is the first
retained result in this repository where two physically independent
transmitters are decoded by a third radio. It is stronger evidence than the
composite runs because the transmitters do not share a local oscillator, power
amplifier, timebase, sample clock or exact digital start time; their relative
keying is only as repeatable as the operating-system scheduler.

| Role | Radio | Station and payload | RF settings | Evidence |
| --- | --- | --- | --- | --- |
| Transmitter 1 | bladeRF x115 | `VE6SLP -> VA6GA`, `independent alpha de VE6SLP`, access signature `0x2f` | `txvga1` -4 dB, `txvga2` 0 dB | `three-radio-manifest.json` |
| Transmitter 2 | USRP B210 | `VE6NAS -> VA6SLP`, `independent bravo de VE6NAS`, access signature `0x5b` | TX gain 52.0 dB | `three-radio-manifest.json`, `usrp-tx.log` |
| Observer | HackRF One | receiver-only bounded SIC decode | LNA 8 dB, VGA 32 dB, 2,000,000 sample/s, 8 s capture | `three-radio-manifest.json`, `hackrf-rx.log` |

Measured receiver result:

| Artifact | Profile | Result | CFOs | Timing in 96 ksample/s audio | Stop condition | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| `run-artifacts/three-radio-uhf-2/` | UHF | CRC-valid `VE6SLP`, `VE6NAS` | `VE6SLP` +804.0 Hz; `VE6NAS` +700.0 Hz | `VE6SLP` 2.9784166666666665-4.228416666666667 s; `VE6NAS` 2.9959375-4.2659375 s | all unique catalogue entries cancelled | `three-radio-manifest.json` |
| `run-artifacts/three-radio-uhf-4station/` | UHF | CRC-valid `VE6NAS`, `VA6GA`, `VE6SLP`, `VA6SLP` | +692.0, +604.0, +796.0, +908.0 Hz | decoded extents span 2.98046875-4.610666666666667 s | all unique catalogue entries cancelled | `three-radio-manifest.json` |
| `run-artifacts/onair-4station-imbalanced/` | UHF | CRC-valid `VA6GA`, `VE6NAS`, `VE6SLP`, `VA6SLP` | +1308.0, +1196.0, +796.0, +444.0 Hz | decoded extents span 3.033-4.563 s | all unique catalogue entries cancelled | `three-radio-manifest.json` |
| `run-artifacts/onair-8station/` | UHF | CRC-valid `VE6NAS-1`, `VE6SLP-2`, `VE6SLP-1`, `VE6NAS-2`, `VA6SLP-1`, `VA6GA-2`, `VA6SLP-2`, `VA6GA-1` | -44.0, +764.0, +340.0, +956.0, +28.0, +548.0, +940.0, -300.0 Hz | decoded extents span 3.0403541666666666-4.59759375 s | all unique catalogue entries cancelled | `three-radio-manifest.json` |
| `run-artifacts/onair-12station/` | UHF | CRC-valid `VE6SLP-1`, `VE6SLP-2`, `VE6SLP-3`, `VE6NAS-1`, `VE6NAS-2`, `VE6NAS-3`, `VA6GA-1`, `VA6GA-2`, `VA6GA-3`, `VA6SLP-1`, `VA6SLP-2`, `VA6SLP-3` | +404.0, +820.0, +20.0, +1012.0, +252.0, +652.0, -236.0, +604.0, +76.0, +996.0, -156.0, +428.0 Hz | decoded extents span 2.9991875-4.66775 s | all unique catalogue entries cancelled | `offline-decode.json` |
| `run-artifacts/onair-12station-random/` | UHF | CRC-valid `VE6SLP-1`, `VE6SLP-2`, `VE6SLP-3`, `VE6NAS-1`, `VE6NAS-2`, `VE6NAS-3`, `VA6GA-1`, `VA6GA-2`, `VA6GA-3`, `VA6SLP-1`, `VA6SLP-2`, `VA6SLP-3` | +564.0, +460.0, +692.0, +1156.0, +1108.0, +796.0, -260.0, +996.0, +692.0, +108.0, -100.0, +1012.0 Hz | decoded extents span 3.131177083333333-4.4988125 s | all unique catalogue entries cancelled | `three-radio-manifest.json` |

For `three-radio-uhf-2`, the two decoded extents overlap for
1.2324791666666667 s, computed from the manifest sample indices and the
MOSAIC-UHF 96,000 sample/s audio rate. The residual power trajectory in that
receiver record is
0.0012211621653254354 -> 0.0006059456766651809 -> 0.00011367312073785727 after
CRC-gated cancellation of `VE6SLP` and then `VE6NAS`.

The four-station run is a stronger receiver-load test, but not a four-radio
claim: it uses two independent RF chains, each carrying two digitally
superimposed logical stations. The bladeRF chain carries `VE6SLP` and
`VA6SLP`; the USRP chain carries `VE6NAS` and `VA6GA`.

The imbalanced four-station run is a stronger unequal-power on-air case. It
puts three logical stations on the bladeRF and one on the B210. Both radios'
waveforms are peak-normalized to 0.9499999284744263 before transmission, so the
bladeRF composite's scale is 0.221676317396694 while the single-station B210
scale is 0.5033002154561952. In the receiver fit, the B210-carried `VA6GA`
gain is 0.08786596575527592; the three bladeRF-carried gains are
0.04468409135754174, 0.04516445963073307 and 0.043744873770962436, putting
`VA6GA` 5.780477366688979-6.057870382925946 dB above them. This is consistent
with the 7.12215636413299 dB digital-scale difference between the B210 and
bladeRF waveforms, partly offset by the solo-calibration result that the
bladeRF at `txvga2` 0 is 1.1797552840885095 dB hotter at the HackRF observer.
All four still decode, with peak 100 ms mean magnitude 0.11614685505628586;
the run sits comfortably inside the -10 dB no-channel near/far bound rather
than at its edge. The operational hazard is that the number of logical stations
composited on one radio silently
sets their transmit level; adding another station to a composite attenuates the
others. That is a bench artifact of the composite generator, not a protocol
property.

The eight-station run uses SSID-style
suffixes to make `VE6SLP-1` and `VE6SLP-2`, for example, distinct station
identities while both are still VE6SLP transmissions. This fixed a bench
identification limitation rather than changing the protocol: `addressed_payload()`
allows source and destination strings up to 63 bytes, and the three-radio tool
now accepts a source that is either the licensed callsign or that callsign plus
an SSID suffix. The run puts four stations on the bladeRF and four on the B210,
observed by the HackRF; the manifest records bladeRF `txvga2` 10, B210 gain
64.0 dB, peak envelope 0.34632688760757446 and minimum envelope bucket
0.008672110736370087. The waveform digital scales were 0.21578100857516117 on
the bladeRF and 0.18644231263234207 on the B210. The handoff records the run as
an example of applying the received-level rule predictively: the higher bladeRF
and B210 gains were checked in a prepare-only run before radiating, rather than
by diagnosing a failed on-air decode.

The twelve-station runs are now the largest retained on-air decodes. The
handoff records six logical stations composited on the bladeRF and six on the
B210, again observed by the HackRF. In `run-artifacts/onair-12station/`, those
twelve stations were deliberately given even arrivals from 0.00 s to 0.66 s in
0.06 s steps and well-separated carrier offsets. The first decode attempt was
interrupted after the radios had already finished, so the retained CS8 observer
capture was decoded offline with `tools/decode_capture.py`;
`offline-decode.json` ties the result to
`run-artifacts/onair-12station/raw_rx.cs8` with SHA-256
`7fdb4bbad1d5f03e4f096fe3724d3fe90aa86cce8f6e5df43408bc8fcc0129f4`. It
recovers all twelve catalogue identities and stops with
`all unique catalogue entries cancelled`.

The follow-up `run-artifacts/onair-12station-random/` used randomly drawn
arrivals and carrier offsets for the same twelve stations; the handoff records
seed 4242, with the closest pair 1.0 ms apart in arrival and 2.0 Hz apart in
offset. It also recovers all twelve stations and stops with
`all unique catalogue entries cancelled`. The earlier claim that the
twelve-station on-air success depended on coordinated arrival is therefore
withdrawn.

### Shared-channel status by profile

The central shared-channel claim is proven on MOSAIC-UHF hardware.
`run-artifacts/onair-12station-random/three-radio-manifest.json` is radiated,
completed on air, uses independent `bladerf` and `usrp` transmitters, has
catalogue size 12, and contains all twelve expected CRC-valid sender IDs:
`VE6SLP-1`, `VE6SLP-2`, `VE6SLP-3`, `VE6NAS-1`, `VE6NAS-2`, `VE6NAS-3`,
`VA6GA-1`, `VA6GA-2`, `VA6GA-3`, `VA6SLP-1`, `VA6SLP-2` and `VA6SLP-3`.
`run-artifacts/onair-8station/` recovers 8/8 and
`run-artifacts/onair-4station-imbalanced/` recovers 4/4 on the same radiated,
two-independent-transmitter basis. The honest nuance is physical: two radios
carried twelve logical stations, so this is not a twelve-converter claim. The
two RF carriers are nevertheless genuinely independent hardware with no shared
clock or timing reference, decoded by a third radio.

The same claim is **not** proven on MOSAIC-HF. Auditing the retained
multi-station artifacts shows that the on-air shared-channel records above all
carry `band_profile: uhf`; HF evidence had been single-packet until the
simulated `hf-audio` scaling runs described below.

### Live keyboard QSO over the antenna path

The handoff now records a bidirectional live keyboard QSO at 431.200 MHz
between two independent stations running the live KISS modem rather than
offline bench scripts:

| Console | Delivered text | Evidence |
| --- | --- | --- |
| VE6SLP | `[08:54:58Z] <<< VE6SLP DE VA6GA UR 599 IN CALGARY K` | `docs/RF-BENCH-HANDOFF.md` |
| VA6GA | `[09:05:44Z] <<< VA6GA DE VE6SLP AGN 73 SK` | `docs/RF-BENCH-HANDOFF.md` |

The station roles were VE6SLP using bladeRF transmit and HackRF receive, and
VA6GA using the B210 for transmit and receive. The measured operating settings
recorded for this path are B210 receive gain 52.0 dB, bladeRF `txvga2` 10 and
`txvga1` -4. The handoff explicitly warns that B210 receive gain 70.0 dB
compresses the front end in this feet-apart bench geometry.

This result corrects the previous "antenna path blocked" interpretation. The
antenna path was not the missing piece; the live software was.

The first live QSO was minutes-latency because the station decoded whole
receive windows. The usability pass crops each non-silent receive window to the
traffic span before decoding. On the retained real B210 observer capture
`run-artifacts/solo-usrp-observer/raw_rx.cf32`, which contains 16,000,000 IQ
samples over 8 s, the crop spans samples 1,200,000 to 5,400,000. The handoff
records 19.0 s for the full-window decode and 4.6 s for the cropped decode, a
4.2x improvement, with the same payload
`b'MSA1\x06\x05\x00\x11VE6SLPVA6GAB210 solo observe'`. Re-running the same
measurement during local verification produced the same crop and byte-identical
payload, with a similar roughly 4x speed-up.

The B210 receive window was also lengthened to amortize UHD setup cost. The
handoff records about 2.0 s of B210 deaf time per receive window, or about 80%
duty cycle with 8 s windows, while the HackRF has effectively no such gap. In
one measured one-way run, 8 s USRP windows delivered 2 of 5 messages and 16 s
windows delivered 5 of 5. At the 16 s default, a symmetric five-message
exchange between VE6SLP and VA6GA delivered 4 of 5 messages in both directions,
with 0 rejected frames and 0 station errors. That is usable, but it is not
5 of 5 and it is still a small-count result. The handoff records an end-to-end
latency example of about 30 s: a message queued at 09:35:47 was on the far
operator's screen at 09:36:22.

The preceding attempt, `run-artifacts/three-radio-uhf-1/`, is also useful
engineering evidence. It used the same independent-radio topology but bladeRF
`txvga2` was 10 dB. The receiver recovered only `VE6SLP`, rejected `VE6NAS` at
the header stage, and stopped with `no CRC-valid candidate in residual
generation`. The successful run lowered bladeRF `txvga2` to 0 dB rather than
raising the B210, keeping total radiated power down while bringing the received
signals closer together. `tools/three_radio_congested.py` also carries a
`--bladerf-lead-seconds` option, defaulting to 1.05 s, because the bladeRF CLI
loads the FPGA image before streaming and otherwise keys late relative to the
other process.

The solo calibration captures explain the power-balance fix. Using the same
HackRF observer and the 100 ms envelope buckets, the B210 at gain 52 measured a
peak mean magnitude of 0.0833497866988182. The bladeRF at `txvga2` 0 measured
0.09547556936740875, which is +1.1797552840885095 dB relative to that B210
reference. The bladeRF at `txvga2` 10 measured 0.30271145701408386, which is
+11.202487313470115 dB relative to the same reference. That 11.2 dB imbalance
sits on the no-channel software near/far bound already reported in the handoff:
UHF recovers both packets at -10 dB but loses one by -15 dB. The failed
three-radio attempt is therefore consistent with a link-budget problem, not an
arbitrary station-count limit.

### On-air congested/composite hardware results

These are measured-on-hardware captures, but the congested signal was produced
by **one radio transmitting a digitally superimposed composite**. The component
bursts have distinct start times, carrier offsets and amplitudes, but they
share one local oscillator, one power amplifier and one timebase. These runs
therefore test receiver cancellation under controlled overlap; the independent
three-radio result above is the stronger hardware claim for uncoordinated
transmitters.

| Artifact | Profile | Composite transmitters | Receiver result | Burst | Stop condition | Evidence |
| --- | --- | --- | --- | --- | --- | --- |
| `run-artifacts/on-air-collision-1/` | HF | second burst +3.5 s, +180 Hz, amplitude 0.7 | CRC-valid `VE6SLP` only; `VA6SLP` rejected at header | 16.769 / 16.8 s, complete | no CRC-valid candidate in residual generation | `collision-run-manifest.json` |
| `run-artifacts/on-air-collision-2/` | HF | second burst +3.5 s, +180 Hz, amplitude 1.0 | CRC-valid `VE6SLP`, `VA6SLP` | 16.769 / 16.8 s, complete | all unique catalogue entries cancelled | `collision-run-manifest.json` |
| `run-artifacts/uhf-on-air-congested-1/` | UHF | second burst starts at 0.85 s, +180 Hz, amplitude 0.85 | CRC-valid `VE6SLP`, `VA6SLP` | 1.759 / 1.98 s, incomplete envelope | all unique catalogue entries cancelled | `collision-run-manifest.json` |
| `run-artifacts/usrp-observer-2tx-hi/` | UHF | B210 observer; bladeRF radiates two asynchronous logical stations, second +0.35 s, +600 Hz, amplitude 0.8 | CRC-valid `VE6SLP`, `VE6NAS` | peak 100 ms mean magnitude 0.0802842453122139; minimum envelope bucket 0.005275574978441 | all unique catalogue entries cancelled | `three-radio-manifest.json`, `usrp-rx.log` |

The UHF congested run's `burst_complete` is false. The handoff explains that
the envelope detector loses the quieter transmitter's tail, while the receiver
still decodes from the samples.

The B210 observer run is a front-end independence result, not an
independent-transmitter result: both logical stations are radiated by the
bladeRF in one composite waveform, while the independent part is the receiving
station.

### Simulated/software results

These are not on-air measurements.

| Result | Label | Evidence |
| --- | --- | --- |
| Both MOSAIC-HF and MOSAIC-UHF show an Eb/N0 cliff at about +1 dB in a matched-noise software sweep. | Simulated | `docs/RF-BENCH-HANDOFF.md` |
| MOSAIC-HF audio ITU-R F.1487-style simulations recover 240/240 through -9 dB SNR (7.8 dB Eb/N0); the flat AWGN threshold is bracketed between +1.8 and -1.2 dB Eb/N0, while the fading cases cost about 3 dB near threshold. | Simulated channel model, not on-air HF | `run-artifacts/hf-channel/itu-f1487.json`, `itu-f1487-waterfall.json`, `itu-f1487-cliff.json` |
| MOSAIC-HF audio multi-user simulations work without fading but under ITU-style HF fading recover only 1-2 stations at 4 and 8 stations, stopping with no CRC-valid residual candidate. | Simulated channel model, not on-air HF | `run-artifacts/hf-multiuser/hf-audio-nofade.json`, `run-artifacts/hf-multiuser/hf-audio-scaling.json` |
| At fixed transmit power, the UHF profile collects 12 dB more noise in its wider bandwidth. | Analytical/software-source claim in handoff | `docs/RF-BENCH-HANDOFF.md` |
| UHF software test cancels a neighbour 6 dB down in a two-transmitter composite. | Simulated, no RF channel | `tests/test_profiles.py` |
| Handoff near/far sweep reports HF: 2/2 at 0 dB, -3 dB and -10 dB; UHF: 2/2 at 0 dB, -3 dB, -6 dB and -10 dB, then 1/2 at -15 dB and -25 dB. | Simulated | `docs/RF-BENCH-HANDOFF.md` |
| Scaling files without an `even_spacing` key are superseded and must not be quoted as current MOSAIC performance. For the old unfaded files, the cause was the sweep tool's signal generator: it produced double-sideband mirror images instead of true frequency offsets. | Superseded simulated scaling results | `run-artifacts/scaling/README.md`, `uhf-balanced-12-seeds.json`, `uhf-balanced-knee.json`, `uhf-balanced-many.json`, `uhf-nofade.json`, `uhf-nofade-8.json`, `uhf-faded-seeds.json`, `uhf-spread0-8.json`, `uhf-spread12-8.json`, `hf-balanced.json` |
| Current no-fade UHF random-arrival seeds with 0.0 dB amplitude spread, 0.6 s arrival span and +/-800.0 Hz CFO spread recovered 12/12 and 12/12. Both stopped with `all unique catalogue entries cancelled`. | Simulated; current harness, no fading, no AWGN added by `tools/many_station_scaling.py` | `run-artifacts/scaling/uhf-analytic-12-seeds.json`, `tools/many_station_scaling.py` |
| Current no-fade UHF random-arrival seed-7 re-sweep with 0.0 dB amplitude spread recovered 8/8, 10/10, 12/12 and 16/16. Every run stopped with `all unique catalogue entries cancelled`. | Simulated; current harness, no fading, no AWGN added by `tools/many_station_scaling.py` | `run-artifacts/scaling/uhf-analytic-resweep.json`, `tools/many_station_scaling.py` |
| Current no-fade UHF random-arrival seed-7 deep sweep with 0.0 dB amplitude spread recovered 22/24 and 26/32. Both larger runs stopped with `no CRC-valid candidate in residual generation after copy-only interference projection`. | Simulated; current harness, no fading, no AWGN added by `tools/many_station_scaling.py`; single seed at each count | `run-artifacts/scaling/uhf-analytic-deep.json`, `tools/many_station_scaling.py` |
| Current no-fade UHF even-spacing twelve-station runs recovered 8/12 at 0.0 dB amplitude spread and 5/12 at 6.0 dB spread. These are coordinated layouts, not uncoordinated random-arrival traffic. | Simulated; current harness, no fading, no AWGN added by `tools/many_station_scaling.py`; even spacing | `run-artifacts/scaling/uhf-even-12-seeds.json`, `run-artifacts/scaling/uhf-even-12-spread6.json` |
| Current-code eight-station faded UHF recheck with 6.0 dB amplitude spread recovered 12/32 station opportunities over four independent draws, an outage rate of 62.5%. | Simulated; flat fading, no AWGN added by `tools/many_station_scaling.py` | `run-artifacts/scaling/uhf-faded-recheck.json`, `tools/many_station_scaling.py` |
| Current-code eight-station faded UHF balanced run with 0.0 dB amplitude spread recovered 15/32 station opportunities over four independent draws, an outage rate of 53.1%. | Simulated; flat fading, no AWGN added by `tools/many_station_scaling.py` | `run-artifacts/scaling/uhf-faded-balanced.json`, `tools/many_station_scaling.py` |

The simulated near/far sweep bounds the cancellation algorithm, not the radio
link. The previous no-fade population scaling interpretation is withdrawn. In
the defective unfaded path, each station's real audio was cast to complex and
rotated, then emitted as `real(audio * exp(j2*pi*f*t))`, which is
`audio * cos(2*pi*f*t)`. That is double-sideband AM: it halves the wanted
component and adds a mirror image of the entire station reflected about DC.
The mirror matches no catalogue entry, so SIC cannot cancel it, and the error
scales with station count. `src/mosaic_hf/channel.py` always applied a Hilbert
transform first; only the unfaded branch in `tools/many_station_scaling.py`
skipped it. `tests/test_many_station_scaling.py` now pins this defect by
asserting that the image is more than 40 dB below the wanted component.

The current scaling provenance rule is simple: quote only files whose result
records carry an `even_spacing` key. Files without that key are superseded;
`run-artifacts/scaling/README.md` shows that old unfaded files can disagree
sharply with current code on identical inputs because the sweep tool generated
the wrong signal, not because the receiver or protocol changed. The `analytic`
and `balanced` words in filenames are lab labels, not tool modes or distinct
channel models.

The fixed no-fade branch now agrees with the twelve-station hardware evidence
for the retained seeds that have landed: the same two twelve-station seeds that
previously failed in superseded files now recover 12/12 and 12/12, and the
current seed-7 random-arrival sweep recovers every station through 16:
8/8, 10/10, 12/12 and 16/16. Sixteen was the size of the signature pool used by
that run, not a receiver limit. `tools/many_station_scaling.py` now contains
32 signatures, and `tests/test_many_station_scaling.py` checks that those
signatures yield 32 distinct preamble sequences, which is what acquisition
actually observes. The current deep random-arrival sweep recovers 22/24 and
26/32; those two large-count points are single seeds, so they show graceful
degradation rather than an outage rate.

The old scaling artifacts provide no current congestion-collapse curve, HF
scaling curve or near/far population rank-order conclusion to quote. Current
even-spacing files are also not interchangeable with random-arrival results:
even spacing is a coordinated layout, while random arrival and carrier offsets
are the clean uncoordinated case.

The faded current-harness runs show a separate many-station problem: fading can
set delivery rate when the packet has no diversity across the fade. This is a
link-budget outage, not a receiver limitation; when a station is in a deep fade
for the duration of its burst, the energy never arrives for any receiver to
recover. The no-fade results above are pure mutual-interference simulations
with no AWGN added, so they do not measure delivery in a fading channel.
Combined with the current 53.1%-62.5% eight-station flat-fading outage results,
the live many-station picture remains materially worse than any no-fade count
alone suggests.

The current headline is therefore narrower and stronger than the withdrawn
collapse story. In a clean no-fade, no-AWGN channel with random arrivals, every
station is recovered through 16, then the single-seed deep sweep recovers 22/24
and 26/32. Under flat fading, only 2 to 5 of 8 stations recover in each current
draw: 62.5% total outage with 6 dB amplitude spread and 53.1% with levels
balanced. Balancing levels improves that eight-station faded result by about
nine percentage points, but does not engineer the outage away. The binding
constraint shown by the retained simulations is the fading channel, not
multi-user interference in the clean channel.


## HF audio channel and real-time receive validation

### ITU-R F.1487-style simulated HF channels

`run-artifacts/hf-channel/itu-f1487.json` and
`run-artifacts/hf-channel/itu-f1487-waterfall.json` record the current
`tools/hf_channel_validation.py` sweeps for `MOSAIC_HF_AUDIO`. These are
simulated channel-model results, not on-air HF contacts and not measurements of
ionospheric propagation.

The first table is a sanity floor for multipath and Doppler behaviour, not a
noise-sensitivity headline. Because MOSAIC-HF uses `B_ref = 2400 Hz` and
`R_b = 50 bit/s`, each SNR value corresponds to `Eb/N0 = SNR + 16.8 dB`; these
initial points therefore have enormous energy-per-bit margin. Each cell
contains 10 trials.

| Channel | Delay spread | Doppler | 20 dB SNR / 36.8 dB Eb/N0 | 15 dB / 31.8 dB | 10 dB / 26.8 dB | 5 dB / 21.8 dB |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| Flat | 0.0 ms | 0.0 Hz | 10/10 | 10/10 | 10/10 | 10/10 |
| Good | 0.5 ms | 0.1 Hz | 10/10 | 10/10 | 10/10 | 10/10 |
| Moderate | 1.0 ms | 0.5 Hz | 10/10 | 10/10 | 10/10 | 10/10 |
| Poor | 2.0 ms | 1.0 Hz | 10/10 | 10/10 | 10/10 | 10/10 |

The aggregate sanity-floor result is 160/160 recovered. The physical
interpretation is that the MOSAIC-HF 80 ms symbol is 40 times longer than the
poor channel's 2 ms delay spread, so multipath smears across 2.5% of a symbol.
The non-coherent 16-FSK detector also does not require carrier phase tracking,
so a 1 Hz Doppler process is not a phase-lock problem for the demodulator.

The follow-up low-SNR waterfall extended the same four channels down to
-9 dB SNR, still in the 2400 Hz reference bandwidth. Each cell contains
12 trials.

| Channel | Delay spread | Doppler | 2 dB SNR / 18.8 dB Eb/N0 | 0 dB / 16.8 dB | -3 dB / 13.8 dB | -6 dB / 10.8 dB | -9 dB / 7.8 dB |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Flat | 0.0 ms | 0.0 Hz | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |
| Good | 0.5 ms | 0.1 Hz | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |
| Moderate | 1.0 ms | 0.5 Hz | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |
| Poor | 2.0 ms | 1.0 Hz | 12/12 | 12/12 | 12/12 | 12/12 | 12/12 |

The aggregate low-SNR waterfall result is 240/240 recovered. That is expected:
-9 dB SNR is still 7.8 dB Eb/N0 after the 16.8 dB processing gain of the
50 bit/s HF profile.

`run-artifacts/hf-channel/itu-f1487-cliff.json` then brackets the sensitivity
cliff. Each cell contains 12 trials.

![MOSAIC-HF ITU-R F.1487-style sensitivity waterfall](figures/hf_itu_sensitivity.png)

| SNR | Eb/N0 | Flat | Good | Moderate | Poor |
| ---: | ---: | ---: | ---: | ---: | ---: |
| -12 dB | +4.8 dB | 12/12 | 8/12 | 7/12 | 10/12 |
| -15 dB | +1.8 dB | 12/12 | 0/12 | 0/12 | 0/12 |
| -18 dB | -1.2 dB | 0/12 | 0/12 | 0/12 | 0/12 |
| -21 dB | -4.2 dB | 0/12 | 0/12 | 0/12 | 0/12 |
| -24 dB | -7.2 dB | 0/12 | 0/12 | 0/12 | 0/12 |

The flat AWGN threshold lies between the tested points of +1.8 and -1.2 dB
Eb/N0: the flat channel is 12/12 at -15 dB SNR and 0/12 at -18 dB SNR. The
transition from full recovery to zero recovery over one 3 dB step is expected
for a well-matched K=7 rate-1/2 convolutional code, not a receiver instability.

The three fading channels all remain decodable at -12 dB SNR (+4.8 dB Eb/N0)
and all fail at -15 dB SNR (+1.8 dB Eb/N0), where the flat channel is still
perfect. The retained data therefore show about a 3 dB threshold cost for the
ITU-R F.1487-style multipath and Doppler cases. The -12 dB faded counts are
not ranked against each other: with 12 trials, their binomial standard error is
about 1.4 counts, so 8/12, 7/12 and 10/12 are overlapping small-sample
outcomes. The supported statement is that all three fading channels degrade
together near threshold, recovering roughly 60-85% at that point.

### HF limits one-pager

![MOSAIC-HF alpha limits: sensitivity cliff, multi-user fade ceiling, proven vs open](figures/hf_limits_summary.png)

MOSAIC-HF at alpha is a proven *waveform and single-user decoder* with a clear
shared-channel hole. The figure compresses four facts:

1. **Single-user sensitivity (simulated F.1487).** Flat AWGN brackets between
   +1.8 and −1.2 dB Eb/N0 (`Eb/N0 = SNR_2400Hz + 10·log₁₀(2400/50) = SNR + 16.8 dB`).
   Good/moderate/poor multipath cost about 3 dB near threshold.
2. **Shared-channel no-fade vs fade (simulated `hf-audio`).** Without fading the
   receiver peels through four stations and most of eight. Under ITU-style
   fading the absolute recovered count saturates near one or two stations.
3. **Proven checklist.** Real converter loopback, streaming throughput checks
   and single-user channel-model cliffs are in; on-air HF contacts,
   transceiver-in-the-loop ionospheric runs and faded multi-user success are
   out.
4. **Mechanism.** Constant-gain cancellation over a ~13 s packet leaves a
   time-varying residual that buries weaker users after the first peel. UHF
   multi-user on-air success does not contradict this: the lab path is
   essentially static.

### HF shared-channel simulation: no-fade success, fading collapse

`tools/many_station_scaling.py --profile hf-audio` now measures concurrent
MOSAIC-HF audio populations with independent start times, carrier offsets,
amplitudes and seeded channel processes. These are simulations, not on-air HF
contacts. The retained artifacts are
`run-artifacts/hf-multiuser/hf-audio-nofade.json` and
`run-artifacts/hf-multiuser/hf-audio-scaling.json`.

| Stations | No fading | ITU-style HF fading |
| ---: | --- | --- |
| 2 | 2/2, 2/2, 2/2 | 2/2, 1/2, 2/2 |
| 4 | 4/4, 4/4, 4/4 | 1/4, 1/4, 2/4 |
| 8 | 8/8, 6/8, 6/8 | 1/8, 2/8, 2/8 |

The no-fade control isolates the variable: in a static channel the HF receiver
recovers all stations through four and most or all of eight. Under ITU-style
HF fading, the absolute recovered count saturates at one or two even as the
population grows; eight stations yields the same absolute count as two. That
is a hard ceiling in this receiver, not graceful degradation. Non-complete
faded runs stop with `no CRC-valid candidate in residual generation`, and the
four- and eight-station faded failures use the copy-only interference
projection stop path.

The mechanism is cancellation error in a time-varying channel. SIC reconstructs
the strongest decoded station and subtracts it, but the replica is only as good
as the channel estimate. A MOSAIC-HF packet runs about 13 s and an ionospheric
channel does not hold still for 13 s. The first station can decode before any
cancellation is needed; later stations depend on a subtraction whose residual
can bury what remains underneath. This is a receiver limitation, not a waveform
limitation: the one-station waveform decodes through the F.1487 channel set,
while the current canceller lacks an in-packet channel tracker.

This also explains why UHF escapes the failure in the retained hardware. Radios
a few feet apart in a static lab present an essentially non-fading channel,
matching the no-fade column where HF also works. UHF does not have a better
receiver; its retained shared-channel runs simply do not exercise the
time-varying HF channel that breaks cancellation.

A dummy-load HF transceiver test would likewise reproduce the no-fade column.
It would be valuable for qualifying rig audio, ALC behaviour, drive level and
keying, but it would not predict on-air shared-channel behaviour because the
dummy load removes the Doppler and multipath that break HF multi-user
cancellation.

The real converter path reinforces the headroom issue. In
`run-artifacts/hf-multiuser/real-2.json`, two overlapping stations through the
Scarlett 4i4 converters recovered 1/2 when the second station was 4.6 dB
weaker; `run-artifacts/hf-multiuser/real-2-equal.json` recovered 0/2 at equal
power. The same unequal mixture decoded offline at 2/2 in
`run-artifacts/hf-multiuser/offline-2.json`. Equal power is hard for peeling
because there is no strongest station to remove first, and the converter path
reduces headroom because transmit drive is capped at 0.25 peak to avoid SSB
ALC while summing stations raises crest factor.

### Continuous-reception real-time requirement

A continuous receiver must decode faster than real time. Otherwise backlog and
operator-visible latency grow for as long as the station runs. This is a
correctness property of the live modem, not merely an optimization target.

The original MOSAIC-HF acquisition grid remains appropriate for free-running
SDR oscillators: it searches +/-2000 Hz and drift hypotheses. `MOSAIC_HF_AUDIO`
uses the same waveform as MOSAIC-HF but narrows the acquisition search to
+/-100 Hz at 12.5 Hz coarse steps and removes drift hypotheses. The reason is
physical. A transceiver audio path occupies 200-2800 Hz inside an SSB filter;
a 2000 Hz dial/audio error would move the waveform to 2200-4800 Hz, outside
the passband. What remains is rig clock error between stations tuned to the
same dial frequency; the source notes that 1 ppm at 14 MHz is 14 Hz each, so
+/-100 Hz is generous. The source also records that the wider search decodes a
13 s packet in 273 s, about 19 times slower than real time.

Local retained real-time checks with `tools/hf_realtime_check.py` use simulated
20 dB SNR streams, 20 s receive windows and 15 s overlap. They exercise the
streaming requirement that a window may contain more than one transmission: the
consumer advances by `PacketDecodeResult.packet_end_sample` rather than
stopping at the first packet.

| Artifact | Audio duration | Transmissions | Decode time | Real-time factor | Recovered | Median latency | Worst latency | Backlog |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| `run-artifacts/hf-realtime-check/minutes5.txt` | 300 s | 16 | 88.8 s | x0.296 | 14/16 | 11.2 s | 17.1 s | 0.0 s |
| `run-artifacts/hf-realtime-check/minutes12.txt` | 720 s | 38 | 153.3 s | x0.213 | 30/38 | 9.4 s | 24.5 s | 0.0 s |

Latency is measured from the end of a transmission to the text being available
and includes waiting for the receive window to close. These are simulated
streaming checks of decoder throughput and window handling; they are not HF
on-air QSOs.

## Alpha release claims

This section is the publish boundary for the alpha tree. A row marked
**claimed** is backed by retained artifacts or executable constants cited in
this paper. A row marked **not claimed** is either unmeasured, failed in
retained work, or still research. Marketing language that exceeds this matrix
is out of scope for alpha.

### Capability matrix

| Capability | Status | Evidence class | Anchor |
| --- | --- | --- | --- |
| P1 wire format (header CRC-8/ATM, payload CRC-16, K=7 rate-1/2, 16-of-192 hop) | **Claimed** | Executable constants | `framing.py`, `waveform.py`, `frame_bit_layout.png` |
| MOSAIC-HF 50 bit/s and MOSAIC-UHF 800 bit/s profiles (`Δf·T_s = 1`) | **Claimed** | Executable constants | `profiles.py`, band-profile table |
| Eb/N0 conversion `SNR_ref_dB + 10·log₁₀(B_ref/R_b)` (= +16.8 dB for both profiles) | **Claimed** | Definition + tables | Mathematical specification |
| CRC-gated SIC with immutable `r₀` and LIFO undo | **Claimed** | Implementation + flowchart | `sic.py`, `sic_flowchart.png` |
| Catalogue-bounded multi-user search (not unlimited blind active set) | **Claimed** | Implementation | `sic.py`, acquisition config |
| Cabled CRC-valid HF-profile and UHF-profile point-to-point on bench SDRs | **Claimed** | Measured hardware | cabled vendor manifests |
| On-air single-packet UHF and HF-profile-on-UHF-carrier CRC recovery | **Claimed** | Measured hardware | on-air vendor / solo manifests |
| Independent two-transmitter on-air UHF decode by a third radio | **Claimed** | Measured hardware | `three-radio-uhf-2/` |
| On-air UHF 4/4, 8/8, 12/12 logical stations on two RF chains (incl. random arrivals) | **Claimed** | Measured hardware | `onair-4station-imbalanced/`, `onair-8station/`, `onair-12station/`, `onair-12station-random/` |
| Live bidirectional keyboard QSO at 431.200 MHz (KISS modem) | **Claimed** | Measured hardware / handoff | live QSO section; small-count |
| MOSAIC-HF real DAC→analogue→ADC loopback byte-exact | **Claimed** | Measured converters | Scarlett 4i4 3/3 |
| MOSAIC-HF single-user F.1487-style cliff (~+1.8…−1.2 dB Eb/N0 flat; ~3 dB fade cost) | **Claimed** | Simulated channel model | `hf-channel/itu-f1487*.json`, `hf_itu_sensitivity.png` |
| MOSAIC-HF no-fade multi-user peel in audio simulation | **Claimed** | Simulated | `hf-multiuser/hf-audio-nofade.json` |
| UHF no-fade software scaling complete through 16; graceful 22/24, 26/32 | **Claimed** | Simulated, current harness | `scaling/uhf-analytic-*.json` (files with `even_spacing`) |
| Continuous HF-audio decode faster than real time with zero backlog (sim streams) | **Claimed** | Simulated streaming | `hf-realtime-check/` |
| Continuous UHF live demod keep-up (packet walk + residual peel + multi-ROI crop, `uhf-live`) | **Claimed** | Simulated streaming | `tools/uhf_realtime_check.py` (wall ratio ≪ 1) |
| Operator web panel (TX inhibit, transcript, receiver health) | **Claimed** | Implementation | `src/mosaic_hf/operator_panel.py`, `--ui-port` |
| Fail-closed FPGA pin, burst-duration check, envelope profile, capture SHA binding | **Claimed** | Bench methodology | guardrails section |
| Twelve independent RF converters on air | **Not claimed** | — | two chains carry logical composites |
| Three simultaneous independent RF transmit chains | **Not claimed** | — | unmeasured |
| On-air HF ionospheric contact / transceiver-in-the-loop QSO | **Not claimed** | — | no retained HF on-air contact |
| MOSAIC-HF shared-channel success under ITU-style fading | **Not claimed** | Simulated failure | `hf-audio-scaling.json` (≈1–2 recovered) |
| Robust UHF multi-user under fading | **Not claimed** | Simulated partial | faded 8-station means ≪ 8 |
| Tracked-gain SIC alone solves HF multi-user fading | **Not claimed** | Root-cause diagnosis | HF limits section; BIC-selected tracking present, fade recovery still limited |
| Spectrum compliance, spurious, antenna or regulatory approval | **Not claimed** | — | out of scope |
| Instant messaging / sub-second chat | **Not claimed** | Air time is the floor | UHF packet ≈2.6 s; HF packet ≈40 s; continuous demod keeps up with the stream but does not shrink airtime |
| Joint ML multi-user receiver / unlimited blind discovery | **Not claimed** | — | architecture is CRC-gated peel |
| Superseded no-fade scaling files without `even_spacing` | **Withdrawn** | Harness defect | `scaling/README.md` |

### What alpha is for

Alpha is a **deterministic reference modem and validation method**: inspectable
constants, CRC-gated cancellation, retained captures, and an honest split
between radiated UHF multi-user proof and HF single-user / audio-path proof.
It is not an HF multi-user service claim and not a twelve-radio field network.

### Required citations when quoting alpha

* Radiated multi-user → cite the specific `run-artifacts/onair-*` or
  `three-radio-*` manifest and the two-chain topology.
* Sensitivity / Eb/N0 → cite the SNR reference bandwidth and the +16.8 dB
  conversion; do not quote SNR as if it were Eb/N0.
* Scaling beyond hardware → cite only current-harness files that carry
  `even_spacing`, and label simulation.
* HF → say “audio path / channel model” unless a future on-air HF artifact
  exists.

## Known limitations and future work

* **Same-frequency simultaneous duplex was deliberately cut.** The handoff
  notes that with 20-30 dB of antenna isolation, a bladeRF's own transmission
  could arrive at its receiver comparable to or stronger than the remote
  signal, risking front-end compression. The live station is half duplex.
* **Fading is the binding measured gap.** Current-code eight-station UHF
  fading simulations recover only 12/32 station opportunities with 6.0 dB
  amplitude spread and 15/32 with 0.0 dB spread. On MOSAIC-HF audio, fading is
  more severe for the shared channel: current simulated multi-user runs
  saturate at one or two recovered stations under ITU-style fading. Closing
  this requires diversity across a fade and, for HF multi-user operation, a
  canceller whose channel estimate tracks within the packet.
* **Live keyboard latency is now usable but still not interactive-chat fast.**
  Cropping reduced the retained B210 observer decode from a full-window 19.0 s
  to a cropped 4.6 s, and the handoff records about 30 s end-to-end delivery in
  a live exchange. The previous minutes-latency statement is superseded, but
  this remains delayed weak-signal text rather than instant chat.
* **Receive duty cycle improved, but delivery is still a small-count result.**
  The B210 loses about 2.0 s to UHD setup per receive window, so its default
  window is now 16 s. A one-way trial improved from 2/5 at 8 s windows to 5/5
  at 16 s windows, and the symmetric 16 s exchange delivered 4/5 in both
  directions with 0 rejected frames and 0 station errors. Four of five is not
  five of five, and no long-run statistics have been measured.
* **No-fade scaling had to be retracted and re-run.** Every previously reported
  `--no-fade` scaling result from `tools/many_station_scaling.py` used a
  defective sweep-tool generator that manufactured mirror-image interference.
  This was a measurement-harness defect, not a receiver or protocol limit.
  Current random-arrival no-fade artifacts recover 12/12 and 12/12 for two
  seeds, then 8/8, 10/10, 12/12 and 16/16 in a seed-7 re-sweep. The single-seed
  deep run recovers 22/24 and 26/32, so degradation beyond 16 is graceful rather
  than a collapse or ceiling.
* **HF shared-channel operation is not yet proven.** Current `hf-audio`
  simulations recover 2/2, 4/4 and up to 8/8 without fading, but ITU-style HF
  fading collapses the absolute recovered count to one or two stations. The
  retained converter-path multi-user checks are also partial: 1/2 with a
  4.6 dB weaker second station and 0/2 at equal power.
* **Load and spacing control remain open design questions.** The current
  corrected artifacts do not show no-fade multi-user collapse, but the 24- and
  32-station points are single seeds. Admission control or spacing control may
  still be useful for a practical many-user service, but the retained artifacts
  do not yet measure such a control mechanism.
* **On-air near/far evidence is limited.** Equal-power and modest unequal-power
  composite cases have been measured, the imbalanced four-station run recovered
  a substantially unequal population, and the first independent-radio attempt
  failed until the bladeRF transmit gain was lowered. Scaling needs per-station
  received-power management, not merely more transmitted power.
* **Many-user scaling remains the open research question.** The current
  independent-radio hardware evidence demonstrates two independent RF transmit
  chains decoded by a third radio. The twelve-station runs put six logical
  stations on each chain; they are not twelve-independent-radio claims, but the
  random-arrival run does show that random timing and offset placement did not
  break the retained twelve-station hardware case. The open problem is
  therefore fading first, then power control, receiver dynamic range, admission
  control and cancellation-chain robustness for a population of uncoordinated
  stations at widely differing ranges and time/frequency spacings.
* **HF is not yet an on-air HF contact.** The bench now has a real path for
  MOSAIC-HF through a transceiver audio interface and has decoded byte-exact
  through real DAC/ADC converters. It also has simulated ITU-R F.1487 channel
  results and a direct-radiated UHF-carrier SDR partial that acquired but did
  not decode. The direct SDR HF RF path is still impossible here: the measured
  limits are 237.5 MHz for the bladeRF x115, 42 MHz for the B210, and a
  half-duplex HackRF. A real transceiver and ionospheric path remain
  unmeasured.
* **Direct-radiated narrow tone grids need better references or finer drift
  search.** The MOSAIC-HF-on-UHF SDR partial is consistent with drift across
  12.5 Hz tones. Future direct-radiated use of that grid requires either a
  disciplined frequency reference or a finer drift hypothesis grid than the
  current -10, 0 and +10 Hz/s search.
* **Per-radio keying latency matters.** The B210 can be scheduled by wall clock;
  the bladeRF CLI must load its FPGA before streaming. The retained
  three-radio tool compensates with a 1.05 s default bladeRF lead, but larger
  networks will need explicit timing and latency accounting.
* **SoapySDR path is not the proven long-payload route.** The handoff records
  Soapy captures that recovered headers but failed payload CRC, while vendor
  tools produced CRC-valid cabled exchanges.
* **B210 receive gain is operationally important.** The B210 is proven as a
  decoding observer in `usrp-observer-2tx-hi`, but the lower-gain
  `usrp-observer-2tx` run recovered no CRC-valid payloads. The envelope profile
  shows that was an under-gained capture rather than evidence of a waveform
  capacity limit.

## Reproducing retained result types

The following are the exact command forms recorded in the source and handoff.
They are reproduction instructions, not commands run while writing this paper.

### Cabled or on-air point-to-point run

```bash
python tools/cabled_bladerf_hackrf.py --output run-artifacts/<name> \
  --mode cabled --frequency-hz 431200000 --profile uhf --capture-seconds 4 \
  --fpga hostedx115-latest.rbf --source VE6SLP --destination VA6GA \
  --text "MOSAIC-UHF cabled first light" --execute
```

Reverse direction:

```bash
python tools/cabled_bladerf_hackrf.py --output run-artifacts/<name> \
  --mode on-air --callsign VE6SLP --frequency-hz 431200000 --profile uhf \
  --direction hackrf-to-bladerf --hackrf-tx-gain-db 20 --capture-seconds 4 \
  --fpga hostedx115-latest.rbf --destination VE6SLP \
  --text "MOSAIC-UHF on air reply" --execute
```

### Congested composite run

```bash
python tools/onair_collision.py --output run-artifacts/<name> \
  --mode on-air --callsign VE6SLP --frequency-hz 431200000 --profile uhf \
  --txvga2-db 10 --capture-seconds 8 --fpga hostedx115-latest.rbf \
  --transmitter "VE6SLP:VA6GA:0x2f:0.20:0.0:1.0:alpha" \
  --transmitter "VA6SLP:VE6NAS:0x5b:0.85:180.0:0.85:bravo" --execute
```

### Three-radio independent-transmitter run

```bash
python tools/three_radio_congested.py --output run-artifacts/<name> \
  --callsign VE6SLP --frequency-hz 431200000 --profile uhf \
  --capture-seconds 8 --txvga2-db 0 --usrp-gain-db 52 \
  --transmitter "bladerf:VE6SLP:VA6GA:0x2f:0.0:0.0:1.0:independent alpha de VE6SLP" \
  --transmitter "usrp:VE6NAS:VA6SLP:0x5b:0.0:0.0:1.0:independent bravo de VE6NAS" \
  --fpga hostedx115-latest.rbf --execute
```

### Carrier leakage measurement

```bash
python tools/tx_carrier_leakage.py --output run-artifacts/<name> \
  --frequency-hz 431200000 --callsign VE6SLP --destination VA6GA \
  --profile uhf --txvga2-db 10 --fpga hostedx115-latest.rbf --execute
```

Do not add `--calibrate` for the retained operating mode; the measured
calibrated run made carrier leakage much worse.

### Offline retained-capture decode

```bash
python tools/decode_capture.py --capture run-artifacts/<name>/raw_rx.cs8 \
  --output run-artifacts/<name>/offline-decode.json --profile uhf \
  --format cs8 --candidate CALLSIGN-1:0x2f --candidate CALLSIGN-2:0x5b
```

For CF32 observer captures, pass `--format cf32`. The output records the
capture SHA-256 so the decode remains tied to the exact recording.

### Live KISS modem

```bash
python tools/mosaic_modem.py --callsign VE6SLP --peer VA6GA --profile uhf \
  --frequency-hz 431200000 --fpga hostedx115-latest.rbf --execute
```

The live station exposes KISS on `127.0.0.1:8001` by default and runs an
`RX -> guard -> TX -> guard` cycle.

### MOSAIC-HF transceiver audio path

List audio devices:

```bash
python tools/mosaic_hf_audio.py devices
```

Run the retained analogue-converter loopback form:

```bash
python tools/mosaic_hf_audio.py --amplitude 0.9 loopback --device 13 \
  --repeat 3 --text "VE6SLP-1 DE VA6GA-2 QSL 599 K"
```

For a real SSB transmitter, start with the default `--amplitude 0.25`, use a
serial `--ptt-port`/`--ptt-line` where possible, and raise drive only until ALC
just begins to move.

### Test environment note

`tests/test_gnuradio_virtual_channel.py` must be run with `/usr/bin/python3`,
not the pyenv interpreter. The repository contains a `gnuradio/` directory that
can shadow the real GNU Radio package as a namespace package from the repository
root; the failure only appears in subprocesses launched from elsewhere. With
system Python, the retained check is:

```bash
/usr/bin/python3 -m pytest tests/test_gnuradio_virtual_channel.py -q
```

## Limits of the current evidence

* Measured hardware evidence extends to two independent RF transmit chains
  active at once. Three-chain operation is unproven.
* The largest retained hardware population is twelve simultaneous logical
  stations carried across two RF chains. The 16-, 24- and 32-station results
  are simulated no-AWGN cases.
* The 24- and 32-station no-fade results are single seeds. They show graceful
  degradation in those runs, not an outage-rate estimate.
* Diversity across a fade and admission-control policy remain unmeasured.
* Long-run live-modem delivery statistics remain open. The retained QSO and
  4/5-per-direction exchange are successful small-count operating results.
* MOSAIC-HF evidence is now precise but incomplete. The profile decoded
  byte-exact through a real DAC/analogue-wiring/ADC path, 3/3. Simulated
  ITU-R F.1487 flat/good/moderate/poor channels establish a flat threshold
  bracket of +1.8 to -1.2 dB Eb/N0 and about a 3 dB fading penalty near
  threshold for one station. Retained continuous-reception checks keep up at
  x0.296 and x0.213 real time with zero backlog in 300 s and 720 s simulated
  streams. Direct SDR radiation of the HF waveform on a UHF carrier acquired
  and correctly attributed the burst but did not decode it. Shared-channel HF
  simulations work without fading but collapse under ITU-style fading to one
  or two recovered stations, and real-converter two-station checks are partial
  at 1/2 and 0/2. MOSAIC-HF still does not have an on-air HF contact,
  ionospheric propagation result, real transceiver-in-the-loop measurement or
  proven shared-channel operation in a fading HF channel.
* On-air near/far evidence remains limited to equal-power or modest
  unequal-power composite cases, solo envelope calibration captures, and the
  two independent three-radio attempts.
* The HF sensitivity cliff is simulated channel-model evidence, not a
  measured-on-hardware waterfall or an on-air HF propagation result.
