Kaera umschaltbar
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"fps": 10,
|
||||
"height": 972,
|
||||
"profile": "1296x972",
|
||||
"width": 1296
|
||||
}
|
||||
@@ -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ö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 · 4:3 · größtes Sichtfeld</option>
|
||||
<option value="1024x768">1024 x 768 · 4:3 · mehr Details</option>
|
||||
<option value="1296x972">1296 x 972 · 4:3 · noch mehr Details</option>
|
||||
</select>
|
||||
<span id="camera_profile_status" class="ctrl-status"></span>
|
||||
</div>
|
||||
<div class="copy-text camera-copy">
|
||||
Die neue Auflösung wird gespeichert und der Kameradienst automatisch neu gestartet. Der Videostream ist dabei fü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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<node name="motors" pkg="exomy" type="motor_node.py" respawn="true" output="screen" />
|
||||
<node name="joystick" pkg="exomy" type="joystick_parser_node.py" respawn="true" output="screen" />
|
||||
<node name="gps" pkg="exomy" type="gps_node.py" respawn="true" output="screen">
|
||||
<param name="port" value="/dev/ttyUSB0"/>
|
||||
<param name="port" value="auto"/>
|
||||
<param name="baud" value="4800"/>
|
||||
<param name="frame_id" value="gps"/>
|
||||
</node>
|
||||
|
||||
@@ -21,7 +21,38 @@ DRIVE_ESTIMATOR_CALIBRATION_FILE = os.path.join(
|
||||
ROUTE_LIBRARY_FILE = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'config', 'route_library.json'
|
||||
)
|
||||
CAMERA_SETTINGS_FILE = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'config', 'camera_settings.json'
|
||||
)
|
||||
MOTOR_TEST_LOCK = threading.Lock()
|
||||
CAMERA_PROFILES = [
|
||||
{
|
||||
'id': '640x480',
|
||||
'label': '640 x 480',
|
||||
'description': '4:3, klein und sparsam',
|
||||
'width': 640,
|
||||
'height': 480,
|
||||
'fps': 10,
|
||||
},
|
||||
{
|
||||
'id': '1024x768',
|
||||
'label': '1024 x 768',
|
||||
'description': '4:3, großes Sichtfeld mit mehr Details',
|
||||
'width': 1024,
|
||||
'height': 768,
|
||||
'fps': 10,
|
||||
},
|
||||
{
|
||||
'id': '1296x972',
|
||||
'label': '1296 x 972',
|
||||
'description': '4:3, großes Sichtfeld mit noch mehr Details',
|
||||
'width': 1296,
|
||||
'height': 972,
|
||||
'fps': 10,
|
||||
},
|
||||
]
|
||||
CAMERA_PROFILE_MAP = {profile['id']: profile for profile in CAMERA_PROFILES}
|
||||
DEFAULT_CAMERA_PROFILE_ID = '1296x972'
|
||||
|
||||
|
||||
class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
|
||||
@@ -285,6 +316,77 @@ def set_speed_limit(percent):
|
||||
return percent
|
||||
|
||||
|
||||
def get_default_camera_profile():
|
||||
return dict(CAMERA_PROFILE_MAP[DEFAULT_CAMERA_PROFILE_ID])
|
||||
|
||||
|
||||
def normalize_camera_profile(profile_id):
|
||||
profile = CAMERA_PROFILE_MAP.get(str(profile_id or '').strip())
|
||||
if profile is None:
|
||||
raise ValueError('Unbekanntes Kamera-Profil')
|
||||
return dict(profile)
|
||||
|
||||
|
||||
def read_camera_settings():
|
||||
default_profile = get_default_camera_profile()
|
||||
try:
|
||||
with open(CAMERA_SETTINGS_FILE, 'r', encoding='utf-8') as handle:
|
||||
payload = json.load(handle)
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError, TypeError):
|
||||
return default_profile
|
||||
|
||||
selected_profile_id = str(payload.get('profile', '')).strip()
|
||||
if selected_profile_id in CAMERA_PROFILE_MAP:
|
||||
return dict(CAMERA_PROFILE_MAP[selected_profile_id])
|
||||
|
||||
try:
|
||||
width = int(payload.get('width', default_profile['width']))
|
||||
height = int(payload.get('height', default_profile['height']))
|
||||
fps = int(payload.get('fps', default_profile['fps']))
|
||||
except (TypeError, ValueError):
|
||||
return default_profile
|
||||
|
||||
for profile in CAMERA_PROFILES:
|
||||
if profile['width'] == width and profile['height'] == height and profile['fps'] == fps:
|
||||
return dict(profile)
|
||||
return default_profile
|
||||
|
||||
|
||||
def write_camera_settings(profile):
|
||||
profile = dict(profile)
|
||||
os.makedirs(os.path.dirname(CAMERA_SETTINGS_FILE), exist_ok=True)
|
||||
payload = {
|
||||
'profile': profile['id'],
|
||||
'width': profile['width'],
|
||||
'height': profile['height'],
|
||||
'fps': profile['fps'],
|
||||
}
|
||||
with open(CAMERA_SETTINGS_FILE, 'w', encoding='utf-8') as handle:
|
||||
json.dump(payload, handle, indent=2, sort_keys=True)
|
||||
handle.write('\n')
|
||||
|
||||
|
||||
def get_camera_config():
|
||||
profile = read_camera_settings()
|
||||
return {
|
||||
'status': 'Bereit',
|
||||
'camera': {
|
||||
'selected_profile': profile['id'],
|
||||
'width': profile['width'],
|
||||
'height': profile['height'],
|
||||
'fps': profile['fps'],
|
||||
'profiles': [dict(item) for item in CAMERA_PROFILES],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def set_camera_profile(profile_id):
|
||||
profile = normalize_camera_profile(profile_id)
|
||||
write_camera_settings(profile)
|
||||
restart_camera_service()
|
||||
return profile
|
||||
|
||||
|
||||
def rumble_controller(duration_ms=400):
|
||||
# Linux FF constants
|
||||
EV_FF = 0x15
|
||||
@@ -856,6 +958,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if self.path == '/api/speed-limit':
|
||||
self.write_json(200, {'speed_limit_percent': get_speed_limit()})
|
||||
return
|
||||
if self.path == '/api/camera-config':
|
||||
self.write_json(200, get_camera_config())
|
||||
return
|
||||
if self.path == '/api/status':
|
||||
self.write_json(200, collect_status())
|
||||
return
|
||||
@@ -932,6 +1037,35 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.write_json(500, {'status': str(exc)})
|
||||
return
|
||||
|
||||
if self.path == '/api/camera-config':
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length', '0'))
|
||||
except ValueError:
|
||||
content_length = 0
|
||||
raw_body = self.rfile.read(content_length) if content_length > 0 else b'{}'
|
||||
try:
|
||||
payload = json.loads(raw_body.decode('utf-8'))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
self.write_json(400, {'status': 'Ungültige Anfrage'})
|
||||
return
|
||||
try:
|
||||
profile = set_camera_profile(payload.get('profile'))
|
||||
self.write_json(200, {
|
||||
'status': 'Kameraauflösung gespeichert, Kameradienst startet neu',
|
||||
'camera': {
|
||||
'selected_profile': profile['id'],
|
||||
'width': profile['width'],
|
||||
'height': profile['height'],
|
||||
'fps': profile['fps'],
|
||||
'profiles': [dict(item) for item in CAMERA_PROFILES],
|
||||
}
|
||||
})
|
||||
except ValueError as exc:
|
||||
self.write_json(400, {'status': str(exc)})
|
||||
except Exception as exc:
|
||||
self.write_json(500, {'status': str(exc)})
|
||||
return
|
||||
|
||||
if self.path == '/api/drive-estimator/calibration':
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length', '0'))
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
import socketserver
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -9,10 +12,40 @@ BOUNDARY = b'--frame'
|
||||
LATEST_JPEG = None
|
||||
LATEST_LOCK = threading.Lock()
|
||||
|
||||
WIDTH = 1280
|
||||
HEIGHT = 720
|
||||
FPS = 10
|
||||
PORT = 8081
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(__file__), '..', 'config', 'camera_settings.json')
|
||||
DEFAULT_CAMERA_SETTINGS = {
|
||||
'width': 1280,
|
||||
'height': 720,
|
||||
'fps': 10,
|
||||
}
|
||||
|
||||
|
||||
def load_camera_settings():
|
||||
settings = dict(DEFAULT_CAMERA_SETTINGS)
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as handle:
|
||||
raw_settings = json.load(handle)
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError, TypeError):
|
||||
return settings
|
||||
|
||||
try:
|
||||
width = int(raw_settings.get('width', settings['width']))
|
||||
height = int(raw_settings.get('height', settings['height']))
|
||||
fps = int(raw_settings.get('fps', settings['fps']))
|
||||
except (TypeError, ValueError):
|
||||
return settings
|
||||
|
||||
settings['width'] = max(320, min(2592, width))
|
||||
settings['height'] = max(240, min(1944, height))
|
||||
settings['fps'] = max(1, min(30, fps))
|
||||
return settings
|
||||
|
||||
|
||||
CAMERA_SETTINGS = load_camera_settings()
|
||||
WIDTH = CAMERA_SETTINGS['width']
|
||||
HEIGHT = CAMERA_SETTINGS['height']
|
||||
FPS = CAMERA_SETTINGS['fps']
|
||||
|
||||
|
||||
def camera_reader():
|
||||
@@ -65,14 +98,18 @@ class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def normalized_path(self):
|
||||
return urllib.parse.urlsplit(self.path).path
|
||||
|
||||
def do_HEAD(self):
|
||||
if self.path in ('/', '/index.html'):
|
||||
path = self.normalized_path()
|
||||
if path in ('/', '/index.html'):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
if self.path == '/stream.mjpg':
|
||||
if path == '/stream.mjpg':
|
||||
self.send_response(200)
|
||||
self.send_header('Age', '0')
|
||||
self.send_header('Cache-Control', 'no-cache, private')
|
||||
@@ -84,7 +121,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_error(404)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path in ('/', '/index.html'):
|
||||
path = self.normalized_path()
|
||||
if path in ('/', '/index.html'):
|
||||
body = b'<html><body><img src="/stream.mjpg" /></body></html>'
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
@@ -93,7 +131,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.wfile.write(body)
|
||||
return
|
||||
|
||||
if self.path != '/stream.mjpg':
|
||||
if path != '/stream.mjpg':
|
||||
self.send_error(404)
|
||||
return
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
import errno
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
@@ -12,7 +13,7 @@ from sensor_msgs.msg import NavSatFix, NavSatStatus
|
||||
from std_msgs.msg import String
|
||||
|
||||
|
||||
PORT = '/dev/ttyUSB0'
|
||||
PORT = 'auto'
|
||||
BAUD = 4800
|
||||
FRAME_ID = 'gps'
|
||||
READ_SIZE = 512
|
||||
@@ -114,8 +115,47 @@ def configure_serial(fd, baud):
|
||||
termios.tcflush(fd, termios.TCIFLUSH)
|
||||
|
||||
|
||||
def open_device(port, baud):
|
||||
def resolve_port_candidates(port_config):
|
||||
if port_config is None:
|
||||
port_config = PORT
|
||||
|
||||
config_text = str(port_config).strip()
|
||||
if not config_text or config_text.lower() == 'auto':
|
||||
raw_candidates = [
|
||||
'/dev/serial/by-id/*',
|
||||
'/dev/serial/by-path/*',
|
||||
'/dev/ttyUSB*',
|
||||
'/dev/ttyACM*',
|
||||
]
|
||||
else:
|
||||
raw_candidates = [item.strip() for item in config_text.split(',') if item.strip()]
|
||||
|
||||
candidates = []
|
||||
seen = set()
|
||||
for candidate in raw_candidates:
|
||||
matches = sorted(glob.glob(candidate)) if any(char in candidate for char in '*?[') else [candidate]
|
||||
for match in matches:
|
||||
if match in seen:
|
||||
continue
|
||||
seen.add(match)
|
||||
candidates.append(match)
|
||||
return candidates
|
||||
|
||||
|
||||
def open_device(port_config, baud, baud_rate):
|
||||
last_signature = None
|
||||
while not rospy.is_shutdown():
|
||||
candidates = resolve_port_candidates(port_config)
|
||||
signature = tuple(candidates)
|
||||
if not candidates:
|
||||
if signature != last_signature:
|
||||
rospy.loginfo('GPS wartet auf Geraet. Suchmuster: %s', port_config)
|
||||
last_signature = signature
|
||||
rospy.sleep(1.0)
|
||||
continue
|
||||
|
||||
try:
|
||||
for port in candidates:
|
||||
try:
|
||||
fd = os.open(port, os.O_RDONLY | os.O_NOCTTY | os.O_NONBLOCK)
|
||||
configure_serial(fd, baud)
|
||||
@@ -123,10 +163,11 @@ def open_device(port, baud):
|
||||
return fd
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ENOENT:
|
||||
rospy.logwarn('GPS kann nicht geoeffnet werden: %s', exc)
|
||||
rospy.sleep(1.0)
|
||||
rospy.logwarn('GPS kann nicht geoeffnet werden (%s): %s', port, exc)
|
||||
except termios.error as exc:
|
||||
rospy.logwarn('GPS-Port kann nicht konfiguriert werden: %s', exc)
|
||||
rospy.logwarn('GPS-Port kann nicht konfiguriert werden (%s): %s', port, exc)
|
||||
finally:
|
||||
last_signature = signature
|
||||
rospy.sleep(1.0)
|
||||
return None
|
||||
|
||||
@@ -440,7 +481,7 @@ if __name__ == '__main__':
|
||||
|
||||
while not rospy.is_shutdown():
|
||||
if serial_fd is None:
|
||||
serial_fd = open_device(port, baud)
|
||||
serial_fd = open_device(port, baud, baud_rate)
|
||||
buffer = bytearray()
|
||||
state.connected = serial_fd is not None
|
||||
publish_fix(fix_publisher, overlay_publisher, diagnostics_publisher, state, frame_id)
|
||||
|
||||
Reference in New Issue
Block a user