Files
elmer/doc/elm-reference.md

28 KiB
Raw Permalink Blame History

ELM327 Communication Patterns — анализ 5+14 проектов

Цель: понять как РЕАЛЬНО работают проекты с ELM327, выбрать лучшие паттерны для Elmer. Дата: 2026-05-27 Источники: 5 LLM-проектов + 14 традиционных OBD2 Android-проектов


Часть 1: Проекты с LLM (ELM → LLM → диагноз)

Сводная таблица

OBD-Droid OBD2AI Automotive-AI obd2-mcp-server Vehicle-Diag-Assist
Язык Java Kotlin Python Python C (W600) + Python
Платформа Android Android Desktop Desktop/Claude MCP Embedded (MCU)
LLM ChatGPT gpt-5-mini GPT-3.5/4 Claude (MCP) DeepSeek/Claude
Чтение Побайтово, 1мс kotlin-obd lib readline() Побайтово (BLE/SPP) UART, семафор
UUID 00001101... 00001101... N/A (pyserial) BLE + serial N/A (UART)
Baud config.py 38400 (auto-retry) 38400 8N1
Timeout Адаптивный 5с 400мс fix 1с 20с connect / 2с config 2000мс
Инит ATD→ATE0→ATL0→ATS0→ATH1→... ATZ→ATE0→ATL0→ATSP0 N/A ATZ→ATE0→ATL0→ATS0→ATH1→ATCAF1→ATAT1→ATST64→ATSP0 ATZ→...
Ретраи requeue + SETPROT 3 strikes → stop Нет [2,5,10]с backoff Нет
Simulator Встроенный demo Нет ELM327-emulator Mock mode (Ford) Gradio + HW sim
DTC база Встроенная Нет Нет 1937 Ford + generic Нет

1. OBD-Droid (Wal33D) — Java Android ЛУЧШИЙ

1.1. StreamHandler.java — побайтовый I/O

// ЧТЕНИЕ: побайтово, сон 1мс между проверками
public void run() {
    while (true) {
        if (in.available() > 0) {
            if ((chr = in.read()) > 0) {
                processRxChar(chr);
            } else break;
        } else {
            Thread.sleep(1);  // ← 1 миллисекунда!
        }
    }
}

// ОБРАБОТКА СИМВОЛОВ: '>' = такой же разделитель как CR/LF!
private void processRxChar(int chr) {
    switch (chr) {
        case 32: break;           // пробел — игнорируем
        case '>':                 // промпт ELM
            message += (char) chr;
            // fall through — НЕ отдельный случай!
        case 10:                  // LF
        case 13:                  // CR
            messageHandler.handleTelegram(message.toCharArray());
            message = "";
            break;
        default:
            message += (char) chr;
    }
}

// ОТПРАВКА: BufferedWriter с буфером 1 байт = flush на каждом байте
out = new BufferedWriter(new OutputStreamWriter(outStream), 1);

public int writeTelegram(final char[] buffer, int type, Object id) {
    new Thread(() -> {
        String msg = new String(buffer) + "\r";  // ELM ждёт CR
        out.write(msg.toCharArray());
        out.flush();  // немедленный flush из-за буфера 1 байт
    }).start();
    return buffer.length;
}

Ключевые выводы:

  • > — НЕ спецсигнал «можно слать дальше». Это просто разделитель строк, как CR/LF.
  • Буфер 1 байт на запись = каждый байт сразу уходит в порт.
  • Отправка в отдельном потоке (не блокирует чтение).

1.2. ElmProt.java — стейт-машина протокола

RSP_ID — все возможные ответы ELM327:

enum RSP_ID {
    PROMPT(">"),          OK("OK"),         MODEL("ELM"),
    NODATA("NODATA"),     SEARCH("SEARCHING"),
    ERROR("ERROR"),       NOCONN("UNABLE"), NOCONN2("NABLETO"),
    CANERROR("CANERROR"), BUSBUSY("BUSBUSY"),
    BUSERROR("BUSERROR"), BUSINIERR("BUSINIT:ERR"),
    BUSINIERR2("BUSINIT:BUS"),  BUSINIERR3("BUSINIT:...ERR"),
    FBERROR("FBERROR"),   DATAERROR("DATAERROR"),
    BUFFERFULL("BUFFERFULL"), STOPPED("STOPPED"),
    RXERROR("<"),         QMARK("?"),
    UNKNOWN("");
}

