diff --git a/ExoMy_Software-master/gui/admin.css b/ExoMy_Software-master/gui/admin.css
deleted file mode 100644
index 7ad6c6d..0000000
--- a/ExoMy_Software-master/gui/admin.css
+++ /dev/null
@@ -1,357 +0,0 @@
-:root {
- --bg: #05080F;
- --pri: #0088FF;
- --sec: #FF8C00;
- --ok: #00ff91;
- --txt: rgba(0,255,145,.8);
- --txt2: rgba(0,210,130,.38);
- --mono: "Consolas","Cascadia Code","Cascadia Mono","Courier New",monospace;
-}
-* { margin:0; padding:0; box-sizing:border-box; }
-html { height:100%; }
-body {
- font-family:var(--mono);
- background-color:var(--bg);
- background-image:
- linear-gradient(rgba(0,255,145,.04) 1px, transparent 1px),
- linear-gradient(90deg, rgba(0,255,145,.04) 1px, transparent 1px);
- background-size:32px 32px;
- color:var(--txt);
- display:flex;
- flex-direction:column;
- height:100%;
- overflow:hidden;
-}
-
-/* ── HEADER ─────────────────────────────────────── */
-header {
- height:52px;
- background:linear-gradient(135deg, #060D1A 0%, #0C1C38 100%);
- border-bottom:2px solid var(--pri);
- box-shadow:0 2px 24px rgba(0,136,255,.28);
- display:flex;
- align-items:center;
- padding:0 16px;
- gap:14px;
- flex-shrink:0;
- z-index:10;
-}
-.hdr-icon { width:32px; height:32px; flex-shrink:0; }
-.hdr-title { font-weight:700; font-size:14px; color:#fff; letter-spacing:5px; text-transform:uppercase; white-space:nowrap; }
-.hdr-title em { font-style:normal; color:var(--pri); }
-.hdr-div { width:1px; height:28px; background:rgba(255,255,255,.12); flex-shrink:0; }
-.hdr-badge { font-size:7px; color:var(--pri); border:1px solid rgba(0,136,255,.5); padding:2px 8px; letter-spacing:2.5px; text-transform:uppercase; white-space:nowrap; }
-.hdr-gap { flex:1; }
-.hdr-status { font-size:8px; color:rgba(0,255,145,.55); letter-spacing:2px; text-transform:uppercase; white-space:nowrap; }
-.hdr-back { color:rgba(0,255,145,.45); text-decoration:none; font-size:8px; letter-spacing:2px; text-transform:uppercase; }
-.hdr-back:hover { color:rgba(0,255,145,.8); }
-.hdr-clk { display:flex; flex-direction:column; align-items:flex-end; }
-.hdr-clk-lbl { font-size:7px; color:rgba(255,255,255,.35); letter-spacing:2px; text-transform:uppercase; }
-.hdr-clk-val { font-weight:700; font-size:14px; color:#fff; letter-spacing:3px; line-height:1.1; }
-
-/* ── PANEL ──────────────────────────────────────── */
-.panel {
- position:relative;
- background:
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 0/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 0/1px 14px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 0/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 0/1px 14px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 100%/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 100%/1px 14px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 100%/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 100%/1px 14px no-repeat,
- #070D18;
- border:1px solid rgba(0,255,145,.1);
- box-shadow:0 0 24px rgba(0,0,0,.5), inset 0 0 40px rgba(0,255,145,.018);
-}
-.p-hdr {
- display:flex; align-items:center; gap:7px;
- padding:7px 12px;
- border-bottom:1px solid rgba(0,255,145,.08);
- background:linear-gradient(90deg, rgba(0,255,145,.05) 0%, transparent 100%);
-}
-.p-dot { width:5px; height:5px; border-radius:50%; background:var(--ok); box-shadow:0 0 6px var(--ok); flex-shrink:0; }
-.p-lbl { font-size:8px; letter-spacing:3px; color:rgba(0,255,145,.7); text-transform:uppercase; font-weight:bold; }
-.p-tag { margin-left:auto; font-size:7px; color:rgba(0,255,145,.25); letter-spacing:1px; }
-
-/* ── STATUS ROWS ────────────────────────────────── */
-.sr { display:flex; align-items:center; padding:4px 10px; gap:7px; border-bottom:1px solid rgba(0,255,145,.04); transition:background .1s; }
-.sr:last-child { border-bottom:none; }
-.sr:hover { background:rgba(0,255,145,.02); }
-.sr-led { width:6px; height:6px; border-radius:50%; flex-shrink:0; border:1px solid rgba(0,255,145,.15); background:#0a1408; }
-.sr-led.led-ok { background:#00ff91; border-color:#00ff91; box-shadow:0 0 5px #00ff91, 0 0 12px rgba(0,255,145,.55); }
-.sr-led.led-warn { background:#FF8C00; border-color:#FF8C00; box-shadow:0 0 5px #FF8C00, 0 0 12px rgba(255,140,0,.5); }
-.sr-led.led-blue { background:#0088FF; border-color:#0088FF; box-shadow:0 0 5px #0088FF, 0 0 12px rgba(0,136,255,.5); }
-.sr-led.led-off { background:#0d1208; border-color:rgba(0,255,145,.08); box-shadow:none; }
-.sr-key { font-size:7px; letter-spacing:2px; color:rgba(0,210,130,.4); text-transform:uppercase; min-width:60px; flex-shrink:0; }
-.sr-val { font-size:9px; font-weight:700; letter-spacing:.8px; margin-left:auto; color:rgba(0,255,145,.85); text-align:right; }
-
-/* ── SERVICE STATE ──────────────────────────────── */
-.state_active { color:#00ff91; }
-.state_inactive { color:#FF3333; }
-.state_unknown { color:#FF8C00; }
-@keyframes statusBlink { 0%,100%{opacity:1} 50%{opacity:.1} }
-
-/* ── SECTION DIVIDER ────────────────────────────── */
-.ss-hdr {
- display:flex; align-items:center; gap:7px;
- padding:5px 10px 4px;
- background:rgba(0,255,145,.035);
- border-top:1px solid rgba(0,255,145,.1);
- border-bottom:1px solid rgba(0,255,145,.05);
-}
-.ss-hdr-line { flex:1; height:1px; background:repeating-linear-gradient(90deg, rgba(0,255,145,.22) 0, rgba(0,255,145,.22) 3px, transparent 3px, transparent 6px); }
-.ss-hdr-txt { font-size:6px; letter-spacing:4px; color:rgba(0,255,145,.45); text-transform:uppercase; }
-
-/* ── GAUGE ──────────────────────────────────────── */
-.sr-gauge { padding-bottom:6px; }
-.gauge { flex:1; height:8px; background:rgba(0,255,145,.04); border:1px solid rgba(0,255,145,.14); position:relative; overflow:hidden; }
-.gauge::after { content:""; position:absolute; inset:0; background:repeating-linear-gradient(90deg, transparent 0, transparent 8px, rgba(4,8,16,.88) 8px, rgba(4,8,16,.88) 9px); z-index:2; pointer-events:none; }
-.gauge-fill { height:100%; width:0%; position:relative; z-index:1; transition:width .6s cubic-bezier(.25,.46,.45,.94); }
-.gauge-temp .gauge-fill { background:linear-gradient(90deg, rgba(180,60,0,.7), rgba(255,160,0,.9)); box-shadow:2px 0 14px rgba(255,140,0,.45); }
-.gauge-num { font-size:8px; font-weight:700; min-width:52px; text-align:right; letter-spacing:.5px; color:rgba(255,160,0,.8); }
-
-/* ── MAIN LAYOUT ────────────────────────────────── */
-.admin-main {
- flex:1; min-height:0; overflow-y:auto;
- display:flex; flex-direction:column; gap:12px;
- padding:12px; width:100%;
-}
-.admin-col { display:flex; flex-direction:column; gap:12px; }
-
-/* ── RESPONSIVE ─────────────────────────────────── */
-@media (min-width:640px) {
- .admin-main { flex-direction:row; overflow:hidden; max-width:1100px; margin:0 auto; }
- .admin-col { flex:1; min-width:0; overflow-y:auto; min-height:0; }
-}
-@media (min-width:1024px) {
- .admin-main { padding:16px; gap:16px; }
-}
-@media (max-width:479px) {
- .hdr-badge, .hdr-clk { display:none; }
-}
-
-/* ── ACTION BUTTONS ─────────────────────────────── */
-.act-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:10px 12px; }
-.act-btn {
- font-family:var(--mono); font-size:8px; letter-spacing:1.5px; text-transform:uppercase;
- padding:10px 8px; border:1px solid rgba(0,255,145,.2); background:rgba(0,255,145,.03);
- color:rgba(0,255,145,.6); cursor:pointer; transition:all .15s;
-}
-.act-btn:hover { background:rgba(0,255,145,.08); color:var(--ok); border-color:rgba(0,255,145,.5); }
-.act-btn.warm { border-color:rgba(255,140,0,.35); color:rgba(255,140,0,.7); background:rgba(255,140,0,.04); }
-.act-btn.warm:hover { background:rgba(255,140,0,.12); color:#FF8C00; border-color:rgba(255,140,0,.7); }
-.act-btn.danger { border-color:rgba(255,51,51,.35); color:rgba(255,51,51,.7); background:rgba(255,51,51,.04); }
-.act-btn.danger:hover { background:rgba(255,51,51,.12); color:#FF3333; border-color:rgba(255,51,51,.7); }
-.act-btn.full { grid-column:span 2; }
-.act-btn.subtle { border-color:rgba(0,255,145,.1); color:rgba(0,255,145,.3); }
-.act-btn.subtle:hover { color:rgba(0,255,145,.6); border-color:rgba(0,255,145,.35); }
-
-/* ── LINK CARD ──────────────────────────────────── */
-.link-card { display:flex; align-items:center; gap:12px; padding:10px 12px; }
-.link-card-text strong { display:block; font-size:9px; letter-spacing:1.5px; color:rgba(0,255,145,.8); margin-bottom:3px; text-transform:uppercase; }
-.link-card-text p { font-size:8px; color:rgba(0,210,130,.45); letter-spacing:.5px; }
-.link-card-action { margin-left:auto; flex-shrink:0; }
-.link-btn {
- font-family:var(--mono); font-size:8px; letter-spacing:1.5px; text-transform:uppercase;
- padding:8px 14px; border:1px solid rgba(0,136,255,.4); background:rgba(0,136,255,.06);
- color:rgba(0,136,255,.8); cursor:pointer; text-decoration:none; display:inline-block; transition:all .15s;
-}
-.link-btn:hover { background:rgba(0,136,255,.14); color:#55AAFF; border-color:rgba(0,136,255,.7); }
-
-/* ── DROPDOWN CONTROL ───────────────────────────── */
-.dropdown-row { display:flex; align-items:center; gap:10px; padding:10px 12px; }
-.dropdown-row label { font-size:7px; letter-spacing:2px; color:rgba(0,210,130,.4); text-transform:uppercase; min-width:40px; }
-.ctrl-select {
- font-family:var(--mono); font-size:8px; letter-spacing:1px;
- color:rgba(0,255,145,.8); background:#050A14;
- border:1px solid rgba(0,255,145,.2); padding:6px 8px;
- cursor:pointer; outline:none; flex:1;
-}
-.ctrl-select:focus { border-color:rgba(0,255,145,.5); }
-.ctrl-status { font-size:7px; letter-spacing:1.5px; color:rgba(0,255,145,.45); white-space:nowrap; min-width:50px; text-align:right; }
-
-.act-btn.is_active {
- background: rgba(0,136,255,.16);
- color: #55aaff;
- border-color: rgba(0,136,255,.55);
- box-shadow: 0 0 14px rgba(0,136,255,.18);
-}
-
-.admin-section-grid_map {
- grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.85fr);
-}
-
-.admin-col_map_main,
-.admin-col_map_side {
- min-width: 0;
-}
-
-.map-panel {
- display: flex;
- flex-direction: column;
- min-height: 620px;
-}
-
-.map-toolbar {
- display: flex;
- gap: 8px;
- padding: 10px 12px 0;
- flex-wrap: wrap;
-}
-
-.map-copy {
- padding: 10px 12px;
-}
-
-.map-stage {
- position: relative;
- flex: 1;
- min-height: 460px;
- margin: 0 12px 12px;
- border: 1px solid rgba(0,255,145,.12);
- background:
- radial-gradient(circle at 20% 20%, rgba(0,136,255,.12), transparent 34%),
- radial-gradient(circle at 78% 24%, rgba(0,255,145,.08), transparent 28%),
- #04101a;
- overflow: hidden;
-}
-
-.rover-map,
-.rover-map-fallback {
- position: absolute;
- inset: 0;
-}
-
-.rover-map-fallback {
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- padding: 20px;
- color: rgba(0,255,145,.65);
- font-size: 12px;
- letter-spacing: .6px;
- background: rgba(4, 16, 26, .92);
-}
-
-.map-waypoint-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 10px 12px 12px;
-}
-
-.map-waypoint-empty {
- padding: 12px;
- border: 1px dashed rgba(0,255,145,.14);
- color: rgba(0,255,145,.42);
- font-size: 8px;
- letter-spacing: 1px;
- text-transform: uppercase;
- text-align: center;
-}
-
-.map-waypoint-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px;
- border: 1px solid rgba(0,255,145,.1);
- background: rgba(0,255,145,.03);
-}
-
-.map-waypoint-index {
- min-width: 42px;
- font-size: 8px;
- color: rgba(0,255,145,.5);
- letter-spacing: 1.4px;
- text-transform: uppercase;
-}
-
-.map-waypoint-coords {
- display: flex;
- flex-direction: column;
- gap: 3px;
- min-width: 0;
-}
-
-.map-waypoint-coords strong,
-.map-waypoint-coords span {
- font-size: 10px;
- letter-spacing: .5px;
- color: rgba(0,255,145,.82);
-}
-
-.leaflet-container {
- font-family: var(--mono);
- background: #04101a;
-}
-
-.leaflet-control-attribution {
- background: rgba(4, 16, 26, .82);
- color: rgba(0,255,145,.58);
-}
-
-.leaflet-control-attribution a {
- color: rgba(85,170,255,.85);
-}
-
-.leaflet-popup-content-wrapper,
-.leaflet-popup-tip {
- background: #07111c;
- color: rgba(0,255,145,.82);
-}
-
-@media (max-width: 1024px) {
- .admin-section-grid_map {
- grid-template-columns: 1fr;
- }
-
- .map-panel {
- min-height: 0;
- }
-
- .map-stage {
- min-height: 360px;
- }
-}
-
-/* ── AUTH OVERLAY ───────────────────────────────── */
-.auth-overlay {
- position:fixed; inset:0; background:rgba(5,8,15,.93);
- display:flex; align-items:center; justify-content:center;
- z-index:1000; backdrop-filter:blur(4px);
-}
-.auth-box {
- width:320px; display:flex; flex-direction:column; gap:14px; padding:28px 24px;
- background:
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 0/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 0/1px 14px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 0/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 0/1px 14px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 100%/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 0 100%/1px 14px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 100%/14px 1px no-repeat,
- linear-gradient(rgba(0,255,145,.55),rgba(0,255,145,.55)) 100% 100%/1px 14px no-repeat,
- #070D18;
- border:1px solid rgba(0,255,145,.12);
- box-shadow:0 0 60px rgba(0,0,0,.8), 0 0 40px rgba(0,136,255,.08);
-}
-.auth-eyebrow { font-size:7px; letter-spacing:3px; color:rgba(0,210,130,.5); text-transform:uppercase; }
-.auth-title { font-size:20px; font-weight:700; color:#fff; letter-spacing:4px; text-transform:uppercase; }
-.auth-sub { font-size:8px; color:rgba(0,255,145,.35); letter-spacing:1px; }
-.auth-input {
- font-family:var(--mono); font-size:16px; background:#050A14;
- border:1px solid rgba(0,255,145,.2); color:var(--ok);
- padding:10px 12px; outline:none; letter-spacing:6px; width:100%;
-}
-.auth-input:focus { border-color:rgba(0,255,145,.5); }
-.auth-error { font-size:8px; color:#FF3333; letter-spacing:1px; min-height:12px; }
-.auth-submit {
- font-family:var(--mono); font-size:9px; letter-spacing:2px; text-transform:uppercase;
- padding:11px; border:1px solid rgba(0,136,255,.5); background:rgba(0,136,255,.1);
- color:rgba(0,136,255,.9); cursor:pointer; transition:all .15s; width:100%;
-}
-.auth-submit:hover { background:rgba(0,136,255,.22); color:#55AAFF; }
diff --git a/ExoMy_Software-master/gui/admin.html b/ExoMy_Software-master/gui/admin.html
index 8feeb6b..95842b1 100644
--- a/ExoMy_Software-master/gui/admin.html
+++ b/ExoMy_Software-master/gui/admin.html
@@ -12,17 +12,26 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
CUNO ADMIN
@@ -30,6 +39,8 @@
Lade...
+
+
← Ground Control
@@ -37,12 +48,17 @@
--:--:--
+
+ Verbindung wird geprüft
+ Warte auf ROS und Admin-API.
+
Admin
Geschwindigkeit / Weg
Karte
+ GPS
Motorentest
@@ -357,6 +373,64 @@
+
+
+
+
+
+
+
GNSS-Satelliten
+
Himmelskarte
+
+
+ Diese Ansicht zeigt die vom Empfänger gemeldeten Satelliten im Himmelsplot. Außen ist der Horizont, die Mitte ist der Zenit.
+
+
+
+
+
+
+
+
+
+
Fix-Diagnose
+
GNSS-Diagnose
+
+
Status Offline
+
Fix-Qualität --
+
Frame --
+
Breite --
+
Länge --
+
Höhe --
+
Geschwindigkeit --
+
Kurs --
+
UTC-Datum --
+
UTC-Zeit --
+
Fix-Alter --
+
+
+
+
+
+
Genauigkeit
+
Qualität
+
+
Positionsgenauigkeit --
+
Horizontale Genauigkeit --
+
Vertikale Genauigkeit --
+
Für Fix genutzt --
+
Im Himmelsplot --
+
+ Für eine gute Fix-Qualität sind niedrige DOP-Werte und hohe, gleichmäßig verteilte Satelliten sinnvoll. Der Himmelsplot zeigt nur Satelliten, die der Empfänger per GSV mit Richtung und Höhe meldet.
+
+
+
+
+
+
@@ -479,6 +553,15 @@
var motorCommandsTopic = null;
var gpsFixTopic = null;
var gpsStatusTopic = null;
+ var gpsDiagnosticsTopic = null;
+ var pageConnection = {
+ apiOk: false,
+ apiLastAt: 0,
+ rosOk: false,
+ gpsLastAt: 0,
+ lastHealthyAt: 0,
+ state: 'offline'
+ };
var isMotorTestRunning = false;
var isContainerActionRunning = false;
@@ -560,6 +643,29 @@
hasCenteredOnFix: false,
suppressMapClickUntil: 0
};
+ var gpsDiagnosticsState = {
+ connected: false,
+ frameId: 'gps',
+ valid: false,
+ fixQuality: 0,
+ satellitesUsed: 0,
+ satellitesVisible: 0,
+ latitude: null,
+ longitude: null,
+ altitudeM: null,
+ hdop: null,
+ pdop: null,
+ vdop: null,
+ speedMps: null,
+ speedKmh: null,
+ trackDeg: null,
+ utcDate: null,
+ utcTime: null,
+ lastFixAgeS: null,
+ satellites: [],
+ selectedSatellitePrn: null,
+ plottedSatellites: []
+ };
function submitPassword() {
var input = document.getElementById('auth_input');
@@ -578,6 +684,129 @@
if (el) el.textContent = text;
}
+ function clearAdminApiDisplay() {
+ setText('info_ips', '--');
+ setText('info_wifi', '--');
+ setText('info_temp', '--');
+ setText('info_uptime', '--');
+ setText('info_disk', '--');
+ setUndervoltageState('info_undervoltage', '--', 'unknown');
+ setServiceState('service_camera', '-');
+ setServiceState('service_admin_api', '-');
+ setServiceState('service_exomy', '-');
+ var bar = document.getElementById('bar-temp-adm');
+ if (bar) bar.style.width = '0%';
+ }
+
+ function clearAdminRosDisplay() {
+ roverMapState.hasFix = false;
+ roverMapState.roverLat = null;
+ roverMapState.roverLon = null;
+ roverMapState.roverAlt = null;
+ roverMapState.accuracyMeters = null;
+ roverMapState.lastFixAt = null;
+ roverMapState.gpsStatusText = 'Offline';
+ roverMapState.satellites = '--';
+ updateMapTelemetryPanel();
+
+ setDriveEstimatorState('offline', 'state_inactive');
+ setText('drive_estimator_speed', '--');
+ setText('drive_estimator_distance', '--');
+ setText('drive_estimator_pulse', '--');
+ setText('drive_estimator_rpm', '--');
+ setText('drive_estimator_mode', '--');
+ setText('drive_estimator_direction', '--');
+ gpsDiagnosticsState.connected = false;
+ gpsDiagnosticsState.valid = false;
+ gpsDiagnosticsState.fixQuality = 0;
+ gpsDiagnosticsState.satellitesUsed = 0;
+ gpsDiagnosticsState.satellitesVisible = 0;
+ gpsDiagnosticsState.latitude = null;
+ gpsDiagnosticsState.longitude = null;
+ gpsDiagnosticsState.altitudeM = null;
+ gpsDiagnosticsState.hdop = null;
+ gpsDiagnosticsState.pdop = null;
+ gpsDiagnosticsState.vdop = null;
+ gpsDiagnosticsState.speedMps = null;
+ gpsDiagnosticsState.speedKmh = null;
+ gpsDiagnosticsState.trackDeg = null;
+ gpsDiagnosticsState.utcDate = null;
+ gpsDiagnosticsState.utcTime = null;
+ gpsDiagnosticsState.lastFixAgeS = null;
+ gpsDiagnosticsState.satellites = [];
+ updateGpsDiagnosticsPanel();
+ }
+
+ function formatConnectionTime(timestamp) {
+ if (!timestamp) return '--:--:--';
+ return new Date(timestamp).toLocaleTimeString('de-DE', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ });
+ }
+
+ function updatePageConnectionState() {
+ var now = Date.now();
+ var apiFresh = pageConnection.apiOk && (now - pageConnection.apiLastAt) < 20000;
+ var rosFresh = !!pageConnection.rosOk;
+ var gpsFresh = pageConnection.gpsLastAt && (now - pageConnection.gpsLastAt) < 15000;
+ var state = 'offline';
+ var title = 'Offline';
+ var details = [];
+
+ if (apiFresh && rosFresh) {
+ state = gpsFresh || !pageConnection.gpsLastAt ? 'online' : 'degraded';
+ title = state === 'online' ? 'Online' : 'Eingeschränkt';
+ } else if (apiFresh || rosFresh) {
+ state = 'degraded';
+ title = 'Eingeschränkt';
+ }
+
+ if (!apiFresh) details.push('Admin-API nicht erreichbar');
+ if (!rosFresh) details.push('ROSBridge getrennt');
+ if (rosFresh && pageConnection.gpsLastAt && !gpsFresh) details.push('GPS-Daten veraltet');
+ if (!details.length) details.push('Alle Live-Dienste antworten');
+ if (state !== 'online' && pageConnection.lastHealthyAt) {
+ details.push('letzter Kontakt ' + formatConnectionTime(pageConnection.lastHealthyAt));
+ }
+
+ pageConnection.state = state;
+ document.body.classList.toggle('page-connection-online', state === 'online');
+ document.body.classList.toggle('page-connection-degraded', state === 'degraded');
+ document.body.classList.toggle('page-connection-offline', state === 'offline');
+ setText('page-connection-label', title);
+ setText('connection-banner-title', title);
+ setText('connection-banner-detail', details.join(' | '));
+
+ if (!apiFresh) clearAdminApiDisplay();
+ if (!rosFresh) clearAdminRosDisplay();
+
+ var banner = document.getElementById('connection-banner');
+ if (banner) banner.hidden = state === 'online';
+ }
+
+ function markAdminApiConnection(ok) {
+ pageConnection.apiOk = !!ok;
+ if (ok) {
+ pageConnection.apiLastAt = Date.now();
+ pageConnection.lastHealthyAt = pageConnection.apiLastAt;
+ }
+ updatePageConnectionState();
+ }
+
+ function markAdminRosConnection(ok) {
+ pageConnection.rosOk = !!ok;
+ if (ok) pageConnection.lastHealthyAt = Date.now();
+ updatePageConnectionState();
+ }
+
+ function markAdminGpsFresh() {
+ pageConnection.gpsLastAt = Date.now();
+ if (pageConnection.rosOk) pageConnection.lastHealthyAt = pageConnection.gpsLastAt;
+ updatePageConnectionState();
+ }
+
function setMotorTestStatus(text) {
setAdminStatus(text);
}
@@ -587,26 +816,32 @@
var systemTab = document.getElementById('admin_tab_system');
var driveTab = document.getElementById('admin_tab_drive');
var mapTab = document.getElementById('admin_tab_map');
+ var gpsTab = document.getElementById('admin_tab_gps');
var motorTab = document.getElementById('admin_tab_motor');
var systemPanel = document.getElementById('admin_panel_system');
var drivePanel = document.getElementById('admin_panel_drive');
var mapPanel = document.getElementById('admin_panel_map');
+ var gpsPanel = document.getElementById('admin_panel_gps');
var motorPanel = document.getElementById('admin_panel_motor');
var isSystem = sectionName === 'system';
var isDrive = sectionName === 'drive';
var isMap = sectionName === 'map';
+ var isGps = sectionName === 'gps';
var isMotor = sectionName === 'motor';
systemTab.classList.toggle('is_active', isSystem);
driveTab.classList.toggle('is_active', isDrive);
mapTab.classList.toggle('is_active', isMap);
+ gpsTab.classList.toggle('is_active', isGps);
motorTab.classList.toggle('is_active', isMotor);
systemTab.setAttribute('aria-selected', isSystem ? 'true' : 'false');
driveTab.setAttribute('aria-selected', isDrive ? 'true' : 'false');
mapTab.setAttribute('aria-selected', isMap ? 'true' : 'false');
+ gpsTab.setAttribute('aria-selected', isGps ? 'true' : 'false');
motorTab.setAttribute('aria-selected', isMotor ? 'true' : 'false');
systemPanel.classList.toggle('is_active', isSystem);
drivePanel.classList.toggle('is_active', isDrive);
mapPanel.classList.toggle('is_active', isMap);
+ gpsPanel.classList.toggle('is_active', isGps);
motorPanel.classList.toggle('is_active', isMotor);
window.location.hash = isSystem ? '' : ('#' + sectionName);
if (isMap) {
@@ -615,6 +850,9 @@
if (roverMapState.map) roverMapState.map.invalidateSize();
}, 80);
}
+ if (isGps) {
+ window.setTimeout(drawGpsSkyPlot, 80);
+ }
if (isMotor) fetchMotorTestStatus();
}
@@ -665,14 +903,14 @@
});
}
- function formatCoordinate(value) {
+ function formatCoordinate(value, digits) {
if (typeof value !== 'number' || isNaN(value)) return '--';
- return formatNumber(value, 5);
+ return formatNumber(value, typeof digits === 'number' ? digits : 5);
}
- function formatAltitude(value) {
+ function formatAltitude(value, digits) {
if (typeof value !== 'number' || isNaN(value)) return '--';
- return formatNumber(value, 1) + ' m';
+ return formatNumber(value, typeof digits === 'number' ? digits : 1) + ' m';
}
function formatAccuracy(value) {
@@ -680,6 +918,26 @@
return formatNumber(value, 1) + ' m';
}
+ function formatDop(value) {
+ if (typeof value !== 'number' || isNaN(value) || value <= 0) return '--';
+ return formatNumber(value, 2);
+ }
+
+ function formatDegrees(value) {
+ if (typeof value !== 'number' || isNaN(value)) return '--';
+ return formatNumber(value, 1) + '°';
+ }
+
+ function formatSpeedKmh(value) {
+ if (typeof value !== 'number' || isNaN(value)) return '--';
+ return formatNumber(value, 1) + ' km/h';
+ }
+
+ function formatFixAge(value) {
+ if (typeof value !== 'number' || isNaN(value)) return '--';
+ return formatNumber(value, 1) + ' s';
+ }
+
function formatFixTime(date) {
if (!date) return '--';
return date.toLocaleTimeString('de-DE', {
@@ -689,6 +947,241 @@
});
}
+ function fixQualityLabel(value) {
+ var labels = {
+ 0: 'Kein Fix',
+ 1: 'GPS Fix',
+ 2: 'DGPS Fix',
+ 4: 'RTK Fixed',
+ 5: 'RTK Float'
+ };
+ return labels[value] || String(value || 0);
+ }
+
+ function describeDopQuality(value) {
+ if (typeof value !== 'number' || isNaN(value) || value <= 0) {
+ return { text: '--', stateClass: 'state_unknown' };
+ }
+ if (value < 1) {
+ return { text: formatNumber(value, 2) + ' sehr gut', stateClass: 'state_active' };
+ }
+ if (value < 2) {
+ return { text: formatNumber(value, 2) + ' gut', stateClass: 'state_active' };
+ }
+ if (value < 5) {
+ return { text: formatNumber(value, 2) + ' brauchbar', stateClass: 'state_unknown' };
+ }
+ if (value < 10) {
+ return { text: formatNumber(value, 2) + ' kritisch', stateClass: 'state_inactive' };
+ }
+ return { text: formatNumber(value, 2) + ' schlecht', stateClass: 'state_inactive' };
+ }
+
+ function setGpsQualityState(id, value) {
+ var node = document.getElementById(id);
+ if (!node) return;
+ var quality = describeDopQuality(value);
+ node.textContent = quality.text;
+ node.classList.remove('state_active', 'state_inactive', 'state_unknown');
+ node.classList.add(quality.stateClass);
+ }
+
+ function updateGpsDiagnosticsPanel() {
+ setText('gps_diag_status', gpsDiagnosticsState.connected ? (gpsDiagnosticsState.valid ? 'Fix aktiv' : 'Empfänger online') : 'Offline');
+ setText('gps_diag_fix_quality', fixQualityLabel(gpsDiagnosticsState.fixQuality));
+ setText('gps_diag_frame', gpsDiagnosticsState.frameId || '--');
+ setText('gps_diag_lat', formatCoordinate(gpsDiagnosticsState.latitude, 8));
+ setText('gps_diag_lon', formatCoordinate(gpsDiagnosticsState.longitude, 8));
+ setText('gps_diag_alt', formatAltitude(gpsDiagnosticsState.altitudeM, 4));
+ setText('gps_diag_speed', formatSpeedKmh(gpsDiagnosticsState.speedKmh));
+ setText('gps_diag_track', formatDegrees(gpsDiagnosticsState.trackDeg));
+ setText('gps_diag_utc_date', gpsDiagnosticsState.utcDate || '--');
+ setText('gps_diag_utc_time', gpsDiagnosticsState.utcTime || '--');
+ setText('gps_diag_fix_age', formatFixAge(gpsDiagnosticsState.lastFixAgeS));
+ setGpsQualityState('gps_diag_pdop', gpsDiagnosticsState.pdop);
+ setGpsQualityState('gps_diag_hdop', gpsDiagnosticsState.hdop);
+ setGpsQualityState('gps_diag_vdop', gpsDiagnosticsState.vdop);
+ setText('gps_diag_sat_used', gpsDiagnosticsState.satellitesUsed);
+ setText('gps_diag_sat_visible', gpsDiagnosticsState.satellitesVisible);
+ drawGpsSkyPlot();
+ }
+
+ function getSelectedSatellite() {
+ if (!gpsDiagnosticsState.satellites.length) return null;
+ var selected = gpsDiagnosticsState.satellites.find(function(item) {
+ return item.prn === gpsDiagnosticsState.selectedSatellitePrn;
+ });
+ if (selected) return selected;
+ gpsDiagnosticsState.selectedSatellitePrn = gpsDiagnosticsState.satellites[0].prn || null;
+ return gpsDiagnosticsState.satellites[0];
+ }
+
+ function hideGpsSatelliteTooltip() {
+ var node = document.getElementById('gps_satellite_tooltip');
+ if (!node) return;
+ node.hidden = true;
+ }
+
+ function renderGpsSatelliteTooltip(leftPx, topPx) {
+ var node = document.getElementById('gps_satellite_tooltip');
+ if (!node) return;
+ var selected = getSelectedSatellite();
+ if (!selected) {
+ node.hidden = true;
+ return;
+ }
+
+ node.innerHTML =
+ '
' + (selected.prn || '--') + '
' +
+ '
System ' + (selected.constellation || '--') + '
' +
+ '
Richtung ' + (typeof selected.azimuth === 'number' ? selected.azimuth + '°' : '--') + '
' +
+ '
Höhe ' + (typeof selected.elevation === 'number' ? selected.elevation + '°' : '--') + '
' +
+ '
Signal ' + (typeof selected.snr === 'number' ? selected.snr + ' dB' : '--') + '
' +
+ '
Qualität ' + (typeof selected.snr === 'number' ? (selected.snr >= 35 ? 'sehr gut' : (selected.snr >= 20 ? 'gut' : 'schwach')) : 'unbekannt') + '
';
+ node.style.left = leftPx + 'px';
+ node.style.top = topPx + 'px';
+ node.hidden = false;
+ }
+
+ function drawGpsSkyPlot() {
+ var canvas = document.getElementById('gps_sky_canvas');
+ if (!canvas || !canvas.getContext) return;
+
+ var ctx = canvas.getContext('2d');
+ var width = canvas.width;
+ var height = canvas.height;
+ var cx = width / 2;
+ var cy = height / 2;
+ var radius = Math.min(width, height) * 0.42;
+
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = '#07111c';
+ ctx.fillRect(0, 0, width, height);
+
+ ctx.strokeStyle = 'rgba(0,255,145,.18)';
+ ctx.lineWidth = 1;
+ [radius, radius * 0.66, radius * 0.33].forEach(function(ring) {
+ ctx.beginPath();
+ ctx.arc(cx, cy, ring, 0, Math.PI * 2);
+ ctx.stroke();
+ });
+
+ ctx.beginPath();
+ ctx.moveTo(cx, cy - radius);
+ ctx.lineTo(cx, cy + radius);
+ ctx.moveTo(cx - radius, cy);
+ ctx.lineTo(cx + radius, cy);
+ ctx.stroke();
+
+ ctx.fillStyle = 'rgba(190,255,225,.84)';
+ ctx.font = '16px Consolas';
+ ctx.textAlign = 'center';
+ ctx.fillText('N', cx, cy - radius - 12);
+ ctx.fillText('S', cx, cy + radius + 24);
+ ctx.fillText('W', cx - radius - 18, cy + 6);
+ ctx.fillText('E', cx + radius + 18, cy + 6);
+
+ ctx.fillStyle = 'rgba(150,255,210,.5)';
+ ctx.font = '12px Consolas';
+ ctx.fillText('30°', cx, cy - radius * 0.33 + 4);
+ ctx.fillText('60°', cx, cy - radius * 0.66 + 4);
+ ctx.fillText('90°', cx, cy + 4);
+
+ gpsDiagnosticsState.plottedSatellites = [];
+ gpsDiagnosticsState.satellites.forEach(function(item) {
+ if (typeof item.azimuth !== 'number' || typeof item.elevation !== 'number') return;
+ var satRadius = radius * (1 - Math.max(0, Math.min(90, item.elevation)) / 90);
+ var angle = (item.azimuth - 90) * Math.PI / 180.0;
+ var x = cx + Math.cos(angle) * satRadius;
+ var y = cy + Math.sin(angle) * satRadius;
+ var snr = typeof item.snr === 'number' ? item.snr : 0;
+ var isSelected = item.prn === gpsDiagnosticsState.selectedSatellitePrn;
+
+ ctx.beginPath();
+ ctx.fillStyle = snr >= 35 ? '#00ff91' : (snr >= 20 ? '#ffb300' : '#ff6b6b');
+ ctx.arc(x, y, isSelected ? 13 : 10, 0, Math.PI * 2);
+ ctx.fill();
+
+ ctx.strokeStyle = isSelected ? '#ffffff' : 'rgba(5,17,29,.9)';
+ ctx.lineWidth = isSelected ? 3 : 2;
+ ctx.stroke();
+
+ ctx.fillStyle = '#041017';
+ ctx.font = 'bold 10px Consolas';
+ ctx.textAlign = 'center';
+ ctx.fillText(String(item.prn || '?'), x, y + 3);
+
+ gpsDiagnosticsState.plottedSatellites.push({
+ prn: item.prn,
+ x: x,
+ y: y,
+ radius: isSelected ? 13 : 10
+ });
+ });
+ }
+
+ function onGpsSkyCanvasClick(event) {
+ var canvas = document.getElementById('gps_sky_canvas');
+ if (!canvas) return;
+ var rect = canvas.getBoundingClientRect();
+ var scaleX = canvas.width / rect.width;
+ var scaleY = canvas.height / rect.height;
+ var x = (event.clientX - rect.left) * scaleX;
+ var y = (event.clientY - rect.top) * scaleY;
+ var selectedPrn = null;
+
+ gpsDiagnosticsState.plottedSatellites.forEach(function(item) {
+ var dx = x - item.x;
+ var dy = y - item.y;
+ if ((dx * dx + dy * dy) <= (item.radius + 6) * (item.radius + 6)) {
+ selectedPrn = item.prn;
+ }
+ });
+
+ if (!selectedPrn) {
+ hideGpsSatelliteTooltip();
+ return;
+ }
+ gpsDiagnosticsState.selectedSatellitePrn = selectedPrn;
+ drawGpsSkyPlot();
+ var tooltipLeft = Math.min(rect.width - 18, (event.clientX - rect.left) + 18);
+ var tooltipTop = Math.max(18, (event.clientY - rect.top) - 18);
+ renderGpsSatelliteTooltip(tooltipLeft, tooltipTop);
+ }
+
+ function handleGpsDiagnostics(message) {
+ if (!message || !message.data) return;
+ try {
+ var data = JSON.parse(message.data);
+ gpsDiagnosticsState.connected = !!data.connected;
+ gpsDiagnosticsState.frameId = data.frame_id || 'gps';
+ gpsDiagnosticsState.valid = !!data.valid;
+ gpsDiagnosticsState.fixQuality = Number(data.fix_quality || 0);
+ gpsDiagnosticsState.satellitesUsed = Number(data.satellites_used || 0);
+ gpsDiagnosticsState.satellitesVisible = Number(data.satellites_visible || 0);
+ gpsDiagnosticsState.latitude = typeof data.latitude === 'number' ? data.latitude : null;
+ gpsDiagnosticsState.longitude = typeof data.longitude === 'number' ? data.longitude : null;
+ gpsDiagnosticsState.altitudeM = typeof data.altitude_m === 'number' ? data.altitude_m : null;
+ gpsDiagnosticsState.hdop = typeof data.hdop === 'number' ? data.hdop : null;
+ gpsDiagnosticsState.pdop = typeof data.pdop === 'number' ? data.pdop : null;
+ gpsDiagnosticsState.vdop = typeof data.vdop === 'number' ? data.vdop : null;
+ gpsDiagnosticsState.speedMps = typeof data.speed_mps === 'number' ? data.speed_mps : null;
+ gpsDiagnosticsState.speedKmh = typeof data.speed_kmh === 'number' ? data.speed_kmh : null;
+ gpsDiagnosticsState.trackDeg = typeof data.track_deg === 'number' ? data.track_deg : null;
+ gpsDiagnosticsState.utcDate = data.utc_date || null;
+ gpsDiagnosticsState.utcTime = data.utc_time || null;
+ gpsDiagnosticsState.lastFixAgeS = typeof data.last_fix_age_s === 'number' ? data.last_fix_age_s : null;
+ gpsDiagnosticsState.satellites = Array.isArray(data.satellites) ? data.satellites : [];
+ if (gpsDiagnosticsState.selectedSatellitePrn) {
+ var stillExists = gpsDiagnosticsState.satellites.some(function(item) {
+ return item.prn === gpsDiagnosticsState.selectedSatellitePrn;
+ });
+ if (!stillExists) gpsDiagnosticsState.selectedSatellitePrn = null;
+ }
+ updateGpsDiagnosticsPanel();
+ } catch (error) {}
+ }
+
function updateMapTelemetryPanel() {
setText('map_gps_status', roverMapState.gpsStatusText || 'Offline');
setText('map_gps_satellites', roverMapState.satellites);
@@ -1113,6 +1606,7 @@
}
function updateGpsStatusSummary(text) {
+ markAdminGpsFresh();
roverMapState.gpsStatusText = text || 'Offline';
var satMatch = String(roverMapState.gpsStatusText).match(/(\d+)\s*Sat/i);
roverMapState.satellites = satMatch ? satMatch[1] : '--';
@@ -1121,6 +1615,7 @@
function handleGpsFix(message) {
if (!message || !message.status) return;
+ markAdminGpsFresh();
roverMapState.hasFix = Number(message.status.status) >= 0;
if (!roverMapState.hasFix) {
@@ -1450,6 +1945,15 @@
}
function renderDriveEstimator() {
+ if (!pageConnection.rosOk) {
+ setText('drive_estimator_speed', '--');
+ setText('drive_estimator_distance', '--');
+ setText('drive_estimator_pulse', '--');
+ setText('drive_estimator_rpm', '--');
+ setText('drive_estimator_mode', '--');
+ setText('drive_estimator_direction', '--');
+ return;
+ }
updateDriveEstimatorModel();
setText('drive_estimator_speed', formatSpeedCms(driveEstimator.speedMps, 1));
setText('drive_estimator_distance', formatNumber(driveEstimator.distanceM, 2));
@@ -1572,9 +2076,11 @@
});
ros.on('connection', function() {
+ markAdminRosConnection(true);
setDriveEstimatorState('verbunden', 'state_active');
});
ros.on('close', function() {
+ markAdminRosConnection(false);
setDriveEstimatorState('getrennt', 'state_inactive');
ros = null;
roverCommandTopic = null;
@@ -1582,6 +2088,7 @@
window.setTimeout(connectDriveEstimatorRos, 3000);
});
ros.on('error', function() {
+ markAdminRosConnection(false);
setDriveEstimatorState('Fehler', 'state_unknown');
});
@@ -1621,6 +2128,13 @@
gpsStatusTopic.subscribe(function(message) {
updateGpsStatusSummary(message.data);
});
+
+ gpsDiagnosticsTopic = new ROSLIB.Topic({
+ ros: ros,
+ name: '/gps/diagnostics_json',
+ messageType: 'std_msgs/String'
+ });
+ gpsDiagnosticsTopic.subscribe(handleGpsDiagnostics);
}
function temperatureToWidth(value) {
@@ -1649,8 +2163,10 @@
try {
var response = await fetch(adminApiBase + '/api/status');
if (!response.ok) throw new Error('status ' + response.status);
+ markAdminApiConnection(true);
renderStatus(await response.json());
} catch (e) {
+ markAdminApiConnection(false);
setAdminStatus('API nicht erreichbar');
}
}
@@ -2116,18 +2632,23 @@
window.addEventListener('load', function() {
var input = document.getElementById('auth_input');
+ var gpsCanvas = document.getElementById('gps_sky_canvas');
input.focus();
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitPassword();
});
+ if (gpsCanvas) gpsCanvas.addEventListener('click', onGpsSkyCanvasClick);
if (window.location.hash === '#motor') {
switchAdminSection('motor');
} else if (window.location.hash === '#drive') {
switchAdminSection('drive');
+ } else if (window.location.hash === '#gps') {
+ switchAdminSection('gps');
} else if (window.location.hash === '#map') {
switchAdminSection('map');
}
updateClock();
+ updatePageConnectionState();
window.setInterval(updateClock, 1000);
fetchStatus();
fetchDelay();
@@ -2140,11 +2661,13 @@
renderWaypointList();
applyWaypointPlanningState();
renderDriveEstimator();
+ updateGpsDiagnosticsPanel();
window.setInterval(fetchStatus, 10000);
window.setInterval(fetchDelay, 5000);
window.setInterval(fetchSpeedLimit, 5000);
window.setInterval(fetchMotorTestStatus, 4000);
window.setInterval(renderDriveEstimator, 250);
+ window.setInterval(updatePageConnectionState, 1000);
});
window.addEventListener('beforeunload', function(event) {
diff --git a/ExoMy_Software-master/gui/app.css b/ExoMy_Software-master/gui/app.css
index 53639f4..41fd846 100644
--- a/ExoMy_Software-master/gui/app.css
+++ b/ExoMy_Software-master/gui/app.css
@@ -184,6 +184,15 @@ header {
}
#stat-panel { flex-shrink: 0; }
+#joy-panel.is-disabled {
+ opacity: .55;
+}
+#joy-panel.is-disabled .joy-base,
+#joy-panel.is-disabled .dpad-btn,
+#joy-panel.is-disabled .dbtn,
+#joy-panel.is-disabled .mtbtn {
+ cursor: not-allowed;
+}
.drive-btns { display: flex; gap: 8px; padding: 10px 12px; }
.dbtn {
@@ -298,6 +307,9 @@ header {
letter-spacing: calc(2px * var(--cam-scale));
z-index: 10;
}
+.c-rec.is-offline {
+ color: rgba(170,255,220,.72);
+}
.c-rec-dot {
width: calc(6px * var(--cam-scale));
height: calc(6px * var(--cam-scale));
@@ -305,6 +317,10 @@ header {
background: #FF3333;
animation: blink 1.1s step-end infinite;
}
+.c-rec.is-offline .c-rec-dot {
+ background: rgba(170,255,220,.72);
+ animation: none;
+}
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
.c-tl, .c-tr, .c-bl, .c-br {
position: absolute;
@@ -324,34 +340,59 @@ header {
left: 50%;
bottom: calc(13px * var(--cam-scale));
transform: translateX(-50%);
- display: flex;
- align-items: center;
- gap: calc(8px * var(--cam-scale));
z-index: 10;
pointer-events: none;
-}
-.c-bm-box {
- display: flex;
- align-items: baseline;
- gap: calc(5px * var(--cam-scale));
- padding: calc(6px * var(--cam-scale)) calc(10px * var(--cam-scale));
- background: rgba(3, 10, 14, .72);
- border: 1px solid rgba(0,255,145,.18);
- box-shadow: inset 0 0 12px rgba(0,255,145,.04), 0 0 16px rgba(0,0,0,.18);
- backdrop-filter: blur(2px);
-}
-.c-bm-lbl,
-.c-bm-unit {
- font-size: calc(8px * var(--cam-scale));
color: rgba(0,255,145,.72);
- letter-spacing: calc(1.2px * var(--cam-scale));
- text-transform: uppercase;
-}
-.c-bm-box strong {
- font-size: calc(14px * var(--cam-scale));
- line-height: 1;
- color: rgba(245,255,250,.98);
+ font-size: calc(9px * var(--cam-scale));
letter-spacing: calc(1px * var(--cam-scale));
+ line-height: 1.9;
+ text-align: center;
+ white-space: nowrap;
+}
+.connection-banner{
+ display:flex;
+ align-items:center;
+ gap:12px;
+ min-height:36px;
+ padding:8px 16px;
+ border-bottom:1px solid rgba(255,179,0,.2);
+ background:linear-gradient(90deg, rgba(255,179,0,.14) 0%, rgba(255,140,0,.06) 60%, transparent 100%);
+ color:rgba(255,235,190,.92);
+ font-size:11px;
+ letter-spacing:.4px;
+}
+.connection-banner strong{
+ font-size:10px;
+ letter-spacing:2px;
+ text-transform:uppercase;
+ color:#fff3cf;
+}
+body.page-connection-offline .connection-banner{
+ border-bottom-color:rgba(255,107,107,.3);
+ background:linear-gradient(90deg, rgba(255,107,107,.18) 0%, rgba(120,12,12,.12) 60%, transparent 100%);
+ color:rgba(255,220,220,.94);
+}
+body.page-connection-offline .mission-ok{
+ color:#ffb0b0;
+}
+body.page-connection-offline .pulse-dot{
+ background:#ff6b6b;
+ box-shadow:0 0 0 0 rgba(255,107,107,.5);
+}
+body.page-connection-degraded .mission-ok{
+ color:#ffd37a;
+}
+body.page-connection-degraded .pulse-dot{
+ background:var(--warn);
+ box-shadow:0 0 0 0 rgba(255,179,0,.5);
+}
+body.page-connection-offline .main,
+body.page-connection-offline .admin-shell{
+ opacity:.74;
+}
+body.page-connection-degraded .main,
+body.page-connection-degraded .admin-shell{
+ opacity:.9;
}
/* ── STAT PANEL ─────────────────────────────────────── */
.c-track {
@@ -1186,13 +1227,23 @@ body.page-admin .drive-calibration-button{
body.page-admin .admin-section-grid_map{
grid-template-columns:minmax(0, 1.7fr) minmax(280px, .85fr);
}
+body.page-admin .admin-section-grid_gps{
+ grid-template-columns:minmax(0, 1.45fr) minmax(300px, .95fr);
+}
body.page-admin .admin-col_map_main,
body.page-admin .admin-col_map_side{ min-width:0; }
+body.page-admin .admin-col_gps_main,
+body.page-admin .admin-col_gps_side{ min-width:0; }
body.page-admin .map-panel{
display:flex;
flex-direction:column;
min-height:620px;
}
+body.page-admin .gps-panel{
+ display:flex;
+ flex-direction:column;
+ min-height:620px;
+}
body.page-admin .map-toolbar{
display:flex;
gap:8px;
@@ -1230,6 +1281,78 @@ body.page-admin .map-route-storage-input{
body.page-admin .map-route-storage-input:focus{
border-color:rgba(0,255,145,.5);
}
+body.page-admin .gps-copy{
+ font-size:10px;
+ color:rgba(205,255,235,.86);
+ letter-spacing:.4px;
+ line-height:1.6;
+ padding:10px 12px;
+ border-bottom:1px solid rgba(0,255,145,.05);
+}
+body.page-admin .gps-copy_compact{
+ border-bottom:none;
+}
+body.page-admin .gps-sky-stage{
+ padding:18px;
+ position:relative;
+}
+body.page-admin .gps-sky-canvas{
+ display:block;
+ width:100%;
+ max-width:560px;
+ aspect-ratio:1 / 1;
+ margin:0 auto;
+ border:1px solid rgba(0,255,145,.12);
+ background:radial-gradient(circle at 50% 50%, rgba(0,255,145,.04) 0%, rgba(4,16,26,.96) 70%);
+ cursor:pointer;
+}
+body.page-admin .gps-satellite-tooltip{
+ position:absolute;
+ z-index:20;
+ min-width:180px;
+ max-width:220px;
+ padding:10px 12px;
+ border:1px solid rgba(0,255,145,.18);
+ background:rgba(4,16,26,.96);
+ box-shadow:0 10px 28px rgba(0,0,0,.34), inset 0 0 14px rgba(0,255,145,.04);
+ transform:translate(-50%, -100%);
+ pointer-events:none;
+}
+body.page-admin .gps-satellite-tooltip-title{
+ margin-bottom:8px;
+ color:rgba(245,255,250,.98);
+ font-size:11px;
+ font-weight:700;
+ letter-spacing:.5px;
+}
+body.page-admin .gps-satellite-tooltip-row{
+ display:flex;
+ align-items:baseline;
+ justify-content:space-between;
+ gap:10px;
+}
+body.page-admin .gps-satellite-tooltip-row + .gps-satellite-tooltip-row{
+ margin-top:5px;
+}
+body.page-admin .gps-satellite-tooltip-row span{
+ color:rgba(170,255,220,.72);
+ font-size:8px;
+ letter-spacing:1px;
+ text-transform:uppercase;
+}
+body.page-admin .gps-satellite-tooltip-row strong{
+ color:rgba(245,255,250,.98);
+ font-size:10px;
+ letter-spacing:.4px;
+}
+body.page-admin .gps-satellite-empty{
+ padding:14px 10px;
+ border:1px solid rgba(0,255,145,.08);
+ background:rgba(0,255,145,.025);
+ color:rgba(175,255,220,.7);
+ font-size:10px;
+ letter-spacing:.4px;
+}
body.page-admin .map-stage{
position:relative;
flex:1;
@@ -1402,7 +1525,9 @@ body.page-admin .leaflet-popup-tip{
body.page-admin .drive-calibration-toolbar{ flex-direction:column; align-items:stretch; }
body.page-admin .drive-calibration-grid{ grid-template-columns:1fr; }
body.page-admin .admin-section-grid_map{ grid-template-columns:1fr; }
+ body.page-admin .admin-section-grid_gps{ grid-template-columns:1fr; }
body.page-admin .map-panel{ min-height:0; }
+ body.page-admin .gps-panel{ min-height:0; }
body.page-admin .map-stage{ min-height:360px; }
body.page-admin .map-route-storage,
body.page-admin .map-route-storage_secondary{ grid-template-columns:1fr; }
diff --git a/ExoMy_Software-master/gui/index.html b/ExoMy_Software-master/gui/index.html
index 8d4c542..e901f46 100644
--- a/ExoMy_Software-master/gui/index.html
+++ b/ExoMy_Software-master/gui/index.html
@@ -9,17 +9,26 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
EXOMY GROUND CTRL
@@ -28,7 +37,7 @@
Admin
-
+
@@ -54,7 +63,7 @@
-
+
CAM: Verbinde…
MODE: Ackermann
@@ -74,16 +83,8 @@
STREAM: 8083
-
- V
- 0,0
- cm/s
-
-
- S
- 0,00
- m
-
+ V:
0,0 cm/s
+ S:
0,00 m
@@ -310,6 +311,13 @@ var trajectoryOverlay = {
steering: 90,
velocity: 0
};
+var pageConnection = {
+ apiOk: false,
+ apiLastAt: 0,
+ rosOk: false,
+ gpsLastAt: 0,
+ state: "offline"
+};
function setText(id, text) {
var el = document.getElementById(id);
@@ -445,6 +453,11 @@ function updateDriveEstimatorModel() {
}
function renderDriveEstimatorOverlay() {
+ if (!pageConnection.rosOk) {
+ setText("overlay-speed", "--");
+ setText("overlay-distance", "--");
+ return;
+ }
updateDriveEstimatorModel();
setText("overlay-speed", formatSpeedCms(driveEstimator.speedMps, 1));
setText("overlay-distance", formatNumber(driveEstimator.distanceM, 2));
@@ -592,6 +605,7 @@ function updateAxesDisplay(x, y) {
function updateGpsOverlayFromFix(message) {
if (!message) return;
+ markGpsFresh();
var hasFix = message.status && message.status.status >= 0;
if (!hasFix) {
@@ -606,6 +620,7 @@ function updateGpsOverlayFromFix(message) {
function updateGpsOverlayFromStatus(text) {
var value = String(text || "Offline");
+ markGpsFresh();
setText("gps-state", value);
var satMatch = value.match(/(\d+)\s*Sat/i);
@@ -675,7 +690,96 @@ function updateInputDisplay() {
function setRosStatus(text) {
setText("status", text);
setText("ros-state", text);
- setText("mission-state", text === "Verbunden" ? "Nominal" : text);
+}
+
+function controlsAreBlocked() {
+ return pageConnection.state === "offline";
+}
+
+function updateControlAvailability() {
+ var panel = document.getElementById("joy-panel");
+ if (panel) {
+ panel.classList.toggle("is-disabled", controlsAreBlocked());
+ }
+}
+
+function clearSystemStatusDisplay() {
+ setText("stat-wifi", "--");
+ setText("stat-ips", "--");
+ updateUndervoltageDisplay("--");
+ setText("stat-uptime", "--");
+ setText("stat-disk", "--");
+ setText("val-cpu", "--");
+ setText("val-temp", "--");
+ setText("val-mem", "--");
+ document.getElementById("bar-cpu").style.width = "0%";
+ document.getElementById("bar-mem").style.width = "0%";
+ document.getElementById("bar-temp").style.width = "0%";
+}
+
+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-sats", "--");
+ setText("gps-lat", "--");
+ setText("gps-lon", "--");
+ setText("overlay-speed", "--");
+ setText("overlay-distance", "--");
+}
+
+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 state = "offline";
+ var cunoTitle = "Cuno offline";
+
+ if (apiFresh && rosFresh) {
+ state = gpsFresh || !pageConnection.gpsLastAt ? "online" : "degraded";
+ } else if (apiFresh || rosFresh) {
+ state = "degraded";
+ }
+
+ cunoTitle = state === "online" ? "Cuno online" : "Cuno offline";
+ pageConnection.state = state;
+ document.body.classList.toggle("page-connection-online", state === "online");
+ document.body.classList.toggle("page-connection-degraded", state === "degraded");
+ document.body.classList.toggle("page-connection-offline", state === "offline");
+ setText("cuno-connection-status", cunoTitle);
+ updateControlAvailability();
+
+ if (!apiFresh) {
+ clearSystemStatusDisplay();
+ }
+ if (!rosFresh) {
+ clearRosLiveDisplay();
+ }
+}
+
+function markApiConnection(ok) {
+ pageConnection.apiOk = !!ok;
+ if (ok) {
+ pageConnection.apiLastAt = Date.now();
+ }
+ setPageConnectionState();
+}
+
+function markRosConnection(ok) {
+ pageConnection.rosOk = !!ok;
+ setPageConnectionState();
+}
+
+function markGpsFresh() {
+ pageConnection.gpsLastAt = Date.now();
+ setPageConnectionState();
}
function updateUndervoltageDisplay(text, state) {
@@ -727,14 +831,16 @@ function fetchSystemStatus() {
}
return response.json();
}).then(function (payload) {
+ markApiConnection(true);
updateServiceState(payload);
}).catch(function () {
+ markApiConnection(false);
setText("stat-wifi", "nicht erreichbar");
});
}
function publishJoy() {
- if (!joyListener) {
+ if (!joyListener || controlsAreBlocked()) {
return;
}
joyListener.publish(new ROSLIB.Message({
@@ -772,6 +878,9 @@ function resetAxes() {
}
function pulseButton(buttonIndex) {
+ if (controlsAreBlocked()) {
+ return;
+ }
axes = [0, 0, 0, 0, 0, 0];
updateAxesDisplay(0, 0);
buttons[buttonIndex] = 1;
@@ -783,11 +892,17 @@ function pulseButton(buttonIndex) {
}
function selectDriveMode(mode, buttonIndex) {
+ if (controlsAreBlocked()) {
+ return;
+ }
applyDriveMode(mode);
pulseButton(buttonIndex);
}
function toggleMotors() {
+ if (controlsAreBlocked()) {
+ return;
+ }
motorsEnabled = !motorsEnabled;
updateMotorDisplay();
pulseButton(9);
@@ -831,6 +946,9 @@ function joyMetrics() {
}
function moveThumb(clientX, clientY) {
+ if (controlsAreBlocked()) {
+ return;
+ }
var data = joyMetrics();
var dx = clientX - data.cx;
var dy = clientY - data.cy;
@@ -865,6 +983,9 @@ function resetThumb() {
}
function setDpadPressed(button, x, y) {
+ if (controlsAreBlocked()) {
+ return;
+ }
document.querySelectorAll(".dpad-btn").forEach(function (btn) {
btn.classList.remove("pressed");
});
@@ -959,10 +1080,12 @@ window.addEventListener("load", function () {
updateClocks();
window.setInterval(updateClocks, 1000);
fetchSystemStatus();
+ setPageConnectionState();
fetchDriveEstimatorData();
renderDriveEstimatorOverlay();
renderTrajectoryOverlay();
statusTimer = window.setInterval(fetchSystemStatus, 5000);
+ window.setInterval(setPageConnectionState, 1000);
window.setInterval(renderDriveEstimatorOverlay, 250);
window.setInterval(renderTrajectoryOverlay, 250);
@@ -970,10 +1093,21 @@ window.addEventListener("load", function () {
video.src = "//" + hostUrl + ":8083/stream.mjpg";
video.addEventListener("load", function () {
setText("cam-state", "Online");
+ setText("cam-host", hostUrl);
+ setText("cam-live-text", "LIVE");
+ var liveIndicator = document.getElementById("cam-live-indicator");
+ if (liveIndicator) {
+ liveIndicator.classList.remove("is-offline");
+ }
updateCameraFrameLayout();
});
video.addEventListener("error", function () {
setText("cam-state", "Fehler");
+ setText("cam-live-text", "OFFLINE");
+ var liveIndicator = document.getElementById("cam-live-indicator");
+ if (liveIndicator) {
+ liveIndicator.classList.add("is-offline");
+ }
});
updateCameraFrameLayout();
window.addEventListener("resize", updateCameraFrameLayout);
@@ -1006,14 +1140,17 @@ window.addEventListener("load", function () {
});
ros.on("connection", function () {
+ markRosConnection(true);
setRosStatus("Verbunden");
});
ros.on("error", function () {
+ markRosConnection(false);
setRosStatus("Fehler");
});
ros.on("close", function () {
+ markRosConnection(false);
setRosStatus("Getrennt");
});
@@ -1074,6 +1211,7 @@ window.addEventListener("load", function () {
messageType: "sensor_msgs/Joy"
});
joySubscriber.subscribe(function (message) {
+ markRosConnection(true);
if (message.header.frame_id === "webgui") return;
var x = (message.axes[0] || 0);
var y = -(message.axes[1] || 0);
@@ -1089,6 +1227,7 @@ window.addEventListener("load", function () {
document.getElementById("motor-toggle").addEventListener("click", toggleMotors);
document.getElementById("mt-joy").addEventListener("click", function () {
+ if (controlsAreBlocked()) return;
inputMode = "joystick";
updateInputDisplay();
document.getElementById("mt-joy").classList.add("active");
@@ -1100,6 +1239,7 @@ window.addEventListener("load", function () {
});
document.getElementById("mt-dpad").addEventListener("click", function () {
+ if (controlsAreBlocked()) return;
inputMode = "dpad";
updateInputDisplay();
document.getElementById("mt-dpad").classList.add("active");
@@ -1112,6 +1252,7 @@ window.addEventListener("load", function () {
var joyBase = document.getElementById("jbase");
joyBase.addEventListener("mousedown", function (event) {
+ if (controlsAreBlocked()) return;
dragging = true;
moveThumb(event.clientX, event.clientY);
event.preventDefault();
@@ -1128,6 +1269,7 @@ window.addEventListener("load", function () {
}
});
joyBase.addEventListener("touchstart", function (event) {
+ if (controlsAreBlocked()) return;
dragging = true;
moveThumb(event.touches[0].clientX, event.touches[0].clientY);
event.preventDefault();
@@ -1147,6 +1289,7 @@ window.addEventListener("load", function () {
document.querySelectorAll(".dpad-btn").forEach(function (button) {
function activate(event) {
+ if (controlsAreBlocked()) return;
var x = parseFloat(button.dataset.x);
var y = parseFloat(button.dataset.y);
setDpadPressed(button, x, y);
diff --git a/ExoMy_Software-master/src/gps_node.py b/ExoMy_Software-master/src/gps_node.py
index d534a87..7d91624 100644
--- a/ExoMy_Software-master/src/gps_node.py
+++ b/ExoMy_Software-master/src/gps_node.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python
import errno
+import json
import os
import select
import termios
@@ -76,9 +77,15 @@ class GpsState(object):
self.track_deg = None
self.fix_quality = 0
self.satellites = 0
+ self.pdop = None
+ self.vdop = None
self.valid = False
self.last_sentence_time = None
self.last_fix_time = None
+ self.utc_datetime = None
+ self.visible_satellites = {}
+ self.visible_satellite_count = 0
+ self._gsv_groups = {}
def summary_text(self):
if not self.connected:
@@ -200,15 +207,129 @@ def parse_rmc(fields, state):
state.valid = True
state.last_fix_time = rospy.Time.now()
- # Optional date/time parse for debugging consistency.
if fields[1] and fields[9]:
try:
- datetime.strptime(fields[9] + fields[1].split('.')[0], '%d%m%y%H%M%S')
+ state.utc_datetime = datetime.strptime(fields[9] + fields[1].split('.')[0], '%d%m%y%H%M%S')
except ValueError:
pass
return True
+def parse_gsa(fields, state):
+ if len(fields) < 18:
+ return False
+
+ try:
+ state.pdop = float(fields[15]) if fields[15] else None
+ except ValueError:
+ state.pdop = None
+
+ try:
+ state.hdop = float(fields[16]) if fields[16] else state.hdop
+ except ValueError:
+ pass
+
+ try:
+ state.vdop = float(fields[17]) if fields[17] else None
+ except ValueError:
+ state.vdop = None
+ return True
+
+
+def merge_visible_satellites(state):
+ now = rospy.Time.now()
+ merged = {}
+
+ stale_talkers = []
+ for talker, group in state._gsv_groups.items():
+ completed_at = group.get('completed_at')
+ if completed_at is None:
+ continue
+ try:
+ if (now - completed_at).to_sec() > 8.0:
+ stale_talkers.append(talker)
+ continue
+ except rospy.ROSException:
+ continue
+
+ constellation = group.get('constellation', talker)
+ for sat_key, satellite in group.get('satellites', {}).items():
+ merged[sat_key] = dict(satellite, constellation=constellation)
+
+ for talker in stale_talkers:
+ state._gsv_groups.pop(talker, None)
+
+ state.visible_satellites = dict(sorted(merged.items()))
+ state.visible_satellite_count = len(state.visible_satellites)
+
+
+def parse_gsv(fields, state, sentence_type):
+ if len(fields) < 4:
+ return False
+
+ try:
+ total_sentences = int(fields[1] or 0)
+ sentence_index = int(fields[2] or 0)
+ total_satellites = int(fields[3] or 0)
+ except ValueError:
+ return False
+
+ if sentence_index <= 0:
+ return False
+
+ talker = sentence_type[1:3] if len(sentence_type) >= 3 else 'GN'
+ group = state._gsv_groups.get(talker)
+ if group is None:
+ group = {
+ 'constellation': talker,
+ 'total_sentences': 0,
+ 'working_set': {},
+ 'satellites': {},
+ 'completed_at': None,
+ 'total_satellites_reported': 0,
+ }
+ state._gsv_groups[talker] = group
+
+ if sentence_index == 1 or total_sentences != group.get('total_sentences'):
+ group['working_set'] = {}
+
+ group['total_sentences'] = total_sentences
+ group['total_satellites_reported'] = total_satellites
+
+ for index in range(4, len(fields), 4):
+ if index + 3 >= len(fields):
+ break
+ prn = fields[index].strip()
+ if not prn:
+ continue
+ try:
+ elevation = int(fields[index + 1]) if fields[index + 1] else None
+ except ValueError:
+ elevation = None
+ try:
+ azimuth = int(fields[index + 2]) if fields[index + 2] else None
+ except ValueError:
+ azimuth = None
+ try:
+ snr = int(fields[index + 3]) if fields[index + 3] else None
+ except ValueError:
+ snr = None
+
+ satellite_key = '{0}:{1}'.format(talker, prn)
+ group['working_set'][satellite_key] = {
+ 'prn': '{0}-{1}'.format(talker, prn),
+ 'elevation': elevation,
+ 'azimuth': azimuth,
+ 'snr': snr
+ }
+
+ if sentence_index >= total_sentences:
+ group['satellites'] = dict(group['working_set'])
+ group['completed_at'] = rospy.Time.now()
+ merge_visible_satellites(state)
+ return True
+
+
def parse_sentence(sentence, state):
if not verify_checksum(sentence):
return False
@@ -224,10 +345,45 @@ def parse_sentence(sentence, state):
return parse_gga(fields, state)
if sentence_type.endswith('RMC'):
return parse_rmc(fields, state)
+ if sentence_type.endswith('GSA'):
+ return parse_gsa(fields, state)
+ if sentence_type.endswith('GSV'):
+ return parse_gsv(fields, state, sentence_type)
return False
-def publish_fix(fix_publisher, overlay_publisher, state, frame_id):
+def build_diagnostics_payload(state, frame_id):
+ last_fix_age = None
+ if state.last_fix_time is not None:
+ try:
+ last_fix_age = max(0.0, (rospy.Time.now() - state.last_fix_time).to_sec())
+ except rospy.ROSException:
+ last_fix_age = None
+
+ return {
+ 'connected': state.connected,
+ 'frame_id': frame_id,
+ 'valid': state.valid,
+ 'fix_quality': state.fix_quality,
+ 'satellites_used': max(0, int(state.satellites or 0)),
+ 'satellites_visible': max(0, int(state.visible_satellite_count or 0)),
+ 'latitude': state.latitude,
+ 'longitude': state.longitude,
+ 'altitude_m': state.altitude,
+ 'hdop': state.hdop,
+ 'pdop': state.pdop,
+ 'vdop': state.vdop,
+ 'speed_mps': state.speed_mps,
+ 'speed_kmh': (state.speed_mps or 0.0) * 3.6 if state.speed_mps is not None else None,
+ 'track_deg': state.track_deg,
+ 'utc_time': state.utc_datetime.strftime('%H:%M:%S') if state.utc_datetime else None,
+ 'utc_date': state.utc_datetime.strftime('%Y-%m-%d') if state.utc_datetime else None,
+ 'last_fix_age_s': last_fix_age,
+ 'satellites': list(state.visible_satellites.values())
+ }
+
+
+def publish_fix(fix_publisher, overlay_publisher, diagnostics_publisher, state, frame_id):
message = NavSatFix()
message.header.stamp = rospy.Time.now()
message.header.frame_id = frame_id
@@ -258,6 +414,7 @@ def publish_fix(fix_publisher, overlay_publisher, state, frame_id):
fix_publisher.publish(message)
overlay_publisher.publish(String(data=state.summary_text()))
+ diagnostics_publisher.publish(String(data=json.dumps(build_diagnostics_payload(state, frame_id), sort_keys=True)))
if __name__ == '__main__':
@@ -270,6 +427,7 @@ if __name__ == '__main__':
fix_publisher = rospy.Publisher('/fix', NavSatFix, queue_size=5)
overlay_publisher = rospy.Publisher('/gps/status_text', String, queue_size=5)
+ diagnostics_publisher = rospy.Publisher('/gps/diagnostics_json', String, queue_size=5)
state = GpsState()
serial_fd = None
@@ -280,14 +438,14 @@ if __name__ == '__main__':
serial_fd = open_device(port, baud)
buffer = bytearray()
state.connected = serial_fd is not None
- publish_fix(fix_publisher, overlay_publisher, state, frame_id)
+ publish_fix(fix_publisher, overlay_publisher, diagnostics_publisher, state, frame_id)
if serial_fd is None:
break
try:
readable, _, _ = select.select([serial_fd], [], [], 0.5)
if serial_fd not in readable:
- publish_fix(fix_publisher, overlay_publisher, state, frame_id)
+ publish_fix(fix_publisher, overlay_publisher, diagnostics_publisher, state, frame_id)
continue
chunk = os.read(serial_fd, READ_SIZE)
@@ -302,7 +460,7 @@ if __name__ == '__main__':
if not line or not line.startswith('$'):
continue
if parse_sentence(line, state):
- publish_fix(fix_publisher, overlay_publisher, state, frame_id)
+ publish_fix(fix_publisher, overlay_publisher, diagnostics_publisher, state, frame_id)
except OSError as exc:
if exc.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):