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

This commit is contained in:
Repinoid
2026-07-10 13:13:42 +04:00
parent 3ea7fc66d5
commit 95834a4245
46 changed files with 0 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# 2026-05-31
## Сервер
- 🔴 **api/db.py** — WAL, `request_id` UNIQUE, `close()`, контекстный менеджер
- 🔴 **api/routes.py** — идемпотентность upload, `/ping-llm` кэш 60с, `/chat` через roles
- 🔴 **obd/protocol.py** — `reset_input_buffer()`, не затирать ERROR
- 🔴 **brain/client.py** — `LLMError`, обработка 429/5xx/Timeout, дефолт gpt-oss-120b
- 🟡 **api/config.py** — `@lru_cache` на `load()`
- 🟡 **api/routes.py** — импорты наверх
- Удалён мёртвый `elmer/elmer/`
## Android
- 🔴 **ServerClient** — `request_id` (UUID), `X-Api-Key`, exponential backoff
- 🔴 **ElmProtocol** — дренаж буфера, не затирать ERROR
- 🔴 **SessionDb** — `ALTER TABLE` вместо DROP, индекс
- 🔴 **MainActivity** — throttle `/ping-llm`, `X-Api-Key`, `chatHistory` persistence
- 🔴 **ScriptRunnerService** — null-intent guard, убран мёртвый `paused`
- 🟡 **build.gradle.kts** — `buildConfigField API_KEY`
## Прочее
- VIN → logo
- Версия: 0.35.0-dev → 0.36.0-dev
- `.instructions.md` — создан
- `doc/architecture.md` — CI/CD расписан
- GitHub Actions удалён и восстановлен (несколько раз)
+13
View File
@@ -0,0 +1,13 @@
# 2026-06-03
## Сервер
- Дефолтный LLM: `api.aillm.ru` → `api.deepseek.com` (deepseek-v4-flash)
- `doc/git-guide.md` — создана инструкция по git
- `doc/architecture.md` — уточнён деплой (git clone, не tar.gz)
## Android
- `build.gradle.kts` — `buildFeatures.buildConfig = true`
## Прочее
- Версия: 0.36.0-dev
- Сервер переведён на git-клон вместо tar.gz (удалён elmer.old)
+21
View File
@@ -0,0 +1,21 @@
# 2026-06-05
## Сервер
- `car_info` в upload — водитель может описать авто текстом, передаётся в LLM
- GitHub Actions разрешён для APK (теперь CI собирает и деплоит)
## Android
- **`ElmChecker.kt`** — новый: проверка ELM327 (версия, напряжение, BT)
- Поле «что за машина» — ввод описания авто перед диагностикой
- Индикаторы загрузки — улучшен UI
- Подпись APK — фиксированный ключ (APK обновляется без удаления)
- **GitHub Actions** — автосборка + деплой APK на сервер
- `gradlew` — добавлен в репо
- `ServerClient` — `ping()` / `pingLlm()`
- 🔴 **ElmChecker** — разделён на `DeviceInfo` (AT, без зажигания) + `EcuData` (VIN/PID)
- 🔴 **ElmChecker** — фикс: `checkDevice()` дёргал `connectAndInit()` второй раз → сбрасывал ELM → пустые ответы
- 🔴 **MainActivity** — секундомер при проверке ELM
- 🟡 **ElmChecker** — `AT@2` добавлен, фильтр бинарного мусора
## Прочее
- Версия: 0.37.0-dev → 0.38.0-dev
+26
View File
@@ -0,0 +1,26 @@
# 2026-06-06
## Сервер
- 🔴 `api/db.py` — `device_uuid` колонка, `phone_lang`, `phone_tz`, `phone_display`, `save_dtc_scan()`
- 🔴 `api/routes.py` — `POST /api/v1/dtc/decode`, `POST /api/v1/dtc/upload` (+ идемпотентность)
- 🔴 `api/routes.py` — промпт-билдер: защита от пустых `{}` и `None` списков
- 🔴 `api/parser.py` — VIN из CAN multi-frame (0: 1: 2:) и ISO-TP (10 14 21 22)
- 🔴 `api/parser.py` — DTC из raw HEX: фикс `mode="43"` вместо `"03"`
- `brain/prompts.py` — правило №8: неточности данных (дубль DTC, клон, напряжение)
- `brain/prompts.py` — правило №7 (было №11): никогда не раскрывать модель/создателя
- `doc/dtc_codes.txt` — справочник 128 кодов DTC
- `doc/SETUP.md` — инструкция для нового разработчика
- `tests/test_all.py` — 91 тест (ELM, DTC, VIN, БД, идемпотентность, экстремальные)
## Android
- 🔴 Кнопка «⚠️ ОШИБКИ» — сканирование DTC (mode 03 + 07)
- 🔴 Кнопка «🔍 ДИАГНОСТИКА» блокирована до скана ошибок
- 🔴 `ElmChecker.scanDtc()` — отдельный метод сканирования ошибок
- 🔴 `SharedPreferences` — `device_uuid` генерируется при первом запуске
- `ScriptRunnerService.buildClientInfo()` — язык, часовой пояс, разрешение
- 3 кнопки в рамке «🔍 Проверка»: Сервер | ELM | ЭБУ
## Прочее
- Версия: 0.39.0-dev → 0.40.0-dev
- APK через nginx напрямую (правило в `.instructions.md`)
- На ВМ установлен Android SDK + Gradle для сборки
+46
View File
@@ -0,0 +1,46 @@
# Планы на 2026-06-07 (вечер)
## 1. 🔴 Исправить 16-ричный вывод параметров
**Проблема**: при считывании PID'ов на экран выводятся hex-коды вместо человеческих значений.
- Температура ОЖ: `41053C` вместо `ОЖ: 20°C`
- Обороты: `410C1A2B` вместо `Обороты: 850 об/мин`
**Причина**: ObdDecoder.decode() не чистит `\r` `\n` из raw ответа → parseInt падает → возвращает raw.
**Фикс**: добавить `.replace("\r", "").replace("\n", "")` в clean.
## 2. 🟡 Кнопки СЕРВЕР/ELM/ЭБУ — только до первой диагностики
После первого успешного "done" — убрать эти кнопки. Они нужны только для первоначальной проверки.
## 3. 🟢 Динамические тесты (две новые кнопки)
Появляются после первичной диагностики вместо СЕРВЕР/ELM/ЭБУ.
### Тест 1: «На месте»
- Подсказка: «Нажмите на педаль газа, поднимите обороты до 3000, держите 3-4 секунды, затем резко сбросьте газ. Нажмите СТАРТ когда готовы.»
- Кнопка: «⏱ Тест на месте» → после нажатия → «▶ Старт»
- После Старт: мониторинг RPM, MAP, STFT, LTFT — 5-10 секунд
- Анализ: отклик дросселя, провалы, богатая/бедная смесь при сбросе
### Тест 2: «В движении»
- Подсказка: «Включите 2-ю передачу (АКПП — ручной режим), разгонитесь до ~3000 об/мин, держите несколько секунд, затем резко сбросьте газ. Нажмите СТАРТ ДО начала движения — программа сама отследит параметры.»
- Кнопка: «🚗 Тест в движении» → после нажатия → «▶ Старт (нажать до движения)»
- После Старт: мониторинг скорости, RPM, нагрузки, STFT — автоматическое определение начала движения и сброса газа
- Анализ: поведение под нагрузкой, детонация, пропуски
## 4. 🟢 Flow кнопок
```
[Начало] → СЕРВЕР | ELM | ЭБУ | ОШИБКИ | ДИАГНОСТИКА
↓ после диагностики
[Результат] → ✕ Закрыть | ⏱ Тест на месте | 🚗 Тест в движении | поле ввода + ➤
```
## 5. 🟡 История — не терять
При переключении между тестами и чатом — сохранять вывод.
## 6. 🔧 Скрипты для тестов
Нужно создать server-side скрипты:
- `script/dynamic_idle.json` — тест на месте (RPM, MAP, STFT, LTFT, дроссель, нагрузка)
- `script/dynamic_drive.json` — тест в движении (RPM, скорость, нагрузка, STFT, MAP)
Оба скрипта должны:
- Считывать параметры с высокой частотой (ATAT1 если v2)
- Автоматически определять фазы: разгон → удержание → сброс
- Отправлять результаты на сервер → LLM-анализ
+96
View File
@@ -0,0 +1,96 @@
# 2026-06-07 — Пробинг ELM327, трехуровневый профиль
## Проблема
`obd/protocol.py` — `init()` посылал ATAT1 и ATSTxx всем устройствам.
Большинство клонов v1.5 не знают этих команд → тишина → `_exec()` делает до 10 ретраев → всё висит на десятки секунд.
## Решение
### 1. Трехуровневый пробинг (`obd/probe.py`)
Каскадный тест: сначала база, потом улучшения.
```
Уровень 0 (база, все клоны):
ATE0 ATL0 ATS0 ATH1 ATSP0 ATDPN ATRV ATI
→ хоть одна не ответила OK → НЕИСПРАВЕН
Уровень 1 (хорошие клоны):
ATAT1
→ OK → уровень 1
Уровень 2 (настоящие ELM):
ATCAF1 ATCFC1
→ OK → уровень 2
```
Результат сохраняется в `device_profiles` по ключу BT MAC.
### 2. Fix `obd/protocol.py` init()
`init()` теперь шлёт ТОЛЬКО базу (уровень 0): ATE0 ATL0 ATS0 ATH1 ATSP0.
Методы под уровень:
- `init_base()` — то же что init()
- `init_l1()` — + ATAT1
- `init_l2()` — + ATAT1 + ATCAF1 + ATCFC1
### 3. Таблица `device_profiles` (`api/db.py`)
```sql
CREATE TABLE device_profiles (
mac TEXT PRIMARY KEY, -- BT MAC
level INTEGER NOT NULL, -- 0/1/2
elm_version TEXT, -- ATI ответ
elm_desc TEXT, -- AT@1 (если есть)
protocol TEXT, -- ATDPN
voltage TEXT, -- ATRV
supported TEXT, -- JSON: ["ATE0","ATL0",...]
unsupported TEXT, -- JSON: ["ATAT1","ATCAF1",...]
first_seen TEXT NOT NULL,
last_seen TEXT NOT NULL
);
```
### 4. Скрипты под уровень (`api/scripts.py`)
- `build_script_l0()` — 5 PIDs + ошибки (однокадровые)
- `build_script_l1()` — 8 PIDs + VIN + ошибки
- `build_script_l2()` — 14 PIDs + VIN + калибровки + все ошибки
### 5. Эндпоинты (`api/routes.py`)
- `POST /api/v1/elm/probe` — принимает MAC + ответы, возвращает профиль
- `GET /api/v1/elm/profile/<mac>` — достаёт из кэша
- `GET /api/v1/script?level=0|1|2` — скрипт под уровень
### 6. Рефакторинг: разделение на независимые сервисы
Файлы разбиты по тематике, каждый — отдельный сервис:
```
obd/
commands.py — Каталог ВСЕХ AT-команд ELM327 (с метаданными)
classifier.py — Классификация ответов + определение уровня
connection.py — Транспортный слой (SerialTransport)
probe.py — Пробинг (использует commands + classifier)
protocol.py — Стейт-машина AndrOBD (использует connection)
state.py — Состояния/типы ответов
timing.py — Адаптивный таймаут
```
**Принцип**: каждый модуль делает одно дело, не дублирует логику.
- `commands.py` — единственный источник правды о командах
- `classifier.py` — единственное место классификации ответов
- `connection.py` — единственное место I/O
- `routes.py` — тонкая прослойка, без бизнес-логики
### 5. Эндпоинт (`api/routes.py`)
`POST /api/v1/elm/probe` — принимает MAC, возвращает профиль.
Сервер сам шлёт команды через реле (Android ElmForwardService).
## Ключевое правило
**Нет в профиле → не слать. Никаких ретраев на неизвестное.**
+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.