docs: пояснения почему выбрана такая логика (reasoning)

This commit is contained in:
Repinoid
2026-06-06 07:24:17 +04:00
parent 8da4876cb8
commit fd8059bb6a
4 changed files with 45 additions and 7 deletions
+13 -2
View File
@@ -37,7 +37,12 @@ def register(app):
@app.route("/api/v1/dtc/decode", methods=["POST"])
def dtc_decode():
"""Расшифровка кодов DTC из справочника."""
"""Расшифровка кодов DTC из справочника doc/dtc_codes.txt.
Принимает: {"codes": ["P0301", "P0302"]}
Возвращает: {"codes": [{"code": "P0301", "desc": "..."}]}
Неизвестные коды возвращаются как есть — справочник неполный.
"""
data = request.get_json(silent=True) or {}
codes = data.get("codes", [])
if not codes:
@@ -48,7 +53,13 @@ def register(app):
@app.route("/api/v1/dtc/upload", methods=["POST"])
def dtc_upload():
"""Приём кодов ошибок (без полной диагностики)."""
"""Приём кодов ошибок (без полной диагностики, только Ошибки).
Идемпотентность: если client шлёт request_id, сервер:
1. Проверяет кэш — если есть, возвращает его (LLM не вызывается)
2. Если нет — сохраняет, кэширует ответ
Это предотвращает дубликаты при повторных отправках (например, 499).
"""
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "empty"}), 400
+8 -1
View File
@@ -44,13 +44,20 @@ def parse_batch(responses: list[dict]) -> dict:
def _parse_vin(result: dict, raw: str, decoded: str):
"""Извлечь VIN из decoded (Android) или из raw HEX (fallback).
Два пути получения VIN:
1. Android-декодер уже распарсил → decoded = "VIN: WVWZZZ..."
2. Сырой HEX от ELM → парсим 490201 + 17 байт ASCII
Fallback нужен для старых клиентов без декодера.
"""
if decoded.startswith("VIN:"):
vin = decoded.replace("VIN:", "").strip()
if len(vin) == 17:
result["vin"] = vin
return
# Fallback: парсим VIN из raw HEX
# Fallback: парсим VIN из raw HEX (CAN multi-frame, ISO-TP, обычный)
if "49" in raw and ("02" in raw or "4902" in raw.replace(" ", "")):
# Чистим: убираем CAN адресацию (N:), ISO-TP (10..), двоеточия
import re
+10 -1
View File
@@ -41,7 +41,15 @@ class Diagnoser:
self.timeout = timeout
def ask(self, messages: list[dict]) -> str:
"""Отправляет сообщения в LLM API, возвращает текст ответа."""
"""Отправляет сообщения в LLM API, возвращает текст ответа.
Обработка ошибок:
- Timeout: 120+ секунд, LLM могла зависнуть — ретраить
- 429: Rate limit — ждать Retry-After
- 5xx: Сервер LLM упал — ретраить
- 4xx: Наша ошибка (auth, запрос) — не ретраить
Клиенту возвращается безопасное сообщение без технических деталей.
"""
try:
resp = requests.post(
f"{self.base_url}/chat/completions",
@@ -71,6 +79,7 @@ class Diagnoser:
elif 500 <= status < 600:
raise LLMError("LLM временно недоступен. Попробуйте позже.")
else:
# 4xx: неверный ключ, неверный запрос — ретрай бесполезен
raise LLMError("Ошибка LLM. Попробуйте позже.")
except Exception as e:
logger.error(f"LLM unexpected: {e}")
+14 -3
View File
@@ -120,7 +120,14 @@ class AndrOBD:
# ── Приватные методы ──────────────────────────────────
def _exec(self, cmd: str, timeout: int) -> str:
"""Выполнить команду с таймаутом и ретраями (до 10 попыток)."""
"""Выполнить команду ELM327 с ретраями (до 10 попыток).
Архитектура AndrOBD: ELM327 может не ответить, если:
- Адаптер ещё не инициализирован (SEARCHING)
- ЭБУ медленно отвечает (увеличиваем AdaptiveTiming)
- Плохой контакт / потеря связи (Busy→Timeout→Error)
10 ретраев — жёсткий лимит из AndrOBD, после — ERROR.
"""
self._last_cmd = cmd
self._write(cmd)
t = timeout
@@ -128,9 +135,11 @@ 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}")
@@ -138,9 +147,9 @@ class AndrOBD:
return ""
def _handle(self, raw: str) -> str:
"""Обработать ответ ELM327: классифицировать и обновить таймаут.
"""Обработать ответ ELM327: классификация → обновление таймаута.
Возвращает raw как есть — обработка данных делается выше.
Возвращает raw как есть — декодирование данных на уровень выше.
"""
t = Rsp.identify(raw)
@@ -150,6 +159,8 @@ class AndrOBD:
self._timing.decrease()
return raw
if t == Rsp.NODATA:
# NODATA = ЭБУ жив, но не может ответить (например, PID недоступен)
# Увеличиваем таймаут — возможно ЭБУ медленный
self._timing.increase()
self._update_atst()
return raw