# Запрос анализа ELM327-кода — для Claude Sonnet ## Контекст Android-приложение для OBD2-диагностики автомобиля через ELM327 Bluetooth-адаптер. Стек: Kotlin, minSdk 24, OkHttp 4.12. **Проблема:** динамический тест (START/STOP) — опрос 3 PID (RPM, MAF, STFT) в реальном времени — даёт 94-100% пустых ответов. ELM327 v1.5 просто перестаёт отвечать. Статическая диагностика (одиночные PID) работает нормально. ## Важно АНАЛИЗИРУЙ ТОЛЬКО ELM-код. Не трогай UI, сервер, БД. Нужен анализ того, почему ELM327 замолкает при динамическом опросе. ## Файлы Ниже полный код всех файлов, связанных с ELM327. --- ### 1. ElmProtocol.kt — стейт-машина AndrOBD (1:1 копия ElmProt.java) ```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() { Log.i(TAG, "init start") 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 Log.i(TAG, "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 } } } Log.e(TAG, "no response for $cmd") 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) -> { Log.w(TAG, "BUS ERROR: ${raw.take(60)}") state = State.DISCONNECTED resetTimeout(); updateAtst() write("ATPC"); tryRead(3000) write("ATSP0"); tryRead(3000) } u.startsWith("ERROR") && !u.startsWith("DATA ERROR") -> { Log.w(TAG, "ERROR — warm start") state = State.ERROR write("ATWS"); tryRead(3000) } isDataError(u) -> { Log.w(TAG, "data error — warm start") state = State.ERROR write("ATWS"); tryRead(3000) } else -> decreaseTimeout() } return raw } private fun recover() { Log.i(TAG, "recovering...") 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() Log.d(TAG, "→ $cmd") } 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) // CR 10, 32 -> {} // LF, space 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): Boolean { return listOf("UNABLE", "BUS BUSY", "BUS ERROR", "CAN ERROR", "BUS INIT", "STOPPED").any { s.startsWith(it) } } private fun isDataError(s: String): Boolean { return listOf("DATA ERROR", "BUFFER FULL", "RX ERROR").any { s.startsWith(it) } } } class TimeoutException(message: String) : Exception(message) ``` --- ### 2. 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. ElmChecker.kt — проверка устройства + speed-test ```kotlin package ru.elmer.client.elm import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothSocket import android.util.Log import java.io.IOException import java.util.UUID class ElmChecker( private val device: BluetoothDevice, private val adapter: BluetoothAdapter ) { companion object { private const val TAG = "ElmChecker" private val SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") } data class DeviceInfo( val version: String, val deviceId: String, val protocol: String, val voltage: String, val hasAdaptive: Boolean ) data class EcuData(val supportsObd: Boolean, val pidMask: String, val vin: String?) data class Result(val good: Boolean, val device: DeviceInfo, val ecu: EcuData, val log: String) private val logLines = mutableListOf() fun getLog(): String = logLines.joinToString("\n") private var socket: BluetoothSocket? = null private var elm: ElmProtocol? = null fun checkDevice(): DeviceInfo? { if (!connectAndInit()) return null val ati = send("ATI"); val version = parseVersion(ati) val isV2 = version.contains("v2", ignoreCase = true) val deviceId = if (isV2) cleanAt2(send("AT@2")) else "—" val dp = send("ATDP"); val protocol = if (dp.length > 3 && dp != "OK") dp.take(60) else dp val rv = send("ATRV"); val voltage = if (rv.contains("V", ignoreCase = true) || rv.matches(Regex("[0-9.]+"))) rv else "—" val hasAdaptive = if (isV2) send("ATAT1") == "OK" else false return DeviceInfo(version, deviceId, protocol, voltage, hasAdaptive) } fun checkEcu(): EcuData { val pid0100 = send("0100"); val supportsObd = pid0100.startsWith("41") val pidMask = if (supportsObd) pid0100.take(60) else "—" val vinRaw = send("0902"); val vin = parseVin(vinRaw) return EcuData(supportsObd, pidMask, vin) } fun scanDtc(): List? { if (!connectAndInit()) { disconnect(); return null } val codes = mutableListOf() codes.addAll(parseDtcCodes(send("03"))) codes.addAll(parseDtcCodes(send("07"))) return codes.distinct() } fun ensureConnected(): Boolean = connectAndInit() fun isConnected(): Boolean = socket?.isConnected == true && elm != null fun getElm(): ElmProtocol? = elm fun quickCheck(): Int? { val e = elm ?: return null return try { val t0 = System.currentTimeMillis() val raw = e.sendCommand("010C"); val dt = System.currentTimeMillis() - t0 if (raw.isBlank() || raw == "(err)") null else dt.toInt() } catch (_: Exception) { null } } data class SpeedTestResult(val perPidAvg: List, val batchTime: Int, val reliable: Boolean, val message: String) fun measureResponseTime(onProgress: (String) -> Unit): SpeedTestResult { val testPids = listOf("010C" to "RPM", "0110" to "MAF", "0106" to "STFT") val perPidAvg = mutableListOf() var hadErrors = false; var reliable = true val reasons = mutableListOf() val e = elm ?: return SpeedTestResult(listOf(250,250,250), 750, false, "❌ ELM не инициализирован") try { e.sendCommand("010C") } catch (_: Exception) {} onProgress("\n⏱ Тест скорости ELM...") for ((pi, pair) in testPids.withIndex()) { val (cmd, name) = pair; val allTimes = mutableListOf() val count = if (pi == 0) 4 else 3 for (i in 0 until count) { val t0 = System.currentTimeMillis() val raw = try { e.sendCommand(cmd) } catch (_: Exception) { "(err)" } val dt = System.currentTimeMillis() - t0; allTimes.add(dt) if (raw == "(err)" || raw.isBlank()) hadErrors = true } val times = if (pi == 0) allTimes.takeLast(2).toMutableList() else allTimes val avg = times.average().toInt(); perPidAvg.add(avg) onProgress("\n $name: ${times.joinToString("ms, ")}ms (среднее ${avg}ms)") for (t in times) { if (t > 0 && avg > 0 && kotlin.math.abs(t - avg).toFloat() / avg > 0.5f) { if (reliable) reliable = false reasons.add("${name} нестабилен: ${t}ms vs среднее ${avg}ms") } } } val batchTime = perPidAvg.sum() val message = if (reliable) "✅ Скорость стабильна: ${perPidAvg.joinToString("+")}=${batchTime}ms" else "⚠️ ${reasons.joinToString("; ")}. Проверьте контакт ELM в OBD-разъёме." onProgress("\n$message") return SpeedTestResult(perPidAvg, batchTime, reliable, message) } fun close() { try { socket?.close() } catch (_: Exception) {}; socket = null; elm = null } // ... (private connect, send, parseVin, parseDtcCodes, parseVersion, failResult опущены для краткости) } ``` --- ## Проблема При динамическом тесте: ``` Сессия #54 (v1.3.0-dev): 55 ответов 0106 (STFT): 1 ok / 18 попыток → 94% ошибок 010C (RPM): 1 ok / 18 попыток → 94% ошибок 0110 (MAF): 0 ok / 18 попыток → 100% ошибок Первые 2 ответа — нормальные (410680, 41110E), затем 17 пустых. ``` **Поток вызовов перед динамическим тестом:** 1. `ElmProtocol.init()` — 5 AT-команд (ATSP0, ATAT1, ATS0, ATL0, ATE0) 2. `ElmChecker.checkDevice()` — 5-6 AT-команд (ATI, AT@2, ATDP, ATRV, ATAT1) 3. `ElmChecker.checkEcu()` — 2 OBD-команды (0100, 0902) 4. В MainActivity: статический проброс 9 PID (0104-011F) 5. Speed-test: 1 warmup + 4 RPM + 3 MAF + 3 STFT = 11 OBD-команд 6. `ATWS` — сброс ELM 7. DynamicCollector: 3 PID в цикле каждые 500ms **Ключевое:** `sendCommand()` → `exec()` при таймауте делает RETRY: - `write(cmd)` — посылает команду - `read(timeout)` — ждёт ответ - таймаут → `increaseTimeout()` → `write(cmd)` ОПЯТЬ - до 10 retry на одну команду ## Вопросы 1. Почему ELM327 v1.5 замолкает после первых 2 ответов в DynamicCollector? 2. Может ли `drainInput()` в `write()` съедать ответ от предыдущей команды? 3. Критична ли последовательность: static probe → speed-test → ATWS → dynamic collect? 4. Нужно ли переподключать ELM перед динамическим тестом вместо ATWS? 5. Как правильно реализовать динамический опрос с учётом медленного ELM327 v1.5 (min ответ 350ms)? Ответ сохрани в файл `doc/claude-analysis-elm.md` --- ### 4. MainActivity.kt — фрагменты (checkElm, checkEcu, startDynamicRecording) ```kotlin // Вызывается при клике на светофор ELM private fun checkElm() { // ... поиск Bluetooth-устройства ... elmDevice = dev try { val checker = ElmChecker(dev, btAdapter!!) val r = checker.checkDevice() // AT-команды, инициализация if (r != null) { elmChecker = checker setIndicator(indElm, "🟢") checkEcu() // ← синхронно на главном потоке! // Speed-test в фоновом потоке thread(name = "SpeedTest", isDaemon = true) { val client = ServerClient(...) val saved = client.getProfileResponseTime(elmMac) if (saved != null && saved > 0) { val quick = checker.quickCheck() // 010C × 1 // сверка с профилем... return@thread } val result = checker.measureResponseTime { msg -> debugLog(msg) } if (result.reliable) client.saveProfile(elmMac, result.batchTime) } } } catch ... } private fun checkEcu() { val checker = elmChecker ?: return try { val raw = checker.getElm()?.sendCommand("03") ?: "" // ← синхронно! val ok = raw.startsWith("43") setIndicator(indEcu, if (ok) "🟢" else "🔴") } catch ... } // Вызывается при нажатии СТАРТ private fun startDynamicRecording() { thread(name = "DynamicTest", isDaemon = true) { val checker = elmChecker if (!checker.ensureConnected()) { /* retry */ } val elmProto = checker.getElm()!! // ── Статика: пробуем 9 PID ── for ((pid, desc) in staticCmds) { val raw = elmProto.sendCommand("01$pid") // ... } // ── ATWS: сброс ELM ── try { elmProto.sendCommand("ATWS") } catch (_: Exception) {} Thread.sleep(300) // ── Динамика: 3 PID, интервал 500ms ── val dynSteps = listOf("010C" to "RPM", "0110" to "MAF", "0106" to "STFT") .map { ElmStep(it.second, it.first, it.second) } val collector = DynamicCollector(elmProto, dynSteps, 500L, ...) collector.start() while (state == State.START && collector.isRunning()) Thread.sleep(200) val samples = collector.stop() // ... контроль качества, слияние со статикой ... } } ```