Anleitungen erstellt und Bildrate einstellbar
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -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://<IP-des-Rovers>: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.
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
Bedienungsanleitung ExoMy - User
|
||||||
|
================================
|
||||||
|
|
||||||
|
Diese Anleitung ist für normale Benutzer gedacht.
|
||||||
|
Gemeint ist nur die Hauptseite des Rovers:
|
||||||
|
|
||||||
|
http://<IP-des-Rovers>: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.
|
||||||
@@ -235,20 +235,35 @@
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="p-hdr">
|
<div class="p-hdr">
|
||||||
<div class="p-dot"></div>
|
<div class="p-dot"></div>
|
||||||
<span class="p-lbl">Stream-Auflösung</span>
|
<span class="p-lbl">Kamera-Stream</span>
|
||||||
<span class="p-tag">Konfiguration</span>
|
<span class="p-tag">Konfiguration</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-row">
|
<div class="dropdown-row">
|
||||||
<label>Profil</label>
|
<label>Profil</label>
|
||||||
<select id="camera_profile_select" class="ctrl-select" onchange="setCameraProfile(this.value)">
|
<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="640x480">640 x 480</option>
|
||||||
<option value="1024x768">1024 x 768 · 4:3 · mehr Details</option>
|
<option value="1024x768">1024 x 768</option>
|
||||||
<option value="1296x972">1296 x 972 · 4:3 · noch mehr Details</option>
|
<option value="1296x972">1296 x 972</option>
|
||||||
</select>
|
</select>
|
||||||
<span id="camera_profile_status" class="ctrl-status"></span>
|
</div>
|
||||||
|
<div class="dropdown-row">
|
||||||
|
<label>Bildrate</label>
|
||||||
|
<select id="camera_fps_select" class="ctrl-select" onchange="setCameraFps(this.value)">
|
||||||
|
<option value="1">1 fps</option>
|
||||||
|
<option value="2">2 fps</option>
|
||||||
|
<option value="3">3 fps</option>
|
||||||
|
<option value="4">4 fps</option>
|
||||||
|
<option value="5">5 fps</option>
|
||||||
|
<option value="7">7 fps</option>
|
||||||
|
<option value="10">10 fps</option>
|
||||||
|
<option value="20">20 fps</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-status-row">
|
||||||
|
<span id="camera_profile_status" class="ctrl-status ctrl-status_block"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="copy-text camera-copy">
|
<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.
|
Auflösung und Bildrate werden gespeichert. Danach startet der Kameradienst automatisch neu. Der Videostream ist dabei für einen kurzen Moment unterbrochen.
|
||||||
</div>
|
</div>
|
||||||
<div class="act-grid act-grid_single">
|
<div class="act-grid act-grid_single">
|
||||||
<button class="act-btn subtle full" onclick="refreshCameraPreview()">Vorschau neu laden</button>
|
<button class="act-btn subtle full" onclick="refreshCameraPreview()">Vorschau neu laden</button>
|
||||||
@@ -2353,8 +2368,15 @@
|
|||||||
function renderCameraConfig(data) {
|
function renderCameraConfig(data) {
|
||||||
var camera = data && data.camera ? data.camera : {};
|
var camera = data && data.camera ? data.camera : {};
|
||||||
var selectedProfile = camera.selected_profile || '';
|
var selectedProfile = camera.selected_profile || '';
|
||||||
var select = document.getElementById('camera_profile_select');
|
var profileSelect = document.getElementById('camera_profile_select');
|
||||||
if (select && selectedProfile) select.value = selectedProfile;
|
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 '<option value="' + option + '">' + option + ' fps</option>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
if (fpsSelect && camera.fps) fpsSelect.value = String(camera.fps);
|
||||||
setText('camera_profile_label', selectedProfile || '--');
|
setText('camera_profile_label', selectedProfile || '--');
|
||||||
setText('camera_resolution_label', (camera.width && camera.height) ? (camera.width + ' x ' + camera.height) : '--');
|
setText('camera_resolution_label', (camera.width && camera.height) ? (camera.width + ' x ' + camera.height) : '--');
|
||||||
setText('camera_fps_label', camera.fps ? (camera.fps + ' fps') : '--');
|
setText('camera_fps_label', camera.fps ? (camera.fps + ' fps') : '--');
|
||||||
@@ -2369,13 +2391,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setCameraProfile(profileId) {
|
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');
|
var statusEl = document.getElementById('camera_profile_status');
|
||||||
if (statusEl) statusEl.textContent = 'Speichere…';
|
if (statusEl) statusEl.textContent = 'Speichere…';
|
||||||
try {
|
try {
|
||||||
var response = await fetch(adminApiBase + '/api/camera-config', {
|
var response = await fetch(adminApiBase + '/api/camera-config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({profile: profileId})
|
body: JSON.stringify({profile: profileId, fps: fps})
|
||||||
});
|
});
|
||||||
var data = await response.json();
|
var data = await response.json();
|
||||||
if (!response.ok) throw new Error(data.status || 'Fehler');
|
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() {
|
async function runRumble() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch(adminApiBase + '/api/rumble', { method: 'POST' });
|
var response = await fetch(adminApiBase + '/api/rumble', { method: 'POST' });
|
||||||
|
|||||||
@@ -1181,14 +1181,16 @@ body.page-admin .link-btn:hover{ background:rgba(0,136,255,.14); color:#55AAFF;
|
|||||||
/* ── DROPDOWN CONTROL ───────────────────────────── */
|
/* ── DROPDOWN CONTROL ───────────────────────────── */
|
||||||
body.page-admin .dropdown-row{ display:flex; align-items:center; gap:10px; padding:10px 12px; }
|
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-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{
|
body.page-admin .ctrl-select{
|
||||||
font-family:var(--mono); font-size:10px; letter-spacing:1px;
|
font-family:var(--mono); font-size:10px; letter-spacing:1px;
|
||||||
color:rgba(245,255,250,.96); background:#050A14;
|
color:rgba(245,255,250,.96); background:#050A14;
|
||||||
border:1px solid rgba(120,255,205,.36); padding:7px 10px;
|
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-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{ 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{
|
body.page-admin .camera-copy{
|
||||||
font-size:10px; color:rgba(205,255,235,.86); letter-spacing:.4px; line-height:1.6;
|
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);
|
padding:8px 12px; border-bottom:1px solid rgba(0,255,145,.05);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<div class="panel" id="cam-panel">
|
<div class="panel" id="cam-panel">
|
||||||
<div class="p-hdr">
|
<div class="p-hdr">
|
||||||
<div class="p-dot"></div>
|
<div class="p-dot"></div>
|
||||||
<span class="p-lbl">Kamera — CAM01</span>
|
<span class="p-lbl">Kamera - CAM01</span>
|
||||||
<span class="p-tag">Live Stream</span>
|
<span class="p-tag">Live Stream</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cam-port">
|
<div class="cam-port">
|
||||||
@@ -288,6 +288,10 @@ var gpsStatusListener = null;
|
|||||||
var gpsDiagnosticsListener = null;
|
var gpsDiagnosticsListener = null;
|
||||||
var rosReconnectTimer = null;
|
var rosReconnectTimer = null;
|
||||||
var rosDisconnectTimer = null;
|
var rosDisconnectTimer = null;
|
||||||
|
var cameraReconnectTimer = null;
|
||||||
|
var cameraRecoveryTimers = [];
|
||||||
|
var lastCameraServiceState = "";
|
||||||
|
var lastCameraSettingsVersion = null;
|
||||||
var publishTimer = null;
|
var publishTimer = null;
|
||||||
var statusTimer = null;
|
var statusTimer = null;
|
||||||
var axes = [0, 0, 0, 0, 0, 0];
|
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) {
|
function updateUndervoltageDisplay(text, state) {
|
||||||
var el = document.getElementById("stat-undervoltage");
|
var el = document.getElementById("stat-undervoltage");
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -952,6 +993,8 @@ function updateUndervoltageDisplay(text, state) {
|
|||||||
function updateServiceState(payload) {
|
function updateServiceState(payload) {
|
||||||
var system = payload.system || {};
|
var system = payload.system || {};
|
||||||
var services = payload.services || {};
|
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-wifi", system.wifi || "unbekannt");
|
||||||
setText("stat-ips", system.ips || "unbekannt");
|
setText("stat-ips", system.ips || "unbekannt");
|
||||||
updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown");
|
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-cpu").style.width = percentTextToWidth(system.cpu_usage);
|
||||||
document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage);
|
document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage);
|
||||||
document.getElementById("bar-temp").style.width = temperatureTextToWidth(system.cpu_temperature);
|
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() {
|
function fetchSystemStatus() {
|
||||||
@@ -1236,8 +1289,10 @@ window.addEventListener("load", function () {
|
|||||||
window.setInterval(renderTrajectoryOverlay, 250);
|
window.setInterval(renderTrajectoryOverlay, 250);
|
||||||
|
|
||||||
var video = document.getElementById("video_output");
|
var video = document.getElementById("video_output");
|
||||||
video.src = "//" + hostUrl + ":8083/stream.mjpg";
|
reloadCameraStream();
|
||||||
video.addEventListener("load", function () {
|
video.addEventListener("load", function () {
|
||||||
|
clearCameraReconnect();
|
||||||
|
clearCameraRecoveryTimers();
|
||||||
setText("cam-state", "Online");
|
setText("cam-state", "Online");
|
||||||
setText("cam-host", hostUrl);
|
setText("cam-host", hostUrl);
|
||||||
setText("cam-live-text", "LIVE");
|
setText("cam-live-text", "LIVE");
|
||||||
@@ -1254,6 +1309,7 @@ window.addEventListener("load", function () {
|
|||||||
if (liveIndicator) {
|
if (liveIndicator) {
|
||||||
liveIndicator.classList.add("is-offline");
|
liveIndicator.classList.add("is-offline");
|
||||||
}
|
}
|
||||||
|
scheduleCameraReconnect(2500);
|
||||||
});
|
});
|
||||||
updateCameraFrameLayout();
|
updateCameraFrameLayout();
|
||||||
window.addEventListener("resize", updateCameraFrameLayout);
|
window.addEventListener("resize", updateCameraFrameLayout);
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ CAMERA_PROFILES = [
|
|||||||
]
|
]
|
||||||
CAMERA_PROFILE_MAP = {profile['id']: profile for profile in CAMERA_PROFILES}
|
CAMERA_PROFILE_MAP = {profile['id']: profile for profile in CAMERA_PROFILES}
|
||||||
DEFAULT_CAMERA_PROFILE_ID = '1296x972'
|
DEFAULT_CAMERA_PROFILE_ID = '1296x972'
|
||||||
|
CAMERA_FPS_OPTIONS = [1, 2, 3, 4, 5, 7, 10, 20]
|
||||||
|
DEFAULT_CAMERA_FPS = 10
|
||||||
|
|
||||||
|
|
||||||
class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
|
class ReusableThreadingTCPServer(socketserver.ThreadingTCPServer):
|
||||||
@@ -292,6 +294,7 @@ def collect_status():
|
|||||||
},
|
},
|
||||||
'services': {
|
'services': {
|
||||||
'camera': get_systemd_state('exomy-camera-stream.service'),
|
'camera': get_systemd_state('exomy-camera-stream.service'),
|
||||||
|
'camera_settings_version': read_camera_settings_version(),
|
||||||
'admin_api': get_systemd_state('exomy-admin-api.service'),
|
'admin_api': get_systemd_state('exomy-admin-api.service'),
|
||||||
'exomy': get_container_state(EXOMY_CONTAINER),
|
'exomy': get_container_state(EXOMY_CONTAINER),
|
||||||
}
|
}
|
||||||
@@ -363,6 +366,16 @@ def normalize_camera_profile(profile_id):
|
|||||||
return dict(profile)
|
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():
|
def read_camera_settings():
|
||||||
default_profile = get_default_camera_profile()
|
default_profile = get_default_camera_profile()
|
||||||
try:
|
try:
|
||||||
@@ -371,37 +384,58 @@ def read_camera_settings():
|
|||||||
except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError, TypeError):
|
except (FileNotFoundError, json.JSONDecodeError, OSError, ValueError, TypeError):
|
||||||
return default_profile
|
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:
|
try:
|
||||||
width = int(payload.get('width', default_profile['width']))
|
width = int(payload.get('width', default_profile['width']))
|
||||||
height = int(payload.get('height', default_profile['height']))
|
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):
|
except (TypeError, ValueError):
|
||||||
return default_profile
|
fps = DEFAULT_CAMERA_FPS
|
||||||
|
|
||||||
for profile in CAMERA_PROFILES:
|
selected_profile_id = str(payload.get('profile', '')).strip()
|
||||||
if profile['width'] == width and profile['height'] == height and profile['fps'] == fps:
|
if selected_profile_id in CAMERA_PROFILE_MAP:
|
||||||
return dict(profile)
|
profile = dict(CAMERA_PROFILE_MAP[selected_profile_id])
|
||||||
return default_profile
|
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)
|
profile = dict(profile)
|
||||||
|
fps = normalize_camera_fps(fps)
|
||||||
os.makedirs(os.path.dirname(CAMERA_SETTINGS_FILE), exist_ok=True)
|
os.makedirs(os.path.dirname(CAMERA_SETTINGS_FILE), exist_ok=True)
|
||||||
payload = {
|
payload = {
|
||||||
'profile': profile['id'],
|
'profile': profile['id'],
|
||||||
'width': profile['width'],
|
'width': profile['width'],
|
||||||
'height': profile['height'],
|
'height': profile['height'],
|
||||||
'fps': profile['fps'],
|
'fps': fps,
|
||||||
|
'updated_at': int(time.time()),
|
||||||
}
|
}
|
||||||
with open(CAMERA_SETTINGS_FILE, 'w', encoding='utf-8') as handle:
|
with open(CAMERA_SETTINGS_FILE, 'w', encoding='utf-8') as handle:
|
||||||
json.dump(payload, handle, indent=2, sort_keys=True)
|
json.dump(payload, handle, indent=2, sort_keys=True)
|
||||||
handle.write('\n')
|
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():
|
def get_camera_config():
|
||||||
profile = read_camera_settings()
|
profile = read_camera_settings()
|
||||||
return {
|
return {
|
||||||
@@ -411,14 +445,23 @@ def get_camera_config():
|
|||||||
'width': profile['width'],
|
'width': profile['width'],
|
||||||
'height': profile['height'],
|
'height': profile['height'],
|
||||||
'fps': profile['fps'],
|
'fps': profile['fps'],
|
||||||
|
'fps_options': list(CAMERA_FPS_OPTIONS),
|
||||||
'profiles': [dict(item) for item in CAMERA_PROFILES],
|
'profiles': [dict(item) for item in CAMERA_PROFILES],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def set_camera_profile(profile_id):
|
def set_camera_config(profile_id=None, fps=None):
|
||||||
profile = normalize_camera_profile(profile_id)
|
current = read_camera_settings()
|
||||||
write_camera_settings(profile)
|
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()
|
restart_camera_service()
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
@@ -1164,14 +1207,15 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.write_json(400, {'status': 'Ungültige Anfrage'})
|
self.write_json(400, {'status': 'Ungültige Anfrage'})
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
profile = set_camera_profile(payload.get('profile'))
|
profile = set_camera_config(payload.get('profile'), payload.get('fps'))
|
||||||
self.write_json(200, {
|
self.write_json(200, {
|
||||||
'status': 'Kameraauflösung gespeichert, Kameradienst startet neu',
|
'status': 'Kameraprofil gespeichert, Kameradienst startet neu',
|
||||||
'camera': {
|
'camera': {
|
||||||
'selected_profile': profile['id'],
|
'selected_profile': profile['id'],
|
||||||
'width': profile['width'],
|
'width': profile['width'],
|
||||||
'height': profile['height'],
|
'height': profile['height'],
|
||||||
'fps': profile['fps'],
|
'fps': profile['fps'],
|
||||||
|
'fps_options': list(CAMERA_FPS_OPTIONS),
|
||||||
'profiles': [dict(item) for item in CAMERA_PROFILES],
|
'profiles': [dict(item) for item in CAMERA_PROFILES],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ LATEST_LOCK = threading.Lock()
|
|||||||
PORT = 8081
|
PORT = 8081
|
||||||
CONFIG_FILE = os.path.join(os.path.dirname(__file__), '..', 'config', 'camera_settings.json')
|
CONFIG_FILE = os.path.join(os.path.dirname(__file__), '..', 'config', 'camera_settings.json')
|
||||||
DEFAULT_CAMERA_SETTINGS = {
|
DEFAULT_CAMERA_SETTINGS = {
|
||||||
'width': 1280,
|
'width': 1296,
|
||||||
'height': 720,
|
'height': 972,
|
||||||
'fps': 10,
|
'fps': 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import os
|
|||||||
import socketserver
|
import socketserver
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
DELAY_FILE = '/tmp/exomy_delay.txt'
|
DELAY_FILE = '/tmp/exomy_delay.txt'
|
||||||
@@ -93,8 +94,11 @@ class ReusableTCPServer(socketserver.ThreadingTCPServer):
|
|||||||
|
|
||||||
|
|
||||||
class Handler(http.server.BaseHTTPRequestHandler):
|
class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def normalized_path(self):
|
||||||
|
return urllib.parse.urlsplit(self.path).path
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path != '/stream.mjpg':
|
if self.normalized_path() != '/stream.mjpg':
|
||||||
self.send_error(404)
|
self.send_error(404)
|
||||||
return
|
return
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
|
|||||||
+59
-3
@@ -57,7 +57,7 @@
|
|||||||
<div class="panel" id="cam-panel">
|
<div class="panel" id="cam-panel">
|
||||||
<div class="p-hdr">
|
<div class="p-hdr">
|
||||||
<div class="p-dot"></div>
|
<div class="p-dot"></div>
|
||||||
<span class="p-lbl">Kamera — CAM01</span>
|
<span class="p-lbl">Kamera - CAM01</span>
|
||||||
<span class="p-tag">Live Stream</span>
|
<span class="p-tag">Live Stream</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cam-port">
|
<div class="cam-port">
|
||||||
@@ -288,6 +288,10 @@ var gpsStatusListener = null;
|
|||||||
var gpsDiagnosticsListener = null;
|
var gpsDiagnosticsListener = null;
|
||||||
var rosReconnectTimer = null;
|
var rosReconnectTimer = null;
|
||||||
var rosDisconnectTimer = null;
|
var rosDisconnectTimer = null;
|
||||||
|
var cameraReconnectTimer = null;
|
||||||
|
var cameraRecoveryTimers = [];
|
||||||
|
var lastCameraServiceState = "";
|
||||||
|
var lastCameraSettingsVersion = null;
|
||||||
var publishTimer = null;
|
var publishTimer = null;
|
||||||
var statusTimer = null;
|
var statusTimer = null;
|
||||||
var axes = [0, 0, 0, 0, 0, 0];
|
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) {
|
function updateUndervoltageDisplay(text, state) {
|
||||||
var el = document.getElementById("stat-undervoltage");
|
var el = document.getElementById("stat-undervoltage");
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -952,6 +993,8 @@ function updateUndervoltageDisplay(text, state) {
|
|||||||
function updateServiceState(payload) {
|
function updateServiceState(payload) {
|
||||||
var system = payload.system || {};
|
var system = payload.system || {};
|
||||||
var services = payload.services || {};
|
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-wifi", system.wifi || "unbekannt");
|
||||||
setText("stat-ips", system.ips || "unbekannt");
|
setText("stat-ips", system.ips || "unbekannt");
|
||||||
updateUndervoltageDisplay(system.undervoltage || "unbekannt", system.undervoltage_state || "unknown");
|
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-cpu").style.width = percentTextToWidth(system.cpu_usage);
|
||||||
document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage);
|
document.getElementById("bar-mem").style.width = percentTextToWidth(system.memory_usage);
|
||||||
document.getElementById("bar-temp").style.width = temperatureTextToWidth(system.cpu_temperature);
|
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() {
|
function fetchSystemStatus() {
|
||||||
@@ -1236,8 +1289,10 @@ window.addEventListener("load", function () {
|
|||||||
window.setInterval(renderTrajectoryOverlay, 250);
|
window.setInterval(renderTrajectoryOverlay, 250);
|
||||||
|
|
||||||
var video = document.getElementById("video_output");
|
var video = document.getElementById("video_output");
|
||||||
video.src = "//" + hostUrl + ":8083/stream.mjpg";
|
reloadCameraStream();
|
||||||
video.addEventListener("load", function () {
|
video.addEventListener("load", function () {
|
||||||
|
clearCameraReconnect();
|
||||||
|
clearCameraRecoveryTimers();
|
||||||
setText("cam-state", "Online");
|
setText("cam-state", "Online");
|
||||||
setText("cam-host", hostUrl);
|
setText("cam-host", hostUrl);
|
||||||
setText("cam-live-text", "LIVE");
|
setText("cam-live-text", "LIVE");
|
||||||
@@ -1254,6 +1309,7 @@ window.addEventListener("load", function () {
|
|||||||
if (liveIndicator) {
|
if (liveIndicator) {
|
||||||
liveIndicator.classList.add("is-offline");
|
liveIndicator.classList.add("is-offline");
|
||||||
}
|
}
|
||||||
|
scheduleCameraReconnect(2500);
|
||||||
});
|
});
|
||||||
updateCameraFrameLayout();
|
updateCameraFrameLayout();
|
||||||
window.addEventListener("resize", updateCameraFrameLayout);
|
window.addEventListener("resize", updateCameraFrameLayout);
|
||||||
|
|||||||
Reference in New Issue
Block a user