Files
telfax/docs/CLASS1_DESIGN.md

8.1 KiB
Raw Permalink Blame History

Telfax Class 1 Fax Design Document

Overview

Telfax is a standalone fax server built in Rust, implementing ITU-T T.30 fax protocol via Class 1 modem control. This document covers the architecture, protocol implementation, and key design decisions for reliable real-world fax transmission over POTS lines.

Architecture

Module Structure

telfax/src/
├── fax/
│   ├── class1/
│   │   ├── send.rs       — TX: T.30 state machine, HDLC, page data
│   │   ├── recv.rs       — RX: T.30 state machine, page data decode
│   │   └── session.rs    — Shared T.30 event types
│   ├── class2/           — Class 2 (modem-managed T.30)
│   ├── hdlc.rs           — HDLC framing, DLE-stuffing, FCS, bit-reversal
│   ├── t4.rs             — T.4 G3 decode (MH → pixels)
│   ├── encoder/
│   │   └── mh.rs         — T.4 Modified Huffman encoder (pixels → MH)
│   └── negotiate.rs      — DIS/DCS capability negotiation
├── document/
│   ├── pdf.rs            — PDF → TIFF-F via Ghostscript
│   ├── convert.rs        — Image → fax format (width clamp, threshold)
│   ├── cover.rs          — Cover page generation (CJK rasterization)
│   └── tiff.rs           — TIFF-F writer (MH compressed output)
├── modem/
│   ├── driver.rs         — Serial port I/O (ATSPEED, read_until)
│   ├── at.rs             — AT command channel
│   └── pool.rs           — Multi-modem pool management
├── ocr/
│   └── mod.rs            — Tesseract OCR wrapper (cover verification)
├── api/
│   └── routes.rs         — HTTP API (axum): send, jobs, cover, health
└── worker/
    └── executor.rs       — Background job processor (queue → modem)

Class 1 Protocol Implementation

T.30 State Machine

Class 1 offloads HDLC framing and modulation to the host software. The modem acts as a raw data pump.

Sender (send.rs)

Dial → Wait CONNECT → Escape → FRH (receive DIS)
  → Parse DIS capabilities → Build DCS
  → FTH=3 (send DCS) → FTM=n (send TCF)
  → FRH (receive CFR/FTT)
  → [For each page]:
      FTM=n (send page data) → FTH=3 (send MPS/EOP)
      → FRH (receive MCF)
  → FTH=3 (send DCN) → Hangup

Receiver (recv.rs)

ATA → Wait RING → Answer → Wait DIS/DCS
  → FRM=n (receive page data) → FTH=3 (send MCF)
  → [For each page]:
      FRH (receive MPS/EOP/DCN)
      → FTH=3 (send MCF if more pages)
  → Hangup

HDLC Layer (hdlc.rs)

The modem strips HDLC flags and bit-stuffing on FRH=3. We handle:

  1. DLE-stuffing: Per T.31, 0x10 → 0x10 0x10. dle_unstuff() reverses this.
  2. FCS stripping: USR5637 outputs the 2-byte CRC-CCITT as part of frame data. parse_modem_hdlc() strips it.
  3. Bit reversal: USR5637 reverses ALL bytes during FTH=3 transmission. The receiver must reverse them back to get correct frame content. parse_modem_hdlc() applies reverse_bits() to all bytes.
  4. FCF masking: Bit 7 of the FCF byte is an address extension bit set by the calling station. parse_modem_hdlc() masks it with 0x7F.

Bit Reversal Details

USR5637 modem reverses bit order (MSB↔LSB) on every byte during FTH=3. This is not documented in the modem manual but was discovered during testing.

Original:  0xFF → 0xFF (all ones, no change)
           0x01 → 0x80
           0x03 → 0xC0
           0x28 → 0x14

The reverse_bits() function pre-reverses bytes before FTH=3 transmission. On FRH=3 receive, the modem does NOT reverse, so received data is already in correct orientation — but the sender's FTH=3 reversal must be undone by the parser.

T.4 Page Data

Encoding (send)

  1. Grayscale pixels → threshold to B&W (128/255 threshold)
  2. Each scan line → MH run-length encoding via MhEncoder
  3. MH-encoded bytes → DLE-stuff → transmit via FTM=n

Decoding (recv)

  1. Raw bytes from FRM=n → DLE-unstuff
  2. MH bitstream → run-length decode → pixel array
  3. find_first_eol_bit() locates the EOL marker (11+0...01 pattern)
  4. transitions_to_pixels() converts alternating run lengths to pixel data

