How I Hacked a DDR Mat to Fly Drones

This afternoon, I taught what will probably be my last workshop as a student at UC Merced. We were going to be flying drones with Python. ΑΣ had the idea of using her Dance Dance Revolution mat as a controller, so we hacked it to fly drones. Let me explain.


Plugging any USB device into a Linux computer creates a special kind of file known as a device file. On *NIX, one of the greatest features is that everything is a file: I/O devices are just streams of bytes exposed through files accessible on the filesystem. On Linux, you can find device files in /dev/. For input devices specifically, you can typically find them in /dev/input/.

Upon plugging the DDR mat into my laptop, I noticed a new event appear: /dev/input/event19. To test, I opened the stream with cat and stepped on a control to see what data would appear, which was of course gibberish and mostly unprintable characters but could at least positively confirm that my /dev/input/event19 was correlated to the user input device that I had just plugged in.

If we can understand the output of the device file, we can write a program that reads the device’s output and maps it to actions.

I collected a series of samples with cat, teeing it to output files so that I could analyze the output data later to hopefully discover patterns. For example, if I was collecting data from the circle control on the mat, I’d run:

$ cat /dev/input/event19 | tee -a circle0.bin

Then, the circle control would be physically triggered, the output data written to the screen and also written to circle0.bin, and then I’d hit CTRL+C to exit and move onto my next sample. I did this for all controls on the mat. You can find all the samples I collected here: GitHub

I’d like to thank ΑΣ and ΤΒ for helping me collect samples, without whom this would not been as fun as it was.

After collecting 4 samples of every control on the map, I moved onto analysis. My favorite hex editor is Okteta, which I used to analyze the hexdumps. Here are a few examples of the hexdumps I analyzed:

Now to clarify the scope of my objective, I didn’t need to fully reverse engineer the entire DDR mat’s USB protocol (though that might be a fun idea for a future post) – I was only looking to reverse it enough such that I could write a program that understood it and could discern one control from another control.

A few things I noticed: pressing a single control output 144 bytes to the device file, except for the center control which output only 96 bytes. These bytes seem to be composed of 24-byte “chunks,” each starting with XX XX 57 62. At offset 0x10 (16) of two of these chunks (for all controls besides the center), I notice a common sequence of bytes: 04 00 04 00, followed by a byte common to all samples collected from that particular control but different from all other controls. The center control is different and I’m not totally sure why, itself instead being 03 00 01 00 FF where those bytes would be, but at least staying common amongst all samples collected of the center control.

At this point, I have all the pieces I need to complete the objective: with the exception of the center control, all controls share 04 00 04 00 starting at offset 0x10 followed by a single byte that can be used to identify that control; the center control is special and we can instead use 03 00 01 00 FF to identify it since no other chunk in any of the other controls uses that sequence of bytes starting from offset 0x10. I wrote some test code to verify my suspicions:

#!/usr/bin/env python3

DEV = "/dev/input/event19"
CHUNKSIZE = 24
CONTROLS = {
	0x01: "LEFT",
	0x02: "DOWN",
	0x03: "UP",
	0x04: "RIGHT",
	0x05: "TRIANGLE",
	0x06: "SQUARE",
	0x07: "CROSS",
	0x08: "CIRCLE",
	0x09: "SELECT",
	0x0A: "START",
	0xFF: "CENTER",
}
SIGNATURES = [
	bytearray([0x04, 0x00, 0x04]),
	bytearray([0x03, 0x00, 0x01]),
]

def main():

	while True:

		with open(DEV, "rb") as f:
			data = f.read(CHUNKSIZE)

		if data[0x10:0x13] in SIGNATURES:
			try:
				print("KEY PRESSED:", CONTROLS[data[0x14]])
			except:
				pass
		else:
			continue

if __name__ == "__main__":
	main()

Every control output twice since the “signature” is present in two chunks per control press data output, but it’s honestly not that big of a deal and I didn’t bother to tweak my code to accommodate for that.

I could successfully start mapping controls to actions! Here’s some code that controls the cursor:

#!/usr/bin/env python3

from pynput.keyboard import Key, Controller

