GPS neustart eingebaut
This commit is contained in:
@@ -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` |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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
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