Kaera umschaltbar

This commit is contained in:
2026-05-25 15:30:56 +02:00
parent 1855a2446b
commit 8c8bfd9e5b
7 changed files with 452 additions and 111 deletions
+134 -57
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CUNO // Admin</title>
<link rel="stylesheet" href="./app.css?v=20260522104500">
<link rel="stylesheet" href="./app.css?v=20260525132000">
<link rel="stylesheet" href="./vendor/leaflet/leaflet.css">
<script src="./vendor/roslib.min.js"></script>
<script src="./vendor/leaflet/leaflet.js"></script>
@@ -56,6 +56,7 @@
<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_camera" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('camera')">Kamera</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>
@@ -190,6 +191,74 @@
</div>
</section>
<section id="admin_panel_camera" class="admin-section-panel" role="tabpanel">
<div class="admin-section-grid">
<div class="admin-col">
<div class="panel camera-panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Kamera-Vorschau</span>
<span class="p-tag">Live Stream</span>
</div>
<div class="copy-text camera-copy">
Die Vorschau kommt direkt vom Kameradienst auf Port <code>8081</code>. Nach einem Profilwechsel startet der Dienst kurz neu, das Bild setzt dann automatisch wieder ein.
</div>
<div class="camera-preview-stage">
<img id="camera_preview_image" class="camera-preview-image" alt="Kamera-Vorschau" src="" loading="eager">
</div>
<div class="camera-preview-meta">
<div class="sr">
<span class="sr-led led-ok"></span>
<span class="sr-key">Profil</span>
<span id="camera_profile_label" class="sr-val">--</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Auflösung</span>
<span id="camera_resolution_label" class="sr-val">--</span>
</div>
<div class="sr">
<span class="sr-led led-blue"></span>
<span class="sr-key">Bildrate</span>
<span id="camera_fps_label" class="sr-val">--</span>
</div>
</div>
</div>
</div>
<div class="admin-col">
<div class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">Stream-Aufl&ouml;sung</span>
<span class="p-tag">Konfiguration</span>
</div>
<div class="dropdown-row">
<label>Profil</label>
<select id="camera_profile_select" class="ctrl-select" onchange="setCameraProfile(this.value)">
<option value="640x480">640 x 480 &middot; 4:3 &middot; gr&ouml;&szlig;tes Sichtfeld</option>
<option value="1024x768">1024 x 768 &middot; 4:3 &middot; mehr Details</option>
<option value="1296x972">1296 x 972 &middot; 4:3 &middot; noch mehr Details</option>
</select>
<span id="camera_profile_status" class="ctrl-status"></span>
</div>
<div class="copy-text camera-copy">
Die neue Aufl&ouml;sung wird gespeichert und der Kameradienst automatisch neu gestartet. Der Videostream ist dabei f&uuml;r einen kurzen Moment unterbrochen.
</div>
<div class="act-grid act-grid_single">
<button class="act-btn subtle full" onclick="refreshCameraPreview()">Vorschau neu laden</button>
</div>
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Dienst</span><div class="ss-hdr-line"></div></div>
<div class="sr">
<span class="sr-led led-ok"></span>
<span class="sr-key">Kamera</span>
<strong id="camera_service_state" class="sr-val service_state state_unknown">-</strong>
</div>
</div>
</div>
</div>
</section>
<section id="admin_panel_drive" class="admin-section-panel" role="tabpanel">
<div class="admin-section-grid">
<div class="admin-col">
@@ -423,25 +492,11 @@
<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">
<div class="copy-text gps-copy">
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 class="panel">
<div class="p-hdr">
<div class="p-dot"></div>
<span class="p-lbl">GPS-Rohdaten</span>
<span class="p-tag">NMEA</span>
</div>
<div class="copy-text gps-copy gps-copy_compact">
Letzte direkt empfangene NMEA-Sätze des Empfängers. Die Ansicht scrollt intern und ist für Diagnose bei Abschattung, Drift und fehlerhaften Fixen gedacht.
</div>
<div class="neutral-toolbar">
<button class="button buttonA" onclick="copyGpsRawData()">Rohdaten kopieren</button>
</div>
<textarea id="gps_raw_output" class="gps-raw-output" readonly spellcheck="false">--</textarea>
</div>
</div>
</div>
</section>
@@ -561,6 +616,7 @@
<script type="text/javascript">
var adminApiBase = window.location.protocol + '//' + window.location.hostname + ':8082';
var cameraPreviewBase = window.location.protocol + '//' + window.location.hostname + ':8081/stream.mjpg';
var AUTH_TOKEN = 'Y3Vubw==';
var activeAdminSection = 'system';
var ros = null;
@@ -678,7 +734,6 @@
utcTime: null,
lastFixAgeS: null,
satellites: [],
rawSentences: [],
selectedSatellitePrn: null,
plottedSatellites: []
};
@@ -708,6 +763,7 @@
setText('info_disk', '--');
setUndervoltageState('info_undervoltage', '--', 'unknown');
setServiceState('service_camera', '-');
setServiceState('camera_service_state', '-');
setServiceState('service_admin_api', '-');
setServiceState('service_exomy', '-');
var bar = document.getElementById('bar-temp-adm');
@@ -750,7 +806,6 @@
gpsDiagnosticsState.utcTime = null;
gpsDiagnosticsState.lastFixAgeS = null;
gpsDiagnosticsState.satellites = [];
gpsDiagnosticsState.rawSentences = [];
updateGpsDiagnosticsPanel();
}
@@ -831,31 +886,37 @@
function switchAdminSection(sectionName) {
activeAdminSection = sectionName;
var systemTab = document.getElementById('admin_tab_system');
var cameraTab = document.getElementById('admin_tab_camera');
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 cameraPanel = document.getElementById('admin_panel_camera');
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 isCamera = sectionName === 'camera';
var isDrive = sectionName === 'drive';
var isMap = sectionName === 'map';
var isGps = sectionName === 'gps';
var isMotor = sectionName === 'motor';
systemTab.classList.toggle('is_active', isSystem);
cameraTab.classList.toggle('is_active', isCamera);
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');
cameraTab.setAttribute('aria-selected', isCamera ? '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);
cameraPanel.classList.toggle('is_active', isCamera);
drivePanel.classList.toggle('is_active', isDrive);
mapPanel.classList.toggle('is_active', isMap);
gpsPanel.classList.toggle('is_active', isGps);
@@ -867,6 +928,9 @@
if (roverMapState.map) roverMapState.map.invalidateSize();
}, 80);
}
if (isCamera) {
refreshCameraPreview();
}
if (isGps) {
window.setTimeout(drawGpsSkyPlot, 80);
}
@@ -1020,47 +1084,9 @@
setGpsQualityState('gps_diag_vdop', gpsDiagnosticsState.vdop);
setText('gps_diag_sat_used', gpsDiagnosticsState.satellitesUsed);
setText('gps_diag_sat_visible', gpsDiagnosticsState.satellitesVisible);
setGpsRawOutput(gpsDiagnosticsState.rawSentences);
drawGpsSkyPlot();
}
function setGpsRawOutput(sentences) {
var node = document.getElementById('gps_raw_output');
if (!node) return;
if (!Array.isArray(sentences) || !sentences.length) {
node.value = '--';
return;
}
var wasNearBottom = (node.scrollTop + node.clientHeight + 24) >= node.scrollHeight;
node.value = sentences.join('\n');
if (wasNearBottom) node.scrollTop = node.scrollHeight;
}
async function copyGpsRawData() {
var node = document.getElementById('gps_raw_output');
if (!node) return;
var text = String(node.value || '').trim();
if (!text || text === '--') {
setAdminStatus('Keine GPS-Rohdaten zum Kopieren');
return;
}
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
node.focus();
node.select();
document.execCommand('copy');
node.setSelectionRange(node.value.length, node.value.length);
}
setAdminStatus('GPS-Rohdaten kopiert');
window.setTimeout(function() { setAdminStatus('Bereit'); }, 1800);
} catch (error) {
setAdminStatus('Kopieren fehlgeschlagen');
}
}
function getSelectedSatellite() {
if (!gpsDiagnosticsState.satellites.length) return null;
var selected = gpsDiagnosticsState.satellites.find(function(item) {
@@ -1227,7 +1253,6 @@
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 : [];
gpsDiagnosticsState.rawSentences = Array.isArray(data.raw_sentences) ? data.raw_sentences : [];
if (gpsDiagnosticsState.selectedSatellitePrn) {
var stillExists = gpsDiagnosticsState.satellites.some(function(item) {
return item.prn === gpsDiagnosticsState.selectedSatellitePrn;
@@ -2211,6 +2236,7 @@
setText('info_uptime', data.system && data.system.uptime);
setText('info_disk', data.system && data.system.disk_free);
setServiceState('service_camera', data.services && data.services.camera);
setServiceState('camera_service_state', data.services && data.services.camera);
setServiceState('service_admin_api', data.services && data.services.admin_api);
setServiceState('service_exomy', data.services && data.services.exomy);
}
@@ -2304,6 +2330,52 @@
}
}
function refreshCameraPreview() {
var image = document.getElementById('camera_preview_image');
if (!image) return;
image.src = cameraPreviewBase + '?t=' + Date.now();
}
function renderCameraConfig(data) {
var camera = data && data.camera ? data.camera : {};
var selectedProfile = camera.selected_profile || '';
var select = document.getElementById('camera_profile_select');
if (select && selectedProfile) select.value = selectedProfile;
setText('camera_profile_label', selectedProfile || '--');
setText('camera_resolution_label', (camera.width && camera.height) ? (camera.width + ' x ' + camera.height) : '--');
setText('camera_fps_label', camera.fps ? (camera.fps + ' fps') : '--');
}
async function fetchCameraConfig() {
try {
var response = await fetch(adminApiBase + '/api/camera-config');
if (!response.ok) return;
renderCameraConfig(await response.json());
} catch (e) {}
}
async function setCameraProfile(profileId) {
var statusEl = document.getElementById('camera_profile_status');
if (statusEl) statusEl.textContent = 'Speichere…';
try {
var response = await fetch(adminApiBase + '/api/camera-config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({profile: profileId})
});
var data = await response.json();
if (!response.ok) throw new Error(data.status || 'Fehler');
renderCameraConfig(data);
if (statusEl) statusEl.textContent = data.status || 'Gespeichert';
window.setTimeout(fetchStatus, 1500);
window.setTimeout(refreshCameraPreview, 2200);
window.setTimeout(fetchCameraConfig, 2500);
window.setTimeout(function() { if (statusEl) statusEl.textContent = ''; }, 3500);
} catch (e) {
if (statusEl) statusEl.textContent = 'Fehler';
}
}
async function runRumble() {
try {
var response = await fetch(adminApiBase + '/api/rumble', { method: 'POST' });
@@ -2696,6 +2768,8 @@
if (gpsCanvas) gpsCanvas.addEventListener('click', onGpsSkyCanvasClick);
if (window.location.hash === '#motor') {
switchAdminSection('motor');
} else if (window.location.hash === '#camera') {
switchAdminSection('camera');
} else if (window.location.hash === '#drive') {
switchAdminSection('drive');
} else if (window.location.hash === '#gps') {
@@ -2707,6 +2781,7 @@
updatePageConnectionState();
window.setInterval(updateClock, 1000);
fetchStatus();
fetchCameraConfig();
fetchDelay();
fetchSpeedLimit();
fetchMotorTestStatus();
@@ -2718,7 +2793,9 @@
applyWaypointPlanningState();
renderDriveEstimator();
updateGpsDiagnosticsPanel();
refreshCameraPreview();
window.setInterval(fetchStatus, 10000);
window.setInterval(fetchCameraConfig, 10000);
window.setInterval(fetchDelay, 5000);
window.setInterval(fetchSpeedLimit, 5000);
window.setInterval(fetchMotorTestStatus, 4000);