According to the research firm IHS Markit, "The average age of light vehicles in operation (VIO) in the U.S. has risen to 12.1 years this year, increasing by nearly 2 months during 2020". If you're following my projects, you'd know that I drive an older truck (probably the one that skewed the curve).
Have you seen car prices lately? Now more than ever, I must keep this truck running. I need to be ready for my check engine light to come on. Like Andreas Spiess said, "It can happen to you, too". The only problem is OBD-II CAN-BUS Development Kit is Out Of Stock.
Upon further research we found ELM 327 V1.5 with Bluetooth connectivity. The idea here is to connect Wio Terminal wirelessly to ELM 327 which will allow us to connect to Engine Control Unit (ECU). We are interested in five parameters:
- Battery voltage
- Engine's Revolutions Per Minute (RPM)
- Vehicle's speed
- Intake air temperature
- Engine coolant temperature
The first one can be obtained by connecting to ELM 327 only, but the remaining parameters need a connection to ECU which can happen on my truck only when the ignition is on.
This project will focus on explaining the code more than other similar projects. Two words of caution before starting:
- Don't use this project while driving until you find out how your car will react. My truck doesn't mind having something plugged in OBD2 port, but other cars might.
- I had to modify BLERemoteCharacteristic.cpp by replacing 'm_semaphoreRegForNotifyEvt.wait("registerForNotify");' with 'm_semaphoreRegForNotifyEvt.timedWait("registerForNotify", 1000);'. There is probably a better way to avoid getting stuck at this line of the library.
Finally, this project builds on great documentation at:
https://wiki.seeedstudio.com/Wio-Terminal-Bluetooth-Overview/
So, make sure you visit that page first.
We start setup by getting buttons ready to use.
pinMode(WIO_KEY_A, INPUT_PULLUP);
pinMode(WIO_KEY_B, INPUT_PULLUP);
pinMode(WIO_KEY_C, INPUT_PULLUP);
Next, we setup display.
tft.init();
tft.setTextWrap(false);
tft.setTextSize(7);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_BLUE);
tft.setCursor(40, 100);
tft.print("OBD2");
Then play a tone on start.
for (int i = 1000; i <= 3000; i += 500)
{
tone(WIO_BUZZER, i);
delay(50);
noTone(WIO_BUZZER);
delay(50);
}
Here we start scanning for Bluetooth devices. We referance a callback function to receive data from the selected device. The selected device must have a Service, Read Characteristic, and Write Characteristic with Universally Unique Identifiers (UUIDs) defined here.
BLEDevice::init("");
BLEScan *ThisBLEScan = BLEDevice::getScan();
ThisBLEScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCB());
ThisBLEScan->setActiveScan(true);
ThisBLEScan->start(20);
BLEClient *ThisClient = BLEDevice::createClient();
ThisClient->connect(OBD2Device);
ThisService = ThisClient->getService((BLEUUID)0xfff0);
NotifyCharacteristic = ThisService->getCharacteristic((BLEUUID)0xfff1);
NotifyCharacteristic->registerForNotify(NotifyCB);
WriteCharacteristic = ThisService->getCharacteristic((BLEUUID)0xfff2);
Now we're connected to ELM 327. We use AT commands to reset the device, turn echo off, and auto select protocol. Next, we send Parameter ID (PID) to make sure we're connected to ECU.
WriteCharacteristic->writeValue("AT Z\r");
WriteCharacteristic->writeValue("AT E0\r");
WriteCharacteristic->writeValue("AT SP 0\r");
WriteCharacteristic->writeValue("01 00\r");
while (NewString.substring(0, 12) != "SEARCHING...")
yield();
while (NewString.length() > 0)
{
if (NewString.substring(0, 17) == "UNABLE TO CONNECT")
{
ItemsInArray = 1;
NewString = "";
}
tft.fillScreen(TFT_BLACK);
tft.setCursor(40, 100);
tft.print("OBD2");
delay(10);
}
NewScreen = ItemsInArray;
WriteCharacteristic->writeValue("AT I\r");
while (NewString == "")
yield();
tft.fillScreen(TFT_BLACK);
Last thing we need to do in setup is attaching interrupts to buttons.
attachInterrupt(digitalPinToInterrupt(WIO_KEY_A), ScreenUp, FALLING);
attachInterrupt(digitalPinToInterrupt(WIO_KEY_B), ScreenHome, FALLING);
attachInterrupt(digitalPinToInterrupt(WIO_KEY_C), ScreenDown, FALLING);
We have to determine in the loop whether we're showing all parameters or just one.
tft.setCursor(10, -30);
for (I = 0; I < ItemsInArray; I++)
{
if (OldScreen != NewScreen)
{
tft.fillScreen(TFT_BLACK);
OldScreen = NewScreen;
break;
}
if (NewScreen != ItemsInArray && NewScreen != I)
continue;
NewString = "";
WriteCharacteristic->writeValue(CommandArray[I]);
while (NewString == "")
yield();
}
At the bottom of each screen we will show time since start in seconds.
tft.setTextSize(3);
tft.fillRect(0, 280, 240, 40, TFT_BLACK);
tft.setCursor(10, 280);
tft.setTextColor(TFT_BLUE);
tft.printf("%d", int(millis() / 1000));
For this code to work we need to define one function for each button.
void ScreenUp(void)
{
NewScreen--;
NewScreen = constrain(NewScreen, 0, ItemsInArray);
tone(WIO_BUZZER, 2500, 100);
}
void ScreenHome(void)
{
NewScreen = ItemsInArray;
tone(WIO_BUZZER, 2500, 100);
}
void ScreenDown(void)
{
NewScreen++;
NewScreen = constrain(NewScreen, 0, ItemsInArray);
tone(WIO_BUZZER, 2500, 100);
}
Earlier we scanned for Bluetooth devices. Similar projects use device MAC address to select one specific ELM 327, but we want our Wio Terminal to work without having to hard code any device specific value. Since it was not easy to get device name, we used other device properties that should be common to all ELM 327.
class AdvertisedDeviceCB : public BLEAdvertisedDeviceCallbacks
{
void onResult(BLEAdvertisedDevice advertisedDevice)
{
if (advertisedDevice.haveServiceUUID() && !advertisedDevice.getAddressType() && advertisedDevice.getAppearance() == 576 && advertisedDevice.getServiceUUID().length() == 2)
{
BLEDevice::getScan()->stop();
OBD2Device = new BLEAdvertisedDevice(advertisedDevice);
}
}
};
Last but not least, the function that displays values when a response has been received.
static void NotifyCB(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify)
{
NewString = (char *)pData;
NewString = NewString.substring(0, max(NewString.indexOf(">"), 0));
NewString.trim();
if (!NewString.length() || I == ItemsInArray)
return;
else
{
if (NewString.substring(0, 2) == "41")
{
unsigned long UnsignedLong = strtoul(NewString.substring(3, 5).c_str(), NULL, 16);
NewString = NewString.substring(6, NewString.length());
NewString.replace(" ", "");
switch (UnsignedLong)
{
case 0x0C:
UnsignedLong = strtoul(NewString.c_str(), NULL, 16) / 4;
break;
case 0x0D:
UnsignedLong = strtoul(NewString.c_str(), NULL, 16) * 0.621371;
break;
case 0x05:
case 0x0F:
UnsignedLong = (strtoul(NewString.c_str(), NULL, 16) - 40) * 9 / 5 + 32;
break;
default:
UnsignedLong = strtoul(NewString.c_str(), NULL, 16);
break;
}
NewString = String(UnsignedLong);
}
if (NewScreen == ItemsInArray)
{
tft.setTextSize(3);
tft.fillRect(0, tft.getCursorY() + 40, 240, 40, TFT_BLACK);
tft.setCursor(10, tft.getCursorY() + 40);
tft.setTextColor(TFT_GREEN);
tft.print(LabelArray[I]);
tft.setTextColor(TFT_RED);
tft.printf(" %s", NewString.c_str());
}
else
{
tft.setTextSize(7);
tft.setCursor(10, 10);
tft.setTextColor(TFT_GREEN);
tft.print(LabelArray[I]);
tft.fillRect(0, 100, 240, 50, TFT_BLACK);
tft.setCursor(10, 100);
tft.setTextColor(TFT_RED);
tft.printf("%s", NewString.c_str());
OldString = NewString;
}
delay(10);
}
}
Comments