/* KIA eNiro Dashboard 1.01, 2020-04-12 working only with OBD BLE 4.0 adapters ex. Vgate ICar Pro (BLE4.0 version) IMPORTANT Replace HM_MAC, serviceUUID, charTxUUID, charRxUUID as described below !! How to obtain MAC + 3x UUID? (I want to add pairing via buttons later) Run Android BLE scanner - choose IOS-VLINK device - get mac address a replace HM_MAC constant, then open CUSTOM service (first of 2) - there is serviceUUID (replace bellow in code) - open it.. there are 2x custom characteristics (first is for NOTIFY (read), and second for WRITE,WRITE_REQUEST). set charTxUUID with UUID from NOTIFY section set charRxUUID with UUID from WRITE section Example. #define HM_MAC "dd:0d:30:50:ed:63" static BLEUUID serviceUUID("000018f0-0000-1000-8000-00805f9b34fb"); static BLEUUID charTxUUID("00002af0-0000-1000-8000-00805f9b34fb"); static BLEUUID charRxUUID("00002af1-0000-1000-8000-00805f9b34fb"); */ #include "SPI.h" #include "TFT_eSPI.h" #include "BLEDevice.h" // PLEASE CHANGE THIS SETTING for your BLE4 uint32_t PIN = 1234; // Temporary moved to initSettings(). Preparing to load/save settings from flash memory //#define HM_MAC "dd:0d:30:50:ed:63" // mac ios-vlink cez nRf connect //static BLEUUID serviceUUID("000018f0-0000-1000-8000-00805f9b34fb"); // nRf connect to ios.vlink / client / dblclick on unknown service - this is service UUID //static BLEUUID charTxUUID("00002af0-0000-1000-8000-00805f9b34fb"); // UUID from NOTIFY section (one of custom characteristics under unknown service) //static BLEUUID charRxUUID("00002af1-0000-1000-8000-00805f9b34fb"); // UUID from WRITE section (one of custom characteristics under unknown service) /////////////////////////////////////////////// // LILYGO TTGO T4 v1.3 BUTTONS #define BUTTON_MIDDLE 37 #define BUTTON_LEFT 38 #define BUTTON_RIGHT 39 /* TFT COLORS */ #define TFT_BLACK 0x0000 /* 0, 0, 0 */ #define TFT_DEFAULT_BK 0x0000 // 0x38E0 #define TFT_TEMP 0x0000 // NAVY #define TFT_GREEN 0x07E0 /* 0, 255, 0 */ #define TFT_RED 0xF800 /* 255, 0, 0 */ #define TFT_SILVER 0xC618 /* 192, 192, 192 */ #define TFT_YELLOW 0xFFE0 /* 255, 255, 0 */ #define TFT_DARKRED 0x3800 /* 128, 0, 0 */ #define TFT_DARKGREEN2 0x01E0 /* 128, 0, 0 */ // Misc #define GFXFF 1 // TFT FOnts #define PSI2BAR_DIVIDER 14.503773800722 // tires psi / 14,503773800722 -> bar TFT_eSPI tft = TFT_eSPI(); static boolean bleConnect = true; static boolean bleConnected = false; static BLEAddress *pServerAddress; static BLERemoteCharacteristic* pRemoteCharacteristic; static BLERemoteCharacteristic* pRemoteCharacteristicWrite; BLEAdvertisedDevice* foundMyBleDevice; BLEClient* pClient; BLEScan* pBLEScan; // Temporary variables char ch; String line; char tmpStr1[20]; char tmpStr2[20]; char tmpStr3[20]; char tmpStr4[20]; // Main #define displayScreenCount 4 byte displayScreen = 1; // 0 - blank screen, 1 - dash board (default), 2 - big speed + kwh/100, 3 - battery cells, 4 - charging graph bool btnLeftPressed = true; bool btnMiddlePressed = true; bool btnRightPressed = true; // Commands loop #define commandQueueCount 23 #define commandQueueLoopFrom 7 String responseRow; String responseRowMerged; byte commandQueueIndex; bool canSendNextAtCommand = false; String commandRequest = ""; String commandQueue[commandQueueCount] = { "AT Z", // Reset all "AT I", // Print the version ID "AT E0", // Echo off "AT L0", // Linefeeds off //"AT SP 6", // Select protocol to ISO 15765-4 CAN (11 bit ID, 500 kbit/s) //"AT AL", // Allow Long (>7 byte) messages //"AT AR", // Automatically receive //"AT H1", // Headers on (debug only) "AT S0", // Printing of spaces on //"AT D1", // Display of the DLC on //"AT CAF0", // Automatic formatting off "AT DP", "atst16", // Loop from (KIA ENIRO) "atsh7e4", // BMS "220101", // power kw, ... "220102", // cell voltages, screen 3 only "220103", // cell voltages, screen 3 only "220104", // cell voltages, screen 3 only "220105", // soh, soc, .. //"220106", "atsh7e2", // VMCU "2101", // speed, ... "2102", // aux, ... //"atsh7df", //"2106", //"220106", "atsh7b3", "220100", // in/out temp "atsh7a0", // TMPS? "22c00b", // tire pressure/temp }; // Menu id/parent/title typedef struct { uint8_t lang; char* sound; char* value; } menuItem; String menu[] = { "1/0/Vehicle type", "100/1/Kia eNiro", "101/1/Hyundai Kona EV", "2/0/Pair OBD2 BLE adapter", "3/0/Other", "30/3/Screen rotation", "300/30/Normal", "301/30/Flip vertical", "4/0/Units", "40/4/Distance", "400/40/Kilometers", "401/40/Miles", "41/4/Temperature", "410/41/Celsius", "411/41/Fahrenheit", "42/4/Pressure", "420/42/Bar", "421/42/Psi", }; // Structure with realtime values struct strucParams { float speedKmh; float socPerc; float sohPerc; float cumulativeEnergyChargedKWh; float cumulativeEnergyChargedKWhStart; float cumulativeEnergyDischargedKWh; float cumulativeEnergyDischargedKWhStart; float batPowerAmp; float batPowerKw; float batPowerKwh100; float batVoltage; float batCellMinV; float batCellMaxV; float batTempC; float batHeaterC; float batInletC; float batMinC; float batMaxC; float batModule01TempC; float batModule02TempC; float batModule03TempC; float batModule04TempC; float auxPerc; float auxCurrentAmp; float auxVoltage; float indoorTemperature; float outdoorTemperature; float tireFrontLeftTempC; float tireFrontLeftPressureBar; float tireFrontRightTempC; float tireFrontRightPressureBar; float tireRearLeftTempC; float tireRearLeftPressureBar; float tireRearRightTempC; float tireRearRightPressureBar; float cellVoltage[98]; // 1..98 has index 0..97 float chargingGraphKw[101]; // 0..100% .. how many HW in each step float chargingGraphMinTempC[101]; // 0..100% .. Min temp in.C float chargingGraphMaxTempC[101]; // 0..100% .. Max temp in.C }; // Setting stored to flash struct strucSettings { byte initFlag; // 183 value byte settingsVersion; // 1 char obdMacAddress[20]; char serviceUUID[40]; char charTxUUID[40]; char charRxUUID[40]; }; strucParams params; // Realtime sensor values strucParams oldParams; // Old states used for change detection (draw optimization) strucSettings settings; // Settings stored into flash /** Init settings */ bool initSettings() { String tmpStr; settings.initFlag = 183; settings.settingsVersion = 1; tmpStr = "dd:0d:30:50:ed:63"; tmpStr.toCharArray(settings.obdMacAddress, 18); tmpStr = "000018f0-0000-1000-8000-00805f9b34fb"; tmpStr.toCharArray(settings.serviceUUID, 37); tmpStr = "00002af0-0000-1000-8000-00805f9b34fb"; tmpStr.toCharArray(settings.charTxUUID, 37); tmpStr = "00002af1-0000-1000-8000-00805f9b34fb"; tmpStr.toCharArray(settings.charRxUUID, 37); return true; } /** Init structure with data */ bool initStructure() { params.speedKmh = -1; params.socPerc = -1; params.sohPerc = -1; params.cumulativeEnergyChargedKWh = -1; params.cumulativeEnergyChargedKWhStart = -1; params.cumulativeEnergyDischargedKWh = -1; params.cumulativeEnergyDischargedKWhStart = -1; params.batPowerAmp = -1; params.batPowerKw = -1; params.batPowerKwh100 = -1; params.batVoltage = -1; params.batCellMinV = -1; params.batCellMaxV = -1; params.batTempC = -1; params.batHeaterC = -1; params.batInletC = -1; params.batMinC = -1; params.batMaxC = -1; params.batModule01TempC = -1; params.batModule02TempC = -1; params.batModule03TempC = -1; params.batModule04TempC = -1; params.auxPerc = -1; params.auxCurrentAmp = -1; params.auxVoltage = -1; params.indoorTemperature = -1; params.outdoorTemperature = -1; params.tireFrontLeftTempC = -1; params.tireFrontLeftPressureBar = -1; params.tireFrontRightTempC = -1; params.tireFrontRightPressureBar = -1; params.tireRearLeftTempC = -1; params.tireRearLeftPressureBar = -1; params.tireRearRightTempC = -1; params.tireRearRightPressureBar = -1; for (int i = 0; i < 98; i++) { params.cellVoltage[i] = 0; } for (int i = 0; i <= 100; i++) { params.chargingGraphKw[i] = 0; params.chargingGraphMinTempC[i] = -100; params.chargingGraphMaxTempC[i] = -100; } oldParams = params; return true; } /** Hex to dec (1-2 byte values, signed/unsigned) For 4 byte change int to long and add part for signed numbers */ float hexToDec(String hexString, byte bytes = 2, bool signedNum = true) { unsigned int decValue = 0; unsigned int nextInt; for (int i = 0; i < hexString.length(); i++) { nextInt = int(hexString.charAt(i)); if (nextInt >= 48 && nextInt <= 57) nextInt = map(nextInt, 48, 57, 0, 9); if (nextInt >= 65 && nextInt <= 70) nextInt = map(nextInt, 65, 70, 10, 15); if (nextInt >= 97 && nextInt <= 102) nextInt = map(nextInt, 97, 102, 10, 15); nextInt = constrain(nextInt, 0, 15); decValue = (decValue * 16) + nextInt; } // Unsigned - do nothing if (!signedNum) { return decValue; } // Signed for 1, 2 bytes if (bytes == 1) { return (decValue > 127 ? (float)decValue - 256.0 : decValue); } return (decValue > 32767 ? (float)decValue - 65536.0 : decValue); } /** Draw cell on dashboard */ bool monitoringRect(int32_t x, int32_t y, int32_t w, int32_t h, const char* text, const char* desc, int16_t bgColor, int16_t fgColor) { int32_t posx, posy; posx = (x * 80) + 4; posy = (y * 60) + 1; tft.fillRect(x * 80, y * 60, ((w) * 80) - 1, ((h) * 60) - 1, bgColor); tft.drawFastVLine(((x + w) * 80) - 1, ((y) * 60) - 1, h * 60, TFT_BLACK); tft.drawFastHLine(((x) * 80) - 1, ((y + h) * 60) - 1, w * 80, TFT_BLACK); tft.setTextDatum(TL_DATUM); // Topleft tft.setTextColor(TFT_SILVER, bgColor); // Bk, fg color tft.setTextSize(1); // Size for small 5x7 font tft.drawString(desc, posx, posy, 2); // Big 2x2 cell in the middle of screen if (w == 2 && h == 2) { // Bottom 2 numbers with charged/discharged kWh from start posx = (x * 80) + 5; posy = ((y + h) * 60) - 32; sprintf(tmpStr3, "-%01.01f", params.cumulativeEnergyDischargedKWh - params.cumulativeEnergyDischargedKWhStart); tft.setFreeFont(&Roboto_Thin_24); tft.setTextDatum(TL_DATUM); tft.drawString(tmpStr3, posx, posy, GFXFF); posx = ((x + w) * 80) - 8; sprintf(tmpStr3, "+%01.01f", params.cumulativeEnergyChargedKWh - params.cumulativeEnergyChargedKWhStart); tft.setTextDatum(TR_DATUM); tft.drawString(tmpStr3, posx, posy, GFXFF); // Main number - kwh on roads, amps on charges posy = (y * 60) + 24; tft.setTextColor(fgColor, bgColor); tft.setFreeFont(&Orbitron_Light_32); tft.drawString(text, posx, posy, 7); } else { // All others 1x1 cells tft.setTextDatum(MC_DATUM); tft.setTextColor(fgColor, bgColor); tft.setFreeFont(&Orbitron_Light_24); posx = (x * 80) + (w * 80 / 2) - 3; posy = (y * 60) + (h * 60 / 2) + 4; tft.drawString(text, posx, posy, (w == 2 ? 7 : GFXFF)); } return true; } /** Draw small rect 80x30 */ bool drawSmallRect(int32_t x, int32_t y, int32_t w, int32_t h, const char* text, const char* desc, int16_t bgColor, int16_t fgColor) { int32_t posx, posy; posx = (x * 80) + 4; posy = (y * 32) + 1; tft.fillRect(x * 80, y * 32, ((w) * 80), ((h) * 32), bgColor); tft.drawFastVLine(((x + w) * 80) - 1, ((y) * 32) - 1, h * 32, TFT_BLACK); tft.drawFastHLine(((x) * 80) - 1, ((y + h) * 32) - 1, w * 80, TFT_BLACK); tft.setTextDatum(TL_DATUM); // Topleft tft.setTextColor(TFT_SILVER, bgColor); // Bk, fg bgColor tft.setTextSize(1); // Size for small 5x7 font tft.drawString(desc, posx, posy, 2); tft.setTextDatum(TC_DATUM); tft.setTextColor(fgColor, bgColor); posx = (x * 80) + (w * 80 / 2) - 3; tft.drawString(text, posx, posy + 14, 2); return true; } /** Show tire pressures / temperatures Custom field */ bool showTires(int32_t x, int32_t y, int32_t w, int32_t h, const char* topleft, const char* topright, const char* bottomleft, const char* bottomright, int16_t color) { int32_t posx, posy; tft.fillRect(x * 80, y * 60, ((w) * 80) - 1, ((h) * 60) - 1, color); tft.drawFastVLine(((x + w) * 80) - 1, ((y) * 60) - 1, h * 60, TFT_BLACK); tft.drawFastHLine(((x) * 80) - 1, ((y + h) * 60) - 1, w * 80, TFT_BLACK); tft.setTextDatum(TL_DATUM); tft.setTextColor(TFT_SILVER, color); tft.setTextSize(1); posx = (x * 80) + 4; posy = (y * 60) + 0; tft.drawString(topleft, posx, posy, 2); posy = (y * 60) + 14; tft.drawString(bottomleft, posx, posy, 2); tft.setTextDatum(TR_DATUM); posx = ((x + w) * 80) - 4; posy = (y * 60) + 0; tft.drawString(topright, posx, posy, 2); posy = (y * 60) + 14; tft.drawString(bottomright, posx, posy, 2); return true; } /** Main screen (Screen 0) */ bool drawSceneMain(bool force) { // Tire pressure if (force || params.tireFrontLeftTempC != oldParams.tireFrontLeftTempC || params.tireFrontRightTempC != oldParams.tireFrontRightTempC || params.tireRearLeftTempC != oldParams.tireRearLeftTempC || params.tireRearRightTempC != oldParams.tireRearRightTempC || oldParams.cumulativeEnergyChargedKWhStart != params.cumulativeEnergyChargedKWhStart || oldParams.cumulativeEnergyChargedKWh != params.cumulativeEnergyChargedKWh || oldParams.cumulativeEnergyDischargedKWhStart != params.cumulativeEnergyDischargedKWhStart || oldParams.cumulativeEnergyDischargedKWh != params.cumulativeEnergyDischargedKWh ) { sprintf(tmpStr1, "%01.01fbar %02.00fC", params.tireFrontLeftPressureBar, params.tireFrontLeftTempC); sprintf(tmpStr2, "%02.00fC %01.01fbar", params.tireFrontRightTempC, params.tireFrontRightPressureBar); sprintf(tmpStr3, "%01.01fbar %02.00fC", params.tireRearLeftPressureBar, params.tireRearLeftTempC); sprintf(tmpStr4, "%02.00fC %01.01fbar", params.tireRearRightTempC, params.tireRearRightPressureBar); showTires(1, 0, 2, 1, tmpStr1, tmpStr2, tmpStr3, tmpStr4, TFT_BLACK); // Added later - kwh total in tires box // TODO: refactoring tft.setTextDatum(TL_DATUM); tft.setTextColor(TFT_GREEN, TFT_BLACK); sprintf(tmpStr1, "C: %01.01f +%01.01fkWh", params.cumulativeEnergyChargedKWh, params.cumulativeEnergyChargedKWh - params.cumulativeEnergyChargedKWhStart); tft.drawString(tmpStr1, (1 * 80) + 4, (0 * 60) + 30, 2); tft.setTextColor(TFT_YELLOW, TFT_BLACK); sprintf(tmpStr1, "D: %01.01f -%01.01fkWh", params.cumulativeEnergyDischargedKWh, params.cumulativeEnergyDischargedKWh - params.cumulativeEnergyDischargedKWhStart); tft.drawString(tmpStr1, (1 * 80) + 4, (0 * 60) + 44, 2); oldParams.tireFrontLeftTempC = params.tireFrontLeftTempC; oldParams.tireFrontLeftPressureBar = params.tireFrontLeftPressureBar; oldParams.tireFrontRightTempC = params.tireFrontRightTempC; oldParams.tireFrontRightPressureBar = params.tireFrontRightPressureBar; oldParams.tireRearLeftTempC = params.tireRearLeftTempC; oldParams.tireRearLeftPressureBar = params.tireRearLeftPressureBar; oldParams.tireRearRightTempC = params.tireRearRightTempC; oldParams.tireRearRightPressureBar = params.tireRearRightPressureBar; oldParams.cumulativeEnergyChargedKWhStart = params.cumulativeEnergyChargedKWhStart; oldParams.cumulativeEnergyChargedKWh = params.cumulativeEnergyChargedKWh; oldParams.cumulativeEnergyDischargedKWhStart = params.cumulativeEnergyDischargedKWhStart; oldParams.cumulativeEnergyDischargedKWh = params.cumulativeEnergyDischargedKWh; } // batPowerKwh100 on roads, else batPowerAmp if (params.speedKmh > 20) { if (force || params.batPowerKwh100 != oldParams.batPowerKwh100) { sprintf(tmpStr1, "%01.01f", params.batPowerKwh100); monitoringRect(1, 1, 2, 2, tmpStr1, "KWH/100KM", (params.batPowerKwh100 >= 0 ? TFT_DARKGREEN2 : (params.batPowerKwh100 < -30.0 ? TFT_RED : TFT_DARKRED)), TFT_WHITE); oldParams.speedKmh = params.batPowerKwh100; } } else { // batPowerAmp on chargers (under 10kmh) if (force || params.batPowerAmp != oldParams.batPowerAmp) { sprintf(tmpStr1, (abs(params.batPowerAmp) > 9.9 ? "%01.00f" : "%01.01f"), params.batPowerAmp); monitoringRect(1, 1, 2, 2, tmpStr1, "BATTERY POWER [A]", (params.batPowerAmp >= 0 ? TFT_DARKGREEN2 : TFT_DARKRED), TFT_WHITE); oldParams.batPowerAmp = params.batPowerAmp; } } // socPerc if (force || params.socPerc != oldParams.socPerc) { sprintf(tmpStr1, "%01.00f%%", params.socPerc); sprintf(tmpStr2, (params.sohPerc == 100.0 ? "SOC/H%01.00f%%" : "SOC/H%01.01f%%"), params.sohPerc); monitoringRect(0, 0, 1, 1, tmpStr1, tmpStr2, (params.socPerc < 10 || params.sohPerc < 100 ? TFT_RED : (params.socPerc > 80 ? TFT_DARKGREEN2 : TFT_DEFAULT_BK)), TFT_WHITE); oldParams.socPerc = params.socPerc; oldParams.sohPerc = params.sohPerc; } // batPowerAmp if (force || params.batPowerKw != oldParams.batPowerKw) { sprintf(tmpStr1, "%01.01f", params.batPowerKw); monitoringRect(0, 1, 1, 1, tmpStr1, "POWER KW", (params.batPowerKw >= 0 ? TFT_DARKGREEN2 : (params.batPowerKw <= -30 ? TFT_RED : TFT_DARKRED)), TFT_WHITE); oldParams.batPowerKw = params.batPowerKw; } // batVoltage if (force || params.batVoltage != oldParams.batVoltage) { sprintf(tmpStr1, "%03.00f", params.batVoltage); monitoringRect(0, 2, 1, 1, tmpStr1, "VOLTAGE", TFT_DEFAULT_BK, TFT_WHITE); oldParams.batVoltage = params.batVoltage; } // batCellMinV if (force || params.batCellMinV != oldParams.batCellMinV || params.batCellMaxV != oldParams.batCellMaxV) { sprintf(tmpStr1, "%01.02f", params.batCellMaxV - params.batCellMinV); sprintf(tmpStr2, "CELLS %01.02f", params.batCellMinV); monitoringRect(0, 3, 1, 1, ( params.batCellMaxV - params.batCellMinV == 0.00 ? "OK" : tmpStr1), tmpStr2, TFT_DEFAULT_BK, TFT_WHITE); oldParams.batCellMaxV = params.batCellMaxV; oldParams.batCellMinV = params.batCellMinV; } // batTempC if (force || params.batTempC != oldParams.batTempC) { sprintf(tmpStr1, "%01.00f", params.batTempC); monitoringRect(1, 3, 1, 1, tmpStr1, "BAT.TEMP.C", TFT_TEMP, (params.batTempC >= 15) ? ((params.batTempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED); oldParams.batTempC = params.batTempC; } // batHeaterC if (force || params.batHeaterC != oldParams.batHeaterC) { sprintf(tmpStr1, "%01.00f", params.batHeaterC); monitoringRect(2, 3, 1, 1, tmpStr1, "BAT.HEAT C", TFT_TEMP, TFT_WHITE); oldParams.batHeaterC = params.batHeaterC; } // Aux perc if (force || params.auxPerc != oldParams.auxPerc) { sprintf(tmpStr1, "%01.00f%%", params.auxPerc); monitoringRect(3, 0, 1, 1, tmpStr1, "AUX BAT.", (params.auxPerc < 60 ? TFT_RED : TFT_DEFAULT_BK), TFT_WHITE); oldParams.auxPerc = params.auxPerc; } // Aux amp if (force || params.auxCurrentAmp != oldParams.auxCurrentAmp) { sprintf(tmpStr1, (abs(params.auxCurrentAmp) > 9.9 ? "%01.00f" : "%01.01f"), params.auxCurrentAmp); monitoringRect(3, 1, 1, 1, tmpStr1, "AUX AMPS", (params.auxCurrentAmp >= 0 ? TFT_DARKGREEN2 : TFT_DARKRED), TFT_WHITE); oldParams.auxCurrentAmp = params.auxCurrentAmp; } // auxVoltage if (force || params.auxVoltage != oldParams.auxVoltage) { sprintf(tmpStr1, "%01.01f", params.auxVoltage); monitoringRect(3, 2, 1, 1, tmpStr1, "AUX VOLTS", (params.auxVoltage < 12.1 ? TFT_RED : (params.auxVoltage < 12.6 ? TFT_ORANGE : TFT_DEFAULT_BK)), TFT_WHITE); oldParams.auxVoltage = params.auxVoltage; } // indoorTemperature if (force || params.indoorTemperature != oldParams.indoorTemperature || params.outdoorTemperature != oldParams.outdoorTemperature) { sprintf(tmpStr1, "%01.01f", params.indoorTemperature); sprintf(tmpStr2, "IN/OUT%01.01fC", params.outdoorTemperature); monitoringRect(3, 3, 1, 1, tmpStr1, tmpStr2, TFT_TEMP, TFT_WHITE); oldParams.indoorTemperature = params.indoorTemperature; oldParams.outdoorTemperature = params.outdoorTemperature; } return true; } /** Speed + kwh/100km (Screen 1) */ bool drawSceneSpeed(bool force) { int32_t posx, posy; posx = 320 / 2; posy = 32; tft.setTextDatum(TC_DATUM); // Top center tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); // Size for small 5x7 font sprintf(tmpStr3, " 0 "); if (params.speedKmh > 10) sprintf(tmpStr3, " %01.00f ", params.speedKmh); tft.drawString(tmpStr3, posx, posy, 7); posy = 140; tft.setTextDatum(TC_DATUM); // Top center tft.setTextSize(1); if (params.speedKmh > 25 && params.batPowerKw > 0) { sprintf(tmpStr3, " %01.01f ", params.batPowerKwh100); } else { sprintf(tmpStr3, " %01.01f ", params.batPowerKw); } tft.drawString(tmpStr3, posx, posy, 7); // Bottom 2 numbers with charged/discharged kWh from start posx = 10; posy = 240 - 10; sprintf(tmpStr3, "-%01.01f ", params.cumulativeEnergyDischargedKWh - params.cumulativeEnergyDischargedKWhStart); tft.setFreeFont(&Roboto_Thin_24); tft.setTextDatum(BL_DATUM); tft.drawString(tmpStr3, posx, posy, GFXFF); posx = 320 - 10; sprintf(tmpStr3, " +%01.01f", params.cumulativeEnergyChargedKWh - params.cumulativeEnergyChargedKWhStart); tft.setTextDatum(BR_DATUM); tft.drawString(tmpStr3, posx, posy, GFXFF); posx = 320 / 2; sprintf(tmpStr3, " %01.01fkw ", params.batPowerKw); tft.setTextDatum(BC_DATUM); tft.drawString(tmpStr3, posx, posy, GFXFF); // Battery "cold gate" detection - red < 15C (43KW limit), <25 (blue - 55kW limit), green all ok sprintf(tmpStr3, "%01.00f", params.batTempC); tft.fillCircle(290, 30, 25, (params.batTempC >= 15) ? ((params.batTempC >= 25) ? TFT_DARKGREEN2 : TFT_BLUE) : TFT_RED); tft.setTextColor(TFT_WHITE, (params.batTempC >= 15) ? ((params.batTempC >= 25) ? TFT_DARKGREEN2 : TFT_BLUE) : TFT_RED); tft.setFreeFont(&Roboto_Thin_24); tft.setTextDatum(MC_DATUM); tft.drawString(tmpStr3, 290, 30, GFXFF); return true; } /** Battery cells (Screen 2) */ bool drawSceneBatteryCells(bool force) { int32_t posx, posy; sprintf(tmpStr1, "%01.00f C", params.batHeaterC); drawSmallRect(0, 0, 1, 1, tmpStr1, "HEATER", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%01.00f C", params.batInletC); drawSmallRect(1, 0, 1, 1, tmpStr1, "BAT.INLET", TFT_TEMP, TFT_CYAN); /*Not needed yet sprintf(tmpStr1, "%01.00fC", params.batMinC); drawSmallRect(2, 0, 1, 1, tmpStr1, "BAT.MIN", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%01.00fC", params.batMaxC); drawSmallRect(3, 0, 1, 1, tmpStr1, "BAT.MAX", TFT_TEMP, TFT_CYAN);*/ sprintf(tmpStr1, "%01.00f C", params.batModule01TempC); drawSmallRect(0, 1, 1, 1, tmpStr1, "MO1", TFT_TEMP, (params.batModule01TempC >= 15) ? ((params.batModule01TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED); sprintf(tmpStr1, "%01.00f C", params.batModule02TempC); drawSmallRect(1, 1, 1, 1, tmpStr1, "MO2", TFT_TEMP, (params.batModule02TempC >= 15) ? ((params.batModule02TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED); sprintf(tmpStr1, "%01.00f C", params.batModule03TempC); drawSmallRect(2, 1, 1, 1, tmpStr1, "MO3", TFT_TEMP, (params.batModule03TempC >= 15) ? ((params.batModule03TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED); sprintf(tmpStr1, "%01.00f C", params.batModule04TempC); drawSmallRect(3, 1, 1, 1, tmpStr1, "MO4", TFT_TEMP, (params.batModule04TempC >= 15) ? ((params.batModule04TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED); tft.setTextDatum(TL_DATUM); // Topleft tft.setTextSize(1); // Size for small 5x7 font // Find min and max val float minVal = -1, maxVal = -1; for (int i = 0; i < 98; i++) { if (params.cellVoltage[i] < minVal || minVal == -1) minVal = params.cellVoltage[i]; if (params.cellVoltage[i] > maxVal || maxVal == -1) maxVal = params.cellVoltage[i]; } // Draw cell matrix for (int i = 0; i < 98; i++) { posx = ((i % 8) * 40) + 4; posy = ((floor(i / 8)) * 13) + 68; sprintf(tmpStr3, "%01.02f", params.cellVoltage[i]); tft.setTextColor(TFT_NAVY, TFT_BLACK); if (params.cellVoltage[i] == minVal && minVal != maxVal) tft.setTextColor(TFT_RED, TFT_BLACK); if (params.cellVoltage[i] == maxVal && minVal != maxVal) tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.drawString(tmpStr3, posx, posy, 2); } return true; } /** Charging graph (Screen 3) */ bool drawSceneChargingGraph(bool force) { int zeroX = 0; int zeroY = 238; int mulX = 3; // 100% = 300px; int mulY = 2; // 100kW = 200px int maxKw = 80; int16_t color; sprintf(tmpStr1, "%01.00f", params.socPerc); drawSmallRect(0, 0, 1, 1, tmpStr1, "SOC", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%01.01f", params.batPowerKw); drawSmallRect(1, 0, 1, 1, tmpStr1, "POWER kW", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%01.01f", params.batPowerAmp); drawSmallRect(2, 0, 1, 1, tmpStr1, "POWER A", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%03.00f", params.batVoltage); drawSmallRect(3, 0, 1, 1, tmpStr1, "VOLTAGE", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%01.00f C", params.batHeaterC); drawSmallRect(0, 1, 1, 1, tmpStr1, "HEATER", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%01.00f C", params.batInletC); drawSmallRect(1, 1, 1, 1, tmpStr1, "BAT.INLET", TFT_TEMP, TFT_CYAN); sprintf(tmpStr1, "%01.00f C", params.batMinC); drawSmallRect(2, 1, 1, 1, tmpStr1, "BAT.MIN", (params.batMinC >= 15) ? ((params.batMinC >= 25) ? TFT_DARKGREEN2 : TFT_BLUE) : TFT_RED, TFT_CYAN); sprintf(tmpStr1, "%01.00f C", params.batMaxC); drawSmallRect(3, 1, 1, 1, tmpStr1, "BAT.MAX", TFT_TEMP, TFT_CYAN); tft.setTextSize(1); // Size for small 5x7 font tft.setTextDatum(TR_DATUM); sprintf(tmpStr1, "%01.00fC", params.batModule01TempC); tft.setTextColor((params.batModule01TempC >= 15) ? ((params.batModule01TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED, TFT_TEMP); tft.drawString(tmpStr1, zeroX + (10 * 10 * mulX), zeroY - (maxKw * mulY) + 00, 2); sprintf(tmpStr1, "%01.00fC", params.batModule02TempC); tft.setTextColor((params.batModule02TempC >= 15) ? ((params.batModule02TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED, TFT_TEMP); tft.drawString(tmpStr1, zeroX + (10 * 10 * mulX), zeroY - (maxKw * mulY) + 20, 2); sprintf(tmpStr1, "%01.00fC", params.batModule03TempC); tft.setTextColor((params.batModule03TempC >= 15) ? ((params.batModule03TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED, TFT_TEMP); tft.drawString(tmpStr1, zeroX + (10 * 10 * mulX), zeroY - (maxKw * mulY) + 40, 2); sprintf(tmpStr1, "%01.00fC", params.batModule04TempC); tft.setTextColor((params.batModule04TempC >= 15) ? ((params.batModule04TempC >= 25) ? TFT_GREEN : TFT_BLUE) : TFT_RED, TFT_TEMP); tft.drawString(tmpStr1, zeroX + (10 * 10 * mulX), zeroY - (maxKw * mulY) + 60, 2); tft.setTextColor(TFT_SILVER, TFT_TEMP); for (int i = 0; i <= 10; i++) { color = TFT_DARKRED; if (i == 0 || i == 5 || i == 10) color = TFT_NAVY; tft.drawFastVLine(zeroX + (i * 10 * mulX), zeroY - (maxKw * mulY), maxKw * mulY, color); if (i != 0 && i != 10) { sprintf(tmpStr1, "%d%%", i*10); tft.setTextDatum(BC_DATUM); tft.drawString(tmpStr1, zeroX + (i * 10 * mulX), zeroY - (maxKw * mulY), 2); } if (i <= (maxKw / 10)) { tft.drawFastHLine(zeroX, zeroY - (i * 10 * mulY), 100 * mulX, color); if (i > 0) { sprintf(tmpStr1, "%d", i*10); tft.setTextDatum(ML_DATUM); tft.drawString(tmpStr1, zeroX + (100 * mulX)+ 3, zeroY - (i * 10 * mulY), 2); } } } for (int i = 0; i <= 100; i++) { if (params.chargingGraphKw[i] > 0) tft.drawFastHLine(zeroX + (i * mulX) - (mulX / 2), zeroY - (params.chargingGraphKw[i]*mulY), mulX, TFT_YELLOW); if (params.chargingGraphMinTempC[i] > -10) tft.drawFastHLine(zeroX + (i * mulX) - (mulX / 2), zeroY - (params.chargingGraphMinTempC[i]*mulY), mulX, TFT_RED); if (params.chargingGraphMaxTempC[i] > -10) tft.drawFastHLine(zeroX + (i * mulX) - (mulX / 2), zeroY - (params.chargingGraphMaxTempC[i]*mulY), mulX, TFT_BLUE); } return true; } /** Redraw screen */ bool redrawScreen(bool force) { // Clear screen if needed if (force) { tft.fillScreen(TFT_BLACK); } // 1. Main screen if (displayScreen == 1) { drawSceneMain(force); } // 2. Big speed + kwh/100km if (displayScreen == 2) { drawSceneSpeed(force); } // 3. Battery cells if (displayScreen == 3) { drawSceneBatteryCells(force); } // Charging graph if (displayScreen == 4) { drawSceneChargingGraph(force); } return true; } /** Do next AT command from queue */ bool doNextAtCommand() { // Restart loop with AT commands if (commandQueueIndex >= commandQueueCount) { commandQueueIndex = commandQueueLoopFrom; // Redraw only changed values redrawScreen(false); } // Send AT command to obd commandRequest = commandQueue[commandQueueIndex]; Serial.print(">>> "); Serial.println(commandRequest); String tmpStr = commandRequest + "\r"; pRemoteCharacteristicWrite->writeValue(tmpStr.c_str(), tmpStr.length()); commandQueueIndex++; return true; } /** Parse result from OBD, create single line responseRowMerged */ bool parseRow() { // Simple 1 line responses Serial.print(""); Serial.println(responseRow); // Merge 0:xxxx 1:yyyy 2:zzzz to single xxxxyyyyzzzz string if (responseRow.length() >= 2 && responseRow.charAt(1) == ':') { if (responseRow.charAt(0) == '0') { responseRowMerged = ""; } responseRowMerged += responseRow.substring(2); } return true; } /** Parse merged row (after merge completed) */ bool parseRowMerged() { Serial.print("merged:"); Serial.println(responseRowMerged); if (commandRequest.equals("2101")) { params.speedKmh = hexToDec(responseRowMerged.substring(32, 36).c_str(), 2, false) * 0.0155; // / 100.0 *1.609 = real to gps is 1.750 } if (commandRequest.equals("2102")) { params.auxPerc = hexToDec(responseRowMerged.substring(50, 52).c_str(), 1, false); // === OK Valid params.auxCurrentAmp = - hexToDec(responseRowMerged.substring(46, 50).c_str(), 2, true) / 1000.0; } if (commandRequest.equals("220100")) { params.indoorTemperature = (hexToDec(responseRowMerged.substring(16, 18).c_str(), 1, false) / 2) - 40; // === OK Valid params.outdoorTemperature = (hexToDec(responseRowMerged.substring(18, 20).c_str(), 1, false) / 2) - 40; // === OK Valid } if (commandRequest.equals("220101")) { params.cumulativeEnergyChargedKWh = float(strtol(responseRowMerged.substring(82, 90).c_str(), 0, 16)) / 10.0; if (params.cumulativeEnergyChargedKWhStart == -1) params.cumulativeEnergyChargedKWhStart = params.cumulativeEnergyChargedKWh; params.cumulativeEnergyDischargedKWh = float(strtol(responseRowMerged.substring(90, 98).c_str(), 0, 16)) / 10.0; if (params.cumulativeEnergyDischargedKWhStart == -1) params.cumulativeEnergyDischargedKWhStart = params.cumulativeEnergyDischargedKWh; params.auxVoltage = hexToDec(responseRowMerged.substring(64, 66).c_str(), 2, true) / 10.0; params.batPowerAmp = - hexToDec(responseRowMerged.substring(26, 30).c_str(), 2, true) / 10.0; params.batVoltage = hexToDec(responseRowMerged.substring(30, 34).c_str(), 2, false) / 10.0; // === OK Valid params.batPowerKw = (params.batPowerAmp * params.batVoltage) / 1000.0; params.batPowerKwh100 = params.batPowerKw / params.speedKmh * 100; params.batCellMaxV = hexToDec(responseRowMerged.substring(52, 54).c_str(), 1, false) / 50.0; // === OK Valid params.batCellMinV = hexToDec(responseRowMerged.substring(56, 58).c_str(), 1, false) / 50.0; // === OK Valid params.batModule01TempC = hexToDec(responseRowMerged.substring(38, 40).c_str(), 1, true); params.batModule02TempC = hexToDec(responseRowMerged.substring(40, 42).c_str(), 1, true); params.batModule03TempC = hexToDec(responseRowMerged.substring(42, 44).c_str(), 1, true); params.batModule04TempC = hexToDec(responseRowMerged.substring(44, 46).c_str(), 1, true); //params.batTempC = hexToDec(responseRowMerged.substring(36, 38).c_str(), 1, true); //params.batMaxC = hexToDec(responseRowMerged.substring(34, 36).c_str(), 1, true); //params.batMinC = hexToDec(responseRowMerged.substring(36, 38).c_str(), 1, true); // This is more accurate than min/max from BMS. It's required to detect kona/eniro cold gates (min 15C is needed > 43kW charging, min 25C is needed > 58kW charging) params.batMinC = params.batMaxC = params.batModule01TempC; params.batMinC = (params.batModule02TempC < params.batMinC) ? params.batModule02TempC : params.batMinC ; params.batMinC = (params.batModule03TempC < params.batMinC) ? params.batModule03TempC : params.batMinC ; params.batMinC = (params.batModule04TempC < params.batMinC) ? params.batModule04TempC : params.batMinC ; params.batMaxC = (params.batModule02TempC > params.batMaxC) ? params.batModule02TempC : params.batMaxC ; params.batMaxC = (params.batModule03TempC > params.batMaxC) ? params.batModule03TempC : params.batMaxC ; params.batMaxC = (params.batModule04TempC > params.batMaxC) ? params.batModule04TempC : params.batMaxC ; params.batTempC = params.batMinC; params.batInletC = hexToDec(responseRowMerged.substring(50, 52).c_str(), 1, true); if (params.speedKmh < 15 && params.batPowerKw >= 1 && params.socPerc > 0 && params.socPerc <= 100) { params.chargingGraphKw[int(params.socPerc)] = params.batPowerKw; params.chargingGraphMinTempC[int(params.socPerc)] = params.batMinC; params.chargingGraphMaxTempC[int(params.socPerc)] = params.batMaxC; } } if (commandRequest.equals("220102")) { for (int i = 0; i < 32; i++) { params.cellVoltage[i] = hexToDec(responseRowMerged.substring(14 + (i * 2), 14 + (i * 2) + 2).c_str(), 1, false) / 50; } } if (commandRequest.equals("220103")) { for (int i = 0; i < 32; i++) { params.cellVoltage[32 + i] = hexToDec(responseRowMerged.substring(14 + (i * 2), 14 + (i * 2) + 2).c_str(), 1, false) / 50; } } if (commandRequest.equals("220104")) { for (int i = 0; i < 32; i++) { params.cellVoltage[64 + i] = hexToDec(responseRowMerged.substring(14 + (i * 2), 14 + (i * 2) + 2).c_str(), 1, false) / 50; } } if (commandRequest.equals("220105")) { params.sohPerc = hexToDec(responseRowMerged.substring(56, 60).c_str(), 2, false) / 10.0; params.socPerc = hexToDec(responseRowMerged.substring(68, 70).c_str(), 1, false) / 2.0; params.batHeaterC = hexToDec(responseRowMerged.substring(52, 54).c_str(), 1, true); // for (int i = 30; i < 32; i++) { // ai/aj position params.cellVoltage[96 - 30 + i] = hexToDec(responseRowMerged.substring(14 + (i * 2), 14 + (i * 2) + 2).c_str(), 1, false) / 50; } } if (commandRequest.equals("22c00b")) { params.tireFrontLeftPressureBar = hexToDec(responseRowMerged.substring(14, 16).c_str(), 2, false) / 72.51886900361; // === OK Valid *0.2 / 14.503773800722 params.tireFrontRightPressureBar = hexToDec(responseRowMerged.substring(22, 24).c_str(), 2, false) / 72.51886900361; // === OK Valid *0.2 / 14.503773800722 params.tireRearLeftPressureBar = hexToDec(responseRowMerged.substring(30, 32).c_str(), 2, false) / 72.51886900361; // === OK Valid *0.2 / 14.503773800722 params.tireRearRightPressureBar = hexToDec(responseRowMerged.substring(38, 40).c_str(), 2, false) / 72.51886900361; // === OK Valid *0.2 / 14.503773800722 params.tireRearLeftTempC = hexToDec(responseRowMerged.substring(16, 18).c_str(), 2, false) - 50; // === OK Valid params.tireRearRightTempC = hexToDec(responseRowMerged.substring(24, 26).c_str(), 2, false) - 50; // === OK Valid params.tireFrontLeftTempC = hexToDec(responseRowMerged.substring(32, 34).c_str(), 2, false) - 50; // === OK Valid params.tireFrontRightTempC = hexToDec(responseRowMerged.substring(40, 42).c_str(), 2, false) - 50; // === OK Valid } return true; } /** Parse test data */ bool testData() { redrawScreen(true); // 2101 commandRequest = "2101"; responseRowMerged = "6101FFF8000009285A3B0648030000B4179D763404080805000000"; parseRowMerged(); // 2102 commandRequest = "2102"; responseRowMerged = "6102F8FFFC000101000000840FBF83BD33270680953033757F59291C76000001010100000007000000"; responseRowMerged = "6102F8FFFC000101000000931CC77F4C39040BE09BA7385D8158832175000001010100000007000000"; parseRowMerged(); // 2106 commandRequest = "2106"; responseRowMerged = "6106FFFF800000000000000200001B001C001C000600060006000E000000010000000000000000013D013D013E013E00"; parseRowMerged(); // 220100 commandRequest = "220100"; responseRowMerged = "6201007E5027C8FF7F765D05B95AFFFF5AFF11FFFFFFFFFFFF6AFFFF2DF0757630FFFF00FFFF000000"; responseRowMerged = "6201007E5027C8FF867C58121010FFFF10FF8EFFFFFFFFFFFF10FFFF0DF0617900FFFF01FFFF000000"; parseRowMerged(); // 220101 commandRequest = "220101"; responseRowMerged = "620101FFF7E7FF99000000000300B10EFE120F11100F12000018C438C30B00008400003864000035850000153A00001374000647010D017F0BDA0BDA03E8"; responseRowMerged = "620101FFF7E7FFB3000000000300120F9B111011101011000014CC38CB3B00009100003A510000367C000015FB000013D3000690250D018E0000000003E8"; parseRowMerged(); // 220102 commandRequest = "220102"; responseRowMerged = "620102FFFFFFFFCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBAAAA"; parseRowMerged(); // 220103 commandRequest = "220103"; responseRowMerged = "620103FFFFFFFFCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCACBCACACFCCCBCBCBCBCBCBCBCBAAAA"; parseRowMerged(); // 220104 commandRequest = "220104"; responseRowMerged = "620104FFFFFFFFCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBCBAAAA"; parseRowMerged(); // 220105 commandRequest = "220105"; responseRowMerged = "620105003fff9000000000000000000F8A86012B4946500101500DAC03E800000000AC0000C7C701000F00000000AAAA"; responseRowMerged = "620105003FFF90000000000000000014918E012927465000015013BB03E800000000BB0000CBCB01001300000000AAAA"; parseRowMerged(); // 220106 commandRequest = "220106"; responseRowMerged = "620106FFFFFFFF14001A00240000003A7C86B4B30000000928EA00"; parseRowMerged(); // 22c002 commandRequest = "22c002"; responseRowMerged = "62C002FFFF0000D2E84E93D2E84EBBD2DBDACBD2E149F3AAAAAAAA"; parseRowMerged(); // 22c00b commandRequest = "22c00b"; responseRowMerged = "62C00BFFFF0000B93D0100B43E0100B43D0100BB3C0100AAAAAAAA"; parseRowMerged(); params.batModule01TempC = 28; params.batModule02TempC = 29; params.batModule03TempC = 28; params.batModule04TempC = 30; //params.batTempC = hexToDec(responseRowMerged.substring(36, 38).c_str(), 1, true); //params.batMaxC = hexToDec(responseRowMerged.substring(34, 36).c_str(), 1, true); //params.batMinC = hexToDec(responseRowMerged.substring(36, 38).c_str(), 1, true); // This is more accurate than min/max from BMS. It's required to detect kona/eniro cold gates (min 15C is needed > 43kW charging, min 25C is needed > 58kW charging) params.batMinC = params.batMaxC = params.batModule01TempC; params.batMinC = (params.batModule02TempC < params.batMinC) ? params.batModule02TempC : params.batMinC ; params.batMinC = (params.batModule03TempC < params.batMinC) ? params.batModule03TempC : params.batMinC ; params.batMinC = (params.batModule04TempC < params.batMinC) ? params.batModule04TempC : params.batMinC ; params.batMaxC = (params.batModule02TempC > params.batMaxC) ? params.batModule02TempC : params.batMaxC ; params.batMaxC = (params.batModule03TempC > params.batMaxC) ? params.batModule03TempC : params.batMaxC ; params.batMaxC = (params.batModule04TempC > params.batMaxC) ? params.batModule04TempC : params.batMaxC ; params.batTempC = params.batMinC; redrawScreen(false); return true; } /** BLE callbacks */ class MyClientCallback : public BLEClientCallbacks { /** On connect */ void onConnect(BLEClient* pclient) { Serial.println("onConnect"); } /** On disconnect */ void onDisconnect(BLEClient* pclient) { //connected = false; Serial.println("onDisconnect"); } }; /** Scan for BLE servers and find the first one that advertises the service we are looking for. */ class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { /** Called for each advertising BLE server. */ void onResult(BLEAdvertisedDevice advertisedDevice) { Serial.print("BLE advertised device found: "); Serial.println(advertisedDevice.toString().c_str()); if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(BLEUUID(settings.serviceUUID))) { Serial.println("Stop scanning. Found my BLE device."); BLEDevice::getScan()->stop(); foundMyBleDevice = new BLEAdvertisedDevice(advertisedDevice); } } }; /** BLE Security */ class MySecurity : public BLESecurityCallbacks { uint32_t onPassKeyRequest() { Serial.printf("Pairing password: %d \r\n", PIN); return PIN; } void onPassKeyNotify(uint32_t pass_key) { Serial.printf("onPassKeyNotify\r\n"); } bool onConfirmPIN(uint32_t pass_key) { Serial.printf("onConfirmPIN\r\n"); return true; } bool onSecurityRequest() { Serial.printf("onSecurityRequest\r\n"); return true; } void onAuthenticationComplete(esp_ble_auth_cmpl_t auth_cmpl) { if (auth_cmpl.success) { Serial.printf("onAuthenticationComplete\r\n"); } else { Serial.println("Auth failure. Incorrect PIN?"); bleConnect = false; } } }; /** Ble notification callback */ static void notifyCallback (BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { char ch; // Parse multi line response to single lines responseRow = ""; for (int i = 0; i <= length; i++) { ch = pData[i]; if (ch == '\r' || ch == '\n' || ch == '\0') { if (responseRow != "") parseRow(); responseRow = ""; } else { responseRow += ch; if (responseRow == ">") { if (responseRowMerged != "") { parseRowMerged(); } responseRowMerged = ""; canSendNextAtCommand = true; } } } } /** Do connect BLE with server (OBD device) */ bool connectToServer(BLEAddress pAddress) { Serial.print("bleConnect "); Serial.println(pAddress.toString().c_str()); BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT); BLEDevice::setSecurityCallbacks(new MySecurity()); BLESecurity *pSecurity = new BLESecurity(); pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND); // pSecurity->setCapability(ESP_IO_CAP_KBDISP); pSecurity->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK); pClient = BLEDevice::createClient(); pClient->setClientCallbacks(new MyClientCallback()); if ( pClient->connect(pAddress, BLE_ADDR_TYPE_RANDOM) ) Serial.println("bleConnected"); Serial.println(" - bleConnected to server"); // Remote service BLERemoteService* pRemoteService = pClient->getService(BLEUUID(settings.serviceUUID)); if (pRemoteService == nullptr) { Serial.print("Failed to find our service UUID: "); Serial.println(settings.serviceUUID); return false; } Serial.println(" - Found our service"); // Get characteristics pRemoteCharacteristic = pRemoteService->getCharacteristic(BLEUUID(settings.charTxUUID)); if (pRemoteCharacteristic == nullptr) { Serial.print("Failed to find our characteristic UUID: "); Serial.println(settings.charTxUUID);//.toString().c_str()); return false; } Serial.println(" - Found our characteristic"); // Get characteristics pRemoteCharacteristicWrite = pRemoteService->getCharacteristic(BLEUUID(settings.charRxUUID)); if (pRemoteCharacteristicWrite == nullptr) { Serial.print("Failed to find our characteristic UUID: "); Serial.println(settings.charRxUUID);//.toString().c_str()); return false; } Serial.println(" - Found our characteristic write"); // Read the value of the characteristic. if (pRemoteCharacteristic->canNotify()) { Serial.println(" - canNotify"); //pRemoteCharacteristic->registerForNotify(notifyCallback); if (pRemoteCharacteristic->canIndicate()) { Serial.println(" - canIndicate"); const uint8_t indicationOn[] = {0x2, 0x0}; //const uint8_t indicationOff[] = {0x0,0x0}; pRemoteCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)indicationOn, 2, true); //pRemoteCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notifyOff,2,true); pRemoteCharacteristic->registerForNotify(notifyCallback, false); delay(200); } } if (pRemoteCharacteristicWrite->canWrite()) { Serial.println(" - canWrite"); } return true; } /** Start ble scan */ bool startBleScan() { foundMyBleDevice = NULL; int32_t posx, posy; // Print message tft.fillScreen(TFT_BLACK); tft.setTextDatum(ML_DATUM); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setFreeFont(&Roboto_Thin_24); tft.setTextDatum(BL_DATUM); tft.drawString(" Searching BLE device... ", 0, 240 / 2, GFXFF); // Start scanning Serial.println("Scanning BLE devices..."); BLEScanResults foundDevices = pBLEScan->start(5, false); Serial.print("Devices found: "); Serial.println(foundDevices.getCount()); Serial.println("Scan done!"); pBLEScan->clearResults(); // delete results fromBLEScan buffer to release memory // Redraw screen redrawScreen(true); return true; } /** Setup device */ void setup(void) { // Serial console, init structures Serial.begin(115200); Serial.println(""); Serial.println("Booting device..."); initStructure(); initSettings(); // Set button pins for input pinMode(BUTTON_MIDDLE, INPUT); pinMode(BUTTON_LEFT, INPUT); pinMode(BUTTON_RIGHT, INPUT); // Init display Serial.println("Init TFT display"); tft.begin(); tft.setRotation(3 ); tft.fillScreen(TFT_BLACK); redrawScreen(true); // Show test data on right button during boot device if (digitalRead(BUTTON_RIGHT) == LOW) { displayScreen = 1; testData(); } // Start BLE connection line = ""; Serial.println("Start BLE with PIN auth"); BLEDevice::init(""); // Retrieve a Scanner and set the callback we want to use to be informed when we have detected a new device. // Specify that we want active scanning and start the scan to run for 5 seconds. Serial.println("Setup BLE scan"); pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan->setInterval(1349); pBLEScan->setWindow(449); pBLEScan->setActiveScan(true); startBleScan(); // End Serial.println("Device setup completed"); } /** Loop */ void loop() { //Serial.println("Loop"); // Connect BLE device if (bleConnect == true && foundMyBleDevice) { pServerAddress = new BLEAddress(settings.obdMacAddress); if (connectToServer(*pServerAddress)) { bleConnected = true; bleConnect = false; Serial.println("We are now connected to the BLE device."); redrawScreen(true); // Serve first command (ATZ) doNextAtCommand(); } else { Serial.println("We have failed to connect to the server; there is nothing more we will do."); } } // Send line from TTY to OBD (custom command) if (bleConnected) { if (Serial.available()) { ch = Serial.read(); line = line + ch; if (ch == '\r' || ch == '\n') { Serial.print("Sending line: "); Serial.println(line); pRemoteCharacteristicWrite->writeValue(line.c_str(), line.length()); line = ""; } } // Can send next command from queue to OBD if (canSendNextAtCommand) { canSendNextAtCommand = false; doNextAtCommand(); } } // Handle buttons (under construction) LOW - pressed, HIGH - not pressed if (digitalRead(BUTTON_MIDDLE) == HIGH) { btnMiddlePressed = false; } else { if (!btnMiddlePressed) { btnMiddlePressed = true; // doAction } } if (digitalRead(BUTTON_LEFT) == HIGH) { btnLeftPressed = false; } else { if (!btnLeftPressed) { btnLeftPressed = true; displayScreen++; if (displayScreen > displayScreenCount) displayScreen = 0; // rotate screens // Turn off display on screen 0 digitalWrite(TFT_BL, (displayScreen == 0) ? LOW : HIGH); redrawScreen(true); } } if (digitalRead(BUTTON_RIGHT) == HIGH) { btnRightPressed = false; } else { if (!btnRightPressed) { btnRightPressed = true; // doAction } } // 1ms delay delay(1); }