From 25c8eb20b9afa08b703cf94849842a3980f3f21c Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sun, 28 Jun 2026 11:46:00 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B0=D1=82=D0=BE=D0=BC=D0=B0=D1=80?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20dequeue,=20=D0=B4=D0=B2=D0=BE=D0=B9=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20ATI,=20MAX=5FRETRIES=20620ms,=20=D1=80=D0=B5?= =?UTF-8?q?=D1=82=D0=B5=D0=BD=D1=88=D0=BD,=20=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D1=80=20device=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/raw_elm.py | 19 ++++-- .../opus-recheck-analysis-2026-06-28.md | 67 +++++++++++++++++++ doc/opus-recheck-request-2026-06-28.md | 31 +++++++++ 3 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 doc/history/opus-recheck-analysis-2026-06-28.md create mode 100644 doc/opus-recheck-request-2026-06-28.md diff --git a/api/raw_elm.py b/api/raw_elm.py index 6f0e017..11be7e1 100644 --- a/api/raw_elm.py +++ b/api/raw_elm.py @@ -192,11 +192,13 @@ def raw_dequeue_cmd(): ).fetchone() if not row: return "", 204 - db.conn.execute( - "UPDATE command_queue SET status='sent', sent_at=datetime('now') WHERE id=?", + cur = db.conn.execute( + "UPDATE command_queue SET status='sent', sent_at=datetime('now') WHERE id=? AND status='pending'", (row["id"],) ) db.conn.commit() + if cur.rowcount == 0: # перехватил другой воркер + return "", 204 result = {"seq": row["seq"], "cmd": row["cmd"], "timeout_ms": row["timeout_ms"], "drain_first": bool(row["drain_first"])} logger.info(f"RawELM: dequeued #{row['seq']} → {row['cmd']}") return jsonify(result) @@ -257,15 +259,22 @@ def raw_get_response(): @bp.route("/api/v1/elm/raw/status", methods=["GET"]) def raw_status(): """Copilot: статус устройства.""" + device_id = request.args.get("device_id", "unknown") with Database() as db: + # Чистка старых записей (старше 1 дня) + db.conn.execute( + "DELETE FROM command_queue WHERE responded_at < datetime('now', '-1 day')" + ) + db.conn.commit() last = db.conn.execute( - "SELECT seq, status FROM command_queue ORDER BY id DESC LIMIT 1" + "SELECT seq, status FROM command_queue WHERE device_id=? ORDER BY id DESC LIMIT 1", + (device_id,) ).fetchone() pending = db.conn.execute( - "SELECT COUNT(*) FROM command_queue WHERE status='pending'" + "SELECT COUNT(*) FROM command_queue WHERE device_id=? AND status='pending'", (device_id,) ).fetchone()[0] total = db.conn.execute( - "SELECT COUNT(*) FROM command_queue WHERE status='done'" + "SELECT COUNT(*) FROM command_queue WHERE device_id=? AND status='done'", (device_id,) ).fetchone()[0] return jsonify({ "device_ready": last is not None, diff --git a/doc/history/opus-recheck-analysis-2026-06-28.md b/doc/history/opus-recheck-analysis-2026-06-28.md new file mode 100644 index 0000000..c99a479 --- /dev/null +++ b/doc/history/opus-recheck-analysis-2026-06-28.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`. diff --git a/doc/opus-recheck-request-2026-06-28.md b/doc/opus-recheck-request-2026-06-28.md new file mode 100644 index 0000000..64ddf41 --- /dev/null +++ b/doc/opus-recheck-request-2026-06-28.md @@ -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?