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