Skip to content

Coding printers

Drive CIJ, TIJ and laser coding printers from a flow: load the right message and set batch, dates and weights from a production system over TCP or RS-232.

Updated View as Markdown

opex-flow can sit between a production system (MES, ProLine, ERP) and the coding printers on a line. A business system posts an order to the ingest API; a flow maps it to printer jobs; a printer node loads the matching message on each printer and fills its variable fields. Every job lands in a job log, and the caller gets a per-printer answer.

Protocols

protocol Printers Link What a job sends
markem_imaje Markem-Imaje 9020/9030/9040 CIJ (“V24” protocol) TCP (built-in Ethernet, default port 2101) or RS-232 Select library message 5Ah (1–127), external variables 5Bh (1–10), counter reset 3Ah / set 51h. Status: jet state 32h + faults 3Bh.
hitachi Hitachi RX / RX2 / UX CIJ (standard communication) RS-232 (factory 4800 8N1, null-modem) or a serial device server Go online 1F 73, recall message 1F 20 31 (number), print items DLE n text (DC2 + resend once when the printer is “not ready”), count block initial value 1F 2C 31. Status: clock inquiry 1F 75.
zipher Videojet CLARiTY family: TTO printers, CLARiTY laser controller TCP (text-comms port, off by default) or RS-232 SLA|job|field=value| (or JDA without a job). Status: GST.
eotsum Videojet/Alltec lasers with an AllprintBasic script (no CLARiTY) RS-232 Your templates, framed STX payload EOT checksum, ACK 06 / NAK 15.
ascii Any text-line input, e.g. “Scan & Print” / “Scan & Select” serial input of TIJ printers TCP or RS-232 Your templates + terminator; optional ack / nak reply regexes (no ack = no reply awaited).
domino Domino Ax-Series CIJ (Ax150i/350i/550i), Codenet 2; other Domino families are not supported yet TCP (default port 7000) or RS-232 (9600 8N1) Select label slot ESC P 1 NNN, one external-data block ESC OE (values by index), counter refused. Status: identity A + status 1 C. See Domino.
zebra Zebra ZPL II printers (ZT/ZD/ZQ …); Honeywell / Intermec in ZPL (ZSim) emulation TCP 9100 (raw) or RS-232 Stored format recall ^XF + ^FN<n> fields + ^PQ, or an inline ZPL template; values hex-escaped (^FH, ^CI28 for UTF-8). Status: ~HS. See Label printers.
tsc TSC TSPL / TSPL2 label printers (TTP / TDP / DA / TE / ML) TCP 9100 or serial / USB Inline template (SIZE/GAP/DIRECTION/CLS/TEXT/BARCODE/QRCODE/PRINT) or RUN "FORM.BAS". Status: <ESC>!? (one byte).
sato SATO SBPL label printers (CL4NX/CL6NX, PW4NX, FX3-LX, CT4-LX, S84NX …) TCP 9100 or RS-232C <ESC>A … <ESC>Z template (<Q> quantity) or stored form overlay recall </>NN. Status: ENQ (Status 4 / 3).

