diff --git a/Anleitungen/.gitkeep b/Anleitungen/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Anleitungen/.gitkeep @@ -0,0 +1 @@ + diff --git a/Anleitungen/Bedienungsanleitung-Admin.txt b/Anleitungen/Bedienungsanleitung-Admin.txt new file mode 100644 index 0000000..9c38f46 --- /dev/null +++ b/Anleitungen/Bedienungsanleitung-Admin.txt @@ -0,0 +1,244 @@ +Bedienungsanleitung ExoMy - Admin +================================= + +Diese Anleitung ist für Administration, Diagnose und technische Bedienung gedacht. +Sie beschreibt die Admin-Seite und die wichtigsten technischen Zusammenhänge. + + +1. Admin-Seite +============== + +Adresse: + +http://:8000/admin.html + +Typische Adressen: +- http://192.168.1.9:8000/admin.html +- http://192.168.1.83:8000/admin.html +- http://192.168.50.1:8000/admin.html + +Passwort: +- cuno + + +2. Zweck der Admin-Seite +======================== + +Die Admin-Seite ist für: +- Neustarts +- Diagnose +- Kameraeinstellungen +- GPS-Aktionen +- Latenz-Simulation +- Überblick über Systemstatus und Dienste + + +3. Wichtige Tabs und Bereiche +============================= + +Aktionen: +- GPS neu starten +- GPS-Kaltstart +- Kameradienst neu starten +- ExoMy-Container neu starten +- Raspberry Pi neu starten +- Raspberry Pi herunterfahren + +Kamera: +- Auswahl der Stream-Auflösung +- nur 4:3-Profile +- aktuelle Vorschau + +GPS: +- GPS-Status +- Satelliten +- Diagnosewerte +- Neustart und Kaltstart + +Systemstatus: +- WLAN +- WLAN-Signal +- CPU-Temperatur +- CPU-Auslastung +- RAM +- Uptime +- freier Speicherplatz +- Unterspannung + + +4. Kamera +========= + +Die Kamera ist bewusst auf 4:3-Profile reduziert. + +Verfügbare Profile: +- 640 x 480 +- 1024 x 768 +- 1296 x 972 + +Grund: +- mit 4:3 bleibt beim vorhandenen Kameramodul das größtmögliche Sichtfeld erhalten +- 16:9-Modi wurden entfernt, weil sie im praktischen Einsatz einen engeren Bildausschnitt liefern + +Wichtig: +- ein Profilwechsel speichert die Auswahl dauerhaft +- danach wird der Kameradienst automatisch neu gestartet + + +5. Kamera-Streams +================= + +Es gibt zwei verschiedene Kamerapfade: + +- Port 8081 + Rohstream ohne Verzögerung + +- Port 8083 + Delay-Proxy mit eingestellter Verzögerung + +Wichtige Zuordnung: +- Hauptseite `index.html` soll bewusst den Stream auf 8083 verwenden +- Admin-Vorschau darf direkt den Rohstream nutzen + +Merksatz: +- 8081 = direkt +- 8083 = verzögert + + +6. Latenz-Simulation +==================== + +Die Latenz-Simulation wird im Admin eingestellt. + +Bereich: +- 0 bis 10 Sekunden + +Die Verzögerung wirkt gleichzeitig auf: +- Steuerbefehle +- Kamerabild + +Technisch bedeutet das: +- Steuerbefehle laufen über den ROS-Knoten `delay_node` +- Kamerabilder laufen über den Dienst `video_delay_proxy` + +Signalweg Kamera: +- Kamera -> 8081 Rohstream -> 8083 Delay-Proxy -> Web-GUI + +Signalweg Steuerung: +- Web-GUI / physischer Controller -> /joy -> /delay_node -> /joy_delayed -> Rover-Steuerung + +Zweck: +- realistische Simulation von Signallaufzeiten +- Test von Fahrverhalten bei Verzögerung + + +7. GPS +====== + +Verwendeter Empfänger: +- GlobalSat BU-353N5 USB-GNSS-Empfänger + +GPS neu starten: +- startet nur den laufenden GPS-Knoten im Container neu + +GPS-Kaltstart: +- sendet einen Kaltstart-Befehl an den Empfänger +- startet danach den GPS-Knoten neu +- der nächste Fix kann deutlich länger dauern + +Die GPS-Porterkennung ist portunabhängig aufgebaut. +Der Node sucht automatisch nach: +- /dev/serial/by-id/* +- /dev/serial/by-path/* +- /dev/ttyUSB* +- /dev/ttyACM* + + +8. ROS und Web-GUI +================== + +Die Hauptseite nutzt: +- Port 8000 für die Web-GUI +- Port 9090 für ROSBridge WebSocket + +Wichtig: +- ROS-Anzeige in der Hauptseite und echte ROS-Laufzeit können sich bei kurzen Verbindungsstörungen unterschiedlich verhalten +- bei GPS-Neustarts kann die Startseite kurz eine fehlerhafte oder zu strenge ROS-Anzeige liefern +- das bedeutet nicht automatisch, dass `rosbridge_websocket` wirklich abgestürzt ist + + +9. Wichtige Dienste auf dem Pi +============================== + +Systemd-Dienste: +- exomy-camera-stream.service +- exomy-video-delay.service +- exomy-admin-api.service +- exomy-wifi-fallback.service + +Bedeutung: +- exomy-camera-stream = direkter MJPEG-Stream +- exomy-video-delay = verzögerter Video-Proxy +- exomy-admin-api = Admin-REST-API +- exomy-wifi-fallback = Netzlogik und Fallback-AP + + +10. Docker und Container +======================== + +Der Rover läuft hauptsächlich im Container: +- exomy_autostart + +Dort laufen unter anderem: +- rosbridge_websocket +- rosapi_node +- gps_node.py +- robot_node.py +- motor_node.py +- joystick_parser_node.py +- f710_joy_node.py +- delay_node + + +11. Netzwerk +============ + +Bevorzugt: +- WLAN 192.168.1.9 + +Alternativ: +- LAN 192.168.1.83 + +Fallback: +- Access Point CUNO +- 192.168.50.1 + + +12. Typische Diagnosefälle +========================== + +Kein Kamerabild auf der Hauptseite: +- prüfen, ob 8083 der verwendete Stream ist +- prüfen, ob exomy-video-delay.service läuft +- prüfen, ob 8081 und 8083 erreichbar sind + +Kein GPS: +- GPS-Tab prüfen +- Satelliten und Diagnose ansehen +- GPS neu starten +- bei Bedarf GPS-Kaltstart + +Hauptseite zeigt komische Verbindungszustände: +- Seite hart neu laden +- Admin-Status gegenprüfen +- prüfen, ob Dienste auf dem Pi laufen + + +13. Zuständigkeit +================= + +Die Hauptseite ist für Bedienung im Betrieb. +Die Admin-Seite ist für technische Eingriffe. + +Auf der Hauptseite sollten keine Wartungsarbeiten gemacht werden. +Für Neustarts, Diagnose und Konfiguration immer den Admin-Bereich nutzen. diff --git a/Anleitungen/Bedienungsanleitung-User.txt b/Anleitungen/Bedienungsanleitung-User.txt new file mode 100644 index 0000000..7a007e1 --- /dev/null +++ b/Anleitungen/Bedienungsanleitung-User.txt @@ -0,0 +1,190 @@ +Bedienungsanleitung ExoMy - User +================================ + +Diese Anleitung ist für normale Benutzer gedacht. +Gemeint ist nur die Hauptseite des Rovers: + +http://:8000/ + + +1. Zweck der Hauptseite +======================= + +Über die Hauptseite kann der Rover beobachtet und gesteuert werden. + +Die Seite zeigt: +- das Kamerabild +- den aktuellen Fahrmodus +- den Verbindungsstatus +- die GPS-Anzeige +- den virtuellen Joystick +- wichtige Systemwerte + + +2. Verbindung zum Rover +======================= + +Der Rover ist je nach Netzwerk unter einer dieser Adressen erreichbar: + +- Heimnetz / WLAN: http://192.168.1.9:8000/ +- LAN: http://192.168.1.83:8000/ +- Fallback-Access-Point: http://192.168.50.1:8000/ + +Wenn die Seite nicht lädt: +- prüfen, ob der Rover eingeschaltet ist +- prüfen, ob das richtige WLAN verwendet wird +- Seite neu laden + + +3. Kamerabild +============= + +Das große Bild in der Mitte ist das Kamerabild des Rovers. + +Wichtig: +- Das Bild kann absichtlich verzögert sein, wenn in der Admin-Seite eine Latenz-Simulation eingestellt wurde. +- Die Hauptseite nutzt dafür den verzögerten Videoweg. + +Wenn kein Bild zu sehen ist: +- Seite neu laden +- Netzwerkverbindung prüfen +- bei anhaltendem Problem Admin oder Technik informieren + + +4. Fahrmodi +=========== + +Der Rover kennt mehrere Fahrmodi: + +- Ackermann + Normales Fahren wie bei einem Auto + +- Turn on Point + Drehen auf der Stelle + +- Crab + Seitliches Fahren + +Der aktive Modus wird auf der Hauptseite angezeigt. + + +5. Steuerung über den virtuellen Joystick +========================================= + +Die Steuerung erfolgt mit dem virtuellen Joystick auf der Hauptseite. + +Grundprinzip: +- nach vorne = vorwärts fahren +- nach hinten = rückwärts fahren +- nach links = links lenken +- nach rechts = rechts lenken + +Hinweise: +- kleine Bewegungen vorsichtig ausführen +- bei Verzögerung ruhig bleiben und nicht hektisch gegensteuern +- nach dem Loslassen geht der virtuelle Joystick wieder in die Mitte zurück + + +6. Motoren +========== + +Der Rover fährt nur, wenn die Motoren freigegeben sind. + +Wenn der Rover nicht reagiert: +- prüfen, ob auf der Hauptseite die Motoren aktiviert sind +- prüfen, ob eine Verbindung zum Rover besteht + + +7. GPS-Anzeige +============== + +Auf der Hauptseite wird bei GPS die Anzahl der empfangenen Satelliten angezeigt. + +Beispiele: +- 0 Sat +- 12 Sat +- 21 Sat + +Wenn dort Offline steht: +- hat die Seite gerade keine brauchbaren GPS-Daten +- nach einem GPS-Neustart oder Kaltstart kann das kurz dauern + + +8. Statusanzeigen +================= + +Auf der Hauptseite werden mehrere Statuswerte angezeigt. + +Wichtige Bedeutungen: +- Cuno online + Die Seite hat Verbindung zu den wichtigen Diensten + +- Cuno eingeschränkt + Ein Teil der Funktionen ist erreichbar, aber nicht alles + +- Cuno offline + Die Verbindung ist derzeit gestört oder unterbrochen + +- ROS + Verbindung zur ROS-Schnittstelle des Rovers + +- CAM + Zustand des Kamerastreams + +- GPS + Anzahl empfangener Satelliten oder Offline + + +9. Verhalten bei Verzögerung +============================ + +Es kann sein, dass absichtlich eine Signallaufzeit simuliert wird. + +Dann reagieren: +- Kamerabild später +- Fahrbefehle später + +In diesem Fall: +- nur kleine Steuerbewegungen machen +- auf die Wirkung warten +- nicht mehrfach hektisch nachkorrigieren + + +10. Wenn der Rover nicht reagiert +================================= + +In dieser Reihenfolge prüfen: + +1. Ist das Kamerabild da? +2. Steht die Seite auf online oder eingeschränkt? +3. Sind die Motoren aktiviert? +4. Reagiert der virtuelle Joystick? +5. Seite neu laden + +Wenn das Problem bleibt: +- nicht weiter experimentieren +- Admin oder Technik informieren + + +11. Sicherheit +============== + +Beim Fahren immer beachten: + +- langsam anfangen +- genug Abstand zu Personen halten +- nicht blind fahren +- bei schlechter Verbindung besonders vorsichtig sein +- bei Verzögerung mit großer Reserve steuern + + +12. Kurzfassung +=============== + +Die Hauptseite ist für drei Dinge da: + +- sehen, was die Kamera zeigt +- den Rover fahren +- den Zustand grob überwachen + +Für Wartung, Neustarts, GPS-Aktionen oder Kameraeinstellungen ist die Admin-Seite zuständig. diff --git a/ExoMy_Software-master/gui/admin.html b/ExoMy_Software-master/gui/admin.html index 4aff3fd..181d321 100644 --- a/ExoMy_Software-master/gui/admin.html +++ b/ExoMy_Software-master/gui/admin.html @@ -235,20 +235,35 @@
- Stream-Auflösung + Kamera-Stream Konfiguration
+ +
- Die neue Auflösung wird gespeichert und der Kameradienst automatisch neu gestartet. Der Videostream ist dabei für einen kurzen Moment unterbrochen. + Auflösung und Bildrate werden gespeichert. Danach startet der Kameradienst automatisch neu. Der Videostream ist dabei für einen kurzen Moment unterbrochen.
@@ -2353,8 +2368,15 @@ 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; + var profileSelect = document.getElementById('camera_profile_select'); + var fpsSelect = document.getElementById('camera_fps_select'); + if (profileSelect && selectedProfile) profileSelect.value = selectedProfile; + if (fpsSelect && Array.isArray(camera.fps_options) && camera.fps_options.length) { + fpsSelect.innerHTML = camera.fps_options.map(function(option) { + return ''; + }).join(''); + } + if (fpsSelect && camera.fps) fpsSelect.value = String(camera.fps); 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') : '--'); @@ -2369,13 +2391,15 @@ } async function setCameraProfile(profileId) { + var fpsSelect = document.getElementById('camera_fps_select'); + var fps = fpsSelect ? Number(fpsSelect.value || 10) : 10; 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}) + body: JSON.stringify({profile: profileId, fps: fps}) }); var data = await response.json(); if (!response.ok) throw new Error(data.status || 'Fehler'); @@ -2390,6 +2414,12 @@ } } + function setCameraFps(fps) { + var profileSelect = document.getElementById('camera_profile_select'); + var profileId = profileSelect ? profileSelect.value : '1296x972'; + setCameraProfile(profileId); + } + async function runRumble() { try { var response = await fetch(adminApiBase + '/api/rumble', { method: 'POST' }); diff --git a/ExoMy_Software-master/gui/app.css b/ExoMy_Software-master/gui/app.css index 9804844..3f24b4b 100644 --- a/ExoMy_Software-master/gui/app.css +++ b/ExoMy_Software-master/gui/app.css @@ -1181,14 +1181,16 @@ body.page-admin .link-btn:hover{ background:rgba(0,136,255,.14); color:#55AAFF; /* ── DROPDOWN CONTROL ───────────────────────────── */ body.page-admin .dropdown-row{ display:flex; align-items:center; gap:10px; padding:10px 12px; } body.page-admin .dropdown-row label{ font-size:9px; letter-spacing:1.6px; color:rgba(210,255,235,.9); text-transform:uppercase; min-width:54px; } +body.page-admin .dropdown-status-row{ padding:0 12px 10px 76px; } body.page-admin .ctrl-select{ font-family:var(--mono); font-size:10px; letter-spacing:1px; color:rgba(245,255,250,.96); background:#050A14; border:1px solid rgba(120,255,205,.36); padding:7px 10px; - cursor:pointer; outline:none; flex:1; + cursor:pointer; outline:none; flex:0 0 190px; width:190px; max-width:190px; } 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 .ctrl-status_block{ display:block; min-width:0; max-width:190px; text-align:left; white-space:normal; line-height:1.45; } 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); diff --git a/ExoMy_Software-master/gui/index.html b/ExoMy_Software-master/gui/index.html index c592ba3..bea1ee9 100644 --- a/ExoMy_Software-master/gui/index.html +++ b/ExoMy_Software-master/gui/index.html @@ -57,7 +57,7 @@
- Kamera — CAM01 + Kamera - CAM01 Live Stream
@@ -288,6 +288,10 @@ var gpsStatusListener = null; var gpsDiagnosticsListener = null; var rosReconnectTimer = null; var rosDisconnectTimer = null; +var cameraReconnectTimer = null; +var cameraRecoveryTimers = []; +var lastCameraServiceState = ""; +var lastCameraSettingsVersion = null; var publishTimer = null; var statusTimer = null; var axes = [0, 0, 0, 0, 0, 0]; @@ -926,6 +930,43 @@ function refreshRosSocketState() { } } +function clearCameraReconnect() { + if (!cameraReconnectTimer) return; + window.clearTimeout(cameraReconnectTimer); + cameraReconnectTimer = null; +} + +function clearCameraRecoveryTimers() { + cameraRecoveryTimers.forEach(function (timerId) { + window.clearTimeout(timerId); + }); + cameraRecoveryTimers = []; +} + +function reloadCameraStream() { + var video = document.getElementById("video_output"); + if (!video) return; + video.src = "//" + hostUrl + ":8083/stream.mjpg"; +} + +function scheduleCameraReconnect(delayMs) { + if (cameraReconnectTimer) return; + cameraReconnectTimer = window.setTimeout(function () { + cameraReconnectTimer = null; + reloadCameraStream(); + }, delayMs || 2500); +} + +function triggerCameraRecovery() { + clearCameraReconnect(); + clearCameraRecoveryTimers(); + [1200, 2600, 4200, 6500].forEach(function (delayMs) { + cameraRecoveryTimers.push(window.setTimeout(function () { + reloadCameraStream(); + }, delayMs)); + }); +} + function updateUndervoltageDisplay(text, state) { var el = document.getElementById("stat-undervoltage"); if (!el) { @@ -952,6 +993,8 @@ function updateUndervoltageDisplay(text, state) { function updateServiceState(payload) { var system = payload.system || {}; var services = payload.services || {}; + var cameraServiceState = String(services.camera || ""); + var cameraSettingsVersion = Number(services.camera_settings_version || 0); setText("stat-wifi", system.wifi || "unbekannt"); setText("stat-ips", system.ips || "unbekannt"); updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown"); @@ -963,7 +1006,17 @@ function updateServiceState(payload) { document.getElementById("bar-cpu").style.width = percentTextToWidth(system.cpu_usage); document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage); document.getElementById("bar-temp").style.width = temperatureTextToWidth(system.cpu_temperature); - setText("cam-state", services.camera === "active" ? "Online" : services.camera || "unbekannt"); + if (cameraServiceState === "active" && lastCameraServiceState && lastCameraServiceState !== "active") { + setText("cam-state", "Verbinde..."); + triggerCameraRecovery(); + } + if (lastCameraSettingsVersion !== null && cameraSettingsVersion !== lastCameraSettingsVersion) { + setText("cam-state", "Verbinde..."); + triggerCameraRecovery(); + } + lastCameraServiceState = cameraServiceState; + lastCameraSettingsVersion = cameraSettingsVersion; + setText("cam-state", cameraServiceState === "active" ? "Online" : cameraServiceState || "unbekannt"); } function fetchSystemStatus() { @@ -1236,8 +1289,10 @@ window.addEventListener("load", function () { window.setInterval(renderTrajectoryOverlay, 250); var video = document.getElementById("video_output"); - video.src = "//" + hostUrl + ":8083/stream.mjpg"; + reloadCameraStream(); video.addEventListener("load", function () { + clearCameraReconnect(); + clearCameraRecoveryTimers(); setText("cam-state", "Online"); setText("cam-host", hostUrl); setText("cam-live-text", "LIVE"); @@ -1254,6 +1309,7 @@ window.addEventListener("load", function () { if (liveIndicator) { liveIndicator.classList.add("is-offline"); } + scheduleCameraReconnect(2500); }); updateCameraFrameLayout(); window.addEventListener("resize", updateCameraFrameLayout); diff --git a/ExoMy_Software-master/scripts/exomy_admin_api.py b/ExoMy_Software-master/scripts/exomy_admin_api.py index 4b5d509..6912dc8 100644 --- a/ExoMy_Software-master/scripts/exomy_admin_api.py +++ b/ExoMy_Software-master/scripts/exomy_admin_api.py @@ -63,6 +63,8 @@ CAMERA_PROFILES = [ ] CAMERA_PROFILE_MAP = {profile['id']: profile for profile in CAMERA_PROFILES} DEFAULT_CAMERA_PROFILE_ID = '1296x972' +CAMERA_FPS_OPTIONS = [1, 2, 3, 4, 5, 7, 10, 20] +DEFAULT_CAMERA_FPS = 10 class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer): @@ -292,6 +294,7 @@ def collect_status(): }, 'services': { 'camera': get_systemd_state('exomy-camera-stream.service'), + 'camera_settings_version': read_camera_settings_version(), 'admin_api': get_systemd_state('exomy-admin-api.service'), 'exomy': get_container_state(EXOMY_CONTAINER), } @@ -363,6 +366,16 @@ def normalize_camera_profile(profile_id): return dict(profile) +def normalize_camera_fps(value): + try: + fps = int(value) + except (TypeError, ValueError): + raise ValueError('Unbekannte Bildrate') + if fps not in CAMERA_FPS_OPTIONS: + raise ValueError('Unbekannte Bildrate') + return fps + + def read_camera_settings(): default_profile = get_default_camera_profile() try: @@ -371,37 +384,58 @@ def read_camera_settings(): 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'])) + fps = normalize_camera_fps(payload.get('fps', DEFAULT_CAMERA_FPS)) except (TypeError, ValueError): - return default_profile + fps = DEFAULT_CAMERA_FPS - for profile in CAMERA_PROFILES: - if profile['width'] == width and profile['height'] == height and profile['fps'] == fps: - return dict(profile) - return default_profile + selected_profile_id = str(payload.get('profile', '')).strip() + if selected_profile_id in CAMERA_PROFILE_MAP: + profile = dict(CAMERA_PROFILE_MAP[selected_profile_id]) + else: + profile = None + for item in CAMERA_PROFILES: + if item['width'] == width and item['height'] == height: + profile = dict(item) + break + if profile is None: + profile = default_profile + + profile['fps'] = fps + return profile -def write_camera_settings(profile): +def write_camera_settings(profile, fps): profile = dict(profile) + fps = normalize_camera_fps(fps) os.makedirs(os.path.dirname(CAMERA_SETTINGS_FILE), exist_ok=True) payload = { 'profile': profile['id'], 'width': profile['width'], 'height': profile['height'], - 'fps': profile['fps'], + 'fps': fps, + 'updated_at': int(time.time()), } with open(CAMERA_SETTINGS_FILE, 'w', encoding='utf-8') as handle: json.dump(payload, handle, indent=2, sort_keys=True) handle.write('\n') +def read_camera_settings_version(): + try: + with open(CAMERA_SETTINGS_FILE, 'r', encoding='utf-8') as handle: + payload = json.load(handle) + except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError, TypeError): + return 0 + + try: + return int(payload.get('updated_at', 0)) + except (TypeError, ValueError): + return 0 + + def get_camera_config(): profile = read_camera_settings() return { @@ -411,14 +445,23 @@ def get_camera_config(): 'width': profile['width'], 'height': profile['height'], 'fps': profile['fps'], + 'fps_options': list(CAMERA_FPS_OPTIONS), 'profiles': [dict(item) for item in CAMERA_PROFILES], } } -def set_camera_profile(profile_id): - profile = normalize_camera_profile(profile_id) - write_camera_settings(profile) +def set_camera_config(profile_id=None, fps=None): + current = read_camera_settings() + if profile_id is None: + profile = normalize_camera_profile(current['id']) + else: + profile = normalize_camera_profile(profile_id) + if fps is None: + fps = current.get('fps', DEFAULT_CAMERA_FPS) + fps = normalize_camera_fps(fps) + profile['fps'] = fps + write_camera_settings(profile, fps) restart_camera_service() return profile @@ -1164,14 +1207,15 @@ class Handler(http.server.BaseHTTPRequestHandler): self.write_json(400, {'status': 'Ungültige Anfrage'}) return try: - profile = set_camera_profile(payload.get('profile')) + profile = set_camera_config(payload.get('profile'), payload.get('fps')) self.write_json(200, { - 'status': 'Kameraauflösung gespeichert, Kameradienst startet neu', + 'status': 'Kameraprofil gespeichert, Kameradienst startet neu', 'camera': { 'selected_profile': profile['id'], 'width': profile['width'], 'height': profile['height'], 'fps': profile['fps'], + 'fps_options': list(CAMERA_FPS_OPTIONS), 'profiles': [dict(item) for item in CAMERA_PROFILES], } }) diff --git a/ExoMy_Software-master/scripts/libcamera_mjpeg_server.py b/ExoMy_Software-master/scripts/libcamera_mjpeg_server.py index 247c9b4..000c20d 100644 --- a/ExoMy_Software-master/scripts/libcamera_mjpeg_server.py +++ b/ExoMy_Software-master/scripts/libcamera_mjpeg_server.py @@ -15,8 +15,8 @@ LATEST_LOCK = threading.Lock() PORT = 8081 CONFIG_FILE = os.path.join(os.path.dirname(__file__), '..', 'config', 'camera_settings.json') DEFAULT_CAMERA_SETTINGS = { - 'width': 1280, - 'height': 720, + 'width': 1296, + 'height': 972, 'fps': 10, } diff --git a/ExoMy_Software-master/scripts/video_delay_proxy.py b/ExoMy_Software-master/scripts/video_delay_proxy.py index 386a511..8f06123 100644 --- a/ExoMy_Software-master/scripts/video_delay_proxy.py +++ b/ExoMy_Software-master/scripts/video_delay_proxy.py @@ -11,6 +11,7 @@ import os import socketserver import threading import time +import urllib.parse import urllib.request DELAY_FILE = '/tmp/exomy_delay.txt' @@ -93,8 +94,11 @@ class ReusableTCPServer(socketserver.ThreadingTCPServer): class Handler(http.server.BaseHTTPRequestHandler): + def normalized_path(self): + return urllib.parse.urlsplit(self.path).path + def do_GET(self): - if self.path != '/stream.mjpg': + if self.normalized_path() != '/stream.mjpg': self.send_error(404) return self.send_response(200) diff --git a/index_upload_tmp.html b/index_upload_tmp.html index 6ccb83b..115eede 100644 --- a/index_upload_tmp.html +++ b/index_upload_tmp.html @@ -57,7 +57,7 @@
- Kamera — CAM01 + Kamera - CAM01 Live Stream
@@ -288,6 +288,10 @@ var gpsStatusListener = null; var gpsDiagnosticsListener = null; var rosReconnectTimer = null; var rosDisconnectTimer = null; +var cameraReconnectTimer = null; +var cameraRecoveryTimers = []; +var lastCameraServiceState = ""; +var lastCameraSettingsVersion = null; var publishTimer = null; var statusTimer = null; var axes = [0, 0, 0, 0, 0, 0]; @@ -926,6 +930,43 @@ function refreshRosSocketState() { } } +function clearCameraReconnect() { + if (!cameraReconnectTimer) return; + window.clearTimeout(cameraReconnectTimer); + cameraReconnectTimer = null; +} + +function clearCameraRecoveryTimers() { + cameraRecoveryTimers.forEach(function (timerId) { + window.clearTimeout(timerId); + }); + cameraRecoveryTimers = []; +} + +function reloadCameraStream() { + var video = document.getElementById("video_output"); + if (!video) return; + video.src = "//" + hostUrl + ":8083/stream.mjpg"; +} + +function scheduleCameraReconnect(delayMs) { + if (cameraReconnectTimer) return; + cameraReconnectTimer = window.setTimeout(function () { + cameraReconnectTimer = null; + reloadCameraStream(); + }, delayMs || 2500); +} + +function triggerCameraRecovery() { + clearCameraReconnect(); + clearCameraRecoveryTimers(); + [1200, 2600, 4200, 6500].forEach(function (delayMs) { + cameraRecoveryTimers.push(window.setTimeout(function () { + reloadCameraStream(); + }, delayMs)); + }); +} + function updateUndervoltageDisplay(text, state) { var el = document.getElementById("stat-undervoltage"); if (!el) { @@ -952,6 +993,8 @@ function updateUndervoltageDisplay(text, state) { function updateServiceState(payload) { var system = payload.system || {}; var services = payload.services || {}; + var cameraServiceState = String(services.camera || ""); + var cameraSettingsVersion = Number(services.camera_settings_version || 0); setText("stat-wifi", system.wifi || "unbekannt"); setText("stat-ips", system.ips || "unbekannt"); updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown"); @@ -963,7 +1006,17 @@ function updateServiceState(payload) { document.getElementById("bar-cpu").style.width = percentTextToWidth(system.cpu_usage); document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage); document.getElementById("bar-temp").style.width = temperatureTextToWidth(system.cpu_temperature); - setText("cam-state", services.camera === "active" ? "Online" : services.camera || "unbekannt"); + if (cameraServiceState === "active" && lastCameraServiceState && lastCameraServiceState !== "active") { + setText("cam-state", "Verbinde..."); + triggerCameraRecovery(); + } + if (lastCameraSettingsVersion !== null && cameraSettingsVersion !== lastCameraSettingsVersion) { + setText("cam-state", "Verbinde..."); + triggerCameraRecovery(); + } + lastCameraServiceState = cameraServiceState; + lastCameraSettingsVersion = cameraSettingsVersion; + setText("cam-state", cameraServiceState === "active" ? "Online" : cameraServiceState || "unbekannt"); } function fetchSystemStatus() { @@ -1236,8 +1289,10 @@ window.addEventListener("load", function () { window.setInterval(renderTrajectoryOverlay, 250); var video = document.getElementById("video_output"); - video.src = "//" + hostUrl + ":8083/stream.mjpg"; + reloadCameraStream(); video.addEventListener("load", function () { + clearCameraReconnect(); + clearCameraRecoveryTimers(); setText("cam-state", "Online"); setText("cam-host", hostUrl); setText("cam-live-text", "LIVE"); @@ -1254,6 +1309,7 @@ window.addEventListener("load", function () { if (liveIndicator) { liveIndicator.classList.add("is-offline"); } + scheduleCameraReconnect(2500); }); updateCameraFrameLayout(); window.addEventListener("resize", updateCameraFrameLayout);