Files
telfax/docs/CLASS1_DESIGN.md

226 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
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) |