LX-D02 BLE thermal printer: PC, Windows printer, Home Assistant

- 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>
This commit is contained in:
Alex Cube
2026-10-21 16:56:45 +03:00
co-authored by Claude Opus 5.5
commit 9e06f3ebd0
12 changed files with 765 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
"""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)