From dcd749d11269b9abf2065646e279465837c4589c Mon Sep 17 00:00:00 2001 From: "github.com/ib-bsb-br/ib-bsb-br.github.io" Date: Wed, 29 Apr 2026 15:01:09 -0300 Subject: [PATCH] Implement Python 3.13 multipart upload backend and inotify watcher merge --- README.md | 31 +- scripts/setup_hl_l1222_android_print.sh | 690 ++++++++++++++++++++++++ 2 files changed, 719 insertions(+), 2 deletions(-) create mode 100755 scripts/setup_hl_l1222_android_print.sh diff --git a/README.md b/README.md index 6cde027..588c917 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ -```codex_answers - +# Brother HL-L1222-V Android Print Server (Raspberry Pi 4B+ / openSUSE Tumbleweed) + +This repository now provides an executable setup script that configures a Raspberry Pi 4B+ (openSUSE Tumbleweed) as a print server for a USB-connected Brother HL-L1222/HL-L1222-V. + +## What this enables +- Android phones on the **same Wi-Fi network** can print through IPP (Mopria-compatible workflow). +- USB Brother printer is shared by CUPS over the local network. +- Firewall and discovery services are configured for LAN printing. + +## Important Bluetooth note +Android-to-Raspberry-Pi Bluetooth printing into CUPS is generally not reliable/supported in modern Android/CUPS workflows. The script still enables Bluetooth service for optional experimentation, but the production path is **Wi-Fi IPP**. + +## Script +- `scripts/setup_hl_l1222_android_print.sh` + +## Run +```bash +sudo bash scripts/setup_hl_l1222_android_print.sh ``` + +## After script completes +1. Keep printer connected via USB to Raspberry Pi. +2. Connect Android phone to same Wi-Fi as Raspberry Pi. +3. Enable Mopria Print Service (or equivalent Android print service). +4. Add printer with: + - `ipp:///printers/Brother_HL_L1222_V` + +## Notes +- Script uses CUPS `everywhere` model as default for broad compatibility. +- If you have an official Brother HL-L1222-V Linux PPD/driver package, install it and adjust queue model if desired. 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" <