Compare commits
4 Commits
48a7016f72
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fdbcb7468 | |||
| 6e47e10a44 | |||
| 573d8574a2 | |||
| 8e69681fc3 |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
img/
|
||||||
|
img_fullwidth/
|
||||||
|
junk/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.env
|
||||||
40
README.md
Normal file
40
README.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
80
receipt.py
80
receipt.py
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user