Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ca56620c1 | ||
|
|
afc42381d0 | ||
|
|
1baaa8dffe | ||
|
|
bbbc06ec68 | ||
|
|
81bebcd566 | ||
|
|
95834a4245 | ||
|
|
3ea7fc66d5 | ||
|
|
ba48cd2daf | ||
|
|
15caff3204 | ||
|
|
129848d772 | ||
|
|
29ace6119e | ||
|
|
b9a33403fe | ||
|
|
f79a1436d5 | ||
|
|
daa7fcf4a6 | ||
|
|
caef3cc970 | ||
|
|
27883a90fa | ||
|
|
cd1c0a977e | ||
|
|
95ebc75484 | ||
|
|
5c1ba751c9 | ||
|
|
3a4701a835 | ||
|
|
8e1e88ee9a | ||
|
|
d3133731d8 | ||
|
|
06c0b9515e | ||
|
|
25c8eb20b9 | ||
|
|
c33bd8e2de | ||
|
|
d80cb26ce5 | ||
|
|
45a6640dae | ||
|
|
f283113ab1 | ||
|
|
d7bdd41df6 |
@@ -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 # Локальный запуск
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
@@ -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.*
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
@@ -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` — резервная копия.
|
||||
|
||||
@@ -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 получает: сырые данные + вопрос пользователя → ответ с диагнозом.
|
||||
@@ -0,0 +1,192 @@
|
||||
# ELM327 Relay — полный журнал неудач
|
||||
|
||||
## Дата: 2026-07-04–05 | Версия: 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.0–v0.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.x–v0.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 — переанализ фаз 0–6 (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»
|
||||
@@ -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.x–v0.3.3-dev)
|
||||
**Что сделано**: handle() при STOPPED/UNABLE/BUS_ERROR слал ATPC→ATWS→ATSP0.
|
||||
**Почему ошибка**: AT-команды отправляются внутри обработки ответа текущей команды. Их ответы загрязняют буфер для следующей команды.
|
||||
**Исправлено**: удалено в v0.3.5-dev. handle() теперь только меняет state.
|
||||
|
||||
### Ошибка 3: AT-команды в recover() (v0.1.x–v0.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-dev–v0.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. Оптимальный размер цикла
|
||||
При паузе 2–3 сек: **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/оригинал»
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
+180
@@ -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: 2688–2807, темп: 84–88°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 — НИЧЕГО не делать без прямого указания
|
||||
@@ -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, браузер |
|
||||
@@ -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
|
||||
@@ -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 — НИЧЕГО не делать без прямого указания
|
||||
@@ -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
@@ -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
@@ -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 — НИЧЕГО не делать без прямого указания
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user