refactor: move legacy docs to doc/legacy/ with opus/sonnet subfolders
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
# Запрос к 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 ⛔
|
||||
Reference in New Issue
Block a user