v0.48.0 — пробинг ELM327, трехуровневый профиль, рефакторинг obd/
- obd/probe.py: трехуровневый каскад (L0/L1/L2) - obd/commands.py: каталог всех AT-команд с метаданными - obd/classifier.py: классификация ответов + определение уровня - obd/connection.py: транспортный слой (SerialTransport) - obd/protocol.py: init() только база, без ATAT1/ATST - api/db.py: таблица device_profiles по BT MAC - api/scripts.py: три уровня скриптов (l0/l1/l2) - api/routes.py: /elm/probe, /elm/profile/<mac>, /script?level= - web/templates/index.html: v0.48.0 - CHANGELOG.md, doc/architecture.md, resume.txt: версии
This commit is contained in:
@@ -1,15 +1,11 @@
|
||||
"""SQLite — сохранение сессий диагностики.
|
||||
"""SQLite — сохранение сессий диагностики + профили ELM-устройств.
|
||||
|
||||
Таблица 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
|
||||
... (см. ниже)
|
||||
|
||||
Таблица device_profiles:
|
||||
mac (TEXT PK), level (INT), elm_version, elm_desc, protocol,
|
||||
supported (JSON), unsupported (JSON), first_seen, last_seen
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -108,6 +104,22 @@ class Database:
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_aid ON sessions(android_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_request_id ON sessions(request_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_uuid ON sessions(device_uuid);
|
||||
|
||||
-- Профили ELM-устройств (по BT MAC)
|
||||
CREATE TABLE IF NOT EXISTS device_profiles (
|
||||
mac TEXT PRIMARY KEY, -- BT MAC-адрес
|
||||
level INTEGER NOT NULL, -- 0/1/2 (-1 = нерабочее)
|
||||
elm_version TEXT, -- ATI ответ
|
||||
elm_desc TEXT, -- AT@1 (если есть)
|
||||
protocol TEXT, -- ATDPN
|
||||
voltage TEXT, -- ATRV
|
||||
supported TEXT, -- JSON: ["ATE0","ATL0",...]
|
||||
unsupported TEXT, -- JSON: ["ATAT1",...]
|
||||
errors TEXT, -- JSON: ["ATCFC1: no response",...]
|
||||
first_seen TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_seen TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_level ON device_profiles(level);
|
||||
""")
|
||||
self.conn.commit()
|
||||
|
||||
@@ -221,3 +233,52 @@ class Database:
|
||||
))
|
||||
self.conn.commit()
|
||||
|
||||
# ── device_profiles ──────────────────────────────────
|
||||
|
||||
def get_device_profile(self, mac: str) -> dict | None:
|
||||
"""Возвращает сохранённый профиль устройства по MAC, или None."""
|
||||
row = self.conn.execute(
|
||||
"SELECT * FROM device_profiles WHERE mac = ?", (mac,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
p = dict(row)
|
||||
for f in ("supported", "unsupported", "errors"):
|
||||
p[f] = json.loads(p[f]) if p.get(f) else []
|
||||
return p
|
||||
|
||||
def save_device_profile(self, mac: str, profile: dict):
|
||||
"""Сохраняет или обновляет профиль устройства.
|
||||
|
||||
profile — результат obd.probe.probe().
|
||||
"""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
self.conn.execute("""
|
||||
INSERT INTO device_profiles
|
||||
(mac, level, elm_version, elm_desc, protocol, voltage,
|
||||
supported, unsupported, errors, first_seen, last_seen)
|
||||
VALUES (?,?,?,?,?,?, ?,?,?, ?,?)
|
||||
ON CONFLICT(mac) DO UPDATE SET
|
||||
level = excluded.level,
|
||||
elm_version = excluded.elm_version,
|
||||
elm_desc = excluded.elm_desc,
|
||||
protocol = excluded.protocol,
|
||||
voltage = excluded.voltage,
|
||||
supported = excluded.supported,
|
||||
unsupported = excluded.unsupported,
|
||||
errors = excluded.errors,
|
||||
last_seen = excluded.last_seen
|
||||
""", (
|
||||
mac,
|
||||
profile.get("level", -1),
|
||||
profile.get("elm_version"),
|
||||
profile.get("elm_desc"),
|
||||
profile.get("protocol"),
|
||||
profile.get("voltage"),
|
||||
json.dumps(profile.get("supported", []), ensure_ascii=False),
|
||||
json.dumps(profile.get("unsupported", []), ensure_ascii=False),
|
||||
json.dumps(profile.get("errors", []), ensure_ascii=False),
|
||||
now, now,
|
||||
))
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
+77
-3
@@ -1,8 +1,9 @@
|
||||
"""Эндпоинты: скрипт, загрузка сессии, чат.
|
||||
"""Эндпоинты: скрипт, загрузка сессии, чат, пробинг ELM.
|
||||
|
||||
GET /api/v1/script — выдача скрипта диагностики
|
||||
POST /api/v1/session/upload — приём батча + LLM
|
||||
POST /api/v1/chat — свободный вопрос к LLM
|
||||
POST /api/v1/elm/probe — пробинг ELM327, определение уровня
|
||||
|
||||
См. также: api/dtc.py (DTC), api/ping.py (ping)
|
||||
"""
|
||||
@@ -15,7 +16,7 @@ from flask import jsonify, request
|
||||
from api.config import load
|
||||
from api.db import Database
|
||||
from api.parser import format_no_llm, parse_batch
|
||||
from api.scripts import build_default_script, build_full_script
|
||||
from api.scripts import build_default_script, build_full_script, build_script_for_level
|
||||
from brain.client import Diagnoser, LLMError
|
||||
from brain.prompts import SYSTEM_PROMPT
|
||||
|
||||
@@ -70,7 +71,16 @@ def register(app):
|
||||
@app.route("/api/v1/script", methods=["GET"])
|
||||
def get_script():
|
||||
mode = request.args.get("mode", "full")
|
||||
script = build_full_script() if mode == "full" else build_default_script()
|
||||
level = request.args.get("level")
|
||||
if level is not None:
|
||||
try:
|
||||
script = build_script_for_level(int(level))
|
||||
except (ValueError, TypeError):
|
||||
script = build_default_script()
|
||||
elif mode == "full":
|
||||
script = build_full_script()
|
||||
else:
|
||||
script = build_default_script()
|
||||
return jsonify(script)
|
||||
|
||||
@app.route("/api/v1/session/upload", methods=["POST"])
|
||||
@@ -195,6 +205,70 @@ def register(app):
|
||||
return jsonify({"answer": answer})
|
||||
|
||||
|
||||
@app.route("/api/v1/elm/probe", methods=["POST"])
|
||||
def probe_elm():
|
||||
"""Пробинг ELM327: определение уровня устройства.
|
||||
|
||||
Принимает MAC и сырые ответы на команды пробинга от Android-клиента.
|
||||
Клиент посылает команды из списка, сервер классифицирует ответы.
|
||||
|
||||
Body: {
|
||||
"mac": "AA:BB:CC:...",
|
||||
"responses": [
|
||||
{"cmd": "ATE0", "raw": "OK"},
|
||||
{"cmd": "ATL0", "raw": "OK"},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
Returns: профиль устройства (level, supported, unsupported, ...)
|
||||
"""
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "mac" not in data or "responses" not in data:
|
||||
return jsonify({"error": "missing 'mac' or 'responses'"}), 400
|
||||
|
||||
mac = data["mac"].strip()
|
||||
responses = data["responses"]
|
||||
|
||||
if not mac:
|
||||
return jsonify({"error": "empty mac"}), 400
|
||||
|
||||
# Классификация ответов через сервис
|
||||
from obd.classifier import determine_level
|
||||
|
||||
# Собираем ответы в словарь cmd→raw
|
||||
resp_map = {}
|
||||
for r in responses:
|
||||
cmd = (r.get("cmd") or "").strip().upper()
|
||||
raw = (r.get("raw") or "").strip()
|
||||
resp_map[cmd] = raw
|
||||
|
||||
result = determine_level(resp_map)
|
||||
result["mac"] = mac
|
||||
|
||||
_save_profile(mac, result)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route("/api/v1/elm/profile/<mac>", methods=["GET"])
|
||||
def get_elm_profile(mac: str):
|
||||
"""Возвращает сохранённый профиль устройства по MAC."""
|
||||
with Database() as db:
|
||||
p = db.get_device_profile(mac)
|
||||
if p is None:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
return jsonify(p)
|
||||
|
||||
|
||||
def _save_profile(mac: str, profile: dict):
|
||||
"""Сохраняет профиль в БД (best-effort)."""
|
||||
try:
|
||||
with Database() as db:
|
||||
db.save_device_profile(mac, profile)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save device profile for {mac}: {e}")
|
||||
|
||||
|
||||
def _summary(p: dict) -> dict:
|
||||
return {
|
||||
"vin": p["vin"],
|
||||
|
||||
+99
-19
@@ -1,29 +1,109 @@
|
||||
"""Сборка диагностических скриптов."""
|
||||
"""Сборка диагностических скриптов.
|
||||
|
||||
Три уровня в зависимости от возможностей ELM327:
|
||||
L0 (все клоны) — 5 PIDs + stored DTC
|
||||
L1 (+ATAT1) — 8 PIDs + VIN + stored/pending DTC
|
||||
L2 (+CAF1/CFC1) — 14 PIDs + VIN + калибровки + все ошибки
|
||||
|
||||
Принцип:
|
||||
- Чем выше уровень — тем больше PIDs и глубже диагностика
|
||||
- Скрипты захардкожены (PIDs по SAE J1979), LLM не составляет
|
||||
- Нет в профиле — не слать (несуществующие команды вешают клонов)
|
||||
"""
|
||||
|
||||
|
||||
def build_default_script() -> dict:
|
||||
"""Минимальный скрипт для отладки: 1 PID → LLM."""
|
||||
def build_script_l0() -> dict:
|
||||
"""Скрипт для уровня 0 — клоны v1.5 и подобные.
|
||||
|
||||
Только однокадровые ответы. Без VIN (много-фреймовый, без CFC1 рвётся).
|
||||
"""
|
||||
return {
|
||||
"version": 1,
|
||||
"title": "Экспресс-диагностика",
|
||||
"title": "Диагностика (базовая)",
|
||||
"steps": [
|
||||
{"id": "pid_05", "cmd": "0105", "desc": "Температура ОЖ"},
|
||||
{"id": "elm_atrv", "cmd": "ATRV", "desc": "Напряжение"},
|
||||
{"id": "pid_05", "cmd": "0105", "desc": "Температура ОЖ"},
|
||||
{"id": "pid_0C", "cmd": "010C", "desc": "Обороты"},
|
||||
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость"},
|
||||
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель"},
|
||||
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка"},
|
||||
{"id": "dtc_03", "cmd": "03", "desc": "Коды ошибок"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_script_l1() -> dict:
|
||||
"""Скрипт для уровня 1 — хорошие клоны с ATAT1.
|
||||
|
||||
Быстрее L0 за счёт адаптивного тайминга. VIN — медленно но возможно.
|
||||
"""
|
||||
return {
|
||||
"version": 1,
|
||||
"title": "Диагностика (стандартная)",
|
||||
"steps": [
|
||||
{"id": "elm_atrv", "cmd": "ATRV", "desc": "Напряжение"},
|
||||
{"id": "pid_05", "cmd": "0105", "desc": "Температура ОЖ"},
|
||||
{"id": "pid_0C", "cmd": "010C", "desc": "Обороты"},
|
||||
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость"},
|
||||
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель"},
|
||||
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка"},
|
||||
{"id": "pid_06", "cmd": "0106", "desc": "STFT"},
|
||||
{"id": "pid_07", "cmd": "0107", "desc": "LTFT"},
|
||||
{"id": "vin_09", "cmd": "0902", "desc": "VIN"},
|
||||
{"id": "dtc_03", "cmd": "03", "desc": "Сохр. ошибки"},
|
||||
{"id": "dtc_07", "cmd": "07", "desc": "Pending ошибки"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_script_l2() -> dict:
|
||||
"""Скрипт для уровня 2 — настоящий ELM327 с CAF1+CFC1.
|
||||
|
||||
Полный фарш: много PIDs, VIN быстро, калибровки, все типы ошибок.
|
||||
"""
|
||||
return {
|
||||
"version": 1,
|
||||
"title": "Диагностика (полная)",
|
||||
"steps": [
|
||||
{"id": "elm_atrv", "cmd": "ATRV", "desc": "Напряжение"},
|
||||
{"id": "pid_05", "cmd": "0105", "desc": "Температура ОЖ"},
|
||||
{"id": "pid_0C", "cmd": "010C", "desc": "Обороты"},
|
||||
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость"},
|
||||
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель"},
|
||||
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка"},
|
||||
{"id": "pid_06", "cmd": "0106", "desc": "STFT"},
|
||||
{"id": "pid_07", "cmd": "0107", "desc": "LTFT"},
|
||||
{"id": "pid_0B", "cmd": "010B", "desc": "MAP"},
|
||||
{"id": "pid_0F", "cmd": "010F", "desc": "Темп. воздуха"},
|
||||
{"id": "pid_10", "cmd": "0110", "desc": "MAF"},
|
||||
{"id": "pid_1C", "cmd": "011C", "desc": "Стандарт OBD"},
|
||||
{"id": "vin_09", "cmd": "0902", "desc": "VIN"},
|
||||
{"id": "cal_09", "cmd": "0904", "desc": "Калибровка"},
|
||||
{"id": "ecu_09", "cmd": "090A", "desc": "Имя ЭБУ"},
|
||||
{"id": "dtc_03", "cmd": "03", "desc": "Сохр. ошибки"},
|
||||
{"id": "dtc_07", "cmd": "07", "desc": "Pending ошибки"},
|
||||
{"id": "dtc_0A", "cmd": "0A", "desc": "Перманентные"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ── Совместимость со старым API ─────────────────────────
|
||||
|
||||
def build_default_script() -> dict:
|
||||
"""Минимальный скрипт (уровень 0)."""
|
||||
return build_script_l0()
|
||||
|
||||
|
||||
def build_full_script() -> dict:
|
||||
"""Полный скрипт диагностики."""
|
||||
return {
|
||||
"version": 1,
|
||||
"title": "Полная диагностика",
|
||||
"steps": [
|
||||
{"id": "pid_05", "cmd": "0105", "desc": "Температура ОЖ"},
|
||||
{"id": "pid_0C", "cmd": "010C", "desc": "Обороты"},
|
||||
{"id": "pid_0D", "cmd": "010D", "desc": "Скорость"},
|
||||
{"id": "pid_11", "cmd": "0111", "desc": "Дроссель"},
|
||||
{"id": "pid_04", "cmd": "0104", "desc": "Нагрузка"},
|
||||
{"id": "pid_06", "cmd": "0106", "desc": "STFT"},
|
||||
{"id": "pid_07", "cmd": "0107", "desc": "LTFT"},
|
||||
],
|
||||
}
|
||||
"""Полный скрипт (уровень 2)."""
|
||||
return build_script_l2()
|
||||
|
||||
|
||||
def build_script_for_level(level: int) -> dict:
|
||||
"""Возвращает скрипт под уровень устройства."""
|
||||
if level >= 2:
|
||||
return build_script_l2()
|
||||
elif level == 1:
|
||||
return build_script_l1()
|
||||
else:
|
||||
return build_script_l0()
|
||||
|
||||
Reference in New Issue
Block a user