# Сессия 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)