docs: история 2026-06-10 — speed-test, адаптивный интервал, threading lock
This commit is contained in:
+21
-5
@@ -1,7 +1,7 @@
|
|||||||
# elmAI — Changelog / Полное описание проекта
|
# elmAI — Changelog / Полное описание проекта
|
||||||
|
|
||||||
> Файл для нового агента: прочитай — и ты в курсе всего.
|
> Файл для нового агента: прочитай — и ты в курсе всего.
|
||||||
> Актуально: v0.77.0-dev, 7 июня 2026
|
> Актуально: v0.93.0-dev, 10 июня 2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -132,6 +132,14 @@
|
|||||||
|
|
||||||
### История версий (сервер)
|
### История версий (сервер)
|
||||||
|
|
||||||
|
#### v0.93.0-dev (10 июня 2026)
|
||||||
|
- **Speed-test ELM327:** при первом подключении нового ELM — замер скорости ответа на 3 PID (RPM, MAF, STFT) × 3 раза каждый
|
||||||
|
- **Адаптивный интервал:** динамический тест использует `max(250, avg_response × 3 × 1.5)` вместо жёстких 250ms
|
||||||
|
- **Профиль устройства:** колонка `response_time_ms` в `device_profiles`, API `PUT /api/v1/elm/profile/<mac>`
|
||||||
|
- **Fix:** `threading.Lock()` в `Database` — 0 ошибок при 20 конкурентных записях (было 8/20)
|
||||||
|
- **UI:** прогресс speed-теста показывается пользователю
|
||||||
|
- Деплой v0.93.0-dev на obdai.ru
|
||||||
|
|
||||||
#### v0.48.0 (7 июня 2026)
|
#### v0.48.0 (7 июня 2026)
|
||||||
- **Пробинг ELM327:** трехуровневый каскад (L0/L1/L2)
|
- **Пробинг ELM327:** трехуровневый каскад (L0/L1/L2)
|
||||||
- **Рефакторинг `obd/`:** разделение на независимые сервисы
|
- **Рефакторинг `obd/`:** разделение на независимые сервисы
|
||||||
@@ -268,7 +276,7 @@ scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/app
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Динамический тест — START/STOP (v0.92+, 10.06.2026)
|
## 9. Динамический тест — START/STOP (v0.93+, 10.06.2026)
|
||||||
|
|
||||||
### Цель
|
### Цель
|
||||||
Выявить **потерю мощности, подсос воздуха, забитый фильтр, проблемы смеси** — ловля STFT/LTFT на сбросе газа.
|
Выявить **потерю мощности, подсос воздуха, забитый фильтр, проблемы смеси** — ловля STFT/LTFT на сбросе газа.
|
||||||
@@ -281,9 +289,17 @@ scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/app
|
|||||||
5. **TPS** (0111)
|
5. **TPS** (0111)
|
||||||
|
|
||||||
→ После тестов #43-#45 выяснилось: ELM327 v1.5 не успевает 5 PID за 250ms (данные склеиваются).
|
→ После тестов #43-#45 выяснилось: ELM327 v1.5 не успевает 5 PID за 250ms (данные склеиваются).
|
||||||
→ **Решение (текущее): 3 PID × 250ms + остальные статикой ДО старта.**
|
→ После тестов #47-#48 (v0.93.0-dev): даже 3 PID × 250ms — 94% ошибок, ELM перестаёт отвечать после ~15 сэмплов.
|
||||||
|
→ **Решение (текущее, v0.93.0): Speed-test при первом подключении ELM + адаптивный интервал.**
|
||||||
|
|
||||||
### Текущая стратегия (v0.92+)
|
### Текущая стратегия (v0.93+)
|
||||||
|
|
||||||
|
#### Этап 0 — Speed-test (только при первом подключении нового ELM)
|
||||||
|
- После инициализации ELM: замерить время ответа на 010C, 0110, 0106 — каждый 3 раза
|
||||||
|
- Показать пользователю: `"⏱ Тест скорости: RPM 82ms MAF 91ms STFT 82ms"`
|
||||||
|
- Сохранить `response_time_ms` в профиль устройства (по BT MAC)
|
||||||
|
- Интервал = `max(250, avg_response × 3 × 1.5)`
|
||||||
|
- При повторных запусках — использовать сохранённое значение
|
||||||
|
|
||||||
#### Этап 1 — Статика (перед СТАРТ)
|
#### Этап 1 — Статика (перед СТАРТ)
|
||||||
- Снять все доступные PID по одному разу
|
- Снять все доступные PID по одному разу
|
||||||
@@ -317,6 +333,6 @@ scp app/build/outputs/apk/debug/app-debug.apk obdai.ru:/opt/elmer/web/static/app
|
|||||||
|
|
||||||
### Планы
|
### Планы
|
||||||
- Скорость — потом через GPS (не через OBD)
|
- Скорость — потом через GPS (не через OBD)
|
||||||
- Адаптивный интервал если ELM быстрее (v2.x)
|
- ~~Адаптивный интервал если ELM быстрее (v2.x)~~ ✅ Сделано в v0.93.0
|
||||||
- Логирование в историю каждого теста
|
- Логирование в историю каждого теста
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class Database:
|
|||||||
elm_desc TEXT,
|
elm_desc TEXT,
|
||||||
protocol TEXT,
|
protocol TEXT,
|
||||||
voltage TEXT,
|
voltage TEXT,
|
||||||
|
response_time_ms INTEGER DEFAULT 250,
|
||||||
supported TEXT,
|
supported TEXT,
|
||||||
unsupported TEXT,
|
unsupported TEXT,
|
||||||
errors TEXT,
|
errors TEXT,
|
||||||
@@ -129,6 +130,7 @@ class Database:
|
|||||||
|
|
||||||
# Миграции: добавляем колонки, которых нет в старых БД
|
# Миграции: добавляем колонки, которых нет в старых БД
|
||||||
migrations = [
|
migrations = [
|
||||||
|
"ALTER TABLE device_profiles ADD COLUMN response_time_ms INTEGER DEFAULT 250",
|
||||||
"ALTER TABLE sessions ADD COLUMN device_uuid TEXT",
|
"ALTER TABLE sessions ADD COLUMN device_uuid TEXT",
|
||||||
"ALTER TABLE sessions ADD COLUMN phone_lang TEXT",
|
"ALTER TABLE sessions ADD COLUMN phone_lang TEXT",
|
||||||
"ALTER TABLE sessions ADD COLUMN phone_tz TEXT",
|
"ALTER TABLE sessions ADD COLUMN phone_tz TEXT",
|
||||||
@@ -286,25 +288,27 @@ class Database:
|
|||||||
def save_device_profile(self, mac: str, profile: dict):
|
def save_device_profile(self, mac: str, profile: dict):
|
||||||
"""Сохраняет или обновляет профиль устройства.
|
"""Сохраняет или обновляет профиль устройства.
|
||||||
|
|
||||||
profile — результат obd.probe.probe().
|
profile — результат obd.probe.probe() + response_time_ms.
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
self.conn.execute("""
|
self.conn.execute("""
|
||||||
INSERT INTO device_profiles
|
INSERT INTO device_profiles
|
||||||
(mac, level, elm_version, elm_desc, protocol, voltage,
|
(mac, level, elm_version, elm_desc, protocol, voltage,
|
||||||
supported, unsupported, errors, first_seen, last_seen)
|
response_time_ms, supported, unsupported, errors,
|
||||||
VALUES (?,?,?,?,?,?, ?,?,?, ?,?)
|
first_seen, last_seen)
|
||||||
|
VALUES (?,?,?,?,?,?, ?,?,?,?, ?,?)
|
||||||
ON CONFLICT(mac) DO UPDATE SET
|
ON CONFLICT(mac) DO UPDATE SET
|
||||||
level = excluded.level,
|
level = excluded.level,
|
||||||
elm_version = excluded.elm_version,
|
elm_version = excluded.elm_version,
|
||||||
elm_desc = excluded.elm_desc,
|
elm_desc = excluded.elm_desc,
|
||||||
protocol = excluded.protocol,
|
protocol = excluded.protocol,
|
||||||
voltage = excluded.voltage,
|
voltage = excluded.voltage,
|
||||||
supported = excluded.supported,
|
response_time_ms = excluded.response_time_ms,
|
||||||
unsupported = excluded.unsupported,
|
supported = excluded.supported,
|
||||||
errors = excluded.errors,
|
unsupported = excluded.unsupported,
|
||||||
last_seen = excluded.last_seen
|
errors = excluded.errors,
|
||||||
|
last_seen = excluded.last_seen
|
||||||
""", (
|
""", (
|
||||||
mac,
|
mac,
|
||||||
profile.get("level", -1),
|
profile.get("level", -1),
|
||||||
@@ -312,6 +316,7 @@ class Database:
|
|||||||
profile.get("elm_desc"),
|
profile.get("elm_desc"),
|
||||||
profile.get("protocol"),
|
profile.get("protocol"),
|
||||||
profile.get("voltage"),
|
profile.get("voltage"),
|
||||||
|
profile.get("response_time_ms", 250),
|
||||||
json.dumps(profile.get("supported", []), ensure_ascii=False),
|
json.dumps(profile.get("supported", []), ensure_ascii=False),
|
||||||
json.dumps(profile.get("unsupported", []), ensure_ascii=False),
|
json.dumps(profile.get("unsupported", []), ensure_ascii=False),
|
||||||
json.dumps(profile.get("errors", []), ensure_ascii=False),
|
json.dumps(profile.get("errors", []), ensure_ascii=False),
|
||||||
|
|||||||
@@ -316,6 +316,19 @@ def register(app):
|
|||||||
return jsonify(p)
|
return jsonify(p)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/elm/profile/<mac>", methods=["PUT"])
|
||||||
|
def update_elm_profile(mac: str):
|
||||||
|
"""Обновляет поля профиля (response_time_ms, elm_desc и т.д.)."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
with Database() as db:
|
||||||
|
p = db.get_device_profile(mac)
|
||||||
|
if p is None:
|
||||||
|
return jsonify({"error": "not found"}), 404
|
||||||
|
p.update(data)
|
||||||
|
_save_profile(mac, p)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
def _save_profile(mac: str, profile: dict):
|
def _save_profile(mac: str, profile: dict):
|
||||||
"""Сохраняет профиль в БД (best-effort)."""
|
"""Сохраняет профиль в БД (best-effort)."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 2026-06-10 — Speed-test ELM327, адаптивный интервал, деплой v0.93.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 |
|
||||||
Reference in New Issue
Block a user