Hacking BMW K-BUS with Raspberry PI
Development | Mario Madzar

Hacking BMW K-BUS with Raspberry PI

Friday, Jun 16, 2017 • 10 min read
In one of our previous articles, we used Raspberry Pi to upgrade media player in the car. This time we'll use it to make a true IoT device.

If you are going to purchase a newer BMW, you have a choice to select ConnectedDrive services as special equipment. That will allow you to interact with your car using Remote Services. They also provide an app for iOS and, according to the BMW, Android app will be available soon. If you have an older car (like BMW E39) and if have Raspberry Pi in it, you can create a remote controller for the car by parsing/injecting messages into the existing bus network. We will be using Baasic as the backend service.

Introduction

Having something like Raspberry Pi in a car gives lots of possibilities for upgrading it with the new features on your own. Since we have one and it’s already connected to the bus network, the only thing we need is to publish status and accept commands from an IoT server to make it a true IoT device.

For the purpose of this post, we will monitor and control the state of the car components - not directly on the I-Bus network. However, all E39 buses are connected in some way through bus master devices, and we can communicate with other devices indirectly.

We are using our previous hardware setup to upgrade the car and control its components remotely. With this upgrade, we can help a car designed and manufactured in the past century to cope with future challenges - we combine latest technologies with the ones a few decades old.

We broadcast the current state of the car components to Baasic and act on state changes. Baasic is a backend service we are using as a free solution for IoT data storage, but will also allow us to control the car remotely over the Internet using its dynamic resources. There are other Baasic modules not mentioned in this post, but can be used to extend app functionality. We are using baasic-sdk-nodejs, but there are also other SDKs available. You can also use one of the starter kits to develop your own app.

Software and Services

This time, instead of Wheezy, we will be using the latest Jessie. As the last time, we will use Node.js and JavaScript, but now we’ll start with a default project created with express-generator and do some cleanup and tweaking. Remote control application is designed as a single-page application (SPA) hosted in Node.js.

Here is the list of main npm packages used:

  • Express web framework,
  • ibus for serial port communication,
  • Socket.io (we use Socket.io for the real-time communication between remote control app and server in the car), and
  • baasic to store/change the state of IoT device and to log the metering data for later analysis.

We store the data about speed (kilometers per hour), engine revolutions (per minute), range left until empty, and outside temperature to the Baasic metering module and parse states from the Baasic dynamic resource module.

The metering module data aggregation mechanism allows you to perform simple transformation operations like sum, count, avg, etc. in order to fetch statistical data from the Baasic end-points.

Baasic dynamic resource module is used for storing the state of the IoT device - in this case, the status of lights, doors, windows, and locks. This module can handle dynamic schemes, so pushing data is straightforward. With Baasic and its easy-to-use dashboard, you can prepare your online app and storage in no time.

Implementation

The idea is to have an app that will monitor bus network traffic, intercept relevant messages, decode them, send decoded status to the connected clients, update Baasic resources, accept messages from the client and send them to the bus network.

Our remote control application is presented as one web page with two main parts. The first part is a display of the labels with OBC values, the status of doors, windows, locks, and lights. There are actions to turn the locks on or off or to combine the lights. The second part is the bus messages logger - it’s a textbox for a custom message user can send, and a live list of all messages received on the bus line. It is used to discover messages specific to the car and test them quickly.

First, we generated the app using express-generator. For the purpose of the article, I cleaned all irrelevant code and used HTML to create pages.

In the app.js file we will set up HTML as our view engine:

app.use(express.static(__dirname + '/public'));

We have two main modules in the app, bus and Baasic modules, both placed in the /bin folder.

The bus module contains all the logic related to the parsing and sending messages to the bus. It triggers an event on relevant messages (which are broadcasted to the connected clients) and initiates an update of Baasic metering and dynamic resource data. After the message is received on the bus, it is parsed, and events status update and busmessage are triggered. This module also exposes a method to send a message to the bus. It accepts a string with a message in a hexadecimal format without length byte and checksum. Message for the bus is then converted from the hex string into a message object through the sendBusMessage() method.

This module also keeps the list of current values in sync, so only the changed statuses are pushed to the server.

function sendBusMessage(msgstr){
        //msgstring - hex string without length and checksum
        var msg = msgstr.split(' ');
        var newMsg = {
            src: parseInt(msg[0], 16),
            dst: parseInt(msg[1], 16),
            msg: []
        };
        msg.shift();
        msg.shift();
        for (var i = 0; i < msg.length; i++) {
            msg[i] = parseInt(msg[i], 16);
        }
        newMsg.msg = new Buffer(msg);

        ibusInterface.sendMessage(newMsg);  
}

