In this arcade project, I’ve chosen to use deprecated solutions often not out of choice, but because despite my best searches, other tools and options seemed not to be available at the time, and I haven’t had time or capacity to build my own.

I’m a big fan of reuse. Why reinvent the wheel when most of the work has been done already and all we need to do is a tweak?
I did this with RGB Commander, and I’ve somewhat done something similar with Pixelcade.
When I found Pixelcade, I attempted an initial install and failed with the V2 instructions. While trying to figure this out, I stumbled on a previous version of the installation page at https://pixelcade.org/pi-install/
Legacy Version – DO NOT USE
There is a particular kind of anxiety that comes from revisiting the installation guide for a working piece of software and finding a large warning at the top. Let’s give it a try, and if I fail, I’ll figure out how to clean this up.
Our cabinet is not a new installation. It’s been slowly curated, cleaned up, and augmented for several years.
It has a working AtGames Legends BitPixel display integrated with RetroPie, EmulationStation, a Raspberry Pi 4, and a Sinden Lightgun among other devices. It starts automatically, follows the system and game selection in the front end, displays artwork, and exposes its companion interface over the network.
I’m documenting this part of the journey for anyone who may be in a similar situation.
What the Pixelcade adds to the cabinet
The BitPixel acts as an active marquee. As we move between systems and games in EmulationStation, the display changes to matching artwork. When a game starts, Pixelcade can show its marquee, scroll “Now Playing” text, and—in supported MAME configurations—cycle high scores.
This behavior is not implemented by one isolated program. Pixelcade sits across several layers of the cabinet:
USB BitPixel hardware
↕
Linux device identity and udev rules
↕
Pixelcade listener and artwork
↕
RetroPie startup configuration
↕
EmulationStation event scripts
↕
System and game selection
Which architecture and how do we get USB consistent?
The original Java-based installation got the BitPixel running, but the Sinden Lightgun exposed a USB serial-port conflict. Solving that eventually led us to Pixelcade’s newer native listener, an architecture mismatch, a stable device path, and a startup configuration that survives USB numbering changes.
The Raspberry Pi 4 and the version of the OS, which I’m using for this project, has a 64-bit kernel and a 32-bit user space, which means we have to be careful about the software we install. Looking only at uname -m made the machine appear to need an ARM64 program. Programs installed through the operating system actually needed 32-bit ARM binaries.
To be clear, the 32-bit user space is a result of the old operating system I’m using behind RetroPie. We don’t specifically need a fully 64-bit operating system.
Rant: I find it frustrating that there isn’t an upgrade path other than wiping and starting new with Raspberry Pi OS. There’s got to be a better way of migrating easily. Especially given we could test on Raspberry Pi OS 12 Bookworm which supports both 32-bit and 64-bit executables on the Pi4, so we can gradually upgrade to 64-bit emulators which can then better leverage the Mesa and OpenGL drivers and Vulkan support. That, is for another day.
The legacy Pixelcade instructions did work. Their installer did much more than copy one program. It installed Java, created /home/pi/pixelcade, copied artwork, added a Pixelcade listener to RetroPie’s startup, installed fonts, and added EmulationStation event scripts.
This worked beautifully, although Pixelweb, which allows us to download updated artwork, upgrade the software and control the Bitpixel display independent of the console didn’t work.
Still, that gave us an important baseline: We were able to confirm the BitPixel hardware, its USB connection, the artwork, and the EmulationStation integration were all capable of working on the cabinet.
The Sinden Lightgun uses both a camera device and a USB serial interface. When we began configuring it, the gun appeared to be connected correctly, but its software could not claim the serial port it needed.
We checked which process owned the device:
sudo fuser -v /dev/ttyACM0
ps -ef | grep '[j]ava'
The result identified the conflict:
/dev/ttyACM0: java
java -jar pixelweb.jar -b -s
Pixelcade’s documentation specifically calls out potential conflicts with devices such as Sinden lightguns and USB volume-control boards.
Pixelcade’s Java listener was scanning the available serial devices and had opened the Sinden’s port. The marquee was working, but it had accidentally claimed the lightgun.
Temporarily stopping pixelweb.jar released the port and let the Sinden software proceed. That gave us a useful diagnostic split: both devices worked independently, but automatic serial discovery allowed one to interfere with the other.
Device numbers were not identities
With both devices connected, Linux created paths such as:
/dev/ttyACM0
/dev/ttyACM1
At one point the Sinden was ttyACM0 and the Pixelcade IOIO controller was ttyACM1. After a reboot, they could swap.
Hard-coding – which I’m never a fan of – one of those numbers would therefore make the fix temporary. We needed to identify the hardware rather than its discovery order.
Linux already provided stable paths under /dev/serial/by-id/:
/dev/serial/by-id/usb-IOIO_Open-Source_Project_IOIO-if00
/dev/serial/by-id/usb-Unknown_SindenLightgun_HIDLG-if00
The first path identifies the Pixelcade’s IOIO interface. The second identifies the Sinden. The names continue to point to the right hardware even when the underlying ttyACM numbers change.
The Pixelcade udev rule may additionally create an alias named /dev/PIXELCADE0 or /dev/PIXELCADE1. We saw /dev/PIXELCADE1 point to the same ttyACM1 device as the IOIO by-id path during troubleshooting. We did not use either numbered Pixelcade alias in the final startup command; the IOIO by-id path is more explicit and remained correct when the raw device numbers swapped.
The different ways Linux can recognize a USB device
Linux exposes several views of the same USB hardware. They answer different questions, and not all of them are equally suitable for configuration.
| Identity | Example | What it tells us | Stability |
|---|---|---|---|
| Kernel device node | /dev/ttyACM0 | The device class and the order in which the kernel discovered it | Convenient, but the number can change at reboot or reconnection |
| USB bus address | Bus 001 Device 006 from lsusb | Where the device is during this boot | Temporary; the device number changes |
| Vendor and product ID | vvvv:pppp from lsusb | The manufacturer and product type | Usually stable for a model, but not unique when two identical devices are attached |
| Serial-based link | /dev/serial/by-id/... | Identity derived from USB descriptors, often including a serial number | Usually the best application path when the device supplies a useful unique identity |
| Physical-path link | /dev/serial/by-path/... | The USB port and hub route through which the device is connected | Stable if the cabling and ports stay fixed; changes when the device is moved |
udev properties | ID_SERIAL, ID_VENDOR_ID, ID_MODEL_ID, ID_PATH | Normalized metadata that rules and services can match | As stable as the underlying property |
| Sysfs path and attributes | /sys/bus/usb/devices/... | The kernel’s complete device tree, interfaces, driver and raw attributes | Authoritative for inspection; parts of the path reflect USB topology |
Custom udev alias | /dev/pixelcade | A human-readable link created from chosen matching properties | Stable when the rule uniquely identifies the intended hardware |
Other device classes have equivalent link directories. Game controllers may appear under /dev/input/by-id/ and /dev/input/by-path/; cameras can have /dev/v4l/by-id/; storage devices add identities such as /dev/disk/by-id/, /dev/disk/by-label/, and /dev/disk/by-uuid/. A filesystem UUID is excellent for mounting a drive, but it does not identify a serial controller or camera.
There are two important caveats. A vendor/product pair identifies a type of device rather than one physical unit. Also, some hardware does not provide a unique serial number. In those cases, by-id may not distinguish two identical units, and by-path or a rule tied to a known physical USB port may be the better choice.
A single USB product can also be a composite device: one physical cable can expose a serial interface, camera, audio device, keyboard or several of these at once. The interface number—often visible as if00, if01, or the ID_USB_INTERFACE_NUM property—can therefore matter just as much as the vendor, product and serial values.
These commands show the device from several angles:
lsusb
lsusb -t
ls -l /dev/serial/by-id/ /dev/serial/by-path/
readlink -f /dev/serial/by-id/usb-IOIO_Open-Source_Project_IOIO-if00
udevadm info --query=property --name=/dev/ttyACM0
udevadm info --attribute-walk --name=/dev/ttyACM0
lsusb is useful for discovery. lsusb -t shows the hub topology and active drivers. udevadm info connects a /dev node to the properties and parent attributes from which a durable rule can be built.
A short primer on udev
The kernel detects the hardware and emits an event. The userspace device manager, udev, receives that event, evaluates its rules, sets permissions and creates the useful symbolic links under /dev. In other words, /dev/serial/by-id/... is not a second serial device—it is another name pointing to the same underlying ttyACM node.
Distribution and package rules normally live under /lib/udev/rules.d/ on this Buster system. Local rules belong under /etc/udev/rules.d/, which keeps cabinet-specific changes separate from files managed by installed packages. Rule files are processed in lexical order, so a descriptive late filename such as 99-arcade-serial.rules is common.
A rule consists of conditions followed by assignments. Match operators such as == ask whether the event belongs to the desired device. Assignment operators set a value, while += adds another value without replacing an existing list.
Common match fields include:
ACTION— whether the device is being added, removed or changed;SUBSYSTEM— such astty,input,video4linuxorblock;KERNEL— the kernel name pattern, such asttyACM*;ATTR{...}— an attribute on the current device;ATTRS{...}— an attribute found while walking through a parent device; andENV{...}— a property such asID_SERIALproduced during rule processing.
Useful assignments include SYMLINK+= for an additional friendly path, GROUP= and MODE= for access, and TAG+="systemd" when the device should participate in a systemd-managed workflow.
For example, after confirming the exact properties reported for the IOIO interface, a local rule could take this shape:
SUBSYSTEM=="tty", KERNEL=="ttyACM*", \
ENV{ID_SERIAL}=="IOIO_Open-Source_Project_IOIO", \
ENV{ID_USB_INTERFACE_NUM}=="00", \
SYMLINK+="pixelcade"
The values above must be checked against udevadm info on the actual cabinet before installing the rule. A good rule uses enough properties to select exactly one intended interface without depending on its current ttyACM number. If two devices report the same serial metadata, a physical ID_PATH match may be necessary instead.
After saving a rule, reload the rules and reconnect the device:
sudo udevadm control --reload-rules
# Unplug and reconnect the device, then:
udevadm settle
ls -l /dev/pixelcade
For live diagnosis, this shows the kernel event followed by the processed udev event and its properties:
sudo udevadm monitor --kernel --udev --property
There are a few practical boundaries worth preserving. A udev rule should identify the device and establish access; it should not run a long-lived application directly with RUN+=. A systemd service is a better home for a listener such as Pixelcade. Likewise, broadly setting a serial device to world-writable mode is usually unnecessary: giving the service account membership in the appropriate group, commonly dialout, is the cleaner permission model.
Finally, identity and ownership are separate questions. udevadm tells us which device this is. Tools such as fuser or lsof tell us which process currently has it open. We needed both views to solve the Pixelcade/Sinden conflict.
The rule became simple:
Pixelcade → usb-IOIO_Open-Source_Project_IOIO-if00
Sinden → usb-Unknown_SindenLightgun_HIDLG-if00
Java to Native Application
When testing the Java version of Pixelweb, we found that despite indicating the serial/by-id device, the ttyACM ports would still be held.
The native application provides an explicit -d option for selecting the Pixelcade device by-id. This is the style documented by the current V2 instructions for systems that also have devices such as a Sinden Lightgun.
The native listener offered a cleaner boundary:
./pixelweb \
-d /dev/serial/by-id/usb-IOIO_Open-Source_Project_IOIO-if00 \
-image system/retropie.png \
-startup &
As I alluded to earlier, there was just one problem: Pixelweb would not run.
A misleading “No such file” error
The file was plainly present and executable:
ls -l /home/pi/pixelcade/pixelweb
Yet running it produced:
-bash: ./pixelweb: No such file or directory
That message does not always mean the requested file is missing. Linux also reports it when the executable requires a loader that does not exist on the system.
The installed pixelweb binary was built for the wrong architecture. The Raspberry Pi reported an aarch64 kernel, which suggested 64-bit ARM, but its Buster userspace was 32-bit armhf. A 64-bit Pixelcade binary cannot be loaded by the 32-bit userspace.
The checks that mattered were:
uname -m
dpkg --print-architecture
file /home/pi/pixelcade/pixelweb
The combination was:
Kernel: aarch64
Userspace: armhf
Required: 32-bit ARM executable
We replaced the incompatible listener with Pixelcade’s ARMv7 Raspberry Pi build from its official build repository. The working file identified as:
ELF 32-bit LSB executable, ARM, EABI5
interpreter /lib/ld-linux-armhf.so.3
The misleading missing-file error disappeared, and the native listener could finally be started with an explicit device.
The working startup configuration
The functional Pixelcade line in autostart.sh became:
cd /home/pi/pixelcade && \
./pixelweb \
-d /dev/serial/by-id/usb-IOIO_Open-Source_Project_IOIO-if00 \
-image system/retropie.png \
-startup &
EmulationStation starts afterward:
emulationstation #auto
This arrangement does four useful things:
- starts Pixelcade automatically with RetroPie;
- uses the native listener instead of
pixelweb.jar; - assigns Pixelcade to its exact IOIO hardware identity; and
- allows EmulationStation to start while Pixelcade remains in the background.
The old Java line was removed so that both listeners could not start together.
We also corrected the physical USB arrangement so the Pixelcade was no longer buried behind the cascaded external hub. The Sinden remained directly connected. Stable software identities solved the numbering problem, while the cleaner physical layout reduced another source of uncertainty.
The working legacy startup logic does not assume that Pixelcade will always appear at one generic serial path. It looks for:
Proving it survived a reboot
Testing before reboot was not sufficient. The entire problem involved discovery order, so a cold start was part of the acceptance test.
After reboot, the raw numbers did in fact change. The IOIO interface became ttyACM0 and the Sinden became ttyACM1. The stable by-id paths continued to point to the correct devices.
The running Pixelcade process showed the intended command:
./pixelweb -d /dev/serial/by-id/usb-IOIO_Open-Source_Project_IOIO-if00 \
-image system/retropie.png -startup
Port ownership also showed the desired separation:
Pixelcade pixelweb → IOIO serial device
Sinden Mono app → Sinden serial device
We then checked the behavior that actually mattered:
- Pixelcade started with the cabinet.
- The BitPixel responded while browsing EmulationStation.
- System and game artwork continued to display.
- The Sinden software could claim its own serial interface.
- The two devices remained separate when
ttyACM0andttyACM1swapped.
That was the point at which the setup was working.
Legacy does not mean worthless
The installation began with the legacy V1 procedure because the V2 installer did not work on this cabinet. Thinking about it, I wonder if it may have had to do with the mixed 64-bit/32-bit nature of the current setup.
Preservation matters. We used a legacy installer to establish a working baseline, allowed the software to evolve, adopted the newer native listener when it solved a real limitation, selected the build that matched the actual userspace, and pinned it to the hardware identity
The Pixelcade folks keeping the legacy instructions and having an upgrade path to the newer version is awesome. To me, this is how software should be developed with their upgrade paths. I can’t express how much I appreciated this.
The warning on Pixelcade’s old page is useful. It tells new users that there is a supported path they should use instead. It also tells owners of existing installations that the assumptions behind their systems are aging and deserve attention.
The BitPixel now behaves like part of the cabinet. It starts automatically, follows the front end, displays its artwork, and leaves the Sinden alone.
This is, yet, another reason why I believe in preservation, archiving old releases and making those releases available for reuse. Two examples stand out as part of this project – RGB Commander which I found via the Internet Wayback Machine, and now Pixelcade.