ECU Diagnostics – part 13 : Three Diagnostic Protocols in the MBE 9A4 ECU

This one’s just a short post to introduce the next three posts. In our investigations of the MBE 9A4 ECU we’ve found three different diagnostics protocols supported on the OBD port.

The following three posts will summarise what we’ve learnt about each protocol and will be updated with new information as it becomes available.

  • 13.1 MBE-Broadcast
  • 13.2 OBD-II
  • 13.3 MBE-ISOTP

ECU Diagnostics – part 12 : OSI 7 Layers for Caterham Diagnostics

Communications protocols are often called protocol stacks… they’re layers, or stacks, of different protocols… one layered on top of the other from lowest level (hardware/physical) to highest level (software application).

This is where the OSI 7 Layer model comes in… it standardises how we think about the layers and allows us to talk about where each protocol and hardware/software sits in the layer stack.

More info on the OSI 7 Layer Model can found here.

If we now load the OSI model with the layers for the MBE ECU protocol stack then the table looks like this:

PurpleMeanie / MBE ECU / OSI model
Layer PDU MBE ECU OSI Function
Host
layers
7 Application Data Application (Dashboard, Wheel Computer etc)
6 Presentation mbe.py (My Python helper class)
5 Session MBE ISOTP (MBE’s adaptions to ISOTP)
4 Transport Segment, Datagram ISOTP / ISO 15765-2 (python-can-isotp & can-isotp kernel module)
Media
layers
3 Network Packet
2 Data link Frame CAN bus ISO 11898 (SocketCAN)
1 Physical Symbol

From the table above…

MBE ECU OSI Function

Application

At the highest OSI level we have the application layer. This is what the user sees, turning all the lower level layers into something pretty to see.

mbe.py

This is my helper class. It takes the more complicated layers below and provides a simpler abstraction layer for the application to use.

MBE ISOTP

I’ve called this layer the MBE ISOTP layer. It adds a communication layer on top of the ISOTP packetization layer. This MBE ISOTP layer provides “commands” for config and data extraction from the ECU. It does so “on top” of the ISOTP layer below, or another way to think of it is to say the MBE ISOTP packets sit “inside of” of the ISOTP packets.

ISOTP

This is the standard used to extend CAN bus packets from 8 bytes up to a maximum of 4095 bytes per packet and sits on top of the CAN bus protocol.

It also provides for an additional paradigm called flow control. In theory flow-control should allow CAN bus devices to negotiate the timing between CAN bus frames that each device can support.  

HOWEVER – The MBE/SBD Easimap and ECU… DO NOT USE FLOW CONTROL. This caused us some concern to start with and meant we had to adjust some of the ISOTP libraries we were using, see here.

CAN Bus

We’ve talked about this layer a lot, the most relevant post about it can be found here.

ECU Diagnostics – part 11 : Logic Analyzer on the CAN Bus

To test out my theories and the code I’d been writing, I needed a simple test and a way to make sure the code (and I) was doing what I thought it should be.

The code part was easy, take the JSON files I’d created from the Easimap EC2 file and use it to ask for some simple data from the car.

As it turns out the “checking the code” part was also fairly straight forwards. I’ve talked about the Saleae Logic Analyzers in the past and how good they are. It also turns out that one of the software decoder modules they ship as standard is a CAN bus decoder. The idea is that you capture an analysis trace and then get the analyser software to decode the trace as CAN bus.

Saleae also have instructions on how to set up a CAN bus capture. They say that in most cases you can connect the analyser to CAN-L and set the analyser input to be high voltage (3.3v) and away you go.

Saleae Logic Analyzer (red thing) connected to PiCAN2 on Raspberry Pi. Yellow wire connected to CAN-L, Black wire to Ground.

You’ll notice that there’s also a TFT LCD sat on top of the Pi, that’s for a later project.

Analyzer Setup

With the analyser input set to 3v3 and a 50MHz sample rate I could capture the 1MHz activity on the CAN bus. I tried some captures at slower sample rates but they weren’t so reliable – clearly the analyzer needs to significantly oversample the signal to get the timing right.

Just for completeness, here’s a picture of the analyser leads connected to the PiCAN2…

Saleae Analyzer connected to Raspberry Pi and PiCAN2

 

Here’s a closeup of the connections…

Analyzer connections to PiCAN2 closeup

