I have had an instrument cluster from a 2012 LW Ford Focus laying around for some time so I thought it would be a fun project to get it working with a racing game. Being from a relatively modern car it collects its information via the CAN bus, however after some searching I was unable to find anyone who has reversed engineered the Focus’s message structure (Well not for the cluster anyway). So having the same model of car I set out to try and decode the messages well enough to drive the cluster on my own.

The first thing I did was to try and find myself a wiring diagram, after a lot of searching I luckily come across one. However to my surprise I discovered the 2012 LW focus has a number of CAN bus networks 2 of these networks where connected to the cluster. The bus’s connected to the cluster where the MS bus and the I-CAN bus looking further at the cars schematics I found that the cluster was not shearing any data bus that are directly connected to the cars ECU. As my first goal was to try and manipulate the RPM needle it seemed the road ahead was harder that I had initially thought it would be. After some reading online I determined that the MS bus was most likely the best candidate for sniffing as it was also connected to the car’s body control module which manages a number of functions in the car and dose share a HS CAN bus connection from the cars ECU.


Collecting the CAN messages

Now that I had a data bus of interest I needed to find a point to tap into the MS CAN bus, fortunately for me the MS bus is brought right out onto the OBD2 connected (pin 11: MS-CAN – and pin 3: MS-CAN +), this was great as I no longer needed to pull apart the dash to wire in a tail. For the CAN adapter I used the CANable board flashed with the knockoff PCAN firmware so I could use the python library (needed for another project I was working on). However any CAN module will work provided it is able to run down to 125kbps as that is the speed the MS bus operates. If your looking for a cheap CAN interface the MCP2515 Arduino shields are a good option (However keep in mind some tools may not support this option, or you might have to make your own).

Now that I had my hardware sorted I moved onto the next peace of the puzzle, the software. I’m still a bit shy with Linux (there are loads of CAN tools for it) so I was more so limited to windows tools. After a bit of research I had settled for a tool called BusMaster. It had a bit of a learning curve but once I got the hang of things it was very useful for my endeavors.

Using my CANable board hooked up the car and BusMaster configured to log the comings and goings on the MS bus I began by manipulating as many things as I could with the car. I started by unlocking the car and opening the door, turning on and off the headlights, starting the engine, messing with the indicators, hand break, etc. essentially trying to collect as much data as possible so I can analyze the changes in the data stream at a later date and hopefully pinpoint what messages belong to each cluster function.

Now I was left with a boat load of data!

Setting up logging in BusMaster is relatively simple go to Logging > Configure(drop down) > Add > ‘name the file and save’ > click OK > in the logging drop down click ‘Activate’.

Once you click connect it will begin logging to the file specified (provided you have setup your CAN board in the driver section of BusMaster otherwise it will default to simulator mode).


Decoding the mess of data

Truth be told at first I was completely overwhelmed by the amount of data that had be collected (all 83,204 frames!) so first I decided to do a quick sanity check. I hooked up the cluster to a 12V supply and my CAN adapter. Using the replay function in BusMaster to replay the log file to the cluster on the bench.

Enabling replay is relatively simple in BusMaster can be done by going to Replay > Configure(drop down) > Add > (select your log file), ensure retain recorded delay is selected and click OK. Now when you click connect (provided you have setup your CAN board in the driver section of BusMaster) it will replay the log file.

Sure enough after waiting a few seconds the cluster came to life replaying the events that had been recorded. Now that I have a base line state I had a rather simple idea to thin out the number of messages that where being replayed as the MS bus is shared by number of modules and It is rather busy. I began by applying filters to message ID’s one by one and looking for changes on the cluster to identify what ID belongs to what function.

Filters in BusMaster are relatively simple to apply to a recording in playback they can be configured by going to: Filters > Add a filter group and set it to whitelist or black list depending on your requirements > add the message ID to the line item and any other requirements and click Add. Lastly you will also need to click on the replay drop down and click “enable filters” now when you connect in replay mode your filters will apply.

Once I had a list of messages that 100% interacted with the cluster I began to experiment with them by shifting bits around working out what other functions they serve and what triggers the function itself. A list of the messages I have either fully or partially decoded can be found on my Github here or an excel version here. Please note this is very much a work in progress so I will update the list as I find more.


