Architecture Overview¶
Module Map¶
| Path | What lives there |
|---|---|
main.c |
app_main, task launch, orchestration |
display/ |
display driver, BSP init, LCD + touch |
ui/ |
LVGL widgets, spectrum/waterfall canvases, FT8 screen |
cat/ |
USB CDC-ACM CAT polling, QMX control |
audio/ |
USB UAC consumer (core 0, polling mode) |
dsp/ |
FFT, spectrum, I/Q balance, CW demod |
ft8_tx.c |
FT8 TX engine, arm/run/abort, tone finder |
ft8_test.c |
FT8 RX: slot loop, capture, decode, state machine |
ft8_sim.c |
FT8 simulation mode (phantom stations) |
ft8_qso.c |
FT8 QSO state machine (pounce + CQ-run) |
render/ |
30 Hz render task, spectrum smoothing, waterfall |
wifi/ |
WiFi + SNTP |
net/webserver.c |
HTTP server, web UI endpoints, WebSocket |
adif/ |
ADIF logging, QRZ/eQSL upload |
storage/ |
NVS settings, config I/O |
rtc/ |
RTC driver (RX8130CE) |
time_sync/ |
Time orchestrator (SNTP/RTC/QMX/manual) |
util/ |
FPS counter, diagnostic logging, bandplan |
espressif__usb_host_uac/ |
UAC + CDC-ACM coexistence (patched) |
espressif__esp_lcd_touch_st7123/ |
ST7121/ST7123 compatibility (patched) |
Data Flow¶
Audio Path (Spectrum + Waterfall)¶
FT8 Receive Path¶
FT8 Transmit Path¶
Task Priorities¶
Priority 25 (highest) — app_main (setup only)
Priority 24 — WiFi / SNTP
Priority 10 — web server
Priority 6 — FT8 transmit ISR
Priority 4 — FFT (ring buffer consumer, spectrum producer)
Priority 1 — FT8 decode, CAT poll, render, LVGL, time sync
Critical: FT8 decode runs at priority 1 — lowest. This ensures real-time tasks (FFT, USB) never starve, and the UI thread remains responsive.
DSP Pipeline¶
Spectrum Calculation¶
Details worth knowing, each of them load-bearing:
- FFT — 1024-point complex. The esp-dsp ANSI fallback is used deliberately: the PIE/vector build crashes under sustained WebSocket load on this silicon.
- IF offset — the QMX presents I/Q at +12 kHz, so the spectrum, waterfall and S-meter all shift bin selection by
n_bins/4to put the VFO signal at the visual centre. Miss that shift and the S-meter reads the DC/LO spike instead of the signal. - DC blocker — a one-pole IIR on the I/Q stream, ahead of the FFT.
- Spectrum smoothing — per-bin EMA, α = 0.4 by default and adjustable in the drawer.
- dBm calibration —
DSP_DB_CALIBRATION_OFFSET = −148.0 dB, measured on a dummy load: the noise floor reads −130 dBm and S9 = −73 dBm. - Waterfall scroll — a 1280×824 double-height canvas, so a new row costs about 130 µs instead of the ~92 ms a
memmovewould take. - Audio task — polling on core 0 with a drain loop, not event-driven. Event-driven reads returned truncated UAC chunks that saturated the FFT input and pumped the noise floor on a slow ~13 s cycle.
I/Q Balance (Gram-Schmidt)¶
Per-sample correction applied in audio.c before ring buffer:
Waterfall¶
FT8 Decode List¶
Storage¶
NVS (Non-Volatile Storage)¶
Settings namespace: QMX
Key Type Example
wifi_ssid string "MyNet"
wifi_pass string "password123"
callsign string "OZ1LAV"
grid string "JO45"
last_freq_20m uint32 14074000
last_mode_20m uint8 2 (USB)
memory_1 blob { freq, mode, bw, name }
...
diag_log uint8 1 (on/off)
ADIF Log¶
File: /spiffs/qso.adi
Format: Standard ADIF (one QSO per <EOR> record). Each QSO stores:
CALL, GRIDSQUARE, RST_SENT, RST_RCVD, QSO_DATE, TIME_ON, FREQ, BAND,
MODE (+ SUBMODE on FT4), STATION_CALLSIGN, MY_GRIDSQUARE,
MY_SIG/MY_SIG_INFO (our activation), SIG/SIG_INFO (theirs), ...
Downloaded via web UI or serial.
Timing & Slots¶
FT8 Slot (15 seconds)¶
UTC Boundary Alignment¶
The capture window is UTC-clamped, not fixed-sample-count. Actual window length is computed:
ms_to_boundary = 15000 - (sys_time_ms % 15000)
dsp_ft8_capture(timeout = min(ms_to_boundary, SLOT_TIMEOUT_MS))
If samples run short before the boundary, DSP zero-pads the rest. This keeps the capture window locked to UTC, preventing the multi-slot drift problem seen in earlier versions.
Next: Read CLAUDE.md for the detailed quirks and critical knowledge, or contribute via Contributing.