project structure: elm.py, db.py, prompts.py, diagnose.py, web UI (Flask), config
This commit is contained in:
+95
@@ -0,0 +1,95 @@
|
||||
"""Elmer Web UI — Flask-приложение для локального тестирования.
|
||||
|
||||
Заодно прототип будущего серверного API.
|
||||
Запуск: DEEPSEEK_API_KEY=sk-... python web/app.py
|
||||
Открыть: http://localhost:5005
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем корень проекта в PYTHONPATH
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
|
||||
from elmer.config import load
|
||||
from elmer.db import Database
|
||||
from elmer.diagnose import Diagnoser
|
||||
from elmer.elm import ELM327
|
||||
from elmer.prompts import SYSTEM_PROMPT, build_user_prompt
|
||||
|
||||
app = Flask(__name__)
|
||||
config = load()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""Главная страница."""
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/api/diagnose", methods=["POST"])
|
||||
def diagnose():
|
||||
"""Подключается к ELM327, читает данные, отправляет в LLM, возвращает результат."""
|
||||
elm_cfg = config["elm327"]
|
||||
|
||||
# ── 1. ELM327 ────────────────────────────────
|
||||
try:
|
||||
elm = ELM327(port=elm_cfg["port"], baudrate=elm_cfg["baudrate"])
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Не удалось открыть порт {elm_cfg['port']}: {e}"}), 500
|
||||
|
||||
if not elm.init():
|
||||
elm.close()
|
||||
return jsonify({"error": "ELM327 не ответил на ATZ"}), 500
|
||||
|
||||
# ── 2. VIN ──────────────────────────────────
|
||||
vin = elm.read_vin()
|
||||
if not vin:
|
||||
elm.close()
|
||||
return jsonify({"error": "Не удалось прочитать VIN (режим 09 не поддерживается?)"}), 500
|
||||
|
||||
# ── 3. Ошибки ───────────────────────────────
|
||||
dtc_codes = elm.read_dtc_codes("03") + elm.read_dtc_codes("07")
|
||||
|
||||
# ── 4. Параметры ────────────────────────────
|
||||
parameters = elm.read_all_pids(config.get("pids"))
|
||||
|
||||
elm.close()
|
||||
|
||||
# ── 5. DeepSeek ─────────────────────────────
|
||||
api_key = config["deepseek"]["api_key"]
|
||||
if not api_key:
|
||||
return jsonify({"error": "DEEPSEEK_API_KEY не задан"}), 500
|
||||
|
||||
diagnoser = Diagnoser(
|
||||
api_key=api_key,
|
||||
model=config["deepseek"].get("model", "deepseek-chat"),
|
||||
)
|
||||
user_prompt = build_user_prompt(vin, dtc_codes, parameters)
|
||||
answer = diagnoser.diagnose(SYSTEM_PROMPT, user_prompt)
|
||||
|
||||
# ── 6. SQLite ───────────────────────────────
|
||||
db = Database()
|
||||
car_id = db.get_or_create_car(vin)
|
||||
token_id = db.create_token(car_id)
|
||||
for dtc in dtc_codes:
|
||||
db.add_dtc(token_id, dtc["code"], dtc.get("description", ""), dtc["status"])
|
||||
for p in parameters:
|
||||
db.add_parameter(token_id, p["pid_code"], p["name"], p["value"], p["unit"])
|
||||
db.add_llm_message(token_id, "user", user_prompt)
|
||||
db.add_llm_message(token_id, "assistant", answer)
|
||||
|
||||
return jsonify({
|
||||
"vin": vin,
|
||||
"dtc_codes": dtc_codes,
|
||||
"parameters": parameters,
|
||||
"diagnosis": answer,
|
||||
"token_id": token_id,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"🌐 Elmer Web: http://localhost:5005")
|
||||
app.run(host="0.0.0.0", port=5005, debug=False)
|
||||
@@ -0,0 +1,174 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
font-size: 1.2em;
|
||||
background: #ff6b35;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #e85d2a;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#status {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #333;
|
||||
border-top-color: #ff6b35;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #16213e;
|
||||
border: 1px solid #0f3460;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.1em;
|
||||
color: #ff6b35;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 1.3em;
|
||||
letter-spacing: 2px;
|
||||
color: #4ecca3;
|
||||
}
|
||||
|
||||
.dtc-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
margin: 4px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.dtc-badge.stored {
|
||||
background: #5c1a1a;
|
||||
color: #ff6b6b;
|
||||
border: 1px solid #8b2a2a;
|
||||
}
|
||||
|
||||
.dtc-badge.pending {
|
||||
background: #5c4a1a;
|
||||
color: #ffd93d;
|
||||
border: 1px solid #8b6a2a;
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
}
|
||||
|
||||
.param-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.param-row span {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.param-row strong {
|
||||
color: #4ecca3;
|
||||
}
|
||||
|
||||
.diagnosis-card {
|
||||
background: #0f1a2e;
|
||||
border-color: #1a3a5c;
|
||||
}
|
||||
|
||||
.diagnosis-card h2 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#diagnosis-text {
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
#diagnosis-text h2 {
|
||||
font-size: 1.2em;
|
||||
margin-top: 16px;
|
||||
color: #4ecca3;
|
||||
}
|
||||
|
||||
#diagnosis-text h3 {
|
||||
font-size: 1.05em;
|
||||
margin-top: 12px;
|
||||
color: #ff6b35;
|
||||
}
|
||||
|
||||
#diagnosis-text strong {
|
||||
color: #ffd93d;
|
||||
}
|
||||
|
||||
.ok {
|
||||
color: #4ecca3;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Elmer — Диагностика авто</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔧 Elmer</h1>
|
||||
<p class="subtitle">Диагностика через ELM327 + ИИ</p>
|
||||
|
||||
<button id="btn-diagnose" onclick="runDiagnose()">
|
||||
🚗 Подключить и диагностировать
|
||||
</button>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<div id="result" style="display:none;">
|
||||
<div class="card" id="vin-card">
|
||||
<h2>📋 VIN</h2>
|
||||
<code id="vin-value"></code>
|
||||
</div>
|
||||
|
||||
<div class="card" id="dtc-card">
|
||||
<h2>⚠️ Коды ошибок</h2>
|
||||
<div id="dtc-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="params-card">
|
||||
<h2>📊 Параметры</h2>
|
||||
<div id="params-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="card diagnosis-card">
|
||||
<h2>🧠 Диагноз</h2>
|
||||
<div id="diagnosis-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function runDiagnose() {
|
||||
const btn = document.getElementById('btn-diagnose');
|
||||
const status = document.getElementById('status');
|
||||
const result = document.getElementById('result');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Читаю данные с ЭБУ...';
|
||||
status.innerHTML = '<div class="spinner"></div> Идёт диагностика...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/diagnose', { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
|
||||
if (!resp.ok) {
|
||||
status.innerHTML = `<div class="error">❌ ${data.error}</div>`;
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔄 Попробовать снова';
|
||||
return;
|
||||
}
|
||||
|
||||
// VIN
|
||||
document.getElementById('vin-value').textContent = data.vin;
|
||||
|
||||
// DTC
|
||||
const dtcList = document.getElementById('dtc-list');
|
||||
if (data.dtc_codes.length === 0) {
|
||||
dtcList.innerHTML = '<p class="ok">✅ Ошибок нет</p>';
|
||||
} else {
|
||||
dtcList.innerHTML = data.dtc_codes
|
||||
.map(d => `<span class="dtc-badge ${d.status}">${d.code} [${d.status}]</span>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Parameters
|
||||
const paramsList = document.getElementById('params-list');
|
||||
paramsList.innerHTML = data.parameters
|
||||
.map(p => `<div class="param-row"><span>${p.name}</span><strong>${p.value} ${p.unit}</strong></div>`)
|
||||
.join('');
|
||||
|
||||
// Diagnosis — markdown to html (simple)
|
||||
document.getElementById('diagnosis-text').innerHTML =
|
||||
data.diagnosis
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
result.style.display = 'block';
|
||||
status.innerHTML = '<div class="ok">✅ Диагностика завершена. Сохранено в базу.</div>';
|
||||
btn.textContent = '🔄 Диагностировать заново';
|
||||
|
||||
} catch (err) {
|
||||
status.innerHTML = `<div class="error">❌ Ошибка: ${err.message}</div>`;
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user