Community project

ESP32 Display Setup

ESP32
Photo of ESP32 Display Setup
Generated with AI

Goldwyn031409

Published October 26, 2026

This guide builds an ESP32-based vehicle dashboard that connects to OBD-II diagnostic adapters via Bluetooth to display real-time engine data on a built-in touchscreen. The project reads parameters like RPM, speed, coolant temperature, and fuel level, then logs the data to a microSD card for later analysis. Riders and mechanics get a complete wiring diagram, parts list, and step-by-step assembly instructions to connect the display, configure the Bluetooth adapter, and load the firmware.

The dashboard supports multiple vehicle types (Honda, Yamaha, Kawasaki, Suzuki, and generic OBD-II) and includes features like dark mode, trip statistics, and fuel consumption tracking. Assembly is straightforward: keep the factory screen connected, insert a microSD card for data logging, pair the ELM327 adapter, and power up the board. The firmware handles all Bluetooth communication, touchscreen input, and SD card operations.

Wiring diagram

Gather all the parts

QtyComponent
1

Classic Bluetooth ELM327 OBD-II adapter

A plug-in vehicle diagnostic adapter that sends live engine data and fault codes wirelessly to the dashboard.

Assemble it in 4 steps

1. Leave the built-in screen connected

Use the ESP32-2432S028R board with its screen facing you horizontally. Do not add any wires to the screen: the display and touch layer are already connected inside the board.

  • Do not connect power to any display pins; extra or swapped wiring can damage the board.

2. Insert a microSD card for logs

With the board unplugged, slide a microSD card formatted as FAT32 into the built-in card slot until it clicks or seats firmly. The board saves a new line of live OBD data to obd_log.csv every 10 seconds while Bluetooth is connected.

  • Use a small, reliable card and leave it inserted while recording. FAT32 is the common format Windows can open directly.
  • Do not remove the card while the board is powered and recording; an interrupted write can damage the saved file.

3. Plug in the Bluetooth diagnostic adapter

With the motorcycle safely parked, locate its diagnostic socket and push the elm327_bt_1 adapter straight in until it is fully seated. Its light should turn on because the diagnostic socket supplies its power.

  • The diagnostic socket supplies the adapter's power, so the adapter needs no separate battery.
  • Do not clear fault codes while riding. Clearing them can erase warning information needed for safe repairs.

4. Power the dashboard board

Plug a USB data cable into the ESP32-2432S028R board. The cable powers the screen and lets Schematik transfer the firmware; the board communicates with the diagnostic adapter wirelessly.

  • After a ride, turn the board off before removing the card, then insert it into a Windows computer and open obd_log.csv in Excel or another spreadsheet program.
  • Do not leave loose boards or cables near steering, brakes, pedals, or other moving motorcycle controls.

Review all connections

1. Connections between "elm327_bt_1" and "ESP32"

Functionelm327_bt_1ESP32
dataOBD-II plug → Vehicle OBD-II diagnostic socketEXT

Deploy the firmware

#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include <BluetoothSerial.h>
#include <SD.h>
#include <FS.h>
#include <ctype.h>

