Hello Guys,
I am currently trying to connect an ESP32 with my car and therefore using the ELM327 Bluetooth Adapter.
My cars coolant temperature gauge is not working as it's supposed to and thats why I want to display it on a small 128x64 display. I can get the esp32 to connect to the ELM327 but no matter what command I try to send I will always get the same answer from the ELM327 wich is " 4F 42 44 49 49" and can be translated to "OBDII". Can someone try to help me find out why I get this response and how I can improve my code to run properly?
The PID for the coolant temperature is 01 05 .
I am new to microcontrollers and do a lot of coding with ChatGPT.
Here is my code.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
BLEClient* pClient;
BLERemoteService* pRemoteService;
BLERemoteCharacteristic* pRemoteCharacteristic;
BLEScan* pBLEScan;
BLEAdvertisedDevice* myDevice = nullptr;
void initializeDisplay()
{
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C))
{
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Initializing BLE...");
display.display();
}
void initializeBLE()
{
Serial.println("Initializing BLE...");
BLEDevice::init("");
pClient = BLEDevice::createClient();
pBLEScan = BLEDevice::getScan();
pBLEScan->setActiveScan(true);
Serial.println("BLE client initialized.");
}
void connectToELM327()
{
Serial.println("Starting scan for BLE devices...");
display.clearDisplay();
display.setCursor(0, 0);
display.println("Scanning for BLE...");
display.display();
BLEScanResults* foundDevices = pBLEScan->start(5);
bool elm327Found = false;
for (int i = 0; i < foundDevices->getCount(); i++)
{
BLEAdvertisedDevice device = foundDevices->getDevice(i);
if (device.getAddress().toString() == "1c:a1:35:69:8d:c5")
{
Serial.println("ELM327 found!");
display.println("ELM327 found!");
display.display();
myDevice = new BLEAdvertisedDevice(device);
elm327Found = true;
break;
}
}
if (!elm327Found)
{
Serial.println("ELM327 not found. Retrying in 2 seconds...");
display.println("ELM327 not found.");
display.display();
delay(2000);
return;
}
Serial.println("Trying to connect to ELM327...");
display.println("Connecting to ELM327...");
display.display();
if (!pClient->connect(myDevice))
{
Serial.println("Connection to ELM327 failed.");
display.println("Connection failed");
display.display();
delay(5000);
return;
}
Serial.println("Connected to ELM327!");
display.println("Connected!");
display.display();
pRemoteService = pClient->getService(BLEUUID("00001800-0000-1000-8000-00805f9b34fb"));
if (!pRemoteService)
{
Serial.println("OBD-II Service not found. Disconnecting.");
display.println("Service not found.");
display.display();
pClient->disconnect();
return;
}
pRemoteCharacteristic = pRemoteService->getCharacteristic(BLEUUID("00002a00-0000-1000-8000-00805f9b34fb"));
if (!pRemoteCharacteristic)
{
Serial.println("Characteristic not found. Disconnecting.");
display.println("Characteristic not found.");
display.display();
pClient->disconnect();
return;
}
Serial.println("Characteristic connected!");
display.println("Characteristic connected!");
display.display();
}
void sendCommand(String command)
{
Serial.print("Sending command: ");
Serial.println(command);
pRemoteCharacteristic->writeValue(command.c_str(), command.length());
delay(500);
if (pRemoteCharacteristic->canRead())
{
String response = pRemoteCharacteristic->readValue();
Serial.print("Response received: ");
Serial.println(response);
}
else
{
Serial.println("The characteristic cannot be read.");
}
}
void setup()
{
Serial.begin(115200);
initializeDisplay();
initializeBLE();
connectToELM327();
}
void loop()
{
if (!pClient->isConnected())
{
Serial.println("No connection to ELM327. Restarting...");
connectToELM327();
delay(5000);
return;
}
sendCommand("AT Z\r"); // Reset command
sendCommand("AT DP\r"); // Query protocol
String obdCommand = "01 05";
sendCommand(obdCommand);
delay(5000);
}
Best regards!
Update 20.04.2026 :
I did invest some time in this project and managed to get it working. Thanks to a lot of AI and Claude!!
The following code reads the coolant temperature of my 2000 Audi TT 8N via the ELM327 and sends it to the ESP32 via Bluetooth (BLE) which send it to a small 1.28 inch OLED Display.
/*
* ESP32 ELM327 BLE OBD2 Coolant Temperature Reader with OLED Display
*
* This code connects an ESP32 via Bluetooth Low Energy (BLE) to an
* ELM327 Mini adapter and reads the coolant temperature from an OBD2 simulator.
* The temperature is shown on a 1.3" OLED display (128x64 I2C).
*
* Required libraries:
* - BLEDevice (included in ESP32 Arduino Core)
* - Adafruit SSD1306
* - Adafruit GFX Library
*
* Hardware:
* - ESP32 Development Board
* - ELM327 Mini (BLE Version)
* - OBD Simulator-B-V1.5
* - 1.3" OLED I2C Display 128x64
*
* OLED wiring:
* VCC → 3.3V (or 5V)
* GND → GND
* SCL → GPIO 22
* SDA → GPIO 21
*/
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ===== OLED Display Configuration =====
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // Reset pin (or -1 if not used)
#define SCREEN_ADDRESS 0x3C // I2C address (usually 0x3C or 0x3D)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ===== Configuration: Choose the protocol =====
// Change this line to test different protocols:
// Option 1: ISO 15765-4 CAN (recommended, modern vehicles)
// #define OBD_PROTOCOL "ATSP6"
// #define PROTOCOL_NAME "ISO 15765-4 CAN (11bit, 500kbaud)"
// Option 2: ISO 9141-2 (older vehicles)
#define OBD_PROTOCOL "ATSP3"
#define PROTOCOL_NAME "ISO 9141-2"
// Option 3: ISO 14230-4 KWP (fast init) - WORKS with your simulator!
//#define OBD_PROTOCOL "ATSP5"
//#define PROTOCOL_NAME "ISO 14230-4 KWP fast"
// Option 4: ISO 14230-4 KWP (5 baud init) - Alternative if Option 3 is not stable
// #define OBD_PROTOCOL "ATSP4"
// #define PROTOCOL_NAME "ISO 14230-4 KWP 5baud"
// ===== BLE UUIDs for ELM327 =====
// Standard UUIDs for ELM327 BLE devices (Serial Port Profile)
#define SERVICE_UUID "0000fff0-0000-1000-8000-00805f9b34fb"
#define CHARACTERISTIC_UUID_TX "0000fff1-0000-1000-8000-00805f9b34fb" // ESP32 receives here
#define CHARACTERISTIC_UUID_RX "0000fff2-0000-1000-8000-00805f9b34fb" // ESP32 sends here
// ===== Global Variables =====
static BLEAdvertisedDevice* elmDevice = nullptr;
static BLEClient* pClient = nullptr;
static BLERemoteCharacteristic* pTxCharacteristic = nullptr;
static BLERemoteCharacteristic* pRxCharacteristic = nullptr;
static bool deviceConnected = false;
static bool doConnect = false;
static String receivedData = "";
// ===== Callback for BLE Scan =====
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
Serial.print("BLE device found: ");
Serial.println(advertisedDevice.toString().c_str());
// Search for ELM327 in device name (common names: "OBDII", "ELM327", "OBD2")
String deviceName = String(advertisedDevice.getName().c_str());
if (deviceName.indexOf("OBD") != -1 ||
deviceName.indexOf("ELM") != -1 ||
deviceName.indexOf("obd") != -1 ||
deviceName.indexOf("elm") != -1) {
Serial.println(">>> ELM327 device found!");
BLEDevice::getScan()->stop();
elmDevice = new BLEAdvertisedDevice(advertisedDevice);
doConnect = true;
}
}
};
// ===== Callback for received data (Notifications) =====
static void notifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {
// Convert received data to string
for (int i = 0; i < length; i++) {
receivedData += (char)pData[i];
}
// Debug output of raw data
//Serial.print("Received: ");
//Serial.println(receivedData);
}
// ===== Callback for connection status =====
class MyClientCallback : public BLEClientCallbacks {
void onConnect(BLEClient* pclient) {
Serial.println("✓ Connected to ELM327");
deviceConnected = true;
}
void onDisconnect(BLEClient* pclient) {
Serial.println("✗ Connection to ELM327 lost");
deviceConnected = false;
// Show connection loss on display
display.clearDisplay();
display.setTextSize(1);
display.setCursor(5, 20);
display.println("Connection");
display.setCursor(5, 35);
display.println("lost!");
display.display();
}
};
// ===== Function: Establish connection to ELM327 =====
bool connectToELM327() {
Serial.println("\n=== Connection setup started ===");
// Create BLE client
pClient = BLEDevice::createClient();
Serial.println("✓ BLE client created");
// Set callback for connection status
pClient->setClientCallbacks(new MyClientCallback());
// Connect to ELM327
Serial.print("Connecting to ");
Serial.println(elmDevice->getAddress().toString().c_str());
if (!pClient->connect(elmDevice)) {
Serial.println("✗ ERROR: Connection failed");
return false;
}
Serial.println("✓ Connection established");
// Retrieve service
BLERemoteService* pRemoteService = pClient->getService(SERVICE_UUID);
if (pRemoteService == nullptr) {
Serial.println("✗ ERROR: Service not found");
pClient->disconnect();
return false;
}
Serial.println("✓ Service found");
// Retrieve TX characteristic (ESP32 receives)
pTxCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_TX);
if (pTxCharacteristic == nullptr) {
Serial.println("✗ ERROR: TX characteristic not found");
pClient->disconnect();
return false;
}
Serial.println("✓ TX characteristic found");
// Retrieve RX characteristic (ESP32 sends)
pRxCharacteristic = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_RX);
if (pRxCharacteristic == nullptr) {
Serial.println("✗ ERROR: RX characteristic not found");
pClient->disconnect();
return false;
}
Serial.println("✓ RX characteristic found");
// Enable notifications for incoming data
if (pTxCharacteristic->canNotify()) {
pTxCharacteristic->registerForNotify(notifyCallback);
Serial.println("✓ Notifications enabled");
}
Serial.println("=== Connection successful ===\n");
return true;
}
// ===== Function: Send OBD command =====
void sendOBDCommand(String command) {
if (!deviceConnected) {
Serial.println("✗ Not connected - command will not be sent");
return;
}
receivedData = ""; // Clear receive buffer
command += "\r"; // Append carriage return (ELM327 expects this)
//Serial.print("Sending command: ");
//Serial.println(command);
pRxCharacteristic->writeValue(command.c_str(), command.length());
}
// ===== Function: Wait for response =====
String waitForResponse(int timeoutMs = 2000) {
unsigned long startTime = millis();
while (millis() - startTime < timeoutMs) {
// Check if data ends with '>' (ELM327 prompt)
if (receivedData.indexOf('>') != -1) {
String response = receivedData;
receivedData = "";
return response;
}
delay(10);
}
Serial.println("⚠ Timeout waiting for response");
return receivedData;
}
// ===== Function: Initialize ELM327 =====
bool initializeELM327() {
Serial.println("\n=== ELM327 Initialization ===");
// Reset
sendOBDCommand("ATZ");
delay(2500); // Extra time for complete reset
String response = waitForResponse(3000);
Serial.println(response);
// Wait until ELM327 is really ready
delay(500);
// Turn off echo (for cleaner responses)
sendOBDCommand("ATE0");
delay(500);
response = waitForResponse();
Serial.print("Echo off: ");
Serial.println(response);
// Remove spaces from responses
sendOBDCommand("ATS0");
delay(500);
response = waitForResponse();
Serial.print("Spaces off: ");
Serial.println(response);
// Turn off linefeed
sendOBDCommand("ATL0");
delay(500);
response = waitForResponse();
// IMPORTANT: Set protocol MANUALLY instead of auto-detection
Serial.print("Setting protocol to ");
Serial.print(PROTOCOL_NAME);
Serial.println("...");
sendOBDCommand(OBD_PROTOCOL);
delay(2000); // Longer wait time for protocol setup
response = waitForResponse(2500);
Serial.print("Protocol set: ");
Serial.println(response);
// Turn off headers (can help with some simulators)
sendOBDCommand("ATH0");
delay(500);
response = waitForResponse();
Serial.print("Headers off: ");
Serial.println(response);
// Set adaptive timing to auto (IMPORTANT for ISO14230-4!)
sendOBDCommand("ATAT2");
delay(500);
response = waitForResponse();
Serial.print("Adaptive timing: ");
Serial.println(response);
// Set timeout to a higher value for ISO14230-4
// ATST FF = 255 × 4ms = 1020ms timeout
sendOBDCommand("ATSTFF");
delay(500);
response = waitForResponse();
Serial.print("Timeout set: ");
Serial.println(response);
// Allow long messages (important for some protocols)
sendOBDCommand("ATAL");
delay(500);
response = waitForResponse();
Serial.print("Long messages: ");
Serial.println(response);
// Enable keep alive - prevents connection drop!
// Automatically sends small packets to maintain connection
sendOBDCommand("ATKW");
delay(500);
response = waitForResponse();
Serial.print("Keep Alive Status: ");
Serial.println(response);
// Query used protocol
sendOBDCommand("ATDPN");
delay(500);
response = waitForResponse();
Serial.print(">>> Used protocol number: ");
Serial.println(response);
sendOBDCommand("ATDP");
delay(500);
response = waitForResponse();
Serial.print(">>> Protocol name: ");
Serial.println(response);
// Voltage test (checks if OBD connection is established)
sendOBDCommand("ATRV");
delay(500);
response = waitForResponse();
Serial.print("Voltage: ");
Serial.println(response);
// IMPORTANT: Initial "dummy" bus initialization
// This ensures the bus is really connected
Serial.println("\n>>> Performing initial bus connection <<<");
sendOBDCommand("0100");
delay(1500);
response = waitForResponse(5000); // Long timeout for first connection
Serial.print("Initial bus response: ");
Serial.println(response);
// If BUS INIT ERROR, try again
if (response.indexOf("ERROR") != -1 || response.indexOf("UNABLE") != -1) {
Serial.println("Bus init failed, retrying...");
delay(1000);
// Set protocol again
sendOBDCommand(OBD_PROTOCOL);
delay(2000);
waitForResponse(2500);
// Try again
sendOBDCommand("0100");
delay(1500);
response = waitForResponse(5000);
Serial.print("Second attempt: ");
Serial.println(response);
}
Serial.println("✓ ELM327 initialized");
Serial.println("=== Initialization complete ===\n");
// Short pause before normal queries start
delay(1000);
return true;
}
// ===== Function: Read coolant temperature =====
float readCoolantTemperature() {
//Serial.println("--- Reading coolant temperature ---");
// OBD2 Mode 01 PID 05: Engine Coolant Temperature
sendOBDCommand("0105");
delay(800); // Longer wait for ISO14230-4
String response = waitForResponse(4000); // Longer timeout for ISO14230-4
//Serial.print("Full response: [");
//Serial.print(response);
//Serial.println("]");
// Check if an error was returned
if (response.indexOf("NO DATA") != -1) {
Serial.println("✗ No data - reinitializing bus...");
// Complete bus reinitialization
sendOBDCommand("ATZ");
delay(2000);
waitForResponse(3000);
// Set protocol again
sendOBDCommand(OBD_PROTOCOL);
delay(1500);
waitForResponse(2000);
// Adaptive timing
sendOBDCommand("ATAT2");
delay(500);
waitForResponse();
Serial.println("Bus reinitialized - retrying...");
// Try again
sendOBDCommand("0105");
delay(800);
response = waitForResponse(4000);
Serial.print("New response after reset: [");
Serial.print(response);
Serial.println("]");
// If still NO DATA, give up
if (response.indexOf("NO DATA") != -1 || response.indexOf("UNABLE") != -1) {
Serial.println("✗ Still no data after reset");
return -999.0;
}
}
if (response.indexOf("UNABLE TO CONNECT") != -1) {
Serial.println("✗ Connection to OBD bus failed - attempting reconnect");
// Similar reset as for NO DATA
sendOBDCommand("ATZ");
delay(2000);
waitForResponse(3000);
sendOBDCommand(OBD_PROTOCOL);
delay(1500);
waitForResponse(2000);
return -999.0;
}
if (response.indexOf("BUS INIT") != -1 && response.indexOf("ERROR") != -1) {
Serial.println("✗ Bus initialization failed");
return -999.0;
}
if (response.indexOf("?") != -1) {
Serial.println("✗ Unknown command");
return -999.0;
}
// Parse response: format is "41 05 XX" where XX is the hex value
// Temperature = XX - 40 (in °C)
// Remove spaces for easier parsing
response.replace(" ", "");
response.replace("\r", "");
response.replace("\n", "");
response.replace(">", "");
int pidPos = response.indexOf("4105");
if (pidPos == -1) {
Serial.println("✗ Invalid response or PID not supported");
Serial.print("Cleaned response was: ");
Serial.println(response);
return -999.0;
}
// Extract hex value after "4105" (2 characters)
String hexValue = response.substring(pidPos + 4, pidPos + 6);
hexValue.trim();
if (hexValue.length() < 2) {
Serial.println("✗ Hex value too short");
return -999.0;
}
// Convert hex to decimal
int tempValue = (int)strtol(hexValue.c_str(), NULL, 16);
// Calculate temperature (formula: value - 40)
float temperature = tempValue - 40.0;
//Serial.print("Raw data (Hex): ");
//Serial.println(hexValue);
//Serial.print("Decimal value: ");
//Serial.println(tempValue);
//Serial.print("✓ Coolant temperature: ");
Serial.print(temperature);
Serial.println(" °C");
displayTemperature(temperature);
return temperature;
}
// ===== Function: Display temperature on OLED =====
void displayTemperature(float temperature) {
display.clearDisplay();
// Check if temperature is valid
if (temperature <= -999.0) {
// Show error message
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 20);
display.println("ERROR");
display.setTextSize(1);
display.setCursor(5, 45);
display.println("No connection");
} else {
// Show temperature large in the center
display.setTextSize(4);
display.setTextColor(SSD1306_WHITE);
// Center temperature
String tempStr = String((int)temperature);
int16_t x = (SCREEN_WIDTH - (tempStr.length() * 24)) / 2;
display.setCursor(x, 15);
display.print((int)temperature);
// Degree symbol and C
display.setTextSize(2);
display.setCursor(x + (tempStr.length() * 24), 20);
display.print("o");
display.setCursor(x + (tempStr.length() * 24) + 12, 25);
display.print("C");
// "Coolant" text at the bottom
display.setTextSize(1);
display.setCursor(20, 55);
display.print("Coolant");
}
display.display();
}
// ===== Setup =====
void setup() {
Serial.begin(115200);
Serial.println("\n\n========================================");
Serial.println("ESP32 ELM327 BLE OBD2 Reader with OLED");
Serial.println("========================================\n");
// ===== Initialize OLED Display =====
Serial.println("Initializing OLED display...");
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println("✗ ERROR: OLED display not found!");
Serial.println("Check wiring:");
Serial.println("VCC → 3.3V, GND → GND");
Serial.println("SCL → GPIO 22, SDA → GPIO 21");
for(;;); // Infinite loop
}
Serial.println("✓ OLED display initialized");
// Welcome screen
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(15, 10);
display.println("OBD2");
display.setCursor(10, 30);
display.println("Reader");
display.setTextSize(1);
display.setCursor(20, 52);
display.println("Starting...");
display.display();
delay(2000);
// Initialize BLE
Serial.println("Initializing BLE...");
BLEDevice::init("ESP32_OBD_Reader");
// Search screen
display.clearDisplay();
display.setTextSize(1);
display.setCursor(5, 25);
display.println("Searching ELM327...");
display.display();
// Start BLE scan
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setInterval(1349);
pBLEScan->setWindow(449);
pBLEScan->setActiveScan(true);
Serial.println("Starting BLE scan (10 seconds)...");
pBLEScan->start(10, false);
}
// ===== Main Loop =====
void loop() {
// If device was found, connect
if (doConnect) {
if (connectToELM327()) {
doConnect = false;
// Initialize ELM327
if (initializeELM327()) {
Serial.println("Ready to read!\n");
}
} else {
doConnect = false;
Serial.println("Connection failed. Restarting in 5 seconds...");
delay(5000);
ESP.restart();
}
}
// If connected, read temperature every 5 seconds
if (deviceConnected) {
// Run a test on first iteration
static bool firstRun = true;
if (firstRun) {
Serial.println("\n>>> Running test query <<<");
// Test: Query supported PIDs (0100)
sendOBDCommand("0100");
delay(1000);
String testResponse = waitForResponse(3000);
Serial.print("Test response (0100): ");
Serial.println(testResponse);
delay(1000);
firstRun = false;
}
readCoolantTemperature();
//Serial.println(); // Empty line for better readability
delay(500); // 500ms works with your setup
}
// Only reconnect if really disconnected AND was previously connected
// (not on first start)
static bool wasConnected = false;
if (!deviceConnected && wasConnected) {
Serial.println("Connection lost! Trying to reconnect...");
// Error screen
display.clearDisplay();
display.setTextSize(1);
display.setCursor(5, 20);
display.println("Connection");
display.setCursor(5, 35);
display.println("lost!");
display.display();
delay(5000);
ESP.restart();
}
// Save status
if (deviceConnected) {
wasConnected = true;
}
}
The problems I had was to find out wich OBD protocol my car was using and to simulate my car to not always get my car started. To find out wich OBD protocol my car uses I send this command AT DP via an app for OBD "Terminal" which replies with the used protocol by the ELM327. To simulate my car I bought an OBD simulator on aliexpress which lets you simulate your car with some values e.g. rpm, speed, temperature. I bought this one without the display.
I also made an upgraded version of that where I can read more values of my car such as rpm, speed, battery voltage, intake air temperature and so on. To display these Values nicely, I bought the Nextion Display (round 2.1'' Link ) and made a "GUI". I also 3D-printed a mount for it to fit into the air vent of my car. The Display being touch allows me to click trough the values. Thanks to the tutorial by UPIR. That video got me to buy that display.
Here is a video that shows how it works.
elm327_esp32.mov.zip (6,6 MB)
Let me know when you got any questions or just use Claude ai. It got the whole project to work which ChatGPT was not able to.

