What this example does

This guide sends a read-only OBD-II request for engine RPM and decodes the response.

The request uses:

  • functional request CAN ID 0x7DF
  • OBD service 0x01, current powertrain data
  • PID 0x0C, engine speed

This is a small diagnostics example. It is not a complete scan tool and it is not UDS implementation code.

Work safely

Use a bench ECU, simulator, or approved test vehicle setup. Do not experiment while a vehicle is moving. Start with documented read-only services.

Never assume that every vehicle follows the same IDs, gateway behavior, or access policy. The target may not expose OBD traffic on the connector or network you selected.

The raw OBD-II request

The eight CAN data bytes are:

Example
02 01 0C 00 00 00 00 00

They mean:

ByteMeaning
02Two application bytes follow in this single-frame request
01Request current powertrain data
0CRequest engine RPM
Remaining bytesPadding

Send it with can-utils:

Terminal
cansend can0 7DF#02010C0000000000

Watch for responses:

Terminal
candump can0,7E8:7F8

Physical responses commonly use IDs from 0x7E8 through 0x7EF.

Example response

Example
can0  7E8   [8]  04 41 0C 1A F8 00 00 00

The important bytes are:

  • 04: four application bytes follow
  • 41: positive response to service 0x01
  • 0C: engine RPM PID
  • 1A F8: RPM data bytes A and B

The formula is:

Example
RPM = ((A x 256) + B) / 4

For A = 0x1A and B = 0xF8:

Example
RPM = ((26 x 256) + 248) / 4
RPM = 1726

Complete Python example

Install python-can:

Terminal
python -m pip install python-can

Save this as read_rpm.py:

Python
import can

REQUEST_ID = 0x7DF
FIRST_RESPONSE_ID = 0x7E8

request = can.Message(
    arbitration_id=REQUEST_ID,
    data=[0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00],
    is_extended_id=False,
)

response_filter = [
    {
        "can_id": FIRST_RESPONSE_ID,
        "can_mask": 0x7F8,  # Accept 0x7E8 through 0x7EF
        "extended": False,
    }
]

with can.Bus(
    interface="socketcan",
    channel="can0",
    can_filters=response_filter,
) as bus:
    bus.send(request, timeout=1.0)
    print("Engine RPM request sent")

    response = bus.recv(timeout=2.0)
    if response is None:
        raise SystemExit("No OBD-II response")

    data = response.data
    if len(data) < 5:
        raise SystemExit(f"Response is too short: {data.hex(' ')}")

    if data[1] != 0x41 or data[2] != 0x0C:
        raise SystemExit(f"Unexpected response: {data.hex(' ')}")

    rpm = ((data[3] << 8) | data[4]) / 4.0
    print(f"Response ID: 0x{response.arbitration_id:X}")
    print(f"Engine speed: {rpm:.0f} rpm")

Run it:

Terminal
python read_rpm.py

Why the first byte looks like ISO-TP

OBD-II messages longer than the basic CAN payload use ISO-TP. Even this short request uses a single-frame length byte.

For a single frame, the low part of the first byte tells the receiver how many application bytes follow. Longer diagnostic messages use first frames, consecutive frames, and flow-control frames.

Do not manually implement multi-frame handling by adding sleeps between raw CAN frames. Use a tested ISO-TP stack.

Functional vs physical addressing

The request ID 0x7DF is commonly used as a functional request: any relevant ECU may respond.

A physical request targets one ECU, often using a paired ID such as:

Example
request:  0x7E0
response: 0x7E8

Actual IDs vary. Use the diagnostic specification for the target.

Common reasons for no response

  • The engine or ECU is not powered in the required state.
  • can0 has the wrong bitrate.
  • A gateway blocks the request.
  • The selected network is not the diagnostic CAN network.
  • The vehicle uses different addressing.
  • The PID is not supported.
  • The interface is bus-off.
  • Another diagnostic session affects responses.

First confirm that raw traffic is present and that the request frame is actually transmitted.

OBD-II vs UDS

OBD-II defines emissions-related diagnostic services and standardized parameters. UDS is a broader diagnostic protocol used for ECU sessions, data identifiers, trouble codes, routines, security access, and programming.

Both can use ISO-TP over CAN, but their service bytes and data models are different.

The simple summary

To read engine RPM, send an OBD-II service 0x01 request for PID 0x0C, wait for a response, check the service and PID bytes, and apply the defined formula. Keep the first diagnostics tests read-only and use ISO-TP libraries for longer requests.

References