diff --git a/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java b/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java index e3eb091ef0e..5e9c2a090b7 100644 --- a/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java +++ b/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java @@ -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_VEHICLE_METRICS = "osmand.vehicle.metrics"; String PLUGIN_WEATHER = "osmand.weather"; //Settings: diff --git a/OsmAnd-shared/build.gradle.kts b/OsmAnd-shared/build.gradle.kts index 36cc990b5f7..8ed7c9582c9 100644 --- a/OsmAnd-shared/build.gradle.kts +++ b/OsmAnd-shared/build.gradle.kts @@ -73,7 +73,6 @@ kotlin { implementation("androidx.sqlite:sqlite:$sqliteVersion") implementation("androidx.sqlite:sqlite-framework:$sqliteVersion") implementation("net.sf.kxml:kxml2:$kxml2Version") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") } iosMain.dependencies { implementation("co.touchlab:sqliter-driver:$sqliterVersion") diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/FuelType.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/FuelType.kt new file mode 100644 index 00000000000..2ac9c1636f5 --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/FuelType.kt @@ -0,0 +1,43 @@ +package net.osmand.shared.obd + +import net.osmand.shared.util.Localization + +enum class FuelType(val code: String, val screenNamId: String) { + NO_PROVIDED("00", "obd_fuel_type_not_provided"), + GASOLINE("01", "obd_fuel_type_gasoline"), + METHANOL("02", "obd_fuel_type_methanol"), + ETHANOL("03", "obd_fuel_type_ethanol"), + DIESEL("04", "obd_fuel_type_diesel"), + LPG("05", "obd_fuel_type_lpg"), + CNG("06", "obd_fuel_type_cng"), + PROPANE("07", "obd_fuel_type_propane"), + ELECTRIC("08", "obd_fuel_type_electric"), + BIFUEL_RUNNING_GASOLINE("09", "obd_fuel_type_bifuel_gasoline"), + BIFUEL_RUNNING_METHANOL("0A", "obd_fuel_type_bifuel_methanol"), + BIFUEL_RUNNING_ETHANOL("0B", "obd_fuel_type_bifuel_ethanol"), + BIFUEL_RUNNING_LPG("0C", "obd_fuel_type_bifuel_lpg"), + BIFUEL_RUNNING_CNG("0D", "obd_fuel_type_bifuel_cng"), + BIFUEL_RUNNING_PROPANE("0E", "obd_fuel_type_bifuel_propane"), + BIFUEL_RUNNING_ELECTRICITY("0F", "obd_fuel_type_bifuel_electricity"), + BIFUEL_RUNNING_ELECTRIC_COMBUSTION_ENGINE("10", "obd_fuel_type_bifuel_electric_combustion"), + HYBRID_GASOLINE("11", "obd_fuel_type_hybrid_gasoline"), + HYBRID_ETHANOL("12", "obd_fuel_type_hybrid_ethanol"), + HYBRID_DIESEL("13", "obd_fuel_type_hybrid_diesel"), + HYBRID_ELECTRIC("14", "obd_fuel_type_hybrid_electric"), + HYBRID_ELECTRIC_COMBUSTION_ENGINE("15", "obd_fuel_type_hybrid_electric_combustion"), + HYBRID_REGENERATIVE("16", "obd_fuel_type_hybrid_regenerative"), + BIFUEL_RUNNING_HYDROGEN("17", "obd_fuel_type_bifuel_hydrogen"), + HYBRID_HYDROGEN("18", "obd_fuel_type_hybrid_hydrogen"), + HYDROGEN("19", "obd_fuel_type_hydrogen"), + UNKNOWN("FF", "obd_fuel_type_unknown"); + + fun getDisplayName(): String { + return Localization.getString(screenNamId) + } + + companion object { + fun fromCode(code: String): FuelType { + return FuelType.entries.find { it.code == code.uppercase()} ?: UNKNOWN + } + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDCommand.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDCommand.kt new file mode 100644 index 00000000000..0fb526e88e4 --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDCommand.kt @@ -0,0 +1,23 @@ +package net.osmand.shared.obd + +enum class OBDCommand( + val command: String, + private val responseParser: (String) -> String, + val isStale: Boolean = false) { + OBD_SUPPORTED_LIST1_COMMAND("00", OBDUtils::parseSupportedCommandsResponse, true), + OBD_SUPPORTED_LIST2_COMMAND("20", OBDUtils::parseSupportedCommandsResponse, true), + OBD_SUPPORTED_LIST3_COMMAND("40", OBDUtils::parseSupportedCommandsResponse, true), + OBD_BATTERY_VOLTAGE_COMMAND("42", OBDUtils::parseBatteryVoltageResponse), + OBD_AMBIENT_AIR_TEMPERATURE_COMMAND("46", OBDUtils::parseAmbientTempResponse), + OBD_RPM_COMMAND("0C", OBDUtils::parseRpmResponse), + OBD_SPEED_COMMAND("0D", OBDUtils::parseSpeedResponse), + OBD_AIR_INTAKE_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, true), + OBD_FUEL_LEVEL_COMMAND("2F", OBDUtils::parseFuelLevelResponse); + + fun parseResponse(response: String): String { + return responseParser.invoke(response.lowercase()) + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataComputer.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataComputer.kt new file mode 100644 index 00000000000..e31b356a1b8 --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataComputer.kt @@ -0,0 +1,320 @@ +package net.osmand.shared.obd + +import net.osmand.shared.data.KLatLon +import net.osmand.shared.extensions.currentTimeMillis +import net.osmand.shared.extensions.format +import net.osmand.shared.obd.OBDDataComputer.OBDTypeWidget.* +import net.osmand.shared.util.KCollectionUtils +import net.osmand.shared.util.KMapUtils +import net.osmand.shared.util.LoggerFactory +import kotlin.math.max +import net.osmand.shared.obd.OBDCommand.* + +object OBDDataComputer { + + private val log = LoggerFactory.getLogger("OBDDataComputer") + + var locations = listOf() + var widgets: List = ArrayList() + private set + var timeoutForInstantValuesSeconds = 0 + + class OBDLocation(val time: Long, val latLon: KLatLon) + + fun acceptValue(value: Map) { + for (widget in widgets) { + widget.acceptValue(value) + } + compute() + } + + fun registerLocation(l: OBDLocation) { + if (widgets.isNotEmpty()) { + // concurrency - change collection in one thread. Other places only read + locations = KCollectionUtils.addToList(locations, l) + cleanupLocations() + } + } + + fun compute() { + val now: Long = currentTimeMillis() + for (widget in widgets) { + widget.cleanup(now) + widget.computeValue() + } + } + + private fun cleanupLocations() { + val now: Long = currentTimeMillis() + // calculate maximum window to clean up + var window = timeoutForInstantValuesSeconds + for (widget in widgets) { + if (widget.type.locationNeeded) { + window = max(window, widget.averageTimeSeconds) + } + } + var inWindow = 0 + while (inWindow < locations.size) { + if (locations[inWindow].time >= now - window * 1000) { + break + } + inWindow++ + } + if (inWindow - 1 > 0) { + locations = locations.subList(inWindow - 1, locations.size) + } + } + + fun registerWidget( + type: OBDTypeWidget, + averageTimeSeconds: Int, + formatter: OBDComputerWidgetFormatter = OBDComputerWidgetFormatter() + ): OBDComputerWidget { + val widget = OBDComputerWidget(formatter, type, averageTimeSeconds) + widgets = KCollectionUtils.addToList(widgets, widget) + updateRequiredCommands() + return widget + } + + fun removeWidget(w: OBDComputerWidget) { + widgets = KCollectionUtils.removeFromList(widgets, w) + updateRequiredCommands() + } + + private fun updateRequiredCommands() { + OBDDispatcher.clearCommands() + widgets.forEach { widget -> + widget.type.requiredCommands.forEach { OBDDispatcher.addCommand(it) } + } + } + + enum class OBDTypeWidget( + val locationNeeded: Boolean, + val requiredCommands: List, + val valueCreator: (Map) -> OBDValue) { + SPEED(false, + listOf(OBD_SPEED_COMMAND), + { data -> OBDIntValue(OBD_SPEED_COMMAND, data) }), + RPM(false, + listOf(OBD_RPM_COMMAND), + { data -> OBDIntValue(OBD_RPM_COMMAND, data) }), + FUEL_LEFT_DISTANCE(true, + listOf(OBD_FUEL_LEVEL_COMMAND), + { data -> OBDValue(OBD_FUEL_LEVEL_COMMAND, data) }), + FUEL_LEFT_LITERS(false, + listOf(OBD_FUEL_LEVEL_COMMAND), + { data -> OBDValue(OBD_FUEL_LEVEL_COMMAND, data) }), + FUEL_LEFT_PERCENT(false, + listOf(OBD_FUEL_LEVEL_COMMAND), + { data -> OBDValue(OBD_FUEL_LEVEL_COMMAND, data) }), + FUEL_CONSUMPTION_RATE(false, + listOf(OBD_FUEL_LEVEL_COMMAND), + { data -> OBDValue(OBD_FUEL_LEVEL_COMMAND, data) }), + TEMPERATURE_INTAKE(false, + listOf(OBD_AIR_INTAKE_TEMP_COMMAND), + { data -> OBDIntValue(OBD_AIR_INTAKE_TEMP_COMMAND, data) }), + TEMPERATURE_AMBIENT(false, + listOf(OBD_AMBIENT_AIR_TEMPERATURE_COMMAND), + { data -> OBDIntValue(OBD_AMBIENT_AIR_TEMPERATURE_COMMAND, data) }), + BATTERY_VOLTAGE(false, + listOf(OBD_BATTERY_VOLTAGE_COMMAND), + { data -> OBDValue(OBD_BATTERY_VOLTAGE_COMMAND, data) }), + FUEL_TYPE(false, + listOf(OBD_FUEL_TYPE_COMMAND), + { data -> OBDIntValue(OBD_FUEL_TYPE_COMMAND, data) }), + TEMPERATURE_COOLANT(false, + listOf(OBD_ENGINE_COOLANT_TEMP_COMMAND), + { data -> OBDIntValue(OBD_ENGINE_COOLANT_TEMP_COMMAND, data) }); + } + + private fun averageDouble(values: List): Double? = + if (values.isNotEmpty()) values.sumOf { it.doubleValue } / values.size else null + + open class OBDComputerWidgetFormatter(val pattern: String = "%s") { + open fun format(v: Any?): String { + return if (v == null) { + "-" + } else { + var ret = "" + try { + ret = pattern.format(v) + } catch (error: Throwable) { + log.error(error.message) + } + ret + } + } + } + + class OBDComputerWidget( + val formatter: OBDComputerWidgetFormatter, + val type: OBDTypeWidget, + var averageTimeSeconds: Int) { + private var values: MutableList = ArrayList() + private var value: Any? = null + private var cachedVersion = 0 + private var version = 0 + + fun computeValue(): Any? { + if (cachedVersion != version) { + val v = version + value = formatter.format(compute()) + cachedVersion = v + } + return value + } + + private fun compute(): Any? { + val locValues = ArrayList(values) + return when (type) { + TEMPERATURE_AMBIENT, + TEMPERATURE_COOLANT, + TEMPERATURE_INTAKE, + SPEED, + BATTERY_VOLTAGE, + RPM -> { + if (averageTimeSeconds == 0 && locValues.size > 0) { + locValues[locValues.size - 1].doubleValue + } else { + averageDouble(locValues) + } + } + + FUEL_CONSUMPTION_RATE -> { + if (locValues.size >= 2) { + val first = locValues[0] + val last = locValues[locValues.size - 1] + val diffPerc = first.doubleValue - last.doubleValue + val diffTime = last.timestamp - first.timestamp + println("diftime $diffTime; diffPerc $diffPerc") + diffPerc / diffTime * 1000 * 3600 + } else { + null + } + } + + FUEL_LEFT_DISTANCE -> { + // works for window > 15 min + if (locValues.size >= 2) { + val first = locValues[0] + val last = locValues[locValues.size - 1] + val diffPerc = last.doubleValue - first.doubleValue + if (diffPerc > 0) { + var start = 0 + var end = locations.size - 1 + while (start < locations.size) { + if (locations[start].time > first.timestamp) { + break + } + start++ + } + while (end >= 0) { + if (locations[end].time < last.timestamp) { + break + } + end-- + } + var dist = 0.0 + if (start < end) { + for (k in start until end) { + dist += KMapUtils.getDistance( + locations[start].latLon, + locations[end].latLon) + } + } + if (dist > 0) { + val lastPerc = last.doubleValue + lastPerc / diffPerc * dist + } + } + } + null + } + + FUEL_LEFT_LITERS, + FUEL_LEFT_PERCENT -> { + if (locValues.size > 0) { + locValues[locValues.size - 1].doubleValue + } else { + null + } + } + + FUEL_TYPE -> { + if (locValues.size > 0) { + locValues[locValues.size - 1].doubleValue + } else { + null + } + } + } + } + + fun acceptValue(value: Map) { + val obdValue = type.valueCreator(value) + if (obdValue.isAccepted) { + version++ + values.add(obdValue) + } + } + + fun cleanup(now: Long) { + val timeout = + if (averageTimeSeconds > 0) averageTimeSeconds else timeoutForInstantValuesSeconds + var inWindow = 0 + while (inWindow < values.size) { + if (values[inWindow].timestamp >= now - timeout * 1000) { + break + } + inWindow++ + } + if (inWindow > 0 && inWindow < values.size - 1) { + values = values.subList(inWindow, values.size) + } + } + } + + open class OBDValue() { + var timestamp = currentTimeMillis() + var isAccepted: Boolean = false + protected set + var doubleValue: Double = 0.0 + + constructor(cmd: OBDCommand, data: Map) : this() { + val dataField = data[cmd] + dataField?.let { + doubleValue = it.getValue().toDouble() + } + isAccepted = acceptData(dataField) + } + + protected open fun acceptData(dataField: OBDDataField?): Boolean { + var accepted = false + dataField?.let { + try { + doubleValue = it.getValue().toDouble() + accepted = true + } catch (error: NumberFormatException) { + log.error("Can\'t parse ${it.getValue()} to Double") + } + } + return accepted + } + } + + class OBDIntValue(cmd: OBDCommand, data: Map) : OBDValue(cmd, data) { + var intValue = 0 + override fun acceptData(dataField: OBDDataField?): Boolean { + var accepted = false + dataField?.let { + try { + intValue = it.getValue().toInt() + accepted = true + } catch (error: NumberFormatException) { + log.error("Can\'t parse ${it.getValue()} to Int") + } + } + return accepted + } + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataField.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataField.kt new file mode 100644 index 00000000000..0234ced6cd4 --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataField.kt @@ -0,0 +1,17 @@ +package net.osmand.shared.obd + +open class OBDDataField( + val type: OBDDataFieldType, + private val stringValue: String) { + fun getDisplayName(): String { + return type.getDisplayName() + } + + fun getDisplayUnit(): String { + return type.getDisplayUnit() + } + + open fun getValue(): String { + return stringValue + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataFieldType.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataFieldType.kt new file mode 100644 index 00000000000..9e37cd56e4c --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDataFieldType.kt @@ -0,0 +1,22 @@ +package net.osmand.shared.obd + +import net.osmand.shared.util.Localization + +enum class OBDDataFieldType(val nameId: String, private val unitNameId: String) { + RPM("obd_rpm", "rpm_unit"), + SPEED("obd_speed_desc", "km_h"), + FUEL_LVL("obd_fuel_level", "percent_unit"), + AMBIENT_AIR_TEMP("obd_ambient_air_temp_desc", "degree_celsius"), + BATTERY_VOLTAGE("obd_battery_voltage_desc", "unit_volt"), + AIR_INTAKE_TEMP("obd_air_intake_temp_desc", "degree_celsius"), + COOLANT_TEMP("obd_engine_coolant_temp", "degree_celsius"), + FUEL_TYPE("obd_fuel_type", ""); + + fun getDisplayName(): String { + return Localization.getString(nameId) + } + + fun getDisplayUnit(): String { + return Localization.getString(unitNameId) + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDispatcher.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDispatcher.kt new file mode 100644 index 00000000000..1db5df88e74 --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDDispatcher.kt @@ -0,0 +1,164 @@ +package net.osmand.shared.obd + +import kotlinx.coroutines.CancellationException +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 kotlinx.datetime.Clock +import net.osmand.shared.extensions.currentTimeMillis +import net.osmand.shared.util.KCollectionUtils +import net.osmand.shared.util.LoggerFactory +import okio.Buffer +import okio.IOException +import okio.Sink +import okio.Source + + +object OBDDispatcher { + + private var commandQueue = listOf() + private val staleCommandsCache: MutableMap = HashMap() + 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 var job: Job? = null + private var scope: CoroutineScope? = null + private var readStatusListener: OBDReadStatusListener? = null + private val sensorDataCache = HashMap() + + interface OBDReadStatusListener { + fun onIOError() + } + + private fun startReadObdLooper() { + job = Job() + scope = CoroutineScope(Dispatchers.IO + job!!) + scope!!.launch { + try { + log.debug("Start reading obd with $inputStream and $outputStream") + while (inputStream != null && outputStream != null) { + val inStream = inputStream!! + val outStream = outputStream!! + try { + for (command in commandQueue) { + if (command.isStale) { + val cachedCommandResponse = staleCommandsCache[command] + if (cachedCommandResponse != null) { + consumeResponse(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 = currentTimeMillis() + while (true) { + if (Clock.System.now() + .toEpochMilliseconds() - 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) { + 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") + consumeResponse(command, result) + if (command.isStale) { + staleCommandsCache[command] = result + break + } + } + log.info("response. ${command.name} **${response.replace('\\', '\\')}") + } + } catch (error: IOException) { + log.error("Run OBD looper error. $error") + readStatusListener?.onIOError() + } + OBDDataComputer.acceptValue(sensorDataCache) + } + } catch (cancelError: CancellationException) { + log.debug("OBD reading canceled") + } + } + } + + fun addCommand(commandToRead: OBDCommand) { + if (commandQueue.indexOf(commandToRead) == -1) { + commandQueue = KCollectionUtils.addToList(commandQueue, commandToRead) + } + } + + fun clearCommands() { + commandQueue = listOf() + } + + fun removeCommand(commandToStopReading: OBDCommand) { + commandQueue = KCollectionUtils.removeFromList(commandQueue, commandToStopReading) + } + + fun setReadStatusListener(listener: OBDReadStatusListener?) { + readStatusListener = listener + } + + fun setReadWriteStreams(readStream: Source, writeStream: Sink) { + scope?.cancel() + inputStream = readStream + outputStream = writeStream + startReadObdLooper() + } + + private fun consumeResponse(command: OBDCommand, result: String) { + sensorDataCache[command] = when (command) { + OBDCommand.OBD_RPM_COMMAND -> OBDDataField(OBDDataFieldType.RPM, result) + OBDCommand.OBD_SPEED_COMMAND -> OBDDataField(OBDDataFieldType.SPEED, result) + OBDCommand.OBD_AIR_INTAKE_TEMP_COMMAND -> OBDDataField(OBDDataFieldType.AIR_INTAKE_TEMP, result) + OBDCommand.OBD_ENGINE_COOLANT_TEMP_COMMAND -> OBDDataField(OBDDataFieldType.COOLANT_TEMP, result) + OBDCommand.OBD_FUEL_TYPE_COMMAND -> OBDDataField(OBDDataFieldType.FUEL_TYPE, result) + OBDCommand.OBD_FUEL_LEVEL_COMMAND -> OBDDataField(OBDDataFieldType.FUEL_LVL, result) + OBDCommand.OBD_AMBIENT_AIR_TEMPERATURE_COMMAND -> OBDDataField(OBDDataFieldType.AMBIENT_AIR_TEMP, result) + OBDCommand.OBD_BATTERY_VOLTAGE_COMMAND -> OBDDataField(OBDDataFieldType.BATTERY_VOLTAGE, result) + else -> null + } + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDFuelTypeFormatter.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDFuelTypeFormatter.kt new file mode 100644 index 00000000000..62c3f3796ab --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDFuelTypeFormatter.kt @@ -0,0 +1,15 @@ +package net.osmand.shared.obd + +import net.osmand.shared.util.Localization + +class OBDFuelTypeFormatter : OBDDataComputer.OBDComputerWidgetFormatter("%02.0f") { + override fun format(v: Any?): String { + return if (v == null) { + "-" + } else { + val code = super.format(v) + val type = FuelType.fromCode(code) + return Localization.getString(type.getDisplayName()) + } + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDResponseListener.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDResponseListener.kt new file mode 100644 index 00000000000..d197955cdb5 --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDResponseListener.kt @@ -0,0 +1,5 @@ +package net.osmand.shared.obd + +interface OBDResponseListener { + fun onCommandResponse(command: OBDCommand, result: String) +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDUtils.kt b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDUtils.kt new file mode 100644 index 00000000000..c4b44b1e404 --- /dev/null +++ b/OsmAnd-shared/src/commonMain/kotlin/net/osmand/shared/obd/OBDUtils.kt @@ -0,0 +1,108 @@ +package net.osmand.shared.obd + +import kotlin.math.roundToInt + +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_AIR_INTAKE_TEMP_COMMAND.command.lowercase()) { + val intakeAirTemp = hexValues[2].toInt(16) + return (intakeAirTemp - 40).toString() + } + return INVALID_RESPONSE_CODE + } + + fun parseAmbientTempResponse(response: String): String { + val hexValues = response.trim().split(" ") + if (hexValues[0] == "41" && hexValues[1] == OBDCommand.OBD_AMBIENT_AIR_TEMPERATURE_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) + .toFloat() / 255 * 100)).toString() + } + return INVALID_RESPONSE_CODE + } + + fun parseBatteryVoltageResponse(response: String): String { + val hexValues = response.trim().split(" ") + if (hexValues.size >= 4 && hexValues[0] == "41" && hexValues[1] == OBDCommand.OBD_BATTERY_VOLTAGE_COMMAND.command.lowercase()) { + val a = hexValues[2].toInt(16).toFloat() + val b = hexValues[3].toInt(16).toFloat() + return (((a * 256) + b) / 1000).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()) { + return responseParts[2] + } + 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 + } +} \ No newline at end of file diff --git a/OsmAnd-shared/src/commonTest/kotlin/net/osmand/shared/OBDTest.kt b/OsmAnd-shared/src/commonTest/kotlin/net/osmand/shared/OBDTest.kt new file mode 100644 index 00000000000..1443c929fde --- /dev/null +++ b/OsmAnd-shared/src/commonTest/kotlin/net/osmand/shared/OBDTest.kt @@ -0,0 +1,38 @@ +package net.osmand.shared + +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import net.osmand.shared.extensions.currentTimeMillis +import net.osmand.shared.obd.OBDCommand +import net.osmand.shared.obd.OBDDataComputer +import net.osmand.shared.obd.OBDDataComputer.OBDTypeWidget.FUEL_CONSUMPTION_RATE +import net.osmand.shared.obd.OBDDataField +import net.osmand.shared.obd.OBDDataFieldType +import kotlin.test.Test +import kotlin.test.assertTrue + +class OBDTest { + + @Test + fun testOBDComputer() = runBlocking { + val widget = OBDDataComputer.registerWidget(FUEL_CONSUMPTION_RATE, 15, OBDDataComputer.OBDComputerWidgetFormatter()) + val coef = 0.05 + var fuelLevelStart = 66.0 + val delay = 1000L + var fuelLevel = fuelLevelStart + var time: Long = currentTimeMillis() + for (i in 0 .. 600) { + val map = HashMap() + map[OBDCommand.OBD_FUEL_LEVEL_COMMAND] = OBDDataField(OBDDataFieldType.FUEL_LVL, fuelLevel.toString()) + OBDDataComputer.acceptValue(map) + val now = currentTimeMillis() + fuelLevel = fuelLevelStart - coef / (1000 / delay) * i + println("$fuelLevel - ${(now - time) / 1000.0}") + time = now + println(widget.computeValue()) + delay(delay) + } + + assertTrue { true } + } +} \ No newline at end of file diff --git a/OsmAnd/res/layout/fragment_obd_main.xml b/OsmAnd/res/layout/fragment_obd_main.xml new file mode 100644 index 00000000000..becb38e24b8 --- /dev/null +++ b/OsmAnd/res/layout/fragment_obd_main.xml @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + +