Among the things I want to do with the Raspberry Pi is to build a miniature arcade thing. I had planned on making one with one of my mini-itx systems, but the Pi just seems so much better. And honestly, it's more stable than any of the mini-itx systems I've used. I had already ordered an Ultimarc Ultrastik 360 and a bunch of arcade buttons. The Ultrastik is awesome, I seriously wouldn't build any type of arcade without using one of these. One of the things it can do is flash 9x9 control maps so it can be a 4-way, 8-way, 2-way, and more. It also hooks up via usb, supports direct connection of buttons, and the 2.5 firmware can use one of those as a shift (so it basically can support about 16 buttons). Awesome. I got the medium-firm spring and the circular restrictor plate to give it a shorter throw.
I started with the Adafruit Occidentalis build for my RPi, which came with the stuff I needed "out of the box" to get the WiFi dongle working.
Earlier experiments trying to get MAME and a frontend gui working didn't go so well, but I ran across a few posts that helped. I followed Chris's guide and parts of Ben's post to get AdvanceMAME and AdvanceMenu compiled and installed. I'll skip the hassle I had getting all the appropriate files & configurations loaded into the right directories.
Then I snagged Al Crate's configuration utility for linux (link goes to Ultimarc page, code is linked at second line from top). To get that to compile, I needed to grab a few things and compile libhid (because it's not available to RPi via apt-get install yet for some reason). But to get *that* to compile without a warning throwing the whole thing out of the make command, I had to edit the libhid-0.2.16/test/lshid.c
There's a line that says:
len = *((unsigned long*)custom);
Right below it, I threw this:
if(len==0){;}
So that got libhid compiled, and after an
ldconfig
I got ultrastikcmd to compile. The next set of problems I ran into were:- When I ran ultrastikcmd to load a map into the Ultrastik, it dropped off the machine.
jstest /dev/input/js0
no longer found it (although anls /dev/input/
shows ajs0
node). - When I unplugged and replugged the joystick, the jstest would work again but the input either didn't work at all or was scrambled. I did a LOT of using my Windows machine to load new maps into joystick.
The section of code I fixed is toward the bottom -- I just commented out the if and closing brace. Since it sets the defaults & overrides them if you load them from a map or command line, it should be ok to write them to the device no matter what. I might not have needed to do this if the Ultrastik configuration maps I used had map values in them.
//if(arguments.border_set){
// Byte[3]-Byte[10] = Map Border Location
memcpy(&data[3],arguments.border,sizeof(arguments.border));
//}
The remaining big issue was the fact that the thing kept falling offline. After hours and hours, and a significant amount of searching, I figured out that when ultrastikcmd takes control of the joystick to flash a map to it, the system unbinds the kernel driver. I tried a couple dozen things to get it rebound programmatically (and learned a lot about devices and usb on linux in the process), but the reattach code isn't in the libusb or other stuff built into the RPi image I was using, and to heck if I'm going to re-compile the whole OS.
I ended up creating a whole lot less elegant solution, but it works.
Here's what we started with. You can see that jstest works - the joystick gives appropriate readings when I move it.
$ jstest /dev/input/js0
Driver version is 2.1.0.
Joystick (Ultimarc Ultra-Stik Ultimarc Ultra-Stik Player 1) has 2 axes (X, Y)
and 16 buttons (Trigger, ThumbBtn, ThumbBtn2, TopBtn, TopBtn2, PinkieBtn, BaseBtn, BaseBtn2, BaseBtn3, BaseBtn4, BaseBtn5, BaseBtn6, BtnDead, BtnA, BtnB, BtnC).
Testing ... (interrupt to exit)
Axes: 0: 0 1: 0 Buttons: 0:off 1:off 2:off 3:off 4:off 5:off 6:off 7:off 8:off 9:off 10:off 11:off 12:off 13:off 14:off 15:off
Now switching to 4-way map
$ ultrastikcmd -vfou ~/ustik_maps/4-way.um
And trying jstest again -- no love$ jstest /dev/input/js0
jstest: No such device
One of the things I learned about along the way is sysfs. That and a bit of poking around yielded something interesting. This presumes you have usbip installed, and gives you a list of usb devices by USB BUS ID. d209:0501 is the Ultrastik.
$ sudo usbip list -l
- busid 1-1.3.3 (d209:0501)
1-1.3.3:1.0 -> unknown
1-1.3.3:1.1 -> usbhid
See that little "unknown" there? That's the not-bound kernel driver. Here are the input devices -- notice there aren't any js0 entries, but there is for event1 (1-1.3.3:1.1), which comes from the Ultrastik. Note: for this and other directory listings, I'm going to edit them down to save space & improve readability.
$ ls -la /sys/class/input/
event1 -> ../../devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3.3/1-1.3.3:1.1/input/input10/event1
mice -> ../../devices/virtual/input/mice
mouse0 -> ../../devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3.3/1-1.3.3:1.1/input/input10/mouse0
It doesn't show above as bound to the driver, but we can double check. No driver listed!
$ ls -la /sys/bus/usb/devices/1-1.3.3:1.0/
bAlternateSetting
bInterfaceClass
bInterfaceNumber
bInterfaceProtocol
bInterfaceSubClass
bNumEndpoints
ep_82
modalias
power
subsystem -> ../../../../../../../../bus/usb
supports_autosuspend
uevent
This one is (the one for event1, which is the other BUS ID listed by the usbid command).
$ ls -la /sys/bus/usb/devices/1-1.3.3:1.1/
0003:D209:0501.000B
bAlternateSetting
bInterfaceClass
bInterfaceNumber
bInterfaceProtocol
bInterfaceSubClass
bNumEndpoints
driver -> ../../../../../../../../bus/usb/drivers/usbhid
ep_81
input
modalias
power
subsystem -> ../../../../../../../../bus/usb
supports_autosuspend
uevent
Yup, a driver!Now for the fun part. Evidently, if you write the appropriate usb bus ID to the kernel driver bind file, it rebinds. Hmmm...
echo -n "1-1.3.3:1.0" | sudo tee -a /sys/bus/usb/drivers/usbhid/bind
You need to put the sudo in the right spot and use tee because the user doesn't have write permissions to bind. I'll fix that later.Now lets check to see if it appears rebound:
$ sudo usbip list -l
- busid 1-1.3.3 (d209:0501)
1-1.3.3:1.0 -> usbhid
1-1.3.3:1.1 -> usbhid
Yes! And importantly, jstest worked again!
Now how do I keep from having to manually do this every time I load a map into the Ultrastik. Enter udev (and two more links) and some shell scripting.
First step, since I want to try and programmatically be able to rebind in user (vs. root) space, I'm changing permissions/group for the usbhid kernel bind. I'm sure someone will point out all kinds of evil wrong things this will do, but this isn't a server/production machine... it's an arcade joystick.
$ sudo chmod g+w /sys/bus/usb/drivers/usbhid/bind
$ sudo chown root:pi /sys/bus/usb/drivers/usbhid/bind
$ ls -la /sys/bus/usb/drivers/usbhid/
--w--w---- 1 root pi 4096 Aug 31 23:45 bind
To make things easy, I also want to create a symlink to the bind in a convenient location.
$ ln -s /sys/bus/usb/drivers/usbhid/bind ~/ultrastik/usbhid_bind
Now I need a udev rule. I've added a rule to write the kernel to a file under the home folder, for ease of access in my shell script.
$ cat /etc/udev/rules.d/81-ultrastik.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="d209", ATTRS{idProduct}=="0501", GROUP="input", RUN+="/home/pi/receive_udev.sh '%s{manufacturer}' '%k'"
That matches the Ultimarc Ultrastik, ensures when it's added that a group the user is in can get to it, and then runs a custom shell script passing in a couple variables. I'm not using the first one (manufacturer), it was just for testing. Here's the script that receives that rule and writes the usb bus id out. I'm hard-coding the ":1.0" part in hopes that doesn't screw it up later.
$ cat receive_udev.sh
#!/bin/bash
echo "$2:1.0" > /home/pi/ultrastik/ultrastik_sysfs_id
So now all I need to do to fix/rebind the joystick is:
$ cat ~/ultrastik/ultrastik_sysfs_id > ~/ultrastik/usbhid_bind
I've tweaked the AdvanceMenu configuration (
~/.advance/advmenu.rc
) emulator line to call a generic emulator (my script):
emulator "AdvanceMAMECustom" generic "/home/pi/ultrastik/advshim.sh" "%s"
That script is as follows -- where you see rom1|rom2, etc. you would replace those with the names of the roms that need a specific Ultrastik map.
#!/bin/bash
# if no command line arg given
# set ROM to Unknown
if [ -z $1 ]
then
ROM="*** Unknown ROM ***"
elif [ -n $1 ]
then
# otherwise make first arg as a ROM
ROM="$1"
fi
case $ROM in
# 8-way
rom1|rom2|rom3)
ultrastikcmd -ou ~/ultrastik/ustik_maps/8-way.um
cat ~/ultrastik/ultrastik_sysfs_id > ~/ultrastik/usbhid_bind
;;
# 4-way
rom4|rom5)
ultrastikcmd -ou ~/ultrastik/ustik_maps/4-way.um
cat ~/ultrastik/ultrastik_sysfs_id > ~/ultrastik/usbhid_bind
;;
*)
echo "Sorry, no ROM defined!"
;;
esac
/usr/local/bin/advmame $ROM
So...
- On boot or Ultrastik insert, udev catches it and writes something like 1-1.3.3:1.0 to
~/ultrastik/ultrastik_sysfs_id
- When you launch a game from advmenu, it calls the
advshim.sh
script, passing in the rom name. - The script checks the rom name to find the appropriate ultrastik configuration map,
- ...runs the ultrastikcmd configurator (losing the joystick in the process),
- ...writes the contents of the previously-written usb bus ID to the bind to pick back up the joystick,
- ...then launches advmame with the rom.
Now all I need to do is wire up the buttons, get them all configured properly, and build a case for the whole assembly.