diff options
Diffstat (limited to 'obd2-lib')
5 files changed, 271 insertions, 7 deletions
diff --git a/obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java b/obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java index 56f64f77..4768cebc 100644 --- a/obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java +++ b/obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java | |||
@@ -43,6 +43,10 @@ public class IntegerArrayStream { | |||
43 | return mData.length - mIndex; | 43 | return mData.length - mIndex; |
44 | } | 44 | } |
45 | 45 | ||
46 | public boolean isEmpty() { | ||
47 | return residualLength() == 0; | ||
48 | } | ||
49 | |||
46 | public boolean hasAtLeast(int n) { | 50 | public boolean hasAtLeast(int n) { |
47 | return residualLength() >= n; | 51 | return residualLength() >= n; |
48 | } | 52 | } |
diff --git a/obd2-lib/src/com/android/car/obd2/Obd2Command.java b/obd2-lib/src/com/android/car/obd2/Obd2Command.java index e3366cfd..30fca0c1 100644 --- a/obd2-lib/src/com/android/car/obd2/Obd2Command.java +++ b/obd2-lib/src/com/android/car/obd2/Obd2Command.java | |||
@@ -172,7 +172,7 @@ public abstract class Obd2Command<ValueType> { | |||
172 | * @param <ValueType> The Java type that represents the command's result type. | 172 | * @param <ValueType> The Java type that represents the command's result type. |
173 | */ | 173 | */ |
174 | public static class FreezeFrameCommand<ValueType> extends Obd2Command<ValueType> { | 174 | public static class FreezeFrameCommand<ValueType> extends Obd2Command<ValueType> { |
175 | private static final int RESPONSE_MARKER = 0x2; | 175 | private static final int RESPONSE_MARKER = 0x42; |
176 | 176 | ||
177 | private int mFrameId; | 177 | private int mFrameId; |
178 | 178 | ||
diff --git a/obd2-lib/src/com/android/car/obd2/Obd2Connection.java b/obd2-lib/src/com/android/car/obd2/Obd2Connection.java index 577b7989..f7a2d369 100644 --- a/obd2-lib/src/com/android/car/obd2/Obd2Connection.java +++ b/obd2-lib/src/com/android/car/obd2/Obd2Connection.java | |||
@@ -20,12 +20,16 @@ import android.util.Log; | |||
20 | import java.io.IOException; | 20 | import java.io.IOException; |
21 | import java.io.InputStream; | 21 | import java.io.InputStream; |
22 | import java.io.OutputStream; | 22 | import java.io.OutputStream; |
23 | import java.util.ArrayList; | ||
23 | import java.util.HashSet; | 24 | import java.util.HashSet; |
25 | import java.util.List; | ||
24 | import java.util.Objects; | 26 | import java.util.Objects; |
25 | import java.util.Set; | 27 | import java.util.Set; |
26 | 28 | ||
27 | /** This class represents a connection between Java code and a "vehicle" that talks OBD2. */ | 29 | /** This class represents a connection between Java code and a "vehicle" that talks OBD2. */ |
28 | public class Obd2Connection { | 30 | public class Obd2Connection { |
31 | private static final String TAG = Obd2Connection.class.getSimpleName(); | ||
32 | private static final boolean DBG = false; | ||
29 | 33 | ||
30 | /** | 34 | /** |
31 | * The transport layer that moves OBD2 requests from us to the remote entity and viceversa. It | 35 | * The transport layer that moves OBD2 requests from us to the remote entity and viceversa. It |
@@ -116,6 +120,10 @@ public class Obd2Connection { | |||
116 | InputStream in = Objects.requireNonNull(mConnection.getInputStream()); | 120 | InputStream in = Objects.requireNonNull(mConnection.getInputStream()); |
117 | OutputStream out = Objects.requireNonNull(mConnection.getOutputStream()); | 121 | OutputStream out = Objects.requireNonNull(mConnection.getOutputStream()); |
118 | 122 | ||
123 | if (DBG) { | ||
124 | Log.i(TAG, "runImpl(" + command + ")"); | ||
125 | } | ||
126 | |||
119 | out.write((command + "\r").getBytes()); | 127 | out.write((command + "\r").getBytes()); |
120 | out.flush(); | 128 | out.flush(); |
121 | 129 | ||
@@ -131,6 +139,11 @@ public class Obd2Connection { | |||
131 | } | 139 | } |
132 | 140 | ||
133 | String responseValue = response.toString(); | 141 | String responseValue = response.toString(); |
142 | |||
143 | if (DBG) { | ||
144 | Log.i(TAG, "runImpl() returned " + responseValue); | ||
145 | } | ||
146 | |||
134 | return responseValue; | 147 | return responseValue; |
135 | } | 148 | } |
136 | 149 | ||
@@ -141,11 +154,30 @@ public class Obd2Connection { | |||
141 | return response; | 154 | return response; |
142 | } | 155 | } |
143 | 156 | ||
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 | |||
144 | public int[] run(String command) throws IOException, InterruptedException { | 169 | public int[] run(String command) throws IOException, InterruptedException { |
145 | String responseValue = runImpl(command); | 170 | String responseValue = runImpl(command); |
146 | String originalResponseValue = responseValue; | 171 | String originalResponseValue = responseValue; |
147 | if (responseValue.startsWith(command)) | 172 | String unspacedCommand = command.replaceAll(" ", ""); |
148 | responseValue = responseValue.substring(command.length()); | 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 | |||
149 | //TODO(egranata): should probably handle these intelligently | 181 | //TODO(egranata): should probably handle these intelligently |
150 | responseValue = | 182 | responseValue = |
151 | removeSideData( | 183 | removeSideData( |
@@ -161,11 +193,12 @@ public class Obd2Connection { | |||
161 | if (responseValue.equals("?")) return new int[] {0}; | 193 | if (responseValue.equals("?")) return new int[] {0}; |
162 | if (responseValue.equals("NODATA")) return new int[] {}; | 194 | if (responseValue.equals("NODATA")) return new int[] {}; |
163 | if (responseValue.equals("UNABLETOCONNECT")) throw new IOException("connection failure"); | 195 | if (responseValue.equals("UNABLETOCONNECT")) throw new IOException("connection failure"); |
196 | if (responseValue.equals("CANERROR")) throw new IOException("CAN bus error"); | ||
164 | try { | 197 | try { |
165 | return toHexValues(responseValue); | 198 | return toHexValues(responseValue); |
166 | } catch (IllegalArgumentException e) { | 199 | } catch (IllegalArgumentException e) { |
167 | Log.e( | 200 | Log.e( |
168 | "OBD2", | 201 | TAG, |
169 | String.format( | 202 | String.format( |
170 | "conversion error: command: '%s', original response: '%s'" | 203 | "conversion error: command: '%s', original response: '%s'" |
171 | + ", processed response: '%s'", | 204 | + ", processed response: '%s'", |
@@ -228,7 +261,7 @@ public class Obd2Connection { | |||
228 | public Set<Integer> getSupportedPIDs() throws IOException, InterruptedException { | 261 | public Set<Integer> getSupportedPIDs() throws IOException, InterruptedException { |
229 | Set<Integer> result = new HashSet<>(); | 262 | Set<Integer> result = new HashSet<>(); |
230 | String[] pids = new String[] {"0100", "0120", "0140", "0160"}; | 263 | String[] pids = new String[] {"0100", "0120", "0140", "0160"}; |
231 | int basePid = 0; | 264 | int basePid = 1; |
232 | for (String pid : pids) { | 265 | for (String pid : pids) { |
233 | int[] responseData = run(pid); | 266 | int[] responseData = run(pid); |
234 | if (responseData.length >= 6) { | 267 | if (responseData.length >= 6) { |
@@ -236,11 +269,19 @@ public class Obd2Connection { | |||
236 | byte byte1 = (byte) (responseData[3] & 0xFF); | 269 | byte byte1 = (byte) (responseData[3] & 0xFF); |
237 | byte byte2 = (byte) (responseData[4] & 0xFF); | 270 | byte byte2 = (byte) (responseData[4] & 0xFF); |
238 | byte byte3 = (byte) (responseData[5] & 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 | } | ||
239 | FourByteBitSet fourByteBitSet = new FourByteBitSet(byte0, byte1, byte2, byte3); | 276 | FourByteBitSet fourByteBitSet = new FourByteBitSet(byte0, byte1, byte2, byte3); |
240 | for (int byteIndex = 0; byteIndex < 4; ++byteIndex) { | 277 | for (int byteIndex = 0; byteIndex < 4; ++byteIndex) { |
241 | for (int bitIndex = 7; bitIndex >= 0; --bitIndex) { | 278 | for (int bitIndex = 7; bitIndex >= 0; --bitIndex) { |
242 | if (fourByteBitSet.getBit(byteIndex, bitIndex)) { | 279 | if (fourByteBitSet.getBit(byteIndex, bitIndex)) { |
243 | result.add(basePid + 8 * byteIndex + 7 - 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); | ||
244 | } | 285 | } |
245 | } | 286 | } |
246 | } | 287 | } |
@@ -250,4 +291,46 @@ public class Obd2Connection { | |||
250 | 291 | ||
251 | return result; | 292 | return result; |
252 | } | 293 | } |
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 | } | ||
253 | } | 336 | } |
diff --git a/obd2-lib/src/com/android/car/obd2/Obd2FreezeFrameGenerator.java b/obd2-lib/src/com/android/car/obd2/Obd2FreezeFrameGenerator.java new file mode 100644 index 00000000..3de48638 --- /dev/null +++ b/obd2-lib/src/com/android/car/obd2/Obd2FreezeFrameGenerator.java | |||
@@ -0,0 +1,177 @@ | |||
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.os.SystemClock; | ||
20 | import android.util.JsonWriter; | ||
21 | import android.util.Log; | ||
22 | import com.android.car.obd2.Obd2Command.FreezeFrameCommand; | ||
23 | import com.android.car.obd2.Obd2Command.OutputSemanticHandler; | ||
24 | import java.io.IOException; | ||
25 | import java.util.ArrayList; | ||
26 | import java.util.List; | ||
27 | import java.util.Optional; | ||
28 | import java.util.Set; | ||
29 | |||
30 | public class Obd2FreezeFrameGenerator { | ||
31 | public static final String FRAME_TYPE_FREEZE = "freeze"; | ||
32 | public static final String TAG = Obd2FreezeFrameGenerator.class.getSimpleName(); | ||
33 | |||
34 | private final Obd2Connection mConnection; | ||
35 | private final List<OutputSemanticHandler<Integer>> mIntegerCommands = new ArrayList<>(); | ||
36 | private final List<OutputSemanticHandler<Float>> mFloatCommands = new ArrayList<>(); | ||
37 | |||
38 | private List<String> mPreviousDtcs = new ArrayList<>(); | ||
39 | |||
40 | public Obd2FreezeFrameGenerator(Obd2Connection connection) | ||
41 | throws IOException, InterruptedException { | ||
42 | mConnection = connection; | ||
43 | Set<Integer> connectionPids = connection.getSupportedPIDs(); | ||
44 | Set<Integer> apiIntegerPids = Obd2Command.getSupportedIntegerCommands(); | ||
45 | Set<Integer> apiFloatPids = Obd2Command.getSupportedFloatCommands(); | ||
46 | apiIntegerPids | ||
47 | .stream() | ||
48 | .filter(connectionPids::contains) | ||
49 | .forEach((Integer pid) -> mIntegerCommands.add(Obd2Command.getIntegerCommand(pid))); | ||
50 | apiFloatPids | ||
51 | .stream() | ||
52 | .filter(connectionPids::contains) | ||
53 | .forEach((Integer pid) -> mFloatCommands.add(Obd2Command.getFloatCommand(pid))); | ||
54 | Log.i( | ||
55 | TAG, | ||
56 | String.format( | ||
57 | "connectionPids = %s\napiIntegerPids=%s\napiFloatPids = %s\n" | ||
58 | + "mIntegerCommands = %s\nmFloatCommands = %s\n", | ||
59 | connectionPids, | ||
60 | apiIntegerPids, | ||
61 | apiFloatPids, | ||
62 | mIntegerCommands, | ||
63 | mFloatCommands)); | ||
64 | } | ||
65 | |||
66 | public JsonWriter generate(JsonWriter jsonWriter) throws IOException, InterruptedException { | ||
67 | return generate(jsonWriter, SystemClock.elapsedRealtimeNanos()); | ||
68 | } | ||
69 | |||
70 | // OBD2 does not have a notion of timestamping the fault codes | ||
71 | // As such, we need to perform additional magic in order to figure out | ||
72 | // whether a fault code we retrieved is the same as a fault code we already | ||
73 | // saw in a past iteration. The logic goes as follows: | ||
74 | // for every position i in currentDtcs, if mPreviousDtcs[i] is the same | ||
75 | // fault code, then assume they are identical. If they are not the same fault code, | ||
76 | // then everything in currentDtcs[i...size()) is assumed to be a new fault code as | ||
77 | // something in the list must have moved around; if currentDtcs is shorter than | ||
78 | // mPreviousDtcs then obviously exit at the end of currentDtcs; if currentDtcs | ||
79 | // is longer, however, anything in currentDtcs past the end of mPreviousDtcs is a new | ||
80 | // fault code and will be included | ||
81 | private final class FreezeFrameIdentity { | ||
82 | public final String dtc; | ||
83 | public final int id; | ||
84 | |||
85 | FreezeFrameIdentity(String dtc, int id) { | ||
86 | this.dtc = dtc; | ||
87 | this.id = id; | ||
88 | } | ||
89 | } | ||
90 | |||
91 | private List<FreezeFrameIdentity> discoverNewDtcs(List<String> currentDtcs) { | ||
92 | List<FreezeFrameIdentity> newDtcs = new ArrayList<>(); | ||
93 | int currentIndex = 0; | ||
94 | boolean inCopyAllMode = false; | ||
95 | |||
96 | for (; currentIndex < currentDtcs.size(); ++currentIndex) { | ||
97 | if (currentIndex == mPreviousDtcs.size()) { | ||
98 | // we have more current DTCs than previous DTCs, copy everything | ||
99 | inCopyAllMode = true; | ||
100 | break; | ||
101 | } | ||
102 | if (!currentDtcs.get(currentIndex).equals(mPreviousDtcs.get(currentIndex))) { | ||
103 | // we found a different DTC, copy everything | ||
104 | inCopyAllMode = true; | ||
105 | break; | ||
106 | } | ||
107 | // same DTC, not at end of either list yet, keep looping | ||
108 | } | ||
109 | |||
110 | if (inCopyAllMode) { | ||
111 | for (; currentIndex < currentDtcs.size(); ++currentIndex) { | ||
112 | newDtcs.add(new FreezeFrameIdentity(currentDtcs.get(currentIndex), currentIndex)); | ||
113 | } | ||
114 | } | ||
115 | |||
116 | return newDtcs; | ||
117 | } | ||
118 | |||
119 | public JsonWriter generate(JsonWriter jsonWriter, long timestamp) | ||
120 | throws IOException, InterruptedException { | ||
121 | List<String> currentDtcs = mConnection.getDiagnosticTroubleCodes(); | ||
122 | List<FreezeFrameIdentity> newDtcs = discoverNewDtcs(currentDtcs); | ||
123 | mPreviousDtcs = currentDtcs; | ||
124 | for (FreezeFrameIdentity freezeFrame : newDtcs) { | ||
125 | jsonWriter.beginObject(); | ||
126 | jsonWriter.name("type").value(FRAME_TYPE_FREEZE); | ||
127 | jsonWriter.name("timestamp").value(timestamp); | ||
128 | jsonWriter.name("stringValue").value(freezeFrame.dtc); | ||
129 | jsonWriter.name("intValues").beginArray(); | ||
130 | for (OutputSemanticHandler<Integer> handler : mIntegerCommands) { | ||
131 | FreezeFrameCommand<Integer> command = | ||
132 | Obd2Command.getFreezeFrameCommand(handler, freezeFrame.id); | ||
133 | try { | ||
134 | Optional<Integer> result = command.run(mConnection); | ||
135 | if (result.isPresent()) { | ||
136 | jsonWriter.beginObject(); | ||
137 | jsonWriter.name("id").value(command.getPid()); | ||
138 | jsonWriter.name("value").value(result.get()); | ||
139 | jsonWriter.endObject(); | ||
140 | } | ||
141 | } catch (IOException | InterruptedException e) { | ||
142 | Log.w( | ||
143 | TAG, | ||
144 | String.format( | ||
145 | "unable to retrieve OBD2 pid %d due to exception: %s", | ||
146 | command.getPid(), e)); | ||
147 | // skip this entry | ||
148 | } | ||
149 | } | ||
150 | jsonWriter.endArray(); | ||
151 | jsonWriter.name("floatValues").beginArray(); | ||
152 | for (OutputSemanticHandler<Float> handler : mFloatCommands) { | ||
153 | FreezeFrameCommand<Float> command = | ||
154 | Obd2Command.getFreezeFrameCommand(handler, freezeFrame.id); | ||
155 | try { | ||
156 | Optional<Float> result = command.run(mConnection); | ||
157 | if (result.isPresent()) { | ||
158 | jsonWriter.beginObject(); | ||
159 | jsonWriter.name("id").value(command.getPid()); | ||
160 | jsonWriter.name("value").value(result.get()); | ||
161 | jsonWriter.endObject(); | ||
162 | } | ||
163 | } catch (IOException | InterruptedException e) { | ||
164 | Log.w( | ||
165 | TAG, | ||
166 | String.format( | ||
167 | "unable to retrieve OBD2 pid %d due to exception: %s", | ||
168 | command.getPid(), e)); | ||
169 | // skip this entry | ||
170 | } | ||
171 | } | ||
172 | jsonWriter.endArray(); | ||
173 | jsonWriter.endObject(); | ||
174 | } | ||
175 | return jsonWriter; | ||
176 | } | ||
177 | } | ||
diff --git a/obd2-lib/src/com/android/car/obd2/commands/RPM.java b/obd2-lib/src/com/android/car/obd2/commands/RPM.java index b277abfc..a062e978 100644 --- a/obd2-lib/src/com/android/car/obd2/commands/RPM.java +++ b/obd2-lib/src/com/android/car/obd2/commands/RPM.java | |||
@@ -30,7 +30,7 @@ public class RPM implements Obd2Command.OutputSemanticHandler<Integer> { | |||
30 | public Optional<Integer> consume(IntegerArrayStream data) { | 30 | public Optional<Integer> consume(IntegerArrayStream data) { |
31 | return data.hasAtLeast( | 31 | return data.hasAtLeast( |
32 | 2, | 32 | 2, |
33 | theData -> Optional.of(theData.consume() * 256 + theData.consume() / 4), | 33 | theData -> Optional.of((theData.consume() * 256 + theData.consume()) / 4), |
34 | theData -> Optional.<Integer>empty()); | 34 | theData -> Optional.<Integer>empty()); |
35 | } | 35 | } |
36 | } | 36 | } |