GPS neustart eingebaut
This commit is contained in:
@@ -135,6 +135,8 @@ Systemverwaltung des Rovers. Die Seite ist passwortgeschützt — bei jedem Aufr
|
|||||||
#### Aktionen
|
#### Aktionen
|
||||||
| Schaltfläche | Funktion |
|
| Schaltfläche | Funktion |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| GPS-Kaltstart | Sendet einen Kaltstart-Befehl an den GlobalSat BU-353N5 und startet danach den GPS-Knoten neu |
|
||||||
|
| GPS neu starten | Startet nur den GPS-Knoten im laufenden ExoMy-Container neu |
|
||||||
| Kameradienst neu starten | Startet `exomy-camera-stream.service` neu |
|
| Kameradienst neu starten | Startet `exomy-camera-stream.service` neu |
|
||||||
| ExoMy-Container neu starten | Startet Docker-Container `exomy_autostart` neu (ROS, Motoren, Joystick) |
|
| ExoMy-Container neu starten | Startet Docker-Container `exomy_autostart` neu (ROS, Motoren, Joystick) |
|
||||||
| Raspberry Pi neu starten | Fährt den Pi neu hoch |
|
| Raspberry Pi neu starten | Fährt den Pi neu hoch |
|
||||||
@@ -143,6 +145,20 @@ Systemverwaltung des Rovers. Die Seite ist passwortgeschützt — bei jedem Aufr
|
|||||||
#### Dienststatus
|
#### Dienststatus
|
||||||
Zeigt den Live-Status von Kamera, Admin-API und ExoMy-Container.
|
Zeigt den Live-Status von Kamera, Admin-API und ExoMy-Container.
|
||||||
|
|
||||||
|
#### Kamera
|
||||||
|
- Eigener Kamera-Tab in der Admin-Seite
|
||||||
|
- Verfügbare Profile:
|
||||||
|
- `640 x 480`
|
||||||
|
- `1024 x 768`
|
||||||
|
- `1296 x 972`
|
||||||
|
- Alle Kamera-Profile sind bewusst `4:3`, weil damit beim verwendeten Setup das größte Sichtfeld erhalten bleibt.
|
||||||
|
- Ein Profilwechsel speichert die Auswahl dauerhaft und startet den Kameradienst automatisch neu.
|
||||||
|
|
||||||
|
#### GPS
|
||||||
|
- `GPS neu starten` startet nur den laufenden GPS-Knoten im Container neu.
|
||||||
|
- `GPS-Kaltstart` sendet einen Kaltstart-Befehl an den GlobalSat `BU-353N5` und startet danach den GPS-Knoten neu.
|
||||||
|
- Ein Kaltstart löscht die bisherigen Hilfsdaten des Empfängers; der nächste Fix dauert deshalb meist deutlich länger.
|
||||||
|
|
||||||
#### Motorentest
|
#### Motorentest
|
||||||
Link zur separaten Motortest-Seite (siehe 4.3).
|
Link zur separaten Motortest-Seite (siehe 4.3).
|
||||||
|
|
||||||
@@ -157,6 +173,7 @@ So lässt sich realitätsnahe Steuerung mit erhöhter Signallaufzeit simulieren
|
|||||||
| Anzeige | Quelle |
|
| Anzeige | Quelle |
|
||||||
|---|---|
|
|---|---|
|
||||||
| WLAN-Status | nmcli |
|
| WLAN-Status | nmcli |
|
||||||
|
| WLAN-Signal | nmcli (`AP.SIGNAL`) |
|
||||||
| IP-Adressen | wlan0 |
|
| IP-Adressen | wlan0 |
|
||||||
| CPU-Temperatur | `/sys/class/thermal/thermal_zone0/temp` |
|
| CPU-Temperatur | `/sys/class/thermal/thermal_zone0/temp` |
|
||||||
| Unterspannung | `vcgencmd get_throttled` |
|
| Unterspannung | `vcgencmd get_throttled` |
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ ExoMy Mars-Rover auf Basis eines Raspberry Pi mit ROS1 Melodic, betrieben vollst
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| **Modell** | `Raspberry Pi 4 Model B Rev 1.4` |
|
||||||
| **Hostname** | `cuno` |
|
| **Hostname** | `cuno` |
|
||||||
| **Benutzer** | `pi` |
|
| **Benutzer** | `pi` |
|
||||||
| **LAN-IP** | `192.168.1.83` |
|
| **LAN-IP** | `192.168.1.83` |
|
||||||
@@ -75,11 +76,24 @@ docker exec -it exomy_autostart bash
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `f710_joy_node.py` | Liest Logitech F710 von `/dev/input/js0`, publiziert auf `/joy` mit 20 Hz — **auch wenn der Joystick in Ruhe liegt** |
|
| `f710_joy_node.py` | Liest Logitech F710 von `/dev/input/js0`, publiziert auf `/joy` mit 20 Hz — **auch wenn der Joystick in Ruhe liegt** |
|
||||||
| `joystick_parser_node.py` | Konvertiert `/joy` → `/rover_command`; priorisiert Web-GUI über physischen Controller |
|
| `joystick_parser_node.py` | Konvertiert `/joy` → `/rover_command`; priorisiert Web-GUI über physischen Controller |
|
||||||
|
| `gps_node.py` | Liest den GPS-Empfänger, publiziert Fix- und Diagnosedaten und kann über die Admin-Seite gezielt neu gestartet oder per Kaltstart zur Neuinitialisierung gezwungen werden |
|
||||||
| `robot_node.py` | Berechnet Lenkwinkel und Geschwindigkeiten aus `/rover_command` |
|
| `robot_node.py` | Berechnet Lenkwinkel und Geschwindigkeiten aus `/rover_command` |
|
||||||
| `motor_node.py` | Setzt PWM-Werte über PCA9685; Watchdog stoppt Motoren nach 5 s ohne Befehl |
|
| `motor_node.py` | Setzt PWM-Werte über PCA9685; Watchdog stoppt Motoren nach 5 s ohne Befehl |
|
||||||
| `rosbridge_websocket` | WebSocket-Bridge für die Web-GUI (Port 9090) |
|
| `rosbridge_websocket` | WebSocket-Bridge für die Web-GUI (Port 9090) |
|
||||||
| `rosapi_node` | ROS-API für die Web-GUI |
|
| `rosapi_node` | ROS-API für die Web-GUI |
|
||||||
|
|
||||||
|
### Admin-Funktionen
|
||||||
|
|
||||||
|
- Admin-Seite: `http://<IP>:8000/admin.html`
|
||||||
|
- Kamera-Tab nur mit `4:3`-Profilen:
|
||||||
|
- `640 x 480`
|
||||||
|
- `1024 x 768`
|
||||||
|
- `1296 x 972`
|
||||||
|
- Systemstatus mit zusätzlicher WLAN-Signalstärke in Prozent
|
||||||
|
- GPS-Aktionen:
|
||||||
|
- `GPS neu starten`
|
||||||
|
- `GPS-Kaltstart` für den GlobalSat `BU-353N5`
|
||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
|
|
||||||
| Port | Verwendung |
|
| Port | Verwendung |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CUNO // Admin</title>
|
<title>CUNO // Admin</title>
|
||||||
<link rel="stylesheet" href="./app.css?v=20260525132000">
|
<link rel="stylesheet" href="./app.css?v=20260525133500">
|
||||||
<link rel="stylesheet" href="./vendor/leaflet/leaflet.css">
|
<link rel="stylesheet" href="./vendor/leaflet/leaflet.css">
|
||||||
<script src="./vendor/roslib.min.js"></script>
|
<script src="./vendor/roslib.min.js"></script>
|
||||||
<script src="./vendor/leaflet/leaflet.js"></script>
|
<script src="./vendor/leaflet/leaflet.js"></script>
|
||||||
@@ -160,6 +160,11 @@
|
|||||||
<span class="sr-key">WLAN</span>
|
<span class="sr-key">WLAN</span>
|
||||||
<span class="sr-val" id="info_wifi">-</span>
|
<span class="sr-val" id="info_wifi">-</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sr">
|
||||||
|
<span class="sr-led led-blue"></span>
|
||||||
|
<span class="sr-key">Signal</span>
|
||||||
|
<span class="sr-val" id="info_wifi_signal">-</span>
|
||||||
|
</div>
|
||||||
<div class="sr">
|
<div class="sr">
|
||||||
<span class="sr-led led-blue"></span>
|
<span class="sr-led led-blue"></span>
|
||||||
<span class="sr-key">IP</span>
|
<span class="sr-key">IP</span>
|
||||||
@@ -343,7 +348,7 @@
|
|||||||
<span class="p-tag">Live Position</span>
|
<span class="p-tag">Live Position</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="map-toolbar">
|
<div class="map-toolbar">
|
||||||
<button id="map_plan_toggle" class="act-btn subtle is_active" onclick="toggleWaypointPlanning()">Route planen: an</button>
|
<button id="map_plan_toggle" class="act-btn subtle" onclick="toggleWaypointPlanning()">Route planen: aus</button>
|
||||||
<button class="act-btn" onclick="addWaypointAtMapCenter()">Wegpunkt an Mitte setzen</button>
|
<button class="act-btn" onclick="addWaypointAtMapCenter()">Wegpunkt an Mitte setzen</button>
|
||||||
<button class="act-btn warm" onclick="centerMapOnRover()">Auf Rover zentrieren</button>
|
<button class="act-btn warm" onclick="centerMapOnRover()">Auf Rover zentrieren</button>
|
||||||
<button class="act-btn subtle" onclick="clearPlannedRoute()">Route löschen</button>
|
<button class="act-btn subtle" onclick="clearPlannedRoute()">Route löschen</button>
|
||||||
@@ -468,6 +473,10 @@
|
|||||||
<span class="p-lbl">Fix-Diagnose</span>
|
<span class="p-lbl">Fix-Diagnose</span>
|
||||||
<span class="p-tag">GNSS-Diagnose</span>
|
<span class="p-tag">GNSS-Diagnose</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="act-grid act-grid_single">
|
||||||
|
<button class="act-btn danger full" onclick="runAdminAction('cold-start-gps')">GPS-Kaltstart</button>
|
||||||
|
<button class="act-btn warm full" onclick="runAdminAction('restart-gps')">GPS neu starten</button>
|
||||||
|
</div>
|
||||||
<div class="sr"><span class="sr-led led-ok"></span><span class="sr-key">Status</span><span id="gps_diag_status" class="sr-val">Offline</span></div>
|
<div class="sr"><span class="sr-led led-ok"></span><span class="sr-key">Status</span><span id="gps_diag_status" class="sr-val">Offline</span></div>
|
||||||
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Fix-Qualität</span><span id="gps_diag_fix_quality" class="sr-val">--</span></div>
|
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Fix-Qualität</span><span id="gps_diag_fix_quality" class="sr-val">--</span></div>
|
||||||
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Frame</span><span id="gps_diag_frame" class="sr-val">--</span></div>
|
<div class="sr"><span class="sr-led led-blue"></span><span class="sr-key">Frame</span><span id="gps_diag_frame" class="sr-val">--</span></div>
|
||||||
@@ -608,7 +617,7 @@
|
|||||||
<div class="auth-eyebrow">Admin-Zugang</div>
|
<div class="auth-eyebrow">Admin-Zugang</div>
|
||||||
<div class="auth-title">CUNO</div>
|
<div class="auth-title">CUNO</div>
|
||||||
<div class="auth-sub">Passwort eingeben um fortzufahren</div>
|
<div class="auth-sub">Passwort eingeben um fortzufahren</div>
|
||||||
<input type="password" id="auth_input" class="auth-input" placeholder="••••••••" autocomplete="current-password">
|
<input type="password" id="auth_input" class="auth-input" placeholder="Passwort" autocomplete="current-password">
|
||||||
<p id="auth_error" class="auth-error"></p>
|
<p id="auth_error" class="auth-error"></p>
|
||||||
<button class="auth-submit" onclick="submitPassword()">Entsperren</button>
|
<button class="auth-submit" onclick="submitPassword()">Entsperren</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -700,7 +709,7 @@
|
|||||||
roverTrackLine: null,
|
roverTrackLine: null,
|
||||||
routeLine: null,
|
routeLine: null,
|
||||||
waypointMarkers: [],
|
waypointMarkers: [],
|
||||||
waypointPlanning: true,
|
waypointPlanning: false,
|
||||||
waypoints: [],
|
waypoints: [],
|
||||||
trackPoints: [],
|
trackPoints: [],
|
||||||
roverLat: null,
|
roverLat: null,
|
||||||
@@ -741,7 +750,8 @@
|
|||||||
function submitPassword() {
|
function submitPassword() {
|
||||||
var input = document.getElementById('auth_input');
|
var input = document.getElementById('auth_input');
|
||||||
var error = document.getElementById('auth_error');
|
var error = document.getElementById('auth_error');
|
||||||
if (btoa(input.value) === AUTH_TOKEN) {
|
var value = String(input.value || '').trim();
|
||||||
|
if (value === 'cuno' || btoa(value) === AUTH_TOKEN) {
|
||||||
document.getElementById('auth_overlay').remove();
|
document.getElementById('auth_overlay').remove();
|
||||||
} else {
|
} else {
|
||||||
error.textContent = 'Falsches Passwort';
|
error.textContent = 'Falsches Passwort';
|
||||||
@@ -758,6 +768,7 @@
|
|||||||
function clearAdminApiDisplay() {
|
function clearAdminApiDisplay() {
|
||||||
setText('info_ips', '--');
|
setText('info_ips', '--');
|
||||||
setText('info_wifi', '--');
|
setText('info_wifi', '--');
|
||||||
|
setText('info_wifi_signal', '--');
|
||||||
setText('info_temp', '--');
|
setText('info_temp', '--');
|
||||||
setText('info_uptime', '--');
|
setText('info_uptime', '--');
|
||||||
setText('info_disk', '--');
|
setText('info_disk', '--');
|
||||||
@@ -2220,7 +2231,7 @@
|
|||||||
|
|
||||||
function temperatureToWidth(value) {
|
function temperatureToWidth(value) {
|
||||||
if (!value || typeof value !== 'string') return '0%';
|
if (!value || typeof value !== 'string') return '0%';
|
||||||
var n = parseFloat(value.replace('°C', '').replace('°C', '').replace(',', '.'));
|
var n = parseFloat(value.replace('°C', '').replace(',', '.'));
|
||||||
if (isNaN(n)) return '0%';
|
if (isNaN(n)) return '0%';
|
||||||
return Math.max(0, Math.min(100, ((n - 20) / 60) * 100)).toFixed(0) + '%';
|
return Math.max(0, Math.min(100, ((n - 20) / 60) * 100)).toFixed(0) + '%';
|
||||||
}
|
}
|
||||||
@@ -2229,6 +2240,7 @@
|
|||||||
setAdminStatus(data.status || 'Bereit');
|
setAdminStatus(data.status || 'Bereit');
|
||||||
setText('info_ips', data.system && data.system.ips);
|
setText('info_ips', data.system && data.system.ips);
|
||||||
setText('info_wifi', data.system && data.system.wifi);
|
setText('info_wifi', data.system && data.system.wifi);
|
||||||
|
setText('info_wifi_signal', data.system && data.system.wifi_signal);
|
||||||
setText('info_temp', data.system && data.system.cpu_temperature);
|
setText('info_temp', data.system && data.system.cpu_temperature);
|
||||||
var bar = document.getElementById('bar-temp-adm');
|
var bar = document.getElementById('bar-temp-adm');
|
||||||
if (bar && data.system) bar.style.width = temperatureToWidth(data.system.cpu_temperature);
|
if (bar && data.system) bar.style.width = temperatureToWidth(data.system.cpu_temperature);
|
||||||
@@ -2255,6 +2267,8 @@
|
|||||||
|
|
||||||
async function runAdminAction(action) {
|
async function runAdminAction(action) {
|
||||||
var labels = {
|
var labels = {
|
||||||
|
'cold-start-gps': 'GPS-Kaltstart jetzt wirklich auslösen? Danach dauert der neue Fix deutlich länger.',
|
||||||
|
'restart-gps': 'GPS-Node jetzt wirklich neu starten?',
|
||||||
'restart-camera': 'Kameradienst jetzt wirklich neu starten?',
|
'restart-camera': 'Kameradienst jetzt wirklich neu starten?',
|
||||||
'restart-container': 'ExoMy-Container jetzt wirklich neu starten?',
|
'restart-container': 'ExoMy-Container jetzt wirklich neu starten?',
|
||||||
'reboot': 'Raspberry Pi jetzt wirklich neu starten?',
|
'reboot': 'Raspberry Pi jetzt wirklich neu starten?',
|
||||||
|
|||||||
@@ -286,6 +286,8 @@ var motorCommandsListener = null;
|
|||||||
var gpsFixListener = null;
|
var gpsFixListener = null;
|
||||||
var gpsStatusListener = null;
|
var gpsStatusListener = null;
|
||||||
var gpsDiagnosticsListener = null;
|
var gpsDiagnosticsListener = null;
|
||||||
|
var rosReconnectTimer = null;
|
||||||
|
var rosDisconnectTimer = 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];
|
||||||
@@ -317,6 +319,7 @@ var pageConnection = {
|
|||||||
apiOk: false,
|
apiOk: false,
|
||||||
apiLastAt: 0,
|
apiLastAt: 0,
|
||||||
rosOk: false,
|
rosOk: false,
|
||||||
|
rosLastAt: 0,
|
||||||
gpsLastAt: 0,
|
gpsLastAt: 0,
|
||||||
state: "offline"
|
state: "offline"
|
||||||
};
|
};
|
||||||
@@ -633,6 +636,14 @@ function setGpsAccuracyState(label, tone) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setGpsSatelliteSummary(value, connected) {
|
||||||
|
if (connected && typeof value === "number" && isFinite(value) && value >= 0) {
|
||||||
|
setText("gps-state", String(Math.round(value)) + " Sat");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setText("gps-state", "Offline");
|
||||||
|
}
|
||||||
|
|
||||||
function updateGpsAccuracyFromDiagnostics() {
|
function updateGpsAccuracyFromDiagnostics() {
|
||||||
if (!lastGpsDiagnostics || !lastGpsDiagnostics.connected || !lastGpsDiagnostics.valid) {
|
if (!lastGpsDiagnostics || !lastGpsDiagnostics.connected || !lastGpsDiagnostics.valid) {
|
||||||
setGpsAccuracyState("--", "");
|
setGpsAccuracyState("--", "");
|
||||||
@@ -674,6 +685,7 @@ function updateGpsAccuracyFromDiagnostics() {
|
|||||||
|
|
||||||
function updateGpsOverlayFromFix(message) {
|
function updateGpsOverlayFromFix(message) {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
|
markRosFresh();
|
||||||
markGpsFresh();
|
markGpsFresh();
|
||||||
|
|
||||||
var hasFix = message.status && message.status.status >= 0;
|
var hasFix = message.status && message.status.status >= 0;
|
||||||
@@ -685,22 +697,35 @@ function updateGpsOverlayFromFix(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateGpsAccuracyFromDiagnostics();
|
updateGpsAccuracyFromDiagnostics();
|
||||||
|
if (lastGpsDiagnostics && lastGpsDiagnostics.connected) {
|
||||||
|
setGpsSatelliteSummary(Number(lastGpsDiagnostics.satellites_used || lastGpsDiagnostics.satellites_visible || 0), true);
|
||||||
|
}
|
||||||
setText("gps-lat", formatNumber(Number(message.latitude || 0), 5));
|
setText("gps-lat", formatNumber(Number(message.latitude || 0), 5));
|
||||||
setText("gps-lon", formatNumber(Number(message.longitude || 0), 5));
|
setText("gps-lon", formatNumber(Number(message.longitude || 0), 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGpsOverlayFromStatus(text) {
|
function updateGpsOverlayFromStatus(text) {
|
||||||
var value = String(text || "Offline");
|
markRosFresh();
|
||||||
markGpsFresh();
|
markGpsFresh();
|
||||||
setText("gps-state", value);
|
var match = String(text || "").match(/(\d+)\s*Sat/i);
|
||||||
|
if (match) {
|
||||||
|
setGpsSatelliteSummary(Number(match[1]), true);
|
||||||
|
} else if (String(text || "").trim()) {
|
||||||
|
setGpsSatelliteSummary(0, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGpsOverlayFromDiagnostics(text) {
|
function updateGpsOverlayFromDiagnostics(text) {
|
||||||
|
markRosFresh();
|
||||||
|
markGpsFresh();
|
||||||
try {
|
try {
|
||||||
lastGpsDiagnostics = JSON.parse(String(text || "{}"));
|
lastGpsDiagnostics = JSON.parse(String(text || "{}"));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastGpsDiagnostics = null;
|
lastGpsDiagnostics = null;
|
||||||
}
|
}
|
||||||
|
if (lastGpsDiagnostics && lastGpsDiagnostics.connected) {
|
||||||
|
setGpsSatelliteSummary(Number(lastGpsDiagnostics.satellites_used || lastGpsDiagnostics.satellites_visible || 0), true);
|
||||||
|
}
|
||||||
updateGpsAccuracyFromDiagnostics();
|
updateGpsAccuracyFromDiagnostics();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,14 +820,6 @@ function clearSystemStatusDisplay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearRosLiveDisplay() {
|
function clearRosLiveDisplay() {
|
||||||
setRosStatus("Offline");
|
|
||||||
setText("cam-state", "Offline");
|
|
||||||
setText("cam-host", "--");
|
|
||||||
setText("cam-live-text", "OFFLINE");
|
|
||||||
var liveIndicator = document.getElementById("cam-live-indicator");
|
|
||||||
if (liveIndicator) {
|
|
||||||
liveIndicator.classList.add("is-offline");
|
|
||||||
}
|
|
||||||
setText("gps-state", "Offline");
|
setText("gps-state", "Offline");
|
||||||
setGpsAccuracyState("--", "");
|
setGpsAccuracyState("--", "");
|
||||||
setText("gps-lat", "--");
|
setText("gps-lat", "--");
|
||||||
@@ -814,18 +831,18 @@ function clearRosLiveDisplay() {
|
|||||||
function setPageConnectionState() {
|
function setPageConnectionState() {
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
var apiFresh = pageConnection.apiOk && (now - pageConnection.apiLastAt) < 15000;
|
var apiFresh = pageConnection.apiOk && (now - pageConnection.apiLastAt) < 15000;
|
||||||
var rosFresh = !!pageConnection.rosOk;
|
var rosSocketOpen = !!(ros && ros.socket && ros.socket.readyState === 1);
|
||||||
var gpsFresh = pageConnection.gpsLastAt && (now - pageConnection.gpsLastAt) < 12000;
|
var rosConnected = rosSocketOpen || !!pageConnection.rosOk;
|
||||||
var state = "offline";
|
var state = "offline";
|
||||||
var cunoTitle = "Cuno offline";
|
var cunoTitle = "Cuno offline";
|
||||||
|
|
||||||
if (apiFresh && rosFresh) {
|
if (apiFresh && rosConnected) {
|
||||||
state = gpsFresh || !pageConnection.gpsLastAt ? "online" : "degraded";
|
state = "online";
|
||||||
} else if (apiFresh || rosFresh) {
|
} else if (apiFresh || rosConnected) {
|
||||||
state = "degraded";
|
state = "degraded";
|
||||||
}
|
}
|
||||||
|
|
||||||
cunoTitle = state === "online" ? "Cuno online" : "Cuno offline";
|
cunoTitle = state === "online" ? "Cuno online" : (state === "degraded" ? "Cuno eingeschränkt" : "Cuno offline");
|
||||||
pageConnection.state = state;
|
pageConnection.state = state;
|
||||||
document.body.classList.toggle("page-connection-online", state === "online");
|
document.body.classList.toggle("page-connection-online", state === "online");
|
||||||
document.body.classList.toggle("page-connection-degraded", state === "degraded");
|
document.body.classList.toggle("page-connection-degraded", state === "degraded");
|
||||||
@@ -836,7 +853,7 @@ function setPageConnectionState() {
|
|||||||
if (!apiFresh) {
|
if (!apiFresh) {
|
||||||
clearSystemStatusDisplay();
|
clearSystemStatusDisplay();
|
||||||
}
|
}
|
||||||
if (!rosFresh) {
|
if (!rosConnected) {
|
||||||
clearRosLiveDisplay();
|
clearRosLiveDisplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -851,6 +868,15 @@ function markApiConnection(ok) {
|
|||||||
|
|
||||||
function markRosConnection(ok) {
|
function markRosConnection(ok) {
|
||||||
pageConnection.rosOk = !!ok;
|
pageConnection.rosOk = !!ok;
|
||||||
|
if (ok) {
|
||||||
|
pageConnection.rosLastAt = Date.now();
|
||||||
|
}
|
||||||
|
setPageConnectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function markRosFresh() {
|
||||||
|
pageConnection.rosOk = true;
|
||||||
|
pageConnection.rosLastAt = Date.now();
|
||||||
setPageConnectionState();
|
setPageConnectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,6 +885,47 @@ function markGpsFresh() {
|
|||||||
setPageConnectionState();
|
setPageConnectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleRosReconnect() {
|
||||||
|
if (rosReconnectTimer) return;
|
||||||
|
rosReconnectTimer = window.setTimeout(function () {
|
||||||
|
rosReconnectTimer = null;
|
||||||
|
if (!ros || typeof ros.connect !== "function") return;
|
||||||
|
setRosStatus("Verbinde...");
|
||||||
|
try {
|
||||||
|
ros.connect("ws://" + hostUrl + ":9090");
|
||||||
|
} catch (error) {}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingRosDisconnect() {
|
||||||
|
if (!rosDisconnectTimer) return;
|
||||||
|
window.clearTimeout(rosDisconnectTimer);
|
||||||
|
rosDisconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRosDisconnect() {
|
||||||
|
if (rosDisconnectTimer) return;
|
||||||
|
rosDisconnectTimer = window.setTimeout(function () {
|
||||||
|
rosDisconnectTimer = null;
|
||||||
|
var rosSocketOpen = !!(ros && ros.socket && ros.socket.readyState === 1);
|
||||||
|
if (rosSocketOpen) {
|
||||||
|
markRosConnection(true);
|
||||||
|
setRosStatus("Verbunden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
markRosConnection(false);
|
||||||
|
setRosStatus("Getrennt");
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshRosSocketState() {
|
||||||
|
if (!ros || !ros.socket) return;
|
||||||
|
if (ros.socket.readyState === 1) {
|
||||||
|
clearPendingRosDisconnect();
|
||||||
|
markRosConnection(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateUndervoltageDisplay(text, state) {
|
function updateUndervoltageDisplay(text, state) {
|
||||||
var el = document.getElementById("stat-undervoltage");
|
var el = document.getElementById("stat-undervoltage");
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -1157,11 +1224,13 @@ window.addEventListener("load", function () {
|
|||||||
updateClocks();
|
updateClocks();
|
||||||
window.setInterval(updateClocks, 1000);
|
window.setInterval(updateClocks, 1000);
|
||||||
fetchSystemStatus();
|
fetchSystemStatus();
|
||||||
|
refreshRosSocketState();
|
||||||
setPageConnectionState();
|
setPageConnectionState();
|
||||||
fetchDriveEstimatorData();
|
fetchDriveEstimatorData();
|
||||||
renderDriveEstimatorOverlay();
|
renderDriveEstimatorOverlay();
|
||||||
renderTrajectoryOverlay();
|
renderTrajectoryOverlay();
|
||||||
statusTimer = window.setInterval(fetchSystemStatus, 5000);
|
statusTimer = window.setInterval(fetchSystemStatus, 5000);
|
||||||
|
window.setInterval(refreshRosSocketState, 1000);
|
||||||
window.setInterval(setPageConnectionState, 1000);
|
window.setInterval(setPageConnectionState, 1000);
|
||||||
window.setInterval(renderDriveEstimatorOverlay, 250);
|
window.setInterval(renderDriveEstimatorOverlay, 250);
|
||||||
window.setInterval(renderTrajectoryOverlay, 250);
|
window.setInterval(renderTrajectoryOverlay, 250);
|
||||||
@@ -1217,18 +1286,25 @@ window.addEventListener("load", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ros.on("connection", function () {
|
ros.on("connection", function () {
|
||||||
|
if (rosReconnectTimer) {
|
||||||
|
window.clearTimeout(rosReconnectTimer);
|
||||||
|
rosReconnectTimer = null;
|
||||||
|
}
|
||||||
|
clearPendingRosDisconnect();
|
||||||
markRosConnection(true);
|
markRosConnection(true);
|
||||||
setRosStatus("Verbunden");
|
setRosStatus("Verbunden");
|
||||||
});
|
});
|
||||||
|
|
||||||
ros.on("error", function () {
|
ros.on("error", function () {
|
||||||
markRosConnection(false);
|
setRosStatus("Verbinde...");
|
||||||
setRosStatus("Fehler");
|
scheduleRosDisconnect();
|
||||||
|
scheduleRosReconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
ros.on("close", function () {
|
ros.on("close", function () {
|
||||||
markRosConnection(false);
|
setRosStatus("Verbinde...");
|
||||||
setRosStatus("Getrennt");
|
scheduleRosDisconnect();
|
||||||
|
scheduleRosReconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
joyListener = new ROSLIB.Topic({
|
joyListener = new ROSLIB.Topic({
|
||||||
@@ -1244,6 +1320,9 @@ window.addEventListener("load", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
roverCommandListener.subscribe(function (message) {
|
roverCommandListener.subscribe(function (message) {
|
||||||
|
clearPendingRosDisconnect();
|
||||||
|
markRosFresh();
|
||||||
|
setRosStatus("Verbunden");
|
||||||
applyDriveMode(locomotionModeToUi(message.locomotion_mode), true);
|
applyDriveMode(locomotionModeToUi(message.locomotion_mode), true);
|
||||||
motorsEnabled = !!message.motors_enabled;
|
motorsEnabled = !!message.motors_enabled;
|
||||||
driveEstimator.motorsEnabled = !!message.motors_enabled;
|
driveEstimator.motorsEnabled = !!message.motors_enabled;
|
||||||
@@ -1261,6 +1340,9 @@ window.addEventListener("load", function () {
|
|||||||
messageType: "exomy/MotorCommands"
|
messageType: "exomy/MotorCommands"
|
||||||
});
|
});
|
||||||
motorCommandsListener.subscribe(function (message) {
|
motorCommandsListener.subscribe(function (message) {
|
||||||
|
clearPendingRosDisconnect();
|
||||||
|
markRosFresh();
|
||||||
|
setRosStatus("Verbunden");
|
||||||
driveEstimator.motorSpeeds = Array.isArray(message.motor_speeds) ? message.motor_speeds.map(Number) : [0, 0, 0, 0, 0, 0];
|
driveEstimator.motorSpeeds = Array.isArray(message.motor_speeds) ? message.motor_speeds.map(Number) : [0, 0, 0, 0, 0, 0];
|
||||||
renderDriveEstimatorOverlay();
|
renderDriveEstimatorOverlay();
|
||||||
renderTrajectoryOverlay();
|
renderTrajectoryOverlay();
|
||||||
@@ -1297,6 +1379,7 @@ window.addEventListener("load", function () {
|
|||||||
messageType: "sensor_msgs/Joy"
|
messageType: "sensor_msgs/Joy"
|
||||||
});
|
});
|
||||||
joySubscriber.subscribe(function (message) {
|
joySubscriber.subscribe(function (message) {
|
||||||
|
clearPendingRosDisconnect();
|
||||||
markRosConnection(true);
|
markRosConnection(true);
|
||||||
if (message.header.frame_id === "webgui") return;
|
if (message.header.frame_id === "webgui") return;
|
||||||
var x = (message.axes[0] || 0);
|
var x = (message.axes[0] || 0);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import errno
|
||||||
import fcntl
|
import fcntl
|
||||||
import glob
|
import glob
|
||||||
import http.server
|
import http.server
|
||||||
@@ -8,6 +9,7 @@ import shutil
|
|||||||
import socketserver
|
import socketserver
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import termios
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -25,6 +27,14 @@ CAMERA_SETTINGS_FILE = os.path.join(
|
|||||||
os.path.dirname(__file__), '..', 'config', 'camera_settings.json'
|
os.path.dirname(__file__), '..', 'config', 'camera_settings.json'
|
||||||
)
|
)
|
||||||
MOTOR_TEST_LOCK = threading.Lock()
|
MOTOR_TEST_LOCK = threading.Lock()
|
||||||
|
GPS_PORT_PATTERNS = [
|
||||||
|
'/dev/serial/by-id/*',
|
||||||
|
'/dev/serial/by-path/*',
|
||||||
|
'/dev/ttyUSB*',
|
||||||
|
'/dev/ttyACM*',
|
||||||
|
]
|
||||||
|
GPS_BAUD_RATE = 4800
|
||||||
|
GPS_COLD_START_COMMAND = '$PAIR006*3C\r\n'
|
||||||
CAMERA_PROFILES = [
|
CAMERA_PROFILES = [
|
||||||
{
|
{
|
||||||
'id': '640x480',
|
'id': '640x480',
|
||||||
@@ -216,6 +226,31 @@ def get_wifi_status():
|
|||||||
return 'kein wlan0'
|
return 'kein wlan0'
|
||||||
|
|
||||||
|
|
||||||
|
def get_wifi_signal():
|
||||||
|
stdout, _, returncode = run_command(['nmcli', '-t', '-f', 'GENERAL.STATE,AP.SIGNAL', 'device', 'show', 'wlan0'])
|
||||||
|
if returncode != 0 or not stdout:
|
||||||
|
return 'unbekannt'
|
||||||
|
|
||||||
|
state = ''
|
||||||
|
signal = ''
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
if line.startswith('GENERAL.STATE:'):
|
||||||
|
state = line.split(':', 1)[1].strip()
|
||||||
|
elif line.startswith('AP[1].SIGNAL:'):
|
||||||
|
signal = line.split(':', 1)[1].strip()
|
||||||
|
|
||||||
|
if 'connected' not in state.lower():
|
||||||
|
return 'nicht verbunden'
|
||||||
|
if not signal:
|
||||||
|
return 'unbekannt'
|
||||||
|
|
||||||
|
try:
|
||||||
|
percent = max(0, min(100, int(float(signal))))
|
||||||
|
except ValueError:
|
||||||
|
return 'unbekannt'
|
||||||
|
return f'{percent} %'
|
||||||
|
|
||||||
|
|
||||||
def get_systemd_state(service_name):
|
def get_systemd_state(service_name):
|
||||||
stdout, _, returncode = run_command(['systemctl', 'is-active', service_name])
|
stdout, _, returncode = run_command(['systemctl', 'is-active', service_name])
|
||||||
if returncode == 0 and stdout:
|
if returncode == 0 and stdout:
|
||||||
@@ -245,6 +280,7 @@ def collect_status():
|
|||||||
'status': 'Bereit',
|
'status': 'Bereit',
|
||||||
'system': {
|
'system': {
|
||||||
'wifi': get_wifi_status(),
|
'wifi': get_wifi_status(),
|
||||||
|
'wifi_signal': get_wifi_signal(),
|
||||||
'ips': get_ip_addresses(),
|
'ips': get_ip_addresses(),
|
||||||
'undervoltage': undervoltage['text'],
|
'undervoltage': undervoltage['text'],
|
||||||
'undervoltage_state': undervoltage['state'],
|
'undervoltage_state': undervoltage['state'],
|
||||||
@@ -451,6 +487,85 @@ def restart_exomy_container():
|
|||||||
subprocess.Popen(['docker', 'restart', EXOMY_CONTAINER])
|
subprocess.Popen(['docker', 'restart', EXOMY_CONTAINER])
|
||||||
|
|
||||||
|
|
||||||
|
def restart_gps_node():
|
||||||
|
subprocess.Popen([
|
||||||
|
'docker', 'exec', EXOMY_CONTAINER, 'bash', '-lc',
|
||||||
|
"pkill -f '/root/exomy_ws/src/exomy/src/gps_node.py' || true; "
|
||||||
|
"sleep 1; "
|
||||||
|
"source /opt/ros/melodic/setup.bash && source /root/exomy_ws/devel/setup.bash && "
|
||||||
|
"nohup python /root/exomy_ws/src/exomy/src/gps_node.py > /tmp/gps_node.log 2>&1 &"
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_gps_device():
|
||||||
|
candidates = []
|
||||||
|
seen = set()
|
||||||
|
for pattern in GPS_PORT_PATTERNS:
|
||||||
|
for path in sorted(glob.glob(pattern)):
|
||||||
|
if path in seen:
|
||||||
|
continue
|
||||||
|
seen.add(path)
|
||||||
|
candidates.append(path)
|
||||||
|
return candidates[0] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def configure_gps_serial(fd, baud_rate):
|
||||||
|
baud_map = {
|
||||||
|
4800: termios.B4800,
|
||||||
|
9600: termios.B9600,
|
||||||
|
19200: termios.B19200,
|
||||||
|
38400: termios.B38400,
|
||||||
|
57600: termios.B57600,
|
||||||
|
115200: termios.B115200,
|
||||||
|
}
|
||||||
|
baud = baud_map.get(int(baud_rate), termios.B4800)
|
||||||
|
attrs = termios.tcgetattr(fd)
|
||||||
|
attrs[0] = 0
|
||||||
|
attrs[1] = 0
|
||||||
|
attrs[2] = termios.CS8 | termios.CREAD | termios.CLOCAL
|
||||||
|
attrs[3] = 0
|
||||||
|
attrs[4] = baud
|
||||||
|
attrs[5] = baud
|
||||||
|
attrs[6][termios.VMIN] = 0
|
||||||
|
attrs[6][termios.VTIME] = 0
|
||||||
|
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
||||||
|
termios.tcflush(fd, termios.TCIOFLUSH)
|
||||||
|
|
||||||
|
|
||||||
|
def send_gps_serial_command(command_text, baud_rate=GPS_BAUD_RATE):
|
||||||
|
device_path = resolve_gps_device()
|
||||||
|
if not device_path:
|
||||||
|
raise RuntimeError('Kein GPS-Empfänger gefunden')
|
||||||
|
|
||||||
|
payload = command_text.encode('ascii')
|
||||||
|
fd = None
|
||||||
|
try:
|
||||||
|
fd = os.open(device_path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
||||||
|
configure_gps_serial(fd, baud_rate)
|
||||||
|
os.write(fd, payload)
|
||||||
|
termios.tcdrain(fd)
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno == errno.ENOENT:
|
||||||
|
raise RuntimeError('GPS-Gerät nicht erreichbar')
|
||||||
|
raise RuntimeError(f'GPS-Befehl fehlgeschlagen: {exc}')
|
||||||
|
except termios.error as exc:
|
||||||
|
raise RuntimeError(f'GPS-Port konnte nicht konfiguriert werden: {exc}')
|
||||||
|
finally:
|
||||||
|
if fd is not None:
|
||||||
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return device_path
|
||||||
|
|
||||||
|
|
||||||
|
def cold_start_gps_receiver():
|
||||||
|
device_path = send_gps_serial_command(GPS_COLD_START_COMMAND)
|
||||||
|
time.sleep(1.5)
|
||||||
|
restart_gps_node()
|
||||||
|
return device_path
|
||||||
|
|
||||||
|
|
||||||
def stop_exomy_container():
|
def stop_exomy_container():
|
||||||
stdout, _, returncode = run_command(['docker', 'stop', '-t', '2', EXOMY_CONTAINER])
|
stdout, _, returncode = run_command(['docker', 'stop', '-t', '2', EXOMY_CONTAINER])
|
||||||
return returncode == 0, stdout
|
return returncode == 0, stdout
|
||||||
@@ -1256,6 +1371,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
'/api/cold-start-gps': {
|
||||||
|
'handler': cold_start_gps_receiver,
|
||||||
|
'status': 'GPS-Kaltstart wird ausgelöst'
|
||||||
|
},
|
||||||
|
'/api/restart-gps': {
|
||||||
|
'handler': restart_gps_node,
|
||||||
|
'status': 'GPS-Node wird neu gestartet'
|
||||||
|
},
|
||||||
'/api/restart-camera': {
|
'/api/restart-camera': {
|
||||||
'handler': restart_camera_service,
|
'handler': restart_camera_service,
|
||||||
'status': 'Kameradienst wird neu gestartet'
|
'status': 'Kameradienst wird neu gestartet'
|
||||||
|
|||||||
+2194
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user