Just go for it: The story of dance-mat.js
Nov 25, 2019
This is the story of my first proper hardware project.
Before Just Dance, before Beat Saber, one of my favourite music games growing up was Dance Dance Revolution.
Let’s look into how Birgit and I built our very own Dance Dance Revolution Controller, from scratch! All the code is available on Github.
Background
How does the game work?
The premise is shown immediately on the screen. Arrows rise to the top and you gotta press the corresponding button to the rhythm:
This is how Dance Dance Revolution (DDR from here on out) looked on the original Playstation.
And the music really was a staple of its time.
Cool! Can I play it on PC?
Well… Kinda.
There’s no recent release of DDR on PC platforms, but there’s an open source alternative! It’s called Stepmania and you can customize any songs you have to be played on it.
How’d you do it?
Parts list
- Bare Conductive paint
- Bare Conductive Pi Cap
- Raspberry Pi Zero W
- 5V power supply
- MicroSD card (8GB is fine)
- Breakout header
- Yoga Mat (I don’t have a link, any will do!)
Raspberry Pi setup
Bare Conductive, the folks who manufacture the Pi Cap and paint, provide a guide to set up the Raspberry Pi as well as the Pi Cap. Once I did this, I was ready to start.
Once I gave my Pi power and connected to it via SSH, I was ready to go!
Project beginnings
The plan is to set up the Raspberry Pi to send USB keyboard strokes to the PC that’s running Stepmania.
I started by creating a project:
$ mkdir picap-dance-mat && cd picap-dance-mat
$ npm init
Once done, I added the Pi Cap package:
$ npm install --save node-picap
With that done, I set the stage based on the sample code:
const MPR121 = require('node-picap');
const mpr121 = new MPR121('0x5C');
mpr121.setTouchThreshold(40);
mpr121.setReleaseThreshold(20);
// Process touches
mpr121.on('data', (data) => {
try {
// SEND DATA TO PC
}
catch(e) {
console.log("ERROR: ", e);
}
});
What we’ve got here is a setup involving the touch and release sensitivity of the electrodes (and by extension, the paint).
Setting up the Raspberry Pi to act as a USB Keyboard
This was by far the hardest part. You see, by default a Pi won’t be recognized as a USB keyboard. I mean, why should it?
Fortunately, since I’m on Linux, I can use a kernel extension.
And even better, I’m not the first to think of doing this!
By following the instructions in the above, I was able to set up a script that runs on boot:
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p ddrusb
cd ddrusb
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
mkdir -p strings/0x409
echo "fedcba9876543210" > strings/0x409/serialnumber
echo "Ramon Huidobro" > strings/0x409/manufacturer
echo "DDR Dance Mat" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# Add functions here
mkdir -p functions/hid.usb0
echo 1 > functions/hid.usb0/protocol
echo 1 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.usb0/report_desc
ln -s functions/hid.usb0 configs/c.1/
# End functions
ls /sys/class/udc > UDC
It looks like a lot, but fortunately I was able to reuse it! With this, I’ve opened up a serial file descriptor through which I can send USB keystrokes.
What does a USB keystroke look like?
After going through the official USB HID information and some searching around, I found out that the way to send keystrokes is in the form of an 8 byte-long array, the first two being reserved for modifiers.
I went ahead and defined the letters a
, b
, c
and d
as the keyboard
buttons the dance mat will press:
const p1Left = 0x04; // A
const p1Right = 0x05; // B
const p1Up = 0x06; // C
const p1Down = 0x07; // D ```
Working through the electrodes
Back in our main function, we can loop over these electrodes and convert these:
parsePressedKeys = (data) => {
const pressedKeys = [];
data.forEach((electrode, i) => {
if (electrode.isTouched) {
switch(i) {
case 0:
pressedKeys.push(p1Left);
break;
case 1:
pressedKeys.push(p1Right);
break;
case 2:
pressedKeys.push(p1Up);
break;
case 3:
pressedKeys.push(p1Down);
break;
}
}
});
return pressedKeys;
}
keystrokeFromPressedKeys = (pressedKeys) => {
keystroke = [0x00, 0x00];
pressedKeys.forEach((key) => {
keystroke.push(key);
});
while(keystroke.length < 8) {
keystroke.push(0x00);
}
return keystroke.slice(0, 8);
}
mpr121.on('data', (data) => {
const keys = parsePressedKeys(data);
keystroke = keystrokeFromPressedKeys(keys);
});
What I did here was match electrodes (the receptors on the picap) to a key that
will be pressed and then build a list of 8 strokes. If none are pressed, then
they’re all 0x00
, meaning that the keyboard has been let go!
Converting the keystrokes into a byte array.
Javascript provides us with a handy dandy function for converting an array into a byte array:
mpr121.on('data', (data) => {
let keys = parsePressedKeys(data);
let keystroke = keystrokeFromPressedKeys(keys);
let buffer = Uint8Array.from(keystroke);
});
With that said and done, we need to now pipe that data into the serial file descriptor!
Piping the keystroke
I found an npm package called linux-device with exactly the functionality I need! With this, I can open up the device for writing, and pipe the data into it as needed:
const DeviceHandle = require('linux-device');
// Open up access to the USB interface
const device = new DeviceHandle('/dev/hidg0', true, 16);
// Process touches
mpr121.on('data', (data) => {
let keys = parsePressedKeys(data);
keystroke = keystrokeFromPressedKeys(keys);
let buffer = Uint8Array.from(keystroke);
device.write(buffer);
});
One last part before we begin testing is to close the device upon exit, like good citizens we are!
const exec = require('child_process').exec;
process.on('SIGINT', () => {
device.close();
process.exit(0);
});
With that, we’re ready to run this!
Testing the device
With all this said and done, we can set up init.d
script to start up the node program:
$ node dance-mat.js
To actually test it, all I had to do was plug the Pi into the USB port of a computer directly, which gives it the power it needs, open up a text editor, and we’re off to the races!
All I had to do was then hook up the electrodes to some conductive paint, and hey…
Let’s paint!
Here’s how the finished product looked after my wife and I finished painting:
All in all, a satisfying result!
That said, a few issues cropped up:
- Painting on a yoga mat caused the paint to crack after extended use.
- Using paint as cabling could be quite unreliable (see above).
I worked on these issues on working on a portable prototype that I could show when speaking at conferences:
Good enough for lugging around!
Now you too can dance!
The best part of this project for me, considering it was my first foray into hardware hacking, is how it demystified the process.
This has led to believe that hardware hacking does not require an electrical engineering set of skills, but just the desire to go for it!
Wanna see it in action? Check me out presenting it live at Webcamp Zagreb!
Buy me a coffee @hola_soy_milk