Data Rates

Modulation FRM/FTM value Speed Use
V.21 (300 baud) 3 HDLC signaling DIS/DCS/MCF/etc.
V.27ter (4800) 5 Page data (low) Fallback
V.27ter (9600) 6 Page data (default) Standard
V.29 (9600) 7 Page data Alternate
V.29 (14400) 8 Page data (fast) High speed
V.17 (12000) 145 Training (TCF) Negotiation
V.17 (12000) 146 Page data Fastest stable

FCS Computation

CRC-CCITT polynomial: x^16 + x^12 + x^5 + 1 (0x1021)

  • Initial value: 0xFFFF
  • Reflected input/output
  • Final XOR: 0xFFFF
  • FCS = complement of CRC

Modem Compatibility

Tested Modems

Modem Class 1 Class 2 Notes
USR5637 ✅ ✅ Bit reversal on FTH=3. FCS not stripped. Reliable.
V90 CX93001 ✅ ❌ Class 2 FDT returns OK but internal T.30 fails (FHNG:025). Use Class 1.

Modem Detection

AT+GMI   → Manufacturer
AT+GMM   → Model
ATI3     → Model (fallback)
AT+FCLASS=?  → Class support
AT+FTM=? → TX rate capabilities
AT+FRM=? → RX rate capabilities

Document Pipeline

Input Formats

Format Path Conversion
TIFF (existing) Direct Parse pages, use as-is
PDF pdf.rs Ghostscript → TIFF-G3 → pixel extraction
PNG/JPEG convert.rs image crate → grayscale → threshold → width clamp

Width Handling

Standard fax width: 1728 pixels (A4 at 204 DPI).

  • PDFs at US Letter (8.5") → 1734px at 204 DPI → clamped to 1728 via width.min(1728)
  • Images wider than 1728 → scaled down proportionally
  • Images narrower than 1728 → no scaling

Cover Pages

Generated via cover.rs:

  • Rasterizes text to grayscale bitmap using PingFang CJK font
  • Supports Traditional Chinese, Simplified Chinese, Japanese, English
  • Output: 1728×2291 pixels (standard fax page)
  • Formats: TIFF (for fax) or HTML (for preview)

OCR Verification

verify-cover command:

  1. Generate cover page TIFF
  2. Run Tesseract OCR (eng + chi_tra) with PSM 3
  3. Strip whitespace from OCR output (CJK characters get spaces inserted)
  4. Compare fields: FACSIMILE, TO, FROM, DATE, PAGES, SUBJECT, NOTES

API Server

Endpoints

Method Path Description
GET /api/health Server health + modem status
POST /api/fax/send Queue a fax job
GET /api/fax/jobs List all jobs
GET /api/fax/jobs/{id} Get job details
PUT /api/fax/jobs/{id}/cover Update cover page fields
POST /api/fax/jobs/{id}/retry Retry a failed job
DELETE /api/fax/jobs/{id} Cancel a job

Worker

Background FaxWorker polls the queue every 5 seconds:

  1. Dequeue pending job
  2. Load document (PDF/TIFF/image)
  3. Prepend cover page if cover fields present
  4. Open modem, negotiate, transmit
  5. Handle retries with speed fallback (V.17 → V.29 → V.27ter)

Key Design Decisions

  1. Class 1 over Class 2: Class 1 gives full control over T.30 protocol. Class 2 depends on modem firmware which varies wildly (V90 Class 2 is broken).

  2. Pre-reverse bytes for FTH=3: Instead of relying on modem firmware, we pre-reverse all bytes before FTH=3 to compensate for USR5637's undocumented bit reversal.

  3. Split parser: parse_hdlc_payload() (clean) vs parse_modem_hdlc() (modem-specific with FCS strip + bit reversal). Tests use the clean parser; production code uses the modem parser.

  4. Width clamping at 1728: Rather than failing on slightly wider documents, clamp to standard fax width. Minor clipping at edges is acceptable.

  5. MH encoding always: Even though TIFF-G3 supports MR and MMR, we use MH (Group 3) for maximum compatibility with all receiving fax machines.

Test Hardware

Component Detail
Modem 1 Agere USR5637 USB, phone: 2528****
Modem 2 CONEXANT V90 CX93001 USB, phone: 2748****
External fax Phone: 2515****
Lines Two separate POTS lines
Host macOS (Apple Silicon)