Linking everything together

Now that we know what dose what we can take a look at displaying information collected from a game. At the time I had been messing about with BeamNg so decided I will start there, fortunately BeamNg already has a basic API for exporting data called OutGauge. Using OutGauge and the PCAN library I was able to through together a rough and ready program for sending data to the cluster.For being rather thrown together it works well thanks to angelo234‘s simple Outgauge example it made the task relatively straight forward.

Below is the WIP python program that is used to send the data to the cluster:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import socket
import struct
import can
import time
import threading


msg_DashWake = can.Message(arbitration_id=0x80, is_extended_id=False, data=[0x77, 0x83, 0x07, 0x3F, 0xD9, 0x06, 0x03, 0x81])
msg_Airbag = can.Message(arbitration_id=0x040, is_extended_id=False, data=[0x66, 0x02, 0x80, 0xFF, 0xBE, 0x00, 0x80, 0x00])
msg_esc = can.Message(arbitration_id=0x70, is_extended_id=False, data=[0x00, 0x98, 0x04, 0x80, 0x10, 0xF4, 0xE8, 0x10])
msg_HnbBrkWar = can.Message(arbitration_id=0x240, is_extended_id=False, data=[0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00])
msg_BrkFluid = can.Message(arbitration_id=0x290, is_extended_id=False, data=[0x98, 0x00, 0x01, 0x00, 0x04, 0x03, 0x92, 0x37])
msg_immobiliser = can.Message(arbitration_id=0x1E0, is_extended_id=False, data=[0x42, 0x80, 0x38, 0x00, 0x80, 0x00, 0x80, 0x00])
msg_HillAsy = can.Message(arbitration_id=0x1B0, is_extended_id=False, data=[0x01, 0x50, 0x27, 0x00, 0x00, 0x00, 0x80, 0x00])
msg_SteeringAndWasher = can.Message(arbitration_id=0x3A, is_extended_id=False, data=[0x82, 0x83, 0x00, 0x02, 0x80, 0x00, 0x00, 0x00])
msg_EngMalf = can.Message(arbitration_id=0x2A0, is_extended_id=False, data=[0x06, 0x00, 0x00, 0x00, 0x3e, 0x4F, 0x80, 0xE5])
msg_Alternator = can.Message(arbitration_id=0x300, is_extended_id=False, data=[0x00, 0x00, 0x00, 0x21, 0xC0, 0x00, 0x00, 0x00])


message_Timers = [17, 63, 1, 101, 88]  # random starting positions to space messages out

Oil_light = True
ENG_light = True

# create a bus instance using 'with' statement,
# this will cause bus.shutdown() to be called on the block exit;
# many other interfaces are supported as well (see documentation)

# Create UDP socket.
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Bind to BeamNG OutGauge.
sock.bind(('127.0.0.1', 4444))


