- printer.py: protocol with flow control (pause / resend / done) - lxd02.py: print text or pictures from the command line - windows/: POS-58 driver + local ESC/POS server = a regular Windows printer, Explorer menu for pictures - custom_components/lxd02: Home Assistant integration with a persistent queue Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""Print on the LX-D02 thermal printer from the PC.
|
||
|
||
Usage: python lxd02.py "text" [more lines...]
|
||
python lxd02.py picture.jpg [more pictures...] (stretched to the tape width)
|
||
Set LXD02_MAC=AA:BB:CC:DD:EE:FF to skip the Bluetooth search.
|
||
"""
|
||
import asyncio
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from bleak import BleakClient, BleakScanner
|
||
from PIL import Image, ImageOps
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent / "custom_components" / "lxd02"))
|
||
from printer import WIDTH, print_payloads, render, to_payloads # noqa: E402
|
||
|
||
PICTURES = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp", ".tif", ".tiff"}
|
||
|
||
|
||
def picture(path):
|
||
"""Any picture -> 1-bit image WIDTH wide. Landscape ones are turned to run
|
||
along the tape, so they come out bigger."""
|
||
img = ImageOps.exif_transpose(Image.open(path)).convert("RGBA")
|
||
white = Image.new("RGBA", img.size, "white")
|
||
img = Image.alpha_composite(white, img).convert("L") # transparent -> white, not black
|
||
if img.width > img.height:
|
||
img = img.rotate(90, expand=True)
|
||
img = img.resize((WIDTH, max(2, round(img.height * WIDTH / img.width))), Image.LANCZOS)
|
||
return ImageOps.autocontrast(img, cutoff=1).convert("1") # dithering for halftones
|
||
|
||
|
||
async def send(img, mac=None):
|
||
mac = mac or os.environ.get("LXD02_MAC")
|
||
if not mac:
|
||
device = await BleakScanner.find_device_by_name("LX-D02", timeout=20)
|
||
if device is None:
|
||
raise RuntimeError("LX-D02 not found: is it on and not connected to a phone?")
|
||
mac = device.address
|
||
for attempt in range(3): # Windows often fails the first connect to a sleeping printer
|
||
try:
|
||
client = BleakClient(mac, timeout=20)
|
||
await client.connect()
|
||
break
|
||
except Exception:
|
||
if attempt == 2:
|
||
raise
|
||
await asyncio.sleep(2)
|
||
try:
|
||
await print_payloads(client, mac, to_payloads(img))
|
||
finally:
|
||
await client.disconnect()
|
||
|
||
|
||
def main(args):
|
||
if args and all(Path(a).suffix.lower() in PICTURES and Path(a).is_file() for a in args):
|
||
for a in args:
|
||
print("printing", a)
|
||
asyncio.run(send(picture(a)))
|
||
else:
|
||
asyncio.run(send(render("\n".join(args) or "Hello, LX-D02!\nПривет с ПК")))
|
||
print("ok")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main(sys.argv[1:])
|
||
except Exception as err: # started from "Send to": keep the window open to show why
|
||
print("ERROR:", err)
|
||
if sys.stdin and sys.stdin.isatty():
|
||
input("Press Enter to close")
|
||
sys.exit(1)
|