How to access a raw network printer as a file on Linux

I got this interesting question on my blog post ‘Setting up an Epson receipt printer‘:

Wondering if you had an idea on how to map a linux device to the netcat command so that I could “convert” the printer to be a local one? -Marco

I have previously written about the opposite use case: How to use a Raspberry Pi as a print server, where I used netcat to pass data to a local USB printer.

The setup

In this setup, I will assume that you have a working Ethernet printer, which accepts raw data on port 9100.


The aim is to make this appear as a file, so that you can print to it as if it were a local USB printer:


Before you begin

Make sure you have netcat. It’s not used for the real setup, but you will need it for testing. There are a few versions of this tool around, the one used here is:

apt-get install netcat

Verify that your printer accepts text on port 9100 via netcat:

echo "Hello world" | nc -q 1 192.168.x.y 9100

Set up a fake printer on localhost, and leave it running:

nc -klp 9100

Now, test that your fake printer shows output when you send it:

echo "Hello world" | nc -q 1 9100

Why I’m not using..


If you search for how to redirect data to a TCP socket, a common suggestion is:

echo "Hello world" > /dev/tcp/

This is a bash built-in, so unless your use case involves printing from bash, read on.


CUPS does not expose the printer as a file, which is the aim here. It also takes a few seconds to print, so we get much faster results if we remove CUPS from the loop and speak to the printer directly.

Use socat to move data

Socat is capable of making a FIFO file (‘pipe’), and writing this out over the network.

apt-get install socat

Redirect /tmp/my-printer to localhost:9100:

socat PIPE:/tmp/my-printer TCP:localhost:9100

Next, test that you can see the printer file and write to it. This line should appear on your local netcat “fake printer”:

echo "Hello world" > /tmp/my-printer

This file is a “pipe”, so it behaves very similarly to a character device.

If the connection is dropped, socat will exit, and the file will be deleted.

There are some big problems though:

  • If your printer is offline, and you try to print, you will create a regular file at “/tmp/my-printer”, breaking it.
  • This is not a self-restarting service, nor does it start on boot
  • Only the user who runs the command can print

Setting this up as a proper service

The usblp driver allows anybody from the lp group to print, so we will try to do something similar, and get a group-writable pipe:

$ ls /dev/usb/lp0  -Ahl
crw-rw---- 1 root lp 180, 0 Jun 12 12:51 /dev/usb/lp0

We don’t need to run socat as root, so make a new user called fileprint who is in the lp group.

useradd --groups lp fileprint

Add yourself to the lp group as well:

usermod -a -G lp mike

Next, write this systemd service file to /etc/systemd/system/fileprint.service


ExecStartPre=/usr/bin/mkfifo -m '0664' /var/run/fileprint/printer
ExecStart=/usr/bin/socat PIPE:/var/run/fileprint/printer TCP:

What’s going on here?

  • Runs as user fileprint and group lp
  • Manages a runtime directory at /var/run/fileprint
  • Creates a pipe that is group-writeable at /var/run/fileprint/printer
  • Forwards traffic to
  • Re-starts automatically

Load and start the service:

systemctl daemon-reload
systemctl start fileprint
systemctl status fileprint

Test out printing to your local netcat “fake printer”:

echo "Test" > /var/run/fileprint/printer

Note that if you are not in the lp group (check with groups command), you should expect a permission error here — just the same as usblp.

Next, replace with your real printer IP, reload systemd, and restart the service:

nano /etc/systemd/system/fileprint.service
systemctl daemon-reload
systemctl restart fileprint
systemctl status fileprint

Test it again, and your actual printer should print a line of text this time:

echo "Test" > /var/run/fileprint/printer

Finally, enable on boot:

systemctl enable fileprint

When the printer is unplugged, the /var/run/fileprint directory will vanish, so that you can see the missing printer as a “File Not Found” error — just the same as usblp.


This will hold a connection open at all times, which (depending on your printer) may prevent other computers from using it.

The 5890 printer does not support barcodes

I haven’t been able to print barcodes on the 5890 model thermal receipt printer, and it looks like other developers can’t either.

The same printer is sold by several companies. As far as I can tell (mainly from bug reports to escpos-php), these are all the same printer, and could have this issue:

  • AGPtEK SC35-5890F
  • EC Line 5890
  • Gainscha GP-5890
  • HSPOS HS-589C
  • POS-5890
  • WinBond 5890
  • Zjiang ZJ-5890 or POS-5890

In my case, I have a Zjiang POS-5890C, which list the barcode command GS k in its documentation, but does not actually respond to the command.

