Files
elmer/doc/history/2026-06-10.md
T

9.2 KiB
Raw Blame History

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:

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
);

Миграция для старых БД:

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/
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:

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 Этот файл