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