Skip to content

Instantly share code, notes, and snippets.

@savetheclocktower
Last active February 16, 2024 15:22
Show Gist options
  • Save savetheclocktower/c7644597c9c4c3990b7fee363d5f03c1 to your computer and use it in GitHub Desktop.
Save savetheclocktower/c7644597c9c4c3990b7fee363d5f03c1 to your computer and use it in GitHub Desktop.
Poor-man's LEDBlinky with RetroPie and Pac-Drive

NOTE: This Gist was an early write-up of this blog post, part of what became an eleven-part series on my arcade cabinet. I'd suggest you read that post instead of this, but some of the comments on this Gist contain updates and field reports that you might find useful.

RetroPie, LED control, and you

I wanted LEDBlinky-style functionality out of my RetroPie cabinet. But I didn't need RGB control or magical frontend integration or anything like that. I had buttons with simple single-color LEDs.

I've got a simple control panel with six buttons per player. All I wanted was this:

  • When I launch Street Fighter 2, all twelve buttons should light up.
  • When I launch The Simpsons, only the first two buttons for each player should light up.
  • When I launch Pac-Man, none of the buttons should light up.

You get the idea.

Here's a demonstration in the form of a shaky video.

Here's how I did it.

Hardware

I got a Pac-Drive from Ultimarc. It's got 16 terminals for LEDs. 1–6 control Player 1's buttons; 7–12 control Player 2's buttons. The remaining four terminals got devoted to the start buttons (13 and 14), the coin buttons (both on 15), and four small buttons I use for functions (all on 16).

After all that's wired up, all you need is a single USB cable running to your Pi — or, even better, to a powered USB hub that is itself connected to the Pi. The LEDs get powered from USB.

Software

Most of the software for the Pac-Drive is meant for a Windows environment. After some digging, I found this third-party utility for Linux that allows for controlling a Pac-Drive from the command-line. It was exactly what I needed, though it was eight years old and took some wrangling to get installed on a Pi.

From a stock RPi with Raspbian Jessie, you'll need at least these packages:

sudo apt-get install libusb-dev build-essential

You'll also need libhid, which is hard to find because it's deprecated. There's no package for it in APT, the source code is behind a login for some reason, and then you've got to make a change to the source to get it to compile on the Pi. I solved all three problems by applying that fix and then putting up the fixed source on my server.

From your home directory:

mkdir src && cd src

wget "http://andrewdupont.net/pi/libhid-0.2.16.tar.gz"
tar xvzf libhid-0.2.16.tar.gz
cd libhid-0.2.16
./configure
make
sudo make install

Now we're ready to install the pacdrive utility.

cd ~/src
wget "http://www.zumbrovalley.net/pacdrive/dnld/pacdrive_0_2.tar.gz"
tar xvzf pacdrive_0_2.tar.gz
cd pacdrive_0_2
make

If you do make install it'll put the pacdrive binary in /usr/bin, though you can edit the Makefile beforehand to specify a different directory. I did neither; I just manually copied the pacdrive binary to /home/pi/bin. (If you do this, make sure you put /home/pi/bin in your PATH.)

Hooking into RetroPie

So now you can control the LEDs from the command line, but that doesn't help much.

How will it know which LEDs to light up for which game?

One option here would be to use something called controls.dat, which is a project for cataloguing the controls for most MAME games. (4-way joystick? 8-way? How many buttons? How many players? Hotseat, or does each player have their own controls? And so on.)

The problem with this approach is that (a) the controls.dat project is dormant; (b) there are lots of gaps in its database.

If you're anything like me, you don't need a comprehensive solution. I don't care how many buttons some random mah-jongg game uses because I'll never play it. I've got about 100 arcade games set up and I don't mind specifying their configs manually.

