Compare commits

..

4 Commits

Author SHA1 Message Date
5fdbcb7468 Better error handling 2026-03-07 14:39:19 +00:00
6e47e10a44 add gitignore 2026-03-07 14:39:09 +00:00
573d8574a2 add more subcommands 2026-03-07 14:38:53 +00:00
8e69681fc3 Update to include subcommands 2026-03-07 14:38:38 +00:00
4 changed files with 137 additions and 34 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
img/
img_fullwidth/
junk/
__pycache__/
*.pyc
*.pyo
.env

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
# Receipt Maker
Prints random images from 4chan's /bant/ board to an Epson TM-T20II thermal receipt printer over Ethernet.
## How it works
1. `connect_recipt.sh` — configures the network interface and connects to the printer at `192.168.192.168:9100`
2. `receipt.py` — CLI with subcommands to print random /bant/ images, text notes, or local images (resized to full printer width, 576px)
## Requirements
- Python 3
- `python-escpos`
- `Pillow`
- `requests`
- `nmap` (for connection script)
- Epson TM-T20II connected via USB-to-Ethernet adapter
## Usage
```bash
# Print a random image from /bant/
python receipt.py image
# Print a text note
python receipt.py note "your message here"
# Print a local image (with optional caption)
python receipt.py print path/to/image.jpg
python receipt.py print path/to/image.jpg "optional caption"
```
The script will run `connect_recipt.sh` automatically to bring up the network interface before printing.
## Files
- `receipt.py` — main script
- `connect_recipt.sh` — network setup for USB Ethernet adapter
- `img/` — downloaded images cache
- `img_fullwidth/` — resized images ready for printing

View File

@@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
set -e
# Try to find the USB Ethernet interface first # Try to find the USB Ethernet interface first
iface=$(ip -o link | awk -F': ' '/enx|enp.*u/{print $2}' | head -n 1) iface=$(ip -o link | awk -F': ' '/enx|enp.*u/{print $2}' | head -n 1)
@@ -16,14 +17,17 @@ else
echo "Using USB Ethernet interface: $iface" echo "Using USB Ethernet interface: $iface"
fi fi
# Assign IP address (only for USB interface, skip eth0) # Bring interface up before assigning IP
# if [[ $iface != "eth0" ]]; then
sudo ip addr add 192.168.192.10/24 dev "$iface" 2>/dev/null
# fi
# Bring interface up
sudo ip link set "$iface" up sudo ip link set "$iface" up
# Scan port 9100 on the printer IP # Assign IP address if not already set
nmap -p 9100 192.168.192.168 if ! ip addr show dev "$iface" | grep -q "192.168.192.10"; then
sudo ip addr add 192.168.192.10/24 dev "$iface"
fi
# Scan port 9100 on the printer IP and verify it is open
if ! nmap -p 9100 --open 192.168.192.168 | grep -q "9100/tcp open"; then
echo "Printer port 9100 is not open."
exit 1
fi

View File

