Using your car as a giant joystick for $20

18 December 2017

A car rigged to play videogames with a projector pointed at a wall

DISCLAIMER: Cars can be dangerous. The electrical and mechanical systems of a car are not designed for this use case, and you run the risk of damaging them. This project worked fine for this particular vehicle, but could damage critical systems such as steering and braking in a different model. Do not attempt this with a vehicle that isn't yours, or a vehicle that would leave you with no contingency if it were to break. The author will not be held accountable for any damage caused from following these instructions.

This project is about how to rig the controls of nearly any recent car (in my case a 2007 edition Mazda 3) to act as a giant game controller. In a nutshell; input from the car will be scraped as CANbus messages from a cheap OBD-II adapter, then converted to standard joystick and keyboard events, which will be used to drive a video game projected onto a screen in front of the windshield. No physical modifications to the car are required; the only one I made was to pull the fuses for the headlights so as not to blind the projector screen.

The full source code used for the above demo is available at GitHub and BitBucket. With luck, anyone with entry-level Python experience should be able to adapt this for their car. I think this is a nice practical introduction to car hacking and reverse engineering, without the need to spend a lot on exotic debugging hardware.

Requirements

  • A car with a CANbus based internal network and an OBD-II port. Most cars manufactured 2005 and later have this port in either the driver or passenger side footwell under the dashboard. Once you find the port, you should do a search for your car's model, see what internal control networks it has, and how they map to pins on the port.
  • A bootleg ELM327 OBD-II adapter. These things are everywhere and dirt cheap, you can pick up one on eBay for about 20 dollarydoos. Make sure you get one with a USB cable and not a WiFi/BT one; honking great security risk aside, the chip used in the wireless variant is a different architecture with worse firmware than a stock bootleg ELM327, and troubleshooting will make you go mad/start smashing things up.
  • A computer running Linux, a projector, and some HDMI/3.5mm audio cables.

Your friend the CANbus

I think it's important that we touch briefly on what CAN is.

  • Controller Area Network (CAN) is a standard for a twisted wire pair bus designed for the very noisy conditions inside a vehicle.
  • There's a few different variants in terms of speed and message size, but communication over the bus is done with messages made from a message ID (either 11 or 29 bits) and a payload (usually 2-12 bytes). In general the message ID denotes the type of message.
  • Unlike OBD-II PID requests, CANbus messages are non-standard; every vehicle model has its own proprietary message formats, usually with multiple sensor readings packed into each message.
  • Being a multi-master bus, there is no source or destination for messages! Electronic Control Units in the vehicle are constantly listening for/sending out messages thousands of times a second. A pretty ideal medium for sniffing and injecting traffic!
Relevant pins on the Mazda 3's OBD-II socket: 3 - HS CAN high, 4 and 5 - ground, 6 - MS CAN high, 11 - HS CAN low, 14 - MS CAN low, 16 - +12V

As mentioned above, every communications bus in your vehicle should be accessible through the OBD-II port. If your vehicle's CANbus is exposed on a different set of pins than the standard pair, be prepared to crack the adapter open and resolder some wires, or to buy a premodified adapter. My Mazda 3 has two CANbus networks; a boring medium-speed bus on the standard OBD-II pins, and a high-speed bus where the cool stuff happens on two of the vendor pins. The adapter I bought has a throw switch to toggle which bus to use; these ones are sold as "modified for Ford".

The ELM327 supports autodetection of several common message formats and CAN baud rates, plus has two slots for custom settings.

Protocol mapping (taken from ELM327 datasheet)
'0': Autodetect (everything except User1 and User2)
'1': SAE J1850 PWM (41.6 kbaud)
'2': SAE J1850 VPW (10.4 kbaud)
'3': ISO 9141-2 (5 baud init)
'4': ISO 14230-4 KWP (5 baud init)
'5': ISO 14230-4 KWP (fast init)
'6': ISO 15765-4 CAN (11 bit ID, 500 kbaud)
'7': ISO 15765-4 CAN (29 bit ID, 500 kbaud)
'8': ISO 15765-4 CAN (11 bit ID, 250 kbaud)
'9': ISO 15765-4 CAN (29 bit ID, 250 kbaud)
'A': SAE J1939 CAN (29 bit ID, 250* kbaud)
'B': User1 CAN (11* bit ID, 125* kbaud)
'C': User2 CAN (11* bit ID, 50* kbaud)

