blob: f7a2d3695af731948b46b10e629afcc284e7f82e [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.obd2;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/** This class represents a connection between Java code and a "vehicle" that talks OBD2. */
public class Obd2Connection {
private static final String TAG = Obd2Connection.class.getSimpleName();
private static final boolean DBG = false;
/**
* The transport layer that moves OBD2 requests from us to the remote entity and viceversa. It
* is possible for this to be USB, Bluetooth, or just as simple as a pty for a simulator.
*/
public interface UnderlyingTransport {
String getAddress();
boolean reconnect();
boolean isConnected();
InputStream getInputStream();
OutputStream getOutputStream();
}
private final UnderlyingTransport mConnection;
private static final String[] initCommands =
new String[] {"ATD", "ATZ", "AT E0", "AT L0", "AT S0", "AT H0", "AT SP 0"};
public Obd2Connection(UnderlyingTransport connection) {
mConnection = Objects.requireNonNull(connection);
runInitCommands();
}
public String getAddress() {
return mConnection.getAddress();
}
private void runInitCommands() {
for (final String initCommand : initCommands) {
try {
runImpl(initCommand);
} catch (IOException | InterruptedException e) {
}
}
}
public boolean reconnect() {
if (!mConnection.reconnect()) return false;
runInitCommands();
return true;
}
public boolean isConnected() {
return mConnection.isConnected();
}
static int toDigitValue(char c) {
if ((c >= '0') && (c <= '9')) return c - '0';
switch (c) {
case 'a':
case 'A':
return 10;
case 'b':
case 'B':
return 11;
case 'c':
case 'C':
return 12;
case 'd':
case 'D':
return 13;
case 'e':
case 'E':
return 14;
case 'f':
case 'F':
return 15;
default:
throw new IllegalArgumentException(c + " is not a valid hex digit");
}
}
int[] toHexValues(String buffer) {
int[] values = new int[buffer.length() / 2];
for (int i = 0; i < values.length; ++i) {
values[i] =
16 * toDigitValue(buffer.charAt(2 * i))
+ toDigitValue(buffer.charAt(2 * i + 1));
}
return values;
}
private String runImpl(String command) throws IOException, InterruptedException {
InputStream in = Objects.requireNonNull(mConnection.getInputStream());
OutputStream out = Objects.requireNonNull(mConnection.getOutputStream());
if (DBG) {
Log.i(TAG, "runImpl(" + command + ")");
}
out.write((command + "\r").getBytes());
out.flush();
StringBuilder response = new StringBuilder();
while (true) {
int value = in.read();
if (value < 0) continue;
char c = (char) value;
// this is the prompt, stop here
if (c == '>') break;
if (c == '\r' || c == '\n' || c == ' ' || c == '\t' || c == '.') continue;
response.append(c);
}
String responseValue = response.toString();
if (DBG) {
Log.i(TAG, "runImpl() returned " + responseValue);
}
return responseValue;
}
String removeSideData(String response, String... patterns) {
for (String pattern : patterns) {
if (response.contains(pattern)) response = response.replaceAll(pattern, "");
}
return response;
}
String unpackLongFrame(String response) {
// long frames come back to us containing colon separated portions
if (response.indexOf(':') < 0) return response;
// remove everything until the first colon
response = response.substring(response.indexOf(':') + 1);
// then remove the <digit>: portions (sequential frame parts)
//TODO(egranata): maybe validate the sequence of digits is progressive
return response.replaceAll("[0-9]:", "");
}
public int[] run(String command) throws IOException, InterruptedException {
String responseValue = runImpl(command);
String originalResponseValue = responseValue;
String unspacedCommand = command.replaceAll(" ", "");
if (responseValue.startsWith(unspacedCommand))
responseValue = responseValue.substring(unspacedCommand.length());
responseValue = unpackLongFrame(responseValue);
if (DBG) {
Log.i(TAG, "post-processed response " + responseValue);
}
//TODO(egranata): should probably handle these intelligently
responseValue =
removeSideData(
responseValue,
"SEARCHING",
"ERROR",
"BUS INIT",
"BUSINIT",
"BUS ERROR",
"BUSERROR",
"STOPPED");
if (responseValue.equals("OK")) return new int[] {1};
if (responseValue.equals("?")) return new int[] {0};
if (responseValue.equals("NODATA")) return new int[] {};
if (responseValue.equals("UNABLETOCONNECT")) throw new IOException("connection failure");
if (responseValue.equals("CANERROR")) throw new IOException("CAN bus error");
try {
return toHexValues(responseValue);
} catch (IllegalArgumentException e) {
Log.e(
TAG,
String.format(
"conversion error: command: '%s', original response: '%s'"
+ ", processed response: '%s'",
command, originalResponseValue, responseValue));
throw e;
}
}
static class FourByteBitSet {
private static final int[] masks =
new int[] {
0b0000_0001,
0b0000_0010,
0b0000_0100,
0b0000_1000,
0b0001_0000,
0b0010_0000,
0b0100_0000,
0b1000_0000
};
private final byte mByte0;
private final byte mByte1;
private final byte mByte2;
private final byte mByte3;
FourByteBitSet(byte b0, byte b1, byte b2, byte b3) {
mByte0 = b0;
mByte1 = b1;
mByte2 = b2;
mByte3 = b3;
}
private byte getByte(int index) {
switch (index) {
case 0:
return mByte0;
case 1:
return mByte1;
case 2:
return mByte2;
case 3:
return mByte3;
default:
throw new IllegalArgumentException(index + " is not a valid byte index");
}
}
private boolean getBit(byte b, int index) {
if (index < 0 || index >= masks.length)
throw new IllegalArgumentException(index + " is not a valid bit index");
return 0 != (b & masks[index]);
}
public boolean getBit(int b, int index) {
return getBit(getByte(b), index);
}
}
public Set<Integer> getSupportedPIDs() throws IOException, InterruptedException {
Set<Integer> result = new HashSet<>();
String[] pids = new String[] {"0100", "0120", "0140", "0160"};
int basePid = 1;
for (String pid : pids) {
int[] responseData = run(pid);
if (responseData.length >= 6) {
byte byte0 = (byte) (responseData[2] & 0xFF);
byte byte1 = (byte) (responseData[3] & 0xFF);
byte byte2 = (byte) (responseData[4] & 0xFF);
byte byte3 = (byte) (responseData[5] & 0xFF);
if (DBG) {
Log.i(TAG, String.format("supported PID at base %d payload %02X%02X%02X%02X",
basePid, byte0, byte1, byte2, byte3));
}
FourByteBitSet fourByteBitSet = new FourByteBitSet(byte0, byte1, byte2, byte3);
for (int byteIndex = 0; byteIndex < 4; ++byteIndex) {
for (int bitIndex = 7; bitIndex >= 0; --bitIndex) {
if (fourByteBitSet.getBit(byteIndex, bitIndex)) {
int command = basePid + 8 * byteIndex + 7 - bitIndex;
if (DBG) {
Log.i(TAG, "command " + command + " found supported");
}
result.add(command);
}
}
}
}
basePid += 0x20;
}
return result;
}
String getDiagnosticTroubleCode(IntegerArrayStream source) {
final char[] components = new char[] {'P', 'C', 'B', 'U'};
final char[] firstDigits = new char[] {'0', '1', '2', '3'};
final char[] otherDigits =
new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
StringBuilder builder = new StringBuilder(5);
int byte0 = source.consume();
int byte1 = source.consume();
int componentMask = (byte0 & 0xC0) >> 6;
int firstDigitMask = (byte0 & 0x30) >> 4;
int secondDigitMask = (byte0 & 0x0F);
int thirdDigitMask = (byte1 & 0xF0) >> 4;
int fourthDigitMask = (byte1 & 0x0F);
builder.append(components[componentMask]);
builder.append(firstDigits[firstDigitMask]);
builder.append(otherDigits[secondDigitMask]);
builder.append(otherDigits[thirdDigitMask]);
builder.append(otherDigits[fourthDigitMask]);
return builder.toString();
}
public List<String> getDiagnosticTroubleCodes() throws IOException, InterruptedException {
List<String> result = new ArrayList<>();
int[] response = run("03");
IntegerArrayStream stream = new IntegerArrayStream(response);
if (stream.isEmpty()) return result;
if (!stream.expect(0x43))
throw new IllegalArgumentException("data from remote end not a mode 3 response");
int count = stream.consume();
for (int i = 0; i < count; ++i) {
result.add(getDiagnosticTroubleCode(stream));
}
return result;
}
}