Files
elmer/doc/elm-raw-relay-plan.md
T

12 KiB
Raw Blame History

План: тонкий Android-ретранслятор ELM327

Дата: 2026-06-14

Цель

Отдельное Android-приложение — тупой ретранслятор команд между сервером и ELM327. Пользователь устанавливает один раз. Вся логика (какие команды слать, как анализировать ответы) — на сервере. Приложение только:

  1. Коннектится к ELM327 по Bluetooth
  2. Сообщает серверу «готов»
  3. Поллит сервер на наличие команды
  4. Отправляет команду в ELM327
  5. Возвращает сырой ответ на сервер
  6. Повторяет с пункта 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 → ATE0
  • sendCommand(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

Порядок работ

  1. Сервер: дополнить api/raw_elm.py эндпоинтами очереди
  2. Сервер: добавить web/app.py — раздача /elm-raw.apk
  3. Сервер: tools/elm_relay.py — консоль для меня
  4. Android: модуль :raw — 5 файлов (.kt + манифест + build.gradle)
  5. Сборка: проверить что оба APK собираются
  6. Тест: поставить APK на телефон, проверить связь с сервером

Что НЕ делаем

  • Не трогаем ru.elmer.client — ни строчки
  • Не меняем app/build.gradle.kts
  • Не меняем существующий AndroidManifest.xml
  • Не изобретаем новый ELM327-протокол — используем AndrOBD как есть
  • Не пишем сложный UI — только статус и лог