diff --git a/docs/wefax_plan.md b/docs/wefax_plan.md new file mode 100644 index 0000000..b6402b7 --- /dev/null +++ b/docs/wefax_plan.md @@ -0,0 +1,355 @@ +# WEFAX / Radiofax Decoder Implementation Plan + +> **Crate**: `trx-wefax` — `src/decoders/trx-wefax/` +> **Status**: Draft — 2026-04-02 + +## 1. Overview + +WEFAX (Weather Facsimile, ITU-T T.4 / WMO) is an analog image transmission +mode used by meteorological agencies worldwide (NOAA, DWD, JMH, etc.) on HF +and satellite downlinks. The decoder converts FM-modulated audio tones into +greyscale (or colour-composited) image lines. + +### Goals + +- Pure Rust, zero C FFI dependencies (matching project conventions). +- Multi-speed support: **60, 90, 120, 240 LPM** (lines per minute). +- Multi-IOC support: **288 and 576** (Index of Cooperation — defines + line pixel width). +- Automatic start/stop detection via APT tones. +- Phase-aligned line assembly from phasing signal. +- Incremental image output (line-by-line progress + final PNG). +- Follow existing decoder patterns (`process_block` / `decode_if_ready`). + +## 2. WEFAX Signal Structure + +``` +Carrier (1900 Hz center, ±400 Hz deviation) + Black = 1500 Hz + White = 2300 Hz + (linear mapping between frequency and luminance) + +Transmission sequence: + ┌─────────────┐ + │ Start tone │ 300 Hz (5s) or 675 Hz (3s) — selects IOC 576 / 288 + ├─────────────┤ + │ Phasing │ >95% white line + narrow black pulse — phase alignment + │ (30 lines) │ + ├─────────────┤ + │ Image lines │ N lines at configured LPM + ├─────────────┤ + │ Stop tone │ 450 Hz (5s) — signals end of transmission + └─────────────┘ +``` + +### Key parameters + +| Parameter | IOC 576 | IOC 288 | +|-----------|---------|---------| +| Pixels per line | 1809 | 904 | +| Line duration (120 LPM) | 500 ms | 500 ms | +| Line duration (60 LPM) | 1000 ms | 1000 ms | +| Pixel clock | ~3618 px/s (120 LPM) | ~1808 px/s (120 LPM) | + +Pixel count per line = `IOC × π` (rounded: 576×π ≈ 1809, 288×π ≈ 904). + +## 3. Architecture + +```mermaid +graph TD + PCM["PCM audio (f32, 48 kHz)"] --> RS["Resampler (to internal rate)"] + RS --> FM["FM Discriminator"] + FM --> LPF["Low-pass filter (anti-alias)"] + LPF --> TD["Tone Detector (APT start/stop)"] + LPF --> PA["Phase Aligner"] + PA --> LS["Line Slicer"] + LS --> IMG["Image Assembler"] + IMG --> OUT["WefaxMessage (line / image)"] + TD --> SM["State Machine"] + SM -->|controls| PA + SM -->|controls| LS +``` + +### Internal sample rate + +Resample input to **11,025 Hz** (sufficient for 2300 Hz max tone with +comfortable margin; matches common WEFAX decoder practice and keeps DSP +cost low). + +## 4. Module Layout + +``` +src/decoders/trx-wefax/ + Cargo.toml + src/ + lib.rs # Public API: WefaxDecoder, WefaxConfig, WefaxEvent + decoder.rs # Top-level decoder state machine + process_block/decode_if_ready + demod.rs # FM discriminator (instantaneous frequency from analytic signal) + tone_detect.rs # Goertzel-based APT tone detector (300/450/675 Hz) + phase.rs # Phasing signal detector and line-start alignment + line_slicer.rs # Pixel clock recovery, line buffer assembly + resampler.rs # Polyphase rational resampler (48k → 11025) + image.rs # Image buffer, PNG encoding, optional colour compositing + config.rs # WefaxConfig: speed, IOC, auto-detect, output path +``` + +## 5. Core Types + +### 5.1 Configuration + +```rust +pub struct WefaxConfig { + /// Lines per minute: 60, 90, 120, 240. `None` = auto-detect from APT. + pub lpm: Option, + /// Index of Cooperation: 288 or 576. `None` = auto-detect from start tone. + pub ioc: Option, + /// Centre frequency of the FM subcarrier (default 1900 Hz). + pub center_freq_hz: f32, + /// Deviation (default ±400 Hz, so black=1500, white=2300). + pub deviation_hz: f32, + /// Directory for saving decoded images. + pub output_dir: Option, + /// Whether to emit line-by-line progress events. + pub emit_progress: bool, +} +``` + +### 5.2 Decoder state machine + +```rust +pub enum WefaxState { + /// Listening for APT start tone. + Idle, + /// Start tone detected; waiting for phasing signal. + StartDetected { ioc: u16, tone_start_sample: u64 }, + /// Receiving phasing lines; aligning line-start phase. + Phasing { ioc: u16, lpm: u16, phase_offset: Option }, + /// Actively decoding image lines. + Receiving { ioc: u16, lpm: u16, line_number: u32 }, + /// Stop tone detected; finalising image. + Stopping, +} +``` + +### 5.3 Output messages (for `trx-core::DecodedMessage`) + +```rust +/// A complete or in-progress WEFAX image. +pub struct WefaxMessage { + pub rig_id: Option, + pub ts_ms: Option, + /// Number of image lines decoded so far. + pub line_count: u32, + /// Detected or configured LPM. + pub lpm: u16, + /// Detected or configured IOC. + pub ioc: u16, + /// Pixels per line (IOC × π, rounded). + pub pixels_per_line: u16, + /// Filesystem path to saved PNG (set on completion). + pub path: Option, + /// True when image is complete (stop tone received). + pub complete: bool, +} + +/// Progress update emitted every N lines during active reception. +pub struct WefaxProgress { + pub rig_id: Option, + pub line_count: u32, + pub lpm: u16, + pub ioc: u16, +} +``` + +## 6. DSP Pipeline Detail + +### 6.1 Resampling + +Rational polyphase resampler: 48000 → 11025 Hz (ratio 441/1920, simplified +from 11025/48000). Follow `docs/Optimization-Guidelines.md` polyphase +resampler guidance. Same pattern as FT8 decoder's 48k→12k resampler. + +### 6.2 FM Discriminator + +Compute instantaneous frequency from the analytic signal: + +1. **Hilbert transform** (FIR, 65-tap) to produce analytic signal `z[n]`. +2. **Instantaneous frequency**: `f[n] = arg(z[n] · conj(z[n-1])) / (2π·Ts)` +3. Map frequency to luminance: `pixel = clamp((f - 1500) / 800, 0, 1)`. + +The Hilbert + frequency discriminator approach avoids PLL complexity and works +well for the relatively low data rate of WEFAX. + +### 6.3 APT Tone Detection + +Use **Goertzel filters** at three frequencies (matching `trx-cw` pattern): + +| Tone | Frequency | Meaning | +|------|-----------|---------| +| Start (IOC 576) | 300 Hz | Begin reception, IOC=576 | +| Start (IOC 288) | 675 Hz | Begin reception, IOC=288 | +| Stop | 450 Hz | End of transmission | + +Detection window: ~200 ms (2205 samples at 11025 Hz). Require sustained +detection for ≥1.5 s to confirm (debounce against noise). Energy ratio +vs broadband noise for reliability. + +### 6.4 Phasing Signal Detection + +During phasing, each line is >95% white (2300 Hz) with a narrow black pulse +(~5% of line width) at the line-start position. + +1. After start tone, begin accumulating demodulated samples. +2. Slice into line-duration windows (e.g., 500 ms for 120 LPM). +3. Cross-correlate against expected phasing template (short black pulse). +4. Average pulse position over 10+ phasing lines → line-start phase offset. +5. Transition to `Receiving` once phase is stable (variance < 2 samples). + +### 6.5 Line Slicing and Pixel Clock + +Once phased: + +1. Accumulate demodulated (frequency → luminance) samples. +2. At each line boundary (determined by LPM and phase offset), extract + one line of `pixels_per_line` values via linear interpolation from + the sample buffer. +3. Push completed line into the image assembler. +4. Emit `WefaxProgress` every 50 lines (configurable). + +### 6.6 Image Assembly + +- Maintain a `Vec>` of greyscale lines (0–255). +- On stop tone or manual stop: encode to 8-bit greyscale PNG. +- Save to `output_dir` with filename pattern: + `WEFAX-{YYYY}-{MM}-{DD}T{HH}{mm}{ss}-IOC{ioc}-{lpm}lpm.png` +- Return `WefaxMessage` with `complete: true` and `path` set. + +## 7. Integration with trx-rs + +### 7.1 Workspace registration + +Add to root `Cargo.toml` workspace members: + +```toml +"src/decoders/trx-wefax" +``` + +### 7.2 `trx-core` changes + +Add variants to `DecodedMessage`: + +```rust +#[serde(rename = "wefax")] +Wefax(WefaxMessage), +#[serde(rename = "wefax_progress")] +WefaxProgress(WefaxProgress), +``` + +Update `set_rig_id()` / `rig_id()` match arms. + +### 7.3 `trx-server` integration + +Add `run_wefax_decoder()` in `audio.rs` following the existing pattern: + +```rust +pub async fn run_wefax_decoder( + sample_rate: u32, + channels: u16, + mut pcm_rx: broadcast::Receiver>, + state_rx: watch::Receiver, + decode_tx: broadcast::Sender, + logs: Option>, + histories: Arc, +) +``` + +Spawn in `main.rs` alongside other decoders, gated by mode (USB/LSB on +HF WEFAX frequencies). + +### 7.4 History and logging + +- Add `wefax: Arc>>` to `DecoderHistories`. +- Add optional `wefax` logger to `DecoderLoggers` (JSON Lines). + +### 7.5 Frontend exposure + +- SSE event stream: emit `wefax` and `wefax_progress` events. +- REST endpoint: `GET /api/rig/{id}/decode/wefax` — list recent images. +- WebSocket: stream in-progress image data for live preview (future). + +## 8. Implementation Phases + +### Phase 1: Core DSP (MVP) + +1. **Resampler** — 48k→11025 polyphase resampler with tests. +2. **FM discriminator** — Hilbert FIR + instantaneous freq, verify + against synthetic 1500–2300 Hz sweeps. +3. **Tone detector** — Goertzel at 300/450/675 Hz with debounce. +4. **Line slicer** — Fixed-config (manual LPM+IOC) line extraction. +5. **Image buffer + PNG** — Greyscale line accumulation, `image` or + `png` crate for encoding. + +Deliverable: decode a known WEFAX WAV recording at a single speed/IOC. + +### Phase 2: Automatic Detection + +6. **State machine** — Full `Idle→StartDetected→Phasing→Receiving→Stopping` + transitions driven by tone detector. +7. **Phase alignment** — Cross-correlation phasing detector. +8. **Auto IOC/LPM** — IOC from start tone frequency; LPM from phasing + line duration measurement. + +Deliverable: fully automatic reception of a single image without manual config. + +### Phase 3: Server Integration + +9. **`trx-core` message types** — `WefaxMessage`, `WefaxProgress` in + `DecodedMessage`. +10. **`trx-server` task** — `run_wefax_decoder()`, history, logging. +11. **Frontend events** — SSE/REST for decoded images. + +Deliverable: end-to-end live WEFAX decoding in trx-rs. + +### Phase 4: Polish + +12. **Multi-speed runtime switching** — handle back-to-back + transmissions at different LPM within one session. +13. **Slant correction** — fine-tune sample clock drift compensation + using phasing pulse tracking. +14. **Colour compositing** — optional IR + visible overlay for + satellite WEFAX (future). +15. **Test suite** — synthetic signal generation, round-trip tests, + edge cases (partial images, noise, frequency offset). + +## 9. Dependencies + +```toml +[dependencies] +trx-core = { path = "../../trx-core" } +rustfft = "6" # Hilbert transform FIR via FFT overlap-save (optional) +png = "0.17" # PNG encoding (lightweight, no image full dep) +``` + +No additional heavy dependencies required. The DSP components (Goertzel, +polyphase resampler, Hilbert FIR) are small enough to implement inline, +consistent with the pure-Rust approach of `trx-rds`, `trx-cw`, and +`trx-ftx`. + +## 10. Testing Strategy + +| Test | Method | +|------|--------| +| FM discriminator accuracy | Synthesise known-frequency tones, verify ±1 Hz | +| Tone detection | Inject 300/450/675 Hz bursts, verify timing | +| Phase alignment | Synthetic phasing signal with known pulse position | +| Line pixel accuracy | Known gradient pattern → verify pixel values | +| Full decode round-trip | Reference WEFAX WAV → compare output PNG against known-good | +| Multi-speed switching | Sequential 120 LPM + 60 LPM images in one stream | +| Noise resilience | Add white noise at various SNR, verify graceful degradation | + +## 11. References + +- ITU-R BT.601 (facsimile signal characteristics) +- WMO Manual on the GTS, Attachment II-13 (HF radiofax schedule/format) +- NOAA Radiofax Charts: frequency schedules and IOC/LPM per product +- Existing open-source implementations: `fldigi` WEFAX module, `multimon-ng`