Separately, a developer has contacted the company and confirmed that this

A few bookmarks for reference:

Testing for barcode support in USB printers

Get yourself a Linux computer or VM, set up the printer with usblp, and run this command:

echo -e '\x1d\x6b\x04000\x00' > /dev/usb/lp0

This command prints a CODE39 barcode containing ‘000’ on printers which support barcodes, and prints garbage on printers which do not.

See also:

libgdx 3D particle effects in HTML


It is not immediately obvious in libgdx why the 3D particle effects don’t work in a HTML target. I’m sharing this snippet for future readers.

In short, the “reflection cache” that is created from the Java code does not include everything required, since many of the classes are only referenced at runtime, when the particle definition is loaded.

These class names are visible in the saved files from the 3D effects editor:

$ cat engine.p | fold -w 80

At runtime, this message is displayed on the web page:

GwtApplication: exception: com.badlogic.gdx.utils.GdxRuntimeException: Could not submit AsyncTask: Error reading file: (filename)
com.badlogic.gdx.utils.GdxRuntimeException: Could not submit AsyncTask: Error reading file: (filename)
Could not submit AsyncTask: Error reading file: (filename)
Error reading file: (filename)
Couldn't find Type for class '$Config'

The reflection is documented on the libgdx Wiki here, and notes that-

  • *.gwt.xml files store this data
  • dependencies defined in the Java are loaded automatically
  • inner classes are also loaded automatically, no need to add them separately

With this in mind, I added the following two lines to the root element in a file called GdxDefinition.gwt.xml

<extend-configuration-property name="gdx.reflect.include" value="" />
<extend-configuration-property name="gdx.reflect.include" value="" />

This did the trick, and the 3D particle feature does indeed work in the libgdx HTML target.

How to add a proper Android Studio launcher on Linux

If you install Android Studio by extracting a Zip file on Linux, it will come without any launch icon. This post assumes that have extracted the install archive to /usr/local and run any recent version of Linux which has Gnome:

unzip android-studio-ide-145.3537/ -d /usr/local

Simply write the following text to ~/.local/share/applications/android-studio.desktop (change paths if your install location differs).

[Desktop Entry]
Comment=Android Studio
Name=Android Studio

Once you save the file, it will appear in your local applications like this:


This can also be added graphically though the Alacarte menu editor if you have it installed.

OpenWrt setup on Netgear WNR2200

I recently wanted to connect some devices for a temporary setup, where a wireless LTE modem would provide Internet access. Unfortunately, one of the devices was not close enough to pick up the signal with its USB WiFi dongle.


Because the modem does not have a LAN port, the usual “run a cable” solution was out. There’s a few other options, from range extenders, to getting better modem, or just upgrading to a “real” USB WiFi dongle. Before purchasing new hardware, I decided to try re-purposing an old Netgear WNR2200 as a wireless client and 4 port switch.


In this setup, the LTE modem does the heavy lifting, with all of the wireless clients using it for LAN and Internet access. In the next room, the Netgear router is placed close enough to pick up the signal, and an Ethernet cable runs to the PC, beyond the reach of WiFi.

Deciding to re-flash

Replacing firmware is worth investigating when the hardware is capable, but you aren’t given the option to configure it the way you want.

The Netgear WNR2200 is a low end wireless router, and the vendor firmware does not support joining a WiFi network as a client.


It also pays to update your research. OpenWrt added support for this router a few days after I bought it, but I hadn’t looked it up again.

Uploading firmware

My main resource was this page on the OpenWRT Wiki. Firmware is organised by wireless chipset, then by router model.

The file I used to update my router was named openwrt-15.05.1-ar71xx-generic-wnr2200-squashfs-factory.img.

This is simply uploaded on the Adminisration → Firmware Upgrade screen:



First impressions

The first thing I noticed was that I lost WiFi, and that the page I had bookmarked for logging in was no longer valid!


This makes sense, of course. The configuration will not be carried across from the vendor firmware, and a different web administration tool is being used.

The Linux userspace is very rich compared with vendor firmware. It has things like dmesg, SSH, ifconfig, ping, and even a networked package manager.

Configuration checklist

I performed all configuration through the web in this setup. The “LuCi” interface allows setting the WiFi chip into “Client” mode, and then searching and joining a network. Once this was done, I assigned it as the “WAN” interface, so that it occupied a single IP address on the WiFi network, and providing a NAT and wired, four port switch.

