V1.0: Class 1 fax — real-world 4-page send to external number confirmed

Core features:
- Class 1 T.30 protocol: full send/receive implementation
- HDLC: DLE-stuffing, FCS strip, USR5637 bit-reversal handling
- T.4 MH encoder/decoder (1728px A4 standard)
- Document pipeline: PDF (Ghostscript), PNG, TIFF input
- Width clamping: US Letter 1734px → 1728px fax standard
- Cover page: CJK rasterization (TW/CN/JP/EN), TIFF + HTML output
- OCR verification: Tesseract 5 with eng+chi_tra, CJK space-tolerant
- API server (axum): health, send, jobs, cover, retry, cancel
- Background worker: auto-poll queue, speed fallback, retry policy
- Modem detection, pool management

Real-world test results (2026-07-23):
- V90 → 25153038: 4 pages, V.17 12000 bps, 2:33 ✅
- USR5637 → 25153038: 4 pages, V.17 12000 bps, 2:26 ✅
- Both faxes confirmed received on remote machine

Tested: loopback (100% pixel match), multi-page, all input formats,
cover pages, OCR verify, API endpoints, worker processing.
13 unit tests pass, 0 new clippy warnings.
This commit is contained in:
Warren
2026-07-24 18:47:15 +08:00
parent d1e92b32fb
commit 55bca92691
155 changed files with 25024 additions and 916 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Generate a demo business letter as a fax-ready TIFF."""
from PIL import Image, ImageDraw, ImageFont
import sys, os
W, H = 1728, 2291 # A4 at 204x196 dpi
def create_demo_document(output_path):
img = Image.new('L', (W, H), 255)
draw = ImageDraw.Draw(img)
# Try to load fonts — fall back to default
try:
title_font = ImageFont.truetype('/System/Library/Fonts/Helvetica.ttc', 72)
h1_font = ImageFont.truetype('/System/Library/Fonts/Helvetica.ttc', 48)
body_font = ImageFont.truetype('/System/Library/Fonts/Helvetica.ttc', 36)
small_font = ImageFont.truetype('/System/Library/Fonts/Helvetica.ttc', 28)
except:
title_font = h1_font = body_font = small_font = ImageFont.load_default()
# ── Letterhead ──
draw.rectangle([0, 0, W, 180], fill=0)
draw.text((80, 40), "TELFAX CORPORATION", fill=255, font=title_font)
draw.text((80, 130), "Digital Fax Solutions", fill=200, font=small_font)
# horizontal rule
draw.line([80, 200, W-80, 200], fill=100, width=4)
# ── Date & Reference ──
draw.text((80, 240), "Date: July 21, 2026", fill=0, font=body_font)
draw.text((80, 290), "Reference: TFX-2026-07-21-001", fill=0, font=body_font)
draw.text((80, 340), "To: ACME Corporation", fill=0, font=body_font)
draw.text((80, 390), " Attn: Mr. John Smith", fill=0, font=body_font)
draw.text((80, 440), " Fax: +1-555-0199", fill=0, font=body_font)
draw.text((80, 490), "From: Jane Doe, CTO", fill=0, font=body_font)
draw.text((80, 540), " Telfax Corp", fill=0, font=body_font)
draw.text((80, 590), " Fax: +886-2-2748-6656", fill=0, font=body_font)
draw.line([80, 640, W-80, 640], fill=0, width=2)
# ── Subject ──
draw.text((80, 680), "Subject: Business Proposal for Enterprise Fax Solution", fill=0, font=h1_font)
# ── Body ──
body_text = [
"Dear Mr. Smith,",
"",
"We are pleased to submit our proposal for an enterprise-grade digital",
"fax solution to replace ACME Corporation's legacy fax infrastructure.",
"",
"Our solution offers the following key benefits:",
"",
" 1. Cost reduction of up to 80% on international fax transmissions",
" 2. Complete audit trail with encrypted storage for compliance",
" 3. Seamless integration with existing PBX and email systems",
" 4. Support for T.38 IP fax and traditional PSTN (Class 1/2)",
" 5. Multi-language cover pages with OCR verification",
"",
"This document was transmitted via real POTS lines using the telfax",
"open-source fax system running on macOS at V.27ter 4800 bps.",
"",
"We look forward to discussing this opportunity with you.",
"",
"Best regards,",
"",
"Jane Doe",
"Chief Technology Officer",
"Telfax Corporation",
]
y = 780
for line in body_text:
draw.text((80, y), line, fill=0, font=body_font)
y += 52
# ── Footer ──
draw.line([80, y+40, W-80, y+40], fill=180, width=2)
draw.text((80, y+60),
"Page 1 of 1 | This fax was transmitted via telfax (opencode.ai) | 204x196 dpi Fine",
fill=120, font=small_font)
img.save(output_path, dpi=(204, 196))
print(f"Demo document created: {output_path} ({W}x{H} px)")
if __name__ == '__main__':
out = sys.argv[1] if len(sys.argv) > 1 else "/tmp/demo_document.tif"
create_demo_document(out)
+258
View File
@@ -0,0 +1,258 @@
#!/bin/bash
set -e
# ═══════════════════════════════════════════════════════════════════════════
# telfax — End-to-End Fax Demo
# Shows the complete fax workflow over real POTS lines.
#
# Modem A (sender): /dev/cu.usbmodem00000021 (USR5637) → 2528-9852
# Modem B (receiver): /dev/cu.usbmodem123456781 (V90) → 2748-6656
# ═══════════════════════════════════════════════════════════════════════════
RECV_DEVICE="/dev/cu.usbmodem123456781" # V90 — answers calls
SEND_DEVICE="/dev/cu.usbmodem00000021" # USR5637 — dials out
RECV_NUMBER="27486656" # V90's phone number
OUTPUT_DIR="/tmp"
RESOLUTION="fine"
CLASS="1" # Class 1 HDLC protocol
# ── Timestamp for output files ──
STAMP=$(date +%s)
DEMO_DOC="/tmp/telfax_demo_doc_${STAMP}.tif"
COVER_PAGE="/tmp/telfax_cover_${STAMP}.tif"
PREVIEW_HTML="/tmp/telfax_demo_${STAMP}.html"
RECV_LOG="/tmp/telfax_recv_${STAMP}.log"
SEND_LOG="/tmp/telfax_send_${STAMP}.log"
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ telfax — End-to-End Fax Demo ║"
echo "║ Real POTS Lines | Class 1 | V.27ter 4800 ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""
# ══════════════════════════════════════════════════════════════════════
# STEP 1 — Create a demo document with visible content
# ══════════════════════════════════════════════════════════════════════
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " STEP 1: Create a demo document (business letter)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
python3 "$(dirname "$0")/create_demo_doc.py" "$DEMO_DOC"
echo " ✓ Document saved: $DEMO_DOC"
echo ""
# ══════════════════════════════════════════════════════════════════════
# STEP 2 — Add a cover page (sender/recipient info)
# ══════════════════════════════════════════════════════════════════════
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " STEP 2: Add a cover page"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Generate cover page only (preview with both cover + doc)
# We'll use the preview command later which handles cover + doc together
echo " Cover parameters:"
echo " From: Jane Doe <jane@telfax.com>"
echo " To: John Smith <john@acme.com>"
echo " Subject: Business Proposal for Enterprise Fax Solution"
echo " Notes: Please review at your earliest convenience."
echo " ✓ Cover page will be auto-generated during send"
echo ""
# ══════════════════════════════════════════════════════════════════════
# STEP 3 — Preview cover page + document
# ══════════════════════════════════════════════════════════════════════
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " STEP 3: Preview cover page + document (HTML in browser)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cargo run --release -- preview "$DEMO_DOC" \
--output "$PREVIEW_HTML" \
--resolution fine \
--to "John Smith <john@acme.com>" \
--from "Jane Doe <jane@telfax.com>" \
--subject "Business Proposal for Enterprise Fax Solution" \
--note "Please review at your earliest convenience." \
2>&1 | grep -v "^warning:\|^ Compiling\|^ Finished\|^ Running\|^$"
echo ""
echo " ✓ Preview saved to: $PREVIEW_HTML"
echo " ✓ First page = cover page, Second page = document"
echo ""
# Try to open in browser (macOS)
if command -v open &>/dev/null; then
echo " → Opening preview in browser..."
open "$PREVIEW_HTML" 2>/dev/null || true
fi
# ══════════════════════════════════════════════════════════════════════
# STEP 4 — Start receiver in background, then send
# ══════════════════════════════════════════════════════════════════════
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " STEP 4 & 5: Send fax + Receive fax"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " ┌─ Sender ─────────────────────────────────────┐"
echo " │ Device: $SEND_DEVICE │"
echo " │ Number: 27486656 (V90) │"
echo " │ Doc: $DEMO_DOC │"
echo " │ Class: 1 (V.27ter 4800 bps) │"
echo " └──────────────────────────────────────────────┘"
echo " ┌─ Receiver ───────────────────────────────────┐"
echo " │ Device: $RECV_DEVICE │"
echo " │ Auto-answer: 2 rings │"
echo " │ Output: $OUTPUT_DIR │"
echo " │ Class: 1 │"
echo " └──────────────────────────────────────────────┘"
echo ""
# Kill any leftover processes
pkill -f "telfax.*receive" 2>/dev/null || true
sleep 2
# Start receiver (background, wait for call)
echo " → Starting receiver (V90 listening on 2748-6656)..."
RUST_LOG=telfax=info cargo run --release -- receive \
-d "$RECV_DEVICE" \
-c 1 \
-r 2 \
-o "$OUTPUT_DIR" \
> "$RECV_LOG" 2>&1 &
RECV_PID=$!
echo " ✓ Receiver PID: $RECV_PID"
# Wait for V90 to settle (auto-answer with ATS0=2 needs ~13s)
echo " → Waiting 40s for V90 to settle before dialing..."
sleep 40
# Send fax (foreground — shows live progress)
echo " → Sending fax (this takes ~60s)..."
echo ""
RUST_LOG=telfax=info cargo run --release -- send \
-d "$SEND_DEVICE" \
-c 1 \
-r "$RESOLUTION" \
--to "John Smith <john@acme.com>" \
--from "Jane Doe <jane@telfax.com>" \
--subject "Business Proposal for Enterprise Fax Solution" \
--note "Please review at your earliest convenience." \
"$RECV_NUMBER" "$DEMO_DOC" \
2>&1 | grep -v "^warning:\|^ Compiling\|^ Finished\|^ Running\|^$" \
| sed 's/.*telfax:/\t| telfax:/' \
| sed 's/.*telfax::/\t| telfax::/' \
|| true
echo ""
# Wait for receive to finish
echo " → Waiting for receiver to complete..."
wait $RECV_PID 2>/dev/null || true
echo ""
# ══════════════════════════════════════════════════════════════════════
# STEP 6 — Show the status / progress
# ══════════════════════════════════════════════════════════════════════
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " STEP 6: Transmission status"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " ┌─ SENDER LOG ────────────────────────────────────┐"
grep -E "Document:|training|Dummy frame|Sending page|Waiting for (CONNECT|OK)|error_rate|FRH CONNECT|MCF|DCN|sent successfully" "$SEND_LOG" \
| sed 's/.*telfax::/\t| telfax::/' \
| sed 's/.*telfax:/\t| telfax:/' || echo " (no sender log found)"
echo " └────────────────────────────────────────────────┘"
echo ""
echo " ┌─ RECEIVER LOG ──────────────────────────────────┐"
grep -E "Sending HDLC|FRH CONNECT|Received HDLC|error_rate|FRM CONNECT|FRM line.*CONNECT|Raw FRM|Found DLE|Destuffed|Received page|EOP|MCF|Hanging|TIFF saved|Decoding" "$RECV_LOG" \
| sed 's/.*telfax::/\t| telfax::/' \
| sed 's/.*telfax:/\t| telfax:/' || echo " (no receiver log found)"
echo " └────────────────────────────────────────────────┘"
echo ""
# ══════════════════════════════════════════════════════════════════════
# STEP 7 — Show the received document
# ══════════════════════════════════════════════════════════════════════
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " STEP 7: Received fax document"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
RECEIVED_TIF=$(grep "TIFF saved to" "$RECV_LOG" | sed 's/.*TIFF saved to //' | tr -d '[:space:]')
if [ -f "$RECEIVED_TIF" ]; then
SIZE=$(stat -f%z "$RECEIVED_TIF" 2>/dev/null || stat -c%s "$RECEIVED_TIF" 2>/dev/null)
echo " ✓ Received file: $RECEIVED_TIF"
echo " ✓ Size: $SIZE bytes"
# Generate a preview of the received pages for quick viewing
RECV_PREVIEW="/tmp/telfax_recv_preview_${STAMP}.html"
cargo run --release -- preview "$RECEIVED_TIF" --output "$RECV_PREVIEW" 2>&1 \
| grep -v "^warning:\|^ Compiling\|^ Finished\|^ Running\|^$" || true
echo ""
echo " ✓ Preview of received fax: $RECV_PREVIEW"
if command -v open &>/dev/null; then
open "$RECV_PREVIEW" 2>/dev/null || true
fi
else
echo " ⚠ No received TIFF found. Check receiver log: $RECV_LOG"
grep -E "error|Error|Error:|Timeout|Protocol|NoCarrier" "$RECV_LOG" \
| sed 's/.*telfax::/\t| /' || true
fi
echo ""
# ══════════════════════════════════════════════════════════════════════
# STEP 8 — Explain the whole flow
# ══════════════════════════════════════════════════════════════════════
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " STEP 8: Protocol Flow Explanation"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " T.30 Class 1 fax transmission over analog POTS:"
echo ""
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ RECEIVER (V90) SENDER (USR5637) │"
echo " │ ────────────── ──────────────── │"
echo " │ │"
echo " │ 1. Auto-answer (2 rings) Dial 27486656 │"
echo " │ ├────── CED (2100 Hz) ──────┤ │"
echo " │ ├────── DIS (HDLC) ─────────┤ │"
echo " │ ├────── DCS (HDLC) ─────────┤ │"
echo " │ │ negotiate V.27ter 4800 │ │"
echo " │ ├────── TCF (FRM) ──────────┤ FTM 1.5s zeros │"
echo " │ │ ✓ CONNECT + receive │ │"
echo " │ ├────── CFR (HDLC) ─────────┤ FTH (HDLC) │"
echo " │ │ ├── dummy FTH (HDLC) │"
echo " │ 2. FRM (enter before training) FTM page data │"
echo " │ ├────── Page Data ──────────┤ V.27ter 4800 bps │"
echo " │ │ ✓ CONNECT + receive │ 14,599 bytes MH │
echo " │ ├────── EOP (HDLC) ─────────┤ FRH │"
echo " │ ├────── MCF (HDLC) ─────────┤ FTH │"
echo " │ ├────── DCN (HDLC) ─────────┤ │"
echo " │ │"
echo " │ Total: 1 page, 14636 bytes MH, TIFF saved to disk │"
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
echo " Key engineering details:"
echo " ─────────────────────────"
echo " • V.27ter 4800 bps (fine resolution ~204×196 dpi)"
echo " • Real POTS lines — no VoIP, no T.38"
echo " • HDLC framing with CRC-16 (DLE-stuffed)"
echo " • MH (Modified Huffman) run-length compression"
echo " • Zero-delay FRM: V90 must catch training from start"
echo " • DLE-ETX end-marker detection via rposition"
echo ""
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ Demo complete! ║"
echo "║ ║"
echo "║ All logs: ║"
echo "║ Sender: $SEND_LOG ║"
echo "║ Receiver: $RECV_LOG ║"
echo "║ ║"
echo "║ Outputs: ║"
echo "║ Preview: $PREVIEW_HTML ║"
echo "║ Received: $RECEIVED_TIF ║"
echo "║ Demo doc: $DEMO_DOC ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""