├── .github └── workflows │ ├── android.yml │ └── mavenCentral.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── obd ├── .gitignore ├── build.gradle.kts ├── proguard-rules.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── dtc-codes.json │ ├── pids-mode1.json │ ├── pids-mode4.json │ └── pids-mode9.json │ ├── java │ └── com │ │ └── pnuema │ │ └── android │ │ └── obd │ │ ├── ObdInitializer.kt │ │ ├── commands │ │ ├── BaseObdCommand.kt │ │ └── OBDCommand.kt │ │ ├── enums │ │ ├── ObdModes.kt │ │ └── ObdProtocols.kt │ │ ├── models │ │ ├── DTC.kt │ │ ├── DTCS.kt │ │ ├── PID.kt │ │ └── PIDS.kt │ │ └── statics │ │ ├── DTCUtils.kt │ │ ├── FileUtils.kt │ │ ├── ObdLibrary.kt │ │ ├── PIDUtils.kt │ │ ├── PersistentStorage.kt │ │ └── Translations.kt │ └── res │ └── values │ └── strings.xml └── settings.gradle.kts /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | java: [ '17' ] 16 | 17 | environment: build 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: set up JDK ${{ matrix.java }} 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: 'zulu' 24 | java-version: ${{ matrix.java }} 25 | check-latest: true 26 | - name: Build with Gradle 27 | run: ./gradlew clean build 28 | env: 29 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVENCENTRALUSERNAME }} 30 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVENCENTRALPASSWORD }} 31 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 32 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 33 | REMOTE_CACHE_URL: ${{ secrets.REMOTE_CACHE_URL }} 34 | REMOTE_CACHE_USER: ${{ secrets.REMOTE_CACHE_USER }} 35 | REMOTE_CACHE_PASS: ${{ secrets.REMOTE_CACHE_PASS }} 36 | - uses: actions/upload-artifact@v4 37 | with: 38 | name: Package 39 | path: obd/build/outputs/aar 40 | -------------------------------------------------------------------------------- /.github/workflows/mavenCentral.yml: -------------------------------------------------------------------------------- 1 | name: MavenCentral Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | mavencentral: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | java: [ '17' ] 12 | 13 | environment: build 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK ${{ matrix.java }} 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: 'zulu' 20 | java-version: ${{ matrix.java }} 21 | check-latest: true 22 | - name: Build and Publish to the Maven Central Repository 23 | run: ./gradlew publishAndReleaseToMavenCentral --no-daemon --no-parallel 24 | env: 25 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVENCENTRALUSERNAME }} 26 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVENCENTRALPASSWORD }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 28 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 29 | REMOTE_CACHE_URL: ${{ secrets.REMOTE_CACHE_URL }} 30 | REMOTE_CACHE_USER: ${{ secrets.REMOTE_CACHE_USER }} 31 | REMOTE_CACHE_PASS: ${{ secrets.REMOTE_CACHE_PASS }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | /local.properties 3 | build/ 4 | javadoc/ 5 | .DS_Store/ 6 | .idea/ 7 | *.iml 8 | *.properties 9 | *.jar 10 | *.war 11 | *.aar 12 | *.apk 13 | *.class 14 | *.xml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brad Barnhill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![AndroidOBD CI](https://github.com/barnhill/AndroidOBD/workflows/Android%20CI/badge.svg) [![API](https://img.shields.io/badge/API-24%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=24) 2 | 3 | ## Android OBD Library 4 | 5 | 6 | ### What is this repository for? ### 7 | 8 | This project offers a developer friendly interface to communicate with ELM 327 OBD devices via BLUETOOTH. 9 | 10 | ### Usage ### 11 | 12 | Add Dependency: 13 | ```Gradle 14 | implementation 'com.pnuema.android:obd:1.7.1' 15 | ``` 16 | 17 | To get started you will need to first send a few commands over bluetooth or usb whatever the input stream is that you negotiate with the ELM-327 device. 18 | 19 | Connection and init: 20 | ``` 21 | val MODE_AT = "AT" 22 | 23 | //set defaults 24 | initPid.mode = MODE_AT 25 | initPid.PID = "D" 26 | var cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 27 | Log.d(TAG, "Set defaults sent (" + initPid.mode + " " + initPid.PID + ") Received: " + cmd.rawResult) 28 | 29 | //resets the ELM327 30 | initPid.mode = MODE_AT 31 | initPid.PID = "Z" 32 | cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 33 | Log.d(TAG, "Reset command sent (" + initPid.mode + " " + initPid.PID + ") Received: " + cmd.rawResult) 34 | 35 | //extended responses off 36 | initPid.mode = MODE_AT 37 | initPid.PID = "E0" 38 | cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 39 | Log.d(TAG, "Extended Responses Off (" + initPid.mode + " " + initPid.PID + ") Received: " + cmd.rawResult) 40 | 41 | //line feeds off 42 | initPid.mode = MODE_AT 43 | initPid.PID = "L0" 44 | cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 45 | Log.d(TAG, "Turn Off Line Feeds (" + initPid.mode + " " + initPid.PID + ") Received: " + cmd.rawResult) 46 | 47 | //printing of spaces off 48 | initPid.mode = MODE_AT 49 | initPid.PID = "S0" 50 | cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 51 | Log.d(TAG, "Printing Spaces Off (" + initPid.mode + " " + initPid.PID + ") Received: " + cmd.rawResult) 52 | 53 | //headers off 54 | initPid.mode = MODE_AT 55 | initPid.PID = "H0" 56 | cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 57 | Log.d(TAG, "Headers Off (" + initPid.mode + " " + initPid.PID + ") Received: " + cmd.rawResult) 58 | 59 | //set protocol 60 | initPid.mode = "$MODE_AT SP" 61 | initPid.PID = ObdProtocols.AUTO.value.toString() 62 | cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 63 | Log.d(TAG, "Select Protocol (" + initPid.mode + " " + initPid.PID + ") Received: " + cmd.rawResult) 64 | 65 | //set timeout for response from the ECU 66 | initPid.mode = "$MODE_AT ST" 67 | initPid.PID = Integer.toHexString(0xFF and ECU_RESPONSE_TIMEOUT) 68 | cmd = OBDCommand(initPid).setIgnoreResult(true).run(inputStream, outputStream) 69 | ``` 70 | 71 | Once a connection has been established and inited you can send commands and get responses as follows: 72 | 73 | Code: 74 | ``` 75 | //Request MODE 1, PID 0C - RPM 76 | val pid = PIDUtils.getPid(ObdModes.MODE_01, "OC") 77 | val command = OBDCommand(pid) 78 | command.run(bluetoothSocket.inputStream, bluetoothSocket.outputStream) 79 | 80 | Log.d("PID", "${pid.description} : ${pid.calculatedResult}") 81 | Log.d("PID Formatted Result", command.formattedResult) 82 | ``` 83 | 84 | ``` 85 | //Clear DTCs - NonPermanent 86 | val pid = PID(ObdModes.MODE_04) //Clear DTCs 87 | val command = OBDCommand(pid) 88 | command.run(bluetoothSocket.inputStream, bluetoothSocket.outputStream) 89 | ``` 90 | 91 | Note that you do not have to take the raw values and calculate it yourself. The library will run the value through the formula that are specified in the specifications for CAN to get the resulting value. This is available in the `calculatedResult` and `formattedResult` fields on the pid after the `pid.run(...)` command finishes. 92 | 93 | ### Who do I talk to? ### 94 | 95 | * Brad Barnhill (https://github.com/barnhill) 96 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | mavenLocal() 6 | } 7 | } 8 | 9 | plugins { 10 | alias(libs.plugins.androidLibrary).apply(false) 11 | alias(libs.plugins.kotlin.android).apply(false) 12 | alias(libs.plugins.maven.publish).apply(false) 13 | alias(libs.plugins.gradle.cachefix).apply(false) 14 | } 15 | 16 | tasks { 17 | wrapper { 18 | gradleVersion = "8.13" 19 | distributionType = Wrapper.DistributionType.BIN 20 | } 21 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | org.gradle.jvmargs=-Xmx1548M -Dkotlin.daemon.jvm.options\="-Xmx2548M" 21 | org.gradle.configureondemand=false 22 | org.gradle.caching=true 23 | android.useAndroidX=true 24 | android.enableJetifier=false 25 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 26 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 27 | 28 | GROUP=com.pnuema.android 29 | VERSION_NAME=1.7.1 30 | 31 | POM_NAME=Android OBD Library 32 | POM_ARTIFACT_ID=obd 33 | POM_PACKAGING=aar 34 | 35 | POM_DESCRIPTION=Android library to communicate with ELM327 based OBD devices 36 | POM_INCEPTION_YEAR=2016 37 | POM_URL=https://github.com/barnhill/AndroidOBD 38 | POM_SCM_URL=https://github.com/barnhill/AndroidOBD 39 | POM_SCM_CONNECTION=scm:git@github.com/barnhill/AndroidOBD.git 40 | POM_SCM_DEV_CONNECTION=scm:git@github.com/barnhill/AndroidOBD.git 41 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 42 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 43 | POM_LICENCE_DIST=repo 44 | POM_DEVELOPER_ID=barnhill 45 | POM_DEVELOPER_NAME=Brad Barnhill 46 | POM_DEVELOPER_URL=https://github.com/barnhill/ 47 | 48 | SONATYPE_HOST=CENTRAL_PORTAL 49 | RELEASE_SIGNING_ENABLED=true 50 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | #libs 3 | kotlinx-serialization = "1.7.1" 4 | vanniktech-maven-publish = "0.29.0" 5 | evalex = "3.4.0" 6 | androidx-startup = "1.2.0" 7 | 8 | #plugins 9 | kotlin= "2.1.20" 10 | gradlePlugins-agp = "8.7.3" 11 | tomlChecker = "0.51.0" 12 | gradleCacheFix = "3.0.1" 13 | dokka = "2.0.0" 14 | 15 | [libraries] 16 | dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 17 | kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 18 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 19 | evalex = { module = "com.ezylang:EvalEx", version.ref = "evalex"} 20 | androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } 21 | 22 | [plugins] 23 | androidLibrary = { id = "com.android.library", version.ref = "gradlePlugins-agp" } 24 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 25 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 26 | maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-maven-publish" } 27 | gradle-cachefix = { id = "org.gradle.android.cache-fix", version.ref = "gradleCacheFix" } 28 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 29 | toml-version-checker = { id = "com.github.ben-manes.versions", version.ref = "tomlChecker" } 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barnhill/AndroidOBD/4827f560d10d576a9732a312d065938815683dc0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /obd/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /obd/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidLibrary) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.maven.publish) 5 | alias(libs.plugins.kotlin.serialization) 6 | alias(libs.plugins.dokka) 7 | alias(libs.plugins.toml.version.checker) 8 | } 9 | 10 | version = project.properties["VERSION_NAME"] as String 11 | group = project.properties["GROUP"] as String 12 | 13 | android { 14 | base.archivesName.set("obd") 15 | namespace = "com.pnuema.android.obd" 16 | compileSdk = 35 17 | defaultConfig { 18 | minSdk = 24 19 | } 20 | 21 | buildTypes { 22 | named("release") { 23 | isMinifyEnabled = false 24 | isShrinkResources = false 25 | setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")) 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_17 30 | targetCompatibility = JavaVersion.VERSION_17 31 | } 32 | 33 | kotlinOptions { 34 | jvmTarget = JavaVersion.VERSION_17.toString() 35 | } 36 | 37 | kotlin { 38 | jvmToolchain(JavaVersion.VERSION_17.toString().toInt()) 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation(libs.evalex) 44 | implementation(libs.kotlinx.serialization.json) 45 | implementation(libs.androidx.startup) 46 | } 47 | 48 | val dokkaOutputDir = layout.buildDirectory.dir("dokka") 49 | tasks { 50 | val sourcesJar by registering(Jar::class, fun Jar.() { 51 | archiveClassifier.set("sources") 52 | from(android.sourceSets.getByName("main").java.srcDirs) 53 | }) 54 | 55 | val javadocJar by registering(Jar::class, fun Jar.() { 56 | dependsOn.add(dokkaGenerate) 57 | archiveClassifier.set("javadoc") 58 | from(android.sourceSets.getByName("main").java.srcDirs) 59 | from(dokkaOutputDir) 60 | }) 61 | 62 | artifacts { 63 | archives(sourcesJar) 64 | archives(javadocJar) 65 | } 66 | 67 | dokka { 68 | moduleName.set(project.properties["POM_NAME"] as String) 69 | dokkaPublications.html { 70 | suppressInheritedMembers.set(true) 71 | failOnWarning.set(true) 72 | outputDirectory.set(dokkaOutputDir) 73 | } 74 | dokkaSourceSets.main { 75 | sourceLink { 76 | localDirectory.set(file("src/main/java")) 77 | remoteUrl(project.properties["POM_URL"] as String) 78 | } 79 | } 80 | pluginsConfiguration.html { 81 | footerMessage.set("(c) " + project.properties["POM_DEVELOPER_NAME"]) 82 | } 83 | } 84 | 85 | build { 86 | dependsOn(dokkaGenerate) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /obd/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Users/bbarnhill/AppData/Local/Android/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # ----- KotlinX Serialization ----- 20 | -keepattributes *Annotation*, InnerClasses 21 | -dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations 22 | 23 | # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer 24 | -keepclassmembers class kotlinx.serialization.json.** { 25 | *** Companion; 26 | } 27 | -keepclasseswithmembers class kotlinx.serialization.json.** { 28 | kotlinx.serialization.KSerializer serializer(...); 29 | } 30 | 31 | -keep,includedescriptorclasses class com.pnuema.android.obd.**$$serializer { *; } 32 | -keepclassmembers class com.pnuema.android.obd.** { 33 | *** Companion; 34 | } 35 | -keepclasseswithmembers class com.pnuema.android.obd.** { 36 | kotlinx.serialization.KSerializer serializer(...); 37 | } 38 | # ----- KotlinX Serialization ----- -------------------------------------------------------------------------------- /obd/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /obd/src/main/assets/pids-mode1.json: -------------------------------------------------------------------------------- 1 | { 2 | "pids": [ 3 | { 4 | "Mode": "01", 5 | "PID": "00", 6 | "Bytes": "4", 7 | "Description": "PIDs supported [01 - 20]", 8 | "Min": "0", 9 | "Max": "9999999999999", 10 | "Units": "", 11 | "Formula": "A", 12 | "isPersistent": true 13 | }, 14 | { 15 | "Mode": "01", 16 | "PID": "20", 17 | "Bytes": "4", 18 | "Description": "PIDs supported [21 - 40]", 19 | "Min": "0", 20 | "Max": "9999999999999", 21 | "Units": "", 22 | "Formula": "A", 23 | "isPersistent": true 24 | }, 25 | { 26 | "Mode": "01", 27 | "PID": "40", 28 | "Bytes": "4", 29 | "Description": "PIDs supported [41 - 60]", 30 | "Min": "0", 31 | "Max": "9999999999999", 32 | "Units": "", 33 | "Formula": "A", 34 | "isPersistent": true 35 | }, 36 | { 37 | "Mode": "01", 38 | "PID": "60", 39 | "Bytes": "4", 40 | "Description": "PIDs supported [61 - 80]", 41 | "Min": "0", 42 | "Max": "9999999999999", 43 | "Units": "", 44 | "Formula": "A", 45 | "isPersistent": true 46 | }, 47 | { 48 | "Mode": "01", 49 | "PID": "80", 50 | "Bytes": "4", 51 | "Description": "PIDs supported [81 - A0]", 52 | "Min": "0", 53 | "Max": "9999999999999", 54 | "Units": "", 55 | "Formula": "A", 56 | "isPersistent": true 57 | }, 58 | { 59 | "Mode": "01", 60 | "PID": "01", 61 | "Bytes": "4", 62 | "Description": "MIL On", 63 | "Min": "0", 64 | "Max": "1", 65 | "Units": "", 66 | "Formula": "(A/128)>=1" 67 | }, 68 | { 69 | "Mode": "01", 70 | "PID": "04", 71 | "Bytes": "1", 72 | "Description": "Calculated engine load value", 73 | "Min": "0", 74 | "Max": "100", 75 | "Units": "%", 76 | "Formula": "A*100/255" 77 | }, 78 | { 79 | "Mode": "01", 80 | "PID": "05", 81 | "Bytes": "1", 82 | "Description": "Engine coolant temperature", 83 | "Min": "-40", 84 | "Max": "215", 85 | "Units": "°C", 86 | "Formula": "A-40", 87 | "ImperialFormula": "((A-40) * 1.8) + 32", 88 | "ImperialUnits": "°F" 89 | }, 90 | { 91 | "Mode": "01", 92 | "PID": "0A", 93 | "Bytes": "1", 94 | "Description": "Fuel pressure", 95 | "Min": "0", 96 | "Max": "765", 97 | "Units": "kPa (gauge)", 98 | "Formula": "A*3", 99 | "ImperialFormula": "(A*3)*0.145037738", 100 | "ImperialUnits": "psi (gauge)" 101 | }, 102 | { 103 | "Mode": "01", 104 | "PID": "0B", 105 | "Bytes": "1", 106 | "Description": "Intake manifold absolute pressure", 107 | "Min": "0", 108 | "Max": "255", 109 | "Units": "kPa (absolute)", 110 | "Formula": "A", 111 | "ImperialFormula": "A*0.145037738", 112 | "ImperialUnits": "psi (absolute)" 113 | }, 114 | { 115 | "Mode": "01", 116 | "PID": "0C", 117 | "Bytes": "2", 118 | "Description": "Engine RPM", 119 | "Min": "0", 120 | "Max": "16383.75", 121 | "Units": "rpm", 122 | "Formula": "((A*256)+B)/4" 123 | }, 124 | { 125 | "Mode": "01", 126 | "PID": "0D", 127 | "Bytes": "1", 128 | "Description": "Vehicle speed", 129 | "Min": "0", 130 | "Max": "255", 131 | "Units": "km/h", 132 | "Formula": "A", 133 | "ImperialFormula": "A*0.62137", 134 | "ImperialUnits": "mph" 135 | }, 136 | { 137 | "Mode": "01", 138 | "PID": "0E", 139 | "Bytes": "1", 140 | "Description": "Timing advance", 141 | "Min": "-64", 142 | "Max": "63.5", 143 | "Units": "° relative to #1 cylinder", 144 | "Formula": "A/2 - 64" 145 | }, 146 | { 147 | "Mode": "01", 148 | "PID": "0F", 149 | "Bytes": "1", 150 | "Description": "Intake air temperature", 151 | "Min": "-40", 152 | "Max": "215", 153 | "Units": "°C", 154 | "Formula": "A-40", 155 | "ImperialFormula": "((A-40) * 1.8) + 32", 156 | "ImperialUnits": "°F" 157 | }, 158 | { 159 | "Mode": "01", 160 | "PID": "10", 161 | "Bytes": "2", 162 | "Description": "MAF air flow rate", 163 | "Min": "0", 164 | "Max": "655.35", 165 | "Units": "grams/sec", 166 | "Formula": "((A*256)+B) / 100" 167 | }, 168 | { 169 | "Mode": "01", 170 | "PID": "11", 171 | "Bytes": "1", 172 | "Description": "Throttle position", 173 | "Min": "0", 174 | "Max": "100", 175 | "Units": "%", 176 | "Formula": "A*100/255" 177 | }, 178 | { 179 | "Mode": "01", 180 | "PID": "1F", 181 | "Bytes": "2", 182 | "Description": "Run time since engine start", 183 | "Min": "0", 184 | "Max": "65.535", 185 | "Units": "seconds", 186 | "Formula": "(A*256)+B" 187 | }, 188 | { 189 | "Mode": "01", 190 | "PID": "21", 191 | "Bytes": "2", 192 | "Description": "Distance traveled with malfunction indicator lamp (MIL) on", 193 | "Min": "0", 194 | "Max": "65.535", 195 | "Units": "km", 196 | "Formula": "(A*256)+B", 197 | "ImperialFormula": "((A*256)+B)*0.62137", 198 | "ImperialUnits": "miles" 199 | }, 200 | { 201 | "Mode": "01", 202 | "PID": "22", 203 | "Bytes": "2", 204 | "Description": "Fuel Rail Pressure (relative to manifold vacuum)", 205 | "Min": "0", 206 | "Max": "5177.265", 207 | "Units": "kPa", 208 | "Formula": "((A*256)+B) * 0.079", 209 | "ImperialFormula": "(((A*256)+B) * 0.079)*0.145037738", 210 | "ImperialUnits": "psi" 211 | }, 212 | { 213 | "Mode": "01", 214 | "PID": "23", 215 | "Bytes": "2", 216 | "Description": "Fuel Rail Pressure (diesel. or gasoline direct inject)", 217 | "Min": "0", 218 | "Max": "655.350", 219 | "Units": "kPa (gauge)", 220 | "Formula": "((A*256)+B) * 10", 221 | "ImperialFormula": "(((A*256)+B) * 10)*0.145037738", 222 | "ImperialUnits": "psi" 223 | }, 224 | { 225 | "Mode": "01", 226 | "PID": "2C", 227 | "Bytes": "1", 228 | "Description": "Commanded EGR", 229 | "Min": "0", 230 | "Max": "100", 231 | "Units": "%", 232 | "Formula": "100*A/255" 233 | }, 234 | { 235 | "Mode": "01", 236 | "PID": "2D", 237 | "Bytes": "1", 238 | "Description": "EGR Error", 239 | "Min": "-100", 240 | "Max": "99.22", 241 | "Units": "%", 242 | "Formula": "(A-128) * 100/128" 243 | }, 244 | { 245 | "Mode": "01", 246 | "PID": "2E", 247 | "Bytes": "1", 248 | "Description": "Commanded evaporative purge", 249 | "Min": "0", 250 | "Max": "100", 251 | "Units": "%", 252 | "Formula": "100*A/255" 253 | }, 254 | { 255 | "Mode": "01", 256 | "PID": "2F", 257 | "Bytes": "1", 258 | "Description": "Fuel Level Input", 259 | "Min": "0", 260 | "Max": "100", 261 | "Units": "%", 262 | "Formula": "100*A/255" 263 | }, 264 | { 265 | "Mode": "01", 266 | "PID": "30", 267 | "Bytes": "1", 268 | "Description": "# of warm-ups since codes cleared", 269 | "Min": "0", 270 | "Max": "255", 271 | "Formula": "A" 272 | }, 273 | { 274 | "Mode": "01", 275 | "PID": "31", 276 | "Bytes": "2", 277 | "Description": "Distance traveled since codes cleared", 278 | "Min": "0", 279 | "Max": "65.535", 280 | "Units": "km", 281 | "Formula": "(A*256)+B", 282 | "ImperialFormula": "((A*256)+B)*0.62137", 283 | "ImperialUnits": "miles" 284 | }, 285 | { 286 | "Mode": "01", 287 | "PID": "32", 288 | "Bytes": "2", 289 | "Description": "Evap. System Vapor Pressure", 290 | "Min": "-8.192", 291 | "Max": "8.192", 292 | "Units": "Pa", 293 | "Formula": "((A*256)+B)/4" 294 | }, 295 | { 296 | "Mode": "01", 297 | "PID": "33", 298 | "Bytes": "1", 299 | "Description": "Barometric pressure", 300 | "Min": "0", 301 | "Max": "255", 302 | "Units": "kPa (Absolute)", 303 | "Formula": "A", 304 | "ImperialFormula": "A*0.145037738", 305 | "ImperialUnits": "psi (Absolute)" 306 | }, 307 | { 308 | "Mode": "01", 309 | "PID": "34", 310 | "Bytes": "4", 311 | "Description": "O2S1_WR_lambda(1):", 312 | "Min": "0", 313 | "Max": "1.999", 314 | "Formula": "((A*256)+B)/32.768" 315 | }, 316 | { 317 | "Mode": "01", 318 | "PID": "35", 319 | "Bytes": "4", 320 | "Description": "O2S2_WR_lambda", 321 | "Min": "0", 322 | "Max": "2", 323 | "Formula": "((A*256)+B)/32.768" 324 | }, 325 | { 326 | "Mode": "01", 327 | "PID": "36", 328 | "Bytes": "4", 329 | "Description": "O2S3_WR_lambda", 330 | "Min": "0", 331 | "Max": "2", 332 | "Formula": "((A*256)+B)/32768" 333 | }, 334 | { 335 | "Mode": "01", 336 | "PID": "37", 337 | "Bytes": "4", 338 | "Description": "O2S4_WR_lambda", 339 | "Min": "0", 340 | "Max": "2", 341 | "Formula": "((A*256)+B)/32.768" 342 | }, 343 | { 344 | "Mode": "01", 345 | "PID": "38", 346 | "Bytes": "4", 347 | "Description": "O2S5_WR_lambda", 348 | "Min": "0", 349 | "Max": "2", 350 | "Formula": "((A*256)+B)/32.768" 351 | }, 352 | { 353 | "Mode": "01", 354 | "PID": "39", 355 | "Bytes": "4", 356 | "Description": "O2S6_WR_lambda", 357 | "Min": "0", 358 | "Max": "2", 359 | "Formula": "((A*256)+B)/32.768" 360 | }, 361 | { 362 | "Mode": "01", 363 | "PID": "3A", 364 | "Bytes": "4", 365 | "Description": "O2S7_WR_lambda", 366 | "Min": "0", 367 | "Max": "2", 368 | "Formula": "((A*256)+B)/32.768" 369 | }, 370 | { 371 | "Mode": "01", 372 | "PID": "3B", 373 | "Bytes": "4", 374 | "Description": "O2S8_WR_lambda", 375 | "Min": "0", 376 | "Max": "2", 377 | "Formula": "((A*256)+B)/32.768" 378 | }, 379 | { 380 | "Mode": "01", 381 | "PID": "3C", 382 | "Bytes": "2", 383 | "Description": "Catalyst Temperature Bank 1 Sensor 1", 384 | "Min": "-40", 385 | "Max": "6.513.50", 386 | "Units": "°C", 387 | "Formula": "((A*256)+B)/10 - 40", 388 | "ImperialFormula": "((((A*256)+B)/10 - 40) * 1.8) + 32", 389 | "ImperialUnits": "°F" 390 | }, 391 | { 392 | "Mode": "01", 393 | "PID": "3D", 394 | "Bytes": "2", 395 | "Description": "Catalyst Temperature Bank 2 Sensor 1", 396 | "Min": "-40", 397 | "Max": "6.513.50", 398 | "Units": "°C", 399 | "Formula": "((A*256)+B)/10 - 40", 400 | "ImperialFormula": "((((A*256)+B)/10 - 40) * 1.8) + 32", 401 | "ImperialUnits": "°F" 402 | }, 403 | { 404 | "Mode": "01", 405 | "PID": "3E", 406 | "Bytes": "2", 407 | "Description": "Catalyst Temperature Bank 1 Sensor 2", 408 | "Min": "-40", 409 | "Max": "6.513.50", 410 | "Units": "°C", 411 | "Formula": "((A*256)+B)/10 - 40", 412 | "ImperialFormula": "((((A*256)+B)/10 - 40) * 1.8) + 32", 413 | "ImperialUnits": "°F" 414 | }, 415 | { 416 | "Mode": "01", 417 | "PID": "3F", 418 | "Bytes": "2", 419 | "Description": "Catalyst Temperature Bank 2 Sensor 2", 420 | "Min": "-40", 421 | "Max": "6.513.50", 422 | "Units": "°C", 423 | "Formula": "((A*256)+B)/10 - 40", 424 | "ImperialFormula": "((((A*256)+B)/10 - 40) * 1.8) + 32", 425 | "ImperialUnits": "°F" 426 | }, 427 | { 428 | "Mode": "01", 429 | "PID": "42", 430 | "Bytes": "2", 431 | "Description": "Control module voltage", 432 | "Min": "0", 433 | "Max": "65.535", 434 | "Units": "V", 435 | "Formula": "((A*256)+B)/1000" 436 | }, 437 | { 438 | "Mode": "01", 439 | "PID": "43", 440 | "Bytes": "2", 441 | "Description": "Absolute load value", 442 | "Min": "0", 443 | "Max": "25.700", 444 | "Units": "°%", 445 | "Formula": "((A*256)+B)*100/255" 446 | }, 447 | { 448 | "Mode": "01", 449 | "PID": "44", 450 | "Bytes": "2", 451 | "Description": "Command equivalence ratio", 452 | "Min": "0", 453 | "Max": "2", 454 | "Formula": "((A*256)+B)/32768" 455 | }, 456 | { 457 | "Mode": "01", 458 | "PID": "45", 459 | "Bytes": "1", 460 | "Description": "Relative throttle position", 461 | "Min": "0", 462 | "Max": "100", 463 | "Units": "%", 464 | "Formula": "A*100/255" 465 | }, 466 | { 467 | "Mode": "01", 468 | "PID": "46", 469 | "Bytes": "1", 470 | "Description": "Ambient air temperature", 471 | "Min": "-40", 472 | "Max": "215", 473 | "Units": "°C", 474 | "Formula": "A-40", 475 | "ImperialFormula": "((A-40) * 1.8) + 32", 476 | "ImperialUnits": "°F" 477 | }, 478 | { 479 | "Mode": "01", 480 | "PID": "47", 481 | "Bytes": "1", 482 | "Description": "Absolute throttle position B", 483 | "Min": "0", 484 | "Max": "100", 485 | "Units": "%", 486 | "Formula": "A*100/255" 487 | }, 488 | { 489 | "Mode": "01", 490 | "PID": "48", 491 | "Bytes": "1", 492 | "Description": "Absolute throttle position C", 493 | "Min": "0", 494 | "Max": "100", 495 | "Units": "%", 496 | "Formula": "A*100/255" 497 | }, 498 | { 499 | "Mode": "01", 500 | "PID": "49", 501 | "Bytes": "1", 502 | "Description": "Accelerator pedal position D", 503 | "Min": "0", 504 | "Max": "100", 505 | "Units": "%", 506 | "Formula": "A*100/255" 507 | }, 508 | { 509 | "Mode": "01", 510 | "PID": "4A", 511 | "Bytes": "1", 512 | "Description": "Accelerator pedal position E", 513 | "Min": "0", 514 | "Max": "100", 515 | "Units": "%", 516 | "Formula": "A*100/255" 517 | }, 518 | { 519 | "Mode": "01", 520 | "PID": "4B", 521 | "Bytes": "1", 522 | "Description": "Accelerator pedal position F", 523 | "Min": "0", 524 | "Max": "100", 525 | "Units": "%", 526 | "Formula": "A*100/255" 527 | }, 528 | { 529 | "Mode": "01", 530 | "PID": "4C", 531 | "Bytes": "1", 532 | "Description": "Commanded throttle actuator", 533 | "Min": "0", 534 | "Max": "100", 535 | "Units": "%", 536 | "Formula": "A*100/255" 537 | }, 538 | { 539 | "Mode": "01", 540 | "PID": "4D", 541 | "Bytes": "2", 542 | "Description": "Time run with MIL on", 543 | "Min": "0", 544 | "Max": "65.535", 545 | "Units": "minutes", 546 | "Formula": "(A*256)+B" 547 | }, 548 | { 549 | "Mode": "01", 550 | "PID": "4E", 551 | "Bytes": "2", 552 | "Description": "Time since trouble codes cleared", 553 | "Min": "0", 554 | "Max": "65.535", 555 | "Units": "minutes", 556 | "Formula": "(A*256)+B" 557 | }, 558 | { 559 | "Mode": "01", 560 | "PID": "51", 561 | "Bytes": "1", 562 | "Description": "Fuel type", 563 | "Min": "", 564 | "Max": "", 565 | "Units": "", 566 | "Formula": "" 567 | }, 568 | { 569 | "Mode": "01", 570 | "PID": "52", 571 | "Bytes": "1", 572 | "Description": "Ethanol fuel %", 573 | "Min": "0", 574 | "Max": "100", 575 | "Units": "%", 576 | "Formula": "A*100/255" 577 | }, 578 | { 579 | "Mode": "01", 580 | "PID": "53", 581 | "Bytes": "2", 582 | "Description": "Absolute Evap system Vapour Pressure", 583 | "Min": "0", 584 | "Max": "327.675", 585 | "Units": "kPa", 586 | "Formula": "((A*256)+B)/200", 587 | "ImperialFormula": "(((A*256)+B)/200)*0.145037738", 588 | "ImperialUnits": "psi" 589 | }, 590 | { 591 | "Mode": "01", 592 | "PID": "54", 593 | "Bytes": "2", 594 | "Description": "Evap system vapor pressure", 595 | "Min": "-32.767", 596 | "Max": "32.768", 597 | "Units": "Pa", 598 | "Formula": "A*256+B - 32768" 599 | }, 600 | { 601 | "Mode": "01", 602 | "PID": "59", 603 | "Bytes": "2", 604 | "Description": "Fuel rail pressure (absolute)", 605 | "Min": "0", 606 | "Max": "655.350", 607 | "Units": "kPa", 608 | "Formula": "((A*256)+B) * 10", 609 | "ImperialFormula": "(((A*256)+B) * 10)*0.145037738", 610 | "ImperialUnits": "psi" 611 | }, 612 | { 613 | "Mode": "01", 614 | "PID": "5A", 615 | "Bytes": "1", 616 | "Description": "Relative accelerator pedal position", 617 | "Min": "0", 618 | "Max": "100", 619 | "Units": "%", 620 | "Formula": "A*100/255" 621 | }, 622 | { 623 | "Mode": "01", 624 | "PID": "5B", 625 | "Bytes": "1", 626 | "Description": "Hybrid battery pack remaining life", 627 | "Min": "0", 628 | "Max": "100", 629 | "Units": "%", 630 | "Formula": "A*100/255" 631 | }, 632 | { 633 | "Mode": "01", 634 | "PID": "5C", 635 | "Bytes": "1", 636 | "Description": "Engine oil temperature", 637 | "Min": "-40", 638 | "Max": "210", 639 | "Units": "°C", 640 | "Formula": "A-40", 641 | "ImperialFormula": "((A-40) * 1.8) + 32", 642 | "ImperialUnits": "°F" 643 | }, 644 | { 645 | "Mode": "01", 646 | "PID": "5D", 647 | "Bytes": "2", 648 | "Description": "Fuel injection timing", 649 | "Min": "-210", 650 | "Max": "301.992", 651 | "Formula": "(((A*256)+B)-26.880)/128" 652 | }, 653 | { 654 | "Mode": "01", 655 | "PID": "5E", 656 | "Bytes": "2", 657 | "Description": "Engine fuel rate", 658 | "Min": "0", 659 | "Max": "3212.75", 660 | "Units": "L/h", 661 | "Formula": "((A*256)+B)*0.05" 662 | }, 663 | { 664 | "Mode": "01", 665 | "PID": "61", 666 | "Bytes": "1", 667 | "Description": "Driver's demand engine - percent torque", 668 | "Min": "-125", 669 | "Max": "125", 670 | "Units": "%", 671 | "Formula": "A-125" 672 | }, 673 | { 674 | "Mode": "01", 675 | "PID": "62", 676 | "Bytes": "1", 677 | "Description": "Actual engine - percent torque", 678 | "Min": "-125", 679 | "Max": "125", 680 | "Units": "%", 681 | "Formula": "A-125" 682 | }, 683 | { 684 | "Mode": "01", 685 | "PID": "63", 686 | "Bytes": "2", 687 | "Description": "Engine reference torque", 688 | "Min": "0", 689 | "Max": "65.535", 690 | "Units": "Nm", 691 | "Formula": "A*256+B" 692 | }, 693 | { 694 | "Mode": "01", 695 | "PID": "65", 696 | "Bytes": "2", 697 | "Description": "Auxiliary input-output supported", 698 | "Formula": "Bit Encoded" 699 | }, 700 | { 701 | "Mode": "01", 702 | "PID": "66", 703 | "Bytes": "5", 704 | "Description": "Mass air flow sensor" 705 | }, 706 | { 707 | "Mode": "01", 708 | "PID": "67", 709 | "Bytes": "3", 710 | "Description": "Engine coolant temperature" 711 | }, 712 | { 713 | "Mode": "01", 714 | "PID": "68", 715 | "Bytes": "7", 716 | "Description": "Intake air temperature sensor" 717 | }, 718 | { 719 | "Mode": "01", 720 | "PID": "69", 721 | "Bytes": "7", 722 | "Description": "Commanded EGR and EGR Error" 723 | }, 724 | { 725 | "Mode": "01", 726 | "PID": "6B", 727 | "Bytes": "5", 728 | "Description": "Exhaust gas recirculation temperature" 729 | }, 730 | { 731 | "Mode": "01", 732 | "PID": "6D", 733 | "Bytes": "6", 734 | "Description": "Fuel pressure control system" 735 | }, 736 | { 737 | "Mode": "01", 738 | "PID": "6E", 739 | "Bytes": "5", 740 | "Description": "Injection pressure control system" 741 | }, 742 | { 743 | "Mode": "01", 744 | "PID": "6F", 745 | "Bytes": "3", 746 | "Description": "Turbocharger compressor inlet pressure" 747 | }, 748 | { 749 | "Mode": "01", 750 | "PID": "70", 751 | "Bytes": "9", 752 | "Description": "Boost pressure control" 753 | }, 754 | { 755 | "Mode": "01", 756 | "PID": "71", 757 | "Bytes": "5", 758 | "Description": "Variable Geometry turbo (VGT) control" 759 | }, 760 | { 761 | "Mode": "01", 762 | "PID": "72", 763 | "Bytes": "5", 764 | "Description": "Wastegate control" 765 | }, 766 | { 767 | "Mode": "01", 768 | "PID": "73", 769 | "Bytes": "5", 770 | "Description": "Exhaust pressure" 771 | }, 772 | { 773 | "Mode": "01", 774 | "PID": "74", 775 | "Bytes": "5", 776 | "Description": "Turbocharger RPM" 777 | }, 778 | { 779 | "Mode": "01", 780 | "PID": "75", 781 | "Bytes": "7", 782 | "Description": "Turbocharger temperature" 783 | }, 784 | { 785 | "Mode": "01", 786 | "PID": "76", 787 | "Bytes": "7", 788 | "Description": "Turbocharger temperature" 789 | }, 790 | { 791 | "Mode": "01", 792 | "PID": "77", 793 | "Bytes": "5", 794 | "Description": "Charge air cooler temperature (CACT)" 795 | }, 796 | { 797 | "Mode": "01", 798 | "PID": "7A", 799 | "Bytes": "7", 800 | "Description": "Diesel particulate filter (DPF)" 801 | }, 802 | { 803 | "Mode": "01", 804 | "PID": "7B", 805 | "Bytes": "7", 806 | "Description": "Diesel particulate filter (DPF)" 807 | }, 808 | { 809 | "Mode": "01", 810 | "PID": "7C", 811 | "Bytes": "9", 812 | "Description": "Diesel Particulate filter (DPF) temperature" 813 | }, 814 | { 815 | "Mode": "01", 816 | "PID": "7D", 817 | "Bytes": "1", 818 | "Description": "NOx NTE control area status" 819 | }, 820 | { 821 | "Mode": "01", 822 | "PID": "7E", 823 | "Bytes": "1", 824 | "Description": "PM NTE control area status" 825 | }, 826 | { 827 | "Mode": "01", 828 | "PID": "7F", 829 | "Bytes": "13", 830 | "Description": "Engine run time" 831 | }, 832 | { 833 | "Mode": "01", 834 | "PID": "81", 835 | "Bytes": "21", 836 | "Description": "Engine run time for AECD" 837 | }, 838 | { 839 | "Mode": "01", 840 | "PID": "82", 841 | "Bytes": "21", 842 | "Description": "Engine run time for AECD" 843 | }, 844 | { 845 | "Mode": "01", 846 | "PID": "83", 847 | "Bytes": "5", 848 | "Description": "NOx sensor" 849 | }, 850 | { 851 | "Mode": "01", 852 | "PID": "84", 853 | "Description": "Manifold surface temperature" 854 | }, 855 | { 856 | "Mode": "01", 857 | "PID": "85", 858 | "Description": "NOx reagent system" 859 | }, 860 | { 861 | "Mode": "01", 862 | "PID": "86", 863 | "Description": "Particulate matter (PM) sensor" 864 | }, 865 | { 866 | "Mode": "01", 867 | "PID": "87", 868 | "Description": "Intake manifold absolute pressure" 869 | } 870 | ] 871 | } -------------------------------------------------------------------------------- /obd/src/main/assets/pids-mode4.json: -------------------------------------------------------------------------------- 1 | { 2 | "pids": [ 3 | { 4 | "Mode": "04", 5 | "PID": "0", 6 | "Bytes": "0", 7 | "Description": "Clear DTCs" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /obd/src/main/assets/pids-mode9.json: -------------------------------------------------------------------------------- 1 | { 2 | "pids": [ 3 | { 4 | "Mode": "09", 5 | "PID": "00", 6 | "Bytes": "4", 7 | "Description": "Mode 9 supported PIDs" 8 | }, 9 | { 10 | "Mode": "09", 11 | "PID": "02", 12 | "Bytes": "20", 13 | "Description": "Vehicle Identification Number (VIN)" 14 | }, 15 | { 16 | "Mode": "09", 17 | "PID": "04", 18 | "Bytes": "16", 19 | "Description": "Calibration ID" 20 | }, 21 | { 22 | "Mode": "09", 23 | "PID": "0A", 24 | "Bytes": "20", 25 | "Description": "ECU Name" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/ObdInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.pnuema.android.obd 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.startup.Initializer 6 | import com.pnuema.android.obd.statics.ObdLibrary 7 | 8 | @Suppress("unused") //inited in the manifest 9 | class ObdInitializer: Initializer { 10 | override fun create(context: Context): Context { 11 | ObdLibrary.init(context as Application) 12 | return context 13 | } 14 | 15 | override fun dependencies(): MutableList>> = mutableListOf() 16 | } -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/commands/BaseObdCommand.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | @file:Suppress("unused") 14 | 15 | package com.pnuema.android.obd.commands 16 | 17 | import com.pnuema.android.obd.models.PID 18 | import com.pnuema.android.obd.statics.PersistentStorage 19 | import java.io.BufferedReader 20 | import java.io.IOException 21 | import java.io.InputStream 22 | import java.io.InputStreamReader 23 | import java.io.OutputStream 24 | import kotlin.system.measureTimeMillis 25 | 26 | /** 27 | * Base OBD command class for communicating with an ELM327 device. 28 | */ 29 | abstract class BaseObdCommand { 30 | /** 31 | * @return a list of integers 32 | */ 33 | protected var buffer: ArrayList = ArrayList() 34 | private var cmd: String? = null 35 | private var useImperialUnits = false 36 | private var rawData: String? = null 37 | internal var mIgnoreResult: Boolean = false 38 | 39 | companion object { 40 | const val NODATA = "NODATA" 41 | const val SEARCHING = "SEARCHING" 42 | const val DATA = "DATA" 43 | const val ELM327 = "ELM327" 44 | 45 | internal lateinit var mPid: PID 46 | 47 | private fun readPersistent() { 48 | if (mPid.isPersistent && PersistentStorage.containsPid(mPid)) { 49 | mPid.calculatedResult = PersistentStorage.getElement(mPid)?.calculatedResult ?: 0f 50 | mPid.calculatedResultString = PersistentStorage.getElement(mPid)?.calculatedResultString 51 | mPid.data = PersistentStorage.getElement(mPid)?.data ?: ArrayList() 52 | } 53 | } 54 | 55 | private fun storePersistent() { 56 | if (mPid.isPersistent && !PersistentStorage.containsPid(mPid)) { 57 | PersistentStorage.addElement(mPid) 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * @return [String] representation of the formatted command response. 64 | */ 65 | abstract val formattedResult: String 66 | 67 | /** 68 | * @return the raw command response in string representation. 69 | */ 70 | val rawResult: String 71 | get() { 72 | rawData = if (rawData == null || rawData!!.contains(SEARCHING) || rawData!!.contains(DATA) || rawData!!.contains(ELM327)) 73 | NODATA 74 | else 75 | rawData 76 | 77 | return rawData!! 78 | } 79 | 80 | /** 81 | * @return the OBD command name. 82 | */ 83 | abstract val name: String 84 | 85 | @Suppress("RemoveEmptySecondaryConstructorBody") 86 | private constructor() {} 87 | 88 | /** 89 | * Default ctor to use 90 | * 91 | * @param command the command to send 92 | */ 93 | private constructor(command: String?) { 94 | this.cmd = command 95 | } 96 | 97 | internal constructor(command: String, pid: PID) : this(command.trim { it <= ' ' }) { 98 | mPid = pid 99 | } 100 | 101 | /** 102 | * Copy constructor. 103 | * 104 | * @param other the ObdCommand to copy. 105 | */ 106 | constructor(other: BaseObdCommand) : this(other.cmd) 107 | 108 | /** 109 | * This method exists so that for each command, there must be a method that is 110 | * called only once to perform calculations. 111 | */ 112 | protected abstract fun performCalculations() 113 | 114 | /** 115 | * Sends the OBD-II request and deals with the response. 116 | * 117 | * This method CAN be overridden in fake commands. 118 | * @param inputStream [InputStream] to read the result of the requested PID. 119 | * @param out [OutputStream] on which to send the request. 120 | * @throws IOException thrown if IO is unable to be performed 121 | * @throws InterruptedException thrown if the process is interrupted 122 | * @return Current instance of [BaseObdCommand] 123 | */ 124 | @Throws(IOException::class, InterruptedException::class) 125 | fun run(inputStream: InputStream, out: OutputStream): BaseObdCommand { 126 | synchronized(BaseObdCommand::class.java) { 127 | mPid.retrievalTime = measureTimeMillis { 128 | if (mPid.isPersistent && PersistentStorage.containsPid(mPid)) { 129 | readPersistent() 130 | } else { 131 | sendCommand(out) 132 | readResult(inputStream) 133 | } 134 | } 135 | } 136 | return this 137 | } 138 | 139 | /** 140 | * Sends the OBD-II request. 141 | * 142 | * This method may be overridden in subclasses 143 | * 144 | * @param out The output stream. 145 | */ 146 | @Throws(IOException::class, InterruptedException::class) 147 | private fun sendCommand(out: OutputStream) { 148 | // write to OutputStream (i.e.: a BluetoothSocket) with an added carriage return 149 | out.write((cmd!! + "\r").toByteArray()) 150 | out.flush() 151 | } 152 | 153 | /** 154 | * Reads the OBD-II response. 155 | * 156 | * 157 | * This method may be overridden in subclasses, such as ObdMultiCommand. 158 | */ 159 | @Throws(IOException::class) 160 | private fun readResult(inputStream: InputStream) { 161 | readRawData(inputStream) 162 | 163 | if (!mIgnoreResult) { 164 | fillBuffer() 165 | performCalculations() 166 | storePersistent() 167 | } 168 | } 169 | 170 | /** 171 | * Fills the buffer from the raw data. 172 | */ 173 | private fun fillBuffer() { 174 | // read string each two chars 175 | buffer.clear() 176 | rawData?.chunked(2)?.forEach { 177 | try { 178 | buffer.add(Integer.decode("0x$it")) 179 | } catch (e: NumberFormatException) { return } 180 | } 181 | } 182 | 183 | @Throws(IOException::class) 184 | private fun readRawData(inputStream: InputStream) { 185 | val reader = BufferedReader(InputStreamReader(inputStream)) 186 | 187 | val res = StringBuilder() 188 | 189 | var c: Char 190 | var b = reader.read() 191 | while (b > -1) { // -1 if the end of the stream is reached 192 | c = b.toChar() 193 | 194 | if (c == '>') { // read until '>' arrives 195 | break 196 | } 197 | res.append(c) 198 | b = reader.read() 199 | } 200 | 201 | /* 202 | * Imagine the following response 41 0c 00 0d. 203 | * 204 | * ELM sends strings!! So, ELM puts spaces between each "byte". Pay 205 | * attention to the fact that I've put the word byte in quotes, because 41 206 | * is actually TWO bytes (two chars) in the socket. So, we must do some more 207 | * processing.. 208 | */ 209 | rawData = res.toString().trim { it <= ' ' } 210 | 211 | /* 212 | * data may have echo or informative text like "INIT BUS..." or similar. 213 | * The response ends with two carriage return characters. So we need to take 214 | * everything from the last carriage return before those two (trimmed above). 215 | */ 216 | rawData = rawData?.let { 217 | it.substring(it.indexOf(13.toChar()) + 1) 218 | } 219 | } 220 | 221 | /** 222 | * Gets whether imperial or metric units should be shown 223 | * @return true if imperial units are used, or false otherwise 224 | */ 225 | fun useImperialUnits(): Boolean = useImperialUnits 226 | 227 | /** 228 | * Sets whether to use imperial units or metric. 229 | * 230 | * @param isImperial Set to 'true' if you want to use imperial units, false otherwise. By 231 | * default this value is set to 'false'. 232 | */ 233 | fun setImperialUnits(isImperial: Boolean) { 234 | this.useImperialUnits = isImperial 235 | } 236 | } -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/commands/OBDCommand.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | @file:Suppress("unused") 14 | 15 | package com.pnuema.android.obd.commands 16 | 17 | import android.util.Log 18 | import com.ezylang.evalex.Expression 19 | import com.ezylang.evalex.config.ExpressionConfiguration 20 | import com.pnuema.android.obd.models.PID 21 | import com.pnuema.android.obd.statics.Translations 22 | 23 | 24 | /** 25 | * Generic class to form an OBD command for communication, also includes the parsing of the result. 26 | * 27 | * @author Brad Barnhill 28 | */ 29 | class OBDCommand(pid: PID) : BaseObdCommand(pid.mode.trim { it <= ' ' } + if (pid.PID.trim { it <= ' ' }.isEmpty()) "" else " " + pid.PID.trim { it <= ' ' }, pid) { 30 | private var mMetricUnits = true 31 | 32 | override val formattedResult: String 33 | get() = mPid.calculatedResult.toString() + " " + if (mMetricUnits || mPid.imperialFormula == null) mPid.units else mPid.imperialUnits 34 | 35 | override val name: String 36 | get() = mPid.description 37 | 38 | val callDuration: Long 39 | get() = mPid.retrievalTime 40 | 41 | companion object { 42 | const val A = "A" 43 | const val B = "B" 44 | const val C = "C" 45 | const val D = "D" 46 | 47 | val expressionConfig: ExpressionConfiguration by lazy { 48 | ExpressionConfiguration.builder() 49 | .decimalPlacesRounding(5) 50 | .stripTrailingZeros(true) 51 | .build() 52 | } 53 | } 54 | 55 | init { 56 | pid.retrievalTime = 0 57 | } 58 | 59 | /** 60 | * Set if the result of the request will be ignored 61 | * @param ignoreResult [Boolean] which is whether the result should be ignored when its returned. 62 | * @return Current [OBDCommand] 63 | */ 64 | fun setIgnoreResult(ignoreResult: Boolean): OBDCommand { 65 | mIgnoreResult = ignoreResult 66 | return this 67 | } 68 | 69 | /** 70 | * Set to metric or imperial units 71 | * @param metric True if to return metric units, false for imperial units. 72 | * @return Current [OBDCommand] 73 | */ 74 | fun setUnitType(metric: Boolean): OBDCommand { 75 | mMetricUnits = metric 76 | return this 77 | } 78 | 79 | override fun performCalculations() { 80 | if (NODATA != rawResult) { 81 | val exprText = if (mMetricUnits || mPid.imperialFormula == null) mPid.formula else mPid.imperialFormula 82 | 83 | val numBytes: Byte = try { 84 | java.lang.Byte.parseByte(mPid.bytes) 85 | } catch (nfex: NumberFormatException) { 86 | 0 87 | } 88 | 89 | mPid.data.clear() 90 | mPid.data.addAll(buffer) 91 | 92 | if (Translations.handleSpecialPidEnumerations(mPid, mPid.data)) { 93 | return 94 | } 95 | 96 | if (exprText == null || !exprText.contains(A) && !exprText.contains(B) && !exprText.contains(C) && !exprText.contains(D) || numBytes > 4 || numBytes <= 0 || mPid.data.size <= 2) { 97 | mPid.calculatedResultString = mPid.data.toString() 98 | return 99 | } 100 | 101 | //TODO: first two bytes show what command the data is for, verify this is the command returning that is expected 102 | 103 | val expression = Expression(exprText, expressionConfig) 104 | 105 | if (mPid.data.size > 2) 106 | expression.with(A, mPid.data[2]) 107 | 108 | if (mPid.data.size > 3) 109 | expression.with(B, mPid.data[3]) 110 | 111 | if (mPid.data.size > 4) 112 | expression.with(C, mPid.data[4]) 113 | 114 | if (mPid.data.size > 5) 115 | expression.with(D, mPid.data[5]) 116 | 117 | try { 118 | mPid.calculatedResult = expression.evaluate().numberValue.toFloat() 119 | mPid.calculatedResultString = mPid.calculatedResult.toString() 120 | } catch (nfex: NumberFormatException) { 121 | Log.e(OBDCommand::class.java.simpleName, "[Expression:" + expression + "] [mode:" + mPid.mode + "] [Pid:" + mPid.PID + "] [formula:" + mPid.formula 122 | + "] [bytes:" + mPid.bytes + "] [BytesReturned:" + mPid.data.size + "]", nfex) 123 | } catch (genEx: Exception) { 124 | Log.e(OBDCommand::class.java.simpleName, "[Expression:" + expression + "] [mode:" + mPid.mode + "] [Pid:" + mPid.PID + "] [formula:" + mPid.formula + "] [bytes:" + mPid.bytes + "] [BytesReturned:" + mPid.data.size + "]", genEx) 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/enums/ObdModes.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | package com.pnuema.android.obd.enums 14 | 15 | import java.lang.Long.parseLong 16 | 17 | /** 18 | * All OBD modes. 19 | * 20 | * @author Brad Barnhill 21 | */ 22 | @Suppress("unused") 23 | enum class ObdModes constructor(val value: Char) { 24 | /** 25 | * This mode returns the common values for some sensors 26 | */ 27 | MODE_01('1'), 28 | 29 | /** 30 | * This mode gives the freeze frame (or instantaneous) data of a fault 31 | */ 32 | MODE_02('2'), 33 | 34 | /** 35 | * This mode shows the stored diagnostic trouble codes 36 | */ 37 | MODE_03('3'), 38 | 39 | /** 40 | * This mode is used to clear recorded fault codes 41 | */ 42 | MODE_04('4'), 43 | 44 | /** 45 | * This mode gives the results of self-diagnostics done on the oxygen/lamda sensors 46 | */ 47 | MODE_05('5'), 48 | 49 | /** 50 | * This mode gives the results of self-diagnostics done on systems not subject to constant surveillance 51 | */ 52 | MODE_06('6'), 53 | 54 | /** 55 | * This mode gives unconfirmed fault codes 56 | */ 57 | MODE_07('7'), 58 | 59 | /** 60 | * This mode gives the results of self-diagnostics on other systems (Hardly used in Europe) 61 | */ 62 | MODE_08('8'), 63 | 64 | /** 65 | * This mode gives the information concerning the vehicle 66 | */ 67 | MODE_09('9'), 68 | 69 | /** 70 | * This mode shows the permanent diagnostic trouble codes 71 | */ 72 | MODE_0A('A'); 73 | 74 | val intValue: Int get() = parseLong(value.toString(), 16).toInt() 75 | 76 | override fun toString(): String = value.toString() 77 | } -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/enums/ObdProtocols.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | package com.pnuema.android.obd.enums 14 | 15 | /** 16 | * All OBD protocols. 17 | * 18 | * @author Brad Barnhill 19 | */ 20 | @Suppress("unused") 21 | enum class ObdProtocols constructor(val value: Char) { 22 | 23 | /** 24 | * Auto select protocol and save. 25 | */ 26 | AUTO('0'), 27 | 28 | /** 29 | * 41.6 kbaud 30 | */ 31 | SAE_J1850_PWM('1'), 32 | 33 | /** 34 | * 10.4 kbaud 35 | */ 36 | SAE_J1850_VPW('2'), 37 | 38 | /** 39 | * 5 baud init 40 | */ 41 | ISO_9141_2('3'), 42 | 43 | /** 44 | * 5 baud init 45 | */ 46 | ISO_14230_4_KWP('4'), 47 | 48 | /** 49 | * Fast init 50 | */ 51 | ISO_14230_4_KWP_FAST('5'), 52 | 53 | /** 54 | * 11 bit ID, 500 kbaud 55 | */ 56 | ISO_15765_4_CAN('6'), 57 | 58 | /** 59 | * 29 bit ID, 500 kbaud 60 | */ 61 | ISO_15765_4_CAN_B('7'), 62 | 63 | /** 64 | * 11 bit ID, 250 kbaud 65 | */ 66 | ISO_15765_4_CAN_C('8'), 67 | 68 | /** 69 | * 29 bit ID, 250 kbaud 70 | */ 71 | ISO_15765_4_CAN_D('9'), 72 | 73 | /** 74 | * 29 bit ID, 250 kbaud (user adjustable) 75 | */ 76 | SAE_J1939_CAN('A'), 77 | 78 | /** 79 | * 11 bit ID (user adjustable), 125 kbaud (user adjustable) 80 | */ 81 | USER1_CAN('B'), 82 | 83 | /** 84 | * 11 bit ID (user adjustable), 50 kbaud (user adjustable) 85 | */ 86 | USER2_CAN('C') 87 | } -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/models/DTC.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | @file:Suppress("unused") 14 | 15 | package com.pnuema.android.obd.models 16 | 17 | import com.pnuema.android.obd.enums.ObdModes 18 | import kotlinx.serialization.Serializable 19 | 20 | /** 21 | * Holder for a single DTC's data 22 | * 23 | * @author Brad Barnhill 24 | */ 25 | @Serializable 26 | data class DTC ( 27 | var mode: String = "01", 28 | var code: String? = null, 29 | var description: String? = null 30 | ) : java.io.Serializable { 31 | /** 32 | * Sets the mode. 33 | * 34 | * @param mode mode to set. 35 | * @return DTC object with the mode set. (returns object for method chaining support) 36 | */ 37 | fun setMode(mode: ObdModes): DTC { 38 | this.mode = "0${mode.value}" 39 | 40 | return this 41 | } 42 | 43 | val modeString get() = mode.trimStart('0') 44 | } 45 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/models/DTCS.kt: -------------------------------------------------------------------------------- 1 | package com.pnuema.android.obd.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Holder for all DTCs 8 | * 9 | * @author Brad Barnhill 10 | */ 11 | @Serializable 12 | data class DTCS ( 13 | @SerialName("dtcs") 14 | val dtcs: List = ArrayList() 15 | ) 16 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/models/PID.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | package com.pnuema.android.obd.models 14 | 15 | import com.pnuema.android.obd.enums.ObdModes 16 | import kotlinx.serialization.SerialName 17 | import kotlinx.serialization.Serializable 18 | 19 | /** 20 | * Holder for a single pids data 21 | * 22 | * @author Brad Barnhill 23 | */ 24 | @Suppress("MemberVisibilityCanBePrivate") 25 | @Serializable 26 | data class PID ( 27 | @SerialName("Mode") 28 | var mode: String = "01", 29 | @SerialName("PID") 30 | var PID: String = "01", 31 | @SerialName("Bytes") 32 | var bytes: String = "", 33 | @SerialName("Description") 34 | var description: String = "", 35 | @SerialName("Min") 36 | var min: String? = null, 37 | @SerialName("Max") 38 | var max: String? = null, 39 | @SerialName("Units") 40 | var units: String? = null, 41 | @SerialName("Formula") 42 | var formula: String? = null, 43 | @SerialName("ImperialFormula") 44 | var imperialFormula: String? = null, 45 | @SerialName("ImperialUnits") 46 | var imperialUnits: String? = null 47 | ): java.io.Serializable { 48 | var data: ArrayList = ArrayList() 49 | var calculatedResultString: String? = null 50 | var calculatedResult: Float = 0.toFloat() 51 | var retrievalTime: Long = 0 52 | var isPersistent: Boolean = false 53 | 54 | constructor(mode: ObdModes, pid: String = ""): this() { 55 | setModeAndPID(mode, pid) 56 | } 57 | 58 | /** 59 | * Sets the mode of the PID 60 | * 61 | * @param mode mode to set. 62 | * @return DTC object with the mode set. (returns object for method chaining support) 63 | */ 64 | fun setMode(mode: ObdModes): PID { 65 | this.mode = "0" + mode.value 66 | 67 | return this 68 | } 69 | 70 | /** 71 | * Sets the PID. 72 | * 73 | * @param pid Pid to set (ex. 0C). 74 | * @param mode Mode to set (ex. 01). 75 | * @return PID object with the Mode and Pid set. (returns object for method chaining support) 76 | */ 77 | fun setModeAndPID(mode: ObdModes, pid: String): PID { 78 | setMode(mode) 79 | this.PID = pid 80 | return this 81 | } 82 | 83 | /** 84 | * Get the string description of the PID. 85 | * 86 | * @return [String] description of the PID 87 | */ 88 | override fun toString(): String { 89 | return description 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/models/PIDS.kt: -------------------------------------------------------------------------------- 1 | package com.pnuema.android.obd.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * Holder for all PIDS 8 | * 9 | * @author Brad Barnhill 10 | */ 11 | @Serializable 12 | data class PIDS ( 13 | @SerialName("pids") 14 | val pids: List = ArrayList() 15 | ) 16 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/statics/DTCUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | package com.pnuema.android.obd.statics 14 | 15 | import com.pnuema.android.obd.models.DTC 16 | import com.pnuema.android.obd.models.DTCS 17 | import kotlinx.serialization.decodeFromString 18 | import kotlinx.serialization.json.Json 19 | import java.io.IOException 20 | 21 | /** 22 | * Class to hold all the static methods necessary for the OBD library 23 | * that pertain to DTCs 24 | * 25 | * @author Brad Barnhill 26 | */ 27 | @Suppress("unused") 28 | object DTCUtils { 29 | val dtcList: List 30 | @Throws(IOException::class) 31 | get() = Json.decodeFromString(FileUtils.readFromFile("dtc-codes.json")).dtcs 32 | } 33 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/statics/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pnuema.android.obd.statics 2 | 3 | import java.io.BufferedReader 4 | import java.io.IOException 5 | import java.io.InputStream 6 | import java.io.InputStreamReader 7 | 8 | /** 9 | * Static definitions for all file related utilities. 10 | * 11 | * @author Brad Barnhill 12 | */ 13 | object FileUtils { 14 | /** 15 | * Read entire file into a string. (used to read json files from assets) 16 | * 17 | * @param fileName name of file to read 18 | * @return Entire file contents in a string 19 | * @throws IOException thrown if IO can not be performed 20 | */ 21 | @Throws(IOException::class) 22 | fun readFromFile(fileName: String): String { 23 | val returnString = StringBuilder() 24 | 25 | val fIn = ObdLibrary.getResourceFileInputStream(fileName) 26 | ?: error("Could not read resource files. ObdLibrary not initialized.") 27 | 28 | InputStreamReader(fIn).use { isr -> 29 | BufferedReader(isr).use { input -> 30 | var line = input.readLine() 31 | while (line != null) { 32 | returnString.append(line) 33 | line = input.readLine() 34 | } 35 | } 36 | } 37 | 38 | return returnString.toString() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/statics/ObdLibrary.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Pnuema Productions LLC ("COMPANY") CONFIDENTIAL 3 | * Unpublished Copyright (c) 2009-2015 Pnuema Productions, All Rights Reserved. 4 | * 5 | * NOTICE: All information contained herein is, and remains the property of COMPANY. The intellectual and technical concepts contained herein are proprietary to COMPANY and may be covered by U.S. and Foreign Patents, patents in process, and are protected by trade secret or copyright law. Dissemination of this information or reproduction of this material is strictly forbidden unless prior written permission is obtained from COMPANY. Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, managers or contractors who have executed Confidentiality and Non-disclosure agreements explicitly covering such access. 6 | * 7 | * The copyright notice above does not evidence any actual or intended publication or disclosure of this source code, which includes information that is confidential and/or proprietary, and is a trade secret, of COMPANY. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, 8 | * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, AND IN VIOLATION OF APPLICABLE 9 | * LAWS AND INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS 10 | * TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. 11 | */ 12 | 13 | package com.pnuema.android.obd.statics 14 | 15 | import android.app.Application 16 | import java.io.InputStream 17 | 18 | /** 19 | * This class handles getting the applications context. 20 | * 21 | * @author Brad Barnhill 22 | */ 23 | internal object ObdLibrary { 24 | fun init(applicationContext: Application) { 25 | this.applicationContext = applicationContext 26 | } 27 | 28 | private lateinit var applicationContext: Application 29 | 30 | fun getResourceString(stringRes: Int): String = applicationContext.getString(stringRes) 31 | 32 | fun getResourceStringArray(stringArrayRes: Int): Array = applicationContext.resources.getStringArray(stringArrayRes) 33 | 34 | fun getResourceFileInputStream(fileName: String): InputStream? = runCatching { applicationContext.assets.open(fileName) }.getOrNull() 35 | 36 | } 37 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/statics/PIDUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | * use this file except in compliance with the License. You may obtain a copy of 4 | * the License at 5 | * http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software 8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | * License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | package com.pnuema.android.obd.statics 14 | 15 | import android.util.Log 16 | import android.util.SparseArray 17 | import com.pnuema.android.obd.enums.ObdModes 18 | import com.pnuema.android.obd.models.PID 19 | import com.pnuema.android.obd.models.PIDS 20 | import kotlinx.serialization.json.Json 21 | import java.io.IOException 22 | import java.util.* 23 | 24 | /** 25 | * Class to hold all the static methods necessary for the OBD library. 26 | * that pertain to DTCs 27 | * 28 | * @author Brad Barnhill 29 | */ 30 | @Suppress("unused") 31 | object PIDUtils { 32 | private val TAG = PIDUtils::class.java.simpleName 33 | private val pidsSparseArray = SparseArray>() 34 | 35 | /** 36 | * Gets list of pids for the mode specified 37 | * 38 | * @param mode mode to look up the list of pids 39 | * @return List of [PIDS] contained in the specified mode 40 | * @throws IOException thrown if IO can not be performed 41 | */ 42 | @Throws(IOException::class) 43 | fun getPidList(mode: ObdModes): List = ArrayList(getPidMap(mode)!!.values) 44 | 45 | /** 46 | * Gets PID object by mode and pid. 47 | * 48 | * @param mode mode to look the pid up in 49 | * @param pid Pid number to retrieve 50 | * @return [PID] object 51 | * @throws IOException thrown if IO can not be performed 52 | */ 53 | @Throws(IOException::class,java.lang.IllegalArgumentException::class) 54 | fun getPid(mode: ObdModes, pid: String): PID? { 55 | getPidMap(mode)?.let { pids -> 56 | return pids[Integer.parseInt(pid, 16)] 57 | } ?: run { 58 | Log.d(TAG, "Pids for this mode do not exist.") 59 | return null 60 | } 61 | } 62 | 63 | @Throws(IOException::class,java.lang.IllegalArgumentException::class) 64 | private fun getPidMap(mode: ObdModes): SortedMap? { 65 | if (pidsSparseArray.size() > 0 && pidsSparseArray.indexOfKey(mode.intValue) >= 0) { 66 | //get value from pid cache 67 | return pidsSparseArray.get(mode.intValue) 68 | } else { 69 | //not found in cache so read it from json files and store it in cache 70 | val pidList = Json.decodeFromString(FileUtils.readFromFile("pids-mode" + mode.intValue + ".json")).pids 71 | val pidMap = TreeMap() 72 | 73 | pidList.forEach { pid -> 74 | try { 75 | pidMap[Integer.parseInt(pid.PID, 16)] = pid 76 | } catch (nfex: NumberFormatException) { 77 | Log.d(TAG, "Parsing PID number to integer failed: " + nfex.message) 78 | } 79 | } 80 | 81 | require(!pidMap.isEmpty()) { "Unsupported mode requested: $mode" } 82 | 83 | pidsSparseArray.put(mode.intValue, pidMap) 84 | return pidsSparseArray.get(mode.intValue) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/statics/PersistentStorage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Pnuema Productions LLC ("COMPANY") CONFIDENTIAL 3 | * Unpublished Copyright (c) 2009-2015 Pnuema Productions, All Rights Reserved. 4 | * 5 | * NOTICE: All information contained herein is, and remains the property of COMPANY. The intellectual and technical concepts contained herein are proprietary to COMPANY and may be covered by U.S. and Foreign Patents, patents in process, and are protected by trade secret or copyright law. Dissemination of this information or reproduction of this material is strictly forbidden unless prior written permission is obtained from COMPANY. Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, managers or contractors who have executed Confidentiality and Non-disclosure agreements explicitly covering such access. 6 | * 7 | * The copyright notice above does not evidence any actual or intended publication or disclosure of this source code, which includes information that is confidential and/or proprietary, and is a trade secret, of COMPANY. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, 8 | * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, AND IN VIOLATION OF APPLICABLE 9 | * LAWS AND INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS 10 | * TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. 11 | */ 12 | 13 | package com.pnuema.android.obd.statics 14 | 15 | import com.pnuema.android.obd.models.PID 16 | 17 | /** 18 | * Storage for the persistent pids so they dont have to be retrieved more than once. 19 | * 20 | * @author Brad Barnhill 21 | */ 22 | @Suppress("unused") 23 | object PersistentStorage { 24 | private val persistentPidStorage by lazy { HashMap() } 25 | 26 | fun addElement(element: PID?) { 27 | element?.let { pid -> 28 | persistentPidStorage[formKey(pid)] = pid 29 | } 30 | } 31 | 32 | fun removeElement(element: PID?) { 33 | element?.let { pid -> 34 | persistentPidStorage.remove(formKey(pid)) 35 | } 36 | } 37 | 38 | fun getElement(element: PID?): PID? { 39 | return element?.let { pid -> 40 | persistentPidStorage[formKey(pid)] 41 | } 42 | } 43 | 44 | fun containsPid(element: PID): Boolean { 45 | return element.isPersistent && persistentPidStorage.containsKey(formKey(element)) 46 | } 47 | 48 | fun clearAll() { 49 | persistentPidStorage.clear() 50 | } 51 | 52 | private fun formKey(pid: PID): String { 53 | return pid.mode + pid.PID 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /obd/src/main/java/com/pnuema/android/obd/statics/Translations.kt: -------------------------------------------------------------------------------- 1 | package com.pnuema.android.obd.statics 2 | 3 | import com.pnuema.android.obd.R 4 | import com.pnuema.android.obd.models.PID 5 | import java.util.* 6 | 7 | /** 8 | * This class provides translations for pids that are based on enumeration or other translation 9 | * methods other than just conversion using a simple formula. 10 | * 11 | * @author Brad Barnhill 12 | */ 13 | @Suppress("unused") 14 | object Translations { 15 | private const val bitsInOneCharValue = 4 16 | private const val HEX_RADIX = 16 17 | 18 | fun handleSpecialPidEnumerations(pid: PID, buffer: ArrayList): Boolean { 19 | if (pid.mode == "01" && pid.PID == "00") { 20 | mode1Pid00Translation(pid, buffer) 21 | return true 22 | } else if (pid.mode == "01" && pid.PID == "01") { 23 | //dtc count 24 | mode1Pid01Translation(pid, buffer) 25 | return true 26 | } else if (pid.mode == "01" && pid.PID == "51") { 27 | //fuel type 28 | mode1Pid51Translation(pid, buffer) 29 | return true 30 | } 31 | 32 | return false 33 | } 34 | 35 | /** 36 | * Not finished yet 37 | */ 38 | private fun mode1Pid00Translation(pid: PID, buffer: ArrayList) { 39 | if (buffer.size != 6) { 40 | return 41 | } 42 | 43 | val hexString = StringBuilder() 44 | val subList = buffer.subList(2, buffer.size) 45 | var temp: String 46 | subList.forEach { item -> 47 | temp = Integer.toHexString(item) 48 | hexString.append(if (temp.length == 1) "0" else "").append(temp) 49 | } 50 | 51 | pid.calculatedResult = java.lang.Long.parseLong(hexString.toString(), HEX_RADIX).toFloat() 52 | pid.calculatedResultString = hexString.toString() 53 | } 54 | 55 | private fun mode1Pid01Translation(pid: PID, buffer: ArrayList) { 56 | if (buffer.isNotEmpty() && buffer.size > 2) { 57 | // ignore first two bytes [hh hh] of the response 58 | val mil = buffer[2] 59 | val on = ObdLibrary.getResourceString(R.string.mode1_pid01_translation_on) 60 | val off = ObdLibrary.getResourceString(R.string.mode1_pid01_translation_off) 61 | val onOff = if (mil and 0x80 == 128) on else off 62 | pid.calculatedResultString = String.format(ObdLibrary.getResourceString(R.string.mode1_pid01_translation), onOff, mil and 0x7F) 63 | } 64 | } 65 | 66 | private fun mode1Pid51Translation(pid: PID, buffer: ArrayList) { 67 | val pidTranslation = ObdLibrary.getResourceStringArray(R.array.mode1_pid51_translation) 68 | if (buffer.isNotEmpty() && buffer.size > 2 && buffer[2] < pidTranslation.size) { 69 | pid.calculatedResultString = pidTranslation[buffer[2]] 70 | } else { 71 | pid.calculatedResultString = ObdLibrary.getResourceString(R.string.pid_value_unavailable) 72 | } 73 | } 74 | 75 | /** 76 | * Convert the given hexadecimal number to bits 77 | * 78 | * @param hex The [String] representation of the hexadecimal 79 | * @return [BitSet] containing the bits of the hexadecimal 80 | */ 81 | private fun hexToBitSet(hex: String): BitSet { 82 | val hexBitSize = hex.length * bitsInOneCharValue 83 | val hexBitSet = BitSet(hexBitSize) 84 | 85 | hex.forEachIndexed { index, hexChar -> 86 | val charBitSet = hexCharToBitSet(hexChar) 87 | 88 | for (j in 0 until bitsInOneCharValue) { 89 | if (charBitSet.get(j)) { 90 | hexBitSet.set(j + (hex.length - index - 1) * bitsInOneCharValue) 91 | } 92 | } 93 | } 94 | 95 | return hexBitSet 96 | } 97 | 98 | /** 99 | * Convert the given hexadecimal character to its bits. Note: valid inputs 100 | * are 0-F 101 | * 102 | * @param hexChar the hexadecimal character to convert 103 | * @return BitSet containing the bits of the hexadecimal character 104 | */ 105 | private fun hexCharToBitSet(hexChar: Char): BitSet { 106 | val charBitSet = BitSet(bitsInOneCharValue) 107 | val hex = Integer.parseInt(hexChar.toString(), HEX_RADIX) 108 | 109 | for (i in 0 until bitsInOneCharValue) { 110 | val bit = Integer.lowestOneBit(hex shr i) 111 | if (bit == 1) 112 | charBitSet.set(i) 113 | } 114 | 115 | return charBitSet 116 | } 117 | 118 | /** 119 | * Gets if a pid is available or not based on the value of a bitset 120 | * 121 | * @param pid PID to check if available to query 122 | * @param pidValue parent pid value (hex) which denotes which pids are available 123 | * @return True if PID is available, false otherwise. 124 | */ 125 | fun isPidAvailable(pid: PID, pidValue: String): Boolean { 126 | //each parent pid governs 32 pids 127 | val pidIndex = Integer.parseInt(pid.PID, HEX_RADIX) % 32 - 1 128 | 129 | return hexToBitSet(pidValue).get(pidIndex) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /obd/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | On 3 | Off 4 | %1$s %2$s 5 | 6 | Error 7 | 8 | 9 | Not available 10 | Gasoline 11 | Methanol 12 | Ethanol 13 | Diesel 14 | LPG 15 | CNG 16 | Propane 17 | Electric 18 | Bifuel running Gasoline 19 | Bifuel running Methanol 20 | Bifuel running Ethanol 21 | Bifuel running LPG 22 | Bifuel running CNG 23 | Bifuel running Propane 24 | Bifuel running Electricity 25 | Bifuel running electric and combustion engine 26 | Hybrid gasoline 27 | Hybrid Ethanol 28 | Hybrid Diesel 29 | Hybrid Electric 30 | Hybrid running electric and combustion engine 31 | Hybrid Regenerative 32 | Bifuel running diesel 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | google() 7 | mavenCentral() 8 | } 9 | } 10 | 11 | plugins { 12 | id("com.gradle.develocity") version "3.19.2" 13 | } 14 | 15 | dependencyResolutionManagement { 16 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 17 | repositories { 18 | gradlePluginPortal() 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | develocity { 25 | buildScan { 26 | termsOfUseUrl = "https://gradle.com/terms-of-service" 27 | termsOfUseAgree = "yes" 28 | } 29 | } 30 | 31 | val remoteCacheUrl: String? by extra 32 | val cacheUrl: String? = if (System.getenv("REMOTE_CACHE_URL") == null) remoteCacheUrl as String else System.getenv("REMOTE_CACHE_URL") 33 | 34 | if (cacheUrl != null) { 35 | buildCache { 36 | remote { 37 | url = uri(cacheUrl) 38 | isEnabled = true 39 | isPush = true 40 | isAllowUntrustedServer = true 41 | isAllowInsecureProtocol = false 42 | if (isEnabled) { 43 | println("Using remote build cache: $cacheUrl") 44 | } 45 | 46 | val remoteCacheUser: String? by extra 47 | val remoteCachePass: String? by extra 48 | credentials { 49 | username = if (System.getenv("REMOTE_CACHE_USER") == null) remoteCacheUser as String else System.getenv("REMOTE_CACHE_USER") 50 | password = if (System.getenv("REMOTE_CACHE_PASS") == null) remoteCachePass as String else System.getenv("REMOTE_CACHE_PASS") 51 | } 52 | } 53 | } 54 | } else { 55 | println("Not using remote build cache!") 56 | } 57 | 58 | include("obd") --------------------------------------------------------------------------------