Commit Graph

31 Commits

Author SHA1 Message Date
sjg 46415fa307 [fix](trx-ft8): add hard-error verification to FT2 OSD decoder
The OSD-lite decoder was the source of FT2 false positives. It tries
685 CRC-14 checks across 5 passes (1 + 16 + 120 per pass), giving a
~4% chance of accepting random noise as a valid decode.

The reference implementation (decode174_91) verifies OSD results
against the received signal; the trx-rs OSD-lite only checked CRC.

Add ft2_count_hard_errors_vs_llr() which counts how many of the 174
coded bits in an OSD candidate disagree with the received hard
decisions. A legitimate correction disagrees in very few positions;
a false CRC match on noise disagrees in ~40-50 parity positions.
Reject OSD results with more than 36 hard errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 21:44:21 +01:00
sjg 8aa1884d2d [fix](trx-ft8): align FT2 decoder thresholds and LLR scaling with reference
The FT2 decoder was failing to decode valid signals due to thresholds
and LLR parameters that were significantly stricter than the reference
implementation (Decodium/raptor ft2_triggered_decode).

Thresholds aligned with reference at depth>=3:
- Peak detection: 1.08 -> 1.03 (reference uses 0.50 on normalized spectrum)
- Scan sync: 0.60 -> 0.50 (reference at depth>=3)
- Decode sync: 0.80 -> 0.65 (reference at depth>=3)
- Sync quality min: 13 -> 10 (reference at depth>=3)

LLR parameters aligned:
- Scale factor: 2.83 -> 3.2 (matches reference scalefac)
- Normalization: sqrt(24/var) -> 1/sigma (matches reference normalizebmet)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 21:38:48 +01:00
sjg 7844cb65c8 [fix](trx-ft8): add missing encode.c to build
The ft8_wrapper.c references ft4_encode and ft8_encode from encode.c,
but encode.c was not included in build.rs, causing linker errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 21:29:42 +01:00
sjg 4464fa3735 [fix](trx-ft8): replace adjacent-bin SNR with post-decode estimation
The previous SNR formula (cand->score * 0.5 - 29.0) used the adjacent
tone bin as a noise reference. On a crowded FT8 band that bin is often
occupied by another station, inflating the apparent noise floor by
10-15 dB and capping reported SNR at around -10 dB even for strong
signals.

Replace with ftx_post_decode_snr(): re-encode the decoded message to
obtain the exact per-symbol tone sequence, compare each signal bin
against the minimum of the remaining (noise-only) bins, average over
all valid symbols, and apply the WSJT-X 2500 Hz bandwidth correction
dynamically per protocol. This produces accurate SNR estimates for both
FT8 and FT4 regardless of band occupancy.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 21:06:43 +01:00
sjg 342adf476c [fix](trx-ft8): limit FT2 OSD-lite to prevent false decodes
The OSD-3 (triples) path over 5 LLR passes was doing ~11,600 CRC checks
per candidate. With a 14-bit CRC this gives ~0.7 expected false positives
per candidate — far too high.

Remove OSD-3 entirely. Cap max_candidates at 16 for OSD-1/OSD-2, giving
136 CRC checks per pass (680 total). Gate OSD-lite behind a check that
LDPC reached within 6 parity errors of converging, so it only fires when
the LLRs are already trustworthy. Combined false-positive rate drops to
~0.04 per near-miss candidate.

Also remove the now-unused ft2_osd_decode and ft2_codeword_distance
functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-15 08:14:22 +01:00
sjg eac2efdb35 [fix](trx-ft8): increase FT2 LDPC iterations and improve OSD fallback
Increase BP/SP iteration count from 30 to 50 to match WSJT-X reference
and give belief propagation more opportunities to converge near-threshold
candidates.

Replace the parity-based OSD-1/OSD-2 fallback (which required LDPC to
have nearly converged) with ft2_osd_lite_decode applied to all five LLR
combination passes. The CRC-based decoder works directly from raw LLRs
without depending on LDPC convergence, searching the 24 least-reliable
systematic bits for up to three bit errors via OSD-3.

Also increase max_candidates in ft2_osd_lite_decode from 12 to 24 for
broader coverage of likely error positions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-15 08:06:19 +01:00
sjg 3b28281684 [fix](trx-ft8): add OSD-1/OSD-2 to FT2 LDPC decode path
Diagnostic logging showed the FT2 BP/SP decoders consistently reach
1-8 residual parity errors rather than zero — the LLRs are correct
in direction but LDPC belief propagation stalls just short of
convergence.

