refactor: move legacy docs to doc/legacy/ with opus/sonnet subfolders
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# Android-клиент: баги найденные при ревизии (2026-05-25)
|
||||
|
||||
Коммиты с исправлениями: `d57336a` (предыдущий раунд) → `7f9b83c` (этот раунд).
|
||||
|
||||
---
|
||||
|
||||
## Баг 1 — КРИТИЧЕСКИЙ: NetworkOnMainThreadException при подключении
|
||||
|
||||
### Что было неверно
|
||||
|
||||
`onStartCommand` вызывался на **главном (UI) потоке**. Из него напрямую вызывались `btConnect()` и `tcpConnect()`.
|
||||
|
||||
Внутри `btConnect()`:
|
||||
```kotlin
|
||||
btSocket?.connect() // ← БЛОКИРУЮЩИЙ вызов на main thread
|
||||
```
|
||||
|
||||
Внутри `tcpConnect()`:
|
||||
```kotlin
|
||||
tcpSocket = Socket(host, port) // ← БЛОКИРУЮЩИЙ вызов на main thread
|
||||
```
|
||||
|
||||
### Почему это баг
|
||||
|
||||
Android с версии 3.0 (Honeycomb, API 11) запрещает сетевые операции на главном потоке. Если нарушение — **`NetworkOnMainThreadException`** и краш при запуске. Даже если бы не падало — UI зависал бы на время соединения (ANR через 5 секунд).
|
||||
|
||||
### Как исправлено
|
||||
|
||||
Оба вызова обёрнуты в `thread { }`:
|
||||
|
||||
```kotlin
|
||||
if (debug != null) {
|
||||
val p = debug.split(":")
|
||||
thread(name = "ElmConnect", isDaemon = true) {
|
||||
tcpConnect(p[0], p.getOrNull(1)?.toIntOrNull() ?: 35000)
|
||||
}
|
||||
} else {
|
||||
intent.getStringExtra(EXTRA_DEVICE_MAC)?.let { mac ->
|
||||
thread(name = "ElmConnect", isDaemon = true) { btConnect(mac) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`isDaemon = true` — поток не удерживает JVM, умирает вместе с сервисом.
|
||||
|
||||
---
|
||||
|
||||
## Баг 2 — КРИТИЧЕСКИЙ: TCP-режим слал данные на obdai.ru вместо локального сервера
|
||||
|
||||
### Что было неверно
|
||||
|
||||
В `MainActivity`, в ветке TCP-режима:
|
||||
|
||||
```kotlin
|
||||
putExtra(ElmForwardService.EXTRA_SERVER_URL, "https://obdai.ru/api/v1/raw-obd")
|
||||
```
|
||||
|
||||
URL захардкожен на продакшн-сервер.
|
||||
|
||||
### Почему это баг
|
||||
|
||||
TCP-режим предназначен для **теста без ELM327**: mock ELM327 (`tools/mock_elm327.py`) и Flask-сервер (`web/app.py`) запускаются на одной машине в локальной сети. Поле URL в UI в этом режиме занято адресом mock-устройства (`192.168.X.X:35000`), поэтому у пользователя нет способа указать другой сервер.
|
||||
|
||||
Результат: телефон подключается к локальному mock по TCP, но OBD-данные уходят на `obdai.ru` — тест ничего не проверяет.
|
||||
|
||||
### Как исправлено
|
||||
|
||||
Сервер выводится автоматически из того же хоста, что и устройство, на стандартный порт Flask (5005):
|
||||
|
||||
```kotlin
|
||||
val deviceHost = debugHost.split(":")[0]
|
||||
val localServerUrl = "http://$deviceHost:5005/api/v1/raw-obd"
|
||||
```
|
||||
|
||||
Итого: ввёл `192.168.1.42:35000` → mock на `:35000`, сервер на `http://192.168.1.42:5005`.
|
||||
|
||||
---
|
||||
|
||||
## Баг 3 — МАЛЫЙ: символ `\n` попадал в raw-строку
|
||||
|
||||
### Что было неверно
|
||||
|
||||
В `loop()` при накоплении байт из устройства:
|
||||
|
||||
```kotlin
|
||||
} else if (c != '\r') sb.append(c)
|
||||
```
|
||||
|
||||
Фильтровался только `\r`, но не `\n`.
|
||||
|
||||
### Почему это баг
|
||||
|
||||
ELM327 отвечает в формате `DATA\r\n>`. Разделитель ответа — `>`. Код правильно использует `>` как признак конца пакета, но `\n` перед `>` или внутри многострочного ответа накапливается в `sb`.
|
||||
|
||||
Пример: ответ `4 1 05 4B\r\n>` → в буфере будет `4 1 05 4B\n` вместо `4 1 05 4B`. `.trim()` при отправке на сервер снимает крайние `\n`, но если `\n` внутри многострочного ответа — он остаётся. Сервер должен быть устойчив, но лучше не давать мусор.
|
||||
|
||||
### Как исправлено
|
||||
|
||||
```kotlin
|
||||
} else if (c != '\r' && c != '\n') sb.append(c)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Итог по файлам
|
||||
|
||||
| Файл | Изменение |
|
||||
|------|-----------|
|
||||
| `ElmForwardService.kt` | `btConnect`/`tcpConnect` → в `thread {}`, скип `\n` |
|
||||
| `MainActivity.kt` | TCP-режим → URL из IP устройства + порт 5005 |
|
||||
@@ -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,63 @@
|
||||
# Задание: аудит проекта elmAI на баги и уязвимости
|
||||
|
||||
## Контекст
|
||||
|
||||
elmAI — Android-приложение + Python-сервер для диагностики авто через ELM327.
|
||||
Текущая версия: v0.70.0-dev, ветка dynamic-tests.
|
||||
Уже было найдено и исправлено ~15 багов, но гарантии что всё чисто — нет.
|
||||
|
||||
## Что анализировать
|
||||
|
||||
Проверь код на:
|
||||
1. Гонки потоков (Android: несколько потоков работают с UI и BT одновременно)
|
||||
2. Утечки ресурсов (BluetoothSocket, SQLite-соединения, таймеры)
|
||||
3. NPE / краши (особенно при отсутствии Bluetooth, ELM, интернета)
|
||||
4. Логические ошибки (состояния кнопки-трансформера, порядок инициализации)
|
||||
5. Сервер: SQL-инъекции, валидация входных данных, таймауты, OOM
|
||||
6. Потерю данных (динамические тесты, история, офлайн-режим)
|
||||
|
||||
## Какие файлы читать
|
||||
|
||||
### Android (максимально критичные)
|
||||
1. `android/app/src/main/java/ru/elmer/client/ui/MainActivity.kt` — главный файл, стейт-машина, UI
|
||||
2. `android/app/src/main/java/ru/elmer/client/elm/ElmChecker.kt` — BT-подключение, DTC, ЭБУ
|
||||
3. `android/app/src/main/java/ru/elmer/client/script/DynamicCollector.kt` — сбор 12 PID 250мс
|
||||
4. `android/app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt` — фоновая диагностика
|
||||
5. `android/app/src/main/java/ru/elmer/client/server/ServerClient.kt` — HTTP к серверу
|
||||
6. `android/app/src/main/java/ru/elmer/client/db/SessionDb.kt` — локальная БД
|
||||
|
||||
### Сервер
|
||||
7. `api/routes.py` — эндпоинты (script, upload, chat, ping)
|
||||
8. `api/db.py` — SQLite-схема и миграции
|
||||
9. `api/scripts.py` — скрипты L0/L1/L2/dynamic
|
||||
10. `brain/prompts.py` — SYSTEM_PROMPT, DYNAMIC_PROMPT
|
||||
11. `brain/client.py` — HTTP-клиент к LLM
|
||||
|
||||
## Что УЖЕ исправлено (не трать время)
|
||||
|
||||
- Двойной вызов checkLlm/checkEcu
|
||||
- setIndicator не в UI-потоке
|
||||
- Кнопка СТОП не работала
|
||||
- ECU не зеленел после сканирования
|
||||
- Дублирование данных при отправке dynamic_samples
|
||||
- elmChecker переиспользуется между операциями
|
||||
- 12 PID захардкожены (не дёргаем сервер)
|
||||
- Диагноз сохраняется в локальную БД
|
||||
- Миграции ALTER TABLE для старых БД
|
||||
- deploy.sh на master вместо fat-client
|
||||
|
||||
## Куда сохранить результат
|
||||
|
||||
Создай файл `doc/audit-2026-06-07.md` с отчётом.
|
||||
Формат:
|
||||
```
|
||||
## Найдено
|
||||
### 🔴 Критичные
|
||||
- описание бага, файл, строка, как исправить
|
||||
|
||||
### 🟡 Средние
|
||||
...
|
||||
|
||||
### 🟢 Косметика
|
||||
...
|
||||
```
|
||||
@@ -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.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Динамические тесты — архитектура
|
||||
|
||||
> v0.49.0-dev, 7 июня 2026
|
||||
> Ветка: `dynamic-tests`
|
||||
|
||||
## Концепция
|
||||
|
||||
Два теста: **на месте** и **в движении**. Разница — только в подсказке юзеру.
|
||||
Физически оба делают одно: опрашивают 12 PID каждые 250мс, копят в памяти.
|
||||
|
||||
## Алгоритм работы (user flow)
|
||||
|
||||
```
|
||||
1. Ошибки → scanDtc() — считывание DTC
|
||||
2. Диагностика → обычный скрипт L0/L1/L2 на сервер → LLM
|
||||
3. LLM сказал сделать тест на месте →
|
||||
⏱ На месте → СТАРТ → ... → СТОП → данные в памяти
|
||||
4. Диагностика → обычный скрипт + динамические данные → LLM
|
||||
5. LLM сказал сделать тест в движении →
|
||||
🚗 В движении → СТАРТ → ... → СТОП → данные в памяти
|
||||
6. Диагностика → обычный скрипт + динамические данные → LLM (финал)
|
||||
```
|
||||
|
||||
## Android → Сервер
|
||||
|
||||
При нажатии **Диагностика**:
|
||||
1. Выполняется обычный скрипт (AT-команды, PID'ы)
|
||||
2. Если есть `dynamicSamples` (данные теста) — прикрепляются к тому же запросу
|
||||
3. POST `/api/v1/session/upload` с полем `dynamic_samples`
|
||||
4. Сервер по наличию `dynamic_samples` понимает, что пришёл динамический тест
|
||||
5. Выбирает промпт: `DYNAMIC_PROMPT` если только динамика, или `SYSTEM_PROMPT + DYNAMIC_PROMPT` если и то и то
|
||||
|
||||
## Что надо сделать
|
||||
|
||||
### Уже сделано (написан код, не проверен)
|
||||
- [x] Сервер: `build_dynamic_script()` — скрипт 12 PID 250мс
|
||||
- [x] Сервер: `?mode=dynamic` в routes.py
|
||||
- [x] Сервер: `DYNAMIC_PROMPT` в prompts.py
|
||||
- [x] Android: кнопки «⏱ На месте» / «🚗 В движении» (layout)
|
||||
- [x] Android: `DynamicCollector.kt` — сбор 12 PID 250мс
|
||||
- [x] Android: `ElmChecker.kt` — добавлены методы для работы с DynamicCollector
|
||||
- [x] Android: MainActivity — СТАРТ/СТОП, данные в памяти
|
||||
- [x] Android: общий таймер `tv_timer`
|
||||
|
||||
### Надо доделать
|
||||
- [ ] Android: при старте Диагностики — прикрепить `dynamicSamples` к upload
|
||||
- [ ] Сервер: принимать `dynamic_samples` в upload_session, выбирать промпт
|
||||
- [ ] Отладка на реальной машине
|
||||
|
||||
### До отправки — в памяти
|
||||
```kotlin
|
||||
data class DynamicSample(
|
||||
val ts: Long, // System.currentTimeMillis()
|
||||
val responses: List<RawResponse> // 12 ответов ELM
|
||||
)
|
||||
|
||||
val samples = mutableListOf<DynamicSample>() // в памяти, быстро
|
||||
```
|
||||
|
||||
### После СТОП — попытка отправки
|
||||
```kotlin
|
||||
1. Собираем все samples в JSON
|
||||
2. POST /api/v1/session/upload
|
||||
3. Если 200 → ОК, забыли
|
||||
4. Если ошибка → пишем ВСЁ в SessionDb
|
||||
```
|
||||
|
||||
### Локальное хранение — через SessionDb (уже есть)
|
||||
```sql
|
||||
-- Таблица sessions (уже существует)
|
||||
session_type = "dynamic" -- отличаем от обычных
|
||||
|
||||
-- Таблица responses (уже существует)
|
||||
-- Каждый сэмпл = одна запись в responses:
|
||||
-- session_id, step_id = "sample_N", cmd = "batch",
|
||||
-- raw = JSON всего опроса, decoded = "запись №N"
|
||||
```
|
||||
|
||||
**Плюсы**: не надо новой таблицы, `SessionDb` уже умеет `createSession`/`addResponse`/`getResponses`.
|
||||
|
||||
**Объём**: 30 секунд × 4 опроса/с × 12 PID = 1440 записей ≈ ~100KB — норм.
|
||||
|
||||
### Ретрай непосланных сессий
|
||||
При старте приложения: `db.getUnuploadedSessions()` → отправить → пометить `uploaded=1`.
|
||||
|
||||
## Промпт для LLM (сервер)
|
||||
|
||||
Отдельный `DYNAMIC_PROMPT` в `brain/prompts.py`:
|
||||
|
||||
```
|
||||
Ты — эксперт по диагностике. Получены временные ряды 12 параметров с интервалом 250мс.
|
||||
Проанализируй:
|
||||
1. Отклик дросселя — есть ли задержка/провалы
|
||||
2. STFT/LTFT — богатая или бедная смесь под нагрузкой и при сбросе
|
||||
3. RPM — плавность роста/падения, пропуски
|
||||
4. MAP — соответствует ли оборотам
|
||||
5. Зажигание — есть ли коррекция, детонация
|
||||
6. Аномалии — резкие скачки, выбросы
|
||||
Отвечай кратко: 2-3 предложения вывода, затем по пунктам что не так.
|
||||
```
|
||||
|
||||
## Что НЕ делаем
|
||||
- GPS / акселерометр — не сейчас
|
||||
- Автодетект фаз (разгон/сброс) — ручной СТАРТ/СТОП
|
||||
- Отправка пачками в реальном времени — копим всё до СТОП
|
||||
@@ -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 — только статус и лог
|
||||
@@ -0,0 +1,144 @@
|
||||
# Полевой тест elmAI v0.48.0 — 7 июня 2026
|
||||
|
||||
## Что тестируем
|
||||
|
||||
APK: https://obdai.ru/elmer.apk (v0.48.0)
|
||||
Сервер: https://obdai.ru
|
||||
|
||||
## Что сделано (все изменения)
|
||||
|
||||
### Пробинг ELM327 (НОВОЕ)
|
||||
- Сервер больше не шлёт ATAT1/ATSTxx слепо — не вешает клоны
|
||||
- При первом подключении нового ELM — каскадный тест:
|
||||
- Уровень 0: ATE0 ATL0 ATS0 ATH1 ATSP0 ATDPN ATRV ATI (все клоны)
|
||||
- Уровень 1: +ATAT1 (хорошие клоны)
|
||||
- Уровень 2: +ATCAF1 ATCFC1 (настоящий ELM)
|
||||
- Результат сохраняется в БД по BT MAC
|
||||
- Нет в профиле → не слать. Никаких ретраев на неизвестное.
|
||||
|
||||
### Скрипты под уровень устройства
|
||||
- L0: 5 PIDs + ошибки (однокадровые, без VIN)
|
||||
- L1: 8 PIDs + VIN + stored/pending ошибки
|
||||
- L2: 14 PIDs + VIN + калибровки + все ошибки
|
||||
|
||||
### Рефакторинг сервера
|
||||
- `obd/commands.py` — каталог ВСЕХ AT-команд с метаданными
|
||||
- `obd/classifier.py` — классификация ответов
|
||||
- `obd/connection.py` — транспортный слой
|
||||
- Код разбит на независимые модули
|
||||
|
||||
### Android: фикс вывода
|
||||
- Статус: `append("\n...")` вместо `text =` (не перекрывается)
|
||||
- Таймер: отдельный TextView, тикает только во время обмена (как Engine Time)
|
||||
- TestService: адаптивные таймауты вместо жёстких sleep
|
||||
- TestService: добавлен ATS0 в инициализацию
|
||||
|
||||
---
|
||||
|
||||
## Как тестировать
|
||||
|
||||
### Подготовка
|
||||
1. Скачай и установи APK: https://obdai.ru/elmer.apk
|
||||
2. Вставь ELM327 в OBD2-разъём машины
|
||||
3. Заведи двигатель (для части PIDs нужен работающий двигатель)
|
||||
4. Сопряги ELM327 с телефоном по Bluetooth (пароль 1234 или 0000)
|
||||
5. Открой приложение Elmer
|
||||
|
||||
### Тест 1: Кнопка «ТЕСТ» (без сервера)
|
||||
|
||||
Поле ввода: **оставь пустым** (Bluetooth-режим)
|
||||
|
||||
Нажми **«🧪 ТЕСТ (всё локально)»**
|
||||
|
||||
Ожидаемое поведение:
|
||||
```
|
||||
══════════════════
|
||||
🔧 ELMER TEST v0.6
|
||||
Режим: Bluetooth
|
||||
══════════════════
|
||||
Найден: OBDII (AA:BB:CC:...)
|
||||
⏳ Подключение...
|
||||
✅ BT OK
|
||||
─── ШАГ 1: Связь ───
|
||||
→ ATZ ⏱ 2с
|
||||
← ELM327 v1.5
|
||||
✅ СВЯЗЬ ЕСТЬ!
|
||||
─── ШАГ 2: Инициализация ───
|
||||
→ ATE0 ⏱ 0с
|
||||
← OK
|
||||
→ ATL0 ⏱ 0с
|
||||
← OK
|
||||
→ ATS0 ⏱ 0с
|
||||
← OK
|
||||
→ ATH1 ⏱ 0с
|
||||
← OK
|
||||
→ ATSP0 ⏱ 1с
|
||||
← OK
|
||||
─── ШАГ 3: VIN ───
|
||||
→ 0902 ⏱ 3с
|
||||
← 49 02 01 57 56...
|
||||
→ VIN: WVWZZZ...
|
||||
─── ШАГ 4: Ошибки ───
|
||||
→ 03 ⏱ 1с
|
||||
← 43 00
|
||||
→ DTC stored: none
|
||||
→ 07 ⏱ 1с
|
||||
← 47 00
|
||||
→ DTC pending: none
|
||||
─── ШАГ 5: Параметры ───
|
||||
→ 0105 ⏱ 0с
|
||||
← 41 05 5A
|
||||
→ ОЖ: 50 °C
|
||||
... (10 параметров)
|
||||
✅ ТЕСТ ПРОЙДЕН!
|
||||
```
|
||||
|
||||
**Что проверять:**
|
||||
- [ ] Таймер ⏱ тикает ТОЛЬКО во время команд (АТZ, 0902, 0105...)
|
||||
- [ ] В паузах между командами таймер пустой
|
||||
- [ ] Строки НЕ перекрываются (каждая с новой строки)
|
||||
- [ ] Тест НЕ виснет (раньше висел на ATAT1)
|
||||
- [ ] Если ELM не отвечает — пишет «❌ ELM не отвечает» за ~5 секунд, не дольше
|
||||
|
||||
### Тест 2: Кнопка «Диагностировать» (с сервером)
|
||||
|
||||
Поле ввода: **оставь пустым** (Bluetooth-режим)
|
||||
|
||||
Нажми **«🚗 Диагностировать (сервер)»**
|
||||
|
||||
**Что проверять:**
|
||||
- [ ] Статус: «BT: AA:BB:CC...» → «BT: OK»
|
||||
- [ ] Появляются «← ...» (ответы ELM) и «→ ...» (команды от сервера)
|
||||
- [ ] В конце — диагноз от LLM в рамке ══════
|
||||
- [ ] Таймер тикает только когда «→ команда» отправляется
|
||||
- [ ] Нет зависаний
|
||||
|
||||
### Тест 3: Если есть второй ELM327
|
||||
|
||||
Подключи другой ELM (другой клон/версия):
|
||||
- [ ] Приложение находит его по имени (OBD/ELM в названии)
|
||||
- [ ] Тест должен работать на любом клоне
|
||||
- [ ] Профиль сохраняется в БД на сервере (по MAC)
|
||||
|
||||
---
|
||||
|
||||
## Возможные проблемы
|
||||
|
||||
| Симптом | Вероятная причина |
|
||||
|---------|-------------------|
|
||||
| «❌ ELM не отвечает» на ATZ | ELM не вставлен в OBD2 или нет питания |
|
||||
| Долго висит на ATSP0 | Машина не поддерживает авто-протокол |
|
||||
| VIN не читается | Старая машина без mode 09 |
|
||||
| «❌ BT: ...» | ELM не сопряжён в настройках Bluetooth |
|
||||
| Пустой экран после нажатия | Не даны разрешения Bluetooth (Android 12+) |
|
||||
|
||||
---
|
||||
|
||||
## После теста
|
||||
|
||||
Вернись к компу и скажи:
|
||||
1. Какие ELM тестировал (версия, цвет, название)
|
||||
2. Прошёл ли тест
|
||||
3. Прошла ли диагностика с сервером
|
||||
4. Были ли зависания
|
||||
5. Скриншоты экрана (если можно)
|
||||
@@ -0,0 +1,75 @@
|
||||
# Инструкция по Git
|
||||
|
||||
> v0.36.0-dev, 3 июня 2026
|
||||
|
||||
## Структура репозиториев
|
||||
|
||||
Один проект — два git-репо в одной папке:
|
||||
|
||||
```
|
||||
elmer/ ← git-репо #1 (gitea)
|
||||
├── api/ серверный код
|
||||
├── brain/ LLM-клиент
|
||||
├── obd/ ELM327
|
||||
├── web/ Flask, сайт, APK
|
||||
├── doc/ документация
|
||||
├── android/ ← git-репо #2 (github) — Android-приложение
|
||||
│ ├── app/ исходники Kotlin
|
||||
│ ├── build.gradle.kts сборка
|
||||
│ └── ...
|
||||
├── config.yaml конфиг сервера
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Ремоуты
|
||||
|
||||
| Репо | URL |
|
||||
|------|-----|
|
||||
| Сервер (elmer/) | `https://gitea.services.ngcloud.ru/Nail/elmer.git` |
|
||||
| Android (elmer/android/) | `https://github.com/Repinoid/elmer-android.git` |
|
||||
|
||||
## Команды
|
||||
|
||||
### Сервер
|
||||
|
||||
```bash
|
||||
cd elmer
|
||||
git pull origin master # забрать изменения
|
||||
git add -A # добавить всё
|
||||
git commit -m "..." # закоммитить
|
||||
git push origin master # отправить
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
cd elmer/android
|
||||
git pull origin master # забрать изменения
|
||||
git add -A # добавить всё
|
||||
git commit -m "..." # закоммитить
|
||||
git push origin master # отправить
|
||||
```
|
||||
|
||||
## Деплой на сервер (obdai.ru)
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
|
||||
"cd /opt/elmer && git pull origin master && sudo systemctl restart elmer"
|
||||
```
|
||||
|
||||
## Сборка APK (на сервере)
|
||||
|
||||
```bash
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
|
||||
"export ANDROID_SDK_ROOT=\$HOME/android-sdk && \
|
||||
export ANDROID_HOME=\$ANDROID_SDK_ROOT && \
|
||||
cd /opt/elmer/android && \
|
||||
./gradlew clean assembleDebug && \
|
||||
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/"
|
||||
```
|
||||
|
||||
## Важно
|
||||
|
||||
- `android/` в `.gitignore` родительского репо — его изменения коммитятся отдельно
|
||||
- Ветка везде `master`
|
||||
- Перед началом работы всегда делать `git pull` в обоих репо
|
||||
@@ -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 удалён и восстановлен (несколько раз)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 для сборки
|
||||
@@ -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-анализ
|
||||
@@ -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).
|
||||
|
||||
## Ключевое правило
|
||||
|
||||
**Нет в профиле → не слать. Никаких ретраев на неизвестное.**
|
||||
@@ -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,113 @@
|
||||
# Новая морда elmAI — v0.57.0+
|
||||
|
||||
> Проект из morda.txt, обсуждение 7 июня 2026
|
||||
|
||||
## Макет
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 🔧 elmAI v0.57.0 │
|
||||
│ [📡●] [🔌●] [🚗●] [🧠●] │ иконки + светофоры (тап = перепроверка)
|
||||
├─────────────────────────────────────┤
|
||||
│ [ ОШИБКИ / ДИАГНОСТИКА / СТАРТ / СТОП ] │ одна кнопка-трансформер
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ поле вывода │ ScrollView
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [📋 История] │ кнопка
|
||||
├─────────────────────────────────────┤
|
||||
│ [________________________] [➤] │ поле ввода 2 строки + кнопка
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Светофоры
|
||||
|
||||
| Иконка | Текст | Зелёный | Жёлтый | Красный |
|
||||
|--------|-------|---------|--------|---------|
|
||||
| 📡 | Сервер | ping < 3с | проверка... | нет связи |
|
||||
| 🔌 | ELM | BT + ATI ok | подключение... | нет ELM |
|
||||
| 🚗 | ЭБУ | 0100 ok | — | нет связи с ЭБУ |
|
||||
| 🧠 | LLM | ping-llm ok | проверка... | нет доступа |
|
||||
|
||||
- Жёлтый только во время проверки
|
||||
- Тап по иконке → перепроверка конкретного компонента
|
||||
- LLM проверяется только если Сервер зелёный
|
||||
|
||||
### Таймауты
|
||||
- ELM: 2 попытки connect по 4с = 8с макс → красный
|
||||
- Сервер: HTTP GET /api/v1/ping, таймаут 3с → красный
|
||||
- LLM: HTTP GET /api/v1/ping-llm, таймаут 5с → красный
|
||||
- Перепроверка при тапе: сбрасывает на жёлтый, запускает проверку
|
||||
|
||||
## Кнопка-трансформер
|
||||
|
||||
Одна кнопка, меняет текст/цвет/действие:
|
||||
|
||||
```
|
||||
[ОШИБКИ] ──→ сканирование DTC ──→ [ДИАГНОСТИКА]
|
||||
↓
|
||||
если сервер ✅ → обычный скрипт + LLM
|
||||
если сервер ❌ → пояснение в выводе
|
||||
↓
|
||||
[СТАРТ]
|
||||
↓
|
||||
газ 3000 → сброс → [СТОП]
|
||||
↓
|
||||
данные в памяти → жми ➤
|
||||
```
|
||||
|
||||
## Поле ввода + кнопка ➤
|
||||
|
||||
- Всегда активно, 2 строки
|
||||
- **Обычный режим**: ввёл текст → ➤ → отправка в чат с LLM
|
||||
- **После СТОП**: ➤ отправляет накопленные данные теста на сервер + текст (если есть)
|
||||
- После отправки → результат LLM в выводе
|
||||
|
||||
## История
|
||||
|
||||
- Кнопка, как сейчас
|
||||
- Выбор записи → диагноз + 📤 Поделиться
|
||||
- После Share — возврат в приложение (стандартное поведение Android)
|
||||
|
||||
## Flow в деталях
|
||||
|
||||
### 1. Запуск приложения
|
||||
- Светофоры: ELM жёлтый, Сервер жёлтый, ЭБУ красный, LLM красный
|
||||
- Проверка ELM (8с) → зелёный/красный
|
||||
- Проверка Сервера (3с) → зелёный/красный
|
||||
- Если Сервер зелёный → проверка LLM (5с) → зелёный/красный
|
||||
- Если ELM зелёный → проверка ЭБУ (через 0100)
|
||||
|
||||
### 2. Ошибки
|
||||
- Тап ОШИБКИ → scanDtc() → результат в выводе
|
||||
- Кнопка → ДИАГНОСТИКА
|
||||
|
||||
### 3. Диагностика
|
||||
- Если сервер зелёный → обычный скрипт L0/L1/L2 → LLM → вывод
|
||||
- Если сервер красный → «Сервер недоступен. Сделайте тест на месте.»
|
||||
- Кнопка → СТАРТ
|
||||
|
||||
### 4. СТАРТ
|
||||
- Подсказка в выводе: «Нажмите газ, 3000 об/мин 3-4с, сбросьте. Нажмите СТОП.»
|
||||
- Запись 12 PID каждые 250мс
|
||||
- Кнопка → СТОП
|
||||
|
||||
### 5. СТОП
|
||||
- Запись остановлена, данные в памяти
|
||||
- Вывод: «Записано N отсчётов. Нажмите ➤ для отправки.»
|
||||
- Если нужно — можно снова СТАРТ (новый тест)
|
||||
- Кнопка → СТАРТ (снова)
|
||||
|
||||
### 6. Отправка (➤)
|
||||
- Если есть данные теста → POST /api/v1/session/upload с dynamic_samples
|
||||
- Если есть текст → добавляется как car_info
|
||||
- LLM-анализ → результат в выводе
|
||||
|
||||
## Что удалить из текущего UI
|
||||
- Кнопки Сервер, ELM, ЭБУ как отдельные кнопки → станут иконками
|
||||
- dynamicButtons (На месте / В движении) → не нужны, всё через одну кнопку
|
||||
- btnDynStart → не нужна
|
||||
- CheckBox «Полная диагностика» → всегда полная
|
||||
- tvDtcStatus → не нужен, всё в выводе
|
||||
- tvPrompt → не нужен
|
||||
@@ -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,144 @@
|
||||
# План правок по отчётам Опуса
|
||||
|
||||
> 31 мая 2026 · ветка `opus-fixes` · порядок: по критичности + зависимостям
|
||||
|
||||
---
|
||||
|
||||
## Этап 1. Сервер (`elmer/`) — 4 правки
|
||||
|
||||
### 1.1 🔴 `api/db.py` — WAL + закрытие соединений + request_id
|
||||
**Файл**: `api/db.py`
|
||||
**Строки**: класс `Database`, методы `_init_schema()`, `save_session()`
|
||||
|
||||
- [x] Добавить `PRAGMA journal_mode=WAL` и `busy_timeout=30000`
|
||||
- [x] Добавить колонку `request_id TEXT UNIQUE` в `sessions`
|
||||
- [x] Метод `close()` и контекстный менеджер (`__enter__`/`__exit__`)
|
||||
- [x] `save_session()` — проверять `request_id` на дубликат, возвращать кэшированный диагноз
|
||||
- [x] Индекс `idx_sessions_request_id`
|
||||
|
||||
### 1.2 🔴 `api/routes.py` — идемпотентность upload + /ping-llm без LLM
|
||||
**Файл**: `api/routes.py`
|
||||
**Строки**: `upload_session()`, `ping_llm()`
|
||||
|
||||
- [x] `upload_session()` — принимать `request_id` из JSON, возвращать кэш при дубликате
|
||||
- [x] `upload_session()` — закрывать `db` через контекстный менеджер
|
||||
- [x] `upload_session()` — не отдавать `str(e)` наружу, логировать, клиенту — обобщённый текст
|
||||
- [x] `/ping-llm` — кэшировать результат на 60с, не вызывать LLM на каждый GET
|
||||
|
||||
### 1.3 🔴 `obd/protocol.py` — сброс буфера + не затирать ERROR
|
||||
**Файл**: `obd/protocol.py`
|
||||
**Строки**: `_write()`, `send()`
|
||||
|
||||
- [x] `_write()` — `self._ser.reset_input_buffer()` перед записью
|
||||
- [x] `send()` — `if self._state == State.BUSY: self._state = State.READY` (не безусловно)
|
||||
|
||||
### 1.4 🟡 `brain/client.py` — таймаут из конфига + модель
|
||||
**Файл**: `brain/client.py`
|
||||
**Строки**: `Diagnoser.__init__()`, `Diagnoser.ask()`
|
||||
|
||||
- [x] `DEFAULT_MODEL` → `"gpt-oss-120b"`
|
||||
- [x] `timeout` — параметр конструктора (по умолчанию 180)
|
||||
- [x] Комментарии: убрать «DeepSeek»
|
||||
|
||||
---
|
||||
|
||||
## Этап 2. Сервер (`elmer/`) — улучшения (без 🔴 но важные)
|
||||
|
||||
### 2.1 🟡 `api/routes.py` — импорты наверх + кэш конфига
|
||||
**Файл**: `api/routes.py`, `api/config.py`
|
||||
|
||||
- [x] Поднять импорты (`from brain.client import Diagnoser` и др.) на уровень модуля
|
||||
- [x] `api/config.py` — `@lru_cache(maxsize=1)` на `load()`
|
||||
|
||||
### 2.2 🟡 `api/routes.py` — история /chat через роли
|
||||
**Файл**: `api/routes.py`
|
||||
**Строки**: `chat()`
|
||||
|
||||
- [x] Передавать историю как массив `messages` с ролями, а не строкой «Водитель:/Автоэксперт:»
|
||||
|
||||
### 2.3 🟡 `brain/client.py` — обработка ошибок LLM
|
||||
**Файл**: `brain/client.py`
|
||||
|
||||
- [x] Различать `Timeout`, `HTTPError(429)`, `HTTPError(5xx)`, `HTTPError(4xx)`
|
||||
- [x] Не отдавать детали исключения наружу
|
||||
|
||||
---
|
||||
|
||||
## Этап 3. Android (`elmer-android/`) — 6 правок
|
||||
|
||||
### 3.1 🔴 `ServerClient.kt` — request_id + идемпотентность
|
||||
**Файл**: `app/src/main/java/ru/elmer/client/server/ServerClient.kt`
|
||||
**Строки**: `uploadSession()`
|
||||
|
||||
- [ ] Генерировать `UUID` один раз до цикла ретраев
|
||||
- [ ] Добавить `"request_id"` в JSON-тело
|
||||
- [ ] Добавить заголовок `Idempotency-Key`
|
||||
|
||||
### 3.2 🔴 `ServerClient.kt` + `build.gradle.kts` — X-Api-Key
|
||||
**Файлы**: `ServerClient.kt`, `app/build.gradle.kts`
|
||||
|
||||
- [ ] `build.gradle.kts` — `buildConfigField("String", "API_KEY", ...)`
|
||||
- [ ] `ServerClient` — добавлять `X-Api-Key` во все запросы
|
||||
- [ ] `MainActivity.sendToLlm()` и `startTest()` — тоже `X-Api-Key`
|
||||
|
||||
### 3.3 🔴 `ElmProtocol.kt` — не затирать ERROR + дренаж буфера
|
||||
**Файл**: `app/src/main/java/ru/elmer/client/elm/ElmProtocol.kt`
|
||||
**Строки**: `sendCommand()`, `write()`
|
||||
|
||||
- [ ] `sendCommand()` — `if (state == State.BUSY) state = State.READY`
|
||||
- [ ] `write()` — `while (input.available() > 0) input.read()` перед записью
|
||||
|
||||
### 3.4 🔴 `SessionDb.kt` — безопасная миграция + индекс
|
||||
**Файл**: `app/src/main/java/ru/elmer/client/db/SessionDb.kt`
|
||||
**Строки**: `onUpgrade()`, `onCreate()`
|
||||
|
||||
- [ ] `onUpgrade()` — `ALTER TABLE` вместо `DROP TABLE`
|
||||
- [ ] Индекс `idx_resp_session ON responses(session_id)`
|
||||
|
||||
### 3.5 🔴 `MainActivity.kt` — /ping-llm без LLM + двойной receiver + chatHistory
|
||||
**Файл**: `app/src/main/java/ru/elmer/client/ui/MainActivity.kt`
|
||||
**Строки**: `startTest()`, `sendToLlm()`, receiver-регистрация
|
||||
|
||||
- [ ] `startTest()` — троттлить `/ping-llm` (не чаще раза в 60с), предупреждать
|
||||
- [ ] Убрать дублирующий receiver `statusReceiver` (оставить `scriptStatusReceiver`)
|
||||
- [ ] `chatHistory` сохранять в `onSaveInstanceState` (JSON)
|
||||
|
||||
### 3.6 🔴 `ScriptRunnerService.kt` — null intent + мёртвый paused + try/finally
|
||||
**Файл**: `app/src/main/java/ru/elmer/client/script/ScriptRunnerService.kt`
|
||||
**Строки**: `onStartCommand()`, `executeScript()`
|
||||
|
||||
- [ ] `onStartCommand()` — `if (intent == null) { stopSelf(); return START_NOT_STICKY }`
|
||||
- [ ] Убрать мёртвый флаг `paused` и `ACTION_RESUME` (или доделать паузу)
|
||||
- [ ] `executeScript()` — `try/finally` вокруг `progress.stop()`
|
||||
|
||||
---
|
||||
|
||||
## Этап 4. Android (`elmer-android/`) — улучшения
|
||||
|
||||
### 4.1 🟡 `ServerClient.kt` — exponential backoff
|
||||
**Файл**: `ServerClient.kt`
|
||||
|
||||
- [ ] `(1 shl (attempt-1)) * 1000 + Random.nextLong(0, 500)` вместо фиксированных 2000
|
||||
|
||||
### 4.2 🟡 `MainActivity.kt` — единый HTTP-клиент
|
||||
**Файл**: `MainActivity.kt`
|
||||
|
||||
- [ ] `sendToLlm()` и `startTest()` перевести на OkHttp (через `ServerClient`)
|
||||
|
||||
### 4.3 🟡 `ScriptRunnerService.kt` — вынести хост в константу
|
||||
**Файл**: `ScriptRunnerService.kt`, `MainActivity.kt`
|
||||
|
||||
- [ ] `obdai.ru` → `BuildConfig.SERVER_HOST` или константа
|
||||
|
||||
---
|
||||
|
||||
## Порядок выполнения
|
||||
|
||||
```
|
||||
Этап 1 (сервер 🔴) → коммит
|
||||
Этап 2 (сервер 🟡) → коммит
|
||||
Этап 3 (Android 🔴) → коммит
|
||||
Этап 4 (Android 🟡) → коммит
|
||||
```
|
||||
|
||||
После каждого этапа — проверка: `python run.py` (сервер), сборка APK (Android).
|
||||
@@ -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,146 @@
|
||||
# Вопросы к Opus 4.8 по проекту elmAI
|
||||
|
||||
> v0.35.0-dev, 31 мая 2026
|
||||
> Сервер: Ubuntu 24, Python/Flask, gunicorn + nginx
|
||||
> Android: Kotlin, minSdk 24, OkHttp
|
||||
> LLM: api.aillm.ru, модель gpt-oss-120b
|
||||
|
||||
---
|
||||
|
||||
## Какие файлы смотреть (и только их)
|
||||
|
||||
### Сервер (elmer/)
|
||||
- `obd/protocol.py` — ELM327 стейт-машина AndrOBD (State, Rsp, AdaptiveTiming)
|
||||
- `brain/client.py` — Diagnoser (HTTP к LLM API)
|
||||
- `brain/prompts.py` — SYSTEM_PROMPT для диагностики
|
||||
- `api/routes.py` — все 5 эндпоинтов (script, upload, chat, ping, ping-llm)
|
||||
- `api/db.py` — SQLite: таблица sessions (30+ полей)
|
||||
- `api/scripts.py` — сборка диагностических скриптов
|
||||
- `api/parser.py` — парсинг ответов ELM327
|
||||
- `web/app.py` — точка входа Flask
|
||||
- `doc/architecture.md` — описание архитектуры
|
||||
|
||||
### Android (elmer-android/)
|
||||
- `script/ScriptRunnerService.kt` — сервис фоновой диагностики
|
||||
- `script/ScriptEngine.kt` — движок выполнения скриптов
|
||||
- `script/UploadProgress.kt` — таймер прогресса загрузки
|
||||
- `server/ServerClient.kt` — HTTP-клиент (OkHttp, retry 3x)
|
||||
- `elm/ElmProtocol.kt` — ELM327 стейт-машина (Kotlin)
|
||||
- `elm/ObdDecoder.kt` — декодер PID/DTC/VIN
|
||||
- `ui/MainActivity.kt` — главный экран
|
||||
- `db/SessionDb.kt` — локальная SQLite
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1. Стейт-машина ELM327: баги и крайние случаи
|
||||
|
||||
**Файлы**: `obd/protocol.py`, `elm/ElmProtocol.kt`
|
||||
|
||||
Стейт-машина — 1:1 копия AndrOBD (ElmProt.java). Ключевые моменты:
|
||||
- Байт-за-байтом чтение с 1мс поллингом
|
||||
- `>` как разделитель ответов
|
||||
- Адаптивный таймаут (200мс ± 4мс)
|
||||
- Восстановление после BUS ERROR (ATPC → ATSP0)
|
||||
|
||||
Вопросы:
|
||||
1. Есть ли race conditions или deadlocks в переходах состояний?
|
||||
2. Что если `>` приходит НЕ после полного ответа (мусор в буфере)?
|
||||
3. Корректна ли логика восстановления после BUS ERROR? Не теряем ли мы ответы при ATPC→ATSP0?
|
||||
4. Достаточен ли 1мс поллинг или на некоторых ELM нужен меньше?
|
||||
5. Есть ли риск бесконечного цикла в `_exec()` (10 ретраев)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2. HTTP 499 при upload с мобильной сети
|
||||
|
||||
**Файлы**: `script/ScriptRunnerService.kt`, `server/ServerClient.kt`, `api/routes.py`
|
||||
|
||||
**Симптом**: сервер получает POST, но клиент обрывает соединение (nginx: 499).
|
||||
- Connect timeout: 30с, read: 180с, write: 60с
|
||||
- 3 ретрая с задержкой 2с
|
||||
- nginx: client_body_timeout 120s, proxy_read_timeout 300s
|
||||
- gunicorn: timeout 180s
|
||||
|
||||
Вопросы:
|
||||
1. Какие ещё причины HTTP 499 на мобильной сети кроме таймаутов?
|
||||
2. Достаточна ли стратегия ретраев? Может, нужен exponential backoff?
|
||||
3. Может ли проблема быть в отправке тела запроса (write timeout) на медленной сети?
|
||||
4. Стоит ли разбивать upload на чанки или сжать JSON?
|
||||
5. Корректно ли мы обрабатываем случай, когда сервер получил запрос но клиент упал — данные могут дублироваться?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3. Архитектура: три модуля + Android пакеты
|
||||
|
||||
**Файлы**: `doc/architecture.md`, `web/app.py`, `api/routes.py`
|
||||
|
||||
Сервер разбит на `obd/`, `brain/`, `api/`. Android — на `elm/`, `server/`, `script/`, `db/`, `ui/`.
|
||||
|
||||
Вопросы:
|
||||
1. Чистые ли границы между модулями? Нет ли неявных зависимостей?
|
||||
2. `api/routes.py` делает `from brain.client import Diagnoser` внутри функций — это нормально или лучше на уровне модуля?
|
||||
3. Стоит ли вынести `config.yaml` из `api/` на уровень выше?
|
||||
4. `web/app.py` зависит от `api/routes.py` — это правильное направление?
|
||||
5. Какие модули можно было бы легко заменить (например, `brain/` на локальный LLM)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4. SQL схема: таблица sessions
|
||||
|
||||
**Файлы**: `api/db.py`
|
||||
|
||||
Таблица `sessions` — 30+ колонок (IP, телефон, ELM, авто, сессия, LLM). VIN — nullable.
|
||||
Также старые таблицы: `cars`, `diagnostic_tokens`, `llm_messages`, `ecu_parameters`, `dtc_codes`.
|
||||
|
||||
Вопросы:
|
||||
1. 30+ колонок в одной таблице — это нормально для SQLite или лучше разбить?
|
||||
2. raw_responses хранится как JSON TEXT — ок ли для SQLite?
|
||||
3. Индексы: по `created_at`, `vin`, `elm_mac`, `android_id` — достаточны?
|
||||
4. Старые таблицы (cars, dtc_codes) всё ещё создаются в `_init_schema()` но не используются. Удалять или оставить для совместимости?
|
||||
5. Нет ли проблем с конкурентным доступом к SQLite из gunicorn (4 воркера)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5. LLM-интеграция: промпты и таймауты
|
||||
|
||||
**Файлы**: `brain/client.py`, `brain/prompts.py`, `api/routes.py`
|
||||
|
||||
- Diagnoser использует `requests.post` без streaming
|
||||
- SYSTEM_PROMPT — 10 правил ответа
|
||||
- Для /chat — лимит 20 строк, история диалога (последние 10 сообщений)
|
||||
|
||||
Вопросы:
|
||||
1. Достаточен ли промпт для качественной диагностики? Чего не хватает?
|
||||
2. `requests.post` без streaming при таймауте 180с — ок или лучше streaming + heartbeat?
|
||||
3. Для /chat: правильно ли форматируется история диалога? Не переполнит ли контекст?
|
||||
4. Модель gpt-oss-120b — адекватный выбор? Какие альтернативы для авто-диагностики?
|
||||
5. Как правильно обрабатывать ошибки LLM API (rate limit, timeout, bad response)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6. Безопасность API
|
||||
|
||||
**Файлы**: `api/routes.py`, `web/app.py`
|
||||
|
||||
- API без аутентификации, только HTTPS через nginx
|
||||
- API ключ LLM на сервере, не в APK
|
||||
- `usesCleartextTraffic` убран из манифеста
|
||||
|
||||
Вопросы:
|
||||
1. Достаточен ли HTTPS без API-ключей для MVP? Какие риски?
|
||||
2. Какие минимальные меры добавить: rate limiting, API key в APK, CORS?
|
||||
3. `raw_responses` пишутся в БД — есть ли риск инъекции через ответы ELM327?
|
||||
4. `/api/v1/chat` без аутентификации — можно ли его абузить (спамить токенами)?
|
||||
5. Нужно ли скрывать API-ключ LLM за прокси или текущая схема ок?
|
||||
|
||||
---
|
||||
|
||||
## Формат ответа
|
||||
|
||||
Пожалуйста, запиши ответ в файл `/home/naeel/elmer/doc/opus-review.md`.
|
||||
|
||||
По каждому вопросу:
|
||||
- 🔴 Критическая проблема (если есть)
|
||||
- 🟡 Потенциальная проблема / улучшение
|
||||
- 🟢 Всё ок
|
||||
- Конкретные рекомендации с примерами кода где уместно
|
||||
@@ -0,0 +1,67 @@
|
||||
# Opus — переанализ фаз 0–6 (28.06.2026)
|
||||
|
||||
Запрос: doc/opus-recheck-request-2026-06-28.md
|
||||
|
||||
---
|
||||
|
||||
## Вердикт
|
||||
|
||||
**Можно запускать на реальном ELM327 v1.5** для стендовых тестов с одним телефоном.
|
||||
Логика протокола корректна. Один баг очереди надо закрыть до перехода на `gunicorn -w 4`.
|
||||
|
||||
---
|
||||
|
||||
## 1. ElmActor.kt — корректен, deadlock нет ✅
|
||||
|
||||
`init()` сабмитит `elm.init()` один раз и ждёт `.get(30s)`. Внутри `elm.init()` вызывает `elm.sendCommand("ATI")` прямым вызовом (не через `executor.submit`), то есть на том же потоке actor'а — рекурсивной постановки в очередь нет → deadlock невозможен. `isClone()` читает `@Volatile` поле напрямую — видимость корректна.
|
||||
|
||||
**Замечание (low):** при таймауте `.get(30, SECONDS)` Future не отменяется — задача `elm.init()` продолжает крутиться на executor'е, и следующий `send()` встанет за ней в очередь. Худший случай init по сумме `tryRead` ≈ 23с (ATSP0 один даёт до 10с) — близко к лимиту 30с. Рекомендация: `future.cancel(true)` в `catch(TimeoutException)` либо поднять лимит до 45с.
|
||||
|
||||
---
|
||||
|
||||
## 2. ElmProtocol.kt (raw) — порядок init верный, но есть двойной ATI ⚠️
|
||||
|
||||
Канонический порядок правильный: `ATE0→ATL0→ATS0→ATI→detectClone→[ATST96|ATAT1]→ATSP0`. Эхо выключается первым. Обработка двойного ответа ATWS корректна во всех ветках recovery/STOPPED/BUS ERROR.
|
||||
|
||||
Проблемы:
|
||||
- **Двойной ATI (medium):** первый `write("ATI"); tryRead; drainInput()` бесполезен. Затем ещё раз `sendCommand("ATI")`. Python-версия сделана правильно (один `_exec("ATI")`).
|
||||
- **MAX_RETRIES=6 даёт потолок 600мс, не 2000 (medium):** 6 шагов по +20 = 600мс. Если медленный ЭБУ требует >600мс — всё равно ошибка. Комментарий вводит в заблуждение.
|
||||
- **exec() не перешлёт команду на ретрае:** пишет `cmd` один раз, при таймауте только перечитывает буфер. Для ELM штатно, но при реальном `NO DATA` повторное чтение не поможет.
|
||||
|
||||
---
|
||||
|
||||
## 3. raw_elm.py — SQLite-очередь: dequeue НЕ атомарен ❌ (главное)
|
||||
|
||||
`GET /api/v1/elm/raw/cmd` делает два отдельных стейтмента без guard'а. При `-w 4` два воркера могут выбрать одну строку → команда уйдёт на ELM дважды.
|
||||
|
||||
**Фикс:** `UPDATE ... SET status='sent' WHERE id=? AND status='pending'` + проверка `rowcount == 0`.
|
||||
|
||||
Прочие замечания:
|
||||
- **Рост таблицы:** `command_queue` не чистится (старый код держал 500). Добавить `DELETE WHERE responded_at < datetime('now','-1 day')`.
|
||||
- **`device_id="unknown"`:** несколько телефонов сольются в одну очередь.
|
||||
- **`GET /response`:** при всплеске может вернуть ответ не на запрошенную команду (для пошагового relay'я ок).
|
||||
|
||||
---
|
||||
|
||||
## 4. Python protocol.py — паритет с Kotlin ✅
|
||||
|
||||
Тот же порядок, один ATI, ATST96 для клона, `try_read(500)` как drain после каждой AT-команды.
|
||||
|
||||
---
|
||||
|
||||
## 5. Безопасность (фаза 0) ✅
|
||||
|
||||
`api_key → env LLM_API_KEY`. Проверить что старый ключ отозван у провайдера.
|
||||
|
||||
---
|
||||
|
||||
## Итоговый список к исправлению (по приоритету)
|
||||
|
||||
1. ❌ **Атомарный dequeue** — guard `AND status='pending'` + `rowcount`
|
||||
2. ⚠️ **Двойной ATI** в `ElmProtocol.kt init()` — убрать первый `write("ATI")`
|
||||
3. ⚠️ **MAX_RETRIES/комментарий** — потолок 600мс, не 2000
|
||||
4. **Ретеншн** `command_queue` + фильтрация `device_id`
|
||||
5. **Отмена Future** при таймауте init в `ElmActor`
|
||||
6. **Отозвать старый LLM-ключ** у провайдера
|
||||
|
||||
Пункты 2–5 не блокируют стендовый прогон. Dequeue-гонку держать в голове при `-w 4`.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Opus — переанализ после фаз 0–6
|
||||
|
||||
Дата: 2026-06-28
|
||||
Предыдущий анализ: doc/history/opus-plan-analysis-2026-06-28.md
|
||||
|
||||
## Что изменилось
|
||||
|
||||
Твой план из 6 фаз реализован в ветке `opus-fixes`. Изменённые файлы:
|
||||
|
||||
### Android (:raw — тестовый модуль, пакет ru.elmer.raw)
|
||||
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/ElmProtocol.kt` — канонический init (ATE0→ATL0→ATS0→ATI→detectClone→ветвл→ATSP0), ATST96 для клонов, MAX_RETRIES=6, @Volatile state, drain после sendCommand, handle STOPPED/NO DATA/UNABLE
|
||||
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/ElmActor.kt` — single-thread executor вокруг ElmProtocol (новый файл)
|
||||
- `/home/naeel/elmer/android/raw/src/main/java/ru/elmer/raw/RawRelayService.kt` — переведён на ElmActor
|
||||
|
||||
### Сервер (Python)
|
||||
- `/home/naeel/elmer/obd/protocol.py` — канонический init (тот же порядок, clone detect, drain после send)
|
||||
- `/home/naeel/elmer/api/raw_elm.py` — полная переработка: SQLite command_queue вместо глобальных переменных (gunicorn-safe, -w 4)
|
||||
- `/home/naeel/elmer/api/db.py` — таблица command_queue
|
||||
- `/home/naeel/elmer/api/scripts.py` — build_dynamic_script: 6 PID, interval_ms 1200
|
||||
- `/home/naeel/elmer/config.yaml` — api_key → env LLM_API_KEY (безопасность)
|
||||
|
||||
### НЕ тронуто
|
||||
- `:app` модуль (ru.elmer.client) — весь старый код без изменений
|
||||
|
||||
## Что нужно
|
||||
|
||||
1. **Валидация** — нет ли новых ошибок, противоречий, race conditions в новом коде
|
||||
2. **ElmActor.kt** — корректна ли реализация single-thread executor? Нет ли deadlock при init()?
|
||||
3. **ElmProtocol.kt (raw)** — правильный ли порядок init? Корректна ли обработка двойного ответа ATWS?
|
||||
4. **raw_elm.py** — корректна ли SQLite-очередь? Атомарен ли dequeue?
|
||||
5. **Общая оценка** — можно ли с этим кодом запускать тесты на реальном ELM327 v1.5?
|
||||
@@ -0,0 +1,280 @@
|
||||
# Ответ Opus 4.8 — ревью elmer-android
|
||||
|
||||
> v0.35.0-dev, 31 мая 2026
|
||||
> Проверены файлы: ElmProtocol.kt, ObdDecoder.kt, ScriptRunnerService.kt, ScriptEngine.kt,
|
||||
> ServerClient.kt, SessionDb.kt, MainActivity.kt, UploadProgress.kt, AndroidManifest.xml
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1. Стейт-машина ElmProtocol.kt
|
||||
|
||||
### 1.1 `startsWith("ERROR") && !startsWith("DATA ERROR")`
|
||||
🟡 **Потенциальная проблема.** `handle()` работает с уже распарсенными строками-ответами, а не с PID-именами, поэтому коллизии с «ERROR_xxx» в данных нет — декодирование имён происходит позже в `ObdDecoder`. НО: реальные ELM-ошибки не всегда начинаются с `ERROR`. Например `?` (неизвестная команда), `UNABLE TO CONNECT` (ловится в `isBusError`), `<RX ERROR` (с префиксом `<`). Строка `<DATA ERROR` из-за лидирующего `<` **не** сматчится `startsWith("DATA ERROR")`. ELM327 при ошибке кадра иногда шлёт `<` перед сообщением.
|
||||
|
||||
Рекомендация — нормализовать перед классификацией:
|
||||
```kotlin
|
||||
val u = raw.uppercase().trim().trimStart('<', '>').trim()
|
||||
```
|
||||
|
||||
### 1.2 `sendCommand()` безусловно ставит READY после exec()
|
||||
🔴 **Критично — маскирование ошибки.**
|
||||
```kotlin
|
||||
fun sendCommand(cmd: String): String {
|
||||
if (state == State.ERROR) recover()
|
||||
state = State.BUSY
|
||||
val result = exec(cmd, timeoutMs) // exec может выставить State.ERROR/DISCONNECTED
|
||||
state = State.READY // ← затирает ошибку
|
||||
return result
|
||||
}
|
||||
```
|
||||
`exec()` при исчерпании ретраев ставит `state = State.ERROR`, а `handle()` — `ERROR`/`DISCONNECTED`. Следующая строка безусловно перетирает это на `READY`. Ошибка «теряется» до следующего вызова. В AndrOBD состояние не сбрасывается слепо.
|
||||
|
||||
Рекомендация:
|
||||
```kotlin
|
||||
val result = exec(cmd, timeoutMs)
|
||||
if (state == State.BUSY) state = State.READY // только если не было ошибки
|
||||
return result
|
||||
```
|
||||
|
||||
### 1.3 `init()` не проверяет результат AT-команд
|
||||
🟡 **Поведение AndrOBD, но рискованное.** AndrOBD действительно прогоняет init-цепочку «оптимистично», полагаясь на то, что первые реальные OBD-команды отловят BUS ERROR. Для MVP допустимо, но `ATSP0` (выбор протокола) стоит проверять — если адаптер вернул `?`, дальнейшие команды бессмысленны. Минимум — логировать ответ и считать в `errorCount`.
|
||||
|
||||
### 1.4 Нет сброса input-буфера перед write()
|
||||
🟡 **Риск десинхронизации есть.** В `read()` чтение идёт до `>` (prompt), но если предыдущая команда оставила хвост в буфере (например после таймаута пришёл запоздалый ответ), он прилипнет к следующему чтению. Рекомендация — дренировать буфер перед записью:
|
||||
```kotlin
|
||||
private fun write(cmd: String) {
|
||||
while (input.available() > 0) input.read() // drain stale bytes
|
||||
output.write((cmd + "\r").toByteArray())
|
||||
output.flush()
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 `BUFFER FULL` → warm start
|
||||
🟡 **Спорно.** В AndrOBD `BUFFER FULL` — это переполнение буфера ELM при большом ответе, лечится **повторным запросом**, а не полным `ATWS` (warm start сбрасывает протокол и теряет адаптацию таймингов). Здесь `BUFFER FULL` попадает в `isDataError` → `ATWS`, что излишне тяжело. Лучше выделить:
|
||||
```kotlin
|
||||
u.startsWith("BUFFER FULL") -> { increaseTimeout() /* retry */ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2. ScriptRunnerService — жизненный цикл
|
||||
|
||||
### 2.1 `START_STICKY` + null intent
|
||||
🔴 **Падение при пересоздании.** При рестарте системой `onStartCommand` получает `intent == null`. Сейчас `when (intent?.action)` отрабатывает в `else`-ветку (ничего не делает) и возвращает `START_STICKY` — краша нет, но сервис висит в foreground без работы и без уведомления о реальной задаче. Лучше:
|
||||
```kotlin
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent == null) { stopSelf(); return START_NOT_STICKY }
|
||||
...
|
||||
}
|
||||
```
|
||||
Для разовой диагностики вообще логичнее `START_NOT_STICKY` — нет смысла воскрешать прерванную сессию.
|
||||
|
||||
### 2.2 Демон-поток `ScriptRunner`
|
||||
🟡 Демон-поток живёт пока жив процесс. Если Activity убита, а сервис foreground — процесс жив, поток работает. Но при нехватке памяти система может убить весь процесс (вместе с потоком) несмотря на foreground. Это нормально для разовой задачи. Замечание: исключения внутри потока никуда не пробрасываются — добавьте `try/catch` обёртку с `errorDone()`.
|
||||
|
||||
### 2.3 / 2.4 `btSocket` и `onDestroy()`
|
||||
🟢 **Уже закрывается.** `onDestroy()` вызывает `disconnect()`, который закрывает `btSocket` и снимает foreground. Утечки сокета нет. ✅
|
||||
|
||||
### 2.5 Флаг `paused`
|
||||
🔴 **Мёртвый код / недоделанная фича.** `paused` выставляется в `false` по `ACTION_RESUME`, но **нигде не проверяется** — ни в `ScriptEngine.run()`, ни в `executeScript()`. Механизм паузы «водитель ответил» (broadcast `BROADCAST_PROMPT`, `scriptPromptReceiver` в UI) фактически не реализован на стороне движка. Либо удалить флаг и UI-приёмник промптов, либо доделать: `ScriptEngine` должен уметь блокироваться на шаге до сброса `paused`.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3. ServerClient — ретраи и идемпотентность
|
||||
|
||||
### 3.1 Повторное использование тела запроса на ретраях
|
||||
🟢 **Работает корректно.** Тело создано через `String.toRequestBody(...)` — это `RequestBody` поверх неизменяемой строки. В OkHttp 4.x такой `RequestBody` **stateless**: `writeTo()` вызывается заново на каждой попытке и пишет ту же строку. Пустого тела на 2-3 ретрае **не будет**. (Проблема была бы только с одноразовым стримом, например `InputStream.source()`.)
|
||||
|
||||
### 3.2 Exponential backoff
|
||||
🟡 Для мобильной сети фиксированные 2с приемлемы, но джиттер + рост лучше против «retry storm»:
|
||||
```kotlin
|
||||
if (attempt < 3) Thread.sleep(1000L * (1 shl (attempt - 1)) + Random.nextLong(0, 500))
|
||||
```
|
||||
|
||||
### 3.3 `downloadScript()` без ретраев
|
||||
🟢 Это сознательный и правильный выбор: есть качественный `DEFAULT_SCRIPT` fallback, поэтому мгновенный переход к нему при оффлайне — корректное поведение. 1 ретрай можно добавить, но не критично.
|
||||
|
||||
### 3.4 gzip на upload
|
||||
🟡 При `count * 200` байт типичный батч < 5 KB — выигрыш от gzip минимален, а overhead на сжатие/совместимость с nginx добавляет риск. Не нужно для MVP.
|
||||
|
||||
### 3.5 Порядок `.string()` / `.close()`
|
||||
🟢 **Корректно.** `val body = resp.body?.string()` сначала читает (и закрывает поток тела), затем `resp.close()`. Порядок верный, двойного закрытия нет. ✅
|
||||
|
||||
### 3.6 Идемпотентность / `request_id`
|
||||
🔴 **Критично (подтверждаю отчёт Q2).** При 499/таймауте и ретрае сервер создаёт дубликат сессии и повторно тратит LLM-токен. Клиент должен генерировать UUID **один раз до цикла ретраев** и слать его в теле:
|
||||
|
||||
```kotlin
|
||||
fun uploadSession(...): JSONObject? {
|
||||
val requestId = java.util.UUID.randomUUID().toString() // один на все 3 попытки
|
||||
val json = JSONObject().apply {
|
||||
put("request_id", requestId)
|
||||
put("session_id", sessionId)
|
||||
...
|
||||
}
|
||||
val req = Request.Builder()
|
||||
.url("$serverUrl/api/v1/session/upload")
|
||||
.header("Idempotency-Key", requestId)
|
||||
.post(json.toString().toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
...
|
||||
}
|
||||
```
|
||||
На сервере (`save_session()`): UNIQUE-индекс по `request_id`, при повторе — вернуть **сохранённый** результат (включая готовый диагноз), не вызывая LLM повторно:
|
||||
```python
|
||||
existing = db.execute("SELECT diagnosis FROM sessions WHERE request_id=?", [rid]).fetchone()
|
||||
if existing:
|
||||
return jsonify(diagnosis=existing["diagnosis"], llm_success=True, cached=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4. ObdDecoder — корректность декодирования
|
||||
|
||||
### 4.1 VIN с пробелами
|
||||
🟢 **Корректно.** `replace(" ", "")` снимает пробелы до проверки `"490201" in clean`, плюс убраны `:` (ISO-TP индикаторы кадров `0:`, `1:`...). Работает и для multi-frame. ✅
|
||||
|
||||
### 4.2 `decodeDtc()` начинает с `i = 2`
|
||||
🟡 **Не всегда верно.** `hex = clean.substring(2)` снимает байт режима (`43`), затем `i = 2` снимает **байт count** (число DTC). Это корректно для классического формата `43 NN <dtc>...`. Но:
|
||||
- Multi-frame CAN ISO-TP: ответ может содержать байты длины PCI (`007`, `10 0E`...), которые здесь **не вычищены** (убраны только пробелы и `:`). Тогда `i=2` указывает не на тот байт.
|
||||
- Некоторые адаптеры на mode 03 не шлют байт count вовсе.
|
||||
|
||||
Для надёжности стоит парсить DTC по парам байт от конца режима и отбрасывать `0000`, что код уже делает (фильтр `P0000`). Главный риск — невычищенные PCI-заголовки multi-frame. Для коротких ответов (1-2 DTC, single frame) работает.
|
||||
|
||||
### 4.3 PID `0100` (4 байта supported)
|
||||
🟡 `decodePid()` читает только `b0, b1`. PID `00/20/40...` (битовые маски supported PIDs, 4 байта) не входят в `pidValue()` → вернётся `"PID 00: raw"`. Поскольку скрипт их не запрашивает — не баг сейчас, но при расширении скрипта декодер их не покажет.
|
||||
|
||||
### 4.4 Только 10 PID
|
||||
🟢 **Ок для MVP.** Скрипт `DEFAULT_SCRIPT` запрашивает ровно эти PID. Для неподдерживаемых — `"PID $pid: raw"`, сырьё всё равно уходит на сервер и в LLM. Расширять по мере надобности.
|
||||
|
||||
### 4.5 STFT/LTFT формула
|
||||
🟢 Формула `(A - 128) * 100 / 128` верна по SAE J1979. Для PID 06/07 это однобайтовые значения (банк 1), `b1` игнорируется правильно. ✅ (Замечание: PID 06/07 — это банк 1 short/long; банки 2 — это 08/09, в скрипте их нет.)
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5. SessionDb — схема и доступ
|
||||
|
||||
### 5.1 `onUpgrade()` DROP TABLE
|
||||
🔴 **Потеря данных при апдейте.** Любое повышение `DB_VERSION` сотрёт всю историю пользователя. Для продакшена недопустимо. Минимальная безопасная миграция:
|
||||
```kotlin
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldV: Int, newV: Int) {
|
||||
if (oldV < 2) db.execSQL("ALTER TABLE sessions ADD COLUMN server_url TEXT")
|
||||
// будущие версии — ALTER, не DROP
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Без явного закрытия соединений
|
||||
🟢 **Ок.** `SQLiteOpenHelper` кэширует одно соединение на хелпер; курсоры закрываются (`cursor.close()`). Не закрывать сам `db` — правильно. ⚠️ Замечание: `SessionDb` создаётся и в сервисе, и в `MainActivity.showHistory()` — два хелпера на одну БД. Лучше один экземпляр (синглтон), иначе при одновременном write возможен `SQLiteDatabaseLockedException`.
|
||||
|
||||
### 5.3 `getPendingSessions()` — мёртвый код
|
||||
🟡 Метод нигде не вызывается. Это задел под «дослать неотправленные сессии при следующем запуске», но фича не реализована. Либо удалить, либо доделать ретрай-аплоад оффлайн-сессий в `onCreate` сервиса.
|
||||
|
||||
### 5.4 `created_at` INTEGER vs сервер TEXT
|
||||
🟡 Несогласованность форматов. На клиенте unix-секунды, на сервере ISO 8601. При синхронизации сервер должен конвертировать. Лучше слать с клиента ISO-8601 (или явно `unix_ts` с понятным именем) в `client_info`/`responses`, чтобы не было путаницы с часовыми поясами. Сейчас `timestamp` ответов уходит как строка unix-секунд — сервер должен это знать.
|
||||
|
||||
### 5.5 Индексы
|
||||
🟡 `WHERE session_id = ?` в `getResponses()` без индекса — full scan. При сотнях ответов на сессию заметно. Добавьте:
|
||||
```kotlin
|
||||
db.execSQL("CREATE INDEX idx_resp_session ON responses(session_id)")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6. MainActivity — чат и UI
|
||||
|
||||
### 6.1 `chatHistory` теряется при повороте
|
||||
🔴 `onSaveInstanceState` сохраняет только `status_text`, `chatHistory` живёт в поле Activity → при повороте/пересоздании теряется, и LLM теряет контекст диалога. Варианты: сохранить в Bundle (сериализовать в JSON), либо вынести в `ViewModel` (`SavedStateHandle`). Минимум:
|
||||
```kotlin
|
||||
outState.putString("chat", JSONArray(chatHistory.map { ... }).toString())
|
||||
```
|
||||
|
||||
### 6.2 Чат на `HttpURLConnection` вместо OkHttp
|
||||
🟡 Дублирование HTTP-логики и таймаутов. `ServerClient` уже инкапсулирует OkHttp — `sendToLlm()` и `startTest()` стоит перевести на него (общие таймауты, ретраи, будущий `X-Api-Key`). Сейчас три места шлют HTTP по-разному.
|
||||
|
||||
### 6.3 `startTest()` дёргает `/ping-llm` (платный токен)
|
||||
🔴 **Расход денег на каждом «Тест».** `/ping-llm` делает реальный LLM-запрос. Кнопку «Тест» пользователь может жать многократно. Варианты: на сервере сделать `/ping-llm` дешёвой проверкой доступности (HEAD к API провайдера / кэш на 60с), либо на клиенте троттлить (не чаще раза в N минут) и предупреждать.
|
||||
|
||||
### 6.4 Двойная регистрация receiver
|
||||
🟡 `scriptStatusReceiver`/`scriptStageReceiver` защищены флагом `scriptRegistered` — двойной регистрации этих двух нет. НО: `statusReceiver` (отдельный, для `BROADCAST_STATUS`) регистрируется в `onCreate` **и** `scriptStatusReceiver` тоже слушает `BROADCAST_STATUS` — два приёмника на один экшен → **каждое сообщение `log()` обработается дважды** (дублирование строк в UI). Также `scriptPromptReceiver` регистрируется... — на самом деле **нигде не регистрируется**, только разрегистрируется в `onDestroy`. Промпты не приходят (связано с мёртвым `paused`, Q2.5).
|
||||
|
||||
Рекомендация: оставить один приёмник на `BROADCAST_STATUS`.
|
||||
|
||||
### 6.5 `btnClose` не чистит `chatHistory` и не стопит сервис
|
||||
🟡 Кнопка ✕ только прячет UI и пишет «Готов». Если сервис ещё работает — он продолжит и пришлёт новые статусы поверх. Для «закрыть» логично слать `ACTION_STOP` в сервис. `chatHistory` чистить не обязательно (диалог отдельный от диагностики), но сервис стоит остановить.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 7. UploadProgress — таймер и батарея
|
||||
|
||||
### 7.1 Broadcast каждую секунду до 180с
|
||||
🟡 Незначительно для батареи (≤180 broadcast на сессию), но это локальный `sendBroadcast` с `setPackage` — дёшево. Не проблема.
|
||||
|
||||
### 7.2 Поток висит при исключении
|
||||
🟡 **Реальный риск.** В `executeScript()` `progress.start()` → `uploadSession()` → `progress.stop()`. Если `uploadSession()` бросит непойманное исключение, `stop()` не вызовется и `UploadTimer` останется крутиться (демон, до смерти процесса). Оберните в `try/finally`:
|
||||
```kotlin
|
||||
val progress = UploadProgress(...); progress.start()
|
||||
val resp = try { client.uploadSession(...) } finally { progress.stop() }
|
||||
```
|
||||
|
||||
### 7.3 `Handler.postDelayed` вместо потока
|
||||
🟢 Можно, но текущий вариант с `AtomicBoolean` + демон-поток корректен и проще. Не критично. Главное — гарантировать `stop()` (см. 7.2).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 8. Общая архитектура
|
||||
|
||||
### 8.1 MainActivity знает про Service и SessionDb
|
||||
🟡 Нарушение SRP есть, но для MVP с одним экраном терпимо. При росте — вынести историю в `Repository`, а UI-логику в `ViewModel`.
|
||||
|
||||
### 8.2 ElmProtocol замокать для тестов
|
||||
🟡 `ScriptEngine` отлично тестируется (lambdas) — это сильная сторона. `ElmProtocol` жёстко завязан на `InputStream/OutputStream`, но это **тестируемо**: подайте `ByteArrayInputStream`/`ByteArrayOutputStream` с заскриптованными ответами ELM. Интерфейс выделять не нужно, потоки — уже абстракция. Рекомендую написать unit-тест на `handle()`-классификацию и таймаут-адаптацию.
|
||||
|
||||
### 8.3 Нет ViewModel/DI/Navigation
|
||||
🟢 Для MVP с одной кнопкой — ок. ViewModel стоит ввести первым (решает 6.1, 6.4). DI/Navigation — преждевременно.
|
||||
|
||||
### 8.4 minSdk 24 + BluetoothAdapter.getDefaultAdapter
|
||||
🟡 `getDefaultAdapter()` deprecated с API 31, но работает на 24+. `createRfcommSocketToServiceRecord` + reflection-fallback `createRfcommSocket(1)` — стандартный надёжный приём для китайских ELM327, покрывает большинство устройств. Замечание: на Android 12+ (API 31) для `connect()` нужен рантайм-`BLUETOOTH_CONNECT` — в манифесте он есть, проверьте что он реально запрашивается в рантайме (в показанном коде `MainActivity` запрос пермишенов есть в константах, но самого `requestPermissions` в прочитанном фрагменте не видно — убедитесь, что вызывается).
|
||||
|
||||
### 8.5 `usesCleartextTraffic` не объявлен
|
||||
🟢 По умолчанию `false` на API 28+, все запросы на `https://obdai.ru` — ок. ✅ Замечание: жёстко зашитый хост `obdai.ru` в нескольких местах (Service, MainActivity) — вынесите в `BuildConfig`/константу.
|
||||
|
||||
### 8.6 Эндпоинты без аутентификации (X-Api-Key)
|
||||
🔴 **Критично (подтверждаю отчёт Q6).** `/chat`, `/upload`, `/ping-llm` открыты → любой может тратить ваши LLM-токены.
|
||||
|
||||
Статический ключ в APK **извлекаем** (reverse engineering), поэтому он защищает только от случайных/ленивых злоупотреблений, не от целевой атаки. Для MVP это разумный первый рубеж:
|
||||
|
||||
```kotlin
|
||||
// BuildConfig.API_KEY из gradle (не в git, через local.properties / CI secret)
|
||||
val req = Request.Builder()
|
||||
.url(...)
|
||||
.header("X-Api-Key", BuildConfig.API_KEY)
|
||||
.post(...)
|
||||
.build()
|
||||
```
|
||||
build.gradle.kts:
|
||||
```kotlin
|
||||
buildConfigField("String", "API_KEY", "\"${project.findProperty("ELMER_API_KEY") ?: ""}\"")
|
||||
```
|
||||
Сервер — отклонять без верного `X-Api-Key` (401) + **rate-limit по IP/ключу** + квота на LLM. Для серьёзной защиты позже: подпись запроса (HMAC от тела + nonce + timestamp), либо Play Integrity API / device attestation. Но для MVP: `X-Api-Key` + rate-limit + серверная квота на LLM — достаточный минимум, при этом главную защиту денег даёт именно **серверный лимит**, а не ключ.
|
||||
|
||||
---
|
||||
|
||||
## Сводка приоритетов
|
||||
|
||||
🔴 **Чинить сейчас:**
|
||||
1. `request_id`/идемпотентность upload (Q3.6) — дубли сессий и двойной расход LLM.
|
||||
2. `X-Api-Key` + серверный rate-limit/квота (Q8.6) — открытые платные эндпоинты.
|
||||
3. `sendCommand()` маскирует ERROR-состояние (Q1.2).
|
||||
4. `onUpgrade()` DROP TABLE — потеря истории (Q5.1).
|
||||
5. `/ping-llm` тратит токен на каждом «Тест» (Q6.3).
|
||||
6. Двойной приёмник `BROADCAST_STATUS` → дублирование строк (Q6.4).
|
||||
|
||||
🟡 **Желательно:**
|
||||
- `paused` — мёртвый код / недоделанная пауза (Q2.5, Q6.4-prompt).
|
||||
- `try/finally` вокруг `UploadProgress` (Q7.2).
|
||||
- Дренаж BT-буфера перед write (Q1.4).
|
||||
- `chatHistory` в onSaveInstanceState/ViewModel (Q6.1).
|
||||
- Индекс `responses(session_id)` (Q5.5).
|
||||
- Единый HTTP-клиент (OkHttp) для чата/теста (Q6.2).
|
||||
- null-intent guard в onStartCommand (Q2.1).
|
||||
|
||||
🟢 **Хорошо как есть:** закрытие сокета (2.3), повторное тело OkHttp (3.1), порядок string/close (3.5), VIN-декод (4.1), STFT/LTFT (4.5), cleartext off (8.5), fallback-скрипт без ретраев (3.3).
|
||||
@@ -0,0 +1,236 @@
|
||||
# Ревью проекта elmAI (ответы на opus-questions.md)
|
||||
|
||||
> Ревьювер: Opus 4.8 · 31 мая 2026 · v0.35.0-dev
|
||||
> Разбор по коду: `obd/protocol.py`, `brain/client.py`, `brain/prompts.py`, `api/routes.py`, `api/db.py`, `api/parser.py`, `web/app.py`.
|
||||
> Android-модуль (`elmer-android/`) в workspace отсутствует — по нему выводы на основе описаний в вопросах.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1. Стейт-машина ELM327: баги и крайние случаи
|
||||
|
||||
### 🔴 Стартовый буфер не сбрасывается перед командой → десинхронизация
|
||||
В `_exec()` сразу идёт `_write(cmd)` без очистки входного буфера. Если предыдущая команда отвалилась по таймауту, в ОС-буфере остаются «хвосты» (часть ответа, поздний `>`). Следующий `_read()` прочитает этот мусор как ответ на новую команду и классифицирует его неверно — классическая рассинхронизация ELM327.
|
||||
|
||||
```python
|
||||
def _write(self, cmd: str):
|
||||
self._ser.reset_input_buffer() # сбросить хвосты предыдущего ответа
|
||||
self._ser.write((cmd + "\r").encode())
|
||||
self._ser.flush()
|
||||
logger.debug(f"AndrOBD → {cmd}")
|
||||
```
|
||||
|
||||
### 🔴 Состояние `DISCONNECTED`/`ERROR` затирается в `send()`
|
||||
`send()` проверяет только `State.ERROR` перед `_recover()`. Но BUS ERROR в `_handle()` ставит `DISCONNECTED`, а в конце `send()` безусловно пишет `self._state = State.READY`. То есть после фатальной ошибки шины машина всё равно объявляется READY, и накопленный сбой маскируется.
|
||||
|
||||
```python
|
||||
def send(self, cmd: str) -> str:
|
||||
if self._state in (State.ERROR, State.DISCONNECTED):
|
||||
self._recover()
|
||||
self._state = State.BUSY
|
||||
result = self._exec(cmd, self._timing.ms)
|
||||
# НЕ ставить READY безусловно — _exec мог уйти в ERROR
|
||||
if self._state == State.BUSY:
|
||||
self._state = State.READY
|
||||
return result
|
||||
```
|
||||
|
||||
### 🟡 `>` посреди мусора (вопрос 1.2)
|
||||
`_read()` возвращает всё накопленное до первого `>`. Если ELM прислал `SEARCHING...` затем данные затем `>`, всё склеится в одну строку через `\n`, а `Rsp.identify()` смотрит только на начало (`startswith`) — реальные данные после `SEARCHING` будут потеряны/неверно классифицированы. AndrOBD обрабатывает каждую строку отдельно. Рекомендация: классифицировать построчно, а не всю склейку.
|
||||
|
||||
### 🟢 Бесконечный цикл в `_exec()` (вопрос 1.5)
|
||||
Цикл жёстко ограничен `range(10)`, по выходу — `State.ERROR` и `return ""`. Бесконечного цикла нет. Но обратите внимание: при инициализации шаг `t += 1000` за 10 итераций даёт суммарно до ~55с ожидания на одну команду — для `INIT_TMO=10000` это может неприятно затянуть `init()`.
|
||||
|
||||
### 🟡 Восстановление после BUS ERROR (вопрос 1.3)
|
||||
Логика `ATPC → ATSP0` корректна по сути, но ответы на них читаются `_try_read()` и **молча выбрасываются**. Если `ATSP0` не подтвердился (ELM завис), машина об этом не узнает и пойдёт слать команды в неинициализированный протокол. Желательно проверять, что на `ATSP0` пришёл `OK`/`>`, иначе — полный reset (`ATZ`).
|
||||
|
||||
### 🟡 Поллинг 1мс (вопрос 1.4)
|
||||
1мс `time.sleep` в Python реально даёт ~1–15мс из-за гранулярности планировщика — на практике это не вредит (ELM медленнее), но и «честных» 1мс там нет. На быстрых ELM327 v1.5/v2.1 это не узкое место; узкое место — таймаут адаптива, а не поллинг. Менять не нужно.
|
||||
|
||||
### Race conditions
|
||||
В Python-версии всё однопоточное — гонок нет, **пока** один экземпляр `AndrOBD` не шарится между потоками. Если планируется параллельный доступ — добавьте `threading.Lock` вокруг `send()`.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2. HTTP 499 при upload с мобильной сети
|
||||
|
||||
### 🔴 Нет идемпотентности → дубликаты при ретрае (вопрос 2.5)
|
||||
Это главная проблема. Сценарий 499: сервер **уже принял и обработал** запрос (LLM-анализ 30–120с), но клиент отвалился по read timeout и шлёт ретрай. Результат — вторая полная LLM-сессия и **вторая запись в `sessions`**. `upload_session()` не имеет ключа идемпотентности.
|
||||
|
||||
Решение — клиент генерирует `request_id` (UUID), сервер кэширует результат:
|
||||
```python
|
||||
data = request.get_json(silent=True)
|
||||
req_id = data.get("request_id")
|
||||
if req_id:
|
||||
cached = db.get_session_by_request_id(req_id) # + колонка request_id UNIQUE
|
||||
if cached:
|
||||
return jsonify(cached["response_json"]), 200
|
||||
```
|
||||
|
||||
### 🟡 Стратегия ретраев — нужен backoff и идемпотентность
|
||||
3 ретрая с фиксированной задержкой 2с на мобильной сети мало помогают: если причина — долгий LLM-ответ (>read timeout 180с), то все 3 попытки упрутся в тот же таймаут и каждая запустит новый LLM-прогон. Рекомендация: exponential backoff (2/4/8с + jitter) **и** обязательно идемпотентность (см. выше), иначе ретраи только множат нагрузку.
|
||||
|
||||
### 🟡 Корень 499 — рассинхрон таймаутов клиент/сервер (вопрос 2.1)
|
||||
Клиентский read 180с ≈ gunicorn timeout 180с. При длинном ответе LLM (`Diagnoser.timeout=120`, но сам upload может суммарно дольше) клиент рвёт соединение ровно в момент, когда сервер ещё пишет ответ. Прочие частые причины 499 на мобильной: смена сети Wi-Fi↔LTE (новый IP, старый сокет мёртв), NAT-таймаут оператора (часто 30–60с тишины), Doze/засыпание приложения. Рекомендация: клиентский read timeout должен быть **строго больше** серверного (например, 240с против gunicorn 180с), а сервер — отвечать быстрее (streaming, см. ниже).
|
||||
|
||||
### 🟡 Write timeout на медленной сети (вопрос 2.3)
|
||||
Да, при толстом батче (`raw_responses` целиком) и слабом upload на LTE write timeout 60с реально достижим. Тело JSON со всеми сырыми ответами может быть десятки–сотни КБ.
|
||||
|
||||
### 🟡 Чанки/сжатие (вопрос 2.4)
|
||||
Чанкинг избыточен для типичного объёма, а вот **gzip тела** даст быстрый выигрыш (JSON сжимается в 5–10 раз) и снимет риск write timeout:
|
||||
```kotlin
|
||||
// OkHttp: добавить gzip-обёртку RequestBody + заголовок
|
||||
.header("Content-Encoding", "gzip")
|
||||
```
|
||||
Сервер: nginx сам разожмёт при наличии `gunzip`/decompression, либо Flask с `request.get_data()` + `gzip.decompress`. Это дешевле, чем переписывать на чанки.
|
||||
|
||||
### Главная архитектурная рекомендация
|
||||
Разделите «приём данных» и «LLM-анализ». Эндпоинт должен **быстро** (1–2с) принять батч, сохранить, вернуть `session_id`, а диагноз отдавать отдельным polling-эндпоинтом (`GET /api/v1/session/<id>/result`) или через streaming. Тогда 499 из-за долгого LLM исчезнет как класс.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3. Архитектура: три модуля + Android пакеты
|
||||
|
||||
### 🟢 Границы модулей в целом чистые
|
||||
`obd/` ничего не знает про `brain/` и `api/`; `brain/` — изолированный LLM-клиент; `api/` оркестрирует. Направление зависимостей `web → api → brain/obd` корректное (вопрос 3.4 — да, правильное).
|
||||
|
||||
### 🟡 Импорты внутри функций (вопрос 3.2)
|
||||
В `routes.py` все `from brain.client import Diagnoser`, `from api.db import Database`, `from api.config import load` сделаны внутри обработчиков. Это не «нормально», а компромисс — обычно так лечат циклические импорты или ускоряют старт. Минусы: `load()` читает конфиг с диска **на каждый запрос**, импорт-резолвинг повторяется. Рекомендация: поднять импорты на уровень модуля, а конфиг закэшировать:
|
||||
```python
|
||||
# api/config.py
|
||||
from functools import lru_cache
|
||||
@lru_cache(maxsize=1)
|
||||
def load(): ...
|
||||
```
|
||||
Если поднятие импортов ломает цикл — это сигнал, что цикл надо разорвать явно, а не прятать.
|
||||
|
||||
### 🟡 `config.yaml` (вопрос 3.3)
|
||||
Конфиг сейчас грузится через `api/config.py`. Держать `config.yaml` в корне проекта (рядом с `web/app.py`) логичнее — он общий для `api/`, `brain/`, `obd/`, а не принадлежит только `api/`. Вынесите на верхний уровень, путь резолвьте от корня.
|
||||
|
||||
### 🟢 Заменяемость модулей (вопрос 3.5)
|
||||
`brain/` заменяется на локальный LLM тривиально — он зависит только от OpenAI-совместимого HTTP (`/chat/completions`). Достаточно сменить `base_url`/`model` в конфиге; код менять не нужно. `obd/` тоже изолирован. Это хороший знак для дизайна.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4. SQL-схема: таблица sessions
|
||||
|
||||
### 🔴 Утечка соединений + конкурентный доступ (вопрос 4.5)
|
||||
`Database()` создаётся в каждом запросе, открывает `sqlite3.connect(...)` и **никогда не закрывается** — connection leak. При 4 gunicorn-воркерах одновременные записи в один файл дают `database is locked` (SQLite по умолчанию: 1 писатель, нет ожидания). Минимум:
|
||||
```python
|
||||
self.conn = sqlite3.connect(str(self.path), timeout=30, check_same_thread=False)
|
||||
self.conn.execute("PRAGMA journal_mode=WAL") # параллельные читатели + 1 писатель
|
||||
self.conn.execute("PRAGMA busy_timeout=30000")
|
||||
```
|
||||
И закрывать соединение (контекстный менеджер / `try/finally` / `db.close()`), либо держать один пул на воркер. WAL критичен для multi-worker.
|
||||
|
||||
### 🟡 30+ колонок в одной таблице (вопрос 4.1)
|
||||
Для SQLite это **нормально** (лимит 2000 колонок), денормализация под аналитику оправдана. Но смешаны три логических домена: телефон, ELM, LLM. Это не баг, а запах. Пока таблица аналитическая (одна запись = одна сессия) — оставьте; если начнёте часто менять набор полей телефона/ELM — выносите в отдельные таблицы или JSON-колонку.
|
||||
|
||||
### 🟢 raw_responses как JSON TEXT (вопрос 4.2)
|
||||
Ок для SQLite. При необходимости запросов внутрь — используйте `json_extract()` (есть в SQLite ≥3.38). Менять не нужно.
|
||||
|
||||
### 🟡 Индексы (вопрос 4.3)
|
||||
`created_at`, `vin`, `elm_mac`, `android_id` — разумный набор. Но `vin` nullable и часто NULL — индекс будет «разреженным», это норм. Добавьте составной `(android_id, created_at)` если будете строить историю по устройству — иначе текущих достаточно.
|
||||
|
||||
### 🟡 Мёртвые таблицы (вопрос 4.4)
|
||||
`cars`, `diagnostic_tokens`, `llm_messages`, `ecu_parameters`, `dtc_codes` создаются в `_init_schema()`, имеют методы-обёртки в `db.py`, но в текущем пути `upload`/`chat` **не используются**. Это «второй контур», который вводит в заблуждение (например, история диалога в `/chat` идёт из клиента, а не из `llm_messages`). Решение: либо подключите их (тогда `/chat` сможет хранить историю на сервере по VIN), либо удалите вместе с методами. Сейчас они — технический долг и риск рассинхрона схемы.
|
||||
|
||||
### 🟢 Инъекции
|
||||
Все запросы параметризованы (`?`), SQL-инъекций нет.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5. LLM-интеграция: промпты и таймауты
|
||||
|
||||
### 🔴 Рассинхрон модели и таймаута в коде
|
||||
- `brain/client.py`: `DEFAULT_MODEL = "gpt-oss-20b"`, а конфиг/доки — `gpt-oss-120b`. Дефолт-fallback тихо подменит модель, если конфиг недокинул `model`.
|
||||
- `Diagnoser.ask(... timeout=120)`, но в вопросе и nginx/gunicorn заявлено 180с. Таймаут захардкожен и не берётся из конфига.
|
||||
- Докстринги и комментарии говорят «DeepSeek», хотя API — `api.aillm.ru` / gpt-oss. Чисто косметика, но путает.
|
||||
|
||||
```python
|
||||
def __init__(self, api_key, model="gpt-oss-120b", base_url=DEFAULT_BASE, timeout=180):
|
||||
...
|
||||
self.timeout = timeout
|
||||
def ask(self, messages):
|
||||
resp = requests.post(..., timeout=self.timeout)
|
||||
```
|
||||
|
||||
### 🟡 Нет streaming + heartbeat (вопрос 5.2)
|
||||
`requests.post` без `stream=True` на 120–180с — это «чёрный ящик»: клиент не видит прогресса и рвёт по таймауту (см. Вопрос 2). Для длинной генерации лучше streaming (SSE) с проксированием токенов клиенту — тогда соединение «живое», NAT не закрывает, 499 пропадает. Минимум — heartbeat-байты каждые N секунд.
|
||||
|
||||
### 🟡 История диалога в /chat (вопрос 5.3)
|
||||
История склеивается в **один user-prompt** строкой («Водитель: …/Автоэксперт: …»), а не передаётся как полноценный массив `messages` с ролями. Модель хуже держит контекст, и при длинной истории (даже срезанной до 10) промпт может раздуться. Лучше передавать историю настоящими `role: user/assistant` сообщениями (метод `diagnose` это уже умеет через `history`!) и считать токены, а не сообщения:
|
||||
```python
|
||||
hist_msgs = [{"role": m["role"], "content": m["content"]} for m in history[-10:]]
|
||||
answer = diagnoser.diagnose(SYSTEM_CHAT, question, history=hist_msgs)
|
||||
```
|
||||
Переполнения контекста сейчас никто не контролирует — добавьте бюджет по токенам.
|
||||
|
||||
### 🟡 Обработка ошибок LLM (вопрос 5.5)
|
||||
Сейчас один общий `except Exception` → строка «LLM недоступен: {e}». Нет различия rate limit (429, нужен retry-after), timeout (нужен ретрай), 5xx (ретрай) vs 4xx (не ретраить). И текст исключения уходит **прямо в ответ пользователю** — может протечь URL/детали. Разделите коды:
|
||||
```python
|
||||
try:
|
||||
...
|
||||
except requests.Timeout: # ретрай
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code == 429: ... # backoff по Retry-After
|
||||
```
|
||||
|
||||
### 🟢 Промпт для диагностики (вопрос 5.1)
|
||||
`SYSTEM_PROMPT` сильный: 10 правил, явный формат с таблицами, проценты уверенности, «проверь перед заменой», секция «если не поможет». Это хорошо. Чего не хватает: (1) данных об авто (make/model/year/engine почти всегда отсутствуют — VIN есть, но не расшифровывается), (2) пробег/условия, (3) явного запрета галлюцинировать значения PID, которых нет в данных. Добавьте расшифровку VIN→марка/год (хотя бы WMI) перед отправкой — резко поднимет качество.
|
||||
|
||||
### 🟡 Выбор gpt-oss-120b (вопрос 5.4)
|
||||
Для авто-диагностики ключевое — знание DTC и инженерная логика. 120b разумен как баланс цена/качество. Альтернативы под задачу: Qwen2.5-72B/Qwen3 (хорош в технике, но у вас отмечен CoT-leak баг на fp8-варианте), DeepSeek-V3 (сильная техничка), либо рассуждающая модель (o-серия/R1) для сложных взаимосвязей — но они дороже и медленнее, что усугубит проблему таймаутов из Вопроса 2. Вывод: 120b ок, менять стоит только если качество разбора DTC не устраивает.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6. Безопасность API
|
||||
|
||||
### 🔴 Любой эндпоинт без аутентификации → бесплатный прокси к платному LLM (вопросы 6.1, 6.4)
|
||||
`/api/v1/chat`, `/api/v1/session/upload`, `/api/v1/ping-llm` дёргают платный LLM **без какой-либо аутентификации и без rate limit**. Любой, кто узнал домен, может в цикле слать `/chat` и жечь ваш токен `api.aillm.ru`, а `/ping-llm` вообще тратит LLM-вызов на каждый GET. Для MVP HTTPS защищает только канал, но не от абуза. Минимум:
|
||||
- статический API-ключ приложения в заголовке (да, его можно вытащить из APK, но он отсекает массовый скан-абуз);
|
||||
- rate limiting на nginx (`limit_req_zone`) и/или Flask-Limiter по IP/`android_id`;
|
||||
- `/ping-llm` не должен реально вызывать LLM на каждый пинг — кэшируйте результат на 1–5 мин.
|
||||
|
||||
```nginx
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m;
|
||||
location /api/v1/chat { limit_req zone=api burst=5 nodelay; ... }
|
||||
```
|
||||
|
||||
### 🟡 XSS через diagnosis/raw (вопрос 6.3 — не инъекция, а отображение)
|
||||
SQL-инъекции через `raw_responses` нет (запросы параметризованы). **Но**: ответ ELM327 и текст диагноза от LLM (markdown с таблицами) где-то рендерятся в вебе (`web/templates/index.html`, дашборд сессий). Если markdown/HTML вставляется без экранирования — это stored XSS: вредонос в `raw` ELM или в ответе LLM выполнится в браузере админа. Проверьте, что вывод экранируется (Jinja autoescape по умолчанию вкл — не отключайте `|safe` на этих полях; markdown рендерьте через санитайзер).
|
||||
|
||||
### 🟡 Утечка деталей в ответах
|
||||
`except ... return f"LLM недоступен: {e}"` и `error: str(e)[:100]` отдают внутренние сообщения наружу. Логируйте полностью, клиенту — обобщённый текст.
|
||||
|
||||
### 🟡 API-ключ LLM (вопрос 6.5)
|
||||
Текущая схема (ключ только на сервере, не в APK) — **правильная**, это лучшее в безопасности проекта. Дополнительный прокси не нужен; достаточно закрыть абуз (rate limit + ключ приложения), чтобы вашим серверным ключом не пользовались чужие.
|
||||
|
||||
### Сводка по безопасности
|
||||
| Мера | Приоритет | Статус |
|
||||
|------|-----------|--------|
|
||||
| Rate limiting (nginx/Flask-Limiter) | 🔴 высокий | нет |
|
||||
| Ключ приложения в заголовке | 🟡 средний | нет |
|
||||
| `/ping-llm` без реального LLM-вызова | 🟡 средний | вызывает LLM |
|
||||
| Экранирование diagnosis/raw в вебе | 🟡 средний | проверить |
|
||||
| Не отдавать текст исключений клиенту | 🟡 средний | отдаёт |
|
||||
| Ключ LLM только на сервере | 🟢 | сделано |
|
||||
|
||||
---
|
||||
|
||||
## Итоговый топ проблем (по убыванию важности)
|
||||
|
||||
1. 🔴 **Нет идемпотентности upload** → дубликаты сессий и двойной расход LLM при 499/ретраях (Q2).
|
||||
2. 🔴 **Открытые LLM-эндпоинты без auth/rate-limit** → абуз платного токена (Q6).
|
||||
3. 🔴 **SQLite: утечка соединений + нет WAL/busy_timeout** при 4 воркерах → `database is locked` (Q4).
|
||||
4. 🔴 **Долгий синхронный LLM в запросе** — корень 499; разделить приём данных и анализ, добавить streaming (Q2, Q5).
|
||||
5. 🔴 **`reset_input_buffer` перед командой** в стейт-машине — иначе десинхрон ELM327 (Q1).
|
||||
6. 🟡 Рассинхрон модели/таймаута в `client.py` (20b vs 120b, 120с vs 180с) (Q5).
|
||||
7. 🟡 История диалога `/chat` строкой вместо ролей `messages` (Q5).
|
||||
8. 🟡 Мёртвые таблицы в схеме — подключить или удалить (Q4).
|
||||
|
||||
## Что уже хорошо 🟢
|
||||
- Чистые границы модулей, заменяемый `brain/`.
|
||||
- Сильный диагностический системный промпт.
|
||||
- Параметризованный SQL (нет инъекций).
|
||||
- Ключ LLM не в APK.
|
||||
- Стейт-машина ограничена по ретраям (нет бесконечных циклов).
|
||||
@@ -0,0 +1,79 @@
|
||||
# Ответы Opus и Sonnet — залипание клона ELM327 v1.5
|
||||
|
||||
Дата: 2026-06-29
|
||||
|
||||
---
|
||||
|
||||
## Opus 4.8
|
||||
|
||||
### 1. Природа залипания
|
||||
Прошивка-эмулятор (MCU имитирует команды, нет настоящего чипа). Под нагрузкой буфер ответа переполняется, MCU зацикливается, отдавая последний кадр (`7F0112`) на всё. CAN-контроллер жив, мёртв эмулятор. UART отвечает, но игнорирует ввод.
|
||||
|
||||
### 2. Детект залипания
|
||||
AT-команда возвращает OBD-кадр = залип. `ATRV` без `V`/напряжения = залип. 3+ одинаковых hex-ответа на разные PID. `7F` на PID 0100 — флаг.
|
||||
|
||||
### 3. Паузы
|
||||
2-3 сек снижают вероятность, но не устраняют. Залипание зависит от **суммарного числа команд**, а не интервалов.
|
||||
|
||||
### 4. Auto-recovery
|
||||
`ATWS`/`ATZ` иногда оживляют, но если завис UART-приём — команды не дойдут. **90% случаев — только обесточивание.** Алгоритм: ATWS → 2 сек → ATRV для проверки. Не сработало → требовать переподключения.
|
||||
|
||||
### 5. Оптимальный размер цикла
|
||||
**2-3 PID** на цикл при паузе 2-3 сек. 4 уже на грани.
|
||||
|
||||
### 6. Влияние оборотов
|
||||
CAN-нагрузка ускоряет залипание. 3500+ → больше трафика → раньше переполнение.
|
||||
|
||||
### 7. Замена адаптера
|
||||
OBDLink SX/MX+ — лучший. v2.1 клон — миф, тот же эмулятор.
|
||||
|
||||
### 8. Стратегия
|
||||
Гибрид: адаптироваться (2-3 PID, паузы, детект+recovery), детектить тип, предупреждать, не блокировать.
|
||||
|
||||
---
|
||||
|
||||
## Sonnet
|
||||
|
||||
### 1. Природа залипания
|
||||
Баг клонов v1.5 (CH340/PIC18F). Залипает CAN-контроллер внутри клона.
|
||||
|
||||
### 2. Детект залипания
|
||||
`ATRV` до/после цикла. 2-3 одинаковых ответа подряд.
|
||||
|
||||
### 3. Паузы
|
||||
2-3 сек помогают. Минимум 1.5 сек. 6+ PID → 3-5 сек.
|
||||
|
||||
### 4. Auto-recovery
|
||||
`ATZ` — надёжный. `ATWS` — мягче. `ATPC` — иногда. Последовательность: ATPC → 500ms → ATZ → переинит.
|
||||
|
||||
### 5. Оптимальный размер цикла
|
||||
**4 PID** — хороший баланс. 6 рискованно. 2 безопасно.
|
||||
|
||||
### 6. Влияние оборотов
|
||||
Да, прямое. Холостые — залипает позже.
|
||||
|
||||
### 7. Замена адаптера
|
||||
OBDLink EX/MX+ → оригинал ELM327 v1.4b/v2.1 → клоны v2.1 лотерея.
|
||||
|
||||
### 8. Стратегия
|
||||
Медленный режим (4 PID, 3 сек) как default. Детект + авто-ATZ. Предупреждение в UI.
|
||||
|
||||
---
|
||||
|
||||
## Моё мнение
|
||||
|
||||
**Opus точнее** в трёх ключевых пунктах:
|
||||
|
||||
1. **Природа залипания** — Opus правильно указывает на эмулятор/прошивку, а не CAN-контроллер. Это объясняет почему AT-команды тоже ломаются.
|
||||
|
||||
2. **Auto-recovery** — Opus реалистичнее: 90% только обесточивание. Соннет излишне оптимистичен про ATZ. Наш опыт подтверждает Опуса — ATZ не помогал.
|
||||
|
||||
3. **Размер цикла** — Opus консервативнее (2-3 PID). С учётом что клон залип после ~10 команд, 2-3 PID на цикл безопаснее.
|
||||
|
||||
**Соннет практичнее** в одном: 4 PID + 3 сек как default для продакшена. Но с учётом рисков, 2-3 PID + детект залипания + авто-предупреждение — более надёжный путь.
|
||||
|
||||
**Что берём в работу:**
|
||||
- Детект залипания через `ATRV` (без `V` = залип)
|
||||
- Циклы по 3 PID с паузой 2-3 сек
|
||||
- Auto-recovery через ATWS → проверка ATRV → если нет → сообщение пользователю
|
||||
- Предупреждение в UI: «Адаптер нестабилен, рекомендуем OBDLink»
|
||||
@@ -0,0 +1,92 @@
|
||||
# ELM327 Relay — хронология ошибок (АРХИВ)
|
||||
|
||||
> **Устарело**. Актуальный документ: `doc/failures-journal.md`
|
||||
|
||||
## Дата: 2026-07-04 | Версия на момент написания: v0.3.7-dev
|
||||
|
||||
---
|
||||
|
||||
## Текущая версия
|
||||
**v0.3.7-dev** (commit `3e4eba7`, ветка `opus-fixes`)
|
||||
|
||||
---
|
||||
|
||||
## Что работает (v0.3.5-dev, тест 13/15)
|
||||
- AndrOBD-совместимый init: `ATSP0 → ATI → ATS0 → ATL0 → ATE0`
|
||||
- Клон v1.5: ATAT1 и ATST пропускаются
|
||||
- `handle()` **без AT-команд** — только state tracking (как AndrOBD)
|
||||
- `recover()` — только сброс state в READY
|
||||
- 87% успешных PID-команд после прогрева
|
||||
|
||||
## Что НЕ работает
|
||||
- **Буферный сдвиг после init**: первые 1-2 команды возвращают мусор/пустоту
|
||||
- **STOPPED от ELM**: протокол останавливается, нужен перезапуск
|
||||
- **Двигатель заглушен**: PID не работают, только ATRV
|
||||
|
||||
---
|
||||
|
||||
## ВСЕ ошибки (хронология)
|
||||
|
||||
### Ошибка 1: Freeze detection через ATRV (v0.3.0-dev)
|
||||
**Что сделано**: каждые 10 команд слали ATRV для проверки залипания клона.
|
||||
**Почему ошибка**: ATRV ломает синхронизацию команд. Ответ "12.6V" попадает в буфер и читается как ответ на следующий PID.
|
||||
**Исправлено**: удалено в v0.3.2-dev.
|
||||
|
||||
### Ошибка 2: AT-команды в handle() (v0.1.x–v0.3.3-dev)
|
||||
**Что сделано**: handle() при STOPPED/UNABLE/BUS_ERROR слал ATPC→ATWS→ATSP0.
|
||||
**Почему ошибка**: AT-команды отправляются внутри обработки ответа текущей команды. Их ответы загрязняют буфер для следующей команды.
|
||||
**Исправлено**: удалено в v0.3.5-dev. handle() теперь только меняет state.
|
||||
|
||||
### Ошибка 3: AT-команды в recover() (v0.1.x–v0.3.3-dev)
|
||||
**Что сделано**: recover() слал ATPC→ATWS→ATSP0→ATE0 при state=ERROR/DISCONNECTED.
|
||||
**Почему ошибка**: recover() вызывается из sendCommand() перед отправкой команды. AT-ответы могут не успеть полностью прийти до отправки PID.
|
||||
**Исправлено**: упрощён в v0.3.3-dev. Только сброс state и timeout.
|
||||
|
||||
### Ошибка 4: Дренаж 0100 после init (v0.3.6-dev)
|
||||
**Что сделано**: попытка поглотить буферный сдвиг отправкой 0100 сразу после init.
|
||||
**Почему ошибка**: дренаж сам вызывает сдвиг буфера и дезориентирует клон.
|
||||
**Исправлено**: удалено в v0.3.7-dev.
|
||||
|
||||
### Ошибка 5: Recovery ATSP0 в relayLoop (v0.3.5-dev–v0.3.7-dev)
|
||||
**Что сделано**: после STOPPED или пустого ответа — ATSP0 для перезапуска протокола.
|
||||
**Почему ошибка**: ATSP0 через sendBlocking блокирует single-thread executor. Последующие команды ждут в очереди, тест видит таймауты.
|
||||
**Текущий статус**: всё ещё в коде v0.3.7-dev.
|
||||
|
||||
### Ошибка 6: Тест без device_id в /response
|
||||
**Что сделано**: тестовый скрипт вызывал `/response?wait=3` без device_id.
|
||||
**Почему ошибка**: сервер возвращал ответы от чужих устройств/сессий.
|
||||
**Исправлено**: сервер (raw_elm.py) обновлён — `/response` принимает `device_id` параметр. Тест обновлён.
|
||||
|
||||
### Ошибка 7: Тест без обновления seq
|
||||
**Что сделано**: тест всегда слал `seq=0` в `/response`.
|
||||
**Почему ошибка**: сервер возвращал один и тот же ответ многократно.
|
||||
**Исправлено**: тест обновляет seq после каждого ответа.
|
||||
|
||||
### Ошибка 8: Версия на сайте не обновлялась
|
||||
**Что сделано**: деплой менял HTML только в `/opt/elmer/web/templates/`.
|
||||
**Почему ошибка**: Flask использует `/opt/elmer/templates/index.html`.
|
||||
**Исправлено**: деплой теперь обновляет оба файла.
|
||||
|
||||
---
|
||||
|
||||
## Отличия от AndrOBD
|
||||
|
||||
| | AndrOBD | Наш код |
|
||||
|---|---|---|
|
||||
| Архитектура | Асинхронный (поток читает → handleTelegram) | Синхронный (sendCommand → ждать ответ) |
|
||||
| write() | Не дренирует перед отправкой | drainInput() перед каждым write() |
|
||||
| Init | ATSP0→ATAT1→ATS0→ATL0→ATE0 | ATSP0→ATI→ATS0→ATL0→ATE0 |
|
||||
| ATI | Не используется | Для детекта клона v1.5 |
|
||||
| handle() | Только state tracking | Только state tracking ✅ |
|
||||
| Ошибки | Не шлёт AT-команд из handle() | Не шлёт AT-команд ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
| Файл | Состояние |
|
||||
|------|-----------|
|
||||
| `ElmProtocol.kt` | handle() без AT-команд, recover() пустой |
|
||||
| `RawRelayService.kt` | relayLoop с recovery ATSP0 |
|
||||
| `ElmActor.kt` | Single-thread executor |
|
||||
| `api/raw_elm.py` | /response с device_id фильтром |
|
||||
@@ -0,0 +1,173 @@
|
||||
# Конкуренты (2026-05-25)
|
||||
|
||||
## Резюме
|
||||
|
||||
На рынке есть 4 буржуазных конкурента. **Русскоязычных — ноль.** RuStore пустой по запросу «OBD2 + AI диагностика».
|
||||
|
||||
Это главное преимущество: мы не «ещё один», а **первый на русском рынке**.
|
||||
|
||||
---
|
||||
|
||||
## DiagnostiX AI
|
||||
| | |
|
||||
|---|---|
|
||||
| **Разработчик** | Exza / Ontario Analytics (Канада) |
|
||||
| **Платформа** | Android (Google Play) |
|
||||
| **ID** | `com.exza.diagnostixai` |
|
||||
| **Описание** | OBD2 scanner + AI mechanic assistant. Чтение/сброс DTC, live data, сенсоры в реальном времени, AI-помощь |
|
||||
| **LLM** | Не раскрыт |
|
||||
| **Слабость** | Всё в одном app, locked-in |
|
||||
|
||||
## OBDAI
|
||||
| | |
|
||||
|---|---|
|
||||
| **Разработчик** | Ontario Analytics (Канада/США) |
|
||||
| **Платформа** | iOS + Android |
|
||||
| **ID** | `com.ontarioanalytics.obdai` |
|
||||
| **Описание** | AI-агент ARIA (Automotive Reasoning & Intelligence Agent). Профессиональные диагностические отчёты. «Поговори с машиной» |
|
||||
| **LLM** | ARIA (вероятно свой/кастомный) |
|
||||
| **Слабость** | Закрытый, платный, английский |
|
||||
|
||||
## Zero Touch Car Diagnostics
|
||||
| | |
|
||||
|---|---|
|
||||
| **Разработчик** | kellinheller (GitHub) |
|
||||
| **Платформа** | Кроссплатформа (Flutter) |
|
||||
| **Репозиторий** | `github.com/kellinheller/zero_touch_car_diagnostics` |
|
||||
| **Описание** | Open source. OBD2 + датчики телефона + GPS + AI-анализ через Google Gemini 2.5 Pro |
|
||||
| **LLM** | Google Gemini 2.5 Pro |
|
||||
| **Слабость** | Gemini платный. Flutter = тяжёлый. Не для ELM327 v1.5 (клонов) |
|
||||
|
||||
## MUCAR 892BT MUAI (THINKCAR)
|
||||
| | |
|
||||
|---|---|
|
||||
| **Производитель** | THINKCAR (Китай) |
|
||||
| **Тип** | Железка + софт |
|
||||
| **Описание** | Полноценный диагностический сканер со своим экраном. 8 диагностических модулей, отчёты |
|
||||
| **LLM** | **DeepSeek** (единственный конкурент на DeepSeek) |
|
||||
| **Слабость** | Свой сканер (~$150-300). Не работает с ELM327 |
|
||||
|
||||
---
|
||||
|
||||
## Сравнение с Elmer
|
||||
|
||||
| Критерий | DiagnostiX AI | OBDAI | Zero Touch | MUCAR MUAI | **Elmer** |
|
||||
|---|---|---|---|---|---|
|
||||
| Open source клиент | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| Тонкий клиент (свой сервер) | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Русский язык | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| RuStore | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Работает с дешёвым ELM327 | ✅ | ✅ | 🟡 | ❌ | ✅ |
|
||||
| DeepSeek | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| Бесплатно | 🟡 | ❌ | ✅ | ❌ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Вывод
|
||||
|
||||
Ниша существует и растёт. MUCAR с DeepSeek — сигнал что LLM-подход правильный.
|
||||
Наш козырь: **открытость + русский рынок + дешёвый ELM327**. Никто не сочетает эти три фактора.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-27: Дополнительные находки
|
||||
|
||||
### Automotive AI (Eloquent-Algorithmics)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Репозиторий** | `github.com/Eloquent-Algorithmics/Automotive-AI` |
|
||||
| **Платформа** | Windows / Linux (Python) |
|
||||
| **Описание** | Экспериментальный проект. ELM327 (BT или эмулятор) → OpenAI GPT. Голосовой ввод/вывод: «считай коды», «сделай отчёт». |
|
||||
| **Технологии** | Python, OpenAI API |
|
||||
| **LLM** | OpenAI GPT (заменяемый) |
|
||||
| **Обновление** | Май 2025 |
|
||||
| **Плюсы** | Прямой конвейер ELM327→LLM. Умеет DTC, параметры, стоп-кадры. Голос — интересная фича для гаража (руки грязные). |
|
||||
| **Минусы** | Только десктоп, не Android. Голос бесполезен в движении. Английский. |
|
||||
| **Вывод** | Смотреть архитектуру: как парсит ответы, как формирует промпт. Голос — возможно позже для нашей веб-панели. |
|
||||
|
||||
### OBD2AI
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Платформа** | Android (готовое приложение) |
|
||||
| **Описание** | ELM327 по BT → ChatGPT → анализ ошибок. Простая схема без промежуточного сервера. |
|
||||
| **LLM** | ChatGPT |
|
||||
| **Плюсы** | Быстрый старт — установил и работает. Можно использовать как референс перед поездкой к машине. |
|
||||
| **Минусы** | Только ChatGPT. Без сервера — нет batch-загрузки, нет истории, нет своей LLM. |
|
||||
| **Вывод** | Проверить как референс Android-клиента. Но наша архитектура (fat-client + свой сервер) гибче. |
|
||||
|
||||
### Car OBD2 Diagnostics (kagibson)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Репозиторий** | `github.com/kagibson/car_obd2_diagnostics` |
|
||||
| **Платформа** | Linux/Windows (Docker) |
|
||||
| **Описание** | «Vibe coding» эксперимент. ELM327 → веб-панель с RPM, скоростью, температурой, DTC, стоп-кадрами. |
|
||||
| **Технологии** | Docker, Python (бэкенд), React (фронтенд) |
|
||||
| **LLM** | Не указан (вероятно подключаемый) |
|
||||
| **Плюсы** | Чистый читаемый код. Веб-панель с графиками — хороший референс для нашей веб-части. Docker — лёгкий деплой. |
|
||||
| **Минусы** | Только десктоп, не Android. Нет LLM-диагноза из коробки. |
|
||||
| **Вывод** | Изучить архитектуру веб-панели (React-компоненты для отображения PID). Docker — возможно для production-деплоя elmer-сервера. |
|
||||
|
||||
---
|
||||
|
||||
## Обновлённое сравнение (2026-05-27)
|
||||
|
||||
| Критерий | DiagnostiX | OBDAI | ZeroTouch | MUCAR | AutoAI | OBD2AI | CarOBD2 | **Elmer** |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **Android** | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
|
||||
| **Offline-скрипты** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
||||
| **Свой сервер** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| **Русский язык** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| **Жалобы водителя** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (план) |
|
||||
| **Cross-val LLM** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (план) |
|
||||
| **Любой LLM** | ❌ | ❌ | ❌ | ❌ | 🟡 | ❌ | ❌ | ✅ |
|
||||
| **Open source** | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ |
|
||||
| **Дешёвый ELM327** | ✅ | ✅ | 🟡 | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
**Уникальных фич Elmer:** 7 из 11 (отмечены ✅ только у нас).
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-27 (2): Масштабный поиск — 50+ проектов
|
||||
|
||||
Найдено через GitHub Code Search: 50+ open-source проектов на стыке ELM327 + LLM.
|
||||
|
||||
### 🔥 Проекты с рабочим Android→ELM→LLM (можно изучать код)
|
||||
|
||||
| Проект | Язык | LLM | Звёзды | Обновлён | Ценность для нас |
|
||||
|--------|------|-----|--------|----------|------------------|
|
||||
| **Wal33D/OBD-Droid** | Java | ChatGPT | 4 | Фев 2025 | ⭐ **ЛУЧШИЙ.** Рабочий Android→ELM→LLM. AI copilot, GPS, NHTSA |
|
||||
| **catsmoker/OBD2AI** | Kotlin | gpt-5-mini | 3 | Окт 2025 | ⭐ Android Kotlin. BT SPP + BLE |
|
||||
| **petrpatek/obd2-mcp-server** | Python | Claude MCP | 2 | 10 дней назад | Свежий. 1937 Ford DTC |
|
||||
| **castlebbs/Vehicle-Diagnostic-Assistant** | Python | DeepSeek/Claude | 2 | Дек 2025 | Hackathon-победитель. W600 MCU |
|
||||
|
||||
### 🧠 Что украдено из OBD-Droid (Java Android)
|
||||
|
||||
**Подтверждённые паттерны ELM327-коммуникации:**
|
||||
|
||||
1. **UUID SPP:** `00001101-0000-1000-8000-00805F9B34FB` ✅ как у нас
|
||||
2. **Чтение:** побайтово, сон 1мс между проверками (не sleep(250)!)
|
||||
3. **Детекция промпта `>`:** КРИТИЧНО — не слать следующую команду пока не получили `>`
|
||||
4. **Обработка ошибок:** SEARCHING → ждать, NO DATA → пропустить, UNABLE → reconnect, CAN ERROR → warm start
|
||||
5. **Мульти-фрейм ISO-TP:** строки с префиксом `:`, буферизация до полного ответа
|
||||
6. **Адаптивный таймаут:** базовый 5000мс, увеличивается для медленных шин
|
||||
7. **`flush()` после каждой команды** — иначе данные теряются
|
||||
8. **ATH1/ATH0:** управление заголовками CAN
|
||||
|
||||
**Что у нас НЕ так по сравнению с OBD-Droid:**
|
||||
- ❌ Мы ждём `sleep(250)` вместо ожидания `>` промпта
|
||||
- ❌ Нет адаптивного таймаута
|
||||
- ❌ Нет recovery при CAN ERROR / UNABLE TO CONNECT
|
||||
- ❌ Нет `flush()` после отправки
|
||||
|
||||
### 📊 Итоговая картина рынка
|
||||
|
||||
| Тип | Количество |
|
||||
|-----|-----------|
|
||||
| ELM327 + LLM (всего) | 50+ |
|
||||
| Рабочий Android→ELM→LLM | 4 |
|
||||
| Русский язык | **0 (только мы)** |
|
||||
| Offline fat-client | **0 (только мы)** |
|
||||
| Жалобы водителя | **0 (только мы)** |
|
||||
@@ -0,0 +1,710 @@
|
||||
# ELM327 Communication Patterns — анализ 5+14 проектов
|
||||
|
||||
> **Цель:** понять как РЕАЛЬНО работают проекты с ELM327, выбрать лучшие паттерны для Elmer.
|
||||
> **Дата:** 2026-05-27
|
||||
> **Источники:** 5 LLM-проектов + 14 традиционных OBD2 Android-проектов
|
||||
|
||||
---
|
||||
|
||||
## Часть 1: Проекты с LLM (ELM → LLM → диагноз)
|
||||
|
||||
### Сводная таблица
|
||||
|
||||
| | OBD-Droid | OBD2AI | Automotive-AI | obd2-mcp-server | Vehicle-Diag-Assist |
|
||||
|---|---|---|---|---|---|
|
||||
| **Язык** | Java | Kotlin | Python | Python | C (W600) + Python |
|
||||
| **Платформа** | Android | Android | Desktop | Desktop/Claude MCP | Embedded (MCU) |
|
||||
| **LLM** | ChatGPT | gpt-5-mini | GPT-3.5/4 | Claude (MCP) | DeepSeek/Claude |
|
||||
| **Чтение** | Побайтово, 1мс | kotlin-obd lib | readline() | Побайтово (BLE/SPP) | UART, семафор |
|
||||
| **UUID** | 00001101... | 00001101... | N/A (pyserial) | BLE + serial | N/A (UART) |
|
||||
| **Baud** | — | — | config.py | 38400 (auto-retry) | 38400 8N1 |
|
||||
| **Timeout** | Адаптивный 5с | 400мс fix | 1с | 20с connect / 2с config | 2000мс |
|
||||
| **Инит** | ATD→ATE0→ATL0→ATS0→ATH1→... | ATZ→ATE0→ATL0→ATSP0 | N/A | ATZ→ATE0→ATL0→ATS0→ATH1→ATCAF1→ATAT1→ATST64→ATSP0 | ATZ→... |
|
||||
| **Ретраи** | requeue + SETPROT | 3 strikes → stop | Нет | [2,5,10]с backoff | Нет |
|
||||
| **Simulator** | Встроенный demo | Нет | ELM327-emulator | Mock mode (Ford) | Gradio + HW sim |
|
||||
| **DTC база** | Встроенная | Нет | Нет | 1937 Ford + generic | Нет |
|
||||
|
||||
---
|
||||
|
||||
## 1. OBD-Droid (Wal33D) — Java Android ⭐ ЛУЧШИЙ
|
||||
|
||||
### 1.1. StreamHandler.java — побайтовый I/O
|
||||
|
||||
```java
|
||||
// ЧТЕНИЕ: побайтово, сон 1мс между проверками
|
||||
public void run() {
|
||||
while (true) {
|
||||
if (in.available() > 0) {
|
||||
if ((chr = in.read()) > 0) {
|
||||
processRxChar(chr);
|
||||
} else break;
|
||||
} else {
|
||||
Thread.sleep(1); // ← 1 миллисекунда!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ОБРАБОТКА СИМВОЛОВ: '>' = такой же разделитель как CR/LF!
|
||||
private void processRxChar(int chr) {
|
||||
switch (chr) {
|
||||
case 32: break; // пробел — игнорируем
|
||||
case '>': // промпт ELM
|
||||
message += (char) chr;
|
||||
// fall through — НЕ отдельный случай!
|
||||
case 10: // LF
|
||||
case 13: // CR
|
||||
messageHandler.handleTelegram(message.toCharArray());
|
||||
message = "";
|
||||
break;
|
||||
default:
|
||||
message += (char) chr;
|
||||
}
|
||||
}
|
||||
|
||||
// ОТПРАВКА: BufferedWriter с буфером 1 байт = flush на каждом байте
|
||||
out = new BufferedWriter(new OutputStreamWriter(outStream), 1);
|
||||
|
||||
public int writeTelegram(final char[] buffer, int type, Object id) {
|
||||
new Thread(() -> {
|
||||
String msg = new String(buffer) + "\r"; // ELM ждёт CR
|
||||
out.write(msg.toCharArray());
|
||||
out.flush(); // немедленный flush из-за буфера 1 байт
|
||||
}).start();
|
||||
return buffer.length;
|
||||
}
|
||||
```
|
||||
|
||||
**Ключевые выводы:**
|
||||
- `>` — НЕ спецсигнал «можно слать дальше». Это просто разделитель строк, как CR/LF.
|
||||
- Буфер 1 байт на запись = каждый байт сразу уходит в порт.
|
||||
- Отправка в отдельном потоке (не блокирует чтение).
|
||||
|
||||
### 1.2. ElmProt.java — стейт-машина протокола
|
||||
|
||||
**RSP_ID — все возможные ответы ELM327:**
|
||||
```java
|
||||
enum RSP_ID {
|
||||
PROMPT(">"), OK("OK"), MODEL("ELM"),
|
||||
NODATA("NODATA"), SEARCH("SEARCHING"),
|
||||
ERROR("ERROR"), NOCONN("UNABLE"), NOCONN2("NABLETO"),
|
||||
CANERROR("CANERROR"), BUSBUSY("BUSBUSY"),
|
||||
BUSERROR("BUSERROR"), BUSINIERR("BUSINIT:ERR"),
|
||||
BUSINIERR2("BUSINIT:BUS"), BUSINIERR3("BUSINIT:...ERR"),
|
||||
FBERROR("FBERROR"), DATAERROR("DATAERROR"),
|
||||
BUFFERFULL("BUFFERFULL"), STOPPED("STOPPED"),
|
||||
RXERROR("<"), QMARK("?"),
|
||||
UNKNOWN("");
|
||||
}
|
||||
```
|
||||
|
||||
**STAT — состояния соединения:**
|
||||
```java
|
||||
UNDEFINED → INITIALIZING → INITIALIZED → ECU_DETECT → ECU_DETECTED
|
||||
→ ECU_SELECTED → CONNECTING → CONNECTED
|
||||
// Ошибки:
|
||||
NODATA, STOPPED, DISCONNECTED, BUSERROR, DATAERROR, RXERROR, ERROR
|
||||
```
|
||||
|
||||
**Инициализация (после ATZ → MODEL):**
|
||||
```
|
||||
ATD // defaults
|
||||
ATE0 // echo off
|
||||
ATL0 // line feeds off
|
||||
ATS0 // spaces off
|
||||
ATH1 // headers ON (для обнаружения ЭБУ)
|
||||
ATDP // узнать протокол
|
||||
ATSPA1 // протокол AUTO
|
||||
ATAT1 // adaptive timing ON
|
||||
ATST<value> // установить таймаут
|
||||
```
|
||||
|
||||
**Обработка ошибок — детально:**
|
||||
```
|
||||
SEARCHING → статус CONNECTING (не ошибка!)
|
||||
NODATA → увеличить OBD timeout + переустановить протокол
|
||||
ERROR → WARMSTART (ATWS)
|
||||
DATAERROR → WARMSTART
|
||||
RXERROR → WARMSTART
|
||||
BUFFERFULL→ WARMSTART
|
||||
BUS ERROR → DISCONNECTED → переустановить протокол + ретрай последней команды
|
||||
UNABLE → DISCONNECTED → переустановить протокол + ретрай
|
||||
```
|
||||
|
||||
**Мульти-фрейм ISO-TP:**
|
||||
```
|
||||
Формат: "0:4100..." — первая строка с длиной
|
||||
"1:4100..." — продолжение
|
||||
charsExpected = байт_длины * 2 (каждый байт = 2 hex символа)
|
||||
```
|
||||
|
||||
### 1.3. BluetoothCommService.java — BT SPP
|
||||
|
||||
```java
|
||||
final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
|
||||
// Первая попытка: стандартный RFCOMM
|
||||
tmp = device.createRfcommSocketToServiceRecord(SPP_UUID); // secure
|
||||
// или
|
||||
tmp = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID); // insecure
|
||||
|
||||
// FALLBACK: reflection-based RFCOMM channel 1 (для глючных адаптеров)
|
||||
Method m = clazz.getMethod("createRfcommSocket", paramTypes);
|
||||
Object[] params = new Object[]{1}; // channel 1
|
||||
sockFallback = (BluetoothSocket) m.invoke(device, params);
|
||||
```
|
||||
|
||||
**Ключевой вывод:** Есть fallback на reflection-based RFCOMM channel 1 — для дешёвых китайских клонов!
|
||||
|
||||
---
|
||||
|
||||
## 2. OBD2AI (catsmoker) — Kotlin Android
|
||||
|
||||
### 2.1. BluetoothHelper
|
||||
|
||||
```kotlin
|
||||
val sppUuid: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
|
||||
|
||||
suspend fun connectToDevice(deviceAddress: String): Pair<InputStream, OutputStream> {
|
||||
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
|
||||
bluetoothSocket = device.createRfcommSocketToServiceRecord(sppUuid).apply {
|
||||
bluetoothAdapter.cancelDiscovery()
|
||||
connect()
|
||||
}
|
||||
return Pair(socket.inputStream, socket.outputStream)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2. ObdHelper — инициализация и команды
|
||||
|
||||
```kotlin
|
||||
// Инициализация: фиксированные задержки, БЕЗ ожидания '>'
|
||||
suspend fun initializeObd() = withContext(Dispatchers.IO) {
|
||||
suspend fun sendRawCommand(command: String) {
|
||||
out.write((command + "\r").toByteArray())
|
||||
out.flush()
|
||||
delay(400) // ← 400мс после КАЖДОЙ команды
|
||||
}
|
||||
|
||||
sendRawCommand("ATZ") // сброс
|
||||
sendRawCommand("ATE0") // эхо выкл
|
||||
sendRawCommand("ATL0") // line feeds выкл
|
||||
sendRawCommand("ATSP0") // авто-протокол
|
||||
|
||||
delay(1000) // дополнительная пауза после инита
|
||||
// Очистка буфера
|
||||
if (`in`.available() > 0) {
|
||||
val buffer = ByteArray(`in`.available())
|
||||
`in`.read(buffer)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Используется библиотека `kotlin-obd` (eltonvs):**
|
||||
```kotlin
|
||||
// Для стандартных команд — библиотека
|
||||
obdConnection = ObdDeviceConnection(inputStream, outputStream)
|
||||
val result = connection.run(TroubleCodesCommand())
|
||||
|
||||
// Для нестандартных — ручной парсинг
|
||||
class MyRPMCommand : ObdCommand() {
|
||||
override val pid = "0C"
|
||||
override val handler = { it: ObdRawResponse ->
|
||||
val rawValue = it.processedValue
|
||||
val identifier = "410C"
|
||||
val aHex = rawValue.substring(index + 4, index + 6)
|
||||
val bHex = rawValue.substring(index + 6, index + 8)
|
||||
((a * 256) + b) / 4 // формула RPM
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3. Live Data Monitoring
|
||||
|
||||
```kotlin
|
||||
suspend fun startLiveDataMonitoring() = withContext(Dispatchers.IO) {
|
||||
var errorCount = 0
|
||||
while (isMonitoring.get()) {
|
||||
try {
|
||||
val speed = runCommand(MySpeedCommand())
|
||||
val rpm = runCommand(MyRPMCommand())
|
||||
val temp = runCommand(MyCoolantTempCommand())
|
||||
errorCount = 0
|
||||
delay(800) // 800мс между циклами
|
||||
} catch (e: Exception) {
|
||||
errorCount++
|
||||
if (errorCount >= 3) break // 3 ошибки подряд = стоп
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Automotive-AI (Eloquent-Algorithmics) — Python Desktop
|
||||
|
||||
### 3.1. ELM327 через pyserial
|
||||
|
||||
```python
|
||||
# config.py
|
||||
SERIAL_PORT = "/dev/ttyUSB0" # или COM3 на Windows
|
||||
BAUD_RATE = 38400
|
||||
|
||||
# Подключение
|
||||
ser = serial.Serial(port=SERIAL_PORT, baudrate=BAUD_RATE, timeout=1)
|
||||
|
||||
# Отправка команды
|
||||
def send_command(ser, command):
|
||||
ser.write((command + "\r\n").encode()) # CRLF терминатор
|
||||
response = ser.readline().decode().strip()
|
||||
response = response.replace("\r", "").replace(">", "")
|
||||
return response
|
||||
```
|
||||
|
||||
**Ключевые отличия от OBD-Droid:**
|
||||
- `readline()` вместо побайтового чтения — ПРОЩЕ, но менее надёжно
|
||||
- `\r\n` вместо просто `\r`
|
||||
- `timeout=1` — ждёт 1 секунду на readline
|
||||
- Убирает `>` из ответа (не использует как разделитель)
|
||||
|
||||
### 3.2. Парсинг ответов
|
||||
|
||||
```python
|
||||
# RPM: 010C → 41 0C HH LL
|
||||
if cmd == "010C":
|
||||
value = (int(response.split()[2], 16) * 256 +
|
||||
int(response.split()[3], 16)) / 4
|
||||
|
||||
# Coolant: 0105 → 41 05 XX
|
||||
if cmd == "0105":
|
||||
value = int(response.split()[2], 16) - 40 # -40 offset
|
||||
|
||||
# VIN: 0902
|
||||
vin_response = parse_vin_response(response)
|
||||
vehicle_data = decode_vin(vin_response)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. obd2-mcp-server (petrpatek) — Python Claude MCP ⭐ САМЫЙ СВЕЖИЙ
|
||||
|
||||
### 4.1. BLE + Serial подключение
|
||||
|
||||
```
|
||||
Поддерживает:
|
||||
- BLE (vLinker FD, STN чип) — асинхронный, asyncio.Lock
|
||||
- Serial (classic Bluetooth SPP) — синхронный, pyserial
|
||||
|
||||
Baud rate auto-retry: [500k, 115.2k, 38.4k, 9.6k]
|
||||
BLE: 30-секундный keepalive heartbeat (без него адаптер засыпает через ~120с)
|
||||
```
|
||||
|
||||
### 4.2. Инициализация (САМАЯ ПОЛНАЯ)
|
||||
|
||||
```python
|
||||
ATZ # сброс
|
||||
ATE0 # эхо выкл
|
||||
ATL0 # line feeds выкл
|
||||
ATS0 # пробелы выкл
|
||||
ATH1 # заголовки CAN ВКЛ (для обнаружения ЭБУ)
|
||||
ATCAF1 # CAN auto-formatting ON
|
||||
ATAT1 # adaptive timing ON
|
||||
ATST64 # timeout = 64*4ms = 256ms
|
||||
ATSP0 # авто-протокол
|
||||
|
||||
# Для STN адаптеров (OBDlink):
|
||||
ATPP 0E SV 00 # отключить сон
|
||||
ATPP 0E ON # включить
|
||||
```
|
||||
|
||||
### 4.3. Ретраи и таймауты
|
||||
|
||||
```python
|
||||
MAX_RETRIES = 3
|
||||
RETRY_BACKOFF = [2, 5, 10] # секунды
|
||||
CONNECT_TIMEOUT = 20 # секунд
|
||||
PROTOCOL_TIMEOUT = 12 # секунд
|
||||
CONFIG_TIMEOUT = 2 # секунды
|
||||
```
|
||||
|
||||
### 4.4. Очистка ответа
|
||||
|
||||
```python
|
||||
def _clean_elm_response(raw: str) -> str:
|
||||
# Убирает: промпт ">", эхо команд, "SEARCHING...", пустые строки
|
||||
...
|
||||
```
|
||||
|
||||
### 4.5. DTC база данных
|
||||
|
||||
```
|
||||
- 1937 Ford-специфичных кодов
|
||||
- Generic OBD-II коды (P, B, C, U)
|
||||
- Ленивая загрузка по бренду
|
||||
- Скрапинг с troublecodes.net
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Vehicle-Diagnostic-Assistant (castlebbs) — Embedded C + Python
|
||||
|
||||
### 5.1. Аппаратная архитектура
|
||||
|
||||
```
|
||||
W600 MCU ←UART1 38400 8N1→ ELM327 чип → CAN → Авто
|
||||
↕ HTTP/MCP
|
||||
LangChain Agent (Python) → DeepSeek / Claude
|
||||
```
|
||||
|
||||
### 5.2. ELM327 Driver (C)
|
||||
|
||||
```c
|
||||
// elm327.c
|
||||
int elm327_send_command(const char* cmd, char* resp, int len, int timeout) {
|
||||
// Пишет команду + \r в UART1
|
||||
// Ждёт ответ через FreeRTOS semaphore (прерывание по приёму)
|
||||
// Таймаут по умолчанию: 2000мс
|
||||
// Макс. длина ответа: 512 байт
|
||||
}
|
||||
|
||||
// Hybrid simulation mode:
|
||||
// AT команды → реальный ELM327
|
||||
// OBD команды → симуляция (если включена)
|
||||
```
|
||||
|
||||
### 5.3. Поддерживаемые режимы OBD
|
||||
|
||||
```
|
||||
Mode 01: live data (30+ PID)
|
||||
Mode 03: stored DTC (формат 43 XX XX XX XX)
|
||||
Mode 04: clear DTC (44)
|
||||
Mode 07: pending DTC (47)
|
||||
Mode 09: vehicle info (VIN, calibration ID)
|
||||
```
|
||||
|
||||
### 5.4. PID формулы (Mode 01)
|
||||
|
||||
| PID | Формула | Пример |
|
||||
|-----|---------|--------|
|
||||
| 0C (RPM) | `(A*256+B)/4` | 0x1AF8 → 1726 |
|
||||
| 0D (Speed) | `A` (km/h) | 0x00 → 0 |
|
||||
| 05 (ECT) | `A-40` (°C) | 0x5A → 50 |
|
||||
| 04 (Load) | `(A*100)/255` (%) | 0x40 → 25.1 |
|
||||
| 10 (MAF) | `((A*256)+B)/100` (g/s) | — |
|
||||
| 2F (Fuel) | `(A*100)/255` (%) | — |
|
||||
|
||||
### 5.5. Safe formula evaluation
|
||||
|
||||
```python
|
||||
def calculate_obd_value(raw_response, formula):
|
||||
# LLM вызывает этот tool для расчёта значений
|
||||
# safe_eval() — ограниченный eval (только +-*/ и переменные A,B,C,D)
|
||||
hex_bytes = raw_response.replace("41 XX ", "").split()
|
||||
A, B, C, D = [int(x, 16) for x in hex_bytes]
|
||||
return safe_eval(formula, {"A": A, "B": B, "C": C, "D": D})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## СРАВНИТЕЛЬНЫЙ АНАЛИЗ: Что взять для Elmer
|
||||
|
||||
### Инициализация ELM327
|
||||
|
||||
| Проект | Последовательность | Задержки |
|
||||
|--------|-------------------|----------|
|
||||
| OBD-Droid | ATD→ATE0→ATL0→ATS0→ATH1→ATDP→ATSPA1→ATAT1→ATST | Стейт-машина, нет фикс. задержек |
|
||||
| OBD2AI | ATZ→ATE0→ATL0→ATSP0 | 400мс после каждой |
|
||||
| obd2-mcp | ATZ→ATE0→ATL0→ATS0→ATH1→ATCAF1→ATAT1→ATST64→ATSP0 | async, по ответам |
|
||||
| Automotive-AI | Нет явной инициализации | — |
|
||||
|
||||
**Рекомендация для Elmer:** взять последовательность obd2-mcp-server (самая полная) + задержки OBD2AI (400мс) + ATH0 вместо ATH1 (для чистых ответов без CAN-заголовков).
|
||||
|
||||
### Чтение ответов
|
||||
|
||||
| Проект | Метод | Плюсы | Минусы |
|
||||
|--------|-------|-------|--------|
|
||||
| OBD-Droid | Побайтово, 1мс sleep | Макс. контроль | Сложный код |
|
||||
| OBD2AI | kotlin-obd lib | Готовое решение | Зависимость от библиотеки |
|
||||
| Automotive-AI | `ser.readline()` | Простой код | Менее надёжно |
|
||||
|
||||
**Рекомендация для Elmer:** для Android — побайтовое чтение как у OBD-Droid (уже есть в TestService). Для Python-мока/сервера — `readline()` достаточно для тестов.
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
| Ошибка | OBD-Droid | OBD2AI | obd2-mcp |
|
||||
|--------|-----------|--------|----------|
|
||||
| SEARCHING | Статус CONNECTING | — | Пропустить, ждать |
|
||||
| NO DATA | Увеличить timeout | — | Вернуть пусто |
|
||||
| BUS ERROR | DISCONNECTED + retry | — | — |
|
||||
| UNABLE | DISCONNECTED + retry | — | — |
|
||||
| ERROR | WARMSTART (ATWS) | — | — |
|
||||
| RX ERROR | WARMSTART | 3 strikes → stop | — |
|
||||
|
||||
**Рекомендация для Elmer:** SEARCHING = ждать + увеличить таймаут. NO DATA = пропустить PID. BUS ERROR/UNABLE = одна попытка reconnect + retry. ERROR = WARMSTART.
|
||||
|
||||
### Тайминги
|
||||
|
||||
| Проект | Между командами | Инит | Таймаут ответа |
|
||||
|--------|-----------------|------|----------------|
|
||||
| OBD-Droid | Нет (стейт-машина) | Стейт-машина | 5000мс адаптивный |
|
||||
| OBD2AI | 400мс fix | 1000мс после всех | ? (внутри lib) |
|
||||
| Automotive-AI | Нет | Нет | 1000мс (readline) |
|
||||
| obd2-mcp | По ответам | По ответам | 2000-20000мс |
|
||||
| castlebbs | По семафору | — | 2000мс |
|
||||
|
||||
**Рекомендация для Elmer:** 400мс между командами (как OBD2AI) + адаптивный таймаут от 2000мс с возможностью увеличения (как OBD-Droid).
|
||||
|
||||
### BT подключение (Android)
|
||||
|
||||
| Проект | Метод | Fallback |
|
||||
|--------|-------|----------|
|
||||
| OBD-Droid | `createRfcommSocketToServiceRecord` secure + insecure | Reflection RFCOMM channel 1 |
|
||||
| OBD2AI | `createRfcommSocketToServiceRecord` | Нет |
|
||||
|
||||
**Рекомендация для Elmer:** взять fallback на reflection channel 1 из OBD-Droid — критично для дешёвых клонов.
|
||||
|
||||
---
|
||||
|
||||
## ИТОГ: Что реализовать в elmer-android
|
||||
|
||||
### Приоритет 1 (обязательно)
|
||||
- [ ] Побайтовое чтение с паузой 1мс (StreamHandler.java)
|
||||
- [ ] `>` = разделитель строк, НЕ спецсигнал
|
||||
- [ ] Fallback RFCOMM channel 1 (BluetoothCommService.java)
|
||||
- [ ] Фиксированные задержки 400мс между командами (OBD2AI)
|
||||
- [ ] Очистка буфера после инициализации
|
||||
|
||||
### Приоритет 2 (важно)
|
||||
- [ ] Обработка SEARCHING, NO DATA, BUS ERROR
|
||||
- [ ] 3-strike retry для live monitoring
|
||||
- [ ] Адаптивный таймаут (базовый 5000мс)
|
||||
|
||||
### Приоритет 3 (для production)
|
||||
- [ ] WARMSTART при ERROR/DATAERROR
|
||||
- [ ] Мульти-фрейм ISO-TP
|
||||
- [ ] DTC база (можно с obd2-mcp-server)
|
||||
- [ ] Экспоненциальный backoff для ретраев
|
||||
|
||||
---
|
||||
|
||||
## Часть 2: Традиционные OBD2 Android-проекты (БЕЗ LLM)
|
||||
|
||||
> Только ELM327 ↔ Android Bluetooth SPP. Именно они интересны для слоя коммуникации — оттестированы годами на тысячах машин.
|
||||
|
||||
### Топ-10 батл-тестед проектов
|
||||
|
||||
| # | Проект | URL | Язык | Создатель | Обновлён | ⭐ | DTC | Live | VIN | Примечания |
|
||||
|---|--------|-----|------|-----------|----------|-------|-----|------|-----|-----------|
|
||||
| 1 | **AndrOBD** | [fr3ts0n/AndrOBD](https://github.com/fr3ts0n/AndrOBD) | Java | Sepp Seidel | Апр 2025 | 1993 | ✅ | ✅ | ✅ | **Король.** 10+ лет, MQTT, графики, плагины, многоязычный |
|
||||
| 2 | **AndroidOBD** | [barnhill/AndroidOBD](https://github.com/barnhill/AndroidOBD) | Kotlin | Brian Barnhill | Ноя 2025 | 79 | ✅ | ✅ | ✅ | **Библиотека.** Хороший API для интеграции |
|
||||
| 3 | **Java OBD** | [Tomiwa-Ot/obd](https://github.com/Tomiwa-Ot/obd) | Java | Tomiwa O. | Март 2025 | 30 | ✅ | ✅ | ✓ | **Библиотека.** SPP + USB. Опубликована на JitPack. Async |
|
||||
| 4 | **ObdGraphs** | [tzebrowski/ObdGraphs](https://github.com/tzebrowski/ObdGraphs) | Kotlin | Tomek Żebrowski | Апр 2025 | 53 | ✅ | ✅ | ✅ | Графики в реальном времени. Проф. дизайн |
|
||||
| 5 | **CarScanApp** | [midnightyoff/CarScanApp](https://github.com/midnightyoff/CarScanApp) | Kotlin | midnightyoff | Окт 2025 | 2 | ✅ | ✅ | ✅ | **Свежий.** Compose, DTC read/clear, maintenance log |
|
||||
| 6 | **Elm327** | [takyonxxx/Elm327](https://github.com/takyonxxx/Elm327) | Java | Türkay Biliyor | Окт 2023 | 48 | ✅ | ✅ | ✅ | В Play Store. WiFi + BT. 22 форка |
|
||||
| 7 | **CarBusInterface** | [theksmith/CarBusInterface](https://github.com/theksmith/CarBusInterface) | Java | Kristoffer Smith | Янв 2016 | 291 | ✅ | ✅ | ✅ | Старый, боевой. 77 форков. Raw command interface |
|
||||
| 8 | **BT OBD-II Diag Tool** | [fussek/Bluetooth-OBD-II-Diagnostic-Tool](https://github.com/fussek/Bluetooth-OBD-II-Diagnostic-Tool) | Java | Sebastian Fussek | Сент 2022 | 21 | ✅ | ✅ | ✅ | Bachelor thesis. Хорошая документация + расчёты PID |
|
||||
| 9 | **obd-scanner-android** | [ETSoftwareStudio/obd-scanner-android](https://github.com/ETSoftwareStudio/obd-scanner-android) | Kotlin | ETSoftware | Апр 2025 | 2 | ✅ | ✅ | ✓ | Jetpack Compose + Hilt. Образец архитектуры |
|
||||
| 10 | **KWP Logger** | [bri3d/kwp-android-logger](https://github.com/bri3d/kwp-android-logger) | Java | Brian Ledbetter | Фев 2016 | 28 | ✅ | ✅ | ✓ | **KWP2000 для VW/Audi.** Специалист по протоколам |
|
||||
|
||||
### Что между ними общего (подтверждено всеми 14 проектами)
|
||||
|
||||
**Bluetooth SPP:**
|
||||
```java
|
||||
UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(SPP_UUID);
|
||||
socket.connect();
|
||||
```
|
||||
|
||||
**Инициализация ELM327 (единый стандарт):**
|
||||
```
|
||||
ATZ → сброс
|
||||
ATE0 → эхо выкл
|
||||
ATL0 → line feeds выкл
|
||||
ATS0 → пробелы выкл (или ATS1 — с пробелами)
|
||||
ATH0/1 → заголовки CAN
|
||||
ATSP0 → авто-протокол
|
||||
```
|
||||
|
||||
**Отправка команд (единый стандарт):**
|
||||
```java
|
||||
outputStream.write((cmd + "\r").getBytes());
|
||||
outputStream.flush();
|
||||
```
|
||||
|
||||
**Чтение ответов — два лагеря:**
|
||||
- **Лагерь 1 (AndrOBD, CarBusInterface):** побайтово, по 1мс
|
||||
- **Лагерь 2 (AndroidOBD, Elm327):** `BufferedReader.readLine()`
|
||||
|
||||
### Кого изучать в первую очередь для Elmer
|
||||
|
||||
| Для чего | Проект | Почему |
|
||||
|----------|--------|--------|
|
||||
| **BT ↔ ELM слой** | AndrOBD + CarBusInterface | 10 лет отладки, все краевые случаи |
|
||||
| **Kotlin-интеграция** | AndroidOBD + CarScanApp | Чистый API, современный код |
|
||||
| **PID/DTC парсинг** | AndrOBD | Все формулы, все режимы |
|
||||
| **KWP2000 (VW)** | KWP Logger | Если Phaeton на старом протоколе |
|
||||
|
||||
---
|
||||
|
||||
## Часть 3: СЫРОЙ КОД AndrOBD — золотой стандарт (1993 ⭐, 10 лет в продакшене)
|
||||
|
||||
### 3.1. Bluetooth SPP подключение (BtCommService.java)
|
||||
|
||||
```java
|
||||
final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
|
||||
// Стандартный socket
|
||||
if (secure) {
|
||||
tmp = device.createRfcommSocketToServiceRecord(SPP_UUID);
|
||||
} else {
|
||||
tmp = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
|
||||
}
|
||||
|
||||
// FALLBACK: reflection RFCOMM channel 1 (для китайских клонов!)
|
||||
catch (IOException e) {
|
||||
Class<?> clazz = mmSocket.getRemoteDevice().getClass();
|
||||
Method m = clazz.getMethod("createRfcommSocket", Integer.TYPE);
|
||||
sockFallback = (BluetoothSocket) m.invoke(mmSocket.getRemoteDevice(), 1);
|
||||
mmSocket = sockFallback;
|
||||
mmSocket.connect(); // пробуем снова
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2. 🚨 КРИТИЧЕСКИ: 500мс пауза после BT connect
|
||||
|
||||
```java
|
||||
// AndrOBD issue #233 — без этой паузы Android теряет данные!
|
||||
Thread.sleep(500); // CRITICAL: Fix for Android Bluetooth timing
|
||||
|
||||
// Только после этого запускаем worker thread
|
||||
mBtWorkerThread = new BtWorkerThread(socket, socketType);
|
||||
mBtWorkerThread.start();
|
||||
```
|
||||
|
||||
### 3.3. Инициализация ELM327 (точный порядок)
|
||||
|
||||
```java
|
||||
private void initialize() {
|
||||
setStatus(STAT.INITIALIZING);
|
||||
|
||||
// 1. Кастомные init-команды (если есть)
|
||||
cmdQueue.addAll(customInitCommands);
|
||||
|
||||
// 2. Установить протокол (AUTO или конкретный)
|
||||
pushCommand(CMD.SETPROT, preferredProtocol.ordinal()); // ATSP<n>
|
||||
|
||||
// 3. Инициализировать адаптивный тайминг
|
||||
mAdaptiveTiming.initialize(); // ATAT1
|
||||
|
||||
// 4. Ускорить протокол (убрать пробелы и переводы строк)
|
||||
pushCommand(CMD.SETSPACES, 0); // ATS0
|
||||
pushCommand(CMD.SETLINEFEED, 0); // ATL0
|
||||
|
||||
// 5. Выключить эхо
|
||||
pushCommand(CMD.ECHO, 0); // ATE0
|
||||
}
|
||||
// → ждёт "ELM" ответ → затем 0100 (запрос поддерживаемых PID)
|
||||
```
|
||||
|
||||
### 3.4. Адаптивный таймаут (ГЕНИАЛЬНО)
|
||||
|
||||
```java
|
||||
// Константы
|
||||
ELM_TIMEOUT_MAX = 1000 // мс — максимум
|
||||
ELM_TIMEOUT_DEFAULT = 200 // мс — стартовое значение
|
||||
ELM_TIMEOUT_RES = 4 // мс — шаг изменения
|
||||
ELM_TIMEOUT_MIN = 12 // мс — минимум
|
||||
|
||||
// На каждый NODATA → увеличить таймаут на 4мс
|
||||
// На каждый успех → уменьшить таймаут на 4мс (но не ниже learned_min)
|
||||
|
||||
void adapt(boolean increaseTimeout) {
|
||||
if (increaseTimeout) {
|
||||
if (elmMsgTimeout + 4 < 1000)
|
||||
setElmMsgTimeout(elmMsgTimeout + 4);
|
||||
} else {
|
||||
if (elmMsgTimeout - 4 >= learnedMin)
|
||||
setElmMsgTimeout(elmMsgTimeout - 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Каждое изменение отправляет: ATST<timeout/4>
|
||||
```
|
||||
|
||||
**Диапазон: 12мс → 1000мс, шаг 4мс. Старт: 200мс.**
|
||||
|
||||
### 3.5. Обработка ответов (handleTelegram)
|
||||
|
||||
```java
|
||||
// 1. Фильтр эха
|
||||
if (lastTxMsg.equalsIgnoreCase(bufferStr))
|
||||
return 0; // проигнорировать
|
||||
|
||||
// 2. Определить тип ответа
|
||||
switch (getResponseId(bufferStr)) {
|
||||
case PROMPT: // ">" — конец ответа
|
||||
case MODEL: // "ELM" → вызвать initialize()
|
||||
case SEARCHING: // идёт поиск
|
||||
case NODATA: // нет данных
|
||||
case ERROR: // ошибка
|
||||
case NOCONN: // "UNABLE"
|
||||
case BUSERROR: // ошибка шины
|
||||
// ... ещё 10+
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6. Обработка ошибок (production-grade)
|
||||
|
||||
```
|
||||
BUS ERROR / UNABLE / CAN ERROR:
|
||||
→ DISCONNECTED
|
||||
→ переставить в очередь последнюю команду
|
||||
→ сбросить протокол (ATSP)
|
||||
→ переинициализировать тайминг
|
||||
→ закрыть протокол (ATPC)
|
||||
|
||||
DATA ERROR / RX ERROR / BUFFER FULL:
|
||||
→ WARM START (ATWS) — мягкий перезапуск
|
||||
|
||||
NO DATA:
|
||||
→ увеличить адаптивный таймаут
|
||||
→ переустановить протокол
|
||||
|
||||
STOPPED:
|
||||
→ переставить последнюю команду в очередь
|
||||
```
|
||||
|
||||
### 3.7. Мульти-фрейм (ISO-TP для VIN и длинных ответов)
|
||||
|
||||
```java
|
||||
// Формат: "014" = 1 строка, 14 hex символов
|
||||
// "0:4902015756..." — первая строка с длиной
|
||||
// "1:5A5A314B5A41..." — продолжение
|
||||
|
||||
if (buffer[0] == '0' && buffer.length == 3) {
|
||||
charsExpected = Integer.valueOf(bufferStr, 16) * 2; // 0x14 = 20 байт
|
||||
lastRxMsg = "";
|
||||
responsePending = true;
|
||||
}
|
||||
|
||||
if (bufferStr.indexOf(':') >= 0) {
|
||||
lastRxMsg += bufferStr.substring(idx + 1);
|
||||
}
|
||||
|
||||
if (lastRxMsg.length() >= charsExpected) {
|
||||
result = handleDataMessage(lastRxMsg); // готово
|
||||
}
|
||||
```
|
||||
|
||||
### 3.8. Итоговый процесс подключения (хронология)
|
||||
|
||||
```
|
||||
1. BT socket connect()
|
||||
2. Thread.sleep(500) ← КРИТИЧЕСКИ! Без этого — потеря данных.
|
||||
3. BtWorkerThread.start()
|
||||
4. StreamHandler.run() — побайтовое чтение
|
||||
5. ATSP0 → ATAT1 → ATS0 → ATL0 → ATE0
|
||||
6. Ждём "ELM" → статус INITIALIZED
|
||||
7. 0100 → опрос поддерживаемых PID
|
||||
8. ECU detect → выбор ECU → готов к работе
|
||||
```
|
||||
@@ -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,89 @@
|
||||
# Заметки и находки
|
||||
|
||||
## 2026-05-25 — Исследование конкурентов
|
||||
|
||||
- Найдено 4 конкурента: DiagnostiX AI, OBDAI, Zero Touch, MUCAR MUAI
|
||||
- Русскоязычных нет, RuStore пустой
|
||||
- MUCAR уже использует DeepSeek → подтверждение правильности выбора LLM
|
||||
- Никто не делает открытый клиент (кроме Zero Touch, но там Flutter/Gemini)
|
||||
|
||||
## 2026-05-25 — Подтверждение гипотезы
|
||||
|
||||
DeepSeek дал полный подробный анализ по логам VCDS с тестового проезда.
|
||||
Лучше любого гугла. Гипотеза подтверждена.
|
||||
|
||||
## 2026-05-25 — Домен
|
||||
|
||||
- Куплен **obdai.ru** (обыгрывается: «обдай грязью» + OBD + AI)
|
||||
- Зона `.ai` дорогая ($60-80/год), не брали
|
||||
- Сервер: `https://obdai.ru/api/v1/raw-obd`
|
||||
- HTTPS: Let's Encrypt (обязательно для RuStore/Google Play)
|
||||
|
||||
## 2026-05-25 — AndrOBD (fr3ts0n)
|
||||
|
||||
- **Разработчик:** Erwin Scheuch-Heilig (fr3ts0n), Австрия, `erwin.scheuch-heilig@gmx.at`
|
||||
- **Репозиторий:** `github.com/fr3ts0n/AndrOBD`
|
||||
- **Лицензия:** GPLv2
|
||||
- **Год начала:** 2015
|
||||
- **Архитектура:** библиотека (`library/`) + приложение (`androbd/`) + плагины
|
||||
|
||||
### Ключевые файлы для форка
|
||||
|
||||
| Файл | Что делает |
|
||||
|---|---|
|
||||
| `library/.../ElmProt.java` | Протокол ELM327: AT-команды, парсинг ответов, DTC, PID, VIN, адаптивные таймауты |
|
||||
| `library/.../ObdProt.java` | OBD2-сервисы, декодирование PID, битовые маски, NRC-коды |
|
||||
| `androbd/.../BtCommService.java` | Bluetooth SPP (классический, не BLE) |
|
||||
| `androbd/.../BleCommService.java` | Bluetooth BLE (запасной вариант) |
|
||||
| `androbd/.../NetworkCommService.java` | WiFi OBD адаптеры |
|
||||
|
||||
### Что НЕ берём
|
||||
|
||||
- `androbd/` — весь UI: графики, дашборды, CSV-экспорт, плагины (MQTT, GPS, сенсоры)
|
||||
- `fastlane/` — метаданные для магазинов
|
||||
|
||||
### Стратегия форка
|
||||
|
||||
- **НЕ копипаст.** Используем Git submodule на AndrOBD
|
||||
- Подключаем только `library/` модуль
|
||||
- Наш код — отдельный `app/` модуль с тонким UI
|
||||
- Лицензия GPLv2 на весь клиент (совместимо с открытостью)
|
||||
- AndrOBD протестирован с 2015 года — экономим недели отладки краевых случаев
|
||||
|
||||
### Решение
|
||||
|
||||
Библиотека AndrOBD (GPLv2) покроет 100% ELM327-клоунов и их глюки.
|
||||
Наш тонкий UI + HTTP-forward — пишем сами.
|
||||
Перед RuStore — форкнуть обязательно. Для MVP (одна машина) — опционально, наш `elm.py` справится.
|
||||
|
||||
- Chrome Android: Web Bluetooth API — только BLE. ELM327 использует Bluetooth Classic SPP → НЕСОВМЕСТИМЫ
|
||||
- Web Serial API — не поддерживается на Android вообще
|
||||
- Termux + Python + pyserial — теоретически возможно, но Bluetooth-доступ в Termux сложен
|
||||
- Вывод: нативное Android-приложение обязательно
|
||||
|
||||
## 2026-05-25 — ELM327 v1.5 (PIC18F25K80)
|
||||
|
||||
- Китайский клон, не оригинальный чип PIC18F2480
|
||||
- Протокол неполный, возможны глюки на高速 CAN
|
||||
- Держать в уме при тестировании
|
||||
|
||||
## 2026-05-25 — RuStore
|
||||
|
||||
- Бесплатная регистрация разработчика
|
||||
- Модерация 1-3 дня
|
||||
- Проверяет: вредоносный код, подозрительные permissions
|
||||
- Не проверяет: скрытые закладки в легальном API
|
||||
- Наши permissions (BLUETOOTH + INTERNET) — минимальны, вопросов не вызовут
|
||||
|
||||
## 2026-05-25 — Sideload (установка APK напрямую)
|
||||
|
||||
- Проверок нет совсем
|
||||
- Android показывает список permissions перед установкой
|
||||
- Пользователь видит только BLUETOOTH + INTERNET → доверие
|
||||
|
||||
## 2026-05-25 — Целевая аудитория
|
||||
|
||||
- Технически любопытный автовладелец с ELM327
|
||||
- Не профессионал, но и не «глубинарий» (это шутка)
|
||||
- Хочет понять проблему, а не просто получить код
|
||||
- Требования к ответам: честная уверенность, пояснения, предупреждения
|
||||
@@ -0,0 +1,34 @@
|
||||
# Дорожная карта
|
||||
|
||||
## 🔴 Фаза 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: как собрать самому, как использовать с чужим сервером
|
||||
@@ -0,0 +1,55 @@
|
||||
# Сессия 2025-05-25 — первый выезд к Фаэтону
|
||||
|
||||
## Цель
|
||||
Подключить ELM327 к ноутбуку (WSL/Windows) и запустить диагностику.
|
||||
|
||||
## Результаты
|
||||
|
||||
### Что сделано
|
||||
- Установлен Python 3.12 в Windows (winget)
|
||||
- Установлены зависимости: pyserial, pyyaml, requests (Windows Python)
|
||||
- Установлен Flask в WSL (--break-system-packages)
|
||||
- Проект скопирован в C:\Users\super\elmer для доступа из Windows
|
||||
- ELM327 сопряжён с Windows (PIN 1234), созданы COM4/COM5 (Bluetooth SPP)
|
||||
|
||||
### Что НЕ работает
|
||||
- **Windows Bluetooth SPP** — COM4/COM5 открываются, но виснут на serial.Serial().
|
||||
Видимо, Windows не умеет нормально поднимать SPP-сессию к ELM327.
|
||||
COM3 — это Intel AMT SOL, а не ELM327.
|
||||
|
||||
### Что работает
|
||||
- **Android + ELM327** — OBD Amy подключается без проблем.
|
||||
ELM327 живой, питание от OBD2 есть (красный горит).
|
||||
- Сервер Flask на http://192.168.18.62:5005 (WSL)
|
||||
|
||||
### Исследование Android-клиента
|
||||
- `MainActivity.kt` — готова: ищет спаренный ELM327, запускает сервис
|
||||
- `ElmForwardService.kt` — готов на 80%:
|
||||
- Bluetooth SPP → ELM327: ✅
|
||||
- Инициализация AT-команд: ✅
|
||||
- Чтение сырых ответов: ✅
|
||||
- HTTP-forward на сервер: ✅
|
||||
- Отправка OBD-команд (VIN/DTC/PID): ❌ (нужно дописать)
|
||||
- `web/raw_endpoint.py` — буферизирует raw-данные, но не парсит
|
||||
|
||||
## Архитектура (план)
|
||||
```
|
||||
Телефон (Android) Ноутбук (WSL/Flask)
|
||||
┌──────────────┐ ┌──────────────────┐
|
||||
│ ElmForward │─HTTP──→ │ /api/v1/raw-obd │
|
||||
│ Service │ │ ↓ │
|
||||
│ ↓ BT SPP │ │ парсер + LLM │
|
||||
│ ELM327 │ │ ↓ │
|
||||
│ ↓ OBD2 │ │ диагноз │
|
||||
│ ЭБУ (Фаэтон) │ └──────────────────┘
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## Дальнейшие шаги
|
||||
1. **Дописать ElmForwardService.kt** — отправка OBD-команд после init:
|
||||
- VIN (0902)
|
||||
- DTC stored (03), pending (07)
|
||||
- PID'ы: 0105, 010C, 010D, 0111, 010B, 010F, 011F, 0104, 0106, 0107
|
||||
2. **Дописать raw_endpoint.py** — парсинг ответов + вызов LLM
|
||||
3. **Собрать APK** (нужен Android SDK)
|
||||
4. **Дописать web/templates/index.html** — форма ручного ввода для телефона
|
||||
@@ -0,0 +1,589 @@
|
||||
# Сессия 2026-05-26 — отладка Android-клиента и Mock ELM327
|
||||
|
||||
## Хронология
|
||||
|
||||
### 08:00 — Старт: пулл изменений с ночи
|
||||
Ночью на другой машине было сделано:
|
||||
- `android/` вынесен в отдельный репо `github.com/Repinoid/elmer-android`
|
||||
- Добавлен `tools/mock_elm327.py` — TCP-эмулятор ELM327 на порту 35000
|
||||
- `web/raw_endpoint.py` — стейт-машина, парсинг ответов, LLM-интеграция
|
||||
- Исправлены 3 бага в Android-клиенте (NetworkOnMainThread, URL сервера, фильтр \n)
|
||||
|
||||
### 08:20 — Попытка 1: телефон → mock → сервер
|
||||
Запущены mock (:35000) и Flask (:5005). Настроен проброс портов через Windows:
|
||||
```powershell
|
||||
netsh interface portproxy add v4tov4 listenport=5005 listenaddress=0.0.0.0 connectport=5005 connectaddress=192.168.18.62
|
||||
netsh advfirewall firewall add rule name="Elmer Flask" dir=in action=allow protocol=TCP localport=5005
|
||||
```
|
||||
|
||||
WiFi IP ноутбука: `10.47.183.102`.
|
||||
|
||||
Телефон подключился к mock (TCP: OK), но команды не шли.
|
||||
|
||||
### 08:30 — Ошибка 1: Server URL захардкожен на obdai.ru
|
||||
Глянув код клиента в `ElmForwardService.kt`, обнаружено:
|
||||
```kotlin
|
||||
// MainActivity.kt строка 66
|
||||
putExtra(EXTRA_SERVER_URL, "https://obdai.ru/api/v1/raw-obd")
|
||||
```
|
||||
|
||||
Телефон слал данные в интернет, а не на локальный Flask.
|
||||
|
||||
**Исправление:** в `MainActivity.kt` — авто-вывод URL сервера из IP устройства:
|
||||
```kotlin
|
||||
val deviceHost = debugHost.split(":")[0]
|
||||
val localServerUrl = "http://$deviceHost:5005/api/v1/raw-obd"
|
||||
```
|
||||
|
||||
### 08:35 — Ошибка 2: Gradle 9.5 слишком новый
|
||||
CI упал с `org.gradle.api.artifacts.SelfResolvingDependency`. Причина: AGP 8.2.0 несовместим с Gradle 9.x.
|
||||
|
||||
**Исправление:** в `build-apk.yml`:
|
||||
```yaml
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
gradle-version: "8.5"
|
||||
```
|
||||
|
||||
И создан `gradle/wrapper/gradle-wrapper.properties` с `gradle-8.5-bin.zip`.
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 2: Тест с реальным ELM327 (Фаэтон)
|
||||
|
||||
### Зелёная кнопка (TestService) — 100% работает
|
||||
- BT-подключение к ELM327 ✅
|
||||
- ATZ → ATEx → ATH1 ✅
|
||||
- VIN: получен (16-ричные данные) ✅
|
||||
- DTC stored/pending: получены ✅
|
||||
- PID'ы: RPM, ОЖ, скорость, дроссель и др. ✅
|
||||
- **Тайминги:** `Thread.sleep(250)` между командами — критически важно
|
||||
|
||||
### Оранжевая кнопка (ElmForwardService + сервер) — НЕ работает
|
||||
Проблема: стейт-машина сервера шлёт команды мгновенно, без пауз. ELM327 не успевает.
|
||||
- ATZ → OK
|
||||
- ATEx → OK
|
||||
- 0902 → SEARCHING...UNABLE TO CONNECT (ELM не может выполнить режим 09)
|
||||
- Сервер переходит к DTC → шлёт 03 → ELM отвечает `?` → бесконечный цикл `?`
|
||||
|
||||
**Корень проблемы:** тонкий клиент требует server-driven архитектуру (сервер даёт команду → клиент пишет в ELM → ELM отвечает → клиент шлёт ответ серверу → сервер даёт следующую). Но сервер не делает пауз, а ELM327 требует ~200мс между командами.
|
||||
|
||||
### Почему Deadlock при первом запуске оранжевой кнопки
|
||||
1. Клиент: BT-коннект ✅, входит в loop()
|
||||
2. ELM327: ждёт команду (не шлёт приветствие без запроса)
|
||||
3. Клиент: `read()` блокируется — нет данных
|
||||
4. Сервер: не получает "READY" — не шлёт ATZ
|
||||
5. **DEADLOCK**
|
||||
|
||||
**Исправлено:** `fwd("READY")` сразу после коннекта — кикстарт сервера.
|
||||
|
||||
---
|
||||
|
||||
## Вывод: архитектура телефон↔сервер в реальных условиях
|
||||
|
||||
### Проблема
|
||||
В движении связи с сервером нет. Архитектура «сервер рулит каждой командой» нежизнеспособна.
|
||||
|
||||
### Решение (обсуждено)
|
||||
**Две фазы работы:**
|
||||
|
||||
**Фаза 1 — ОФЛАЙН (в машине):**
|
||||
- Клиент получает со старта **скрипт** от сервера
|
||||
- Скрипт: последовательность команд + промпты водителю
|
||||
- Клиент сам гоняет протокол (как зелёная кнопка)
|
||||
- Данные пишутся локально (SQLite) с таймстемпами
|
||||
- Водитель видит промпты: «Разгон 0-100», «Кикдаун», «Холостой ход 30с»
|
||||
- Кнопка Старт / Стоп
|
||||
|
||||
**Фаза 2 — ОНЛАЙН (дома):**
|
||||
- Клиент заливает всю сессию одним POST на сервер
|
||||
- Сервер парсит, анализирует, LLM → диагноз
|
||||
- Возможно — выдаёт следующий скрипт для нового теста
|
||||
|
||||
### Формат скрипта (пример):
|
||||
```json
|
||||
{
|
||||
"name": "Тест турбины",
|
||||
"steps": [
|
||||
{"type": "obd", "cmd": "ATZ"},
|
||||
{"type": "obd", "cmd": "010C", "label": "RPM"},
|
||||
{"type": "prompt", "text": "Разгон 0-100, кикдаун"},
|
||||
{"type": "loop", "pid": "010C", "duration": 30, "rate_ms": 200},
|
||||
{"type": "obd", "cmd": "03"},
|
||||
{"type": "upload"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Кто что делает
|
||||
| Компонент | Файл | Статус | Что добавить |
|
||||
|-----------|------|--------|-------------|
|
||||
| Тестовый клиент | `TestService.kt` | ✅ гоняет протокол | Сохранение в БД, скрипты, промпты |
|
||||
| Транспортный клиент | `ElmForwardService.kt` | ⚠️ требует стабильной связи | Возможно удалить |
|
||||
| Стейт-машина | `raw_endpoint.py` | ⚠️ нет пауз | Переделать под батчевую обработку |
|
||||
| Mock ELM327 | `mock_elm327.py` | ✅ эмулятор | Добавить задержки для реализма |
|
||||
| Сервер приёма | `web/app.py` | ✅ | POST-эндпоинт для заливки сессии |
|
||||
|
||||
### Договорённости по процессу
|
||||
- **НИКОГДА не кодить без прямой команды**
|
||||
- Сначала обсуждать → потом делать
|
||||
- Коммитить часто, с понятными сообщениями
|
||||
- Документировать все ошибки и решения
|
||||
|
||||
### 08:40 — Ошибка 3: Коммиты не в ту ветку
|
||||
Изначально все изменения ушли в `master`, но рабочая ветка — `relay-only`.
|
||||
|
||||
**Исправление:** переключился на `relay-only`, применил изменения туда, master откатил через `git reset --hard && git push --force`.
|
||||
|
||||
### 08:45 — Ветка relay-only: добавлена отладка
|
||||
В `ElmForwardService.kt` добавлено:
|
||||
- `say("🌐 Server: $serverUrl")` — показ URL при подключении
|
||||
- `say("← $raw")` — каждое сырое сообщение от устройства
|
||||
- `say("→ $cmd")` — каждая команда сервера
|
||||
- Обработка ошибок: `⚠️ Server unreachable`, `⚠️ Bad JSON`
|
||||
|
||||
### 09:00 — Ошибка 4: Cleartext HTTP заблокирован
|
||||
Телефон показал `⚠️ Server down: cleartext...`. Android 9+ блокирует HTTP (не-HTTPS) по умолчанию.
|
||||
|
||||
**Исправление:** в `AndroidManifest.xml`:
|
||||
```xml
|
||||
android:usesCleartextTraffic="true"
|
||||
```
|
||||
|
||||
### 09:05 — Ошибка 5: APK не скачивается с сервера
|
||||
Телефон открыл `http://10.47.183.102:5005`, страница загрузилась, но APK — 404.
|
||||
Flask отдаёт статику из `/static/`, а ссылка была `/app-debug.apk`.
|
||||
|
||||
**Исправление:** ссылка изменена на `/static/app-debug.apk`.
|
||||
|
||||
### 09:10 — Ошибка 6: Приложение не устанавливается поверх
|
||||
Google Play Protect проверил APK, но «приложение не установлено». Причина: каждый CI-билд генерирует новый debug-keystore → сигнатуры не совпадают → Android блокирует установку поверх.
|
||||
|
||||
**Исправление:** сгенерирован фиксированный `debug.keystore` (пароль `android`, alias `androiddebugkey`) и закоммичен в репо. В CI добавлен шаг:
|
||||
```yaml
|
||||
- name: Setup debug keystore
|
||||
run: cp debug.keystore ~/.android/debug.keystore
|
||||
```
|
||||
|
||||
### 09:15 — Тестовая версия: вместо сервера — локальный протокол
|
||||
Пользователь потребовал тестовую версию без сервера. Создан `TestService.kt`:
|
||||
- TCP-подключение к mock
|
||||
- Самостоятельная отправка AT-команд (ATZ, ATE0, ATL0, ATSP0, ATH1)
|
||||
- Чтение VIN (0902)
|
||||
- Чтение DTC stored (03), pending (07)
|
||||
- Чтение 10 PID'ов (0105..0107)
|
||||
- Весь вывод на экран в реальном времени
|
||||
|
||||
Интерфейс: зелёная кнопка «🧪 ТЕСТ», версия `v0.2.0-test`, URL по умолчанию `10.47.183.102:35000`.
|
||||
|
||||
### 09:25 — Ошибка 7: Мусор в командах
|
||||
Mock получил `01070 ATZ` вместо `ATZ`. Причина: клиент слал `\r`, а mock использовал `readline()` (ждёт `\n`). В буфере накопился мусор.
|
||||
|
||||
**Исправление в клиенте:** `cmd + "\r\n"` вместо `cmd + "\r"`.
|
||||
|
||||
**Исправление в mock:** полная переделка чтения — побайтовый разбор:
|
||||
```python
|
||||
# Было: self.rfile.readline()
|
||||
# Стало: читаем по 1 байту, \r и \n — разделители
|
||||
ch = self.rfile.read(1)
|
||||
if ch in (b'\n', b'\r'):
|
||||
# обработать накопленный буфер
|
||||
else:
|
||||
buf += ch
|
||||
```
|
||||
|
||||
### 09:30 — 100% успешный тест
|
||||
Mock лог (чистый!):
|
||||
```
|
||||
🔌 Подключение
|
||||
📥 ATZ → ELM327 v1.5
|
||||
📥 ATE0 → OK
|
||||
📥 ATL0 → OK
|
||||
📥 ATSP0 → OK
|
||||
📥 ATH1 → OK
|
||||
📥 0902 → VIN: WVWZZZ1KZAW123456
|
||||
📥 03 → DTC: P0301, P0303
|
||||
📥 07 → DTC: none
|
||||
📥 0105..07 → 10 PID'ов
|
||||
🔌 Отключение
|
||||
```
|
||||
|
||||
18 команд — 18 ответов. Ноль мусора.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура (текущая)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ ТЕСТОВЫЙ РЕЖИМ (работает на 100%) │
|
||||
│ │
|
||||
│ Телефон (Android) Ноутбук (WSL) │
|
||||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ TestService │──TCP──→ │ mock_elm327.py │ │
|
||||
│ │ │←──TCP── │ :35000 │ │
|
||||
│ │ ATZ→ATEx→ │ │ │ │
|
||||
│ │ 0902→03/07 │ │ Фейковые данные: │ │
|
||||
│ │ PID'ы │ │ VIN, DTC, PID │ │
|
||||
│ └──────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ РАБОЧИЙ РЕЖИМ (клиент готов, сервер частично) │
|
||||
│ │
|
||||
│ Телефон Ноутбук │
|
||||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ElmForwardSvc │─HTTP→ │ Flask :5005 │ │
|
||||
│ │ (транспорт) │←─JSON─ │ raw_endpoint.py │ │
|
||||
│ │ │ │ ↓ стейт-машина │ │
|
||||
│ │ BT/TCP → │ │ ↓ парсер │ │
|
||||
│ │ ELM327/mock │ │ ↓ LLM (DeepSeek)│ │
|
||||
│ └──────────────┘ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Все ошибки и решения
|
||||
|
||||
| # | Ошибка | Причина | Решение |
|
||||
|---|--------|---------|---------|
|
||||
| 1 | Телефон не слал команды | server_url = obdai.ru (интернет) | Авто-вывод `http://host:5005` |
|
||||
| 2 | CI build fail | Gradle 9.5 ≠ AGP 8.2 | Закрепить Gradle 8.5 |
|
||||
| 3 | Коммиты в master | Не переключил ветку | Перенос в relay-only, откат master |
|
||||
| 4 | Server down: cleartext | Android блокирует HTTP | `usesCleartextTraffic="true"` |
|
||||
| 5 | APK 404 на сервере | Flask static path | `/static/app-debug.apk` |
|
||||
| 6 | Не устанавливается поверх | Разные debug-ключи | Фиксированный keystore в репо |
|
||||
| 7 | Мусор `01070 ATZ` | `\r` vs `\r\n` + readline() | Побайтовое чтение в mock + `\r\n` |
|
||||
|
||||
---
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
### Android (ветка relay-only)
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `TestService.kt` | **ТЕСТОВЫЙ** — сам гонит протокол, без сервера |
|
||||
| `ElmForwardService.kt` | **РАБОЧИЙ** — транспорт BT/TCP ↔ HTTP |
|
||||
| `MainActivity.kt` | UI: кнопки ТЕСТ и Диагностировать |
|
||||
| `debug.keystore` | Фиксированный ключ подписи (пароль `android`) |
|
||||
| `build-apk.yml` | CI: Gradle 8.5, сборка debug APK |
|
||||
|
||||
### Сервер (ветка master)
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `tools/mock_elm327.py` | Эмулятор ELM327 на TCP :35000 |
|
||||
| `web/app.py` | Flask сервер :5005 |
|
||||
| `web/raw_endpoint.py` | Стейт-машина: парсинг, сессии, LLM |
|
||||
| `web/templates/index.html` | Страница загрузки APK |
|
||||
|
||||
---
|
||||
|
||||
## Команды для запуска
|
||||
|
||||
```bash
|
||||
# Ноутбук — терминал 1: mock
|
||||
python tools/mock_elm327.py
|
||||
|
||||
# Ноутбук — терминал 2: сервер
|
||||
python web/app.py
|
||||
|
||||
# Телефон: открыть http://10.47.183.102:5005 → скачать APK → кнопка ТЕСТ
|
||||
```
|
||||
|
||||
## Версии APK
|
||||
| Версия | Статус | Ключ |
|
||||
|--------|--------|------|
|
||||
| 0.1.0 | Устарела | Случайный |
|
||||
| 0.2.0-test | На телефоне | Случайный |
|
||||
| 0.5.0-test | В CI | Фиксированный |
|
||||
|
||||
---
|
||||
|
||||
## Что дальше (рабочая версия)
|
||||
1. Дописать `raw_endpoint.py` — надёжный парсинг + сохранение в БД
|
||||
2. Настроить `DEEPSEEK_API_KEY` для LLM
|
||||
3. Протестировать цепочку: телефон → сервер → LLM → диагноз
|
||||
4. Подключить к реальному ELM327 в машине (Bluetooth вместо mock)
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 3: Толстый клиент (fat-client)
|
||||
|
||||
### Архитектура (обсуждено)
|
||||
Две фазы:
|
||||
- **Офлайн**: клиент скачивает JSON-скрипт, сам гонит ELM327, пишет в SQLite
|
||||
- **Онлайн**: заливает батч на сервер → LLM → диагноз на экране
|
||||
|
||||
Скрипт содержит `steps[]`: OBD-команды, промпты водителю, `wait_for_user`.
|
||||
|
||||
### Ветки
|
||||
- `elmer-android`: `fat-client` (от `relay-only`)
|
||||
- `elmer`: `fat-client` (от `master`)
|
||||
|
||||
### Новые файлы
|
||||
|
||||
| Файл | Что |
|
||||
|------|-----|
|
||||
| `ScriptRunnerService.kt` | Движок скриптов: скачать JSON, гонять ELM, SQLite, залить батч |
|
||||
| `SessionDb.kt` | SQLite: `sessions` и `responses` |
|
||||
| `web/script_endpoint.py` | `GET /api/v1/script`, `POST /api/v1/session/upload` |
|
||||
|
||||
### Ошибки и решения (fat-client)
|
||||
|
||||
| # | Ошибка | Причина | Решение |
|
||||
|---|--------|---------|---------|
|
||||
| 8 | `optString("cmd", null)` → строка `"null"` | Android JSONObject возвращает `"null"` строку | `optString("cmd", "")` + `.isNotEmpty()` |
|
||||
| 9 | Два `companion object` в классе | Kotlin запрещает | Склеить в один |
|
||||
| 10 | CI не собирал `fat-client` | Workflow: `branches: [master, relay-only]` | Добавить `fat-client` |
|
||||
| 11 | «Плохой JSON скрипта» | `scriptUrl` = `http://host:5005` (HTML) вместо `/api/v1/script` | `EXTRA_SCRIPT_URL` с полным путём |
|
||||
| 12 | `?` → бесконечный цикл на реальном ELM | Сервер без пауз, ELM не успевает | ATH0 вместо ATH1 + fallback-парсинг raw |
|
||||
| 13 | CAN-заголовки `83 F1 1A 41 05...` | ATH1 включает заголовки, декодер не понимает | ATH0 — заголовки выкл |
|
||||
|
||||
### Mock ELM327 — реалистичные задержки
|
||||
|
||||
| Команда | Задержка | Разброс |
|
||||
|---------|----------|---------|
|
||||
| ATZ | ~2.5 сек | ±30% |
|
||||
| ATSP0 | ~1.8 сек + SEARCHING... | ±30% |
|
||||
| PID 01XX | ~0.2 сек | ±30% |
|
||||
| DTC 03/07 | ~0.3 сек | ±30% |
|
||||
| VIN 0902 | ~0.5 сек | ±30% |
|
||||
|
||||
Убрано авто-приветствие при подключении (реальный ELM по BT не шлёт).
|
||||
|
||||
### Результаты тестов
|
||||
|
||||
**Mock (TCP 10.47.183.102:35000):** 18/18 команд успешно, VIN/DTC/PID декодированы ✅
|
||||
|
||||
**Реальный ELM327 (BT, Фаэтон):**
|
||||
- BT-подключение ✅
|
||||
- ATZ → ATEx ✅
|
||||
- ATH1 → CAN-заголовки ломали декодер → исправлено на ATH0
|
||||
- Данные получены, но требуют доработки парсера под формат Фаэтона
|
||||
|
||||
### Текущее состояние (ветки fat-client)
|
||||
|
||||
| Компонент | Статус |
|
||||
|-----------|--------|
|
||||
| `ScriptRunnerService` | ✅ работает с mock |
|
||||
| `ScriptRunnerService` + реальный ELM | ⚠️ требуется ATH0 (исправлено, ждёт APK) |
|
||||
| `SessionDb` (SQLite) | ✅ |
|
||||
| `GET /api/v1/script` | ✅ |
|
||||
| `POST /api/v1/session/upload` | ✅ парсинг + fallback raw |
|
||||
| LLM (DeepSeek) | ⚠️ нет API-ключа |
|
||||
| APK-сборка CI | ✅ `fat-client` триггерит |
|
||||
|
||||
---
|
||||
|
||||
## ЧАСТЬ 4: Тест с реальным ELM327 на Фаэтоне
|
||||
|
||||
### Mock-прогоны (3 успешных)
|
||||
Все 3 теста с TCP-моком прошли идеально: 18/18 команд, VIN/DTC/PID декодированы.
|
||||
Третий прогон с ATH0 подтвердил совместимость.
|
||||
|
||||
### Реальный ELM327 (BT, Фаэтон, зажигание)
|
||||
Загрузка батча: 18 ответов.
|
||||
|
||||
| Команда | Результат |
|
||||
|---------|-----------|
|
||||
| ATZ | ✅ ELM327 v1.5 |
|
||||
| ATE0 | ⚠️ ATE0OK (эхо смешано, норма) |
|
||||
| ATL0, ATSP0, ATH0 | ✅ OK |
|
||||
| 0902 (VIN) | ❌ SEARCHING... → STOPPED. Режим 09 не поддерживается ЭБУ Фаэтона |
|
||||
| 03 (DTC stored) | ❌ STOPPED |
|
||||
| 07 (DTC pending) | ⚠️ P0047 — **реальная pending-ошибка!** |
|
||||
| PID'ы (0105-0107) | ❌ 7F 10 12 — ЭБУ отвечает «не поддерживается» |
|
||||
|
||||
### Выводы
|
||||
1. **Система работает end-to-end:** телефон → ELM → скрипт → SQLite → батч → сервер → диагноз
|
||||
2. ЭБУ Фаэтона не поддерживает режим 09 (VIN) и некоторые PID'ы
|
||||
3. P0047 — реальный код pending-ошибки, полученный с машины
|
||||
4. Серверный парсер требует доработки: отличать 7F-ответы от ошибок парсинга
|
||||
5. DTC-парсинг иногда ловит ложные срабатывания на PID-ответах (режим 47 в сырых данных)
|
||||
|
||||
### Что дальше
|
||||
- Настроить `DEEPSEEK_API_KEY` и прогнать LLM на реальных данных
|
||||
- Исправить парсер: фильтровать 7F, не путать mode 47 в PID-ответах с DTC pending
|
||||
- В скрипте: пропускать 0902 если ЭБУ не поддерживает (VIN не критичен)
|
||||
|
||||
---
|
||||
|
||||
## 21:30 — План: трёхфазный флоу с жалобами водителя
|
||||
|
||||
### Концепция
|
||||
|
||||
Текущая проблема: водитель подключает ELM → молча сканирует → загружает → ждёт LLM.
|
||||
Нет контекста от водителя (он лучше всех знает машину). Нет понимания холодный/горячий двигатель.
|
||||
|
||||
**Философия:** мы не конкурируем с Torque/OBD Fusion за расшифровку кодов.
|
||||
У водителя уже есть эти программы. Наша задача — собрать контекст (ошибки + параметры + **жалобы водителя**)
|
||||
и дать AI-диагноз, который учитывает человеческий фактор.
|
||||
|
||||
### Трёхфазный флоу (Android)
|
||||
|
||||
```
|
||||
ФАЗА 1 — БЫСТРЫЙ ОПРОС (без интернета, ~5 сек)
|
||||
Инициализация: ATZ→ATE0→ATL0→ATSP0→ATH0
|
||||
Чтение: 03 (stored DTC) → 07 (pending DTC) → 0105 (ОЖ) → 010C (RPM)
|
||||
|
||||
↓ Показываем МИНИМАЛЬНУЮ карточку:
|
||||
|
||||
┌─────────────────────────────┐
|
||||
│ 🔌 ELM327 подключён │
|
||||
│ │
|
||||
│ 🌡 Двигатель: ХОЛОДНЫЙ 32°C │
|
||||
│ ⚠️ Ошибок: 2 │
|
||||
│ 🔄 Обороты: 0 (заглушен) │
|
||||
│ │
|
||||
│ 📝 Опишите жалобы: │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ (свободный ввод) │ │
|
||||
│ └─────────────────────────┘ │
|
||||
│ │
|
||||
│ [ПРОПУСТИТЬ] [ДАЛЕЕ ➤] │
|
||||
└─────────────────────────────┘
|
||||
|
||||
ФАЗА 2 — ОСНОВНОЙ СКАН (завёл двигатель, ~15-20 сек)
|
||||
0902 (VIN) → 0104 (нагрузка) → 0106 (STFT) → 0107 (LTFT)
|
||||
→ 010B (MAP) → 010D (скорость) → 010F (IAT) → 0111 (дроссель) → 011F (время работы)
|
||||
|
||||
ФАЗА 3 — ОТПРАВКА + LLM (нужен интернет)
|
||||
POST /api/v1/session/upload
|
||||
{
|
||||
"responses": [...],
|
||||
"driver_complaint": "троит на холодную, стук при разгоне",
|
||||
"engine_state": {
|
||||
"temp_c": 32,
|
||||
"status": "cold", // "cold" (<40°C) | "warming" (40-80°C) | "hot" (>80°C)
|
||||
"rpm": 0,
|
||||
"running": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Что меняется на сервере (elmer)
|
||||
|
||||
1. **`build_default_script()`** — новый порядок шагов:
|
||||
- Фаза 1: ATZ→ATE0→ATL0→ATSP0→ATH0→03→07→0105→010C
|
||||
- Спец-шаг: `show_summary` (триггер для Android показать карточку + спросить жалобы)
|
||||
- Спец-шаг: `driver_complaint` (текстовый ввод, можно пропустить)
|
||||
- Спец-шаг: `prompt_engine` (завести двигатель)
|
||||
- Фаза 2: 0902→0104→0106→0107→010B→010D→010F→0111→011F
|
||||
|
||||
2. **`upload_session()`** — принимает `driver_complaint` и `engine_state` из тела запроса
|
||||
|
||||
3. **`_build_diagnosis_prompt()`** — включает жалобы водителя и состояние двигателя:
|
||||
```
|
||||
## Данные диагностики
|
||||
**VIN:** ...
|
||||
**Состояние двигателя:** ХОЛОДНЫЙ (32°C), заглушен
|
||||
**Сохранённые ошибки:** P0301, P0303
|
||||
**Жалобы водителя:** "троит на холодную, стук при разгоне"
|
||||
...
|
||||
```
|
||||
|
||||
4. **SYSTEM_PROMPT** — дополнен инструкцией учитывать жалобы водителя:
|
||||
- Жалобы водителя — ПРИОРИТЕТНАЯ информация
|
||||
- Сопоставлять жалобы с кодами ошибок и параметрами
|
||||
- Если жалоба не объясняется кодами — предлагать что проверить дополнительно
|
||||
|
||||
### Что меняется на Android (elmer-android)
|
||||
|
||||
1. **`ScriptRunnerService.kt`:**
|
||||
- Новый тип шага: `show_summary` — после фазы 1 декодирует результаты, отправляет broadcast `ACTION_SHOW_SUMMARY` с данными:
|
||||
```json
|
||||
{
|
||||
"temp_c": 32, "temp_status": "cold",
|
||||
"dtc_stored_count": 2, "dtc_pending_count": 1,
|
||||
"rpm": 0, "running": false
|
||||
}
|
||||
```
|
||||
- Новый тип шага: `driver_complaint` — отправляет broadcast `ACTION_PROMPT_COMPLAINT`, ждёт текстовый ввод
|
||||
- При upload добавляет поля `driver_complaint` и `engine_state` в JSON
|
||||
|
||||
2. **`MainActivity.kt`:**
|
||||
- Приёмник `ACTION_SHOW_SUMMARY` — показывает карточку с ОЖ, ошибками, RPM
|
||||
- Приёмник `ACTION_PROMPT_COMPLAINT` — показывает поле ввода + кнопки ПРОПУСТИТЬ / ДАЛЕЕ
|
||||
- Отправляет результат обратно в сервис через broadcast
|
||||
|
||||
3. **`activity_main.xml`:**
|
||||
- Новый overlay `layout_complaint`: карточка + EditText + кнопки
|
||||
|
||||
### Почему клиент НЕ хранит базу DTC
|
||||
|
||||
- Полная база: ~10 000+ кодов, ~100+ КБ в JSON → клиент становится «слоном»
|
||||
- У водителя уже есть Torque/OBD Fusion для расшифровки
|
||||
- Наша задача — не расшифровка, а AI-диагноз с контекстом
|
||||
- Клиент показывает только **количество** ошибок и **состояние** двигателя
|
||||
- LLM на сервере сам знает что значит P0301 и P0047
|
||||
|
||||
### Приоритет жалоб водителя для LLM
|
||||
|
||||
Правило: **жалоба водителя > коды ошибок > параметры**
|
||||
|
||||
Пример:
|
||||
- Коды: P0301, P0303 (пропуски зажигания)
|
||||
- Параметры: STFT=0%, O₂=0.32V (бедная смесь)
|
||||
- Жалоба: «троит ТОЛЬКО на холодную, после прогрева всё нормально»
|
||||
|
||||
Без жалобы LLM скажет: «топливная система, форсунки, MAP/MAF»
|
||||
С жалобой LLM скажет: «свечи/катушки — на холодную зазор меньше, искра слабее;
|
||||
после прогрева металл расширяется и контакт восстанавливается»
|
||||
|
||||
---
|
||||
|
||||
## 22:00 — Оценка качества LLM и стратегия
|
||||
|
||||
### Тестирование gpt-oss-120b
|
||||
|
||||
**Данные:** P0301, P0303, ОЖ=50°C, RPM=1726, Нагрузка=25.1%, STFT=0%, Дроссель=50.2%
|
||||
|
||||
**Результат:** модель проигнорировала русские метки («Нагрузка», «STFT») и выдумала:
|
||||
- PID 0104 → «O₂-датчик 1, 0.32V — бедная смесь» (на самом деле: Calculated Load 25.1%)
|
||||
- PID 0106 → «O₂-датчик 2, 0.64V» (на самом деле: STFT 0%)
|
||||
- На этой галлюцинации построила весь диагноз про вакуумные утечки
|
||||
|
||||
**Вывод:** gpt-oss-120b уверенно и красиво врёт. Не знает OBD2 PID-маппинг.
|
||||
|
||||
### Тестирование qwen3-6-27b-fp8
|
||||
|
||||
**Результат:**
|
||||
- Тоже ошиблась в PID-маппинге (0104=Throttle, 0106=IAT, 0111=MAF)
|
||||
- НО проявила метакогницию: заметила расхождение меток с данными, перепроверяла
|
||||
- Потратила все токены на CoT-размышления (вытекли в ответ)
|
||||
|
||||
**Вывод:** умнее чем gpt-oss, но формат ответа сломан (CoT-утечка).
|
||||
|
||||
### Общий вывод по LLM
|
||||
|
||||
| Модель | PID-точность | Метакогниция | Формат | Вердикт |
|
||||
|--------|-------------|-------------|--------|---------|
|
||||
| gpt-oss-120b | ❌ 0% | ❌ уверенно врёт | ✅ | Опасен |
|
||||
| qwen3-6-27b-fp8 | ❌ 0% | ✅ сомневается | ❌ CoT-утечка | Перспективен |
|
||||
|
||||
**Корень проблемы:** ни одна LLM не обучалась на OBD2 PID-номерах.
|
||||
Решение: не подавать PID-номера в промпт, только декодированные названия + PID-справочник в SYSTEM_PROMPT.
|
||||
|
||||
### Стратегия качества LLM (на будущее)
|
||||
|
||||
1. **Cross-Validation:** один запрос → две разные модели → сравнить
|
||||
- Совпали → ✅ надёжно
|
||||
- Разошлись → ⚠️ показать обе версии
|
||||
- Одна призналась «не знаю» → отфильтровать
|
||||
|
||||
2. **Промпт-самозащита:** «Если НЕ уверен в расшифровке — честно напиши "не знаю". НИКОГДА не выдумывай.»
|
||||
|
||||
3. **Выбор модели — юзером:** `config.yaml` позволяет сменить модель/ключ/URL без кода.
|
||||
Пользователь сам платит за качество: от бесплатного gpt-oss до Claude Opus.
|
||||
|
||||
4. **Архитектурно готово:** `diagnose.py` уже параметризован — любой OpenAI-совместимый API.
|
||||
|
||||
### Приоритеты
|
||||
|
||||
| Приоритет | Задача | Почему |
|
||||
|-----------|--------|--------|
|
||||
| 🔴 **P0** | Физическое взаимодействие с ELM327 | Самое сложное, без этого ничего не работает |
|
||||
| 🟡 **P1** | ScriptRunner v2 (защитное чтение) | Надёжность на уровне протокола |
|
||||
| 🟢 **P2** | Трёхфазный флоу с жалобами | UX, можно потом |
|
||||
| 🔵 **P3** | Cross-validation LLM | Серверная логика, не блокирует клиент |
|
||||
@@ -0,0 +1,103 @@
|
||||
# Сессия 2026-05-27 — домен obdai.ru, стратегия деплоя, исследование ELM/BT
|
||||
|
||||
## Домен
|
||||
|
||||
- `obdai.ru` — куплен, DNS-зона в ngcloud (PowerDNS)
|
||||
- NS: `ns5.rcloud.ru` / `ns4.rcloud.ru` (у регистратора сменили с reg.ru)
|
||||
- A-записи: `@`, `www`, `ai`, `test` → `5.172.178.213`
|
||||
- Ждём propagation (TTL старой зоны 86400)
|
||||
|
||||
## Решение: obdai.ru пока НЕ деплоим
|
||||
|
||||
### Причина
|
||||
|
||||
Главная задача сейчас — **стабильный обмен ELM327↔Android**. Пока это звено не отлажено на 100%, деплой на ВМ и домен не дают никакой ценности:
|
||||
|
||||
- В машине телефон на одном WiFi с ноутбуком — локальный IP быстрее и надёжнее
|
||||
- Клиент уже принимает URL сервера как параметр — сменить на `obdai.ru` = одна строка
|
||||
- DNS ещё не пророс
|
||||
|
||||
### Порядок приоритетов
|
||||
|
||||
1. **Стабильный обмен ELM↔Android** — критический путь, уже выявлено 4+ багов
|
||||
2. **Fat-client протокол** (скрипт + батч) — обкатать на реальном ELM
|
||||
3. **Gunicorn + nginx на ВМ** — только после пунктов 1-2
|
||||
4. **Переключение клиента на `obdai.ru`** — последний шаг, одна строка
|
||||
|
||||
### Состояние звена ELM↔Android
|
||||
|
||||
| Баг | Статус |
|
||||
|-----|--------|
|
||||
| Сервер шлёт команды без пауз → ELM не успевает | ⚠️ не решён |
|
||||
| `\r` vs `\r\n` → мусор в буфере | ✅ исправлено |
|
||||
| Deadlock при старте (ELM не шлёт приветствие) | ✅ исправлено (fwd READY) |
|
||||
| Android блокирует HTTP (cleartext) | ✅ исправлено (usesCleartextTraffic) |
|
||||
|
||||
### Архитектура деплоя (в будущем)
|
||||
|
||||
```
|
||||
obdai.ru (95.172.178.213)
|
||||
├── nginx (reverse proxy + SSL)
|
||||
│ ├── / → лендинг (статический HTML)
|
||||
│ ├── /static/app.apk → APK для скачивания
|
||||
│ └── /api/v1/* → gunicorn (Flask)
|
||||
│ ├── /script → выдача скрипта диагностики
|
||||
│ └── /session/upload → приём батча + LLM
|
||||
└── gunicorn (web/app.py)
|
||||
├── script_endpoint (fat-client)
|
||||
└── возможно raw_endpoint (опционально)
|
||||
```
|
||||
|
||||
### Что НЕ переносится на ВМ
|
||||
|
||||
- Десктоп-диагностика (`POST /api/diagnose`, `ELM327.elm.py`) — требует Bluetooth на ноутбуке в машине
|
||||
- Локальный Flask для тестов — остаётся на ноутбуке разработчика
|
||||
|
||||
---
|
||||
|
||||
## 11:00-15:00 — Исследование ELM327/BT: 19 проектов
|
||||
|
||||
### Найдено и изучено
|
||||
|
||||
| Тип | Кол-во | Лучший |
|
||||
|-----|--------|--------|
|
||||
| ELM→LLM проекты | 5 | OBD-Droid (Wal33D) |
|
||||
| Традиционные OBD2 Android | 14 | **AndrOBD** (1993⭐) |
|
||||
|
||||
### Ключевой результат: AndrOBD — золотой стандарт
|
||||
|
||||
Изучен **сырой исходный код**. Выписаны все паттерны в `doc/elm-reference.md` (1100+ строк).
|
||||
|
||||
**Критические находки, которых не было в нашем коде:**
|
||||
1. `Thread.sleep(500)` после BT connect — без этого Android теряет данные
|
||||
2. Fallback RFCOMM channel 1 через reflection (для китайских клонов)
|
||||
3. Адаптивный таймаут: 200мс ± 4мс, диапазон 12-1000мс
|
||||
4. ATST меняется на лету
|
||||
5. Побайтовое чтение с паузой 1мс (не readLine!)
|
||||
6. `>` — НЕ спецсигнал, а разделитель строк как CR/LF
|
||||
7. Мульти-фрейм ISO-TP с префиксом длины
|
||||
8. Полная обработка ошибок: SEARCHING, NO DATA, BUS ERROR, UNABLE, DATA ERROR
|
||||
9. Инициализация: ATSP→ATAT→ATS0→ATL0→ATE0 (без ATZ при каждом подключении)
|
||||
|
||||
### Созданные документы
|
||||
|
||||
| Файл | Содержание |
|
||||
|------|-----------|
|
||||
| `doc/elm-reference.md` | 1100+ строк: паттерны 19 проектов + сырой код AndrOBD |
|
||||
| `doc/competitors.md` | 7 конкурентов, русских 0 |
|
||||
| `elmer/elm_proto.py` | ❌ Переусложнён (написан до изучения AndrOBD) |
|
||||
| `tools/mock_elm327_v2.py` | ❌ Требует доработки под реальные паттерны |
|
||||
|
||||
### План: переписать ELM-слой по AndrOBD
|
||||
|
||||
**Шаг 1 — СЕЙЧАС:** Переписать `elmer/elm_proto.py` — эталонный Python-референс.
|
||||
**Шаг 2 — ЗАВТРА:** Перенести в `ScriptRunnerService.kt` (Kotlin).
|
||||
**Шаг 3:** Тест на Mock ELM327 v2.
|
||||
**Шаг 4:** Выезд к Фаэтону.
|
||||
|
||||
### Что НЕ делаем сейчас (приоритеты)
|
||||
|
||||
| P0 | Стабильный ELM↔Android | 🔴 сейчас |
|
||||
| P1 | ScriptRunner v2 + Mock v2 | 🟡 завтра |
|
||||
| P2 | Трёхфазный флоу, жалобы водителя | 🟢 потом |
|
||||
| P3 | LLM-промпты, cross-val, DTC-база | 🔵 потом |
|
||||
@@ -0,0 +1,142 @@
|
||||
# Сессия 2026-05-28 — AndrOBD стейт-машина, ELM-протокол, отладка
|
||||
|
||||
## Хронология
|
||||
|
||||
### 08:00 — Тест v0.12.0-dev на моке
|
||||
Скрипт отработал (ELM — зелёный), но upload «Сервер недоступен».
|
||||
Flask работал локально, проброс портов был настроен.
|
||||
|
||||
**Диагностика:** `curl -X POST` на `/api/v1/session/upload` зависал на 60+ секунд —
|
||||
LLM `api.aillm.ru` долго отвечал, Flask ждал, OkHttp на телефоне таймаутил через 30с.
|
||||
|
||||
**Исправление:** OkHttp `readTimeout` увеличен с 30с до 120с → v0.13.0-dev.
|
||||
|
||||
### 09:00 — Стейт-машина AndrOBD
|
||||
Пользователь потребовал НЕ изобретать своё, а скопировать 1:1 отлаженный код AndrOBD.
|
||||
|
||||
Создана ветка `androbd-proto`.
|
||||
|
||||
**Проблема 1: команды слались подряд, ответы перемешивались**
|
||||
|
||||
Было (`init()` старая):
|
||||
```python
|
||||
sendRaw("ATSP0"); sleep(200) # шлём, НЕ читаем ответ
|
||||
sendRaw("ATAT1"); sleep(200) # шлём, НЕ читаем ответ
|
||||
# → ответы на ATSP0 и ATAT1 в буфере → OBD-команды читают чужие ответы
|
||||
```
|
||||
|
||||
Стало (`init()` новая):
|
||||
```python
|
||||
_exec("ATSP0", timeout=10000) # шлём, ЖДЁМ ответ, читаем
|
||||
_exec("ATAT1", timeout=5000)
|
||||
# → каждая команда ждёт свой ответ → буфер чист
|
||||
```
|
||||
|
||||
**Проблема 2: ATST отправлялся напрямую, ответ не читался**
|
||||
|
||||
`_process()` вызывал `_send_atst()` которая писала `ATST` в порт и НЕ читала ответ.
|
||||
Следующая команда читала `OK` от ATST вместо своего ответа.
|
||||
|
||||
Исправление: `_send_atst()` теперь читает и отбрасывает ответ на ATST.
|
||||
|
||||
**Проблема 3: `_read()` возвращал частичный ответ при таймауте**
|
||||
|
||||
При таймауте `_read()` возвращал что успел прочитать — например, первую строку DTC.
|
||||
Вторая строка попадала в следующую команду.
|
||||
|
||||
Исправление: `_read()` теперь требует `>` перед возвратом. Без `>` — TimeoutError.
|
||||
|
||||
**Проблема 4: таймаут 200мс мал для мока**
|
||||
|
||||
Мок ждёт 300-500мс перед ответом на OBD-команды. AndrOBD использует 200мс с адаптивным
|
||||
увеличением через ATST, но мок не поддерживает аппаратный ATST.
|
||||
|
||||
Исправление: увеличен базовый таймаут до 500мс, 10 ретраев вместо 5.
|
||||
|
||||
**Проблема 5: BUS ERROR recovery блокирует всё**
|
||||
|
||||
При BUS ERROR стейт-машина отправляет ATPC + ATSP0 для восстановления.
|
||||
Мок отвечает на ATSP0 через 1.8с. `_try_read()` ждал только 1с.
|
||||
Ответ на ATSP0 оставался в буфере и попадал в следующую команду.
|
||||
|
||||
Исправление: `_try_read` использует таймаут 5с для recovery-команд.
|
||||
|
||||
---
|
||||
|
||||
## Итоговая архитектура AndrOBD
|
||||
|
||||
### Стейт-машина (AndrOBD ElmProt.java)
|
||||
|
||||
```
|
||||
UNDEFINED → INITIALIZING → READY → BUSY → READY
|
||||
↓ ERROR ↓ BUS ERROR
|
||||
RECOVERING DISCONNECTED
|
||||
```
|
||||
|
||||
### Классификация ответов (Rsp.identify)
|
||||
|
||||
| Ответ | Тип | Реакция |
|
||||
|-------|-----|---------|
|
||||
| `>` | PROMPT | Конец ответа (разделитель) |
|
||||
| `OK` | OK | Успех, уменьшить таймаут |
|
||||
| `SEARCHING...` | SEARCHING | Нормально при ините |
|
||||
| `NODATA` | NODATA | Увеличить таймаут, ATST |
|
||||
| `UNABLE`, `BUS BUSY`, `CAN ERROR`, etc. | BUS ERROR | DISCONNECTED, ATPC, ATSP0 |
|
||||
| `ERROR` | ERROR | WARM START (ATWS) |
|
||||
| `DATA ERROR`, `BUFFER FULL`, `RX ERROR` | DATA ERROR | WARM START |
|
||||
| Всё остальное | DATA | Успех, уменьшить таймаут |
|
||||
|
||||
### Чтение (StreamHandler.java)
|
||||
|
||||
- Побайтовое, пауза 1мс
|
||||
- CR (13) = разделитель строк
|
||||
- `>` (62) = конец ответа
|
||||
- LF (10) и пробел (32) = игнорируются
|
||||
- **Без `>` ответ не возвращается** (TimeoutError)
|
||||
|
||||
### Адаптивный таймаут (AdaptiveTiming.java)
|
||||
|
||||
- Старт: 500мс (для мока; реальный ELM → 200мс)
|
||||
- Шаг: 20мс
|
||||
- Диапазон: 50-2000мс
|
||||
- ATST отправляется через `_queue_atst()` → ответ читается корректно
|
||||
|
||||
---
|
||||
|
||||
## Результаты тестирования
|
||||
|
||||
Python-тест (`tools/test_androbd.py`) против Mock ELM327 v2:
|
||||
|
||||
| Тест | Результат |
|
||||
|------|-----------|
|
||||
| VIN (0902) → 490201... | ✅ |
|
||||
| DTC (03) → 430113... + 430133... | ✅ |
|
||||
| RPM (010C) → 410C1AF8 | ✅ |
|
||||
| ОЖ (0105) → 41055A | ✅ |
|
||||
| Ответы не перемешаны | ✅ 2/3 прогонов |
|
||||
|
||||
1/3 прогонов упал из-за случайного BUS BUSY в моке (3%) — стейт-машина корректно
|
||||
восстановилась, но DTC-ответ был пустой (ожидаемое поведение).
|
||||
|
||||
---
|
||||
|
||||
## Версии APK
|
||||
|
||||
| Версия | Что |
|
||||
|--------|-----|
|
||||
| v0.11.0-prod | Старая, ответы перемешаны |
|
||||
| v0.12.0-dev | Стейт-машина, но OkHttp 30с → «сервер недоступен» |
|
||||
| **v0.13.0-dev** | Стейт-машина + OkHttp 120с |
|
||||
|
||||
---
|
||||
|
||||
## Что дальше (P0 → P3)
|
||||
|
||||
| P0 | ELM-протокол со стейт-машиной | ✅ сделано (Python + Kotlin) |
|
||||
| P0 | Тест на моке | ✅ 2/3 зелёные |
|
||||
| P0 | OkHttp timeout | ✅ 30→120с |
|
||||
| P1 | GPS-модуль | запланировано |
|
||||
| P1 | Трёхфазный флоу с жалобами | запланировано |
|
||||
| P2 | DTC-база на сервере | потом |
|
||||
| P2 | LLM cross-validation | потом |
|
||||
| P3 | Web-панель | потом |
|
||||
@@ -0,0 +1,53 @@
|
||||
# 2026-06-06 — Полная сессия (полевой тест + 9 багов + фиксы)
|
||||
|
||||
## Текущий статус
|
||||
- **Версия:** v0.47.0-dev
|
||||
- **APK:** https://obdai.ru/elmer.apk
|
||||
- **Сервер:** https://obdai.ru (5.172.178.213, nginx+gunicorn)
|
||||
- **Android repo:** github.com/Repinoid/elmer-android
|
||||
- **Server repo:** gitea.services.ngcloud.ru/Nail/elmer
|
||||
|
||||
## Хронология коммитов (Android)
|
||||
|
||||
| Коммит | Описание |
|
||||
|--------|----------|
|
||||
| `6a33048` | fix: checkDevice() забыл connectAndInit() |
|
||||
| `739afa9` | fix: connect() идемпотентный + run() без двойного connect |
|
||||
| `94b890f` | bump v0.43.0-dev |
|
||||
| `71618c6` | fix: статус-строка — append вместо overwrite |
|
||||
| `d49f6d6` | fix: все appendStatus с \n, таймер на своей строке |
|
||||
| `3ef3532` | fix: scriptRegistered сброс в onDestroy |
|
||||
| `dead66c` | fix: init без ретраев, v1.5-совместимость |
|
||||
|
||||
## Все 9 багов
|
||||
|
||||
1. checkDevice без connectAndInit — AT-команды без BT-сокета
|
||||
2. connect не идемпотентный — guard socket.isConnected
|
||||
3. run двойной connect
|
||||
4. Статус-строка overwrite — всё на appendStatus(\n...)
|
||||
5. Таймер съедал заголовок — \n вместо пробела
|
||||
6. scriptRegistered не сбрасывался после поворота
|
||||
7. init() 70 секунд на ATAT1 — write+tryRead(2s) вместо exec
|
||||
8. checkDevice слал v2-команды на v1.5 — проверка isV2
|
||||
9. recover/updateAtst/handle длинные таймауты — 2000-3000мс
|
||||
|
||||
## ELM327: версии и команды (Wikipedia)
|
||||
|
||||
- **v1.5 НЕ СУЩЕСТВУЕТ** — клон v1.0/v1.4 с фейковой версией
|
||||
- ATAT1 (adaptive timing): с v1.2
|
||||
- AT@1/AT@2 (device ID): с v1.3
|
||||
- ATST (set timeout): с v1.2
|
||||
- Базовые (ATI, ATDP, ATRV, ATSP, ATE0, ATL0, ATS0, ATWS): с v1.0
|
||||
|
||||
## Правила для Copilot
|
||||
|
||||
1. Коммит после каждой правки: git add -A && git commit -m "..." && git push
|
||||
2. При деплое bump версии в android/app/build.gradle.kts
|
||||
3. Формат: fix:/feat:/refactor:/bump:/docs:
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Разбить MainActivity.kt (~470 -> <=200 строк)
|
||||
- [ ] Разбить ElmChecker.kt (~270 -> <=200 строк)
|
||||
- [ ] Подробные комментарии перед каждой функцией
|
||||
- [ ] Полевой тест v0.47.0-dev на машине
|
||||
@@ -0,0 +1,113 @@
|
||||
# Резюме проекта elmAI — 14 июня 2026
|
||||
|
||||
## Текущая версия: v1.18.0-dev
|
||||
|
||||
## Архитектура
|
||||
|
||||
**Сервер (Python/Flask):** `gitea.services.ngcloud.ru/Nail/elmer`, ветка `bugfix-2026-06-07`
|
||||
- Хост: `obdai.ru` (5.172.178.213), доступ по SSH: `ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213`
|
||||
- Репо на сервере: `/opt/elmer`, сервис `elmer` (gunicorn), nginx прокси
|
||||
|
||||
**Android (Kotlin):** `github.com/Repinoid/elmer-android`, ветка `bugfix-2026-06-07`
|
||||
- APK собирается на сервере: `export ANDROID_SDK_ROOT=$HOME/android-sdk && cd /opt/elmer/android && ./gradlew clean assembleDebug`
|
||||
- APK на сайте: `web/static/app-debug.apk` → `https://obdai.ru/elmer.apk`
|
||||
|
||||
## Что работает ✅
|
||||
|
||||
1. **Статическая диагностика** — одиночные PID (0104-011F), DTC, VIN — ИДЕАЛЬНО
|
||||
2. **Speed-test** — замер задержек RPM/MAF/STFT при клике на 🔵 ELM (если нет профиля)
|
||||
3. **Серверный тестовый скрипт** — `GET /api/v1/script?mode=test`
|
||||
4. **Авто-подбор таймингов** — `POST /api/v1/test/next` — сервер получает результаты, увеличивает wait_ms если ошибок >20%
|
||||
|
||||
## Что НЕ работает ❌
|
||||
|
||||
1. **Динамический тест (СТАРТ/СТОП)** — ELM327 v1.5 замолкает после первых 1-2 команд. Статика работает, динамика нет. Причина не найдена.
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
### Сервер
|
||||
| Файл | Что |
|
||||
|------|-----|
|
||||
| `api/routes.py` | `?mode=test`, `POST /api/v1/test/next`, авто-подбор |
|
||||
| `api/scripts.py` | `build_test_script(wait_ms, pids, repeat)` |
|
||||
| `api/db.py` | `device_profiles` таблица с `response_time_ms` |
|
||||
| `api/ping.py` | ping/ping-llm эндпоинты |
|
||||
|
||||
### Android
|
||||
| Файл | Что |
|
||||
|------|-----|
|
||||
| `elm/ElmProtocol.kt` | Стейт-машина AndrOBD. `MAX_RETRIES=3`, `state=ERROR` убран |
|
||||
| `elm/ElmChecker.kt` | checkDevice, checkEcu, speed-test, quickCheck |
|
||||
| `script/DynamicCollector.kt` | Оригинальный DynamicCollector (не используется сейчас) |
|
||||
| `script/ScriptEngine.kt` | Выполнение скриптов с сервера, поддержка `wait` |
|
||||
| `server/ServerClient.kt` | `downloadTestScript()`, `postTestNext()` |
|
||||
| `ui/MainActivity.kt` | `startDynamicRecording()` — авто-подбор с сервера |
|
||||
|
||||
## Логика авто-подбора (v1.17+)
|
||||
|
||||
```
|
||||
Пользователь: СТАРТ
|
||||
↓
|
||||
Статика 9 PID (как обычно)
|
||||
↓
|
||||
Цикл 1: GET /api/v1/script?mode=test → wait=2000ms, 2 PID (010C,0106), 8 повторов
|
||||
↓ выполняет
|
||||
↓ POST /api/v1/test/next {run:0, results:[...]}
|
||||
↓ ответ: {done:false, message:"50% ошибок — увеличиваю до 2500ms"}
|
||||
↓
|
||||
Цикл 2: wait=2500ms → выполняет → POST → ответ
|
||||
↓
|
||||
... пока done:true или run≥5
|
||||
```
|
||||
|
||||
## Деплой
|
||||
|
||||
### Только сервер (без APK):
|
||||
```bash
|
||||
cd /home/naeel/elmer
|
||||
git add -A && git commit -m "..." && git push origin bugfix-2026-06-07
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 'cd /opt/elmer && git pull origin bugfix-2026-06-07 && sudo systemctl restart elmer'
|
||||
```
|
||||
|
||||
### APK + сервер:
|
||||
```bash
|
||||
# Бамп версии в android/app/build.gradle.kts и web/templates/index.html
|
||||
# Затем:
|
||||
cd /home/naeel/elmer/android
|
||||
git add -A && git commit -m "..." && git push origin bugfix-2026-06-07
|
||||
|
||||
cd /home/naeel/elmer
|
||||
git add -A && git commit -m "bump vX.Y.Z-dev" && git push origin bugfix-2026-06-07
|
||||
|
||||
cd /home/naeel/elmer
|
||||
tar czf /tmp/android-src.tar.gz --exclude='.git' --exclude='build' --exclude='.gradle' android/
|
||||
scp -i ~/.ssh/naeel_vm_id_ed25519 /tmp/android-src.tar.gz naeel@5.172.178.213:/tmp/
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 '\
|
||||
cd /opt/elmer && git pull origin bugfix-2026-06-07 && \
|
||||
export ANDROID_SDK_ROOT=$HOME/android-sdk && export ANDROID_HOME=$ANDROID_SDK_ROOT && \
|
||||
rm -rf android && tar xzf /tmp/android-src.tar.gz -C /opt/elmer/ && \
|
||||
cd android && ./gradlew clean assembleDebug && \
|
||||
cp app/build/outputs/apk/debug/app-debug.apk /opt/elmer/web/static/'
|
||||
```
|
||||
|
||||
## Нерешённая проблема
|
||||
|
||||
ELM327 v1.5 замолкает при последовательных OBD-командах. Статика (одиночные) — ок. Динамика (подряд) — пустые ответы. Уже пробовали:
|
||||
- Разные паузы (250ms → 4000ms) — не помогает
|
||||
- ATWS перед динамикой — делает хуже
|
||||
- Убирали/возвращали drainInput() — v1.9 без drain хуже
|
||||
- Retry в exec() — оригинальный AndrOBD код не помогает
|
||||
- Разное количество PID — не помогает
|
||||
- DynamicCollector → серверный скрипт — не помогает
|
||||
|
||||
Текущий авто-подбор должен найти рабочий интервал, но если даже 4000ms не помогает — проблема глубже таймингов.
|
||||
|
||||
## Конфигурация авто-подбора (api/routes.py)
|
||||
- Старт: 2000ms
|
||||
- Шаг: +500ms
|
||||
- Макс: 5 циклов
|
||||
- PIDs: 010C (RPM), 0106 (STFT)
|
||||
- Повторов: 6
|
||||
|
||||
## Профиль ELM
|
||||
Таблица `device_profiles` в `elmer.db`. MAC: `AA:BB:CC:11:22:33`. Был удалён старый мусорный профиль.
|
||||
@@ -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. Нужно 1200–1500 мс.
|
||||
- После 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 — парсинг ответов ломается
|
||||
@@ -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)`.
|
||||
@@ -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 ⛔
|
||||
@@ -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. Оптимальный размер цикла
|
||||
При паузе 2–3 сек: **4 PID** — хороший баланс. 6 PID уже рискованно на нагруженной шине. 2 PID — безопасно, но медленно.
|
||||
|
||||
## 6. Влияние оборотов
|
||||
Да, напрямую. При высоких оборотах CAN-шина загружена больше (ECU шлёт больше фреймов), буфер клона заполняется быстрее. На холостых — залипает позже.
|
||||
|
||||
## 7. Замена адаптера
|
||||
Приоритет: **OBDLink EX/MX+** (оригинальный чип STN2120, нет этой проблемы) → оригинальный ELM327 v1.4b/v2.1 (реже). Клоны v2.1 — лотерея, та же болезнь. Китайские v1.5 — для продакшена непригодны.
|
||||
|
||||
## 8. Стратегия для продакшена
|
||||
Адаптироваться под клонов, но с явными ограничениями:
|
||||
- Медленный режим (4 PID, пауза 3 сек) как default
|
||||
- Детект залипания + авто-`ATZ`
|
||||
- В UI показывать «адаптер нестабилен, рекомендуем OBDLink/оригинал»
|
||||
Reference in New Issue
Block a user