refactor: архитектура сервера — obd/ brain/ api/
- obd/protocol.py — ELM327 стейт-машина AndrOBD (подробные докстринги) - brain/client.py — LLM-клиент Diagnoser - brain/prompts.py — системные промпты - api/db.py — SQLite (sessions + старые таблицы) - api/scripts.py — сборка диагностических скриптов - api/parser.py — парсинг ответов ELM327 - api/routes.py — все REST-эндпоинты - api/config.py — загрузка конфига - web/app.py — точка входа Flask (упрощена) Удалено: elmer/elm.py, elmer/elm_proto.py, web/raw_endpoint.py
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
"""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)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, path: str | Path = "elmer.db"):
|
||||
self.path = Path(path)
|
||||
self.conn = sqlite3.connect(str(self.path))
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self._init_schema()
|
||||
|
||||
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,
|
||||
|
||||
-- Сервер
|
||||
client_ip TEXT,
|
||||
real_ip TEXT,
|
||||
user_agent TEXT,
|
||||
content_length INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
|
||||
-- Телефон
|
||||
phone_model TEXT,
|
||||
phone_maker TEXT,
|
||||
android_version TEXT,
|
||||
android_sdk INTEGER,
|
||||
app_version TEXT,
|
||||
android_id TEXT,
|
||||
|
||||
-- ELM327
|
||||
elm_mac TEXT,
|
||||
elm_bt_name TEXT,
|
||||
obd_protocol TEXT,
|
||||
|
||||
-- Авто
|
||||
vin TEXT,
|
||||
dtc_count INTEGER DEFAULT 0,
|
||||
pid_count INTEGER DEFAULT 0,
|
||||
|
||||
-- Сессия
|
||||
duration_ms INTEGER,
|
||||
response_count INTEGER DEFAULT 0,
|
||||
error_count INTEGER DEFAULT 0,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
timeout_count INTEGER DEFAULT 0,
|
||||
script_mode TEXT,
|
||||
transport TEXT, -- 'bt' | 'tcp'
|
||||
mock_mode INTEGER DEFAULT 0,
|
||||
|
||||
-- LLM
|
||||
diagnosis_text TEXT,
|
||||
diagnosis_len INTEGER,
|
||||
llm_model TEXT,
|
||||
llm_duration_ms INTEGER,
|
||||
llm_success INTEGER DEFAULT 0,
|
||||
|
||||
-- Сырые данные (JSON)
|
||||
raw_responses TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_created ON sessions(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_vin ON sessions(vin);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_mac ON sessions(elm_mac);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_aid ON sessions(android_id);
|
||||
""")
|
||||
self.conn.commit()
|
||||
|
||||
# ── sessions ──────────────────────────────────────────
|
||||
|
||||
def save_session(self, client_info: dict, responses: list[dict],
|
||||
diagnosis: str = "", llm_model: str = "",
|
||||
llm_duration_ms: int = 0, llm_success: bool = False):
|
||||
"""Сохраняет сводную запись о сессии."""
|
||||
ci = client_info
|
||||
|
||||
# Подсчёт DTC/PID из ответов
|
||||
dtc_count = 0
|
||||
pid_count = 0
|
||||
for r in responses:
|
||||
dec = (r.get("decoded") or "").lower()
|
||||
if dec.startswith("dtc"):
|
||||
dtc_count += 1
|
||||
elif ":" in dec and not dec.startswith(("vin", "dtc", "elm", "protocol")):
|
||||
pid_count += 1
|
||||
|
||||
# VIN из ответов
|
||||
vin = None
|
||||
for r in responses:
|
||||
dec = (r.get("decoded") or "")
|
||||
if dec.startswith("VIN:"):
|
||||
vin = dec[4:].strip()
|
||||
if len(vin) != 17:
|
||||
vin = None
|
||||
break
|
||||
|
||||
self.conn.execute("""
|
||||
INSERT INTO sessions (
|
||||
client_ip, real_ip, user_agent, content_length,
|
||||
phone_model, phone_maker, android_version, android_sdk,
|
||||
app_version, android_id,
|
||||
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,
|
||||
diagnosis_text, diagnosis_len, llm_model,
|
||||
llm_duration_ms, llm_success,
|
||||
raw_responses
|
||||
) VALUES (?,?,?,?, ?,?,?,?, ?,?, ?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?,
|
||||
?,?,?, ?,?, ?)
|
||||
""", (
|
||||
ci.get("client_ip"), ci.get("real_ip"), ci.get("user_agent"),
|
||||
ci.get("content_length"),
|
||||
ci.get("phone_model"), ci.get("phone_maker"), ci.get("android_version"),
|
||||
ci.get("android_sdk"), ci.get("app_version"), ci.get("android_id"),
|
||||
ci.get("elm_mac"), ci.get("elm_bt_name"), ci.get("obd_protocol"),
|
||||
vin, dtc_count, pid_count,
|
||||
ci.get("duration_ms"), len(responses), ci.get("error_count", 0),
|
||||
ci.get("retry_count", 0), ci.get("timeout_count", 0),
|
||||
ci.get("script_mode"), ci.get("transport"), ci.get("mock_mode", 0),
|
||||
diagnosis, len(diagnosis), llm_model,
|
||||
llm_duration_ms, 1 if llm_success else 0,
|
||||
json.dumps(responses, ensure_ascii=False) if responses else None,
|
||||
))
|
||||
self.conn.commit()
|
||||
|
||||
def get_recent_sessions(self, limit: int = 50) -> list[dict]:
|
||||
"""Последние N сессий."""
|
||||
rows = self.conn.execute(
|
||||
"SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?", (limit,)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
# ── 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]
|
||||
Reference in New Issue
Block a user