Found during design review of #1428, then verified here by reading the three call sites. This is
about the shipped product, not about #1363's proposed fix, and it stands whatever #1363 decides.
The split
ModemEngine::receive_from_samples (engine.rs:4497) prefers the soft demodulator whenever the
plugin advertises one:
let snr = self.rx_snr_db(mode, &samples.samples);
if plugin.supports_soft_demod(mode) {
let llrs = openpulse_modem_descramble_soft(plugin.demodulate_soft(&samples.samples, &mod_cfg)?);
BpskPlugin::supports_soft_demod (plugins/bpsk/src/lib.rs:151) returns true unconditionally,
ignoring the mode. And BPSK's soft path deliberately does not apply cancel_crossfade_isi — the
reason is in its own NOTE at demodulate.rs:374 and is a real one (#832: the recursion breaks the
LLR calibration HARQ combining relies on).
So, by construction:
| decode |
path |
cancellation |
uncoded (FecMode::None) |
decode_attempt → receive_from_samples (engine.rs:4557) |
OFF (soft arm, hard-decided) |
coded (Rs and friends) |
receive_from_samples_with_fec |
ON |
Three call sites reach the uncancelled arm: receive() (engine.rs:3714), decode_attempt's
FecMode::None arm (4557), and decode_burst_phase1 (2399).
Why this is not academic
Uncoded BPSK is a live production path. Per #1123 the OTA arm's non-ladder traffic — periodic §97.119
station ID, filexfer, handshake, QSY, relay envelopes — is uncoded, and hpx_hf's entry rungs are
BPSK. So the frames that carry a station's identity and its control traffic decode through the arm
that #821's fix never reached.
The test that validates it exercises the other arm
crossfade_cancellation_lowers_awgn_ber (plugins/bpsk/src/lib.rs:286) is an uncoded BER test —
and it calls plugin.demodulate(...), the hard, cancelled arm. So the guard standing over
#821's cancellation, on uncoded data, tests a demodulator that no uncoded production decode uses.
That is the self-consistent-checker shape: the test and the product disagree about which code is
under test, and nothing says so.
The justification comment is false for BPSK
The soft preference is justified on the grounds that a hard retry "can't succeed where the soft pass
failed — both share the same acquisition front end". For BPSK the two arms differ by exactly the
cancellation, which is the thing #1363 has spent a week measuring: worth several dB of flip-bit
margin on AWGN, and worth 8 frames of 96 against you on a delayed-dominant fade. They are not
interchangeable in either direction.
What is NOT being claimed
Not that the uncoded path is wrong. #1363's measurements make the uncancelled arm the better
choice on moderate_f1 and the worse one on AWGN and pure Doppler, so the current split happens to
give uncoded traffic the fade-favourable arm and coded traffic the AWGN-favourable one. That may even
be the right answer — but it is an accident of a capability flag, it is undocumented, and the one
test standing over it measures the other branch.
Suggested shape
- Decide deliberately which arm uncoded BPSK should use, and record the reason.
- Make the validating test exercise the path the product takes — or add a second one that does.
- Fix the justification comment, which is false for the one plugin whose arms differ.
Sizing: #1363 is measuring precisely this trade, so its numbers should inform (1) rather than being
duplicated here. Related: #821 (the cancellation), #832 (why soft skips it), #1361 (the soft/hard
split), #1363, #1428, #1123 (uncoded OTA traffic).
🤖 Generated with Claude Code
https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
Found during design review of #1428, then verified here by reading the three call sites. This is
about the shipped product, not about #1363's proposed fix, and it stands whatever #1363 decides.
The split
ModemEngine::receive_from_samples(engine.rs:4497) prefers the soft demodulator whenever theplugin advertises one:
BpskPlugin::supports_soft_demod(plugins/bpsk/src/lib.rs:151) returnstrueunconditionally,ignoring the mode. And BPSK's soft path deliberately does not apply
cancel_crossfade_isi— thereason is in its own NOTE at
demodulate.rs:374and is a real one (#832: the recursion breaks theLLR calibration HARQ combining relies on).
So, by construction:
FecMode::None)decode_attempt→receive_from_samples(engine.rs:4557)Rsand friends)receive_from_samples_with_fecThree call sites reach the uncancelled arm:
receive()(engine.rs:3714),decode_attempt'sFecMode::Nonearm (4557), anddecode_burst_phase1(2399).Why this is not academic
Uncoded BPSK is a live production path. Per #1123 the OTA arm's non-ladder traffic — periodic §97.119
station ID, filexfer, handshake, QSY, relay envelopes — is uncoded, and
hpx_hf's entry rungs areBPSK. So the frames that carry a station's identity and its control traffic decode through the arm
that #821's fix never reached.
The test that validates it exercises the other arm
crossfade_cancellation_lowers_awgn_ber(plugins/bpsk/src/lib.rs:286) is an uncoded BER test —and it calls
plugin.demodulate(...), the hard, cancelled arm. So the guard standing over#821's cancellation, on uncoded data, tests a demodulator that no uncoded production decode uses.
That is the self-consistent-checker shape: the test and the product disagree about which code is
under test, and nothing says so.
The justification comment is false for BPSK
The soft preference is justified on the grounds that a hard retry "can't succeed where the soft pass
failed — both share the same acquisition front end". For BPSK the two arms differ by exactly the
cancellation, which is the thing #1363 has spent a week measuring: worth several dB of flip-bit
margin on AWGN, and worth 8 frames of 96 against you on a delayed-dominant fade. They are not
interchangeable in either direction.
What is NOT being claimed
Not that the uncoded path is wrong. #1363's measurements make the uncancelled arm the better
choice on
moderate_f1and the worse one on AWGN and pure Doppler, so the current split happens togive uncoded traffic the fade-favourable arm and coded traffic the AWGN-favourable one. That may even
be the right answer — but it is an accident of a capability flag, it is undocumented, and the one
test standing over it measures the other branch.
Suggested shape
Sizing: #1363 is measuring precisely this trade, so its numbers should inform (1) rather than being
duplicated here. Related: #821 (the cancellation), #832 (why soft skips it), #1361 (the soft/hard
split), #1363, #1428, #1123 (uncoded OTA traffic).
🤖 Generated with Claude Code
https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6