711 lines
28 KiB
Markdown
711 lines
28 KiB
Markdown
# 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
|
||
|
||
```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 для ретраев
|
||
|
||
---
|
||
|
||
## Часть 2: Традиционные OBD2 Android-проекты (БЕЗ LLM)
|
||
|
||
> Только ELM327 ↔ Android Bluetooth SPP. Именно они интересны для слоя коммуникации — оттестированы годами на тысячах машин.
|
||
|
||
### Топ-10 батл-тестед проектов
|
||
|
||
| # | Проект | URL | Язык | Создатель | Обновлён | ⭐ | DTC | Live | VIN | Примечания |
|
||
|---|--------|-----|------|-----------|----------|-------|-----|------|-----|-----------|
|
||
| 1 | **AndrOBD** | [fr3ts0n/AndrOBD](https://github.com/fr3ts0n/AndrOBD) | Java | Sepp Seidel | Апр 2025 | 1993 | ✅ | ✅ | ✅ | **Король.** 10+ лет, MQTT, графики, плагины, многоязычный |
|
||
| 2 | **AndroidOBD** | [barnhill/AndroidOBD](https://github.com/barnhill/AndroidOBD) | Kotlin | Brian Barnhill | Ноя 2025 | 79 | ✅ | ✅ | ✅ | **Библиотека.** Хороший API для интеграции |
|
||
| 3 | **Java OBD** | [Tomiwa-Ot/obd](https://github.com/Tomiwa-Ot/obd) | Java | Tomiwa O. | Март 2025 | 30 | ✅ | ✅ | ✓ | **Библиотека.** SPP + USB. Опубликована на JitPack. Async |
|
||
| 4 | **ObdGraphs** | [tzebrowski/ObdGraphs](https://github.com/tzebrowski/ObdGraphs) | Kotlin | Tomek Żebrowski | Апр 2025 | 53 | ✅ | ✅ | ✅ | Графики в реальном времени. Проф. дизайн |
|
||
| 5 | **CarScanApp** | [midnightyoff/CarScanApp](https://github.com/midnightyoff/CarScanApp) | Kotlin | midnightyoff | Окт 2025 | 2 | ✅ | ✅ | ✅ | **Свежий.** Compose, DTC read/clear, maintenance log |
|
||
| 6 | **Elm327** | [takyonxxx/Elm327](https://github.com/takyonxxx/Elm327) | Java | Türkay Biliyor | Окт 2023 | 48 | ✅ | ✅ | ✅ | В Play Store. WiFi + BT. 22 форка |
|
||
| 7 | **CarBusInterface** | [theksmith/CarBusInterface](https://github.com/theksmith/CarBusInterface) | Java | Kristoffer Smith | Янв 2016 | 291 | ✅ | ✅ | ✅ | Старый, боевой. 77 форков. Raw command interface |
|
||
| 8 | **BT OBD-II Diag Tool** | [fussek/Bluetooth-OBD-II-Diagnostic-Tool](https://github.com/fussek/Bluetooth-OBD-II-Diagnostic-Tool) | Java | Sebastian Fussek | Сент 2022 | 21 | ✅ | ✅ | ✅ | Bachelor thesis. Хорошая документация + расчёты PID |
|
||
| 9 | **obd-scanner-android** | [ETSoftwareStudio/obd-scanner-android](https://github.com/ETSoftwareStudio/obd-scanner-android) | Kotlin | ETSoftware | Апр 2025 | 2 | ✅ | ✅ | ✓ | Jetpack Compose + Hilt. Образец архитектуры |
|
||
| 10 | **KWP Logger** | [bri3d/kwp-android-logger](https://github.com/bri3d/kwp-android-logger) | Java | Brian Ledbetter | Фев 2016 | 28 | ✅ | ✅ | ✓ | **KWP2000 для VW/Audi.** Специалист по протоколам |
|
||
|
||
### Что между ними общего (подтверждено всеми 14 проектами)
|
||
|
||
**Bluetooth SPP:**
|
||
```java
|
||
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 → авто-протокол
|
||
```
|
||
|
||
**Отправка команд (единый стандарт):**
|
||
```java
|
||
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)
|
||
|
||
```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
|
||
|
||
```java
|
||
// 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 (точный порядок)
|
||
|
||
```java
|
||
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. Адаптивный таймаут (ГЕНИАЛЬНО)
|
||
|
||
```java
|
||
// Константы
|
||
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)
|
||
|
||
```java
|
||
// 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 и длинных ответов)
|
||
|
||
```java
|
||
// Формат: "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 → готов к работе
|
||
```
|