project_inertial-control/processing/final_server_auto.py

453 lines
19 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
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:#0f0f0f; font-family: 'Courier New', monospace; }
#topbar {
position: fixed; top: 0; left: 0; right: 0; height: 48px;
background: #181818; border-bottom: 1px solid #2a2a2a;
display: flex; align-items: center; padding: 0 16px; gap: 24px;
z-index: 200;
}
#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: #181818; color: #6ee7b7;
padding: 12px 16px; font-size: 17px; z-index: 100;
border-radius: 10px; border: 1px solid #2a2a2a;
line-height: 1.8;
}
#legend {
position: fixed; top: 60px; left: 12px;
background: #181818; color: #d1d5db;
padding: 10px 14px; font-size: 13px; z-index: 100;
border-radius: 10px; border: 1px solid #2a2a2a;
line-height: 2.0;
}
#status {
position: fixed; bottom: 6px; left: 12px;
background: #0f0f0f; color: #4b5563;
padding: 4px 10px; font-size: 11px; z-index: 100;
border-radius: 6px;
}
#main-wrap {
position: fixed; top: 48px; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column;
}
#plot3d { flex: 0 0 62%; width: 100%; }
#plots2d {
flex: 1 1 0; display: flex; flex-direction: row;
border-top: 1px solid #2a2a2a;
}
.plot2d { flex: 1 1 0; min-width: 0; }
.plot2d:not(:last-child) { border-right: 1px solid #2a2a2a; }
</style>
</head>
<body>
<div id="topbar">
<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:#ffffff;">●</span> Положение робота<br>
<span style="color:#fbbf24;">━━</span> Направление (жёлтый луч)
</div>
<div id="status">Подключение...</div>
<div id="main-wrap">
<div id="plot3d"></div>
<div id="plots2d">
<div id="plot-xy" class="plot2d"></div>
<div id="plot-yz" class="plot2d"></div>
<div id="plot-zx" class="plot2d"></div>
</div>
</div>
<script>
let updateCount = 0;
let lastRange = null;
let totalDist = 0;
let lastPos = null;
let startTime = null;
let sessionTimer = null;
const BG = '#0f0f0f';
const GRID = '#1e1e1e';
const ZERO = '#2a2a2a';
const AXIS_COLOR = '#555';
const ARROW_LEN = 0.5; // длина жёлтого луча (м)
// ── Матрица полного поворота ─────────────────────────────────────────
function rotFull(roll, pitch, yaw) {
const cr=Math.cos(roll), sr=Math.sin(roll);
const cp=Math.cos(pitch), sp=Math.sin(pitch);
const cy=Math.cos(yaw), sy=Math.sin(yaw);
return [
[cy*cp, cy*sp*sr-sy*cr, cy*sp*cr+sy*sr],
[sy*cp, sy*sp*sr+cy*cr, sy*sp*cr-cy*sr],
[-sp, cp*sr, cp*cr]
];
}
// Применение матрицы поворота к вектору
function applyRot(R, v) {
return [
R[0][0]*v[0] + R[0][1]*v[1] + R[0][2]*v[2],
R[1][0]*v[0] + R[1][1]*v[1] + R[1][2]*v[2],
R[2][0]*v[0] + R[2][1]*v[1] + R[2][2]*v[2]
];
}
// ── Вспомогательные функции ──────────────────────────────────────────
function minMax(arr) {
if (!arr || !arr.length) return [0,0];
let mn=arr[0], mx=arr[0];
for (const v of arr) { if(v<mn)mn=v; if(v>mx)mx=v; }
return [mn, mx];
}
function squareRanges(ax, ay, az) {
const rx=minMax(ax), ry=minMax(ay), rz=minMax(az);
const maxSpan=Math.max(rx[1]-rx[0],ry[1]-ry[0],rz[1]-rz[0],0.5);
const half=maxSpan/2*1.2;
const mid=r=>(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(nR, oR) {
if (!oR) return true;
for (let i=0;i<nR.length;i++) {
if(Math.abs(nR[i][0]-oR[i][0])>0.25||Math.abs(nR[i][1]-oR[i][1])>0.25) return true;
}
return false;
}
function square2D(ax, ay) {
const rx=minMax(ax), ry=minMax(ay);
const maxSpan=Math.max(rx[1]-rx[0],ry[1]-ry[0],0.5);
const half=maxSpan/2*1.2;
const mid=r=>(r[0]+r[1])/2;
return [[mid(rx)-half,mid(rx)+half],[mid(ry)-half,mid(ry)+half]];
}
function fmtTime(sec) {
const m=Math.floor(sec/60), s=Math.floor(sec%60);
return m+':'+String(s).padStart(2,'0');
}
const darkAxis3D = label => ({
title:label, color:AXIS_COLOR,
gridcolor:GRID, zerolinecolor:ZERO,
backgroundcolor: BG, showbackground: true
});
const darkAxis2D = label => ({
title:label, color:AXIS_COLOR,
gridcolor:GRID, zerolinecolor:ZERO,
tickfont:{color:AXIS_COLOR}
});
const darkLayout2D = (xL, yL) => ({
xaxis: darkAxis2D(xL), yaxis: darkAxis2D(yL),
paper_bgcolor: BG, plot_bgcolor: BG,
showlegend: false,
margin:{l:44,r:10,b:36,t:24},
font:{color:AXIS_COLOR, size:11}
});
// ── Инициализация 3D-графика ─────────────────────────────────────────
// Трейсы:
// 0 начальная точка (белый ромб)
// 1 траектория (красная линия)
// 2 лидар (голубые точки)
// 3 кружок текущее положение (белый круг)
// 4 жёлтый луч направления (линия)
Plotly.newPlot('plot3d', [
{
name:'Начало', x:[0],y:[0],z:[0],
mode:'markers', type:'scatter3d',
marker:{size:10,color:'white',symbol:'diamond',line:{color:'#2a2a2a',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:'white',symbol:'circle',line:{color:'#fbbf24',width:2}}
},
{
name:'Направление', x:[0,0], y:[0,0], z:[0,0],
mode:'lines', type:'scatter3d',
line:{color:'#fbbf24',width:4},
hoverinfo:'none'
}
], {
scene: {
xaxis: darkAxis3D('X (м)'),
yaxis: darkAxis3D('Y (м)'),
zaxis: darkAxis3D('Z (м)'),
bgcolor: BG,
aspectmode: 'cube',
camera: {eye:{x:1.4,y:1.4,z:1.0}}
},
paper_bgcolor: BG,
plot_bgcolor: BG,
showlegend: false,
margin:{l:0,r:0,b:0,t:0}
}, {responsive:true});
// ── 2D проекции ───────────────────────────────────────────────────────
function init2D(divId, xL, yL) {
Plotly.newPlot(divId, [
{ x:[0],y:[0], mode:'markers', type:'scatter',
marker:{size:8,color:'white',symbol:'diamond',line:{color:'#2a2a2a',width:1}} },
{ x:[],y:[], mode:'lines', type:'scatter',
line:{color:'#f87171',width:2} },
{ x:[0],y:[0], mode:'markers', type:'scatter',
marker:{size:10,color:'#34d399',line:{color:'#6ee7b7',width:2}} }
], darkLayout2D(xL,yL), {responsive:true});
}
init2D('plot-xy','X (м)','Y (м)');
init2D('plot-yz','Y (м)','Z (м)');
init2D('plot-zx','Z (м)','X (м)');
// ── Главный цикл обновления ────────────────────────────────────────────
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 if (totalDist>0.02) {
badge.textContent='В движении'; badge.className='badge badge-moving';
} else {
badge.textContent='В покое'; badge.className='badge badge-still';
}
// Обновляем траекторию и лидар
Plotly.restyle('plot3d',{x:[d.traj.x],y:[d.traj.y],z:[d.traj.z]},1);
Plotly.restyle('plot3d',{x:[d.lidar.x],y:[d.lidar.y],z:[d.lidar.z]},2);
// ── Кружок (текущее положение) ────────────────────────────────
Plotly.restyle('plot3d',{x:[pos.x], y:[pos.y], z:[pos.z]}, 3);
// ── Жёлтый луч направления ────────────────────────────────────
const roll = d.orient.roll;
const pitch = d.orient.pitch;
const yaw = d.orient.yaw;
const R = rotFull(roll, pitch, yaw);
const dir = applyRot(R, [ARROW_LEN, 0, 0]); // вектор вперёд
const endX = pos.x + dir[0];
const endY = pos.y + dir[1];
const endZ = pos.z + dir[2];
Plotly.restyle('plot3d',{
x:[[pos.x, endX]], y:[[pos.y, endY]], z:[[pos.z, endZ]]
}, 4);
// Автомасштаб 3D
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('plot3d',{
'scene.xaxis.range':newRange[0],
'scene.yaxis.range':newRange[1],
'scene.zaxis.range':newRange[2]
});
}
// 2D проекции
const tx=d.traj.x, ty=d.traj.y, tz=d.traj.z;
Plotly.restyle('plot-xy',{x:[tx],y:[ty]},1);
Plotly.restyle('plot-xy',{x:[[pos.x]],y:[[pos.y]]},2);
const rXY=square2D([0,pos.x].concat(tx),[0,pos.y].concat(ty));
Plotly.relayout('plot-xy',{'xaxis.range':rXY[0],'yaxis.range':rXY[1]});
Plotly.restyle('plot-yz',{x:[ty],y:[tz]},1);
Plotly.restyle('plot-yz',{x:[[pos.y]],y:[[pos.z]]},2);
const rYZ=square2D([0,pos.y].concat(ty),[0,pos.z].concat(tz));
Plotly.relayout('plot-yz',{'xaxis.range':rYZ[0],'yaxis.range':rYZ[1]});
Plotly.restyle('plot-zx',{x:[tz],y:[tx]},1);
Plotly.restyle('plot-zx',{x:[[pos.z]],y:[[pos.x]]},2);
const rZX=square2D([0,pos.z].concat(tz),[0,pos.x].concat(tx));
Plotly.relayout('plot-zx',{'xaxis.range':rZX[0],'yaxis.range':rZX[1]});
document.getElementById('status').textContent =
`Обновлений: ${updateCount} | Traj: ${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():
conn = sqlite3.connect(f'file:{DB_PATH}?mode=ro', uri=True)
try:
cur = conn.execute(
"SELECT x, y, z, roll, pitch, yaw FROM trajectory ORDER BY timestamp DESC LIMIT 1"
)
row = cur.fetchone()
if row:
pos = {'x': row[0], 'y': row[1], 'z': row[2]}
orient = {'roll': row[3] or 0.0, 'pitch': row[4] or 0.0, 'yaw': row[5] or 0.0}
else:
pos = {'x': 0.0, 'y': 0.0, 'z': 0.0}
orient = {'roll': 0.0, 'pitch': 0.0, 'yaw': 0.0}
cur = conn.execute(
f"SELECT x, y, z FROM trajectory ORDER BY timestamp ASC 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],
}
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, 'orient': orient, '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()