From b081ce24c2c4acd1ce8ce1dce2c2817b70be84e0 Mon Sep 17 00:00:00 2001 From: PB2 Date: Fri, 25 Jun 2021 11:50:35 -0400 Subject: [PATCH 01/88] Update example --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4a1de6e..96f329a 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,17 @@ uint32_t rpm = 0; void setup() { +#if LED_BUILTIN pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, HIGH); + digitalWrite(LED_BUILTIN, LOW); +#endif Serial.begin(115200); ELM_PORT.begin(115200); Serial.println("Attempting to connect to ELM327..."); - if (!myELM327.begin(ELM_PORT)) + if (!myELM327.begin(ELM_PORT, true, 2000)) { Serial.println("Couldn't connect to OBD scanner"); while (1); @@ -58,11 +60,7 @@ void loop() Serial.print("RPM: "); Serial.println(rpm); } else - { - Serial.print(F("\tERROR: ")); - Serial.println(myELM327.status); - delay(100); - } + myELM327.printError(); } ``` From 46732790e7733cdce871c57980f9f79c77c0f6b2 Mon Sep 17 00:00:00 2001 From: PB2 Date: Fri, 25 Jun 2021 11:52:15 -0400 Subject: [PATCH 02/88] Simplify example --- README.md | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 96f329a..3eaf402 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ If you're having difficulty in connecting/keeping connection to your ELM327, try SoftwareSerial mySerial(2, 3); // RX, TX -#define ELM_PORT mySerial - - ELM327 myELM327; @@ -30,23 +27,9 @@ uint32_t rpm = 0; void setup() { -#if LED_BUILTIN - pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, LOW); -#endif - Serial.begin(115200); - ELM_PORT.begin(115200); - - Serial.println("Attempting to connect to ELM327..."); - - if (!myELM327.begin(ELM_PORT, true, 2000)) - { - Serial.println("Couldn't connect to OBD scanner"); - while (1); - } - - Serial.println("Connected to ELM327"); + mySerial.begin(115200); + myELM327.begin(mySerial, true, 2000); } From f14b06b01bf0b1b4e0acfc708540be2b5a1c4e37 Mon Sep 17 00:00:00 2001 From: PB2 Date: Fri, 25 Jun 2021 11:54:45 -0400 Subject: [PATCH 03/88] Simplify example even more --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 3eaf402..a9bae62 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,6 @@ void loop() rpm = (uint32_t)tempRPM; Serial.print("RPM: "); Serial.println(rpm); } - else - myELM327.printError(); } ``` From 7b90d1254c7d505252b94f386a75acbeef7c57b7 Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 11 Aug 2021 10:38:14 -0700 Subject: [PATCH 04/88] Decrease data timeout (AT ST) for faster samples --- src/ELMduino.cpp | 30 ++++--- src/ELMduino.h | 208 +++++++++++++++++++++++------------------------ 2 files changed, 123 insertions(+), 115 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 0e968c3..eaaa275 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -4,7 +4,7 @@ /* - bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, const char& protocol, const uint16_t& payloadLen) + bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, const char& protocol, const uint16_t& payloadLen, const byte& dataTimeout) Description: ------------ @@ -12,18 +12,19 @@ Inputs: ------- - * Stream &stream - Pointer to Serial port connected to ELM327 + * Stream &stream - Reference to Serial port connected to ELM327 * bool debug - Specify whether or not to print debug statements to "Serial" * uint16_t timeout - Time in ms to wait for a query response * char protocol - Protocol ID to specify the ELM327 to communicate with the ECU over * uint16_t payloadLen - Maximum number of bytes expected to be returned by the ELM327 after a query - + * byte dataTimeout - Number of ms to wait after receiving data before the ELM327 will + return the data - see https://www.elmelectronics.com/help/obd/tips/#UnderstandingOBD Return: ------- * bool - Whether or not the ELM327 was propperly initialized */ -bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, const char& protocol, const uint16_t& payloadLen) +bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, const char& protocol, const uint16_t& payloadLen, const byte& dataTimeout) { elm_port = &stream; PAYLOAD_LEN = payloadLen; @@ -37,7 +38,7 @@ bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, c return false; // try to connect - if (!initializeELM(protocol)) + if (!initializeELM(protocol, dataTimeout)) return false; return true; @@ -47,7 +48,7 @@ bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, c /* - bool ELM327::initializeELM(const char& protocol) + bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) Description: ------------ @@ -55,7 +56,9 @@ bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, c Inputs: ------- - * char protocol - Protocol ID to specify the ELM327 to communicate with the ECU over + * char protocol - Protocol ID to specify the ELM327 to communicate with the ECU over + * byte dataTimeout - Number of ms to wait after receiving data before the ELM327 will + return the data - see https://www.elmelectronics.com/help/obd/tips/#UnderstandingOBD Return: ------- @@ -80,7 +83,7 @@ bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, c * --> *user adjustable */ -bool ELM327::initializeELM(const char& protocol) +bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) { char command[10] = { '\0' }; connected = false; @@ -100,6 +103,11 @@ bool ELM327::initializeELM(const char& protocol) sendCommand(ALLOW_LONG_MESSAGES); delay(100); + // Set data timeout + sprintf(command, SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); + sendCommand(command); + delay(100); + // Set protocol sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); @@ -123,9 +131,9 @@ bool ELM327::initializeELM(const char& protocol) // Set protocol and save sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - if (sendCommand(command) == ELM_SUCCESS) - if (strstr(payload, "OK") != NULL) - connected = true; + if (sendCommand(command) == ELM_SUCCESS) + if (strstr(payload, "OK") != NULL) + connected = true; if (debugMode) { diff --git a/src/ELMduino.h b/src/ELMduino.h index 725b452..6758704 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -145,112 +145,112 @@ const uint8_t SERVICE_02 = 2; //-------------------------------------------------------------------------------------// // AT commands (https://www.sparkfun.com/datasheets/Widgets/ELM327_AT_Commands.pdf) //-------------------------------------------------------------------------------------// -const char * const DISP_DEVICE_DESCRIPT = "AT @1"; // General -const char * const DISP_DEVICE_ID = "AT @2"; // General -const char * const STORE_DEVICE_ID = "AT @3 %s"; // General -const char * const REPEAT_LAST_COMMAND = "AT \r"; // General -const char * const ALLOW_LONG_MESSAGES = "AT AL"; // General -const char * const AUTOMATIC_RECEIVE = "AT AR"; // OBD -const char * const ADAPTIVE_TIMING_OFF = "AT AT0"; // OBD -const char * const ADAPTIVE_TIMING_AUTO_1 = "AT AT1"; // OBD -const char * const ADAPTIVE_TIMING_AUTO_2 = "AT AT2"; // OBD -const char * const DUMP_BUFFER = "AT BD"; // OBD -const char * const BYPASS_INIT_SEQUENCE = "AT BI"; // OBD -const char * const TRY_BAUD_DIVISOR = "AT BRD %s"; // General -const char * const SET_HANDSHAKE_TIMEOUT = "AT BRT %s"; // General -const char * const CAN_AUTO_FORMAT_OFF = "AT CAF0"; // CAN -const char * const CAN_AUTO_FORMAT_ON = "AT CAF1"; // CAN -const char * const CAN_EXTENDED_ADDRESS_OFF = "AT CEA"; // CAN -const char * const USE_CAN_EXTENDED_ADDRESS = "AT CEA %s"; // CAN -const char * const SET_ID_FILTER = "AT CF %s"; // CAN -const char * const CAN_FLOW_CONTROL_OFF = "AT CFC0"; // CAN -const char * const CAN_FLOW_CONTROL_ON = "AT CFC1"; // CAN -const char * const SET_ID_MASK = "AT CM %s"; // CAN -const char * const SET_CAN_PRIORITY = "AT CP %s"; // CAN -const char * const SHOW_CAN_STATUS = "AT CS"; // CAN -const char * const CAN_SILENT_MODE_OFF = "AT CSM0"; // CAN -const char * const CAN_SILENT_MODE_ON = "AT CSM1"; // CAN -const char * const CALIBRATE_VOLTAGE_CUSTOM = "AT CV %s"; // Volts -const char * const RESTORE_CV_TO_FACTORY = "AT CV 0000"; // Volts -const char * const SET_ALL_TO_DEFAULTS = "AT D"; // General -const char * const DISP_DLC_OFF = "AT D0"; // CAN -const char * const DISP_DLC_ON = "AT D1"; // CAN -const char * const MONITOR_FOR_DM1_MESSAGES = "AT DM1"; // J1939 -const char * const DISP_CURRENT_PROTOCOL = "AT DP"; // OBD -const char * const DISP_CURRENT_PROTOCOL_NUM = "AT DPN"; // OBD -const char * const ECHO_OFF = "AT E0"; // General -const char * const ECHO_ON = "AT E1"; // General +const char * const DISP_DEVICE_DESCRIPT = "AT @1"; // General +const char * const DISP_DEVICE_ID = "AT @2"; // General +const char * const STORE_DEVICE_ID = "AT @3 %s"; // General +const char * const REPEAT_LAST_COMMAND = "AT \r"; // General +const char * const ALLOW_LONG_MESSAGES = "AT AL"; // General +const char * const AUTOMATIC_RECEIVE = "AT AR"; // OBD +const char * const ADAPTIVE_TIMING_OFF = "AT AT0"; // OBD +const char * const ADAPTIVE_TIMING_AUTO_1 = "AT AT1"; // OBD +const char * const ADAPTIVE_TIMING_AUTO_2 = "AT AT2"; // OBD +const char * const DUMP_BUFFER = "AT BD"; // OBD +const char * const BYPASS_INIT_SEQUENCE = "AT BI"; // OBD +const char * const TRY_BAUD_DIVISOR = "AT BRD %02d"; // General +const char * const SET_HANDSHAKE_TIMEOUT = "AT BRT %02d"; // General +const char * const CAN_AUTO_FORMAT_OFF = "AT CAF0"; // CAN +const char * const CAN_AUTO_FORMAT_ON = "AT CAF1"; // CAN +const char * const CAN_EXTENDED_ADDRESS_OFF = "AT CEA"; // CAN +const char * const USE_CAN_EXTENDED_ADDRESS = "AT CEA %02d"; // CAN +const char * const SET_ID_FILTER = "AT CF %s"; // CAN +const char * const CAN_FLOW_CONTROL_OFF = "AT CFC0"; // CAN +const char * const CAN_FLOW_CONTROL_ON = "AT CFC1"; // CAN +const char * const SET_ID_MASK = "AT CM %s"; // CAN +const char * const SET_CAN_PRIORITY = "AT CP %02d"; // CAN +const char * const SHOW_CAN_STATUS = "AT CS"; // CAN +const char * const CAN_SILENT_MODE_OFF = "AT CSM0"; // CAN +const char * const CAN_SILENT_MODE_ON = "AT CSM1"; // CAN +const char * const CALIBRATE_VOLTAGE_CUSTOM = "AT CV %04d"; // Volts +const char * const RESTORE_CV_TO_FACTORY = "AT CV 0000"; // Volts +const char * const SET_ALL_TO_DEFAULTS = "AT D"; // General +const char * const DISP_DLC_OFF = "AT D0"; // CAN +const char * const DISP_DLC_ON = "AT D1"; // CAN +const char * const MONITOR_FOR_DM1_MESSAGES = "AT DM1"; // J1939 +const char * const DISP_CURRENT_PROTOCOL = "AT DP"; // OBD +const char * const DISP_CURRENT_PROTOCOL_NUM = "AT DPN"; // OBD +const char * const ECHO_OFF = "AT E0"; // General +const char * const ECHO_ON = "AT E1"; // General const char * const FLOW_CONTROL_SET_DATA_TO = "AT FC SD %s"; // CAN const char * const FLOW_CONTROL_SET_HEAD_TO = "AT FC SH %s"; // CAN const char * const FLOW_CONTROL_SET_MODE_TO = "AT FC SM %c"; // CAN -const char * const FORGE_EVENTS = "AT FE"; // General -const char * const PERFORM_FAST_INIT = "AT FI"; // ISO -const char * const HEADERS_OFF = "AT H0"; // OBD -const char * const HEADERS_ON = "AT H1"; // OBD -const char * const DISP_ID = "AT I"; // General -const char * const SET_ISO_BAUD_10400 = "AT IB 10"; // ISO -const char * const SET_ISO_BAUD_4800 = "AT IB 48"; // ISO -const char * const SET_ISO_BAUD_9600 = "AT IB 96"; // ISO -const char * const IFR_VAL_FROM_HEADER = "AT IFR H"; // J1850 -const char * const IFR_VAL_FROM_SOURCE = "AT IFR S"; // J1850 -const char * const IFRS_OFF = "AT IFR0"; // J1850 -const char * const IFRS_AUTO = "AT IFR1"; // J1850 -const char * const IFRS_ON = "AT IFR2"; // J1850 -const char * const IREAD_IGNMON_INPUT_LEVEL = "AT IGN"; // Other -const char * const SET_ISO_SLOW_INIT_ADDRESS = "AT IIA %s"; // ISO -const char * const USE_J1939_ELM_DATA_FORMAT = "AT JE"; // J1850 -const char * const J1939_HEAD_FORMAT_OFF = "AT JHF0"; // J1850 -const char * const J1939_HEAD_FORMAT_ON = "AT JHF1"; // J1850 -const char * const USE_J1939_SAE_DATA_FORMAT = "AT JS"; // J1850 -const char * const SET_J1939_TIMER_X_TO_1X = "AT JTM1"; // J1850 -const char * const SET_J1939_TIMER_X_TO_5X = "AT JTM5"; // J1850 -const char * const DISP_KEY_WORDS = "AT KW"; // ISO -const char * const KEY_WORD_CHECKING_OFF = "AT KW0"; // ISO -const char * const KEY_WORD_CHECKING_ON = "AT KW1"; // ISO -const char * const LINEFEEDS_OFF = "AT L0"; // General -const char * const LINEFEEDS_ON = "AT L1"; // General -const char * const LOW_POWER_MODE = "AT LP"; // General -const char * const MEMORY_OFF = "AT M0"; // General -const char * const MEMORY_ON = "AT M1"; // General -const char * const MONITOR_ALL = "AT MA"; // OBD -const char * const MONITOR_FOR_PGN = "AT MP %s"; // J1939 -const char * const MONITOR_FOR_RECEIVER = "AT MR %s"; // OBD -const char * const MONITOR_FOR_TRANSMITTER = "AT MT %s"; // OBD -const char * const NORMAL_LENGTH_MESSAGES = "AT NL"; // OBD -const char * const SET_PROTO_OPTIONS_AND_BAUD = "AT PB %s"; // OBD -const char * const PROTOCOL_CLOSE = "AT PC"; // OBD -const char * const ALL_PROG_PARAMS_OFF = "AT PP FF OFF"; // PPs -const char * const ALL_PROG_PARAMS_ON = "AT PP FF ON"; // PPs -const char * const SET_PROG_PARAM_OFF = "AT PP %s OFF"; // PPs -const char * const SET_PROG_PARAM_ON = "AT PP %s ON"; // PPs +const char * const FORGE_EVENTS = "AT FE"; // General +const char * const PERFORM_FAST_INIT = "AT FI"; // ISO +const char * const HEADERS_OFF = "AT H0"; // OBD +const char * const HEADERS_ON = "AT H1"; // OBD +const char * const DISP_ID = "AT I"; // General +const char * const SET_ISO_BAUD_10400 = "AT IB 10"; // ISO +const char * const SET_ISO_BAUD_4800 = "AT IB 48"; // ISO +const char * const SET_ISO_BAUD_9600 = "AT IB 96"; // ISO +const char * const IFR_VAL_FROM_HEADER = "AT IFR H"; // J1850 +const char * const IFR_VAL_FROM_SOURCE = "AT IFR S"; // J1850 +const char * const IFRS_OFF = "AT IFR0"; // J1850 +const char * const IFRS_AUTO = "AT IFR1"; // J1850 +const char * const IFRS_ON = "AT IFR2"; // J1850 +const char * const IREAD_IGNMON_INPUT_LEVEL = "AT IGN"; // Other +const char * const SET_ISO_SLOW_INIT_ADDRESS = "AT IIA %02d"; // ISO +const char * const USE_J1939_ELM_DATA_FORMAT = "AT JE"; // J1850 +const char * const J1939_HEAD_FORMAT_OFF = "AT JHF0"; // J1850 +const char * const J1939_HEAD_FORMAT_ON = "AT JHF1"; // J1850 +const char * const USE_J1939_SAE_DATA_FORMAT = "AT JS"; // J1850 +const char * const SET_J1939_TIMER_X_TO_1X = "AT JTM1"; // J1850 +const char * const SET_J1939_TIMER_X_TO_5X = "AT JTM5"; // J1850 +const char * const DISP_KEY_WORDS = "AT KW"; // ISO +const char * const KEY_WORD_CHECKING_OFF = "AT KW0"; // ISO +const char * const KEY_WORD_CHECKING_ON = "AT KW1"; // ISO +const char * const LINEFEEDS_OFF = "AT L0"; // General +const char * const LINEFEEDS_ON = "AT L1"; // General +const char * const LOW_POWER_MODE = "AT LP"; // General +const char * const MEMORY_OFF = "AT M0"; // General +const char * const MEMORY_ON = "AT M1"; // General +const char * const MONITOR_ALL = "AT MA"; // OBD +const char * const MONITOR_FOR_PGN = "AT MP %s"; // J1939 +const char * const MONITOR_FOR_RECEIVER = "AT MR %02d"; // OBD +const char * const MONITOR_FOR_TRANSMITTER = "AT MT %02d"; // OBD +const char * const NORMAL_LENGTH_MESSAGES = "AT NL"; // OBD +const char * const SET_PROTO_OPTIONS_AND_BAUD = "AT PB %s"; // OBD +const char * const PROTOCOL_CLOSE = "AT PC"; // OBD +const char * const ALL_PROG_PARAMS_OFF = "AT PP FF OFF";// PPs +const char * const ALL_PROG_PARAMS_ON = "AT PP FF ON"; // PPs +const char * const SET_PROG_PARAM_OFF = "AT PP %s OFF";// PPs +const char * const SET_PROG_PARAM_ON = "AT PP %s ON"; // PPs const char * const SET_PROG_PARAM_VAL = "AT PP %s SV %s"; // PPs -const char * const DISP_PP_SUMMARY = "AT PPS"; // PPs -const char * const RESPONSES_OFF = "AT R0"; // OBD -const char * const RESPONSES_ON = "AT R1"; // OBD -const char * const SET_RECEIVE_ADDRESS_TO = "AT RA %s"; // OBD -const char * const READ_STORED_DATA = "AT RD"; // General -const char * const SEND_RTR_MESSAGE = "AT RTR"; // CAN -const char * const READ_VOLTAGE = "AT RV"; // Volts -const char * const PRINTING_SPACES_OFF = "AT S0"; // OBD -const char * const PRINTING_SPACES_ON = "AT S1"; // OBD -const char * const STORE_DATA_BYTE = "AT SD "; // General -const char * const SET_HEADER = "AT SH %s"; // OBD -const char * const PERFORM_SLOW_INIT = "AT SI"; // ISO -const char * const SET_PROTOCOL_TO_AUTO_H_SAVE = "AT SP A%c"; // OBD -const char * const SET_PROTOCOL_TO_H_SAVE = "AT SP %c"; // OBD -const char * const SET_PROTOCOL_TO_AUTO_SAVE = "AT SP 00"; // OBD -const char * const SET_REC_ADDRESS = "AT SR %s"; // OBD -const char * const SET_STANDARD_SEARCH_ORDER = "AT SS"; // OBD -const char * const SET_TIMEOUT_TO_H_X_4MS = "AT ST %s"; // OBD -const char * const SET_WAKEUP_TO_H_X_20MS = "AT SW %s"; // ISO -const char * const SET_TESTER_ADDRESS_TO = "AT TA %s"; // OBD -const char * const TRY_PROT_H_AUTO_SEARCH = "AT TP A%c"; // OBD -const char * const TRY_PROT_H = "AT TP %c"; // OBD -const char * const VARIABLE_DLC_OFF = "AT V0"; // CAN -const char * const VARIABLE_DLC_ON = "AT V1"; // CAN -const char * const SET_WAKEUP_MESSAGE = "AT WM"; // ISO -const char * const WARM_START = "AT WS"; // General -const char * const RESET_ALL = "AT Z"; // General +const char * const DISP_PP_SUMMARY = "AT PPS"; // PPs +const char * const RESPONSES_OFF = "AT R0"; // OBD +const char * const RESPONSES_ON = "AT R1"; // OBD +const char * const SET_RECEIVE_ADDRESS_TO = "AT RA %02d"; // OBD +const char * const READ_STORED_DATA = "AT RD"; // General +const char * const SEND_RTR_MESSAGE = "AT RTR"; // CAN +const char * const READ_VOLTAGE = "AT RV"; // Volts +const char * const PRINTING_SPACES_OFF = "AT S0"; // OBD +const char * const PRINTING_SPACES_ON = "AT S1"; // OBD +const char * const STORE_DATA_BYTE = "AT SD "; // General +const char * const SET_HEADER = "AT SH %s"; // OBD +const char * const PERFORM_SLOW_INIT = "AT SI"; // ISO +const char * const SET_PROTOCOL_TO_AUTO_H_SAVE = "AT SP A%c"; // OBD +const char * const SET_PROTOCOL_TO_H_SAVE = "AT SP %c"; // OBD +const char * const SET_PROTOCOL_TO_AUTO_SAVE = "AT SP 00"; // OBD +const char * const SET_REC_ADDRESS = "AT SR %02d"; // OBD +const char * const SET_STANDARD_SEARCH_ORDER = "AT SS"; // OBD +const char * const SET_TIMEOUT_TO_H_X_4MS = "AT ST %02d"; // OBD +const char * const SET_WAKEUP_TO_H_X_20MS = "AT SW %02d"; // ISO +const char * const SET_TESTER_ADDRESS_TO = "AT TA %02d"; // OBD +const char * const TRY_PROT_H_AUTO_SEARCH = "AT TP A%c"; // OBD +const char * const TRY_PROT_H = "AT TP %c"; // OBD +const char * const VARIABLE_DLC_OFF = "AT V0"; // CAN +const char * const VARIABLE_DLC_ON = "AT V1"; // CAN +const char * const SET_WAKEUP_MESSAGE = "AT WM"; // ISO +const char * const WARM_START = "AT WS"; // General +const char * const RESET_ALL = "AT Z"; // General @@ -300,8 +300,8 @@ class ELM327 - bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40); - bool initializeELM(const char& protocol='0'); + bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40, const byte& dataTimeout = 0); + bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); void flushInputBuff(); uint64_t findResponse(); bool queryPID(uint8_t service, uint16_t pid); From 246d994b387da39c331c5926f03095213121bde0 Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 11 Aug 2021 13:40:21 -0400 Subject: [PATCH 05/88] Update library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index b7f67f4..6eaafe4 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=2.6.0 +version=2.6.1 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 807c0c3a23dd1341516a63b1f2cef6948199662a Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 7 Sep 2021 15:17:18 -0400 Subject: [PATCH 06/88] Prevent dropping of semicolons for special PID responses (#90) --- src/ELMduino.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index eaaa275..d498e2c 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2425,7 +2425,7 @@ int8_t ELM327::sendCommand(const char *cmd) break; } - else if (!isalnum(recChar)) + else if (!isalnum(recChar) && (recChar != ':')) continue; payload[counter] = recChar; From b14d4c9634448e98fdccebc07a773c8b76e9ccc4 Mon Sep 17 00:00:00 2001 From: PB2 Date: Fri, 10 Sep 2021 08:26:45 -0400 Subject: [PATCH 07/88] Update library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 6eaafe4..ea689e9 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=2.6.1 +version=2.6.2 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 92f307850b6e2f688a13aa25d13da44b5ca40b7f Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 6 Oct 2021 15:54:57 -0400 Subject: [PATCH 08/88] Make conditionResponse() public (#92) --- src/ELMduino.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ELMduino.h b/src/ELMduino.h index 6758704..4ba3aa5 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -308,6 +308,7 @@ class ELM327 bool queryPID(char queryStr[]); int8_t sendCommand(const char *cmd); bool timeout(); + float conditionResponse(const uint64_t& response, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); uint32_t supportedPIDs_1_20(); @@ -413,5 +414,4 @@ class ELM327 int8_t nextIndex(char const *str, char const *target, uint8_t numOccur); - float conditionResponse(const uint64_t& response, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); }; From 52b51eba788f2c7c66dab1dc3d24b3e766df90ae Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 6 Oct 2021 15:55:38 -0400 Subject: [PATCH 09/88] Update library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index ea689e9..3744658 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=2.6.2 +version=2.6.3 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 0daab76ee090c7222cdb05f26b8274e17864a6b0 Mon Sep 17 00:00:00 2001 From: idavidka Date: Tue, 16 Nov 2021 14:39:00 +0100 Subject: [PATCH 10/88] Update ELMduino.cpp increade counter size as there are some pids which gets payload more than 255 bytes. For example Hyundai Ioniq EV 2101 Sample response: 7EC103D6101FFFFFFFF7ED10336101FFFFFBC07EA10166101FFE000007EB101E6101000003FF7ED2100991A94DF59A67EE037F21127EC219926112648A3FF7EA21092112380636037EB210838019994ED367EC22A60EDB0E0D0E0F7ED220472A6024700BB7EA2200000000A871347EB220000000A0027027EC230D0D0E000EC60A7ED23057143102000C87EA23072000000000007EB233F04DE360000007ED24078C014C7600177EC24C53800008A00037EB24000000000000007ED25001301F495272A7EC25099200030723007ED26C10012003203E77EC26011A24000110287EC2700B1D46309017C7ED27058401000000007EC280000000003E800 --- src/ELMduino.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index d498e2c..c4e65f8 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2367,7 +2367,7 @@ uint16_t ELM327::auxSupported() */ int8_t ELM327::sendCommand(const char *cmd) { - uint8_t counter = 0; + uint16_t counter = 0; connected = false; // clear payload buffer From 44084a25e4941ae5efabc8c1f2953239759e8706 Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 16 Nov 2021 15:36:03 -0500 Subject: [PATCH 11/88] Update library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 3744658..d162bd3 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=2.6.3 +version=2.6.4 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 8780d5fb05bf097b2e0d1d0268ca075110b51a36 Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Sat, 1 Jan 2022 19:14:43 +1030 Subject: [PATCH 12/88] Make library non-blocking --- .../non blocking M5Stack Core2.ino | 253 ++ .../non_blocking_1/non blocking example 1.ino | 55 + .../non_blocking_2/non blocking example 2.ino | 127 + src/ELMduino.cpp | 2976 +++++++++-------- src/ELMduino.h | 29 +- 5 files changed, 2020 insertions(+), 1420 deletions(-) create mode 100644 examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino create mode 100644 examples/non_blocking_1/non blocking example 1.ino create mode 100644 examples/non_blocking_2/non blocking example 2.ino diff --git a/examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino b/examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino new file mode 100644 index 0000000..267025c --- /dev/null +++ b/examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino @@ -0,0 +1,253 @@ +#include +#include + +#include "BluetoothSerial.h" + +ELM327 myELM327; +BluetoothSerial SerialBT; + +char txt[80] = ""; + +// #define USE_MAC_NAME // Comment this line out to specify OBDII scanner bluetooth mac address. Uncomment to specify bluetooth name +#if defined USE_MAC_NAME +char bt_name[] = "OBDLink LX"; +// char bt_name[] = "OBDII"; +// char bt_name[] = "V-LINK"; +#else +// char bluetooth_mac_str[] = "00:1D:A5:68:98:8C"; // EM327 mini clone Blue +// char bluetooth_mac_str[] = "00:1D:A5:22:69:9F"; // VGate iCar Pro black and gold +char bluetooth_mac_str[] = "00:1d:a5:22:5f:f6"; // VGate iCar Pro black and gold +// char bluetooth_mac_str[] = "00:04:3E:53:5E:0D"; +// char bluetooth_mac_str[] = "00:04:3E:53:5E:0D"; // OBDLink LX green +uint8_t bt_mac[6] = {0x00}; +#endif +char pin[] = "1234"; + +typedef enum { IDLE, + BARO_PRESS, + COOLANT_TEMP, + OIL_TEMP, + MANIFOLD_PRESS, + ENG_RPM, + BATT_VOLT } obd_pid_states; + +obd_pid_states obd_state = IDLE; +float coolant_temperature = 0.0; +float battery_voltage = 0.0; +float eng_rpm = 0.0; +uint32_t slow_data_last_time = millis(); +uint32_t fast_data_last_time = millis(); + +void setup() { + auto cfg = M5.config(); + cfg.serial_baudrate = 115200; // default=115200. if "Serial" is not needed, set it to 0. + cfg.clear_display = true; // default=true. clear the screen when begin. + cfg.output_power = false; // default=true. use external port 5V output. + cfg.internal_imu = false; // default=true. use internal IMU. + cfg.internal_rtc = true; // default=true. use internal RTC. + cfg.external_imu = false; // default=false. use Unit Accel & Gyro. + cfg.external_rtc = false; // default=false. use Unit RTC. + cfg.led_brightness = 0; // default= 0. Green LED brightness (0=off / 255=max) (※ not NeoPixel) + + M5.begin(cfg); + M5.Lcd.setBrightness((70 * 255) / 100); // Set LCD brightness to 70% of maximum + + M5.Lcd.setFont(&fonts::FreeSans12pt7b); + M5.Lcd.setTextDatum(top_left); + M5.Lcd.setTextPadding(0); + M5.Lcd.setTextColor(TFT_GREEN, TFT_BLACK); + + uint16_t xx = 0; + uint16_t yy = 10; + M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); + sprintf(txt, "%s", "Starting ESP32 BT"); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + + // Start ESP32 bluetooth + SerialBT.begin("ELM_NonBlock", true); + + M5.Lcd.setTextPadding(M5.Lcd.textWidth(txt) + 10); + M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); + sprintf(txt, "%s", "BT Started"); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + + yy += M5.Lcd.fontHeight(); + M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); + M5.Lcd.setFont(&fonts::FreeSans9pt7b); + sprintf(txt, "%s", "Trying to connect OBDII scanner"); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + + yy += M5.Lcd.fontHeight(); + M5.Lcd.setTextColor(TFT_GREEN, TFT_BLACK); + bool bt_connected = false; +// Attempt to connect ESP32 to bluetooth OBDII scanner +// SerialBT.setPin(pin); +#if defined USE_MAC_NAME + sprintf(txt, "%s = %s", "BT Name", bt_name); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + bt_connected = SerialBT.connect(bt_name); +#else + // Convert bluetooth MAC address string into integer array[6] + sscanf(bluetooth_mac_str, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", + bt_mac, bt_mac + 1, bt_mac + 2, + bt_mac + 3, bt_mac + 4, bt_mac + 5); + sprintf(txt, "%s = %s", "MAC address", bluetooth_mac_str); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + bt_connected = SerialBT.connect(bt_mac); +#endif + + yy += M5.Lcd.fontHeight(); + + if (!bt_connected) { + sprintf(txt, "%s", "Can't connect OBD - Phase 1"); + Serial.println(txt); + M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); + M5.Lcd.drawString(txt, xx, yy); + while (1) + ; + } + + M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); + sprintf(txt, "%s", "Phase 1 ok"); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + + yy += M5.Lcd.fontHeight(); + M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); + sprintf(txt, "%s", "Trying to start ELM327"); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + + bt_connected = myELM327.begin(SerialBT, true, 2000); + + yy += M5.Lcd.fontHeight(); + + if (!bt_connected) { + sprintf(txt, "%s", "Can't connect OBD - Phase 2"); + Serial.println(txt); + M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); + M5.Lcd.drawString(txt, xx, yy); + + while (1) + ; + } + + M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); + sprintf(txt, "%s", "Phase 2 ok"); + Serial.println(txt); + M5.Lcd.drawString(txt, xx, yy); + + delay(2000); + + M5.Lcd.clear(); + + // Label for Coolant Temperature + xx = 0; + M5.Lcd.setFont(&fonts::FreeSans12pt7b); + M5.Lcd.setTextDatum(top_left); + M5.Lcd.setTextPadding(100); + M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); + M5.Lcd.drawString("Coolant Temp:", xx, 10); + + // Label for RPM + M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); + M5.Lcd.drawString("RPM:", xx, 50); + + // Label for battery voltage + M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); + M5.Lcd.drawString("Battery:", xx, 90); + +} + +void loop() { + uint16_t xx = 180; + uint16_t yy = 10; + + switch (obd_state) { + case IDLE: + if (millis() > slow_data_last_time + 5000) { + slow_data_last_time = millis(); + obd_state = BATT_VOLT; + Serial.printf("IDLE, query SLOW PIDs at %.3f s\n", slow_data_last_time / 1000.0); + } else if (millis() > fast_data_last_time + 1000) { + fast_data_last_time = millis(); + obd_state = COOLANT_TEMP; + Serial.printf("IDLE, query FAST PIDs at %.3f s\n", fast_data_last_time / 1000.0); + } else { + // Serial.printf("[%.3f s] IDLE\n", millis() / 1000.0); + } + break; + + case BARO_PRESS: + break; + + case COOLANT_TEMP: + coolant_temperature = myELM327.engineCoolantTemp(); + yy = 10; + if (myELM327.nb_rx_state == ELM_SUCCESS) { + // Display coolant temperature + M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); + sprintf(txt, "%.0f deg", coolant_temperature); + M5.Lcd.drawString(txt, xx, yy); + Serial.printf("Coolant temp = %s\n", txt); + obd_state = ENG_RPM; + } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { + myELM327.printError(); + M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); + sprintf(txt, "%s", "N/A deg"); + M5.Lcd.drawString(txt, xx, yy); + obd_state = ENG_RPM; + } + break; + + case OIL_TEMP: + break; + + case MANIFOLD_PRESS: + break; + + case ENG_RPM: + eng_rpm = myELM327.rpm(); + yy = 50; + if (myELM327.nb_rx_state == ELM_SUCCESS) { + // Display coolant temperature + M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); + sprintf(txt, "%.0f RPM", eng_rpm); + M5.Lcd.drawString(txt, xx, yy); + Serial.printf("Engine RPM = %s\n", txt); + obd_state = IDLE; + } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { + myELM327.printError(); + M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); + sprintf(txt, "%s", "N/A RPM"); + M5.Lcd.drawString(txt, xx, yy); + obd_state = IDLE; + } + break; + + case BATT_VOLT: + battery_voltage = myELM327.batteryVoltage(); + yy = 90; + if (myELM327.nb_rx_state == ELM_SUCCESS) { + // Display vehicle battery voltage + M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); + sprintf(txt, "%.1f VDC", battery_voltage); + M5.Lcd.drawString(txt, xx, yy); + Serial.printf("Battery voltage = %s\n", txt); + + obd_state = IDLE; + } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { + myELM327.printError(); + M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); + sprintf(txt, "%s", "N/A VDC"); + M5.Lcd.drawString(txt, xx, yy); + obd_state = IDLE; + } + break; + } +} diff --git a/examples/non_blocking_1/non blocking example 1.ino b/examples/non_blocking_1/non blocking example 1.ino new file mode 100644 index 0000000..7753bdf --- /dev/null +++ b/examples/non_blocking_1/non blocking example 1.ino @@ -0,0 +1,55 @@ +#include "BluetoothSerial.h" +#include "ELMduino.h" + + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + + +ELM327 myELM327; + + +uint32_t rpm = 0; + +uint8_t bt_mac[6] = {0x00, 0x1d, 0xa5, 0x22, 0x5f, 0xf6}; + +void setup() +{ +#if LED_BUILTIN + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); +#endif + + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect(bt_mac)) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while(1); + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + Serial.println("Couldn't connect to OBD scanner - Phase 2"); + while (1); + } + + Serial.println("Connected to ELM327"); +} + + +void loop() +{ + float tempRPM = myELM327.rpm(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + rpm = (uint32_t)tempRPM; + Serial.print("RPM: "); Serial.println(rpm); + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + myELM327.printError(); +} diff --git a/examples/non_blocking_2/non blocking example 2.ino b/examples/non_blocking_2/non blocking example 2.ino new file mode 100644 index 0000000..aa783f9 --- /dev/null +++ b/examples/non_blocking_2/non blocking example 2.ino @@ -0,0 +1,127 @@ +#include +#include "BluetoothSerial.h" + +ELM327 myELM327; +BluetoothSerial SerialBT; + +char txt[80] = ""; + +// #define USE_MAC_NAME // Comment this line out to specify OBDII scanner bluetooth mac address. Uncomment to specify bluetooth name +#if defined USE_MAC_NAME +char bt_name[] = "OBDII"; +#else +char bluetooth_mac_str[] = "00:1d:a5:22:5f:f6"; // VGate iCar Pro black and gold +uint8_t bt_mac[6] = {0x00}; +#endif +char pin[] = "1234"; + +typedef enum { IDLE, + COOLANT_TEMP, + OIL_TEMP, + ENG_RPM, + BATT_VOLT } obd_pid_states; + +obd_pid_states obd_state = IDLE; +float coolant_temperature = 0.0; +float battery_voltage = 0.0; +float eng_rpm = 0.0; +uint32_t slow_data_last_time = millis(); +uint32_t fast_data_last_time = millis(); + +void setup() { + Serial.begin(115200); + Serial.println(F("Starting ESP32 BT")); + + // Start ESP32 bluetooth + SerialBT.begin("ELM_NonBlock", true); + + Serial.println(F("BT Started")); + Serial.println(F("Trying to connect OBDII scanner")); + + bool bt_connected = false; +// Attempt to connect ESP32 to bluetooth OBDII scanner +// SerialBT.setPin(pin); +#if defined USE_MAC_NAME + Serial.printf("%s = %s\n", "BT Name", bt_name); + bt_connected = SerialBT.connect(bt_name); +#else + // Convert bluetooth MAC address string into integer array[6] + sscanf(bluetooth_mac_str, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", + bt_mac, bt_mac + 1, bt_mac + 2, + bt_mac + 3, bt_mac + 4, bt_mac + 5); + Serial.printf("%s = %s\n", "MAC address", bluetooth_mac_str); + bt_connected = SerialBT.connect(bt_mac); +#endif + + if (!bt_connected) { + Serial.println(F("Couldn't connect to OBD scanner - Phase 1")); + while (1); + } + + Serial.println(F("Phase 1 ok. Trying to start ELM327")); + bt_connected = myELM327.begin(SerialBT, true, 2000); + + if (!bt_connected) { + Serial.println("Couldn't connect to OBD scanner - Phase 2"); + while (1); + } + + Serial.println(F("Phase 2 ok")); +} + +void loop() { + switch (obd_state) { + case IDLE: + // Schedule PIDs to be read at different rates (slow PIDs and fast PIDs) + if (millis() > slow_data_last_time + 5000) { + slow_data_last_time = millis(); + obd_state = BATT_VOLT; + Serial.printf("IDLE, query SLOW PIDs at %.3f s\n", slow_data_last_time / 1000.0); + } else if (millis() > fast_data_last_time + 1000) { + fast_data_last_time = millis(); + obd_state = COOLANT_TEMP; // Specify the first slow PID here + Serial.printf("IDLE, query FAST PIDs at %.3f s\n", fast_data_last_time / 1000.0); + } else { + // Serial.printf("[%.3f s] IDLE\n", millis() / 1000.0); + } + break; + + case COOLANT_TEMP: + coolant_temperature = myELM327.engineCoolantTemp(); + if (myELM327.nb_rx_state == ELM_SUCCESS) { + Serial.printf("Coolant temp = %.0f\n", coolant_temperature); + obd_state = OIL_TEMP; + } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { + myELM327.printError(); + obd_state = OIL_TEMP; + } + break; + + case OIL_TEMP: + // Not yet implemented + obd_state = ENG_RPM; + break; + + case ENG_RPM: + eng_rpm = myELM327.rpm(); + if (myELM327.nb_rx_state == ELM_SUCCESS) { + Serial.printf("Engine RPM = %.0f\n", eng_rpm); + obd_state = IDLE; + } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { + myELM327.printError(); + obd_state = IDLE; + } + break; + + case BATT_VOLT: + battery_voltage = myELM327.batteryVoltage(); + if (myELM327.nb_rx_state == ELM_SUCCESS) { + Serial.printf("Battery voltage = %.1f\n", battery_voltage); + obd_state = IDLE; + } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { + myELM327.printError(); + obd_state = IDLE; + } + break; + } +} diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index c4e65f8..b5afe46 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -21,17 +21,16 @@ return the data - see https://www.elmelectronics.com/help/obd/tips/#UnderstandingOBD Return: ------- - * bool - Whether or not the ELM327 was propperly - initialized + * bool - Whether or not the ELM327 was properly initialized */ -bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, const char& protocol, const uint16_t& payloadLen, const byte& dataTimeout) +bool ELM327::begin(Stream &stream, const bool &debug, const uint16_t &timeout, const char &protocol, const uint16_t &payloadLen, const byte &dataTimeout) { - elm_port = &stream; + elm_port = &stream; PAYLOAD_LEN = payloadLen; - debugMode = debug; - timeout_ms = timeout; + debugMode = debug; + timeout_ms = timeout; - payload = (char*)malloc(PAYLOAD_LEN + 1); // allow for terminating '\0' + payload = (char *)malloc(PAYLOAD_LEN + 1); // allow for terminating '\0' // test if serial port is connected if (!elm_port) @@ -40,7 +39,7 @@ bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, c // try to connect if (!initializeELM(protocol, dataTimeout)) return false; - + return true; } @@ -83,35 +82,35 @@ bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, c * --> *user adjustable */ -bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) +bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) { - char command[10] = { '\0' }; + char command[10] = {'\0'}; connected = false; - sendCommand(SET_ALL_TO_DEFAULTS); + sendCommand_Blocking(SET_ALL_TO_DEFAULTS); delay(100); - sendCommand(RESET_ALL); + sendCommand_Blocking(RESET_ALL); delay(100); - sendCommand(ECHO_OFF); + sendCommand_Blocking(ECHO_OFF); delay(100); - sendCommand(PRINTING_SPACES_OFF); + sendCommand_Blocking(PRINTING_SPACES_OFF); delay(100); - sendCommand(ALLOW_LONG_MESSAGES); + sendCommand_Blocking(ALLOW_LONG_MESSAGES); delay(100); // Set data timeout sprintf(command, SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); - sendCommand(command); + sendCommand_Blocking(command); delay(100); // Set protocol sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); - if (sendCommand(command) == ELM_SUCCESS) + if (sendCommand_Blocking(command) == ELM_SUCCESS) { if (strstr(payload, "OK") != NULL) { @@ -127,11 +126,11 @@ bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) Serial.print(F(" did not work - trying via ")); Serial.println(SET_PROTOCOL_TO_H_SAVE); } - + // Set protocol and save sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - if (sendCommand(command) == ELM_SUCCESS) + if (sendCommand_Blocking(command) == ELM_SUCCESS) if (strstr(payload, "OK") != NULL) connected = true; @@ -149,7 +148,7 @@ bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) /* - void ELM327::formatQueryArray(uint8_t service, uint16_t pid) + void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses) Description: ------------ @@ -159,12 +158,13 @@ bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) ------- * uint16_t service - Service number of the queried PID * uint32_t pid - PID number of the queried PID + * uint8_t num_responses - see function header for "queryPID()" Return: ------- * void */ -void ELM327::formatQueryArray(uint8_t service, uint16_t pid) +void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses) { if (debugMode) { @@ -190,6 +190,8 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid) query[3] = ((pid >> 8) & 0xF) + '0'; query[4] = ((pid >> 4) & 0xF) + '0'; query[5] = (pid & 0xF) + '0'; + query[6] = num_responses + '0'; + query[7] = '\0'; upper(query, 6); } @@ -202,7 +204,7 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid) query[2] = ((pid >> 4) & 0xF) + '0'; query[3] = (pid & 0xF) + '0'; - query[4] = '\0'; + query[4] = num_responses + '0'; query[5] = '\0'; upper(query, 4); @@ -324,14 +326,14 @@ uint8_t ELM327::ctoi(uint8_t value) numOccur'th instance of target in str */ int8_t ELM327::nextIndex(char const *str, - char const *target, - uint8_t numOccur=1) + char const *target, + uint8_t numOccur = 1) { char const *p = str; char const *r = str; uint8_t count; - for (count = 0; ; ++count) + for (count = 0;; ++count) { p = strstr(p, target); @@ -353,7 +355,7 @@ int8_t ELM327::nextIndex(char const *str, -float ELM327::conditionResponse(const uint64_t& response, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) +float ELM327::conditionResponse(const uint64_t &response, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { return ((response >> (((numPayChars / 2) - numExpectedBytes) * 8)) * scaleFactor) + bias; } @@ -389,29 +391,30 @@ void ELM327::flushInputBuff() /* - bool ELM327::queryPID(uint8_t service, - uint16_t PID, - float &value) + bool ELM327::queryPID(uint8_t service, uint16_t pid, uint8_t num_responses) - Description: - ------------ - * Queries ELM327 for a specific type of vehicle telemetry data + Description: + ------------ + * create a PID query command string and send the command - Inputs: - ------- - * uint8_t service - Service number - * uint8_t PID - PID number + Inputs: + ------- + * uint8_t service - the diagnostic service ID. 01 is "Show current data" + * uint16_t pid - the Parameter ID (PID) from the service + * uint8_t num_responses - number of lines of data to receive - see ELM datasheet "Talking to the vehicle". + This can speed up retrieval of information if you know how many responses will be sent. + Basically the OBD scanner will not wait for more responses if it does not need to go through + final timeout. Also prevents OBD scanners from sending mulitple of the same response. - Return: - ------- + Return: + ------- * bool - Whether or not the query was submitted successfully */ -bool ELM327::queryPID(uint8_t service, - uint16_t pid) +bool ELM327::queryPID(uint8_t service, uint16_t pid, uint8_t num_responses) { - formatQueryArray(service, pid); + formatQueryArray(service, pid, num_responses); sendCommand(query); - + return connected; } @@ -441,147 +444,147 @@ bool ELM327::queryPID(char queryStr[]) longQuery = true; sendCommand(queryStr); - + return connected; } -/* - uint32_t ELM327::supportedPIDs_1_20() +// /* +// uint32_t ELM327::supportedPIDs_1_20() - Description: - ------------ - * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) +// Description: +// ------------ +// * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint32_t - Bit encoded booleans of supported PIDs 0x1-0x20 -*/ -uint32_t ELM327::supportedPIDs_1_20() -{ - if (queryPID(SERVICE_01, SUPPORTED_PIDS_1_20)) - return conditionResponse(findResponse(), 4); +// Return: +// ------- +// * uint32_t - Bit encoded booleans of supported PIDs 0x1-0x20 +// */ +// uint32_t ELM327::supportedPIDs_1_20() +// { +// if (queryPID(SERVICE_01, SUPPORTED_PIDS_1_20)) +// return conditionResponse(findResponse(), 4); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint32_t ELM327::monitorStatus() +// /* +// uint32_t ELM327::monitorStatus() - Description: - ------------ - * Monitor status since DTCs cleared (Includes malfunction indicator - lamp (MIL) status and number of DTCs). See https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01 - for more info +// Description: +// ------------ +// * Monitor status since DTCs cleared (Includes malfunction indicator +// lamp (MIL) status and number of DTCs). See https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01 +// for more info - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01) -*/ -uint32_t ELM327::monitorStatus() -{ - if (queryPID(SERVICE_01, MONITOR_STATUS_SINCE_DTC_CLEARED)) - return conditionResponse(findResponse(), 4); +// Return: +// ------- +// * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01) +// */ +// uint32_t ELM327::monitorStatus() +// { +// if (queryPID(SERVICE_01, MONITOR_STATUS_SINCE_DTC_CLEARED)) +// return conditionResponse(findResponse(), 4); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint16_t ELM327::freezeDTC() +// /* +// uint16_t ELM327::freezeDTC() - Description: - ------------ - * Freeze DTC - see https://www.samarins.com/diagnose/freeze-frame.html for more info +// Description: +// ------------ +// * Freeze DTC - see https://www.samarins.com/diagnose/freeze-frame.html for more info - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint16_t - Various vehicle information (https://www.samarins.com/diagnose/freeze-frame.html) -*/ -uint16_t ELM327::freezeDTC() -{ - if (queryPID(SERVICE_01, FREEZE_DTC)) - return conditionResponse(findResponse(), 2); +// Return: +// ------- +// * uint16_t - Various vehicle information (https://www.samarins.com/diagnose/freeze-frame.html) +// */ +// uint16_t ELM327::freezeDTC() +// { +// if (queryPID(SERVICE_01, FREEZE_DTC)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint16_t ELM327::fuelSystemStatus() +// /* +// uint16_t ELM327::fuelSystemStatus() - Description: - ------------ - * Freeze DTC - see https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03 for more info +// Description: +// ------------ +// * Freeze DTC - see https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03 for more info - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint16_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03) -*/ -uint16_t ELM327::fuelSystemStatus() -{ - if (queryPID(SERVICE_01, FUEL_SYSTEM_STATUS)) - return conditionResponse(findResponse(), 2); +// Return: +// ------- +// * uint16_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03) +// */ +// uint16_t ELM327::fuelSystemStatus() +// { +// if (queryPID(SERVICE_01, FUEL_SYSTEM_STATUS)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::engineLoad() +// /* +// float ELM327::engineLoad() - Description: - ------------ - * Find the current engine load in % +// Description: +// ------------ +// * Find the current engine load in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Engine load % -*/ -float ELM327::engineLoad() -{ - if (queryPID(SERVICE_01, ENGINE_LOAD)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Engine load % +// */ +// float ELM327::engineLoad() +// { +// if (queryPID(SERVICE_01, ENGINE_LOAD)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } /* - float ELM327::engineCoolantTemp() + float ELM327::engineCoolantTemp(void) Description: ------------ @@ -593,171 +596,184 @@ float ELM327::engineLoad() Return: ------- - * float - Engine load % + * float - Coolant temperature in C */ -float ELM327::engineCoolantTemp() +float ELM327::engineCoolantTemp(void) { - if (queryPID(SERVICE_01, ENGINE_COOLANT_TEMP)) - return conditionResponse(findResponse(), 1, 1, -40.0); - - return ELM_GENERAL_ERROR; + if (nb_query_state == SEND_COMMAND) + { + queryPID(SERVICE_01, ENGINE_COOLANT_TEMP, 1); + nb_query_state = WAITING_RESP; + } + else if (nb_query_state == WAITING_RESP) + { + get_response(); + if (nb_rx_state == ELM_SUCCESS) + { + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + return conditionResponse(findResponse(), 1, 1.0, -40.0); + } + else if (nb_rx_state != ELM_GETTING_MSG) + nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command + } + return 0.0; } -/* - float ELM327::shortTermFuelTrimBank_1() +// /* +// float ELM327::shortTermFuelTrimBank_1() - Description: - ------------ - * Find fuel trim % +// Description: +// ------------ +// * Find fuel trim % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel trim % -*/ -float ELM327::shortTermFuelTrimBank_1() -{ - if (queryPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_1)) - return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); +// Return: +// ------- +// * float - Fuel trim % +// */ +// float ELM327::shortTermFuelTrimBank_1() +// { +// if (queryPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_1)) +// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::longTermFuelTrimBank_1() +// /* +// float ELM327::longTermFuelTrimBank_1() - Description: - ------------ - * Find fuel trim % +// Description: +// ------------ +// * Find fuel trim % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel trim % -*/ -float ELM327::longTermFuelTrimBank_1() -{ - if (queryPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_1)) - return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); +// Return: +// ------- +// * float - Fuel trim % +// */ +// float ELM327::longTermFuelTrimBank_1() +// { +// if (queryPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_1)) +// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::shortTermFuelTrimBank_2() +// /* +// float ELM327::shortTermFuelTrimBank_2() - Description: - ------------ - * Find fuel trim % +// Description: +// ------------ +// * Find fuel trim % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel trim % -*/ -float ELM327::shortTermFuelTrimBank_2() -{ - if (queryPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_2)) - return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); +// Return: +// ------- +// * float - Fuel trim % +// */ +// float ELM327::shortTermFuelTrimBank_2() +// { +// if (queryPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_2)) +// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::longTermFuelTrimBank_2() +// /* +// float ELM327::longTermFuelTrimBank_2() - Description: - ------------ - * Find fuel trim % +// Description: +// ------------ +// * Find fuel trim % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel trim % -*/ -float ELM327::longTermFuelTrimBank_2() -{ - if (queryPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_2)) - return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); +// Return: +// ------- +// * float - Fuel trim % +// */ +// float ELM327::longTermFuelTrimBank_2() +// { +// if (queryPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_2)) +// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::fuelPressure() +// /* +// float ELM327::fuelPressure() - Description: - ------------ - * Find fuel pressure in kPa +// Description: +// ------------ +// * Find fuel pressure in kPa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel pressure in kPa -*/ -float ELM327::fuelPressure() -{ - if (queryPID(SERVICE_01, FUEL_PRESSURE)) - return conditionResponse(findResponse(), 1, 3.0); +// Return: +// ------- +// * float - Fuel pressure in kPa +// */ +// float ELM327::fuelPressure() +// { +// if (queryPID(SERVICE_01, FUEL_PRESSURE)) +// return conditionResponse(findResponse(), 1, 3.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::manifoldPressure() +// /* +// uint8_t ELM327::manifoldPressure() - Description: - ------------ - * Find intake manifold absolute pressure in kPa +// Description: +// ------------ +// * Find intake manifold absolute pressure in kPa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Intake manifold absolute pressure in kPa -*/ -uint8_t ELM327::manifoldPressure() -{ - if (queryPID(SERVICE_01, INTAKE_MANIFOLD_ABS_PRESSURE)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * uint8_t - Intake manifold absolute pressure in kPa +// */ +// uint8_t ELM327::manifoldPressure() +// { +// if (queryPID(SERVICE_01, INTAKE_MANIFOLD_ABS_PRESSURE)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } @@ -767,7 +783,7 @@ uint8_t ELM327::manifoldPressure() Description: ------------ - * Queries and parses received message for/returns vehicle RMP data + * Queries and parses received message for/returns vehicle RPM data Inputs: ------- @@ -777,1669 +793,1763 @@ uint8_t ELM327::manifoldPressure() ------- * float - Vehicle RPM */ -float ELM327::rpm() +float ELM327::rpm(void) { - if (queryPID(SERVICE_01, ENGINE_RPM)) - return conditionResponse(findResponse(), 2, 1.0 / 4.0); - - return ELM_GENERAL_ERROR; + if (nb_query_state == SEND_COMMAND) + { + queryPID(SERVICE_01, ENGINE_RPM, 1); + nb_query_state = WAITING_RESP; + } + else if (nb_query_state == WAITING_RESP) + { + get_response(); + if (nb_rx_state == ELM_SUCCESS) + { + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + return conditionResponse(findResponse(), 2, 1.0 / 4.0); + } + else if (nb_rx_state != ELM_GETTING_MSG) + nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command + } + return 0.0; } -/* - int32_t ELM327::kph() +// /* +// int32_t ELM327::kph() - Description: - ------------ - * Queries and parses received message for/returns vehicle speed data (kph) +// Description: +// ------------ +// * Queries and parses received message for/returns vehicle speed data (kph) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * int32_t - Vehicle speed in kph -*/ -int32_t ELM327::kph() -{ - if (queryPID(SERVICE_01, VEHICLE_SPEED)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * int32_t - Vehicle speed in kph +// */ +// int32_t ELM327::kph() +// { +// if (queryPID(SERVICE_01, VEHICLE_SPEED)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::mph() +// /* +// float ELM327::mph() - Description: - ------------ - * Queries and parses received message for/returns vehicle speed data (mph) +// Description: +// ------------ +// * Queries and parses received message for/returns vehicle speed data (mph) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Vehicle speed in mph -*/ -float ELM327::mph() -{ - float mph = kph(); +// Return: +// ------- +// * float - Vehicle speed in mph +// */ +// float ELM327::mph() +// { +// float mph = kph(); - if (status == ELM_SUCCESS) - return mph * KPH_MPH_CONVERT; +// if (status == ELM_SUCCESS) +// return mph * KPH_MPH_CONVERT; - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::timingAdvance() +// /* +// float ELM327::timingAdvance() - Description: - ------------ - * Find timing advance in degrees before Top Dead Center (TDC) +// Description: +// ------------ +// * Find timing advance in degrees before Top Dead Center (TDC) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Timing advance in degrees before Top Dead Center (TDC) -*/ -float ELM327::timingAdvance() -{ - if (queryPID(SERVICE_01, TIMING_ADVANCE)) - return conditionResponse(findResponse(), 1, 1.0 / 2.0, -64.0); +// Return: +// ------- +// * float - Timing advance in degrees before Top Dead Center (TDC) +// */ +// float ELM327::timingAdvance() +// { +// if (queryPID(SERVICE_01, TIMING_ADVANCE)) +// return conditionResponse(findResponse(), 1, 1.0 / 2.0, -64.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::intakeAirTemp() +// /* +// float ELM327::intakeAirTemp() - Description: - ------------ - * Find intake air temperature in C +// Description: +// ------------ +// * Find intake air temperature in C - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Intake air temperature in C -*/ -float ELM327::intakeAirTemp() -{ - if (queryPID(SERVICE_01, INTAKE_AIR_TEMP)) - return conditionResponse(findResponse(), 1, 1, -40.0); +// Return: +// ------- +// * float - Intake air temperature in C +// */ +// float ELM327::intakeAirTemp() +// { +// if (queryPID(SERVICE_01, INTAKE_AIR_TEMP)) +// return conditionResponse(findResponse(), 1, 1, -40.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::mafRate() +// /* +// float ELM327::mafRate() - Description: - ------------ - * Find mass air flow sensor (MAF) air flow rate rate in g/s +// Description: +// ------------ +// * Find mass air flow sensor (MAF) air flow rate rate in g/s - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Mass air flow sensor (MAF) air flow rate rate in g/s -*/ -float ELM327::mafRate() -{ - if (queryPID(SERVICE_01, MAF_FLOW_RATE)) - return conditionResponse(findResponse(), 2, 1.0 / 100.0); +// Return: +// ------- +// * float - Mass air flow sensor (MAF) air flow rate rate in g/s +// */ +// float ELM327::mafRate() +// { +// if (queryPID(SERVICE_01, MAF_FLOW_RATE)) +// return conditionResponse(findResponse(), 2, 1.0 / 100.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::throttle() +// /* +// float ELM327::throttle() - Description: - ------------ - * Find throttle position in % +// Description: +// ------------ +// * Find throttle position in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Throttle position in % -*/ -float ELM327::throttle() -{ - if (queryPID(SERVICE_01, THROTTLE_POSITION)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Throttle position in % +// */ +// float ELM327::throttle() +// { +// if (queryPID(SERVICE_01, THROTTLE_POSITION)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::commandedSecAirStatus() +// /* +// uint8_t ELM327::commandedSecAirStatus() - Description: - ------------ - * Find commanded secondary air status +// Description: +// ------------ +// * Find commanded secondary air status - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_12) -*/ -uint8_t ELM327::commandedSecAirStatus() -{ - if (queryPID(SERVICE_01, COMMANDED_SECONDARY_AIR_STATUS)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * uint8_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_12) +// */ +// uint8_t ELM327::commandedSecAirStatus() +// { +// if (queryPID(SERVICE_01, COMMANDED_SECONDARY_AIR_STATUS)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::oxygenSensorsPresent_2banks() +// /* +// uint8_t ELM327::oxygenSensorsPresent_2banks() - Description: - ------------ - * Find which oxygen sensors are present ([A0..A3] == Bank 1, Sensors 1-4. [A4..A7] == Bank 2...) +// Description: +// ------------ +// * Find which oxygen sensors are present ([A0..A3] == Bank 1, Sensors 1-4. [A4..A7] == Bank 2...) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Bit encoded -*/ -uint8_t ELM327::oxygenSensorsPresent_2banks() -{ - if (queryPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_2_BANKS)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * uint8_t - Bit encoded +// */ +// uint8_t ELM327::oxygenSensorsPresent_2banks() +// { +// if (queryPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_2_BANKS)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::obdStandards() +// /* +// uint8_t ELM327::obdStandards() - Description: - ------------ - * Find the OBD standards this vehicle conforms to (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) +// Description: +// ------------ +// * Find the OBD standards this vehicle conforms to (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) -*/ -uint8_t ELM327::obdStandards() -{ - if (queryPID(SERVICE_01, OBD_STANDARDS)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) +// */ +// uint8_t ELM327::obdStandards() +// { +// if (queryPID(SERVICE_01, OBD_STANDARDS)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::oxygenSensorsPresent_4banks() +// /* +// uint8_t ELM327::oxygenSensorsPresent_4banks() - Description: - ------------ - * Find which oxygen sensors are present (Similar to PID 13, but [A0..A7] == [B1S1, B1S2, B2S1, B2S2, B3S1, B3S2, B4S1, B4S2]) +// Description: +// ------------ +// * Find which oxygen sensors are present (Similar to PID 13, but [A0..A7] == [B1S1, B1S2, B2S1, B2S2, B3S1, B3S2, B4S1, B4S2]) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Bit encoded -*/ -uint8_t ELM327::oxygenSensorsPresent_4banks() -{ - if (queryPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_4_BANKS)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * uint8_t - Bit encoded +// */ +// uint8_t ELM327::oxygenSensorsPresent_4banks() +// { +// if (queryPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_4_BANKS)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - bool ELM327::auxInputStatus() +// /* +// bool ELM327::auxInputStatus() - Description: - ------------ - * Find Power Take Off (PTO) status +// Description: +// ------------ +// * Find Power Take Off (PTO) status - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * bool - Power Take Off (PTO) status -*/ -bool ELM327::auxInputStatus() -{ - if (queryPID(SERVICE_01, AUX_INPUT_STATUS)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * bool - Power Take Off (PTO) status +// */ +// bool ELM327::auxInputStatus() +// { +// if (queryPID(SERVICE_01, AUX_INPUT_STATUS)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint16_t ELM327::runTime() +// /* +// uint16_t ELM327::runTime() - Description: - ------------ - * Find run time since engine start in s +// Description: +// ------------ +// * Find run time since engine start in s - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint16_t - Run time since engine start in s -*/ -uint16_t ELM327::runTime() -{ - if (queryPID(SERVICE_01, RUN_TIME_SINCE_ENGINE_START)) - return conditionResponse(findResponse(), 2); +// Return: +// ------- +// * uint16_t - Run time since engine start in s +// */ +// uint16_t ELM327::runTime() +// { +// if (queryPID(SERVICE_01, RUN_TIME_SINCE_ENGINE_START)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint32_t ELM327::supportedPIDs_21_40() +// /* +// uint32_t ELM327::supportedPIDs_21_40() - Description: - ------------ - * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) +// Description: +// ------------ +// * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint32_t - Bit encoded booleans of supported PIDs 0x21-0x20 -*/ -uint32_t ELM327::supportedPIDs_21_40() -{ - if (queryPID(SERVICE_01, SUPPORTED_PIDS_21_40)) - return conditionResponse(findResponse(), 4); +// Return: +// ------- +// * uint32_t - Bit encoded booleans of supported PIDs 0x21-0x20 +// */ +// uint32_t ELM327::supportedPIDs_21_40() +// { +// if (queryPID(SERVICE_01, SUPPORTED_PIDS_21_40)) +// return conditionResponse(findResponse(), 4); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint16_t ELM327::distTravelWithMIL() +// /* +// uint16_t ELM327::distTravelWithMIL() - Description: - ------------ - * Find distance traveled with malfunction indicator lamp (MIL) on in km +// Description: +// ------------ +// * Find distance traveled with malfunction indicator lamp (MIL) on in km - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint16_t - Distance traveled with malfunction indicator lamp (MIL) on in km -*/ -uint16_t ELM327::distTravelWithMIL() -{ - if (queryPID(SERVICE_01, DISTANCE_TRAVELED_WITH_MIL_ON)) - return conditionResponse(findResponse(), 2); +// Return: +// ------- +// * uint16_t - Distance traveled with malfunction indicator lamp (MIL) on in km +// */ +// uint16_t ELM327::distTravelWithMIL() +// { +// if (queryPID(SERVICE_01, DISTANCE_TRAVELED_WITH_MIL_ON)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::fuelRailPressure() +// /* +// float ELM327::fuelRailPressure() - Description: - ------------ - * Find fuel Rail Pressure (relative to manifold vacuum) in kPa +// Description: +// ------------ +// * Find fuel Rail Pressure (relative to manifold vacuum) in kPa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel Rail Pressure (relative to manifold vacuum) in kPa -*/ -float ELM327::fuelRailPressure() -{ - if (queryPID(SERVICE_01, FUEL_RAIL_PRESSURE)) - return conditionResponse(findResponse(), 2, 0.079); +// Return: +// ------- +// * float - Fuel Rail Pressure (relative to manifold vacuum) in kPa +// */ +// float ELM327::fuelRailPressure() +// { +// if (queryPID(SERVICE_01, FUEL_RAIL_PRESSURE)) +// return conditionResponse(findResponse(), 2, 0.079); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::fuelRailGuagePressure() +// /* +// float ELM327::fuelRailGuagePressure() - Description: - ------------ - * Find fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa +// Description: +// ------------ +// * Find fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa -*/ -float ELM327::fuelRailGuagePressure() -{ - if (queryPID(SERVICE_01, FUEL_RAIL_GUAGE_PRESSURE)) - return conditionResponse(findResponse(), 2, 10.0); +// Return: +// ------- +// * float - Fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa +// */ +// float ELM327::fuelRailGuagePressure() +// { +// if (queryPID(SERVICE_01, FUEL_RAIL_GUAGE_PRESSURE)) +// return conditionResponse(findResponse(), 2, 10.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::commandedEGR() +// /* +// float ELM327::commandedEGR() - Description: - ------------ - * Find commanded Exhaust Gas Recirculation (EGR) in % +// Description: +// ------------ +// * Find commanded Exhaust Gas Recirculation (EGR) in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Commanded Exhaust Gas Recirculation (EGR) in % -*/ -float ELM327::commandedEGR() -{ - if (queryPID(SERVICE_01, COMMANDED_EGR)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Commanded Exhaust Gas Recirculation (EGR) in % +// */ +// float ELM327::commandedEGR() +// { +// if (queryPID(SERVICE_01, COMMANDED_EGR)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::egrError() +// /* +// float ELM327::egrError() - Description: - ------------ - * Find Exhaust Gas Recirculation (EGR) error in % +// Description: +// ------------ +// * Find Exhaust Gas Recirculation (EGR) error in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Exhaust Gas Recirculation (EGR) error in % -*/ -float ELM327::egrError() -{ - if (queryPID(SERVICE_01, EGR_ERROR)) - return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100); +// Return: +// ------- +// * float - Exhaust Gas Recirculation (EGR) error in % +// */ +// float ELM327::egrError() +// { +// if (queryPID(SERVICE_01, EGR_ERROR)) +// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::commandedEvapPurge() +// /* +// float ELM327::commandedEvapPurge() - Description: - ------------ - * Find commanded evaporative purge in % +// Description: +// ------------ +// * Find commanded evaporative purge in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Commanded evaporative purge in % -*/ -float ELM327::commandedEvapPurge() -{ - if (queryPID(SERVICE_01, COMMANDED_EVAPORATIVE_PURGE)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Commanded evaporative purge in % +// */ +// float ELM327::commandedEvapPurge() +// { +// if (queryPID(SERVICE_01, COMMANDED_EVAPORATIVE_PURGE)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::fuelLevel() +// /* +// float ELM327::fuelLevel() - Description: - ------------ - * Find fuel tank level input in % +// Description: +// ------------ +// * Find fuel tank level input in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel tank level input in % -*/ -float ELM327::fuelLevel() -{ - if (queryPID(SERVICE_01, FUEL_TANK_LEVEL_INPUT)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Fuel tank level input in % +// */ +// float ELM327::fuelLevel() +// { +// if (queryPID(SERVICE_01, FUEL_TANK_LEVEL_INPUT)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::warmUpsSinceCodesCleared() +// /* +// uint8_t ELM327::warmUpsSinceCodesCleared() - Description: - ------------ - * Find num warm-ups since codes cleared +// Description: +// ------------ +// * Find num warm-ups since codes cleared - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Num warm-ups since codes cleared -*/ -uint8_t ELM327::warmUpsSinceCodesCleared() -{ - if (queryPID(SERVICE_01, WARM_UPS_SINCE_CODES_CLEARED)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * uint8_t - Num warm-ups since codes cleared +// */ +// uint8_t ELM327::warmUpsSinceCodesCleared() +// { +// if (queryPID(SERVICE_01, WARM_UPS_SINCE_CODES_CLEARED)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint16_t ELM327::distSinceCodesCleared() +// /* +// uint16_t ELM327::distSinceCodesCleared() - Description: - ------------ - * Find distance traveled since codes cleared in km +// Description: +// ------------ +// * Find distance traveled since codes cleared in km - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint16_t - Distance traveled since codes cleared in km -*/ -uint16_t ELM327::distSinceCodesCleared() -{ - if (queryPID(SERVICE_01, DIST_TRAV_SINCE_CODES_CLEARED)) - return conditionResponse(findResponse(), 2); +// Return: +// ------- +// * uint16_t - Distance traveled since codes cleared in km +// */ +// uint16_t ELM327::distSinceCodesCleared() +// { +// if (queryPID(SERVICE_01, DIST_TRAV_SINCE_CODES_CLEARED)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::evapSysVapPressure() +// /* +// float ELM327::evapSysVapPressure() - Description: - ------------ - * Find evap. system vapor pressure in Pa +// Description: +// ------------ +// * Find evap. system vapor pressure in Pa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Evap. system vapor pressure in Pa -*/ -float ELM327::evapSysVapPressure() -{ - if (queryPID(SERVICE_01, EVAP_SYSTEM_VAPOR_PRESSURE)) - return conditionResponse(findResponse(), 2, 1.0 / 4.0); +// Return: +// ------- +// * float - Evap. system vapor pressure in Pa +// */ +// float ELM327::evapSysVapPressure() +// { +// if (queryPID(SERVICE_01, EVAP_SYSTEM_VAPOR_PRESSURE)) +// return conditionResponse(findResponse(), 2, 1.0 / 4.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::absBaroPressure() +// /* +// uint8_t ELM327::absBaroPressure() - Description: - ------------ - * Find absolute barometric pressure in kPa +// Description: +// ------------ +// * Find absolute barometric pressure in kPa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Absolute barometric pressure in kPa -*/ -uint8_t ELM327::absBaroPressure() -{ - if (queryPID(SERVICE_01, ABS_BAROMETRIC_PRESSURE)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * uint8_t - Absolute barometric pressure in kPa +// */ +// uint8_t ELM327::absBaroPressure() +// { +// if (queryPID(SERVICE_01, ABS_BAROMETRIC_PRESSURE)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::catTempB1S1() +// /* +// float ELM327::catTempB1S1() - Description: - ------------ - * Find catalyst temperature in C +// Description: +// ------------ +// * Find catalyst temperature in C - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Catalyst temperature in C -*/ -float ELM327::catTempB1S1() -{ - if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_1)) - return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); +// Return: +// ------- +// * float - Catalyst temperature in C +// */ +// float ELM327::catTempB1S1() +// { +// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_1)) +// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::catTempB2S1() +// /* +// float ELM327::catTempB2S1() - Description: - ------------ - * Find catalyst temperature in C +// Description: +// ------------ +// * Find catalyst temperature in C - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Catalyst temperature in C -*/ -float ELM327::catTempB2S1() -{ - if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_1)) - return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); +// Return: +// ------- +// * float - Catalyst temperature in C +// */ +// float ELM327::catTempB2S1() +// { +// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_1)) +// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::catTempB1S2() +// /* +// float ELM327::catTempB1S2() - Description: - ------------ - * Find catalyst temperature in C +// Description: +// ------------ +// * Find catalyst temperature in C - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Catalyst temperature in C -*/ -float ELM327::catTempB1S2() -{ - if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_2)) - return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); +// Return: +// ------- +// * float - Catalyst temperature in C +// */ +// float ELM327::catTempB1S2() +// { +// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_2)) +// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::catTempB2S2() +// /* +// float ELM327::catTempB2S2() - Description: - ------------ - * Find catalyst temperature in C +// Description: +// ------------ +// * Find catalyst temperature in C - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Catalyst temperature in C -*/ -float ELM327::catTempB2S2() -{ - if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_2)) - return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); +// Return: +// ------- +// * float - Catalyst temperature in C +// */ +// float ELM327::catTempB2S2() +// { +// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_2)) +// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint32_t ELM327::supportedPIDs_41_60() +// /* +// uint32_t ELM327::supportedPIDs_41_60() - Description: - ------------ - * Determine which of PIDs 0x41 through 0x60 are supported (bit encoded) +// Description: +// ------------ +// * Determine which of PIDs 0x41 through 0x60 are supported (bit encoded) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint32_t - Bit encoded booleans of supported PIDs 0x41-0x60 -*/ -uint32_t ELM327::supportedPIDs_41_60() -{ - if (queryPID(SERVICE_01, SUPPORTED_PIDS_41_60)) - return conditionResponse(findResponse(), 4); +// Return: +// ------- +// * uint32_t - Bit encoded booleans of supported PIDs 0x41-0x60 +// */ +// uint32_t ELM327::supportedPIDs_41_60() +// { +// if (queryPID(SERVICE_01, SUPPORTED_PIDS_41_60)) +// return conditionResponse(findResponse(), 4); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint32_t ELM327::monitorDriveCycleStatus() +// /* +// uint32_t ELM327::monitorDriveCycleStatus() - Description: - ------------ - * Find status this drive cycle (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) +// Description: +// ------------ +// * Find status this drive cycle (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) -*/ -uint32_t ELM327::monitorDriveCycleStatus() -{ - if (queryPID(SERVICE_01, MONITOR_STATUS_THIS_DRIVE_CYCLE)) - return conditionResponse(findResponse(), 4); +// Return: +// ------- +// * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) +// */ +// uint32_t ELM327::monitorDriveCycleStatus() +// { +// if (queryPID(SERVICE_01, MONITOR_STATUS_THIS_DRIVE_CYCLE)) +// return conditionResponse(findResponse(), 4); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::ctrlModVoltage() +// /* +// float ELM327::ctrlModVoltage() - Description: - ------------ - * Find control module voltage in V +// Description: +// ------------ +// * Find control module voltage in V - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Control module voltage in V -*/ -float ELM327::ctrlModVoltage() -{ - if (queryPID(SERVICE_01, CONTROL_MODULE_VOLTAGE)) - return conditionResponse(findResponse(), 2, 1.0 / 1000.0); +// Return: +// ------- +// * float - Control module voltage in V +// */ +// float ELM327::ctrlModVoltage() +// { +// if (queryPID(SERVICE_01, CONTROL_MODULE_VOLTAGE)) +// return conditionResponse(findResponse(), 2, 1.0 / 1000.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absLoad() +// /* +// float ELM327::absLoad() - Description: - ------------ - * Find absolute load value in % +// Description: +// ------------ +// * Find absolute load value in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Absolute load value in % -*/ -float ELM327::absLoad() -{ - if (queryPID(SERVICE_01, ABS_LOAD_VALUE)) - return conditionResponse(findResponse(), 2, 100.0 / 255.0); +// Return: +// ------- +// * float - Absolute load value in % +// */ +// float ELM327::absLoad() +// { +// if (queryPID(SERVICE_01, ABS_LOAD_VALUE)) +// return conditionResponse(findResponse(), 2, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::commandedAirFuelRatio() +// /* +// float ELM327::commandedAirFuelRatio() - Description: - ------------ - * Find commanded air-fuel equivalence ratio +// Description: +// ------------ +// * Find commanded air-fuel equivalence ratio - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Commanded air-fuel equivalence ratio -*/ -float ELM327::commandedAirFuelRatio() -{ - if (queryPID(SERVICE_01, FUEL_AIR_COMMANDED_EQUIV_RATIO)) - return conditionResponse(findResponse(), 2, 2.0 / 65536.0); +// Return: +// ------- +// * float - Commanded air-fuel equivalence ratio +// */ +// float ELM327::commandedAirFuelRatio() +// { +// if (queryPID(SERVICE_01, FUEL_AIR_COMMANDED_EQUIV_RATIO)) +// return conditionResponse(findResponse(), 2, 2.0 / 65536.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::relativeThrottle() +// /* +// float ELM327::relativeThrottle() - Description: - ------------ - * Find relative throttle position in % +// Description: +// ------------ +// * Find relative throttle position in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Relative throttle position in % -*/ -float ELM327::relativeThrottle() -{ - if (queryPID(SERVICE_01, RELATIVE_THROTTLE_POSITION)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Relative throttle position in % +// */ +// float ELM327::relativeThrottle() +// { +// if (queryPID(SERVICE_01, RELATIVE_THROTTLE_POSITION)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::ambientAirTemp() +// /* +// float ELM327::ambientAirTemp() - Description: - ------------ - * Find ambient air temperature in C +// Description: +// ------------ +// * Find ambient air temperature in C - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Ambient air temperature in C -*/ -float ELM327::ambientAirTemp() -{ - if (queryPID(SERVICE_01, AMBIENT_AIR_TEMP)) - return conditionResponse(findResponse(), 1, 1, -40); +// Return: +// ------- +// * float - Ambient air temperature in C +// */ +// float ELM327::ambientAirTemp() +// { +// if (queryPID(SERVICE_01, AMBIENT_AIR_TEMP)) +// return conditionResponse(findResponse(), 1, 1, -40); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absThrottlePosB() +// /* +// float ELM327::absThrottlePosB() - Description: - ------------ - * Find absolute throttle position B in % +// Description: +// ------------ +// * Find absolute throttle position B in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Absolute throttle position B in % -*/ -float ELM327::absThrottlePosB() -{ - if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_B)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Absolute throttle position B in % +// */ +// float ELM327::absThrottlePosB() +// { +// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_B)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absThrottlePosC() +// /* +// float ELM327::absThrottlePosC() - Description: - ------------ - * Find absolute throttle position C in % +// Description: +// ------------ +// * Find absolute throttle position C in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Absolute throttle position C in % -*/ -float ELM327::absThrottlePosC() -{ - if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_C)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Absolute throttle position C in % +// */ +// float ELM327::absThrottlePosC() +// { +// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_C)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absThrottlePosD() +// /* +// float ELM327::absThrottlePosD() - Description: - ------------ - * Find absolute throttle position D in % +// Description: +// ------------ +// * Find absolute throttle position D in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Absolute throttle position D in % -*/ -float ELM327::absThrottlePosD() -{ - if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_D)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Absolute throttle position D in % +// */ +// float ELM327::absThrottlePosD() +// { +// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_D)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absThrottlePosE() +// /* +// float ELM327::absThrottlePosE() - Description: - ------------ - * Find absolute throttle position E in % +// Description: +// ------------ +// * Find absolute throttle position E in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Absolute throttle position E in % -*/ -float ELM327::absThrottlePosE() -{ - if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_E)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Absolute throttle position E in % +// */ +// float ELM327::absThrottlePosE() +// { +// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_E)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absThrottlePosF() +// /* +// float ELM327::absThrottlePosF() - Description: - ------------ - * Find absolute throttle position F in % +// Description: +// ------------ +// * Find absolute throttle position F in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Absolute throttle position F in % -*/ -float ELM327::absThrottlePosF() -{ - if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_F)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Absolute throttle position F in % +// */ +// float ELM327::absThrottlePosF() +// { +// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_F)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::commandedThrottleActuator() +// /* +// float ELM327::commandedThrottleActuator() - Description: - ------------ - * Find commanded throttle actuator in % +// Description: +// ------------ +// * Find commanded throttle actuator in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Commanded throttle actuator in % -*/ -float ELM327::commandedThrottleActuator() -{ - if (queryPID(SERVICE_01, COMMANDED_THROTTLE_ACTUATOR)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Commanded throttle actuator in % +// */ +// float ELM327::commandedThrottleActuator() +// { +// if (queryPID(SERVICE_01, COMMANDED_THROTTLE_ACTUATOR)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint16_t ELM327::timeRunWithMIL() +// /* +// uint16_t ELM327::timeRunWithMIL() - Description: - ------------ - * Find time run with MIL on in min +// Description: +// ------------ +// * Find time run with MIL on in min - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint16_t - Time run with MIL on in min -*/ -uint16_t ELM327::timeRunWithMIL() -{ - if (queryPID(SERVICE_01, TIME_RUN_WITH_MIL_ON)) - return conditionResponse(findResponse(), 2); +// Return: +// ------- +// * uint16_t - Time run with MIL on in min +// */ +// uint16_t ELM327::timeRunWithMIL() +// { +// if (queryPID(SERVICE_01, TIME_RUN_WITH_MIL_ON)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint16_t ELM327::timeSinceCodesCleared() +// /* +// uint16_t ELM327::timeSinceCodesCleared() - Description: - ------------ - * Find time since trouble codes cleared in min +// Description: +// ------------ +// * Find time since trouble codes cleared in min - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint16_t - Time since trouble codes cleared in min -*/ -uint16_t ELM327::timeSinceCodesCleared() -{ - if (queryPID(SERVICE_01, TIME_SINCE_CODES_CLEARED)) - return conditionResponse(findResponse(), 2); +// Return: +// ------- +// * uint16_t - Time since trouble codes cleared in min +// */ +// uint16_t ELM327::timeSinceCodesCleared() +// { +// if (queryPID(SERVICE_01, TIME_SINCE_CODES_CLEARED)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::maxMafRate() +// /* +// float ELM327::maxMafRate() - Description: - ------------ - * Find maximum value for air flow rate from mass air flow sensor in g/s +// Description: +// ------------ +// * Find maximum value for air flow rate from mass air flow sensor in g/s + +// Inputs: +// ------- +// * void + +// Return: +// ------- +// * float - Maximum value for air flow rate from mass air flow sensor in g/s +// */ +// float ELM327::maxMafRate() +// { +// if (queryPID(SERVICE_01, MAX_MAF_RATE)) +// return conditionResponse(findResponse(), 1, 10.0); + +// return ELM_GENERAL_ERROR; +// } + + + + +// /* +// uint8_t ELM327::fuelType() + +// Description: +// ------------ +// * Find fuel type (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) + +// Inputs: +// ------- +// * void + +// Return: +// ------- +// * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) +// */ +// uint8_t ELM327::fuelType() +// { +// if (queryPID(SERVICE_01, FUEL_TYPE)) +// return conditionResponse(findResponse(), 1); + +// return ELM_GENERAL_ERROR; +// } + + + + +// /* +// float ELM327::ethonolPercent() + +// Description: +// ------------ +// * Find ethanol fuel in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Maximum value for air flow rate from mass air flow sensor in g/s -*/ -float ELM327::maxMafRate() -{ - if (queryPID(SERVICE_01, MAX_MAF_RATE)) - return conditionResponse(findResponse(), 1, 10.0); +// Return: +// ------- +// * float - Ethanol fuel in % +// */ +// float ELM327::ethonolPercent() +// { +// if (queryPID(SERVICE_01, ETHONOL_FUEL_PERCENT)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::fuelType() +// /* +// float ELM327::absEvapSysVapPressure() - Description: - ------------ - * Find fuel type (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) +// Description: +// ------------ +// * Find absolute evap. system vapor pressure in kPa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) -*/ -uint8_t ELM327::fuelType() -{ - if (queryPID(SERVICE_01, FUEL_TYPE)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * float - Absolute evap. system vapor pressure in kPa +// */ +// float ELM327::absEvapSysVapPressure() +// { +// if (queryPID(SERVICE_01, ABS_EVAP_SYS_VAPOR_PRESSURE)) +// return conditionResponse(findResponse(), 2, 1.0 / 200.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::ethonolPercent() +// /* +// float ELM327::evapSysVapPressure2() - Description: - ------------ - * Find ethanol fuel in % +// Description: +// ------------ +// * Find evap. system vapor pressure in Pa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Ethanol fuel in % -*/ -float ELM327::ethonolPercent() -{ - if (queryPID(SERVICE_01, ETHONOL_FUEL_PERCENT)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Evap. system vapor pressure in Pa +// */ +// float ELM327::evapSysVapPressure2() +// { +// if (queryPID(SERVICE_01, EVAP_SYS_VAPOR_PRESSURE)) +// return conditionResponse(findResponse(), 2, 1, -32767); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absEvapSysVapPressure() +// /* +// float ELM327::absFuelRailPressure() - Description: - ------------ - * Find absolute evap. system vapor pressure in kPa +// Description: +// ------------ +// * Find absolute fuel rail pressure in kPa - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Absolute evap. system vapor pressure in kPa -*/ -float ELM327::absEvapSysVapPressure() -{ - if (queryPID(SERVICE_01, ABS_EVAP_SYS_VAPOR_PRESSURE)) - return conditionResponse(findResponse(), 2, 1.0 / 200.0); +// Return: +// ------- +// * float - absolute fuel rail pressure in kPa +// */ +// float ELM327::absFuelRailPressure() +// { +// if (queryPID(SERVICE_01, FUEL_RAIL_ABS_PRESSURE)) +// return conditionResponse(findResponse(), 2, 10.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::evapSysVapPressure2() +// /* +// float ELM327::relativePedalPos() - Description: - ------------ - * Find evap. system vapor pressure in Pa +// Description: +// ------------ +// * Find relative accelerator pedal position in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Evap. system vapor pressure in Pa -*/ -float ELM327::evapSysVapPressure2() -{ - if (queryPID(SERVICE_01, EVAP_SYS_VAPOR_PRESSURE)) - return conditionResponse(findResponse(), 2, 1, -32767); +// Return: +// ------- +// * float - Relative accelerator pedal position in % +// */ +// float ELM327::relativePedalPos() +// { +// if (queryPID(SERVICE_01, RELATIVE_ACCELERATOR_PEDAL_POS)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::absFuelRailPressure() +// /* +// float ELM327::hybridBatLife() - Description: - ------------ - * Find absolute fuel rail pressure in kPa +// Description: +// ------------ +// * Find hybrid battery pack remaining life in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - absolute fuel rail pressure in kPa -*/ -float ELM327::absFuelRailPressure() -{ - if (queryPID(SERVICE_01, FUEL_RAIL_ABS_PRESSURE)) - return conditionResponse(findResponse(), 2, 10.0); +// Return: +// ------- +// * float - Hybrid battery pack remaining life in % +// */ +// float ELM327::hybridBatLife() +// { +// if (queryPID(SERVICE_01, HYBRID_BATTERY_REMAINING_LIFE)) +// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::relativePedalPos() +// /* +// float ELM327::oilTemp() - Description: - ------------ - * Find relative accelerator pedal position in % +// Description: +// ------------ +// * Find engine oil temperature in C - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Relative accelerator pedal position in % -*/ -float ELM327::relativePedalPos() -{ - if (queryPID(SERVICE_01, RELATIVE_ACCELERATOR_PEDAL_POS)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Engine oil temperature in C +// */ +// float ELM327::oilTemp() +// { +// if (queryPID(SERVICE_01, ENGINE_OIL_TEMP)) +// return conditionResponse(findResponse(), 1, 1, -40.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::hybridBatLife() +// /* +// float ELM327::fuelInjectTiming() - Description: - ------------ - * Find hybrid battery pack remaining life in % +// Description: +// ------------ +// * Find fuel injection timing in degrees - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Hybrid battery pack remaining life in % -*/ -float ELM327::hybridBatLife() -{ - if (queryPID(SERVICE_01, HYBRID_BATTERY_REMAINING_LIFE)) - return conditionResponse(findResponse(), 1, 100.0 / 255.0); +// Return: +// ------- +// * float - Fuel injection timing in degrees +// */ +// float ELM327::fuelInjectTiming() +// { +// if (queryPID(SERVICE_01, FUEL_INJECTION_TIMING)) +// return conditionResponse(findResponse(), 2, 1.0 / 128.0, -210.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::oilTemp() +// /* +// float ELM327::fuelRate() - Description: - ------------ - * Find engine oil temperature in C +// Description: +// ------------ +// * Find engine fuel rate in L/h - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Engine oil temperature in C -*/ -float ELM327::oilTemp() -{ - if (queryPID(SERVICE_01, ENGINE_OIL_TEMP)) - return conditionResponse(findResponse(), 1, 1, -40.0); +// Return: +// ------- +// * float - Engine fuel rate in L/h +// */ +// float ELM327::fuelRate() +// { +// if (queryPID(SERVICE_01, ENGINE_FUEL_RATE)) +// return conditionResponse(findResponse(), 2, 1.0 / 20.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::fuelInjectTiming() +// /* +// uint8_t ELM327::emissionRqmts() - Description: - ------------ - * Find fuel injection timing in degrees +// Description: +// ------------ +// * Find emission requirements to which vehicle is designed - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Fuel injection timing in degrees -*/ -float ELM327::fuelInjectTiming() -{ - if (queryPID(SERVICE_01, FUEL_INJECTION_TIMING)) - return conditionResponse(findResponse(), 2, 1.0 / 128.0, -210.0); +// Return: +// ------- +// * uint8_t - Bit encoded (?) +// */ +// uint8_t ELM327::emissionRqmts() +// { +// if (queryPID(SERVICE_01, EMISSION_REQUIREMENTS)) +// return conditionResponse(findResponse(), 1); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::fuelRate() +// /* +// uint32_t ELM327::supportedPIDs_61_80() - Description: - ------------ - * Find engine fuel rate in L/h +// Description: +// ------------ +// * Determine which of PIDs 0x61 through 0x80 are supported (bit encoded) - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Engine fuel rate in L/h -*/ -float ELM327::fuelRate() -{ - if (queryPID(SERVICE_01, ENGINE_FUEL_RATE)) - return conditionResponse(findResponse(), 2, 1.0 / 20.0); +// Return: +// ------- +// * uint32_t - Bit encoded booleans of supported PIDs 0x61-0x80 +// */ +// uint32_t ELM327::supportedPIDs_61_80() +// { +// if (queryPID(SERVICE_01, SUPPORTED_PIDS_61_80)) +// return conditionResponse(findResponse(), 4); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint8_t ELM327::emissionRqmts() +// /* +// float ELM327::demandedTorque() - Description: - ------------ - * Find emission requirements to which vehicle is designed +// Description: +// ------------ +// * Find driver's demanded engine torque in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint8_t - Bit encoded (?) -*/ -uint8_t ELM327::emissionRqmts() -{ - if (queryPID(SERVICE_01, EMISSION_REQUIREMENTS)) - return conditionResponse(findResponse(), 1); +// Return: +// ------- +// * float - Driver's demanded engine torque in % +// */ +// float ELM327::demandedTorque() +// { +// if (queryPID(SERVICE_01, DEMANDED_ENGINE_PERCENT_TORQUE)) +// return conditionResponse(findResponse(), 1, 1, -125.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - uint32_t ELM327::supportedPIDs_61_80() +// /* +// float ELM327::torque() - Description: - ------------ - * Determine which of PIDs 0x61 through 0x80 are supported (bit encoded) +// Description: +// ------------ +// * Find actual engine torque in % - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * uint32_t - Bit encoded booleans of supported PIDs 0x61-0x80 -*/ -uint32_t ELM327::supportedPIDs_61_80() -{ - if (queryPID(SERVICE_01, SUPPORTED_PIDS_61_80)) - return conditionResponse(findResponse(), 4); +// Return: +// ------- +// * float - Actual engine torque in % +// */ +// float ELM327::torque() +// { +// if (queryPID(SERVICE_01, ACTUAL_ENGINE_TORQUE)) +// return conditionResponse(findResponse(), 1, 1, -125.0); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::demandedTorque() +// /* +// uint16_t ELM327::referenceTorque() - Description: - ------------ - * Find driver's demanded engine torque in % +// Description: +// ------------ +// * Find engine reference torque in Nm - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Driver's demanded engine torque in % -*/ -float ELM327::demandedTorque() -{ - if (queryPID(SERVICE_01, DEMANDED_ENGINE_PERCENT_TORQUE)) - return conditionResponse(findResponse(), 1, 1, -125.0); +// Return: +// ------- +// * uint16_t - Engine reference torque in Nm +// */ +// uint16_t ELM327::referenceTorque() +// { +// if (queryPID(SERVICE_01, ENGINE_REFERENCE_TORQUE)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } -/* - float ELM327::torque() +// /* +// uint16_t ELM327::auxSupported() - Description: - ------------ - * Find actual engine torque in % +// Description: +// ------------ +// * Find auxiliary input/output supported - Inputs: - ------- - * void +// Inputs: +// ------- +// * void - Return: - ------- - * float - Actual engine torque in % -*/ -float ELM327::torque() -{ - if (queryPID(SERVICE_01, ACTUAL_ENGINE_TORQUE)) - return conditionResponse(findResponse(), 1, 1, -125.0); +// Return: +// ------- +// * uint16_t - Bit encoded (?) +// */ +// uint16_t ELM327::auxSupported() +// { +// if (queryPID(SERVICE_01, AUX_INPUT_OUTPUT_SUPPORTED)) +// return conditionResponse(findResponse(), 2); - return ELM_GENERAL_ERROR; -} +// return ELM_GENERAL_ERROR; +// } /* - uint16_t ELM327::referenceTorque() + void ELM327::sendCommand(const char *cmd) Description: ------------ - * Find engine reference torque in Nm + * Sends a command/query for Non-Blocking PID queries Inputs: ------- - * void + * const char *cmd - Command/query to send to ELM327 Return: ------- - * uint16_t - Engine reference torque in Nm + * void */ -uint16_t ELM327::referenceTorque() +void ELM327::sendCommand(const char *cmd) { - if (queryPID(SERVICE_01, ENGINE_REFERENCE_TORQUE)) - return conditionResponse(findResponse(), 2); + // clear payload buffer + memset(payload, '\0', PAYLOAD_LEN + 1); + + // reset input serial buffer and number of received bytes + recBytes = 0; + flushInputBuff(); + connected = false; + + // Reset the receive state ready to start receiving a response message + nb_rx_state = ELM_GETTING_MSG; + + if (debugMode) + { + Serial.print(F("Sending the following command/query: ")); + Serial.println(cmd); + } + + elm_port->print(cmd); + elm_port->print('\r'); - return ELM_GENERAL_ERROR; + // prime the timeout timer + previousTime = millis(); + currentTime = previousTime; } /* - uint16_t ELM327::auxSupported() + obd_rx_states ELM327::sendCommand_Blocking(const char* cmd) Description: ------------ - * Find auxiliary input/output supported + * Sends a command/query for Non-Blocking PID queries. + Sometimes it's ok to use a blocking command, e.g when sending an AT command. + This function removes the need for the caller to set up a loop waiting for the command to finish. + Caller is free to parse the payload string if they need to use the response. Inputs: ------- - * void + * const char *cmd - Command/query to send to ELM327 Return: ------- - * uint16_t - Bit encoded (?) + * obd_rx_states - the status of the command */ -uint16_t ELM327::auxSupported() +int8_t ELM327::sendCommand_Blocking(const char *cmd) { - if (queryPID(SERVICE_01, AUX_INPUT_OUTPUT_SUPPORTED)) - return conditionResponse(findResponse(), 2); - - return ELM_GENERAL_ERROR; + int8_t receive_status; + sendCommand(cmd); + do + { + receive_status = get_response(); + } while (receive_status == ELM_GETTING_MSG); + return receive_status; } /* - int8_t ELM327::sendCommand(const char *cmd) + obd_rx_states ELM327::get_response(void) Description: ------------ - * Sends a command/query and reads/buffers the ELM327's response + * Non Blocking (NB) receive OBD scanner response. Must be called in repeatedly until + the status progresses past ELM_GETTING_MSG. Inputs: ------- - * const char *cmd - Command/query to send to ELM327 + * void Return: ------- - * int8_t - Response status + * obd_rx_states - the status of getting the response */ -int8_t ELM327::sendCommand(const char *cmd) +int8_t ELM327::get_response(void) { - uint16_t counter = 0; - connected = false; - - // clear payload buffer - memset(payload, '\0', PAYLOAD_LEN + 1); - - // reset input serial buffer and number of received bytes - recBytes = 0; - flushInputBuff(); - - if (debugMode) - { - Serial.print(F("Sending the following command/query: ")); - Serial.println(cmd); - } - - elm_port->print(cmd); - elm_port->print('\r'); - - // prime the timeout timer - previousTime = millis(); - currentTime = previousTime; - // buffer the response of the ELM327 until either the // end marker is read or a timeout has occurred - // last valid idx is PAYLOAD_LEN but want to keep on free for terminating '\0' - // so limit counter to < PAYLOAD_LEN - while ((counter < PAYLOAD_LEN) && !timeout()) + // last valid idx is PAYLOAD_LEN but want to keep one free for terminating '\0' + // so limit counter to < PAYLOAD_LEN + if (!elm_port->available()) + { + nb_rx_state = ELM_GETTING_MSG; + if (timeout()) + nb_rx_state = ELM_TIMEOUT; + } + else { - if (elm_port->available()) + char recChar = elm_port->read(); + + if (debugMode) { - char recChar = elm_port->read(); + Serial.print(F("\tReceived char: ")); + // display each received character, make non-printables printable + if (recChar == '\f') + Serial.println(F("\\f")); + else if (recChar == '\n') + Serial.println(F("\\n")); + else if (recChar == '\r') + Serial.println(F("\\r")); + else if (recChar == '\t') + Serial.println(F("\\t")); + else if (recChar == '\v') + Serial.println(F("\\v")); + // display the hex code for non-alpha numeric characters + else if (recChar < 32 || recChar > 123) + Serial.printf("0x%02hhX\n", recChar); + // convert spaces to underscore, easier to see in debug output + else if (recChar == ' ') + Serial.println("_"); + // display regular printable + else + Serial.println(recChar); + } + // this is the end of the OBD response + if (recChar == '>') + { if (debugMode) - { - Serial.print(F("\tReceived char: ")); - - if (recChar == '\f') - Serial.println(F("\\f")); - else if (recChar == '\n') - Serial.println(F("\\n")); - else if (recChar == '\r') - Serial.println(F("\\r")); - else if (recChar == '\t') - Serial.println(F("\\t")); - else if (recChar == '\v') - Serial.println(F("\\v")); - else - Serial.println(recChar); - } + Serial.println(F("Delimiter found.")); - if (recChar == '>') + nb_rx_state = ELM_MSG_RXD; + } + else if (!isalnum(recChar) && (recChar != ':') && (recChar != '.')) + // discard all characters except for alphanumeric and decimal places. + // decimal places needed to extract floating point numbers, e.g. battery voltage + nb_rx_state = ELM_GETTING_MSG; // Discard this character + else + { + if (recBytes < PAYLOAD_LEN) { - if (debugMode) - Serial.println(F("Delimiter found")); - - break; + payload[recBytes] = recChar; + recBytes++; + nb_rx_state = ELM_GETTING_MSG; } - else if (!isalnum(recChar) && (recChar != ':')) - continue; - - payload[counter] = recChar; - counter++; + else + nb_rx_state = ELM_BUFFER_OVERFLOW; } } - if (debugMode) + // Message is still being received (or is timing out), so exit early without doing all the other checks + if (nb_rx_state == ELM_GETTING_MSG) + return nb_rx_state; + + // End of response delimiter was found + if (debugMode && nb_rx_state == ELM_MSG_RXD) { Serial.print(F("All chars received: ")); Serial.println(payload); } - if (timeout()) + if (nb_rx_state == ELM_TIMEOUT) { if (debugMode) { @@ -2447,18 +2557,28 @@ int8_t ELM327::sendCommand(const char *cmd) Serial.print((currentTime - previousTime) - timeout_ms); Serial.println(F("ms")); } + return nb_rx_state; + } - status = ELM_TIMEOUT; - return status; + if (nb_rx_state == ELM_BUFFER_OVERFLOW) + { + if (debugMode) + { + Serial.print(F("OBD receive buffer overflow (> ")); + Serial.print(PAYLOAD_LEN); + Serial.println(F(" bytes)")); + } + return nb_rx_state; } - + + // Now we have successfully received OBD response, check if the payload indicates any OBD errors if (nextIndex(payload, "UNABLETOCONNECT") >= 0) { if (debugMode) Serial.println(F("ELM responded with errror \"UNABLE TO CONNECT\"")); - status = ELM_UNABLE_TO_CONNECT; - return status; + nb_rx_state = ELM_UNABLE_TO_CONNECT; + return nb_rx_state; } connected = true; @@ -2468,8 +2588,8 @@ int8_t ELM327::sendCommand(const char *cmd) if (debugMode) Serial.println(F("ELM responded with errror \"NO DATA\"")); - status = ELM_NO_DATA; - return status; + nb_rx_state = ELM_NO_DATA; + return nb_rx_state; } if (nextIndex(payload, "STOPPED") >= 0) @@ -2477,8 +2597,8 @@ int8_t ELM327::sendCommand(const char *cmd) if (debugMode) Serial.println(F("ELM responded with errror \"STOPPED\"")); - status = ELM_STOPPED; - return status; + nb_rx_state = ELM_STOPPED; + return nb_rx_state; } if (nextIndex(payload, "ERROR") >= 0) @@ -2486,17 +2606,12 @@ int8_t ELM327::sendCommand(const char *cmd) if (debugMode) Serial.println(F("ELM responded with \"ERROR\"")); - status = ELM_GENERAL_ERROR; - return status; + nb_rx_state = ELM_GENERAL_ERROR; + return nb_rx_state; } - // keep track of how many bytes were received in - // the ELM327's response (not counting the - // end-marker '>') if a valid response is found - recBytes = counter; - - status = ELM_SUCCESS; - return status; + nb_rx_state = ELM_SUCCESS; + return nb_rx_state; } @@ -2520,7 +2635,7 @@ int8_t ELM327::sendCommand(const char *cmd) uint64_t ELM327::findResponse() { uint8_t firstDatum = 0; - char header[7] = { '\0' }; + char header[7] = {'\0'}; if (longQuery) { @@ -2545,7 +2660,7 @@ uint64_t ELM327::findResponse() Serial.println(header); } - int8_t firstHeadIndex = nextIndex(payload, header); + int8_t firstHeadIndex = nextIndex(payload, header); int8_t secondHeadIndex = nextIndex(payload, header, 2); if (firstHeadIndex >= 0) @@ -2575,14 +2690,14 @@ uint64_t ELM327::findResponse() } response = 0; - for(uint8_t i = 0; i < numPayChars; i++) + for (uint8_t i = 0; i < numPayChars; i++) { uint8_t payloadIndex = firstDatum + i; uint8_t bitsOffset = 4 * (numPayChars - i - 1); response = response | (ctoi(payload[payloadIndex]) << bitsOffset); } - // It is usefull to have the response bytes + // It is useful to have the response bytes // broken-out because some PID algorithms (standard // and custom) require special operations for each // byte returned @@ -2615,7 +2730,7 @@ uint64_t ELM327::findResponse() Serial.print(F("\tresponseByte_7: ")); Serial.println(responseByte_7); } - + return response; } @@ -2648,24 +2763,65 @@ void ELM327::printError() Serial.print(F("Received: ")); Serial.println(payload); - if (status == ELM_SUCCESS) + if (nb_rx_state == ELM_SUCCESS) Serial.println(F("ELM_SUCCESS")); - else if (status == ELM_NO_RESPONSE) + else if (nb_rx_state == ELM_NO_RESPONSE) Serial.println(F("ERROR: ELM_NO_RESPONSE")); - else if (status == ELM_BUFFER_OVERFLOW) + else if (nb_rx_state == ELM_BUFFER_OVERFLOW) Serial.println(F("ERROR: ELM_BUFFER_OVERFLOW")); - else if (status == ELM_UNABLE_TO_CONNECT) + else if (nb_rx_state == ELM_UNABLE_TO_CONNECT) Serial.println(F("ERROR: ELM_UNABLE_TO_CONNECT")); - else if (status == ELM_NO_DATA) + else if (nb_rx_state == ELM_NO_DATA) Serial.println(F("ERROR: ELM_NO_DATA")); - else if (status == ELM_STOPPED) + else if (nb_rx_state == ELM_STOPPED) Serial.println(F("ERROR: ELM_STOPPED")); - else if (status == ELM_TIMEOUT) + else if (nb_rx_state == ELM_TIMEOUT) Serial.println(F("ERROR: ELM_TIMEOUT")); - else if (status == ELM_TIMEOUT) + else if (nb_rx_state == ELM_BUFFER_OVERFLOW) + Serial.println(F("ERROR: BUFFER OVERFLOW")); + else if (nb_rx_state == ELM_GENERAL_ERROR) Serial.println(F("ERROR: ELM_GENERAL_ERROR")); else Serial.println(F("No error detected")); delay(100); } + + + + +/* + float ELM327::batteryVoltage(void) + + Description: + ------------ + * Get the current vehicle battery voltage in Volts DC + + Inputs: + ------- + * void + + Return: + ------- + * flaot - vehicle battery voltage in VDC +*/ +float ELM327::batteryVoltage(void) +{ + if (nb_query_state == SEND_COMMAND) + { + sendCommand(READ_VOLTAGE); + nb_query_state = WAITING_RESP; + } + else if (nb_query_state == WAITING_RESP) + { + get_response(); + if (nb_rx_state == ELM_SUCCESS) + { + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + return strtof(payload, NULL); + } + else if (nb_rx_state != ELM_GETTING_MSG) + nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command + } + return 0.0; +} \ No newline at end of file diff --git a/src/ELMduino.h b/src/ELMduino.h index 4ba3aa5..5266892 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -259,8 +259,7 @@ const char * const RESET_ALL = "AT Z"; // General // Class constants //-------------------------------------------------------------------------------------// const float KPH_MPH_CONVERT = 0.6213711922; -const float RPM_CONVERT = 0.25; -const int8_t QUERY_LEN = 7; +const int8_t QUERY_LEN = 8; const int8_t ELM_SUCCESS = 0; const int8_t ELM_NO_RESPONSE = 1; const int8_t ELM_BUFFER_OVERFLOW = 2; @@ -269,9 +268,17 @@ const int8_t ELM_UNABLE_TO_CONNECT = 4; const int8_t ELM_NO_DATA = 5; const int8_t ELM_STOPPED = 6; const int8_t ELM_TIMEOUT = 7; +const int8_t ELM_GETTING_MSG = 8; +const int8_t ELM_MSG_RXD = 9; const int8_t ELM_GENERAL_ERROR = -1; +// Non-blocking (NB) command states +typedef enum { SEND_COMMAND, + WAITING_RESP, + RESPONSE_RECEIVED, + DECODED_OK, + ERROR } obd_cmd_states; class ELM327 @@ -283,7 +290,7 @@ class ELM327 bool debugMode; char* payload; uint16_t PAYLOAD_LEN; - int8_t status = ELM_GENERAL_ERROR; + int8_t nb_rx_state = ELM_GETTING_MSG; uint64_t response; uint16_t recBytes; uint8_t numPayChars; @@ -297,19 +304,22 @@ class ELM327 byte responseByte_6; byte responseByte_7; - + bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40, const byte& dataTimeout = 0); bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); void flushInputBuff(); uint64_t findResponse(); - bool queryPID(uint8_t service, uint16_t pid); + bool queryPID(uint8_t service, uint16_t pid, uint8_t num_responses); bool queryPID(char queryStr[]); - int8_t sendCommand(const char *cmd); + void sendCommand(const char *cmd); + int8_t sendCommand_Blocking(const char *cmd); + int8_t get_response(); bool timeout(); float conditionResponse(const uint64_t& response, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + float batteryVoltage(void); uint32_t supportedPIDs_1_20(); @@ -395,7 +405,7 @@ class ELM327 uint16_t referenceTorque(); uint16_t auxSupported(); void printError(); - + @@ -405,11 +415,10 @@ class ELM327 uint32_t currentTime; uint32_t previousTime; - - + obd_cmd_states nb_query_state = SEND_COMMAND; // Non-blocking query state void upper(char string[], uint8_t buflen); - void formatQueryArray(uint8_t service, uint16_t pid); + void formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses); uint8_t ctoi(uint8_t value); int8_t nextIndex(char const *str, char const *target, From d4a55faf655017887ce600fe6b3a1edcee4fbf31 Mon Sep 17 00:00:00 2001 From: PB2 Date: Sat, 1 Jan 2022 15:32:10 -0500 Subject: [PATCH 13/88] Expand non-blocking functionality to all built-in functions --- src/ELMduino.cpp | 2584 +++++++++++++++++++++------------------------- src/ELMduino.h | 3 +- 2 files changed, 1203 insertions(+), 1384 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index b5afe46..0c8d0e9 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -25,10 +25,10 @@ */ bool ELM327::begin(Stream &stream, const bool &debug, const uint16_t &timeout, const char &protocol, const uint16_t &payloadLen, const byte &dataTimeout) { - elm_port = &stream; + elm_port = &stream; PAYLOAD_LEN = payloadLen; - debugMode = debug; - timeout_ms = timeout; + debugMode = debug; + timeout_ms = timeout; payload = (char *)malloc(PAYLOAD_LEN + 1); // allow for terminating '\0' @@ -355,6 +355,24 @@ int8_t ELM327::nextIndex(char const *str, +/* + void ELM327::conditionResponse(const uint64_t &response, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) + + Description: + ------------ + * Converts the ELM327's response into it's correct, numerical value + + Inputs: + ------- + * uint64_t response - ELM327's response + * uint8_t numExpectedBytes - Number of valid bytes from the response to process + * float scaleFactor - Amount to scale the response by + * float bias - Amount to bias the response by + + Return: + ------- + * float - Converted numerical value +*/ float ELM327::conditionResponse(const uint64_t &response, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { return ((response >> (((numPayChars / 2) - numExpectedBytes) * 8)) * scaleFactor) + bias; @@ -391,7 +409,7 @@ void ELM327::flushInputBuff() /* - bool ELM327::queryPID(uint8_t service, uint16_t pid, uint8_t num_responses) + bool ELM327::queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses) Description: ------------ @@ -399,9 +417,9 @@ void ELM327::flushInputBuff() Inputs: ------- - * uint8_t service - the diagnostic service ID. 01 is "Show current data" - * uint16_t pid - the Parameter ID (PID) from the service - * uint8_t num_responses - number of lines of data to receive - see ELM datasheet "Talking to the vehicle". + * uint8_t service - The diagnostic service ID. 01 is "Show current data" + * uint16_t pid - The Parameter ID (PID) from the service + * uint8_t num_responses - Number of lines of data to receive - see ELM datasheet "Talking to the vehicle". This can speed up retrieval of information if you know how many responses will be sent. Basically the OBD scanner will not wait for more responses if it does not need to go through final timeout. Also prevents OBD scanners from sending mulitple of the same response. @@ -410,7 +428,7 @@ void ELM327::flushInputBuff() ------- * bool - Whether or not the query was submitted successfully */ -bool ELM327::queryPID(uint8_t service, uint16_t pid, uint8_t num_responses) +bool ELM327::queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses) { formatQueryArray(service, pid, num_responses); sendCommand(query); @@ -451,158 +469,34 @@ bool ELM327::queryPID(char queryStr[]) -// /* -// uint32_t ELM327::supportedPIDs_1_20() - -// Description: -// ------------ -// * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * uint32_t - Bit encoded booleans of supported PIDs 0x1-0x20 -// */ -// uint32_t ELM327::supportedPIDs_1_20() -// { -// if (queryPID(SERVICE_01, SUPPORTED_PIDS_1_20)) -// return conditionResponse(findResponse(), 4); - -// return ELM_GENERAL_ERROR; -// } - - - - -// /* -// uint32_t ELM327::monitorStatus() - -// Description: -// ------------ -// * Monitor status since DTCs cleared (Includes malfunction indicator -// lamp (MIL) status and number of DTCs). See https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01 -// for more info - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01) -// */ -// uint32_t ELM327::monitorStatus() -// { -// if (queryPID(SERVICE_01, MONITOR_STATUS_SINCE_DTC_CLEARED)) -// return conditionResponse(findResponse(), 4); - -// return ELM_GENERAL_ERROR; -// } - - - - -// /* -// uint16_t ELM327::freezeDTC() - -// Description: -// ------------ -// * Freeze DTC - see https://www.samarins.com/diagnose/freeze-frame.html for more info - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * uint16_t - Various vehicle information (https://www.samarins.com/diagnose/freeze-frame.html) -// */ -// uint16_t ELM327::freezeDTC() -// { -// if (queryPID(SERVICE_01, FREEZE_DTC)) -// return conditionResponse(findResponse(), 2); - -// return ELM_GENERAL_ERROR; -// } - - - - -// /* -// uint16_t ELM327::fuelSystemStatus() - -// Description: -// ------------ -// * Freeze DTC - see https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03 for more info - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * uint16_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03) -// */ -// uint16_t ELM327::fuelSystemStatus() -// { -// if (queryPID(SERVICE_01, FUEL_SYSTEM_STATUS)) -// return conditionResponse(findResponse(), 2); - -// return ELM_GENERAL_ERROR; -// } - - - - -// /* -// float ELM327::engineLoad() - -// Description: -// ------------ -// * Find the current engine load in % - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * float - Engine load % -// */ -// float ELM327::engineLoad() -// { -// if (queryPID(SERVICE_01, ENGINE_LOAD)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); - -// return ELM_GENERAL_ERROR; -// } - - - - /* - float ELM327::engineCoolantTemp(void) + float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) Description: ------------ - * Find the current engine coolant temp in C + * Queries ELM327 for a specific type of vehicle telemetry data Inputs: ------- - * void + * uint8_t service - The diagnostic service ID. 01 is "Show current data" + * uint16_t pid - The Parameter ID (PID) from the service + * uint8_t num_responses - Number of lines of data to receive - see ELM datasheet "Talking to the vehicle". + This can speed up retrieval of information if you know how many responses will be sent. + Basically the OBD scanner will not wait for more responses if it does not need to go through + final timeout. Also prevents OBD scanners from sending mulitple of the same response. + * uint8_t numExpectedBytes - Number of valid bytes from the response to process + * float scaleFactor - Amount to scale the response by + * float bias - Amount to bias the response by Return: ------- - * float - Coolant temperature in C + * bool - Whether or not the query was submitted successfully */ -float ELM327::engineCoolantTemp(void) +float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) { if (nb_query_state == SEND_COMMAND) { - queryPID(SERVICE_01, ENGINE_COOLANT_TEMP, 1); + queryPID(service, pid, num_responses); nb_query_state = WAITING_RESP; } else if (nb_query_state == WAITING_RESP) @@ -611,7 +505,7 @@ float ELM327::engineCoolantTemp(void) if (nb_rx_state == ELM_SUCCESS) { nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - return conditionResponse(findResponse(), 1, 1.0, -40.0); + return conditionResponse(findResponse(), numExpectedBytes, scaleFactor, bias); } else if (nb_rx_state != ELM_GETTING_MSG) nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command @@ -622,168 +516,175 @@ float ELM327::engineCoolantTemp(void) -// /* -// float ELM327::shortTermFuelTrimBank_1() +/* + uint32_t ELM327::supportedPIDs_1_20() + + Description: + ------------ + * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) -// Description: -// ------------ -// * Find fuel trim % + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * uint32_t - Bit encoded booleans of supported PIDs 0x1-0x20 +*/ +uint32_t ELM327::supportedPIDs_1_20() +{ + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_1_20, 1, 4); +} -// Return: -// ------- -// * float - Fuel trim % -// */ -// float ELM327::shortTermFuelTrimBank_1() -// { -// if (queryPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_1)) -// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); -// return ELM_GENERAL_ERROR; -// } +/* + uint32_t ELM327::monitorStatus() + Description: + ------------ + * Monitor status since DTCs cleared (Includes malfunction indicator + lamp (MIL) status and number of DTCs). See https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01 + for more info -// /* -// float ELM327::longTermFuelTrimBank_1() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find fuel trim % + Return: + ------- + * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_01) +*/ +uint32_t ELM327::monitorStatus() +{ + return (uint32_t)processPID(SERVICE_01, MONITOR_STATUS_SINCE_DTC_CLEARED, 1, 4); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Fuel trim % -// */ -// float ELM327::longTermFuelTrimBank_1() -// { -// if (queryPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_1)) -// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); -// return ELM_GENERAL_ERROR; -// } +/* + uint16_t ELM327::freezeDTC() + Description: + ------------ + * Freeze DTC - see https://www.samarins.com/diagnose/freeze-frame.html for more info + Inputs: + ------- + * void -// /* -// float ELM327::shortTermFuelTrimBank_2() + Return: + ------- + * uint16_t - Various vehicle information (https://www.samarins.com/diagnose/freeze-frame.html) +*/ +uint16_t ELM327::freezeDTC() +{ + return (uint16_t)processPID(SERVICE_01, FREEZE_DTC, 1, 2); +} -// Description: -// ------------ -// * Find fuel trim % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Fuel trim % -// */ -// float ELM327::shortTermFuelTrimBank_2() -// { -// if (queryPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_2)) -// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); -// return ELM_GENERAL_ERROR; -// } +/* + uint16_t ELM327::fuelSystemStatus() + Description: + ------------ + * Freeze DTC - see https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03 for more info + Inputs: + ------- + * void + Return: + ------- + * uint16_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_03) +*/ +uint16_t ELM327::fuelSystemStatus() +{ + return (uint16_t)processPID(SERVICE_01, FUEL_SYSTEM_STATUS, 1, 2); +} -// /* -// float ELM327::longTermFuelTrimBank_2() -// Description: -// ------------ -// * Find fuel trim % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Fuel trim % -// */ -// float ELM327::longTermFuelTrimBank_2() -// { -// if (queryPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_2)) -// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100.0); +/* + float ELM327::engineLoad() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find the current engine load in % + Inputs: + ------- + * void + Return: + ------- + * float - Engine load % +*/ +float ELM327::engineLoad() +{ + return processPID(SERVICE_01, ENGINE_LOAD, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::fuelPressure() -// Description: -// ------------ -// * Find fuel pressure in kPa -// Inputs: -// ------- -// * void +/* + float ELM327::engineCoolantTemp() -// Return: -// ------- -// * float - Fuel pressure in kPa -// */ -// float ELM327::fuelPressure() -// { -// if (queryPID(SERVICE_01, FUEL_PRESSURE)) -// return conditionResponse(findResponse(), 1, 3.0); + Description: + ------------ + * Find the current engine coolant temp in C -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * float - Engine load % +*/ +float ELM327::engineCoolantTemp() +{ + return processPID(SERVICE_01, ENGINE_COOLANT_TEMP, 1, 1, 1, -40.0); +} -// /* -// uint8_t ELM327::manifoldPressure() -// Description: -// ------------ -// * Find intake manifold absolute pressure in kPa +/* + float ELM327::shortTermFuelTrimBank_1() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find fuel trim % -// Return: -// ------- -// * uint8_t - Intake manifold absolute pressure in kPa -// */ -// uint8_t ELM327::manifoldPressure() -// { -// if (queryPID(SERVICE_01, INTAKE_MANIFOLD_ABS_PRESSURE)) -// return conditionResponse(findResponse(), 1); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Fuel trim % +*/ +float ELM327::shortTermFuelTrimBank_1() +{ + return processPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_1, 1, 1, 100.0 / 128.0, -100.0); +} /* - float ELM327::rpm() + float ELM327::longTermFuelTrimBank_1() Description: ------------ - * Queries and parses received message for/returns vehicle RPM data + * Find fuel trim % Inputs: ------- @@ -791,1590 +692,1507 @@ float ELM327::engineCoolantTemp(void) Return: ------- - * float - Vehicle RPM + * float - Fuel trim % */ -float ELM327::rpm(void) +float ELM327::longTermFuelTrimBank_1() { - if (nb_query_state == SEND_COMMAND) - { - queryPID(SERVICE_01, ENGINE_RPM, 1); - nb_query_state = WAITING_RESP; - } - else if (nb_query_state == WAITING_RESP) - { - get_response(); - if (nb_rx_state == ELM_SUCCESS) - { - nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - return conditionResponse(findResponse(), 2, 1.0 / 4.0); - } - else if (nb_rx_state != ELM_GETTING_MSG) - nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command - } - return 0.0; + return processPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_1, 1, 1, 100.0 / 128.0, -100.0); } -// /* -// int32_t ELM327::kph() - -// Description: -// ------------ -// * Queries and parses received message for/returns vehicle speed data (kph) +/* + float ELM327::shortTermFuelTrimBank_2() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find fuel trim % -// Return: -// ------- -// * int32_t - Vehicle speed in kph -// */ -// int32_t ELM327::kph() -// { -// if (queryPID(SERVICE_01, VEHICLE_SPEED)) -// return conditionResponse(findResponse(), 1); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Fuel trim % +*/ +float ELM327::shortTermFuelTrimBank_2() +{ + return processPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_2, 1, 1, 100.0 / 128.0, -100.0); +} -// /* -// float ELM327::mph() +/* + float ELM327::longTermFuelTrimBank_2() -// Description: -// ------------ -// * Queries and parses received message for/returns vehicle speed data (mph) + Description: + ------------ + * Find fuel trim % -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * float - Vehicle speed in mph -// */ -// float ELM327::mph() -// { -// float mph = kph(); + Return: + ------- + * float - Fuel trim % +*/ +float ELM327::longTermFuelTrimBank_2() +{ + return processPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_2, 1, 1, 100.0 / 128.0, -100.0); +} -// if (status == ELM_SUCCESS) -// return mph * KPH_MPH_CONVERT; -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::fuelPressure() + Description: + ------------ + * Find fuel pressure in kPa -// /* -// float ELM327::timingAdvance() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find timing advance in degrees before Top Dead Center (TDC) + Return: + ------- + * float - Fuel pressure in kPa +*/ +float ELM327::fuelPressure() +{ + return processPID(SERVICE_01, FUEL_PRESSURE, 1, 1, 3.0); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Timing advance in degrees before Top Dead Center (TDC) -// */ -// float ELM327::timingAdvance() -// { -// if (queryPID(SERVICE_01, TIMING_ADVANCE)) -// return conditionResponse(findResponse(), 1, 1.0 / 2.0, -64.0); -// return ELM_GENERAL_ERROR; -// } +/* + uint8_t ELM327::manifoldPressure() + Description: + ------------ + * Find intake manifold absolute pressure in kPa + Inputs: + ------- + * void -// /* -// float ELM327::intakeAirTemp() + Return: + ------- + * uint8_t - Intake manifold absolute pressure in kPa +*/ +uint8_t ELM327::manifoldPressure() +{ + return (uint8_t)processPID(SERVICE_01, INTAKE_MANIFOLD_ABS_PRESSURE, 1, 1); +} -// Description: -// ------------ -// * Find intake air temperature in C -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Intake air temperature in C -// */ -// float ELM327::intakeAirTemp() -// { -// if (queryPID(SERVICE_01, INTAKE_AIR_TEMP)) -// return conditionResponse(findResponse(), 1, 1, -40.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::rpm() + Description: + ------------ + * Queries and parses received message for/returns vehicle RMP data + Inputs: + ------- + * void + Return: + ------- + * float - Vehicle RPM +*/ +float ELM327::rpm() +{ + return processPID(SERVICE_01, ENGINE_RPM, 1, 2, 1.0 / 4.0); +} -// /* -// float ELM327::mafRate() -// Description: -// ------------ -// * Find mass air flow sensor (MAF) air flow rate rate in g/s -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Mass air flow sensor (MAF) air flow rate rate in g/s -// */ -// float ELM327::mafRate() -// { -// if (queryPID(SERVICE_01, MAF_FLOW_RATE)) -// return conditionResponse(findResponse(), 2, 1.0 / 100.0); +/* + int32_t ELM327::kph() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Queries and parses received message for/returns vehicle speed data (kph) + Inputs: + ------- + * void + Return: + ------- + * int32_t - Vehicle speed in kph +*/ +int32_t ELM327::kph() +{ + return (int32_t)processPID(SERVICE_01, VEHICLE_SPEED, 1, 1); +} -// /* -// float ELM327::throttle() -// Description: -// ------------ -// * Find throttle position in % -// Inputs: -// ------- -// * void +/* + float ELM327::mph() -// Return: -// ------- -// * float - Throttle position in % -// */ -// float ELM327::throttle() -// { -// if (queryPID(SERVICE_01, THROTTLE_POSITION)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); + Description: + ------------ + * Queries and parses received message for/returns vehicle speed data (mph) -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * float - Vehicle speed in mph +*/ +float ELM327::mph() +{ + return kph() * KPH_MPH_CONVERT; +} -// /* -// uint8_t ELM327::commandedSecAirStatus() -// Description: -// ------------ -// * Find commanded secondary air status +/* + float ELM327::timingAdvance() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find timing advance in degrees before Top Dead Center (TDC) -// Return: -// ------- -// * uint8_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_12) -// */ -// uint8_t ELM327::commandedSecAirStatus() -// { -// if (queryPID(SERVICE_01, COMMANDED_SECONDARY_AIR_STATUS)) -// return conditionResponse(findResponse(), 1); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Timing advance in degrees before Top Dead Center (TDC) +*/ +float ELM327::timingAdvance() +{ + return processPID(SERVICE_01, TIMING_ADVANCE, 1, 1, 1.0 / 2.0, -64.0); +} -// /* -// uint8_t ELM327::oxygenSensorsPresent_2banks() +/* + float ELM327::intakeAirTemp() -// Description: -// ------------ -// * Find which oxygen sensors are present ([A0..A3] == Bank 1, Sensors 1-4. [A4..A7] == Bank 2...) + Description: + ------------ + * Find intake air temperature in C -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * uint8_t - Bit encoded -// */ -// uint8_t ELM327::oxygenSensorsPresent_2banks() -// { -// if (queryPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_2_BANKS)) -// return conditionResponse(findResponse(), 1); + Return: + ------- + * float - Intake air temperature in C +*/ +float ELM327::intakeAirTemp() +{ + return processPID(SERVICE_01, INTAKE_AIR_TEMP, 1, 1, -40.0); +} -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::mafRate() -// /* -// uint8_t ELM327::obdStandards() + Description: + ------------ + * Find mass air flow sensor (MAF) air flow rate rate in g/s -// Description: -// ------------ -// * Find the OBD standards this vehicle conforms to (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) -// */ -// uint8_t ELM327::obdStandards() -// { -// if (queryPID(SERVICE_01, OBD_STANDARDS)) -// return conditionResponse(findResponse(), 1); - -// return ELM_GENERAL_ERROR; -// } - - - - -// /* -// uint8_t ELM327::oxygenSensorsPresent_4banks() - -// Description: -// ------------ -// * Find which oxygen sensors are present (Similar to PID 13, but [A0..A7] == [B1S1, B1S2, B2S1, B2S2, B3S1, B3S2, B4S1, B4S2]) - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * uint8_t - Bit encoded -// */ -// uint8_t ELM327::oxygenSensorsPresent_4banks() -// { -// if (queryPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_4_BANKS)) -// return conditionResponse(findResponse(), 1); - -// return ELM_GENERAL_ERROR; -// } - - - - -// /* -// bool ELM327::auxInputStatus() - -// Description: -// ------------ -// * Find Power Take Off (PTO) status - -// Inputs: -// ------- -// * void - -// Return: -// ------- -// * bool - Power Take Off (PTO) status -// */ -// bool ELM327::auxInputStatus() -// { -// if (queryPID(SERVICE_01, AUX_INPUT_STATUS)) -// return conditionResponse(findResponse(), 1); - -// return ELM_GENERAL_ERROR; -// } - - - - -// /* -// uint16_t ELM327::runTime() - -// Description: -// ------------ -// * Find run time since engine start in s - -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * uint16_t - Run time since engine start in s -// */ -// uint16_t ELM327::runTime() -// { -// if (queryPID(SERVICE_01, RUN_TIME_SINCE_ENGINE_START)) -// return conditionResponse(findResponse(), 2); + Return: + ------- + * float - Mass air flow sensor (MAF) air flow rate rate in g/s +*/ +float ELM327::mafRate() +{ + return processPID(SERVICE_01, MAF_FLOW_RATE, 1, 2, 1.0 / 100.0); +} -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::throttle() -// /* -// uint32_t ELM327::supportedPIDs_21_40() + Description: + ------------ + * Find throttle position in % -// Description: -// ------------ -// * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * float - Throttle position in % +*/ +float ELM327::throttle() +{ + return processPID(SERVICE_01, THROTTLE_POSITION, 1, 1, 100.0 / 255.0); +} -// Return: -// ------- -// * uint32_t - Bit encoded booleans of supported PIDs 0x21-0x20 -// */ -// uint32_t ELM327::supportedPIDs_21_40() -// { -// if (queryPID(SERVICE_01, SUPPORTED_PIDS_21_40)) -// return conditionResponse(findResponse(), 4); -// return ELM_GENERAL_ERROR; -// } +/* + uint8_t ELM327::commandedSecAirStatus() + Description: + ------------ + * Find commanded secondary air status -// /* -// uint16_t ELM327::distTravelWithMIL() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find distance traveled with malfunction indicator lamp (MIL) on in km + Return: + ------- + * uint8_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_12) +*/ +uint8_t ELM327::commandedSecAirStatus() +{ + return (uint8_t)processPID(SERVICE_01, COMMANDED_SECONDARY_AIR_STATUS, 1, 1); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint16_t - Distance traveled with malfunction indicator lamp (MIL) on in km -// */ -// uint16_t ELM327::distTravelWithMIL() -// { -// if (queryPID(SERVICE_01, DISTANCE_TRAVELED_WITH_MIL_ON)) -// return conditionResponse(findResponse(), 2); -// return ELM_GENERAL_ERROR; -// } +/* + uint8_t ELM327::oxygenSensorsPresent_2banks() + Description: + ------------ + * Find which oxygen sensors are present ([A0..A3] == Bank 1, Sensors 1-4. [A4..A7] == Bank 2...) + Inputs: + ------- + * void -// /* -// float ELM327::fuelRailPressure() + Return: + ------- + * uint8_t - Bit encoded +*/ +uint8_t ELM327::oxygenSensorsPresent_2banks() +{ + return (uint8_t)processPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_2_BANKS, 1, 1); +} -// Description: -// ------------ -// * Find fuel Rail Pressure (relative to manifold vacuum) in kPa -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Fuel Rail Pressure (relative to manifold vacuum) in kPa -// */ -// float ELM327::fuelRailPressure() -// { -// if (queryPID(SERVICE_01, FUEL_RAIL_PRESSURE)) -// return conditionResponse(findResponse(), 2, 0.079); -// return ELM_GENERAL_ERROR; -// } +/* + uint8_t ELM327::obdStandards() + Description: + ------------ + * Find the OBD standards this vehicle conforms to (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) + Inputs: + ------- + * void + Return: + ------- + * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_1C) +*/ +uint8_t ELM327::obdStandards() +{ + return (uint8_t)processPID(SERVICE_01, OBD_STANDARDS, 1, 1); +} -// /* -// float ELM327::fuelRailGuagePressure() -// Description: -// ------------ -// * Find fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa -// */ -// float ELM327::fuelRailGuagePressure() -// { -// if (queryPID(SERVICE_01, FUEL_RAIL_GUAGE_PRESSURE)) -// return conditionResponse(findResponse(), 2, 10.0); +/* + uint8_t ELM327::oxygenSensorsPresent_4banks() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find which oxygen sensors are present (Similar to PID 13, but [A0..A7] == [B1S1, B1S2, B2S1, B2S2, B3S1, B3S2, B4S1, B4S2]) + Inputs: + ------- + * void + Return: + ------- + * uint8_t - Bit encoded +*/ +uint8_t ELM327::oxygenSensorsPresent_4banks() +{ + return (uint8_t)processPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_4_BANKS, 1, 1); +} -// /* -// float ELM327::commandedEGR() -// Description: -// ------------ -// * Find commanded Exhaust Gas Recirculation (EGR) in % -// Inputs: -// ------- -// * void +/* + bool ELM327::auxInputStatus() -// Return: -// ------- -// * float - Commanded Exhaust Gas Recirculation (EGR) in % -// */ -// float ELM327::commandedEGR() -// { -// if (queryPID(SERVICE_01, COMMANDED_EGR)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); + Description: + ------------ + * Find Power Take Off (PTO) status -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * bool - Power Take Off (PTO) status +*/ +bool ELM327::auxInputStatus() +{ + return (bool)processPID(SERVICE_01, AUX_INPUT_STATUS, 1, 1); +} -// /* -// float ELM327::egrError() -// Description: -// ------------ -// * Find Exhaust Gas Recirculation (EGR) error in % +/* + uint16_t ELM327::runTime() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find run time since engine start in s -// Return: -// ------- -// * float - Exhaust Gas Recirculation (EGR) error in % -// */ -// float ELM327::egrError() -// { -// if (queryPID(SERVICE_01, EGR_ERROR)) -// return conditionResponse(findResponse(), 1, 100.0 / 128.0, -100); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * uint16_t - Run time since engine start in s +*/ +uint16_t ELM327::runTime() +{ + return (uint16_t)processPID(SERVICE_01, RUN_TIME_SINCE_ENGINE_START, 1, 2); +} -// /* -// float ELM327::commandedEvapPurge() +/* + uint32_t ELM327::supportedPIDs_21_40() -// Description: -// ------------ -// * Find commanded evaporative purge in % + Description: + ------------ + * Determine which of PIDs 0x1 through 0x20 are supported (bit encoded) -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * float - Commanded evaporative purge in % -// */ -// float ELM327::commandedEvapPurge() -// { -// if (queryPID(SERVICE_01, COMMANDED_EVAPORATIVE_PURGE)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); + Return: + ------- + * uint32_t - Bit encoded booleans of supported PIDs 0x21-0x20 +*/ +uint32_t ELM327::supportedPIDs_21_40() +{ + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_21_40, 1, 4); +} -// return ELM_GENERAL_ERROR; -// } +/* + uint16_t ELM327::distTravelWithMIL() -// /* -// float ELM327::fuelLevel() + Description: + ------------ + * Find distance traveled with malfunction indicator lamp (MIL) on in km -// Description: -// ------------ -// * Find fuel tank level input in % + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * uint16_t - Distance traveled with malfunction indicator lamp (MIL) on in km +*/ +uint16_t ELM327::distTravelWithMIL() +{ + return (uint16_t)processPID(SERVICE_01, DISTANCE_TRAVELED_WITH_MIL_ON, 1, 2); +} -// Return: -// ------- -// * float - Fuel tank level input in % -// */ -// float ELM327::fuelLevel() -// { -// if (queryPID(SERVICE_01, FUEL_TANK_LEVEL_INPUT)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::fuelRailPressure() + Description: + ------------ + * Find fuel Rail Pressure (relative to manifold vacuum) in kPa -// /* -// uint8_t ELM327::warmUpsSinceCodesCleared() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find num warm-ups since codes cleared + Return: + ------- + * float - Fuel Rail Pressure (relative to manifold vacuum) in kPa +*/ +float ELM327::fuelRailPressure() +{ + return processPID(SERVICE_01, FUEL_RAIL_PRESSURE, 1, 2, 0.079); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint8_t - Num warm-ups since codes cleared -// */ -// uint8_t ELM327::warmUpsSinceCodesCleared() -// { -// if (queryPID(SERVICE_01, WARM_UPS_SINCE_CODES_CLEARED)) -// return conditionResponse(findResponse(), 1); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::fuelRailGuagePressure() + Description: + ------------ + * Find fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa + Inputs: + ------- + * void -// /* -// uint16_t ELM327::distSinceCodesCleared() + Return: + ------- + * float - Fuel Rail Gauge Pressure (diesel, or gasoline direct injection) in kPa +*/ +float ELM327::fuelRailGuagePressure() +{ + return processPID(SERVICE_01, FUEL_RAIL_GUAGE_PRESSURE, 1, 2, 10.0); +} -// Description: -// ------------ -// * Find distance traveled since codes cleared in km -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint16_t - Distance traveled since codes cleared in km -// */ -// uint16_t ELM327::distSinceCodesCleared() -// { -// if (queryPID(SERVICE_01, DIST_TRAV_SINCE_CODES_CLEARED)) -// return conditionResponse(findResponse(), 2); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::commandedEGR() + Description: + ------------ + * Find commanded Exhaust Gas Recirculation (EGR) in % + Inputs: + ------- + * void + Return: + ------- + * float - Commanded Exhaust Gas Recirculation (EGR) in % +*/ +float ELM327::commandedEGR() +{ + return processPID(SERVICE_01, COMMANDED_EGR, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::evapSysVapPressure() -// Description: -// ------------ -// * Find evap. system vapor pressure in Pa -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Evap. system vapor pressure in Pa -// */ -// float ELM327::evapSysVapPressure() -// { -// if (queryPID(SERVICE_01, EVAP_SYSTEM_VAPOR_PRESSURE)) -// return conditionResponse(findResponse(), 2, 1.0 / 4.0); +/* + float ELM327::egrError() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find Exhaust Gas Recirculation (EGR) error in % + Inputs: + ------- + * void + Return: + ------- + * float - Exhaust Gas Recirculation (EGR) error in % +*/ +float ELM327::egrError() +{ + return processPID(SERVICE_01, EGR_ERROR, 1, 1, 100.0 / 128.0, -100); +} -// /* -// uint8_t ELM327::absBaroPressure() -// Description: -// ------------ -// * Find absolute barometric pressure in kPa -// Inputs: -// ------- -// * void +/* + float ELM327::commandedEvapPurge() -// Return: -// ------- -// * uint8_t - Absolute barometric pressure in kPa -// */ -// uint8_t ELM327::absBaroPressure() -// { -// if (queryPID(SERVICE_01, ABS_BAROMETRIC_PRESSURE)) -// return conditionResponse(findResponse(), 1); + Description: + ------------ + * Find commanded evaporative purge in % -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * float - Commanded evaporative purge in % +*/ +float ELM327::commandedEvapPurge() +{ + return processPID(SERVICE_01, COMMANDED_EVAPORATIVE_PURGE, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::catTempB1S1() -// Description: -// ------------ -// * Find catalyst temperature in C +/* + float ELM327::fuelLevel() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find fuel tank level input in % -// Return: -// ------- -// * float - Catalyst temperature in C -// */ -// float ELM327::catTempB1S1() -// { -// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_1)) -// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Fuel tank level input in % +*/ +float ELM327::fuelLevel() +{ + return processPID(SERVICE_01, FUEL_TANK_LEVEL_INPUT, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::catTempB2S1() +/* + uint8_t ELM327::warmUpsSinceCodesCleared() -// Description: -// ------------ -// * Find catalyst temperature in C + Description: + ------------ + * Find num warm-ups since codes cleared -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * float - Catalyst temperature in C -// */ -// float ELM327::catTempB2S1() -// { -// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_1)) -// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); + Return: + ------- + * uint8_t - Num warm-ups since codes cleared +*/ +uint8_t ELM327::warmUpsSinceCodesCleared() +{ + return (uint8_t)processPID(SERVICE_01, WARM_UPS_SINCE_CODES_CLEARED, 1, 1); +} -// return ELM_GENERAL_ERROR; -// } +/* + uint16_t ELM327::distSinceCodesCleared() -// /* -// float ELM327::catTempB1S2() + Description: + ------------ + * Find distance traveled since codes cleared in km -// Description: -// ------------ -// * Find catalyst temperature in C + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * uint16_t - Distance traveled since codes cleared in km +*/ +uint16_t ELM327::distSinceCodesCleared() +{ + return (uint16_t)processPID(SERVICE_01, DIST_TRAV_SINCE_CODES_CLEARED, 1, 2); +} -// Return: -// ------- -// * float - Catalyst temperature in C -// */ -// float ELM327::catTempB1S2() -// { -// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_2)) -// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::evapSysVapPressure() + Description: + ------------ + * Find evap. system vapor pressure in Pa -// /* -// float ELM327::catTempB2S2() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find catalyst temperature in C + Return: + ------- + * float - Evap. system vapor pressure in Pa +*/ +float ELM327::evapSysVapPressure() +{ + return processPID(SERVICE_01, EVAP_SYSTEM_VAPOR_PRESSURE, 1, 2, 1.0 / 4.0); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Catalyst temperature in C -// */ -// float ELM327::catTempB2S2() -// { -// if (queryPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_2)) -// return conditionResponse(findResponse(), 2, 1.0 / 10.0, -40.0); -// return ELM_GENERAL_ERROR; -// } +/* + uint8_t ELM327::absBaroPressure() + Description: + ------------ + * Find absolute barometric pressure in kPa + Inputs: + ------- + * void -// /* -// uint32_t ELM327::supportedPIDs_41_60() + Return: + ------- + * uint8_t - Absolute barometric pressure in kPa +*/ +uint8_t ELM327::absBaroPressure() +{ + return (uint8_t)processPID(SERVICE_01, ABS_BAROMETRIC_PRESSURE, 1, 1); +} -// Description: -// ------------ -// * Determine which of PIDs 0x41 through 0x60 are supported (bit encoded) -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint32_t - Bit encoded booleans of supported PIDs 0x41-0x60 -// */ -// uint32_t ELM327::supportedPIDs_41_60() -// { -// if (queryPID(SERVICE_01, SUPPORTED_PIDS_41_60)) -// return conditionResponse(findResponse(), 4); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::catTempB1S1() + Description: + ------------ + * Find catalyst temperature in C + Inputs: + ------- + * void + Return: + ------- + * float - Catalyst temperature in C +*/ +float ELM327::catTempB1S1() +{ + return processPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_1, 1, 2, 1.0 / 10.0, -40.0); +} -// /* -// uint32_t ELM327::monitorDriveCycleStatus() -// Description: -// ------------ -// * Find status this drive cycle (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) -// */ -// uint32_t ELM327::monitorDriveCycleStatus() -// { -// if (queryPID(SERVICE_01, MONITOR_STATUS_THIS_DRIVE_CYCLE)) -// return conditionResponse(findResponse(), 4); +/* + float ELM327::catTempB2S1() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find catalyst temperature in C + Inputs: + ------- + * void + Return: + ------- + * float - Catalyst temperature in C +*/ +float ELM327::catTempB2S1() +{ + return processPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_1, 1, 2, 1.0 / 10.0, -40.0); +} -// /* -// float ELM327::ctrlModVoltage() -// Description: -// ------------ -// * Find control module voltage in V -// Inputs: -// ------- -// * void +/* + float ELM327::catTempB1S2() -// Return: -// ------- -// * float - Control module voltage in V -// */ -// float ELM327::ctrlModVoltage() -// { -// if (queryPID(SERVICE_01, CONTROL_MODULE_VOLTAGE)) -// return conditionResponse(findResponse(), 2, 1.0 / 1000.0); + Description: + ------------ + * Find catalyst temperature in C -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * float - Catalyst temperature in C +*/ +float ELM327::catTempB1S2() +{ + return processPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_2, 1, 2, 1.0 / 10.0, -40.0); +} -// /* -// float ELM327::absLoad() -// Description: -// ------------ -// * Find absolute load value in % +/* + float ELM327::catTempB2S2() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find catalyst temperature in C -// Return: -// ------- -// * float - Absolute load value in % -// */ -// float ELM327::absLoad() -// { -// if (queryPID(SERVICE_01, ABS_LOAD_VALUE)) -// return conditionResponse(findResponse(), 2, 100.0 / 255.0); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Catalyst temperature in C +*/ +float ELM327::catTempB2S2() +{ + return processPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_2, 1, 2, 1.0 / 10.0, -40.0); +} -// /* -// float ELM327::commandedAirFuelRatio() +/* + uint32_t ELM327::supportedPIDs_41_60() -// Description: -// ------------ -// * Find commanded air-fuel equivalence ratio + Description: + ------------ + * Determine which of PIDs 0x41 through 0x60 are supported (bit encoded) -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * float - Commanded air-fuel equivalence ratio -// */ -// float ELM327::commandedAirFuelRatio() -// { -// if (queryPID(SERVICE_01, FUEL_AIR_COMMANDED_EQUIV_RATIO)) -// return conditionResponse(findResponse(), 2, 2.0 / 65536.0); + Return: + ------- + * uint32_t - Bit encoded booleans of supported PIDs 0x41-0x60 +*/ +uint32_t ELM327::supportedPIDs_41_60() +{ + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_41_60, 1, 4); +} -// return ELM_GENERAL_ERROR; -// } +/* + uint32_t ELM327::monitorDriveCycleStatus() -// /* -// float ELM327::relativeThrottle() + Description: + ------------ + * Find status this drive cycle (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) -// Description: -// ------------ -// * Find relative throttle position in % + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * uint32_t - Bit encoded status (https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01_PID_41) +*/ +uint32_t ELM327::monitorDriveCycleStatus() +{ + return (uint32_t)processPID(SERVICE_01, MONITOR_STATUS_THIS_DRIVE_CYCLE, 1, 4); +} -// Return: -// ------- -// * float - Relative throttle position in % -// */ -// float ELM327::relativeThrottle() -// { -// if (queryPID(SERVICE_01, RELATIVE_THROTTLE_POSITION)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::ctrlModVoltage() + Description: + ------------ + * Find control module voltage in V -// /* -// float ELM327::ambientAirTemp() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find ambient air temperature in C + Return: + ------- + * float - Control module voltage in V +*/ +float ELM327::ctrlModVoltage() +{ + return processPID(SERVICE_01, CONTROL_MODULE_VOLTAGE, 1, 2, 1.0 / 1000.0); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Ambient air temperature in C -// */ -// float ELM327::ambientAirTemp() -// { -// if (queryPID(SERVICE_01, AMBIENT_AIR_TEMP)) -// return conditionResponse(findResponse(), 1, 1, -40); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::absLoad() + Description: + ------------ + * Find absolute load value in % + Inputs: + ------- + * void -// /* -// float ELM327::absThrottlePosB() + Return: + ------- + * float - Absolute load value in % +*/ +float ELM327::absLoad() +{ + return processPID(SERVICE_01, ABS_LOAD_VALUE, 1, 2, 100.0 / 255.0); +} -// Description: -// ------------ -// * Find absolute throttle position B in % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Absolute throttle position B in % -// */ -// float ELM327::absThrottlePosB() -// { -// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_B)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::commandedAirFuelRatio() + Description: + ------------ + * Find commanded air-fuel equivalence ratio + Inputs: + ------- + * void + Return: + ------- + * float - Commanded air-fuel equivalence ratio +*/ +float ELM327::commandedAirFuelRatio() +{ + return processPID(SERVICE_01, FUEL_AIR_COMMANDED_EQUIV_RATIO, 1, 2, 2.0 / 65536.0); +} -// /* -// float ELM327::absThrottlePosC() -// Description: -// ------------ -// * Find absolute throttle position C in % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Absolute throttle position C in % -// */ -// float ELM327::absThrottlePosC() -// { -// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_C)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); +/* + float ELM327::relativeThrottle() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find relative throttle position in % + Inputs: + ------- + * void + Return: + ------- + * float - Relative throttle position in % +*/ +float ELM327::relativeThrottle() +{ + return processPID(SERVICE_01, RELATIVE_THROTTLE_POSITION, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::absThrottlePosD() -// Description: -// ------------ -// * Find absolute throttle position D in % -// Inputs: -// ------- -// * void +/* + float ELM327::ambientAirTemp() -// Return: -// ------- -// * float - Absolute throttle position D in % -// */ -// float ELM327::absThrottlePosD() -// { -// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_D)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); + Description: + ------------ + * Find ambient air temperature in C -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * float - Ambient air temperature in C +*/ +float ELM327::ambientAirTemp() +{ + return processPID(SERVICE_01, AMBIENT_AIR_TEMP, 1, 1, 1, -40); +} -// /* -// float ELM327::absThrottlePosE() -// Description: -// ------------ -// * Find absolute throttle position E in % +/* + float ELM327::absThrottlePosB() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find absolute throttle position B in % -// Return: -// ------- -// * float - Absolute throttle position E in % -// */ -// float ELM327::absThrottlePosE() -// { -// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_E)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Absolute throttle position B in % +*/ +float ELM327::absThrottlePosB() +{ + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_B, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::absThrottlePosF() +/* + float ELM327::absThrottlePosC() -// Description: -// ------------ -// * Find absolute throttle position F in % + Description: + ------------ + * Find absolute throttle position C in % -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * float - Absolute throttle position F in % -// */ -// float ELM327::absThrottlePosF() -// { -// if (queryPID(SERVICE_01, ABS_THROTTLE_POSITION_F)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); + Return: + ------- + * float - Absolute throttle position C in % +*/ +float ELM327::absThrottlePosC() +{ + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_C, 1, 1, 100.0 / 255.0); +} -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::absThrottlePosD() -// /* -// float ELM327::commandedThrottleActuator() + Description: + ------------ + * Find absolute throttle position D in % -// Description: -// ------------ -// * Find commanded throttle actuator in % + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * float - Absolute throttle position D in % +*/ +float ELM327::absThrottlePosD() +{ + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_D, 1, 1, 100.0 / 255.0); +} -// Return: -// ------- -// * float - Commanded throttle actuator in % -// */ -// float ELM327::commandedThrottleActuator() -// { -// if (queryPID(SERVICE_01, COMMANDED_THROTTLE_ACTUATOR)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::absThrottlePosE() + Description: + ------------ + * Find absolute throttle position E in % -// /* -// uint16_t ELM327::timeRunWithMIL() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find time run with MIL on in min + Return: + ------- + * float - Absolute throttle position E in % +*/ +float ELM327::absThrottlePosE() +{ + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_E, 1, 1, 100.0 / 255.0); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint16_t - Time run with MIL on in min -// */ -// uint16_t ELM327::timeRunWithMIL() -// { -// if (queryPID(SERVICE_01, TIME_RUN_WITH_MIL_ON)) -// return conditionResponse(findResponse(), 2); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::absThrottlePosF() + Description: + ------------ + * Find absolute throttle position F in % + Inputs: + ------- + * void -// /* -// uint16_t ELM327::timeSinceCodesCleared() + Return: + ------- + * float - Absolute throttle position F in % +*/ +float ELM327::absThrottlePosF() +{ + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_F, 1, 1, 100.0 / 255.0); +} -// Description: -// ------------ -// * Find time since trouble codes cleared in min -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint16_t - Time since trouble codes cleared in min -// */ -// uint16_t ELM327::timeSinceCodesCleared() -// { -// if (queryPID(SERVICE_01, TIME_SINCE_CODES_CLEARED)) -// return conditionResponse(findResponse(), 2); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::commandedThrottleActuator() + Description: + ------------ + * Find commanded throttle actuator in % + Inputs: + ------- + * void + Return: + ------- + * float - Commanded throttle actuator in % +*/ +float ELM327::commandedThrottleActuator() +{ + return processPID(SERVICE_01, COMMANDED_THROTTLE_ACTUATOR, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::maxMafRate() -// Description: -// ------------ -// * Find maximum value for air flow rate from mass air flow sensor in g/s -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Maximum value for air flow rate from mass air flow sensor in g/s -// */ -// float ELM327::maxMafRate() -// { -// if (queryPID(SERVICE_01, MAX_MAF_RATE)) -// return conditionResponse(findResponse(), 1, 10.0); +/* + uint16_t ELM327::timeRunWithMIL() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find time run with MIL on in min + Inputs: + ------- + * void + Return: + ------- + * uint16_t - Time run with MIL on in min +*/ +uint16_t ELM327::timeRunWithMIL() +{ + return (uint16_t)processPID(SERVICE_01, TIME_RUN_WITH_MIL_ON, 1, 2); +} -// /* -// uint8_t ELM327::fuelType() -// Description: -// ------------ -// * Find fuel type (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) -// Inputs: -// ------- -// * void +/* + uint16_t ELM327::timeSinceCodesCleared() -// Return: -// ------- -// * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) -// */ -// uint8_t ELM327::fuelType() -// { -// if (queryPID(SERVICE_01, FUEL_TYPE)) -// return conditionResponse(findResponse(), 1); + Description: + ------------ + * Find time since trouble codes cleared in min -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * uint16_t - Time since trouble codes cleared in min +*/ +uint16_t ELM327::timeSinceCodesCleared() +{ + return (uint16_t)processPID(SERVICE_01, TIME_SINCE_CODES_CLEARED, 1, 2); +} -// /* -// float ELM327::ethonolPercent() -// Description: -// ------------ -// * Find ethanol fuel in % +/* + float ELM327::maxMafRate() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find maximum value for air flow rate from mass air flow sensor in g/s -// Return: -// ------- -// * float - Ethanol fuel in % -// */ -// float ELM327::ethonolPercent() -// { -// if (queryPID(SERVICE_01, ETHONOL_FUEL_PERCENT)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Maximum value for air flow rate from mass air flow sensor in g/s +*/ +float ELM327::maxMafRate() +{ + return processPID(SERVICE_01, MAX_MAF_RATE, 1, 1, 10.0); +} -// /* -// float ELM327::absEvapSysVapPressure() +/* + uint8_t ELM327::fuelType() -// Description: -// ------------ -// * Find absolute evap. system vapor pressure in kPa + Description: + ------------ + * Find fuel type (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * float - Absolute evap. system vapor pressure in kPa -// */ -// float ELM327::absEvapSysVapPressure() -// { -// if (queryPID(SERVICE_01, ABS_EVAP_SYS_VAPOR_PRESSURE)) -// return conditionResponse(findResponse(), 2, 1.0 / 200.0); + Return: + ------- + * uint8_t - Bit encoded (https://en.wikipedia.org/wiki/OBD-II_PIDs#Fuel_Type_Coding) +*/ +uint8_t ELM327::fuelType() +{ + return (uint8_t)processPID(SERVICE_01, FUEL_TYPE, 1, 1); +} -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::ethonolPercent() -// /* -// float ELM327::evapSysVapPressure2() + Description: + ------------ + * Find ethanol fuel in % -// Description: -// ------------ -// * Find evap. system vapor pressure in Pa + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * float - Ethanol fuel in % +*/ +float ELM327::ethonolPercent() +{ + return processPID(SERVICE_01, ETHONOL_FUEL_PERCENT, 1, 1, 100.0 / 255.0); +} -// Return: -// ------- -// * float - Evap. system vapor pressure in Pa -// */ -// float ELM327::evapSysVapPressure2() -// { -// if (queryPID(SERVICE_01, EVAP_SYS_VAPOR_PRESSURE)) -// return conditionResponse(findResponse(), 2, 1, -32767); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::absEvapSysVapPressure() + Description: + ------------ + * Find absolute evap. system vapor pressure in kPa -// /* -// float ELM327::absFuelRailPressure() + Inputs: + ------- + * void -// Description: -// ------------ -// * Find absolute fuel rail pressure in kPa + Return: + ------- + * float - Absolute evap. system vapor pressure in kPa +*/ +float ELM327::absEvapSysVapPressure() +{ + return processPID(SERVICE_01, ABS_EVAP_SYS_VAPOR_PRESSURE, 1, 2, 1.0 / 200.0); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - absolute fuel rail pressure in kPa -// */ -// float ELM327::absFuelRailPressure() -// { -// if (queryPID(SERVICE_01, FUEL_RAIL_ABS_PRESSURE)) -// return conditionResponse(findResponse(), 2, 10.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::evapSysVapPressure2() + Description: + ------------ + * Find evap. system vapor pressure in Pa + Inputs: + ------- + * void -// /* -// float ELM327::relativePedalPos() + Return: + ------- + * float - Evap. system vapor pressure in Pa +*/ +float ELM327::evapSysVapPressure2() +{ + return processPID(SERVICE_01, EVAP_SYS_VAPOR_PRESSURE, 1, 2, 1, -32767); +} -// Description: -// ------------ -// * Find relative accelerator pedal position in % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Relative accelerator pedal position in % -// */ -// float ELM327::relativePedalPos() -// { -// if (queryPID(SERVICE_01, RELATIVE_ACCELERATOR_PEDAL_POS)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::absFuelRailPressure() + Description: + ------------ + * Find absolute fuel rail pressure in kPa + Inputs: + ------- + * void + Return: + ------- + * float - absolute fuel rail pressure in kPa +*/ +float ELM327::absFuelRailPressure() +{ + return processPID(SERVICE_01, FUEL_RAIL_ABS_PRESSURE, 1, 2, 10.0); +} -// /* -// float ELM327::hybridBatLife() -// Description: -// ------------ -// * Find hybrid battery pack remaining life in % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Hybrid battery pack remaining life in % -// */ -// float ELM327::hybridBatLife() -// { -// if (queryPID(SERVICE_01, HYBRID_BATTERY_REMAINING_LIFE)) -// return conditionResponse(findResponse(), 1, 100.0 / 255.0); +/* + float ELM327::relativePedalPos() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find relative accelerator pedal position in % + Inputs: + ------- + * void + Return: + ------- + * float - Relative accelerator pedal position in % +*/ +float ELM327::relativePedalPos() +{ + return processPID(SERVICE_01, RELATIVE_ACCELERATOR_PEDAL_POS, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::oilTemp() -// Description: -// ------------ -// * Find engine oil temperature in C -// Inputs: -// ------- -// * void +/* + float ELM327::hybridBatLife() -// Return: -// ------- -// * float - Engine oil temperature in C -// */ -// float ELM327::oilTemp() -// { -// if (queryPID(SERVICE_01, ENGINE_OIL_TEMP)) -// return conditionResponse(findResponse(), 1, 1, -40.0); + Description: + ------------ + * Find hybrid battery pack remaining life in % -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * float - Hybrid battery pack remaining life in % +*/ +float ELM327::hybridBatLife() +{ + return processPID(SERVICE_01, HYBRID_BATTERY_REMAINING_LIFE, 1, 1, 100.0 / 255.0); +} -// /* -// float ELM327::fuelInjectTiming() -// Description: -// ------------ -// * Find fuel injection timing in degrees +/* + float ELM327::oilTemp() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find engine oil temperature in C -// Return: -// ------- -// * float - Fuel injection timing in degrees -// */ -// float ELM327::fuelInjectTiming() -// { -// if (queryPID(SERVICE_01, FUEL_INJECTION_TIMING)) -// return conditionResponse(findResponse(), 2, 1.0 / 128.0, -210.0); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * float - Engine oil temperature in C +*/ +float ELM327::oilTemp() +{ + return processPID(SERVICE_01, ENGINE_OIL_TEMP, 1, 1, 1, -40.0); +} -// /* -// float ELM327::fuelRate() +/* + float ELM327::fuelInjectTiming() -// Description: -// ------------ -// * Find engine fuel rate in L/h + Description: + ------------ + * Find fuel injection timing in degrees -// Inputs: -// ------- -// * void + Inputs: + ------- + * void -// Return: -// ------- -// * float - Engine fuel rate in L/h -// */ -// float ELM327::fuelRate() -// { -// if (queryPID(SERVICE_01, ENGINE_FUEL_RATE)) -// return conditionResponse(findResponse(), 2, 1.0 / 20.0); + Return: + ------- + * float - Fuel injection timing in degrees +*/ +float ELM327::fuelInjectTiming() +{ + return processPID(SERVICE_01, FUEL_INJECTION_TIMING, 1, 2, 1.0 / 128.0, -210.0); +} -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::fuelRate() -// /* -// uint8_t ELM327::emissionRqmts() + Description: + ------------ + * Find engine fuel rate in L/h -// Description: -// ------------ -// * Find emission requirements to which vehicle is designed + Inputs: + ------- + * void -// Inputs: -// ------- -// * void + Return: + ------- + * float - Engine fuel rate in L/h +*/ +float ELM327::fuelRate() +{ + return processPID(SERVICE_01, ENGINE_FUEL_RATE, 1, 2, 1.0 / 20.0); +} -// Return: -// ------- -// * uint8_t - Bit encoded (?) -// */ -// uint8_t ELM327::emissionRqmts() -// { -// if (queryPID(SERVICE_01, EMISSION_REQUIREMENTS)) -// return conditionResponse(findResponse(), 1); -// return ELM_GENERAL_ERROR; -// } +/* + uint8_t ELM327::emissionRqmts() + Description: + ------------ + * Find emission requirements to which vehicle is designed -// /* -// uint32_t ELM327::supportedPIDs_61_80() + Inputs: + ------- + * void -// Description: -// ------------ -// * Determine which of PIDs 0x61 through 0x80 are supported (bit encoded) + Return: + ------- + * uint8_t - Bit encoded (?) +*/ +uint8_t ELM327::emissionRqmts() +{ + return (uint8_t)processPID(SERVICE_01, EMISSION_REQUIREMENTS, 1, 1); +} -// Inputs: -// ------- -// * void -// Return: -// ------- -// * uint32_t - Bit encoded booleans of supported PIDs 0x61-0x80 -// */ -// uint32_t ELM327::supportedPIDs_61_80() -// { -// if (queryPID(SERVICE_01, SUPPORTED_PIDS_61_80)) -// return conditionResponse(findResponse(), 4); -// return ELM_GENERAL_ERROR; -// } +/* + uint32_t ELM327::supportedPIDs_61_80() + Description: + ------------ + * Determine which of PIDs 0x61 through 0x80 are supported (bit encoded) + Inputs: + ------- + * void -// /* -// float ELM327::demandedTorque() + Return: + ------- + * uint32_t - Bit encoded booleans of supported PIDs 0x61-0x80 +*/ +uint32_t ELM327::supportedPIDs_61_80() +{ + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_61_80, 1, 4); +} -// Description: -// ------------ -// * Find driver's demanded engine torque in % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Driver's demanded engine torque in % -// */ -// float ELM327::demandedTorque() -// { -// if (queryPID(SERVICE_01, DEMANDED_ENGINE_PERCENT_TORQUE)) -// return conditionResponse(findResponse(), 1, 1, -125.0); -// return ELM_GENERAL_ERROR; -// } +/* + float ELM327::demandedTorque() + Description: + ------------ + * Find driver's demanded engine torque in % + Inputs: + ------- + * void + Return: + ------- + * float - Driver's demanded engine torque in % +*/ +float ELM327::demandedTorque() +{ + return processPID(SERVICE_01, DEMANDED_ENGINE_PERCENT_TORQUE, 1, 1, 1, -125.0); +} -// /* -// float ELM327::torque() -// Description: -// ------------ -// * Find actual engine torque in % -// Inputs: -// ------- -// * void -// Return: -// ------- -// * float - Actual engine torque in % -// */ -// float ELM327::torque() -// { -// if (queryPID(SERVICE_01, ACTUAL_ENGINE_TORQUE)) -// return conditionResponse(findResponse(), 1, 1, -125.0); +/* + float ELM327::torque() -// return ELM_GENERAL_ERROR; -// } + Description: + ------------ + * Find actual engine torque in % + Inputs: + ------- + * void + Return: + ------- + * float - Actual engine torque in % +*/ +float ELM327::torque() +{ + return processPID(SERVICE_01, ACTUAL_ENGINE_TORQUE, 1, 1, 1, -125.0); +} -// /* -// uint16_t ELM327::referenceTorque() -// Description: -// ------------ -// * Find engine reference torque in Nm -// Inputs: -// ------- -// * void +/* + uint16_t ELM327::referenceTorque() -// Return: -// ------- -// * uint16_t - Engine reference torque in Nm -// */ -// uint16_t ELM327::referenceTorque() -// { -// if (queryPID(SERVICE_01, ENGINE_REFERENCE_TORQUE)) -// return conditionResponse(findResponse(), 2); + Description: + ------------ + * Find engine reference torque in Nm -// return ELM_GENERAL_ERROR; -// } + Inputs: + ------- + * void + Return: + ------- + * uint16_t - Engine reference torque in Nm +*/ +uint16_t ELM327::referenceTorque() +{ + return processPID(SERVICE_01, ENGINE_REFERENCE_TORQUE, 1, 2); +} -// /* -// uint16_t ELM327::auxSupported() -// Description: -// ------------ -// * Find auxiliary input/output supported +/* + uint16_t ELM327::auxSupported() -// Inputs: -// ------- -// * void + Description: + ------------ + * Find auxiliary input/output supported -// Return: -// ------- -// * uint16_t - Bit encoded (?) -// */ -// uint16_t ELM327::auxSupported() -// { -// if (queryPID(SERVICE_01, AUX_INPUT_OUTPUT_SUPPORTED)) -// return conditionResponse(findResponse(), 2); + Inputs: + ------- + * void -// return ELM_GENERAL_ERROR; -// } + Return: + ------- + * uint16_t - Bit encoded (?) +*/ +uint16_t ELM327::auxSupported() +{ + return (uint16_t)processPID(SERVICE_01, AUX_INPUT_OUTPUT_SUPPORTED, 1, 2); +} diff --git a/src/ELMduino.h b/src/ELMduino.h index 5266892..7cb3c68 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -311,8 +311,9 @@ class ELM327 bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); void flushInputBuff(); uint64_t findResponse(); - bool queryPID(uint8_t service, uint16_t pid, uint8_t num_responses); + bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); bool queryPID(char queryStr[]); + float processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); void sendCommand(const char *cmd); int8_t sendCommand_Blocking(const char *cmd); int8_t get_response(); From 706a00b8545f118ed82e538173516a4e61bd699d Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Mon, 3 Jan 2022 19:55:34 +1030 Subject: [PATCH 14/88] fix comment typos. Add VIN to test multiline response --- src/ELMduino.cpp | 92 +++++++++++++++++++++++++++++++++++++++++------- src/ELMduino.h | 1 + 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 0c8d0e9..a1464ec 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -490,7 +490,7 @@ bool ELM327::queryPID(char queryStr[]) Return: ------- - * bool - Whether or not the query was submitted successfully + * float - The PID value if successfully received, else 0.0 */ float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) { @@ -2247,8 +2247,8 @@ void ELM327::sendCommand(const char *cmd) Description: ------------ - * Sends a command/query for Non-Blocking PID queries. - Sometimes it's ok to use a blocking command, e.g when sending an AT command. + * Sends a command/query and waits for a respoonse (blocking function) + Sometimes it's desirable to use a blocking command, e.g when sending an AT command. This function removes the need for the caller to set up a loop waiting for the command to finish. Caller is free to parse the payload string if they need to use the response. @@ -2258,17 +2258,13 @@ void ELM327::sendCommand(const char *cmd) Return: ------- - * obd_rx_states - the status of the command + * int8_t - the ELM_XXX status of getting the OBD response */ int8_t ELM327::sendCommand_Blocking(const char *cmd) { - int8_t receive_status; sendCommand(cmd); - do - { - receive_status = get_response(); - } while (receive_status == ELM_GETTING_MSG); - return receive_status; + while (get_response() == ELM_GETTING_MSG); + return nb_rx_state; } @@ -2279,7 +2275,7 @@ int8_t ELM327::sendCommand_Blocking(const char *cmd) Description: ------------ - * Non Blocking (NB) receive OBD scanner response. Must be called in repeatedly until + * Non Blocking (NB) receive OBD scanner response. Must be called repeatedly until the status progresses past ELM_GETTING_MSG. Inputs: @@ -2288,7 +2284,7 @@ int8_t ELM327::sendCommand_Blocking(const char *cmd) Return: ------- - * obd_rx_states - the status of getting the response + * int8_t - the ELM_XXX status of getting the OBD response */ int8_t ELM327::get_response(void) { @@ -2642,4 +2638,74 @@ float ELM327::batteryVoltage(void) nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command } return 0.0; -} \ No newline at end of file +} + + + + +/* + int8_t ELM327::get_vin_blocking(char *vin) + + Description: + ------------ + * Read Vehicle Identification Number (VIN). This is a blocking function. + + Inputs: + ------- + * char vin[] - pointer to c-string in which to store VIN + Note: (allocate memory for 18 character c-string in calling function) + + Return: + ------- + * int8_t - the ELM_XXX status of getting the VIN +*/ +int8_t ELM327::get_vin_blocking(char vin[]) +{ + char temp[3] = {0}; + char *idx; + uint8_t vin_counter = 0; + uint8_t ascii_val; + uint8_t num_vin_chars; + + Serial.println("Getting VIN..."); + sendCommand("0902"); // VIN is command 0902 + while (get_response() == ELM_GETTING_MSG) {} + + // strcpy(payload, "0140:4902013144341:475030305235352:42313233343536"); + if (nb_rx_state == ELM_SUCCESS) + { + memset(vin, 0, 18); + if (debugMode) + Serial.printf("VIN response: %s, bytes received=%d\n", payload, recBytes); + + // **** Decoding **** + if (strstr(payload, "490201")) + { + // payload = "0140:4902013144341:475030305235352:42313233343536" ==> VIN="1D4GP00R55B123456" (17-chars) + // 014 ==> 0x14 = 20 bytes following + // 0: 49 02 01 31 44 34 ==> 49 02 = Header. 01 = 1 VIN number in message. 31, 44, 34 = First 3 VIN bytes + // 1: 47 50 30 30 52 35 35 ==> 47->35 next 7 VIN bytes + // 2: 42 31 32 33 34 35 36 ==> 42->36 next 7 VIN bytes + idx = strstr(payload, "490201") + 6; // Pointer to first VIN digit + // Adjust recBytes == 49 for multiline row headers (line_no + semicolon) and 2 chars/digit + // i.e. 2 * (49 - 30) = 2 * 19 = 38 + num_vin_chars = 2 * (recBytes - 30); + for (int i = 0; i < num_vin_chars; i += 2) { + temp[0] = *(idx + i); // Get first digit of ASCII code + temp[1] = *(idx + i + 1); // Get second digit of ASCII code + // No need to add string termination, temp[3] always == 0 + if (strstr(temp, ":")) continue; + ascii_val = strtol(temp, 0, 16); // Convert ASCII code to integer + sprintf(vin + vin_counter++, "%c", ascii_val); // Convert ASCII code integer back to character + // Serial.printf("Chars %s, ascii_val=%d[dec] 0x%02hhx[hex] ==> VIN=%s\n", temp, ascii_val, ascii_val, vin); + } + } + Serial.printf("VIN=%s\n", vin); + } + else + { + Serial.println("No VIN response"); + printError(); + } + return nb_rx_state; +} diff --git a/src/ELMduino.h b/src/ELMduino.h index 7cb3c68..0cbd360 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -321,6 +321,7 @@ class ELM327 float conditionResponse(const uint64_t& response, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); float batteryVoltage(void); + int8_t get_vin_blocking(char vin[]); uint32_t supportedPIDs_1_20(); From 8a4a8de1471e0ad99b32450a22d3c2119e68717d Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Mon, 3 Jan 2022 20:35:11 +1030 Subject: [PATCH 15/88] update examples for new API --- .../Arduino_hardware_serial_test.ino | 4 +- .../Arduino_software_serial_test.ino | 4 +- .../ESP32_Bluetooth_Serial.ino | 4 +- .../non blocking M5Stack Core2.ino | 253 ------------------ examples/ESP32_WiFi/ESP32_WiFi.ino | 4 +- examples/ESP32_dual_core/ESP32_dual_core.ino | 2 +- examples/ESP8266_WiFi/ESP8266_WiFi.ino | 4 +- .../non_blocking_1/non blocking example 1.ino | 55 ---- .../non_blocking_2/non blocking example 2.ino | 127 --------- 9 files changed, 11 insertions(+), 446 deletions(-) delete mode 100644 examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino delete mode 100644 examples/non_blocking_1/non blocking example 1.ino delete mode 100644 examples/non_blocking_2/non blocking example 2.ino diff --git a/examples/Arduino_hardware_serial_test/Arduino_hardware_serial_test.ino b/examples/Arduino_hardware_serial_test/Arduino_hardware_serial_test.ino index 559dbe8..733b0aa 100644 --- a/examples/Arduino_hardware_serial_test/Arduino_hardware_serial_test.ino +++ b/examples/Arduino_hardware_serial_test/Arduino_hardware_serial_test.ino @@ -36,11 +36,11 @@ void loop() { float tempRPM = myELM327.rpm(); - if (myELM327.status == ELM_SUCCESS) + if (myELM327.nb_rx_state == ELM_SUCCESS) { rpm = (uint32_t)tempRPM; Serial.print("RPM: "); Serial.println(rpm); } - else + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) myELM327.printError(); } diff --git a/examples/Arduino_software_serial_test/Arduino_software_serial_test.ino b/examples/Arduino_software_serial_test/Arduino_software_serial_test.ino index 190a243..30f42e4 100644 --- a/examples/Arduino_software_serial_test/Arduino_software_serial_test.ino +++ b/examples/Arduino_software_serial_test/Arduino_software_serial_test.ino @@ -38,11 +38,11 @@ void loop() { float tempRPM = myELM327.rpm(); - if (myELM327.status == ELM_SUCCESS) + if (myELM327.nb_rx_state == ELM_SUCCESS) { rpm = (uint32_t)tempRPM; Serial.print("RPM: "); Serial.println(rpm); } - else + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) myELM327.printError(); } diff --git a/examples/ESP32_Bluetooth_Serial/ESP32_Bluetooth_Serial.ino b/examples/ESP32_Bluetooth_Serial/ESP32_Bluetooth_Serial.ino index a19ec93..5dd0ecb 100644 --- a/examples/ESP32_Bluetooth_Serial/ESP32_Bluetooth_Serial.ino +++ b/examples/ESP32_Bluetooth_Serial/ESP32_Bluetooth_Serial.ino @@ -44,11 +44,11 @@ void loop() { float tempRPM = myELM327.rpm(); - if (myELM327.status == ELM_SUCCESS) + if (myELM327.nb_rx_state == ELM_SUCCESS) { rpm = (uint32_t)tempRPM; Serial.print("RPM: "); Serial.println(rpm); } - else + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) myELM327.printError(); } diff --git a/examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino b/examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino deleted file mode 100644 index 267025c..0000000 --- a/examples/ESP32_M5Stack_Core2_non_block/non blocking M5Stack Core2.ino +++ /dev/null @@ -1,253 +0,0 @@ -#include -#include - -#include "BluetoothSerial.h" - -ELM327 myELM327; -BluetoothSerial SerialBT; - -char txt[80] = ""; - -// #define USE_MAC_NAME // Comment this line out to specify OBDII scanner bluetooth mac address. Uncomment to specify bluetooth name -#if defined USE_MAC_NAME -char bt_name[] = "OBDLink LX"; -// char bt_name[] = "OBDII"; -// char bt_name[] = "V-LINK"; -#else -// char bluetooth_mac_str[] = "00:1D:A5:68:98:8C"; // EM327 mini clone Blue -// char bluetooth_mac_str[] = "00:1D:A5:22:69:9F"; // VGate iCar Pro black and gold -char bluetooth_mac_str[] = "00:1d:a5:22:5f:f6"; // VGate iCar Pro black and gold -// char bluetooth_mac_str[] = "00:04:3E:53:5E:0D"; -// char bluetooth_mac_str[] = "00:04:3E:53:5E:0D"; // OBDLink LX green -uint8_t bt_mac[6] = {0x00}; -#endif -char pin[] = "1234"; - -typedef enum { IDLE, - BARO_PRESS, - COOLANT_TEMP, - OIL_TEMP, - MANIFOLD_PRESS, - ENG_RPM, - BATT_VOLT } obd_pid_states; - -obd_pid_states obd_state = IDLE; -float coolant_temperature = 0.0; -float battery_voltage = 0.0; -float eng_rpm = 0.0; -uint32_t slow_data_last_time = millis(); -uint32_t fast_data_last_time = millis(); - -void setup() { - auto cfg = M5.config(); - cfg.serial_baudrate = 115200; // default=115200. if "Serial" is not needed, set it to 0. - cfg.clear_display = true; // default=true. clear the screen when begin. - cfg.output_power = false; // default=true. use external port 5V output. - cfg.internal_imu = false; // default=true. use internal IMU. - cfg.internal_rtc = true; // default=true. use internal RTC. - cfg.external_imu = false; // default=false. use Unit Accel & Gyro. - cfg.external_rtc = false; // default=false. use Unit RTC. - cfg.led_brightness = 0; // default= 0. Green LED brightness (0=off / 255=max) (※ not NeoPixel) - - M5.begin(cfg); - M5.Lcd.setBrightness((70 * 255) / 100); // Set LCD brightness to 70% of maximum - - M5.Lcd.setFont(&fonts::FreeSans12pt7b); - M5.Lcd.setTextDatum(top_left); - M5.Lcd.setTextPadding(0); - M5.Lcd.setTextColor(TFT_GREEN, TFT_BLACK); - - uint16_t xx = 0; - uint16_t yy = 10; - M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); - sprintf(txt, "%s", "Starting ESP32 BT"); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - - // Start ESP32 bluetooth - SerialBT.begin("ELM_NonBlock", true); - - M5.Lcd.setTextPadding(M5.Lcd.textWidth(txt) + 10); - M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); - sprintf(txt, "%s", "BT Started"); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - - yy += M5.Lcd.fontHeight(); - M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); - M5.Lcd.setFont(&fonts::FreeSans9pt7b); - sprintf(txt, "%s", "Trying to connect OBDII scanner"); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - - yy += M5.Lcd.fontHeight(); - M5.Lcd.setTextColor(TFT_GREEN, TFT_BLACK); - bool bt_connected = false; -// Attempt to connect ESP32 to bluetooth OBDII scanner -// SerialBT.setPin(pin); -#if defined USE_MAC_NAME - sprintf(txt, "%s = %s", "BT Name", bt_name); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - bt_connected = SerialBT.connect(bt_name); -#else - // Convert bluetooth MAC address string into integer array[6] - sscanf(bluetooth_mac_str, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", - bt_mac, bt_mac + 1, bt_mac + 2, - bt_mac + 3, bt_mac + 4, bt_mac + 5); - sprintf(txt, "%s = %s", "MAC address", bluetooth_mac_str); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - bt_connected = SerialBT.connect(bt_mac); -#endif - - yy += M5.Lcd.fontHeight(); - - if (!bt_connected) { - sprintf(txt, "%s", "Can't connect OBD - Phase 1"); - Serial.println(txt); - M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); - M5.Lcd.drawString(txt, xx, yy); - while (1) - ; - } - - M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); - sprintf(txt, "%s", "Phase 1 ok"); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - - yy += M5.Lcd.fontHeight(); - M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); - sprintf(txt, "%s", "Trying to start ELM327"); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - - bt_connected = myELM327.begin(SerialBT, true, 2000); - - yy += M5.Lcd.fontHeight(); - - if (!bt_connected) { - sprintf(txt, "%s", "Can't connect OBD - Phase 2"); - Serial.println(txt); - M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); - M5.Lcd.drawString(txt, xx, yy); - - while (1) - ; - } - - M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); - sprintf(txt, "%s", "Phase 2 ok"); - Serial.println(txt); - M5.Lcd.drawString(txt, xx, yy); - - delay(2000); - - M5.Lcd.clear(); - - // Label for Coolant Temperature - xx = 0; - M5.Lcd.setFont(&fonts::FreeSans12pt7b); - M5.Lcd.setTextDatum(top_left); - M5.Lcd.setTextPadding(100); - M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); - M5.Lcd.drawString("Coolant Temp:", xx, 10); - - // Label for RPM - M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); - M5.Lcd.drawString("RPM:", xx, 50); - - // Label for battery voltage - M5.Lcd.setTextColor(TFT_ORANGE, TFT_BLACK); - M5.Lcd.drawString("Battery:", xx, 90); - -} - -void loop() { - uint16_t xx = 180; - uint16_t yy = 10; - - switch (obd_state) { - case IDLE: - if (millis() > slow_data_last_time + 5000) { - slow_data_last_time = millis(); - obd_state = BATT_VOLT; - Serial.printf("IDLE, query SLOW PIDs at %.3f s\n", slow_data_last_time / 1000.0); - } else if (millis() > fast_data_last_time + 1000) { - fast_data_last_time = millis(); - obd_state = COOLANT_TEMP; - Serial.printf("IDLE, query FAST PIDs at %.3f s\n", fast_data_last_time / 1000.0); - } else { - // Serial.printf("[%.3f s] IDLE\n", millis() / 1000.0); - } - break; - - case BARO_PRESS: - break; - - case COOLANT_TEMP: - coolant_temperature = myELM327.engineCoolantTemp(); - yy = 10; - if (myELM327.nb_rx_state == ELM_SUCCESS) { - // Display coolant temperature - M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); - sprintf(txt, "%.0f deg", coolant_temperature); - M5.Lcd.drawString(txt, xx, yy); - Serial.printf("Coolant temp = %s\n", txt); - obd_state = ENG_RPM; - } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { - myELM327.printError(); - M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); - sprintf(txt, "%s", "N/A deg"); - M5.Lcd.drawString(txt, xx, yy); - obd_state = ENG_RPM; - } - break; - - case OIL_TEMP: - break; - - case MANIFOLD_PRESS: - break; - - case ENG_RPM: - eng_rpm = myELM327.rpm(); - yy = 50; - if (myELM327.nb_rx_state == ELM_SUCCESS) { - // Display coolant temperature - M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); - sprintf(txt, "%.0f RPM", eng_rpm); - M5.Lcd.drawString(txt, xx, yy); - Serial.printf("Engine RPM = %s\n", txt); - obd_state = IDLE; - } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { - myELM327.printError(); - M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); - sprintf(txt, "%s", "N/A RPM"); - M5.Lcd.drawString(txt, xx, yy); - obd_state = IDLE; - } - break; - - case BATT_VOLT: - battery_voltage = myELM327.batteryVoltage(); - yy = 90; - if (myELM327.nb_rx_state == ELM_SUCCESS) { - // Display vehicle battery voltage - M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK); - sprintf(txt, "%.1f VDC", battery_voltage); - M5.Lcd.drawString(txt, xx, yy); - Serial.printf("Battery voltage = %s\n", txt); - - obd_state = IDLE; - } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { - myELM327.printError(); - M5.Lcd.setTextColor(TFT_RED, TFT_BLACK); - sprintf(txt, "%s", "N/A VDC"); - M5.Lcd.drawString(txt, xx, yy); - obd_state = IDLE; - } - break; - } -} diff --git a/examples/ESP32_WiFi/ESP32_WiFi.ino b/examples/ESP32_WiFi/ESP32_WiFi.ino index 705665f..7e5592c 100644 --- a/examples/ESP32_WiFi/ESP32_WiFi.ino +++ b/examples/ESP32_WiFi/ESP32_WiFi.ino @@ -54,11 +54,11 @@ void loop() { float tempRPM = myELM327.rpm(); - if (myELM327.status == ELM_SUCCESS) + if (myELM327.nb_rx_state == ELM_SUCCESS) { rpm = (uint32_t)tempRPM; Serial.print("RPM: "); Serial.println(rpm); } - else + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) myELM327.printError(); } diff --git a/examples/ESP32_dual_core/ESP32_dual_core.ino b/examples/ESP32_dual_core/ESP32_dual_core.ino index 16a37c5..6b4f253 100644 --- a/examples/ESP32_dual_core/ESP32_dual_core.ino +++ b/examples/ESP32_dual_core/ESP32_dual_core.ino @@ -136,7 +136,7 @@ void read_rpm() Serial.print("RPM: "); Serial.println(rpm); previous_rpm_time = current_time; - if (obd.status != ELM_SUCCESS) + if ((obd.nb_rx_state != ELM_GETTING_MSG && obd.nb_rx_state != ELM_SUCCESS) { obd.printError(); } diff --git a/examples/ESP8266_WiFi/ESP8266_WiFi.ino b/examples/ESP8266_WiFi/ESP8266_WiFi.ino index 418f4fd..f29989b 100644 --- a/examples/ESP8266_WiFi/ESP8266_WiFi.ino +++ b/examples/ESP8266_WiFi/ESP8266_WiFi.ino @@ -54,11 +54,11 @@ void loop() { float tempRPM = myELM327.rpm(); - if (myELM327.status == ELM_SUCCESS) + if (myELM327.nb_rx_state == ELM_SUCCESS) { rpm = (uint32_t)tempRPM; Serial.print("RPM: "); Serial.println(rpm); } - else + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) myELM327.printError(); } diff --git a/examples/non_blocking_1/non blocking example 1.ino b/examples/non_blocking_1/non blocking example 1.ino deleted file mode 100644 index 7753bdf..0000000 --- a/examples/non_blocking_1/non blocking example 1.ino +++ /dev/null @@ -1,55 +0,0 @@ -#include "BluetoothSerial.h" -#include "ELMduino.h" - - -BluetoothSerial SerialBT; -#define ELM_PORT SerialBT -#define DEBUG_PORT Serial - - -ELM327 myELM327; - - -uint32_t rpm = 0; - -uint8_t bt_mac[6] = {0x00, 0x1d, 0xa5, 0x22, 0x5f, 0xf6}; - -void setup() -{ -#if LED_BUILTIN - pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, LOW); -#endif - - DEBUG_PORT.begin(115200); - // SerialBT.setPin("1234"); - ELM_PORT.begin("ArduHUD", true); - - if (!ELM_PORT.connect(bt_mac)) - { - DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); - while(1); - } - - if (!myELM327.begin(ELM_PORT, true, 2000)) - { - Serial.println("Couldn't connect to OBD scanner - Phase 2"); - while (1); - } - - Serial.println("Connected to ELM327"); -} - - -void loop() -{ - float tempRPM = myELM327.rpm(); - - if (myELM327.nb_rx_state == ELM_SUCCESS) - { - rpm = (uint32_t)tempRPM; - Serial.print("RPM: "); Serial.println(rpm); - } - else if (myELM327.nb_rx_state != ELM_GETTING_MSG) - myELM327.printError(); -} diff --git a/examples/non_blocking_2/non blocking example 2.ino b/examples/non_blocking_2/non blocking example 2.ino deleted file mode 100644 index aa783f9..0000000 --- a/examples/non_blocking_2/non blocking example 2.ino +++ /dev/null @@ -1,127 +0,0 @@ -#include -#include "BluetoothSerial.h" - -ELM327 myELM327; -BluetoothSerial SerialBT; - -char txt[80] = ""; - -// #define USE_MAC_NAME // Comment this line out to specify OBDII scanner bluetooth mac address. Uncomment to specify bluetooth name -#if defined USE_MAC_NAME -char bt_name[] = "OBDII"; -#else -char bluetooth_mac_str[] = "00:1d:a5:22:5f:f6"; // VGate iCar Pro black and gold -uint8_t bt_mac[6] = {0x00}; -#endif -char pin[] = "1234"; - -typedef enum { IDLE, - COOLANT_TEMP, - OIL_TEMP, - ENG_RPM, - BATT_VOLT } obd_pid_states; - -obd_pid_states obd_state = IDLE; -float coolant_temperature = 0.0; -float battery_voltage = 0.0; -float eng_rpm = 0.0; -uint32_t slow_data_last_time = millis(); -uint32_t fast_data_last_time = millis(); - -void setup() { - Serial.begin(115200); - Serial.println(F("Starting ESP32 BT")); - - // Start ESP32 bluetooth - SerialBT.begin("ELM_NonBlock", true); - - Serial.println(F("BT Started")); - Serial.println(F("Trying to connect OBDII scanner")); - - bool bt_connected = false; -// Attempt to connect ESP32 to bluetooth OBDII scanner -// SerialBT.setPin(pin); -#if defined USE_MAC_NAME - Serial.printf("%s = %s\n", "BT Name", bt_name); - bt_connected = SerialBT.connect(bt_name); -#else - // Convert bluetooth MAC address string into integer array[6] - sscanf(bluetooth_mac_str, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", - bt_mac, bt_mac + 1, bt_mac + 2, - bt_mac + 3, bt_mac + 4, bt_mac + 5); - Serial.printf("%s = %s\n", "MAC address", bluetooth_mac_str); - bt_connected = SerialBT.connect(bt_mac); -#endif - - if (!bt_connected) { - Serial.println(F("Couldn't connect to OBD scanner - Phase 1")); - while (1); - } - - Serial.println(F("Phase 1 ok. Trying to start ELM327")); - bt_connected = myELM327.begin(SerialBT, true, 2000); - - if (!bt_connected) { - Serial.println("Couldn't connect to OBD scanner - Phase 2"); - while (1); - } - - Serial.println(F("Phase 2 ok")); -} - -void loop() { - switch (obd_state) { - case IDLE: - // Schedule PIDs to be read at different rates (slow PIDs and fast PIDs) - if (millis() > slow_data_last_time + 5000) { - slow_data_last_time = millis(); - obd_state = BATT_VOLT; - Serial.printf("IDLE, query SLOW PIDs at %.3f s\n", slow_data_last_time / 1000.0); - } else if (millis() > fast_data_last_time + 1000) { - fast_data_last_time = millis(); - obd_state = COOLANT_TEMP; // Specify the first slow PID here - Serial.printf("IDLE, query FAST PIDs at %.3f s\n", fast_data_last_time / 1000.0); - } else { - // Serial.printf("[%.3f s] IDLE\n", millis() / 1000.0); - } - break; - - case COOLANT_TEMP: - coolant_temperature = myELM327.engineCoolantTemp(); - if (myELM327.nb_rx_state == ELM_SUCCESS) { - Serial.printf("Coolant temp = %.0f\n", coolant_temperature); - obd_state = OIL_TEMP; - } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { - myELM327.printError(); - obd_state = OIL_TEMP; - } - break; - - case OIL_TEMP: - // Not yet implemented - obd_state = ENG_RPM; - break; - - case ENG_RPM: - eng_rpm = myELM327.rpm(); - if (myELM327.nb_rx_state == ELM_SUCCESS) { - Serial.printf("Engine RPM = %.0f\n", eng_rpm); - obd_state = IDLE; - } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { - myELM327.printError(); - obd_state = IDLE; - } - break; - - case BATT_VOLT: - battery_voltage = myELM327.batteryVoltage(); - if (myELM327.nb_rx_state == ELM_SUCCESS) { - Serial.printf("Battery voltage = %.1f\n", battery_voltage); - obd_state = IDLE; - } else if (myELM327.nb_rx_state != ELM_GETTING_MSG) { - myELM327.printError(); - obd_state = IDLE; - } - break; - } -} From 8fd1213621f3c4d95d018149249d415e01fea41c Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Mon, 3 Jan 2022 21:11:40 +1030 Subject: [PATCH 16/88] update the readme with concept of execution --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9bae62..20c2644 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Install ELMduino using the Arduino IDE's Libraries Manager (search "ELMduino.h") # Note: If you're having difficulty in connecting/keeping connection to your ELM327, try using 38400 baud instead of 115200. If you still have trouble, try all other possible bauds. Lastly, if using BluetoothSerial on an ESP32, try using the ELM327's MAC address instead of the device name "OBDII" and [remove paired devices using this sketch](https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/bt_remove_paired_devices/bt_remove_paired_devices.ino). +# Concept of Execution +The library is non-blocking. This means when you query a PID e.g. `myELM327.rpm()`, the code does not wait around for the response, which would block your other code in the main loop from executing. With ELMDuino, your main loop can do other tasks. To make this work, you need to repeatedly call the PID query funtion and check the non-blocking receive state (`myELM327.nb_rx_state`) until it is equal to `ELM_SUCCESS`. If the status is not ELM_SUCCESS, the library could still be waiting for a response to be received. This is indicated by `myELM327.nb_rx_state` being equal to `ELM_GETTING_MSG`. If the status is not equal to either of these values (ELM_SUCCESS or ELM_GETTING_MSG), it indicates an error has occurred. You can call `.printError()` to check what the problem was. See the simple example below which queries the engine speed in RPM. + # Example Code: ```C++ #include @@ -19,7 +22,7 @@ If you're having difficulty in connecting/keeping connection to your ELM327, try SoftwareSerial mySerial(2, 3); // RX, TX -ELM327 myELM327; +ELM327 myELM327; // Create the ELMDuino object. You change myELM327 to anything else you like uint32_t rpm = 0; @@ -37,11 +40,13 @@ void loop() { float tempRPM = myELM327.rpm(); - if (myELM327.status == ELM_SUCCESS) + if (myELM327.nb_rx_state == ELM_SUCCESS) { rpm = (uint32_t)tempRPM; Serial.print("RPM: "); Serial.println(rpm); } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + myELM327.printError(); } ``` @@ -132,6 +137,12 @@ uint16_t referenceTorque(); uint16_t auxSupported(); ``` +# Other commands +```C++ +float batteryVoltage(void); // Get's vehicle battery voltage +int8_t get_vin_blocking(char vin[]); // Get's Vehicle Identification Number (VIN) +``` + # List of OBD Protocols: ```C++ const char AUTOMATIC = '0'; From d1040f23e9672405abd656a62770a2ece457223a Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Mon, 3 Jan 2022 21:16:00 +1030 Subject: [PATCH 17/88] fix typos --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 20c2644..cfead2d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Install ELMduino using the Arduino IDE's Libraries Manager (search "ELMduino.h") If you're having difficulty in connecting/keeping connection to your ELM327, try using 38400 baud instead of 115200. If you still have trouble, try all other possible bauds. Lastly, if using BluetoothSerial on an ESP32, try using the ELM327's MAC address instead of the device name "OBDII" and [remove paired devices using this sketch](https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/bt_remove_paired_devices/bt_remove_paired_devices.ino). # Concept of Execution -The library is non-blocking. This means when you query a PID e.g. `myELM327.rpm()`, the code does not wait around for the response, which would block your other code in the main loop from executing. With ELMDuino, your main loop can do other tasks. To make this work, you need to repeatedly call the PID query funtion and check the non-blocking receive state (`myELM327.nb_rx_state`) until it is equal to `ELM_SUCCESS`. If the status is not ELM_SUCCESS, the library could still be waiting for a response to be received. This is indicated by `myELM327.nb_rx_state` being equal to `ELM_GETTING_MSG`. If the status is not equal to either of these values (ELM_SUCCESS or ELM_GETTING_MSG), it indicates an error has occurred. You can call `.printError()` to check what the problem was. See the simple example below which queries the engine speed in RPM. +The library is non-blocking. This means when you query a PID e.g. `myELM327.rpm()`, the code does not wait around for the response, which would block your other code in the main loop from executing. With ELMDuino, your main loop can continue do other tasks. To make this work, you need to repeatedly call the PID query function and check the non-blocking receive state (`myELM327.nb_rx_state`) until it is equal to `ELM_SUCCESS`. If the status is not `ELM_SUCCESS`, the library could still be waiting for a response to be received. This is indicated by `myELM327.nb_rx_state` being equal to `ELM_GETTING_MSG`. If the status is not equal to either of these values (ELM_SUCCESS or ELM_GETTING_MSG), it indicates an error has occurred. You can call `myELM327.printError()` to check what the problem was. See the simple example below which queries the engine speed in RPM. # Example Code: ```C++ @@ -139,8 +139,8 @@ uint16_t auxSupported(); # Other commands ```C++ -float batteryVoltage(void); // Get's vehicle battery voltage -int8_t get_vin_blocking(char vin[]); // Get's Vehicle Identification Number (VIN) +float batteryVoltage(void); // Gets vehicle battery voltage +int8_t get_vin_blocking(char vin[]); // Gets Vehicle Identification Number (VIN) ``` # List of OBD Protocols: From 2ccd9be0a9b918154daf020f5136301d21449305 Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Tue, 4 Jan 2022 07:52:58 +1030 Subject: [PATCH 18/88] address review comments on VIN --- README.md | 4 +++- src/ELMduino.cpp | 47 ++++++++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index cfead2d..2745ea6 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ If you're having difficulty in connecting/keeping connection to your ELM327, try # Concept of Execution The library is non-blocking. This means when you query a PID e.g. `myELM327.rpm()`, the code does not wait around for the response, which would block your other code in the main loop from executing. With ELMDuino, your main loop can continue do other tasks. To make this work, you need to repeatedly call the PID query function and check the non-blocking receive state (`myELM327.nb_rx_state`) until it is equal to `ELM_SUCCESS`. If the status is not `ELM_SUCCESS`, the library could still be waiting for a response to be received. This is indicated by `myELM327.nb_rx_state` being equal to `ELM_GETTING_MSG`. If the status is not equal to either of these values (ELM_SUCCESS or ELM_GETTING_MSG), it indicates an error has occurred. You can call `myELM327.printError()` to check what the problem was. See the simple example below which queries the engine speed in RPM. +Just to be clear, do not try to query more than one PID at a time. You must wait for the current PID query to complete before starting the next one. + # Example Code: ```C++ #include @@ -22,7 +24,7 @@ The library is non-blocking. This means when you query a PID e.g. `myELM327.rpm( SoftwareSerial mySerial(2, 3); // RX, TX -ELM327 myELM327; // Create the ELMDuino object. You change myELM327 to anything else you like +ELM327 myELM327; uint32_t rpm = 0; diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index a1464ec..c9be5c3 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2665,47 +2665,52 @@ int8_t ELM327::get_vin_blocking(char vin[]) char *idx; uint8_t vin_counter = 0; uint8_t ascii_val; - uint8_t num_vin_chars; - Serial.println("Getting VIN..."); + if (debugMode) + Serial.println("Getting VIN..."); sendCommand("0902"); // VIN is command 0902 - while (get_response() == ELM_GETTING_MSG) {} + while (get_response() == ELM_GETTING_MSG); // strcpy(payload, "0140:4902013144341:475030305235352:42313233343536"); if (nb_rx_state == ELM_SUCCESS) { memset(vin, 0, 18); - if (debugMode) - Serial.printf("VIN response: %s, bytes received=%d\n", payload, recBytes); - // **** Decoding **** if (strstr(payload, "490201")) { - // payload = "0140:4902013144341:475030305235352:42313233343536" ==> VIN="1D4GP00R55B123456" (17-chars) + // OBD scanner provides this multiline response: // 014 ==> 0x14 = 20 bytes following - // 0: 49 02 01 31 44 34 ==> 49 02 = Header. 01 = 1 VIN number in message. 31, 44, 34 = First 3 VIN bytes - // 1: 47 50 30 30 52 35 35 ==> 47->35 next 7 VIN bytes - // 2: 42 31 32 33 34 35 36 ==> 42->36 next 7 VIN bytes - idx = strstr(payload, "490201") + 6; // Pointer to first VIN digit - // Adjust recBytes == 49 for multiline row headers (line_no + semicolon) and 2 chars/digit - // i.e. 2 * (49 - 30) = 2 * 19 = 38 - num_vin_chars = 2 * (recBytes - 30); - for (int i = 0; i < num_vin_chars; i += 2) { + // 0: 49 02 01 31 44 34 ==> 49 02 = Header. 01 = 1 VIN number in message. 31, 44, 34 = First 3 VIN digits + // 1: 47 50 30 30 52 35 35 ==> 47->35 next 7 VIN digits + // 2: 42 31 32 33 34 35 36 ==> 42->36 next 7 VIN digits + // + // The resulitng payload buffer is: + // "0140:4902013144341:475030305235352:42313233343536" ==> VIN="1D4GP00R55B123456" (17-digits) + idx = strstr(payload, "490201") + 6; // Pointer to first ASCII code digit of first VIN digit + // Loop over each pair of ASCII code digits. 17 VIN digits + 2 skipped line numbers = 19 loops + for (int i = 0; i < (19 * 2); i += 2) { temp[0] = *(idx + i); // Get first digit of ASCII code temp[1] = *(idx + i + 1); // Get second digit of ASCII code - // No need to add string termination, temp[3] always == 0 - if (strstr(temp, ":")) continue; + // No need to add string termination, temp[3] always == 0 + if (strstr(temp, ":")) continue; // Skip the second "1:" and third "2:" line numbers ascii_val = strtol(temp, 0, 16); // Convert ASCII code to integer sprintf(vin + vin_counter++, "%c", ascii_val); // Convert ASCII code integer back to character - // Serial.printf("Chars %s, ascii_val=%d[dec] 0x%02hhx[hex] ==> VIN=%s\n", temp, ascii_val, ascii_val, vin); + // Serial.printf("Chars %s, ascii_val=%d[dec] 0x%02hhx[hex] ==> VIN=%s\n", temp, ascii_val, ascii_val, vin); } } - Serial.printf("VIN=%s\n", vin); + if (debugMode) + { + Serial.print("VIN: "); + Serial.println(vin); + } } else { - Serial.println("No VIN response"); - printError(); + if (debugMode) + { + Serial.println("No VIN response"); + printError(); + } } return nb_rx_state; } From 4b6a3365869e0a5c344d24615b46b2b7266f9b46 Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Wed, 5 Jan 2022 14:55:07 +1030 Subject: [PATCH 19/88] remove the last Serial.printf --- src/ELMduino.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index c9be5c3..df6afd9 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2317,9 +2317,6 @@ int8_t ELM327::get_response(void) else if (recChar == '\v') Serial.println(F("\\v")); // display the hex code for non-alpha numeric characters - else if (recChar < 32 || recChar > 123) - Serial.printf("0x%02hhX\n", recChar); - // convert spaces to underscore, easier to see in debug output else if (recChar == ' ') Serial.println("_"); // display regular printable From 37fa7d8a298951370666c41ecdd66be344d18528 Mon Sep 17 00:00:00 2001 From: Patrick Felstead Date: Wed, 5 Jan 2022 14:58:31 +1030 Subject: [PATCH 20/88] remove last Serial.printf --- src/ELMduino.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index df6afd9..d7c3b64 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2316,7 +2316,7 @@ int8_t ELM327::get_response(void) Serial.println(F("\\t")); else if (recChar == '\v') Serial.println(F("\\v")); - // display the hex code for non-alpha numeric characters + // convert spaces to underscore, easier to see in debug output else if (recChar == ' ') Serial.println("_"); // display regular printable From cc4392ed8cf15b894e684eec68c49dafc6bf1ceb Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 5 Jan 2022 00:02:19 -0500 Subject: [PATCH 21/88] Update library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index d162bd3..6b249ca 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=2.6.4 +version=3.0.0 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From d45dd04129bf5163b63cef2dc63e77362ec7ee94 Mon Sep 17 00:00:00 2001 From: PB2 Date: Thu, 27 Jan 2022 00:39:20 -0500 Subject: [PATCH 22/88] Fix compile error on Uno --- src/ELMduino.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index d7c3b64..7af92c4 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2629,7 +2629,7 @@ float ELM327::batteryVoltage(void) if (nb_rx_state == ELM_SUCCESS) { nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - return strtof(payload, NULL); + return (float)strtod(payload, NULL); } else if (nb_rx_state != ELM_GETTING_MSG) nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command From 6e37fc7ef931eeebd300f42b42844ac7f50abe2f Mon Sep 17 00:00:00 2001 From: PB2 Date: Thu, 27 Jan 2022 00:39:49 -0500 Subject: [PATCH 23/88] Update library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 6b249ca..9df2dc0 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.0.0 +version=3.0.1 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 24982474e2d590be1d1d62e86a4743b947e2b50a Mon Sep 17 00:00:00 2001 From: PB2 Date: Sun, 20 Mar 2022 15:17:40 -0400 Subject: [PATCH 24/88] Add files via upload --- src/ELMduino.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 7af92c4..1732f5f 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -903,7 +903,7 @@ float ELM327::timingAdvance() */ float ELM327::intakeAirTemp() { - return processPID(SERVICE_01, INTAKE_AIR_TEMP, 1, 1, -40.0); + return processPID(SERVICE_01, INTAKE_AIR_TEMP, 1, 1, 1, -40.0); } From d2be43ce5424b9782e09ce1357bba42acda15ce6 Mon Sep 17 00:00:00 2001 From: PB2 Date: Sun, 20 Mar 2022 15:19:54 -0400 Subject: [PATCH 25/88] Update library.properties --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 9df2dc0..f9dea68 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.0.1 +version=3.0.2 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 11040a2e33b8234b0a7a70c44b96048db438ed57 Mon Sep 17 00:00:00 2001 From: PB2 Date: Sat, 4 Jun 2022 16:23:41 -0400 Subject: [PATCH 26/88] Create multiple_pids.ino --- examples/multiple_pids/multiple_pids.ino | 96 ++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 examples/multiple_pids/multiple_pids.ino diff --git a/examples/multiple_pids/multiple_pids.ino b/examples/multiple_pids/multiple_pids.ino new file mode 100644 index 0000000..7a8fb49 --- /dev/null +++ b/examples/multiple_pids/multiple_pids.ino @@ -0,0 +1,96 @@ +#include "ELMduino.h" + + + + +#define ELM_PORT Serial1 + + + + +const bool DEBUG = true; +const int TIMEOUT = 2000; +const bool HALT_ON_FAIL = false; + + + + +ELM327 myELM327; + + + + +typedef enum { ENG_RPM, + SPEED } obd_pid_states; +obd_pid_states obd_state = ENG_RPM; + +float rpm = 0; +float mph = 0; + + + + +void setup() +{ + Serial.begin(115200); + ELM_PORT.begin(115200); + + Serial.println("Attempting to connect to ELM327..."); + + if (!myELM327.begin(ELM_PORT, DEBUG, TIMEOUT)) + { + Serial.println("Couldn't connect to OBD scanner"); + + if (HALT_ON_FAIL) + while (1); + } + + Serial.println("Connected to ELM327"); +} + + + + +void loop() +{ + switch (obd_state) + { + case ENG_RPM: + { + rpm = myELM327.rpm(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + Serial.print("rpm: "); + Serial.println(rpm); + obd_state = SPEED; + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + obd_state = SPEED; + } + + break; + } + + case SPEED: + { + mph = myELM327.mph(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + Serial.print("mph: "); + Serial.println(mph); + obd_state = ENG_RPM; + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + obd_state = ENG_RPM; + } + + break; + } + } +} From fb24b93bee86b2d299dec27883f70587a1e2d01f Mon Sep 17 00:00:00 2001 From: PB2 Date: Mon, 1 Aug 2022 20:53:50 -0400 Subject: [PATCH 27/88] Typo fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2745ea6..a725c98 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ELMduino [![GitHub version](https://badge.fury.io/gh/PowerBroker2%2FELMduino.svg)](https://badge.fury.io/gh/PowerBroker2%2FELMduino) [![arduino-library-badge](https://www.ardu-badge.com/badge/ELMDuino.svg?)](https://www.ardu-badge.com/ELMDuino)

-This is a simple yet powerful library to effortlessly interface your Arduino with an ELM327 OBD-II scanner. With this library, you can query any and all OBD-II supported PIDs to collect a wide variety of car data (i.e. speed, rpm, engine temp, etc). Also, you can use ELMduino to view and clear your car's trouble codes - no need ot go to AutoZone anymore! +This is a simple yet powerful library to effortlessly interface your Arduino with an ELM327 OBD-II scanner. With this library, you can query any and all OBD-II supported PIDs to collect a wide variety of car data (i.e. speed, rpm, engine temp, etc). Also, you can use ELMduino to view and clear your car's trouble codes - no need to go to AutoZone anymore! # Example Project: From 7cd508077cb56796f7c78c4fc526c1de36d9ecfe Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Sun, 4 Sep 2022 15:49:56 -0400 Subject: [PATCH 28/88] Address issue #141 (robustly handle responses with padding/trailing zeros) --- src/ELMduino.cpp | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 1732f5f..6432ddc 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -360,7 +360,7 @@ int8_t ELM327::nextIndex(char const *str, Description: ------------ - * Converts the ELM327's response into it's correct, numerical value + * Converts the ELM327's response into it's correct, numerical value. Returns 0 if numExpectedBytes > numPayChars Inputs: ------- @@ -375,7 +375,34 @@ int8_t ELM327::nextIndex(char const *str, */ float ELM327::conditionResponse(const uint64_t &response, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { - return ((response >> (((numPayChars / 2) - numExpectedBytes) * 8)) * scaleFactor) + bias; + if (numExpectedBytes > numPayChars) + { + if (debugMode) + Serial.println(F("WARNING: Number of expected response bytes is greater than the number of payload chars returned by ELM327 - returning 0")); + + return 0; + } + else if (numExpectedBytes == numPayChars) + { + return (response * scaleFactor) + bias; + } + + // If there were more payload bytes returned than we expected, test the first and last bytes in the + // returned payload and see which gives us a higher value. Sometimes ELM327's return leading zeros + // and others return trailing zeros. The following approach gives us the best chance at determining + // where the real data is. Note that if the payload returns BOTH leading and trailing zeros, this + // will not give accurate results! + uint64_t mask = 0; + + for (int i = 0; i < (numExpectedBytes * 8); i++) + mask |= (1 << i); + + float leadingResponse = ((response >> ((numPayChars - numExpectedBytes) * 8)) * scaleFactor) + bias; + float laggingResponse = ((response & mask) * scaleFactor) + bias; + + if (leadingResponse > laggingResponse) + return leadingResponse; + return laggingResponse; } From 9cbe2bdb21144455ef63e929919f84bb58a59d52 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Thu, 22 Sep 2022 13:51:30 -0400 Subject: [PATCH 29/88] Add ELM327 datasheet --- reference/ELM327DS.pdf | Bin 0 -> 463576 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 reference/ELM327DS.pdf diff --git a/reference/ELM327DS.pdf b/reference/ELM327DS.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ca4962bc82238644e9d089ccce59e2d442eb657e GIT binary patch literal 463576 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

c0Z$j7N6^(~vofOfG%{)erI`B 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 z%0A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 Date: Thu, 22 Sep 2022 13:55:06 -0400 Subject: [PATCH 30/88] Second attempt to fix #141 --- src/ELMduino.cpp | 46 +++++++++++++++++++++++++++++++++------------- src/ELMduino.h | 2 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 6432ddc..4574af6 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -356,7 +356,7 @@ int8_t ELM327::nextIndex(char const *str, /* - void ELM327::conditionResponse(const uint64_t &response, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) + void ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) Description: ------------ @@ -373,8 +373,16 @@ int8_t ELM327::nextIndex(char const *str, ------- * float - Converted numerical value */ -float ELM327::conditionResponse(const uint64_t &response, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) +float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { + if (numExpectedBytes > 8) + { + if (debugMode) + Serial.println(F("WARNING: Number of expected response bytes is greater than 8 - returning 0")); + + return 0; + } + if (numExpectedBytes > numPayChars) { if (debugMode) @@ -383,26 +391,35 @@ float ELM327::conditionResponse(const uint64_t &response, const uint8_t &numExpe return 0; } else if (numExpectedBytes == numPayChars) - { return (response * scaleFactor) + bias; - } // If there were more payload bytes returned than we expected, test the first and last bytes in the // returned payload and see which gives us a higher value. Sometimes ELM327's return leading zeros // and others return trailing zeros. The following approach gives us the best chance at determining // where the real data is. Note that if the payload returns BOTH leading and trailing zeros, this // will not give accurate results! - uint64_t mask = 0; - for (int i = 0; i < (numExpectedBytes * 8); i++) - mask |= (1 << i); + uint64_t leadingResponse = 0; + for (uint8_t i = 0; i < numExpectedBytes; i++) + { + uint8_t payloadIndex = PAYLOAD_LEN - numPayChars + i; + uint8_t bitsOffset = 4 * (numExpectedBytes - i - 1); + + leadingResponse |= (ctoi(payload[payloadIndex]) << bitsOffset); + } - float leadingResponse = ((response >> ((numPayChars - numExpectedBytes) * 8)) * scaleFactor) + bias; - float laggingResponse = ((response & mask) * scaleFactor) + bias; + uint64_t laggingResponse = 0; + for (uint8_t i = 0; i < numExpectedBytes; i++) + { + uint8_t payloadIndex = PAYLOAD_LEN - numExpectedBytes + i; + uint8_t bitsOffset = 4 * (numExpectedBytes - i - 1); + + laggingResponse |= (ctoi(payload[payloadIndex]) << bitsOffset); + } if (leadingResponse > laggingResponse) - return leadingResponse; - return laggingResponse; + return ((float)leadingResponse * scaleFactor) + bias; + return ((float)laggingResponse * scaleFactor) + bias; } @@ -532,7 +549,10 @@ float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint if (nb_rx_state == ELM_SUCCESS) { nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - return conditionResponse(findResponse(), numExpectedBytes, scaleFactor, bias); + + findResponse(); + + return conditionResponse(numExpectedBytes, scaleFactor, bias); } else if (nb_rx_state != ELM_GETTING_MSG) nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command @@ -2498,7 +2518,7 @@ uint64_t ELM327::findResponse() Serial.println(header); } - int8_t firstHeadIndex = nextIndex(payload, header); + int8_t firstHeadIndex = nextIndex(payload, header); int8_t secondHeadIndex = nextIndex(payload, header, 2); if (firstHeadIndex >= 0) diff --git a/src/ELMduino.h b/src/ELMduino.h index 0cbd360..b3f3d65 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -318,7 +318,7 @@ class ELM327 int8_t sendCommand_Blocking(const char *cmd); int8_t get_response(); bool timeout(); - float conditionResponse(const uint64_t& response, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + float conditionResponse(const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); float batteryVoltage(void); int8_t get_vin_blocking(char vin[]); From d0b1e29ceda216743be9a7b7f1f06a2085b33ffd Mon Sep 17 00:00:00 2001 From: JunoField Date: Thu, 31 Aug 2023 18:09:30 +0100 Subject: [PATCH 31/88] corrected 'ethonol' to 'ethanol' --- README.md | 4 ++-- src/ELMduino.cpp | 6 +++--- src/ELMduino.h | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a725c98..9aa8d19 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ uint16_t timeRunWithMIL(); uint16_t timeSinceCodesCleared(); float maxMafRate(); uint8_t fuelType(); -float ethonolPercent(); +float ethanolPercent(); float absEvapSysVapPressure(); float evapSysVapPressure2(); float absFuelRailPressure(); @@ -248,7 +248,7 @@ const uint8_t TIME_SINCE_CODES_CLEARED = 78; // 0x4E - min const uint8_t MAX_VALUES_EQUIV_V_I_PRESSURE = 79; // 0x4F - ratio V mA kPa const uint8_t MAX_MAF_RATE = 80; // 0x50 - g/s const uint8_t FUEL_TYPE = 81; // 0x51 - ref table -const uint8_t ETHONOL_FUEL_PERCENT = 82; // 0x52 - % +const uint8_t ETHANOL_FUEL_PERCENT = 82; // 0x52 - % const uint8_t ABS_EVAP_SYS_VAPOR_PRESSURE = 83; // 0x53 - kPa const uint8_t EVAP_SYS_VAPOR_PRESSURE = 84; // 0x54 - Pa const uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_1_3 = 85; // 0x55 - % diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 4574af6..d282343 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -1900,7 +1900,7 @@ uint8_t ELM327::fuelType() /* - float ELM327::ethonolPercent() + float ELM327::ethanolPercent() Description: ------------ @@ -1914,9 +1914,9 @@ uint8_t ELM327::fuelType() ------- * float - Ethanol fuel in % */ -float ELM327::ethonolPercent() +float ELM327::ethanolPercent() { - return processPID(SERVICE_01, ETHONOL_FUEL_PERCENT, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ETHANOL_FUEL_PERCENT, 1, 1, 100.0 / 255.0); } diff --git a/src/ELMduino.h b/src/ELMduino.h index b3f3d65..9eb934d 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -114,7 +114,7 @@ const uint8_t TIME_SINCE_CODES_CLEARED = 78; // 0x4E - min const uint8_t MAX_VALUES_EQUIV_V_I_PRESSURE = 79; // 0x4F - ratio V mA kPa const uint8_t MAX_MAF_RATE = 80; // 0x50 - g/s const uint8_t FUEL_TYPE = 81; // 0x51 - ref table -const uint8_t ETHONOL_FUEL_PERCENT = 82; // 0x52 - % +const uint8_t ETHANOL_FUEL_PERCENT = 82; // 0x52 - % const uint8_t ABS_EVAP_SYS_VAPOR_PRESSURE = 83; // 0x53 - kPa const uint8_t EVAP_SYS_VAPOR_PRESSURE = 84; // 0x54 - Pa const uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_1_3 = 85; // 0x55 - % @@ -388,7 +388,7 @@ class ELM327 uint16_t timeSinceCodesCleared(); float maxMafRate(); uint8_t fuelType(); - float ethonolPercent(); + float ethanolPercent(); float absEvapSysVapPressure(); float evapSysVapPressure2(); float absFuelRailPressure(); From a29a7f13e1b8660a4b122b0288ae53ee1cc298ef Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Sun, 26 Nov 2023 13:17:46 -0700 Subject: [PATCH 32/88] Fix for "SEARCHING..." timeout on first OBD request when using AUTO protocol --- src/ELMduino.cpp | 74 ++++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index d282343..3a14d3b 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -106,47 +106,65 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) sprintf(command, SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); sendCommand_Blocking(command); delay(100); + + // Automatic searching for protocol requires setting the protocol to AUTO and then + // sending an OBD command to initiate the protocol search. The OBD command "0100" + // requests a list of supported PIDs 0x00 - 0x20 and is guaranteed to work + + if ((int)protocol == 0) + { + sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); + if (sendCommand_Blocking(command) == ELM_SUCCESS) + { + if (sendCommand_Blocking("0100") == ELM_SUCCESS) + { + connected = true; + return connected; + } + } + } + else + { + // Set protocol + sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); - // Set protocol - sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); + if (sendCommand_Blocking(command) == ELM_SUCCESS) + { + if (strstr(payload, "OK") != NULL) + { + connected = true; + return connected; + } + } - if (sendCommand_Blocking(command) == ELM_SUCCESS) - { - if (strstr(payload, "OK") != NULL) + if (debugMode) { - connected = true; - return connected; + Serial.print(F("Setting protocol via ")); + Serial.print(TRY_PROT_H_AUTO_SEARCH); + Serial.print(F(" did not work - trying via ")); + Serial.println(SET_PROTOCOL_TO_H_SAVE); } - } - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(TRY_PROT_H_AUTO_SEARCH); - Serial.print(F(" did not work - trying via ")); - Serial.println(SET_PROTOCOL_TO_H_SAVE); - } + // Set protocol and save + sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - // Set protocol and save - sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); + if (sendCommand_Blocking(command) == ELM_SUCCESS) + if (strstr(payload, "OK") != NULL) + connected = true; - if (sendCommand_Blocking(command) == ELM_SUCCESS) - if (strstr(payload, "OK") != NULL) - connected = true; + if (debugMode) + { + Serial.print(F("Setting protocol via ")); + Serial.print(SET_PROTOCOL_TO_H_SAVE); + Serial.println(F(" did not work")); + } - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(SET_PROTOCOL_TO_H_SAVE); - Serial.println(F(" did not work")); + return connected; } - - return connected; } - /* void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses) From 12fbaf1bedf022d6a63df1403434e059990afa41 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Sun, 26 Nov 2023 13:28:07 -0700 Subject: [PATCH 33/88] Enhancement for AUTO protocol search --- src/ELMduino.cpp | 57 ++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 3a14d3b..1aeef1e 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -110,16 +110,20 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) // Automatic searching for protocol requires setting the protocol to AUTO and then // sending an OBD command to initiate the protocol search. The OBD command "0100" // requests a list of supported PIDs 0x00 - 0x20 and is guaranteed to work - + if ((int)protocol == 0) { sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); + if (sendCommand_Blocking(command) == ELM_SUCCESS) { - if (sendCommand_Blocking("0100") == ELM_SUCCESS) - { - connected = true; - return connected; + if (strstr(payload, "OK") != NULL) + { + if (sendCommand_Blocking("0100") == ELM_SUCCESS) + { + connected = true; + return connected; + } } } } @@ -136,31 +140,32 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) return connected; } } + } + + if (debugMode) + { + Serial.print(F("Setting protocol via ")); + Serial.print(TRY_PROT_H_AUTO_SEARCH); + Serial.print(F(" did not work - trying via ")); + Serial.println(SET_PROTOCOL_TO_H_SAVE); + } - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(TRY_PROT_H_AUTO_SEARCH); - Serial.print(F(" did not work - trying via ")); - Serial.println(SET_PROTOCOL_TO_H_SAVE); - } - - // Set protocol and save - sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - - if (sendCommand_Blocking(command) == ELM_SUCCESS) - if (strstr(payload, "OK") != NULL) - connected = true; + // Set protocol and save + sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(SET_PROTOCOL_TO_H_SAVE); - Serial.println(F(" did not work")); - } + if (sendCommand_Blocking(command) == ELM_SUCCESS) + if (strstr(payload, "OK") != NULL) + connected = true; - return connected; + if (debugMode) + { + Serial.print(F("Setting protocol via ")); + Serial.print(SET_PROTOCOL_TO_H_SAVE); + Serial.println(F(" did not work")); } + + return connected; + } From 7265f5c75d36b5f97d6a718d30473307ec04464f Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Sun, 26 Nov 2023 13:31:18 -0700 Subject: [PATCH 34/88] Fix for typo "error" --- src/ELMduino.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index d282343..2fe2e96 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2433,7 +2433,7 @@ int8_t ELM327::get_response(void) if (nextIndex(payload, "UNABLETOCONNECT") >= 0) { if (debugMode) - Serial.println(F("ELM responded with errror \"UNABLE TO CONNECT\"")); + Serial.println(F("ELM responded with error \"UNABLE TO CONNECT\"")); nb_rx_state = ELM_UNABLE_TO_CONNECT; return nb_rx_state; @@ -2444,7 +2444,7 @@ int8_t ELM327::get_response(void) if (nextIndex(payload, "NODATA") >= 0) { if (debugMode) - Serial.println(F("ELM responded with errror \"NO DATA\"")); + Serial.println(F("ELM responded with error \"NO DATA\"")); nb_rx_state = ELM_NO_DATA; return nb_rx_state; @@ -2453,7 +2453,7 @@ int8_t ELM327::get_response(void) if (nextIndex(payload, "STOPPED") >= 0) { if (debugMode) - Serial.println(F("ELM responded with errror \"STOPPED\"")); + Serial.println(F("ELM responded with error \"STOPPED\"")); nb_rx_state = ELM_STOPPED; return nb_rx_state; From 9557bf3195bfabfeedf00787966e5d4564b6e1c6 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Sun, 26 Nov 2023 13:28:07 -0700 Subject: [PATCH 35/88] Enhancement for AUTO protocol search --- src/ELMduino.cpp | 69 ++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 3a14d3b..ad8522e 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -110,16 +110,28 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) // Automatic searching for protocol requires setting the protocol to AUTO and then // sending an OBD command to initiate the protocol search. The OBD command "0100" // requests a list of supported PIDs 0x00 - 0x20 and is guaranteed to work - - if ((int)protocol == 0) + if ((String)protocol == "0") { - sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); + // Tell the ELM327 to do an auto protocol search. If a valid protocol is found, it will be saved to memory. + // Some ELM clones may not have memory enabled and thus will perform the search every time. + sprintf(command, SET_PROTOCOL_TO_AUTO_H_SAVE, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) { - if (sendCommand_Blocking("0100") == ELM_SUCCESS) - { - connected = true; - return connected; + if (strstr(payload, "OK") != NULL) + { + // Protocol search can take a comparatively long time. Temporarily set + // the timeout value to 30 seconds, then restore the previous value. + uint16_t prevTimeout = timeout_ms; + timeout_ms = 30000; + + if (sendCommand_Blocking("0100") == ELM_SUCCESS) + { + timeout_ms = prevTimeout; + connected = true; + return connected; + } + + timeout_ms = prevTimeout; } } } @@ -136,31 +148,32 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) return connected; } } + } + + if (debugMode) + { + Serial.print(F("Setting protocol via ")); + Serial.print(TRY_PROT_H_AUTO_SEARCH); + Serial.print(F(" did not work - trying via ")); + Serial.println(SET_PROTOCOL_TO_H_SAVE); + } - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(TRY_PROT_H_AUTO_SEARCH); - Serial.print(F(" did not work - trying via ")); - Serial.println(SET_PROTOCOL_TO_H_SAVE); - } - - // Set protocol and save - sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - - if (sendCommand_Blocking(command) == ELM_SUCCESS) - if (strstr(payload, "OK") != NULL) - connected = true; + // Set protocol and save + sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(SET_PROTOCOL_TO_H_SAVE); - Serial.println(F(" did not work")); - } + if (sendCommand_Blocking(command) == ELM_SUCCESS) + if (strstr(payload, "OK") != NULL) + connected = true; - return connected; + if (debugMode) + { + Serial.print(F("Setting protocol via ")); + Serial.print(SET_PROTOCOL_TO_H_SAVE); + Serial.println(F(" did not work")); } + + return connected; + } From ad19e6faad8acee8f61726b217eaf2d96106bd59 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 27 Nov 2023 17:37:25 -0700 Subject: [PATCH 36/88] whitespace fixes --- src/ELMduino.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index ad8522e..8c76eb8 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -119,16 +119,16 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) { if (strstr(payload, "OK") != NULL) { - // Protocol search can take a comparatively long time. Temporarily set + // Protocol search can take a comparatively long time. Temporarily set // the timeout value to 30 seconds, then restore the previous value. uint16_t prevTimeout = timeout_ms; - timeout_ms = 30000; + timeout_ms = 30000; if (sendCommand_Blocking("0100") == ELM_SUCCESS) { timeout_ms = prevTimeout; - connected = true; - return connected; + connected = true; + return connected; } timeout_ms = prevTimeout; From 0ba09013059013c47337aa61e318d5b8ed56d421 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 27 Nov 2023 17:39:12 -0700 Subject: [PATCH 37/88] whitespace fixes --- src/ELMduino.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 8c76eb8..8eb4609 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -112,7 +112,7 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) // requests a list of supported PIDs 0x00 - 0x20 and is guaranteed to work if ((String)protocol == "0") { - // Tell the ELM327 to do an auto protocol search. If a valid protocol is found, it will be saved to memory. + // Tell the ELM327 to do an auto protocol search. If a valid protocol is found, it will be saved to memory. // Some ELM clones may not have memory enabled and thus will perform the search every time. sprintf(command, SET_PROTOCOL_TO_AUTO_H_SAVE, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) @@ -125,11 +125,11 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) timeout_ms = 30000; if (sendCommand_Blocking("0100") == ELM_SUCCESS) - { + { timeout_ms = prevTimeout; - connected = true; - return connected; - } + connected = true; + return connected; + } timeout_ms = prevTimeout; } From f81c7b0323b95519eee1addb7d0747bc87d48cf4 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 27 Nov 2023 17:43:18 -0700 Subject: [PATCH 38/88] whitespace --- src/ELMduino.cpp | 1610 +++++++++++++++++++--------------------------- 1 file changed, 669 insertions(+), 941 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 8eb4609..bfb818f 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -1,8 +1,5 @@ #include "ELMduino.h" - - - /* bool ELM327::begin(Stream &stream, const bool& debug, const uint16_t& timeout, const char& protocol, const uint16_t& payloadLen, const byte& dataTimeout) @@ -18,34 +15,31 @@ * char protocol - Protocol ID to specify the ELM327 to communicate with the ECU over * uint16_t payloadLen - Maximum number of bytes expected to be returned by the ELM327 after a query * byte dataTimeout - Number of ms to wait after receiving data before the ELM327 will - return the data - see https://www.elmelectronics.com/help/obd/tips/#UnderstandingOBD + return the data - see https://www.elmelectronics.com/help/obd/tips/#UnderstandingOBD Return: ------- * bool - Whether or not the ELM327 was properly initialized */ bool ELM327::begin(Stream &stream, const bool &debug, const uint16_t &timeout, const char &protocol, const uint16_t &payloadLen, const byte &dataTimeout) { - elm_port = &stream; - PAYLOAD_LEN = payloadLen; - debugMode = debug; - timeout_ms = timeout; + elm_port = &stream; + PAYLOAD_LEN = payloadLen; + debugMode = debug; + timeout_ms = timeout; - payload = (char *)malloc(PAYLOAD_LEN + 1); // allow for terminating '\0' + payload = (char *)malloc(PAYLOAD_LEN + 1); // allow for terminating '\0' - // test if serial port is connected - if (!elm_port) - return false; + // test if serial port is connected + if (!elm_port) + return false; - // try to connect - if (!initializeELM(protocol, dataTimeout)) - return false; + // try to connect + if (!initializeELM(protocol, dataTimeout)) + return false; - return true; + return true; } - - - /* bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) @@ -84,100 +78,97 @@ bool ELM327::begin(Stream &stream, const bool &debug, const uint16_t &timeout, c */ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) { - char command[10] = {'\0'}; - connected = false; + char command[10] = {'\0'}; + connected = false; + + sendCommand_Blocking(SET_ALL_TO_DEFAULTS); + delay(100); - sendCommand_Blocking(SET_ALL_TO_DEFAULTS); - delay(100); + sendCommand_Blocking(RESET_ALL); + delay(100); - sendCommand_Blocking(RESET_ALL); - delay(100); + sendCommand_Blocking(ECHO_OFF); + delay(100); - sendCommand_Blocking(ECHO_OFF); - delay(100); + sendCommand_Blocking(PRINTING_SPACES_OFF); + delay(100); - sendCommand_Blocking(PRINTING_SPACES_OFF); - delay(100); + sendCommand_Blocking(ALLOW_LONG_MESSAGES); + delay(100); - sendCommand_Blocking(ALLOW_LONG_MESSAGES); - delay(100); + // Set data timeout + sprintf(command, SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); + sendCommand_Blocking(command); + delay(100); - // Set data timeout - sprintf(command, SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); - sendCommand_Blocking(command); - delay(100); - - // Automatic searching for protocol requires setting the protocol to AUTO and then - // sending an OBD command to initiate the protocol search. The OBD command "0100" - // requests a list of supported PIDs 0x00 - 0x20 and is guaranteed to work - if ((String)protocol == "0") - { + // Automatic searching for protocol requires setting the protocol to AUTO and then + // sending an OBD command to initiate the protocol search. The OBD command "0100" + // requests a list of supported PIDs 0x00 - 0x20 and is guaranteed to work + if ((String)protocol == "0") + { // Tell the ELM327 to do an auto protocol search. If a valid protocol is found, it will be saved to memory. // Some ELM clones may not have memory enabled and thus will perform the search every time. - sprintf(command, SET_PROTOCOL_TO_AUTO_H_SAVE, protocol); - if (sendCommand_Blocking(command) == ELM_SUCCESS) - { - if (strstr(payload, "OK") != NULL) - { - // Protocol search can take a comparatively long time. Temporarily set - // the timeout value to 30 seconds, then restore the previous value. + sprintf(command, SET_PROTOCOL_TO_AUTO_H_SAVE, protocol); + if (sendCommand_Blocking(command) == ELM_SUCCESS) + { + if (strstr(payload, "OK") != NULL) + { + // Protocol search can take a comparatively long time. Temporarily set + // the timeout value to 30 seconds, then restore the previous value. uint16_t prevTimeout = timeout_ms; - timeout_ms = 30000; - - if (sendCommand_Blocking("0100") == ELM_SUCCESS) - { + timeout_ms = 30000; + + if (sendCommand_Blocking("0100") == ELM_SUCCESS) + { timeout_ms = prevTimeout; connected = true; return connected; } - timeout_ms = prevTimeout; - } - } - } - else - { - // Set protocol - sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); - - if (sendCommand_Blocking(command) == ELM_SUCCESS) - { - if (strstr(payload, "OK") != NULL) - { - connected = true; - return connected; - } - } - } - - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(TRY_PROT_H_AUTO_SEARCH); - Serial.print(F(" did not work - trying via ")); - Serial.println(SET_PROTOCOL_TO_H_SAVE); - } - - // Set protocol and save - sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); - - if (sendCommand_Blocking(command) == ELM_SUCCESS) - if (strstr(payload, "OK") != NULL) - connected = true; - - if (debugMode) - { - Serial.print(F("Setting protocol via ")); - Serial.print(SET_PROTOCOL_TO_H_SAVE); - Serial.println(F(" did not work")); - } - - return connected; - + timeout_ms = prevTimeout; + } + } + } + else + { + // Set protocol + sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); + + if (sendCommand_Blocking(command) == ELM_SUCCESS) + { + if (strstr(payload, "OK") != NULL) + { + connected = true; + return connected; + } + } + } + + if (debugMode) + { + Serial.print(F("Setting protocol via ")); + Serial.print(TRY_PROT_H_AUTO_SEARCH); + Serial.print(F(" did not work - trying via ")); + Serial.println(SET_PROTOCOL_TO_H_SAVE); + } + + // Set protocol and save + sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); + + if (sendCommand_Blocking(command) == ELM_SUCCESS) + if (strstr(payload, "OK") != NULL) + connected = true; + + if (debugMode) + { + Serial.print(F("Setting protocol via ")); + Serial.print(SET_PROTOCOL_TO_H_SAVE); + Serial.println(F(" did not work")); + } + + return connected; } - - /* void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses) @@ -197,60 +188,57 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) */ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses) { - if (debugMode) - { - Serial.print(F("Service: ")); - Serial.println(service); - Serial.print(F("PID: ")); - Serial.println(pid); - } + if (debugMode) + { + Serial.print(F("Service: ")); + Serial.println(service); + Serial.print(F("PID: ")); + Serial.println(pid); + } - query[0] = ((service >> 4) & 0xF) + '0'; - query[1] = (service & 0xF) + '0'; + query[0] = ((service >> 4) & 0xF) + '0'; + query[1] = (service & 0xF) + '0'; - // determine PID length (standard queries have 16-bit PIDs, - // but some custom queries have PIDs with 32-bit values) - if (pid & 0xFF00) - { - if (debugMode) - Serial.println(F("Long query detected")); + // determine PID length (standard queries have 16-bit PIDs, + // but some custom queries have PIDs with 32-bit values) + if (pid & 0xFF00) + { + if (debugMode) + Serial.println(F("Long query detected")); - longQuery = true; + longQuery = true; - query[2] = ((pid >> 12) & 0xF) + '0'; - query[3] = ((pid >> 8) & 0xF) + '0'; - query[4] = ((pid >> 4) & 0xF) + '0'; - query[5] = (pid & 0xF) + '0'; - query[6] = num_responses + '0'; - query[7] = '\0'; + query[2] = ((pid >> 12) & 0xF) + '0'; + query[3] = ((pid >> 8) & 0xF) + '0'; + query[4] = ((pid >> 4) & 0xF) + '0'; + query[5] = (pid & 0xF) + '0'; + query[6] = num_responses + '0'; + query[7] = '\0'; - upper(query, 6); - } - else - { - if (debugMode) - Serial.println(F("Normal length query detected")); + upper(query, 6); + } + else + { + if (debugMode) + Serial.println(F("Normal length query detected")); - longQuery = false; + longQuery = false; - query[2] = ((pid >> 4) & 0xF) + '0'; - query[3] = (pid & 0xF) + '0'; - query[4] = num_responses + '0'; - query[5] = '\0'; + query[2] = ((pid >> 4) & 0xF) + '0'; + query[3] = (pid & 0xF) + '0'; + query[4] = num_responses + '0'; + query[5] = '\0'; - upper(query, 4); - } + upper(query, 4); + } - if (debugMode) - { - Serial.print(F("Query string: ")); - Serial.println(query); - } + if (debugMode) + { + Serial.print(F("Query string: ")); + Serial.println(query); + } } - - - /* void ELM327::upper(char string[], uint8_t buflen) @@ -270,18 +258,15 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons */ void ELM327::upper(char string[], uint8_t buflen) { - for (uint8_t i = 0; i < buflen; i++) - { - if (string[i] > 'Z') - string[i] -= 32; - else if ((string[i] > '9') && (string[i] < 'A')) - string[i] += 7; - } + for (uint8_t i = 0; i < buflen; i++) + { + if (string[i] > 'Z') + string[i] -= 32; + else if ((string[i] > '9') && (string[i] < 'A')) + string[i] += 7; + } } - - - /* bool ELM327::timeout() @@ -299,15 +284,12 @@ void ELM327::upper(char string[], uint8_t buflen) */ bool ELM327::timeout() { - currentTime = millis(); - if ((currentTime - previousTime) >= timeout_ms) - return true; - return false; + currentTime = millis(); + if ((currentTime - previousTime) >= timeout_ms) + return true; + return false; } - - - /* uint8_t ELM327::ctoi(uint8_t value) @@ -325,15 +307,12 @@ bool ELM327::timeout() */ uint8_t ELM327::ctoi(uint8_t value) { - if (value >= 'A') - return value - 'A' + 10; - else - return value - '0'; + if (value >= 'A') + return value - 'A' + 10; + else + return value - '0'; } - - - /* int8_t ELM327::nextIndex(char const *str, char const *target, @@ -349,7 +328,7 @@ uint8_t ELM327::ctoi(uint8_t value) * char const *str - string to search target within * char const *target - String to search for in str * uint8_t numOccur - Which instance of target in str - + Return: ------- * int8_t - First char index of numOccur'th @@ -357,35 +336,32 @@ uint8_t ELM327::ctoi(uint8_t value) numOccur'th instance of target in str */ int8_t ELM327::nextIndex(char const *str, - char const *target, - uint8_t numOccur = 1) + char const *target, + uint8_t numOccur = 1) { - char const *p = str; - char const *r = str; - uint8_t count; + char const *p = str; + char const *r = str; + uint8_t count; - for (count = 0;; ++count) - { - p = strstr(p, target); + for (count = 0;; ++count) + { + p = strstr(p, target); - if (count == (numOccur - 1)) - break; + if (count == (numOccur - 1)) + break; - if (!p) - break; + if (!p) + break; - p++; - } + p++; + } - if (!p) - return -1; + if (!p) + return -1; - return p - r; + return p - r; } - - - /* void ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) @@ -406,55 +382,52 @@ int8_t ELM327::nextIndex(char const *str, */ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { - if (numExpectedBytes > 8) - { - if (debugMode) - Serial.println(F("WARNING: Number of expected response bytes is greater than 8 - returning 0")); - - return 0; - } - - if (numExpectedBytes > numPayChars) - { - if (debugMode) - Serial.println(F("WARNING: Number of expected response bytes is greater than the number of payload chars returned by ELM327 - returning 0")); - - return 0; - } - else if (numExpectedBytes == numPayChars) - return (response * scaleFactor) + bias; + if (numExpectedBytes > 8) + { + if (debugMode) + Serial.println(F("WARNING: Number of expected response bytes is greater than 8 - returning 0")); - // If there were more payload bytes returned than we expected, test the first and last bytes in the - // returned payload and see which gives us a higher value. Sometimes ELM327's return leading zeros - // and others return trailing zeros. The following approach gives us the best chance at determining - // where the real data is. Note that if the payload returns BOTH leading and trailing zeros, this - // will not give accurate results! + return 0; + } - uint64_t leadingResponse = 0; - for (uint8_t i = 0; i < numExpectedBytes; i++) - { - uint8_t payloadIndex = PAYLOAD_LEN - numPayChars + i; - uint8_t bitsOffset = 4 * (numExpectedBytes - i - 1); + if (numExpectedBytes > numPayChars) + { + if (debugMode) + Serial.println(F("WARNING: Number of expected response bytes is greater than the number of payload chars returned by ELM327 - returning 0")); - leadingResponse |= (ctoi(payload[payloadIndex]) << bitsOffset); - } + return 0; + } + else if (numExpectedBytes == numPayChars) + return (response * scaleFactor) + bias; - uint64_t laggingResponse = 0; - for (uint8_t i = 0; i < numExpectedBytes; i++) - { - uint8_t payloadIndex = PAYLOAD_LEN - numExpectedBytes + i; - uint8_t bitsOffset = 4 * (numExpectedBytes - i - 1); + // If there were more payload bytes returned than we expected, test the first and last bytes in the + // returned payload and see which gives us a higher value. Sometimes ELM327's return leading zeros + // and others return trailing zeros. The following approach gives us the best chance at determining + // where the real data is. Note that if the payload returns BOTH leading and trailing zeros, this + // will not give accurate results! - laggingResponse |= (ctoi(payload[payloadIndex]) << bitsOffset); - } + uint64_t leadingResponse = 0; + for (uint8_t i = 0; i < numExpectedBytes; i++) + { + uint8_t payloadIndex = PAYLOAD_LEN - numPayChars + i; + uint8_t bitsOffset = 4 * (numExpectedBytes - i - 1); - if (leadingResponse > laggingResponse) - return ((float)leadingResponse * scaleFactor) + bias; - return ((float)laggingResponse * scaleFactor) + bias; -} + leadingResponse |= (ctoi(payload[payloadIndex]) << bitsOffset); + } + uint64_t laggingResponse = 0; + for (uint8_t i = 0; i < numExpectedBytes; i++) + { + uint8_t payloadIndex = PAYLOAD_LEN - numExpectedBytes + i; + uint8_t bitsOffset = 4 * (numExpectedBytes - i - 1); + laggingResponse |= (ctoi(payload[payloadIndex]) << bitsOffset); + } + if (leadingResponse > laggingResponse) + return ((float)leadingResponse * scaleFactor) + bias; + return ((float)laggingResponse * scaleFactor) + bias; +} /* void ELM327::flushInputBuff() @@ -473,16 +446,13 @@ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &sc */ void ELM327::flushInputBuff() { - if (debugMode) - Serial.println(F("Clearing input serial buffer")); + if (debugMode) + Serial.println(F("Clearing input serial buffer")); - while (elm_port->available()) - elm_port->read(); + while (elm_port->available()) + elm_port->read(); } - - - /* bool ELM327::queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses) @@ -496,24 +466,21 @@ void ELM327::flushInputBuff() * uint16_t pid - The Parameter ID (PID) from the service * uint8_t num_responses - Number of lines of data to receive - see ELM datasheet "Talking to the vehicle". This can speed up retrieval of information if you know how many responses will be sent. - Basically the OBD scanner will not wait for more responses if it does not need to go through + Basically the OBD scanner will not wait for more responses if it does not need to go through final timeout. Also prevents OBD scanners from sending mulitple of the same response. Return: ------- * bool - Whether or not the query was submitted successfully */ -bool ELM327::queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses) +bool ELM327::queryPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses) { - formatQueryArray(service, pid, num_responses); - sendCommand(query); + formatQueryArray(service, pid, num_responses); + sendCommand(query); - return connected; + return connected; } - - - /* bool ELM327::queryPID(char queryStr[]) @@ -531,19 +498,16 @@ bool ELM327::queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t */ bool ELM327::queryPID(char queryStr[]) { - if (strlen(queryStr) <= 4) - longQuery = false; - else - longQuery = true; + if (strlen(queryStr) <= 4) + longQuery = false; + else + longQuery = true; - sendCommand(queryStr); + sendCommand(queryStr); - return connected; + return connected; } - - - /* float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) @@ -557,7 +521,7 @@ bool ELM327::queryPID(char queryStr[]) * uint16_t pid - The Parameter ID (PID) from the service * uint8_t num_responses - Number of lines of data to receive - see ELM datasheet "Talking to the vehicle". This can speed up retrieval of information if you know how many responses will be sent. - Basically the OBD scanner will not wait for more responses if it does not need to go through + Basically the OBD scanner will not wait for more responses if it does not need to go through final timeout. Also prevents OBD scanners from sending mulitple of the same response. * uint8_t numExpectedBytes - Number of valid bytes from the response to process * float scaleFactor - Amount to scale the response by @@ -567,32 +531,29 @@ bool ELM327::queryPID(char queryStr[]) ------- * float - The PID value if successfully received, else 0.0 */ -float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) +float ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { - if (nb_query_state == SEND_COMMAND) - { - queryPID(service, pid, num_responses); - nb_query_state = WAITING_RESP; - } - else if (nb_query_state == WAITING_RESP) - { - get_response(); - if (nb_rx_state == ELM_SUCCESS) - { - nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - - findResponse(); - - return conditionResponse(numExpectedBytes, scaleFactor, bias); - } - else if (nb_rx_state != ELM_GETTING_MSG) - nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command - } - return 0.0; -} - + if (nb_query_state == SEND_COMMAND) + { + queryPID(service, pid, num_responses); + nb_query_state = WAITING_RESP; + } + else if (nb_query_state == WAITING_RESP) + { + get_response(); + if (nb_rx_state == ELM_SUCCESS) + { + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + findResponse(); + return conditionResponse(numExpectedBytes, scaleFactor, bias); + } + else if (nb_rx_state != ELM_GETTING_MSG) + nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command + } + return 0.0; +} /* uint32_t ELM327::supportedPIDs_1_20() @@ -611,12 +572,9 @@ float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint */ uint32_t ELM327::supportedPIDs_1_20() { - return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_1_20, 1, 4); + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_1_20, 1, 4); } - - - /* uint32_t ELM327::monitorStatus() @@ -636,12 +594,9 @@ uint32_t ELM327::supportedPIDs_1_20() */ uint32_t ELM327::monitorStatus() { - return (uint32_t)processPID(SERVICE_01, MONITOR_STATUS_SINCE_DTC_CLEARED, 1, 4); + return (uint32_t)processPID(SERVICE_01, MONITOR_STATUS_SINCE_DTC_CLEARED, 1, 4); } - - - /* uint16_t ELM327::freezeDTC() @@ -659,12 +614,9 @@ uint32_t ELM327::monitorStatus() */ uint16_t ELM327::freezeDTC() { - return (uint16_t)processPID(SERVICE_01, FREEZE_DTC, 1, 2); + return (uint16_t)processPID(SERVICE_01, FREEZE_DTC, 1, 2); } - - - /* uint16_t ELM327::fuelSystemStatus() @@ -682,12 +634,9 @@ uint16_t ELM327::freezeDTC() */ uint16_t ELM327::fuelSystemStatus() { - return (uint16_t)processPID(SERVICE_01, FUEL_SYSTEM_STATUS, 1, 2); + return (uint16_t)processPID(SERVICE_01, FUEL_SYSTEM_STATUS, 1, 2); } - - - /* float ELM327::engineLoad() @@ -705,12 +654,9 @@ uint16_t ELM327::fuelSystemStatus() */ float ELM327::engineLoad() { - return processPID(SERVICE_01, ENGINE_LOAD, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ENGINE_LOAD, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::engineCoolantTemp() @@ -728,12 +674,9 @@ float ELM327::engineLoad() */ float ELM327::engineCoolantTemp() { - return processPID(SERVICE_01, ENGINE_COOLANT_TEMP, 1, 1, 1, -40.0); + return processPID(SERVICE_01, ENGINE_COOLANT_TEMP, 1, 1, 1, -40.0); } - - - /* float ELM327::shortTermFuelTrimBank_1() @@ -751,12 +694,9 @@ float ELM327::engineCoolantTemp() */ float ELM327::shortTermFuelTrimBank_1() { - return processPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_1, 1, 1, 100.0 / 128.0, -100.0); + return processPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_1, 1, 1, 100.0 / 128.0, -100.0); } - - - /* float ELM327::longTermFuelTrimBank_1() @@ -774,12 +714,9 @@ float ELM327::shortTermFuelTrimBank_1() */ float ELM327::longTermFuelTrimBank_1() { - return processPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_1, 1, 1, 100.0 / 128.0, -100.0); + return processPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_1, 1, 1, 100.0 / 128.0, -100.0); } - - - /* float ELM327::shortTermFuelTrimBank_2() @@ -797,12 +734,9 @@ float ELM327::longTermFuelTrimBank_1() */ float ELM327::shortTermFuelTrimBank_2() { - return processPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_2, 1, 1, 100.0 / 128.0, -100.0); + return processPID(SERVICE_01, SHORT_TERM_FUEL_TRIM_BANK_2, 1, 1, 100.0 / 128.0, -100.0); } - - - /* float ELM327::longTermFuelTrimBank_2() @@ -820,12 +754,9 @@ float ELM327::shortTermFuelTrimBank_2() */ float ELM327::longTermFuelTrimBank_2() { - return processPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_2, 1, 1, 100.0 / 128.0, -100.0); + return processPID(SERVICE_01, LONG_TERM_FUEL_TRIM_BANK_2, 1, 1, 100.0 / 128.0, -100.0); } - - - /* float ELM327::fuelPressure() @@ -843,12 +774,9 @@ float ELM327::longTermFuelTrimBank_2() */ float ELM327::fuelPressure() { - return processPID(SERVICE_01, FUEL_PRESSURE, 1, 1, 3.0); + return processPID(SERVICE_01, FUEL_PRESSURE, 1, 1, 3.0); } - - - /* uint8_t ELM327::manifoldPressure() @@ -866,12 +794,9 @@ float ELM327::fuelPressure() */ uint8_t ELM327::manifoldPressure() { - return (uint8_t)processPID(SERVICE_01, INTAKE_MANIFOLD_ABS_PRESSURE, 1, 1); + return (uint8_t)processPID(SERVICE_01, INTAKE_MANIFOLD_ABS_PRESSURE, 1, 1); } - - - /* float ELM327::rpm() @@ -889,12 +814,9 @@ uint8_t ELM327::manifoldPressure() */ float ELM327::rpm() { - return processPID(SERVICE_01, ENGINE_RPM, 1, 2, 1.0 / 4.0); + return processPID(SERVICE_01, ENGINE_RPM, 1, 2, 1.0 / 4.0); } - - - /* int32_t ELM327::kph() @@ -912,12 +834,9 @@ float ELM327::rpm() */ int32_t ELM327::kph() { - return (int32_t)processPID(SERVICE_01, VEHICLE_SPEED, 1, 1); + return (int32_t)processPID(SERVICE_01, VEHICLE_SPEED, 1, 1); } - - - /* float ELM327::mph() @@ -935,12 +854,9 @@ int32_t ELM327::kph() */ float ELM327::mph() { - return kph() * KPH_MPH_CONVERT; + return kph() * KPH_MPH_CONVERT; } - - - /* float ELM327::timingAdvance() @@ -958,12 +874,9 @@ float ELM327::mph() */ float ELM327::timingAdvance() { - return processPID(SERVICE_01, TIMING_ADVANCE, 1, 1, 1.0 / 2.0, -64.0); + return processPID(SERVICE_01, TIMING_ADVANCE, 1, 1, 1.0 / 2.0, -64.0); } - - - /* float ELM327::intakeAirTemp() @@ -981,12 +894,9 @@ float ELM327::timingAdvance() */ float ELM327::intakeAirTemp() { - return processPID(SERVICE_01, INTAKE_AIR_TEMP, 1, 1, 1, -40.0); + return processPID(SERVICE_01, INTAKE_AIR_TEMP, 1, 1, 1, -40.0); } - - - /* float ELM327::mafRate() @@ -1004,12 +914,9 @@ float ELM327::intakeAirTemp() */ float ELM327::mafRate() { - return processPID(SERVICE_01, MAF_FLOW_RATE, 1, 2, 1.0 / 100.0); + return processPID(SERVICE_01, MAF_FLOW_RATE, 1, 2, 1.0 / 100.0); } - - - /* float ELM327::throttle() @@ -1027,12 +934,9 @@ float ELM327::mafRate() */ float ELM327::throttle() { - return processPID(SERVICE_01, THROTTLE_POSITION, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, THROTTLE_POSITION, 1, 1, 100.0 / 255.0); } - - - /* uint8_t ELM327::commandedSecAirStatus() @@ -1050,12 +954,9 @@ float ELM327::throttle() */ uint8_t ELM327::commandedSecAirStatus() { - return (uint8_t)processPID(SERVICE_01, COMMANDED_SECONDARY_AIR_STATUS, 1, 1); + return (uint8_t)processPID(SERVICE_01, COMMANDED_SECONDARY_AIR_STATUS, 1, 1); } - - - /* uint8_t ELM327::oxygenSensorsPresent_2banks() @@ -1073,12 +974,9 @@ uint8_t ELM327::commandedSecAirStatus() */ uint8_t ELM327::oxygenSensorsPresent_2banks() { - return (uint8_t)processPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_2_BANKS, 1, 1); + return (uint8_t)processPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_2_BANKS, 1, 1); } - - - /* uint8_t ELM327::obdStandards() @@ -1096,12 +994,9 @@ uint8_t ELM327::oxygenSensorsPresent_2banks() */ uint8_t ELM327::obdStandards() { - return (uint8_t)processPID(SERVICE_01, OBD_STANDARDS, 1, 1); + return (uint8_t)processPID(SERVICE_01, OBD_STANDARDS, 1, 1); } - - - /* uint8_t ELM327::oxygenSensorsPresent_4banks() @@ -1119,12 +1014,9 @@ uint8_t ELM327::obdStandards() */ uint8_t ELM327::oxygenSensorsPresent_4banks() { - return (uint8_t)processPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_4_BANKS, 1, 1); + return (uint8_t)processPID(SERVICE_01, OXYGEN_SENSORS_PRESENT_4_BANKS, 1, 1); } - - - /* bool ELM327::auxInputStatus() @@ -1142,12 +1034,9 @@ uint8_t ELM327::oxygenSensorsPresent_4banks() */ bool ELM327::auxInputStatus() { - return (bool)processPID(SERVICE_01, AUX_INPUT_STATUS, 1, 1); + return (bool)processPID(SERVICE_01, AUX_INPUT_STATUS, 1, 1); } - - - /* uint16_t ELM327::runTime() @@ -1165,12 +1054,9 @@ bool ELM327::auxInputStatus() */ uint16_t ELM327::runTime() { - return (uint16_t)processPID(SERVICE_01, RUN_TIME_SINCE_ENGINE_START, 1, 2); + return (uint16_t)processPID(SERVICE_01, RUN_TIME_SINCE_ENGINE_START, 1, 2); } - - - /* uint32_t ELM327::supportedPIDs_21_40() @@ -1188,12 +1074,9 @@ uint16_t ELM327::runTime() */ uint32_t ELM327::supportedPIDs_21_40() { - return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_21_40, 1, 4); + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_21_40, 1, 4); } - - - /* uint16_t ELM327::distTravelWithMIL() @@ -1211,12 +1094,9 @@ uint32_t ELM327::supportedPIDs_21_40() */ uint16_t ELM327::distTravelWithMIL() { - return (uint16_t)processPID(SERVICE_01, DISTANCE_TRAVELED_WITH_MIL_ON, 1, 2); + return (uint16_t)processPID(SERVICE_01, DISTANCE_TRAVELED_WITH_MIL_ON, 1, 2); } - - - /* float ELM327::fuelRailPressure() @@ -1234,12 +1114,9 @@ uint16_t ELM327::distTravelWithMIL() */ float ELM327::fuelRailPressure() { - return processPID(SERVICE_01, FUEL_RAIL_PRESSURE, 1, 2, 0.079); + return processPID(SERVICE_01, FUEL_RAIL_PRESSURE, 1, 2, 0.079); } - - - /* float ELM327::fuelRailGuagePressure() @@ -1257,12 +1134,9 @@ float ELM327::fuelRailPressure() */ float ELM327::fuelRailGuagePressure() { - return processPID(SERVICE_01, FUEL_RAIL_GUAGE_PRESSURE, 1, 2, 10.0); + return processPID(SERVICE_01, FUEL_RAIL_GUAGE_PRESSURE, 1, 2, 10.0); } - - - /* float ELM327::commandedEGR() @@ -1280,12 +1154,9 @@ float ELM327::fuelRailGuagePressure() */ float ELM327::commandedEGR() { - return processPID(SERVICE_01, COMMANDED_EGR, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, COMMANDED_EGR, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::egrError() @@ -1303,12 +1174,9 @@ float ELM327::commandedEGR() */ float ELM327::egrError() { - return processPID(SERVICE_01, EGR_ERROR, 1, 1, 100.0 / 128.0, -100); + return processPID(SERVICE_01, EGR_ERROR, 1, 1, 100.0 / 128.0, -100); } - - - /* float ELM327::commandedEvapPurge() @@ -1326,12 +1194,9 @@ float ELM327::egrError() */ float ELM327::commandedEvapPurge() { - return processPID(SERVICE_01, COMMANDED_EVAPORATIVE_PURGE, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, COMMANDED_EVAPORATIVE_PURGE, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::fuelLevel() @@ -1349,12 +1214,9 @@ float ELM327::commandedEvapPurge() */ float ELM327::fuelLevel() { - return processPID(SERVICE_01, FUEL_TANK_LEVEL_INPUT, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, FUEL_TANK_LEVEL_INPUT, 1, 1, 100.0 / 255.0); } - - - /* uint8_t ELM327::warmUpsSinceCodesCleared() @@ -1372,12 +1234,9 @@ float ELM327::fuelLevel() */ uint8_t ELM327::warmUpsSinceCodesCleared() { - return (uint8_t)processPID(SERVICE_01, WARM_UPS_SINCE_CODES_CLEARED, 1, 1); + return (uint8_t)processPID(SERVICE_01, WARM_UPS_SINCE_CODES_CLEARED, 1, 1); } - - - /* uint16_t ELM327::distSinceCodesCleared() @@ -1395,12 +1254,9 @@ uint8_t ELM327::warmUpsSinceCodesCleared() */ uint16_t ELM327::distSinceCodesCleared() { - return (uint16_t)processPID(SERVICE_01, DIST_TRAV_SINCE_CODES_CLEARED, 1, 2); + return (uint16_t)processPID(SERVICE_01, DIST_TRAV_SINCE_CODES_CLEARED, 1, 2); } - - - /* float ELM327::evapSysVapPressure() @@ -1418,12 +1274,9 @@ uint16_t ELM327::distSinceCodesCleared() */ float ELM327::evapSysVapPressure() { - return processPID(SERVICE_01, EVAP_SYSTEM_VAPOR_PRESSURE, 1, 2, 1.0 / 4.0); + return processPID(SERVICE_01, EVAP_SYSTEM_VAPOR_PRESSURE, 1, 2, 1.0 / 4.0); } - - - /* uint8_t ELM327::absBaroPressure() @@ -1441,12 +1294,9 @@ float ELM327::evapSysVapPressure() */ uint8_t ELM327::absBaroPressure() { - return (uint8_t)processPID(SERVICE_01, ABS_BAROMETRIC_PRESSURE, 1, 1); + return (uint8_t)processPID(SERVICE_01, ABS_BAROMETRIC_PRESSURE, 1, 1); } - - - /* float ELM327::catTempB1S1() @@ -1464,12 +1314,9 @@ uint8_t ELM327::absBaroPressure() */ float ELM327::catTempB1S1() { - return processPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_1, 1, 2, 1.0 / 10.0, -40.0); + return processPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_1, 1, 2, 1.0 / 10.0, -40.0); } - - - /* float ELM327::catTempB2S1() @@ -1487,12 +1334,9 @@ float ELM327::catTempB1S1() */ float ELM327::catTempB2S1() { - return processPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_1, 1, 2, 1.0 / 10.0, -40.0); + return processPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_1, 1, 2, 1.0 / 10.0, -40.0); } - - - /* float ELM327::catTempB1S2() @@ -1510,12 +1354,9 @@ float ELM327::catTempB2S1() */ float ELM327::catTempB1S2() { - return processPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_2, 1, 2, 1.0 / 10.0, -40.0); + return processPID(SERVICE_01, CATALYST_TEMP_BANK_1_SENSOR_2, 1, 2, 1.0 / 10.0, -40.0); } - - - /* float ELM327::catTempB2S2() @@ -1533,12 +1374,9 @@ float ELM327::catTempB1S2() */ float ELM327::catTempB2S2() { - return processPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_2, 1, 2, 1.0 / 10.0, -40.0); + return processPID(SERVICE_01, CATALYST_TEMP_BANK_2_SENSOR_2, 1, 2, 1.0 / 10.0, -40.0); } - - - /* uint32_t ELM327::supportedPIDs_41_60() @@ -1556,12 +1394,9 @@ float ELM327::catTempB2S2() */ uint32_t ELM327::supportedPIDs_41_60() { - return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_41_60, 1, 4); + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_41_60, 1, 4); } - - - /* uint32_t ELM327::monitorDriveCycleStatus() @@ -1579,12 +1414,9 @@ uint32_t ELM327::supportedPIDs_41_60() */ uint32_t ELM327::monitorDriveCycleStatus() { - return (uint32_t)processPID(SERVICE_01, MONITOR_STATUS_THIS_DRIVE_CYCLE, 1, 4); + return (uint32_t)processPID(SERVICE_01, MONITOR_STATUS_THIS_DRIVE_CYCLE, 1, 4); } - - - /* float ELM327::ctrlModVoltage() @@ -1602,12 +1434,9 @@ uint32_t ELM327::monitorDriveCycleStatus() */ float ELM327::ctrlModVoltage() { - return processPID(SERVICE_01, CONTROL_MODULE_VOLTAGE, 1, 2, 1.0 / 1000.0); + return processPID(SERVICE_01, CONTROL_MODULE_VOLTAGE, 1, 2, 1.0 / 1000.0); } - - - /* float ELM327::absLoad() @@ -1625,12 +1454,9 @@ float ELM327::ctrlModVoltage() */ float ELM327::absLoad() { - return processPID(SERVICE_01, ABS_LOAD_VALUE, 1, 2, 100.0 / 255.0); + return processPID(SERVICE_01, ABS_LOAD_VALUE, 1, 2, 100.0 / 255.0); } - - - /* float ELM327::commandedAirFuelRatio() @@ -1648,12 +1474,9 @@ float ELM327::absLoad() */ float ELM327::commandedAirFuelRatio() { - return processPID(SERVICE_01, FUEL_AIR_COMMANDED_EQUIV_RATIO, 1, 2, 2.0 / 65536.0); + return processPID(SERVICE_01, FUEL_AIR_COMMANDED_EQUIV_RATIO, 1, 2, 2.0 / 65536.0); } - - - /* float ELM327::relativeThrottle() @@ -1671,12 +1494,9 @@ float ELM327::commandedAirFuelRatio() */ float ELM327::relativeThrottle() { - return processPID(SERVICE_01, RELATIVE_THROTTLE_POSITION, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, RELATIVE_THROTTLE_POSITION, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::ambientAirTemp() @@ -1694,12 +1514,9 @@ float ELM327::relativeThrottle() */ float ELM327::ambientAirTemp() { - return processPID(SERVICE_01, AMBIENT_AIR_TEMP, 1, 1, 1, -40); + return processPID(SERVICE_01, AMBIENT_AIR_TEMP, 1, 1, 1, -40); } - - - /* float ELM327::absThrottlePosB() @@ -1717,12 +1534,9 @@ float ELM327::ambientAirTemp() */ float ELM327::absThrottlePosB() { - return processPID(SERVICE_01, ABS_THROTTLE_POSITION_B, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_B, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::absThrottlePosC() @@ -1740,12 +1554,9 @@ float ELM327::absThrottlePosB() */ float ELM327::absThrottlePosC() { - return processPID(SERVICE_01, ABS_THROTTLE_POSITION_C, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_C, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::absThrottlePosD() @@ -1763,12 +1574,9 @@ float ELM327::absThrottlePosC() */ float ELM327::absThrottlePosD() { - return processPID(SERVICE_01, ABS_THROTTLE_POSITION_D, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_D, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::absThrottlePosE() @@ -1786,12 +1594,9 @@ float ELM327::absThrottlePosD() */ float ELM327::absThrottlePosE() { - return processPID(SERVICE_01, ABS_THROTTLE_POSITION_E, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_E, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::absThrottlePosF() @@ -1809,12 +1614,9 @@ float ELM327::absThrottlePosE() */ float ELM327::absThrottlePosF() { - return processPID(SERVICE_01, ABS_THROTTLE_POSITION_F, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ABS_THROTTLE_POSITION_F, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::commandedThrottleActuator() @@ -1832,12 +1634,9 @@ float ELM327::absThrottlePosF() */ float ELM327::commandedThrottleActuator() { - return processPID(SERVICE_01, COMMANDED_THROTTLE_ACTUATOR, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, COMMANDED_THROTTLE_ACTUATOR, 1, 1, 100.0 / 255.0); } - - - /* uint16_t ELM327::timeRunWithMIL() @@ -1855,12 +1654,9 @@ float ELM327::commandedThrottleActuator() */ uint16_t ELM327::timeRunWithMIL() { - return (uint16_t)processPID(SERVICE_01, TIME_RUN_WITH_MIL_ON, 1, 2); + return (uint16_t)processPID(SERVICE_01, TIME_RUN_WITH_MIL_ON, 1, 2); } - - - /* uint16_t ELM327::timeSinceCodesCleared() @@ -1878,12 +1674,9 @@ uint16_t ELM327::timeRunWithMIL() */ uint16_t ELM327::timeSinceCodesCleared() { - return (uint16_t)processPID(SERVICE_01, TIME_SINCE_CODES_CLEARED, 1, 2); + return (uint16_t)processPID(SERVICE_01, TIME_SINCE_CODES_CLEARED, 1, 2); } - - - /* float ELM327::maxMafRate() @@ -1901,12 +1694,9 @@ uint16_t ELM327::timeSinceCodesCleared() */ float ELM327::maxMafRate() { - return processPID(SERVICE_01, MAX_MAF_RATE, 1, 1, 10.0); + return processPID(SERVICE_01, MAX_MAF_RATE, 1, 1, 10.0); } - - - /* uint8_t ELM327::fuelType() @@ -1924,12 +1714,9 @@ float ELM327::maxMafRate() */ uint8_t ELM327::fuelType() { - return (uint8_t)processPID(SERVICE_01, FUEL_TYPE, 1, 1); + return (uint8_t)processPID(SERVICE_01, FUEL_TYPE, 1, 1); } - - - /* float ELM327::ethanolPercent() @@ -1947,12 +1734,9 @@ uint8_t ELM327::fuelType() */ float ELM327::ethanolPercent() { - return processPID(SERVICE_01, ETHANOL_FUEL_PERCENT, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, ETHANOL_FUEL_PERCENT, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::absEvapSysVapPressure() @@ -1970,12 +1754,9 @@ float ELM327::ethanolPercent() */ float ELM327::absEvapSysVapPressure() { - return processPID(SERVICE_01, ABS_EVAP_SYS_VAPOR_PRESSURE, 1, 2, 1.0 / 200.0); + return processPID(SERVICE_01, ABS_EVAP_SYS_VAPOR_PRESSURE, 1, 2, 1.0 / 200.0); } - - - /* float ELM327::evapSysVapPressure2() @@ -1993,12 +1774,9 @@ float ELM327::absEvapSysVapPressure() */ float ELM327::evapSysVapPressure2() { - return processPID(SERVICE_01, EVAP_SYS_VAPOR_PRESSURE, 1, 2, 1, -32767); + return processPID(SERVICE_01, EVAP_SYS_VAPOR_PRESSURE, 1, 2, 1, -32767); } - - - /* float ELM327::absFuelRailPressure() @@ -2016,12 +1794,9 @@ float ELM327::evapSysVapPressure2() */ float ELM327::absFuelRailPressure() { - return processPID(SERVICE_01, FUEL_RAIL_ABS_PRESSURE, 1, 2, 10.0); + return processPID(SERVICE_01, FUEL_RAIL_ABS_PRESSURE, 1, 2, 10.0); } - - - /* float ELM327::relativePedalPos() @@ -2039,12 +1814,9 @@ float ELM327::absFuelRailPressure() */ float ELM327::relativePedalPos() { - return processPID(SERVICE_01, RELATIVE_ACCELERATOR_PEDAL_POS, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, RELATIVE_ACCELERATOR_PEDAL_POS, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::hybridBatLife() @@ -2062,12 +1834,9 @@ float ELM327::relativePedalPos() */ float ELM327::hybridBatLife() { - return processPID(SERVICE_01, HYBRID_BATTERY_REMAINING_LIFE, 1, 1, 100.0 / 255.0); + return processPID(SERVICE_01, HYBRID_BATTERY_REMAINING_LIFE, 1, 1, 100.0 / 255.0); } - - - /* float ELM327::oilTemp() @@ -2085,12 +1854,9 @@ float ELM327::hybridBatLife() */ float ELM327::oilTemp() { - return processPID(SERVICE_01, ENGINE_OIL_TEMP, 1, 1, 1, -40.0); + return processPID(SERVICE_01, ENGINE_OIL_TEMP, 1, 1, 1, -40.0); } - - - /* float ELM327::fuelInjectTiming() @@ -2108,12 +1874,9 @@ float ELM327::oilTemp() */ float ELM327::fuelInjectTiming() { - return processPID(SERVICE_01, FUEL_INJECTION_TIMING, 1, 2, 1.0 / 128.0, -210.0); + return processPID(SERVICE_01, FUEL_INJECTION_TIMING, 1, 2, 1.0 / 128.0, -210.0); } - - - /* float ELM327::fuelRate() @@ -2131,12 +1894,9 @@ float ELM327::fuelInjectTiming() */ float ELM327::fuelRate() { - return processPID(SERVICE_01, ENGINE_FUEL_RATE, 1, 2, 1.0 / 20.0); + return processPID(SERVICE_01, ENGINE_FUEL_RATE, 1, 2, 1.0 / 20.0); } - - - /* uint8_t ELM327::emissionRqmts() @@ -2154,12 +1914,9 @@ float ELM327::fuelRate() */ uint8_t ELM327::emissionRqmts() { - return (uint8_t)processPID(SERVICE_01, EMISSION_REQUIREMENTS, 1, 1); + return (uint8_t)processPID(SERVICE_01, EMISSION_REQUIREMENTS, 1, 1); } - - - /* uint32_t ELM327::supportedPIDs_61_80() @@ -2177,12 +1934,9 @@ uint8_t ELM327::emissionRqmts() */ uint32_t ELM327::supportedPIDs_61_80() { - return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_61_80, 1, 4); + return (uint32_t)processPID(SERVICE_01, SUPPORTED_PIDS_61_80, 1, 4); } - - - /* float ELM327::demandedTorque() @@ -2200,12 +1954,9 @@ uint32_t ELM327::supportedPIDs_61_80() */ float ELM327::demandedTorque() { - return processPID(SERVICE_01, DEMANDED_ENGINE_PERCENT_TORQUE, 1, 1, 1, -125.0); + return processPID(SERVICE_01, DEMANDED_ENGINE_PERCENT_TORQUE, 1, 1, 1, -125.0); } - - - /* float ELM327::torque() @@ -2223,12 +1974,9 @@ float ELM327::demandedTorque() */ float ELM327::torque() { - return processPID(SERVICE_01, ACTUAL_ENGINE_TORQUE, 1, 1, 1, -125.0); + return processPID(SERVICE_01, ACTUAL_ENGINE_TORQUE, 1, 1, 1, -125.0); } - - - /* uint16_t ELM327::referenceTorque() @@ -2246,12 +1994,9 @@ float ELM327::torque() */ uint16_t ELM327::referenceTorque() { - return processPID(SERVICE_01, ENGINE_REFERENCE_TORQUE, 1, 2); + return processPID(SERVICE_01, ENGINE_REFERENCE_TORQUE, 1, 2); } - - - /* uint16_t ELM327::auxSupported() @@ -2269,12 +2014,9 @@ uint16_t ELM327::referenceTorque() */ uint16_t ELM327::auxSupported() { - return (uint16_t)processPID(SERVICE_01, AUX_INPUT_OUTPUT_SUPPORTED, 1, 2); + return (uint16_t)processPID(SERVICE_01, AUX_INPUT_OUTPUT_SUPPORTED, 1, 2); } - - - /* void ELM327::sendCommand(const char *cmd) @@ -2292,40 +2034,37 @@ uint16_t ELM327::auxSupported() */ void ELM327::sendCommand(const char *cmd) { - // clear payload buffer - memset(payload, '\0', PAYLOAD_LEN + 1); + // clear payload buffer + memset(payload, '\0', PAYLOAD_LEN + 1); - // reset input serial buffer and number of received bytes - recBytes = 0; - flushInputBuff(); - connected = false; + // reset input serial buffer and number of received bytes + recBytes = 0; + flushInputBuff(); + connected = false; - // Reset the receive state ready to start receiving a response message - nb_rx_state = ELM_GETTING_MSG; + // Reset the receive state ready to start receiving a response message + nb_rx_state = ELM_GETTING_MSG; - if (debugMode) - { - Serial.print(F("Sending the following command/query: ")); - Serial.println(cmd); - } + if (debugMode) + { + Serial.print(F("Sending the following command/query: ")); + Serial.println(cmd); + } - elm_port->print(cmd); - elm_port->print('\r'); + elm_port->print(cmd); + elm_port->print('\r'); - // prime the timeout timer - previousTime = millis(); - currentTime = previousTime; + // prime the timeout timer + previousTime = millis(); + currentTime = previousTime; } - - - /* obd_rx_states ELM327::sendCommand_Blocking(const char* cmd) Description: ------------ - * Sends a command/query and waits for a respoonse (blocking function) + * Sends a command/query and waits for a respoonse (blocking function) Sometimes it's desirable to use a blocking command, e.g when sending an AT command. This function removes the need for the caller to set up a loop waiting for the command to finish. Caller is free to parse the payload string if they need to use the response. @@ -2340,14 +2079,12 @@ void ELM327::sendCommand(const char *cmd) */ int8_t ELM327::sendCommand_Blocking(const char *cmd) { - sendCommand(cmd); - while (get_response() == ELM_GETTING_MSG); - return nb_rx_state; + sendCommand(cmd); + while (get_response() == ELM_GETTING_MSG) + ; + return nb_rx_state; } - - - /* obd_rx_states ELM327::get_response(void) @@ -2366,146 +2103,143 @@ int8_t ELM327::sendCommand_Blocking(const char *cmd) */ int8_t ELM327::get_response(void) { - // buffer the response of the ELM327 until either the - // end marker is read or a timeout has occurred - // last valid idx is PAYLOAD_LEN but want to keep one free for terminating '\0' - // so limit counter to < PAYLOAD_LEN - if (!elm_port->available()) - { - nb_rx_state = ELM_GETTING_MSG; - if (timeout()) - nb_rx_state = ELM_TIMEOUT; - } - else - { - char recChar = elm_port->read(); - - if (debugMode) - { - Serial.print(F("\tReceived char: ")); - // display each received character, make non-printables printable - if (recChar == '\f') - Serial.println(F("\\f")); - else if (recChar == '\n') - Serial.println(F("\\n")); - else if (recChar == '\r') - Serial.println(F("\\r")); - else if (recChar == '\t') - Serial.println(F("\\t")); - else if (recChar == '\v') - Serial.println(F("\\v")); - // convert spaces to underscore, easier to see in debug output - else if (recChar == ' ') - Serial.println("_"); - // display regular printable - else - Serial.println(recChar); - } - - // this is the end of the OBD response - if (recChar == '>') - { - if (debugMode) - Serial.println(F("Delimiter found.")); - - nb_rx_state = ELM_MSG_RXD; - } - else if (!isalnum(recChar) && (recChar != ':') && (recChar != '.')) - // discard all characters except for alphanumeric and decimal places. - // decimal places needed to extract floating point numbers, e.g. battery voltage - nb_rx_state = ELM_GETTING_MSG; // Discard this character - else - { - if (recBytes < PAYLOAD_LEN) - { - payload[recBytes] = recChar; - recBytes++; - nb_rx_state = ELM_GETTING_MSG; - } - else - nb_rx_state = ELM_BUFFER_OVERFLOW; - } - } - - // Message is still being received (or is timing out), so exit early without doing all the other checks - if (nb_rx_state == ELM_GETTING_MSG) - return nb_rx_state; - - // End of response delimiter was found - if (debugMode && nb_rx_state == ELM_MSG_RXD) - { - Serial.print(F("All chars received: ")); - Serial.println(payload); - } - - if (nb_rx_state == ELM_TIMEOUT) - { - if (debugMode) - { - Serial.print(F("Timeout detected with overflow of ")); - Serial.print((currentTime - previousTime) - timeout_ms); - Serial.println(F("ms")); - } - return nb_rx_state; - } - - if (nb_rx_state == ELM_BUFFER_OVERFLOW) - { - if (debugMode) - { - Serial.print(F("OBD receive buffer overflow (> ")); - Serial.print(PAYLOAD_LEN); - Serial.println(F(" bytes)")); - } - return nb_rx_state; - } - - // Now we have successfully received OBD response, check if the payload indicates any OBD errors - if (nextIndex(payload, "UNABLETOCONNECT") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with errror \"UNABLE TO CONNECT\"")); - - nb_rx_state = ELM_UNABLE_TO_CONNECT; - return nb_rx_state; - } - - connected = true; - - if (nextIndex(payload, "NODATA") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with errror \"NO DATA\"")); - - nb_rx_state = ELM_NO_DATA; - return nb_rx_state; - } - - if (nextIndex(payload, "STOPPED") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with errror \"STOPPED\"")); - - nb_rx_state = ELM_STOPPED; - return nb_rx_state; - } - - if (nextIndex(payload, "ERROR") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with \"ERROR\"")); - - nb_rx_state = ELM_GENERAL_ERROR; - return nb_rx_state; - } - - nb_rx_state = ELM_SUCCESS; - return nb_rx_state; + // buffer the response of the ELM327 until either the + // end marker is read or a timeout has occurred + // last valid idx is PAYLOAD_LEN but want to keep one free for terminating '\0' + // so limit counter to < PAYLOAD_LEN + if (!elm_port->available()) + { + nb_rx_state = ELM_GETTING_MSG; + if (timeout()) + nb_rx_state = ELM_TIMEOUT; + } + else + { + char recChar = elm_port->read(); + + if (debugMode) + { + Serial.print(F("\tReceived char: ")); + // display each received character, make non-printables printable + if (recChar == '\f') + Serial.println(F("\\f")); + else if (recChar == '\n') + Serial.println(F("\\n")); + else if (recChar == '\r') + Serial.println(F("\\r")); + else if (recChar == '\t') + Serial.println(F("\\t")); + else if (recChar == '\v') + Serial.println(F("\\v")); + // convert spaces to underscore, easier to see in debug output + else if (recChar == ' ') + Serial.println("_"); + // display regular printable + else + Serial.println(recChar); + } + + // this is the end of the OBD response + if (recChar == '>') + { + if (debugMode) + Serial.println(F("Delimiter found.")); + + nb_rx_state = ELM_MSG_RXD; + } + else if (!isalnum(recChar) && (recChar != ':') && (recChar != '.')) + // discard all characters except for alphanumeric and decimal places. + // decimal places needed to extract floating point numbers, e.g. battery voltage + nb_rx_state = ELM_GETTING_MSG; // Discard this character + else + { + if (recBytes < PAYLOAD_LEN) + { + payload[recBytes] = recChar; + recBytes++; + nb_rx_state = ELM_GETTING_MSG; + } + else + nb_rx_state = ELM_BUFFER_OVERFLOW; + } + } + + // Message is still being received (or is timing out), so exit early without doing all the other checks + if (nb_rx_state == ELM_GETTING_MSG) + return nb_rx_state; + + // End of response delimiter was found + if (debugMode && nb_rx_state == ELM_MSG_RXD) + { + Serial.print(F("All chars received: ")); + Serial.println(payload); + } + + if (nb_rx_state == ELM_TIMEOUT) + { + if (debugMode) + { + Serial.print(F("Timeout detected with overflow of ")); + Serial.print((currentTime - previousTime) - timeout_ms); + Serial.println(F("ms")); + } + return nb_rx_state; + } + + if (nb_rx_state == ELM_BUFFER_OVERFLOW) + { + if (debugMode) + { + Serial.print(F("OBD receive buffer overflow (> ")); + Serial.print(PAYLOAD_LEN); + Serial.println(F(" bytes)")); + } + return nb_rx_state; + } + + // Now we have successfully received OBD response, check if the payload indicates any OBD errors + if (nextIndex(payload, "UNABLETOCONNECT") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with errror \"UNABLE TO CONNECT\"")); + + nb_rx_state = ELM_UNABLE_TO_CONNECT; + return nb_rx_state; + } + + connected = true; + + if (nextIndex(payload, "NODATA") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with errror \"NO DATA\"")); + + nb_rx_state = ELM_NO_DATA; + return nb_rx_state; + } + + if (nextIndex(payload, "STOPPED") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with errror \"STOPPED\"")); + + nb_rx_state = ELM_STOPPED; + return nb_rx_state; + } + + if (nextIndex(payload, "ERROR") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with \"ERROR\"")); + + nb_rx_state = ELM_GENERAL_ERROR; + return nb_rx_state; + } + + nb_rx_state = ELM_SUCCESS; + return nb_rx_state; } - - - /* uint64_t ELM327::findResponse() @@ -2523,115 +2257,112 @@ int8_t ELM327::get_response(void) */ uint64_t ELM327::findResponse() { - uint8_t firstDatum = 0; - char header[7] = {'\0'}; - - if (longQuery) - { - header[0] = query[0] + 4; - header[1] = query[1]; - header[2] = query[2]; - header[3] = query[3]; - header[4] = query[4]; - header[5] = query[5]; - } - else - { - header[0] = query[0] + 4; - header[1] = query[1]; - header[2] = query[2]; - header[3] = query[3]; - } - - if (debugMode) - { - Serial.print(F("Expected response header: ")); - Serial.println(header); - } - - int8_t firstHeadIndex = nextIndex(payload, header); - int8_t secondHeadIndex = nextIndex(payload, header, 2); - - if (firstHeadIndex >= 0) - { - if (longQuery) - firstDatum = firstHeadIndex + 6; - else - firstDatum = firstHeadIndex + 4; - - // Some ELM327s (such as my own) respond with two - // "responses" per query. "numPayChars" represents the - // correct number of bytes returned by the ELM327 - // regardless of how many "responses" were returned - if (secondHeadIndex >= 0) - { - if (debugMode) - Serial.println(F("Double response detected")); - - numPayChars = secondHeadIndex - firstDatum; - } - else - { - if (debugMode) - Serial.println(F("Single response detected")); - - numPayChars = recBytes - firstDatum; - } - - response = 0; - for (uint8_t i = 0; i < numPayChars; i++) - { - uint8_t payloadIndex = firstDatum + i; - uint8_t bitsOffset = 4 * (numPayChars - i - 1); - response = response | (ctoi(payload[payloadIndex]) << bitsOffset); - } - - // It is useful to have the response bytes - // broken-out because some PID algorithms (standard - // and custom) require special operations for each - // byte returned - responseByte_0 = response & 0xFF; - responseByte_1 = (response >> 8) & 0xFF; - responseByte_2 = (response >> 16) & 0xFF; - responseByte_3 = (response >> 24) & 0xFF; - responseByte_4 = (response >> 32) & 0xFF; - responseByte_5 = (response >> 40) & 0xFF; - responseByte_6 = (response >> 48) & 0xFF; - responseByte_7 = (response >> 56) & 0xFF; - - if (debugMode) - { - Serial.println(F("64-bit response: ")); - Serial.print(F("\tresponseByte_0: ")); - Serial.println(responseByte_0); - Serial.print(F("\tresponseByte_1: ")); - Serial.println(responseByte_1); - Serial.print(F("\tresponseByte_2: ")); - Serial.println(responseByte_2); - Serial.print(F("\tresponseByte_3: ")); - Serial.println(responseByte_3); - Serial.print(F("\tresponseByte_4: ")); - Serial.println(responseByte_4); - Serial.print(F("\tresponseByte_5: ")); - Serial.println(responseByte_5); - Serial.print(F("\tresponseByte_6: ")); - Serial.println(responseByte_6); - Serial.print(F("\tresponseByte_7: ")); - Serial.println(responseByte_7); - } - - return response; - } - - if (debugMode) - Serial.println(F("Response not detected")); - - return 0; + uint8_t firstDatum = 0; + char header[7] = {'\0'}; + + if (longQuery) + { + header[0] = query[0] + 4; + header[1] = query[1]; + header[2] = query[2]; + header[3] = query[3]; + header[4] = query[4]; + header[5] = query[5]; + } + else + { + header[0] = query[0] + 4; + header[1] = query[1]; + header[2] = query[2]; + header[3] = query[3]; + } + + if (debugMode) + { + Serial.print(F("Expected response header: ")); + Serial.println(header); + } + + int8_t firstHeadIndex = nextIndex(payload, header); + int8_t secondHeadIndex = nextIndex(payload, header, 2); + + if (firstHeadIndex >= 0) + { + if (longQuery) + firstDatum = firstHeadIndex + 6; + else + firstDatum = firstHeadIndex + 4; + + // Some ELM327s (such as my own) respond with two + // "responses" per query. "numPayChars" represents the + // correct number of bytes returned by the ELM327 + // regardless of how many "responses" were returned + if (secondHeadIndex >= 0) + { + if (debugMode) + Serial.println(F("Double response detected")); + + numPayChars = secondHeadIndex - firstDatum; + } + else + { + if (debugMode) + Serial.println(F("Single response detected")); + + numPayChars = recBytes - firstDatum; + } + + response = 0; + for (uint8_t i = 0; i < numPayChars; i++) + { + uint8_t payloadIndex = firstDatum + i; + uint8_t bitsOffset = 4 * (numPayChars - i - 1); + response = response | (ctoi(payload[payloadIndex]) << bitsOffset); + } + + // It is useful to have the response bytes + // broken-out because some PID algorithms (standard + // and custom) require special operations for each + // byte returned + responseByte_0 = response & 0xFF; + responseByte_1 = (response >> 8) & 0xFF; + responseByte_2 = (response >> 16) & 0xFF; + responseByte_3 = (response >> 24) & 0xFF; + responseByte_4 = (response >> 32) & 0xFF; + responseByte_5 = (response >> 40) & 0xFF; + responseByte_6 = (response >> 48) & 0xFF; + responseByte_7 = (response >> 56) & 0xFF; + + if (debugMode) + { + Serial.println(F("64-bit response: ")); + Serial.print(F("\tresponseByte_0: ")); + Serial.println(responseByte_0); + Serial.print(F("\tresponseByte_1: ")); + Serial.println(responseByte_1); + Serial.print(F("\tresponseByte_2: ")); + Serial.println(responseByte_2); + Serial.print(F("\tresponseByte_3: ")); + Serial.println(responseByte_3); + Serial.print(F("\tresponseByte_4: ")); + Serial.println(responseByte_4); + Serial.print(F("\tresponseByte_5: ")); + Serial.println(responseByte_5); + Serial.print(F("\tresponseByte_6: ")); + Serial.println(responseByte_6); + Serial.print(F("\tresponseByte_7: ")); + Serial.println(responseByte_7); + } + + return response; + } + + if (debugMode) + Serial.println(F("Response not detected")); + + return 0; } - - - /* void ELM327::printError() @@ -2649,36 +2380,33 @@ uint64_t ELM327::findResponse() */ void ELM327::printError() { - Serial.print(F("Received: ")); - Serial.println(payload); - - if (nb_rx_state == ELM_SUCCESS) - Serial.println(F("ELM_SUCCESS")); - else if (nb_rx_state == ELM_NO_RESPONSE) - Serial.println(F("ERROR: ELM_NO_RESPONSE")); - else if (nb_rx_state == ELM_BUFFER_OVERFLOW) - Serial.println(F("ERROR: ELM_BUFFER_OVERFLOW")); - else if (nb_rx_state == ELM_UNABLE_TO_CONNECT) - Serial.println(F("ERROR: ELM_UNABLE_TO_CONNECT")); - else if (nb_rx_state == ELM_NO_DATA) - Serial.println(F("ERROR: ELM_NO_DATA")); - else if (nb_rx_state == ELM_STOPPED) - Serial.println(F("ERROR: ELM_STOPPED")); - else if (nb_rx_state == ELM_TIMEOUT) - Serial.println(F("ERROR: ELM_TIMEOUT")); - else if (nb_rx_state == ELM_BUFFER_OVERFLOW) - Serial.println(F("ERROR: BUFFER OVERFLOW")); - else if (nb_rx_state == ELM_GENERAL_ERROR) - Serial.println(F("ERROR: ELM_GENERAL_ERROR")); - else - Serial.println(F("No error detected")); - - delay(100); + Serial.print(F("Received: ")); + Serial.println(payload); + + if (nb_rx_state == ELM_SUCCESS) + Serial.println(F("ELM_SUCCESS")); + else if (nb_rx_state == ELM_NO_RESPONSE) + Serial.println(F("ERROR: ELM_NO_RESPONSE")); + else if (nb_rx_state == ELM_BUFFER_OVERFLOW) + Serial.println(F("ERROR: ELM_BUFFER_OVERFLOW")); + else if (nb_rx_state == ELM_UNABLE_TO_CONNECT) + Serial.println(F("ERROR: ELM_UNABLE_TO_CONNECT")); + else if (nb_rx_state == ELM_NO_DATA) + Serial.println(F("ERROR: ELM_NO_DATA")); + else if (nb_rx_state == ELM_STOPPED) + Serial.println(F("ERROR: ELM_STOPPED")); + else if (nb_rx_state == ELM_TIMEOUT) + Serial.println(F("ERROR: ELM_TIMEOUT")); + else if (nb_rx_state == ELM_BUFFER_OVERFLOW) + Serial.println(F("ERROR: BUFFER OVERFLOW")); + else if (nb_rx_state == ELM_GENERAL_ERROR) + Serial.println(F("ERROR: ELM_GENERAL_ERROR")); + else + Serial.println(F("No error detected")); + + delay(100); } - - - /* float ELM327::batteryVoltage(void) @@ -2696,39 +2424,36 @@ void ELM327::printError() */ float ELM327::batteryVoltage(void) { - if (nb_query_state == SEND_COMMAND) - { - sendCommand(READ_VOLTAGE); - nb_query_state = WAITING_RESP; - } - else if (nb_query_state == WAITING_RESP) - { - get_response(); - if (nb_rx_state == ELM_SUCCESS) - { - nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - return (float)strtod(payload, NULL); - } - else if (nb_rx_state != ELM_GETTING_MSG) - nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command - } - return 0.0; + if (nb_query_state == SEND_COMMAND) + { + sendCommand(READ_VOLTAGE); + nb_query_state = WAITING_RESP; + } + else if (nb_query_state == WAITING_RESP) + { + get_response(); + if (nb_rx_state == ELM_SUCCESS) + { + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + return (float)strtod(payload, NULL); + } + else if (nb_rx_state != ELM_GETTING_MSG) + nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command + } + return 0.0; } - - - /* int8_t ELM327::get_vin_blocking(char *vin) Description: ------------ - * Read Vehicle Identification Number (VIN). This is a blocking function. + * Read Vehicle Identification Number (VIN). This is a blocking function. Inputs: ------- - * char vin[] - pointer to c-string in which to store VIN - Note: (allocate memory for 18 character c-string in calling function) + * char vin[] - pointer to c-string in which to store VIN + Note: (allocate memory for 18 character c-string in calling function) Return: ------- @@ -2736,56 +2461,59 @@ float ELM327::batteryVoltage(void) */ int8_t ELM327::get_vin_blocking(char vin[]) { - char temp[3] = {0}; - char *idx; - uint8_t vin_counter = 0; - uint8_t ascii_val; - - if (debugMode) - Serial.println("Getting VIN..."); - sendCommand("0902"); // VIN is command 0902 - while (get_response() == ELM_GETTING_MSG); - - // strcpy(payload, "0140:4902013144341:475030305235352:42313233343536"); - if (nb_rx_state == ELM_SUCCESS) - { - memset(vin, 0, 18); - // **** Decoding **** - if (strstr(payload, "490201")) - { - // OBD scanner provides this multiline response: - // 014 ==> 0x14 = 20 bytes following - // 0: 49 02 01 31 44 34 ==> 49 02 = Header. 01 = 1 VIN number in message. 31, 44, 34 = First 3 VIN digits - // 1: 47 50 30 30 52 35 35 ==> 47->35 next 7 VIN digits - // 2: 42 31 32 33 34 35 36 ==> 42->36 next 7 VIN digits - // - // The resulitng payload buffer is: - // "0140:4902013144341:475030305235352:42313233343536" ==> VIN="1D4GP00R55B123456" (17-digits) - idx = strstr(payload, "490201") + 6; // Pointer to first ASCII code digit of first VIN digit - // Loop over each pair of ASCII code digits. 17 VIN digits + 2 skipped line numbers = 19 loops - for (int i = 0; i < (19 * 2); i += 2) { - temp[0] = *(idx + i); // Get first digit of ASCII code - temp[1] = *(idx + i + 1); // Get second digit of ASCII code - // No need to add string termination, temp[3] always == 0 - if (strstr(temp, ":")) continue; // Skip the second "1:" and third "2:" line numbers - ascii_val = strtol(temp, 0, 16); // Convert ASCII code to integer - sprintf(vin + vin_counter++, "%c", ascii_val); // Convert ASCII code integer back to character - // Serial.printf("Chars %s, ascii_val=%d[dec] 0x%02hhx[hex] ==> VIN=%s\n", temp, ascii_val, ascii_val, vin); - } - } - if (debugMode) - { - Serial.print("VIN: "); - Serial.println(vin); - } - } - else - { - if (debugMode) - { - Serial.println("No VIN response"); - printError(); - } - } - return nb_rx_state; + char temp[3] = {0}; + char *idx; + uint8_t vin_counter = 0; + uint8_t ascii_val; + + if (debugMode) + Serial.println("Getting VIN..."); + sendCommand("0902"); // VIN is command 0902 + while (get_response() == ELM_GETTING_MSG) + ; + + // strcpy(payload, "0140:4902013144341:475030305235352:42313233343536"); + if (nb_rx_state == ELM_SUCCESS) + { + memset(vin, 0, 18); + // **** Decoding **** + if (strstr(payload, "490201")) + { + // OBD scanner provides this multiline response: + // 014 ==> 0x14 = 20 bytes following + // 0: 49 02 01 31 44 34 ==> 49 02 = Header. 01 = 1 VIN number in message. 31, 44, 34 = First 3 VIN digits + // 1: 47 50 30 30 52 35 35 ==> 47->35 next 7 VIN digits + // 2: 42 31 32 33 34 35 36 ==> 42->36 next 7 VIN digits + // + // The resulitng payload buffer is: + // "0140:4902013144341:475030305235352:42313233343536" ==> VIN="1D4GP00R55B123456" (17-digits) + idx = strstr(payload, "490201") + 6; // Pointer to first ASCII code digit of first VIN digit + // Loop over each pair of ASCII code digits. 17 VIN digits + 2 skipped line numbers = 19 loops + for (int i = 0; i < (19 * 2); i += 2) + { + temp[0] = *(idx + i); // Get first digit of ASCII code + temp[1] = *(idx + i + 1); // Get second digit of ASCII code + // No need to add string termination, temp[3] always == 0 + if (strstr(temp, ":")) + continue; // Skip the second "1:" and third "2:" line numbers + ascii_val = strtol(temp, 0, 16); // Convert ASCII code to integer + sprintf(vin + vin_counter++, "%c", ascii_val); // Convert ASCII code integer back to character + // Serial.printf("Chars %s, ascii_val=%d[dec] 0x%02hhx[hex] ==> VIN=%s\n", temp, ascii_val, ascii_val, vin); + } + } + if (debugMode) + { + Serial.print("VIN: "); + Serial.println(vin); + } + } + else + { + if (debugMode) + { + Serial.println("No VIN response"); + printError(); + } + } + return nb_rx_state; } From 587a2024ce80a14c44f2d98a37a0a03e34eb5fef Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Mon, 4 Dec 2023 18:29:34 -0500 Subject: [PATCH 39/88] Fix #185 --- src/ELMduino.cpp | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index ad284d4..5c8fd5c 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -382,6 +382,9 @@ int8_t ELM327::nextIndex(char const *str, */ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { + uint8_t numExpectedPayChars = numExpectedBytes * 2; + uint8_t payCharDiff = numPayChars - numExpectedPayChars; + if (numExpectedBytes > 8) { if (debugMode) @@ -390,14 +393,21 @@ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &sc return 0; } - if (numExpectedBytes > numPayChars) + if (numPayChars < numExpectedPayChars) + { + if (debugMode) + Serial.println(F("WARNING: Number of payload chars is less than the number of expected response chars returned by ELM327 - returning 0")); + + return 0; + } + else if (numPayChars & 0x1) { if (debugMode) - Serial.println(F("WARNING: Number of expected response bytes is greater than the number of payload chars returned by ELM327 - returning 0")); + Serial.println(F("WARNING: Number of payload chars returned by ELM327 is an odd value - returning 0")); return 0; } - else if (numExpectedBytes == numPayChars) + else if (numExpectedPayChars == numPayChars) return (response * scaleFactor) + bias; // If there were more payload bytes returned than we expected, test the first and last bytes in the @@ -406,27 +416,29 @@ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &sc // where the real data is. Note that if the payload returns BOTH leading and trailing zeros, this // will not give accurate results! - uint64_t leadingResponse = 0; - for (uint8_t i = 0; i < numExpectedBytes; i++) - { - uint8_t payloadIndex = PAYLOAD_LEN - numPayChars + i; - uint8_t bitsOffset = 4 * (numExpectedBytes - i - 1); + if (debugMode) + Serial.println("Looking for lagging zeros"); - leadingResponse |= (ctoi(payload[payloadIndex]) << bitsOffset); - } + uint16_t numExpectedBits = numExpectedBytes * 8; + uint64_t laggingZerosMask = 0; + + for (uint16_t i=0; i> (4 * payCharDiff)) * scaleFactor) + bias; } + else + { + if (debugMode) + Serial.println("Lagging zeros not found - assuming leading zeros"); - if (leadingResponse > laggingResponse) - return ((float)leadingResponse * scaleFactor) + bias; - return ((float)laggingResponse * scaleFactor) + bias; + return (response * scaleFactor) + bias; + } } /* From b077f190ad48a3a6f601193d93b6d45ad5eb75d6 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Mon, 4 Dec 2023 18:31:24 -0500 Subject: [PATCH 40/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index f9dea68..9dbe4f3 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.0.2 +version=3.1.0 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From b7426c44cb80a4ec60854c7714c61278b58fa2e3 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Tue, 5 Dec 2023 20:20:02 -0700 Subject: [PATCH 41/88] Adds resetDTC() method. Does what it says on the tin; a method to reset all DTC codes. --- examples/ESP32_Reset_DTC/ESP32_Reset_DTC.ino | 43 ++++++++++++++++++++ src/ELMduino.cpp | 43 ++++++++++++++++++++ src/ELMduino.h | 1 + 3 files changed, 87 insertions(+) create mode 100644 examples/ESP32_Reset_DTC/ESP32_Reset_DTC.ino diff --git a/examples/ESP32_Reset_DTC/ESP32_Reset_DTC.ino b/examples/ESP32_Reset_DTC/ESP32_Reset_DTC.ino new file mode 100644 index 0000000..f334c58 --- /dev/null +++ b/examples/ESP32_Reset_DTC/ESP32_Reset_DTC.ino @@ -0,0 +1,43 @@ +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; + +void setup() +{ + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ESP32", true); + + if (!ELM_PORT.connect("OBDII")) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 2"); + while (1) + ; + } + + DEBUG_PORT.println("Connected to ELM327"); + DEBUG_PORT.println("Sending DTC reset command..."); + + if (myELM327.resetDTC()) + { + DEBUG_PORT.println("DTC reset successful."); + } + else + { + DEBUG_PORT.println("DTC reset failed."); + } +} + +void loop(){} diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 1aeb7b6..40291b9 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2517,3 +2517,46 @@ int8_t ELM327::get_vin_blocking(char vin[]) } return nb_rx_state; } + +/* + bool ELM327::resetDTC() + + Description: + ------------ + * Resets the stored DTCs in the ECU. This is a blocking function. + Note: The SAE spec requires that scan tools verify that a reset + is intended ("Are you sure?") before sending the mode 04 + reset command to the vehicle. See p.32 of ELM327 datasheet. + + Inputs: + ------- + * void + + Return: + ------- + * bool - Indicates the success (or not) of the reset command. +*/ +bool ELM327::resetDTC() +{ + if (sendCommand_Blocking("04") == ELM_SUCCESS) + { + if (strstr(payload, "44") != NULL) + { + if(debugMode) + { + Serial.println("ELMduino: DTC successfully reset."); + } + + return true; + } + } + else + { + if(debugMode) + { + Serial.println("ELMduino: Resetting DTC codes failed."); + } + } + + return false; +} \ No newline at end of file diff --git a/src/ELMduino.h b/src/ELMduino.h index 9eb934d..deea4c1 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -322,6 +322,7 @@ class ELM327 float batteryVoltage(void); int8_t get_vin_blocking(char vin[]); + bool resetDTC(); uint32_t supportedPIDs_1_20(); From b7abf5876d6a00fdffd10450a087ebdb7c549794 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 11 Dec 2023 15:00:18 -0700 Subject: [PATCH 42/88] Added method to check/retrieve current DTCs (#200) --- .gitignore | 13 + examples/ESP32_Check_DTC/ESP32_Check_DTC.ino | 133 ++++++ src/ELMduino.cpp | 477 +++++++++++++------ src/ELMduino.h | 17 +- 4 files changed, 488 insertions(+), 152 deletions(-) create mode 100644 examples/ESP32_Check_DTC/ESP32_Check_DTC.ino diff --git a/.gitignore b/.gitignore index 259148f..967e2ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ + +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch + # Prerequisites *.d @@ -30,3 +37,9 @@ *.exe *.out *.app +include/README +lib/README +test/README +.vscode/extensions.json +platformio.ini +.gitignore diff --git a/examples/ESP32_Check_DTC/ESP32_Check_DTC.ino b/examples/ESP32_Check_DTC/ESP32_Check_DTC.ino new file mode 100644 index 0000000..f818cff --- /dev/null +++ b/examples/ESP32_Check_DTC/ESP32_Check_DTC.ino @@ -0,0 +1,133 @@ +#include "ELMduino.h" +#include + +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +typedef enum +{ MILSTATUS, + DTCCODES +} dtc_states; + +BluetoothSerial SerialBT; +ELM327 myELM327; +dtc_states dtc_state = MILSTATUS; +uint8_t numCodes = 0; +uint8_t milStatus =0; + +void setup() +{ + DEBUG_PORT.begin(115200); + ELM_PORT.begin("ArduHUD", true); + ELM_PORT.setPin("1234"); + + DEBUG_PORT.println("Starting connection..."); + if (!ELM_PORT.connect("ELMULATOR")) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT)) + { + DEBUG_PORT.println("ELM327 Couldn't connect to ECU - Phase 2"); + while (1) + ; + } + + DEBUG_PORT.println("Connected to ELM327"); + + delay(1000); + + // Demonstration of calling currentDTCCodes in blocking (default) mode + // This is the simplest use case. Just scan for codes without first + // checking if MIL is on or how many codes are present. + + DEBUG_PORT.println("Performing DTC check in blocking mode..."); + + myELM327.currentDTCCodes(); + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + DEBUG_PORT.println("Current DTCs found: "); + + for (int i = 0; i < myELM327.DTC_Response.codesFound; i++) + { + DEBUG_PORT.println(myELM327.DTC_Response.codes[i]); + } + delay(10000); // Pause for 10 sec after successful fetch of DTC codes. + } + + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + } +} + +void loop() +{ + // This is the typical use case: First check if any codes are present, and then make a request to get them. + // monitorStatus() is a non-blocking call and must be called repeatedly until a response is found. + switch (dtc_state) + { + case MILSTATUS: + myELM327.monitorStatus(); // Gets the number of current DTC codes present + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + // We are only interested in the third byte of the response that + // encodes the MIL status and number of codes present + milStatus = (myELM327.responseByte_2 & 0x80); + numCodes = (myELM327.responseByte_2 - 0x80); + + DEBUG_PORT.print("MIL (Check Engine Light) is: "); + if (milStatus) + { + DEBUG_PORT.println("ON."); + } + else + { + DEBUG_PORT.println("OFF."); + } + + DEBUG_PORT.print("Number of codes present: "); + DEBUG_PORT.println(numCodes); + dtc_state = DTCCODES; + break; + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + dtc_state = DTCCODES; + } + break; + + case DTCCODES: // If current DTC codes were found in previous request, then retrieve the codes + if (numCodes > 0) + { + myELM327.currentDTCCodes(false); // Call in NB mode + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + DEBUG_PORT.println("Current DTCs found: "); + + for (int i = 0; i < myELM327.DTC_Response.codesFound; i++) + { + DEBUG_PORT.println(myELM327.DTC_Response.codes[i]); + } + dtc_state = MILSTATUS; + delay(10000); // Pause for 10 sec after successful fetch of DTC codes. + } + + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + dtc_state = MILSTATUS; + } + } + break; + + default: + break; + } +} diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 0ed8987..5f2f83a 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -96,7 +96,7 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) sendCommand_Blocking(ALLOW_LONG_MESSAGES); delay(100); - // Set data timeout + // // Set data timeout sprintf(command, SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); sendCommand_Blocking(command); delay(100); @@ -383,7 +383,7 @@ int8_t ELM327::nextIndex(char const *str, float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { uint8_t numExpectedPayChars = numExpectedBytes * 2; - uint8_t payCharDiff = numPayChars - numExpectedPayChars; + uint8_t payCharDiff = numPayChars - numExpectedPayChars; if (numExpectedBytes > 8) { @@ -419,10 +419,10 @@ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &sc if (debugMode) Serial.println("Looking for lagging zeros"); - uint16_t numExpectedBits = numExpectedBytes * 8; + uint16_t numExpectedBits = numExpectedBytes * 8; uint64_t laggingZerosMask = 0; - for (uint16_t i=0; iavailable()) - { - nb_rx_state = ELM_GETTING_MSG; - if (timeout()) - nb_rx_state = ELM_TIMEOUT; - } - else - { - char recChar = elm_port->read(); - - if (debugMode) - { - Serial.print(F("\tReceived char: ")); - // display each received character, make non-printables printable - if (recChar == '\f') - Serial.println(F("\\f")); - else if (recChar == '\n') - Serial.println(F("\\n")); - else if (recChar == '\r') - Serial.println(F("\\r")); - else if (recChar == '\t') - Serial.println(F("\\t")); - else if (recChar == '\v') - Serial.println(F("\\v")); - // convert spaces to underscore, easier to see in debug output - else if (recChar == ' ') - Serial.println("_"); - // display regular printable - else - Serial.println(recChar); - } - - // this is the end of the OBD response - if (recChar == '>') - { - if (debugMode) - Serial.println(F("Delimiter found.")); - - nb_rx_state = ELM_MSG_RXD; - } - else if (!isalnum(recChar) && (recChar != ':') && (recChar != '.')) - // discard all characters except for alphanumeric and decimal places. - // decimal places needed to extract floating point numbers, e.g. battery voltage - nb_rx_state = ELM_GETTING_MSG; // Discard this character - else - { - if (recBytes < PAYLOAD_LEN) - { - payload[recBytes] = recChar; - recBytes++; - nb_rx_state = ELM_GETTING_MSG; - } - else - nb_rx_state = ELM_BUFFER_OVERFLOW; - } - } - - // Message is still being received (or is timing out), so exit early without doing all the other checks - if (nb_rx_state == ELM_GETTING_MSG) - return nb_rx_state; - - // End of response delimiter was found - if (debugMode && nb_rx_state == ELM_MSG_RXD) - { - Serial.print(F("All chars received: ")); - Serial.println(payload); - } - - if (nb_rx_state == ELM_TIMEOUT) - { - if (debugMode) - { - Serial.print(F("Timeout detected with overflow of ")); - Serial.print((currentTime - previousTime) - timeout_ms); - Serial.println(F("ms")); - } - return nb_rx_state; - } - - if (nb_rx_state == ELM_BUFFER_OVERFLOW) - { - if (debugMode) - { - Serial.print(F("OBD receive buffer overflow (> ")); - Serial.print(PAYLOAD_LEN); - Serial.println(F(" bytes)")); - } - return nb_rx_state; - } - - // Now we have successfully received OBD response, check if the payload indicates any OBD errors - if (nextIndex(payload, "UNABLETOCONNECT") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with error \"UNABLE TO CONNECT\"")); - - nb_rx_state = ELM_UNABLE_TO_CONNECT; - return nb_rx_state; - } - - connected = true; - - if (nextIndex(payload, "NODATA") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with error \"NO DATA\"")); - - nb_rx_state = ELM_NO_DATA; - return nb_rx_state; - } - - if (nextIndex(payload, "STOPPED") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with error \"STOPPED\"")); - - nb_rx_state = ELM_STOPPED; - return nb_rx_state; - } - - if (nextIndex(payload, "ERROR") >= 0) - { - if (debugMode) - Serial.println(F("ELM responded with \"ERROR\"")); - - nb_rx_state = ELM_GENERAL_ERROR; - return nb_rx_state; - } - - nb_rx_state = ELM_SUCCESS; - return nb_rx_state; + // buffer the response of the ELM327 until either the + // end marker is read or a timeout has occurred + // last valid idx is PAYLOAD_LEN but want to keep one free for terminating '\0' + // so limit counter to < PAYLOAD_LEN + if (!elm_port->available()) + { + nb_rx_state = ELM_GETTING_MSG; + if (timeout()) + nb_rx_state = ELM_TIMEOUT; + } + else + { + char recChar = elm_port->read(); + + if (debugMode) + { + Serial.print(F("\tReceived char: ")); + // display each received character, make non-printables printable + if (recChar == '\f') + Serial.println(F("\\f")); + else if (recChar == '\n') + Serial.println(F("\\n")); + else if (recChar == '\r') + Serial.println(F("\\r")); + else if (recChar == '\t') + Serial.println(F("\\t")); + else if (recChar == '\v') + Serial.println(F("\\v")); + // convert spaces to underscore, easier to see in debug output + else if (recChar == ' ') + Serial.println("_"); + // display regular printable + else + Serial.println(recChar); + } + + // this is the end of the OBD response + if (recChar == '>') + { + if (debugMode) + Serial.println(F("Delimiter found.")); + + nb_rx_state = ELM_MSG_RXD; + } + else if (!isalnum(recChar) && (recChar != ':') && (recChar != '.')) + // discard all characters except for alphanumeric and decimal places. + // decimal places needed to extract floating point numbers, e.g. battery voltage + nb_rx_state = ELM_GETTING_MSG; // Discard this character + else + { + if (recBytes < PAYLOAD_LEN) + { + payload[recBytes] = recChar; + recBytes++; + nb_rx_state = ELM_GETTING_MSG; + } + else + nb_rx_state = ELM_BUFFER_OVERFLOW; + } + } + + // Message is still being received (or is timing out), so exit early without doing all the other checks + if (nb_rx_state == ELM_GETTING_MSG) + return nb_rx_state; + + // End of response delimiter was found + if (debugMode && nb_rx_state == ELM_MSG_RXD) + { + Serial.print(F("All chars received: ")); + Serial.println(payload); + } + + if (nb_rx_state == ELM_TIMEOUT) + { + if (debugMode) + { + Serial.print(F("Timeout detected with overflow of ")); + Serial.print((currentTime - previousTime) - timeout_ms); + Serial.println(F("ms")); + } + return nb_rx_state; + } + + if (nb_rx_state == ELM_BUFFER_OVERFLOW) + { + if (debugMode) + { + Serial.print(F("OBD receive buffer overflow (> ")); + Serial.print(PAYLOAD_LEN); + Serial.println(F(" bytes)")); + } + return nb_rx_state; + } + + // Now we have successfully received OBD response, check if the payload indicates any OBD errors + if (nextIndex(payload, "UNABLETOCONNECT") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with error \"UNABLE TO CONNECT\"")); + + nb_rx_state = ELM_UNABLE_TO_CONNECT; + return nb_rx_state; + } + + connected = true; + + if (nextIndex(payload, "NODATA") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with error \"NO DATA\"")); + + nb_rx_state = ELM_NO_DATA; + return nb_rx_state; + } + + if (nextIndex(payload, "STOPPED") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with error \"STOPPED\"")); + + nb_rx_state = ELM_STOPPED; + return nb_rx_state; + } + + if (nextIndex(payload, "ERROR") >= 0) + { + if (debugMode) + Serial.println(F("ELM responded with \"ERROR\"")); + + nb_rx_state = ELM_GENERAL_ERROR; + return nb_rx_state; + } + + nb_rx_state = ELM_SUCCESS; + return nb_rx_state; } /* @@ -2554,21 +2554,206 @@ bool ELM327::resetDTC() { if (strstr(payload, "44") != NULL) { - if(debugMode) + if (debugMode) { - Serial.println("ELMduino: DTC successfully reset."); + Serial.println("ELMduino: DTC successfully reset."); } - + return true; } } - else + else { - if(debugMode) + if (debugMode) { Serial.println("ELMduino: Resetting DTC codes failed."); } } - return false; -} \ No newline at end of file + return false; +} + +/* + void ELM327::currentDTCCodes() + + Description: + ------------ + * Get the list of current DTC codes. This method is blocking by default, but can be run + in non-blocking mode if desired with optional boolean argument. Typical use involves + calling the monitorStatus() function first to get the number of DTC current codes stored, + then calling this function to retrieve those codes. This would not typically + be done in NB mode in a loop, but optional NB mode is supported. + + * To check the results of this query, inspect the DTC_Response struct: DTC_Response.codesFound + will contain the number of codes present and DTC_Response.codes is an array + of 5 char codes that were retrieved. + + Inputs: + ------- + * bool isBlocking - optional arg to set (non)blocking mode - defaults to true / blocking mode + + Return: + ------- + * void +*/ +void ELM327::currentDTCCodes(const bool &isBlocking) +{ + char *idx; + char codeType = '\0'; + char codeNumber[5] = {0}; + char temp[6] = {0}; + + if (isBlocking) // In blocking mode, we loop here until get_response() is past ELM_GETTING_MSG state + { + sendCommand("03"); // Check DTC is always Service 03 with no PID + while (get_response() == ELM_GETTING_MSG) + ; + } + else + { + if (nb_query_state == SEND_COMMAND) + { + sendCommand("03"); + nb_query_state = WAITING_RESP; + } + + else if (nb_query_state == WAITING_RESP) + { + get_response(); + } + } + + if (nb_rx_state == ELM_SUCCESS) + { + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + memset(DTC_Response.codes, 0, DTC_CODE_LEN * DTC_MAX_CODES); + + if (strstr(payload, "43") != NULL) // Successful response to Mode 03 request + { + // OBD scanner will provide a response that contains one or more lines indicating the codes present. + // Each response line will start with "43" indicating it is a response to a Mode 03 request. + // See p. 31 of ELM327 datasheet for details and lookup table of code types. + + uint codesFound = strlen(payload) / 8; // Each code found returns 8 chars starting with "43" + idx = strstr(payload, "43") + 4; // Pointer to first DTC code digit (third char in the response) + + if (codesFound > DTC_MAX_CODES) // I don't think the ELM is capable of returning + { // more than 0xF (16) codes, but just in case... + codesFound = DTC_MAX_CODES; + Serial.print("DTC response truncated at "); + Serial.print(DTC_MAX_CODES); + Serial.println(" codes."); + } + + DTC_Response.codesFound = codesFound; + + for (int i = 0; i < codesFound; i++) + { + memset(temp, 0, sizeof(temp)); + memset(codeNumber, 0, sizeof(codeNumber)); + + codeType = *idx; // Get first digit of second byte + codeNumber[0] = *(idx + 1); // Get second digit of second byte + codeNumber[1] = *(idx + 2); // Get first digit of third byte + codeNumber[2] = *(idx + 3); // Get second digit of third byte + + switch (codeType) // Set the correct type prefix for the code + { + case '0': + strcat(temp, "P0"); + break; + + case '1': + strcat(temp, "P1"); + break; + + case '2': + strcat(temp, "P2"); + break; + case '3': + strcat(temp, "P3"); + break; + + case '4': + strcat(temp, "C0"); + break; + + case '5': + strcat(temp, "C1"); + break; + + case '6': + strcat(temp, "C2"); + break; + + case '7': + strcat(temp, "C3"); + break; + + case '8': + strcat(temp, "B0"); + break; + + case '9': + strcat(temp, "B1"); + break; + + case 'A': + strcat(temp, "B2"); + break; + + case 'B': + strcat(temp, "B3"); + break; + + case 'C': + strcat(temp, "U0"); + break; + + case 'D': + strcat(temp, "U1"); + break; + + case 'E': + strcat(temp, "U2"); + break; + + case 'F': + strcat(temp, "U3"); + break; + + default: + break; + } + + strcat(temp, codeNumber); // Append the code number to the prefix + strcpy(DTC_Response.codes[i], temp); // Add the fully parsed code to the list (array) + idx = idx + 8; // reset idx to start of next code + + if (debugMode) + { + Serial.print("ELMduino: Found code: "); + Serial.println(temp); + } + } + } + else + { + if (debugMode) + { + Serial.println("ELMduino: DTC response received with no valid data."); + } + } + return; + } + else if (nb_rx_state != ELM_GETTING_MSG) + { + nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command + + if (debugMode) + { + Serial.println("ELMduino: Getting current DTC codes failed."); + printError(); + } + } +} diff --git a/src/ELMduino.h b/src/ELMduino.h index deea4c1..aeb408e 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -138,7 +138,7 @@ const uint8_t AUX_INPUT_OUTPUT_SUPPORTED = 101; // 0x65 - bit encoded const uint8_t SERVICE_02 = 2; - +const uint8_t SERVICE_03 = 3; @@ -271,7 +271,8 @@ const int8_t ELM_TIMEOUT = 7; const int8_t ELM_GETTING_MSG = 8; const int8_t ELM_MSG_RXD = 9; const int8_t ELM_GENERAL_ERROR = -1; - +const int8_t DTC_CODE_LEN = 6; +const int8_t DTC_MAX_CODES = 16; // Non-blocking (NB) command states typedef enum { SEND_COMMAND, @@ -281,6 +282,8 @@ typedef enum { SEND_COMMAND, ERROR } obd_cmd_states; + + class ELM327 { public: @@ -304,9 +307,11 @@ class ELM327 byte responseByte_6; byte responseByte_7; - - - + struct dtcResponse { + uint codesFound = 0; + char codes[DTC_MAX_CODES][DTC_CODE_LEN]; + } DTC_Response; + bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40, const byte& dataTimeout = 0); bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); void flushInputBuff(); @@ -323,6 +328,7 @@ class ELM327 float batteryVoltage(void); int8_t get_vin_blocking(char vin[]); bool resetDTC(); + void currentDTCCodes(const bool& isBlocking = true); uint32_t supportedPIDs_1_20(); @@ -370,7 +376,6 @@ class ELM327 float catTempB1S2(); float catTempB2S2(); - uint32_t supportedPIDs_41_60(); uint32_t monitorDriveCycleStatus(); From c77adceebaa590450f5588d3690ee49a2c9acf05 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 11 Dec 2023 17:18:55 -0700 Subject: [PATCH 43/88] MIL_status_example (#201) * Added example program to check MIL (Check Engine Light) status and number of current DTC codes. --- examples/ESP32_Check_MIL/ESP32_Check_MIL.ino | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 examples/ESP32_Check_MIL/ESP32_Check_MIL.ino diff --git a/examples/ESP32_Check_MIL/ESP32_Check_MIL.ino b/examples/ESP32_Check_MIL/ESP32_Check_MIL.ino new file mode 100644 index 0000000..1cfaaf7 --- /dev/null +++ b/examples/ESP32_Check_MIL/ESP32_Check_MIL.ino @@ -0,0 +1,79 @@ +#include "ELMduino.h" +#include + +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +typedef enum +{ MILSTATUS, + DTCCODES +} dtc_states; + +BluetoothSerial SerialBT; +ELM327 myELM327; +dtc_states dtc_state = MILSTATUS; +uint8_t numCodes = 0; +uint8_t milStatus =0; + +// This example program demonstrates how to use ELMduino to check the MIL (Check Engine Light) +// status and the number of current DTC codes that are present. + +void setup() +{ + DEBUG_PORT.begin(115200); + ELM_PORT.begin("ArduHUD", true); + ELM_PORT.setPin("1234"); + + DEBUG_PORT.println("Starting connection..."); + if (!ELM_PORT.connect("ELMULATOR")) + { + DEBUG_PORT.println("Couldn't connect to ELM327 - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT)) + { + DEBUG_PORT.println("ELM327 Couldn't connect to ECU - Phase 2"); + while (1) + ; + } + + DEBUG_PORT.println("Connected to ELM327"); + +} + +void loop() +{ + myELM327.monitorStatus(); // Gets the number of current DTC codes present + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + // We are only interested in the third byte of the response that + // encodes the MIL status and number of codes present + milStatus = (myELM327.responseByte_2 & 0x80); + numCodes = (myELM327.responseByte_2 - 0x80); + + DEBUG_PORT.print("MIL (Check Engine Light) is: "); + if (milStatus) + { + DEBUG_PORT.println("ON."); + } + else + { + DEBUG_PORT.println("OFF."); + } + + DEBUG_PORT.print("Number of codes present: "); + DEBUG_PORT.println(numCodes); + + DEBUG_PORT.println("Checking again in 10 seconds.\n\n"); + delay(10000); // Wait 10 seconds before we query again. + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + DEBUG_PORT.println("Trying again in 10 seconds.\n\n"); + delay(10000); + } +} From 7768476afc0d7217d6f7bbfdf44222c5a685850d Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Tue, 12 Dec 2023 22:07:34 -0500 Subject: [PATCH 44/88] Fix bug when formatting query string --- src/ELMduino.cpp | 54 +++++++++++++++++++++++++++++++++++++----------- src/ELMduino.h | 2 +- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 5c8fd5c..133add1 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -208,14 +208,27 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons longQuery = true; - query[2] = ((pid >> 12) & 0xF) + '0'; - query[3] = ((pid >> 8) & 0xF) + '0'; - query[4] = ((pid >> 4) & 0xF) + '0'; - query[5] = (pid & 0xF) + '0'; - query[6] = num_responses + '0'; - query[7] = '\0'; - - upper(query, 6); + query[2] = ((pid >> 12) & 0xF) + '0'; + query[3] = ((pid >> 8) & 0xF) + '0'; + query[4] = ((pid >> 4) & 0xF) + '0'; + query[5] = (pid & 0xF) + '0'; + + if (num_responses > 0xF) + { + query[6] = ((num_responses >> 4) & 0xF) + '0'; + query[7] = ( num_responses & 0xF) + '0'; + query[8] = '\0'; + + upper(query, 8); + } + else + { + query[6] = (num_responses & 0xF) + '0'; + query[7] = '\0'; + query[8] = '\0'; + + upper(query, 7); + } } else { @@ -225,11 +238,28 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons longQuery = false; query[2] = ((pid >> 4) & 0xF) + '0'; - query[3] = (pid & 0xF) + '0'; - query[4] = num_responses + '0'; - query[5] = '\0'; + query[3] = (pid & 0xF) + '0'; - upper(query, 4); + if (num_responses > 0xF) + { + query[4] = ((num_responses >> 4) & 0xF) + '0'; + query[5] = ( num_responses & 0xF) + '0'; + query[6] = '\0'; + query[7] = '\0'; + query[8] = '\0'; + + upper(query, 6); + } + else + { + query[4] = (num_responses & 0xF) + '0'; + query[5] = '\0'; + query[6] = '\0'; + query[7] = '\0'; + query[8] = '\0'; + + upper(query, 5); + } } if (debugMode) diff --git a/src/ELMduino.h b/src/ELMduino.h index 9eb934d..4b1c6c0 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -259,7 +259,7 @@ const char * const RESET_ALL = "AT Z"; // General // Class constants //-------------------------------------------------------------------------------------// const float KPH_MPH_CONVERT = 0.6213711922; -const int8_t QUERY_LEN = 8; +const int8_t QUERY_LEN = 9; const int8_t ELM_SUCCESS = 0; const int8_t ELM_NO_RESPONSE = 1; const int8_t ELM_BUFFER_OVERFLOW = 2; From 5198be67b224dc6b5559d2042d6dfbef27763c35 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Sat, 16 Dec 2023 15:23:42 -0700 Subject: [PATCH 45/88] Fix for #203 batteryVoltage() returns zero (#204) * Added currentDTCCodes() method - works for a single code * Preparing for mulitple code responses * First pass at multiple response support * Implemented non-blocking version. * Simplified code, implemented loop * Modifications to support both blocking and non-blocking * Fixed up check for number of codes present. * Added bluetooth PIN * Updates for actual returned DTC code format * Working to fetch codes. Needs fix for parsing. * Fixes for parsing codes correctly. Next task, update foundCodes[] array properly. * Working with foundCodes arrray now. Next do cleanup on debug messages * Removed no longer needed debug statements * Working as designed. * Moved dtcResponse into ELMduino class * Updated to get correct byte of monitorStatus() response * Added MIL Status reporting * Removed need to pass numCodes to currentDTCCodes() * Added blocking call to currentDTCCodes() * Updated comments * Added method to check / retrieve current DTCs * Added example program to check MIL (Check Engine Light) status and number of current DTC codes. * fix for batteryVoltage() returns zero #203 --- .../ESP32_Check_Voltage.ino | 52 +++++++++++++++++++ src/ELMduino.cpp | 11 ++-- 2 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 examples/ESP32_Check_Voltage/ESP32_Check_Voltage.ino diff --git a/examples/ESP32_Check_Voltage/ESP32_Check_Voltage.ino b/examples/ESP32_Check_Voltage/ESP32_Check_Voltage.ino new file mode 100644 index 0000000..06bb5ab --- /dev/null +++ b/examples/ESP32_Check_Voltage/ESP32_Check_Voltage.ino @@ -0,0 +1,52 @@ +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; + +void setup() +{ +#if LED_BUILTIN + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); +#endif + + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect("OBDII")) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + Serial.println("Couldn't connect to OBD scanner - Phase 2"); + while (1) + ; + } + + Serial.println("Connected to ELM327"); +} + +void loop() +{ + float volts = myELM327.batteryVoltage(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + Serial.print("Battery Voltage: "); + Serial.println(volts); + delay(10000); + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + } +} diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index c5112f3..69c84cf 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2476,7 +2476,8 @@ float ELM327::batteryVoltage(void) get_response(); if (nb_rx_state == ELM_SUCCESS) { - nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + payload[strlen(payload) - 1] = '\0'; // remove the last char ("V") from the payload value return (float)strtod(payload, NULL); } else if (nb_rx_state != ELM_GETTING_MSG) @@ -2613,10 +2614,10 @@ bool ELM327::resetDTC() calling the monitorStatus() function first to get the number of DTC current codes stored, then calling this function to retrieve those codes. This would not typically be done in NB mode in a loop, but optional NB mode is supported. - + * To check the results of this query, inspect the DTC_Response struct: DTC_Response.codesFound will contain the number of codes present and DTC_Response.codes is an array - of 5 char codes that were retrieved. + of 5 char codes that were retrieved. Inputs: ------- @@ -2663,11 +2664,11 @@ void ELM327::currentDTCCodes(const bool &isBlocking) // OBD scanner will provide a response that contains one or more lines indicating the codes present. // Each response line will start with "43" indicating it is a response to a Mode 03 request. // See p. 31 of ELM327 datasheet for details and lookup table of code types. - + uint codesFound = strlen(payload) / 8; // Each code found returns 8 chars starting with "43" idx = strstr(payload, "43") + 4; // Pointer to first DTC code digit (third char in the response) - if (codesFound > DTC_MAX_CODES) // I don't think the ELM is capable of returning + if (codesFound > DTC_MAX_CODES) // I don't think the ELM is capable of returning { // more than 0xF (16) codes, but just in case... codesFound = DTC_MAX_CODES; Serial.print("DTC response truncated at "); From 7a411f2ae5cc61f69766647934e896d698a51d28 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Sat, 16 Dec 2023 22:57:46 -0700 Subject: [PATCH 46/88] Fix for issue myELM327.supportedPIDs_1_20() not working as expected #157 --- .../ESP32_CheckPIDs_1_20.ino | 53 +++++++++++++++++++ src/ELMduino.cpp | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 examples/ESP32_CheckPIDs_1_20/ESP32_CheckPIDs_1_20.ino diff --git a/examples/ESP32_CheckPIDs_1_20/ESP32_CheckPIDs_1_20.ino b/examples/ESP32_CheckPIDs_1_20/ESP32_CheckPIDs_1_20.ino new file mode 100644 index 0000000..735bbda --- /dev/null +++ b/examples/ESP32_CheckPIDs_1_20/ESP32_CheckPIDs_1_20.ino @@ -0,0 +1,53 @@ +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; + +uint32_t rpm = 0; + +void setup() +{ +#if LED_BUILTIN + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); +#endif + + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect("OBDII")) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + Serial.println("Couldn't connect to OBD scanner - Phase 2"); + while (1) + ; + } + + Serial.println("Connected to ELM327"); +} + +void loop() +{ + uint32_t pids = myELM327.supportedPIDs_1_20(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + Serial.print("Supported PIDS: "); Serial.println(pids); + delay(10000); + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + } +} diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 69c84cf..ac57041 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2359,7 +2359,7 @@ uint64_t ELM327::findResponse() { uint8_t payloadIndex = firstDatum + i; uint8_t bitsOffset = 4 * (numPayChars - i - 1); - response = response | (ctoi(payload[payloadIndex]) << bitsOffset); + response = response | ((uint64_t)ctoi(payload[payloadIndex]) << bitsOffset); } // It is useful to have the response bytes From 38194223c7c00d96385d351575bbb368b590f70e Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 20 Dec 2023 22:49:42 -0500 Subject: [PATCH 47/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 9dbe4f3..1cb73ba 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.1.0 +version=3.2.0 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From ae3319229ba0b39478c15b785a7ccab169a151c7 Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 20 Dec 2023 22:53:39 -0500 Subject: [PATCH 48/88] Update with multiple PIDs example and up-to-date API --- README.md | 115 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9aa8d19..efd16c2 100644 --- a/README.md +++ b/README.md @@ -19,41 +19,124 @@ Just to be clear, do not try to query more than one PID at a time. You must wait # Example Code: ```C++ -#include #include "ELMduino.h" -SoftwareSerial mySerial(2, 3); // RX, TX + + +#define ELM_PORT Serial1 + + + + +const bool DEBUG = true; +const int TIMEOUT = 2000; +const bool HALT_ON_FAIL = false; + + + + ELM327 myELM327; -uint32_t rpm = 0; + + +typedef enum { ENG_RPM, + SPEED } obd_pid_states; +obd_pid_states obd_state = ENG_RPM; + +float rpm = 0; +float mph = 0; + + void setup() { Serial.begin(115200); - mySerial.begin(115200); - myELM327.begin(mySerial, true, 2000); + ELM_PORT.begin(115200); + + Serial.println("Attempting to connect to ELM327..."); + + if (!myELM327.begin(ELM_PORT, DEBUG, TIMEOUT)) + { + Serial.println("Couldn't connect to OBD scanner"); + + if (HALT_ON_FAIL) + while (1); + } + + Serial.println("Connected to ELM327"); } + + void loop() { - float tempRPM = myELM327.rpm(); - - if (myELM327.nb_rx_state == ELM_SUCCESS) + switch (obd_state) { - rpm = (uint32_t)tempRPM; - Serial.print("RPM: "); Serial.println(rpm); + case ENG_RPM: + { + rpm = myELM327.rpm(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + Serial.print("rpm: "); + Serial.println(rpm); + obd_state = SPEED; + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + obd_state = SPEED; + } + + break; + } + + case SPEED: + { + mph = myELM327.mph(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + Serial.print("mph: "); + Serial.println(mph); + obd_state = ENG_RPM; + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + obd_state = ENG_RPM; + } + + break; + } } - else if (myELM327.nb_rx_state != ELM_GETTING_MSG) - myELM327.printError(); } ``` # List of Supported OBD PID Processing Functions: ```C++ +bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40, const byte& dataTimeout = 0); +bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); +void flushInputBuff(); +uint64_t findResponse(); +bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); +bool queryPID(char queryStr[]); +float processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); +void sendCommand(const char *cmd); +int8_t sendCommand_Blocking(const char *cmd); +int8_t get_response(); +bool timeout(); +float conditionResponse(const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + +float batteryVoltage(void); +int8_t get_vin_blocking(char vin[]); +bool resetDTC(); +void currentDTCCodes(const bool& isBlocking = true); + uint32_t supportedPIDs_1_20(); uint32_t monitorStatus(); @@ -100,7 +183,6 @@ float catTempB2S1(); float catTempB1S2(); float catTempB2S2(); - uint32_t supportedPIDs_41_60(); uint32_t monitorDriveCycleStatus(); @@ -137,12 +219,7 @@ float demandedTorque(); float torque(); uint16_t referenceTorque(); uint16_t auxSupported(); -``` - -# Other commands -```C++ -float batteryVoltage(void); // Gets vehicle battery voltage -int8_t get_vin_blocking(char vin[]); // Gets Vehicle Identification Number (VIN) +void printError(); ``` # List of OBD Protocols: From 881decc27d00413389ff55148ed8b91d9c8c047f Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Sat, 30 Dec 2023 20:38:50 -0700 Subject: [PATCH 49/88] Adds convenience method to query if a particular PID is supported by the ECU. --- .../ESP32_Check_PID_Support.ino | 59 +++++++++++++++++++ src/ELMduino.cpp | 56 ++++++++++++++++++ src/ELMduino.h | 3 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 examples/ESP32_Check_PID_Support/ESP32_Check_PID_Support.ino diff --git a/examples/ESP32_Check_PID_Support/ESP32_Check_PID_Support.ino b/examples/ESP32_Check_PID_Support/ESP32_Check_PID_Support.ino new file mode 100644 index 0000000..d6d74a2 --- /dev/null +++ b/examples/ESP32_Check_PID_Support/ESP32_Check_PID_Support.ino @@ -0,0 +1,59 @@ +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; + +uint32_t rpm = 0; + +void setup() +{ +#if LED_BUILTIN + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); +#endif + + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect("ELMULATOR")) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 2"); + while (1) + ; + } + + Serial.println("Connected to ELM327"); +} + +void loop() +{ + uint8_t pid = ENGINE_RPM; + bool pidOK = myELM327.isPidSupported(pid); + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + if(pidOK) { + Serial.print("PID "); Serial.print(pid); Serial.println(" is supported"); + } + else + { + Serial.print("PID "); Serial.print(pid); Serial.println(" is not supported"); + } + delay(10000); + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + } +} diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index ac57041..0ae0f69 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2788,3 +2788,59 @@ void ELM327::currentDTCCodes(const bool &isBlocking) } } } + + +/* + bool ELM327::isPidSupported(uint8_t pid) + + Description: + ------------ + * Checks if a particular PID is supported by the connected ECU. + + * This is a convenience method that selects the correct supportedPIDS_xx_xx() query and parses + the bit-encoded result, returning a simple Boolean value indicating PID support from the ECU. + + Inputs: + ------- + * uint8_t pid - the PID to check for support. + + Return: + ------- + * bool - Whether or not the queried PID is supported by the ECU. +*/ +bool ELM327::isPidSupported(uint8_t pid) +{ + uint8_t pidInterval = (pid / PID_INTERVAL_OFFSET) * PID_INTERVAL_OFFSET; + uint8_t checkPid = 0; + + switch (pidInterval) + { + case SUPPORTED_PIDS_1_20: + supportedPIDs_1_20(); + break; + + case SUPPORTED_PIDS_21_40: + supportedPIDs_21_40(); + pid = (pid - SUPPORTED_PIDS_21_40); + break; + + case SUPPORTED_PIDS_41_60: + supportedPIDs_41_60(); + pid = (pid - SUPPORTED_PIDS_41_60); + break; + + case SUPPORTED_PIDS_61_80: + supportedPIDs_61_80(); + pid = (pid - SUPPORTED_PIDS_61_80); + break; + + default: + break; + } + + if (nb_rx_state == ELM_SUCCESS) + { + return ((response >> (32 - pid)) & 0x1); + } + return false; +} \ No newline at end of file diff --git a/src/ELMduino.h b/src/ELMduino.h index 863d7f2..dec93fc 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -139,7 +139,7 @@ const uint8_t AUX_INPUT_OUTPUT_SUPPORTED = 101; // 0x65 - bit encoded const uint8_t SERVICE_02 = 2; const uint8_t SERVICE_03 = 3; - +const uint8_t PID_INTERVAL_OFFSET = 0x20; //-------------------------------------------------------------------------------------// @@ -329,6 +329,7 @@ class ELM327 int8_t get_vin_blocking(char vin[]); bool resetDTC(); void currentDTCCodes(const bool& isBlocking = true); + bool isPidSupported(uint8_t pid); uint32_t supportedPIDs_1_20(); From b3784832dd004921b016e3de8fb4ffc4d255caf7 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 1 Jan 2024 00:49:01 -0700 Subject: [PATCH 50/88] Fix for Value returned by supportedPIDS_xx_xx() methods is off by one bit. #211 Changes processPID() and conditionResponse() to return double instead of float to preserve all bits of _response_. --- src/ELMduino.cpp | 38 +++++++++++++++++++++++++++++++------- src/ELMduino.h | 4 ++-- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 0ae0f69..3e8e395 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -410,7 +410,7 @@ int8_t ELM327::nextIndex(char const *str, ------- * float - Converted numerical value */ -float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) +double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { uint8_t numExpectedPayChars = numExpectedBytes * 2; uint8_t payCharDiff = numPayChars - numExpectedPayChars; @@ -438,7 +438,17 @@ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &sc return 0; } else if (numExpectedPayChars == numPayChars) - return (response * scaleFactor) + bias; + { + if (scaleFactor == 1 && bias == 0) // No scale/bias needed + { + return response; + } + else + { + return (response * scaleFactor) + bias; + } + } + // If there were more payload bytes returned than we expected, test the first and last bytes in the // returned payload and see which gives us a higher value. Sometimes ELM327's return leading zeros @@ -459,15 +469,29 @@ float ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &sc { if (debugMode) Serial.println("Lagging zeros found"); - - return ((float)(response >> (4 * payCharDiff)) * scaleFactor) + bias; + if (scaleFactor == 1 && bias == 0) // No scale/bias needed + { + return (response >> (4 * payCharDiff)); + } + else + { + return ((float)(response >> (4 * payCharDiff)) * scaleFactor) + bias; + } + } else { if (debugMode) Serial.println("Lagging zeros not found - assuming leading zeros"); - return (response * scaleFactor) + bias; + if (scaleFactor == 1 && bias == 0) // No scale/bias needed + { + return response; + } + else + { + return (response * scaleFactor) + bias; + } } } @@ -573,7 +597,7 @@ bool ELM327::queryPID(char queryStr[]) ------- * float - The PID value if successfully received, else 0.0 */ -float ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) +double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) { if (nb_query_state == SEND_COMMAND) { @@ -588,7 +612,7 @@ float ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint nb_query_state = SEND_COMMAND; // Reset the query state machine for next command findResponse(); - + return conditionResponse(numExpectedBytes, scaleFactor, bias); } else if (nb_rx_state != ELM_GETTING_MSG) diff --git a/src/ELMduino.h b/src/ELMduino.h index dec93fc..b7f63ec 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -318,12 +318,12 @@ class ELM327 uint64_t findResponse(); bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); bool queryPID(char queryStr[]); - float processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); void sendCommand(const char *cmd); int8_t sendCommand_Blocking(const char *cmd); int8_t get_response(); bool timeout(); - float conditionResponse(const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + double conditionResponse(const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); float batteryVoltage(void); int8_t get_vin_blocking(char vin[]); From d95dc1941777927771b379f1bab1798357ed33f4 Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 2 Jan 2024 02:38:20 -0500 Subject: [PATCH 51/88] Fix typecast --- src/ELMduino.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 3e8e395..f794553 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -475,7 +475,7 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &s } else { - return ((float)(response >> (4 * payCharDiff)) * scaleFactor) + bias; + return ((double)(response >> (4 * payCharDiff)) * scaleFactor) + bias; } } @@ -2867,4 +2867,4 @@ bool ELM327::isPidSupported(uint8_t pid) return ((response >> (32 - pid)) & 0x1); } return false; -} \ No newline at end of file +} From a2d71ba589d60b7064f790aafa49958a2b8dc0f9 Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 2 Jan 2024 02:39:12 -0500 Subject: [PATCH 52/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 1cb73ba..b5ebdbd 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.2.0 +version=3.2.1 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From f501c2e8f1ea9051117bf016d599f4a6c754f207 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Fri, 5 Jan 2024 02:30:53 -0700 Subject: [PATCH 53/88] Update for fix for #211 --- src/ELMduino.cpp | 12 ++++++------ src/ELMduino.h | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index f794553..f0f41b9 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -393,7 +393,7 @@ int8_t ELM327::nextIndex(char const *str, } /* - void ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) + double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) Description: ------------ @@ -408,9 +408,9 @@ int8_t ELM327::nextIndex(char const *str, Return: ------- - * float - Converted numerical value + * double - Converted numerical value */ -double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) +double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double &scaleFactor, const float &bias) { uint8_t numExpectedPayChars = numExpectedBytes * 2; uint8_t payCharDiff = numPayChars - numExpectedPayChars; @@ -475,7 +475,7 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &s } else { - return ((double)(response >> (4 * payCharDiff)) * scaleFactor) + bias; + return ((response >> (4 * payCharDiff)) * scaleFactor) + bias; } } @@ -575,7 +575,7 @@ bool ELM327::queryPID(char queryStr[]) } /* - float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) + double ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) Description: ------------ @@ -597,7 +597,7 @@ bool ELM327::queryPID(char queryStr[]) ------- * float - The PID value if successfully received, else 0.0 */ -double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) +double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const double &scaleFactor, const float &bias) { if (nb_query_state == SEND_COMMAND) { diff --git a/src/ELMduino.h b/src/ELMduino.h index b7f63ec..4b9f8da 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -318,12 +318,12 @@ class ELM327 uint64_t findResponse(); bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); bool queryPID(char queryStr[]); - double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); void sendCommand(const char *cmd); int8_t sendCommand_Blocking(const char *cmd); int8_t get_response(); bool timeout(); - double conditionResponse(const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + double conditionResponse(const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); float batteryVoltage(void); int8_t get_vin_blocking(char vin[]); From 8361ca797bffeecee9ef6eb9005f3472da4b99ee Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Fri, 5 Jan 2024 02:49:03 -0700 Subject: [PATCH 54/88] Squashed commit of the following: commit f501c2e8f1ea9051117bf016d599f4a6c754f207 Author: Jim Whitelaw Date: Fri Jan 5 02:30:53 2024 -0700 Update for fix for #211 --- src/ELMduino.cpp | 12 ++++++------ src/ELMduino.h | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index f794553..f0f41b9 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -393,7 +393,7 @@ int8_t ELM327::nextIndex(char const *str, } /* - void ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) + double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) Description: ------------ @@ -408,9 +408,9 @@ int8_t ELM327::nextIndex(char const *str, Return: ------- - * float - Converted numerical value + * double - Converted numerical value */ -double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) +double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double &scaleFactor, const float &bias) { uint8_t numExpectedPayChars = numExpectedBytes * 2; uint8_t payCharDiff = numPayChars - numExpectedPayChars; @@ -475,7 +475,7 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &s } else { - return ((double)(response >> (4 * payCharDiff)) * scaleFactor) + bias; + return ((response >> (4 * payCharDiff)) * scaleFactor) + bias; } } @@ -575,7 +575,7 @@ bool ELM327::queryPID(char queryStr[]) } /* - float ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) + double ELM327::processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor, const float& bias) Description: ------------ @@ -597,7 +597,7 @@ bool ELM327::queryPID(char queryStr[]) ------- * float - The PID value if successfully received, else 0.0 */ -double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) +double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const double &scaleFactor, const float &bias) { if (nb_query_state == SEND_COMMAND) { diff --git a/src/ELMduino.h b/src/ELMduino.h index b7f63ec..4b9f8da 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -318,12 +318,12 @@ class ELM327 uint64_t findResponse(); bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); bool queryPID(char queryStr[]); - double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); void sendCommand(const char *cmd); int8_t sendCommand_Blocking(const char *cmd); int8_t get_response(); bool timeout(); - double conditionResponse(const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); + double conditionResponse(const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); float batteryVoltage(void); int8_t get_vin_blocking(char vin[]); From 17cc8b3ce75495e05345a627fa4073582fcf8b5b Mon Sep 17 00:00:00 2001 From: PB2 Date: Fri, 5 Jan 2024 05:42:41 -0800 Subject: [PATCH 55/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index b5ebdbd..8744c12 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.2.1 +version=3.2.2 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From c0d0da37248b6c58f0d137acf120d7554ff94fb0 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 8 Jan 2024 21:27:56 -0700 Subject: [PATCH 56/88] Fix for compile error re: unsigned int #200 --- src/ELMduino.cpp | 2 +- src/ELMduino.h | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index f0f41b9..825b1ea 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2689,7 +2689,7 @@ void ELM327::currentDTCCodes(const bool &isBlocking) // Each response line will start with "43" indicating it is a response to a Mode 03 request. // See p. 31 of ELM327 datasheet for details and lookup table of code types. - uint codesFound = strlen(payload) / 8; // Each code found returns 8 chars starting with "43" + uint8_t codesFound = strlen(payload) / 8; // Each code found returns 8 chars starting with "43" idx = strstr(payload, "43") + 4; // Pointer to first DTC code digit (third char in the response) if (codesFound > DTC_MAX_CODES) // I don't think the ELM is capable of returning diff --git a/src/ELMduino.h b/src/ELMduino.h index 4b9f8da..8040dc1 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -271,8 +271,8 @@ const int8_t ELM_TIMEOUT = 7; const int8_t ELM_GETTING_MSG = 8; const int8_t ELM_MSG_RXD = 9; const int8_t ELM_GENERAL_ERROR = -1; -const int8_t DTC_CODE_LEN = 6; -const int8_t DTC_MAX_CODES = 16; +const uint8_t DTC_CODE_LEN = 6; +const uint8_t DTC_MAX_CODES = 16; // Non-blocking (NB) command states typedef enum { SEND_COMMAND, @@ -308,7 +308,7 @@ class ELM327 byte responseByte_7; struct dtcResponse { - uint codesFound = 0; + uint8_t codesFound = 0; char codes[DTC_MAX_CODES][DTC_CODE_LEN]; } DTC_Response; From 4c6c8f7940dfbbfe5d3defa3ea2bd2ca0a52220c Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 9 Jan 2024 07:40:28 -0800 Subject: [PATCH 57/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 8744c12..7614628 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.2.2 +version=3.2.3 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 7b2de3c397b04e79a57697b892a75e6b212e5fe3 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Wed, 17 Jan 2024 12:15:57 -0700 Subject: [PATCH 58/88] Fix for batteryVoltage returns 0 with some ELM327 devices --- src/ELMduino.cpp | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 825b1ea..873b844 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -208,15 +208,15 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons longQuery = true; - query[2] = ((pid >> 12) & 0xF) + '0'; - query[3] = ((pid >> 8) & 0xF) + '0'; - query[4] = ((pid >> 4) & 0xF) + '0'; - query[5] = (pid & 0xF) + '0'; + query[2] = ((pid >> 12) & 0xF) + '0'; + query[3] = ((pid >> 8) & 0xF) + '0'; + query[4] = ((pid >> 4) & 0xF) + '0'; + query[5] = (pid & 0xF) + '0'; if (num_responses > 0xF) { query[6] = ((num_responses >> 4) & 0xF) + '0'; - query[7] = ( num_responses & 0xF) + '0'; + query[7] = (num_responses & 0xF) + '0'; query[8] = '\0'; upper(query, 8); @@ -238,12 +238,12 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons longQuery = false; query[2] = ((pid >> 4) & 0xF) + '0'; - query[3] = (pid & 0xF) + '0'; + query[3] = (pid & 0xF) + '0'; if (num_responses > 0xF) { query[4] = ((num_responses >> 4) & 0xF) + '0'; - query[5] = ( num_responses & 0xF) + '0'; + query[5] = (num_responses & 0xF) + '0'; query[6] = '\0'; query[7] = '\0'; query[8] = '\0'; @@ -443,12 +443,11 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double & { return response; } - else + else { return (response * scaleFactor) + bias; } } - // If there were more payload bytes returned than we expected, test the first and last bytes in the // returned payload and see which gives us a higher value. Sometimes ELM327's return leading zeros @@ -473,11 +472,10 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double & { return (response >> (4 * payCharDiff)); } - else + else { return ((response >> (4 * payCharDiff)) * scaleFactor) + bias; } - } else { @@ -488,7 +486,7 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double & { return response; } - else + else { return (response * scaleFactor) + bias; } @@ -612,7 +610,7 @@ double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uin nb_query_state = SEND_COMMAND; // Reset the query state machine for next command findResponse(); - + return conditionResponse(numExpectedBytes, scaleFactor, bias); } else if (nb_rx_state != ELM_GETTING_MSG) @@ -2500,8 +2498,13 @@ float ELM327::batteryVoltage(void) get_response(); if (nb_rx_state == ELM_SUCCESS) { - nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - payload[strlen(payload) - 1] = '\0'; // remove the last char ("V") from the payload value + nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + payload[strlen(payload) - 1] = '\0'; // Remove the last char ("V") from the payload value + char *start = strstr(payload, "ATRV"); // Get start of voltage value + if (start != NULL) + { + payload = start + 4; + } return (float)strtod(payload, NULL); } else if (nb_rx_state != ELM_GETTING_MSG) @@ -2690,10 +2693,10 @@ void ELM327::currentDTCCodes(const bool &isBlocking) // See p. 31 of ELM327 datasheet for details and lookup table of code types. uint8_t codesFound = strlen(payload) / 8; // Each code found returns 8 chars starting with "43" - idx = strstr(payload, "43") + 4; // Pointer to first DTC code digit (third char in the response) + idx = strstr(payload, "43") + 4; // Pointer to first DTC code digit (third char in the response) - if (codesFound > DTC_MAX_CODES) // I don't think the ELM is capable of returning - { // more than 0xF (16) codes, but just in case... + if (codesFound > DTC_MAX_CODES) // I don't think the ELM is capable of returning + { // more than 0xF (16) codes, but just in case... codesFound = DTC_MAX_CODES; Serial.print("DTC response truncated at "); Serial.print(DTC_MAX_CODES); @@ -2813,7 +2816,6 @@ void ELM327::currentDTCCodes(const bool &isBlocking) } } - /* bool ELM327::isPidSupported(uint8_t pid) @@ -2826,11 +2828,11 @@ void ELM327::currentDTCCodes(const bool &isBlocking) Inputs: ------- - * uint8_t pid - the PID to check for support. + * uint8_t pid - the PID to check for support. Return: ------- - * bool - Whether or not the queried PID is supported by the ECU. + * bool - Whether or not the queried PID is supported by the ECU. */ bool ELM327::isPidSupported(uint8_t pid) { @@ -2857,10 +2859,10 @@ bool ELM327::isPidSupported(uint8_t pid) supportedPIDs_61_80(); pid = (pid - SUPPORTED_PIDS_61_80); break; - + default: break; - } + } if (nb_rx_state == ELM_SUCCESS) { From 77874c0d13e262e59a15211c49642ecc5eb34e28 Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 17 Jan 2024 14:05:15 -0800 Subject: [PATCH 59/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 7614628..1906b4b 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.2.3 +version=3.2.4 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 35760840152b29106b599b036648280fff49c288 Mon Sep 17 00:00:00 2001 From: PB2 Date: Mon, 29 Jan 2024 14:18:51 -0800 Subject: [PATCH 60/88] Attempt to remedy #220 when 0100 command returns more data than can be handled --- src/ELMduino.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 873b844..dd264c4 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -117,13 +117,20 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) // the timeout value to 30 seconds, then restore the previous value. uint16_t prevTimeout = timeout_ms; timeout_ms = 30000; + + int8_t state = sendCommand_Blocking("0100"); - if (sendCommand_Blocking("0100") == ELM_SUCCESS) + if (state == ELM_SUCCESS) { timeout_ms = prevTimeout; connected = true; return connected; } + else if (state == ELM_BUFFER_OVERFLOW) + { + while (elm_port->available()) + elm_port->read(); + } timeout_ms = prevTimeout; } From 77ad96edb7957f49cdce61f4e863f67a6f143bb0 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Wed, 31 Jan 2024 16:44:22 -0700 Subject: [PATCH 61/88] Fix for Custom PIDs #218 Mode 0x22 response headers always zero-pad the pid to 4 chars, even for a 2-char pid. That confused the header detection and response parsing. This change checks for the mode and pid in findResponse() and adjusts the expected header accordingly. --- src/ELMduino.cpp | 26 +++++++++++++++++++------- src/ELMduino.h | 3 ++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index dd264c4..4c59631 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -117,7 +117,7 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) // the timeout value to 30 seconds, then restore the previous value. uint16_t prevTimeout = timeout_ms; timeout_ms = 30000; - + int8_t state = sendCommand_Blocking("0100"); if (state == ELM_SUCCESS) @@ -203,6 +203,8 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons Serial.println(pid); } + isMode0x22Query = (service == 0x22 && pid <= 0xFF); // mode 0x22 responses always zero-pad the pid to 4 chars, even for a 2-char pid + query[0] = ((service >> 4) & 0xF) + '0'; query[1] = (service & 0xF) + '0'; @@ -616,7 +618,7 @@ double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uin { nb_query_state = SEND_COMMAND; // Reset the query state machine for next command - findResponse(); + findResponse(service, pid); return conditionResponse(numExpectedBytes, scaleFactor, bias); } @@ -2312,7 +2314,7 @@ int8_t ELM327::get_response(void) } /* - uint64_t ELM327::findResponse() + uint64_t ELM327::findResponse(uint8_t &service) Description: ------------ @@ -2326,7 +2328,7 @@ int8_t ELM327::get_response(void) ------- * uint64_t - Query response value */ -uint64_t ELM327::findResponse() +uint64_t ELM327::findResponse(const uint8_t &service, const uint8_t &pid) { uint8_t firstDatum = 0; char header[7] = {'\0'}; @@ -2344,8 +2346,18 @@ uint64_t ELM327::findResponse() { header[0] = query[0] + 4; header[1] = query[1]; - header[2] = query[2]; - header[3] = query[3]; + if (isMode0x22Query) // mode 0x22 responses always zero-pad the pid to 4 chars, even for a 2-char pid + { + header[2] = '0'; + header[3] = '0'; + header[4] = query[2]; + header[5] = query[3]; + } + else + { + header[2] = query[2]; + header[3] = query[3]; + } } if (debugMode) @@ -2359,7 +2371,7 @@ uint64_t ELM327::findResponse() if (firstHeadIndex >= 0) { - if (longQuery) + if (longQuery | isMode0x22Query) firstDatum = firstHeadIndex + 6; else firstDatum = firstHeadIndex + 4; diff --git a/src/ELMduino.h b/src/ELMduino.h index 8040dc1..5516b5a 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -315,7 +315,7 @@ class ELM327 bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40, const byte& dataTimeout = 0); bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); void flushInputBuff(); - uint64_t findResponse(); + uint64_t findResponse(const uint8_t &service, const uint8_t &pid); bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); bool queryPID(char queryStr[]); double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); @@ -421,6 +421,7 @@ class ELM327 private: char query[QUERY_LEN] = { '\0' }; bool longQuery = false; + bool isMode0x22Query = false; uint32_t currentTime; uint32_t previousTime; From 2338fe5b2756ce9dc18e4e68d8a3f866f39b6cf6 Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 6 Feb 2024 20:25:04 -0800 Subject: [PATCH 62/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 1906b4b..49203db 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.2.4 +version=3.2.5 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From fdfbe4c3c245ae7785cded22666832510834799b Mon Sep 17 00:00:00 2001 From: PB2 Date: Mon, 26 Feb 2024 12:36:28 -0500 Subject: [PATCH 63/88] Rm unused variable - fix #227 --- src/ELMduino.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 4c59631..1e94151 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2856,7 +2856,6 @@ void ELM327::currentDTCCodes(const bool &isBlocking) bool ELM327::isPidSupported(uint8_t pid) { uint8_t pidInterval = (pid / PID_INTERVAL_OFFSET) * PID_INTERVAL_OFFSET; - uint8_t checkPid = 0; switch (pidInterval) { From bdd3db6b2f124c2fab168329e321c88a9433d46f Mon Sep 17 00:00:00 2001 From: Tiny_Spider Date: Mon, 18 Mar 2024 18:46:08 +0100 Subject: [PATCH 64/88] Use the F() macro wherever possible, organize header file and add ELM327 responses to header (#236) --- src/ELMduino.cpp | 50 +++++++++++++++++++++++++----------------------- src/ELMduino.h | 14 ++++++++++---- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 1e94151..4978860 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -111,7 +111,7 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) sprintf(command, SET_PROTOCOL_TO_AUTO_H_SAVE, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) { - if (strstr(payload, "OK") != NULL) + if (strstr(payload, RESPONSE_OK) != NULL) { // Protocol search can take a comparatively long time. Temporarily set // the timeout value to 30 seconds, then restore the previous value. @@ -143,7 +143,7 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) if (sendCommand_Blocking(command) == ELM_SUCCESS) { - if (strstr(payload, "OK") != NULL) + if (strstr(payload, RESPONSE_OK) != NULL) { connected = true; return connected; @@ -163,7 +163,7 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) - if (strstr(payload, "OK") != NULL) + if (strstr(payload, RESPONSE_OK) != NULL) connected = true; if (debugMode) @@ -465,7 +465,7 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double & // will not give accurate results! if (debugMode) - Serial.println("Looking for lagging zeros"); + Serial.println(F("Looking for lagging zeros")); uint16_t numExpectedBits = numExpectedBytes * 8; uint64_t laggingZerosMask = 0; @@ -476,7 +476,8 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double & if (!(laggingZerosMask & response)) // Detect all lagging zeros in `response` { if (debugMode) - Serial.println("Lagging zeros found"); + Serial.println(F("Lagging zeros found")); + if (scaleFactor == 1 && bias == 0) // No scale/bias needed { return (response >> (4 * payCharDiff)); @@ -489,7 +490,7 @@ double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double & else { if (debugMode) - Serial.println("Lagging zeros not found - assuming leading zeros"); + Serial.println(F("Lagging zeros not found - assuming leading zeros")); if (scaleFactor == 1 && bias == 0) // No scale/bias needed { @@ -2206,7 +2207,7 @@ int8_t ELM327::get_response(void) Serial.println(F("\\v")); // convert spaces to underscore, easier to see in debug output else if (recChar == ' ') - Serial.println("_"); + Serial.println(F("_")); // display regular printable else Serial.println(recChar); @@ -2271,7 +2272,7 @@ int8_t ELM327::get_response(void) } // Now we have successfully received OBD response, check if the payload indicates any OBD errors - if (nextIndex(payload, "UNABLETOCONNECT") >= 0) + if (nextIndex(payload, RESPONSE_UNABLE_TO_CONNECT) >= 0) { if (debugMode) Serial.println(F("ELM responded with error \"UNABLE TO CONNECT\"")); @@ -2282,7 +2283,7 @@ int8_t ELM327::get_response(void) connected = true; - if (nextIndex(payload, "NODATA") >= 0) + if (nextIndex(payload, RESPONSE_NO_DATA) >= 0) { if (debugMode) Serial.println(F("ELM responded with error \"NO DATA\"")); @@ -2291,7 +2292,7 @@ int8_t ELM327::get_response(void) return nb_rx_state; } - if (nextIndex(payload, "STOPPED") >= 0) + if (nextIndex(payload, RESPONSE_STOPPED) >= 0) { if (debugMode) Serial.println(F("ELM responded with error \"STOPPED\"")); @@ -2300,7 +2301,7 @@ int8_t ELM327::get_response(void) return nb_rx_state; } - if (nextIndex(payload, "ERROR") >= 0) + if (nextIndex(payload, RESPONSE_ERROR) >= 0) { if (debugMode) Serial.println(F("ELM responded with \"ERROR\"")); @@ -2491,7 +2492,7 @@ void ELM327::printError() } /* - float ELM327::batteryVoltage(void) + float ELM327::batteryVoltage() Description: ------------ @@ -2503,9 +2504,9 @@ void ELM327::printError() Return: ------- - * flaot - vehicle battery voltage in VDC + * float - vehicle battery voltage in VDC */ -float ELM327::batteryVoltage(void) +float ELM327::batteryVoltage() { if (nb_query_state == SEND_COMMAND) { @@ -2556,7 +2557,8 @@ int8_t ELM327::get_vin_blocking(char vin[]) uint8_t ascii_val; if (debugMode) - Serial.println("Getting VIN..."); + Serial.println(F("Getting VIN...")); + sendCommand("0902"); // VIN is command 0902 while (get_response() == ELM_GETTING_MSG) ; @@ -2592,7 +2594,7 @@ int8_t ELM327::get_vin_blocking(char vin[]) } if (debugMode) { - Serial.print("VIN: "); + Serial.print(F("VIN: ")); Serial.println(vin); } } @@ -2600,7 +2602,7 @@ int8_t ELM327::get_vin_blocking(char vin[]) { if (debugMode) { - Serial.println("No VIN response"); + Serial.println(F("No VIN response")); printError(); } } @@ -2633,7 +2635,7 @@ bool ELM327::resetDTC() { if (debugMode) { - Serial.println("ELMduino: DTC successfully reset."); + Serial.println(F("ELMduino: DTC successfully reset.")); } return true; @@ -2643,7 +2645,7 @@ bool ELM327::resetDTC() { if (debugMode) { - Serial.println("ELMduino: Resetting DTC codes failed."); + Serial.println(F("ELMduino: Resetting DTC codes failed.")); } } @@ -2717,9 +2719,9 @@ void ELM327::currentDTCCodes(const bool &isBlocking) if (codesFound > DTC_MAX_CODES) // I don't think the ELM is capable of returning { // more than 0xF (16) codes, but just in case... codesFound = DTC_MAX_CODES; - Serial.print("DTC response truncated at "); + Serial.print(F("DTC response truncated at ")); Serial.print(DTC_MAX_CODES); - Serial.println(" codes."); + Serial.println(F(" codes.")); } DTC_Response.codesFound = codesFound; @@ -2809,7 +2811,7 @@ void ELM327::currentDTCCodes(const bool &isBlocking) if (debugMode) { - Serial.print("ELMduino: Found code: "); + Serial.print(F("ELMduino: Found code: ")); Serial.println(temp); } } @@ -2818,7 +2820,7 @@ void ELM327::currentDTCCodes(const bool &isBlocking) { if (debugMode) { - Serial.println("ELMduino: DTC response received with no valid data."); + Serial.println(F("ELMduino: DTC response received with no valid data.")); } } return; @@ -2829,7 +2831,7 @@ void ELM327::currentDTCCodes(const bool &isBlocking) if (debugMode) { - Serial.println("ELMduino: Getting current DTC codes failed."); + Serial.println(F("ELMduino: Getting current DTC codes failed.")); printError(); } } diff --git a/src/ELMduino.h b/src/ELMduino.h index 5516b5a..1f420ec 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -28,7 +28,10 @@ const char USER_2_CAN = 'C'; // PIDs (https://en.wikipedia.org/wiki/OBD-II_PIDs) //-------------------------------------------------------------------------------------// const uint8_t SERVICE_01 = 1; - +const uint8_t SERVICE_02 = 2; +const uint8_t SERVICE_03 = 3; +const uint8_t PID_INTERVAL_OFFSET = 0x20; + const uint8_t SUPPORTED_PIDS_1_20 = 0; // 0x00 - bit encoded const uint8_t MONITOR_STATUS_SINCE_DTC_CLEARED = 1; // 0x01 - bit encoded @@ -137,9 +140,6 @@ const uint8_t ENGINE_PERCENT_TORQUE_DATA = 100; // 0x64 - % const uint8_t AUX_INPUT_OUTPUT_SUPPORTED = 101; // 0x65 - bit encoded -const uint8_t SERVICE_02 = 2; -const uint8_t SERVICE_03 = 3; -const uint8_t PID_INTERVAL_OFFSET = 0x20; //-------------------------------------------------------------------------------------// @@ -274,6 +274,12 @@ const int8_t ELM_GENERAL_ERROR = -1; const uint8_t DTC_CODE_LEN = 6; const uint8_t DTC_MAX_CODES = 16; +const char * const RESPONSE_OK = "OK"; +const char * const RESPONSE_UNABLE_TO_CONNECT = "UNABLETOCONNECT"; +const char * const RESPONSE_NO_DATA = "NODATA"; +const char * const RESPONSE_STOPPED = "STOPPED"; +const char * const RESPONSE_ERROR = "ERROR"; + // Non-blocking (NB) command states typedef enum { SEND_COMMAND, WAITING_RESP, From 0e8a94316dae85d69cecf92494a09333dafcdc27 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Mon, 18 Mar 2024 13:05:57 -0600 Subject: [PATCH 65/88] Refactor to try to avoid crash bug in ESP-IDF #226 --- src/ELMduino.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 4c59631..e10369d 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2503,7 +2503,7 @@ void ELM327::printError() Return: ------- - * flaot - vehicle battery voltage in VDC + * float - vehicle battery voltage in VDC */ float ELM327::batteryVoltage(void) { @@ -2519,12 +2519,14 @@ float ELM327::batteryVoltage(void) { nb_query_state = SEND_COMMAND; // Reset the query state machine for next command payload[strlen(payload) - 1] = '\0'; // Remove the last char ("V") from the payload value - char *start = strstr(payload, "ATRV"); // Get start of voltage value - if (start != NULL) + if (strncmp(payload, "ATRV", 4) == 0) { - payload = start + 4; + return (float)strtod(payload + 4, NULL); + } + else + { + return (float)strtod(payload, NULL); } - return (float)strtod(payload, NULL); } else if (nb_rx_state != ELM_GETTING_MSG) nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command From e81f308098188baf2e51f1f32a94cd3783377c7f Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Thu, 28 Mar 2024 21:51:38 -0600 Subject: [PATCH 66/88] New example sketch for custom headers (#239) --- .../ESP32_CustomHeader/ESP32_CustomHeader.ino | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 examples/ESP32_CustomHeader/ESP32_CustomHeader.ino diff --git a/examples/ESP32_CustomHeader/ESP32_CustomHeader.ino b/examples/ESP32_CustomHeader/ESP32_CustomHeader.ino new file mode 100644 index 0000000..a47b5cd --- /dev/null +++ b/examples/ESP32_CustomHeader/ESP32_CustomHeader.ino @@ -0,0 +1,91 @@ +/* + +This example shows how to perform several opearations for using custom (non-OBD2 standard) +queries. + +Custom Header +We perform a query that requires a custom header to be sent first. The custom header in this +case determines which ECU in the vehicle's system we want to query. + +Custom PID +The query sent to the vehicle is also a custom (non OBD2 standard) PID using the 0x22 +(enhanced data) mode. + +Manually Processing Response +In this example, we manually extract the data value from the query response and perform +some post-processing to calculate the correct value. + +Managing Query State +We also demonstrate managing the query state used by the loop() method. This is typically +managed internally by ELMduino for standard PID methods. + +Customize all the things! +The header value, PID, data value bytes and adjustment formula are generally unique for +each different vehicle and PID. You will need to source the correct values for those for +your specific vehicle. This example almost certainly will not work for you "as-is". + +*/ + +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; + +int nb_query_state = SEND_COMMAND; // Set the inital query state ready to send a command + +void setup() +{ + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect("OBDII")) + { + DEBUG_PORT.println("Couldn't connect to ELM327 device."); + while (1); + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + Serial.println("ELM327 device couldn't connect to ECU."); + while (1); + } + + Serial.println("Connected to ELM327"); + + // Set a custom header using ATSH command. + myELM327.sendCommand_Blocking("ATSH DA18F1"); +} + +void loop() +{ + if (nb_query_state == SEND_COMMAND) // We are ready to send a new command + { + myELM327.sendCommand("2204FE"); // Send the custom PID commnad + nb_query_state = WAITING_RESP; // Set the query state so we are waiting for response + } + else if (nb_query_state == WAITING_RESP) // Our query has been sent, check for a response + { + myELM327.get_response(); // Each time through the loop we will check again + } + + if (myELM327.nb_rx_state == ELM_SUCCESS) // Our response is fully received, let's get our data + { + int A = myELM327.payload[6]; // Parse the temp value A from the response + float temperature = A - 40; // Apply the formula for the temperature + Serial.println(temperature); // Print the adjusted value + nb_query_state = SEND_COMMAND; // Reset the query state for the next command + delay(5000); // Wait 5 seconds until we query again + } + + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { // If state == ELM_GETTING_MSG, response is not yet complete. Restart the loop. + nb_query_state = SEND_COMMAND; // Reset the query state for the next command + myELM327.printError(); + delay(5000); // Wait 5 seconds until we query again + } +} \ No newline at end of file From 3949290ce1fdbee5cfa1d69208fece14223999ad Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Fri, 12 Apr 2024 21:30:39 -0600 Subject: [PATCH 67/88] Debug output will erroneously declare defeat in the face of victory. --- src/ELMduino.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index ed0c56a..abffc90 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -163,9 +163,14 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) + { if (strstr(payload, RESPONSE_OK) != NULL) + { connected = true; - + return connected; + } + } + if (debugMode) { Serial.print(F("Setting protocol via ")); From ac5043115751f120c518d553562b797d1cb7cb3e Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Tue, 16 Apr 2024 15:50:35 -0400 Subject: [PATCH 68/88] Add property that lets the user control whether or not to specify the number of responses in PID queries --- src/ELMduino.cpp | 56 ++++++++++++++++++++++++++++++++---------------- src/ELMduino.h | 1 + 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index abffc90..756b619 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -227,21 +227,30 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons query[4] = ((pid >> 4) & 0xF) + '0'; query[5] = (pid & 0xF) + '0'; - if (num_responses > 0xF) + if (specifyNumResponses) { - query[6] = ((num_responses >> 4) & 0xF) + '0'; - query[7] = (num_responses & 0xF) + '0'; - query[8] = '\0'; + if (num_responses > 0xF) + { + query[6] = ((num_responses >> 4) & 0xF) + '0'; + query[7] = (num_responses & 0xF) + '0'; + query[8] = '\0'; + + upper(query, 8); + } + else + { + query[6] = (num_responses & 0xF) + '0'; + query[7] = '\0'; + query[8] = '\0'; - upper(query, 8); + upper(query, 7); + } } else { - query[6] = (num_responses & 0xF) + '0'; + query[6] = '\0'; query[7] = '\0'; query[8] = '\0'; - - upper(query, 7); } } else @@ -254,25 +263,36 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons query[2] = ((pid >> 4) & 0xF) + '0'; query[3] = (pid & 0xF) + '0'; - if (num_responses > 0xF) + if (specifyNumResponses) { - query[4] = ((num_responses >> 4) & 0xF) + '0'; - query[5] = (num_responses & 0xF) + '0'; - query[6] = '\0'; - query[7] = '\0'; - query[8] = '\0'; + if (num_responses > 0xF) + { + query[4] = ((num_responses >> 4) & 0xF) + '0'; + query[5] = (num_responses & 0xF) + '0'; + query[6] = '\0'; + query[7] = '\0'; + query[8] = '\0'; - upper(query, 6); + upper(query, 6); + } + else + { + query[4] = (num_responses & 0xF) + '0'; + query[5] = '\0'; + query[6] = '\0'; + query[7] = '\0'; + query[8] = '\0'; + + upper(query, 5); + } } else { - query[4] = (num_responses & 0xF) + '0'; + query[4] = '\0'; query[5] = '\0'; query[6] = '\0'; query[7] = '\0'; query[8] = '\0'; - - upper(query, 5); } } diff --git a/src/ELMduino.h b/src/ELMduino.h index 1f420ec..b0759a6 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -296,6 +296,7 @@ class ELM327 Stream* elm_port; bool connected = false; + bool specifyNumResponses = true; bool debugMode; char* payload; uint16_t PAYLOAD_LEN; From 3d290ef6b19d6f172fdd1620717ae93b6f92f784 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Wed, 17 Apr 2024 11:53:46 -0600 Subject: [PATCH 69/88] Fix for query formatting when specifyNumReaponses == false. --- src/ELMduino.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 756b619..a046339 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -251,6 +251,8 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons query[6] = '\0'; query[7] = '\0'; query[8] = '\0'; + + upper(query, 6); } } else @@ -293,6 +295,8 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons query[6] = '\0'; query[7] = '\0'; query[8] = '\0'; + + upper(query, 4); } } From af150fcb444da7c61024404abe4fa76a4208d301 Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 17 Apr 2024 15:09:49 -0700 Subject: [PATCH 70/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 49203db..8772ac4 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.2.5 +version=3.3.0 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 530067befdcc902cced0aca3da07b0aa16f9614a Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Thu, 18 Apr 2024 00:02:03 -0600 Subject: [PATCH 71/88] Example sketch to query multiple pids with a Bluetooth connection --- .../ESP32_Bluetooth_multiple_pids.ino | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 examples/ESP32_Bluetooth_multiple_pids/ESP32_Bluetooth_multiple_pids.ino diff --git a/examples/ESP32_Bluetooth_multiple_pids/ESP32_Bluetooth_multiple_pids.ino b/examples/ESP32_Bluetooth_multiple_pids/ESP32_Bluetooth_multiple_pids.ino new file mode 100644 index 0000000..4668de2 --- /dev/null +++ b/examples/ESP32_Bluetooth_multiple_pids/ESP32_Bluetooth_multiple_pids.ino @@ -0,0 +1,82 @@ +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; + +typedef enum { ENG_RPM, + SPEED } obd_pid_states; +obd_pid_states obd_state = ENG_RPM; + +float rpm = 0; +float mph = 0; + +void setup() +{ + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect("OBDII")) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 2"); + while (1) + ; + } + + DEBUG_PORT.println("Connected to ELM327"); +} + +void loop() +{ + switch (obd_state) + { + case ENG_RPM: + { + rpm = myELM327.rpm(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + DEBUG_PORT.print("rpm: "); + DEBUG_PORT.println(rpm); + obd_state = SPEED; + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + obd_state = SPEED; + } + + break; + } + + case SPEED: + { + mph = myELM327.mph(); + + if (myELM327.nb_rx_state == ELM_SUCCESS) + { + DEBUG_PORT.print("mph: "); + DEBUG_PORT.println(mph); + obd_state = ENG_RPM; + } + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { + myELM327.printError(); + obd_state = ENG_RPM; + } + + break; + } + } +} From 892176905d4981f612ef12e70e58c5f10f931d5a Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Fri, 17 May 2024 22:06:03 -0600 Subject: [PATCH 72/88] Fixes for issue #248. - change return type for queryPID() methods - update code comments for queryPID() - update README document - update code comments for conditionResponse() --- README.md | 4 ++-- src/ELMduino.cpp | 20 ++++++++------------ src/ELMduino.h | 4 ++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index efd16c2..7a50b20 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); void flushInputBuff(); uint64_t findResponse(); -bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); -bool queryPID(char queryStr[]); +void queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); +void queryPID(char queryStr[]); float processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); void sendCommand(const char *cmd); int8_t sendCommand_Blocking(const char *cmd); diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index a046339..f2aff29 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -441,7 +441,7 @@ int8_t ELM327::nextIndex(char const *str, ------- * uint64_t response - ELM327's response * uint8_t numExpectedBytes - Number of valid bytes from the response to process - * float scaleFactor - Amount to scale the response by + * double scaleFactor - Amount to scale the response by * float bias - Amount to bias the response by Return: @@ -557,7 +557,7 @@ void ELM327::flushInputBuff() } /* - bool ELM327::queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses) + void ELM327::queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses) Description: ------------ @@ -574,18 +574,16 @@ void ELM327::flushInputBuff() Return: ------- - * bool - Whether or not the query was submitted successfully + * void */ -bool ELM327::queryPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses) +void ELM327::queryPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses) { formatQueryArray(service, pid, num_responses); sendCommand(query); - - return connected; } /* - bool ELM327::queryPID(char queryStr[]) + void ELM327::queryPID(char queryStr[]) Description: ------------ @@ -597,9 +595,9 @@ bool ELM327::queryPID(const uint8_t &service, const uint16_t &pid, const uint8_t Return: ------- - * bool - Whether or not the query was submitted successfully + * void */ -bool ELM327::queryPID(char queryStr[]) +void ELM327::queryPID(char queryStr[]) { if (strlen(queryStr) <= 4) longQuery = false; @@ -607,8 +605,6 @@ bool ELM327::queryPID(char queryStr[]) longQuery = true; sendCommand(queryStr); - - return connected; } /* @@ -632,7 +628,7 @@ bool ELM327::queryPID(char queryStr[]) Return: ------- - * float - The PID value if successfully received, else 0.0 + * double - The PID value if successfully received, else 0.0 */ double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const double &scaleFactor, const float &bias) { diff --git a/src/ELMduino.h b/src/ELMduino.h index b0759a6..9d36855 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -323,8 +323,8 @@ class ELM327 bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); void flushInputBuff(); uint64_t findResponse(const uint8_t &service, const uint8_t &pid); - bool queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); - bool queryPID(char queryStr[]); + void queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); + void queryPID(char queryStr[]); double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); void sendCommand(const char *cmd); int8_t sendCommand_Blocking(const char *cmd); From 036a6f7c374381d9e7e051a7222df20065e1ac7f Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Tue, 18 Feb 2025 10:14:05 -0500 Subject: [PATCH 73/88] Fix formatting --- src/ELMduino.cpp | 43 +++++-- src/ELMduino.h | 317 +++++++++++++++++++++++++---------------------- 2 files changed, 199 insertions(+), 161 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index f2aff29..59528a7 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -20,7 +20,12 @@ ------- * bool - Whether or not the ELM327 was properly initialized */ -bool ELM327::begin(Stream &stream, const bool &debug, const uint16_t &timeout, const char &protocol, const uint16_t &payloadLen, const byte &dataTimeout) +bool ELM327::begin( Stream& stream, + const bool& debug, + const uint16_t& timeout, + const char& protocol, + const uint16_t& payloadLen, + const byte& dataTimeout) { elm_port = &stream; PAYLOAD_LEN = payloadLen; @@ -76,7 +81,8 @@ bool ELM327::begin(Stream &stream, const bool &debug, const uint16_t &timeout, c * --> *user adjustable */ -bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) +bool ELM327::initializeELM(const char& protocol, + const byte& dataTimeout) { char command[10] = {'\0'}; connected = false; @@ -198,7 +204,9 @@ bool ELM327::initializeELM(const char &protocol, const byte &dataTimeout) ------- * void */ -void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses) +void ELM327::formatQueryArray(const uint8_t& service, + const uint16_t& pid, + const uint8_t& num_responses) { if (debugMode) { @@ -324,7 +332,8 @@ void ELM327::formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_respons ------- * void */ -void ELM327::upper(char string[], uint8_t buflen) +void ELM327::upper(char string[], + uint8_t buflen) { for (uint8_t i = 0; i < buflen; i++) { @@ -405,7 +414,7 @@ uint8_t ELM327::ctoi(uint8_t value) */ int8_t ELM327::nextIndex(char const *str, char const *target, - uint8_t numOccur = 1) + uint8_t numOccur) { char const *p = str; char const *r = str; @@ -448,7 +457,9 @@ int8_t ELM327::nextIndex(char const *str, ------- * double - Converted numerical value */ -double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const double &scaleFactor, const float &bias) +double ELM327::conditionResponse(const uint8_t& numExpectedBytes, + const double& scaleFactor, + const float& bias) { uint8_t numExpectedPayChars = numExpectedBytes * 2; uint8_t payCharDiff = numPayChars - numExpectedPayChars; @@ -576,7 +587,9 @@ void ELM327::flushInputBuff() ------- * void */ -void ELM327::queryPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses) +void ELM327::queryPID(const uint8_t& service, + const uint16_t& pid, + const uint8_t& num_responses) { formatQueryArray(service, pid, num_responses); sendCommand(query); @@ -630,7 +643,12 @@ void ELM327::queryPID(char queryStr[]) ------- * double - The PID value if successfully received, else 0.0 */ -double ELM327::processPID(const uint8_t &service, const uint16_t &pid, const uint8_t &num_responses, const uint8_t &numExpectedBytes, const double &scaleFactor, const float &bias) +double ELM327::processPID(const uint8_t& service, + const uint16_t& pid, + const uint8_t& num_responses, + const uint8_t& numExpectedBytes, + const double& scaleFactor, + const float& bias) { if (nb_query_state == SEND_COMMAND) { @@ -2340,7 +2358,7 @@ int8_t ELM327::get_response(void) } /* - uint64_t ELM327::findResponse(uint8_t &service) + uint64_t ELM327::findResponse(const uint8_t& service, const uint8_t& pid) Description: ------------ @@ -2354,7 +2372,8 @@ int8_t ELM327::get_response(void) ------- * uint64_t - Query response value */ -uint64_t ELM327::findResponse(const uint8_t &service, const uint8_t &pid) +uint64_t ELM327::findResponse(const uint8_t& service, + const uint8_t& pid) { uint8_t firstDatum = 0; char header[7] = {'\0'}; @@ -2680,7 +2699,7 @@ bool ELM327::resetDTC() } /* - void ELM327::currentDTCCodes() + void ELM327::currentDTCCodes(const bool& isBlocking) Description: ------------ @@ -2702,7 +2721,7 @@ bool ELM327::resetDTC() ------- * void */ -void ELM327::currentDTCCodes(const bool &isBlocking) +void ELM327::currentDTCCodes(const bool& isBlocking) { char *idx; char codeType = '\0'; diff --git a/src/ELMduino.h b/src/ELMduino.h index 9d36855..ce0d87a 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -30,7 +30,7 @@ const char USER_2_CAN = 'C'; const uint8_t SERVICE_01 = 1; const uint8_t SERVICE_02 = 2; const uint8_t SERVICE_03 = 3; -const uint8_t PID_INTERVAL_OFFSET = 0x20; +const uint8_t PID_INTERVAL_OFFSET = 0x20; const uint8_t SUPPORTED_PIDS_1_20 = 0; // 0x00 - bit encoded @@ -259,7 +259,7 @@ const char * const RESET_ALL = "AT Z"; // General // Class constants //-------------------------------------------------------------------------------------// const float KPH_MPH_CONVERT = 0.6213711922; -const int8_t QUERY_LEN = 9; +const int8_t QUERY_LEN = 9; const int8_t ELM_SUCCESS = 0; const int8_t ELM_NO_RESPONSE = 1; const int8_t ELM_BUFFER_OVERFLOW = 2; @@ -271,14 +271,14 @@ const int8_t ELM_TIMEOUT = 7; const int8_t ELM_GETTING_MSG = 8; const int8_t ELM_MSG_RXD = 9; const int8_t ELM_GENERAL_ERROR = -1; -const uint8_t DTC_CODE_LEN = 6; -const uint8_t DTC_MAX_CODES = 16; +const uint8_t DTC_CODE_LEN = 6; +const uint8_t DTC_MAX_CODES = 16; -const char * const RESPONSE_OK = "OK"; -const char * const RESPONSE_UNABLE_TO_CONNECT = "UNABLETOCONNECT"; -const char * const RESPONSE_NO_DATA = "NODATA"; -const char * const RESPONSE_STOPPED = "STOPPED"; -const char * const RESPONSE_ERROR = "ERROR"; +const char * const RESPONSE_OK = "OK"; +const char * const RESPONSE_UNABLE_TO_CONNECT = "UNABLETOCONNECT"; +const char * const RESPONSE_NO_DATA = "NODATA"; +const char * const RESPONSE_STOPPED = "STOPPED"; +const char * const RESPONSE_ERROR = "ERROR"; // Non-blocking (NB) command states typedef enum { SEND_COMMAND, @@ -293,151 +293,170 @@ typedef enum { SEND_COMMAND, class ELM327 { public: - Stream* elm_port; + Stream* elm_port; - bool connected = false; + bool connected = false; bool specifyNumResponses = true; - bool debugMode; - char* payload; - uint16_t PAYLOAD_LEN; - int8_t nb_rx_state = ELM_GETTING_MSG; - uint64_t response; - uint16_t recBytes; - uint8_t numPayChars; - uint16_t timeout_ms; - byte responseByte_0; - byte responseByte_1; - byte responseByte_2; - byte responseByte_3; - byte responseByte_4; - byte responseByte_5; - byte responseByte_6; - byte responseByte_7; - - struct dtcResponse { - uint8_t codesFound = 0; - char codes[DTC_MAX_CODES][DTC_CODE_LEN]; - } DTC_Response; - - bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40, const byte& dataTimeout = 0); - bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); - void flushInputBuff(); - uint64_t findResponse(const uint8_t &service, const uint8_t &pid); - void queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); - void queryPID(char queryStr[]); - double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); - void sendCommand(const char *cmd); - int8_t sendCommand_Blocking(const char *cmd); - int8_t get_response(); - bool timeout(); - double conditionResponse(const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); - - float batteryVoltage(void); - int8_t get_vin_blocking(char vin[]); - bool resetDTC(); - void currentDTCCodes(const bool& isBlocking = true); - bool isPidSupported(uint8_t pid); - - uint32_t supportedPIDs_1_20(); - - uint32_t monitorStatus(); - uint16_t freezeDTC(); - uint16_t fuelSystemStatus(); - float engineLoad(); - float engineCoolantTemp(); - float shortTermFuelTrimBank_1(); - float longTermFuelTrimBank_1(); - float shortTermFuelTrimBank_2(); - float longTermFuelTrimBank_2(); - float fuelPressure(); - uint8_t manifoldPressure(); - float rpm(); - int32_t kph(); - float mph(); - float timingAdvance(); - float intakeAirTemp(); - float mafRate(); - float throttle(); - uint8_t commandedSecAirStatus(); - uint8_t oxygenSensorsPresent_2banks(); - uint8_t obdStandards(); - uint8_t oxygenSensorsPresent_4banks(); - bool auxInputStatus(); - uint16_t runTime(); - - - uint32_t supportedPIDs_21_40(); - - uint16_t distTravelWithMIL(); - float fuelRailPressure(); - float fuelRailGuagePressure(); - float commandedEGR(); - float egrError(); - float commandedEvapPurge(); - float fuelLevel(); - uint8_t warmUpsSinceCodesCleared(); - uint16_t distSinceCodesCleared(); - float evapSysVapPressure(); - uint8_t absBaroPressure(); - float catTempB1S1(); - float catTempB2S1(); - float catTempB1S2(); - float catTempB2S2(); - - uint32_t supportedPIDs_41_60(); - - uint32_t monitorDriveCycleStatus(); - float ctrlModVoltage(); - float absLoad(); - float commandedAirFuelRatio(); - float relativeThrottle(); - float ambientAirTemp(); - float absThrottlePosB(); - float absThrottlePosC(); - float absThrottlePosD(); - float absThrottlePosE(); - float absThrottlePosF(); - float commandedThrottleActuator(); - uint16_t timeRunWithMIL(); - uint16_t timeSinceCodesCleared(); - float maxMafRate(); - uint8_t fuelType(); - float ethanolPercent(); - float absEvapSysVapPressure(); - float evapSysVapPressure2(); - float absFuelRailPressure(); - float relativePedalPos(); - float hybridBatLife(); - float oilTemp(); - float fuelInjectTiming(); - float fuelRate(); - uint8_t emissionRqmts(); - - - uint32_t supportedPIDs_61_80(); - - float demandedTorque(); - float torque(); - uint16_t referenceTorque(); - uint16_t auxSupported(); - void printError(); + bool debugMode; + char* payload; + uint16_t PAYLOAD_LEN; + int8_t nb_rx_state = ELM_GETTING_MSG; + uint64_t response; + uint16_t recBytes; + uint8_t numPayChars; + uint16_t timeout_ms; + byte responseByte_0; + byte responseByte_1; + byte responseByte_2; + byte responseByte_3; + byte responseByte_4; + byte responseByte_5; + byte responseByte_6; + byte responseByte_7; + + struct dtcResponse { + uint8_t codesFound = 0; + char codes[DTC_MAX_CODES][DTC_CODE_LEN]; + } DTC_Response; + + bool begin( Stream& stream, + const bool& debug = false, + const uint16_t& timeout = 1000, + const char& protocol = '0', + const uint16_t& payloadLen = 40, + const byte& dataTimeout = 0); + bool initializeELM(const char& protocol = '0', + const byte& dataTimeout = 0); + void flushInputBuff(); + virtual uint64_t findResponse(const uint8_t &service, + const uint8_t &pid); + void queryPID(const uint8_t& service, + const uint16_t& pid, + const uint8_t& num_responses = 1); + void queryPID(char queryStr[]); + double processPID(const uint8_t& service, + const uint16_t& pid, + const uint8_t& num_responses, + const uint8_t& numExpectedBytes, + const double& scaleFactor = 1, + const float& bias = 0); + void sendCommand(const char *cmd); + int8_t sendCommand_Blocking(const char *cmd); + int8_t get_response(); + bool timeout(); + double conditionResponse(const uint8_t& numExpectedBytes, + const double& scaleFactor = 1, + const float& bias = 0); + + float batteryVoltage(void); + int8_t get_vin_blocking(char vin[]); + bool resetDTC(); + void currentDTCCodes(const bool& isBlocking = true); + bool isPidSupported(uint8_t pid); + + uint32_t supportedPIDs_1_20(); + + uint32_t monitorStatus(); + uint16_t freezeDTC(); + uint16_t fuelSystemStatus(); + float engineLoad(); + float engineCoolantTemp(); + float shortTermFuelTrimBank_1(); + float longTermFuelTrimBank_1(); + float shortTermFuelTrimBank_2(); + float longTermFuelTrimBank_2(); + float fuelPressure(); + uint8_t manifoldPressure(); + float rpm(); + int32_t kph(); + float mph(); + float timingAdvance(); + float intakeAirTemp(); + float mafRate(); + float throttle(); + uint8_t commandedSecAirStatus(); + uint8_t oxygenSensorsPresent_2banks(); + uint8_t obdStandards(); + uint8_t oxygenSensorsPresent_4banks(); + bool auxInputStatus(); + uint16_t runTime(); + + + uint32_t supportedPIDs_21_40(); + + uint16_t distTravelWithMIL(); + float fuelRailPressure(); + float fuelRailGuagePressure(); + float commandedEGR(); + float egrError(); + float commandedEvapPurge(); + float fuelLevel(); + uint8_t warmUpsSinceCodesCleared(); + uint16_t distSinceCodesCleared(); + float evapSysVapPressure(); + uint8_t absBaroPressure(); + float catTempB1S1(); + float catTempB2S1(); + float catTempB1S2(); + float catTempB2S2(); + + uint32_t supportedPIDs_41_60(); + + uint32_t monitorDriveCycleStatus(); + float ctrlModVoltage(); + float absLoad(); + float commandedAirFuelRatio(); + float relativeThrottle(); + float ambientAirTemp(); + float absThrottlePosB(); + float absThrottlePosC(); + float absThrottlePosD(); + float absThrottlePosE(); + float absThrottlePosF(); + float commandedThrottleActuator(); + uint16_t timeRunWithMIL(); + uint16_t timeSinceCodesCleared(); + float maxMafRate(); + uint8_t fuelType(); + float ethanolPercent(); + float absEvapSysVapPressure(); + float evapSysVapPressure2(); + float absFuelRailPressure(); + float relativePedalPos(); + float hybridBatLife(); + float oilTemp(); + float fuelInjectTiming(); + float fuelRate(); + uint8_t emissionRqmts(); + + + uint32_t supportedPIDs_61_80(); + + float demandedTorque(); + float torque(); + uint16_t referenceTorque(); + uint16_t auxSupported(); + void printError(); private: - char query[QUERY_LEN] = { '\0' }; - bool longQuery = false; - bool isMode0x22Query = false; - uint32_t currentTime; - uint32_t previousTime; - - obd_cmd_states nb_query_state = SEND_COMMAND; // Non-blocking query state - - void upper(char string[], uint8_t buflen); - void formatQueryArray(uint8_t service, uint16_t pid, uint8_t num_responses); - uint8_t ctoi(uint8_t value); - int8_t nextIndex(char const *str, - char const *target, - uint8_t numOccur); + char query[QUERY_LEN] = { '\0' }; + bool longQuery = false; + bool isMode0x22Query = false; + uint32_t currentTime; + uint32_t previousTime; + + obd_cmd_states nb_query_state = SEND_COMMAND; // Non-blocking query state + + void upper(char string[], + uint8_t buflen); + void formatQueryArray(const uint8_t& service, + const uint16_t& pid, + const uint8_t& num_responses); + uint8_t ctoi(uint8_t value); + int8_t nextIndex(char const *str, + char const *target, + uint8_t numOccur = 1); }; From 0119eb07b83d20e77dbeaac8cc8efa40ef16c417 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Tue, 18 Feb 2025 10:20:36 -0500 Subject: [PATCH 74/88] More formatting --- src/ELMduino.cpp | 44 +++++++++++++------------------------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 59528a7..e605b8a 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -27,10 +27,10 @@ bool ELM327::begin( Stream& stream, const uint16_t& payloadLen, const byte& dataTimeout) { - elm_port = &stream; + elm_port = &stream; PAYLOAD_LEN = payloadLen; - debugMode = debug; - timeout_ms = timeout; + debugMode = debug; + timeout_ms = timeout; payload = (char *)malloc(PAYLOAD_LEN + 1); // allow for terminating '\0' @@ -462,7 +462,7 @@ double ELM327::conditionResponse(const uint8_t& numExpectedBytes, const float& bias) { uint8_t numExpectedPayChars = numExpectedBytes * 2; - uint8_t payCharDiff = numPayChars - numExpectedPayChars; + uint8_t payCharDiff = numPayChars - numExpectedPayChars; if (numExpectedBytes > 8) { @@ -489,13 +489,9 @@ double ELM327::conditionResponse(const uint8_t& numExpectedBytes, else if (numExpectedPayChars == numPayChars) { if (scaleFactor == 1 && bias == 0) // No scale/bias needed - { return response; - } else - { return (response * scaleFactor) + bias; - } } // If there were more payload bytes returned than we expected, test the first and last bytes in the @@ -507,7 +503,7 @@ double ELM327::conditionResponse(const uint8_t& numExpectedBytes, if (debugMode) Serial.println(F("Looking for lagging zeros")); - uint16_t numExpectedBits = numExpectedBytes * 8; + uint16_t numExpectedBits = numExpectedBytes * 8; uint64_t laggingZerosMask = 0; for (uint16_t i = 0; i < numExpectedBits; i++) @@ -519,13 +515,9 @@ double ELM327::conditionResponse(const uint8_t& numExpectedBytes, Serial.println(F("Lagging zeros found")); if (scaleFactor == 1 && bias == 0) // No scale/bias needed - { return (response >> (4 * payCharDiff)); - } else - { return ((response >> (4 * payCharDiff)) * scaleFactor) + bias; - } } else { @@ -533,13 +525,9 @@ double ELM327::conditionResponse(const uint8_t& numExpectedBytes, Serial.println(F("Lagging zeros not found - assuming leading zeros")); if (scaleFactor == 1 && bias == 0) // No scale/bias needed - { return response; - } else - { return (response * scaleFactor) + bias; - } } } @@ -2391,6 +2379,7 @@ uint64_t ELM327::findResponse(const uint8_t& service, { header[0] = query[0] + 4; header[1] = query[1]; + if (isMode0x22Query) // mode 0x22 responses always zero-pad the pid to 4 chars, even for a 2-char pid { header[2] = '0'; @@ -2411,7 +2400,7 @@ uint64_t ELM327::findResponse(const uint8_t& service, Serial.println(header); } - int8_t firstHeadIndex = nextIndex(payload, header); + int8_t firstHeadIndex = nextIndex(payload, header); int8_t secondHeadIndex = nextIndex(payload, header, 2); if (firstHeadIndex >= 0) @@ -2452,8 +2441,8 @@ uint64_t ELM327::findResponse(const uint8_t& service, // broken-out because some PID algorithms (standard // and custom) require special operations for each // byte returned - responseByte_0 = response & 0xFF; - responseByte_1 = (response >> 8) & 0xFF; + responseByte_0 = response & 0xFF; + responseByte_1 = (response >> 8) & 0xFF; responseByte_2 = (response >> 16) & 0xFF; responseByte_3 = (response >> 24) & 0xFF; responseByte_4 = (response >> 32) & 0xFF; @@ -2564,14 +2553,11 @@ float ELM327::batteryVoltage() { nb_query_state = SEND_COMMAND; // Reset the query state machine for next command payload[strlen(payload) - 1] = '\0'; // Remove the last char ("V") from the payload value + if (strncmp(payload, "ATRV", 4) == 0) - { return (float)strtod(payload + 4, NULL); - } - else - { + else return (float)strtod(payload, NULL); - } } else if (nb_rx_state != ELM_GETTING_MSG) nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command @@ -2631,8 +2617,10 @@ int8_t ELM327::get_vin_blocking(char vin[]) temp[0] = *(idx + i); // Get first digit of ASCII code temp[1] = *(idx + i + 1); // Get second digit of ASCII code // No need to add string termination, temp[3] always == 0 + if (strstr(temp, ":")) continue; // Skip the second "1:" and third "2:" line numbers + ascii_val = strtol(temp, 0, 16); // Convert ASCII code to integer sprintf(vin + vin_counter++, "%c", ascii_val); // Convert ASCII code integer back to character // Serial.printf("Chars %s, ascii_val=%d[dec] 0x%02hhx[hex] ==> VIN=%s\n", temp, ascii_val, ascii_val, vin); @@ -2680,9 +2668,7 @@ bool ELM327::resetDTC() if (strstr(payload, "44") != NULL) { if (debugMode) - { Serial.println(F("ELMduino: DTC successfully reset.")); - } return true; } @@ -2690,9 +2676,7 @@ bool ELM327::resetDTC() else { if (debugMode) - { Serial.println(F("ELMduino: Resetting DTC codes failed.")); - } } return false; @@ -2743,9 +2727,7 @@ void ELM327::currentDTCCodes(const bool& isBlocking) } else if (nb_query_state == WAITING_RESP) - { get_response(); - } } if (nb_rx_state == ELM_SUCCESS) From c4b733aabcfe74ba8bf1e433a59224d36565a400 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Tue, 18 Feb 2025 10:56:59 -0500 Subject: [PATCH 75/88] Handle log responses in payload (#275) --- src/ELMduino.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index e605b8a..151fe89 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2354,7 +2354,8 @@ int8_t ELM327::get_response(void) Inputs: ------- - * void + * const uint8_t& service - The diagnostic service ID. 01 is "Show current data" + * const uint8_t& pid - The Parameter ID (PID) from the service Return: ------- @@ -2400,11 +2401,14 @@ uint64_t ELM327::findResponse(const uint8_t& service, Serial.println(header); } - int8_t firstHeadIndex = nextIndex(payload, header); + int8_t firstHeadIndex = nextIndex(payload, header, 1); int8_t secondHeadIndex = nextIndex(payload, header, 2); if (firstHeadIndex >= 0) { + int8_t logColonIndex = 0; + int8_t logBytes = 0; + if (longQuery | isMode0x22Query) firstDatum = firstHeadIndex + 6; else @@ -2420,6 +2424,9 @@ uint64_t ELM327::findResponse(const uint8_t& service, Serial.println(F("Double response detected")); numPayChars = secondHeadIndex - firstDatum; + + // Look for the first colon after the last found header - this is the colon separating the data response from the logging response + logColonIndex = nextIndex(payload + secondHeadIndex, header, 1); } else { @@ -2427,8 +2434,17 @@ uint64_t ELM327::findResponse(const uint8_t& service, Serial.println(F("Single response detected")); numPayChars = recBytes - firstDatum; + + // Look for the first colon after the last found header - this is the colon separating the data response from the logging response + logColonIndex = nextIndex(payload + firstHeadIndex, header, 1); } + if (logColonIndex >= 0) + logBytes = recBytes - logColonIndex; // Number of logging bytes INCLUDING the colon + + // Do not process logging bytes + numPayChars -= logBytes; + response = 0; for (uint8_t i = 0; i < numPayChars; i++) { From 21e168f5ef3ceb56280758549231f56fe16e85f7 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Tue, 18 Feb 2025 15:10:09 -0500 Subject: [PATCH 76/88] Bugfix in support of (#275) --- src/ELMduino.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 151fe89..592dbbf 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2426,7 +2426,7 @@ uint64_t ELM327::findResponse(const uint8_t& service, numPayChars = secondHeadIndex - firstDatum; // Look for the first colon after the last found header - this is the colon separating the data response from the logging response - logColonIndex = nextIndex(payload + secondHeadIndex, header, 1); + logColonIndex = nextIndex(payload + secondHeadIndex, ':', 1); } else { @@ -2436,7 +2436,7 @@ uint64_t ELM327::findResponse(const uint8_t& service, numPayChars = recBytes - firstDatum; // Look for the first colon after the last found header - this is the colon separating the data response from the logging response - logColonIndex = nextIndex(payload + firstHeadIndex, header, 1); + logColonIndex = nextIndex(payload + firstHeadIndex, ':', 1); } if (logColonIndex >= 0) From 04c786913da213e660e8c088241142c0bee8ce06 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Tue, 18 Feb 2025 15:13:31 -0500 Subject: [PATCH 77/88] Add debug statements (#275) --- src/ELMduino.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 592dbbf..1ff14a8 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2440,16 +2440,28 @@ uint64_t ELM327::findResponse(const uint8_t& service, } if (logColonIndex >= 0) + { + if (debugMode) + Serial.println(F("Log response detected")); + logBytes = recBytes - logColonIndex; // Number of logging bytes INCLUDING the colon - - // Do not process logging bytes - numPayChars -= logBytes; + + if (debugMode) + { + Serial.print(logBytes); + Serial.println(F(" log bytes found - ignoring these bytes during processing")); + } + + // Do not process logging bytes + numPayChars -= logBytes; + } response = 0; for (uint8_t i = 0; i < numPayChars; i++) { uint8_t payloadIndex = firstDatum + i; - uint8_t bitsOffset = 4 * (numPayChars - i - 1); + uint8_t bitsOffset = 4 * (numPayChars - i - 1); + response = response | ((uint64_t)ctoi(payload[payloadIndex]) << bitsOffset); } From 6f5402da000895e3d0b7c74ebd296dbca9ff7a3e Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Wed, 19 Feb 2025 00:21:35 -0500 Subject: [PATCH 78/88] Another bugfix for (#275) --- src/ELMduino.cpp | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 1ff14a8..e266e4c 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2404,16 +2404,27 @@ uint64_t ELM327::findResponse(const uint8_t& service, int8_t firstHeadIndex = nextIndex(payload, header, 1); int8_t secondHeadIndex = nextIndex(payload, header, 2); + // int8_t firstLogColonIndex = nextIndex(payload, ":", 1); + int8_t secondLogColonIndex = nextIndex(payload, ":", 2); + if (firstHeadIndex >= 0) { - int8_t logColonIndex = 0; - int8_t logBytes = 0; - if (longQuery | isMode0x22Query) firstDatum = firstHeadIndex + 6; else firstDatum = firstHeadIndex + 4; + if (secondLogColonIndex >= 0) + { + if (debugMode) + { + Serial.print(F("Log response detected at index: ")); + Serial.println(secondLogColonIndex); + } + + firstDatum = secondLogColonIndex + 1; + } + // Some ELM327s (such as my own) respond with two // "responses" per query. "numPayChars" represents the // correct number of bytes returned by the ELM327 @@ -2424,9 +2435,6 @@ uint64_t ELM327::findResponse(const uint8_t& service, Serial.println(F("Double response detected")); numPayChars = secondHeadIndex - firstDatum; - - // Look for the first colon after the last found header - this is the colon separating the data response from the logging response - logColonIndex = nextIndex(payload + secondHeadIndex, ':', 1); } else { @@ -2434,26 +2442,6 @@ uint64_t ELM327::findResponse(const uint8_t& service, Serial.println(F("Single response detected")); numPayChars = recBytes - firstDatum; - - // Look for the first colon after the last found header - this is the colon separating the data response from the logging response - logColonIndex = nextIndex(payload + firstHeadIndex, ':', 1); - } - - if (logColonIndex >= 0) - { - if (debugMode) - Serial.println(F("Log response detected")); - - logBytes = recBytes - logColonIndex; // Number of logging bytes INCLUDING the colon - - if (debugMode) - { - Serial.print(logBytes); - Serial.println(F(" log bytes found - ignoring these bytes during processing")); - } - - // Do not process logging bytes - numPayChars -= logBytes; } response = 0; @@ -2461,6 +2449,9 @@ uint64_t ELM327::findResponse(const uint8_t& service, { uint8_t payloadIndex = firstDatum + i; uint8_t bitsOffset = 4 * (numPayChars - i - 1); + + Serial.print("\tProcessing hex nibble: "); + Serial.println(payload[payloadIndex]); response = response | ((uint64_t)ctoi(payload[payloadIndex]) << bitsOffset); } From 9c70bee2d477735ecbbedc08f803d605d24dd19d Mon Sep 17 00:00:00 2001 From: PB2 Date: Wed, 19 Feb 2025 11:04:38 -0800 Subject: [PATCH 79/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 8772ac4..6aa0891 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.3.0 +version=3.3.1 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 82c7d8c84312a5e206480287a809d4bc864e26ec Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Sun, 2 Mar 2025 14:51:14 -0500 Subject: [PATCH 80/88] Fix #280 --- src/ELMduino.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index e266e4c..01ecdbe 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2450,8 +2450,11 @@ uint64_t ELM327::findResponse(const uint8_t& service, uint8_t payloadIndex = firstDatum + i; uint8_t bitsOffset = 4 * (numPayChars - i - 1); - Serial.print("\tProcessing hex nibble: "); - Serial.println(payload[payloadIndex]); + if (debugMode) + { + Serial.print("\tProcessing hex nibble: "); + Serial.println(payload[payloadIndex]); + } response = response | ((uint64_t)ctoi(payload[payloadIndex]) << bitsOffset); } From 1f2bbd2ae3717090717da96c4c30520815fc201c Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Sun, 2 Mar 2025 14:51:30 -0500 Subject: [PATCH 81/88] Increment version number --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 8772ac4..87464ec 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.3.0 +version=3.3.2 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From 94ddd24212d61dd4a02ea52b0c842fe8d2a15dde Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Wed, 12 Mar 2025 22:44:06 -0600 Subject: [PATCH 82/88] Squashing commits into single commit --- .gitignore | 1 + .../ESP32_CustomMultilinePID.ino | 83 +++ .../ESP32_test_functions.ino | 168 ++++++ src/ELMduino.cpp | 490 ++++++++++++++++-- src/ELMduino.h | 362 ++++++------- 5 files changed, 891 insertions(+), 213 deletions(-) create mode 100644 examples/ESP32_CustomMultilinePID/ESP32_CustomMultilinePID.ino create mode 100644 examples/ESP32_Test_Functions/ESP32_test_functions.ino diff --git a/.gitignore b/.gitignore index 967e2ae..8c01f2e 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ test/README .vscode/extensions.json platformio.ini .gitignore +.vscode/settings.json diff --git a/examples/ESP32_CustomMultilinePID/ESP32_CustomMultilinePID.ino b/examples/ESP32_CustomMultilinePID/ESP32_CustomMultilinePID.ino new file mode 100644 index 0000000..5ead1c0 --- /dev/null +++ b/examples/ESP32_CustomMultilinePID/ESP32_CustomMultilinePID.ino @@ -0,0 +1,83 @@ +/* + +This example shows how to query and process OBD2 standard PIDs that +return a multiline response and require custom processing. + +Processing Response with a custom calculation function +In this example, we manually extract the data value from the query response and perform +some post-processing to calculate the correct value. + +Managing Query State +We also demonstrate managing the query state used by the loop() method. This is typically +managed internally by ELMduino for standard PID methods. + +*/ + +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; + +int nb_query_state = SEND_COMMAND; // Set the inital query state ready to send a command + + +// Applies calculation formula for PID 0x22 (Fuel Rail Pressure) +double calcDPF() { + double B = myELM327.payload[6]; + double C = myELM327.payload[5]; + return ((B * 256) + C) / 100; +} + +void setup() +{ + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect("OBDII")) + { + DEBUG_PORT.println("Couldn't connect to ELM327 device."); + while (1); + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + Serial.println("ELM327 device couldn't connect to ECU."); + while (1); + } + + Serial.println("Connected to ELM327"); + +} + +void loop() +{ + if (nb_query_state == SEND_COMMAND) // We are ready to send a new command + { + myELM327.sendCommand("017A"); // Send the PID commnad + nb_query_state = WAITING_RESP; // Set the query state so we are waiting for response + } + else if (nb_query_state == WAITING_RESP) // Our query has been sent, check for a response + { + myELM327.get_response(); // Each time through the loop we will check again + } + + if (myELM327.nb_rx_state == ELM_SUCCESS) // Our response is fully received, let's get our data + { + double dpf = myELM327.conditionResponse(calcDPF); // Apply the formula for dpf + Serial.println(dpf); // Print the adjusted value + nb_query_state = SEND_COMMAND; // Reset the query state for the next command + delay(5000); // Wait 5 seconds until we query again + } + + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { // If state == ELM_GETTING_MSG, response is not yet complete. Restart the loop. + nb_query_state = SEND_COMMAND; // Reset the query state for the next command + myELM327.printError(); + delay(5000); // Wait 5 seconds until we query again + } +} \ No newline at end of file diff --git a/examples/ESP32_Test_Functions/ESP32_test_functions.ino b/examples/ESP32_Test_Functions/ESP32_test_functions.ino new file mode 100644 index 0000000..f0091c1 --- /dev/null +++ b/examples/ESP32_Test_Functions/ESP32_test_functions.ino @@ -0,0 +1,168 @@ +#include "BluetoothSerial.h" +#include "ELMduino.h" + +BluetoothSerial SerialBT; +#define ELM_PORT SerialBT +#define DEBUG_PORT Serial + +ELM327 myELM327; +uint8_t current_pid = 0; +int nb_query_state = SEND_COMMAND; + +// Define a list of PIDs to test +const uint8_t pidsToTest[] = { + SUPPORTED_PIDS_1_20, // 0x00 + MONITOR_STATUS_SINCE_DTC_CLEARED, // 0x01 + FREEZE_DTC, // 0x02 + FUEL_SYSTEM_STATUS, // 0x03 + ENGINE_LOAD, // 0x04 + ENGINE_COOLANT_TEMP, // 0x05 + SHORT_TERM_FUEL_TRIM_BANK_1, // 0x06 + LONG_TERM_FUEL_TRIM_BANK_1, // 0x07 + SHORT_TERM_FUEL_TRIM_BANK_2, // 0x08 + LONG_TERM_FUEL_TRIM_BANK_2, // 0x09 + FUEL_PRESSURE, // 0x0A + INTAKE_MANIFOLD_ABS_PRESSURE, // 0x0B + ENGINE_RPM, // 0x0C + VEHICLE_SPEED, // 0x0D + TIMING_ADVANCE, // 0x0E + INTAKE_AIR_TEMP, // 0x0F + MAF_FLOW_RATE, // 0x10 + THROTTLE_POSITION, // 0x11 + COMMANDED_SECONDARY_AIR_STATUS, // 0x12 + OXYGEN_SENSORS_PRESENT_2_BANKS, // 0x13 + OXYGEN_SENSOR_1_A, // 0x14 + OXYGEN_SENSOR_2_A, // 0x15 + OXYGEN_SENSOR_3_A, // 0x16 + OXYGEN_SENSOR_4_A, // 0x17 + OXYGEN_SENSOR_5_A, // 0x18 + OXYGEN_SENSOR_6_A, // 0x19 + OXYGEN_SENSOR_7_A, // 0x1A + OXYGEN_SENSOR_8_A, // 0x1B + OBD_STANDARDS, // 0x1C + OXYGEN_SENSORS_PRESENT_4_BANKS, // 0x1D + AUX_INPUT_STATUS, // 0x1E + RUN_TIME_SINCE_ENGINE_START, // 0x1F + SUPPORTED_PIDS_21_40, // 0x20 + DISTANCE_TRAVELED_WITH_MIL_ON, // 0x21 + FUEL_RAIL_PRESSURE, // 0x22 + FUEL_RAIL_GUAGE_PRESSURE, // 0x23 + OXYGEN_SENSOR_1_B, // 0x24 + OXYGEN_SENSOR_2_B, // 0x25 + OXYGEN_SENSOR_3_B, // 0x26 + OXYGEN_SENSOR_4_B, // 0x27 + OXYGEN_SENSOR_5_B, // 0x28 + OXYGEN_SENSOR_6_B, // 0x29 + OXYGEN_SENSOR_7_B, // 0x2A + OXYGEN_SENSOR_8_B, // 0x2B + COMMANDED_EGR, // 0x2C + EGR_ERROR, // 0x2D + COMMANDED_EVAPORATIVE_PURGE, // 0x2E + FUEL_TANK_LEVEL_INPUT, // 0x2F + WARM_UPS_SINCE_CODES_CLEARED, // 0x30 - count + DIST_TRAV_SINCE_CODES_CLEARED, // 0x31 - km + EVAP_SYSTEM_VAPOR_PRESSURE, // 0x32 - Pa + ABS_BAROMETRIC_PRESSURE, // 0x33 - kPa + OXYGEN_SENSOR_1_C, // 0x34 - ratio mA + OXYGEN_SENSOR_2_C, // 0x35 - ratio mA + OXYGEN_SENSOR_3_C, // 0x36 - ratio mA + OXYGEN_SENSOR_4_C, // 0x37 - ratio mA + OXYGEN_SENSOR_5_C, // 0x38 - ratio mA + OXYGEN_SENSOR_6_C, // 0x39 - ratio mA + OXYGEN_SENSOR_7_C, // 0x3A - ratio mA + OXYGEN_SENSOR_8_C, // 0x3B - ratio mA + CATALYST_TEMP_BANK_1_SENSOR_1, // 0x3C - °C + CATALYST_TEMP_BANK_2_SENSOR_1, // 0x3D - °C + CATALYST_TEMP_BANK_1_SENSOR_2, // 0x3E - °C + CATALYST_TEMP_BANK_2_SENSOR_2, // 0x3F - °C + SUPPORTED_PIDS_41_60, // 0x40 - bit encoded + MONITOR_STATUS_THIS_DRIVE_CYCLE, // 0x41 - bit encoded + CONTROL_MODULE_VOLTAGE, // 0x42 - V + ABS_LOAD_VALUE, // 0x43 - % + FUEL_AIR_COMMANDED_EQUIV_RATIO, // 0x44 - ratio + RELATIVE_THROTTLE_POSITION, // 0x45 - % + AMBIENT_AIR_TEMP, // 0x46 - °C + ABS_THROTTLE_POSITION_B, // 0x47 - % + ABS_THROTTLE_POSITION_C, // 0x48 - % + ABS_THROTTLE_POSITION_D, // 0x49 - % + ABS_THROTTLE_POSITION_E, // 0x4A - % + ABS_THROTTLE_POSITION_F, // 0x4B - % + COMMANDED_THROTTLE_ACTUATOR, // 0x4C - % + TIME_RUN_WITH_MIL_ON, // 0x4D - min + TIME_SINCE_CODES_CLEARED, // 0x4E - min + MAX_VALUES_EQUIV_V_I_PRESSURE, // 0x4F - ratio V mA kPa + MAX_MAF_RATE, // 0x50 - g/s + FUEL_TYPE, // 0x51 + ETHANOL_FUEL_PERCENT, // 0x52 + ABS_EVAP_SYS_VAPOR_PRESSURE, // 0x53 + EVAP_SYS_VAPOR_PRESSURE, // 0x54 + SHORT_TERM_SEC_OXY_SENS_TRIM_1_3, // 0x55 + LONG_TERM_SEC_OXY_SENS_TRIM_1_3, // 0x56 + SHORT_TERM_SEC_OXY_SENS_TRIM_2_4, // 0x57 + LONG_TERM_SEC_OXY_SENS_TRIM_2_4, // 0x58 + FUEL_RAIL_ABS_PRESSURE, // 0x59 + RELATIVE_ACCELERATOR_PEDAL_POS, // 0x5A + HYBRID_BATTERY_REMAINING_LIFE, // 0x5B + ENGINE_OIL_TEMP, // 0x5C + FUEL_INJECTION_TIMING, // 0x5D + ENGINE_FUEL_RATE, // 0x5E + EMISSION_REQUIREMENTS, // 0x5F + SUPPORTED_PIDS_61_80, // 0x60 + DEMANDED_ENGINE_PERCENT_TORQUE, // 0x61 + ACTUAL_ENGINE_TORQUE, // 0x62 + ENGINE_REFERENCE_TORQUE, // 0x63 + ENGINE_PERCENT_TORQUE_DATA // 0x64 +}; + +void setup() +{ + DEBUG_PORT.begin(115200); + // SerialBT.setPin("1234"); + ELM_PORT.begin("ArduHUD", true); + + if (!ELM_PORT.connect("OBDII")) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 1"); + while (1) + ; + } + + if (!myELM327.begin(ELM_PORT, true, 2000)) + { + DEBUG_PORT.println("Couldn't connect to OBD scanner - Phase 2"); + while (1) + ; + } + + DEBUG_PORT.println("Connected to ELM327"); +} + + + +void loop() { + uint8_t pid = pidsToTest[current_pid]; + if (nb_query_state == SEND_COMMAND) // We are ready to send a new command + { + char query[5] = {0}; + sprintf(query, "01%02X", pid); + myELM327.sendCommand(query); // Send the custom PID commnad + nb_query_state = WAITING_RESP; // Set the query state so we are waiting for response + } + else if (nb_query_state == WAITING_RESP) // Our query has been sent, check for a response + { + myELM327.get_response(); // Each time through the loop we will check again + } + + if (myELM327.nb_rx_state == ELM_SUCCESS) // Our response is fully received, let's get our data + { + nb_query_state = SEND_COMMAND; // Reset the query state for the next command + delay(500); // Wait 0.5 seconds until we query again + } + + else if (myELM327.nb_rx_state != ELM_GETTING_MSG) + { // If state == ELM_GETTING_MSG, response is not yet complete. Restart the loop. + nb_query_state = SEND_COMMAND; // Reset the query state for the next command + myELM327.printError(); + delay(500); // Wait 0.5 seconds until we query again + } + } diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 01ecdbe..c3d8a8b 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -31,7 +31,6 @@ bool ELM327::begin( Stream& stream, PAYLOAD_LEN = payloadLen; debugMode = debug; timeout_ms = timeout; - payload = (char *)malloc(PAYLOAD_LEN + 1); // allow for terminating '\0' // test if serial port is connected @@ -45,6 +44,10 @@ bool ELM327::begin( Stream& stream, return true; } +ELM327::~ELM327() { + if (payload) free(payload); +} + /* bool ELM327::initializeELM(const char& protocol, const byte& dataTimeout) @@ -103,18 +106,18 @@ bool ELM327::initializeELM(const char& protocol, delay(100); // // Set data timeout - sprintf(command, SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); + snprintf(command, sizeof(command), SET_TIMEOUT_TO_H_X_4MS, dataTimeout / 4); sendCommand_Blocking(command); delay(100); // Automatic searching for protocol requires setting the protocol to AUTO and then // sending an OBD command to initiate the protocol search. The OBD command "0100" // requests a list of supported PIDs 0x00 - 0x20 and is guaranteed to work - if ((String)protocol == "0") + if (protocol == '0') { // Tell the ELM327 to do an auto protocol search. If a valid protocol is found, it will be saved to memory. // Some ELM clones may not have memory enabled and thus will perform the search every time. - sprintf(command, SET_PROTOCOL_TO_AUTO_H_SAVE, protocol); + snprintf(command, sizeof(command), SET_PROTOCOL_TO_AUTO_H_SAVE, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) { if (strstr(payload, RESPONSE_OK) != NULL) @@ -145,7 +148,7 @@ bool ELM327::initializeELM(const char& protocol, else { // Set protocol - sprintf(command, TRY_PROT_H_AUTO_SEARCH, protocol); + snprintf(command, sizeof(command), TRY_PROT_H_AUTO_SEARCH, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) { @@ -166,7 +169,7 @@ bool ELM327::initializeELM(const char& protocol, } // Set protocol and save - sprintf(command, SET_PROTOCOL_TO_H_SAVE, protocol); + snprintf(command, sizeof(command), SET_PROTOCOL_TO_H_SAVE, protocol); if (sendCommand_Blocking(command) == ELM_SUCCESS) { @@ -439,12 +442,39 @@ int8_t ELM327::nextIndex(char const *str, return p - r; } +/* + void ELM327::removeChar(char *from, + char const *remove) + + Description: + ------------ + * Removes all instances of each char in string "remove" from the string "from" + + Inputs: + ------- + * char *from - String to remove target(s) from + * char const *remove - Chars to find/remove + + Return: + ------- + * void +*/ +void ELM327::removeChar(char *from, const char *remove) +{ + size_t i = 0, j = 0; + while (from[i]) { + if (!strchr(remove, from[i])) + from[j++] = from[i]; + i++; + } + from[j] = '\0'; +} /* double ELM327::conditionResponse(const uint8_t &numExpectedBytes, const float &scaleFactor, const float &bias) Description: ------------ - * Converts the ELM327's response into it's correct, numerical value. Returns 0 if numExpectedBytes > numPayChars + * Converts the ELM327's response into its correct, numerical value. Returns 0 if numExpectedBytes > numPayChars Inputs: ------- @@ -459,7 +489,7 @@ int8_t ELM327::nextIndex(char const *str, */ double ELM327::conditionResponse(const uint8_t& numExpectedBytes, const double& scaleFactor, - const float& bias) + const double& bias) { uint8_t numExpectedPayChars = numExpectedBytes * 2; uint8_t payCharDiff = numPayChars - numExpectedPayChars; @@ -531,6 +561,28 @@ double ELM327::conditionResponse(const uint8_t& numExpectedBytes, } } +/* + double ELM327::conditionResponse(double (*func)()) + + Description: + ------------ + * Provides a means to pass in a user-defined function to process the response. Used for PIDs that + don't use the common scaleFactor + Bias formula to calculate the value from the response data. Also + useful for processing OEM custom PIDs which are too numerous and varied to encode in the lib. + + Inputs: + ------- + * (*func)() - pointer to function to do calculate response value + + Return: + ------- + * double - Converted numerical value +*/ + +double ELM327::conditionResponse(double (*func)()) { + return func(); +} + /* void ELM327::flushInputBuff() @@ -560,7 +612,7 @@ void ELM327::flushInputBuff() Description: ------------ - * create a PID query command string and send the command + * Create a PID query command string and send the command Inputs: ------- @@ -649,16 +701,199 @@ double ELM327::processPID(const uint8_t& service, if (nb_rx_state == ELM_SUCCESS) { nb_query_state = SEND_COMMAND; // Reset the query state machine for next command + findResponse(); + + /* This data manipulation seems duplicative of the responseByte_0, responseByte_1, etc vars and it is. + The duplcation is deliberate to provide a clear way for the calculator functions to access the relevant + data bytes from the response in the format they are commonly expressed in and without breaking backward + compatability with existing code that may be using the responseByte_n vars. + + In addition, we need to place the response values into static vars that can be accessed by the (static) + calculator functions. A future (breaking!) change could be made to eliminate this duplication. + */ + uint8_t responseBits = numExpectedBytes * 8; + uint8_t extractedBytes[8] = {0}; // Store extracted bytes + + // Extract bytes only if shift is non-negative + for (int i = 0; i < numExpectedBytes; i++) + { + int shiftAmount = responseBits - (8 * (i + 1)); // Compute shift amount + if (shiftAmount >= 0) { // Ensure valid shift + extractedBytes[i] = (response >> shiftAmount) & 0xFF; // Extract byte + } + } - findResponse(service, pid); + // Assign extracted values to response_A, response_B, ..., response_H safely + response_A = extractedBytes[0]; + response_B = extractedBytes[1]; + response_C = extractedBytes[2]; + response_D = extractedBytes[3]; + response_E = extractedBytes[4]; + response_F = extractedBytes[5]; + response_G = extractedBytes[6]; + response_H = extractedBytes[7]; + + double (*calculator)() = selectCalculator(pid); - return conditionResponse(numExpectedBytes, scaleFactor, bias); + if (nullptr == calculator) { + //Use the default scaleFactor + Bias calculation + return conditionResponse(numExpectedBytes, scaleFactor, bias); + } + else { + return conditionResponse(calculator); + } } else if (nb_rx_state != ELM_GETTING_MSG) nb_query_state = SEND_COMMAND; // Error or timeout, so reset the query state machine for next command } return 0.0; } +/* + double ELM327::selectCalculator(uint16_t pid))() + + Description: + ------------ + * Selects the appropriate calculation function for a given PID. + + Inputs: + ------- + * uint16_t pid - The Parameter ID (PID) from the service + + + Return: + ------- + * double (*func()) - Pointer to a function to be used to calculate the value for this PID. + Returns nullptr if the PID is calculated using the default scaleFactor + Bias formula as + implemented in conditionResponse(). (Maintained for backward compatibility) +*/ +double (*ELM327::selectCalculator(uint16_t pid))() +{ + switch (pid) + { + case ENGINE_LOAD: + case ENGINE_COOLANT_TEMP: + case SHORT_TERM_FUEL_TRIM_BANK_1: + case LONG_TERM_FUEL_TRIM_BANK_1: + case SHORT_TERM_FUEL_TRIM_BANK_2: + case LONG_TERM_FUEL_TRIM_BANK_2: + case FUEL_PRESSURE: + case INTAKE_MANIFOLD_ABS_PRESSURE: + case VEHICLE_SPEED: + case TIMING_ADVANCE: + case INTAKE_AIR_TEMP: + case THROTTLE_POSITION: + case COMMANDED_EGR: + case EGR_ERROR: + case COMMANDED_EVAPORATIVE_PURGE: + case FUEL_TANK_LEVEL_INPUT: + case WARM_UPS_SINCE_CODES_CLEARED: + case ABS_BAROMETRIC_PRESSURE: + case RELATIVE_THROTTLE_POSITION: + case AMBIENT_AIR_TEMP: + case ABS_THROTTLE_POSITION_B: + case ABS_THROTTLE_POSITION_C: + case ABS_THROTTLE_POSITION_D: + case ABS_THROTTLE_POSITION_E: + case ABS_THROTTLE_POSITION_F: + case COMMANDED_THROTTLE_ACTUATOR: + case ETHANOL_FUEL_PERCENT: + case RELATIVE_ACCELERATOR_PEDAL_POS: + case HYBRID_BATTERY_REMAINING_LIFE: + case ENGINE_OIL_TEMP: + case DEMANDED_ENGINE_PERCENT_TORQUE: + case ACTUAL_ENGINE_TORQUE: + return nullptr; + + case ENGINE_RPM: + return calculator_0C; + + case MAF_FLOW_RATE: + return calculator_10; + + case OXYGEN_SENSOR_1_A: + case OXYGEN_SENSOR_2_A: + case OXYGEN_SENSOR_3_A: + case OXYGEN_SENSOR_4_A: + case OXYGEN_SENSOR_5_A: + case OXYGEN_SENSOR_6_A: + case OXYGEN_SENSOR_7_A: + case OXYGEN_SENSOR_8_A: + case OXYGEN_SENSOR_1_B: + case OXYGEN_SENSOR_2_B: + case OXYGEN_SENSOR_3_B: + case OXYGEN_SENSOR_4_B: + case OXYGEN_SENSOR_6_B: + case OXYGEN_SENSOR_7_B: + case OXYGEN_SENSOR_8_B: + case OXYGEN_SENSOR_1_C: + case OXYGEN_SENSOR_2_C: + case OXYGEN_SENSOR_3_C: + case OXYGEN_SENSOR_4_C: + case OXYGEN_SENSOR_5_C: + case OXYGEN_SENSOR_6_C: + case OXYGEN_SENSOR_7_C: + case OXYGEN_SENSOR_8_C: + return calculator_14; + + case RUN_TIME_SINCE_ENGINE_START: + case DISTANCE_TRAVELED_WITH_MIL_ON: + case DIST_TRAV_SINCE_CODES_CLEARED: + case TIME_RUN_WITH_MIL_ON: + case TIME_SINCE_CODES_CLEARED: + case ENGINE_REFERENCE_TORQUE: + return calculator_1F; + + case FUEL_RAIL_PRESSURE: + return calculator_22; + + case FUEL_RAIL_GUAGE_PRESSURE: + case FUEL_RAIL_ABS_PRESSURE: + return calculator_23; + + case EVAP_SYSTEM_VAPOR_PRESSURE: + return calculator_32; + + case CATALYST_TEMP_BANK_1_SENSOR_1: + case CATALYST_TEMP_BANK_2_SENSOR_1: + case CATALYST_TEMP_BANK_1_SENSOR_2: + case CATALYST_TEMP_BANK_2_SENSOR_2: + return calculator_3C; + + case CONTROL_MODULE_VOLTAGE: + return calculator_42; + + case ABS_LOAD_VALUE: + return calculator_43; + + case FUEL_AIR_COMMANDED_EQUIV_RATIO: + return calculator_44; + + case MAX_VALUES_EQUIV_V_I_PRESSURE: + return calculator_4F; + + case MAX_MAF_RATE: + return calculator_50; + + case ABS_EVAP_SYS_VAPOR_PRESSURE: + return calculator_53; + + case SHORT_TERM_SEC_OXY_SENS_TRIM_1_3: + case LONG_TERM_SEC_OXY_SENS_TRIM_1_3: + case SHORT_TERM_SEC_OXY_SENS_TRIM_2_4: + case LONG_TERM_SEC_OXY_SENS_TRIM_2_4: + return calculator_55; + + case FUEL_INJECTION_TIMING: + return calculator_5D; + + case ENGINE_FUEL_RATE: + return calculator_5E; + + default: + return nullptr; + + } +} /* uint32_t ELM327::supportedPIDs_1_20() @@ -2185,8 +2420,10 @@ void ELM327::sendCommand(const char *cmd) int8_t ELM327::sendCommand_Blocking(const char *cmd) { sendCommand(cmd); - while (get_response() == ELM_GETTING_MSG) - ; + uint32_t startTime = millis(); + while (get_response() == ELM_GETTING_MSG) { + if (millis() - startTime > timeout_ms) break; + } return nb_rx_state; } @@ -2252,8 +2489,8 @@ int8_t ELM327::get_response(void) nb_rx_state = ELM_MSG_RXD; } - else if (!isalnum(recChar) && (recChar != ':') && (recChar != '.')) - // discard all characters except for alphanumeric and decimal places. + else if (!isalnum(recChar) && (recChar != ':') && (recChar != '.') && (recChar != '\r')) + // Keep only alphanumeric, decimal, colon, CR. These are needed for response parsing // decimal places needed to extract floating point numbers, e.g. battery voltage nb_rx_state = ELM_GETTING_MSG; // Discard this character else @@ -2342,9 +2579,121 @@ int8_t ELM327::get_response(void) } nb_rx_state = ELM_SUCCESS; + // Need to process multiline repsonses, remove '\r' from non multiline resp + if (NULL != strchr(payload, ':')) { + parseMultiLineResponse(); + } + else { + removeChar(payload, " \r"); + } + recBytes = strlen(payload); return nb_rx_state; } +/* + void ELM327::parseMultilineResponse() + + Description: + ------------ + * Parses a buffered multiline response into a single line with the specified data + * Modifies the value of payload for further processing and removes the '\r' chars + + Inputs: + ------- + * void + + Return: + ------- + * void +*/ +void ELM327::parseMultiLineResponse() { + uint8_t totalBytes = 0; + uint8_t bytesReceived = 0; + bool headerFound = false; + char newResponse[PAYLOAD_LEN] = {0}; + char line[256] = ""; + char* start = payload; + char* end = strchr(start, '\r'); + + do + { //Step 1: Get a line from the response + memset(line, '\0', 256); + if (end != NULL) { + strncpy(line, start, end - start); + line[end - start] = '\0'; + } else { + strncpy(line, start, strlen(start)); + line[strlen(start)] = '\0'; + + // Exit when there's no more data + if (strlen(line) == 0) break; + } + + if (debugMode) { + Serial.print(F("Found line in response: ")); + Serial.println(line); + } + // Step 2: Check if this is the first line of the response + if (0 == totalBytes) + // Some devices return the response header in the first line instead of the data length, ignore this line + // Line containing totalBytes indicator is 3 hex chars only, longer first line will be a header. + { + if (strlen(line) > 3) { + if (debugMode) + { + Serial.print(F("Found header in response line: ")); + Serial.println(line); + } + } + else { + if (strlen(line) > 0) { + totalBytes = strtol(line, NULL, 16) * 2; + if (debugMode) { + Serial.print(F("totalBytes = ")); + Serial.println(totalBytes); + } + } + } + } + // Step 3: Process data response lines + else { + if (strchr(line, ':')) { + char* dataStart = strchr(line, ':') + 1; + uint8_t dataLength = strlen(dataStart); + uint8_t bytesToCopy = (bytesReceived + dataLength > totalBytes) ? (totalBytes - bytesReceived) : dataLength; + if (bytesReceived + bytesToCopy > PAYLOAD_LEN - 1) { + bytesToCopy = (PAYLOAD_LEN - 1) - bytesReceived; + } + strncat(newResponse, dataStart, bytesToCopy); + bytesReceived += bytesToCopy; + + if (debugMode) { + Serial.print(F("Response data: ")); + Serial.println(dataStart); + } + } + } + if (*(end + 1) == '\0') { + start = NULL; + } else { + start = end + 1; + } + end = (start != NULL) ? strchr(start, '\r') : NULL; + + } while ((bytesReceived < totalBytes || 0 == totalBytes) && start != NULL); + + // Replace payload with parsed response, null-terminate after totalBytes + int nullTermPos = (totalBytes < PAYLOAD_LEN - 1) ? totalBytes : PAYLOAD_LEN - 1; + strncpy(payload, newResponse, nullTermPos); + payload[nullTermPos] = '\0'; // Ensure null termination + if (debugMode) + { + Serial.print(F("Parsed multiline response: ")); + Serial.println(payload); + } +} + + /* uint64_t ELM327::findResponse(const uint8_t& service, const uint8_t& pid) @@ -2359,10 +2708,9 @@ int8_t ELM327::get_response(void) Return: ------- - * uint64_t - Query response value + * void */ -uint64_t ELM327::findResponse(const uint8_t& service, - const uint8_t& pid) +uint64_t ELM327::findResponse() { uint8_t firstDatum = 0; char header[7] = {'\0'}; @@ -2404,9 +2752,6 @@ uint64_t ELM327::findResponse(const uint8_t& service, int8_t firstHeadIndex = nextIndex(payload, header, 1); int8_t secondHeadIndex = nextIndex(payload, header, 2); - // int8_t firstLogColonIndex = nextIndex(payload, ":", 1); - int8_t secondLogColonIndex = nextIndex(payload, ":", 2); - if (firstHeadIndex >= 0) { if (longQuery | isMode0x22Query) @@ -2414,17 +2759,6 @@ uint64_t ELM327::findResponse(const uint8_t& service, else firstDatum = firstHeadIndex + 4; - if (secondLogColonIndex >= 0) - { - if (debugMode) - { - Serial.print(F("Log response detected at index: ")); - Serial.println(secondLogColonIndex); - } - - firstDatum = secondLogColonIndex + 1; - } - // Some ELM327s (such as my own) respond with two // "responses" per query. "numPayChars" represents the // correct number of bytes returned by the ELM327 @@ -2441,7 +2775,7 @@ uint64_t ELM327::findResponse(const uint8_t& service, if (debugMode) Serial.println(F("Single response detected")); - numPayChars = recBytes - firstDatum; + numPayChars = strlen(payload) - firstDatum; } response = 0; @@ -2452,10 +2786,9 @@ uint64_t ELM327::findResponse(const uint8_t& service, if (debugMode) { - Serial.print("\tProcessing hex nibble: "); + Serial.print(F("\tProcessing hex nibble: ")); Serial.println(payload[payloadIndex]); } - response = response | ((uint64_t)ctoi(payload[payloadIndex]) << bitsOffset); } @@ -2463,7 +2796,8 @@ uint64_t ELM327::findResponse(const uint8_t& service, // broken-out because some PID algorithms (standard // and custom) require special operations for each // byte returned - responseByte_0 = response & 0xFF; + + responseByte_0 = response & 0xFF; responseByte_1 = (response >> 8) & 0xFF; responseByte_2 = (response >> 16) & 0xFF; responseByte_3 = (response >> 24) & 0xFF; @@ -2644,7 +2978,7 @@ int8_t ELM327::get_vin_blocking(char vin[]) continue; // Skip the second "1:" and third "2:" line numbers ascii_val = strtol(temp, 0, 16); // Convert ASCII code to integer - sprintf(vin + vin_counter++, "%c", ascii_val); // Convert ASCII code integer back to character + snprintf(vin + vin_counter++, sizeof(uint8_t), "%c", ascii_val); // Convert ASCII code integer back to character // Serial.printf("Chars %s, ascii_val=%d[dec] 0x%02hhx[hex] ==> VIN=%s\n", temp, ascii_val, ascii_val, vin); } } @@ -2940,3 +3274,85 @@ bool ELM327::isPidSupported(uint8_t pid) } return false; } + +double ELM327::calculator_0C() { + return (double)((response_A << 8) | response_B)/4; +} + +double ELM327::calculator_10() { + return (double)((response_A << 8) | response_B)/100; +} + +double ELM327::calculator_14(){ + return (double)(response_A/200) ; +} + +double ELM327::calculator_1F() { + return (double)((response_A << 8) | response_B); +} + +double ELM327::calculator_22() { + return (double) ((response_A << 8) | response_B) * 0.079; +} + +double ELM327::calculator_23() { + return (double) ((response_A << 8) | response_B) * 10; +} + +double ELM327::calculator_32() +{ + return (double) ((int16_t)((response_A << 8) | response_B)) / 4.0; +} + +double ELM327::calculator_3C() { + return (double) (((response_A << 8) | response_B) / 10) - 40; +} + +double ELM327::calculator_42() { + return (double) ((response_A << 8) | response_B) / 1000; +} + +double ELM327::calculator_43() { + return (double) ((response_A << 8) | response_B) * (100.0 / 255.0); +} + +double ELM327::calculator_44() { + return ((double) ((response_A << 8) | response_B) * 2.0) / 65536.0; +} + +double ELM327::calculator_4F() { + return (double) (response_A); +} + +double ELM327::calculator_50() { + return (double) (response_A * 10.0); +} + +double ELM327::calculator_53() { + return (double) ((response_A << 8) | response_B) / 200; +} + +double ELM327::calculator_54() { + return (double) ((int16_t)((response_A << 8) | response_B)); +} + +double ELM327::calculator_55() { + return ((double) response_A * (100.0 / 128.0)) - 100.0; +} + +//calc 23 +double ELM327::calculator_59() { + return (double) ((response_A << 8) | response_B) * 10; +} + +double ELM327::calculator_5D() { + return (double) (((response_A << 8) | response_B) / 128) - 210; +} + +double ELM327::calculator_5E() { + return (double) ((response_A << 8) | response_B) / 20; +} + +double ELM327::calculator_61() { + return (double) response_A - 125; +} \ No newline at end of file diff --git a/src/ELMduino.h b/src/ELMduino.h index ce0d87a..6060b83 100644 --- a/src/ELMduino.h +++ b/src/ELMduino.h @@ -1,9 +1,6 @@ #pragma once #include "Arduino.h" - - - //-------------------------------------------------------------------------------------// // Protocol IDs //-------------------------------------------------------------------------------------// @@ -21,126 +18,120 @@ const char SAE_J1939_29_BIT_250_KBAUD = 'A'; const char USER_1_CAN = 'B'; const char USER_2_CAN = 'C'; - - - //-------------------------------------------------------------------------------------// // PIDs (https://en.wikipedia.org/wiki/OBD-II_PIDs) //-------------------------------------------------------------------------------------// -const uint8_t SERVICE_01 = 1; -const uint8_t SERVICE_02 = 2; -const uint8_t SERVICE_03 = 3; -const uint8_t PID_INTERVAL_OFFSET = 0x20; - - -const uint8_t SUPPORTED_PIDS_1_20 = 0; // 0x00 - bit encoded -const uint8_t MONITOR_STATUS_SINCE_DTC_CLEARED = 1; // 0x01 - bit encoded -const uint8_t FREEZE_DTC = 2; // 0x02 - -const uint8_t FUEL_SYSTEM_STATUS = 3; // 0x03 - bit encoded -const uint8_t ENGINE_LOAD = 4; // 0x04 - % -const uint8_t ENGINE_COOLANT_TEMP = 5; // 0x05 - °C -const uint8_t SHORT_TERM_FUEL_TRIM_BANK_1 = 6; // 0x06 - % -const uint8_t LONG_TERM_FUEL_TRIM_BANK_1 = 7; // 0x07 - % -const uint8_t SHORT_TERM_FUEL_TRIM_BANK_2 = 8; // 0x08 - % -const uint8_t LONG_TERM_FUEL_TRIM_BANK_2 = 9; // 0x09 - % -const uint8_t FUEL_PRESSURE = 10; // 0x0A - kPa -const uint8_t INTAKE_MANIFOLD_ABS_PRESSURE = 11; // 0x0B - kPa -const uint8_t ENGINE_RPM = 12; // 0x0C - rpm -const uint8_t VEHICLE_SPEED = 13; // 0x0D - km/h -const uint8_t TIMING_ADVANCE = 14; // 0x0E - ° before TDC -const uint8_t INTAKE_AIR_TEMP = 15; // 0x0F - °C -const uint8_t MAF_FLOW_RATE = 16; // 0x10 - g/s -const uint8_t THROTTLE_POSITION = 17; // 0x11 - % -const uint8_t COMMANDED_SECONDARY_AIR_STATUS = 18; // 0x12 - bit encoded -const uint8_t OXYGEN_SENSORS_PRESENT_2_BANKS = 19; // 0x13 - bit encoded -const uint8_t OXYGEN_SENSOR_1_A = 20; // 0x14 - V % -const uint8_t OXYGEN_SENSOR_2_A = 21; // 0x15 - V % -const uint8_t OXYGEN_SENSOR_3_A = 22; // 0x16 - V % -const uint8_t OXYGEN_SENSOR_4_A = 23; // 0x17 - V % -const uint8_t OXYGEN_SENSOR_5_A = 24; // 0x18 - V % -const uint8_t OXYGEN_SENSOR_6_A = 25; // 0x19 - V % -const uint8_t OXYGEN_SENSOR_7_A = 26; // 0x1A - V % -const uint8_t OXYGEN_SENSOR_8_A = 27; // 0x1B - V % -const uint8_t OBD_STANDARDS = 28; // 0x1C - bit encoded -const uint8_t OXYGEN_SENSORS_PRESENT_4_BANKS = 29; // 0x1D - bit encoded -const uint8_t AUX_INPUT_STATUS = 30; // 0x1E - bit encoded -const uint8_t RUN_TIME_SINCE_ENGINE_START = 31; // 0x1F - sec - -const uint8_t SUPPORTED_PIDS_21_40 = 32; // 0x20 - bit encoded -const uint8_t DISTANCE_TRAVELED_WITH_MIL_ON = 33; // 0x21 - km -const uint8_t FUEL_RAIL_PRESSURE = 34; // 0x22 - kPa -const uint8_t FUEL_RAIL_GUAGE_PRESSURE = 35; // 0x23 - kPa -const uint8_t OXYGEN_SENSOR_1_B = 36; // 0x24 - ratio V -const uint8_t OXYGEN_SENSOR_2_B = 37; // 0x25 - ratio V -const uint8_t OXYGEN_SENSOR_3_B = 38; // 0x26 - ratio V -const uint8_t OXYGEN_SENSOR_4_B = 39; // 0x27 - ratio V -const uint8_t OXYGEN_SENSOR_5_B = 40; // 0x28 - ratio V -const uint8_t OXYGEN_SENSOR_6_B = 41; // 0x29 - ratio V -const uint8_t OXYGEN_SENSOR_7_B = 42; // 0x2A - ratio V -const uint8_t OXYGEN_SENSOR_8_B = 43; // 0x2B - ratio V -const uint8_t COMMANDED_EGR = 44; // 0x2C - % -const uint8_t EGR_ERROR = 45; // 0x2D - % -const uint8_t COMMANDED_EVAPORATIVE_PURGE = 46; // 0x2E - % -const uint8_t FUEL_TANK_LEVEL_INPUT = 47; // 0x2F - % -const uint8_t WARM_UPS_SINCE_CODES_CLEARED = 48; // 0x30 - count -const uint8_t DIST_TRAV_SINCE_CODES_CLEARED = 49; // 0x31 - km -const uint8_t EVAP_SYSTEM_VAPOR_PRESSURE = 50; // 0x32 - Pa -const uint8_t ABS_BAROMETRIC_PRESSURE = 51; // 0x33 - kPa -const uint8_t OXYGEN_SENSOR_1_C = 52; // 0x34 - ratio mA -const uint8_t OXYGEN_SENSOR_2_C = 53; // 0x35 - ratio mA -const uint8_t OXYGEN_SENSOR_3_C = 54; // 0x36 - ratio mA -const uint8_t OXYGEN_SENSOR_4_C = 55; // 0x37 - ratio mA -const uint8_t OXYGEN_SENSOR_5_C = 56; // 0x38 - ratio mA -const uint8_t OXYGEN_SENSOR_6_C = 57; // 0x39 - ratio mA -const uint8_t OXYGEN_SENSOR_7_C = 58; // 0x3A - ratio mA -const uint8_t OXYGEN_SENSOR_8_C = 59; // 0x3B - ratio mA -const uint8_t CATALYST_TEMP_BANK_1_SENSOR_1 = 60; // 0x3C - °C -const uint8_t CATALYST_TEMP_BANK_2_SENSOR_1 = 61; // 0x3D - °C -const uint8_t CATALYST_TEMP_BANK_1_SENSOR_2 = 62; // 0x3E - °C -const uint8_t CATALYST_TEMP_BANK_2_SENSOR_2 = 63; // 0x3F - °C - -const uint8_t SUPPORTED_PIDS_41_60 = 64; // 0x40 - bit encoded -const uint8_t MONITOR_STATUS_THIS_DRIVE_CYCLE = 65; // 0x41 - bit encoded -const uint8_t CONTROL_MODULE_VOLTAGE = 66; // 0x42 - V -const uint8_t ABS_LOAD_VALUE = 67; // 0x43 - % -const uint8_t FUEL_AIR_COMMANDED_EQUIV_RATIO = 68; // 0x44 - ratio -const uint8_t RELATIVE_THROTTLE_POSITION = 69; // 0x45 - % -const uint8_t AMBIENT_AIR_TEMP = 70; // 0x46 - °C -const uint8_t ABS_THROTTLE_POSITION_B = 71; // 0x47 - % -const uint8_t ABS_THROTTLE_POSITION_C = 72; // 0x48 - % -const uint8_t ABS_THROTTLE_POSITION_D = 73; // 0x49 - % -const uint8_t ABS_THROTTLE_POSITION_E = 74; // 0x4A - % -const uint8_t ABS_THROTTLE_POSITION_F = 75; // 0x4B - % -const uint8_t COMMANDED_THROTTLE_ACTUATOR = 76; // 0x4C - % -const uint8_t TIME_RUN_WITH_MIL_ON = 77; // 0x4D - min -const uint8_t TIME_SINCE_CODES_CLEARED = 78; // 0x4E - min -const uint8_t MAX_VALUES_EQUIV_V_I_PRESSURE = 79; // 0x4F - ratio V mA kPa -const uint8_t MAX_MAF_RATE = 80; // 0x50 - g/s -const uint8_t FUEL_TYPE = 81; // 0x51 - ref table -const uint8_t ETHANOL_FUEL_PERCENT = 82; // 0x52 - % -const uint8_t ABS_EVAP_SYS_VAPOR_PRESSURE = 83; // 0x53 - kPa -const uint8_t EVAP_SYS_VAPOR_PRESSURE = 84; // 0x54 - Pa -const uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_1_3 = 85; // 0x55 - % -const uint8_t LONG_TERM_SEC_OXY_SENS_TRIM_1_3 = 86; // 0x56 - % -const uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_2_4 = 87; // 0x57 - % -const uint8_t LONG_TERM_SEC_OXY_SENS_TRIM_2_4 = 88; // 0x58 - % -const uint8_t FUEL_RAIL_ABS_PRESSURE = 89; // 0x59 - kPa -const uint8_t RELATIVE_ACCELERATOR_PEDAL_POS = 90; // 0x5A - % -const uint8_t HYBRID_BATTERY_REMAINING_LIFE = 91; // 0x5B - % -const uint8_t ENGINE_OIL_TEMP = 92; // 0x5C - °C -const uint8_t FUEL_INJECTION_TIMING = 93; // 0x5D - ° -const uint8_t ENGINE_FUEL_RATE = 94; // 0x5E - L/h -const uint8_t EMISSION_REQUIREMENTS = 95; // 0x5F - bit encoded - -const uint8_t SUPPORTED_PIDS_61_80 = 96; // 0x60 - bit encoded -const uint8_t DEMANDED_ENGINE_PERCENT_TORQUE = 97; // 0x61 - % -const uint8_t ACTUAL_ENGINE_TORQUE = 98; // 0x62 - % -const uint8_t ENGINE_REFERENCE_TORQUE = 99; // 0x63 - Nm -const uint8_t ENGINE_PERCENT_TORQUE_DATA = 100; // 0x64 - % -const uint8_t AUX_INPUT_OUTPUT_SUPPORTED = 101; // 0x65 - bit encoded - - - +constexpr uint8_t SERVICE_01 = 1; +constexpr uint8_t SERVICE_02 = 2; +constexpr uint8_t SERVICE_03 = 3; +constexpr uint8_t PID_INTERVAL_OFFSET = 0x20; + + +constexpr uint8_t SUPPORTED_PIDS_1_20 = 0; // 0x00 - bit encoded +constexpr uint8_t MONITOR_STATUS_SINCE_DTC_CLEARED = 1; // 0x01 - bit encoded +constexpr uint8_t FREEZE_DTC = 2; // 0x02 - +constexpr uint8_t FUEL_SYSTEM_STATUS = 3; // 0x03 - bit encoded +constexpr uint8_t ENGINE_LOAD = 4; // 0x04 - % +constexpr uint8_t ENGINE_COOLANT_TEMP = 5; // 0x05 - °C +constexpr uint8_t SHORT_TERM_FUEL_TRIM_BANK_1 = 6; // 0x06 - % +constexpr uint8_t LONG_TERM_FUEL_TRIM_BANK_1 = 7; // 0x07 - % +constexpr uint8_t SHORT_TERM_FUEL_TRIM_BANK_2 = 8; // 0x08 - % +constexpr uint8_t LONG_TERM_FUEL_TRIM_BANK_2 = 9; // 0x09 - % +constexpr uint8_t FUEL_PRESSURE = 10; // 0x0A - kPa +constexpr uint8_t INTAKE_MANIFOLD_ABS_PRESSURE = 11; // 0x0B - kPa +constexpr uint8_t ENGINE_RPM = 12; // 0x0C - rpm +constexpr uint8_t VEHICLE_SPEED = 13; // 0x0D - km/h +constexpr uint8_t TIMING_ADVANCE = 14; // 0x0E - ° before TDC +constexpr uint8_t INTAKE_AIR_TEMP = 15; // 0x0F - °C +constexpr uint8_t MAF_FLOW_RATE = 16; // 0x10 - g/s +constexpr uint8_t THROTTLE_POSITION = 17; // 0x11 - % +constexpr uint8_t COMMANDED_SECONDARY_AIR_STATUS = 18; // 0x12 - bit encoded +constexpr uint8_t OXYGEN_SENSORS_PRESENT_2_BANKS = 19; // 0x13 - bit encoded +constexpr uint8_t OXYGEN_SENSOR_1_A = 20; // 0x14 - V % +constexpr uint8_t OXYGEN_SENSOR_2_A = 21; // 0x15 - V % +constexpr uint8_t OXYGEN_SENSOR_3_A = 22; // 0x16 - V % +constexpr uint8_t OXYGEN_SENSOR_4_A = 23; // 0x17 - V % +constexpr uint8_t OXYGEN_SENSOR_5_A = 24; // 0x18 - V % +constexpr uint8_t OXYGEN_SENSOR_6_A = 25; // 0x19 - V % +constexpr uint8_t OXYGEN_SENSOR_7_A = 26; // 0x1A - V % +constexpr uint8_t OXYGEN_SENSOR_8_A = 27; // 0x1B - V % +constexpr uint8_t OBD_STANDARDS = 28; // 0x1C - bit encoded +constexpr uint8_t OXYGEN_SENSORS_PRESENT_4_BANKS = 29; // 0x1D - bit encoded +constexpr uint8_t AUX_INPUT_STATUS = 30; // 0x1E - bit encoded +constexpr uint8_t RUN_TIME_SINCE_ENGINE_START = 31; // 0x1F - sec + +constexpr uint8_t SUPPORTED_PIDS_21_40 = 32; // 0x20 - bit encoded +constexpr uint8_t DISTANCE_TRAVELED_WITH_MIL_ON = 33; // 0x21 - km +constexpr uint8_t FUEL_RAIL_PRESSURE = 34; // 0x22 - kPa +constexpr uint8_t FUEL_RAIL_GUAGE_PRESSURE = 35; // 0x23 - kPa +constexpr uint8_t OXYGEN_SENSOR_1_B = 36; // 0x24 - ratio V +constexpr uint8_t OXYGEN_SENSOR_2_B = 37; // 0x25 - ratio V +constexpr uint8_t OXYGEN_SENSOR_3_B = 38; // 0x26 - ratio V +constexpr uint8_t OXYGEN_SENSOR_4_B = 39; // 0x27 - ratio V +constexpr uint8_t OXYGEN_SENSOR_5_B = 40; // 0x28 - ratio V +constexpr uint8_t OXYGEN_SENSOR_6_B = 41; // 0x29 - ratio V +constexpr uint8_t OXYGEN_SENSOR_7_B = 42; // 0x2A - ratio V +constexpr uint8_t OXYGEN_SENSOR_8_B = 43; // 0x2B - ratio V +constexpr uint8_t COMMANDED_EGR = 44; // 0x2C - % +constexpr uint8_t EGR_ERROR = 45; // 0x2D - % +constexpr uint8_t COMMANDED_EVAPORATIVE_PURGE = 46; // 0x2E - % +constexpr uint8_t FUEL_TANK_LEVEL_INPUT = 47; // 0x2F - % +constexpr uint8_t WARM_UPS_SINCE_CODES_CLEARED = 48; // 0x30 - count +constexpr uint8_t DIST_TRAV_SINCE_CODES_CLEARED = 49; // 0x31 - km +constexpr uint8_t EVAP_SYSTEM_VAPOR_PRESSURE = 50; // 0x32 - Pa +constexpr uint8_t ABS_BAROMETRIC_PRESSURE = 51; // 0x33 - kPa +constexpr uint8_t OXYGEN_SENSOR_1_C = 52; // 0x34 - ratio mA +constexpr uint8_t OXYGEN_SENSOR_2_C = 53; // 0x35 - ratio mA +constexpr uint8_t OXYGEN_SENSOR_3_C = 54; // 0x36 - ratio mA +constexpr uint8_t OXYGEN_SENSOR_4_C = 55; // 0x37 - ratio mA +constexpr uint8_t OXYGEN_SENSOR_5_C = 56; // 0x38 - ratio mA +constexpr uint8_t OXYGEN_SENSOR_6_C = 57; // 0x39 - ratio mA +constexpr uint8_t OXYGEN_SENSOR_7_C = 58; // 0x3A - ratio mA +constexpr uint8_t OXYGEN_SENSOR_8_C = 59; // 0x3B - ratio mA +constexpr uint8_t CATALYST_TEMP_BANK_1_SENSOR_1 = 60; // 0x3C - °C +constexpr uint8_t CATALYST_TEMP_BANK_2_SENSOR_1 = 61; // 0x3D - °C +constexpr uint8_t CATALYST_TEMP_BANK_1_SENSOR_2 = 62; // 0x3E - °C +constexpr uint8_t CATALYST_TEMP_BANK_2_SENSOR_2 = 63; // 0x3F - °C + +constexpr uint8_t SUPPORTED_PIDS_41_60 = 64; // 0x40 - bit encoded +constexpr uint8_t MONITOR_STATUS_THIS_DRIVE_CYCLE = 65; // 0x41 - bit encoded +constexpr uint8_t CONTROL_MODULE_VOLTAGE = 66; // 0x42 - V +constexpr uint8_t ABS_LOAD_VALUE = 67; // 0x43 - % +constexpr uint8_t FUEL_AIR_COMMANDED_EQUIV_RATIO = 68; // 0x44 - ratio +constexpr uint8_t RELATIVE_THROTTLE_POSITION = 69; // 0x45 - % +constexpr uint8_t AMBIENT_AIR_TEMP = 70; // 0x46 - °C +constexpr uint8_t ABS_THROTTLE_POSITION_B = 71; // 0x47 - % +constexpr uint8_t ABS_THROTTLE_POSITION_C = 72; // 0x48 - % +constexpr uint8_t ABS_THROTTLE_POSITION_D = 73; // 0x49 - % +constexpr uint8_t ABS_THROTTLE_POSITION_E = 74; // 0x4A - % +constexpr uint8_t ABS_THROTTLE_POSITION_F = 75; // 0x4B - % +constexpr uint8_t COMMANDED_THROTTLE_ACTUATOR = 76; // 0x4C - % +constexpr uint8_t TIME_RUN_WITH_MIL_ON = 77; // 0x4D - min +constexpr uint8_t TIME_SINCE_CODES_CLEARED = 78; // 0x4E - min +constexpr uint8_t MAX_VALUES_EQUIV_V_I_PRESSURE = 79; // 0x4F - ratio V mA kPa +constexpr uint8_t MAX_MAF_RATE = 80; // 0x50 - g/s +constexpr uint8_t FUEL_TYPE = 81; // 0x51 - ref table +constexpr uint8_t ETHANOL_FUEL_PERCENT = 82; // 0x52 - % +constexpr uint8_t ABS_EVAP_SYS_VAPOR_PRESSURE = 83; // 0x53 - kPa +constexpr uint8_t EVAP_SYS_VAPOR_PRESSURE = 84; // 0x54 - Pa +constexpr uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_1_3 = 85; // 0x55 - % +constexpr uint8_t LONG_TERM_SEC_OXY_SENS_TRIM_1_3 = 86; // 0x56 - % +constexpr uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_2_4 = 87; // 0x57 - % +constexpr uint8_t LONG_TERM_SEC_OXY_SENS_TRIM_2_4 = 88; // 0x58 - % +constexpr uint8_t FUEL_RAIL_ABS_PRESSURE = 89; // 0x59 - kPa +constexpr uint8_t RELATIVE_ACCELERATOR_PEDAL_POS = 90; // 0x5A - % +constexpr uint8_t HYBRID_BATTERY_REMAINING_LIFE = 91; // 0x5B - % +constexpr uint8_t ENGINE_OIL_TEMP = 92; // 0x5C - °C +constexpr uint8_t FUEL_INJECTION_TIMING = 93; // 0x5D - ° +constexpr uint8_t ENGINE_FUEL_RATE = 94; // 0x5E - L/h +constexpr uint8_t EMISSION_REQUIREMENTS = 95; // 0x5F - bit encoded + +constexpr uint8_t SUPPORTED_PIDS_61_80 = 96; // 0x60 - bit encoded +constexpr uint8_t DEMANDED_ENGINE_PERCENT_TORQUE = 97; // 0x61 - % +constexpr uint8_t ACTUAL_ENGINE_TORQUE = 98; // 0x62 - % +constexpr uint8_t ENGINE_REFERENCE_TORQUE = 99; // 0x63 - Nm +constexpr uint8_t ENGINE_PERCENT_TORQUE_DATA = 100; // 0x64 - % +constexpr uint8_t AUX_INPUT_OUTPUT_SUPPORTED = 101; // 0x65 - bit encoded //-------------------------------------------------------------------------------------// // AT commands (https://www.sparkfun.com/datasheets/Widgets/ELM327_AT_Commands.pdf) @@ -252,27 +243,24 @@ const char * const SET_WAKEUP_MESSAGE = "AT WM"; // ISO const char * const WARM_START = "AT WS"; // General const char * const RESET_ALL = "AT Z"; // General - - - //-------------------------------------------------------------------------------------// // Class constants //-------------------------------------------------------------------------------------// -const float KPH_MPH_CONVERT = 0.6213711922; -const int8_t QUERY_LEN = 9; -const int8_t ELM_SUCCESS = 0; -const int8_t ELM_NO_RESPONSE = 1; -const int8_t ELM_BUFFER_OVERFLOW = 2; -const int8_t ELM_GARBAGE = 3; -const int8_t ELM_UNABLE_TO_CONNECT = 4; -const int8_t ELM_NO_DATA = 5; -const int8_t ELM_STOPPED = 6; -const int8_t ELM_TIMEOUT = 7; -const int8_t ELM_GETTING_MSG = 8; -const int8_t ELM_MSG_RXD = 9; -const int8_t ELM_GENERAL_ERROR = -1; -const uint8_t DTC_CODE_LEN = 6; -const uint8_t DTC_MAX_CODES = 16; +constexpr float KPH_MPH_CONVERT = 0.6213711922; +constexpr int8_t QUERY_LEN = 9; +constexpr int8_t ELM_SUCCESS = 0; +constexpr int8_t ELM_NO_RESPONSE = 1; +constexpr int8_t ELM_BUFFER_OVERFLOW = 2; +constexpr int8_t ELM_GARBAGE = 3; +constexpr int8_t ELM_UNABLE_TO_CONNECT = 4; +constexpr int8_t ELM_NO_DATA = 5; +constexpr int8_t ELM_STOPPED = 6; +constexpr int8_t ELM_TIMEOUT = 7; +constexpr int8_t ELM_GETTING_MSG = 8; +constexpr int8_t ELM_MSG_RXD = 9; +constexpr int8_t ELM_GENERAL_ERROR = -1; +constexpr uint8_t DTC_CODE_LEN = 6; +constexpr uint8_t DTC_MAX_CODES = 16; const char * const RESPONSE_OK = "OK"; const char * const RESPONSE_UNABLE_TO_CONNECT = "UNABLETOCONNECT"; @@ -287,7 +275,16 @@ typedef enum { SEND_COMMAND, DECODED_OK, ERROR } obd_cmd_states; - +// Pointers to existing response bytes, to be used for new calculators without breaking +// backward compatability with code that may use the above response bytes. +static byte response_A; +static byte response_B; +static byte response_C; +static byte response_D; +static byte response_E; +static byte response_F; +static byte response_G; +static byte response_H; class ELM327 @@ -301,7 +298,7 @@ class ELM327 char* payload; uint16_t PAYLOAD_LEN; int8_t nb_rx_state = ELM_GETTING_MSG; - uint64_t response; + uint64_t response = 0; uint16_t recBytes; uint8_t numPayChars; uint16_t timeout_ms; @@ -313,47 +310,35 @@ class ELM327 byte responseByte_5; byte responseByte_6; byte responseByte_7; - + + struct dtcResponse { uint8_t codesFound = 0; char codes[DTC_MAX_CODES][DTC_CODE_LEN]; } DTC_Response; - bool begin( Stream& stream, - const bool& debug = false, - const uint16_t& timeout = 1000, - const char& protocol = '0', - const uint16_t& payloadLen = 40, - const byte& dataTimeout = 0); - bool initializeELM(const char& protocol = '0', - const byte& dataTimeout = 0); - void flushInputBuff(); - virtual uint64_t findResponse(const uint8_t &service, - const uint8_t &pid); - void queryPID(const uint8_t& service, - const uint16_t& pid, - const uint8_t& num_responses = 1); - void queryPID(char queryStr[]); - double processPID(const uint8_t& service, - const uint16_t& pid, - const uint8_t& num_responses, - const uint8_t& numExpectedBytes, - const double& scaleFactor = 1, - const float& bias = 0); - void sendCommand(const char *cmd); - int8_t sendCommand_Blocking(const char *cmd); - int8_t get_response(); - bool timeout(); - double conditionResponse(const uint8_t& numExpectedBytes, - const double& scaleFactor = 1, - const float& bias = 0); - + bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 128, const byte& dataTimeout = 0); + ~ELM327(); + bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); + void flushInputBuff(); + uint64_t findResponse(); + void queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); + void queryPID(char queryStr[]); + double processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const float& bias = 0); + void sendCommand(const char *cmd); + int8_t sendCommand_Blocking(const char *cmd); + int8_t get_response(); + bool timeout(); + double conditionResponse(const uint8_t& numExpectedBytes, const double& scaleFactor = 1, const double& bias = 0); + double conditionResponse(double (*func)()); + double (*selectCalculator(uint16_t pid))(); float batteryVoltage(void); int8_t get_vin_blocking(char vin[]); bool resetDTC(); void currentDTCCodes(const bool& isBlocking = true); bool isPidSupported(uint8_t pid); - + void parseMultiLineResponse(); + uint32_t supportedPIDs_1_20(); uint32_t monitorStatus(); @@ -442,11 +427,34 @@ class ELM327 private: - char query[QUERY_LEN] = { '\0' }; - bool longQuery = false; - bool isMode0x22Query = false; - uint32_t currentTime; - uint32_t previousTime; + char query[QUERY_LEN] = { '\0' }; + bool longQuery = false; + bool isMode0x22Query = false; + uint32_t currentTime; + uint32_t previousTime; + double* calculator; + + static double calculator_0C(); + static double calculator_10(); + static double calculator_14(); + static double calculator_1F(); + static double calculator_22(); + static double calculator_23(); + static double calculator_24(); + static double calculator_32(); + static double calculator_3C(); + static double calculator_42(); + static double calculator_43(); + static double calculator_44(); + static double calculator_4F(); + static double calculator_50(); + static double calculator_53(); + static double calculator_54(); + static double calculator_55(); + static double calculator_59(); + static double calculator_5D(); + static double calculator_5E(); + static double calculator_61(); obd_cmd_states nb_query_state = SEND_COMMAND; // Non-blocking query state @@ -455,8 +463,10 @@ class ELM327 void formatQueryArray(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses); + uint8_t ctoi(uint8_t value); int8_t nextIndex(char const *str, char const *target, uint8_t numOccur = 1); + void removeChar(char *from, const char *remove); }; From f898b86475ef007cf9a3de469064668fcc5d7616 Mon Sep 17 00:00:00 2001 From: Jim Whitelaw Date: Thu, 13 Mar 2025 18:00:59 -0600 Subject: [PATCH 83/88] Fixes for test program --- .../ESP32_test_functions.ino | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/ESP32_Test_Functions/ESP32_test_functions.ino b/examples/ESP32_Test_Functions/ESP32_test_functions.ino index f0091c1..4984a9f 100644 --- a/examples/ESP32_Test_Functions/ESP32_test_functions.ino +++ b/examples/ESP32_Test_Functions/ESP32_test_functions.ino @@ -10,8 +10,8 @@ uint8_t current_pid = 0; int nb_query_state = SEND_COMMAND; // Define a list of PIDs to test -const uint8_t pidsToTest[] = { - SUPPORTED_PIDS_1_20, // 0x00 +const uint16_t pidsToTest[] = { + SUPPORTED_PIDS_1_20, // 0x00 MONITOR_STATUS_SINCE_DTC_CLEARED, // 0x01 FREEZE_DTC, // 0x02 FUEL_SYSTEM_STATUS, // 0x03 @@ -113,6 +113,14 @@ const uint8_t pidsToTest[] = { ENGINE_REFERENCE_TORQUE, // 0x63 ENGINE_PERCENT_TORQUE_DATA // 0x64 }; +const uint8_t responseBytes[0xA9] = +{ + 4, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, + 4, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 1, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 2, 2, 2, 2, + 4, 4, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 4, 4, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, + 4, 1, 1, 2, 5, 2, 5, 3, 3, 7, 5, 5, 5, 11, 9, 3, 10, 6, 5, 5, 5, 7, 7, 5, 9, 9, 7, 7, 9, 1, 1, 13, + 4, 41, 41, 9, 1, 10, 5, 5, 13, 41, 41, 7, 17, 1, 1, 7, 3, 5, 2, 3, 12, 9, 9, 6, 4, 17, 4, 2, 9 +}; void setup() { @@ -140,22 +148,19 @@ void setup() void loop() { - uint8_t pid = pidsToTest[current_pid]; - if (nb_query_state == SEND_COMMAND) // We are ready to send a new command + if (current_pid > (sizeof(pidsToTest)/sizeof(uint16_t)) - 1) { - char query[5] = {0}; - sprintf(query, "01%02X", pid); - myELM327.sendCommand(query); // Send the custom PID commnad - nb_query_state = WAITING_RESP; // Set the query state so we are waiting for response - } - else if (nb_query_state == WAITING_RESP) // Our query has been sent, check for a response - { - myELM327.get_response(); // Each time through the loop we will check again + current_pid = 0; } + uint16_t pid = pidsToTest[current_pid]; + double result = myELM327.processPID(0x01, pid, 1, responseBytes[current_pid], 1, 0); if (myELM327.nb_rx_state == ELM_SUCCESS) // Our response is fully received, let's get our data { - nb_query_state = SEND_COMMAND; // Reset the query state for the next command + nb_query_state = SEND_COMMAND; + DEBUG_PORT.print("Result: "); + DEBUG_PORT.println(result); + current_pid++; // Reset the query state for the next command delay(500); // Wait 0.5 seconds until we query again } From 827247c5dd14c13d389b5af193746035e35ad076 Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 18 Mar 2025 22:57:12 -0400 Subject: [PATCH 84/88] Update version --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 87464ec..1f207b4 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.3.2 +version=3.4.0 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327 From e2cc87362e04266c181b5765c8e1948ca53df21b Mon Sep 17 00:00:00 2001 From: PB2 Date: Tue, 18 Mar 2025 23:03:29 -0400 Subject: [PATCH 85/88] Update README.md --- README.md | 345 +----------------------------------------------------- 1 file changed, 3 insertions(+), 342 deletions(-) diff --git a/README.md b/README.md index 7a50b20..4c00cea 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ This is a simple yet powerful library to effortlessly interface your Arduino wit # Install: Install ELMduino using the Arduino IDE's Libraries Manager (search "ELMduino.h") +# Available Features: +See the contents of ELMduino.h for lists of supported OBD PID processing functions, OBD protocols, standard PIDs, and AT commands. The associated functions are documented in "doc strings" in ELMduino.cpp. + # Note: If you're having difficulty in connecting/keeping connection to your ELM327, try using 38400 baud instead of 115200. If you still have trouble, try all other possible bauds. Lastly, if using BluetoothSerial on an ESP32, try using the ELM327's MAC address instead of the device name "OBDII" and [remove paired devices using this sketch](https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/bt_remove_paired_devices/bt_remove_paired_devices.ino). @@ -116,345 +119,3 @@ void loop() } } ``` - -# List of Supported OBD PID Processing Functions: -```C++ -bool begin(Stream& stream, const bool& debug = false, const uint16_t& timeout = 1000, const char& protocol = '0', const uint16_t& payloadLen = 40, const byte& dataTimeout = 0); -bool initializeELM(const char& protocol = '0', const byte& dataTimeout = 0); -void flushInputBuff(); -uint64_t findResponse(); -void queryPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses = 1); -void queryPID(char queryStr[]); -float processPID(const uint8_t& service, const uint16_t& pid, const uint8_t& num_responses, const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); -void sendCommand(const char *cmd); -int8_t sendCommand_Blocking(const char *cmd); -int8_t get_response(); -bool timeout(); -float conditionResponse(const uint8_t& numExpectedBytes, const float& scaleFactor = 1, const float& bias = 0); - -float batteryVoltage(void); -int8_t get_vin_blocking(char vin[]); -bool resetDTC(); -void currentDTCCodes(const bool& isBlocking = true); - -uint32_t supportedPIDs_1_20(); - -uint32_t monitorStatus(); -uint16_t freezeDTC(); -uint16_t fuelSystemStatus(); -float engineLoad(); -float engineCoolantTemp(); -float shortTermFuelTrimBank_1(); -float longTermFuelTrimBank_1(); -float shortTermFuelTrimBank_2(); -float longTermFuelTrimBank_2(); -float fuelPressure(); -uint8_t manifoldPressure(); -float rpm(); -int32_t kph(); -float mph(); -float timingAdvance(); -float intakeAirTemp(); -float mafRate(); -float throttle(); -uint8_t commandedSecAirStatus(); -uint8_t oxygenSensorsPresent_2banks(); -uint8_t obdStandards(); -uint8_t oxygenSensorsPresent_4banks(); -bool auxInputStatus(); -uint16_t runTime(); - - -uint32_t supportedPIDs_21_40(); - -uint16_t distTravelWithMIL(); -float fuelRailPressure(); -float fuelRailGuagePressure(); -float commandedEGR(); -float egrError(); -float commandedEvapPurge(); -float fuelLevel(); -uint8_t warmUpsSinceCodesCleared(); -uint16_t distSinceCodesCleared(); -float evapSysVapPressure(); -uint8_t absBaroPressure(); -float catTempB1S1(); -float catTempB2S1(); -float catTempB1S2(); -float catTempB2S2(); - -uint32_t supportedPIDs_41_60(); - -uint32_t monitorDriveCycleStatus(); -float ctrlModVoltage(); -float absLoad(); -float commandedAirFuelRatio(); -float relativeThrottle(); -float ambientAirTemp(); -float absThrottlePosB(); -float absThrottlePosC(); -float absThrottlePosD(); -float absThrottlePosE(); -float absThrottlePosF(); -float commandedThrottleActuator(); -uint16_t timeRunWithMIL(); -uint16_t timeSinceCodesCleared(); -float maxMafRate(); -uint8_t fuelType(); -float ethanolPercent(); -float absEvapSysVapPressure(); -float evapSysVapPressure2(); -float absFuelRailPressure(); -float relativePedalPos(); -float hybridBatLife(); -float oilTemp(); -float fuelInjectTiming(); -float fuelRate(); -uint8_t emissionRqmts(); - - -uint32_t supportedPIDs_61_80(); - -float demandedTorque(); -float torque(); -uint16_t referenceTorque(); -uint16_t auxSupported(); -void printError(); -``` - -# List of OBD Protocols: -```C++ -const char AUTOMATIC = '0'; -const char SAE_J1850_PWM_41_KBAUD = '1'; -const char SAE_J1850_PWM_10_KBAUD = '2'; -const char ISO_9141_5_BAUD_INIT = '3'; -const char ISO_14230_5_BAUD_INIT = '4'; -const char ISO_14230_FAST_INIT = '5'; -const char ISO_15765_11_BIT_500_KBAUD = '6'; -const char ISO_15765_29_BIT_500_KBAUD = '7'; -const char ISO_15765_11_BIT_250_KBAUD = '8'; -const char ISO_15765_29_BIT_250_KBAUD = '9'; -const char SAE_J1939_29_BIT_250_KBAUD = 'A'; -const char USER_1_CAN = 'B'; -const char USER_2_CAN = 'C'; -``` - -# List of standard PIDs: -```C++ -const uint8_t SUPPORTED_PIDS_1_20 = 0; // 0x00 - bit encoded -const uint8_t MONITOR_STATUS_SINCE_DTC_CLEARED = 1; // 0x01 - bit encoded -const uint8_t FREEZE_DTC = 2; // 0x02 - -const uint8_t FUEL_SYSTEM_STATUS = 3; // 0x03 - bit encoded -const uint8_t ENGINE_LOAD = 4; // 0x04 - % -const uint8_t ENGINE_COOLANT_TEMP = 5; // 0x05 - °C -const uint8_t SHORT_TERM_FUEL_TRIM_BANK_1 = 6; // 0x06 - % -const uint8_t LONG_TERM_FUEL_TRIM_BANK_1 = 7; // 0x07 - % -const uint8_t SHORT_TERM_FUEL_TRIM_BANK_2 = 8; // 0x08 - % -const uint8_t LONG_TERM_FUEL_TRIM_BANK_2 = 9; // 0x09 - % -const uint8_t FUEL_PRESSURE = 10; // 0x0A - kPa -const uint8_t INTAKE_MANIFOLD_ABS_PRESSURE = 11; // 0x0B - kPa -const uint8_t ENGINE_RPM = 12; // 0x0C - rpm -const uint8_t VEHICLE_SPEED = 13; // 0x0D - km/h -const uint8_t TIMING_ADVANCE = 14; // 0x0E - ° before TDC -const uint8_t INTAKE_AIR_TEMP = 15; // 0x0F - °C -const uint8_t MAF_FLOW_RATE = 16; // 0x10 - g/s -const uint8_t THROTTLE_POSITION = 17; // 0x11 - % -const uint8_t COMMANDED_SECONDARY_AIR_STATUS = 18; // 0x12 - bit encoded -const uint8_t OXYGEN_SENSORS_PRESENT_2_BANKS = 19; // 0x13 - bit encoded -const uint8_t OXYGEN_SENSOR_1_A = 20; // 0x14 - V % -const uint8_t OXYGEN_SENSOR_2_A = 21; // 0x15 - V % -const uint8_t OXYGEN_SENSOR_3_A = 22; // 0x16 - V % -const uint8_t OXYGEN_SENSOR_4_A = 23; // 0x17 - V % -const uint8_t OXYGEN_SENSOR_5_A = 24; // 0x18 - V % -const uint8_t OXYGEN_SENSOR_6_A = 25; // 0x19 - V % -const uint8_t OXYGEN_SENSOR_7_A = 26; // 0x1A - V % -const uint8_t OXYGEN_SENSOR_8_A = 27; // 0x1B - V % -const uint8_t OBD_STANDARDS = 28; // 0x1C - bit encoded -const uint8_t OXYGEN_SENSORS_PRESENT_4_BANKS = 29; // 0x1D - bit encoded -const uint8_t AUX_INPUT_STATUS = 30; // 0x1E - bit encoded -const uint8_t RUN_TIME_SINCE_ENGINE_START = 31; // 0x1F - sec - -const uint8_t SUPPORTED_PIDS_21_40 = 32; // 0x20 - bit encoded -const uint8_t DISTANCE_TRAVELED_WITH_MIL_ON = 33; // 0x21 - km -const uint8_t FUEL_RAIL_PRESSURE = 34; // 0x22 - kPa -const uint8_t FUEL_RAIL_GUAGE_PRESSURE = 35; // 0x23 - kPa -const uint8_t OXYGEN_SENSOR_1_B = 36; // 0x24 - ratio V -const uint8_t OXYGEN_SENSOR_2_B = 37; // 0x25 - ratio V -const uint8_t OXYGEN_SENSOR_3_B = 38; // 0x26 - ratio V -const uint8_t OXYGEN_SENSOR_4_B = 39; // 0x27 - ratio V -const uint8_t OXYGEN_SENSOR_5_B = 40; // 0x28 - ratio V -const uint8_t OXYGEN_SENSOR_6_B = 41; // 0x29 - ratio V -const uint8_t OXYGEN_SENSOR_7_B = 42; // 0x2A - ratio V -const uint8_t OXYGEN_SENSOR_8_B = 43; // 0x2B - ratio V -const uint8_t COMMANDED_EGR = 44; // 0x2C - % -const uint8_t EGR_ERROR = 45; // 0x2D - % -const uint8_t COMMANDED_EVAPORATIVE_PURGE = 46; // 0x2E - % -const uint8_t FUEL_TANK_LEVEL_INPUT = 47; // 0x2F - % -const uint8_t WARM_UPS_SINCE_CODES_CLEARED = 48; // 0x30 - count -const uint8_t DIST_TRAV_SINCE_CODES_CLEARED = 49; // 0x31 - km -const uint8_t EVAP_SYSTEM_VAPOR_PRESSURE = 50; // 0x32 - Pa -const uint8_t ABS_BAROMETRIC_PRESSURE = 51; // 0x33 - kPa -const uint8_t OXYGEN_SENSOR_1_C = 52; // 0x34 - ratio mA -const uint8_t OXYGEN_SENSOR_2_C = 53; // 0x35 - ratio mA -const uint8_t OXYGEN_SENSOR_3_C = 54; // 0x36 - ratio mA -const uint8_t OXYGEN_SENSOR_4_C = 55; // 0x37 - ratio mA -const uint8_t OXYGEN_SENSOR_5_C = 56; // 0x38 - ratio mA -const uint8_t OXYGEN_SENSOR_6_C = 57; // 0x39 - ratio mA -const uint8_t OXYGEN_SENSOR_7_C = 58; // 0x3A - ratio mA -const uint8_t OXYGEN_SENSOR_8_C = 59; // 0x3B - ratio mA -const uint8_t CATALYST_TEMP_BANK_1_SENSOR_1 = 60; // 0x3C - °C -const uint8_t CATALYST_TEMP_BANK_2_SENSOR_1 = 61; // 0x3D - °C -const uint8_t CATALYST_TEMP_BANK_1_SENSOR_2 = 62; // 0x3E - °C -const uint8_t CATALYST_TEMP_BANK_2_SENSOR_2 = 63; // 0x3F - °C - -const uint8_t SUPPORTED_PIDS_41_60 = 64; // 0x40 - bit encoded -const uint8_t MONITOR_STATUS_THIS_DRIVE_CYCLE = 65; // 0x41 - bit encoded -const uint8_t CONTROL_MODULE_VOLTAGE = 66; // 0x42 - V -const uint8_t ABS_LOAD_VALUE = 67; // 0x43 - % -const uint8_t FUEL_AIR_COMMANDED_EQUIV_RATIO = 68; // 0x44 - ratio -const uint8_t RELATIVE_THROTTLE_POSITION = 69; // 0x45 - % -const uint8_t AMBIENT_AIR_TEMP = 70; // 0x46 - °C -const uint8_t ABS_THROTTLE_POSITION_B = 71; // 0x47 - % -const uint8_t ABS_THROTTLE_POSITION_C = 72; // 0x48 - % -const uint8_t ACCELERATOR_PEDAL_POSITION_D = 73; // 0x49 - % -const uint8_t ACCELERATOR_PEDAL_POSITION_E = 74; // 0x4A - % -const uint8_t ACCELERATOR_PEDAL_POSITION_F = 75; // 0x4B - % -const uint8_t COMMANDED_THROTTLE_ACTUATOR = 76; // 0x4C - % -const uint8_t TIME_RUN_WITH_MIL_ON = 77; // 0x4D - min -const uint8_t TIME_SINCE_CODES_CLEARED = 78; // 0x4E - min -const uint8_t MAX_VALUES_EQUIV_V_I_PRESSURE = 79; // 0x4F - ratio V mA kPa -const uint8_t MAX_MAF_RATE = 80; // 0x50 - g/s -const uint8_t FUEL_TYPE = 81; // 0x51 - ref table -const uint8_t ETHANOL_FUEL_PERCENT = 82; // 0x52 - % -const uint8_t ABS_EVAP_SYS_VAPOR_PRESSURE = 83; // 0x53 - kPa -const uint8_t EVAP_SYS_VAPOR_PRESSURE = 84; // 0x54 - Pa -const uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_1_3 = 85; // 0x55 - % -const uint8_t LONG_TERM_SEC_OXY_SENS_TRIM_1_3 = 86; // 0x56 - % -const uint8_t SHORT_TERM_SEC_OXY_SENS_TRIM_2_4 = 87; // 0x57 - % -const uint8_t LONG_TERM_SEC_OXY_SENS_TRIM_2_4 = 88; // 0x58 - % -const uint8_t FUEL_RAIL_ABS_PRESSURE = 89; // 0x59 - kPa -const uint8_t RELATIVE_ACCELERATOR_PEDAL_POS = 90; // 0x5A - % -const uint8_t HYBRID_BATTERY_REMAINING_LIFE = 91; // 0x5B - % -const uint8_t ENGINE_OIL_TEMP = 92; // 0x5C - °C -const uint8_t FUEL_INJECTION_TIMING = 93; // 0x5D - ° -const uint8_t ENGINE_FUEL_RATE = 94; // 0x5E - L/h -const uint8_t EMISSION_REQUIREMENTS = 95; // 0x5F - bit encoded - -const uint8_t SUPPORTED_PIDS_61_80 = 96; // 0x60 - bit encoded -const uint8_t DEMANDED_ENGINE_PERCENT_TORQUE = 97; // 0x61 - % -const uint8_t ACTUAL_ENGINE_TORQUE = 98; // 0x62 - % -const uint8_t ENGINE_REFERENCE_TORQUE = 99; // 0x63 - Nm -const uint8_t ENGINE_PERCENT_TORQUE_DATA = 100; // 0x64 - % -const uint8_t AUX_INPUT_OUTPUT_SUPPORTED = 101; // 0x65 - bit encoded -``` - -# List of AT Commands: -(https://www.sparkfun.com/datasheets/Widgets/ELM327_AT_Commands.pdf) -```C++ -const char * const DISP_DEVICE_DESCRIPT = "AT @1"; // General -const char * const DISP_DEVICE_ID = "AT @2"; // General -const char * const STORE_DEVICE_ID = "AT @3 %s"; // General -const char * const REPEAT_LAST_COMMAND = "AT \r"; // General -const char * const ALLOW_LONG_MESSAGES = "AT AL"; // General -const char * const AUTOMATIC_RECEIVE = "AT AR"; // OBD -const char * const ADAPTIVE_TIMING_OFF = "AT AT0"; // OBD -const char * const ADAPTIVE_TIMING_AUTO_1 = "AT AT1"; // OBD -const char * const ADAPTIVE_TIMING_AUTO_2 = "AT AT2"; // OBD -const char * const DUMP_BUFFER = "AT BD"; // OBD -const char * const BYPASS_INIT_SEQUENCE = "AT BI"; // OBD -const char * const TRY_BAUD_DIVISOR = "AT BRD %s"; // General -const char * const SET_HANDSHAKE_TIMEOUT = "AT BRT %s"; // General -const char * const CAN_AUTO_FORMAT_OFF = "AT CAF0"; // CAN -const char * const CAN_AUTO_FORMAT_ON = "AT CAF1"; // CAN -const char * const CAN_EXTENDED_ADDRESS_OFF = "AT CEA"; // CAN -const char * const USE_CAN_EXTENDED_ADDRESS = "AT CEA %s"; // CAN -const char * const SET_ID_FILTER = "AT CF %s"; // CAN -const char * const CAN_FLOW_CONTROL_OFF = "AT CFC0"; // CAN -const char * const CAN_FLOW_CONTROL_ON = "AT CFC1"; // CAN -const char * const SET_ID_MASK = "AT CM %s"; // CAN -const char * const SET_CAN_PRIORITY = "AT CP %s"; // CAN -const char * const SHOW_CAN_STATUS = "AT CS"; // CAN -const char * const CAN_SILENT_MODE_OFF = "AT CSM0"; // CAN -const char * const CAN_SILENT_MODE_ON = "AT CSM1"; // CAN -const char * const CALIBRATE_VOLTAGE_CUSTOM = "AT CV %s"; // Volts -const char * const RESTORE_CV_TO_FACTORY = "AT CV 0000"; // Volts -const char * const SET_ALL_TO_DEFAULTS = "AT D"; // General -const char * const DISP_DLC_OFF = "AT D0"; // CAN -const char * const DISP_DLC_ON = "AT D1"; // CAN -const char * const MONITOR_FOR_DM1_MESSAGES = "AT DM1"; // J1939 -const char * const DISP_CURRENT_PROTOCOL = "AT DP"; // OBD -const char * const DISP_CURRENT_PROTOCOL_NUM = "AT DPN"; // OBD -const char * const ECHO_OFF = "AT E0"; // General -const char * const ECHO_ON = "AT E1"; // General -const char * const FLOW_CONTROL_SET_DATA_TO = "AT FC SD %s"; // CAN -const char * const FLOW_CONTROL_SET_HEAD_TO = "AT FC SH %s"; // CAN -const char * const FLOW_CONTROL_SET_MODE_TO = "AT FC SM %s"; // CAN -const char * const FORGE_EVENTS = "AT FE"; // General -const char * const PERFORM_FAST_INIT = "AT FI"; // ISO -const char * const HEADERS_OFF = "AT H0"; // OBD -const char * const HEADERS_ON = "AT H1"; // OBD -const char * const DISP_ID = "AT I"; // General -const char * const SET_ISO_BAUD_10400 = "AT IB 10"; // ISO -const char * const SET_ISO_BAUD_4800 = "AT IB 48"; // ISO -const char * const SET_ISO_BAUD_9600 = "AT IB 96"; // ISO -const char * const IFR_VAL_FROM_HEADER = "AT IFR H"; // J1850 -const char * const IFR_VAL_FROM_SOURCE = "AT IFR S"; // J1850 -const char * const IFRS_OFF = "AT IFR0"; // J1850 -const char * const IFRS_AUTO = "AT IFR1"; // J1850 -const char * const IFRS_ON = "AT IFR2"; // J1850 -const char * const IREAD_IGNMON_INPUT_LEVEL = "AT IGN"; // Other -const char * const SET_ISO_SLOW_INIT_ADDRESS = "AT IIA %s"; // ISO -const char * const USE_J1939_ELM_DATA_FORMAT = "AT JE"; // J1850 -const char * const J1939_HEAD_FORMAT_OFF = "AT JHF0"; // J1850 -const char * const J1939_HEAD_FORMAT_ON = "AT JHF1"; // J1850 -const char * const USE_J1939_SAE_DATA_FORMAT = "AT JS"; // J1850 -const char * const SET_J1939_TIMER_X_TO_1X = "AT JTM1"; // J1850 -const char * const SET_J1939_TIMER_X_TO_5X = "AT JTM5"; // J1850 -const char * const DISP_KEY_WORDS = "AT KW"; // ISO -const char * const KEY_WORD_CHECKING_OFF = "AT KW0"; // ISO -const char * const KEY_WORD_CHECKING_ON = "AT KW1"; // ISO -const char * const LINEFEEDS_OFF = "AT L0"; // General -const char * const LINEFEEDS_ON = "AT L1"; // General -const char * const LOW_POWER_MODE = "AT LP"; // General -const char * const MEMORY_OFF = "AT M0"; // General -const char * const MEMORY_ON = "AT M1"; // General -const char * const MONITOR_ALL = "AT MA"; // OBD -const char * const MONITOR_FOR_PGN = "AT MP %s"; // J1939 -const char * const MONITOR_FOR_RECEIVER = "AT MR %s"; // OBD -const char * const MONITOR_FOR_TRANSMITTER = "AT MT %s"; // OBD -const char * const NORMAL_LENGTH_MESSAGES = "AT NL"; // OBD -const char * const SET_PROTO_OPTIONS_AND_BAUD = "AT PB %s"; // OBD -const char * const PROTOCOL_CLOSE = "AT PC"; // OBD -const char * const ALL_PROG_PARAMS_OFF = "AT PP FF OFF"; // PPs -const char * const ALL_PROG_PARAMS_ON = "AT PP FF ON"; // PPs -const char * const SET_PROG_PARAM_OFF = "AT PP %s OFF"; // PPs -const char * const SET_PROG_PARAM_ON = "AT PP %s ON"; // PPs -const char * const SET_PROG_PARAM_VAL = "AT PP %s SV %s"; // PPs -const char * const DISP_PP_SUMMARY = "AT PPS"; // PPs -const char * const RESPONSES_OFF = "AT R0"; // OBD -const char * const RESPONSES_ON = "AT R1"; // OBD -const char * const SET_RECEIVE_ADDRESS_TO = "AT RA %s"; // OBD -const char * const READ_STORED_DATA = "AT RD"; // General -const char * const SEND_RTR_MESSAGE = "AT RTR"; // CAN -const char * const READ_VOLTAGE = "AT RV"; // Volts -const char * const PRINTING_SPACES_OFF = "AT S0"; // OBD -const char * const PRINTING_SPACES_ON = "AT S1"; // OBD -const char * const STORE_DATA_BYTE = "AT SD "; // General -const char * const SET_HEADER = "AT SH %s"; // OBD -const char * const PERFORM_SLOW_INIT = "AT SI"; // ISO -const char * const SET_PROTOCOL_TO_AUTO_H_SAVE = "AT SP A%c"; // OBD -const char * const SET_PROTOCOL_TO_H_SAVE = "AT SP %c"; // OBD -const char * const SET_PROTOCOL_TO_AUTO_SAVE = "AT SP 00"; // OBD -const char * const SET_REC_ADDRESS = "AT SR %s"; // OBD -const char * const SET_STANDARD_SEARCH_ORDER = "AT SS"; // OBD -const char * const SET_TIMEOUT_TO_H_X_4MS = "AT ST %s"; // OBD -const char * const SET_WAKEUP_TO_H_X_20MS = "AT SW %s"; // ISO -const char * const SET_TESTER_ADDRESS_TO = "AT TA %s"; // OBD -const char * const TRY_PROT_H_AUTO_SEARCH = "AT TP A%s"; // OBD -const char * const TRY_PROT_H = "AT TP %s"; // OBD -const char * const VARIABLE_DLC_OFF = "AT V0"; // CAN -const char * const VARIABLE_DLC_ON = "AT V1"; // CAN -const char * const SET_WAKEUP_MESSAGE = "AT WM"; // ISO -const char * const WARM_START = "AT WS"; // General -const char * const RESET_ALL = "AT Z"; // General -``` From 52f753892179de94078519a80969cd76ab8abe4b Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Wed, 2 Apr 2025 21:57:51 +0200 Subject: [PATCH 86/88] Initialize newResponse because PAYLOAD_LEN is not a compile time constant --- src/ELMduino.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index c3d8a8b..6a442df 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2610,7 +2610,9 @@ void ELM327::parseMultiLineResponse() { uint8_t totalBytes = 0; uint8_t bytesReceived = 0; bool headerFound = false; - char newResponse[PAYLOAD_LEN] = {0}; + char newResponse[PAYLOAD_LEN]; + memset(newResponse, 0, PAYLOAD_LEN * sizeof(char)); // Initialize newResponse to empty string + char line[256] = ""; char* start = payload; char* end = strchr(start, '\r'); From a7478c07fa4318f4745658e59ed2b5faecf3e184 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Wed, 2 Apr 2025 18:22:57 -0400 Subject: [PATCH 87/88] rm unused variable --- src/ELMduino.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ELMduino.cpp b/src/ELMduino.cpp index 6a442df..59b87c9 100644 --- a/src/ELMduino.cpp +++ b/src/ELMduino.cpp @@ -2609,10 +2609,8 @@ int8_t ELM327::get_response(void) void ELM327::parseMultiLineResponse() { uint8_t totalBytes = 0; uint8_t bytesReceived = 0; - bool headerFound = false; char newResponse[PAYLOAD_LEN]; memset(newResponse, 0, PAYLOAD_LEN * sizeof(char)); // Initialize newResponse to empty string - char line[256] = ""; char* start = payload; char* end = strchr(start, '\r'); From e7c9e7b634876778f236a0eb544ea9fd5641ce14 Mon Sep 17 00:00:00 2001 From: PowerBroker2 Date: Wed, 2 Apr 2025 18:23:19 -0400 Subject: [PATCH 88/88] Update version number --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index 1f207b4..e81e23b 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ELMDuino -version=3.4.0 +version=3.4.1 author=PowerBroker2 maintainer=PowerBroker2 sentence=Arduino library to easily interface with the common OBDII scanner: ELM327

N@_mT6jKw+1tJ(~;=cRQ?*r+OlmNg3f0^L@;f% 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/H?UlpzGr|OXw^6Fs7zWZu+PZXUIG@!GZ0*USL-e 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