docs: запрос Claude v2 — СТРОГО только анализ, код не менять
This commit is contained in:
+255
-51
@@ -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<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)
|
||||
|
||||
```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<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() (фрагмент)
|
||||
|
||||
```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 ⛔
|
||||
|
||||
Reference in New Issue
Block a user