Anyways, that setup is well within the capabilities of the Saleae and gave very reliable results.

Saleae Logic Analyzer Setup for CAN bus Capture on Channel 0

Test Code

Just to check my theories I wrote this simple app to pull just a few data samples from the car’s ECU. I started with RT_ENGINESPEED (Engine RPM) and got back the result I was expecting (i.e. 0 RPM, as the car wasn’t running).

Here’s the code, which can also be found here.

# TestECU
# Application for testing python-isotp-mbe class
#
# 2019-08-26 John Martin
#

import logging
import argparse
import json
import pprint
import mbe
import curses

version = "0.1"

if (mbe.test_mode):
  variables_to_follow = [
      'RT_ENGINESPEED',
    'RT_AIRTEMP1(LIM)',
    'RT_COOLANTTEMP1(LIM)',
    'RT_BATTERYVOLTAGE(LIM)',
    'RT_SOFTCUTTIME',
    'RT_HARDCUTTIME'
  ]
else:
  variables_to_follow = [
    'RT_ENGINESPEED'
  ]

def main():
  parser = argparse.ArgumentParser(prog='mbepcap2txt', description='Takes an pcap with ISOTP formatted MBE transactions and makes it human readable.')
  parser.add_argument('--interface',     '-i',                   help='The can interface to open', required=True)
  parser.add_argument('--variables',     '-v',                   help='Input MBE variables filename', required=True)
  parser.add_argument('--query_id',      '-q',                   help='CAN query ID (default 0x0cbe1101)', default=0x0cbe1101)
  parser.add_argument('--response_id',   '-r',                   help='CAN resdponse ID (default 0x0cbe0111', default=0x0cbe0111)
  parser.add_argument('--loglevel',      '-l',                   help='Logging level to show', choices=['INFO','DEBUG','WARNING', 'ERROR', 'NONE'], default="ERROR")
  parser.add_argument('--logfile',       '-f',                   help='If set logging will be sent to this file')
  parser.add_argument('--version',       '-V', action='version', version='%(prog)s '+version)

  args = parser.parse_args()

  logging_level = getattr(logging, args.loglevel, None)
  logging.basicConfig(level=logging_level, filename=args.logfile, filemode='w')

  ecu = mbe.mbe()
  
  ret = ecu.set_options(args.variables, args.query_id, args.response_id, args.interface)
  if(not ret):
    logging.error("Unable to set options")
    exit()

  if(ecu.add_variable_list_to_follow(variables_to_follow) != len(variables_to_follow)):
    logging.warning("Ooops, didn't add all the vars I wanted to")
  else:
    logging.info("Added all the variables we expected")

  ecu.bind()

  results = dict()
  if (ecu.process_all_pages(results) != False):
    logging.debug(pprint.pformat(results))

if __name__ == '__main__':
  main()

When we run the code above it makes a single request to the car for Engine RPM and displays the results. Here are the ISOTP CAN bus packets for this test:

REQUEST:  0100000000f87c7d
RESPONSE: 810000

As you can see in first packet, there’s a data request (0x01)  to page 0xf8 with 0x7c and 0x7d as the requested data address.

Then in the response packet there’s a data response (0x81) with 0x0000 as the RPM. Bingo! And because the car engine wasn’t running at the time, a response of zero RPM makes perfect sense!

Here’s the 3 raw CAN bus frames that correspond to those ISOTP packets:

#     CAN ID     Data
[290] 0x0cbe1101 10 08 01 00 00 00 00 f8
[291] 0x0cbe1101 21 7c 7d
[292] 0x0cbe0111 03 81 00 00 00 00 f8 7c

And here’s what that looked like in the logic analyser. The three images are for each of the three CAN bus frames (you might need to click on the images to see them full scale)…

CAN bus request: 0x0cbe1101 10 08 01 00 00 00 00 f8
CAN bus request: 0x0cbe1101 21 7c 7d
CAN bus response: 0x0cbe0111 03 81 00 00 00 00 f8 7c

You can see details of the CAN bus protocol in full technicolor. The analyser software doesn’t show the start but you can see the ID, data and CRC. A more detailed discussion on the CAN bus protocol can be found in this post.

So my software is able to send a good ISOTP CAN bus request to the car and we get a good response back.

It Works!

So… it works! We’ve figured out how the three communications protocols work that these ECUs seem to use. There may be more to know but we can now use the more complicated protocol used by Easimap and dig deeper into the ECU.