The Baasic module contains all the code needed to get and update its resources. Before you start, you need to create an app on the Baasic portal and get the application key to use with the Baasic API. Depending on the permission setup on the Baasic portal, you may also need to log in to be able to get/update data.

For the login, we use membershipClient - we log only on init and membershipClient is responsible for adding authorization token on every request and keeping the session live by prolonging token expiration. If there is an error during login, the system will retry in 10 seconds. After successful login, we can start checking states from the Baasic dynamic resources and act accordingly. We check for state change using the get command every second.

var baasic = new baasic.BaasicApp('baasic app key');

function loginToBaasic(){
    baasic.membershipClient.login.login({
        username: 'username',
        password: 'password'})
            .then(function (data) {
                //connected to baasic and logged in
                checkBaasicCommands();
            },
            function (err) {
                //on error will retry in 10s
                setTimeout(() => {
                    console.log('retry login: ', err);
                    loginToBaasic();
                }, 10000);
            });
}
function checkBaasicCommands(){
    baasic.dynamicResourceClient.get('Commands', bckey)
        .then(function(request){
                var data=request.data;
                if(data.id===bckey && data.vehicle ==='BMW528'){
                    executeMessageFromBaasic(data);
                }
            },
            function(err){
                    console.log('ErrorCHECK : ' + err);
            });

    setTimeout(() => {
        checkBaasicCommands();
    }, 1000);
}

Since we want to optimize network traffic, we will not update complete dynamic resource object every time. Instead, we are sending only changed properties using the patch method. This will update object partially.

var forUpdate = { id: idToSync, schemaName: keyToSync, state: cmd};
baasic.dynamicResourceClient.patch(keyToSync, forUpdate)
    .then(function (data) {   
            console.log('patched baasic.');
            // perform success action here 
        },
         function (response, status, headers, config) {   
            console.log('error: ', response);
        });

We are using meteringClient and create method for metering purposes:

function baasicUpdateMetering(k, changed){
    var msg=[];
    var item;
    if(changed!==undefined && changed!==[]){
        if(k==='favorite'){
            if(changed.speed!==undefined){
                msg.push({name:'KPH',moduleName:'Speed',status:200,value:changed.speed,category:'Drive'});
            }
            if(changed.rpm!==undefined){
                msg.push({name:'RPM',moduleName:'Speed',status:200,value:changed.rpm,category:'Drive'});
            }
            if(changed.rangeleft!==undefined){
                msg.push({name:'RangeLeft',moduleName:'State',status:200,value:changed.rangeleft,category:'Drive'});
            }
            if(changed.tempoutside!==undefined){
                msg.push({name:'TempOutside',moduleName:'Temperature',status:200,value:changed.tempoutside,category:'ambient'});
            }
            if(changed.total!==undefined){
                msg.push({name:'Total',moduleName:'Mileage',status:200,value:changed.total,category:'mileage'});
            }
        }
    }
    if(msg.length>0){
        baasic.meteringClient.batch.create(msg)
            .then(function (data) {   
                console.log('sent to baasic.');
            },
             function (err) {   
                console.log('Err sending to baasic: ', err);
            });
    }
}

After completing the server side of the code, we will create an HTML page for the client and implement real-time status refresh. First, we need to initialize our socket.io server in the www file in the bin folder. Here, we also implement the behavior on received events from evEmitter and socket.io messages. After the user is connected to the server, we will inform him with the message io ready. and connection to the server is ready to use. Messages about status changes are broadcasted using this channel. Every message contains the key of the object in place and a list of changed properties. Simple javascript code scans element for the keys and updates their text values.

var io = require('socket.io')(server);
io.on('connection', function(socket){
  var dt = new Date();
  io.emit('chat message', dt.toUTCString() + '\t' + 'io ready.');
  socket.on('send bus message', function(msg){
    app.bus.sendMessage(msg);
  });
});

//The Emitter events
app.evEmitter.on('busmessage', function(data){
  io.emit('chat message', data);
});
app.evEmitter.on('status update', function(data){
  io.emit('status update', data);
});

Our UI for the the remote control app is in the index.html file. It is a plain HTML page for displaying elements for our actions and statuses. The Javascript code is placed in the carctrl.js file, as well as the socket.io client side implementation. After the message is received, we find the element with id equal to the one in the message and set its value to the one received in the message. On connect, the connection status is displayed in the log window.

