Feasibility/design session output: print account+date labels on sample-tray unload via P-touch Template mode, as a shared OMNY_shared utility rather than an ophyd device. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGC1TfxZJvJHs6Qz1V8w1h
8.8 KiB
Brother P-touch PT-P950NW label printing for BEC: design plan
Status: proposal, not yet implemented. This document captures the plan from an initial feasibility/design session so it isn't lost; it should be updated (or removed / folded into an architecture note) once the feature is actually built.
Context
When a measured sample is unloaded from the flomni sample tray, the only record
of "who owns it / when it was measured" today is digital (tray-slot metadata
in dev.flomni_samples, plus the OMNY sample database via TomoIDManager).
There is no physical label on the actual sample holder, so a user picking it
up later has no way to tell at a glance whose sample it is or when it was
run. The goal of this feature is to print a small paper label (account +
date, on TZ tape) at the moment a measured sample is stowed back in the tray,
so it can be stuck straight onto the sample holder.
This is explicitly not an experiment hardware component (no motion, no
staging, nothing that needs to be part of a scan) — it's a side-effect
utility, similar in spirit to the existing TomoIDManager (HTTP registration
call) and HttpUploader (webpage push) helpers, which are plain Python
classes invoked from the flomni/lamni plugin code at well-defined workflow
points, not ophyd devices.
Decisions already made:
- Protocol: P-touch Template mode (Brother's official, documented
command protocol) rather than the raster/reverse-engineered protocol used
by third-party libraries like
rasterprynt. This avoids needing an image library (Pillow) client-side and avoids depending on an undocumented protocol that could break on a firmware update — fits the goal of minimizing external packages. - Scope: build as a shared utility in
bec_ipython_client/plugins/OMNY_shared/, so lamni/omny can adopt it later without duplicating printer/network code — mirrors whereTomoIDManagerandHttpUploaderalready live. - Trigger: both. Automatic prompt-then-print on sample unmount in flomni, plus a manual/standalone command for on-demand (re-)printing.
How P-touch Template mode works (research summary)
- The PT-P950NW supports raw network printing on TCP port 9100 (listed in its spec sheet alongside LPR/IPP/mDNS etc.), the same "JetDirect-style" raw port most network printers use — no CUPS/driver install needed.
- "P-touch Template" is a printer-side mode built for exactly this use case
(the manual explicitly lists "data from a scale, testing machine,
controller, or PLC" as the target): you design a label layout once in
Brother's Windows-only P-touch Editor (place named text objects, e.g.
ACCOUNT,DATE), transfer that template onto the printer's internal memory (via Editor's Transfer Manager over USB or network), and from then on printing is just: connect to the printer, send a short ESC i X command sequence selecting the template number and supplying the text for each named field, then a print-start command. - The command set is documented in Brother's "P-touch Template Command Reference" (per-printer-family PDF from Brother's support site — confirm the PT-P900W/P950NW-specific revision before implementing, since control codes can differ slightly between printer families/firmware generations).
- No actively-maintained Python package implements this specific protocol
(unlike raster printing, which has
rasterprynt/brother_ql-style libraries). That's fine here: the runtime protocol is just building a byte string and pushing it over asocket— stdlib only, no new dependency.
Implementation plan
1. New shared module: PTouchLabelPrinter
File: csaxs_bec/bec_ipython_client/plugins/OMNY_shared/ptouch_printer.py
(new file, alongside omny_general_tools.py/web_common.py).
class PTouchLabelPrinter:
"""Prints a label on a Brother PT-P950NW via P-touch Template mode."""
DEFAULT_PORT = 9100
def __init__(self, host: str, template_number: int = 1, port: int = DEFAULT_PORT, timeout: float = 3.0):
...
def print_label(self, fields: dict[str, str], copies: int = 1) -> bool:
"""Build the P-touch Template command for `fields` and send it.
Returns True on success, False on any network/printer error --
never raises, matching TomoIDManager's fail-soft convention so a
printer being offline can never block a sample transfer.
"""
...
- Builds the ESC-prefixed command sequence (select template mode, select
template number, set each named field's text, set copy count, print
command) as pure bytes, opens a short-lived
socket.create_connection((host, port), timeout=timeout), sends, closes. - Wrap all I/O in
try/except,logger.warning(...)on failure, returnFalse— same fail-soft style asTomoIDManager.register()(omny_general_tools.py:396-) andHttpUploader(web_common.py:48-114). - Printer host/IP and template number are constructor args (not hardcoded
constants), passed in from each plugin's config — check how other
beamline-specific endpoints are configured in this repo (e.g. is there a
per-instrument config file, or are they class constants like
TomoIDManager.OMNY_URL?) and follow that convention when wiring it up.
2. Wire into flomni
File: csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py
- Instantiate once in
__init__, next toself.OMNYTools = OMNYTools(...)(line ~1654) andself.tomo_id_manager = TomoIDManager():self.label_printer = PTouchLabelPrinter(host=<printer-ip>, ...). - Automatic path: in the unmount branch of
ftransfer_sample_change(thenew_sample_position == -1case,flomni.py:867-906) or inftransfer_get_sample(flomni.py:734, name/owner already unpacked at line 786-787 asname, owner = unpack_desc(signal_name.get())), after the stow completes: askself.OMNYTools.yesno("Print a label for this sample?", "y")(same helper already used elsewhere, e.g.flomni.py:2507) and onTruecallself.label_printer.print_label({"ACCOUNT": owner, "DATE": str(datetime.date.today())}). - Manual path: add a small method, e.g.
fprint_sample_label(position: int), that reads name/owner via the sameunpack_desc(...)pattern assample_get_name(flomni.py:853) and callslabel_printer.print_labeldirectly — usable standalone for reprints, without going through a transfer.
3. Open items to resolve before/during implementation
- No date is currently stored per sample slot —
pack_desc/unpack_desc(devices/omny/sample_desc_codec.py:19,40) only carry(name, owner), no timestamp. Usingdatetime.date.today()at unmount time is a reasonable v1 approximation (unmount typically happens same-day as the scan), but if an exact measurement date is wanted later, the DESC codec would need a third field. - Confirm the exact model-specific command reference. Brother publishes a separate "P-touch Template Command Reference" per printer family; download the PT-P900W/P950NW-specific PDF from Brother's support site before writing the byte-level command builder.
- Template design is a one-time, Windows-only step (P-touch Editor + Transfer Manager) — not scriptable from the Linux beamline control host. Worth doing once per label layout/TZ tape width; layout changes require redoing this step.
- Network reachability: confirm the printer's IP is reachable from
wherever
bec_ipython_clientruns (same VLAN/firewall rules as other network endpoints it already calls, e.g. the OMNY/tomo-ID server) and assign it a static/reserved IP. - Extend to lamni/omny by instantiating
PTouchLabelPrinterin those plugins too once flomni is validated — no code changes needed to the shared module itself.
Verification
- Bench test the printer/template alone: install the PT-P950NW on the
beamline network with a fixed IP, design the label template once in
P-touch Editor (fields
ACCOUNT/DATE, chosen TZ tape width), load it via Transfer Manager, and print a manual test label from the desktop app to confirm the template renders as expected. - Bench test the protocol module standalone: write
PTouchLabelPrinter, then from a scratch script callprint_label({"ACCOUNT": "e12345", "DATE": "2026-07-31"})against the real printer IP and confirm a correct physical label comes out (check encoding, field placement, tape length/cut). - Integration test in flomni: in a real or simulated
bec_ipython_clientsession, runftransfer_sample_change(-1)(orftransfer_get_sample) end-to-end, confirm the yes/no prompt appears, confirm printing on "y", confirm declining or a deliberately-unreachable printer IP does not raise or block the sample transfer (only logs a warning). - Test the manual
fprint_sample_label(position)command independently of any transfer, for reprints.