Printing pictures like its 1873 using Oki 3321 dot-matrix printer

Steinway hall 1873
As wikipedia says oldest halftone image printed in a newspaper back in 1873

Long, long time ago, before prices of inkjet and laser printers fell to levels allowing home users to own and use them, there was a primitive printing technology called dot-matrix. As any technology of the past, it is not competitive anymore. However it still has few advantages and one of them is reliability of these devices. Some time ago I found quite a cheap Oki 3321 printer that has 9 pin head and is capable of printing on A3 paper in portrait orientation. Usual mode of printing for these devices was simple text mode, where you just were writing your text in ASCII (or any weird coding popular in your country of origin) to its parallel port. Fortunately these printers usually had also graphic mode, where you could fully use capabilities of the device.

I already was experimenting some time with my device, so I already know it uses Mazovia variant (with zł as single glyph) as its codepage. I was also able to guess how to switch into graphic mode, so in theory I was able to print images for some time. Unfortunately any CUPS drivers I used did not provide acceptable results, so all I could do was to write some support tool myself.

Meet png2lp

png2lp is a tool that (as its name suggests) converts PNG images to format understandable by line printers. I tried to write it in a way that allows further extensions, so in theory it is possible to write support for a printer that works completely different way than mine. There is only one limitation – PNG image that comes as input must be saved in indexed mode with only two colors in a table. So, you cannot support printer that can print more colors or shades than one (or you could but png2lp can’t give you input of sufficient quality).

Usage is quite straightforward – you supply filename as a parameter or give filename “-” and put data on standard in. On stdout, you get converted image. As program supports more than one output format, you have to specify desired format also. You can list available sinks by typing:

png2lp -l

Then you can choose your printer from the list and list available page formats:

png2lp -L oki3321

And finally you can convert the image by typing:

png2lp -p oki3321 -P a4-p 484px-Tux_mono.svg.png

Converting ordinary images to 1bit PNG

Tux logo
Tux logo (source:

As you might have noticed, there are not many 1bit PNG images these days, so what you might want to do is to convert some existing image to what png2lp expects. It is easiest with SVG images, as they are quite flexible and you won’t loose much of a quality during conversion. In case of Oki 3321 and probably also other Oki printers from these days, maximum resolution I was able to get for portrait A4 page is 484×760 dots. However in practice I can see the image printed is not exactly proportional. This is a thing that should not be fixed in png2lp itself, as this might break more precise images. So, we have to manage this problem during conversion to input PNG image.

Let’s take well known Tux logo as an example. It can be downloaded from Wikipedia as SVG, then we can use ImageMagick to do all the transformations at once:

convert Tux_Mono.svg -resize 90%x106% -resize 484x760 -monochrome 484px-Tux_mono.svg.png
484px Tux logo
Tux converted to 1bit PNG and stretched to match printer’s distortion

After that we can try to pipe result of png2lp example above to our line printer device, that is usually /dev/usb/lp0 or /dev/lp0 on Linux. One note here: png2lp does not produce form feed character to allow potential embedding its results into bigger page. You have to write it manually or press button on your printer for it to give the page back to you!


Tux printed on Oki 3321
Printed Tux

As can be seen in the picture above, there is quite a huge margin on the bottom. There is not much that can be done with it, as if I try to write something there, page sensor disconnects and printer stops printing until new page is fed. In theory it is possible to override this behavior by pressing select button few times (every press allows printing one additional line).

Another thing we can see here is exceptional quality of the printout 🙂 With this, we can do even less, as this page was printed using so called “high quality” mode.

Output format

Finally, few words about format of the input data printer have to receive. They are based on escape codes, so at first may seem similar to ANSI escape codes used in terminals for e.g. coloring or presenting window-like experience with help of ncurses. Despite that, I don’t think any of these codes were standardized somehow. At least some part of them come from IBM printers, so may be compatible with many devices of different vendors, as IBM was the one creating standards at that time.

For printing in graphical mode there are two codes, I am aware of, that allows to print virtually everything: \eK and \eJ. Their formats are as follows:

struct K {
  uint8_t esc;
  uint8_t K;
  uint16_t columns;

struct K k = {'\033', 'K', htole16(columns)};

struct J {
  uint8_t esc;
  uint8_t J;
  uint8_t offset;

struct J j = {'\033', 'J', 0x18}; // 0x18 - experimentally found magic

mhz14a – program for managing MH-Z14/MH-Z14A CO2 sensors via UART

MH-Z14A CO2 sensor

When I have seen CO2 sensor for the first time, it was quite expensive device. Well, if one want to buy consumer device these days, it still could cost a lot. However in the days of cheap Chinese electronics sellers on biggest auction platforms, for makers, situation is quite different now. MH-Z14 is the cheapest CO2 sensor I was able to find. I costs about $19 and comes in few variants: MH-Z14 and MH-Z14A. Also it can measure up to 1000 ppm, up to 2000 ppm or up to 5000 ppm. However the range does not matter in practice, as it is possible to switch between them using UART.

The device interfaces are quite flexible for such a cheap device, as beside mentioned UART port it provides PWM and analog output. However, I was not able to measure valid value using analog and my cheap multimeter. Maybe some more sophisticated equipment is required for that.

I have to make one note here: device I bought is labeled as MH-Z14A and its range is 0-5000 ppm. Other variants might have different features. For mine, there is no UART protocol documentation. Yet, protocol documented under name MH-Z14 works, so be careful.

mhz14a – UART protocol implementation

As UART protocol of the device is quite complex and what is more important a binary one, it is required to communicate with the device programatically. It is impractical to use just a terminal application. For the purpose I created mhz14a program. It wraps all the internals with nice getopt-based interface, so can be used from both interactive shells and scripts. What user has to know is what is his/her UART device path and choose desired command (most likely read – -r).

At first program have to be compiled. For now installation in a system is not possible, so compilation is going to leave binary in build/ directory. Project uses cmake, so to compile one have to execute:

mkdir -p build && cd build
cmake ..
src/mhz14a --help

To sum up, basic usage of the program is like below (white are the commands I type):

$ src/mhz14a -r -d /dev/ttyUSB0

Device documentation

Some sellers provide some basic guide of what is where themselves, so digging for a datasheet might be unnecessary. Fortunately, even if they don’t provide anything, Winsen, who is their manufacturer, provides everything needed. In this case, what is nothing new in the world of Chinese manufacturers, it is worth to see Chinese language variant instead of the English one, as it contain more information.

Final words

For the moment, I don’t have commands described only in Chinese implemented, so it is not possible to switch supported ranges and turn off automatic calibration. However this should not be issue in most cases, as what most users need is just to read sensor data.

I might do some work with the sensor in future, as my initial idea was to switch to analog output just after setting up UART. Unfortunately I have problems with analog pin readings. If I decide to develop the project again, I am definitely going to implement these hidden commands, as well as do some refactoring, as code sometimes looks really bad (thankfully, I was writing some unit tests, thanks to one of my previous projects – cmocka/cmake template, which I used here).