Hacking BMW I-BUS With Raspberry PI
Development | Mario Madzar

Hacking BMW I-BUS With Raspberry PI

Thursday, Dec 1, 2016 • 7 min read
With credit card-sized computers being widely available, putting one in a car and using it as a media player becomes pretty easy. That way we can focus on other challenges, like using original car radio display and buttons for controlling it - instead of putting in aftermarket components and messing with the original dashboard layout.

Nowadays, there are lots of aftermarket devices available for different car models, ranging from simple radio devices with CD to full blown Android devices with touch screens, GPS, cameras, and other goodies. Putting one in a car as the media player/carputer is the simplest thing if you want to upgrade your car’s media. You can even find ones that imitate the original Navigation module, like the one from Erisin. But making your solution using credit card-sized computer with original controls and displays will provide all the functionality of aftermarket components while keeping the original dashboard layout and retro look.

If you have Navigation module and want to use the original display, you can try OpenBM.

Introduction

A few years ago, after a lot of searching, I got my hands on decent 11 / 98 BMW 528iA E39. It is a submodel produced from 09/98 until 09/01 with two Vanos and native OBDII support. It has lots of options including CD changer, CD player, and High Cluster option. Although it is almost 20 years old, it’s full of electronics and can be used for everything you want to do, or even to see what is wrong with it - you just need to connect it to a diagnostics computer. Understanding how the electronics works becomes crucial even for simple things like resetting service intervals or erasing CAN errors.

The core of car communication is a bus line - two-way serial communication line, with one or two wires for the signal. All cars have multiple bus systems that group electronics depending on their use in the car, like CAN-bus, I-bus, K-bus, D-bus, etc. We will focus on I-bus (Information Bus) which is used to control radio, CD, navigation, interior lights, parking sensors, steering wheel buttons, etc. I-Bus can be found on all BMW E39 (5-series 1995-2003), E38 (7-series 1994-2001), E46 (3-series 1998-2005), E53 (X5 1999-2006), some Minies under BMW and some Rovers.

Hardware

I decided to put in Raspberry PI 2 as a media player but use all original displays and controls already present in the car. Since the CD changer unit works only with audio CDs and is already using I-Bus protocol for communication with other devices in the car, the obvious choice was to replace it with RPI and put RPI in the trunk in its place.

Beside RPI itself, we need a power supply and a device for connecting to I-Bus line - and of course, a car.

For the power supply, I am using a universal Dual USB Port Car Lighter Socket Charger adapter I already had (like this one). I dismantled it and connected wires directly with some soldering. You can use other solutions also, as long as the input is 12V and output is 5V 2A at least.

For the I-Bus connection, I am using Reslers USB I-Bus adapter. The interface is based on a Melexis TH3122 chip that implements collision detection and FTDI chip as USB-serial controller. This setup allows plug and play solution by simply putting the adapter into RPI’s USB port and connecting I-Bus line used for CD changer on other side. I also tried using MCP2025 chip - LIN Transceiver found in airbag modules. It is cheaper than Melexis TH3122 but I had problems with sending messages and abandoned it as a solution.

Here are the settings used to configure serial port to work correctly connected to the I-Bus network:

bps: 9600
data bits: 8
parity: none
stop bits: 1
Row control: none

use FIFO buffers - 16550 compatible UART
receive buffer: 16
transmit buffer: 16

And here is how RPI is installed in the trunk replacing the CD changer: RPi in the trunk

Software

Obviously, we will use Linux as OS on RPI. I picked Wheezy distribution because we want to cut down boot time to the minimum and keep it as small as possible. Since Wheezy is obsolete now, you can use Jessy. Both will fit on a 4GB SD card.

We will host our app in Node.js and use JavaScript as the programming language.

Messages on I-Bus are sent as bytes with no end-message flag, so we need to parse all received bytes and try to compose a message from those, calculate hash and use the last byte for comparison. Here you can find the description of the I-Bus message format. To send messages, we need to monitor “empty” time after the last byte received and send our message then to avoid collision with others. Luckily, Osvath-Boros Robert developed node-ibus plugin that allows us to send and receive messages to and from I-Bus easily.

