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 <ID>. Printer address via HPCS_LABEL_PRINTER
or --printer.
This commit is contained in:
2026-07-09 18:15:01 +07:00
parent 7ef2960254
commit 719ddcf04a
4 changed files with 141 additions and 1 deletions
+5 -1
View File
@@ -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/<ID>/ 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 <ID>`. 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.
+109
View File
@@ -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://<ip> 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://<ip> 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()
+24
View File
@@ -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)
+3
View File
@@ -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",
]