socket.on('disconnect', function(reason){
    var dt = new Date();
    displayInLog(dt.toUTCString() + '\t' + 'io error: ' + reason);
});
socket.on('chat message', function(msg){
    displayInLog(msg);
});
socket.on('status update', function(msg){
    parseStatusUpdate(msg);
});

Decoding/Encoding Bus Messages

For the purpose of this post, we are monitoring commands received from three devices:

  • 0x00 GM III - General Module - responsible for the status of interior lights, locks, doors, windows, sunroof, hood, and trunk,
  • 0x80 IKE - Instrument Cluster Electronics - responsible for OBC values ignition status, speed, rpm, temperatures, average speed, and range left to empty, and
  • 0xD0 LCM - Light Control Module - responsible for the status of the lights.

To intercept those messages on I-Bus, we will use P-Bus and Diagnosis Bus over IKE, which will forward messages both ways.

Parsing IKE messages is pretty simple since you can find many of those already decoded, and they are the same for all cars using ISO-9142-2.

We monitor only messages with 0x80 in the first byte (coming from IKE).

  • Messages with 0x11 in the third byte are storing ignition status in fourth byte (0x00 - off, 0x01 - pos1, 0x03 - pos2, 0x07 - start).
  • Messages with 0x17 in the third byte are messages with total traveled distance - see the code for conversion into a readable format.
  • Messages with 0x18 in the third byte are storing speed and rpm. Multiply the fourth byte with 2 to get the speed in kilometers per hour and multiply the fifth byte with 100 to get revolutions per minute.
  • Messages with 0x19 in the third byte are storing temperature information in degrees Celsius. The fourth byte is outside temperature, and fifth is engine coolant temperature.
  • Messages with 0x24 in the third byte are values from OBC. The ones with 0x06 in the fourth byte are the value of range in kilometers left before empty. The string value is stored in bytes from 6 to 9. Messages with 0x0A store average speed in the same manner as previous.
  • Messages that we monitor from GM and LCM modules are composed by the same principle - they store values in bits, and every bit defines a different function. These messages tend to differ from year to year or even model to model, so it’s crucial to test and find the ones that work on your car.

For the GM messages, we are monitoring only messages with 0x00 in the first byte (coming from GM III module), and 0x7A in the fourth byte. The value is contained in bytes six and seven - the fifth one is empty. If we convert them from hexadecimal to binary, we get two groups with 8 bits (0 and/or 1). From there it’s all test and log - open door and check message - over and over, and you will have enough to decode bits and map them to functions.

There is a function in the bus module that you can use to convert to binary. It accepts a message without source, destination, and lengths - only message data. This specific one will convert array from second byte (to skip the fifth byte in the message) and return an array of strings. Every item in the array represents one byte.

function Buff2Bin2(bArray) {
    //from second element on in the array
    var r = [];
    for (var i = 1; i < bArray.length; i++) {
        r.push(padLeft(bArray[i].toString(2), 8));
    }
    return r;
}

Here is a list of decoded bits in the first byte:

1 - interior lights
2 - lock locked
3 - lock unlocked 
4 - right rear door open
5 - left rear door open
6 - right front door open
7 - left front door open

Here is a list of decoded bits in the second byte:

1 - hood
2 - trunk
3 - sunroof
4 - right rear window open
5 - left rear window open
6 - right front window open
7 - left front window open

For the lights status, we will monitor the LCM module. Those messages have 0xD0 in the first byte. We filter only those with 0x5B in the forth byte. Structurally, this message differs from previous only because it stores data in four bytes, and not in two like GM.

Here is a list of decoded bits for my car:

Byte 1:
0 - turn signals/indicator fast blink
1 - right indicator
2 - left indicator
3 - rear fogs
4 - front fogs
5 - high beam
6 - low beam
7 - parking lights

Byte 2: this byte indicated not working lights
1 - error indicator right
2 - error indicator left
3 - error rear fogs
4 - error front fogs
5 - error high beam
6 - error low beam
7 - error parking lights

Byte 3:
4 - reverse light on
5 - indicator sync on
6 - brake lights

Byte 4:
4 - parking light left
5 - parking light right
6 - brake light left
7 - brake light right

Even if there are codes that can be found online, those will probably not work on your car - you may have to tweak them to suite your car configuration. For a start, a good list of E39 specific codes can be found on e39-forum.de.

Testing

To run the application and install dependencies, use:

npm install

To start the application using npm:

npm start

After that, the Remote control app will be accessible at http://localhost:3001.

The project is available on our demo site Baasic ShowCase IoT RaspberryPI BMW, and on Github here and here.