What the hell! Why?
Trying the driver out of the box
When the printer arrived, I plugged it into a spare Raspberry Pi, and... success! Linux recognized it as a printer and it shows up as
/dev/usb/lp0. It doesn't show up as a printer in the linux GUI, but that's ok — printing in Linux is always a bit tricky. I eventually remember that Linux uses a printing system called CUPS, and figured this just needs to be configured first. I don't see it in the list of available printers, so I try installing the Linux driver for the PL60. Simple enough, it's a tarball with an
installscript. I get a warning of
strip: Unable to recognize the format of the input file '/usr/lib/cups/filter/raster-tspl'`
but then a few lines down see
Hooray! Now I see the PL60 listed in the CUPS admin interface. This seems promising!
Well, after clicking through the CUPS interface to add the printer, and trying to print a test page, we see a failed job. The printer now has a Status of
Idle - "File "/usr/lib/cups/filter/raster-tspl" not available: No such file or directory". Bummer, perhaps that warning from the install script wasn't something we could ignore.
I dug into all the files in the driver's tarball and see that there's a
ppddirectory. After some Googling, I learn that PPD files describe the capabilities of each printer, such as paper size, color space, etc. Filters are the executables that convert one document format to another, for example, turning a PNG image into an ASCII ZPL file that can be sent to the printer. But in this tarball's
filterdirectory there are only x64 and x86 filters.
I really want to turn this Raspberry Pi into a permanent shipping station, so at this point, I'm pretty upset that these "Linux drivers" are just pre-compiled x64 and x86 binaries, so I start the Amazon return process and head to dinner. After all, I've got to ship our PiBoxes out soon, so I'll just bite the bullet and spend the money on a Zebra. But I've got 2 days until the Zebra arrives, so after dinner my mind is still wondering if there's a way to compile these drivers for arm64. Googling terms like
raster tspl pl60return very little of value. I even dug through the PPD file above, looking for clues.
*Manufacturer: "POLONO" *ModelName: "POLONO PL60" *ShortNickName: "HPRT N41"
Clues indeed! As it turns out, this printer is really a re-branded clone of the HPRT N41, or even it's sibling HPRT SL42! If you've read this far, you've probably realized that these printers do not support ZPL. The support page for the HPRT printer has a lot more technical details than Polono's site, and clues me into realizing that these printers instead support TSPL, which is a printer programming language much like ZPL. Only this language is from a printer manufacturer called TSC, which is based in Taiwan
I finally stumble upon some PHP code for talking to TSPL printers which gives me hope! Lots of stars, and some interesting notes about writing directly to the printer, e.g.
php hello-world.php > /dev/usb/lp0. This did get my printer to spit out paper, but the pages were blank 😢. But finally, I came across the Linux SDK TSPL for SL42 and even found a TSPL/TSPL2 Programming Manual!
Deciding to write my own driver
Even though I found the SDK, the
libsfolder inside were still pre-compiled for x86 or x64. This SDK seemed so promising, but why not give out the full source code?! Ok, it was time to get my hands dirty, and start reading through the 204-page TSPL Programming Manual. As I started reading I had a big realization that the only
filterincluded in the driver above was
raster-tspl. The "raster" part of that name tells me that they took a shortcut and only wrote drivers for printing raster images, such as bitmaps, PNGs, etc.
For shipping our PiBoxes, we decided on using EasyPost, which has a fantastic API for comparing shipping rates across carriers. When you purchase a shipping label through them, they give you the output in either a PDF, PNG, or ZPL format. ZPL would be ideal, since it's just ASCII text which can easily be modified, or even stored in a database along side each order. But that would require some sort of ZPL to TSPL converter script, which doesn't seem to exist, according to Google. At least not yet — leave a comment or send me a message if you want to help write one!
I had to make a choice of whether I wanted to take the same "raster only" shortcut or if I should try and write this converter script. This would require handling every ZPL function, and implementing at least enough TSPL functions (like rendering QR codes, barcodes, various ASCII fonts, etc) in order to print a scannable shipping label. I've got a whole business to run, and we are not in the shipping label industry — obviously I chose the shortcut.
Ok, now that I had a clear objective, I wanted to see if I could get a reference TSPL output file from a known PNG input file. Thankfully, Macs also use CUPS (Apple maintains it after all), and I was able to install the Polono printer driver on my Mac (rather humorously, my Mac is an ARM M1 chip, so it's emulating the x64 driver in Rosetta). CUPS has a helpful command line utility,
cupsfilterthat lets you run the filter script, and save the output to a file rather than straight to the printer.
cupsfilter test.png -p /Library/Printers/PPDs/Contents/Resources/PL60.tspl.ppd -m printer/foo -e > out.tspl
Awesome! I was able to take my
test.pngas an input, and get back an
out.tsplfile. I even copied this file over to the Raspberry Pi and tried writing out out via
cat out.tspl > /dev/usb/lp0and lo and behold... it printed the image! 🎉🎉🎉
Taking a look at the .tspl file in a text editor, we see a file that is mostly text lines, with one gigantic binary line. I even tried modifying some things, like adding a
SPEED 2line, and was able to get it to print a bit faster or slower, depending on the number. This is amazing... we are now communicating directly with the printer!
SIZE 99.8 mm, 149.9 mm SET TEAR ON SET CUTTER OFF SET PEEL OFF CLS BITMAP 0,0,100,1198,1,���...��� PRINT 1,1
My only question now: how does the filter turn the PNG image into that BITMAP line full of binary data? It really doesn't look like a standard bitmap format. I looked in the TSPL programming manual for the "BITMAP" function, and it actually didn't seem that complicated! It's a thermal printer, so each pixel is either black or white, with no grayscale or colors to worry about. And each pixel is represented by a single bit.
BITMAP 0,0,100,1198,1,���...���line above, we see that the binary data starts at X coordinate
0, Y coordinate
100bytes of data across, and
1198dots in length. This lines up with what we know, since there are 8 bits in a byte for a total of 800 dots in width, and the specs state
203 dpi (8 dots/mm), so ~4 inches in width. Same with the 1196 dots in length — this comes out to an image that's 4"x6", or a standard shipping label size. Excellent.
Converting the Image to a BITMAP Command
At the end you have a Buffer of bytes containing pixel data that you can add to the end of your
BITMAPline. To verify my results, I re-created the image that's the same as the example from the manual. I start with the same 16x16 image, but paint it on a canvas that's 800x1198, the same as our output label so we can get a 1-to-1 pixel mapping when we print.
Now, whether we run this image through our script using
node print.js test.pngor the manufacturer's driver using the
cupsfiltercommand above, we end up with the same binary file to send to the printer. We can inspect it using a hex editor:
And sure enough, we get 16 bits (2 bytes) of zeros, (
00 00in hex), followed by 98 bytes of ones (
FF FF FF FF FF ...). Each area selected in blue is the start of a new row of pixels (100 bytes). Let's convert these first few rows of hex back to binary, and see if it looks like the image we expect.
Hot damn, that looks like our test image! At the end of our JS script, we can even send the resulting file directly to the printer
and out comes a label. Now I just have a wrapper script that asks EasyPost for the PNG labels, saves the image to disk, and runs my printer script. The whole script takes ~2 seconds on a Raspberry Pi, which isn't as fast as compiled code, but plenty fast for our needs.
With a bit more work, I believe someone could turn this into a viable startup. After trying Stamps.com, Dymo, PirateShip, and a handful of others, I am convinced there are no good shipping hardware & software combos out there. All of it feels overly proprietary, and none of it is hackable or interfaces with the database I already have. I would love to see someone selling a kit with:
- A $35 Raspberry Pi for WiFi printing anywhere in the warehouse
- A cheap ~$100 Polono / HPRT printer
- A ~$60 wireless barcode scanner to associate serial numbers & automatically print shipping labels
- A web interface served from the Raspberry Pi so I can print labels from my phone / laptop
- A way to print a quick shipping label with address autocompletion (for one off orders)
- A simple API for sending addresses / labels that need to be printed from our ordering system
Here's my shipping station that checks all of those boxes. A lot of it is internal scripts that are hacked together, so I haven't taken the time to polish it and make it generic enough to release. If you want to see more details in a future blog post, please let me know!
If you want your very own label powered by this shipping station (well, along with a really cool product!), you can now pre-order a PiBox. It arrives loaded with everything you need to self-host apps at home. New orders are estimated to ship in July.