Skill: yardfarmer/broder-printer (version 5.0.0)

--- name: broder-printer description: Brother DCP-T426W network printer skill — IPP + driverless (primary), TCP direct (fallback for text-only, no sudo). version: 5.0.0 --- # Brother DCP-T426W Printer Skill ## When to Use - When you need to print text, images, or PDFs to a Brother DCP-T426W from a CLI or script - When you're on a NAS, Linux, or macOS machine with CUPS and want driverless IPP printing - When you need a text-only fallback on a headless or no-root system (TCP direct, no CUPS) - When you want to check printer status or print a test page programmatically ## How It Works Printing to a Brother DCP-T426W inkjet printer via two methods: 1. **IPP + driverless PPD (primary)** — Full support: text, images, PDFs. Requires CUPS with `driverless`. 2. **TCP direct (fallback)** — Text only, no CUPS, no sudo required. **Supported platforms:** Any device with CUPS + `driverless` (NAS, Linux, macOS) or Python 3 with TCP access (fallback). ## Printer Info | Field | Value | |-------|-------| | Model | Brother DCP-T426W | | IP | 192.168.50.232 | | Protocol | IPP (`ipp://192.168.50.232:631/ipp/print`) — primary; TCP 9100 fallback | | Type | Color Inkjet MFP (Print / Scan / Copy) | | Max Resolution | 4800 x 1200 dpi | | Paper | A4, Letter, A5, A6, Photo (10x15cm), Envelopes | ## Quick Start ```bash # Print a text string (CUPS IPP) python scripts/print.py --text "Hello World" # Print a file (text, image, PDF — CUPS handles conversion) python scripts/print.py --file document.txt python scripts/print.py --file photo.jpg python scripts/print.py --file report.pdf # Print a receipt-style text python scripts/print.py --text "Invoice #123\nTotal: $42.00" --mode receipt # Print a test page python scripts/print.py --test # Check printer status python scripts/print.py --status # Fallback: TCP direct (text only, no CUPS needed) python scripts/print.py --text "Hello" --method tcp ``` ## Architecture ``` broder-printer/ ├── SKILL.md # This file ├── CLAUDE.md # Project documentation ├── Brother_DCP-T426W/ # Brother driver files (backup only) │ ├── drivers/ │ │ ├── Brother_DCP-T426W.ppd │ │ └── Brother DCP-T426W CUPS.gz │ └── readme.md ├── requirements.txt └── scripts/ └── print.py # Main print CLI (IPP/CUPS primary, TCP fallback) ``` ## Setup (One-Time) ### Step 1: Install CUPS + driverless ```bash # Linux (Debian/Ubuntu) sudo apt install cups cups-filters ipp-usb sudo systemctl enable --now cups # macOS — CUPS is built-in, skip this step ``` ### Step 2: Generate driverless PPD and add printer ```bash # Generate PPD from printer's IPP capabilities driverless "ipp://192.168.50.232:631/ipp/print" > ~/brother.ppd # Add printer to CUPS sudo lpadmin -p Brother_DCP-T426W \ -v "ipp://192.168.50.232:631/ipp/print" \ -E \ -P ~/brother.ppd sudo lpadmin -p Brother_DCP-T426W -o printer-is-shared=false # Verify lpstat -p Brother_DCP-T426W -l ``` ### Step 3: Test ```bash echo "Hello" | lp -d Brother_DCP-T426W ``` ## How It Works **IPP + driverless (default):** 1. Script calls CUPS `lp` command 2. CUPS converts input (text/image/PDF) to PWG-raster using the driverless PPD 3. Data sent to printer via IPP protocol 4. Printer renders and prints **TCP direct (fallback, `--method tcp`):** 1. Script opens TCP socket to printer port 9100 2. Wraps text in PJL (Printer Job Language) commands 3. Sends data directly to printer 4. Text only — images/PDFs need CUPS conversion ## IPP vs TCP | Feature | IPP + driverless | TCP Direct | |---------|-----------------|------------| | Setup | CUPS + `driverless` | None | | sudo required | Setup only (once) | No | | Text printing | Yes | Yes | | Image printing | Yes (PWG-raster) | No (raw bytes only) | | PDF printing | Yes | No | | Print queue | Yes | No | | Use case | NAS, desktop, full features | Headless, no-root, text-only | ## Direct CUPS Commands ```bash # Print any file CUPS understands lp -d Brother_DCP-T426W file.txt # Print with options lp -d Brother_DCP-T426W -o media=A4 -o sides=one-sided document.pdf # Check jobs lpstat -o Brother_DCP-T426W # Cancel jobs cancel Brother_DCP-T426W ``` ## Troubleshooting | Issue | Solution | |-------|----------| | `driverless: command not found` | `sudo apt install cups-filters ipp-usb` | | IPP connection refused | Verify printer supports IPP: `driverless` (list available IPP printers) | | No paper output | Check paper tray, ink levels, printer not in error state | | TCP prints garbled text | Printer may not support PJL PLAINTEXT — use IPP method | | CUPS printer not found | `lpstat -p` to list printers; re-run lpadmin setup | | `lp` command not found | `sudo apt install cups-client` |

