Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ $ pip install -r requirements.txt
# Usage
```bash
$ ./print.py --help
usage: print.py [-h] [-l {debug,info,warn,error}] [-b {mean-threshold,floyd-steinberg,atkinson,halftone,none}] [-s] [-d DEVICE] [-e ENERGY]
usage: print.py [-h] [-l {debug,info,warn,error}] [-b {mean-threshold,floyd-steinberg,atkinson,halftone,none}] [-s] [-d DEVICE] [-e ENERGY] [--no-resize]
filename

prints an image on your cat thermal printer
Expand All @@ -40,6 +40,33 @@ options:
services.
-e ENERGY, --energy ENERGY
Thermal energy. Between 0x0000 (light) and 0xffff (darker, default).
--no-resize Disable automatic image resizing. Small images print as-is,
large images scale to width without changing height.
```

# New Features

## Image Resize Control

The `--no-resize` option provides more control over image printing:

- By default, images are resized to match printer width
- With `--no-resize`, small images print at original size
- Large images are scaled to width while maintaining aspect ratio

Example:

```bash
# Print without automatic resizing
./print.py --no-resize test.png
```

## Printer Keep-Awake
A keep-awake mechanism can be configured in the manager project to prevent printer sleep mode. This is typically managed externally and can be activated when running the server.

```bash
# Managed by parent project
node server.js --keep-awake
Comment thread
sofiaferro marked this conversation as resolved.
```

# Example
Expand Down
4 changes: 2 additions & 2 deletions catprinter/ble.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
SCAN_TIMEOUT_S = 10

# Wait time after sending each chunk of data through BLE.
WAIT_AFTER_EACH_CHUNK_S = 0.02
WAIT_AFTER_EACH_CHUNK_S = 0.2

# Wait for printer done event timeout.
WAIT_FOR_PRINTER_DONE_TIMEOUT = 30
WAIT_FOR_PRINTER_DONE_TIMEOUT = 60


async def scan(name: Optional[str], timeout: int):
Expand Down
26 changes: 18 additions & 8 deletions catprinter/img.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,29 @@ def read_img(
filename,
print_width,
img_binarization_algo,
no_resize=False
):

im = cv2.imread(filename, cv2.IMREAD_GRAYSCALE)
height = im.shape[0]
width = im.shape[1]
factor = print_width / width
resized = cv2.resize(
im,
(
print_width,
int(height * factor)
),
interpolation=cv2.INTER_AREA)

if no_resize and width > print_width:
# With no_resize: only resize if image width exceeds print width
resized = cv2.resize(
im,
(print_width, int(height * width / print_width)),
interpolation=cv2.INTER_AREA
)
else:
# Default behavior: resize to fit print width while maintaining aspect ratio
factor = print_width / width
resized = cv2.resize(
im,
(print_width, int(height * factor)),
interpolation=cv2.INTER_AREA
)

if img_binarization_algo == 'atkinson':
logger.info('⏳ Applying Atkinson dithering to image...')
resized = atkinson_dither(resized)
Expand Down
64 changes: 64 additions & 0 deletions dummy-print.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import asyncio
Copy link
Copy Markdown
Owner

@rbaron rbaron Jan 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dummy-print.py shares a lot of code with print.py. I would suggest adding a --send-keepalive to print.py instead. It will make maintenance easier.

Maybe we can also mention it in the README.md instead of the nodejs application there. Something like "To prevent the printer from sleeping, call print.py --send-keepalive every x seconds". I think that would be helpful to others.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that dummy-print.py shares a lot of code with ble.py. The keep-alive functionality currently requires a parent process to keep the printer awake between prints, which isn't something handled directly by print.py or ble.py.

Alternatively, we could move this functionality to ble.py to avoid duplication, or let dummy-print.py exist in a fork if it's too specific for the main branch. This way, the main codebase stays clean while supporting this use case. Let me know your thoughts!

Copy link
Copy Markdown
Owner

@rbaron rbaron Jan 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about something like adding a --send-keepalive "dummy switch" to print.py that only pings the printer with the functionality you already implemented. I think this would be great already.

