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