There are more advanced, bridged setups that are possible. You should investigate this if you want one network, so that things like printer auto-discovery and internal SSH work consistently. I was only interested in sharing the Internet connection, which is why the setup was so simple.

What didn’t work

USB, but I didn’t spend long on this either. I was considering using USB to connect the modem to the Netgear router. The Wiki suggests that this is now possible, but after installing some packages for “USB tethering” and rebooting, I had no luck. Typing lsusb, only the “root hub” was listed, and the device was not getting any power.

This was necessary for the setup, so I just abandoned it. The vendor firmware couldn’t use the USB port for networking either, so no real loss.

libvirt: Migrate a VM from qemu:///session to qemu:///system

In recent versions of the libvirt virtualisation libraries, you to create and manage virtual machines as a regular user, using the qemu:///session connection.

This is great, but the networking is quite limited. I found that machines defined in Gnome Boxes could not speak to each-other, and that libvirt commands for networking were unavailable.

For this reason, I’ve written this quick guide for booting up an existing same VM image under the qemu:///system instance, which is faster than re-installing the machine. Unlike most sorts of migrations, this leaves the disk image at the same location on the same host machine.

There’s many different ways to do VM’s in Linux. This setup will be useful only if you use libvirt/kvm using qcow2 images on Debian. As always, consider doing a backup before trying new things.


First, find your virtual machine in virsh, and dump its configuration to a text file in your home directory, as a regular user.

$ virsh list --all
 Id    Name                           State
 -     foo-machine                    shut off
$ virsh dumpxml > foo-machine.xml

Now remove the VM definition from your user:

$ virsh undefine foo-machine
Domain foo-machine has been undefined

Import the definitions into virsh as the root user:

$ sudo virsh define foo-machine.xml 
Domain foo-machin defined from foo-machine.xml

Attempt to start the new VM definition. Depending on where the disk image is, expect an error.

$ sudo virsh start foo-machine

Disk images

The disk image needs to be accessible to the libvirt-qemu user. There’s two basic ways to achieve this: Re-permission the directories above it, or move it.

I chose to just re-permission it, since it’s not an issue to have world-readable directories on this particular box:

$ cat foo-machine.xml | grep source
      <source file='/home/example/.local/share/gnome-boxes/images/foo-machine'/>

This one-liner outputs the commands to run to make a directory work-navigable:

$ dir=`pwd`; while [ "$dir" != "/" ]; do echo "chmod o+x,g+x \"$dir\""; dir=`dirname $dir`; done
chmod o+x,g+x "/home/example/.local/share/gnome-boxes/images"
chmod o+x,g+x "/home/example/.local/share/gnome-boxes"
chmod o+x,g+x "/home/example/.local/share"
chmod o+x,g+x "/home/example/.local"
chmod o+x,g+x "/home/example"
chmod o+x,g+x "/home"

And the user account needs to be able to write as well:

$ sudo chown libvirt-qemu /home/example/.local/share/gnome-boxes/images/foo-machine

Once you have the permissions right, the VM should start, using the same command as before:

$ sudo virsh start foo-machine

More importantly, you can now hook up virt-manager and view your machine on qemu:///system, allowing you to configure the VM with any network settings you need.

How to print PDF417 codes with escpos-php

This post is a reference for printing PDF417 2-dimensional codes to a receipt printer, using the escpos-php.

I’ve got an older post about printing QR codes with escpos-php, which follows the same format and has some more background and links if you haven’t printed receipts from PHP before.

Straight the documentation, the syntax for the command that I’m demonstrating is:

pdf417Code($content, $width, $heightMultiplier, $dataColumnCount, $ec, $options)

Print a two-dimensional data code using the PDF417 standard.

  • string $content: Text or numbers to store in the code
  • number $width: Width of a module (pixel) in the printed code. Default is 3 dots.
  • number $heightMultiplier: Multiplier for height of a module. Default is 3 times the width.
  • number $dataColumnCount: Number of data columns to use. 0 (default) is to auto-calculate. Smaller numbers will result in a narrower code, making larger pixel sizes possible. Larger numbers require smaller pixel sizes.
  • real $ec: Error correction ratio, from 0.01 to 4.00. Default is 0.10 (10%).
  • number $options: Standard code Printer::PDF417_STANDARD with start/end bars, or truncated code Printer::PDF417_TRUNCATED with start bars only.

These PDF417 snippets above appear in the examples of escpos-php.

Simple example

A basic code that just says ‘testing 123’, and a demonstration of a narrower code that has been aligned:

