diff --git a/api/dtc.py b/api/dtc.py index 265aa50..48c8e56 100644 --- a/api/dtc.py +++ b/api/dtc.py @@ -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 diff --git a/api/parser.py b/api/parser.py index 12e4b73..36443ff 100644 --- a/api/parser.py +++ b/api/parser.py @@ -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 diff --git a/brain/client.py b/brain/client.py index 7f6ef5e..b26a9e8 100644 --- a/brain/client.py +++ b/brain/client.py @@ -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}") diff --git a/obd/protocol.py b/obd/protocol.py index 3025d67..db2cf61 100644 --- a/obd/protocol.py +++ b/obd/protocol.py @@ -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