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

19 KiB
Raw Blame History

Запрос анализа 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)

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 — сборщик для динамического теста

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

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)

// Вызывается при клике на светофор 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()
        // ... контроль качества, слияние со статикой ...
    }
}