#define TFT_BL 21
#define TOUCH_CS 33
#define TOUCH_IRQ 36
#define TOUCH_SCK 25
#define TOUCH_MISO 39
#define TOUCH_MOSI 32
#define RGB_RED 4
#define RGB_GREEN 16
#define RGB_BLUE 17
#define SD_CS 5
#define SD_SCK 18
#define SD_MISO 19
#define SD_MOSI 23
TFT_eSPI tft;
SPIClass touchSPI(VSPI);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);
BluetoothSerial SerialBT;
uint16_t BG=TFT_BLACK, W=TFT_WHITE, C=TFT_CYAN, G=TFT_GREEN, R=TFT_RED, Y=TFT_YELLOW, GR=0x7BEF;
bool darkMode=true, displayMenu=false;
void applyTheme(){
  if(darkMode){
    BG=TFT_BLACK; W=TFT_WHITE; C=TFT_CYAN; G=TFT_GREEN; R=TFT_RED; Y=TFT_YELLOW; GR=0x7BEF;
  }else{
    BG=TFT_WHITE; W=TFT_BLACK; C=TFT_BLUE; G=0x03E0; R=TFT_RED; Y=0xD5A0; GR=0x7BEF;
  }
}
char adapterNames[6][32]={{"OBDII"},{"OBD2"},{"V-LINK"},{"ELM327"}};
int adapterCount=4;
const char *bikeNames[]={"GENERIC OBD-II","HONDA OBD-II","YAMAHA OBD-II","KAWASAKI OBD-II","SUZUKI OBD-II"};
int choice=0,bikeChoice=0,rpm=-1,speed=-1,temp=-1,fuel=-1; float odo=-1;
// Fuel percentage is used with this adjustable nominal tank size for a ride-average estimate.
const float NOMINAL_TANK_LITRES=5.0f;
float fuelAtStart=-1, odoAtStart=-1, averageKmPerL=-1;
bool menu=false,bikeMenu=false,connected=false;
bool confirmBike=false,confirmConnect=false,confirmClear=false,connecting=false,scanning=false;
unsigned long scanStarted=0;
bool clearingErrors=false;
unsigned long clearStarted=0;
enum LedAlert { LED_NORMAL, LED_CLEAR_STEADY, LED_CLEAR_OK, LED_CLEAR_FAIL };
LedAlert ledAlert=LED_NORMAL;
unsigned long ledAlertStarted=0;
int pendingBike=0;
volatile bool connectionFinished=false, connectionOK=false;
volatile uint32_t connectToken=0;
unsigned long lastPoll=0,lastTouch=0,lastLog=0;
bool sdAvailable=false;
const unsigned long LOG_INTERVAL_MS=10000UL;
const char *LOG_FILE="/obd_log.csv";
char status[45]="Tap CONNECT and choose your adapter";
char dtc[40]="Tap READ DTC to check faults";

void msg(const char*s){strncpy(status,s,44);status[44]=0;}

// The touchscreen and card socket share the ESP32's VSPI controller but use
// different physical pins. Switch the controller immediately before use.
void selectTouchBus(){
  // This XPT2046 driver uses the ESP32's default VSPI controller.
  SPI.end();
  SPI.begin(TOUCH_SCK,TOUCH_MISO,TOUCH_MOSI,TOUCH_CS);
  touch.begin();
}
void selectSdBus(){
  touchSPI.end();
  touchSPI.begin(SD_SCK,SD_MISO,SD_MOSI,SD_CS);
}
void initialiseSd(){
  selectSdBus();
  sdAvailable=SD.begin(SD_CS,touchSPI,8000000);
  if(sdAvailable && !SD.exists(LOG_FILE)){
    File logFile=SD.open(LOG_FILE,FILE_WRITE);
    if(logFile){
      logFile.println("elapsed_s,profile,rpm,speed_kmh,coolant_c,fuel_pct,odometer_km,average_km_per_l,dtc");
      logFile.close();
    }else sdAvailable=false;
  }
  selectTouchBus();
}
void appendLog(){
  if(!sdAvailable || !connected)return;
  selectSdBus();
  File logFile=SD.open(LOG_FILE,FILE_APPEND);
  if(logFile){
    logFile.printf("%lu,%s,",millis()/1000UL,bikeNames[bikeChoice]);
    if(rpm>=0) logFile.print(rpm);
    logFile.print(',');
    if(speed>=0) logFile.print(speed);
    logFile.print(',');
    if(temp>=0) logFile.print(temp);
    logFile.print(',');
    if(fuel>=0) logFile.print(fuel);
    logFile.print(',');
    if(odo>=0) logFile.print(odo,1);
    logFile.print(',');
    if(averageKmPerL>=0) logFile.print(averageKmPerL,1);
    logFile.print(",\"");
    logFile.print(dtc);
    logFile.println("\"");
    logFile.close();
  }else sdAvailable=false;
  selectTouchBus();
}

// This board's RGB LED is common-anode, so LOW turns each color on.
void setRgb(bool red, bool green, bool blue){
  digitalWrite(RGB_RED, red ? LOW : HIGH);
  digitalWrite(RGB_GREEN, green ? LOW : HIGH);
  digitalWrite(RGB_BLUE, blue ? LOW : HIGH);
}