STAT — состояния соединения:

UNDEFINED  INITIALIZING  INITIALIZED  ECU_DETECT  ECU_DETECTED
 ECU_SELECTED  CONNECTING  CONNECTED
// Ошибки:
NODATA, STOPPED, DISCONNECTED, BUSERROR, DATAERROR, RXERROR, ERROR

Инициализация (после ATZ → MODEL):

ATD          // defaults
ATE0         // echo off
ATL0         // line feeds off
ATS0         // spaces off
ATH1         // headers ON (для обнаружения ЭБУ)
ATDP         // узнать протокол
ATSPA1       // протокол AUTO
ATAT1        // adaptive timing ON
ATST<value>  // установить таймаут

Обработка ошибок — детально:

SEARCHING → статус CONNECTING (не ошибка!)
NODATA    → увеличить OBD timeout + переустановить протокол
ERROR     → WARMSTART (ATWS)
DATAERROR → WARMSTART
RXERROR   → WARMSTART
BUFFERFULL→ WARMSTART
BUS ERROR → DISCONNECTED → переустановить протокол + ретрай последней команды
UNABLE    → DISCONNECTED → переустановить протокол + ретрай

Мульти-фрейм ISO-TP:

Формат: "0:4100..." — первая строка с длиной
        "1:4100..." — продолжение
charsExpected = байт_длины * 2 (каждый байт = 2 hex символа)

1.3. BluetoothCommService.java — BT SPP

final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

// Первая попытка: стандартный RFCOMM
tmp = device.createRfcommSocketToServiceRecord(SPP_UUID);  // secure
// или
tmp = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);  // insecure

// FALLBACK: reflection-based RFCOMM channel 1 (для глючных адаптеров)
Method m = clazz.getMethod("createRfcommSocket", paramTypes);
Object[] params = new Object[]{1};  // channel 1
sockFallback = (BluetoothSocket) m.invoke(device, params);

Ключевой вывод: Есть fallback на reflection-based RFCOMM channel 1 — для дешёвых китайских клонов!


2. OBD2AI (catsmoker) — Kotlin Android

2.1. BluetoothHelper

val sppUuid: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")

suspend fun connectToDevice(deviceAddress: String): Pair<InputStream, OutputStream> {
    val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
    bluetoothSocket = device.createRfcommSocketToServiceRecord(sppUuid).apply {
        bluetoothAdapter.cancelDiscovery()
        connect()
    }
    return Pair(socket.inputStream, socket.outputStream)
}

2.2. ObdHelper — инициализация и команды

// Инициализация: фиксированные задержки, БЕЗ ожидания '>'
suspend fun initializeObd() = withContext(Dispatchers.IO) {
    suspend fun sendRawCommand(command: String) {
        out.write((command + "\r").toByteArray())
        out.flush()
        delay(400)  // ← 400мс после КАЖДОЙ команды
    }

    sendRawCommand("ATZ")      // сброс
    sendRawCommand("ATE0")     // эхо выкл
    sendRawCommand("ATL0")     // line feeds выкл
    sendRawCommand("ATSP0")    // авто-протокол

    delay(1000)  // дополнительная пауза после инита
    // Очистка буфера
    if (`in`.available() > 0) {
        val buffer = ByteArray(`in`.available())
        `in`.read(buffer)
    }
}

Используется библиотека kotlin-obd (eltonvs):

// Для стандартных команд — библиотека
obdConnection = ObdDeviceConnection(inputStream, outputStream)
val result = connection.run(TroubleCodesCommand())

// Для нестандартных — ручной парсинг
class MyRPMCommand : ObdCommand() {
    override val pid = "0C"
    override val handler = { it: ObdRawResponse ->
        val rawValue = it.processedValue
        val identifier = "410C"
        val aHex = rawValue.substring(index + 4, index + 6)
        val bHex = rawValue.substring(index + 6, index + 8)
        ((a * 256) + b) / 4  // формула RPM
    }
}

2.3. Live Data Monitoring

suspend fun startLiveDataMonitoring() = withContext(Dispatchers.IO) {
    var errorCount = 0
    while (isMonitoring.get()) {
        try {
            val speed = runCommand(MySpeedCommand())
            val rpm = runCommand(MyRPMCommand())
            val temp = runCommand(MyCoolantTempCommand())
            errorCount = 0
            delay(800)  // 800мс между циклами
        } catch (e: Exception) {
            errorCount++
            if (errorCount >= 3) break  // 3 ошибки подряд = стоп
            delay(1000)
        }
    }
}

