68 lines
4.7 KiB
Markdown
68 lines
4.7 KiB
Markdown
# 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`.
|