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
+538 -15
View File
@@ -12,17 +12,26 @@
<body class="page-admin">
<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">CUNO&nbsp;<em>ADMIN</em></span>
<div class="hdr-div"></div>
@@ -30,6 +39,8 @@
<div class="hdr-gap"></div>
<span id="admin_status" class="hdr-status">Lade...</span>
<div class="hdr-div"></div>
<div class="mission-ok"><div class="pulse-dot"></div><span id="page-connection-label">Prüfe…</span></div>
<div class="hdr-div"></div>
<a class="hdr-back" href="./index.html">← Ground Control</a>
<div class="hdr-div"></div>
<div class="hdr-clk">
@@ -37,12 +48,17 @@
<div class="hdr-clk-val" id="utc-clock">--:--:--</div>
</div>
</header>
<div id="connection-banner" class="connection-banner" hidden>
<strong id="connection-banner-title">Verbindung wird geprüft</strong>
<span id="connection-banner-detail">Warte auf ROS und Admin-API.</span>
</div>
<div class="admin-shell">
<div class="admin-section-tabs" role="tablist" aria-label="Admin-Bereiche">
<button id="admin_tab_system" class="admin-section-tab is_active" role="tab" aria-selected="true" onclick="switchAdminSection('system')">Admin</button>
<button id="admin_tab_drive" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('drive')">Geschwindigkeit / Weg</button>
<button id="admin_tab_map" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('map')">Karte</button>
<button id="admin_tab_gps" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('gps')">GPS</button>
<button id="admin_tab_motor" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('motor')">Motorentest</button>
</div>
@@ -357,6 +373,64 @@
</div>
</section>
<section id="admin_panel_gps" class="admin-section-panel" role="tabpanel">
<div class="admin-section-grid admin-section-grid_gps">
<div class="admin-col admin-col_gps_main">
<div class="panel gps-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">GNSS-Satelliten</span>
<span class="p-tag">Himmelskarte</span>
</div>
<div class="copy-text gps-copy">
Diese Ansicht zeigt die vom Empfänger gemeldeten Satelliten im Himmelsplot. Außen ist der Horizont, die Mitte ist der Zenit.
</div>
<div class="gps-sky-stage">
<canvas id="gps_sky_canvas" class="gps-sky-canvas" width="560" height="560"></canvas>
<div id="gps_satellite_tooltip" class="gps-satellite-tooltip" hidden></div>
</div>
</div>
</div>
<div class="admin-col admin-col_gps_side">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Fix-Diagnose</span>
<span class="p-tag">GNSS-Diagnose</span>
</div>
<div class="sr"><span class="sr-led led-ok"></span><span class="sr-key">Status</span><span id="gps_diag_status" class="sr-val">Offline</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Fix-Qualität</span><span id="gps_diag_fix_quality" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Frame</span><span id="gps_diag_frame" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Breite</span><span id="gps_diag_lat" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Länge</span><span id="gps_diag_lon" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Höhe</span><span id="gps_diag_alt" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Geschwindigkeit</span><span id="gps_diag_speed" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Kurs</span><span id="gps_diag_track" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">UTC-Datum</span><span id="gps_diag_utc_date" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">UTC-Zeit</span><span id="gps_diag_utc_time" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Fix-Alter</span><span id="gps_diag_fix_age" class="sr-val">--</span></div>
</div>
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Genauigkeit</span>
<span class="p-tag">Qualität</span>
</div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Positionsgenauigkeit</span><span id="gps_diag_pdop" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Horizontale Genauigkeit</span><span id="gps_diag_hdop" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Vertikale Genauigkeit</span><span id="gps_diag_vdop" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Für Fix genutzt</span><span id="gps_diag_sat_used" class="sr-val">--</span></div>
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Im Himmelsplot</span><span id="gps_diag_sat_visible" class="sr-val">--</span></div>
<div class="copy-text gps-copy gps-copy_compact">
Für eine gute Fix-Qualität sind niedrige DOP-Werte und hohe, gleichmäßig verteilte Satelliten sinnvoll. Der Himmelsplot zeigt nur Satelliten, die der Empfänger per GSV mit Richtung und Höhe meldet.
</div>
</div>
</div>
</div>
</section>
<section id="admin_panel_motor" class="admin-section-panel motor-admin-tab" role="tabpanel">
<div class="admin-section-grid">
<div class="admin-col">
@@ -479,6 +553,15 @@
var motorCommandsTopic = null;
var gpsFixTopic = null;
var gpsStatusTopic = null;
var gpsDiagnosticsTopic = null;
var pageConnection = {
apiOk: false,
apiLastAt: 0,
rosOk: false,
gpsLastAt: 0,
lastHealthyAt: 0,
state: 'offline'
};
var isMotorTestRunning = false;
var isContainerActionRunning = false;
@@ -560,6 +643,29 @@
hasCenteredOnFix: false,
suppressMapClickUntil: 0
};
var gpsDiagnosticsState = {
connected: false,
frameId: 'gps',
valid: false,
fixQuality: 0,
satellitesUsed: 0,
satellitesVisible: 0,
latitude: null,
longitude: null,
altitudeM: null,
hdop: null,
pdop: null,
vdop: null,
speedMps: null,
speedKmh: null,
trackDeg: null,
utcDate: null,
utcTime: null,
lastFixAgeS: null,
satellites: [],
selectedSatellitePrn: null,
plottedSatellites: []
};
function submitPassword() {
var input = document.getElementById('auth_input');
@@ -578,6 +684,129 @@
if (el) el.textContent = text;
}
function clearAdminApiDisplay() {
setText('info_ips', '--');
setText('info_wifi', '--');
setText('info_temp', '--');
setText('info_uptime', '--');
setText('info_disk', '--');
setUndervoltageState('info_undervoltage', '--', 'unknown');
setServiceState('service_camera', '-');
setServiceState('service_admin_api', '-');
setServiceState('service_exomy', '-');
var bar = document.getElementById('bar-temp-adm');
if (bar) bar.style.width = '0%';
}
function clearAdminRosDisplay() {
roverMapState.hasFix = false;
roverMapState.roverLat = null;
roverMapState.roverLon = null;
roverMapState.roverAlt = null;
roverMapState.accuracyMeters = null;
roverMapState.lastFixAt = null;
roverMapState.gpsStatusText = 'Offline';
roverMapState.satellites = '--';
updateMapTelemetryPanel();
setDriveEstimatorState('offline', 'state_inactive');
setText('drive_estimator_speed', '--');
setText('drive_estimator_distance', '--');
setText('drive_estimator_pulse', '--');
setText('drive_estimator_rpm', '--');
setText('drive_estimator_mode', '--');
setText('drive_estimator_direction', '--');
gpsDiagnosticsState.connected = false;
gpsDiagnosticsState.valid = false;
gpsDiagnosticsState.fixQuality = 0;
gpsDiagnosticsState.satellitesUsed = 0;
gpsDiagnosticsState.satellitesVisible = 0;
gpsDiagnosticsState.latitude = null;
gpsDiagnosticsState.longitude = null;
gpsDiagnosticsState.altitudeM = null;
gpsDiagnosticsState.hdop = null;
gpsDiagnosticsState.pdop = null;
gpsDiagnosticsState.vdop = null;
gpsDiagnosticsState.speedMps = null;
gpsDiagnosticsState.speedKmh = null;
gpsDiagnosticsState.trackDeg = null;
gpsDiagnosticsState.utcDate = null;
gpsDiagnosticsState.utcTime = null;
gpsDiagnosticsState.lastFixAgeS = null;
gpsDiagnosticsState.satellites = [];
updateGpsDiagnosticsPanel();
}
function formatConnectionTime(timestamp) {
if (!timestamp) return '--:--:--';
return new Date(timestamp).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function updatePageConnectionState() {
var now = Date.now();
var apiFresh = pageConnection.apiOk && (now - pageConnection.apiLastAt) < 20000;
var rosFresh = !!pageConnection.rosOk;
var gpsFresh = pageConnection.gpsLastAt && (now - pageConnection.gpsLastAt) < 15000;
var state = 'offline';
var title = 'Offline';
var details = [];
if (apiFresh && rosFresh) {
state = gpsFresh || !pageConnection.gpsLastAt ? 'online' : 'degraded';
title = state === 'online' ? 'Online' : 'Eingeschränkt';
} else if (apiFresh || rosFresh) {
state = 'degraded';
title = 'Eingeschränkt';
}
if (!apiFresh) details.push('Admin-API nicht erreichbar');
if (!rosFresh) details.push('ROSBridge getrennt');
if (rosFresh && pageConnection.gpsLastAt && !gpsFresh) details.push('GPS-Daten veraltet');
if (!details.length) details.push('Alle Live-Dienste antworten');
if (state !== 'online' && pageConnection.lastHealthyAt) {
details.push('letzter Kontakt ' + formatConnectionTime(pageConnection.lastHealthyAt));
}
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('page-connection-label', title);
setText('connection-banner-title', title);
setText('connection-banner-detail', details.join(' | '));
if (!apiFresh) clearAdminApiDisplay();
if (!rosFresh) clearAdminRosDisplay();
var banner = document.getElementById('connection-banner');
if (banner) banner.hidden = state === 'online';
}
function markAdminApiConnection(ok) {
pageConnection.apiOk = !!ok;
if (ok) {
pageConnection.apiLastAt = Date.now();
pageConnection.lastHealthyAt = pageConnection.apiLastAt;
}
updatePageConnectionState();
}
function markAdminRosConnection(ok) {
pageConnection.rosOk = !!ok;
if (ok) pageConnection.lastHealthyAt = Date.now();
updatePageConnectionState();
}
function markAdminGpsFresh() {
pageConnection.gpsLastAt = Date.now();
if (pageConnection.rosOk) pageConnection.lastHealthyAt = pageConnection.gpsLastAt;
updatePageConnectionState();
}
function setMotorTestStatus(text) {
setAdminStatus(text);
}
@@ -587,26 +816,32 @@
var systemTab = document.getElementById('admin_tab_system');
var driveTab = document.getElementById('admin_tab_drive');
var mapTab = document.getElementById('admin_tab_map');
var gpsTab = document.getElementById('admin_tab_gps');
var motorTab = document.getElementById('admin_tab_motor');
var systemPanel = document.getElementById('admin_panel_system');
var drivePanel = document.getElementById('admin_panel_drive');
var mapPanel = document.getElementById('admin_panel_map');
var gpsPanel = document.getElementById('admin_panel_gps');
var motorPanel = document.getElementById('admin_panel_motor');
var isSystem = sectionName === 'system';
var isDrive = sectionName === 'drive';
var isMap = sectionName === 'map';
var isGps = sectionName === 'gps';
var isMotor = sectionName === 'motor';
systemTab.classList.toggle('is_active', isSystem);
driveTab.classList.toggle('is_active', isDrive);
mapTab.classList.toggle('is_active', isMap);
gpsTab.classList.toggle('is_active', isGps);
motorTab.classList.toggle('is_active', isMotor);
systemTab.setAttribute('aria-selected', isSystem ? 'true' : 'false');
driveTab.setAttribute('aria-selected', isDrive ? 'true' : 'false');
mapTab.setAttribute('aria-selected', isMap ? 'true' : 'false');
gpsTab.setAttribute('aria-selected', isGps ? 'true' : 'false');
motorTab.setAttribute('aria-selected', isMotor ? 'true' : 'false');
systemPanel.classList.toggle('is_active', isSystem);
drivePanel.classList.toggle('is_active', isDrive);
mapPanel.classList.toggle('is_active', isMap);
gpsPanel.classList.toggle('is_active', isGps);
motorPanel.classList.toggle('is_active', isMotor);
window.location.hash = isSystem ? '' : ('#' + sectionName);
if (isMap) {
@@ -615,6 +850,9 @@
if (roverMapState.map) roverMapState.map.invalidateSize();
}, 80);
}
if (isGps) {
window.setTimeout(drawGpsSkyPlot, 80);
}
if (isMotor) fetchMotorTestStatus();
}
@@ -665,14 +903,14 @@
});
}
function formatCoordinate(value) {
function formatCoordinate(value, digits) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return formatNumber(value, 5);
return formatNumber(value, typeof digits === 'number' ? digits : 5);
}
function formatAltitude(value) {
function formatAltitude(value, digits) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return formatNumber(value, 1) + ' m';
return formatNumber(value, typeof digits === 'number' ? digits : 1) + ' m';
}
function formatAccuracy(value) {
@@ -680,6 +918,26 @@
return formatNumber(value, 1) + ' m';
}
function formatDop(value) {
if (typeof value !== 'number' || isNaN(value) || value <= 0) return '--';
return formatNumber(value, 2);
}
function formatDegrees(value) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return formatNumber(value, 1) + '°';
}
function formatSpeedKmh(value) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return formatNumber(value, 1) + ' km/h';
}
function formatFixAge(value) {
if (typeof value !== 'number' || isNaN(value)) return '--';
return formatNumber(value, 1) + ' s';
}
function formatFixTime(date) {
if (!date) return '--';
return date.toLocaleTimeString('de-DE', {
@@ -689,6 +947,241 @@
});
}
function fixQualityLabel(value) {
var labels = {
0: 'Kein Fix',
1: 'GPS Fix',
2: 'DGPS Fix',
4: 'RTK Fixed',
5: 'RTK Float'
};
return labels[value] || String(value || 0);
}
function describeDopQuality(value) {
if (typeof value !== 'number' || isNaN(value) || value <= 0) {
return { text: '--', stateClass: 'state_unknown' };
}
if (value < 1) {
return { text: formatNumber(value, 2) + ' sehr gut', stateClass: 'state_active' };
}
if (value < 2) {
return { text: formatNumber(value, 2) + ' gut', stateClass: 'state_active' };
}
if (value < 5) {
return { text: formatNumber(value, 2) + ' brauchbar', stateClass: 'state_unknown' };
}
if (value < 10) {
return { text: formatNumber(value, 2) + ' kritisch', stateClass: 'state_inactive' };
}
return { text: formatNumber(value, 2) + ' schlecht', stateClass: 'state_inactive' };
}
function setGpsQualityState(id, value) {
var node = document.getElementById(id);
if (!node) return;
var quality = describeDopQuality(value);
node.textContent = quality.text;
node.classList.remove('state_active', 'state_inactive', 'state_unknown');
node.classList.add(quality.stateClass);
}
function updateGpsDiagnosticsPanel() {
setText('gps_diag_status', gpsDiagnosticsState.connected ? (gpsDiagnosticsState.valid ? 'Fix aktiv' : 'Empfänger online') : 'Offline');
setText('gps_diag_fix_quality', fixQualityLabel(gpsDiagnosticsState.fixQuality));
setText('gps_diag_frame', gpsDiagnosticsState.frameId || '--');
setText('gps_diag_lat', formatCoordinate(gpsDiagnosticsState.latitude, 8));
setText('gps_diag_lon', formatCoordinate(gpsDiagnosticsState.longitude, 8));
setText('gps_diag_alt', formatAltitude(gpsDiagnosticsState.altitudeM, 4));
setText('gps_diag_speed', formatSpeedKmh(gpsDiagnosticsState.speedKmh));
setText('gps_diag_track', formatDegrees(gpsDiagnosticsState.trackDeg));
setText('gps_diag_utc_date', gpsDiagnosticsState.utcDate || '--');
setText('gps_diag_utc_time', gpsDiagnosticsState.utcTime || '--');
setText('gps_diag_fix_age', formatFixAge(gpsDiagnosticsState.lastFixAgeS));
setGpsQualityState('gps_diag_pdop', gpsDiagnosticsState.pdop);
setGpsQualityState('gps_diag_hdop', gpsDiagnosticsState.hdop);
setGpsQualityState('gps_diag_vdop', gpsDiagnosticsState.vdop);
setText('gps_diag_sat_used', gpsDiagnosticsState.satellitesUsed);
setText('gps_diag_sat_visible', gpsDiagnosticsState.satellitesVisible);
drawGpsSkyPlot();
}
function getSelectedSatellite() {
if (!gpsDiagnosticsState.satellites.length) return null;
var selected = gpsDiagnosticsState.satellites.find(function(item) {
return item.prn === gpsDiagnosticsState.selectedSatellitePrn;
});
if (selected) return selected;
gpsDiagnosticsState.selectedSatellitePrn = gpsDiagnosticsState.satellites[0].prn || null;
return gpsDiagnosticsState.satellites[0];
}
function hideGpsSatelliteTooltip() {
var node = document.getElementById('gps_satellite_tooltip');
if (!node) return;
node.hidden = true;
}
function renderGpsSatelliteTooltip(leftPx, topPx) {
var node = document.getElementById('gps_satellite_tooltip');
if (!node) return;
var selected = getSelectedSatellite();
if (!selected) {
node.hidden = true;
return;
}
node.innerHTML =
'<div class="gps-satellite-tooltip-title">' + (selected.prn || '--') + '</div>' +
'<div class="gps-satellite-tooltip-row"><span>System</span><strong>' + (selected.constellation || '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Richtung</span><strong>' + (typeof selected.azimuth === 'number' ? selected.azimuth + '°' : '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Höhe</span><strong>' + (typeof selected.elevation === 'number' ? selected.elevation + '°' : '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Signal</span><strong>' + (typeof selected.snr === 'number' ? selected.snr + ' dB' : '--') + '</strong></div>' +
'<div class="gps-satellite-tooltip-row"><span>Qualität</span><strong>' + (typeof selected.snr === 'number' ? (selected.snr >= 35 ? 'sehr gut' : (selected.snr >= 20 ? 'gut' : 'schwach')) : 'unbekannt') + '</strong></div>';
node.style.left = leftPx + 'px';
node.style.top = topPx + 'px';
node.hidden = false;
}
function drawGpsSkyPlot() {
var canvas = document.getElementById('gps_sky_canvas');
if (!canvas || !canvas.getContext) return;
var ctx = canvas.getContext('2d');
var width = canvas.width;
var height = canvas.height;
var cx = width / 2;
var cy = height / 2;
var radius = Math.min(width, height) * 0.42;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#07111c';
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = 'rgba(0,255,145,.18)';
ctx.lineWidth = 1;
[radius, radius * 0.66, radius * 0.33].forEach(function(ring) {
ctx.beginPath();
ctx.arc(cx, cy, ring, 0, Math.PI * 2);
ctx.stroke();
});
ctx.beginPath();
ctx.moveTo(cx, cy - radius);
ctx.lineTo(cx, cy + radius);
ctx.moveTo(cx - radius, cy);
ctx.lineTo(cx + radius, cy);
ctx.stroke();
ctx.fillStyle = 'rgba(190,255,225,.84)';
ctx.font = '16px Consolas';
ctx.textAlign = 'center';
ctx.fillText('N', cx, cy - radius - 12);
ctx.fillText('S', cx, cy + radius + 24);
ctx.fillText('W', cx - radius - 18, cy + 6);
ctx.fillText('E', cx + radius + 18, cy + 6);
ctx.fillStyle = 'rgba(150,255,210,.5)';
ctx.font = '12px Consolas';
ctx.fillText('30°', cx, cy - radius * 0.33 + 4);
ctx.fillText('60°', cx, cy - radius * 0.66 + 4);
ctx.fillText('90°', cx, cy + 4);
gpsDiagnosticsState.plottedSatellites = [];
gpsDiagnosticsState.satellites.forEach(function(item) {
if (typeof item.azimuth !== 'number' || typeof item.elevation !== 'number') return;
var satRadius = radius * (1 - Math.max(0, Math.min(90, item.elevation)) / 90);
var angle = (item.azimuth - 90) * Math.PI / 180.0;
var x = cx + Math.cos(angle) * satRadius;
var y = cy + Math.sin(angle) * satRadius;
var snr = typeof item.snr === 'number' ? item.snr : 0;
var isSelected = item.prn === gpsDiagnosticsState.selectedSatellitePrn;
ctx.beginPath();
ctx.fillStyle = snr >= 35 ? '#00ff91' : (snr >= 20 ? '#ffb300' : '#ff6b6b');
ctx.arc(x, y, isSelected ? 13 : 10, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = isSelected ? '#ffffff' : 'rgba(5,17,29,.9)';
ctx.lineWidth = isSelected ? 3 : 2;
ctx.stroke();
ctx.fillStyle = '#041017';
ctx.font = 'bold 10px Consolas';
ctx.textAlign = 'center';
ctx.fillText(String(item.prn || '?'), x, y + 3);
gpsDiagnosticsState.plottedSatellites.push({
prn: item.prn,
x: x,
y: y,
radius: isSelected ? 13 : 10
});
});
}
function onGpsSkyCanvasClick(event) {
var canvas = document.getElementById('gps_sky_canvas');
if (!canvas) return;
var rect = canvas.getBoundingClientRect();
var scaleX = canvas.width / rect.width;
var scaleY = canvas.height / rect.height;
var x = (event.clientX - rect.left) * scaleX;
var y = (event.clientY - rect.top) * scaleY;
var selectedPrn = null;
gpsDiagnosticsState.plottedSatellites.forEach(function(item) {
var dx = x - item.x;
var dy = y - item.y;
if ((dx * dx + dy * dy) <= (item.radius + 6) * (item.radius + 6)) {
selectedPrn = item.prn;
}
});
if (!selectedPrn) {
hideGpsSatelliteTooltip();
return;
}
gpsDiagnosticsState.selectedSatellitePrn = selectedPrn;
drawGpsSkyPlot();
var tooltipLeft = Math.min(rect.width - 18, (event.clientX - rect.left) + 18);
var tooltipTop = Math.max(18, (event.clientY - rect.top) - 18);
renderGpsSatelliteTooltip(tooltipLeft, tooltipTop);
}
function handleGpsDiagnostics(message) {
if (!message || !message.data) return;
try {
var data = JSON.parse(message.data);
gpsDiagnosticsState.connected = !!data.connected;
gpsDiagnosticsState.frameId = data.frame_id || 'gps';
gpsDiagnosticsState.valid = !!data.valid;
gpsDiagnosticsState.fixQuality = Number(data.fix_quality || 0);
gpsDiagnosticsState.satellitesUsed = Number(data.satellites_used || 0);
gpsDiagnosticsState.satellitesVisible = Number(data.satellites_visible || 0);
gpsDiagnosticsState.latitude = typeof data.latitude === 'number' ? data.latitude : null;
gpsDiagnosticsState.longitude = typeof data.longitude === 'number' ? data.longitude : null;
gpsDiagnosticsState.altitudeM = typeof data.altitude_m === 'number' ? data.altitude_m : null;
gpsDiagnosticsState.hdop = typeof data.hdop === 'number' ? data.hdop : null;
gpsDiagnosticsState.pdop = typeof data.pdop === 'number' ? data.pdop : null;
gpsDiagnosticsState.vdop = typeof data.vdop === 'number' ? data.vdop : null;
gpsDiagnosticsState.speedMps = typeof data.speed_mps === 'number' ? data.speed_mps : null;
gpsDiagnosticsState.speedKmh = typeof data.speed_kmh === 'number' ? data.speed_kmh : null;
gpsDiagnosticsState.trackDeg = typeof data.track_deg === 'number' ? data.track_deg : null;
gpsDiagnosticsState.utcDate = data.utc_date || null;
gpsDiagnosticsState.utcTime = data.utc_time || null;
gpsDiagnosticsState.lastFixAgeS = typeof data.last_fix_age_s === 'number' ? data.last_fix_age_s : null;
gpsDiagnosticsState.satellites = Array.isArray(data.satellites) ? data.satellites : [];
if (gpsDiagnosticsState.selectedSatellitePrn) {
var stillExists = gpsDiagnosticsState.satellites.some(function(item) {
return item.prn === gpsDiagnosticsState.selectedSatellitePrn;
});
if (!stillExists) gpsDiagnosticsState.selectedSatellitePrn = null;
}
updateGpsDiagnosticsPanel();
} catch (error) {}
}
function updateMapTelemetryPanel() {
setText('map_gps_status', roverMapState.gpsStatusText || 'Offline');
setText('map_gps_satellites', roverMapState.satellites);
@@ -1113,6 +1606,7 @@
}
function updateGpsStatusSummary(text) {
markAdminGpsFresh();
roverMapState.gpsStatusText = text || 'Offline';
var satMatch = String(roverMapState.gpsStatusText).match(/(\d+)\s*Sat/i);
roverMapState.satellites = satMatch ? satMatch[1] : '--';
@@ -1121,6 +1615,7 @@
function handleGpsFix(message) {
if (!message || !message.status) return;
markAdminGpsFresh();
roverMapState.hasFix = Number(message.status.status) >= 0;
if (!roverMapState.hasFix) {
@@ -1450,6 +1945,15 @@
}
function renderDriveEstimator() {
if (!pageConnection.rosOk) {
setText('drive_estimator_speed', '--');
setText('drive_estimator_distance', '--');
setText('drive_estimator_pulse', '--');
setText('drive_estimator_rpm', '--');
setText('drive_estimator_mode', '--');
setText('drive_estimator_direction', '--');
return;
}
updateDriveEstimatorModel();
setText('drive_estimator_speed', formatSpeedCms(driveEstimator.speedMps, 1));
setText('drive_estimator_distance', formatNumber(driveEstimator.distanceM, 2));
@@ -1572,9 +2076,11 @@
});
ros.on('connection', function() {
markAdminRosConnection(true);
setDriveEstimatorState('verbunden', 'state_active');
});
ros.on('close', function() {
markAdminRosConnection(false);
setDriveEstimatorState('getrennt', 'state_inactive');
ros = null;
roverCommandTopic = null;
@@ -1582,6 +2088,7 @@
window.setTimeout(connectDriveEstimatorRos, 3000);
});
ros.on('error', function() {
markAdminRosConnection(false);
setDriveEstimatorState('Fehler', 'state_unknown');
});
@@ -1621,6 +2128,13 @@
gpsStatusTopic.subscribe(function(message) {
updateGpsStatusSummary(message.data);
});
gpsDiagnosticsTopic = new ROSLIB.Topic({
ros: ros,
name: '/gps/diagnostics_json',
messageType: 'std_msgs/String'
});
gpsDiagnosticsTopic.subscribe(handleGpsDiagnostics);
}
function temperatureToWidth(value) {
@@ -1649,8 +2163,10 @@
try {
var response = await fetch(adminApiBase + '/api/status');
if (!response.ok) throw new Error('status ' + response.status);
markAdminApiConnection(true);
renderStatus(await response.json());
} catch (e) {
markAdminApiConnection(false);
setAdminStatus('API nicht erreichbar');
}
}
@@ -2116,18 +2632,23 @@
window.addEventListener('load', function() {
var input = document.getElementById('auth_input');
var gpsCanvas = document.getElementById('gps_sky_canvas');
input.focus();
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitPassword();
});
if (gpsCanvas) gpsCanvas.addEventListener('click', onGpsSkyCanvasClick);
if (window.location.hash === '#motor') {
switchAdminSection('motor');
} else if (window.location.hash === '#drive') {
switchAdminSection('drive');
} else if (window.location.hash === '#gps') {
switchAdminSection('gps');
} else if (window.location.hash === '#map') {
switchAdminSection('map');
}
updateClock();
updatePageConnectionState();
window.setInterval(updateClock, 1000);
fetchStatus();
fetchDelay();
@@ -2140,11 +2661,13 @@
renderWaypointList();
applyWaypointPlanningState();
renderDriveEstimator();
updateGpsDiagnosticsPanel();
window.setInterval(fetchStatus, 10000);
window.setInterval(fetchDelay, 5000);
window.setInterval(fetchSpeedLimit, 5000);
window.setInterval(fetchMotorTestStatus, 4000);
window.setInterval(renderDriveEstimator, 250);
window.setInterval(updatePageConnectionState, 1000);
});
window.addEventListener('beforeunload', function(event) {