project_inertial-control/processing/final_server_auto.py

369 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sqlite3
import json
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
DB_PATH = os.path.expanduser('~/inertial_control/inertial_data.db')
PORT = 8050
TRAJ_LIMIT = 5000 # показываем последние 5000 точек траектории (~60 сек движения)
LIDAR_LIMIT = 2000 # последние точки лидара
HTML = """
<!DOCTYPE html>
<html>
<head>
<title>Inertial Tracker — Реальное время</title>
<script src="plotly-latest.min.js"></script>
<style>
* { box-sizing: border-box; }
body { margin:0; background:#111827; font-family: 'Courier New', monospace; }
#topbar {
position: fixed; top: 0; left: 0; right: 0; height: 48px;
background: #1f2937; border-bottom: 1px solid #374151;
display: flex; align-items: center; padding: 0 16px; gap: 24px;
z-index: 200;
}
#topbar .title { color: #f9fafb; font-size: 15px; font-weight: bold; letter-spacing: 1px; }
#topbar .badge {
padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: bold;
}
.badge-moving { background: #065f46; color: #6ee7b7; }
.badge-still { background: #1e3a5f; color: #93c5fd; }
.badge-nodata { background: #3f2a2a; color: #fca5a5; }
#topbar .dist { color: #9ca3af; font-size: 13px; }
#topbar .dist span { color: #e5e7eb; }
#coords {
position: fixed; top: 60px; right: 12px;
background: #1f2937; color: #6ee7b7;
padding: 12px 16px; font-size: 17px; z-index: 100;
border-radius: 10px; border: 1px solid #374151;
line-height: 1.8;
}
#legend {
position: fixed; top: 60px; left: 12px;
background: #1f2937; color: #d1d5db;
padding: 10px 14px; font-size: 13px; z-index: 100;
border-radius: 10px; border: 1px solid #374151;
line-height: 2.0;
}
#hint {
position: fixed; bottom: 42px; left: 12px;
background: #1f2937; color: #6b7280;
padding: 7px 12px; font-size: 11px; z-index: 100;
border-radius: 8px; border: 1px solid #374151;
max-width: 260px; line-height: 1.5;
}
#status {
position: fixed; bottom: 10px; left: 12px;
background: #111827; color: #6b7280;
padding: 4px 10px; font-size: 11px; z-index: 100;
border-radius: 6px;
}
#plot { width: 100%; height: 100vh; padding-top: 48px; }
</style>
</head>
<body>
<div id="topbar">
<div class="title">INERTIAL TRACKER</div>
<div id="motion-badge" class="badge badge-nodata">Нет данных</div>
<div class="dist">Пройдено: <span id="dist-val">0.00</span> м</div>
<div class="dist">Время: <span id="time-val">0:00</span></div>
</div>
<div id="coords">X: —<br>Y: —<br>Z: —</div>
<div id="legend">
<span style="color:#ffffff; font-size:16px;">◆</span> Начальная точка (0, 0, 0)<br>
<span style="color:#f87171; font-size:14px;">━━</span> Траектория движения<br>
<span style="color:#60a5fa; font-size:10px;">●●●</span> Карта окружения (лидар)<br>
<span style="color:#34d399; font-size:16px;">●</span> Текущее положение
</div>
<div id="hint">
Оси X, Y, Z — перемещение в метрах.<br>
Масштаб одинаковый по всем осям.<br>
Вращайте сцену мышью.
</div>
<div id="status">Подключение...</div>
<div id="plot"></div>
<script>
let updateCount = 0;
let lastRange = null;
let totalDist = 0;
let lastPos = null;
let startTime = null;
let sessionTimer = null;
// Вычисляет равные диапазоны для всех трёх осей
// (одинаковый масштаб = квадратный куб, без растяжки по Z)
function squareRanges(allX, allY, allZ) {
function minMax(arr) {
if (!arr || arr.length === 0) return [0, 0];
let mn = arr[0], mx = arr[0];
for (let v of arr) { if (v < mn) mn = v; if (v > mx) mx = v; }
return [mn, mx];
}
const rx = minMax(allX), ry = minMax(allY), rz = minMax(allZ);
const spans = [rx[1]-rx[0], ry[1]-ry[0], rz[1]-rz[0]];
const maxSpan = Math.max(...spans, 0.5); // минимум 0.5 м
const half = maxSpan / 2 * 1.2; // запас 20%
function mid(r) { return (r[0] + r[1]) / 2; }
return [
[mid(rx) - half, mid(rx) + half],
[mid(ry) - half, mid(ry) + half],
[mid(rz) - half, mid(rz) + half]
];
}
function rangeChanged(newR, oldR) {
if (!oldR) return true;
for (let i = 0; i < newR.length; i++) {
if (Math.abs(newR[i][0] - oldR[i][0]) > 0.25) return true;
if (Math.abs(newR[i][1] - oldR[i][1]) > 0.25) return true;
}
return false;
}
function fmtTime(sec) {
const m = Math.floor(sec / 60), s = Math.floor(sec % 60);
return m + ':' + String(s).padStart(2, '0');
}
// Инициализация графика
Plotly.newPlot('plot', [
{
name: 'Начало',
x: [0], y: [0], z: [0],
mode: 'markers',
type: 'scatter3d',
marker: { size: 10, color: 'white', symbol: 'diamond',
line: { color: '#374151', width: 2 } }
},
{
name: 'Траектория',
x: [], y: [], z: [],
mode: 'lines',
type: 'scatter3d',
line: { color: '#f87171', width: 3 }
},
{
name: 'Лидар',
x: [], y: [], z: [],
mode: 'markers',
type: 'scatter3d',
marker: { size: 2, color: '#60a5fa', opacity: 0.5 }
},
{
name: 'Позиция',
x: [0], y: [0], z: [0],
mode: 'markers',
type: 'scatter3d',
marker: { size: 12, color: '#34d399', symbol: 'circle',
line: { color: '#6ee7b7', width: 2 } }
}
], {
scene: {
xaxis: { title: 'X (м)', color: '#9ca3af', gridcolor: '#374151', zerolinecolor: '#6b7280' },
yaxis: { title: 'Y (м)', color: '#9ca3af', gridcolor: '#374151', zerolinecolor: '#6b7280' },
zaxis: { title: 'Z (м)', color: '#9ca3af', gridcolor: '#374151', zerolinecolor: '#6b7280' },
bgcolor: '#111827',
aspectmode: 'cube',
camera: { eye: { x: 1.4, y: 1.4, z: 1.0 } }
},
paper_bgcolor: '#111827',
plot_bgcolor: '#111827',
showlegend: false,
margin: { l: 0, r: 0, b: 0, t: 0 }
});
async function update() {
try {
const r = await fetch('/api/data?t=' + Date.now());
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
updateCount++;
const pos = d.pos;
const hasData = d.traj.x.length > 0;
// Координаты
document.getElementById('coords').innerHTML =
`X: ${pos.x.toFixed(3)} м<br>Y: ${pos.y.toFixed(3)} м<br>Z: ${pos.z.toFixed(3)} м`;
// Таймер сессии
if (hasData && !startTime) {
startTime = Date.now();
sessionTimer = setInterval(() => {
document.getElementById('time-val').textContent =
fmtTime((Date.now() - startTime) / 1000);
}, 1000);
}
// Пройденное расстояние
if (lastPos && hasData) {
const dx = pos.x - lastPos.x,
dy = pos.y - lastPos.y,
dz = pos.z - lastPos.z;
const step = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (step < 0.5) totalDist += step; // фильтр прыжков
}
lastPos = pos;
document.getElementById('dist-val').textContent = totalDist.toFixed(2);
// Статус движения
const badge = document.getElementById('motion-badge');
if (!hasData) {
badge.textContent = 'Нет данных';
badge.className = 'badge badge-nodata';
} else {
const spd = lastPos ? Math.sqrt(
(pos.x-lastPos.x)**2 + (pos.y-lastPos.y)**2 + (pos.z-lastPos.z)**2
) : 0;
if (totalDist > 0.02 && spd > 0.001) {
badge.textContent = 'В движении';
badge.className = 'badge badge-moving';
} else {
badge.textContent = 'В покое';
badge.className = 'badge badge-still';
}
}
// Обновляем трассы
Plotly.restyle('plot', { x:[d.traj.x], y:[d.traj.y], z:[d.traj.z] }, 1);
Plotly.restyle('plot', { x:[d.lidar.x], y:[d.lidar.y], z:[d.lidar.z] }, 2);
Plotly.restyle('plot', { x:[[pos.x]], y:[[pos.y]], z:[[pos.z]] }, 3);
// Автоподгонка с одинаковым масштабом по всем осям
const allX = [0, pos.x].concat(d.traj.x);
const allY = [0, pos.y].concat(d.traj.y);
const allZ = [0, pos.z].concat(d.traj.z);
const newRange = squareRanges(allX, allY, allZ);
if (rangeChanged(newRange, lastRange)) {
lastRange = newRange;
Plotly.relayout('plot', {
'scene.xaxis.range': newRange[0],
'scene.yaxis.range': newRange[1],
'scene.zaxis.range': newRange[2]
});
}
document.getElementById('status').textContent =
`Обновлений: ${updateCount} | Траектория: ${d.traj.x.length} тч | Лидар: ${d.lidar.x.length} тч | ${new Date().toLocaleTimeString()}`;
} catch(e) {
document.getElementById('status').textContent = 'Ошибка: ' + e.message;
}
}
update();
setInterval(update, 200);
</script>
</body>
</html>
"""
def fetch_data():
"""Читает свежие данные из БД в read-only режиме (не мешает писателям)."""
conn = sqlite3.connect(f'file:{DB_PATH}?mode=ro', uri=True)
try:
# Текущая позиция
cur = conn.execute(
"SELECT x, y, z FROM trajectory ORDER BY timestamp DESC LIMIT 1"
)
row = cur.fetchone()
pos = {'x': row[0], 'y': row[1], 'z': row[2]} if row else {'x': 0.0, 'y': 0.0, 'z': 0.0}
# Последние N точек траектории (разворачиваем в хронологический порядок)
cur = conn.execute(
f"SELECT x, y, z FROM trajectory ORDER BY timestamp DESC LIMIT {TRAJ_LIMIT}"
)
traj_rows = cur.fetchall()[::-1]
traj = {
'x': [r[0] for r in traj_rows],
'y': [r[1] for r in traj_rows],
'z': [r[2] for r in traj_rows],
}
# Последние N точек лидара
cur = conn.execute(
f"SELECT x, y, z FROM lidar_points ORDER BY rowid DESC LIMIT {LIDAR_LIMIT}"
)
lidar_rows = cur.fetchall()
lidar = {
'x': [r[0] for r in lidar_rows],
'y': [r[1] for r in lidar_rows],
'z': [r[2] for r in lidar_rows],
}
finally:
conn.close()
return {'pos': pos, 'traj': traj, 'lidar': lidar}
class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
def do_GET(self):
if self.path == '/':
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write(HTML.encode('utf-8'))
elif self.path == '/plotly-latest.min.js':
self.send_response(200)
self.send_header('Content-type', 'application/javascript')
self.end_headers()
with open('plotly-latest.min.js', 'rb') as f:
self.wfile.write(f.read())
elif self.path.startswith('/api/data'):
try:
data = fetch_data()
payload = json.dumps(data).encode('utf-8')
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-store')
self.end_headers()
self.wfile.write(payload)
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(str(e).encode('utf-8'))
elif self.path.startswith('/api/pos'):
try:
conn = sqlite3.connect(f'file:{DB_PATH}?mode=ro', uri=True)
cur = conn.execute(
"SELECT x, y, z FROM trajectory ORDER BY timestamp DESC LIMIT 1"
)
row = cur.fetchone()
conn.close()
pos = {'x': row[0], 'y': row[1], 'z': row[2]} if row else {'x': 0, 'y': 0, 'z': 0}
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(pos).encode('utf-8'))
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(str(e).encode('utf-8'))
else:
self.send_response(404)
self.end_headers()
if __name__ == '__main__':
print("=" * 50)
print("INERTIAL TRACKER — реальное время")
print(f"Открыть: http://192.168.0.106:{PORT}")
print("=" * 50)
HTTPServer(('0.0.0.0', PORT), Handler).serve_forever()