void updateRgb(){
  // Keep blue solid for the whole time the clear command is in progress.
  if(ledAlert==LED_CLEAR_STEADY){
    setRgb(false, false, true);
    return;
  }
  // A completed command is reported by exactly three 200 ms flashes.
  if(ledAlert==LED_CLEAR_OK || ledAlert==LED_CLEAR_FAIL){
    unsigned long elapsed=millis()-ledAlertStarted;
    if(elapsed<1200UL){
      bool on=((elapsed/200UL)&1U)==0;
      if(ledAlert==LED_CLEAR_OK) setRgb(false, false, on);
      else setRgb(on, false, false);
      return;
    }
    ledAlert=LED_NORMAL;
  }
  bool phase = ((millis() / 250UL) & 1U) == 0;
  // Green flashes while the board is trying to connect to the OBD adapter.
  if(connecting) setRgb(false, phase, false);
  // Green stays on after Bluetooth has connected.
  else if(connected) setRgb(false, true, false);
  // Red flashes when no adapter is connected.
  else setRgb(phase, false, false);
}

void btn(int x,int y,int w,int h,const char*s,uint16_t c){tft.drawRoundRect(x,y,w,h,4,c);tft.setTextSize(1);tft.setTextColor(c,BG);tft.setCursor(x+(w-(int)strlen(s)*6)/2,y+11);tft.print(s);}
void box(int y,const char*label,const char*value,uint16_t c){tft.drawRoundRect(224,y,92,31,4,GR);tft.setTextSize(1);tft.setTextColor(W,BG);tft.setCursor(230,y+4);tft.print(label);tft.setTextColor(c,BG);tft.setCursor(230,y+17);tft.print(value);}
void header(){tft.fillRect(0,0,320,28,BG);tft.setTextSize(2);tft.setTextColor(C,BG);tft.setCursor(7,5);tft.print("MOTO OBD");tft.setTextSize(1);tft.setTextColor(connected?G:Y,BG);tft.setCursor(225,7);tft.print(connected?"CONNECTED":"OFFLINE");tft.drawFastHLine(0,27,320,C);}
void gauge(){const int gx=60,gy=102,gr=43;tft.drawCircle(gx,gy,gr,C);tft.drawCircle(gx,gy,gr-1,GR);for(int n=0;n<=10;n++){float a=(135+n*27)*PI/180;int x1=gx+cos(a)*(gr-13),y1=gy+sin(a)*(gr-13),x2=gx+cos(a)*(gr-5),y2=gy+sin(a)*(gr-5);tft.drawLine(x1,y1,x2,y2,n>=8?R:W);tft.setTextColor(n>=8?R:W,BG);tft.setTextSize(1);tft.setCursor(gx+cos(a)*(gr-22)-3,gy+sin(a)*(gr-22)-3);tft.print(n);}tft.setTextColor(C,BG);tft.setCursor(32,140);tft.print("x1000 RPM");}
void dashboard(){menu=false;tft.fillScreen(BG);header();gauge();tft.setTextSize(1);tft.setTextColor(GR,BG);tft.setCursor(144,48);tft.print("SPEED");tft.setTextSize(2);tft.setTextColor(C,BG);tft.setCursor(150,113);tft.print("km/h");box(34,"ENGINE RPM","--",C);box(70,"TEMP","--",G);box(106,"FUEL","--",G);box(142,"ODO KM","N/A",W);box(176,"AVG KM/L","--",Y);tft.drawRoundRect(4,151,215,25,4,GR);tft.setTextSize(1);tft.setTextColor(Y,BG);tft.setCursor(9,155);tft.print("DTC:");tft.setCursor(39,155);tft.print(dtc);btn(4,210,100,26,"CONNECT",C);btn(110,210,100,26,"READ DTC",Y);btn(216,210,100,26,"CLEAR DTC",R);}
void values(){if(menu)return;char s[18];tft.fillRect(5,31,110,122,BG);gauge();if(rpm>=0){float a=(135+(rpm>10000?10000:rpm)*.027)*PI/180;tft.drawLine(60,102,60+cos(a)*27,102+sin(a)*27,R);}tft.fillCircle(60,102,3,W);tft.fillRect(120,62,100,48,BG);tft.setTextSize(4);tft.setTextColor(W,BG);if(speed<0)strcpy(s,"---");else snprintf(s,18,"%d",speed);tft.setCursor(170-(int)strlen(s)*12,70);tft.print(s);if(rpm<0)strcpy(s,"--");else snprintf(s,18,"%d",rpm);box(34,"ENGINE RPM",s,rpm>=8000?R:C);if(temp<0)strcpy(s,"--");else snprintf(s,18,"%d C",temp);box(70,"TEMP",s,temp>=100?R:G);if(fuel<0)strcpy(s,"--");else snprintf(s,18,"%d%%",fuel);box(106,"FUEL",s,fuel>=0&&fuel<=20?R:G);if(odo<0)strcpy(s,"N/A");else snprintf(s,18,"%.1f",odo);box(142,"ODO KM",s,W);if(averageKmPerL<0)strcpy(s,"--");else snprintf(s,18,"%.1f",averageKmPerL);box(176,"AVG KM/L",s,Y);tft.fillRect(5,152,213,23,BG);tft.drawRoundRect(4,151,215,25,4,GR);tft.setTextSize(1);tft.setTextColor(Y,BG);tft.setCursor(9,155);tft.print("DTC:");tft.setCursor(39,155);tft.print(dtc);}
void settings(){menu=true;bikeMenu=false;displayMenu=false;tft.fillScreen(BG);tft.setTextSize(2);tft.setTextColor(C,BG);tft.setCursor(8,9);tft.print("BLUETOOTH OBD");tft.setTextSize(1);tft.setTextColor(W,BG);tft.setCursor(8,35);tft.print("Scan, then tap the adapter name you want:");for(int i=0;i<6;i++){int y=50+i*25;if(i<adapterCount){uint16_t c=i==choice?C:GR;tft.drawRoundRect(10,y,145,21,4,c);tft.setTextColor(c,BG);tft.setCursor(18,y+7);tft.print(i==choice?"> ":"  ");tft.print(adapterNames[i]);}}btn(170,50,140,30,"SCAN DEVICES",C);btn(170,89,140,30,"CONNECT",G);btn(170,128,67,30,"BIKE",Y);btn(243,128,67,30,"DISPLAY",C);btn(170,167,140,30,"DASHBOARD",C);tft.drawRoundRect(8,207,304,25,4,GR);tft.setTextColor(connected?G:Y,BG);tft.setCursor(13,216);tft.print(status);}
void displaySettings(){menu=true;bikeMenu=false;displayMenu=true;tft.fillScreen(BG);tft.setTextSize(2);tft.setTextColor(C,BG);tft.setCursor(10,16);tft.print("DISPLAY SETTINGS");tft.setTextSize(1);tft.setTextColor(W,BG);tft.setCursor(15,48);tft.print("Choose the screen background you prefer:");btn(24,82,130,45,"DARK MODE",darkMode?C:GR);btn(166,82,130,45,"LIGHT MODE",darkMode?GR:C);tft.setTextColor(W,BG);tft.setCursor(28,145);tft.print(darkMode?"Dark mode: black background, bright readings.":"Light mode: white background, dark readings.");btn(88,181,144,38,"BACK",C);}
void bikeSettings(){menu=true;bikeMenu=true;confirmBike=confirmConnect=confirmClear=false;tft.fillScreen(BG);tft.setTextSize(2);tft.setTextColor(C,BG);tft.setCursor(8,9);tft.print("MOTORCYCLE TYPE");tft.setTextSize(1);tft.setTextColor(W,BG);tft.setCursor(8,32);tft.print("Use the matching standard OBD-II profile:");for(int i=0;i<5;i++){int y=45+i*28;uint16_t c=i==bikeChoice?C:GR;tft.drawRoundRect(10,y,220,23,4,c);tft.setTextColor(c,BG);tft.setCursor(20,y+8);tft.print(i==bikeChoice?"> ":"  ");tft.print(bikeNames[i]);}btn(242,45,70,30,"BACK",C);tft.setTextColor(Y,BG);tft.setCursor(10,200);tft.print("Requires a motorcycle with standard OBD-II.");tft.setCursor(10,214);tft.print("Factory-only DTC systems need their own tool.");}
void confirmScreen(const char*title,const char*line1,const char*line2,const char*yes,uint16_t yesColor){menu=true;tft.fillScreen(BG);tft.setTextSize(2);tft.setTextColor(C,BG);tft.setCursor(10,20);tft.print(title);tft.drawRoundRect(10,55,300,75,5,GR);tft.setTextSize(1);tft.setTextColor(W,BG);tft.setCursor(20,76);tft.print(line1);tft.setCursor(20,96);tft.print(line2);btn(18,157,132,42,"CANCEL",C);btn(170,157,132,42,yes,yesColor);tft.setTextColor(Y,BG);tft.setCursor(16,220);tft.print("You can go back without changing anything.");}
void showBikeConfirm(){char line[42];snprintf(line,sizeof(line),"Use profile: %s?",bikeNames[pendingBike]);confirmBike=true;confirmScreen("CONFIRM BIKE TYPE",line,"Choose CANCEL if this is not your bike.","USE THIS",G);}
void showConnectConfirm(){char line[42];snprintf(line,sizeof(line),"Connect to: %s?",adapterNames[choice]);confirmConnect=true;confirmScreen("CONFIRM ADAPTER",line,"Choose CANCEL to select a different adapter.","CONNECT",G);}
void showClearConfirm(){confirmClear=true;confirmScreen("CLEAR FAULT CODES","This asks the motorcycle to erase stored codes.","Only do this while safely parked.","CLEAR",R);}
void connectingScreen(){menu=true;bikeMenu=false;confirmBike=confirmConnect=confirmClear=false;tft.fillScreen(BG);tft.setTextSize(2);tft.setTextColor(C,BG);tft.setCursor(10,25);tft.print("CONNECTING...");tft.setTextSize(1);tft.setTextColor(W,BG);tft.setCursor(18,65);tft.print("Trying the selected Bluetooth adapter.");tft.setCursor(18,82);tft.print("This can take a few seconds.");btn(78,145,164,42,"CANCEL CONNECT",R);}
bool looksLikeObdAdapter(const String &name){
  String n=name;
  n.toUpperCase();
  // Common advertised ELM327-compatible names, including spacing and dash variants.
  return n.indexOf("ELM327")>=0 || n.indexOf("JF327")>=0 ||
         n.indexOf("V-LINK")>=0 || n.indexOf("VLINK")>=0 ||
         n.indexOf("OBD2")>=0 || n.indexOf("OBD II")>=0 ||
         n.indexOf("OBD-II")>=0 || n.indexOf("OBD ")>=0;
}

