839 lines
28 KiB
HTML
839 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>ExoMy // Ground Control</title>
|
|
<script type="text/javascript" src="./vendor/roslib.min.js"></script>
|
|
<link rel="stylesheet" href="./app.css?v=20260522105000">
|
|
</head>
|
|
<body class="page-index">
|
|
<header>
|
|
<svg class="hdr-icon" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="7" y="11" width="18" height="10" rx="1" fill="#0088FF" opacity=".85"/>
|
|
<rect x="11" y="7" width="10" height="4" rx="1" fill="#0088FF" opacity=".5"/>
|
|
<circle cx="9" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
|
|
<circle cx="23" cy="23" r="3.5" fill="none" stroke="#0088FF" stroke-width="1.5"/>
|
|
<circle cx="9" cy="23" r="1.3" fill="#0088FF"/>
|
|
<circle cx="23" cy="23" r="1.3" fill="#0088FF"/>
|
|
<rect x="22" y="8" width="4" height="3" rx=".5" fill="#FF6B00" opacity=".9"/>
|
|
<line x1="16" y1="7" x2="16" y2="3" stroke="#0088FF" stroke-width="1.5"/>
|
|
<circle cx="16" cy="2.5" r="1" fill="#0088FF"/>
|
|
<rect x="3" y="14" width="4" height="2" rx="1" fill="#0088FF" opacity=".6"/>
|
|
</svg>
|
|
|
|
<span class="hdr-title">EXO<em>MY</em> GROUND CTRL</span>
|
|
<div class="hdr-div"></div>
|
|
<div class="hdr-badge">Mission Ops</div>
|
|
<div class="hdr-gap"></div>
|
|
<a class="admin-link" href="./admin.html">Admin</a>
|
|
<div class="hdr-div"></div>
|
|
<div class="mission-ok"><div class="pulse-dot"></div><span id="mission-state">Bereit</span></div>
|
|
<div class="hdr-div"></div>
|
|
<div class="hdr-clocks">
|
|
<div class="hdr-clk">
|
|
<div class="hdr-clk-lbl">Session</div>
|
|
<div class="hdr-clk-val ora" id="met">00:00:00</div>
|
|
</div>
|
|
<div class="hdr-div"></div>
|
|
<div class="hdr-clk">
|
|
<div class="hdr-clk-lbl">UTC</div>
|
|
<div class="hdr-clk-val" id="utc">--:--:--</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="main">
|
|
<div class="col-l">
|
|
<div class="panel" id="cam-panel">
|
|
<div class="p-hdr">
|
|
<div class="p-dot"></div>
|
|
<span class="p-lbl">Kamera — CAM01</span>
|
|
<span class="p-tag">Live Stream</span>
|
|
</div>
|
|
<div class="cam-port">
|
|
<div class="cam-frame" id="video_frame">
|
|
<img id="video_output" alt="Kamerabild vom ExoMy" />
|
|
<div class="c-rec"><div class="c-rec-dot"></div>LIVE</div>
|
|
<div class="c-tl">
|
|
CAM: <span id="cam-state">Verbinde…</span><br>
|
|
MODE: <span id="overlay-mode">Ackermann</span>
|
|
</div>
|
|
<div class="c-tr">
|
|
ROS: <span id="ros-state">Offline</span><br>
|
|
CTRL: <span id="input-state">Joystick</span>
|
|
</div>
|
|
<div class="c-bl">
|
|
X: <span id="cam-x">+0.00</span><br>
|
|
Y: <span id="cam-y">+0.00</span>
|
|
</div>
|
|
<div class="c-br">
|
|
HOST: <span id="cam-host">--</span><br>
|
|
STREAM: 8083
|
|
</div>
|
|
<div class="c-xhair">
|
|
<svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
|
|
<!-- äußerer rotierender Strichelring -->
|
|
<circle class="xh-rot" cx="0" cy="0" r="82" fill="none" stroke="rgba(0,255,145,.2)" stroke-width="1" stroke-dasharray="4 10"/>
|
|
<!-- mittlerer Ring, Gegenrichtung -->
|
|
<circle class="xh-rot-rev" cx="0" cy="0" r="60" fill="none" stroke="rgba(0,255,145,.15)" stroke-width=".75" stroke-dasharray="2 16"/>
|
|
<!-- innerer fester Ring -->
|
|
<circle cx="0" cy="0" r="16" fill="none" stroke="rgba(0,255,145,.3)" stroke-width=".75"/>
|
|
<!-- Kreuzlinien mit Lücke in der Mitte -->
|
|
<line x1="-86" y1="0" x2="-19" y2="0" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
|
|
<line x1="19" y1="0" x2="86" y2="0" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
|
|
<line x1="0" y1="-86" x2="0" y2="-19" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
|
|
<line x1="0" y1="19" x2="0" y2="86" stroke="rgba(0,255,145,.6)" stroke-width="1"/>
|
|
<!-- Skalenstriche bei r=60 -->
|
|
<line x1="-60" y1="-6" x2="-60" y2="6" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
|
|
<line x1="60" y1="-6" x2="60" y2="6" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
|
|
<line x1="-6" y1="-60" x2="6" y2="-60" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
|
|
<line x1="-6" y1="60" x2="6" y2="60" stroke="rgba(0,255,145,.45)" stroke-width=".75"/>
|
|
<!-- Skalenstriche bei r=38 -->
|
|
<line x1="-38" y1="-3.5" x2="-38" y2="3.5" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
|
|
<line x1="38" y1="-3.5" x2="38" y2="3.5" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
|
|
<line x1="-3.5" y1="-38" x2="3.5" y2="-38" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
|
|
<line x1="-3.5" y1="38" x2="3.5" y2="38" stroke="rgba(0,255,145,.3)" stroke-width=".5"/>
|
|
<!-- Eckklammern -->
|
|
<path d="M-74,-58 L-74,-74 L-58,-74" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
|
|
<path d="M58,-74 L74,-74 L74,-58" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
|
|
<path d="M-74,58 L-74,74 L-58,74" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
|
|
<path d="M58,74 L74,74 L74,58" fill="none" stroke="rgba(0,255,145,.75)" stroke-width="1.5"/>
|
|
<!-- pulsierender Ring -->
|
|
<circle class="xh-pulse" cx="0" cy="0" r="6" fill="none" stroke="rgba(0,255,145,.7)" stroke-width="1"/>
|
|
<!-- Mittelpunkt -->
|
|
<circle cx="0" cy="0" r="2.5" fill="rgba(0,255,145,.9)"/>
|
|
<circle cx="0" cy="0" r="1" fill="rgba(255,255,255,.7)"/>
|
|
</svg>
|
|
</div>
|
|
<div class="c-scanlines"></div>
|
|
<div class="c-vignette"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-r">
|
|
<div class="panel collapsed" id="stat-panel">
|
|
<div class="p-hdr stat-panel-toggle" id="stat-panel-toggle" role="button" tabindex="0" aria-expanded="false" aria-controls="stat-panel-body">
|
|
<div class="p-dot"></div>
|
|
<span class="p-lbl">Systemstatus</span>
|
|
<span class="p-tag">Web Control</span>
|
|
<span class="stat-toggle-indicator" aria-hidden="true">+</span>
|
|
</div>
|
|
<div class="stat-list" id="stat-panel-body">
|
|
|
|
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Status</span><div class="ss-hdr-line"></div></div>
|
|
|
|
<div class="sr">
|
|
<span class="sr-led led-ok"></span>
|
|
<span class="sr-key">Link</span>
|
|
<span class="sr-val sval-conn"><span class="pulse-dot"></span><span id="status">Verbinde…</span></span>
|
|
</div>
|
|
<div class="sr">
|
|
<span class="sr-led led-blue"></span>
|
|
<span class="sr-key">Modus</span>
|
|
<span class="sr-val b" id="stat-mode">Ackermann</span>
|
|
</div>
|
|
<div class="sr">
|
|
<span class="sr-led led-warn"></span>
|
|
<span class="sr-key">Motoren</span>
|
|
<span class="sr-val o" id="stat-motors">Unbekannt</span>
|
|
</div>
|
|
<div class="sr">
|
|
<span class="sr-led led-blue"></span>
|
|
<span class="sr-key">Eingabe</span>
|
|
<span class="sr-val b" id="stat-input">Joystick</span>
|
|
</div>
|
|
|
|
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Telemetrie</span><div class="ss-hdr-line"></div></div>
|
|
|
|
<div class="sr sr-gauge gauge-cpu">
|
|
<span class="sr-key">CPU</span>
|
|
<div class="gauge"><div class="gauge-fill" id="bar-cpu"></div></div>
|
|
<span class="gauge-num" id="val-cpu">--</span>
|
|
</div>
|
|
<div class="sr sr-gauge gauge-temp">
|
|
<span class="sr-key">Temp</span>
|
|
<div class="gauge"><div class="gauge-fill" id="bar-temp"></div></div>
|
|
<span class="gauge-num" id="val-temp">--</span>
|
|
</div>
|
|
<div class="sr sr-gauge gauge-mem">
|
|
<span class="sr-key">RAM</span>
|
|
<div class="gauge"><div class="gauge-fill" id="bar-mem"></div></div>
|
|
<span class="gauge-num" id="val-mem">--</span>
|
|
</div>
|
|
|
|
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Netzwerk</span><div class="ss-hdr-line"></div></div>
|
|
|
|
<div class="sr">
|
|
<span class="sr-led led-ok"></span>
|
|
<span class="sr-key">WLAN</span>
|
|
<span class="sr-val b" id="stat-wifi">--</span>
|
|
</div>
|
|
<div class="sr">
|
|
<span class="sr-led led-blue"></span>
|
|
<span class="sr-key">IP</span>
|
|
<span class="sr-val b" id="stat-ips">--</span>
|
|
</div>
|
|
<div class="sr">
|
|
<span class="sr-led led-blue"></span>
|
|
<span class="sr-key">Uptime</span>
|
|
<span class="sr-val b" id="stat-uptime">--</span>
|
|
</div>
|
|
<div class="sr">
|
|
<span class="sr-led led-blue"></span>
|
|
<span class="sr-key">Disk</span>
|
|
<span class="sr-val b" id="stat-disk">--</span>
|
|
</div>
|
|
<div class="sr">
|
|
<span class="sr-led led-off"></span>
|
|
<span class="sr-key">Spannung</span>
|
|
<span class="sr-val b" id="stat-undervoltage">--</span>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel" id="joy-panel">
|
|
<div class="p-hdr">
|
|
<div class="p-dot"></div>
|
|
<span class="p-lbl">Lokomotion</span>
|
|
<div class="mode-toggle">
|
|
<button class="mtbtn active" id="mt-joy">Joystick</button>
|
|
<button class="mtbtn" id="mt-dpad">D-Pad</button>
|
|
</div>
|
|
</div>
|
|
<div class="drive-inline">
|
|
<button class="dbtn active" data-mode="ackermann" data-button="1">ACK</button>
|
|
<button class="dbtn" data-mode="turn-on-point" data-button="0">TOP</button>
|
|
<button class="dbtn" data-mode="crab" data-button="3">CRAB</button>
|
|
<button class="dbtn" id="motor-toggle">MOT</button>
|
|
</div>
|
|
|
|
<div class="joy-area" id="joy-area">
|
|
<div class="joy-base" id="jbase">
|
|
<div class="jg-h"></div><div class="jg-v"></div>
|
|
<div class="jring jr1"></div>
|
|
<div class="jring jr2"></div>
|
|
<div class="joy-thumb" id="jthumb"></div>
|
|
</div>
|
|
<div class="joy-vals">
|
|
<div class="joy-ax">
|
|
<div class="joy-ax-n" id="joy-x-label">X-Achse</div>
|
|
<div class="joy-ax-v" id="jx">+0.00</div>
|
|
</div>
|
|
<div class="joy-ax">
|
|
<div class="joy-ax-n">Y-Achse</div>
|
|
<div class="joy-ax-v" id="jy">+0.00</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dpad-wrap" id="dpad-area">
|
|
<div class="dpad-grid">
|
|
<button class="dpad-btn dpad-diag" data-x="-0.71" data-y="0.71">↖</button>
|
|
<button class="dpad-btn" data-x="0" data-y="1">▲</button>
|
|
<button class="dpad-btn dpad-diag" data-x="0.71" data-y="0.71">↗</button>
|
|
<button class="dpad-btn" data-x="-1" data-y="0">◄</button>
|
|
<div class="dpad-mid"></div>
|
|
<button class="dpad-btn" data-x="1" data-y="0">►</button>
|
|
<button class="dpad-btn dpad-diag" data-x="-0.71" data-y="-0.71">↙</button>
|
|
<button class="dpad-btn" data-x="0" data-y="-1">▼</button>
|
|
<button class="dpad-btn dpad-diag" data-x="0.71" data-y="-0.71">↘</button>
|
|
</div>
|
|
<div class="joy-vals">
|
|
<div class="joy-ax">
|
|
<div class="joy-ax-n" id="dpad-x-label">X-Achse</div>
|
|
<div class="joy-ax-v" id="dpx">+0.00</div>
|
|
</div>
|
|
<div class="joy-ax">
|
|
<div class="joy-ax-n">Y-Achse</div>
|
|
<div class="joy-ax-v" id="dpy">+0.00</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var hostUrl = window.location.hostname;
|
|
var ros = null;
|
|
var joyListener = null;
|
|
var roverCommandListener = null;
|
|
var publishTimer = null;
|
|
var statusTimer = null;
|
|
var axes = [0, 0, 0, 0, 0, 0];
|
|
var buttons = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
var driveMode = "ackermann";
|
|
var motorsEnabled = true;
|
|
var inputMode = "joystick";
|
|
var metBase = Date.now();
|
|
var dragging = false;
|
|
|
|
function setText(id, text) {
|
|
var el = document.getElementById(id);
|
|
if (el) {
|
|
el.textContent = text;
|
|
}
|
|
}
|
|
|
|
function formatAxis(value) {
|
|
return (value >= 0 ? "+" : "") + value.toFixed(2);
|
|
}
|
|
|
|
function axisBarWidth(value) {
|
|
return Math.max(0, Math.min(100, ((value + 1) / 2) * 100)).toFixed(0) + "%";
|
|
}
|
|
|
|
function percentTextToWidth(value) {
|
|
if (!value || typeof value !== "string") {
|
|
return "0%";
|
|
}
|
|
var numeric = parseInt(value.replace("%", ""), 10);
|
|
if (isNaN(numeric)) {
|
|
return "0%";
|
|
}
|
|
return Math.max(0, Math.min(100, numeric)) + "%";
|
|
}
|
|
|
|
function temperatureTextToWidth(value) {
|
|
if (!value || typeof value !== "string") {
|
|
return "0%";
|
|
}
|
|
var numeric = parseFloat(value.replace("°C", "").replace(",", "."));
|
|
if (isNaN(numeric)) {
|
|
return "0%";
|
|
}
|
|
return Math.max(0, Math.min(100, ((numeric - 20) / 60) * 100)).toFixed(0) + "%";
|
|
}
|
|
|
|
function updateAxesDisplay(x, y) {
|
|
setText("jx", formatAxis(x));
|
|
setText("jy", formatAxis(y));
|
|
setText("dpx", formatAxis(x));
|
|
setText("dpy", formatAxis(y));
|
|
setText("cam-x", formatAxis(x));
|
|
setText("cam-y", formatAxis(y));
|
|
}
|
|
|
|
function updateModeDisplay() {
|
|
var label = "Ackermann";
|
|
if (driveMode === "turn-on-point") {
|
|
label = "Turn on Point";
|
|
} else if (driveMode === "crab") {
|
|
label = "Crab";
|
|
}
|
|
setText("overlay-mode", label);
|
|
setText("stat-mode", label);
|
|
}
|
|
|
|
function applyDriveMode(mode, flash) {
|
|
var changed = mode !== driveMode;
|
|
driveMode = mode;
|
|
document.querySelectorAll(".dbtn[data-mode]").forEach(function (btn) {
|
|
var isActive = btn.dataset.mode === mode;
|
|
btn.classList.toggle("active", isActive);
|
|
if (isActive && changed && flash) {
|
|
btn.classList.remove("flash");
|
|
void btn.offsetWidth;
|
|
btn.classList.add("flash");
|
|
}
|
|
});
|
|
updateModeDisplay();
|
|
updateControlLocks();
|
|
}
|
|
|
|
function mirrorJoyThumb(x, y) {
|
|
if (dragging || inputMode !== "joystick") return;
|
|
var data = joyMetrics();
|
|
var dx = x * data.maxD;
|
|
var dy = -y * data.maxD;
|
|
var thumb = document.getElementById("jthumb");
|
|
thumb.style.transition = "left .05s, top .05s";
|
|
thumb.style.left = "calc(50% + " + dx + "px)";
|
|
thumb.style.top = "calc(50% + " + dy + "px)";
|
|
thumb.style.transform = "translate(-50%,-50%)";
|
|
updateAxesDisplay(x, y);
|
|
}
|
|
|
|
function locomotionModeToUi(modeValue) {
|
|
if (modeValue === 2) {
|
|
return "turn-on-point";
|
|
}
|
|
if (modeValue === 3) {
|
|
return "crab";
|
|
}
|
|
return "ackermann";
|
|
}
|
|
|
|
function updateMotorDisplay() {
|
|
setText("stat-motors", motorsEnabled ? "Aktiv" : "Aus");
|
|
}
|
|
|
|
function updateInputDisplay() {
|
|
var label = inputMode === "joystick" ? "Joystick" : "D-Pad";
|
|
setText("input-state", label);
|
|
setText("stat-input", label);
|
|
}
|
|
|
|
function setRosStatus(text) {
|
|
setText("status", text);
|
|
setText("ros-state", text);
|
|
setText("mission-state", text === "Verbunden" ? "Nominal" : text);
|
|
}
|
|
|
|
function updateUndervoltageDisplay(text, state) {
|
|
var el = document.getElementById("stat-undervoltage");
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.textContent = text || "--";
|
|
el.classList.remove("b", "g", "o", "alert", "warntext");
|
|
|
|
if (state === "active") {
|
|
el.classList.add("alert");
|
|
return;
|
|
}
|
|
if (state === "past") {
|
|
el.classList.add("warntext");
|
|
return;
|
|
}
|
|
if (state === "clear") {
|
|
el.classList.add("g");
|
|
return;
|
|
}
|
|
el.classList.add("b");
|
|
}
|
|
|
|
function updateServiceState(payload) {
|
|
var system = payload.system || {};
|
|
var services = payload.services || {};
|
|
setText("stat-wifi", system.wifi || "unbekannt");
|
|
setText("stat-ips", system.ips || "unbekannt");
|
|
updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown");
|
|
setText("stat-uptime", system.uptime || "unbekannt");
|
|
setText("stat-disk", system.disk_free || "unbekannt");
|
|
setText("val-cpu", system.cpu_usage || "--");
|
|
setText("val-temp", system.cpu_temperature || "--");
|
|
setText("val-mem", system.memory_usage || "--");
|
|
document.getElementById("bar-cpu").style.width = percentTextToWidth(system.cpu_usage);
|
|
document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage);
|
|
document.getElementById("bar-temp").style.width = temperatureTextToWidth(system.cpu_temperature);
|
|
setText("cam-state", services.camera === "active" ? "Online" : services.camera || "unbekannt");
|
|
}
|
|
|
|
function fetchSystemStatus() {
|
|
fetch(window.location.protocol + "//" + hostUrl + ":8082/api/status", {
|
|
cache: "no-store"
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
throw new Error("status");
|
|
}
|
|
return response.json();
|
|
}).then(function (payload) {
|
|
updateServiceState(payload);
|
|
}).catch(function () {
|
|
setText("stat-wifi", "nicht erreichbar");
|
|
});
|
|
}
|
|
|
|
function publishJoy() {
|
|
if (!joyListener) {
|
|
return;
|
|
}
|
|
joyListener.publish(new ROSLIB.Message({
|
|
header: {
|
|
frame_id: "webgui"
|
|
},
|
|
axes: axes,
|
|
buttons: buttons
|
|
}));
|
|
}
|
|
|
|
function startPublishing() {
|
|
if (publishTimer) {
|
|
return;
|
|
}
|
|
publishJoy();
|
|
publishTimer = window.setInterval(publishJoy, 50);
|
|
}
|
|
|
|
function stopPublishing() {
|
|
if (publishTimer) {
|
|
clearInterval(publishTimer);
|
|
publishTimer = null;
|
|
}
|
|
}
|
|
|
|
function setAxes(x, y) {
|
|
axes = [x, y, 0, 0, 0, 0];
|
|
updateAxesDisplay(x, y);
|
|
}
|
|
|
|
function resetAxes() {
|
|
setAxes(0, 0);
|
|
publishJoy();
|
|
}
|
|
|
|
function pulseButton(buttonIndex) {
|
|
axes = [0, 0, 0, 0, 0, 0];
|
|
updateAxesDisplay(0, 0);
|
|
buttons[buttonIndex] = 1;
|
|
publishJoy();
|
|
window.setTimeout(function () {
|
|
buttons[buttonIndex] = 0;
|
|
publishJoy();
|
|
}, 120);
|
|
}
|
|
|
|
function selectDriveMode(mode, buttonIndex) {
|
|
applyDriveMode(mode);
|
|
pulseButton(buttonIndex);
|
|
}
|
|
|
|
function toggleMotors() {
|
|
motorsEnabled = !motorsEnabled;
|
|
updateMotorDisplay();
|
|
pulseButton(9);
|
|
}
|
|
|
|
function updateControlLocks() {
|
|
var xOnly = driveMode === "turn-on-point";
|
|
setText("joy-x-label", driveMode === "turn-on-point" ? "Rotation" : "X-Achse");
|
|
setText("dpad-x-label", driveMode === "turn-on-point" ? "Rotation" : "X-Achse");
|
|
document.querySelectorAll("#joy-area .joy-ax")[1].style.opacity = xOnly ? "0.25" : "1";
|
|
document.querySelectorAll("#dpad-area .joy-ax")[1].style.opacity = xOnly ? "0.25" : "1";
|
|
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
|
|
var x = parseFloat(btn.dataset.x || "0");
|
|
var y = parseFloat(btn.dataset.y || "0");
|
|
var disable = xOnly && y !== 0;
|
|
btn.disabled = disable;
|
|
btn.style.opacity = disable ? "0.15" : "";
|
|
btn.style.pointerEvents = disable ? "none" : "";
|
|
});
|
|
}
|
|
|
|
function updateClocks() {
|
|
var now = new Date();
|
|
setText("utc", now.toUTCString().split(" ")[4]);
|
|
var s = Math.floor((Date.now() - metBase) / 1000);
|
|
var h = String(Math.floor(s / 3600)).padStart(2, "0");
|
|
var m = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
|
|
var sec = String(s % 60).padStart(2, "0");
|
|
setText("met", h + ":" + m + ":" + sec);
|
|
}
|
|
|
|
function joyMetrics() {
|
|
var rect = document.getElementById("jbase").getBoundingClientRect();
|
|
var baseR = Math.min(rect.width, rect.height) / 2;
|
|
var thumbR = baseR * 0.33;
|
|
return {
|
|
cx: rect.left + rect.width / 2,
|
|
cy: rect.top + rect.height / 2,
|
|
maxD: baseR - thumbR
|
|
};
|
|
}
|
|
|
|
function moveThumb(clientX, clientY) {
|
|
var data = joyMetrics();
|
|
var dx = clientX - data.cx;
|
|
var dy = clientY - data.cy;
|
|
if (driveMode === "turn-on-point" || driveMode === "crab") {
|
|
dy = 0;
|
|
}
|
|
var distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance > data.maxD && distance > 0) {
|
|
dx = dx / distance * data.maxD;
|
|
dy = dy / distance * data.maxD;
|
|
}
|
|
var thumb = document.getElementById("jthumb");
|
|
thumb.style.left = "calc(50% + " + dx + "px)";
|
|
thumb.style.top = "calc(50% + " + dy + "px)";
|
|
thumb.style.transform = "translate(-50%,-50%)";
|
|
thumb.style.transition = "none";
|
|
var nx = dx / data.maxD;
|
|
var ny = -dy / data.maxD;
|
|
setAxes(nx, ny);
|
|
publishJoy();
|
|
startPublishing();
|
|
}
|
|
|
|
function resetThumb() {
|
|
var thumb = document.getElementById("jthumb");
|
|
thumb.style.transition = "left .22s cubic-bezier(.25,.46,.45,.94), top .22s cubic-bezier(.25,.46,.45,.94)";
|
|
thumb.style.left = "50%";
|
|
thumb.style.top = "50%";
|
|
thumb.style.transform = "translate(-50%,-50%)";
|
|
stopPublishing();
|
|
resetAxes();
|
|
}
|
|
|
|
function setDpadPressed(button, x, y) {
|
|
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
|
|
btn.classList.remove("pressed");
|
|
});
|
|
button.classList.add("pressed");
|
|
setAxes(x, y);
|
|
startPublishing();
|
|
}
|
|
|
|
function clearDpad() {
|
|
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
|
|
btn.classList.remove("pressed");
|
|
});
|
|
stopPublishing();
|
|
resetAxes();
|
|
}
|
|
|
|
function updateCameraFrameLayout() {
|
|
var port = document.querySelector(".cam-port");
|
|
var frame = document.getElementById("video_frame");
|
|
var video = document.getElementById("video_output");
|
|
if (!port || !frame) return;
|
|
|
|
var portWidth = port.clientWidth;
|
|
var portHeight = port.clientHeight;
|
|
if (!portWidth || !portHeight) return;
|
|
|
|
var videoWidth = (video && video.naturalWidth) || 1280;
|
|
var videoHeight = (video && video.naturalHeight) || 720;
|
|
var aspectRatio = videoWidth / videoHeight;
|
|
|
|
var frameWidth = portWidth;
|
|
var frameHeight = frameWidth / aspectRatio;
|
|
if (frameHeight > portHeight) {
|
|
frameHeight = portHeight;
|
|
frameWidth = frameHeight * aspectRatio;
|
|
}
|
|
|
|
frame.style.width = frameWidth + "px";
|
|
frame.style.height = frameHeight + "px";
|
|
frame.style.left = ((portWidth - frameWidth) / 2) + "px";
|
|
frame.style.top = ((portHeight - frameHeight) / 2) + "px";
|
|
frame.style.setProperty("--cam-scale", Math.max(0.55, Math.min(1.1, frameWidth / 640)).toFixed(3));
|
|
}
|
|
|
|
function setStatPanelCollapsed(collapsed, immediate) {
|
|
var panel = document.getElementById("stat-panel");
|
|
var toggle = document.getElementById("stat-panel-toggle");
|
|
var body = document.getElementById("stat-panel-body");
|
|
if (!panel || !toggle || !body) return;
|
|
var isCollapsed = panel.classList.contains("collapsed");
|
|
if (isCollapsed === collapsed && !immediate) return;
|
|
|
|
body.style.overflow = "hidden";
|
|
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
var indicator = toggle.querySelector(".stat-toggle-indicator");
|
|
if (indicator) {
|
|
indicator.textContent = collapsed ? "+" : "-";
|
|
}
|
|
|
|
if (immediate) {
|
|
panel.classList.toggle("collapsed", collapsed);
|
|
body.style.height = collapsed ? "0px" : "auto";
|
|
return;
|
|
}
|
|
|
|
if (collapsed) {
|
|
body.style.height = body.scrollHeight + "px";
|
|
body.offsetHeight;
|
|
panel.classList.add("collapsed");
|
|
body.style.height = "0px";
|
|
} else {
|
|
panel.classList.remove("collapsed");
|
|
body.style.height = "0px";
|
|
body.offsetHeight;
|
|
body.style.height = body.scrollHeight + "px";
|
|
}
|
|
}
|
|
|
|
function toggleStatPanel() {
|
|
var panel = document.getElementById("stat-panel");
|
|
if (!panel) return;
|
|
setStatPanelCollapsed(!panel.classList.contains("collapsed"));
|
|
}
|
|
|
|
window.addEventListener("load", function () {
|
|
setText("cam-host", hostUrl);
|
|
updateModeDisplay();
|
|
updateMotorDisplay();
|
|
updateInputDisplay();
|
|
updateAxesDisplay(0, 0);
|
|
updateControlLocks();
|
|
updateClocks();
|
|
window.setInterval(updateClocks, 1000);
|
|
fetchSystemStatus();
|
|
statusTimer = window.setInterval(fetchSystemStatus, 5000);
|
|
|
|
var video = document.getElementById("video_output");
|
|
video.src = "//" + hostUrl + ":8083/stream.mjpg";
|
|
video.addEventListener("load", function () {
|
|
setText("cam-state", "Online");
|
|
updateCameraFrameLayout();
|
|
});
|
|
video.addEventListener("error", function () {
|
|
setText("cam-state", "Fehler");
|
|
});
|
|
updateCameraFrameLayout();
|
|
window.addEventListener("resize", updateCameraFrameLayout);
|
|
window.addEventListener("orientationchange", function () {
|
|
window.setTimeout(updateCameraFrameLayout, 80);
|
|
});
|
|
var statBody = document.getElementById("stat-panel-body");
|
|
if (statBody) {
|
|
statBody.addEventListener("transitionend", function (event) {
|
|
if (event.propertyName !== "height") return;
|
|
var panel = document.getElementById("stat-panel");
|
|
if (!panel) return;
|
|
statBody.style.height = panel.classList.contains("collapsed") ? "0px" : "auto";
|
|
});
|
|
}
|
|
setStatPanelCollapsed(true, true);
|
|
var statToggle = document.getElementById("stat-panel-toggle");
|
|
if (statToggle) {
|
|
statToggle.addEventListener("click", toggleStatPanel);
|
|
statToggle.addEventListener("keydown", function (event) {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
toggleStatPanel();
|
|
}
|
|
});
|
|
}
|
|
|
|
ros = new ROSLIB.Ros({
|
|
url: "ws://" + hostUrl + ":9090"
|
|
});
|
|
|
|
ros.on("connection", function () {
|
|
setRosStatus("Verbunden");
|
|
});
|
|
|
|
ros.on("error", function () {
|
|
setRosStatus("Fehler");
|
|
});
|
|
|
|
ros.on("close", function () {
|
|
setRosStatus("Getrennt");
|
|
});
|
|
|
|
joyListener = new ROSLIB.Topic({
|
|
ros: ros,
|
|
name: "/joy",
|
|
messageType: "sensor_msgs/Joy"
|
|
});
|
|
|
|
roverCommandListener = new ROSLIB.Topic({
|
|
ros: ros,
|
|
name: "/rover_command",
|
|
messageType: "exomy/RoverCommand"
|
|
});
|
|
|
|
roverCommandListener.subscribe(function (message) {
|
|
applyDriveMode(locomotionModeToUi(message.locomotion_mode), true);
|
|
motorsEnabled = !!message.motors_enabled;
|
|
updateMotorDisplay();
|
|
});
|
|
|
|
var joySubscriber = new ROSLIB.Topic({
|
|
ros: ros,
|
|
name: "/joy",
|
|
messageType: "sensor_msgs/Joy"
|
|
});
|
|
joySubscriber.subscribe(function (message) {
|
|
if (message.header.frame_id === "webgui") return;
|
|
var x = (message.axes[0] || 0);
|
|
var y = -(message.axes[1] || 0);
|
|
mirrorJoyThumb(x, y);
|
|
});
|
|
|
|
document.querySelectorAll(".dbtn[data-mode]").forEach(function (button) {
|
|
button.addEventListener("click", function () {
|
|
selectDriveMode(button.dataset.mode, parseInt(button.dataset.button, 10));
|
|
});
|
|
});
|
|
|
|
document.getElementById("motor-toggle").addEventListener("click", toggleMotors);
|
|
|
|
document.getElementById("mt-joy").addEventListener("click", function () {
|
|
inputMode = "joystick";
|
|
updateInputDisplay();
|
|
document.getElementById("mt-joy").classList.add("active");
|
|
document.getElementById("mt-dpad").classList.remove("active");
|
|
document.getElementById("joy-area").style.display = "";
|
|
document.getElementById("dpad-area").classList.remove("visible");
|
|
clearDpad();
|
|
resetThumb();
|
|
});
|
|
|
|
document.getElementById("mt-dpad").addEventListener("click", function () {
|
|
inputMode = "dpad";
|
|
updateInputDisplay();
|
|
document.getElementById("mt-dpad").classList.add("active");
|
|
document.getElementById("mt-joy").classList.remove("active");
|
|
document.getElementById("joy-area").style.display = "none";
|
|
document.getElementById("dpad-area").classList.add("visible");
|
|
resetThumb();
|
|
clearDpad();
|
|
});
|
|
|
|
var joyBase = document.getElementById("jbase");
|
|
joyBase.addEventListener("mousedown", function (event) {
|
|
dragging = true;
|
|
moveThumb(event.clientX, event.clientY);
|
|
event.preventDefault();
|
|
});
|
|
document.addEventListener("mousemove", function (event) {
|
|
if (dragging) {
|
|
moveThumb(event.clientX, event.clientY);
|
|
}
|
|
});
|
|
document.addEventListener("mouseup", function () {
|
|
if (dragging) {
|
|
dragging = false;
|
|
resetThumb();
|
|
}
|
|
});
|
|
joyBase.addEventListener("touchstart", function (event) {
|
|
dragging = true;
|
|
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
|
|
event.preventDefault();
|
|
}, { passive: false });
|
|
document.addEventListener("touchmove", function (event) {
|
|
if (dragging) {
|
|
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
|
|
event.preventDefault();
|
|
}
|
|
}, { passive: false });
|
|
document.addEventListener("touchend", function () {
|
|
if (dragging) {
|
|
dragging = false;
|
|
resetThumb();
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll(".dpad-btn").forEach(function (button) {
|
|
function activate(event) {
|
|
var x = parseFloat(button.dataset.x);
|
|
var y = parseFloat(button.dataset.y);
|
|
setDpadPressed(button, x, y);
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
function deactivate() {
|
|
clearDpad();
|
|
}
|
|
button.addEventListener("mousedown", activate);
|
|
button.addEventListener("mouseup", deactivate);
|
|
button.addEventListener("mouseleave", deactivate);
|
|
button.addEventListener("touchstart", activate, { passive: false });
|
|
button.addEventListener("touchend", deactivate);
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|