/**
* (C) Copyright 2015 by fr3ts0n <erwin.scheuch-heilig@gmx.at>
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.ecu.Conversion;
import com.fr3ts0n.ecu.EcuCodeItem;
import com.fr3ts0n.ecu.EcuCodeList;
import com.fr3ts0n.ecu.EcuConversions;
import com.fr3ts0n.ecu.EcuDataItem;
import com.fr3ts0n.ecu.EcuDataItems;
import com.fr3ts0n.ecu.EcuDataPv;
import com.fr3ts0n.ecu.ObdCodeItem;
import com.fr3ts0n.prot.ProtoHeader;
import com.fr3ts0n.prot.TelegramListener;
import com.fr3ts0n.prot.TelegramWriter;
import com.fr3ts0n.pvs.PvChangeEvent;
import com.fr3ts0n.pvs.PvList;
import java.beans.PropertyChangeEvent;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Vector;
/**
* OBD communication protocol layer
*
* The OBD protocol supports services which serve multiple PID's
* A request of PID which is a multiple of 0x20 (0x00,0x20...0xE0) for each
* service returns a bitmask of the next 32 PIDs which are suppoted by the vehicle
* @author erwin
*/
public class ObdProt extends ProtoHeader
implements TelegramListener, TelegramWriter
{
public static final int OBD_SVC_NONE = 0x00;
public static final int OBD_SVC_DATA = 0x01;
public static final int OBD_SVC_FREEZEFRAME = 0x02;
public static final int OBD_SVC_READ_CODES = 0x03;
public static final int OBD_SVC_CLEAR_CODES = 0x04;
public static final int OBD_SVC_O2_RESULT = 0x05;
public static final int OBD_SVC_MON_RESULT = 0x06;
public static final int OBD_SVC_PENDINGCODES = 0x07;
public static final int OBD_SVC_CTRL_MODE = 0x08;
public static final int OBD_SVC_VEH_INFO = 0x09;
public static final int OBD_SVC_PERMACODES = 0x0A;
/** negative response ID */
public static final int OBD_ID_NRC = 0x7F;
/** perform immediate reset on NRC reception? */
public boolean isResetOnNrc()
{
return resetOnNrc;
}
/** Set protocol parameter
* @param resetOnNrc perform immediate reset on NRC reception?
*/
public void setResetOnNrc(boolean resetOnNrc)
{
log.info(String.format("Reset on NRC = %b", resetOnNrc));
this.resetOnNrc = resetOnNrc;
}
/** negative response codes */
public enum NRC
{
GR(0x10, "General reject"),
SNS(0x11, "Service 0x%02X not supported"),
SFNS(0x12, "Sub-Function not supported (SVC:0x%02X)"),
IMLOIF(0x13, "Incorrect message length or invalid format"),
RTL(0x14, "Response too long"),
BRR(0x21, "Busy repeat request"),
CNC(0x22, "Conditions not correct"),
RSE(0x24, "Request sequence error"),
NRFSC(0x25, "No response from sub-net component"),
FPEORA(0x26, "Failure prevents execution of requested action"),
ROOR(0x31, "Request out of range (SVC:0x%02X)"),
SAD(0x33, "Security access denied"),
IK(0x35, "Invalid key"),
ENOA(0x36, "Exceeded number of attempts"),
RTDNE(0x37, "Required time delay not expired"),
UDNA(0x70, "Upload/Download not accepted"),
TDS(0x71, "Transfer data suspended"),
GPF(0x72, "General programming failure"),
WBSC(0x73, "Wrong Block Sequence Counter"),
RCRRP(0x78, "Request correctly received but response is pending"),
SFNSIAS(0x7E, "Sub-Function not supported in active session (SVC:0x%02X)"),
SNSIAS(0x7F, "Service 0x%02X not supported in active session");
public int code;
public String description;
NRC(int _code, String _description)
{
code = _code;
description = _description;
}
/**
* Get NRC with specified ID (NRC-code)
* @param id ID (NRC-code) to search
* @return specified NRC, or null if not found
*/
public static NRC get(int id)
{
NRC result = null;
for (NRC nrc : values())
{
if (nrc.code == id)
{
result = nrc;
break;
}
}
return result;
}
/** return String representative */
public String toString(int service)
{
return String.format("(NRC:0x%02X) %s", code, String.format(description, service));
}
}
/** property name "number of codes" */
public static final String PROP_NUM_CODES = "numCodes";
public static final String PROP_NRC = "NRC";
// current supported PID
static int currSupportedPid = 0;
static boolean pidsWrapped = false;
/** content of last sent message */
protected static String lastTxMsg = "";
/** content of last received message */
protected static String lastRxMsg = "";
/** Holds value of property service. */
protected int service = OBD_SVC_NONE;
/** service of last incoming message */
protected int msgService = OBD_SVC_NONE;
/** List of PIDs supported by the vehicle */
static Vector<Integer> pidSupported = new Vector<Integer>();
/** positive response fields */
public static final int ID_OBD_SVC = 0;
public static final int ID_OBD_PID = 1;
public static final int ID_OBD_FRAMEID = 2;
/** negative response fields */
public static final int ID_NR_ID = 0;
public static final int ID_NR_SVC = 1;
public static final int ID_NR_CODE = 2;
/**
* Negative response parameters
* List of telegram parameters in order of appearance
*/
static final int NR_PARAMETERS[][] =
/* START, LEN, PARAM-TYPE // REMARKS */
/* ------------------------------------------- */
{{0, 2, PT_HEX}, // ID_NR_ID
{2, 2, PT_HEX}, // ID_NR_SVC
{4, 2, PT_HEX}, // ID_NR_CODE
};
/**
* List of telegram parameters in order of appearance
*/
static final int SVC_PARAMETERS[][] =
/* START, LEN, PARAM-TYPE // REMARKS */
/* ------------------------------------------- */
{{0, 2, PT_HEX}, // ID_OBD_SVC
};
/**
* List of telegram parameters in order of appearance
*/
static final int OBD_PARAMETERS[][] =
/* START, LEN, PARAM-TYPE // REMARKS */
/* ------------------------------------------- */
{{0, 2, PT_HEX}, // ID_OBD_SVC
{2, 2, PT_HEX}, // ID_OBD_PID
};
/**
* List of telegram parameters in order of appearance
*/
static final int FRZFRM_PARAMETERS[][] =
/* START, LEN, PARAM-TYPE // REMARKS */
/* ------------------------------------------- */
{{0, 2, PT_HEX}, // ID_OBD_SVC
{2, 2, PT_HEX}, // ID_OBD_PID
{2, 2, PT_HEX}, // ID_OBD_FRAMEID
};
public static final int ID_NUM_CODES = 0;
public static final int ID_MSK_CODES = 1;
/**
* List of telegram parameters in order of appearance
*/
static final int NUMCODE_PARAMETERS[][] =
/* START, LEN, PARAM-TYPE // REMARKS */
/* ------------------------------------------- */
{{4, 2, PT_HEX}, // ID_NUM_CODES
{6, 6, PT_HEX}, // ID_MSK_CODES
};
static final String[] OBD_DESCRIPTORS =
{
"OBD Service",
"OBD PID",
};
/** new style data items */
public static final EcuDataItems dataItems = new EcuDataItems();
/** OBD data items */
public static PvList PidPvs = new PvList();
/** OBD vehicle identification items */
public static PvList VidPvs = new PvList();
/** current fault codes */
public static PvList tCodes = new PvList();
/** list of known fault codes */
public static EcuCodeList knownCodes = EcuConversions.codeList;
/** queue of ELM commands to be sent */
static Vector<String> cmdQueue = new Vector<String>();
/** freeze frame ID to request */
private int freezeFrame_Id = 0;
/** perform reset on NRC reception */
private boolean resetOnNrc = false;
/** Creates a new instance of ObdProt */
public ObdProt()
{
paddingChr = '0';
// prepare PID PV list
PidPvs.put(0, new EcuDataPv());
VidPvs.put(0, new EcuDataPv());
tCodes.put(0, new ObdCodeItem(0, "No trouble codes set"));
}
/**
* set Freeze frame id to be requested
* @param freezeFrame_Id ID of freeze frame to be requested
*/
public void setFreezeFrame_Id(int freezeFrame_Id)
{
log.info(String.format("FreezeFrame ID: %d", freezeFrame_Id));
this.freezeFrame_Id = freezeFrame_Id;
setService(OBD_SVC_FREEZEFRAME, true);
}
/**
* list of parameters for specific protocol
* @return complete set of protocol parameters
*/
public int[][] getTelegramParams()
{
return (getTelegramParams(msgService));
}
/**
* list of parameters for specific protocol
* @param service Service which this header is requested for
* @return complete set of protocol parameters
*/
public int[][] getTelegramParams(int service)
{
int fldMap[][];
switch (service)
{
// negative response
case OBD_ID_NRC:
fldMap = NR_PARAMETERS;
break;
case OBD_SVC_FREEZEFRAME:
fldMap = FRZFRM_PARAMETERS;
break;
case OBD_SVC_READ_CODES:
case OBD_SVC_PENDINGCODES:
case OBD_SVC_PERMACODES:
case OBD_SVC_CLEAR_CODES:
fldMap = SVC_PARAMETERS;
break;
default:
fldMap = OBD_PARAMETERS;
}
return (fldMap);
}
/**
* return message footer for protocol payload
* @param buffer buffer of payload data
* @return buffer of message footer
*/
public char[] getFooter(char[] buffer)
{
return (emptyBuffer);
}
/**
* create a new telegram header for selected payload data buffer
* inclunding setting all ID's, sizes and validity issues
* @param buffer buffer of payload data
* @return buffer of new telegram header
*/
protected char[] getNewHeader(char[] buffer)
{
return (getNewHeader(buffer, OBD_SVC_DATA, Integer.valueOf(0)));
}
/**
* create a new telegram header inclunding setting all ID's, sizes and
* validity issues
* @param buffer buffer of payload data
* @param type type of telegram content
* @param id identifier for telegram (may be null)
* @return buffer of new telegram header
*/
@SuppressWarnings("fallthrough")
protected char[] getNewHeader(char[] buffer, int type, Object id)
{
int[][] fldMap = getTelegramParams(type);
char[] header = createEmptyBuffer(fldMap, '0');
setParamValue(ID_OBD_SVC, fldMap, header, Integer.valueOf(type));
switch (type)
{
// these commands do not require parametrs
case OBD_SVC_READ_CODES:
case OBD_SVC_PENDINGCODES:
case OBD_SVC_PERMACODES:
case OBD_SVC_CLEAR_CODES:
break;
// freezeframes require additional frame id
case OBD_SVC_FREEZEFRAME:
setParamValue(ID_OBD_FRAMEID, fldMap, header, freezeFrame_Id);
// NO break here
// all other commands require PID to be set
default:
setParamValue(ID_OBD_PID, fldMap, header, id);
}
return (header);
}
/**
* list of parameter descriptions for specific protocol
* @return complete set of protocol parameter description strings
*/
protected String[] getParamDescriptors()
{
return (OBD_DESCRIPTORS);
}
/**
* prepare process variables for each PID
* @param pvList list of process vars
*/
public void preparePidPvs(int obdService, PvList pvList)
{
// reset fixed PIDs
resetFixedPid();
HashMap<String, EcuDataPv> newList = new HashMap<String, EcuDataPv>();
for (Integer currPid : pidSupported)
{
Vector<EcuDataItem> items = dataItems.getPidDataItems(obdService, currPid);
// if no items defined, create dummy item
if (items == null)
{
log.warn(String.format("unknown PID %02X", currPid));
// create new dummy item / OneToOne conversion
Conversion[] dummyCnvs = {EcuConversions.dfltCnv, EcuConversions.dfltCnv};
EcuDataItem newItem = new EcuDataItem(currPid, 0, 0, 0, 32, 0xFFFFFFFF, dummyCnvs,
"%#08x", null, null,
String.format("PID %02X", currPid)
);
dataItems.appendItemToService(obdService, newItem);
// re-load data items for this PID
items = dataItems.getPidDataItems(obdService, currPid);
}
// loop through all items found ...
for (EcuDataItem pidPv : items)
{
if (pidPv != null)
{
newList.put(pidPv.toString(), pidPv.pv);
}
}
}
pvList.putAll(newList, PvChangeEvent.PV_ADDED, false);
}
/**
* mark all PIDs supported by the vehicle
* @param start Start PID (multiple of 0x20) to process bitmask for
* @param bitmask 32-Bit bitmask which indicates support for the next 32 PIDs
*/
synchronized protected void markSupportedPids(int obdService, int start, long bitmask,
PvList pvList)
{
currSupportedPid = 0;
// loop through bits and mark corresponding PIDs as supported
for (int i = 0; i < 0x1F; i++)
{
if ((bitmask & (0x80000000L >> i)) != 0)
{
pidSupported.add(i + start + 1);
}
}
log.debug(Long.toHexString(bitmask).toUpperCase() + "(" + Long.toHexString(
start) + "):" + pidSupported);
// if next block may be requested
if ((bitmask & 1) != 0)
// request next block
cmdQueue.add(String.format("%02X%02X", obdService, start + 0x20));
else
// setup PID PVs
preparePidPvs(obdService, pvList);
}
/** Holds value of property numCodes. */
private int numCodes;
/** fixed PIDs to limit PID loop to single access */
private static Vector<Integer> fixedPids = new Vector<Integer>();
/**
* Set fixed PID for faster data update
* @param pidCodes the fixedPid to set
*/
public static synchronized void setFixedPid(int[] pidCodes)
{
int curr;
currSupportedPid = 0;
for (Integer aPidSupported : pidSupported)
{
curr = aPidSupported;
if (Arrays.binarySearch(pidCodes, curr) >= 0)
{
fixedPids.add(curr);
}
}
}
public static synchronized void resetFixedPid()
{
fixedPids.clear();
}
/**
* get the next available supported PID
* @return next available supported PID
*/
protected synchronized Integer getNextSupportedPid()
{
Vector<Integer> pidsToCheck = (fixedPids.size() > 0) ? fixedPids : pidSupported;
Integer result = 0;
if (pidsToCheck.size() > 0)
{
result = pidsToCheck.get(currSupportedPid);
currSupportedPid++;
currSupportedPid %= (pidsToCheck.size());
pidsWrapped = (currSupportedPid == 0);
}
return (result);
}
/**
* handle OBD response telegram
* @param buffer - telegram buffer
* @return number of listeners notified
*/
@Override
@SuppressWarnings("fallthrough")
public synchronized int handleTelegram(char[] buffer)
{
int result = 0;
int msgPid;
if (checkTelegram(buffer))
{
try
{
msgService = (Integer) getParamValue(ID_OBD_SVC, buffer);
// check for negative result
if (msgService == OBD_ID_NRC)
{
// get NR service
int svc = (Integer) getParamValue(ID_NR_SVC, buffer);
// get NRC code
int nrcCode = (Integer) getParamValue(ID_NR_CODE, buffer);
// get NRC object
NRC nrc = NRC.get(nrcCode);
// create NRC error message
String error = String.format(nrc.toString(svc));
// log error
log.error(error);
if (isResetOnNrc())
{
// perform immediate reset because NRC reception
reset();
} else
{
// otherwise just switch off any active service
setService(OBD_SVC_NONE, true);
}
// notify change listeners
firePropertyChange(new PropertyChangeEvent(this, PROP_NRC, null, error));
// handling finished
return result;
}
// positive response -> mask service ID
msgService &= ~0x40;
// check service of message
switch (msgService)
{
// OBD Data frame
case OBD_SVC_FREEZEFRAME:
case OBD_SVC_DATA:
msgPid = (Integer) getParamValue(ID_OBD_PID, buffer);
switch (msgPid)
{
case 0x00:
case 0x20:
case 0x40:
case 0x60:
case 0x80:
case 0xA0:
case 0xC0:
case 0xE0:
long msgPayload = Long.valueOf(new String(getPayLoad(buffer)), 16);
markSupportedPids(msgService, msgPid, msgPayload, PidPvs);
break;
// OBD number of fault codes
case 1:
msgPayload = ((Integer) getParamValue(ID_NUM_CODES,
NUMCODE_PARAMETERS,
buffer)).longValue();
setNumCodes(Long.valueOf(msgPayload).intValue());
// no break here ...
default:
dataItems.updateDataItems(msgService,
msgPid,
hexToBytes(String.valueOf(
getPayLoad(buffer))));
break;
}
break;
// get vehicle information (mode 9)
case OBD_SVC_VEH_INFO:
msgPid = (Integer) getParamValue(ID_OBD_PID, buffer);
switch (msgPid)
{
case 0x00:
case 0x20:
case 0x40:
case 0x60:
case 0x80:
case 0xA0:
case 0xC0:
case 0xE0:
long msgPayload = Long.valueOf(new String(getPayLoad(buffer)), 16);
markSupportedPids(msgService, msgPid, msgPayload, VidPvs);
break;
default:
dataItems.updateDataItems(msgService,
msgPid,
hexToBytes(String.valueOf(
getPayLoad(buffer))));
break;
}
break;
// fault code response
case OBD_SVC_READ_CODES:
case OBD_SVC_PENDINGCODES:
case OBD_SVC_PERMACODES:
int currCode;
Integer key;
EcuCodeItem code;
int nCodes = 0;
// default DTC data to start at offset 2 (Byte 1)
int DTCOffs = 2;
// If message contains optional number of codes (1 Byte) then set it ...
if ((buffer.length % 4) == 0)
{
nCodes = Integer.valueOf(new String(buffer, 2, 2), 16);
setNumCodes(nCodes);
// DTC data starts at offset 4 (byte 2)
DTCOffs = 4;
}
// read in all trouble codes
for (int i = DTCOffs; i < buffer.length; i += 4)
{
key = Integer.valueOf(new String(buffer, i, 4), 16);
currCode = key.intValue();
if (currCode != 0)
{
if ((code = (EcuCodeItem) knownCodes.get(key)) == null)
{
code = new ObdCodeItem(key.intValue(),
Messages.getString(
"customer.specific.trouble.code.see.manual"));
}
log.debug(String.format("+DFC: %04x: %s", key, code.toString()));
tCodes.put(key, code);
// increment number of codes
nCodes++;
}
}
if (nCodes == 0)
{
tCodes.put(0, new ObdCodeItem(0, Messages.getString(
"no.trouble.codes.set")));
}
break;
// clear code response
case OBD_SVC_CLEAR_CODES:
break;
default:
log.warn("Service not (yet) supported: " + msgService);
}
} catch (NumberFormatException e)
{
log.warn("'" + buffer.toString() + "':" + e.getMessage());
}
}
return (result);
}
/**
* Notify all telegram Writers about new telegram
* @param buffer - telegram buffer
*/
@Override
public void sendTelegram(char[] buffer)
{
// remember last sent message
lastTxMsg = new String(buffer);
super.sendTelegram(buffer);
}
/**
* Getter for property numCodes.
* @return Value of property numCodes.
*/
public int getNumCodes()
{
return this.numCodes;
}
/**
* Setter for property numCodes.
* @param numCodes New value of property numCodes.
*/
protected void setNumCodes(int numCodes)
{
int old = this.numCodes;
this.numCodes = numCodes;
firePropertyChange(new PropertyChangeEvent(this,
PROP_NUM_CODES,
Integer.valueOf(old),
Integer.valueOf(numCodes)));
}
/**
* Getter for property service.
* @return Value of property service.
*/
public int getService()
{
return this.service;
}
/**
* reset all protocol settings
*/
public void reset()
{
// switch off any active service
setService(OBD_SVC_NONE, true);
// clear command queue
cmdQueue.clear();
// clear supported PIDs
pidSupported.clear();
// reset fixed PIDs
resetFixedPid();
// Clear data items
PidPvs.clear();
tCodes.clear();
VidPvs.clear();
}
/**
* clear data lists for selected service
* @param obdService OBD service to clear lists for
*/
protected void clearDataLists(int obdService)
{
// clean up data lists
switch (obdService)
{
case OBD_SVC_DATA:
case OBD_SVC_FREEZEFRAME:
// Clear data items
pidSupported.clear();
PidPvs.clear();
break;
case OBD_SVC_READ_CODES:
case OBD_SVC_PENDINGCODES:
case OBD_SVC_PERMACODES:
tCodes.clear();
break;
case OBD_SVC_VEH_INFO:
// Clear data items
pidSupported.clear();
VidPvs.clear();
break;
}
}
/**
* Setter for property service.
* This includes initialisation of the requested service to the vehicle
* @param obdService New OBD service to be requested.
* @param clearLists clear data list for this service
*/
public void setService(int obdService, boolean clearLists)
{
this.service = obdService;
pidsWrapped = false;
// if lists shall be cleared
if (clearLists)
{
// then do it
clearDataLists(obdService);
}
// set specified OBD service
switch (obdService)
{
case OBD_SVC_NONE:
// sendCommand(CMD_RESET,0);
break;
case OBD_SVC_DATA:
case OBD_SVC_FREEZEFRAME:
// request for PID's supported
writeTelegram(emptyBuffer, obdService, 0);
break;
case OBD_SVC_READ_CODES:
case OBD_SVC_PENDINGCODES:
case OBD_SVC_PERMACODES:
numCodes = 0;
// read PID number of codes ...
cmdQueue.add("0101");
// read trouble codes
writeTelegram(emptyBuffer, obdService, 0);
break;
case OBD_SVC_CLEAR_CODES:
// clear trouble codes
writeTelegram(emptyBuffer, obdService, 0);
// wait for codes to be cleared
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
// Intentionally do nothing
}
break;
case OBD_SVC_VEH_INFO:
// read vehicle information
writeTelegram(emptyBuffer, obdService, 0);
break;
case OBD_SVC_CTRL_MODE:
case OBD_SVC_O2_RESULT:
case OBD_SVC_MON_RESULT:
default:
log.warn("Service not supported: " + obdService);
}
}
}