def thread_function():
    global car_RPM
    global car_Speed
    global car_Fuel


    global Oil_light
    global ENG_light


    bus = can.Bus(interface='pcan', channel='PCAN_USBBUS1', bitrate=125000)

    while True:

        # ---------------------------------------------------------------------------------
        if message_Timers[0] >= 50:

            # Speed and RPM
            high_byte_RPM = (car_RPM >> 8) & 0xFF  # Extracting high byte (most significant bits)
            high_byte_RPM |= 0xC0  # Set the high nibble to D (1101 in binary)
            low_byte_RPM = car_RPM & 0xFF  # Extracting low byte (least significant bits)
            high_byte_Speed = (car_Speed >> 8) & 0xFF  # Extracting high byte (most significant bits)
            low_byte_Speed = car_Speed & 0xFF  # Extracting low byte (least significant bits)
            bus.send(can.Message(arbitration_id=0x110, is_extended_id=False, data=[0xC0, 0x03, 0x0A, 0x00, high_byte_RPM, low_byte_RPM, high_byte_Speed, low_byte_Speed]))
            time.sleep(0.0001)  # Small spacing between messages
            bus.send(msg_Airbag)
            time.sleep(0.0001)  # Small spacing between messages
            bus.send(msg_esc)
            time.sleep(0.0001)  # Small spacing between messages
            bus.send(msg_DashWake)
            time.sleep(0.0001)  # Small spacing between messages
            bus.send(msg_SteeringAndWasher)
            message_Timers[0] = 0
        else:
            message_Timers[0] = message_Timers[0] + 1

        # ---------------------------------------------------------------------------------
        if message_Timers[1] >= 100:
            bus.send(msg_HillAsy)
            time.sleep(0.0001)  # Small spacing between messages
            bus.send(msg_immobiliser)
            message_Timers[1] = 0
        else:
            message_Timers[1] = message_Timers[1] + 1

        # ---------------------------------------------------------------------------------
        if message_Timers[2] >= 120:
            bus.send(msg_HnbBrkWar)
            time.sleep(0.0001)  # Small spacing between messages
            # Engine and oil light
            if Oil_light and ENG_light:
                bus.send(can.Message(arbitration_id=0x250, is_extended_id=False,
                                     data=[0x30, 0x89, 0x18, 0x04, 0x1C, 0x11, 0x00, 0x13]))
            elif Oil_light:
                bus.send(can.Message(arbitration_id=0x250, is_extended_id=False,
                                     data=[0x20, 0x0D, 0x18, 0x04, 0x1C, 0x11, 0x00, 0x00]))
            elif ENG_light:
                bus.send(can.Message(arbitration_id=0x250, is_extended_id=False,
                                     data=[0x30, 0xD5, 0x18, 0x04, 0x1C, 0x11, 0x00, 0x13]))
            else:
                bus.send(can.Message(arbitration_id=0x250, is_extended_id=False,
                                     data=[0x20, 0xD5, 0x18, 0x04, 0x1C, 0x11, 0x00, 0x00]))
            message_Timers[2] = 0
        else:
            message_Timers[2] = message_Timers[2] + 1

        # ---------------------------------------------------------------------------------
        if message_Timers[3] >= 250:
            bus.send(msg_EngMalf)
            time.sleep(0.0001)  # Small spacing between messages
            bus.send(msg_BrkFluid)
            message_Timers[3] = 0
        else:
            message_Timers[3] = message_Timers[3] + 1

        # ---------------------------------------------------------------------------------
        if message_Timers[4] >= 400:
            # turn off the alternator light if rpm is above 250
            if car_RPM > 125:
                bus.send(msg_Alternator)

            time.sleep(0.0001)  # Small spacing between messages

            # Fuel
            high_byte_fuel = (car_Fuel >> 8) & 0xFF  # Extracting high byte (most significant bits)
            low_byte_fuel = car_Fuel & 0xFF  # Extracting low byte (least significant bits)
            high_byte_fuel |= 0x10

            bus.send(can.Message(arbitration_id=0x320, is_extended_id=False,
                                 data=[0x00, 0x00, high_byte_fuel, low_byte_fuel, 0x00, 0x00, 0x00, 0x00]))
            message_Timers[4] = 0
        else:
            message_Timers[4] = message_Timers[4] + 1

        time.sleep(0.001)  # Hold for 1ms

thread = threading.Thread(target=thread_function)
thread.start()


while True:
    data = sock.recv(96)
    temp = 0

    if not data:
        print("Error")  # break - Lost connection

    # Unpack the data.
    outsim_pack = struct.unpack('I4sH2c7f2I3f16s16si', data)

    # Process the Outsim Gauges RPM ----------------------------------------
    number = outsim_pack[6] / 2
    car_RPM = int(number)

    # Process the Outsim Gauges Speed ----------------------------------------
    number = outsim_pack[5] * 3.6
    number = number * 95
    car_Speed = int(number)

    # Process the Outsim Gauges Fuel ----------------------------------------
    temp = outsim_pack[9] * 100
    temp = int(temp)
    temp = 7936 - (temp * 5.3)
    car_Fuel = int(temp)

    # Outgauge dose not export oil pres so if the oil is too hot trigger the oil light
    if outsim_pack[11] > 145:
        Oil_light = True
    else:
        Oil_light = False

    # Set the flashing engine light  if the engine is overheating
    print(str(outsim_pack[8]))
    if outsim_pack[8] > 125:
        ENG_light = True
    else:
        ENG_light = False

Final product

Overall for a start things went really well there is still a lot of things that need to be decoded on the cluster and the python program is still a bit rough. But it is a WIP and I will update things as I go.

By Inky

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.