267 lines
8.3 KiB
HTML
267 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Robot Tracker v009</title>
|
|
<style>
|
|
html, body { margin: 0; padding: 0; background: #0e0e11; color: #e0e0e0; font-family: system-ui, sans-serif; }
|
|
canvas { display: block; margin: auto; background: #14141a; cursor: crosshair; }
|
|
#hud {
|
|
position: absolute; top: 10px; left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex; gap: 10px; align-items: center;
|
|
pointer-events: none;
|
|
}
|
|
#status { font-size: 12px; padding: 4px 12px; border-radius: 99px; background: #1f1f28; color: #888; }
|
|
#status.ok { color: #4fd1c5; }
|
|
#status.err { color: #f87171; }
|
|
#cal-status { font-size: 12px; padding: 4px 12px; border-radius: 99px; background: #1f1f28; color: #f59e0b; }
|
|
#cal-status.ok { color: #4fd1c5; }
|
|
#cal-btn { font-size: 12px; padding: 4px 14px; border-radius: 99px; background: #4fd1c5; color: #0e0e11; border: none; cursor: pointer; pointer-events: all; font-weight: 600; }
|
|
#cal-btn:hover { background: #38b2ac; }
|
|
#info { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); font-size: 12px; color: #555; pointer-events: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="hud">
|
|
<div id="status">Verbinde…</div>
|
|
<div id="cal-status">Nicht kalibriert</div>
|
|
<button id="cal-btn" onclick="calibrate()">Kalibrieren</button>
|
|
</div>
|
|
<canvas id="view" width="1280" height="720"></canvas>
|
|
<div id="info">Klick → Ziel setzen | Hindernisse = rot markiert | Geschwindigkeit = grüner Balken</div>
|
|
|
|
<script>
|
|
const canvas = document.getElementById("view");
|
|
const ctx = canvas.getContext("2d");
|
|
const statusEl = document.getElementById("status");
|
|
const calEl = document.getElementById("cal-status");
|
|
|
|
const FADE_SECONDS = 0.8;
|
|
const ARROW_LENGTH = 34;
|
|
const VEL_SCALE = 0.3;
|
|
const VEL_MIN_SHOW = 8;
|
|
const SAFE_DIST_PX = 60;
|
|
const SPEED_MAX_MMS = 350; // für Balkenanzeige
|
|
|
|
let goalX = null, goalY = null;
|
|
let isCalibrated = false;
|
|
let ws;
|
|
|
|
function drawGrid() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.strokeStyle = "#1f1f28";
|
|
ctx.lineWidth = 1;
|
|
const step = 80;
|
|
for (let x = 0; x <= canvas.width; x += step) {
|
|
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
|
|
}
|
|
for (let y = 0; y <= canvas.height; y += step) {
|
|
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
|
}
|
|
ctx.strokeStyle = "#3a3a44";
|
|
ctx.strokeRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
function drawFieldCorners(corners) {
|
|
if (!corners || corners.length < 4) return;
|
|
ctx.save();
|
|
ctx.strokeStyle = "#1D9E75";
|
|
ctx.lineWidth = 1.5;
|
|
ctx.setLineDash([8, 4]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(corners[0][0], corners[0][1]);
|
|
ctx.lineTo(corners[1][0], corners[1][1]);
|
|
ctx.lineTo(corners[2][0], corners[2][1]);
|
|
ctx.lineTo(corners[3][0], corners[3][1]);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
ctx.fillStyle = "#1D9E75";
|
|
ctx.font = "11px system-ui";
|
|
ctx.fillText("Kalibrierfeld", corners[0][0] + 5, corners[0][1] + 15);
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawSpeedBar(x, y, speed_mm_s, speed_l, speed_r) {
|
|
if (speed_mm_s === undefined) return;
|
|
const barW = 60;
|
|
const barH = 6;
|
|
const bx = x - barW / 2;
|
|
const by = y + 28;
|
|
const pct = Math.min(1, Math.abs(speed_mm_s) / SPEED_MAX_MMS);
|
|
|
|
// Hintergrund
|
|
ctx.save();
|
|
ctx.fillStyle = "#2a2a35";
|
|
ctx.fillRect(bx, by, barW, barH);
|
|
|
|
// Füllstand
|
|
const color = speed_mm_s > 200 ? "#f59e0b" : speed_mm_s > 100 ? "#4fd1c5" : "#1D9E75";
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(bx, by, barW * pct, barH);
|
|
|
|
// Geschwindigkeit als Text
|
|
ctx.fillStyle = "#e0e0e0";
|
|
ctx.font = "10px system-ui";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText(Math.round(speed_mm_s) + " mm/s", x, by + 18);
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawObstacle(obs) {
|
|
const { x, y, id, x_mm, y_mm } = obs;
|
|
ctx.save();
|
|
ctx.strokeStyle = "rgba(220, 50, 50, 0.25)";
|
|
ctx.lineWidth = 1;
|
|
ctx.setLineDash([3, 3]);
|
|
ctx.beginPath(); ctx.arc(x, y, SAFE_DIST_PX, 0, Math.PI * 2); ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
ctx.strokeStyle = "#E24B4A";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(x, y, 12, 0, Math.PI * 2); ctx.stroke();
|
|
ctx.fillStyle = "#E24B4A";
|
|
ctx.font = "11px system-ui";
|
|
ctx.textAlign = "center";
|
|
ctx.fillText("ID " + id, x, y - 18);
|
|
if (x_mm !== undefined) {
|
|
ctx.fillStyle = "#888";
|
|
ctx.fillText(x_mm.toFixed(0) + "mm, " + y_mm.toFixed(0) + "mm", x, y - 6);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawGoal(x, y) {
|
|
ctx.save();
|
|
ctx.strokeStyle = "#f59e0b";
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.beginPath(); ctx.arc(x, y, 30, 0, Math.PI * 2); ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x - 10, y); ctx.lineTo(x + 10, y);
|
|
ctx.moveTo(x, y - 10); ctx.lineTo(x, y + 10);
|
|
ctx.stroke();
|
|
ctx.fillStyle = "#f59e0b";
|
|
ctx.font = "11px system-ui";
|
|
ctx.fillText("Ziel", x + 14, y - 14);
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawTag(tag) {
|
|
const { id, x, y, theta, age, vx = 0, vy = 0, omega = 0, x_mm, y_mm, speed_mm_s, speed_actual_l, speed_actual_r } = tag;
|
|
const alpha = Math.max(0, 1.0 - (age ?? 0) / FADE_SECONDS);
|
|
if (alpha <= 0) return;
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = alpha;
|
|
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(theta * Math.PI / 180);
|
|
ctx.strokeStyle = "#4fd1c5";
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -ARROW_LENGTH); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(-5, -ARROW_LENGTH + 8); ctx.lineTo(0, -ARROW_LENGTH); ctx.lineTo(5, -ARROW_LENGTH + 8); ctx.stroke();
|
|
ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.stroke();
|
|
ctx.restore();
|
|
|
|
const speed = Math.sqrt(vx * vx + vy * vy);
|
|
if (speed >= VEL_MIN_SHOW) {
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.strokeStyle = "#a78bfa";
|
|
ctx.lineWidth = 1.5;
|
|
ctx.setLineDash([4, 3]);
|
|
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(vx * VEL_SCALE, vy * VEL_SCALE); ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
|
|
ctx.fillStyle = "#e0e0e0";
|
|
ctx.font = "11px system-ui";
|
|
ctx.textAlign = "left";
|
|
ctx.textBaseline = "top";
|
|
const lines = [
|
|
"ID " + id,
|
|
x_mm !== undefined ? x_mm.toFixed(1) + "mm, " + y_mm.toFixed(1) + "mm" : "x=" + x.toFixed(0) + " y=" + y.toFixed(0),
|
|
"θ=" + theta.toFixed(1) + "°",
|
|
];
|
|
// Geschwindigkeit nur für Roboter (wenn speed_mm_s vorhanden)
|
|
if (speed_mm_s !== undefined) {
|
|
lines.push("L:" + (speed_actual_l||0).toFixed(0) + " R:" + (speed_actual_r||0).toFixed(0) + " mm/s");
|
|
}
|
|
lines.forEach((line, i) => ctx.fillText(line, x + 10, y + 10 + i * 13));
|
|
|
|
// Geschwindigkeitsbalken für Roboter
|
|
if (speed_mm_s !== undefined) {
|
|
drawSpeedBar(x, y, speed_mm_s, speed_actual_l, speed_actual_r);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function calibrate() {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: "calibrate" }));
|
|
calEl.textContent = "Kalibriere…";
|
|
}
|
|
}
|
|
|
|
canvas.addEventListener("click", (e) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const scaleX = canvas.width / rect.width;
|
|
const scaleY = canvas.height / rect.height;
|
|
goalX = (e.clientX - rect.left) * scaleX;
|
|
goalY = (e.clientY - rect.top) * scaleY;
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type: "goal", x: goalX, y: goalY }));
|
|
}
|
|
});
|
|
|
|
drawGrid();
|
|
let reconnectTimer;
|
|
|
|
function connect() {
|
|
ws = new WebSocket("ws://" + location.host + "/ws");
|
|
|
|
ws.onopen = () => { statusEl.textContent = "Verbunden"; statusEl.className = "ok"; };
|
|
|
|
ws.onmessage = (ev) => {
|
|
const data = JSON.parse(ev.data);
|
|
|
|
if (data.type === "calibration_result") {
|
|
calEl.textContent = data.success ? "Kalibriert ✓" : "Fehlgeschlagen";
|
|
calEl.className = data.success ? "ok" : "";
|
|
return;
|
|
}
|
|
|
|
if (data.calibrated) {
|
|
isCalibrated = true;
|
|
calEl.textContent = "Kalibriert ✓";
|
|
calEl.className = "ok";
|
|
}
|
|
|
|
drawGrid();
|
|
if (data.field_corners_px) drawFieldCorners(data.field_corners_px);
|
|
(data.obstacles || []).forEach(drawObstacle);
|
|
if (goalX !== null) drawGoal(goalX, goalY);
|
|
|
|
(data.tags || []).forEach(tag => {
|
|
const isObstacle = (data.obstacles || []).some(o => o.id === tag.id);
|
|
if (!isObstacle) drawTag(tag);
|
|
});
|
|
};
|
|
|
|
ws.onerror = () => { statusEl.textContent = "Verbindungsfehler"; statusEl.className = "err"; };
|
|
ws.onclose = () => {
|
|
statusEl.textContent = "Getrennt — reconnect…";
|
|
statusEl.className = "err";
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = setTimeout(connect, 2000);
|
|
};
|
|
}
|
|
|
|
connect();
|
|
</script>
|
|
</body>
|
|
</html>
|