Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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; |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 23 | import java.util.ArrayList; |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 24 | import java.util.HashSet; |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 25 | import java.util.List; |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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 { |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 31 | private static final String TAG = Obd2Connection.class.getSimpleName(); |
| 32 | private static final boolean DBG = false; |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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 | |
Enrico Granata | 7888bdc | 2017-03-22 14:21:52 -0700 | [diff] [blame] | 43 | boolean isConnected(); |
| 44 | |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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 | |
Enrico Granata | fb08d62 | 2017-03-28 10:36:16 -0700 | [diff] [blame] | 79 | public boolean isConnected() { |
| 80 | return mConnection.isConnected(); |
| 81 | } |
| 82 | |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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 | |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 123 | if (DBG) { |
| 124 | Log.i(TAG, "runImpl(" + command + ")"); |
| 125 | } |
| 126 | |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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(); |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 142 | |
| 143 | if (DBG) { |
| 144 | Log.i(TAG, "runImpl() returned " + responseValue); |
| 145 | } |
| 146 | |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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 | |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 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 | |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 169 | public int[] run(String command) throws IOException, InterruptedException { |
| 170 | String responseValue = runImpl(command); |
| 171 | String originalResponseValue = responseValue; |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 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 | |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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"); |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 196 | if (responseValue.equals("CANERROR")) throw new IOException("CAN bus error"); |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 197 | try { |
| 198 | return toHexValues(responseValue); |
| 199 | } catch (IllegalArgumentException e) { |
| 200 | Log.e( |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 201 | TAG, |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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"}; |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 264 | int basePid = 1; |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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); |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 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 | } |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 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)) { |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 280 | int command = basePid + 8 * byteIndex + 7 - bitIndex; |
| 281 | if (DBG) { |
| 282 | Log.i(TAG, "command " + command + " found supported"); |
| 283 | } |
| 284 | result.add(command); |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 285 | } |
| 286 | } |
| 287 | } |
| 288 | } |
| 289 | basePid += 0x20; |
| 290 | } |
| 291 | |
| 292 | return result; |
| 293 | } |
Enrico Granata | 1d59008 | 2017-04-12 17:25:23 -0700 | [diff] [blame] | 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 | } |
Enrico Granata | 759d291 | 2017-03-14 12:33:30 -0700 | [diff] [blame] | 336 | } |