void btDeviceFound(BTAdvertisedDevice *device){
  String found(device->getName().c_str());
  if(found.length()==0 || adapterCount>=6)return;
  for(int i=0;i<adapterCount;i++)if(found.equalsIgnoreCase(adapterNames[i]))return;
  int addedAt=adapterCount;
  found.toCharArray(adapterNames[addedAt],sizeof(adapterNames[0]));
  adapterCount++;
  // Prefer a likely diagnostic adapter automatically, but never start a connection.
  if(looksLikeObdAdapter(found)){
    choice=addedAt;
    msg("OBD-style adapter found and selected. Tap CONNECT.");
  }
}

void scanForAdapters(){
  if(connecting || connected){msg("Disconnect before scanning for another adapter.");settings();return;}
  adapterCount=0;
  choice=0;
  scanning=true;
  scanStarted=millis();
  msg("Scanning nearby Bluetooth devices for 8 seconds...");
  settings();
  if(!SerialBT.discoverAsync(btDeviceFound,8)){
    scanning=false;
    msg("Bluetooth scan could not start. Try again.");
    settings();
  }
}

void drain(){while(SerialBT.available())SerialBT.read();}
bool command(const char*c,char*out,size_t z,unsigned long wait=900){if(!SerialBT.connected(0))return false;drain();SerialBT.print(c);SerialBT.print('\r');unsigned long start=millis();size_t n=0;while(millis()-start<wait){while(SerialBT.available()){char ch=SerialBT.read();if(ch=='>'){out[n]=0;return true;}if(n+1<z)out[n++]=ch;}delay(5);}out[n]=0;return n>0;}
int bytes(const char*s,uint8_t*b,int max){int n=0,h=-1;for(;*s&&n<max;s++){if(!isxdigit((unsigned char)*s))continue;int v=isdigit((unsigned char)*s)?*s-'0':toupper((unsigned char)*s)-'A'+10;if(h<0)h=v;else{b[n++]=(h<<4)|v;h=-1;}}return n;}
bool pid(uint8_t p,uint8_t*d,int need){char q[5],r[96];snprintf(q,5,"01%02X",p);if(!command(q,r,sizeof(r)))return false;uint8_t b[40];int n=bytes(r,b,40);for(int i=0;i+need+1<n;i++)if(b[i]==0x41&&b[i+1]==p){for(int j=0;j<need;j++)d[j]=b[i+2+j];return true;}return false;}
void connectTask(void *parameter){uint32_t token=(uint32_t)(uintptr_t)parameter;SerialBT.disconnect();bool ok=SerialBT.connect(String(adapterNames[choice]));if(ok&&token==connectToken){char r[96];command("ATZ",r,sizeof(r),1800);command("ATE0",r,sizeof(r));command("ATL0",r,sizeof(r));command("ATS0",r,sizeof(r));command("ATH0",r,sizeof(r));command("ATSP0",r,sizeof(r));}if(token==connectToken){connectionOK=ok;connectionFinished=true;}vTaskDelete(NULL);}
void connectElm(){connected=false;connectionFinished=false;connectionOK=false;connecting=true;connectToken++;msg("Connecting to selected adapter...");connectingScreen();xTaskCreatePinnedToCore(connectTask,"obdConnect",4096,(void *)(uintptr_t)connectToken,1,NULL,0);}
void cancelConnect(){connectToken++;SerialBT.disconnect();connecting=false;msg("Connection cancelled. Choose an adapter when ready.");settings();}
void updateFuelAverage(){
  if(fuel<0 || odo<0)return;
  // Start a new ride baseline on the first valid pair, or after refuelling.
  if(fuelAtStart<0 || odoAtStart<0 || fuel>fuelAtStart+3){
    fuelAtStart=fuel;
    odoAtStart=odo;
    averageKmPerL=-1;
    return;
  }
  float usedLitres=(fuelAtStart-fuel)*NOMINAL_TANK_LITRES/100.0f;
  float distanceKm=odo-odoAtStart;
  if(usedLitres>=0.05f && distanceKm>=0) averageKmPerL=distanceKm/usedLitres;
}
void poll(){if(!connected||!SerialBT.connected(0)){connected=false;return;}uint8_t d[4];if(pid(0x0C,d,2))rpm=((int)d[0]*256+d[1])/4;if(pid(0x0D,d,1))speed=d[0];if(pid(0x05,d,1))temp=(int)d[0]-40;if(pid(0x2F,d,1))fuel=((int)d[0]*100)/255;if(pid(0xA6,d,4))odo=((uint32_t)d[0]<<24|(uint32_t)d[1]<<16|(uint32_t)d[2]<<8|d[3])/10.0f;updateFuelAverage();values();}
void readDtc(){if(!connected){msg("Connect first before reading fault codes.");settings();return;}char r[120];if(!command("03",r,sizeof(r))){msg("No reply from the adapter.");settings();return;}uint8_t b[50];int n=bytes(r,b,50),at=-1;for(int i=0;i<n;i++)if(b[i]==0x43){at=i+1;break;}if(at<0||at+1>=n||(b[at]==0&&!b[at+1]))strcpy(dtc,"No stored fault codes");else{const char f[]={'P','C','B','U'};snprintf(dtc,40,"%c%01X%01X%01X%01X (first code)",f[b[at]>>6],(b[at]>>4)&3,b[at]&15,b[at+1]>>4,b[at+1]&15);}msg("Fault-code reading finished.");if(menu)settings();else values();}
void clearDtc(){
  char r[80];
  bool sent=command("04",r,sizeof(r));
  clearingErrors=false;
  ledAlert=sent ? LED_CLEAR_OK : LED_CLEAR_FAIL;
  ledAlertStarted=millis();
  if(sent){
    strcpy(dtc,"Clear command sent to vehicle");
    msg("Clear sent. Re-read to confirm.");
  }else{
    msg("No reply; faults were not cleared.");
  }
  settings();
}

