Getting started with vehicle programming can be an exciting journey, and one of the best ways to begin is by using an ELM327 device. This versatile tool allows you to communicate with a wide range of vehicles manufactured from 1998 onwards. Once you've mastered the basics of ELM327, you can explore more advanced methods of vehicle communication, such as Controller Area Network (CAN). These devices enable you to extract a wealth of information from any compatible car. In this tutorial, I'll guide you through the process of developing a simple scan tool using an ELM327 microcontroller. Our goal is to check if our car will pass emissions testing, saving us a potentially wasted trip to the testing facility. For this project, we'll be using C# and .NET, as they provide a great balance between abstraction and hardware device handling. To get started, you'll need a wired ELM327 device, which can be easily purchased from online marketplaces like Amazon. While it's possible to adapt the code to work with Bluetooth, it's important to consider your target platform, as Bluetooth code is often not compatible across different systems. Most of the code will remain the same, but you'll need to make some modifications to accommodate Bluetooth functionality. This can be more challenging than it initially appears, which is why we've chosen to focus on the wired version in this tutorial.
Read more...
Step 1: Creating the ICommunicator Interface
To ensure flexibility and adaptability in our code, we'll start by creating an interface named ICommunicator
. This interface defines the common methods and properties required for communicating with our physical device.
By defining this interface, we can create different implementations of communication protocols while maintaining a consistent interface for interacting with the device. This promotes code reusability and allows for easy substitution of communication methods in the future.
public interface ICommunicator : IDisposable
{
Task ConnectAsync();
void Disconnect();
bool IsConnected { get; }
void Write(string command);
string ReadString();
}
Step 2: Implementing the SerialCommunicator Class
To establish a connection with the ELM327 device, we need to create an RS232 serial connection using the SerialPort
class provided by C#. We will encapsulate this functionality within a class called SerialCommunicator
, which implements the ICommunicator
interface.
This class provides methods for connecting, disconnecting, writing, and reading data from the serial port. It also handles proper disposal of resources when the connection is no longer needed.
public class SerialCommunicator : ICommunicator
{
public SerialPort Port { get; private set; }
public SerialCommunicator(string portName, int baudRate, Parity parity, int dataBits)
{
Port = new SerialPort(portName, baudRate, parity, dataBits);
}
public async Task ConnectAsync()
{
try
{
if (!Port.IsOpen)
{
await Task.Run(() => Port.Open());
}
}
catch (Exception ex)
{
Console.WriteLine("Error: Unable to establish a connection with serial port.");
Console.WriteLine(ex.Message);
}
}
public void Disconnect()
{
if (Port.IsOpen)
{
Port.Close();
}
}
public void Write(string message)
{
Port.DiscardInBuffer();
Port.DiscardOutBuffer();
Port.Write(message);
}
public string ReadString()
{
return Port.ReadExisting();
}
public bool IsConnected
{
get
{
return Port.IsOpen;
}
}
public void Dispose()
{
if (Port.IsOpen)
{
Port.Close();
Port.Dispose();
}
}
public Task ReadDesiredStringResponse(Func desiredResultFunc, int timeoutDuration)
{
throw new NotImplementedException();
}
}
Step 3: Creating the ELM327Controller Class
To facilitate communication with the ELM327 device, we will create a dedicated class called ELM327Controller
. This class will handle sending commands to the device and receiving responses from it.
It utilizes regular expressions to clean up and process the responses received from the ELM device, ensuring a consistent and reliable format for the data we work with. The ELM327Controller
also includes a DebugMode
flag for debugging and troubleshooting purposes.
using System.Text.RegularExpressions;
public static class ELM327Controller
{
public static bool DebugMode { get; set; } = false;
public static void Write(ICommunicator communication, string message)
{
if (DebugMode)
Console.WriteLine("Sent: " + message);
try
{
communication.Write(message + "\r");
}
catch (Exception e)
{
Console.WriteLine("Exception occurred during write: " + e.ToString());
}
}
public static string ReadString(ICommunicator communication)
{
string response = communication.ReadString().Trim();
response = Regex.Replace(response, ">", "");
response = Regex.Replace(response, "\r", "");
if (DebugMode && !string.IsNullOrEmpty(response))
Console.WriteLine("Received: " + response);
return response;
}
public static async Task ReadDesiredStringResponse(ICommunicator communication, Func desiredResultFunc, int timeoutDuration)
{
string response = "";
int timeout = timeoutDuration;
while (!desiredResultFunc(response) && timeout > 0)
{
response = ELM327Controller.ReadString(communication);
timeout -= 10;
await Task.Delay(10);
}
if (DebugMode && timeout <= 0)
Console.WriteLine("Timeout reached before desired response was received.");
if (timeout <= 0)
throw new TimeoutException("Desired response not received within the specified timeout.");
return response;
}
}
Step 4: Defining the ICommand Interface and Command Class
To send commands to our ELM327 controller, we'll define an interface called ICommand
and a class named Command
that implements this interface. The ICommand
interface defines the basic structure of a command, including the command identifier and the Execute
method.
The Command
class provides the implementation for executing commands, handling response validation, and timeout management.
public interface ICommand
{
string pid { get; }
Task Execute(ICommunicator communication);
}
public class Command : ICommand
{
public string pid { get; }
private readonly Func validator;
private readonly int timeout;
public Command(string pid, Func validator, int timeout)
{
this.pid = pid;
this.validator = validator;
this.timeout = timeout;
}
public async Task Execute(ICommunicator communication)
{
return await SendCommand(communication, pid, validator);
}
private async Task SendCommand(ICommunicator communication, string pid, Func responseValidator)
{
ELM327Controller.Write(communication, pid);
string response = "";
try
{
response = await ELM327Controller.ReadDesiredStringResponse(communication, responseValidator, timeout);
if (!responseValidator(response))
{
throw new Exception("Failed to retrieve data.");
}
}
catch (Exception e)
{
Console.WriteLine("Error: " + e.Message);
throw;
}
return response;
}
}
Step 5: Creating Specific Commands
With the Command
class in place, we can now create specific commands for interacting with the ELM327 device. In this example, we create two commands: one for setting the vehicle's protocol and another for retrieving emission codes.
These commands are implemented as static methods that return instances of the Command
class with the appropriate parameters.
public static Command CreateSetProtocolToAutoCommand()
{
return new Command("ATSP0", response => response == "ATSP0OK", 10000);
}
public static Command CreateImReadinessCommand()
{
return new Command("0101", response => response.StartsWith("41 01") && response.Length >= 17, 10000);
}
Step 6: Interpreting Emission Codes
After receiving the emission codes from the ELM327 device, we need a way to interpret the data. We create an Emissions
class that provides a method called RetrieveEmissionReadinessData
.
This method takes the response from the ELM device, decodes the relevant bytes, and returns a dictionary containing the emission test results for each component, indicating whether they are supported and complete.
public static class Emissions
{
public static Dictionary RetrieveEmissionReadinessData(string response)
{
string[] parts = response.Split(' ');
byte thirdByte = Convert.ToByte(parts[3], 16);
byte fourthByte = Convert.ToByte(parts[4], 16);
byte fifthByte = Convert.ToByte(parts[5], 16);
int bitValue = (thirdByte >> 2) & 1;
var engineType = bitValue == 1 ? "Spark-ignited" : "Compression-ignited";
var readinessDict = new Dictionary();
string[] emissionTestsSpark = new string[] { "Catalyst", "Heated Catalyst", "Evaporative System", "Secondary Air System",
"Gasoline Particulate Filter", "Oxygen Sensor", "Oxygen Sensor Heater", "EGR and/or VVT System" };
string[] emissionTestsDiesel = new string[] { "NMHC Catalyst", "NOx/SCR Monitor", "Reserved", "Boost Pressure", "Reserved",
"Exhaust Gas Sensor", "PM filter monitoring", "EGR and/or VVT System" };
if (engineType == "Spark-ignited")
{
for (int i = 0; i < 8; i++)
{
var testAbility = ((fourthByte >> i) & 1) == 1 ? "Supported" : "Not supported";
var testCompleteness = ((fifthByte >> i) & 1) == 1 ? "Complete" : "Not complete";
readinessDict.Add(emissionTestsSpark[i], $"{testAbility}, {testCompleteness}");
}
}
else if (engineType == "Compression-ignited")
{
for (int i = 0; i < 8; i++)
{
var testAbility = ((fourthByte >> i) & 1) == 1 ? "Supported" : "Not supported";
var testCompleteness = ((fifthByte >> i) & 1) == 1 ? "Complete" : "Not complete";
readinessDict.Add(emissionTestsDiesel[i], $"{testAbility}, {testCompleteness}");
}
}
return readinessDict;
}
}
Step 7: Putting It All Together
Finally, we write the driving code that executes everything we have written. We create an instance of the SerialCommunicator
, connect to the ELM327 device, execute the specific commands, retrieve the emission readiness data, and display the results.
After this, you have a fully functional emission scan tool for your car. Feel free to play around more and challenge yourself with real-world problems, such as reading vehicle fault codes and creating a system for looking up their names and descriptions. Hint: You can use the same Command to get the Engine DTC codes
Command("0101", response => response.StartsWith("41 01") && response.Length >= 17, 10000);
using (var serial = new SerialCommunicator("/dev/tty.usbserial-113010763101", 115200, Parity.None, 8))
{
await serial.ConnectAsync();
ELM327Controller.DebugMode = true;
ICommand setProtocol = CreateSetProtocolToAutoCommand();
await setProtocol.Execute(serial);
ICommand ImReadinessCommand = CreateFaultsAndImReadinessCommand();
string data = await ImReadinessCommand.Execute(serial);
var readinessData = Emissions.RetrieveEmissionReadinessData(data);
foreach (var pair in readinessData)
{
Console.WriteLine($"Monitor: {pair.Key}, Ready: {pair.Value}");
}
serial.Disconnect();
}
Enjoy!