CAN Bus Reverse Engineering – Door Status

By | December 24, 2020

Previously, I wrote about how I sniffed the CAN bus on my vehicle to locate the VIN. In this article, I am going to discuss how I located the status of the doors on my vehicle. All code in this article is licensed under the MIT License and I want to stress that I am not liable for any claims, damages or other liability from executing the software in this article – I have purposefully left all the channels on vcan0 so someone would need to modify the code first before it could do anything on a real network. Please use common sense and don’t inject CAN messages on a vehicle that is driving or a vehicle that is not compatible as messages containing Engine RPM information on one manufacturers vehicle might be a blow airbags event in another.

First I fired up cansniffer which is very useful for this sort of operation for the fact that it groups messages by CAN ID. To find the door signal, I made the assumption that it would be a lower priority message and therefore have a higher CAN ID ($200 <). On a CAN Bus, the lower the CAN ID value the higher the priority. When two CAN nodes on the network are transmitting at the same time the arbitration process will let the higher priority (lower CAN ID) message win and therefore be transmitted. Therefore, for example, a message containing critical Powertrain data using CAN ID $100 will win out over a message containing relatively trivial infotainment setting data using CAN ID $250. Most people would agree keeping the engine running optimally is more important than the current selected radio channel showing up on the instrument cluster without delay. Consequently, I looked at the data in the higher CAN ID messages as I opened and closed my driver door. After a couple of minutes, I made my way to CAN ID $360 and noticed the value was changing with the door opening and closing.

Output from cansniffer:
Door Open/Close cansniffer capture

*** Please note that I am counting starting at 0 from here on out when talking about bus data. ***

Now that I thought I had my message, I moved over to candump and set a filter on CAN ID $360:

candump -ta -a can0,360:7FF

Next I opened the closed driver door:

 
 (1607988909.806400)  can0  360   [8]  C0 40 3F 02 87 EE BC B7   '.@?.....'
 (1607988909.950275)  can0  360   [8]  C0 40 3F 02 87 EE B8 B7   '.@?.....'
 (1607988910.104414)  can0  360   [8]  C0 40 3F 02 87 EE B8 B7   '.@?.....'
 (1607988910.250150)  can0  360   [8]  C0 40 3F 02 87 EE B8 B7   '.@?.....'
 (1607988910.405517)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988910.550144)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988910.704636)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988910.850137)  can0  360   [8]  C0 40 3E 02 87 EE FC B7   '.@>.....'
 (1607988911.005631)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988911.150253)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988911.304501)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988911.450249)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988911.606372)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'
 (1607988911.750243)  can0  360   [8]  C0 40 3E 02 87 EE FC B7   '.@>.....'

When analyzing the data I noted that this CAN message goes out on the bus every 150 ms. Next, I noted that the lower nibble (bits 0-3) of byte 6 cycled from $8 to $C (6 messages $8, 1 message $C). Most importantly in the context of this post, I made the observation that in the CAN bus log 1 bit when from 0 to 1 and another bit went from 1 to 0. For those that missed it, I will call out the specific moment of transition from the bus log above:

 
 (1607988910.250150)  can0  360   [8]  C0 40 3F 02 87 EE B8 B7   '.@?.....'
 (1607988910.405517)  can0  360   [8]  C0 40 3E 02 87 EE F8 B7   '.@>.....'

When the door went from closed to opened byte 2 bit 0 (2[0]) went from 1 to 0 (cleared) and byte 6 bit 6 (6[6]) went from 0 to 1 (set). At this point I made the assumption that Driver Side Front Door (Front Left) Closed indicator must be at 2[0] and Open indicator must be at 6[6]. I tested this assumption a few more times under various conditions until I was certain that this was the meaning of those two bits. I found it odd that there was a redundancy; why not have a single bit for an individual door status? I am sure the engineers had a reason for it. My current theory is that as this CAN message has no CRC in it and perhaps it was a plausibility check in the event that there was corruption in the transmission of a message then a receiver could safely ignore a “clopen” door status. I then lathered, rinsed, and repeated this process for the Driver Side Rear Door, Passenger Doors, Hatchback/Luggage Compartment, and the Hood/Bonnet. The result of this process is the table below:

Offset Byte[Bit] Length Byte[Bit] Value Comments
0[0] 1[0] $C0 Message Type (Static)
1[0] 0[1] Open – 1
Shut – 0
Driver Side Rear Door (Rear Left) Open
1[1] 0[1] Open – 1
Shut – 0
Passenger Side Front Door (Front Right) Open
1[2] 0[1] Unused – 0 Reserved? Sunroof/Moonroof?
1[3] 0[1] Open – 1
Shut – 0
Hood/Bonnet Open
1[4] 0[4] $4 Unknown (Static)
2[0] 0[1] Closed – 1
Opened – 0
Driver Side Front Door (Front Left) Closed
2[1] 0[1] Closed – 1
Opened – 0
Passenger Side Front Door (Front Right) Closed
2[2] 0[1] Closed – 1
Opened – 0
Driver Side Rear Door (Rear Left) Closed
2[3] 0[1] Closed – 1
Opened – 0
Passenger Side Rear Door (Rear Right) Closed
2[4] 0[1] Closed – 1
Opened – 0
Hatchback (Luggage Compartment) Closed
2[5] 0[1] Closed – 1
Opened – 0
Hood/Bonnet (Engine Compartment) Closed
2[6] 0[1] Open – 1
Shut – 0
Hatchback (Luggage Compartment) Open
2[7] 0[1] Open – 1
Shut – 0
Passenger Side Rear Door (Rear Right) Open
3[0] 1[0] $02 Unknown (Static)
4[0] 1[0] $87 Unknown (Static)
5[0] 1[0] $EE Unknown (Static)
6[0] 0[4] $C/$8 Cycles between 1 $C, 6 $8
6[4] 0[2] $03 Unknown (Static)
6[6] 0[1] Open – 1
Shut – 0
Driver Side Front Door (Front Left) Open
6[7] 0[1] $01 Unknown (Static)
7[0] 1[0] Engine Off – $02
Engine Cranking – $03
Engine Recently Started – $BF
Engine Running (Idle) – $BB
Engine Kill (Keying Off) – $BA
Engine Running (After a while) – $B7
Engine Running Status

Another thing I wanted to see was if I could override the regular CAN signal to make the Instrument Panel Cluster (IPC) believe the the door was open when it was not. I installed the python-can module for Python and wrote a little Python script to send a message that the Driver Side Front Door (Front Left) was open. The key to overriding a CAN signal on a bus is to send it faster than the normal CAN signal so I sent mine every 50 ms since it is normally sent every 150 ms. The script is below:

import can
import time

bustype = 'socketcan'
channel = 'vcan0' # can0 on non-virtual physical bus

bus = can.interface.Bus(channel=channel, bustype=bustype, bitrate=500000)
for i in range(600):
    cyclicNibble = 0x0C if i % 7 == 0 else 0x08
    #driver door open, engine off
    msg = can.Message(arbitration_id=0x360, 
        data=[0xC0, 0x40, 0x3E, 0x02, 0x87, 0xEE, 0xF0 | cyclicNibble, 0x02])
    print(msg)
    bus.send(msg)
    time.sleep(0.05) # send every 50 ms

Running this on vcan0 (virtual can bus 0) outputs the following:

Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee fc 02
Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee f8 02
Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee f8 02
Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee f8 02
Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee f8 02
Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee f8 02
Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee f8 02
Timestamp:        0.000000    ID: 00000360    X                DLC:  8    c0 40 3e 02 87 ee fc 02

When I switched over to can0 and ran this on my vehicle, I got no results. The closed door still showed up as closed on my Instrument Panel Cluster. In my search to understand why I found a couple of very useful resources. The first bit of useful information I found on my vehicle, a Ford Focus MK3 (C346), was from the OpenXC project. This project which is run by Ford included an OBDII Pin Diagram. The pin diagram at the link showed that for my C-Platform Ford vehicle I had three accessible networks which I could easily verify on my vehicle’s OBD connector. The second bit of useful information was some network topology diagrams that came up in my search engine from focusst.org and so putting everything together I was able to determine:

OBD Pins Bus Name Description
6 (+) & 14 (-) CAN1/HS-CAN OBD-Mandated CAN
3 (+) & 11 (-) CAN2-1/MS-CAN Ford Secondary CAN
1 (+) & 8 (-) CAN2-2/I-CAN Ford Infotainment CAN

Those topology diagrams showed the following:
Bus Topology Diagram
From here it was clear that sending the Door Status message that resides on HS-CAN would have no effect on what the Instrument Panel Cluster (IPC) was using and therefore displaying since it resides on the MS-CAN bus and the I-CAN bus. I assumed based on looking at these bus topology diagrams that the Body Control Module (BCM) is the sender of the door status message and must broadcast the door status message on both the HS-CAN and MS-CAN buses. At a later date, I will need to investigate the MS-CAN bus later to see if I can control the display regardless of the true vehicle status.

