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);
+77 -32
View File
@@ -995,30 +995,65 @@ body.page-admin .admin-shell{
padding:12px; width:100%;
}
body.page-admin .admin-section-tabs{
display:flex; gap:6px; flex-shrink:0;
display:flex; gap:0; flex-shrink:0;
padding:8px 8px 0;
border-bottom:1px solid rgba(120,255,205,.18);
background:linear-gradient(180deg, rgba(9,18,24,.94), rgba(7,13,18,.88));
border-radius:14px 14px 0 0;
box-shadow:inset 0 -1px 0 rgba(255,255,255,.02);
}
body.page-admin .admin-section-tab{
font-family:var(--disp); font-size:11px; font-weight:700; letter-spacing:1px; text-transform:uppercase;
padding:10px 16px; border:1px solid rgba(120,255,205,.24);
background:rgba(0,255,145,.04); color:rgba(215,255,238,.88);
cursor:pointer; transition:all .15s;
padding:10px 16px;
margin:0 4px -1px 0;
border:1px solid rgba(120,255,205,.18);
border-bottom-color:rgba(120,255,205,.18);
border-radius:10px 10px 0 0;
background:linear-gradient(180deg, rgba(10,22,28,.92), rgba(7,14,20,.96));
color:rgba(215,255,238,.76);
cursor:pointer;
transition:all .15s;
position:relative;
}
body.page-admin .admin-section-tab::after{
content:'';
position:absolute;
left:12px;
right:12px;
bottom:0;
height:2px;
background:transparent;
transition:background .15s ease;
}
body.page-admin .admin-section-tab:hover{
border-color:rgba(180,255,225,.48);
background:rgba(0,255,145,.08);
border-color:rgba(180,255,225,.36);
background:linear-gradient(180deg, rgba(16,34,42,.95), rgba(8,18,24,.98));
color:#fff;
}
body.page-admin .admin-section-tab.is_active{
border-color:rgba(180,255,225,.72);
background:linear-gradient(180deg, rgba(20,120,82,.3), rgba(6,40,28,.95));
border-color:rgba(180,255,225,.62);
border-bottom-color:#0b1218;
background:linear-gradient(180deg, rgba(20,120,82,.24), rgba(11,18,24,1) 68%);
color:#fff;
box-shadow:0 0 14px rgba(120,255,205,.16), inset 0 0 14px rgba(180,255,225,.08);
box-shadow:
0 -1px 0 rgba(255,255,255,.03) inset,
0 0 0 1px rgba(0,255,145,.04),
0 10px 22px rgba(0,0,0,.16);
z-index:2;
}
body.page-admin .admin-section-tab.is_active::after{
background:linear-gradient(90deg, rgba(120,255,205,0), rgba(120,255,205,.92), rgba(120,255,205,0));
}
body.page-admin .admin-section-panel{
flex:1; min-height:0; overflow-y:auto; display:none;
}
body.page-admin .admin-section-panel.is_active{
display:block;
border:1px solid rgba(120,255,205,.16);
border-top:none;
border-radius:0 0 16px 16px;
background:linear-gradient(180deg, rgba(9,16,22,.98), rgba(7,13,18,.98));
box-shadow:0 18px 38px rgba(0,0,0,.16);
}
body.page-admin .admin-section-grid{
min-height:100%;
@@ -1092,7 +1127,13 @@ body.page-admin .motor-admin-tab .steering_neutral_adjust{ padding:4px 10px !imp
}
@media (max-width:479px){
body.page-admin .hdr-badge, body.page-admin .hdr-clk{ display:none; }
body.page-admin .admin-section-tab{ flex:1; text-align:center; padding:10px 8px; }
body.page-admin .admin-section-tabs{ flex-wrap:wrap; gap:0; }
body.page-admin .admin-section-tab{
flex:1 1 calc(50% - 4px);
text-align:center;
padding:10px 8px;
margin:0 4px -1px 0;
}
body.page-admin .motor-admin-tab .tab-row{ grid-template-columns:1fr; }
body.page-admin .motor-admin-tab .tab-box + .tab-box{ border-left:none; border-top:1px solid rgba(0,255,145,.06); }
body.page-admin .drive-estimator-toolbar{ flex-direction:column; align-items:stretch; }
@@ -1115,6 +1156,7 @@ body.page-admin .act-btn.warm:hover{ background:rgba(255,140,0,.12); color:#FF8C
body.page-admin .act-btn.danger{ border-color:rgba(255,51,51,.35); color:rgba(255,51,51,.7); background:rgba(255,51,51,.04); }
body.page-admin .act-btn.danger:hover{ background:rgba(255,51,51,.12); color:#FF3333; border-color:rgba(255,51,51,.7); }
body.page-admin .act-btn.full{ grid-column:span 2; }
body.page-admin .act-grid_single{ grid-template-columns:1fr; }
body.page-admin .act-btn.subtle{ border-color:rgba(0,255,145,.1); color:rgba(0,255,145,.3); }
body.page-admin .act-btn.subtle:hover{ color:rgba(0,255,145,.6); border-color:rgba(0,255,145,.35); }
body.page-admin .act-btn.is_active{
@@ -1147,6 +1189,31 @@ body.page-admin .ctrl-select{
}
body.page-admin .ctrl-select:focus{ border-color:rgba(0,255,145,.5); }
body.page-admin .ctrl-status{ font-size:9px; letter-spacing:1.2px; color:rgba(225,255,242,.92); white-space:nowrap; min-width:64px; text-align:right; }
body.page-admin .camera-copy{
font-size:10px; color:rgba(205,255,235,.86); letter-spacing:.4px; line-height:1.6;
padding:8px 12px; border-bottom:1px solid rgba(0,255,145,.05);
}
body.page-admin .camera-preview-stage{
padding:14px 12px 12px;
background:
radial-gradient(circle at top, rgba(30,90,140,.24), transparent 55%),
linear-gradient(180deg, rgba(7,17,28,.96), rgba(4,10,18,.98));
border-bottom:1px solid rgba(0,255,145,.05);
}
body.page-admin .camera-preview-image{
display:block;
width:100%;
aspect-ratio:16/9;
object-fit:contain;
background:#03070c;
border:1px solid rgba(120,255,205,.16);
border-radius:14px;
box-shadow:0 14px 28px rgba(0,0,0,.28), inset 0 0 0 1px rgba(255,255,255,.03);
}
body.page-admin .camera-preview-meta{
display:grid;
gap:0;
}
body.page-admin .drive-estimator-copy{
font-size:10px; color:rgba(205,255,235,.86); letter-spacing:.4px; line-height:1.6;
padding:8px 12px; border-bottom:1px solid rgba(0,255,145,.05);
@@ -1292,28 +1359,6 @@ body.page-admin .gps-copy{
padding:10px 12px;
border-bottom:1px solid rgba(0,255,145,.05);
}
body.page-admin .gps-copy_compact{
border-bottom:none;
}
body.page-admin .gps-raw-output{
width:100%;
min-height:220px;
max-height:320px;
resize:vertical;
overflow:auto;
margin-top:14px;
padding:12px 14px;
border:1px solid rgba(0,255,145,.18);
border-radius:12px;
background:#07111c;
color:#b8ffe1;
font:12px/1.45 Consolas, "Courier New", monospace;
box-shadow:inset 0 0 0 1px rgba(255,255,255,.02);
}
body.page-admin .gps-raw-output:focus{
outline:none;
border-color:rgba(0,255,145,.28);
}
body.page-admin .gps-sky-stage{
padding:18px;
position:relative;