diff --git a/README.md b/README.md index 6cde027..315a74b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,251 @@ -```codex_answers - +# iPhone Tutorial: Using the Raspberry Pi + openSUSE CUPS Brother HL-L1222 Print Server + +## Goal +Use an iPhone on the same Wi-Fi network to print through your Raspberry Pi 4B+ openSUSE CUPS server connected by USB to a Brother HL-L1222/HL-L1222-V. + +This tutorial assumes your setup script has already run successfully and created: +- CUPS queue: `impressora_brother` +- Optional upload web service: `hll1222-upload` on port `8088` +- Optional hotfolder watcher: `hll1222-hotfolder` + +--- + +## 1) Understand what works from iPhone + +### Primary iPhone path (recommended) +- **AirPrint/IPP over Wi-Fi** through CUPS shared printer. +- This is the native iOS experience from apps like Files, Safari, Photos, Mail, Notes, etc. + +### Secondary iPhone path (fallback) +- **Web upload page** at `http://:8088/` (if upload service enabled). +- Upload supported files; the Pi watcher prints them automatically. + +### Important limitation +- Your Brother model is USB/non-wireless. iPhone never talks to the printer directly. +- iPhone talks to the **Pi print server**, and the Pi sends jobs to USB printer. + +--- + +## 2) Verify server readiness on Raspberry Pi first +Run these checks on the Pi before testing iPhone: + +```bash +systemctl status cups --no-pager +systemctl status avahi-daemon --no-pager +systemctl status firewalld --no-pager +lpstat -t ``` + +If you enabled upload mode: + +```bash +systemctl status hll1222-upload --no-pager +systemctl status hll1222-hotfolder --no-pager +``` + +Check LAN IP: + +```bash +hostname -I | awk '{print $1}' +``` + +Expected: keep this IP for iPhone tests (example `192.168.1.50`). + +--- + +## 3) Confirm CUPS queue and sharing state + +Use: + +```bash +lpstat -p impressora_brother -l +lpoptions -p impressora_brother -l +``` + +Confirm queue is enabled/shared. If uncertain, enforce: + +```bash +sudo lpadmin -p impressora_brother -o printer-is-shared=true +sudo cupsctl --share-printers --remote-any +sudo systemctl restart cups +``` + +--- + +## 4) iPhone method A: Native Print (AirPrint/IPP discovery) + +1. Connect iPhone to the **same SSID/Wi-Fi** as the Raspberry Pi. +2. Open a printable item (e.g., PDF in Files, webpage in Safari, photo in Photos). +3. Tap **Share** → **Print**. +4. Tap **Printer** and wait for discovery. +5. Select printer queue shown by CUPS (commonly `impressora_brother` or host-derived name). +6. Set copies/page range/media options and tap **Print**. + +### If printer does not appear +- Wait 10–20 seconds in the printer picker. +- Ensure no client isolation on Wi-Fi router. +- Re-check Pi services and firewall. +- Retry after toggling iPhone Wi-Fi off/on. + +--- + +## 5) iPhone method B: Browser upload page fallback + +If AirPrint discovery fails or the app cannot print directly: + +1. On iPhone Safari, open: + - `http://:8088/` +2. Choose file to upload (PDF/TXT/JPG/PNG/TIFF). +3. Submit upload. +4. File lands in hotfolder and is auto-printed by watcher. + +### Useful examples +- `http://192.168.1.50:8088/` + +### File behavior +- Allowed extensions: `.pdf`, `.txt`, `.png`, `.jpg`, `.jpeg`, `.tif`, `.tiff` +- Oversized uploads are rejected by configured max bytes. + +--- + +## 6) Validate job pipeline end-to-end + +After sending from iPhone, inspect queue/jobs on Pi: + +```bash +lpstat -t +journalctl -u cups -n 100 --no-pager +``` + +For upload route: + +```bash +journalctl -u hll1222-upload -n 100 --no-pager +journalctl -u hll1222-hotfolder -n 100 --no-pager +ls -la /var/spool/hll1222-hotfolder +``` + +What success looks like: +- Job appears in CUPS queue then completes. +- Uploaded file is consumed by watcher and removed/processed. +- Physical printer starts within seconds. + +--- + +## 7) iPhone-specific troubleshooting matrix + +### Symptom: “No AirPrint Printers Found” +Check: +- Same Wi-Fi network/subnet +- `avahi-daemon` running +- Firewall has `ipp` and `mdns` + +Commands: + +```bash +sudo firewall-cmd --list-services +systemctl is-active avahi-daemon +systemctl is-active cups +``` + +Expected services include `ipp` and `mdns`. + +--- + +### Symptom: iPhone sees printer but print fails immediately +Check: + +```bash +lpstat -t +journalctl -u cups -n 200 --no-pager +``` + +Likely causes: +- Driver/model mismatch +- USB URI changed after reconnect/reboot + +Recovery: +- Re-run your setup script with explicit URI/model if needed. + +--- + +### Symptom: Upload page opens, but upload fails +Check upload services: + +```bash +systemctl status hll1222-upload --no-pager +journalctl -u hll1222-upload -n 200 --no-pager +python3 -c 'import multipart; print("ok")' +``` + +If `multipart` import fails, install dependency and restart service: + +```bash +sudo zypper --non-interactive install --no-recommends python3-python-multipart +sudo systemctl restart hll1222-upload +``` + +--- + +### Symptom: Upload accepted, nothing prints +Check watcher: + +```bash +systemctl status hll1222-hotfolder --no-pager +journalctl -u hll1222-hotfolder -n 200 --no-pager +``` + +Manual test: + +```bash +echo 'test from pi' | lp -d impressora_brother +``` + +If manual CUPS print works, issue is watcher path/permissions/file type. + +--- + +## 8) Router and network conditions that matter + +For iPhone discovery/printing reliability: +- Disable AP/client isolation on guest Wi-Fi. +- Keep Pi and iPhone in same VLAN/subnet. +- Avoid VPN on iPhone during test. +- Ensure multicast/mDNS isn’t blocked. + +--- + +## 9) Operational best practices + +- Keep a static DHCP lease for Raspberry Pi IP. +- Keep queue name stable (`impressora_brother`). +- Prefer PDF for predictable rendering. +- Do periodic status checks after updates: + +```bash +systemctl status cups avahi-daemon firewalld --no-pager +lpstat -t +``` + +--- + +## 10) Quick runbook (short version) + +1. iPhone and Pi on same Wi-Fi. +2. Print from iPhone Share → Print. +3. If printer not found, use Safari upload fallback `http://:8088/`. +4. If failure persists, check: + +```bash +systemctl status cups avahi-daemon hll1222-upload hll1222-hotfolder --no-pager +lpstat -t +journalctl -u cups -n 100 --no-pager +``` + +--- + +## 11) Security note + +Your script currently uses permissive hotfolder permissions (`0777`) to maximize compatibility for upload + watcher workflows. This is practical in trusted home LAN setups, but you should avoid exposing port `8088` beyond local network. + +If you later want a hardened variant (group-based ACL instead of `0777`), create a dedicated service user/group and restrict write access to that principal only. diff --git a/scripts/setup_hl_l1222_android_print.sh b/scripts/setup_hl_l1222_android_print.sh new file mode 100755 index 0000000..c50b2d9 --- /dev/null +++ b/scripts/setup_hl_l1222_android_print.sh @@ -0,0 +1,690 @@ +#!/bin/bash +set -euo pipefail + +# Ensure sbin paths are available when script is run with plain `bash script.sh` +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" + +# setup_hl_l1222_android_print.sh +# +# Purpose: +# Configure Raspberry Pi 4B+ (ARM64) running openSUSE Tumbleweed as a +# print server for a USB-connected Brother HL-L1222 / HL-L1222-V so Android +# devices can print over same Wi-Fi via IPP/Mopria. +# +# Execution context: +# Run as a normal user from home directory (~/). Script uses sudo for +# privileged operations. +# +# Safety: +# - Backs up /etc/cups/cupsd.conf before any modification. +# - Requires explicit confirmation before modifying CUPS config. +# - Requires explicit confirmation before deleting an existing queue +# with the same name. + +QUEUE_NAME="Brother_HL_L1222_V" +DEVICE_URI="" +MODEL_NAME="" +ENABLE_BLUETOOTH="yes" +ENABLE_UPLOAD="no" +UPLOAD_PORT="8088" +MAX_UPLOAD_BYTES="52428800" + +SCRIPT_DIR="/usr/local/lib/hll1222-print" +HOTFOLDER="/var/spool/hll1222-hotfolder" +HOTFOLDER_SERVICE="hll1222-hotfolder" +UPLOAD_SERVICE="hll1222-upload" + +TMP_DIR="" +CUPSD_CONF="/etc/cups/cupsd.conf" + +log() { echo "[INFO] $*"; } +warn() { echo "[WARN] $*" >&2; } +err() { echo "[ERROR] $*" >&2; } + + +resolve_bin() { + local name="$1" + local found + found="$(command -v "$name" 2>/dev/null || true)" + if [[ -n "$found" ]]; then + echo "$found" + return 0 + fi + for d in /usr/sbin /usr/bin /sbin /bin; do + if [[ -x "$d/$name" ]]; then + echo "$d/$name" + return 0 + fi + done + return 1 +} + +cleanup() { + if [[ -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then + rm -rf "${TMP_DIR}" + fi +} +trap cleanup EXIT SIGINT SIGTERM + +require_command() { + local cmd="$1" + local pkg="$2" + if ! command -v "$cmd" >/dev/null 2>&1; then + log "Command '$cmd' missing; installing package '$pkg'..." + if ! sudo zypper --non-interactive install --no-recommends "$pkg"; then + err "Failed to install package '$pkg' for command '$cmd'." + exit 1 + fi + fi +} + +confirm_high_risk() { + local message="$1" + read -r -p "WARNING: ${message} Type 'yes' to continue: " answer + if [[ "$answer" != "yes" ]]; then + err "Operation cancelled by user." + exit 1 + fi +} + +usage() { + cat </dev/null + fi + if ! sudo grep -qi '^Browsing[[:space:]]\+Yes' "${CUPSD_CONF}"; then + echo 'Browsing Yes' | sudo tee -a "${CUPSD_CONF}" >/dev/null + fi + if ! sudo grep -qi '^BrowseLocalProtocols[[:space:]]\+dnssd' "${CUPSD_CONF}"; then + echo 'BrowseLocalProtocols dnssd' | sudo tee -a "${CUPSD_CONF}" >/dev/null + fi + + if ! sudo systemctl restart cups.service; then + err "Failed to restart cups.service after config change." + exit 1 + fi +} + +detect_usb_uri() { + if [[ -n "$DEVICE_URI" ]]; then + return 0 + fi + + log "Detecting Brother USB URI..." + DEVICE_URI="$("$LPINFO_BIN" -v | awk 'BEGIN{IGNORECASE=1} /^direct[[:space:]]+usb:\/\// && /Brother|HL|L1222/ {print $2; exit}')" + + if [[ -z "$DEVICE_URI" ]]; then + err "Could not detect Brother USB URI." + echo "Available CUPS devices:" + "$LPINFO_BIN" -v || true + exit 1 + fi + + log "Detected URI: ${DEVICE_URI}" +} + +select_model() { + if [[ -n "$MODEL_NAME" ]]; then + return 0 + fi + + log "Selecting best available print model..." + + # 1) Prefer brlaser/Brother model lines if available. + MODEL_NAME="$("$LPINFO_BIN" -m | awk 'BEGIN{IGNORECASE=1} /brlaser/ || (/Brother/ && /HL/ && /1200|1210|1212|1222|L12/) {print $1; exit}')" + if [[ -n "$MODEL_NAME" ]]; then + log "Selected model: ${MODEL_NAME}" + return 0 + fi + + # 2) Fallback to everywhere if available. + if "$LPINFO_BIN" -m | awk '{print $1}' | grep -qx 'everywhere'; then + MODEL_NAME="everywhere" + warn "Falling back to 'everywhere' model." + return 0 + fi + + # 3) Fallback to generic mono laser. + MODEL_NAME="$("$LPINFO_BIN" -m | awk 'BEGIN{IGNORECASE=1} /Generic/ && /laser|mono|pcl/ {print $1; exit}')" + if [[ -n "$MODEL_NAME" ]]; then + warn "Using generic fallback model: ${MODEL_NAME}" + return 0 + fi + + err "No suitable model found automatically." + echo "Candidate models:" + "$LPINFO_BIN" -m | grep -Ei 'brother|hl|brlaser|everywhere|generic|laser|mono|pcl' || true + exit 1 +} + +create_or_replace_queue() { + detect_usb_uri + select_model + + if "$LPSTAT_BIN" -p "$QUEUE_NAME" >/dev/null 2>&1; then + confirm_high_risk "Queue '${QUEUE_NAME}' already exists and will be deleted/recreated." + if ! sudo "$LPADMIN_BIN" -x "$QUEUE_NAME"; then + err "Failed to remove existing queue ${QUEUE_NAME}." + exit 1 + fi + fi + + log "Creating printer queue ${QUEUE_NAME}..." + if ! sudo "$LPADMIN_BIN" -p "$QUEUE_NAME" -E -v "$DEVICE_URI" -m "$MODEL_NAME"; then + err "Failed to create queue ${QUEUE_NAME}." + exit 1 + fi + + if ! sudo "$LPADMIN_BIN" -p "$QUEUE_NAME" -o printer-is-shared=true -o media=A4 -o PageSize=A4 -o sides=one-sided; then + err "Failed to set queue defaults for ${QUEUE_NAME}." + exit 1 + fi + + if ! sudo cupsenable "$QUEUE_NAME"; then + err "Failed to enable queue ${QUEUE_NAME}." + exit 1 + fi + + if ! sudo cupsaccept "$QUEUE_NAME"; then + err "Failed to set queue ${QUEUE_NAME} to accept jobs." + exit 1 + fi + + if ! sudo lpoptions -d "$QUEUE_NAME"; then + err "Failed to set default printer to ${QUEUE_NAME}." + exit 1 + fi + + if ! sudo systemctl restart cups.service; then + err "Failed to restart CUPS after queue creation." + exit 1 + fi +} + +install_hotfolder_service() { + log "Installing hotfolder auto-print watcher (inotify-tools)..." + + if ! sudo install -d -m 0755 "$SCRIPT_DIR"; then + err "Failed to create script directory ${SCRIPT_DIR}." + exit 1 + fi + if ! sudo install -d -m 0775 -o lp -g lp "$HOTFOLDER"; then + err "Failed to create hotfolder ${HOTFOLDER}." + exit 1 + fi + # Keep folder writable for upload service and watcher workflows. + if ! sudo chmod 0777 "$HOTFOLDER"; then + err "Failed to set writable permissions on ${HOTFOLDER}." + exit 1 + fi + + TMP_DIR="$(mktemp -d)" + if ! command -v inotifywait >/dev/null 2>&1; then + if ! sudo zypper --non-interactive install --no-recommends inotify-tools; then + err "Failed to install inotify-tools for hotfolder watcher." + exit 1 + fi + fi + + local watcher_sh + watcher_sh="${TMP_DIR}/watch_print.sh" + + cat > "$watcher_sh" <<'SH' +#!/bin/bash +set -euo pipefail +QUEUE="${PRINT_QUEUE:-Brother_HL_L1222_V}" +HOTFOLDER="${PRINT_HOTFOLDER:-/var/spool/hll1222-hotfolder}" +mkdir -p "$HOTFOLDER" +inotifywait -m "$HOTFOLDER" -e close_write -e moved_to --format '%w%f' | while read -r file; do + case "${file,,}" in + *.pdf|*.jpg|*.jpeg|*.png|*.txt|*.tif|*.tiff) + lp -d "$QUEUE" -o fit-to-page "$file" || true + rm -f "$file" || true + ;; + *) + ;; + esac +done +SH + + if ! sudo install -m 0755 "$watcher_sh" "${SCRIPT_DIR}/watch_print.sh"; then + err "Failed to install hotfolder watcher script." + exit 1 + fi + + local service_file + service_file="${TMP_DIR}/${HOTFOLDER_SERVICE}.service" + cat > "$service_file" </dev/null 2>&1; then + warn "python3-python-multipart is unavailable." + warn "Skipping upload web service installation. Use IPP or hotfolder fallback." + return 0 + fi + + log "Installing optional LAN upload service on port ${UPLOAD_PORT}..." + + TMP_DIR="$(mktemp -d)" + local upload_py + upload_py="${TMP_DIR}/upload_server.py" + + cat > "$upload_py" <<'PY' +#!/usr/bin/env python3 +import html +import os +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from multipart import parse_form_data +from pathlib import Path +from urllib.parse import urlparse + +hot = Path(os.environ.get("PRINT_HOTFOLDER", "/var/spool/hll1222-hotfolder")) +queue = os.environ.get("PRINT_QUEUE", "Brother_HL_L1222_V") +maxb = int(os.environ.get("MAX_UPLOAD_BYTES", "52428800")) +allowed = {".pdf", ".txt", ".png", ".jpg", ".jpeg", ".tif", ".tiff"} + +hot.mkdir(parents=True, exist_ok=True) + +page = f"""HL-L1222 Upload

