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

294 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: тонкий 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 — только статус и лог