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:
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
obd/classifier.py — Классификация ответов ELM327 и определение уровня.
|
||||
|
||||
Отдельный сервис:
|
||||
- classify(raw) → (tag: str, is_ok: bool)
|
||||
- determine_level(responses: dict) → int (-1/0/1/2)
|
||||
|
||||
Использует obd/commands.py для списков команд по уровням.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from obd.state import Rsp
|
||||
from obd.commands import L0_NAMES, L1_NAMES, L2_NAMES
|
||||
|
||||
logger = logging.getLogger("elmer.classifier")
|
||||
|
||||
|
||||
def classify(raw: str) -> Tuple[str, bool]:
|
||||
"""Классифицирует сырой ответ ELM327.
|
||||
|
||||
Returns:
|
||||
(tag, is_ok)
|
||||
tag: Rsp.OK / Rsp.ERROR / Rsp.UNKNOWN / ...
|
||||
is_ok: True если устройство ответило нормально (OK или данные)
|
||||
"""
|
||||
tag = Rsp.identify(raw)
|
||||
is_ok = tag in (Rsp.OK, Rsp.UNKNOWN, Rsp.NODATA, Rsp.SEARCHING)
|
||||
return (tag, is_ok)
|
||||
|
||||
|
||||
def determine_level(responses: Dict[str, str]) -> dict:
|
||||
"""Определяет уровень устройства по ответам на пробинг-команды.
|
||||
|
||||
Args:
|
||||
responses: {cmd_name: raw_response} — ответы на команды пробинга.
|
||||
|
||||
Returns:
|
||||
Профиль: {level, elm_version, protocol, voltage, supported, unsupported, errors}
|
||||
"""
|
||||
result: dict = {
|
||||
"level": -1,
|
||||
"elm_version": None,
|
||||
"protocol": None,
|
||||
"voltage": None,
|
||||
"supported": [],
|
||||
"unsupported": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
# ── Уровень 0 ──────────────────────────────────
|
||||
l0_ok = True
|
||||
for cmd in L0_NAMES:
|
||||
raw = responses.get(cmd, "")
|
||||
tag, ok = classify(raw)
|
||||
if ok and raw:
|
||||
result["supported"].append(cmd)
|
||||
if cmd == "ATI":
|
||||
result["elm_version"] = raw.strip()
|
||||
elif cmd == "ATDPN":
|
||||
result["protocol"] = raw.strip()
|
||||
elif cmd == "ATRV":
|
||||
result["voltage"] = raw.strip()
|
||||
else:
|
||||
result["unsupported"].append(cmd)
|
||||
result["errors"].append(f"{cmd}: {tag if tag else 'no response'}")
|
||||
l0_ok = False
|
||||
|
||||
if not l0_ok:
|
||||
logger.warning("classifier: L0 failed")
|
||||
return result
|
||||
|
||||
result["level"] = 0
|
||||
|
||||
# ── Уровень 1 ──────────────────────────────────
|
||||
l1_ok = True
|
||||
for cmd in L1_NAMES:
|
||||
raw = responses.get(cmd, "")
|
||||
tag, ok = classify(raw)
|
||||
if ok and raw:
|
||||
result["supported"].append(cmd)
|
||||
else:
|
||||
result["unsupported"].append(cmd)
|
||||
l1_ok = False
|
||||
|
||||
if not l1_ok:
|
||||
return result
|
||||
|
||||
result["level"] = 1
|
||||
|
||||
# ── Уровень 2 ──────────────────────────────────
|
||||
l2_ok = True
|
||||
for cmd in L2_NAMES:
|
||||
raw = responses.get(cmd, "")
|
||||
tag, ok = classify(raw)
|
||||
if ok and raw:
|
||||
result["supported"].append(cmd)
|
||||
else:
|
||||
result["unsupported"].append(cmd)
|
||||
l2_ok = False
|
||||
|
||||
if l2_ok:
|
||||
result["level"] = 2
|
||||
|
||||
logger.info(f"classifier: level={result['level']}")
|
||||
return result
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
obd/commands.py — Каталог AT-команд ELM327.
|
||||
|
||||
Все известные команды ELM327 с метаданными:
|
||||
- name: имя команды (напр. "ATE0")
|
||||
- desc: краткое описание
|
||||
- level: минимальный уровень устройства (0/1/2)
|
||||
- safe: можно ли слать при пробинге (False для опасных)
|
||||
|
||||
Принцип:
|
||||
- Нет в профиле → не слать.
|
||||
- Опасные команды (меняют baud rate, мониторинг) — никогда не пробингуются.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class AtCommand:
|
||||
"""AT-команда ELM327 с метаданными."""
|
||||
name: str # "ATE0"
|
||||
desc: str # "Echo off"
|
||||
level: int = 0 # минимальный уровень (0=все, 1=ATAT1, 2=CAF1/CFC1)
|
||||
safe: bool = True # можно ли пробинговать
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Уровень 0 — Универсальные (есть у ВСЕХ клонов)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
CMDS_L0: List[AtCommand] = [
|
||||
AtCommand("ATE0", "Echo off"),
|
||||
AtCommand("ATE1", "Echo on", safe=False), # не слать — вернём эхо
|
||||
AtCommand("ATL0", "Linefeeds off"),
|
||||
AtCommand("ATL1", "Linefeeds on", safe=False),
|
||||
AtCommand("ATS0", "Spaces off"),
|
||||
AtCommand("ATS1", "Spaces on", safe=False),
|
||||
AtCommand("ATH0", "Headers off"),
|
||||
AtCommand("ATH1", "Headers on"),
|
||||
AtCommand("ATSP0", "Auto protocol"),
|
||||
AtCommand("ATDPN", "Describe protocol number"),
|
||||
AtCommand("ATRV", "Read voltage"),
|
||||
AtCommand("ATI", "Identify"),
|
||||
AtCommand("ATWS", "Warm start", safe=False), # сбрасывает состояние
|
||||
AtCommand("ATPC", "Protocol close", safe=False),
|
||||
AtCommand("ATR0", "Responses off", safe=False),
|
||||
AtCommand("ATR1", "Responses on"),
|
||||
AtCommand("ATNL", "Normalize length"),
|
||||
AtCommand("ATAL", "Allow long messages"),
|
||||
AtCommand("ATAR", "Auto receive", safe=False), # включает авто-приём
|
||||
AtCommand("ATSW", "Stop waiting"),
|
||||
AtCommand("ATFE", "Forget events"),
|
||||
AtCommand("ATSI", "Slow init"),
|
||||
AtCommand("ATFI", "Fast init"),
|
||||
AtCommand("ATKW", "Keyword"),
|
||||
AtCommand("ATTA", "Tester address"),
|
||||
AtCommand("ATTP", "Try protocol"),
|
||||
AtCommand("ATWM", "Wakeup message"),
|
||||
AtCommand("ATIGN", "Ignore"),
|
||||
AtCommand("ATIFR", "IFR"),
|
||||
AtCommand("ATIIA", "ISO init address"),
|
||||
AtCommand("ATRD", "Read data"),
|
||||
AtCommand("ATCS", "CAN silent"),
|
||||
AtCommand("ATCV", "Calibrate voltage"),
|
||||
AtCommand("ATD0", "Display off"),
|
||||
AtCommand("ATM0", "Memory off"),
|
||||
AtCommand("ATM1", "Memory on"),
|
||||
]
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Уровень 1 — Хорошие клоны и оригиналы
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
CMDS_L1: List[AtCommand] = [
|
||||
AtCommand("ATAT0", "Adaptive timing off", level=1),
|
||||
AtCommand("ATAT1", "Adaptive timing on", level=1),
|
||||
AtCommand("ATAT2", "Adaptive timing aggressive", level=1),
|
||||
]
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# Уровень 2 — Только настоящий ELM327
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
CMDS_L2: List[AtCommand] = [
|
||||
AtCommand("ATCAF0", "CAN auto formatting off", level=2),
|
||||
AtCommand("ATCAF1", "CAN auto formatting on", level=2),
|
||||
AtCommand("ATCFC0", "CAN flow control off", level=2),
|
||||
AtCommand("ATCFC1", "CAN flow control on", level=2),
|
||||
AtCommand("ATCM", "CAN mask", level=2),
|
||||
AtCommand("ATCRA", "CAN receive address", level=2),
|
||||
AtCommand("ATCP", "CAN priority", level=2),
|
||||
AtCommand("ATSH", "Set header", level=2),
|
||||
AtCommand("AT@1", "Device description", level=2),
|
||||
AtCommand("AT@2", "Device identifier", level=2),
|
||||
AtCommand("ATJE", "J1939 ELM", level=2),
|
||||
AtCommand("ATJS", "J1939 setup", level=2),
|
||||
AtCommand("ATIB", "ISO baud", level=2, safe=False),
|
||||
]
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# ОПАСНЫЕ — никогда не пробинговать
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
CMDS_DANGEROUS: List[AtCommand] = [
|
||||
AtCommand("ATZ", "Hard reset", safe=False),
|
||||
AtCommand("ATD", "Set defaults", safe=False),
|
||||
AtCommand("ATBD", "Baud rate", safe=False),
|
||||
AtCommand("ATBRD", "Baud rate divisor", safe=False),
|
||||
AtCommand("ATMA", "Monitor all", safe=False),
|
||||
AtCommand("ATMR", "Monitor receiver", safe=False),
|
||||
AtCommand("ATMT", "Monitor transmitter", safe=False),
|
||||
]
|
||||
|
||||
|
||||
# ── Хелперы ─────────────────────────────────────────────
|
||||
|
||||
def get_commands_for_level(level: int) -> List[str]:
|
||||
"""Возвращает имена команд, доступных на заданном уровне."""
|
||||
result = [c.name for c in CMDS_L0 if c.safe]
|
||||
if level >= 1:
|
||||
result += [c.name for c in CMDS_L1 if c.safe]
|
||||
if level >= 2:
|
||||
result += [c.name for c in CMDS_L2 if c.safe]
|
||||
return result
|
||||
|
||||
|
||||
def get_probe_commands() -> dict:
|
||||
"""Возвращает команды для пробинга, сгруппированные по уровням.
|
||||
|
||||
Returns: {"L0": ["ATE0",...], "L1": ["ATAT1",...], "L2": ["ATCAF1",...]}
|
||||
"""
|
||||
return {
|
||||
"L0": [c.name for c in CMDS_L0 if c.safe and c.level == 0][:8],
|
||||
"L1": [c.name for c in CMDS_L1 if c.safe],
|
||||
"L2": [c.name for c in CMDS_L2 if c.safe],
|
||||
}
|
||||
|
||||
|
||||
# Плоские списки для импорта (обратная совместимость)
|
||||
L0_NAMES = [c.name for c in CMDS_L0 if c.safe and c.level == 0]
|
||||
L1_NAMES = [c.name for c in CMDS_L1 if c.safe]
|
||||
L2_NAMES = [c.name for c in CMDS_L2 if c.safe]
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
obd/connection.py — Транспортный слой ELM327 (serial/Bluetooth).
|
||||
|
||||
Чтение байт-за-байтом с поллингом 1мс, запись + flush.
|
||||
Выделен из protocol.py чтобы разделить I/O и логику протокола.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
logger = logging.getLogger("elm.connection")
|
||||
|
||||
|
||||
class SerialTransport:
|
||||
"""Транспорт: открыть порт, читать/писать байты, закрыть.
|
||||
|
||||
Не знает протокол. Только I/O.
|
||||
"""
|
||||
|
||||
def __init__(self, port: str, baudrate: int = 38400):
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self._ser = None
|
||||
|
||||
def connect(self):
|
||||
"""Открыть serial-соединение."""
|
||||
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"SerialTransport: connected {self.port}")
|
||||
|
||||
def close(self):
|
||||
"""Закрыть serial-соединение."""
|
||||
if self._ser and self._ser.is_open:
|
||||
self._ser.close()
|
||||
logger.info("SerialTransport: closed")
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
"""Открыт ли порт."""
|
||||
return self._ser is not None and self._ser.is_open
|
||||
|
||||
def write(self, data: str):
|
||||
"""Отправить данные в ELM: data + CR → flush."""
|
||||
self._ser.write((data + "\r").encode())
|
||||
self._ser.flush()
|
||||
logger.debug(f"→ {data}")
|
||||
|
||||
def read(self, timeout_ms: int) -> str:
|
||||
"""Прочитать ответ ELM327 байт-за-байтом.
|
||||
|
||||
Читает до символа '>' (промпт) или до таймаута.
|
||||
Возвращает сырой ответ без '>'.
|
||||
|
||||
Raises:
|
||||
TimeoutError: если '>' не получен за timeout_ms.
|
||||
"""
|
||||
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"read timeout {timeout_ms}ms")
|
||||
return "\n".join(lines)
|
||||
|
||||
def try_read(self, timeout: int = 5000):
|
||||
"""Прочитать и проигнорировать ответ."""
|
||||
try:
|
||||
self.read(timeout)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _push(cur, lines):
|
||||
"""Добавить накопленные байты как строку в lines."""
|
||||
if cur:
|
||||
lines.append("".join(cur))
|
||||
cur.clear()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
obd/probe.py — Пробинг ELM327: определение уровня устройства.
|
||||
|
||||
Трехуровневый каскад (использует obd/commands.py + obd/classifier.py):
|
||||
Уровень 0 (база, все клоны):
|
||||
ATE0 ATL0 ATS0 ATH1 ATSP0 ATDPN ATRV ATI
|
||||
→ хоть одна не ответила → НЕИСПРАВЕН (-1)
|
||||
|
||||
Уровень 1 (хорошие клоны):
|
||||
ATAT1 → уровень 1
|
||||
|
||||
Уровень 2 (настоящие ELM):
|
||||
ATCAF1 ATCFC1 → уровень 2
|
||||
|
||||
Принцип:
|
||||
- Каждая команда: ОДИН запрос, короткий таймаут (500мс)
|
||||
- Нет ретраев на неизвестные команды
|
||||
- Команды и классификация — в отдельных модулях
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from obd.commands import L0_NAMES, L1_NAMES, L2_NAMES
|
||||
from obd.classifier import classify, determine_level
|
||||
|
||||
logger = logging.getLogger("elmer.probe")
|
||||
|
||||
# Таймауты
|
||||
PROBE_TMO = 500 # мс — таймаут на одну команду при пробинге
|
||||
INTER_CMD_DELAY = 0.05 # с — пауза между командами
|
||||
|
||||
|
||||
def probe(elm) -> dict:
|
||||
"""Пробинг ELM327: определяет уровень и профиль устройства.
|
||||
|
||||
Args:
|
||||
elm: AndrOBD с открытым connect().
|
||||
|
||||
Returns:
|
||||
{"level": 0|1|2|-1, "elm_version": str|None, ...}
|
||||
level = -1 → устройство нерабочее.
|
||||
"""
|
||||
# Собираем ответы на все команды пробинга
|
||||
responses = _probe_commands(elm, L0_NAMES + L1_NAMES + L2_NAMES)
|
||||
|
||||
# Классифицируем — определяем уровень
|
||||
result = determine_level(responses)
|
||||
|
||||
if result["level"] >= 0:
|
||||
logger.info(f"probe: device level={result['level']}")
|
||||
else:
|
||||
logger.warning("probe: device UNUSABLE")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _probe_commands(elm, cmds: list[str]) -> dict[str, str]:
|
||||
"""Посылает список команд и собирает ответы.
|
||||
|
||||
Returns:
|
||||
{cmd_name: raw_response}
|
||||
"""
|
||||
responses = {}
|
||||
for cmd in cmds:
|
||||
ok, raw = _try_one(elm, cmd)
|
||||
responses[cmd] = raw if ok else ""
|
||||
return responses
|
||||
|
||||
|
||||
def _try_one(elm, cmd: str) -> tuple[bool, str]:
|
||||
"""Посылает ОДНУ команду в ELM, ждёт PROBE_TMO мс.
|
||||
|
||||
Returns:
|
||||
(ok, raw_response)
|
||||
"""
|
||||
elm._write(cmd)
|
||||
time.sleep(INTER_CMD_DELAY)
|
||||
try:
|
||||
raw = elm._read(PROBE_TMO)
|
||||
except TimeoutError:
|
||||
return False, ""
|
||||
|
||||
_, ok = classify(raw)
|
||||
return (ok, raw)
|
||||
|
||||
+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