-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Add obd plugin #20818
Changes from 3 commits
52cc734
4916da4
3ab34d7
ea37e1f
7dedf0d
6127ee8
de75b9d
834aa88
08f28cb
a74ba69
6b0d2d7
1fb7543
b482a85
5b5ef9b
4320dfc
ed3ae95
a1f3aef
261de6a
b52b311
602138b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package net.osmand.shared.obd | ||
|
||
enum class OBDCommand( | ||
val command: String, | ||
private val responseParser: (String) -> String, | ||
val isStale: Boolean = false) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. staleCommandsCache looks useless, since isStale is always false. Also staleCommandsCache never clears. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't scope?.cancel() enough? |
||
scope?.cancel() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
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 | ||
} | ||
} |
There was a problem hiding this comment.
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