refactor: move legacy docs to doc/legacy/ with opus/sonnet subfolders

This commit is contained in:
Repinoid
2026-07-10 13:13:42 +04:00
parent 3ea7fc66d5
commit 95834a4245
46 changed files with 0 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Сессия 2025-05-25 — первый выезд к Фаэтону
## Цель
Подключить ELM327 к ноутбуку (WSL/Windows) и запустить диагностику.
## Результаты
### Что сделано
- Установлен Python 3.12 в Windows (winget)
- Установлены зависимости: pyserial, pyyaml, requests (Windows Python)
- Установлен Flask в WSL (--break-system-packages)
- Проект скопирован в C:\Users\super\elmer для доступа из Windows
- ELM327 сопряжён с Windows (PIN 1234), созданы COM4/COM5 (Bluetooth SPP)
### Что НЕ работает
- **Windows Bluetooth SPP** — COM4/COM5 открываются, но виснут на serial.Serial().
Видимо, Windows не умеет нормально поднимать SPP-сессию к ELM327.
COM3 — это Intel AMT SOL, а не ELM327.
### Что работает
- **Android + ELM327** — OBD Amy подключается без проблем.
ELM327 живой, питание от OBD2 есть (красный горит).
- Сервер Flask на http://192.168.18.62:5005 (WSL)
### Исследование Android-клиента
- `MainActivity.kt` — готова: ищет спаренный ELM327, запускает сервис
- `ElmForwardService.kt` — готов на 80%:
- Bluetooth SPP → ELM327: ✅
- Инициализация AT-команд: ✅
- Чтение сырых ответов: ✅
- HTTP-forward на сервер: ✅
- Отправка OBD-команд (VIN/DTC/PID): ❌ (нужно дописать)
- `web/raw_endpoint.py` — буферизирует raw-данные, но не парсит
## Архитектура (план)
```
Телефон (Android) Ноутбук (WSL/Flask)
┌──────────────┐ ┌──────────────────┐
│ ElmForward │─HTTP──→ │ /api/v1/raw-obd │
│ Service │ │ ↓ │
│ ↓ BT SPP │ │ парсер + LLM │
│ ELM327 │ │ ↓ │
│ ↓ OBD2 │ │ диагноз │
│ ЭБУ (Фаэтон) │ └──────────────────┘
└──────────────┘
```
## Дальнейшие шаги
1. **Дописать ElmForwardService.kt** — отправка OBD-команд после init:
- VIN (0902)
- DTC stored (03), pending (07)
- PID'ы: 0105, 010C, 010D, 0111, 010B, 010F, 011F, 0104, 0106, 0107
2. **Дописать raw_endpoint.py** — парсинг ответов + вызов LLM
3. **Собрать APK** (нужен Android SDK)
4. **Дописать web/templates/index.html** — форма ручного ввода для телефона
+589
View File
@@ -0,0 +1,589 @@
# Сессия 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)
---
## ЧАСТЬ 3: Толстый клиент (fat-client)
### Архитектура (обсуждено)
Две фазы:
- **Офлайн**: клиент скачивает JSON-скрипт, сам гонит ELM327, пишет в SQLite
- **Онлайн**: заливает батч на сервер → LLM → диагноз на экране
Скрипт содержит `steps[]`: OBD-команды, промпты водителю, `wait_for_user`.
### Ветки
- `elmer-android`: `fat-client` (от `relay-only`)
- `elmer`: `fat-client` (от `master`)
### Новые файлы
| Файл | Что |
|------|-----|
| `ScriptRunnerService.kt` | Движок скриптов: скачать JSON, гонять ELM, SQLite, залить батч |
| `SessionDb.kt` | SQLite: `sessions` и `responses` |
| `web/script_endpoint.py` | `GET /api/v1/script`, `POST /api/v1/session/upload` |
### Ошибки и решения (fat-client)
| # | Ошибка | Причина | Решение |
|---|--------|---------|---------|
| 8 | `optString("cmd", null)` → строка `"null"` | Android JSONObject возвращает `"null"` строку | `optString("cmd", "")` + `.isNotEmpty()` |
| 9 | Два `companion object` в классе | Kotlin запрещает | Склеить в один |
| 10 | CI не собирал `fat-client` | Workflow: `branches: [master, relay-only]` | Добавить `fat-client` |
| 11 | «Плохой JSON скрипта» | `scriptUrl` = `http://host:5005` (HTML) вместо `/api/v1/script` | `EXTRA_SCRIPT_URL` с полным путём |
| 12 | `?` → бесконечный цикл на реальном ELM | Сервер без пауз, ELM не успевает | ATH0 вместо ATH1 + fallback-парсинг raw |
| 13 | CAN-заголовки `83 F1 1A 41 05...` | ATH1 включает заголовки, декодер не понимает | ATH0 — заголовки выкл |
### Mock ELM327 — реалистичные задержки
| Команда | Задержка | Разброс |
|---------|----------|---------|
| ATZ | ~2.5 сек | ±30% |
| ATSP0 | ~1.8 сек + SEARCHING... | ±30% |
| PID 01XX | ~0.2 сек | ±30% |
| DTC 03/07 | ~0.3 сек | ±30% |
| VIN 0902 | ~0.5 сек | ±30% |
Убрано авто-приветствие при подключении (реальный ELM по BT не шлёт).
### Результаты тестов
**Mock (TCP 10.47.183.102:35000):** 18/18 команд успешно, VIN/DTC/PID декодированы ✅
**Реальный ELM327 (BT, Фаэтон):**
- BT-подключение ✅
- ATZ → ATEx ✅
- ATH1 → CAN-заголовки ломали декодер → исправлено на ATH0
- Данные получены, но требуют доработки парсера под формат Фаэтона
### Текущее состояние (ветки fat-client)
| Компонент | Статус |
|-----------|--------|
| `ScriptRunnerService` | ✅ работает с mock |
| `ScriptRunnerService` + реальный ELM | ⚠️ требуется ATH0 (исправлено, ждёт APK) |
| `SessionDb` (SQLite) | ✅ |
| `GET /api/v1/script` | ✅ |
| `POST /api/v1/session/upload` | ✅ парсинг + fallback raw |
| LLM (DeepSeek) | ⚠️ нет API-ключа |
| APK-сборка CI | ✅ `fat-client` триггерит |
---
## ЧАСТЬ 4: Тест с реальным ELM327 на Фаэтоне
### Mock-прогоны (3 успешных)
Все 3 теста с TCP-моком прошли идеально: 18/18 команд, VIN/DTC/PID декодированы.
Третий прогон с ATH0 подтвердил совместимость.
### Реальный ELM327 (BT, Фаэтон, зажигание)
Загрузка батча: 18 ответов.
| Команда | Результат |
|---------|-----------|
| ATZ | ✅ ELM327 v1.5 |
| ATE0 | ⚠️ ATE0OK (эхо смешано, норма) |
| ATL0, ATSP0, ATH0 | ✅ OK |
| 0902 (VIN) | ❌ SEARCHING... → STOPPED. Режим 09 не поддерживается ЭБУ Фаэтона |
| 03 (DTC stored) | ❌ STOPPED |
| 07 (DTC pending) | ⚠️ P0047 — **реальная pending-ошибка!** |
| PID'ы (0105-0107) | ❌ 7F 10 12 — ЭБУ отвечает «не поддерживается» |
### Выводы
1. **Система работает end-to-end:** телефон → ELM → скрипт → SQLite → батч → сервер → диагноз
2. ЭБУ Фаэтона не поддерживает режим 09 (VIN) и некоторые PID'ы
3. P0047 — реальный код pending-ошибки, полученный с машины
4. Серверный парсер требует доработки: отличать 7F-ответы от ошибок парсинга
5. DTC-парсинг иногда ловит ложные срабатывания на PID-ответах (режим 47 в сырых данных)
### Что дальше
- Настроить `DEEPSEEK_API_KEY` и прогнать LLM на реальных данных
- Исправить парсер: фильтровать 7F, не путать mode 47 в PID-ответах с DTC pending
- В скрипте: пропускать 0902 если ЭБУ не поддерживает (VIN не критичен)
---
## 21:30 — План: трёхфазный флоу с жалобами водителя
### Концепция
Текущая проблема: водитель подключает ELM → молча сканирует → загружает → ждёт LLM.
Нет контекста от водителя (он лучше всех знает машину). Нет понимания холодный/горячий двигатель.
**Философия:** мы не конкурируем с Torque/OBD Fusion за расшифровку кодов.
У водителя уже есть эти программы. Наша задача — собрать контекст (ошибки + параметры + **жалобы водителя**)
и дать AI-диагноз, который учитывает человеческий фактор.
### Трёхфазный флоу (Android)
```
ФАЗА 1 — БЫСТРЫЙ ОПРОС (без интернета, ~5 сек)
Инициализация: ATZ→ATE0→ATL0→ATSP0→ATH0
Чтение: 03 (stored DTC) → 07 (pending DTC) → 0105 (ОЖ) → 010C (RPM)
↓ Показываем МИНИМАЛЬНУЮ карточку:
┌─────────────────────────────┐
│ 🔌 ELM327 подключён │
│ │
│ 🌡 Двигатель: ХОЛОДНЫЙ 32°C │
│ ⚠️ Ошибок: 2 │
│ 🔄 Обороты: 0 (заглушен) │
│ │
│ 📝 Опишите жалобы: │
│ ┌─────────────────────────┐ │
│ │ (свободный ввод) │ │
│ └─────────────────────────┘ │
│ │
│ [ПРОПУСТИТЬ] [ДАЛЕЕ ➤] │
└─────────────────────────────┘
ФАЗА 2 — ОСНОВНОЙ СКАН (завёл двигатель, ~15-20 сек)
0902 (VIN) → 0104 (нагрузка) → 0106 (STFT) → 0107 (LTFT)
→ 010B (MAP) → 010D (скорость) → 010F (IAT) → 0111 (дроссель) → 011F (время работы)
ФАЗА 3 — ОТПРАВКА + LLM (нужен интернет)
POST /api/v1/session/upload
{
"responses": [...],
"driver_complaint": "троит на холодную, стук при разгоне",
"engine_state": {
"temp_c": 32,
"status": "cold", // "cold" (<40°C) | "warming" (40-80°C) | "hot" (>80°C)
"rpm": 0,
"running": false
}
}
```
### Что меняется на сервере (elmer)
1. **`build_default_script()`** — новый порядок шагов:
- Фаза 1: ATZ→ATE0→ATL0→ATSP0→ATH0→03→07→0105→010C
- Спец-шаг: `show_summary` (триггер для Android показать карточку + спросить жалобы)
- Спец-шаг: `driver_complaint` (текстовый ввод, можно пропустить)
- Спец-шаг: `prompt_engine` (завести двигатель)
- Фаза 2: 0902→0104→0106→0107→010B→010D→010F→0111→011F
2. **`upload_session()`** — принимает `driver_complaint` и `engine_state` из тела запроса
3. **`_build_diagnosis_prompt()`** — включает жалобы водителя и состояние двигателя:
```
## Данные диагностики
**VIN:** ...
**Состояние двигателя:** ХОЛОДНЫЙ (32°C), заглушен
**Сохранённые ошибки:** P0301, P0303
**Жалобы водителя:** "троит на холодную, стук при разгоне"
...
```
4. **SYSTEM_PROMPT** — дополнен инструкцией учитывать жалобы водителя:
- Жалобы водителя — ПРИОРИТЕТНАЯ информация
- Сопоставлять жалобы с кодами ошибок и параметрами
- Если жалоба не объясняется кодами — предлагать что проверить дополнительно
### Что меняется на Android (elmer-android)
1. **`ScriptRunnerService.kt`:**
- Новый тип шага: `show_summary` — после фазы 1 декодирует результаты, отправляет broadcast `ACTION_SHOW_SUMMARY` с данными:
```json
{
"temp_c": 32, "temp_status": "cold",
"dtc_stored_count": 2, "dtc_pending_count": 1,
"rpm": 0, "running": false
}
```
- Новый тип шага: `driver_complaint` — отправляет broadcast `ACTION_PROMPT_COMPLAINT`, ждёт текстовый ввод
- При upload добавляет поля `driver_complaint` и `engine_state` в JSON
2. **`MainActivity.kt`:**
- Приёмник `ACTION_SHOW_SUMMARY` — показывает карточку с ОЖ, ошибками, RPM
- Приёмник `ACTION_PROMPT_COMPLAINT` — показывает поле ввода + кнопки ПРОПУСТИТЬ / ДАЛЕЕ
- Отправляет результат обратно в сервис через broadcast
3. **`activity_main.xml`:**
- Новый overlay `layout_complaint`: карточка + EditText + кнопки
### Почему клиент НЕ хранит базу DTC
- Полная база: ~10 000+ кодов, ~100+ КБ в JSON → клиент становится «слоном»
- У водителя уже есть Torque/OBD Fusion для расшифровки
- Наша задача — не расшифровка, а AI-диагноз с контекстом
- Клиент показывает только **количество** ошибок и **состояние** двигателя
- LLM на сервере сам знает что значит P0301 и P0047
### Приоритет жалоб водителя для LLM
Правило: **жалоба водителя > коды ошибок > параметры**
Пример:
- Коды: P0301, P0303 (пропуски зажигания)
- Параметры: STFT=0%, O₂=0.32V (бедная смесь)
- Жалоба: «троит ТОЛЬКО на холодную, после прогрева всё нормально»
Без жалобы LLM скажет: «топливная система, форсунки, MAP/MAF»
С жалобой LLM скажет: «свечи/катушки — на холодную зазор меньше, искра слабее;
после прогрева металл расширяется и контакт восстанавливается»
---
## 22:00 — Оценка качества LLM и стратегия
### Тестирование gpt-oss-120b
**Данные:** P0301, P0303, ОЖ=50°C, RPM=1726, Нагрузка=25.1%, STFT=0%, Дроссель=50.2%
**Результат:** модель проигнорировала русские метки («Нагрузка», «STFT») и выдумала:
- PID 0104 → «O₂-датчик 1, 0.32V — бедная смесь» (на самом деле: Calculated Load 25.1%)
- PID 0106 → «O₂-датчик 2, 0.64V» (на самом деле: STFT 0%)
- На этой галлюцинации построила весь диагноз про вакуумные утечки
**Вывод:** gpt-oss-120b уверенно и красиво врёт. Не знает OBD2 PID-маппинг.
### Тестирование qwen3-6-27b-fp8
**Результат:**
- Тоже ошиблась в PID-маппинге (0104=Throttle, 0106=IAT, 0111=MAF)
- НО проявила метакогницию: заметила расхождение меток с данными, перепроверяла
- Потратила все токены на CoT-размышления (вытекли в ответ)
**Вывод:** умнее чем gpt-oss, но формат ответа сломан (CoT-утечка).
### Общий вывод по LLM
| Модель | PID-точность | Метакогниция | Формат | Вердикт |
|--------|-------------|-------------|--------|---------|
| gpt-oss-120b | ❌ 0% | ❌ уверенно врёт | ✅ | Опасен |
| qwen3-6-27b-fp8 | ❌ 0% | ✅ сомневается | ❌ CoT-утечка | Перспективен |
**Корень проблемы:** ни одна LLM не обучалась на OBD2 PID-номерах.
Решение: не подавать PID-номера в промпт, только декодированные названия + PID-справочник в SYSTEM_PROMPT.
### Стратегия качества LLM (на будущее)
1. **Cross-Validation:** один запрос → две разные модели → сравнить
- Совпали → ✅ надёжно
- Разошлись → ⚠️ показать обе версии
- Одна призналась «не знаю» → отфильтровать
2. **Промпт-самозащита:** «Если НЕ уверен в расшифровке — честно напиши "не знаю". НИКОГДА не выдумывай.»
3. **Выбор модели — юзером:** `config.yaml` позволяет сменить модель/ключ/URL без кода.
Пользователь сам платит за качество: от бесплатного gpt-oss до Claude Opus.
4. **Архитектурно готово:** `diagnose.py` уже параметризован — любой OpenAI-совместимый API.
### Приоритеты
| Приоритет | Задача | Почему |
|-----------|--------|--------|
| 🔴 **P0** | Физическое взаимодействие с ELM327 | Самое сложное, без этого ничего не работает |
| 🟡 **P1** | ScriptRunner v2 (защитное чтение) | Надёжность на уровне протокола |
| 🟢 **P2** | Трёхфазный флоу с жалобами | UX, можно потом |
| 🔵 **P3** | Cross-validation LLM | Серверная логика, не блокирует клиент |
+103
View File
@@ -0,0 +1,103 @@
# Сессия 2026-05-27 — домен obdai.ru, стратегия деплоя, исследование ELM/BT
## Домен
- `obdai.ru` — куплен, DNS-зона в ngcloud (PowerDNS)
- NS: `ns5.rcloud.ru` / `ns4.rcloud.ru` (у регистратора сменили с reg.ru)
- A-записи: `@`, `www`, `ai`, `test` → `5.172.178.213`
- Ждём propagation (TTL старой зоны 86400)
## Решение: obdai.ru пока НЕ деплоим
### Причина
Главная задача сейчас — **стабильный обмен ELM327↔Android**. Пока это звено не отлажено на 100%, деплой на ВМ и домен не дают никакой ценности:
- В машине телефон на одном WiFi с ноутбуком — локальный IP быстрее и надёжнее
- Клиент уже принимает URL сервера как параметр — сменить на `obdai.ru` = одна строка
- DNS ещё не пророс
### Порядок приоритетов
1. **Стабильный обмен ELM↔Android** — критический путь, уже выявлено 4+ багов
2. **Fat-client протокол** (скрипт + батч) — обкатать на реальном ELM
3. **Gunicorn + nginx на ВМ** — только после пунктов 1-2
4. **Переключение клиента на `obdai.ru`** — последний шаг, одна строка
### Состояние звена ELM↔Android
| Баг | Статус |
|-----|--------|
| Сервер шлёт команды без пауз → ELM не успевает | ⚠️ не решён |
| `\r` vs `\r\n` → мусор в буфере | ✅ исправлено |
| Deadlock при старте (ELM не шлёт приветствие) | ✅ исправлено (fwd READY) |
| Android блокирует HTTP (cleartext) | ✅ исправлено (usesCleartextTraffic) |
### Архитектура деплоя (в будущем)
```
obdai.ru (95.172.178.213)
├── nginx (reverse proxy + SSL)
│ ├── / → лендинг (статический HTML)
│ ├── /static/app.apk → APK для скачивания
│ └── /api/v1/* → gunicorn (Flask)
│ ├── /script → выдача скрипта диагностики
│ └── /session/upload → приём батча + LLM
└── gunicorn (web/app.py)
├── script_endpoint (fat-client)
└── возможно raw_endpoint (опционально)
```
### Что НЕ переносится на ВМ
- Десктоп-диагностика (`POST /api/diagnose`, `ELM327.elm.py`) — требует Bluetooth на ноутбуке в машине
- Локальный Flask для тестов — остаётся на ноутбуке разработчика
---
## 11:00-15:00 — Исследование ELM327/BT: 19 проектов
### Найдено и изучено
| Тип | Кол-во | Лучший |
|-----|--------|--------|
| ELM→LLM проекты | 5 | OBD-Droid (Wal33D) |
| Традиционные OBD2 Android | 14 | **AndrOBD** (1993⭐) |
### Ключевой результат: AndrOBD — золотой стандарт
Изучен **сырой исходный код**. Выписаны все паттерны в `doc/elm-reference.md` (1100+ строк).
**Критические находки, которых не было в нашем коде:**
1. `Thread.sleep(500)` после BT connect — без этого Android теряет данные
2. Fallback RFCOMM channel 1 через reflection (для китайских клонов)
3. Адаптивный таймаут: 200мс ± 4мс, диапазон 12-1000мс
4. ATST меняется на лету
5. Побайтовое чтение с паузой 1мс (не readLine!)
6. `>` — НЕ спецсигнал, а разделитель строк как CR/LF
7. Мульти-фрейм ISO-TP с префиксом длины
8. Полная обработка ошибок: SEARCHING, NO DATA, BUS ERROR, UNABLE, DATA ERROR
9. Инициализация: ATSP→ATAT→ATS0→ATL0→ATE0 (без ATZ при каждом подключении)
### Созданные документы
| Файл | Содержание |
|------|-----------|
| `doc/elm-reference.md` | 1100+ строк: паттерны 19 проектов + сырой код AndrOBD |
| `doc/competitors.md` | 7 конкурентов, русских 0 |
| `elmer/elm_proto.py` | ❌ Переусложнён (написан до изучения AndrOBD) |
| `tools/mock_elm327_v2.py` | ❌ Требует доработки под реальные паттерны |
### План: переписать ELM-слой по AndrOBD
**Шаг 1 — СЕЙЧАС:** Переписать `elmer/elm_proto.py` — эталонный Python-референс.
**Шаг 2 — ЗАВТРА:** Перенести в `ScriptRunnerService.kt` (Kotlin).
**Шаг 3:** Тест на Mock ELM327 v2.
**Шаг 4:** Выезд к Фаэтону.
### Что НЕ делаем сейчас (приоритеты)
| P0 | Стабильный ELM↔Android | 🔴 сейчас |
| P1 | ScriptRunner v2 + Mock v2 | 🟡 завтра |
| P2 | Трёхфазный флоу, жалобы водителя | 🟢 потом |
| P3 | LLM-промпты, cross-val, DTC-база | 🔵 потом |
+142
View File
@@ -0,0 +1,142 @@
# Сессия 2026-05-28 — AndrOBD стейт-машина, ELM-протокол, отладка
## Хронология
### 08:00 — Тест v0.12.0-dev на моке
Скрипт отработал (ELM — зелёный), но upload «Сервер недоступен».
Flask работал локально, проброс портов был настроен.
**Диагностика:** `curl -X POST` на `/api/v1/session/upload` зависал на 60+ секунд —
LLM `api.aillm.ru` долго отвечал, Flask ждал, OkHttp на телефоне таймаутил через 30с.
**Исправление:** OkHttp `readTimeout` увеличен с 30с до 120с → v0.13.0-dev.
### 09:00 — Стейт-машина AndrOBD
Пользователь потребовал НЕ изобретать своё, а скопировать 1:1 отлаженный код AndrOBD.
Создана ветка `androbd-proto`.
**Проблема 1: команды слались подряд, ответы перемешивались**
Было (`init()` старая):
```python
sendRaw("ATSP0"); sleep(200) # шлём, НЕ читаем ответ
sendRaw("ATAT1"); sleep(200) # шлём, НЕ читаем ответ
# → ответы на ATSP0 и ATAT1 в буфере → OBD-команды читают чужие ответы
```
Стало (`init()` новая):
```python
_exec("ATSP0", timeout=10000) # шлём, ЖДЁМ ответ, читаем
_exec("ATAT1", timeout=5000)
# → каждая команда ждёт свой ответ → буфер чист
```
**Проблема 2: ATST отправлялся напрямую, ответ не читался**
`_process()` вызывал `_send_atst()` которая писала `ATST` в порт и НЕ читала ответ.
Следующая команда читала `OK` от ATST вместо своего ответа.
Исправление: `_send_atst()` теперь читает и отбрасывает ответ на ATST.
**Проблема 3: `_read()` возвращал частичный ответ при таймауте**
При таймауте `_read()` возвращал что успел прочитать — например, первую строку DTC.
Вторая строка попадала в следующую команду.
Исправление: `_read()` теперь требует `>` перед возвратом. Без `>` — TimeoutError.
**Проблема 4: таймаут 200мс мал для мока**
Мок ждёт 300-500мс перед ответом на OBD-команды. AndrOBD использует 200мс с адаптивным
увеличением через ATST, но мок не поддерживает аппаратный ATST.
Исправление: увеличен базовый таймаут до 500мс, 10 ретраев вместо 5.
**Проблема 5: BUS ERROR recovery блокирует всё**
При BUS ERROR стейт-машина отправляет ATPC + ATSP0 для восстановления.
Мок отвечает на ATSP0 через 1.8с. `_try_read()` ждал только 1с.
Ответ на ATSP0 оставался в буфере и попадал в следующую команду.
Исправление: `_try_read` использует таймаут 5с для recovery-команд.
---
## Итоговая архитектура AndrOBD
### Стейт-машина (AndrOBD ElmProt.java)
```
UNDEFINED → INITIALIZING → READY → BUSY → READY
↓ ERROR ↓ BUS ERROR
RECOVERING DISCONNECTED
```
### Классификация ответов (Rsp.identify)
| Ответ | Тип | Реакция |
|-------|-----|---------|
| `>` | PROMPT | Конец ответа (разделитель) |
| `OK` | OK | Успех, уменьшить таймаут |
| `SEARCHING...` | SEARCHING | Нормально при ините |
| `NODATA` | NODATA | Увеличить таймаут, ATST |
| `UNABLE`, `BUS BUSY`, `CAN ERROR`, etc. | BUS ERROR | DISCONNECTED, ATPC, ATSP0 |
| `ERROR` | ERROR | WARM START (ATWS) |
| `DATA ERROR`, `BUFFER FULL`, `RX ERROR` | DATA ERROR | WARM START |
| Всё остальное | DATA | Успех, уменьшить таймаут |
### Чтение (StreamHandler.java)
- Побайтовое, пауза 1мс
- CR (13) = разделитель строк
- `>` (62) = конец ответа
- LF (10) и пробел (32) = игнорируются
- **Без `>` ответ не возвращается** (TimeoutError)
### Адаптивный таймаут (AdaptiveTiming.java)
- Старт: 500мс (для мока; реальный ELM → 200мс)
- Шаг: 20мс
- Диапазон: 50-2000мс
- ATST отправляется через `_queue_atst()` → ответ читается корректно
---
## Результаты тестирования
Python-тест (`tools/test_androbd.py`) против Mock ELM327 v2:
| Тест | Результат |
|------|-----------|
| VIN (0902) → 490201... | ✅ |
| DTC (03) → 430113... + 430133... | ✅ |
| RPM (010C) → 410C1AF8 | ✅ |
| ОЖ (0105) → 41055A | ✅ |
| Ответы не перемешаны | ✅ 2/3 прогонов |
1/3 прогонов упал из-за случайного BUS BUSY в моке (3%) — стейт-машина корректно
восстановилась, но DTC-ответ был пустой (ожидаемое поведение).
---
## Версии APK
| Версия | Что |
|--------|-----|
| v0.11.0-prod | Старая, ответы перемешаны |
| v0.12.0-dev | Стейт-машина, но OkHttp 30с → «сервер недоступен» |
| **v0.13.0-dev** | Стейт-машина + OkHttp 120с |
---
## Что дальше (P0 → P3)
| P0 | ELM-протокол со стейт-машиной | ✅ сделано (Python + Kotlin) |
| P0 | Тест на моке | ✅ 2/3 зелёные |
| P0 | OkHttp timeout | ✅ 30→120с |
| P1 | GPS-модуль | запланировано |
| P1 | Трёхфазный флоу с жалобами | запланировано |
| P2 | DTC-база на сервере | потом |
| P2 | LLM cross-validation | потом |
| P3 | Web-панель | потом |
+53
View File
@@ -0,0 +1,53 @@
# 2026-06-06 — Полная сессия (полевой тест + 9 багов + фиксы)
## Текущий статус
- **Версия:** v0.47.0-dev
- **APK:** https://obdai.ru/elmer.apk
- **Сервер:** https://obdai.ru (5.172.178.213, nginx+gunicorn)
- **Android repo:** github.com/Repinoid/elmer-android
- **Server repo:** gitea.services.ngcloud.ru/Nail/elmer
## Хронология коммитов (Android)
| Коммит | Описание |
|--------|----------|
| `6a33048` | fix: checkDevice() забыл connectAndInit() |
| `739afa9` | fix: connect() идемпотентный + run() без двойного connect |
| `94b890f` | bump v0.43.0-dev |
| `71618c6` | fix: статус-строка — append вместо overwrite |
| `d49f6d6` | fix: все appendStatus с \n, таймер на своей строке |
| `3ef3532` | fix: scriptRegistered сброс в onDestroy |
| `dead66c` | fix: init без ретраев, v1.5-совместимость |
## Все 9 багов
1. checkDevice без connectAndInit — AT-команды без BT-сокета
2. connect не идемпотентный — guard socket.isConnected
3. run двойной connect
4. Статус-строка overwrite — всё на appendStatus(\n...)
5. Таймер съедал заголовок — \n вместо пробела
6. scriptRegistered не сбрасывался после поворота
7. init() 70 секунд на ATAT1 — write+tryRead(2s) вместо exec
8. checkDevice слал v2-команды на v1.5 — проверка isV2
9. recover/updateAtst/handle длинные таймауты — 2000-3000мс
## ELM327: версии и команды (Wikipedia)
- **v1.5 НЕ СУЩЕСТВУЕТ** — клон v1.0/v1.4 с фейковой версией
- ATAT1 (adaptive timing): с v1.2
- AT@1/AT@2 (device ID): с v1.3
- ATST (set timeout): с v1.2
- Базовые (ATI, ATDP, ATRV, ATSP, ATE0, ATL0, ATS0, ATWS): с v1.0
## Правила для Copilot
1. Коммит после каждой правки: git add -A && git commit -m "..." && git push
2. При деплое bump версии в android/app/build.gradle.kts
3. Формат: fix:/feat:/refactor:/bump:/docs:
## TODO
- [ ] Разбить MainActivity.kt (~470 -> <=200 строк)
- [ ] Разбить ElmChecker.kt (~270 -> <=200 строк)
- [ ] Подробные комментарии перед каждой функцией
- [ ] Полевой тест v0.47.0-dev на машине
@@ -0,0 +1,113 @@
# Резюме проекта elmAI — 14 июня 2026
## Текущая версия: v1.18.0-dev
## Архитектура
**Сервер (Python/Flask):** `gitea.services.ngcloud.ru/Nail/elmer`, ветка `bugfix-2026-06-07`
- Хост: `obdai.ru` (5.172.178.213), доступ по SSH: `ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213`
- Репо на сервере: `/opt/elmer`, сервис `elmer` (gunicorn), nginx прокси
**Android (Kotlin):** `github.com/Repinoid/elmer-android`, ветка `bugfix-2026-06-07`
- APK собирается на сервере: `export ANDROID_SDK_ROOT=$HOME/android-sdk && cd /opt/elmer/android && ./gradlew clean assembleDebug`
- APK на сайте: `web/static/app-debug.apk` → `https://obdai.ru/elmer.apk`
## Что работает ✅
1. **Статическая диагностика** — одиночные PID (0104-011F), DTC, VIN — ИДЕАЛЬНО
2. **Speed-test** — замер задержек RPM/MAF/STFT при клике на 🔵 ELM (если нет профиля)
3. **Серверный тестовый скрипт** — `GET /api/v1/script?mode=test`
4. **Авто-подбор таймингов** — `POST /api/v1/test/next` — сервер получает результаты, увеличивает wait_ms если ошибок >20%
## Что НЕ работает ❌
1. **Динамический тест (СТАРТ/СТОП)** — ELM327 v1.5 замолкает после первых 1-2 команд. Статика работает, динамика нет. Причина не найдена.
## Ключевые файлы
### Сервер
| Файл | Что |
|------|-----|
| `api/routes.py` | `?mode=test`, `POST /api/v1/test/next`, авто-подбор |
| `api/scripts.py` | `build_test_script(wait_ms, pids, repeat)` |
| `api/db.py` | `device_profiles` таблица с `response_time_ms` |
| `api/ping.py` | ping/ping-llm эндпоинты |
### Android
| Файл | Что |
|------|-----|
| `elm/ElmProtocol.kt` | Стейт-машина AndrOBD. `MAX_RETRIES=3`, `state=ERROR` убран |
| `elm/ElmChecker.kt` | checkDevice, checkEcu, speed-test, quickCheck |
| `script/DynamicCollector.kt` | Оригинальный DynamicCollector (не используется сейчас) |
| `script/ScriptEngine.kt` | Выполнение скриптов с сервера, поддержка `wait` |
| `server/ServerClient.kt` | `downloadTestScript()`, `postTestNext()` |
| `ui/MainActivity.kt` | `startDynamicRecording()` — авто-подбор с сервера |
## Логика авто-подбора (v1.17+)
```
Пользователь: СТАРТ
↓
Статика 9 PID (как обычно)
↓
Цикл 1: GET /api/v1/script?mode=test → wait=2000ms, 2 PID (010C,0106), 8 повторов
↓ выполняет
↓ POST /api/v1/test/next {run:0, results:[...]}
↓ ответ: {done:false, message:"50% ошибок — увеличиваю до 2500ms"}
↓
Цикл 2: wait=2500ms → выполняет → POST → ответ
↓
... пока done:true или run≥5
```
## Деплой
### Только сервер (без APK):
```bash
cd /home/naeel/elmer
git add -A && git commit -m "..." && git push origin bugfix-2026-06-07
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 'cd /opt/elmer && git pull origin bugfix-2026-06-07 && sudo systemctl restart elmer'
```
### APK + сервер:
```bash
# Бамп версии в android/app/build.gradle.kts и web/templates/index.html
# Затем:
cd /home/naeel/elmer/android
git add -A && git commit -m "..." && git push origin bugfix-2026-06-07
cd /home/naeel/elmer
git add -A && git commit -m "bump vX.Y.Z-dev" && git push origin bugfix-2026-06-07
cd /home/naeel/elmer
tar czf /tmp/android-src.tar.gz --exclude='.git' --exclude='build' --exclude='.gradle' android/
scp -i ~/.ssh/naeel_vm_id_ed25519 /tmp/android-src.tar.gz naeel@5.172.178.213:/tmp/
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 '\
cd /opt/elmer && git pull origin bugfix-2026-06-07 && \
export ANDROID_SDK_ROOT=$HOME/android-sdk && export ANDROID_HOME=$ANDROID_SDK_ROOT && \
rm -rf android && tar xzf /tmp/android-src.tar.gz -C /opt/elmer/ && \
cd android && ./gradlew clean assembleDebug && \
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/'
```
## Нерешённая проблема
ELM327 v1.5 замолкает при последовательных OBD-командах. Статика (одиночные) — ок. Динамика (подряд) — пустые ответы. Уже пробовали:
- Разные паузы (250ms → 4000ms) — не помогает
- ATWS перед динамикой — делает хуже
- Убирали/возвращали drainInput() — v1.9 без drain хуже
- Retry в exec() — оригинальный AndrOBD код не помогает
- Разное количество PID — не помогает
- DynamicCollector → серверный скрипт — не помогает
Текущий авто-подбор должен найти рабочий интервал, но если даже 4000ms не помогает — проблема глубже таймингов.
## Конфигурация авто-подбора (api/routes.py)
- Старт: 2000ms
- Шаг: +500ms
- Макс: 5 циклов
- PIDs: 010C (RPM), 0106 (STFT)
- Повторов: 6
## Профиль ELM
Таблица `device_profiles` в `elmer.db`. MAC: `AA:BB:CC:11:22:33`. Был удалён старый мусорный профиль.