blob: f7a2d3695af731948b46b10e629afcc284e7f82e [file] [log] [blame]
Enrico Granata759d2912017-03-14 12:33:30 -07001/*
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
17package com.android.car.obd2;
18
19import android.util.Log;
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.OutputStream;
Enrico Granata1d590082017-04-12 17:25:23 -070023import java.util.ArrayList;
Enrico Granata759d2912017-03-14 12:33:30 -070024import java.util.HashSet;
Enrico Granata1d590082017-04-12 17:25:23 -070025import java.util.List;
Enrico Granata759d2912017-03-14 12:33:30 -070026import java.util.Objects;
27import java.util.Set;
28
29/** This class represents a connection between Java code and a "vehicle" that talks OBD2. */
30public class Obd2Connection {
Enrico Granata1d590082017-04-12 17:25:23 -070031 private static final String TAG = Obd2Connection.class.getSimpleName();
32 private static final boolean DBG = false;
Enrico Granata759d2912017-03-14 12:33:30 -070033
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
Enrico Granata7888bdc2017-03-22 14:21:52 -070043 boolean isConnected();
44
Enrico Granata759d2912017-03-14 12:33:30 -070045 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
Enrico Granatafb08d622017-03-28 10:36:16 -070079 public boolean isConnected() {
80 return mConnection.isConnected();
81 }
82
Enrico Granata759d2912017-03-14 12:33:30 -070083 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
Enrico Granata1d590082017-04-12 17:25:23 -0700123 if (DBG) {
124 Log.i(TAG, "runImpl(" + command + ")");
125 }
126
Enrico Granata759d2912017-03-14 12:33:30 -0700127 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();
Enrico Granata1d590082017-04-12 17:25:23 -0700142
143 if (DBG) {
144 Log.i(TAG, "runImpl() returned " + responseValue);
145 }
146
Enrico Granata759d2912017-03-14 12:33:30 -0700147 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
Enrico Granata1d590082017-04-12 17:25:23 -0700157 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
Enrico Granata759d2912017-03-14 12:33:30 -0700169 public int[] run(String command) throws IOException, InterruptedException {
170 String responseValue = runImpl(command);
171 String originalResponseValue = responseValue;
Enrico Granata1d590082017-04-12 17:25:23 -0700172 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
Enrico Granata759d2912017-03-14 12:33:30 -0700181 //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");
Enrico Granata1d590082017-04-12 17:25:23 -0700196 if (responseValue.equals("CANERROR")) throw new IOException("CAN bus error");
Enrico Granata759d2912017-03-14 12:33:30 -0700197 try {
198 return toHexValues(responseValue);
199 } catch (IllegalArgumentException e) {
200 Log.e(
Enrico Granata1d590082017-04-12 17:25:23 -0700201 TAG,
Enrico Granata759d2912017-03-14 12:33:30 -0700202 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"};
Enrico Granata1d590082017-04-12 17:25:23 -0700264 int basePid = 1;
Enrico Granata759d2912017-03-14 12:33:30 -0700265 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);
Enrico Granata1d590082017-04-12 17:25:23 -0700272 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 }
Enrico Granata759d2912017-03-14 12:33:30 -0700276 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)) {
Enrico Granata1d590082017-04-12 17:25:23 -0700280 int command = basePid + 8 * byteIndex + 7 - bitIndex;
281 if (DBG) {
282 Log.i(TAG, "command " + command + " found supported");
283 }
284 result.add(command);
Enrico Granata759d2912017-03-14 12:33:30 -0700285 }
286 }
287 }
288 }
289 basePid += 0x20;
290 }
291
292 return result;
293 }
Enrico Granata1d590082017-04-12 17:25:23 -0700294
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 }
Enrico Granata759d2912017-03-14 12:33:30 -0700336}