Linux only: the runtime ALSA library (not the -dev headers the

September 1, 2026 · View on GitHub

Pancetta logo

Pancetta

A full FT8 station in one Rust binary — decode, score, work, log.
Built and operated by Tony Hagale, W5AU.

CI License: MIT OR Apache-2.0

Pancetta is an FT8 station that lives in your terminal.

Pancetta running a replayed band recording

pancetta --replay feeding archived WAV captures through the real pipeline: startup, band monitoring, the TX-placement scorer ranking candidate offsets, a burst of real FT8 decodes filling Band Activity when the capture reaches its decodable slot, and a clean self-shutdown when the recording ends. One recording of one small corpus proves nothing about recall — for measured decode performance see docs/decoder-comparison.md.

The pitch

Operating FT8 seriously today means WSJT-X for the modem, a logger for the log, GridTracker for what's needed, and a cluster client for spots — four alt-tabbed windows, each with its own idea of what your station is doing. Pancetta is one binary and one terminal: decode, priority scoring, QSO state machine, DX cluster, PSKReporter, and logbook upload in a single process that runs as happily over SSH on a headless box behind the radio as on a desktop.

Why Pancetta

  • A decoder measured against the real reference. Against WSJT-X's jt9 — the decoder hams actually run — Pancetta needs ~2.1 dB more SNR to reach 50% recall on FT8, and ~3.95 dB more on FT4, where it caps at 78% recall, at a false-positive cost of 1 decode per 1,000 pure-noise recordings (0.1%). In short: competitive, not yet class-leading. (An older 1,201-file comparison against ft8_lib showed +11.6% more decodes, but ft8_lib is not the bar operators care about.) Full numbers and caveats: docs/decoder-comparison.md.
  • A priority engine, not a list. Every decoded CQ is scored against needed DXCC, needed grid, POTA/SOTA, rarity, signal, and recent activity — so the station you should work is the one at the top.
  • Multi-stream TX. A smart frequency allocator places N simultaneous FT8 signals inside one 15-second slot, each on its own audio offset, all on the same parity.
  • Headless by design. --headless logs instead of drawing; the decoder is an anytime algorithm with a per-window budget that self-tunes to the host, so slow hardware degrades recall instead of falling over.
  • pancetta doctor. One command checks config, clock vs. NTP, audio device and level, decoder, and rigctld — and prints the fix for each failure.

On-air TX has been validated end-to-end against a Yaesu FTdx10 (clean ALC, PSKReporter spots across North America and Europe). ~295 FT8 tests cover encode, decode, LDPC, CRC, and OSD.

What it looks like

Every capture below — and the GIF further down, and the hero GIF above — was recorded on a development host with pancetta --replay, feeding archived off-air WAV captures through the real pipeline (see .tapes/). Real audio, real decodes, real panels — no mockups. The corpus is one short recording, so treat these as a tour of the interface, not a performance claim: the QSO panel in particular is shown idle (STANDBY, "No QSOs yet"), because a completed QSO needs a multi-slot two-way handshake that a receive- only replay of someone else's traffic can't produce.

Operate overviewDX Hunter priority panel
Operate overview: TX placement, band activity, QSO status, callers.DX Hunter: the priority-scoring table (entity, grid, SNR, rarity, score).
QSO status panelMonitor / waterfall view
QSO status (idle in this replay): state machine, exchanges, TX/RX reports.Monitor: the waterfall and the TX-frequency marker.

Decode effort is a live control — press e to cycle presets and the status chip follows:

Cycling decode-effort presets with the e key

PresetBehavior
Auto (default)Budget derived from the auto-probed hardware tier.
EcoFloor pass only — fastest, lowest recall.
StandardA moderate per-window budget.
DeepA generous budget — more passes/candidates, better recall.
MaxRuns every decode stage, still bounded by the coordinator's per-slot ceiling (2 s on FT8).

Pin it at startup via [decoder] in pancetta.toml (docs/CONFIG.md); e is the only live control. Full keybinding reference: docs/KEYBINDINGS.md, or press ? in the TUI.

Autonomy with a control operator present

Hands-off operation is opt-in ([autonomous] enabled = true) and assumes a licensed control operator is at the keyboard and can stop it instantly:

  • Shift+Q is an emergency stop — halts TX and switches autonomous off. It drops the runtime autonomy gate, and every TX item the engine produced for that cycle is discarded before it can be keyed.
  • Autonomous initiation requires a present operator. Calling CQ or pouncing needs a console keypress within the last two minutes (FCC §97.221: a station with nobody at the control point may respond, not originate). Headless or idle, Pancetta drops to respond-only; QSOs already in progress still finish. Every autonomous TX item additionally has to clear the active TxPolicy (e.g. dry-run/listen-only modes).
  • The remote-TX arm gate fails closed. Separate from the above, and specific to transmissions originated over the remote-operation protocol: a poisoned lock means no transmit, never a permissive fallback, and it ANDs under the active TX policy. It governs TxOrigin::Remote only — local and autonomous TX are gated by the mechanisms above.
  • Drop-stale-TX. The TX worker re-checks QSO liveness at the last instant before PTT, so a frame for a QSO that just ended never goes out.
  • One parity, always. Concurrent QSOs share a slot parity; Pancetta never transmits in sequential windows.

Regulatory notes, including FCC §97.221 automatic-control considerations: docs/fcc-part97-compliance.md.

cqdx.io integration

Pancetta pairs with cqdx.io, the DX/logbook layer around the station:

  • Hunting. The needed-DXCC feed from cqdx.io flows straight into the priority scorer, so DX Hunter weighs decoded CQs by what you actually still need — not just what's loudest. Matching is prefix-based, so a needed entity represented by a short/overlapping prefix can occasionally mismatch (e.g. a needed K entry can false-match KH6/KL7/KP4); treat rankings as a strong hint, not a guarantee. Needed-grid is wired on the Pancetta side but has no live cqdx.io endpoint yet, so it doesn't affect rankings today.
  • Scoring. Rarity and live-spot data from cqdx.io feed the same scorer alongside signal and recent activity, so the top of the list is the contact worth working right now.
  • Logbook. Opt in via [network.cqdx] (with a token) and every completed QSO and spot report uploads to cqdx.io directly — no ADIF export/import round-trip. (This tracks completed QSOs, not LoTW/ClubLog-confirmed ones — confirmation status arrives separately.)
  • Remote rig access. cqdx.io also brokers authorization for remote control: an operator can grant a delegate device — panino, the companion remote-control client, or a browser — scoped, revocable access to the pancetta-controlled rig. Delegated guests get status/QSY visibility and can request a QSO/TX action, but never authorization to actually key the radio — only the station's own (non-delegated) client can arm TX, subject to the remote-TX arm described above.

Not every path is equally mature yet — see Why not (yet) for current status.

Quick start

Prebuilt binaries. No Rust toolchain needed. Starting with the first tagged release after this note was added, every release ships CI-verified binaries for macOS (Apple Silicon), Linux x86_64, Linux aarch64 (Raspberry Pi 4/5 and other 64-bit ARM boards — needs glibc >= 2.35, i.e. Raspberry Pi OS Bookworm (2023-10) or newer; Bullseye-era images can't load it), and Windows x86_64 — if /releases/latest doesn't list an archive for your platform yet, either build from source below or wait for the next tag.

# Linux/macOS: the archive extracts into a version-named directory — the
# binary isn't at the archive root.
tar xzf pancetta-*.tar.gz && cd pancetta-*/

# Linux only: the runtime ALSA library (not the `-dev` headers the
# from-source table below lists) — install if `./pancetta` fails to start
# with a missing libasound.so.2 error.
sudo apt install -y libasound2   # libasound2t64 on Debian 13/Ubuntu 24.04+

# macOS only: this binary isn't notarized yet, so a browser download is
# quarantined and Gatekeeper blocks it until you clear that bit once.
xattr -d com.apple.quarantine ./pancetta 2>/dev/null || true

./pancetta        # first-run wizard — same as step 3 below

(Windows: extract the .zip, open the extracted folder, and run pancetta.exe.)

Run it again the same way to start the station — step 4's cargo run --release -p pancetta is the source-build equivalent of that second run; a prebuilt binary doesn't need it or a source checkout. Hamlib is still needed at runtime if you want to key a radio (see the table below).

To build from source instead:

1. Dependencies.

# Linux (Debian/Ubuntu):
sudo apt update
sudo apt install -y libasound2-dev libudev-dev libssl-dev pkg-config libhamlib-utils
# macOS:
brew install hamlib
curl https://sh.rustup.rs -sSf | sh
RequirementLinuxmacOSWindows
Rust toolchainrustup → stablerustup → stablerustup → stable
Audio dev headerslibasound2-dev, libudev-dev(built in)(built in)
TLSlibssl-dev, pkg-config(built in)(built in)
Hamlib (CAT — optional, runtime-only)apt install libhamlib-utilsbrew install hamlibhamlib Windows build

Hamlib is only needed to key a radio, not to build or to decode.

2. Build. --recursive matters: without the submodule the build warns and falls back to the Rust-only decode path. First build is 5–10 minutes.

git clone --recursive https://github.com/HagaleTechnologies/pancetta.git
cd pancetta
cargo build --release

3. Configure. The first run walks you through writing ~/.pancetta/pancetta.toml — callsign, grid, audio devices, rig. Every key is documented in docs/CONFIG.md.

./target/release/pancetta        # first-run wizard
cargo install --path pancetta    # optional: put `pancetta` on PATH

4. Run. With [rig.interface] disabled (the default) this is decode-only and cannot key the radio. Enable the rig interface for TX; see docs/RUNBOOK.md for the autonomous-QSO procedure.

cargo run --release -p pancetta

Put your real callsign in station.callsign before transmitting. Pancetta refuses to call CQ as NOCALL/N0CALL, but it will transmit whatever else you give it — Part 97 is your responsibility, not the software's.

Command-line tools

One binary, several subcommands (pancetta <cmd> --help for details):

CommandWhat it does
pancettaRun the station (TUI). First run launches the setup wizard.
pancetta doctorStation health check with a printed fix per failure.
pancetta setupInteractive station/audio/rig/PTT wizard. Safe to re-run.
pancetta test-audio --listList audio devices exactly as pancetta sees them.
pancetta test-rigTest the rig link; --ptt keys TX for 1 s (careful!).
pancetta config --validateValidate config, non-zero on error (--show, --generate).
pancetta export --output log.adiExport logged QSOs to ADIF.
pancetta benchmark-decode <wav-or-dir>Native decoder vs. ft8_lib on WAV captures.
pancetta --wav <file>Decode one 15-s WAV and exit (no audio hardware).
pancetta --replay <wav-dir>Feed a directory of WAVs through the full TUI pipeline.
pancetta --headlessNo TUI; logs to ~/.pancetta/logs/.

Why not (yet)

Honest gaps, so you can judge whether it fits your station:

  • Pre-1.0. Config keys, CLI surface, and on-disk formats can still change between releases.
  • Some integrations are scaffolded, not live. pancetta-hamlib's FFI bindings are done, but the project's own status table still calls the crate an integration stub; pancetta-cqdx awaits live API validation; LoTW/eQSL upload is scaffolded where ClubLog/QRZ/cqdx paths are live.
  • Platform coverage is narrow. Developed on macOS (Apple Silicon), CI on Linux, deployed on a Windows 11 MiniPC. Prebuilt Linux aarch64 binaries are published for Pi-class ARM, but real-hardware validation (the effort-budget work's actual target) hasn't happened yet — CI only proves the binary builds and decodes correctly on an ARM64 cloud runner, not on a Raspberry Pi.
  • One radio, well tested. CAT/PTT is exercised against a Yaesu FTdx10. Other hamlib-supported rigs should work; nobody has proven it.
  • pancetta-research is a local-only decoder harness, excluded from CI and unsupported.

Building, testing, lint

cargo build --workspace
cargo test --workspace --features transmit
cargo test -p pancetta-hamlib --lib -- --test-threads=1   # deterministic mock rig
cargo test -p pancetta --test loopback_qso                # end-to-end encode→decode
cargo clippy --workspace --features transmit
cargo fmt --all -- --check

CI runs all of this per PR plus a macOS cargo check lane; cargo deny check guards advisories and license drift. The 14-crate strictly layered workspace is mapped in docs/ARCHITECTURE.md.

Documentation

API docs: cargo doc --workspace --no-deps --open.

Acknowledgments

  • Joe Taylor (K1JT) and Steve Franke (K9AN) designed FT8 — the LDPC code, Costas sync arrays, modulation, and message schema this project implements, documented in The FT4 and FT8 Communication Protocols.
  • Kārlis Goba (YL3JG) authored ft8_lib, the MIT-licensed C implementation Pancetta vendors at pancetta-ft8/vendor/ft8_lib/ and calls via FFI. Several algorithms in the native Rust decoder are ported from it (CRC-14, LDPC tables, Gray code mapping, sliding spectrogram, LLR normalization) and attributed in the source where they appear.
  • The WSJT-X project (GPL) is the de-facto reference FT8 application. Pancetta does not link or vendor any WSJT-X source; it interoperates through the published protocol only.

Full third-party license text: THIRD-PARTY-NOTICES.md. Licensing provenance and the clean-room process: docs/PROVENANCE.md.

License

Dual-licensed under your choice of:

Contributions are accepted under the same dual-license terms unless explicitly stated otherwise in the PR.