Files
elmer/obd/protocol.py
T
“Naeel” aa37a1a70c v0.48.0 — пробинг ELM327, трехуровневый профиль, рефакторинг obd/
- obd/probe.py: трехуровневый каскад (L0/L1/L2)
- obd/commands.py: каталог всех AT-команд с метаданными
- obd/classifier.py: классификация ответов + определение уровня
- obd/connection.py: транспортный слой (SerialTransport)
- obd/protocol.py: init() только база, без ATAT1/ATST
- api/db.py: таблица device_profiles по BT MAC
- api/scripts.py: три уровня скриптов (l0/l1/l2)
- api/routes.py: /elm/probe, /elm/profile/<mac>, /script?level=
- web/templates/index.html: v0.48.0
- CHANGELOG.md, doc/architecture.md, resume.txt: версии
2026-06-07 05:55:02 +04:00

196 lines
7.2 KiB
Python
Raw 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/protocol.py — ELM327 стейт-машина (AndrOBD).
Основана на AndrOBD (ElmProt.java, github.com/fr3ts0n/AndrOBD).
## Архитектура
┌──────────┐ команда ┌──────────┐
│ READY │──────────────▶│ BUSY │
└──────────┘ └────┬─────┘
▲ │ ответ получен
│ ┌────────────────┘
│ ▼
┌────┴─────┐ ошибка ┌──────────┐
│ ERROR │◀─────────│ (любое) │
└────┬─────┘ └──────────┘
│ восстановление ▲
└─────────────────────┘
## Зависимости
obd/connection.py — транспорт (SerialTransport)
obd/state.py — состояния/ответы (State, Rsp)
obd/timing.py — адаптивный таймаут (AdaptiveTiming)
obd/commands.py — каталог команд
obd/classifier.py — классификация ответов
## Использование
elm = AndrOBD("/dev/rfcomm0", 38400)
elm.connect()
elm.init()
vin = elm.send("0902")
elm.close()
"""
import logging
import time
from typing import Optional
from obd.connection import SerialTransport
from obd.state import State, Rsp
from obd.timing import AdaptiveTiming
logger = logging.getLogger("androbd")
class AndrOBD:
"""Стейт-машина ELM327.
Управляет жизненным циклом ELM327:
1. connect() — открыть serial/Bluetooth порт
2. init() — базовая инициализация (уровень 0)
3. send(cmd) — отправить OBD-команду, получить ответ
4. close() — закрыть порт
Автоматически обрабатывает: таймауты, BUS ERROR, восстановление.
"""
INIT_TMO = 10000 # мс — таймаут для команд инициализации
DEF_TMO = 200 # мс — начальный таймаут
def __init__(self, port: str, baudrate: int = 38400):
self._transport = SerialTransport(port, baudrate)
self._timing = AdaptiveTiming()
self._state = State.UNDEFINED
self._last_cmd: Optional[str] = None
def connect(self):
"""Открыть serial-соединение с ELM327."""
self._transport.connect()
logger.info(f"AndrOBD: connected {self._transport.port}")
def close(self):
"""Закрыть serial-соединение."""
self._transport.close()
def init(self):
"""Базовая инициализация ELM327 (уровень 0 — все клоны).
ТОЛЬКО команды которые есть у ВСЕХ клонов:
ATE0 ATL0 ATS0 ATH1 ATSP0
"""
logger.info("AndrOBD: init (L0)")
self._state = State.INITIALIZING
self._exec("ATE0", self.DEF_TMO * 5)
self._exec("ATL0", self.DEF_TMO * 5)
self._exec("ATS0", self.DEF_TMO * 5)
self._exec("ATH1", self.DEF_TMO * 5)
self._exec("ATSP0", self.INIT_TMO)
self._state = State.READY
logger.info("AndrOBD: ready (L0)")
def init_l1(self):
"""Инициализация уровня 1: база + адаптивный тайминг."""
logger.info("AndrOBD: init L1 (+ATAT1)")
self._state = State.INITIALIZING
self._exec("ATAT1", self.DEF_TMO * 5)
self._state = State.READY
logger.info("AndrOBD: ready (L1)")
def init_l2(self):
"""Инициализация уровня 2: L1 + CAN автоформат + flow control."""
logger.info("AndrOBD: init L2 (+ATCAF1 +ATCFC1)")
self._state = State.INITIALIZING
self._exec("ATCAF1", self.DEF_TMO * 5)
self._exec("ATCFC1", self.DEF_TMO * 5)
self._state = State.READY
logger.info("AndrOBD: ready (L2)")
def send(self, cmd: str) -> str:
"""Отправить OBD-команду и получить ответ."""
if self._state == State.ERROR:
self._recover()
self._state = State.BUSY
result = self._exec(cmd, self._timing.ms)
if self._state == State.BUSY:
self._state = State.READY
return result
# ── Приватные методы ──────────────────────────────
def _exec(self, cmd: str, timeout: int) -> str:
"""Выполнить команду с ретраями (до 10)."""
self._last_cmd = cmd
self._write(cmd)
t = timeout
for _ in range(10):
try:
return self._handle(self._read(t))
except TimeoutError:
if self._state == State.INITIALIZING:
t += 1000
else:
self._timing.increase()
t = self._timing.ms
logger.error(f"AndrOBD: no response for {cmd}")
self._state = State.ERROR
return ""
def _handle(self, raw: str) -> str:
"""Обработать ответ ELM327."""
t = Rsp.identify(raw)
if t == Rsp.SEARCHING:
return raw
if t == Rsp.OK:
self._timing.decrease()
return raw
if t == Rsp.NODATA:
self._timing.increase()
return raw
if t in (Rsp.UNABLE, Rsp.BUS_BUSY, Rsp.BUS_ERROR,
Rsp.CAN_ERROR, Rsp.BUS_INIT, Rsp.STOPPED):
logger.warning(f"AndrOBD: BUS ERROR ({t})")
self._state = State.DISCONNECTED
self._timing.reset()
self._write("ATPC")
self._try_read()
self._write("ATSP0")
self._try_read()
return raw
if t in (Rsp.ERROR, Rsp.DATA_ERROR, Rsp.BUFFER_FULL, Rsp.RX_ERROR):
logger.warning(f"AndrOBD: {t} — warm start")
self._state = State.ERROR
self._write("ATWS")
self._try_read()
return raw
self._timing.decrease()
return raw
def _recover(self):
"""Восстановление после ошибки."""
logger.info("AndrOBD: recovering...")
self._state = State.INITIALIZING
self._write("ATWS")
self._try_read()
self._write("ATSP0")
self._try_read()
self._write("ATE0")
self._try_read()
self._state = State.READY
def _write(self, cmd: str):
"""Отправить команду в ELM327."""
self._transport.write(cmd)
def _read(self, timeout_ms: int) -> str:
"""Прочитать ответ ELM327."""
return self._transport.read(timeout_ms)
def _try_read(self, timeout: int = 5000):
"""Прочитать и проигнорировать ответ."""
self._transport.try_read(timeout)