feat: raw ELM327 console — сырой слой, API, интерактивная консоль

This commit is contained in:
Repinoid
2026-06-14 18:24:27 +04:00
parent 7d23a874e0
commit d01028b12b
5 changed files with 945 additions and 0 deletions
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""
elm_console.py — Интерактивная консоль ELM327 (сырой режим).
НИКАКОЙ автоматики:
- Нет init(), probe(), send() со стейт-машиной
- Нет классификации ответов
- Нет адаптивных таймингов
ТОЛЬКО вы вводите команду — ELM отвечает сырыми байтами.
ЗАПУСК:
python tools/elm_console.py # порт по умолчанию /dev/rfcomm0
python tools/elm_console.py --port /dev/rfcomm0 # явно указать порт
python tools/elm_console.py --baud 38400 # другая скорость
python tools/elm_console.py --timeout 1000 # таймаут 1с
python tools/elm_console.py --no-init # не слать AT-инит
КОМАНДЫ КОНСОЛИ:
ATZ — отправить "ATZ" в ELM
0105 — отправить "0105" (PID coolant temp)
!drain — очистить входной буфер
!timeout 2000 — установить таймаут 2000 мс
!delay 0.5 — пауза между командой и чтением (сек)
!log — показать историю команд
!available — сколько байт в буфере
!save file.json — сохранить лог в файл
!help — справка
!quit — выход
ЦЕЛЬ:
Понять, КАК на самом деле работает ELM327.
Почему статика работает, а динамика ломается?
Ответ — в сырых байтах.
"""
import argparse
import cmd
import json
import logging
import sys
import time
from pathlib import Path
# Добавляем корень проекта в PYTHONPATH
sys.path.insert(0, str(Path(__file__).parent.parent))
from obd.connection import SerialTransport
from obd.raw_console import RawELM, format_response, format_log
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(name)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("elm.console")
class ElmConsole(cmd.Cmd):
"""Интерактивная консоль ELM327."""
intro = """
╔══════════════════════════════════════════════════════╗
║ ELM327 Raw Console ║
║ Сырое взаимодействие — без стейт-машины ║
║ Команды: ATZ, 0105, !drain, !help, !quit ║
╚══════════════════════════════════════════════════════╝
"""
prompt = "\nelm> "
def __init__(self, port: str, baudrate: int, timeout: int, delay: float, no_init: bool):
super().__init__()
self._port = port
self._baud = baudrate
self._no_init = no_init
self._transport = None
self._elm: RawELM | None = None
# ── Подключение ─────────────────────────────────
def connect(self):
"""Открыть порт и создать RawELM."""
print(f"🔌 Подключение к {self._port} @ {self._baud}...")
try:
self._transport = SerialTransport(self._port, self._baud)
self._transport.connect()
except Exception as e:
print(f"❌ Не удалось открыть порт: {e}")
print(" Проверь: bash scripts/setup-bt.sh")
return False
self._elm = RawELM(self._transport)
print(f"✅ Порт открыт. RawELM готов.")
print(f" Таймаут: {self._elm.timeout}ms, пауза: {self._elm.inter_delay}s")
print(f" Буфер: {self._elm.available()} байт")
if not self._no_init:
print("\n📡 Быстрая проверка связи (ATZ)...")
r = self._elm.send("ATZ", timeout=3000)
print(format_response(r))
if r["error"]:
print("⚠️ ELM327 не ответил на ATZ. Проверь питание адаптера.")
print(" Продолжаем, но команды могут не работать.")
return True
def close(self):
"""Закрыть порт."""
if self._transport:
self._transport.close()
print("🔌 Порт закрыт.")
# ── cmd.Cmd overrides ────────────────────────────
def default(self, line: str):
"""Любая не-! команда = отправить в ELM327."""
cmd_str = line.strip()
if not cmd_str:
return
if cmd_str.startswith("!"):
print(f"Неизвестная команда: {cmd_str}. !help для списка.")
return
# Отправить в ELM
result = self._elm.send(cmd_str)
print(format_response(result))
def emptyline(self):
"""Пустая строка — ничего не делаем."""
pass
# ── Специальные команды (!) ──────────────────────
def do_drain(self, arg):
"""!drain — очистить входной буфер ELM327."""
r = self._elm.drain()
if r["bytes"] > 0:
print(f"🗑 Выброшено {r['bytes']} байт: {r['drained']!r}")
else:
print("✅ Буфер пуст.")
def do_timeout(self, arg):
"""!timeout <ms> — установить таймаут чтения."""
try:
ms = int(arg.strip())
self._elm.timeout = ms
print(f"⏱ Таймаут: {ms}ms")
except ValueError:
print(f"❌ Нужно число: !timeout 2000")
def do_delay(self, arg):
"""!delay <sec> — пауза между командой и чтением."""
try:
sec = float(arg.strip())
self._elm.inter_delay = sec
print(f"⏱ Пауза: {sec}s")
except ValueError:
print(f"❌ Нужно число: !delay 0.5")
def do_log(self, arg):
"""!log [N] — показать последние N команд (по умолчанию все)."""
entries = self._elm.log
if not entries:
print("📭 Лог пуст.")
return
try:
n = int(arg.strip()) if arg.strip() else len(entries)
except ValueError:
n = len(entries)
to_show = entries[-n:] if n < len(entries) else entries
print(format_log(to_show))
print(f"Всего: {len(entries)} команд.")
def do_available(self, arg):
"""!available — сколько байт в буфере."""
n = self._elm.available()
if n < 0:
print("⚠️ Не удалось проверить буфер (порт закрыт?).")
elif n == 0:
print("✅ Буфер пуст.")
else:
print(f"📥 В буфере: {n} байт.")
def do_save(self, arg):
"""!save <file.json> — сохранить лог в JSON."""
path = arg.strip()
if not path:
print("❌ Укажи имя файла: !save log.json")
return
try:
with open(path, "w") as f:
json.dump(self._elm.log, f, indent=2, default=str)
print(f"💾 Сохранено: {path} ({len(self._elm.log)} команд)")
except Exception as e:
print(f"❌ Ошибка: {e}")
def do_raw(self, arg):
"""!raw — показать последний ответ в repr (все символы)."""
last = self._elm.last()
if not last:
print("📭 Нет команд.")
return
print(f"raw = {last['raw']!r}")
print(f"prompt = {last['prompt']}")
print(f"elapsed = {last['elapsed_ms']}ms")
print(f"bytes = {last['bytes']}")
def do_help(self, arg):
"""!help — справка."""
print("""
╔══════════════════════════════════════════════════════╗
║ КОМАНДЫ ELM327 (вводи как есть): ║
║ ATZ — сброс ║
║ ATI — идентификация ║
║ ATE0 — echo off ║
║ ATL0 — linefeeds off ║
║ ATS0 — spaces off ║
║ ATH1 — headers on ║
║ ATSP0 — авто-протокол ║
║ ATRV — напряжение ║
║ ATDPN — номер протокола ║
║ 0105 — температура ОЖ (PID) ║
║ 010C — обороты ║
║ 010D — скорость ║
║ 03 — сохранённые DTC ║
║ 07 — pending DTC ║
║ 0902 — VIN ║
║ ║
║ КОМАНДЫ КОНСОЛИ (с !): ║
║ !drain — очистить буфер ║
║ !timeout N — таймаут (ms) ║
║ !delay N — пауза перед чтением (s) ║
║ !log [N] — история команд ║
║ !raw — последний ответ в repr ║
║ !available — байт в буфере ║
║ !save f.json — сохранить лог ║
║ !help — эта справка ║
║ !quit — выход ║
╚══════════════════════════════════════════════════════╝
""")
def do_quit(self, arg):
"""!quit — выход."""
print("👋")
self.close()
return True
def do_exit(self, arg):
"""!exit — то же что !quit."""
return self.do_quit(arg)
# Сокращения
do_q = do_quit
do_h = do_help
do_d = do_drain
do_t = do_timeout
do_l = do_log
do_a = do_available
# ── main ─────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="ELM327 Raw Console — интерактивное сырое взаимодействие"
)
parser.add_argument("--port", default="/dev/rfcomm0", help="Порт (default: /dev/rfcomm0)")
parser.add_argument("--baud", type=int, default=38400, help="Скорость (default: 38400)")
parser.add_argument("--timeout", type=int, default=500, help="Таймаут чтения ms (default: 500)")
parser.add_argument("--delay", type=float, default=0.05, help="Пауза перед чтением s (default: 0.05)")
parser.add_argument("--no-init", action="store_true", help="Не слать ATZ при старте")
args = parser.parse_args()
console = ElmConsole(
port=args.port,
baudrate=args.baud,
timeout=args.timeout,
delay=args.delay,
no_init=args.no_init,
)
if not console.connect():
sys.exit(1)
try:
console.cmdloop()
except KeyboardInterrupt:
print("\n👋")
finally:
console.close()
if __name__ == "__main__":
main()