// Most simple example
title($printer, "PDF417 code demo\n");
$testStr = "Testing 123";
$printer -> pdf417Code($testStr);
$printer -> text("Most simple example\n");
$printer -> feed();

// Demo that alignment is the same as text
$printer -> setJustification(Printer::JUSTIFY_CENTER);
$printer -> pdf417Code($testStr, 3, 3, 2);
$printer -> text("Same content, narrow and centred\n");
$printer -> setJustification();
$printer -> feed();

Error correction

This implementation accepts an error correction ratio as a percentage. The minimum is 1%, the highest is 400%, expressed as a decimal (0.01 – 4.00).

Higher error correction settings create lager codes that are more resilient to scanning errors due to damage.

// Demo of error correction
title($printer, "Error correction\n");
$testStr = "Testing 123";
$ec = array(0.1, 0.5, 1.0, 2.0, 4.0);
foreach ($ec as $level) {
    $printer -> pdf417Code($testStr, 3, 3, 0, $level);
    $printer -> text("Error correction ratio $level\n");
    $printer -> feed();

Pixel size (width)

The same example string, with some different module widths. Note that the blocks in the code scale in bot directions when the width is changed.

Larger print is easier for a scanner to read, but takes up more paper.

// Change size
title($printer, "Pixel size\n");
$testStr = "Testing 123";
$sizes = array(
    2 => "(minimum)",
    3 => "(default)",
    4 => "",
    8 => "(maximum)");
foreach ($sizes as $size => $label) {
    $printer -> pdf417Code($testStr, $size);
    $printer -> text("Module width $size dots $label\n");
    $printer -> feed();

Height multiplier

The height of the modules in the code can also be changed, stretching it do a different degree. PDF417 that are too vertically squashy are more prone to scanning errors.

// Change height
title($printer, "Height multiplier\n");
$testStr = "Testing 123";
$sizes = array(
    2 => "(minimum)",
    3 => "(default)",
    4 => "",
    8 => "(maximum)");
foreach ($sizes as $size => $label) {
    $printer -> pdf417Code($testStr, 3, $size);
    $printer -> text("Height multiplier $size $label\n");
    $printer -> feed();

Data column count

The of data columns to print in the code can be customised to produce a narrower code. But beware, if you request a code that’s too big for the paper, nothing will be printed!

// Change data column count
title($printer, "Data column count\n");
$testStr = "Testing 123";
$columnCounts = array(
    0 => "(auto, default)",
    1 => "",
    2 => "",
    3 => "",
    4 => "",
    5 => "",
    30 => "(maximum, doesnt fit!)");
foreach ($columnCounts as $columnCount => $label) {
    $printer -> pdf417Code($testStr, 3, 3, $columnCount);
    $printer -> text("Column count $columnCount $label\n");
    $printer -> feed();

Truncated code option

Use this setting to select the alternative, ‘truncated’ code format.

// Change options
title($printer, "Options\n");
$testStr = "Testing 123";
$models = array(
    Printer::PDF417_STANDARD => "Standard",
    Printer::PDF417_TRUNCATED => "Truncated");
foreach ($models as $model => $name) {
    $printer -> pdf417Code($testStr, 3, 3, 0, 0.10, $model);
    $printer -> text("$name\n");
    $printer -> feed();


To run the snippets, you need to initialise the printer, and define a title() function to print headings, like so:

/* Demonstration of available options on the qrCode() command */
require __DIR__ . '/autoload.php';
use Mike42\Escpos\Printer;
use Mike42\Escpos\PrintConnectors\FilePrintConnector;
$connector = new FilePrintConnector("/dev/usb/lp0");
$printer = new Printer($connector);

// ....

// Cut & close
$printer -> cut();
$printer -> close();

function title(Escpos $printer, $str) {
	$printer -> selectPrintMode(Printer::MODE_DOUBLE_HEIGHT | Printer::MODE_DOUBLE_WIDTH);
	$printer -> text($str);
	$printer -> selectPrintMode();

In the QR code post, I posted a fallback which used software rendering. As I don’t have a PHP-based PDF417 code library, you will need a printer which supports them to be blue to use these examples.

Good luck!

What’s in the AEC data feed, and how to use it

With an election coming up, it’s probably a good time to post some notes about using Australian election data feeds. This is mainly aimed at any interested programmers who have a use for this sort of data.

I’ve left out any example code here, as what you write will be specific to how you use the data.


The AEC publishes results on election night, by simply posting zipped files on an FTP server every few minutes. The detailed documentation is at

I have experimented with loading the data into a database during a recent by-election with some success. The data model that I assembled for this was:


You will be able to locate many electoral concepts in this data model, as candidates with a particular affiliation receive votes (contest eachother) at different polling places for a a seat in one of the houses. This maps to the more extensive XML format to extract the fields that I was most interested in mapping.

Getting the data

The FTP server is, and it accepts anonymous login on election night.

Here you will find a numbered folder for the night’s election. Within this, there is a lot of repetition, so you can ignore most of the files straight away. The sub-folders that I suggest loading are Detailed/LightProgress and Detailed/Preload. For example, in a past election, the file layout was:


Loading the data

Start with the Preload data. This zip file is available first, and contains the candidate names, polling places, event details, and an initial (zeroed-out) results feed. Using a custom script and a schema like the one I’ve posted above, you will be able to import this into your database.

Next, the LightProgress feed updates will start being added every few minutes from 7pm. These Zip files contain a small piece of XML, each one superseding older files.

I’ve cut down one of these files as an example:

<?xml version="1.0" encoding="utf-8"?>
<MediaFeed Id="e9334806-990e-4773-8b00-7d74fa58b6af" Created="2015-09-19T19:55:00" SchemaVersion="3" EmlVersion="5" xmlns="" xmlns:eml="urn:oasis:names:tc:evs:schema:eml" xmlns:ds="" xmlns:xal="urn:oasis:names:tc:ciq:xsdschema:xAL:2.0" xmlns:xnl="urn:oasis:names:tc:ciq:xsdschema:xNL:2.0" xmlns:ts="urn:oasis:names:tc:evs:schema:eml:ts" xmlns:xs="" xs:schemaLocation=" ../Schema/AEC/aec-mediafeed-results-v3-0.xsd">
    <eml:AuthorityIdentifier Id="AEC">Australian Electoral Commission</eml:AuthorityIdentifier>
    <Name>Virtual Tally Room</Name>
  <Cycle Created="2015-09-19T19:54:47">37f177b4-ab20-4073-be85-0edefa5c8e96</Cycle>
  <Results Phase="ElectionNight" Verbosity="LightProgress" Granularity="Detailed">
    <eml:EventIdentifier Id="18126">
      <eml:EventName>Canning By-election</eml:EventName>
      <eml:ElectionIdentifier Id="H">
        <eml:ElectionName>House of Representatives By-election for the division of Canning</eml:ElectionName>
          <Contest Projected="true">
            <eml:ContestIdentifier Id="236">
            <FirstPreferences PollingPlacesReturned="0" PollingPlacesExpected="53">
                <eml:CandidateIdentifier Id="25424" />
                <Votes MatchedHistoric="0">0</Votes>
                  <Votes Type="Ordinary">0</Votes>
                  <Votes Type="Absent">0</Votes>
                  <Votes Type="Provisional">0</Votes>
                  <Votes Type="PrePoll">0</Votes>
                  <Votes Type="Postal">0</Votes>
              <!-- (other candidates, ghost candidates) -->
                <Votes MatchedHistoric="0">0</Votes>
                  <Votes Type="Ordinary">0</Votes>
                  <Votes Type="Absent">0</Votes>
                  <Votes Type="Provisional">0</Votes>
                  <Votes Type="PrePoll">0</Votes>
                  <Votes Type="Postal">0</Votes>
                <Votes MatchedHistoric="0">0</Votes>
                  <Votes Type="Ordinary">0</Votes>
                  <Votes Type="Absent">0</Votes>
                  <Votes Type="Provisional">0</Votes>
                  <Votes Type="PrePoll">0</Votes>
                  <Votes Type="Postal">0</Votes>
                <Votes MatchedHistoric="0">0</Votes>
                  <Votes Type="Ordinary">0</Votes>
                  <Votes Type="Absent">0</Votes>
                  <Votes Type="Provisional">0</Votes>
                  <Votes Type="PrePoll">0</Votes>
                  <Votes Type="Postal">0</Votes>
            <TwoCandidatePreferred Restricted="true" PollingPlacesReturned="0" PollingPlacesExpected="53" />
                <CoalitionIdentifier Id="2" />
                <CoalitionIdentifier Id="1" />
                <PollingPlaceIdentifier Id="7467" />
                <FirstPreferences />
                <TwoCandidatePreferred Restricted="true" />
              <!-- (other polling places, ghost candidates) -->

Spatial data

You can also download electorate boundaries as spatial data here to back any visualisations of the results.

The locations of polling places are already encoded in the Preload data file.

Good luck!