├── gradle.properties ├── settings.gradle.kts ├── img └── kotlin-obd-api-logo.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── encodings.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml └── kotlinScripting.xml ├── src ├── main │ └── kotlin │ │ └── com │ │ └── github │ │ └── eltonvs │ │ └── obd │ │ ├── command │ │ ├── ATCommand.kt │ │ ├── ParserFunctions.kt │ │ ├── ObdCommand.kt │ │ ├── egr │ │ │ └── Egr.kt │ │ ├── at │ │ │ ├── Actions.kt │ │ │ ├── Info.kt │ │ │ └── Mutations.kt │ │ ├── control │ │ │ ├── AvailableCommands.kt │ │ │ ├── MIL.kt │ │ │ ├── Control.kt │ │ │ ├── Monitor.kt │ │ │ └── TroubleCodes.kt │ │ ├── RegexUtils.kt │ │ ├── fuel │ │ │ ├── Ratio.kt │ │ │ └── Fuel.kt │ │ ├── temperature │ │ │ └── Temperature.kt │ │ ├── Response.kt │ │ ├── pressure │ │ │ └── Pressure.kt │ │ ├── Enums.kt │ │ ├── engine │ │ │ └── Engine.kt │ │ └── Exceptions.kt │ │ └── connection │ │ └── ObdDeviceConnection.kt └── test │ └── kotlin │ └── com │ └── github │ └── eltonvs │ └── obd │ └── command │ ├── ParserFunctions.kt │ ├── egr │ └── Egr.kt │ ├── fuel │ ├── Ratio.kt │ └── Fuel.kt │ ├── control │ ├── MIL.kt │ ├── Control.kt │ ├── Monitor.kt │ ├── AvailableCommands.kt │ └── TroubleCodes.kt │ ├── temperature │ └── Temperature.kt │ ├── pressure │ └── Pressure.kt │ └── engine │ └── Engine.kt ├── .github └── workflows │ └── ci.yml ├── gradlew.bat ├── .gitignore ├── SUPPORTED_COMMANDS.md ├── gradlew ├── README.md └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-obd-api" 2 | -------------------------------------------------------------------------------- /img/kotlin-obd-api-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltonvs/kotlin-obd-api/HEAD/img/kotlin-obd-api-logo.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eltonvs/kotlin-obd-api/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/ATCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | 4 | abstract class ATCommand : ObdCommand() { 5 | override val mode = "AT" 6 | override val skipDigitCheck = true 7 | } -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 20 00:43:35 BRT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - uses: eskatos/gradle-command-action@v1 11 | with: 12 | arguments: cleanTest test --rerun-tasks 13 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/ParserFunctions.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | import kotlin.math.pow 4 | 5 | 6 | fun bytesToInt(bufferedValue: IntArray, start: Int = 2, bytesToProcess: Int = -1): Long { 7 | var bufferToProcess = bufferedValue.drop(start) 8 | if (bytesToProcess != -1) { 9 | bufferToProcess = bufferToProcess.take(bytesToProcess) 10 | } 11 | return bufferToProcess.foldIndexed(0L) { index, total, current -> 12 | total + current * 2f.pow((bufferToProcess.size - index - 1) * 8).toLong() 13 | } 14 | } 15 | 16 | fun calculatePercentage(bufferedValue: IntArray, bytesToProcess: Int = -1): Float = 17 | (bytesToInt(bufferedValue, bytesToProcess = bytesToProcess) * 100f) / 255f 18 | 19 | fun Int.getBitAt(position: Int, last: Int = 32) = this shr (last - position) and 1 20 | 21 | fun Long.getBitAt(position: Int, last: Int = 32) = (this shr (last - position) and 1).toInt() 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/ObdCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | 4 | abstract class ObdCommand { 5 | abstract val tag: String 6 | abstract val name: String 7 | abstract val mode: String 8 | abstract val pid: String 9 | 10 | open val defaultUnit: String = "" 11 | open val skipDigitCheck: Boolean = false 12 | open val handler: (ObdRawResponse) -> String = { it.value } 13 | 14 | val rawCommand: String 15 | get() = listOf(mode, pid).joinToString(" ") 16 | 17 | fun handleResponse(rawResponse: ObdRawResponse): ObdResponse { 18 | val checkedRawResponse = BadResponseException.checkForExceptions(this, rawResponse) 19 | return ObdResponse( 20 | command = this, 21 | rawResponse = checkedRawResponse, 22 | value = handler(checkedRawResponse), 23 | unit = defaultUnit 24 | ) 25 | } 26 | 27 | open fun format(response: ObdResponse): String = "${response.value}${response.unit}" 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/egr/Egr.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.egr 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.bytesToInt 6 | import com.github.eltonvs.obd.command.calculatePercentage 7 | 8 | class CommandedEgrCommand : ObdCommand() { 9 | override val tag = "COMMANDED_EGR" 10 | override val name = "Commanded EGR" 11 | override val mode = "01" 12 | override val pid = "2C" 13 | 14 | override val defaultUnit = "%" 15 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculatePercentage(it.bufferedValue, bytesToProcess = 1)) } 16 | } 17 | 18 | class EgrErrorCommand : ObdCommand() { 19 | override val tag = "EGR_ERROR" 20 | override val name = "EGR Error" 21 | override val mode = "01" 22 | override val pid = "2D" 23 | 24 | override val defaultUnit = "%" 25 | override val handler = { it: ObdRawResponse -> "%.1f".format(bytesToInt(it.bufferedValue, bytesToProcess = 1) * (100f / 128) - 100) } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/at/Actions.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.at 2 | 3 | import com.github.eltonvs.obd.command.ATCommand 4 | 5 | 6 | class ResetAdapterCommand : ATCommand() { 7 | override val tag = "RESET_ADAPTER" 8 | override val name = "Reset OBD Adapter" 9 | override val pid = "Z" 10 | } 11 | 12 | class WarmStartCommand : ATCommand() { 13 | override val tag = "WARM_START" 14 | override val name = "OBD Warm Start" 15 | override val pid = "WS" 16 | } 17 | 18 | class SlowInitiationCommand : ATCommand() { 19 | override val tag = "SLOW_INITIATION" 20 | override val name = "OBD Slow Initiation" 21 | override val pid = "SI" 22 | } 23 | 24 | class LowPowerModeCommand : ATCommand() { 25 | override val tag = "LOW_POWER_MODE" 26 | override val name = "OBD Low Power Mode" 27 | override val pid = "LP" 28 | } 29 | 30 | class BufferDumpCommand : ATCommand() { 31 | override val tag = "BUFFER_DUMP" 32 | override val name = "OBD Buffer Dump" 33 | override val pid = "BD" 34 | } 35 | 36 | class BypassInitializationCommand : ATCommand() { 37 | override val tag = "BYPASS_INITIALIZATION" 38 | override val name = "OBD Bypass Initialization Sequence" 39 | override val pid = "BI" 40 | } 41 | 42 | class ProtocolCloseCommand : ATCommand() { 43 | override val tag = "PROTOCOL_CLOSE" 44 | override val name = "OBD Protocol Close" 45 | override val pid = "PC" 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/control/AvailableCommands.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.getBitAt 6 | 7 | 8 | class AvailablePIDsCommand(private val range: AvailablePIDsRanges) : ObdCommand() { 9 | override val tag = "AVAILABLE_COMMANDS_${range.name}" 10 | override val name = "Available Commands - ${range.displayName}" 11 | override val mode = "01" 12 | override val pid = range.pid 13 | 14 | override val defaultUnit = "" 15 | override val handler = { it: ObdRawResponse -> 16 | parsePIDs(it.processedValue).joinToString(",") { "%02X".format(it) } 17 | } 18 | 19 | private fun parsePIDs(rawValue: String): IntArray { 20 | val value = rawValue.toLong(radix = 16) 21 | val initialPID = range.pid.toInt(radix = 16) 22 | return (1..33).fold(intArrayOf()) { acc, i -> 23 | if (value.getBitAt(i) == 1) acc.plus(i + initialPID) else acc 24 | } 25 | } 26 | 27 | enum class AvailablePIDsRanges(val displayName: String, internal val pid: String) { 28 | PIDS_01_TO_20("PIDs from 01 to 20", "00"), 29 | PIDS_21_TO_40("PIDs from 21 to 40", "20"), 30 | PIDS_41_TO_60("PIDs from 41 to 60", "40"), 31 | PIDS_61_TO_80("PIDs from 61 to 80", "60"), 32 | PIDS_81_TO_A0("PIDs from 81 to A0", "80") 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/RegexUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | import java.util.regex.Pattern 4 | 5 | 6 | object RegexPatterns { 7 | val WHITESPACE_PATTERN: Pattern = Pattern.compile("\\s") 8 | val BUS_INIT_PATTERN: Pattern = Pattern.compile("(BUS INIT)|(BUSINIT)|(\\.)") 9 | val SEARCHING_PATTERN: Pattern = Pattern.compile("SEARCHING") 10 | val CARRIAGE_PATTERN: Pattern = Pattern.compile("[\r\n]") 11 | val CARRIAGE_COLON_PATTERN: Pattern = Pattern.compile("[\r\n].:") 12 | val COLON_PATTERN: Pattern = Pattern.compile(":") 13 | val DIGITS_LETTERS_PATTERN: Pattern = Pattern.compile("([0-9A-F:])+") 14 | val STARTS_WITH_ALPHANUM_PATTERN: Pattern = Pattern.compile("[^a-z0-9 ]", Pattern.CASE_INSENSITIVE) 15 | 16 | // Error patterns 17 | const val BUSINIT_ERROR_MESSAGE_PATTERN = "BUS INIT... ERROR" 18 | const val MISUNDERSTOOD_COMMAND_MESSAGE_PATTERN = "?" 19 | const val NO_DATE_MESSAGE_PATTERN = "NO DATA" 20 | const val STOPPED_MESSAGE_PATERN = "STOPPED" 21 | const val UNABLE_TO_CONNECT_MESSAGE_PATTERN = "UNABLE TO CONNECT" 22 | const val ERROR_MESSAGE_PATTERN = "ERROR" 23 | const val UNSUPPORTED_COMMAND_MESSAGE_PATTERN = "7F 0[0-A] 1[1-2]" 24 | } 25 | 26 | 27 | fun removeAll(pattern: Pattern, input: String): String { 28 | return pattern.matcher(input).replaceAll("") 29 | } 30 | 31 | fun removeAll(input: String, vararg patterns: Pattern) = 32 | patterns.fold(input) { acc, pattern -> removeAll(pattern, acc) } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/at/Info.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.at 2 | 3 | import com.github.eltonvs.obd.command.ATCommand 4 | import com.github.eltonvs.obd.command.ObdProtocols 5 | import com.github.eltonvs.obd.command.ObdRawResponse 6 | 7 | 8 | class DescribeProtocolCommand : ATCommand() { 9 | override val tag = "DESCRIBE_PROTOCOL" 10 | override val name = "Describe Protocol" 11 | override val pid = "DP" 12 | } 13 | 14 | class DescribeProtocolNumberCommand : ATCommand() { 15 | override val tag = "DESCRIBE_PROTOCOL_NUMBER" 16 | override val name = "Describe Protocol Number" 17 | override val pid = "DPN" 18 | 19 | override val handler = { it: ObdRawResponse -> parseProtocolNumber(it).displayName } 20 | 21 | private fun parseProtocolNumber(rawResponse: ObdRawResponse): ObdProtocols { 22 | val result = rawResponse.value 23 | val protocolNumber = result[if (result.length == 2) 1 else 0].toString() 24 | 25 | return ObdProtocols.values().find { it.command == protocolNumber } ?: ObdProtocols.UNKNOWN 26 | } 27 | } 28 | 29 | class IgnitionMonitorCommand : ATCommand() { 30 | override val tag = "IGNITION_MONITOR" 31 | override val name = "Ignition Monitor" 32 | override val pid = "IGN" 33 | 34 | override val handler = { it: ObdRawResponse -> it.value.trim().uppercase() } 35 | } 36 | 37 | class AdapterVoltageCommand : ATCommand() { 38 | override val tag = "ADAPTER_VOLTAGE" 39 | override val name = "OBD Adapter Voltage" 40 | override val pid = "RV" 41 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/ParserFunctions.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | import org.junit.runner.RunWith 4 | import org.junit.runners.Parameterized 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | 9 | @RunWith(Parameterized::class) 10 | class BytesToIntParameterizedTests( 11 | private val bufferedValue: IntArray, 12 | private val start: Int, 13 | private val bytesToProcess: Int, 14 | private val expected: Long 15 | ) { 16 | companion object { 17 | @JvmStatic 18 | @Parameterized.Parameters 19 | fun values() = listOf( 20 | arrayOf(intArrayOf(0x0), 0, -1, 0), 21 | arrayOf(intArrayOf(0x1), 0, -1, 1), 22 | arrayOf(intArrayOf(0x1), 0, 1, 1), 23 | arrayOf(intArrayOf(0x10), 0, -1, 16), 24 | arrayOf(intArrayOf(0x11), 0, -1, 17), 25 | arrayOf(intArrayOf(0xFF), 0, -1, 255), 26 | arrayOf(intArrayOf(0xFF, 0xFF), 0, -1, 65535), 27 | arrayOf(intArrayOf(0xFF, 0xFF), 0, 1, 255), 28 | arrayOf(intArrayOf(0xFF, 0x00), 0, -1, 65280), 29 | arrayOf(intArrayOf(0xFF, 0x00), 0, 1, 255), 30 | arrayOf(intArrayOf(0x41, 0x0D, 0x40), 2, -1, 64), 31 | arrayOf(intArrayOf(0x41, 0x0D, 0x40, 0xFF), 2, 1, 64) 32 | ) 33 | } 34 | 35 | @Test 36 | fun `test valid results for bytesToInt`() { 37 | val result = bytesToInt(bufferedValue, start = start, bytesToProcess = bytesToProcess) 38 | assertEquals(expected, result) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/control/MIL.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.ObdResponse 6 | import com.github.eltonvs.obd.command.bytesToInt 7 | 8 | 9 | class MILOnCommand : ObdCommand() { 10 | override val tag = "MIL_ON" 11 | override val name = "MIL on" 12 | override val mode = "01" 13 | override val pid = "01" 14 | 15 | override val handler = { it: ObdRawResponse -> 16 | val mil = it.bufferedValue[2] 17 | val milOn = (mil and 0x80) == 128 18 | milOn.toString() 19 | } 20 | 21 | override fun format(response: ObdResponse): String { 22 | val milOn = response.value.toBoolean() 23 | return "MIL is ${if (milOn) "ON" else "OFF"}" 24 | } 25 | } 26 | 27 | class DistanceMILOnCommand : ObdCommand() { 28 | override val tag = "DISTANCE_TRAVELED_MIL_ON" 29 | override val name = "Distance traveled with MIL on" 30 | override val mode = "01" 31 | override val pid = "21" 32 | 33 | override val defaultUnit = "Km" 34 | override val handler = { it: ObdRawResponse -> bytesToInt(it.bufferedValue).toString() } 35 | } 36 | 37 | class TimeSinceMILOnCommand : ObdCommand() { 38 | override val tag = "TIME_TRAVELED_MIL_ON" 39 | override val name = "Time run with MIL on" 40 | override val mode = "01" 41 | override val pid = "4D" 42 | 43 | override val defaultUnit = "min" 44 | override val handler = { it: ObdRawResponse -> bytesToInt(it.bufferedValue).toString() } 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/fuel/Ratio.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.fuel 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.bytesToInt 6 | 7 | 8 | private fun calculateFuelAirRatio(rawValue: IntArray): Float = bytesToInt(rawValue, bytesToProcess = 2) * (2 / 65_536f) 9 | 10 | class CommandedEquivalenceRatioCommand : ObdCommand() { 11 | override val tag = "COMMANDED_EQUIVALENCE_RATIO" 12 | override val name = "Fuel-Air Commanded Equivalence Ratio" 13 | override val mode = "01" 14 | override val pid = "44" 15 | 16 | override val defaultUnit = "F/A" 17 | override val handler = { it: ObdRawResponse -> "%.2f".format(calculateFuelAirRatio(it.bufferedValue)) } 18 | } 19 | 20 | class FuelAirEquivalenceRatioCommand(oxygenSensor: OxygenSensor) : ObdCommand() { 21 | override val tag = "FUEL_AIR_EQUIVALENCE_RATIO_${oxygenSensor.name}" 22 | override val name = "Fuel-Air Equivalence Ratio - ${oxygenSensor.displayName}" 23 | override val mode = "01" 24 | override val pid = oxygenSensor.pid 25 | 26 | override val defaultUnit = "F/A" 27 | override val handler = { it: ObdRawResponse -> "%.2f".format(calculateFuelAirRatio(it.bufferedValue)) } 28 | 29 | enum class OxygenSensor(val displayName: String, internal val pid: String) { 30 | OXYGEN_SENSOR_1("Oxygen Sensor 1", "34"), 31 | OXYGEN_SENSOR_2("Oxygen Sensor 2", "35"), 32 | OXYGEN_SENSOR_3("Oxygen Sensor 3", "36"), 33 | OXYGEN_SENSOR_4("Oxygen Sensor 4", "37"), 34 | OXYGEN_SENSOR_5("Oxygen Sensor 5", "38"), 35 | OXYGEN_SENSOR_6("Oxygen Sensor 6", "39"), 36 | OXYGEN_SENSOR_7("Oxygen Sensor 7", "3A"), 37 | OXYGEN_SENSOR_8("Oxygen Sensor 8", "3B"), 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/temperature/Temperature.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.temperature 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.bytesToInt 6 | 7 | 8 | private fun calculateTemperature(rawValue: IntArray): Float = bytesToInt(rawValue, bytesToProcess = 1) - 40f 9 | 10 | class AirIntakeTemperatureCommand : ObdCommand() { 11 | override val tag = "AIR_INTAKE_TEMPERATURE" 12 | override val name = "Air Intake Temperature" 13 | override val mode = "01" 14 | override val pid = "0F" 15 | 16 | override val defaultUnit = "°C" 17 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculateTemperature(it.bufferedValue)) } 18 | } 19 | 20 | class AmbientAirTemperatureCommand : ObdCommand() { 21 | override val tag = "AMBIENT_AIR_TEMPERATURE" 22 | override val name = "Ambient Air Temperature" 23 | override val mode = "01" 24 | override val pid = "46" 25 | 26 | override val defaultUnit = "°C" 27 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculateTemperature(it.bufferedValue)) } 28 | } 29 | 30 | class EngineCoolantTemperatureCommand : ObdCommand() { 31 | override val tag = "ENGINE_COOLANT_TEMPERATURE" 32 | override val name = "Engine Coolant Temperature" 33 | override val mode = "01" 34 | override val pid = "05" 35 | 36 | override val defaultUnit = "°C" 37 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculateTemperature(it.bufferedValue)) } 38 | } 39 | 40 | class OilTemperatureCommand : ObdCommand() { 41 | override val tag = "ENGINE_OIL_TEMPERATURE" 42 | override val name = "Engine Oil Temperature" 43 | override val mode = "01" 44 | override val pid = "5C" 45 | 46 | override val defaultUnit = "°C" 47 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculateTemperature(it.bufferedValue)) } 48 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/egr/Egr.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.egr 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | @RunWith(Parameterized::class) 10 | class CommandedEgrCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 11 | companion object { 12 | @JvmStatic 13 | @Parameterized.Parameters 14 | fun values() = listOf( 15 | arrayOf("414545", 27.1f), 16 | arrayOf("414500", 0f), 17 | arrayOf("4145FF", 100f), 18 | arrayOf("4145FFFF", 100f) 19 | ) 20 | } 21 | 22 | @Test 23 | fun `test valid commanded egr responses handler`() { 24 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 25 | val obdResponse = CommandedEgrCommand().run { 26 | handleResponse(rawResponse) 27 | } 28 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 29 | } 30 | } 31 | 32 | @RunWith(Parameterized::class) 33 | class EgrErrorCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 34 | companion object { 35 | @JvmStatic 36 | @Parameterized.Parameters 37 | fun values() = listOf( 38 | arrayOf("410610", -87.5f), 39 | arrayOf("410643", -47.7f), 40 | arrayOf("410680", 0f), 41 | arrayOf("4106C8", 56.25f), 42 | arrayOf("410600", -100f), 43 | arrayOf("4106FF", 99.2f), 44 | arrayOf("4106FFFF", 99.2f) 45 | ) 46 | } 47 | 48 | @Test 49 | fun `test valid egr error responses handler`() { 50 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 51 | val obdResponse = EgrErrorCommand().run { 52 | handleResponse(rawResponse) 53 | } 54 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/at/Mutations.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.at 2 | 3 | import com.github.eltonvs.obd.command.ATCommand 4 | import com.github.eltonvs.obd.command.AdaptiveTimingMode 5 | import com.github.eltonvs.obd.command.ObdProtocols 6 | import com.github.eltonvs.obd.command.Switcher 7 | 8 | 9 | class SelectProtocolCommand(protocol: ObdProtocols) : ATCommand() { 10 | private val _protocol = if (protocol == ObdProtocols.UNKNOWN) ObdProtocols.AUTO else protocol 11 | override val tag = "SELECT_PROTOCOL_${_protocol.name}" 12 | override val name = "Select Protocol - ${_protocol.displayName}" 13 | override val pid = "SP ${_protocol.command}" 14 | } 15 | 16 | class SetAdaptiveTimingCommand(value: AdaptiveTimingMode) : ATCommand() { 17 | override val tag = "SET_ADAPTIVE_TIMING_${value.name}" 18 | override val name = "Set Adaptive Timing Control ${value.displayName}" 19 | override val pid = "AT ${value.command}" 20 | } 21 | 22 | class SetEchoCommand(value: Switcher) : ATCommand() { 23 | override val tag = "SET_ECHO_${value.name}" 24 | override val name = "Set Echo ${value.name}" 25 | override val pid = "E${value.command}" 26 | } 27 | 28 | class SetHeadersCommand(value: Switcher) : ATCommand() { 29 | override val tag = "SET_HEADERS_${value.name}" 30 | override val name = "Set Headers ${value.name}" 31 | override val pid = "H${value.command}" 32 | } 33 | 34 | class SetLineFeedCommand(value: Switcher) : ATCommand() { 35 | override val tag = "SET_LINE_FEED_${value.name}" 36 | override val name = "Set Line Feed ${value.name}" 37 | override val pid = "L${value.command}" 38 | } 39 | 40 | class SetSpacesCommand(value: Switcher) : ATCommand() { 41 | override val tag = "SET_SPACES_${value.name}" 42 | override val name = "Set Spaces ${value.name}" 43 | override val pid = "S${value.command}" 44 | } 45 | 46 | class SetTimeoutCommand(timeout: Int) : ATCommand() { 47 | override val tag = "SET_TIMEOUT" 48 | override val name = "Set Timeout - $timeout" 49 | override val pid = "ST ${Integer.toHexString(0xFF and timeout)}" 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/Response.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | import com.github.eltonvs.obd.command.RegexPatterns.BUS_INIT_PATTERN 4 | import com.github.eltonvs.obd.command.RegexPatterns.COLON_PATTERN 5 | import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN 6 | 7 | 8 | fun T.pipe(vararg functions: (T) -> T): T = 9 | functions.fold(this) { value, f -> f(value) } 10 | 11 | 12 | data class ObdRawResponse( 13 | val value: String, 14 | val elapsedTime: Long 15 | ) { 16 | private val valueProcessorPipeline by lazy { 17 | arrayOf<(String) -> String>( 18 | { 19 | /* 20 | * Imagine the following response 41 0c 00 0d. 21 | * 22 | * ELM sends strings!! So, ELM puts spaces between each "byte". And pay 23 | * attention to the fact that I've put the word byte in quotes, because 41 24 | * is actually TWO bytes (two chars) in the socket. So, we must do some more 25 | * processing... 26 | */ 27 | removeAll(WHITESPACE_PATTERN, it) // removes all [ \t\n\x0B\f\r] 28 | }, 29 | { 30 | /* 31 | * Data may have echo or informative text like "INIT BUS..." or similar. 32 | * The response ends with two carriage return characters. So we need to take 33 | * everything from the last carriage return before those two (trimmed above). 34 | */ 35 | removeAll(BUS_INIT_PATTERN, it) 36 | }, 37 | { 38 | removeAll(COLON_PATTERN, it) 39 | } 40 | ) 41 | } 42 | 43 | val processedValue by lazy { value.pipe(*valueProcessorPipeline) } 44 | 45 | val bufferedValue by lazy { processedValue.chunked(2) { it.toString().toInt(radix = 16) }.toIntArray() } 46 | } 47 | 48 | data class ObdResponse( 49 | val command: ObdCommand, 50 | val rawResponse: ObdRawResponse, 51 | val value: String, 52 | val unit: String = "" 53 | ) { 54 | val formattedValue: String get() = command.format(this) 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/pressure/Pressure.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.pressure 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.bytesToInt 6 | 7 | 8 | class BarometricPressureCommand : ObdCommand() { 9 | override val tag = "BAROMETRIC_PRESSURE" 10 | override val name = "Barometric Pressure" 11 | override val mode = "01" 12 | override val pid = "33" 13 | 14 | override val defaultUnit = "kPa" 15 | override val handler = { it: ObdRawResponse -> bytesToInt(it.bufferedValue, bytesToProcess = 1).toString() } 16 | } 17 | 18 | class IntakeManifoldPressureCommand : ObdCommand() { 19 | override val tag = "INTAKE_MANIFOLD_PRESSURE" 20 | override val name = "Intake Manifold Pressure" 21 | override val mode = "01" 22 | override val pid = "0B" 23 | 24 | override val defaultUnit = "kPa" 25 | override val handler = { it: ObdRawResponse -> bytesToInt(it.bufferedValue, bytesToProcess = 1).toString() } 26 | } 27 | 28 | class FuelPressureCommand : ObdCommand() { 29 | override val tag = "FUEL_PRESSURE" 30 | override val name = "Fuel Pressure" 31 | override val mode = "01" 32 | override val pid = "0A" 33 | 34 | override val defaultUnit = "kPa" 35 | override val handler = { it: ObdRawResponse -> (bytesToInt(it.bufferedValue, bytesToProcess = 1) * 3).toString() } 36 | } 37 | 38 | class FuelRailPressureCommand : ObdCommand() { 39 | override val tag = "FUEL_RAIL_PRESSURE" 40 | override val name = "Fuel Rail Pressure" 41 | override val mode = "01" 42 | override val pid = "22" 43 | 44 | override val defaultUnit = "kPa" 45 | override val handler = { it: ObdRawResponse -> "%.3f".format(bytesToInt(it.bufferedValue) * 0.079) } 46 | } 47 | 48 | class FuelRailGaugePressureCommand : ObdCommand() { 49 | override val tag = "FUEL_RAIL_GAUGE_PRESSURE" 50 | override val name = "Fuel Rail Gauge Pressure" 51 | override val mode = "01" 52 | override val pid = "23" 53 | 54 | override val defaultUnit = "kPa" 55 | override val handler = { it: ObdRawResponse -> (bytesToInt(it.bufferedValue) * 10).toString() } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/fuel/Ratio.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.fuel 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class CommandedEquivalenceRatioCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | arrayOf("41441234", 0.14f), 17 | arrayOf("41444040", 0.5f), 18 | arrayOf("41448080", 1f), 19 | arrayOf("41440000", 0f), 20 | arrayOf("4144FFFF", 2f), 21 | arrayOf("4144FFFFFFFF", 2f) 22 | ) 23 | } 24 | 25 | @Test 26 | fun `test valid commanded equivalence ratio responses handler`() { 27 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 28 | val obdResponse = CommandedEquivalenceRatioCommand().run { 29 | handleResponse(rawResponse) 30 | } 31 | assertEquals("%.2fF/A".format(expected), obdResponse.formattedValue) 32 | } 33 | } 34 | 35 | 36 | @RunWith(Parameterized::class) 37 | class FuelAirEquivalenceRatioCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 38 | companion object { 39 | @JvmStatic 40 | @Parameterized.Parameters 41 | fun values() = listOf( 42 | arrayOf("41341234", 0.14f), 43 | arrayOf("41344040", 0.5f), 44 | arrayOf("41348080", 1f), 45 | arrayOf("41340000", 0f), 46 | arrayOf("4134FFFF", 2f), 47 | arrayOf("4134FFFFFFFF", 2f) 48 | ) 49 | } 50 | 51 | @Test 52 | fun `test valid fuel air equivalence ratio responses handler`() { 53 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 54 | FuelAirEquivalenceRatioCommand.OxygenSensor.values().forEach { 55 | val obdResponse = FuelAirEquivalenceRatioCommand(it).run { 56 | handleResponse(rawResponse) 57 | } 58 | assertEquals("%.2fF/A".format(expected), obdResponse.formattedValue) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/Enums.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | 4 | enum class ObdProtocols(val displayName: String, internal val command: String) { 5 | // Unknown protocol 6 | UNKNOWN("Unknown Protocol", ""), 7 | // Auto select protocol and save. 8 | AUTO("Auto", "0"), 9 | // 41.6 kbaud 10 | SAE_J1850_PWM("SAE J1850 PWM", "1"), 11 | // 10.4 kbaud 12 | SAE_J1850_VPW("SAE J1850 VPW", "2"), 13 | // 5 baud init 14 | ISO_9141_2("ISO 9141-2", "3"), 15 | // 5 baud init 16 | ISO_14230_4_KWP("ISO 14230-4 (KWP 5BAUD)", "4"), 17 | // Fast init 18 | ISO_14230_4_KWP_FAST("ISO 14230-4 (KWP FAST)", "5"), 19 | // 11 bit ID, 500 kbaud 20 | ISO_15765_4_CAN("ISO 15765-4 (CAN 11/500)", "6"), 21 | // 29 bit ID, 500 kbaud 22 | ISO_15765_4_CAN_B("ISO 15765-4 (CAN 29/500)", "7"), 23 | // 11 bit ID, 250 kbaud 24 | ISO_15765_4_CAN_C("ISO 15765-4 (CAN 11/250)", "8"), 25 | // 29 bit ID, 250 kbaud 26 | ISO_15765_4_CAN_D("ISO 15765-4 (CAN 29/250)", "9"), 27 | // 29 bit ID, 250 kbaud (user adjustable) 28 | SAE_J1939_CAN("SAE J1939 (CAN 29/250)", "A"), 29 | } 30 | 31 | enum class AdaptiveTimingMode(val displayName: String, internal val command: String) { 32 | OFF("Off", "0"), 33 | AUTO_1("Auto 1", "1"), 34 | AUTO_2("Auto 2", "2"), 35 | } 36 | 37 | enum class Switcher(internal val command: String) { 38 | ON("1"), 39 | OFF("0"), 40 | } 41 | 42 | enum class Monitors( 43 | internal val displayName: String, 44 | internal val isSparkIgnition: Boolean? = null, 45 | internal val bitPos: Int 46 | ) { 47 | // Common 48 | MISFIRE("Misfire", bitPos = 0), 49 | FUEL_SYSTEM("Fuel System", bitPos = 1), 50 | COMPREHENSIVE_COMPONENT("Comprehensive Component", bitPos = 2), 51 | // Spark Ignition Monitors 52 | CATALYST("Catalyst (CAT)", true, 0), 53 | HEATED_CATALYST("Heated Catalyst", true, 1), 54 | EVAPORATIVE_SYSTEM("Evaporative (EVAP) System", true, 2), 55 | SECONDARY_AIR_SYSTEM("Secondary Air System", true, 3), 56 | AC_REFRIGERANT("A/C Refrigerant", true, 4), 57 | OXYGEN_SENSOR("Oxygen (O2) Sensor", true, 5), 58 | OXYGEN_SENSOR_HEATER("Oxygen Sennsor Heater", true, 6), 59 | EGR_SYSTEM("EGR (Exhaust Gas Recirculation) and/or VVT System", true, 7), 60 | // Compression Ignition Monitors 61 | NMHC_CATALYST("NMHC Catalyst", false, 0), 62 | NOX_SCR_MONITOR("NOx/SCR Aftertreatment", false, 1), 63 | BOOST_PRESSURE("Boost Pressure", false, 3), 64 | EXHAUST_GAS_SENSOR("Exhaust Gas Sensor", false, 5), 65 | PM_FILTER("PM Filter", false, 6), 66 | EGR_VVT_SYSTEM("EGR (Exhaust Gas Recirculation) and/or VVT System", false, 7), 67 | } 68 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/control/Control.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.RegexPatterns.BUS_INIT_PATTERN 6 | import com.github.eltonvs.obd.command.RegexPatterns.STARTS_WITH_ALPHANUM_PATTERN 7 | import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN 8 | import com.github.eltonvs.obd.command.bytesToInt 9 | import com.github.eltonvs.obd.command.removeAll 10 | 11 | 12 | class ModuleVoltageCommand : ObdCommand() { 13 | override val tag = "CONTROL_MODULE_VOLTAGE" 14 | override val name = "Control Module Power Supply" 15 | override val mode = "01" 16 | override val pid = "42" 17 | 18 | override val defaultUnit = "V" 19 | override val handler = { it: ObdRawResponse -> "%.2f".format(bytesToInt(it.bufferedValue) / 1000f) } 20 | } 21 | 22 | class TimingAdvanceCommand : ObdCommand() { 23 | override val tag = "TIMING_ADVANCE" 24 | override val name = "Timing Advance" 25 | override val mode = "01" 26 | override val pid = "0E" 27 | 28 | override val defaultUnit = "°" 29 | override val handler = { it: ObdRawResponse -> "%.2f".format(bytesToInt(it.bufferedValue, bytesToProcess = 1) / 2f - 64) } 30 | } 31 | 32 | class VINCommand : ObdCommand() { 33 | override val tag = "VIN" 34 | override val name = "Vehicle Identification Number (VIN)" 35 | override val mode = "09" 36 | override val pid = "02" 37 | 38 | override val defaultUnit = "" 39 | override val handler = { it: ObdRawResponse -> parseVIN(removeAll(it.value, WHITESPACE_PATTERN, BUS_INIT_PATTERN)) } 40 | 41 | private fun parseVIN(rawValue: String): String { 42 | val workingData = 43 | if (rawValue.contains(":")) { 44 | // CAN(ISO-15765) protocol. 45 | // 9 is xxx490201, xxx is bytes of information to follow. 46 | val value = rawValue.replace(".:".toRegex(), "").substring(9) 47 | if (STARTS_WITH_ALPHANUM_PATTERN.matcher(convertHexToString(value)).find()) { 48 | rawValue.replace("0:49", "").replace(".:".toRegex(), "") 49 | } else { 50 | value 51 | } 52 | } else { 53 | // ISO9141-2, KWP2000 Fast and KWP2000 5Kbps (ISO15031) protocols. 54 | rawValue.replace("49020.".toRegex(), "") 55 | } 56 | return convertHexToString(workingData).replace("[\u0000-\u001f]".toRegex(), "") 57 | } 58 | 59 | private fun convertHexToString(hex: String): String = 60 | hex.chunked(2) { Integer.parseInt(it.toString(), 16).toChar() }.joinToString("") 61 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/control/MIL.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class MILOnCommandParameterizedTests(private val rawValue: String, private val expected: Boolean) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | arrayOf("410100452100", false), 17 | arrayOf("410100000000", false), 18 | arrayOf("41017F000000", false), 19 | arrayOf("41017FFFFFFF", false), 20 | arrayOf("410180000000", true), 21 | arrayOf("410180FFFFFF", true), 22 | arrayOf("4101FFFFFFFF", true) 23 | ) 24 | } 25 | 26 | @Test 27 | fun `test valid MIL on responses handler`() { 28 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 29 | val obdResponse = MILOnCommand().run { 30 | handleResponse(rawResponse) 31 | } 32 | assertEquals("MIL is ${if (expected) "ON" else "OFF"}", obdResponse.formattedValue) 33 | } 34 | } 35 | 36 | 37 | @RunWith(Parameterized::class) 38 | class DistanceMILOnCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 39 | companion object { 40 | @JvmStatic 41 | @Parameterized.Parameters 42 | fun values() = listOf( 43 | arrayOf("41210000", 0), 44 | arrayOf("41215C8D", 23_693), 45 | arrayOf("4121FFFF", 65_535) 46 | ) 47 | } 48 | 49 | @Test 50 | fun `test valid distance MIL on responses handler`() { 51 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 52 | val obdResponse = DistanceMILOnCommand().run { 53 | handleResponse(rawResponse) 54 | } 55 | assertEquals("${expected}Km", obdResponse.formattedValue) 56 | } 57 | } 58 | 59 | 60 | @RunWith(Parameterized::class) 61 | class TimeSinceMILOnCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 62 | companion object { 63 | @JvmStatic 64 | @Parameterized.Parameters 65 | fun values() = listOf( 66 | arrayOf("414D0000", 0), 67 | arrayOf("414D5C8D", 23_693), 68 | arrayOf("414DFFFF", 65_535) 69 | ) 70 | } 71 | 72 | @Test 73 | fun `test valid time since MIL on responses handler`() { 74 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 75 | val obdResponse = TimeSinceMILOnCommand().run { 76 | handleResponse(rawResponse) 77 | } 78 | assertEquals("${expected}min", obdResponse.formattedValue) 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/control/Monitor.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.Monitors 4 | import com.github.eltonvs.obd.command.ObdCommand 5 | import com.github.eltonvs.obd.command.ObdRawResponse 6 | import com.github.eltonvs.obd.command.getBitAt 7 | 8 | data class SensorStatus(val available: Boolean, val complete: Boolean) 9 | data class SensorStatusData( 10 | val milOn: Boolean, 11 | val dtcCount: Int, 12 | val isSpark: Boolean, 13 | val items: Map 14 | ) 15 | 16 | abstract class BaseMonitorStatus : ObdCommand() { 17 | override val mode = "01" 18 | 19 | override val defaultUnit = "" 20 | override val handler = { it: ObdRawResponse -> 21 | parseData(it.bufferedValue.takeLast(4)).let { "" } 22 | } 23 | 24 | var data: SensorStatusData? = null 25 | 26 | /** 27 | * Parses the Monitor Status data 28 | * 29 | * ┌Components not ready 30 | * |┌Fuel not ready 31 | * ||┌Misfire not ready 32 | * |||┌Spark vs. Compression 33 | * ||||┌Components supported 34 | * |||||┌Fuel supported 35 | * ┌MIL ||||||┌Misfire supported 36 | * | ||||||| 37 | * 10000011 00000111 11111111 00000000 38 | * [# DTC] X [supprt] [~ready] 39 | */ 40 | private fun parseData(values: List) { 41 | if (values.size != 4) { 42 | return 43 | } 44 | val milOn = values[0].getBitAt(1, 8) == 1 45 | val dtcCount = values[0] and 0x7F 46 | val isSpark = values[1].getBitAt(5, 8) == 0 47 | 48 | val monitorMap = HashMap() 49 | Monitors.values().forEach { 50 | val normalizedPos = 8 - it.bitPos 51 | if (it.isSparkIgnition == null) { 52 | val isAvailable = values[1].getBitAt(normalizedPos, 8) == 1 53 | val isComplete = values[1].getBitAt(normalizedPos - 4, 8) == 0 54 | monitorMap[it] = SensorStatus(isAvailable, isComplete) 55 | } else if (it.isSparkIgnition == isSpark) { 56 | val isAvailable = values[2].getBitAt(normalizedPos, 8) == 1 57 | val isComplete = values[3].getBitAt(normalizedPos, 8) == 0 58 | monitorMap[it] = SensorStatus(isAvailable, isComplete) 59 | } 60 | } 61 | data = SensorStatusData(milOn, dtcCount, isSpark, monitorMap) 62 | } 63 | } 64 | 65 | class MonitorStatusSinceCodesClearedCommand : BaseMonitorStatus() { 66 | override val tag = "MONITOR_STATUS_SINCE_CODES_CLEARED" 67 | override val name = "Monitor Status Since Codes Cleared" 68 | override val pid = "01" 69 | } 70 | 71 | class MonitorStatusCurrentDriveCycleCommand : BaseMonitorStatus() { 72 | override val tag = "MONITOR_STATUS_CURRENT_DRIVE_CYCLE" 73 | override val name = "Monitor Status Current Drive Cycle" 74 | override val pid = "41" 75 | } 76 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/control/Control.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class ModuleVoltageCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | arrayOf("414204E2", 1.25f), 17 | arrayOf("41420000", 0f), 18 | arrayOf("4142FFFF", 65.535f) 19 | ) 20 | } 21 | 22 | @Test 23 | fun `test valid module voltage responses handler`() { 24 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 25 | val obdResponse = ModuleVoltageCommand().run { 26 | handleResponse(rawResponse) 27 | } 28 | assertEquals("%.2fV".format(expected), obdResponse.formattedValue) 29 | } 30 | } 31 | 32 | 33 | @RunWith(Parameterized::class) 34 | class TimingAdvanceCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 35 | companion object { 36 | @JvmStatic 37 | @Parameterized.Parameters 38 | fun values() = listOf( 39 | arrayOf("410E70", -8f), 40 | arrayOf("410E00", -64f), 41 | arrayOf("410EFF", 63.5f), 42 | arrayOf("410EFFFF", 63.5f) 43 | ) 44 | } 45 | 46 | @Test 47 | fun `test valid timing advance responses handler`() { 48 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 49 | val obdResponse = TimingAdvanceCommand().run { 50 | handleResponse(rawResponse) 51 | } 52 | assertEquals("%.2f°".format(expected), obdResponse.formattedValue) 53 | } 54 | } 55 | 56 | 57 | @RunWith(Parameterized::class) 58 | class VINCommandParameterizedTests(private val rawValue: String, private val expected: String) { 59 | companion object { 60 | @JvmStatic 61 | @Parameterized.Parameters 62 | fun values() = listOf( 63 | // CAN (ISO-15765) format 64 | arrayOf("0140:4902013933591:425352375248452:4A323938313136", "93YBSR7RHEJ298116"), 65 | arrayOf("0140:4902015750301:5A5A5A39395A542:53333932313234", "WP0ZZZ99ZTS392124"), 66 | // ISO9141-2, KWP2000 Fast and KWP2000 5Kbps (ISO15031) format 67 | arrayOf("490201000000394902023359425349020352375248490204454A323949020538313136", "93YBSR7RHEJ298116"), 68 | arrayOf("4902010000005749020250305A5A4902035A39395A4902045453333949020532313234", "WP0ZZZ99ZTS392124"), 69 | arrayOf("014 0: 49 02 01 39 42 47 1: 4B 54 34 38 56 30 4A 2: 47 31 34 31 38 30 39", "9BGKT48V0JG141809") 70 | ) 71 | } 72 | 73 | @Test 74 | fun `test valid vin responses handler`() { 75 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 76 | val obdResponse = VINCommand().run { 77 | handleResponse(rawResponse) 78 | } 79 | assertEquals(expected, obdResponse.formattedValue) 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/connection/ObdDeviceConnection.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.connection 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.ObdResponse 6 | import com.github.eltonvs.obd.command.RegexPatterns.SEARCHING_PATTERN 7 | import com.github.eltonvs.obd.command.removeAll 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.runBlocking 11 | import kotlinx.coroutines.withContext 12 | import java.io.InputStream 13 | import java.io.OutputStream 14 | import kotlin.system.measureTimeMillis 15 | 16 | 17 | class ObdDeviceConnection( 18 | private val inputStream: InputStream, 19 | private val outputStream: OutputStream 20 | ) { 21 | private val responseCache = mutableMapOf() 22 | 23 | suspend fun run( 24 | command: ObdCommand, 25 | useCache: Boolean = false, 26 | delayTime: Long = 0, 27 | maxRetries: Int = 5, 28 | ): ObdResponse = runBlocking { 29 | val obdRawResponse = 30 | if (useCache && responseCache[command] != null) { 31 | responseCache.getValue(command) 32 | } else { 33 | runCommand(command, delayTime, maxRetries).also { 34 | // Save response to cache 35 | if (useCache) { 36 | responseCache[command] = it 37 | } 38 | } 39 | } 40 | command.handleResponse(obdRawResponse) 41 | } 42 | 43 | private suspend fun runCommand(command: ObdCommand, delayTime: Long, maxRetries: Int): ObdRawResponse { 44 | var rawData = "" 45 | val elapsedTime = measureTimeMillis { 46 | sendCommand(command, delayTime) 47 | rawData = readRawData(maxRetries) 48 | } 49 | return ObdRawResponse(rawData, elapsedTime) 50 | } 51 | 52 | private suspend fun sendCommand(command: ObdCommand, delayTime: Long) = runBlocking { 53 | withContext(Dispatchers.IO) { 54 | outputStream.write("${command.rawCommand}\r".toByteArray()) 55 | outputStream.flush() 56 | if (delayTime > 0) { 57 | delay(delayTime) 58 | } 59 | } 60 | } 61 | 62 | private suspend fun readRawData(maxRetries: Int): String = runBlocking { 63 | var b: Byte 64 | var c: Char 65 | val res = StringBuffer() 66 | var retriesCount = 0 67 | 68 | withContext(Dispatchers.IO) { 69 | // read until '>' arrives OR end of stream reached (-1) 70 | while (retriesCount <= maxRetries) { 71 | if (inputStream.available() > 0) { 72 | b = inputStream.read().toByte() 73 | if (b < 0) { 74 | break 75 | } 76 | c = b.toInt().toChar() 77 | if (c == '>') { 78 | break 79 | } 80 | res.append(c) 81 | } else { 82 | retriesCount += 1 83 | delay(500) 84 | } 85 | } 86 | removeAll(SEARCHING_PATTERN, res.toString()).trim() 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/fuel/Fuel.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.fuel 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.bytesToInt 6 | import com.github.eltonvs.obd.command.calculatePercentage 7 | 8 | 9 | class FuelConsumptionRateCommand : ObdCommand() { 10 | override val tag = "FUEL_CONSUMPTION_RATE" 11 | override val name = "Fuel Consumption Rate" 12 | override val mode = "01" 13 | override val pid = "5E" 14 | 15 | override val defaultUnit = "L/h" 16 | override val handler = { it: ObdRawResponse -> "%.1f".format(bytesToInt(it.bufferedValue) * 0.05) } 17 | } 18 | 19 | class FuelTypeCommand : ObdCommand() { 20 | override val tag = "FUEL_TYPE" 21 | override val name = "Fuel Type" 22 | override val mode = "01" 23 | override val pid = "51" 24 | 25 | override val handler = { it: ObdRawResponse -> getFuelType(bytesToInt(it.bufferedValue, bytesToProcess = 1).toInt()) } 26 | 27 | private fun getFuelType(code: Int): String = when (code) { 28 | 0x00 -> "Not Available" 29 | 0x01 -> "Gasoline" 30 | 0x02 -> "Methanol" 31 | 0x03 -> "Ethanol" 32 | 0x04 -> "Diesel" 33 | 0x05 -> "GPL/LGP" 34 | 0x06 -> "Natural Gas" 35 | 0x07 -> "Propane" 36 | 0x08 -> "Electric" 37 | 0x09 -> "Biodiesel + Gasoline" 38 | 0x0A -> "Biodiesel + Methanol" 39 | 0x0B -> "Biodiesel + Ethanol" 40 | 0x0C -> "Biodiesel + GPL/LGP" 41 | 0x0D -> "Biodiesel + Natural Gas" 42 | 0x0E -> "Biodiesel + Propane" 43 | 0x0F -> "Biodiesel + Electric" 44 | 0x10 -> "Biodiesel + Gasoline/Electric" 45 | 0x11 -> "Hybrid Gasoline" 46 | 0x12 -> "Hybrid Ethanol" 47 | 0x13 -> "Hybrid Diesel" 48 | 0x14 -> "Hybrid Electric" 49 | 0x15 -> "Hybrid Mixed" 50 | 0x16 -> "Hybrid Regenerative" 51 | else -> "Unknown" 52 | } 53 | } 54 | 55 | class FuelLevelCommand : ObdCommand() { 56 | override val tag = "FUEL_LEVEL" 57 | override val name = "Fuel Level" 58 | override val mode = "01" 59 | override val pid = "2F" 60 | 61 | override val defaultUnit = "%" 62 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculatePercentage(it.bufferedValue, bytesToProcess = 1)) } 63 | } 64 | 65 | class EthanolLevelCommand : ObdCommand() { 66 | override val tag = "ETHANOL_LEVEL" 67 | override val name = "Ethanol Level" 68 | override val mode = "01" 69 | override val pid = "52" 70 | 71 | override val defaultUnit = "%" 72 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculatePercentage(it.bufferedValue, bytesToProcess = 1)) } 73 | } 74 | 75 | class FuelTrimCommand(fuelTrimBank: FuelTrimBank) : ObdCommand() { 76 | override val tag = fuelTrimBank.name 77 | override val name = fuelTrimBank.displayName 78 | override val mode = "01" 79 | override val pid = fuelTrimBank.pid 80 | 81 | override val defaultUnit = "%" 82 | override val handler = { it: ObdRawResponse -> "%.1f".format(bytesToInt(it.bufferedValue, bytesToProcess = 1) * (100f / 128) - 100) } 83 | 84 | enum class FuelTrimBank(val displayName: String, internal val pid: String) { 85 | SHORT_TERM_BANK_1("Short Term Fuel Trim Bank 1", "06"), 86 | SHORT_TERM_BANK_2("Short Term Fuel Trim Bank 2", "07"), 87 | LONG_TERM_BANK_1("Long Term Fuel Trim Bank 1", "08"), 88 | LONG_TERM_BANK_2("Long Term Fuel Trim Bank 2", "09"), 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/engine/Engine.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.engine 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.bytesToInt 6 | import com.github.eltonvs.obd.command.calculatePercentage 7 | 8 | 9 | class SpeedCommand : ObdCommand() { 10 | override val tag = "SPEED" 11 | override val name = "Vehicle Speed" 12 | override val mode = "01" 13 | override val pid = "0D" 14 | 15 | override val defaultUnit = "Km/h" 16 | override val handler = { it: ObdRawResponse -> bytesToInt(it.bufferedValue, bytesToProcess = 1).toString() } 17 | } 18 | 19 | class RPMCommand : ObdCommand() { 20 | override val tag = "ENGINE_RPM" 21 | override val name = "Engine RPM" 22 | override val mode = "01" 23 | override val pid = "0C" 24 | 25 | override val defaultUnit = "RPM" 26 | override val handler = { it: ObdRawResponse -> (bytesToInt(it.bufferedValue) / 4).toString() } 27 | } 28 | 29 | class MassAirFlowCommand : ObdCommand() { 30 | override val tag = "MAF" 31 | override val name = "Mass Air Flow" 32 | override val mode = "01" 33 | override val pid = "10" 34 | 35 | override val defaultUnit = "g/s" 36 | override val handler = { it: ObdRawResponse -> "%.2f".format(bytesToInt(it.bufferedValue) / 100f) } 37 | } 38 | 39 | class RuntimeCommand : ObdCommand() { 40 | override val tag = "ENGINE_RUNTIME" 41 | override val name = "Engine Runtime" 42 | override val mode = "01" 43 | override val pid = "0F" 44 | 45 | override val handler = { it: ObdRawResponse -> parseRuntime(it.bufferedValue) } 46 | 47 | private fun parseRuntime(rawValue: IntArray): String { 48 | val seconds = bytesToInt(rawValue) 49 | val hh = seconds / 3600 50 | val mm = (seconds % 3600) / 60 51 | val ss = seconds % 60 52 | return listOf(hh, mm, ss).joinToString(":") { it.toString().padStart(2, '0') } 53 | } 54 | } 55 | 56 | class LoadCommand : ObdCommand() { 57 | override val tag = "ENGINE_LOAD" 58 | override val name = "Engine Load" 59 | override val mode = "01" 60 | override val pid = "04" 61 | 62 | override val defaultUnit = "%" 63 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculatePercentage(it.bufferedValue, bytesToProcess = 1)) } 64 | } 65 | 66 | class AbsoluteLoadCommand : ObdCommand() { 67 | override val tag = "ENGINE_ABSOLUTE_LOAD" 68 | override val name = "Engine Absolute Load" 69 | override val mode = "01" 70 | override val pid = "43" 71 | 72 | override val defaultUnit = "%" 73 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculatePercentage(it.bufferedValue)) } 74 | } 75 | 76 | class ThrottlePositionCommand : ObdCommand() { 77 | override val tag = "THROTTLE_POSITION" 78 | override val name = "Throttle Position" 79 | override val mode = "01" 80 | override val pid = "11" 81 | 82 | override val defaultUnit = "%" 83 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculatePercentage(it.bufferedValue, bytesToProcess = 1)) } 84 | } 85 | 86 | class RelativeThrottlePositionCommand : ObdCommand() { 87 | override val tag = "RELATIVE_THROTTLE_POSITION" 88 | override val name = "Relative Throttle Position" 89 | override val mode = "01" 90 | override val pid = "45" 91 | 92 | override val defaultUnit = "%" 93 | override val handler = { it: ObdRawResponse -> "%.1f".format(calculatePercentage(it.bufferedValue, bytesToProcess = 1)) } 94 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command 2 | 3 | import com.github.eltonvs.obd.command.RegexPatterns.BUSINIT_ERROR_MESSAGE_PATTERN 4 | import com.github.eltonvs.obd.command.RegexPatterns.DIGITS_LETTERS_PATTERN 5 | import com.github.eltonvs.obd.command.RegexPatterns.ERROR_MESSAGE_PATTERN 6 | import com.github.eltonvs.obd.command.RegexPatterns.MISUNDERSTOOD_COMMAND_MESSAGE_PATTERN 7 | import com.github.eltonvs.obd.command.RegexPatterns.NO_DATE_MESSAGE_PATTERN 8 | import com.github.eltonvs.obd.command.RegexPatterns.STOPPED_MESSAGE_PATERN 9 | import com.github.eltonvs.obd.command.RegexPatterns.UNABLE_TO_CONNECT_MESSAGE_PATTERN 10 | import com.github.eltonvs.obd.command.RegexPatterns.UNSUPPORTED_COMMAND_MESSAGE_PATTERN 11 | import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN 12 | 13 | 14 | private fun String.sanitize(): String = removeAll(WHITESPACE_PATTERN, this).uppercase() 15 | 16 | abstract class BadResponseException(private val command: ObdCommand, private val response: ObdRawResponse) : 17 | RuntimeException() { 18 | companion object { 19 | fun checkForExceptions(command: ObdCommand, response: ObdRawResponse): ObdRawResponse = 20 | with(response.value.sanitize()) { 21 | when { 22 | contains(BUSINIT_ERROR_MESSAGE_PATTERN.sanitize()) -> 23 | throw BusInitException(command, response) 24 | contains(MISUNDERSTOOD_COMMAND_MESSAGE_PATTERN.sanitize()) -> 25 | throw MisunderstoodCommandException(command, response) 26 | contains(NO_DATE_MESSAGE_PATTERN.sanitize()) -> 27 | throw NoDataException(command, response) 28 | contains(STOPPED_MESSAGE_PATERN.sanitize()) -> 29 | throw StoppedException(command, response) 30 | contains(UNABLE_TO_CONNECT_MESSAGE_PATTERN.sanitize()) -> 31 | throw UnableToConnectException(command, response) 32 | contains(ERROR_MESSAGE_PATTERN.sanitize()) -> 33 | throw UnknownErrorException(command, response) 34 | matches(UNSUPPORTED_COMMAND_MESSAGE_PATTERN.toRegex()) -> 35 | throw UnSupportedCommandException(command, response) 36 | !command.skipDigitCheck && !matches(DIGITS_LETTERS_PATTERN.toRegex()) -> 37 | throw NonNumericResponseException(command, response) 38 | else -> response 39 | } 40 | } 41 | } 42 | 43 | override fun toString(): String = 44 | "${this.javaClass.simpleName} while executing command [${command.tag}], response [${response.value}]" 45 | } 46 | 47 | 48 | private typealias BRE = BadResponseException 49 | 50 | class NonNumericResponseException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 51 | class BusInitException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 52 | class MisunderstoodCommandException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 53 | class NoDataException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 54 | class StoppedException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 55 | class UnableToConnectException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 56 | class UnknownErrorException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 57 | class UnSupportedCommandException(command: ObdCommand, response: ObdRawResponse) : BRE(command, response) 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/macos,kotlin,gradle,intellij+iml 2 | # Edit at https://www.gitignore.io/?templates=macos,kotlin,gradle,intellij+iml 3 | 4 | ### Intellij+iml ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # Gradle and Maven with auto-import 32 | # When using Gradle or Maven with auto-import, you should exclude module files, 33 | # since they will be recreated, and may cause churn. Uncomment if using 34 | # auto-import. 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | 39 | # CMake 40 | cmake-build-*/ 41 | 42 | # Mongo Explorer plugin 43 | .idea/**/mongoSettings.xml 44 | 45 | # File-based project format 46 | *.iws 47 | 48 | # IntelliJ 49 | out/ 50 | 51 | # mpeltonen/sbt-idea plugin 52 | .idea_modules/ 53 | 54 | # JIRA plugin 55 | atlassian-ide-plugin.xml 56 | 57 | # Cursive Clojure plugin 58 | .idea/replstate.xml 59 | 60 | # Crashlytics plugin (for Android Studio and IntelliJ) 61 | com_crashlytics_export_strings.xml 62 | crashlytics.properties 63 | crashlytics-build.properties 64 | fabric.properties 65 | 66 | # Editor-based Rest Client 67 | .idea/httpRequests 68 | 69 | # Android studio 3.1+ serialized cache file 70 | .idea/caches/build_file_checksums.ser 71 | 72 | ### Intellij+iml Patch ### 73 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 74 | 75 | *.iml 76 | modules.xml 77 | .idea/misc.xml 78 | *.ipr 79 | 80 | ### Kotlin ### 81 | # Compiled class file 82 | *.class 83 | 84 | # Log file 85 | *.log 86 | 87 | # BlueJ files 88 | *.ctxt 89 | 90 | # Mobile Tools for Java (J2ME) 91 | .mtj.tmp/ 92 | 93 | # Package Files # 94 | *.jar 95 | *.war 96 | *.nar 97 | *.ear 98 | *.zip 99 | *.tar.gz 100 | *.rar 101 | 102 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 103 | hs_err_pid* 104 | 105 | ### macOS ### 106 | # General 107 | .DS_Store 108 | .AppleDouble 109 | .LSOverride 110 | 111 | # Icon must end with two \r 112 | Icon 113 | 114 | # Thumbnails 115 | ._* 116 | 117 | # Files that might appear in the root of a volume 118 | .DocumentRevisions-V100 119 | .fseventsd 120 | .Spotlight-V100 121 | .TemporaryItems 122 | .Trashes 123 | .VolumeIcon.icns 124 | .com.apple.timemachine.donotpresent 125 | 126 | # Directories potentially created on remote AFP share 127 | .AppleDB 128 | .AppleDesktop 129 | Network Trash Folder 130 | Temporary Items 131 | .apdisk 132 | 133 | ### Gradle ### 134 | .gradle 135 | build/ 136 | 137 | # Ignore Gradle GUI config 138 | gradle-app.setting 139 | 140 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 141 | !gradle-wrapper.jar 142 | 143 | # Cache of project 144 | .gradletasknamecache 145 | 146 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 147 | # gradle/wrapper/gradle-wrapper.properties 148 | 149 | ### Gradle Patch ### 150 | **/build/ 151 | 152 | # End of https://www.gitignore.io/api/macos,kotlin,gradle,intellij+iml 153 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/temperature/Temperature.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.temperature 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class AirIntakeTemperatureCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | arrayOf("410F40", 24f), 17 | arrayOf("410F5D", 53f), 18 | arrayOf("410F00", -40f), 19 | arrayOf("410FFF", 215f), 20 | arrayOf("410FFFFF", 215f) 21 | ) 22 | } 23 | 24 | @Test 25 | fun `test valid air intake temperature responses handler`() { 26 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 27 | val obdResponse = AirIntakeTemperatureCommand().run { 28 | handleResponse(rawResponse) 29 | } 30 | assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) 31 | } 32 | } 33 | 34 | 35 | @RunWith(Parameterized::class) 36 | class AmbientAirTemperatureCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 37 | companion object { 38 | @JvmStatic 39 | @Parameterized.Parameters 40 | fun values() = listOf( 41 | arrayOf("414640", 24f), 42 | arrayOf("41465D", 53f), 43 | arrayOf("414600", -40f), 44 | arrayOf("4146FF", 215f), 45 | arrayOf("4146FFFF", 215f) 46 | ) 47 | } 48 | 49 | @Test 50 | fun `test valid ambient air intake temperature responses handler`() { 51 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 52 | val obdResponse = AmbientAirTemperatureCommand().run { 53 | handleResponse(rawResponse) 54 | } 55 | assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) 56 | } 57 | } 58 | 59 | 60 | @RunWith(Parameterized::class) 61 | class EngineCoolantTemperatureCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 62 | companion object { 63 | @JvmStatic 64 | @Parameterized.Parameters 65 | fun values() = listOf( 66 | arrayOf("410540", 24f), 67 | arrayOf("41055D", 53f), 68 | arrayOf("410500", -40f), 69 | arrayOf("4105FF", 215f), 70 | arrayOf("4105FFFF", 215f) 71 | ) 72 | } 73 | 74 | @Test 75 | fun `test valid engine coolant temperature responses handler`() { 76 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 77 | val obdResponse = EngineCoolantTemperatureCommand().run { 78 | handleResponse(rawResponse) 79 | } 80 | assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) 81 | } 82 | } 83 | 84 | 85 | @RunWith(Parameterized::class) 86 | class OilTemperatureCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 87 | companion object { 88 | @JvmStatic 89 | @Parameterized.Parameters 90 | fun values() = listOf( 91 | arrayOf("415C40", 24f), 92 | arrayOf("415C5D", 53f), 93 | arrayOf("415C00", -40f), 94 | arrayOf("415CFF", 215f), 95 | arrayOf("415CFFFF", 215f) 96 | ) 97 | } 98 | 99 | @Test 100 | fun `test valid oil temperature responses handler`() { 101 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 102 | val obdResponse = OilTemperatureCommand().run { 103 | handleResponse(rawResponse) 104 | } 105 | assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) 106 | } 107 | } -------------------------------------------------------------------------------- /SUPPORTED_COMMANDS.md: -------------------------------------------------------------------------------- 1 | # Supported Commands 2 | 3 | Full list of supported commands. 4 | 5 | ## `AT` Commands (ELM327) 6 | | Command | Name | Description | 7 | | :- | :- | :-| 8 | | `Z` | `RESET_ADAPTER` | Reset OBD Adapter | 9 | | `WS` | `WARM_START` | OBD Warm Start | 10 | | `SI` | `SLOW_INITIATION` | OBD Slow Initiation | 11 | | `LP` | `LOW_POWER_MODE` | OBD Low Power Mode | 12 | | `BD` | `BUFFER_DUMP` | OBD Buffer Dump | 13 | | `BI` | `BYPASS_INITIALIZATION` | OBD Bypass Initialization Sequence | 14 | | `PC` | `PROTOCOL_CLOSE` | OBD Protocol Close | 15 | | `DP` | `DESCRIBE_PROTOCOL` | Describe Protocol | 16 | | `DPN` | `DESCRIBE_PROTOCOL_NUMBER` | Describe Protocol Number | 17 | | `IGN` | `IGNITION_MONITOR` | Ignition Monitor | 18 | | `RN` | `ADAPTER_VOLTAGE` | OBD Adapter Voltage | 19 | | `SP {X}` | `SELECT_PROTOCOL_{X}` | Select Protocol where `{X}` is an `ObdProtocols` constant | 20 | | `AT {X}` | `SET_ADAPTIVE_TIMING_{X}` | Set Adaptive Timing Control where `{X}` is an `AdaptiveTimingMode` constant | 21 | | `E{X}` | `SET_ECHO_{X}` | Set Echo where `{X}` is a `Switcher` constant | 22 | | `H{X}` | `SET_HEADERS_{X}` | Set Headers where `{X}` is a `Switcher` constant | 23 | | `L{X}` | `SET_LINE_FEED_{X}` | Set Line Feed where `{X}` is a `Switcher` constant | 24 | | `S{X}` | `SET_SPACES_{X}` | Set Spaces where `{X}` is a `Switcher` constant | 25 | | `ST {X}` | `SET_TIMEOUT` | Set Timeout where `{X}` is an `Int` value | 26 | 27 | ## Mode 01 28 | | Command | Name | Description | 29 | | -- | :- | :-| 30 | | `00`, `20`, `40`, `60`, `80` | `AVAILABLE_COMMANDS_{RANGE}` | Available PIDs for each range, where `{RANGE}` is an `AvailablePIDsRanges` constant | 31 | | `01` | `DTC_NUMBER` | Diagnostic Trouble Codes Number | 32 | | `01` | `MIL_ON` | MIL ON/OFF | 33 | | `01` | `MONITOR_STATUS_SINCE_CODES_CLEARED` | Monitor Status Since Codes Cleared | 34 | | `04` | `ENGINE_LOAD` | Engine Load | 35 | | `05` | `ENGINE_COOLANT_TEMPERATURE` | Engine Coolant Temperature | 36 | | `06` | `SHORT_TERM_BANK_1` | Short Term Fuel Trim Bank 1 | 37 | | `07` | `SHORT_TERM_BANK_2` | Short Term Fuel Trim Bank 2 | 38 | | `08` | `LONG_TERM_BANK_1` | Long Term Fuel Trim Bank 1 | 39 | | `09` | `LONG_TERM_BANK_2` | Long Term Fuel Trim Bank 2 | 40 | | `0A` | `FUEL_PRESSURE` | Fuel Pressure | 41 | | `0B` | `INTAKE_MANIFOLD_PRESSURE` | Intake Manifold Pressure | 42 | | `0C` | `ENGINE_RPM` | Engine RPM | 43 | | `0D` | `SPEED` | Vehicle Speed | 44 | | `0E` | `TIMING_ADVANCE` | Timing Advance | 45 | | `0F` | `AIR_INTAKE_TEMPERATURE` | Air Intake Temperature | 46 | | `10` | `MAF` | Mass Air Flow | 47 | | `11` | `THROTTLE_POSITION` | Throttle Position | 48 | | `1F` | `ENGINE_RUNTIME` | Engine Runtime | 49 | | `21` | `DISTANCE_TRAVELED_MIL_ON` | Distance traveled with MIL on | 50 | | `22` | `FUEL_RAIL_PRESSURE` | Fuel Rail Pressure | 51 | | `23` | `FUEL_RAIL_GAUGE_PRESSURE` | Fuel Rail Gauge Pressure | 52 | | `2C` | `COMMANDED_EGR` | Commanded EGR | 53 | | `2D` | `EGR_ERROR` | EGR Error | 54 | | `2F` | `FUEL_LEVEL` | Fuel Level | 55 | | `31` | `DISTANCE_TRAVELED_AFTER_CODES_CLEARED` | Distance traveled since codes cleared | 56 | | `33` | `BAROMETRIC_PRESSURE` | Barometric Pressure | 57 | | `34` | `OXYGEN_SENSOR_1` | Oxygen Sensor 1 | 58 | | `35` | `OXYGEN_SENSOR_2` | Oxygen Sensor 2 | 59 | | `36` | `OXYGEN_SENSOR_3` | Oxygen Sensor 3 | 60 | | `37` | `OXYGEN_SENSOR_4` | Oxygen Sensor 4 | 61 | | `38` | `OXYGEN_SENSOR_5` | Oxygen Sensor 5 | 62 | | `39` | `OXYGEN_SENSOR_6` | Oxygen Sensor 6 | 63 | | `3A` | `OXYGEN_SENSOR_7` | Oxygen Sensor 7 | 64 | | `3B` | `OXYGEN_SENSOR_8` | Oxygen Sensor 8 | 65 | | `41` | `MONITOR_STATUS_CURRENT_DRIVE_CYCLE` | Monitor Status Current Drive Cycle | 66 | | `42` | `CONTROL_MODULE_VOLTAGE` | Control Module Power Supply | 67 | | `43` | `ENGINE_ABSOLUTE_LOAD` | Engine Absolute Load | 68 | | `44` | `COMMANDED_EQUIVALENCE_RATIO` | Fuel-Air Commanded Equivalence Ratio | 69 | | `45` | `RELATIVE_THROTTLE_POSITION` | Relative Throttle Position | 70 | | `46` | `AMBIENT_AIR_TEMPERATURE` | Ambient Air Temperature | 71 | | `4D` | `TIME_TRAVELED_MIL_ON` | Time run with MIL on | 72 | | `4E` | `TIME_SINCE_CODES_CLEARED` | Time since codes cleared | 73 | | `51` | `FUEL_TYPE` | Fuel Type | 74 | | `52` | `ETHANOL_LEVEL` | Ethanol Level | 75 | | `5C` | `ENGINE_OIL_TEMPERATURE` | Engine Oil Temperature | 76 | | `5E` | `FUEL_CONSUMPTION_RATE` | Fuel Consumption Rate | 77 | 78 | 79 | ## Mode 03 80 | 81 | | Name | Description | 82 | | :- | :- | 83 | | `TROUBLE_CODES` | Trouble Codes | 84 | 85 | 86 | ## Mode 04 87 | 88 | | Name | Description | 89 | | :- | :- | 90 | | `RESET_TROUBLE_CODES` | Reset Trouble Codes | 91 | 92 | 93 | ## Mode 07 94 | 95 | | Name | Description | 96 | | :- | :- | 97 | | `PENDING_TROUBLE_CODES` | Pending Trouble Codes | 98 | 99 | 100 | ## Mode 09 101 | 102 | | Command | Name | Description | 103 | | :- | :- | :-| 104 | | `02` | `VIN` | Vehicle Identification Number (VIN) | 105 | 106 | 107 | ## Mode 0A 108 | 109 | | Name | Description | 110 | | :- | :- | 111 | | `PERMANENT_TROUBLE_CODES` | Permanent Trouble Codes | 112 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/pressure/Pressure.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.pressure 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class BarometricPressureCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | arrayOf("413312", 18), 17 | arrayOf("413340", 64), 18 | arrayOf("413364", 100), 19 | arrayOf("413380", 128), 20 | arrayOf("413300", 0), 21 | arrayOf("4133FF", 255), 22 | arrayOf("4133FFFF", 255) 23 | ) 24 | } 25 | 26 | @Test 27 | fun `test valid barometric pressure responses handler`() { 28 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 29 | val obdResponse = BarometricPressureCommand().run { 30 | handleResponse(rawResponse) 31 | } 32 | assertEquals("${expected}kPa", obdResponse.formattedValue) 33 | } 34 | } 35 | 36 | 37 | @RunWith(Parameterized::class) 38 | class IntakeManifoldPressureCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 39 | companion object { 40 | @JvmStatic 41 | @Parameterized.Parameters 42 | fun values() = listOf( 43 | arrayOf("410B12", 18), 44 | arrayOf("410B39", 57), 45 | arrayOf("410B40", 64), 46 | arrayOf("410B64", 100), 47 | arrayOf("410B80", 128), 48 | arrayOf("410B00", 0), 49 | arrayOf("410BFF", 255), 50 | arrayOf("410BFFFF", 255) 51 | ) 52 | } 53 | 54 | @Test 55 | fun `test valid intake manifold pressure responses handler`() { 56 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 57 | val obdResponse = IntakeManifoldPressureCommand().run { 58 | handleResponse(rawResponse) 59 | } 60 | assertEquals("${expected}kPa", obdResponse.formattedValue) 61 | } 62 | } 63 | 64 | @RunWith(Parameterized::class) 65 | class FuelPressureCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 66 | companion object { 67 | @JvmStatic 68 | @Parameterized.Parameters 69 | fun values() = listOf( 70 | arrayOf("410A12", 54), 71 | arrayOf("410A40", 192), 72 | arrayOf("410A64", 300), 73 | arrayOf("410A80", 384), 74 | arrayOf("410A00", 0), 75 | arrayOf("410AFF", 765), 76 | arrayOf("410AFFFF", 765) 77 | ) 78 | } 79 | 80 | @Test 81 | fun `test valid fuel pressure responses handler`() { 82 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 83 | val obdResponse = FuelPressureCommand().run { 84 | handleResponse(rawResponse) 85 | } 86 | assertEquals("${expected}kPa", obdResponse.formattedValue) 87 | } 88 | } 89 | 90 | @RunWith(Parameterized::class) 91 | class FuelRailPressureCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 92 | companion object { 93 | @JvmStatic 94 | @Parameterized.Parameters 95 | fun values() = listOf( 96 | arrayOf("41230000", 0.000f), 97 | arrayOf("410B39", 4.503f), 98 | arrayOf("410B6464", 2030.300f), 99 | arrayOf("4123FFFF", 5177.265f) 100 | ) 101 | } 102 | 103 | @Test 104 | fun `test valid fuel rail pressure responses handler`() { 105 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 106 | val obdResponse = FuelRailPressureCommand().run { 107 | handleResponse(rawResponse) 108 | } 109 | 110 | assertEquals("%.3f".format(expected) + "kPa", obdResponse.formattedValue) 111 | } 112 | } 113 | 114 | @RunWith(Parameterized::class) 115 | class FuelRailGaugePressureCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 116 | companion object { 117 | @JvmStatic 118 | @Parameterized.Parameters 119 | fun values() = listOf( 120 | arrayOf("41231234", 46_600), 121 | arrayOf("41234354", 172_360), 122 | arrayOf("412360ED", 248_130), 123 | arrayOf("41238080", 328_960), 124 | arrayOf("41230000", 0), 125 | arrayOf("4123FFFF", 655_350) 126 | ) 127 | } 128 | 129 | @Test 130 | fun `test valid fuel rail gauge pressure responses handler`() { 131 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 132 | val obdResponse = FuelRailGaugePressureCommand().run { 133 | handleResponse(rawResponse) 134 | } 135 | assertEquals("${expected}kPa", obdResponse.formattedValue) 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/eltonvs/obd/command/control/TroubleCodes.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.ObdCommand 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import com.github.eltonvs.obd.command.RegexPatterns.CARRIAGE_COLON_PATTERN 6 | import com.github.eltonvs.obd.command.RegexPatterns.CARRIAGE_PATTERN 7 | import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN 8 | import com.github.eltonvs.obd.command.bytesToInt 9 | import com.github.eltonvs.obd.command.removeAll 10 | import java.util.regex.Pattern 11 | 12 | 13 | class DTCNumberCommand : ObdCommand() { 14 | override val tag = "DTC_NUMBER" 15 | override val name = "Diagnostic Trouble Codes Number" 16 | override val mode = "01" 17 | override val pid = "01" 18 | 19 | override val defaultUnit = " codes" 20 | override val handler = { it: ObdRawResponse -> 21 | val mil = it.bufferedValue[2] 22 | val codeCount = mil and 0x7F 23 | codeCount.toString() 24 | } 25 | } 26 | 27 | class DistanceSinceCodesClearedCommand : ObdCommand() { 28 | override val tag = "DISTANCE_TRAVELED_AFTER_CODES_CLEARED" 29 | override val name = "Distance traveled since codes cleared" 30 | override val mode = "01" 31 | override val pid = "31" 32 | 33 | override val defaultUnit = "Km" 34 | override val handler = { it: ObdRawResponse -> bytesToInt(it.bufferedValue).toString() } 35 | } 36 | 37 | class TimeSinceCodesClearedCommand : ObdCommand() { 38 | override val tag = "TIME_SINCE_CODES_CLEARED" 39 | override val name = "Time since codes cleared" 40 | override val mode = "01" 41 | override val pid = "4E" 42 | 43 | override val defaultUnit = "min" 44 | override val handler = { it: ObdRawResponse -> bytesToInt(it.bufferedValue).toString() } 45 | } 46 | 47 | class ResetTroubleCodesCommand : ObdCommand() { 48 | override val tag = "RESET_TROUBLE_CODES" 49 | override val name = "Reset Trouble Codes" 50 | override val mode = "04" 51 | override val pid = "" 52 | } 53 | 54 | abstract class BaseTroubleCodesCommand : ObdCommand() { 55 | override val pid = "" 56 | 57 | override val handler = { it: ObdRawResponse -> parseTroubleCodesList(it.value).joinToString(separator = ",") } 58 | 59 | abstract val carriageNumberPattern: Pattern 60 | 61 | var troubleCodesList = listOf() 62 | private set 63 | 64 | private fun parseTroubleCodesList(rawValue: String): List { 65 | val canOneFrame: String = removeAll(rawValue, CARRIAGE_PATTERN, WHITESPACE_PATTERN) 66 | val canOneFrameLength: Int = canOneFrame.length 67 | 68 | val workingData = 69 | when { 70 | /* CAN(ISO-15765) protocol one frame: 43yy[codes] 71 | Header is 43yy, yy showing the number of data items. */ 72 | (canOneFrameLength <= 16) and (canOneFrameLength % 4 == 0) -> canOneFrame.drop(4) 73 | /* CAN(ISO-15765) protocol two and more frames: xxx43yy[codes] 74 | Header is xxx43yy, xxx is bytes of information to follow, yy showing the number of data items. */ 75 | rawValue.contains(":") -> removeAll(CARRIAGE_COLON_PATTERN, rawValue).drop(7) 76 | // ISO9141-2, KWP2000 Fast and KWP2000 5Kbps (ISO15031) protocols. 77 | else -> removeAll(rawValue, carriageNumberPattern, WHITESPACE_PATTERN) 78 | } 79 | 80 | /* For each chunk of 4 chars: 81 | it: "0100" 82 | HEX: 0 1 0 0 83 | BIN: 00000001 00000000 84 | [][][ hex ] 85 | | / / 86 | DTC: P0100 */ 87 | val troubleCodesList = workingData.chunked(4) { 88 | val b1 = it.first().toString().toInt(radix = 16) 89 | val ch1 = (b1 shr 2) and 0b11 90 | val ch2 = b1 and 0b11 91 | "${DTC_LETTERS[ch1]}${HEX_ARRAY[ch2]}${it.drop(1)}".padEnd(5, '0') 92 | } 93 | 94 | val idx = troubleCodesList.indexOf("P0000") 95 | return (if (idx < 0) troubleCodesList else troubleCodesList.take(idx)).also { 96 | this.troubleCodesList = it 97 | } 98 | } 99 | 100 | protected companion object { 101 | private val DTC_LETTERS = charArrayOf('P', 'C', 'B', 'U') 102 | private val HEX_ARRAY = "0123456789ABCDEF".toCharArray() 103 | } 104 | } 105 | 106 | class TroubleCodesCommand : BaseTroubleCodesCommand() { 107 | override val tag = "TROUBLE_CODES" 108 | override val name = "Trouble Codes" 109 | override val mode = "03" 110 | 111 | override val carriageNumberPattern: Pattern = Pattern.compile("^43|[\r\n]43|[\r\n]") 112 | } 113 | 114 | class PendingTroubleCodesCommand : BaseTroubleCodesCommand() { 115 | override val tag = "PENDING_TROUBLE_CODES" 116 | override val name = "Pending Trouble Codes" 117 | override val mode = "07" 118 | 119 | override val carriageNumberPattern: Pattern = Pattern.compile("^47|[\r\n]47|[\r\n]") 120 | } 121 | 122 | class PermanentTroubleCodesCommand : BaseTroubleCodesCommand() { 123 | override val tag = "PERMANENT_TROUBLE_CODES" 124 | override val name = "Permanent Trouble Codes" 125 | override val mode = "0A" 126 | 127 | override val carriageNumberPattern: Pattern = Pattern.compile("^4A|[\r\n]4A|[\r\n]") 128 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/control/Monitor.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.Monitors 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import org.junit.runner.RunWith 6 | import org.junit.runners.Parameterized 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | private val completeStatus = SensorStatus(available = true, complete = true) 11 | private val incompleteStatus = SensorStatus(available = true, complete = false) 12 | private val notAvailableCompleteStatus = SensorStatus(available = false, complete = true) 13 | private val notAvailableIncompleteStatus = SensorStatus(available = false, complete = false) 14 | 15 | private val expected1 = SensorStatusData( 16 | milOn = true, 17 | dtcCount = 3, 18 | isSpark = true, 19 | items = Monitors.values().filter { it.isSparkIgnition ?: true }.map { it to completeStatus }.toMap() 20 | ) 21 | private val expected2 = SensorStatusData( 22 | milOn = false, 23 | dtcCount = 0, 24 | isSpark = false, 25 | items = mapOf( 26 | Monitors.MISFIRE to incompleteStatus, 27 | Monitors.FUEL_SYSTEM to notAvailableIncompleteStatus, 28 | Monitors.COMPREHENSIVE_COMPONENT to notAvailableIncompleteStatus, 29 | Monitors.NMHC_CATALYST to incompleteStatus, 30 | Monitors.NOX_SCR_MONITOR to incompleteStatus, 31 | Monitors.BOOST_PRESSURE to notAvailableCompleteStatus, 32 | Monitors.EXHAUST_GAS_SENSOR to notAvailableCompleteStatus, 33 | Monitors.PM_FILTER to notAvailableCompleteStatus, 34 | Monitors.EGR_VVT_SYSTEM to notAvailableCompleteStatus 35 | ) 36 | ) 37 | private val expected3 = SensorStatusData( 38 | milOn = false, 39 | dtcCount = 0, 40 | isSpark = true, 41 | items = mapOf( 42 | Monitors.CATALYST to completeStatus, 43 | Monitors.EGR_SYSTEM to incompleteStatus, 44 | Monitors.SECONDARY_AIR_SYSTEM to incompleteStatus, 45 | Monitors.COMPREHENSIVE_COMPONENT to completeStatus, 46 | Monitors.OXYGEN_SENSOR_HEATER to incompleteStatus, 47 | Monitors.HEATED_CATALYST to completeStatus, 48 | Monitors.FUEL_SYSTEM to completeStatus, 49 | Monitors.OXYGEN_SENSOR to completeStatus, 50 | Monitors.MISFIRE to completeStatus, 51 | Monitors.EVAPORATIVE_SYSTEM to notAvailableCompleteStatus, 52 | Monitors.AC_REFRIGERANT to notAvailableCompleteStatus 53 | ) 54 | ) 55 | private val expected4 = SensorStatusData( 56 | milOn = false, 57 | dtcCount = 0, 58 | isSpark = true, 59 | items = Monitors.values().filter { it.isSparkIgnition ?: true }.map { it to completeStatus }.toMap() 60 | ) 61 | private val expected5 = SensorStatusData( 62 | milOn = false, 63 | dtcCount = 0, 64 | isSpark = false, 65 | items = mapOf( 66 | Monitors.FUEL_SYSTEM to notAvailableCompleteStatus, 67 | Monitors.NMHC_CATALYST to incompleteStatus, 68 | Monitors.EXHAUST_GAS_SENSOR to incompleteStatus, 69 | Monitors.MISFIRE to notAvailableCompleteStatus, 70 | Monitors.PM_FILTER to notAvailableCompleteStatus, 71 | Monitors.BOOST_PRESSURE to notAvailableCompleteStatus, 72 | Monitors.EGR_VVT_SYSTEM to notAvailableCompleteStatus, 73 | Monitors.NOX_SCR_MONITOR to notAvailableCompleteStatus, 74 | Monitors.COMPREHENSIVE_COMPONENT to notAvailableIncompleteStatus 75 | ) 76 | ) 77 | 78 | @RunWith(Parameterized::class) 79 | class MonitorStatusSinceCodesClearedCommandTests(private val rawValue: String, private val expected: SensorStatusData) { 80 | companion object { 81 | @JvmStatic 82 | @Parameterized.Parameters 83 | fun values() = listOf( 84 | arrayOf("41018307FF00", expected1), 85 | arrayOf("41 01 83 07 FF 00", expected1), 86 | arrayOf("8307FF00", expected1), 87 | arrayOf("410100790303", expected2), 88 | arrayOf("41 01 00 79 03 03", expected2), 89 | arrayOf("00790303", expected2), 90 | arrayOf("41010007EBC8", expected3), 91 | arrayOf("41 01 00 07 EB C8", expected3), 92 | arrayOf("0007EBC8", expected3) 93 | ) 94 | } 95 | 96 | @Test 97 | fun `test valid monitor status since CC responses handler`() { 98 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 99 | val obdResponse = MonitorStatusSinceCodesClearedCommand().also { 100 | it.handleResponse(rawResponse) 101 | } 102 | assertEquals(expected, obdResponse.data) 103 | } 104 | } 105 | 106 | @RunWith(Parameterized::class) 107 | class MonitorStatusCurrentDriveCycleCommandTests(private val rawValue: String, private val expected: SensorStatusData) { 108 | companion object { 109 | @JvmStatic 110 | @Parameterized.Parameters 111 | fun values() = listOf( 112 | arrayOf("41410007FF00", expected4), 113 | arrayOf("41 41 00 07 FF 00", expected4), 114 | arrayOf("0007FF00", expected4), 115 | arrayOf("414100790303", expected2), 116 | arrayOf("41 41 00 79 03 03", expected2), 117 | arrayOf("00790303", expected2), 118 | arrayOf("414100482135", expected5), 119 | arrayOf("41 41 00 48 21 35", expected5), 120 | arrayOf("00482135", expected5) 121 | ) 122 | } 123 | 124 | @Test 125 | fun `test valid monitor status current drive cycle responses handler`() { 126 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 127 | val obdResponse = MonitorStatusCurrentDriveCycleCommand().also { 128 | it.handleResponse(rawResponse) 129 | } 130 | assertEquals(expected, obdResponse.data) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/fuel/Fuel.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.fuel 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class FuelConsumptionRateCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | arrayOf("415E10E3", 216.2f), 17 | arrayOf("415E1234", 233f), 18 | arrayOf("415E0000", 0f), 19 | arrayOf("415EFFFF", 3276.75f) 20 | ) 21 | } 22 | 23 | @Test 24 | fun `test valid fuel consumption responses handler`() { 25 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 26 | val obdResponse = FuelConsumptionRateCommand().run { 27 | handleResponse(rawResponse) 28 | } 29 | assertEquals("%.1fL/h".format(expected), obdResponse.formattedValue) 30 | } 31 | } 32 | 33 | 34 | @RunWith(Parameterized::class) 35 | class FuelTypeCommandParameterizedTests(private val rawValue: String, private val expected: String) { 36 | companion object { 37 | @JvmStatic 38 | @Parameterized.Parameters 39 | fun values() = listOf( 40 | arrayOf("415100", "Not Available"), 41 | arrayOf("415101", "Gasoline"), 42 | arrayOf("415102", "Methanol"), 43 | arrayOf("415103", "Ethanol"), 44 | arrayOf("415104", "Diesel"), 45 | arrayOf("415105", "GPL/LGP"), 46 | arrayOf("415106", "Natural Gas"), 47 | arrayOf("415107", "Propane"), 48 | arrayOf("415108", "Electric"), 49 | arrayOf("415109", "Biodiesel + Gasoline"), 50 | arrayOf("41510A", "Biodiesel + Methanol"), 51 | arrayOf("41510B", "Biodiesel + Ethanol"), 52 | arrayOf("41510C", "Biodiesel + GPL/LGP"), 53 | arrayOf("41510D", "Biodiesel + Natural Gas"), 54 | arrayOf("41510E", "Biodiesel + Propane"), 55 | arrayOf("41510F", "Biodiesel + Electric"), 56 | arrayOf("415110", "Biodiesel + Gasoline/Electric"), 57 | arrayOf("415111", "Hybrid Gasoline"), 58 | arrayOf("415112", "Hybrid Ethanol"), 59 | arrayOf("415113", "Hybrid Diesel"), 60 | arrayOf("415114", "Hybrid Electric"), 61 | arrayOf("415115", "Hybrid Mixed"), 62 | arrayOf("415116", "Hybrid Regenerative"), 63 | arrayOf("415116FF", "Hybrid Regenerative"), 64 | arrayOf("4151FF", "Unknown"), 65 | arrayOf("4151FFFF", "Unknown") 66 | ) 67 | } 68 | 69 | @Test 70 | fun `test valid fuel type responses handler`() { 71 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 72 | val obdResponse = FuelTypeCommand().run { 73 | handleResponse(rawResponse) 74 | } 75 | assertEquals(expected, obdResponse.formattedValue) 76 | } 77 | } 78 | 79 | 80 | @RunWith(Parameterized::class) 81 | class GenericFuelLevelCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 82 | companion object { 83 | @JvmStatic 84 | @Parameterized.Parameters 85 | fun values() = listOf( 86 | arrayOf("412F10", 6.3f), 87 | arrayOf("412FC8", 78.4f), 88 | arrayOf("412F00", 0f), 89 | arrayOf("412FFF", 100f), 90 | arrayOf("412FFFFF", 100f) 91 | ) 92 | } 93 | 94 | @Test 95 | fun `test valid fuel level responses handler`() { 96 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 97 | val obdResponse = FuelLevelCommand().run { 98 | handleResponse(rawResponse) 99 | } 100 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 101 | } 102 | 103 | @Test 104 | fun `test valid ethanol level responses handler`() { 105 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 106 | val obdResponse = EthanolLevelCommand().run { 107 | handleResponse(rawResponse) 108 | } 109 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 110 | } 111 | } 112 | 113 | 114 | @RunWith(Parameterized::class) 115 | class GenericFuelTrimCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 116 | companion object { 117 | @JvmStatic 118 | @Parameterized.Parameters 119 | fun values() = listOf( 120 | arrayOf("410610", -87.5f), 121 | arrayOf("410643", -47.7f), 122 | arrayOf("410680", 0f), 123 | arrayOf("4106C8", 56.25f), 124 | arrayOf("410600", -100f), 125 | arrayOf("4106FF", 99.2f), 126 | arrayOf("4106FFFF", 99.2f) 127 | ) 128 | } 129 | 130 | @Test 131 | fun `test valid fuel trim short term bank 1 responses handler`() { 132 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 133 | val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.SHORT_TERM_BANK_1).run { 134 | handleResponse(rawResponse) 135 | } 136 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 137 | } 138 | 139 | @Test 140 | fun `test valid fuel trim short term bank 2 responses handler`() { 141 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 142 | val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.SHORT_TERM_BANK_2).run { 143 | handleResponse(rawResponse) 144 | } 145 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 146 | } 147 | 148 | @Test 149 | fun `test valid fuel trim long term bank 1 responses handler`() { 150 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 151 | val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.LONG_TERM_BANK_1).run { 152 | handleResponse(rawResponse) 153 | } 154 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 155 | } 156 | 157 | @Test 158 | fun `test valid fuel trim long term bank 2 responses handler`() { 159 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 160 | val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.LONG_TERM_BANK_2).run { 161 | handleResponse(rawResponse) 162 | } 163 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 164 | } 165 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/engine/Engine.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.engine 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class SpeedCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | arrayOf("410D15", 21), 17 | arrayOf("410D40", 64), 18 | arrayOf("410D00", 0), 19 | arrayOf("410DFF", 255), 20 | arrayOf("410DFFFF", 255) 21 | ) 22 | } 23 | 24 | @Test 25 | fun `test valid speed responses handler`() { 26 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 27 | val obdResponse = SpeedCommand().run { 28 | handleResponse(rawResponse) 29 | } 30 | assertEquals("${expected}Km/h", obdResponse.formattedValue) 31 | } 32 | } 33 | 34 | 35 | @RunWith(Parameterized::class) 36 | class RPMCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 37 | companion object { 38 | @JvmStatic 39 | @Parameterized.Parameters 40 | fun values() = listOf( 41 | arrayOf("410C200D", 2051), 42 | arrayOf("410C283C", 2575), 43 | arrayOf("410C0A00", 640), 44 | arrayOf("410C0000", 0), 45 | arrayOf("410CFFFF", 16_383) 46 | ) 47 | } 48 | 49 | @Test 50 | fun `test valid rpm responses handler`() { 51 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 52 | val obdResponse = RPMCommand().run { 53 | handleResponse(rawResponse) 54 | } 55 | assertEquals("${expected}RPM", obdResponse.formattedValue) 56 | } 57 | } 58 | 59 | 60 | @RunWith(Parameterized::class) 61 | class MassAirFlowCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 62 | companion object { 63 | @JvmStatic 64 | @Parameterized.Parameters 65 | fun values() = listOf( 66 | arrayOf("41109511", 381.61f), 67 | arrayOf("41101234", 46.6f), 68 | arrayOf("41100000", 0f), 69 | arrayOf("4110FFFF", 655.35f) 70 | ) 71 | } 72 | 73 | @Test 74 | fun `test valid maf responses handler`() { 75 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 76 | val obdResponse = MassAirFlowCommand().run { 77 | handleResponse(rawResponse) 78 | } 79 | assertEquals("%.2fg/s".format(expected), obdResponse.formattedValue) 80 | } 81 | } 82 | 83 | 84 | @RunWith(Parameterized::class) 85 | class RuntimeCommandParameterizedTests(private val rawValue: String, private val expected: String) { 86 | companion object { 87 | @JvmStatic 88 | @Parameterized.Parameters 89 | fun values() = listOf( 90 | arrayOf("411F4543", "04:55:31"), 91 | arrayOf("411F1234", "01:17:40"), 92 | arrayOf("411F0000", "00:00:00"), 93 | arrayOf("411FFFFF", "18:12:15") 94 | ) 95 | } 96 | 97 | @Test 98 | fun `test valid runtime responses handler`() { 99 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 100 | val obdResponse = RuntimeCommand().run { 101 | handleResponse(rawResponse) 102 | } 103 | assertEquals(expected, obdResponse.formattedValue) 104 | } 105 | } 106 | 107 | 108 | @RunWith(Parameterized::class) 109 | class LoadCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 110 | companion object { 111 | @JvmStatic 112 | @Parameterized.Parameters 113 | fun values() = listOf( 114 | arrayOf("410410", 6.3f), 115 | arrayOf("410400", 0f), 116 | arrayOf("4104FF", 100f), 117 | arrayOf("4104FFFF", 100f) 118 | ) 119 | } 120 | 121 | @Test 122 | fun `test valid engine load responses handler`() { 123 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 124 | val obdResponse = LoadCommand().run { 125 | handleResponse(rawResponse) 126 | } 127 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 128 | } 129 | } 130 | 131 | 132 | @RunWith(Parameterized::class) 133 | class AbsoluteLoadCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 134 | companion object { 135 | @JvmStatic 136 | @Parameterized.Parameters 137 | fun values() = listOf( 138 | arrayOf("41434143", 6551.8f), 139 | arrayOf("41431234", 1827.5f), 140 | arrayOf("41430000", 0f), 141 | arrayOf("4143FFFF", 25_700f) 142 | ) 143 | } 144 | 145 | @Test 146 | fun `test valid engine absolute load responses handler`() { 147 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 148 | val obdResponse = AbsoluteLoadCommand().run { 149 | handleResponse(rawResponse) 150 | } 151 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 152 | } 153 | } 154 | 155 | 156 | @RunWith(Parameterized::class) 157 | class ThrottlePositionCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 158 | companion object { 159 | @JvmStatic 160 | @Parameterized.Parameters 161 | fun values() = listOf( 162 | arrayOf("411111", 6.7f), 163 | arrayOf("411100", 0f), 164 | arrayOf("4111FF", 100f), 165 | arrayOf("4111FFFF", 100f) 166 | ) 167 | } 168 | 169 | @Test 170 | fun `test valid throttle position responses handler`() { 171 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 172 | val obdResponse = ThrottlePositionCommand().run { 173 | handleResponse(rawResponse) 174 | } 175 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 176 | } 177 | } 178 | 179 | 180 | @RunWith(Parameterized::class) 181 | class RelativeThrottlePositionCommandParameterizedTests(private val rawValue: String, private val expected: Float) { 182 | companion object { 183 | @JvmStatic 184 | @Parameterized.Parameters 185 | fun values() = listOf( 186 | arrayOf("414545", 27.1f), 187 | arrayOf("414500", 0f), 188 | arrayOf("4145FF", 100f), 189 | arrayOf("4145FFFF", 100f) 190 | ) 191 | } 192 | 193 | @Test 194 | fun `test valid relative throttle position responses handler`() { 195 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 196 | val obdResponse = RelativeThrottlePositionCommand().run { 197 | handleResponse(rawResponse) 198 | } 199 | assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) 200 | } 201 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