HL-L1222 Upload

Queue: {html.escape(queue)}

LAN only. Allowed: PDF/TXT/PNG/JPG/TIFF

""" + +class H(BaseHTTPRequestHandler): + def send_body(self, code, body, ctype="text/html; charset=utf-8"): + data = body.encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", ctype) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_GET(self): + if urlparse(self.path).path == "/": + self.send_body(200, page) + else: + self.send_body(404, "Not found") + + def do_POST(self): + if urlparse(self.path).path != "/upload": + self.send_body(404, "Not found") + return + ctype = self.headers.get("Content-Type", "") + if "multipart/form-data" not in ctype: + self.send_body(400, "Content-Type must be multipart/form-data") + return + clen = int(self.headers.get("Content-Length", "0")) + if clen <= 0 or clen > maxb: + self.send_body(413, "Invalid upload size") + return + env = { + "REQUEST_METHOD": "POST", + "CONTENT_TYPE": ctype, + "CONTENT_LENGTH": str(clen), + "wsgi.input": self.rfile, + } + _form, files = parse_form_data(env) + if "file" not in files: + self.send_body(400, "Missing file") + return + item = files["file"] + name = Path(getattr(item, "filename", "")).name + if not name: + self.send_body(400, "Missing filename") + return + ext = Path(name).suffix.lower() + if ext not in allowed: + self.send_body(400, f"Unsupported file type: {html.escape(ext)}") + return + safe = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in name)[:160] + dst = hot / safe + i = 1 + while dst.exists(): + dst = hot / f"{Path(safe).stem}_{i}{ext}" + i += 1 + with open(dst, "wb") as f: + f.write(item.raw) + os.chmod(dst, 0o664) + self.send_body(200, "

Uploaded successfully. File queued.

Back

") + +if __name__ == "__main__": + host = "0.0.0.0" + port = int(os.environ.get("UPLOAD_PORT", "8088")) + ThreadingHTTPServer((host, port), H).serve_forever() +PY + + if ! sudo install -d -m 0755 "$SCRIPT_DIR"; then + err "Failed to ensure ${SCRIPT_DIR}." + exit 1 + fi + + if ! sudo install -m 0755 "$upload_py" "${SCRIPT_DIR}/upload_server.py"; then + err "Failed to install upload server script." + exit 1 + fi + + local upload_unit + upload_unit="${TMP_DIR}/${UPLOAD_SERVICE}.service" + cat > "$upload_unit" <