Files
elmer/obd/raw_console.py

252 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
obd/raw_console.py — Сырой слой ELM327 (без стейт-машины).
НИКАКОЙ логики протокола:
- Нет state machine (State)
- Нет классификации ответов (Rsp)
- Нет адаптивных таймингов (AdaptiveTiming)
- Нет ретраев
- Нет хендлеров ошибок
ТОЛЬКО:
- send(cmd) → отправляет команду + CR
- read(timeout) → читает ВСЁ до '>' или таймаута, байт-за-байтом
- drain() → очищает входной буфер
- available() → сколько байт ждёт в буфере
ПРЕДНАЗНАЧЕНИЕ:
Изучение реального поведения ELM327.
«Почему статика работает, а динамика ломается?»
Ответ — в сырых байтах.
"""
import logging
import time
logger = logging.getLogger("elm.raw")
# ── Конфигурация по умолчанию (можно переопределить) ──
DEFAULT_TIMEOUT = 500 # мс
INTER_CMD_DELAY = 0.05 # с — пауза между командой и чтением
class RawELM:
"""Сырой слой ELM327 — только send/read/drain, без протокольной логики."""
def __init__(self, transport):
"""
Args:
transport: объект с методами .write(str) и .read(timeout_ms) → str
(обычно SerialTransport из obd.connection)
"""
self._t = transport
self._timeout = DEFAULT_TIMEOUT
self._inter_delay = INTER_CMD_DELAY
self._log: list[dict] = [] # история команд
# ── Настройка ────────────────────────────────────
@property
def timeout(self) -> int:
return self._timeout
@timeout.setter
def timeout(self, ms: int):
self._timeout = ms
@property
def inter_delay(self) -> float:
return self._inter_delay
@inter_delay.setter
def inter_delay(self, sec: float):
self._inter_delay = sec
# ── Основные операции ────────────────────────────
def send(self, cmd: str, timeout: int | None = None) -> dict:
"""Отправить команду и прочитать сырой ответ.
Args:
cmd: команда (без \r, добавится автоматически)
timeout: таймаут в мс (None = использовать self.timeout)
Returns:
{
"cmd": str, # что отправили
"raw": str, # сырой ответ (без '>')
"prompt": bool, # получен ли '>'
"elapsed_ms": int, # сколько мс заняло
"bytes": int, # сколько байт в ответе
"error": str|None, # ошибка если есть
}
Не бросает исключений — всегда возвращает dict с полем error.
"""
tmo = timeout if timeout is not None else self._timeout
entry = {"cmd": cmd, "timeout_ms": tmo, "ts": time.time()}
try:
# 1. Отправить
self._t.write(cmd)
# 2. Пауза (ELM начинает отвечать не мгновенно)
if self._inter_delay > 0:
time.sleep(self._inter_delay)
# 3. Прочитать
t0 = time.monotonic()
raw, prompt, nbytes = self._read_raw(tmo)
elapsed = int((time.monotonic() - t0) * 1000)
entry.update({
"raw": raw,
"prompt": prompt,
"elapsed_ms": elapsed,
"bytes": nbytes,
"error": None,
})
except TimeoutError:
entry.update({
"raw": "",
"prompt": False,
"elapsed_ms": tmo,
"bytes": 0,
"error": f"timeout {tmo}ms",
})
except Exception as e:
entry.update({
"raw": "",
"prompt": False,
"elapsed_ms": 0,
"bytes": 0,
"error": str(e),
})
self._log.append(entry)
return entry
def drain(self) -> dict:
"""Очистить входной буфер. Возвращает что было выброшено.
Returns:
{"drained": str, "bytes": int}
"""
t0 = time.monotonic()
drained = []
total = 0
dl = t0 + 0.5 # 500 мс максимум на дренаж
while time.monotonic() < dl:
try:
ch = self._read_byte(0.05)
if ch is not None:
drained.append(chr(ch))
total += 1
else:
break # буфер пуст
except Exception:
break
elapsed = int((time.monotonic() - t0) * 1000)
result = {"drained": "".join(drained), "bytes": total, "elapsed_ms": elapsed}
if total > 0:
logger.info(f"RawELM: drained {total} bytes: {result['drained']!r}")
return result
def available(self) -> int:
"""Сколько байт ждёт во входном буфере (0 = пусто)."""
try:
return self._t._ser.in_waiting
except Exception:
return -1
# ── Лог ──────────────────────────────────────────
@property
def log(self) -> list[dict]:
"""История всех команд."""
return self._log
def clear_log(self):
"""Очистить историю."""
self._log.clear()
def last(self) -> dict | None:
"""Последняя команда."""
return self._log[-1] if self._log else None
# ── Приватные ────────────────────────────────────
def _read_raw(self, timeout_ms: int) -> tuple[str, bool, int]:
"""Читает байт-за-байтом до '>' или таймаута.
Returns:
(raw_text, got_prompt, byte_count)
"""
dl = time.monotonic() + timeout_ms / 1000.0
lines, cur = [], []
got_prompt = False
while time.monotonic() < dl:
ch = self._read_byte(0.05)
if ch is None:
continue
cp = ch
if cp == 62: # '>' — промпт ELM327
if cur:
lines.append("".join(cur))
cur.clear()
got_prompt = True
break
elif cp == 13: # CR — конец строки
if cur:
lines.append("".join(cur))
cur.clear()
elif cp in (10, 32): # LF и пробел — игнорируем
pass
else:
cur.append(chr(cp))
if cur:
lines.append("".join(cur))
return ("\n".join(lines), got_prompt, sum(len(s) for s in lines))
def _read_byte(self, timeout_s: float) -> int | None:
"""Прочитать один байт с таймаутом. None = таймаут/нет данных."""
import serial
try:
if self._t._ser.in_waiting > 0:
b = self._t._ser.read(1)
return b[0] if b else None
else:
time.sleep(0.001) # поллинг 1мс
return None
except serial.SerialException:
return None
# ── Хелпер ───────────────────────────────────────────
def format_response(entry: dict) -> str:
"""Форматирует ответ RawELM.send() для вывода в консоль."""
lines = [
f"→ {entry['cmd']}",
f"← {entry['raw']!r}" if entry["raw"] else "← (пусто)",
]
if entry["prompt"]:
lines.append(" prompt: ✅ >")
else:
lines.append(" prompt: ❌")
lines.append(f" time: {entry['elapsed_ms']}ms, bytes: {entry['bytes']}")
if entry["error"]:
lines.append(f" ⚠️ {entry['error']}")
return "\n".join(lines)
def format_log(entries: list[dict]) -> str:
"""Форматирует всю историю команд."""
return "\n" + "─" * 50 + "\n" + \
"\n".join(format_response(e) for e in entries) + \
"\n" + "─" * 50