308 lines
16 KiB
Markdown
308 lines
16 KiB
Markdown
# Сессия 2026-05-26 — отладка Android-клиента и Mock ELM327
|
||
|
||
## Хронология
|
||
|
||
### 08:00 — Старт: пулл изменений с ночи
|
||
Ночью на другой машине было сделано:
|
||
- `android/` вынесен в отдельный репо `github.com/Repinoid/elmer-android`
|
||
- Добавлен `tools/mock_elm327.py` — TCP-эмулятор ELM327 на порту 35000
|
||
- `web/raw_endpoint.py` — стейт-машина, парсинг ответов, LLM-интеграция
|
||
- Исправлены 3 бага в Android-клиенте (NetworkOnMainThread, URL сервера, фильтр \n)
|
||
|
||
### 08:20 — Попытка 1: телефон → mock → сервер
|
||
Запущены mock (:35000) и Flask (:5005). Настроен проброс портов через Windows:
|
||
```powershell
|
||
netsh interface portproxy add v4tov4 listenport=5005 listenaddress=0.0.0.0 connectport=5005 connectaddress=192.168.18.62
|
||
netsh advfirewall firewall add rule name="Elmer Flask" dir=in action=allow protocol=TCP localport=5005
|
||
```
|
||
|
||
WiFi IP ноутбука: `10.47.183.102`.
|
||
|
||
Телефон подключился к mock (TCP: OK), но команды не шли.
|
||
|
||
### 08:30 — Ошибка 1: Server URL захардкожен на obdai.ru
|
||
Глянув код клиента в `ElmForwardService.kt`, обнаружено:
|
||
```kotlin
|
||
// MainActivity.kt строка 66
|
||
putExtra(EXTRA_SERVER_URL, "https://obdai.ru/api/v1/raw-obd")
|
||
```
|
||
|
||
Телефон слал данные в интернет, а не на локальный Flask.
|
||
|
||
**Исправление:** в `MainActivity.kt` — авто-вывод URL сервера из IP устройства:
|
||
```kotlin
|
||
val deviceHost = debugHost.split(":")[0]
|
||
val localServerUrl = "http://$deviceHost:5005/api/v1/raw-obd"
|
||
```
|
||
|
||
### 08:35 — Ошибка 2: Gradle 9.5 слишком новый
|
||
CI упал с `org.gradle.api.artifacts.SelfResolvingDependency`. Причина: AGP 8.2.0 несовместим с Gradle 9.x.
|
||
|
||
**Исправление:** в `build-apk.yml`:
|
||
```yaml
|
||
- name: Setup Gradle
|
||
uses: gradle/actions/setup-gradle@v4
|
||
with:
|
||
gradle-version: "8.5"
|
||
```
|
||
|
||
И создан `gradle/wrapper/gradle-wrapper.properties` с `gradle-8.5-bin.zip`.
|
||
|
||
---
|
||
|
||
## ЧАСТЬ 2: Тест с реальным ELM327 (Фаэтон)
|
||
|
||
### Зелёная кнопка (TestService) — 100% работает
|
||
- BT-подключение к ELM327 ✅
|
||
- ATZ → ATEx → ATH1 ✅
|
||
- VIN: получен (16-ричные данные) ✅
|
||
- DTC stored/pending: получены ✅
|
||
- PID'ы: RPM, ОЖ, скорость, дроссель и др. ✅
|
||
- **Тайминги:** `Thread.sleep(250)` между командами — критически важно
|
||
|
||
### Оранжевая кнопка (ElmForwardService + сервер) — НЕ работает
|
||
Проблема: стейт-машина сервера шлёт команды мгновенно, без пауз. ELM327 не успевает.
|
||
- ATZ → OK
|
||
- ATEx → OK
|
||
- 0902 → SEARCHING...UNABLE TO CONNECT (ELM не может выполнить режим 09)
|
||
- Сервер переходит к DTC → шлёт 03 → ELM отвечает `?` → бесконечный цикл `?`
|
||
|
||
**Корень проблемы:** тонкий клиент требует server-driven архитектуру (сервер даёт команду → клиент пишет в ELM → ELM отвечает → клиент шлёт ответ серверу → сервер даёт следующую). Но сервер не делает пауз, а ELM327 требует ~200мс между командами.
|
||
|
||
### Почему Deadlock при первом запуске оранжевой кнопки
|
||
1. Клиент: BT-коннект ✅, входит в loop()
|
||
2. ELM327: ждёт команду (не шлёт приветствие без запроса)
|
||
3. Клиент: `read()` блокируется — нет данных
|
||
4. Сервер: не получает "READY" — не шлёт ATZ
|
||
5. **DEADLOCK**
|
||
|
||
**Исправлено:** `fwd("READY")` сразу после коннекта — кикстарт сервера.
|
||
|
||
---
|
||
|
||
## Вывод: архитектура телефон↔сервер в реальных условиях
|
||
|
||
### Проблема
|
||
В движении связи с сервером нет. Архитектура «сервер рулит каждой командой» нежизнеспособна.
|
||
|
||
### Решение (обсуждено)
|
||
**Две фазы работы:**
|
||
|
||
**Фаза 1 — ОФЛАЙН (в машине):**
|
||
- Клиент получает со старта **скрипт** от сервера
|
||
- Скрипт: последовательность команд + промпты водителю
|
||
- Клиент сам гоняет протокол (как зелёная кнопка)
|
||
- Данные пишутся локально (SQLite) с таймстемпами
|
||
- Водитель видит промпты: «Разгон 0-100», «Кикдаун», «Холостой ход 30с»
|
||
- Кнопка Старт / Стоп
|
||
|
||
**Фаза 2 — ОНЛАЙН (дома):**
|
||
- Клиент заливает всю сессию одним POST на сервер
|
||
- Сервер парсит, анализирует, LLM → диагноз
|
||
- Возможно — выдаёт следующий скрипт для нового теста
|
||
|
||
### Формат скрипта (пример):
|
||
```json
|
||
{
|
||
"name": "Тест турбины",
|
||
"steps": [
|
||
{"type": "obd", "cmd": "ATZ"},
|
||
{"type": "obd", "cmd": "010C", "label": "RPM"},
|
||
{"type": "prompt", "text": "Разгон 0-100, кикдаун"},
|
||
{"type": "loop", "pid": "010C", "duration": 30, "rate_ms": 200},
|
||
{"type": "obd", "cmd": "03"},
|
||
{"type": "upload"}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Кто что делает
|
||
| Компонент | Файл | Статус | Что добавить |
|
||
|-----------|------|--------|-------------|
|
||
| Тестовый клиент | `TestService.kt` | ✅ гоняет протокол | Сохранение в БД, скрипты, промпты |
|
||
| Транспортный клиент | `ElmForwardService.kt` | ⚠️ требует стабильной связи | Возможно удалить |
|
||
| Стейт-машина | `raw_endpoint.py` | ⚠️ нет пауз | Переделать под батчевую обработку |
|
||
| Mock ELM327 | `mock_elm327.py` | ✅ эмулятор | Добавить задержки для реализма |
|
||
| Сервер приёма | `web/app.py` | ✅ | POST-эндпоинт для заливки сессии |
|
||
|
||
### Договорённости по процессу
|
||
- **НИКОГДА не кодить без прямой команды**
|
||
- Сначала обсуждать → потом делать
|
||
- Коммитить часто, с понятными сообщениями
|
||
- Документировать все ошибки и решения
|
||
|
||
### 08:40 — Ошибка 3: Коммиты не в ту ветку
|
||
Изначально все изменения ушли в `master`, но рабочая ветка — `relay-only`.
|
||
|
||
**Исправление:** переключился на `relay-only`, применил изменения туда, master откатил через `git reset --hard && git push --force`.
|
||
|
||
### 08:45 — Ветка relay-only: добавлена отладка
|
||
В `ElmForwardService.kt` добавлено:
|
||
- `say("🌐 Server: $serverUrl")` — показ URL при подключении
|
||
- `say("← $raw")` — каждое сырое сообщение от устройства
|
||
- `say("→ $cmd")` — каждая команда сервера
|
||
- Обработка ошибок: `⚠️ Server unreachable`, `⚠️ Bad JSON`
|
||
|
||
### 09:00 — Ошибка 4: Cleartext HTTP заблокирован
|
||
Телефон показал `⚠️ Server down: cleartext...`. Android 9+ блокирует HTTP (не-HTTPS) по умолчанию.
|
||
|
||
**Исправление:** в `AndroidManifest.xml`:
|
||
```xml
|
||
android:usesCleartextTraffic="true"
|
||
```
|
||
|
||
### 09:05 — Ошибка 5: APK не скачивается с сервера
|
||
Телефон открыл `http://10.47.183.102:5005`, страница загрузилась, но APK — 404.
|
||
Flask отдаёт статику из `/static/`, а ссылка была `/app-debug.apk`.
|
||
|
||
**Исправление:** ссылка изменена на `/static/app-debug.apk`.
|
||
|
||
### 09:10 — Ошибка 6: Приложение не устанавливается поверх
|
||
Google Play Protect проверил APK, но «приложение не установлено». Причина: каждый CI-билд генерирует новый debug-keystore → сигнатуры не совпадают → Android блокирует установку поверх.
|
||
|
||
**Исправление:** сгенерирован фиксированный `debug.keystore` (пароль `android`, alias `androiddebugkey`) и закоммичен в репо. В CI добавлен шаг:
|
||
```yaml
|
||
- name: Setup debug keystore
|
||
run: cp debug.keystore ~/.android/debug.keystore
|
||
```
|
||
|
||
### 09:15 — Тестовая версия: вместо сервера — локальный протокол
|
||
Пользователь потребовал тестовую версию без сервера. Создан `TestService.kt`:
|
||
- TCP-подключение к mock
|
||
- Самостоятельная отправка AT-команд (ATZ, ATE0, ATL0, ATSP0, ATH1)
|
||
- Чтение VIN (0902)
|
||
- Чтение DTC stored (03), pending (07)
|
||
- Чтение 10 PID'ов (0105..0107)
|
||
- Весь вывод на экран в реальном времени
|
||
|
||
Интерфейс: зелёная кнопка «🧪 ТЕСТ», версия `v0.2.0-test`, URL по умолчанию `10.47.183.102:35000`.
|
||
|
||
### 09:25 — Ошибка 7: Мусор в командах
|
||
Mock получил `01070 ATZ` вместо `ATZ`. Причина: клиент слал `\r`, а mock использовал `readline()` (ждёт `\n`). В буфере накопился мусор.
|
||
|
||
**Исправление в клиенте:** `cmd + "\r\n"` вместо `cmd + "\r"`.
|
||
|
||
**Исправление в mock:** полная переделка чтения — побайтовый разбор:
|
||
```python
|
||
# Было: self.rfile.readline()
|
||
# Стало: читаем по 1 байту, \r и \n — разделители
|
||
ch = self.rfile.read(1)
|
||
if ch in (b'\n', b'\r'):
|
||
# обработать накопленный буфер
|
||
else:
|
||
buf += ch
|
||
```
|
||
|
||
### 09:30 — 100% успешный тест
|
||
Mock лог (чистый!):
|
||
```
|
||
🔌 Подключение
|
||
📥 ATZ → ELM327 v1.5
|
||
📥 ATE0 → OK
|
||
📥 ATL0 → OK
|
||
📥 ATSP0 → OK
|
||
📥 ATH1 → OK
|
||
📥 0902 → VIN: WVWZZZ1KZAW123456
|
||
📥 03 → DTC: P0301, P0303
|
||
📥 07 → DTC: none
|
||
📥 0105..07 → 10 PID'ов
|
||
🔌 Отключение
|
||
```
|
||
|
||
18 команд — 18 ответов. Ноль мусора.
|
||
|
||
---
|
||
|
||
## Архитектура (текущая)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ ТЕСТОВЫЙ РЕЖИМ (работает на 100%) │
|
||
│ │
|
||
│ Телефон (Android) Ноутбук (WSL) │
|
||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||
│ │ TestService │──TCP──→ │ mock_elm327.py │ │
|
||
│ │ │←──TCP── │ :35000 │ │
|
||
│ │ ATZ→ATEx→ │ │ │ │
|
||
│ │ 0902→03/07 │ │ Фейковые данные: │ │
|
||
│ │ PID'ы │ │ VIN, DTC, PID │ │
|
||
│ └──────────────┘ └──────────────────┘ │
|
||
│ │
|
||
├─────────────────────────────────────────────────────┤
|
||
│ РАБОЧИЙ РЕЖИМ (клиент готов, сервер частично) │
|
||
│ │
|
||
│ Телефон Ноутбук │
|
||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||
│ │ElmForwardSvc │─HTTP→ │ Flask :5005 │ │
|
||
│ │ (транспорт) │←─JSON─ │ raw_endpoint.py │ │
|
||
│ │ │ │ ↓ стейт-машина │ │
|
||
│ │ BT/TCP → │ │ ↓ парсер │ │
|
||
│ │ ELM327/mock │ │ ↓ LLM (DeepSeek)│ │
|
||
│ └──────────────┘ └──────────────────┘ │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Все ошибки и решения
|
||
|
||
| # | Ошибка | Причина | Решение |
|
||
|---|--------|---------|---------|
|
||
| 1 | Телефон не слал команды | server_url = obdai.ru (интернет) | Авто-вывод `http://host:5005` |
|
||
| 2 | CI build fail | Gradle 9.5 ≠ AGP 8.2 | Закрепить Gradle 8.5 |
|
||
| 3 | Коммиты в master | Не переключил ветку | Перенос в relay-only, откат master |
|
||
| 4 | Server down: cleartext | Android блокирует HTTP | `usesCleartextTraffic="true"` |
|
||
| 5 | APK 404 на сервере | Flask static path | `/static/app-debug.apk` |
|
||
| 6 | Не устанавливается поверх | Разные debug-ключи | Фиксированный keystore в репо |
|
||
| 7 | Мусор `01070 ATZ` | `\r` vs `\r\n` + readline() | Побайтовое чтение в mock + `\r\n` |
|
||
|
||
---
|
||
|
||
## Ключевые файлы
|
||
|
||
### Android (ветка relay-only)
|
||
| Файл | Назначение |
|
||
|------|-----------|
|
||
| `TestService.kt` | **ТЕСТОВЫЙ** — сам гонит протокол, без сервера |
|
||
| `ElmForwardService.kt` | **РАБОЧИЙ** — транспорт BT/TCP ↔ HTTP |
|
||
| `MainActivity.kt` | UI: кнопки ТЕСТ и Диагностировать |
|
||
| `debug.keystore` | Фиксированный ключ подписи (пароль `android`) |
|
||
| `build-apk.yml` | CI: Gradle 8.5, сборка debug APK |
|
||
|
||
### Сервер (ветка master)
|
||
| Файл | Назначение |
|
||
|------|-----------|
|
||
| `tools/mock_elm327.py` | Эмулятор ELM327 на TCP :35000 |
|
||
| `web/app.py` | Flask сервер :5005 |
|
||
| `web/raw_endpoint.py` | Стейт-машина: парсинг, сессии, LLM |
|
||
| `web/templates/index.html` | Страница загрузки APK |
|
||
|
||
---
|
||
|
||
## Команды для запуска
|
||
|
||
```bash
|
||
# Ноутбук — терминал 1: mock
|
||
python tools/mock_elm327.py
|
||
|
||
# Ноутбук — терминал 2: сервер
|
||
python web/app.py
|
||
|
||
# Телефон: открыть http://10.47.183.102:5005 → скачать APK → кнопка ТЕСТ
|
||
```
|
||
|
||
## Версии APK
|
||
| Версия | Статус | Ключ |
|
||
|--------|--------|------|
|
||
| 0.1.0 | Устарела | Случайный |
|
||
| 0.2.0-test | На телефоне | Случайный |
|
||
| 0.5.0-test | В CI | Фиксированный |
|
||
|
||
---
|
||
|
||
## Что дальше (рабочая версия)
|
||
1. Дописать `raw_endpoint.py` — надёжный парсинг + сохранение в БД
|
||
2. Настроить `DEEPSEEK_API_KEY` для LLM
|
||
3. Протестировать цепочку: телефон → сервер → LLM → диагноз
|
||
4. Подключить к реальному ELM327 в машине (Bluetooth вместо mock)
|