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: версии
This commit is contained in:
+55
-122
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
obd/protocol.py — ELM327 стейт-машина (AndrOBD).
|
||||
|
||||
Точная копия логики из AndrOBD (ElmProt.java, github.com/fr3ts0n/AndrOBD).
|
||||
Основана на AndrOBD (ElmProt.java, github.com/fr3ts0n/AndrOBD).
|
||||
|
||||
## Архитектура
|
||||
┌──────────┐ команда ┌──────────┐
|
||||
@@ -16,26 +16,26 @@ obd/protocol.py — ELM327 стейт-машина (AndrOBD).
|
||||
│ восстановление ▲
|
||||
└─────────────────────┘
|
||||
|
||||
## Зависимости
|
||||
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")
|
||||
rpm = elm.send("010C")
|
||||
elm.close()
|
||||
|
||||
## Ключевые особенности
|
||||
- Байт-за-байтом чтение с 1мс поллингом
|
||||
- `>` как разделитель ответов (промпт ELM327)
|
||||
- Адаптивный таймаут (200мс ± 4мс, ATST)
|
||||
- Восстановление после BUS ERROR (ATPC → ATSP0)
|
||||
- Не тот ответ → переход в ERROR → восстановление
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from obd.connection import SerialTransport
|
||||
from obd.state import State, Rsp
|
||||
from obd.timing import AdaptiveTiming
|
||||
|
||||
@@ -43,11 +43,11 @@ logger = logging.getLogger("androbd")
|
||||
|
||||
|
||||
class AndrOBD:
|
||||
"""Стейт-машина ELM327 — 1:1 копия AndrOBD (ElmProt.java).
|
||||
"""Стейт-машина ELM327.
|
||||
|
||||
Управляет жизненным циклом ELM327:
|
||||
1. connect() — открыть serial/Bluetooth порт
|
||||
2. init() — инициализация (ATSP0, ATAT1, ATST, ATS0, ATL0, ATE0)
|
||||
2. init() — базовая инициализация (уровень 0)
|
||||
3. send(cmd) — отправить OBD-команду, получить ответ
|
||||
4. close() — закрыть порт
|
||||
|
||||
@@ -55,60 +55,58 @@ class AndrOBD:
|
||||
"""
|
||||
|
||||
INIT_TMO = 10000 # мс — таймаут для команд инициализации
|
||||
DEF_TMO = 200 # мс — начальный таймаут (заменяется AdaptiveTiming)
|
||||
DEF_TMO = 200 # мс — начальный таймаут
|
||||
|
||||
def __init__(self, port: str, baudrate: int = 38400):
|
||||
"""port — устройство (напр. /dev/rfcomm0), baudrate — скорость."""
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self._ser = None
|
||||
self._transport = SerialTransport(port, baudrate)
|
||||
self._timing = AdaptiveTiming()
|
||||
self._state = State.UNDEFINED
|
||||
self._last_cmd: Optional[str] = None
|
||||
|
||||
def connect(self):
|
||||
"""Открыть serial-соединение с ELM327."""
|
||||
import serial
|
||||
self._ser = serial.Serial(
|
||||
port=self.port, baudrate=self.baudrate, timeout=0.1,
|
||||
bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE)
|
||||
time.sleep(0.5)
|
||||
logger.info(f"AndrOBD: connected {self.port}")
|
||||
self._transport.connect()
|
||||
logger.info(f"AndrOBD: connected {self._transport.port}")
|
||||
|
||||
def close(self):
|
||||
"""Закрыть serial-соединение."""
|
||||
if self._ser and self._ser.is_open:
|
||||
self._ser.close()
|
||||
self._transport.close()
|
||||
|
||||
def init(self):
|
||||
"""Инициализация ELM327 — 6 AT-команд.
|
||||
"""Базовая инициализация ELM327 (уровень 0 — все клоны).
|
||||
|
||||
ATSP0 — авто-протокол
|
||||
ATAT1 — адаптивный таймаут вкл
|
||||
ATSTxx — установить таймаут
|
||||
ATS0 — без пробелов в ответах
|
||||
ATL0 — без перевода строки
|
||||
ATE0 — без эха
|
||||
ТОЛЬКО команды которые есть у ВСЕХ клонов:
|
||||
ATE0 ATL0 ATS0 ATH1 ATSP0
|
||||
"""
|
||||
logger.info("AndrOBD: init")
|
||||
logger.info("AndrOBD: init (L0)")
|
||||
self._state = State.INITIALIZING
|
||||
self._exec("ATSP0", self.INIT_TMO)
|
||||
self._exec("ATAT1", self.DEF_TMO * 5)
|
||||
self._update_atst()
|
||||
self._exec("ATS0", self.DEF_TMO * 5)
|
||||
self._exec("ATL0", self.DEF_TMO * 5)
|
||||
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")
|
||||
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-команду и получить ответ.
|
||||
|
||||
cmd — команда (напр. '0105', '0902', '03').
|
||||
Возвращает сырой ответ ELM327.
|
||||
При ошибке — авто-восстановление.
|
||||
"""
|
||||
"""Отправить OBD-команду и получить ответ."""
|
||||
if self._state == State.ERROR:
|
||||
self._recover()
|
||||
self._state = State.BUSY
|
||||
@@ -117,17 +115,10 @@ class AndrOBD:
|
||||
self._state = State.READY
|
||||
return result
|
||||
|
||||
# ── Приватные методы ──────────────────────────────────
|
||||
# ── Приватные методы ──────────────────────────────
|
||||
|
||||
def _exec(self, cmd: str, timeout: int) -> str:
|
||||
"""Выполнить команду ELM327 с ретраями (до 10 попыток).
|
||||
|
||||
Архитектура AndrOBD: ELM327 может не ответить, если:
|
||||
- Адаптер ещё не инициализирован (SEARCHING)
|
||||
- ЭБУ медленно отвечает (увеличиваем AdaptiveTiming)
|
||||
- Плохой контакт / потеря связи (Busy→Timeout→Error)
|
||||
10 ретраев — жёсткий лимит из AndrOBD, после — ERROR.
|
||||
"""
|
||||
"""Выполнить команду с ретраями (до 10)."""
|
||||
self._last_cmd = cmd
|
||||
self._write(cmd)
|
||||
t = timeout
|
||||
@@ -135,11 +126,9 @@ class AndrOBD:
|
||||
try:
|
||||
return self._handle(self._read(t))
|
||||
except TimeoutError:
|
||||
# Инициализация: каждый ретрай +1с (ELM может быть занят init)
|
||||
if self._state == State.INITIALIZING:
|
||||
t += 1000
|
||||
else:
|
||||
# Активный режим: растём таймаут адаптивно
|
||||
self._timing.increase()
|
||||
t = self._timing.ms
|
||||
logger.error(f"AndrOBD: no response for {cmd}")
|
||||
@@ -147,10 +136,7 @@ class AndrOBD:
|
||||
return ""
|
||||
|
||||
def _handle(self, raw: str) -> str:
|
||||
"""Обработать ответ ELM327: классификация → обновление таймаута.
|
||||
|
||||
Возвращает raw как есть — декодирование данных на уровень выше.
|
||||
"""
|
||||
"""Обработать ответ ELM327."""
|
||||
t = Rsp.identify(raw)
|
||||
|
||||
if t == Rsp.SEARCHING:
|
||||
@@ -159,26 +145,20 @@ class AndrOBD:
|
||||
self._timing.decrease()
|
||||
return raw
|
||||
if t == Rsp.NODATA:
|
||||
# NODATA = ЭБУ жив, но не может ответить (например, PID недоступен)
|
||||
# Увеличиваем таймаут — возможно ЭБУ медленный
|
||||
self._timing.increase()
|
||||
self._update_atst()
|
||||
return raw
|
||||
|
||||
# BUS ERROR — сброс протокола
|
||||
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._update_atst()
|
||||
self._write("ATPC") # закрыть протокол
|
||||
self._write("ATPC")
|
||||
self._try_read()
|
||||
self._write("ATSP0") # переоткрыть авто-протокол
|
||||
self._write("ATSP0")
|
||||
self._try_read()
|
||||
return raw
|
||||
|
||||
# Другие ошибки — warm start
|
||||
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
|
||||
@@ -186,13 +166,11 @@ class AndrOBD:
|
||||
self._try_read()
|
||||
return raw
|
||||
|
||||
# Данные — успешный ответ
|
||||
self._timing.decrease()
|
||||
return raw
|
||||
|
||||
def _recover(self):
|
||||
"""Восстановление после ошибки: ATWS → ATSP0 → ATE0."""
|
||||
"""Восстановление после ошибки: ATWS → ATSP0 → ATE0."""
|
||||
"""Восстановление после ошибки."""
|
||||
logger.info("AndrOBD: recovering...")
|
||||
self._state = State.INITIALIZING
|
||||
self._write("ATWS")
|
||||
@@ -204,59 +182,14 @@ class AndrOBD:
|
||||
self._state = State.READY
|
||||
|
||||
def _write(self, cmd: str):
|
||||
"""Отправить команду в ELM327: сброс буфера → write + CR → flush."""
|
||||
self._ser.reset_input_buffer() # сбросить хвосты предыдущего ответа
|
||||
self._ser.write((cmd + "\r").encode())
|
||||
self._ser.flush()
|
||||
logger.debug(f"AndrOBD → {cmd}")
|
||||
"""Отправить команду в ELM327."""
|
||||
self._transport.write(cmd)
|
||||
|
||||
def _read(self, timeout_ms: int) -> str:
|
||||
"""Прочитать ответ ELM327 байт-за-байтом.
|
||||
|
||||
Читает до символа `>` (промпт) или до таймаута.
|
||||
Возвращает сырой ответ без `>`.
|
||||
"""
|
||||
dl = time.monotonic() + timeout_ms / 1000.0
|
||||
lines, cur = [], []
|
||||
got_prompt = False
|
||||
while time.monotonic() < dl:
|
||||
if self._ser.in_waiting > 0:
|
||||
ch = self._ser.read(1)
|
||||
if not ch:
|
||||
continue
|
||||
cp = ch[0]
|
||||
if cp == 62: # '>' — промпт ELM327
|
||||
self._push(cur, lines)
|
||||
got_prompt = True
|
||||
break
|
||||
elif cp == 13: # CR — конец строки
|
||||
self._push(cur, lines)
|
||||
elif cp in (10, 32): # LF и пробел — игнорируем
|
||||
pass
|
||||
else:
|
||||
cur.append(chr(cp))
|
||||
else:
|
||||
time.sleep(0.001) # поллинг 1мс
|
||||
self._push(cur, lines)
|
||||
if not got_prompt:
|
||||
raise TimeoutError(f"timeout {timeout_ms}ms")
|
||||
return "\n".join(lines)
|
||||
"""Прочитать ответ ELM327."""
|
||||
return self._transport.read(timeout_ms)
|
||||
|
||||
def _try_read(self, timeout: int = 5000):
|
||||
"""Прочитать и проигнорировать ответ (для команд восстановления)."""
|
||||
try:
|
||||
self._read(timeout)
|
||||
except TimeoutError:
|
||||
pass
|
||||
"""Прочитать и проигнорировать ответ."""
|
||||
self._transport.try_read(timeout)
|
||||
|
||||
@staticmethod
|
||||
def _push(cur, lines):
|
||||
"""Добавить накопленные байты как строку в lines."""
|
||||
if cur:
|
||||
lines.append("".join(cur))
|
||||
cur.clear()
|
||||
|
||||
def _update_atst(self):
|
||||
"""Отправить ATST с текущим значением адаптивного таймаута."""
|
||||
self._write(f"ATST{self._timing.atst:02X}")
|
||||
self._try_read()
|
||||
|
||||
Reference in New Issue
Block a user