DEV = "/dev/input/event19"
CHUNKSIZE = 24
CONTROLS = {
	"LEFT"     : 0x01,
	"DOWN"     : 0x02,
	"UP"       : 0x03,
	"RIGHT"    : 0x04,
	"TRIANGLE" : 0x05,
	"SQUARE"   : 0x06,
	"CROSS"    : 0x07,
	"CIRCLE"   : 0x08,
	"SELECT"   : 0x09,
	"START"    : 0x0A,
	"CENTER"   : 0xFF,
}
SIGNATURES = [
	bytearray([0x04, 0x00, 0x04]),
	bytearray([0x03, 0x00, 0x01]),
]

def main():

	quantum = 10
	keyboard = Controller()

	while True:

		with open(DEV, "rb") as f:
			data = f.read(CHUNKSIZE)

		if data[0x10:0x13] not in SIGNATURES:
			continue

		if data[0x14] == CONTROLS["LEFT"]:
			keyboard.press(Key.left)
			keyboard.release(Key.left)

		elif data[0x14] == CONTROLS["DOWN"]:
			keyboard.press(Key.down)
			keyboard.release(Key.down)

		elif data[0x14] == CONTROLS["UP"]:
			keyboard.press(Key.up)
			keyboard.release(Key.up)

		elif data[0x14] == CONTROLS["RIGHT"]:
			keyboard.press(Key.right)
			keyboard.release(Key.right)

		if quantum < 0:
			quantum = 0

if __name__ == "__main__":
	main()

Neat, huh? Now let’s move onto the drones.

The drones supplied to us by Engineering Service Learning are Tello drones by Ryze robotics. Upon bootup, these drones spawn and broadcast a wireless network with themselves as the router located at 192.168.10.1. The drone listens for instructions on UDP port 8889, which are just some basic plaintext instructions like “forward 30” and whatnot.

The easyTello library for Python abstracts away from all of this and gives users an object-oriented interface to control the drone with from within Python, but it unfortunately contains a bug with the way it processes video on Windows systems. That’s fine, though, since I forked their project and fixed their bug so that the workshop attendees could use it: GitHub

During the day of the workshop, attendees were challenged to integrate the skills, code, and concepts they’ve learned in our prior two workshops with some sample code provided to them in order to map the DDR mat controls to control the drone. One of the students came up with this:

#!/usr/bin/env python3

import tello

DEV = "/dev/input/event19"
CHUNKSIZE = 24
CONTROLS = {
	"LEFT"	   : 0x01,
	"DOWN"	   : 0x02,
	"UP"	   : 0x03,
	"RIGHT"	   : 0x04,
	"TRIANGLE" : 0x05,
	"SQUARE"   : 0x06,
	"CROSS"	   : 0x07,
	"CIRCLE"   : 0x08,
	"SELECT"   : 0x09,
	"START"	   : 0x0A,
	"CENTER"   : 0xFF,
}
SIGNATURES = [
	bytearray([0x04, 0x00, 0x04]),
	bytearray([0x03, 0x00, 0x01]),
]

def main():

	quantum = 10
	drone = tello.Tello()

	while True:

		with open(DEV, "rb") as f:
			data = f.read(CHUNKSIZE)

		if data[0x10:0x13] not in SIGNATURES:
			continue

		if data[0x14] == CONTROLS["LEFT"]:
			drone.left(100)

		elif data[0x14] == CONTROLS["DOWN"]:
			drone.back(100)

		elif data[0x14] == CONTROLS["UP"]:
			drone.forward(100)

		elif data[0x14] == CONTROLS["RIGHT"]:
			drone.right(100)

		elif data[0x14] == CONTROLS["START"]:
			drone.takeoff()		

		elif data[0x14] == CONTROLS["SELECT"]:
			drone.land()
		
		elif data[0x14] == CONTROLS["TRIANGLE"]:
			drone.ccw(22)

		elif data[0x14] == CONTROLS["SQUARE"]:
			drone.cw(22)

if __name__ == "__main__":
	main()

We uploaded it to my laptop (since I’m running on Linux) and tested it out, and it worked.


In summary, it was a really successful and fun day and a lot of programming newbies got more exposure to computer programming. I’m happy it happened and I’d have ended off my career at the SEA no other way.

Happy hacking!