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 {
getAddress()39         String getAddress();
40 
reconnect()41         boolean reconnect();
42 
isConnected()43         boolean isConnected();
44 
getInputStream()45         InputStream getInputStream();
46 
getOutputStream()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 
Obd2Connection(UnderlyingTransport connection)55     public Obd2Connection(UnderlyingTransport connection) {
56         mConnection = Objects.requireNonNull(connection);
57         runInitCommands();
58     }
59 
getAddress()60     public String getAddress() {
61         return mConnection.getAddress();
62     }
63 
runInitCommands()64     private void runInitCommands() {
65         for (final String initCommand : initCommands) {
66             try {
67                 runImpl(initCommand);
68             } catch (IOException | InterruptedException e) {
69             }
70         }
71     }
72 
reconnect()73     public boolean reconnect() {
74         if (!mConnection.reconnect()) return false;
75         runInitCommands();
76         return true;
77     }
78 
isConnected()79     public boolean isConnected() {
80         return mConnection.isConnected();
81     }
82 
toDigitValue(char c)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 
toHexValues(String buffer)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 
runImpl(String command)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 
removeSideData(String response, String... patterns)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 
unpackLongFrame(String response)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 
run(String command)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 
FourByteBitSet(byte b0, byte b1, byte b2, byte b3)228         FourByteBitSet(byte b0, byte b1, byte b2, byte b3) {
229             mByte0 = b0;
230             mByte1 = b1;
231             mByte2 = b2;
232             mByte3 = b3;
233         }
234 
getByte(int index)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 
getBit(byte b, int index)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 
getBit(int b, int index)256         public boolean getBit(int b, int index) {
257             return getBit(getByte(b), index);
258         }
259     }
260 
getSupportedPIDs()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 
getDiagnosticTroubleCode(IntegerArrayStream source)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 
getDiagnosticTroubleCodes()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