eotsum and ascii render templates: {message}, {counter} and field names are placeholders; {BATCH:10} left-aligns in 10 characters (truncating), {WEIGHT:>7} right-aligns, {COUNT:06} zero-pads, {{ is a literal brace. Their data template sends every field each time, so a job that updates only WEIGHT reuses the last batch and date sent to that printer; defaults covers fields no job has sent yet.

Printers

Printers come from printers: in the config file (top level) and from the API; API edits are saved in <data_dir>/printers.runtime.json and win on restart, so IT can move or swap a printer without editing the config file.

printers:
  - id: mi9040-s6l-01
    name: Sachet 6L line 1
    line: S6L-01                  # production line (flows can target it)
    protocol: markem_imaje        # + protocol options: head, jet, enq
    transport: { kind: tcp, host: 192.168.10.11, port: 2101 }
    messages: { FMT-3: "1", FMT-4: "2" }   # logical format → library message number
    fields: { LINE1: "1", LINE2: "2" }     # logical field → external variable
    timeout_ms: 3000              # per reply (default 3000)
    connect_timeout_ms: 5000      # default 5000
  - id: rx2-dus-01
    protocol: hitachi             # options: go_online, count_block, counter_start, recall_delay_ms
    transport: { kind: serial, path: /dev/ttyUSB0, baud: 4800, data_bits: 8, parity: none, stop_bits: 1 }
    messages: { FMT-1: "1" }
    fields: { LINE1: "1", LINE2: "2" }

Name maps. Flows speak logical names; each printer maps them to its own. A non-empty map is a whitelist: a printer takes only the fields it maps (a flow may send a superset), and refuses a message missing from its messages (it never prints with the wrong layout). Map a message to "" to accept it without selecting anything (one fixed layout). Empty maps pass names through.

Each printer has one connection, opened on the first job and kept open; jobs to one printer run one at a time, different printers in parallel. A job that fails on a dropped connection is retried once on a fresh one.

Domino Ax-Series (Codenet)

protocol: domino drives the Domino Ax-Series continuous ink jet printers (Ax150i, Ax350i, Ax550i) over Codenet 2, the remote protocol Domino documents for them. It does not cover Domino’s other families: Gx/G thermal ink jet, D/F/Dx lasers and Cx/Vx printers use Domino’s Dynamark interface protocol, which Domino does not publish (see what to ask Domino). variant: ax (the default and only value today) reserves the switch for them; until then use protocol: ascii templates once Domino gives you that guide.

Link. TCP to the printer’s Codenet port (7000 default; 7001, 7002 and 7004 are also available) or RS-232 (printer defaults 9600 baud 8N1, RTS/CTS flow control). On the printer: Home > Setup > Printer network > Advanced: Protocol Setting = Codenet, Protocol Mode = TCP, TCP Port, tick Protocol Enabled, restart. TCP needs the Basic Comms hardware pack, RS-232 the RS232 pack.

Framing. Every command is ESC id parameters EOT (1B … 04). The printer answers ACK (06) or NAK (15 + three digits; the driver reports the code with its meaning from the manual’s NAK table, e.g. NAK 017 (specified print label number is invalid)). With the printer setting Response Length = Fixed ACK is 06 30 30 30: set fixed_response: true. Unsolicited status reports (ESC 1 …) between a command and its reply are skipped.

Job part Sent as Mapping
message ESC P 1 NNN EOT: get label from store NNN and put it online messages: value = label slot 1–255
fields one ESC OE LLLL data EOT external-data block: the values joined by delimiter in index order (gaps stay empty) fields: value = item index 1–16
counter not supported: refused with a clear error Codenet documents no counter command; make the counter part of a field the label reads, or reset it on the printer
status ESC A ? EOT (identity, also sent on every connect: the manual says it is the first command on a link) + ESC 1 C ? EOT (current status) + ESC OP ? EOT (FiFo setup, raw)

The data block is 0123,456,ABCD-style: the label’s updatable text fields (external data, comma delimited) each read their item by index (manual, “Insert Updatable Text Field”, example 2). Values are printable ASCII without the delimiter (a block is at most 1024 bytes). Every update re-sends all fields, so fields a job leaves out keep the value last sent to that printer (until the connector restarts), like the ascii driver.

Status returns the printer type and firmware, the status number (000 = printer ready), its class (normal / warning / printing_inhibited / undefined by the first digit), the ink jet and the time of the status change.

printers:
  - id: ax350-s6l-01
    name: Sachet 6L line 1 — Domino Ax350i
    line: S6L-01
    protocol: domino
    # variant: ax                  # only Ax-Series Codenet 2 today
    # delimiter: ","               # must equal the delimiter of the label's updatable text fields
    # fixed_response: false        # true when the printer is set to Response Length: Fixed
    transport: { kind: tcp, host: 192.168.10.41, port: 7000 }
    # transport: { kind: serial, path: /dev/ttyUSB0, baud: 9600, data_bits: 8, parity: none, stop_bits: 1, flow_control: hardware }
    messages: { FMT-3: "3", FMT-4: "4" }          # logical format → label slot (001–255) in the printer's label store
    fields: { BATCH: "1", PD: "2", ED: "3" }      # logical field → index in the external-data block (1–16)

Verified and assumed

Verified in Domino’s manual (EPT033760 Issue 1): the ESC … EOT framing, ACK/NAK and the NAK code table, P (label select), OE (external data), A (identity), 1 C (status and its reply layout), the ports, the RS-232 defaults and the printer settings Send Response and Response Length. The status request and ACK reply were also seen on a real Ax350i in a field report (TCP 7000).

Assumed, confirm on your printer:

  • The label has updatable text fields whose source is external data with your delimiter and the indexes in fields:. The driver never uploads label designs.
  • How long an external-data block lives (consumed per print, kept until replaced, behaviour when the label changes) depends on the printer’s FiFo setup (OP). The driver never writes OP; the status call returns its raw current value.
  • Counters / serial numbers (], U) appear in the manual’s compatibility tables but are not specified there, so they are refused instead of guessed.
  • The meaning of status numbers is decoded from the manual’s status table by the last two digits (the table is partly garbled in the copy we read); the number itself is always returned.

Commissioning checklist

  1. Protocol = Codenet, Mode = TCP (or serial settings), a port, Protocol Enabled, restart.
  2. Printer setting Send Response = On Processed. On Received ACKs before the data is validated, so a bad label slot would look successful.
  3. GET /api/printers/{id} then POST /api/printers/{id}/status: identity and status come back (ready: true at status 000).
  4. Build the label in the printer UI with updatable text fields (external data), note slot number and field indexes, put them in messages: / fields:.
  5. Run a job by hand (POST /api/printers/{id}/jobs) and read the printed label; try a wrong slot to see the NAK message in the job log.
  6. Check what happens to a field when no new data arrives for the next print (FiFo behaviour).

What to confirm with Domino

  • The Dynamark Interface Communications User Guide (and the Dynamark Authentication Protocol guide) for Gx/G, D/F/Dx, Cx/Vx: transport/port, command syntax and variable addressing. Domino’s EtherNet/IP command list names the commands (LOADPROJECT, SETTEXT, SETVARIABLES, SETCOUNT, GETVERSION, BEGINTRANS, EXECTRANS, …) but not their wire form.
  • For Ax: the OE note about clearing the buffer, OP settings for your use, and the counter command (], U).

