18 KiB
ELM327 Communication Patterns — анализ 5 отлаженных проектов
Цель: понять как РЕАЛЬНО работают проекты с ELM327, выбрать лучшие паттерны для Elmer. Дата: 2026-05-27 Источники: исходный код 5 проектов (Java, Kotlin, Python, C)
Сводная таблица
| 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вместо просто\rtimeout=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 для ретраев