Retropie Console – Journeys in legacy software installation and USB conflicts

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.

Screenshot

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.

IdentityExampleWhat it tells usStability
Kernel device node/dev/ttyACM0The device class and the order in which the kernel discovered itConvenient, but the number can change at reboot or reconnection
USB bus addressBus 001 Device 006 from lsusbWhere the device is during this bootTemporary; the device number changes
Vendor and product IDvvvv:pppp from lsusbThe manufacturer and product typeUsually 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 numberUsually 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 connectedStable if the cabling and ports stay fixed; changes when the device is moved
udev propertiesID_SERIAL, ID_VENDOR_ID, ID_MODEL_ID, ID_PATHNormalized metadata that rules and services can matchAs stable as the underlying property
Sysfs path and attributes/sys/bus/usb/devices/...The kernel’s complete device tree, interfaces, driver and raw attributesAuthoritative for inspection; parts of the path reflect USB topology
Custom udev alias/dev/pixelcadeA human-readable link created from chosen matching propertiesStable 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 as tty, input, video4linux or block;
  • KERNEL — the kernel name pattern, such as ttyACM*;
  • ATTR{...} — an attribute on the current device;
  • ATTRS{...} — an attribute found while walking through a parent device; and
  • ENV{...} — a property such as ID_SERIAL produced 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:

  1. Pixelcade started with the cabinet.
  2. The BitPixel responded while browsing EmulationStation.
  3. System and game artwork continued to display.
  4. The Sinden software could claim its own serial interface.
  5. The two devices remained separate when ttyACM0 and ttyACM1 swapped.

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.