Sources

Label printers (Zebra, TSC, SATO)

zebra, tsc and sato print one label per job on thermal label printers: pack, pallet and shipping labels with the order’s data. They differ from coding printers in two ways. A job is a whole page description (there is no “current message” to edit field by field), and the same flow field map picks the data. Each driver has two ways to say what to print:

  • a stored format or program in the printer (message names it), or
  • an inline template of the printer’s templates: map. message is the template name and ${field} placeholders are replaced by the escaped value of the job field whose native name (the right-hand side of fields:) is field.

The native field name quantity is the label count (fields: { QTY: quantity }); ${quantity} puts it in a template. Without it the quantity is 1. counter jobs are refused on all three (use the printer’s own serial-number command inside the format).

A refused printer state ends the job with rejected (the flow sees Fail, nothing was sent). No answer is timeout (Retry). Every status check is made before the job is sent.

Zebra (ZPL II, TCP 9100)

Send to the printer’s raw port 9100 (or serial). Honeywell and Intermec printers in ZPL (ZSim) emulation use this same driver: set the printer’s command language to ZPL, and see the note below.

Save a format once with ^DF (any ZPL tool or nc printer 9100), numbering the variable fields with ^FN:

^XA^DFE:SHIPLBL.ZPL^FS
^FO50,50^A0N,40,40^FN1"Order"^FS
^FO50,110^A0N,40,40^FN2"Customer"^FS
^FO50,170^BCN,80^FN1^FS
^XZ

Each job then sends ^XA^XFE:SHIPLBL.ZPL^FS^FN1^FDSO-1001^FS^FN2^FDACME^FS^PQ4^XZ:

printers:
  - id: zt411-pack-01
    name: Pack station 1
    line: PACK-01
    protocol: zebra
    # drive: "E:"                 # where stored formats live (R: RAM, E: flash)
    # precheck: true              # ~HS before the job: refuse when paused / head open / out of media
    # confirm: false              # poll ~HS until no labels remain (confirm_timeout_ms, confirm_poll_ms)
    transport: { kind: tcp, host: 192.168.10.60, port: 9100 }
    messages: { SHIP: SHIPLBL, PALLET: PALLET }   # logical message → stored format name / template name
    fields: { ORDER: "1", CUSTOMER: "2", SKU: sku, QTY: quantity }   # logical field → ^FN number, ${placeholder} or quantity
    templates:                                    # inline jobs; message = template name
      PALLET: |
        ^XA
        ^FO50,50^A0N,50,50^FH^FD${sku}^FS
        ^FO50,130^BCN,100^FH^FD${sku}^FS
        ^XZ

With that map, a job {message: PALLET, fields: {SKU: "A-1"}} sends ^XA^FO50,50^A0N,50,50^FH^FDA-1^FS^FO50,130^BCN,100^FH^FDA-1^FS^PQ1^XZ (template lines are joined without separators; ^PQ<qty> is added before the last ^XZ unless the template has ^PQ${quantity} itself).

Values are escaped. Every value is written with ^FH hex escapes (_5E for ^, _7E for ~, _5F for _, _XX for controls), so a value can never end its field or start a command. Non-ASCII text goes out as UTF-8 bytes with ^CI28 (firmware V60.14 / V50.14 or later). In templates, every ${field} must sit in a field with ^FH before its ^FD; the connector rejects the printer configuration otherwise.