Now lets summarize all we know about the various protocols.

ECU Diagnostics – part 10 : Decoding EC2 Files

Now we need to start proving some of our theories about how Easimap is actually talking to the ECU. And it seemed to me that if I was going to prove anything then I’d need to pass a lot of data through any theory I came up with, and that meant writing a program – since bit bashing (doing the decoding by hand) hundreds of packets of CAN bus data was probably going to take as long to decode by hand as it would to write a program to do it. Having a program would also mean I could push lots of examples through my code to check out my theories, but also tweak my theories and quickly run my code again to check them.

And if I was going to write a program then it was going to need to translate the binary data on the CAN bus into something human readable. Of course that’s what Easimap uses the EC2 files for and I wanted to use that same information in any programs I wrote.

The EC2 files are plain text files using a similar, but not quite the same, layout as legacy Windows ini files. There a lots of programs that now use this file format and it seems MBE/SBD have come up with their own variation – at least I’ve not seen anything quite like it before.

It uses the regular square brackets “[ ]” to create sections in the file, but it then nests these sections to create hierarchies. And it means that standard libraries to read in an EC2 file won’t work. Time to write some code then…

EC2 File Sections

The 9A4 EC2 file has the following hierarchical, top-level sections, each contained in the EC2 file and having square brackets around them:

  • HISTORY
  • PROPERTIES
  • PARAMETER PROTOTYPES
  • NUMERIC SCALES
  • STRING SCALES
  • PARAMETER DEFINITIONS
  • COLLECTIONS
  • ALARMS
  • SPECIAL FUNCTIONS
  • MAPPING VECTORS
  • MAPPING CONTROLS
  • SETUP PROTOTYPES
  • SETUP DEFINITIONS
  • MAP PROGRAMS
  • VARIANTS
  • SPECIAL INTERFACE PROTOTYPES
  • MATRICES

So, in the EC2 file the History section has a line with [ HISTORY ] in it.

There’s clearly a lot of those sections that will be interesting to look into at some point. But I had a very focused desire to get the raw sensor diagnostic data out of the car and that meant I only needed to look at three sections for the moment.

Parameter Definitions

After a bunch of trial and error it seemed I needed to use the Parameter Definitions to get a list of all the data variables that the car could divulge.

So for Engine RPM the corresponding Parameter Definition is:

[RT_ENGINESPEED]
Number of Dimensions = 0
Page = F8
Address = 237C
Bytes per Cell = 2
0 = SCALE_ENGINESPEED

That allowed me to figure out what the request was that I needed to make to the ECU in order for the ECU to respond with Enging RPM. See the previous post for a breakdown of that.

It also told me that I was going to get two bytes of data back from the ECU (Bytes per Cell = 2).

But 2 bytes of data can range from 0 to 65535 (assuming its a positive integer). And I wasn’t sure how to interpret the 2 bytes. Other data variables, like battery voltage, were going to be floating point numbers and while Engine RPM could be a 2 byte integer with no scaling or offset, that wasn’t going to be the case for most of the other data coming from the car.

Numeric Scales

That’s where the Numeric Scales top level section comes in…

It turns out that the line:

0 = SCALE_ENGINESPEED

refers to a SCALE_ENGINSPEED section in the Numeric Scales section of the EC2 file. The “0 = ” bit refers to the fact that this is the zeroth dimension for the variable, something that becomes important for 1D, 2D and 3D maps where there are more scales defined for each of the axies on the map. The definition of SCALE_ENGINESPEED looks like this:

[SCALE_ENGINESPEED]
Units = RPM
Scale Minimum = 0.000000
Scale Maximum = 65535.000000
Display Minimum = 0.000000
Display Maximum = 12000.000000
Display Interval = 1000.000000

I suspect that’s fairly obvious what’s going on here, but the idea is that these Numeric Scales set the limits for the data coming back from the car, along with any scale factor that needs to be applied and any offset.

For instance, the temperate variables have a scale (allowing them to be floating point numbers when the result is divided by the scale) and offsets (meaning that the data coming from the car needs to be offset by a particular value, allowing the number to also be negative – as might happen with air temperature on a cold day).

You can also see that the Numeric Scale also sets the characters we can use for the Units, RPM in this case.