@@ -1,28 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from escpos import * from escpos import printer
from PIL import Image from PIL import Image
import os import os
import subprocess import subprocess
import time
import random import random
import requests import requests
from urllib.parse import quote
# Return T20II printer # Return T20II printer
def get_T20II_usb(): def get_T20II_usb():
p = printer.Usb(0x04b8, 0x0202) #, profile="TM-T20II") p = printer.Usb(0x04B8, 0x0202) # , profile="TM-T20II")
# p.set_with_default(align='center', font='a', bold=False, width=2, height=2, custom_size=True, smooth=True) # p.set_with_default(align='center', font='a', bold=False, width=2, height=2, custom_size=True, smooth=True)
return p return p
def get_T20II_ethernet(): def get_T20II_ethernet():
p = printer.Network("192.168.192.168", port=9100) # , profile="TM-T20II") p = printer.Network("192.168.192.168", port=9100) # , profile="TM-T20II")
# p.set_with_default(align='center', font='a', bold=False, width=2, height=2, custom_size=True, smooth=True) # p.set_with_default(align='center', font='a', bold=False, width=2, height=2, custom_size=True, smooth=True)
return p return p
def resize_image_to_fullwidth(img_src_name, target_width=576): def resize_image_to_fullwidth(img_src_name, target_width=576):
# Make fullwidth dir # Make fullwidth dir
os.makedirs('img_fullwidth', exist_ok=True) os.makedirs("img_fullwidth", exist_ok=True)
# Open the image # Open the image
img = Image.open(img_src_name) img = Image.open(img_src_name)
@@ -38,7 +39,7 @@ def resize_image_to_fullwidth(img_src_name, target_width=576):
resized_img = img.resize((target_width, new_height), Image.LANCZOS) resized_img = img.resize((target_width, new_height), Image.LANCZOS)
# Save the resized image # Save the resized image
name, ext = os.path.splitext(img_src_name) _, ext = os.path.splitext(img_src_name)
filename_no_ext = os.path.splitext(os.path.basename(img_src_name))[0] filename_no_ext = os.path.splitext(os.path.basename(img_src_name))[0]
# Build destination filename # Build destination filename
@@ -49,7 +50,7 @@ def resize_image_to_fullwidth(img_src_name, target_width=576):
def get_random_bant_image(): def get_random_bant_image():
# Create img directory if it doesn't exist # Create img directory if it doesn't exist
os.makedirs('img', exist_ok=True) os.makedirs("img", exist_ok=True)
# Fetch the catalog JSON for /bant/ # Fetch the catalog JSON for /bant/
url = "https://a.4cdn.org/bant/catalog.json" url = "https://a.4cdn.org/bant/catalog.json"
@@ -58,12 +59,15 @@ def get_random_bant_image():
catalog = response.json() catalog = response.json()
# Flatten the list of threads from all pages # Flatten the list of threads from all pages
threads = [thread for page in catalog for thread in page['threads']] threads = [thread for page in catalog for thread in page["threads"]]
# Filter threads that have images (tim and ext fields) # Filter threads that have images (tim and ext fields)
image_threads = [ image_threads = [
thread for thread in threads thread
if 'tim' in thread and 'ext' in thread and thread['ext'].lower() not in ['.gif', '.mp4', '.webm'] for thread in threads
if "tim" in thread
and "ext" in thread
and thread["ext"].lower() not in [".gif", ".mp4", ".webm"]
] ]
if not image_threads: if not image_threads:
@@ -80,31 +84,79 @@ def get_random_bant_image():
img_response = requests.get(image_url) img_response = requests.get(image_url)
img_response.raise_for_status() img_response.raise_for_status()
with open(filename, 'wb') as f: with open(filename, "wb") as f:
f.write(img_response.content) f.write(img_response.content)
return filename return filename
def print_random_bant_image(p, sh_file): def print_random_bant_image(p, sh_file):
img = get_random_bant_image() img = get_random_bant_image()
if img is None:
print("No images found on /bant/.")
return
filename_no_ext = os.path.splitext(os.path.basename(img))[0] filename_no_ext = os.path.splitext(os.path.basename(img))[0]
p.textln(filename_no_ext) p.textln(filename_no_ext)
fimg = resize_image_to_fullwidth(img) fimg = resize_image_to_fullwidth(img)
try: try:
p.image(fimg, impl='bitImageColumn'); p.image(fimg, impl="bitImageColumn")
except: except Exception as e:
print(f"Print failed, reconnecting: {e}")
subprocess.run(["bash", sh_file], check=True) subprocess.run(["bash", sh_file], check=True)
p = get_T20II_ethernet() p = get_T20II_ethernet()
p.cut() p.cut()
def print_note(p, text):
p.textln(text)
p.cut()
def print_local_image(p, sh_file, img_path, message=None):
if not os.path.isfile(img_path):
print(f"File not found: {img_path}")
return
if message:
p.textln(message)
fimg = resize_image_to_fullwidth(img_path)
try:
p.image(fimg, impl="bitImageColumn")
except Exception as e:
print(f"Print failed, reconnecting: {e}")
subprocess.run(["bash", sh_file], check=True)
p = get_T20II_ethernet()
p.cut()
def main(): def main():
import argparse
parser = argparse.ArgumentParser(description="Receipt printer CLI")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("image", help="Print a random /bant/ image")
note_p = subparsers.add_parser("note", help="Print a text note")
note_p.add_argument("text", help="The note to print")
print_p = subparsers.add_parser("print", help="Print a local image")
print_p.add_argument("path", help="Path to the image file")
print_p.add_argument("message", nargs="?", default=None, help="Optional caption")
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
sh_file = os.path.join(script_dir, "connect_recipt.sh") sh_file = os.path.join(script_dir, "connect_recipt.sh")
subprocess.run(["bash", sh_file], check=True) subprocess.run(["bash", sh_file], check=True)
p = get_T20II_ethernet() p = get_T20II_ethernet()
if args.command == "image":
print_random_bant_image(p, sh_file) print_random_bant_image(p, sh_file)
return elif args.command == "note":
print_note(p, args.text)
elif args.command == "print":
print_local_image(p, sh_file, args.path, args.message)
if __name__ == "__main__": if __name__ == "__main__":
main() main()