So here's what I came up with:

  1. Create a /home/pi/leds directory.
  2. Inside that directory, create another directory for each system you want to control. (I've got arcade and daphne; you might have others.)
  3. Inside each system's directory, define a text file whose name matches the name of the game. For instance: simpsons2p is the ROM name for the two-player version of The Simpsons, so I'd create a file called simpsons2p.cfg with these contents: 1, 2, 7, 8. In other words: light up buttons 1, 2, 7, and 8, and turn off all the other LEDs.

This way it's simple to define new configs and simple for RetroPie to know where to look for configs knowing only the system and the ROM name.

How will it light up the LEDs on game launch?

All emulator launches go through something called runcommand.sh. There's a new feature in RetroPie 4.x: the ability to define scripts called runcommand-onstart.sh and runcommand-onend.sh. These scripts will run before a game starts and after a game ends, respectively, and they receive some metadata, including the name of the system and the name of the game.

If you want to do it exactly how I did:

  1. cd /opt/retropie/configs/all
  2. nano runcommand-start.sh (shouldn't need sudo, but I might be wrong)
  3. Paste the contents of the runcommand-onstart.sh from this gist.
  4. Repeat steps 3 and 4 for runcommand-onend.sh.

These scripts reference two other scripts called led-start and led-end, which you should put in /home/pi/bin. This is completely overengineered, but it lets me define some buttons as "always on" without having to include them in every single config file. That happens in a file called /home/pi/.ledrc. Here's what mine looks like:

+13, +14, +15, +16

That means "always light up both start buttons, the coin buttons, and the small function buttons." You can read the source of led-start to learn more.

If you want to use these scripts, you'll need Ruby, which isn't installed by default on Raspbian Jessie. sudo apt-get install ruby2.1 should take care of it.

What if a game doesn't have a config? Well, if you followed these instructions to the letter, none of the buttons for either player will light up. That's fine with me, because I don't have any games without configs. You might want it to do something different in that situation.

Problems?

If this doesn't work, it's probably because I left something out. Leave a comment and we'll figure it out together.

#!/usr/bin/env ruby
# For now, let's just turn all LEDs back on.
exec(%Q[/home/pi/bin/pacdrive -a -q])
#!/usr/bin/env ruby
require 'optparse'
require 'pathname'
require 'pp'
$options = {
debug: false,
debug_path: '/home/pi/led_log.txt'
}
# Takes a string like "1,2,3,7,8,9" and determines which LEDs should be lit
# up. Shells out to the `pacdrive` binary to do it.
$usage = OptionParser.new do |opts|
opts.banner = "Usage: led-start [OPTIONS] system game"
opts.separator ""
opts.on('-d', '--debug', "Writes debug output to the specified file (defaults to ~/led_log.txt).") do |path|
$options[:debug] = true
$options[:debug_path] = path unless (path.nil? || path.empty?)
end
opts.on_tail('-h', '--help', "Displays this help message.")
end
$usage.parse!
# The path to the file that sets defaults for LED state. If no config is
# found for a certain game, this file ends up determining all states. If a
# game config omits a certain button, the default config can specify that
# that button should nonetheless be on. (This is useful for, e.g., coin
# buttons, aux buttons, and such. These should almost always be on, but it's
# dumb to have to specify them in every game config file.)
DEFAULT_CONFIG_PATH = Pathname.new('/home/pi/.ledrc')
# The base directory where all configs are stored. Each system should be its
# own folder; each game should be a text file in that folder whose name
# matches the game's name and has a "cfg" extension.
BASE_CONFIG_PATH = Pathname.new('/home/pi/leds')
class LEDState
def initialize(path)
@contents = path.is_a?(String) ? path : File.read(path).chomp
@button_map = {}
debug "config file contents: #{@contents}"
buttons = @contents.split(/,\s*/)
(1..16).each do |digit|
state = { value: 0, force: false }
if buttons.include?(digit.to_s)
state = { value: 1, force: false }
elsif buttons.include?("+#{digit}")
# "+x" syntax means we should force this button to be on.
state = { value: 1, force: true }
elsif buttons.include?("-#{digit}")
# "-x" syntax means we should force this button to be off.
state = { value: 0, force: true }
end
debug "State for button #{digit}:"
debug state
@button_map[digit] = state
end
self
end
def with_default(path)
debug "Combining with default config"
default = LEDState.new(path)
merge!(default)
self
end
def map
@button_map
end
# Converts the state to the format expected by the `pacdrive` utility.
def to_s
str = (1..16).map do |digit|
state = '0'
data = @button_map[digit]
if data && data.has_key?(:value)
state = data[:value] == 1 ? '1' : '0'
end
end
binary = str.join('').reverse
"0x%04x" % binary.to_i(2)
end
# Calls the external `pacdrive` app to set the LEDs to the represented
# state.
#
# Uses `exec`, so once this method is called, we implicitly exit.
def apply!
exec("/home/pi/bin/pacdrive", "-q", "-s", to_s)
end
protected
# For combining this state with another. The argument to this function is
# assumed to be a default, which means it will take precedence over this
# state when conflicts happen.
def merge!(default)
output = {}
default_map = default.map
@button_map.keys.each do |digit|
own = @button_map[digit]
dflt = default_map[digit]
result = nil
if !dflt[:force]
# Our version always wins.
result = own[:value]
else
# Default wants to force a value...
if !own[:force]
# ...and we don't, so use the default.
result = dflt[:value]
else
# ...and so do we, so we win.
result = own[:value]
end
end
output[digit] = { value: result }
end # each
# Replace the button map with the merged version.
@button_map = output
end # merge
end # LEDState
def debug(str)
return unless $options[:debug]
`echo "#{str}" >> #{$options[:debug_path]}`
end
if ARGV.size == 1
# We've been given a string like "1,2,3,7,8,9" (probably for debugging).
# Ignore all configs and just create and apply an LED state.
debug "Given input: #{ARGV[0]}"
LEDState.new(ARGV[0]).apply!
elsif ARGV.size == 2
# We've been given a system and a ROM name. Look up its config file and
# turn it into a binary string.
system, game = ARGV
debug "Setting LEDs for game #{game} on system #{system}..."
game_config_path = BASE_CONFIG_PATH.join(system, "#{game}.cfg")
if game_config_path.exist?
state = LEDState.new(game_config_path).with_default(DEFAULT_CONFIG_PATH)
else
# No specific LEDs for this game, so just apply the default.
state = LEDState.new(DEFAULT_CONFIG_PATH)
end
state.apply!
else
puts $usage
end
#!/usr/bin/bash
# ARGUMENTS, IN ORDER:
# 1. System (e.g., "arcade")
# 2. Emulator (e.g. "lr-fba-next")
# 3. Full path to game (e.g., /home/pi/RetroPie/roms/arcade/wjammers.zip)
if [ -z "$3" ]; then
exit 0
fi
system=$1
emulator=$2
# Gets the basename of the game (e.g., "wjammers")
game=$(basename $3)
game=${game%.*}
/home/pi/bin/led-end "$system" "$game" >/dev/null
#!/usr/bin/bash
# ARGUMENTS, IN ORDER:
# 1. System (e.g., "arcade")
# 2. Emulator (e.g. "lr-fba-next")
# 3. Full path to game (e.g., /home/pi/RetroPie/roms/arcade/wjammers.zip)
if [ -z "$3" ]; then
exit 0
fi
system=$1
emulator=$2
# Gets the basename of the game (e.g., "wjammers")
game=$(basename $3)
game=${game%.*}
# Turn on the relevant LEDs for this game.
/home/pi/bin/led-start "$system" "$game" >/dev/null
@BastelBaus
Copy link

BastelBaus commented Jan 14, 2020

Nice, Thank you !
I got it to work with one pac drive only. But conneting two at the same (with differnt ID's) seamed not to work on my pi.

  • It detects only the board I connect last. lsusb find both pacdrives
  • I have ID 1 and ID3, on my pc with LED blinky it worked
    Did any body get it to work or has hints for debugging ?

@egorka123456
Copy link

Does this work for the Raspberry Pi4, or just 3B plus?

@lrdfoo
Copy link

lrdfoo commented Feb 18, 2021

i'm getting the following error on raspberry pi 4 with retropie 4.4

root@playbox4:~/src/pacdrive_0_2# make
/usr/bin/gcc -Wall -c -DLINUX pac_prog.c
In file included from pac_prog.c:5:
pac_prog.h:30:10: fatal error: hid.h: No such file or directory
#include <hid.h>
^~~~~~~
compilation terminated.
make: *** [Makefile:40: pac_prog.o] Error 1

@jamesb1486
Copy link

would this work for the EG Starts USB Arcade controls?

@Demionx10
Copy link

Demionx10 commented Jan 9, 2022

Hi, i get Error when trying to install on PRI4/400, any chance for a Fix ? - pi@retropie:~/src/libhid-0.2.16 $ sudo make install
Making install in hidparser
make[1]: Entering directory '/home/pi/src/libhid-0.2.16/hidparser'
if /bin/bash ../libtool --tag=CC --mode=compile gcc -DHAVE_CONFIG_H -I. -I. -I.. -DNDEBUG -fPIC -O2 -Wall -W -Werror -MT hidparser.lo -MD -MP -MF ".deps/hidpa rser.Tpo" -c -o hidparser.lo hidparser.c;
then mv -f ".deps/hidparser.Tpo" ".deps/hidparser.Plo"; else rm -f ".deps/hidpar ser.Tpo"; exit 1; fi
gcc -DHAVE_CONFIG_H -I. -I. -I.. -DNDEBUG -fPIC -O2 -Wall -W -Werror -MT hidpar ser.lo -MD -MP -MF .deps/hidparser.Tpo -c hidparser.c -fPIC -DPIC -o .libs/hidp arser.o
gcc -DHAVE_CONFIG_H -I. -I. -I.. -DNDEBUG -fPIC -O2 -Wall -W -Werror -MT hidpar ser.lo -MD -MP -MF .deps/hidparser.Tpo -c hidparser.c -o hidparser.o >/dev/null 2>&1
/bin/bash ../libtool --tag=CC --mode=link gcc -fPIC -O2 -Wall -W -Werror -o li bhidparser.la -static hidparser.lo
rm -fr .libs/libhidparser.a .libs/libhidparser.la
ar cru .libs/libhidparser.a hidparser.o
ar: u' modifier ignored since D' is the default (see `U')
ranlib .libs/libhidparser.a
creating libhidparser.la
(cd .libs && rm -f libhidparser.la && ln -s ../libhidparser.la libhidparser.la)
make[2]: Entering directory '/home/pi/src/libhid-0.2.16/hidparser'
make[2]: Nothing to be done for 'install-exec-am'.
test -z "/usr/local/include" || mkdir -p -- "/usr/local/include"
/usr/bin/install -c -m 644 'hidparser.h' '/usr/local/include/hidparser.h'
/usr/bin/install -c -m 644 'hidtypes.h' '/usr/local/include/hidtypes.h'
make[2]: Leaving directory '/home/pi/src/libhid-0.2.16/hidparser'
make[1]: Leaving directory '/home/pi/src/libhid-0.2.16/hidparser'
Making install in src
make[1]: Entering directory '/home/pi/src/libhid-0.2.16/src'
if /bin/bash ../libtool --tag=CC --mode=compile gcc -DHAVE_CONFIG_H -I. -I. -I.. -DNDEBUG -fPIC -I../include -I../hidparser -O2 -Wall -W -Werror -MT linux.lo -MD -MP -MF ".deps/linux.Tpo" -c -o linux.lo linux.c;
then mv -f ".deps/linux.Tpo" ".deps/linux.Plo"; else rm -f ".deps/linux.Tpo"; ex it 1; fi
gcc -DHAVE_CONFIG_H -I. -I. -I.. -DNDEBUG -fPIC -I../include -I../hidparser -O2 -Wall -W -Werror -MT linux.lo -MD -MP -MF .deps/linux.Tpo -c linux.c -fPIC -DP IC -o .libs/linux.o
In file included from linux.c:6:
../include/hid.h:5:10: fatal error: usb.h: No such file or directory
#include <usb.h>
^~~~~~~
compilation terminated.
make[1]: *** [Makefile:427: linux.lo] Error 1
make[1]: Leaving directory '/home/pi/src/libhid-0.2.16/src'
make: *** [Makefile:394: install-recursive] Error 1

Something about missing USB.h ?

@bustinex
Copy link

try ./configure --disable-warnings

@StevenLee75
Copy link

Hi, I don't have much experience with these things, so forgive me because this error seems to be common.
I get this far. How exactly do I continue?

pi@retropie:~/src/pacdrive_0_2 $ make
/usr/bin/gcc -Wall -c -DLINUX pac_prog.c
In file included from pac_prog.c:5:
pac_prog.h:30:10: fatal error: hid.h: No such file or directory
#include <hid.h>
^~~~~~~

@savetheclocktower
Copy link
Author

@StevenLee75, did you follow the directions to install libhid from the “Software” section above?

@StevenLee75
Copy link

StevenLee75 commented Feb 28, 2023 via email

@savetheclocktower
Copy link
Author

@StevenLee75, I'd suggest you try the instructions again starting with the line mkdir src && cd src. Enter each command individually. When you get to the ./configure line, pay close attention to the output to see whether it was successful or unsuccessful. And do the same for the make line.

My only guess is that libhid didn't actually install correctly. If both ./configure and make appear to have worked, then I'm not sure what could be going on.

@StevenLee75
Copy link

StevenLee75 commented Feb 28, 2023 via email

@savetheclocktower
Copy link
Author

Just using the Pac Drive would give me mostly everything I need, but is it possible to use the I-PAC Ultimate I/O or the PacLED64 instead?

Possibly, but it wouldn't involve the pacdrive utility, which seems to have been written for the exact Pac-Drive hardware. I'd be astonished if it worked for anything else.

But keep in mind that this gist captures the state of things in 2016. There may be other utilities out there for interfacing with those other devices.

That said, I'd wager that the errors you're getting are fixable somehow.

@StevenLee75
Copy link

StevenLee75 commented Feb 28, 2023 via email

@StevenLee75
Copy link

StevenLee75 commented Mar 1, 2023 via email

@StevenLee75
Copy link

StevenLee75 commented Mar 1, 2023 via email

@savetheclocktower
Copy link
Author

@StevenLee75, hadn't given up, just was busy with other things. Here's something I think might help this compile: open Makefile for editing with nano Makefile.

Then find the line that says

CFLAGS = -O2 -Wall -W -Werror

and change it to look like this (transcribe it exactly, copying and pasting if necessary):

CFLAGS = -O2 -Wall -W -Werror -Wno-format-truncation

Then find this line:

CXXFLAGS = -O2 -Wall -W -Werror

and change it to be:

CXXFLAGS = -O2 -Wall -W -Werror -Wno-format-truncation

Then do ctrl-O and hit Enter to save the file, and ctrl-X to return to the command line. Then try running make again and see if you get an error.

It's hard to diagnose what's going on without trying to recreate it on my end, but it sounds like GCC (the compiler) has added a new check for something that could cause problems, but is innocuous in this case. The fix is to turn off this new warning type.

Your Arduino idea is a good one, but I'd suggest to make it so that the Arduino needs to know as little as possible about game configuration. If you launch simpsons2p, for example, you'd want to send something like 110000110000 to the Arduino — e.g., “LEDs 1, 2, 7, 8 on, all others off” (the first two buttons for each player). Integers should also be easier to send over SPI than arbitrary strings.

@StevenLee75
Copy link

StevenLee75 commented Mar 1, 2023 via email

@StevenLee75
Copy link

StevenLee75 commented Mar 2, 2023 via email

@StevenLee75
Copy link

StevenLee75 commented Mar 3, 2023 via email

@compukit007
Copy link

whel i try to install it on a nieuw rsb 3+ build bad only get erros and dont have a idee how to fix it.
2 -Wall -W -Werror -MT linux.lo -MD -MP -MF .deps/linux.Tpo -c linux.c -fPIC -DPIC -o .libs/linux.o
In file included from linux.c:6:
../include/hid.h:5:10: fatal error: usb.h: No such file or directory
#include <usb.h>
^~~~~~~
compilation terminated.
make[1]: *** [Makefile:427: linux.lo] Error 1
make[1]: Leaving directory '/home/pi/src/libhid-0.2.16/src'
make: *** [Makefile:394: install-recursive] Error 1

pi@retropie:/src $ cd pacdrive_0_2
pi@retropie:
/src/pacdrive_0_2 $ make
/usr/bin/gcc -Wall -c -DLINUX pac_prog.c
In file included from pac_prog.c:5:
pac_prog.h:30:10: fatal error: hid.h: No such file or directory
#include <hid.h>
^~~~~~~
compilation terminated.
make: *** [Makefile:40: pac_prog.o] Error 1

can sombady please help my ?

@compukit007
Copy link

wel i fount the problem by the ./configure i nedit to put ./configure --disable-warnings now i can use pacdrive -f bad it wont change the leds when i start a game

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment