/* * enviroCar 2013 * Copyright (C) 2013 * Martin Dueren, Jakob Moellers, Gerald Pape, Christopher Stephan * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ package org.envirocar.obd.adapter.async; import android.util.Base64; import org.envirocar.core.logging.Logger; import org.envirocar.obd.adapter.ResponseQuirkWorkaround; import org.envirocar.obd.commands.PID; import org.envirocar.obd.commands.PIDSupported; import org.envirocar.obd.commands.request.BasicCommand; import org.envirocar.obd.commands.response.DataResponse; import org.envirocar.obd.exception.AdapterSearchingException; import org.envirocar.obd.exception.InvalidCommandResponseException; import org.envirocar.obd.exception.NoDataReceivedException; import org.envirocar.obd.exception.UnmatchedResponseException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Queue; import java.util.Set; public class DriveDeckSportAdapter extends AsyncAdapter { private static final Logger logger = Logger.getLogger(DriveDeckSportAdapter.class); private int pidSupportedResponsesParsed; private int connectingMessageCount; private int totalResponseCount; private boolean supportForLambdaVoltage = true; private static enum Protocol { CAN11500, CAN11250, CAN29500, CAN29250, KWP_SLOW, KWP_FAST, ISO9141 } public static final char CARRIAGE_RETURN = '\r'; public static final char END_OF_LINE_RESPONSE = '>'; private static final char RESPONSE_PREFIX_CHAR = 'B'; private static final char CYCLIC_TOKEN_SEPARATOR_CHAR = '<'; private static final long SEND_CYCLIC_COMMAND_DELTA = 60000; private Protocol protocol; private String vin; private BasicCommand cycleCommand; public long lastCyclicCommandSent; private Set<String> loggedPids = new HashSet<>(); private org.envirocar.obd.commands.response.ResponseParser parser = new org.envirocar.obd.commands.response.ResponseParser(); private Queue<BasicCommand> pendingCommands; private Set<PID> supportedPIDs = new HashSet<>(); public DriveDeckSportAdapter() { super(CARRIAGE_RETURN, END_OF_LINE_RESPONSE); this.pendingCommands = new ArrayDeque<>(); this.pendingCommands.offer(new CarriageReturnCommand()); } @Override protected ResponseQuirkWorkaround getQuirk() { return new PIDSupportedQuirk(); } private void createAndSendCycleCommand() { List<CycleCommand.DriveDeckPID> pidList = new ArrayList<>(); for (PID p: PID.values()) { addIfSupported(p, pidList); } this.cycleCommand = new CycleCommand(pidList); logger.info("Static Cycle Command: " + Base64.encodeToString(this.cycleCommand.getOutputBytes(), Base64.DEFAULT)); this.pendingCommands.offer(this.cycleCommand); /** * as the default we will parse ASCII-PID Response '4D' to Lambda Voltage. * If Lambda Current was found, use that instead */ if (supportedPIDs == null || supportedPIDs.isEmpty()) { supportForLambdaVoltage = true; } else if (supportedPIDs.contains(PID.O2_LAMBDA_PROBE_1_VOLTAGE)) { supportForLambdaVoltage = false; } else { supportForLambdaVoltage = true; } } private void addIfSupported(PID pid, List<CycleCommand.DriveDeckPID> pidList) { CycleCommand.DriveDeckPID driveDeckPID = CycleCommand.DriveDeckPID.fromDefaultPID(pid); if (driveDeckPID == null) { logger.info("No DriveDeck equivalent for PID: "+pid); return; } if (supportedPIDs == null || supportedPIDs.isEmpty()) { pidList.add(driveDeckPID); } else if (supportedPIDs.contains(pid)) { pidList.add(driveDeckPID); } else { logger.info("PID "+pid+" not supported. Skipping."); } } private void processDiscoveredControlUnits(String substring) { logger.info("Discovered CUs... "); } protected void processSupportedPID(byte[] bytes) throws InvalidCommandResponseException, NoDataReceivedException, UnmatchedResponseException, AdapterSearchingException { logger.info("PID Supported response: " + Base64.encodeToString(bytes, Base64.DEFAULT)); if (bytes.length < 14) { logger.info("PID Supported response to small: "+bytes.length); return; } /** * check for group 00 */ String group = new String(new byte[]{bytes[6], bytes[7]}); PIDSupported pidCmd = new PIDSupported(group); byte[] rawBytes = new byte[12]; rawBytes[0] = '4'; rawBytes[1] = '1'; rawBytes[2] = (byte) pidCmd.getGroup().charAt(0); rawBytes[3] = (byte) pidCmd.getGroup().charAt(1); int target = 4; String hexTmp; for (int i = 9; i < bytes.length; i++) { if (i == 11) continue; hexTmp = oneByteToHex(bytes[i]); rawBytes[target++] = (byte) hexTmp.charAt(0); rawBytes[target++] = (byte) hexTmp.charAt(1); } this.supportedPIDs.addAll(pidCmd.parsePIDs(rawBytes)); pidSupportedResponsesParsed++; logger.info("Supported PIDs: "+ this.supportedPIDs); } private String oneByteToHex(byte b) { String result = Integer.toString(b & 0xff, 16).toUpperCase(Locale.US); if (result.length() == 1) result = "0".concat(result); return result; } private void processVIN(String vinInt) { this.vin = vinInt; logger.info("VIN is: " + this.vin); } private void determineProtocol(String protocolInt) { if (protocolInt == null || protocolInt.trim().isEmpty()) { return; } int prot; try { prot = Integer.parseInt(protocolInt); } catch (NumberFormatException e) { logger.warn("NFE: " + e.getMessage()); return; } switch (prot) { case 1: protocol = Protocol.CAN11500; break; case 2: protocol = Protocol.CAN11250; break; case 3: protocol = Protocol.CAN29500; break; case 4: protocol = Protocol.CAN29250; break; case 5: protocol = Protocol.KWP_SLOW; break; case 6: protocol = Protocol.KWP_FAST; break; case 7: protocol = Protocol.ISO9141; break; default: return; } logger.info("Protocol is: " + protocol.toString()); } @Override public boolean supportsDevice(String deviceName) { return deviceName != null && deviceName.toLowerCase().contains("drivedeck") && deviceName.toLowerCase().contains("w4"); } @Override public boolean hasCertifiedConnection() { /** * this is a drivedeck if a VIN response was parsed OR the protocol was communicated * OR the adapter reported the "CONNECTED" state more than x times (unlikely to be a * mistaken other adapter) */ int x = 4; return vin != null || protocol != null || connectingMessageCount > x; } @Override protected boolean hasEstablishedConnection() { return vin != null || protocol != null; } @Override public long getExpectedInitPeriod() { return 30000; } protected Set<PID> getSupportedPIDs() { return supportedPIDs; } private DataResponse parsePIDResponse(String pid, byte[] rawBytes) throws InvalidCommandResponseException, NoDataReceivedException, UnmatchedResponseException, AdapterSearchingException { logger.verbose(String.format("PID Response: %s; %s", pid, Base64.encodeToString(rawBytes, Base64.DEFAULT)).trim()); /* * resulting HEX values are 0x0d additive to the * default PIDs of OBD. e.g. RPM = 0x19 = 0x0c + 0x0d */ PID result = null; if (pid.equals("41")) { //Speed result = PID.SPEED; } else if (pid.equals("42")) { //MAF result = PID.MAF; } else if (pid.equals("52")) { //IAP result = PID.INTAKE_MAP; } else if (pid.equals("49")) { //IAT result = PID.INTAKE_AIR_TEMP; } else if (pid.equals("40")) { //RPM result = PID.RPM; } else if (pid.equals("51")) { //RPM special case: data is stored in bytes 2, 3 result = PID.RPM; rawBytes[0] = rawBytes[2]; rawBytes[1] = rawBytes[3]; } else if (pid.equals("44")) { result = PID.TPS; } else if (pid.equals("45")) { result = PID.CALCULATED_ENGINE_LOAD; } else if (pid.equals("4D")) { //lambda probe if (supportForLambdaVoltage) { result = PID.O2_LAMBDA_PROBE_1_VOLTAGE; } else { result = PID.O2_LAMBDA_PROBE_1_CURRENT; } /** * DriveDeck stores voltage bytes (C, D) in bytes 4, 5 (TODO: Check!) */ rawBytes[2] = rawBytes[4]; rawBytes[3] = rawBytes[5]; } else if (pid.equals("DUMMY")) { //TODO: implement Engine Load, TPS, others } oneTimePIDLog(pid, rawBytes); if (result != null) { byte[] rawData = createRawData(rawBytes, result.getHexadecimalRepresentation()); DataResponse parsed = parser.parse(rawData); return parsed; } return null; } private void oneTimePIDLog(String pid, byte[] rawBytes) { if (pid == null || rawBytes == null || rawBytes.length == 0) return; if (!loggedPids.contains(pid)) { logger.info("First response for PID: " + pid + "; Base64: " + Base64.encodeToString(rawBytes, Base64.DEFAULT)); loggedPids.add(pid); } } private byte[] createRawData(byte[] rawBytes, String type) { byte[] result = new byte[4 + rawBytes.length * 2]; byte[] typeBytes = type.getBytes(); result[0] = (byte) '4'; result[1] = (byte) '1'; result[2] = typeBytes[0]; result[3] = typeBytes[1]; for (int i = 0; i < rawBytes.length; i++) { String hex = oneByteToHex(rawBytes[i]); result[(i * 2) + 4] = (byte) hex.charAt(0); result[(i * 2) + 1 + 4] = (byte) hex.charAt(1); } return result; } @Override protected BasicCommand pollNextCommand() { BasicCommand result = this.pendingCommands.poll(); /** * send the cycle command once in a while to keep the connection alive * TODO: is this required? */ if (result == null && this.protocol != null && System.currentTimeMillis() - lastCyclicCommandSent > SEND_CYCLIC_COMMAND_DELTA) { lastCyclicCommandSent = System.currentTimeMillis(); result = this.cycleCommand; } if (result != null && result == this.cycleCommand) { logger.info("Sending Cyclic command to DriveDeck - data should be received now"); } return result; } @Override protected DataResponse processResponse(byte[] bytes) throws InvalidCommandResponseException, NoDataReceivedException, UnmatchedResponseException, AdapterSearchingException { if (bytes.length <= 0) { return null; } char type = (char) bytes[0]; if (type == RESPONSE_PREFIX_CHAR) { if (bytes.length < 3) { logger.warn("Received a response with too less bytes. length="+bytes.length); return null; } String pid = new String(bytes, 1, 2); /* * METADATA Stuff */ if (pid.equals("14")) { logger.debug("Status: CONNECTING"); connectingMessageCount++; } else if (pid.equals("15")) { processVIN(new String(bytes, 3, bytes.length - 3)); } else if (pid.equals("70")) { processSupportedPID(bytes); } else if (pid.equals("71")) { processDiscoveredControlUnits(new String(bytes, 3, bytes.length - 3)); } else if (pid.equals("31")) { // engine on logger.debug("Engine: On"); } else if (pid.equals("32")) { // engine off (= RPM < 500) logger.debug("Engine: Off"); } else { if (bytes.length < 6) { throw new NoDataReceivedException("the response did only contain " + bytes.length + " bytes. For PID " + "responses 6 are minimum"); } if ((char) bytes[4] == CYCLIC_TOKEN_SEPARATOR_CHAR) { return null; } /* * A PID response */ super.disableQuirk(); byte[] pidResponseValue = new byte[6]; int target = 0; for (int i = 4; i < bytes.length; i++) { if (target >= pidResponseValue.length) { break; } if ((char) bytes[i] == CYCLIC_TOKEN_SEPARATOR_CHAR) { continue; } pidResponseValue[target++] = bytes[i]; } DataResponse result = parsePIDResponse(pid, pidResponseValue); return result; } /** * if the protocol has been determined, wait a fair amount of responses * to ensure that all PIDSupported were reported */ if (this.protocol != null) { this.totalResponseCount++; checkForCycleCommandCreation(); } } else if (type == 'C') { determineProtocol(new String(bytes, 1, bytes.length - 1)); } return null; } private void checkForCycleCommandCreation() { /** * it might be the case that PID supported responses do not come in order (eg group 40 * before 20). So we wait for a few idle responses before going to real-time mode */ if (pidSupportedResponsesParsed > 0 && totalResponseCount > 7 && this.cycleCommand == null) { logger.info("Received PID supported responses and enough responses to start pulling data. Creating cycle command"); createAndSendCycleCommand(); } } }