/* CanZE Take a closer look at your ZE car Copyright (C) 2015 - The CanZE Team http://canze.fisch.lu 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 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, see <http://www.gnu.org/licenses/>. */ package lu.fisch.canze.devices; import java.io.IOException; import java.util.Calendar; import lu.fisch.canze.activities.MainActivity; import lu.fisch.canze.actors.Ecu; import lu.fisch.canze.actors.Ecus; import lu.fisch.canze.actors.Frame; import lu.fisch.canze.actors.Message; import lu.fisch.canze.bluetooth.BluetoothManager; /** * Created by robertfisch on 07.09.2015. * Main loop fir ELM */ public class ELM327 extends Device { // *** needed by the "decoder" part of this device //private String buffer = ""; //private final String SEPARATOR = "\r\n"; // define the Timeout we may wait to get an answer private static int DEFAULT_TIMEOUT = 500; private static int MINIMUM_TIMEOUT = 100; private int generalTimeout = 500; // define End Of Message for this type of reader private static final char EOM1 = '\r'; private static final char EOM2 = '>'; private static final char EOM3 = '?'; private boolean deviceIsInitialized = false; /** * the index of the actual field to request */ private int lastId = 0; private boolean lastCommandWasFreeFrame = false; @Override public void registerFilter(int frameId) { // not needed for this device } @Override public void unregisterFilter(int frameId) { // not needed for this device } protected boolean initDevice (int toughness, int retries) { if (initDevice(toughness)) return true; while (retries-- > 0) { MainActivity.debug("ELM327: flushWithTimeout"); flushWithTimeout(500); MainActivity.debug("ELM327: initDevice("+toughness+"), "+retries+" retries left"); if (initDevice(toughness)) return true; } MainActivity.toast(MainActivity.TOAST_ELM, "Hard reset failed, restarting Bluetooth ..."); MainActivity.debug("ELM327: Hard reset failed, restarting Bluetooth ..."); ///----- WE ARE HERE INSIDE THE POLLER THREAD, SO ///----- JOINING CAN'T WORK! // ... but we don't want the next request to happen, // so we need to stop the poller here anyway, but // DO NOT JOIN IT! setPollerActive(false); (new Thread(new Runnable() { @Override public void run() { // -- give up and restart BT // stop BT without resetting the registered fields MainActivity.debug("ELM327: stopBluetooth (via MainActivity)"); MainActivity.getInstance().stopBluetooth(false); // restart BT without reloading all settings MainActivity.debug("ELM327: reloadBluetooth (via MainActivity)"); MainActivity.getInstance().reloadBluetooth(false); } })).start(); return false; } public boolean initDevice(int toughness) { MainActivity.debug("ELM327: initDevice ("+toughness+")"); String response; int elmVersion = 0; lastInitProblem = ""; // ensure the dongle header field is set again lastId = 0; // extremely soft, just clear the global error condition if (toughness == TOUGHNESS_NONE){ deviceIsInitialized = true; return deviceIsInitialized; } killCurrentOperation (); if (toughness == TOUGHNESS_HARD ) { // the default 500mS should be enough to answer, however, the answer contains various <cr>'s, so we need to set untilEmpty to true response = sendAndWaitForAnswer("atws", 0, true, -1 , true); MainActivity.debug("ELM327: version = "+response); } else if (toughness == TOUGHNESS_MEDIUM) { response = sendAndWaitForAnswer("atws", 0, true, -1 , true); MainActivity.debug("ELM327: version = "+response); } else { // TOUGHNESS_WEAK // not used response = sendAndWaitForAnswer("atd", 0, true, -1 , true); MainActivity.debug("ELM327: version = "+response); } if (response.trim().equals("")) { lastInitProblem = "ELM is not responding (toughness = "+toughness+")"; MainActivity.toast(MainActivity.TOAST_ELM, lastInitProblem); return false; } // only do version control at a full reset if (toughness <= TOUGHNESS_MEDIUM) { if (response.toUpperCase().contains("V1.3")) { elmVersion = 13; } else if (response.toUpperCase().contains("V1.4")) { elmVersion = 14; } else if (response.toUpperCase().contains("V1.5")) { elmVersion = 15; } else if (response.toUpperCase().contains("V2.")) { elmVersion = 20; } else if (response.toUpperCase().contains("INNOCAR")) { elmVersion = 8015; } else { lastInitProblem = "Unrecognized ELM version response [" + response.replace("\r", "<cr>").replace(" ", "<sp>") + "]"; MainActivity.toast(MainActivity.TOAST_ELM, lastInitProblem); return false; } } // at this point, echo is still on (except when atd was issued), so we still need to absorb the echoed command // ate0 (no echo) if (!initCommandExpectOk("ate0", true)) { lastInitProblem = "ATE0 command problem"; deviceIsInitialized = false; return deviceIsInitialized; } // at this point, echo is finally off so we can safely check for OK messages. If the app starts responding with toasts showing the responses // in brackets equal to the commands, somehow the echo was not executed. so maybe we need to check for that specific condition in the next // command. // ats0 (no spaces) if (!initCommandExpectOk("ats0")) { lastInitProblem = "ATS0 command problem"; deviceIsInitialized = false; return deviceIsInitialized; } // atsp6 (CAN 500K 11 bit) if (!initCommandExpectOk("atsp6")) { lastInitProblem = "ATSP6 command problem"; deviceIsInitialized = false; return deviceIsInitialized; } // atat1 (auto timing) if (!initCommandExpectOk("atat1")) { lastInitProblem = "ATAT1 command problem"; return false; } // atcaf0 (no formatting) if (!initCommandExpectOk("atcaf0")) { lastInitProblem = "ATCAF0 command problem"; deviceIsInitialized = false; return deviceIsInitialized; } // atfcsh77b Set flow control response ID to 77b. This is needed to set the flow control response, but that one is remembered :-) if (!initCommandExpectOk("atfcsh77b")) { lastInitProblem = "ATFCSH77B command problem"; deviceIsInitialized = false; return deviceIsInitialized; } // atfcsd300020 Set the flow control response data to 300010 (flow control, clear to send, // all frames, 16 ms wait between frames. Note that it is not possible to let // the ELM request each frame as the Altered Flow Control only responds to a // First Frame (not a Next Frame) if (!initCommandExpectOk("atfcsd300010")) { lastInitProblem = "ATFCSD300010 command problem"; deviceIsInitialized = false; return deviceIsInitialized; } // atfcsm1 Set flow control mode 1 (ID and data suplied) if (!initCommandExpectOk("atfcsm1")) { lastInitProblem = "ATFCSM1 command problem"; deviceIsInitialized = false; return deviceIsInitialized; } if (toughness == TOUGHNESS_HARD ) { switch (elmVersion) { case 13: MainActivity.toast(MainActivity.TOAST_ELM, "ELM ready, version 1.3, should work"); break; case 14: MainActivity.toast(MainActivity.TOAST_ELM, "ELM ready, version 1.4, should work"); break; case 15: MainActivity.toast(MainActivity.TOAST_ELM, "ELM is now ready"); break; case 20: lastInitProblem = "ELM ready, version 2.x, will probably not work, please report if it does"; MainActivity.toast(MainActivity.TOAST_ELM, lastInitProblem); break; case 8015: MainActivity.toast(MainActivity.TOAST_ELM, "ELM ready, version innocar, should work"); break; // default should never be reached!! default: lastInitProblem = "ELM ready, unknown version, will probably not work, please report if it does"; MainActivity.toast(MainActivity.TOAST_ELM, lastInitProblem); break; } } deviceIsInitialized = true; return deviceIsInitialized; } private void killCurrentOperation() { // ensure any running operation is stopped // sending a return might restart the last command. Bad plan. sendNoWait("x"); // discard everything that still comes in flushWithTimeoutCore (200, '\0'); // if a command was running, it is interrupted now and the ELM is waiting for a command. However, if there was no command running, the x // in the buffer will screw up the next command. There are two possibilities: Sending a Backspace and hope for the best, or sending x <CR> // and being sure the ELM will report an unknow command (prompt a ? mark), as it will be processing either x <CR> or xx <CR>. We choose the latter // discard the ? anser sendNoWait("x\r"); if (!flushWithTimeoutCore (500, '\0')) { MainActivity.debug("ELM327: KillCurrentOperation unable to flush after x"); } } private void flushWithTimeout(int timeout) { flushWithTimeout(timeout, '\0'); } private void flushWithTimeout(int timeout, char eom) { if (flushWithTimeoutCore(timeout, eom)) return; killCurrentOperation (); } private boolean flushWithTimeoutCore(int timeout, char eom) { // empty incoming buffer // just make sure there is no previous response // the ELM might be in a mode where it is spewing out data, and that might put this // mothod in an endless loop. If there are more than 200 character to flushed, return false // this should normally be followed by a failure and thus device re-initialisation int count = 100; try { // fast track, don't use expensive calendar..... if (timeout == 0) { while (BluetoothManager.getInstance().isConnected() && BluetoothManager.getInstance().available() > 0) { BluetoothManager.getInstance().read(); if (count-- == 0) return false; } } else { long end = Calendar.getInstance().getTimeInMillis() + timeout; while (Calendar.getInstance().getTimeInMillis() < end) { // read a byte if (!BluetoothManager.getInstance().isConnected()) return false; if (BluetoothManager.getInstance().available() > 0) { // absorb the characters while (BluetoothManager.getInstance().available() > 0) { int c = BluetoothManager.getInstance().read(); if (c == (int)eom) return true; if (count-- == 0) return false; } // restart the timer end = Calendar.getInstance().getTimeInMillis() + timeout; } else { // let the system breath if there was no data Thread.sleep(5); } } } } catch (IOException | InterruptedException e) { // ignore } return true; } private boolean initCommandExpectOk (String command) { return initCommandExpectOk(command, false, true); } private boolean initCommandExpectOk (String command, boolean untilEmpty) { return initCommandExpectOk(command, untilEmpty, true); } private boolean initCommandExpectOk (String command, boolean untilEmpty, boolean addReturn) { MainActivity.debug("ELM327: initCommandExpectOk [" + command + "] untilempty [" + untilEmpty + "]"); String response = ""; for (int i = 2; i > 0; i--) { if (untilEmpty) { response = sendAndWaitForAnswer(command, 40, true, -1, addReturn); // wait 40 ms for untilempty } else { response = sendAndWaitForAnswer(command, 0, false, -1, addReturn); // // just one line } if (response.toUpperCase().contains("OK")) return true; // we're done if we got an OK } // we've tried and tried and failed here /* if (timeoutLogLevel >= 2 || (timeoutLogLevel >= 1 && !command.startsWith("atma") && command.startsWith("at"))) { MainActivity.toast("Err " + command + " [" + response.replace("\r", "<cr>").replace(" ", "<sp>") + "]"); } */ MainActivity.toast(MainActivity.TOAST_ELM, "Error [" + command + "] [" + response.replace("\r", "<cr>").replace(" ", "<sp>") + "]"); MainActivity.debug("ELM327: initCommandExpectOk > Response was > " + response); return false; } private void sendNoWait(String command) { if(!BluetoothManager.getInstance().isConnected()) return; if(command!=null) { BluetoothManager.getInstance().write(command); } } private String sendAndWaitForAnswer(String command, int waitMillis) { return sendAndWaitForAnswer(command,waitMillis,false,-1, true); } private String sendAndWaitForAnswer(String command, int waitMillis, int answerLinesCount) { return sendAndWaitForAnswer(command,waitMillis,false,answerLinesCount, true); } private String sendAndWaitForAnswer(String command, int waitMillis, boolean untilEmpty) { return sendAndWaitForAnswer(command,waitMillis,untilEmpty,-1, true); } private String sendAndWaitForAnswer(String command, int waitMillis, boolean untilEmpty, int answerLinesCount, boolean addReturn) { int maxUntilEmptyCounter = 10; int maxLengthCounter = 500; // char = nibble, so 2000 bits if(!BluetoothManager.getInstance().isConnected()) return ""; if(command!=null) { flushWithTimeout (10); // send the command //connectedBluetoothThread.write(command + "\r\n"); BluetoothManager.getInstance().write(command + (addReturn ? "\r" : "")); } //MainActivity.debug("Send > "+command); // wait if needed (JM: tbh, I think waiting here is never needed. Any waiting should be handled in the wait for an answer timeout. But that's me. /* if(waitMillis>0) { try { Thread.sleep(waitMillis); } catch (InterruptedException e) { e.printStackTrace(); } } */ // init the buffer boolean stop = false; String readBuffer = ""; // wait for answer long end = Calendar.getInstance().getTimeInMillis() + generalTimeout; boolean timedOut = false; while(!stop && !timedOut) { //MainActivity.debug("Delta = "+(Calendar.getInstance().getTimeInMillis()-start)); try { // read a byte if(BluetoothManager.getInstance().isConnected() && BluetoothManager.getInstance().available()>0) { //MainActivity.debug("Reading ..."); int data = BluetoothManager.getInstance().read(); //MainActivity.debug("... done"); // if it is a real one if (data != -1) { // we might be JUST approaching the generalTimeout, so give it a chance to get to the EOM, // end = end + 2; // convert it to a character char ch = (char) data; // add it to the readBuffer readBuffer += ch; // if we reach the end of a line if (ch == EOM1 || ch == EOM2 || ch == EOM3) { //MainActivity.debug("ALC: "+answerLinesCount+")\n"+readBuffer); // decrease awaiting answer lines answerLinesCount--; // if we not asked to keep on and we got enough lines, stop if(!untilEmpty){ if(answerLinesCount<=0) { // the number of lines is in MainActivity.debug("ELM327: sendAndWaitForAnswer > stop on decimal char [" + data + "]"); stop = true; // so quit } else // the number of lines is NOT in { end = Calendar.getInstance().getTimeInMillis() + generalTimeout; // so restart the timeout } } else { // if (untilEmpty) { stop=(BluetoothManager.getInstance().available()==0); // a problem here is that we assume the next character is already available, which might not be the case, so adding..... if (stop) { // wait a fraction try { Thread.sleep(50); } catch (InterruptedException e) { // do nothing } stop=(BluetoothManager.getInstance().available()==0); } else { if (--maxUntilEmptyCounter <= 0) timedOut = true; // well, this is a timed"In", as in, too many lines } } } else { if (--maxLengthCounter <= 0) timedOut = true; // well, this is a timed"In", as in, too many lines } } } else { // let the system breath if there was no data try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } if (Calendar.getInstance().getTimeInMillis() > end) { timedOut = true; // MainActivity.toast("Sum Ting Wong on command " + command); } } catch (IOException e) { // ignore: e.printStackTrace(); } } // set the flag that a timeout has occurred. someThingWrong can be inspected anywhere, but we reset the device after a full filter has been run if (timedOut) { /* if (timeoutLogLevel >= 2 || (timeoutLogLevel >= 1 && (command==null || (!command.startsWith("atma") && command.startsWith("at"))))) { MainActivity.toast("Timeout on [" + command + "][" + readBuffer.replace("\r", "<cr>").replace(" ", "<sp>") + "]"); } */ MainActivity.toast(MainActivity.TOAST_ELM, "Timeout on [" + command + "] [" + readBuffer.replace("\r", "<cr>").replace(" ", "<sp>") + "]"); MainActivity.debug("ELM327: sendAndWaitForAnswer > timed out on [" + command + "] [" + readBuffer.replace("\r", "<cr>").replace(" ", "<sp>") + "]"); return (""); } //MainActivity.debug("ALC: "+answerLinesCount+" && Stop: "+stop+" && Delta: "+(Calendar.getInstance().getTimeInMillis()-start)); //MainActivity.debug("Recv < "+readBuffer); return readBuffer; } private int getToId(int fromId) { Ecu ecu = Ecus.getInstance().getByFromId(fromId); return ecu != null ? ecu.getToId() : 0; } private String getToIdHex(int fromId) { return Integer.toHexString(getToId(fromId)); } @Override public void clearFields() { super.clearFields(); //fieldIndex=0; } @Override public Message requestFreeFrame(Frame frame) { if (!deviceIsInitialized) {return new Message(frame, "-E-Re-initialisation needed", true); } String hexData; // ensure the ATCRA filter is reset in the next NON free frame request lastCommandWasFreeFrame = true; // EML needs the filter to be 3 symbols and contains the from CAN id of the ECU String emlFilter = frame.getHexId() + ""; while (emlFilter.length() < 3) emlFilter = "0" + emlFilter; MainActivity.debug("ELM327: requestFreeFrame: atcra" + emlFilter); if (!initCommandExpectOk("atcra" + emlFilter)) return new Message(frame, "-E-Problem sending atcra command", true); //sendAndWaitForAnswer("atcra" + emlFilter, 400); // atma (wait for one answer line) generalTimeout = (int) (frame.getInterval() * intervalMultiplicator + 50); if (generalTimeout < MINIMUM_TIMEOUT) generalTimeout = MINIMUM_TIMEOUT; MainActivity.debug("ELM327: requestFreeFrame > TIMEOUT = "+ generalTimeout); hexData = sendAndWaitForAnswer("atma", 20); MainActivity.debug("ELM327: requestFreeFrame > hexData = [" + hexData + "]"); // the dongle starts babbling now. sendAndWaitForAnswer should stop at the first full line // ensure any running operation is stopped // sending a return might restart the last command. Bad plan. sendNoWait("x"); // let it settle down, the ELM should indicate STOPPED then prompt > flushWithTimeout(100, '>'); generalTimeout = DEFAULT_TIMEOUT; // atar (clear filter) // AM has suggested the atar might not be neccesary as it might only influence cra filters and they are always set // however, make sure proper flushing is done // if cra does influence ISO-TP requests, an small optimization might be to only sending an atar when switching from free // frames to isotp frames. // if (!initCommandExpectOk("atar")) someThingWrong |= true; hexData = hexData.trim(); if(hexData.equals("")) return new Message(frame, "-E-data empty", true); else return new Message(frame, hexData, false); } @Override public Message requestIsoTpFrame(Frame frame) { if (!deviceIsInitialized) {return new Message(frame, "-E-Re-initialisation needed", true); } String hexData; int len = 0; // PERFORMANCE ENHANCEMENT: only send ATAR if coming from a free frame if (lastCommandWasFreeFrame) { // atar (clear filter set by free frame capture method) if (!initCommandExpectOk("atar")){ return new Message(frame, "-E-Problem sending atar command", true); } lastCommandWasFreeFrame = false; } // PERFORMANCE ENHANCEMENT II: lastId contains the CAN id of the previous ISO-TP command. If the current ID is the same, no need to re-address that ECU lastId = 0; if (lastId != frame.getId()) { lastId = frame.getId(); // request contains the to CAN id of the ECU. Note that we store the FromId in a frame String toIdHex = getToIdHex(frame.getId()); // Set header if (!initCommandExpectOk("atsh" + toIdHex)) return new Message(frame, "-E-Problem sending atsh command", true); // Set flow control response ID if (!initCommandExpectOk("atfcsh" + toIdHex)) return new Message(frame, "-E-Problem sending atfcsh command", true); } // 022104 ISO-TP single frame - length 2 - payload 2104, which means PID 21 (??), id 04 (see first tab). String elmCommand = "0" + (frame.getRequestId().length() / 2) + frame.getRequestId(); //MainActivity.debug("R: "+request+" - C: "+pre+field.getToId()); // get 0x1 frame. No delays, and no waiting until done. String elmResponse = sendAndWaitForAnswer(elmCommand, 0, false).replace("\r", ""); if (elmResponse.compareTo("CAN ERROR") == 0) { return new Message(frame, "-E-Can Error", true); } if (elmResponse.compareTo("?") == 0) { return new Message(frame, "-E-Unknown command", true); } if (elmResponse.compareTo("") == 0) { return new Message(frame, "-E-Empty result", true); } // process first line (SINGLE or FIRST frame) elmResponse = elmResponse.trim(); // clean-up if there is mess around if (elmResponse.startsWith(">")) elmResponse = elmResponse.substring(1); if (elmResponse.isEmpty()) { return new Message(frame, "-E-unexpected ISO-TP 1st line empty", true); } // get type (first nibble) switch (elmResponse.substring(0, 1)) { case "0": // SINGLE frame len = Integer.parseInt(elmResponse.substring(1, 2), 16); // remove 2 nibbles (type + length) hexData = elmResponse.substring(2); break; case "1": // FIRST frame len = Integer.parseInt(elmResponse.substring(1, 4), 16); // remove 4 nibbles (type + length) hexData = elmResponse.substring(4); // calculate the # of frames to come. 6 byte are in and each of the 0x2 frames has a payload of 7 bytes int framesToReceive = len / 7; // read this as ((len - 6 [remaining characters]) + 6 [offset to / 7, so 0->0, 1-7->7, etc]) / 7 // get remaining 0x2 (NEXT) frames String lines0x1 = sendAndWaitForAnswer(null, 0, framesToReceive); // split into lines //String[] hexDataLines = lines0x1.split(String.valueOf(EOM1)); String[] hexDataLines = lines0x1.replaceAll("\n", "\r").split("[\\r]+"); for (String hexDataLine : hexDataLines) { elmResponse = hexDataLine; //MainActivity.debug("Line "+(i+1)+": " + line); if (!elmResponse.isEmpty() && elmResponse.length() > 2) { // cut off the first byte (type + sequence) // adding sequence checking would be wise to detect collisions hexData += elmResponse.substring(2); } } break; default: // a NEXT, FLOWCONTROL should not be received. Neither should any other string (such as NO DATA) flushWithTimeout(400, '>'); return new Message(frame, "-E-unexpected ISO-TP 1st nibble of first frame:" + elmResponse, true); } // There was spurious error here, that immediately sending another command STOPPED the still not entirely finished ISO-TP command. // It was probably still sending "OK>" or just ">". So, the next command files and if it was i.e. an atcra f a free frame capture, // the following ATMA immediately overwhelmed the ELM as no filter was set. // As a solution, added this wait for a > after an ISO-TP command. flushWithTimeout(400, '>'); len *= 2; // Having less data than specified in length is actually an error, but at least we do not need so substr it // if there is more data than specified in length, that is OK (filler bytes in the last frame), so cut those away hexData = (hexData.length() <= len) ? hexData.trim() : hexData.substring(0, len).trim(); if (hexData.equals("")) return new Message(frame, "-E-data empty", true); else return new Message(frame, hexData, false); } }