Repository: eltonvs/kotlin-obd-api Branch: master Commit: 4a1232b436da Files: 58 Total size: 161.2 KB Directory structure: gitextract_kdisc01z/ ├── .editorconfig ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .idea/ │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── encodings.xml │ ├── kotlinScripting.xml │ └── vcs.xml ├── LICENSE ├── LLM_CONTEXT.md ├── README.md ├── SUPPORTED_COMMANDS.md ├── build.gradle.kts ├── context7.json ├── detekt.yml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── llms.txt ├── settings.gradle.kts └── src/ ├── main/ │ └── kotlin/ │ └── com/ │ └── github/ │ └── eltonvs/ │ └── obd/ │ ├── command/ │ │ ├── ATCommand.kt │ │ ├── Enums.kt │ │ ├── Exceptions.kt │ │ ├── ObdCommand.kt │ │ ├── ParserFunctions.kt │ │ ├── RegexPatterns.kt │ │ ├── Response.kt │ │ ├── at/ │ │ │ ├── Actions.kt │ │ │ ├── Info.kt │ │ │ └── Mutations.kt │ │ ├── control/ │ │ │ ├── AvailablePIDsCommand.kt │ │ │ ├── Control.kt │ │ │ ├── MIL.kt │ │ │ ├── Monitor.kt │ │ │ └── TroubleCodes.kt │ │ ├── egr/ │ │ │ └── Egr.kt │ │ ├── engine/ │ │ │ └── Engine.kt │ │ ├── fuel/ │ │ │ ├── Fuel.kt │ │ │ └── Ratio.kt │ │ ├── pressure/ │ │ │ └── Pressure.kt │ │ └── temperature/ │ │ └── Temperature.kt │ └── connection/ │ └── ObdDeviceConnection.kt └── test/ └── kotlin/ └── com/ └── github/ └── eltonvs/ └── obd/ ├── command/ │ ├── BytesToIntParameterizedTests.kt │ ├── control/ │ │ ├── AvailableCommands.kt │ │ ├── Control.kt │ │ ├── MIL.kt │ │ ├── Monitor.kt │ │ └── TroubleCodes.kt │ ├── egr/ │ │ └── Egr.kt │ ├── engine/ │ │ └── Engine.kt │ ├── fuel/ │ │ ├── Fuel.kt │ │ └── Ratio.kt │ ├── pressure/ │ │ └── Pressure.kt │ └── temperature/ │ └── Temperature.kt └── connection/ └── ObdDeviceConnectionTest.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true [*.{kt,kts}] ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = true ktlint_standard_backing-property-naming = disabled ktlint_standard_filename = disabled ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: branches: - master push: branches: - master jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: "25" - uses: gradle/actions/setup-gradle@v4 - name: Verify run: ./gradlew --no-daemon clean test ktlintCheck detekt ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/macos,kotlin,gradle,intellij+iml # Edit at https://www.gitignore.io/?templates=macos,kotlin,gradle,intellij+iml ### Intellij+iml ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/modules.xml # .idea/*.iml # .idea/modules # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### Intellij+iml Patch ### # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 *.iml modules.xml .idea/misc.xml *.ipr ### Kotlin ### # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Gradle ### .gradle build/ # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # Cache of project .gradletasknamecache # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties ### Gradle Patch ### **/build/ # End of https://www.gitignore.io/api/macos,kotlin,gradle,intellij+iml ================================================ FILE: .idea/codeStyles/Project.xml ================================================ ================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/encodings.xml ================================================ ================================================ FILE: .idea/kotlinScripting.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: LLM_CONTEXT.md ================================================ # LLM Context for `kotlin-obd-api` This document helps coding assistants correctly choose and apply this library for OBD-II tasks. ## Project Summary - Name: `kotlin-obd-api` - Language: Kotlin (JVM) - Package: `com.github.eltonvs:kotlin-obd-api` (JitPack) - Primary purpose: query and parse OBD-II / ELM327 commands from Kotlin apps - Typical targets: any Kotlin/JVM app that can provide an `InputStream` and an `OutputStream` (Android, desktop, backend services, CLI tools, embedded Linux, etc.) ## When to Use Use this library when the task involves: - Reading OBD-II telemetry (RPM, speed, MAF, temperatures, pressures, etc.) - Reading VIN - Reading or clearing trouble codes (DTC) - Executing ELM327 AT setup commands Do not use this library when the task is: - Managing Bluetooth pairing/UI (not provided here) - Implementing low-level transport drivers (you provide `InputStream`/`OutputStream`) - Rendering vehicle-specific PID catalogs beyond standard supported commands ## Core Types and API ### Connection `ObdDeviceConnection(inputStream, outputStream, ioDispatcher = Dispatchers.IO)` ### Execute command ```kotlin suspend fun run( command: ObdCommand, useCache: Boolean = false, delayTime: Long = 0, maxRetries: Int = 5, ): ObdResponse ``` Execution model: - `run` is `suspend` - Commands are serialized per connection instance (internal `Mutex`) - Cache is keyed by command class + raw command when `useCache = true` ### Response types - `ObdResponse` - `command: ObdCommand` - `rawResponse: ObdRawResponse` - `value: String` - `unit: String` - `ObdRawResponse` - `value: String` - `elapsedTime: Long` - `processedValue: String` - `bufferedValue: IntArray` ## Recommended Initialization Flow (ELM327) ```kotlin obdConnection.run(ResetAdapterCommand()) obdConnection.run(SetEchoCommand(Switcher.OFF)) obdConnection.run(SetLineFeedCommand(Switcher.OFF)) obdConnection.run(SetHeadersCommand(Switcher.OFF)) ``` Depending on adapter/car, protocol selection may be explicit: ```kotlin obdConnection.run(SelectProtocolCommand(ObdProtocols.AUTO)) ``` ## Common Implementation Patterns ### Read live telemetry ```kotlin val rpm = obdConnection.run(RPMCommand()) val speed = obdConnection.run(SpeedCommand()) ``` ### Read VIN once and cache ```kotlin val vin = obdConnection.run(VINCommand(), useCache = true) ``` ### Read and clear DTC ```kotlin val currentCodes = obdConnection.run(TroubleCodesCommand()) obdConnection.run(ResetTroubleCodesCommand()) ``` ## Error Handling The library throws command/response-specific runtime exceptions (subclasses of `BadResponseException`), including: - `NoDataException` - `UnableToConnectException` - `BusInitException` - `MisunderstoodCommandException` - `UnknownErrorException` - `UnSupportedCommandException` For robust apps, catch `BadResponseException` around command execution and apply retries/fallback based on command criticality. ## Concurrency Notes - Call `run()` from a background coroutine context (for example `Dispatchers.IO`) - Reuse one `ObdDeviceConnection` per physical adapter/session - Do not execute commands in parallel against the same connection instance - On Android specifically, avoid calling `run()` on the main thread ## Command Coverage For the full command matrix (AT commands + Mode 01/03/04/07/09/0A), see: - [`SUPPORTED_COMMANDS.md`](./SUPPORTED_COMMANDS.md) ## References - README: [`README.md`](./README.md) - Source entrypoint: [`src/main/kotlin/com/github/eltonvs/obd/connection/ObdDeviceConnection.kt`](./src/main/kotlin/com/github/eltonvs/obd/connection/ObdDeviceConnection.kt) ================================================ FILE: README.md ================================================

Kotlin OBD API