path: CHANGELOG.md
content: # Changelog

## 5.0.0 - 2026-04-12

- Initial ClawHub release
- IPP + driverless PPD (primary): text, images, PDFs
- TCP direct fallback (text only, no CUPS required)
- Receipt mode for formatted text output
- Printer status checking
- Support for Linux, macOS, and NAS environments


path: CLAUDE.md
content: # Brother DCP-T426W Printer Project

## Overview

Printing skill for the **Brother DCP-T426W** inkjet printer. Primary method uses CUPS + IPP driverless for full format support. TCP direct available as text-only fallback.

**Supported platforms:** Any device with CUPS + `driverless` (NAS, Linux, macOS) or Python 3 with TCP access (fallback).

## Printer Details

| Field | Value |
|-------|-------|
| Model | Brother DCP-T426W |
| IP Address | 192.168.50.232 |
| Protocol | IPP (`ipp://192.168.50.232:631/ipp/print`) — primary; TCP 9100 fallback |
| Type | Color Inkjet MFP |

## Quick Commands

```bash
# Print text (IPP/CUPS, default)
python scripts/print.py --text "Hello World"

# Print file (text, image, PDF)
python scripts/print.py --file document.txt
python scripts/print.py --file photo.jpg
python scripts/print.py --file report.pdf

# Print text with receipt formatting
python scripts/print.py --text "Invoice #123" --mode receipt

# Print test page
python scripts/print.py --test

# Check printer status
python scripts/print.py --status

# Use TCP fallback (text only, no CUPS)
python scripts/print.py --file document.txt --method tcp
```

## Architecture

- `scripts/print.py` — Main CLI entrypoint (IPP/CUPS primary, TCP fallback)

## Print Methods

### IPP + driverless (default)

1. Calls CUPS `lp` command
2. CUPS converts input to PWG-raster using the driverless PPD
3. Data sent to printer via IPP protocol
4. Full support for text, images, PDFs

### TCP direct (fallback, `--method tcp`)

1. Opens TCP socket to printer at 192.168.50.232:9100
2. Wraps text in PJL (Printer Job Language) commands
3. Sends data directly to printer
4. Text only — images/PDFs need CUPS conversion

## Key Technical Notes

### CUPS Setup (one-time)

```bash
# Install CUPS + driverless
sudo apt install cups cups-filters ipp-usb
sudo systemctl enable --now cups

# Generate PPD and add printer
driverless "ipp://192.168.50.232:631/ipp/print" > ~/brother.ppd
sudo lpadmin -p Brother_DCP-T426W \
  -v "ipp://192.168.50.232:631/ipp/print" \
  -E \
  -P ~/brother.ppd
```

### PJL Protocol (TCP fallback)

The TCP direct method uses PJL (Printer Job Language) to wrap text data:
- `\x1b%-12345X` — Universal Exit Language (UEL)
- `@PJL ENTER LANGUAGE = PLAINTEXT` — Switch to plain text mode
- `\x0c` — Form feed (eject page)

### Platform Differences

| Concern | macOS | Linux |
|---------|-------|-------|
| CUPS service | `launchctl` | `systemctl` |
| PPD path | `/Library/Printers/PPDs/Contents/Resources/` | `/usr/share/ppd/` or `~/brother.ppd` |
| Driver install | `.dmg` / `.pkg` | `driverless` (IPP) or `.deb` / `.rpm` |
| `lp` / `lpstat` | Same commands | Same commands |

### PDL Limitations

The DCP-T426W is a **consumer inkjet** — it does NOT support:
- Native PostScript
- Native PCL
- Direct PDF printing (without CUPS conversion)
- IPP Everywhere / AirPrint

CUPS driverless handles all format conversion to PWG-raster.

## Troubleshooting

