294 lines
12 KiB
Markdown
294 lines
12 KiB
Markdown
# План: тонкий 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 — только статус и лог
|