278 lines
10 KiB
Markdown
278 lines
10 KiB
Markdown
# Запрос к 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<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. **exec()** делает retry с ПОВТОРНОЙ ОТПРАВКОЙ команды — не забивает ли это ELM327?
|
|
2. **drainInput()** в write() — почему без него (v1.9.0) стало ХУЖЕ?
|
|
3. **ATWS** — нужен ли? Сколько ждать?
|
|
4. **sleep(350)** между PID — правильно или избыточно?
|
|
5. Почему статика работает а динамика нет?
|
|
|
|
## ⛔ НАПОМИНАНИЕ: НЕ МЕНЯТЬ КОД, НЕ КОММИТИТЬ. ТОЛЬКО АНАЛИЗ В ФАЙЛ doc/claude-analysis-elm-v2.md ⛔
|