Full Code of eltonvs/kotlin-obd-api for AI

master 4a1232b436da cached
58 files
161.2 KB
42.9k tokens
1 requests
Download .txt
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
================================================
<component name="ProjectCodeStyleConfiguration">
  <code_scheme name="Project" version="173">
    <JetCodeStyleSettings>
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
    </JetCodeStyleSettings>
    <codeStyleSettings language="kotlin">
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
    </codeStyleSettings>
  </code_scheme>
</component>

================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
  <state>
    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
  </state>
</component>

================================================
FILE: .idea/encodings.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

================================================
FILE: .idea/kotlinScripting.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="KotlinScriptingSettings">
    <option name="isAutoReloadEnabled" value="true" />
    <option name="suppressDefinitionsCheck" value="true" />
  </component>
</project>

================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>

================================================
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
================================================
<p align="center">
  <img width="300px" src="img/kotlin-obd-api-logo.png" />
</p>

<h1 align="center">Kotlin OBD API</h1>

[![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
<repositories>
  <repository>
      <id>jitpack.io</id>
      <url>https://jitpack.io</url>
  </repository>
</repositories>
```

Add the dependency:
```xml
<dependency>
  <groupId>com.github.eltonvs</groupId>
  <artifactId>kotlin-obd-api</artifactId>
  <version>1.4.1</version>
</dependency>
```

### 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<Test>().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<MavenPublication>("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> 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<Monitors, SensorStatus>,
)

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<Int>) {
        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, SensorStatus>()
        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<String>()
        private set

    private fun parseTroubleCodesList(rawValue: String): List<String> {
        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<String, ObdRawResponse>()

        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<Any>(intArrayOf(0x0), 0, -1, 0),
                arrayOf<Any>(intArrayOf(0x1), 0, -1, 1),
                arrayOf<Any>(intArrayOf(0x1), 0, 1, 1),
                arrayOf<Any>(intArrayOf(0x10), 0, -1, 16),
                arrayOf<Any>(intArrayOf(0x11), 0, -1, 17),
                arrayOf<Any>(intArrayOf(0xFF), 0, -1, 255),
                arrayOf<Any>(intArrayOf(0xFF, 0xFF), 0, -1, 65535),
                arrayOf<Any>(intArrayOf(0xFF, 0xFF), 0, 1, 255),
                arrayOf<Any>(intArrayOf(0xFF, 0x00), 0, -1, 65280),
                arrayOf<Any>(intArrayOf(0xFF, 0x00), 0, 1, 255),
                arrayOf<Any>(intArrayOf(0x41, 0x0D, 0x40), 2, -1, 64),
                arrayOf<Any>(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<Any>(
            "BE3EB811",
            intArrayOf(
                0x1,
                0x3,
                0x4,
                0x5,
                0x6,
                0x7,
                0xb,
                0xc,
                0xd,
                0xe,
                0xf,
                0x11,
                0x13,
                0x14,
                0x15,
                0x1c,
                0x20,
            ),
        ),
        // Chevrolet Onix 2015
        arrayOf<Any>(
            "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<Any>(
            "BE1FA813",
            intArrayOf(
                0x1,
                0x3,
                0x4,
                0x5,
                0x6,
                0x7,
                0xc,
                0xd,
                0xe,
                0xf,
                0x10,
                0x11,
                0x13,
                0x15,
                0x1c,
                0x1f,
                0x20,
            ),
        ),
        // Fiat Siena 2011
        arrayOf<Any>(
            "BE3EB811",
            intArrayOf(
                0x1,
                0x3,
                0x4,
                0x5,
                0x6,
                0x7,
                0xb,
                0xc,
                0xd,
                0xe,
                0xf,
                0x11,
                0x13,
                0x14,
                0x15,
                0x1c,
                0x20,
            ),
        ),
        // VW Gol 2014
        arrayOf<Any>(
            "BE3EB813",
            intArrayOf(
                0x1,
                0x3,
                0x4,
                0x5,
                0x6,
                0x7,
                0xb,
                0xc,
                0xd,
                0xe,
                0xf,
                0x11,
                0x13,
                0x14,
                0x15,
                0x1c,
                0x1f,
                0x20,
            ),
        ),
        // Empty
        arrayOf<Any>("00000000", intArrayOf()),
        // Complete
        arrayOf<Any>("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<Any>("80018001", intArrayOf(0x21, 0x30, 0x31, 0x40)),
                // Chevrolet Onix 2015
                arrayOf<Any>(
                    "8007A011",
                    intArrayOf(0x21, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x3c, 0x40),
                ),
                // Toyota Corolla 2015
                arrayOf<Any>(
                    "9005B015",
                    intArrayOf(0x21, 0x24, 0x2e, 0x30, 0x31, 0x33, 0x34, 0x3c, 0x3e, 0x40),
                ),
                // Fiat Siena 2011
                arrayOf<Any>("80000000", intArrayOf(0x21)),
                // VW Gol 2014
                arrayOf<Any>(
                    "8007A011",
                    intArrayOf(0x21, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x3c, 0x40),
                ),
                // Empty
                arrayOf<Any>("00000000", intArrayOf()),
                // Complete
                arrayOf<Any>("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<Any>("80000000", intArrayOf(0x41)),
                // Chevrolet Onix 2015
                arrayOf<Any>(
                    "FED0C000",
                    intArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x49, 0x4a, 0x4c, 0x51, 0x52),
                ),
                // Toyota Corolla 2015
                arrayOf<Any>(
                    "7ADC8001",
                    intArrayOf(0x42, 0x43, 0x44, 0x45, 0x47, 0x49, 0x4a, 0x4c, 0x4d, 0x4e, 0x51, 0x60),
                ),
                // VW Gol 2014
                arrayOf<Any>(
                    "FED14400",
                    intArrayOf(0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x49, 0x4a, 0x4c, 0x50, 0x52, 0x56),
                ),
                // Empty
                arrayOf<Any>("00000000", intArrayOf()),
                // Complete
                arrayOf<Any>("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<Any>("08000000", intArrayOf(0x65)),
                // Empty
                arrayOf<Any>("00000000", intArrayOf()),
                // Complete
                arrayOf<Any>("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<Any>("00000000", intArrayOf()),
                // Complete
                arrayOf<Any>("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<Any>("414204E2", 1.25f),
                arrayOf<Any>("41420000", 0f),
                arrayOf<Any>("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<Any>("410E70", -8f),
                arrayOf<Any>("410E00", -64f),
                arrayOf<Any>("410EFF", 63.5f),
                arrayOf<Any>("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<Any>(
                    "0140:4902013933591:425352375248452:4A323938313136",
                    "93YBSR7RHEJ298116",
                ),
                arrayOf<Any>(
                    "0140:4902015750301:5A5A5A39395A542:53333932313234",
                    "WP0ZZZ99ZTS392124",
                ),
                // ISO9141-2, KWP2000 Fast and KWP2000 5Kbps (ISO15031) format
                arrayOf<Any>(
                    "490201000000394902023359425349020352375248490204454A323949020538313136",
                    "93YBSR7RHEJ298116",
                ),
                arrayOf<Any>(
                    "4902010000005749020250305A5A4902035A39395A4902045453333949020532313234",
                    "WP0ZZZ99ZTS392124",
                ),
                arrayOf<Any>(
                    "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<Any>("410100452100", false),
                arrayOf<Any>("410100000000", false),
                arrayOf<Any>("41017F000000", false),
                arrayOf<Any>("41017FFFFFFF", false),
                arrayOf<Any>("410180000000", true),
                arrayOf<Any>("410180FFFFFF", true),
                arrayOf<Any>("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<Any>("41210000", 0),
                arrayOf<Any>("41215C8D", 23_693),
                arrayOf<Any>("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<Any>("414D0000", 0),
                arrayOf<Any>("414D5C8D", 23_693),
                arrayOf<Any>("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<Any>("41018307FF00", expected1),
                arrayOf<Any>("41 01 83 07 FF 00", expected1),
                arrayOf<Any>("8307FF00", expected1),
                arrayOf<Any>("410100790303", expected2),
                arrayOf<Any>("41 01 00 79 03 03", expected2),
                arrayOf<Any>("00790303", expected2),
                arrayOf<Any>("41010007EBC8", expected3),
                arrayOf<Any>("41 01 00 07 EB C8", expected3),
                arrayOf<Any>("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<Any>("41410007FF00", expected4),
                arrayOf<Any>("41 41 00 07 FF 00", expected4),
                arrayOf<Any>("0007FF00", expected4),
                arrayOf<Any>("414100790303", expected2),
                arrayOf<Any>("41 41 00 79 03 03", expected2),
                arrayOf<Any>("00790303", expected2),
                arrayOf<Any>("414100482135", expected5),
                arrayOf<Any>("41 41 00 48 21 35", expected5),
                arrayOf<Any>("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<Any>("410100452100", 0),
                arrayOf<Any>("410100000000", 0),
                arrayOf<Any>("41017F000000", 127),
                arrayOf<Any>("410123456789", 35),
                arrayOf<Any>("41017FFFFFFF", 127),
                arrayOf<Any>("410180000000", 0),
                arrayOf<Any>("410180FFFFFF", 0),
                arrayOf<Any>("410189ABCDEF", 9),
                arrayOf<Any>("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<Any>("4131F967", 63_847),
                arrayOf<Any>("41310000", 0),
                arrayOf<Any>("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<Any>("414E4543", 17_731),
                arrayOf<Any>("414E0000", 0),
                arrayOf<Any>("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<String>,
) {
    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun values() =
            listOf(
                // Two frames with four dtc
                arrayOf<Any>("4300035104A1AB\r43F10600000000", listOf("P0003", "C1104", "B21AB", "U3106")),
                // One frame with three dtc
                arrayOf<Any>("43010301040105", listOf("P0103", "P0104", "P0105")),
                // One frame with two dtc
                arrayOf<Any>("43010301040000", listOf("P0103", "P0104")),
                // Two frames with four dtc CAN (ISO-15765) format
                arrayOf<Any>("00A\r0:430401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")),
                // One frame with two dtc CAN (ISO-15765) format
                arrayOf<Any>("430201200121", listOf("P0120", "P0121")),
                // Empty data
                arrayOf<Any>("4300", listOf<String>()),
            )
    }

    @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<String>,
) {
    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun values() =
            listOf(
                // Two frames with four dtc
                arrayOf<Any>("4700035104A1AB\r47F10600000000", listOf("P0003", "C1104", "B21AB", "U3106")),
                // One frame with three dtc
                arrayOf<Any>("47010301040105", listOf("P0103", "P0104", "P0105")),
                // One frame with two dtc
                arrayOf<Any>("47010301040000", listOf("P0103", "P0104")),
                // Two frames with four dtc CAN (ISO-15765) format
                arrayOf<Any>("00A\r0:470401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")),
                // One frame with two dtc CAN (ISO-15765) format
                arrayOf<Any>("470201200121", listOf("P0120", "P0121")),
                // Empty data
                arrayOf<Any>("4700", listOf<String>()),
            )
    }

    @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<String>,
) {
    companion object {
        @JvmStatic
        @Parameterized.Parameters
        fun values() =
            listOf(
                // Two frames with four dtc
                arrayOf<Any>("4A00035104A1AB\r4AF10600000000", listOf("P0003", "C1104", "B21AB", "U3106")),
                // One frame with three dtc
                arrayOf<Any>("4A010301040105", listOf("P0103", "P0104", "P0105")),
                // One frame with two dtc
                arrayOf<Any>("4A010301040000", listOf("P0103", "P0104")),
                // Two frames with four dtc CAN (ISO-15765) format
                arrayOf<Any>("00A\r0:4A0401080118\r1:011901200000", listOf("P0108", "P0118", "P0119", "P0120")),
                // One frame with two dtc CAN (ISO-15765) format
                arrayOf<Any>("4A0201200121", listOf("P0120", "P0121")),
                // Empty data
                arrayOf<Any>("4A00", listOf<String>()),
            )
    }

    @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<Any>("414545", 27.1f),
                arrayOf<Any>("414500", 0f),
                arrayOf<Any>("4145FF", 100f),
                arrayOf<Any>("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<Any>("410610", -87.5f),
                arrayOf<Any>("410643", -47.7f),
                arrayOf<Any>("410680", 0f),
                arrayOf<Any>("4106C8", 56.25f),
                arrayOf<Any>("410600", -100f),
                arrayOf<Any>("4106FF", 99.2f),
                arrayOf<Any>("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<Any>("410D15", 21),
                arrayOf<Any>("410D40", 64),
                arrayOf<Any>("410D00", 0),
                arrayOf<Any>("410DFF", 255),
                arrayOf<Any>("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<Any>("410C200D", 2051),
                arrayOf<Any>("410C10A4", 1065),
                arrayOf<Any>("41 0C 10 A4", 1065),
                arrayOf<Any>("41 0c 10 a4", 1065),
                arrayOf<Any>("410C10A4410C10C1", 1065),
                arrayOf<Any>("41 0C 10 A4 41 0C 10 C1", 1065),
                arrayOf<Any>("410C0E02410C0E02", 896),
                arrayOf<Any>("41 0C 0E 02  41 0C 0E 02", 896),
                arrayOf<Any>("410C283C", 2575),
                arrayOf<Any>("410C0A00", 640),
                arrayOf<Any>("410C0000", 0),
                arrayOf<Any>("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<Any>("41109511", 381.61f),
                arrayOf<Any>("41101234", 46.6f),
                arrayOf<Any>("41100000", 0f),
                arrayOf<Any>("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<Any>("411F4543", "04:55:31"),
                arrayOf<Any>("411F1234", "01:17:40"),
                arrayOf<Any>("411F0000", "00:00:00"),
                arrayOf<Any>("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<Any>("410410", 6.3f),
                arrayOf<Any>("410400", 0f),
                arrayOf<Any>("4104FF", 100f),
                arrayOf<Any>("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<Any>("41434143", 6551.8f),
                arrayOf<Any>("41431234", 1827.5f),
                arrayOf<Any>("41430000", 0f),
                arrayOf<Any>("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<Any>("411111", 6.7f),
                arrayOf<Any>("411100", 0f),
                arrayOf<Any>("4111FF", 100f),
                arrayOf<Any>("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<Any>("414545", 27.1f),
                arrayOf<Any>("414500", 0f),
                arrayOf<Any>("4145FF", 100f),
                arrayOf<Any>("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<Any>("415E10E3", 216.2f),
                arrayOf<Any>("415E1234", 233f),
                arrayOf<Any>("415E0000", 0f),
                arrayOf<Any>("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<Any>("415100", "Not Available"),
                arrayOf<Any>("415101", "Gasoline"),
                arrayOf<Any>("415102", "Methanol"),
                arrayOf<Any>("415103", "Ethanol"),
                arrayOf<Any>("415104", "Diesel"),
                arrayOf<Any>("415105", "GPL/LGP"),
                arrayOf<Any>("415106", "Natural Gas"),
                arrayOf<Any>("415107", "Propane"),
                arrayOf<Any>("415108", "Electric"),
                arrayOf<Any>("415109", "Biodiesel + Gasoline"),
                arrayOf<Any>("41510A", "Biodiesel + Methanol"),
                arrayOf<Any>("41510B", "Biodiesel + Ethanol"),
                arrayOf<Any>("41510C", "Biodiesel + GPL/LGP"),
                arrayOf<Any>("41510D", "Biodiesel + Natural Gas"),
                arrayOf<Any>("41510E", "Biodiesel + Propane"),
                arrayOf<Any>("41510F", "Biodiesel + Electric"),
                arrayOf<Any>("415110", "Biodiesel + Gasoline/Electric"),
                arrayOf<Any>("415111", "Hybrid Gasoline"),
                arrayOf<Any>("415112", "Hybrid Ethanol"),
                arrayOf<Any>("415113", "Hybrid Diesel"),
                arrayOf<Any>("415114", "Hybrid Electric"),
                arrayOf<Any>("415115", "Hybrid Mixed"),
                arrayOf<Any>("415116", "Hybrid Regenerative"),
                arrayOf<Any>("415116FF", "Hybrid Regenerative"),
                arrayOf<Any>("4151FF", "Unknown"),
                arrayOf<Any>("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<Any>("412F10", 6.3f),
                arrayOf<Any>("412FC8", 78.4f),
                arrayOf<Any>("412F00", 0f),
                arrayOf<Any>("412FFF", 100f),
                arrayOf<Any>("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<Any>("410610", -87.5f),
                arrayOf<Any>("410643", -47.7f),
                arrayOf<Any>("410680", 0f),
                arrayOf<Any>("4106C8", 56.25f),
                arrayOf<Any>("410600", -100f),
                arrayOf<Any>("4106FF", 99.2f),
                arrayOf<Any>("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<Any>("41441234", 0.14f),
                arrayOf<Any>("41444040", 0.5f),
                arrayOf<Any>("41448080", 1f),
                arrayOf<Any>("41440000", 0f),
                arrayOf<Any>("4144FFFF", 2f),
                arrayOf<Any>("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<Any>("41341234", 0.14f),
                arrayOf<Any>("41344040", 0.5f),
                arrayOf<Any>("41348080", 1f),
                arrayOf<Any>("41340000", 0f),
                arrayOf<Any>("4134FFFF", 2f),
                arrayOf<Any>("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<Any>("413312", 18),
                arrayOf<Any>("413340", 64),
                arrayOf<Any>("413364", 100),
                arrayOf<Any>("413380", 128),
                arrayOf<Any>("413300", 0),
                arrayOf<Any>("4133FF", 255),
                arrayOf<Any>("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<Any>("410B12", 18),
                arrayOf<Any>("410B39", 57),
                arrayOf<Any>("410B40", 64),
                arrayOf<Any>("410B64", 100),
                arrayOf<Any>("410B80", 128),
                arrayOf<Any>("410B00", 0),
                arrayOf<Any>("410BFF", 255),
                arrayOf<Any>("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<Any>("410A12", 54),
                arrayOf<Any>("410A40", 192),
                arrayOf<Any>("410A64", 300),
                arrayOf<Any>("410A80", 384),
                arrayOf<Any>("410A00", 0),
                arrayOf<Any>("410AFF", 765),
                arrayOf<Any>("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<Any>("41230000", 0.000f),
                arrayOf<Any>("410B39", 4.503f),
                arrayOf<Any>("410B6464", 2030.300f),
                arrayOf<Any>("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<Any>("41231234", 46_600),
                arrayOf<Any>("41234354", 172_360),
                arrayOf<Any>("412360ED", 248_130),
                arrayOf<Any>("41238080", 328_960),
                arrayOf<Any>("41230000", 0),
                arrayOf<Any>("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<Any>("410F40", 24f),
                arrayOf<Any>("410F5D", 53f),
                arrayOf<Any>("410F00", -40f),
                arrayOf<Any>("410FFF", 215f),
                arrayOf<Any>("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<Any>("414640", 24f),
                arrayOf<Any>("41465D", 53f),
                arrayOf<Any>("414600", -40f),
                arrayOf<Any>("4146FF", 215f),
                arrayOf<Any>("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<Any>("410540", 24f),
                arrayOf<Any>("41055D", 53f),
                arrayOf<Any>("410500", -40f),
                arrayOf<Any>("4105FF", 215f),
                arrayOf<Any>("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<Any>("415C40", 24f),
                arrayOf<Any>("415C5D", 53f),
                arrayOf<Any>("415C00", -40f),
                arrayOf<Any>("415CFF", 215f),
                arrayOf<Any>("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<Unit>()
            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<CancellationException> { 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<Int>()

    @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<String, ResponsePlan>,
) : OutputStream() {
    val writes = mutableListOf<String>()

    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
}
Download .txt
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
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (176K chars).
[
  {
    "path": ".editorconfig",
    "chars": 385,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\nindent_size = 4\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 427,
    "preview": "name: CI\n\non:\n  pull_request:\n    branches:\n      - master\n  push:\n    branches:\n      - master\n\njobs:\n  verify:\n    run"
  },
  {
    "path": ".gitignore",
    "chars": 2891,
    "preview": "# Created by https://www.gitignore.io/api/macos,kotlin,gradle,intellij+iml\n# Edit at https://www.gitignore.io/?templates"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "chars": 381,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <JetCodeStyleSettings>"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "chars": 142,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
  },
  {
    "path": ".idea/encodings.xml",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"Encoding\" addBOMForNewFiles=\"with NO BOM"
  },
  {
    "path": ".idea/kotlinScripting.xml",
    "chars": 246,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"KotlinScriptingSettings\">\n    <option na"
  },
  {
    "path": ".idea/vcs.xml",
    "chars": 180,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping dire"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "LLM_CONTEXT.md",
    "chars": 3630,
    "preview": "# LLM Context for `kotlin-obd-api`\n\nThis document helps coding assistants correctly choose and apply this library for OB"
  },
  {
    "path": "README.md",
    "chars": 7801,
    "preview": "<p align=\"center\">\n  <img width=\"300px\" src=\"img/kotlin-obd-api-logo.png\" />\n</p>\n\n<h1 align=\"center\">Kotlin OBD API</h1"
  },
  {
    "path": "SUPPORTED_COMMANDS.md",
    "chars": 4534,
    "preview": "# Supported Commands\n\nFull list of supported commands.\n\n## `AT` Commands (ELM327)\n| Command | Name | Description |\n| :- "
  },
  {
    "path": "build.gradle.kts",
    "chars": 3002,
    "preview": "import org.gradle.api.GradleException\nimport org.gradle.api.tasks.testing.Test\nimport org.gradle.api.tasks.testing.TestD"
  },
  {
    "path": "context7.json",
    "chars": 107,
    "preview": "{\n    \"url\": \"https://context7.com/eltonvs/kotlin-obd-api\",\n    \"public_key\": \"pk_Zyw2G86BTmzAjNaaoVjyB\"\n}\n"
  },
  {
    "path": "detekt.yml",
    "chars": 90,
    "preview": "# Project detekt overrides.\n# Intentionally minimal to keep default detekt rules enabled.\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 202,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "gradle.properties",
    "chars": 26,
    "preview": "kotlin.code.style=official"
  },
  {
    "path": "gradlew",
    "chars": 8047,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "gradlew.bat",
    "chars": 2674,
    "preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "jitpack.yml",
    "chars": 19,
    "preview": "jdk:\n  - openjdk17\n"
  },
  {
    "path": "llms.txt",
    "chars": 1165,
    "preview": "# kotlin-obd-api\n\nKotlin OBD-II (ELM327) library for Kotlin/JVM applications.\n\n## Canonical\n- Repository: https://github"
  },
  {
    "path": "settings.gradle.kts",
    "chars": 36,
    "preview": "rootProject.name = \"kotlin-obd-api\"\n"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/ATCommand.kt",
    "chars": 152,
    "preview": "package com.github.eltonvs.obd.command\n\nabstract class ATCommand : ObdCommand() {\n    override val mode = \"AT\"\n    overr"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/Enums.kt",
    "chars": 2827,
    "preview": "package com.github.eltonvs.obd.command\n\nprivate const val BIT_POS_0 = 0\nprivate const val BIT_POS_1 = 1\nprivate const va"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/Exceptions.kt",
    "chars": 3832,
    "preview": "package com.github.eltonvs.obd.command\n\nimport com.github.eltonvs.obd.command.RegexPatterns.BUSINIT_ERROR_MESSAGE_PATTER"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/ObdCommand.kt",
    "chars": 865,
    "preview": "package com.github.eltonvs.obd.command\n\nabstract class ObdCommand {\n    abstract val tag: String\n    abstract val name: "
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/ParserFunctions.kt",
    "chars": 1005,
    "preview": "package com.github.eltonvs.obd.command\n\nimport kotlin.math.pow\n\nprivate const val BYTE_BITS = 8\nprivate const val PERCEN"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/RegexPatterns.kt",
    "chars": 1344,
    "preview": "package com.github.eltonvs.obd.command\n\nimport java.util.regex.Pattern\n\nobject RegexPatterns {\n    val WHITESPACE_PATTER"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/Response.kt",
    "chars": 1918,
    "preview": "package com.github.eltonvs.obd.command\n\nimport com.github.eltonvs.obd.command.RegexPatterns.BUS_INIT_PATTERN\nimport com."
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/at/Actions.kt",
    "chars": 1211,
    "preview": "package com.github.eltonvs.obd.command.at\n\nimport com.github.eltonvs.obd.command.ATCommand\n\nclass ResetAdapterCommand : "
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/at/Info.kt",
    "chars": 1372,
    "preview": "package com.github.eltonvs.obd.command.at\n\nimport com.github.eltonvs.obd.command.ATCommand\nimport com.github.eltonvs.obd"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/at/Mutations.kt",
    "chars": 1973,
    "preview": "package com.github.eltonvs.obd.command.at\n\nimport com.github.eltonvs.obd.command.ATCommand\nimport com.github.eltonvs.obd"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/control/AvailablePIDsCommand.kt",
    "chars": 1386,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.elton"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/control/Control.kt",
    "chars": 2979,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.elton"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/control/MIL.kt",
    "chars": 1546,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.elton"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/control/Monitor.kt",
    "chars": 3530,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.Monitors\nimport com.github.eltonvs"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/control/TroubleCodes.kt",
    "chars": 5647,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.elton"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/egr/Egr.kt",
    "chars": 1236,
    "preview": "package com.github.eltonvs.obd.command.egr\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.eltonvs.o"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/engine/Engine.kt",
    "chars": 3762,
    "preview": "package com.github.eltonvs.obd.command.engine\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.eltonv"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/fuel/Fuel.kt",
    "chars": 5559,
    "preview": "package com.github.eltonvs.obd.command.fuel\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.eltonvs."
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/fuel/Ratio.kt",
    "chars": 1857,
    "preview": "package com.github.eltonvs.obd.command.fuel\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.eltonvs."
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/pressure/Pressure.kt",
    "chars": 2364,
    "preview": "package com.github.eltonvs.obd.command.pressure\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.elto"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/command/temperature/Temperature.kt",
    "chars": 1900,
    "preview": "package com.github.eltonvs.obd.command.temperature\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.e"
  },
  {
    "path": "src/main/kotlin/com/github/eltonvs/obd/connection/ObdDeviceConnection.kt",
    "chars": 4169,
    "preview": "package com.github.eltonvs.obd.connection\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.eltonvs.ob"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/BytesToIntParameterizedTests.kt",
    "chars": 1478,
    "preview": "package com.github.eltonvs.obd.command\n\nimport org.junit.runner.RunWith\nimport org.junit.runners.Parameterized\nimport ko"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/control/AvailableCommands.kt",
    "chars": 9068,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.ru"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/control/Control.kt",
    "chars": 3439,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.ru"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/control/MIL.kt",
    "chars": 2789,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.ru"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/control/Monitor.kt",
    "chars": 6030,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.Monitors\nimport com.github.eltonvs"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/control/TroubleCodes.kt",
    "chars": 8345,
    "preview": "package com.github.eltonvs.obd.command.control\n\nimport com.github.eltonvs.obd.command.NoDataException\nimport com.github."
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/egr/Egr.kt",
    "chars": 2014,
    "preview": "package com.github.eltonvs.obd.command.egr\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.runner"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/engine/Engine.kt",
    "chars": 7292,
    "preview": "package com.github.eltonvs.obd.command.engine\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.run"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/fuel/Fuel.kt",
    "chars": 6687,
    "preview": "package com.github.eltonvs.obd.command.fuel\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.runne"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/fuel/Ratio.kt",
    "chars": 2253,
    "preview": "package com.github.eltonvs.obd.command.fuel\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.runne"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/pressure/Pressure.kt",
    "chars": 4965,
    "preview": "package com.github.eltonvs.obd.command.pressure\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.junit.r"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/command/temperature/Temperature.kt",
    "chars": 3811,
    "preview": "package com.github.eltonvs.obd.command.temperature\n\nimport com.github.eltonvs.obd.command.ObdRawResponse\nimport org.juni"
  },
  {
    "path": "src/test/kotlin/com/github/eltonvs/obd/connection/ObdDeviceConnectionTest.kt",
    "chars": 7008,
    "preview": "package com.github.eltonvs.obd.connection\n\nimport com.github.eltonvs.obd.command.ObdCommand\nimport com.github.eltonvs.ob"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the eltonvs/kotlin-obd-api GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (161.2 KB), approximately 42.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!