GPS neustart eingebaut

This commit is contained in:
2026-05-25 16:34:07 +02:00
parent 8c8bfd9e5b
commit 5b1ac5fe62
7 changed files with 3954 additions and 27 deletions
+17
View File
@@ -135,6 +135,8 @@ Systemverwaltung des Rovers. Die Seite ist passwortgeschützt — bei jedem Aufr
#### Aktionen
| 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 |
| ExoMy-Container neu starten | Startet Docker-Container `exomy_autostart` neu (ROS, Motoren, Joystick) |
| 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
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
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 |
|---|---|
| WLAN-Status | nmcli |
| WLAN-Signal | nmcli (`AP.SIGNAL`) |
| IP-Adressen | wlan0 |
| CPU-Temperatur | `/sys/class/thermal/thermal_zone0/temp` |
| Unterspannung | `vcgencmd get_throttled` |
+14
View File
@@ -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` |
| **Benutzer** | `pi` |
| **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** |
| `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` |
| `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) |
| `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
| Port | Verwendung |
+20 -6
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CUNO // Admin</title>
<link rel="stylesheet" href="./app.css?v=20260525132000">
<link rel="stylesheet" href="./app.css?v=20260525133500">
<link rel="stylesheet" href="./vendor/leaflet/leaflet.css">
<script src="./vendor/roslib.min.js"></script>
<script src="./vendor/leaflet/leaflet.js"></script>
@@ -160,6 +160,11 @@
<span class="sr-key">WLAN</span>
<span class="sr-val" id="info_wifi">-</span>
</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">
<span class="sr-led led-blue"></span>
<span class="sr-key">IP</span>
@@ -343,7 +348,7 @@
<span class="p-tag">Live Position</span>
</div>
<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 warm" onclick="centerMapOnRover()">Auf Rover zentrieren</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-tag">GNSS-Diagnose</span>
</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-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>
@@ -608,7 +617,7 @@
<div class="auth-eyebrow">Admin-Zugang</div>
<div class="auth-title">CUNO</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>
<button class="auth-submit" onclick="submitPassword()">Entsperren</button>
</div>
@@ -700,7 +709,7 @@
roverTrackLine: null,
routeLine: null,
waypointMarkers: [],
waypointPlanning: true,
waypointPlanning: false,
waypoints: [],
trackPoints: [],
roverLat: null,
@@ -741,7 +750,8 @@
function submitPassword() {
var input = document.getElementById('auth_input');
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();
} else {
error.textContent = 'Falsches Passwort';
@@ -758,6 +768,7 @@
function clearAdminApiDisplay() {
setText('info_ips', '--');
setText('info_wifi', '--');
setText('info_wifi_signal', '--');
setText('info_temp', '--');
setText('info_uptime', '--');
setText('info_disk', '--');
@@ -2220,7 +2231,7 @@
function temperatureToWidth(value) {
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%';
return Math.max(0, Math.min(100, ((n - 20) / 60) * 100)).toFixed(0) + '%';
}
@@ -2229,6 +2240,7 @@
setAdminStatus(data.status || 'Bereit');
setText('info_ips', data.system && data.system.ips);
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);
var bar = document.getElementById('bar-temp-adm');
if (bar && data.system) bar.style.width = temperatureToWidth(data.system.cpu_temperature);
@@ -2255,6 +2267,8 @@
async function runAdminAction(action) {
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-container': 'ExoMy-Container jetzt wirklich neu starten?',
'reboot': 'Raspberry Pi jetzt wirklich neu starten?',
+104 -21
View File
@@ -286,6 +286,8 @@ var motorCommandsListener = null;
var gpsFixListener = null;
var gpsStatusListener = null;
var gpsDiagnosticsListener = null;
var rosReconnectTimer = null;
var rosDisconnectTimer = null;
var publishTimer = null;
var statusTimer = null;
var axes = [0, 0, 0, 0, 0, 0];
@@ -317,6 +319,7 @@ var pageConnection = {
apiOk: false,
apiLastAt: 0,
rosOk: false,
rosLastAt: 0,
gpsLastAt: 0,
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() {
if (!lastGpsDiagnostics || !lastGpsDiagnostics.connected || !lastGpsDiagnostics.valid) {
setGpsAccuracyState("--", "");
@@ -674,6 +685,7 @@ function updateGpsAccuracyFromDiagnostics() {
function updateGpsOverlayFromFix(message) {
if (!message) return;
markRosFresh();
markGpsFresh();
var hasFix = message.status && message.status.status >= 0;
@@ -685,22 +697,35 @@ function updateGpsOverlayFromFix(message) {
}
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-lon", formatNumber(Number(message.longitude || 0), 5));
}
function updateGpsOverlayFromStatus(text) {
var value = String(text || "Offline");
markRosFresh();
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) {
markRosFresh();
markGpsFresh();
try {
lastGpsDiagnostics = JSON.parse(String(text || "{}"));
} catch (error) {
lastGpsDiagnostics = null;
}
if (lastGpsDiagnostics && lastGpsDiagnostics.connected) {
setGpsSatelliteSummary(Number(lastGpsDiagnostics.satellites_used || lastGpsDiagnostics.satellites_visible || 0), true);
}
updateGpsAccuracyFromDiagnostics();
}
@@ -795,14 +820,6 @@ function clearSystemStatusDisplay() {
}
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");
setGpsAccuracyState("--", "");
setText("gps-lat", "--");
@@ -814,18 +831,18 @@ function clearRosLiveDisplay() {
function setPageConnectionState() {
var now = Date.now();
var apiFresh = pageConnection.apiOk && (now - pageConnection.apiLastAt) < 15000;
var rosFresh = !!pageConnection.rosOk;
var gpsFresh = pageConnection.gpsLastAt && (now - pageConnection.gpsLastAt) < 12000;
var rosSocketOpen = !!(ros && ros.socket && ros.socket.readyState === 1);
var rosConnected = rosSocketOpen || !!pageConnection.rosOk;
var state = "offline";
var cunoTitle = "Cuno offline";
if (apiFresh && rosFresh) {
state = gpsFresh || !pageConnection.gpsLastAt ? "online" : "degraded";
} else if (apiFresh || rosFresh) {
if (apiFresh && rosConnected) {
state = "online";
} else if (apiFresh || rosConnected) {
state = "degraded";
}
cunoTitle = state === "online" ? "Cuno online" : "Cuno offline";
cunoTitle = state === "online" ? "Cuno online" : (state === "degraded" ? "Cuno eingeschränkt" : "Cuno offline");
pageConnection.state = state;
document.body.classList.toggle("page-connection-online", state === "online");
document.body.classList.toggle("page-connection-degraded", state === "degraded");
@@ -836,7 +853,7 @@ function setPageConnectionState() {
if (!apiFresh) {
clearSystemStatusDisplay();
}
if (!rosFresh) {
if (!rosConnected) {
clearRosLiveDisplay();
}
}
@@ -851,6 +868,15 @@ function markApiConnection(ok) {
function markRosConnection(ok) {
pageConnection.rosOk = !!ok;
if (ok) {
pageConnection.rosLastAt = Date.now();
}
setPageConnectionState();
}
function markRosFresh() {
pageConnection.rosOk = true;
pageConnection.rosLastAt = Date.now();
setPageConnectionState();
}
@@ -859,6 +885,47 @@ function markGpsFresh() {
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) {
var el = document.getElementById("stat-undervoltage");
if (!el) {
@@ -1157,11 +1224,13 @@ window.addEventListener("load", function () {
updateClocks();
window.setInterval(updateClocks, 1000);
fetchSystemStatus();
refreshRosSocketState();
setPageConnectionState();
fetchDriveEstimatorData();
renderDriveEstimatorOverlay();
renderTrajectoryOverlay();
statusTimer = window.setInterval(fetchSystemStatus, 5000);
window.setInterval(refreshRosSocketState, 1000);
window.setInterval(setPageConnectionState, 1000);
window.setInterval(renderDriveEstimatorOverlay, 250);
window.setInterval(renderTrajectoryOverlay, 250);
@@ -1217,18 +1286,25 @@ window.addEventListener("load", function () {
});
ros.on("connection", function () {
if (rosReconnectTimer) {
window.clearTimeout(rosReconnectTimer);
rosReconnectTimer = null;
}
clearPendingRosDisconnect();
markRosConnection(true);
setRosStatus("Verbunden");
});
ros.on("error", function () {
markRosConnection(false);
setRosStatus("Fehler");
setRosStatus("Verbinde...");
scheduleRosDisconnect();
scheduleRosReconnect();
});
ros.on("close", function () {
markRosConnection(false);
setRosStatus("Getrennt");
setRosStatus("Verbinde...");
scheduleRosDisconnect();
scheduleRosReconnect();
});
joyListener = new ROSLIB.Topic({
@@ -1244,6 +1320,9 @@ window.addEventListener("load", function () {
});
roverCommandListener.subscribe(function (message) {
clearPendingRosDisconnect();
markRosFresh();
setRosStatus("Verbunden");
applyDriveMode(locomotionModeToUi(message.locomotion_mode), true);
motorsEnabled = !!message.motors_enabled;
driveEstimator.motorsEnabled = !!message.motors_enabled;
@@ -1261,6 +1340,9 @@ window.addEventListener("load", function () {
messageType: "exomy/MotorCommands"
});
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];
renderDriveEstimatorOverlay();
renderTrajectoryOverlay();
@@ -1297,6 +1379,7 @@ window.addEventListener("load", function () {
messageType: "sensor_msgs/Joy"
});
joySubscriber.subscribe(function (message) {
clearPendingRosDisconnect();
markRosConnection(true);
if (message.header.frame_id === "webgui") return;
var x = (message.axes[0] || 0);
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import errno
import fcntl
import glob
import http.server
@@ -8,6 +9,7 @@ import shutil
import socketserver
import struct
import subprocess
import termios
import threading
import time
@@ -25,6 +27,14 @@ CAMERA_SETTINGS_FILE = os.path.join(
os.path.dirname(__file__), '..', 'config', 'camera_settings.json'
)
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 = [
{
'id': '640x480',
@@ -216,6 +226,31 @@ def get_wifi_status():
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):
stdout, _, returncode = run_command(['systemctl', 'is-active', service_name])
if returncode == 0 and stdout:
@@ -245,6 +280,7 @@ def collect_status():
'status': 'Bereit',
'system': {
'wifi': get_wifi_status(),
'wifi_signal': get_wifi_signal(),
'ips': get_ip_addresses(),
'undervoltage': undervoltage['text'],
'undervoltage_state': undervoltage['state'],
@@ -451,6 +487,85 @@ def 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():
stdout, _, returncode = run_command(['docker', 'stop', '-t', '2', EXOMY_CONTAINER])
return returncode == 0, stdout
@@ -1256,6 +1371,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
return
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': {
'handler': restart_camera_service,
'status': 'Kameradienst wird neu gestartet'
+2194
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff