Home | History | Annotate | Download | only in obd2
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.car.obd2;
     18 
     19 import android.util.Log;
     20 import java.io.IOException;
     21 import java.io.InputStream;
     22 import java.io.OutputStream;
     23 import java.util.ArrayList;
     24 import java.util.HashSet;
     25 import java.util.List;
     26 import java.util.Objects;
     27 import java.util.Set;
     28 
     29 /** This class represents a connection between Java code and a "vehicle" that talks OBD2. */
     30 public class Obd2Connection {
     31     private static final String TAG = Obd2Connection.class.getSimpleName();
     32     private static final boolean DBG = false;
     33 
     34     /**
     35      * The transport layer that moves OBD2 requests from us to the remote entity and viceversa. It
     36      * is possible for this to be USB, Bluetooth, or just as simple as a pty for a simulator.
     37      */
     38     public interface UnderlyingTransport {
     39         String getAddress();
     40 
     41         boolean reconnect();
     42 
     43         boolean isConnected();
     44 
     45         InputStream getInputStream();
     46 
     47         OutputStream getOutputStream();
     48     }
     49 
     50     private final UnderlyingTransport mConnection;
     51 
     52     private static final String[] initCommands =
     53             new String[] {"ATD", "ATZ", "AT E0", "AT L0", "AT S0", "AT H0", "AT SP 0"};
     54 
     55     public Obd2Connection(UnderlyingTransport connection) {
     56         mConnection = Objects.requireNonNull(connection);
     57         runInitCommands();
     58     }
     59 
     60     public String getAddress() {
     61         return mConnection.getAddress();
     62     }
     63 
     64     private void runInitCommands() {
     65         for (final String initCommand : initCommands) {
     66             try {
     67                 runImpl(initCommand);
     68             } catch (IOException | InterruptedException e) {
     69             }
     70         }
     71     }
     72 
     73     public boolean reconnect() {
     74         if (!mConnection.reconnect()) return false;
     75         runInitCommands();
     76         return true;
     77     }
     78 
     79     public boolean isConnected() {
     80         return mConnection.isConnected();
     81     }
     82 
     83     static int toDigitValue(char c) {
     84         if ((c >= '0') && (c <= '9')) return c - '0';
     85         switch (c) {
     86             case 'a':
     87             case 'A':
     88                 return 10;
     89             case 'b':
     90             case 'B':
     91                 return 11;
     92             case 'c':
     93             case 'C':
     94                 return 12;
     95             case 'd':
     96             case 'D':
     97                 return 13;
     98             case 'e':
     99             case 'E':
    100                 return 14;
    101             case 'f':
    102             case 'F':
    103                 return 15;
    104             default:
    105                 throw new IllegalArgumentException(c + " is not a valid hex digit");
    106         }
    107     }
    108 
    109     int[] toHexValues(String buffer) {
    110         int[] values = new int[buffer.length() / 2];
    111         for (int i = 0; i < values.length; ++i) {
    112             values[i] =
    113                     16 * toDigitValue(buffer.charAt(2 * i))
    114                             + toDigitValue(buffer.charAt(2 * i + 1));
    115         }
    116         return values;
    117     }
    118 
    119     private String runImpl(String command) throws IOException, InterruptedException {
    120         InputStream in = Objects.requireNonNull(mConnection.getInputStream());
    121         OutputStream out = Objects.requireNonNull(mConnection.getOutputStream());
    122 
    123         if (DBG) {
    124             Log.i(TAG, "runImpl(" + command + ")");
    125         }
    126 
    127         out.write((command + "\r").getBytes());
    128         out.flush();
    129 
    130         StringBuilder response = new StringBuilder();
    131         while (true) {
    132             int value = in.read();
    133             if (value < 0) continue;
    134             char c = (char) value;
    135             // this is the prompt, stop here
    136             if (c == '>') break;
    137             if (c == '\r' || c == '\n' || c == ' ' || c == '\t' || c == '.') continue;
    138             response.append(c);
    139         }
    140 
    141         String responseValue = response.toString();
    142 
    143         if (DBG) {
    144             Log.i(TAG, "runImpl() returned " + responseValue);
    145         }
    146 
    147         return responseValue;
    148     }
    149 
    150     String removeSideData(String response, String... patterns) {
    151         for (String pattern : patterns) {
    152             if (response.contains(pattern)) response = response.replaceAll(pattern, "");
    153         }
    154         return response;
    155     }
    156 
    157     String unpackLongFrame(String response) {
    158         // long frames come back to us containing colon separated portions
    159         if (response.indexOf(':') < 0) return response;
    160 
    161         // remove everything until the first colon
    162         response = response.substring(response.indexOf(':') + 1);
    163 
    164         // then remove the <digit>: portions (sequential frame parts)
    165         //TODO(egranata): maybe validate the sequence of digits is progressive
    166         return response.replaceAll("[0-9]:", "");
    167     }
    168 
    169     public int[] run(String command) throws IOException, InterruptedException {
    170         String responseValue = runImpl(command);
    171         String originalResponseValue = responseValue;
    172         String unspacedCommand = command.replaceAll(" ", "");
    173         if (responseValue.startsWith(unspacedCommand))
    174             responseValue = responseValue.substring(unspacedCommand.length());
    175         responseValue = unpackLongFrame(responseValue);
    176 
    177         if (DBG) {
    178             Log.i(TAG, "post-processed response " + responseValue);
    179         }
    180 
    181         //TODO(egranata): should probably handle these intelligently
    182         responseValue =
    183                 removeSideData(
    184                         responseValue,
    185                         "SEARCHING",
    186                         "ERROR",
    187                         "BUS INIT",
    188                         "BUSINIT",
    189                         "BUS ERROR",
    190                         "BUSERROR",
    191                         "STOPPED");
    192         if (responseValue.equals("OK")) return new int[] {1};
    193         if (responseValue.equals("?")) return new int[] {0};
    194         if (responseValue.equals("NODATA")) return new int[] {};
    195         if (responseValue.equals("UNABLETOCONNECT")) throw new IOException("connection failure");
    196         if (responseValue.equals("CANERROR")) throw new IOException("CAN bus error");
    197         try {
    198             return toHexValues(responseValue);
    199         } catch (IllegalArgumentException e) {
    200             Log.e(
    201                     TAG,
    202                     String.format(
    203                             "conversion error: command: '%s', original response: '%s'"
    204                                     + ", processed response: '%s'",
    205                             command, originalResponseValue, responseValue));
    206             throw e;
    207         }
    208     }
    209 
    210     static class FourByteBitSet {
    211         private static final int[] masks =
    212                 new int[] {
    213                     0b0000_0001,
    214                     0b0000_0010,
    215                     0b0000_0100,
    216                     0b0000_1000,
    217                     0b0001_0000,
    218                     0b0010_0000,
    219                     0b0100_0000,
    220                     0b1000_0000
    221                 };
    222 
    223         private final byte mByte0;
    224         private final byte mByte1;
    225         private final byte mByte2;
    226         private final byte mByte3;
    227 
    228         FourByteBitSet(byte b0, byte b1, byte b2, byte b3) {
    229             mByte0 = b0;
    230             mByte1 = b1;
    231             mByte2 = b2;
    232             mByte3 = b3;
    233         }
    234 
    235         private byte getByte(int index) {
    236             switch (index) {
    237                 case 0:
    238                     return mByte0;
    239                 case 1:
    240                     return mByte1;
    241                 case 2:
    242                     return mByte2;
    243                 case 3:
    244                     return mByte3;
    245                 default:
    246                     throw new IllegalArgumentException(index + " is not a valid byte index");
    247             }
    248         }
    249 
    250         private boolean getBit(byte b, int index) {
    251             if (index < 0 || index >= masks.length)
    252                 throw new IllegalArgumentException(index + " is not a valid bit index");
    253             return 0 != (b & masks[index]);
    254         }
    255 
    256         public boolean getBit(int b, int index) {
    257             return getBit(getByte(b), index);
    258         }
    259     }
    260 
    261     public Set<Integer> getSupportedPIDs() throws IOException, InterruptedException {
    262         Set<Integer> result = new HashSet<>();
    263         String[] pids = new String[] {"0100", "0120", "0140", "0160"};
    264         int basePid = 1;
    265         for (String pid : pids) {
    266             int[] responseData = run(pid);
    267             if (responseData.length >= 6) {
    268                 byte byte0 = (byte) (responseData[2] & 0xFF);
    269                 byte byte1 = (byte) (responseData[3] & 0xFF);
    270                 byte byte2 = (byte) (responseData[4] & 0xFF);
    271                 byte byte3 = (byte) (responseData[5] & 0xFF);
    272                 if (DBG) {
    273                     Log.i(TAG, String.format("supported PID at base %d payload %02X%02X%02X%02X",
    274                         basePid, byte0, byte1, byte2, byte3));
    275                 }
    276                 FourByteBitSet fourByteBitSet = new FourByteBitSet(byte0, byte1, byte2, byte3);
    277                 for (int byteIndex = 0; byteIndex < 4; ++byteIndex) {
    278                     for (int bitIndex = 7; bitIndex >= 0; --bitIndex) {
    279                         if (fourByteBitSet.getBit(byteIndex, bitIndex)) {
    280                             int command = basePid + 8 * byteIndex + 7 - bitIndex;
    281                             if (DBG) {
    282                                 Log.i(TAG, "command " + command + " found supported");
    283                             }
    284                             result.add(command);
    285                         }
    286                     }
    287                 }
    288             }
    289             basePid += 0x20;
    290         }
    291 
    292         return result;
    293     }
    294 
    295     String getDiagnosticTroubleCode(IntegerArrayStream source) {
    296         final char[] components = new char[] {'P', 'C', 'B', 'U'};
    297         final char[] firstDigits = new char[] {'0', '1', '2', '3'};
    298         final char[] otherDigits =
    299                 new char[] {
    300                     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
    301                 };
    302 
    303         StringBuilder builder = new StringBuilder(5);
    304 
    305         int byte0 = source.consume();
    306         int byte1 = source.consume();
    307 
    308         int componentMask = (byte0 & 0xC0) >> 6;
    309         int firstDigitMask = (byte0 & 0x30) >> 4;
    310         int secondDigitMask = (byte0 & 0x0F);
    311         int thirdDigitMask = (byte1 & 0xF0) >> 4;
    312         int fourthDigitMask = (byte1 & 0x0F);
    313 
    314         builder.append(components[componentMask]);
    315         builder.append(firstDigits[firstDigitMask]);
    316         builder.append(otherDigits[secondDigitMask]);
    317         builder.append(otherDigits[thirdDigitMask]);
    318         builder.append(otherDigits[fourthDigitMask]);
    319 
    320         return builder.toString();
    321     }
    322 
    323     public List<String> getDiagnosticTroubleCodes() throws IOException, InterruptedException {
    324         List<String> result = new ArrayList<>();
    325         int[] response = run("03");
    326         IntegerArrayStream stream = new IntegerArrayStream(response);
    327         if (stream.isEmpty()) return result;
    328         if (!stream.expect(0x43))
    329             throw new IllegalArgumentException("data from remote end not a mode 3 response");
    330         int count = stream.consume();
    331         for (int i = 0; i < count; ++i) {
    332             result.add(getDiagnosticTroubleCode(stream));
    333         }
    334         return result;
    335     }
    336 }
    337