Then users can choose if/how to periodically run the keepalive loop. For example, some may be happy with a quick and dirty bash loop like:

$ while true; do python3 print.py --send-keep-alive; sleep 30; done

So I think we could get rid of dummy-print.py and use the same print.py with an additional if-else send_keepalive. In your manager, I think it would be enough to change this line to print.py --send-keepalive here.

I would still link your catprinter-manager in the README because it has nice features for advanced users (queue, better keepalive etc), and we get to keep this one very simple. What do you think?

import contextlib
from typing import Optional
import uuid
from bleak import BleakClient, BleakScanner
from bleak.backends.device import BLEDevice
from catprinter import logger

POSSIBLE_SERVICE_UUIDS = [
"0000ae30-0000-1000-8000-00805f9b34fb",
"0000af30-0000-1000-8000-00805f9b34fb",
]

TX_CHARACTERISTIC_UUID = "0000ae01-0000-1000-8000-00805f9b34fb"
RX_CHARACTERISTIC_UUID = "0000ae02-0000-1000-8000-00805f9b34fb"

PRINTER_READY_NOTIFICATION = b"\x51\x78\xae\x01\x01\x00\x00\x00\xff"

SCAN_TIMEOUT_S = 10
WAIT_AFTER_EACH_CHUNK_S = 0.2
WAIT_FOR_PRINTER_DONE_TIMEOUT = 60

# Scan and connect to the printer
async def scan(name: Optional[str], timeout: int):
autodiscover = not name
if autodiscover:
logger.info("⏳ Trying to auto-discover a printer...")
else:
logger.info(f"⏳ Looking for a BLE device named {name}...")
# Filter function for devices
def filter_fn(device: BLEDevice, adv_data):
if autodiscover:
return any(uuid in adv_data.service_uuids for uuid in POSSIBLE_SERVICE_UUIDS)
else:
return device.name == name

device = await BleakScanner.find_device_by_filter(filter_fn, timeout=timeout)
if device is None:
raise RuntimeError("Unable to find printer, make sure it is turned on and in range")
logger.info(f"✅ Found printer: {device}")
return device

# Simulate a dummy print activity (ping)
async def run_dummy_print(device: Optional[str]):
try:
address = await scan(device, timeout=SCAN_TIMEOUT_S)
except RuntimeError as e:
logger.error(f"🛑 {e}")
return
logger.info(f"⏳ Connecting to {address}...")
async with BleakClient(address) as client:
logger.info(f"✅ Connected: {client.is_connected}; MTU: {client.mtu_size}")
event = asyncio.Event()

# Sending a dummy "ping" to the printer to keep it awake
logger.info("⏳ Sending dummy signal to the printer...")
await client.write_gatt_char(TX_CHARACTERISTIC_UUID, PRINTER_READY_NOTIFICATION)
await asyncio.sleep(WAIT_AFTER_EACH_CHUNK_S)

logger.info("✅ Printer activity simulated successfully. Exiting.")

# Run the dummy print to simulate the interaction with the printer
if __name__ == "__main__":
asyncio.run(run_dummy_print(None))
6 changes: 5 additions & 1 deletion print.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ def parse_args():
'The printer\'s Bluetooth Low Energy (BLE) address '
'(MAC address on Linux; UUID on macOS) '
'or advertisement name (e.g.: "GT01", "GB02", "GB03"). '
'If omitted, the the script will try to auto discover '
'If omitted, the script will try to auto discover '
'the printer based on its advertised BLE services.'
))
args.add_argument('-e', '--energy', type=lambda h: int(h.removeprefix("0x"), 16),
help="Thermal energy. Between 0x0000 (light) and 0xffff (darker, default).",
default="0xffff")
args.add_argument('--no-resize', action='store_true',
help='Disable automatic image resizing')

return args.parse_args()


Expand All @@ -64,6 +67,7 @@ def main():
args.filename,
PRINT_WIDTH,
args.img_binarization_algo,
no_resize=args.no_resize
)
if args.show_preview:
show_preview(bin_img)
Expand Down