Finally, I wanted to be able to see the data coming from the car in a human readable form. And while RT_ENGINESPEED is reasonably simple to understand, there are other variables that are much more obtuse, like RT_TPSFUEL+TRIMBANK1. 

Parameter Prototypes

That’s where the Parameter Prototype section comes in with English definitions of what each variable is. For instance, RT_ENGINESPEED has this parameter prototype:

RT_ENGINESPEED, 
Engine Speed,
EngSpeed,
Engine Speed in rpm as measured by the crankshaft sensor. This is
the primary input for the fuel and ignition maps

I spread that definition across a few lines, it’s on a single line in the EC2 file.

One thing to note about these Parameter Prototypes is that at the end of the line in the EC2 file, you can get the word “DISABLED”. I ignored this at first but soon came to realise that it really did mean “disabled”. If I didn’t take account of it in the code I wrote, then I found there were variables defined more than once. Only one of the definitions was not disabled, so by ignoring the disabled ones I was able to get a clean list of valid variables.

ec2parse.py

Now I knew the layout of the EC2 file, I could write some code. My weapon of choice here is Python. I’m reasonably new to Python but there’s a huge library base now behind Python and I can code with it on Linux, Mac and PC reasonably easily. 

The code for ec2parse.py can be found here. The code is far from pretty… but it does a job. I may go and tidy it up when I get a chance (but I think everyone says that and I always advocate writing neat code in the first place so you don’t have to go back to it later – do as I say, not a I do!).

This Python code will take an EC2 file and create an output file that can be used later, i.e. in the mbepcap2txt.py example below. For the moment I’ve been using the JSON output that I created which creates a formatted file that another Python file can read (use) instead of it having to decode the EC2 file every time.

mbepcap2txt.py

Once I’d written this parsing code for the EC2 files I also the wrote a test app to take a CAN bus Wireshark capture file (.pcap or .pcapng) and turn it into something human readable – remember this is an onion with protocol layers… CAN bus being turned into a SocketCAN format, decoded as CAN then as ISOTP – phew!.

This allowed me to take many hundreds of messages sent between Easimap and the ECU and verify that I had all my theories validated before I actually used any of this software I’d written to send any messages to the car…. I didn’t want to have to fork out for a new ECU, or even worse a new engine because I’d sent some random message to the ECU.

And it all worked… here’s some output in something more human readable that the CAN bus messages:

REQUEST: 0100000000f83031363744454c4d4e4f50515a5b5c5d646a6b7c7d9e9fa0a1d8d9dadb
RESPONSE: 814e39094a9c52b886e8844e39a80d70a000b04f00005695800900800080
RT_THROTTLEANGLE1(RAW)=1.1192 V ( Throttle Angle 1 (Raw) )
RT_AIRTEMP1(LIM)=16.273 ( Air Temp )
RT_COOLANTTEMP1(LIM)=21.632 ( Coolant Temp )
RT_COOLANTFUELFACTOR=110.5 ( Coolant Fuel Factor )
RT_AIRTEMPFUELFACTOR=3.8346 ( Air Temp Fuel Factor )
RT_THROTTLEANGLEINCREASING=1.1192 V ( Throttle Angle Increasing )
RT_TPSFUEL+TRIMBANK1=5.6013 ms ( TPS Fuel + Trim )
RT_TPSVSSPEEDIGN+TRIM1=4.7938 ( TPS vs Speed Ign + Trim )
RT_THROTTLESITE1=0.0 Site ( Throttle Site 1 )
RT_BAROSCALEDLIM=1.04 Bar ( Baro Pressure )
RT_ENGINESPEED=0.0 RPM ( Engine Speed )
RT_BATTERYVOLTAGE(LIM)=11.667 V ( Battery Voltage )
RT_BATTERYVOLTAGECOMP=0.48725 ms ( Battery Voltage Comp )
RT_MAPPINGPOT1LIM=0.0015259 ( Mapping Pot 1 )
RT_MAPPINGPOT2LIM=0.0015259 ( Mapping Pot 2 )

#[28]# ISOTP REQUEST :0100000000f9babbbcbd
#[29]# ISOTP RESPONSE :81781edc1e
RT_SOFTCUTTIME=7800.0 RPM ( Soft Cut )
RT_HARDCUTTIME=7900.0 RPM ( Hard Cut )