Add ft2_osd_decode() implementing Ordered Statistics Decoding orders
1 and 2: after the five-pass BP/SP loop fails, sort the 174 codeword
bits by |LLR| ascending and trial-flip single bits (OSD-1, always)
or all pairs of the 50 least-reliable bits (OSD-2, when the remaining
error count is <= 4).  Each trial costs one O(83) parity check;
worst-case overhead is ~1300 checks per candidate, negligible next to
the 5 x 30-iteration BP/SP passes already performed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-15 07:30:17 +01:00
sjg 0d2eb7adcd [fix](trx-ft8): log minimum LDPC parity errors per pass in FT2 window
Add err=N/N/N/N/N to the FT2 window diagnostic log line, showing the
minimum number of unsatisfied parity equations across all candidates
for each of the five LLR passes. This makes it possible to distinguish
between a signal-quality-limited failure (small error count) and a
systematic decoder bug (large error count), which is the key unknown
in diagnosing the current FT2 LDPC non-convergence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-15 07:21:38 +01:00
sjg 188fa38f1b [fix](trx-ft8): improve FT2 decode fallback
Normalize FT2 log-likelihoods before LDPC and fall back to\nthe standard waterfall candidate decoder when the raw FT2\npath produces no decodes.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 22:39:07 +01:00
sjg 71bfc5fca1 [fix](trx-rs): route FT2 decode through shared LDPC path 2026-03-14 22:24:16 +01:00
sjg 5dd8f4112a [fix](trx-rs): isolate FT2 OSD LDPC from shared ft8_lib 2026-03-14 22:10:49 +01:00
sjg cededd9c3f [fix](trx-rs): align FT2 downsample path with reference
Reuse a shared FT2 downsample FFT context per decode window and\nfix filter shift/bin handling to match the reference implementation.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 21:45:35 +01:00
sjg 4efb0dc5ef [fix](trx-rs): correct FT2 symbol bin indexing 2026-03-14 21:29:26 +01:00
sjg 2ad555efd1 [debug](trx-rs): trace FT2 decode pass outcomes 2026-03-14 21:22:29 +01:00
sjg f43f3a27fd [fix](trx-rs): correct FT2 OSD generator setup 2026-03-14 21:18:13 +01:00
sjg e6a78d0ec0 [feat](trx-rs): port FT2 hybrid LDPC decoder 2026-03-14 21:15:20 +01:00
sjg 0b97329f63 [fix](trx-rs): normalize FT2 bitmetrics before LDPC 2026-03-14 21:05:55 +01:00
sjg 5b168fd6d5 [debug](trx-rs): add FT2 decode stage logging 2026-03-14 21:02:02 +01:00
sjg 3c538d5712 [fix](trx-rs): correct FT2 sync phase progression 2026-03-14 20:46:42 +01:00
sjg d3abce9ab0 [fix](trx-rs): align FT2 async window geometry 2026-03-14 20:44:25 +01:00
sjg 2ab05f5001 [fix](trx-rs): move FT2 decoding onto raw complex path 2026-03-14 20:40:11 +01:00
sjg 6d430a4300 [fix](trx-rs): add FT2 raw-window candidate search
Use an FT2-specific raw sample window and candidate acquisition path.
Keep the build clean by removing the stale monitor warning.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 20:32:21 +01:00
sjg 27de75cf79 [fix](trx-rs): improve FT2 coherent decoding
Add coherent FT2 sync and likelihood extraction.
Align FT2 frequency and DT reporting with the reference decoder.
Fix vendored monitor phase-mode type mismatches.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 20:25:59 +01:00
sjg 2570fe739e [fix](trx-rs): improve FT2 decoder acquisition
Use FT2-specific analysis settings and a wider receive span.
Switch server-side FT2 decoding to a rolling async window.
Widen FT2 candidate timing search in the vendored decoder.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 20:20:18 +01:00
sjg ad6aa6aab4 [feat](trx-rs): add FT2 decoder protocol support
Implement a distinct FT2 protocol path in the decoder stack and align\nits timing with the confirmed FT2 framing used by Decodium.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 19:49:32 +01:00
sjg d547c45a9c [feat](trx-rs): add FT2 decoder support (wired to FT4)
Mirrors the FT4 implementation across the full stack. The trx-ft8
crate wires Ft8Decoder::new_ft2() to FTX_PROTOCOL_FT4 as a
placeholder pending a dedicated FT2 implementation.

