Wednesday Jun 12, 2013

The Raspberry Pi JavaFX In-Car System (Part 2)

Raspberry Pi JavaFX Car Pt2 In my last post (which was rather further back in time than I had planned) I described the ideas behind my in-car Raspberry Pi JavaFX system.  Now it's time to get started on the technical stuff.

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:
  • SAE J1850 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)
You can think of this as the transport layer, which can be changed by the car manufacturer to suit their needs.  The message protocol which uses the signal protocol is defined by the OBD-II standard.  The format of these commands is pretty straightforward requiring a sequence  of pairs of hexadecimal digits.  The first pair indicates the 'mode' (of which there are 10); the second, and possibly third, pair indicates the 'parameter identification' or PID being sent.  The mode and PID combination defines the command that you are sending to the vehicle.  Results are returned as a sequence of bytes that form a string containing pairs of hexadecimal digits encoding the data.

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 start with I just used the AT I command which reports back the version of the interface and AT RV which gives the current car battery voltage.  These worked fine via telnet, so it was time to start developing the Java code. 

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.

About

A blog covering aspects of Java SE, JavaFX and embedded Java that I find fun and interesting and want to share with other developers. As part of the Developer Outreach team at Oracle I write a lot of demo code and I use this blog to highlight useful tips and techniques I learn along the way.

Search

Categories
Archives
« June 2013 »
SunMonTueWedThuFriSat
      
1
2
3
4
5
6
7
8
9
10
11
13
15
16
17
18
19
20
21
22
23
24
25
26
27
29
30
      
Today