Remember this is all test data that I’ve “captured” or “sniffed” when Easimap was communicating with the cars ECU. To this point none of my code is sending anything to the car. So, before I could do some real damage to the car and send my own data to the ECU, there was one more test I wanted to do to make sure everything was going to be as safe as I could make it. I wanted to connect up a Logic Analyser to the CAN bus so I could make sure the communications were working as expected…

 

 

 

ECU Diagnostics – part 9 : The Easimap Protocol Theory

Ok. so we’ve set ourselves up to be able to look at what is going on with a Caterham diagnostics port, we’ve talked a bit about the theory of what we might see from the port and we’ve also seen that Easimap talks to the ECU using a higher level protocol called ISO 15765-2 (ISOTP). We also saw how Easimap is asking the ECU for its model and serial number.

Easimap Data Request Protocol

At about this time, we also made a bit of a leap in our understanding of the communications between Easimap and the car.

We’d seen how Easimap sending 0x04000d to the car gave a response of the model and serial numbers (0xe4000d23394134626535333000). Notice how the first byte of the request is 0x04 and the first byte of the response is 0xe4. Seemed to me as though the response was echoing the 4 and setting some higher order bits creating a response code of 0xe4… Hmm…

If we look further down the communications that Easimap sends to the car (with the default gauges and pages set in Easimap) then we can also see some patterns.

It seemed to me that the first byte of the model/serial request was going to be a message type (0x04 means get some config data).

Looking through the next communications we see Easimap send:

0x0100000000126667a8a9

and gets a response of:

0x81aaaa1600

If we think back to the 0x04 and 0xe4, request/response, of the model/serial exchange then this new exchange looks like a request/resonse of 0x01/0x81. That’s the same sort of pattern… a request of 0x01 and a response with the topmost bit set. That’s starting to make sense, but what of all the other data?

It was at about this time that James (Aerobod) had been looking at his car and had seen a pattern where there was an Lowest Significant Byte (LSB) and Most Significant Byte (MSB) being sent periodically that matched some of the data he was looking for.

It then dawned on me that my previous hunch about pages and addresses in the EC2 files was starting to make sense. If we look at our request to the car of:

0x0100000000126667a8a9

then the first byte we know about (0x01) and then there are some more bytes of 0x00 with a 0x12 next, that could a page. Then there are sequences of similar numbers 0x66 followed by 0x67, then 0xa8 followed by 0xa9. They could be addresses.

I searched the EC2 file for 0x6667 and 0xa8a9 but there wasn’t anything that made sense.

It then occurred to me that perhaps the pages and addresses in the EC2 file weren’t being used exactly as written in the file. And if James’ thinking about LSB and MSB was right then perhaps the 0x6667 was the LSB of the address in the EC2 file followed by the same LSB+1.

With the data we have above that would mean we needed to look in the EC2 file for a variable with a page of 0x12 and address LSB of 0x66. A quick search in our EC2 file turns up:

[RT_IGNITIONADVANCEBANK1]
Number of Dimensions = 0
Page = 12
Address = 6766
Bytes per Cell = 2
0 = SCALE_NADV

Some of the right data was present, the 12, 66 and 67 were there. And it seemed to indicate that this was related to Ignition Advance Bank1. Ok, that could be, our Caterhams could have something like that in them.

Next was to look at the response from the car:

0x81aaaa1600

We know that 0x81 is the response type for a request of 0x01. That leaves data of 0xaaaa1600. That’s probably 0xaaaa and 0x1600. Neither seemed to make much sense for an Ignition Advance Bank 1 response. It could be right, it could be wrong.

At this point I also started to pull apart the test.lsn file I’d found in the Easimap configuration directories on the PC. That file seemed to be the setup Easimap was using to say what gauges and displays to put on the screen for each “page” of the software display. It had a section like this:

