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