Author SHA1 Message Date
Repinoid 6ca56620c1 docs: update PLANS.md — raw fixes progress 2026-07-10 15:53:11 +04:00
Repinoid afc42381d0 chore: move docs (CHANGELOG, STRUCTURE, QUICKSTART, resume, logo) to doc/, update AGENTS.md and PLANS.md paths 2026-07-10 15:45:47 +04:00
Repinoid 1baaa8dffe chore: remove trash from root — analysis.md, idea.md, legacy-*, .instructions.md, logo.jpg, morda.md 2026-07-10 15:43:22 +04:00
Repinoid bbbc06ec68 docs: PLANS.md — master plan index for all elmAI tasks 2026-07-10 15:40:48 +04:00
Repinoid 81bebcd566 docs: create AGENTS.md — comprehensive project description for AI agents 2026-07-10 15:05:34 +04:00
Repinoid 95834a4245 refactor: move legacy docs to doc/legacy/ with opus/sonnet subfolders 2026-07-10 13:13:42 +04:00
Repinoid 3ea7fc66d5 docs: STRUCTURE.md — описание всех файлов и папок 2026-07-10 12:58:09 +04:00
Repinoid ba48cd2daf fix: /response с фильтром device_id 2026-07-10 12:56:40 +04:00
Repinoid 15caff3204 docs: новое резюме для чата — raw-реле, 12 провалов, правила 2026-07-10 12:54:53 +04:00
Repinoid 129848d772 docs: архитектура — raw-реле, command_queue, два режима 2026-07-10 12:45:31 +04:00
Repinoid 29ace6119e docs: журнал неудач (12 провалов) + архив старого 2026-07-10 12:39:35 +04:00
Repinoid b9a33403fe docs: логика диагностики — два режима 2026-07-05 20:45:29 +04:00
Repinoid f79a1436d5 docs: хронология ошибок relay 2026-07-04 17:26:32 +04:00
Repinoid daa7fcf4a6 bump: v0.3.0-dev на морде 2026-07-04 14:51:40 +04:00
Repinoid caef3cc970 bump: v0.2.6-dev на морде 2026-06-28 19:59:55 +04:00
Repinoid 27883a90fa bump: v0.2.5-dev на морде 2026-06-28 19:46:00 +04:00
Repinoid cd1c0a977e fix: hello чистит старые команды для device_id — новая сессия = чистый старт 2026-06-28 19:35:47 +04:00
Repinoid 95ebc75484 bump: v0.2.4-dev на морде 2026-06-28 19:25:45 +04:00
Repinoid 5c1ba751c9 bump: v0.2.3-dev на морде 2026-06-28 19:13:03 +04:00
Repinoid 3a4701a835 fix: текст ELM Relay v2 на странице 2026-06-28 18:47:02 +04:00
Repinoid 8e1e88ee9a bump: v0.2.2-dev, новое имя ELM Relay v2 2026-06-28 18:43:44 +04:00
Repinoid d3133731d8 fix: ?v=5 в ссылке на APK — обход кеша браузера 2026-06-28 18:18:29 +04:00
Repinoid 06c0b9515e bump: v0.2.1-dev на морде 2026-06-28 11:47:50 +04:00
Repinoid 25c8eb20b9 fix: атомарный dequeue, двойной ATI, MAX_RETRIES 620ms, ретеншн, фильтр device_id 2026-06-28 11:46:00 +04:00
Repinoid c33bd8e2de fix: раздельные execute для CREATE TABLE + INDEX 2026-06-28 11:32:07 +04:00
Repinoid d80cb26ce5 fix: raw_elm.py на SQLite очередь (gunicorn-safe), таблица command_queue в db.py 2026-06-28 11:28:15 +04:00
Repinoid 45a6640dae fix: build_dynamic_script — 6 PID (4 частых + 2 средних/редких), interval_ms 1200ms 2026-06-28 11:26:14 +04:00
Repinoid f283113ab1 fix: канонический init Python (ATE0→..., drain после send, clone detect) 2026-06-28 11:23:12 +04:00
Repinoid d7bdd41df6 security: убрать api_key из config.yaml в env LLM_API_KEY 2026-06-28 11:15:03 +04:00
70 changed files with 1811 additions and 1048 deletions
-57
View File
@@ -1,57 +0,0 @@
# Инструкция для Copilot — проект elmAI
> Последнее обновление: 31 мая 2026 · v0.36.0-dev
## Репозитории
| Репо | Назначение | Хостинг |
|------|-----------|---------|
| `elmer/` (этот) | Сервер Python/Flask | gitea.services.ngcloud.ru/Nail/elmer |
| `elmer/android/` | Android-приложение Kotlin | github.com/Repinoid/elmer-android |
## Деплой
### Сервер (obdai.ru, 5.172.178.213)
- Код: `/opt/elmer` (git clone gitea)
- Ветка: `master` (по умолчанию)
- Деплой: `ssh obdai.ru "cd /opt/elmer && git pull && sudo systemctl restart elmer"`
- Сервис: `gunicorn -w 4 -b 127.0.0.1:8000 web.app:app`
- Прокси: nginx :443 → :8000
- **APK отдавать напрямую через nginx, НЕ через Flask/gunicorn:** `location = /elmer.apk { alias /opt/elmer/web/static/app-debug.apk; }`
### APK
- Сборка: **автоматически GitHub Actions** при пуше в master
- Деплой: CI сам заливает APK на сервер (`appleboy/scp-action`)
- Ссылка: `https://obdai.ru/elmer.apk` → nginx отдаёт напрямую `web/static/app-debug.apk`
### Версионирование
- APK: `android/app/build.gradle.kts``versionName`
- Сайт: `web/templates/index.html` (синхронизировать вручную)
- Документация: в заголовках `.md` файлов
## Правила работы
1. **ЕСЛИ в диалоге содержится ВОПРОС в любой форме — только ответить. НИЧЕГО НЕ ПРЕДПРИНИМАТЬ.** Не писать код, не редактировать файлы, не коммитить, не деплоить. Только прямые императивы («сделай», «исправь», «напиши», «внеси», «задеплой») — команда к действию.
2. **Не выдумывать инфраструктуру.** Никаких Docker, Kubernetes. Всё на голом железе.
3. **Читать документацию перед действиями.** `doc/architecture.md` — канонический источник.
3. **Не редактировать отчёты Опуса.** `doc/opus-review*.md` — только для чтения.
4. **Ключи и токены:** LLM-ключ только на сервере (`config.yaml`), НЕ в APK. `X-Api-Key` приложения — через `BuildConfig.API_KEY` из `local.properties`.
5. **Git:** `elmer/` и `elmer/android/` — отдельные репо, отдельные коммиты.
6. **Ветки:** `master` — продакшен, `opus-fixes` и др. — для правок. Вливать в master когда готово.
7. **После правок:** коммит + пуш + (если сервер) деплой через SSH.
8. **Версия:** менять в трёх местах — `build.gradle.kts`, `index.html`, доки.
9. **Документировать изменения** в `doc/history/YYYY-MM-DD.md` после каждого сеанса работы. Формат: 🔴/🟡/🟢 для приоритета, по файлам.
## Структура сервера
```
elmer/
├── api/ # REST, БД, скрипты, парсер
├── brain/ # LLM-клиент, промпты
├── obd/ # ELM327 стейт-машина
├── web/ # Flask, шаблоны, статика (APK)
├── android/ # Android-приложение (отдельный репо)
├── doc/ # Документация
├── config.yaml # LLM API key, порты
└── run.py # Локальный запуск
```
+285
View File
@@ -0,0 +1,285 @@
# elmAI — полное описание проекта для AI-агентов
> Последнее обновление: 2026-07-10 | Версия app: 1.18.0-dev | Версия raw: 0.4.1-dev
---
## 1. ЧТО ЭТО
elmAI — OBD2-диагностика автомобиля через ELM327-адаптер + LLM (DeepSeek).
Телефон подключается к ELM327 по Bluetooth, собирает данные с ЭБУ, отправляет на сервер, сервер анализирует через LLM и возвращает диагноз.
---
## 2. РЕПОЗИТОРИИ
| Репо | URL | Ветка | Что внутри |
|------|-----|-------|-----------|
| **Сервер** | `gitea.services.ngcloud.ru/Nail/elmer` | `dynamic-tests` | Python Flask + elmAI бэкенд |
| **Android** | `github.com/Repinoid/elmer-android` | `opus-fixes` | Kotlin Android-приложение |
**ВАЖНО**: Android-репо лежит ВНУТРИ серверного: `/home/naeel/elmer/android/`. Это отдельный git-репо со своим remote. Коммитить и пушить надо ИЗНУТРИ `android/`.
---
## 3. СЕРВЕР (obdai.ru, 5.172.178.213)
```
elmer/
├── api/ # Flask API
│ ├── routes.py # Основные эндпоинты (script, upload, chat, probe, sessions)
│ ├── raw_elm.py # Командная очередь для raw-реле (SQLite table command_queue)
│ ├── db.py # SQLite (sessions, device_profiles, command_queue)
│ ├── scripts.py # Сборка диагностических скриптов L0/L1/L2 + dynamic
│ ├── parser.py # Парсинг ответов ELM327 (PID, DTC, VIN)
│ ├── dtc.py # Эндпоинты DTC
│ ├── ping.py # Эндпоинты ping/ping-llm
│ └── config.py # Загрузка config.yaml
├── brain/ # LLM-клиент
│ ├── client.py # Diagnoser — HTTP к DeepSeek
│ └── prompts.py # Промпты для диагностики
├── obd/ # ELM327 протокол (Python)
│ ├── protocol.py # Стейт-машина AndrOBD (ElmProt.java)
│ ├── connection.py, commands.py, classifier.py, probe.py, state.py, timing.py
├── web/ # Flask web
│ ├── app.py # Точка входа, регистрация blueprints
│ ├── templates/index.html # Страница загрузки APK
│ └── static/ # APK-файлы
├── doc/ # ВСЯ документация
├── config.yaml # API-ключи, порты
└── deploy.sh # Скрипт деплоя
```
**Стек**: Python 3, Flask, gunicorn (4 воркера, порт 8000), nginx (:443 → :8000), SQLite.
**Сервис**: `sudo systemctl restart elmer`
---
## 4. ANDROID
### 4.1. Основное приложение (`app/`) — прямая диагностика
**Пакет**: `ru.elmer.client` | **Версия**: 1.18.0-dev (versionCode 38)
```
app/src/main/java/ru/elmer/client/
├── Config.kt # Константы: HOST, SCRIPT_URL, defaultScript(), client()
├── db/SessionDb.kt # SQLite: sessions, responses
├── elm/
│ ├── ElmProtocol.kt # Стейт-машина AndrOBD (1:1 с ElmProt.java)
│ ├── ElmChecker.kt # Проверка ELM: checkDevice(), checkEcu(), scanDtc(), sendRaw()
│ └── ObdDecoder.kt # Декодер PID/DTC/VIN (object-синглтон)
├── script/
│ └── DynamicCollector.kt # Циклический опрос PID с интервалом
├── server/
│ └── ServerClient.kt # HTTP к серверу (OkHttp): ping, pingLlm, chat, getSessions, uploadSession, downloadScript
└── ui/
└── MainActivity.kt # UI: индикаторы, кнопка-трансформер, чат, диагностика, динамический тест
```
**8 классов**. Зависимости: OkHttp 4.12.0, AndroidX, org.json. БЕЗ Room, Coroutines, DI.
**Поток диагностики**: MainActivity → ElmChecker → ElmProtocol → команды → ObdDecoder → SessionDb → ServerClient.uploadSession() → ответ от LLM.
### 4.2. Raw-реле (`raw/`) — ретранслятор команд
**Пакет**: `ru.elmer.raw` | **Версия**: 0.4.1-dev (versionCode 18)
```
raw/src/main/java/ru/elmer/raw/
├── ElmProtocol.kt # 1:1 копия app/ElmProtocol.kt
├── ElmActor.kt # Single-thread executor вокруг ElmProtocol
├── RawRelayService.kt # Foreground-сервис: BT→init→поллинг команд→ответ
├── RelayClient.kt # HTTP к /api/v1/elm/raw/* (OkHttp, БЕЗ X-Api-Key)
└── MainActivity.kt # Минимальный UI (выбор BT, статус, счётчики)
```
**5 классов**. Отдельный APK (`applicationId: ru.elmer.raw`).
**Поток**: RawRelayService → connectBt → ElmProtocol.init() → hello → цикл: pollCommand → sendCommand → postResponse.
**Назначение**: тупой ретранслятор. Сервер диктует команды, телефон передаёт в ELM и возвращает ответы. Используется для интерактивной диагностики через Copilot и тестов 3 мин / 5 мин.
---
## 5. ANDROID — ЧТО СДЕЛАНО (рефакторинг 2026-07-10)
Ветка `opus-fixes`, 8 коммитов:
1. **Config.kt** — единый источник хоста (`https://obdai.ru`), замена всех хардкодов
2. **default_script.json**`assets/`, DEFAULT_SCRIPT из кода удалён
3. **ServerClient** — добавлены `chat()` и `getSessions()`, весь HTTP через OkHttp + X-Api-Key
4. **HttpURLConnection** выпилен из MainActivity
5. **ElmChecker.sendRaw()** — инкапсуляция ElmProtocol, `getElm()` удалён
6. **FQN → import** — все полные имена заменены на нормальные import'ы
7. **Удалён мёртвый код**: ScriptRunnerService, ScriptEngine, UploadProgress + FOREGROUND_SERVICE permissions
**Результат**: 8 классов (было 10), 0 HttpURLConnection, 0 FQN, 0 getElm(), 0 obdai.ru вне Config, весь HTTP аутентифицирован.
---
## 6. ANDROID — ЧТО НЕ СДЕЛАНО (TODO)
### app/
- Разбить MainActivity (~750 строк) — вынести логику из UI
- Разбить ElmChecker (372 строки) — отделить BT от ELM-команд
- v1.5 клоны: деградация после 10-12 команд (ограничение железа)
### raw/
- **deviceId** генерится заново при каждом создании RelayClient — сохранить в SharedPreferences
- **Нет аутентификации** — BuildConfig.API_KEY есть, но RelayClient его не шлёт
- **Нет retry** при ошибках HTTP
- **SERVER_URL** захардкожен в build.gradle.kts и дублируется в Intent extra
- **drainInput() в write()** — потенциальный сдвиг буфера (Неудача #5 из failures-journal.md)
### Сервер
- Script Engine для режимов «3 мин на месте» / «5 мин в движении» — НЕ РЕАЛИЗОВАН
- Дублирование: routes.py и script_endpoint.py (script_endpoint.py — мёртвый)
---
## 7. РЕЖИМЫ ДИАГНОСТИКИ
### Режим 1: Прямая диагностика (app/)
Однократный сбор данных: скрипт → батч ответов → сервер → LLM → диагноз.
### Режим 2: Динамический тест (app/)
Циклический опрос PID. Кнопка СТАРТ/СТОП. Сервер подбирает тайминги через `/api/v1/test/next`.
### Режим 3: Raw-реле (raw/)
Сервер управляет потоком команд. Телефон — тупой ретранслятор.
### Режим 4: Прогрев на месте (raw/, 3 минуты)
3 PID (RPM, темп, дроссель) каждые 2 секунды × 90 циклов = 270 запросов. Оценка прогрева, холостых, реакции на газ.
### Режим 5: В движении (raw/, 5 минут)
Те же 3 PID каждые 2 секунды × 150 циклов = 450 запросов. Нагрузочный тест, динамика разгона.
*Режимы 4 и 5 требует реализации Script Engine на сервере.*
---
## 8. API ЭНДПОИНТЫ
### Основные (app/)
| Метод | Путь | Назначение |
|-------|------|-----------|
| GET | `/api/v1/ping` | Проверка сервера |
| GET | `/api/v1/ping-llm` | Проверка LLM |
| GET | `/api/v1/script?mode=test` | Скрипт диагностики |
| POST | `/api/v1/session/upload` | Загрузка батча + LLM-анализ |
| POST | `/api/v1/chat` | Чат с LLM |
| GET | `/api/v1/sessions` | Список сессий |
| POST | `/api/v1/test/next` | Следующий шаг динамического теста |
| GET/PUT | `/api/v1/elm/profile/<mac>` | Профиль скорости ELM |
### Raw-реле (raw/)
| Метод | Путь | Назначение |
|-------|------|-----------|
| POST | `/api/v1/elm/raw/hello` | Android: «я готов» (чистит старые команды) |
| POST | `/api/v1/elm/raw/cmd` | Copilot/сервер: поставить команду в очередь |
| GET | `/api/v1/elm/raw/cmd?device_id=X` | Android: забрать pending команду |
| POST | `/api/v1/elm/raw/response` | Android: вернуть ответ |
| GET | `/api/v1/elm/raw/response?device_id=X&seq=N` | Copilot: прочитать ответ |
| GET | `/api/v1/elm/raw/status` | Статус устройства |
---
## 9. ДЕПЛОЙ
### 9.1. Сервер
```bash
cd /home/naeel/elmer
git add -A && git commit -m "..." && git push origin dynamic-tests
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 "
cd /opt/elmer && git checkout dynamic-tests && git pull origin dynamic-tests
pip install -r requirements.txt
sudo systemctl restart elmer
"
```
### 9.2. Android APK (app)
```bash
# 1. Bump версии в app/build.gradle.kts (versionCode и versionName)
cd /home/naeel/elmer/android
sed -i 's/versionCode = XX/versionCode = YY/' app/build.gradle.kts
sed -i 's/versionName = "X.Y.Z-dev"/versionName = "X.Y+1.Z-dev"/' app/build.gradle.kts
# 2. Закоммитить + запушить
git add -A && git commit -m "bump vX.Y+1.Z-dev" && git push origin opus-fixes
# 3. Залить исходники на сервер и собрать APK
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 "
rm -rf /opt/elmer/android && tar xzf /tmp/android-src.tar.gz -C /opt/elmer/
cd /opt/elmer/android && gradle wrapper --gradle-version 8.7
export ANDROID_SDK_ROOT=\$HOME/android-sdk
./gradlew :app:clean :app:assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/
"
# 4. Обновить версию в /opt/elmer/web/templates/index.html и /opt/elmer/templates/index.html
```
### 9.3. Raw APK
```bash
# Аналогично app, но:
# - bump версии в raw/build.gradle.kts
# - сборка: ./gradlew :raw:assembleDebug
# - копия: cp raw/build/outputs/apk/debug/raw-debug.apk /opt/elmer/web/static/elm-raw-v022.apk
```
---
## 10. ПРАВИЛА (НЕ НАРУШАТЬ)
1. **НИЧЕГО не делать без прямого указания пользователя.** Даже если видишь проблему — только сказать.
2. **На вопрос — только ответ.** Не продолжать «а ещё могу...», не предлагать помощь.
3. **После выполнения команды — сказать «готово» и ЖДАТЬ.**
4. **Коммит + push после КАЖДОЙ правки.** Один коммит = одна правка. Формат: `fix:`, `feat:`, `refactor:`, `bump:`, `docs:`, `style:`, `chore:`.
5. **При деплое — всегда bump версии.** Инкрементировать патч (Z в X.Y.Z-dev).
6. **ELM327 — только как AndrOBD (ElmProt.java).** Никакой самодеятельности в протоколе. Init: ATSP0→ATAT1→ATS0→ATL0→ATE0. Никакого drainInput() перед write().
7. **Не материться.** Пользователь матерится — ты нет.
8. **Не гадать.** Если не уверен — проверить факты чтением кода.
9. **Код правит ТОЛЬКО пользователь или Copilot по команде.** Не исполнять советы Opus по ELM-командам — Opus специалист по архитектуре кода, а не по ELM327.
---
## 11. КЛЮЧЕВЫЕ ДОКУМЕНТЫ
| Файл | Содержание |
|------|-----------|
| `AGENTS.md` | Этот файл — полное описание проекта |
| `PLANS.md` | Мастер-план: что сделано, что предстоит |
| `doc/architecture.md` | Архитектура сервера и Android |
| `doc/diagnostic-logic.md` | 5 режимов диагностики |
| `doc/failures-journal.md` | 12 провалов при разработке raw-реле |
| `doc/SETUP.md` | Настройка окружения |
| `doc/CHANGELOG.md` | История версий |
| `doc/STRUCTURE.md` | Полное дерево файлов |
| `doc/QUICKSTART.md` | Быстрый старт |
| `doc/resume.txt` | Краткое резюме для нового чата |
| `.github/copilot-instructions.md` | Правила для Copilot |
| `android/README.md` | Документация Android-проекта |
| `android/RAW-FIX-PLAN.md` | План исправлений raw-реле |
| `android/doc/opus-response-arch-2026-07-10.md` | Анализ архитектуры от Opus |
| `android/doc/opus-response-plan-2026-07-10.md` | План рефакторинга от Opus |
---
## 12. КОНТАКТЫ / ДОСТУП
- **Сервер**: 5.172.178.213, SSH: `naeel@5.172.178.213`, ключ: `~/.ssh/naeel_vm_id_ed25519`
- **Домен**: obdai.ru (SSL через certbot)
- **Gitea**: gitea.services.ngcloud.ru/Nail/elmer
- **GitHub**: github.com/Repinoid/elmer-android
+55
View File
@@ -0,0 +1,55 @@
# Планы elmAI
> Последнее обновление: 2026-07-10
## ✅ Сделано
### Android (`:app`) — рефакторинг (2026-07-10)
- [x] Config.kt — единый источник хоста
- [x] default_script.json → assets
- [x] ServerClient.chat() + getSessions() — весь HTTP через OkHttp + X-Api-Key
- [x] HttpURLConnection выпилен из MainActivity
- [x] ElmChecker.sendRaw() вместо getElm()
- [x] FQN → import
- [x] Удалён мёртвый код: ScriptRunnerService, ScriptEngine, UploadProgress
- [x] BtConnector — BT вынесен из ElmChecker
- [x] ChatController — чат вынесен из MainActivity
- [x] DiagnosisRunner — диагностика вынесена из MainActivity
- [x] IndicatorBar — светофоры вынесены из MainActivity
- [x] AGENTS.md — полное описание проекта
- [x] README.md — документация Android-проекта
## 🔜 Предстоит
### Android (`:raw`) — исправления (2026-07-10)
- [x] Config.kt — единый источник SERVER_URL, API_KEY
- [x] deviceId в SharedPreferences — RelayClient(context)
- [x] X-Api-Key — auth() во всех запросах
- [x] Retry HTTP — 3 попытки exponential backoff
- [x] SERVER_URL — только BuildConfig, EXTRA_SERVER_URL удалён
- [x] RawRelayService + MainActivity — полные комментарии
- [ ] detectClone() из app ElmProtocol — v1.5 клоны вешаются на ATAT1
- [ ] drainInput() в write() — потенциальный сдвиг буфера
→ План: [`android/RAW-FIX-PLAN.md`](android/RAW-FIX-PLAN.md)
### Сервер — рефакторинг
- [ ] Разобраться с дублированием routes.py / script_endpoint.py
- [ ] Script Engine для режимов «3 мин на месте» / «5 мин в движении»
### Архитектурные вопросы (не решено)
- [ ] Перенос диагностики в foreground-сервис (риск handoff сокета для v1.5)
- [ ] Разбивка MainActivity дальше (динамический тест, история)
---
## Архив планов
| Файл | Дата | Тема |
|------|------|------|
| `android/doc/opus-response-arch-2026-07-10.md` | 2026-07-10 | Анализ архитектуры от Opus |
| `android/doc/opus-response-plan-2026-07-10.md` | 2026-07-10 | План рефакторинга от Opus |
| `android/doc/opus-arch-questions-2026-07-10.md` | 2026-07-10 | Вопросы Opus по архитектуре |
| `android/RAW-FIX-PLAN.md` | 2026-07-10 | План исправлений raw-реле |
| `doc/diagnostic-logic.md` | — | 5 режимов диагностики |
| `doc/CHANGELOG.md` | — | История версий |
-249
View File
@@ -1,249 +0,0 @@
# Elmer — анализ и план (2026-05-25)
## Суть проекта
Сервис анализа ошибок электроники автомобиля через ELM327 OBD2 + LLM.
## Железо
- **Сканер:** ELM327 Bluetooth v1.5, чип PIC18F25K80 (китайский клон)
- **Ноутбук разработчика:** с Bluetooth, будет соединяться с ELM327 напрямую для отладки
## Целевая аудитория
- Технически любопытный автовладелец (не профессионал, но и не «глубинарий»)
- Уже имеет ELM327 — значит базовое понимание есть
- Хочет понять проблему, а не просто получить код ошибки
## Ключевые требования к ответам
- **Честная уверенность:** «С вероятностью ~80% проблема в X, потому что...»
- **Пояснение логики:** почему именно этот вывод
- **Предупреждения:** «Если НЕ помогло — тогда проверь Y»
- **Никаких категоричных «меняй X»** без 100% уверенности
- **Liability Protection:** нельзя чтобы пользователь сломал машину из-за неверного диагноза
## LLM
- Рассматривается DeepSeek (дёшево через API)
- Или другая простая/дешёвая модель
- Нужен RAG/grounding на реальных repair manuals и TSB, чтобы минимизировать галлюцинации
## Компоненты системы
1. **Android-приложение** (в последнюю очередь)
- Стабильная версия Android (не гоняться за новейшей)
- Максимально простое: минимум кнопок
- Русский язык
- Bluetooth SPP → ELM327
2. **Сервер** (после отладки логики на ноутбуке)
- Принимает данные от приложения
- Формирует запросы к LLM
- Итеративный цикл: запрос → ответ → может запросить ещё параметры или действия от пользователя
- Отдаёт диагноз с пояснениями
3. **LLM-слой**
- Промпт с контекстом автомобиля (VIN → марка/модель/двигатель)
- RAG на базу знаний (ошибки, мануалы, TSB)
- Итеративная диагностика: сервер может переспрашивать LLM
## Протокол диагностики (конечный автомат)
Цикл:
1. Приложение считывает VIN → сервер
2. Приложение считывает коды ошибок → сервер
3. Сервер → LLM: первичный анализ
4. LLM может запросить:
- Дополнительные PID'ы с ЭБУ (live data)
- Действия от пользователя (прогазовать, проехать, считать на холодную и т.д.)
5. Повторять пока не будет достаточно данных для диагноза
6. Финальный ответ: диагноз + степень уверенности + пояснения + что делать
## План разработки (три фазы)
### Фаза 1: Ноутбук + ELM327 (СЕЙЧАС)
- Python-скрипт: Bluetooth → ELM327 → читаем VIN, ошибки, PID'ы
- Отправляем в LLM вручную — отлаживаем логику, промпты, цикл вопросов-ответов
- Никакого сервера, никакого Android
### Фаза 2: Сервер
- Flask/FastAPI — принимать данные, проксировать в LLM
- База знаний / RAG
- State machine диагностики
### Фаза 3: Android-приложение
- Bluetooth SPP (Serial Port Profile) — есть нюансы на Android 12+
- Минималистичный UI
- Отправка данных на сервер, отображение ответов
## Риски
1. **ELM327 v1.5 клон** — неполный протокол, глюки на高速 CAN
2. **PID'ы разные у разных марок** — нужна БД по производителям
3. **LLM галлюцинации** — только grounding/RAG спасёт
4. **Bluetooth SPP на Android 12+** — permissions, pairing
## Ресурсы
- `python-OBD` — библиотека для работы с ELM327 (или свой serial-протокол)
- `pyserial` уже установлен в системе
- DeepSeek API (или OpenRouter как альтернатива)
---
## Структура проекта (создана 2026-05-25)
```
elmer/
├── elmer/ # Python-пакет
│ ├── __init__.py # версия 0.1.0
│ ├── config.py # загрузка config.yaml + подстановка ${ENV}
│ ├── elm.py # ELM327: pyserial, VIN, DTC, PID
│ ├── db.py # SQLite: cars, tokens, llm_messages, ecu_parameters, dtc_codes
│ ├── prompts.py # SYSTEM_PROMPT + build_user_prompt()
│ └── diagnose.py # DeepSeek API (OpenAI-совместимый)
├── config.yaml # настройки (BT-порт, API-ключ, PID'ы)
├── requirements.txt # pyserial, pyyaml, requests
├── run.py # главный вход: ELM → данные → LLM → печать + сохранение
├── idea.md # исходная задумка
└── analysis.md # этот файл
```
## Запуск (в салоне авто)
```bash
# 1. Установить зависимости
pip install -r requirements.txt
# 2. Сопрячь ELM327 по Bluetooth
bluetoothctl pair 11:22:33:44:55:66
# (в config.yaml прописан порт /dev/rfcomm0)
# 3. Запустить
DEEPSEEK_API_KEY=sk-... python run.py
```
Что произойдёт:
1. Подключится к ELM327
2. Прочитает VIN
3. Считает ошибки (stored mode 03 + pending mode 07)
4. Считает параметры (обороты, температура, скорость, дроссель, MAP, IAT, топливные тримы)
5. Отправит в DeepSeek → напечатает диагноз
6. Сохранит всё в `elmer.db` (SQLite)
---
## Десктопный UI (Web)
Для тестирования на ноутбуке (не тыкать грязным пальцем в телефон):
- `web/app.py` — Flask (порт 5005), один endpoint `/api/diagnose` (POST)
- `web/templates/index.html` — одна кнопка, тёмная тема, результат
- В будущем этот же код — прототип серверного API
Запуск:
```bash
DEEPSEEK_API_KEY=sk-... python web/app.py
# Открыть http://localhost:5005
```
## Версии для пользователей (будущее)
- **Android** — Kotlin/Java, Bluetooth SPP
- **Windows** — тот же веб-интерфейс в WebView (или Electron, или просто браузер)
- Общий серверный API между ними
---
---
## Архитектура клиент-сервер (решено 2026-05-25)
### Принцип: тонкий клиент
Клиент ничего не знает о диагнозе. Только транспорт:
```
ELM327 ←Bluetooth SPP→ Android Client ←HTTP JSON→ Сервер ←API→ DeepSeek
```
### Клиент как универсальный SDK
- Пользователь вводит URL своего сервера (или используется наш по умолчанию)
- Протокол HTTP/JSON документирован — любой backend
- Два режима работы клиента:
1. **«Опрос» (основной):** клиент сам читает VIN + DTC + PID'ы, шлёт JSON серверу
2. **«Ретранслятор» (расширенный):** сервер шлёт сырые AT-команды, клиент пересылает ответ
### Десктоп
- Браузер (Chrome) → локальный Flask → pyserial → ELM327
- Отдельного «приложения» для Windows не нужно
- Тот же `web/app.py` — и тестовый UI, и прототип сервера
### Открытость и доверие
| Что | Где | Зачем |
|---|---|---|
| **Клиент (Android)** | GitHub (открытый) | Доверие — любой может проверить код, собрать сам |
| **Сервер (Python)** | Gitea (закрытый) | API-ключи, логика, коммерческая часть |
| **Публикация** | RuStore | Бесплатно, модерация = дополнительное доверие |
### Git-стратегия
- `gitea.services.ngcloud.ru/Nail/elmer` — разработка сервера (текущий репо)
- `github.com/Nail/elmer-android` — клиент (будет создан), лицензия MIT
- Серверный репо на GitHub НЕ публикуем
---
## TODO / Дорожная карта
### 🔴 Фаза 1 — отладка на ноутбуке (ближайшая)
- [ ] Проверить Bluetooth на ноутбуке: `hciconfig`, `bluetoothctl`
- [ ] Сопрячь ELM327: `bluetoothctl pair <MAC>`
- [ ] Привязать к `/dev/rfcomm0`: `rfcomm bind 0 <MAC>`
- [ ] `pip install -r requirements.txt`
- [ ] `DEEPSEEK_API_KEY=sk-... python run.py` — консольный тест
- [ ] `DEEPSEEK_API_KEY=sk-... python web/app.py` — веб-тест
- [ ] Подключить реальную машину, считать VIN + ошибки + параметры
- [ ] Оценить качество ответа DeepSeek
### 🟡 Фаза 2 — сервер
- [ ] Выделенный сервер/ВМ (или Kubernetes pod)
- [ ] Flask → production (gunicorn)
- [ ] API: `/api/diagnose` (как сейчас), + `/api/sessions`, + `/api/history/<vin>`
- [ ] База: миграция SQLite → PostgreSQL
- [ ] RAG: база знаний (repair manuals, TSB) для grounding
- [ ] State machine: итеративные запросы к LLM
- [ ] HTTPS (Let's Encrypt)
### 🟢 Фаза 3 — Android-клиент
- [ ] Создать репо `github.com/Nail/elmer-android`
- [ ] Kotlin, minSdk ~24 (Android 7), targetSdk 34
- [ ] Bluetooth SPP: поиск, pairing, connect, read/write
- [ ] OBD2 парсер: VIN (0902), DTC (03/07), PID (01XX)
- [ ] UI: одна кнопка «Диагностика» + поле ввода URL сервера
- [ ] HTTP-клиент: POST JSON на сервер, показ ответа (Markdown → текст)
- [ ] Permissions: только BLUETOOTH + INTERNET
- [ ] Подпись APK, публикация в RuStore
- [ ] README: как собрать самому, как использовать с чужим сервером
---
## Заметки по итогам обсуждения
- **Гипотеза подтверждена:** DeepSeek уже дал полный анализ по логам VCDS. Лучше гугла.
- **Модерация RuStore:** проверяет вредоносный код и permissions. BLUETOOTH + INTERNET — вопросов не вызовет.
- **Sideload (APK напрямую):** проверок нет, но permissions видны до установки.
- **Chrome на Android НЕ может:** Web Bluetooth API только BLE, Web Serial API не поддерживается.
- **Termux с Python:** теоретически, но Bluetooth-доступ сложен.
- **ELM327 v1.5 (PIC18F25K80):** китайский клон. Неполный протокол, возможны глюки. Держать в уме.
- **Нет готового аналога:** ниша новая (LLM + OBD2), старые приложения без AI-анализа.
---
*Продолжить: тестировать в салоне авто с реальным ELM327.*
+24
View File
@@ -160,6 +160,30 @@ class Database:
self.conn.commit()
# command_queue — для raw-ретранслятора (gunicorn-safe)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS command_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
seq INTEGER NOT NULL,
cmd TEXT NOT NULL,
timeout_ms INTEGER DEFAULT 500,
drain_first INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
raw_response TEXT,
elapsed_ms INTEGER,
prompt INTEGER,
error TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
sent_at TEXT,
responded_at TEXT
)
""")
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_cq_device_status_seq ON command_queue(device_id, status, seq)"
)
self.conn.commit()
# ── sessions ──────────────────────────────────────────
def get_cached_response(self, request_id: str) -> dict | None:
+144 -140
View File
@@ -23,23 +23,16 @@ import threading
import time
from flask import jsonify, request, Blueprint
from api.db import Database
logger = logging.getLogger("elmer.raw_api")
# ══════════════════════════════════════════════════════════
# Глобальное состояние
# Локальное состояние (только для прямого подключения ELM)
# ══════════════════════════════════════════════════════════
_raw_mode = False
_raw_elm = None # локальный RawELM
# Очередь команд (удалённый режим)
_lock = threading.Lock()
_pending_cmd: dict | None = None # команда, которую ждёт Android
_pending_seq: int = 0 # номер последней команды
_last_response: dict | None = None # последний ответ от ELM327
_device_ready: bool = False # Android подключён и готов
_device_info: dict = {} # информация об устройстве (из hello)
_history: list[dict] = [] # история команд через Android
_raw_elm = None # локальный RawELM (прямое подключение к серверу)
def is_raw_mode() -> bool:
@@ -119,7 +112,14 @@ def raw_available():
@bp.route("/api/v1/elm/raw/log", methods=["GET"])
def raw_log():
if not _raw_elm:
return jsonify({"log": _history, "count": len(_history)})
with Database() as db:
rows = db.conn.execute(
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE status='done' ORDER BY id DESC LIMIT 50"
).fetchall()
history = [{"seq": r["seq"], "cmd": r["cmd"], "raw": r["raw_response"],
"elapsed_ms": r["elapsed_ms"], "prompt": r["prompt"], "error": r["error"]}
for r in rows]
return jsonify({"log": history, "count": len(history)})
return jsonify({"log": _raw_elm.log, "count": len(_raw_elm.log)})
@@ -130,185 +130,189 @@ def raw_mode_control():
data = request.get_json(silent=True) or {}
on = data.get("raw_mode", False)
set_raw_mode(on)
return jsonify({"raw_mode": _raw_mode, "has_local_elm": _raw_elm is not None,
"device_ready": _device_ready})
return jsonify({"raw_mode": _raw_mode, "has_local_elm": _raw_elm is not None,
"device_ready": _device_ready})
with Database() as db:
device = db.conn.execute(
"SELECT device_id FROM command_queue ORDER BY id DESC LIMIT 1"
).fetchone()
return jsonify({
"raw_mode": _raw_mode,
"has_local_elm": _raw_elm is not None,
"device_ready": device is not None,
})
# ══════════════════════════════════════════════════════════
# УДАЛЁННЫЙ РЕЖИМ — Android-ретранслятор
# УДАЛЁННЫЙ РЕЖИМ — Android-ретранслятор (SQLite-очередь)
# ══════════════════════════════════════════════════════════
@bp.route("/api/v1/elm/raw/hello", methods=["POST"])
def raw_hello():
"""Android сообщает: «я подключился к ELM327, готов принимать команды».
Body: {
"device_id": "android-xyz",
"elm_version": "ELM327 v1.5",
"protocol": "A4",
"voltage": "12.3V"
}
"""
global _device_ready, _device_info, _pending_cmd, _pending_seq, _last_response
"""Android: «я подключился, готов принимать команды»."""
data = request.get_json(silent=True) or {}
with _lock:
_device_ready = True
_device_info = {
"device_id": data.get("device_id", "unknown"),
"elm_version": data.get("elm_version", "?"),
"protocol": data.get("protocol", "?"),
"voltage": data.get("voltage", "?"),
"connected_at": time.time(),
}
_pending_cmd = None
_pending_seq = 0
_last_response = None
logger.info(f"RawELM: device ready — {_device_info['device_id']} "
f"({_device_info['elm_version']}, proto {_device_info['protocol']})")
device_id = data.get("device_id", "unknown")
with Database() as db:
# Очистить старые команды для этого устройства (новая сессия)
db.conn.execute("DELETE FROM command_queue WHERE device_id = ?", (device_id,))
db.conn.commit()
logger.info(f"RawELM: device ready — {device_id} "
f"({data.get('elm_version', '?')}, proto {data.get('protocol', '?')})")
return jsonify({"ok": True, "seq": 0})
@bp.route("/api/v1/elm/raw/cmd", methods=["POST"])
def raw_enqueue_cmd():
"""Copilot: поставить команду в очередь для Android.
Body: {
"cmd": "0105",
"timeout_ms": 500,
"drain_first": false
}
"""
global _pending_cmd, _pending_seq
"""Copilot: поставить команду в очередь."""
data = request.get_json(silent=True)
if not data or "cmd" not in data:
return jsonify({"error": "missing 'cmd'"}), 400
cmd = data["cmd"].strip()
if not cmd:
return jsonify({"error": "empty cmd"}), 400
with _lock:
_pending_seq += 1
_pending_cmd = {
"cmd": cmd,
"timeout_ms": data.get("timeout_ms", 500),
"drain_first": data.get("drain_first", False),
"seq": _pending_seq,
}
logger.info(f"RawELM: enqueued #{_pending_seq}{cmd}")
return jsonify({"ok": True, "seq": _pending_seq, "cmd": cmd})
device_id = data.get("device_id", "unknown")
tmo = data.get("timeout_ms", 500)
drain = 1 if data.get("drain_first") else 0
with Database() as db:
cur = db.conn.execute("SELECT COALESCE(MAX(seq), 0) + 1 FROM command_queue WHERE device_id = ?", (device_id,))
seq = cur.fetchone()[0]
db.conn.execute(
"INSERT INTO command_queue (device_id, seq, cmd, timeout_ms, drain_first, status) VALUES (?,?,?,?,?,'pending')",
(device_id, seq, cmd, tmo, drain)
)
db.conn.commit()
logger.info(f"RawELM: enqueued #{seq}{cmd}")
return jsonify({"ok": True, "seq": seq, "cmd": cmd})
@bp.route("/api/v1/elm/raw/cmd", methods=["GET"])
def raw_dequeue_cmd():
"""Android: забрать команду из очереди.
Returns:
200 {"cmd": "0105", "seq": 1, ...} — есть команда
204 — нет команды, полли дальше
"""
global _pending_cmd
device_id = request.args.get("device_id", "")
with _lock:
if not _device_ready:
return jsonify({"error": "device not ready"}), 503
if _pending_cmd is None:
return "", 204 # No Content — полли дальше
cmd = _pending_cmd
_pending_cmd = None # забрали
logger.info(f"RawELM: dequeued #{cmd['seq']}{cmd['cmd']} (device={device_id})")
return jsonify(cmd)
"""Android: забрать команду из очереди."""
device_id = request.args.get("device_id", "unknown")
with Database() as db:
row = db.conn.execute(
"SELECT id, seq, cmd, timeout_ms, drain_first FROM command_queue WHERE device_id=? AND status='pending' ORDER BY seq LIMIT 1",
(device_id,)
).fetchone()
if not row:
return "", 204
cur = db.conn.execute(
"UPDATE command_queue SET status='sent', sent_at=datetime('now') WHERE id=? AND status='pending'",
(row["id"],)
)
db.conn.commit()
if cur.rowcount == 0: # перехватил другой воркер
return "", 204
result = {"seq": row["seq"], "cmd": row["cmd"], "timeout_ms": row["timeout_ms"], "drain_first": bool(row["drain_first"])}
logger.info(f"RawELM: dequeued #{row['seq']}{row['cmd']}")
return jsonify(result)
@bp.route("/api/v1/elm/raw/response", methods=["POST"])
def raw_post_response():
"""Android: отправить ответ ELM327 на сервер.
Body: {
"device_id": "android-xyz",
"seq": 1,
"cmd": "0105",
"raw": "41 05 5C",
"prompt": true,
"elapsed_ms": 48,
"bytes": 8,
"error": null
}
"""
global _last_response, _history
"""Android: отправить ответ ELM327."""
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "empty body"}), 400
device_id = data.get("device_id", "unknown")
seq = data.get("seq", 0)
raw_resp = data.get("raw", "")
elapsed = data.get("elapsed_ms", 0)
prompt = 1 if data.get("prompt") else 0
error = data.get("error")
with _lock:
_last_response = {
"seq": data.get("seq", 0),
"cmd": data.get("cmd", ""),
"raw": data.get("raw", ""),
"prompt": data.get("prompt", False),
"elapsed_ms": data.get("elapsed_ms", 0),
"bytes": data.get("bytes", 0),
"error": data.get("error"),
"received_at": time.time(),
}
_history.append(dict(_last_response))
if len(_history) > 1000:
_history = _history[-500:]
logger.info(f"RawELM: response #{_last_response['seq']}{_last_response['raw'][:80]}")
with Database() as db:
db.conn.execute(
"UPDATE command_queue SET status='done', raw_response=?, elapsed_ms=?, prompt=?, error=?, responded_at=datetime('now') WHERE device_id=? AND seq=?",
(raw_resp, elapsed, prompt, error, device_id, seq)
)
db.conn.commit()
logger.info(f"RawELM: response #{seq}{raw_resp[:80]}")
return jsonify({"ok": True})
@bp.route("/api/v1/elm/raw/response", methods=["GET"])
def raw_get_response():
"""Copilot: прочитать последний ответ от ELM327.
Query: ?wait=30 — ждать до 30 сек пока появится новый ответ
"""
global _last_response, _pending_cmd
wait_s = int(request.args.get("wait", 0))
"""Copilot: прочитать последний ответ."""
seq = int(request.args.get("seq", 0))
wait_s = int(request.args.get("wait", 0))
device_id = request.args.get("device_id", "")
if wait_s > 0:
# Ждём пока появится ответ на команду с seq > указанного
dl = time.time() + wait_s
while time.time() < dl:
with _lock:
if _last_response and _last_response["seq"] > seq:
return jsonify(_last_response)
if _pending_cmd is None and _last_response:
# команд в очереди нет, ответ уже есть
return jsonify(_last_response)
with Database() as db:
if device_id:
row = db.conn.execute(
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE seq > ? AND status='done' AND device_id=? ORDER BY seq DESC LIMIT 1",
(seq, device_id)
).fetchone()
else:
row = db.conn.execute(
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE seq > ? AND status='done' ORDER BY seq DESC LIMIT 1",
(seq,)
).fetchone()
if row:
return jsonify({"seq": row["seq"], "cmd": row["cmd"], "raw": row["raw_response"],
"elapsed_ms": row["elapsed_ms"], "prompt": bool(row["prompt"]), "error": row["error"]})
time.sleep(0.5)
with _lock:
if _last_response is None:
return jsonify({"error": "no response yet", "seq": 0})
return jsonify(_last_response)
with Database() as db:
if device_id:
row = db.conn.execute(
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE status='done' AND device_id=? ORDER BY seq DESC LIMIT 1",
(device_id,)
).fetchone()
else:
row = db.conn.execute(
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE status='done' ORDER BY seq DESC LIMIT 1"
).fetchone()
if not row:
return jsonify({"error": "no response yet", "seq": 0})
return jsonify({"seq": row["seq"], "cmd": row["cmd"], "raw": row["raw_response"],
"elapsed_ms": row["elapsed_ms"], "prompt": bool(row["prompt"]), "error": row["error"]})
@bp.route("/api/v1/elm/raw/status", methods=["GET"])
def raw_status():
"""Copilot: статус Android-устройства."""
with _lock:
return jsonify({
"device_ready": _device_ready,
"device_info": _device_info,
"pending_cmd": bool(_pending_cmd),
"pending_seq": _pending_seq,
"last_response_seq": _last_response["seq"] if _last_response else 0,
"history_count": len(_history),
})
"""Copilot: статус устройства."""
device_id = request.args.get("device_id", "unknown")
with Database() as db:
# Чистка старых записей (старше 1 дня)
db.conn.execute(
"DELETE FROM command_queue WHERE responded_at < datetime('now', '-1 day')"
)
db.conn.commit()
last = db.conn.execute(
"SELECT seq, status FROM command_queue WHERE device_id=? ORDER BY id DESC LIMIT 1",
(device_id,)
).fetchone()
pending = db.conn.execute(
"SELECT COUNT(*) FROM command_queue WHERE device_id=? AND status='pending'", (device_id,)
).fetchone()[0]
total = db.conn.execute(
"SELECT COUNT(*) FROM command_queue WHERE device_id=? AND status='done'", (device_id,)
).fetchone()[0]
return jsonify({
"device_ready": last is not None,
"pending_cmd": pending > 0,
"pending_seq": pending,
"last_response_seq": last["seq"] if last else 0,
"history_count": total,
})
@bp.route("/api/v1/elm/raw/history", methods=["GET"])
def raw_history():
"""Copilot: история всех команд через Android."""
"""Copilot: история команд."""
n = int(request.args.get("n", 50))
with _lock:
return jsonify({"history": _history[-n:], "total": len(_history)})
with Database() as db:
rows = db.conn.execute(
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE status='done' ORDER BY id DESC LIMIT ?",
(n,)
).fetchall()
history = [{"seq": r["seq"], "cmd": r["cmd"], "raw": r["raw_response"],
"elapsed_ms": r["elapsed_ms"], "prompt": bool(r["prompt"]), "error": r["error"]}
for r in rows]
return jsonify({"history": history, "total": len(history)})
+17 -15
View File
@@ -109,26 +109,28 @@ def build_script_for_level(level: int) -> dict:
return build_script_l0()
def build_dynamic_script() -> dict:
"""Скрипт для динамического теста — 12 PID, опрос каждые 250мс."""
def build_dynamic_script(interval_ms: int = 1200) -> dict:
"""Скрипт для динамического теста — 6 стабильных PID.
Частые (каждый цикл): RPM, нагрузка, дроссель, ОЖ
Средние (каждые 3 цикла): STFT, LTFT — реализуется на клиенте
Редкие: IAT, MAP, напряжение — раз в 5+ циклов
interval_ms: период цикла (default 1200мс для клона v1.5).
6 PID × ~200мс/PID ≈ 1.2с.
"""
return {
"version": 1,
"title": "Динамический тест",
"mode": "dynamic",
"interval_ms": 250,
"interval_ms": interval_ms,
"steps": [
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка"},
{"id": "pid_05", "cmd": "0105", "desc": "ОЖ"},
{"id": "pid_06", "cmd": "0106", "desc": "STFT"},
{"id": "pid_07", "cmd": "0107", "desc": "LTFT"},
{"id": "pid_0B", "cmd": "010B", "desc": "MAP"},
{"id": "pid_0C", "cmd": "010C", "desc": "RPM"},
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость"},
{"id": "pid_0E", "cmd": "010E", "desc": "Зажигание"},
{"id": "pid_0F", "cmd": "010F", "desc": "IAT"},
{"id": "pid_10", "cmd": "0110", "desc": "MAF"},
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель"},
{"id": "pid_1F", "cmd": "011F", "desc": "Время работы"},
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка", "freq": "high"},
{"id": "pid_05", "cmd": "0105", "desc": "ОЖ", "freq": "high"},
{"id": "pid_0C", "cmd": "010C", "desc": "RPM", "freq": "high"},
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель", "freq": "high"},
{"id": "pid_06", "cmd": "0106", "desc": "STFT", "freq": "mid"},
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость", "freq": "low"},
],
}
+1 -1
View File
@@ -2,7 +2,7 @@
# Значения вида ${VAR} подставляются из переменных окружения
llm:
api_key: "sk-78ec529c1eba4ba69995091046c9fa33"
api_key: "${LLM_API_KEY}"
model: "deepseek-v4-flash"
base_url: "https://api.deepseek.com/v1"
View File
View File
+181
View File
@@ -0,0 +1,181 @@
# Структура репозитория elmAI
> **elmAI** — сервис OBD2-диагностики автомобилей через ELM327 + LLM (DeepSeek).
> Android-приложение + Python-сервер. Анализ ошибок ЭБУ, live-параметры, диагноз через ИИ.
---
## Корневые файлы
| Файл | Назначение |
|------|-----------|
| `run.py` | Главная точка входа (CLI). Подключается к ELM327 по Bluetooth, читает VIN/DTC/PID, сохраняет в SQLite, отправляет в LLM. Запуск: `python run.py [--no-llm] [--port]` |
| `config.yaml` | Конфигурация: LLM (API key, модель), ELM327 (порт, baudrate), список PID для чтения |
| `requirements.txt` | Зависимости Python: pyserial, pyyaml, requests, flask, flask-cors |
| `deploy.sh` | Скрипт деплоя на сервер obdai.ru: обновление репо, venv, systemd-сервис (gunicorn), nginx, SSL (certbot) |
| `legacy-deploy.sh` | Устаревшая версия деплоя (ветка fat-client, GPT-OSS модель) |
| `analysis.md` | Анализ и план проекта от 2025-05-25: железо, ЦА, требования, компоненты, протокол, риски |
| `idea.md` | Концепция сервиса: OBD2 + AI диагностика, три компонента (сервер, Android, десктоп) |
| `morda.md` | Макет UI (морда) v2: иконки-светофоры, кнопка-трансформер, поле вывода, поле ввода |
| `QUICKSTART.md` | Быстрый старт: тест с mock ELM327, тест в машине, веб-интерфейс |
| `resume.txt` | Резюме проекта для нового чата: версия v0.77.0-dev, инструкции по деплою |
| `legacy-resume.txt` | Устаревшее резюме (v0.48.0, ветка master) |
| `CHANGELOG.md` | Полное описание проекта: архитектура, модули, эндпоинты, БД, стейт-машина (актуально v0.95.0-dev) |
| `token.txt` | Токены и ключи: gitea, DeepSeek API, SSH-ключ VM |
| `STRUCTURE.md` | **Этот файл** — описание структуры репозитория |
---
## `api/` — Flask REST API + БД + парсинг
| Файл | Назначение |
|------|-----------|
| `__init__.py` | Пустой (пакет) |
| `config.py` | Загрузка `config.yaml` с подстановкой `${VAR}` из переменных окружения. Кэш через `@lru_cache` |
| `db.py` | SQLite-база данных (WAL mode). Таблицы: `sessions`, `cars`, `diagnostic_tokens`, `llm_messages`, `ecu_parameters`, `dtc_codes`, `command_queue`. Класс `Database` |
| `routes.py` | Основные эндпоинты: `GET /api/v1/script`, `POST /api/v1/session/upload`, `POST /api/v1/chat`, `POST /api/v1/elm/probe`. Проверка X-Api-Key, сборка промпта для LLM |
| `dtc.py` | DTC-эндпоинты: `POST /api/v1/dtc/decode` (расшифровка кодов из справочника), `POST /api/v1/dtc/upload`. Справочник из `doc/dtc_codes.txt` |
| `ping.py` | Эндпоинты проверки: `GET /api/v1/ping` (доступность), `GET /api/v1/ping-llm` (проверка LLM с адаптивным кэшем 60с/7с) |
| `parser.py` | Парсинг батча ELM-ответов: VIN (из decoded и raw HEX), DTC stored/pending (mode 03/07), PID-параметры (mode 01) |
| `scripts.py` | Сборка диагностических скриптов трёх уровней: L0 (5 PID + stored DTC), L1 (8 PID + VIN + stored/pending), L2 (14 PID + калибровки). + динамические скрипты |
| `raw_elm.py` | Сырое взаимодействие с ELM327: локальный режим (прямое подключение) и удалённый (через Android-реле). HTTP-очередь команд |
---
## `brain/` — LLM-клиент и промпты
| Файл | Назначение |
|------|-----------|
| `__init__.py` | Пустой (пакет) |
| `client.py` | `Diagnoser` — HTTP-клиент к OpenAI-совместимому API (api.aillm.ru). Модели: `gpt-oss-120b`, `qwen3-6-27b-fp8`. Обработка ошибок: Timeout, 429, 5xx, 4xx |
| `prompts.py` | `SYSTEM_PROMPT` (10 правил для диагноза: расшифровка, отклонения, степени уверенности), `DYNAMIC_PROMPT` (для динамических тестов), `build_user_prompt()` |
---
## `obd/` — ELM327-протокол (Python, порт AndrOBD)
| Файл | Назначение |
|------|-----------|
| `__init__.py` | Пустой (пакет) |
| `connection.py` | `SerialTransport` — транспортный слой: открыть serial/Bluetooth порт, побайтовое чтение до `>`, запись + flush |
| `protocol.py` | `AndrOBD` — стейт-машина ELM327 (порт ElmProt.java). Состояния: UNDEFINED → INITIALIZING → READY → BUSY → ERROR. Канонический init, обработка BUS ERROR |
| `state.py` | `State` (enum состояний) и `Rsp` (классификация ответов: PROMPT, OK, SEARCHING, ERROR, BUS_ERROR, NODATA и т.д.) |
| `timing.py` | `AdaptiveTiming` — адаптивный таймаут (50..2000мс). Увеличивается при таймаутах, уменьшается при быстрых ответах, сброс при BUS ERROR |
| `commands.py` | Каталог AT-команд ELM327 с метаданными: name, desc, level (0/1/2), safe. L0 (универсальные), L1 (ATAT), L2 (CAF/CFC) |
| `classifier.py` | Классификация сырых ответов ELM327 и определение уровня устройства по ответам на пробинг |
| `probe.py` | Пробинг ELM327: трехуровневый каскад (L0→L1→L2), каждая команда с таймаутом 500мс, без ретраев |
| `raw_console.py` | `RawELM` — сырой слой без стейт-машины: только send/read/drain/available. Для изучения поведения ELM327 |
---
## `web/` — Веб-интерфейс (Flask)
| Файл | Назначение |
|------|-----------|
| `app.py` | Точка входа Flask: регистрация эндпоинтов, режим RAW (блокировка всех, кроме `/elm/raw/*`), раздача APK, главная страница |
| `script_builder.py` | Сборка диагностических скриптов (устаревшая версия — дублирует `api/scripts.py`) |
| `script_endpoint.py` | Эндпоинты скриптов (устаревшая версия — дублирует `api/routes.py`) |
| `script_parser.py` | Парсинг батча (устаревшая версия — дублирует `api/parser.py`) |
| `templates/index.html` | Главная HTML-страница: скачивание APK, десктоп-диагностика, отображение результатов |
| `static/style.css` | Стили: тёмная тема, оранжевый акцент, карточки, спиннеры, DTC-бейджи |
---
## `android/` — Android-приложение (Kotlin)
| Файл | Назначение |
|------|-----------|
| `build.gradle.kts` | Корневой build-файл Gradle: плагины Android + Kotlin |
| `settings.gradle.kts` | Настройки Gradle-проекта |
| `gradle.properties` | Свойства Gradle |
| `gradlew` | Gradle Wrapper (исполняемый) |
| `app/build.gradle.kts` | Модуль app: minSdk 24, OkHttp 4.12.0, зависимости |
| `app/src/` | Исходники Android-приложения (Kotlin) — основной клиент + raw-реле |
| `raw/build.gradle.kts` | Модуль raw — ретранслятор ELM327 через HTTP |
| `doc/opus-review-android.md` | Рецензия кода Android-приложения |
| `doc/opus-questions-android.md` | Вопросы по Android после рецензии |
| `gradle/wrapper/` | Gradle Wrapper JAR и настройки |
---
## `tools/` — Вспомогательные утилиты
| Файл | Назначение |
|------|-----------|
| `mock_elm327.py` | Эмулятор ELM327 v1.5 через TCP (порт 35000). Отвечает на AT-команды, PID, DTC, VIN. Для тестирования без реального сканера |
| `mock_elm327_v2.py` | Улучшенный мок: поддержка `>` как разделителя, ATST, случайные ошибки (BUS BUSY, UNABLE), побайтовая отправка |
| `elm_console.py` | Интерактивная консоль ELM327 (сырой режим). Команды: ATZ, 0105, !drain, !timeout, !log. Для изучения поведения ELM |
| `elm_relay.py` | Интерактивная консоль удалённого управления ELM327 через Android-реле. HTTP-команды: `!status`, `!history`, `!mode` |
| `test_androbd.py` | Тест AndrOBD-протокола против Mock ELM327 v2: проверка что ответы не перемешаны (VIN → DTC → RPM → coolant) |
| `analyze_sessions.py` | Анализ сессий из SQLite: статистика команд, ошибок, пустых ответов |
---
## `scripts/` — Скрипты развёртывания
| Файл | Назначение |
|------|-----------|
| `setup-bt.sh` | Настройка Bluetooth-сопряжения с ELM327: поиск, pairing, rfcomm bind на /dev/rfcomm0 |
---
## `tests/` — Автотесты
| Файл | Назначение |
|------|-----------|
| `test_all.py` | Сквозные тесты (без LLM): сборка скриптов, парсер ELM-ответов (VIN из decoded/raw, DTC, PID), работа с БД, идемпотентность, эндпоинты |
---
## `doc/` — Документация и исследования
| Файл | Назначение |
|------|-----------|
| `architecture.md` | Полная архитектура проекта: два режима (app/raw), схема, эндпоинты, модули |
| `roadmap.md` | План развития проекта |
| `research.md` | Исследования и заметки |
| `competitors.md` | Анализ конкурентов |
| `diagnostic-logic.md` | Логика диагностики |
| `dynamic-diagnostics-analysis-2026-06-14.md` | Анализ динамической диагностики |
| `dynamic-tests.md` | Динамические тесты |
| `dtc_codes.txt` | Справочник DTC-кодов (формат: `P0301=Пропуски зажигания цилиндр 1`) |
| `elm-reference.md` | Справочник по ELM327 |
| `elm-raw-relay-plan.md` | План raw-реле |
| `failures-journal.md` | Журнал отказов |
| `field-test-2026-06-07.md` | Полевой тест |
| `git-guide.md` | Гайд по Git |
| `morda-v2.md` | Макет UI v2 |
| `mpscholar-automotive-sensing-actuators.md` | Обучающий материал |
| `opinion-dynamic-diagnostics-2026-06-14.md` | Мнение по динамической диагностике |
| `relay-mistakes-2026-07-04.md` | Ошибки реле |
| `test-cases.md` | Тест-кейсы |
| `SETUP.md` | Инструкция по установке |
| `audit-2026-06-07.md` | Аудит проекта |
| `audit-prompt.md` | Промпт для аудита |
| `opus-review.md` | Рецензия кода (Opus) |
| `opus-fix-plan.md` | План исправлений по рецензии |
| `opus-questions.md` | Вопросы к Opus |
| `opus-questions-post-tests-2026-06-29.md` | Вопросы после тестов |
| `opus-recheck-request-2026-06-28.md` | Запрос на перепроверку |
| `opus-review-android.md` | Рецензия Android-кода |
| `sonnet-apk-cache-questions-2026-06-28.md` | Вопросы по кэшу APK |
| `android-bugs-2026-05-25.md` | Баги Android |
| `claude-analysis-elm.md` / `claude-analysis-elm-v2.md` | Анализ ELM от Claude |
| `claude-request-elm.md` / `claude-request-elm-v2.md` | Запросы к Claude по ELM |
| `session-*.md` | Логи сессий разработки по датам |
| `session-resume-2026-06-14.md` | Резюме сессии |
| `history/` | Архив старых заметок, логов сессий и результатов тестов по датам |
| `history/2026-05-31.md` | Лог сессии 31 мая |
| `history/2026-06-03.md` | Лог сессии 3 июня |
| `history/2026-06-05.md` | Лог сессии 5 июня |
| `history/2026-06-06.md` | Лог сессии 6 июня |
| `history/2026-06-07.md` | Лог сессии 7 июня |
| `history/2026-06-07-plans.md` | Планы на 7 июня |
| `history/2026-06-10.md` | Лог сессии 10 июня |
| `history/opus-recheck-analysis-2026-06-28.md` | Анализ перепроверки Opus |
| `history/opus-sonnet-comparison-2026-06-29.md` | Сравнение Opus vs Sonnet |
| `history/session-summary-2026-06-28.md` | Сводка сессии 28 июня |
| `history/sonnet-response-post-tests-2026-06-29.md` | Ответ Sonnet после тестов |
| `history/test-results-2026-06-28.md` | Результаты тестов 28 июня |
| `history/test-results-2026-07-04.md` | Результаты тестов 4 июля |
+156 -89
View File
@@ -1,144 +1,211 @@
# Архитектура elmAI
> v0.77.0-dev, 7 июня 2026
> v0.42.0-dev (основной APK) / v0.4.0-dev (raw-реле), 10 июля 2026
## Общая схема
```
📱 Android (elmer-android)
Bluetooth
🔌 ELM327
│ OBD-ответы
📱 Android (ScriptRunnerService)
│ HTTPS POST /api/v1/session/upload
├── Bluetooth ──── 🔌 ELM327 ──── ECU (OBD-II)
├── app/ (основное приложение)
│ └── HTTPS POST /api/v1/session/upload
└── raw/ (реле)
└── HTTP-поллинг /api/v1/elm/raw/*
🌐 Сервер (5.172.178.213)
├── nginx :443 → gunicorn :8000
├── obd/ — ELM327 протокол
├── brain/ — LLM-клиент
├── api/ — REST, БД, скрипты
── web/ — точка входа Flask, статика
├── api/raw_elm.py — командная очередь для реле (SQLite)
├── api/ — REST, БД, скрипты
├── brain/ — LLM-клиент
── obd/ — ELM327 протокол (Python)
└── web/ — точка входа Flask, статика
```
## Два режима работы
### Режим 1: Основное приложение (`app/`)
Прямая диагностика: телефон → ELM → скрипт → батч → сервер → LLM
### Режим 2: Raw-реле (`raw/`)
Тупой ретранслятор: сервер диктует команды, телефон передаёт в ELM и возвращает ответы.
Используется для интерактивной диагностики и тестирования.
```
Copilot/сервер Android (raw) ELM327
│ │ │
├─ POST /cmd ──────────→│ │
│ ├─ sendCommand() ─────→│
│ │←─ raw response ──────┤
│←─ POST /response ─────┤ │
│ │ │
├─ GET /response?wait=N─→ (поллинг ответа) │
│←─ {raw: "41 0C ..."}─┤ │
```
## Структура сервера
```
elmer/
├── obd/ # Модуль 1: ELM327 протокол
── protocol.py # AndrOBD — стейт-машина (1:1 копия AndrOBD)
# State, Rsp, AdaptiveTiming
├── api/
── config.py # Загрузка config.yaml
├── db.py # SQLite (sessions, cars, dtc, command_queue)
│ ├── routes.py # Эндпоинты: script, upload, chat, probe
│ ├── dtc.py # Эндпоинты DTC
│ ├── ping.py # Эндпоинты проверки
│ ├── scripts.py # Сборка диагностических скриптов
│ ├── parser.py # Парсинг ответов ELM327
│ └── raw_elm.py # Командная очередь для реле (SQLite command_queue)
├── brain/ # Модуль 2: LLM-взаимодействие
│ ├── client.py # Diagnoser — HTTP к api.aillm.ru
│ └── prompts.py # SYSTEM_PROMPT для диагностики
├── brain/
│ ├── client.py # Diagnoser — HTTP к LLM
│ └── prompts.py # SYSTEM_PROMPT для диагностики
├── api/ # Модуль 3: REST API + БД
── config.py # Загрузка config.yaml
│ ├── db.py # SQLite (sessions, cars, dtc)
│ ├── routes.py # Все эндпоинты (5 шт)
│ ├── scripts.py # Сборка диагностических скриптов
│ └── parser.py # Парсинг ответов ELM327
├── obd/
── protocol.py # Python-версия AndrOBD стейт-машины
├── web/ # Веб-интерфейс
│ ├── app.py # Точка входа Flask
│ ├── templates/index.html
│ └── static/app-debug.apk
├── tools/ # Разработка
├── mock_elm327_v2.py # Мок ELM327 (TCP)
│ └── test_androbd.py # Тесты стейт-машины
├── web/
│ ├── app.py # Точка входа Flask
│ ├── templates/
│ └── index.html # Страница загрузки APK
└── static/
│ ├── app-debug.apk # Основной APK
└── elm-raw-v022.apk # Raw-реле APK
├── doc/ # Документация
│ ├── architecture.md # Этот файл
│ ├── roadmap.md
│ └── session-*.md # Логи сессий
├── config.yaml # LLM API key, порты
└── requirements.txt
```
## Взаимодействие модулей
```
web/app.py
└─ import api/routes.py
├─ import api/config.py → config.yaml
├─ import api/db.py → SQLite
├─ import api/scripts.py → сборка скриптов
├─ import api/parser.py → парсинг батча
├─ import brain/client.py → Diagnoser → api.aillm.ru
└─ import brain/prompts.py → SYSTEM_PROMPT
```
Каждый модуль можно тестировать отдельно. Циклических зависимостей нет.
## API эндпоинты
| Метод | Путь | Описание | Время |
|---|---|---|---|
| GET | /api/v1/ping | Проверка сервера | ~5мс |
| GET | /api/v1/ping-llm | Проверка LLM | ~2с |
| GET | /api/v1/script?mode= | Скрипт диагностики | ~50мс |
| POST | /api/v1/session/upload | Загрузка батча + LLM | ~30-120с |
| POST | /api/v1/chat | Вопрос к LLM | ~5-15с |
### Основные (app/)
## Android (отдельный репо)
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/v1/ping | Проверка сервера |
| GET | /api/v1/ping-llm | Проверка LLM |
| GET | /api/v1/script?mode= | Скрипт диагностики |
| POST | /api/v1/session/upload | Загрузка батча + LLM |
| POST | /api/v1/chat | Вопрос к LLM |
### Raw-реле (raw/)
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/v1/elm/raw/hello | Android: «я готов» |
| POST | /api/v1/elm/raw/cmd | Copilot: поставить команду в очередь |
| GET | /api/v1/elm/raw/cmd | Android: забрать команду |
| POST | /api/v1/elm/raw/response | Android: отправить ответ |
| GET | /api/v1/elm/raw/response | Copilot: прочитать ответ (с device_id) |
| GET | /api/v1/elm/raw/status | Статус устройства |
## SQLite: command_queue
Таблица для очереди команд raw-реле:
```sql
CREATE TABLE command_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
seq INTEGER NOT NULL,
cmd TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending | done
raw_response TEXT,
elapsed_ms INTEGER,
prompt INTEGER DEFAULT 0,
error TEXT,
created_at TEXT DEFAULT (datetime('now')),
responded_at TEXT
);
```
- `hello` чистит старые команды для device_id
- `cmd` (POST) добавляет команду со статусом `pending`
- `cmd` (GET) атомарно забирает pending → обновляет статус
- `response` (POST) сохраняет ответ
- `response` (GET) возвращает ответы с фильтром по device_id и seq
## Android (отдельный репо: elmer-android)
```
elmer-android/app/src/main/java/ru/elmer/client/
├── ElmProtocol.kt # ELM327 стейт-машина
├── ObdDecoder.kt # Декодер PID/DTC/VIN
├── ServerClient.kt # HTTP к серверу (retry 3x)
├── ScriptEngine.kt # Движок скриптов
├── ScriptRunnerService.kt # Фоновая диагностика
├── SessionDb.kt # Локальная история
├── MainActivity.kt # UI
├── TestService.kt # (устарел)
└── ElmForwardService.kt # (устарел)
android/
├── app/ — основное приложение (диагностика)
│ └── src/.../ru/elmer/client/
│ ├── ElmProtocol.kt # ELM327 стейт-машина (AndrOBD)
│ ├── ObdDecoder.kt # Декодер PID/DTC/VIN
├── ServerClient.kt # HTTP к серверу
│ ├── ScriptEngine.kt # Движок скриптов
│ ├── ScriptRunnerService.kt # Фоновая диагностика
│ ├── SessionDb.kt # Локальная история
│ └── MainActivity.kt # UI
└── raw/ — реле (ретранслятор команд)
└── src/.../ru/elmer/raw/
├── ElmProtocol.kt # 1:1 копия app/ElmProtocol.kt
├── ElmActor.kt # Single-thread executor
├── RawRelayService.kt # Foreground-сервис: BT + поллинг
├── RelayClient.kt # HTTP-клиент к /api/v1/elm/raw/*
└── MainActivity.kt # Минимальный UI (выбор BT, статус)
```
## RawRelayService — главный цикл
```
relayLoop():
1. Bluetooth connect
2. ElmProtocol.init() # AndrOBD: ATSP0→ATAT1→ATST→ATS0→ATL0→ATE0
3. client.hello() # HTTP → /api/v1/elm/raw/hello
4. while running:
cmd = client.pollCommand() # GET /api/v1/elm/raw/cmd
raw = actor.sendBlocking(cmd, 5000)
client.postResponse(seq, cmd, raw)
```
## CI/CD и деплой
**Никаких сторонних CI/CD-сервисов (GitHub Actions, GitLab CI, Jenkins и т.д.). Всё вручную.**
### Сервер (elmer)
```
Локально: git push gitea master
Сервер: ssh obdai.ru
cd /opt/elmer && git pull origin master
sudo systemctl restart elmer
Сервер: ssh obdai.ru
cd /opt/elmer && git pull origin master
sudo systemctl restart elmer
```
- Репо: `gitea.services.ngcloud.ru/Nail/elmer`
- Ветка: `master`
- Сервис: `gunicorn -w 4 -b 127.0.0.1:8000 web.app:app`
- Прокси: nginx :443 → 127.0.0.1:8000
- Конфиг: `/opt/elmer/config.yaml`
### Android APK (elmer-android)
### Android APK
**Два репо:** `elmer/` (gitea) и `elmer/android/` (github)
```
Локально: cd android && ./gradlew assembleDebug
scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/
# Основной APK
cd android && ./gradlew :app:assembleDebug
scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/
# Raw-реле APK
cd android && ./gradlew :raw:assembleDebug
scp raw/build/outputs/apk/debug/raw-debug.apk obdai.ru:/opt/elmer/web/static/elm-raw-v022.apk
```
- Репо: `github.com/Repinoid/elmer-android`
- Сборка: `./gradlew assembleDebug`
- Доставка: `scp` на сервер в `web/static/app-debug.apk`
- Ссылка для пользователей: `https://obdai.ru/elmer.apk`
- APK обновляется **только** при изменениях в Android-коде
- Репо Android: `github.com/Repinoid/elmer-android`
- Ветка: `opus-fixes`
- APK на сервере: `/opt/elmer/web/static/`
### Версионирование
| Где | Файл |
|-----|------|
| APK | `android/app/build.gradle.kts``versionName` |
| Сайт | `web/templates/index.html` |
| Документация | заголовки `.md` файлов |
| Основной APK | `android/app/build.gradle.kts``versionName` |
| Raw APK | `android/raw/build.gradle.kts``versionName` |
| Сайт (основной) | `/opt/elmer/templates/index.html` |
| Сайт (raw) | `/opt/elmer/templates/index.html` (та же строка) |
**Версию менять одновременно во всех трёх местах.**
**Важно:** Flask использует `/opt/elmer/templates/index.html`. Файл `/opt/elmer/web/templates/index.html` — резервная копия.
+85
View File
@@ -0,0 +1,85 @@
# Логика диагностики — два режима
## Архитектура
Три слоя:
1. **Relay (Android)** — тупой ретранслятор: получил команду → отправил в ELM → вернул ответ
2. **Script Engine (сервер)** — отправляет команды по расписанию, собирает ответы
3. **LLM (сервер)** — анализирует собранные данные
---
## Режим 1: Прогрев на месте (3 минуты)
**Цель**: снять показания на холостых, погазовать, оценить прогрев
**Скрипт**:
```
Каждые 2 секунды — 3 PID: 010C (RPM), 0105 (темп), 0111 (дроссель)
Итого: 90 циклов × 3 PID = 270 запросов
При 70% успехе: ~190 ответов (~63 на каждый PID)
```
**Метрики**:
- RPM: мин/макс/среднее на холостых, отклик на газ
- Температура: скорость прогрева (°C/мин), выход на рабочую
- Дроссель: положение на холостых, реакция на педаль
---
## Режим 2: В движении (5 минут)
**Цель**: нагрузочный тест, динамика разгона, поведение под нагрузкой
**Скрипт**:
```
Каждые 2 секунды — 3 PID: 010C (RPM), 0105 (темп), 0111 (дроссель)
Итого: 150 циклов × 3 PID = 450 запросов
При 70% успехе: ~315 ответов (~105 на каждый PID)
```
**Метрики**:
- RPM: разгон/торможение, переключение передач
- Температура: стабильность под нагрузкой
- Дроссель: соответствие нагрузке
---
## Обработка ошибок
| Ответ | Действие |
|-------|----------|
| `41xx...` (hex данные) | Сохранить, декодировать |
| `STOPPED` | Пропустить, relay сам восстановит |
| Пусто / таймаут | Пропустить, следующий запрос |
| 3 таймаута подряд | Пауза 2с (клон перегрелся) |
---
## Выходные данные для LLM
```json
{
"mode": "warmup",
"duration_s": 180,
"total_requests": 270,
"successful": 190,
"pids": {
"010C": {
"min": 680, "max": 3200, "avg": 1250,
"samples": [680, 720, 750, ...]
},
"0105": {
"start_temp": 25, "end_temp": 87,
"warmup_rate": 0.34,
"samples": [25, 27, 30, ...]
},
"0111": {
"min": 0, "max": 45,
"samples": [0, 0, 1.5, ...]
}
}
}
```
LLM получает: сырые данные + вопрос пользователя → ответ с диагнозом.
+192
View File
@@ -0,0 +1,192 @@
# ELM327 Relay — полный журнал неудач
## Дата: 2026-07-0405 | Версия: v0.4.0-dev
---
## Итоговое состояние
| Параметр | Значение |
|----------|----------|
| Версия на сервере | v0.4.0-dev (commit `ebc41e0`) |
| Основа | 1:1 с `app/.../ElmProtocol.kt` (AndrOBD) |
| Клон 1 (v1.5) | 12/12 → 13/16 (72-81%) |
| Клон 2 (v1.5) | 13/18 (72%) |
| Характер отказов | Деградация после 10-12 команд |
| Двигатель заглушен | PID не работают (STOPPED), только ATRV |
---
## НЕУДАЧА 1: Игнорирование рабочего кода
**Симптом**: `raw/ElmProtocol.kt` написан с нуля вместо копирования `app/ElmProtocol.kt`
**Когда**: создание raw-модуля (июнь 2026)
**Что сделано не так**: В репозитории УЖЕ был `app/src/.../ElmProtocol.kt` — рабочий протокол на основе AndrOBD. Вместо копирования написан новый с «улучшениями».
**Корневая причина**: предположение что raw-реле требует особой логики. Не проверено что `app/ElmProtocol.kt` уже работает.
**Исправлено**: v0.4.0-dev — raw/ElmProtocol.kt = 1:1 копия app/ElmProtocol.kt
**Урок**: всегда проверять существующий код перед созданием нового.
---
## НЕУДАЧА 2: Доверие Opus без проверки исходников
**Симптом**: init() с ATE0 первым, ATST96 для клонов, drain перед write
**Когда**: июнь–июль 2026
**Что сделано не так**: Opus посоветовал — реализовано без сверки с AndrOBD (ElmProt.java). AndrOBD 10 лет в проде, все «улучшения» были ошибками.
**Корневая причина**: доверие внешнему анализу вместо проверки первоисточника.
**Исправлено**: полная сверка с ElmProt.java (fr3ts0n/AndrOBD). Все отличия задокументированы.
**Урок**: первоисточник (код) > любой анализ.
---
## НЕУДАЧА 3: ATST96 убивает клон v1.5
**Симптом**: клон зависает намертво после init, даже ATRV не отвечает
**Когда**: v0.3.0-dev
**Что сделано не так**: добавлен `ATST96` (150×4=600ms) для клонов. Команда не поддерживается клоном v1.5 и вешает его.
**Корневая причина**: предположение что клону нужен фиксированный таймаут. AndrOBD использует `updateAtst()` с адаптивным таймаутом для всех устройств.
**Исправлено**: ATST96 удалён. `updateAtst()` теперь вызывается всегда (как в AndrOBD).
**Урок**: не добавлять команды, которых нет в AndrOBD.
---
## НЕУДАЧА 4: ATI в init() ломает порядок
**Симптом**: буферный сдвиг после каждой команды
**Когда**: v0.3.0v0.3.7-dev
**Что сделано не так**: `ATI` вставлен в середину init() для детекта клона. AndrOBD не использует ATI — версия определяется из ответа ELM на MODEL.
**Корневая причина**: желание детектить клон внутри init(). AndrOBD определяет клона иначе.
**Исправлено**: v0.4.0-dev — ATI удалён из init(). Init: ATSP0→ATAT1→ATST→ATS0→ATL0→ATE0.
**Урок**: не вставлять команды в init(), которых нет в AndrOBD.
---
## НЕУДАЧА 5: drainInput() в write()
**Симптом**: ответ команды N читается как ответ команды N+1
**Когда**: все версии raw
**Что сделано не так**: `write()` вызывает `drainInput()` перед отправкой. Если ответ предыдущей команды запаздывает, drain съедает его и наступает сдвиг буфера на 1 позицию.
**Корневая причина**: AndrOBD не использует drain. Синхронная модель (sendCommand → ждать ответ) требует drain для очистки мусора, но drain сам создаёт мусор если ответ приходит с задержкой.
**Статус**: НЕ ИСПРАВЛЕНО. Остаётся в коде v0.4.0-dev. Без drain буфер забивается мусором, с drain — сдвиг на 1 команду. Меньшее из зол.
**Урок**: синхронная модель принципиально ограничена. AndrOBD использует асинхронную (поток читает → handleTelegram) и не имеет этой проблемы.
---
## НЕУДАЧА 6: Freeze detection через ATRV
**Симптом**: ответ "12.6V" читается как ответ на PID, все команды возвращают одно и то же
**Когда**: v0.3.0-dev
**Что сделано не так**: каждые 10 команд слался ATRV для проверки залипания. Ответ ATRV попадал в буфер и читался как ответ следующего PID.
**Корневая причина**: ATRV — AT-команда, не OBD. Её ответ не должен смешиваться с PID-ответами. В синхронной модели это неизбежно.
**Исправлено**: удалено в v0.3.2-dev.
**Урок**: не смешивать AT-команды с OBD-командами в одном потоке.
---
## НЕУДАЧА 7: AT-команды в handle()
**Симптом**: после STOPPED/BUS_ERROR следующая команда получает мусор
**Когда**: v0.1.xv0.3.3-dev
**Что сделано не так**: handle() при ошибках слал ATPC, ATWS, ATSP0. Эти AT-команды отправлялись внутри обработки ответа текущей команды, их ответы загрязняли буфер.
**Корневая причина**: handle() вызывается из exec() который находится в процессе чтения ответа. Отправка AT-команд внутри exec() создаёт вложенные чтения, которые путают буфер.
**Исправлено**: в v0.4.0-dev handle() шлёт AT-команды только для BUS_ERROR и ERROR (как в AndrOBD). Для STOPPED — только state tracking.
**Урок**: AT-команды в handle() допустимы только если их ответы полностью потребляются (tryRead с таймаутом).
---
## НЕУДАЧА 8: Recovery ATSP0 в relayLoop
**Симптом**: тест показывает 0/60 (все таймауты)
**Когда**: v0.3.5-dev
**Что сделано не так**: после STOPPED или пустого ответа relayLoop слал ATSP0 через sendBlocking. ATSP0 блокирует single-thread executor на 3+ секунд. Все последующие команды ждут в очереди, тест видит таймауты.
**Корневая причина**: recovery выполнялся в том же потоке что и обработка команд. Команды накапливались в очереди executor'а.
**Исправлено**: удалено в v0.4.0-dev. Recovery теперь только в handle().
**Урок**: recovery должен быть частью протокольного уровня (ElmProtocol), а не оркестратора (relayLoop).
---
## НЕУДАЧА 9: Дренаж 0100 после init
**Симптом**: relay не отвечает ни на одну команду после init
**Когда**: v0.3.6-dev
**Что сделано не так**: после init отправлялся 0100 для поглощения буферного сдвига. Дренаж сам создавал сдвиг и дезориентировал клон.
**Корневая причина**: попытка «подчистить» буфер создаёт новую команду и новый ответ, который тоже надо чистить — бесконечная рекурсия.
**Исправлено**: удалено в v0.3.7-dev.
**Урок**: не пытаться чистить буфер дополнительными командами.
---
## НЕУДАЧА 10: Тест читал чужие ответы
**Симптом**: тест показывал OK (411100) когда relay возвращал пустоту
**Когда**: все тесты до исправления сервера
**Что сделано не так**: тестовый скрипт вызывал `/response?wait=3` без `device_id`. Сервер возвращал ответы от других устройств или предыдущих сессий.
**Корневая причина**: серверный SQL не фильтровал по device_id. Тест не обновлял seq.
**Исправлено**: сервер (`api/raw_elm.py`) — `/response` принимает `device_id`. Тест обновляет seq после каждого ответа.
**Урок**: всегда проверять что тест читает данные того устройства которое тестируется.
---
## НЕУДАЧА 11: Версия на сайте не обновлялась
**Симптом**: сайт показывал v0.3.0-dev при v0.4.0-dev на сервере
**Когда**: все деплои
**Что сделано не так**: `sed` правил только `/opt/elmer/web/templates/index.html`. Flask использует `/opt/elmer/templates/index.html`.
**Корневая причина**: два index.html в разных директориях, неизвестно какой использует Flask.
**Исправлено**: деплой обновляет оба файла. Выяснено что Flask использует `/opt/elmer/templates/`.
**Урок**: проверять какой файл реально сервится перед правкой.
---
## НЕУДАЧА 12: v0.4.0-dev не долетел до телефона
**Симптом**: пользователь тестировал v0.3.7-dev думая что это v0.4.0-dev
**Когда**: 2026-07-04 вечер
**Что сделано не так**: деплой v0.4.0-dev прошёл, APK на сервере, но пользователь не переустановил приложение.
**Корневая причина**: отсутствие проверки версии на телефоне.
**Исправлено**: явное указание версии при каждом деплое. Проверка MD5 APK на сервере.
**Урок**: всегда проверять что пользователь обновил APK перед тестированием.
---
## ОГРАНИЧЕНИЯ КЛОНОВ v1.5 (железо)
| Параметр | Значение |
|----------|----------|
| Надёжность (холодный) | 100% первые 10-12 команд |
| Надёжность (горячий) | 70-80%, деградация после 10-12 команд |
| Буферный сдвиг после init | 1-2 команды (неизбежно в синхронной модели) |
| Двигатель заглушен | PID не работают, ATRV — ок |
| Двигатель заведён | 70-80% успех |
| ATST96 | Убивает клон намертво |
| ATAT1 | Клон игнорирует, не мешает |
| Время восстановления | 2-3 секунды паузы |
---
## ТЕКУЩАЯ АРХИТЕКТУРА (v0.4.0-dev)
```
Android phone Server (obdai.ru)
┌──────────────────┐ ┌─────────────────────┐
│ RawRelayService │──HTTP────→│ /api/v1/elm/raw/cmd │
│ relayLoop() │←──poll───│ /api/v1/elm/raw/ │
│ ↓ │ │ response │
│ ElmActor │ └─────────────────────┘
│ ↓ │
│ ElmProtocol │──BT────→ ELM327 → OBD-II → ECU
│ (AndrOBD) │←──BT───
└──────────────────┘
```
---
## КЛЮЧЕВЫЕ ФАЙЛЫ
| Файл | Версия | Описание |
|------|--------|----------|
| `raw/.../ElmProtocol.kt` | v0.4.0-dev | 1:1 с app/ElmProtocol.kt |
| `raw/.../ElmActor.kt` | v0.4.0-dev | Single-thread executor |
| `raw/.../RawRelayService.kt` | v0.4.0-dev | Поллинг + обработка ответов |
| `api/raw_elm.py` | исправлен | /response с device_id |
| `app/.../ElmProtocol.kt` | исходный | Протокол основного приложения |
@@ -0,0 +1,34 @@
# Итоги тестирования протокола ELM327 v1.5 — 28.06.2026
## Результаты
| Версия | Условия | Результат |
|--------|---------|-----------|
| v0.2.3 | Зажиг ON, двиг OFF, старый код | 12/12 (100%) один раз, потом нестабильно |
| v0.2.4 | Двиг OFF, с drain (сломан) | 1/20 |
| v0.2.4 | Двиг OFF, без drain | 9/12 (после чистки очереди) |
| v0.2.6 | Двиг ON (заведён), без info-команд | 8/12 (первые 4 — хвост инита, потом 8/8) |
| v0.2.6 | Двиг ON, после сброса ELM | 16/16 (100%) — но ELM залип на одном ответе |
## Что работает
- Канонический протокол (ATE0→ATL0→ATS0→ATI→detectClone→ATST96→ATSP0) — стабилен
- ElmActor (single-thread executor) — без нареканий
- SQLite command_queue с gunicorn -w 4 — работает
- Поллинг Android↔сервер — стабилен
- hello чистит очередь для device_id — новые сессии без мусора
- VERSION_NAME через val appVersionName — больше не хардкод
## Проблемы
1. **Клон v1.5 залипает** — после ~10 команд начинает повторять один ответ (`410405\n7F0112`).
Нужен сброс питания (вынуть из OBD) для восстановления.
2. **ATI/ATDPN/ATRV засоряют буфер** — убраны из relayLoop, но init() всё ещё делает ATI.
3. **Без заведённого двигателя ~50% ответов пустые** — ECU медленнее отвечает.
## Что дальше
1. Тест с паузами 2-3 сек между циклами — проверить, уходит ли залипание
2. Тест с меньшим числом PID в цикле (2-3 вместо 4)
3. После стабилизации — перенос фиксов в основное приложение (:app)
4. Смержить opus-fixes в master
@@ -0,0 +1,48 @@
# Результаты тестирования протокола ELM327 v1.5 — 28.06.2026
## Конфигурация
- **ELM327:** клон v1.5, протокол A0 (CAN), 12.2V
- **Приложение:** ELM Relay v2 (v0.2.2-dev, пакет ru.elmer.raw)
- **Протокол:** канонический init (ATE0→ATL0→ATS0→ATI→detectClone→ATST96→ATSP0)
- **ECU:** зажигание ON, двигатель OFF
## Результаты
### Статика (6 PID по одному, пауза 1с)
- 2/6 успешно (33%)
- Сбои: хвост "12.3V" от ATRV после инита, STOPPED, таймауты
- **Причина:** после init() и hello-команд (ATI, ATDPN, ATRV) в буфере остаются хвосты
### Динамика (3 цикла × 4 PID, пауза 300мс)
- **12/12 = 100% успешно** ✅
- RPM: 286-506ms, стабильно
- Дроссель: 294-402ms, стабильно
- ОЖ: 326-506ms, стабильно
- Нагрузка: 294-340ms, стабильно
### Сравнение с предыдущими тестами (14 июня)
| Дата | Протокол | Динамика |
|------|----------|----------|
| 14.06 (до фиксов) | AndrOBD init, нет drain, ATAT1 | 0/8 (0%) |
| 14.06 (с drain) | drain_first=true | 8/8 (100%)* |
| 14.06 (без drain) | — | 0/8 (0%) |
| 28.06 (канон) | новый init, ElmActor, SQLite | **12/12 (100%)** |
*нестабильно — в повторных тестах 3/12
## Выводы
1. **Канонический протокол работает.** Динамика 100% без drain_first — drain встроен в sendCommand.
2. **Проблема статики — хвост после инита.** Нужен drain после hello-команд в RawRelayService.
3. **ElmActor + @Volatile + SQLite-очередь — без нареканий.**
4. **Клон v1.5 стабилен на 6 PID с паузой 300мс.**
## Дальнейшие шаги
1. Исправить drain после hello в RawRelayService (ATI/ATDPN/ATRV → drain)
2. Длинный тест: 50+ циклов динамики
3. Тест с заведённым двигателем (RPM > 0)
4. При успехе — перенести фиксы в основное приложение (:app)
5. Смержить opus-fixes в master
@@ -0,0 +1,24 @@
# Тест AndrOBD протокола на клоне v1.5 — 04.07.2026
## Конфигурация
- **ELM:** клон v1.5, протокол A0 (CAN)
- **ECU:** зажигание ON, двигатель OFF
- **Приложение:** ELM Relay v2 (v0.3.1-dev)
- **Инит:** ATSP0→ATI→[без ATAT1/ATST]→ATS0→ATL0→ATE0 (AndrOBD порядок)
## Результат: 14/15 (93%)
| Цикл | PID | Ответ |
|------|-----|-------|
| 0 | 010C | ❌ (хвост инита) |
| 0 | 0105 | ✅ 410579 (ОЖ 79°C) |
| 0 | 0111 | ✅ 410579 |
| 1 | 010C | ✅ 410C0000 (RPM 0) |
| 1-4 | все | ✅ (12/12) |
## Выводы
1. **ATST96 убивает клон v1.5.** Без него — инит работает.
2. **AndrOBD порядок (ATSP0 первый) — правильный.** ATE0 первым не нужен.
3. **93% без залипания на 15 командах.** Раньше залипало на 4-й.
4. **Первая команда провалена** — хвост от ATI/ATSP0 в буфере. Требует drain перед первым PID.
@@ -0,0 +1,28 @@
# Вопросы к Opus 4.8 — по результатам тестов 28.06.2026
## Контекст
Протестирован канонический протокол на клоне ELM327 v1.5 (заведённый двигатель).
Результаты: протокол стабилен (16/16), НО клон залипает после ~10 команд —
начинает возвращать один и тот же ответ `410405\n7F0112` на ВСЕ команды,
включая AT (ATRV возвращает тот же OBD-ответ вместо напряжения).
Единственный способ восстановления — физическое извлечение из OBD.
## Вопросы
1. **Природа залипания.** Это известный баг клонов v1.5 или проблема конкретного экземпляра? Что именно залипает: внутренний буфер ELM, CAN-контроллер, или сам чип перестаёт принимать новые команды?
2. **Детект залипания.** Как программно определить что ELM залип? Проверять что ответ не меняется 3+ команды подряд? Сравнивать ATRV до и после цикла?
3. **Паузы между циклами.** Какая минимальная пауза предотвращает залипание? Достаточно ли 2-3 секунд между циклами по 4 PID? Зависит ли от числа PID в цикле?
4. **Auto-recovery без извлечения.** Есть ли программный способ вывести клон из залипания? ATPC? ATWS? ATZ? Или только обесточивание?
5. **Оптимальный размер цикла.** При паузе 2-3 сек между циклами — сколько PID оптимально в одном цикле? 2? 4? 6?
6. **Влияние оборотов двигателя.** Залипание зависит от нагрузки на CAN-шину? При высоких оборотах (3500+) залипает быстрее?
7. **Замена адаптера.** Если клон v1.5 принципиально нестабилен — какой адаптер рекомендовать? Оригинальный ELM327? v2.1 клон? OBDLink?
8. **Стратегия для продакшена.** Учитывая что 50%+ пользователей будут с клонами — какую стратегию выбрать: адаптироваться под клонов (медленный сбор) или требовать оригинал?
@@ -0,0 +1,67 @@
# Opus — переанализ фаз 06 (28.06.2026)
Запрос: doc/opus-recheck-request-2026-06-28.md
---
## Вердикт
**Можно запускать на реальном ELM327 v1.5** для стендовых тестов с одним телефоном.
Логика протокола корректна. Один баг очереди надо закрыть до перехода на `gunicorn -w 4`.
---
## 1. ElmActor.kt — корректен, deadlock нет ✅
`init()` сабмитит `elm.init()` один раз и ждёт `.get(30s)`. Внутри `elm.init()` вызывает `elm.sendCommand("ATI")` прямым вызовом (не через `executor.submit`), то есть на том же потоке actor'а — рекурсивной постановки в очередь нет → deadlock невозможен. `isClone()` читает `@Volatile` поле напрямую — видимость корректна.
**Замечание (low):** при таймауте `.get(30, SECONDS)` Future не отменяется — задача `elm.init()` продолжает крутиться на executor'е, и следующий `send()` встанет за ней в очередь. Худший случай init по сумме `tryRead` ≈ 23с (ATSP0 один даёт до 10с) — близко к лимиту 30с. Рекомендация: `future.cancel(true)` в `catch(TimeoutException)` либо поднять лимит до 45с.
---
## 2. ElmProtocol.kt (raw) — порядок init верный, но есть двойной ATI ⚠️
Канонический порядок правильный: `ATE0→ATL0→ATS0→ATI→detectClone→[ATST96|ATAT1]→ATSP0`. Эхо выключается первым. Обработка двойного ответа ATWS корректна во всех ветках recovery/STOPPED/BUS ERROR.
Проблемы:
- **Двойной ATI (medium):** первый `write("ATI"); tryRead; drainInput()` бесполезен. Затем ещё раз `sendCommand("ATI")`. Python-версия сделана правильно (один `_exec("ATI")`).
- **MAX_RETRIES=6 даёт потолок 600мс, не 2000 (medium):** 6 шагов по +20 = 600мс. Если медленный ЭБУ требует >600мс — всё равно ошибка. Комментарий вводит в заблуждение.
- **exec() не перешлёт команду на ретрае:** пишет `cmd` один раз, при таймауте только перечитывает буфер. Для ELM штатно, но при реальном `NO DATA` повторное чтение не поможет.
---
## 3. raw_elm.py — SQLite-очередь: dequeue НЕ атомарен ❌ (главное)
`GET /api/v1/elm/raw/cmd` делает два отдельных стейтмента без guard'а. При `-w 4` два воркера могут выбрать одну строку → команда уйдёт на ELM дважды.
**Фикс:** `UPDATE ... SET status='sent' WHERE id=? AND status='pending'` + проверка `rowcount == 0`.
Прочие замечания:
- **Рост таблицы:** `command_queue` не чистится (старый код держал 500). Добавить `DELETE WHERE responded_at < datetime('now','-1 day')`.
- **`device_id="unknown"`:** несколько телефонов сольются в одну очередь.
- **`GET /response`:** при всплеске может вернуть ответ не на запрошенную команду (для пошагового relay'я ок).
---
## 4. Python protocol.py — паритет с Kotlin ✅
Тот же порядок, один ATI, ATST96 для клона, `try_read(500)` как drain после каждой AT-команды.
---
## 5. Безопасность (фаза 0) ✅
`api_key → env LLM_API_KEY`. Проверить что старый ключ отозван у провайдера.
---
## Итоговый список к исправлению (по приоритету)
1.**Атомарный dequeue** — guard `AND status='pending'` + `rowcount`
2. ⚠️ **Двойной ATI** в `ElmProtocol.kt init()` — убрать первый `write("ATI")`
3. ⚠️ **MAX_RETRIES/комментарий** — потолок 600мс, не 2000
4. **Ретеншн** `command_queue` + фильтрация `device_id`
5. **Отмена Future** при таймауте init в `ElmActor`
6. **Отозвать старый LLM-ключ** у провайдера
Пункты 2–5 не блокируют стендовый прогон. Dequeue-гонку держать в голове при `-w 4`.
@@ -0,0 +1,31 @@
# Opus — переанализ после фаз 0–6
Дата: 2026-06-28
Предыдущий анализ: doc/history/opus-plan-analysis-2026-06-28.md
## Что изменилось
Твой план из 6 фаз реализован в ветке `opus-fixes`. Изменённые файлы:
### Android (:raw — тестовый модуль, пакет ru.elmer.raw)
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/ElmProtocol.kt` — канонический init (ATE0→ATL0→ATS0→ATI→detectClone→ветвл→ATSP0), ATST96 для клонов, MAX_RETRIES=6, @Volatile state, drain после sendCommand, handle STOPPED/NO DATA/UNABLE
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/ElmActor.kt` — single-thread executor вокруг ElmProtocol (новый файл)
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/RawRelayService.kt` — переведён на ElmActor
### Сервер (Python)
- `/home/naeel/elmer/obd/protocol.py` — канонический init (тот же порядок, clone detect, drain после send)
- `/home/naeel/elmer/api/raw_elm.py` — полная переработка: SQLite command_queue вместо глобальных переменных (gunicorn-safe, -w 4)
- `/home/naeel/elmer/api/db.py` — таблица command_queue
- `/home/naeel/elmer/api/scripts.py` — build_dynamic_script: 6 PID, interval_ms 1200
- `/home/naeel/elmer/config.yaml` — api_key → env LLM_API_KEY (безопасность)
### НЕ тронуто
- `:app` модуль (ru.elmer.client) — весь старый код без изменений
## Что нужно
1. **Валидация** — нет ли новых ошибок, противоречий, race conditions в новом коде
2. **ElmActor.kt** — корректна ли реализация single-thread executor? Нет ли deadlock при init()?
3. **ElmProtocol.kt (raw)** — правильный ли порядок init? Корректна ли обработка двойного ответа ATWS?
4. **raw_elm.py** — корректна ли SQLite-очередь? Атомарен ли dequeue?
5. **Общая оценка** — можно ли с этим кодом запускать тесты на реальном ELM327 v1.5?
@@ -0,0 +1,79 @@
# Ответы Opus и Sonnet — залипание клона ELM327 v1.5
Дата: 2026-06-29
---
## Opus 4.8
### 1. Природа залипания
Прошивка-эмулятор (MCU имитирует команды, нет настоящего чипа). Под нагрузкой буфер ответа переполняется, MCU зацикливается, отдавая последний кадр (`7F0112`) на всё. CAN-контроллер жив, мёртв эмулятор. UART отвечает, но игнорирует ввод.
### 2. Детект залипания
AT-команда возвращает OBD-кадр = залип. `ATRV` без `V`/напряжения = залип. 3+ одинаковых hex-ответа на разные PID. `7F` на PID 0100 — флаг.
### 3. Паузы
2-3 сек снижают вероятность, но не устраняют. Залипание зависит от **суммарного числа команд**, а не интервалов.
### 4. Auto-recovery
`ATWS`/`ATZ` иногда оживляют, но если завис UART-приём — команды не дойдут. **90% случаев — только обесточивание.** Алгоритм: ATWS → 2 сек → ATRV для проверки. Не сработало → требовать переподключения.
### 5. Оптимальный размер цикла
**2-3 PID** на цикл при паузе 2-3 сек. 4 уже на грани.
### 6. Влияние оборотов
CAN-нагрузка ускоряет залипание. 3500+ → больше трафика → раньше переполнение.
### 7. Замена адаптера
OBDLink SX/MX+ — лучший. v2.1 клон — миф, тот же эмулятор.
### 8. Стратегия
Гибрид: адаптироваться (2-3 PID, паузы, детект+recovery), детектить тип, предупреждать, не блокировать.
---
## Sonnet
### 1. Природа залипания
Баг клонов v1.5 (CH340/PIC18F). Залипает CAN-контроллер внутри клона.
### 2. Детект залипания
`ATRV` до/после цикла. 2-3 одинаковых ответа подряд.
### 3. Паузы
2-3 сек помогают. Минимум 1.5 сек. 6+ PID → 3-5 сек.
### 4. Auto-recovery
`ATZ` — надёжный. `ATWS` — мягче. `ATPC` — иногда. Последовательность: ATPC → 500ms → ATZ → переинит.
### 5. Оптимальный размер цикла
**4 PID** — хороший баланс. 6 рискованно. 2 безопасно.
### 6. Влияние оборотов
Да, прямое. Холостые — залипает позже.
### 7. Замена адаптера
OBDLink EX/MX+ → оригинал ELM327 v1.4b/v2.1 → клоны v2.1 лотерея.
### 8. Стратегия
Медленный режим (4 PID, 3 сек) как default. Детект + авто-ATZ. Предупреждение в UI.
---
## Моё мнение
**Opus точнее** в трёх ключевых пунктах:
1. **Природа залипания** — Opus правильно указывает на эмулятор/прошивку, а не CAN-контроллер. Это объясняет почему AT-команды тоже ломаются.
2. **Auto-recovery** — Opus реалистичнее: 90% только обесточивание. Соннет излишне оптимистичен про ATZ. Наш опыт подтверждает Опуса — ATZ не помогал.
3. **Размер цикла** — Opus консервативнее (2-3 PID). С учётом что клон залип после ~10 команд, 2-3 PID на цикл безопаснее.
**Соннет практичнее** в одном: 4 PID + 3 сек как default для продакшена. Но с учётом рисков, 2-3 PID + детект залипания + авто-предупреждение — более надёжный путь.
**Что берём в работу:**
- Детект залипания через `ATRV` (без `V` = залип)
- Циклы по 3 PID с паузой 2-3 сек
- Auto-recovery через ATWS → проверка ATRV → если нет → сообщение пользователю
- Предупреждение в UI: «Адаптер нестабилен, рекомендуем OBDLink»
+92
View File
@@ -0,0 +1,92 @@
# ELM327 Relay — хронология ошибок (АРХИВ)
> **Устарело**. Актуальный документ: `doc/failures-journal.md`
## Дата: 2026-07-04 | Версия на момент написания: v0.3.7-dev
---
## Текущая версия
**v0.3.7-dev** (commit `3e4eba7`, ветка `opus-fixes`)
---
## Что работает (v0.3.5-dev, тест 13/15)
- AndrOBD-совместимый init: `ATSP0 → ATI → ATS0 → ATL0 → ATE0`
- Клон v1.5: ATAT1 и ATST пропускаются
- `handle()` **без AT-команд** — только state tracking (как AndrOBD)
- `recover()` — только сброс state в READY
- 87% успешных PID-команд после прогрева
## Что НЕ работает
- **Буферный сдвиг после init**: первые 1-2 команды возвращают мусор/пустоту
- **STOPPED от ELM**: протокол останавливается, нужен перезапуск
- **Двигатель заглушен**: PID не работают, только ATRV
---
## ВСЕ ошибки (хронология)
### Ошибка 1: Freeze detection через ATRV (v0.3.0-dev)
**Что сделано**: каждые 10 команд слали ATRV для проверки залипания клона.
**Почему ошибка**: ATRV ломает синхронизацию команд. Ответ "12.6V" попадает в буфер и читается как ответ на следующий PID.
**Исправлено**: удалено в v0.3.2-dev.
### Ошибка 2: AT-команды в handle() (v0.1.xv0.3.3-dev)
**Что сделано**: handle() при STOPPED/UNABLE/BUS_ERROR слал ATPC→ATWS→ATSP0.
**Почему ошибка**: AT-команды отправляются внутри обработки ответа текущей команды. Их ответы загрязняют буфер для следующей команды.
**Исправлено**: удалено в v0.3.5-dev. handle() теперь только меняет state.
### Ошибка 3: AT-команды в recover() (v0.1.xv0.3.3-dev)
**Что сделано**: recover() слал ATPC→ATWS→ATSP0→ATE0 при state=ERROR/DISCONNECTED.
**Почему ошибка**: recover() вызывается из sendCommand() перед отправкой команды. AT-ответы могут не успеть полностью прийти до отправки PID.
**Исправлено**: упрощён в v0.3.3-dev. Только сброс state и timeout.
### Ошибка 4: Дренаж 0100 после init (v0.3.6-dev)
**Что сделано**: попытка поглотить буферный сдвиг отправкой 0100 сразу после init.
**Почему ошибка**: дренаж сам вызывает сдвиг буфера и дезориентирует клон.
**Исправлено**: удалено в v0.3.7-dev.
### Ошибка 5: Recovery ATSP0 в relayLoop (v0.3.5-devv0.3.7-dev)
**Что сделано**: после STOPPED или пустого ответа — ATSP0 для перезапуска протокола.
**Почему ошибка**: ATSP0 через sendBlocking блокирует single-thread executor. Последующие команды ждут в очереди, тест видит таймауты.
**Текущий статус**: всё ещё в коде v0.3.7-dev.
### Ошибка 6: Тест без device_id в /response
**Что сделано**: тестовый скрипт вызывал `/response?wait=3` без device_id.
**Почему ошибка**: сервер возвращал ответы от чужих устройств/сессий.
**Исправлено**: сервер (raw_elm.py) обновлён — `/response` принимает `device_id` параметр. Тест обновлён.
### Ошибка 7: Тест без обновления seq
**Что сделано**: тест всегда слал `seq=0` в `/response`.
**Почему ошибка**: сервер возвращал один и тот же ответ многократно.
**Исправлено**: тест обновляет seq после каждого ответа.
### Ошибка 8: Версия на сайте не обновлялась
**Что сделано**: деплой менял HTML только в `/opt/elmer/web/templates/`.
**Почему ошибка**: Flask использует `/opt/elmer/templates/index.html`.
**Исправлено**: деплой теперь обновляет оба файла.
---
## Отличия от AndrOBD
| | AndrOBD | Наш код |
|---|---|---|
| Архитектура | Асинхронный (поток читает → handleTelegram) | Синхронный (sendCommand → ждать ответ) |
| write() | Не дренирует перед отправкой | drainInput() перед каждым write() |
| Init | ATSP0→ATAT1→ATS0→ATL0→ATE0 | ATSP0→ATI→ATS0→ATL0→ATE0 |
| ATI | Не используется | Для детекта клона v1.5 |
| handle() | Только state tracking | Только state tracking ✅ |
| Ошибки | Не шлёт AT-команд из handle() | Не шлёт AT-команд ✅ |
---
## Ключевые файлы
| Файл | Состояние |
|------|-----------|
| `ElmProtocol.kt` | handle() без AT-команд, recover() пустой |
| `RawRelayService.kt` | relayLoop с recovery ATSP0 |
| `ElmActor.kt` | Single-thread executor |
| `api/raw_elm.py` | /response с device_id фильтром |
@@ -0,0 +1,28 @@
# Вопросы для Соннета — почему телефон получает старый APK
Дата: 2026-06-28
## Ситуация
Сервер obdai.ru (Ubuntu 24, nginx, 5.172.178.213).
APK собран через `./gradlew :raw:clean :raw:assembleDebug`, 36/36 tasks fresh.
Проверено:
- `aapt dump badging` → versionCode=6, versionName=0.2.2-dev ✅
- `curl -sI https://obdai.ru/static/elm-raw-v022.apk` → 200 OK, 3726961 bytes ✅
- Файл переименован: elm-raw-v022.apk (абсолютно новое имя, не могло быть в кеше)
- Приложение переименовано: app_name = "ELM Relay v2" (strings.xml)
- Страница обновлена: ссылка на /static/elm-raw-v022.apk, текст "Скачать ELM Relay v2"
- nginx reload делался, gunicorn рестартовал
НО: пользователь с Android-телефона скачивает и получает **v0.1.1-dev**.
Приложение на рабочем столе называется "ELM Relay v2", но внутри показывает v0.1.1-dev.
## Вопросы
1. Может ли Android (Google Play Protect / Verify Apps) подменять APK при установке на старую версию из кеша?
2. Может ли nginx кешировать файлы, несмотря на `alias`? Нужен ли `expires -1` или `add_header Cache-Control "no-cache"`?
3. Может ли `curl` и браузер телефона видеть РАЗНЫЕ файлы из-за Cloudflare/CDN/прокси провайдера? Как проверить?
4. Может ли `wget` или `curl` с сервера через внешний IP показать, что отдаётся НЕ тот файл?
5. Может ли Android Download Manager кешировать по имени пакета (ru.elmer.raw), а не по URL?
6. Какие ещё механизмы кеширования APK существуют на стороне Android, которые мы не учли?
7. Как ПРОВЕРИТЬ на сервере, что по HTTP реально отдаётся правильный файл, а не старый — поMD5 или размеру?
@@ -0,0 +1,30 @@
# Ответ Соннета — по результатам тестов 28.06.2026
(Отправлено по ошибке вместо Опуса)
## 1. Природа залипания
Известный баг клонов (особенно v1.5 на CH340/PIC18F). Залипает обычно CAN-контроллер внутри клона — накапливает необработанные фреймы и перестаёт принимать команды через UART. Настоящий ELM327 сбрасывает буфер сам; клоны — нет.
## 2. Детект залипания
Лучший вариант — сравнивать `ATRV` до и после цикла (у живого ELM всегда выдаёт реальное напряжение ~12.x V). Дополнительно: если два последних ответа на разные PID идентичны побайтово — залип. Трёх совпадений достаточно.
## 3. Паузы
2–3 секунды между циклами по 4 PID обычно помогают. Минимум ~1.5 сек. При 6+ PID в цикле лучше 3–5 сек. Чем больше PID, тем больше CAN-трафика, тем быстрее переполняется буфер.
## 4. Auto-recovery
`ATZ` (полный сброс, ~1 сек) — самый надёжный программный способ. `ATWS` (warm start) — мягче, но не всегда помогает клонам. `ATPC` (protocol close) — иногда достаточно. Последовательность: `ATPC` → 500 мс → если не помогло → `ATZ` → переинициализация.
## 5. Оптимальный размер цикла
При паузе 23 сек: **4 PID** — хороший баланс. 6 PID уже рискованно на нагруженной шине. 2 PID — безопасно, но медленно.
## 6. Влияние оборотов
Да, напрямую. При высоких оборотах CAN-шина загружена больше (ECU шлёт больше фреймов), буфер клона заполняется быстрее. На холостых — залипает позже.
## 7. Замена адаптера
Приоритет: **OBDLink EX/MX+** (оригинальный чип STN2120, нет этой проблемы) → оригинальный ELM327 v1.4b/v2.1 (реже). Клоны v2.1 — лотерея, та же болезнь. Китайские v1.5 — для продакшена непригодны.
## 8. Стратегия для продакшена
Адаптироваться под клонов, но с явными ограничениями:
- Медленный режим (4 PID, пауза 3 сек) как default
- Детект залипания + авто-`ATZ`
- В UI показывать «адаптер нестабилен, рекомендуем OBDLink/оригинал»
View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+180
View File
@@ -0,0 +1,180 @@
# Резюме elmAI — для нового чата
> Создано: 2026-07-10 | Предыдущий чат: relay-отладка
## Проект
elmAI — OBD2-диагностика через ELM327 + LLM (DeepSeek).
Два репо: `elmer/` (gitea, сервер) и `elmer/android/` (github, Android).
## Версии
| Компонент | Версия |
|-----------|--------|
| Основной APK (app) | v0.42.0-dev |
| Raw-реле APK (raw) | v0.4.0-dev |
## Два режима
1. **app/** — прямая диагностика: телефон→ELM→скрипт→батч→сервер→LLM
2. **raw/** — ретранслятор: Copilot→сервер→телефон→ELM→ответ. HTTP-поллинг.
## Raw-реле (raw/)
```
Copilot → POST /cmd → очередь SQLite → GET /cmd (телефон) → sendCommand() → ELM
Copilot ← GET /response?device_id=X&seq=N ← POST /response (телефон) ← ответ
```
**Ключевые файлы:**
- `ElmProtocol.kt` — 1:1 с `app/.../ElmProtocol.kt` (AndrOBD: ATSP0→ATAT1→ATST→ATS0→ATL0→ATE0)
- `ElmActor.kt` — single-thread executor
- `RawRelayService.kt` — foreground-сервис: BT→init→поллинг
- `RelayClient.kt` — HTTP к `/api/v1/elm/raw/*`
**handle()**: BUS_ERROR→ATPC+ATSP0, ERROR→ATWS, STOPPED→только state. MAX_RETRIES=3.
## Результаты тестов (клоны v1.5)
- Клон #1: 12/12 (100% первые 12), потом 13/16 (81%)
- Клон #2: 13/18 (72%)
- Закономерность: деградация после 10-12 команд
- Двигатель заглушен: PID не работают (STOPPED)
- RPM: 26882807, темп: 8488°C, дроссель: 0–1.5% — данные верны
## Сервер (obdai.ru, 5.172.178.213)
- nginx:443 → gunicorn:8000 (4 воркера)
- Flask: `web/app.py`, blueprints: routes, dtc, ping, raw_elm
- SQLite: `command_queue` (raw), `sessions` (app)
- `/api/v1/elm/raw/*`: hello, cmd×2, response×2, status
- **device_id обязателен** в `/response`
- **Flask использует** `/opt/elmer/templates/index.html` (не web/templates)
## 12 главных провалов (подробно: `doc/failures-journal.md`)
1. Не скопировал рабочий `app/ElmProtocol.kt` — писал с нуля
2. Доверял Opus вместо проверки AndrOBD
3. ATST96 убивает клон
4. ATI в init ломает порядок
5. drainInput() в write() — буферный сдвиг
6. Freeze detection через ATRV
7. AT-команды в handle()
8. Recovery ATSP0 в relayLoop
9. Дренаж 0100 после init
10. Тест без device_id
11. Версия сайта не обновлялась
12. APK не долетел до телефона
## Правила (`.github/copilot-instructions.md`)
1. НИЧЕГО не делать без прямого указания
2. На вопросы — только отвечать
3. Коммит + push после каждой правки
4. При деплое — bump версии в build.gradle.kts
5. Формат: fix/feat/refactor/bump/docs
6. ELM327 — только как AndrOBD, без самодеятельности
7. Рассуждение ≠ команда
## Деплой raw APK
```bash
cd android
sed -i 's/appVersionCode = XX/appVersionCode = YY/' raw/build.gradle.kts
sed -i 's/X.Y.Z-dev/X.Y+1.Z-dev/' raw/build.gradle.kts
git add -A && git commit -m "bump vX.Y+1.Z-dev" && git push origin opus-fixes
scp raw/build.gradle.kts raw/src/.../*.kt obdai.ru:/opt/elmer/android/raw/
ssh obdai.ru './gradlew :raw:assembleDebug && cp ...apk /opt/elmer/web/static/elm-raw-v022.apk'
# Обновить версию в /opt/elmer/templates/index.html и /opt/elmer/web/templates/index.html
```
## Ключевые документы
- `doc/failures-journal.md` — 12 провалов
- `doc/architecture.md` — полная архитектура
- `doc/diagnostic-logic.md` — режимы диагностики
- `STRUCTURE.md` — все файлы и папки
### Репозитории
- Сервер: https://gitea.services.ngcloud.ru/Nail/elmer (ветка **dynamic-tests**)
- Android: https://github.com/Repinoid/elmer-android (ветка **dynamic-tests**)
- Сервер живёт на 5.172.178.213 (SSH: naeel@5.172.178.213, ключ ~/.ssh/naeel_vm_id_ed25519)
### Деплой (localhost → сервер)
```bash
# 1. bump версии в android/app/build.gradle.kts на локальной машине!
# 2. закоммитить + запушить (dynamic-tests!)
cd /home/naeel/elmer && git add -A && git commit -m "..." && git push origin dynamic-tests
# 3. залить android-исходники на сервер
# 3. залить android-исходники на сервер
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/
# 4. на сервере: обновить сервер + собрать APK
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 "
cd /opt/elmer && git checkout dynamic-tests && git pull origin dynamic-tests
pip install -r requirements.txt
sudo systemctl restart elmer
rm -rf android && tar xzf /tmp/android-src.tar.gz
cd android && gradle wrapper --gradle-version 8.7
export ANDROID_SDK_ROOT=\$HOME/android-sdk
./gradlew clean assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/
"
```
### Структура проекта
```
elmer/
android/ — Android-приложение (Kotlin)
.../client/
MainActivity.kt — UI + приёмники
ElmForwardService.kt — BT/TCP-реле
TestService.kt — автономный тест ELM
api/ — Flask API (Python)
db.py — БД (sessions + device_profiles)
routes.py — эндпоинты
scripts.py — скрипты L0/L1/L2
parser.py — парсинг ответов
config.py, dtc.py, ping.py
brain/ — LLM-клиент, промпты
obd/ — ELM-протокол (Python)
commands.py — каталог AT-команд
classifier.py — классификация ответов
connection.py — SerialTransport
probe.py — пробинг ELM
protocol.py — стейт-машина AndrOBD
state.py — состояния/ответы
timing.py — адаптивный таймаут
doc/ — Документация, сессии
web/ — Flask web, статика
```
### Что сделано (сессия 2026-06-07)
#### Сервер
- **Пробинг ELM**: трехуровневый каскад (L0/L1/L2), профили в БД по MAC
- **Fix init**: больше не шлём ATAT1/ATSTxx (вешало клоны)
- **Скрипты под уровень**: L0 (5 PIDs), L1 (8 + VIN), L2 (14 + калибровки)
- **Рефакторинг**: код разбит на независимые модули (commands, classifier, connection...)
#### Android
- **Вывод**: append вместо overwrite (строки не перекрываются)
- **Таймер**: отдельный TextView, тикает только во время обмена (Engine Time)
- **TestService**: адаптивные таймауты, ATS0, \r терминатор
### Что НЕ сделано (TODO)
- Полевой тест на машине ← СЕЙЧАС
- Разбить MainActivity.kt
- Разбить ElmChecker.kt
### Важные правила
- ELM327 v1.5 — фейк, НЕ слать ATAT1/AT@1/AT@2/ATST/ATCAF1/ATCFC1
- Все статусы через append("\n..."), не tvStatus.text =
- Таймер операций (opTimerStart/Stop), не сессии
- Версию поднимать ВЕЗДЕ: build.gradle.kts, index.html (2 места), CHANGELOG.md, resume.txt, doc/architecture.md
- В проекте elmer — НИЧЕГО не делать без прямого указания
-60
View File
@@ -1,60 +0,0 @@
# obdai.ru — AI-диагностика автомобиля через ELM327
Сервис анализа ошибок электроники автомобиля с помощью ИИ (DeepSeek).
## Как работает
```
ELM327 ←Bluetooth→ Телефон (тонкий клиент) ←HTTP→ Сервер ←API→ DeepSeek
вся логика здесь
```
## Железо пользователя
- ELM327 Bluetooth (клон PIC18F25K80 — дешёвый, массовый)
- Android-телефон (7+)
## Тонкий клиент (Android)
Только транспорт, никакой логики:
- Bluetooth SPP → ELM327
- Читает сырые OBD-ответы
- Отправляет HTTP POST на сервер
- Показывает ответ от сервера
Открытый исходный код на GitHub → доверие пользователей.
Публикация в RuStore.
## Сервер (вся логика)
1. Принимает сырые данные от клиента (`/api/v1/raw-obd`)
2. Парсит: VIN, коды ошибок, параметры ЭБУ
3. Формирует промпт → DeepSeek API
4. LLM отвечает → сервер анализирует → может запросить ещё данные
5. Итеративный цикл:
- диагноз
- или команда «считай ещё параметр X»
- или задание водителю («прогазуй до 3000 об/мин», «проедь 5 км», «дожми до кикдауна»)
6. Финальный ответ: диагноз + степень уверенности + пояснения + что делать
## Десктоп (для тестирования)
Ноутбук → Bluetooth → ELM327 → локальный Flask → DeepSeek.
Браузерный UI. Без Android.
## Домен
**obdai.ru** (обыгрывается: «обдай грязью» + OBD + AI)
## LLM
DeepSeek (через OpenAI-совместимый API). Дёшево, справляется с анализом логов.
## Компоненты
| Компонент | Где | Технологии |
|---|---|---|
| Сервер | obdai.ru | Python, Flask → gunicorn, PostgreSQL |
| Клиент Android | GitHub + RuStore | Kotlin, AndrOBD library (GPLv2), OkHttp |
| Десктоп | Локально | Python, Flask, pyserial, браузер |
-86
View File
@@ -1,86 +0,0 @@
#!/bin/bash
# Деплой Elmer на obdai.ru
set -e
echo "=== Установка пакетов ==="
apt update && apt install -y python3-pip python3-venv nginx certbot python3-certbot-nginx
echo "=== Клонирование репо ==="
cd /opt
git clone https://gitea.services.ngcloud.ru/Nail/elmer.git || (cd elmer && git pull)
cd elmer
git checkout fat-client
echo "=== Виртуальное окружение ==="
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install gunicorn
echo "=== Конфигурация ==="
cp config.yaml config.yaml.bak
cat > config.yaml << 'YAML'
elm327:
port: /dev/rfcomm0
baudrate: 38400
llm:
api_key: "sk-ucI5YvOticoOQ9Kuj5K9mQ"
model: "gpt-oss-120b"
base_url: "https://api.aillm.ru/v1"
pids:
"0105": ["coolant_temp", "°C"]
"010C": ["rpm", "об/мин"]
"010D": ["speed", "км/ч"]
YAML
echo "=== Systemd сервис ==="
cat > /etc/systemd/system/elmer.service << 'UNIT'
[Unit]
Description=Elmer Flask API
After=network.target
[Service]
User=naeel
WorkingDirectory=/opt/elmer
ExecStart=/opt/elmer/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 web.app:app
Restart=always
[Install]
WantedBy=multi-user.target
UNIT
echo "=== Nginx ==="
cat > /etc/nginx/sites-available/elmer << 'NGX'
server {
listen 80;
server_name obdai.ru www.obdai.ru ai.obdai.ru test.obdai.ru;
location /static/ {
alias /opt/elmer/web/static/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
NGX
ln -sf /etc/nginx/sites-available/elmer /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
echo "=== SSL ==="
certbot --nginx -d obdai.ru -d www.obdai.ru --non-interactive --agree-tos -m tazet@narod.ru || true
echo "=== Запуск ==="
systemctl daemon-reload
systemctl enable elmer
systemctl restart elmer
systemctl restart nginx
echo "=== ГОТОВО ==="
curl -s http://obdai.ru/api/v1/script | head -c 50
-78
View File
@@ -1,78 +0,0 @@
## 4. Контекст проекта (РЕЗЮМЕ для нового чата)
### Что это
elmAI — Android-приложение + Python-сервер для диагностики авто через ELM327.
### Текущая версия
**v0.48.0** (APK: https://obdai.ru/elmer.apk)
### Репозитории
- Сервер: https://gitea.services.ngcloud.ru/Nail/elmer (ветка master)
- Android: https://github.com/Repinoid/elmer-android (ветка relay-only)
- Сервер живёт на 5.172.178.213 (SSH: naeel@5.172.178.213, ключ ~/.ssh/naeel_vm_id_ed25519)
### Деплой
```bash
# ВСЕГДА сначала bump версии в android/app/build.gradle.kts!
cd /home/naeel/elmer/android && git add -A && git commit -m "..." && git push origin master
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 && rm -rf android && tar xzf /tmp/android-src.tar.gz && \
cd android && gradle wrapper --gradle-version 8.7 && \
export ANDROID_SDK_ROOT=\$HOME/android-sdk && \
./gradlew clean assembleDebug && \
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/"
```
### Структура проекта
```
elmer/
android/ — Android-приложение (Kotlin)
.../client/
MainActivity.kt — UI + приёмники
ElmForwardService.kt — BT/TCP-реле
TestService.kt — автономный тест ELM
api/ — Flask API (Python)
db.py — БД (sessions + device_profiles)
routes.py — эндпоинты
scripts.py — скрипты L0/L1/L2
parser.py — парсинг ответов
config.py, dtc.py, ping.py
brain/ — LLM-клиент, промпты
obd/ — ELM-протокол (Python)
commands.py — каталог AT-команд
classifier.py — классификация ответов
connection.py — SerialTransport
probe.py — пробинг ELM
protocol.py — стейт-машина AndrOBD
state.py — состояния/ответы
timing.py — адаптивный таймаут
doc/ — Документация, сессии
web/ — Flask web, статика
```
### Что сделано (сессия 2026-06-07)
#### Сервер
- **Пробинг ELM**: трехуровневый каскад (L0/L1/L2), профили в БД по MAC
- **Fix init**: больше не шлём ATAT1/ATSTxx (вешало клоны)
- **Скрипты под уровень**: L0 (5 PIDs), L1 (8 + VIN), L2 (14 + калибровки)
- **Рефакторинг**: код разбит на независимые модули (commands, classifier, connection...)
#### Android
- **Вывод**: append вместо overwrite (строки не перекрываются)
- **Таймер**: отдельный TextView, тикает только во время обмена (Engine Time)
- **TestService**: адаптивные таймауты, ATS0, \r терминатор
### Что НЕ сделано (TODO)
- Полевой тест на машине ← СЕЙЧАС
- Разбить MainActivity.kt
- Разбить ElmChecker.kt
### Важные правила
- ELM327 v1.5 — фейк, НЕ слать ATAT1/AT@1/AT@2/ATST/ATCAF1/ATCFC1
- Все статусы через append("\n..."), не tvStatus.text =
- Таймер операций (opTimerStart/Stop), не сессии
- Версию поднимать ВЕЗДЕ: build.gradle.kts, index.html (2 места), CHANGELOG.md, resume.txt, doc/architecture.md
- В проекте elmer — НИЧЕГО не делать без прямого указания
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

-173
View File
@@ -1,173 +0,0 @@
# Морда elmAI v2
> Новая архитектура UI. Обсуждено 7 июня 2026.
## Макет
```
┌──────────────────────────────────────────┐
│ 🔧 elmAI v?.?.? │
│ 📡 Сервер ● 🔌 ELM ● 🚗 ЭБУ ● 🧠 LLM ● │ иконки + светофоры
├──────────────────────────────────────────┤
│ [ ОШИБКИ / ДИАГНОСТИКА / СТАРТ / СТОП ] │ одна кнопка
├──────────────────────────────────────────┤
│ │
│ поле вывода результатов │
│ │
├──────────────────────────────────────────┤
│ [📋 История] │
├──────────────────────────────────────────┤
│ [____________________________] [➤] │ поле ввода + кнопка
└──────────────────────────────────────────┘
```
## Иконки и светофоры
Четыре иконки в строке под версией. Справа от каждой — цветной индикатор.
| Иконка | Текст | 🟢 Зелёный | 🟡 Жёлтый | 🔴 Красный |
|--------|-------|-----------|----------|-----------|
| 📡 | Сервер | ping < 3с | проверка... | нет связи |
| 🔌 | ELM | BT + ATI ok | подключение... | нет ELM |
| 🚗 | ЭБУ | 0100 ответил | — | нет связи с ЭБУ |
| 🧠 | LLM | ping-llm ok | проверка... | нет доступа |
- Жёлтый — только на время проверки
- LLM проверяется только если Сервер зелёный
- ЭБУ проверяется только если ELM зелёный
- **Тап по иконке** — перепроверка. Сбрасывает цвет на жёлтый, запускает проверку заново.
### Таймауты
- **ELM**: 2 попытки BT-подключения по ~4с = макс 8с → 🔴
- **Сервер**: HTTP GET `/api/v1/ping`, таймаут 3с → 🔴
- **LLM**: HTTP GET `/api/v1/ping-llm`, таймаут 5с → 🔴 (только если Сервер 🟢)
- При тапе на иконку проверка запускается немедленно, без задержек.
### Стартовый сценарий
1. Открыли приложение → все индикаторы 🟡 или 🔴
2. ELM 🟡 — пробуем подключиться (8с макс) → 🟢 или 🔴
3. Сервер 🟡 — ping (3с) → 🟢 или 🔴
4. Если Сервер 🟢 → LLM 🟡 — ping-llm (5с) → 🟢 или 🔴
5. Если ELM 🟢 → ЭБУ — пробуем `0100` → 🟢 или 🔴
После первого цикла индикаторы не обновляются автоматически — только по тапу.
## Кнопка-трансформер
Одна широкая кнопка. Меняет текст, цвет и действие в зависимости от этапа.
```
🔴 [⚠️ ОШИБКИ] — начальное состояние
↓ тап
сканирование DTC (03 + 07)
🔵 [🔍 ДИАГНОСТИКА] — ошибки считаны
↓ тап
ЕСЛИ сервер 🟢 → запрос параметров ЭБУ → отправка на сервер → LLM-анализ → вывод
ЕСЛИ сервер 🔴 → пояснение в выводе: «Сервер недоступен. Сделайте тест.»
🟢 [▶ СТАРТ] — готов к динамическому тесту
↓ тап
подсказка в выводе: «Газ до 3000, 3-4с, сброс. → СТОП»
запись 12 PID каждые 250мс
🔴 [⏹ СТОП] — запись идёт
↓ тап
запись остановлена, данные в памяти
вывод: «Записано N отсчётов. ➤ для отправки.»
🟢 [▶ СТАРТ] — можно повторить тест
```
- После СТОП можно снова нажать СТАРТ — новый тест, старые данные сохраняются.
- **Отправка данных** — не кнопкой, а через ➤ в поле ввода.
## Поле вывода
- ScrollView, моноширинный шрифт
- После done — кнопка «✕ Закрыть» (сворачивает вывод, возвращает все кнопки)
- После Share в истории — возврат в приложение (стандартное поведение Android)
## Поле ввода + кнопка ➤
2 строки, всегда активно. Плейсхолдер: «Что беспокоит? Чем подробнее — тем лучше».
Кнопка ➤ справа:
- **Обычный режим**: текст из поля → отправка на сервер (`/api/v1/chat`) → сервер спрашивает LLM → ответ в вывод
- **После СТОП**: накопленные данные теста → отправка на сервер (`/api/v1/session/upload`) + текст (если есть) → сервер анализирует через LLM → ответ в вывод
- Ответ → в поле вывода
## История
- Кнопка «📋 История»
- Список последних 20 сессий (дата, заголовок, статус загрузки)
- Выбор → диагноз во всплывающем окне
- Кнопка «📤 Поделиться» → системный Share Sheet (Telegram, WhatsApp, Gmail...)
- После Share — возврат в наше приложение
## Что удаляется
- Кнопки «📡 Сервер», «🔌 ELM», «🚗 ЭБУ» как отдельные — заменены на иконки со светофорами
- Кнопки «⏱ На месте», «🚗 В движении» — всё через одну кнопку-трансформер
- `tvDtcStatus` («Сначала считай ошибки») — не нужен
- `cbFullMode` (чекбокс полной диагностики) — всегда полная
- `tvPrompt` (оверлей) — не нужен
- `btnDynStart` — не нужен
## Серверные изменения
Ничего нового — всё уже есть:
- `/api/v1/ping`
- `/api/v1/ping-llm`
- `/api/v1/chat`
- `/api/v1/session/upload` с `dynamic_samples`
## Текущий макет (реализовано)
```
elmAI v0.68 📡🟢 ELM🟢 ECU🟢 LLM🟢 ← одна строка
[ ОШИБКИ / ДИАГНОСТИКА / СТАРТ / СТОП ]
вывод
[ 📋 История ] ⏱ 12с ← таймер справа
[____________________________] [➤] ← 3 строки ввода
```
## Логика кнопки (стейт-машина)
| State | Текст | Цвет | Действие |
|-------|-------|------|----------|
| INIT | ⚠️ ОШИБКИ | 🟠 | scanDtc() |
| DTC | 🔍 ДИАГНОСТИКА | 🔵 | runDiagnostics() |
| DIAG | ▶ СТАРТ | 🟢 | startDynamicRecording() |
| START | ⏹ СТОП | 🔴 | stopDynamicRecording() |
| STOP | ▶ СТАРТ | 🟢 | не используется |
- Кнопка неактивна (alpha=0.4) если ELM 🔴 или ECU 🔴
- Поле ввода неактивно если LLM 🔴
- После DTC-сканирования ECU → 🟢
- После СТОП → DIAG (можно снова СТАРТ)
## Исправлено (v0.57 → v0.68)
| v | Что |
|---|-----|
| 0.57 | Layout v2, иконки, трансформер |
| 0.58 | Всё в одной строке |
| 0.59 | setIndicator в runOnUiThread |
| 0.60 | LLM → 🤓 |
| 0.61 | Таймер в строке Истории, нет ELM → кнопка неактивна |
| 0.62 | Поле ввода неактивно без LLM |
| 0.63 | 🤓 → 🎓 |
| 0.64 | 🎓 → LLM текст |
| 0.65 | СТОП работает, ECU 🟢 после сканирования |
| 0.66 | ensureConnected всегда перед dynamic test |
| 0.67 | Кнопка неактивна без ECU |
| 0.68 | Двойной вызов checkLlm/checkEcu убран, таймер в catch |
## Известные проблемы (TODO)
- `findElmDevice()` возвращает null при >1 устройствах (диалог асинхронный)
- Android 12+ — startChecks до получения BT-разрешений
- `State.STOP` не используется
+26 -5
View File
@@ -56,6 +56,7 @@ class AndrOBD:
INIT_TMO = 10000 # мс — таймаут для команд инициализации
DEF_TMO = 200 # мс — начальный таймаут
ATST_CLONE = 0x96 # 150×4=600ms — фиксированный для клонов
def __init__(self, port: str, baudrate: int = 38400):
self._transport = SerialTransport(port, baudrate)
@@ -73,20 +74,39 @@ class AndrOBD:
self._transport.close()
def init(self):
"""Базовая инициализация ELM327 (уровень 0 — все клоны).
"""Канонический init: ATE0→ATL0→ATS0→ATI→[ветвление]→ATSP0.
ТОЛЬКО команды которые есть у ВСЕХ клонов:
ATE0 ATL0 ATS0 ATH1 ATSP0
Эхо ПЕРВЫМ. ATI ДО протокола. Клон: ATST фикс, без ATAT1.
"""
logger.info("AndrOBD: init (L0)")
self._state = State.INITIALIZING
# Шаг 1: clone-safe, без протокола
self._exec("ATE0", self.DEF_TMO * 5)
self._transport.try_read(500) # drain после AT-команды
self._exec("ATL0", self.DEF_TMO * 5)
self._transport.try_read(500)
self._exec("ATS0", self.DEF_TMO * 5)
self._exec("ATH1", self.DEF_TMO * 5)
self._transport.try_read(500)
# Шаг 2: ATI → detectClone ДО ветвления
ati = self._exec("ATI", self.DEF_TMO * 5)
self._transport.try_read(500)
is_clone = "v1.5" in ati.lower()
if is_clone:
logger.info("AndrOBD: clone v1.5 — ATST fixed, no ATAT1")
self._exec(f"ATST{self.ATST_CLONE:02X}", self.DEF_TMO * 5)
self._transport.try_read(500)
else:
self._exec("ATAT1", self.DEF_TMO * 5)
self._transport.try_read(500)
# Шаг 3: протокол
self._exec("ATSP0", self.INIT_TMO)
self._transport.try_read(500)
self._state = State.READY
logger.info("AndrOBD: ready (L0)")
logger.info(f"AndrOBD: ready (L0, clone={is_clone})")
def init_l1(self):
"""Инициализация уровня 1: база + адаптивный тайминг."""
@@ -111,6 +131,7 @@ class AndrOBD:
self._recover()
self._state = State.BUSY
result = self._exec(cmd, self._timing.ms)
self._transport.try_read(500) # drain после read — клон v1.5
if self._state == State.BUSY:
self._state = State.READY
return result
-92
View File
@@ -1,92 +0,0 @@
## 4. Контекст проекта (РЕЗЮМЕ для нового чата)
### Что это
elmAI — Android-приложение + Python-сервер для диагностики авто через ELM327.
### Текущая версия
<!-- !!! АКТУАЛЬНАЯ ВЕРСИЯ !!! -->
<!-- Перед деплоем bump в android/app/build.gradle.kts, index.html (2 места), CHANGELOG.md, resume.txt, doc/architecture.md -->
**v0.77.0-dev** (APK: https://obdai.ru/elmer.apk)
### Репозитории
- Сервер: https://gitea.services.ngcloud.ru/Nail/elmer (ветка **dynamic-tests**)
- Android: https://github.com/Repinoid/elmer-android (ветка **dynamic-tests**)
- Сервер живёт на 5.172.178.213 (SSH: naeel@5.172.178.213, ключ ~/.ssh/naeel_vm_id_ed25519)
### Деплой (localhost → сервер)
```bash
# 1. bump версии в android/app/build.gradle.kts на локальной машине!
# 2. закоммитить + запушить (dynamic-tests!)
cd /home/naeel/elmer && git add -A && git commit -m "..." && git push origin dynamic-tests
# 3. залить android-исходники на сервер
# 3. залить android-исходники на сервер
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/
# 4. на сервере: обновить сервер + собрать APK
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 "
cd /opt/elmer && git checkout dynamic-tests && git pull origin dynamic-tests
pip install -r requirements.txt
sudo systemctl restart elmer
rm -rf android && tar xzf /tmp/android-src.tar.gz
cd android && gradle wrapper --gradle-version 8.7
export ANDROID_SDK_ROOT=\$HOME/android-sdk
./gradlew clean assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/
"
```
### Структура проекта
```
elmer/
android/ — Android-приложение (Kotlin)
.../client/
MainActivity.kt — UI + приёмники
ElmForwardService.kt — BT/TCP-реле
TestService.kt — автономный тест ELM
api/ — Flask API (Python)
db.py — БД (sessions + device_profiles)
routes.py — эндпоинты
scripts.py — скрипты L0/L1/L2
parser.py — парсинг ответов
config.py, dtc.py, ping.py
brain/ — LLM-клиент, промпты
obd/ — ELM-протокол (Python)
commands.py — каталог AT-команд
classifier.py — классификация ответов
connection.py — SerialTransport
probe.py — пробинг ELM
protocol.py — стейт-машина AndrOBD
state.py — состояния/ответы
timing.py — адаптивный таймаут
doc/ — Документация, сессии
web/ — Flask web, статика
```
### Что сделано (сессия 2026-06-07)
#### Сервер
- **Пробинг ELM**: трехуровневый каскад (L0/L1/L2), профили в БД по MAC
- **Fix init**: больше не шлём ATAT1/ATSTxx (вешало клоны)
- **Скрипты под уровень**: L0 (5 PIDs), L1 (8 + VIN), L2 (14 + калибровки)
- **Рефакторинг**: код разбит на независимые модули (commands, classifier, connection...)
#### Android
- **Вывод**: append вместо overwrite (строки не перекрываются)
- **Таймер**: отдельный TextView, тикает только во время обмена (Engine Time)
- **TestService**: адаптивные таймауты, ATS0, \r терминатор
### Что НЕ сделано (TODO)
- Полевой тест на машине ← СЕЙЧАС
- Разбить MainActivity.kt
- Разбить ElmChecker.kt
### Важные правила
- ELM327 v1.5 — фейк, НЕ слать ATAT1/AT@1/AT@2/ATST/ATCAF1/ATCFC1
- Все статусы через append("\n..."), не tvStatus.text =
- Таймер операций (opTimerStart/Stop), не сессии
- Версию поднимать ВЕЗДЕ: build.gradle.kts, index.html (2 места), CHANGELOG.md, resume.txt, doc/architecture.md
- В проекте elmer — НИЧЕГО не делать без прямого указания
+4 -3
View File
@@ -25,10 +25,11 @@
<div class="card" style="text-align:center;margin-bottom:20px;background:#1a1a2e;">
<p style="margin:0 0 10px 0;">🔧 Отладка ELM327:</p>
<a href="/static/elm-raw.apk" style="color:#00ff88;font-size:18px;font-weight:bold;text-decoration:none;">
⬇️ Скачать ELM Raw Relay
<a href="/static/elm-raw-v022.apk"
style="color:#00ff88;font-size:18px;font-weight:bold;text-decoration:none;">
⬇️ Скачать ELM Relay v2
</a>
<p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v0.1.1-dev • ретранслятор команд</p>
<p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v0.3.0-dev • ретранслятор команд</p>
</div>
<!-- Кнопка десктоп-диагностики скрыта — только для разработчика с прямым ELM327 -->