|
|
@@ -35,6 +35,8 @@ class ObdService(
|
|
|
private const val CONNECT_TIMEOUT_MS = 12_000L
|
|
|
private const val MAX_FAILURES = 3
|
|
|
private const val AMBIENT_EVERY = 30
|
|
|
+ private const val FUEL_RATE_PROBE_EVERY = 30
|
|
|
+ private const val MAX_FUEL_RATE_MISSES = 5
|
|
|
}
|
|
|
|
|
|
private val socketLock = Any()
|
|
|
@@ -53,6 +55,7 @@ class ObdService(
|
|
|
@Volatile private var mafGramsPerSecond: Double? = null
|
|
|
@Volatile private var fuelRateLph: Double? = null
|
|
|
@Volatile private var fuelRateSupported: Boolean? = null
|
|
|
+ private var fuelRateMisses = 0
|
|
|
@Volatile private var ambientCelsius: Double? = null
|
|
|
@Volatile private var error: String? = null
|
|
|
@Volatile private var lastPollAt: Long = 0L
|
|
|
@@ -156,8 +159,8 @@ class ObdService(
|
|
|
|
|
|
fuelRateLph = null
|
|
|
fuelRateSupported = null
|
|
|
+ fuelRateMisses = 0
|
|
|
handshake()
|
|
|
- detectFuelRateSupport()
|
|
|
if (!running) {
|
|
|
closeSocket()
|
|
|
return
|
|
|
@@ -234,6 +237,11 @@ class ObdService(
|
|
|
if (!responded) {
|
|
|
throw IllegalStateException("OBD adapter did not respond")
|
|
|
}
|
|
|
+ // ATSP0 can start a protocol search on the first OBD request. Let it
|
|
|
+ // finish with a generous timeout so the search response is not left in
|
|
|
+ // the socket to be mistaken for the next PID's answer.
|
|
|
+ send("0100")
|
|
|
+ readResponse(3_000L)
|
|
|
}
|
|
|
|
|
|
private fun pollLoop() {
|
|
|
@@ -242,13 +250,28 @@ class ObdService(
|
|
|
val speed = readPid("010D", 1)?.firstOrNull()?.toInt()?.and(0xFF)?.toDouble()
|
|
|
// PID 5E is the ECU's own fuel rate. It matches the car's trip
|
|
|
// computer and is the only reliable source for a diesel engine.
|
|
|
- val fuelRate = if (fuelRateSupported != false) {
|
|
|
+ // Probe it occasionally when it is unsupported or still unknown so
|
|
|
+ // a slow/unsupported PID cannot add a timeout to every poll.
|
|
|
+ val probeFuelRate =
|
|
|
+ fuelRateSupported == true || tick % FUEL_RATE_PROBE_EVERY == 0
|
|
|
+ val fuelRate = if (probeFuelRate) {
|
|
|
readPid("015E", 2)?.let { bytes ->
|
|
|
(((bytes[0].toInt() and 0xFF) shl 8) or (bytes[1].toInt() and 0xFF)) / 20.0
|
|
|
}
|
|
|
} else {
|
|
|
null
|
|
|
}
|
|
|
+ if (probeFuelRate) {
|
|
|
+ if (fuelRate != null) {
|
|
|
+ fuelRateSupported = true
|
|
|
+ fuelRateMisses = 0
|
|
|
+ } else {
|
|
|
+ fuelRateMisses++
|
|
|
+ if (fuelRateMisses >= MAX_FUEL_RATE_MISSES) {
|
|
|
+ fuelRateSupported = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
// Keep MAF next to speed and fuel rate so a transient sample pairs
|
|
|
// the economy inputs as closely as possible.
|
|
|
val maf = readPid("0110", 2)?.let { bytes ->
|
|
|
@@ -273,16 +296,6 @@ class ObdService(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * Reads the supported-PID bitmap for 0x41-0x60. PID 0x5E is bit 2 of the
|
|
|
- * fourth byte; skipping unsupported PID reads keeps the poll fast.
|
|
|
- */
|
|
|
- private fun detectFuelRateSupport() {
|
|
|
- val bitmap = readPid("0140", 4) ?: return
|
|
|
- val fourth = bitmap[3].toInt() and 0xFF
|
|
|
- fuelRateSupported = (fourth and 0x04) != 0
|
|
|
- }
|
|
|
-
|
|
|
private fun readThrottle(): Double? {
|
|
|
val pedal = readPid("0149", 1)?.firstOrNull()?.toInt()?.and(0xFF)
|
|
|
if (pedal != null) return pedal * 100.0 / 255.0
|
|
|
@@ -316,10 +329,31 @@ class ObdService(
|
|
|
|
|
|
private fun send(command: String) {
|
|
|
val out = output ?: throw IllegalStateException("OBD socket is not open")
|
|
|
+ // A late response from the previous command must not be mistaken for
|
|
|
+ // this command's answer; drop anything still queued first.
|
|
|
+ drainInput()
|
|
|
out.write("$command\r".toByteArray())
|
|
|
out.flush()
|
|
|
}
|
|
|
|
|
|
+ private fun drainInput() {
|
|
|
+ val source = input ?: return
|
|
|
+ try {
|
|
|
+ val scratch = ByteArray(256)
|
|
|
+ var quietUntil = System.currentTimeMillis() + 40
|
|
|
+ while (System.currentTimeMillis() < quietUntil) {
|
|
|
+ if (source.available() > 0) {
|
|
|
+ if (source.read(scratch) <= 0) break
|
|
|
+ quietUntil = System.currentTimeMillis() + 40
|
|
|
+ } else {
|
|
|
+ Thread.sleep(5)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (_: Throwable) {
|
|
|
+ // The next read/write will surface a socket that has gone away.
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private fun readResponse(timeoutMs: Long): String {
|
|
|
val source = input ?: return ""
|
|
|
val deadline = System.currentTimeMillis() + timeoutMs
|