Skip to content

Marklife wire protocols

Every chassis in the marklife catalogue binds to exactly one wire protocol through engines[].protocol. Six tags exist; each has its own page with the byte-level reference.

protocolWire formatChassis boundHardware status
marklife-l1110 FF session frame, uncompressed GS v 0 raster9 (P12, P15, P15R, A1, R15, LP15, LP90, LP25, BARABOGO P12)bench-confirmed on the P12 and P15 over USB, SPP and BLE
marklife-yxq1F-prefixed command stream, zlib raster, per-id variants13 (S2, S8, P50, D210, X2 and their rebrands)bench-confirmed on the S2 (protocol id 2) over SPP and BLE; other ids inferred
marklife-cpclCPCL text inside a 1F 80 envelope, zlib ZG raster1 (T3)inferred, never driven
marklife-jbigESC/POS-shaped framing, JBIG raster in a 1F ( J wrapper5 (D100, X4, X8, U210, L100)inferred; payload encoder not implemented
marklife-tsplTSC TSPL II, BITMAP mode 3 with a zlib payload0inferred, never driven
marklife-escposEpson ESC/POS GS v 0 raster subset0inferred, never driven

Provenance

The byte sequences on these pages come from our own on-the-wire analysis — BLE HCI and USB captures, GATT probes of real units, job dumps, and bench prints — plus published specifications where a sub-engine follows one (Epson ESC/POS, TSC TSPL II, ITU-T T.82 for JBIG, CPCL) and the public prior art at tomLadder/thermoprint for the L11 family.

Each page opens by stating which of its claims a real unit has confirmed. Everything else is inference and is labelled as such; a green test suite confirms only that the encoder is self-consistent.

Common ground

  • 203 dpi, 1 bpp, direct thermal across the whole catalogue.
  • Bit polarity 1 = dark, MSB first, row-major, ceil(width / 8) bytes per row on every sub-engine. This is the natural output of renderImage from @mbtech-nl/bitmap, so no encoder repacks or inverts the bitmap.
  • One job per print() call. Copies are the caller's loop.
  • No status query is wired. The only reply observed from any chassis is a single 0xAA byte after it accepts a job (S2 and P12, 2026-09-11). getStatus() reports link state only.

The wire bytes are transport-independent, but two link-level facts apply to every sub-engine and are easy to lose:

Packet size and pacing. A chassis takes its job as a stream of fixed-size packets with a pause between them. The S2 accepts a 176-byte BLE packet, acks it, and prints nothing; the same job in 95-byte packets prints. Firing a full window of packets back to back loses raster the same way. The registry carries the packet ceiling (transports['bluetooth-gatt'].mtu) and, where a chassis differs from the family's 30 ms, the pause (engines[].capabilities.interChunkDelayMs); the printer classes apply both on every transport.

BLE credit gate (Profile A). On the ff00 service the chassis publishes flow control on a third characteristic, ff03:

02 lo hi    MTU announcement — payload per packet is MTU − 3
01 n        credit grant — n more packets may be written

The opening grant is 01 04. Every packet costs one credit and the chassis tops the window up as it drains. MarklifeBleTransport in the web package implements the gate; the other GATT profiles a chassis may host (Microchip transparent UART 49535343-…, fd00, 18f0) carry no flow control. Which profile a unit exposes is resolved by probing after connect — see the hardware page.

Two compressors

Two sub-engines deflate their raster and they do not share parameters:

PathContainerWindowConfirmed
marklife-yxq raster, marklife-cpcl ZGzlib1 KiB (windowBits 10, CMF byte 0x28)S2 bench print + captured S2 jobs
marklife-tspl BITMAP mode 3zlib32 KiB (default)no

The 1 KiB window is load-bearing: a decoder with a small inflate window cannot read a stream written with a larger one, so a default-window deflate is accepted by the S2 and silently discarded. yxqZlibCompress in core produces the 1 KiB-window stream in both runtimes.