- **`driverless: command not found`**: `sudo apt install cups-filters ipp-usb`
- **IPP connection refused**: Verify printer supports IPP: `driverless` (list available printers)
- **No paper output**: Check paper tray, ink levels, printer not in error state
- **TCP prints garbled text**: Use IPP method — PJL PLAINTEXT may not be fully supported
- **CUPS printer not found**: Run `lpstat -p`; re-run lpadmin setup


path: requirements.txt
content: # No external dependencies required — uses Python stdlib (socket, argparse, subprocess)
# Python 3.7+ required
# CUPS + driverless recommended for full image/PDF support


path: scripts/print.py
content: """
Brother DCP-T426W Printer — Main Print CLI

Primary: CUPS + IPP driverless (full support: text, images, PDFs)
Fallback: TCP direct printing (text only, no CUPS required)

Usage:
    python scripts/print.py --text "Hello World"
    python scripts/print.py --file document.txt
    python scripts/print.py --file photo.jpg
    python scripts/print.py --status
    python scripts/print.py --test
"""

import argparse
import os
import socket
import subprocess
import sys
from datetime import datetime

PRINTER_IP = "192.168.50.232"
PRINTER_PORT = 9100
PRINTER_NAME = "Brother_DCP-T426W"
SOCKET_TIMEOUT = 10  # seconds


def main():
    parser = argparse.ArgumentParser(
        description="Print to Brother DCP-T426W (IPP/CUPS primary, TCP fallback)"
    )
    parser.add_argument("--text", "-t", help="Text string to print")
    parser.add_argument("--file", "-f", help="File to print")
    parser.add_argument(
        "--mode",
        choices=["normal", "receipt"],
        default="normal",
        help="Print formatting mode",
    )
    parser.add_argument("--status", "-s", action="store_true", help="Check printer status")
    parser.add_argument("--test", action="store_true", help="Print a test page")
    parser.add_argument(
        "--method",
        choices=["tcp", "cups"],
        default="cups",
        help="Print method (default: cups/IPP)",
    )

    args = parser.parse_args()

    if args.status:
        check_printer_status()
        return

    if args.test:
        if args.method == "cups":
            print_test_page_cups()
        else:
            print_test_page_tcp()
        return

    if not args.text and not args.file:
        parser.print_help()
        sys.exit(1)

    if args.method == "tcp":
        if args.text:
            print_text_tcp(args.text, args.mode)
        elif args.file:
            print_file_tcp(args.file)
    else:
        if args.text:
            print_text_cups(args.text, args.mode)
        elif args.file:
            print_file_cups(args.file)


# ─── CUPS/IPP Printing (primary, full support) ─────────────────────────────


