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.HashSet; 24 import java.util.Objects; 25 import java.util.Set; 26 27 /** This class represents a connection between Java code and a "vehicle" that talks OBD2. */ 28 public class Obd2Connection { 29 30 /** 31 * The transport layer that moves OBD2 requests from us to the remote entity and viceversa. It 32 * is possible for this to be USB, Bluetooth, or just as simple as a pty for a simulator. 33 */ 34 public interface UnderlyingTransport { 35 String getAddress(); 36 37 boolean reconnect(); 38 39 boolean isConnected(); 40 41 InputStream getInputStream(); 42 43 OutputStream getOutputStream(); 44 } 45 46 private final UnderlyingTransport mConnection; 47 48 private static final String[] initCommands = 49 new String[] {"ATD", "ATZ", "AT E0", "AT L0", "AT S0", "AT H0", "AT SP 0"}; 50 51 public Obd2Connection(UnderlyingTransport connection) { 52 mConnection = Objects.requireNonNull(connection); 53 runInitCommands(); 54 } 55 56 public String getAddress() { 57 return mConnection.getAddress(); 58 } 59 60 private void runInitCommands() { 61 for (final String initCommand : initCommands) { 62 try { 63 runImpl(initCommand); 64 } catch (IOException | InterruptedException e) { 65 } 66 } 67 } 68 69 public boolean reconnect() { 70 if (!mConnection.reconnect()) return false; 71 runInitCommands(); 72 return true; 73 } 74 75 static int toDigitValue(char c) { 76 if ((c >= '0') && (c <= '9')) return c - '0'; 77 switch (c) { 78 case 'a': 79 case 'A': 80 return 10; 81 case 'b': 82 case 'B': 83 return 11; 84 case 'c': 85 case 'C': 86 return 12; 87 case 'd': 88 case 'D': 89 return 13; 90 case 'e': 91 case 'E': 92 return 14; 93 case 'f': 94 case 'F': 95 return 15; 96 default: 97 throw new IllegalArgumentException(c + " is not a valid hex digit"); 98 } 99 } 100 101 int[] toHexValues(String buffer) { 102 int[] values = new int[buffer.length() / 2]; 103 for (int i = 0; i < values.length; ++i) { 104 values[i] = 105 16 * toDigitValue(buffer.charAt(2 * i)) 106 + toDigitValue(buffer.charAt(2 * i + 1)); 107 } 108 return values; 109 } 110 111 private String runImpl(String command) throws IOException, InterruptedException { 112 InputStream in = Objects.requireNonNull(mConnection.getInputStream()); 113 OutputStream out = Objects.requireNonNull(mConnection.getOutputStream()); 114 115 out.write((command + "\r").getBytes()); 116 out.flush(); 117 118 StringBuilder response = new StringBuilder(); 119 while (true) { 120 int value = in.read(); 121 if (value < 0) continue; 122 char c = (char) value; 123 // this is the prompt, stop here 124 if (c == '>') break; 125 if (c == '\r' || c == '\n' || c == ' ' || c == '\t' || c == '.') continue; 126 response.append(c); 127 } 128 129 String responseValue = response.toString(); 130 return responseValue; 131 } 132 133 String removeSideData(String response, String... patterns) { 134 for (String pattern : patterns) { 135 if (response.contains(pattern)) response = response.replaceAll(pattern, ""); 136 } 137 return response; 138 } 139 140 public int[] run(String command) throws IOException, InterruptedException { 141 String responseValue = runImpl(command); 142 String originalResponseValue = responseValue; 143 if (responseValue.startsWith(command)) 144 responseValue = responseValue.substring(command.length()); 145 //TODO(egranata): should probably handle these intelligently 146 responseValue = 147 removeSideData( 148 responseValue, 149 "SEARCHING", 150 "ERROR", 151 "BUS INIT", 152 "BUSINIT", 153 "BUS ERROR", 154 "BUSERROR", 155 "STOPPED"); 156 if (responseValue.equals("OK")) return new int[] {1}; 157 if (responseValue.equals("?")) return new int[] {0}; 158 if (responseValue.equals("NODATA")) return new int[] {}; 159 if (responseValue.equals("UNABLETOCONNECT")) throw new IOException("connection failure"); 160 try { 161 return toHexValues(responseValue); 162 } catch (IllegalArgumentException e) { 163 Log.e( 164 "OBD2", 165 String.format( 166 "conversion error: command: '%s', original response: '%s'" 167 + ", processed response: '%s'", 168 command, originalResponseValue, responseValue)); 169 throw e; 170 } 171 } 172 173 static class FourByteBitSet { 174 private static final int[] masks = 175 new int[] { 176 0b0000_0001, 177 0b0000_0010, 178 0b0000_0100, 179 0b0000_1000, 180 0b0001_0000, 181 0b0010_0000, 182 0b0100_0000, 183 0b1000_0000 184 }; 185 186 private final byte mByte0; 187 private final byte mByte1; 188 private final byte mByte2; 189 private final byte mByte3; 190 191 FourByteBitSet(byte b0, byte b1, byte b2, byte b3) { 192 mByte0 = b0; 193 mByte1 = b1; 194 mByte2 = b2; 195 mByte3 = b3; 196 } 197 198 private byte getByte(int index) { 199 switch (index) { 200 case 0: 201 return mByte0; 202 case 1: 203 return mByte1; 204 case 2: 205 return mByte2; 206 case 3: 207 return mByte3; 208 default: 209 throw new IllegalArgumentException(index + " is not a valid byte index"); 210 } 211 } 212 213 private boolean getBit(byte b, int index) { 214 if (index < 0 || index >= masks.length) 215 throw new IllegalArgumentException(index + " is not a valid bit index"); 216 return 0 != (b & masks[index]); 217 } 218 219 public boolean getBit(int b, int index) { 220 return getBit(getByte(b), index); 221 } 222 } 223 224 public Set<Integer> getSupportedPIDs() throws IOException, InterruptedException { 225 Set<Integer> result = new HashSet<>(); 226 String[] pids = new String[] {"0100", "0120", "0140", "0160"}; 227 int basePid = 0; 228 for (String pid : pids) { 229 int[] responseData = run(pid); 230 if (responseData.length >= 6) { 231 byte byte0 = (byte) (responseData[2] & 0xFF); 232 byte byte1 = (byte) (responseData[3] & 0xFF); 233 byte byte2 = (byte) (responseData[4] & 0xFF); 234 byte byte3 = (byte) (responseData[5] & 0xFF); 235 FourByteBitSet fourByteBitSet = new FourByteBitSet(byte0, byte1, byte2, byte3); 236 for (int byteIndex = 0; byteIndex < 4; ++byteIndex) { 237 for (int bitIndex = 7; bitIndex >= 0; --bitIndex) { 238 if (fourByteBitSet.getBit(byteIndex, bitIndex)) { 239 result.add(basePid + 8 * byteIndex + 7 - bitIndex); 240 } 241 } 242 } 243 } 244 basePid += 0x20; 245 } 246 247 return result; 248 } 249 } 250