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.os.SystemClock;
20import android.util.JsonWriter;
21import android.util.Log;
22import com.android.car.obd2.Obd2Command.FreezeFrameCommand;
23import com.android.car.obd2.Obd2Command.OutputSemanticHandler;
24import java.io.IOException;
25import java.util.ArrayList;
26import java.util.List;
27import java.util.Optional;
28import java.util.Set;
29
30public 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}
178