And yes, there are some vehicle models which have an OBD-II port but don't use (ISO 15765-4) CAN. If you have one of these, I would suggest searching to check if anyone has had success using the ELM327 with your car model, and from there seeing if you can dump bus messages through the "Monitor All" feature (described below).

Speaking of searching, check to see if anyone on the internet has mapped out the CAN message format for your vehicle. We will be reversing the message format ourselves for kicks, but it's always nice to have reference material and build off the research of others. I was lucky that someone had mapped most of the CANbus format for the Mazda 3, but in the end I didn't really use the information other than to confirm my own findings.

So how's this going to work then?

We are going to make a software bridge in Python that connects between sensors on the car's CANbus, and a virtual joystick made with Linux uinput. uinput is amazing; writing a driver for is literally just calling a function every time you want to press/unpress a button or change the readong on an axis. Better still, in Python you can use pyserial and python-uinput to skip nearly all of the messy boilerplate.

If you're not running Linux that's ok, a live USB of Linux with persistent storage will work just as well for running games and emulators. I imagine a lot of readers right now are internally screaming "yes yes but what about the only OS that ever truly matters which is Microsoft Windows?!?!?". Things there are... considerably harder, as the OS does not come with a easy-to-script userspace input driver. You would need to install a third party driver like vJoy, write a Python shim for the C API it offers or maybe chain it through another input manager like AutoHotkey, then replace all of the calls to uinput with that. I should mention that nearly all of the joystick configurations I made generated keyboard presses for some buttons, which is a seperate Windows API to joystick events but you could use a cross-platform wrapper like PyUserInput. And the other software option would be to write a Windows kernel driver! So that's nice.

(I'm starting to think that the massive popularity of microcontroller boards such as the Teensy, pretending to be standard USB devices so as to piggyback generic driver support, might be related to Windows' userspace driver options being so bad?)

Limitations of cheap hardware

Real talk: the ELM327 is not a proper CAN interface, it's a cheapo OBD-II diagnostic unit with CAN sniffing bolted on as an afterthought. ELM327 based devices are not very good at capturing all of the packets off the CANbus. The read buffer on the chip is a paltry 256 bytes, so in the default configuration the "Monitor All" command (which prints incoming CAN messages until the buffer overflows) will vomit blood and give up after a few seconds.

By contrast, a real CAN interface will allow duplex communication (aka. near-simultaneous read and write) with the CANbus; you can sort of write messages to the CANbus with the ELM327, but it's not duplex and you have to switch modes first and the process is really painful. In Linux there is a whole subsystem devoted to CAN support (SocketCAN), which makes CANbus interfaces show up just like network interfaces (i.e. can0 instead of eth0), and all Linux CAN software (e.g. the can-utils collection) builds on top of this generic layer. A proper CAN interface will have native kernel support for SocketCAN, or provide a serial interface compatible with the barebones SLCAN/LAWICEL protocol.

Anyway. So those downsides make the ELM327 not the greatest adapter ever for CAN reverse engineering, BUT! Doesn't mean we can't do it.

Connecting to the ELM327

I purchased a bootleg ELM327 adapter with a beautifully soldered "Ford" mod switch on eBay for $30. Straight out of the gate, I could not get my adapter to respond in either Linux or Windows on any of the standard serial baud rates, even with the (cracked, commercial) software included on the complimentary CD-R! But the little TX light did flash momentarily after turning it on, which suggested that the adapter was sending out the startup message, and I could make the TX/RX lights flash by sending garbage over the serial connection. Evidently the chip was fine and I was trying to communicate over serial at the wrong baud rate (aka. speed in bits/second), but none of the standard RS232 rates seemed to work!

Oscilloscope wired to ELM327 adapter

