Skip to content

marklife-yxq — command stream with zlib raster

The wire protocol of the 2" mobile and 4" desktop chassis: S2, S8, P50, D210, X2 and their rebrands. Commands are opcode-prefixed byte strings, most of them on a 0x1F lead byte, and the raster travels as a zlib stream behind a 1F 10 header. It is a vendor format with no published specification and no Epson or TSC equivalent — the 1D-prefixed commands it borrows (1D 0C, 1D 76 30) are the only overlap.

The chassis differ in more than a constant. Each registry entry carries a protocol id (engines[].capabilities.protocolId), and the id selects the opcode bytes, the job shape and the density scale. Eight ids share this tag; they are documented per id below.

Provenance

Bench-confirmed on protocol id 2 (S2): the 1F command prefix, the raster header layout, the 1 KiB deflate window, headDots: 384, the density scale, the 1D 0C page advance on gap stock, the 95-byte packet ceiling and the 30 ms pacing. Two captured S2 jobs and multi-packet prints over BLE and SPP (through the harness, 2026-09-13) agree on all of it.

Every other id is inference from our own analysis of the family and has never been driven. Treat the per-id tables as a starting point for a bench session, not as fact.

Chassis by protocol id

idChassisJob shapeRaster
1S8simple1F 10 + zlib
2S2, Jammuk S2, ewtto ET-Z0535, M50simple1F 10 + zlib
3P50, M57, T2, LPW40positioned1F 10 + zlib
5D210positioned, with retract1F 10 + zlib
8X2 over BLEpositioned1F 10 + zlib
9X2, M60 over SPPpositioned, no wakeup1D 76 30 + raw rows
4, 11simple1F 10 + zlib

Ids 4 and 11 are retained in the encoder but bind no chassis: the units that once carried them (P12, R15) speak L11 and were rebound. Ids 7, 10 and 12 use the JBIG wrapper and never reach this encoder.

Opcode vocabulary

OpcodeBytesIdsDescription
Density1F 70 01 dd1, 2, 5Set density dd on register slot 1.
Density1F 70 02 dd3, 8, 9Same command, register slot 2. Addressing the wrong slot leaves the stored density in force.
Density10 FF 10 00 dd4Density in the 10 FF command family.
Density gear10 FF 10 00 gg5Emitted with gg = 2 before the density proper.
Wakeup00 × 151, 11Run of NULs before the enable.
Wakeup00 × 63, 4, 8; 5 on black-mark stock onlyShorter run. On id 5 the six NULs are read as job bytes on gap and continuous stock.
Enable10 FF F1 022Open the print session.
Enable10 FF F1 031, 5Open the print session.
Enable1F C0 01 003, 4, 8, 9, 11Open the print session.
Stop10 FF F1 451, 2Close the print session.
Stop10 FF FE 455Close the print session.
Stop1F C0 01 013, 4, 8, 9, 11Close the print session.
Paper type1F 80 01 mm5Sensor mode: 10 continuous, 30 gap, 20 black-mark, 40 black-mark 2.
Paper type1F 80 02 mm3, 8, 9Sensor mode on slot 2: 20 gap, 30 black-mark, 40 black-mark 2. No command on continuous stock. The two tables are not interchangeable — slot 1's gap code is slot 2's black-mark code.
Gap seek1F 11 513, 8, 9Advance to the next label edge before the raster (gap stock).
Retract1F 11 51 ll hh5The same seek with an explicit LE16 step; sent with step 0 to retract to the sensor.
Park1F 11 503, 8, 9 (after stop); 5 (before stop)Park the media at the tear-off edge. Gap stock.
Home1F 11 008, 9Continuous-stock counterpart of park, after the stop.
Post-raster position1F 12 20 00 / 1F 12 00 003 (gap only); 8, 9Gap / continuous. Without it the label stays under the head.
Feed (short)1B 4A nn1, 4, 11 (nn = 70 after the raster); 2 (nn = 100, continuous stock)ESC J — feed nn dots.
Feed (wide)1B 4A ll hh 003, 5, 8LE16 step plus a trailing NUL. A different command from the 3-byte form; sending one where the other is expected leaves argument bytes to be parsed as opcodes.
Page advance1D 0C2 (gap stock); 5 (non-continuous)Run the stock forward to the next gap. Not in the vocabulary of ids 3, 8, 9.
Raster1F 10 wH wL hH hL l3 l2 l1 l0all but 9Header: width in bytes and height in dots, big-endian 16; zlib payload length, big-endian 32. Payload follows.
Raster (raw)1D 76 30 00 wL wH hL hH9Little-endian width in bytes and height; uncompressed rows follow with no length field.

