GPS im Admin und offline verbesserungen

This commit is contained in:
2026-05-24 10:43:57 +02:00
parent 91fb1fcf0b
commit 1899a49f4d
5 changed files with 1019 additions and 427 deletions
+168 -25
View File
@@ -9,17 +9,26 @@
</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 class="hdr-icon" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<ellipse cx="14" cy="51" rx="6" ry="7" fill="#FFB21F"/>
<ellipse cx="28" cy="50" rx="6" ry="8" fill="#FFB21F"/>
<ellipse cx="43" cy="50" rx="6" ry="8" fill="#FFB21F"/>
<ellipse cx="55" cy="49" rx="5" ry="7" fill="#FFB21F"/>
<path d="M11 36L23 30H50L58 32L47 37H14L11 36Z" fill="#1F43C9"/>
<rect x="24" y="14" width="7" height="23" rx="3.5" fill="#1B120F"/>
<path d="M18 38L25 36L41 36L51 40L51 46L42 49L38 59L31 59L29 50L19 47L18 38Z" fill="#1B120F"/>
<rect x="21" y="38" width="5" height="10" rx="2.5" fill="#1B120F"/>
<rect x="43" y="39" width="5" height="11" rx="2.5" fill="#1B120F"/>
<path d="M15 41L9 44L8 49" stroke="#1B120F" stroke-width="4" stroke-linecap="round"/>
<path d="M49 41L57 43L57 47" stroke="#1B120F" stroke-width="4" stroke-linecap="round"/>
<path d="M14 16C14 12 17 9 22 9H32C38 9 41 12 41 16C41 22 36 26 28 26H20C16 26 14 23 14 16Z" fill="#FFB21F"/>
<circle cx="21" cy="15.5" r="3.2" fill="white"/>
<circle cx="30.5" cy="14.5" r="3.6" fill="white"/>
<circle cx="21.5" cy="17" r="1.8" fill="#000"/>
<circle cx="30.8" cy="16" r="2.1" fill="#000"/>
<path d="M25 18C25.8 19.2 27.2 19.2 28 18V20.2C28 21.8 26.8 22.7 25.5 22.7C24.2 22.7 23 21.8 23 20.2V18C23.6 18.8 24.4 19.1 25 18Z" fill="#1B120F"/>
<path d="M12 12C13.5 9.2 17.8 8.2 21 10" stroke="#1B120F" stroke-width="2.8" stroke-linecap="round"/>
<path d="M29 7.5C31.8 6.2 35.6 7.1 37.6 9.7" stroke="#1B120F" stroke-width="2.8" stroke-linecap="round"/>
</svg>
<span class="hdr-title">EXO<em>MY</em>&nbsp;GROUND CTRL</span>
@@ -28,7 +37,7 @@
<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="mission-ok"><div class="pulse-dot"></div><span id="cuno-connection-status">Cuno offline</span></div>
<div class="hdr-div"></div>
<div class="hdr-clocks">
<div class="hdr-clk">
@@ -54,7 +63,7 @@
<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-rec" id="cam-live-indicator"><div class="c-rec-dot"></div><span id="cam-live-text">LIVE</span></div>
<div class="c-tl">
CAM: <span id="cam-state">Verbinde…</span><br>
MODE: <span id="overlay-mode">Ackermann</span>
@@ -74,16 +83,8 @@
STREAM: 8083
</div>
<div class="c-bm">
<div class="c-bm-box">
<span class="c-bm-lbl">V</span>
<strong id="overlay-speed">0,0</strong>
<span class="c-bm-unit">cm/s</span>
</div>
<div class="c-bm-box">
<span class="c-bm-lbl">S</span>
<strong id="overlay-distance">0,00</strong>
<span class="c-bm-unit">m</span>
</div>
V: <span id="overlay-speed">0,0</span> cm/s<br>
S: <span id="overlay-distance">0,00</span> m
</div>
<svg class="c-track" id="track-overlay" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
<path id="track-center" class="track-center" d=""></path>
@@ -310,6 +311,13 @@ var trajectoryOverlay = {
steering: 90,
velocity: 0
};
var pageConnection = {
apiOk: false,
apiLastAt: 0,
rosOk: false,
gpsLastAt: 0,
state: "offline"
};
function setText(id, text) {
var el = document.getElementById(id);
@@ -445,6 +453,11 @@ function updateDriveEstimatorModel() {
}
function renderDriveEstimatorOverlay() {
if (!pageConnection.rosOk) {
setText("overlay-speed", "--");
setText("overlay-distance", "--");
return;
}
updateDriveEstimatorModel();
setText("overlay-speed", formatSpeedCms(driveEstimator.speedMps, 1));
setText("overlay-distance", formatNumber(driveEstimator.distanceM, 2));
@@ -592,6 +605,7 @@ function updateAxesDisplay(x, y) {
function updateGpsOverlayFromFix(message) {
if (!message) return;
markGpsFresh();
var hasFix = message.status && message.status.status >= 0;
if (!hasFix) {
@@ -606,6 +620,7 @@ function updateGpsOverlayFromFix(message) {
function updateGpsOverlayFromStatus(text) {
var value = String(text || "Offline");
markGpsFresh();
setText("gps-state", value);
var satMatch = value.match(/(\d+)\s*Sat/i);
@@ -675,7 +690,96 @@ function updateInputDisplay() {
function setRosStatus(text) {
setText("status", text);
setText("ros-state", text);
setText("mission-state", text === "Verbunden" ? "Nominal" : text);
}
function controlsAreBlocked() {
return pageConnection.state === "offline";
}
function updateControlAvailability() {
var panel = document.getElementById("joy-panel");
if (panel) {
panel.classList.toggle("is-disabled", controlsAreBlocked());
}
}
function clearSystemStatusDisplay() {
setText("stat-wifi", "--");
setText("stat-ips", "--");
updateUndervoltageDisplay("--");
setText("stat-uptime", "--");
setText("stat-disk", "--");
setText("val-cpu", "--");
setText("val-temp", "--");
setText("val-mem", "--");
document.getElementById("bar-cpu").style.width = "0%";
document.getElementById("bar-mem").style.width = "0%";
document.getElementById("bar-temp").style.width = "0%";
}
function clearRosLiveDisplay() {
setRosStatus("Offline");
setText("cam-state", "Offline");
setText("cam-host", "--");
setText("cam-live-text", "OFFLINE");
var liveIndicator = document.getElementById("cam-live-indicator");
if (liveIndicator) {
liveIndicator.classList.add("is-offline");
}
setText("gps-state", "Offline");
setText("gps-sats", "--");
setText("gps-lat", "--");
setText("gps-lon", "--");
setText("overlay-speed", "--");
setText("overlay-distance", "--");
}
function setPageConnectionState() {
var now = Date.now();
var apiFresh = pageConnection.apiOk && (now - pageConnection.apiLastAt) < 15000;
var rosFresh = !!pageConnection.rosOk;
var gpsFresh = pageConnection.gpsLastAt && (now - pageConnection.gpsLastAt) < 12000;
var state = "offline";
var cunoTitle = "Cuno offline";
if (apiFresh && rosFresh) {
state = gpsFresh || !pageConnection.gpsLastAt ? "online" : "degraded";
} else if (apiFresh || rosFresh) {
state = "degraded";
}
cunoTitle = state === "online" ? "Cuno online" : "Cuno offline";
pageConnection.state = state;
document.body.classList.toggle("page-connection-online", state === "online");
document.body.classList.toggle("page-connection-degraded", state === "degraded");
document.body.classList.toggle("page-connection-offline", state === "offline");
setText("cuno-connection-status", cunoTitle);
updateControlAvailability();
if (!apiFresh) {
clearSystemStatusDisplay();
}
if (!rosFresh) {
clearRosLiveDisplay();
}
}
function markApiConnection(ok) {
pageConnection.apiOk = !!ok;
if (ok) {
pageConnection.apiLastAt = Date.now();
}
setPageConnectionState();
}
function markRosConnection(ok) {
pageConnection.rosOk = !!ok;
setPageConnectionState();
}
function markGpsFresh() {
pageConnection.gpsLastAt = Date.now();
setPageConnectionState();
}
function updateUndervoltageDisplay(text, state) {
@@ -727,14 +831,16 @@ function fetchSystemStatus() {
}
return response.json();
}).then(function (payload) {
markApiConnection(true);
updateServiceState(payload);
}).catch(function () {
markApiConnection(false);
setText("stat-wifi", "nicht erreichbar");
});
}
function publishJoy() {
if (!joyListener) {
if (!joyListener || controlsAreBlocked()) {
return;
}
joyListener.publish(new ROSLIB.Message({
@@ -772,6 +878,9 @@ function resetAxes() {
}
function pulseButton(buttonIndex) {
if (controlsAreBlocked()) {
return;
}
axes = [0, 0, 0, 0, 0, 0];
updateAxesDisplay(0, 0);
buttons[buttonIndex] = 1;
@@ -783,11 +892,17 @@ function pulseButton(buttonIndex) {
}
function selectDriveMode(mode, buttonIndex) {
if (controlsAreBlocked()) {
return;
}
applyDriveMode(mode);
pulseButton(buttonIndex);
}
function toggleMotors() {
if (controlsAreBlocked()) {
return;
}
motorsEnabled = !motorsEnabled;
updateMotorDisplay();
pulseButton(9);
@@ -831,6 +946,9 @@ function joyMetrics() {
}
function moveThumb(clientX, clientY) {
if (controlsAreBlocked()) {
return;
}
var data = joyMetrics();
var dx = clientX - data.cx;
var dy = clientY - data.cy;
@@ -865,6 +983,9 @@ function resetThumb() {
}
function setDpadPressed(button, x, y) {
if (controlsAreBlocked()) {
return;
}
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
btn.classList.remove("pressed");
});
@@ -959,10 +1080,12 @@ window.addEventListener("load", function () {
updateClocks();
window.setInterval(updateClocks, 1000);
fetchSystemStatus();
setPageConnectionState();
fetchDriveEstimatorData();
renderDriveEstimatorOverlay();
renderTrajectoryOverlay();
statusTimer = window.setInterval(fetchSystemStatus, 5000);
window.setInterval(setPageConnectionState, 1000);
window.setInterval(renderDriveEstimatorOverlay, 250);
window.setInterval(renderTrajectoryOverlay, 250);
@@ -970,10 +1093,21 @@ window.addEventListener("load", function () {
video.src = "//" + hostUrl + ":8083/stream.mjpg";
video.addEventListener("load", function () {
setText("cam-state", "Online");
setText("cam-host", hostUrl);
setText("cam-live-text", "LIVE");
var liveIndicator = document.getElementById("cam-live-indicator");
if (liveIndicator) {
liveIndicator.classList.remove("is-offline");
}
updateCameraFrameLayout();
});
video.addEventListener("error", function () {
setText("cam-state", "Fehler");
setText("cam-live-text", "OFFLINE");
var liveIndicator = document.getElementById("cam-live-indicator");
if (liveIndicator) {
liveIndicator.classList.add("is-offline");
}
});
updateCameraFrameLayout();
window.addEventListener("resize", updateCameraFrameLayout);
@@ -1006,14 +1140,17 @@ window.addEventListener("load", function () {
});
ros.on("connection", function () {
markRosConnection(true);
setRosStatus("Verbunden");
});
ros.on("error", function () {
markRosConnection(false);
setRosStatus("Fehler");
});
ros.on("close", function () {
markRosConnection(false);
setRosStatus("Getrennt");
});
@@ -1074,6 +1211,7 @@ window.addEventListener("load", function () {
messageType: "sensor_msgs/Joy"
});
joySubscriber.subscribe(function (message) {
markRosConnection(true);
if (message.header.frame_id === "webgui") return;
var x = (message.axes[0] || 0);
var y = -(message.axes[1] || 0);
@@ -1089,6 +1227,7 @@ window.addEventListener("load", function () {
document.getElementById("motor-toggle").addEventListener("click", toggleMotors);
document.getElementById("mt-joy").addEventListener("click", function () {
if (controlsAreBlocked()) return;
inputMode = "joystick";
updateInputDisplay();
document.getElementById("mt-joy").classList.add("active");
@@ -1100,6 +1239,7 @@ window.addEventListener("load", function () {
});
document.getElementById("mt-dpad").addEventListener("click", function () {
if (controlsAreBlocked()) return;
inputMode = "dpad";
updateInputDisplay();
document.getElementById("mt-dpad").classList.add("active");
@@ -1112,6 +1252,7 @@ window.addEventListener("load", function () {
var joyBase = document.getElementById("jbase");
joyBase.addEventListener("mousedown", function (event) {
if (controlsAreBlocked()) return;
dragging = true;
moveThumb(event.clientX, event.clientY);
event.preventDefault();
@@ -1128,6 +1269,7 @@ window.addEventListener("load", function () {
}
});
joyBase.addEventListener("touchstart", function (event) {
if (controlsAreBlocked()) return;
dragging = true;
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
event.preventDefault();
@@ -1147,6 +1289,7 @@ window.addEventListener("load", function () {
document.querySelectorAll(".dpad-btn").forEach(function (button) {
function activate(event) {
if (controlsAreBlocked()) return;
var x = parseFloat(button.dataset.x);
var y = parseFloat(button.dataset.y);
setDpadPressed(button, x, y);