After a few hours I gave up poking it, stripped the adapter down, shoved some tiny wires down the vias on the main chip's TX and RX lines, and ran them through an oscilloscope. Lo and behold it was communicating at exactly 500000 bit/s! (AKA a completely non-standard speed for RS232 that I never would have guessed in a million years).

Non-standard speeds aren't a bad thing; the datasheet suggested that stock ELM327s run at 38400 bit/s or 9600 bit/s, which are unbearably slow for data capture given our high-speed CANbus is rated for 500kbit/s. But that's still some expert trolling by whoever rigged this, as it rendered the adapter incompatible with pretty much EVERY OBD-II/ELM327 program ever. Maybe they picked 500000 because the HS CANbus was rated for "exactly" that much... except the ELM327 dumps CAN messages in human-readable hexadecimal, which cuts the effective data rate by more than half. Oops.

Once you've figured out the baud rate, you can connect to the device using whatever serial terminal you want. Make sure your user has permission to access the /dev/ttyUSBx interfaces!

  • Device: Run "dmesg" just after you plug in the USB to see what it comes up as (most likely /dev/ttyUSB0)
  • Baud rate: As discussed (possibly 38400, 9600, or 500000)
  • Data bits: 8
  • Parity bits: None
  • Stop bit: 1
  • Hardware flow control: No
  • Software flow control: No
  • Extra: Add line feeds

One thing I learnt when doing this project; there are hundreds of open source serial terminals and nearly all of them are bad, or have the same dumb problems. To save time I recommend grabbing ssterm (a.k.a. the only good serial terminal) and using it like so.

[email protected]:~$ ssterm /dev/ttyUSB0 -b 500000 --rx-nl cr --tx-nl cr

?

>AT I
ELM327 v1.4

Hitting the enter key once should give you a > prompt. Typing "AT I" and hitting enter should give you the version message. At any time, Ctrl-] stops the terminal. "AT I" is an example of an AT command supported by the ELM327, which you can find in the official chip datasheet/AT Commands list.

Changing the comms speed

The very first thing we're going to do is tempt fate and force the ELM327 to run at the fastest speed it can. I know from the FT232RL datasheet that the USB serial interface is capable of up to 2Mbit/second. According to the ELM327 datasheet, baud rate is stored as a divisor for 4Mbit/second in Programmable Parameter 0C. I confirmed this by looking at the PP summary:

>AT PPS
00:FF F  01:FF F  02:FF F  03:32 F
04:01 F  05:FF F  06:F1 F  07:09 F
08:FF F  09:00 F  0A:0A F  0B:FF F
0C:08 N  0D:0D F  0E:9A F  0F:FF F
10:0D F  11:00 F  12:FF F  13:32 F
14:FF F  15:0A F  16:FF F  17:92 F
18:00 F  19:28 F  1A:FF F  1B:FF F
1C:FF F  1D:FF F  1E:FF F  1F:FF F
20:FF F  21:FF F  22:FF F  23:FF F
24:00 F  25:00 F  26:00 F  27:FF F
28:FF F  29:FF F  2A:38 F  2B:02 F
2C:E0 F  2D:04 F  2E:80 F  2F:0A F

Seems legit. PP 0C is on (N means oN!) and set to 0x08, meaning 4Mbit/sec / 8 = 500kbit/sec.

By default, PP 0C is set to off, which sets the baud rate to the default of 38400. If your ELM327 is running at 38400, you can boost it to 500000 by running the following commands:

>AT PP 0C ON
OK

>AT PP 0C SV 08
OK

>AT Z

Disconnect your serial terminal and you should be able to reconnect at 500000. To revert it back to the default (and make all your ELM327/OBD scanning apps work again):

>AT PP 0C OFF
OK

>AT Z

On my bootleg chip 500000 baud is the fastest you can go; setting the baud rate divisor any lower than 08 will return '?'. That kinda sucks, but whatever.

Teaching Python ELM327ese