Status is ~HS: the three host-status strings become paper out, pause, head open, ribbon out (thermal transfer mode), buffer full, over / under temperature, formats in the receive buffer and labels remaining. With precheck on, a job to a printer that is paused, head open, out of media / ribbon or too hot is refused with printer not ready: print head is open (or the matching reason); a full receive buffer and a printer that does not answer are Retry. Zebra documents that the printer stays silent to ~HS while media out, ribbon out, head open or over-temperature, so on such printers an unready state can also show as a timeout. With confirm: true the connector polls ~HS after sending until no labels remain on two polls in a row; a stall (printer stopped answering, head opened mid-batch, timeout) is Fail (never retried, so a batch cannot print twice).

TSC (TSPL / TSPL2)

printers:
  - id: tsc-ship-01
    protocol: tsc
    transport: { kind: tcp, host: 192.168.10.61, port: 9100 }   # or serial / USB bridge
    fields: { NAME: name, SKU: sku, QTY: quantity }
    templates:
      SHIP: |
        SIZE 100 mm,60 mm
        GAP 3 mm,0 mm
        DIRECTION 1
        CLS
        TEXT 20,20,"3",0,1,1,"${name}"
        BARCODE 20,80,"128",60,1,0,2,2,"${sku}"
        PRINT 1,${quantity}