void beginClearDtc(){
  if(!connected){
    msg("Connect first before clearing faults.");
    settings();
    return;
  }
  clearingErrors=true;
  ledAlert=LED_CLEAR_STEADY;
  clearStarted=millis();
  msg("Clearing fault codes...");
  dashboard();
}
bool tap(int&x,int&y){selectTouchBus();if(!touch.touched()||millis()-lastTouch<250)return false;TS_Point p=touch.getPoint();x=constrain(map(p.x,220,3800,0,319),0,319);y=constrain(map(p.y,220,3800,0,239),0,239);lastTouch=millis();return true;}
void taps(){int x,y;if(!tap(x,y))return;if(connecting){if(y>=145&&y<=190)cancelConnect();return;}if(confirmBike){if(y>=157&&y<=199&&x<160){confirmBike=false;bikeSettings();}else if(y>=157&&y<=199){bikeChoice=pendingBike;msg("Motorcycle profile selected.");confirmBike=false;bikeSettings();}return;}if(confirmConnect){if(y>=157&&y<=199&&x<160){confirmConnect=false;settings();}else if(y>=157&&y<=199){confirmConnect=false;connectElm();}return;}if(confirmClear){if(y>=157&&y<=199&&x<160){confirmClear=false;dashboard();}else if(y>=157&&y<=199){confirmClear=false;beginClearDtc();}return;}if(!menu){if(y>=207&&x<106)settings();else if(y>=207&&x<212)readDtc();else if(y>=207)showClearConfirm();return;}if(bikeMenu){for(int i=0;i<5;i++)if(x<232&&y>=45+i*28&&y<=68+i*28){pendingBike=i;showBikeConfirm();return;}if(x>=242&&y>=45&&y<=75)settings();return;}if(displayMenu){if(y>=82&&y<=127&&x<160){darkMode=true;applyTheme();displaySettings();}else if(y>=82&&y<=127&&x>=160){darkMode=false;applyTheme();displaySettings();}else if(y>=181&&y<=219)settings();return;}for(int i=0;i<adapterCount;i++)if(x<156&&y>=50+i*25&&y<=71+i*25){choice=i;settings();return;}if(x>=170&&y>=50&&y<=80)scanForAdapters();else if(x>=170&&y>=89&&y<=119&&adapterCount>0)showConnectConfirm();else if(x>=170&&x<=237&&y>=128&&y<=158)bikeSettings();else if(x>=243&&y>=128&&y<=158)displaySettings();else if(x>=170&&y>=167&&y<=197)dashboard();}
void setup(){
  pinMode(TFT_BL,OUTPUT);
  digitalWrite(TFT_BL,HIGH);
  pinMode(RGB_RED,OUTPUT);
  pinMode(RGB_GREEN,OUTPUT);
  pinMode(RGB_BLUE,OUTPUT);
  setRgb(false,false,false);
  tft.init();tft.setRotation(1);tft.invertDisplay(true);
  applyTheme();
  selectTouchBus();
  touch.setRotation(1);
  initialiseSd();
  SerialBT.begin("CLICK-OBD",true);
  SerialBT.setPin("1234",4);
  if(sdAvailable) msg("SD card ready. Logs save every 10 seconds.");
  else msg("Insert a FAT32 microSD card to save logs.");
  dashboard();}
void loop(){
  updateRgb();
  taps();
  if(scanning && millis()-scanStarted>=9000UL){
    scanning=false;
    if(adapterCount==0)msg("No Bluetooth names found. Power the adapter, then scan again.");
    else msg("Scan complete. Tap the adapter name, then CONNECT.");
    settings();
  }
  if(connecting&&connectionFinished){
    connecting=false;
    connected=connectionOK;
    if(connected)msg("Connected. Dashboard reads live data.");
    else msg("Not found. Pair it first, then try again.");
    settings();
  }
  // Let the blue LED visibly blink before the clear command is sent.
  if(clearingErrors && millis()-clearStarted>=750UL){
    clearDtc();
  }
  if(!menu&&connected&&millis()-lastPoll>=1000){
    lastPoll=millis();
    poll();
  }
  if(connected && sdAvailable && millis()-lastLog>=LOG_INTERVAL_MS){
    lastLog=millis();
    appendLog();
  }
  delay(5);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik