From bc928defdbc70e06880863ce5e83fe309d1392d2 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sat, 13 Jun 2026 10:27:30 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20?= =?UTF-8?q?Claude=20v2=20=E2=80=94=20=D0=A1=D0=A2=D0=A0=D0=9E=D0=93=D0=9E?= =?UTF-8?q?=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7,=20=D0=BA=D0=BE=D0=B4=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=8F=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/claude-request-elm-v2.md | 306 +++++++++++++++++++++++++++++------ 1 file changed, 255 insertions(+), 51 deletions(-) diff --git a/doc/claude-request-elm-v2.md b/doc/claude-request-elm-v2.md index ca627dd..368fde7 100644 --- a/doc/claude-request-elm-v2.md +++ b/doc/claude-request-elm-v2.md @@ -1,73 +1,277 @@ -# Запрос к Claude Sonnet — поиск багов в ELM-коде +# Запрос к Claude Sonnet — ТОЛЬКО АНАЛИЗ -## Контекст +## ⛔ ЗАПРЕЩЕНО МЕНЯТЬ КОД ⛔ +## ⛔ НЕ ДЕЛАТЬ КОММИТЫ ⛔ +## ⛔ НЕ ПРАВИТЬ ФАЙЛЫ ⛔ +## ⛔ ТОЛЬКО АНАЛИЗ — вывод в файл `doc/claude-analysis-elm-v2.md` ⛔ -Android-приложение для OBD2-диагностики через ELM327 Bluetooth. -Стек: Kotlin, minSdk 24. +Ты — эксперт по ELM327 и OBD2. Тебе дан код Android-приложения (Kotlin). НАЙДИ БАГИ, ОБЪЯСНИ, ДАЙ РЕКОМЕНДАЦИИ. Код не менять. -**Проблема:** динамический тест (опрос 3 PID в цикле) — ELM327 v1.5 перестаёт отвечать. +--- -### Данные тестов (реальные сессии из БД) +## Проблема -**v1.3.0-dev (#54):** статика работает, ATWS + 300ms перед динамикой +Динамический тест: опрос 3 PID (010C RPM, 0110 MAF, 0106 STFT) в цикле. ELM327 v1.5 замолкает. + +### Реальные данные с машины + +**v1.3.0-dev** (drainInput в каждой write, ATWS+300ms): ``` -0106 (STFT): 1 ok / 18 попыток -010C (RPM): 1 ok / 18 попыток -0110 (MAF): 0 ok / 18 попыток -Первые 2 ответа — данные, затем 16 пустых. +0106: 1/18 ok 010C: 1/18 ok 0110: 0/18 ok +Первые 2 ответа — данные, дальше 16 пустых. ``` -**v1.9.0-dev (#56):** без ATWS, без drain, без sleep(350) +**v1.9.0-dev** (drainInput отключён, без ATWS): ``` -0106 (STFT): 0 ok / 15 попыток ← СТАЛО ХУЖЕ -010C (RPM): 0 ok / 15 попыток -0110 (MAF): 0 ok / 15 попыток -ВСЕ 15 ответов пустые. ELM вообще не отвечает. +0106: 0/15 ok 010C: 0/15 ok 0110: 0/15 ok ← СТАЛО ХУЖЕ +ВСЕ 15 пустые. ``` -### Что изменилось в v1.9.0 -1. Убрали `ATWS` перед динамикой (заменили на `ATE0`/`ATL0`/`ATS0`) -2. Убрали `Thread.sleep(350)` между командами внутри батча -3. Добавили `setDrainBeforeWrite(false)` — drainInput() не вызывается в write() -4. Вызвали `drainInput()` один раз перед стартом DynamicCollector +**Статическая диагностика** — одиночные PID — работает идеально. -### Статика работает идеально (сессия #55, v1.9.0-dev) -Все 9 PID (0104-0111) — ok. Проблема ТОЛЬКО в DynamicCollector. +--- ## Код -Файлы лежат в `android/app/src/main/java/ru/elmer/client/`: +Все файлы в `android/app/src/main/java/ru/elmer/client/`. -### ElmProtocol.kt (elm/ElmProtocol.kt) — ПОЛНОСТЬЮ ВАЖЕН -Стейт-машина AndrOBD. Методы: `sendCommand()` → `exec()` → `write()` → `read()`. -Ключевое: `write()` вызывает `drainInput()` перед каждой командой. -`exec()` делает до 10 retry при таймауте (каждый retry = новый write()). +### 1. ElmProtocol.kt (elm/ElmProtocol.kt) — Стейт-машина AndrOBD -### DynamicCollector.kt (script/DynamicCollector.kt) — ПОЛНОСТЬЮ ВАЖЕН -Цикл опроса: `while(running) { for(step in steps) { elm.sendCommand(step.cmd) } sleep(interval) }` -Сейчас без sleep между командами внутри батча. +```kotlin +package ru.elmer.client.elm -### MainActivity.kt (ui/MainActivity.kt) — строки 344-470 -Метод `startDynamicRecording()`. Поток выполнения: -1. `checker.ensureConnected()` — переподключение если нужно -2. Статический проброс 9 PID (каждый через sendCommand) -3. ATE0/ATL0/ATS0 — переинициализация -4. `elmProto.drainInput()` — однократная очистка -5. `elmProto.setDrainBeforeWrite(false)` — отключаем drain -6. `DynamicCollector.start()` — цикл опроса -7. После stop: `elmProto.setDrainBeforeWrite(true)` +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. **Почему v1.9.0 стал ХУЖЕ чем v1.3.0?** В v1.3.0 было ATWS+300ms и ELM отвечал хотя бы на первые 2 команды. В v1.9.0 — ни одного ответа. +1. **exec()** делает retry с ПОВТОРНОЙ ОТПРАВКОЙ команды — не забивает ли это ELM327? +2. **drainInput()** в write() — почему без него (v1.9.0) стало ХУЖЕ? +3. **ATWS** — нужен ли? Сколько ждать? +4. **sleep(350)** между PID — правильно или избыточно? +5. Почему статика работает а динамика нет? -2. **Нужен ли ATWS перед динамикой?** Если да — сколько ждать после него? - -3. **Нужен ли drainInput() в write()?** Или достаточно одного drain'а перед стартом? Не съедает ли drainInput() ответы от медленного ELM? - -4. **Почему статика работает а динамика нет?** Разница: статика = одиночные sendCommand с паузами между шагами. Динамика = 3 sendCommand подряд в цикле. - -5. **Правильно ли `exec()` делает retry с повторной отправкой команды?** Не забивает ли это ELM327? - -Выдай СВОЙ анализ и КОНКРЕТНЫЕ исправления в файл `doc/claude-analysis-elm-v2.md`. -Не пиши общие рекомендации — дай точные правки кода. +## ⛔ НАПОМИНАНИЕ: НЕ МЕНЯТЬ КОД, НЕ КОММИТИТЬ. ТОЛЬКО АНАЛИЗ В ФАЙЛ doc/claude-analysis-elm-v2.md ⛔