1286 lines
31 KiB
Java
Executable File
1286 lines
31 KiB
Java
Executable File
/*
|
|
* (C) Copyright 2015 by fr3ts0n <erwin.scheuch-heilig@gmx.at>
|
|
*
|
|
* 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 2 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., 59 Temple Place, Suite 330, Boston,
|
|
* MA 02111-1307 USA
|
|
*/
|
|
|
|
package com.fr3ts0n.ecu.prot.obd;
|
|
|
|
import com.fr3ts0n.prot.TelegramListener;
|
|
import com.fr3ts0n.prot.TelegramWriter;
|
|
|
|
import java.beans.PropertyChangeEvent;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.TreeSet;
|
|
import java.util.Vector;
|
|
|
|
|
|
/**
|
|
* Communication protocol to talk to a ELM327 OBD interface
|
|
*
|
|
* @author erwin
|
|
*/
|
|
public class ElmProt
|
|
extends ObdProt
|
|
implements TelegramListener, TelegramWriter, Runnable
|
|
{
|
|
/**
|
|
* virtual OBD service for CAN monitoring
|
|
*/
|
|
public static final int OBD_SVC_CAN_MONITOR = 256;
|
|
|
|
/**
|
|
* property name for ECU addresses
|
|
*/
|
|
public static final String PROP_ECU_ADDRESS = "ecuaddr";
|
|
/**
|
|
* property name for protocol status
|
|
*/
|
|
public static final String PROP_STATUS = "status";
|
|
|
|
/**
|
|
* CAN protocol handler
|
|
*/
|
|
public static final CanProtFord canProt = new CanProtFord();
|
|
/**
|
|
* Adaptive timing handler
|
|
*/
|
|
public final AdaptiveTiming mAdaptiveTiming = new AdaptiveTiming();
|
|
|
|
/**
|
|
* number of bytes expected from opponent
|
|
*/
|
|
private int charsExpected = 0;
|
|
/**
|
|
* remember last command which was sent
|
|
*/
|
|
private char[] lastCommand;
|
|
|
|
/**
|
|
* preferred ELM protocol to be selected
|
|
*/
|
|
static private PROT preferredProtocol = PROT.ELM_PROT_AUTO;
|
|
|
|
/**
|
|
* list of identified ECU addresses
|
|
*/
|
|
private final TreeSet<Integer> ecuAddresses = new TreeSet<Integer>();
|
|
/**
|
|
* selected ECU address
|
|
*/
|
|
private int selectedEcuAddress = 0;
|
|
/**
|
|
* custom ELM initialisation commands
|
|
*/
|
|
private final Vector<String> customInitCommands = new Vector<String>();
|
|
/**
|
|
* last detected message counter ID
|
|
*/
|
|
private int lastMsgId = 0;
|
|
|
|
/**
|
|
* ELM protocol ID's
|
|
*/
|
|
public enum PROT
|
|
{
|
|
ELM_PROT_AUTO("Automatic"),
|
|
ELM_PROT_J1850PWM("SAE J1850 PWM (41.6 KBaud)"),
|
|
ELM_PROT_J1850VPW("SAE J1850 VPW (10.4 KBaud)"),
|
|
ELM_PROT_9141_2("ISO 9141-2 (5 Baud Init)"),
|
|
ELM_PROT_14230_4("ISO 14230-4 KWP (5 Baud Init)"),
|
|
ELM_PROT_14230_4F("ISO 14230-4 KWP (fast Init)"),
|
|
ELM_PROT_15765_11_F("ISO 15765-4 CAN (11 Bit ID, 500 KBit)"),
|
|
ELM_PROT_15765_29_F("ISO 15765-4 CAN (29 Bit ID, 500 KBit)"),
|
|
ELM_PROT_15765_11_S("ISO 15765-4 CAN (11 Bit ID, 250 KBit)"),
|
|
ELM_PROT_15765_29_S("ISO 15765-4 CAN (29 Bit ID, 250 KBit)"),
|
|
ELM_PROT_J1939_29_S("SAE J1939 CAN (29 bit ID, 250* kbaud)"),
|
|
ELM_PROT_USER1_CAN_11_S("User1 CAN (11* bit ID, 125* kbaud)"),
|
|
ELM_PROT_USER2_CAN_11_S("User2 CAN (11* bit ID, 50* kbaud)");
|
|
private final String description;
|
|
|
|
PROT(String _description)
|
|
{
|
|
description = _description;
|
|
}
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
return description;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* possible ELM responses and ID's
|
|
*/
|
|
enum RSP_ID
|
|
{
|
|
PROMPT(">"),
|
|
OK("OK"),
|
|
MODEL("ELM"),
|
|
NODATA("NODATA"),
|
|
SEARCH("SEARCHING"),
|
|
ERROR("ERROR"),
|
|
NOCONN("UNABLE"),
|
|
NOCONN2("NABLETO"),
|
|
CANERROR("CANERROR"),
|
|
BUSBUSY("BUSBUSY"),
|
|
BUSERROR("BUSERROR"),
|
|
BUSINIERR("BUSINIT:ERR"),
|
|
BUSINIERR2("BUSINIT:BUS"),
|
|
BUSINIERR3("BUSINIT:...ERR"),
|
|
FBERROR("FBERROR"),
|
|
DATAERROR("DATAERROR"),
|
|
BUFFERFULL("BUFFERFULL"),
|
|
STOPPED("STOPPED"),
|
|
RXERROR("<"),
|
|
QMARK("?"),
|
|
UNKNOWN("");
|
|
private final String response;
|
|
|
|
RSP_ID(String response)
|
|
{
|
|
this.response = response;
|
|
}
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
return response;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* possible communication states
|
|
*/
|
|
public enum STAT
|
|
{
|
|
UNDEFINED("Undefined"),
|
|
INITIALIZING("Initializing"),
|
|
INITIALIZED("Initialized"),
|
|
ECU_DETECT("ECU detect"),
|
|
ECU_DETECTED("ECU detected"),
|
|
CONNECTING("Connecting"),
|
|
CONNECTED("Connected"),
|
|
NODATA("No data"),
|
|
STOPPED("Stopped"),
|
|
DISCONNECTED("Disconnected"),
|
|
BUSERROR("BUS error"),
|
|
DATAERROR("DATA error"),
|
|
RXERROR("RX error"),
|
|
ERROR("Error");
|
|
private final String elmState;
|
|
|
|
STAT(String state)
|
|
{
|
|
elmState = state;
|
|
}
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
return elmState;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* numeric IDs for commands
|
|
*/
|
|
public enum CMD
|
|
{
|
|
RESET("Z", 0, true), ///< reset adapter
|
|
WARMSTART("WS", 0, true), ///< warm start
|
|
PROTOCLOSE("PC", 0, true), ///< protocol close
|
|
DEFAULTS("D", 0, true), ///< set all to defaults
|
|
INFO("I", 0, true), ///< request adapter info
|
|
LOWPOWER("LP", 0, true), ///< switch to low power mode
|
|
ECHO("E", 1, true), ///< enable/disable echo
|
|
SETLINEFEED("L", 1, true), ///< enable/disable line feeds
|
|
SETSPACES("S", 1, true), ///< enable/disable spaces
|
|
SETHEADER("H", 1, true), ///< enable/disable header response
|
|
GETPROT("DP", 0, true), ///< get protocol
|
|
SETPROT("SP", 1, true), ///< set protocol
|
|
CANMONITOR("MA", 0, true), ///< monitor CAN messages
|
|
SETPROTAUTO("SPA", 1, true), ///< set protocol auto
|
|
ADAPTTIMING("AT", 1, true), ///< Set ELM internal adaptive timing (0-2)
|
|
SETTIMEOUT("ST", 2, true), ///< set timeout (x*4ms)
|
|
SETTXHDR("SH", 3, true), ///< set TX header
|
|
SETCANRXFLT("CRA", 3, true), ///< set CAN RX filter
|
|
CLRCANRXFLT("CRA", 0, true); ///< clear CAN RX filter
|
|
|
|
static final String CMD_HEADER = "AT";
|
|
private final String command;
|
|
final int paramDigits;
|
|
private final boolean disablingAllowed;
|
|
private boolean enabled = true;
|
|
|
|
CMD(String cmd, int numDigitsParameter, @SuppressWarnings("SameParameterValue") boolean allowAdaption)
|
|
{
|
|
command = cmd;
|
|
paramDigits = numDigitsParameter;
|
|
disablingAllowed = allowAdaption;
|
|
}
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
return CMD_HEADER + command;
|
|
}
|
|
|
|
public boolean isEnabled()
|
|
{
|
|
return enabled;
|
|
}
|
|
|
|
void setEnabled(boolean enabled)
|
|
{
|
|
if (disablingAllowed)
|
|
{
|
|
this.enabled = enabled;
|
|
}
|
|
// log current state
|
|
log.fine(String.format("ELM command '%s' -> %s",
|
|
toString(),
|
|
this.enabled ? "enabled" : "disabled"));
|
|
}
|
|
|
|
public boolean isDisablingAllowed()
|
|
{
|
|
return disablingAllowed;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adaptive timing mode
|
|
*/
|
|
public enum AdaptTimingMode
|
|
{
|
|
OFF,
|
|
ELM_AT1,
|
|
ELM_AT2,
|
|
SOFTWARE
|
|
}
|
|
|
|
/**
|
|
* Adaptive ELM timing handler
|
|
* * optimizes ELM message timeout at runtime
|
|
*/
|
|
public class AdaptiveTiming
|
|
{
|
|
/**
|
|
* for ELM message timeout handling
|
|
*/
|
|
/**
|
|
* max. ELM Message Timeout [ms]
|
|
*/
|
|
private static final int ELM_TIMEOUT_MAX = 1000;
|
|
/**
|
|
* default ELM message timeout
|
|
*/
|
|
private static final int ELM_TIMEOUT_DEFAULT = 200;
|
|
/**
|
|
* Learning resolution of ELM Message Timeout [ms]
|
|
*/
|
|
private static final int ELM_TIMEOUT_RES = 4;
|
|
/**
|
|
* minimum ELM timeout
|
|
*/
|
|
int ELM_TIMEOUT_MIN = 12;
|
|
/**
|
|
* minimum ELM timeout (learned from vehicle)
|
|
*/
|
|
int ELM_TIMEOUT_LRN_LOW = 12;
|
|
/**
|
|
* ELM message timeout: defaults to approx 200 [ms]
|
|
*/
|
|
int elmMsgTimeout = ELM_TIMEOUT_MAX;
|
|
|
|
/**
|
|
* adaptive timing handling enabled?
|
|
*/
|
|
private AdaptTimingMode mode = AdaptTimingMode.OFF;
|
|
|
|
public AdaptTimingMode getMode()
|
|
{
|
|
return mode;
|
|
}
|
|
|
|
public void setMode(AdaptTimingMode mode)
|
|
{
|
|
|
|
log.info(String.format("AdaptiveTiming mode: %s -> %s",
|
|
this.mode.toString(),
|
|
mode.toString()));
|
|
this.mode = mode;
|
|
// initialize with new mode
|
|
initialize();
|
|
}
|
|
|
|
/**
|
|
* min. (configured) ELM Message Timeout
|
|
*
|
|
* @return minimum (configured) ELM timeout value [ms]
|
|
*/
|
|
public int getElmTimeoutMin()
|
|
{
|
|
return ELM_TIMEOUT_MIN;
|
|
}
|
|
|
|
/**
|
|
* Set min. (configured) ELM Message Timeout
|
|
*
|
|
* @param elmTimeoutMin minimum (configured) ELM timeout value [ms]
|
|
*/
|
|
public void setElmTimeoutMin(int elmTimeoutMin)
|
|
{
|
|
log.info(String.format("ELM min timeout: %d -> %d",
|
|
ELM_TIMEOUT_MIN, elmTimeoutMin));
|
|
ELM_TIMEOUT_MIN = elmTimeoutMin;
|
|
}
|
|
|
|
/**
|
|
* Initialize timing hadler
|
|
*/
|
|
void initialize()
|
|
{
|
|
if (mode == AdaptTimingMode.SOFTWARE)
|
|
{
|
|
// ... reset learned minimum timeout ...
|
|
setElmTimeoutLrnLow(getElmTimeoutMin());
|
|
// set default timeout
|
|
setElmMsgTimeout(ELM_TIMEOUT_DEFAULT);
|
|
// switch OFF ELM internal adaptive timing
|
|
pushCommand(CMD.ADAPTTIMING, 0);
|
|
}
|
|
else
|
|
{
|
|
pushCommand(CMD.ADAPTTIMING, mode.ordinal());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adapt ELM message timeout
|
|
*
|
|
* @param increaseTimeout increase/decrease timeout
|
|
*/
|
|
void adapt(boolean increaseTimeout)
|
|
{
|
|
if (mode != AdaptTimingMode.SOFTWARE) { return; }
|
|
if (increaseTimeout)
|
|
{
|
|
// increase OBD timeout since we may expect answers too fast
|
|
if ((elmMsgTimeout + ELM_TIMEOUT_RES) < ELM_TIMEOUT_MAX)
|
|
{
|
|
// increase timeout, since we have just timed out
|
|
setElmMsgTimeout(elmMsgTimeout + ELM_TIMEOUT_RES);
|
|
// ... and limit MIN timeout for this session
|
|
setElmTimeoutLrnLow(elmMsgTimeout);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// reduce OBD timeout towards minimum limit
|
|
if ((elmMsgTimeout - ELM_TIMEOUT_RES) >= getElmTimeoutLrnLow())
|
|
{
|
|
setElmMsgTimeout(elmMsgTimeout - ELM_TIMEOUT_RES);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* LOW Learn value ELM Message Timeout
|
|
*
|
|
* @return currently learned timout value [ms]
|
|
*/
|
|
private int getElmTimeoutLrnLow()
|
|
{
|
|
return ELM_TIMEOUT_LRN_LOW;
|
|
}
|
|
|
|
/**
|
|
* set LOW Learn value ELM Message Timeout
|
|
*
|
|
* @param elmTimeoutLrnLow new learn value [ms]
|
|
*/
|
|
private void setElmTimeoutLrnLow(int elmTimeoutLrnLow)
|
|
{
|
|
log.info(String.format("ELM learn timeout: %d -> %d",
|
|
ELM_TIMEOUT_LRN_LOW, elmTimeoutLrnLow));
|
|
ELM_TIMEOUT_LRN_LOW = elmTimeoutLrnLow;
|
|
}
|
|
|
|
/**
|
|
* Set message timeout to ELM adapter to wait for valid response from vehicle
|
|
* If this timeout expires before a valid response is received from the
|
|
* vehicle, the ELM adapter will respond with "NO DATA"
|
|
*
|
|
* @param newTimeout desired timeout in milliseconds
|
|
*/
|
|
private void setElmMsgTimeout(int newTimeout)
|
|
{
|
|
if (newTimeout > 0 && newTimeout != elmMsgTimeout)
|
|
{
|
|
log.info("ELM Timeout: " + elmMsgTimeout + " -> " + newTimeout);
|
|
// set the timeout variable
|
|
elmMsgTimeout = newTimeout;
|
|
// queue the new timeout message
|
|
pushCommand(CMD.SETTIMEOUT, newTimeout / 4);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance of ElmProtocol
|
|
*/
|
|
public ElmProt()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* set preferred ELM protocol to be used
|
|
*
|
|
* @param protoIndex preferred ELM protocol index
|
|
*/
|
|
public static void setPreferredProtocol(int protoIndex)
|
|
{
|
|
preferredProtocol = PROT.values()[protoIndex];
|
|
log.info("Preferred protocol: " + preferredProtocol);
|
|
}
|
|
|
|
/**
|
|
* set ECU address to be received
|
|
*
|
|
* @param ecuAddress ECU address to be filtered / 0 = clear address filter
|
|
*/
|
|
public void setEcuAddress(int ecuAddress)
|
|
{
|
|
log.info(String.format("Set ECU address: 0x%x", ecuAddress));
|
|
selectedEcuAddress = ecuAddress;
|
|
// ensure headers are off
|
|
pushCommand(CMD.SETHEADER, 0);
|
|
// set/clear RX filter
|
|
pushCommand((selectedEcuAddress != 0) ? CMD.SETCANRXFLT : CMD.CLRCANRXFLT,
|
|
selectedEcuAddress);
|
|
}
|
|
|
|
/**
|
|
* disable a set of ELM commands ELM commands from preference
|
|
*
|
|
* @param disabledCmds set of ELM commands (ATxx strings) to be disabled
|
|
*/
|
|
public static void disableCommands(Set<String> disabledCmds)
|
|
{
|
|
for (CMD cmd : CMD.values())
|
|
{
|
|
cmd.setEnabled(disabledCmds == null
|
|
|| !disabledCmds.contains(cmd.toString()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* create ELM command string from command id and paramter
|
|
*
|
|
* @param cmdID ID of ELM command
|
|
* @param param parameter for ELM command (0 if not required)
|
|
* @return command char sequence or NULL if command disabled/invalid
|
|
*/
|
|
private String createCommand(CMD cmdID, int param)
|
|
{
|
|
String cmd = null;
|
|
if (cmdID.isEnabled())
|
|
{
|
|
cmd = cmdID.toString();
|
|
// if parameter is required and provided, add parameter to command
|
|
if (cmdID.paramDigits > 0)
|
|
{
|
|
String fmtString = "%0".concat(String.valueOf(cmdID.paramDigits)).concat("X");
|
|
cmd += String.format(fmtString, param);
|
|
}
|
|
}
|
|
// return command String
|
|
return cmd;
|
|
}
|
|
|
|
/**
|
|
* send command to ELM adapter
|
|
*
|
|
* @param cmdID ID of ELM command
|
|
* @param param parameter for ELM command (0 if not required)
|
|
*/
|
|
public void sendCommand(CMD cmdID, int param)
|
|
{
|
|
// now send command
|
|
String cmd = createCommand(cmdID, param);
|
|
if (cmd != null) { sendTelegram(cmd.toCharArray()); }
|
|
}
|
|
|
|
/**
|
|
* queue command to ELM command queue
|
|
*
|
|
* @param cmdID ID of ELM command
|
|
* @param param parameter for ELM command (0 if not required)
|
|
*/
|
|
private void pushCommand(CMD cmdID, int param)
|
|
{
|
|
String cmd = createCommand(cmdID, param);
|
|
if (cmd != null) { cmdQueue.add(cmd); }
|
|
}
|
|
|
|
@Override
|
|
public void sendTelegram(char[] buffer)
|
|
{
|
|
log.fine(this.toString() + " TX:'" + String.valueOf(buffer) + "'");
|
|
lastCommand = buffer;
|
|
super.sendTelegram(buffer);
|
|
}
|
|
|
|
/**
|
|
* return numeric ID to given response
|
|
*
|
|
* @param response clear text response from ELM adapter
|
|
*/
|
|
private static RSP_ID getResponseId(String response)
|
|
{
|
|
RSP_ID result = RSP_ID.UNKNOWN;
|
|
for (RSP_ID id : RSP_ID.values())
|
|
{
|
|
if (response.startsWith(id.toString()))
|
|
{
|
|
result = id;
|
|
break;
|
|
}
|
|
}
|
|
// return ID
|
|
return (result);
|
|
}
|
|
|
|
/**
|
|
* send ELM adapter to sleep mode
|
|
*/
|
|
public void goToSleep()
|
|
{
|
|
sendCommand(CMD.LOWPOWER, 0);
|
|
}
|
|
|
|
/**
|
|
* reset ELM adapter
|
|
*/
|
|
public void reset()
|
|
{
|
|
// reset all learned protocol data
|
|
super.reset();
|
|
// either RESET or INFO command needs to be enabled
|
|
if (CMD.RESET.isEnabled())
|
|
{ sendCommand(CMD.RESET, 0); }
|
|
else
|
|
{ sendCommand(CMD.INFO, 0); }
|
|
}
|
|
|
|
/**
|
|
* request addresses of all connected ECUs
|
|
* (received IDs are evaluated in @ref:handleDataMessage)
|
|
*/
|
|
private void queryEcus()
|
|
{
|
|
// set status to ECU detection
|
|
setStatus(STAT.ECU_DETECT);
|
|
|
|
// clear all identified ECU addresses
|
|
ecuAddresses.clear();
|
|
// clear selected ECU
|
|
selectedEcuAddress = 0;
|
|
// remember to disable headers again
|
|
pushCommand(CMD.SETHEADER, 0);
|
|
// request PIDs (from all devices)
|
|
cmdQueue.add("0100");
|
|
// enable headers
|
|
sendCommand(CMD.SETHEADER, 1);
|
|
}
|
|
|
|
private void initialize()
|
|
{
|
|
// set status to INITIALIZING
|
|
setStatus(STAT.INITIALIZING);
|
|
|
|
// push custom init commands
|
|
cmdQueue.addAll(customInitCommands);
|
|
|
|
// set to preferred protocol
|
|
pushCommand(CMD.SETPROT, preferredProtocol.ordinal());
|
|
|
|
// initialize adaptive timing handler
|
|
mAdaptiveTiming.initialize();
|
|
|
|
// speed up protocol by removing spaces and line feeds from output
|
|
pushCommand(CMD.SETSPACES, 0);
|
|
pushCommand(CMD.SETLINEFEED, 0);
|
|
|
|
// immediate set echo off
|
|
pushCommand(CMD.ECHO, 0);
|
|
}
|
|
|
|
/**
|
|
* Implementation of TelegramListener
|
|
*/
|
|
|
|
/**
|
|
* multiline response is pending, for responses w/o a length info
|
|
*/
|
|
private boolean responsePending = false;
|
|
|
|
/**
|
|
* handle incoming protocol telegram
|
|
*
|
|
* @param buffer - telegram buffer
|
|
* @return number of listeners notified
|
|
*/
|
|
@Override
|
|
public synchronized int handleTelegram(char[] buffer)
|
|
{
|
|
int result = 0;
|
|
String bufferStr = new String(buffer);
|
|
|
|
log.fine(this.toString() + " RX:'" + bufferStr + "'");
|
|
|
|
// empty result
|
|
if (buffer.length == 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// if ths is echo of last command
|
|
if (lastTxMsg.compareToIgnoreCase(bufferStr) == 0)
|
|
{
|
|
// ignore echoed command
|
|
return result;
|
|
}
|
|
|
|
// log message reception as answer to last TX message
|
|
log.fine("ELM rx:'" + bufferStr + "' (" + lastTxMsg + ")");
|
|
|
|
// handle response
|
|
switch (getResponseId(bufferStr))
|
|
{
|
|
case SEARCH:
|
|
setStatus(status != STAT.ECU_DETECT ? STAT.CONNECTING : status);
|
|
// NO break here
|
|
case QMARK:
|
|
case NODATA:
|
|
case OK:
|
|
case ERROR:
|
|
case NOCONN:
|
|
case NOCONN2:
|
|
case CANERROR:
|
|
case BUSERROR:
|
|
case BUSINIERR:
|
|
case BUSINIERR2:
|
|
case BUSINIERR3:
|
|
case BUSBUSY:
|
|
case FBERROR:
|
|
case DATAERROR:
|
|
case BUFFERFULL:
|
|
case RXERROR:
|
|
// remember this as last received message
|
|
// do NOT respond immediately
|
|
lastRxMsg = bufferStr;
|
|
break;
|
|
|
|
case STOPPED:
|
|
// remember this as last received message
|
|
lastRxMsg = bufferStr;
|
|
// re-queue last command
|
|
cmdQueue.add(String.valueOf(lastCommand));
|
|
break;
|
|
|
|
case MODEL:
|
|
initialize();
|
|
break;
|
|
|
|
// received a PROMPT, what was the last response?
|
|
case PROMPT:
|
|
// check for last received message
|
|
switch (getResponseId(lastRxMsg))
|
|
{
|
|
case NOCONN:
|
|
case NOCONN2:
|
|
case CANERROR:
|
|
case BUSERROR:
|
|
case BUSINIERR:
|
|
case BUSINIERR2:
|
|
case BUSINIERR3:
|
|
case BUSBUSY:
|
|
case FBERROR:
|
|
setStatus(STAT.DISCONNECTED);
|
|
// re-queue last command
|
|
cmdQueue.add(String.valueOf(lastCommand));
|
|
// queue setting to preferred protocol
|
|
pushCommand(CMD.SETPROT, preferredProtocol.ordinal());
|
|
// Initialize adaptive timing
|
|
mAdaptiveTiming.initialize();
|
|
// immediately close current protocol
|
|
sendCommand(CMD.PROTOCLOSE, 0);
|
|
break;
|
|
|
|
case DATAERROR:
|
|
setStatus(STAT.DATAERROR);
|
|
sendCommand(CMD.WARMSTART, 0);
|
|
break;
|
|
|
|
case BUFFERFULL:
|
|
case RXERROR:
|
|
setStatus(STAT.RXERROR);
|
|
sendCommand(CMD.WARMSTART, 0);
|
|
break;
|
|
|
|
case ERROR:
|
|
setStatus(STAT.ERROR);
|
|
sendCommand(CMD.WARMSTART, 0);
|
|
break;
|
|
|
|
case NODATA:
|
|
setStatus(STAT.NODATA);
|
|
// re-queue next data item
|
|
if (service != OBD_SVC_NONE)
|
|
{
|
|
cmdQueue.add(
|
|
String.valueOf(
|
|
createTelegram(emptyBuffer, service, getNextSupportedPid()))
|
|
);
|
|
}
|
|
// increase OBD timeout since we may expect answers too fast
|
|
mAdaptiveTiming.adapt(true);
|
|
// set to preferred protocol
|
|
pushCommand(CMD.SETPROT, preferredProtocol.ordinal());
|
|
// NO break here since reaction is only quqeued
|
|
|
|
case MODEL:
|
|
case SEARCH:
|
|
case STOPPED:
|
|
// was already handled before prompt
|
|
case QMARK:
|
|
// last command stays ignored
|
|
|
|
case OK:
|
|
default:
|
|
// if there is a pending data response, handle it now ...
|
|
if (responsePending)
|
|
{
|
|
result = handleDataMessage(lastRxMsg);
|
|
}
|
|
|
|
// queued commands will be sent first
|
|
if (cmdQueue.size() > 0)
|
|
{
|
|
// get last command
|
|
String cmd = cmdQueue.lastElement();
|
|
// and remove it from list
|
|
cmdQueue.remove(cmd);
|
|
// send the command
|
|
sendTelegram(cmd.toCharArray());
|
|
}
|
|
else
|
|
{
|
|
// all queued commands are sent -> we are done initializing
|
|
if (status == STAT.INITIALIZING)
|
|
{
|
|
// set status to initialized
|
|
setStatus(STAT.INITIALIZED);
|
|
// initiate query of connected ECUs
|
|
queryEcus();
|
|
break;
|
|
}
|
|
|
|
// all queued commands are sent -> we are done detecting ECUs
|
|
setStatus(status == STAT.ECU_DETECT ? STAT.ECU_DETECTED : status);
|
|
|
|
switch (service)
|
|
{
|
|
case OBD_SVC_VEH_INFO:
|
|
// if all pid's have been read once ...
|
|
if (pidsWrapped)
|
|
{
|
|
// ... terminate service loop
|
|
break;
|
|
}
|
|
// no break here ...
|
|
case OBD_SVC_DATA:
|
|
case OBD_SVC_FREEZEFRAME:
|
|
{
|
|
// otherwise the next PID will be requested
|
|
writeTelegram(emptyBuffer, service, getNextSupportedPid());
|
|
// reduce OBD timeout towards minimum limit
|
|
mAdaptiveTiming.adapt(false);
|
|
}
|
|
break;
|
|
|
|
case OBD_SVC_NONE:
|
|
default:
|
|
// do nothing
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
// handle data response
|
|
default:
|
|
// if we are still initializing check for address entries
|
|
switch (status)
|
|
{
|
|
case ECU_DETECT:
|
|
{
|
|
// start of 0100 response is end of address
|
|
int adrEnd = bufferStr.indexOf("41");
|
|
// if not a service response, check for possible NRC
|
|
if(adrEnd < 0)
|
|
{
|
|
adrEnd = bufferStr.indexOf("7F01");
|
|
}
|
|
int adrStart = bufferStr.lastIndexOf(".") + 1;
|
|
if (adrEnd > adrStart)
|
|
{
|
|
int adrLen = adrEnd - adrStart;
|
|
if ((adrLen % 2) != 0)
|
|
{
|
|
/*
|
|
* odd address length
|
|
* -> CAN address length = 3 digits + 2 digits frame type
|
|
*/
|
|
adrLen = 3;
|
|
}
|
|
else
|
|
{
|
|
if (adrLen == 6)
|
|
{
|
|
/*
|
|
* 6 char (3 byte) prefix -> ISO9141 address <FF><RR><SS>
|
|
* FF - Frame type
|
|
* RR - receiver address
|
|
* SS - sender address
|
|
*/
|
|
adrLen = 2;
|
|
adrStart = adrEnd - adrLen;
|
|
}
|
|
else if (adrLen == 10)
|
|
{
|
|
/*
|
|
* 29 bit CAN address
|
|
* <CP><AABBCC><FT>
|
|
* CP = CAN priority (5 relevant bits)
|
|
* AABBCC = Address (24 Bit)
|
|
* FT = Frame type
|
|
* -> CAN address length = 32 bits / 8 digits <CP><AABBCC>
|
|
*/
|
|
adrLen = 8;
|
|
adrStart = 0;
|
|
}
|
|
}
|
|
// extract address
|
|
String address = bufferStr.substring(adrStart, adrStart + adrLen);
|
|
|
|
log.fine(String.format("Found ECU address: 0x%s", address));
|
|
// and add to list of addresses
|
|
ecuAddresses.add(Integer.valueOf(address, 16));
|
|
}
|
|
return lastRxMsg.length();
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// we are connected ...
|
|
setStatus(STAT.CONNECTED);
|
|
|
|
// ELM clone verbose message (starting with '+')
|
|
if(buffer[0] == '+')
|
|
{
|
|
// ignore message
|
|
return (result);
|
|
}
|
|
|
|
// is this a length identifier?
|
|
if (buffer[0] == '0' && buffer.length == 3)
|
|
{
|
|
// then remember the length to be expected
|
|
charsExpected = Integer.valueOf(bufferStr, 16) * 2;
|
|
lastRxMsg = "";
|
|
return (result);
|
|
}
|
|
|
|
// is this a multi-line response
|
|
int idx = bufferStr.indexOf(':');
|
|
|
|
// .. or a ISO multi line response with format SVC PID MSGID DATA...
|
|
if((idx < 0) && (buffer.length == 14))
|
|
{
|
|
final int[] dfcServices = {OBD_SVC_READ_CODES, OBD_SVC_PENDINGCODES, OBD_SVC_PERMACODES};
|
|
int msgService = Integer.valueOf(bufferStr.substring(0, 2), 0x10) & ~0x40;
|
|
// If response to current service and no DFC response ...
|
|
if(msgService == getService()
|
|
&& Arrays.binarySearch(dfcServices, msgService) < 0)
|
|
{
|
|
// Use header on 1st response, cut from continuation messages
|
|
int msgId = Integer.valueOf(bufferStr.substring(4,6),0x10);
|
|
if(msgId <= 1)
|
|
{
|
|
// 1st response line
|
|
idx = 0;
|
|
lastMsgId = msgId;
|
|
}
|
|
else if (msgId == (lastMsgId + 1))
|
|
{
|
|
// continuation line
|
|
idx = 5;
|
|
lastMsgId = msgId;
|
|
}
|
|
else
|
|
{
|
|
// NOT a ISO multi line message
|
|
idx = -1;
|
|
// cut additional (padding) byte
|
|
bufferStr = bufferStr.substring(0,buffer.length-2);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (idx >= 0)
|
|
{
|
|
if(idx == 0)
|
|
{
|
|
// initial ISO multiline message
|
|
lastRxMsg = bufferStr;
|
|
charsExpected = 0;
|
|
}
|
|
else if (buffer[0] == '0')
|
|
{
|
|
// first line of a multiline message
|
|
lastRxMsg = bufferStr.substring(idx + 1);
|
|
}
|
|
else
|
|
{
|
|
// continuation lines
|
|
// concat response without line counter
|
|
lastRxMsg += bufferStr.substring(idx + 1);
|
|
}
|
|
|
|
/* no length known, set marker for pending response
|
|
response will be finished on reception of prompt */
|
|
responsePending = (charsExpected == 0);
|
|
}
|
|
else
|
|
{
|
|
// otherwise use this as last received message
|
|
lastRxMsg = bufferStr;
|
|
charsExpected = 0;
|
|
responsePending = false;
|
|
}
|
|
|
|
|
|
// if we haven't received complete result yet, then wait for the rest
|
|
if (lastRxMsg.length() < charsExpected)
|
|
{
|
|
return (result);
|
|
}
|
|
|
|
// Trim a multiline response to expected length (cut off padding)
|
|
if((charsExpected > 0) && (lastRxMsg.length() > charsExpected))
|
|
{
|
|
lastRxMsg = lastRxMsg.substring(0, charsExpected);
|
|
}
|
|
|
|
// if response is finished, handle it
|
|
if (!responsePending)
|
|
{
|
|
result = handleDataMessage(lastRxMsg);
|
|
}
|
|
}
|
|
return (result);
|
|
}
|
|
|
|
/**
|
|
* forward data message for further handling
|
|
*
|
|
* @param lastRxMsg received message to be forwarded
|
|
* @return number of bytes processed
|
|
*/
|
|
private int handleDataMessage(String lastRxMsg)
|
|
{
|
|
int result = 0;
|
|
|
|
// otherwise process response
|
|
switch (service)
|
|
{
|
|
case OBD_SVC_NONE:
|
|
// ignore messages
|
|
break;
|
|
|
|
case OBD_SVC_CAN_MONITOR:
|
|
result = canProt.handleTelegram(lastRxMsg.toCharArray());
|
|
break;
|
|
|
|
default:
|
|
// Let the OBD protocol handle the telegram
|
|
result = super.handleTelegram(lastRxMsg.toCharArray());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// switch to exit the demo thread
|
|
public static boolean runDemo;
|
|
|
|
/**
|
|
* run threaded loop to simulate incoming telegrams
|
|
*/
|
|
public void run()
|
|
{
|
|
int value = 0;
|
|
Integer pid;
|
|
runDemo = true;
|
|
|
|
log.info("ELM DEMO thread started");
|
|
while (runDemo)
|
|
{
|
|
try
|
|
{
|
|
handleTelegram(RSP_ID.MODEL.toString().toCharArray());
|
|
// test case for issue AndrOBD/#61
|
|
handleTelegram("+CONNECTING<<94:65:2D:9E:DF:B5".toCharArray());
|
|
|
|
setStatus(STAT.ECU_DETECT);
|
|
handleTelegram("SEARCHING...".toCharArray());
|
|
handleTelegram("7EA074100000000".toCharArray());
|
|
handleTelegram("486B104100BF9FA8919B".toCharArray());
|
|
handleTelegram("...486B104100BF9FA8919B".toCharArray());
|
|
handleTelegram("7E8064100000000".toCharArray());
|
|
handleTelegram("7E9074100000000".toCharArray());
|
|
handleTelegram("7EA074100000000".toCharArray());
|
|
// test case for issue AndrOBD/#60
|
|
handleTelegram("18DAF110064100BE3EB811".toCharArray());
|
|
// test case for issue AndrOBD-Plugin/#10 (NRC22 on detect)
|
|
handleTelegram("7E8037F0122".toCharArray());
|
|
setStatus(STAT.ECU_DETECTED);
|
|
|
|
while (runDemo)
|
|
{
|
|
switch (service)
|
|
{
|
|
// read any kinds of trouble codes
|
|
case OBD_SVC_READ_CODES:
|
|
case OBD_SVC_PENDINGCODES:
|
|
case OBD_SVC_PERMACODES:
|
|
// simulate 12 TCs set as multy line response
|
|
// number of codes = 12 + MIL ON
|
|
handleTelegram("41018C000000".toCharArray());
|
|
// send codes as multy line response
|
|
handleTelegram("014".toCharArray());
|
|
handleTelegram("0:438920B920BD".toCharArray());
|
|
handleTelegram("1:C002242A246E02".toCharArray());
|
|
handleTelegram("2:36010101162453".toCharArray());
|
|
|
|
// simulate 12 TCs set as subsequent single line responses
|
|
// send codes as subsequent single line responses
|
|
handleTelegram("478420BA20BC".toCharArray());
|
|
handleTelegram("4784C004242B".toCharArray());
|
|
|
|
// test pattern from AndrOBD/#78
|
|
// 4 DFCs, 10 byte multiline response , padded
|
|
handleTelegram("00A".toCharArray());
|
|
handleTelegram("0:4A8401180122".toCharArray());
|
|
handleTelegram("1:02232610000000".toCharArray());
|
|
|
|
Thread.sleep(500);
|
|
break;
|
|
|
|
// otherwise send data ...
|
|
case OBD_SVC_DATA:
|
|
case OBD_SVC_FREEZEFRAME:
|
|
pid = getNextSupportedPid();
|
|
if (pid != 0)
|
|
{
|
|
value++;
|
|
value &= 0xFF;
|
|
// format new data message and handle it as new reception
|
|
handleTelegram(String.format(
|
|
service == OBD_SVC_DATA ? "4%X%02X%02X%02X%02X%02X"
|
|
: "4%X%02X00%02X%02X%02X%02X",
|
|
service, pid, value, value, value, value).toCharArray());
|
|
}
|
|
else
|
|
{
|
|
// simulate "ALL PIDs supported"
|
|
int i;
|
|
for (i = 0; i < 0xE0; i += 0x20)
|
|
{
|
|
handleTelegram(String.format(
|
|
service == OBD_SVC_DATA ? "4%X%02XFFFFFFFF"
|
|
: "4%X%02X00FFFFFFFF", service, i)
|
|
.toCharArray());
|
|
}
|
|
handleTelegram(String.format(
|
|
service == OBD_SVC_DATA ? "4%X%02XFFFFFFFE"
|
|
: "4%X%02X00FFFFFFFE", service, i)
|
|
.toCharArray());
|
|
}
|
|
break;
|
|
|
|
case OBD_SVC_VEH_INFO:
|
|
pid = getNextSupportedPid();
|
|
if (pid == 0)
|
|
{
|
|
// simulate "ALL pids supported"
|
|
handleTelegram("490054000000".toCharArray());
|
|
}
|
|
|
|
// send VIN "0123456789ABCDEFG"
|
|
handleTelegram("014".toCharArray());
|
|
handleTelegram("1:49020130313233".toCharArray());
|
|
handleTelegram("2:343536373839".toCharArray());
|
|
handleTelegram("3:41424344454647".toCharArray());
|
|
|
|
// send 2 CAL-IDs "GSPA..." without length id
|
|
handleTelegram("0:490402475350".toCharArray());
|
|
handleTelegram("1:412D3132333435".toCharArray());
|
|
handleTelegram("2:363738393030".toCharArray());
|
|
handleTelegram("3:30313233".toCharArray());
|
|
handleTelegram("4:343536373839".toCharArray());
|
|
handleTelegram("5:414243444546".toCharArray());
|
|
|
|
// CAL-ID 01234567
|
|
handleTelegram("490601234567".toCharArray());
|
|
break;
|
|
|
|
case OBD_SVC_CTRL_MODE:
|
|
handleTelegram("4800C0000000".toCharArray());
|
|
break;
|
|
|
|
case OBD_SVC_NONE:
|
|
// just keep quiet until soneone requests something
|
|
break;
|
|
|
|
default:
|
|
// respond "service not supported"
|
|
handleTelegram(String.format("7F%02X11", service).toCharArray());
|
|
Thread.sleep(500);
|
|
break;
|
|
|
|
}
|
|
Thread.sleep(50);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.severe(ex.getLocalizedMessage());
|
|
}
|
|
}
|
|
log.info("ELM DEMO thread finished");
|
|
}
|
|
|
|
/**
|
|
* set custom initialisation commands
|
|
*
|
|
* @param commands custom initialisation commands
|
|
*/
|
|
public void setCustomInitCommands(String[] commands)
|
|
{
|
|
List<String> cmds = Arrays.asList(commands);
|
|
// reverse list, since all commands are pushed rather than queued
|
|
Collections.reverse(cmds);
|
|
// clear list
|
|
customInitCommands.clear();
|
|
// add all entries
|
|
customInitCommands.addAll(cmds);
|
|
}
|
|
|
|
/**
|
|
* Setter for property service.
|
|
*
|
|
* @param service New value of property service.
|
|
* @param clearLists clear data list for this service
|
|
*/
|
|
@Override
|
|
public void setService(int service, boolean clearLists)
|
|
{
|
|
// log the change in service
|
|
if (service != this.service)
|
|
{
|
|
log.info("OBD Service: " + this.service + "->" + service);
|
|
this.service = service;
|
|
|
|
// send corresponding command(s)
|
|
switch (service)
|
|
{
|
|
case OBD_SVC_CAN_MONITOR:
|
|
sendCommand(CMD.CANMONITOR, 0);
|
|
break;
|
|
|
|
default:
|
|
super.setService(service, clearLists);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* set OBD service - compatibility function
|
|
*
|
|
* @param service New value of property service.
|
|
*/
|
|
public void setService(int service)
|
|
{
|
|
setService(service, true);
|
|
}
|
|
|
|
/**
|
|
* Holds value of property status.
|
|
*/
|
|
private STAT status = STAT.UNDEFINED;
|
|
|
|
/**
|
|
* Getter for property status.
|
|
*
|
|
* @return Value of property status.
|
|
*/
|
|
public STAT getStatus()
|
|
{
|
|
return this.status;
|
|
}
|
|
|
|
/**
|
|
* Setter for property status.
|
|
*
|
|
* @param status New value of property status.
|
|
*/
|
|
private void setStatus(STAT status)
|
|
{
|
|
STAT oldStatus = this.status;
|
|
this.status = status;
|
|
if (status != oldStatus)
|
|
{
|
|
log.info("Status change: " + oldStatus + "->" + status);
|
|
// ECUs detected -> send identified ECU addresses
|
|
if (status == STAT.ECU_DETECTED)
|
|
{
|
|
firePropertyChange(
|
|
new PropertyChangeEvent(this, PROP_ECU_ADDRESS, null, ecuAddresses));
|
|
}
|
|
|
|
// now fire regular status change
|
|
firePropertyChange(new PropertyChangeEvent(this, PROP_STATUS, oldStatus, status));
|
|
}
|
|
}
|
|
}
|