# Запрос к 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 ```kotlin 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() 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) { 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) ```kotlin 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, 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>() 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() 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> { running.set(false) try { threadRef?.join(3000) } catch (_: Exception) {} return synchronized(samples) { samples.toList() } } fun isRunning(): Boolean = running.get() } ``` ### 3. MainActivity.kt — startDynamicRecording() (фрагмент) ```kotlin // 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() } } ``` --- ## Вопросы 1. **exec()** делает retry с ПОВТОРНОЙ ОТПРАВКОЙ команды — не забивает ли это ELM327? 2. **drainInput()** в write() — почему без него (v1.9.0) стало ХУЖЕ? 3. **ATWS** — нужен ли? Сколько ждать? 4. **sleep(350)** между PID — правильно или избыточно? 5. Почему статика работает а динамика нет? ## ⛔ НАПОМИНАНИЕ: НЕ МЕНЯТЬ КОД, НЕ КОММИТИТЬ. ТОЛЬКО АНАЛИЗ В ФАЙЛ doc/claude-analysis-elm-v2.md ⛔