Files
elmer/doc/legacy/opus/opus-recheck-analysis-2026-06-28.md
T

4.7 KiB
Raw Blame History

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.