10 KiB
10 KiB
Запрос к Claude Sonnet — ТОЛЬКО АНАЛИЗ
⛔ ЗАПРЕЩЕНО МЕНЯТЬ КОД ⛔
⛔ НЕ ДЕЛАТЬ КОММИТЫ ⛔
⛔ НЕ ПРАВИТЬ ФАЙЛЫ ⛔
⛔ ТОЛЬКО АНАЛИЗ — вывод в файл doc/claude-analysis-elm-v2.md ⛔
Ты — эксперт по ELM327 и OBD2. Тебе дан код Android-приложения (Kotlin). НАЙДИ БАГИ, ОБЪЯСНИ, ДАЙ РЕКОМЕНДАЦИИ. Код не менять.
Проблема
Динамический тест: опрос 3 PID (010C RPM, 0110 MAF, 0106 STFT) в цикле. ELM327 v1.5 замолкает.
Реальные данные с машины
v1.3.0-dev (drainInput в каждой write, ATWS+300ms):
0106: 1/18 ok 010C: 1/18 ok 0110: 0/18 ok
Первые 2 ответа — данные, дальше 16 пустых.
v1.9.0-dev (drainInput отключён, без ATWS):
0106: 0/15 ok 010C: 0/15 ok 0110: 0/15 ok ← СТАЛО ХУЖЕ
ВСЕ 15 пустые.
Статическая диагностика — одиночные PID — работает идеально.
Код
Все файлы в android/app/src/main/java/ru/elmer/client/.
1. ElmProtocol.kt (elm/ElmProtocol.kt) — Стейт-машина AndrOBD
package ru.elmer.client.elm
import android.util.Log
import java.io.InputStream
import java.io.OutputStream
class ElmProtocol(
private val input: InputStream,
private val output: OutputStream
) {
companion object {
private const val TAG = "ElmProto"
private const val POLL_DELAY = 1L
private const val INIT_TIMEOUT = 10000L
private const val DEF_TIMEOUT = 500L
private const val TIMEOUT_MIN = 50L
private const val TIMEOUT_MAX = 2000L
private const val TIMEOUT_STEP = 20L
private const val TIMEOUT_RES = 4
private const val MAX_RETRIES = 10
}
private enum class State { UNDEFINED, INITIALIZING, READY, BUSY, ERROR, DISCONNECTED }
private var state = State.UNDEFINED
private var timeoutMs = DEF_TIMEOUT
private var learnedMin = TIMEOUT_MIN
fun init() {
state = State.INITIALIZING
write("ATSP0"); tryRead(4000); drainInput()
write("ATAT1"); tryRead(2000); drainInput()
updateAtst()
write("ATS0"); tryRead(2000); drainInput()
write("ATL0"); tryRead(2000); drainInput()
write("ATE0"); tryRead(2000); drainInput()
state = State.READY
}
fun sendCommand(cmd: String): String {
if (state == State.ERROR || state == State.DISCONNECTED) recover()
state = State.BUSY
val result = exec(cmd, timeoutMs)
if (state == State.BUSY) state = State.READY
return result
}
private fun exec(cmd: String, timeout: Long): String {
write(cmd)
var t = timeout
for (i in 0 until MAX_RETRIES) {
try {
return handle(read(t))
} catch (_: TimeoutException) {
if (state == State.INITIALIZING) t += 1000
else { increaseTimeout(); t = timeoutMs }
}
}
state = State.ERROR
return ""
}
private fun handle(raw: String): String {
val u = raw.uppercase().trim()
when {
u.startsWith("SEARCHING") -> {}
u.startsWith("OK") -> decreaseTimeout()
u.startsWith("NODATA") || u.startsWith("NO DATA") -> { increaseTimeout(); updateAtst() }
isBusError(u) -> {
state = State.DISCONNECTED; resetTimeout(); updateAtst()
write("ATPC"); tryRead(3000); write("ATSP0"); tryRead(3000)
}
u.startsWith("ERROR") && !u.startsWith("DATA ERROR") -> { state = State.ERROR; write("ATWS"); tryRead(3000) }
isDataError(u) -> { state = State.ERROR; write("ATWS"); tryRead(3000) }
else -> decreaseTimeout()
}
return raw
}
private fun recover() {
state = State.INITIALIZING
write("ATWS"); tryRead(2000); drainInput()
write("ATSP0"); tryRead(2000); drainInput()
write("ATE0"); tryRead(2000); drainInput()
state = State.READY
}
private fun write(cmd: String) {
drainInput()
output.write((cmd + "\r").toByteArray())
output.flush()
}
private fun drainInput() {
while (input.available() > 0) input.read()
}
@Throws(TimeoutException::class)
private fun read(timeout: Long): String {
val dl = System.currentTimeMillis() + timeout
val sb = StringBuilder()
val lines = mutableListOf<String>()
var gotPrompt = false
while (System.currentTimeMillis() < dl) {
if (input.available() > 0) {
val b = input.read()
if (b == -1) break
when (b) {
62 -> { push(sb, lines); gotPrompt = true; break }
13 -> push(sb, lines)
10, 32 -> {}
else -> sb.append(b.toChar())
}
} else { Thread.sleep(POLL_DELAY) }
}
push(sb, lines)
if (!gotPrompt) throw TimeoutException("timeout ${timeout}ms")
return lines.joinToString("\n")
}
private fun tryRead(timeout: Long) { try { read(timeout) } catch (_: TimeoutException) {} }
private fun push(sb: StringBuilder, lines: MutableList<String>) {
if (sb.isNotEmpty()) { lines.add(sb.toString()); sb.clear() }
}
private fun increaseTimeout() { if (timeoutMs + TIMEOUT_STEP < TIMEOUT_MAX) timeoutMs += TIMEOUT_STEP }
private fun decreaseTimeout() { if (timeoutMs - TIMEOUT_STEP >= learnedMin) timeoutMs -= TIMEOUT_STEP }
private fun resetTimeout() { timeoutMs = DEF_TIMEOUT }
fun resetAdaptiveTiming() { timeoutMs = DEF_TIMEOUT }
private fun updateAtst() {
val v = (timeoutMs / TIMEOUT_RES).toInt().coerceAtLeast(1)
write("ATST${v.toString(16).uppercase().padStart(2, '0')}")
tryRead(2000); drainInput()
}
private fun isBusError(s: String) = listOf("UNABLE","BUS BUSY","BUS ERROR","CAN ERROR","BUS INIT","STOPPED").any { s.startsWith(it) }
private fun isDataError(s: String) = listOf("DATA ERROR","BUFFER FULL","RX ERROR").any { s.startsWith(it) }
}
class TimeoutException(message: String) : Exception(message)
2. DynamicCollector.kt (script/DynamicCollector.kt)
package ru.elmer.client.script
import ru.elmer.client.elm.ElmProtocol
import ru.elmer.client.elm.ObdDecoder
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
class DynamicCollector(
private val elm: ElmProtocol,
private val steps: List<ElmStep>,
private val intervalMs: Long,
private val onSample: (sampleIndex: Int) -> Unit,
private val onLog: (msg: String) -> Unit
) {
data class ElmStep(val id: String, val cmd: String, val desc: String)
private val running = AtomicBoolean(false)
private val samples = mutableListOf<List<SampleResponse>>()
private var threadRef: Thread? = null
data class SampleResponse(val stepId: String, val cmd: String, val raw: String, val decoded: String, val ts: Long = 0)
fun start() {
running.set(true)
val startTs = System.currentTimeMillis()
threadRef = thread(name = "DynamicCollector", isDaemon = true) {
var idx = 0
while (running.get()) {
val t0 = System.currentTimeMillis()
val batch = mutableListOf<SampleResponse>()
for (step in steps) {
if (!running.get()) break
try {
val raw = elm.sendCommand(step.cmd)
val dec = ObdDecoder.decode(step.cmd, raw)
batch.add(SampleResponse(step.id, step.cmd, raw, dec, System.currentTimeMillis() - startTs))
} catch (e: Exception) {
batch.add(SampleResponse(step.id, step.cmd, "(err)", e.message ?: "error", System.currentTimeMillis() - startTs))
}
Thread.sleep(350)
}
if (batch.isNotEmpty()) { synchronized(samples) { samples.add(batch) }; onSample(idx); idx++ }
val elapsed = System.currentTimeMillis() - t0
val sleep = intervalMs - elapsed
if (sleep > 0 && running.get()) Thread.sleep(sleep)
}
}
}
fun stop(): List<List<SampleResponse>> {
running.set(false)
try { threadRef?.join(3000) } catch (_: Exception) {}
return synchronized(samples) { samples.toList() }
}
fun isRunning(): Boolean = running.get()
}
3. MainActivity.kt — startDynamicRecording() (фрагмент)
// v1.10.0-dev — текущая версия
private fun startDynamicRecording() {
thread(name = "DynamicTest", isDaemon = true) {
checker.ensureConnected()
val elmProto = checker.getElm()!!
// Статика — 9 PID по одному (работает)
for ((pid, desc) in staticCmds) {
elmProto.sendCommand("01$pid")
}
// Подготовка к динамике
try { elmProto.sendCommand("ATWS") } catch (_: Exception) {}
Thread.sleep(800)
// Динамика: 3 PID, интервал 500ms
val dynSteps = listOf("010C" to "RPM", "0110" to "MAF", "0106" to "STFT")
.map { ElmStep(it.second, it.first, it.second) }
DynamicCollector(elmProto, dynSteps, 500L, ...).start()
}
}
Вопросы
- exec() делает retry с ПОВТОРНОЙ ОТПРАВКОЙ команды — не забивает ли это ELM327?
- drainInput() в write() — почему без него (v1.9.0) стало ХУЖЕ?
- ATWS — нужен ли? Сколько ждать?
- sleep(350) между PID — правильно или избыточно?
- Почему статика работает а динамика нет?