As a startup project, we will use node-ibus-mediacenter - thanks to Robert. His solution works with Navigation module on the I-Bus network, so we just need to develop our part for the MID unit.

For the media player, we will use MPlayer which supports command line interface and can play all known media formats including online streams. As a wrapper around MPlayer, we will use node-mplayer plugin.

Implementation

The idea is to use MID buttons to navigate through folders with our media, start playing media, fast forward and reverse through the current song, and use the steering wheel buttons to skip songs back and forth.

Also, we want to use all 20 characters of the MID display - not only first eight, but also all others that are normally used by the board computer.

Because we are replacing CD changer with RPI, we need to make RPI act as the original CD Changer. To do that, we need to mimic CD changer behavior. We monitor for all I-Bus network traffic in IbusEventListenerMID.js using ‘data’ event of the node-ibus plugin.

Here is the flow of events and actions when original CD changer is present:

  1. After the system starts, MID listens for device broadcasts to detect their presence
    • We want to replace the ‘CD’ label on button 6 with ‘MP3’ text, so after system starts, we will monitor for message that describes action buttons (message that is sent from radio to MID (68 xx c0), starts with 21 40 00 09 and ends with 43 44), and replace it with our custom label (using message 68 xx c0 21 40 00 09 05 05 4D 50 33)
  2. RPI broadcasts 18 xx FF 02 01 - announce CD - this needs to be sent at least every three seconds for MID to be able to detect that CD changer is present
  3. MID receives broadcasted message and sends CD pool message - 68 xx 18 01
  4. RPI responds as CD changer with 18 xx 68 02 00
  5. MID requests CD status with 68 xx 18 38 00 00
  6. RPI responds with 68 xx 18 xx 01 - CD playing CD 01 - we will always send this command when a song is played, and later we will replace this with our custom text (for example artist and song name)

Note: We keep all messages in the messages.js file. Feel free to modify those to support your units. The node-ibus plugin automatically calculates the length of the message and puts value in the second byte, so we will use xx in that place to make things more readable.

All messages are in hexadecimal format.

List of media files is loaded on application start. If there is ‘list.txt’ file present, its content will be loaded, and if not, then folder defined in ‘config.js’ will be parsed.

Now we have MID unit thinking that our RPI is the CD changer.

To make things simpler while composing hexadecimal messages, we are using a getPaddedLenBuf() method to convert ASCII string to string that contains hexadecimal codes which we can use in our I-Bus messages:

function getPaddedLenBuf(text, len) {
    var outputTextBuf = new Buffer(len);
    outputTextBuf.fill(0x20);

    var textBuf = (new Buffer(text, 'utf-8')).slice(0, len);

    // copy to the new padded buffer
    textBuf.copy(outputTextBuf);
    return outputTextBuf;
}

Let’s move forward and create a custom menu for our RPI player…

Button no. 6 is usually used to activate the CD changer. MID will send the CD changer status request message, and we will respond with our custom menu that will replace the CD changer’s menu.

We are also replacing CD string in the Audio menu with the MP3: Main Menu

var msg = {
        src: 0x68,
        dst: 0xc0,
        msg: Buffer.concat(
            [new Buffer([0x21, 0x40, 0x00, 0x40, 0x06]),
                getPaddedLenBuf('^ FIND ', 7), new Buffer([0xc1, 0x06]),
                getPaddedLenBuf('OK  BACK', 8),
                new Buffer([0x20, 0x06]),
                getPaddedLenBuf('PLAY    ', 8)
            ])
    };

Here is how our new menu looks now: RPi Menu

Menu commands:

  • arrow up - move to previous folder/file
  • arrow down - move to next folder/file
  • OK - select folder/file
  • BACK - exit folder
  • PLAY - play folder/file

Now we need to monitor MID buttons (and act accordingly) and handle steering wheel buttons by responding with the song title when the CD-01 message is displayed. Here is that part of the code:

