First, we need a short review of modern car electronics. Things have certainly moved on from my first car, which was a 1971 Mini Clubman. This didn't even have electronics in it (unless you count the radio), as everything was electro-mechanical (anyone remember setting the gap for the points on the distributor?) Today, in Europe at least, things like anti-lock brakes (ABS) and stability control (ESC) which require complex sensors and electronics are mandated by law. Also, since 2001, all petrol driven vehicles have to be fitted with an EOBD (European On-Board Diagnostics) interface. This conforms to the OBD-II standard which is where the ELM327 interface from my first blog entry comes in.
As a standard, OBD-II mandates some parts while other parts are optional. That way certain basic facilities are guaranteed to be present (mainly those that are related to the measuring of exhaust emission performance) and then each car manufacturer can implement the optional parts that make sense for the vehicle they're building.
There are five signal protocols that can be used with the OBD-II interface:
- SAEJ1850 PWM (Pulse-width modulation, used by Ford)
- SAE J1850 VPW (Variable pulse-width, used by General Motors)
- ISO 9141-2 (which is a bit like RS-232)
- ISO 14230
- ISO 15765 (also referred to as Controller Area Network, or CAN bus)
For my current vehicle, which is an Audi S3, the protocol is ISO 15765 as the car has multiple CAN buses for communication between the various control units (we'll come back to this in more detail later).
So where to start?
The first thing that is necessary is to establish communication between a Java application and the ELM327. One of the great things about using Java for an application like this is that the development can easily be done on a laptop and the production code moved easily to the target hardware. No cross compilation tool chains needed here, thank you.
My ELM327 interface communicates via 802.11 (Wi-Fi). The address of my interface is 192.168.0.11 (which seems pretty common for these devices) and uses port 35000 for all communication. To test that things are working I set my MacBook to use a static IP address on Wi-Fi and then connected directly to the ELM327 which appeared in the list of available Wi-Fi devices. Having established communication at the IP level I could then telnet into the ELM327. If you want to start playing with this it's best to get hold of the documentation, which is really well written and complete. The ELM327 essentially uses two modes of communication:
- AT commands for talking to the interface itself
- OBD commands that conform to the description above. The
ELM327 does all the hard work of converting this to the necessary
packet format, adding headers, checksums and so on as well as
unmarshalling the response data.
To keep things simple I wrote a class that would encapsulate the connection to the ELM327. Here's the code that initialises the connection so that we can read and write bytes, as required
/* Copyright © 2013, Oracle and/or its affiliates. All rights
reserved. */
private static final String ELM327_IP_ADDRESS = "192.168.0.10";
private static final int ELM327_IP_PORT = 35000;
private static final byte OBD_RESPONSE = (byte)0x40;
private static final String CR = "\n";
private static final String LF = "\r";
private static final String CR_LF = "\n\r";
private static final String PROMPT = ">";
private Socket elmSocket;
private OutputStream elmOutput;
private InputStream elmInput;
private boolean debugOn = false;
private int debugLevel = 5;
private byte[] rawResponse = new byte[1024];
protected byte[] responseData = new byte[1024];
/**
* Common initialisation code
*
* @throws IOException If there is a communications problem
*/
private void init() throws IOException {
/* Establish a socket to the port of the ELM327 box
and create
* input and output streams to it
*/
try {
elmSocket = new
Socket(ELM327_IP_ADDRESS, ELM327_IP_PORT);
elmOutput = elmSocket.getOutputStream();
elmInput = elmSocket.getInputStream();
} catch (UnknownHostException ex) {
System.out.println("ELM327: Unknown
host, [" + ELM327_IP_ADDRESS + "]");
System.exit(1);
} catch (IOException ex) {
System.out.println("ELM327: IO error
talking to car");
System.out.println(ex.getMessage());
System.exit(2);
}
/* Ensure we have an input and output stream */
if (elmInput == null || elmOutput == null) {
System.out.println("ELM327: input or
output to device is null");
System.exit(1);
}
/* Lastly send a reset command to and turn character
echo off
* (it's not clear that turning echo off has
any effect)
*/
resetInterface();
sendATCommand("E0");
debug("ELM327: Connection established.", 1);
}
Having got a connection we then need some methods to provide a simple interface for sending commands and getting back the results. Here's the common methods for sending messages.
/**
* Send an AT command to control the ELM327 interface
*
* @param command The command string to send
* @return The response from the ELM327
* @throws IOException If there is a communication error
*/
protected String sendATCommand(String command) throws
IOException {
/* Construct the full command string to send.
We must remember to
* include a carriage return (ASCII 0x0D)
*/
String atCommand = "AT " + command + CR_LF;
debug("ELM327: Sending AT command [AT " + command +"]", 1);
/* Send it to the interface */
elmOutput.write(atCommand.getBytes());
debug("ELM327: Command sent", 1);
String response = getResponse();
/* Delete the command, which may be echoed back */
response = response.replace("AT " + command, "");
return response;
}
/**
* Send an OBD command to the car via the ELM327.
*
* @param command The command as a string of hexadecimal
values
* @return The number of bytes returned by the command
* @throws IOException If there is a problem communicating
*/
protected int sendOBDCommand(String command)
throws IOException, ELM327Exception {
byte[] commandBytes = byteStringToArray(command);
/* A valid OBD command must be at least two bytes to
indicate the mode
* and then the information request
*/
if (commandBytes.length < 2)
throw new ELM327Exception("ELM327: OBD
command must be at least 2 bytes");
byte obdMode = commandBytes[0];
/* Send the command to the ELM327 */
debug("ELM327: sendOBDCommand: [" + command + "],
mode = " + obdMode, 1);
elmOutput.write((command + CR_LF).getBytes());
debug("ELM327: Command sent", 1);
/* Read the response */
String response = getResponse();
/* Remove the original command in case that gets
echoed back */
response = response.replace(command, "");
debug("ELM327: OBD response = " + response, 1);
/* If there is NO DATA, there is no data */
if (response.compareTo("NO DATA") ==
0)
return 0;
/* Trap error message from CAN bus */
if (response.compareTo("CAN ERROR") == 0)
throw new ELM327Exception("ELM327: CAN
ERROR detected");
rawResponse = byteStringToArray(response);
int responseDataLength = rawResponse.length;
/* The first byte indicates a response for the
request mode and the
* second byte is a repeat of the PID. We
test these to ensure that
* the response is of the correct format
*/
if (responseDataLength < 2)
throw new ELM327Exception("ELM327:
Response was too short");
if (rawResponse[0] != (byte)(obdMode +
OBD_RESPONSE))
throw new ELM327Exception("ELM327:
Incorrect response [" +
String.format("%02X", responseData[0]) + " != " +
String.format("%02X", (byte)(obdMode + OBD_RESPONSE)) + "]");
if (rawResponse[1] != commandBytes[1])
throw new ELM327Exception("ELM327:
Incorrect command response [" +
String.format("%02X", responseData[1]) + " != " +
String.format("%02X", commandBytes[1]));
debug("ELM327: byte count = " + responseDataLength,
1);
for (int i = 0; i < responseDataLength; i++)
debug(String.format("ELM327: byte %d =
%02X", i, rawResponse[i]), 1);
responseData = Arrays.copyOfRange(rawResponse, 2,
responseDataLength);
return responseDataLength - 2;
}
/**
* Send an OBD command to the car via the ELM327. Test the
length of the
* response to see if it matches an expected value
*
* @param command The command as a string of hexadecimal
values
* @param expectedLength The expected length of the response
* @return The length of the response
* @throws IOException If there is a communication error or
wrong length
*/
protected int sendOBDCommand(String command, int expectedLength)
throws IOException, ELM327Exception {
int responseLength = this.sendOBDCommand(command);
if (responseLength !=
expectedLength)
throw new IOException("ELM327:
sendOBDCommand: bad reply length ["
+ responseLength
+ " != " + expectedLength + "]");
return responseLength;
}
and the method for reading back the results.
/**
* Get the response to a command, having first cleaned it
up so it only
* contains the data we're interested in.
*
* @return The response data
* @throws IOException If there is a communications problem
*/
private String getResponse() throws IOException {
boolean readComplete = false;
StringBuilder responseBuilder = new StringBuilder();
/* Read the response. Sometimes timing issues
mean we only get part of
* the message in the first read. To
ensure we always get all the intended
* data (and therefore do not get confused on
the the next read) we keep
* reading until we see a prompt character in
the data. That way we know
* we have definitely got all the response.
*/
while (!readComplete) {
int readLength =
elmInput.read(rawResponse);
debug("ELM327: Response received, length
= " + readLength, 1);
String data = new
String(Arrays.copyOfRange(rawResponse, 0, readLength));
responseBuilder.append(data);
/* Check for the prompt */
if (data.contains(PROMPT)) {
debug("ELM327: Got a
prompt", 1);
break;
}
}
/* Strip out newline, carriage return and the prompt
*/
String response = responseBuilder.toString();
response = response.replace(CR, "");
response = response.replace(LF, "");
response = response.replace(PROMPT, "");
return response;
}
Using these methods it becomes pretty simple to implement methods that start to expose the OBD protocol. For example to get the version information about the interface we just need this simple method:
/**
* Get the version number of the ELM327 connected
*
* @return The version number string
* @throws IOException If there is a communications problem
*/
public String getInterfaceVersionNumber() throws IOException {
return sendATCommand("I");
}
Another very useful method is one that returns the details about which of the PIDs are supported for a given mode.
/**
* Determine which PIDs for OBDII are supported. The OBD
standards docs are
* required for a fuller explanation of these.
*
* @param pid Determines which range of PIDs support is
reported for
* @return An array indicating which PIDs are supported
* @throws IOException If there is a communication error
*/
public boolean[] getPIDSupport(byte pid) throws IOException,
ELM327Exception {
int dataLength = sendOBDCommand("01 " +
String.format("%02X", pid));
/* If we get zero bytes back then we assume that
there are no
* supported PIDs for the requested range
*/
if (dataLength == 0)
return null;
int pidCount = dataLength * 8;
debug("ELM327: pid count = " + pidCount, 1);
boolean[] pidList = new boolean[pidCount];
int p = 0;
/* Now decode the bit map of supported PIDs */
for (int i = 2; i < dataLength; i++)
for (int j = 0; j < 8; j++) {
if ((responseData[i] &
(1 << j)) != 0)
pidList[p++] =
true;
else
pidList[p++] =
false;
}
return pidList;
}
The PIDs 0x00, 0x20, 0x40, 0x60, 0x80, 0xA0 and 0xC0 of mode 1 will report back the supported PIDs for the following 31 values as a four byte bit map. There appear to only be definitions for commands up to 0x87 in the specification I found.
In the next part we'll look at how we can start to use this class to get some real data from the car.