Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add obd plugin #20818

Merged
merged 20 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
52cc734
#2678: Added base for OBD plugin (https://github.com/osmandapp/OsmAnd…
Corwin-Kh Sep 4, 2024
4916da4
added sending commands and getting answers
Corwin-Kh Sep 6, 2024
3ab34d7
Implemented continuous getting data from OBD; fixed parsing responses
Corwin-Kh Sep 12, 2024
ea37e1f
Added obd widgets
Corwin-Kh Sep 15, 2024
7dedf0d
Fixed getting fuel type
Corwin-Kh Sep 16, 2024
6127ee8
Added read obd error checks
Corwin-Kh Sep 16, 2024
de75b9d
Merge branch 'master' of github.com:osmandapp/OsmAnd into add_obd_plugin
Corwin-Kh Sep 16, 2024
834aa88
Merge branch 'master' of github.com:osmandapp/OsmAnd into add_obd_plugin
Corwin-Kh Sep 16, 2024
08f28cb
Added fixes after review
Corwin-Kh Sep 16, 2024
a74ba69
Fiexes after review; added ambient air temperature
Corwin-Kh Sep 17, 2024
6b0d2d7
Added battery voltage capmmand and widget
Corwin-Kh Sep 17, 2024
1fb7543
Implemented OBD computer
Corwin-Kh Sep 18, 2024
b482a85
Added fuel consumption rate and fuel left distance widgets; fixed wid…
Corwin-Kh Sep 19, 2024
5b5ef9b
Merge branch 'master' of github.com:osmandapp/OsmAnd into add_obd_plugin
Corwin-Kh Sep 19, 2024
4320dfc
Merge branch 'master' of github.com:osmandapp/OsmAnd into add_obd_plugin
Corwin-Kh Sep 19, 2024
ed3ae95
Merge branch 'master' of github.com:osmandapp/OsmAnd into add_obd_plugin
Corwin-Kh Sep 19, 2024
a1f3aef
Added test to debug consumption calculation; concurrence fixes
Corwin-Kh Sep 20, 2024
261de6a
Fixes after review
Corwin-Kh Sep 20, 2024
b52b311
Removed printSTackTrace usage
Corwin-Kh Sep 20, 2024
602138b
Minor fixes after refactoring
Corwin-Kh Sep 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public interface OsmAndCustomizationConstants {
String PLUGIN_ACCESSIBILITY = "osmand.accessibility";
String PLUGIN_WIKIPEDIA = "osmand.wikipedia";
String PLUGIN_ANT_PLUS = "osmand.antplus";
String PLUGIN_OBD = "osmand.obd";
String PLUGIN_WEATHER = "osmand.weather";

//Settings:
Expand Down
1 change: 1 addition & 0 deletions OsmAnd-shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesCoreVersion")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:$datetimeVersion")
implementation("com.squareup.okio:okio:$okioVersion")
implementation("co.touchlab:stately-concurrent-collections:2.0.0")
}
jvmMain.dependencies {
//implementation(kotlin("stdlib-jdk8"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.osmand.shared.obd

enum class OBDCommand(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably there should be abstract class AbstractOBDCommand and subclasses for each command

val command: String,
private val responseParser: (String) -> String,
val isStale: Boolean = false) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear what is stale and why it always false

OBD_SUPPORTED_LIST1_COMMAND("00", OBDUtils::parseSupportedCommandsResponse),
OBD_SUPPORTED_LIST2_COMMAND("20", OBDUtils::parseSupportedCommandsResponse),
OBD_SUPPORTED_LIST3_COMMAND("40", OBDUtils::parseSupportedCommandsResponse),
OBD_RPM_COMMAND("0C", OBDUtils::parseRpmResponse),
OBD_SPEED_COMMAND("0D", OBDUtils::parseSpeedResponse),
OBD_INTAKE_AIR_TEMP_COMMAND("0F", OBDUtils::parseIntakeAirTempResponse),
OBD_ENGINE_COOLANT_TEMP_COMMAND("05", OBDUtils::parseEngineCoolantTempResponse),
OBD_FUEL_CONSUMPTION_RATE_COMMAND("5E", OBDUtils::parseFuelConsumptionRateResponse),
OBD_FUEL_TYPE_COMMAND("51", OBDUtils::parseFuelTypeResponse),
OBD_FUEL_LEVEL_COMMAND("2F", OBDUtils::parseFuelLevelResponse);

fun parseResponse(response: String): String {
return responseParser.invoke(response.lowercase())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package net.osmand.shared.obd

import co.touchlab.stately.collections.ConcurrentMutableList
import co.touchlab.stately.collections.ConcurrentMutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import net.osmand.shared.util.LoggerFactory
import net.osmand.shared.util.PlatformUtil
import okio.Buffer
import okio.IOException
import okio.Sink
import okio.Source


object OBDDispatcher {

private val commandQueue = ConcurrentMutableList<OBDCommand>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use just List and KCollectionUtils for modifying

private val staleCommandsCache = ConcurrentMutableMap<OBDCommand, String>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

staleCommandsCache looks useless, since isStale is always false. Also staleCommandsCache never clears.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added stale commands

private var inputStream: Source? = null
private var outputStream: Sink? = null
private val log = LoggerFactory.getLogger("OBDDispatcher")
private const val TERMINATE_SYMBOL = "\r\r>"
private const val RESPONSE_LINE_TERMINATOR = "\r"
private const val READ_DATA_COMMAND_CODE = "01"
private val responseListeners = ArrayList<OBDResponseListener>()
private var job: Job? = null
private var scope: CoroutineScope? = null

private fun startReadObdLooper() {
job = Job()
scope = CoroutineScope(Dispatchers.IO + job!!)
scope!!.launch {
var inStream = inputStream
var outStream = outputStream
while (inStream != null && outStream != null) {
try {
val commands = ArrayList(commandQueue)
for (command in commands) {
if (command.isStale) {
val cachedCommandResponse = staleCommandsCache[command]
if (cachedCommandResponse != null) {
dispatchResult(command, cachedCommandResponse)
continue
}
}
val fullCommand = "$READ_DATA_COMMAND_CODE${command.command}\r"
val bufferToWrite = Buffer()
bufferToWrite.write(fullCommand.encodeToByteArray())
outStream.write(bufferToWrite, bufferToWrite.size)
outStream.flush()
log.debug("sent $fullCommand command")
val readBuffer = Buffer()
var resultRaw = StringBuilder()
var readResponseFailed = false
try {
val startReadTime = PlatformUtil.currentTimeMillis()
while (true) {
if (PlatformUtil.currentTimeMillis() - startReadTime > 3000) {
readResponseFailed = true
log.error("Read command ${command.name} timeout")
break
}
val bytesRead = inStream.read(readBuffer, 1024)
log.debug("read $bytesRead bytes")
if (bytesRead == -1L) {
log.debug("end of stream")
break
}
val receivedData = readBuffer.readByteArray()
resultRaw.append(receivedData.decodeToString())

log.debug("response so far ${resultRaw}")
if (resultRaw.contains(TERMINATE_SYMBOL)) {
log.debug("found terminator")
break
} else {
log.debug("no terminator found")
log.debug("${resultRaw.lines().size}")

}
}
} catch (e: IOException) {
e.printStackTrace()
log.error("Error reading data: ${e.message}")
}
if (readResponseFailed) {
continue
}
var response = resultRaw.toString()
response = response.replace(TERMINATE_SYMBOL, "")
val listResponses = response.split(RESPONSE_LINE_TERMINATOR)
for (responseIndex in 1 until listResponses.size) {
val result = command.parseResponse(listResponses[responseIndex])
log.debug("raw_response_$responseIndex: $result")
dispatchResult(command, result)
if (command.isStale) {
staleCommandsCache[command] = result
break
}
}
log.info("response. ${command.name} **${response.replace('\\', '\\')}")
}
} catch (error: Throwable) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should process exceptions more carefully. For example CancellationException should be handled separately

log.error("Run OBD looper error. $error")
}
inStream = inputStream
outStream = outputStream
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like mess in reading / writing streams

}
}
}

fun addCommand(commandToRead: OBDCommand) {
if (commandQueue.indexOf(commandToRead) == -1) {
commandQueue.add(commandToRead)
}
}

fun removeCommand(commandToStopReading: OBDCommand) {
commandQueue.remove(commandToStopReading)
}

fun addResponseListener(responseListener: OBDResponseListener) {
responseListeners.add(responseListener)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KCollectionUtils

}

fun removeResponseListener(responseListener: OBDResponseListener) {
responseListeners.remove(responseListener)
}

fun setReadWriteStreams(readStream: Source, writeStream: Sink) {
inputStream = readStream
outputStream = writeStream
job?.cancel()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't scope?.cancel() enough?

scope?.cancel()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why cancelling scope and job after streams redefined? looks like potential bug

startReadObdLooper()
}

fun getCommandQueue(): List<OBDCommand> {
return ArrayList(commandQueue)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commandQueue is immutable, do not create duplicate list

}

private fun dispatchResult(command: OBDCommand, result: String) {
for (listener in responseListeners) {
listener.onCommandResponse(command, result)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.osmand.shared.obd

interface OBDResponseListener {
fun onCommandResponse(command: OBDCommand, result: String)
}
104 changes: 104 additions & 0 deletions OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package net.osmand.shared.obd

object OBDUtils {
const val INVALID_RESPONSE_CODE = "-1"

fun parseSupportedCommandsResponse(response: String): String {
val responseParts = response.trim().split(" ")

if (responseParts.size >= 3 && responseParts[0] == "41") {
var supportedPIDs = ""
for (i in 2 until responseParts.size) {
val byteValue = responseParts[i].toInt(16)
for (bitIndex in 0..7) {
if ((byteValue and (1 shl (7 - bitIndex))) != 0) {
val pidNumber = ((i - 2) * 8) + bitIndex + 1
supportedPIDs += (" ${pidNumber.toString(16)}")
}
}
}
return supportedPIDs
}
return INVALID_RESPONSE_CODE
}

fun parseRpmResponse(response: String): String {
val hexValues = response.trim().split(" ")
if (hexValues.size >= 4) {
val A = hexValues[2].toInt(16)
val B = hexValues[3].toInt(16)
return (((A * 256) + B) / 4).toString()
}
return INVALID_RESPONSE_CODE
}

fun parseSpeedResponse(response: String): String {
val hexValues = response.trim().split(" ")
if (hexValues.size >= 3 && hexValues[0] == "41" && hexValues[1] == OBDCommand.OBD_SPEED_COMMAND.command.lowercase()) {
return (hexValues[2].toInt(16)).toString()
}
return INVALID_RESPONSE_CODE
}

fun parseIntakeAirTempResponse(response: String): String {
val hexValues = response.trim().split(" ")
if (hexValues[0] == "41" && hexValues[1] == OBDCommand.OBD_INTAKE_AIR_TEMP_COMMAND.command.lowercase()) {
val intakeAirTemp = hexValues[2].toInt(16)
return (intakeAirTemp - 40).toString()
}
return INVALID_RESPONSE_CODE
}

fun parseEngineCoolantTempResponse(response: String): String {
val hexValues = response.trim().split(" ")
if (hexValues.size >= 3 && hexValues[0] == "41" && hexValues[1] == OBDCommand.OBD_ENGINE_COOLANT_TEMP_COMMAND.command.lowercase()) {
return (hexValues[2].toInt(16) - 40).toString()
}
return INVALID_RESPONSE_CODE
}

fun parseFuelLevelResponse(response: String): String {
val hexValues = response.trim().split(" ")
if (hexValues.size >= 3 && hexValues[0] == "41" && hexValues[1] == OBDCommand.OBD_FUEL_LEVEL_COMMAND.command.lowercase()) {
return (hexValues[2].toInt(16) / 255 * 100).toString()
}
return INVALID_RESPONSE_CODE
}

fun parseFuelTypeResponse(response: String): String {
val responseParts = response.split(" ")

if (responseParts[0] == "41" && responseParts[1] == OBDCommand.OBD_FUEL_TYPE_COMMAND.command.lowercase()) {
val fuelTypeCode = responseParts[2].toInt(16)
return when (fuelTypeCode) {
0x01 -> "Бензин"
0x02 -> "Метанол"
0x03 -> "Этанол"
0x04 -> "Дизель"
0x05 -> "Пропан"
0x06 -> "Природный газ (сжатый)"
0x07 -> "Природный газ (сжиженный)"
0x08 -> "Сжиженный нефтяной газ (LPG)"
0x09 -> "Электричество"
0x0A -> "Гибрид (бензин/электричество)"
0x0B -> "Гибрид (дизель/электричество)"
0x0C -> "Гибрид (сжатый природный газ)"
0x0D -> "Гибрид (сжиженный природный газ)"
0x0E -> "Гибрид (сжиженный нефтяной газ)"
0x0F -> "Гибрид (аккумулятор)"
else -> "Неизвестный тип топлива"
}
}
return INVALID_RESPONSE_CODE
}

fun parseFuelConsumptionRateResponse(response: String): String {
val hexValues = response.trim().split(" ")
if (hexValues.size >= 4 && hexValues[0] == "41" && hexValues[1] == OBDCommand.OBD_FUEL_CONSUMPTION_RATE_COMMAND.command.lowercase()) {
val a = hexValues[2].toInt(16)
val b = hexValues[3].toInt(16)
return (((a * 256) + b) / 20.0).toString()
}
return INVALID_RESPONSE_CODE
}
}
Loading
Loading