I wrote a little Python harness for bootstrapping and communicating over serial with the ELM327. By default, the ELM327 will echo the user's input back to them, and seperate new lines with a carriage return ('\r'). Prompts for input are made with a >. Because it's all terminal-styled plaintext, we have no idea how long to expect a serial message to be; we just know it'll be terminated with a '>' (when ready to receive input again) or a '\r' (for a stream of CAN messages).

As we are both cheap and lazy, we will use pyserial and synchronous IO. If you call read( n ), pyserial will block until there's enough data in the receive buffer to fulfil your request. So the easiest way around that is to repeatedly ask for 1 byte (oh man I die a little on the inside when I think how inefficient this is), then stop when you hit the terminator character.

class ELM327:

    def __init__( self, device='/dev/ttyUSB0', baud_rate='500000', protocol='0' ):
        self.elm = serial.Serial( device, baud_rate )
        self.protocol = protocol


    def send( self, cmd ):
        self.elm.reset_input_buffer()
        self.elm.reset_output_buffer()
        self.elm.write( cmd )
        self.elm.write( b'\r' )


    def recv( self ):
        output = bytearray()
        while True:
            b = self.elm.read( 1 )
            if b == b'>':
                break
            output.append( b[0] )
        return output


    def recv_line( self ):
        output = bytearray()
        while True:
            b = self.elm.read( 1 )
            if b == b'\r':
                break
            elif b == b'>':
                raise EOFError
            output.append( b[0] )
        return output

Now that we have serial comms, we'll have to write a small set of bootstrap commands to get the adapter into a receive-ready state.

def get_prompt( self ):
    # send garbage
    self.send( b'this will reset the prompt' )
    self.recv()


def reset( self ):
    # reset interface
    self.get_prompt()
    self.send( b'AT Z' )
    self.recv()

    # return to defaults
    self.send( b'AT D' )
    self.recv()

    # turn echo off
    self.send( b'AT E0' )
    self.recv()

    # check that information string works
    # (my bootleg responds with 'ELM327 v1.4')
    self.send( b'AT I' )
    ack = self.recv()
    print( ack )
    assert ack.startswith( b'ELM' )

    # set the CANbus protocol used by the car
    self.send( 'AT SP {}'.format( self.protocol ).encode('ascii') )
    self.recv()

    # initialize the CANbus interface
    self.send( b'0100' )
    self.recv()

    # get protocol
    self.send( b'AT DPN' )
    print( self.recv() )

    # next 3 are about disabling the "helpful" autoformatting of CAN messages
    # enable messages longer than 7 bytes (standards are for chumps!)
    self.send( b'AT AL' )
    self.recv()

    # turn on headers (why would you not want those)
    self.send( b'AT H1' )
    self.recv()

    # disable formatting (boo)
    self.send( b'AT CAF0' )
    self.recv()

    # disable whitespace in CAN messages
    # ELM327 has only 256 bytes, so this nets you ~33% extra buffer space!
    self.send( b'AT S0' )
    self.recv()

    # set huuuuge timeout before giving up
    self.send( b'AT STFF' )
    self.recv()


def start_can( self ):
    self.get_prompt()
    self.send( b'AT MA' )

And yes we need the recv() after each command; the ELM327 only buffers input when the prompt is displayed and Python will execute faster than the chip has time to react, so we need to block Python until we get another '>'. All of the above was figured out by trial and error; playing with the ELM327 through a serial terminal helped a lot, plus reading all the AT commands in the ELM327 datasheet and the source code for some ELM327-targeted software (e.g. pyOBD). The ELM327 communicates everything in human-readable text, so I just kept poking at it in the terminal until it produced CAN output in a nice compact format.

Sniffing the CANbus

I mentioned earlier the ELM327 isn't the best for reverse engineering. If you don't mind writing your own sniffer, it's actually alright for basic things like this!

The Python harness will set everything up into "Monitor All" mode. Once we ask the ELM327 for messages, it will begin transmitting them over the serial connection encoded as plaintext hexadecimal; first three characters are the message ID (the CANbus in the Mazda 3 uses 11-bit message IDs; 000 to 7FF), the rest is message body. We will need to quickly decode this back into bytes (groan) so we can scrape the sensor information.