Since I had mined useful information from the vehicle and it would have been a shame not to make use of it, I wrote a little Python script to monitor the bus for the door status message and to show updates whenever the vehicle status changed.

import can

bustype = 'socketcan'
channel = 'vcan0' # can0 on non-virtual physical bus

driver_front_door_closed = False
passenger_front_door_closed = False
driver_rear_door_closed = False
passenger_rear_door_closed = False
hatchback_closed = False
hood_closed = False

def isbitset(b, pos):
    return (b & (1 << pos)) != 0

bus = can.interface.Bus(channel=channel, bustype=bustype, bitrate=500000)
bus.set_filters([{"can_id": 0x360, "can_mask": 0x7ff, "extended": False}])
while True:
    prev_driver_front_door_closed = driver_front_door_closed
    prev_passenger_front_door_closed = passenger_front_door_closed
    prev_driver_rear_door_closed = driver_rear_door_closed
    prev_passenger_rear_door_closed = passenger_rear_door_closed
    prev_hatchback_closed = hatchback_closed
    prev_hood_closed = hood_closed

    msg = bus.recv(0.0)
    if msg is not None:
        driver_front_door_closed = isbitset(msg.data[2], 0)
        passenger_front_door_closed = isbitset(msg.data[2], 1)
        driver_rear_door_closed = isbitset(msg.data[2], 2)
        passenger_rear_door_closed = isbitset(msg.data[2], 3)
        hatchback_closed = isbitset(msg.data[2], 4)
        hood_closed = isbitset(msg.data[2], 5)

    if (prev_driver_front_door_closed != driver_front_door_closed or 
        prev_passenger_front_door_closed != passenger_front_door_closed or
        prev_driver_rear_door_closed != driver_rear_door_closed or
        prev_passenger_rear_door_closed != passenger_rear_door_closed or
        prev_hatchback_closed != hatchback_closed or
        prev_hood_closed != hood_closed):
            print('*** Door Status Update ***')
            print(f'Driver Front Door Closed: {driver_front_door_closed}')
            print(f'Passenger Front Door Closed: {passenger_front_door_closed}')
            print(f'Driver Rear Door Closed: {driver_rear_door_closed}')
            print(f'Passenger Rear Door Closed: {passenger_rear_door_closed}')
            print(f'Hatchback Closed: {hatchback_closed}')
            print(f'Hood Closed: {hood_closed}')

When I ran this script on my vehicle using can0, whenever a door was opened or closed on my vehicle the script printed an updated status report.

*** Door Status Update ***
Driver Front Door Closed: True
Passenger Front Door Closed: False
Driver Rear Door Closed: True
Passenger Rear Door Closed: True
Hatchback Closed: True
Hood Closed: True
*** Door Status Update ***
Driver Front Door Closed: True
Passenger Front Door Closed: True
Driver Rear Door Closed: True
Passenger Rear Door Closed: True
Hatchback Closed: True
Hood Closed: True
*** Door Status Update ***
Driver Front Door Closed: False
Passenger Front Door Closed: True
Driver Rear Door Closed: True
Passenger Rear Door Closed: True
Hatchback Closed: True
Hood Closed: True

That was a fun exercise and definitely more involved and challenging than locating the VIN on the CAN bus. I hope you enjoyed it and thanks for reading as always.

4 thoughts on “CAN Bus Reverse Engineering – Door Status

  1. Sandman

    Nice work Derek! I’m looking to write a small android app that would bring the doors status on an Android HU installed on a Ford Focus mk2. What I didn’t get from your post (sorry if you specified but I overlooked) is where did you hook the CAN sniffer. On ODB connector? Elsewhere? If on ODB connector, on which CAN is the door status information available, that is, HS-CAN or MS-CAN? They are both available at ODB connector. Thank you.

    Reply
    1. derek Post author

      I plugged into the OBDII Port for this article. The Door Status information was from HS CAN (Pins 6 & 14). This work was done on a Ford Focus MK3 so I suspect the CAN content will be different from a Focus MK2, but maybe not.

      Reply
  2. Marric

    You wouldnt happen to have any HS Can logs relating with the HVAC running would you? I’m doing an engine swap using a Focus ST PCM and need to determine the message that triggers A/C. I just dont have an actual focus to packet sniff on, and am not sure if the fusion/mondeo would use the same messages.

    What I do know: The A/C command from the climate control panel is sent to the BCM on MS-CAN and the BCM sends the request to the PCM over HS.

    Reply
    1. derek Post author

      Hi Marric, sorry for the late response. I will spend some time looking into the A/C message on HS-CAN when I get a chance.

      Reply

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.