[![GitHub release](https://img.shields.io/github/v/release/eltonvs/kotlin-obd-api)](https://github.com/eltonvs/kotlin-obd-api/releases) [![CI Status](https://github.com/eltonvs/kotlin-obd-api/actions/workflows/ci.yml/badge.svg)](https://github.com/eltonvs/kotlin-obd-api/actions/workflows/ci.yml) [![Maintainability](https://qlty.sh/gh/eltonvs/projects/kotlin-obd-api/maintainability.svg)](https://qlty.sh/gh/eltonvs/projects/kotlin-obd-api) [![GitHub license](https://img.shields.io/github/license/eltonvs/kotlin-obd-api)](https://github.com/eltonvs/kotlin-obd-api/blob/master/LICENSE) [![Open Source](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://opensource.org/) A lightweight and developer-driven Kotlin OBD-II (ELM327) library for any Kotlin/JVM project to query and parse OBD commands. 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: Use it to read and parse vehicle diagnostics over Bluetooth, Wi-Fi, or USB: - Live telemetry (RPM, speed, throttle position, MAF, temperatures, pressure and more) - Diagnostic Trouble Codes (DTC): current, pending and permanent - VIN and monitor status commands - Adapter-level AT commands for ELM327 setup The API is connection-agnostic and receives an `InputStream` and an `OutputStream`, so you can integrate it with your own Bluetooth, Wi-Fi, or USB transport. ## Installation ### Gradle (Kotlin DSL) In your root `build.gradle.kts` file: ```kotlin repositories { maven("https://jitpack.io") } dependencies { implementation("com.github.eltonvs:kotlin-obd-api:1.4.1") } ``` ### Gradle (Groovy) In your root `build.gradle` file: ```gradle repositories { maven { url 'https://jitpack.io' } } dependencies { // Kotlin OBD API implementation 'com.github.eltonvs:kotlin-obd-api:1.4.1' } ``` ### Maven Add JitPack to the repositories section: ```xml jitpack.io https://jitpack.io ``` Add the dependency: ```xml com.github.eltonvs kotlin-obd-api 1.4.1 ``` ### Manual You can download a jar from GitHub's [releases page](https://github.com/eltonvs/kotlin-obd-api/releases). ## Quickstart Get an `InputStream` and an `OutputStream` from your connection interface and create an `ObdDeviceConnection` instance. ```kotlin import com.github.eltonvs.obd.command.Switcher import com.github.eltonvs.obd.command.at.ResetAdapterCommand import com.github.eltonvs.obd.command.at.SetEchoCommand import com.github.eltonvs.obd.command.control.TroubleCodesCommand import com.github.eltonvs.obd.command.control.VINCommand import com.github.eltonvs.obd.command.engine.RPMCommand import com.github.eltonvs.obd.connection.ObdDeviceConnection import java.io.InputStream import java.io.OutputStream suspend fun readObd(inputStream: InputStream, outputStream: OutputStream) { val obdConnection = ObdDeviceConnection(inputStream, outputStream) // Typical ELM327 setup obdConnection.run(ResetAdapterCommand()) obdConnection.run(SetEchoCommand(Switcher.OFF)) val rpm = obdConnection.run(RPMCommand()) val vin = obdConnection.run(VINCommand(), useCache = true) val troubleCodes = obdConnection.run(TroubleCodesCommand()) println("RPM: ${rpm.value} ${rpm.unit}") println("VIN: ${vin.value}") println("DTC: ${troubleCodes.value.ifBlank { "none" }}") } ``` `run` parameters: - `command`: any `ObdCommand` - `useCache` (default `false`): reuses previous raw responses for identical commands - `delayTime` (default `0`): delay in milliseconds after sending command - `maxRetries` (default `5`): read polling retries before giving up Runtime note: call `run()` from a background coroutine context (for example `Dispatchers.IO`). On Android, do not call it from the main thread. Concurrency note: each `ObdDeviceConnection` instance is a serialized command channel guarded by a coroutine `Mutex`. Reuse one instance per physical connection. The returned object is an `ObdResponse` with: | Attribute | Type | Description | | :- | :- | :- | | `command` | `ObdCommand` | The command passed to the `run` method | | `rawResponse` | `ObdRawResponse` | This class holds the raw data returned from the car | | `value` | `String` | The parsed value | | `unit` | `String` | The unit from the parsed value (for example: `Km/h`, `RPM`) | `ObdRawResponse` attributes: | Attribute | Type | Description | | :- | :- | :- | | `value` | `String` | The raw value (hex) | | `elapsedTime` | `Long` | The elapsed time (in milliseconds) to run the command | | `processedValue` | `String` | The raw (hex) value without whitespaces, colons or any other "noise" | | `bufferedValue` | `IntArray` | The raw (hex) value as a `IntArray` | ## Extending the library Create a custom command by extending `ObdCommand` and overriding the required fields: ```kotlin class CustomCommand : ObdCommand() { // Required override val tag = "CUSTOM_COMMAND" override val name = "Custom Command" override val mode = "01" override val pid = "FF" // Optional override val defaultUnit = "" override val handler = { response: ObdRawResponse -> "Calculated value from ${response.processedValue}" } } ``` ## Commands Here is a short list of supported commands. For the full list, see [SUPPORTED_COMMANDS.md](SUPPORTED_COMMANDS.md). - Available Commands - Vehicle Speed - Engine RPM - DTC Number - Trouble Codes (Current, Pending and Permanent) - Throttle Position - Fuel Pressure - Timing Advance - Intake Air Temperature - Mass Air Flow Rate (MAF) - Engine Run Time - Fuel Level Input - MIL ON/OFF - Vehicle Identification Number (VIN) NOTE: Support for those commands will vary from car to car. ## LLM Context This repository includes LLM-focused docs for coding assistants and agents: - [llms.txt](llms.txt): short machine-readable index for quick retrieval - [LLM_CONTEXT.md](LLM_CONTEXT.md): API conventions, command examples, and decision guide ## Contributing Want to help or have something to add to the repo? Found an issue in a specific feature? - Open an issue to explain the problem you want to solve: [Open an issue](https://github.com/eltonvs/kotlin-obd-api/issues) - After discussion, open a PR (or draft PR for larger contributions): [Current PRs](https://github.com/eltonvs/kotlin-obd-api/pulls) - Run local verification before opening a PR: `./gradlew clean test ktlintCheck detekt` - Auto-format Kotlin sources when needed: `./gradlew ktlintFormat` ## Versioning 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). ## Authors - **Elton Viana** - Initial work - Also created the [java-obd-api](https://github.com/eltonvs/java-obd-api) See also the list of [contributors](https://github.com/eltonvs/kotlin-obd-api/contributors) who participated in this project. ## License This project is licensed under the Apache 2.0 License - See the [LICENCE](LICENSE) file for more details. ## Acknowledgments - **Paulo Pires** - Creator of the [obd-java-api](https://github.com/pires/obd-java-api), on which the initial steps were based. - **[SmartMetropolis Project](http://smartmetropolis.imd.ufrn.br/)** (Digital Metropolis Institute - UFRN, Brazil) - Backed and sponsored the project development during the initial steps. - **[Ivanovitch Silva](https://github.com/ivanovitchm)** - Helped a lot during the initial steps and with the OBD research. ================================================ FILE: SUPPORTED_COMMANDS.md ================================================ # Supported Commands Full list of supported commands. ## `AT` Commands (ELM327) | Command | Name | Description | | :- | :- | :-| | `Z` | `RESET_ADAPTER` | Reset OBD Adapter | | `WS` | `WARM_START` | OBD Warm Start | | `SI` | `SLOW_INITIATION` | OBD Slow Initiation | | `LP` | `LOW_POWER_MODE` | OBD Low Power Mode | | `BD` | `BUFFER_DUMP` | OBD Buffer Dump | | `BI` | `BYPASS_INITIALIZATION` | OBD Bypass Initialization Sequence | | `PC` | `PROTOCOL_CLOSE` | OBD Protocol Close | | `DP` | `DESCRIBE_PROTOCOL` | Describe Protocol | | `DPN` | `DESCRIBE_PROTOCOL_NUMBER` | Describe Protocol Number | | `IGN` | `IGNITION_MONITOR` | Ignition Monitor | | `RN` | `ADAPTER_VOLTAGE` | OBD Adapter Voltage | | `SP {X}` | `SELECT_PROTOCOL_{X}` | Select Protocol where `{X}` is an `ObdProtocols` constant | | `AT {X}` | `SET_ADAPTIVE_TIMING_{X}` | Set Adaptive Timing Control where `{X}` is an `AdaptiveTimingMode` constant | | `E{X}` | `SET_ECHO_{X}` | Set Echo where `{X}` is a `Switcher` constant | | `H{X}` | `SET_HEADERS_{X}` | Set Headers where `{X}` is a `Switcher` constant | | `L{X}` | `SET_LINE_FEED_{X}` | Set Line Feed where `{X}` is a `Switcher` constant | | `S{X}` | `SET_SPACES_{X}` | Set Spaces where `{X}` is a `Switcher` constant | | `ST {X}` | `SET_TIMEOUT` | Set Timeout where `{X}` is an `Int` value | ## Mode 01 | Command | Name | Description | | -- | :- | :-| | `00`, `20`, `40`, `60`, `80` | `AVAILABLE_COMMANDS_{RANGE}` | Available PIDs for each range, where `{RANGE}` is an `AvailablePIDsRanges` constant | | `01` | `DTC_NUMBER` | Diagnostic Trouble Codes Number | | `01` | `MIL_ON` | MIL ON/OFF | | `01` | `MONITOR_STATUS_SINCE_CODES_CLEARED` | Monitor Status Since Codes Cleared | | `04` | `ENGINE_LOAD` | Engine Load | | `05` | `ENGINE_COOLANT_TEMPERATURE` | Engine Coolant Temperature | | `06` | `SHORT_TERM_BANK_1` | Short Term Fuel Trim Bank 1 | | `07` | `SHORT_TERM_BANK_2` | Short Term Fuel Trim Bank 2 | | `08` | `LONG_TERM_BANK_1` | Long Term Fuel Trim Bank 1 | | `09` | `LONG_TERM_BANK_2` | Long Term Fuel Trim Bank 2 | | `0A` | `FUEL_PRESSURE` | Fuel Pressure | | `0B` | `INTAKE_MANIFOLD_PRESSURE` | Intake Manifold Pressure | | `0C` | `ENGINE_RPM` | Engine RPM | | `0D` | `SPEED` | Vehicle Speed | | `0E` | `TIMING_ADVANCE` | Timing Advance | | `0F` | `AIR_INTAKE_TEMPERATURE` | Air Intake Temperature | | `10` | `MAF` | Mass Air Flow | | `11` | `THROTTLE_POSITION` | Throttle Position | | `1F` | `ENGINE_RUNTIME` | Engine Runtime | | `21` | `DISTANCE_TRAVELED_MIL_ON` | Distance traveled with MIL on | | `22` | `FUEL_RAIL_PRESSURE` | Fuel Rail Pressure | | `23` | `FUEL_RAIL_GAUGE_PRESSURE` | Fuel Rail Gauge Pressure | | `2C` | `COMMANDED_EGR` | Commanded EGR | | `2D` | `EGR_ERROR` | EGR Error | | `2F` | `FUEL_LEVEL` | Fuel Level | | `31` | `DISTANCE_TRAVELED_AFTER_CODES_CLEARED` | Distance traveled since codes cleared | | `33` | `BAROMETRIC_PRESSURE` | Barometric Pressure | | `34` | `OXYGEN_SENSOR_1` | Oxygen Sensor 1 | | `35` | `OXYGEN_SENSOR_2` | Oxygen Sensor 2 | | `36` | `OXYGEN_SENSOR_3` | Oxygen Sensor 3 | | `37` | `OXYGEN_SENSOR_4` | Oxygen Sensor 4 | | `38` | `OXYGEN_SENSOR_5` | Oxygen Sensor 5 | | `39` | `OXYGEN_SENSOR_6` | Oxygen Sensor 6 | | `3A` | `OXYGEN_SENSOR_7` | Oxygen Sensor 7 | | `3B` | `OXYGEN_SENSOR_8` | Oxygen Sensor 8 | | `41` | `MONITOR_STATUS_CURRENT_DRIVE_CYCLE` | Monitor Status Current Drive Cycle | | `42` | `CONTROL_MODULE_VOLTAGE` | Control Module Power Supply | | `43` | `ENGINE_ABSOLUTE_LOAD` | Engine Absolute Load | | `44` | `COMMANDED_EQUIVALENCE_RATIO` | Fuel-Air Commanded Equivalence Ratio | | `45` | `RELATIVE_THROTTLE_POSITION` | Relative Throttle Position | | `46` | `AMBIENT_AIR_TEMPERATURE` | Ambient Air Temperature | | `4D` | `TIME_TRAVELED_MIL_ON` | Time run with MIL on | | `4E` | `TIME_SINCE_CODES_CLEARED` | Time since codes cleared | | `51` | `FUEL_TYPE` | Fuel Type | | `52` | `ETHANOL_LEVEL` | Ethanol Level | | `5C` | `ENGINE_OIL_TEMPERATURE` | Engine Oil Temperature | | `5E` | `FUEL_CONSUMPTION_RATE` | Fuel Consumption Rate | ## Mode 03 | Name | Description | | :- | :- | | `TROUBLE_CODES` | Trouble Codes | ## Mode 04 | Name | Description | | :- | :- | | `RESET_TROUBLE_CODES` | Reset Trouble Codes | ## Mode 07 | Name | Description | | :- | :- | | `PENDING_TROUBLE_CODES` | Pending Trouble Codes | ## Mode 09 | Command | Name | Description | | :- | :- | :-| | `02` | `VIN` | Vehicle Identification Number (VIN) | ## Mode 0A | Name | Description | | :- | :- | | `PERMANENT_TROUBLE_CODES` | Permanent Trouble Codes | ================================================ FILE: build.gradle.kts ================================================ import org.gradle.api.GradleException import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.TestDescriptor import org.gradle.api.tasks.testing.TestListener import org.gradle.api.tasks.testing.TestResult import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.dsl.JvmTarget val publicationName = "kotlin-obd-api" plugins { kotlin("jvm") version "2.3.10" id("org.jlleitschuh.gradle.ktlint") version "14.0.1" id("dev.detekt") version "2.0.0-alpha.2" `maven-publish` } group = "com.github.eltonvs" version = "1.4.1" repositories { mavenCentral() } dependencies { implementation(kotlin("stdlib-jdk8")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") testImplementation(kotlin("test")) testImplementation(kotlin("test-junit")) } ktlint { version.set("1.8.0") ignoreFailures.set(false) } detekt { buildUponDefaultConfig = true allRules = false ignoreFailures = false config.setFrom(files("$rootDir/detekt.yml")) } kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) } } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } tasks.withType().configureEach { testLogging { events = setOf(TestLogEvent.SKIPPED, TestLogEvent.FAILED) exceptionFormat = TestExceptionFormat.FULL } addTestListener( object : TestListener { override fun beforeSuite(suite: TestDescriptor) = Unit override fun beforeTest(testDescriptor: TestDescriptor) = Unit override fun afterTest( testDescriptor: TestDescriptor, result: TestResult, ) = Unit override fun afterSuite( suite: TestDescriptor, result: TestResult, ) { if (suite.parent == null) { logger.lifecycle( "Test summary for $path: " + "${result.testCount} executed, " + "${result.successfulTestCount} succeeded, " + "${result.failedTestCount} failed, " + "${result.skippedTestCount} skipped", ) if (result.testCount == 0L) { throw GradleException( "No tests were executed for task $path. " + "Check source sets and CI task configuration.", ) } } } }, ) } publishing { publications { create("maven") { groupId = project.group.toString() artifactId = project.name version = project.version.toString() from(components["java"]) } } } ================================================ FILE: context7.json ================================================ { "url": "https://context7.com/eltonvs/kotlin-obd-api", "public_key": "pk_Zyw2G86BTmzAjNaaoVjyB" } ================================================ FILE: detekt.yml ================================================ # Project detekt overrides. # Intentionally minimal to keep default detekt rules enabled. ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ kotlin.code.style=official ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: jitpack.yml ================================================ jdk: - openjdk17 ================================================ FILE: llms.txt ================================================ # kotlin-obd-api Kotlin OBD-II (ELM327) library for Kotlin/JVM applications. ## Canonical - Repository: https://github.com/eltonvs/kotlin-obd-api - License: Apache-2.0 - Artifact (JitPack): com.github.eltonvs:kotlin-obd-api:1.4.1 ## Use this library when - You need OBD-II diagnostics or live telemetry in Kotlin code. - You have an ELM327-compatible adapter exposed as `InputStream` + `OutputStream`. - You want parsed command responses (`ObdResponse`) instead of raw hex parsing. ## Core API - `ObdDeviceConnection(inputStream, outputStream)` - `suspend fun run(command, useCache = false, delayTime = 0, maxRetries = 5): ObdResponse` ## Common tasks - Read RPM/speed/temperatures/pressure. - Read VIN. - Read and clear trouble codes (DTC). - Send adapter AT commands (echo/headers/protocol/timeouts). ## Behavior notes - Commands are serialized per connection instance (coroutine `Mutex`). - `run` is suspend and should be called from a background coroutine context. - Most parsed values are returned as strings with optional `unit`. ## Docs - README: ./README.md - Full command list: ./SUPPORTED_COMMANDS.md - LLM implementation context: ./LLM_CONTEXT.md ================================================ FILE: settings.gradle.kts ================================================ rootProject.name = "kotlin-obd-api" ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/ATCommand.kt ================================================ package com.github.eltonvs.obd.command abstract class ATCommand : ObdCommand() { override val mode = "AT" override val skipDigitCheck = true } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/Enums.kt ================================================ package com.github.eltonvs.obd.command private const val BIT_POS_0 = 0 private const val BIT_POS_1 = 1 private const val BIT_POS_2 = 2 private const val BIT_POS_3 = 3 private const val BIT_POS_4 = 4 private const val BIT_POS_5 = 5 private const val BIT_POS_6 = 6 private const val BIT_POS_7 = 7 enum class ObdProtocols( val displayName: String, internal val command: String, ) { // Unknown protocol UNKNOWN("Unknown Protocol", ""), // Auto select protocol and save. AUTO("Auto", "0"), // 41.6 kbaud SAE_J1850_PWM("SAE J1850 PWM", "1"), // 10.4 kbaud SAE_J1850_VPW("SAE J1850 VPW", "2"), // 5 baud init ISO_9141_2("ISO 9141-2", "3"), // 5 baud init ISO_14230_4_KWP("ISO 14230-4 (KWP 5BAUD)", "4"), // Fast init ISO_14230_4_KWP_FAST("ISO 14230-4 (KWP FAST)", "5"), // 11 bit ID, 500 kbaud ISO_15765_4_CAN("ISO 15765-4 (CAN 11/500)", "6"), // 29 bit ID, 500 kbaud ISO_15765_4_CAN_B("ISO 15765-4 (CAN 29/500)", "7"), // 11 bit ID, 250 kbaud ISO_15765_4_CAN_C("ISO 15765-4 (CAN 11/250)", "8"), // 29 bit ID, 250 kbaud ISO_15765_4_CAN_D("ISO 15765-4 (CAN 29/250)", "9"), // 29 bit ID, 250 kbaud (user adjustable) SAE_J1939_CAN("SAE J1939 (CAN 29/250)", "A"), } enum class AdaptiveTimingMode( val displayName: String, internal val command: String, ) { OFF("Off", "0"), AUTO_1("Auto 1", "1"), AUTO_2("Auto 2", "2"), } enum class Switcher( internal val command: String, ) { ON("1"), OFF("0"), } enum class Monitors( internal val displayName: String, internal val isSparkIgnition: Boolean? = null, internal val bitPos: Int, ) { // Common MISFIRE("Misfire", bitPos = BIT_POS_0), FUEL_SYSTEM("Fuel System", bitPos = BIT_POS_1), COMPREHENSIVE_COMPONENT("Comprehensive Component", bitPos = BIT_POS_2), // Spark Ignition Monitors CATALYST("Catalyst (CAT)", true, BIT_POS_0), HEATED_CATALYST("Heated Catalyst", true, BIT_POS_1), EVAPORATIVE_SYSTEM("Evaporative (EVAP) System", true, BIT_POS_2), SECONDARY_AIR_SYSTEM("Secondary Air System", true, BIT_POS_3), AC_REFRIGERANT("A/C Refrigerant", true, BIT_POS_4), OXYGEN_SENSOR("Oxygen (O2) Sensor", true, BIT_POS_5), OXYGEN_SENSOR_HEATER("Oxygen Sennsor Heater", true, BIT_POS_6), EGR_SYSTEM("EGR (Exhaust Gas Recirculation) and/or VVT System", true, BIT_POS_7), // Compression Ignition Monitors NMHC_CATALYST("NMHC Catalyst", false, BIT_POS_0), NOX_SCR_MONITOR("NOx/SCR Aftertreatment", false, BIT_POS_1), BOOST_PRESSURE("Boost Pressure", false, BIT_POS_3), EXHAUST_GAS_SENSOR("Exhaust Gas Sensor", false, BIT_POS_5), PM_FILTER("PM Filter", false, BIT_POS_6), EGR_VVT_SYSTEM("EGR (Exhaust Gas Recirculation) and/or VVT System", false, BIT_POS_7), } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/Exceptions.kt ================================================ package com.github.eltonvs.obd.command import com.github.eltonvs.obd.command.RegexPatterns.BUSINIT_ERROR_MESSAGE_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.DIGITS_LETTERS_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.ERROR_MESSAGE_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.MISUNDERSTOOD_COMMAND_MESSAGE_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.NO_DATE_MESSAGE_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.STOPPED_MESSAGE_PATERN import com.github.eltonvs.obd.command.RegexPatterns.UNABLE_TO_CONNECT_MESSAGE_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.UNSUPPORTED_COMMAND_MESSAGE_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN private fun String.sanitize(): String = removeAll(WHITESPACE_PATTERN, this).uppercase() abstract class BadResponseException( private val command: ObdCommand, private val response: ObdRawResponse, ) : RuntimeException() { companion object { fun checkForExceptions( command: ObdCommand, response: ObdRawResponse, ): ObdRawResponse = with(response.value.sanitize()) { when { contains(BUSINIT_ERROR_MESSAGE_PATTERN.sanitize()) -> { throw BusInitException(command, response) } contains(MISUNDERSTOOD_COMMAND_MESSAGE_PATTERN.sanitize()) -> { throw MisunderstoodCommandException(command, response) } contains(NO_DATE_MESSAGE_PATTERN.sanitize()) -> { throw NoDataException(command, response) } contains(STOPPED_MESSAGE_PATERN.sanitize()) -> { throw StoppedException(command, response) } contains(UNABLE_TO_CONNECT_MESSAGE_PATTERN.sanitize()) -> { throw UnableToConnectException(command, response) } contains(ERROR_MESSAGE_PATTERN.sanitize()) -> { throw UnknownErrorException(command, response) } matches(UNSUPPORTED_COMMAND_MESSAGE_PATTERN.toRegex()) -> { throw UnSupportedCommandException(command, response) } !command.skipDigitCheck && !matches(DIGITS_LETTERS_PATTERN.toRegex()) -> { throw NonNumericResponseException(command, response) } else -> { response } } } } override fun toString(): String = "${this.javaClass.simpleName} while executing command [${command.tag}], " + "response [${response.value}]" } private typealias BRE = BadResponseException class NonNumericResponseException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) class BusInitException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) class MisunderstoodCommandException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) class NoDataException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) class StoppedException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) class UnableToConnectException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) class UnknownErrorException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) class UnSupportedCommandException( command: ObdCommand, response: ObdRawResponse, ) : BRE(command, response) ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/ObdCommand.kt ================================================ package com.github.eltonvs.obd.command abstract class ObdCommand { abstract val tag: String abstract val name: String abstract val mode: String abstract val pid: String open val defaultUnit: String = "" open val skipDigitCheck: Boolean = false open val handler: (ObdRawResponse) -> String = { it.value } val rawCommand: String get() = listOf(mode, pid).joinToString(" ") fun handleResponse(rawResponse: ObdRawResponse): ObdResponse { val checkedRawResponse = BadResponseException.checkForExceptions(this, rawResponse) return ObdResponse( command = this, rawResponse = checkedRawResponse, value = handler(checkedRawResponse), unit = defaultUnit, ) } open fun format(response: ObdResponse): String = "${response.value}${response.unit}" } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/ParserFunctions.kt ================================================ package com.github.eltonvs.obd.command import kotlin.math.pow private const val BYTE_BITS = 8 private const val PERCENT_SCALE = 100f private const val MAX_PERCENTAGE_VALUE = 255f fun bytesToInt( bufferedValue: IntArray, start: Int = 2, bytesToProcess: Int = -1, ): Long { var bufferToProcess = bufferedValue.drop(start) if (bytesToProcess != -1) { bufferToProcess = bufferToProcess.take(bytesToProcess) } return bufferToProcess.foldIndexed(0L) { index, total, current -> total + current * 2f.pow((bufferToProcess.size - index - 1) * BYTE_BITS).toLong() } } fun calculatePercentage( bufferedValue: IntArray, bytesToProcess: Int = -1, ): Float = (bytesToInt(bufferedValue, bytesToProcess = bytesToProcess) * PERCENT_SCALE) / MAX_PERCENTAGE_VALUE fun Int.getBitAt( position: Int, last: Int = 32, ) = this shr (last - position) and 1 fun Long.getBitAt( position: Int, last: Int = 32, ) = (this shr (last - position) and 1).toInt() ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/RegexPatterns.kt ================================================ package com.github.eltonvs.obd.command import java.util.regex.Pattern object RegexPatterns { val WHITESPACE_PATTERN: Pattern = Pattern.compile("\\s") val BUS_INIT_PATTERN: Pattern = Pattern.compile("(BUS INIT)|(BUSINIT)|(\\.)") val SEARCHING_PATTERN: Pattern = Pattern.compile("SEARCHING") val CARRIAGE_PATTERN: Pattern = Pattern.compile("[\r\n]") val CARRIAGE_COLON_PATTERN: Pattern = Pattern.compile("[\r\n].:") val COLON_PATTERN: Pattern = Pattern.compile(":") val DIGITS_LETTERS_PATTERN: Pattern = Pattern.compile("([0-9A-F:])+") val STARTS_WITH_ALPHANUM_PATTERN: Pattern = Pattern.compile("[^a-z0-9 ]", Pattern.CASE_INSENSITIVE) // Error patterns const val BUSINIT_ERROR_MESSAGE_PATTERN = "BUS INIT... ERROR" const val MISUNDERSTOOD_COMMAND_MESSAGE_PATTERN = "?" const val NO_DATE_MESSAGE_PATTERN = "NO DATA" const val STOPPED_MESSAGE_PATERN = "STOPPED" const val UNABLE_TO_CONNECT_MESSAGE_PATTERN = "UNABLE TO CONNECT" const val ERROR_MESSAGE_PATTERN = "ERROR" const val UNSUPPORTED_COMMAND_MESSAGE_PATTERN = "7F 0[0-A] 1[1-2]" } fun removeAll( pattern: Pattern, input: String, ): String = pattern.matcher(input).replaceAll("") fun removeAll( input: String, vararg patterns: Pattern, ) = patterns.fold(input) { acc, pattern -> removeAll(pattern, acc) } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/Response.kt ================================================ package com.github.eltonvs.obd.command import com.github.eltonvs.obd.command.RegexPatterns.BUS_INIT_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.COLON_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN fun T.pipe(vararg functions: (T) -> T): T = functions.fold(this) { value, f -> f(value) } data class ObdRawResponse( val value: String, val elapsedTime: Long, ) { private val valueProcessorPipeline by lazy { arrayOf<(String) -> String>( { /* * Imagine the following response 41 0c 00 0d. * * ELM sends strings!! So, ELM puts spaces between each "byte". And pay * attention to the fact that I've put the word byte in quotes, because 41 * is actually TWO bytes (two chars) in the socket. So, we must do some more * processing... */ removeAll(WHITESPACE_PATTERN, it) // removes all [ \t\n\x0B\f\r] }, { /* * Data may have echo or informative text like "INIT BUS..." or similar. * The response ends with two carriage return characters. So we need to take * everything from the last carriage return before those two (trimmed above). */ removeAll(BUS_INIT_PATTERN, it) }, { removeAll(COLON_PATTERN, it) }, ) } val processedValue by lazy { value.pipe(*valueProcessorPipeline) } val bufferedValue by lazy { processedValue.chunked(2) { it.toString().toInt(radix = 16) }.toIntArray() } } data class ObdResponse( val command: ObdCommand, val rawResponse: ObdRawResponse, val value: String, val unit: String = "", ) { val formattedValue: String get() = command.format(this) } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/at/Actions.kt ================================================ package com.github.eltonvs.obd.command.at import com.github.eltonvs.obd.command.ATCommand class ResetAdapterCommand : ATCommand() { override val tag = "RESET_ADAPTER" override val name = "Reset OBD Adapter" override val pid = "Z" } class WarmStartCommand : ATCommand() { override val tag = "WARM_START" override val name = "OBD Warm Start" override val pid = "WS" } class SlowInitiationCommand : ATCommand() { override val tag = "SLOW_INITIATION" override val name = "OBD Slow Initiation" override val pid = "SI" } class LowPowerModeCommand : ATCommand() { override val tag = "LOW_POWER_MODE" override val name = "OBD Low Power Mode" override val pid = "LP" } class BufferDumpCommand : ATCommand() { override val tag = "BUFFER_DUMP" override val name = "OBD Buffer Dump" override val pid = "BD" } class BypassInitializationCommand : ATCommand() { override val tag = "BYPASS_INITIALIZATION" override val name = "OBD Bypass Initialization Sequence" override val pid = "BI" } class ProtocolCloseCommand : ATCommand() { override val tag = "PROTOCOL_CLOSE" override val name = "OBD Protocol Close" override val pid = "PC" } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/at/Info.kt ================================================ package com.github.eltonvs.obd.command.at import com.github.eltonvs.obd.command.ATCommand import com.github.eltonvs.obd.command.ObdProtocols import com.github.eltonvs.obd.command.ObdRawResponse class DescribeProtocolCommand : ATCommand() { override val tag = "DESCRIBE_PROTOCOL" override val name = "Describe Protocol" override val pid = "DP" } class DescribeProtocolNumberCommand : ATCommand() { override val tag = "DESCRIBE_PROTOCOL_NUMBER" override val name = "Describe Protocol Number" override val pid = "DPN" override val handler = { response: ObdRawResponse -> parseProtocolNumber(response).displayName } private fun parseProtocolNumber(rawResponse: ObdRawResponse): ObdProtocols { val result = rawResponse.value val protocolNumber = result[if (result.length == 2) 1 else 0].toString() return ObdProtocols.values().find { it.command == protocolNumber } ?: ObdProtocols.UNKNOWN } } class IgnitionMonitorCommand : ATCommand() { override val tag = "IGNITION_MONITOR" override val name = "Ignition Monitor" override val pid = "IGN" override val handler = { response: ObdRawResponse -> response.value.trim().uppercase() } } class AdapterVoltageCommand : ATCommand() { override val tag = "ADAPTER_VOLTAGE" override val name = "OBD Adapter Voltage" override val pid = "RV" } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/at/Mutations.kt ================================================ package com.github.eltonvs.obd.command.at import com.github.eltonvs.obd.command.ATCommand import com.github.eltonvs.obd.command.AdaptiveTimingMode import com.github.eltonvs.obd.command.ObdProtocols import com.github.eltonvs.obd.command.Switcher private const val TIMEOUT_MASK = 0xFF class SelectProtocolCommand( protocol: ObdProtocols, ) : ATCommand() { private val _protocol = if (protocol == ObdProtocols.UNKNOWN) ObdProtocols.AUTO else protocol override val tag = "SELECT_PROTOCOL_${_protocol.name}" override val name = "Select Protocol - ${_protocol.displayName}" override val pid = "SP ${_protocol.command}" } class SetAdaptiveTimingCommand( value: AdaptiveTimingMode, ) : ATCommand() { override val tag = "SET_ADAPTIVE_TIMING_${value.name}" override val name = "Set Adaptive Timing Control ${value.displayName}" override val pid = "AT ${value.command}" } class SetEchoCommand( value: Switcher, ) : ATCommand() { override val tag = "SET_ECHO_${value.name}" override val name = "Set Echo ${value.name}" override val pid = "E${value.command}" } class SetHeadersCommand( value: Switcher, ) : ATCommand() { override val tag = "SET_HEADERS_${value.name}" override val name = "Set Headers ${value.name}" override val pid = "H${value.command}" } class SetLineFeedCommand( value: Switcher, ) : ATCommand() { override val tag = "SET_LINE_FEED_${value.name}" override val name = "Set Line Feed ${value.name}" override val pid = "L${value.command}" } class SetSpacesCommand( value: Switcher, ) : ATCommand() { override val tag = "SET_SPACES_${value.name}" override val name = "Set Spaces ${value.name}" override val pid = "S${value.command}" } class SetTimeoutCommand( timeout: Int, ) : ATCommand() { override val tag = "SET_TIMEOUT" override val name = "Set Timeout - $timeout" override val pid = "ST ${Integer.toHexString(TIMEOUT_MASK and timeout)}" } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/control/AvailablePIDsCommand.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.getBitAt private const val PID_RANGE_SIZE = 33 class AvailablePIDsCommand( private val range: AvailablePIDsRanges, ) : ObdCommand() { override val tag = "AVAILABLE_COMMANDS_${range.name}" override val name = "Available Commands - ${range.displayName}" override val mode = "01" override val pid = range.pid override val defaultUnit = "" override val handler = { response: ObdRawResponse -> parsePIDs(response.processedValue).joinToString(",") { "%02X".format(it) } } private fun parsePIDs(rawValue: String): IntArray { val value = rawValue.toLong(radix = 16) val initialPID = range.pid.toInt(radix = 16) return (1..PID_RANGE_SIZE).fold(intArrayOf()) { acc, i -> if (value.getBitAt(i) == 1) acc.plus(i + initialPID) else acc } } enum class AvailablePIDsRanges( val displayName: String, internal val pid: String, ) { PIDS_01_TO_20("PIDs from 01 to 20", "00"), PIDS_21_TO_40("PIDs from 21 to 40", "20"), PIDS_41_TO_60("PIDs from 41 to 60", "40"), PIDS_61_TO_80("PIDs from 61 to 80", "60"), PIDS_81_TO_A0("PIDs from 81 to A0", "80"), } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/control/Control.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.RegexPatterns.BUS_INIT_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.STARTS_WITH_ALPHANUM_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN import com.github.eltonvs.obd.command.bytesToInt import com.github.eltonvs.obd.command.removeAll private const val MODULE_VOLTAGE_DIVISOR = 1000f private const val TIMING_ADVANCE_DIVISOR = 2f private const val TIMING_ADVANCE_OFFSET = 64f private const val VIN_CAN_FRAME_PREFIX_LENGTH = 9 private const val HEX_CHUNK_SIZE = 2 private const val HEX_RADIX = 16 class ModuleVoltageCommand : ObdCommand() { override val tag = "CONTROL_MODULE_VOLTAGE" override val name = "Control Module Power Supply" override val mode = "01" override val pid = "42" override val defaultUnit = "V" override val handler = { response: ObdRawResponse -> "%.2f".format(bytesToInt(response.bufferedValue) / MODULE_VOLTAGE_DIVISOR) } } class TimingAdvanceCommand : ObdCommand() { override val tag = "TIMING_ADVANCE" override val name = "Timing Advance" override val mode = "01" override val pid = "0E" override val defaultUnit = "°" override val handler = { response: ObdRawResponse -> "%.2f".format( bytesToInt(response.bufferedValue, bytesToProcess = 1) / TIMING_ADVANCE_DIVISOR - TIMING_ADVANCE_OFFSET, ) } } class VINCommand : ObdCommand() { override val tag = "VIN" override val name = "Vehicle Identification Number (VIN)" override val mode = "09" override val pid = "02" override val defaultUnit = "" override val handler = { response: ObdRawResponse -> parseVIN(removeAll(response.value, WHITESPACE_PATTERN, BUS_INIT_PATTERN)) } private fun parseVIN(rawValue: String): String { val workingData = if (rawValue.contains(":")) { // CAN(ISO-15765) protocol. // 9 is xxx490201, xxx is bytes of information to follow. val value = rawValue.replace(".:".toRegex(), "").substring(VIN_CAN_FRAME_PREFIX_LENGTH) if (STARTS_WITH_ALPHANUM_PATTERN.matcher(convertHexToString(value)).find()) { rawValue.replace("0:49", "").replace(".:".toRegex(), "") } else { value } } else { // ISO9141-2, KWP2000 Fast and KWP2000 5Kbps (ISO15031) protocols. rawValue.replace("49020.".toRegex(), "") } return convertHexToString(workingData).replace("[\u0000-\u001f]".toRegex(), "") } private fun convertHexToString(hex: String): String = hex .chunked(HEX_CHUNK_SIZE) { Integer.parseInt(it.toString(), HEX_RADIX).toChar() }.joinToString("") } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/control/MIL.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.ObdResponse import com.github.eltonvs.obd.command.bytesToInt private const val MIL_BYTE_INDEX = 2 private const val MIL_ON_MASK = 0x80 class MILOnCommand : ObdCommand() { override val tag = "MIL_ON" override val name = "MIL on" override val mode = "01" override val pid = "01" override val handler = { response: ObdRawResponse -> val mil = response.bufferedValue[MIL_BYTE_INDEX] val milOn = (mil and MIL_ON_MASK) == MIL_ON_MASK milOn.toString() } override fun format(response: ObdResponse): String { val milOn = response.value.toBoolean() return "MIL is ${if (milOn) "ON" else "OFF"}" } } class DistanceMILOnCommand : ObdCommand() { override val tag = "DISTANCE_TRAVELED_MIL_ON" override val name = "Distance traveled with MIL on" override val mode = "01" override val pid = "21" override val defaultUnit = "Km" override val handler = { response: ObdRawResponse -> bytesToInt(response.bufferedValue).toString() } } class TimeSinceMILOnCommand : ObdCommand() { override val tag = "TIME_TRAVELED_MIL_ON" override val name = "Time run with MIL on" override val mode = "01" override val pid = "4D" override val defaultUnit = "min" override val handler = { response: ObdRawResponse -> bytesToInt(response.bufferedValue).toString() } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/control/Monitor.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.Monitors import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.getBitAt private const val MONITOR_BYTES_COUNT = 4 private const val MIL_BIT_POSITION = 1 private const val LAST_BIT_POSITION = 8 private const val SPARK_CHECK_BIT_POSITION = 5 private const val COMMON_MONITOR_COMPLETION_OFFSET = 4 private const val DTC_COUNT_MASK = 0x7F private const val BIT_SET = 1 private const val DATA_BYTE_0 = 0 private const val DATA_BYTE_1 = 1 private const val DATA_BYTE_2 = 2 private const val DATA_BYTE_3 = 3 data class SensorStatus( val available: Boolean, val complete: Boolean, ) data class SensorStatusData( val milOn: Boolean, val dtcCount: Int, val isSpark: Boolean, val items: Map, ) abstract class BaseMonitorStatus : ObdCommand() { override val mode = "01" override val defaultUnit = "" override val handler = { response: ObdRawResponse -> parseData(response.bufferedValue.takeLast(MONITOR_BYTES_COUNT)) "" } var data: SensorStatusData? = null /** * Parses the Monitor Status data * * ┌Components not ready * |┌Fuel not ready * ||┌Misfire not ready * |||┌Spark vs. Compression * ||||┌Components supported * |||||┌Fuel supported * ┌MIL ||||||┌Misfire supported * | ||||||| * 10000011 00000111 11111111 00000000 * [# DTC] X [supprt] [~ready] */ private fun parseData(values: List) { if (values.size != MONITOR_BYTES_COUNT) { return } val milOn = values[DATA_BYTE_0].getBitAt(MIL_BIT_POSITION, LAST_BIT_POSITION) == BIT_SET val dtcCount = values[DATA_BYTE_0] and DTC_COUNT_MASK val isSpark = values[DATA_BYTE_1].getBitAt(SPARK_CHECK_BIT_POSITION, LAST_BIT_POSITION) == 0 val monitorMap = HashMap() Monitors.values().forEach { val normalizedPos = LAST_BIT_POSITION - it.bitPos if (it.isSparkIgnition == null) { val isAvailable = values[DATA_BYTE_1].getBitAt(normalizedPos, LAST_BIT_POSITION) == BIT_SET val isComplete = values[DATA_BYTE_1].getBitAt( normalizedPos - COMMON_MONITOR_COMPLETION_OFFSET, LAST_BIT_POSITION, ) == 0 monitorMap[it] = SensorStatus(isAvailable, isComplete) } else if (it.isSparkIgnition == isSpark) { val isAvailable = values[DATA_BYTE_2].getBitAt(normalizedPos, LAST_BIT_POSITION) == BIT_SET val isComplete = values[DATA_BYTE_3].getBitAt(normalizedPos, LAST_BIT_POSITION) != BIT_SET monitorMap[it] = SensorStatus(isAvailable, isComplete) } } data = SensorStatusData(milOn, dtcCount, isSpark, monitorMap) } } class MonitorStatusSinceCodesClearedCommand : BaseMonitorStatus() { override val tag = "MONITOR_STATUS_SINCE_CODES_CLEARED" override val name = "Monitor Status Since Codes Cleared" override val pid = "01" } class MonitorStatusCurrentDriveCycleCommand : BaseMonitorStatus() { override val tag = "MONITOR_STATUS_CURRENT_DRIVE_CYCLE" override val name = "Monitor Status Current Drive Cycle" override val pid = "41" } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/control/TroubleCodes.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.RegexPatterns.CARRIAGE_COLON_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.CARRIAGE_PATTERN import com.github.eltonvs.obd.command.RegexPatterns.WHITESPACE_PATTERN import com.github.eltonvs.obd.command.bytesToInt import com.github.eltonvs.obd.command.removeAll import java.util.regex.Pattern private const val STATUS_BYTE_INDEX = 2 private const val DTC_COUNT_MASK = 0x7F private const val CAN_ONE_FRAME_MAX_LENGTH = 16 private const val DTC_HEX_CHUNK_SIZE = 4 private const val CAN_ONE_FRAME_HEADER_SIZE = 4 private const val CAN_MULTI_FRAME_HEADER_SIZE = 7 private const val DTC_TYPE_SHIFT = 2 private const val DTC_TYPE_MASK = 0b11 private const val DTC_CODE_MASK = 0b11 private const val PAD_DTC_LENGTH = 5 private const val PAD_DTC_CHAR = '0' class DTCNumberCommand : ObdCommand() { override val tag = "DTC_NUMBER" override val name = "Diagnostic Trouble Codes Number" override val mode = "01" override val pid = "01" override val defaultUnit = " codes" override val handler = { response: ObdRawResponse -> val mil = response.bufferedValue[STATUS_BYTE_INDEX] val codeCount = mil and DTC_COUNT_MASK codeCount.toString() } } class DistanceSinceCodesClearedCommand : ObdCommand() { override val tag = "DISTANCE_TRAVELED_AFTER_CODES_CLEARED" override val name = "Distance traveled since codes cleared" override val mode = "01" override val pid = "31" override val defaultUnit = "Km" override val handler = { response: ObdRawResponse -> bytesToInt(response.bufferedValue).toString() } } class TimeSinceCodesClearedCommand : ObdCommand() { override val tag = "TIME_SINCE_CODES_CLEARED" override val name = "Time since codes cleared" override val mode = "01" override val pid = "4E" override val defaultUnit = "min" override val handler = { response: ObdRawResponse -> bytesToInt(response.bufferedValue).toString() } } class ResetTroubleCodesCommand : ObdCommand() { override val tag = "RESET_TROUBLE_CODES" override val name = "Reset Trouble Codes" override val mode = "04" override val pid = "" } abstract class BaseTroubleCodesCommand : ObdCommand() { override val pid = "" override val handler = { response: ObdRawResponse -> parseTroubleCodesList(response.value).joinToString(separator = ",") } abstract val carriageNumberPattern: Pattern var troubleCodesList = listOf() private set private fun parseTroubleCodesList(rawValue: String): List { val canOneFrame: String = removeAll(rawValue, CARRIAGE_PATTERN, WHITESPACE_PATTERN) val canOneFrameLength = canOneFrame.length val workingData = when { /* CAN(ISO-15765) protocol one frame: 43yy[codes] Header is 43yy, yy showing the number of data items. */ (canOneFrameLength <= CAN_ONE_FRAME_MAX_LENGTH) and (canOneFrameLength % DTC_HEX_CHUNK_SIZE == 0) -> { canOneFrame.drop(CAN_ONE_FRAME_HEADER_SIZE) } /* CAN(ISO-15765) protocol two and more frames: xxx43yy[codes] Header is xxx43yy, xxx is bytes of information to follow, yy showing the number of data items. */ rawValue.contains(":") -> { removeAll(CARRIAGE_COLON_PATTERN, rawValue).drop(CAN_MULTI_FRAME_HEADER_SIZE) } // ISO9141-2, KWP2000 Fast and KWP2000 5Kbps (ISO15031) protocols. else -> { removeAll(rawValue, carriageNumberPattern, WHITESPACE_PATTERN) } } /* For each chunk of 4 chars: it: "0100" HEX: 0 1 0 0 BIN: 00000001 00000000 [][][ hex ] | / / DTC: P0100 */ val troubleCodesList = workingData.chunked(DTC_HEX_CHUNK_SIZE) { val b1 = it.first().toString().toInt(radix = 16) val ch1 = (b1 shr DTC_TYPE_SHIFT) and DTC_TYPE_MASK val ch2 = b1 and DTC_CODE_MASK "${DTC_LETTERS[ch1]}${HEX_ARRAY[ch2]}${it.drop(1)}".padEnd(PAD_DTC_LENGTH, PAD_DTC_CHAR) } val idx = troubleCodesList.indexOf("P0000") return (if (idx < 0) troubleCodesList else troubleCodesList.take(idx)).also { this.troubleCodesList = it } } protected companion object { private val DTC_LETTERS = charArrayOf('P', 'C', 'B', 'U') private val HEX_ARRAY = "0123456789ABCDEF".toCharArray() } } class TroubleCodesCommand : BaseTroubleCodesCommand() { override val tag = "TROUBLE_CODES" override val name = "Trouble Codes" override val mode = "03" override val carriageNumberPattern: Pattern = Pattern.compile("^43|[\r\n]43|[\r\n]") } class PendingTroubleCodesCommand : BaseTroubleCodesCommand() { override val tag = "PENDING_TROUBLE_CODES" override val name = "Pending Trouble Codes" override val mode = "07" override val carriageNumberPattern: Pattern = Pattern.compile("^47|[\r\n]47|[\r\n]") } class PermanentTroubleCodesCommand : BaseTroubleCodesCommand() { override val tag = "PERMANENT_TROUBLE_CODES" override val name = "Permanent Trouble Codes" override val mode = "0A" override val carriageNumberPattern: Pattern = Pattern.compile("^4A|[\r\n]4A|[\r\n]") } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/egr/Egr.kt ================================================ package com.github.eltonvs.obd.command.egr import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.bytesToInt import com.github.eltonvs.obd.command.calculatePercentage private const val SINGLE_BYTE = 1 private const val HUNDRED_PERCENT = 100f private const val HALF_SCALE = 128f class CommandedEgrCommand : ObdCommand() { override val tag = "COMMANDED_EGR" override val name = "Commanded EGR" override val mode = "01" override val pid = "2C" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> "%.1f".format(calculatePercentage(response.bufferedValue, bytesToProcess = SINGLE_BYTE)) } } class EgrErrorCommand : ObdCommand() { override val tag = "EGR_ERROR" override val name = "EGR Error" override val mode = "01" override val pid = "2D" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> val value = bytesToInt(response.bufferedValue, bytesToProcess = SINGLE_BYTE) val normalized = value * (HUNDRED_PERCENT / HALF_SCALE) - HUNDRED_PERCENT "%.1f".format( normalized, ) } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/engine/Engine.kt ================================================ package com.github.eltonvs.obd.command.engine import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.bytesToInt import com.github.eltonvs.obd.command.calculatePercentage private const val SINGLE_BYTE = 1 private const val RPM_DIVISOR = 4 private const val MAF_DIVISOR = 100f private const val SECONDS_PER_HOUR = 3600 private const val SECONDS_PER_MINUTE = 60 private const val TIME_PADDING = 2 private const val PAD_CHAR = '0' class SpeedCommand : ObdCommand() { override val tag = "SPEED" override val name = "Vehicle Speed" override val mode = "01" override val pid = "0D" override val defaultUnit = "Km/h" override val handler = { response: ObdRawResponse -> bytesToInt(response.bufferedValue, bytesToProcess = SINGLE_BYTE).toString() } } class RPMCommand : ObdCommand() { override val tag = "ENGINE_RPM" override val name = "Engine RPM" override val mode = "01" override val pid = "0C" override val defaultUnit = "RPM" override val handler = { response: ObdRawResponse -> (bytesToInt(response.bufferedValue, bytesToProcess = 2) / RPM_DIVISOR).toString() } } class MassAirFlowCommand : ObdCommand() { override val tag = "MAF" override val name = "Mass Air Flow" override val mode = "01" override val pid = "10" override val defaultUnit = "g/s" override val handler = { response: ObdRawResponse -> val value = bytesToInt(response.bufferedValue) / MAF_DIVISOR "%.2f".format(value) } } class RuntimeCommand : ObdCommand() { override val tag = "ENGINE_RUNTIME" override val name = "Engine Runtime" override val mode = "01" override val pid = "0F" override val handler = { response: ObdRawResponse -> parseRuntime(response.bufferedValue) } private fun parseRuntime(rawValue: IntArray): String { val seconds = bytesToInt(rawValue) val hh = seconds / SECONDS_PER_HOUR val mm = (seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE val ss = seconds % SECONDS_PER_MINUTE return listOf(hh, mm, ss).joinToString(":") { it.toString().padStart(TIME_PADDING, PAD_CHAR) } } } class LoadCommand : ObdCommand() { override val tag = "ENGINE_LOAD" override val name = "Engine Load" override val mode = "01" override val pid = "04" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> "%.1f".format(calculatePercentage(response.bufferedValue, bytesToProcess = SINGLE_BYTE)) } } class AbsoluteLoadCommand : ObdCommand() { override val tag = "ENGINE_ABSOLUTE_LOAD" override val name = "Engine Absolute Load" override val mode = "01" override val pid = "43" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> "%.1f".format(calculatePercentage(response.bufferedValue)) } } class ThrottlePositionCommand : ObdCommand() { override val tag = "THROTTLE_POSITION" override val name = "Throttle Position" override val mode = "01" override val pid = "11" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> "%.1f".format(calculatePercentage(response.bufferedValue, bytesToProcess = SINGLE_BYTE)) } } class RelativeThrottlePositionCommand : ObdCommand() { override val tag = "RELATIVE_THROTTLE_POSITION" override val name = "Relative Throttle Position" override val mode = "01" override val pid = "45" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> "%.1f".format(calculatePercentage(response.bufferedValue, bytesToProcess = SINGLE_BYTE)) } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/fuel/Fuel.kt ================================================ package com.github.eltonvs.obd.command.fuel import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.bytesToInt import com.github.eltonvs.obd.command.calculatePercentage private const val SINGLE_BYTE = 1 private const val FUEL_CONSUMPTION_FACTOR = 0.05 private const val HUNDRED_PERCENT = 100f private const val HALF_SCALE = 128f private const val FUEL_CODE_NOT_AVAILABLE = 0x00 private const val FUEL_CODE_GASOLINE = 0x01 private const val FUEL_CODE_METHANOL = 0x02 private const val FUEL_CODE_ETHANOL = 0x03 private const val FUEL_CODE_DIESEL = 0x04 private const val FUEL_CODE_GPL = 0x05 private const val FUEL_CODE_NATURAL_GAS = 0x06 private const val FUEL_CODE_PROPANE = 0x07 private const val FUEL_CODE_ELECTRIC = 0x08 private const val FUEL_CODE_BIODIESEL_GASOLINE = 0x09 private const val FUEL_CODE_BIODIESEL_METHANOL = 0x0A private const val FUEL_CODE_BIODIESEL_ETHANOL = 0x0B private const val FUEL_CODE_BIODIESEL_GPL = 0x0C private const val FUEL_CODE_BIODIESEL_NATURAL_GAS = 0x0D private const val FUEL_CODE_BIODIESEL_PROPANE = 0x0E private const val FUEL_CODE_BIODIESEL_ELECTRIC = 0x0F private const val FUEL_CODE_BIODIESEL_GASOLINE_ELECTRIC = 0x10 private const val FUEL_CODE_HYBRID_GASOLINE = 0x11 private const val FUEL_CODE_HYBRID_ETHANOL = 0x12 private const val FUEL_CODE_HYBRID_DIESEL = 0x13 private const val FUEL_CODE_HYBRID_ELECTRIC = 0x14 private const val FUEL_CODE_HYBRID_MIXED = 0x15 private const val FUEL_CODE_HYBRID_REGENERATIVE = 0x16 class FuelConsumptionRateCommand : ObdCommand() { override val tag = "FUEL_CONSUMPTION_RATE" override val name = "Fuel Consumption Rate" override val mode = "01" override val pid = "5E" override val defaultUnit = "L/h" override val handler = { response: ObdRawResponse -> "%.1f".format(bytesToInt(response.bufferedValue) * FUEL_CONSUMPTION_FACTOR) } } class FuelTypeCommand : ObdCommand() { override val tag = "FUEL_TYPE" override val name = "Fuel Type" override val mode = "01" override val pid = "51" override val handler = { response: ObdRawResponse -> getFuelType(bytesToInt(response.bufferedValue, bytesToProcess = SINGLE_BYTE).toInt()) } private fun getFuelType(code: Int): String = FUEL_TYPE_BY_CODE[code] ?: "Unknown" private companion object { private val FUEL_TYPE_BY_CODE = mapOf( FUEL_CODE_NOT_AVAILABLE to "Not Available", FUEL_CODE_GASOLINE to "Gasoline", FUEL_CODE_METHANOL to "Methanol", FUEL_CODE_ETHANOL to "Ethanol", FUEL_CODE_DIESEL to "Diesel", FUEL_CODE_GPL to "GPL/LGP", FUEL_CODE_NATURAL_GAS to "Natural Gas", FUEL_CODE_PROPANE to "Propane", FUEL_CODE_ELECTRIC to "Electric", FUEL_CODE_BIODIESEL_GASOLINE to "Biodiesel + Gasoline", FUEL_CODE_BIODIESEL_METHANOL to "Biodiesel + Methanol", FUEL_CODE_BIODIESEL_ETHANOL to "Biodiesel + Ethanol", FUEL_CODE_BIODIESEL_GPL to "Biodiesel + GPL/LGP", FUEL_CODE_BIODIESEL_NATURAL_GAS to "Biodiesel + Natural Gas", FUEL_CODE_BIODIESEL_PROPANE to "Biodiesel + Propane", FUEL_CODE_BIODIESEL_ELECTRIC to "Biodiesel + Electric", FUEL_CODE_BIODIESEL_GASOLINE_ELECTRIC to "Biodiesel + Gasoline/Electric", FUEL_CODE_HYBRID_GASOLINE to "Hybrid Gasoline", FUEL_CODE_HYBRID_ETHANOL to "Hybrid Ethanol", FUEL_CODE_HYBRID_DIESEL to "Hybrid Diesel", FUEL_CODE_HYBRID_ELECTRIC to "Hybrid Electric", FUEL_CODE_HYBRID_MIXED to "Hybrid Mixed", FUEL_CODE_HYBRID_REGENERATIVE to "Hybrid Regenerative", ) } } class FuelLevelCommand : ObdCommand() { override val tag = "FUEL_LEVEL" override val name = "Fuel Level" override val mode = "01" override val pid = "2F" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> "%.1f".format(calculatePercentage(response.bufferedValue, bytesToProcess = SINGLE_BYTE)) } } class EthanolLevelCommand : ObdCommand() { override val tag = "ETHANOL_LEVEL" override val name = "Ethanol Level" override val mode = "01" override val pid = "52" override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> "%.1f".format(calculatePercentage(response.bufferedValue, bytesToProcess = SINGLE_BYTE)) } } class FuelTrimCommand( fuelTrimBank: FuelTrimBank, ) : ObdCommand() { override val tag = fuelTrimBank.name override val name = fuelTrimBank.displayName override val mode = "01" override val pid = fuelTrimBank.pid override val defaultUnit = "%" override val handler = { response: ObdRawResponse -> val value = bytesToInt(response.bufferedValue, bytesToProcess = SINGLE_BYTE) val normalized = value * (HUNDRED_PERCENT / HALF_SCALE) - HUNDRED_PERCENT "%.1f".format( normalized, ) } enum class FuelTrimBank( val displayName: String, internal val pid: String, ) { SHORT_TERM_BANK_1("Short Term Fuel Trim Bank 1", "06"), SHORT_TERM_BANK_2("Short Term Fuel Trim Bank 2", "07"), LONG_TERM_BANK_1("Long Term Fuel Trim Bank 1", "08"), LONG_TERM_BANK_2("Long Term Fuel Trim Bank 2", "09"), } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/fuel/Ratio.kt ================================================ package com.github.eltonvs.obd.command.fuel import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.bytesToInt private const val RATIO_BYTES_TO_PROCESS = 2 private const val RATIO_SCALE = 2f private const val RATIO_DIVISOR = 65_536f private fun calculateFuelAirRatio(rawValue: IntArray): Float = bytesToInt(rawValue, bytesToProcess = RATIO_BYTES_TO_PROCESS) * (RATIO_SCALE / RATIO_DIVISOR) class CommandedEquivalenceRatioCommand : ObdCommand() { override val tag = "COMMANDED_EQUIVALENCE_RATIO" override val name = "Fuel-Air Commanded Equivalence Ratio" override val mode = "01" override val pid = "44" override val defaultUnit = "F/A" override val handler = { response: ObdRawResponse -> "%.2f".format(calculateFuelAirRatio(response.bufferedValue)) } } class FuelAirEquivalenceRatioCommand( oxygenSensor: OxygenSensor, ) : ObdCommand() { override val tag = "FUEL_AIR_EQUIVALENCE_RATIO_${oxygenSensor.name}" override val name = "Fuel-Air Equivalence Ratio - ${oxygenSensor.displayName}" override val mode = "01" override val pid = oxygenSensor.pid override val defaultUnit = "F/A" override val handler = { response: ObdRawResponse -> "%.2f".format(calculateFuelAirRatio(response.bufferedValue)) } enum class OxygenSensor( val displayName: String, internal val pid: String, ) { OXYGEN_SENSOR_1("Oxygen Sensor 1", "34"), OXYGEN_SENSOR_2("Oxygen Sensor 2", "35"), OXYGEN_SENSOR_3("Oxygen Sensor 3", "36"), OXYGEN_SENSOR_4("Oxygen Sensor 4", "37"), OXYGEN_SENSOR_5("Oxygen Sensor 5", "38"), OXYGEN_SENSOR_6("Oxygen Sensor 6", "39"), OXYGEN_SENSOR_7("Oxygen Sensor 7", "3A"), OXYGEN_SENSOR_8("Oxygen Sensor 8", "3B"), } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/pressure/Pressure.kt ================================================ package com.github.eltonvs.obd.command.pressure import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.bytesToInt private const val SINGLE_BYTE = 1 private const val FUEL_PRESSURE_MULTIPLIER = 3 private const val FUEL_RAIL_PRESSURE_FACTOR = 0.079 private const val FUEL_RAIL_GAUGE_PRESSURE_MULTIPLIER = 10 class BarometricPressureCommand : ObdCommand() { override val tag = "BAROMETRIC_PRESSURE" override val name = "Barometric Pressure" override val mode = "01" override val pid = "33" override val defaultUnit = "kPa" override val handler = { response: ObdRawResponse -> bytesToInt(response.bufferedValue, bytesToProcess = SINGLE_BYTE).toString() } } class IntakeManifoldPressureCommand : ObdCommand() { override val tag = "INTAKE_MANIFOLD_PRESSURE" override val name = "Intake Manifold Pressure" override val mode = "01" override val pid = "0B" override val defaultUnit = "kPa" override val handler = { response: ObdRawResponse -> bytesToInt(response.bufferedValue, bytesToProcess = SINGLE_BYTE).toString() } } class FuelPressureCommand : ObdCommand() { override val tag = "FUEL_PRESSURE" override val name = "Fuel Pressure" override val mode = "01" override val pid = "0A" override val defaultUnit = "kPa" override val handler = { response: ObdRawResponse -> (bytesToInt(response.bufferedValue, bytesToProcess = SINGLE_BYTE) * FUEL_PRESSURE_MULTIPLIER).toString() } } class FuelRailPressureCommand : ObdCommand() { override val tag = "FUEL_RAIL_PRESSURE" override val name = "Fuel Rail Pressure" override val mode = "01" override val pid = "22" override val defaultUnit = "kPa" override val handler = { response: ObdRawResponse -> "%.3f".format(bytesToInt(response.bufferedValue) * FUEL_RAIL_PRESSURE_FACTOR) } } class FuelRailGaugePressureCommand : ObdCommand() { override val tag = "FUEL_RAIL_GAUGE_PRESSURE" override val name = "Fuel Rail Gauge Pressure" override val mode = "01" override val pid = "23" override val defaultUnit = "kPa" override val handler = { response: ObdRawResponse -> (bytesToInt(response.bufferedValue) * FUEL_RAIL_GAUGE_PRESSURE_MULTIPLIER).toString() } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/command/temperature/Temperature.kt ================================================ package com.github.eltonvs.obd.command.temperature import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.bytesToInt private const val SINGLE_BYTE = 1 private const val CELSIUS_OFFSET = 40f private fun calculateTemperature(rawValue: IntArray): Float = bytesToInt( rawValue, bytesToProcess = SINGLE_BYTE, ) - CELSIUS_OFFSET class AirIntakeTemperatureCommand : ObdCommand() { override val tag = "AIR_INTAKE_TEMPERATURE" override val name = "Air Intake Temperature" override val mode = "01" override val pid = "0F" override val defaultUnit = "°C" override val handler = { response: ObdRawResponse -> "%.1f".format(calculateTemperature(response.bufferedValue)) } } class AmbientAirTemperatureCommand : ObdCommand() { override val tag = "AMBIENT_AIR_TEMPERATURE" override val name = "Ambient Air Temperature" override val mode = "01" override val pid = "46" override val defaultUnit = "°C" override val handler = { response: ObdRawResponse -> "%.1f".format(calculateTemperature(response.bufferedValue)) } } class EngineCoolantTemperatureCommand : ObdCommand() { override val tag = "ENGINE_COOLANT_TEMPERATURE" override val name = "Engine Coolant Temperature" override val mode = "01" override val pid = "05" override val defaultUnit = "°C" override val handler = { response: ObdRawResponse -> "%.1f".format(calculateTemperature(response.bufferedValue)) } } class OilTemperatureCommand : ObdCommand() { override val tag = "ENGINE_OIL_TEMPERATURE" override val name = "Engine Oil Temperature" override val mode = "01" override val pid = "5C" override val defaultUnit = "°C" override val handler = { response: ObdRawResponse -> "%.1f".format(calculateTemperature(response.bufferedValue)) } } ================================================ FILE: src/main/kotlin/com/github/eltonvs/obd/connection/ObdDeviceConnection.kt ================================================ package com.github.eltonvs.obd.connection import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import com.github.eltonvs.obd.command.ObdResponse import com.github.eltonvs.obd.command.RegexPatterns.SEARCHING_PATTERN import com.github.eltonvs.obd.command.removeAll import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.InputStream import java.io.OutputStream import kotlin.system.measureTimeMillis private const val READ_RETRY_DELAY_MS = 500L class ObdDeviceConnection @JvmOverloads constructor( private val inputStream: InputStream, private val outputStream: OutputStream, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { private val runMutex = Mutex() private val responseCache = mutableMapOf() suspend fun run( command: ObdCommand, useCache: Boolean = false, delayTime: Long = 0, maxRetries: Int = 5, ): ObdResponse = runMutex.withLock { val cacheKey = cacheKey(command) val obdRawResponse = if (useCache && responseCache[cacheKey] != null) { responseCache.getValue(cacheKey) } else { runCommand(command, delayTime, maxRetries).also { if (useCache) { responseCache[cacheKey] = it } } } command.handleResponse(obdRawResponse) } private suspend fun runCommand( command: ObdCommand, delayTime: Long, maxRetries: Int, ): ObdRawResponse { var rawData = "" val elapsedTime = measureTimeMillis { sendCommand(command, delayTime) rawData = readRawData(maxRetries) } return ObdRawResponse(rawData, elapsedTime) } private suspend fun sendCommand( command: ObdCommand, delayTime: Long, ) { withContext(ioDispatcher) { outputStream.write("${command.rawCommand}\r".toByteArray()) outputStream.flush() } if (delayTime > 0) { delay(delayTime) } } private suspend fun readRawData(maxRetries: Int): String = withContext(ioDispatcher) { val res = StringBuilder() var retriesCount = 0 // Read until '>' arrives, stream ends (-1), or retries are exhausted. var isReading = true while (isReading) { if (inputStream.available() > 0) { val byteValue = inputStream.read() if (byteValue == -1) { isReading = false } else { val charValue = byteValue.toChar() if (charValue == '>') { isReading = false } else { res.append(charValue) } } } else { if (retriesCount >= maxRetries) { isReading = false } else { retriesCount += 1 delay(READ_RETRY_DELAY_MS) } } } removeAll(SEARCHING_PATTERN, res.toString()).trim() } private fun cacheKey(command: ObdCommand): String { val classKey = command::class.qualifiedName ?: command.javaClass.name return "$classKey:${command.rawCommand}" } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/BytesToIntParameterizedTests.kt ================================================ package com.github.eltonvs.obd.command import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class BytesToIntParameterizedTests( private val bufferedValue: IntArray, private val start: Int, private val bytesToProcess: Int, private val expected: Long, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf(intArrayOf(0x0), 0, -1, 0), arrayOf(intArrayOf(0x1), 0, -1, 1), arrayOf(intArrayOf(0x1), 0, 1, 1), arrayOf(intArrayOf(0x10), 0, -1, 16), arrayOf(intArrayOf(0x11), 0, -1, 17), arrayOf(intArrayOf(0xFF), 0, -1, 255), arrayOf(intArrayOf(0xFF, 0xFF), 0, -1, 65535), arrayOf(intArrayOf(0xFF, 0xFF), 0, 1, 255), arrayOf(intArrayOf(0xFF, 0x00), 0, -1, 65280), arrayOf(intArrayOf(0xFF, 0x00), 0, 1, 255), arrayOf(intArrayOf(0x41, 0x0D, 0x40), 2, -1, 64), arrayOf(intArrayOf(0x41, 0x0D, 0x40, 0xFF), 2, 1, 64), ) } @Test fun `test valid results for bytesToInt`() { val result = bytesToInt(bufferedValue, start = start, bytesToProcess = bytesToProcess) assertEquals(expected, result) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/control/AvailableCommands.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals private val AVAILABLE_PIDS_01_TO_20_VALUES = listOf( // Renault Sandero 2014 arrayOf( "BE3EB811", intArrayOf( 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x20, ), ), // Chevrolet Onix 2015 arrayOf( "BE3FB813", intArrayOf( 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x1f, 0x20, ), ), // Toyota Corolla 2015 arrayOf( "BE1FA813", intArrayOf( 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x13, 0x15, 0x1c, 0x1f, 0x20, ), ), // Fiat Siena 2011 arrayOf( "BE3EB811", intArrayOf( 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x20, ), ), // VW Gol 2014 arrayOf( "BE3EB813", intArrayOf( 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0xb, 0xc, 0xd, 0xe, 0xf, 0x11, 0x13, 0x14, 0x15, 0x1c, 0x1f, 0x20, ), ), // Empty arrayOf("00000000", intArrayOf()), // Complete arrayOf("FFFFFFFF", (0x1..0x20).toList().toIntArray()), ) @RunWith(Parameterized::class) class AvailablePIDsCommand01to20ParameterizedTests( private val rawValue: String, private val expected: IntArray, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = AVAILABLE_PIDS_01_TO_20_VALUES } @Test fun `test valid available PIDs 01 to 20 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_01_TO_20).run { handleResponse(rawResponse) } assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) } } @RunWith(Parameterized::class) class AvailablePIDsCommand21to40ParameterizedTests( private val rawValue: String, private val expected: IntArray, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // Renault Sandero 2014 arrayOf("80018001", intArrayOf(0x21, 0x30, 0x31, 0x40)), // Chevrolet Onix 2015 arrayOf( "8007A011", intArrayOf(0x21, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x3c, 0x40), ), // Toyota Corolla 2015 arrayOf( "9005B015", intArrayOf(0x21, 0x24, 0x2e, 0x30, 0x31, 0x33, 0x34, 0x3c, 0x3e, 0x40), ), // Fiat Siena 2011 arrayOf("80000000", intArrayOf(0x21)), // VW Gol 2014 arrayOf( "8007A011", intArrayOf(0x21, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x3c, 0x40), ), // Empty arrayOf("00000000", intArrayOf()), // Complete arrayOf("FFFFFFFF", (0x21..0x40).toList().toIntArray()), ) } @Test fun `test valid available PIDs 21 to 40 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_21_TO_40).run { handleResponse(rawResponse) } assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) } } @RunWith(Parameterized::class) class AvailablePIDsCommand41to60ParameterizedTests( private val rawValue: String, private val expected: IntArray, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // Renault Sandero 2014 arrayOf("80000000", intArrayOf(0x41)), // Chevrolet Onix 2015 arrayOf( "FED0C000", intArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x49, 0x4a, 0x4c, 0x51, 0x52), ), // Toyota Corolla 2015 arrayOf( "7ADC8001", intArrayOf(0x42, 0x43, 0x44, 0x45, 0x47, 0x49, 0x4a, 0x4c, 0x4d, 0x4e, 0x51, 0x60), ), // VW Gol 2014 arrayOf( "FED14400", intArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x49, 0x4a, 0x4c, 0x50, 0x52, 0x56), ), // Empty arrayOf("00000000", intArrayOf()), // Complete arrayOf("FFFFFFFF", (0x41..0x60).toList().toIntArray()), ) } @Test fun `test valid available PIDs 41 to 60 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_41_TO_60).run { handleResponse(rawResponse) } assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) } } @RunWith(Parameterized::class) class AvailablePIDsCommand61to80ParameterizedTests( private val rawValue: String, private val expected: IntArray, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // Toyota Corolla 2015 arrayOf("08000000", intArrayOf(0x65)), // Empty arrayOf("00000000", intArrayOf()), // Complete arrayOf("FFFFFFFF", (0x61..0x80).toList().toIntArray()), ) } @Test fun `test valid available PIDs 61 to 80 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_61_TO_80).run { handleResponse(rawResponse) } assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) } } @RunWith(Parameterized::class) class AvailablePIDsCommand81toA0ParameterizedTests( private val rawValue: String, private val expected: IntArray, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // Empty arrayOf("00000000", intArrayOf()), // Complete arrayOf("FFFFFFFF", (0x81..0xA0).toList().toIntArray()), ) } @Test fun `test valid available PIDs 81 to A0 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AvailablePIDsCommand(AvailablePIDsCommand.AvailablePIDsRanges.PIDS_81_TO_A0).run { handleResponse(rawResponse) } assertEquals(expected.joinToString(",") { "%02X".format(it) }, obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/control/Control.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class ModuleVoltageCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("414204E2", 1.25f), arrayOf("41420000", 0f), arrayOf("4142FFFF", 65.535f), ) } @Test fun `test valid module voltage responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = ModuleVoltageCommand().run { handleResponse(rawResponse) } assertEquals("%.2fV".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class TimingAdvanceCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410E70", -8f), arrayOf("410E00", -64f), arrayOf("410EFF", 63.5f), arrayOf("410EFFFF", 63.5f), ) } @Test fun `test valid timing advance responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = TimingAdvanceCommand().run { handleResponse(rawResponse) } assertEquals("%.2f°".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class VINCommandParameterizedTests( private val rawValue: String, private val expected: String, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // CAN (ISO-15765) format arrayOf( "0140:4902013933591:425352375248452:4A323938313136", "93YBSR7RHEJ298116", ), arrayOf( "0140:4902015750301:5A5A5A39395A542:53333932313234", "WP0ZZZ99ZTS392124", ), // ISO9141-2, KWP2000 Fast and KWP2000 5Kbps (ISO15031) format arrayOf( "490201000000394902023359425349020352375248490204454A323949020538313136", "93YBSR7RHEJ298116", ), arrayOf( "4902010000005749020250305A5A4902035A39395A4902045453333949020532313234", "WP0ZZZ99ZTS392124", ), 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", ), ) } @Test fun `test valid vin responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = VINCommand().run { handleResponse(rawResponse) } assertEquals(expected, obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/control/MIL.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class MILOnCommandParameterizedTests( private val rawValue: String, private val expected: Boolean, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410100452100", false), arrayOf("410100000000", false), arrayOf("41017F000000", false), arrayOf("41017FFFFFFF", false), arrayOf("410180000000", true), arrayOf("410180FFFFFF", true), arrayOf("4101FFFFFFFF", true), ) } @Test fun `test valid MIL on responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = MILOnCommand().run { handleResponse(rawResponse) } assertEquals("MIL is ${if (expected) "ON" else "OFF"}", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class DistanceMILOnCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41210000", 0), arrayOf("41215C8D", 23_693), arrayOf("4121FFFF", 65_535), ) } @Test fun `test valid distance MIL on responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = DistanceMILOnCommand().run { handleResponse(rawResponse) } assertEquals("${expected}Km", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class TimeSinceMILOnCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("414D0000", 0), arrayOf("414D5C8D", 23_693), arrayOf("414DFFFF", 65_535), ) } @Test fun `test valid time since MIL on responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = TimeSinceMILOnCommand().run { handleResponse(rawResponse) } assertEquals("${expected}min", obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/control/Monitor.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.Monitors import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals private val completeStatus = SensorStatus(available = true, complete = true) private val incompleteStatus = SensorStatus(available = true, complete = false) private val notAvailableCompleteStatus = SensorStatus(available = false, complete = true) private val notAvailableIncompleteStatus = SensorStatus(available = false, complete = false) private val expected1 = SensorStatusData( milOn = true, dtcCount = 3, isSpark = true, items = Monitors .values() .filter { it.isSparkIgnition ?: true } .map { it to completeStatus } .toMap(), ) private val expected2 = SensorStatusData( milOn = false, dtcCount = 0, isSpark = false, items = mapOf( Monitors.MISFIRE to incompleteStatus, Monitors.FUEL_SYSTEM to notAvailableIncompleteStatus, Monitors.COMPREHENSIVE_COMPONENT to notAvailableIncompleteStatus, Monitors.NMHC_CATALYST to incompleteStatus, Monitors.NOX_SCR_MONITOR to incompleteStatus, Monitors.BOOST_PRESSURE to notAvailableCompleteStatus, Monitors.EXHAUST_GAS_SENSOR to notAvailableCompleteStatus, Monitors.PM_FILTER to notAvailableCompleteStatus, Monitors.EGR_VVT_SYSTEM to notAvailableCompleteStatus, ), ) private val expected3 = SensorStatusData( milOn = false, dtcCount = 0, isSpark = true, items = mapOf( Monitors.CATALYST to completeStatus, Monitors.EGR_SYSTEM to incompleteStatus, Monitors.SECONDARY_AIR_SYSTEM to incompleteStatus, Monitors.COMPREHENSIVE_COMPONENT to completeStatus, Monitors.OXYGEN_SENSOR_HEATER to incompleteStatus, Monitors.HEATED_CATALYST to completeStatus, Monitors.FUEL_SYSTEM to completeStatus, Monitors.OXYGEN_SENSOR to completeStatus, Monitors.MISFIRE to completeStatus, Monitors.EVAPORATIVE_SYSTEM to notAvailableCompleteStatus, Monitors.AC_REFRIGERANT to notAvailableCompleteStatus, ), ) private val expected4 = SensorStatusData( milOn = false, dtcCount = 0, isSpark = true, items = Monitors .values() .filter { it.isSparkIgnition ?: true } .map { it to completeStatus } .toMap(), ) private val expected5 = SensorStatusData( milOn = false, dtcCount = 0, isSpark = false, items = mapOf( Monitors.FUEL_SYSTEM to notAvailableCompleteStatus, Monitors.NMHC_CATALYST to incompleteStatus, Monitors.EXHAUST_GAS_SENSOR to incompleteStatus, Monitors.MISFIRE to notAvailableCompleteStatus, Monitors.PM_FILTER to notAvailableCompleteStatus, Monitors.BOOST_PRESSURE to notAvailableCompleteStatus, Monitors.EGR_VVT_SYSTEM to notAvailableCompleteStatus, Monitors.NOX_SCR_MONITOR to notAvailableCompleteStatus, Monitors.COMPREHENSIVE_COMPONENT to notAvailableIncompleteStatus, ), ) @RunWith(Parameterized::class) class MonitorStatusSinceCodesClearedCommandTests( private val rawValue: String, private val expected: SensorStatusData, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41018307FF00", expected1), arrayOf("41 01 83 07 FF 00", expected1), arrayOf("8307FF00", expected1), arrayOf("410100790303", expected2), arrayOf("41 01 00 79 03 03", expected2), arrayOf("00790303", expected2), arrayOf("41010007EBC8", expected3), arrayOf("41 01 00 07 EB C8", expected3), arrayOf("0007EBC8", expected3), ) } @Test fun `test valid monitor status since CC responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = MonitorStatusSinceCodesClearedCommand().also { it.handleResponse(rawResponse) } assertEquals(expected, obdResponse.data) } } @RunWith(Parameterized::class) class MonitorStatusCurrentDriveCycleCommandTests( private val rawValue: String, private val expected: SensorStatusData, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41410007FF00", expected4), arrayOf("41 41 00 07 FF 00", expected4), arrayOf("0007FF00", expected4), arrayOf("414100790303", expected2), arrayOf("41 41 00 79 03 03", expected2), arrayOf("00790303", expected2), arrayOf("414100482135", expected5), arrayOf("41 41 00 48 21 35", expected5), arrayOf("00482135", expected5), ) } @Test fun `test valid monitor status current drive cycle responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = MonitorStatusCurrentDriveCycleCommand().also { it.handleResponse(rawResponse) } assertEquals(expected, obdResponse.data) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/control/TroubleCodes.kt ================================================ package com.github.eltonvs.obd.command.control import com.github.eltonvs.obd.command.NoDataException import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class DTCNumberCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410100452100", 0), arrayOf("410100000000", 0), arrayOf("41017F000000", 127), arrayOf("410123456789", 35), arrayOf("41017FFFFFFF", 127), arrayOf("410180000000", 0), arrayOf("410180FFFFFF", 0), arrayOf("410189ABCDEF", 9), arrayOf("4101FFFFFFFF", 127), ) } @Test fun `test valid DTC number responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = DTCNumberCommand().run { handleResponse(rawResponse) } assertEquals("$expected codes", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class DistanceSinceCodesClearedCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("4131F967", 63_847), arrayOf("41310000", 0), arrayOf("4131FFFF", 65_535), ) } @Test fun `test valid distance since codes cleared responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = DistanceSinceCodesClearedCommand().run { handleResponse(rawResponse) } assertEquals("${expected}Km", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class TimeSinceCodesClearedCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("414E4543", 17_731), arrayOf("414E0000", 0), arrayOf("414EFFFF", 65_535), ) } @Test fun `test valid time since codes cleared responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = TimeSinceCodesClearedCommand().run { handleResponse(rawResponse) } assertEquals("${expected}min", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class TroubleCodesCommandsParameterizedTests( private val rawValue: String, private val expected: List, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // Two frames with four dtc arrayOf("4300035104A1AB\r43F10600000000", listOf("P0003", "C1104", "B21AB", "U3106")), // One frame with three dtc arrayOf("43010301040105", listOf("P0103", "P0104", "P0105")), // One frame with two dtc arrayOf("43010301040000", listOf("P0103", "P0104")), // Two frames with four dtc CAN (ISO-15765) format arrayOf("00A\r0:430401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")), // One frame with two dtc CAN (ISO-15765) format arrayOf("430201200121", listOf("P0120", "P0121")), // Empty data arrayOf("4300", listOf()), ) } @Test fun `test valid trouble codes responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = TroubleCodesCommand().run { handleResponse(rawResponse) } assertEquals(expected.joinToString(separator = ","), obdResponse.formattedValue) } } class TroubleCodesCommandsTests { @Test(expected = NoDataException::class) fun `test trouble codes no data response`() { val rawResponse = ObdRawResponse(value = "43NODATA", elapsedTime = 0) TroubleCodesCommand().run { handleResponse(rawResponse) } } } @RunWith(Parameterized::class) class PendingTroubleCodesCommandsParameterizedTests( private val rawValue: String, private val expected: List, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // Two frames with four dtc arrayOf("4700035104A1AB\r47F10600000000", listOf("P0003", "C1104", "B21AB", "U3106")), // One frame with three dtc arrayOf("47010301040105", listOf("P0103", "P0104", "P0105")), // One frame with two dtc arrayOf("47010301040000", listOf("P0103", "P0104")), // Two frames with four dtc CAN (ISO-15765) format arrayOf("00A\r0:470401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")), // One frame with two dtc CAN (ISO-15765) format arrayOf("470201200121", listOf("P0120", "P0121")), // Empty data arrayOf("4700", listOf()), ) } @Test fun `test valid pending trouble codes responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = PendingTroubleCodesCommand().run { handleResponse(rawResponse) } assertEquals(expected.joinToString(separator = ","), obdResponse.formattedValue) } } class PendingTroubleCodesCommandsTests { @Test(expected = NoDataException::class) fun `test pending trouble codes no data response`() { val rawResponse = ObdRawResponse(value = "47NODATA", elapsedTime = 0) PendingTroubleCodesCommand().run { handleResponse(rawResponse) } } } @RunWith(Parameterized::class) class PermanentTroubleCodesCommandsParameterizedTests( private val rawValue: String, private val expected: List, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( // Two frames with four dtc arrayOf("4A00035104A1AB\r4AF10600000000", listOf("P0003", "C1104", "B21AB", "U3106")), // One frame with three dtc arrayOf("4A010301040105", listOf("P0103", "P0104", "P0105")), // One frame with two dtc arrayOf("4A010301040000", listOf("P0103", "P0104")), // Two frames with four dtc CAN (ISO-15765) format arrayOf("00A\r0:4A0401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")), // One frame with two dtc CAN (ISO-15765) format arrayOf("4A0201200121", listOf("P0120", "P0121")), // Empty data arrayOf("4A00", listOf()), ) } @Test fun `test valid permanent trouble codes responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = PermanentTroubleCodesCommand().run { handleResponse(rawResponse) } assertEquals(expected.joinToString(separator = ","), obdResponse.formattedValue) } } class PermanentTroubleCodesCommandsTests { @Test(expected = NoDataException::class) fun `test permanent trouble codes no data response`() { val rawResponse = ObdRawResponse(value = "4ANODATA", elapsedTime = 0) PermanentTroubleCodesCommand().run { handleResponse(rawResponse) } } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/egr/Egr.kt ================================================ package com.github.eltonvs.obd.command.egr import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class CommandedEgrCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("414545", 27.1f), arrayOf("414500", 0f), arrayOf("4145FF", 100f), arrayOf("4145FFFF", 100f), ) } @Test fun `test valid commanded egr responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = CommandedEgrCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } @RunWith(Parameterized::class) class EgrErrorCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410610", -87.5f), arrayOf("410643", -47.7f), arrayOf("410680", 0f), arrayOf("4106C8", 56.25f), arrayOf("410600", -100f), arrayOf("4106FF", 99.2f), arrayOf("4106FFFF", 99.2f), ) } @Test fun `test valid egr error responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = EgrErrorCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/engine/Engine.kt ================================================ package com.github.eltonvs.obd.command.engine import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class SpeedCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410D15", 21), arrayOf("410D40", 64), arrayOf("410D00", 0), arrayOf("410DFF", 255), arrayOf("410DFFFF", 255), ) } @Test fun `test valid speed responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = SpeedCommand().run { handleResponse(rawResponse) } assertEquals("${expected}Km/h", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class RPMCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410C200D", 2051), arrayOf("410C10A4", 1065), arrayOf("41 0C 10 A4", 1065), arrayOf("41 0c 10 a4", 1065), arrayOf("410C10A4410C10C1", 1065), arrayOf("41 0C 10 A4 41 0C 10 C1", 1065), arrayOf("410C0E02410C0E02", 896), arrayOf("41 0C 0E 02 41 0C 0E 02", 896), arrayOf("410C283C", 2575), arrayOf("410C0A00", 640), arrayOf("410C0000", 0), arrayOf("410CFFFF", 16_383), ) } @Test fun `test valid rpm responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = RPMCommand().run { handleResponse(rawResponse) } assertEquals("${expected}RPM", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class MassAirFlowCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41109511", 381.61f), arrayOf("41101234", 46.6f), arrayOf("41100000", 0f), arrayOf("4110FFFF", 655.35f), ) } @Test fun `test valid maf responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = MassAirFlowCommand().run { handleResponse(rawResponse) } assertEquals("%.2fg/s".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class RuntimeCommandParameterizedTests( private val rawValue: String, private val expected: String, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("411F4543", "04:55:31"), arrayOf("411F1234", "01:17:40"), arrayOf("411F0000", "00:00:00"), arrayOf("411FFFFF", "18:12:15"), ) } @Test fun `test valid runtime responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = RuntimeCommand().run { handleResponse(rawResponse) } assertEquals(expected, obdResponse.formattedValue) } } @RunWith(Parameterized::class) class LoadCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410410", 6.3f), arrayOf("410400", 0f), arrayOf("4104FF", 100f), arrayOf("4104FFFF", 100f), ) } @Test fun `test valid engine load responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = LoadCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } @RunWith(Parameterized::class) class AbsoluteLoadCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41434143", 6551.8f), arrayOf("41431234", 1827.5f), arrayOf("41430000", 0f), arrayOf("4143FFFF", 25_700f), ) } @Test fun `test valid engine absolute load responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AbsoluteLoadCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } @RunWith(Parameterized::class) class ThrottlePositionCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("411111", 6.7f), arrayOf("411100", 0f), arrayOf("4111FF", 100f), arrayOf("4111FFFF", 100f), ) } @Test fun `test valid throttle position responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = ThrottlePositionCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } @RunWith(Parameterized::class) class RelativeThrottlePositionCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("414545", 27.1f), arrayOf("414500", 0f), arrayOf("4145FF", 100f), arrayOf("4145FFFF", 100f), ) } @Test fun `test valid relative throttle position responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = RelativeThrottlePositionCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/fuel/Fuel.kt ================================================ package com.github.eltonvs.obd.command.fuel import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class FuelConsumptionRateCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("415E10E3", 216.2f), arrayOf("415E1234", 233f), arrayOf("415E0000", 0f), arrayOf("415EFFFF", 3276.75f), ) } @Test fun `test valid fuel consumption responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelConsumptionRateCommand().run { handleResponse(rawResponse) } assertEquals("%.1fL/h".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class FuelTypeCommandParameterizedTests( private val rawValue: String, private val expected: String, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("415100", "Not Available"), arrayOf("415101", "Gasoline"), arrayOf("415102", "Methanol"), arrayOf("415103", "Ethanol"), arrayOf("415104", "Diesel"), arrayOf("415105", "GPL/LGP"), arrayOf("415106", "Natural Gas"), arrayOf("415107", "Propane"), arrayOf("415108", "Electric"), arrayOf("415109", "Biodiesel + Gasoline"), arrayOf("41510A", "Biodiesel + Methanol"), arrayOf("41510B", "Biodiesel + Ethanol"), arrayOf("41510C", "Biodiesel + GPL/LGP"), arrayOf("41510D", "Biodiesel + Natural Gas"), arrayOf("41510E", "Biodiesel + Propane"), arrayOf("41510F", "Biodiesel + Electric"), arrayOf("415110", "Biodiesel + Gasoline/Electric"), arrayOf("415111", "Hybrid Gasoline"), arrayOf("415112", "Hybrid Ethanol"), arrayOf("415113", "Hybrid Diesel"), arrayOf("415114", "Hybrid Electric"), arrayOf("415115", "Hybrid Mixed"), arrayOf("415116", "Hybrid Regenerative"), arrayOf("415116FF", "Hybrid Regenerative"), arrayOf("4151FF", "Unknown"), arrayOf("4151FFFF", "Unknown"), ) } @Test fun `test valid fuel type responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelTypeCommand().run { handleResponse(rawResponse) } assertEquals(expected, obdResponse.formattedValue) } } @RunWith(Parameterized::class) class GenericFuelLevelCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("412F10", 6.3f), arrayOf("412FC8", 78.4f), arrayOf("412F00", 0f), arrayOf("412FFF", 100f), arrayOf("412FFFFF", 100f), ) } @Test fun `test valid fuel level responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelLevelCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } @Test fun `test valid ethanol level responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = EthanolLevelCommand().run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } @RunWith(Parameterized::class) class GenericFuelTrimCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410610", -87.5f), arrayOf("410643", -47.7f), arrayOf("410680", 0f), arrayOf("4106C8", 56.25f), arrayOf("410600", -100f), arrayOf("4106FF", 99.2f), arrayOf("4106FFFF", 99.2f), ) } @Test fun `test valid fuel trim short term bank 1 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.SHORT_TERM_BANK_1).run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } @Test fun `test valid fuel trim short term bank 2 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.SHORT_TERM_BANK_2).run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } @Test fun `test valid fuel trim long term bank 1 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.LONG_TERM_BANK_1).run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } @Test fun `test valid fuel trim long term bank 2 responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelTrimCommand(FuelTrimCommand.FuelTrimBank.LONG_TERM_BANK_2).run { handleResponse(rawResponse) } assertEquals("%.1f".format(expected) + '%', obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/fuel/Ratio.kt ================================================ package com.github.eltonvs.obd.command.fuel import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class CommandedEquivalenceRatioCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41441234", 0.14f), arrayOf("41444040", 0.5f), arrayOf("41448080", 1f), arrayOf("41440000", 0f), arrayOf("4144FFFF", 2f), arrayOf("4144FFFFFFFF", 2f), ) } @Test fun `test valid commanded equivalence ratio responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = CommandedEquivalenceRatioCommand().run { handleResponse(rawResponse) } assertEquals("%.2fF/A".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class FuelAirEquivalenceRatioCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41341234", 0.14f), arrayOf("41344040", 0.5f), arrayOf("41348080", 1f), arrayOf("41340000", 0f), arrayOf("4134FFFF", 2f), arrayOf("4134FFFFFFFF", 2f), ) } @Test fun `test valid fuel air equivalence ratio responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) FuelAirEquivalenceRatioCommand.OxygenSensor.values().forEach { val obdResponse = FuelAirEquivalenceRatioCommand(it).run { handleResponse(rawResponse) } assertEquals("%.2fF/A".format(expected), obdResponse.formattedValue) } } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/pressure/Pressure.kt ================================================ package com.github.eltonvs.obd.command.pressure import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class BarometricPressureCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("413312", 18), arrayOf("413340", 64), arrayOf("413364", 100), arrayOf("413380", 128), arrayOf("413300", 0), arrayOf("4133FF", 255), arrayOf("4133FFFF", 255), ) } @Test fun `test valid barometric pressure responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = BarometricPressureCommand().run { handleResponse(rawResponse) } assertEquals("${expected}kPa", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class IntakeManifoldPressureCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410B12", 18), arrayOf("410B39", 57), arrayOf("410B40", 64), arrayOf("410B64", 100), arrayOf("410B80", 128), arrayOf("410B00", 0), arrayOf("410BFF", 255), arrayOf("410BFFFF", 255), ) } @Test fun `test valid intake manifold pressure responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = IntakeManifoldPressureCommand().run { handleResponse(rawResponse) } assertEquals("${expected}kPa", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class FuelPressureCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410A12", 54), arrayOf("410A40", 192), arrayOf("410A64", 300), arrayOf("410A80", 384), arrayOf("410A00", 0), arrayOf("410AFF", 765), arrayOf("410AFFFF", 765), ) } @Test fun `test valid fuel pressure responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelPressureCommand().run { handleResponse(rawResponse) } assertEquals("${expected}kPa", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class FuelRailPressureCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41230000", 0.000f), arrayOf("410B39", 4.503f), arrayOf("410B6464", 2030.300f), arrayOf("4123FFFF", 5177.265f), ) } @Test fun `test valid fuel rail pressure responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelRailPressureCommand().run { handleResponse(rawResponse) } assertEquals("%.3f".format(expected) + "kPa", obdResponse.formattedValue) } } @RunWith(Parameterized::class) class FuelRailGaugePressureCommandParameterizedTests( private val rawValue: String, private val expected: Int, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("41231234", 46_600), arrayOf("41234354", 172_360), arrayOf("412360ED", 248_130), arrayOf("41238080", 328_960), arrayOf("41230000", 0), arrayOf("4123FFFF", 655_350), ) } @Test fun `test valid fuel rail gauge pressure responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = FuelRailGaugePressureCommand().run { handleResponse(rawResponse) } assertEquals("${expected}kPa", obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/command/temperature/Temperature.kt ================================================ package com.github.eltonvs.obd.command.temperature import com.github.eltonvs.obd.command.ObdRawResponse import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.Test import kotlin.test.assertEquals @RunWith(Parameterized::class) class AirIntakeTemperatureCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410F40", 24f), arrayOf("410F5D", 53f), arrayOf("410F00", -40f), arrayOf("410FFF", 215f), arrayOf("410FFFFF", 215f), ) } @Test fun `test valid air intake temperature responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AirIntakeTemperatureCommand().run { handleResponse(rawResponse) } assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class AmbientAirTemperatureCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("414640", 24f), arrayOf("41465D", 53f), arrayOf("414600", -40f), arrayOf("4146FF", 215f), arrayOf("4146FFFF", 215f), ) } @Test fun `test valid ambient air intake temperature responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = AmbientAirTemperatureCommand().run { handleResponse(rawResponse) } assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class EngineCoolantTemperatureCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("410540", 24f), arrayOf("41055D", 53f), arrayOf("410500", -40f), arrayOf("4105FF", 215f), arrayOf("4105FFFF", 215f), ) } @Test fun `test valid engine coolant temperature responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = EngineCoolantTemperatureCommand().run { handleResponse(rawResponse) } assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) } } @RunWith(Parameterized::class) class OilTemperatureCommandParameterizedTests( private val rawValue: String, private val expected: Float, ) { companion object { @JvmStatic @Parameterized.Parameters fun values() = listOf( arrayOf("415C40", 24f), arrayOf("415C5D", 53f), arrayOf("415C00", -40f), arrayOf("415CFF", 215f), arrayOf("415CFFFF", 215f), ) } @Test fun `test valid oil temperature responses handler`() { val rawResponse = ObdRawResponse(value = rawValue, elapsedTime = 0) val obdResponse = OilTemperatureCommand().run { handleResponse(rawResponse) } assertEquals("%.1f°C".format(expected), obdResponse.formattedValue) } } ================================================ FILE: src/test/kotlin/com/github/eltonvs/obd/connection/ObdDeviceConnectionTest.kt ================================================ package com.github.eltonvs.obd.connection import com.github.eltonvs.obd.command.ObdCommand import com.github.eltonvs.obd.command.ObdRawResponse import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import kotlinx.coroutines.yield import java.io.ByteArrayOutputStream import java.io.InputStream import java.io.OutputStream import java.util.ArrayDeque import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue class ObdDeviceConnectionTest { @Test fun `runs one command at a time when invoked concurrently`() = runBlocking { val input = ScriptedInputStream() val output = ScriptedOutputStream( input = input, responses = mapOf( "01 0D" to ResponsePlan(payload = "410D40>", autoEnqueue = false), "01 0C" to ResponsePlan(payload = "410C1AF8>"), ), ) val connection = ObdDeviceConnection(input, output, Dispatchers.Default) val speedCommand = TestObdCommand(tag = "SPEED", pid = "0D") val rpmCommand = TestObdCommand(tag = "RPM", pid = "0C") val first = async { connection.run(speedCommand) } withTimeout(1_000) { while (output.writes.isEmpty()) { delay(10) } } val secondStarted = CompletableDeferred() val second = async { secondStarted.complete(Unit) connection.run(rpmCommand) } secondStarted.await() repeat(20) { yield() } assertEquals(listOf("01 0D"), output.writes) input.enqueue("410D40>") first.await() second.await() assertEquals(listOf("01 0D", "01 0C"), output.writes) } @Test fun `uses cache safely when called concurrently`() = runBlocking { val input = ScriptedInputStream() val output = ScriptedOutputStream( input = input, responses = mapOf("01 05" to ResponsePlan(payload = "41057B>")), ) val connection = ObdDeviceConnection(input, output, Dispatchers.Default) val commandA = TestObdCommand(tag = "COOLANT_TEMP", pid = "05") val commandB = TestObdCommand(tag = "COOLANT_TEMP", pid = "05") val first = async { connection.run(commandA, useCache = true) } val second = async { connection.run(commandB, useCache = true) } assertEquals("41057B", first.await().value) assertEquals("41057B", second.await().value) assertEquals(1, output.writes.size) } @Test fun `handles EOF while reading response`() = runBlocking { val input = DisconnectAfterPayloadInputStream(payload = "410D40") val output = ByteArrayOutputStream() val connection = ObdDeviceConnection(input, output, Dispatchers.Default) val command = TestObdCommand(tag = "SPEED", pid = "0D") val response = connection.run(command) assertEquals("410D40", response.value) assertEquals("410D40", response.rawResponse.value) } @Test fun `propagates cancellation while waiting for data`() { runBlocking { val input = IdleInputStream() val output = ByteArrayOutputStream() val connection = ObdDeviceConnection(input, output, Dispatchers.Default) val command = TestObdCommand(tag = "SPEED", pid = "0D") withTimeout(2_000) { val runningCommand = async { connection.run(command, maxRetries = 1_000) } delay(100) runningCommand.cancel() assertFailsWith { runningCommand.await() } } } } @Test fun `returns quickly when max retries is zero and no data is available`() = runBlocking { val input = IdleInputStream() val output = ByteArrayOutputStream() val connection = ObdDeviceConnection(input, output, Dispatchers.Default) val command = TestObdCommand(tag = "SPEED", pid = "0D") withTimeout(400) { val response = connection.run(command, maxRetries = 0) assertEquals("", response.value) } } } private class TestObdCommand( override val tag: String, override val pid: String, override val mode: String = "01", ) : ObdCommand() { override val name: String = tag override val skipDigitCheck: Boolean = true override val handler: (ObdRawResponse) -> String = { it.processedValue } } private data class ResponsePlan( val payload: String, val autoEnqueue: Boolean = true, ) private class ScriptedInputStream : InputStream() { private val bytes = ArrayDeque() @Synchronized fun enqueue(payload: String) { payload.toByteArray().forEach { byte -> bytes.addLast(byte.toInt() and 0xFF) } } @Synchronized override fun available(): Int = bytes.size @Synchronized override fun read(): Int = if (bytes.isNotEmpty()) bytes.removeFirst() else -1 } private class ScriptedOutputStream( private val input: ScriptedInputStream, private val responses: Map, ) : OutputStream() { val writes = mutableListOf() override fun write(b: Int): Unit = throw UnsupportedOperationException("write(Int) is not used in these tests") @Synchronized override fun write( b: ByteArray, off: Int, len: Int, ) { val rawCommand = String(b, off, len).trim() writes.add(rawCommand) val plan = responses[rawCommand] ?: throw IllegalArgumentException("Missing scripted response for command [$rawCommand]") if (plan.autoEnqueue) { input.enqueue(plan.payload) } } } private class DisconnectAfterPayloadInputStream( payload: String, ) : InputStream() { private val bytes = payload.toByteArray() private var index = 0 private var eofReturned = false override fun available(): Int = if (index < bytes.size || !eofReturned) 1 else 0 override fun read(): Int = if (index < bytes.size) { bytes[index++].toInt() and 0xFF } else { eofReturned = true -1 } } private class IdleInputStream : InputStream() { override fun available(): Int = 0 override fun read(): Int = -1 }