CAN_RE = re.compile( b'([0-9A-F]{3})\\W*([0-9A-F\\W]+)' )
...
    def recv_can( self ):
        msg_raw = self.recv_line()
        msg_m = CAN_RE.match( msg_raw )
        if msg_m:
            msg_id = int( msg_m.group( 1 ), 16 )
            msg_b = bytes.fromhex( msg_m.group( 2 ).decode( 'ascii' ) )
            return (msg_id, msg_b)
        return (-1, b'')

There is a CAN filtering feature in the ELM327 which sounded ideal for cutting down traffic, but I struggled a bit figuring out how it worked. There's two components: a filter (CF) and a mask (CM). Like an idiot I thought it would work like any other mask, as in the test would pass any message ID that ANDs with it and remains the same. It's kind of the opposite: you set the filter to any message ID that passes the test, and set the mask to be 1s for the bits in the filter (1 or 0) that must match with the incoming message ID. So you could implement an AND-style filter by setting CF to your mask and CM to (0x7ff ^ mask).

As it happens, cranking the baud rate and disabling whitespace gave juuuuust enough buffer space for the ELM327 to deal with every CAN message produced by my car. Which is good, because the message IDs I needed were so sprawled I couldn't use filtering.

With that working I rigged up a simple tool that read every CAN message and printed only the changes for each message ID. This is available in the source code repository as elm_scan.py.

There was a little bit of noise; two CAN message types had a clock signal of some kind, and there was some in-place oscillation from (I think) the level sensor in the fuel tank, so I just ignored those specific message IDs. I then performed the highly-advanced reverse engineering technique of wiggling a thing in the car while staring at the screen. If a message showed up, it meant the wiggling had made it in some way to the CANbus, and the changed bits were what I needed to check for in the joystick driver.

Designing a joystick

