From 90718df8ce46b9c014db687bdb0fb3c878976aef Mon Sep 17 00:00:00 2001 From: Repinoid Date: Wed, 10 Jun 2026 22:37:56 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=84=D0=B0=D0=B9=D0=BB=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20Claude=20Sonnet=20=E2=80=94=20=D0=B0=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=20ELM=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/claude-request-elm.md | 499 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 499 insertions(+) create mode 100644 doc/claude-request-elm.md diff --git a/doc/claude-request-elm.md b/doc/claude-request-elm.md new file mode 100644 index 0000000..1b94889 --- /dev/null +++ b/doc/claude-request-elm.md @@ -0,0 +1,499 @@ +# Запрос анализа 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() + // ... контроль качества, слияние со статикой ... + } +} +```