Skip to main content

OBD-II Diagnostic Logger & Telemetry

Project Overview #

I do a lot of DIY maintenance on my 2007 Honda Accord LX, and I was tired of the limitations of generic OBD-II scanners. To get better visibility into what the engine was actually doing, I built my own telemetry logger from scratch. Instead of relying on a commercial black box, this taps into the car’s Engine Control Unit (ECU) to pull live engine metrics, read diagnostic trouble codes (DTCs), and see raw CAN bus traffic.

Hardware Architecture #

To build the physical module, I used an ESP32 microcontroller because it has a built in CAN controller (TWAI) and plenty of processing speed to handle the vehicle’s network traffic.

  • Bridging the Logic Gap: The ESP32 operates on 3.3V logic, while the car’s internal CAN network uses higher differential voltages. I wired in an SN65HVD230 CAN transceiver to safely translate signals between the two.
  • Power & Interface: I used a standard J1962 OBD-II to DB9 cable to tap right into the car’s CAN High (Pin 6) and CAN Low (Pin 14) lines. To keep the setup entirely self contained, the board draws 12V power directly from the diagnostic port, which I stepped down through a buck converter to safely power the ESP32.

Software & Protocol Implementation #

I wrote the firmware in C++, focusing on fast polling and ensuring the microcontroller could speak the car’s language over the Controller Area Network.

  • Asking for Data (PID Querying): I implemented the standard SAE J1979 diagnostic protocol. The ESP32 broadcasts a request across the network to the ECU (using ID 0x7DF) asking for specific Parameter IDs (PIDs)—like 0x0C for RPM, 0x0D for Vehicle Speed, or 0x05 for Engine Coolant Temperature.
  • Translating the Response: I set up an interrupt receiver to catch the ECU’s reply (0x7E8). The software extracts the raw hexadecimal payload and runs it through standard OBD-II formulas to turn those bytes into readable values.
/*Simplified example of req and pars Engine RPM over CAN*/
void request_rpm() {
    can_message_t tx_msg;
    tx_msg.identifier = 0x7DF; 
    tx_msg.extd = 0;
    tx_msg.data_length_code = 8;
    tx_msg.data[0] = 0x02; //Number of additional bytes
    tx_msg.data[1] = 0x01; //Service 01 (Show current data)
    tx_msg.data[2] = 0x0C; //PID 0C (Engine RPM)
    
    //Pad remaining bytes to complete the frame
    for(int i = 3;i < 8; i++) tx_msg.data[i] = 0xAA; 
    
    twai_transmit(&tx_msg, pdMS_TO_TICKS(1000));
}

void parse_rpm_response(can_message_t *rx_msg) {
    //Check if response is from ECU and matches the RPM PID
    if (rx_msg->identifier == 0x7E8 && rx_msg->data[2] == 0x0C) {
        uint8_t a = rx_msg->data[3];
        uint8_t b = rx_msg->data[4];
        
        //Standard OBD-II formula for RPM
        float rpm = ((a * 256.0) + b) / 4.0;
        printf("Current RPM: %.2f\n", rpm);
    }
}