d1e92b32fb
Features:
- Cover page generation using Python/Pillow with Apple PingFang font
- CLI commands: cover-create, job-cover for cover page management
- API endpoints: PUT /api/fax/jobs/{id}/cover for cover modification
- Send fax with cover page: --to, --from, --subject, --note options
- Multi-page TIFF parsing for PDF conversion (fix: all pages now sent)
- Preview HTML generation with pan/zoom support
- OCR verification for cover page content
- Class 2 receive support for incoming faxes
Fixes:
- Modem initialization before Class 2 mode entry
- Dial timeout handling with retry logic
- Multi-page fax transmission between pages
- Chinese character rendering in cover pages (Apple native fonts)
177 lines
5.0 KiB
Python
Executable File
177 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Class 1 Fax Receiver - for testing with USR sender"""
|
|
import termios
|
|
import os
|
|
import time
|
|
import sys
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: class1_receive.py <device>")
|
|
sys.exit(1)
|
|
|
|
device = sys.argv[1]
|
|
fd = os.open(device, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
|
|
|
attrs = termios.tcgetattr(fd)
|
|
attrs[4] = termios.B115200
|
|
attrs[5] = termios.B115200
|
|
attrs[2] |= termios.CLOCAL | termios.CREAD
|
|
attrs[3] &= ~termios.ICANON
|
|
attrs[3] &= ~termios.ECHO
|
|
attrs[3] &= ~termios.ECHOE
|
|
attrs[3] &= ~termios.ISIG
|
|
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
|
termios.tcflush(fd, termios.TCIOFLUSH)
|
|
|
|
def drain():
|
|
try:
|
|
while True:
|
|
os.read(fd, 4096)
|
|
except BlockingIOError:
|
|
pass
|
|
|
|
def send_cmd(cmd, wait=0.5):
|
|
drain()
|
|
os.write(fd, cmd.encode() + b"\r")
|
|
time.sleep(wait)
|
|
result = b""
|
|
try:
|
|
while True:
|
|
data = os.read(fd, 4096)
|
|
result += data
|
|
except BlockingIOError:
|
|
pass
|
|
return result.decode(errors="replace")
|
|
|
|
def wait_for(pattern, timeout=30):
|
|
start = time.time()
|
|
result = b""
|
|
while time.time() - start < timeout:
|
|
try:
|
|
data = os.read(fd, 4096)
|
|
result += data
|
|
decoded = result.decode(errors="replace")
|
|
for p in pattern if isinstance(pattern, list) else [pattern]:
|
|
if p in decoded:
|
|
return decoded
|
|
except BlockingIOError:
|
|
time.sleep(0.1)
|
|
return result.decode(errors="replace")
|
|
|
|
print("[RECV] Initializing Class 1...")
|
|
send_cmd("AT", 0.3)
|
|
send_cmd("AT+FCLASS=1", 0.3)
|
|
|
|
print("[RECV] Waiting for rings...")
|
|
|
|
ring_count = 0
|
|
while ring_count < 2:
|
|
try:
|
|
data = os.read(fd, 4096)
|
|
text = data.decode(errors="replace")
|
|
if "RING" in text:
|
|
ring_count += text.count("RING")
|
|
print(f"[RECV] Ring {ring_count}")
|
|
except BlockingIOError:
|
|
time.sleep(0.3)
|
|
|
|
print("[RECV] Answering...")
|
|
answer_result = send_cmd("ATA", 3)
|
|
print(f"[RECV] ATA result: {answer_result.strip()}")
|
|
|
|
if "CONNECT" not in answer_result:
|
|
drain()
|
|
os.write(fd, b"ATA\r")
|
|
wait_result = wait_for("CONNECT", 10)
|
|
print(f"[RECV] Wait result: {wait_result.strip()}")
|
|
|
|
print("[RECV] Connected! Sending CED tone...")
|
|
time.sleep(0.5)
|
|
|
|
# Send CED tone (2100Hz for 2.6-4 seconds)
|
|
ced_result = send_cmd("AT+FTS=8", 4) # FTS=8 sends 2100Hz
|
|
print(f"[RECV] CED result: {ced_result.strip()}")
|
|
|
|
# Wait 75ms then send DIS
|
|
time.sleep(0.1)
|
|
|
|
print("[RECV] Sending DIS frame...")
|
|
|
|
# Start HDLC transmit
|
|
os.write(fd, b"AT+FTH=3\r")
|
|
fth_result = wait_for("CONNECT", 5)
|
|
print(f"[RECV] FTH=3: {fth_result.strip()}")
|
|
|
|
if "CONNECT" in fth_result:
|
|
# DIS frame: preamble+flags+DCS+data+RTC
|
|
# Address: 0xFF (broadcast), Control: 0x80 (DIS from called)
|
|
# DIS data: minimum capabilities
|
|
dis_data = bytes([
|
|
0xFF, 0x13, 0x80, # Address + Control (DIS from called)
|
|
0x00, 0x50, 0x1F, # DIS parameters
|
|
])
|
|
# Add FCS (we'll let modem calculate)
|
|
os.write(fd, dis_data)
|
|
os.write(fd, b"\x00\x00\x00") # Padding
|
|
time.sleep(0.5)
|
|
|
|
# Wait for OK
|
|
dis_ok = wait_for("OK", 5)
|
|
print(f"[RECV] DIS sent: {dis_ok.strip()}")
|
|
|
|
print("[RECV] Waiting for DCS...")
|
|
drain()
|
|
os.write(fd, b"AT+FRH=3\r")
|
|
dcs_result = wait_for("OK", 10)
|
|
print(f"[RECV] DCS received: {len(dcs_result)} bytes")
|
|
|
|
print("[RECV] Waiting for TCF...")
|
|
drain()
|
|
os.write(fd, b"AT+FRM=96\r") # Receive at 14400 bps
|
|
tcf_result = wait_for("CONNECT", 5)
|
|
print(f"[RECV] TCF: {tcf_result.strip()}")
|
|
|
|
# Send CFR
|
|
print("[RECV] Sending CFR...")
|
|
drain()
|
|
os.write(fd, b"AT+FTH=3\r")
|
|
wait_for("CONNECT", 5)
|
|
cfr_data = bytes([0xFF, 0x13, 0x21]) # CFR frame
|
|
os.write(fd, cfr_data)
|
|
os.write(fd, b"\x00\x00\x00")
|
|
time.sleep(0.5)
|
|
wait_for("OK", 5)
|
|
print("[RECV] CFR sent")
|
|
|
|
print("[RECV] Waiting for page data...")
|
|
# Receive page
|
|
drain()
|
|
os.write(fd, b"AT+FRM=96\r")
|
|
page_result = wait_for("OK", 60)
|
|
print(f"[RECV] Page received!")
|
|
|
|
# Send MCF
|
|
print("[RECV] Sending MCF...")
|
|
drain()
|
|
os.write(fd, b"AT+FTH=3\r")
|
|
wait_for("CONNECT", 5)
|
|
mcf_data = bytes([0xFF, 0x13, 0x31]) # MCF frame
|
|
os.write(fd, mcf_data)
|
|
os.write(fd, b"\x00\x00\x00")
|
|
time.sleep(0.5)
|
|
wait_for("OK", 5)
|
|
print("[RECV] MCF sent")
|
|
|
|
# Wait for DCN or hangup
|
|
print("[RECV] Waiting for DCN...")
|
|
drain()
|
|
os.write(fd, b"AT+FRH=3\r")
|
|
dcn_result = wait_for("OK", 10)
|
|
print(f"[RECV] Session ended: {dcn_result[:50]}")
|
|
|
|
os.close(fd)
|
|
print("[RECV] Done!")
|
|
|
|
if __name__ == "__main__":
|
|
main() |