3. Automotive-AI (Eloquent-Algorithmics) — Python Desktop

3.1. ELM327 через pyserial

# config.py
SERIAL_PORT = "/dev/ttyUSB0"  # или COM3 на Windows
BAUD_RATE = 38400

# Подключение
ser = serial.Serial(port=SERIAL_PORT, baudrate=BAUD_RATE, timeout=1)

# Отправка команды
def send_command(ser, command):
    ser.write((command + "\r\n").encode())   # CRLF терминатор
    response = ser.readline().decode().strip()
    response = response.replace("\r", "").replace(">", "")
    return response

Ключевые отличия от OBD-Droid:

  • readline() вместо побайтового чтения — ПРОЩЕ, но менее надёжно
  • \r\n вместо просто \r
  • timeout=1 — ждёт 1 секунду на readline
  • Убирает > из ответа (не использует как разделитель)

3.2. Парсинг ответов

# RPM: 010C → 41 0C HH LL
if cmd == "010C":
    value = (int(response.split()[2], 16) * 256 +
             int(response.split()[3], 16)) / 4

# Coolant: 0105 → 41 05 XX
if cmd == "0105":
    value = int(response.split()[2], 16) - 40  # -40 offset

# VIN: 0902
vin_response = parse_vin_response(response)
vehicle_data = decode_vin(vin_response)

4. obd2-mcp-server (petrpatek) — Python Claude MCP САМЫЙ СВЕЖИЙ

4.1. BLE + Serial подключение

Поддерживает:
- BLE (vLinker FD, STN чип) — асинхронный, asyncio.Lock
- Serial (classic Bluetooth SPP) — синхронный, pyserial

Baud rate auto-retry: [500k, 115.2k, 38.4k, 9.6k]
BLE: 30-секундный keepalive heartbeat (без него адаптер засыпает через ~120с)

4.2. Инициализация (САМАЯ ПОЛНАЯ)

ATZ           # сброс
ATE0          # эхо выкл
ATL0          # line feeds выкл
ATS0          # пробелы выкл
ATH1          # заголовки CAN ВКЛ (для обнаружения ЭБУ)
ATCAF1        # CAN auto-formatting ON
ATAT1         # adaptive timing ON
ATST64        # timeout = 64*4ms = 256ms
ATSP0         # авто-протокол

# Для STN адаптеров (OBDlink):
ATPP 0E SV 00 # отключить сон
ATPP 0E ON    # включить

4.3. Ретраи и таймауты

MAX_RETRIES = 3
RETRY_BACKOFF = [2, 5, 10]  # секунды
CONNECT_TIMEOUT = 20  # секунд
PROTOCOL_TIMEOUT = 12  # секунд
CONFIG_TIMEOUT = 2     # секунды

4.4. Очистка ответа

def _clean_elm_response(raw: str) -> str:
    # Убирает: промпт ">", эхо команд, "SEARCHING...", пустые строки
    ...

4.5. DTC база данных

- 1937 Ford-специфичных кодов
- Generic OBD-II коды (P, B, C, U)
- Ленивая загрузка по бренду
- Скрапинг с troublecodes.net

5. Vehicle-Diagnostic-Assistant (castlebbs) — Embedded C + Python

5.1. Аппаратная архитектура

W600 MCU ←UART1 38400 8N1→ ELM327 чип → CAN → Авто
    ↕ HTTP/MCP
LangChain Agent (Python) → DeepSeek / Claude

5.2. ELM327 Driver (C)

// elm327.c
int elm327_send_command(const char* cmd, char* resp, int len, int timeout) {
    // Пишет команду + \r в UART1
    // Ждёт ответ через FreeRTOS semaphore (прерывание по приёму)
    // Таймаут по умолчанию: 2000мс
    // Макс. длина ответа: 512 байт
}

// Hybrid simulation mode:
// AT команды → реальный ELM327
// OBD команды → симуляция (если включена)

5.3. Поддерживаемые режимы OBD

Mode 01: live data (30+ PID)
Mode 03: stored DTC (формат 43 XX XX XX XX)
Mode 04: clear DTC (44)
Mode 07: pending DTC (47)
Mode 09: vehicle info (VIN, calibration ID)

5.4. PID формулы (Mode 01)

