88 Commits
Author SHA1 Message Date
Repinoid 6ca56620c1 docs: update PLANS.md — raw fixes progress 2026-07-10 15:53:11 +04:00
Repinoid afc42381d0 chore: move docs (CHANGELOG, STRUCTURE, QUICKSTART, resume, logo) to doc/, update AGENTS.md and PLANS.md paths 2026-07-10 15:45:47 +04:00
Repinoid 1baaa8dffe chore: remove trash from root — analysis.md, idea.md, legacy-*, .instructions.md, logo.jpg, morda.md 2026-07-10 15:43:22 +04:00
Repinoid bbbc06ec68 docs: PLANS.md — master plan index for all elmAI tasks 2026-07-10 15:40:48 +04:00
Repinoid 81bebcd566 docs: create AGENTS.md — comprehensive project description for AI agents 2026-07-10 15:05:34 +04:00
Repinoid 95834a4245 refactor: move legacy docs to doc/legacy/ with opus/sonnet subfolders 2026-07-10 13:13:42 +04:00
Repinoid 3ea7fc66d5 docs: STRUCTURE.md — описание всех файлов и папок 2026-07-10 12:58:09 +04:00
Repinoid ba48cd2daf fix: /response с фильтром device_id 2026-07-10 12:56:40 +04:00
Repinoid 15caff3204 docs: новое резюме для чата — raw-реле, 12 провалов, правила 2026-07-10 12:54:53 +04:00
Repinoid 129848d772 docs: архитектура — raw-реле, command_queue, два режима 2026-07-10 12:45:31 +04:00
Repinoid 29ace6119e docs: журнал неудач (12 провалов) + архив старого 2026-07-10 12:39:35 +04:00
Repinoid b9a33403fe docs: логика диагностики — два режима 2026-07-05 20:45:29 +04:00
Repinoid f79a1436d5 docs: хронология ошибок relay 2026-07-04 17:26:32 +04:00
Repinoid daa7fcf4a6 bump: v0.3.0-dev на морде 2026-07-04 14:51:40 +04:00
Repinoid caef3cc970 bump: v0.2.6-dev на морде 2026-06-28 19:59:55 +04:00
Repinoid 27883a90fa bump: v0.2.5-dev на морде 2026-06-28 19:46:00 +04:00
Repinoid cd1c0a977e fix: hello чистит старые команды для device_id — новая сессия = чистый старт 2026-06-28 19:35:47 +04:00
Repinoid 95ebc75484 bump: v0.2.4-dev на морде 2026-06-28 19:25:45 +04:00
Repinoid 5c1ba751c9 bump: v0.2.3-dev на морде 2026-06-28 19:13:03 +04:00
Repinoid 3a4701a835 fix: текст ELM Relay v2 на странице 2026-06-28 18:47:02 +04:00
Repinoid 8e1e88ee9a bump: v0.2.2-dev, новое имя ELM Relay v2 2026-06-28 18:43:44 +04:00
Repinoid d3133731d8 fix: ?v=5 в ссылке на APK — обход кеша браузера 2026-06-28 18:18:29 +04:00
Repinoid 06c0b9515e bump: v0.2.1-dev на морде 2026-06-28 11:47:50 +04:00
Repinoid 25c8eb20b9 fix: атомарный dequeue, двойной ATI, MAX_RETRIES 620ms, ретеншн, фильтр device_id 2026-06-28 11:46:00 +04:00
Repinoid c33bd8e2de fix: раздельные execute для CREATE TABLE + INDEX 2026-06-28 11:32:07 +04:00
Repinoid d80cb26ce5 fix: raw_elm.py на SQLite очередь (gunicorn-safe), таблица command_queue в db.py 2026-06-28 11:28:15 +04:00
Repinoid 45a6640dae fix: build_dynamic_script — 6 PID (4 частых + 2 средних/редких), interval_ms 1200ms 2026-06-28 11:26:14 +04:00
Repinoid f283113ab1 fix: канонический init Python (ATE0→..., drain после send, clone detect) 2026-06-28 11:23:12 +04:00
Repinoid d7bdd41df6 security: убрать api_key из config.yaml в env LLM_API_KEY 2026-06-28 11:15:03 +04:00
Repinoid 0fcbb1ab26 bump: v0.1.1-dev — версия на сайте 2026-06-14 19:14:14 +04:00
Repinoid 900d44806c fix: elm-raw.apk через /static/ 2026-06-14 19:05:47 +04:00
Repinoid c229b730c6 feat: raw relay — очередь команд, эндпоинты, консоль, index.html 2026-06-14 18:53:24 +04:00
Repinoid d01028b12b feat: raw ELM327 console — сырой слой, API, интерактивная консоль 2026-06-14 18:24:27 +04:00
Repinoid 7d23a874e0 docs: резюме сессии — v1.18.0-dev, авто-подбор, нерешённая проблема 2026-06-14 16:13:52 +04:00
Repinoid 2ea989e5a3 bump v1.18.0-dev 2026-06-14 15:10:05 +04:00
Repinoid 658b161730 fix: старт 2000ms, шаг +500ms — агрессивнее 2026-06-14 15:01:26 +04:00
Repinoid 1c37ea029e bump v1.17.0-dev 2026-06-14 14:05:01 +04:00
Repinoid de54c1ff40 feat: POST /api/v1/test/next + авто-подбор 2026-06-14 14:05:00 +04:00
Repinoid f9c2152c9d fix: routes.py wait=1500, repeat=8 2026-06-14 13:58:16 +04:00
Repinoid 7c1391dcf4 test: wait=1500ms, 2 PID — минимальный тест связи 2026-06-14 13:57:35 +04:00
Repinoid 9952c4507f bump v1.16.0-dev 2026-06-13 20:59:52 +04:00
Repinoid 6e59fef093 feat: build_test_script + mode=test 2026-06-13 20:57:24 +04:00
Repinoid 71200709ab bump v1.15.0-dev 2026-06-13 20:40:35 +04:00
Repinoid ecb42ee8d7 bump v1.14.0-dev 2026-06-13 20:35:37 +04:00
Repinoid a6eb06618f bump v1.13.0-dev 2026-06-13 20:33:51 +04:00
Repinoid 2b1422352c bump v1.12.0-dev 2026-06-13 20:20:40 +04:00
Repinoid f988478336 bump v1.11.0-dev 2026-06-13 10:36:58 +04:00
Repinoid bc928defdb docs: запрос Claude v2 — СТРОГО только анализ, код не менять 2026-06-13 10:27:30 +04:00
Repinoid 097740f348 fix: remove harmful OBD retries and improve ELM v1.5 stability 2026-06-13 10:21:51 +04:00
Repinoid e380efa98c bump v1.10.0-dev 2026-06-13 10:19:04 +04:00
Repinoid f5d2472184 docs: запрос Claude v2 — почему v1.9.0 хуже v1.3.0 2026-06-13 10:18:13 +04:00
Repinoid 5eeda440be bump v1.9.0-dev 2026-06-10 22:44:43 +04:00
Repinoid 90718df8ce docs: файл для Claude Sonnet — анализ ELM кода 2026-06-10 22:37:56 +04:00
Repinoid 29d09661a4 bump v1.8.0-dev 2026-06-10 22:34:52 +04:00
Repinoid fb1fae4f6e bump v1.7.0-dev 2026-06-10 22:32:22 +04:00
Repinoid b3b37e80e4 bump v1.6.0-dev 2026-06-10 22:30:32 +04:00
Repinoid 20649df8a5 bump v1.5.0-dev 2026-06-10 22:27:56 +04:00
Repinoid 89fcf5b8dc bump v1.4.0-dev 2026-06-10 22:26:44 +04:00
Repinoid 50cc615d80 fix: SyntaxError - triple quotes 2026-06-10 21:39:44 +04:00
Repinoid 6cf46401b0 bump v1.3.0-dev 2026-06-10 21:35:02 +04:00
Repinoid 0598c5f8e1 fix: PUT profile создаёт профиль если его нет (upsert) 2026-06-10 21:33:47 +04:00
Repinoid 3de1860494 bump v1.2.0-dev 2026-06-10 21:30:58 +04:00
Repinoid 105fc3b982 bump v1.1.0-dev 2026-06-10 21:28:22 +04:00
Repinoid 398a74c34d bump v1.0.0-dev 2026-06-10 21:25:27 +04:00
Repinoid d3a03359f2 bump v0.99.0-dev 2026-06-10 21:22:19 +04:00
Repinoid 29050c06c9 bump v0.98.0-dev 2026-06-10 21:17:27 +04:00
Repinoid 1215e82dc0 bump v0.96.0-dev 2026-06-10 21:10:09 +04:00
Repinoid 79c03e9dd6 bump v0.95.0-dev 2026-06-10 20:53:01 +04:00
Repinoid 6c6bed1f14 bump v0.94.0-dev, docs: история speed-test 2026-06-10 20:36:26 +04:00
Repinoid 74808332be docs: история 2026-06-10 — speed-test, адаптивный интервал, threading lock 2026-06-10 20:13:11 +04:00
Repinoid 008b1c11b5 fix: threading lock in Database for concurrent writes 2026-06-10 19:49:28 +04:00
Repinoid 77a374f7a4 docs: save MPScholar Automotive Sensing and Actuators article 2026-06-10 18:38:30 +04:00
Repinoid c7430c7106 bump v0.93.0-dev 2026-06-10 18:04:04 +04:00
Repinoid da38eccb72 docs: CHANGELOG — стратегия динамического теста 2026-06-10 16:48:54 +04:00
Repinoid 383bfb9566 bump v0.92.0 2026-06-10 16:37:28 +04:00
Repinoid 004a327afc bump v0.91.0 2026-06-10 15:52:13 +04:00
Repinoid 237149e87a bump v0.90.0 2026-06-10 15:46:20 +04:00
Repinoid e763999e85 bump v0.89.0-dev 2026-06-08 11:57:54 +04:00
Repinoid 04ac6e3097 bump v0.88.0-dev 2026-06-08 11:53:20 +04:00
Repinoid 31cba53798 fix: sessions API — нет колонки title, используем vin/car_info 2026-06-08 11:48:14 +04:00
Repinoid e5bc77ec15 bump v0.87.0-dev 2026-06-08 11:47:11 +04:00
Repinoid 4071a420c7 bump v0.86.0-dev, /api/v1/sessions 2026-06-08 11:33:48 +04:00
Repinoid 4491a02ef5 bump v0.85.0-dev 2026-06-08 11:17:09 +04:00
Repinoid a3ae9c9ddf bump v0.84.0-dev, debugLog → runOnUiThread 2026-06-08 09:13:51 +04:00
Repinoid 9c67ce6abb bump v0.83.0-dev, шрифт 10sp 2026-06-08 09:07:20 +04:00
Repinoid fd17b9d03e bump v0.82.0-dev, fix ping-llm auth, шрифт 11sp 2026-06-08 09:04:43 +04:00
Repinoid ca9d5b8eda bump v0.81.0-dev 2026-06-08 09:00:08 +04:00
Repinoid bac9445026 bump v0.80.0-dev 2026-06-08 08:50:51 +04:00
81 changed files with 5449 additions and 1085 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"servers": {
"elmer-server": {
"type": "stdio",
"command": "python3",
"args": [
"/home/naeel/elmer/.vscode/mcp_server.py"
]
}
}
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""MCP сервер для Elmer — БД, логи, ssh."""
import json, subprocess, sys
def handle(req):
method = req.get("method", "")
params = req.get("params", {})
if method == "list_tools":
return {
"tools": [
{"name": "query_db", "description": "SQL-запрос к elmer.db", "inputSchema": {"type": "object", "properties": {"sql": {"type": "string"}}}},
{"name": "server_logs", "description": "Логи сервера (последние N строк)", "inputSchema": {"type": "object", "properties": {"lines": {"type": "number", "default": 30}}}},
{"name": "ssh", "description": "Выполнить bash-команду на ВМ", "inputSchema": {"type": "object", "properties": {"cmd": {"type": "string"}}}},
]
}
if method == "call_tool":
name = params.get("name", "")
args = params.get("arguments", {})
if name == "query_db":
ssh(f"sqlite3 /opt/elmer/elmer.db \"{args['sql']}\"")
elif name == "server_logs":
ssh(f"sudo journalctl -u elmer --no-pager -n {args.get('lines', 30)}")
elif name == "ssh":
ssh(args["cmd"])
else: return {"error": f"unknown tool: {name}"}
return {"result": "ok"}
def ssh(cmd):
r = subprocess.run(["ssh", "-i", "/home/naeel/.ssh/naeel_vm_id_ed25519", "naeel@5.172.178.213", cmd], capture_output=True, text=True)
return {"stdout": r.stdout, "stderr": r.stderr}
for line in sys.stdin:
line = line.strip()
if line:
resp = handle(json.loads(line))
print(json.dumps(resp), flush=True)
-57
View File
@@ -1,57 +0,0 @@
# Инструкция для Copilot — проект elmAI
> Последнее обновление: 31 мая 2026 · v0.36.0-dev
## Репозитории
| Репо | Назначение | Хостинг |
|------|-----------|---------|
| `elmer/` (этот) | Сервер Python/Flask | gitea.services.ngcloud.ru/Nail/elmer |
| `elmer/android/` | Android-приложение Kotlin | github.com/Repinoid/elmer-android |
## Деплой
### Сервер (obdai.ru, 5.172.178.213)
- Код: `/opt/elmer` (git clone gitea)
- Ветка: `master` (по умолчанию)
- Деплой: `ssh obdai.ru "cd /opt/elmer && git pull && sudo systemctl restart elmer"`
- Сервис: `gunicorn -w 4 -b 127.0.0.1:8000 web.app:app`
- Прокси: nginx :443 → :8000
- **APK отдавать напрямую через nginx, НЕ через Flask/gunicorn:** `location = /elmer.apk { alias /opt/elmer/web/static/app-debug.apk; }`
### APK
- Сборка: **автоматически GitHub Actions** при пуше в master
- Деплой: CI сам заливает APK на сервер (`appleboy/scp-action`)
- Ссылка: `https://obdai.ru/elmer.apk` → nginx отдаёт напрямую `web/static/app-debug.apk`
### Версионирование
- APK: `android/app/build.gradle.kts``versionName`
- Сайт: `web/templates/index.html` (синхронизировать вручную)
- Документация: в заголовках `.md` файлов
## Правила работы
1. **ЕСЛИ в диалоге содержится ВОПРОС в любой форме — только ответить. НИЧЕГО НЕ ПРЕДПРИНИМАТЬ.** Не писать код, не редактировать файлы, не коммитить, не деплоить. Только прямые императивы («сделай», «исправь», «напиши», «внеси», «задеплой») — команда к действию.
2. **Не выдумывать инфраструктуру.** Никаких Docker, Kubernetes. Всё на голом железе.
3. **Читать документацию перед действиями.** `doc/architecture.md` — канонический источник.
3. **Не редактировать отчёты Опуса.** `doc/opus-review*.md` — только для чтения.
4. **Ключи и токены:** LLM-ключ только на сервере (`config.yaml`), НЕ в APK. `X-Api-Key` приложения — через `BuildConfig.API_KEY` из `local.properties`.
5. **Git:** `elmer/` и `elmer/android/` — отдельные репо, отдельные коммиты.
6. **Ветки:** `master` — продакшен, `opus-fixes` и др. — для правок. Вливать в master когда готово.
7. **После правок:** коммит + пуш + (если сервер) деплой через SSH.
8. **Версия:** менять в трёх местах — `build.gradle.kts`, `index.html`, доки.
9. **Документировать изменения** в `doc/history/YYYY-MM-DD.md` после каждого сеанса работы. Формат: 🔴/🟡/🟢 для приоритета, по файлам.
## Структура сервера
```
elmer/
├── api/ # REST, БД, скрипты, парсер
├── brain/ # LLM-клиент, промпты
├── obd/ # ELM327 стейт-машина
├── web/ # Flask, шаблоны, статика (APK)
├── android/ # Android-приложение (отдельный репо)
├── doc/ # Документация
├── config.yaml # LLM API key, порты
└── run.py # Локальный запуск
```
+285
View File
@@ -0,0 +1,285 @@
# elmAI — полное описание проекта для AI-агентов
> Последнее обновление: 2026-07-10 | Версия app: 1.18.0-dev | Версия raw: 0.4.1-dev
---
## 1. ЧТО ЭТО
elmAI — OBD2-диагностика автомобиля через ELM327-адаптер + LLM (DeepSeek).
Телефон подключается к ELM327 по Bluetooth, собирает данные с ЭБУ, отправляет на сервер, сервер анализирует через LLM и возвращает диагноз.
---
## 2. РЕПОЗИТОРИИ
| Репо | URL | Ветка | Что внутри |
|------|-----|-------|-----------|
| **Сервер** | `gitea.services.ngcloud.ru/Nail/elmer` | `dynamic-tests` | Python Flask + elmAI бэкенд |
| **Android** | `github.com/Repinoid/elmer-android` | `opus-fixes` | Kotlin Android-приложение |
**ВАЖНО**: Android-репо лежит ВНУТРИ серверного: `/home/naeel/elmer/android/`. Это отдельный git-репо со своим remote. Коммитить и пушить надо ИЗНУТРИ `android/`.
---
## 3. СЕРВЕР (obdai.ru, 5.172.178.213)
```
elmer/
├── api/ # Flask API
│ ├── routes.py # Основные эндпоинты (script, upload, chat, probe, sessions)
│ ├── raw_elm.py # Командная очередь для raw-реле (SQLite table command_queue)
│ ├── db.py # SQLite (sessions, device_profiles, command_queue)
│ ├── scripts.py # Сборка диагностических скриптов L0/L1/L2 + dynamic
│ ├── parser.py # Парсинг ответов ELM327 (PID, DTC, VIN)
│ ├── dtc.py # Эндпоинты DTC
│ ├── ping.py # Эндпоинты ping/ping-llm
│ └── config.py # Загрузка config.yaml
├── brain/ # LLM-клиент
│ ├── client.py # Diagnoser — HTTP к DeepSeek
│ └── prompts.py # Промпты для диагностики
├── obd/ # ELM327 протокол (Python)
│ ├── protocol.py # Стейт-машина AndrOBD (ElmProt.java)
│ ├── connection.py, commands.py, classifier.py, probe.py, state.py, timing.py
├── web/ # Flask web
│ ├── app.py # Точка входа, регистрация blueprints
│ ├── templates/index.html # Страница загрузки APK
│ └── static/ # APK-файлы
├── doc/ # ВСЯ документация
├── config.yaml # API-ключи, порты
└── deploy.sh # Скрипт деплоя
```
**Стек**: Python 3, Flask, gunicorn (4 воркера, порт 8000), nginx (:443 → :8000), SQLite.
**Сервис**: `sudo systemctl restart elmer`
---
## 4. ANDROID
### 4.1. Основное приложение (`app/`) — прямая диагностика
**Пакет**: `ru.elmer.client` | **Версия**: 1.18.0-dev (versionCode 38)
```
app/src/main/java/ru/elmer/client/
├── Config.kt # Константы: HOST, SCRIPT_URL, defaultScript(), client()
├── db/SessionDb.kt # SQLite: sessions, responses
├── elm/
│ ├── ElmProtocol.kt # Стейт-машина AndrOBD (1:1 с ElmProt.java)
│ ├── ElmChecker.kt # Проверка ELM: checkDevice(), checkEcu(), scanDtc(), sendRaw()
│ └── ObdDecoder.kt # Декодер PID/DTC/VIN (object-синглтон)
├── script/
│ └── DynamicCollector.kt # Циклический опрос PID с интервалом
├── server/
│ └── ServerClient.kt # HTTP к серверу (OkHttp): ping, pingLlm, chat, getSessions, uploadSession, downloadScript
└── ui/
└── MainActivity.kt # UI: индикаторы, кнопка-трансформер, чат, диагностика, динамический тест
```
**8 классов**. Зависимости: OkHttp 4.12.0, AndroidX, org.json. БЕЗ Room, Coroutines, DI.
**Поток диагностики**: MainActivity → ElmChecker → ElmProtocol → команды → ObdDecoder → SessionDb → ServerClient.uploadSession() → ответ от LLM.
### 4.2. Raw-реле (`raw/`) — ретранслятор команд
**Пакет**: `ru.elmer.raw` | **Версия**: 0.4.1-dev (versionCode 18)
```
raw/src/main/java/ru/elmer/raw/
├── ElmProtocol.kt # 1:1 копия app/ElmProtocol.kt
├── ElmActor.kt # Single-thread executor вокруг ElmProtocol
├── RawRelayService.kt # Foreground-сервис: BT→init→поллинг команд→ответ
├── RelayClient.kt # HTTP к /api/v1/elm/raw/* (OkHttp, БЕЗ X-Api-Key)
└── MainActivity.kt # Минимальный UI (выбор BT, статус, счётчики)
```
**5 классов**. Отдельный APK (`applicationId: ru.elmer.raw`).
**Поток**: RawRelayService → connectBt → ElmProtocol.init() → hello → цикл: pollCommand → sendCommand → postResponse.
**Назначение**: тупой ретранслятор. Сервер диктует команды, телефон передаёт в ELM и возвращает ответы. Используется для интерактивной диагностики через Copilot и тестов 3 мин / 5 мин.
---
## 5. ANDROID — ЧТО СДЕЛАНО (рефакторинг 2026-07-10)
Ветка `opus-fixes`, 8 коммитов:
1. **Config.kt** — единый источник хоста (`https://obdai.ru`), замена всех хардкодов
2. **default_script.json**`assets/`, DEFAULT_SCRIPT из кода удалён
3. **ServerClient** — добавлены `chat()` и `getSessions()`, весь HTTP через OkHttp + X-Api-Key
4. **HttpURLConnection** выпилен из MainActivity
5. **ElmChecker.sendRaw()** — инкапсуляция ElmProtocol, `getElm()` удалён
6. **FQN → import** — все полные имена заменены на нормальные import'ы
7. **Удалён мёртвый код**: ScriptRunnerService, ScriptEngine, UploadProgress + FOREGROUND_SERVICE permissions
**Результат**: 8 классов (было 10), 0 HttpURLConnection, 0 FQN, 0 getElm(), 0 obdai.ru вне Config, весь HTTP аутентифицирован.
---
## 6. ANDROID — ЧТО НЕ СДЕЛАНО (TODO)
### app/
- Разбить MainActivity (~750 строк) — вынести логику из UI
- Разбить ElmChecker (372 строки) — отделить BT от ELM-команд
- v1.5 клоны: деградация после 10-12 команд (ограничение железа)
### raw/
- **deviceId** генерится заново при каждом создании RelayClient — сохранить в SharedPreferences
- **Нет аутентификации** — BuildConfig.API_KEY есть, но RelayClient его не шлёт
- **Нет retry** при ошибках HTTP
- **SERVER_URL** захардкожен в build.gradle.kts и дублируется в Intent extra
- **drainInput() в write()** — потенциальный сдвиг буфера (Неудача #5 из failures-journal.md)
### Сервер
- Script Engine для режимов «3 мин на месте» / «5 мин в движении» — НЕ РЕАЛИЗОВАН
- Дублирование: routes.py и script_endpoint.py (script_endpoint.py — мёртвый)
---
## 7. РЕЖИМЫ ДИАГНОСТИКИ
### Режим 1: Прямая диагностика (app/)
Однократный сбор данных: скрипт → батч ответов → сервер → LLM → диагноз.
### Режим 2: Динамический тест (app/)
Циклический опрос PID. Кнопка СТАРТ/СТОП. Сервер подбирает тайминги через `/api/v1/test/next`.
### Режим 3: Raw-реле (raw/)
Сервер управляет потоком команд. Телефон — тупой ретранслятор.
### Режим 4: Прогрев на месте (raw/, 3 минуты)
3 PID (RPM, темп, дроссель) каждые 2 секунды × 90 циклов = 270 запросов. Оценка прогрева, холостых, реакции на газ.
### Режим 5: В движении (raw/, 5 минут)
Те же 3 PID каждые 2 секунды × 150 циклов = 450 запросов. Нагрузочный тест, динамика разгона.
*Режимы 4 и 5 требует реализации Script Engine на сервере.*
---
## 8. API ЭНДПОИНТЫ
### Основные (app/)
| Метод | Путь | Назначение |
|-------|------|-----------|
| GET | `/api/v1/ping` | Проверка сервера |
| GET | `/api/v1/ping-llm` | Проверка LLM |
| GET | `/api/v1/script?mode=test` | Скрипт диагностики |
| POST | `/api/v1/session/upload` | Загрузка батча + LLM-анализ |
| POST | `/api/v1/chat` | Чат с LLM |
| GET | `/api/v1/sessions` | Список сессий |
| POST | `/api/v1/test/next` | Следующий шаг динамического теста |
| GET/PUT | `/api/v1/elm/profile/<mac>` | Профиль скорости ELM |
### Raw-реле (raw/)
| Метод | Путь | Назначение |
|-------|------|-----------|
| POST | `/api/v1/elm/raw/hello` | Android: «я готов» (чистит старые команды) |
| POST | `/api/v1/elm/raw/cmd` | Copilot/сервер: поставить команду в очередь |
| GET | `/api/v1/elm/raw/cmd?device_id=X` | Android: забрать pending команду |
| POST | `/api/v1/elm/raw/response` | Android: вернуть ответ |
| GET | `/api/v1/elm/raw/response?device_id=X&seq=N` | Copilot: прочитать ответ |
| GET | `/api/v1/elm/raw/status` | Статус устройства |
---
## 9. ДЕПЛОЙ
### 9.1. Сервер
```bash
cd /home/naeel/elmer
git add -A && git commit -m "..." && git push origin dynamic-tests
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 "
cd /opt/elmer && git checkout dynamic-tests && git pull origin dynamic-tests
pip install -r requirements.txt
sudo systemctl restart elmer
"
```
### 9.2. Android APK (app)
```bash
# 1. Bump версии в app/build.gradle.kts (versionCode и versionName)
cd /home/naeel/elmer/android
sed -i 's/versionCode = XX/versionCode = YY/' app/build.gradle.kts
sed -i 's/versionName = "X.Y.Z-dev"/versionName = "X.Y+1.Z-dev"/' app/build.gradle.kts
# 2. Закоммитить + запушить
git add -A && git commit -m "bump vX.Y+1.Z-dev" && git push origin opus-fixes
# 3. Залить исходники на сервер и собрать APK
cd /home/naeel/elmer
tar czf /tmp/android-src.tar.gz --exclude='.git' --exclude='build' --exclude='.gradle' android/
scp -i ~/.ssh/naeel_vm_id_ed25519 /tmp/android-src.tar.gz naeel@5.172.178.213:/tmp/
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 "
rm -rf /opt/elmer/android && tar xzf /tmp/android-src.tar.gz -C /opt/elmer/
cd /opt/elmer/android && gradle wrapper --gradle-version 8.7
export ANDROID_SDK_ROOT=\$HOME/android-sdk
./gradlew :app:clean :app:assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/
"
# 4. Обновить версию в /opt/elmer/web/templates/index.html и /opt/elmer/templates/index.html
```
### 9.3. Raw APK
```bash
# Аналогично app, но:
# - bump версии в raw/build.gradle.kts
# - сборка: ./gradlew :raw:assembleDebug
# - копия: cp raw/build/outputs/apk/debug/raw-debug.apk /opt/elmer/web/static/elm-raw-v022.apk
```
---
## 10. ПРАВИЛА (НЕ НАРУШАТЬ)
1. **НИЧЕГО не делать без прямого указания пользователя.** Даже если видишь проблему — только сказать.
2. **На вопрос — только ответ.** Не продолжать «а ещё могу...», не предлагать помощь.
3. **После выполнения команды — сказать «готово» и ЖДАТЬ.**
4. **Коммит + push после КАЖДОЙ правки.** Один коммит = одна правка. Формат: `fix:`, `feat:`, `refactor:`, `bump:`, `docs:`, `style:`, `chore:`.
5. **При деплое — всегда bump версии.** Инкрементировать патч (Z в X.Y.Z-dev).
6. **ELM327 — только как AndrOBD (ElmProt.java).** Никакой самодеятельности в протоколе. Init: ATSP0→ATAT1→ATS0→ATL0→ATE0. Никакого drainInput() перед write().
7. **Не материться.** Пользователь матерится — ты нет.
8. **Не гадать.** Если не уверен — проверить факты чтением кода.
9. **Код правит ТОЛЬКО пользователь или Copilot по команде.** Не исполнять советы Opus по ELM-командам — Opus специалист по архитектуре кода, а не по ELM327.
---
## 11. КЛЮЧЕВЫЕ ДОКУМЕНТЫ
| Файл | Содержание |
|------|-----------|
| `AGENTS.md` | Этот файл — полное описание проекта |
| `PLANS.md` | Мастер-план: что сделано, что предстоит |
| `doc/architecture.md` | Архитектура сервера и Android |
| `doc/diagnostic-logic.md` | 5 режимов диагностики |
| `doc/failures-journal.md` | 12 провалов при разработке raw-реле |
| `doc/SETUP.md` | Настройка окружения |
| `doc/CHANGELOG.md` | История версий |
| `doc/STRUCTURE.md` | Полное дерево файлов |
| `doc/QUICKSTART.md` | Быстрый старт |
| `doc/resume.txt` | Краткое резюме для нового чата |
| `.github/copilot-instructions.md` | Правила для Copilot |
| `android/README.md` | Документация Android-проекта |
| `android/RAW-FIX-PLAN.md` | План исправлений raw-реле |
| `android/doc/opus-response-arch-2026-07-10.md` | Анализ архитектуры от Opus |
| `android/doc/opus-response-plan-2026-07-10.md` | План рефакторинга от Opus |
---
## 12. КОНТАКТЫ / ДОСТУП
- **Сервер**: 5.172.178.213, SSH: `naeel@5.172.178.213`, ключ: `~/.ssh/naeel_vm_id_ed25519`
- **Домен**: obdai.ru (SSL через certbot)
- **Gitea**: gitea.services.ngcloud.ru/Nail/elmer
- **GitHub**: github.com/Repinoid/elmer-android
+55
View File
@@ -0,0 +1,55 @@
# Планы elmAI
> Последнее обновление: 2026-07-10
## ✅ Сделано
### Android (`:app`) — рефакторинг (2026-07-10)
- [x] Config.kt — единый источник хоста
- [x] default_script.json → assets
- [x] ServerClient.chat() + getSessions() — весь HTTP через OkHttp + X-Api-Key
- [x] HttpURLConnection выпилен из MainActivity
- [x] ElmChecker.sendRaw() вместо getElm()
- [x] FQN → import
- [x] Удалён мёртвый код: ScriptRunnerService, ScriptEngine, UploadProgress
- [x] BtConnector — BT вынесен из ElmChecker
- [x] ChatController — чат вынесен из MainActivity
- [x] DiagnosisRunner — диагностика вынесена из MainActivity
- [x] IndicatorBar — светофоры вынесены из MainActivity
- [x] AGENTS.md — полное описание проекта
- [x] README.md — документация Android-проекта
## 🔜 Предстоит
### Android (`:raw`) — исправления (2026-07-10)
- [x] Config.kt — единый источник SERVER_URL, API_KEY
- [x] deviceId в SharedPreferences — RelayClient(context)
- [x] X-Api-Key — auth() во всех запросах
- [x] Retry HTTP — 3 попытки exponential backoff
- [x] SERVER_URL — только BuildConfig, EXTRA_SERVER_URL удалён
- [x] RawRelayService + MainActivity — полные комментарии
- [ ] detectClone() из app ElmProtocol — v1.5 клоны вешаются на ATAT1
- [ ] drainInput() в write() — потенциальный сдвиг буфера
→ План: [`android/RAW-FIX-PLAN.md`](android/RAW-FIX-PLAN.md)
### Сервер — рефакторинг
- [ ] Разобраться с дублированием routes.py / script_endpoint.py
- [ ] Script Engine для режимов «3 мин на месте» / «5 мин в движении»
### Архитектурные вопросы (не решено)
- [ ] Перенос диагностики в foreground-сервис (риск handoff сокета для v1.5)
- [ ] Разбивка MainActivity дальше (динамический тест, история)
---
## Архив планов
| Файл | Дата | Тема |
|------|------|------|
| `android/doc/opus-response-arch-2026-07-10.md` | 2026-07-10 | Анализ архитектуры от Opus |
| `android/doc/opus-response-plan-2026-07-10.md` | 2026-07-10 | План рефакторинга от Opus |
| `android/doc/opus-arch-questions-2026-07-10.md` | 2026-07-10 | Вопросы Opus по архитектуре |
| `android/RAW-FIX-PLAN.md` | 2026-07-10 | План исправлений raw-реле |
| `doc/diagnostic-logic.md` | — | 5 режимов диагностики |
| `doc/CHANGELOG.md` | — | История версий |
-249
View File
@@ -1,249 +0,0 @@
# Elmer — анализ и план (2026-05-25)
## Суть проекта
Сервис анализа ошибок электроники автомобиля через ELM327 OBD2 + LLM.
## Железо
- **Сканер:** ELM327 Bluetooth v1.5, чип PIC18F25K80 (китайский клон)
- **Ноутбук разработчика:** с Bluetooth, будет соединяться с ELM327 напрямую для отладки
## Целевая аудитория
- Технически любопытный автовладелец (не профессионал, но и не «глубинарий»)
- Уже имеет ELM327 — значит базовое понимание есть
- Хочет понять проблему, а не просто получить код ошибки
## Ключевые требования к ответам
- **Честная уверенность:** «С вероятностью ~80% проблема в X, потому что...»
- **Пояснение логики:** почему именно этот вывод
- **Предупреждения:** «Если НЕ помогло — тогда проверь Y»
- **Никаких категоричных «меняй X»** без 100% уверенности
- **Liability Protection:** нельзя чтобы пользователь сломал машину из-за неверного диагноза
## LLM
- Рассматривается DeepSeek (дёшево через API)
- Или другая простая/дешёвая модель
- Нужен RAG/grounding на реальных repair manuals и TSB, чтобы минимизировать галлюцинации
## Компоненты системы
1. **Android-приложение** (в последнюю очередь)
- Стабильная версия Android (не гоняться за новейшей)
- Максимально простое: минимум кнопок
- Русский язык
- Bluetooth SPP → ELM327
2. **Сервер** (после отладки логики на ноутбуке)
- Принимает данные от приложения
- Формирует запросы к LLM
- Итеративный цикл: запрос → ответ → может запросить ещё параметры или действия от пользователя
- Отдаёт диагноз с пояснениями
3. **LLM-слой**
- Промпт с контекстом автомобиля (VIN → марка/модель/двигатель)
- RAG на базу знаний (ошибки, мануалы, TSB)
- Итеративная диагностика: сервер может переспрашивать LLM
## Протокол диагностики (конечный автомат)
Цикл:
1. Приложение считывает VIN → сервер
2. Приложение считывает коды ошибок → сервер
3. Сервер → LLM: первичный анализ
4. LLM может запросить:
- Дополнительные PID'ы с ЭБУ (live data)
- Действия от пользователя (прогазовать, проехать, считать на холодную и т.д.)
5. Повторять пока не будет достаточно данных для диагноза
6. Финальный ответ: диагноз + степень уверенности + пояснения + что делать
## План разработки (три фазы)
### Фаза 1: Ноутбук + ELM327 (СЕЙЧАС)
- Python-скрипт: Bluetooth → ELM327 → читаем VIN, ошибки, PID'ы
- Отправляем в LLM вручную — отлаживаем логику, промпты, цикл вопросов-ответов
- Никакого сервера, никакого Android
### Фаза 2: Сервер
- Flask/FastAPI — принимать данные, проксировать в LLM
- База знаний / RAG
- State machine диагностики
### Фаза 3: Android-приложение
- Bluetooth SPP (Serial Port Profile) — есть нюансы на Android 12+
- Минималистичный UI
- Отправка данных на сервер, отображение ответов
## Риски
1. **ELM327 v1.5 клон** — неполный протокол, глюки на高速 CAN
2. **PID'ы разные у разных марок** — нужна БД по производителям
3. **LLM галлюцинации** — только grounding/RAG спасёт
4. **Bluetooth SPP на Android 12+** — permissions, pairing
## Ресурсы
- `python-OBD` — библиотека для работы с ELM327 (или свой serial-протокол)
- `pyserial` уже установлен в системе
- DeepSeek API (или OpenRouter как альтернатива)
---
## Структура проекта (создана 2026-05-25)
```
elmer/
├── elmer/ # Python-пакет
│ ├── __init__.py # версия 0.1.0
│ ├── config.py # загрузка config.yaml + подстановка ${ENV}
│ ├── elm.py # ELM327: pyserial, VIN, DTC, PID
│ ├── db.py # SQLite: cars, tokens, llm_messages, ecu_parameters, dtc_codes
│ ├── prompts.py # SYSTEM_PROMPT + build_user_prompt()
│ └── diagnose.py # DeepSeek API (OpenAI-совместимый)
├── config.yaml # настройки (BT-порт, API-ключ, PID'ы)
├── requirements.txt # pyserial, pyyaml, requests
├── run.py # главный вход: ELM → данные → LLM → печать + сохранение
├── idea.md # исходная задумка
└── analysis.md # этот файл
```
## Запуск (в салоне авто)
```bash
# 1. Установить зависимости
pip install -r requirements.txt
# 2. Сопрячь ELM327 по Bluetooth
bluetoothctl pair 11:22:33:44:55:66
# (в config.yaml прописан порт /dev/rfcomm0)
# 3. Запустить
DEEPSEEK_API_KEY=sk-... python run.py
```
Что произойдёт:
1. Подключится к ELM327
2. Прочитает VIN
3. Считает ошибки (stored mode 03 + pending mode 07)
4. Считает параметры (обороты, температура, скорость, дроссель, MAP, IAT, топливные тримы)
5. Отправит в DeepSeek → напечатает диагноз
6. Сохранит всё в `elmer.db` (SQLite)
---
## Десктопный UI (Web)
Для тестирования на ноутбуке (не тыкать грязным пальцем в телефон):
- `web/app.py` — Flask (порт 5005), один endpoint `/api/diagnose` (POST)
- `web/templates/index.html` — одна кнопка, тёмная тема, результат
- В будущем этот же код — прототип серверного API
Запуск:
```bash
DEEPSEEK_API_KEY=sk-... python web/app.py
# Открыть http://localhost:5005
```
## Версии для пользователей (будущее)
- **Android** — Kotlin/Java, Bluetooth SPP
- **Windows** — тот же веб-интерфейс в WebView (или Electron, или просто браузер)
- Общий серверный API между ними
---
---
## Архитектура клиент-сервер (решено 2026-05-25)
### Принцип: тонкий клиент
Клиент ничего не знает о диагнозе. Только транспорт:
```
ELM327 ←Bluetooth SPP→ Android Client ←HTTP JSON→ Сервер ←API→ DeepSeek
```
### Клиент как универсальный SDK
- Пользователь вводит URL своего сервера (или используется наш по умолчанию)
- Протокол HTTP/JSON документирован — любой backend
- Два режима работы клиента:
1. **«Опрос» (основной):** клиент сам читает VIN + DTC + PID'ы, шлёт JSON серверу
2. **«Ретранслятор» (расширенный):** сервер шлёт сырые AT-команды, клиент пересылает ответ
### Десктоп
- Браузер (Chrome) → локальный Flask → pyserial → ELM327
- Отдельного «приложения» для Windows не нужно
- Тот же `web/app.py` — и тестовый UI, и прототип сервера
### Открытость и доверие
| Что | Где | Зачем |
|---|---|---|
| **Клиент (Android)** | GitHub (открытый) | Доверие — любой может проверить код, собрать сам |
| **Сервер (Python)** | Gitea (закрытый) | API-ключи, логика, коммерческая часть |
| **Публикация** | RuStore | Бесплатно, модерация = дополнительное доверие |
### Git-стратегия
- `gitea.services.ngcloud.ru/Nail/elmer` — разработка сервера (текущий репо)
- `github.com/Nail/elmer-android` — клиент (будет создан), лицензия MIT
- Серверный репо на GitHub НЕ публикуем
---
## TODO / Дорожная карта
### 🔴 Фаза 1 — отладка на ноутбуке (ближайшая)
- [ ] Проверить Bluetooth на ноутбуке: `hciconfig`, `bluetoothctl`
- [ ] Сопрячь ELM327: `bluetoothctl pair <MAC>`
- [ ] Привязать к `/dev/rfcomm0`: `rfcomm bind 0 <MAC>`
- [ ] `pip install -r requirements.txt`
- [ ] `DEEPSEEK_API_KEY=sk-... python run.py` — консольный тест
- [ ] `DEEPSEEK_API_KEY=sk-... python web/app.py` — веб-тест
- [ ] Подключить реальную машину, считать VIN + ошибки + параметры
- [ ] Оценить качество ответа DeepSeek
### 🟡 Фаза 2 — сервер
- [ ] Выделенный сервер/ВМ (или Kubernetes pod)
- [ ] Flask → production (gunicorn)
- [ ] API: `/api/diagnose` (как сейчас), + `/api/sessions`, + `/api/history/<vin>`
- [ ] База: миграция SQLite → PostgreSQL
- [ ] RAG: база знаний (repair manuals, TSB) для grounding
- [ ] State machine: итеративные запросы к LLM
- [ ] HTTPS (Let's Encrypt)
### 🟢 Фаза 3 — Android-клиент
- [ ] Создать репо `github.com/Nail/elmer-android`
- [ ] Kotlin, minSdk ~24 (Android 7), targetSdk 34
- [ ] Bluetooth SPP: поиск, pairing, connect, read/write
- [ ] OBD2 парсер: VIN (0902), DTC (03/07), PID (01XX)
- [ ] UI: одна кнопка «Диагностика» + поле ввода URL сервера
- [ ] HTTP-клиент: POST JSON на сервер, показ ответа (Markdown → текст)
- [ ] Permissions: только BLUETOOTH + INTERNET
- [ ] Подпись APK, публикация в RuStore
- [ ] README: как собрать самому, как использовать с чужим сервером
---
## Заметки по итогам обсуждения
- **Гипотеза подтверждена:** DeepSeek уже дал полный анализ по логам VCDS. Лучше гугла.
- **Модерация RuStore:** проверяет вредоносный код и permissions. BLUETOOTH + INTERNET — вопросов не вызовет.
- **Sideload (APK напрямую):** проверок нет, но permissions видны до установки.
- **Chrome на Android НЕ может:** Web Bluetooth API только BLE, Web Serial API не поддерживается.
- **Termux с Python:** теоретически, но Bluetooth-доступ сложен.
- **ELM327 v1.5 (PIC18F25K80):** китайский клон. Неполный протокол, возможны глюки. Держать в уме.
- **Нет готового аналога:** ниша новая (LLM + OBD2), старые приложения без AI-анализа.
---
*Продолжить: тестировать в салоне авто с реальным ELM327.*
+138 -105
View File
@@ -10,6 +10,7 @@
import json
import sqlite3
import threading
from datetime import datetime, timezone
from pathlib import Path
@@ -21,6 +22,7 @@ class Database:
self.conn.row_factory = sqlite3.Row
self.conn.execute("PRAGMA journal_mode=WAL")
self.conn.execute("PRAGMA busy_timeout=30000")
self._lock = threading.Lock()
self._init_schema()
def __enter__(self):
@@ -112,6 +114,7 @@ class Database:
elm_desc TEXT,
protocol TEXT,
voltage TEXT,
response_time_ms INTEGER DEFAULT 250,
supported TEXT,
unsupported TEXT,
errors TEXT,
@@ -127,6 +130,7 @@ class Database:
# Миграции: добавляем колонки, которых нет в старых БД
migrations = [
"ALTER TABLE device_profiles ADD COLUMN response_time_ms INTEGER DEFAULT 250",
"ALTER TABLE sessions ADD COLUMN device_uuid TEXT",
"ALTER TABLE sessions ADD COLUMN phone_lang TEXT",
"ALTER TABLE sessions ADD COLUMN phone_tz TEXT",
@@ -156,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:
@@ -175,65 +203,65 @@ class Database:
Если request_id передан и уже существует — silently return (идемпотентность).
"""
ci = client_info
with self._lock:
ci = client_info
# Подсчёт DTC/PID из ответов
dtc_count = 0
pid_count = 0
for r in responses:
dec = (r.get("decoded") or "").lower()
if dec.startswith("dtc"):
dtc_count += 1
elif ":" in dec and not dec.startswith(("vin", "dtc", "elm", "protocol")):
pid_count += 1
# Подсчёт DTC/PID из ответов
dtc_count = 0
pid_count = 0
for r in responses:
dec = (r.get("decoded") or "").lower()
if dec.startswith("dtc"):
dtc_count += 1
elif ":" in dec and not dec.startswith(("vin", "dtc", "elm", "protocol")):
pid_count += 1
# VIN из ответов
vin = None
for r in responses:
dec = (r.get("decoded") or "")
if dec.startswith("VIN:"):
vin = dec[4:].strip()
if len(vin) != 17:
vin = None
break
# VIN из ответов
vin = None
for r in responses:
dec = (r.get("decoded") or "")
if dec.startswith("VIN:"):
vin = dec[4:].strip()
if len(vin) != 17:
vin = None
break
resp_json_str = json.dumps(response_json, ensure_ascii=False) if response_json else None
resp_json_str = json.dumps(response_json, ensure_ascii=False) if response_json else None
self.conn.execute("""
INSERT OR IGNORE INTO sessions (
client_ip, real_ip, user_agent, content_length,
phone_model, phone_maker, android_version, android_sdk,
app_version, android_id, device_uuid, phone_lang, phone_tz, phone_display,
elm_mac, elm_bt_name, obd_protocol,
self.conn.execute("""
INSERT OR IGNORE INTO sessions (
client_ip, real_ip, user_agent, content_length,
phone_model, phone_maker, android_version, android_sdk,
app_version, android_id, device_uuid, phone_lang, phone_tz, phone_display,
elm_mac, elm_bt_name, obd_protocol,
vin, dtc_count, pid_count,
duration_ms, response_count, error_count,
retry_count, timeout_count, script_mode,
transport, mock_mode, car_info,
diagnosis_text, diagnosis_len, llm_model,
llm_duration_ms, llm_success,
raw_responses, request_id, response_json
) VALUES (?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?, ?,?,?,?, ?,?,?,?,?)
""", (
ci.get("client_ip"), ci.get("real_ip"), ci.get("user_agent"),
ci.get("content_length"),
ci.get("phone_model"), ci.get("phone_maker"), ci.get("android_version"),
ci.get("android_sdk"), ci.get("app_version"), ci.get("android_id"),
ci.get("device_uuid"),
ci.get("phone_lang"), ci.get("phone_tz"), ci.get("phone_display"),
ci.get("elm_mac"), ci.get("elm_bt_name"), ci.get("obd_protocol"),
vin, dtc_count, pid_count,
duration_ms, response_count, error_count,
retry_count, timeout_count, script_mode,
transport, mock_mode, car_info,
diagnosis_text, diagnosis_len, llm_model,
llm_duration_ms, llm_success,
raw_responses, request_id, response_json
) VALUES (?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?, ?,?,?,?, ?,?,?,?,?)
""", (
ci.get("client_ip"), ci.get("real_ip"), ci.get("user_agent"),
ci.get("content_length"),
ci.get("phone_model"), ci.get("phone_maker"), ci.get("android_version"),
ci.get("android_sdk"), ci.get("app_version"), ci.get("android_id"),
ci.get("device_uuid"),
ci.get("phone_lang"), ci.get("phone_tz"), ci.get("phone_display"),
ci.get("elm_mac"), ci.get("elm_bt_name"), ci.get("obd_protocol"),
vin, dtc_count, pid_count,
ci.get("duration_ms"), len(responses), ci.get("error_count", 0),
ci.get("retry_count", 0), ci.get("timeout_count", 0),
ci.get("script_mode"), ci.get("transport"), ci.get("mock_mode", 0),
ci.get("car_info", ""),
diagnosis, len(diagnosis), llm_model,
llm_duration_ms, 1 if llm_success else 0,
json.dumps(responses, ensure_ascii=False) if responses else None,
request_id if request_id else None,
resp_json_str,
))
self.conn.commit()
ci.get("duration_ms"), len(responses), ci.get("error_count", 0),
ci.get("retry_count", 0), ci.get("timeout_count", 0),
ci.get("script_mode"), ci.get("transport"), ci.get("mock_mode", 0),
ci.get("car_info", ""),
diagnosis, len(diagnosis), llm_model,
llm_duration_ms, 1 if llm_success else 0,
json.dumps(responses, ensure_ascii=False) if responses else None,
request_id if request_id else None,
resp_json_str,
))
self.conn.commit()
def get_recent_sessions(self, limit: int = 50) -> list[dict]:
"""Последние N сессий."""
@@ -244,27 +272,28 @@ class Database:
def save_dtc_scan(self, client_info: dict, dtc_codes: list[str]):
"""Сохраняет быстрый скан кодов ошибок."""
self.conn.execute("""
INSERT INTO sessions (
client_ip, real_ip, user_agent,
phone_model, phone_maker, android_version, android_sdk,
app_version, android_id, device_uuid,
elm_mac, elm_bt_name,
dtc_count, response_count,
script_mode, transport,
raw_responses
) VALUES (?,?,?, ?,?,?,?, ?,?,?, ?,?, ?,?, ?,?,?)
""", (
client_info.get("client_ip"), client_info.get("real_ip"), client_info.get("user_agent"),
client_info.get("phone_model"), client_info.get("phone_maker"), client_info.get("android_version"),
client_info.get("android_sdk"), client_info.get("app_version"), client_info.get("android_id"),
client_info.get("device_uuid"),
client_info.get("elm_mac"), client_info.get("elm_bt_name"),
len(dtc_codes), 0,
"dtc_scan", client_info.get("transport", "bt"),
json.dumps([{"decoded": f"DTC stored: {c}"} for c in dtc_codes], ensure_ascii=False)
))
self.conn.commit()
with self._lock:
self.conn.execute("""
INSERT INTO sessions (
client_ip, real_ip, user_agent,
phone_model, phone_maker, android_version, android_sdk,
app_version, android_id, device_uuid,
elm_mac, elm_bt_name,
dtc_count, response_count,
script_mode, transport,
raw_responses
) VALUES (?,?,?, ?,?,?,?, ?,?,?, ?,?, ?,?, ?,?,?)
""", (
client_info.get("client_ip"), client_info.get("real_ip"), client_info.get("user_agent"),
client_info.get("phone_model"), client_info.get("phone_maker"), client_info.get("android_version"),
client_info.get("android_sdk"), client_info.get("app_version"), client_info.get("android_id"),
client_info.get("device_uuid"),
client_info.get("elm_mac"), client_info.get("elm_bt_name"),
len(dtc_codes), 0,
"dtc_scan", client_info.get("transport", "bt"),
json.dumps([{"decoded": f"DTC stored: {c}"} for c in dtc_codes], ensure_ascii=False)
))
self.conn.commit()
# ── device_profiles ──────────────────────────────────
@@ -283,35 +312,39 @@ class Database:
def save_device_profile(self, mac: str, profile: dict):
"""Сохраняет или обновляет профиль устройства.
profile — результат obd.probe.probe().
profile — результат obd.probe.probe() + response_time_ms.
"""
now = datetime.now(timezone.utc).isoformat()
self.conn.execute("""
INSERT INTO device_profiles
(mac, level, elm_version, elm_desc, protocol, voltage,
supported, unsupported, errors, first_seen, last_seen)
VALUES (?,?,?,?,?,?, ?,?,?, ?,?)
ON CONFLICT(mac) DO UPDATE SET
level = excluded.level,
elm_version = excluded.elm_version,
elm_desc = excluded.elm_desc,
protocol = excluded.protocol,
voltage = excluded.voltage,
supported = excluded.supported,
unsupported = excluded.unsupported,
errors = excluded.errors,
last_seen = excluded.last_seen
""", (
mac,
profile.get("level", -1),
profile.get("elm_version"),
profile.get("elm_desc"),
profile.get("protocol"),
profile.get("voltage"),
json.dumps(profile.get("supported", []), ensure_ascii=False),
json.dumps(profile.get("unsupported", []), ensure_ascii=False),
json.dumps(profile.get("errors", []), ensure_ascii=False),
now, now,
))
self.conn.commit()
with self._lock:
now = datetime.now(timezone.utc).isoformat()
self.conn.execute("""
INSERT INTO device_profiles
(mac, level, elm_version, elm_desc, protocol, voltage,
response_time_ms, supported, unsupported, errors,
first_seen, last_seen)
VALUES (?,?,?,?,?,?, ?,?,?,?, ?,?)
ON CONFLICT(mac) DO UPDATE SET
level = excluded.level,
elm_version = excluded.elm_version,
elm_desc = excluded.elm_desc,
protocol = excluded.protocol,
voltage = excluded.voltage,
response_time_ms = excluded.response_time_ms,
supported = excluded.supported,
unsupported = excluded.unsupported,
errors = excluded.errors,
last_seen = excluded.last_seen
""", (
mac,
profile.get("level", -1),
profile.get("elm_version"),
profile.get("elm_desc"),
profile.get("protocol"),
profile.get("voltage"),
profile.get("response_time_ms", 250),
json.dumps(profile.get("supported", []), ensure_ascii=False),
json.dumps(profile.get("unsupported", []), ensure_ascii=False),
json.dumps(profile.get("errors", []), ensure_ascii=False),
now, now,
))
self.conn.commit()
+1 -4
View File
@@ -28,14 +28,11 @@ def register(app):
@app.route("/api/v1/ping-llm", methods=["GET"])
def ping_llm():
"""Проверка LLM с адаптивным кэшем. # API key check
"""Проверка LLM с адаптивным кэшем (успех=60с, ошибка=7с)."""
cfg = load()
required = cfg.get("api", {}).get("key", "")
if required and request.headers.get("X-Api-Key", "") != required:
return jsonify({"ok": False, "error": "unauthorized"}), 401
- Успех → кэш 60с
- Ошибка → кэш 7с (LLM мог уже ожить)
"""
global _ping_llm_cache
now = time.time()
if _ping_llm_cache:
+318
View File
@@ -0,0 +1,318 @@
"""
api/raw_elm.py — Сырое взаимодействие с ELM327 (локальное + удалённое через Android).
ЛОКАЛЬНЫЙ РЕЖИМ (ELM327 подключён к серверу напрямую):
POST /api/v1/elm/raw — отправить команду, получить сырой ответ
POST /api/v1/elm/raw/batch — несколько команд
POST /api/v1/elm/raw/drain — очистить буфер
GET /api/v1/elm/raw/available — байт в буфере
GET /api/v1/elm/raw/log — история команд
GET /api/v1/elm/raw/mode — режим (normal/raw)
УДАЛЁННЫЙ РЕЖИМ (Android-ретранслятор):
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: прочитать ответ
GET /api/v1/elm/raw/status — Copilot: статус устройства
"""
import logging
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 (прямое подключение к серверу)
def is_raw_mode() -> bool:
return _raw_mode
def set_raw_mode(on: bool):
global _raw_mode
_raw_mode = on
logger.info(f"RawELM mode: {'ON' if on else 'OFF'}")
def set_raw_elm(instance):
global _raw_elm
_raw_elm = instance
bp = Blueprint("raw_elm", __name__)
# ══════════════════════════════════════════════════════════
# ЛОКАЛЬНЫЙ РЕЖИМ — ELM327 подключён к серверу напрямую
# ══════════════════════════════════════════════════════════
@bp.route("/api/v1/elm/raw", methods=["POST"])
def raw_command():
if not _raw_elm:
return jsonify({"error": "no local ELM connection"}), 503
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
timeout = data.get("timeout_ms")
drain_first = data.get("drain_first", False)
if drain_first:
_raw_elm.drain()
result = _raw_elm.send(cmd, timeout=timeout)
return jsonify(result)
@bp.route("/api/v1/elm/raw/batch", methods=["POST"])
def raw_batch():
if not _raw_elm:
return jsonify({"error": "no local ELM connection"}), 503
data = request.get_json(silent=True)
if not data or "cmds" not in data:
return jsonify({"error": "missing 'cmds'"}), 400
cmds = data["cmds"]
if len(cmds) > 100:
return jsonify({"error": "too many commands (max 100)"}), 400
timeout = data.get("timeout_ms")
drain_between = data.get("drain_between", False)
t0 = time.time()
results = []
for cmd in cmds:
if drain_between:
_raw_elm.drain()
results.append(_raw_elm.send(cmd, timeout=timeout))
total_elapsed = int((time.time() - t0) * 1000)
return jsonify({"results": results, "total_elapsed_ms": total_elapsed})
@bp.route("/api/v1/elm/raw/drain", methods=["POST"])
def raw_drain():
if not _raw_elm:
return jsonify({"error": "no local ELM connection"}), 503
return jsonify(_raw_elm.drain())
@bp.route("/api/v1/elm/raw/available", methods=["GET"])
def raw_available():
if not _raw_elm:
return jsonify({"error": "no local ELM connection"}), 503
return jsonify({"available": _raw_elm.available()})
@bp.route("/api/v1/elm/raw/log", methods=["GET"])
def raw_log():
if not _raw_elm:
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)})
@bp.route("/api/v1/elm/raw/mode", methods=["GET", "POST"])
def raw_mode_control():
global _raw_mode
if request.method == "POST":
data = request.get_json(silent=True) or {}
on = data.get("raw_mode", False)
set_raw_mode(on)
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-ретранслятор (SQLite-очередь)
# ══════════════════════════════════════════════════════════
@bp.route("/api/v1/elm/raw/hello", methods=["POST"])
def raw_hello():
"""Android: «я подключился, готов принимать команды»."""
data = request.get_json(silent=True) or {}
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: поставить команду в очередь."""
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
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: забрать команду из очереди."""
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."""
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 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: прочитать последний ответ."""
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:
dl = time.time() + wait_s
while time.time() < dl:
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 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: статус устройства."""
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: история команд."""
n = int(request.args.get("n", 50))
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)})
+74 -1
View File
@@ -16,7 +16,7 @@ from flask import jsonify, request
from api.config import load
from api.db import Database
from api.parser import format_no_llm, parse_batch
from api.scripts import build_default_script, build_full_script, build_script_for_level, build_dynamic_script
from api.scripts import build_default_script, build_full_script, build_script_for_level, build_dynamic_script, build_test_script
from brain.client import Diagnoser, LLMError
from brain.prompts import SYSTEM_PROMPT, DYNAMIC_PROMPT
@@ -93,6 +93,12 @@ def register(app):
script = build_default_script()
elif mode == "dynamic":
script = build_dynamic_script()
elif mode == "test":
wait = int(request.args.get("wait", 2000))
pid_str = request.args.get("pids", "")
pids = pid_str.split(",") if pid_str else None
repeat = int(request.args.get("repeat", 8))
script = build_test_script(wait_ms=wait, pids=pids, repeat=repeat)
elif mode == "full":
script = build_full_script()
else:
@@ -241,6 +247,24 @@ def register(app):
return jsonify({"answer": answer})
@app.route("/api/v1/sessions", methods=["GET"])
def get_sessions():
"""История сессий для мобильного приложения."""
if not _check_api_key():
return _auth_error()
with Database() as db:
rows = db.conn.execute(
"SELECT id, vin, car_info, created_at, diagnosis_text as diagnosis FROM sessions ORDER BY id DESC LIMIT 50"
).fetchall()
return jsonify([{
"id": r["id"],
"title": (r["vin"] or r["car_info"] or "Диагностика"),
"created_at": r["created_at"],
"uploaded": 1,
"diagnosis": r["diagnosis"] or ""
} for r in rows])
@app.route("/api/v1/elm/probe", methods=["POST"])
def probe_elm():
"""Пробинг ELM327: определение уровня устройства.
@@ -298,6 +322,55 @@ def register(app):
return jsonify(p)
@app.route("/api/v1/elm/profile/<mac>", methods=["PUT"])
def update_elm_profile(mac: str):
"""Обновляет поля профиля (response_time_ms и т.д.). Если профиля нет — создаёт."""
data = request.get_json(silent=True) or {}
with Database() as db:
p = db.get_device_profile(mac)
if p is None:
p = {"mac": mac, "level": 0, "supported": [], "unsupported": [], "errors": []}
p.update(data)
_save_profile(mac, p)
return jsonify({"ok": True})
@app.route("/api/v1/test/next", methods=["POST"])
def test_next():
"""Авто-подбор параметров теста. Принимает результаты, возвращает следующий скрипт или done."""
data = request.get_json(silent=True) or {}
results = data.get("results", [])
run = data.get("run", 0)
# Анализ результатов
total = len(results)
if total == 0:
return jsonify({"done": True, "message": "Нет данных", "script": None})
ok_count = sum(1 for r in results if r.get("raw", "").startswith("41"))
err_pct = (total - ok_count) * 100 // total if total > 0 else 100
wait_ms = int(request.args.get("wait", data.get("wait_ms", 2000)))
max_runs = 6
if err_pct < 20 or run >= max_runs:
return jsonify({
"done": True,
"message": f"✅ Стабильно: {ok_count}/{total} ({err_pct}% ошибок) на wait={wait_ms}ms",
"script": None,
"final_wait_ms": wait_ms,
})
# Увеличиваем паузу
new_wait = wait_ms + 500
return jsonify({
"done": False,
"message": f"⚠️ {err_pct}% ошибок — увеличиваю паузу до {new_wait}ms",
"run": run + 1,
"wait_ms": new_wait,
"script": build_test_script(wait_ms=new_wait, pids=["010C", "0106"], repeat=6),
})
def _save_profile(mac: str, profile: dict):
"""Сохраняет профиль в БД (best-effort)."""
try:
+56 -15
View File
@@ -109,25 +109,66 @@ 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"},
],
}
def build_test_script(wait_ms: int = 1500, pids: list[str] | None = None, repeat: int = 8) -> dict:
"""Тестовый скрипт для отладки таймингов ELM327.
Сервер управляет таймингами — можно менять wait_ms/pids/repeat без передеплоя APK.
Args:
wait_ms: пауза ПЕРЕД каждой OBD-командой (ms)
pids: список PID для опроса (по умолчанию 010C,0110,0106)
repeat: сколько раз повторить цикл
"""
if pids is None:
pids = ["010C", "0106"] # RPM + STFT — минимум для проверки связи
pid_names = {"010C": "RPM", "0110": "MAF", "0106": "STFT", "0105": "ОЖ",
"0104": "Нагрузка", "0107": "LTFT", "0111": "Дроссель",
"010D": "Скорость", "010B": "MAP", "010F": "IAT"}
steps = []
# Статика — один проход по всем PID для калибровки
for pid in pids:
name = pid_names.get(pid, pid)
steps.append({"id": f"static_{pid}", "cmd": pid, "desc": f"{name} (статик)", "wait": 0})
# Динамика — repeat циклов
for cycle in range(repeat):
for pid in pids:
name = pid_names.get(pid, pid)
steps.append({"id": f"dyn{cycle}_{pid}", "cmd": pid, "desc": f"{name}", "wait": wait_ms})
return {
"version": 1,
"mode": "test",
"title": f"Тест: {len(pids)} PID × {repeat}, пауза {wait_ms}ms",
"wait_ms": wait_ms,
"repeat": repeat,
"steps": steps,
}
+1 -1
View File
@@ -2,7 +2,7 @@
# Значения вида ${VAR} подставляются из переменных окружения
llm:
api_key: "sk-78ec529c1eba4ba69995091046c9fa33"
api_key: "${LLM_API_KEY}"
model: "deepseek-v4-flash"
base_url: "https://api.deepseek.com/v1"
+72 -1
View File
@@ -1,7 +1,7 @@
# elmAI — Changelog / Полное описание проекта
> Файл для нового агента: прочитай — и ты в курсе всего.
> Актуально: v0.77.0-dev, 7 июня 2026
> Актуально: v0.95.0-dev, 10 июня 2026
---
@@ -132,6 +132,14 @@
### История версий (сервер)
#### v0.93.0-dev (10 июня 2026)
- **Speed-test ELM327:** при первом подключении нового ELM — замер скорости ответа на 3 PID (RPM, MAF, STFT) × 3 раза каждый
- **Адаптивный интервал:** динамический тест использует `max(250, avg_response × 3 × 1.5)` вместо жёстких 250ms
- **Профиль устройства:** колонка `response_time_ms` в `device_profiles`, API `PUT /api/v1/elm/profile/<mac>`
- **Fix:** `threading.Lock()` в `Database` — 0 ошибок при 20 конкурентных записях (было 8/20)
- **UI:** прогресс speed-теста показывается пользователю
- Деплой v0.93.0-dev на obdai.ru
#### v0.48.0 (7 июня 2026)
- **Пробинг ELM327:** трехуровневый каскад (L0/L1/L2)
- **Рефакторинг `obd/`:** разделение на независимые сервисы
@@ -265,3 +273,66 @@ scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/app
| `fat-client` | Старая fat-client архитектура (устарела) |
| `androbd-proto` | Прототип AndrOBD стейт-машины (устарела) |
| `elm-layer-v2` | Старый ELM-слой (устарела) |
---
## 9. Динамический тест — START/STOP (v0.93+, 10.06.2026)
### Цель
Выявить **потерю мощности, подсос воздуха, забитый фильтр, проблемы смеси** — ловля STFT/LTFT на сбросе газа.
### 5 быстрых PID (планировалось)
1. **RPM** (010C)
2. **MAF** (0110)
3. **STFT** (0106)
4. **LTFT** (0107)
5. **TPS** (0111)
→ После тестов #43-#45 выяснилось: ELM327 v1.5 не успевает 5 PID за 250ms (данные склеиваются).
→ После тестов #47-#48 (v0.93.0-dev): даже 3 PID × 250ms — 94% ошибок, ELM перестаёт отвечать после ~15 сэмплов.
→ **Решение (текущее, v0.93.0): Speed-test при первом подключении ELM + адаптивный интервал.**
### Текущая стратегия (v0.93+)
#### Этап 0 — Speed-test (только при первом подключении нового ELM)
- После инициализации ELM: замерить время ответа на 010C, 0110, 0106 — каждый 3 раза
- Показать пользователю: `"⏱ Тест скорости: RPM 82ms MAF 91ms STFT 82ms"`
- Сохранить `response_time_ms` в профиль устройства (по BT MAC)
- Интервал = `max(250, avg_response × 3 × 1.5)`
- При повторных запусках — использовать сохранённое значение
#### Этап 1 — Статика (перед СТАРТ)
- Снять все доступные PID по одному разу
- Определить какие PID отвечают, какие нет (7F 01 12)
- **Запомнить** неподдерживаемые — больше не опрашивать
- Время: ~3-4 секунды
#### Этап 2 — Динамика (250ms)
- **3 PID**: RPM (010C), MAF (0110), STFT (0106)
- LTFT, TPS, MAP, Load, coolant, IAT — один раз в статике
#### Этап 3 — Контроль качества
- После СТОП проверить количество сэмплов и % ошибок
- Если < 6-8 сэмплов или > 30% errors — сообщить водителю:
> «Слишком быстро. Нажмите СТАРТ, плавно наберите ~3000 об/мин, **сбросьте газ, подождите 3-4 секунды**, нажмите СТОП.»
### Процедура для водителя
1. Дождаться ДИАГНОСТИКА → зелёный
2. Нажать СТАРТ
3. Плавно газ до ~3000 об/мин
4. **Резко сбросить газ**
5. **Подождать 3-4 секунды** (без нажатий) — ЭБУ корректирует смесь
6. СТОП
7. ➤ (Send) — отправка на сервер
### Зачем ждать 3-4 секунды после сброса
- MAF падает → STFT резко уходит в минус/плюс
- ЭБУ пытается стабилизировать смесь
- LTFT начинает подстраиваться
- Именно эти 3-4 секунды — самое ценное для анализа
### Планы
- Скорость — потом через GPS (не через OBD)
- ~~Адаптивный интервал если ELM быстрее (v2.x)~~ ✅ Сделано в v0.93.0
- Логирование в историю каждого теста
View File
+181
View File
@@ -0,0 +1,181 @@
# Структура репозитория elmAI
> **elmAI** — сервис OBD2-диагностики автомобилей через ELM327 + LLM (DeepSeek).
> Android-приложение + Python-сервер. Анализ ошибок ЭБУ, live-параметры, диагноз через ИИ.
---
## Корневые файлы
| Файл | Назначение |
|------|-----------|
| `run.py` | Главная точка входа (CLI). Подключается к ELM327 по Bluetooth, читает VIN/DTC/PID, сохраняет в SQLite, отправляет в LLM. Запуск: `python run.py [--no-llm] [--port]` |
| `config.yaml` | Конфигурация: LLM (API key, модель), ELM327 (порт, baudrate), список PID для чтения |
| `requirements.txt` | Зависимости Python: pyserial, pyyaml, requests, flask, flask-cors |
| `deploy.sh` | Скрипт деплоя на сервер obdai.ru: обновление репо, venv, systemd-сервис (gunicorn), nginx, SSL (certbot) |
| `legacy-deploy.sh` | Устаревшая версия деплоя (ветка fat-client, GPT-OSS модель) |
| `analysis.md` | Анализ и план проекта от 2025-05-25: железо, ЦА, требования, компоненты, протокол, риски |
| `idea.md` | Концепция сервиса: OBD2 + AI диагностика, три компонента (сервер, Android, десктоп) |
| `morda.md` | Макет UI (морда) v2: иконки-светофоры, кнопка-трансформер, поле вывода, поле ввода |
| `QUICKSTART.md` | Быстрый старт: тест с mock ELM327, тест в машине, веб-интерфейс |
| `resume.txt` | Резюме проекта для нового чата: версия v0.77.0-dev, инструкции по деплою |
| `legacy-resume.txt` | Устаревшее резюме (v0.48.0, ветка master) |
| `CHANGELOG.md` | Полное описание проекта: архитектура, модули, эндпоинты, БД, стейт-машина (актуально v0.95.0-dev) |
| `token.txt` | Токены и ключи: gitea, DeepSeek API, SSH-ключ VM |
| `STRUCTURE.md` | **Этот файл** — описание структуры репозитория |
---
## `api/` — Flask REST API + БД + парсинг
| Файл | Назначение |
|------|-----------|
| `__init__.py` | Пустой (пакет) |
| `config.py` | Загрузка `config.yaml` с подстановкой `${VAR}` из переменных окружения. Кэш через `@lru_cache` |
| `db.py` | SQLite-база данных (WAL mode). Таблицы: `sessions`, `cars`, `diagnostic_tokens`, `llm_messages`, `ecu_parameters`, `dtc_codes`, `command_queue`. Класс `Database` |
| `routes.py` | Основные эндпоинты: `GET /api/v1/script`, `POST /api/v1/session/upload`, `POST /api/v1/chat`, `POST /api/v1/elm/probe`. Проверка X-Api-Key, сборка промпта для LLM |
| `dtc.py` | DTC-эндпоинты: `POST /api/v1/dtc/decode` (расшифровка кодов из справочника), `POST /api/v1/dtc/upload`. Справочник из `doc/dtc_codes.txt` |
| `ping.py` | Эндпоинты проверки: `GET /api/v1/ping` (доступность), `GET /api/v1/ping-llm` (проверка LLM с адаптивным кэшем 60с/7с) |
| `parser.py` | Парсинг батча ELM-ответов: VIN (из decoded и raw HEX), DTC stored/pending (mode 03/07), PID-параметры (mode 01) |
| `scripts.py` | Сборка диагностических скриптов трёх уровней: L0 (5 PID + stored DTC), L1 (8 PID + VIN + stored/pending), L2 (14 PID + калибровки). + динамические скрипты |
| `raw_elm.py` | Сырое взаимодействие с ELM327: локальный режим (прямое подключение) и удалённый (через Android-реле). HTTP-очередь команд |
---
## `brain/` — LLM-клиент и промпты
| Файл | Назначение |
|------|-----------|
| `__init__.py` | Пустой (пакет) |
| `client.py` | `Diagnoser` — HTTP-клиент к OpenAI-совместимому API (api.aillm.ru). Модели: `gpt-oss-120b`, `qwen3-6-27b-fp8`. Обработка ошибок: Timeout, 429, 5xx, 4xx |
| `prompts.py` | `SYSTEM_PROMPT` (10 правил для диагноза: расшифровка, отклонения, степени уверенности), `DYNAMIC_PROMPT` (для динамических тестов), `build_user_prompt()` |
---
## `obd/` — ELM327-протокол (Python, порт AndrOBD)
| Файл | Назначение |
|------|-----------|
| `__init__.py` | Пустой (пакет) |
| `connection.py` | `SerialTransport` — транспортный слой: открыть serial/Bluetooth порт, побайтовое чтение до `>`, запись + flush |
| `protocol.py` | `AndrOBD` — стейт-машина ELM327 (порт ElmProt.java). Состояния: UNDEFINED → INITIALIZING → READY → BUSY → ERROR. Канонический init, обработка BUS ERROR |
| `state.py` | `State` (enum состояний) и `Rsp` (классификация ответов: PROMPT, OK, SEARCHING, ERROR, BUS_ERROR, NODATA и т.д.) |
| `timing.py` | `AdaptiveTiming` — адаптивный таймаут (50..2000мс). Увеличивается при таймаутах, уменьшается при быстрых ответах, сброс при BUS ERROR |
| `commands.py` | Каталог AT-команд ELM327 с метаданными: name, desc, level (0/1/2), safe. L0 (универсальные), L1 (ATAT), L2 (CAF/CFC) |
| `classifier.py` | Классификация сырых ответов ELM327 и определение уровня устройства по ответам на пробинг |
| `probe.py` | Пробинг ELM327: трехуровневый каскад (L0→L1→L2), каждая команда с таймаутом 500мс, без ретраев |
| `raw_console.py` | `RawELM` — сырой слой без стейт-машины: только send/read/drain/available. Для изучения поведения ELM327 |
---
## `web/` — Веб-интерфейс (Flask)
| Файл | Назначение |
|------|-----------|
| `app.py` | Точка входа Flask: регистрация эндпоинтов, режим RAW (блокировка всех, кроме `/elm/raw/*`), раздача APK, главная страница |
| `script_builder.py` | Сборка диагностических скриптов (устаревшая версия — дублирует `api/scripts.py`) |
| `script_endpoint.py` | Эндпоинты скриптов (устаревшая версия — дублирует `api/routes.py`) |
| `script_parser.py` | Парсинг батча (устаревшая версия — дублирует `api/parser.py`) |
| `templates/index.html` | Главная HTML-страница: скачивание APK, десктоп-диагностика, отображение результатов |
| `static/style.css` | Стили: тёмная тема, оранжевый акцент, карточки, спиннеры, DTC-бейджи |
---
## `android/` — Android-приложение (Kotlin)
| Файл | Назначение |
|------|-----------|
| `build.gradle.kts` | Корневой build-файл Gradle: плагины Android + Kotlin |
| `settings.gradle.kts` | Настройки Gradle-проекта |
| `gradle.properties` | Свойства Gradle |
| `gradlew` | Gradle Wrapper (исполняемый) |
| `app/build.gradle.kts` | Модуль app: minSdk 24, OkHttp 4.12.0, зависимости |
| `app/src/` | Исходники Android-приложения (Kotlin) — основной клиент + raw-реле |
| `raw/build.gradle.kts` | Модуль raw — ретранслятор ELM327 через HTTP |
| `doc/opus-review-android.md` | Рецензия кода Android-приложения |
| `doc/opus-questions-android.md` | Вопросы по Android после рецензии |
| `gradle/wrapper/` | Gradle Wrapper JAR и настройки |
---
## `tools/` — Вспомогательные утилиты
| Файл | Назначение |
|------|-----------|
| `mock_elm327.py` | Эмулятор ELM327 v1.5 через TCP (порт 35000). Отвечает на AT-команды, PID, DTC, VIN. Для тестирования без реального сканера |
| `mock_elm327_v2.py` | Улучшенный мок: поддержка `>` как разделителя, ATST, случайные ошибки (BUS BUSY, UNABLE), побайтовая отправка |
| `elm_console.py` | Интерактивная консоль ELM327 (сырой режим). Команды: ATZ, 0105, !drain, !timeout, !log. Для изучения поведения ELM |
| `elm_relay.py` | Интерактивная консоль удалённого управления ELM327 через Android-реле. HTTP-команды: `!status`, `!history`, `!mode` |
| `test_androbd.py` | Тест AndrOBD-протокола против Mock ELM327 v2: проверка что ответы не перемешаны (VIN → DTC → RPM → coolant) |
| `analyze_sessions.py` | Анализ сессий из SQLite: статистика команд, ошибок, пустых ответов |
---
## `scripts/` — Скрипты развёртывания
| Файл | Назначение |
|------|-----------|
| `setup-bt.sh` | Настройка Bluetooth-сопряжения с ELM327: поиск, pairing, rfcomm bind на /dev/rfcomm0 |
---
## `tests/` — Автотесты
| Файл | Назначение |
|------|-----------|
| `test_all.py` | Сквозные тесты (без LLM): сборка скриптов, парсер ELM-ответов (VIN из decoded/raw, DTC, PID), работа с БД, идемпотентность, эндпоинты |
---
## `doc/` — Документация и исследования
| Файл | Назначение |
|------|-----------|
| `architecture.md` | Полная архитектура проекта: два режима (app/raw), схема, эндпоинты, модули |
| `roadmap.md` | План развития проекта |
| `research.md` | Исследования и заметки |
| `competitors.md` | Анализ конкурентов |
| `diagnostic-logic.md` | Логика диагностики |
| `dynamic-diagnostics-analysis-2026-06-14.md` | Анализ динамической диагностики |
| `dynamic-tests.md` | Динамические тесты |
| `dtc_codes.txt` | Справочник DTC-кодов (формат: `P0301=Пропуски зажигания цилиндр 1`) |
| `elm-reference.md` | Справочник по ELM327 |
| `elm-raw-relay-plan.md` | План raw-реле |
| `failures-journal.md` | Журнал отказов |
| `field-test-2026-06-07.md` | Полевой тест |
| `git-guide.md` | Гайд по Git |
| `morda-v2.md` | Макет UI v2 |
| `mpscholar-automotive-sensing-actuators.md` | Обучающий материал |
| `opinion-dynamic-diagnostics-2026-06-14.md` | Мнение по динамической диагностике |
| `relay-mistakes-2026-07-04.md` | Ошибки реле |
| `test-cases.md` | Тест-кейсы |
| `SETUP.md` | Инструкция по установке |
| `audit-2026-06-07.md` | Аудит проекта |
| `audit-prompt.md` | Промпт для аудита |
| `opus-review.md` | Рецензия кода (Opus) |
| `opus-fix-plan.md` | План исправлений по рецензии |
| `opus-questions.md` | Вопросы к Opus |
| `opus-questions-post-tests-2026-06-29.md` | Вопросы после тестов |
| `opus-recheck-request-2026-06-28.md` | Запрос на перепроверку |
| `opus-review-android.md` | Рецензия Android-кода |
| `sonnet-apk-cache-questions-2026-06-28.md` | Вопросы по кэшу APK |
| `android-bugs-2026-05-25.md` | Баги Android |
| `claude-analysis-elm.md` / `claude-analysis-elm-v2.md` | Анализ ELM от Claude |
| `claude-request-elm.md` / `claude-request-elm-v2.md` | Запросы к Claude по ELM |
| `session-*.md` | Логи сессий разработки по датам |
| `session-resume-2026-06-14.md` | Резюме сессии |
| `history/` | Архив старых заметок, логов сессий и результатов тестов по датам |
| `history/2026-05-31.md` | Лог сессии 31 мая |
| `history/2026-06-03.md` | Лог сессии 3 июня |
| `history/2026-06-05.md` | Лог сессии 5 июня |
| `history/2026-06-06.md` | Лог сессии 6 июня |
| `history/2026-06-07.md` | Лог сессии 7 июня |
| `history/2026-06-07-plans.md` | Планы на 7 июня |
| `history/2026-06-10.md` | Лог сессии 10 июня |
| `history/opus-recheck-analysis-2026-06-28.md` | Анализ перепроверки Opus |
| `history/opus-sonnet-comparison-2026-06-29.md` | Сравнение Opus vs Sonnet |
| `history/session-summary-2026-06-28.md` | Сводка сессии 28 июня |
| `history/sonnet-response-post-tests-2026-06-29.md` | Ответ Sonnet после тестов |
| `history/test-results-2026-06-28.md` | Результаты тестов 28 июня |
| `history/test-results-2026-07-04.md` | Результаты тестов 4 июля |
+156 -89
View File
@@ -1,144 +1,211 @@
# Архитектура elmAI
> v0.77.0-dev, 7 июня 2026
> v0.42.0-dev (основной APK) / v0.4.0-dev (raw-реле), 10 июля 2026
## Общая схема
```
📱 Android (elmer-android)
Bluetooth
🔌 ELM327
│ OBD-ответы
📱 Android (ScriptRunnerService)
│ HTTPS POST /api/v1/session/upload
├── Bluetooth ──── 🔌 ELM327 ──── ECU (OBD-II)
├── app/ (основное приложение)
│ └── HTTPS POST /api/v1/session/upload
└── raw/ (реле)
└── HTTP-поллинг /api/v1/elm/raw/*
🌐 Сервер (5.172.178.213)
├── nginx :443 → gunicorn :8000
├── obd/ — ELM327 протокол
├── brain/ — LLM-клиент
├── api/ — REST, БД, скрипты
── web/ — точка входа Flask, статика
├── api/raw_elm.py — командная очередь для реле (SQLite)
├── api/ — REST, БД, скрипты
├── brain/ — LLM-клиент
── obd/ — ELM327 протокол (Python)
└── web/ — точка входа Flask, статика
```
## Два режима работы
### Режим 1: Основное приложение (`app/`)
Прямая диагностика: телефон → ELM → скрипт → батч → сервер → LLM
### Режим 2: Raw-реле (`raw/`)
Тупой ретранслятор: сервер диктует команды, телефон передаёт в ELM и возвращает ответы.
Используется для интерактивной диагностики и тестирования.
```
Copilot/сервер Android (raw) ELM327
│ │ │
├─ POST /cmd ──────────→│ │
│ ├─ sendCommand() ─────→│
│ │←─ raw response ──────┤
│←─ POST /response ─────┤ │
│ │ │
├─ GET /response?wait=N─→ (поллинг ответа) │
│←─ {raw: "41 0C ..."}─┤ │
```
## Структура сервера
```
elmer/
├── obd/ # Модуль 1: ELM327 протокол
── protocol.py # AndrOBD — стейт-машина (1:1 копия AndrOBD)
# State, Rsp, AdaptiveTiming
├── api/
── config.py # Загрузка config.yaml
├── db.py # SQLite (sessions, cars, dtc, command_queue)
│ ├── routes.py # Эндпоинты: script, upload, chat, probe
│ ├── dtc.py # Эндпоинты DTC
│ ├── ping.py # Эндпоинты проверки
│ ├── scripts.py # Сборка диагностических скриптов
│ ├── parser.py # Парсинг ответов ELM327
│ └── raw_elm.py # Командная очередь для реле (SQLite command_queue)
├── brain/ # Модуль 2: LLM-взаимодействие
│ ├── client.py # Diagnoser — HTTP к api.aillm.ru
│ └── prompts.py # SYSTEM_PROMPT для диагностики
├── brain/
│ ├── client.py # Diagnoser — HTTP к LLM
│ └── prompts.py # SYSTEM_PROMPT для диагностики
├── api/ # Модуль 3: REST API + БД
── config.py # Загрузка config.yaml
│ ├── db.py # SQLite (sessions, cars, dtc)
│ ├── routes.py # Все эндпоинты (5 шт)
│ ├── scripts.py # Сборка диагностических скриптов
│ └── parser.py # Парсинг ответов ELM327
├── obd/
── protocol.py # Python-версия AndrOBD стейт-машины
├── web/ # Веб-интерфейс
│ ├── app.py # Точка входа Flask
│ ├── templates/index.html
│ └── static/app-debug.apk
├── tools/ # Разработка
├── mock_elm327_v2.py # Мок ELM327 (TCP)
│ └── test_androbd.py # Тесты стейт-машины
├── web/
│ ├── app.py # Точка входа Flask
│ ├── templates/
│ └── index.html # Страница загрузки APK
└── static/
│ ├── app-debug.apk # Основной APK
└── elm-raw-v022.apk # Raw-реле APK
├── doc/ # Документация
│ ├── architecture.md # Этот файл
│ ├── roadmap.md
│ └── session-*.md # Логи сессий
├── config.yaml # LLM API key, порты
└── requirements.txt
```
## Взаимодействие модулей
```
web/app.py
└─ import api/routes.py
├─ import api/config.py → config.yaml
├─ import api/db.py → SQLite
├─ import api/scripts.py → сборка скриптов
├─ import api/parser.py → парсинг батча
├─ import brain/client.py → Diagnoser → api.aillm.ru
└─ import brain/prompts.py → SYSTEM_PROMPT
```
Каждый модуль можно тестировать отдельно. Циклических зависимостей нет.
## API эндпоинты
| Метод | Путь | Описание | Время |
|---|---|---|---|
| GET | /api/v1/ping | Проверка сервера | ~5мс |
| GET | /api/v1/ping-llm | Проверка LLM | ~2с |
| GET | /api/v1/script?mode= | Скрипт диагностики | ~50мс |
| POST | /api/v1/session/upload | Загрузка батча + LLM | ~30-120с |
| POST | /api/v1/chat | Вопрос к LLM | ~5-15с |
### Основные (app/)
## Android (отдельный репо)
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/v1/ping | Проверка сервера |
| GET | /api/v1/ping-llm | Проверка LLM |
| GET | /api/v1/script?mode= | Скрипт диагностики |
| POST | /api/v1/session/upload | Загрузка батча + LLM |
| POST | /api/v1/chat | Вопрос к LLM |
### Raw-реле (raw/)
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/v1/elm/raw/hello | Android: «я готов» |
| POST | /api/v1/elm/raw/cmd | Copilot: поставить команду в очередь |
| GET | /api/v1/elm/raw/cmd | Android: забрать команду |
| POST | /api/v1/elm/raw/response | Android: отправить ответ |
| GET | /api/v1/elm/raw/response | Copilot: прочитать ответ (с device_id) |
| GET | /api/v1/elm/raw/status | Статус устройства |
## SQLite: command_queue
Таблица для очереди команд raw-реле:
```sql
CREATE TABLE command_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
seq INTEGER NOT NULL,
cmd TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending | done
raw_response TEXT,
elapsed_ms INTEGER,
prompt INTEGER DEFAULT 0,
error TEXT,
created_at TEXT DEFAULT (datetime('now')),
responded_at TEXT
);
```
- `hello` чистит старые команды для device_id
- `cmd` (POST) добавляет команду со статусом `pending`
- `cmd` (GET) атомарно забирает pending → обновляет статус
- `response` (POST) сохраняет ответ
- `response` (GET) возвращает ответы с фильтром по device_id и seq
## Android (отдельный репо: elmer-android)
```
elmer-android/app/src/main/java/ru/elmer/client/
├── ElmProtocol.kt # ELM327 стейт-машина
├── ObdDecoder.kt # Декодер PID/DTC/VIN
├── ServerClient.kt # HTTP к серверу (retry 3x)
├── ScriptEngine.kt # Движок скриптов
├── ScriptRunnerService.kt # Фоновая диагностика
├── SessionDb.kt # Локальная история
├── MainActivity.kt # UI
├── TestService.kt # (устарел)
└── ElmForwardService.kt # (устарел)
android/
├── app/ — основное приложение (диагностика)
│ └── src/.../ru/elmer/client/
│ ├── ElmProtocol.kt # ELM327 стейт-машина (AndrOBD)
│ ├── ObdDecoder.kt # Декодер PID/DTC/VIN
├── ServerClient.kt # HTTP к серверу
│ ├── ScriptEngine.kt # Движок скриптов
│ ├── ScriptRunnerService.kt # Фоновая диагностика
│ ├── SessionDb.kt # Локальная история
│ └── MainActivity.kt # UI
└── raw/ — реле (ретранслятор команд)
└── src/.../ru/elmer/raw/
├── ElmProtocol.kt # 1:1 копия app/ElmProtocol.kt
├── ElmActor.kt # Single-thread executor
├── RawRelayService.kt # Foreground-сервис: BT + поллинг
├── RelayClient.kt # HTTP-клиент к /api/v1/elm/raw/*
└── MainActivity.kt # Минимальный UI (выбор BT, статус)
```
## RawRelayService — главный цикл
```
relayLoop():
1. Bluetooth connect
2. ElmProtocol.init() # AndrOBD: ATSP0→ATAT1→ATST→ATS0→ATL0→ATE0
3. client.hello() # HTTP → /api/v1/elm/raw/hello
4. while running:
cmd = client.pollCommand() # GET /api/v1/elm/raw/cmd
raw = actor.sendBlocking(cmd, 5000)
client.postResponse(seq, cmd, raw)
```
## CI/CD и деплой
**Никаких сторонних CI/CD-сервисов (GitHub Actions, GitLab CI, Jenkins и т.д.). Всё вручную.**
### Сервер (elmer)
```
Локально: git push gitea master
Сервер: ssh obdai.ru
cd /opt/elmer && git pull origin master
sudo systemctl restart elmer
Сервер: ssh obdai.ru
cd /opt/elmer && git pull origin master
sudo systemctl restart elmer
```
- Репо: `gitea.services.ngcloud.ru/Nail/elmer`
- Ветка: `master`
- Сервис: `gunicorn -w 4 -b 127.0.0.1:8000 web.app:app`
- Прокси: nginx :443 → 127.0.0.1:8000
- Конфиг: `/opt/elmer/config.yaml`
### Android APK (elmer-android)
### Android APK
**Два репо:** `elmer/` (gitea) и `elmer/android/` (github)
```
Локально: cd android && ./gradlew assembleDebug
scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/
# Основной APK
cd android && ./gradlew :app:assembleDebug
scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/
# Raw-реле APK
cd android && ./gradlew :raw:assembleDebug
scp raw/build/outputs/apk/debug/raw-debug.apk obdai.ru:/opt/elmer/web/static/elm-raw-v022.apk
```
- Репо: `github.com/Repinoid/elmer-android`
- Сборка: `./gradlew assembleDebug`
- Доставка: `scp` на сервер в `web/static/app-debug.apk`
- Ссылка для пользователей: `https://obdai.ru/elmer.apk`
- APK обновляется **только** при изменениях в Android-коде
- Репо Android: `github.com/Repinoid/elmer-android`
- Ветка: `opus-fixes`
- APK на сервере: `/opt/elmer/web/static/`
### Версионирование
| Где | Файл |
|-----|------|
| APK | `android/app/build.gradle.kts``versionName` |
| Сайт | `web/templates/index.html` |
| Документация | заголовки `.md` файлов |
| Основной APK | `android/app/build.gradle.kts``versionName` |
| Raw APK | `android/raw/build.gradle.kts``versionName` |
| Сайт (основной) | `/opt/elmer/templates/index.html` |
| Сайт (raw) | `/opt/elmer/templates/index.html` (та же строка) |
**Версию менять одновременно во всех трёх местах.**
**Важно:** Flask использует `/opt/elmer/templates/index.html`. Файл `/opt/elmer/web/templates/index.html` — резервная копия.
-65
View File
@@ -1,65 +0,0 @@
# Отчёт об аудите (07.06.2026)
## Найдено
### 🔴 Критичные
1. **Краш при отсутствии Bluetooth**
- **Файл:** [android/app/src/main/java/ru/elmer/client/ui/MainActivity.kt](android/app/src/main/java/ru/elmer/client/ui/MainActivity.kt)
- **Строки:** 155, 230, 310
- **Описание:** Используется оператор `!!` для `btAdapter`. На устройствах без Bluetooth (или в эмуляторе) приложение упадёт при попытке проверить прибор или запустить сканирование.
- **Как исправить:** Добавить проверку `if (btAdapter == null)` перед использованием и выводить сообщение об ошибке.
2. **Отсутствие аутентификации на сервере**
- **Файл:** [api/routes.py](api/routes.py)
- **Описание:** Эндпоинты `/api/v1/session/upload`, `/api/v1/chat` и `/api/v1/ping-llm` принимают запросы без проверки API-ключа. Клиент передаёт `X-Api-Key`, но сервер его игнорирует. Любой может отправлять запросы и тратить токены LLM.
- **Как исправить:** Добавить декоратор `@api_key_required` или проверку заголовка в `before_request`.
3. **Состязание потоков (Race Condition) в ScriptRunnerService**
- **Файл:** [android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt](android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt)
- **Описание:** `onStartCommand` не проверяет, запущен ли уже процесс. Если дважды вызвать `startForegroundService` (нажав кнопку несколько раз), создадутся два конкурирующих потока `ScriptRunner`, которые будут одновременно работать с одним и тем же Bluetooth-сокетом.
- **Как исправить:** В `startRun` проверять флаг `running` и игнорировать повторные запуски.
4. **Логическая ошибка в выборе Bluetooth-устройства**
- **Файл:** [android/app/src/main/java/ru/elmer/client/ui/MainActivity.kt](android/app/src/main/java/ru/elmer/client/ui/MainActivity.kt#L457)
- **Описание:** Функция `findElmDevice()` при наличии двух и более устройств показывает диалог, но возвращает `null` немедленно. Стейт-машина (`checkElm`, `scanDtc`) видит `null` и прерывает работу с ошибкой «ELM не найден».
- **Как исправить:** Перестроить логику: диалог выбора должен вызываться отдельно, сохранять `elmDevice`, и только потом запускать операции.
### 🟡 Средние
5. **Утечка памяти в ElmChecker**
- **Файл:** [android/app/src/main/java/ru/elmer/client/elm/ElmChecker.kt](android/app/src/main/java/ru/elmer/client/elm/ElmChecker.kt)
- **Описание:** Список `logLines` является членом класса. В `MainActivity` экземпляр `elmChecker` переиспользуется. Приложение копит логи всех операций в памяти до своей гибели.
- **Как исправить:** Очищать `logLines` в начале каждой операции или переносить лог в локальную переменную метода `run()`.
6. **Утечка курсоров в БД**
- **Файл:** [android/app/src/main/java/ru/elmer/client/db/SessionDb.kt](android/app/src/main/java/ru/elmer/client/db/SessionDb.kt)
- **Описание:** Методы `getResponses`, `getSessions`, `getPendingSessions` вызывают `cursor.close()` в конце цикла, но не в `finally`. При ошибке чтения курсор останется открытым.
- **Как исправить:** Использовать конструкцию `.use { ... }` (в Kotlin для `Cursor` доступно начиная с определенных версий) или `try { ... } finally { cursor.close() }`.
7. **Не включены Foreign Keys**
- **Файл:** [android/app/src/main/java/ru/elmer/client/db/SessionDb.kt](android/app/src/main/java/ru/elmer/client/db/SessionDb.kt)
- **Описание:** Несмотря на наличие `REFERENCES sessions(id)`, SQLite в Android по умолчанию не проверяет целостность связей.
- **Как исправить:** Добавить `db.setForeignKeyConstraintsEnabled(true)` в `onConfigure`.
8. **Слепой выбор устройства в сервисе**
- **Файл:** [android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt](android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt#L143)
- **Описание:** Сервис берёт `bonded[0]` — первое попавшееся сопряжённое устройство. Это могут быть наушники или магнитола.
- **Как исправить:** Передавать MAC-адрес выбранного ELM через `Intent`.
9. **Отсутствие лимита на размер запроса**
- **Файл:** [api/routes.py](api/routes.py)
- **Описание:** Сервер принимает список `responses` любого размера. Злоумышленник может отправить миллион строк, вызвав OOM или переполнение диска логами.
- **Как исправить:** Проверять `len(responses)` перед обработкой.
### 🟢 Косметика
10. **Неиспользуемые переменные**
- В `ElmProtocol.kt` константа `INIT_TIMEOUT` и другие не используются.
- В `MainActivity.kt` список `chatHistory` хранится в памяти, но не восстанавливается после `onSaveInstanceState`.
11. **Устаревший API**
- `BluetoothAdapter.getDefaultAdapter()` помечен как Deprecated. В современных Android рекомендуется использовать `BluetoothManager`.
12. **Бесполезный пинг LLM**
- Эндпоинт `/api/v1/ping-llm` делает реальный `diagnose`, что стоит денег (токенов). Кэш есть, но при перезапуске сервера или по таймауту он всё равно будет жечь токены на "пустые" проверки.
+85
View File
@@ -0,0 +1,85 @@
# Логика диагностики — два режима
## Архитектура
Три слоя:
1. **Relay (Android)** — тупой ретранслятор: получил команду → отправил в ELM → вернул ответ
2. **Script Engine (сервер)** — отправляет команды по расписанию, собирает ответы
3. **LLM (сервер)** — анализирует собранные данные
---
## Режим 1: Прогрев на месте (3 минуты)
**Цель**: снять показания на холостых, погазовать, оценить прогрев
**Скрипт**:
```
Каждые 2 секунды — 3 PID: 010C (RPM), 0105 (темп), 0111 (дроссель)
Итого: 90 циклов × 3 PID = 270 запросов
При 70% успехе: ~190 ответов (~63 на каждый PID)
```
**Метрики**:
- RPM: мин/макс/среднее на холостых, отклик на газ
- Температура: скорость прогрева (°C/мин), выход на рабочую
- Дроссель: положение на холостых, реакция на педаль
---
## Режим 2: В движении (5 минут)
**Цель**: нагрузочный тест, динамика разгона, поведение под нагрузкой
**Скрипт**:
```
Каждые 2 секунды — 3 PID: 010C (RPM), 0105 (темп), 0111 (дроссель)
Итого: 150 циклов × 3 PID = 450 запросов
При 70% успехе: ~315 ответов (~105 на каждый PID)
```
**Метрики**:
- RPM: разгон/торможение, переключение передач
- Температура: стабильность под нагрузкой
- Дроссель: соответствие нагрузке
---
## Обработка ошибок
| Ответ | Действие |
|-------|----------|
| `41xx...` (hex данные) | Сохранить, декодировать |
| `STOPPED` | Пропустить, relay сам восстановит |
| Пусто / таймаут | Пропустить, следующий запрос |
| 3 таймаута подряд | Пауза 2с (клон перегрелся) |
---
## Выходные данные для LLM
```json
{
"mode": "warmup",
"duration_s": 180,
"total_requests": 270,
"successful": 190,
"pids": {
"010C": {
"min": 680, "max": 3200, "avg": 1250,
"samples": [680, 720, 750, ...]
},
"0105": {
"start_temp": 25, "end_temp": 87,
"warmup_rate": 0.34,
"samples": [25, 27, 30, ...]
},
"0111": {
"min": 0, "max": 45,
"samples": [0, 0, 1.5, ...]
}
}
}
```
LLM получает: сырые данные + вопрос пользователя → ответ с диагнозом.
+192
View File
@@ -0,0 +1,192 @@
# ELM327 Relay — полный журнал неудач
## Дата: 2026-07-0405 | Версия: v0.4.0-dev
---
## Итоговое состояние
| Параметр | Значение |
|----------|----------|
| Версия на сервере | v0.4.0-dev (commit `ebc41e0`) |
| Основа | 1:1 с `app/.../ElmProtocol.kt` (AndrOBD) |
| Клон 1 (v1.5) | 12/12 → 13/16 (72-81%) |
| Клон 2 (v1.5) | 13/18 (72%) |
| Характер отказов | Деградация после 10-12 команд |
| Двигатель заглушен | PID не работают (STOPPED), только ATRV |
---
## НЕУДАЧА 1: Игнорирование рабочего кода
**Симптом**: `raw/ElmProtocol.kt` написан с нуля вместо копирования `app/ElmProtocol.kt`
**Когда**: создание raw-модуля (июнь 2026)
**Что сделано не так**: В репозитории УЖЕ был `app/src/.../ElmProtocol.kt` — рабочий протокол на основе AndrOBD. Вместо копирования написан новый с «улучшениями».
**Корневая причина**: предположение что raw-реле требует особой логики. Не проверено что `app/ElmProtocol.kt` уже работает.
**Исправлено**: v0.4.0-dev — raw/ElmProtocol.kt = 1:1 копия app/ElmProtocol.kt
**Урок**: всегда проверять существующий код перед созданием нового.
---
## НЕУДАЧА 2: Доверие Opus без проверки исходников
**Симптом**: init() с ATE0 первым, ATST96 для клонов, drain перед write
**Когда**: июнь–июль 2026
**Что сделано не так**: Opus посоветовал — реализовано без сверки с AndrOBD (ElmProt.java). AndrOBD 10 лет в проде, все «улучшения» были ошибками.
**Корневая причина**: доверие внешнему анализу вместо проверки первоисточника.
**Исправлено**: полная сверка с ElmProt.java (fr3ts0n/AndrOBD). Все отличия задокументированы.
**Урок**: первоисточник (код) > любой анализ.
---
## НЕУДАЧА 3: ATST96 убивает клон v1.5
**Симптом**: клон зависает намертво после init, даже ATRV не отвечает
**Когда**: v0.3.0-dev
**Что сделано не так**: добавлен `ATST96` (150×4=600ms) для клонов. Команда не поддерживается клоном v1.5 и вешает его.
**Корневая причина**: предположение что клону нужен фиксированный таймаут. AndrOBD использует `updateAtst()` с адаптивным таймаутом для всех устройств.
**Исправлено**: ATST96 удалён. `updateAtst()` теперь вызывается всегда (как в AndrOBD).
**Урок**: не добавлять команды, которых нет в AndrOBD.
---
## НЕУДАЧА 4: ATI в init() ломает порядок
**Симптом**: буферный сдвиг после каждой команды
**Когда**: v0.3.0v0.3.7-dev
**Что сделано не так**: `ATI` вставлен в середину init() для детекта клона. AndrOBD не использует ATI — версия определяется из ответа ELM на MODEL.
**Корневая причина**: желание детектить клон внутри init(). AndrOBD определяет клона иначе.
**Исправлено**: v0.4.0-dev — ATI удалён из init(). Init: ATSP0→ATAT1→ATST→ATS0→ATL0→ATE0.
**Урок**: не вставлять команды в init(), которых нет в AndrOBD.
---
## НЕУДАЧА 5: drainInput() в write()
**Симптом**: ответ команды N читается как ответ команды N+1
**Когда**: все версии raw
**Что сделано не так**: `write()` вызывает `drainInput()` перед отправкой. Если ответ предыдущей команды запаздывает, drain съедает его и наступает сдвиг буфера на 1 позицию.
**Корневая причина**: AndrOBD не использует drain. Синхронная модель (sendCommand → ждать ответ) требует drain для очистки мусора, но drain сам создаёт мусор если ответ приходит с задержкой.
**Статус**: НЕ ИСПРАВЛЕНО. Остаётся в коде v0.4.0-dev. Без drain буфер забивается мусором, с drain — сдвиг на 1 команду. Меньшее из зол.
**Урок**: синхронная модель принципиально ограничена. AndrOBD использует асинхронную (поток читает → handleTelegram) и не имеет этой проблемы.
---
## НЕУДАЧА 6: Freeze detection через ATRV
**Симптом**: ответ "12.6V" читается как ответ на PID, все команды возвращают одно и то же
**Когда**: v0.3.0-dev
**Что сделано не так**: каждые 10 команд слался ATRV для проверки залипания. Ответ ATRV попадал в буфер и читался как ответ следующего PID.
**Корневая причина**: ATRV — AT-команда, не OBD. Её ответ не должен смешиваться с PID-ответами. В синхронной модели это неизбежно.
**Исправлено**: удалено в v0.3.2-dev.
**Урок**: не смешивать AT-команды с OBD-командами в одном потоке.
---
## НЕУДАЧА 7: AT-команды в handle()
**Симптом**: после STOPPED/BUS_ERROR следующая команда получает мусор
**Когда**: v0.1.xv0.3.3-dev
**Что сделано не так**: handle() при ошибках слал ATPC, ATWS, ATSP0. Эти AT-команды отправлялись внутри обработки ответа текущей команды, их ответы загрязняли буфер.
**Корневая причина**: handle() вызывается из exec() который находится в процессе чтения ответа. Отправка AT-команд внутри exec() создаёт вложенные чтения, которые путают буфер.
**Исправлено**: в v0.4.0-dev handle() шлёт AT-команды только для BUS_ERROR и ERROR (как в AndrOBD). Для STOPPED — только state tracking.
**Урок**: AT-команды в handle() допустимы только если их ответы полностью потребляются (tryRead с таймаутом).
---
## НЕУДАЧА 8: Recovery ATSP0 в relayLoop
**Симптом**: тест показывает 0/60 (все таймауты)
**Когда**: v0.3.5-dev
**Что сделано не так**: после STOPPED или пустого ответа relayLoop слал ATSP0 через sendBlocking. ATSP0 блокирует single-thread executor на 3+ секунд. Все последующие команды ждут в очереди, тест видит таймауты.
**Корневая причина**: recovery выполнялся в том же потоке что и обработка команд. Команды накапливались в очереди executor'а.
**Исправлено**: удалено в v0.4.0-dev. Recovery теперь только в handle().
**Урок**: recovery должен быть частью протокольного уровня (ElmProtocol), а не оркестратора (relayLoop).
---
## НЕУДАЧА 9: Дренаж 0100 после init
**Симптом**: relay не отвечает ни на одну команду после init
**Когда**: v0.3.6-dev
**Что сделано не так**: после init отправлялся 0100 для поглощения буферного сдвига. Дренаж сам создавал сдвиг и дезориентировал клон.
**Корневая причина**: попытка «подчистить» буфер создаёт новую команду и новый ответ, который тоже надо чистить — бесконечная рекурсия.
**Исправлено**: удалено в v0.3.7-dev.
**Урок**: не пытаться чистить буфер дополнительными командами.
---
## НЕУДАЧА 10: Тест читал чужие ответы
**Симптом**: тест показывал OK (411100) когда relay возвращал пустоту
**Когда**: все тесты до исправления сервера
**Что сделано не так**: тестовый скрипт вызывал `/response?wait=3` без `device_id`. Сервер возвращал ответы от других устройств или предыдущих сессий.
**Корневая причина**: серверный SQL не фильтровал по device_id. Тест не обновлял seq.
**Исправлено**: сервер (`api/raw_elm.py`) — `/response` принимает `device_id`. Тест обновляет seq после каждого ответа.
**Урок**: всегда проверять что тест читает данные того устройства которое тестируется.
---
## НЕУДАЧА 11: Версия на сайте не обновлялась
**Симптом**: сайт показывал v0.3.0-dev при v0.4.0-dev на сервере
**Когда**: все деплои
**Что сделано не так**: `sed` правил только `/opt/elmer/web/templates/index.html`. Flask использует `/opt/elmer/templates/index.html`.
**Корневая причина**: два index.html в разных директориях, неизвестно какой использует Flask.
**Исправлено**: деплой обновляет оба файла. Выяснено что Flask использует `/opt/elmer/templates/`.
**Урок**: проверять какой файл реально сервится перед правкой.
---
## НЕУДАЧА 12: v0.4.0-dev не долетел до телефона
**Симптом**: пользователь тестировал v0.3.7-dev думая что это v0.4.0-dev
**Когда**: 2026-07-04 вечер
**Что сделано не так**: деплой v0.4.0-dev прошёл, APK на сервере, но пользователь не переустановил приложение.
**Корневая причина**: отсутствие проверки версии на телефоне.
**Исправлено**: явное указание версии при каждом деплое. Проверка MD5 APK на сервере.
**Урок**: всегда проверять что пользователь обновил APK перед тестированием.
---
## ОГРАНИЧЕНИЯ КЛОНОВ v1.5 (железо)
| Параметр | Значение |
|----------|----------|
| Надёжность (холодный) | 100% первые 10-12 команд |
| Надёжность (горячий) | 70-80%, деградация после 10-12 команд |
| Буферный сдвиг после init | 1-2 команды (неизбежно в синхронной модели) |
| Двигатель заглушен | PID не работают, ATRV — ок |
| Двигатель заведён | 70-80% успех |
| ATST96 | Убивает клон намертво |
| ATAT1 | Клон игнорирует, не мешает |
| Время восстановления | 2-3 секунды паузы |
---
## ТЕКУЩАЯ АРХИТЕКТУРА (v0.4.0-dev)
```
Android phone Server (obdai.ru)
┌──────────────────┐ ┌─────────────────────┐
│ RawRelayService │──HTTP────→│ /api/v1/elm/raw/cmd │
│ relayLoop() │←──poll───│ /api/v1/elm/raw/ │
│ ↓ │ │ response │
│ ElmActor │ └─────────────────────┘
│ ↓ │
│ ElmProtocol │──BT────→ ELM327 → OBD-II → ECU
│ (AndrOBD) │←──BT───
└──────────────────┘
```
---
## КЛЮЧЕВЫЕ ФАЙЛЫ
| Файл | Версия | Описание |
|------|--------|----------|
| `raw/.../ElmProtocol.kt` | v0.4.0-dev | 1:1 с app/ElmProtocol.kt |
| `raw/.../ElmActor.kt` | v0.4.0-dev | Single-thread executor |
| `raw/.../RawRelayService.kt` | v0.4.0-dev | Поллинг + обработка ответов |
| `api/raw_elm.py` | исправлен | /response с device_id |
| `app/.../ElmProtocol.kt` | исходный | Протокол основного приложения |
+91
View File
@@ -0,0 +1,91 @@
# Отчёт об аудите безопасности и багах (07.06.2026)
## 🔴 КРИТИЧЕСКИЕ (7)
### 1. Жестко заданный API ключ в конфигурации
- **Файл:** [config.yaml](config.yaml#L5)
- **Описание:** \`api_key: "sk-78ec529c1eba4ba69995091046c9fa33"\` — настоящий ключ DeepSeek находится непосредственно в репозитории.
- **Влияние:** Экспозиция платного LLM, финансовый ущерб, возможность несанкционированного использования лимитов.
### 2. check_same_thread=False в SQLite
- **Файл:** [api/db.py](api/db.py#L20)
- **Описание:** Использование \`check_same_thread=False\` без механизмов синхронизации в многопоточном Flask-приложении.
- **Влияние:** Состояние гонки (Race conditions), повреждение базы данных при одновременной записи.
### 3. /api/v1/ping-llm без аутентификации тратит токены
- **Файлы:** [api/ping.py](api/ping.py#L40-45), [web/script_endpoint.py](web/script_endpoint.py#L215-219)
- **Описание:** Эндпоинт доступен без заголовка \`X-Api-Key\` и выполняет реальный запрос к LLM.
- **Влияние:** Возможность DoS-атаки на кошелек API через бесконечные пинги.
### 4. Краш при отсутствии Bluetooth (Android)
- **Файл:** [android/app/src/main/java/ru/elmer/client/ui/MainActivity.kt](android/app/src/main/java/ru/elmer/client/ui/MainActivity.kt)
- **Описание:** Используется оператор \`!!\` для \`btAdapter\`. На устройствах без Bluetooth приложение упадёт.
- **Влияние:** Нестабильность приложения на эмуляторах и старых устройствах.
### 5. Отсутствие аутентификации на критичных эндпоинтах
- **Файл:** [api/routes.py](api/routes.py#L245), [api/routes.py](api/routes.py#L292)
- **Описание:** Эндпоинты \`POST /api/v1/elm/probe\` и \`GET /api/v1/elm/profile/<mac>\` не защищены API-ключом.
- **Влияние:** Идентификация структуры OBD-профилей любых пользователей.
### 6. Дублирование API с разной логикой
- **Файлы:** [web/script_endpoint.py](web/script_endpoint.py) vs [api/routes.py](api/routes.py)
- **Описание:** Маршрут \`/api/v1/session/upload\` реализован дважды. В \`web/\` версии отсутствует проверка идемпотентности (\`request_id\`).
- **Влияние:** Неконсистентное поведение, дублирование LLM-запросов при ретраях из мобильного приложения.
### 7. Состязание потоков в ScriptRunnerService (Android)
- **Файл:** [android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt](android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt)
- **Описание:** Повторный запуск сервиса создает новый поток \`ScriptRunner\`, конкурирующий за Bluetooth-сокет.
---
## 🟡 ВАЖНЫЕ (10)
### 8. Database.close() не гарантирован в web/
- **Файл:** [web/script_endpoint.py](web/script_endpoint.py#L117-120)
- **Описание:** Соединение с БД открывается, но не закрывается в блоке \`finally\`.
- **Влияние:** Утечка дескрипторов файлов и соединений SQLite.
### 9. Отсутствие checkpoint для WAL в SQLite
- **Файл:** [api/db.py](api/db.py#L22)
- **Описание:** Режим WAL включен, но \`wal_checkpoint\` никогда не вызывается явно. Журналы могут расти бесконечно.
### 10. Утечка курсоров в БД (Android)
- **Файл:** [android/app/src/main/java/ru/elmer/client/db/SessionDb.kt](android/app/src/main/java/ru/elmer/client/db/SessionDb.kt)
- **Описание:** Курсоры закрываются только в конце успешных циклов, а не в \`finally\`.
### 11. Не включены Foreign Keys (Android)
- **Файл:** [android/app/src/main/java/ru/elmer/client/db/SessionDb.kt](android/app/src/main/java/ru/elmer/client/db/SessionDb.kt)
- **Описание:** SQLite игнорирует \`REFERENCES\` без явной команды \`PRAGMA foreign_keys = ON\`.
### 12. Раскрытие sensitive info в ошибках
- **Файлы:** [web/script_endpoint.py](web/script_endpoint.py#L127), [api/ping.py](api/ping.py#L48)
- **Описание:** \`str(e)\` пробрасывается клиенту, может содержать детали API или токены.
### 13. Нет лимита на размер payload
- **Описание:** Сервер принимает JSON любого объема, что ведет к OOM (Out Of Memory).
### 14. Нет rate-limiting
- **Описание:** Отсутствует защита от перебора ключей и спама запросами.
### 15. Отсутствие CORS ограничений
- **Файл:** [web/app.py](web/app.py#L48)
- **Описание:** Flask слушает на \`0.0.0.0\`, разрешая запросы с любых источников.
### 16. Уязвимость потокобезопасности AndrOBD
- **Файл:** [obd/protocol.py](obd/protocol.py#L114-121)
- **Описание:** Метод \`send()\` не синхронизирован, состояние протокола может быть повреждено при параллельном доступе.
### 17. Слепой выбор Bluetooth-устройства в сервисе (Android)
- **Файл:** [android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt](android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt#L143)
- **Описание:** Берется первое сопряженное устройство (\`bonded[0]\`), что часто ошибочно.
---
## 📋 ЗАМЕЧАНИЯ ПО АРХИТЕКТУРЕ
- **Dynamic Imports:** В \`web/script_endpoint.py\` импорты используют \`elmer.*\`, что может конфликтовать с установленными пакетами.
- **Git Hygiene:** Файл \`config.yaml\` содержит секреты и должен быть добавлен в \`.gitignore\` с предоставлением \`config.yaml.example\`.
- **Логирование:** Недостаточно информации для трассировки багов пользователя (отсутствуют IP и User-Agent в логах сессий).
---
*Дата аудита: 07.06.2026*
*Инструмент: GitHub Copilot (Gemini 3 Flash)*
@@ -0,0 +1,209 @@
# Анализ динамического сбоя ELM327
Дата: 2026-06-14
## Контекст
Проверено:
- статическая диагностика работает стабильно;
- чтение VIN работает;
- чтение DTC работает;
- последовательное чтение нескольких PID в статическом режиме работает;
- ELM327 выдерживает не менее 9 PID подряд в статике;
- увеличение пауз до 4000 мс не устраняет проблему;
- автоподбор таймингов не устраняет проблему;
- сбой проявляется только в динамическом сборе данных через ScriptEngine.
Это сильно сужает пространство причин. Проблема почти наверняка не в "скорости ELM вообще", не в "ECU не успевает" и не в банальном "надо ещё увеличить задержку".
## Что объясняет факты лучше всего
### 1. Несовпадение между тем, как ElmChecker и ScriptEngine общаются с ELM
Вероятность: высокая.
Смысл гипотезы: статический путь и динамический путь используют не одинаковую коммуникационную последовательность. Ломается не ELM как таковой, а конкретный сценарий: кто пишет, кто читает, когда читают, что считается окончанием ответа, как очищается буфер, как меняются состояния.
Что подтверждает:
- статический путь работает полностью;
- динамический ломается только в ScriptEngine;
- один и тот же адаптер выдерживает длинную серию PID в статике;
- в проектных заметках уже зафиксировано, что проблема может быть именно в различии между ElmChecker и ScriptEngine, а не в тайминге как таковом.
Что противоречит:
- если в удачном и неудачном сценарии полностью совпадают AT-команды, порядок команд, чтение и ожидание конца ответа.
Быстрый эксперимент:
- снять полный лог команд и сырых ответов для успешного статического пути и для динамического пути;
- сравнить именно последовательность TX/RX, а не распарсенные значения;
- проверить, расходится ли сценарий уже до первого PID.
### 2. InputStream не дочитывается до символа ">", и следующий запрос попадает в хвост прошлого ответа
Вероятность: высокая.
Смысл гипотезы: динамический читатель завершает чтение раньше, чем ELM реально закончил ответ. В буфере остаётся промпт `>` или другой хвост, и следующий запрос читает не чистый ответ, а остаток прошлого цикла.
Что подтверждает:
- это прямо совпадает с типовым режимом отказа ELM327;
- в проектных заметках символ `>` отдельно выделен как конец ответа;
- в обсуждениях проекта уже встречалась версия, что ответы смешиваются и хвост остаётся в буфере;
- статика может это маскировать, потому что между командами там больше естественных пауз.
Что противоречит:
- если сырые логи показывают, что каждый ответ полностью доходит до `>` и следующий запрос стартует только после этого;
- если после сбоя буфер точно пуст.
Быстрый эксперимент:
- включить сырой дамп RX/TX без парсинга;
- для первого сбойного цикла проверить, присутствует ли `>` в сыром потоке полностью;
- перед следующим запросом проверить, не остаётся ли в InputStream ничего, кроме уже считанного ответа.
### 3. Остатки данных в буфере ломают синхронизацию между командами
Вероятность: высокая.
Смысл гипотезы: чтение и запись идут корректно по отдельности, но между ними нет надёжной очистки буфера. В результате следующий запрос потребляет не только свежий ответ, но и мусор: эхо, переносы строк, старые байты, задержавшийся ответ предыдущего PID.
Что подтверждает:
- проектные заметки отдельно говорят, что drainInput раньше был механизмом синхронизации запрос/ответ;
- после отключения drainInput в одной из версий стало хуже;
- описан эффект сдвига: ответ на команду N прочитан как ответ на N+1;
- статический сценарий выдерживает это лучше из-за более редкой частоты обращений.
Что противоречит:
- если перед каждым запросом в динамическом цикле буфер гарантированно очищается и при этом проблема остаётся;
- если в логах нет признаков мусора, эха или сдвига границ ответов.
Быстрый эксперимент:
- один раз до старта динамики и один раз перед вторым запросом вывести количество доступных байт в InputStream и содержимое остатка;
- сравнить результат между успешным статическим и неудачным динамическим прогоном;
- проверить, есть ли хвосты после первого ответа.
### 4. ScriptEngine выполняет не тот же state machine, что ElmChecker
Вероятность: средняя.
Смысл гипотезы: проблема не в самом Bluetooth и не в самом ELM, а в том, что динамический движок переходит между состояниями раньше или иначе, чем ElmChecker. Например, команда считается завершённой по временному признаку, а не по фактическому окончанию ответа.
Что подтверждает:
- пользователь отдельно выделил риск state machine;
- динамический режим содержит свои шаги, цикл и внутренние переходы;
- статический путь короче и проще, поэтому ошибки state machine там могут не проявляться;
- уже были замечания, что в таких сценариях рассинхрон появляется раньше, чем кажется.
Что противоречит:
- если логически и по времени state transitions происходят только после полного ответа ELM;
- если обе машины выполняют одинаковый сценарий завершения команды.
Быстрый эксперимент:
- на одном прогоне логировать каждое состояние до и после отправки команды;
- отметить момент, когда реально получен `>`;
- проверить, не уходит ли ScriptEngine в следующий шаг до фактического конца ответа.
### 5. Доступ к одному сокету или одному InputStream идёт из двух потоков
Вероятность: средняя.
Смысл гипотезы: чтение или запись в динамике пересекаются с другим потоком, который тоже читает или пишет тот же канал. Для ELM это критично: поток байтов становится недетерминированным, и команда может лишиться части ответа.
Что подтверждает:
- пользователь отдельно попросил проверить конкурентный доступ к сокету;
- динамический режим по определению более многопоточен: цикл, сбор данных, UI, возможные фоновые операции;
- статический путь может не задевать гонку из-за более редкой частоты и меньшего числа активных операций.
Что противоречит:
- если трасса покажет строго одного читателя и одного писателя на весь жизненный цикл соединения;
- если динамика воспроизводится даже в полностью однопоточном прогоне.
Быстрый эксперимент:
- вывести thread id для каждого read и write;
- проверить, нет ли второго consumer на InputStream;
- сравнить идентичность владельца сокета в статике и динамике.
### 6. Неправильная последовательность команд, а не неправильная пауза
Вероятность: средняя-низкая.
Смысл гипотезы: дело не в длительности ожидания как таковой, а в том, что динамический сценарий отправляет команды в другом порядке или с другим набором служебных AT-команд, чем успешный статический сценарий. Тогда ELM оказывается в другом режиме, и дальнейшая обработка ломается.
Что подтверждает:
- в проекте есть отдельные сценарии для статической диагностики, тестового скрипта и динамики;
- серверный build_test_script и build_dynamic_script действительно строят разные последовательности;
- в заметках по ELM отдельно обсуждаются последствия ATWS, ATE0/ATL0/ATS0 и различий в инит-последовательности.
Что противоречит:
- если сравнение трасс покажет полностью одинаковый init и только разный темп;
- если тот же набор команд в статике и динамике повторяет поломку только из-за способа выполнения, а не порядка.
Быстрый эксперимент:
- распечатать полный список команд, которые реально уходят в ELM в обоих режимах;
- сравнить not only PID, но и все AT-команды, входы в state machine и возможные reset-команды;
- проверить, совпадает ли стартовая инициализация побайтно.
## Что менее вероятно
### Adaptive timing как первопричина
Вероятность: низкая.
Почему низкая:
- уже проверяли увеличение пауз до 4000 мс;
- уже проверяли автоподбор;
- одиночные запросы и статический набор PID работают.
Вывод: adaptive timing может усиливать или маскировать проблему, но не выглядит корнем сбоя.
### Просто "мало ждать"
Вероятность: низкая.
Почему низкая:
- паузы уже увеличивали;
- первый запрос проходит, второй ломается;
- для обычного ELM327 это больше похоже на ошибку синхронизации, чем на нехватку миллисекунд.
## Итоговая интерпретация
Новое мнение хорошо согласуется с уже собранными фактами. Оно сдвигает фокус с "таймингов вообще" на более узкий класс проблем:
- границы ответа ELM, особенно символ `>`;
- остатки в InputStream;
- различие между ElmChecker и ScriptEngine;
- state machine, которая может идти вперёд раньше времени;
- возможная конкуренция за сокет или поток чтения.
Главный вывод: если статический путь стабилен, а динамический ломается даже при больших паузах, то первичная причина почти наверняка находится не в задержках, а в чтении потока, границах ответа и разнице в сценарии исполнения.
## Порядок расследования
1. Снять сырой RX/TX лог без парсинга для статического и динамического режима.
2. Проверить, доходит ли каждый ответ до `>` и не остаются ли байты в буфере перед следующим запросом.
3. Сопоставить полную последовательность команд ElmChecker и ScriptEngine.
4. Подтвердить или опровергнуть второй consumer на сокете/InputStream.
5. Проверить, не идёт ли state machine вперёд до фактического завершения ответа.
## Краткий вывод
Наиболее правдоподобно, что проблема не в скорости ELM, а в том, как динамический сценарий читает и синхронизирует поток ответов. Внутри этого класса причин самые сильные кандидаты: неполное дочитывание до `>`, остатки в InputStream, и различие между ElmChecker и ScriptEngine.
+293
View File
@@ -0,0 +1,293 @@
# План: тонкий Android-ретранслятор ELM327
Дата: 2026-06-14
## Цель
Отдельное Android-приложение — тупой ретранслятор команд между сервером и ELM327.
Пользователь устанавливает один раз. Вся логика (какие команды слать, как анализировать
ответы) — на сервере. Приложение только:
1. Коннектится к ELM327 по Bluetooth
2. Сообщает серверу «готов»
3. Поллит сервер на наличие команды
4. Отправляет команду в ELM327
5. Возвращает сырой ответ на сервер
6. Повторяет с пункта 3
## Почему отдельное приложение
- Ноль риска сломать существующий `ru.elmer.client`
- Независимый пакет `ru.elmer.raw`
- Свой APK, свой URL на сервере (`/elm-raw.apk`)
- Можно удалить/переустановить независимо от основного
## Архитектура
```
┌─────────────────────────────────────────────────┐
│ Сервер (elmer/python) │
│ │
│ POST /api/v1/elm/raw/cmd ← я ставлю команду │
│ GET /api/v1/elm/raw/cmd ← приложение поллит │
│ POST /api/v1/elm/raw/response ← приложение шлёт │
│ GET /api/v1/elm/raw/response ← я читаю ответ │
│ /elm-raw.apk ← раздача APK │
└──────────────┬──────────────────────────────────┘
│ HTTP (OkHttp)
┌──────────────▼──────────────────────────────────┐
│ Android-приложение (ru.elmer.raw) │
│ │
│ RawRelayService (foreground) │
│ ├─ Bluetooth → ELM327 │
│ ├─ ElmProtocol (AndrOBD, проверенный) │
│ ├─ Polling: GET /cmd каждые 500ms │
│ └─ POST /response с сырым ответом │
│ │
│ MainActivity (минимальный UI) │
│ ├─ Статус: сервер / ELM / ECU │
│ ├─ Лог последних команд │
│ └─ Кнопка «Стоп» │
└──────────────────────────────────────────────────┘
```
## Компоненты Android-приложения
### 1. Пакет: `ru.elmer.raw`
Новый пакет, не пересекается с `ru.elmer.client`.
### 2. Файлы (5 штук)
| Файл | Размер | Назначение |
|------|--------|-----------|
| `MainActivity.kt` | ~100 строк | UI: статус, лог, кнопка стоп |
| `RawRelayService.kt` | ~150 строк | Foreground-сервис: BT+поллинг+команды |
| `ElmProtocol.kt` | копия | Точная копия из `ru.elmer.client.elm` |
| `ServerClient.kt` | ~80 строк | Урезанный HTTP-клиент (только cmd/response) |
| `AndroidManifest.xml` | ~40 строк | Свой манифест для `ru.elmer.raw` |
**Почему копия ElmProtocol.kt, а не общий модуль:**
- Не трогаем существующий код вообще
- AndrOBD-логика отлажена годами, меняться не будет
- Две копии живут независимо, никаких конфликтов
### 3. ElmProtocol.kt — как есть
Используем **без изменений** проверенную стейт-машину:
- `init()`: ATSP0 → ATAT1 → ATS0 → ATL0 → ATE0
- `sendCommand(cmd)`: отправить → прочитать до `>` → вернуть сырой ответ
- Обработка ошибок: BUS ERROR, CAN ERROR, BUFFER FULL, ретраи, восстановление
- Адаптивные тайминги
Единственное что добавим — вызов `sendCommand()` оборачиваем в `try/catch`,
результат всегда возвращается на сервер (даже если ошибка).
### 4. Протокол обмена с сервером
#### Приложение → Сервер: «я готов»
```
POST /api/v1/elm/raw/hello
{
"device_id": "android-xyz",
"elm_version": "ELM327 v1.5",
"protocol": "A4",
"voltage": "12.3V"
}
```
#### Сервер → Приложение: команда
```
GET /api/v1/elm/raw/cmd?device_id=android-xyz
Ответ 200:
{
"cmd": "0105",
"timeout_ms": 500,
"drain_first": false,
"seq": 1
}
Ответ 204: (нет команды — полли дальше)
```
#### Приложение → Сервер: ответ
```
POST /api/v1/elm/raw/response
{
"device_id": "android-xyz",
"seq": 1,
"cmd": "0105",
"raw": "41 05 5C",
"prompt": true,
"elapsed_ms": 48,
"bytes": 8,
"error": null
}
```
#### Сервер → Приложение: подтверждение
```
200 {"ok": true}
```
### 5. RawRelayService — жизненный цикл
```
onStartCommand(Intent: serverUrl)
1. Подключить Bluetooth к ELM327 (UUID SPP 00001101-0000-1000-8000-00805F9B34FB)
2. ElmProtocol.init() — базовая инициализация
3. POST /hello — сообщить серверу «готов»
4. Цикл (в фоновом потоке):
GET /cmd — ждать команду (500ms поллинг)
если 204 → sleep 500ms → снова GET /cmd
если 200 →
drain? → ElmProtocol.sendCommand("ATPC") → read/discard
ElmProtocol.sendCommand(cmd)
POST /response — отправить сырой ответ
→ снова GET /cmd
5. onDestroy(): закрыть BT, stopForeground, остановить поток
```
### 6. MainActivity — UI
```
┌──────────────────────────────┐
│ ELM327 Raw Relay │
│ │
│ Сервер: ✅ obdai.ru │
│ ELM: 🔵 подключён │
│ ECU: ✅ отвечает │
│ │
│ Последняя команда: │
│ → 0105 │
│ ← 41 05 5C (48ms, 8 байт) │
│ │
│ Лог: 12 команд, 0 ошибок │
│ │
│ [ СТОП ] │
└──────────────────────────────┘
```
Минимальный UI:
- Три индикатора статуса (сервер, ELM, ECU)
- Последняя команда и ответ
- Счётчик команд/ошибок
- Кнопка «Стоп»
## Изменения на серверной стороне (elmer/python)
### 1. Очередь команд — `api/raw_elm.py`
Добавить эндпоинты (дополнить существующий `api/raw_elm.py`):
```
POST /api/v1/elm/raw/cmd — я ставлю команду в очередь
GET /api/v1/elm/raw/cmd — приложение забирает команду
POST /api/v1/elm/raw/response — приложение шлёт ответ
GET /api/v1/elm/raw/response — я читаю последний ответ
POST /api/v1/elm/raw/hello — приложение регистрируется
GET /api/v1/elm/raw/status — статус: готово/ждёт/ошибка
```
### 2. Хранение очереди
В памяти (глобальная переменная), не в БД:
- `_pending_cmd: dict | None` — команда, которую ждёт приложение
- `_last_response: dict | None` — последний ответ от ELM327
- `_device_ready: bool` — готово ли приложение
- `_device_info: dict` — информация об устройстве
Зачем в памяти: одна сессия отладки, один поток команд. Не нужна персистентность.
### 3. Раздача APK — `web/app.py`
```python
@app.route("/elm-raw.apk")
def download_raw_apk():
return send_from_directory("static", "elm-raw.apk", ...)
```
В `templates/index.html` — ссылка «Скачать ELM Raw Relay».
### 4. Интерактивная консоль — `tools/elm_relay.py`
Скрипт для меня (Copilot):
- Читает статус устройства
- Ставит команду в очередь
- Ждёт ответ
- Показывает сырой ответ
- Анализирует, ставит следующую команду
- История всех команд сохраняется
## Сборка и деплой
### Сборка APK
```bash
cd android
./gradlew :app:assembleDebug
# APK: android/app/build/outputs/apk/debug/app-debug.apk
```
Но нам нужен **отдельный** APK для `ru.elmer.raw`. Два варианта:
**Вариант A: Product Flavor** (в одном проекте)
- В `app/build.gradle.kts` добавить `flavorDimensions` + два flavor: `client` и `raw`
- Разные `applicationId`, разные `AndroidManifest.xml`
- Общий код в `main/`, специфичный — в `client/` и `raw/`
- Минус: трогаем `build.gradle.kts` основного приложения
**Вариант B: Новый модуль** (рекомендую)
- Новый Gradle-модуль `android/raw/`
- Свой `build.gradle.kts`, свой манифест, свой пакет
- Не трогаем вообще ничего в `android/app/`
- `settings.gradle.kts` — добавить `include(":raw")`
- Минус: ElmProtocol.kt — физическая копия файла
### Я за Вариант B: новый модуль `:raw`
```
android/
├── app/ ← существующее, НЕ ТРОГАЕМ
├── raw/ ← НОВЫЙ модуль
│ ├── build.gradle.kts
│ └── src/main/
│ ├── AndroidManifest.xml
│ └── java/ru/elmer/raw/
│ ├── MainActivity.kt
│ ├── RawRelayService.kt
│ ├── ElmProtocol.kt ← копия из :app
│ └── ServerClient.kt
├── settings.gradle.kts ← + include(":raw")
└── build.gradle.kts ← не трогаем
```
### Деплой
```bash
cd android
./gradlew :raw:assembleDebug
cp raw/build/outputs/apk/debug/raw-debug.apk ../web/static/elm-raw.apk
# Задеплоить на сервер через deploy.sh
```
## Порядок работ
1. **Сервер**: дополнить `api/raw_elm.py` эндпоинтами очереди
2. **Сервер**: добавить `web/app.py` — раздача `/elm-raw.apk`
3. **Сервер**: `tools/elm_relay.py` — консоль для меня
4. **Android**: модуль `:raw` — 5 файлов (.kt + манифест + build.gradle)
5. **Сборка**: проверить что оба APK собираются
6. **Тест**: поставить APK на телефон, проверить связь с сервером
## Что НЕ делаем
- Не трогаем `ru.elmer.client` — ни строчки
- Не меняем `app/build.gradle.kts`
- Не меняем существующий `AndroidManifest.xml`
- Не изобретаем новый ELM327-протокол — используем AndrOBD как есть
- Не пишем сложный UI — только статус и лог
+207
View File
@@ -0,0 +1,207 @@
# 2026-06-10 — Speed-test ELM327, адаптивный интервал, валидация, деплой v0.94.0-dev
## Проблема
Динамический тест (START/STOP) использует жёстко заданный интервал 250мс для опроса 3 PID (RPM, MAF, STFT). Реальные тесты на машине показали:
- **Session #47** (3 PID × 250ms): 94% ошибок — ELM327 v1.5 не успевает
- **Session #48** (3 PID × 250ms): первые 15 сэмплов ок, потом все пустые — буфер ELM переполняется
- **Session #45** (5 PID × 500ms, старый): после ~20 сэмплов тоже падает
**Корень:** 3 команды занимают ~240-300ms на ELM327 v1.5. При интервале 250ms пауза между батчами ≈ 0ms. Буфер UART переполняется, ELM перестаёт отвечать.
Также: в `api/db.py` не было защиты `threading.Lock` — 20 конкурентных записей в БД давали 8 ошибок.
---
## Решения
### 1. Threading lock в Database
`api/db.py` — добавлен `threading.Lock()`, обёрнуты все write-методы (`save_session`, `save_dtc_scan`, `save_device_profile`).
Было: `self.conn.execute()` + `self.conn.commit()` без блокировки → 8/20 ошибок при конкурентном доступе.
Стало: `with self._lock:` → 0 ошибок.
Коммит: `fix: threading lock in Database for concurrent writes`
### 2. Speed-test ELM327 — адаптивный интервал
#### Концепция
При первом подключении нового ELM327 (уникальный BT MAC) — замерить скорость ответа на разных PID, сохранить в профиль. При последующих запусках использовать сохранённое значение для расчёта интервала.
#### Сервер — `api/db.py`
Добавлена колонка `response_time_ms INTEGER DEFAULT 250` в таблицу `device_profiles`:
```sql
CREATE TABLE device_profiles (
mac TEXT PRIMARY KEY,
level INTEGER NOT NULL,
elm_version TEXT,
elm_desc TEXT,
protocol TEXT,
voltage TEXT,
response_time_ms INTEGER DEFAULT 250, -- <-- NEW
supported TEXT,
unsupported TEXT,
errors TEXT,
first_seen TEXT,
last_seen TEXT
);
```
Миграция для старых БД:
```sql
ALTER TABLE device_profiles ADD COLUMN response_time_ms INTEGER DEFAULT 250;
```
`save_device_profile()` обновлена: принимает и сохраняет `response_time_ms`.
#### Сервер — `api/routes.py`
Добавлен эндпоинт:
```
PUT /api/v1/elm/profile/<mac>
Body: {"response_time_ms": 180}
```
Позволяет Android-клиенту обновить скорость ELM в профиле.
#### Android — `ElmChecker.kt`
Добавлен метод `measureResponseTime(elm: ElmProtocol, log: (String) -> Unit): Int`:
```
Алгоритм:
1. Выбрать 3 PID: 010C (RPM), 0110 (MAF), 0106 (STFT)
2. Каждый PID послать 3 раза
3. Замерить round-trip время для каждого
4. Усреднить
5. Вернуть среднее в миллисекундах
6. Логировать в UI: "⏱ Тест скорости: 010C — 82ms, 89ms, 78ms"
```
Вызывается после connectAndInit(), перед стартом динамического теста.
#### Android — `MainActivity.kt` — `startDynamicRecording()`
В流程 добавлен speed-test между статическим пробросом PID и динамическим сбором:
```
1. Статика: пробуем 9 PID, log в UI
2. Speed-test: 3 PID × 3 раза, замер времени
→ "⏱ Тест скорости ELM..."
→ " RPM: 82ms 89ms 78ms (среднее 83ms)"
→ " MAF: 95ms 91ms 88ms (среднее 91ms)"
→ " STFT: 79ms 82ms 85ms (среднее 82ms)"
→ " Среднее по всем: 85ms"
3. Расчёт интервала: max(250, avg_response_time × 3 × 1.5)
→ "📡 Интервал опроса: 383ms (запас 50%)"
4. Сохранение response_time_ms на сервер
5. Запуск DynamicCollector с вычисленным интервалом
```
Формула интервала:
```
interval = max(250, avg_response_time × num_pids × 1.5)
```
Где:
- `avg_response_time` — среднее время ответа ELM на одну команду (ms)
- `num_pids` — количество PID в динамическом тесте (3)
- `1.5` — запас 50% на вариативность
- `250` — минимальный интервал (для быстрых ELM327 v2.x)
Если профиль уже существует (повторный запуск) — speed-test пропускается, интервал берётся из профиля. По кнопке «принудительно» можно перезамерить.
---
## Итог тестов
После фикса threading lock:
```
134 ✅ / 0 ❌ — все тесты проходят
```
---
## Файлы
| Файл | Что изменено |
|------|-------------|
| `api/db.py` | threading lock, response_time_ms колонка, миграция |
| `api/routes.py` | PUT /api/v1/elm/profile/<mac> |
| `android/.../ElmChecker.kt` | measureResponseTime() |
| `android/.../MainActivity.kt` | speed-test перед динамикой, адаптивный интервал |
| `android/.../ServerClient.kt` | saveProfile() — отправка response_time_ms |
| `android/.../DynamicCollector.kt` | intervalMs параметр (уже есть) |
| `android/app/build.gradle.kts` | versionName = "0.93.0-dev" |
| `web/templates/index.html` | v0.93.0-dev |
---
## 3. Валидация speed-test (v0.94.0-dev)
### Проблема
Если ELM327 плохо вставлен в OBD-разъём (контакт болтается), замеры скорости — мусор: часть команд падает с `(err)`, время прыгает от 10ms до 900ms. Сохранять такой профиль нельзя.
### Решение
`ElmChecker.kt``measureResponseTime()` возвращает `SpeedTestResult`:
```kotlin
data class SpeedTestResult(
val perPidAvg: List<Int>,
val batchTime: Int,
val reliable: Boolean,
val message: String
)
```
**Критерии отбраковки (reliable=false):**
1. Любой замер отклоняется от среднего по своему PID >50%
2. Любая команда вернула `(err)` или пустой ответ
3. Все замеры <20ms (ELM не отвечает, мусор)
При `reliable=false` — профиль **не сохраняется**, интервал 250ms по умолчанию.
### Quick-check при каждом connect
`ElmChecker.kt` — добавлен `quickCheck(): Int?`:
```
При каждом клике на светофор ELM:
1. Загрузить профиль с сервера
2. Послать 010C (RPM), замерить время
3. Сравнить с профилем:
- расхождение <50% → "✅ ELM стабилен: ~85ms (профиль 256ms)"
- расхождение >50% → "⚠️ Скорость ELM изменилась: было 256ms, сейчас ~510ms"
4. Если профиля нет → полный speed-test (3 PID × 3 раза)
```
### Принудительный перетест
Клик на 🔵 ELM → переинициализация → quick-check. Если нужно полностью перемерить — очистить `response_time_ms` в `device_profiles` на сервере.
---
## Итоговая логика
| Ситуация | При connect | При СТАРТ |
|----------|------------|-----------|
| Новый ELM (нет профиля) | Полный speed-test 3 PID × 3 → сохранить | Интервал из профиля |
| Знакомый ELM, контакт ок | Quick-check: "стабилен" | Интервал из профиля |
| Знакомый ELM, контакт плохой | Quick-check: "изменилась" | Интервал 250ms (дефолт) |
| После переподключения | Quick-check → сверка | Интервал из профиля если ок |
## Файлы (v0.94.0-dev)
| Файл | Что изменено |
|------|-------------|
| `android/.../ElmChecker.kt` | `SpeedTestResult`, `quickCheck()`, валидация |
| `android/.../MainActivity.kt` | Speed-test при инициализации, quick-check |
| `android/.../ServerClient.kt` | `getProfileResponseTime()`, `saveProfile()` |
| `api/db.py` | `response_time_ms` колонка, threading lock |
| `api/routes.py` | `PUT /api/v1/elm/profile/<mac>` |
| `android/app/build.gradle.kts` | versionName = "0.94.0-dev" |
| `web/templates/index.html` | v0.94.0-dev |
| `doc/history/2026-06-10.md` | Этот файл |
@@ -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,60 @@
# Мнение по анализу динамического сбоя ELM327
Дата: 2026-06-14
## Общая оценка
Анализ написан правильно. Методология верная: исключение невозможного через уже проведённые эксперименты (паузы 4000 мс, автоподбор таймингов), затем ранжирование оставшихся гипотез. Главный вывод — проблема не в скорости, а в чтении потока — звучит убедительно.
---
## Что поддерживаю
**Гипотезы 1–3 (вероятность: высокая)** — расставлены верно.
Из трёх наиболее вероятных причин **непрочитанный `>` в InputStream** — самая классическая ELM327-ловушка. Если ScriptEngine завершает чтение по таймауту или по числу строк вместо `>`, это объясняет всё: первый запрос проходит, потому что `>` ещё не накапливается, второй ломается из-за хвоста. Это надо проверять первым.
**Buffer drain перед send, а не только после receive** — часто игнорируемое место. Если drain делается только после чтения, но перед отправкой нового запроса остаток `>` или пустая строка ещё лежат в буфере — это незаметно даже в логах, если читать только "полезные" байты.
---
## Что добавил бы
### 1. NO DATA / UNABLE TO CONNECT в динамике
В анализе не рассмотрен сценарий, когда в ходе динамики ELM вернул `NO DATA` или `UNABLE TO CONNECT`. Это вполне реально при смене контекста CAN. Если ScriptEngine на такой ответ зависает в ожидании данных или некорректно парсит следующий ответ — результат идентичен описанному сбою. Стоит явно проверить, как ScriptEngine обрабатывает негативные ответы ELM, и логировать их.
### 2. AT ST (тайм-аут ELM) может различаться между режимами
Если ElmChecker и ScriptEngine отправляют разные значения `AT ST` (или один вообще не устанавливает его), ELM сам будет обрезать ответ или отвечать с разной задержкой. При высокой нагрузке ECU (динамика) тайм-аут ELM по умолчанию (200 мс) может быть недостаточен, и ELM уйдёт в `NO DATA` раньше, чем ECU ответил. Нужно убедиться, что `AT ST FF` (максимальный) или фиксированное значение установлены одинаково в обоих путях.
### 3. Клон ELM327 vs оригинал
Клоны (особенно v1.5 китайские) имеют известный баг: при высокой частоте запросов они перестают выдавать `>` — промпт появляется только после задержки или вообще пропадает. Если адаптер — клон, нужно явно учесть это при трактовке сырых логов: отсутствие `>` может быть аппаратным поведением, а не ошибкой кода.
### 4. Конкурентный доступ — недооценённый риск
Гипотезе 5 (два потока на сокет) поставлена средняя вероятность, но в Android-проектах это случается чаще, чем кажется. Достаточно одного фонового alive-check, который читает тот же InputStream в момент динамического цикла. Стоит выйти не только на проверку thread id, но и на `synchronized`-блоки или single-threaded executor для всех операций с сокетом.
---
## Что менее убедительно
**Гипотеза 6 (порядок команд)** — оценка "средняя-низкая" верна, но её стоит проверять параллельно с гипотезами 1–3, не последовательно: это дёшево (достаточно дампа команд) и может мгновенно закрыть вопрос или исключить этот класс причин.
---
## Порядок расследования (скорректированный)
1. **Сырой RX/TX лог** с явным маркером `>` — сравнить статику и динамику. Первый приоритет.
2. **Проверить обработку негативных ответов** (`NO DATA`, `UNABLE TO CONNECT`) в ScriptEngine.
3. **Сравнить AT-последовательности** ElmChecker и ScriptEngine — весь init, включая `AT ST`.
4. **Убедиться в drain перед send**, а не только после receive.
5. **Thread id на каждый read/write** — исключить второй consumer.
6. **Дамп команд** обоих режимов — закрыть гипотезу 6 параллельно с остальными.
---
## Итог
Анализ хороший. Главное не растягивать расследование на последовательное прохождение всех гипотез: сырой лог с маркером `>` и лог негативных ответов ELM — два дешёвых эксперимента, которые скорее всего сразу покажут, где рвётся синхронизация.
@@ -0,0 +1,28 @@
# Вопросы к Opus 4.8 — по результатам тестов 28.06.2026
## Контекст
Протестирован канонический протокол на клоне ELM327 v1.5 (заведённый двигатель).
Результаты: протокол стабилен (16/16), НО клон залипает после ~10 команд —
начинает возвращать один и тот же ответ `410405\n7F0112` на ВСЕ команды,
включая AT (ATRV возвращает тот же OBD-ответ вместо напряжения).
Единственный способ восстановления — физическое извлечение из OBD.
## Вопросы
1. **Природа залипания.** Это известный баг клонов v1.5 или проблема конкретного экземпляра? Что именно залипает: внутренний буфер ELM, CAN-контроллер, или сам чип перестаёт принимать новые команды?
2. **Детект залипания.** Как программно определить что ELM залип? Проверять что ответ не меняется 3+ команды подряд? Сравнивать ATRV до и после цикла?
3. **Паузы между циклами.** Какая минимальная пауза предотвращает залипание? Достаточно ли 2-3 секунд между циклами по 4 PID? Зависит ли от числа PID в цикле?
4. **Auto-recovery без извлечения.** Есть ли программный способ вывести клон из залипания? ATPC? ATWS? ATZ? Или только обесточивание?
5. **Оптимальный размер цикла.** При паузе 2-3 сек между циклами — сколько PID оптимально в одном цикле? 2? 4? 6?
6. **Влияние оборотов двигателя.** Залипание зависит от нагрузки на CAN-шину? При высоких оборотах (3500+) залипает быстрее?
7. **Замена адаптера.** Если клон v1.5 принципиально нестабилен — какой адаптер рекомендовать? Оригинальный ELM327? v2.1 клон? OBDLink?
8. **Стратегия для продакшена.** Учитывая что 50%+ пользователей будут с клонами — какую стратегию выбрать: адаптироваться под клонов (медленный сбор) или требовать оригинал?
@@ -0,0 +1,67 @@
# Opus — переанализ фаз 06 (28.06.2026)
Запрос: doc/opus-recheck-request-2026-06-28.md
---
## Вердикт
**Можно запускать на реальном ELM327 v1.5** для стендовых тестов с одним телефоном.
Логика протокола корректна. Один баг очереди надо закрыть до перехода на `gunicorn -w 4`.
---
## 1. ElmActor.kt — корректен, deadlock нет ✅
`init()` сабмитит `elm.init()` один раз и ждёт `.get(30s)`. Внутри `elm.init()` вызывает `elm.sendCommand("ATI")` прямым вызовом (не через `executor.submit`), то есть на том же потоке actor'а — рекурсивной постановки в очередь нет → deadlock невозможен. `isClone()` читает `@Volatile` поле напрямую — видимость корректна.
**Замечание (low):** при таймауте `.get(30, SECONDS)` Future не отменяется — задача `elm.init()` продолжает крутиться на executor'е, и следующий `send()` встанет за ней в очередь. Худший случай init по сумме `tryRead` ≈ 23с (ATSP0 один даёт до 10с) — близко к лимиту 30с. Рекомендация: `future.cancel(true)` в `catch(TimeoutException)` либо поднять лимит до 45с.
---
## 2. ElmProtocol.kt (raw) — порядок init верный, но есть двойной ATI ⚠️
Канонический порядок правильный: `ATE0→ATL0→ATS0→ATI→detectClone→[ATST96|ATAT1]→ATSP0`. Эхо выключается первым. Обработка двойного ответа ATWS корректна во всех ветках recovery/STOPPED/BUS ERROR.
Проблемы:
- **Двойной ATI (medium):** первый `write("ATI"); tryRead; drainInput()` бесполезен. Затем ещё раз `sendCommand("ATI")`. Python-версия сделана правильно (один `_exec("ATI")`).
- **MAX_RETRIES=6 даёт потолок 600мс, не 2000 (medium):** 6 шагов по +20 = 600мс. Если медленный ЭБУ требует >600мс — всё равно ошибка. Комментарий вводит в заблуждение.
- **exec() не перешлёт команду на ретрае:** пишет `cmd` один раз, при таймауте только перечитывает буфер. Для ELM штатно, но при реальном `NO DATA` повторное чтение не поможет.
---
## 3. raw_elm.py — SQLite-очередь: dequeue НЕ атомарен ❌ (главное)
`GET /api/v1/elm/raw/cmd` делает два отдельных стейтмента без guard'а. При `-w 4` два воркера могут выбрать одну строку → команда уйдёт на ELM дважды.
**Фикс:** `UPDATE ... SET status='sent' WHERE id=? AND status='pending'` + проверка `rowcount == 0`.
Прочие замечания:
- **Рост таблицы:** `command_queue` не чистится (старый код держал 500). Добавить `DELETE WHERE responded_at < datetime('now','-1 day')`.
- **`device_id="unknown"`:** несколько телефонов сольются в одну очередь.
- **`GET /response`:** при всплеске может вернуть ответ не на запрошенную команду (для пошагового relay'я ок).
---
## 4. Python protocol.py — паритет с Kotlin ✅
Тот же порядок, один ATI, ATST96 для клона, `try_read(500)` как drain после каждой AT-команды.
---
## 5. Безопасность (фаза 0) ✅
`api_key → env LLM_API_KEY`. Проверить что старый ключ отозван у провайдера.
---
## Итоговый список к исправлению (по приоритету)
1.**Атомарный dequeue** — guard `AND status='pending'` + `rowcount`
2. ⚠️ **Двойной ATI** в `ElmProtocol.kt init()` — убрать первый `write("ATI")`
3. ⚠️ **MAX_RETRIES/комментарий** — потолок 600мс, не 2000
4. **Ретеншн** `command_queue` + фильтрация `device_id`
5. **Отмена Future** при таймауте init в `ElmActor`
6. **Отозвать старый LLM-ключ** у провайдера
Пункты 2–5 не блокируют стендовый прогон. Dequeue-гонку держать в голове при `-w 4`.
@@ -0,0 +1,31 @@
# Opus — переанализ после фаз 0–6
Дата: 2026-06-28
Предыдущий анализ: doc/history/opus-plan-analysis-2026-06-28.md
## Что изменилось
Твой план из 6 фаз реализован в ветке `opus-fixes`. Изменённые файлы:
### Android (:raw — тестовый модуль, пакет ru.elmer.raw)
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/ElmProtocol.kt` — канонический init (ATE0→ATL0→ATS0→ATI→detectClone→ветвл→ATSP0), ATST96 для клонов, MAX_RETRIES=6, @Volatile state, drain после sendCommand, handle STOPPED/NO DATA/UNABLE
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/ElmActor.kt` — single-thread executor вокруг ElmProtocol (новый файл)
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/RawRelayService.kt` — переведён на ElmActor
### Сервер (Python)
- `/home/naeel/elmer/obd/protocol.py` — канонический init (тот же порядок, clone detect, drain после send)
- `/home/naeel/elmer/api/raw_elm.py` — полная переработка: SQLite command_queue вместо глобальных переменных (gunicorn-safe, -w 4)
- `/home/naeel/elmer/api/db.py` — таблица command_queue
- `/home/naeel/elmer/api/scripts.py` — build_dynamic_script: 6 PID, interval_ms 1200
- `/home/naeel/elmer/config.yaml` — api_key → env LLM_API_KEY (безопасность)
### НЕ тронуто
- `:app` модуль (ru.elmer.client) — весь старый код без изменений
## Что нужно
1. **Валидация** — нет ли новых ошибок, противоречий, race conditions в новом коде
2. **ElmActor.kt** — корректна ли реализация single-thread executor? Нет ли deadlock при init()?
3. **ElmProtocol.kt (raw)** — правильный ли порядок init? Корректна ли обработка двойного ответа ATWS?
4. **raw_elm.py** — корректна ли SQLite-очередь? Атомарен ли dequeue?
5. **Общая оценка** — можно ли с этим кодом запускать тесты на реальном ELM327 v1.5?
@@ -0,0 +1,79 @@
# Ответы Opus и Sonnet — залипание клона ELM327 v1.5
Дата: 2026-06-29
---
## Opus 4.8
### 1. Природа залипания
Прошивка-эмулятор (MCU имитирует команды, нет настоящего чипа). Под нагрузкой буфер ответа переполняется, MCU зацикливается, отдавая последний кадр (`7F0112`) на всё. CAN-контроллер жив, мёртв эмулятор. UART отвечает, но игнорирует ввод.
### 2. Детект залипания
AT-команда возвращает OBD-кадр = залип. `ATRV` без `V`/напряжения = залип. 3+ одинаковых hex-ответа на разные PID. `7F` на PID 0100 — флаг.
### 3. Паузы
2-3 сек снижают вероятность, но не устраняют. Залипание зависит от **суммарного числа команд**, а не интервалов.
### 4. Auto-recovery
`ATWS`/`ATZ` иногда оживляют, но если завис UART-приём — команды не дойдут. **90% случаев — только обесточивание.** Алгоритм: ATWS → 2 сек → ATRV для проверки. Не сработало → требовать переподключения.
### 5. Оптимальный размер цикла
**2-3 PID** на цикл при паузе 2-3 сек. 4 уже на грани.
### 6. Влияние оборотов
CAN-нагрузка ускоряет залипание. 3500+ → больше трафика → раньше переполнение.
### 7. Замена адаптера
OBDLink SX/MX+ — лучший. v2.1 клон — миф, тот же эмулятор.
### 8. Стратегия
Гибрид: адаптироваться (2-3 PID, паузы, детект+recovery), детектить тип, предупреждать, не блокировать.
---
## Sonnet
### 1. Природа залипания
Баг клонов v1.5 (CH340/PIC18F). Залипает CAN-контроллер внутри клона.
### 2. Детект залипания
`ATRV` до/после цикла. 2-3 одинаковых ответа подряд.
### 3. Паузы
2-3 сек помогают. Минимум 1.5 сек. 6+ PID → 3-5 сек.
### 4. Auto-recovery
`ATZ` — надёжный. `ATWS` — мягче. `ATPC` — иногда. Последовательность: ATPC → 500ms → ATZ → переинит.
### 5. Оптимальный размер цикла
**4 PID** — хороший баланс. 6 рискованно. 2 безопасно.
### 6. Влияние оборотов
Да, прямое. Холостые — залипает позже.
### 7. Замена адаптера
OBDLink EX/MX+ → оригинал ELM327 v1.4b/v2.1 → клоны v2.1 лотерея.
### 8. Стратегия
Медленный режим (4 PID, 3 сек) как default. Детект + авто-ATZ. Предупреждение в UI.
---
## Моё мнение
**Opus точнее** в трёх ключевых пунктах:
1. **Природа залипания** — Opus правильно указывает на эмулятор/прошивку, а не CAN-контроллер. Это объясняет почему AT-команды тоже ломаются.
2. **Auto-recovery** — Opus реалистичнее: 90% только обесточивание. Соннет излишне оптимистичен про ATZ. Наш опыт подтверждает Опуса — ATZ не помогал.
3. **Размер цикла** — Opus консервативнее (2-3 PID). С учётом что клон залип после ~10 команд, 2-3 PID на цикл безопаснее.
**Соннет практичнее** в одном: 4 PID + 3 сек как default для продакшена. Но с учётом рисков, 2-3 PID + детект залипания + авто-предупреждение — более надёжный путь.
**Что берём в работу:**
- Детект залипания через `ATRV` (без `V` = залип)
- Циклы по 3 PID с паузой 2-3 сек
- Auto-recovery через ATWS → проверка ATRV → если нет → сообщение пользователю
- Предупреждение в UI: «Адаптер нестабилен, рекомендуем OBDLink»
+92
View File
@@ -0,0 +1,92 @@
# ELM327 Relay — хронология ошибок (АРХИВ)
> **Устарело**. Актуальный документ: `doc/failures-journal.md`
## Дата: 2026-07-04 | Версия на момент написания: v0.3.7-dev
---
## Текущая версия
**v0.3.7-dev** (commit `3e4eba7`, ветка `opus-fixes`)
---
## Что работает (v0.3.5-dev, тест 13/15)
- AndrOBD-совместимый init: `ATSP0 → ATI → ATS0 → ATL0 → ATE0`
- Клон v1.5: ATAT1 и ATST пропускаются
- `handle()` **без AT-команд** — только state tracking (как AndrOBD)
- `recover()` — только сброс state в READY
- 87% успешных PID-команд после прогрева
## Что НЕ работает
- **Буферный сдвиг после init**: первые 1-2 команды возвращают мусор/пустоту
- **STOPPED от ELM**: протокол останавливается, нужен перезапуск
- **Двигатель заглушен**: PID не работают, только ATRV
---
## ВСЕ ошибки (хронология)
### Ошибка 1: Freeze detection через ATRV (v0.3.0-dev)
**Что сделано**: каждые 10 команд слали ATRV для проверки залипания клона.
**Почему ошибка**: ATRV ломает синхронизацию команд. Ответ "12.6V" попадает в буфер и читается как ответ на следующий PID.
**Исправлено**: удалено в v0.3.2-dev.
### Ошибка 2: AT-команды в handle() (v0.1.xv0.3.3-dev)
**Что сделано**: handle() при STOPPED/UNABLE/BUS_ERROR слал ATPC→ATWS→ATSP0.
**Почему ошибка**: AT-команды отправляются внутри обработки ответа текущей команды. Их ответы загрязняют буфер для следующей команды.
**Исправлено**: удалено в v0.3.5-dev. handle() теперь только меняет state.
### Ошибка 3: AT-команды в recover() (v0.1.xv0.3.3-dev)
**Что сделано**: recover() слал ATPC→ATWS→ATSP0→ATE0 при state=ERROR/DISCONNECTED.
**Почему ошибка**: recover() вызывается из sendCommand() перед отправкой команды. AT-ответы могут не успеть полностью прийти до отправки PID.
**Исправлено**: упрощён в v0.3.3-dev. Только сброс state и timeout.
### Ошибка 4: Дренаж 0100 после init (v0.3.6-dev)
**Что сделано**: попытка поглотить буферный сдвиг отправкой 0100 сразу после init.
**Почему ошибка**: дренаж сам вызывает сдвиг буфера и дезориентирует клон.
**Исправлено**: удалено в v0.3.7-dev.
### Ошибка 5: Recovery ATSP0 в relayLoop (v0.3.5-devv0.3.7-dev)
**Что сделано**: после STOPPED или пустого ответа — ATSP0 для перезапуска протокола.
**Почему ошибка**: ATSP0 через sendBlocking блокирует single-thread executor. Последующие команды ждут в очереди, тест видит таймауты.
**Текущий статус**: всё ещё в коде v0.3.7-dev.
### Ошибка 6: Тест без device_id в /response
**Что сделано**: тестовый скрипт вызывал `/response?wait=3` без device_id.
**Почему ошибка**: сервер возвращал ответы от чужих устройств/сессий.
**Исправлено**: сервер (raw_elm.py) обновлён — `/response` принимает `device_id` параметр. Тест обновлён.
### Ошибка 7: Тест без обновления seq
**Что сделано**: тест всегда слал `seq=0` в `/response`.
**Почему ошибка**: сервер возвращал один и тот же ответ многократно.
**Исправлено**: тест обновляет seq после каждого ответа.
### Ошибка 8: Версия на сайте не обновлялась
**Что сделано**: деплой менял HTML только в `/opt/elmer/web/templates/`.
**Почему ошибка**: Flask использует `/opt/elmer/templates/index.html`.
**Исправлено**: деплой теперь обновляет оба файла.
---
## Отличия от AndrOBD
| | AndrOBD | Наш код |
|---|---|---|
| Архитектура | Асинхронный (поток читает → handleTelegram) | Синхронный (sendCommand → ждать ответ) |
| write() | Не дренирует перед отправкой | drainInput() перед каждым write() |
| Init | ATSP0→ATAT1→ATS0→ATL0→ATE0 | ATSP0→ATI→ATS0→ATL0→ATE0 |
| ATI | Не используется | Для детекта клона v1.5 |
| handle() | Только state tracking | Только state tracking ✅ |
| Ошибки | Не шлёт AT-команд из handle() | Не шлёт AT-команд ✅ |
---
## Ключевые файлы
| Файл | Состояние |
|------|-----------|
| `ElmProtocol.kt` | handle() без AT-команд, recover() пустой |
| `RawRelayService.kt` | relayLoop с recovery ATSP0 |
| `ElmActor.kt` | Single-thread executor |
| `api/raw_elm.py` | /response с device_id фильтром |
@@ -0,0 +1,396 @@
# Automotive Sensing and Actuators
> Источник: [MPScholar — Monolithic Power Systems](https://www.monolithicpower.com/en/learning/mpscholar/automotive-electronics/automotive-sensing-and-actuators)
> Дата сохранения: 2026-06-10
---
## Содержание
1. [Introduction to Automotive Sensors and Actuators](#1-introduction-to-automotive-sensors-and-actuators)
2. [Types and Functions of Sensors in Automotive Systems](#2-types-and-functions-of-sensors-in-automotive-systems)
3. [Types and Functions of Actuators in Automotive Systems](#3-types-and-functions-of-actuators-in-automotive-systems)
4. [Power Management for Sensors and Actuators](#4-power-management-for-sensors-and-actuators)
5. [Integration and Interfacing of Sensors and Actuators](#5-integration-and-interfacing-of-sensors-and-actuators)
---
## 1. Introduction to Automotive Sensors and Actuators
### The Role of Sensors and Actuators in Modern Vehicles
A new era of unheard-of performance, safety, and control in automobiles has begun with the introduction of sensors and actuators in automotive engineering. The future of mobility can be understood by comprehending the complex functions that these devices play, especially at a time when we are on the verge of a revolution in transportation.
#### Overview of Vehicle Automation and Control
The 21st-century automobile is changing from a mechanical device to an extremely complex electrical system on wheels. This change has been made possible in large part by the growing integration of actuators and sensors, which work together to enhance vehicle functioning.
- **Role of Sensors:** In essence, sensors are the eyes and ears of a vehicle. They keep an eye on a number of variables, including proximity, temperature, acceleration, and speed. Numerous control systems rely on this data to provide them with real-time information about the vehicle and its surroundings.
- **Role of Actuators:** If sensors are the information gatherers, actuators are the doers. Actuators receive signals and respond with specified actions, including changing the air-fuel ratio in the engine, tightening up the suspension, or even applying the brakes. They convert electrical information into mechanical action, directly influencing and controlling a variety of vehicle components.
#### Improving Safety, Efficiency, and Performance
The ultimate goal of sensor and actuator integration is to improve driving in three critical areas: performance, efficiency, and safety.
- **Safety Enhancements:** In order to provide power to advanced driver-assistance systems (ADAS), sensors such as radar, lidar, and cameras collaborate with one another. Meticulous sensor input and actuator reaction enable features like automated emergency braking, adaptive cruise control, and lane-keeping assistance. Through anticipatory threat detection and proactive measures, these technologies significantly lower accident rates and save lives.
- **Efficiency Optimization:** In today's automotive world, fuel economy and pollution management are critical. Onboard computers can modify combustion settings due to sensors that track pollutants and engine data. Actuators then put these adjustments into practice, maximizing fuel efficiency and lowering dangerous emissions. In a similar vein, sensors aid in the best possible battery utilization in electric cars, guaranteeing optimal range and longevity.
- **Performance Upgrades:** Today's drivers need a car that is strong, nimble, and responsive. Sensors evaluate performance metrics like grip, acceleration, and aerodynamic drag through continuous feedback loops. Actuators then modify components such as the suspension, engine, and gearbox to improve the vehicle's performance and provide for a thrilling ride.
To sum up, the integration of actuators and sensors in contemporary automobiles has completely reshaped the concepts of automotive engineering. These elements will become even more crucial as we approach the future of autonomous driving and smart transportation, spurring innovation and setting new standards for performance, safety, and efficiency.
### Basic Principles of Sensing and Actuation
The two main pillars that support the current vehicle control system are actuation and sensing. The sophisticated and sensitive behavior of today's cars, which allows them to easily interact with constantly changing environments, depends on both of these components.
#### Sensing as Information Gathering
In the context of automobiles, sensing can be conceptualized as the means by which the vehicle perceives its internal states and external environment. Similar to how our senses of sight, touch, and hearing feed us vital information about the world around us, automobile sensors pick up on particular factors that affect how well vehicles operate.
- **Types of Sensors:** Sensors vary widely based on their functional requirement. Common varieties include position sensors (for crankshaft or throttle position), temperature sensors (for engine and interior conditions), pressure sensors (in tire monitoring systems or fuel lines), and more sophisticated devices (such as cameras and radars for ADAS functions).
- **Data Acquisition:** Every sensor operates on the principle of converting a physical quantity into an electrical signal. Electronic control units (ECUs) interpret and analyze these electrical impulses, making real-time analysis possible. For this reason, this conversion is essential.
- **Feedback Mechanism:** Continuous data collection guarantees that a feedback loop is maintained at all times, which in turn supplies the control systems of the vehicle with the most recent information. This ongoing cycle enables the behavior of the vehicle to be improved and adjusted.
#### Actuation as Control Execution
Actuation takes over to make the required adjustments after the sensors have collected the crucial data. Actuators essentially function as the vehicle's "muscles," translating the electrical impulses that are processed back into motion.
- **Types of Actuators:** Actuators in vehicles are diverse, including components like fuel injectors (which control fuel delivery), electric motors (steering, braking, or throttle control), and solenoids (for valve operation or gear shifts).
- **Signal Interpretation:** The ECUs of the car send signals to the actuators, which decipher the sensor data. These signals specify the precise action that the actuator must do in order to accomplish the intended result.
- **Responsive and Adaptive Actions:** Vehicles that use actuators can be made to be both responsive and adaptable. When an obstruction is detected, responsive actions take rapid action, such as automated braking. Adaptive actions, like adaptive cruise control, which modifies vehicle speed based on traffic circumstances, change over time based on continuous sensor data.
In conclusion, the modern vehicle's intelligence is defined by the combination of sensing and actuation. Actuators implement the necessary modifications to maximize safety, performance, and efficiency, while sensors offer a thorough understanding of the surroundings and the condition of the vehicle.
### Historical Development of Automotive Sensors and Actuators
When one looks at the realm of sensing and actuation, the evolution of the automobile is a fascinating tapestry of engineering achievements and discoveries.
#### Evolution of Sensing Technologies in Vehicles
The early autos' basic mechanical and electro-mechanical systems are where sensing in cars first appeared.
- **Mechanical Era:** The nascent stages of automotive development predominantly employed mechanical systems. An example of an early speedometer was a cable-driven device that sent speed through a rotating cable and was directly attached to the gearbox.
- **Electro-Mechanical Onset:** Transitioning into the 20th century, electro-mechanical components began surfacing. For instance, bimetallic strips and Bourdon tubes were utilized in temperature and oil pressure gauges, respectively.
- **Electronic Revolution:** Thanks to developments in semiconductor technologies, electronic sensing saw a boom after the 1970s. The advent of sensors such as oxygen sensors, manifold absolute pressure sensors, and throttle position sensors during this era laid the foundation for advanced engine management and electronic fuel injection systems.
- **Advent of ADAS and Connectivity:** Advanced driver-assistance systems (ADAS) were introduced in the late 20th and early 21st centuries. Advances in autonomous driving, collision avoidance, and lane departure warning systems were made possible by technological innovations, including radar, LIDAR, and cameras.
#### Trends and Future Directions
The scope of sensing and actuation in the automobile industry is expanding in step with the constant advancement of technology.
- **Miniaturization and Integration:** Miniaturization is a trend in modern sensors, making them smaller without compromising on functionality. Integrated sensor systems are increasingly widely used; they combine several sensing functions into a single unit.
- **Self-Diagnostics and Predictive Maintenance:** The upcoming generation of sensors and actuators are not only operational devices but also self-aware. They are able to keep an eye on their performance, anticipate malfunctions, and notify the driver or the car's central system of possible problems.
- **Holistic Vehicle Sensing:** An automobile that senses its environment holistically is the automotive industry's vision of the future. To ensure peak performance, safety, and comfort, a confluence of internal and external sensors must cooperate.
- **Actuators in Electric and Autonomous Vehicles:** With electric cars (EVs) gaining pace, specialized actuators customized for EVs are on the horizon. Actuators will also become increasingly important as autonomous driving technologies advance, guaranteeing precise, split-second responses to sensor input.
- **Material Innovations:** Actuators can now respond faster, with greater precision, and for longer periods of time thanks to new materials including shape-memory alloys and piezoelectric compounds.
---
## 2. Types and Functions of Sensors in Automotive Systems
### Classification of Automotive Sensors
Automotive sensors are essential to the smooth operation of modern automobiles. These sensors provide information about numerous vehicle parameters to the Electronic Control Unit (ECU) so that safety, efficiency, and performance are maximized. They do this by translating physical quantities into electrical impulses. These sensors can be categorized along two main lines: first, by the physical characteristics they measure, and second, by the underlying technology they use.
#### Classification Based on Physical Properties
- **Pressure Sensors:** These devices identify and gauge the pressure of the car's various fluids, including air, fuel, and oil. They make sure that the pressures stay within predetermined limits for ideal functioning and are frequently utilized in fuel injection and brake systems. They are predicated either on differential pressure sensing or absolute pressure sensing theory.
- **Temperature Sensors:** Integral to engine management, temperature sensors monitor the engine's coolant, oil, and air temperatures. By doing this, possible harm is avoided and the engine is guaranteed to run within a safe temperature range. Furthermore, temperature sensors are integrated into all power electronic controllers so that, in the event that the temperature rises above safe limits, the power can be derated or switched off.
- **Position Sensors:** These sensors determine where different parts are located. Examples are the Camshaft/Crankshaft Position Sensors, which help with engine timing, and the Throttle Position Sensor (TPS), which senses the position of the throttle in internal combustion engines.
- **Speed Sensors:** These sensors detect the rotational speed of the wheels and axis and are frequently used in the Anti-lock Braking System (ABS) and Transmission Control Units (TCU). This information enables the ECU, for example, to make real-time changes to prevent wheel lockup while braking.
- **Level Sensors:** These sensors keep an eye on the fluid levels in a variety of reservoirs, such as engine oil sump pumps, braking fluid reservoirs, and gasoline tanks.
#### Classification Based on Technology
- **Capacitive Sensors:** When a physical quantity varies, they work on the basis of capacitance alteration. In capacitive proximity sensors, for example, an object's approach modifies the capacitance, which is then detected. Certain fluid-level sensors rely on the fluid's capacitance.
- **Ultrasonic Sensors:** These sensors produce ultrasonic waves and are mostly utilized in parking assistance and obstacle detection. The sensor measures the distance by measuring the time it takes for the waves to reflect back after hitting an obstruction and receiving the information.
- **Infrared Sensors:** These sensors use the infrared spectrum to detect obstacles and provide night vision, particularly in low-light situations.
- **Piezoelectric Sensors:** These sensors produce a voltage in response to mechanical stress. Engine knock sensors use this feature to identify engine knock or pinging.
- **Hall-Effect Sensors:** Operating on the principle of the Hall Effect, these sensors can detect magnetic fields and are commonly employed for position detection, notably in the context of camshaft and crankshaft positions.
- **Resistive Sensors:** These sensors, such as temperature sensors, whose resistance varies inversely with temperature, alter their resistance in response to the physical quantity they detect.
### Applications of Sensors in Automotive Systems
#### Engine Management and Control
The engine management system's core components are the sensors, they enable peak performance, fuel economy, and emission control:
- **Fuel/Air Mixture Control:** By measuring the amount of oxygen in exhaust gasses through the use of oxygen sensors installed inside the exhaust system, the engine control module is able to modify the fuel-air mixture for the best possible combustion.
- **Ignition Timing:** Crankshaft and camshaft position sensors help establish the engine's phase and speed. This information helps the engine control unit (ECU) to time the spark for combustion exactly.
- **Cooling System:** Temperature sensors monitor the engine's coolant temperature. If the temperature crosses a defined threshold, the ECU can modify the functioning of the cooling fan or communicate a potential overheating issue to the driver.
- **Turbocharger Control:** Pressure sensors are used in turbocharged engines to monitor the boost pressure and ensure that it remains within the safe operating parameters established for the engine.
#### Safety Systems
Safety is fundamental in vehicle design, and sensors play a critical part in numerous safety-enhancing systems:
- **Airbag Deployment:** Accelerometers detect fast deceleration characteristics of a collision. The sensor alerts the airbag control unit to activate the airbags, which cushion the occupants and lower the possibility of injury in the event of a large accident.
- **Anti-Lock Braking System (ABS):** Wheel speed sensors constantly track the rotational speed of each wheel in the anti-lock braking system (ABS). The ABS adjusts brake pressure to prevent wheel lockup when it senses it is about to happen, preserving steering control.
- **Traction Control System:** This system detects when one or more wheels lose grip by using wheel speed sensors. In order to regain traction, the ECU can then lower engine power or apply brake force to particular wheels.
- **Collision Sensors:** These are particularly crucial for battery electric vehicles (BEVs), as they ensure that all high-voltage parts are deactivated in the event of a collision. This is accomplished via the collision sensor circuit, which modifies the crash signal state that high-voltage components expect in the case of a crash and ensures that any circuits that may have become accessible to persons due to the collision and vehicle damage are de-energized.
#### Driver-Assistance Systems
- **Parking Assistance:** This is provided by ultrasonic sensors installed all around the car to identify nearby obstructions. By giving the driver input regarding the distance to objects, these sensors help make parking in confined places easier to handle.
- **Lane-Keeping Assistance:** Roadside lane markers are detected by optical or infrared sensors. Depending on how sophisticated the system is, it may alert the driver or even take corrective action if it detects an inadvertent lane departure without signaling.
- **Adaptive Cruise Control:** This technology keeps a safe following distance between itself and the car in front of you using radar or LIDAR sensors. The mechanism automatically lowers speed to preserve the predetermined gap if the car in front of it slows down.
- **Blind Spot Detection:** This system lowers the likelihood of side-swiping accidents by alerting drivers to cars in their blind spots, usually through the use of radar or ultrasonic sensors.
### Key Specifications and Performance Criteria
#### Accuracy and Resolution
- **Accuracy:** This indicates the degree to which the sensor's reading agrees with the real value. A temperature sensor that is precise to within 0.5°C of the real temperature, for example, is more reliable than one that could be 2°C off.
- **Resolution:** The smallest change in the quantity being measured that causes the related output signal to alter noticeably is referred to as this. For example, a pressure sensor is said to have 0.01 psi resolution if it can measure variations as small as 0.01 psi.
#### Sensitivity and Range
- **Sensitivity:** This is defined as the sensor's response, or change in output, to a change in the input or amount being measured.
- **Range:** The physical quantity that the sensor is capable of measuring is shown, along with its minimum and maximum values.
#### Environmental Considerations
- **Temperature Stability:** Because cars operate in a variety of conditions, sensors need to be able to function accurately and consistently across a wide temperature range.
- **Resistance to Contaminants:** To ensure lifetime and reliable operation, automotive sensors should be resistant to fuel, oil, dust, moisture, and other contaminants.
- **Vibration Resistance:** Cars can cause a lot of vibrations and shock, especially in rough terrain. For constant readings, sensors must be unaffected by these vibrations.
#### Type of Sensor Errors
- **Offset Error:** An ongoing inaccuracy injected into the sensor data.
- **Gain Error:** Errors proportionate to the input signal are called gain errors.
- **Drift Error:** Errors that gradually change over time.
- **Random Error:** Typically indicative of noise in the sensor circuit, random errors lack a clear pattern.
- **Quantization Error:** This kind of error is caused by the sensor's restricted resolution.
#### Fault Diagnostics
Modern car systems have built-in self-diagnostic features to keep an eye on the condition and performance of their sensors.
- **5V Output Sensors:** Sensors with a 5-volt output voltage range frequently use the lower voltage band (below 0.5V) and upper voltage band (above 4.5V) to indicate a fault.
- **Digital Temperature Sensors:** High safety-rated temperature sensors frequently display a false, implausible temperature value, such as -200°C, to signify that a chip internal problem has occurred.
#### Detection of Faults
- **Redundancy:** Making use of several sensors to make a single measurement.
- **Self-Test Mechanisms:** Modern sensors are equipped with self-test functions.
- **Plausibility Checks:** Comparing sensor outputs to established physical models to make sure they are consistent.
---
## 3. Types and Functions of Actuators in Automotive Systems
### Classification of Automotive Actuators
In automotive systems, actuators operate as a conduit between the physical actions occurring inside a car and the control systems. They convert incoming energy into motion in order to carry out commands.
#### Classification Based on Control Action
**Linear Actuators**
- **Description:** These actuators produce linear motion, usually in the form of push or pull actions.
- **Application:** An example of an application is the operation of the brake master cylinder, in which the hydraulic fluid is pushed through the system by the actuator to engage the brake pads.
**Rotary Actuators**
- **Description:** These produce rotational motion, which is usually expressed in terms of angles or whole revolutions.
- **Application:** An example of an application is the fuel injection system's throttle plate adjustment, where the actuator spins the plate to regulate airflow. The liquid-cooled systems pressure pump serves as an additional illustration.
#### Classification Based on Technology
**Electric Motors**
- **Description:** Produce motion by means of electrical energy. Their working principle is based on electromagnetic principles, in which motion is produced by a magnetic field created by current flowing through a coil.
- **Application:** One example of such application is electric power steering systems, which, in response to driver input, use motors to help in steering.
**Solenoids**
- **Description:** These are electromagnetic devices that, when powered on, create a regulated magnetic field. Subsequently, a plunger or rod experiences linear motion due to the magnetic field.
- **Application:** An example of an application is transmission shift control, in which a solenoid engages or disengages gears in response to commands from the driver or computer.
**Piezoelectric Actuators**
- **Description:** Use the piezoelectric effect. When mechanical stress is applied, some materials generate an electric charge. In contrast, these materials undergo a shape-changing process that results in mechanical motion when voltage is given to them.
- **Application:** Fuel injector systems in some sophisticated engines. Because of their high-frequency response, piezoelectric actuators can provide injections that are extremely rapid and precise.
### Applications with Actuators in Automotive Systems
#### Throttle Control
- **Role of Actuators:** The throttle actuator controls how much air enters the engine. In the past, this operation was mainly mechanical. On the other hand, "drive-by-wire" or electronic throttle control (ETC) systems are used in modern systems.
- **How It Works:** Rather than physically pulling a cable, depressing the gas pedal in an ETC system delivers an electrical signal. This signal is interpreted by an actuator at the throttle body, which then modifies the throttle plate to control engine airflow.
#### Transmission Shift Control
- **Role of Actuators:** In both automated and manual transmission systems, transmission actuators help with gear shifting.
- **How It Works:** Solenoid actuators in contemporary automatic transmissions decode electrical signals from the transmission control module. By regulating the hydraulic fluid flow to various transmission tunnels, these solenoids regulate which gear set is in operation.
#### Active Suspension Systems
- **Role of Actuators:** Active suspensions are cutting-edge devices that instantly adjust to changing road conditions and driving demands to improve handling dynamics and ride comfort.
- **How It Works:** The system uses a mix of actuators and sensors to identify cornering forces, vehicle speed, and road defects. Actuators quickly change the ride height or damper stiffness. They are typically electromagnetic or electro-hydraulic.
### Key Specifications and Performance Criteria
#### Force and Torque Capabilities
- **Definition:** Two essential indicators of an actuator's performance are force and torque. Torque, which is typically linked with rotary actuators, represents rotational force, whereas force is a push or pull action that is linear in nature.
- **Measurement:** Generally, torque is expressed in Newton-meters (Nm) or foot-pounds (ft-lb), while force is expressed in Newton's (N) or pounds-force (lbf).
#### Speed and Response Time
- **Definition:** Response time is the amount of time an actuator takes to begin moving after receiving a command, whereas speed is the fastest an actuator may move to reach its desired location.
- **Measurement:** For linear motions, speed can be stated in mm/sec, while for rotating actuators, it can be given in RPM. Milliseconds (ms) are commonly used to indicate response time.
#### Reliability and Durability
- **Definition:** Durability is the number of operational cycles an actuator can withstand before wearing out or malfunctioning, whereas reliability is the capacity to perform consistently over time without failure.
- **Measurement:** While durability may be described in terms of operating cycles or hours of operation under specific conditions, reliability is frequently measured using metrics like Mean Time Between Failures (MTBF).
---
## 4. Power Management for Sensors and Actuators
### Power Requirements for Sensors and Actuators
#### Operating Voltage and Current Ranges
- **Definition:** Specific voltage and current ranges are intended for the operation of each sensor and actuator.
- **Importance:** Staying within these parameters guarantees that the sensor or actuator operates as intended without running the risk of damage or malfunction.
- **Measurement:** Common operating voltages for automotive applications may be between 5V and 24V.
#### Power Consumption and Efficiency
- **Definition:** Power consumption measures the total amount of energy that a sensor or actuator uses over time. Efficiency quantifies how well a device transforms the power it consumes into useful output.
- **Importance:** Energy is a limited resource in automobiles, particularly in electric or hybrid versions.
- **Factors Affecting Consumption and Efficiency:** The device's design, the materials utilized, the working environment, and operation frequency.
- **Measurement:** For smaller devices, power consumption is commonly expressed in milliwatts (mW) or watts (W). Efficiency is the ratio of usable power output to total power input, stated as a percentage.
### Power Optimization Strategies
#### Power-Saving Modes for Sensors
- **Sleep Mode:** In sleep mode, the sensor uses very little power and is largely inactive. It can become "awakened" when its purpose is required.
- **Idle Mode:** The sensor keeps working but at a reduced capacity, ready to go back to full operation when needed.
- **Interrupt-Driven Mode:** Until an external trigger or interrupt activates the sensor, it stays in low-power mode.
#### Always-Awake Sensors in Vehicles
- **Theft-Detection Sensors:** They keep a close eye out for any indications of tampering or illegal access.
- **Key Fob Detection Sensors:** These sensors are always on the lookout for signals from the key fob in cars with keyless entry systems.
#### Energy Efficient Actuation Techniques
- **Adaptive Control:** The actuator modifies its actions in response to immediate feedback.
- *Variable Displacement Pumps:* Modify the fluid flow rate in accordance with the system's present requirements.
- *Dynamic Brake Energy Recovery:* The energy generated during braking is recovered and transformed back into useful electrical energy.
- *Electric Motors with Load Sensing:* The motor can adjust its power output according to the required torque.
- **Pulse-Width Modulation (PWM):** Enables more precise control over the amount of energy utilized by altering the width of the electrical pulse delivered to the actuator.
- **Optimized Drive Circuits:** Energy efficiency can be achieved in the design of the electronic circuits that drive actuators.
- **Variable Load Sensing:** Certain sophisticated actuators have the ability to detect the load they are experiencing and modify their energy usage accordingly.
---
## 5. Integration and Interfacing of Sensors and Actuators
### Sensor and Actuator Interfaces
#### Analog vs. Digital Interfaces
**Analog Interfaces**
- **Nature:** Use a continuous signal that fluctuates in frequency or amplitude to transmit data.
- **Pros:** They offer a clear representation of a measured or controlled quantity and can be easy to use and reasonably priced.
- **Cons:** Limited range and susceptibility to noise interference. The connecting ECU must supply a distinct sensor ground specifically for that sensor.
- **Usage:** Commonly seen in simple sensors like pressure or temperature sensors.
**Digital Interfaces**
- **Nature:** Discrete signals, mostly binary (0s and 1s), are used to transmit data.
- **Pros:** They provide accurate and strong noise immunity.
- **Cons:** Their cost is usually higher than that of analog sensors. They require additional computational power from the DSPs and microcontroller interface.
- **Usage:** Common in contemporary automobile systems where accurate control and data collection are essential.
#### Communication Protocols for Sensors
**Inter-Integrated Circuit (I²C)**
- A packet-switched, single-ended, multi-master, multi-slave serial communication protocol. Frequently used to connect slower peripheral integrated circuits (ICs) to microcontrollers and processors.
- **Example:** Ambient light sensors in cars.
**Single-Edge Nibble Transmission (SENT)**
- A point-to-point protocol that allows sensor readings to be sent from a controller to a sensor. Designed with low power consumption and the fewest possible sensor connection pins.
- **Example:** Throttle position sensors.
**One-Wire**
- This protocol just needs one wire to communicate. Intended for low-speed data transmission.
- **Example:** Tire pressure monitoring sensors.
**Serial Peripheral Interface (SPI)**
- A synchronous serial communication protocol that selects the target device using a select line in addition to distinct clock and data lines.
- **Example:** High-speed gyroscopic sensors used in advanced stability control systems.
**Controller Area Network (CAN)**
- A common protocol for higher-level vehicle communications. Reliable, able to function in noisy settings, and appropriate for real-time applications.
- **Example:** Wheel speed sensors for ABS and traction control.
**Local Interconnect Network (LIN)**
- For non-critical sub-networks inside a car, a more affordable option to CAN.
- **Example:** Rain or light-detecting modules.
### Integration Challenges and Solutions
#### Ensuring Compatibility Between Components
**Challenge:** The variety of sensors and actuators that may originate from different manufacturers, different eras of technology, or different design paradigms.
**Solutions:**
- **Standardization:** Using standardized interfaces, voltages, and communication protocols (SAE, ISO standards).
- **ISO:** ISO 14229, ISO 15765 (vehicular communication), ISO 26262 (functional safety).
- **SAE:** SAE J1979 (OBD systems), SAE J1939 (heavy-duty communication).
- **Interfacing Modules:** Use interface modules or gateways that can translate between different protocols.
- **Unified Development Platforms:** Develop and test on the same platform or environment.
- **Comprehensive Documentation:** Keep detailed documentation for every component.
### Procedures for Sensors and Actuators
#### Sensor Calibration
- **Procedure:** Recording the sensor's reaction after subjecting it to a variety of known situations. The output is modified to match the anticipated values.
- **Example:** A temperature sensor may be subjected to a range of exact temperatures while modifications are made to guarantee that its output corresponds to the input values that are known.
#### Actuator Calibration
- **Procedure:** Change the control signal that is supplied to the actuator, measure its reaction, and make adjustments as needed to get the desired result.
- **Example:** To make sure a solenoid delivers the appropriate force or displacement for each level, it may be driven at different current levels. The correlation between current and displacement can be used as an integrated look-up table in the DSP or Microcontroller of the ECU.
---
*Сохранено с MPScholar (Monolithic Power Systems) — Automotive Electronics / Automotive Sensing and Actuators*
@@ -0,0 +1,113 @@
# Резюме проекта elmAI — 14 июня 2026
## Текущая версия: v1.18.0-dev
## Архитектура
**Сервер (Python/Flask):** `gitea.services.ngcloud.ru/Nail/elmer`, ветка `bugfix-2026-06-07`
- Хост: `obdai.ru` (5.172.178.213), доступ по SSH: `ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213`
- Репо на сервере: `/opt/elmer`, сервис `elmer` (gunicorn), nginx прокси
**Android (Kotlin):** `github.com/Repinoid/elmer-android`, ветка `bugfix-2026-06-07`
- APK собирается на сервере: `export ANDROID_SDK_ROOT=$HOME/android-sdk && cd /opt/elmer/android && ./gradlew clean assembleDebug`
- APK на сайте: `web/static/app-debug.apk``https://obdai.ru/elmer.apk`
## Что работает ✅
1. **Статическая диагностика** — одиночные PID (0104-011F), DTC, VIN — ИДЕАЛЬНО
2. **Speed-test** — замер задержек RPM/MAF/STFT при клике на 🔵 ELM (если нет профиля)
3. **Серверный тестовый скрипт**`GET /api/v1/script?mode=test`
4. **Авто-подбор таймингов**`POST /api/v1/test/next` — сервер получает результаты, увеличивает wait_ms если ошибок >20%
## Что НЕ работает ❌
1. **Динамический тест (СТАРТ/СТОП)** — ELM327 v1.5 замолкает после первых 1-2 команд. Статика работает, динамика нет. Причина не найдена.
## Ключевые файлы
### Сервер
| Файл | Что |
|------|-----|
| `api/routes.py` | `?mode=test`, `POST /api/v1/test/next`, авто-подбор |
| `api/scripts.py` | `build_test_script(wait_ms, pids, repeat)` |
| `api/db.py` | `device_profiles` таблица с `response_time_ms` |
| `api/ping.py` | ping/ping-llm эндпоинты |
### Android
| Файл | Что |
|------|-----|
| `elm/ElmProtocol.kt` | Стейт-машина AndrOBD. `MAX_RETRIES=3`, `state=ERROR` убран |
| `elm/ElmChecker.kt` | checkDevice, checkEcu, speed-test, quickCheck |
| `script/DynamicCollector.kt` | Оригинальный DynamicCollector (не используется сейчас) |
| `script/ScriptEngine.kt` | Выполнение скриптов с сервера, поддержка `wait` |
| `server/ServerClient.kt` | `downloadTestScript()`, `postTestNext()` |
| `ui/MainActivity.kt` | `startDynamicRecording()` — авто-подбор с сервера |
## Логика авто-подбора (v1.17+)
```
Пользователь: СТАРТ
Статика 9 PID (как обычно)
Цикл 1: GET /api/v1/script?mode=test → wait=2000ms, 2 PID (010C,0106), 8 повторов
↓ выполняет
↓ POST /api/v1/test/next {run:0, results:[...]}
↓ ответ: {done:false, message:"50% ошибок — увеличиваю до 2500ms"}
Цикл 2: wait=2500ms → выполняет → POST → ответ
... пока done:true или run≥5
```
## Деплой
### Только сервер (без APK):
```bash
cd /home/naeel/elmer
git add -A && git commit -m "..." && git push origin bugfix-2026-06-07
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 'cd /opt/elmer && git pull origin bugfix-2026-06-07 && sudo systemctl restart elmer'
```
### APK + сервер:
```bash
# Бамп версии в android/app/build.gradle.kts и web/templates/index.html
# Затем:
cd /home/naeel/elmer/android
git add -A && git commit -m "..." && git push origin bugfix-2026-06-07
cd /home/naeel/elmer
git add -A && git commit -m "bump vX.Y.Z-dev" && git push origin bugfix-2026-06-07
cd /home/naeel/elmer
tar czf /tmp/android-src.tar.gz --exclude='.git' --exclude='build' --exclude='.gradle' android/
scp -i ~/.ssh/naeel_vm_id_ed25519 /tmp/android-src.tar.gz naeel@5.172.178.213:/tmp/
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 '\
cd /opt/elmer && git pull origin bugfix-2026-06-07 && \
export ANDROID_SDK_ROOT=$HOME/android-sdk && export ANDROID_HOME=$ANDROID_SDK_ROOT && \
rm -rf android && tar xzf /tmp/android-src.tar.gz -C /opt/elmer/ && \
cd android && ./gradlew clean assembleDebug && \
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/'
```
## Нерешённая проблема
ELM327 v1.5 замолкает при последовательных OBD-командах. Статика (одиночные) — ок. Динамика (подряд) — пустые ответы. Уже пробовали:
- Разные паузы (250ms → 4000ms) — не помогает
- ATWS перед динамикой — делает хуже
- Убирали/возвращали drainInput() — v1.9 без drain хуже
- Retry в exec() — оригинальный AndrOBD код не помогает
- Разное количество PID — не помогает
- DynamicCollector → серверный скрипт — не помогает
Текущий авто-подбор должен найти рабочий интервал, но если даже 4000ms не помогает — проблема глубже таймингов.
## Конфигурация авто-подбора (api/routes.py)
- Старт: 2000ms
- Шаг: +500ms
- Макс: 5 циклов
- PIDs: 010C (RPM), 0106 (STFT)
- Повторов: 6
## Профиль ELM
Таблица `device_profiles` в `elmer.db`. MAC: `AA:BB:CC:11:22:33`. Был удалён старый мусорный профиль.
@@ -0,0 +1,40 @@
# Анализ: ELM327 динамический тест
## 1. exec() — retry с повторной write(cmd) — ГЛАВНЫЙ БАГ
При таймауте ELM уже отправил запрос в CAN-шину и ждёт ответа от ЭБУ. write(cmd) снова вызывает drainInput() — сбрасывает буфер с ответом, которого мы ждём — и шлёт команду повторно. ELM получает 010C пока обрабатывает предыдущий 010C → BUFFER FULL / зависание. 10 retry = 10 одновременных CAN-запросов. Статика не задевает этот путь, потому что там таймауты не случаются (пауза между командами — секунды).
## 2. Почему v1.9.0 (без drainInput) стало хуже
drainInput() в write() — единственный механизм синхронизации запрос/ответ. Без него:
- Ответ на команду N читается как ответ на команду N+1
- read() видит > из старого ответа — возвращает мусор, считает успехом
- К 3-му PID цикла батча сдвиг накопился: ответы не совпадают с командами
- BT-буфер на стороне ELM забивается необработанными данными → ELM перестаёт отвечать
В v1.3.0 drainInput() маскировал проблему retry: хотя бы буфер чистился перед каждой командой.
## 3. ATWS — нужен ли, сколько ждать
Нужен: сбрасывает SEARCHING..., очищает внутренние ошибки CAN-протокола ELM.
Проблемы текущего использования:
- Пауза 800 мс — мало. CAN-шина после warm start поднимается 700–1000 мс, плюс ATSP0 negotiate. Нужно 12001500 мс.
- После ATWS ELM сбрасывает настройки в дефолт: ATE1 (эхо ON), ATL1 (LF ON), ATS1 (пробелы ON). Код не восстанавливает ATE0/ATL0/ATS0 → read() начинает видеть эхо команды и переносы строк → парсинг ломается.
## 4. sleep(350) между PID — правильно?
Для ELM327 v1.5 — приемлемо. Адаптер не успевает переключаться быстрее 100–200 мс между разными PID (CAN frame turnaround). Но 350 мс не решает проблему, потому что рассинхрон возникает раньше — внутри retry в exec(). Пауза между командами маскирует, но не лечит.
## 5. Почему статика работает, динамика нет
Статика: пауза между командами — секунды (UI-обработка). ELM успевает ответить. Retry не срабатывает. Накопления сдвига нет.
Динамика: пауза 350–500 мс. При первом таймауте retry запускает цепочку дублей. Синхронизация батча ломается. Следующий батч начинается на сломанном состоянии.
## Итог: приоритет причин
1. exec() повторяет write(cmd) при таймауте — нельзя дублировать OBD-команды в CAN (первична)
2. Убрали drainInput() в write() — потеряна синхронизация запрос/ответ
3. После ATWS не восстанавливают ATE0/ATL0/ATS0 — парсинг ответов ломается
+43
View File
@@ -0,0 +1,43 @@
# Анализ причин отказа ELM327 v1.5 при динамическом опросе
Анализ кода `ElmProtocol.kt`, `DynamicCollector.kt` и `ElmChecker.kt` выявил ряд критических проблем, которые в совокупности приводят к "зависанию" адаптера ELM327 (особенно дешевых клонов v1.5) при переходе к быстрому циклу опроса.
## Ответы на вопросы
### 1. Почему ELM327 v1.5 замолкает после первых 2 ответов?
Основная причина — **десинхронизация и переполнение буфера**.
* **ATWS прямо перед циклом:** Команда `ATWS` (Warm Start) сбрасывает микроконтроллер. Ему требуется время на инициализацию (обычно 500-1000 мс). Код в `MainActivity` ждет всего 300 мс. Первые команды `010C` прилетают, когда ELM еще "просыпается" или находится в неопределенном состоянии.
* **Эффект домино в `exec()`:** Если первый PID в цикле (`010C`) не успел ответить вовремя, `exec` возвращает пустую строку, но ELM продолжает обработку. Следующий вызов `sendCommand` через `write()` делает `drainInput()`, удаляя запоздавший ответ, и посылает новую команду. Для клона v1.5 типична ситуация, когда он "захлебывается", если получает новую команду, не закончив передачу предыдущего ответа или символа `>`.
### 2. Может ли `drainInput()` в `write()` съедать ответ?
**Да, и это главная проблема надежности.**
Если ELM327 ответил на 50 мс позже таймаута, данные уже лежат в буфере Bluetooth-сокета. Вызов `write()` для следующей команды в цикле безусловно их очищает. В итоге `read()` следующей команды видит пустоту, провоцируя новые таймауты и ретраи. Происходит рассинхрон: приложение ждет ответ на команду B, а ELM (если не завис) шлет ответ на команду A.
### 3. Критична ли последовательность: static -> speed-test -> ATWS -> dynamic?
Последовательность перегружена сбросами.
* `ATWS` сбрасывает настройки `ATAT1`, `ATSP`, `ATL0` и т.д., которые были установлены в `init()`.
* После `ATWS` протокол может вернуться к `AUTO` (`ATSP0`), что заставляет ELM тратить время на "SEARCHING..." при первом же запросе `010C`. Это гарантированный таймаут в динамическом тесте.
### 4. Нужно ли переподключать ELM вместо ATWS?
Переподключать Bluetooth-сокет не обязательно, но **вместо `ATWS` лучше вызвать серию настроечных команд**, гарантирующих состояние:
1. `ATE0` (эхо выкл)
2. `ATL0` (переносы строк выкл)
3. `ATS0` (пробелы выкл) — крайне важно для скорости и предотвращения переполнения буфера.
4. `ATSP X` (принудительная установка протокола, найденного в `checkDevice`), чтобы исключить стадию поиска.
### 5. Как правильно реализовать динамический опрос для v1.5?
Для минимизации ошибок на медленных адаптерах:
1. **Убрать `Thread.sleep(350)` внутри цикла `for (step in steps)`.** Пауза должна быть только между *пакетами* (батчами) PID, если нужно ограничить частоту. Внутри батча команды должны идти максимально плотно: послал -> дождался `>` -> сразу следующий.
2. **Увеличить таймаут для v1.5.** 500 мс — это предел для v1.5. Первичный запрос (особенно после сброса) может занимать до 1500 мс.
3. **Оптимизировать `read()`:** v1.5 очень чувствителен к таймингам. Текущий `Thread.sleep(1)` в `read()` — это хорошо, но логика `drainInput` должна быть перемещена: чистить буфер нужно только один раз *перед стартом всего динамического теста*, а не перед каждой командой.
## Рекомендации по исправлению
1. **В `ElmProtocol.kt`:**
* Сделать `drainInput()` опциональным параметром в `write()` или убрать его из `sendCommand` по умолчанию.
* В `handle()` при получении `NODATA` или `ERROR` не делать `ATWS` мгновенно, так как это убивает сессию опроса.
2. **В `DynamicCollector.kt`:**
* Удалить `Thread.sleep(350)` в цикле `for`. Вместо этого полагаться на таймауты `ElmProtocol`.
3. **В `MainActivity.kt`:**
* Убрать `ATWS` перед стартом. Если нужен сброс — использовать `ATZ` и ждать 2 секунды, после чего заново прогнать весь `init()`.
* Перед запуском `DynamicCollector` зафиксировать протокол: `elmProto.sendCommand("ATSP" + currentProtocol)`.
+277
View File
@@ -0,0 +1,277 @@
# Запрос к Claude Sonnet — ТОЛЬКО АНАЛИЗ
## ⛔ ЗАПРЕЩЕНО МЕНЯТЬ КОД ⛔
## ⛔ НЕ ДЕЛАТЬ КОММИТЫ ⛔
## ⛔ НЕ ПРАВИТЬ ФАЙЛЫ ⛔
## ⛔ ТОЛЬКО АНАЛИЗ — вывод в файл `doc/claude-analysis-elm-v2.md` ⛔
Ты — эксперт по ELM327 и OBD2. Тебе дан код Android-приложения (Kotlin). НАЙДИ БАГИ, ОБЪЯСНИ, ДАЙ РЕКОМЕНДАЦИИ. Код не менять.
---
## Проблема
Динамический тест: опрос 3 PID (010C RPM, 0110 MAF, 0106 STFT) в цикле. ELM327 v1.5 замолкает.
### Реальные данные с машины
**v1.3.0-dev** (drainInput в каждой write, ATWS+300ms):
```
0106: 1/18 ok 010C: 1/18 ok 0110: 0/18 ok
Первые 2 ответа — данные, дальше 16 пустых.
```
**v1.9.0-dev** (drainInput отключён, без ATWS):
```
0106: 0/15 ok 010C: 0/15 ok 0110: 0/15 ok ← СТАЛО ХУЖЕ
ВСЕ 15 пустые.
```
**Статическая диагностика** — одиночные PID — работает идеально.
---
## Код
Все файлы в `android/app/src/main/java/ru/elmer/client/`.
### 1. ElmProtocol.kt (elm/ElmProtocol.kt) — Стейт-машина AndrOBD
```kotlin
package ru.elmer.client.elm
import android.util.Log
import java.io.InputStream
import java.io.OutputStream
class ElmProtocol(
private val input: InputStream,
private val output: OutputStream
) {
companion object {
private const val TAG = "ElmProto"
private const val POLL_DELAY = 1L
private const val INIT_TIMEOUT = 10000L
private const val DEF_TIMEOUT = 500L
private const val TIMEOUT_MIN = 50L
private const val TIMEOUT_MAX = 2000L
private const val TIMEOUT_STEP = 20L
private const val TIMEOUT_RES = 4
private const val MAX_RETRIES = 10
}
private enum class State { UNDEFINED, INITIALIZING, READY, BUSY, ERROR, DISCONNECTED }
private var state = State.UNDEFINED
private var timeoutMs = DEF_TIMEOUT
private var learnedMin = TIMEOUT_MIN
fun init() {
state = State.INITIALIZING
write("ATSP0"); tryRead(4000); drainInput()
write("ATAT1"); tryRead(2000); drainInput()
updateAtst()
write("ATS0"); tryRead(2000); drainInput()
write("ATL0"); tryRead(2000); drainInput()
write("ATE0"); tryRead(2000); drainInput()
state = State.READY
}
fun sendCommand(cmd: String): String {
if (state == State.ERROR || state == State.DISCONNECTED) recover()
state = State.BUSY
val result = exec(cmd, timeoutMs)
if (state == State.BUSY) state = State.READY
return result
}
private fun exec(cmd: String, timeout: Long): String {
write(cmd)
var t = timeout
for (i in 0 until MAX_RETRIES) {
try {
return handle(read(t))
} catch (_: TimeoutException) {
if (state == State.INITIALIZING) t += 1000
else { increaseTimeout(); t = timeoutMs }
}
}
state = State.ERROR
return ""
}
private fun handle(raw: String): String {
val u = raw.uppercase().trim()
when {
u.startsWith("SEARCHING") -> {}
u.startsWith("OK") -> decreaseTimeout()
u.startsWith("NODATA") || u.startsWith("NO DATA") -> { increaseTimeout(); updateAtst() }
isBusError(u) -> {
state = State.DISCONNECTED; resetTimeout(); updateAtst()
write("ATPC"); tryRead(3000); write("ATSP0"); tryRead(3000)
}
u.startsWith("ERROR") && !u.startsWith("DATA ERROR") -> { state = State.ERROR; write("ATWS"); tryRead(3000) }
isDataError(u) -> { state = State.ERROR; write("ATWS"); tryRead(3000) }
else -> decreaseTimeout()
}
return raw
}
private fun recover() {
state = State.INITIALIZING
write("ATWS"); tryRead(2000); drainInput()
write("ATSP0"); tryRead(2000); drainInput()
write("ATE0"); tryRead(2000); drainInput()
state = State.READY
}
private fun write(cmd: String) {
drainInput()
output.write((cmd + "\r").toByteArray())
output.flush()
}
private fun drainInput() {
while (input.available() > 0) input.read()
}
@Throws(TimeoutException::class)
private fun read(timeout: Long): String {
val dl = System.currentTimeMillis() + timeout
val sb = StringBuilder()
val lines = mutableListOf<String>()
var gotPrompt = false
while (System.currentTimeMillis() < dl) {
if (input.available() > 0) {
val b = input.read()
if (b == -1) break
when (b) {
62 -> { push(sb, lines); gotPrompt = true; break }
13 -> push(sb, lines)
10, 32 -> {}
else -> sb.append(b.toChar())
}
} else { Thread.sleep(POLL_DELAY) }
}
push(sb, lines)
if (!gotPrompt) throw TimeoutException("timeout ${timeout}ms")
return lines.joinToString("\n")
}
private fun tryRead(timeout: Long) { try { read(timeout) } catch (_: TimeoutException) {} }
private fun push(sb: StringBuilder, lines: MutableList<String>) {
if (sb.isNotEmpty()) { lines.add(sb.toString()); sb.clear() }
}
private fun increaseTimeout() { if (timeoutMs + TIMEOUT_STEP < TIMEOUT_MAX) timeoutMs += TIMEOUT_STEP }
private fun decreaseTimeout() { if (timeoutMs - TIMEOUT_STEP >= learnedMin) timeoutMs -= TIMEOUT_STEP }
private fun resetTimeout() { timeoutMs = DEF_TIMEOUT }
fun resetAdaptiveTiming() { timeoutMs = DEF_TIMEOUT }
private fun updateAtst() {
val v = (timeoutMs / TIMEOUT_RES).toInt().coerceAtLeast(1)
write("ATST${v.toString(16).uppercase().padStart(2, '0')}")
tryRead(2000); drainInput()
}
private fun isBusError(s: String) = listOf("UNABLE","BUS BUSY","BUS ERROR","CAN ERROR","BUS INIT","STOPPED").any { s.startsWith(it) }
private fun isDataError(s: String) = listOf("DATA ERROR","BUFFER FULL","RX ERROR").any { s.startsWith(it) }
}
class TimeoutException(message: String) : Exception(message)
```
### 2. DynamicCollector.kt (script/DynamicCollector.kt)
```kotlin
package ru.elmer.client.script
import ru.elmer.client.elm.ElmProtocol
import ru.elmer.client.elm.ObdDecoder
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
class DynamicCollector(
private val elm: ElmProtocol,
private val steps: List<ElmStep>,
private val intervalMs: Long,
private val onSample: (sampleIndex: Int) -> Unit,
private val onLog: (msg: String) -> Unit
) {
data class ElmStep(val id: String, val cmd: String, val desc: String)
private val running = AtomicBoolean(false)
private val samples = mutableListOf<List<SampleResponse>>()
private var threadRef: Thread? = null
data class SampleResponse(val stepId: String, val cmd: String, val raw: String, val decoded: String, val ts: Long = 0)
fun start() {
running.set(true)
val startTs = System.currentTimeMillis()
threadRef = thread(name = "DynamicCollector", isDaemon = true) {
var idx = 0
while (running.get()) {
val t0 = System.currentTimeMillis()
val batch = mutableListOf<SampleResponse>()
for (step in steps) {
if (!running.get()) break
try {
val raw = elm.sendCommand(step.cmd)
val dec = ObdDecoder.decode(step.cmd, raw)
batch.add(SampleResponse(step.id, step.cmd, raw, dec, System.currentTimeMillis() - startTs))
} catch (e: Exception) {
batch.add(SampleResponse(step.id, step.cmd, "(err)", e.message ?: "error", System.currentTimeMillis() - startTs))
}
Thread.sleep(350)
}
if (batch.isNotEmpty()) { synchronized(samples) { samples.add(batch) }; onSample(idx); idx++ }
val elapsed = System.currentTimeMillis() - t0
val sleep = intervalMs - elapsed
if (sleep > 0 && running.get()) Thread.sleep(sleep)
}
}
}
fun stop(): List<List<SampleResponse>> {
running.set(false)
try { threadRef?.join(3000) } catch (_: Exception) {}
return synchronized(samples) { samples.toList() }
}
fun isRunning(): Boolean = running.get()
}
```
### 3. MainActivity.kt — startDynamicRecording() (фрагмент)
```kotlin
// v1.10.0-dev — текущая версия
private fun startDynamicRecording() {
thread(name = "DynamicTest", isDaemon = true) {
checker.ensureConnected()
val elmProto = checker.getElm()!!
// Статика — 9 PID по одному (работает)
for ((pid, desc) in staticCmds) {
elmProto.sendCommand("01$pid")
}
// Подготовка к динамике
try { elmProto.sendCommand("ATWS") } catch (_: Exception) {}
Thread.sleep(800)
// Динамика: 3 PID, интервал 500ms
val dynSteps = listOf("010C" to "RPM", "0110" to "MAF", "0106" to "STFT")
.map { ElmStep(it.second, it.first, it.second) }
DynamicCollector(elmProto, dynSteps, 500L, ...).start()
}
}
```
---
## Вопросы
1. **exec()** делает retry с ПОВТОРНОЙ ОТПРАВКОЙ команды — не забивает ли это ELM327?
2. **drainInput()** в write() — почему без него (v1.9.0) стало ХУЖЕ?
3. **ATWS** — нужен ли? Сколько ждать?
4. **sleep(350)** между PID — правильно или избыточно?
5. Почему статика работает а динамика нет?
## ⛔ НАПОМИНАНИЕ: НЕ МЕНЯТЬ КОД, НЕ КОММИТИТЬ. ТОЛЬКО АНАЛИЗ В ФАЙЛ doc/claude-analysis-elm-v2.md ⛔
+499
View File
@@ -0,0 +1,499 @@
# Запрос анализа ELM327-кода — для Claude Sonnet
## Контекст
Android-приложение для OBD2-диагностики автомобиля через ELM327 Bluetooth-адаптер.
Стек: Kotlin, minSdk 24, OkHttp 4.12.
**Проблема:** динамический тест (START/STOP) — опрос 3 PID (RPM, MAF, STFT) в реальном времени —
даёт 94-100% пустых ответов. ELM327 v1.5 просто перестаёт отвечать.
Статическая диагностика (одиночные PID) работает нормально.
## Важно
АНАЛИЗИРУЙ ТОЛЬКО ELM-код. Не трогай UI, сервер, БД.
Нужен анализ того, почему ELM327 замолкает при динамическом опросе.
## Файлы
Ниже полный код всех файлов, связанных с ELM327.
---
### 1. ElmProtocol.kt — стейт-машина AndrOBD (1:1 копия ElmProt.java)
```kotlin
package ru.elmer.client.elm
import android.util.Log
import java.io.InputStream
import java.io.OutputStream
class ElmProtocol(
private val input: InputStream,
private val output: OutputStream
) {
companion object {
private const val TAG = "ElmProto"
private const val POLL_DELAY = 1L
private const val INIT_TIMEOUT = 10000L
private const val DEF_TIMEOUT = 500L
private const val TIMEOUT_MIN = 50L
private const val TIMEOUT_MAX = 2000L
private const val TIMEOUT_STEP = 20L
private const val TIMEOUT_RES = 4
private const val MAX_RETRIES = 10
}
private enum class State { UNDEFINED, INITIALIZING, READY, BUSY, ERROR, DISCONNECTED }
private var state = State.UNDEFINED
private var timeoutMs = DEF_TIMEOUT
private var learnedMin = TIMEOUT_MIN
fun init() {
Log.i(TAG, "init start")
state = State.INITIALIZING
write("ATSP0"); tryRead(4000); drainInput()
write("ATAT1"); tryRead(2000); drainInput()
updateAtst()
write("ATS0"); tryRead(2000); drainInput()
write("ATL0"); tryRead(2000); drainInput()
write("ATE0"); tryRead(2000); drainInput()
state = State.READY
Log.i(TAG, "ready")
}
fun sendCommand(cmd: String): String {
if (state == State.ERROR || state == State.DISCONNECTED) recover()
state = State.BUSY
val result = exec(cmd, timeoutMs)
if (state == State.BUSY) state = State.READY
return result
}
private fun exec(cmd: String, timeout: Long): String {
write(cmd)
var t = timeout
for (i in 0 until MAX_RETRIES) {
try {
return handle(read(t))
} catch (_: TimeoutException) {
if (state == State.INITIALIZING) t += 1000
else { increaseTimeout(); t = timeoutMs }
}
}
Log.e(TAG, "no response for $cmd")
state = State.ERROR
return ""
}
private fun handle(raw: String): String {
val u = raw.uppercase().trim()
when {
u.startsWith("SEARCHING") -> {}
u.startsWith("OK") -> decreaseTimeout()
u.startsWith("NODATA") || u.startsWith("NO DATA") -> {
increaseTimeout(); updateAtst()
}
isBusError(u) -> {
Log.w(TAG, "BUS ERROR: ${raw.take(60)}")
state = State.DISCONNECTED
resetTimeout(); updateAtst()
write("ATPC"); tryRead(3000)
write("ATSP0"); tryRead(3000)
}
u.startsWith("ERROR") && !u.startsWith("DATA ERROR") -> {
Log.w(TAG, "ERROR — warm start")
state = State.ERROR
write("ATWS"); tryRead(3000)
}
isDataError(u) -> {
Log.w(TAG, "data error — warm start")
state = State.ERROR
write("ATWS"); tryRead(3000)
}
else -> decreaseTimeout()
}
return raw
}
private fun recover() {
Log.i(TAG, "recovering...")
state = State.INITIALIZING
write("ATWS"); tryRead(2000); drainInput()
write("ATSP0"); tryRead(2000); drainInput()
write("ATE0"); tryRead(2000); drainInput()
state = State.READY
}
private fun write(cmd: String) {
drainInput()
output.write((cmd + "\r").toByteArray())
output.flush()
Log.d(TAG, "$cmd")
}
private fun drainInput() {
while (input.available() > 0) input.read()
}
@Throws(TimeoutException::class)
private fun read(timeout: Long): String {
val dl = System.currentTimeMillis() + timeout
val sb = StringBuilder()
val lines = mutableListOf<String>()
var gotPrompt = false
while (System.currentTimeMillis() < dl) {
if (input.available() > 0) {
val b = input.read()
if (b == -1) break
when (b) {
62 -> { push(sb, lines); gotPrompt = true; break } // '>'
13 -> push(sb, lines) // CR
10, 32 -> {} // LF, space
else -> sb.append(b.toChar())
}
} else {
Thread.sleep(POLL_DELAY)
}
}
push(sb, lines)
if (!gotPrompt) throw TimeoutException("timeout ${timeout}ms")
return lines.joinToString("\n")
}
private fun tryRead(timeout: Long) {
try { read(timeout) } catch (_: TimeoutException) {}
}
private fun push(sb: StringBuilder, lines: MutableList<String>) {
if (sb.isNotEmpty()) { lines.add(sb.toString()); sb.clear() }
}
private fun increaseTimeout() {
if (timeoutMs + TIMEOUT_STEP < TIMEOUT_MAX) timeoutMs += TIMEOUT_STEP
}
private fun decreaseTimeout() {
if (timeoutMs - TIMEOUT_STEP >= learnedMin) timeoutMs -= TIMEOUT_STEP
}
private fun resetTimeout() { timeoutMs = DEF_TIMEOUT }
fun resetAdaptiveTiming() { timeoutMs = DEF_TIMEOUT }
private fun updateAtst() {
val v = (timeoutMs / TIMEOUT_RES).toInt().coerceAtLeast(1)
write("ATST${v.toString(16).uppercase().padStart(2, '0')}")
tryRead(2000)
drainInput()
}
private fun isBusError(s: String): Boolean {
return listOf("UNABLE", "BUS BUSY", "BUS ERROR", "CAN ERROR",
"BUS INIT", "STOPPED").any { s.startsWith(it) }
}
private fun isDataError(s: String): Boolean {
return listOf("DATA ERROR", "BUFFER FULL", "RX ERROR").any { s.startsWith(it) }
}
}
class TimeoutException(message: String) : Exception(message)
```
---
### 2. DynamicCollector.kt — сборщик для динамического теста
```kotlin
package ru.elmer.client.script
import ru.elmer.client.elm.ElmProtocol
import ru.elmer.client.elm.ObdDecoder
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
class DynamicCollector(
private val elm: ElmProtocol,
private val steps: List<ElmStep>,
private val intervalMs: Long,
private val onSample: (sampleIndex: Int) -> Unit,
private val onLog: (msg: String) -> Unit
) {
data class ElmStep(val id: String, val cmd: String, val desc: String)
private val running = AtomicBoolean(false)
private val samples = mutableListOf<List<SampleResponse>>()
private var threadRef: Thread? = null
data class SampleResponse(
val stepId: String, val cmd: String, val raw: String,
val decoded: String, val ts: Long = 0
)
fun start() {
running.set(true)
val startTs = System.currentTimeMillis()
threadRef = thread(name = "DynamicCollector", isDaemon = true) {
var idx = 0
while (running.get()) {
val t0 = System.currentTimeMillis()
val batch = mutableListOf<SampleResponse>()
for (step in steps) {
if (!running.get()) break
try {
val raw = elm.sendCommand(step.cmd)
val dec = ObdDecoder.decode(step.cmd, raw)
batch.add(SampleResponse(step.id, step.cmd, raw, dec, System.currentTimeMillis() - startTs))
} catch (e: Exception) {
batch.add(SampleResponse(step.id, step.cmd, "(err)", e.message ?: "error", System.currentTimeMillis() - startTs))
}
Thread.sleep(350)
}
if (batch.isNotEmpty()) {
synchronized(samples) { samples.add(batch) }
onSample(idx); idx++
}
val elapsed = System.currentTimeMillis() - t0
val sleep = intervalMs - elapsed
if (sleep > 0 && running.get()) Thread.sleep(sleep)
}
}
}
fun stop(): List<List<SampleResponse>> {
running.set(false)
try { threadRef?.join(3000) } catch (_: Exception) {}
return synchronized(samples) { samples.toList() }
}
fun isRunning(): Boolean = running.get()
}
```
---
### 3. ElmChecker.kt — проверка устройства + speed-test
```kotlin
package ru.elmer.client.elm
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.util.Log
import java.io.IOException
import java.util.UUID
class ElmChecker(
private val device: BluetoothDevice,
private val adapter: BluetoothAdapter
) {
companion object {
private const val TAG = "ElmChecker"
private val SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
}
data class DeviceInfo(
val version: String, val deviceId: String, val protocol: String,
val voltage: String, val hasAdaptive: Boolean
)
data class EcuData(val supportsObd: Boolean, val pidMask: String, val vin: String?)
data class Result(val good: Boolean, val device: DeviceInfo, val ecu: EcuData, val log: String)
private val logLines = mutableListOf<String>()
fun getLog(): String = logLines.joinToString("\n")
private var socket: BluetoothSocket? = null
private var elm: ElmProtocol? = null
fun checkDevice(): DeviceInfo? {
if (!connectAndInit()) return null
val ati = send("ATI"); val version = parseVersion(ati)
val isV2 = version.contains("v2", ignoreCase = true)
val deviceId = if (isV2) cleanAt2(send("AT@2")) else ""
val dp = send("ATDP"); val protocol = if (dp.length > 3 && dp != "OK") dp.take(60) else dp
val rv = send("ATRV"); val voltage = if (rv.contains("V", ignoreCase = true) || rv.matches(Regex("[0-9.]+"))) rv else ""
val hasAdaptive = if (isV2) send("ATAT1") == "OK" else false
return DeviceInfo(version, deviceId, protocol, voltage, hasAdaptive)
}
fun checkEcu(): EcuData {
val pid0100 = send("0100"); val supportsObd = pid0100.startsWith("41")
val pidMask = if (supportsObd) pid0100.take(60) else ""
val vinRaw = send("0902"); val vin = parseVin(vinRaw)
return EcuData(supportsObd, pidMask, vin)
}
fun scanDtc(): List<String>? {
if (!connectAndInit()) { disconnect(); return null }
val codes = mutableListOf<String>()
codes.addAll(parseDtcCodes(send("03")))
codes.addAll(parseDtcCodes(send("07")))
return codes.distinct()
}
fun ensureConnected(): Boolean = connectAndInit()
fun isConnected(): Boolean = socket?.isConnected == true && elm != null
fun getElm(): ElmProtocol? = elm
fun quickCheck(): Int? {
val e = elm ?: return null
return try {
val t0 = System.currentTimeMillis()
val raw = e.sendCommand("010C"); val dt = System.currentTimeMillis() - t0
if (raw.isBlank() || raw == "(err)") null else dt.toInt()
} catch (_: Exception) { null }
}
data class SpeedTestResult(val perPidAvg: List<Int>, val batchTime: Int, val reliable: Boolean, val message: String)
fun measureResponseTime(onProgress: (String) -> Unit): SpeedTestResult {
val testPids = listOf("010C" to "RPM", "0110" to "MAF", "0106" to "STFT")
val perPidAvg = mutableListOf<Int>()
var hadErrors = false; var reliable = true
val reasons = mutableListOf<String>()
val e = elm ?: return SpeedTestResult(listOf(250,250,250), 750, false, "❌ ELM не инициализирован")
try { e.sendCommand("010C") } catch (_: Exception) {}
onProgress("\n⏱ Тест скорости ELM...")
for ((pi, pair) in testPids.withIndex()) {
val (cmd, name) = pair; val allTimes = mutableListOf<Long>()
val count = if (pi == 0) 4 else 3
for (i in 0 until count) {
val t0 = System.currentTimeMillis()
val raw = try { e.sendCommand(cmd) } catch (_: Exception) { "(err)" }
val dt = System.currentTimeMillis() - t0; allTimes.add(dt)
if (raw == "(err)" || raw.isBlank()) hadErrors = true
}
val times = if (pi == 0) allTimes.takeLast(2).toMutableList() else allTimes
val avg = times.average().toInt(); perPidAvg.add(avg)
onProgress("\n $name: ${times.joinToString("ms, ")}ms (среднее ${avg}ms)")
for (t in times) {
if (t > 0 && avg > 0 && kotlin.math.abs(t - avg).toFloat() / avg > 0.5f) {
if (reliable) reliable = false
reasons.add("${name} нестабилен: ${t}ms vs среднее ${avg}ms")
}
}
}
val batchTime = perPidAvg.sum()
val message = if (reliable) "✅ Скорость стабильна: ${perPidAvg.joinToString("+")}=${batchTime}ms"
else "⚠️ ${reasons.joinToString("; ")}. Проверьте контакт ELM в OBD-разъёме."
onProgress("\n$message")
return SpeedTestResult(perPidAvg, batchTime, reliable, message)
}
fun close() { try { socket?.close() } catch (_: Exception) {}; socket = null; elm = null }
// ... (private connect, send, parseVin, parseDtcCodes, parseVersion, failResult опущены для краткости)
}
```
---
## Проблема
При динамическом тесте:
```
Сессия #54 (v1.3.0-dev): 55 ответов
0106 (STFT): 1 ok / 18 попыток → 94% ошибок
010C (RPM): 1 ok / 18 попыток → 94% ошибок
0110 (MAF): 0 ok / 18 попыток → 100% ошибок
Первые 2 ответа — нормальные (410680, 41110E), затем 17 пустых.
```
**Поток вызовов перед динамическим тестом:**
1. `ElmProtocol.init()` — 5 AT-команд (ATSP0, ATAT1, ATS0, ATL0, ATE0)
2. `ElmChecker.checkDevice()` — 5-6 AT-команд (ATI, AT@2, ATDP, ATRV, ATAT1)
3. `ElmChecker.checkEcu()` — 2 OBD-команды (0100, 0902)
4. В MainActivity: статический проброс 9 PID (0104-011F)
5. Speed-test: 1 warmup + 4 RPM + 3 MAF + 3 STFT = 11 OBD-команд
6. `ATWS` — сброс ELM
7. DynamicCollector: 3 PID в цикле каждые 500ms
**Ключевое:** `sendCommand()``exec()` при таймауте делает RETRY:
- `write(cmd)` — посылает команду
- `read(timeout)` — ждёт ответ
- таймаут → `increaseTimeout()``write(cmd)` ОПЯТЬ
- до 10 retry на одну команду
## Вопросы
1. Почему ELM327 v1.5 замолкает после первых 2 ответов в DynamicCollector?
2. Может ли `drainInput()` в `write()` съедать ответ от предыдущей команды?
3. Критична ли последовательность: static probe → speed-test → ATWS → dynamic collect?
4. Нужно ли переподключать ELM перед динамическим тестом вместо ATWS?
5. Как правильно реализовать динамический опрос с учётом медленного ELM327 v1.5 (min ответ 350ms)?
Ответ сохрани в файл `doc/claude-analysis-elm.md`
---
### 4. MainActivity.kt — фрагменты (checkElm, checkEcu, startDynamicRecording)
```kotlin
// Вызывается при клике на светофор ELM
private fun checkElm() {
// ... поиск Bluetooth-устройства ...
elmDevice = dev
try {
val checker = ElmChecker(dev, btAdapter!!)
val r = checker.checkDevice() // AT-команды, инициализация
if (r != null) {
elmChecker = checker
setIndicator(indElm, "🟢")
checkEcu() // ← синхронно на главном потоке!
// Speed-test в фоновом потоке
thread(name = "SpeedTest", isDaemon = true) {
val client = ServerClient(...)
val saved = client.getProfileResponseTime(elmMac)
if (saved != null && saved > 0) {
val quick = checker.quickCheck() // 010C × 1
// сверка с профилем...
return@thread
}
val result = checker.measureResponseTime { msg -> debugLog(msg) }
if (result.reliable) client.saveProfile(elmMac, result.batchTime)
}
}
} catch ...
}
private fun checkEcu() {
val checker = elmChecker ?: return
try {
val raw = checker.getElm()?.sendCommand("03") ?: "" // ← синхронно!
val ok = raw.startsWith("43")
setIndicator(indEcu, if (ok) "🟢" else "🔴")
} catch ...
}
// Вызывается при нажатии СТАРТ
private fun startDynamicRecording() {
thread(name = "DynamicTest", isDaemon = true) {
val checker = elmChecker
if (!checker.ensureConnected()) { /* retry */ }
val elmProto = checker.getElm()!!
// ── Статика: пробуем 9 PID ──
for ((pid, desc) in staticCmds) {
val raw = elmProto.sendCommand("01$pid")
// ...
}
// ── ATWS: сброс ELM ──
try { elmProto.sendCommand("ATWS") } catch (_: Exception) {}
Thread.sleep(300)
// ── Динамика: 3 PID, интервал 500ms ──
val dynSteps = listOf("010C" to "RPM", "0110" to "MAF", "0106" to "STFT")
.map { ElmStep(it.second, it.first, it.second) }
val collector = DynamicCollector(elmProto, dynSteps, 500L, ...)
collector.start()
while (state == State.START && collector.isRunning()) Thread.sleep(200)
val samples = collector.stop()
// ... контроль качества, слияние со статикой ...
}
}
```
@@ -0,0 +1,28 @@
# Вопросы для Соннета — почему телефон получает старый APK
Дата: 2026-06-28
## Ситуация
Сервер obdai.ru (Ubuntu 24, nginx, 5.172.178.213).
APK собран через `./gradlew :raw:clean :raw:assembleDebug`, 36/36 tasks fresh.
Проверено:
- `aapt dump badging` → versionCode=6, versionName=0.2.2-dev ✅
- `curl -sI https://obdai.ru/static/elm-raw-v022.apk` → 200 OK, 3726961 bytes ✅
- Файл переименован: elm-raw-v022.apk (абсолютно новое имя, не могло быть в кеше)
- Приложение переименовано: app_name = "ELM Relay v2" (strings.xml)
- Страница обновлена: ссылка на /static/elm-raw-v022.apk, текст "Скачать ELM Relay v2"
- nginx reload делался, gunicorn рестартовал
НО: пользователь с Android-телефона скачивает и получает **v0.1.1-dev**.
Приложение на рабочем столе называется "ELM Relay v2", но внутри показывает v0.1.1-dev.
## Вопросы
1. Может ли Android (Google Play Protect / Verify Apps) подменять APK при установке на старую версию из кеша?
2. Может ли nginx кешировать файлы, несмотря на `alias`? Нужен ли `expires -1` или `add_header Cache-Control "no-cache"`?
3. Может ли `curl` и браузер телефона видеть РАЗНЫЕ файлы из-за Cloudflare/CDN/прокси провайдера? Как проверить?
4. Может ли `wget` или `curl` с сервера через внешний IP показать, что отдаётся НЕ тот файл?
5. Может ли Android Download Manager кешировать по имени пакета (ru.elmer.raw), а не по URL?
6. Какие ещё механизмы кеширования APK существуют на стороне Android, которые мы не учли?
7. Как ПРОВЕРИТЬ на сервере, что по HTTP реально отдаётся правильный файл, а не старый — поMD5 или размеру?
@@ -0,0 +1,30 @@
# Ответ Соннета — по результатам тестов 28.06.2026
(Отправлено по ошибке вместо Опуса)
## 1. Природа залипания
Известный баг клонов (особенно v1.5 на CH340/PIC18F). Залипает обычно CAN-контроллер внутри клона — накапливает необработанные фреймы и перестаёт принимать команды через UART. Настоящий ELM327 сбрасывает буфер сам; клоны — нет.
## 2. Детект залипания
Лучший вариант — сравнивать `ATRV` до и после цикла (у живого ELM всегда выдаёт реальное напряжение ~12.x V). Дополнительно: если два последних ответа на разные PID идентичны побайтово — залип. Трёх совпадений достаточно.
## 3. Паузы
2–3 секунды между циклами по 4 PID обычно помогают. Минимум ~1.5 сек. При 6+ PID в цикле лучше 3–5 сек. Чем больше PID, тем больше CAN-трафика, тем быстрее переполняется буфер.
## 4. Auto-recovery
`ATZ` (полный сброс, ~1 сек) — самый надёжный программный способ. `ATWS` (warm start) — мягче, но не всегда помогает клонам. `ATPC` (protocol close) — иногда достаточно. Последовательность: `ATPC` → 500 мс → если не помогло → `ATZ` → переинициализация.
## 5. Оптимальный размер цикла
При паузе 23 сек: **4 PID** — хороший баланс. 6 PID уже рискованно на нагруженной шине. 2 PID — безопасно, но медленно.
## 6. Влияние оборотов
Да, напрямую. При высоких оборотах CAN-шина загружена больше (ECU шлёт больше фреймов), буфер клона заполняется быстрее. На холостых — залипает позже.
## 7. Замена адаптера
Приоритет: **OBDLink EX/MX+** (оригинальный чип STN2120, нет этой проблемы) → оригинальный ELM327 v1.4b/v2.1 (реже). Клоны v2.1 — лотерея, та же болезнь. Китайские v1.5 — для продакшена непригодны.
## 8. Стратегия для продакшена
Адаптироваться под клонов, но с явными ограничениями:
- Медленный режим (4 PID, пауза 3 сек) как default
- Детект залипания + авто-`ATZ`
- В UI показывать «адаптер нестабилен, рекомендуем OBDLink/оригинал»
View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

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

Before

Width:  |  Height:  |  Size: 586 KiB

-173
View File
@@ -1,173 +0,0 @@
# Морда elmAI v2
> Новая архитектура UI. Обсуждено 7 июня 2026.
## Макет
```
┌──────────────────────────────────────────┐
│ 🔧 elmAI v?.?.? │
│ 📡 Сервер ● 🔌 ELM ● 🚗 ЭБУ ● 🧠 LLM ● │ иконки + светофоры
├──────────────────────────────────────────┤
│ [ ОШИБКИ / ДИАГНОСТИКА / СТАРТ / СТОП ] │ одна кнопка
├──────────────────────────────────────────┤
│ │
│ поле вывода результатов │
│ │
├──────────────────────────────────────────┤
│ [📋 История] │
├──────────────────────────────────────────┤
│ [____________________________] [➤] │ поле ввода + кнопка
└──────────────────────────────────────────┘
```
## Иконки и светофоры
Четыре иконки в строке под версией. Справа от каждой — цветной индикатор.
| Иконка | Текст | 🟢 Зелёный | 🟡 Жёлтый | 🔴 Красный |
|--------|-------|-----------|----------|-----------|
| 📡 | Сервер | ping < 3с | проверка... | нет связи |
| 🔌 | ELM | BT + ATI ok | подключение... | нет ELM |
| 🚗 | ЭБУ | 0100 ответил | — | нет связи с ЭБУ |
| 🧠 | LLM | ping-llm ok | проверка... | нет доступа |
- Жёлтый — только на время проверки
- LLM проверяется только если Сервер зелёный
- ЭБУ проверяется только если ELM зелёный
- **Тап по иконке** — перепроверка. Сбрасывает цвет на жёлтый, запускает проверку заново.
### Таймауты
- **ELM**: 2 попытки BT-подключения по ~4с = макс 8с → 🔴
- **Сервер**: HTTP GET `/api/v1/ping`, таймаут 3с → 🔴
- **LLM**: HTTP GET `/api/v1/ping-llm`, таймаут 5с → 🔴 (только если Сервер 🟢)
- При тапе на иконку проверка запускается немедленно, без задержек.
### Стартовый сценарий
1. Открыли приложение → все индикаторы 🟡 или 🔴
2. ELM 🟡 — пробуем подключиться (8с макс) → 🟢 или 🔴
3. Сервер 🟡 — ping (3с) → 🟢 или 🔴
4. Если Сервер 🟢 → LLM 🟡 — ping-llm (5с) → 🟢 или 🔴
5. Если ELM 🟢 → ЭБУ — пробуем `0100` → 🟢 или 🔴
После первого цикла индикаторы не обновляются автоматически — только по тапу.
## Кнопка-трансформер
Одна широкая кнопка. Меняет текст, цвет и действие в зависимости от этапа.
```
🔴 [⚠️ ОШИБКИ] — начальное состояние
↓ тап
сканирование DTC (03 + 07)
🔵 [🔍 ДИАГНОСТИКА] — ошибки считаны
↓ тап
ЕСЛИ сервер 🟢 → запрос параметров ЭБУ → отправка на сервер → LLM-анализ → вывод
ЕСЛИ сервер 🔴 → пояснение в выводе: «Сервер недоступен. Сделайте тест.»
🟢 [▶ СТАРТ] — готов к динамическому тесту
↓ тап
подсказка в выводе: «Газ до 3000, 3-4с, сброс. → СТОП»
запись 12 PID каждые 250мс
🔴 [⏹ СТОП] — запись идёт
↓ тап
запись остановлена, данные в памяти
вывод: «Записано N отсчётов. ➤ для отправки.»
🟢 [▶ СТАРТ] — можно повторить тест
```
- После СТОП можно снова нажать СТАРТ — новый тест, старые данные сохраняются.
- **Отправка данных** — не кнопкой, а через ➤ в поле ввода.
## Поле вывода
- ScrollView, моноширинный шрифт
- После done — кнопка «✕ Закрыть» (сворачивает вывод, возвращает все кнопки)
- После Share в истории — возврат в приложение (стандартное поведение Android)
## Поле ввода + кнопка ➤
2 строки, всегда активно. Плейсхолдер: «Что беспокоит? Чем подробнее — тем лучше».
Кнопка ➤ справа:
- **Обычный режим**: текст из поля → отправка на сервер (`/api/v1/chat`) → сервер спрашивает LLM → ответ в вывод
- **После СТОП**: накопленные данные теста → отправка на сервер (`/api/v1/session/upload`) + текст (если есть) → сервер анализирует через LLM → ответ в вывод
- Ответ → в поле вывода
## История
- Кнопка «📋 История»
- Список последних 20 сессий (дата, заголовок, статус загрузки)
- Выбор → диагноз во всплывающем окне
- Кнопка «📤 Поделиться» → системный Share Sheet (Telegram, WhatsApp, Gmail...)
- После Share — возврат в наше приложение
## Что удаляется
- Кнопки «📡 Сервер», «🔌 ELM», «🚗 ЭБУ» как отдельные — заменены на иконки со светофорами
- Кнопки «⏱ На месте», «🚗 В движении» — всё через одну кнопку-трансформер
- `tvDtcStatus` («Сначала считай ошибки») — не нужен
- `cbFullMode` (чекбокс полной диагностики) — всегда полная
- `tvPrompt` (оверлей) — не нужен
- `btnDynStart` — не нужен
## Серверные изменения
Ничего нового — всё уже есть:
- `/api/v1/ping`
- `/api/v1/ping-llm`
- `/api/v1/chat`
- `/api/v1/session/upload` с `dynamic_samples`
## Текущий макет (реализовано)
```
elmAI v0.68 📡🟢 ELM🟢 ECU🟢 LLM🟢 ← одна строка
[ ОШИБКИ / ДИАГНОСТИКА / СТАРТ / СТОП ]
вывод
[ 📋 История ] ⏱ 12с ← таймер справа
[____________________________] [➤] ← 3 строки ввода
```
## Логика кнопки (стейт-машина)
| State | Текст | Цвет | Действие |
|-------|-------|------|----------|
| INIT | ⚠️ ОШИБКИ | 🟠 | scanDtc() |
| DTC | 🔍 ДИАГНОСТИКА | 🔵 | runDiagnostics() |
| DIAG | ▶ СТАРТ | 🟢 | startDynamicRecording() |
| START | ⏹ СТОП | 🔴 | stopDynamicRecording() |
| STOP | ▶ СТАРТ | 🟢 | не используется |
- Кнопка неактивна (alpha=0.4) если ELM 🔴 или ECU 🔴
- Поле ввода неактивно если LLM 🔴
- После DTC-сканирования ECU → 🟢
- После СТОП → DIAG (можно снова СТАРТ)
## Исправлено (v0.57 → v0.68)
| v | Что |
|---|-----|
| 0.57 | Layout v2, иконки, трансформер |
| 0.58 | Всё в одной строке |
| 0.59 | setIndicator в runOnUiThread |
| 0.60 | LLM → 🤓 |
| 0.61 | Таймер в строке Истории, нет ELM → кнопка неактивна |
| 0.62 | Поле ввода неактивно без LLM |
| 0.63 | 🤓 → 🎓 |
| 0.64 | 🎓 → LLM текст |
| 0.65 | СТОП работает, ECU 🟢 после сканирования |
| 0.66 | ensureConnected всегда перед dynamic test |
| 0.67 | Кнопка неактивна без ECU |
| 0.68 | Двойной вызов checkLlm/checkEcu убран, таймер в catch |
## Известные проблемы (TODO)
- `findElmDevice()` возвращает null при >1 устройствах (диалог асинхронный)
- Android 12+ — startChecks до получения BT-разрешений
- `State.STOP` не используется
+26 -5
View File
@@ -56,6 +56,7 @@ class AndrOBD:
INIT_TMO = 10000 # мс — таймаут для команд инициализации
DEF_TMO = 200 # мс — начальный таймаут
ATST_CLONE = 0x96 # 150×4=600ms — фиксированный для клонов
def __init__(self, port: str, baudrate: int = 38400):
self._transport = SerialTransport(port, baudrate)
@@ -73,20 +74,39 @@ class AndrOBD:
self._transport.close()
def init(self):
"""Базовая инициализация ELM327 (уровень 0 — все клоны).
"""Канонический init: ATE0→ATL0→ATS0→ATI→[ветвление]→ATSP0.
ТОЛЬКО команды которые есть у ВСЕХ клонов:
ATE0 ATL0 ATS0 ATH1 ATSP0
Эхо ПЕРВЫМ. ATI ДО протокола. Клон: ATST фикс, без ATAT1.
"""
logger.info("AndrOBD: init (L0)")
self._state = State.INITIALIZING
# Шаг 1: clone-safe, без протокола
self._exec("ATE0", self.DEF_TMO * 5)
self._transport.try_read(500) # drain после AT-команды
self._exec("ATL0", self.DEF_TMO * 5)
self._transport.try_read(500)
self._exec("ATS0", self.DEF_TMO * 5)
self._exec("ATH1", self.DEF_TMO * 5)
self._transport.try_read(500)
# Шаг 2: ATI → detectClone ДО ветвления
ati = self._exec("ATI", self.DEF_TMO * 5)
self._transport.try_read(500)
is_clone = "v1.5" in ati.lower()
if is_clone:
logger.info("AndrOBD: clone v1.5 — ATST fixed, no ATAT1")
self._exec(f"ATST{self.ATST_CLONE:02X}", self.DEF_TMO * 5)
self._transport.try_read(500)
else:
self._exec("ATAT1", self.DEF_TMO * 5)
self._transport.try_read(500)
# Шаг 3: протокол
self._exec("ATSP0", self.INIT_TMO)
self._transport.try_read(500)
self._state = State.READY
logger.info("AndrOBD: ready (L0)")
logger.info(f"AndrOBD: ready (L0, clone={is_clone})")
def init_l1(self):
"""Инициализация уровня 1: база + адаптивный тайминг."""
@@ -111,6 +131,7 @@ class AndrOBD:
self._recover()
self._state = State.BUSY
result = self._exec(cmd, self._timing.ms)
self._transport.try_read(500) # drain после read — клон v1.5
if self._state == State.BUSY:
self._state = State.READY
return result
+251
View File
@@ -0,0 +1,251 @@
"""
obd/raw_console.py — Сырой слой ELM327 (без стейт-машины).
НИКАКОЙ логики протокола:
- Нет state machine (State)
- Нет классификации ответов (Rsp)
- Нет адаптивных таймингов (AdaptiveTiming)
- Нет ретраев
- Нет хендлеров ошибок
ТОЛЬКО:
- send(cmd) → отправляет команду + CR
- read(timeout) → читает ВСЁ до '>' или таймаута, байт-за-байтом
- drain() → очищает входной буфер
- available() → сколько байт ждёт в буфере
ПРЕДНАЗНАЧЕНИЕ:
Изучение реального поведения ELM327.
«Почему статика работает, а динамика ломается?»
Ответ — в сырых байтах.
"""
import logging
import time
logger = logging.getLogger("elm.raw")
# ── Конфигурация по умолчанию (можно переопределить) ──
DEFAULT_TIMEOUT = 500 # мс
INTER_CMD_DELAY = 0.05 # с — пауза между командой и чтением
class RawELM:
"""Сырой слой ELM327 — только send/read/drain, без протокольной логики."""
def __init__(self, transport):
"""
Args:
transport: объект с методами .write(str) и .read(timeout_ms) → str
(обычно SerialTransport из obd.connection)
"""
self._t = transport
self._timeout = DEFAULT_TIMEOUT
self._inter_delay = INTER_CMD_DELAY
self._log: list[dict] = [] # история команд
# ── Настройка ────────────────────────────────────
@property
def timeout(self) -> int:
return self._timeout
@timeout.setter
def timeout(self, ms: int):
self._timeout = ms
@property
def inter_delay(self) -> float:
return self._inter_delay
@inter_delay.setter
def inter_delay(self, sec: float):
self._inter_delay = sec
# ── Основные операции ────────────────────────────
def send(self, cmd: str, timeout: int | None = None) -> dict:
"""Отправить команду и прочитать сырой ответ.
Args:
cmd: команда (без \r, добавится автоматически)
timeout: таймаут в мс (None = использовать self.timeout)
Returns:
{
"cmd": str, # что отправили
"raw": str, # сырой ответ (без '>')
"prompt": bool, # получен ли '>'
"elapsed_ms": int, # сколько мс заняло
"bytes": int, # сколько байт в ответе
"error": str|None, # ошибка если есть
}
Не бросает исключений — всегда возвращает dict с полем error.
"""
tmo = timeout if timeout is not None else self._timeout
entry = {"cmd": cmd, "timeout_ms": tmo, "ts": time.time()}
try:
# 1. Отправить
self._t.write(cmd)
# 2. Пауза (ELM начинает отвечать не мгновенно)
if self._inter_delay > 0:
time.sleep(self._inter_delay)
# 3. Прочитать
t0 = time.monotonic()
raw, prompt, nbytes = self._read_raw(tmo)
elapsed = int((time.monotonic() - t0) * 1000)
entry.update({
"raw": raw,
"prompt": prompt,
"elapsed_ms": elapsed,
"bytes": nbytes,
"error": None,
})
except TimeoutError:
entry.update({
"raw": "",
"prompt": False,
"elapsed_ms": tmo,
"bytes": 0,
"error": f"timeout {tmo}ms",
})
except Exception as e:
entry.update({
"raw": "",
"prompt": False,
"elapsed_ms": 0,
"bytes": 0,
"error": str(e),
})
self._log.append(entry)
return entry
def drain(self) -> dict:
"""Очистить входной буфер. Возвращает что было выброшено.
Returns:
{"drained": str, "bytes": int}
"""
t0 = time.monotonic()
drained = []
total = 0
dl = t0 + 0.5 # 500 мс максимум на дренаж
while time.monotonic() < dl:
try:
ch = self._read_byte(0.05)
if ch is not None:
drained.append(chr(ch))
total += 1
else:
break # буфер пуст
except Exception:
break
elapsed = int((time.monotonic() - t0) * 1000)
result = {"drained": "".join(drained), "bytes": total, "elapsed_ms": elapsed}
if total > 0:
logger.info(f"RawELM: drained {total} bytes: {result['drained']!r}")
return result
def available(self) -> int:
"""Сколько байт ждёт во входном буфере (0 = пусто)."""
try:
return self._t._ser.in_waiting
except Exception:
return -1
# ── Лог ──────────────────────────────────────────
@property
def log(self) -> list[dict]:
"""История всех команд."""
return self._log
def clear_log(self):
"""Очистить историю."""
self._log.clear()
def last(self) -> dict | None:
"""Последняя команда."""
return self._log[-1] if self._log else None
# ── Приватные ────────────────────────────────────
def _read_raw(self, timeout_ms: int) -> tuple[str, bool, int]:
"""Читает байт-за-байтом до '>' или таймаута.
Returns:
(raw_text, got_prompt, byte_count)
"""
dl = time.monotonic() + timeout_ms / 1000.0
lines, cur = [], []
got_prompt = False
while time.monotonic() < dl:
ch = self._read_byte(0.05)
if ch is None:
continue
cp = ch
if cp == 62: # '>' — промпт ELM327
if cur:
lines.append("".join(cur))
cur.clear()
got_prompt = True
break
elif cp == 13: # CR — конец строки
if cur:
lines.append("".join(cur))
cur.clear()
elif cp in (10, 32): # LF и пробел — игнорируем
pass
else:
cur.append(chr(cp))
if cur:
lines.append("".join(cur))
return ("\n".join(lines), got_prompt, sum(len(s) for s in lines))
def _read_byte(self, timeout_s: float) -> int | None:
"""Прочитать один байт с таймаутом. None = таймаут/нет данных."""
import serial
try:
if self._t._ser.in_waiting > 0:
b = self._t._ser.read(1)
return b[0] if b else None
else:
time.sleep(0.001) # поллинг 1мс
return None
except serial.SerialException:
return None
# ── Хелпер ───────────────────────────────────────────
def format_response(entry: dict) -> str:
"""Форматирует ответ RawELM.send() для вывода в консоль."""
lines = [
f"{entry['cmd']}",
f"{entry['raw']!r}" if entry["raw"] else "← (пусто)",
]
if entry["prompt"]:
lines.append(" prompt: ✅ >")
else:
lines.append(" prompt: ❌")
lines.append(f" time: {entry['elapsed_ms']}ms, bytes: {entry['bytes']}")
if entry["error"]:
lines.append(f" ⚠️ {entry['error']}")
return "\n".join(lines)
def format_log(entries: list[dict]) -> str:
"""Форматирует всю историю команд."""
return "\n" + "" * 50 + "\n" + \
"\n".join(format_response(e) for e in entries) + \
"\n" + "" * 50
-92
View File
@@ -1,92 +0,0 @@
## 4. Контекст проекта (РЕЗЮМЕ для нового чата)
### Что это
elmAI — Android-приложение + Python-сервер для диагностики авто через ELM327.
### Текущая версия
<!-- !!! АКТУАЛЬНАЯ ВЕРСИЯ !!! -->
<!-- Перед деплоем bump в android/app/build.gradle.kts, index.html (2 места), CHANGELOG.md, resume.txt, doc/architecture.md -->
**v0.77.0-dev** (APK: https://obdai.ru/elmer.apk)
### Репозитории
- Сервер: https://gitea.services.ngcloud.ru/Nail/elmer (ветка **dynamic-tests**)
- Android: https://github.com/Repinoid/elmer-android (ветка **dynamic-tests**)
- Сервер живёт на 5.172.178.213 (SSH: naeel@5.172.178.213, ключ ~/.ssh/naeel_vm_id_ed25519)
### Деплой (localhost → сервер)
```bash
# 1. bump версии в android/app/build.gradle.kts на локальной машине!
# 2. закоммитить + запушить (dynamic-tests!)
cd /home/naeel/elmer && git add -A && git commit -m "..." && git push origin dynamic-tests
# 3. залить android-исходники на сервер
# 3. залить android-исходники на сервер
cd /home/naeel/elmer && tar czf /tmp/android-src.tar.gz --exclude='.git' --exclude='build' --exclude='.gradle' android/
scp -i ~/.ssh/naeel_vm_id_ed25519 /tmp/android-src.tar.gz naeel@5.172.178.213:/tmp/
# 4. на сервере: обновить сервер + собрать APK
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 "
cd /opt/elmer && git checkout dynamic-tests && git pull origin dynamic-tests
pip install -r requirements.txt
sudo systemctl restart elmer
rm -rf android && tar xzf /tmp/android-src.tar.gz
cd android && gradle wrapper --gradle-version 8.7
export ANDROID_SDK_ROOT=\$HOME/android-sdk
./gradlew clean assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/
"
```
### Структура проекта
```
elmer/
android/ — Android-приложение (Kotlin)
.../client/
MainActivity.kt — UI + приёмники
ElmForwardService.kt — BT/TCP-реле
TestService.kt — автономный тест ELM
api/ — Flask API (Python)
db.py — БД (sessions + device_profiles)
routes.py — эндпоинты
scripts.py — скрипты L0/L1/L2
parser.py — парсинг ответов
config.py, dtc.py, ping.py
brain/ — LLM-клиент, промпты
obd/ — ELM-протокол (Python)
commands.py — каталог AT-команд
classifier.py — классификация ответов
connection.py — SerialTransport
probe.py — пробинг ELM
protocol.py — стейт-машина AndrOBD
state.py — состояния/ответы
timing.py — адаптивный таймаут
doc/ — Документация, сессии
web/ — Flask web, статика
```
### Что сделано (сессия 2026-06-07)
#### Сервер
- **Пробинг ELM**: трехуровневый каскад (L0/L1/L2), профили в БД по MAC
- **Fix init**: больше не шлём ATAT1/ATSTxx (вешало клоны)
- **Скрипты под уровень**: L0 (5 PIDs), L1 (8 + VIN), L2 (14 + калибровки)
- **Рефакторинг**: код разбит на независимые модули (commands, classifier, connection...)
#### Android
- **Вывод**: append вместо overwrite (строки не перекрываются)
- **Таймер**: отдельный TextView, тикает только во время обмена (Engine Time)
- **TestService**: адаптивные таймауты, ATS0, \r терминатор
### Что НЕ сделано (TODO)
- Полевой тест на машине ← СЕЙЧАС
- Разбить MainActivity.kt
- Разбить ElmChecker.kt
### Важные правила
- ELM327 v1.5 — фейк, НЕ слать ATAT1/AT@1/AT@2/ATST/ATCAF1/ATCFC1
- Все статусы через append("\n..."), не tvStatus.text =
- Таймер операций (opTimerStart/Stop), не сессии
- Версию поднимать ВЕЗДЕ: build.gradle.kts, index.html (2 места), CHANGELOG.md, resume.txt, doc/architecture.md
- В проекте elmer — НИЧЕГО не делать без прямого указания
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import sqlite3, json
db = sqlite3.connect("/opt/elmer/elmer.db")
for sid in [41, 40]:
r = db.execute("SELECT id, created_at, raw_responses FROM sessions WHERE id=?", (sid,)).fetchone()
print(f"\n=== SESSION #{r[0]} {r[1]} ===")
if not r[2]: print(" (no raw data)"); continue
data = json.loads(r[2])
print(f" Total responses: {len(data)}")
cmds = {}
for d in data:
c = d.get("cmd","?")
s = d.get("step_id","?")
raw = d.get("raw","")
dec = d.get("decoded","")
key = f"{s} ({c})"
if key not in cmds:
cmds[key] = {"cnt": 0, "err": 0, "empty": 0, "ok": 0, "samples": []}
cmds[key]["cnt"] += 1
if not raw or raw in ["?","(err)","NO DATA"]:
cmds[key]["err"] += 1
elif raw == "":
cmds[key]["empty"] += 1
else:
cmds[key]["ok"] += 1
if len(cmds[key]["samples"]) < 2:
cmds[key]["samples"].append(f"{raw} -> {dec}")
for k, v in sorted(cmds.items()):
print(f" {k:30s} total={v['cnt']:3d} ok={v['ok']} err={v['err']} empty={v['empty']}")
for s in v["samples"]:
print(f" {s}")
db.close()
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""
elm_console.py — Интерактивная консоль ELM327 (сырой режим).
НИКАКОЙ автоматики:
- Нет init(), probe(), send() со стейт-машиной
- Нет классификации ответов
- Нет адаптивных таймингов
ТОЛЬКО вы вводите команду — ELM отвечает сырыми байтами.
ЗАПУСК:
python tools/elm_console.py # порт по умолчанию /dev/rfcomm0
python tools/elm_console.py --port /dev/rfcomm0 # явно указать порт
python tools/elm_console.py --baud 38400 # другая скорость
python tools/elm_console.py --timeout 1000 # таймаут 1с
python tools/elm_console.py --no-init # не слать AT-инит
КОМАНДЫ КОНСОЛИ:
ATZ — отправить "ATZ" в ELM
0105 — отправить "0105" (PID coolant temp)
!drain — очистить входной буфер
!timeout 2000 — установить таймаут 2000 мс
!delay 0.5 — пауза между командой и чтением (сек)
!log — показать историю команд
!available — сколько байт в буфере
!save file.json — сохранить лог в файл
!help — справка
!quit — выход
ЦЕЛЬ:
Понять, КАК на самом деле работает ELM327.
Почему статика работает, а динамика ломается?
Ответ — в сырых байтах.
"""
import argparse
import cmd
import json
import logging
import sys
import time
from pathlib import Path
# Добавляем корень проекта в PYTHONPATH
sys.path.insert(0, str(Path(__file__).parent.parent))
from obd.connection import SerialTransport
from obd.raw_console import RawELM, format_response, format_log
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(name)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("elm.console")
class ElmConsole(cmd.Cmd):
"""Интерактивная консоль ELM327."""
intro = """
╔══════════════════════════════════════════════════════╗
║ ELM327 Raw Console ║
║ Сырое взаимодействие — без стейт-машины ║
║ Команды: ATZ, 0105, !drain, !help, !quit ║
╚══════════════════════════════════════════════════════╝
"""
prompt = "\nelm> "
def __init__(self, port: str, baudrate: int, timeout: int, delay: float, no_init: bool):
super().__init__()
self._port = port
self._baud = baudrate
self._no_init = no_init
self._transport = None
self._elm: RawELM | None = None
# ── Подключение ─────────────────────────────────
def connect(self):
"""Открыть порт и создать RawELM."""
print(f"🔌 Подключение к {self._port} @ {self._baud}...")
try:
self._transport = SerialTransport(self._port, self._baud)
self._transport.connect()
except Exception as e:
print(f"❌ Не удалось открыть порт: {e}")
print(" Проверь: bash scripts/setup-bt.sh")
return False
self._elm = RawELM(self._transport)
print(f"✅ Порт открыт. RawELM готов.")
print(f" Таймаут: {self._elm.timeout}ms, пауза: {self._elm.inter_delay}s")
print(f" Буфер: {self._elm.available()} байт")
if not self._no_init:
print("\n📡 Быстрая проверка связи (ATZ)...")
r = self._elm.send("ATZ", timeout=3000)
print(format_response(r))
if r["error"]:
print("⚠️ ELM327 не ответил на ATZ. Проверь питание адаптера.")
print(" Продолжаем, но команды могут не работать.")
return True
def close(self):
"""Закрыть порт."""
if self._transport:
self._transport.close()
print("🔌 Порт закрыт.")
# ── cmd.Cmd overrides ────────────────────────────
def default(self, line: str):
"""Любая не-! команда = отправить в ELM327."""
cmd_str = line.strip()
if not cmd_str:
return
if cmd_str.startswith("!"):
print(f"Неизвестная команда: {cmd_str}. !help для списка.")
return
# Отправить в ELM
result = self._elm.send(cmd_str)
print(format_response(result))
def emptyline(self):
"""Пустая строка — ничего не делаем."""
pass
# ── Специальные команды (!) ──────────────────────
def do_drain(self, arg):
"""!drain — очистить входной буфер ELM327."""
r = self._elm.drain()
if r["bytes"] > 0:
print(f"🗑 Выброшено {r['bytes']} байт: {r['drained']!r}")
else:
print("✅ Буфер пуст.")
def do_timeout(self, arg):
"""!timeout <ms> — установить таймаут чтения."""
try:
ms = int(arg.strip())
self._elm.timeout = ms
print(f"⏱ Таймаут: {ms}ms")
except ValueError:
print(f"❌ Нужно число: !timeout 2000")
def do_delay(self, arg):
"""!delay <sec> — пауза между командой и чтением."""
try:
sec = float(arg.strip())
self._elm.inter_delay = sec
print(f"⏱ Пауза: {sec}s")
except ValueError:
print(f"❌ Нужно число: !delay 0.5")
def do_log(self, arg):
"""!log [N] — показать последние N команд (по умолчанию все)."""
entries = self._elm.log
if not entries:
print("📭 Лог пуст.")
return
try:
n = int(arg.strip()) if arg.strip() else len(entries)
except ValueError:
n = len(entries)
to_show = entries[-n:] if n < len(entries) else entries
print(format_log(to_show))
print(f"Всего: {len(entries)} команд.")
def do_available(self, arg):
"""!available — сколько байт в буфере."""
n = self._elm.available()
if n < 0:
print("⚠️ Не удалось проверить буфер (порт закрыт?).")
elif n == 0:
print("✅ Буфер пуст.")
else:
print(f"📥 В буфере: {n} байт.")
def do_save(self, arg):
"""!save <file.json> — сохранить лог в JSON."""
path = arg.strip()
if not path:
print("❌ Укажи имя файла: !save log.json")
return
try:
with open(path, "w") as f:
json.dump(self._elm.log, f, indent=2, default=str)
print(f"💾 Сохранено: {path} ({len(self._elm.log)} команд)")
except Exception as e:
print(f"❌ Ошибка: {e}")
def do_raw(self, arg):
"""!raw — показать последний ответ в repr (все символы)."""
last = self._elm.last()
if not last:
print("📭 Нет команд.")
return
print(f"raw = {last['raw']!r}")
print(f"prompt = {last['prompt']}")
print(f"elapsed = {last['elapsed_ms']}ms")
print(f"bytes = {last['bytes']}")
def do_help(self, arg):
"""!help — справка."""
print("""
╔══════════════════════════════════════════════════════╗
║ КОМАНДЫ ELM327 (вводи как есть): ║
║ ATZ — сброс ║
║ ATI — идентификация ║
║ ATE0 — echo off ║
║ ATL0 — linefeeds off ║
║ ATS0 — spaces off ║
║ ATH1 — headers on ║
║ ATSP0 — авто-протокол ║
║ ATRV — напряжение ║
║ ATDPN — номер протокола ║
║ 0105 — температура ОЖ (PID) ║
║ 010C — обороты ║
║ 010D — скорость ║
║ 03 — сохранённые DTC ║
║ 07 — pending DTC ║
║ 0902 — VIN ║
║ ║
║ КОМАНДЫ КОНСОЛИ (с !): ║
║ !drain — очистить буфер ║
║ !timeout N — таймаут (ms) ║
║ !delay N — пауза перед чтением (s) ║
║ !log [N] — история команд ║
║ !raw — последний ответ в repr ║
║ !available — байт в буфере ║
║ !save f.json — сохранить лог ║
║ !help — эта справка ║
║ !quit — выход ║
╚══════════════════════════════════════════════════════╝
""")
def do_quit(self, arg):
"""!quit — выход."""
print("👋")
self.close()
return True
def do_exit(self, arg):
"""!exit — то же что !quit."""
return self.do_quit(arg)
# Сокращения
do_q = do_quit
do_h = do_help
do_d = do_drain
do_t = do_timeout
do_l = do_log
do_a = do_available
# ── main ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="ELM327 Raw Console — интерактивное сырое взаимодействие"
)
parser.add_argument("--port", default="/dev/rfcomm0", help="Порт (default: /dev/rfcomm0)")
parser.add_argument("--baud", type=int, default=38400, help="Скорость (default: 38400)")
parser.add_argument("--timeout", type=int, default=500, help="Таймаут чтения ms (default: 500)")
parser.add_argument("--delay", type=float, default=0.05, help="Пауза перед чтением s (default: 0.05)")
parser.add_argument("--no-init", action="store_true", help="Не слать ATZ при старте")
args = parser.parse_args()
console = ElmConsole(
port=args.port,
baudrate=args.baud,
timeout=args.timeout,
delay=args.delay,
no_init=args.no_init,
)
if not console.connect():
sys.exit(1)
try:
console.cmdloop()
except KeyboardInterrupt:
print("\n👋")
finally:
console.close()
if __name__ == "__main__":
main()
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
elm_relay.py — Интерактивная консоль для удалённого управления ELM327 через Android.
Работает через HTTP-очередь на сервере:
1. Ставит команду → POST /api/v1/elm/raw/cmd
2. Ждёт ответ → GET /api/v1/elm/raw/response?wait=N
3. Показывает сырой ответ
4. Анализирует → следующая команда
ЗАПУСК:
python3 tools/elm_relay.py # сервер по умолчанию http://localhost:5005
python3 tools/elm_relay.py --server https://obdai.ru
python3 tools/elm_relay.py --timeout 1000 # таймаут команд 1000мс
ИНТЕРАКТИВНЫЕ КОМАНДЫ:
ATZ — отправить "ATZ"
0105 — отправить PID
!status — статус устройства
!history [N] — последние N ответов
!drain — очистить буфер (ATPC)
!mode raw — включить raw-режим на сервере
!mode normal — выключить
!timeout N — таймаут команд (мс)
!help — справка
!quit — выход
"""
import argparse
import cmd
import json
import sys
import time
import urllib.request
import urllib.error
DEFAULT_SERVER = "http://localhost:5005"
class ElmRelay(cmd.Cmd):
"""Интерактивная консоль для удалённого ELM327."""
intro = """
╔══════════════════════════════════════════════════════╗
║ ELM327 Remote Relay Console ║
║ Сервер → Android → ELM327 → ответ → анализ ║
║ !help для списка команд ║
╚══════════════════════════════════════════════════════╝
"""
prompt = "\nelm-relay> "
def __init__(self, server: str, timeout: int):
super().__init__()
self.server = server.rstrip("/")
self.timeout = timeout
self._last_seq = 0
# ── Отправка команд ──────────────────────────────
def default(self, line: str):
"""Любая не-! команда → отправить в ELM327."""
cmd_str = line.strip()
if not cmd_str:
return
if cmd_str.startswith("!"):
print(f"Неизвестная команда: {cmd_str}")
return
self._send_and_wait(cmd_str)
def _send_and_wait(self, cmd: str, drain_first: bool = False):
"""Поставить команду в очередь и дождаться ответа."""
# 1. Отправить команду
try:
enq = self._post("/api/v1/elm/raw/cmd", {
"cmd": cmd,
"timeout_ms": self.timeout,
"drain_first": drain_first,
})
except Exception as e:
print(f"❌ Ошибка отправки: {e}")
return
seq = enq.get("seq", 0)
print(f"{cmd} (seq={seq}, timeout={self.timeout}ms)")
# 2. Ждать ответ
try:
resp = self._get(f"/api/v1/elm/raw/response?wait=30&seq={self._last_seq}")
except Exception as e:
print(f"❌ Ошибка ожидания: {e}")
return
self._last_seq = resp.get("seq", seq)
# 3. Показать
self._print_response(resp)
# ── Вывод ответа ──────────────────────────────────
def _print_response(self, r: dict):
raw = r.get("raw", "")
prompt = r.get("prompt", False)
elapsed = r.get("elapsed_ms", 0)
nbytes = r.get("bytes", 0)
error = r.get("error")
print(f"{raw!r}" if raw else "← (пусто)")
status = []
if prompt:
status.append("✅ >")
else:
status.append("❌ нет >")
status.append(f"{elapsed}ms")
status.append(f"{nbytes}B")
if error:
status.append(f"⚠️ {error}")
print(f" {' | '.join(status)}")
# ── Специальные команды ───────────────────────────
def do_status(self, arg):
"""!status — статус устройства."""
try:
s = self._get("/api/v1/elm/raw/status")
except Exception as e:
print(f"{e}")
return
print(f"Устройство: {'✅ готово' if s.get('device_ready') else '❌ не подключено'}")
info = s.get("device_info", {})
if info:
print(f" ID: {info.get('device_id', '?')}")
print(f" ELM: {info.get('elm_version', '?')}")
print(f" Протокол: {info.get('protocol', '?')}")
print(f" Напряжение: {info.get('voltage', '?')}")
print(f"Очередь: {'есть команда' if s.get('pending_cmd') else 'пусто'}")
print(f"Последний seq: {s.get('last_response_seq', 0)}")
print(f"История: {s.get('history_count', 0)} команд")
def do_history(self, arg):
"""!history [N] — последние N ответов."""
try:
n = int(arg.strip()) if arg.strip() else 10
except ValueError:
n = 10
try:
h = self._get(f"/api/v1/elm/raw/history?n={n}")
except Exception as e:
print(f"{e}")
return
items = h.get("history", [])
if not items:
print("📭 История пуста.")
return
print(f"Последние {len(items)} из {h.get('total', 0)}:\n")
for r in items:
seq = r.get("seq", "?")
cmd = r.get("cmd", "?")
raw = (r.get("raw") or "")[:60]
elapsed = r.get("elapsed_ms", 0)
prompt = "✅>" if r.get("prompt") else ""
error = f" ⚠️{r['error']}" if r.get("error") else ""
print(f" #{seq}{cmd}{raw}{'' if len(r.get('raw',''))>60 else ''} ({elapsed}ms, {prompt}){error}")
def do_drain(self, arg):
"""!drain — попросить Android очистить буфер ELM327."""
self._send_and_wait("ATPC", drain_first=False)
print("🗑 Буфер очищен.")
def do_mode(self, arg):
"""!mode raw|normal — переключить режим сервера."""
mode = arg.strip().lower()
if mode not in ("raw", "normal"):
print("❌ !mode raw или !mode normal")
return
on = mode == "raw"
try:
self._post("/api/v1/elm/raw/mode", {"raw_mode": on})
print(f"✅ Режим: {'RAW' if on else 'NORMAL'}")
except Exception as e:
print(f"{e}")
def do_timeout(self, arg):
"""!timeout N — таймаут команд (мс)."""
try:
self.timeout = int(arg.strip())
print(f"⏱ Таймаут: {self.timeout}ms")
except ValueError:
print(f"❌ Нужно число: !timeout 1000")
def do_help(self, arg):
"""!help — справка."""
print("""
╔══════════════════════════════════════════════════════╗
║ ELM327 КОМАНДЫ (вводи как есть): ║
║ ATZ ATI ATE0 ATL0 ATS0 ║
║ ATH1 ATSP0 ATRV ATDPN ATSTxx ║
║ 0105 (ОЖ) 010C (RPM) 010D (Speed) ║
║ 03 (DTC) 07 (pending) 0902 (VIN) ║
║ ║
║ КОНСОЛЬ: ║
║ !status — статус устройства ║
║ !history [N] — последние ответы ║
║ !drain — очистить буфер ║
║ !mode raw — включить raw-режим ║
║ !timeout N — таймаут команд ║
║ !help — эта справка ║
║ !quit — выход ║
╚══════════════════════════════════════════════════════╝
""")
def do_quit(self, arg):
print("👋")
return True
def do_exit(self, arg):
return self.do_quit(arg)
do_q = do_quit
do_h = do_help
do_s = do_status
# ── HTTP-хелперы ──────────────────────────────────
def _get(self, path: str) -> dict:
url = f"{self.server}{path}"
req = urllib.request.Request(url)
with urllib.request.urlopen(req, timeout=35) as resp:
return json.loads(resp.read().decode())
def _post(self, path: str, data: dict) -> dict:
url = f"{self.server}{path}"
body = json.dumps(data).encode()
req = urllib.request.Request(url, data=body,
headers={"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode())
# ── main ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="ELM327 Remote Relay Console — удалённое управление через Android"
)
parser.add_argument("--server", default=DEFAULT_SERVER, help=f"URL сервера (default: {DEFAULT_SERVER})")
parser.add_argument("--timeout", type=int, default=500, help="Таймаут команд ms (default: 500)")
args = parser.parse_args()
console = ElmRelay(server=args.server, timeout=args.timeout)
# Проверим связь с сервером
try:
s = console._get("/api/v1/elm/raw/status")
ready = s.get("device_ready", False)
print(f"Сервер: {args.server}")
print(f"Устройство: {'✅ готово' if ready else '❌ не подключено (запусти Android-приложение)'}")
except Exception as e:
print(f"⚠️ Сервер недоступен: {e}")
print(f" Проверь: curl {args.server}/api/v1/ping")
try:
console.cmdloop()
except KeyboardInterrupt:
print("\n👋")
if __name__ == "__main__":
main()
+24
View File
@@ -22,6 +22,7 @@ from api.config import load
from api.routes import register as register_api
from api.dtc import register as register_dtc
from api.ping import register as register_ping
from api.raw_elm import bp as raw_bp, is_raw_mode
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
@@ -30,6 +31,23 @@ config = load()
register_api(app)
register_dtc(app)
register_ping(app)
app.register_blueprint(raw_bp)
# ── Режим RAW: отключаем все эндпоинты кроме /elm/raw/* ──
_RAW_PREFIX = "/api/v1/elm/raw"
@app.before_request
def _check_raw_mode():
"""В режиме RAW все эндпоинты кроме /elm/raw/* отключены."""
if is_raw_mode() and not request.path.startswith(_RAW_PREFIX):
# Разрешаем только статику и корень
if request.path not in ("/", "/elmer.apk") and not request.path.startswith("/static"):
return jsonify({
"error": "raw_mode_active",
"hint": "Сервер в режиме сырого взаимодействия с ELM327. "
"Все остальные эндпоинты отключены. "
"Используйте /api/v1/elm/raw/mode чтобы выключить."
}), 503
@app.route("/")
@@ -44,6 +62,12 @@ def download_apk():
return send_from_directory("static", "app-debug.apk", as_attachment=True, download_name="elmer.apk")
@app.route("/elm-raw.apk")
def download_raw_apk():
"""Прямая ссылка на APK Raw Relay."""
return send_from_directory("static", "elm-raw.apk", as_attachment=True, download_name="elm-raw.apk")
if __name__ == "__main__":
print(f"🌐 elmAI Web: http://localhost:5005")
app.run(host="0.0.0.0", port=5005, debug=False)
+1 -1
View File
@@ -64,7 +64,7 @@ def register(app):
def upload_session():
from flask import request, jsonify
from elmer.config import load
from elmer.db import Database
from api.db import Database
from elmer.diagnose import Diagnoser
from elmer.prompts import SYSTEM_PROMPT
+12 -3
View File
@@ -13,14 +13,23 @@
<img src="/static/logo.png" alt="elmAI" style="width:96px;height:96px;border-radius:20px;margin-bottom:10px;">
<h1>elmAI</h1>
<p class="subtitle">Диагностика авто через ELM327 + ИИ</p>
<p class="subtitle" style="font-size:12px;opacity:0.7;">v0.78.0-dev — 7 июня 2026</p>
<p class="subtitle" style="font-size:12px;opacity:0.7;">v1.18.0-dev — 7 июня 2026</p>
<div class="card" style="text-align:center;margin-bottom:20px;">
<p style="margin:0 0 10px 0;">📱 Скачай приложение на телефон:</p>
<a href="/static/app-debug.apk" style="color:#ff6b35;font-size:18px;font-weight:bold;text-decoration:none;">
<a href="/elmer.apk" style="color:#ff6b35;font-size:18px;font-weight:bold;text-decoration:none;">
⬇️ Скачать elmAI APK
</a>
<p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v0.78.0-dev • нажмите чтобы скачать</p>
<p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v1.18.0-dev • основное приложение</p>
</div>
<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-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.3.0-dev • ретранслятор команд</p>
</div>
<!-- Кнопка десктоп-диагностики скрыта — только для разработчика с прямым ELM327 -->