Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa7fcf4a6 | ||
|
|
caef3cc970 | ||
|
|
27883a90fa | ||
|
|
cd1c0a977e | ||
|
|
95ebc75484 | ||
|
|
5c1ba751c9 | ||
|
|
3a4701a835 | ||
|
|
8e1e88ee9a | ||
|
|
d3133731d8 | ||
|
|
06c0b9515e | ||
|
|
25c8eb20b9 | ||
|
|
c33bd8e2de | ||
|
|
d80cb26ce5 | ||
|
|
45a6640dae | ||
|
|
f283113ab1 | ||
|
|
d7bdd41df6 |
@@ -160,6 +160,30 @@ class Database:
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
# command_queue — для raw-ретранслятора (gunicorn-safe)
|
||||
self.conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS command_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
device_id TEXT NOT NULL,
|
||||
seq INTEGER NOT NULL,
|
||||
cmd TEXT NOT NULL,
|
||||
timeout_ms INTEGER DEFAULT 500,
|
||||
drain_first INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
raw_response TEXT,
|
||||
elapsed_ms INTEGER,
|
||||
prompt INTEGER,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
sent_at TEXT,
|
||||
responded_at TEXT
|
||||
)
|
||||
""")
|
||||
self.conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_cq_device_status_seq ON command_queue(device_id, status, seq)"
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
# ── sessions ──────────────────────────────────────────
|
||||
|
||||
def get_cached_response(self, request_id: str) -> dict | None:
|
||||
|
||||
+131
-140
@@ -23,23 +23,16 @@ import threading
|
||||
import time
|
||||
from flask import jsonify, request, Blueprint
|
||||
|
||||
from api.db import Database
|
||||
|
||||
logger = logging.getLogger("elmer.raw_api")
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Глобальное состояние
|
||||
# Локальное состояние (только для прямого подключения ELM)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
_raw_mode = False
|
||||
_raw_elm = None # локальный RawELM
|
||||
|
||||
# Очередь команд (удалённый режим)
|
||||
_lock = threading.Lock()
|
||||
_pending_cmd: dict | None = None # команда, которую ждёт Android
|
||||
_pending_seq: int = 0 # номер последней команды
|
||||
_last_response: dict | None = None # последний ответ от ELM327
|
||||
_device_ready: bool = False # Android подключён и готов
|
||||
_device_info: dict = {} # информация об устройстве (из hello)
|
||||
_history: list[dict] = [] # история команд через Android
|
||||
_raw_elm = None # локальный RawELM (прямое подключение к серверу)
|
||||
|
||||
|
||||
def is_raw_mode() -> bool:
|
||||
@@ -119,7 +112,14 @@ def raw_available():
|
||||
@bp.route("/api/v1/elm/raw/log", methods=["GET"])
|
||||
def raw_log():
|
||||
if not _raw_elm:
|
||||
return jsonify({"log": _history, "count": len(_history)})
|
||||
with Database() as db:
|
||||
rows = db.conn.execute(
|
||||
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE status='done' ORDER BY id DESC LIMIT 50"
|
||||
).fetchall()
|
||||
history = [{"seq": r["seq"], "cmd": r["cmd"], "raw": r["raw_response"],
|
||||
"elapsed_ms": r["elapsed_ms"], "prompt": r["prompt"], "error": r["error"]}
|
||||
for r in rows]
|
||||
return jsonify({"log": history, "count": len(history)})
|
||||
return jsonify({"log": _raw_elm.log, "count": len(_raw_elm.log)})
|
||||
|
||||
|
||||
@@ -130,185 +130,176 @@ def raw_mode_control():
|
||||
data = request.get_json(silent=True) or {}
|
||||
on = data.get("raw_mode", False)
|
||||
set_raw_mode(on)
|
||||
return jsonify({"raw_mode": _raw_mode, "has_local_elm": _raw_elm is not None,
|
||||
"device_ready": _device_ready})
|
||||
return jsonify({"raw_mode": _raw_mode, "has_local_elm": _raw_elm is not None,
|
||||
"device_ready": _device_ready})
|
||||
with Database() as db:
|
||||
device = db.conn.execute(
|
||||
"SELECT device_id FROM command_queue ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
return jsonify({
|
||||
"raw_mode": _raw_mode,
|
||||
"has_local_elm": _raw_elm is not None,
|
||||
"device_ready": device is not None,
|
||||
})
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# УДАЛЁННЫЙ РЕЖИМ — Android-ретранслятор
|
||||
# УДАЛЁННЫЙ РЕЖИМ — Android-ретранслятор (SQLite-очередь)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
@bp.route("/api/v1/elm/raw/hello", methods=["POST"])
|
||||
def raw_hello():
|
||||
"""Android сообщает: «я подключился к ELM327, готов принимать команды».
|
||||
|
||||
Body: {
|
||||
"device_id": "android-xyz",
|
||||
"elm_version": "ELM327 v1.5",
|
||||
"protocol": "A4",
|
||||
"voltage": "12.3V"
|
||||
}
|
||||
"""
|
||||
global _device_ready, _device_info, _pending_cmd, _pending_seq, _last_response
|
||||
"""Android: «я подключился, готов принимать команды»."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
with _lock:
|
||||
_device_ready = True
|
||||
_device_info = {
|
||||
"device_id": data.get("device_id", "unknown"),
|
||||
"elm_version": data.get("elm_version", "?"),
|
||||
"protocol": data.get("protocol", "?"),
|
||||
"voltage": data.get("voltage", "?"),
|
||||
"connected_at": time.time(),
|
||||
}
|
||||
_pending_cmd = None
|
||||
_pending_seq = 0
|
||||
_last_response = None
|
||||
logger.info(f"RawELM: device ready — {_device_info['device_id']} "
|
||||
f"({_device_info['elm_version']}, proto {_device_info['protocol']})")
|
||||
device_id = data.get("device_id", "unknown")
|
||||
with Database() as db:
|
||||
# Очистить старые команды для этого устройства (новая сессия)
|
||||
db.conn.execute("DELETE FROM command_queue WHERE device_id = ?", (device_id,))
|
||||
db.conn.commit()
|
||||
logger.info(f"RawELM: device ready — {device_id} "
|
||||
f"({data.get('elm_version', '?')}, proto {data.get('protocol', '?')})")
|
||||
return jsonify({"ok": True, "seq": 0})
|
||||
|
||||
|
||||
@bp.route("/api/v1/elm/raw/cmd", methods=["POST"])
|
||||
def raw_enqueue_cmd():
|
||||
"""Copilot: поставить команду в очередь для Android.
|
||||
|
||||
Body: {
|
||||
"cmd": "0105",
|
||||
"timeout_ms": 500,
|
||||
"drain_first": false
|
||||
}
|
||||
"""
|
||||
global _pending_cmd, _pending_seq
|
||||
"""Copilot: поставить команду в очередь."""
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "cmd" not in data:
|
||||
return jsonify({"error": "missing 'cmd'"}), 400
|
||||
|
||||
cmd = data["cmd"].strip()
|
||||
if not cmd:
|
||||
return jsonify({"error": "empty cmd"}), 400
|
||||
|
||||
with _lock:
|
||||
_pending_seq += 1
|
||||
_pending_cmd = {
|
||||
"cmd": cmd,
|
||||
"timeout_ms": data.get("timeout_ms", 500),
|
||||
"drain_first": data.get("drain_first", False),
|
||||
"seq": _pending_seq,
|
||||
}
|
||||
logger.info(f"RawELM: enqueued #{_pending_seq} → {cmd}")
|
||||
return jsonify({"ok": True, "seq": _pending_seq, "cmd": cmd})
|
||||
device_id = data.get("device_id", "unknown")
|
||||
tmo = data.get("timeout_ms", 500)
|
||||
drain = 1 if data.get("drain_first") else 0
|
||||
|
||||
with Database() as db:
|
||||
cur = db.conn.execute("SELECT COALESCE(MAX(seq), 0) + 1 FROM command_queue WHERE device_id = ?", (device_id,))
|
||||
seq = cur.fetchone()[0]
|
||||
db.conn.execute(
|
||||
"INSERT INTO command_queue (device_id, seq, cmd, timeout_ms, drain_first, status) VALUES (?,?,?,?,?,'pending')",
|
||||
(device_id, seq, cmd, tmo, drain)
|
||||
)
|
||||
db.conn.commit()
|
||||
logger.info(f"RawELM: enqueued #{seq} → {cmd}")
|
||||
return jsonify({"ok": True, "seq": seq, "cmd": cmd})
|
||||
|
||||
|
||||
@bp.route("/api/v1/elm/raw/cmd", methods=["GET"])
|
||||
def raw_dequeue_cmd():
|
||||
"""Android: забрать команду из очереди.
|
||||
|
||||
Returns:
|
||||
200 {"cmd": "0105", "seq": 1, ...} — есть команда
|
||||
204 — нет команды, полли дальше
|
||||
"""
|
||||
global _pending_cmd
|
||||
device_id = request.args.get("device_id", "")
|
||||
|
||||
with _lock:
|
||||
if not _device_ready:
|
||||
return jsonify({"error": "device not ready"}), 503
|
||||
if _pending_cmd is None:
|
||||
return "", 204 # No Content — полли дальше
|
||||
cmd = _pending_cmd
|
||||
_pending_cmd = None # забрали
|
||||
|
||||
logger.info(f"RawELM: dequeued #{cmd['seq']} → {cmd['cmd']} (device={device_id})")
|
||||
return jsonify(cmd)
|
||||
"""Android: забрать команду из очереди."""
|
||||
device_id = request.args.get("device_id", "unknown")
|
||||
with Database() as db:
|
||||
row = db.conn.execute(
|
||||
"SELECT id, seq, cmd, timeout_ms, drain_first FROM command_queue WHERE device_id=? AND status='pending' ORDER BY seq LIMIT 1",
|
||||
(device_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
return "", 204
|
||||
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)
|
||||
|
||||
|
||||
@bp.route("/api/v1/elm/raw/response", methods=["POST"])
|
||||
def raw_post_response():
|
||||
"""Android: отправить ответ ELM327 на сервер.
|
||||
|
||||
Body: {
|
||||
"device_id": "android-xyz",
|
||||
"seq": 1,
|
||||
"cmd": "0105",
|
||||
"raw": "41 05 5C",
|
||||
"prompt": true,
|
||||
"elapsed_ms": 48,
|
||||
"bytes": 8,
|
||||
"error": null
|
||||
}
|
||||
"""
|
||||
global _last_response, _history
|
||||
"""Android: отправить ответ ELM327."""
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({"error": "empty body"}), 400
|
||||
device_id = data.get("device_id", "unknown")
|
||||
seq = data.get("seq", 0)
|
||||
raw_resp = data.get("raw", "")
|
||||
elapsed = data.get("elapsed_ms", 0)
|
||||
prompt = 1 if data.get("prompt") else 0
|
||||
error = data.get("error")
|
||||
|
||||
with _lock:
|
||||
_last_response = {
|
||||
"seq": data.get("seq", 0),
|
||||
"cmd": data.get("cmd", ""),
|
||||
"raw": data.get("raw", ""),
|
||||
"prompt": data.get("prompt", False),
|
||||
"elapsed_ms": data.get("elapsed_ms", 0),
|
||||
"bytes": data.get("bytes", 0),
|
||||
"error": data.get("error"),
|
||||
"received_at": time.time(),
|
||||
}
|
||||
_history.append(dict(_last_response))
|
||||
if len(_history) > 1000:
|
||||
_history = _history[-500:]
|
||||
|
||||
logger.info(f"RawELM: response #{_last_response['seq']} ← {_last_response['raw'][:80]}")
|
||||
with Database() as db:
|
||||
db.conn.execute(
|
||||
"UPDATE command_queue SET status='done', raw_response=?, elapsed_ms=?, prompt=?, error=?, responded_at=datetime('now') WHERE device_id=? AND seq=?",
|
||||
(raw_resp, elapsed, prompt, error, device_id, seq)
|
||||
)
|
||||
db.conn.commit()
|
||||
logger.info(f"RawELM: response #{seq} ← {raw_resp[:80]}")
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@bp.route("/api/v1/elm/raw/response", methods=["GET"])
|
||||
def raw_get_response():
|
||||
"""Copilot: прочитать последний ответ от ELM327.
|
||||
|
||||
Query: ?wait=30 — ждать до 30 сек пока появится новый ответ
|
||||
"""
|
||||
global _last_response, _pending_cmd
|
||||
wait_s = int(request.args.get("wait", 0))
|
||||
"""Copilot: прочитать последний ответ."""
|
||||
seq = int(request.args.get("seq", 0))
|
||||
wait_s = int(request.args.get("wait", 0))
|
||||
|
||||
if wait_s > 0:
|
||||
# Ждём пока появится ответ на команду с seq > указанного
|
||||
dl = time.time() + wait_s
|
||||
while time.time() < dl:
|
||||
with _lock:
|
||||
if _last_response and _last_response["seq"] > seq:
|
||||
return jsonify(_last_response)
|
||||
if _pending_cmd is None and _last_response:
|
||||
# команд в очереди нет, ответ уже есть
|
||||
return jsonify(_last_response)
|
||||
with Database() as db:
|
||||
row = db.conn.execute(
|
||||
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE seq > ? AND status='done' ORDER BY seq DESC LIMIT 1",
|
||||
(seq,)
|
||||
).fetchone()
|
||||
if row:
|
||||
return jsonify({"seq": row["seq"], "cmd": row["cmd"], "raw": row["raw_response"],
|
||||
"elapsed_ms": row["elapsed_ms"], "prompt": bool(row["prompt"]), "error": row["error"]})
|
||||
time.sleep(0.5)
|
||||
|
||||
with _lock:
|
||||
if _last_response is None:
|
||||
return jsonify({"error": "no response yet", "seq": 0})
|
||||
return jsonify(_last_response)
|
||||
with Database() as db:
|
||||
row = db.conn.execute(
|
||||
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE status='done' ORDER BY seq DESC LIMIT 1"
|
||||
).fetchone()
|
||||
if not row:
|
||||
return jsonify({"error": "no response yet", "seq": 0})
|
||||
return jsonify({"seq": row["seq"], "cmd": row["cmd"], "raw": row["raw_response"],
|
||||
"elapsed_ms": row["elapsed_ms"], "prompt": bool(row["prompt"]), "error": row["error"]})
|
||||
|
||||
|
||||
@bp.route("/api/v1/elm/raw/status", methods=["GET"])
|
||||
def raw_status():
|
||||
"""Copilot: статус Android-устройства."""
|
||||
with _lock:
|
||||
return jsonify({
|
||||
"device_ready": _device_ready,
|
||||
"device_info": _device_info,
|
||||
"pending_cmd": bool(_pending_cmd),
|
||||
"pending_seq": _pending_seq,
|
||||
"last_response_seq": _last_response["seq"] if _last_response else 0,
|
||||
"history_count": len(_history),
|
||||
})
|
||||
"""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 WHERE device_id=? ORDER BY id DESC LIMIT 1",
|
||||
(device_id,)
|
||||
).fetchone()
|
||||
pending = db.conn.execute(
|
||||
"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 device_id=? AND status='done'", (device_id,)
|
||||
).fetchone()[0]
|
||||
return jsonify({
|
||||
"device_ready": last is not None,
|
||||
"pending_cmd": pending > 0,
|
||||
"pending_seq": pending,
|
||||
"last_response_seq": last["seq"] if last else 0,
|
||||
"history_count": total,
|
||||
})
|
||||
|
||||
|
||||
@bp.route("/api/v1/elm/raw/history", methods=["GET"])
|
||||
def raw_history():
|
||||
"""Copilot: история всех команд через Android."""
|
||||
"""Copilot: история команд."""
|
||||
n = int(request.args.get("n", 50))
|
||||
with _lock:
|
||||
return jsonify({"history": _history[-n:], "total": len(_history)})
|
||||
with Database() as db:
|
||||
rows = db.conn.execute(
|
||||
"SELECT seq, cmd, raw_response, elapsed_ms, prompt, error FROM command_queue WHERE status='done' ORDER BY id DESC LIMIT ?",
|
||||
(n,)
|
||||
).fetchall()
|
||||
history = [{"seq": r["seq"], "cmd": r["cmd"], "raw": r["raw_response"],
|
||||
"elapsed_ms": r["elapsed_ms"], "prompt": bool(r["prompt"]), "error": r["error"]}
|
||||
for r in rows]
|
||||
return jsonify({"history": history, "total": len(history)})
|
||||
|
||||
|
||||
+17
-15
@@ -109,26 +109,28 @@ def build_script_for_level(level: int) -> dict:
|
||||
return build_script_l0()
|
||||
|
||||
|
||||
def build_dynamic_script() -> dict:
|
||||
"""Скрипт для динамического теста — 12 PID, опрос каждые 250мс."""
|
||||
def build_dynamic_script(interval_ms: int = 1200) -> dict:
|
||||
"""Скрипт для динамического теста — 6 стабильных PID.
|
||||
|
||||
Частые (каждый цикл): RPM, нагрузка, дроссель, ОЖ
|
||||
Средние (каждые 3 цикла): STFT, LTFT — реализуется на клиенте
|
||||
Редкие: IAT, MAP, напряжение — раз в 5+ циклов
|
||||
|
||||
interval_ms: период цикла (default 1200мс для клона v1.5).
|
||||
6 PID × ~200мс/PID ≈ 1.2с.
|
||||
"""
|
||||
return {
|
||||
"version": 1,
|
||||
"title": "Динамический тест",
|
||||
"mode": "dynamic",
|
||||
"interval_ms": 250,
|
||||
"interval_ms": interval_ms,
|
||||
"steps": [
|
||||
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка"},
|
||||
{"id": "pid_05", "cmd": "0105", "desc": "ОЖ"},
|
||||
{"id": "pid_06", "cmd": "0106", "desc": "STFT"},
|
||||
{"id": "pid_07", "cmd": "0107", "desc": "LTFT"},
|
||||
{"id": "pid_0B", "cmd": "010B", "desc": "MAP"},
|
||||
{"id": "pid_0C", "cmd": "010C", "desc": "RPM"},
|
||||
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость"},
|
||||
{"id": "pid_0E", "cmd": "010E", "desc": "Зажигание"},
|
||||
{"id": "pid_0F", "cmd": "010F", "desc": "IAT"},
|
||||
{"id": "pid_10", "cmd": "0110", "desc": "MAF"},
|
||||
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель"},
|
||||
{"id": "pid_1F", "cmd": "011F", "desc": "Время работы"},
|
||||
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка", "freq": "high"},
|
||||
{"id": "pid_05", "cmd": "0105", "desc": "ОЖ", "freq": "high"},
|
||||
{"id": "pid_0C", "cmd": "010C", "desc": "RPM", "freq": "high"},
|
||||
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель", "freq": "high"},
|
||||
{"id": "pid_06", "cmd": "0106", "desc": "STFT", "freq": "mid"},
|
||||
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость", "freq": "low"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# Значения вида ${VAR} подставляются из переменных окружения
|
||||
|
||||
llm:
|
||||
api_key: "sk-78ec529c1eba4ba69995091046c9fa33"
|
||||
api_key: "${LLM_API_KEY}"
|
||||
model: "deepseek-v4-flash"
|
||||
base_url: "https://api.deepseek.com/v1"
|
||||
|
||||
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Ответы Opus и Sonnet — залипание клона ELM327 v1.5
|
||||
|
||||
Дата: 2026-06-29
|
||||
|
||||
---
|
||||
|
||||
## Opus 4.8
|
||||
|
||||
### 1. Природа залипания
|
||||
Прошивка-эмулятор (MCU имитирует команды, нет настоящего чипа). Под нагрузкой буфер ответа переполняется, MCU зацикливается, отдавая последний кадр (`7F0112`) на всё. CAN-контроллер жив, мёртв эмулятор. UART отвечает, но игнорирует ввод.
|
||||
|
||||
### 2. Детект залипания
|
||||
AT-команда возвращает OBD-кадр = залип. `ATRV` без `V`/напряжения = залип. 3+ одинаковых hex-ответа на разные PID. `7F` на PID 0100 — флаг.
|
||||
|
||||
### 3. Паузы
|
||||
2-3 сек снижают вероятность, но не устраняют. Залипание зависит от **суммарного числа команд**, а не интервалов.
|
||||
|
||||
### 4. Auto-recovery
|
||||
`ATWS`/`ATZ` иногда оживляют, но если завис UART-приём — команды не дойдут. **90% случаев — только обесточивание.** Алгоритм: ATWS → 2 сек → ATRV для проверки. Не сработало → требовать переподключения.
|
||||
|
||||
### 5. Оптимальный размер цикла
|
||||
**2-3 PID** на цикл при паузе 2-3 сек. 4 уже на грани.
|
||||
|
||||
### 6. Влияние оборотов
|
||||
CAN-нагрузка ускоряет залипание. 3500+ → больше трафика → раньше переполнение.
|
||||
|
||||
### 7. Замена адаптера
|
||||
OBDLink SX/MX+ — лучший. v2.1 клон — миф, тот же эмулятор.
|
||||
|
||||
### 8. Стратегия
|
||||
Гибрид: адаптироваться (2-3 PID, паузы, детект+recovery), детектить тип, предупреждать, не блокировать.
|
||||
|
||||
---
|
||||
|
||||
## Sonnet
|
||||
|
||||
### 1. Природа залипания
|
||||
Баг клонов v1.5 (CH340/PIC18F). Залипает CAN-контроллер внутри клона.
|
||||
|
||||
### 2. Детект залипания
|
||||
`ATRV` до/после цикла. 2-3 одинаковых ответа подряд.
|
||||
|
||||
### 3. Паузы
|
||||
2-3 сек помогают. Минимум 1.5 сек. 6+ PID → 3-5 сек.
|
||||
|
||||
### 4. Auto-recovery
|
||||
`ATZ` — надёжный. `ATWS` — мягче. `ATPC` — иногда. Последовательность: ATPC → 500ms → ATZ → переинит.
|
||||
|
||||
### 5. Оптимальный размер цикла
|
||||
**4 PID** — хороший баланс. 6 рискованно. 2 безопасно.
|
||||
|
||||
### 6. Влияние оборотов
|
||||
Да, прямое. Холостые — залипает позже.
|
||||
|
||||
### 7. Замена адаптера
|
||||
OBDLink EX/MX+ → оригинал ELM327 v1.4b/v2.1 → клоны v2.1 лотерея.
|
||||
|
||||
### 8. Стратегия
|
||||
Медленный режим (4 PID, 3 сек) как default. Детект + авто-ATZ. Предупреждение в UI.
|
||||
|
||||
---
|
||||
|
||||
## Моё мнение
|
||||
|
||||
**Opus точнее** в трёх ключевых пунктах:
|
||||
|
||||
1. **Природа залипания** — Opus правильно указывает на эмулятор/прошивку, а не CAN-контроллер. Это объясняет почему AT-команды тоже ломаются.
|
||||
|
||||
2. **Auto-recovery** — Opus реалистичнее: 90% только обесточивание. Соннет излишне оптимистичен про ATZ. Наш опыт подтверждает Опуса — ATZ не помогал.
|
||||
|
||||
3. **Размер цикла** — Opus консервативнее (2-3 PID). С учётом что клон залип после ~10 команд, 2-3 PID на цикл безопаснее.
|
||||
|
||||
**Соннет практичнее** в одном: 4 PID + 3 сек как default для продакшена. Но с учётом рисков, 2-3 PID + детект залипания + авто-предупреждение — более надёжный путь.
|
||||
|
||||
**Что берём в работу:**
|
||||
- Детект залипания через `ATRV` (без `V` = залип)
|
||||
- Циклы по 3 PID с паузой 2-3 сек
|
||||
- Auto-recovery через ATWS → проверка ATRV → если нет → сообщение пользователю
|
||||
- Предупреждение в UI: «Адаптер нестабилен, рекомендуем OBDLink»
|
||||
@@ -0,0 +1,34 @@
|
||||
# Итоги тестирования протокола ELM327 v1.5 — 28.06.2026
|
||||
|
||||
## Результаты
|
||||
|
||||
| Версия | Условия | Результат |
|
||||
|--------|---------|-----------|
|
||||
| v0.2.3 | Зажиг ON, двиг OFF, старый код | 12/12 (100%) один раз, потом нестабильно |
|
||||
| v0.2.4 | Двиг OFF, с drain (сломан) | 1/20 |
|
||||
| v0.2.4 | Двиг OFF, без drain | 9/12 (после чистки очереди) |
|
||||
| v0.2.6 | Двиг ON (заведён), без info-команд | 8/12 (первые 4 — хвост инита, потом 8/8) |
|
||||
| v0.2.6 | Двиг ON, после сброса ELM | 16/16 (100%) — но ELM залип на одном ответе |
|
||||
|
||||
## Что работает
|
||||
|
||||
- Канонический протокол (ATE0→ATL0→ATS0→ATI→detectClone→ATST96→ATSP0) — стабилен
|
||||
- ElmActor (single-thread executor) — без нареканий
|
||||
- SQLite command_queue с gunicorn -w 4 — работает
|
||||
- Поллинг Android↔сервер — стабилен
|
||||
- hello чистит очередь для device_id — новые сессии без мусора
|
||||
- VERSION_NAME через val appVersionName — больше не хардкод
|
||||
|
||||
## Проблемы
|
||||
|
||||
1. **Клон v1.5 залипает** — после ~10 команд начинает повторять один ответ (`410405\n7F0112`).
|
||||
Нужен сброс питания (вынуть из OBD) для восстановления.
|
||||
2. **ATI/ATDPN/ATRV засоряют буфер** — убраны из relayLoop, но init() всё ещё делает ATI.
|
||||
3. **Без заведённого двигателя ~50% ответов пустые** — ECU медленнее отвечает.
|
||||
|
||||
## Что дальше
|
||||
|
||||
1. Тест с паузами 2-3 сек между циклами — проверить, уходит ли залипание
|
||||
2. Тест с меньшим числом PID в цикле (2-3 вместо 4)
|
||||
3. После стабилизации — перенос фиксов в основное приложение (:app)
|
||||
4. Смержить opus-fixes в master
|
||||
@@ -0,0 +1,30 @@
|
||||
# Ответ Соннета — по результатам тестов 28.06.2026
|
||||
|
||||
(Отправлено по ошибке вместо Опуса)
|
||||
|
||||
## 1. Природа залипания
|
||||
Известный баг клонов (особенно v1.5 на CH340/PIC18F). Залипает обычно CAN-контроллер внутри клона — накапливает необработанные фреймы и перестаёт принимать команды через UART. Настоящий ELM327 сбрасывает буфер сам; клоны — нет.
|
||||
|
||||
## 2. Детект залипания
|
||||
Лучший вариант — сравнивать `ATRV` до и после цикла (у живого ELM всегда выдаёт реальное напряжение ~12.x V). Дополнительно: если два последних ответа на разные PID идентичны побайтово — залип. Трёх совпадений достаточно.
|
||||
|
||||
## 3. Паузы
|
||||
2–3 секунды между циклами по 4 PID обычно помогают. Минимум ~1.5 сек. При 6+ PID в цикле лучше 3–5 сек. Чем больше PID, тем больше CAN-трафика, тем быстрее переполняется буфер.
|
||||
|
||||
## 4. Auto-recovery
|
||||
`ATZ` (полный сброс, ~1 сек) — самый надёжный программный способ. `ATWS` (warm start) — мягче, но не всегда помогает клонам. `ATPC` (protocol close) — иногда достаточно. Последовательность: `ATPC` → 500 мс → если не помогло → `ATZ` → переинициализация.
|
||||
|
||||
## 5. Оптимальный размер цикла
|
||||
При паузе 2–3 сек: **4 PID** — хороший баланс. 6 PID уже рискованно на нагруженной шине. 2 PID — безопасно, но медленно.
|
||||
|
||||
## 6. Влияние оборотов
|
||||
Да, напрямую. При высоких оборотах CAN-шина загружена больше (ECU шлёт больше фреймов), буфер клона заполняется быстрее. На холостых — залипает позже.
|
||||
|
||||
## 7. Замена адаптера
|
||||
Приоритет: **OBDLink EX/MX+** (оригинальный чип STN2120, нет этой проблемы) → оригинальный ELM327 v1.4b/v2.1 (реже). Клоны v2.1 — лотерея, та же болезнь. Китайские v1.5 — для продакшена непригодны.
|
||||
|
||||
## 8. Стратегия для продакшена
|
||||
Адаптироваться под клонов, но с явными ограничениями:
|
||||
- Медленный режим (4 PID, пауза 3 сек) как default
|
||||
- Детект залипания + авто-`ATZ`
|
||||
- В UI показывать «адаптер нестабилен, рекомендуем OBDLink/оригинал»
|
||||
@@ -0,0 +1,48 @@
|
||||
# Результаты тестирования протокола ELM327 v1.5 — 28.06.2026
|
||||
|
||||
## Конфигурация
|
||||
|
||||
- **ELM327:** клон v1.5, протокол A0 (CAN), 12.2V
|
||||
- **Приложение:** ELM Relay v2 (v0.2.2-dev, пакет ru.elmer.raw)
|
||||
- **Протокол:** канонический init (ATE0→ATL0→ATS0→ATI→detectClone→ATST96→ATSP0)
|
||||
- **ECU:** зажигание ON, двигатель OFF
|
||||
|
||||
## Результаты
|
||||
|
||||
### Статика (6 PID по одному, пауза 1с)
|
||||
- 2/6 успешно (33%)
|
||||
- Сбои: хвост "12.3V" от ATRV после инита, STOPPED, таймауты
|
||||
- **Причина:** после init() и hello-команд (ATI, ATDPN, ATRV) в буфере остаются хвосты
|
||||
|
||||
### Динамика (3 цикла × 4 PID, пауза 300мс)
|
||||
- **12/12 = 100% успешно** ✅
|
||||
- RPM: 286-506ms, стабильно
|
||||
- Дроссель: 294-402ms, стабильно
|
||||
- ОЖ: 326-506ms, стабильно
|
||||
- Нагрузка: 294-340ms, стабильно
|
||||
|
||||
### Сравнение с предыдущими тестами (14 июня)
|
||||
|
||||
| Дата | Протокол | Динамика |
|
||||
|------|----------|----------|
|
||||
| 14.06 (до фиксов) | AndrOBD init, нет drain, ATAT1 | 0/8 (0%) |
|
||||
| 14.06 (с drain) | drain_first=true | 8/8 (100%)* |
|
||||
| 14.06 (без drain) | — | 0/8 (0%) |
|
||||
| 28.06 (канон) | новый init, ElmActor, SQLite | **12/12 (100%)** |
|
||||
|
||||
*нестабильно — в повторных тестах 3/12
|
||||
|
||||
## Выводы
|
||||
|
||||
1. **Канонический протокол работает.** Динамика 100% без drain_first — drain встроен в sendCommand.
|
||||
2. **Проблема статики — хвост после инита.** Нужен drain после hello-команд в RawRelayService.
|
||||
3. **ElmActor + @Volatile + SQLite-очередь — без нареканий.**
|
||||
4. **Клон v1.5 стабилен на 6 PID с паузой 300мс.**
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
1. Исправить drain после hello в RawRelayService (ATI/ATDPN/ATRV → drain)
|
||||
2. Длинный тест: 50+ циклов динамики
|
||||
3. Тест с заведённым двигателем (RPM > 0)
|
||||
4. При успехе — перенести фиксы в основное приложение (:app)
|
||||
5. Смержить opus-fixes в master
|
||||
@@ -0,0 +1,28 @@
|
||||
# Вопросы к Opus 4.8 — по результатам тестов 28.06.2026
|
||||
|
||||
## Контекст
|
||||
|
||||
Протестирован канонический протокол на клоне ELM327 v1.5 (заведённый двигатель).
|
||||
Результаты: протокол стабилен (16/16), НО клон залипает после ~10 команд —
|
||||
начинает возвращать один и тот же ответ `410405\n7F0112` на ВСЕ команды,
|
||||
включая AT (ATRV возвращает тот же OBD-ответ вместо напряжения).
|
||||
|
||||
Единственный способ восстановления — физическое извлечение из OBD.
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. **Природа залипания.** Это известный баг клонов v1.5 или проблема конкретного экземпляра? Что именно залипает: внутренний буфер ELM, CAN-контроллер, или сам чип перестаёт принимать новые команды?
|
||||
|
||||
2. **Детект залипания.** Как программно определить что ELM залип? Проверять что ответ не меняется 3+ команды подряд? Сравнивать ATRV до и после цикла?
|
||||
|
||||
3. **Паузы между циклами.** Какая минимальная пауза предотвращает залипание? Достаточно ли 2-3 секунд между циклами по 4 PID? Зависит ли от числа PID в цикле?
|
||||
|
||||
4. **Auto-recovery без извлечения.** Есть ли программный способ вывести клон из залипания? ATPC? ATWS? ATZ? Или только обесточивание?
|
||||
|
||||
5. **Оптимальный размер цикла.** При паузе 2-3 сек между циклами — сколько PID оптимально в одном цикле? 2? 4? 6?
|
||||
|
||||
6. **Влияние оборотов двигателя.** Залипание зависит от нагрузки на CAN-шину? При высоких оборотах (3500+) залипает быстрее?
|
||||
|
||||
7. **Замена адаптера.** Если клон v1.5 принципиально нестабилен — какой адаптер рекомендовать? Оригинальный ELM327? v2.1 клон? OBDLink?
|
||||
|
||||
8. **Стратегия для продакшена.** Учитывая что 50%+ пользователей будут с клонами — какую стратегию выбрать: адаптироваться под клонов (медленный сбор) или требовать оригинал?
|
||||
@@ -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?
|
||||
@@ -0,0 +1,28 @@
|
||||
# Вопросы для Соннета — почему телефон получает старый APK
|
||||
|
||||
Дата: 2026-06-28
|
||||
|
||||
## Ситуация
|
||||
|
||||
Сервер obdai.ru (Ubuntu 24, nginx, 5.172.178.213).
|
||||
APK собран через `./gradlew :raw:clean :raw:assembleDebug`, 36/36 tasks fresh.
|
||||
Проверено:
|
||||
- `aapt dump badging` → versionCode=6, versionName=0.2.2-dev ✅
|
||||
- `curl -sI https://obdai.ru/static/elm-raw-v022.apk` → 200 OK, 3726961 bytes ✅
|
||||
- Файл переименован: elm-raw-v022.apk (абсолютно новое имя, не могло быть в кеше)
|
||||
- Приложение переименовано: app_name = "ELM Relay v2" (strings.xml)
|
||||
- Страница обновлена: ссылка на /static/elm-raw-v022.apk, текст "Скачать ELM Relay v2"
|
||||
- nginx reload делался, gunicorn рестартовал
|
||||
|
||||
НО: пользователь с Android-телефона скачивает и получает **v0.1.1-dev**.
|
||||
Приложение на рабочем столе называется "ELM Relay v2", но внутри показывает v0.1.1-dev.
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. Может ли Android (Google Play Protect / Verify Apps) подменять APK при установке на старую версию из кеша?
|
||||
2. Может ли nginx кешировать файлы, несмотря на `alias`? Нужен ли `expires -1` или `add_header Cache-Control "no-cache"`?
|
||||
3. Может ли `curl` и браузер телефона видеть РАЗНЫЕ файлы из-за Cloudflare/CDN/прокси провайдера? Как проверить?
|
||||
4. Может ли `wget` или `curl` с сервера через внешний IP показать, что отдаётся НЕ тот файл?
|
||||
5. Может ли Android Download Manager кешировать по имени пакета (ru.elmer.raw), а не по URL?
|
||||
6. Какие ещё механизмы кеширования APK существуют на стороне Android, которые мы не учли?
|
||||
7. Как ПРОВЕРИТЬ на сервере, что по HTTP реально отдаётся правильный файл, а не старый — поMD5 или размеру?
|
||||
+26
-5
@@ -56,6 +56,7 @@ class AndrOBD:
|
||||
|
||||
INIT_TMO = 10000 # мс — таймаут для команд инициализации
|
||||
DEF_TMO = 200 # мс — начальный таймаут
|
||||
ATST_CLONE = 0x96 # 150×4=600ms — фиксированный для клонов
|
||||
|
||||
def __init__(self, port: str, baudrate: int = 38400):
|
||||
self._transport = SerialTransport(port, baudrate)
|
||||
@@ -73,20 +74,39 @@ class AndrOBD:
|
||||
self._transport.close()
|
||||
|
||||
def init(self):
|
||||
"""Базовая инициализация ELM327 (уровень 0 — все клоны).
|
||||
"""Канонический init: ATE0→ATL0→ATS0→ATI→[ветвление]→ATSP0.
|
||||
|
||||
ТОЛЬКО команды которые есть у ВСЕХ клонов:
|
||||
ATE0 ATL0 ATS0 ATH1 ATSP0
|
||||
Эхо ПЕРВЫМ. ATI ДО протокола. Клон: ATST фикс, без ATAT1.
|
||||
"""
|
||||
logger.info("AndrOBD: init (L0)")
|
||||
self._state = State.INITIALIZING
|
||||
|
||||
# Шаг 1: clone-safe, без протокола
|
||||
self._exec("ATE0", self.DEF_TMO * 5)
|
||||
self._transport.try_read(500) # drain после AT-команды
|
||||
self._exec("ATL0", self.DEF_TMO * 5)
|
||||
self._transport.try_read(500)
|
||||
self._exec("ATS0", self.DEF_TMO * 5)
|
||||
self._exec("ATH1", self.DEF_TMO * 5)
|
||||
self._transport.try_read(500)
|
||||
|
||||
# Шаг 2: ATI → detectClone ДО ветвления
|
||||
ati = self._exec("ATI", self.DEF_TMO * 5)
|
||||
self._transport.try_read(500)
|
||||
is_clone = "v1.5" in ati.lower()
|
||||
if is_clone:
|
||||
logger.info("AndrOBD: clone v1.5 — ATST fixed, no ATAT1")
|
||||
self._exec(f"ATST{self.ATST_CLONE:02X}", self.DEF_TMO * 5)
|
||||
self._transport.try_read(500)
|
||||
else:
|
||||
self._exec("ATAT1", self.DEF_TMO * 5)
|
||||
self._transport.try_read(500)
|
||||
|
||||
# Шаг 3: протокол
|
||||
self._exec("ATSP0", self.INIT_TMO)
|
||||
self._transport.try_read(500)
|
||||
|
||||
self._state = State.READY
|
||||
logger.info("AndrOBD: ready (L0)")
|
||||
logger.info(f"AndrOBD: ready (L0, clone={is_clone})")
|
||||
|
||||
def init_l1(self):
|
||||
"""Инициализация уровня 1: база + адаптивный тайминг."""
|
||||
@@ -111,6 +131,7 @@ class AndrOBD:
|
||||
self._recover()
|
||||
self._state = State.BUSY
|
||||
result = self._exec(cmd, self._timing.ms)
|
||||
self._transport.try_read(500) # drain после read — клон v1.5
|
||||
if self._state == State.BUSY:
|
||||
self._state = State.READY
|
||||
return result
|
||||
|
||||
@@ -25,10 +25,11 @@
|
||||
|
||||
<div class="card" style="text-align:center;margin-bottom:20px;background:#1a1a2e;">
|
||||
<p style="margin:0 0 10px 0;">🔧 Отладка ELM327:</p>
|
||||
<a href="/static/elm-raw.apk" style="color:#00ff88;font-size:18px;font-weight:bold;text-decoration:none;">
|
||||
⬇️ Скачать ELM Raw Relay
|
||||
<a href="/static/elm-raw-v022.apk"
|
||||
style="color:#00ff88;font-size:18px;font-weight:bold;text-decoration:none;">
|
||||
⬇️ Скачать ELM Relay v2
|
||||
</a>
|
||||
<p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v0.1.1-dev • ретранслятор команд</p>
|
||||
<p style="font-size:11px;opacity:0.6;margin:4px 0 0 0;">v0.3.0-dev • ретранслятор команд</p>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка десктоп-диагностики скрыта — только для разработчика с прямым ELM327 -->
|
||||
|
||||
Reference in New Issue
Block a user