Lines are sent with CR LF. A " in a value becomes \["] (the manual’s escape); control characters and the sequence \[ are refused. Add CODEPAGE UTF-8 to the template to print non-ASCII text. A stored program is run with message: run:FORM.BAS (RUN "FORM.BAS", TSPL2 printers only); the manual documents no way to pass variables to it, so these jobs carry no fields. Status is <ESC>!?: one byte (head open, paper jam, out of paper, out of ribbon, pause, printing, other error), refused before the job is sent on every problem bit.

SATO (SBPL)

Templates use the manual’s notation: < stands for ESC and > is dropped, so <A><V>100<H>200<XM>${text}<Q>${quantity}<Z> is ESC A ESC V100 ESC H200 ESC XM… ESC Q… ESC Z (\< and \> are literal). Templates start with <A> and end with <Z>.

printers:
  - id: sato-pallet-01
    protocol: sato
    # encoding: ascii             # or latin1 (single-byte code page); anything else is refused
    transport: { kind: tcp, host: 192.168.10.62, port: 9100 }   # Port 3: data + status
    messages: { PALLET: PALLET, STD: "form:01" }
    fields: { TEXT: text, CODE: code, QTY: quantity }
    templates:
      PALLET: "<A><V>100<H>200<P>2<L>0202<XM>${text}<V>300<H>200<B>103100*${code}*<Q>${quantity}<Z>"

message: form:01 sends ESC A ESC / 01 ESC Q n ESC Z, which prints the form overlay saved with <&>01 (fixed content only; data fields need a template). Status is ENQ: the printer’s PROTOCOL setting must be Status 4 (ENQ) or Status 3. Offline, print halt and error codes (head open, paper end, ribbon end, media / sensor / head / cutter errors …) refuse the job; buffer near full is Retry. Values containing control characters (including ESC) are refused.

Honeywell / Intermec

Honeywell (ex-Intermec) PM / PC / PD printers ship with Fingerprint as the default command language but can run ZPL emulation (ZSim): select ZPL as the printer’s command language and configure it as a zebra printer. How much of ^XF / ^FN / ^FH / ^CI28 / ~HS a model’s ZSim covers is in Honeywell’s ZSIM Command Reference; set precheck: false on a model that does not answer ~HS. There is no Fingerprint (PRTXT / PRINTFEED) driver: it needs a downloaded BASIC-like program per label, which the public documents do not cover well enough to drive safely.

Sources and unconfirmed points

Zebra, ZPL II, ZBI 2, Set-Get-Do, Mirror, WML Programming Guide (~HS, ^XF, ^FN, ^FH, ^PQ, ^CI); TSC, TSPL/TSPL2 Programming Manual; SATO, SBPL Command Reference v7.0; Honeywell, ZSIM Command Reference. Research notes with every [UNCONFIRMED] point are in the repository: docs/printers/printer-zebra-zpl.md, printer-tsc-tspl.md, printer-sato-sbpl.md, printer-honeywell-intermec.md. The ones to check on a real printer: Zebra’s silent ~HS in error states and ^CI28 firmware support; how a literal \[ is written in TSPL; the byte-level SBPL status reply (the test vectors come from the manual’s layout and status table, not from a capture).

The printer node

A printer node has exactly one target:

Target Reaches Example
printer one printer (id, may be a ${path} template) { "type": "printer", "printer": "vj3340-s6l-13b", "message": "${metadata.message}" }
line every enabled printer whose config has line: <that line>, looked up at run time (literal or ${path} template) { "type": "printer", "line": "S6L-13", "message": "${metadata.message}", "fields_from": "metadata.fields" }
jobs_from the printers named in an array of { printer, message?, fields?, counter? } in the record (the jobs run concurrently) { "type": "printer", "jobs_from": "jobs" }

For printer and line the same message, fields ({ "BATCH": "${order.batch}" }), fields_from (path of an object of fields) and counter (reset or digits) templates apply to each printer reached, and each printer then applies its own message / field maps (the whitelist above: unmapped fields are dropped, an unmapped message is refused). At least one of them is required. Values are ${path} lookups into the record; a value that renders empty is not sent. timeout_ms caps each job (default: from the printer’s timeouts, within the invocation deadline).

On success the record continues with printer_result: { ok, printers: [{ printer_id, status, detail, duration_ms }] }. A refused or invalid job fails the node (Term); an unreachable or silent printer is a retry (Nak); with several printers the node fails if any of them failed (the others’ jobs still ran, see the job log). A line with no enabled printer fails the node (“no enabled printer is on line …”). In a dry run (POST /api/flow/test) the jobs are recorded, not sent: each printer node adds an entry with every job (printer, message, fields, counter) to sink_effects.

Deploy-time checks: a printer node without anything to print is an error; a literal printer id or line that no configured printer matches is only a warning, so a flow can be deployed before the printers are commissioned (templated targets are checked at run time).

Lines and the per-line pattern

Give every printer a line (line: S6L-13, letters/digits/-_.); GET /api/printers/lines returns { lines: [{ line, printers: [ids] }] }. Many printers across lines that make different products are best kept as one small branch per line rather than one bulk node:

  1. a transform turns the incoming order into a machine record whose metadata carries the line, the message and the fields (no printer lists in the code);
  2. a branch routes by metadata.line (one port per line, else → a node that refuses the order, so the caller gets a 422 naming the line);
  3. one printer node per line with "line": "<line>".

Commissioning a printer on an existing line needs no flow edit (set line: on it); a new line needs one branch port and one printer node. A printer that needs its own message or field override gets a single-printer node of its own instead. The “Order to coding printer by line” example (see Examples) builds this pattern: the branch, one printer node per line and a refusal for an unknown line.

Ingest API

POST /api/ingest/{source}/{id}?wait_ms=30000 (bearer token) takes any JSON object and runs it through the flow source matching {source}:{id} or {source}:* (never the catch-all *). The call waits for the flow to finish:

HTTP result Meaning
200 done Every step and printer succeeded.
422 failed Refused (bad order data, printer NAK); resending the same body fails again.
503 retry A printer was unreachable or silent; resend later.
202 pending Still running after wait_ms; see GET /api/printers/jobs?invocation=.
404 No flow source matches.
{ "ok": false, "result": "failed", "invocation_id": 1761712345,
  "error": "set-printers: 1 of 2 printer job(s) failed: rx2-dus-01: rejected: set 2 field(s): NAK",
  "printers": [ { "printer_id": "mi9040-s6l-01", "status": "ok", "detail": "select message 2: ACK; set 2 field(s): ACK", … },
                { "printer_id": "rx2-dus-01", "status": "rejected", … } ] }

Printer API and job log

Method Path
GET /api/printers Printers (incl. line) with origin (config/runtime), connection state, job counters, last job.
GET /api/printers/lines Printers grouped by production line: { lines: [{ line, printers: [ids] }] }.
GET / PUT / DELETE /api/printers/{id} Read, add or replace (body = printer config), remove.
POST /api/printers/import Bulk create / replace: body = the printers: list of printers.yaml (YAML), a bare list, or JSON (send Content-Type: application/json). All-or-nothing: every printer is validated first (422 { create, update, errors: [{ id?, message }] } when any is refused). ?dry_run=true only returns { create, update, errors }. Example: curl -X POST -H "Authorization: Bearer $TOKEN" --data-binary @printers.yaml "$HOST/api/printers/import?dry_run=true".
POST /api/printers/{id}/status Live status exchange (printer state, faults, clock: whatever the protocol offers).
POST /api/printers/{id}/jobs Run a job by hand: { "message": "FMT-3", "fields": { "LINE1": "…" } }.
GET /api/printers/jobs?printer=&invocation=&before=&limit= Job log, newest first.

The job log (<data_dir>/printer_jobs.db) keeps every job (printer, invocation, source, logical message/fields/counter, outcome, steps, duration) for 90 days.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close