colourful words and phrases

techy tangents and general life chatter from a tired sysadmin.

Taking a ride on the Universal Serial Bus

Hello! I'd like to take a minute here to talk about some fun I've been having with the USB interface for the Paperang P1 and P2 portable thermal printers.

I discovered recently that these devices had a USB interface (I'd previously thought the USB port was only for charging the internal battery) when I stumbled onto the (Chinese-only) download page for the Paperang software, which contains something omitted from the English variant of the page; Mac and Windows software, and mentions of using USB to print.

Unfortunately the official Paperang software is very sparse in its featureset, and doesn't appear to properly communicate with the P2 model (It'll produce incorrect print results), so I was motivated to try and reverse-engineer the communications protocol used (a fantastic series of blog posts made by a good friend of mine on the subject of hacking a cheap chinese USB mouse provided added inspiration, and I sincerely recommend you read them).

When first plugged into a Windows machine, it's recognised as a USBPRINT device (USB class 0x07) with vendor 0x4348 (WinChipHead) and product 0x5584 (CH34x printer adapter cable). The host will, as part of device initialisation, send a URB_CONTROL request type 0xA1 (device-to-host class 0x1, interface 0x1), to which the printer responds with 0x0060 followed by MANUFACTURER:;COMMAND SET:ESC/POS;MODEL:MiaoMiaoJi;COMMENT:Impact Printer;ACTIVE COMMAND:ESC/POS;. However, the printer doesn't actually contain a CH34x-type chip, and the USB data lines are directly connected to the main MCU (an STM32-type chip). Further, the URB_CONTROL response states that the printer uses the ESC/POS method of communication, however this doesn't appear to actually be the case. It's very unclear why they'd put this in, however both the P1 and P2 models report this. I did test whether they supported ESC/POS communications, however the messages were ignored by the printer.

When the Paperang Windows application first detects the printer, it'll begin sending messages to the printer via URB_BULK output. These messages are formed of a distinct structure:

--------------------------------------------------------------
| Frame Begin | CMD/OpCode | Data   | CRC        | Frame End |
--------------------------------------------------------------
| 0x02        | 0x00000000 | 0x0..0 | 0x00000000 | 0x03      |
--------------------------------------------------------------

The Data block is of variable length, and from my observations may be as small as 2 bytes, and may be as large as 1008 bytes. The CMD/OpCode block is always 4 bytes, and I have identified the following OpCodes:

  • 0x06000200 - Appears to be something of a "session" start indicator (Data block has only ever been observed to be 0x0000 for this OpCode)
  • 0x1a000200 - Feed command (doubles as a no-op) - Data block for this is simply the hex representation of an integer, corresponding to the number of milliseconds the printer should feed the paper for
  • 0x18010401 - CRC data transmission (Data block will be 4 bytes in length). This is an interesting one, in that the Data block consists of an initial value for the CRC32 algorithm, and the CRC block will be the initial value's checksum calculated using the magic value 0x35769521 as the "initial" initial value. Once this OpCode has been used, the initial value will be XORed by the magic value and will then be used for future CRC calculations. The official Paperang software has an internal table of "initial values" which it picks from at random, however as it uses the C standard library's random number function, the value it ultimately uses is almost always the same.
  • 0x0001FFFF - Print data transmission OpCode - this one is somewhat special in that the latter 2 bytes (denoted here as 0xFFFF) are dynamic, and correspond to an integer of the length of the Data block. The Data block will be a variable-length (up to 1008 bytes) block of data to be printed.

The first message sent to the printer is the CRC data transmission message. An example is:

------------------------------------------------------------------
| Frame Begin | CMD/OpCode | Data       | CRC        | Frame End |
------------------------------------------------------------------
| 0x02        | 0x18010400 | 0x4d0dc477 | 0x699295ed | 0x03      |
------------------------------------------------------------------

And the CRC result for all further messages where the Data block was 0x0000 was 0x906f4576

From this point, the printer can be used normally. There may be other functionality available through the USB interface, but the first thing I looked at implementing was actually printing data. Upon printing some sample data using the official software, it very much looked like there was no special encoding - if I printed a single dash (-), the bytes sent to the printer appeared to match up with the physical dots printed on the paper.

Although I could've then spent a while printing some test images on the paper and looking at the transmitted bytes in Wireshark, I figured I may as well just start bit-fiddling and see what happens. I'd already determined that the OpCode for printing data contained the length of the transmitted data in its latter two bytes, so I figured I'd start with the maximum length for a single packet - 1008 bytes - and set segments at 8-byte intervals to determine roughly what was going on:

byte[] Data = new byte[1008];
for(int i=0;i<Data.Length;i+=8)
	Data[i]=0xFF;

I sent this along to the printer, and was pleased to see it printed 6 vertical lines. Based on the fact that the lines were not skewed, it seemed like the line length in bytes was divisible by 8, and based on there being six vertical lines, it seemed like the line length was 48 bytes. To test this theory, I adjusted my code to the following:

byte[] Data = new byte[1008];
for(int i=0;i<Data.Length;i+=48)
	Data[i]=0xFF;

And was pleased to find that it now only printed one vertical line. Cool, we've now worked out the line length, and we've confirmed that 0xFF prints a line..now how do we get a single dot? I tweaked my code again, this time setting every 48th byte to an alternating pattern of 0x0F and 0xF0:

byte[] Data = new byte[960];
for(int i=0;i<Data.Length;i+=96) {
	Data[i]=0x0F;
	Data[i+48]=0xF0;
}

It printed something resembling a zig-zag! Awesome. It seems like every byte can be controlled 4 bits at a time, at least, for controlling what gets printed. Can we go deeper? Let's try alternating byte patterns! We'll make every 48th byte an alternating pattern of 0x33 and 0xCC. This should produce two narrow zig-zags:

byte[] Data = new byte[960];
for(int i=0;i<Data.Length;i+=96) {
	Data[i]=0x33;
	Data[i+48]=0xCC;
}

Print..and hey presto! Right on the money! Can we go even deeper? Let's try an alternating pattern of 0x55 and 0xAA, which in binary is 0b01010101 and 0b10101010:

byte[] Data = new byte[960];
for(int i=0;i<Data.Length;i+=96) {
	Data[i]=0x55;
	Data[i+48]=0xAA;
}

Just like that, we've got what looks visually to be a dithered (50% dark) vertical line! Radical. From this we can deduce that every bit in the byte corresponds to an individual "dot" produced by the thermal print head. Thus, given that we know every 48-byte block is one line, we also know now that each line is 384 "dots" wide.

Awesome, we know everything we need to in order to print an image!

From here, the process of printing an image is pretty obvious - resize the image to 384 pixels wide, and convert it to a 1-bit bitmap. This presents a problem though; if the image is even slightly large, it'll exceed the maximum packet size for a message sent to the printer (1008 bytes). Luckily for us, however, C# does present a fairly nice way of splitting at specific lengths, using LINQ:

using System.Linq;
using System.Collections.Generic;
List<byte[]> segments =
	Data
		.Select( (byt, idx) => new {Index=idx,Value=byt} )
		.GroupBy( byt => byt.Index / 1008)
		.Select( byt => byt.Select( bytval => bytval.Value )
			.ToArray()
		).ToList();

We can then iterate over the resulting segments variable! Thanks chief, very cool.

Where's all this leading to? Well, the long and short is that I've written a C# library for interfacing with P1 and P2 model Paperang printers over USB, as well as a basic Windows front-end for it. You can find it here or on my GitHub!