Changes:
- trx-ft8: Ft8Decoder::new_ft2() delegates to with_protocol(Ft4)
- trx-core: DecodedMessage::Ft2, AUDIO_MSG_FT2_DECODE (0x15),
  ft2_decode_enabled/ft2_decode_reset_seq state, SetFt2DecodeEnabled/
  ResetFt2Decoder commands, protocol mapping
- trx-server: DecoderHistories::ft2, run_ft2_decoder (7.5s slots),
  run_background_ft2_decoder, history push/replay, decoder task spawn
- trx-frontend-http: ft2_history in FrontendRuntimeContext,
  toggle/clear endpoints, /ft2.js route, bookmark/scheduler/background
  decode support, DecodeHistoryPayload ft2 field
- web: ft2.js plugin (3.75s period timer), FT2 subtab in index.html,
  FT2 map source (distinct hue), app.js dispatch, decode-history-worker
  HISTORY_GROUP_KEYS, bookmarks read/write/apply

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 19:34:41 +01:00
sjg 8eae376c56 [feat](trx-rs): add FT4 decoder support
Reuse the existing ft8_lib C library (FTX_PROTOCOL_FT4) and FT8
decoder infrastructure to add FT4 decoding across the full stack.

Changes:
- trx-ft8: add protocol param to ft8_decoder_create; add Ft8Decoder::new_ft4()
- trx-core: DecodedMessage::Ft4 variant, AUDIO_MSG_FT4_DECODE (0x14),
  ft4_decode_enabled/ft4_decode_reset_seq state, SetFt4DecodeEnabled/
  ResetFt4Decoder commands, protocol mapping
- trx-server: DecoderHistories::ft4, run_ft4_decoder (7.5s slots via
  now*2/15), run_background_ft4_decoder, history push/replay, decoder
  task spawn
- trx-frontend-http: ft4_history in FrontendRuntimeContext,
  toggle/clear endpoints, /ft4.js route, bookmark/scheduler/background
  decode support, DecodeHistoryPayload ft4 field
- web: ft4.js plugin (7.5s period timer, reuses FT8 CSS/map infra),
  FT4 subtab in index.html, app.js dispatch (onServerFt4/Batch,
  restoreFt4History), decode-history-worker HISTORY_GROUP_KEYS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 18:50:08 +01:00
sjg 9292bf3830 [fix](trx-ft8): drop FT8 messages that fail to unpack
Skip decoded candidates where ftx_message_decode() returns a non-OK
status instead of forwarding a synthetic error string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:33:02 +01:00
sjg a3c098b68f [fix](trx-ft8): correct FT8 SNR to WSJT-X 2500 Hz convention
The score from ft8_lib is an averaged uint8 difference between Costas
sync tones and their neighbours (each unit = 0.5 dB). The previous
score * 0.5 gave the signal-above-adjacent-noise in dB relative to a
single 3.125 Hz waterfall bin, yielding values of +5 to +50 dB —
all wrong.

Subtract 10*log10(2500/3.125) ≈ 29 dB to normalise to the 2500 Hz
reference bandwidth used by WSJT-X and expected by PSKReporter:

  snr = score * 0.5 - 29.0

This maps score 10 (minimum decodable) → -24 dB and score 60 → +1 dB,
matching typical WSJT-X SNR report ranges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 12:13:03 +01:00
sjg 1dec2c2663 [chore](trx-rs): centralize version in workspace.package
Add [workspace.package] version = "0.1.0" to the root Cargo.toml and
switch all 21 member crates to version.workspace = true so the entire
workspace is versioned from a single place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:13:06 +01:00
sjg 3ebd185a7e [refactor](decoders): consolidate decoder crates under src/decoders/
Move trx-ft8 and trx-wspr into src/decoders/ alongside a new trx-aprs
crate that extracts the Bell 202/AX.25 decoder from trx-server, giving
all three modems a consistent crate-per-decoder layout.

- src/decoders/trx-ft8/  (moved from src/trx-ft8/)
- src/decoders/trx-wspr/ (moved from src/trx-wspr/)
- src/decoders/trx-aprs/ (new — Bell 202 AFSK + AX.25/APRS decoder)
- trx-ft8/build.rs: fix external/ft8_lib relative path after move
- trx-server: drop decode::aprs module, use trx_aprs::AprsDecoder
- AprsPacket stays in trx-core (mirrors Ft8Message / WsprMessage)
- Workspace Cargo.toml updated with new member paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 18:30:04 +01:00