├── 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 |
4 |
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 |
5 |
6 |
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 |
5 |
6 |
7 |
8 |
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 | [](https://github.com/eltonvs/kotlin-obd-api/releases)
8 | [](https://github.com/eltonvs/kotlin-obd-api/actions?query=workflow%3ACI)
9 | [](https://codeclimate.com/github/eltonvs/kotlin-obd-api/maintainability)
10 | [](https://codebeat.co/projects/github-com-eltonvs-kotlin-obd-api-master)
11 | [](https://github.com/eltonvs/kotlin-obd-api/blob/master/LICENSE)
12 | [](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 |
--------------------------------------------------------------------------------