function onData(data) {
    if (parseInt(data.src, 16) == msgs.devices.radio) { //From radio
        if (parseInt(data.dst, 16) == msgs.devices.cd_changer) { //To CD changer
            if (tools.compareMsg(data, msgs.messages.rad_cdReqParams) || tools.compare(data, msgs.messages.rad_cdReqPlay)) {
              _self.cdChangerDevice.sendPlaying0101();
            }
            else if (tools.compareMsg(data, msgs.messages.rad_cdPool)) {
                _self.cdChangerDevice.respondAsCDplayer();
            }
        }
        else if (parseInt(data.dst, 16) == msgs.devices.mid) { //To MID
            if (tools.startsWith(data, msgs.messageParts.mid_buttons_for_replaceStart)
                    && tools.endsWith(data, msgs.messageParts.mid_buttons_for_replaceEnd)) {
                _self.midDevice.showMp3Menu();
            }
            else if (tools.compareMsg(data, msgs.messages.replace_rad2mid_CD0101)) {
                _self.midDevice.setTitle1(_self.title1);
                _self.midDevice.setTitle2(_self.title2);
            }
            else if (tools.compareMsg(data, msgs.messages.replace_rad2midCDbuttons)) {
                _self.midDevice.showMenu1();
            }
        }
    }
    else if (parseInt(data.src, 16) == msgs.devices.mid) { //From MID
        if (parseInt(data.dst, 16) == msgs.devices.radio) { //To radio
            if (tools.compareMsg(data, msgs.messages.radio_1_press)) {
                _self.currentPlaylist.up();
            }
            else if (tools.compareMsg(data, msgs.messages.radio_2_press)) {
                _self.currentPlaylist.down();
            }
            else if (tools.compareMsg(data, msgs.messages.radio_3_press)) {
                _self.currentPlaylist.enter();
            }
            else if (tools.compareMsg(data, msgs.messages.radio_4_press)) {
                _self.currentPlaylist.back();
            }
            else if (tools.compareMsg(data, msgs.messages.radio_5_press)) {
                _self.currentPlaylist.current = _self.currentPlaylist.browseCurrent;
                _self.currentPlaylist.mode = "play";
                _self.currentPlaylist.play();
            }
            else if (tools.compareMsg(data, msgs.messages.radio_6_press)) {
                _self.currentPlaylist.queue(_self.currentPlaylist.browseCurrent);
            }
            else if (tools.compareMsg(data, msgs.messages.radio_rev_press)) {
                _self.currentPlaylist.seek(-10);
            }
            else if (tools.compareMsg(data, msgs.messages.radio_ff_press)) {
                _self.currentPlaylist.seek(10);
            }
            else if (tools.compareMsg(data, msgs.messages.radio_m_press)) {
                _self.currentPlaylist.currentTime(function (data) {
                });
            }
        }
    }
    else if (parseInt(data.src, 16) == msgs.devices.mfl) { //From MFL
        if (parseInt(data.dst, 16) == msgs.devices.radio) { //To radio
            if (tools.compareMsg(data, msgs.messages.previous_press)) {
                _self.currentPlaylist.previous();
            }
            else if (tools.compareMsg(data, msgs.messages.next_press)) {
                _self.currentPlaylist.next();
            }
        }
    }
}

Navigation through our media is handled by the ‘Playlist.js’ object - whenever a display change is required, it will emit ‘statusUpdate’ event. ‘IbusEventListenerMID.js’ will monitor for that event and store titles in variables, so when it gets the request to display current status of the CD changer (‘replace_rad2mid_CD0101’ message is received), it will display our custom text by calling ‘setTitle1’ and ‘setTitle2’ from ‘MidDevice.js’. This will result in executing ‘UpdateScreen1()’ method:

function updateScreen1() {
    //working radio screen
    ibusInterface.sendMessage({
        src: 0x68,
        dst: 0xc0,
        msg: Buffer.concat([new Buffer([0x23, 0x40, 0x20]), getPaddedLenBuf(_self.title2, 11)])
    });

    //working BC screen
    ibusInterface.sendMessage({
        src: 0x80,
        dst: 0xc0,
        msg: Buffer.concat([new Buffer([0x23, 0x04, 0x20]), getPaddedLenBuf(_self.title1, 20)])
    });
}

You can find a complete code for this solution in the attachment below. Feel free to modify it to fulfill your needs.

Download attachment