From 719ddcf04a8174abc2312dbb6ed082515b0d7379 Mon Sep 17 00:00:00 2001 From: grabowski Date: Thu, 9 Jul 2026 18:15:01 +0700 Subject: [PATCH] Print lamp ID labels on the Brother QL-820NWB label_printer.py renders a DK-11221 23x23 mm label (QR code linking to the lamp's buildfor.life page plus the ID) and sends it over the network backend (brother-ql-next). The station prints two after each successful measurement, one for the box and one for the lamp; printer failures never lose a run, and any lamp can be reprinted with uv run label_printer.py . Printer address via HPCS_LABEL_PRINTER or --printer. --- README.md | 6 ++- label_printer.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++ lamp_station.py | 24 +++++++++++ pyproject.toml | 3 ++ 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 label_printer.py diff --git a/README.md b/README.md index 1ab4967..95e2c18 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,11 @@ from the packaging (flux, CCT, power, CRI, lifetime, equivalent W -> stored under "rated" in metrics.json for claimed-vs-measured comparison), insert the lamp, and the tool measures per the published procedure (230 V / 50 Hz, 60 s settle, 5 readings averaged), writes lamps// into the comparison-data -checkout, commits, and pushes. The website picks the lamp up automatically: +checkout, commits, and pushes, then prints two ID labels (QR code linking to +the lamp's page plus the ID, DK-11221 23x23 mm) on a Brother QL-820NWB network +printer: one for the box, one for the lamp. Set HPCS_LABEL_PRINTER (e.g. +tcp://192.168.1.50) or pass --printer; reprint anytime with +`uv run label_printer.py `. The website picks the lamp up automatically: its device list is data-driven from the comparison-data bundles and the push triggers the auto-bump deploy. An .md page in the web repo is only ever needed for photos, where-to-buy links, or notes. diff --git a/label_printer.py b/label_printer.py new file mode 100644 index 0000000..ccaf052 --- /dev/null +++ b/label_printer.py @@ -0,0 +1,109 @@ +""" +Lamp ID labels on the Brother QL-820NWB (DK-11221, 23x23 mm die-cut). + +Each label carries a QR code linking to the lamp's page on buildfor.life and +the lamp ID as text. The station prints two per lamp: one for the box, one +for the lamp itself. + +Printer address comes from --printer or the HPCS_LABEL_PRINTER environment +variable, e.g. tcp://192.168.1.50 (the QL-820NWB listens on port 9100). + +Usage: + uv run label_printer.py L0006 # print 2 labels + uv run label_printer.py L0006 --copies 1 + uv run label_printer.py L0006 --preview out.png # render only, no printer +""" + +import argparse +import os +import sys + +import qrcode +from PIL import Image, ImageDraw, ImageFont + +LABEL_ID = "23x23" # DK-11221 +DOTS = (202, 202) # printable area of the 23x23 label +MODEL = "QL-820NWB" +URL_BASE = "https://buildfor.life/comparisons/lamps/" +TEXT_HEIGHT = 52 # bottom strip for the ID text + + +def _font(size): + for name in ("arialbd.ttf", "arial.ttf", "DejaVuSans-Bold.ttf"): + try: + return ImageFont.truetype(name, size) + except OSError: + continue + return ImageFont.load_default(size) + + +def render_label(lamp_id: str) -> Image.Image: + """202x202 1-bit label: QR to the lamp page on top, ID text below.""" + image = Image.new("1", DOTS, 1) + + qr = qrcode.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_M, + box_size=1, + border=2, # quiet zone; tight but fine for close-range scanning + ) + qr.add_data(f"{URL_BASE}{lamp_id.lower()}/") + qr.make(fit=True) + modules = len(qr.get_matrix()) + qr_size = DOTS[1] - TEXT_HEIGHT + qr.box_size = max(1, qr_size // modules) + qr_img = qr.make_image().get_image().convert("1") + image.paste(qr_img, ((DOTS[0] - qr_img.width) // 2, (qr_size - qr_img.height) // 2)) + + draw = ImageDraw.Draw(image) + size = 44 + while size > 10: + font = _font(size) + box = draw.textbbox((0, 0), lamp_id, font=font) + if box[2] - box[0] <= DOTS[0] - 8: + break + size -= 2 + draw.text( + ((DOTS[0] - (box[2] - box[0])) // 2 - box[0], DOTS[1] - TEXT_HEIGHT + (TEXT_HEIGHT - (box[3] - box[1])) // 2 - box[1]), + lamp_id, + font=font, + fill=0, + ) + return image + + +def print_labels(lamp_id: str, printer: str, copies: int = 2) -> None: + """Render and send `copies` labels to the QL-820NWB.""" + from brother_ql.backends.helpers import send + from brother_ql.conversion import convert + from brother_ql.raster import BrotherQLRaster + + image = render_label(lamp_id) + raster = BrotherQLRaster(MODEL) + instructions = convert(qlr=raster, images=[image] * copies, label=LABEL_ID, cut=True) + send(instructions=instructions, printer_identifier=printer, backend_identifier="network", blocking=True) + + +def main(): + parser = argparse.ArgumentParser(description="Print lamp ID labels (QR + ID)") + parser.add_argument("lamp_id", help="e.g. L0006") + parser.add_argument("--printer", default=os.environ.get("HPCS_LABEL_PRINTER", ""), + help="tcp:// of the QL-820NWB (default: HPCS_LABEL_PRINTER env)") + parser.add_argument("--copies", type=int, default=2) + parser.add_argument("--preview", metavar="PNG", + help="Write the rendered label to a PNG instead of printing") + args = parser.parse_args() + + lamp_id = args.lamp_id.upper() + if args.preview: + render_label(lamp_id).save(args.preview) + print(f"Wrote {args.preview}") + return + if not args.printer: + print("ERROR: no printer configured. Pass --printer tcp:// or set HPCS_LABEL_PRINTER.") + sys.exit(1) + print_labels(lamp_id, args.printer, args.copies) + print(f"Printed {args.copies} label(s) for {lamp_id} on {args.printer}") + + +if __name__ == "__main__": + main() diff --git a/lamp_station.py b/lamp_station.py index 9e49232..24c72b1 100644 --- a/lamp_station.py +++ b/lamp_station.py @@ -31,6 +31,10 @@ supports unbranded lamps without any barcode. website picks it up automatically (data-driven device list plus the auto-bump deploy); an .md page in the web repo is only ever needed for photos, where-to-buy links, or notes. + 6. Two ID labels (QR to the lamp's page + the ID, DK-11221 23x23 mm) + print on the Brother QL-820NWB: one for the box, one for the lamp. + Configure the printer via HPCS_LABEL_PRINTER or --printer; reprint + any lamp later with `uv run label_printer.py L0006`. Usage: uv run lamp_station.py # defaults below @@ -40,6 +44,7 @@ Usage: import argparse import json +import os import re import subprocess import sys @@ -304,6 +309,20 @@ def measure_one(new_id, ean, lamps, args, data_repo, existing_id=None): print(" The lamp publishes automatically (auto-bump deploy); add an .md under") print(" src/content/comparisons/lamps/ only for photos, buy links, or notes.") + # Two ID labels on the QL-820NWB: one for the box, one for the lamp. + if not args.no_labels: + if args.printer: + try: + from label_printer import print_labels + print_labels(lamp_id, args.printer, copies=2) + print(f" Printed 2 ID labels for {lamp_id}.") + except Exception as e: # noqa: BLE001 — a printer problem must not lose the run + print(f" Label printing failed ({e}); reprint later with:") + print(f" uv run label_printer.py {lamp_id}") + else: + print(f" (no label printer configured; set HPCS_LABEL_PRINTER or use --printer,") + print(f" reprint later with: uv run label_printer.py {lamp_id})") + def main(): parser = argparse.ArgumentParser(description="Barcode-driven lamp measurement station") @@ -320,6 +339,11 @@ def main(): help="Write the bundle but skip git entirely") parser.add_argument("--no-push", action="store_true", help="Commit locally but do not push") + parser.add_argument("--printer", default=os.environ.get("HPCS_LABEL_PRINTER", ""), + help="QL-820NWB address, e.g. tcp://192.168.1.50 " + "(default: HPCS_LABEL_PRINTER env)") + parser.add_argument("--no-labels", action="store_true", + help="Skip printing the two ID labels after each lamp") args = parser.parse_args() data_repo = Path(args.data_repo) diff --git a/pyproject.toml b/pyproject.toml index 52cbe88..615a096 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,4 +7,7 @@ requires-python = ">=3.11" dependencies = [ "pyserial>=3.5", "colour-science>=0.4.4", + "qrcode>=8.2", + "pillow>=12.3.0", + "brother-ql-next>=0.12.0", ]