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

10 KiB

Запрос к 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

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)

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() (фрагмент)

// 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