12 KiB
План: тонкий Android-ретранслятор ELM327
Дата: 2026-06-14
Цель
Отдельное Android-приложение — тупой ретранслятор команд между сервером и ELM327. Пользователь устанавливает один раз. Вся логика (какие команды слать, как анализировать ответы) — на сервере. Приложение только:
- Коннектится к ELM327 по Bluetooth
- Сообщает серверу «готов»
- Поллит сервер на наличие команды
- Отправляет команду в ELM327
- Возвращает сырой ответ на сервер
- Повторяет с пункта 3
Почему отдельное приложение
- Ноль риска сломать существующий
ru.elmer.client - Независимый пакет
ru.elmer.raw - Свой APK, свой URL на сервере (
/elm-raw.apk) - Можно удалить/переустановить независимо от основного
Архитектура
┌─────────────────────────────────────────────────┐
│ Сервер (elmer/python) │
│ │
│ POST /api/v1/elm/raw/cmd ← я ставлю команду │
│ GET /api/v1/elm/raw/cmd ← приложение поллит │
│ POST /api/v1/elm/raw/response ← приложение шлёт │
│ GET /api/v1/elm/raw/response ← я читаю ответ │
│ /elm-raw.apk ← раздача APK │
└──────────────┬──────────────────────────────────┘
│ HTTP (OkHttp)
┌──────────────▼──────────────────────────────────┐
│ Android-приложение (ru.elmer.raw) │
│ │
│ RawRelayService (foreground) │
│ ├─ Bluetooth → ELM327 │
│ ├─ ElmProtocol (AndrOBD, проверенный) │
│ ├─ Polling: GET /cmd каждые 500ms │
│ └─ POST /response с сырым ответом │
│ │
│ MainActivity (минимальный UI) │
│ ├─ Статус: сервер / ELM / ECU │
│ ├─ Лог последних команд │
│ └─ Кнопка «Стоп» │
└──────────────────────────────────────────────────┘
Компоненты Android-приложения
1. Пакет: ru.elmer.raw
Новый пакет, не пересекается с ru.elmer.client.
2. Файлы (5 штук)
| Файл | Размер | Назначение |
|---|---|---|
MainActivity.kt |
~100 строк | UI: статус, лог, кнопка стоп |
RawRelayService.kt |
~150 строк | Foreground-сервис: BT+поллинг+команды |
ElmProtocol.kt |
копия | Точная копия из ru.elmer.client.elm |
ServerClient.kt |
~80 строк | Урезанный HTTP-клиент (только cmd/response) |
AndroidManifest.xml |
~40 строк | Свой манифест для ru.elmer.raw |
Почему копия ElmProtocol.kt, а не общий модуль:
- Не трогаем существующий код вообще
- AndrOBD-логика отлажена годами, меняться не будет
- Две копии живут независимо, никаких конфликтов
3. ElmProtocol.kt — как есть
Используем без изменений проверенную стейт-машину:
init(): ATSP0 → ATAT1 → ATS0 → ATL0 → ATE0sendCommand(cmd): отправить → прочитать до>→ вернуть сырой ответ- Обработка ошибок: BUS ERROR, CAN ERROR, BUFFER FULL, ретраи, восстановление
- Адаптивные тайминги
Единственное что добавим — вызов sendCommand() оборачиваем в try/catch,
результат всегда возвращается на сервер (даже если ошибка).
4. Протокол обмена с сервером
Приложение → Сервер: «я готов»
POST /api/v1/elm/raw/hello
{
"device_id": "android-xyz",
"elm_version": "ELM327 v1.5",
"protocol": "A4",
"voltage": "12.3V"
}
Сервер → Приложение: команда
GET /api/v1/elm/raw/cmd?device_id=android-xyz
Ответ 200:
{
"cmd": "0105",
"timeout_ms": 500,
"drain_first": false,
"seq": 1
}
Ответ 204: (нет команды — полли дальше)
Приложение → Сервер: ответ
POST /api/v1/elm/raw/response
{
"device_id": "android-xyz",
"seq": 1,
"cmd": "0105",
"raw": "41 05 5C",
"prompt": true,
"elapsed_ms": 48,
"bytes": 8,
"error": null
}
Сервер → Приложение: подтверждение
200 {"ok": true}
5. RawRelayService — жизненный цикл
onStartCommand(Intent: serverUrl)
↓
1. Подключить Bluetooth к ELM327 (UUID SPP 00001101-0000-1000-8000-00805F9B34FB)
↓
2. ElmProtocol.init() — базовая инициализация
↓
3. POST /hello — сообщить серверу «готов»
↓
4. Цикл (в фоновом потоке):
GET /cmd — ждать команду (500ms поллинг)
если 204 → sleep 500ms → снова GET /cmd
если 200 →
drain? → ElmProtocol.sendCommand("ATPC") → read/discard
ElmProtocol.sendCommand(cmd)
POST /response — отправить сырой ответ
→ снова GET /cmd
↓
5. onDestroy(): закрыть BT, stopForeground, остановить поток
6. MainActivity — UI
┌──────────────────────────────┐
│ ELM327 Raw Relay │
│ │
│ Сервер: ✅ obdai.ru │
│ ELM: 🔵 подключён │
│ ECU: ✅ отвечает │
│ │
│ Последняя команда: │
│ → 0105 │
│ ← 41 05 5C (48ms, 8 байт) │
│ │
│ Лог: 12 команд, 0 ошибок │
│ │
│ [ СТОП ] │
└──────────────────────────────┘
Минимальный UI:
- Три индикатора статуса (сервер, ELM, ECU)
- Последняя команда и ответ
- Счётчик команд/ошибок
- Кнопка «Стоп»
Изменения на серверной стороне (elmer/python)
1. Очередь команд — api/raw_elm.py
Добавить эндпоинты (дополнить существующий api/raw_elm.py):
POST /api/v1/elm/raw/cmd — я ставлю команду в очередь
GET /api/v1/elm/raw/cmd — приложение забирает команду
POST /api/v1/elm/raw/response — приложение шлёт ответ
GET /api/v1/elm/raw/response — я читаю последний ответ
POST /api/v1/elm/raw/hello — приложение регистрируется
GET /api/v1/elm/raw/status — статус: готово/ждёт/ошибка
2. Хранение очереди
В памяти (глобальная переменная), не в БД:
_pending_cmd: dict | None— команда, которую ждёт приложение_last_response: dict | None— последний ответ от ELM327_device_ready: bool— готово ли приложение_device_info: dict— информация об устройстве
Зачем в памяти: одна сессия отладки, один поток команд. Не нужна персистентность.
3. Раздача APK — web/app.py
@app.route("/elm-raw.apk")
def download_raw_apk():
return send_from_directory("static", "elm-raw.apk", ...)
В templates/index.html — ссылка «Скачать ELM Raw Relay».
4. Интерактивная консоль — tools/elm_relay.py
Скрипт для меня (Copilot):
- Читает статус устройства
- Ставит команду в очередь
- Ждёт ответ
- Показывает сырой ответ
- Анализирует, ставит следующую команду
- История всех команд сохраняется
Сборка и деплой
Сборка APK
cd android
./gradlew :app:assembleDebug
# APK: android/app/build/outputs/apk/debug/app-debug.apk
Но нам нужен отдельный APK для ru.elmer.raw. Два варианта:
Вариант A: Product Flavor (в одном проекте)
- В
app/build.gradle.ktsдобавитьflavorDimensions+ два flavor:clientиraw - Разные
applicationId, разныеAndroidManifest.xml - Общий код в
main/, специфичный — вclient/иraw/ - Минус: трогаем
build.gradle.ktsосновного приложения
Вариант B: Новый модуль (рекомендую)
- Новый Gradle-модуль
android/raw/ - Свой
build.gradle.kts, свой манифест, свой пакет - Не трогаем вообще ничего в
android/app/ settings.gradle.kts— добавитьinclude(":raw")- Минус: ElmProtocol.kt — физическая копия файла
Я за Вариант B: новый модуль :raw
android/
├── app/ ← существующее, НЕ ТРОГАЕМ
├── raw/ ← НОВЫЙ модуль
│ ├── build.gradle.kts
│ └── src/main/
│ ├── AndroidManifest.xml
│ └── java/ru/elmer/raw/
│ ├── MainActivity.kt
│ ├── RawRelayService.kt
│ ├── ElmProtocol.kt ← копия из :app
│ └── ServerClient.kt
├── settings.gradle.kts ← + include(":raw")
└── build.gradle.kts ← не трогаем
Деплой
cd android
./gradlew :raw:assembleDebug
cp raw/build/outputs/apk/debug/raw-debug.apk ../web/static/elm-raw.apk
# Задеплоить на сервер через deploy.sh
Порядок работ
- Сервер: дополнить
api/raw_elm.pyэндпоинтами очереди - Сервер: добавить
web/app.py— раздача/elm-raw.apk - Сервер:
tools/elm_relay.py— консоль для меня - Android: модуль
:raw— 5 файлов (.kt + манифест + build.gradle) - Сборка: проверить что оба APK собираются
- Тест: поставить APK на телефон, проверить связь с сервером
Что НЕ делаем
- Не трогаем
ru.elmer.client— ни строчки - Не меняем
app/build.gradle.kts - Не меняем существующий
AndroidManifest.xml - Не изобретаем новый ELM327-протокол — используем AndrOBD как есть
- Не пишем сложный UI — только статус и лог