Kotlin OBD API

6 | 7 | [![GitHub release](https://img.shields.io/github/v/release/eltonvs/kotlin-obd-api)](https://github.com/eltonvs/kotlin-obd-api/releases) 8 | [![CI Status](https://github.com/eltonvs/kotlin-obd-api/workflows/CI/badge.svg)](https://github.com/eltonvs/kotlin-obd-api/actions?query=workflow%3ACI) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/e70f6ab78bdae20de178/maintainability)](https://codeclimate.com/github/eltonvs/kotlin-obd-api/maintainability) 10 | [![codebeat badge](https://codebeat.co/badges/6af3e4ad-1171-4868-871f-9e5e6be63df9)](https://codebeat.co/projects/github-com-eltonvs-kotlin-obd-api-master) 11 | [![GitHub license](https://img.shields.io/github/license/eltonvs/kotlin-obd-api)](https://github.com/eltonvs/kotlin-obd-api/blob/master/LICENSE) 12 | [![Open Source](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://opensource.org/) 13 | 14 | 15 | A lightweight and developer-driven API to query and parse OBD commands. 16 | 17 | Written in pure Kotlin and platform agnostic with a simple and easy to use interface, so you can hack your car without any hassle. :blue_car: 18 | 19 | This is a flexible API that allows developers to plug-in to any connection interface (Bluetooth, Wifi, USB...). By default we use an `ObdDeviceConnection` that receives an `InputStream` and an `OutputStream` as parameters (so if you can get this from your connection interface, you're good to go :thumbsup:). 20 | 21 | 22 | ## Installation 23 | 24 | 25 | ### Gradle 26 | 27 | In your root `build.gradle` file, at the end of repositories: 28 | ```gradle 29 | repositories { 30 | ... 31 | maven { url 'https://jitpack.io' } 32 | } 33 | ``` 34 | 35 | Add the dependency 36 | ```gradle 37 | dependencies { 38 | ... 39 | 40 | // Kolin OBD API 41 | implementation 'com.github.eltonvs:kotlin-obd-api:1.3.0' 42 | } 43 | ``` 44 | 45 | ### Maven 46 | 47 | Add jitpack to the repositories section 48 | ```xml 49 | 50 | 51 | jitpack.io 52 | https://jitpack.io 53 | 54 | 55 | ``` 56 | 57 | Add the dependency 58 | ```xml 59 | 60 | com.github.eltonvs 61 | kotlin-obd-api 62 | 1.3.0 63 | 64 | ``` 65 | 66 | ### Manual 67 | 68 | You can download a jar from GitHub's [releases page](https://github.com/eltonvs/kotlin-obd-api/releases). 69 | 70 | ## Basic Usage 71 | 72 | Get an `InputStream` and an `OutputStream` from your connection interface and create an `ObdDeviceConnection` instance. 73 | 74 | ```kotlin 75 | // Create ObdDeviceConnection instance 76 | val obdConnection = ObdDeviceConnection(inputStream, outputStream) 77 | ``` 78 | 79 | With this, you're ready to run any command you want, just pass the command instance to the `.run` method. This command accepts 3 parameters: `command`, `useCache` (default = `false`) and `delayTime` (default = `0`). 80 | 81 | ```kotlin 82 | // Retrieving OBD Speed Command 83 | val response = obdConnection.run(SpeedCommand()) 84 | 85 | // Using cache (use with caution) 86 | val cachedResponse = obdConnection.run(VINCommand(), useCache = true) 87 | 88 | // With a delay time - with this, the API will wait 500ms after executing the command 89 | val delayedResponse = obdConnection(RPMCommand(), delayTime = 500L) 90 | ``` 91 | 92 | The retuned object is a `ObdResponse` and has the following attributes: 93 | 94 | | Attribute | Type | Description | 95 | | :- | :- | :- | 96 | | `command` | `ObdCommand` | The command passed to the `run` method | 97 | | `rawResponse` | `ObdRawResponse` | This class holds the raw data returned from the car | 98 | | `value` | `String` | The parsed value | 99 | | `unit` | `String` | The unit from the parsed value (e.g.: `Km/h`, `RPM`, ... | 100 | 101 | 102 | The `ObdRawResponse` has the following attributes: 103 | 104 | | Attribute | Type | Description | 105 | | :- | :- | :- | 106 | | `value` | `String` | The raw value (hex) | 107 | | `elapsedTime` | `Long` | The elapsed time (in milliseconds) to run the command | 108 | | `processedValue` | `String` | The raw (hex) value without whitespaces, colons or any other "noise" | 109 | | `bufferedValue` | `IntArray` | The raw (hex) value as a `IntArray` | 110 | 111 | 112 | ## Extending the library 113 | 114 | It's easy to add a custom command using this library, all you need to do is create a class extending the `ObdCommand` class and overriding the following methods: 115 | ```kotlin 116 | class CustomCommand : ObdCommand() { 117 | // Required 118 | override val tag = "CUSTOM_COMMAND" 119 | override val name "Custom Command" 120 | override val mode = "01" 121 | override val pid = "FF" 122 | 123 | // Optional 124 | override val defaultUnit = "" 125 | override val handler = { it: ObdRawResponse -> "Calculations to parse value from ${it.processedValue}" } 126 | } 127 | ``` 128 | 129 | 130 | ## Commands 131 | 132 | Here are a handul list of the main supported commands (sensors). For a full list, see [here](SUPPORTED_COMMANDS.md). 133 | 134 | - Available Commands 135 | - Vehicle Speed 136 | - Engine RPM 137 | - DTC Number 138 | - Trouble Codes (Current, Pending and Permanent) 139 | - Throttle Position 140 | - Fuel Pressure 141 | - Timing Advance 142 | - Intake Air Temperature 143 | - Mass Air Flow Rate (MAF) 144 | - Engine Run Time 145 | - Fuel Level Input 146 | - MIL ON/OFF 147 | - Vehicle Identification Number (VIN) 148 | 149 | NOTE: Support for those commands will vary from car to car. 150 | 151 | 152 | ## Contributing 153 | 154 | Want to help or have something to add to the repo? problem on a specific feature? 155 | 156 | - Open an issue to explain the issue you want to solve [Open an issue](https://github.com/eltonvs/kotlin-obd-api/issues) 157 | - After discussion to validate your ideas, you can open a PR or even a draft PR if the contribution is a big one [Current PRs](https://github.com/eltonvs/kotlin-obd-api/pulls) 158 | 159 | 160 | ## Versioning 161 | 162 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/eltonvs/kotlin-obd-api/tags). 163 | 164 | 165 | ## Authors 166 | 167 | - **Elton Viana** - Initial work - Also created the [java-obd-api](https://github.com/eltonvs/java-obd-api) 168 | 169 | See also the list of [contributors](https://github.com/eltonvs/kotlin-obd-api/contributors) who participated in this project. 170 | 171 | 172 | ## License 173 | 174 | This project is licensed under the Apache 2.0 License - See the [LICENCE](LICENSE) file for more details. 175 | 176 | 177 | ## Acknowledgments 178 | 179 | - **Paulo Pires** - Creator of the [obd-java-api](https://github.com/pires/obd-java-api), on which the initial steps were based. 180 | - **[SmartMetropolis Project](http://smartmetropolis.imd.ufrn.br/)** (Digital Metropolis Institute - UFRN, Brazil) - Backed and sponsored the project development during the initial steps. 181 | - **[Ivanovitch Silva](https://github.com/ivanovitchm)** - Helped a lot during the initial steps and with the OBD research. 182 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/control/AvailableCommands.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.ObdRawResponse 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.Parameterized 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | @RunWith(Parameterized::class) 11 | class AvailablePIDsCommand01to20ParameterizedTests(private val rawValue: String, private val expected: IntArray) { 12 | companion object { 13 | @JvmStatic 14 | @Parameterized.Parameters 15 | fun values() = listOf( 16 | // Renault Sandero 2014 17 | arrayOf( 18 | "BE3EB811", 19 | intArrayOf(0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x20) 20 | ), 21 | // Chevrolet Onix 2015 22 | arrayOf( 23 | "BE3FB813", 24 | intArrayOf( 25 | 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x1f, 26 | 0x20 27 | ) 28 | ), 29 | // Toyota Corolla 2015 30 | arrayOf( 31 | "BE1FA813", 32 | intArrayOf(0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x13, 0x15, 0x1c, 0x1f, 0x20) 33 | ), 34 | // Fiat Siena 2011 35 | arrayOf( 36 | "BE3EB811", 37 | intArrayOf(0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x20) 38 | ), 39 | // VW Gol 2014 40 | arrayOf( 41 | "BE3EB813", 42 | intArrayOf( 43 | 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x1f, 0x20 44 | ) 45 | ), 46 | // Empty 47 | arrayOf("00000000", intArrayOf()), 48 | // Complete 49 | arrayOf("FFFFFFFF", (0x1..0x20).toList().toIntArray()) 50 | ) 51 | } 52 | 53 | @Test 54 | fun `test valid available PIDs 01 to 20 responses handler`() { 55 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 56 | val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_01_TO_20).run { 57 | handleResponse(rawResponse) 58 | } 59 | assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) 60 | } 61 | } 62 | 63 | 64 | @RunWith(Parameterized::class) 65 | class AvailablePIDsCommand21to40ParameterizedTests(private val rawValue: String, private val expected: IntArray) { 66 | companion object { 67 | @JvmStatic 68 | @Parameterized.Parameters 69 | fun values() = listOf( 70 | // Renault Sandero 2014 71 | arrayOf("80018001", intArrayOf(0x21, 0x30, 0x31, 0x40)), 72 | // Chevrolet Onix 2015 73 | arrayOf("8007A011", intArrayOf(0x21, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x3c, 0x40)), 74 | // Toyota Corolla 2015 75 | arrayOf("9005B015", intArrayOf(0x21, 0x24, 0x2e, 0x30, 0x31, 0x33, 0x34, 0x3c, 0x3e, 0x40)), 76 | // Fiat Siena 2011 77 | arrayOf("80000000", intArrayOf(0x21)), 78 | // VW Gol 2014 79 | arrayOf("8007A011", intArrayOf(0x21, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x3c, 0x40)), 80 | // Empty 81 | arrayOf("00000000", intArrayOf()), 82 | // Complete 83 | arrayOf("FFFFFFFF", (0x21..0x40).toList().toIntArray()) 84 | ) 85 | } 86 | 87 | @Test 88 | fun `test valid available PIDs 21 to 40 responses handler`() { 89 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 90 | val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_21_TO_40).run { 91 | handleResponse(rawResponse) 92 | } 93 | assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) 94 | } 95 | } 96 | 97 | 98 | @RunWith(Parameterized::class) 99 | class AvailablePIDsCommand41to60ParameterizedTests(private val rawValue: String, private val expected: IntArray) { 100 | companion object { 101 | @JvmStatic 102 | @Parameterized.Parameters 103 | fun values() = listOf( 104 | // Renault Sandero 2014 105 | arrayOf("80000000", intArrayOf(0x41)), 106 | // Chevrolet Onix 2015 107 | arrayOf("FED0C000", intArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x49, 0x4a, 0x4c, 0x51, 0x52)), 108 | // Toyota Corolla 2015 109 | arrayOf("7ADC8001", intArrayOf(0x42, 0x43, 0x44, 0x45, 0x47, 0x49, 0x4a, 0x4c, 0x4d, 0x4e, 0x51, 0x60)), 110 | // VW Gol 2014 111 | arrayOf( 112 | "FED14400", 113 | intArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x49, 0x4a, 0x4c, 0x50, 0x52, 0x56) 114 | ), 115 | // Empty 116 | arrayOf("00000000", intArrayOf()), 117 | // Complete 118 | arrayOf("FFFFFFFF", (0x41..0x60).toList().toIntArray()) 119 | ) 120 | } 121 | 122 | @Test 123 | fun `test valid available PIDs 41 to 60 responses handler`() { 124 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 125 | val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_41_TO_60).run { 126 | handleResponse(rawResponse) 127 | } 128 | assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) 129 | } 130 | } 131 | 132 | 133 | @RunWith(Parameterized::class) 134 | class AvailablePIDsCommand61to80ParameterizedTests(private val rawValue: String, private val expected: IntArray) { 135 | companion object { 136 | @JvmStatic 137 | @Parameterized.Parameters 138 | fun values() = listOf( 139 | // Toyota Corolla 2015 140 | arrayOf("08000000", intArrayOf(0x65)), 141 | // Empty 142 | arrayOf("00000000", intArrayOf()), 143 | // Complete 144 | arrayOf("FFFFFFFF", (0x61..0x80).toList().toIntArray()) 145 | ) 146 | } 147 | 148 | @Test 149 | fun `test valid available PIDs 61 to 80 responses handler`() { 150 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 151 | val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_61_TO_80).run { 152 | handleResponse(rawResponse) 153 | } 154 | assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) 155 | } 156 | } 157 | 158 | 159 | @RunWith(Parameterized::class) 160 | class AvailablePIDsCommand81toA0ParameterizedTests(private val rawValue: String, private val expected: IntArray) { 161 | companion object { 162 | @JvmStatic 163 | @Parameterized.Parameters 164 | fun values() = listOf( 165 | // Empty 166 | arrayOf("00000000", intArrayOf()), 167 | // Complete 168 | arrayOf("FFFFFFFF", (0x81..0xA0).toList().toIntArray()) 169 | ) 170 | } 171 | 172 | @Test 173 | fun `test valid available PIDs 81 to A0 responses handler`() { 174 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 175 | val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_81_TO_A0).run { 176 | handleResponse(rawResponse) 177 | } 178 | assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) 179 | } 180 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/eltonvs/obd/command/control/TroubleCodes.kt: -------------------------------------------------------------------------------- 1 | package com.github.eltonvs.obd.command.control 2 | 3 | import com.github.eltonvs.obd.command.NoDataException 4 | import com.github.eltonvs.obd.command.ObdRawResponse 5 | import org.junit.runner.RunWith 6 | import org.junit.runners.Parameterized 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | 11 | @RunWith(Parameterized::class) 12 | class DTCNumberCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 13 | companion object { 14 | @JvmStatic 15 | @Parameterized.Parameters 16 | fun values() = listOf( 17 | arrayOf("410100452100", 0), 18 | arrayOf("410100000000", 0), 19 | arrayOf("41017F000000", 127), 20 | arrayOf("410123456789", 35), 21 | arrayOf("41017FFFFFFF", 127), 22 | arrayOf("410180000000", 0), 23 | arrayOf("410180FFFFFF", 0), 24 | arrayOf("410189ABCDEF", 9), 25 | arrayOf("4101FFFFFFFF", 127) 26 | ) 27 | } 28 | 29 | @Test 30 | fun `test valid DTC number responses handler`() { 31 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 32 | val obdResponse = DTCNumberCommand().run { 33 | handleResponse(rawResponse) 34 | } 35 | assertEquals("$expected codes", obdResponse.formattedValue) 36 | } 37 | } 38 | 39 | 40 | @RunWith(Parameterized::class) 41 | class DistanceSinceCodesClearedCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 42 | companion object { 43 | @JvmStatic 44 | @Parameterized.Parameters 45 | fun values() = listOf( 46 | arrayOf("4131F967", 63_847), 47 | arrayOf("41310000", 0), 48 | arrayOf("4131FFFF", 65_535) 49 | ) 50 | } 51 | 52 | @Test 53 | fun `test valid distance since codes cleared responses handler`() { 54 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 55 | val obdResponse = DistanceSinceCodesClearedCommand().run { 56 | handleResponse(rawResponse) 57 | } 58 | assertEquals("${expected}Km", obdResponse.formattedValue) 59 | } 60 | } 61 | 62 | 63 | @RunWith(Parameterized::class) 64 | class TimeSinceCodesClearedCommandParameterizedTests(private val rawValue: String, private val expected: Int) { 65 | companion object { 66 | @JvmStatic 67 | @Parameterized.Parameters 68 | fun values() = listOf( 69 | arrayOf("414E4543", 17_731), 70 | arrayOf("414E0000", 0), 71 | arrayOf("414EFFFF", 65_535) 72 | ) 73 | } 74 | 75 | @Test 76 | fun `test valid time since codes cleared responses handler`() { 77 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 78 | val obdResponse = TimeSinceCodesClearedCommand().run { 79 | handleResponse(rawResponse) 80 | } 81 | assertEquals("${expected}min", obdResponse.formattedValue) 82 | } 83 | } 84 | 85 | 86 | @RunWith(Parameterized::class) 87 | class TroubleCodesCommandsParameterizedTests(private val rawValue: String, private val expected: List) { 88 | companion object { 89 | @JvmStatic 90 | @Parameterized.Parameters 91 | fun values() = listOf( 92 | // Two frames with four dtc 93 | arrayOf("4300035104A1AB\r43F10600000000", listOf("P0003", "C1104", "B21AB", "U3106")), 94 | // One frame with three dtc 95 | arrayOf("43010301040105", listOf("P0103", "P0104", "P0105")), 96 | // One frame with two dtc 97 | arrayOf("43010301040000", listOf("P0103", "P0104")), 98 | // Two frames with four dtc CAN (ISO-15765) format 99 | arrayOf("00A\r0:430401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")), 100 | // One frame with two dtc CAN (ISO-15765) format 101 | arrayOf("430201200121", listOf("P0120", "P0121")), 102 | // Empty data 103 | arrayOf("4300", listOf()) 104 | ) 105 | } 106 | 107 | @Test 108 | fun `test valid trouble codes responses handler`() { 109 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 110 | val obdResponse = TroubleCodesCommand().run { 111 | handleResponse(rawResponse) 112 | } 113 | assertEquals(expected.joinToString(separator = ","), obdResponse.formattedValue) 114 | } 115 | } 116 | 117 | class TroubleCodesCommandsTests() { 118 | @Test(expected = NoDataException::class) 119 | fun `test trouble codes no data response`() { 120 | val rawResponse = ObdRawResponse(value = "43NODATA", elapsedTime = 0) 121 | TroubleCodesCommand().run { 122 | handleResponse(rawResponse) 123 | } 124 | } 125 | } 126 | 127 | @RunWith(Parameterized::class) 128 | class PendingTroubleCodesCommandsParameterizedTests(private val rawValue: String, private val expected: List) { 129 | companion object { 130 | @JvmStatic 131 | @Parameterized.Parameters 132 | fun values() = listOf( 133 | // Two frames with four dtc 134 | arrayOf("4700035104A1AB\r47F10600000000", listOf("P0003", "C1104", "B21AB", "U3106")), 135 | // One frame with three dtc 136 | arrayOf("47010301040105", listOf("P0103", "P0104", "P0105")), 137 | // One frame with two dtc 138 | arrayOf("47010301040000", listOf("P0103", "P0104")), 139 | // Two frames with four dtc CAN (ISO-15765) format 140 | arrayOf("00A\r0:470401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")), 141 | // One frame with two dtc CAN (ISO-15765) format 142 | arrayOf("470201200121", listOf("P0120", "P0121")), 143 | // Empty data 144 | arrayOf("4700", listOf()) 145 | ) 146 | } 147 | 148 | @Test 149 | fun `test valid pending trouble codes responses handler`() { 150 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 151 | val obdResponse = PendingTroubleCodesCommand().run { 152 | handleResponse(rawResponse) 153 | } 154 | assertEquals(expected.joinToString(separator = ","), obdResponse.formattedValue) 155 | } 156 | } 157 | 158 | class PendingTroubleCodesCommandsTests() { 159 | @Test(expected = NoDataException::class) 160 | fun `test pending trouble codes no data response`() { 161 | val rawResponse = ObdRawResponse(value = "47NODATA", elapsedTime = 0) 162 | PendingTroubleCodesCommand().run { 163 | handleResponse(rawResponse) 164 | } 165 | } 166 | } 167 | 168 | @RunWith(Parameterized::class) 169 | class PermanentTroubleCodesCommandsParameterizedTests( 170 | private val rawValue: String, 171 | private val expected: List 172 | ) { 173 | companion object { 174 | @JvmStatic 175 | @Parameterized.Parameters 176 | fun values() = listOf( 177 | // Two frames with four dtc 178 | arrayOf("4A00035104A1AB\r4AF10600000000", listOf("P0003", "C1104", "B21AB", "U3106")), 179 | // One frame with three dtc 180 | arrayOf("4A010301040105", listOf("P0103", "P0104", "P0105")), 181 | // One frame with two dtc 182 | arrayOf("4A010301040000", listOf("P0103", "P0104")), 183 | // Two frames with four dtc CAN (ISO-15765) format 184 | arrayOf("00A\r0:4A0401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")), 185 | // One frame with two dtc CAN (ISO-15765) format 186 | arrayOf("4A0201200121", listOf("P0120", "P0121")), 187 | // Empty data 188 | arrayOf("4A00", listOf()) 189 | ) 190 | } 191 | 192 | @Test 193 | fun `test valid permanent trouble codes responses handler`() { 194 | val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) 195 | val obdResponse = PermanentTroubleCodesCommand().run { 196 | handleResponse(rawResponse) 197 | } 198 | assertEquals(expected.joinToString(separator = ","), obdResponse.formattedValue) 199 | } 200 | } 201 | 202 | class PermanentTroubleCodesCommandsTests() { 203 | @Test(expected = NoDataException::class) 204 | fun `test permanent trouble codes no data response`() { 205 | val rawResponse = ObdRawResponse(value = "4ANODATA", elapsedTime = 0) 206 | PermanentTroubleCodesCommand().run { 207 | handleResponse(rawResponse) 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------