# План: тонкий 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` ```python @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 ```bash 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 ← не трогаем ``` ### Деплой ```bash 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 — только статус и лог