Files
elmer/doc/claude-request-elm.md
T

500 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Запрос анализа 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<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) // 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<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): 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<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. 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<String>()
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<String>? {
if (!connectAndInit()) { disconnect(); return null }
val codes = mutableListOf<String>()
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<Int>, 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<Int>()
var hadErrors = false; var reliable = true
val reasons = mutableListOf<String>()
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<Long>()
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()
// ... контроль качества, слияние со статикой ...
}
}
```