We're going to be using the car with the ignition set to ON, the engine OFF (duh), transmission in park and handbrake on, so there are physical limits on what controls we can use. In the Mazda 3 power steering isn't available with the engine switched off, but you can still tug on the fixed steering wheel and get a decent force reading left and right. I read some research online which suggested you could get the brake position as a force axis measurement, but I only saw it as an on-off switch with my sniffer. (maybe the ECU only reports force when the engine is running and the pedal isn't locked?) Also to my chagrin, most of the buttons on the steering wheel etc. are wired up directly to an ECU, so those signals never even reach the CANbus! So sad.

UPDATE: someone pointed out correctly that a lot of cars will enable power steering and turn the wheels even if the engine is off. If this is your vehicle, I would strongly advise placing something slippy like a folded tarp underneath each front tyre, else you might end up with some nice bald spots where the rubber meets the bitumen.

Regardless I found some good controls after a bit of sniffing, all on the HS CANbus. I was worried I'd need to rig a second adapter in tandem to collect traffic from the car's second MS CANbus, but there weren't any usable inputs I could find (other than the indicators and the volume control) so I didn't bother.

STEERING ANGLE (range while locked):
4da: 8000c00000000000 - neutral
4da: 80d2c00000000000 - hard right
4da: 7f2ec00000000000 - hard left

ACCELERATOR (range):
201: 000040000000c800 - floored
201: 0000400000000000 - depressed

BRAKE (on/off):
205: 0000400000000000 - on
205: 0000000000000000 - off

CRUISE CONTROL (on/off):
4ec: 8000000000000000 - on
4ec: 0000000000000000 - off

DRIVER'S SIDE DOOR (open/shut):
433: 800450010004f000 - open
433: 000450010004f000 - shut

HIGH BEAMS (on/off):
433: 000450410004f000 - on
433: 000450010004f000 - off

Each CAN message has a bunch of sensors bitpacked into it, so we need some fast routines to extract only the data we care about. I used the struct unpacker from my library Mr. Crowbar (plug, plug!) to do the dirty work:

class Steering( mrc.Block ):
    RANGE = 0x00D2

    axis_raw = mrc.UInt16_BE( 0x00 )

    @property
    def axis( self ):
        return min( max( (255*(self.axis_raw - 0x8000)//self.RANGE), -255 ), 255 )


class Accelerator( mrc.Block ):
    RANGE = 0xC8

    axis_raw = mrc.UInt8( 0x06 )

    @property
    def axis( self ):
        return min( max( (255*(self.axis_raw)//self.RANGE), 0 ), 255 )


class Brake( mrc.Block ):
    button = mrc.Bits( 0x02, 0b01000000 )


class Cruise( mrc.Block ):
    button = mrc.Bits( 0x00, 0b10000000 )


class Controls( mrc.Block ):
    driver_door = mrc.Bits( 0x00, 0b10000000 )
    high_beams = mrc.Bits( 0x03, 0b01000000 )

Of course you could use Python's built in struct module to do the same thing, or hack something together with mask and shift operations. But look how clean and pure the message parser is now!

if msg_id == 0x4da:
    self.steering = Steering( msg_b ).axis
elif msg_id == 0x201:
    self.accelerator = Accelerator( msg_b ).axis
elif msg_id == 0x205:
    self.brake = Brake( msg_b ).button
elif msg_id == 0x4ec:
    self.cruise = Cruise( msg_b ).button
elif msg_id == 0x433:
    obj = Controls( msg_b )
    self.high_beams = obj.high_beams
    self.driver_door = obj.driver_door

Cruise control is a latched on/off switch, and the driver's side door is a bit unwieldy to use as a pressable button, so I had to wrap those to emit a single button press when the state changed. Bit annoying you can't register them as a long press, but whatever.

You can see the results in mazda3_joystick.py. It's pretty basic; there's a base class called Mazda3 storing all the state, with a method update( msg_id, msg_b ) that tries to parse each incoming CAN message, and if there's a match the state gets changed and an update of the uinput controls is triggered.

Obligatory live action demo

Showing is better than telling, so I taped myself testing a pile of games on my car joystick with a projector. Apologies for the quality; I am not into streaming and don't have a convenient arsenal of broadcast cameras and studio mics laying around, so you get to savour the lo-fi goodness of a single unlit 720p camcorder gaffer-taped to a car headrest.

In order, the games I tested were:

I had to make almost as many joystick configurations as there were games. A common thing I had to tweak was the range of the accelerator pedal; even if you define a uinput axis with a range of (0, 255), nearly all games expecting an analogue stick will renormalize that to something like (-255, 255). The solution was to size the uinput axis as (-255, 255) but create an enormous dead zone from (-255, 0). I was glad the event processing loop was Python code as it meant I could do stupid things like overload a control to act as two buttons, giving me the misplaced confidence to think playing Descent on camera was a good idea.

Conclusions

Doom is objectively the best game to play on a car.

I was kind of taken aback by how few people had attempted something like this already, given how accessible it turned out to be. As of the time of this writeup I found a few similar projects:

  • This team from a Finnish university rigged a VW Scirocco to play Rally Trophy. But they never wrote it up and their website is long gone :(
  • A father and son team hacked a Chevrolet Volt to play Mario Kart 64 (code). Their approach was similar to mine; main difference is they used a Raspberry PI + native CANbus module to capture the vehicle messages, then shunted the raw data via UDP to a Windows laptop which converted them to keyboard presses.
  • wirklichkeitssteuerungsgeraet (code), a wildcard demo at Revision 2017 by Akronyme Analogiker which involved connecting up a live motorcycle to drive an Amiga joystick?! This was passed to UAE through an Amiga-to-USB adapter in order to play The Cycles: International Grand Prix Racing while coasting around a car park.
  • After publishing, Stan "P1kachu" Lejay let me know about his project from earlier this year to rig a Fiat 500c to play Dirt Showdown and VDrift (code, slides). His approach uses a RasPi native CAN module + uinput. Go check out his talk at 34C3!

If you want to lay down 20 clams for an ELM327 and rig your own car to play games, nice! Hopefully there should be enough information in this writeup and the source code repositories to help you along. I am always available through email or Twitter for advice.

If you're interested in more general car hacking resources I can thoroughly recommend the following: