Files
elmer/doc/elm-reference.md
T

485 lines
18 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 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
```java
// ЧТЕНИЕ: побайтово, сон 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:**
```java
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 — состояния соединения:**
```java
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
```java
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
```kotlin
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 — инициализация и команды
```kotlin
// Инициализация: фиксированные задержки, БЕЗ ожидания '>'
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):**
```kotlin
// Для стандартных команд — библиотека
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
```kotlin
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
```python
# 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. Парсинг ответов
```python
# 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. Инициализация (САМАЯ ПОЛНАЯ)
```python
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. Ретраи и таймауты
```python
MAX_RETRIES = 3
RETRY_BACKOFF = [2, 5, 10] # секунды
CONNECT_TIMEOUT = 20 # секунд
PROTOCOL_TIMEOUT = 12 # секунд
CONFIG_TIMEOUT = 2 # секунды
```
### 4.4. Очистка ответа
```python
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)
```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
```python
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 для ретраев