PID Формула Пример
0C (RPM) (A*256+B)/4 0x1AF8 → 1726
0D (Speed) A (km/h) 0x00 → 0
05 (ECT) A-40 (°C) 0x5A → 50
04 (Load) (A*100)/255 (%) 0x40 → 25.1
10 (MAF) ((A*256)+B)/100 (g/s)
2F (Fuel) (A*100)/255 (%)

5.5. Safe formula evaluation

def calculate_obd_value(raw_response, formula):
    # LLM вызывает этот tool для расчёта значений
    # safe_eval() — ограниченный eval (только +-*/ и переменные A,B,C,D)
    hex_bytes = raw_response.replace("41 XX ", "").split()
    A, B, C, D = [int(x, 16) for x in hex_bytes]
    return safe_eval(formula, {"A": A, "B": B, "C": C, "D": D})

СРАВНИТЕЛЬНЫЙ АНАЛИЗ: Что взять для Elmer

Инициализация ELM327

Проект Последовательность Задержки
OBD-Droid ATD→ATE0→ATL0→ATS0→ATH1→ATDP→ATSPA1→ATAT1→ATST Стейт-машина, нет фикс. задержек
OBD2AI ATZ→ATE0→ATL0→ATSP0 400мс после каждой
obd2-mcp ATZ→ATE0→ATL0→ATS0→ATH1→ATCAF1→ATAT1→ATST64→ATSP0 async, по ответам
Automotive-AI Нет явной инициализации

Рекомендация для Elmer: взять последовательность obd2-mcp-server (самая полная) + задержки OBD2AI (400мс) + ATH0 вместо ATH1 (для чистых ответов без CAN-заголовков).

Чтение ответов

Проект Метод Плюсы Минусы
OBD-Droid Побайтово, 1мс sleep Макс. контроль Сложный код
OBD2AI kotlin-obd lib Готовое решение Зависимость от библиотеки
Automotive-AI ser.readline() Простой код Менее надёжно

Рекомендация для Elmer: для Android — побайтовое чтение как у OBD-Droid (уже есть в TestService). Для Python-мока/сервера — readline() достаточно для тестов.

Обработка ошибок

Ошибка OBD-Droid OBD2AI obd2-mcp
SEARCHING Статус CONNECTING Пропустить, ждать
NO DATA Увеличить timeout Вернуть пусто
BUS ERROR DISCONNECTED + retry
UNABLE DISCONNECTED + retry
ERROR WARMSTART (ATWS)
RX ERROR WARMSTART 3 strikes → stop

Рекомендация для Elmer: SEARCHING = ждать + увеличить таймаут. NO DATA = пропустить PID. BUS ERROR/UNABLE = одна попытка reconnect + retry. ERROR = WARMSTART.

Тайминги

Проект Между командами Инит Таймаут ответа
OBD-Droid Нет (стейт-машина) Стейт-машина 5000мс адаптивный
OBD2AI 400мс fix 1000мс после всех ? (внутри lib)
Automotive-AI Нет Нет 1000мс (readline)
obd2-mcp По ответам По ответам 2000-20000мс
castlebbs По семафору 2000мс

Рекомендация для Elmer: 400мс между командами (как OBD2AI) + адаптивный таймаут от 2000мс с возможностью увеличения (как OBD-Droid).

BT подключение (Android)

Проект Метод Fallback
OBD-Droid createRfcommSocketToServiceRecord secure + insecure Reflection RFCOMM channel 1
OBD2AI createRfcommSocketToServiceRecord Нет

Рекомендация для Elmer: взять fallback на reflection channel 1 из OBD-Droid — критично для дешёвых клонов.


ИТОГ: Что реализовать в elmer-android

Приоритет 1 (обязательно)

  • Побайтовое чтение с паузой 1мс (StreamHandler.java)
  • > = разделитель строк, НЕ спецсигнал
  • Fallback RFCOMM channel 1 (BluetoothCommService.java)
  • Фиксированные задержки 400мс между командами (OBD2AI)
  • Очистка буфера после инициализации

Приоритет 2 (важно)

  • Обработка SEARCHING, NO DATA, BUS ERROR
  • 3-strike retry для live monitoring
  • Адаптивный таймаут (базовый 5000мс)

Приоритет 3 (для production)

  • WARMSTART при ERROR/DATAERROR
  • Мульти-фрейм ISO-TP
  • DTC база (можно с obd2-mcp-server)
  • Экспоненциальный backoff для ретраев

Часть 2: Традиционные OBD2 Android-проекты (БЕЗ LLM)

Только ELM327 ↔ Android Bluetooth SPP. Именно они интересны для слоя коммуникации — оттестированы годами на тысячах машин.

Топ-10 батл-тестед проектов

# Проект URL Язык Создатель Обновлён DTC Live VIN Примечания
1 AndrOBD fr3ts0n/AndrOBD Java Sepp Seidel Апр 2025 1993 Король. 10+ лет, MQTT, графики, плагины, многоязычный
2 AndroidOBD barnhill/AndroidOBD Kotlin Brian Barnhill Ноя 2025 79 Библиотека. Хороший API для интеграции
3 Java OBD Tomiwa-Ot/obd Java Tomiwa O. Март 2025 30 Библиотека. SPP + USB. Опубликована на JitPack. Async
4 ObdGraphs tzebrowski/ObdGraphs Kotlin Tomek Żebrowski Апр 2025 53 Графики в реальном времени. Проф. дизайн
5 CarScanApp midnightyoff/CarScanApp Kotlin midnightyoff Окт 2025 2 Свежий. Compose, DTC read/clear, maintenance log
6 Elm327 takyonxxx/Elm327 Java Türkay Biliyor Окт 2023 48 В Play Store. WiFi + BT. 22 форка
7 CarBusInterface theksmith/CarBusInterface Java Kristoffer Smith Янв 2016 291 Старый, боевой. 77 форков. Raw command interface
8 BT OBD-II Diag Tool fussek/Bluetooth-OBD-II-Diagnostic-Tool Java Sebastian Fussek Сент 2022 21 Bachelor thesis. Хорошая документация + расчёты PID
9 obd-scanner-android ETSoftwareStudio/obd-scanner-android Kotlin ETSoftware Апр 2025 2 Jetpack Compose + Hilt. Образец архитектуры
10 KWP Logger bri3d/kwp-android-logger Java Brian Ledbetter Фев 2016 28 KWP2000 для VW/Audi. Специалист по протоколам

Что между ними общего (подтверждено всеми 14 проектами)

Bluetooth SPP:

UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(SPP_UUID);
socket.connect();

Инициализация ELM327 (единый стандарт):

ATZ      → сброс
ATE0     → эхо выкл
ATL0     → line feeds выкл
ATS0     → пробелы выкл (или ATS1 — с пробелами)
ATH0/1   → заголовки CAN
ATSP0    → авто-протокол

Отправка команд (единый стандарт):

outputStream.write((cmd + "\r").getBytes());
outputStream.flush();

Чтение ответов — два лагеря:

  • Лагерь 1 (AndrOBD, CarBusInterface): побайтово, по 1мс
  • Лагерь 2 (AndroidOBD, Elm327): BufferedReader.readLine()

Кого изучать в первую очередь для Elmer

Для чего Проект Почему
BT ↔ ELM слой AndrOBD + CarBusInterface 10 лет отладки, все краевые случаи
Kotlin-интеграция AndroidOBD + CarScanApp Чистый API, современный код
PID/DTC парсинг AndrOBD Все формулы, все режимы
KWP2000 (VW) KWP Logger Если Phaeton на старом протоколе

Часть 3: СЫРОЙ КОД AndrOBD — золотой стандарт (1993 , 10 лет в продакшене)

3.1. Bluetooth SPP подключение (BtCommService.java)

final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

// Стандартный socket
if (secure) {
    tmp = device.createRfcommSocketToServiceRecord(SPP_UUID);
} else {
    tmp = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
}

// FALLBACK: reflection RFCOMM channel 1 (для китайских клонов!)
catch (IOException e) {
    Class<?> clazz = mmSocket.getRemoteDevice().getClass();
    Method m = clazz.getMethod("createRfcommSocket", Integer.TYPE);
    sockFallback = (BluetoothSocket) m.invoke(mmSocket.getRemoteDevice(), 1);
    mmSocket = sockFallback;
    mmSocket.connect();  // пробуем снова
}

3.2. 🚨 КРИТИЧЕСКИ: 500мс пауза после BT connect

// AndrOBD issue #233 — без этой паузы Android теряет данные!
Thread.sleep(500);  // CRITICAL: Fix for Android Bluetooth timing

// Только после этого запускаем worker thread
mBtWorkerThread = new BtWorkerThread(socket, socketType);
mBtWorkerThread.start();