def send_to_printer(data: bytes):
    """Send raw bytes to printer via TCP socket port 9100."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(SOCKET_TIMEOUT)
    try:
        sock.connect((PRINTER_IP, PRINTER_PORT))
        sock.sendall(data)
        sock.shutdown(socket.SHUT_WR)
        # Read printer response (status feedback)
        sock.settimeout(3)
        try:
            response = sock.recv(1024)
            if response:
                return response
        except socket.timeout:
            pass
    except ConnectionRefusedError:
        print(f"Error: Printer {PRINTER_IP}:{PRINTER_PORT} refused connection.")
        print("Check: printer is on, connected to WiFi, correct IP address.")
        sys.exit(1)
    except socket.timeout:
        print(f"Error: Connection to {PRINTER_IP}:{PRINTER_PORT} timed out.")
        sys.exit(1)
    finally:
        sock.close()
    return b""


def print_text_tcp(text: str, mode: str):
    """Print text via TCP direct."""
    if mode == "receipt":
        text = format_receipt(text)

    # PJL + plain text: most Brother printers accept this on port 9100
    # PJL header enters printer language mode, then we send text
    pjl_header = b"\x1b%-12345X@PJL\n@PJL ENTER LANGUAGE = PLAINTEXT\n"
    pjl_footer = b"\x1b%-12345X\n"
    form_feed = b"\x0c"  # Form feed to eject page

    data = pjl_header + text.encode("utf-8") + b"\n" + form_feed + pjl_footer
    send_to_printer(data)
    print("Print job sent via TCP. Check printer for output.")


def print_file_tcp(filepath: str):
    """Print a file via TCP direct. Text files sent as-is, binary files sent raw."""
    if not os.path.exists(filepath):
        print(f"Error: File not found: {filepath}")
        sys.exit(1)

    abs_path = os.path.abspath(filepath)
    ext = os.path.splitext(filepath)[1].lower()

    with open(abs_path, "rb") as f:
        data = f.read()

    if ext in (".txt", ".csv", ".log", ".md", ".html"):
        # Text files: wrap in PJL for proper handling
        pjl_header = b"\x1b%-12345X@PJL\n@PJL ENTER LANGUAGE = PLAINTEXT\n"
        pjl_footer = b"\x1b%-12345X\n"
        form_feed = b"\x0c"
        data = pjl_header + data + b"\n" + form_feed + pjl_footer
    elif ext in (".jpg", ".jpeg", ".png", ".gif", ".bmp"):
        print(
            f"Warning: Image files ({ext}) need raster conversion.\n"
            f"TCP direct sends raw bytes which the printer may not interpret.\n"
            f"Use --method cups for image printing, or convert to PDF first."
        )
        print("Sending raw data anyway...")

    send_to_printer(data)
    print(f"File '{filepath}' sent via TCP. Check printer for output.")


def print_test_page_tcp():
    """Print a test page via TCP."""
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    text = (
        f"=== Brother DCP-T426W Test Page ===\n"
        f"Printer IP: {PRINTER_IP}\n"
        f"Print Method: TCP Direct (port {PRINTER_PORT})\n"
        f"Date: {now}\n"
        f"Status: OK\n"
        f"=================================\n"
    )
    print_text_tcp(text, "normal")


# ─── TCP Direct Printing (fallback, text only) ─────────────────────────────


def run_cups(args: list[str]) -> subprocess.CompletedProcess:
    """Run a CUPS command and handle errors."""
    result = subprocess.run(args, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"CUPS Error: {result.stderr.strip()}")
        sys.exit(1)
    return result


def print_text_cups(text: str, mode: str):
    """Print text via CUPS."""
    if mode == "receipt":
        text = format_receipt(text)

    process = subprocess.Popen(
        ["lp", "-d", PRINTER_NAME, "-"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    stdout, stderr = process.communicate(input=text.encode("utf-8"))
    if process.returncode != 0:
        print(f"CUPS Error: {stderr.decode().strip()}")
        sys.exit(1)
    print(stdout.decode().strip())


def print_file_cups(filepath: str):
    """Print a file via CUPS."""
    if not os.path.exists(filepath):
        print(f"Error: File not found: {filepath}")
        sys.exit(1)

    abs_path = os.path.abspath(filepath)
    result = subprocess.run(
        ["lp", "-d", PRINTER_NAME, abs_path],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        print(f"CUPS Error: {result.stderr.strip()}")
        sys.exit(1)
    print(result.stdout.strip())


def print_test_page_cups():
    """Print a test page via CUPS."""
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    text = (
        f"=== Brother DCP-T426W Test Page ===\n"
        f"Printer: {PRINTER_NAME}\n"
        f"IP: {PRINTER_IP}\n"
        f"Date: {now}\n"
        f"Status: OK\n"
    )
    print_text_cups(text, "normal")


# ─── Shared ────────────────────────────────────────────────────────────────


def format_receipt(text: str, width: int = 40) -> str:
    """Format text in a receipt style."""
    lines = text.strip().split("\n")
    border = "=" * width

    parts = [border, ""]
    for i, line in enumerate(lines):
        if i == 0:
            parts.append(line.center(width))
            parts.append("")
        else:
            parts.append(line)
    parts.append("")
    parts.append(border)
    return "\n".join(parts)


def check_printer_status():
    """Check printer status via TCP + CUPS (if available)."""
    # TCP connectivity (always available)
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(5)
        sock.connect((PRINTER_IP, PRINTER_PORT))
        sock.close()
        tcp_status = "Online (TCP reachable)"
    except Exception as e:
        tcp_status = f"Offline ({e})"

    print(f"Printer: {PRINTER_NAME}")
    print(f"IP: {PRINTER_IP}:{PRINTER_PORT}")
    print(f"Network: {tcp_status}")

    # CUPS status (optional, if available)
    try:
        result = subprocess.run(
            ["lpstat", "-p", PRINTER_NAME],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            print(f"CUPS: {result.stdout.strip()}")
            result = subprocess.run(
                ["lpstat", "-v", PRINTER_NAME],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.stdout:
                print(f"Device: {result.stdout.strip()}")
        else:
            print("CUPS: printer not configured (TCP direct still works)")
    except FileNotFoundError:
        print("CUPS: not installed (TCP direct still works)")
    except subprocess.TimeoutExpired:
        pass


if __name__ == "__main__":
    main()


path: skills/sonoscli/_meta.json
content: ownerId: kn70pywhg0fyz996kpa8xj89s57yhv26
slug: sonoscli
version: 1.0.0
publishedAt: 1767545381030

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)