summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEnrico Granata2017-03-14 14:33:30 -0500
committerEnrico Granata2017-03-15 17:01:12 -0500
commit759d291564677287e6a8c0aa8397b7e7de81ee22 (patch)
treef0d6741250a323c5091172405cec3e6683332097 /obd2-lib
parentc1144826c723ec38b1d73233edc6edf49c49f6f9 (diff)
downloadplatform-packages-services-car-759d291564677287e6a8c0aa8397b7e7de81ee22.tar.gz
platform-packages-services-car-759d291564677287e6a8c0aa8397b7e7de81ee22.tar.xz
platform-packages-services-car-759d291564677287e6a8c0aa8397b7e7de81ee22.zip
Initial commit of a library that can talk OBD2 over a remote connection (be it Bluetooth, serial, a file, ...)
This commit includes the basic architecture to be able to send live and freeze frame commands and decode the response as either an integer or a floating-point value. Follow-up work will include: a) providing real connection implementations (e.g. Bluetooth); b) providing support for more commands; c) providing helpers to create frames in a format compatible with VehiclePropValue. Test: run the following commands in a shell: runtest -x packages/services/Car/tests/obd2_test/src/com/android/car/obd2/test/Obd2ConnectionTest.java runtest -x packages/services/Car/tests/obd2_test/src/com/android/car/obd2/test/IntegerArrayStreamTest.java Change-Id: I1def34e514f7c61ebb1856703953b3168e951e88
Diffstat (limited to 'obd2-lib')
-rw-r--r--obd2-lib/Android.mk31
-rw-r--r--obd2-lib/AndroidManifest.xml21
-rw-r--r--obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java95
-rw-r--r--obd2-lib/src/com/android/car/obd2/Obd2Command.java157
-rw-r--r--obd2-lib/src/com/android/car/obd2/Obd2Connection.java247
-rw-r--r--obd2-lib/src/com/android/car/obd2/commands/AmbientAirTemperature.java36
-rw-r--r--obd2-lib/src/com/android/car/obd2/commands/EngineOilTemperature.java34
7 files changed, 621 insertions, 0 deletions
diff --git a/obd2-lib/Android.mk b/obd2-lib/Android.mk
new file mode 100644
index 00000000..544dfc14
--- /dev/null
+++ b/obd2-lib/Android.mk
@@ -0,0 +1,31 @@
1# Copyright (C) 2017 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15#
16
17#disble build in PDK, missing aidl import breaks build
18ifneq ($(TARGET_BUILD_PDK),true)
19
20LOCAL_PATH:= $(call my-dir)
21
22include $(CLEAR_VARS)
23
24LOCAL_SRC_FILES := $(call all-java-files-under, src)
25
26LOCAL_MODULE := com.android.car.obd2
27LOCAL_JAVA_LANGUAGE_VERSION := 1.8
28
29include $(BUILD_STATIC_JAVA_LIBRARY)
30
31endif #TARGET_BUILD_PDK
diff --git a/obd2-lib/AndroidManifest.xml b/obd2-lib/AndroidManifest.xml
new file mode 100644
index 00000000..ea69d059
--- /dev/null
+++ b/obd2-lib/AndroidManifest.xml
@@ -0,0 +1,21 @@
1<?xml version="1.0" encoding="utf-8"?>
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<manifest xmlns:android="http://schemas.android.com/apk/res/android"
18 xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
19 package="com.android.car.obd2" >
20 <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
21</manifest>
diff --git a/obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java b/obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java
new file mode 100644
index 00000000..56f64f77
--- /dev/null
+++ b/obd2-lib/src/com/android/car/obd2/IntegerArrayStream.java
@@ -0,0 +1,95 @@
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
17package com.android.car.obd2;
18
19import java.util.function.Function;
20
21/**
22 * A wrapper over an int[] that offers a moving offset into the array, allowing for sequential
23 * consumption of the array data
24 */
25public class IntegerArrayStream {
26 private final int[] mData;
27 private int mIndex;
28
29 public IntegerArrayStream(int[] data) {
30 mData = data;
31 mIndex = 0;
32 }
33
34 public int peek() {
35 return mData[mIndex];
36 }
37
38 public int consume() {
39 return mData[mIndex++];
40 }
41
42 public int residualLength() {
43 return mData.length - mIndex;
44 }
45
46 public boolean hasAtLeast(int n) {
47 return residualLength() >= n;
48 }
49
50 public <T> T hasAtLeast(int n, Function<IntegerArrayStream, T> ifTrue) {
51 return hasAtLeast(n, ifTrue, null);
52 }
53
54 public <T> T hasAtLeast(
55 int n,
56 Function<IntegerArrayStream, T> ifTrue,
57 Function<IntegerArrayStream, T> ifFalse) {
58 if (hasAtLeast(n)) {
59 return ifTrue.apply(this);
60 } else {
61 if (ifFalse != null) {
62 return ifFalse.apply(this);
63 } else {
64 return null;
65 }
66 }
67 }
68
69 /**
70 * Validates the content of this stream against an expected data-set.
71 *
72 * <p>If any element of values causes a mismatch, that element will not be consumed and this
73 * method will return false. All elements that do match are consumed from the stream.
74 *
75 * <p>For instance, given a stream with {1,2,3,4}, a call of expect(1,2,5) will consume 1 and 2,
76 * will return false, and stream.peek() will return 3 since it is the first element that did not
77 * match and was not consumed.
78 *
79 * @param values The values to compare this stream's elements against.
80 * @return true if all elements of values match this stream, false otherwise.
81 */
82 public boolean expect(int... values) {
83 if (!hasAtLeast(values.length)) {
84 return false;
85 }
86 for (int value : values) {
87 if (value != peek()) {
88 return false;
89 } else {
90 consume();
91 }
92 }
93 return true;
94 }
95}
diff --git a/obd2-lib/src/com/android/car/obd2/Obd2Command.java b/obd2-lib/src/com/android/car/obd2/Obd2Command.java
new file mode 100644
index 00000000..3cbbf58e
--- /dev/null
+++ b/obd2-lib/src/com/android/car/obd2/Obd2Command.java
@@ -0,0 +1,157 @@
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
17package com.android.car.obd2;
18
19import com.android.car.obd2.commands.AmbientAirTemperature;
20import com.android.car.obd2.commands.EngineOilTemperature;
21import java.io.IOException;
22import java.util.HashMap;
23import java.util.Optional;
24import java.util.Set;
25
26/**
27 * Base class of OBD2 command objects that query a "vehicle" and return an individual data point
28 * represented as a Java type.
29 *
30 * @param <ValueType> The Java type that represents the value of this command's output.
31 */
32public abstract class Obd2Command<ValueType> {
33
34 /**
35 * Abstract representation of an object whose job it is to receive the bytes read from the OBD2
36 * connection and return a Java representation of a command's value.
37 *
38 * @param <ValueType>
39 */
40 public interface OutputSemanticHandler<ValueType> {
41 int getPid();
42
43 Optional<ValueType> consume(IntegerArrayStream data);
44 }
45
46 public static final int LIVE_FRAME = 1;
47 public static final int FREEZE_FRAME = 2;
48
49 private static final HashMap<Integer, OutputSemanticHandler<Integer>>
50 SUPPORTED_INTEGER_COMMANDS = new HashMap<>();
51 private static final HashMap<Integer, OutputSemanticHandler<Float>> SUPPORTED_FLOAT_COMMANDS =
52 new HashMap<>();
53
54 private static void addSupportedIntegerCommand(
55 OutputSemanticHandler<Integer> integerOutputSemanticHandler) {
56 SUPPORTED_INTEGER_COMMANDS.put(
57 integerOutputSemanticHandler.getPid(), integerOutputSemanticHandler);
58 }
59
60 private static void addSupportedFloatCommand(
61 OutputSemanticHandler<Float> floatOutputSemanticHandler) {
62 SUPPORTED_FLOAT_COMMANDS.put(
63 floatOutputSemanticHandler.getPid(), floatOutputSemanticHandler);
64 }
65
66 public static Set<Integer> getSupportedIntegerCommands() {
67 return SUPPORTED_INTEGER_COMMANDS.keySet();
68 }
69
70 public static Set<Integer> getSupportedFloatCommands() {
71 return SUPPORTED_FLOAT_COMMANDS.keySet();
72 }
73
74 public static OutputSemanticHandler<Integer> getIntegerCommand(int pid) {
75 return SUPPORTED_INTEGER_COMMANDS.get(pid);
76 }
77
78 public static OutputSemanticHandler<Float> getFloatCommand(int pid) {
79 return SUPPORTED_FLOAT_COMMANDS.get(pid);
80 }
81
82 static {
83 addSupportedFloatCommand(new AmbientAirTemperature());
84 addSupportedIntegerCommand(new EngineOilTemperature());
85 }
86
87 protected final int mMode;
88 protected final OutputSemanticHandler<ValueType> mSemanticHandler;
89
90 Obd2Command(int mode, OutputSemanticHandler<ValueType> semanticHandler) {
91 mMode = mode;
92 mSemanticHandler = semanticHandler;
93 }
94
95 public abstract Optional<ValueType> run(Obd2Connection connection) throws Exception;
96
97 public static final <T> LiveFrameCommand<T> getLiveFrameCommand(OutputSemanticHandler handler) {
98 return new LiveFrameCommand<>(handler);
99 }
100
101 public static final <T> FreezeFrameCommand<T> getFreezeFrameCommand(
102 OutputSemanticHandler handler, int frameId) {
103 return new FreezeFrameCommand<>(handler, frameId);
104 }
105
106 /**
107 * An OBD2 command that returns live frame data.
108 *
109 * @param <ValueType> The Java type that represents the command's result type.
110 */
111 public static class LiveFrameCommand<ValueType> extends Obd2Command<ValueType> {
112 private static final int RESPONSE_MARKER = 0x41;
113
114 LiveFrameCommand(OutputSemanticHandler<ValueType> semanticHandler) {
115 super(LIVE_FRAME, semanticHandler);
116 }
117
118 public Optional<ValueType> run(Obd2Connection connection)
119 throws IOException, InterruptedException {
120 String command = String.format("%02X%02X", mMode, mSemanticHandler.getPid());
121 int[] data = connection.run(command);
122 IntegerArrayStream stream = new IntegerArrayStream(data);
123 if (stream.expect(RESPONSE_MARKER, mSemanticHandler.getPid())) {
124 return mSemanticHandler.consume(stream);
125 }
126 return Optional.empty();
127 }
128 }
129
130 /**
131 * An OBD2 command that returns freeze frame data.
132 *
133 * @param <ValueType> The Java type that represents the command's result type.
134 */
135 public static class FreezeFrameCommand<ValueType> extends Obd2Command<ValueType> {
136 private static final int RESPONSE_MARKER = 0x2;
137
138 private int mFrameId;
139
140 FreezeFrameCommand(OutputSemanticHandler<ValueType> semanticHandler, int frameId) {
141 super(FREEZE_FRAME, semanticHandler);
142 mFrameId = frameId;
143 }
144
145 public Optional<ValueType> run(Obd2Connection connection)
146 throws IOException, InterruptedException {
147 String command =
148 String.format("%02X%02X %02X", mMode, mSemanticHandler.getPid(), mFrameId);
149 int[] data = connection.run(command);
150 IntegerArrayStream stream = new IntegerArrayStream(data);
151 if (stream.expect(RESPONSE_MARKER, mSemanticHandler.getPid(), mFrameId)) {
152 return mSemanticHandler.consume(stream);
153 }
154 return Optional.empty();
155 }
156 }
157}
diff --git a/obd2-lib/src/com/android/car/obd2/Obd2Connection.java b/obd2-lib/src/com/android/car/obd2/Obd2Connection.java
new file mode 100644
index 00000000..62b070a0
--- /dev/null
+++ b/obd2-lib/src/com/android/car/obd2/Obd2Connection.java
@@ -0,0 +1,247 @@
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
17package com.android.car.obd2;
18
19import android.util.Log;
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.OutputStream;
23import java.util.HashSet;
24import java.util.Objects;
25import java.util.Set;
26
27/** This class represents a connection between Java code and a "vehicle" that talks OBD2. */
28public 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 InputStream getInputStream();
40
41 OutputStream getOutputStream();
42 }
43
44 private final UnderlyingTransport mConnection;
45
46 private static final String[] initCommands =
47 new String[] {"ATD", "ATZ", "AT E0", "AT L0", "AT S0", "AT H0", "AT SP 0"};
48
49 public Obd2Connection(UnderlyingTransport connection) {
50 mConnection = Objects.requireNonNull(connection);
51 runInitCommands();
52 }
53
54 public String getAddress() {
55 return mConnection.getAddress();
56 }
57
58 private void runInitCommands() {
59 for (final String initCommand : initCommands) {
60 try {
61 runImpl(initCommand);
62 } catch (IOException | InterruptedException e) {
63 }
64 }
65 }
66
67 public boolean reconnect() {
68 if (!mConnection.reconnect()) return false;
69 runInitCommands();
70 return true;
71 }
72
73 static int toDigitValue(char c) {
74 if ((c >= '0') && (c <= '9')) return c - '0';
75 switch (c) {
76 case 'a':
77 case 'A':
78 return 10;
79 case 'b':
80 case 'B':
81 return 11;
82 case 'c':
83 case 'C':
84 return 12;
85 case 'd':
86 case 'D':
87 return 13;
88 case 'e':
89 case 'E':
90 return 14;
91 case 'f':
92 case 'F':
93 return 15;
94 default:
95 throw new IllegalArgumentException(c + " is not a valid hex digit");
96 }
97 }
98
99 int[] toHexValues(String buffer) {
100 int[] values = new int[buffer.length() / 2];
101 for (int i = 0; i < values.length; ++i) {
102 values[i] =
103 16 * toDigitValue(buffer.charAt(2 * i))
104 + toDigitValue(buffer.charAt(2 * i + 1));
105 }
106 return values;
107 }
108
109 private String runImpl(String command) throws IOException, InterruptedException {
110 InputStream in = Objects.requireNonNull(mConnection.getInputStream());
111 OutputStream out = Objects.requireNonNull(mConnection.getOutputStream());
112
113 out.write((command + "\r").getBytes());
114 out.flush();
115
116 StringBuilder response = new StringBuilder();
117 while (true) {
118 int value = in.read();
119 if (value < 0) continue;
120 char c = (char) value;
121 // this is the prompt, stop here
122 if (c == '>') break;
123 if (c == '\r' || c == '\n' || c == ' ' || c == '\t' || c == '.') continue;
124 response.append(c);
125 }
126
127 String responseValue = response.toString();
128 return responseValue;
129 }
130
131 String removeSideData(String response, String... patterns) {
132 for (String pattern : patterns) {
133 if (response.contains(pattern)) response = response.replaceAll(pattern, "");
134 }
135 return response;
136 }
137
138 public int[] run(String command) throws IOException, InterruptedException {
139 String responseValue = runImpl(command);
140 String originalResponseValue = responseValue;
141 if (responseValue.startsWith(command))
142 responseValue = responseValue.substring(command.length());
143 //TODO(egranata): should probably handle these intelligently
144 responseValue =
145 removeSideData(
146 responseValue,
147 "SEARCHING",
148 "ERROR",
149 "BUS INIT",
150 "BUSINIT",
151 "BUS ERROR",
152 "BUSERROR",
153 "STOPPED");
154 if (responseValue.equals("OK")) return new int[] {1};
155 if (responseValue.equals("?")) return new int[] {0};
156 if (responseValue.equals("NODATA")) return new int[] {};
157 if (responseValue.equals("UNABLETOCONNECT")) throw new IOException("connection failure");
158 try {
159 return toHexValues(responseValue);
160 } catch (IllegalArgumentException e) {
161 Log.e(
162 "OBD2",
163 String.format(
164 "conversion error: command: '%s', original response: '%s'"
165 + ", processed response: '%s'",
166 command, originalResponseValue, responseValue));
167 throw e;
168 }
169 }
170
171 static class FourByteBitSet {
172 private static final int[] masks =
173 new int[] {
174 0b0000_0001,
175 0b0000_0010,
176 0b0000_0100,
177 0b0000_1000,
178 0b0001_0000,
179 0b0010_0000,
180 0b0100_0000,
181 0b1000_0000
182 };
183
184 private final byte mByte0;
185 private final byte mByte1;
186 private final byte mByte2;
187 private final byte mByte3;
188
189 FourByteBitSet(byte b0, byte b1, byte b2, byte b3) {
190 mByte0 = b0;
191 mByte1 = b1;
192 mByte2 = b2;
193 mByte3 = b3;
194 }
195
196 private byte getByte(int index) {
197 switch (index) {
198 case 0:
199 return mByte0;
200 case 1:
201 return mByte1;
202 case 2:
203 return mByte2;
204 case 3:
205 return mByte3;
206 default:
207 throw new IllegalArgumentException(index + " is not a valid byte index");
208 }
209 }
210
211 private boolean getBit(byte b, int index) {
212 if (index < 0 || index >= masks.length)
213 throw new IllegalArgumentException(index + " is not a valid bit index");
214 return 0 != (b & masks[index]);
215 }
216
217 public boolean getBit(int b, int index) {
218 return getBit(getByte(b), index);
219 }
220 }
221
222 public Set<Integer> getSupportedPIDs() throws IOException, InterruptedException {
223 Set<Integer> result = new HashSet<>();
224 String[] pids = new String[] {"0100", "0120", "0140", "0160"};
225 int basePid = 0;
226 for (String pid : pids) {
227 int[] responseData = run(pid);
228 if (responseData.length >= 6) {
229 byte byte0 = (byte) (responseData[2] & 0xFF);
230 byte byte1 = (byte) (responseData[3] & 0xFF);
231 byte byte2 = (byte) (responseData[4] & 0xFF);
232 byte byte3 = (byte) (responseData[5] & 0xFF);
233 FourByteBitSet fourByteBitSet = new FourByteBitSet(byte0, byte1, byte2, byte3);
234 for (int byteIndex = 0; byteIndex < 4; ++byteIndex) {
235 for (int bitIndex = 7; bitIndex >= 0; --bitIndex) {
236 if (fourByteBitSet.getBit(byteIndex, bitIndex)) {
237 result.add(basePid + 8 * byteIndex + 7 - bitIndex);
238 }
239 }
240 }
241 }
242 basePid += 0x20;
243 }
244
245 return result;
246 }
247}
diff --git a/obd2-lib/src/com/android/car/obd2/commands/AmbientAirTemperature.java b/obd2-lib/src/com/android/car/obd2/commands/AmbientAirTemperature.java
new file mode 100644
index 00000000..30108468
--- /dev/null
+++ b/obd2-lib/src/com/android/car/obd2/commands/AmbientAirTemperature.java
@@ -0,0 +1,36 @@
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
17package com.android.car.obd2.commands;
18
19import com.android.car.obd2.IntegerArrayStream;
20import com.android.car.obd2.Obd2Command;
21import java.util.Optional;
22
23public class AmbientAirTemperature implements Obd2Command.OutputSemanticHandler<Float> {
24 @Override
25 public int getPid() {
26 return 0x46;
27 }
28
29 @Override
30 public Optional<Float> consume(IntegerArrayStream data) {
31 return data.hasAtLeast(
32 1,
33 theData -> Optional.of(theData.consume() * (100.0f / 255.0f)),
34 theData -> Optional.empty());
35 }
36}
diff --git a/obd2-lib/src/com/android/car/obd2/commands/EngineOilTemperature.java b/obd2-lib/src/com/android/car/obd2/commands/EngineOilTemperature.java
new file mode 100644
index 00000000..9db714cb
--- /dev/null
+++ b/obd2-lib/src/com/android/car/obd2/commands/EngineOilTemperature.java
@@ -0,0 +1,34 @@
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
17package com.android.car.obd2.commands;
18
19import com.android.car.obd2.IntegerArrayStream;
20import com.android.car.obd2.Obd2Command;
21import java.util.Optional;
22
23public class EngineOilTemperature implements Obd2Command.OutputSemanticHandler<Integer> {
24 @Override
25 public int getPid() {
26 return 0x5C;
27 }
28
29 @Override
30 public Optional<Integer> consume(IntegerArrayStream data) {
31 return data.hasAtLeast(
32 1, theData -> Optional.of(theData.consume() - 40), theData -> Optional.empty());
33 }
34}