All multi-byte integers are big-endian in the 1F 10 header and little-endian everywhere else.

Raster block

1F 10  <widthBytes BE16>  <heightDots BE16>  <payloadLen BE32>
<payload: zlib, windowBits 10, level 6>

widthBytes = ceil(widthPx / 8). The payload inflates to exactly widthBytes × heightDots bytes of 1-bpp rows, 1 = dark. The zlib CMF byte is 0x28 — a 1 KiB window — and that is not a detail: the S2 accepts a default-window stream (0x78) and discards the raster. See Two compressors.

Id 9 sends the same rows uncompressed behind a little-endian 1D 76 30 header, which is the L11 raster header.

Job byte streams

One raster block per job. The order and presence of the other elements is per id.

Ids 1, 2, 4, 11 — simple

[density]              1F 70 01 dd          (id 4: 10 FF 10 00 dd)
[wakeup]               00 × 15              (ids 1, 11; none on 2, 4)
[enable]
[raster]               1F 10 … + zlib
[advance]              1B 4A 46             (ids 1, 4, 11: ESC J 70)
                       1D 0C                (id 2, gap stock)
                       1B 4A 64             (id 2, continuous, final copy only)
[stop]

A captured S2 job on 50 × 30 mm gap stock is, in order: 1F 70 01 0A · 10 FF F1 02 · 1F 10 00 30 00 F0 … · <zlib> · 1D 0C · 10 FF F1 45. Without the 1D 0C the S2 composes a valid job that never advances, and the label stays under the head.

Id 3 — positioned

[paper type]           1F 80 02 mm          (not on continuous)
[density]              1F 70 02 dd
[wakeup]               00 × 6
[enable]               1F C0 01 00
[pre-raster]           1F 11 51             (gap)  /  1B 4A 64 00 00  (continuous)
[raster]               1F 10 … + zlib
[post-raster]          1F 12 20 00          (gap only)
[feed]                 1B 4A 64 00 00       (continuous only — the feed is sent twice)
[stop]                 1F C0 01 01
[park]                 1F 11 50             (gap only, after the stop)

Id 5 — positioned, with retract

[paper type]           1F 80 01 mm
[retract]              1F 11 51 00 00
[density gear]         10 FF 10 00 02
[density]              1F 70 01 dd
[wakeup]               00 × 6               (black-mark stock only)
[enable]               10 FF F1 03
[raster]               1F 10 … + zlib
[advance]              1D 0C                (gap)  /  1B 4A 3C 00 00  (continuous)
[park]                 1F 11 50
[stop]                 10 FF FE 45

Ids 8 and 9 — positioned

[paper type]           1F 80 02 mm          (not on continuous)
[density]              1F 70 02 dd
[wakeup]               00 × 6               (id 8 only)
[enable]               1F C0 01 00
[pre-raster]           1F 11 51             (gap)
                       1B 4A 64 00 00       (id 8 continuous; id 9 sends nothing)
[raster]               1F 10 … + zlib       (id 8)  /  1D 76 30 … + raw rows  (id 9)
[post-raster]          1F 12 20 00          (gap)  /  1F 12 00 00  (continuous)
[stop]                 1F C0 01 01
[position]             1F 11 50             (gap)  /  1F 11 00  (continuous)

Density

print() maps 'light' | 'normal' | 'dark' onto a per-id scale; densityLevel sends a value through directly, clamped on ids that have a known window.

idlightnormaldarkclamp
112131412..14
231014
331014
83814
92815— (a 5-step scale; 5 and 11 are reachable through densityLevel)
4, 5, 11111315

Id 2's 0A for normal is what every captured S2 job carries.

Media

media.type selects the paper kind the position and paper-type commands are keyed on: continuous is continuous, everything else (die-cut, gap, tape) is gap. The two black-mark sensor modes are implemented in the builders but no catalogue media reaches them.

Status

No status query is on any print path. The commands 1F 20 00 (status), 1F 80 00 (paper type) and the 10 FF 20 Fx family are believed to be queries and parseYxqStatus decodes a reply shaped <opcode> <payload…> structurally, but no such reply has ever been captured. The one observed reply is a single 0xAA after the S2 accepts a job.

X2 red + black

The X2 chassis can print a second (red) plane; the registry records it as capabilities.redBlackDuplex: true. The encoder emits the black plane only, so a two-colour job prints in black rather than failing.