From 7aaebf8481486d92476a231196241feec7cf2224 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sat, 6 Jun 2026 07:16:51 +0400 Subject: [PATCH] =?UTF-8?q?refactor:=20=D1=80=D0=B0=D0=B7=D0=B1=D0=B8?= =?UTF-8?q?=D0=BB=20routes/db/protocol=20=D0=BD=D0=B0=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/db.py | 144 ++++------------------------------------------ api/dtc.py | 90 +++++++++++++++++++++++++++++ api/ping.py | 64 +++++++++++++++++++++ api/routes.py | 124 ++------------------------------------- obd/protocol.py | 106 +--------------------------------- obd/state.py | 64 +++++++++++++++++++++ obd/timing.py | 49 ++++++++++++++++ tests/test_all.py | 3 +- web/app.py | 4 ++ 9 files changed, 291 insertions(+), 357 deletions(-) create mode 100644 api/dtc.py create mode 100644 api/ping.py create mode 100644 obd/state.py create mode 100644 obd/timing.py diff --git a/api/db.py b/api/db.py index 1b314b6..a9dbc09 100644 --- a/api/db.py +++ b/api/db.py @@ -1,12 +1,15 @@ """SQLite — сохранение сессий диагностики. -Схема: - cars — VIN, марка, модель, год, двигатель - diagnostic_tokens — id (PK), car_id (FK), created_at - llm_messages — token_id (FK), role, content, timestamp - ecu_parameters — token_id (FK), pid_code, value, unit, timestamp - dtc_codes — token_id (FK), code, description, status - sessions — сводная таблица всех сессий (клиент, ELM, авто, LLM) +Таблица sessions (35+ колонок): + client_ip, real_ip, user_agent, content_length, created_at + phone_model, phone_maker, android_version, android_sdk, app_version + android_id, device_uuid, phone_lang, phone_tz, phone_display + elm_mac, elm_bt_name, obd_protocol + vin, dtc_count, pid_count + duration_ms, response_count, error_count, retry_count, timeout_count + script_mode, transport, mock_mode, car_info + diagnosis_text, diagnosis_len, llm_model, llm_duration_ms, llm_success + raw_responses, request_id, response_json """ import json @@ -38,49 +41,6 @@ class Database: def _init_schema(self): self.conn.executescript(""" - CREATE TABLE IF NOT EXISTS cars ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - vin TEXT NOT NULL UNIQUE, - make TEXT, - model TEXT, - year INTEGER, - engine TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS diagnostic_tokens ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - car_id INTEGER NOT NULL REFERENCES cars(id), - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS llm_messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - token_id INTEGER NOT NULL REFERENCES diagnostic_tokens(id), - role TEXT NOT NULL, -- 'system' | 'user' | 'assistant' - content TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS ecu_parameters ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - token_id INTEGER NOT NULL REFERENCES diagnostic_tokens(id), - pid_code TEXT NOT NULL, -- напр. '0105', '010C' - name TEXT, -- напр. 'coolant_temp', 'rpm' - value REAL, - unit TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - - CREATE TABLE IF NOT EXISTS dtc_codes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - token_id INTEGER NOT NULL REFERENCES diagnostic_tokens(id), - code TEXT NOT NULL, -- напр. 'P0301' - description TEXT, - status TEXT, -- 'stored' | 'pending' - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - CREATE TABLE IF NOT EXISTS sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -258,87 +218,3 @@ class Database: )) self.conn.commit() - # ── cars ────────────────────────────────────────────── - - def get_or_create_car(self, vin: str) -> int: - """Возвращает car_id по VIN, создаёт запись если нет.""" - row = self.conn.execute("SELECT id FROM cars WHERE vin = ?", (vin,)).fetchone() - if row: - return row["id"] - cur = self.conn.execute("INSERT INTO cars (vin) VALUES (?)", (vin,)) - self.conn.commit() - return cur.lastrowid - - def update_car_info(self, car_id: int, make: str, model: str, year: int, engine: str): - self.conn.execute( - "UPDATE cars SET make=?, model=?, year=?, engine=? WHERE id=?", - (make, model, year, engine, car_id), - ) - self.conn.commit() - - # ── tokens ──────────────────────────────────────────── - - def create_token(self, car_id: int) -> int: - """Создаёт новую сессию диагностики, возвращает token_id.""" - cur = self.conn.execute( - "INSERT INTO diagnostic_tokens (car_id) VALUES (?)", (car_id,) - ) - self.conn.commit() - return cur.lastrowid - - def last_token_for_car(self, car_id: int) -> int | None: - """Последняя сессия для VIN (для продолжения диалога), или None.""" - row = self.conn.execute( - "SELECT id FROM diagnostic_tokens WHERE car_id=? ORDER BY created_at DESC LIMIT 1", - (car_id,), - ).fetchone() - return row["id"] if row else None - - # ── llm_messages ────────────────────────────────────── - - def add_llm_message(self, token_id: int, role: str, content: str): - self.conn.execute( - "INSERT INTO llm_messages (token_id, role, content) VALUES (?, ?, ?)", - (token_id, role, content), - ) - self.conn.commit() - - def get_llm_messages(self, token_id: int) -> list[dict]: - """Возвращает историю диалога для токена.""" - rows = self.conn.execute( - "SELECT role, content FROM llm_messages WHERE token_id=? ORDER BY id", - (token_id,), - ).fetchall() - return [{"role": r["role"], "content": r["content"]} for r in rows] - - # ── ecu_parameters ──────────────────────────────────── - - def add_parameter(self, token_id: int, pid_code: str, name: str, value: float, unit: str): - self.conn.execute( - "INSERT INTO ecu_parameters (token_id, pid_code, name, value, unit) VALUES (?, ?, ?, ?, ?)", - (token_id, pid_code, name, value, unit), - ) - self.conn.commit() - - def get_parameters(self, token_id: int) -> list[dict]: - rows = self.conn.execute( - "SELECT pid_code, name, value, unit FROM ecu_parameters WHERE token_id=? ORDER BY id", - (token_id,), - ).fetchall() - return [dict(r) for r in rows] - - # ── dtc_codes ───────────────────────────────────────── - - def add_dtc(self, token_id: int, code: str, description: str = "", status: str = "stored"): - self.conn.execute( - "INSERT INTO dtc_codes (token_id, code, description, status) VALUES (?, ?, ?, ?)", - (token_id, code, description, status), - ) - self.conn.commit() - - def get_dtcs(self, token_id: int) -> list[dict]: - rows = self.conn.execute( - "SELECT code, description, status FROM dtc_codes WHERE token_id=? ORDER BY id", - (token_id,), - ).fetchall() - return [dict(r) for r in rows] diff --git a/api/dtc.py b/api/dtc.py new file mode 100644 index 0000000..7222eb1 --- /dev/null +++ b/api/dtc.py @@ -0,0 +1,90 @@ +"""Эндпоинты DTC: расшифровка и сохранение кодов ошибок. + +POST /api/v1/dtc/decode — расшифровка кодов +POST /api/v1/dtc/upload — приём скана ошибок +""" + +import logging +from pathlib import Path + +from flask import jsonify, request + +from api.db import Database + +logger = logging.getLogger("elmer.dtc") + +# Загрузка справочника DTC +_DTC_CODES: dict[str, str] = {} +_DTC_PATH = Path(__file__).parent.parent / "doc" / "dtc_codes.txt" + + +def _load_dtc_dict() -> dict[str, str]: + global _DTC_CODES + if _DTC_CODES: + return _DTC_CODES + if _DTC_PATH.exists(): + for line in _DTC_PATH.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if "=" in line and not line.startswith("#"): + code, desc = line.split("=", 1) + _DTC_CODES[code.strip()] = desc.strip() + return _DTC_CODES + + +def register(app): + """Регистрирует DTC-эндпоинты на Flask-приложении.""" + + @app.route("/api/v1/dtc/decode", methods=["POST"]) + def dtc_decode(): + """Расшифровка кодов DTC из справочника.""" + data = request.get_json(silent=True) or {} + codes = data.get("codes", []) + if not codes: + return jsonify({"codes": []}) + dtc = _load_dtc_dict() + decoded = [{"code": c.strip().upper(), "desc": dtc.get(c.strip().upper(), c)} for c in codes] + return jsonify({"codes": decoded}) + + @app.route("/api/v1/dtc/upload", methods=["POST"]) + def dtc_upload(): + """Приём кодов ошибок (без полной диагностики).""" + data = request.get_json(silent=True) + if not data: + return jsonify({"error": "empty"}), 400 + + codes = data.get("dtc_codes", []) + client_info = data.get("client_info", {}) + client_info["client_ip"] = request.remote_addr + + # Идемпотентность + request_id = (data.get("request_id") or "").strip() + if request_id: + with Database() as db: + cached = db.get_cached_response(request_id) + if cached is not None: + return jsonify(cached), 200 + + try: + with Database() as db: + db.save_dtc_scan(client_info, codes) + except Exception as e: + logger.error(f"DTC upload failed: {e}") + + dtc = _load_dtc_dict() + decoded = [{"code": c, "desc": dtc.get(c.strip().upper(), c)} for c in codes] + response = {"codes": decoded, "count": len(decoded)} + + # Кэш для идемпотентности + if request_id: + try: + with Database() as db: + db.save_session( + client_info=client_info, + responses=[{"decoded": f"DTC stored: {c}"} for c in codes], + request_id=request_id, + response_json=response, + ) + except Exception as e: + logger.error(f"DTC idempotency cache failed: {e}") + + return jsonify(response) diff --git a/api/ping.py b/api/ping.py new file mode 100644 index 0000000..9bb802e --- /dev/null +++ b/api/ping.py @@ -0,0 +1,64 @@ +"""Эндпоинты проверки доступности. + +GET /api/v1/ping — проверка сервера +GET /api/v1/ping-llm — проверка LLM (с адаптивным кэшем) +""" + +import logging +import time + +from flask import jsonify, request + +from api.config import load +from brain.client import Diagnoser + +logger = logging.getLogger("elmer.ping") + +# Кэш для /ping-llm (успех=60с, ошибка=7с) +_ping_llm_cache: dict = {} + + +def register(app): + """Регистрирует ping-эндпоинты на Flask-приложении.""" + + @app.route("/api/v1/ping", methods=["GET"]) + def ping(): + """Быстрая проверка доступности сервера.""" + return {"ok": True} + + @app.route("/api/v1/ping-llm", methods=["GET"]) + def ping_llm(): + """Проверка LLM с адаптивным кэшем. + + - Успех → кэш 60с + - Ошибка → кэш 7с (LLM мог уже ожить) + """ + global _ping_llm_cache + now = time.time() + if _ping_llm_cache: + ttl = _ping_llm_cache.get("ttl", 7) + if (now - _ping_llm_cache.get("ts", 0)) < ttl: + return jsonify(_ping_llm_cache["data"]) + + cfg = load() + api_key = cfg["llm"]["api_key"] + if not api_key: + result = {"ok": False, "error": "no API key"} + else: + t0 = time.time() + try: + diagnoser = Diagnoser( + api_key=api_key, + model=cfg["llm"].get("model", "gpt-oss-120b"), + base_url=cfg["llm"].get("base_url", "https://api.aillm.ru/v1"), + ) + diagnoser.diagnose("Отвечай одним словом.", "OK") + ms = int((time.time() - t0) * 1000) + result = {"ok": True, "ms": ms} + except Exception as e: + ms = int((time.time() - t0) * 1000) + result = {"ok": False, "ms": ms, "error": "LLM unavailable"} + + ttl = 60 if result.get("ok") else 7 + _ping_llm_cache = {"ts": now, "data": result, "ttl": ttl} + return jsonify(result) diff --git a/api/routes.py b/api/routes.py index c22f67e..7280284 100644 --- a/api/routes.py +++ b/api/routes.py @@ -1,12 +1,14 @@ -"""Эндпоинты для толстого клиента: скрипты и батчевая загрузка. +"""Эндпоинты: скрипт, загрузка сессии, чат. GET /api/v1/script — выдача скрипта диагностики -POST /api/v1/session/upload — приём батча, LLM-анализ, возврат диагноза +POST /api/v1/session/upload — приём батча + LLM +POST /api/v1/chat — свободный вопрос к LLM + +См. также: api/dtc.py (DTC), api/ping.py (ping) """ import logging import time -from pathlib import Path from flask import jsonify, request @@ -19,9 +21,6 @@ from brain.prompts import SYSTEM_PROMPT logger = logging.getLogger("elmer.script") -# Кэш для /ping-llm (60 секунд) -_ping_llm_cache: dict = {} - def _build_diagnosis_prompt(data: dict, car_info: str = "") -> str: """Строит промпт для LLM из распарсенных данных.""" @@ -65,20 +64,6 @@ def _build_diagnosis_prompt(data: dict, car_info: str = "") -> str: # Загрузка справочника DTC _DTC_CODES: dict[str, str] = {} -def _load_dtc_dict(): - global _DTC_CODES - if _DTC_CODES: - return _DTC_CODES - path = Path(__file__).parent.parent / "doc" / "dtc_codes.txt" - if path.exists(): - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if "=" in line and not line.startswith("#"): - code, desc = line.split("=", 1) - _DTC_CODES[code.strip()] = desc.strip() - return _DTC_CODES - - def register(app): """Регистрирует эндпоинты скриптов на Flask-приложении.""" @@ -209,105 +194,6 @@ def register(app): return jsonify({"answer": answer}) - @app.route("/api/v1/ping", methods=["GET"]) - def ping(): - """Быстрая проверка доступности сервера.""" - return {"ok": True} - - @app.route("/api/v1/ping-llm", methods=["GET"]) - def ping_llm(): - """Проверка доступности LLM с адаптивным кэшем. - - - Успех → кэш 60с (нет смысла переспрашивать) - - Ошибка → кэш 7с (возможно LLM уже ожил) - """ - global _ping_llm_cache - now = time.time() - if _ping_llm_cache: - ttl = _ping_llm_cache.get("ttl", 7) - if (now - _ping_llm_cache.get("ts", 0)) < ttl: - return jsonify(_ping_llm_cache["data"]) - - cfg = load() - api_key = cfg["llm"]["api_key"] - if not api_key: - result = {"ok": False, "error": "no API key"} - else: - t0 = time.time() - try: - diagnoser = Diagnoser( - api_key=api_key, - model=cfg["llm"].get("model", "gpt-oss-120b"), - base_url=cfg["llm"].get("base_url", "https://api.aillm.ru/v1"), - ) - diagnoser.diagnose("Отвечай одним словом.", "OK") - ms = int((time.time() - t0) * 1000) - result = {"ok": True, "ms": ms} - except Exception as e: - ms = int((time.time() - t0) * 1000) - result = {"ok": False, "ms": ms, "error": "LLM unavailable"} - - ttl = 60 if result.get("ok") else 7 - _ping_llm_cache = {"ts": now, "data": result, "ttl": ttl} - return jsonify(result) - - @app.route("/api/v1/dtc/decode", methods=["POST"]) - def dtc_decode(): - """Расшифровка кодов DTC.""" - data = request.get_json(silent=True) or {} - codes = data.get("codes", []) - if not codes: - return jsonify({"codes": []}) - dtc = _load_dtc_dict() - decoded = [] - for c in codes: - c = c.strip().upper() - decoded.append({"code": c, "desc": dtc.get(c, c)}) - return jsonify({"codes": decoded}) - - @app.route("/api/v1/dtc/upload", methods=["POST"]) - def dtc_upload(): - """Приём кодов ошибок без полной диагностики.""" - data = request.get_json(silent=True) - if not data: - return jsonify({"error": "empty"}), 400 - codes = data.get("dtc_codes", []) - client_info = data.get("client_info", {}) - client_info["client_ip"] = request.remote_addr - - # Идемпотентность - request_id = (data.get("request_id") or "").strip() - if request_id: - with Database() as db: - cached = db.get_cached_response(request_id) - if cached is not None: - return jsonify(cached), 200 - - try: - with Database() as db: - db.save_dtc_scan(client_info, codes) - except Exception as e: - logger.error(f"DTC upload failed: {e}") - - dtc = _load_dtc_dict() - decoded = [{"code": c, "desc": dtc.get(c.strip().upper(), c)} for c in codes] - response = {"codes": decoded, "count": len(decoded)} - - # Сохраняем кэш для идемпотентности - if request_id: - try: - with Database() as db: - db.save_session( - client_info=client_info, - responses=[{"decoded": f"DTC stored: {c}"} for c in codes], - request_id=request_id, - response_json=response, - ) - except Exception as e: - logger.error(f"DTC idempotency cache failed: {e}") - - return jsonify(response) - def _summary(p: dict) -> dict: return { diff --git a/obd/protocol.py b/obd/protocol.py index 72a0a51..ad5aeea 100644 --- a/obd/protocol.py +++ b/obd/protocol.py @@ -34,114 +34,14 @@ obd/protocol.py — ELM327 стейт-машина (AndrOBD). import logging import time -from enum import Enum, auto from typing import Optional +from obd.state import State, Rsp +from obd.timing import AdaptiveTiming + logger = logging.getLogger("androbd") -class State(Enum): - """Состояния стейт-машины (AndrOBD STAT). - - UNDEFINED → INITIALIZING → READY — нормальный путь. - BUSY — во время выполнения команды. - ERROR/DISCONNECTED — ошибка, требуется восстановление. - """ - UNDEFINED = auto() - INITIALIZING = auto() - READY = auto() - BUSY = auto() - ERROR = auto() - DISCONNECTED = auto() - - -class Rsp: - """Классификация ответов ELM327 (AndrOBD RSP_ID). - - Каждый сырой ответ классифицируется: - - PROMPT (`>`) — готов к следующей команде - - OK — команда выполнена - - SEARCHING — идёт поиск протокола - - BUS_ERROR/BUS_BUSY/CAN_ERROR/STOPPED — ошибка шины → DISCONNECTED - - ERROR/DATA_ERROR/BUFFER_FULL — ошибка → warm start - - UNKNOWN — данные (ответ на PID/DTC) - """ - PROMPT = ">" - OK = "OK" - SEARCHING = "SEARCHING" - NODATA = "NODATA" - ERROR = "ERROR" - UNABLE = "UNABLE" - BUS_BUSY = "BUS BUSY" - BUS_ERROR = "BUS ERROR" - CAN_ERROR = "CAN ERROR" - BUS_INIT = "BUS INIT" - STOPPED = "STOPPED" - DATA_ERROR = "DATA ERROR" - BUFFER_FULL= "BUFFER FULL" - RX_ERROR = "RX ERROR" - UNKNOWN = "" - - @classmethod - def identify(cls, raw: str) -> str: - """Определяет тип ответа по сырой строке.""" - u = raw.upper().strip() - for tag in (cls.SEARCHING, cls.NODATA, cls.ERROR, cls.UNABLE, - cls.BUS_BUSY, cls.BUS_ERROR, cls.CAN_ERROR, - cls.BUS_INIT, cls.STOPPED, cls.DATA_ERROR, - cls.BUFFER_FULL, cls.RX_ERROR, cls.OK): - if u.startswith(tag): - return tag - if raw.strip() == ">": - return cls.PROMPT - return cls.UNKNOWN - - -class AdaptiveTiming: - """Адаптивный таймаут ожидания ответа (AndrOBD AdaptiveTiming). - - Динамически подстраивается под скорость ответа ЭБУ: - - Успешный ответ → уменьшаем таймаут (быстрее) - - Таймаут/NO DATA → увеличиваем таймаут (медленнее) - - BUS ERROR → сброс до DEFAULT - - ATST = таймаут / 4 (отправляется в ELM327 как ATSTxx). - """ - DEFAULT = 500 # мс — начальный таймаут - MIN = 50 # мс — минимальный - MAX = 2000 # мс — максимальный - STEP = 20 # мс — шаг изменения - RES = 4 # делитель для ATST - - def __init__(self): - self._t = self.DEFAULT - self._min = self.MIN - - @property - def ms(self) -> int: - """Текущий таймаут в миллисекундах.""" - return self._t - - @property - def atst(self) -> int: - """Значение для ATST (таймаут / 4).""" - return max(1, self._t // self.RES) - - def increase(self): - """Увеличить таймаут (ЭБУ медленно отвечает).""" - if self._t + self.STEP < self.MAX: - self._t += self.STEP - - def decrease(self): - """Уменьшить таймаут (ЭБУ отвечает быстро).""" - if self._t - self.STEP >= self._min: - self._t -= self.STEP - - def reset(self): - """Сбросить до DEFAULT (после BUS ERROR).""" - self._t = self.DEFAULT - - class AndrOBD: """Стейт-машина ELM327 — 1:1 копия AndrOBD (ElmProt.java). diff --git a/obd/state.py b/obd/state.py new file mode 100644 index 0000000..6936819 --- /dev/null +++ b/obd/state.py @@ -0,0 +1,64 @@ +"""Состояния и классификация ответов ELM327 (AndrOBD STAT/RSP_ID). + +State — состояния стейт-машины (UNDEFINED → INITIALIZING → READY → BUSY → ...) +Rsp — классификация сырых ответов ELM327 +""" + +from enum import Enum, auto + + +class State(Enum): + """Состояния стейт-машины (AndrOBD STAT). + + UNDEFINED → INITIALIZING → READY — нормальный путь. + BUSY — во время выполнения команды. + ERROR/DISCONNECTED — ошибка, требуется восстановление. + """ + UNDEFINED = auto() + INITIALIZING = auto() + READY = auto() + BUSY = auto() + ERROR = auto() + DISCONNECTED = auto() + + +class Rsp: + """Классификация ответов ELM327 (AndrOBD RSP_ID). + + Каждый сырой ответ классифицируется: + - PROMPT (`>`) — готов к следующей команде + - OK — команда выполнена + - SEARCHING — идёт поиск протокола + - BUS_ERROR/BUS_BUSY/CAN_ERROR/STOPPED — ошибка шины → DISCONNECTED + - ERROR/DATA_ERROR/BUFFER_FULL — ошибка → warm start + - UNKNOWN — данные (ответ на PID/DTC) + """ + PROMPT = ">" + OK = "OK" + SEARCHING = "SEARCHING" + NODATA = "NODATA" + ERROR = "ERROR" + UNABLE = "UNABLE" + BUS_BUSY = "BUS BUSY" + BUS_ERROR = "BUS ERROR" + CAN_ERROR = "CAN ERROR" + BUS_INIT = "BUS INIT" + STOPPED = "STOPPED" + DATA_ERROR = "DATA ERROR" + BUFFER_FULL= "BUFFER FULL" + RX_ERROR = "RX ERROR" + UNKNOWN = "" + + @classmethod + def identify(cls, raw: str) -> str: + """Определяет тип ответа по сырой строке.""" + u = raw.upper().strip() + for tag in (cls.SEARCHING, cls.NODATA, cls.ERROR, cls.UNABLE, + cls.BUS_BUSY, cls.BUS_ERROR, cls.CAN_ERROR, + cls.BUS_INIT, cls.STOPPED, cls.DATA_ERROR, + cls.BUFFER_FULL, cls.RX_ERROR, cls.OK): + if u.startswith(tag): + return tag + if raw.strip() == ">": + return cls.PROMPT + return cls.UNKNOWN diff --git a/obd/timing.py b/obd/timing.py new file mode 100644 index 0000000..5317743 --- /dev/null +++ b/obd/timing.py @@ -0,0 +1,49 @@ +"""Адаптивный таймаут ожидания ответа ЭБУ (AndrOBD AdaptiveTiming). + +Динамически подстраивается под скорость ответа: + - Успешный ответ → уменьшаем таймаут + - Таймаут/NO DATA → увеличиваем таймаут + - BUS ERROR → сброс до DEFAULT + +ATST = таймаут / 4 (отправляется в ELM327 как ATSTxx). +""" + + +class AdaptiveTiming: + """Адаптивный таймаут ожидания ответа (AndrOBD AdaptiveTiming). + + ATST = таймаут / 4 (отправляется в ELM327 как ATSTxx). + """ + DEFAULT = 500 # мс — начальный таймаут + MIN = 50 # мс — минимальный + MAX = 2000 # мс — максимальный + STEP = 20 # мс — шаг изменения + RES = 4 # делитель для ATST + + def __init__(self): + self._t = self.DEFAULT + self._min = self.MIN + + @property + def ms(self) -> int: + """Текущий таймаут в миллисекундах.""" + return self._t + + @property + def atst(self) -> int: + """Значение для ATST (таймаут / 4).""" + return max(1, self._t // self.RES) + + def increase(self): + """Увеличить таймаут (ЭБУ медленно отвечает).""" + if self._t + self.STEP < self.MAX: + self._t += self.STEP + + def decrease(self): + """Уменьшить таймаут (ЭБУ отвечает быстро).""" + if self._t - self.STEP >= self._min: + self._t -= self.STEP + + def reset(self): + """Сбросить до DEFAULT (после BUS ERROR).""" + self._t = self.DEFAULT diff --git a/tests/test_all.py b/tests/test_all.py index effbd83..e978969 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -18,7 +18,8 @@ os.environ["ELMER_CONFIG"] = str(Path(__file__).parent / "config.yaml") from api.db import Database from api.parser import parse_batch, format_no_llm -from api.routes import _load_dtc_dict, _build_diagnosis_prompt +from api.routes import _build_diagnosis_prompt +from api.dtc import _load_dtc_dict from api.scripts import build_default_script, build_full_script passed = 0 diff --git a/web/app.py b/web/app.py index 9d0ca95..8796431 100644 --- a/web/app.py +++ b/web/app.py @@ -20,12 +20,16 @@ from flask import Flask, jsonify, redirect, render_template, request, send_from_ from api.config import load from api.routes import register as register_api +from api.dtc import register as register_dtc +from api.ping import register as register_ping logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") app = Flask(__name__) config = load() register_api(app) +register_dtc(app) +register_ping(app) @app.route("/")