3.3. Инициализация ELM327 (точный порядок)

private void initialize() {
    setStatus(STAT.INITIALIZING);

    // 1. Кастомные init-команды (если есть)
    cmdQueue.addAll(customInitCommands);

    // 2. Установить протокол (AUTO или конкретный)
    pushCommand(CMD.SETPROT, preferredProtocol.ordinal());  // ATSP<n>

    // 3. Инициализировать адаптивный тайминг
    mAdaptiveTiming.initialize();  // ATAT1

    // 4. Ускорить протокол (убрать пробелы и переводы строк)
    pushCommand(CMD.SETSPACES, 0);     // ATS0
    pushCommand(CMD.SETLINEFEED, 0);   // ATL0

    // 5. Выключить эхо
    pushCommand(CMD.ECHO, 0);          // ATE0
}
// → ждёт "ELM" ответ → затем 0100 (запрос поддерживаемых PID)

3.4. Адаптивный таймаут (ГЕНИАЛЬНО)

// Константы
ELM_TIMEOUT_MAX     = 1000   // мс — максимум
ELM_TIMEOUT_DEFAULT = 200    // мс — стартовое значение
ELM_TIMEOUT_RES     = 4      // мс — шаг изменения
ELM_TIMEOUT_MIN     = 12     // мс — минимум

// На каждый NODATA → увеличить таймаут на 4мс
// На каждый успех   → уменьшить таймаут на 4мс (но не ниже learned_min)

void adapt(boolean increaseTimeout) {
    if (increaseTimeout) {
        if (elmMsgTimeout + 4 < 1000)
            setElmMsgTimeout(elmMsgTimeout + 4);
    } else {
        if (elmMsgTimeout - 4 >= learnedMin)
            setElmMsgTimeout(elmMsgTimeout - 4);
    }
}

// Каждое изменение отправляет: ATST<timeout/4>

Диапазон: 12мс → 1000мс, шаг 4мс. Старт: 200мс.

3.5. Обработка ответов (handleTelegram)

// 1. Фильтр эха
if (lastTxMsg.equalsIgnoreCase(bufferStr))
    return 0;  // проигнорировать

// 2. Определить тип ответа
switch (getResponseId(bufferStr)) {
    case PROMPT:    // ">" — конец ответа
    case MODEL:     // "ELM" → вызвать initialize()
    case SEARCHING: // идёт поиск
    case NODATA:    // нет данных
    case ERROR:     // ошибка
    case NOCONN:    // "UNABLE"
    case BUSERROR:  // ошибка шины
    // ... ещё 10+
}

3.6. Обработка ошибок (production-grade)

BUS ERROR / UNABLE / CAN ERROR:
  → DISCONNECTED
  → переставить в очередь последнюю команду
  → сбросить протокол (ATSP)
  → переинициализировать тайминг
  → закрыть протокол (ATPC)

DATA ERROR / RX ERROR / BUFFER FULL:
  → WARM START (ATWS) — мягкий перезапуск

NO DATA:
  → увеличить адаптивный таймаут
  → переустановить протокол

STOPPED:
  → переставить последнюю команду в очередь

3.7. Мульти-фрейм (ISO-TP для VIN и длинных ответов)

// Формат: "014" = 1 строка, 14 hex символов
//         "0:4902015756..." — первая строка с длиной
//         "1:5A5A314B5A41..." — продолжение

if (buffer[0] == '0' && buffer.length == 3) {
    charsExpected = Integer.valueOf(bufferStr, 16) * 2;  // 0x14 = 20 байт
    lastRxMsg = "";
    responsePending = true;
}

if (bufferStr.indexOf(':') >= 0) {
    lastRxMsg += bufferStr.substring(idx + 1);
}

if (lastRxMsg.length() >= charsExpected) {
    result = handleDataMessage(lastRxMsg);  // готово
}

3.8. Итоговый процесс подключения (хронология)

1. BT socket connect()
2. Thread.sleep(500)           ← КРИТИЧЕСКИ! Без этого — потеря данных.
3. BtWorkerThread.start()
4. StreamHandler.run() — побайтовое чтение
5. ATSP0 → ATAT1 → ATS0 → ATL0 → ATE0
6. Ждём "ELM" → статус INITIALIZED
7. 0100 → опрос поддерживаемых PID
8. ECU detect → выбор ECU → готов к работе