[Parameters]
RT_THROTTLESITE1, Throttle Position|Throttle Position Limits and Default Setup, Throttle Site 1
RT_BATTERYVOLTAGECOMP, Fuel|Voltage Fuel Compensation, Battery Voltage Comp
RT_ENGINESPEED, Engine Speed Setup & Limiter, Engine Speed
RT_IGNITIONADVANCEBANK1, Ignition, Ignition Advance
RT_TPSVSSPEEDIGN+TRIM1, Ignition, TPS vs Speed Ign + Trim
RT_INJECTIONTIMEA, Fuel, Final Injection Time
RT_COOLANTTEMP1(LIM), Sensors|Coolant Temperature, Coolant Temp
RT_AIRTEMP1(LIM), Sensors|Air Temperature, Air Temp
RT_MAPPINGPOT1LIM, Mapping Box|Mapping Pot 1, Mapping Pot 1
RT_MAPPINGPOT2LIM, Mapping Box|Mapping Pot 2, Mapping Pot 2
RT_COOLANTFUELFACTOR, Fuel|Coolant Temperature Compensation, Coolant Fuel Factor
RT_BATTERYVOLTAGE(LIM), Sensors|Battery Voltage, Battery Voltage
RT_AIRTEMPFUELFACTOR, Fuel|Air Temperature Compensation, Air Temp Fuel Factor
RT_DUTYCYCLEA, Fuel|Duty Cycle, Duty Cycle
RT_TPSFUEL+TRIMBANK1, Fuel, TPS Fuel + Trim
RT_SOFTCUTTIME, Engine Speed Setup & Limiter, Soft Cut
RT_HARDCUTTIME, Engine Speed Setup & Limiter, Hard Cut
RT_THROTTLEANGLE1(RAW), Throttle Position|Throttle Position Limits and Default Setup, Throttle Angle 1 (Raw)
RT_ENGINERUNTIME, Data Logging, ERT
RT_ECUSTATUS, Fault Conditions, ECU Status
RT_BAROSCALEDLIM, Sensors|Barometric Pressure, Baro Pressure
RT_THROTTLEANGLEINCREASING, Throttle Position|Throttle Position Limits and Default Setup, Throttle Angle Increasing
RT_BAROFUELCOMP, Fuel|Barometric Pressure Fuel Compensation, Barometric Pressure Fuel Compensation
RT_CRANKCOUNT, Development, Crank Count

That was clearly a list of variables (in upper case) that Easimap was going to request data for from the car.

And an obvious candidate for a search in there was RT_ENGINESPEED, or Engine RPMs. I knew that I’d not had the car engine running when I did my packet captures, so there was a good chance that if I found the request for RPM then I could see a response of with zeros back from the car.

Looking in the EC2 file, you can find definition of RPM variable as:

[RT_ENGINESPEED]
Number of Dimensions = 0
Page = F8
Address = 237C
Bytes per Cell = 2
0 = SCALE_ENGINESPEED

Ok. So, we’ll looking for a request with a page of 0xf8 and an address that’s either 0x237c or possibly 0x7c7d.

Looking through the CAN bus as ISOTP output again we find this request:

0100000000f83031363744454c4d4e4f50515a5b5c5d646a6b7c7d9e9fa0a1d8d9dadb

and this response:

814e39094a9c52b886e8844e39a80d70a000b04f00005695800900800080

In the request there’s the 0x01 at that start that is some kind of data request and the corresponding 0x81 in the response. We then see 0xf8 after the 4 x 0x00’s. Ok, that’s looking good, we’re looking for a page of 0xf8 for RT_ENGINESPEED.

We can also see that 3/4 of the way along the request we can see 0x7c7d. If we break the request down into the bit at the start (pre-amble) of 0x0100000000f8 and then the pairs that could be LSB/MSB combinations, then we get this:

0100000000f8
3031
3637
4445
4c4d
4e4f
5051
5a5b
5c5d
64
6a6b
7c7d
9e9f
a0a1
d8d9
dadb

That’s all looking good for the LSB/MSB theory of LSB/LSB+1 and shows us that 0x7c7d is 19 bytes into the data section. I also took note that some data being requested has only one byte of request information… I’ll need to remember that later.

Now if we look at the response from the car and break it down like the request data and slot in the request data on the left:

 Request         Response
0100000000f8 = 81
3031 = 4e39
3637 = 094a
4445 = 9c52
4c4d = b886
4e4f = e884
5051 = 4e39
5a5b = a80d
5c5d = 70a0
64 = 00
6a6b = b04f
[7c7d = 0000]
9e9f = 5695
a0a1 = 8009
d8d9 = 0080
dadb = 0080

Then… low and behold… where I’ve put brackets around the line, we can see that our theory about RT_ENGINESPEED needing a request of 0x7c7d is returning 0x0000 in exactly the right spot in the response… showing that the car isn’t running.

That’s good enough for me… I think my theory is as good as I’m going to get, I need to write some code to test this out.